@uzooai/anime-ai-cli 0.0.0-stage → 0.5.0

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.
Files changed (123) hide show
  1. package/.agents/skills/uzoo-anime-cli/SKILL.md +83 -0
  2. package/.agents/skills/uzoo-anime-cli/references/agreement-workflow.md +51 -0
  3. package/.agents/skills/uzoo-anime-cli/references/canvas-workflow.md +53 -0
  4. package/.agents/skills/uzoo-anime-cli/references/commands.md +250 -0
  5. package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +152 -0
  6. package/.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md +175 -0
  7. package/.agents/skills/uzoo-anime-cli/references/interaction-workflow.md +67 -0
  8. package/.agents/skills/uzoo-anime-cli/references/material-graph-workflow.md +123 -0
  9. package/.agents/skills/uzoo-anime-cli/references/node-capabilities.md +95 -0
  10. package/.agents/skills/uzoo-anime-cli/references/node-workflow.md +94 -0
  11. package/.agents/skills/uzoo-anime-cli/references/remake-workflow.md +54 -0
  12. package/.agents/skills/uzoo-anime-cli/references/repaint-workflow.md +300 -0
  13. package/.agents/skills/uzoo-anime-cli/references/seedance-review-workflow.md +41 -0
  14. package/.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md +71 -0
  15. package/CHANGELOG.md +92 -0
  16. package/LICENSE +21 -0
  17. package/README.md +242 -3
  18. package/assets/README.md +4 -0
  19. package/assets/favicon-uzoo.png +0 -0
  20. package/assets/logo-uzoo.svg +7 -0
  21. package/docs/CLI/345/275/223/345/211/215/350/214/203/345/233/264/344/270/216/346/234/254/345/234/260/346/211/223/345/214/205-20261103.md +134 -0
  22. package/docs/CLI/346/216/210/346/235/203/346/216/245/345/217/243/344/270/216/344/273/244/347/211/214/350/207/252/345/212/250/345/210/267/346/226/260/350/256/276/350/256/241.md +540 -0
  23. package/docs/CLI/347/274/272/345/217/243/346/240/270/345/257/271/344/270/216/346/224/266/345/260/276-20261105.md +65 -0
  24. package/docs/CLI/347/275/221/351/241/265/344/277/235/345/255/230/350/212/202/347/202/271/347/274/272/345/244/261/344/272/244/346/216/245.md +52 -0
  25. package/docs/M0-M5/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +89 -0
  26. package/docs/npm/347/272/277/344/270/212/345/217/221/345/270/203/344/270/216/345/256/211/350/243/205/344/275/277/347/224/250.md +151 -0
  27. package/docs//344/270/213/350/275/275/346/216/245/345/217/243/351/200/202/351/205/215/344/270/216/351/252/214/346/224/266.md +107 -0
  28. package/docs//344/270/232/345/212/241/350/203/275/345/212/233/350/241/245/345/205/250/344/270/216Skill/347/273/223/346/236/204/350/247/204/345/210/222.md +311 -0
  29. package/docs//345/205/250/345/261/200/351/231/204/344/273/266/350/265/204/346/272/220/345/234/260/345/235/200/344/270/216/346/250/241/345/236/213/350/257/273/345/217/226/345/267/256/345/274/202.md +47 -0
  30. package/docs//345/206/205/351/203/250Windows/345/256/211/350/243/205/344/270/216/345/215/207/347/272/247/351/252/214/346/224/266/346/270/205/345/215/225.md +34 -0
  31. package/docs//345/233/276/347/211/207/346/212/240/345/233/276/346/216/245/345/205/245/344/270/216/351/252/214/346/224/266-20261104.md +110 -0
  32. package/docs//345/274/200/345/217/221/350/256/241/345/210/222.md +127 -0
  33. package/docs//345/274/202/346/255/245/346/212/240/345/233/276CLI/346/235/203/351/231/220/344/272/244/346/216/245-20261105.md +50 -0
  34. package/docs//345/274/202/346/255/245/346/212/240/345/233/276/347/234/237/345/256/236/351/252/214/346/224/266-20261105.md +44 -0
  35. package/docs//346/226/260/344/274/232/350/257/235/344/273/273/345/212/241/344/272/244/344/273/243-/351/230/266/346/256/2652A.md +79 -0
  36. package/docs//346/226/260/351/203/250/347/275/262/347/224/273/345/270/203/345/206/262/347/252/201/351/252/214/346/224/266-20261102.md +61 -0
  37. package/docs//346/231/256/351/200/232/346/265/217/350/247/210/345/231/250/347/231/273/345/275/225.md +53 -0
  38. package/docs//346/255/243/345/274/217/347/253/231/345/210/207/346/215/242/344/270/216/351/252/214/346/224/266-20261104.md +90 -0
  39. package/docs//347/224/273/345/270/203CLI/344/270/213/344/270/200/351/230/266/346/256/265/345/274/200/345/217/221/350/256/241/345/210/222.md +136 -0
  40. package/docs//347/224/273/345/270/203CLI/351/235/236/350/275/254/347/273/230/345/256/214/345/226/204/350/256/241/345/210/222.md +183 -0
  41. package/docs//347/224/273/345/270/203/350/203/275/345/212/233/345/245/221/347/272/246/346/240/270/345/257/271.md +59 -0
  42. package/docs//347/234/237/345/256/236/344/270/232/345/212/241/346/216/245/345/205/245/344/270/216/346/265/213/350/257/225.md +1004 -0
  43. package/docs//347/264/240/346/235/220/345/205/245/345/272/223/344/270/216/347/224/273/345/270/203/344/277/235/345/255/230/351/227/256/351/242/230/345/256/232/344/275/215.md +52 -0
  44. package/docs//351/230/266/346/256/2653B/351/252/214/346/224/266/344/272/244/346/216/245.md +44 -0
  45. package/docs//351/230/266/346/256/265/344/270/200/351/252/214/346/224/266.md +60 -0
  46. package/docs//351/235/236/350/275/254/347/273/230/346/214/201/347/273/255/345/256/214/345/226/204/344/270/216/351/252/214/346/224/266-20261103.md +76 -0
  47. package/docs//351/235/236/350/275/254/347/273/230/347/254/254/344/272/214/346/211/271/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +39 -0
  48. package/docs//351/235/236/350/275/254/347/273/230/350/241/245/345/205/205/346/235/241/344/273/266/351/252/214/346/224/266-20261103.md +113 -0
  49. package/docs//351/235/236/350/275/254/347/273/230/351/246/226/346/211/271/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +71 -0
  50. package/examples/matting.json +5 -0
  51. package/examples/real-business/image-request.template.json +7 -0
  52. package/examples/real-business/node-patch.template.json +11 -0
  53. package/extensions/uzoo-cli-login/manifest.json +9 -0
  54. package/extensions/uzoo-cli-login/popup.css +1 -0
  55. package/extensions/uzoo-cli-login/popup.html +14 -0
  56. package/extensions/uzoo-cli-login/popup.js +65 -0
  57. package/package.json +56 -6
  58. package/scripts/check-package-resources.mjs +56 -0
  59. package/scripts/check-release.mjs +19 -0
  60. package/scripts/pack-local.mjs +33 -0
  61. package/scripts/release-policy.mjs +22 -0
  62. package/src/agreement-page.js +62 -0
  63. package/src/agreement.js +419 -0
  64. package/src/annotation-media.js +386 -0
  65. package/src/api.js +168 -0
  66. package/src/browser-login.js +13 -0
  67. package/src/callback-page.js +44 -0
  68. package/src/canvas.js +105 -0
  69. package/src/cli-authorization.js +122 -0
  70. package/src/cli.js +459 -0
  71. package/src/download-name.js +17 -0
  72. package/src/draft-finalize.js +229 -0
  73. package/src/generation-graph.js +124 -0
  74. package/src/generation-input.js +472 -0
  75. package/src/graph-disconnect.js +89 -0
  76. package/src/graph.js +431 -0
  77. package/src/group-edit.js +53 -0
  78. package/src/group-layout.js +113 -0
  79. package/src/group-membership.js +126 -0
  80. package/src/history-add.js +119 -0
  81. package/src/image-annotation.js +246 -0
  82. package/src/image-crop.js +283 -0
  83. package/src/image-matting.js +82 -0
  84. package/src/image-media.js +89 -0
  85. package/src/image-output.js +385 -0
  86. package/src/local-output.js +23 -0
  87. package/src/material-download.js +55 -0
  88. package/src/material-save.js +254 -0
  89. package/src/materials.js +147 -0
  90. package/src/matting-background.js +73 -0
  91. package/src/matting-jobs.js +63 -0
  92. package/src/matting-worker.js +49 -0
  93. package/src/matting.js +170 -0
  94. package/src/media-copy.js +53 -0
  95. package/src/media.js +343 -0
  96. package/src/model-options.js +76 -0
  97. package/src/multipart.js +28 -0
  98. package/src/node-copy.js +95 -0
  99. package/src/node-delete.js +49 -0
  100. package/src/node-history.js +231 -0
  101. package/src/normal-login.js +118 -0
  102. package/src/package-info.js +8 -0
  103. package/src/png-alpha.js +60 -0
  104. package/src/protocol.js +83 -0
  105. package/src/remake.js +738 -0
  106. package/src/repaint-archive.js +53 -0
  107. package/src/repaint-delivery.js +783 -0
  108. package/src/repaint-export.js +63 -0
  109. package/src/repaint-graph.js +214 -0
  110. package/src/repaint-history.js +267 -0
  111. package/src/repaint-materials.js +601 -0
  112. package/src/repaint-reverse.js +614 -0
  113. package/src/repaint-sequence.js +132 -0
  114. package/src/repaint-settings.js +576 -0
  115. package/src/repaint-video.js +798 -0
  116. package/src/repaint.js +326 -0
  117. package/src/resource.js +110 -0
  118. package/src/review.js +435 -0
  119. package/src/service.js +85 -0
  120. package/src/store.js +100 -0
  121. package/src/video-clip.js +381 -0
  122. package/src/video-frame.js +296 -0
  123. package/src/workflows.js +214 -0
@@ -0,0 +1,229 @@
1
+ import crypto from 'node:crypto'
2
+ import path from 'node:path'
3
+ import { isDeepStrictEqual as same } from 'node:util'
4
+ import { atomic, json, root, safeName } from './store.js'
5
+ import { scopedCanvas, materialScope } from './materials.js'
6
+ import { save } from './graph.js'
7
+ import { segment, wait } from './workflows.js'
8
+ import { readProtocol, agreementError } from './agreement.js'
9
+ import { requireServiceReady } from './service.js'
10
+
11
+ const hash = value => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
12
+ const fail = (code, message, cause) => Object.assign(new Error(message, { cause }), { code })
13
+ export const draftFinalizePath = (profile, operation) => path.join(root(), 'draft-finalize', safeName(profile), safeName(operation) + '.json')
14
+ const persist = record => atomic(draftFinalizePath(record.profile, record.operation), record)
15
+ function inputOf(value) {
16
+ if (!value || Array.isArray(value) || typeof value !== 'object' ||
17
+ Object.keys(value).some(k => !['doc_id', 'node_id', 'space', 'team_uid'].includes(k)) ||
18
+ !['doc_id', 'node_id'].every(k => typeof value[k] === 'string' && value[k].trim()))
19
+ throw new Error('样片转正片输入仅支持 doc_id/node_id/space/team_uid;参数继承原样片')
20
+ materialScope(value.space, value.team_uid)
21
+ return structuredClone(value)
22
+ }
23
+ async function owner(client) {
24
+ const me = await client.raw('/auth/me')
25
+ if (!me.uid) throw new Error('账号缺少 UID')
26
+ return String(me.uid)
27
+ }
28
+ function snapshot(doc) {
29
+ if (!Number.isSafeInteger(doc.version) || doc.version < 1 || !Array.isArray(doc.nodes) ||
30
+ !Array.isArray(doc.edges) || !doc.meta || !doc.viewport || typeof doc.name !== 'string' ||
31
+ new Set(doc.nodes.map(n => n.id)).size !== doc.nodes.length || new Set(doc.edges.map(e => e.id)).size !== doc.edges.length)
32
+ throw new Error('画布快照不完整或 ID 重复')
33
+ }
34
+ const taskId = node => Number(node?.data?.last_task_id || node?.data?.lastTaskId || node?.data?.runtime?.taskId)
35
+ function parentIdentity(task, history) {
36
+ // Task GET omits prompt/references/duration on the deployed service. Read the
37
+ // original history row instead of showing the node's possibly edited inputs.
38
+ return JSON.parse(JSON.stringify({ task_id: task.task_id ?? task.id, doc_id: task.doc_id, node_id: task.node_id,
39
+ model_name: task.model_name, prompt: history.prompt, duration: history.duration,
40
+ aspect_ratio: history.aspect_ratio || history.extra?.aspect_ratio || task.result?.aspect_ratio,
41
+ source_resolution: history.resolution || history.extra?.resolution, final_resolution: '1080p',
42
+ generation_mode: history.generation_mode || history.extra?.generation_mode,
43
+ reference_image_urls: history.reference_image_urls || [],
44
+ reference_video_urls: history.reference_video_urls || history.extra?.reference_video_urls || [],
45
+ reference_audio_urls: history.reference_audio_urls || history.extra?.reference_audio_urls || [],
46
+ reference_asset_ids: history.reference_asset_ids || [],
47
+ draft: { step: task.draft.step, expires_at: task.draft.expires_at }, file_ref: task.result.file_ref }))
48
+ }
49
+ async function previewHistory(client, doc, node, id) {
50
+ for (let offset = 0; offset < 10000; offset += 100) {
51
+ const result = await client.raw('/canvas/docs/' + segment(doc.id) + '/nodes/' + segment(node.id) +
52
+ '/history?' + new URLSearchParams({ status: 'completed', limit: 100, offset }))
53
+ if (result.doc_id !== doc.id || result.node_id !== node.id || !Array.isArray(result.items) || typeof result.has_more !== 'boolean')
54
+ throw new Error('样片原历史响应绑定不完整')
55
+ const matches = result.items.filter(t => Number(t.task_id) === id && !t.source)
56
+ if (matches.length > 1) throw new Error('样片原历史任务不唯一')
57
+ if (matches.length) {
58
+ const item = matches[0]
59
+ if (item.doc_id !== doc.id || item.node_id !== node.id || item.segment_type !== 'video' || item.status !== 'completed' ||
60
+ item.draft?.step !== 'preview' || typeof item.prompt !== 'string' || !Number.isFinite(item.duration) || item.duration <= 0)
61
+ throw new Error('样片原历史缺少可核验的继承参数')
62
+ return item
63
+ }
64
+ if (!result.has_more) break
65
+ }
66
+ throw new Error('未找到样片原历史,不用当前节点提示词冒充原任务输入')
67
+ }
68
+ async function context(client, input, expectedParent) {
69
+ const doc = await scopedCanvas(client, input.doc_id, input.space, input.team_uid)
70
+ snapshot(doc)
71
+ const node = doc.nodes.find(n => n.id === input.node_id), data = node?.data || {}, extra = data.extra || {}
72
+ const id = taskId(node)
73
+ if (!node || node.type !== 'video' || extra.node_kind === 'asset' || extra.repaint || extra.video_remake ||
74
+ extra.clip_redraw || extra.draft_finalize || data.repaint || data.videoRemake || data.draftFinalizeGeneration ||
75
+ !Number.isSafeInteger(id) || id < 1 || ['preparing','queued','processing','retrying','submitting'].includes(data.runtime?.status || data.status))
76
+ throw new Error('来源须为已有已完成普通视频样片节点')
77
+ if (expectedParent && id !== Number(expectedParent.task_id)) throw new Error('样片节点已切换任务,停止原操作')
78
+ const task = await client.raw('/canvas/generations/' + segment(id))
79
+ if (Number(task.task_id ?? task.id) !== id || task.doc_id !== doc.id || task.node_id !== node.id ||
80
+ task.segment_type !== 'video' || task.status !== 'completed' || task.billing_status !== 'charged' ||
81
+ task.draft?.step !== 'preview' || !['preview_ready','finalizing','consumed'].includes(task.draft.status) ||
82
+ !task.result?.file_ref || task.result.file_ref !== data.result?.file_ref)
83
+ throw new Error('原任务不是已结算且可用的真实样片;不凭模型名称认定样片')
84
+ if (task.draft.expires_at && (!Number.isFinite(Date.parse(task.draft.expires_at)) || Date.parse(task.draft.expires_at) <= Date.now()))
85
+ throw new Error('样片已过期或有效期不可确认')
86
+ const history = await previewHistory(client, doc, node, id)
87
+ const parent = parentIdentity(task, history)
88
+ // The estimate endpoint is authoritative for entitlement, expiry and inherited price.
89
+ const estimate = await client.raw('/canvas/generations/estimate', { method: 'POST', body: { segment_type: 'video', draft_task_id: id } }, { retry: false })
90
+ if (!Number.isFinite(estimate.estimated_credit_cost) || estimate.estimated_credit_cost < 0 || estimate.will_intercept)
91
+ throw new Error('正片报价无效或被拦截,未提交')
92
+ return { doc, node, task, parent, estimate }
93
+ }
94
+ const summary = r => ({ operation: r.operation, state: r.state, doc_id: r.input.doc_id,
95
+ parent_node_id: r.input.node_id, parent_task_id: r.parent.task_id, node_id: r.output.id,
96
+ task_id: r.taskId || null, estimate: r.estimate, wallet: r.wallet,
97
+ inherited: r.parent, generation_submitted: r.taskId ? true : ['submitting','unknown'].includes(r.state) ? null : false })
98
+ async function bound(client, profile, operation) {
99
+ const r = await json(draftFinalizePath(profile, operation), null)
100
+ if (!r || r.baseUrl !== client.baseUrl || r.owner !== await owner(client)) throw new Error('样片 operation 的服务或账号不匹配')
101
+ if (r.hash !== hash({ input: r.input, parent: r.parent, output: r.output, edge: r.edge })) throw new Error('样片 operation 摘要不匹配')
102
+ return r
103
+ }
104
+ export async function prepareDraftFinalize(client, profile, value, operation) {
105
+ const input = inputOf(value), file = draftFinalizePath(profile, operation)
106
+ if (await json(file, null)) {
107
+ const r = await bound(client, profile, operation)
108
+ if (!same(r.input, input)) throw new Error('operation 已绑定其他输入')
109
+ return summary(r)
110
+ }
111
+ await requireServiceReady(client)
112
+ const uid = await owner(client), { doc, node, task, parent, estimate } = await context(client, input)
113
+ const output = { id: 'draft-final-' + crypto.randomUUID(), type: 'video',
114
+ position: { x: Math.round(Number(node.position?.x || 0) + Number(node.width || 0) + 80), y: Math.round(Number(node.position?.y || 0)) },
115
+ data: { title: '正式视频', mediaType: 'video', status: 'preparing', last_task_id: null,
116
+ extra: { node_kind: 'asset', aspect_ratio: task.result.aspect_ratio || '16:9',
117
+ draft_finalize: { role: 'generation-result', parent_node_id: node.id, parent_task_id: Number(task.task_id ?? task.id) } } } }
118
+ const edge = { id: 'draft-edge-' + crypto.randomUUID(), source: node.id, target: output.id, data: { draftFinalize: true } }
119
+ const wallet = { space: input.space, team_uid: doc.team_uid || null, source: doc.team_uid ? 'team' : 'personal' }
120
+ const r = { profile, operation, baseUrl: client.baseUrl, owner: uid, input, parent, output, edge,
121
+ hash: hash({ input, parent, output, edge }), estimate, wallet, key: crypto.randomUUID(), state: 'prepared', createdAt: new Date().toISOString() }
122
+ await persist(r)
123
+ return { ...summary(r), canvas_written: false }
124
+ }
125
+ function verifyOutput(doc, r) {
126
+ const node = doc.nodes.find(n => n.id === r.output.id)
127
+ if (!node || node.type !== 'video' || node.data?.extra?.node_kind !== 'asset' ||
128
+ !same(node.data.extra.draft_finalize, r.output.data.extra.draft_finalize) ||
129
+ !doc.edges.some(e => e.id === r.edge.id && e.source === r.edge.source && e.target === r.edge.target && e.data?.draftFinalize === true))
130
+ throw fail('DRAFT_SYNC_PENDING', '派生节点或来源线已变化;不重建、不提交、不覆盖')
131
+ return node
132
+ }
133
+ function verifyTask(task, r) {
134
+ if (!Number.isSafeInteger(Number(task.task_id ?? task.id)) || Number(task.task_id ?? task.id) < 1 ||
135
+ (r.taskId && Number(task.task_id ?? task.id) !== Number(r.taskId)) ||
136
+ task.doc_id !== r.input.doc_id || task.node_id !== r.output.id || task.segment_type !== 'video' ||
137
+ task.draft?.step !== 'finalize' || Number(task.draft.parent_task_id) !== Number(r.parent.task_id))
138
+ throw fail('DRAFT_SUBMISSION_UNKNOWN', '正片任务绑定未确认;保留原 operation,不换键重提')
139
+ }
140
+ export async function submitDraftFinalize(client, profile, operation, confirmed, expectedCost) {
141
+ const r = await bound(client, profile, operation)
142
+ if (r.taskId || ['submitting','unknown','saving','save_unknown'].includes(r.state)) return getDraftFinalize(client, profile, operation)
143
+ await requireServiceReady(client)
144
+ const { doc, parent, estimate } = await context(client, r.input, r.parent)
145
+ if (!same(parent, r.parent)) throw new Error('原样片参数或引用已变化;未提交')
146
+ if (!confirmed) return { ...summary(r), estimate, generation_submitted: false }
147
+ if (!Number.isFinite(expectedCost) || expectedCost < 0 || expectedCost !== estimate.estimated_credit_cost)
148
+ throw new Error('请展示实际钱包及继承参数、最新报价并取得当次确认;--expected-cost 必须匹配')
149
+ const protocol = await readProtocol(client)
150
+ if (protocol.required && !protocol.agreed) {
151
+ r.state = 'blocked_agreement'; await persist(r)
152
+ throw agreementError('AGREEMENT_REQUIRED', '由用户本人签署当前协议后,沿用原 operation 与原费用确认继续', protocol)
153
+ }
154
+ r.estimate = estimate; r.approvedCost = expectedCost
155
+ if (!r.nodeSaved) {
156
+ if (doc.nodes.some(n => n.id === r.output.id) || doc.edges.some(e => e.id === r.edge.id)) throw new Error('派生 ID 已存在;先查询原 operation')
157
+ const next = structuredClone(doc); next.nodes.push(r.output); next.edges.push(r.edge)
158
+ r.state = 'saving'; r.expectedVersion = doc.version; await persist(r)
159
+ try {
160
+ const saved = await save(client, doc, { nodes: next.nodes, edges: next.edges })
161
+ const actual = await scopedCanvas(client, r.input.doc_id, r.input.space, r.input.team_uid)
162
+ if (actual.version < saved.version || !same(actual.nodes, next.nodes) || !same(actual.edges, next.edges) ||
163
+ !same(actual.meta, doc.meta) || !same(actual.viewport, doc.viewport) || actual.name !== doc.name) throw new Error('正片节点读回不一致')
164
+ r.nodeSaved = true; r.state = 'node_saved'; await persist(r)
165
+ } catch (error) { r.state = 'save_unknown'; await persist(r); throw fail('DRAFT_SYNC_PENDING', '正片节点保存未确认;查询原 operation,不重放画布保存', error) }
166
+ } else verifyOutput(doc, r)
167
+ r.state = 'submitting'; await persist(r)
168
+ try {
169
+ const body = { doc_id: r.input.doc_id, node_id: r.output.id, draft_task_id: Number(r.parent.task_id), protocol_agreed: Boolean(protocol.agreed) }
170
+ const task = await client.raw('/canvas/generations/draft-finalize', { method: 'POST', body, headers: { 'Idempotency-Key': r.key } }, { retry: false })
171
+ verifyTask(task, r); r.taskId = task.task_id ?? task.id; r.state = 'submitted'; await persist(r)
172
+ return { ...summary(r), task }
173
+ } catch (error) {
174
+ r.state = error.apiCode === 4401 ? 'blocked_agreement' : 'unknown'; await persist(r)
175
+ if (error.apiCode === 4401) throw agreementError('AGREEMENT_REQUIRED', '当前协议需要本人签署;保留原 operation 和派生节点', protocol)
176
+ throw fail('DRAFT_SUBMISSION_UNKNOWN', '正片提交未知;只查询原 operation,不更换键、不删除派生节点', error)
177
+ }
178
+ }
179
+ export async function getDraftFinalize(client, profile, operation, sync = false) {
180
+ const r = await bound(client, profile, operation)
181
+ if (sync) await requireServiceReady(client)
182
+ const doc = await scopedCanvas(client, r.input.doc_id, r.input.space, r.input.team_uid); snapshot(doc)
183
+ if (['saving','save_unknown'].includes(r.state)) {
184
+ verifyOutput(doc, r)
185
+ const node = doc.nodes.find(n => n.id === r.output.id)
186
+ if (!same(node, r.output)) throw fail('DRAFT_SYNC_PENDING', '保存未知且派生内容变化,保留现场')
187
+ r.nodeSaved = true; r.state = 'node_saved'; await persist(r)
188
+ }
189
+ if (!r.taskId && ['submitting','unknown'].includes(r.state)) {
190
+ // Query the dedicated output node. No POST replay: parent expiry/entitlement
191
+ // checks can precede the backend's idempotency lookup.
192
+ const result = await client.raw('/canvas/generations?' + new URLSearchParams({ doc_id: r.input.doc_id, node_id: r.output.id, limit: 100 }))
193
+ if (!Array.isArray(result.items)) throw new Error('正片恢复查询响应无效')
194
+ const candidates = result.items.filter(t => t.node_id === r.output.id)
195
+ if (candidates.length !== 1) return { ...summary(r), next: '原任务尚未唯一可见;继续查询,不重提', replayed: false }
196
+ const task = await client.raw('/canvas/generations/' + segment(candidates[0].task_id ?? candidates[0].id))
197
+ verifyTask(task, { ...r, taskId: candidates[0].task_id ?? candidates[0].id }); r.taskId = task.task_id ?? task.id; r.state = 'submitted'; await persist(r)
198
+ }
199
+ if (!r.taskId) return summary(r)
200
+ const task = await client.raw('/canvas/generations/' + segment(r.taskId)); verifyTask(task, r)
201
+ if (!sync) return { ...summary(r), task }
202
+ const node = verifyOutput(doc, r), current = taskId(node)
203
+ if (current && current !== Number(r.taskId)) throw fail('DRAFT_SYNC_PENDING', '正片节点已绑定其他任务,不覆盖')
204
+ const next = structuredClone(doc), target = next.nodes.find(n => n.id === node.id)
205
+ target.data = { ...target.data, last_task_id: r.taskId, status: task.status,
206
+ runtime: { status: task.status, progress: task.progress || 0, error: task.error_message || '' },
207
+ extra: { ...target.data.extra, draft: task.draft } }
208
+ if (task.status === 'completed') {
209
+ if (!task.result?.url || !task.result.file_ref || !String(task.result.mime_type).startsWith('video/') || task.delivery_blocked) throw fail('DRAFT_SYNC_PENDING', '正片尚无可交付视频,未回填')
210
+ target.data.result = { ...task.result, task_id: r.taskId, status: 'completed' }
211
+ target.data.extra.file_ref = task.result.file_ref
212
+ }
213
+ if (!same(node, target)) {
214
+ try {
215
+ const saved = await save(client, doc, { nodes: next.nodes })
216
+ const actual = await scopedCanvas(client, r.input.doc_id, r.input.space, r.input.team_uid)
217
+ if (actual.version < saved.version || !same(actual.nodes, next.nodes) || !same(actual.edges, doc.edges) ||
218
+ !same(actual.meta, doc.meta) || !same(actual.viewport, doc.viewport) || actual.name !== doc.name) throw new Error('正片回填读回不一致')
219
+ } catch (error) { throw fail('DRAFT_SYNC_PENDING', '正片回填未确认;重新查询原 operation,不重提任务', error) }
220
+ }
221
+ r.state = task.status; await persist(r)
222
+ return { ...summary(r), task, canvas_synced: true }
223
+ }
224
+ export async function waitDraftFinalize(client, profile, operation, interval = 3000, timeout = 600000) {
225
+ const current = await getDraftFinalize(client, profile, operation)
226
+ if (!current.task_id) return current
227
+ await wait(client, current.task_id, interval, timeout)
228
+ return getDraftFinalize(client, profile, operation, true)
229
+ }
@@ -0,0 +1,124 @@
1
+ import { writeNode } from './canvas.js'
2
+ import { segment } from './workflows.js'
3
+ import fs from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import { root, safeName, json } from './store.js'
6
+ import { isDeepStrictEqual } from 'node:util'
7
+
8
+ async function submissionBaseline(client,taskId) {
9
+ if(!client.profile)return null
10
+ const directory=path.join(root(),'operations',safeName(client.profile))
11
+ const files=await fs.readdir(directory).catch(error=>{if(error.code==='ENOENT')return [];throw error})
12
+ const candidates=[]
13
+ for(const file of files.filter(f=>f.endsWith('.json'))){
14
+ const record=await json(path.join(directory,file),null)
15
+ if(String(record?.taskId)===String(taskId)&&record.baseUrl===client.baseUrl&&record.syncBaseline)candidates.push(record)
16
+ }
17
+ if(candidates.length!==1)return null
18
+ const me=await client.raw('/auth/me')
19
+ return String(me.uid??me.id)===candidates[0].owner?candidates[0].syncBaseline:null
20
+ }
21
+ async function matchingDraftModel(client,doc,node,task){
22
+ if(task.segment_type!=='video'||task.draft?.step!=='preview'||node.data?.extra?.draft_preview!==true)return false
23
+ const query=doc.team_uid?'?team_uid='+segment(doc.team_uid):''
24
+ const catalog=await client.raw('/canvas/models'+query)
25
+ const sample=catalog.items?.find(m=>m.label===node.data.model_name&&m.is_sample_mode===true)
26
+ if(!sample)return false
27
+ // The public deployment omits model_code/base_model_code. Only the verified
28
+ // Seedance 2.5 Draft alias is accepted in that reduced catalog contract.
29
+ if(!sample.base_model_code)return sample.label==='Seedance 2.5 Draft'&&task.model_name==='Seedance 2.5'&&
30
+ sample.sample_mode?.preview_resolution==='480p'&&catalog.items.some(m=>m.label===task.model_name&&m.is_sample_mode!==true)
31
+ const base=catalog.items.find(m=>m.model_code===sample.base_model_code&&m.is_sample_mode!==true)
32
+ return task.model_name===sample.base_model_code||!!base&&task.model_name===base.label
33
+ }
34
+
35
+ // Task completion and canvas persistence are separate contracts. Reconcile the
36
+ // original real task; never create or re-submit a generation here.
37
+ export async function syncGeneration(client, taskId, { baseline } = {}) {
38
+ const task = await client.raw(`/canvas/generations/${segment(taskId)}`)
39
+ if (
40
+ String(task.task_id ?? task.id) !== String(taskId) ||
41
+ !task.doc_id ||
42
+ !task.node_id ||
43
+ !['text', 'image', 'video'].includes(task.segment_type)
44
+ )
45
+ throw new Error('生成任务绑定不完整')
46
+ if (!['completed', 'failed', 'cancelled', 'canceled'].includes(task.status))
47
+ return { synced: false, task_id: taskId, status: task.status, next: '继续查询原任务' }
48
+ const doc = await client.raw(`/canvas/docs/${segment(task.doc_id)}`)
49
+ const node = doc.nodes?.find((n) => n.id === task.node_id)
50
+ if (
51
+ !node ||
52
+ node.type !== task.segment_type ||
53
+ node.data?.extra?.node_kind === 'asset' ||
54
+ node.data?.extra?.repaint ||
55
+ node.data?.extra?.video_remake ||
56
+ node.data?.extra?.clip_redraw || node.data?.extra?.draft_finalize || node.data?.draftFinalizeGeneration
57
+ )
58
+ throw new Error('任务目标不是普通生成节点;派生流程不能通用回填')
59
+ const current = String(node.data?.last_task_id || node.data?.result?.task_id || '')
60
+ if (current && current !== String(taskId)) {
61
+ const original=baseline || await submissionBaseline(client,taskId)
62
+ if(!original || original.doc_id!==doc.id || (original.team_uid||null)!==(doc.team_uid||null) ||
63
+ !isDeepStrictEqual(original.node,node) || !/^\d+$/.test(current) || BigInt(current)>=BigInt(taskId))
64
+ throw new Error('节点已关联其他生成任务;提交前快照不匹配,停止以保留后续结果')
65
+ const query=new URLSearchParams({doc_id:task.doc_id,node_id:task.node_id,limit:'1'})
66
+ const latest=await client.raw('/canvas/generations?'+query)
67
+ if(String(latest.items?.[0]?.task_id??latest.items?.[0]?.id)!==String(taskId))
68
+ throw new Error('节点已关联其他生成任务;存在后续任务或最新任务无法核验,停止回填')
69
+ }
70
+ if (node.data?.model_name && node.data.model_name !== task.model_name && !await matchingDraftModel(client,doc,node,task))
71
+ throw new Error('节点模型已变化;先核对任务与节点')
72
+ const patch = {
73
+ status: task.status,
74
+ last_task_id: task.task_id ?? task.id,
75
+ error_message: task.error_message || ''
76
+ }
77
+ if (task.status === 'completed') {
78
+ const result = task.result
79
+ if (
80
+ !result ||
81
+ (task.segment_type === 'text'
82
+ ? typeof result.text !== 'string' || !result.text.trim()
83
+ : !result.url || !result.file_ref)
84
+ )
85
+ throw new Error('已完成任务缺少真实输出;停止回填')
86
+ patch.result = { ...result, task_id: task.task_id ?? task.id, status: 'completed' }
87
+ if (task.segment_type === 'text') patch.output = result.text
88
+ else patch.extra = { file_ref: result.file_ref,
89
+ ...(task.segment_type==='video'&&task.draft?.step==='preview'?{draft:structuredClone(task.draft)}:{}) }
90
+ if(task.segment_type==='video'&&task.draft?.step==='preview'&&Object.hasOwn(node.data||{},'draft'))patch.draft=structuredClone(task.draft)
91
+ }
92
+ const same = Object.entries(patch).every(([key, value]) =>
93
+ key === 'extra'
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)
98
+ return {
99
+ synced: true,
100
+ unchanged: true,
101
+ task_id: taskId,
102
+ doc_id: doc.id,
103
+ node_id: node.id,
104
+ version: doc.version,
105
+ status: task.status
106
+ }
107
+ const bound = await writeNode(client, {
108
+ docId: doc.id,
109
+ nodeId: node.id,
110
+ patch: { data: patch },
111
+ update: true,
112
+ expectedSpace: doc.team_uid ? 'team' : 'personal',
113
+ expectedTeamUid: doc.team_uid,
114
+ expectedVersion: doc.version
115
+ })
116
+ return {
117
+ synced: true,
118
+ task_id: taskId,
119
+ doc_id: doc.id,
120
+ node_id: node.id,
121
+ version: bound.version,
122
+ status: task.status
123
+ }
124
+ }