@xiaohhhh1/canvas-agent 0.4.3 → 0.4.5
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.d.ts +7 -1
- package/dist/agent/codex.js +6 -3
- package/dist/server/ensure-http.d.ts +2 -1
- package/dist/server/ensure-http.js +37 -5
- package/dist/server/http.js +11 -3
- package/dist/server/mcp.js +3 -2
- package/dist/workflow/constants.d.ts +3 -0
- package/dist/workflow/constants.js +3 -0
- package/dist/workflow/manager.js +28 -11
- package/package.json +1 -1
package/dist/agent/codex.d.ts
CHANGED
|
@@ -9,9 +9,15 @@ type CodexRunOptions = {
|
|
|
9
9
|
onTurn?: (turnId: string) => void;
|
|
10
10
|
onFinish?: () => void;
|
|
11
11
|
};
|
|
12
|
+
export type CodexRunResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
12
18
|
export { summarizeCodexThread } from "./codex-history.js";
|
|
13
19
|
/** 将 Codex turn 加入串行队列并等待执行完成。 */
|
|
14
|
-
export declare function runCodexTurn(prompt: string, emit: AgentEmit, attachments?: AgentAttachment[], options?: CodexRunOptions): Promise<
|
|
20
|
+
export declare function runCodexTurn(prompt: string, emit: AgentEmit, attachments?: AgentAttachment[], options?: CodexRunOptions): Promise<CodexRunResult>;
|
|
15
21
|
/** 中断当前线程正在执行的 Codex turn。 */
|
|
16
22
|
export declare function interruptCodexTurn(threadId?: string): Promise<boolean>;
|
|
17
23
|
/** 回复当前 app-server 的待处理权限请求。 */
|
package/dist/agent/codex.js
CHANGED
|
@@ -14,9 +14,9 @@ export { summarizeCodexThread } from "./codex-history.js";
|
|
|
14
14
|
/** 将 Codex turn 加入串行队列并等待执行完成。 */
|
|
15
15
|
export async function runCodexTurn(prompt, emit, attachments = [], options = {}) {
|
|
16
16
|
if (!prompt.trim())
|
|
17
|
-
return;
|
|
17
|
+
return { ok: false, error: "Codex prompt is empty" };
|
|
18
18
|
codexQueue = codexQueue.catch(() => undefined).then(() => runCodexTurnNow(prompt, emit, attachments, options));
|
|
19
|
-
await codexQueue;
|
|
19
|
+
return await codexQueue;
|
|
20
20
|
}
|
|
21
21
|
/** 中断当前线程正在执行的 Codex turn。 */
|
|
22
22
|
export async function interruptCodexTurn(threadId) {
|
|
@@ -115,10 +115,13 @@ async function runCodexTurnNow(prompt, emit, attachments, options) {
|
|
|
115
115
|
unmaterializedThreadIds.delete(threadId);
|
|
116
116
|
await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn);
|
|
117
117
|
}
|
|
118
|
+
return { ok: true };
|
|
118
119
|
}
|
|
119
120
|
catch (error) {
|
|
120
121
|
logger.error("Codex turn failed", error);
|
|
121
|
-
|
|
122
|
+
const message = errorMessage(error);
|
|
123
|
+
emit("agent_error", { message });
|
|
124
|
+
return { ok: false, error: message };
|
|
122
125
|
}
|
|
123
126
|
finally {
|
|
124
127
|
options.onFinish?.();
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { loadConfig } from "../config.js";
|
|
3
|
-
/**
|
|
2
|
+
import { loadConfig, VERSION } from "../config.js";
|
|
3
|
+
/** Ensure the local HTTP worker is running the same version as the MCP process. */
|
|
4
4
|
export async function ensureHttpServer() {
|
|
5
5
|
const config = loadConfig(true);
|
|
6
|
-
|
|
6
|
+
const current = await probeHealth(config.url);
|
|
7
|
+
if (current.ok && current.version === VERSION)
|
|
7
8
|
return;
|
|
9
|
+
if (current.ok) {
|
|
10
|
+
const stopped = await requestShutdown(config.url, config.token);
|
|
11
|
+
if (!stopped)
|
|
12
|
+
throw new Error(`本机 Canvas Agent ${current.version || "旧版本"} 仍在运行,请关闭后重试以升级到 ${VERSION}`);
|
|
13
|
+
for (let attempt = 0; attempt < 40 && (await probeHealth(config.url)).ok; attempt += 1) {
|
|
14
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
15
|
+
}
|
|
16
|
+
if ((await probeHealth(config.url)).ok)
|
|
17
|
+
throw new Error("旧版 Canvas Agent 未能正常退出,请关闭后重试");
|
|
18
|
+
}
|
|
8
19
|
const entry = process.argv[1];
|
|
9
20
|
if (!entry)
|
|
10
21
|
throw new Error("无法定位 Canvas Agent 启动文件");
|
|
@@ -12,14 +23,35 @@ export async function ensureHttpServer() {
|
|
|
12
23
|
child.unref();
|
|
13
24
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
14
25
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
15
|
-
|
|
26
|
+
const next = await probeHealth(config.url);
|
|
27
|
+
if (next.ok && next.version === VERSION)
|
|
16
28
|
return;
|
|
17
29
|
}
|
|
18
30
|
throw new Error("本机 Canvas Agent 后台服务启动失败");
|
|
19
31
|
}
|
|
20
|
-
|
|
32
|
+
export function healthVersion(value) {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
34
|
+
return "";
|
|
35
|
+
return typeof value.version === "string" ? String(value.version) : "";
|
|
36
|
+
}
|
|
37
|
+
async function probeHealth(url) {
|
|
21
38
|
try {
|
|
22
39
|
const response = await fetch(new URL("/health", url), { signal: AbortSignal.timeout(800) });
|
|
40
|
+
if (!response.ok)
|
|
41
|
+
return { ok: false, version: "" };
|
|
42
|
+
return { ok: true, version: healthVersion(await response.json()) };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { ok: false, version: "" };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function requestShutdown(url, token) {
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(new URL("/agent/shutdown", url), {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "x-canvas-agent-token": token },
|
|
53
|
+
signal: AbortSignal.timeout(1500),
|
|
54
|
+
});
|
|
23
55
|
return response.ok;
|
|
24
56
|
}
|
|
25
57
|
catch {
|
package/dist/server/http.js
CHANGED
|
@@ -5,7 +5,7 @@ import express from "express";
|
|
|
5
5
|
import { runClaudeTurn } from "../agent/claude.js";
|
|
6
6
|
import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js";
|
|
7
7
|
import { CanvasSession } from "../canvas/session.js";
|
|
8
|
-
import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace } from "../config.js";
|
|
8
|
+
import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, VERSION } from "../config.js";
|
|
9
9
|
import { startRelayBridge } from "../relay-bridge.js";
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
11
|
import { windowsRootExecutable, windowsSystemExecutable } from "../utils/windows.js";
|
|
@@ -53,13 +53,21 @@ export function startHttpServer() {
|
|
|
53
53
|
return void res.json({});
|
|
54
54
|
next();
|
|
55
55
|
});
|
|
56
|
-
app.get("/health", (_req, res) => res.json(session.health()));
|
|
56
|
+
app.get("/health", (_req, res) => res.json({ ...session.health(), version: VERSION }));
|
|
57
57
|
app.get("/config", (_req, res) => res.json({ ok: true, url: config.url, hasToken: true }));
|
|
58
58
|
app.use((req, res, next) => {
|
|
59
59
|
if (validToken(req, requestUrl(req, config), config.token))
|
|
60
60
|
return next();
|
|
61
61
|
res.status(401).json({ ok: false, error: "invalid token" });
|
|
62
62
|
});
|
|
63
|
+
let httpServer;
|
|
64
|
+
app.post("/agent/shutdown", (_req, res) => {
|
|
65
|
+
res.json({ ok: true, version: VERSION });
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
httpServer?.close(() => process.exit(0));
|
|
68
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
69
|
+
}, 50).unref();
|
|
70
|
+
});
|
|
63
71
|
app.get("/events", (req, res) => session.openEvents(requestUrl(req, config), res));
|
|
64
72
|
app.post("/canvas/state", (req, res) => {
|
|
65
73
|
session.updateState(req.body, String(req.query.clientId || "") || undefined);
|
|
@@ -259,7 +267,7 @@ export function startHttpServer() {
|
|
|
259
267
|
logger.error("HTTP request failed", { method: req.method, path: req.path, error });
|
|
260
268
|
res.status(500).json({ ok: false, error: error.message });
|
|
261
269
|
});
|
|
262
|
-
app.listen(port, "127.0.0.1", () => {
|
|
270
|
+
httpServer = app.listen(port, "127.0.0.1", () => {
|
|
263
271
|
console.log("Infinite Canvas Agent");
|
|
264
272
|
console.log(`Local URL: ${config.url}`);
|
|
265
273
|
console.log(`Connect token: ${config.token}`);
|
package/dist/server/mcp.js
CHANGED
|
@@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { toolDescriptions, toolInputSchemas, toolNames } from "../canvas/schemas.js";
|
|
5
5
|
import { AGENT_PROMPT, loadConfig, VERSION } from "../config.js";
|
|
6
|
+
import { FLOW_C_SCRIPT_CHUNK_MAX } from "../workflow/constants.js";
|
|
6
7
|
/** 启动通过标准输入输出通信的 MCP 服务。 */
|
|
7
8
|
export async function startMcpServer() {
|
|
8
9
|
const config = loadConfig(true);
|
|
@@ -22,7 +23,7 @@ function registerWorkflowTools(server, config) {
|
|
|
22
23
|
inputSchema: { handoffId: z.string().uuid().describe("网站创建的脚本交接 ID") },
|
|
23
24
|
}, async ({ handoffId }) => workflowTool(config, `/agent/workflow/script-handoffs/${encodeURIComponent(handoffId)}/task`, { method: "GET" }));
|
|
24
25
|
server.registerTool("flow_c_submit_script_chunk", {
|
|
25
|
-
description:
|
|
26
|
+
description: `把本段独立完成的 Flow C 高质量脚本持久化回传给网站。每次 1–${FLOW_C_SCRIPT_CHUNK_MAX} 条;成功后再创作下一段。`,
|
|
26
27
|
inputSchema: {
|
|
27
28
|
handoffId: z.string().uuid().describe("脚本交接 ID"),
|
|
28
29
|
jobs: z.array(z.object({
|
|
@@ -30,7 +31,7 @@ function registerWorkflowTools(server, config) {
|
|
|
30
31
|
productIndex: z.number().int().nonnegative(),
|
|
31
32
|
sellingFormId: z.string().min(1).max(100),
|
|
32
33
|
script: z.string().min(40).max(20_000),
|
|
33
|
-
})).min(1).max(
|
|
34
|
+
})).min(1).max(FLOW_C_SCRIPT_CHUNK_MAX),
|
|
34
35
|
},
|
|
35
36
|
}, async ({ handoffId, jobs }) => workflowTool(config, `/agent/workflow/script-handoffs/${encodeURIComponent(handoffId)}/draft-chunks`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jobs }) }));
|
|
36
37
|
}
|
package/dist/workflow/manager.js
CHANGED
|
@@ -9,8 +9,8 @@ import { 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
|
+
import { FLOW_C_SCRIPT_CHUNK_MAX, FLOW_C_SCRIPT_CHUNK_SIZES } from "./constants.js";
|
|
12
13
|
const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
|
|
13
|
-
const SCRIPT_CHUNK_SIZE = 30;
|
|
14
14
|
const DOWNLOAD_POLL_MS = 10_000;
|
|
15
15
|
/** 本机持久化的 Flow C 控制器:脚本交给本机 Codex,视频直接落盘。 */
|
|
16
16
|
export class WorkflowManager {
|
|
@@ -85,8 +85,8 @@ export class WorkflowManager {
|
|
|
85
85
|
/** MCP 分段回传脚本,中心接口再次执行数量、产品索引和脚本完整性校验。 */
|
|
86
86
|
async submitScriptChunk(idValue, jobsValue) {
|
|
87
87
|
const record = this.scriptRecord(workflowId(idValue, "脚本交接 ID"));
|
|
88
|
-
if (!Array.isArray(jobsValue) || !jobsValue.length || jobsValue.length >
|
|
89
|
-
throw new Error(`每段必须包含 1–${
|
|
88
|
+
if (!Array.isArray(jobsValue) || !jobsValue.length || jobsValue.length > FLOW_C_SCRIPT_CHUNK_MAX)
|
|
89
|
+
throw new Error(`每段必须包含 1–${FLOW_C_SCRIPT_CHUNK_MAX} 条脚本`);
|
|
90
90
|
const jobs = jobsValue;
|
|
91
91
|
const data = await commerceJson(`${record.apiBase}/workflow-script-handoffs/${encodeURIComponent(record.id)}/draft-chunks`, record.accessToken, "x-workflow-handoff-token", { method: "POST", body: JSON.stringify({ jobs }) });
|
|
92
92
|
if (Array.isArray(data.receivedOrdinals))
|
|
@@ -217,23 +217,39 @@ export class WorkflowManager {
|
|
|
217
217
|
this.save();
|
|
218
218
|
}
|
|
219
219
|
while (record.receivedOrdinals.length < task.requested_count) {
|
|
220
|
-
const missing = missingOrdinals(task.requested_count, record.receivedOrdinals).slice(0, SCRIPT_CHUNK_SIZE);
|
|
221
|
-
if (!missing.length)
|
|
222
|
-
break;
|
|
223
220
|
let progressed = false;
|
|
224
|
-
|
|
221
|
+
let lastRange = "";
|
|
222
|
+
let lastError = "";
|
|
223
|
+
for (const [attemptIndex, chunkSize] of FLOW_C_SCRIPT_CHUNK_SIZES.entries()) {
|
|
224
|
+
const missing = missingOrdinals(task.requested_count, record.receivedOrdinals).slice(0, chunkSize);
|
|
225
|
+
if (!missing.length) {
|
|
226
|
+
progressed = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
lastRange = `${missing[0]}–${missing.at(-1)}`;
|
|
225
230
|
const before = record.receivedOrdinals.length;
|
|
226
231
|
record.attempts += 1;
|
|
227
232
|
record.message = `本机 Codex 正在写第 ${missing[0]}–${missing.at(-1)} 条(总计 ${task.requested_count} 条)`;
|
|
228
233
|
record.updatedAt = now();
|
|
229
234
|
this.save();
|
|
230
|
-
await runCodexTurn(scriptChunkPrompt(id, task, missing), this.emit, [], { threadId: record.threadId, cwd: workspace.workspacePath, permissionMode: "full", onThread: (threadId) => { record.threadId = threadId; this.save(); } });
|
|
235
|
+
const result = await runCodexTurn(scriptChunkPrompt(id, task, missing), this.emit, [], { threadId: record.threadId, cwd: workspace.workspacePath, permissionMode: "full", onThread: (threadId) => { record.threadId = threadId; this.save(); } });
|
|
236
|
+
await this.scriptTask(id);
|
|
231
237
|
progressed = record.receivedOrdinals.length > before;
|
|
232
|
-
if (
|
|
233
|
-
|
|
238
|
+
if (progressed)
|
|
239
|
+
break;
|
|
240
|
+
// Keep provider/client details out of the browser-facing state. The
|
|
241
|
+
// underlying Codex runner already records the technical error locally.
|
|
242
|
+
lastError = result.ok ? "Codex 未调用回传工具" : "Codex 本轮执行失败";
|
|
243
|
+
if (attemptIndex < FLOW_C_SCRIPT_CHUNK_SIZES.length - 1) {
|
|
244
|
+
const nextSize = FLOW_C_SCRIPT_CHUNK_SIZES[attemptIndex + 1];
|
|
245
|
+
record.message = `第 ${lastRange} 条未成功回传,正在换新会话并缩小为每段 ${nextSize} 条重试`;
|
|
246
|
+
const thread = await startCodexThread(this.emit, workspace.workspacePath, "full");
|
|
247
|
+
record.threadId = String(thread.id || "");
|
|
248
|
+
this.save();
|
|
249
|
+
}
|
|
234
250
|
}
|
|
235
251
|
if (!progressed)
|
|
236
|
-
throw new Error(`本机 Codex
|
|
252
|
+
throw new Error(`本机 Codex 已自动换会话并降级到每段 10 条,仍未回传第 ${lastRange} 条${lastError ? `(${lastError})` : ""},请点击重试`);
|
|
237
253
|
}
|
|
238
254
|
const finalTask = await this.scriptTask(id);
|
|
239
255
|
if (finalTask.status === "ready" && record.receivedOrdinals.length === task.requested_count) {
|
|
@@ -374,6 +390,7 @@ function scriptChunkPrompt(id, task, ordinals) {
|
|
|
374
390
|
必须使用 MCP 工具 flow_c_get_script_task 读取完整任务,再只创作 ordinal ${ordinals[0]} 到 ${ordinals.at(-1)}(精确列表:${ordinals.join(", ")})。
|
|
375
391
|
本段 productIndex 必须严格按此映射填写:${scriptProductAssignments(task.product_quantities, ordinals)}。不得凭产品名称猜测或把相邻产品编号混用。
|
|
376
392
|
脚本质量绝不能因批量而降低:每条都必须独立构思、完整、真实合规,严格遵守任务 instructions、产品图片顺序和 productIndex;使用目标市场 ${task.market} 的自然本地语言与偏快但清晰的 10 秒节奏。
|
|
393
|
+
用户没有指定带货方向时,不得随意只用一种泛化形式;必须按任务 instructions 中“已批准的创意方向”逐条做产品适配轮换。轮换必须按每个产品自己的序号连续计算,不能因 30/15/10 条分段、换会话或跨产品边界而从第一个方向重新开始;让同一产品在重复某一方向前优先覆盖其他适配方向。
|
|
377
394
|
写完后必须调用 flow_c_submit_script_chunk 一次回传这 ${ordinals.length} 条,handoffId=${id}。不要创建付费批次,不要调用供应商模型,不要在聊天输出大段 JSON。工具返回成功后仅简短结束。`;
|
|
378
395
|
}
|
|
379
396
|
function scriptProductAssignments(productQuantities, ordinals) {
|