@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. 客户只需首次选择一个本机目录。Agent 10 秒检查交付,先写 `.part`,校验大小与 SHA-256 后原子改名;清单已记录的批次序号不会重复下载,网页关闭后仍会继续。
90
+ 4. 客户先选择一个本机目录,再对需要的批次点击“一键下载本批全部”。登录另一台电脑、选择文件夹或查看历史批次都不会触发下载。Agent 以最多四路并行写 `.part`,校验大小与 SHA-256 后原子改名;清单已记录且校验有效的批次序号不会重复下载。
91
91
 
92
92
  本机持久状态默认保存在 `~/.infinite-canvas/workflow-state.json`,权限设为仅当前用户可读写。这里包含短期能力令牌和本机路径,不应上传、提交到 Git 或发到聊天中。
93
93
 
@@ -1,4 +1,3 @@
1
1
  /** Flow C 脚本分段的唯一上限和自动降级顺序,MCP 与调度器必须共用。 */
2
2
  export declare const FLOW_C_SCRIPT_CHUNK_SIZES: readonly [30, 15, 10];
3
3
  export declare const FLOW_C_SCRIPT_CHUNK_MAX: 30;
4
- export declare const FLOW_C_DOWNLOAD_POLL_MS = 3000;
@@ -1,4 +1,3 @@
1
1
  /** Flow C 脚本分段的唯一上限和自动降级顺序,MCP 与调度器必须共用。 */
2
2
  export const FLOW_C_SCRIPT_CHUNK_SIZES = [30, 15, 10];
3
3
  export const FLOW_C_SCRIPT_CHUNK_MAX = FLOW_C_SCRIPT_CHUNK_SIZES[0];
4
- export const FLOW_C_DOWNLOAD_POLL_MS = 3_000;
@@ -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: {
@@ -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 { FLOW_C_DOWNLOAD_POLL_MS, FLOW_C_SCRIPT_CHUNK_MAX, FLOW_C_SCRIPT_CHUNK_SIZES } from "./constants.js";
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: previous?.status === "complete" ? "complete" : "waiting",
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 (this.syncingDownloads || !this.state.downloadDirectory)
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
- const records = Object.values(this.state.downloads).filter((record) => !onlyBatchId || record.batchId === onlyBatchId);
285
- for (const record of records)
286
- await this.syncDownloadRecord(record);
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
- for (const delivery of batch.deliveries || []) {
305
- await this.saveDelivery(batch, delivery);
306
- if (!record.downloadedOrdinals.includes(delivery.ordinal))
307
- record.downloadedOrdinals.push(delivery.ordinal);
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
- ? `已自动保存 ${record.downloadedOrdinals.length} 条视频;${finished ? "本批次已结束" : "继续等待新视频"}`
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",