@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.
- package/dist/agent/codex-client.d.ts +2 -0
- package/dist/agent/codex-client.js +11 -0
- package/dist/agent/codex.d.ts +2 -0
- package/dist/agent/codex.js +8 -0
- 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
|
@@ -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);
|
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/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) {
|