mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
7a7e205cc8
Address remaining review feedback: 1) Add HistoryTokens field to ContextUsage/ContextStats, showing history-only token count in /context and frontend UI alongside SummarizeAtTokens so users can see the actual summarization trigger comparison. 2) Remove .codebuddy/github-contribute/ state files accidentally included in the PR.
252 lines
6.7 KiB
TypeScript
252 lines
6.7 KiB
TypeScript
import { toast } from "sonner"
|
|
|
|
import {
|
|
parseAssistantMessageCreateState,
|
|
parseAssistantMessageUpdateState,
|
|
} from "@/features/chat/assistant-message-state"
|
|
import { normalizeUnixTimestamp } from "@/features/chat/state"
|
|
import {
|
|
type ChatAttachment,
|
|
type ContextUsage,
|
|
updateChatStore,
|
|
} from "@/store/chat"
|
|
|
|
export interface PicoMessage {
|
|
type: string
|
|
id?: string
|
|
session_id?: string
|
|
timestamp?: number | string
|
|
payload?: Record<string, unknown>
|
|
}
|
|
|
|
function parseAttachments(
|
|
payload: Record<string, unknown>,
|
|
): ChatAttachment[] | undefined {
|
|
const raw = payload.attachments
|
|
if (!Array.isArray(raw)) {
|
|
return undefined
|
|
}
|
|
|
|
const attachments: ChatAttachment[] = []
|
|
for (const item of raw) {
|
|
if (!item || typeof item !== "object") {
|
|
continue
|
|
}
|
|
|
|
const attachment = item as Record<string, unknown>
|
|
const url = typeof attachment.url === "string" ? attachment.url : ""
|
|
if (!url) {
|
|
continue
|
|
}
|
|
|
|
const type =
|
|
attachment.type === "audio" ||
|
|
attachment.type === "video" ||
|
|
attachment.type === "file" ||
|
|
attachment.type === "image"
|
|
? attachment.type
|
|
: "file"
|
|
|
|
const filename =
|
|
typeof attachment.filename === "string" ? attachment.filename : undefined
|
|
const contentType =
|
|
typeof attachment.content_type === "string"
|
|
? attachment.content_type
|
|
: undefined
|
|
|
|
attachments.push({
|
|
type,
|
|
url,
|
|
...(filename ? { filename } : {}),
|
|
...(contentType ? { contentType } : {}),
|
|
})
|
|
}
|
|
|
|
return attachments.length > 0 ? attachments : undefined
|
|
}
|
|
|
|
function parseContextUsage(
|
|
payload: Record<string, unknown>,
|
|
): ContextUsage | undefined {
|
|
const raw = payload.context_usage
|
|
if (!raw || typeof raw !== "object") return undefined
|
|
const obj = raw as Record<string, unknown>
|
|
const used = Number(obj.used_tokens)
|
|
const total = Number(obj.total_tokens)
|
|
if (!Number.isFinite(used) || !Number.isFinite(total) || total <= 0)
|
|
return undefined
|
|
return {
|
|
used_tokens: used,
|
|
total_tokens: total,
|
|
history_tokens: obj.history_tokens != null ? Number(obj.history_tokens) : undefined,
|
|
compress_at_tokens: Number(obj.compress_at_tokens) || 0,
|
|
summarize_at_tokens: obj.summarize_at_tokens != null ? Number(obj.summarize_at_tokens) : undefined,
|
|
used_percent: Number(obj.used_percent) || 0,
|
|
}
|
|
}
|
|
|
|
function parseModelName(payload: Record<string, unknown>): string | undefined {
|
|
if (typeof payload.model_name !== "string") {
|
|
return undefined
|
|
}
|
|
const modelName = payload.model_name.trim()
|
|
return modelName || undefined
|
|
}
|
|
|
|
export function handlePicoMessage(
|
|
message: PicoMessage,
|
|
expectedSessionId: string,
|
|
) {
|
|
if (message.session_id && message.session_id !== expectedSessionId) {
|
|
return
|
|
}
|
|
|
|
const payload = message.payload || {}
|
|
|
|
switch (message.type) {
|
|
case "message.create":
|
|
case "media.create": {
|
|
const messageId = (payload.message_id as string) || `pico-${Date.now()}`
|
|
const { content, kind, toolCalls } =
|
|
parseAssistantMessageCreateState(payload)
|
|
const attachments = parseAttachments(payload)
|
|
const contextUsage = parseContextUsage(payload)
|
|
const isPlaceholder = payload.placeholder === true
|
|
const modelName = parseModelName(payload)
|
|
const timestamp =
|
|
message.timestamp !== undefined &&
|
|
Number.isFinite(Number(message.timestamp))
|
|
? normalizeUnixTimestamp(Number(message.timestamp))
|
|
: Date.now()
|
|
|
|
updateChatStore((prev) => ({
|
|
messages: [
|
|
...prev.messages,
|
|
{
|
|
id: messageId,
|
|
role: "assistant",
|
|
content,
|
|
kind,
|
|
...(modelName ? { modelName } : {}),
|
|
...(toolCalls ? { toolCalls } : {}),
|
|
attachments,
|
|
timestamp,
|
|
},
|
|
],
|
|
isTyping:
|
|
!isPlaceholder &&
|
|
(kind === "normal" || message.type === "media.create")
|
|
? false
|
|
: prev.isTyping,
|
|
...(contextUsage ? { contextUsage } : {}),
|
|
}))
|
|
break
|
|
}
|
|
|
|
case "message.update": {
|
|
const messageId = payload.message_id as string
|
|
const attachments = parseAttachments(payload)
|
|
const contextUsage = parseContextUsage(payload)
|
|
const modelName = parseModelName(payload)
|
|
const timestamp =
|
|
message.timestamp !== undefined &&
|
|
Number.isFinite(Number(message.timestamp))
|
|
? normalizeUnixTimestamp(Number(message.timestamp))
|
|
: Date.now()
|
|
if (!messageId) {
|
|
break
|
|
}
|
|
|
|
updateChatStore((prev) => ({
|
|
messages: (() => {
|
|
let found = false
|
|
const messages = prev.messages.map((msg) => {
|
|
if (msg.id !== messageId) {
|
|
return msg
|
|
}
|
|
found = true
|
|
const { content, kind, toolCalls } =
|
|
parseAssistantMessageUpdateState(payload, msg)
|
|
return {
|
|
...msg,
|
|
id: messageId,
|
|
content,
|
|
kind,
|
|
toolCalls,
|
|
...(modelName ? { modelName } : {}),
|
|
...(attachments ? { attachments } : {}),
|
|
}
|
|
})
|
|
if (found) {
|
|
return messages
|
|
}
|
|
|
|
const { content, kind, toolCalls } =
|
|
parseAssistantMessageUpdateState(payload)
|
|
|
|
return [
|
|
...messages,
|
|
{
|
|
id: messageId,
|
|
role: "assistant" as const,
|
|
content,
|
|
kind,
|
|
toolCalls,
|
|
...(modelName ? { modelName } : {}),
|
|
...(attachments ? { attachments } : {}),
|
|
timestamp,
|
|
},
|
|
]
|
|
})(),
|
|
...(contextUsage ? { contextUsage } : {}),
|
|
}))
|
|
break
|
|
}
|
|
|
|
case "message.delete": {
|
|
const messageId = payload.message_id as string
|
|
if (!messageId) {
|
|
break
|
|
}
|
|
|
|
updateChatStore((prev) => ({
|
|
messages: prev.messages.filter((msg) => msg.id !== messageId),
|
|
}))
|
|
break
|
|
}
|
|
|
|
case "typing.start":
|
|
updateChatStore({ isTyping: true })
|
|
break
|
|
|
|
case "typing.stop":
|
|
updateChatStore({ isTyping: false })
|
|
break
|
|
|
|
case "error": {
|
|
const requestId =
|
|
typeof payload.request_id === "string" ? payload.request_id : ""
|
|
const errorMessage =
|
|
typeof payload.message === "string" ? payload.message : ""
|
|
|
|
console.error("Pico error:", payload)
|
|
if (errorMessage) {
|
|
toast.error(errorMessage)
|
|
}
|
|
updateChatStore((prev) => ({
|
|
messages: requestId
|
|
? prev.messages.filter((msg) => msg.id !== requestId)
|
|
: prev.messages,
|
|
isTyping: false,
|
|
}))
|
|
break
|
|
}
|
|
|
|
case "pong":
|
|
break
|
|
|
|
default:
|
|
console.log("Unknown pico message type:", message.type)
|
|
}
|
|
}
|