mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-29 20:34:59 +08:00
feat(codex): upgrade Codex CLI to version 0.145.0 and refactor interrupt handling for improved turn management
This commit is contained in:
@@ -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 诊断日志,过滤轮询和流式重复事件并改用任务摘要展示。
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<string, string>();
|
||||
private lastUsage: unknown = null;
|
||||
private pending = new Map<number, PendingRequest>();
|
||||
@@ -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 extends CodexRequestMethod>(method: Method, params: CodexRequestParams<Method>) {
|
||||
const id = this.nextId++;
|
||||
this.write({ id, method, params });
|
||||
return new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
||||
return new Promise<CodexRequestResult<Method>>((resolve, reject) => this.pending.set(id, { resolve: (result) => resolve(result as CodexRequestResult<Method>), 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<CodexTurnInput>((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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, never>;
|
||||
};
|
||||
"turn/start": {
|
||||
params: { threadId: string; input: CodexTurnInput[]; approvalPolicy: "never" };
|
||||
result: { turn: CodexTurn };
|
||||
};
|
||||
"turn/interrupt": {
|
||||
params: { threadId: string; turnId: string };
|
||||
result: Record<string, never>;
|
||||
};
|
||||
};
|
||||
|
||||
export type CodexRequestMethod = keyof CodexRequestSpec;
|
||||
export type CodexRequestParams<Method extends CodexRequestMethod> = CodexRequestSpec[Method]["params"];
|
||||
export type CodexRequestResult<Method extends CodexRequestMethod> = 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<Method extends CodexNotificationMethod> = CodexNotificationSpec[Method];
|
||||
@@ -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。 */
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
|
||||
|
||||
Reference in New Issue
Block a user