@uzooai/anime-ai-cli 0.6.2 → 0.6.4
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/.agents/skills/uzoo-anime-cli/SKILL.md +2 -1
- package/.agents/skills/uzoo-anime-cli/references/commands.md +5 -4
- package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +35 -2
- package/.agents/skills/uzoo-anime-cli/references/interaction-workflow.md +1 -0
- package/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/cli.js +32 -7
- package/src/generation-graph.js +64 -16
- package/src/workflows.js +2 -1
|
@@ -30,6 +30,7 @@ If a newer stable version exists, show the installed and available versions and
|
|
|
30
30
|
| Explain node purposes, toolbar tools or missing CLI features | [Node capabilities](references/node-capabilities.md) |
|
|
31
31
|
| Upload, save node results to library, reuse assets, download material originals, arrange/group and connect nodes | [Material and graph workflow](references/material-graph-workflow.md) |
|
|
32
32
|
| Prepare references, estimate, generate, synchronize/recover results, finalize an existing Seedance draft, or select/add ordinary image/video history | [Generation workflow](references/generation-workflow.md) |
|
|
33
|
+
| Ordinary generation submission limits, duplicate requests, or HTTP 429 on submit | Run `uzoo-anime generation limits` for the offline reference and follow [submission limits and recovery](references/generation-workflow.md#生成提交限额与429处理) |
|
|
33
34
|
| Automatically apply sd25-pe while preparing Seedance 2.5 video generation; no explicit skill invocation needed | [Prompt handoff](references/generation-workflow.md#seedance-25提示词优化sd25-pe) |
|
|
34
35
|
| Explicit Seedance image/video material review, status and recovery | [Material review workflow](references/seedance-review-workflow.md) |
|
|
35
36
|
| Account consent, first use/updated agreement or API code 4401 | [Agreement workflow](references/agreement-workflow.md) |
|
|
@@ -68,7 +69,7 @@ Saving a generation/processing result to the canvas does not save it to the mate
|
|
|
68
69
|
3. Before business discovery or writes, ask whether to use personal or team space unless the user already chose it for this task. Do not infer personal space from missing flags or an empty list. For team space, resolve the user's choice against actual account `teams`; if there is only one available team, announce and use it; if several remain possible, show names/UIDs and ask which. No available team means stop, not fallback to personal. Confirm a project only when relevant, never guess its ID. Keep the confirmed choice for this task; ask again when the user switches context.
|
|
69
70
|
4. Follow the [canvas workflow](references/canvas-workflow.md) to list the chosen scope and ask via a card whether to create or use an existing canvas when the target is unspecified. For creation, obtain a name or explicit acceptance of the suggested name; then create once and read back ownership. A canvas-only request ends there: no model discovery, nodes, uploads or generation. For node creation, follow the [node workflow](references/node-workflow.md); directly create ordinary nodes only, and interpret “all nodes” as all ordinary node types. Derived nodes must come from a real workflow, not empty placeholders or injected roles. A node-only request ends after read-back verification, without requiring models or materials for empty ordinary nodes. For a generation request, use model options in that same space, collect the model and applicable settings through cards unless already specified or explicitly delegated, save the selected node configuration and run generation prepare. Do not hand-assemble an ordinary generation request to bypass preparation. Dedicated workflows use their own verified request builders. Use `node update` for input/position changes; read [commands](references/commands.md) for the node patch schema. Team model visibility and promotions can differ.
|
|
70
71
|
5. Before node writes, upload, estimation, submission or recovery, read the target canvas and verify its team_uid matches the confirmed space/team (personal means no team_uid). Stop on mismatch; do not transfer ownership or silently switch wallets. A generation uses the canvas’s actual ownership, not a client billing override. After the user selects node parameters, use a confirmation card to show the space, team/project, personal or team wallet source and estimated OO cost and wait for the user’s confirmation for each submission. Changed parameters or price require confirmation again. Never retry a timed-out submission blindly.
|
|
71
|
-
6.
|
|
72
|
+
6. For an authorized ordinary canvas generation, keep the task open until the original node is updated and verified. Prefer `generation wait <id>`; if polling with `generation get`, run `generation sync <id>` on the same completed task before delivery. Verify the saved node with `canvas get`. After interruption, find the original task and last sync state with `generation operations` on the same profile. Task completion or download alone does not verify canvas persistence. Follow [canvas result delivery](references/generation-workflow.md#生成结果回填与交付) for recovery and read-only requests.
|
|
72
73
|
|
|
73
74
|
The following segmentation procedure is retained for historical maintenance and explicit future resumption only; it is not an active development or acceptance task while repaint is paused. Follow the repaint reference instead of improvising API calls or generic node writes. `repaint start` first persists a preview of the actual wallet and analysis fee; require explicit approval before `--yes`. Review the real segment plan before `repaint confirm --yes`, then wait for CLI to save the derived chain. A segment chain contains video generation nodes, not generated videos. Further paid actions need their own parameters, estimate and approval.
|
|
74
75
|
|
|
@@ -57,12 +57,13 @@
|
|
|
57
57
|
| `generation protocol-status` | 只读查询视频《统一协议》及《Seedance2.0模型使用承诺书》的 required/agreed/version;status=awaiting_agreement 与 api_code=4401 表示待签署 |
|
|
58
58
|
| `generation protocol-open` | 打开独立本机协议阅读确认页;可选 --browser default/msedge/chrome --timeout MS;本人勾选点击后记录并查询核验,不提交生成 |
|
|
59
59
|
| `generation estimate` | `--input request.json`;保留文档/节点上下文,返回费用和 `will_intercept` |
|
|
60
|
+
| `generation limits` | 可选 `--format json\|table`;离线查看普通生成提交限额和等待建议,不需要登录,不查询实时剩余额度或提交任务。遇到 429 见 [生成提交限额与处理](generation-workflow.md#生成提交限额与429处理) |
|
|
60
61
|
| `generation submit` | `--input request.json --operation NAME` 默认只预估;Seedance 预览额外返回 protocol;--yes 在需要协议时打开阅读页并等待本人确认,核验后继续原操作;可选 --browser 和 --agreement-timeout;首次 --yes 必须带 `--expected-cost OO` 与当次确认费用严格比对,不一致停止;返回 `{operation, estimate, task}`。保存 operation,任务 ID 是 `task.task_id` |
|
|
61
62
|
| `generation recover NAME` | 有已知任务 ID 时只查询该任务;未知提交结果时默认只预估,加 `--yes` 以原请求和原幂等键恢复;原记录无确认金额或价格变化时必须重新卡片确认并带 `--expected-cost OO`,已有有效金额且报价未变可沿用。新授权金额先落原记录,不改请求或键。不得改用新 operation |
|
|
62
|
-
| `generation operations` |
|
|
63
|
-
| `generation get ID` / `logs ID` |
|
|
64
|
-
| `generation wait ID` | `--interval MS` 默认 3000,`--timeout MS` 默认 600000;轮询原任务,completed
|
|
65
|
-
| `generation sync ID` | 读取原任务及最新画布,保存真实任务 ID/状态/媒体结果或文本输出;无生成提交;CAS
|
|
63
|
+
| `generation operations` | 只读列出本地 operation、taskId、状态与创建时间;已知任务含 doc_id、node_id 和上次 canvas_sync(unverified/pending/verified);失败时包含脱敏 lastFailure,不输出原始请求,不主动查询或写画布 |
|
|
64
|
+
| `generation get ID` / `logs ID` | 只读返回任务或日志;`get` 轮询至 completed 后,如需交付到画布且尚未回填,使用同一任务 ID 执行 sync |
|
|
65
|
+
| `generation wait ID` | `--interval MS` 默认 3000,`--timeout MS` 默认 600000;轮询原任务,completed 后同步普通生成节点并返回 canvas_sync;成功后用 canvas get 读回确认,不再次提交任务 |
|
|
66
|
+
| `generation sync ID` | 读取原任务及最新画布,保存真实任务 ID/状态/媒体结果或文本输出;无生成提交;CAS 冲突或节点关联变化停止。交付与续接见[生成结果回填与交付](generation-workflow.md#生成结果回填与交付) |
|
|
66
67
|
| `generation download ID` | `--output FILE`;可选 `--sha256 HEX` 比对预期摘要,返回本地路径、bytes、sha256、filename。--output省略时优先采用实际文件响应Content-Disposition的安全文件名,再回退业务响应filename;显式路径保留。已有文件不覆盖 |
|
|
67
68
|
| `review start` | `--input review.json --operation NAME` 默认只预检;明确审核范围后加 `--yes`,仅审核素材,不提交生成。输入 doc_id/node_id/space、团队 team_uid,mode 为 current/upstream;同 operation 不重发 POST |
|
|
68
69
|
| `review get NAME` | 查询原审核并更新本地记录,不保存画布;返回 operation/state/canvas_synced/items(task_id/source_node_ids/status/review/可选 last_failure) |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
以下命令均以安装后的 `uzoo-anime --profile PROFILE` 为前缀,本任务保持同一 profile。
|
|
6
6
|
|
|
7
7
|
1. 运行 `auth status` 确认当前账号并复用有效登录;按 [画布流程](canvas-workflow.md) 确定空间、必要团队/项目及目标画布,本任务内沿用明确选择。核对画布 team_uid 与选择一致。用 `model list --space personal` 或 `model list --space team --team-uid UID` 读取实际模型能力,在前端已使用且 CLI 已接入的节点参数范围内,根据用户目标确认缺少的模型、提示词、比例/分辨率/时长、数量与素材;已明确的不要重复询问。目录字段不自动等于可配置开关,不擅自承诺“无声”等未接入能力。缺少目标画布时按画布流程用卡片选择新建/已有及名称,再创建或读取;缺少对应类型节点时执行 `node add --doc ID --type image`(文本/视频换相应类型),再保存选定参数,不需要网页准备。没有生成请求时不要进入本流程。
|
|
8
|
-
2. 有素材时按 [素材与分组流程](material-graph-workflow.md)
|
|
8
|
+
2. 有素材时按 [素材与分组流程](material-graph-workflow.md) 复用、上传或导入到原画布,按[参考素材可见性](#参考素材可见性)核验目标节点的关联。需要 Seedance 素材审核时先按 [审核流程](seedance-review-workflow.md) 完成并回填原素材。用 `generation prepare --doc DOC_ID --node NODE_ID --space personal --output request.json` 准备请求(团队须改用已确认团队参数);它读取当前合法上游输出及持久参考,核对模型、参数、参考及审核关联后再预估。不要把本地路径当服务端 URL,不擅自设置 `protocol_agreed` 等用户同意字段。
|
|
9
9
|
3. 将 generation prepare 输出的 request 原样保存为本地 UTF-8 JSON 文件;专用路线使用专用预检/构造器。下方仅解释结构,不作为 Agent 手工拼装请求的操作模板,文档、节点、模型须来自实际查询:
|
|
10
10
|
|
|
11
11
|
```json
|
|
@@ -14,10 +14,21 @@
|
|
|
14
14
|
|
|
15
15
|
4. `generation estimate --input request.json` 或 `generation submit --input request.json --operation cat-window-001` 查看费用。`will_intercept=true` 时停止并调整参数。费用为预估,最终计费由后端决定;卡片区分预估、实际预扣与结算,不将 --expected-cost 说成最终扣费上限。
|
|
16
16
|
5. 每次先通过交互确认卡片向用户展示个人/团队归属、具体付款钱包、节点、参数和本次预估 OO 币,等待用户当次确认后,执行 `generation submit --input request.json --operation cat-window-001 --yes --expected-cost 已确认的数值`。缺少授权时先向用户展示具体请求和预估。保留 operation 和返回的 task ID。
|
|
17
|
-
6.
|
|
17
|
+
6. 提交后继续执行[生成结果回填与交付](#生成结果回填与交付)。只有原画布节点读回核验通过,才能报告画布结果已交付。
|
|
18
18
|
|
|
19
19
|
生成结果回填画布不等于存入素材库。当前前端没有自动入库,生成/恢复成功后不追加 material save;只有用户要求保存到库时,才按 [素材入库流程](material-graph-workflow.md#已有节点结果直接存素材库) 明确目标空间及目录选择,再独立执行。
|
|
20
20
|
|
|
21
|
+
## 生成结果回填与交付
|
|
22
|
+
|
|
23
|
+
适用于已授权的普通画布文本、图片和视频生成。保留同一 profile、operation、原 task ID、画布和节点;沿用本次有效的[画布标签关闭确认](interaction-workflow.md#画布标签关闭确认),保持到回填读回结束。用户明确只查状态、只读排查或只下载原文件时,不执行会写画布的命令。
|
|
24
|
+
|
|
25
|
+
1. **默认等待并回填。** 提交成功后运行 `generation wait TASK_ID`,让命令运行至完成。收到 `canvas_sync.synced=true` 后直接读回,不再重复同步。等待超时只继续跟踪同一任务,不把超时判为生成失败。
|
|
26
|
+
2. **分开轮询时补齐回填。** `generation get TASK_ID` 只读。若用它查到 `completed`,先确认返回的是原任务、原画布和原节点,核对最新节点及任务记录;已有更新任务或目标不再匹配时停止。原结果尚未落图且可以安全继续时,执行一次 `generation sync TASK_ID`。下载成功也不替代回填,不重新提交、不重新上传素材。
|
|
27
|
+
3. **独立读回再交付。** 用 `canvas get DOC_ID` 找到原节点:`data.last_task_id` 必须是本次任务,`data.status` 为 `completed`,`data.result` 对应原任务。图片/视频核对可用媒体引用(以 `file_ref` 核对来源,不以签名 URL 是否逐字相同判断),并核对任务返回的必要媒体信息;文本核对 `data.output`。按需要下载,并分别报告生成、回填和下载结果。
|
|
28
|
+
4. **中断后续接原任务。** Agent 或 CLI 退出后不会在后台自动回填。用同一 profile 的 `generation operations` 找回原任务及画布/节点。先用 `generation get` 和 `canvas get` 核对当前状态,再按本节继续等待或同步。若上次保存响应未知,先读回;确认原结果已在时,可用同一任务的 `generation sync` 核验并补记,不重复写入已匹配的结果。版本冲突、节点删除、模型或任务绑定改变时停止并报告。若任务已完成但回填失败或未核验,明确报「已生成,画布回填待处理」,不要改用 `node update` 手填结果或另起生成。
|
|
29
|
+
|
|
30
|
+
`generation operations` 的 `canvas_sync.state` 是本机最后一次记录,不是实时画布状态:`unverified` 表示未记录回填核验(包括旧记录),`pending` 表示 CLI 已看到任务完成但尚未核验落图,`verified` 表示 CLI 已核验原节点结果,并给出当时的画布版本。仅用 `generation get` 查询不会改变该记录;对待续接任务仍以最新任务和节点读回为准。不在启动时自动扫描并写画布。
|
|
31
|
+
|
|
21
32
|
## 团队共享钱包与费用核验
|
|
22
33
|
|
|
23
34
|
- 团队 OO 币余额由团队成员共用。普通成员只能看到自己的消费记录,不能查看其他成员的消费;本人没有执行消费任务或没有新增本人消费记录时,余额仍可能因其他成员同时使用而减少。这是用户明确的产品规则,不把成员可见账单当作团队全部账单。
|
|
@@ -43,12 +54,34 @@ CLI 按当前前端普通节点及请求构造器已使用的字段维护生成
|
|
|
43
54
|
- Seedance 视频的比例/分辨率按前端放在顶层;图片和其他视频模型按前端放入 extra。prepare 不补用户协议同意;active 审核可生成 reference_canvas_task_ids,素材引用与保存的审核不一致时停止。
|
|
44
55
|
- 当前前端普通图片运行路径以入边判断模式,但参考列表可包含断线保留素材,因此可能出现有 reference_image_urls 而模式为 text2image。CLI 保留该差异并展示实际请求;这不是对服务端能成功执行该组合的保证。需要图生图时先核对实际模型和合法连线,不静默更换模式或丢弃参考。
|
|
45
56
|
|
|
57
|
+
### 参考素材可见性
|
|
58
|
+
|
|
59
|
+
生成请求里的媒体 URL、素材 ID 或审核任务 ID 不会自动在画布创建素材节点,也不会自动挂到生成节点。对授权交付到画布的任务,先将用户选定的素材复用、上传或导入同一画布,再通过合法连线连接到原生成节点;已保存且能显示的节点引用可直接复用。用 `canvas get` 确认关联已保存,再用 `generation prepare` 对照 `source_node_ids`、`references` 和 `reference_mapping` 核对用户指定的素材及其顺序。不把 URL 手工加进生成请求来代替关联。
|
|
60
|
+
|
|
61
|
+
回填时只更新生成结果,保留现有连线和节点引用。读回结果时一并确认预期素材关联仍在;不把「文件存在」或「结果已回填」当作「引用已显示」。历史任务若没有可核验的原素材来源,报告引用待核对,不凭 URL 猜测源节点,不自动把历史素材添加到当前节点或再次生成。
|
|
62
|
+
|
|
46
63
|
## 视频协议前置检查
|
|
47
64
|
|
|
48
65
|
Seedance 首次使用或协议更新时,按 [协议签署与继续原任务](agreement-workflow.md) 查询当前 required/agreed/version。HTTP 200 + code 4401 表示《统一协议》中《Seedance2.0模型使用承诺书》待签署,是生成前置条件,不是生成任务失败。
|
|
49
66
|
|
|
50
67
|
`generation protocol-open` 可单独打开本机阅读确认页,不提交生成。已获当次费用确认的 submit/recover --yes 在需要签署时会打开同样页面,用户本人阅读、勾选并确认后,CLI 核验账号/版本/输入/费用,再以原请求与幂等键继续一次。独立页面不重新打开画布,不向网页暴露 CLI 令牌。正文不可用、用户不同意/超时、版本或参数/费用变化时停止,保留原 operation。不得代填 protocol_agreed 或自动同意。
|
|
51
68
|
|
|
69
|
+
## 生成提交限额与429处理
|
|
70
|
+
|
|
71
|
+
以下是普通 AI 生成提交(`POST /canvas/generations`)的规则参考,也可用 `uzoo-anime generation limits --format table` 离线查看。它不是实时剩余额度;预估、任务查询及专用提交接口不套用此表。普通生成逐笔提交,上一笔提交请求返回后再发送下一笔,无需等生成完成。
|
|
72
|
+
|
|
73
|
+
| 规则 | 限额 | 触发时怎么处理 |
|
|
74
|
+
| --- | ---: | --- |
|
|
75
|
+
| 单用户每秒提交 | 20 次 | 等约 1 秒 |
|
|
76
|
+
| 单用户每分钟提交 | 120 次 | 等额度释放,当前窗口最多 60 秒 |
|
|
77
|
+
| 全站每分钟提交 | 1,200 次 | 等额度释放,当前窗口最多 60 秒 |
|
|
78
|
+
| 同时提交请求 | 32 个 | 等前面的提交请求结束;这不是正在生成的任务数上限 |
|
|
79
|
+
| 相同内容去重窗口 | 2 秒 | 等 2 秒,先确认是否已有原任务 |
|
|
80
|
+
|
|
81
|
+
- 优先按服务端给出的具体原因和 `Retry-After`(若有)等待。仅凭 HTTP 429 不能判断触发了哪条规则,也不能据此认定登录失效;分钟窗口结束不保证繁忙时下一次必定成功。
|
|
82
|
+
- 保留错误码、服务端提示与请求编号(若有),先查原节点任务(`generation list --doc DOC_ID --node NODE_ID`)。已有匹配任务就按原 task ID 查询或等待。暂时查不到不等于未提交;不要重复上传仍可用的素材,也不要换 operation、改请求内容或换幂等键重提。
|
|
83
|
+
- CLI 不自动重提。按建议等待、核对原任务后,确需恢复且原费用授权仍有效时,按下节以原 operation、请求和幂等键恢复;报价或输入变化按费用确认流程处理。若再次限流,停止连续重试,报告服务端原因和请求编号(若有)。
|
|
84
|
+
|
|
52
85
|
## 提交中断
|
|
53
86
|
|
|
54
87
|
遇到超时、断连、网关或内部业务错误时先运行 service status,优先判断当前后端连接和数据库/Redis就绪,再查原 operation/任务和画布副作用。CLI 会对网络/服务类异常追加同源诊断,但保留原错误;ready 不代表对象存储/队列/业务事务都正常,也不证明先前没有提交。连接恢复不构成重发或另一次生成授权。
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
- 预估、实际预扣和最终结算分别报告,不能把估价或 expected-cost 描述为保证最终费用上限;团队余额差额不代表本次消费。
|
|
56
56
|
- 卡片授权与系统命令执行审批独立。执行权限继续走宿主审批工具;用户协议必须通过既有阅读签署流程由本人确认,聊天卡片不能代签。
|
|
57
57
|
- 确认后执行同一 operation,保留幂等键和 task ID,自动查询、同步、下载并交付。等待超时继续查询原任务;响应未知先查原副作用再按专用恢复规则处理,不换键重提。不要在提交成功或排队时就结束整个创作任务。
|
|
58
|
+
- 对授权交付到画布的普通生成,按[生成结果回填与交付](generation-workflow.md#生成结果回填与交付)收尾:优先用 wait 等待并回填;如果用 get 轮询,完成后还需同步原任务并读回。只读查询和只下载不触发回填。
|
|
58
59
|
|
|
59
60
|
## 验收情景
|
|
60
61
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.6.4
|
|
4
|
+
|
|
5
|
+
- 普通生成支持从本地 operation 查找原任务和上次回填状态;等待或同步后读回画布,确认结果已落到原节点。
|
|
6
|
+
- 更新随包 Skill:任务中断后续接原任务;提交前核对参考素材已关联到画布节点。
|
|
7
|
+
|
|
8
|
+
## 0.6.3
|
|
9
|
+
|
|
10
|
+
- 新增 `generation limits`,可离线查看普通 AI 生成提交限额与等待建议。
|
|
11
|
+
- 生成提交遇到 HTTP 429 时提示先查询原节点任务,再按原操作恢复;随包 Skill 同步补充处理规则。
|
|
12
|
+
|
|
3
13
|
## 0.6.2
|
|
4
14
|
|
|
5
15
|
- 新建普通节点和从素材库导入时,未指定坐标会自动避开已有节点和分组。
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { materialFolders, materialList, materialGet, materialUpload, materialUse
|
|
|
17
17
|
import { downloadMaterial } from './material-download.js'
|
|
18
18
|
import { graphWrite, prepareGeneration, renameGraphGroup, ungroupGraph } from './graph.js'
|
|
19
19
|
import { uploadAndBind, bindUpload } from './resource.js'
|
|
20
|
-
import {syncGeneration} from './generation-graph.js'
|
|
20
|
+
import {generationOperationSummary,syncGeneration} from './generation-graph.js'
|
|
21
21
|
import { prepareDraftFinalize, submitDraftFinalize, getDraftFinalize, waitDraftFinalize } from './draft-finalize.js'
|
|
22
22
|
import { listNodeHistory, selectNodeHistory, recoverNodeHistory } from './node-history.js'
|
|
23
23
|
import { addNodeHistory, recoverHistoryAdd } from './history-add.js'
|
|
@@ -104,15 +104,17 @@ const help = `Uzoo Anime CLI — 浏览器授权与独立 CLI 会话
|
|
|
104
104
|
generation prepare --doc ID --node ID --space personal|team [--team-uid UID --output request.json]
|
|
105
105
|
generation list --doc ID [--node ID] [--status STATUS] [--cursor ID] [--limit N]
|
|
106
106
|
generation estimate --input request.json
|
|
107
|
+
generation limits [--format json|table](离线查看普通 AI 生成提交限额及 429 处理)
|
|
107
108
|
generation submit --input request.json --operation NAME [--yes --expected-cost OO]
|
|
108
|
-
generation recover NAME [--yes] | operations
|
|
109
|
-
generation get ID | logs ID
|
|
109
|
+
generation recover NAME [--yes] | operations(本地任务及回填记录)
|
|
110
|
+
generation get ID | logs ID(只读;get 查到完成不会回填)
|
|
111
|
+
generation wait ID [--interval MS] [--timeout MS](等待原任务,完成后回填画布)
|
|
110
112
|
generation history --doc ID --node ID --space personal|team [--team-uid UID --status STATUS --limit N --offset N]
|
|
111
113
|
generation history-select --input FILE --operation NAME [--yes]
|
|
112
114
|
generation history-add --input FILE --operation NAME [--yes]
|
|
113
115
|
generation history-add-get|history-add-recover OPERATION
|
|
114
116
|
generation history-get NAME | history-recover NAME(查询原历史选择,不重发写入)
|
|
115
|
-
generation sync ID
|
|
117
|
+
generation sync ID(补回填原任务,不重新生成;用 canvas get 读回确认)
|
|
116
118
|
generation protocol-status(只读查询视频《统一协议》确认状态)
|
|
117
119
|
generation protocol-open [--browser default|msedge|chrome] [--timeout MS]
|
|
118
120
|
generation download ID [--output FILE] [--sha256 HEX]
|
|
@@ -179,6 +181,23 @@ async function input(file) {
|
|
|
179
181
|
}
|
|
180
182
|
function query(endpoint, fields) { const q = new URLSearchParams(Object.entries(fields).filter(([, v]) => v !== undefined)); return `${endpoint}${q.size ? '?' + q : ''}` }
|
|
181
183
|
|
|
184
|
+
const generationLimits = {
|
|
185
|
+
endpoint: 'POST /canvas/generations',
|
|
186
|
+
source: 'documented_reference',
|
|
187
|
+
items: [
|
|
188
|
+
{ rule: '单用户每秒提交', limit: 20, wait: '约 1 秒' },
|
|
189
|
+
{ rule: '单用户每分钟提交', limit: 120, wait: '等额度释放,当前窗口最多 60 秒' },
|
|
190
|
+
{ rule: '全站每分钟提交', limit: 1200, wait: '等额度释放,当前窗口最多 60 秒' },
|
|
191
|
+
{ rule: '同时提交请求', limit: 32, wait: '等前面的提交请求结束,无需等生成完成' },
|
|
192
|
+
{ rule: '相同内容去重窗口(秒)', limit: 2, wait: '等 2 秒,并先确认是否已有任务' },
|
|
193
|
+
],
|
|
194
|
+
notes: [
|
|
195
|
+
'这是平台提供的提交规则参考,并非实时剩余额度;以服务端实际响应和 Retry-After(若有)为准。',
|
|
196
|
+
'HTTP 429 本身不能确定触发哪条规则。先查原节点任务;已有任务就继续查询或等待,不重复提交。',
|
|
197
|
+
'CLI 不自动重提。需要恢复时沿用原 operation、请求和幂等键;确认费用授权仍有效后再执行恢复。',
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
|
|
182
201
|
let diagnosticClient
|
|
183
202
|
async function main(args) {
|
|
184
203
|
if (args[0] === 'install') {
|
|
@@ -195,6 +214,12 @@ async function main(args) {
|
|
|
195
214
|
if (!['json', 'table'].includes(format)) throw new Error('--format 必须是 json 或 table')
|
|
196
215
|
if (p.length > 3) throw new Error('多余的位置参数')
|
|
197
216
|
const [group, action, idArg] = p
|
|
217
|
+
if (group === 'generation' && action === 'limits') {
|
|
218
|
+
if (idArg || Object.keys(o).some(key => key !== 'format')) throw new Error('generation limits 仅支持 --format json|table')
|
|
219
|
+
print(generationLimits, format)
|
|
220
|
+
if (format === 'table') console.log('\n' + generationLimits.notes.join('\n'))
|
|
221
|
+
return
|
|
222
|
+
}
|
|
198
223
|
if(group==='version'||group==='skill'){
|
|
199
224
|
if(Object.keys(o).some(k=>k!=='format')||idArg||
|
|
200
225
|
(group==='version'&&p.length!==1)||(group==='skill'&&(action!=='path'||p.length!==2)))
|
|
@@ -305,7 +330,7 @@ async function main(args) {
|
|
|
305
330
|
const dir = path.join(root(), 'operations', store.name)
|
|
306
331
|
const files = await fs.readdir(dir).catch(e => { if (e.code === 'ENOENT') return []; throw e })
|
|
307
332
|
const records = await Promise.all(files.filter(f => f.endsWith('.json')).map(f => json(path.join(dir, f))))
|
|
308
|
-
print(records.map(
|
|
333
|
+
print(records.map(generationOperationSummary), format); return
|
|
309
334
|
}
|
|
310
335
|
if (group === 'repaint' && action === 'operations') { print(await listRepaints(store.name), format); return }
|
|
311
336
|
if (group === 'service') {
|
|
@@ -444,8 +469,8 @@ async function main(args) {
|
|
|
444
469
|
else if (action === 'estimate') { const body = await input(o.input); validateGeneration(body); data = await client.raw('/canvas/generations/estimate', { method: 'POST', body }) }
|
|
445
470
|
else if (action === 'submit') data = await submit(client, store.name, await input(o.input), need(o.operation, '--operation'), o.yes, expectedCost,{onAgreementRequired:({operation,body,estimate})=>openAgreement({operation,model:body.model_name,cost:estimate.estimated_credit_cost})})
|
|
446
471
|
else if (action === 'recover') data = await recover(client, store.name, id(), o.yes, expectedCost,{onAgreementRequired:({operation,body,estimate})=>openAgreement({operation,model:body.model_name,cost:estimate.estimated_credit_cost})})
|
|
447
|
-
else if (action === 'sync') data=await syncGeneration(client,id())
|
|
448
|
-
else if (action === 'wait') {data = await wait(client, id(), number(o.interval, 3000), number(o.timeout, 600000));data={...data,canvas_sync:await syncGeneration(client,id())}}
|
|
472
|
+
else if (action === 'sync') data=await syncGeneration(client,id(),{track:true})
|
|
473
|
+
else if (action === 'wait') {data = await wait(client, id(), number(o.interval, 3000), number(o.timeout, 600000));data={...data,canvas_sync:await syncGeneration(client,id(),{track:true})}}
|
|
449
474
|
else if (action === 'download') { if (o.sha256 && !/^[a-fA-F0-9]{64}$/.test(o.sha256)) throw new Error('--sha256 必须是 64 位十六进制'); data = await download(client, id(), o.output, o.sha256) }
|
|
450
475
|
else data = await client.raw(`/canvas/generations/${segment(id())}${action === 'logs' ? '/logs' : ''}`)
|
|
451
476
|
print(data, format)
|
package/src/generation-graph.js
CHANGED
|
@@ -2,21 +2,51 @@ import { writeNode } from './canvas.js'
|
|
|
2
2
|
import { segment } from './workflows.js'
|
|
3
3
|
import fs from 'node:fs/promises'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import { root, safeName, json } from './store.js'
|
|
5
|
+
import { atomic, root, safeName, json } from './store.js'
|
|
6
6
|
import { isDeepStrictEqual } from 'node:util'
|
|
7
7
|
|
|
8
|
-
async function
|
|
8
|
+
async function matchingOperation(client, task) {
|
|
9
9
|
if(!client.profile)return null
|
|
10
10
|
const directory=path.join(root(),'operations',safeName(client.profile))
|
|
11
11
|
const files=await fs.readdir(directory).catch(error=>{if(error.code==='ENOENT')return [];throw error})
|
|
12
12
|
const candidates=[]
|
|
13
|
-
for(const
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
for(const name of files.filter(f=>f.endsWith('.json'))){
|
|
14
|
+
const file=path.join(directory,name)
|
|
15
|
+
const record=await json(file,null)
|
|
16
|
+
if(String(record?.taskId)===String(task.task_id??task.id)&&record.baseUrl===client.baseUrl)candidates.push({file,record})
|
|
16
17
|
}
|
|
17
18
|
if(candidates.length!==1)return null
|
|
19
|
+
const match=candidates[0]
|
|
20
|
+
const binding=match.record.body || match.record.syncBaseline
|
|
21
|
+
if(binding?.doc_id!==task.doc_id || (binding.node_id??binding.node?.id)!==task.node_id)return null
|
|
18
22
|
const me=await client.raw('/auth/me')
|
|
19
|
-
return String(me.uid??me.id)===
|
|
23
|
+
return String(me.uid??me.id)===match.record.owner?match:null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function generationOperationSummary({ operation, taskId, state, createdAt, lastFailure, body, syncBaseline, canvas_sync }) {
|
|
27
|
+
const syncState=['pending','verified'].includes(canvas_sync?.state)?canvas_sync.state:'unverified'
|
|
28
|
+
return { operation, taskId, state:lastFailure?.apiCode===4401?'blocked_agreement':state, createdAt,
|
|
29
|
+
...(lastFailure?{lastFailure}:{}),
|
|
30
|
+
...(taskId?{
|
|
31
|
+
doc_id:body?.doc_id??syncBaseline?.doc_id,
|
|
32
|
+
node_id:body?.node_id??syncBaseline?.node?.id,
|
|
33
|
+
canvas_sync:{state:syncState,...(syncState==='verified'&&Number.isSafeInteger(canvas_sync.version)?{version:canvas_sync.version}:{})}
|
|
34
|
+
}:{})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function recordCanvasSync(match, state, version) {
|
|
39
|
+
if(!match)return
|
|
40
|
+
const next={state,...(version===undefined?{}:{version})}
|
|
41
|
+
if(isDeepStrictEqual(match.record.canvas_sync,next))return
|
|
42
|
+
match.record.canvas_sync=next
|
|
43
|
+
await atomic(match.file,match.record)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasResultPatch(node, patch) {
|
|
47
|
+
return Object.entries(patch).every(([key,value])=>key==='extra'
|
|
48
|
+
? Object.entries(value).every(([k,v])=>isDeepStrictEqual(node?.data?.extra?.[k],v))
|
|
49
|
+
: isDeepStrictEqual(node?.data?.[key],value))
|
|
20
50
|
}
|
|
21
51
|
async function matchingDraftModel(client,doc,node,task){
|
|
22
52
|
if(task.segment_type!=='video'||task.draft?.step!=='preview'||node.data?.extra?.draft_preview!==true)return false
|
|
@@ -34,7 +64,7 @@ async function matchingDraftModel(client,doc,node,task){
|
|
|
34
64
|
|
|
35
65
|
// Task completion and canvas persistence are separate contracts. Reconcile the
|
|
36
66
|
// original real task; never create or re-submit a generation here.
|
|
37
|
-
export async function syncGeneration(client, taskId, { baseline } = {}) {
|
|
67
|
+
export async function syncGeneration(client, taskId, { baseline, track = false } = {}) {
|
|
38
68
|
const task = await client.raw(`/canvas/generations/${segment(taskId)}`)
|
|
39
69
|
if (
|
|
40
70
|
String(task.task_id ?? task.id) !== String(taskId) ||
|
|
@@ -45,7 +75,11 @@ export async function syncGeneration(client, taskId, { baseline } = {}) {
|
|
|
45
75
|
throw new Error('生成任务绑定不完整')
|
|
46
76
|
if (!['completed', 'failed', 'cancelled', 'canceled'].includes(task.status))
|
|
47
77
|
return { synced: false, task_id: taskId, status: task.status, next: '继续查询原任务' }
|
|
48
|
-
const
|
|
78
|
+
const trackCompleted=track && task.status==='completed'
|
|
79
|
+
const operation=trackCompleted?await matchingOperation(client,task):null
|
|
80
|
+
if(operation?.record.canvas_sync?.state!=='verified')await recordCanvasSync(operation,'pending')
|
|
81
|
+
const docPath=`/canvas/docs/${segment(task.doc_id)}`
|
|
82
|
+
const doc = await client.raw(docPath)
|
|
49
83
|
const node = doc.nodes?.find((n) => n.id === task.node_id)
|
|
50
84
|
if (
|
|
51
85
|
!node ||
|
|
@@ -58,7 +92,7 @@ export async function syncGeneration(client, taskId, { baseline } = {}) {
|
|
|
58
92
|
throw new Error('任务目标不是普通生成节点;派生流程不能通用回填')
|
|
59
93
|
const current = String(node.data?.last_task_id || node.data?.result?.task_id || '')
|
|
60
94
|
if (current && current !== String(taskId)) {
|
|
61
|
-
const original=baseline || await
|
|
95
|
+
const original=baseline || (trackCompleted?operation:await matchingOperation(client,task))?.record.syncBaseline
|
|
62
96
|
if(!original || original.doc_id!==doc.id || (original.team_uid||null)!==(doc.team_uid||null) ||
|
|
63
97
|
!isDeepStrictEqual(original.node,node) || !/^\d+$/.test(current) || BigInt(current)>=BigInt(taskId))
|
|
64
98
|
throw new Error('节点已关联其他生成任务;提交前快照不匹配,停止以保留后续结果')
|
|
@@ -89,12 +123,8 @@ export async function syncGeneration(client, taskId, { baseline } = {}) {
|
|
|
89
123
|
...(task.segment_type==='video'&&task.draft?.step==='preview'?{draft:structuredClone(task.draft)}:{}) }
|
|
90
124
|
if(task.segment_type==='video'&&task.draft?.step==='preview'&&Object.hasOwn(node.data||{},'draft'))patch.draft=structuredClone(task.draft)
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
? Object.entries(value).every(([k, v]) => isDeepStrictEqual(node.data?.extra?.[k],v))
|
|
95
|
-
: JSON.stringify(node.data?.[key]) === JSON.stringify(value)
|
|
96
|
-
)
|
|
97
|
-
if (same)
|
|
126
|
+
if (hasResultPatch(node,patch)) {
|
|
127
|
+
await recordCanvasSync(operation,'verified',doc.version)
|
|
98
128
|
return {
|
|
99
129
|
synced: true,
|
|
100
130
|
unchanged: true,
|
|
@@ -104,6 +134,14 @@ export async function syncGeneration(client, taskId, { baseline } = {}) {
|
|
|
104
134
|
version: doc.version,
|
|
105
135
|
status: task.status
|
|
106
136
|
}
|
|
137
|
+
}
|
|
138
|
+
await recordCanvasSync(operation,'pending')
|
|
139
|
+
if(trackCompleted && (!current || current===String(taskId))){
|
|
140
|
+
const query=new URLSearchParams({doc_id:task.doc_id,node_id:task.node_id,limit:'1'})
|
|
141
|
+
const latest=await client.raw('/canvas/generations?'+query)
|
|
142
|
+
if(String(latest.items?.[0]?.task_id??latest.items?.[0]?.id)!==String(taskId))
|
|
143
|
+
throw new Error('存在后续任务或最新任务无法核验,停止回填')
|
|
144
|
+
}
|
|
107
145
|
const bound = await writeNode(client, {
|
|
108
146
|
docId: doc.id,
|
|
109
147
|
nodeId: node.id,
|
|
@@ -113,12 +151,22 @@ export async function syncGeneration(client, taskId, { baseline } = {}) {
|
|
|
113
151
|
expectedTeamUid: doc.team_uid,
|
|
114
152
|
expectedVersion: doc.version
|
|
115
153
|
})
|
|
154
|
+
let version=bound.version
|
|
155
|
+
if(trackCompleted){
|
|
156
|
+
const saved=await client.raw(docPath)
|
|
157
|
+
if(saved.id!==doc.id || (saved.team_uid||null)!==(doc.team_uid||null) ||
|
|
158
|
+
!Number.isSafeInteger(saved.version) || saved.version<bound.version ||
|
|
159
|
+
!hasResultPatch(saved.nodes?.find(n=>n.id===task.node_id&&n.type===task.segment_type),patch))
|
|
160
|
+
throw new Error(`任务 ${taskId} 已完成,但画布回填读回未通过;先核对原节点,不要重新生成`)
|
|
161
|
+
version=saved.version
|
|
162
|
+
await recordCanvasSync(operation,'verified',version)
|
|
163
|
+
}
|
|
116
164
|
return {
|
|
117
165
|
synced: true,
|
|
118
166
|
task_id: taskId,
|
|
119
167
|
doc_id: doc.id,
|
|
120
168
|
node_id: node.id,
|
|
121
|
-
version
|
|
169
|
+
version,
|
|
122
170
|
status: task.status
|
|
123
171
|
}
|
|
124
172
|
}
|
package/src/workflows.js
CHANGED
|
@@ -129,7 +129,8 @@ export async function submit(client, profile, body, operation, confirmed, expect
|
|
|
129
129
|
await atomic(file, record)
|
|
130
130
|
if(error.apiCode===4401)throw agreementError('AGREEMENT_REQUIRED','《统一协议》《Seedance2.0模型使用承诺书》需要签署或版本已更新(code 4401);本次生成提交被前置检查阻止,不是生成任务执行失败;保留原 operation',error.protocol || protocol)
|
|
131
131
|
const detail=record.lastFailure.apiCode ? `(接口返回 code ${record.lastFailure.apiCode},HTTP ${record.lastFailure.status})` : record.lastFailure.status ? `(HTTP ${record.lastFailure.status})` : ''
|
|
132
|
-
const
|
|
132
|
+
const rateLimitHint = error.status === 429 ? ';请先查原节点是否已有任务,再按 generation limits 的等待建议处理' : ''
|
|
133
|
+
const failure = new Error(`提交结果未确认${detail}${rateLimitHint};使用 generation recover ${operation} 以原幂等键恢复,勿创建新 operation`,{cause:error})
|
|
133
134
|
failure.code = 'SUBMISSION_UNKNOWN'; throw failure
|
|
134
135
|
}
|
|
135
136
|
}
|