@xiaohhhh1/canvas-agent 0.4.43 → 0.4.45
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.
- package/dist/agent/codex-client.d.ts +16 -1
- package/dist/agent/codex-client.js +101 -4
- package/dist/agent/codex-protocol.d.ts +1 -0
- package/dist/agent/codex.d.ts +2 -0
- package/dist/agent/codex.js +8 -0
- package/dist/canvas/schemas.d.ts +2 -2
- package/dist/canvas/tools.d.ts +1 -1
- package/dist/index.js +4 -0
- package/dist/server/http.js +1 -1
- package/dist/workflow/manager.d.ts +21 -0
- package/dist/workflow/manager.js +37 -3
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JsonRecord } from "../utils/value.js";
|
|
2
|
-
import type { CodexPlanUpdate, CodexRequestParams } from "./codex-protocol.js";
|
|
2
|
+
import type { CodexPlanUpdate, CodexRequestParams, CodexTurn } from "./codex-protocol.js";
|
|
3
3
|
import type { AgentEmit, AgentPermissionMode } from "./types.js";
|
|
4
4
|
/** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
|
|
5
5
|
export declare class CodexAppClient {
|
|
@@ -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。 */
|
|
@@ -64,6 +66,13 @@ export declare class CodexAppClient {
|
|
|
64
66
|
private emitDelta;
|
|
65
67
|
/** 合并短时间内的文本增量,减少 SSE 传输和前端渲染次数。 */
|
|
66
68
|
private flushDelta;
|
|
69
|
+
/**
|
|
70
|
+
* app-server 的流式通知偶尔会在本地重连时丢失。定期读取线程的最终
|
|
71
|
+
* 状态,保证被中断的 turn 不会只因少了一条通知而永久占着 Flow C 队列。
|
|
72
|
+
*/
|
|
73
|
+
private scheduleTurnReconciliation;
|
|
74
|
+
/** 清除 turn 等待器与其轮询计时器。 */
|
|
75
|
+
private clearActiveTurn;
|
|
67
76
|
/** 自动回复 app-server 发起的授权或交互请求。 */
|
|
68
77
|
private answerServerRequest;
|
|
69
78
|
/** 完成指定 JSON-RPC 请求。 */
|
|
@@ -73,3 +82,9 @@ export declare class CodexAppClient {
|
|
|
73
82
|
/** 拒绝进程退出时仍未完成的请求与 turn。 */
|
|
74
83
|
private failAll;
|
|
75
84
|
}
|
|
85
|
+
/** 只有正常 completed 才能被调用方视为可用脚本。 */
|
|
86
|
+
export declare function isTerminalTurnStatus(status: unknown): boolean;
|
|
87
|
+
/** interrupted 也必须作为失败传回,不能被误当成空的成功响应。 */
|
|
88
|
+
export declare function turnFailure(turn: Pick<CodexTurn, "status" | "error">): Error | null;
|
|
89
|
+
/** 补偿轮询发现终态时,尽量取回已完成的结构化文本。 */
|
|
90
|
+
export declare function turnText(turn: CodexTurn): string;
|
|
@@ -8,6 +8,7 @@ import { field } from "../utils/value.js";
|
|
|
8
8
|
const canvasAgentMcp = canvasAgentMcpCommand();
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const STREAM_UPDATE_INTERVAL_MS = 40;
|
|
11
|
+
const TURN_RECONCILE_INTERVAL_MS = 5_000;
|
|
11
12
|
/** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
|
|
12
13
|
export class CodexAppClient {
|
|
13
14
|
child;
|
|
@@ -110,7 +111,10 @@ export class CodexAppClient {
|
|
|
110
111
|
throw completed.error;
|
|
111
112
|
return completed?.text || "";
|
|
112
113
|
}
|
|
113
|
-
return await new Promise((resolve, reject) =>
|
|
114
|
+
return await new Promise((resolve, reject) => {
|
|
115
|
+
this.activeTurns.set(turnId, { resolve, reject });
|
|
116
|
+
this.scheduleTurnReconciliation(threadId, turnId);
|
|
117
|
+
});
|
|
114
118
|
}
|
|
115
119
|
/** 中断当前正在运行的 Codex turn。 */
|
|
116
120
|
async interruptCurrentTurn() {
|
|
@@ -128,6 +132,17 @@ export class CodexAppClient {
|
|
|
128
132
|
return false;
|
|
129
133
|
}
|
|
130
134
|
}
|
|
135
|
+
/** 终止失去响应的 app-server,让后续队列可以在新进程继续。 */
|
|
136
|
+
terminate(message = "Codex app-server was restarted") {
|
|
137
|
+
this.failAll(message);
|
|
138
|
+
if (this.child.exitCode !== null)
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const timer = setTimeout(resolve, 2000);
|
|
142
|
+
this.child.once("exit", () => { clearTimeout(timer); resolve(); });
|
|
143
|
+
this.child.kill();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
131
146
|
/** 回复网页端已经确认的 Codex 权限请求。 */
|
|
132
147
|
resolveApproval(requestId, decision) {
|
|
133
148
|
const request = this.approvalRequests.get(requestId);
|
|
@@ -256,11 +271,11 @@ export class CodexAppClient {
|
|
|
256
271
|
const turn = params.turn;
|
|
257
272
|
const turnId = turn.id;
|
|
258
273
|
const pending = this.activeTurns.get(turnId);
|
|
259
|
-
const error = turn
|
|
274
|
+
const error = turnFailure(turn);
|
|
260
275
|
const text = this.finalTextByTurn.get(turnId) || "";
|
|
261
276
|
this.finalTextByTurn.delete(turnId);
|
|
262
277
|
if (pending) {
|
|
263
|
-
this.
|
|
278
|
+
this.clearActiveTurn(turnId);
|
|
264
279
|
error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(text);
|
|
265
280
|
}
|
|
266
281
|
else if (turnId) {
|
|
@@ -300,6 +315,53 @@ export class CodexAppClient {
|
|
|
300
315
|
if (pending.delta)
|
|
301
316
|
this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: pending.itemType, delta: pending.delta }, ...codexEventScope(pending.params) });
|
|
302
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* app-server 的流式通知偶尔会在本地重连时丢失。定期读取线程的最终
|
|
320
|
+
* 状态,保证被中断的 turn 不会只因少了一条通知而永久占着 Flow C 队列。
|
|
321
|
+
*/
|
|
322
|
+
scheduleTurnReconciliation(threadId, turnId) {
|
|
323
|
+
const check = async () => {
|
|
324
|
+
const pending = this.activeTurns.get(turnId);
|
|
325
|
+
if (!pending)
|
|
326
|
+
return;
|
|
327
|
+
try {
|
|
328
|
+
const result = await this.readThread(threadId, true);
|
|
329
|
+
const thread = field(result, "thread");
|
|
330
|
+
const turns = Array.isArray(field(thread, "turns")) ? field(thread, "turns") : [];
|
|
331
|
+
const turn = turns.find((item) => item?.id === turnId);
|
|
332
|
+
if (turn && isTerminalTurnStatus(turn.status)) {
|
|
333
|
+
const error = turnFailure(turn);
|
|
334
|
+
const text = turnText(turn);
|
|
335
|
+
this.finalTextByTurn.delete(turnId);
|
|
336
|
+
this.clearActiveTurn(turnId);
|
|
337
|
+
error ? pending.reject(new Error(error.message)) : pending.resolve(text);
|
|
338
|
+
if (turnId === this.currentTurnId) {
|
|
339
|
+
this.currentThreadId = "";
|
|
340
|
+
this.currentTurnId = "";
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
// The normal deadline in the workflow manager remains the hard
|
|
347
|
+
// fallback. A transient read error must not cancel live work.
|
|
348
|
+
logger.debug("Codex turn reconciliation read failed", { threadId, turnId, error });
|
|
349
|
+
}
|
|
350
|
+
const current = this.activeTurns.get(turnId);
|
|
351
|
+
if (current)
|
|
352
|
+
current.reconcileTimer = setTimeout(() => void check(), TURN_RECONCILE_INTERVAL_MS);
|
|
353
|
+
};
|
|
354
|
+
const pending = this.activeTurns.get(turnId);
|
|
355
|
+
if (pending)
|
|
356
|
+
pending.reconcileTimer = setTimeout(() => void check(), TURN_RECONCILE_INTERVAL_MS);
|
|
357
|
+
}
|
|
358
|
+
/** 清除 turn 等待器与其轮询计时器。 */
|
|
359
|
+
clearActiveTurn(turnId) {
|
|
360
|
+
const pending = this.activeTurns.get(turnId);
|
|
361
|
+
if (pending?.reconcileTimer)
|
|
362
|
+
clearTimeout(pending.reconcileTimer);
|
|
363
|
+
this.activeTurns.delete(turnId);
|
|
364
|
+
}
|
|
303
365
|
/** 自动回复 app-server 发起的授权或交互请求。 */
|
|
304
366
|
answerServerRequest(message) {
|
|
305
367
|
const method = String(message.method);
|
|
@@ -328,7 +390,13 @@ export class CodexAppClient {
|
|
|
328
390
|
}
|
|
329
391
|
/** 拒绝进程退出时仍未完成的请求与 turn。 */
|
|
330
392
|
failAll(message) {
|
|
331
|
-
[...this.pending.values()
|
|
393
|
+
[...this.pending.values()].forEach((item) => item.reject(new Error(message)));
|
|
394
|
+
[...this.activeTurns.entries()].forEach(([turnId, item]) => {
|
|
395
|
+
if (item.reconcileTimer)
|
|
396
|
+
clearTimeout(item.reconcileTimer);
|
|
397
|
+
this.activeTurns.delete(turnId);
|
|
398
|
+
item.reject(new Error(message));
|
|
399
|
+
});
|
|
332
400
|
this.pendingDeltas.forEach((item) => clearTimeout(item.timer));
|
|
333
401
|
this.pending.clear();
|
|
334
402
|
this.activeTurns.clear();
|
|
@@ -340,6 +408,35 @@ export class CodexAppClient {
|
|
|
340
408
|
this.currentTurnId = "";
|
|
341
409
|
}
|
|
342
410
|
}
|
|
411
|
+
/** 只有正常 completed 才能被调用方视为可用脚本。 */
|
|
412
|
+
export function isTerminalTurnStatus(status) {
|
|
413
|
+
return ["completed", "failed", "interrupted", "cancelled", "canceled"].includes(String(status || "").toLowerCase());
|
|
414
|
+
}
|
|
415
|
+
/** interrupted 也必须作为失败传回,不能被误当成空的成功响应。 */
|
|
416
|
+
export function turnFailure(turn) {
|
|
417
|
+
if (turn.error)
|
|
418
|
+
return new Error(turn.error.message || "Codex turn failed");
|
|
419
|
+
const status = String(turn.status || "completed").toLowerCase();
|
|
420
|
+
return status === "completed" ? null : new Error(`Codex turn ${status}`);
|
|
421
|
+
}
|
|
422
|
+
/** 补偿轮询发现终态时,尽量取回已完成的结构化文本。 */
|
|
423
|
+
export function turnText(turn) {
|
|
424
|
+
const items = Array.isArray(field(turn, "items")) ? field(turn, "items") : [];
|
|
425
|
+
for (const item of [...items].reverse()) {
|
|
426
|
+
if (!["agent_message", "agentMessage"].includes(String(field(item, "type") || "")))
|
|
427
|
+
continue;
|
|
428
|
+
const text = field(item, "text");
|
|
429
|
+
if (typeof text === "string" && text)
|
|
430
|
+
return text;
|
|
431
|
+
const content = field(item, "content");
|
|
432
|
+
if (Array.isArray(content)) {
|
|
433
|
+
const joined = content.map((part) => String(field(part, "text") || "")).join("");
|
|
434
|
+
if (joined)
|
|
435
|
+
return joined;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return "";
|
|
439
|
+
}
|
|
343
440
|
/** 生成 Codex 调用 Canvas Agent MCP 的启动命令。 */
|
|
344
441
|
function canvasAgentMcpCommand() {
|
|
345
442
|
const current = process.argv.find((arg) => /index\.(t|j)s$/.test(arg)) || "";
|
package/dist/agent/codex.d.ts
CHANGED
|
@@ -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。 */
|
package/dist/agent/codex.js
CHANGED
|
@@ -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/canvas/schemas.d.ts
CHANGED
|
@@ -975,16 +975,16 @@ export declare const toolInputSchemas: {
|
|
|
975
975
|
}, "strip", z.ZodTypeAny, {
|
|
976
976
|
title: string;
|
|
977
977
|
kind: "text" | "image";
|
|
978
|
-
source?: string | undefined;
|
|
979
978
|
content?: string | undefined;
|
|
979
|
+
source?: string | undefined;
|
|
980
980
|
tags?: string[] | undefined;
|
|
981
981
|
imageUrl?: string | undefined;
|
|
982
982
|
note?: string | undefined;
|
|
983
983
|
}, {
|
|
984
984
|
title: string;
|
|
985
985
|
kind: "text" | "image";
|
|
986
|
-
source?: string | undefined;
|
|
987
986
|
content?: string | undefined;
|
|
987
|
+
source?: string | undefined;
|
|
988
988
|
tags?: string[] | undefined;
|
|
989
989
|
imageUrl?: string | undefined;
|
|
990
990
|
note?: string | undefined;
|
package/dist/canvas/tools.d.ts
CHANGED
|
@@ -173,8 +173,8 @@ export declare function parseToolInput(name: ToolName, input: unknown): {
|
|
|
173
173
|
} | {
|
|
174
174
|
title: string;
|
|
175
175
|
kind: "text" | "image";
|
|
176
|
-
source?: string | undefined;
|
|
177
176
|
content?: string | undefined;
|
|
177
|
+
source?: string | undefined;
|
|
178
178
|
tags?: string[] | undefined;
|
|
179
179
|
imageUrl?: string | undefined;
|
|
180
180
|
note?: string | undefined;
|
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
|
package/dist/server/http.js
CHANGED
|
@@ -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 {};
|
package/dist/workflow/manager.js
CHANGED
|
@@ -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(
|
|
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
|
|
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) {
|