@xiaohhhh1/canvas-agent 0.4.14 → 0.4.15
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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Infinite Canvas Agent
|
|
2
2
|
|
|
3
|
-
本地 Canvas Agent 用来连接网站和用户自己电脑上的 Codex。它还负责持久化 Flow C 脚本队列、分段调用本机 Codex
|
|
3
|
+
本地 Canvas Agent 用来连接网站和用户自己电脑上的 Codex。它还负责持久化 Flow C 脚本队列、分段调用本机 Codex、自动回传草案,以及在用户明确点击“一键下载本批全部”后把完成视频校验并保存到所选磁盘目录。
|
|
4
4
|
|
|
5
5
|
## 启动
|
|
6
6
|
|
|
@@ -87,7 +87,7 @@ npx -y @xiaohhhh1/canvas-agent mcp
|
|
|
87
87
|
1. 客户在网站添加一个或多个产品,按顺序上传每个产品 1–5 张图片并填写数量。
|
|
88
88
|
2. 点击“交给本机 Codex 写全部脚本”。本机助手每次只处理 10 条并持久化进度,断网或重启后可继续;脚本完成会自动回传网站,不创建付费任务。
|
|
89
89
|
3. 客户审阅脚本并确认费用后,中心 `workflow-runner` 才执行故事板和视频生成。
|
|
90
|
-
4.
|
|
90
|
+
4. 客户先选择一个本机目录,再对需要的批次点击“一键下载本批全部”。登录另一台电脑、选择文件夹或查看历史批次都不会触发下载。Agent 以最多四路并行写 `.part`,校验大小与 SHA-256 后原子改名;清单已记录且校验有效的批次序号不会重复下载。
|
|
91
91
|
|
|
92
92
|
本机持久状态默认保存在 `~/.infinite-canvas/workflow-state.json`,权限设为仅当前用户可读写。这里包含短期能力令牌和本机路径,不应上传、提交到 Git 或发到聊天中。
|
|
93
93
|
|
|
@@ -22,8 +22,8 @@ export declare class WorkflowManager {
|
|
|
22
22
|
private runningScripts;
|
|
23
23
|
private scriptQueueRunning;
|
|
24
24
|
private syncingDownloads;
|
|
25
|
+
private pendingDownloadBatchIds;
|
|
25
26
|
private directorySelection?;
|
|
26
|
-
private downloadTimer?;
|
|
27
27
|
constructor(config: CanvasAgentConfig, emit: AgentEmit);
|
|
28
28
|
/** 接收网站创建的短期交接能力并立即启动本机 Codex。 */
|
|
29
29
|
enqueueScript(input: {
|
package/dist/workflow/manager.js
CHANGED
|
@@ -9,7 +9,7 @@ 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 {
|
|
12
|
+
import { FLOW_C_SCRIPT_CHUNK_MAX, FLOW_C_SCRIPT_CHUNK_SIZES } from "./constants.js";
|
|
13
13
|
const STATE_FILE = path.join(CONFIG_DIR, "workflow-state.json");
|
|
14
14
|
/** 本机持久化的 Flow C 控制器:脚本交给本机 Codex,视频直接落盘。 */
|
|
15
15
|
export class WorkflowManager {
|
|
@@ -19,8 +19,8 @@ export class WorkflowManager {
|
|
|
19
19
|
runningScripts = new Set();
|
|
20
20
|
scriptQueueRunning = false;
|
|
21
21
|
syncingDownloads = false;
|
|
22
|
+
pendingDownloadBatchIds = new Set();
|
|
22
23
|
directorySelection;
|
|
23
|
-
downloadTimer;
|
|
24
24
|
constructor(config, emit) {
|
|
25
25
|
this.config = config;
|
|
26
26
|
this.emit = emit;
|
|
@@ -28,9 +28,6 @@ export class WorkflowManager {
|
|
|
28
28
|
if (record.status === "queued" || record.status === "running")
|
|
29
29
|
this.scheduleScript(record.id);
|
|
30
30
|
}
|
|
31
|
-
this.downloadTimer = setInterval(() => void this.syncDownloads(), FLOW_C_DOWNLOAD_POLL_MS);
|
|
32
|
-
this.downloadTimer.unref?.();
|
|
33
|
-
void this.syncDownloads();
|
|
34
31
|
}
|
|
35
32
|
/** 接收网站创建的短期交接能力并立即启动本机 Codex。 */
|
|
36
33
|
enqueueScript(input) {
|
|
@@ -127,6 +124,7 @@ export class WorkflowManager {
|
|
|
127
124
|
}
|
|
128
125
|
clearDownloadDirectory() {
|
|
129
126
|
delete this.state.downloadDirectory;
|
|
127
|
+
this.pendingDownloadBatchIds.clear();
|
|
130
128
|
this.save();
|
|
131
129
|
return this.downloadState();
|
|
132
130
|
}
|
|
@@ -137,15 +135,15 @@ export class WorkflowManager {
|
|
|
137
135
|
batchId,
|
|
138
136
|
apiBase: commerceApiBase(input.apiBase),
|
|
139
137
|
accessToken: secret(input.accessToken, "下载能力令牌"),
|
|
140
|
-
status:
|
|
138
|
+
status: "waiting",
|
|
141
139
|
downloadedOrdinals: previous?.downloadedOrdinals || [],
|
|
142
140
|
market: previous?.market,
|
|
143
141
|
expiresAt: String(input.expiresAt || previous?.expiresAt || "") || undefined,
|
|
144
|
-
message: this.state.downloadDirectory ? "
|
|
142
|
+
message: this.state.downloadDirectory ? "已开始手动下载本批全部成片" : "请先选择本机保存文件夹",
|
|
145
143
|
updatedAt: now(),
|
|
146
144
|
};
|
|
147
145
|
this.save();
|
|
148
|
-
void this.syncDownloads();
|
|
146
|
+
void this.syncDownloads(batchId);
|
|
149
147
|
return publicDownload(this.state.downloads[batchId]);
|
|
150
148
|
}
|
|
151
149
|
async syncDownload(batchIdValue) {
|
|
@@ -164,7 +162,6 @@ export class WorkflowManager {
|
|
|
164
162
|
throw new Error("选择的路径不是文件夹");
|
|
165
163
|
this.state.downloadDirectory = resolved;
|
|
166
164
|
this.save();
|
|
167
|
-
void this.syncDownloads();
|
|
168
165
|
return this.downloadState();
|
|
169
166
|
}
|
|
170
167
|
scheduleScript(_id) {
|
|
@@ -277,13 +274,24 @@ export class WorkflowManager {
|
|
|
277
274
|
}
|
|
278
275
|
}
|
|
279
276
|
async syncDownloads(onlyBatchId) {
|
|
280
|
-
if (
|
|
277
|
+
if (!this.state.downloadDirectory)
|
|
278
|
+
return;
|
|
279
|
+
if (onlyBatchId)
|
|
280
|
+
this.pendingDownloadBatchIds.add(onlyBatchId);
|
|
281
|
+
else
|
|
282
|
+
for (const record of Object.values(this.state.downloads))
|
|
283
|
+
this.pendingDownloadBatchIds.add(record.batchId);
|
|
284
|
+
if (this.syncingDownloads)
|
|
281
285
|
return;
|
|
282
286
|
this.syncingDownloads = true;
|
|
283
287
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
288
|
+
while (this.pendingDownloadBatchIds.size) {
|
|
289
|
+
const batchId = this.pendingDownloadBatchIds.values().next().value;
|
|
290
|
+
this.pendingDownloadBatchIds.delete(batchId);
|
|
291
|
+
const record = this.state.downloads[batchId];
|
|
292
|
+
if (record)
|
|
293
|
+
await this.syncDownloadRecord(record);
|
|
294
|
+
}
|
|
287
295
|
}
|
|
288
296
|
finally {
|
|
289
297
|
this.syncingDownloads = false;
|
|
@@ -293,7 +301,7 @@ export class WorkflowManager {
|
|
|
293
301
|
async syncDownloadRecord(record) {
|
|
294
302
|
if (record.expiresAt && Date.parse(record.expiresAt) <= Date.now()) {
|
|
295
303
|
record.status = "expired";
|
|
296
|
-
record.message = "
|
|
304
|
+
record.message = "下载授权已过期;请在网站重新点击一键下载全部";
|
|
297
305
|
return;
|
|
298
306
|
}
|
|
299
307
|
try {
|
|
@@ -301,21 +309,30 @@ export class WorkflowManager {
|
|
|
301
309
|
const data = await commerceJson(`${record.apiBase}/workflow-downloads/${encodeURIComponent(record.batchId)}`, record.accessToken, "x-workflow-download-token");
|
|
302
310
|
const batch = data.batch;
|
|
303
311
|
record.market = batch.market;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
312
|
+
const deliveries = batch.deliveries || [];
|
|
313
|
+
let cursor = 0;
|
|
314
|
+
const workers = Array.from({ length: Math.min(4, deliveries.length) }, async () => {
|
|
315
|
+
while (cursor < deliveries.length) {
|
|
316
|
+
const delivery = deliveries[cursor++];
|
|
317
|
+
await this.saveDelivery(batch, delivery);
|
|
318
|
+
if (!record.downloadedOrdinals.includes(delivery.ordinal))
|
|
319
|
+
record.downloadedOrdinals.push(delivery.ordinal);
|
|
320
|
+
record.message = `正在保存本批全部成片:${record.downloadedOrdinals.length}/${deliveries.length}`;
|
|
321
|
+
record.updatedAt = now();
|
|
322
|
+
this.save();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
await Promise.all(workers);
|
|
309
326
|
record.downloadedOrdinals.sort((a, b) => a - b);
|
|
310
327
|
const finished = ["completed", "cancelled", "failed"].includes(batch.status);
|
|
311
328
|
record.status = finished && record.downloadedOrdinals.length >= (batch.deliveries?.length || 0) ? "complete" : "waiting";
|
|
312
329
|
record.message = batch.deliveries?.length
|
|
313
|
-
?
|
|
314
|
-
: finished ? "批次已结束,暂无可下载视频" : "
|
|
330
|
+
? `已保存本批 ${record.downloadedOrdinals.length} 条成片;${finished ? "本批次已结束" : "如有新成片请再次点击下载"}`
|
|
331
|
+
: finished ? "批次已结束,暂无可下载视频" : "当前暂无成片可下载";
|
|
315
332
|
}
|
|
316
333
|
catch (error) {
|
|
317
334
|
record.status = /expired|not found/i.test(error instanceof Error ? error.message : "") ? "expired" : "error";
|
|
318
|
-
record.message = error instanceof Error ? error.message : "
|
|
335
|
+
record.message = error instanceof Error ? error.message : "下载失败,请再次点击下载";
|
|
319
336
|
logger.warn("Local Flow C download sync failed", { batchId: record.batchId, error: record.message });
|
|
320
337
|
}
|
|
321
338
|
finally {
|