import { type PermissionReply } from "../runtime/opencode.js"; import { type ActivityPayload, initialActivity, type PermissionRequestPayload, type QuestionRequestPayload, type TodoUpdatePayload, } from "./chatStream.js"; export type RunStatus = "running" | "completed" | "error" | "aborted"; export type StreamSubscriber = { write: (event: string, data: Record) => void; close: () => void; }; export type ActiveRun = { clientSessionId: string; controller: AbortController; messages: unknown[]; pendingPermissions: Map; pendingQuestions: Map; status: RunStatus; subscribers: Set; }; type ToolArtifactKind = "chart" | "map" | "panel" | "tool"; type ToolCallPayload = { session_id?: string; tool?: string; params?: unknown; /** Legacy payload field. New tool calls inherit purpose from their Activity. */ reason?: string; }; export const isObjectRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); const createFrontendMessageId = () => `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; export const createInitialStreamingMessages = ( existingMessages: unknown[], userContent: string, ) => { const userMessage = { id: createFrontendMessageId(), role: "user", content: userContent, }; return [ ...existingMessages, { ...userMessage, branchRootId: userMessage.id, }, { id: createFrontendMessageId(), role: "assistant", content: "", activities: [ { id: initialActivity.id, status: "running", title: initialActivity.title, reason: initialActivity.reason, actions: [], startedAt: Date.now(), elapsedMs: 0, elapsedSnapshotAt: Date.now(), }, ], }, ]; }; const toFrontendActivityAction = ( action: ActivityPayload["actions"][number], ) => ({ id: action.id, tool: action.tool, title: action.title, status: action.status, target: action.target, error: action.error, startedAt: action.started_at, endedAt: action.ended_at, elapsedMs: action.elapsed_ms, elapsedSnapshotAt: action.elapsed_ms === undefined ? undefined : Date.now(), durationMs: action.duration_ms, }); const toFrontendActivity = (activity: ActivityPayload) => ({ id: activity.id, title: activity.title, reason: activity.reason, status: activity.status, actions: activity.actions.map(toFrontendActivityAction), startedAt: activity.started_at, endedAt: activity.ended_at, elapsedMs: activity.elapsed_ms, elapsedSnapshotAt: activity.elapsed_ms === undefined ? undefined : Date.now(), durationMs: activity.duration_ms, }); export const upsertBackendActivity = ( activities: unknown, activity: ActivityPayload, ) => { const next = Array.isArray(activities) ? [...activities] : []; const index = next.findIndex( (item) => isObjectRecord(item) && item.id === activity.id, ); const nextItem = toFrontendActivity(activity); if (index >= 0) next[index] = nextItem; else next.push(nextItem); return next; }; export const completeBackendActivities = ( activities: unknown, status: "completed" | "error" | "cancelled" = "completed", ) => Array.isArray(activities) ? activities.map((activity) => { if (!isObjectRecord(activity) || activity.status !== "running") return activity; const endedAt = Date.now(); const startedAt = typeof activity.startedAt === "number" ? activity.startedAt : endedAt; const actions = Array.isArray(activity.actions) ? activity.actions.map((action) => { if (!isObjectRecord(action) || action.status !== "running") return action; const actionStartedAt = typeof action.startedAt === "number" ? action.startedAt : endedAt; return { ...action, status: status === "error" ? "error" : "completed", endedAt, elapsedMs: undefined, elapsedSnapshotAt: undefined, durationMs: Math.max(0, endedAt - actionStartedAt), ...(status === "error" ? { error: action.error ?? "活动执行失败" } : {}), }; }) : activity.actions; return { ...activity, status, actions, endedAt, elapsedMs: undefined, elapsedSnapshotAt: undefined, durationMs: Math.max(0, endedAt - startedAt), }; }) : activities; export const upsertBackendProgress = ( progress: unknown, payload: Record, ) => { const next = Array.isArray(progress) ? [...progress] : []; const id = typeof payload.id === "string" ? payload.id : `progress-${Date.now()}`; const index = next.findIndex((item) => isObjectRecord(item) && item.id === id); const nextItem = { id, phase: typeof payload.phase === "string" ? payload.phase : "progress", status: payload.status === "completed" || payload.status === "error" ? payload.status : "running", title: typeof payload.title === "string" ? payload.title : "正在处理", detail: typeof payload.detail === "string" ? payload.detail : undefined, startedAt: typeof payload.started_at === "number" ? payload.started_at : undefined, endedAt: typeof payload.ended_at === "number" ? payload.ended_at : undefined, elapsedMs: typeof payload.elapsed_ms === "number" ? payload.elapsed_ms : undefined, elapsedSnapshotAt: typeof payload.elapsed_ms === "number" ? Date.now() : undefined, durationMs: typeof payload.duration_ms === "number" ? payload.duration_ms : undefined, }; if (index >= 0) { next[index] = nextItem; } else { next.push(nextItem); } return next; }; export const completeBackendProgress = (progress: unknown) => Array.isArray(progress) ? progress.map((item) => { if (!isObjectRecord(item) || item.status !== "running") { return item; } const endedAt = Date.now(); const startedAt = typeof item.startedAt === "number" ? item.startedAt : undefined; return { ...item, status: "completed", endedAt, elapsedMs: undefined, elapsedSnapshotAt: undefined, durationMs: typeof item.durationMs === "number" ? item.durationMs : startedAt !== undefined ? Math.max(0, endedAt - startedAt) : item.elapsedMs, }; }) : progress; export const cancelBackendTodos = (todos: unknown) => Array.isArray(todos) ? todos.map((todoUpdate) => { if (!isObjectRecord(todoUpdate) || !Array.isArray(todoUpdate.todos)) { return todoUpdate; } return { ...todoUpdate, todos: todoUpdate.todos.map((todo) => { if (!isObjectRecord(todo)) { return todo; } if (todo.status !== "pending" && todo.status !== "in_progress") { return todo; } return { ...todo, status: "cancelled", updatedAt: Date.now(), }; }), }; }) : todos; export const completeBackendTodos = (todos: unknown) => Array.isArray(todos) ? todos.map((todoUpdate) => { if (!isObjectRecord(todoUpdate) || !Array.isArray(todoUpdate.todos)) { return todoUpdate; } return { ...todoUpdate, todos: todoUpdate.todos.map((todo) => { if (!isObjectRecord(todo)) { return todo; } if (todo.status !== "pending" && todo.status !== "in_progress") { return todo; } return { ...todo, status: "completed", updatedAt: Date.now(), }; }), }; }) : todos; export const updateLastAssistantMessage = ( messages: unknown[], updater: (message: Record) => Record, ) => { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (isObjectRecord(message) && message.role === "assistant") { const next = [...messages]; next[index] = updater(message); return next; } } return messages; }; export const updateLastAssistantPermission = ( messages: unknown[], requestId: string, updater: (permission: Record) => Record, ) => updateLastAssistantMessage(messages, (message) => { const permissions = Array.isArray(message.permissions) ? message.permissions : []; return { ...message, permissions: permissions.map((permission) => isObjectRecord(permission) && permission.requestId === requestId ? updater(permission) : permission, ), }; }); export const updateLastAssistantQuestion = ( messages: unknown[], requestId: string, updater: (question: Record) => Record, ) => updateLastAssistantMessage(messages, (message) => { const questions = Array.isArray(message.questions) ? message.questions : []; return { ...message, questions: questions.map((question) => isObjectRecord(question) && question.requestId === requestId ? updater(question) : question, ), }; }); const getToolArtifactKind = (tool: string): ToolArtifactKind => { if (tool === "show_chart" || tool === "chart") return "chart"; if ( tool === "locate_features" || tool === "zoom_to_map" || tool === "render_junctions" || tool === "apply_layer_style" || tool.startsWith("locate_") ) { return "map"; } if (tool === "view_history" || tool === "view_scada") return "panel"; return "tool"; }; const getToolArtifactTitle = (tool: string, params: Record) => { if (typeof params.title === "string" && params.title.trim()) { return params.title.trim(); } if (tool === "show_chart" || tool === "chart") return "生成图表"; if (tool === "zoom_to_map") return "缩放到地图坐标"; if (tool === "render_junctions") return "渲染节点分区"; if (tool === "view_history") return "打开计算结果曲线"; if (tool === "view_scada") return "打开 SCADA 数据面板"; if (tool === "apply_layer_style") return "应用图层样式"; if (tool === "locate_features" || tool.startsWith("locate_")) return "地图定位"; return tool || "工具调用"; }; export const appendBackendToolArtifact = ( artifacts: unknown, payload: ToolCallPayload, ) => { const tool = typeof payload.tool === "string" ? payload.tool.trim() : ""; if (!tool) { return artifacts; } const params = isObjectRecord(payload.params) ? payload.params : {}; const next = Array.isArray(artifacts) ? [...artifacts] : []; next.push({ id: `${tool}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, tool, kind: getToolArtifactKind(tool), title: getToolArtifactTitle(tool, params), params, }); return next; }; export const toFrontendPermission = ( payload: PermissionRequestPayload, status: "pending" | "approved_once" | "approved_always" | "rejected" | "error" = "pending", ) => ({ requestId: payload.request_id, sessionId: payload.session_id, permission: payload.permission, patterns: payload.patterns, target: payload.target, activityId: payload.activity_id, reason: payload.reason, always: payload.always, tool: payload.tool, createdAt: payload.created_at, status, }); const toFrontendQuestion = ( payload: QuestionRequestPayload, status: "pending" | "submitting" | "answered" | "rejected" | "error" = "pending", ) => ({ requestId: payload.request_id, sessionId: payload.session_id, questions: payload.questions, tool: payload.tool, createdAt: payload.created_at, status, }); export const toPermissionStatus = (reply: PermissionReply) => { if (reply === "always") return "approved_always"; if (reply === "once") return "approved_once"; return "rejected"; }; export const upsertBackendQuestion = ( questions: unknown, payload: QuestionRequestPayload, ) => { const next = Array.isArray(questions) ? [...questions] : []; const index = next.findIndex((item) => { if (!isObjectRecord(item)) return false; if (item.requestId === payload.request_id) return true; const tool = isObjectRecord(item.tool) ? item.tool : undefined; return Boolean( payload.tool?.callID && tool?.callID === payload.tool.callID, ); }); const nextItem = toFrontendQuestion(payload); if (index >= 0) { const current = next[index]; const currentRequestId = isObjectRecord(current) ? current.requestId : undefined; const currentTool = isObjectRecord(current) && isObjectRecord(current.tool) ? current.tool : undefined; const currentIsActionable = typeof currentRequestId === "string" && currentRequestId !== currentTool?.callID; const payloadIsToolPlaceholder = Boolean(payload.tool?.callID) && payload.request_id === payload.tool?.callID; next[index] = { ...(isObjectRecord(current) ? current : {}), ...(currentIsActionable && payloadIsToolPlaceholder ? { questions: nextItem.questions, tool: nextItem.tool, createdAt: nextItem.createdAt, } : nextItem), status: isObjectRecord(current) && current.status === "submitting" ? "submitting" : nextItem.status, }; } else { next.push(nextItem); } return next; }; export const upsertBackendTodoUpdate = ( _todos: unknown, payload: TodoUpdatePayload, ) => [ { sessionId: payload.session_id, messageId: payload.message_id, todos: payload.todos, createdAt: payload.created_at, }, ];