@xiaohhhh1/canvas-agent 0.4.43 → 0.4.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,6 +46,8 @@ export declare class CodexAppClient {
46
46
  startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, onTurn?: (turnId: string) => void, outputSchema?: JsonRecord): Promise<string>;
47
47
  /** 中断当前正在运行的 Codex turn。 */
48
48
  interruptCurrentTurn(): Promise<boolean>;
49
+ /** 终止失去响应的 app-server,让后续队列可以在新进程继续。 */
50
+ terminate(message?: string): Promise<void>;
49
51
  /** 回复网页端已经确认的 Codex 权限请求。 */
50
52
  resolveApproval(requestId: string, decision: string): boolean;
51
53
  /** 发送 JSON-RPC 请求并保存待处理 Promise。 */
@@ -128,6 +128,17 @@ export class CodexAppClient {
128
128
  return false;
129
129
  }
130
130
  }
131
+ /** 终止失去响应的 app-server,让后续队列可以在新进程继续。 */
132
+ terminate(message = "Codex app-server was restarted") {
133
+ this.failAll(message);
134
+ if (this.child.exitCode !== null)
135
+ return Promise.resolve();
136
+ return new Promise((resolve) => {
137
+ const timer = setTimeout(resolve, 2000);
138
+ this.child.once("exit", () => { clearTimeout(timer); resolve(); });
139
+ this.child.kill();
140
+ });
141
+ }
131
142
  /** 回复网页端已经确认的 Codex 权限请求。 */
132
143
  resolveApproval(requestId, decision) {
133
144
  const request = this.approvalRequests.get(requestId);
@@ -22,6 +22,8 @@ export { summarizeCodexThread } from "./codex-history.js";
22
22
  export declare function runCodexTurn(prompt: string, emit: AgentEmit, attachments?: AgentAttachment[], options?: CodexRunOptions): Promise<CodexRunResult>;
23
23
  /** 中断当前线程正在执行的 Codex turn。 */
24
24
  export declare function interruptCodexTurn(threadId?: string): Promise<boolean>;
25
+ /** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
26
+ export declare function restartCodexApp(message?: string): Promise<void>;
25
27
  /** 回复当前 app-server 的待处理权限请求。 */
26
28
  export declare function resolveCodexApproval(requestId: string, decision: string): Promise<boolean>;
27
29
  /** 创建新的 Codex 线程并记录当前线程 ID。 */
@@ -24,6 +24,14 @@ export async function interruptCodexTurn(threadId) {
24
24
  return false;
25
25
  return await codexApp.interruptCurrentTurn();
26
26
  }
27
+ /** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
28
+ export async function restartCodexApp(message = "Codex 执行超时,正在重启本机脚本引擎") {
29
+ const app = codexApp;
30
+ codexApp = null;
31
+ codexAppStart = null;
32
+ codexThreadId = "";
33
+ await app?.terminate(message);
34
+ }
27
35
  /** 回复当前 app-server 的待处理权限请求。 */
28
36
  export async function resolveCodexApproval(requestId, decision) {
29
37
  return Boolean(codexApp?.resolveApproval(requestId, decision));
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@ if (process.argv[2] === "mcp") {
7
7
  await ensureHttpServer();
8
8
  await startMcpServer();
9
9
  }
10
+ else if (process.argv[2] === "upgrade") {
11
+ await ensureHttpServer();
12
+ console.log("Canvas Agent upgraded and running in the background.");
13
+ }
10
14
  else if (process.argv[2] === "watch")
11
15
  startHttpSupervisor();
12
16
  else
@@ -59,7 +59,7 @@ export function startHttpServer() {
59
59
  return void res.json({});
60
60
  next();
61
61
  });
62
- app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION, relayReady: relayStatus.ready, relayLastReadyAt: relayStatus.lastReadyAt, relayLastDisconnectAt: relayStatus.lastDisconnectAt }));
62
+ app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION, workflow: workflows.health(), relayReady: relayStatus.ready, relayLastReadyAt: relayStatus.lastReadyAt, relayLastDisconnectAt: relayStatus.lastDisconnectAt }));
63
63
  app.get("/config", (_req, res) => res.json({ ok: true, url: config.url, hasToken: true }));
64
64
  app.use((req, res, next) => {
65
65
  if (validToken(req, requestUrl(req, config), config.token))
@@ -1,7 +1,22 @@
1
1
  import type { AgentEmit } from "../agent/types.js";
2
2
  import { type CanvasAgentConfig } from "../config.js";
3
+ export declare const FLOW_C_CODEX_TURN_TIMEOUT_MS: number;
3
4
  type ScriptStatus = "queued" | "running" | "complete" | "error" | "expired";
4
5
  type DownloadStatus = "waiting" | "running" | "complete" | "error" | "expired";
6
+ type ScriptRecord = {
7
+ id: string;
8
+ apiBase: string;
9
+ accessToken: string;
10
+ status: ScriptStatus;
11
+ requestedCount: number;
12
+ receivedOrdinals: number[];
13
+ threadId?: string;
14
+ message?: string;
15
+ expiresAt?: string;
16
+ attempts: number;
17
+ priorityAt?: string;
18
+ updatedAt: string;
19
+ };
5
20
  type ScriptTask = {
6
21
  id: string;
7
22
  workflow: "flow-c";
@@ -86,6 +101,11 @@ export declare class WorkflowManager {
86
101
  updatedAt: string;
87
102
  }[];
88
103
  };
104
+ health(): {
105
+ activeScriptHandoffIds: string[];
106
+ queuedScripts: number;
107
+ blockedScripts: number;
108
+ };
89
109
  startDownloadDirectorySelection(): {
90
110
  id: string;
91
111
  status: "selecting" | "selected" | "cancelled" | "error";
@@ -174,4 +194,5 @@ export declare class WorkflowManager {
174
194
  private downloadRecord;
175
195
  private save;
176
196
  }
197
+ export declare function compareScriptQueueRecords(left: Pick<ScriptRecord, "priorityAt" | "updatedAt">, right: Pick<ScriptRecord, "priorityAt" | "updatedAt">): number;
177
198
  export {};
@@ -5,13 +5,14 @@ import { mkdir, open, rename, stat, unlink } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { Readable, Transform } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
- import { runCodexTurn, startCodexThread } from "../agent/codex.js";
8
+ import { restartCodexApp, runCodexTurn, startCodexThread } from "../agent/codex.js";
9
9
  import { CONFIG_DIR, ensureSiteWorkspace } from "../config.js";
10
10
  import { logger } from "../utils/logger.js";
11
11
  import { windowsPowerShellExecutable } from "../utils/windows.js";
12
12
  import { FLOW_C_SCRIPT_CHUNK_MAX, FLOW_C_SCRIPT_CHUNK_SIZES } from "./constants.js";
13
13
  import { flowCScriptOutputSchema, parseFlowCScriptOutput } from "./script-output.js";
14
14
  const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
15
+ export const FLOW_C_CODEX_TURN_TIMEOUT_MS = 8 * 60 * 1000;
15
16
  /** 本机持久化的 Flow C 控制器:脚本交给本机 Codex,视频直接落盘。 */
16
17
  export class WorkflowManager {
17
18
  config;
@@ -46,6 +47,7 @@ export class WorkflowManager {
46
47
  threadId: previous?.threadId,
47
48
  expiresAt: String(input.expiresAt || previous?.expiresAt || "") || undefined,
48
49
  attempts: previous?.attempts || 0,
50
+ priorityAt: now(),
49
51
  message: previous?.status === "complete" ? previous.message : "已进入本机 Codex 队列",
50
52
  updatedAt: now(),
51
53
  };
@@ -57,6 +59,7 @@ export class WorkflowManager {
57
59
  const id = workflowId(idValue, "脚本交接 ID");
58
60
  const record = this.scriptRecord(id);
59
61
  record.status = "queued";
62
+ record.priorityAt = now();
60
63
  record.message = "正在重新连接本机 Codex";
61
64
  record.updatedAt = now();
62
65
  this.save();
@@ -109,6 +112,14 @@ export class WorkflowManager {
109
112
  subscriptions: Object.values(this.state.downloads).map(publicDownload),
110
113
  };
111
114
  }
115
+ health() {
116
+ const records = Object.values(this.state.scripts);
117
+ return {
118
+ activeScriptHandoffIds: [...this.runningScripts],
119
+ queuedScripts: records.filter((record) => record.status === "queued" || record.status === "running").length,
120
+ blockedScripts: records.filter((record) => record.status === "error").length,
121
+ };
122
+ }
112
123
  startDownloadDirectorySelection() {
113
124
  if (this.directorySelection?.status === "selecting")
114
125
  return this.directorySelection;
@@ -177,7 +188,7 @@ export class WorkflowManager {
177
188
  while (true) {
178
189
  const next = Object.values(this.state.scripts)
179
190
  .filter((record) => (record.status === "queued" || record.status === "running") && !this.runningScripts.has(record.id))
180
- .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt))[0];
191
+ .sort(compareScriptQueueRecords)[0];
181
192
  if (!next)
182
193
  break;
183
194
  await this.runScript(next.id);
@@ -207,6 +218,7 @@ export class WorkflowManager {
207
218
  this.runningScripts.add(id);
208
219
  const record = this.scriptRecord(id);
209
220
  try {
221
+ delete record.priorityAt;
210
222
  record.status = "running";
211
223
  record.message = "正在读取完整产品清单";
212
224
  record.updatedAt = now();
@@ -236,7 +248,7 @@ export class WorkflowManager {
236
248
  record.message = `本机 Codex 正在写第 ${missing[0]}–${missing.at(-1)} 条(总计 ${task.requested_count} 条)`;
237
249
  record.updatedAt = now();
238
250
  this.save();
239
- const result = await runCodexTurn(scriptChunkPrompt(id, task, missing), this.emit, [], { threadId: record.threadId, cwd: workspace.workspacePath, permissionMode: "full", outputSchema: flowCScriptOutputSchema(Number(task.duration_seconds || 10), missing.length), onThread: (threadId) => { record.threadId = threadId; this.save(); } });
251
+ const result = await runCodexScriptWithTimeout(scriptChunkPrompt(id, task, missing), this.emit, { threadId: record.threadId, cwd: workspace.workspacePath, permissionMode: "full", outputSchema: flowCScriptOutputSchema(Number(task.duration_seconds || 10), missing.length), onThread: (threadId) => { record.threadId = threadId; this.save(); } });
240
252
  await this.scriptTask(id);
241
253
  progressed = record.receivedOrdinals.length > before;
242
254
  if (!progressed && result.ok && result.text) {
@@ -417,6 +429,28 @@ export class WorkflowManager {
417
429
  }
418
430
  save() { saveState(this.state); }
419
431
  }
432
+ export function compareScriptQueueRecords(left, right) {
433
+ if (left.priorityAt || right.priorityAt)
434
+ return String(right.priorityAt || "").localeCompare(String(left.priorityAt || ""));
435
+ return left.updatedAt.localeCompare(right.updatedAt);
436
+ }
437
+ async function runCodexScriptWithTimeout(prompt, emit, options) {
438
+ let timer;
439
+ const turn = runCodexTurn(prompt, emit, [], options);
440
+ const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve("timeout"), FLOW_C_CODEX_TURN_TIMEOUT_MS); });
441
+ try {
442
+ const result = await Promise.race([turn, timeout]);
443
+ if (result !== "timeout")
444
+ return result;
445
+ await restartCodexApp("Flow C 脚本回合超过 8 分钟,已自动终止并换新会话");
446
+ await turn;
447
+ return { ok: false, error: "Flow C 脚本回合超过 8 分钟,已自动终止并换新会话" };
448
+ }
449
+ finally {
450
+ if (timer)
451
+ clearTimeout(timer);
452
+ }
453
+ }
420
454
  class ExpiredCapabilityError extends Error {
421
455
  }
422
456
  function scriptChunkPrompt(id, task, ordinals) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.43",
3
+ "version": "0.4.44",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",