diff --git a/CHANGELOG.md b/CHANGELOG.md index 092981b..140c736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased ++ [优化] Canvas Agent 内置 Codex CLI 升级至 0.145.0。 ++ [修复] Canvas Agent 停止任务改为中断当前 Codex turn,避免终止整个 App Server 进程。 + [新增] Canvas Agent 支持通过 `--debug` 输出详细日志并按日期保存到本地日志文件。 + [优化] Canvas Agent Debug 日志改为简洁的纯文本单行格式。 + [优化] 精简网页与本地 Agent 的 HTTP 诊断日志,过滤轮询和流式重复事件并改用任务摘要展示。 diff --git a/canvas-agent/bun.lock b/canvas-agent/bun.lock index 6941cde..cd0a3dc 100644 --- a/canvas-agent/bun.lock +++ b/canvas-agent/bun.lock @@ -6,7 +6,7 @@ "name": "@basketikun/canvas-agent", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", - "@openai/codex": "^0.139.0", + "@openai/codex": "0.145.0", "express": "^5.1.0", "winston": "^3.19.0", "zod": "^3.25.0", @@ -80,19 +80,19 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], - "@openai/codex": ["@openai/codex@0.139.0", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0.tgz", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.139.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.139.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.139.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.139.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.139.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.139.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-wr2fRE+fzW0CjEbfFsLh1ftarVEcw0CMLWS7QyA0nyOz5qacQPVq3cq2+/U7oEbwm1TOqoi0Fm1nxniB5FkpmA=="], + "@openai/codex": ["@openai/codex@0.145.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.145.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.145.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.145.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.145.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.145.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.145.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-/PSPSFujjjmiyVFvG2yu/grOFhsWdokTH8t2KGWhXSo/M5n/dIDsnbsnO82/7bLtIoDuzQf7ATBUMWqPWQINlQ=="], - "@openai/codex-darwin-arm64": ["@openai/codex@0.139.0-darwin-arm64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-darwin-arm64.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-o+0ZKWwgDFMMLO7rwinzO0PQsgK+Vme1pMN2GeAxsX29ZgGZcyPICfpJbeGSUO1mb2a36Skjx6nfdRnxMY0r7w=="], + "@openai/codex-darwin-arm64": ["@openai/codex@0.145.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h6aQ0UxnaP8mIM/9/qPAH9MNkRliJo88toq1T36IxNM2L5JSU0TFamu+MZn7YkFgDsrp0RfiI+97Tm8AVVxqtA=="], - "@openai/codex-darwin-x64": ["@openai/codex@0.139.0-darwin-x64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-darwin-x64.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-9gkBWzu6DB2rqU4DbpxD3DE5bofGpsK46Lp0h0I+bKWc2IIcxvSi8K2utKmBLoJCbKrn4JQu7dFNGRqEfENung=="], + "@openai/codex-darwin-x64": ["@openai/codex@0.145.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-FCYzVKCa9VoLtg9gVyzKpqylonfgZrfcWZN6HsXAZPeuo8CukdMqdgTUOhDn2V6h3MbqS0z6VqQVKUllN/yKhA=="], - "@openai/codex-linux-arm64": ["@openai/codex@0.139.0-linux-arm64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-linux-arm64.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-tBQE5lZciRHeWZGuURgjP9S717MvTIpQMc593+DNxY2LQxozkngOkzFSQd1+/UmQKGrCqdFLu5irIwPXpSZyEw=="], + "@openai/codex-linux-arm64": ["@openai/codex@0.145.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-8OLcPXaAol/FOrRoDxWhIiHIFa73KRsM41EKocjRZOwiT4TcelzJWn3dHyiuSb7teWF25rrslvSPyvhULYRRCQ=="], - "@openai/codex-linux-x64": ["@openai/codex@0.139.0-linux-x64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-linux-x64.tgz", { "os": "linux", "cpu": "x64" }, "sha512-14UgzDS+X4crkvdt6S02A/ZZOrS8ZyWiuTRpguCtnhNamb7unSuDxy86BWgpAl3sqiTaN2CP8VLyp2ohQ8Nbzw=="], + "@openai/codex-linux-x64": ["@openai/codex@0.145.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-u8w8LLv3DvsfrDCoswLIemZ0SoNEXyi511WsfFsSiYUazk9qMsB/NtU8N9vhAfN7mZAxLFoMex4v66JjHuZWwA=="], - "@openai/codex-win32-arm64": ["@openai/codex@0.139.0-win32-arm64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-win32-arm64.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-nlwRjsYotH1Rtqu/Q0VwQbIeO2UX1mkHK84Ov9qn/hl29QqqoBtno0tRyqIPbkXFIVQuWiAYXlV3ugLwH5fTrQ=="], + "@openai/codex-win32-arm64": ["@openai/codex@0.145.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-sub61rjEFevi1i3Zx7nAd4JM5XxoNFqMqFc5LfTo2xSI8ixHjFvEYDFDXwXOftT04n3Ht1Wh271ioUZpDiEjEg=="], - "@openai/codex-win32-x64": ["@openai/codex@0.139.0-win32-x64", "https://registry.npmmirror.com/@openai/codex/-/codex-0.139.0-win32-x64.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lQrVLNz+90wdvWVNFDvCkHQRiAK9ZllmkTka3c8eqSDqdJk35Gpgppfv9Xtw5M2ZBtTq0sBdWBiCMyzGDBSpmQ=="], + "@openai/codex-win32-x64": ["@openai/codex@0.145.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-u0h9lk094CaXRSqE34SBW2dRaQTPa6fASXqehczWH9QdsU62mBsiAgAdp6tCG4i+YzPmmhjD8FdXNnYGNmwuMg=="], "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], diff --git a/canvas-agent/package.json b/canvas-agent/package.json index 0d5ef30..36be50c 100644 --- a/canvas-agent/package.json +++ b/canvas-agent/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", - "@openai/codex": "^0.139.0", + "@openai/codex": "0.145.0", "express": "^5.1.0", "winston": "^3.19.0", "zod": "^3.25.0" diff --git a/canvas-agent/src/agent/codex-client.ts b/canvas-agent/src/agent/codex-client.ts index d91b34e..5e744b5 100644 --- a/canvas-agent/src/agent/codex-client.ts +++ b/canvas-agent/src/agent/codex-client.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; import { VERSION } from "../config.js"; import { logger } from "../utils/logger.js"; import { field, type JsonRecord } from "../utils/value.js"; +import type { CodexNotificationParams, CodexRequestMethod, CodexRequestParams, CodexRequestResult, CodexTurnInput } from "./codex-protocol.js"; import type { AgentEmit } from "./types.js"; type AgentEvent = JsonRecord & { type: string; usage?: unknown }; @@ -19,6 +20,7 @@ export class CodexAppClient { private nextId = 1; private buffer = ""; private currentThreadId = ""; + private currentTurnId = ""; private textByItem = new Map(); private lastUsage: unknown = null; private pending = new Map(); @@ -56,24 +58,20 @@ export class CodexAppClient { /** 创建新的 Codex 线程。 */ async startThread(cwd?: string) { - const result = await this.request("thread/start", { approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}), threadSource: "user" }); - const thread = field(result, "thread") as JsonRecord | undefined; - const id = String(field(thread, "id") || ""); - if (!id) throw new Error("Codex app-server 没有返回 thread id"); - return thread || {}; + const { thread } = await this.request("thread/start", { approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}), threadSource: "user" }); + if (!thread.id) throw new Error("Codex app-server 没有返回 thread id"); + return thread; } /** 恢复已有 Codex 线程。 */ async resumeThread(threadId: string, cwd?: string) { - const result = await this.request("thread/resume", { threadId, approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}) }); - const thread = field(result, "thread") as JsonRecord | undefined; - const id = String(field(thread, "id") || ""); - if (!id) throw new Error("Codex app-server 没有返回 thread id"); - return thread || {}; + const { thread } = await this.request("thread/resume", { threadId, approvalPolicy: "never", sandbox: "workspace-write", config: codexConfig(), ...(cwd ? { cwd } : {}) }); + if (!thread.id) throw new Error("Codex app-server 没有返回 thread id"); + return thread; } /** 查询 Codex 线程列表。 */ - listThreads(params: JsonRecord) { + listThreads(params: CodexRequestParams<"thread/list">) { return this.request("thread/list", params); } @@ -89,14 +87,17 @@ export class CodexAppClient { /** 启动一个 Codex turn 并等待完成通知。 */ async startTurn(threadId: string, prompt: string, images: string[], onTurn?: (turnId: string) => void) { - const result = await this.request("turn/start", { threadId, input: codexInput(prompt, images), approvalPolicy: "never" }); - const turnId = String(field(field(result, "turn"), "id") || ""); + const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), approvalPolicy: "never" }); + const turnId = turn.id; if (!turnId) throw new Error("Codex app-server 没有返回 turn id"); this.currentThreadId = threadId; + this.currentTurnId = turnId; onTurn?.(turnId); const completed = this.completedTurns.get(turnId); if (this.completedTurns.has(turnId)) { this.completedTurns.delete(turnId); + this.currentThreadId = ""; + this.currentTurnId = ""; if (completed) throw completed; return; } @@ -104,22 +105,25 @@ export class CodexAppClient { } /** 中断当前正在运行的 Codex turn。 */ - interruptCurrentTurn() { - if (this.activeTurns.size === 0) return false; + async interruptCurrentTurn() { + const threadId = this.currentThreadId; + const turnId = this.currentTurnId; + if (!threadId || !turnId) return false; try { - logger.warn("Interrupting active Codex turn", { threadId: this.currentThreadId, activeTurns: this.activeTurns.size }); - this.child.kill("SIGINT"); + logger.warn("Interrupting active Codex turn", { threadId, turnId }); + await this.request("turn/interrupt", { threadId, turnId }); return true; - } catch { + } catch (error) { + logger.warn("Failed to interrupt Codex turn", { error, threadId, turnId }); return false; } } /** 发送 JSON-RPC 请求并保存待处理 Promise。 */ - private request(method: string, params: unknown) { + private request(method: Method, params: CodexRequestParams) { const id = this.nextId++; this.write({ id, method, params }); - return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject })); + return new Promise>((resolve, reject) => this.pending.set(id, { resolve: (result) => resolve(result as CodexRequestResult), reject })); } /** 发送无需响应的 JSON-RPC 通知。 */ @@ -165,9 +169,9 @@ export class CodexAppClient { /** 转换并广播 app-server 通知。 */ private handleNotification(method: string, params: JsonRecord) { - if (method === "item/agentMessage/delta") return this.emitDelta(params); + if (method === "item/agentMessage/delta") return this.emitDelta(params as unknown as CodexNotificationParams<"item/agentMessage/delta">); if (method === "thread/tokenUsage/updated") { - this.lastUsage = normalizeUsage(params); + this.lastUsage = normalizeUsage(params as unknown as CodexNotificationParams<"thread/tokenUsage/updated">); this.emit("agent_event", { agent: "codex", type: "usage.updated", usage: this.lastUsage, ...codexEventScope(params) }); return; } @@ -183,24 +187,28 @@ export class CodexAppClient { if (event.type === "turn.completed") event.usage = this.lastUsage; this.emit("agent_event", { agent: "codex", ...event }); if (event.type === "turn.completed") { - const turnId = String(field(params, "turnId") || field(field(params, "turn"), "id") || ""); + const turn = (params as unknown as CodexNotificationParams<"turn/completed">).turn; + const turnId = turn.id; const pending = this.activeTurns.get(turnId); - const error = field(field(params, "turn"), "error"); + const error = turn.error; if (pending) { this.activeTurns.delete(turnId); - error ? pending.reject(new Error(String(field(error, "message") || "Codex turn failed"))) : pending.resolve(event); + error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(event); } else if (turnId) { - this.completedTurns.set(turnId, error ? new Error(String(field(error, "message") || "Codex turn failed")) : null); + this.completedTurns.set(turnId, error ? new Error(error.message || "Codex turn failed") : null); + } + if (turnId === this.currentTurnId) { + this.currentThreadId = ""; + this.currentTurnId = ""; } - if (this.activeTurns.size === 0) this.currentThreadId = ""; this.emit("agent_done", { agent: "codex", usage: event.usage, ...codexEventScope(params) }); } } /** 合并并广播 Agent 文本增量。 */ - private emitDelta(params: JsonRecord) { - const id = String(field(params, "itemId") || ""); - const text = `${this.textByItem.get(id) || ""}${String(field(params, "delta") || "")}`; + private emitDelta(params: CodexNotificationParams<"item/agentMessage/delta">) { + const id = params.itemId; + const text = `${this.textByItem.get(id) || ""}${params.delta}`; this.textByItem.set(id, text); this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: "agent_message", text }, ...codexEventScope(params) }); } @@ -231,6 +239,7 @@ export class CodexAppClient { this.pending.clear(); this.activeTurns.clear(); this.currentThreadId = ""; + this.currentTurnId = ""; } } @@ -248,8 +257,8 @@ function codexConfig() { } /** 将文本和本地图片转换为 Codex turn 输入。 */ -function codexInput(prompt: string, images: string[]) { - return [{ type: "text", text: prompt, text_elements: [] }, ...images.map((file) => ({ type: "localImage", path: file }))]; +function codexInput(prompt: string, images: string[]): CodexTurnInput[] { + return [{ type: "text", text: prompt, text_elements: [] }, ...images.map((file) => ({ type: "localImage", path: file }))]; } /** 将 app-server 通知转换为前端使用的 Agent 事件。 */ @@ -260,7 +269,7 @@ function normalizeCodexNotification(method: string, params: JsonRecord): AgentEv if (method === "turn/completed") return { type: "turn.completed", usage: null, duration_ms: field(field(params, "turn"), "durationMs"), ...scope }; if (method === "item/started") return { type: "item.started", item: normalizeItem(field(params, "item")), ...scope }; if (method === "item/completed") return { type: "item.completed", item: normalizeItem(field(params, "item")), ...scope }; - if (method === "error") return { type: "error", message: field(params, "message"), ...scope }; + if (method === "error") return { type: "error", message: field(field(params, "error"), "message"), ...scope }; return null; } @@ -282,13 +291,13 @@ function normalizeItem(item: unknown) { } /** 将 Codex token usage 转换为前端字段。 */ -function normalizeUsage(params: JsonRecord) { - const last = field(field(params, "tokenUsage"), "last") as JsonRecord | undefined; +function normalizeUsage(params: CodexNotificationParams<"thread/tokenUsage/updated">) { + const last = params.tokenUsage.last; return { - input_tokens: field(last, "inputTokens"), - cached_input_tokens: field(last, "cachedInputTokens"), - output_tokens: field(last, "outputTokens"), - reasoning_output_tokens: field(last, "reasoningOutputTokens"), + input_tokens: last.inputTokens, + cached_input_tokens: last.cachedInputTokens, + output_tokens: last.outputTokens, + reasoning_output_tokens: last.reasoningOutputTokens, }; } diff --git a/canvas-agent/src/agent/codex-protocol.ts b/canvas-agent/src/agent/codex-protocol.ts new file mode 100644 index 0000000..639e129 --- /dev/null +++ b/canvas-agent/src/agent/codex-protocol.ts @@ -0,0 +1,87 @@ +import type { JsonRecord } from "../utils/value.js"; + +export type CodexThread = JsonRecord & { id: string; cwd: string; turns?: CodexTurn[] }; +export type CodexTurn = JsonRecord & { id: string; error?: CodexTurnError | null; durationMs?: number | null }; +export type CodexTurnError = JsonRecord & { message: string }; +export type CodexItem = JsonRecord & { id: string; type: string; text?: string }; + +export type CodexTurnInput = + | { type: "text"; text: string; text_elements: [] } + | { type: "localImage"; path: string }; + +type ThreadOptions = { + approvalPolicy: "never"; + sandbox: "workspace-write"; + config: JsonRecord; + cwd?: string; +}; + +type CodexRequestSpec = { + initialize: { + params: { + clientInfo: { name: string; title: string; version: string }; + capabilities: { experimentalApi: boolean; requestAttestation: boolean }; + }; + result: JsonRecord; + }; + "thread/start": { + params: ThreadOptions & { threadSource: "user" }; + result: { thread: CodexThread }; + }; + "thread/resume": { + params: ThreadOptions & { threadId: string }; + result: { thread: CodexThread }; + }; + "thread/list": { + params: { + limit: number; + sortKey: "updated_at"; + sortDirection: "desc"; + sourceKinds: Array<"cli" | "vscode" | "appServer" | "exec">; + cwd: string; + searchTerm?: string; + }; + result: { data: CodexThread[]; nextCursor: string | null; backwardsCursor: string | null }; + }; + "thread/read": { + params: { threadId: string; includeTurns: boolean }; + result: { thread: CodexThread }; + }; + "thread/archive": { + params: { threadId: string }; + result: Record; + }; + "turn/start": { + params: { threadId: string; input: CodexTurnInput[]; approvalPolicy: "never" }; + result: { turn: CodexTurn }; + }; + "turn/interrupt": { + params: { threadId: string; turnId: string }; + result: Record; + }; +}; + +export type CodexRequestMethod = keyof CodexRequestSpec; +export type CodexRequestParams = CodexRequestSpec[Method]["params"]; +export type CodexRequestResult = CodexRequestSpec[Method]["result"]; + +type TokenUsageBreakdown = { + inputTokens: number; + cachedInputTokens: number; + outputTokens: number; + reasoningOutputTokens: number; +}; + +type CodexNotificationSpec = { + "thread/started": { thread: CodexThread }; + "turn/started": { threadId: string; turn: CodexTurn }; + "turn/completed": { threadId: string; turn: CodexTurn }; + "item/started": { threadId: string; turnId: string; item: CodexItem }; + "item/completed": { threadId: string; turnId: string; item: CodexItem }; + "item/agentMessage/delta": { threadId: string; turnId: string; itemId: string; delta: string }; + "thread/tokenUsage/updated": { threadId: string; turnId: string; tokenUsage: { last: TokenUsageBreakdown } }; + error: { threadId: string; turnId: string; error: CodexTurnError; willRetry: boolean }; +}; + +export type CodexNotificationMethod = keyof CodexNotificationSpec; +export type CodexNotificationParams = CodexNotificationSpec[Method]; diff --git a/canvas-agent/src/agent/codex.ts b/canvas-agent/src/agent/codex.ts index c8b19a7..01af3dc 100644 --- a/canvas-agent/src/agent/codex.ts +++ b/canvas-agent/src/agent/codex.ts @@ -25,9 +25,9 @@ export async function runCodexTurn(prompt: string, emit: AgentEmit, attachments: } /** 中断当前线程正在执行的 Codex turn。 */ -export function interruptCodexTurn(threadId?: string) { +export async function interruptCodexTurn(threadId?: string) { if (!codexApp || (threadId && threadId !== codexThreadId)) return false; - return codexApp.interruptCurrentTurn(); + return await codexApp.interruptCurrentTurn(); } /** 创建新的 Codex 线程并记录当前线程 ID。 */ diff --git a/canvas-agent/src/server/http.ts b/canvas-agent/src/server/http.ts index 4d2b3e8..af8fd3e 100644 --- a/canvas-agent/src/server/http.ts +++ b/canvas-agent/src/server/http.ts @@ -181,10 +181,7 @@ export function startHttpServer() { throw error; } })); - app.post("/agent/codex/interrupt", (req, res) => { - const ok = interruptCodexTurn(String(req.body?.threadId || "")); - res.json({ ok }); - }); + app.post("/agent/codex/interrupt", route(async (req, res) => res.json({ ok: await interruptCodexTurn(String(req.body?.threadId || "")) }))); app.post("/agent/claude/turn", (req, res) => { runClaudeTurn(String(req.body?.prompt || ""), emit); res.json({ ok: true }); diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index d7004f9..88e804b 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- Canvas Agent Codex 升级:启动 Canvas Agent 后应能正常连接 Codex、创建或恢复会话、发送消息并调用画布工具,实际运行的 Codex CLI 版本应为 0.145.0;运行中停止任务应只中断当前 turn,随后无需重启 Agent 即可继续发送新任务。 - Canvas Agent Debug:使用 `npx -y @basketikun/canvas-agent --debug` 启动后,终端应按“时间 级别 消息 详情”的纯文本单行格式输出日志并显示日志文件路径,`~/.infinite-canvas/logs/` 下应按启动日期生成相同格式的 `canvas-agent-YYYY-MM-DD.log`,同一天多次启动应追加到同一文件;普通启动保持原有简洁输出,日志中不应出现连接 token 或图片 Data URL 原文。 - Agent HTTP 诊断日志:网页发送一条普通消息后,本地 Debug 日志不应重复输出 `/health`、`/canvas/state`、`/canvas/activate` 成功请求、流式增量或完整会话响应,只保留 HTTP 请求与 Codex 生命周期摘要;右侧「日志」应以单行时间线展示发送、开始、回复、工具、完成用量和错误,不再输出 userMessage started/completed、流式摘要、重复 threadId 或大段原始 JSON。 - Agent 对话统计:用户消息应右对齐并使用透明无边框、无气泡背景的简洁样式,用户消息和 Codex 回复下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。