@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,783 @@
1
+ import { archiveRepaintResult } from './repaint-archive.js'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import crypto from 'node:crypto'
5
+ import { isDeepStrictEqual } from 'node:util'
6
+ import { setTimeout as sleep } from 'node:timers/promises'
7
+ import { atomic, json, root, safeName } from './store.js'
8
+ import {
9
+ repaintPromptContext,
10
+ repaintPromptFields as fields
11
+ } from './repaint-reverse.js'
12
+ import { repaintOf, jobPath } from './repaint-graph.js'
13
+ import { requireServiceReady } from './service.js'
14
+ import { cancellation } from './api.js'
15
+ import { repaintAttachmentView } from './repaint-settings.js'
16
+
17
+ const hash = (value) =>
18
+ crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
19
+ const text = (value) => String(value ?? '')
20
+ const fail = (code, message, cause) =>
21
+ Object.assign(new Error(message), {
22
+ code,
23
+ ...(cause
24
+ ? {
25
+ cause,
26
+ status: cause.status,
27
+ apiCode: cause.apiCode,
28
+ requestId: cause.requestId
29
+ }
30
+ : {})
31
+ })
32
+ export const deliveryPath = (profile, operation) =>
33
+ path.join(
34
+ root(),
35
+ 'repaint-delivery',
36
+ safeName(profile),
37
+ safeName(operation) + '.json'
38
+ )
39
+ const persist = (record) =>
40
+ atomic(deliveryPath(record.profile, record.operation), record)
41
+ const api = (client, endpoint, options = {}, controls) =>
42
+ client.raw(
43
+ endpoint,
44
+ {
45
+ ...options,
46
+ headers: { ...options.headers, 'X-Client-Source': 'canvas' }
47
+ },
48
+ controls
49
+ )
50
+ const object = (value) =>
51
+ value && typeof value === 'object' && !Array.isArray(value)
52
+ export function deliveryInput(value = {}) {
53
+ if (
54
+ !object(value) ||
55
+ Object.keys(value).some((k) => !['rules', 'edited_sections'].includes(k))
56
+ )
57
+ throw new Error(
58
+ '最终提示词输入只支持rules/edited_sections;附件来自已同步全局节点,单次附件、模型或文化指令额外字段尚未接入'
59
+ )
60
+ if (
61
+ value.rules !== undefined &&
62
+ (!object(value.rules) ||
63
+ Object.keys(value.rules).some(
64
+ (k) =>
65
+ !['character', 'scene', 'sceneList', 'prop', 'dialogue'].includes(k)
66
+ ))
67
+ )
68
+ throw new Error('替换规则仅支持character/scene/sceneList/prop/dialogue')
69
+ const rules = value.rules || {}
70
+ for (const key of ['character', 'scene', 'sceneList', 'prop', 'dialogue']) {
71
+ const list = rules[key]
72
+ if (list === undefined) continue
73
+ if (key === 'scene' && typeof list === 'string') {
74
+ if (list.length > 12000) throw new Error('场景替换过长')
75
+ continue
76
+ }
77
+ if (
78
+ !Array.isArray(list) ||
79
+ list.length > 100 ||
80
+ list.some(
81
+ (item) =>
82
+ !object(item) ||
83
+ Object.keys(item).some((k) => !['source', 'target'].includes(k)) ||
84
+ typeof item.source !== 'string' ||
85
+ typeof item.target !== 'string' ||
86
+ item.source.length > 300 ||
87
+ item.target.length > 300
88
+ )
89
+ )
90
+ throw new Error(
91
+ '替换列表须为最多100项的source/target字符串,单项最长300字符'
92
+ )
93
+ }
94
+ if (
95
+ value.edited_sections !== undefined &&
96
+ (!object(value.edited_sections) ||
97
+ Object.entries(value.edited_sections).some(
98
+ ([k, v]) =>
99
+ !['character', 'scene', 'prop', 'dialogue'].includes(k) ||
100
+ typeof v !== 'string' ||
101
+ v.length > 12000
102
+ ))
103
+ )
104
+ throw new Error('edited_sections只接受四类文本')
105
+ return structuredClone(value)
106
+ }
107
+ function dialogueReplace(section, rules) {
108
+ const map = new Map(rules.map((item) => [item.source, item.target]))
109
+ return section
110
+ .split(/(\r?\n)/)
111
+ .map((line) => {
112
+ const source = line.trim(),
113
+ replacement = map.get(source)
114
+ if (!replacement) return line
115
+ const quoted = source.match(/^(.+?)\s*[::]\s*[“\"]([\s\S]*?)[”\"]\s*$/),
116
+ plain = source.match(/^(.+?)\s*[::]\s*(.+)$/)
117
+ const speaker = (quoted || plain)?.[1]?.trim()
118
+ return speaker ? `${speaker}:“${replacement}”` : replacement
119
+ })
120
+ .join('')
121
+ }
122
+ // Kept equivalent to the website's buildDeliveryPromptPayload; verified against it.
123
+ export function deliveryBody(reversePrompt, value = {}) {
124
+ const input = deliveryInput(value),
125
+ sections = { character: '', scene: '', prop: '', dialogue: '' }
126
+ if (input.edited_sections) Object.assign(sections, input.edited_sections)
127
+ else {
128
+ const keys = {
129
+ 角色: 'character',
130
+ 场景: 'scene',
131
+ 道具: 'prop',
132
+ 台词: 'dialogue'
133
+ }
134
+ const pattern =
135
+ /【(角色|场景|道具|台词)】\s*([\s\S]*?)(?=【(?:上一段视频结尾|开头|镜头|画面|角色|场景|道具|台词|负面提示词)】|$)/g
136
+ for (const match of text(reversePrompt?.prompt).matchAll(pattern))
137
+ sections[keys[match[1]]] = match[2].trim()
138
+ }
139
+ const normalize = (list) =>
140
+ (Array.isArray(list) ? list : [])
141
+ .map((item) => ({
142
+ source: item.source.trim(),
143
+ target: item.target.trim()
144
+ }))
145
+ .filter(
146
+ (item) => item.source && item.target && item.source !== item.target
147
+ )
148
+ const rules = input.rules || {},
149
+ replacement = typeof rules.scene === 'string' ? rules.scene.trim() : ''
150
+ const scene = replacement
151
+ ? sections.scene !== replacement
152
+ ? [{ source: sections.scene, target: replacement }]
153
+ : []
154
+ : normalize(Array.isArray(rules.scene) ? rules.scene : rules.sceneList)
155
+ sections.dialogue = dialogueReplace(
156
+ sections.dialogue,
157
+ normalize(rules.dialogue)
158
+ )
159
+ if (
160
+ Object.values(sections).some(
161
+ (value) => !value.trim() || value.length > 12000
162
+ )
163
+ )
164
+ throw new Error('反推正文缺少有效角色/场景/道具/台词章节;不能补造内容提交')
165
+ if (
166
+ scene.some((item) => item.source.length > 300 || item.target.length > 300)
167
+ )
168
+ throw new Error(
169
+ '场景映射超过接口300字符限制,改用有效的章节编辑或短名称替换'
170
+ )
171
+ return {
172
+ character: normalize(rules.character),
173
+ scene,
174
+ prop: normalize(rules.prop),
175
+ cultural_reference: '',
176
+ sections,
177
+ reference_images: []
178
+ }
179
+ }
180
+ const promptOf = (job, index) =>
181
+ (job.delivery_prompts || []).find((item) => item.segment_index === index) ||
182
+ null
183
+ const runState = (ctx) => ({
184
+ status:
185
+ ctx.output.delivery_prompt_status ||
186
+ (ctx.result ? 'completed' : 'not_started'),
187
+ prompt: ctx.result,
188
+ billing: ctx.output.delivery_prompt_billing || null
189
+ })
190
+ // The current attachment worker passes image URLs straight to the model and
191
+ // fetches PDFs by URL. A browser-only proxy address is not a storage resolver.
192
+ export function deliveryAttachmentBlockers(attachments) {
193
+ return attachments.flatMap((a, index) => {
194
+ const url = new URL(a.url),
195
+ host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '')
196
+ if (url.protocol === 'data:') {
197
+ const prefix = `data:${a.media_type.toLowerCase()};base64,`
198
+ if (
199
+ a.url.length <= 8000000 &&
200
+ a.url.toLowerCase().startsWith(prefix) &&
201
+ /^[a-zA-Z0-9+/]+={0,2}$/.test(a.url.slice(prefix.length))
202
+ )
203
+ return []
204
+ } else if (
205
+ url.protocol === 'https:' &&
206
+ !(
207
+ /^(?:localhost|.*\.localhost|.*\.local|0\.|10\.|127\.|169\.254\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(
208
+ host
209
+ ) ||
210
+ host === '::' ||
211
+ host === '::1' ||
212
+ /^(?:fc|fd|fe[89ab])[0-9a-f]*:/i.test(host) ||
213
+ host.includes(':')
214
+ )
215
+ )
216
+ return []
217
+ return [
218
+ {
219
+ code: 'DELIVERY_ATTACHMENT_ADDRESS_UNUSABLE',
220
+ attachment_index: index + 1,
221
+ name: a.name,
222
+ message:
223
+ '附件已上传,但当前地址只适用于本机/内网或格式不适合服务端读取;原文件引用保留。需同服务可访问的HTTPS文件地址,或服务端解析存储引用后再处理。'
224
+ }
225
+ ]
226
+ })
227
+ }
228
+ async function context(client, profile, parent, index) {
229
+ const ctx = await repaintPromptContext(client, profile, parent, index)
230
+ const reverse = ctx.output.reverse_prompt,
231
+ reverseStatus =
232
+ ctx.output.reverse_prompt_status ||
233
+ (reverse ? 'completed' : 'not_started')
234
+ if (reverseStatus !== 'completed' || !text(reverse?.prompt).trim())
235
+ throw new Error('先完成原片段反推,不用空节点代替反推结果')
236
+ if (ctx.final.type !== 'text') throw new Error('最终提示词节点类型已变化')
237
+ const generation = ctx.doc.nodes.filter(
238
+ (n) =>
239
+ repaintOf(n).jobId === ctx.job.id &&
240
+ repaintOf(n).role === 'generation' &&
241
+ repaintOf(n).segmentIndex === index
242
+ )
243
+ if (
244
+ generation.length !== 1 ||
245
+ !ctx.doc.edges.some(
246
+ (e) =>
247
+ e.source === ctx.final.id &&
248
+ e.target === generation[0].id &&
249
+ e.data?.role === 'final-generation-link'
250
+ ) ||
251
+ !ctx.doc.edges.some(
252
+ (e) =>
253
+ e.source === ctx.global.id &&
254
+ e.target === ctx.final.id &&
255
+ e.data?.role === 'global-prompt-link'
256
+ )
257
+ )
258
+ throw new Error('最终提示词下游或全局系统线缺失,不重建')
259
+ const localAttachments = repaintAttachmentView(
260
+ repaintOf(ctx.global).attachments,
261
+ client.baseUrl
262
+ ),
263
+ remoteAttachments = repaintAttachmentView(
264
+ ctx.job.delivery_prompt_attachments,
265
+ client.baseUrl
266
+ )
267
+ if (!isDeepStrictEqual(localAttachments, remoteAttachments))
268
+ throw new Error('画布与后端全局附件不一致,不清空或覆盖后提交')
269
+ ctx.attachments = remoteAttachments
270
+ ctx.result = promptOf(ctx.job, index)
271
+ ctx.node = ctx.final
272
+ ctx.binding = {
273
+ ...ctx.binding,
274
+ node_id: ctx.final.id,
275
+ reverse_hash: hash(reverse),
276
+ ...(ctx.attachments.length ? { attachments: ctx.attachments } : {})
277
+ }
278
+ return ctx
279
+ }
280
+ async function quote(client, ctx) {
281
+ const pricing = await api(client, '/video-segmentation/pricing'),
282
+ unit = pricing.ai_edit_credits_per_segment,
283
+ benefit = ctx.job.billing?.has_base_benefit
284
+ if (
285
+ pricing.currency !== 'OO' ||
286
+ !Number.isSafeInteger(unit) ||
287
+ unit < 0 ||
288
+ typeof benefit !== 'boolean'
289
+ )
290
+ throw new Error('最终提示词实时单价或基础权益未知')
291
+ const me = await client.raw('/auth/me'),
292
+ team =
293
+ ctx.parent.input.space === 'team'
294
+ ? me.teams?.find(
295
+ (t) =>
296
+ text(t.uid) === ctx.parent.input.team_uid && t.status === 'active'
297
+ )
298
+ : null
299
+ if (
300
+ text(me.uid) !== ctx.parent.owner ||
301
+ (ctx.parent.input.space === 'team' && !team)
302
+ )
303
+ throw new Error('账号或团队成员身份变化')
304
+ const first = benefit && !ctx.result
305
+ let extra = 0,
306
+ attachmentPricing
307
+ if (ctx.attachments.length) {
308
+ const keys = [
309
+ 'attachment_free_images',
310
+ 'attachment_free_docs',
311
+ 'attachment_extra_fee',
312
+ 'attachment_max'
313
+ ]
314
+ if (
315
+ keys.some((k) => !Number.isSafeInteger(pricing[k]) || pricing[k] < 0) ||
316
+ pricing.attachment_max < ctx.attachments.length
317
+ )
318
+ throw new Error('附件实时限额/价格无效或数量超限')
319
+ const images = ctx.attachments.filter((a) =>
320
+ a.media_type.toLowerCase().startsWith('image/')
321
+ ).length,
322
+ docs = ctx.attachments.length - images
323
+ extra =
324
+ images > pricing.attachment_free_images ||
325
+ docs > pricing.attachment_free_docs
326
+ ? pricing.attachment_extra_fee
327
+ : 0
328
+ const estimated = await api(
329
+ client,
330
+ jobPath(ctx.job.id) + '/billing-estimate',
331
+ {
332
+ method: 'POST',
333
+ body: { segment_count: 1, attachments: ctx.attachments }
334
+ },
335
+ { retry: false }
336
+ )
337
+ if (
338
+ estimated?.currency !== 'OO' ||
339
+ estimated.segment_count !== 1 ||
340
+ ['ai_edit_credits_per_segment', ...keys].some(
341
+ (k) => estimated.pricing?.[k] !== pricing[k]
342
+ ) ||
343
+ estimated.manual_segmentation !== !benefit ||
344
+ estimated.attachments?.count !== ctx.attachments.length ||
345
+ estimated.attachments.image_files !== images ||
346
+ estimated.attachments.pdf_files !== docs ||
347
+ estimated.attachments.extra_fee !== extra ||
348
+ estimated.breakdown?.ai_edit?.first_run !== (benefit ? 0 : unit) ||
349
+ estimated.breakdown?.ai_edit?.rerun_all !== unit ||
350
+ estimated.breakdown?.attachment_extra?.first_run !== extra ||
351
+ estimated.breakdown?.attachment_extra?.rerun_all !== extra
352
+ )
353
+ throw new Error(
354
+ '附件预估与实时价格/数量/权益不一致;不使用历史任务价格或整任务总价提交单段处理'
355
+ )
356
+ attachmentPricing = {
357
+ images,
358
+ docs,
359
+ ...Object.fromEntries(keys.map((k) => [k, pricing[k]])),
360
+ extra_fee_per_segment_per_run: extra,
361
+ server_estimate_verified: true
362
+ }
363
+ }
364
+ return {
365
+ currency: 'OO',
366
+ credits: (first ? 0 : unit) + extra,
367
+ base_credits: first ? 0 : unit,
368
+ attachment_credits: extra,
369
+ unit_price: unit,
370
+ first_run_included: first,
371
+ attachments: ctx.attachments.length,
372
+ ...(attachmentPricing ? { attachment_pricing: attachmentPricing } : {}),
373
+ wallet: {
374
+ space: ctx.parent.input.space,
375
+ team_uid: ctx.parent.input.team_uid || null,
376
+ team_name: team?.name || null,
377
+ shared: Boolean(team),
378
+ balance_snapshot: (team || me).oo_balance ?? null
379
+ }
380
+ }
381
+ }
382
+ const costKey = (q) =>
383
+ hash({
384
+ credits: q.credits,
385
+ unit_price: q.unit_price,
386
+ first_run_included: q.first_run_included,
387
+ space: q.wallet.space,
388
+ team_uid: q.wallet.team_uid,
389
+ ...(q.attachments
390
+ ? { attachments: q.attachments, attachment_pricing: q.attachment_pricing }
391
+ : {})
392
+ })
393
+ async function bound(client, profile, operation) {
394
+ const record = await json(deliveryPath(profile, operation), null)
395
+ if (
396
+ !record ||
397
+ record.schema !== 1 ||
398
+ record.profile !== profile ||
399
+ record.baseUrl !== client.baseUrl
400
+ )
401
+ throw new Error('最终提示词operation不存在或服务不匹配')
402
+ const ctx = await context(
403
+ client,
404
+ profile,
405
+ record.parentOperation,
406
+ record.index
407
+ )
408
+ if (
409
+ record.owner !== ctx.parent.owner ||
410
+ !isDeepStrictEqual(ctx.binding, record.binding)
411
+ )
412
+ throw new Error('原方案、反推原文、全局规则/附件或节点关联变化,停止恢复')
413
+ return { record, ctx }
414
+ }
415
+ async function preventDuplicate(profile, operation, ctx) {
416
+ const dir = path.dirname(deliveryPath(profile, operation)),
417
+ files = await fs.readdir(dir).catch((e) => {
418
+ if (e.code === 'ENOENT') return []
419
+ throw e
420
+ })
421
+ for (const file of files.filter((f) => f.endsWith('.json'))) {
422
+ const r = await json(path.join(dir, file))
423
+ if (
424
+ r.operation !== operation &&
425
+ r.owner === ctx.parent.owner &&
426
+ r.baseUrl === ctx.parent.baseUrl &&
427
+ r.binding?.job_id === ctx.job.id &&
428
+ r.index === ctx.binding.index &&
429
+ r.attempted &&
430
+ !['completed', 'completed_fallback', 'failed'].includes(r.state)
431
+ )
432
+ throw new Error('该片段已有未结束或未知最终提示词operation,不能换名重提')
433
+ }
434
+ }
435
+ function summary(record, ctx) {
436
+ const state = runState(ctx),
437
+ prompt = state.prompt?.prompt
438
+ return {
439
+ operation: record.operation,
440
+ repaint_operation: record.parentOperation,
441
+ state: record.state,
442
+ job_id: ctx.job.id,
443
+ plan_version: ctx.job.plan_version,
444
+ segment_index: record.index,
445
+ node_id: ctx.node.id,
446
+ attachment_count: ctx.attachments.length,
447
+ attachment_address_blockers: deliveryAttachmentBlockers(ctx.attachments),
448
+ processing_submitted: record.accepted
449
+ ? true
450
+ : record.attempted
451
+ ? null
452
+ : false,
453
+ remote_status: state.status,
454
+ canvas_synced:
455
+ record.canvasSynced === true &&
456
+ Boolean(prompt) &&
457
+ isDeepStrictEqual(fields(ctx.node), {
458
+ input: prompt,
459
+ prompt,
460
+ output: prompt
461
+ }),
462
+ billing: state.billing,
463
+ delivery_prompt: state.prompt,
464
+ fallback: ctx.output.delivery_prompt_fallback === true,
465
+ warning: ctx.output.delivery_prompt_warning || null,
466
+ remote_error: ctx.output.delivery_prompt_error || null,
467
+ library_saved: false
468
+ }
469
+ }
470
+ export function validateDeliveryRun(record, ctx) {
471
+ if (!record.accepted)
472
+ throw fail(
473
+ 'DELIVERY_SUBMISSION_UNKNOWN',
474
+ '最终提示词响应未知;仅查询原任务,不认领观察结果或重发POST'
475
+ )
476
+ const state = runState(ctx),
477
+ billing = state.billing
478
+ if (
479
+ !billing ||
480
+ billing.plan_version !== record.binding.plan_version ||
481
+ billing.amount !== record.approvedCost ||
482
+ (record.reservationKey && billing.reservation_key !== record.reservationKey)
483
+ )
484
+ throw new Error('最终提示词运行/计费标记变化,不认领另一轮结果')
485
+ if (state.status === 'completed' && !text(state.prompt?.prompt).trim())
486
+ throw new Error('完成但缺少真实最终提示词,不回填空内容')
487
+ if (
488
+ record.resultHash &&
489
+ (state.status !== 'completed' || hash(state.prompt) !== record.resultHash)
490
+ )
491
+ throw new Error('原最终提示词被另一轮替换,停止覆盖')
492
+ return state
493
+ }
494
+ const validateRun = validateDeliveryRun
495
+ export async function startDelivery(
496
+ client,
497
+ profile,
498
+ parent,
499
+ index,
500
+ operation,
501
+ value = {},
502
+ confirmed = false,
503
+ expectedCost
504
+ ) {
505
+ safeName(parent)
506
+ safeName(operation)
507
+ const input = deliveryInput(value),
508
+ ctx = await context(client, profile, parent, index)
509
+ let record = await json(deliveryPath(profile, operation), null)
510
+ if (
511
+ record &&
512
+ (record.parentOperation !== parent ||
513
+ record.index !== index ||
514
+ record.baseUrl !== client.baseUrl ||
515
+ record.owner !== ctx.parent.owner ||
516
+ !isDeepStrictEqual(record.binding, ctx.binding) ||
517
+ !isDeepStrictEqual(record.input, input))
518
+ )
519
+ throw new Error('原最终提示词operation参数不能修改')
520
+ if (record?.attempted) return recoverDelivery(client, profile, operation)
521
+ await preventDuplicate(profile, operation, ctx)
522
+ const state = runState(ctx)
523
+ if (!['not_started', 'completed'].includes(state.status))
524
+ throw new Error(
525
+ '该片段最终提示词运行中、失败或状态未知,先查原操作,不自动重做'
526
+ )
527
+ const body = deliveryBody(ctx.output.reverse_prompt, input),
528
+ estimate = await quote(client, ctx),
529
+ blockers = deliveryAttachmentBlockers(ctx.attachments)
530
+ if (!confirmed) {
531
+ record = {
532
+ schema: 1,
533
+ profile,
534
+ operation,
535
+ parentOperation: parent,
536
+ index,
537
+ owner: ctx.parent.owner,
538
+ baseUrl: client.baseUrl,
539
+ binding: ctx.binding,
540
+ input,
541
+ body,
542
+ baseline: state,
543
+ nodeBefore: fields(ctx.node),
544
+ estimate,
545
+ estimateKey: costKey(estimate),
546
+ state: 'prepared',
547
+ createdAt: new Date().toISOString(),
548
+ ...(record?.settingsAttempted
549
+ ? { settingsAttempted: true, settingsVerified: record.settingsVerified }
550
+ : {})
551
+ }
552
+ await persist(record)
553
+ return {
554
+ ...summary(record, ctx),
555
+ submitted: false,
556
+ submission_available: blockers.length === 0,
557
+ blockers,
558
+ estimate,
559
+ parameters: {
560
+ start_ms: ctx.binding.start_ms,
561
+ end_ms: ctx.binding.end_ms,
562
+ global_rules: ctx.binding.rules,
563
+ attachments: ctx.attachments.map((a) => ({
564
+ name: a.name,
565
+ media_type: a.media_type
566
+ })),
567
+ body,
568
+ model: 'service_managed'
569
+ },
570
+ next: '通过交互卡片明确替换要求或不替换,展示钱包/规则/费用;当次确认后同operation加--yes --expected-cost。不生成视频'
571
+ }
572
+ }
573
+ if (blockers.length)
574
+ throw Object.assign(
575
+ fail(
576
+ 'DELIVERY_ATTACHMENT_ADDRESS_UNUSABLE',
577
+ '附件已上传,但服务端/模型读取地址不适用;未提交最终提示词,不重新上传素材'
578
+ ),
579
+ { blockers }
580
+ )
581
+ if (
582
+ !record ||
583
+ expectedCost === undefined ||
584
+ Number(expectedCost) !== estimate.credits ||
585
+ record.estimateKey !== costKey(estimate) ||
586
+ !isDeepStrictEqual(record.body, body) ||
587
+ !isDeepStrictEqual(record.baseline, state) ||
588
+ !isDeepStrictEqual(record.nodeBefore, fields(ctx.node))
589
+ )
590
+ throw new Error('先预估并当次确认;费用/原文/规则/节点变化须重新核对')
591
+ await requireServiceReady(client)
592
+ if (record.settingsAttempted && !record.settingsVerified)
593
+ throw fail(
594
+ 'DELIVERY_SUBMISSION_UNKNOWN',
595
+ '设置保存响应未知;先delivery-recover核对,不重发设置或生成'
596
+ )
597
+ if (!record.settingsVerified) {
598
+ record.settingsAttempted = true
599
+ record.state = 'saving_settings'
600
+ await persist(record)
601
+ try {
602
+ await api(
603
+ client,
604
+ jobPath(ctx.job.id) + '/delivery-prompt-settings',
605
+ {
606
+ method: 'POST',
607
+ body: {
608
+ custom_prompt: ctx.binding.rules,
609
+ attachments: ctx.attachments
610
+ }
611
+ },
612
+ { retry: false }
613
+ )
614
+ record.settingsVerified = true
615
+ await persist(record)
616
+ } catch (error) {
617
+ record.state = 'settings_unknown'
618
+ await persist(record)
619
+ throw fail(
620
+ 'DELIVERY_SUBMISSION_UNKNOWN',
621
+ '全局设置保存未确认,未发送生成;先检查后端并查原操作',
622
+ error
623
+ )
624
+ }
625
+ }
626
+ const fresh = await context(client, profile, parent, index)
627
+ if (
628
+ !isDeepStrictEqual(fresh.binding, record.binding) ||
629
+ !isDeepStrictEqual(runState(fresh), record.baseline) ||
630
+ !isDeepStrictEqual(fields(fresh.node), record.nodeBefore) ||
631
+ costKey(await quote(client, fresh)) !== record.estimateKey
632
+ )
633
+ throw new Error('设置保存后参数/费用变化,尚未提交最终提示词')
634
+ record.attempted = true
635
+ record.approvedCost = estimate.credits
636
+ record.state = 'submitting'
637
+ await persist(record)
638
+ try {
639
+ const response = await api(
640
+ client,
641
+ jobPath(ctx.job.id) + '/segments/' + index + '/delivery-prompt',
642
+ { method: 'POST', body },
643
+ { retry: false }
644
+ ),
645
+ output = response?.outputs?.find((o) => o.index === index && !o.type)
646
+ if (
647
+ response?.id === ctx.job.id &&
648
+ response.canvas_doc_id === ctx.doc.id &&
649
+ text(response.team_uid || '') === text(ctx.doc.team_uid || '') &&
650
+ response.plan_version === record.binding.plan_version &&
651
+ output?.delivery_prompt_billing?.plan_version ===
652
+ record.binding.plan_version
653
+ ) {
654
+ record.accepted = true
655
+ record.reservationKey =
656
+ output.delivery_prompt_billing.reservation_key || null
657
+ await persist(record)
658
+ }
659
+ if (!record.accepted) throw new Error('响应缺少本次原任务运行标记')
660
+ record.state = 'running'
661
+ await persist(record)
662
+ } catch (error) {
663
+ record.state = 'submission_unknown'
664
+ await persist(record)
665
+ throw fail(
666
+ 'DELIVERY_SUBMISSION_UNKNOWN',
667
+ '最终提示词提交结果未确认;仅查原job/segment,不重发或换operation',
668
+ error
669
+ )
670
+ }
671
+ return recoverDelivery(client, profile, operation)
672
+ }
673
+ export async function getDelivery(client, profile, operation) {
674
+ const { record, ctx } = await bound(client, profile, operation)
675
+ if (record.accepted) validateRun(record, ctx)
676
+ return summary(record, ctx)
677
+ }
678
+ export async function recoverDelivery(client, profile, operation) {
679
+ const { record, ctx } = await bound(client, profile, operation)
680
+ if (!record.attempted) {
681
+ if (record.settingsAttempted) {
682
+ record.settingsVerified = true
683
+ record.state = 'settings_verified'
684
+ await persist(record)
685
+ }
686
+ return summary(record, ctx)
687
+ }
688
+ const state = validateRun(record, ctx)
689
+ if (state.status === 'failed') {
690
+ record.state = 'failed'
691
+ await persist(record)
692
+ return summary(record, ctx)
693
+ }
694
+ if (state.status !== 'completed') {
695
+ record.state = 'running'
696
+ await persist(record)
697
+ return summary(record, ctx)
698
+ }
699
+ await archiveRepaintResult(record, 'delivery', state.prompt, state.billing, { fallback: ctx.output.delivery_prompt_fallback === true, warning: ctx.output.delivery_prompt_warning || null })
700
+ const prompt = state.prompt.prompt,
701
+ expected = { input: prompt, prompt, output: prompt },
702
+ current = fields(ctx.node)
703
+ if (
704
+ !isDeepStrictEqual(current, expected) &&
705
+ !isDeepStrictEqual(current, record.nodeBefore)
706
+ )
707
+ throw fail(
708
+ 'DELIVERY_SYNC_PENDING',
709
+ '最终提示词节点已被用户编辑,不覆盖;原结果仍可查询'
710
+ )
711
+ record.resultHash = hash(state.prompt)
712
+ record.state = ctx.output.delivery_prompt_fallback
713
+ ? 'completed_fallback'
714
+ : 'completed'
715
+ await persist(record)
716
+ if (!isDeepStrictEqual(current, expected)) {
717
+ ctx.node.data = { ...ctx.node.data, ...expected }
718
+ try {
719
+ const saved = await api(
720
+ client,
721
+ '/canvas/docs/' + encodeURIComponent(ctx.doc.id),
722
+ {
723
+ method: 'PUT',
724
+ body: {
725
+ version: ctx.doc.version,
726
+ name: ctx.doc.name,
727
+ viewport: ctx.doc.viewport,
728
+ nodes: ctx.doc.nodes,
729
+ edges: ctx.doc.edges,
730
+ meta: ctx.doc.meta
731
+ }
732
+ },
733
+ { retry: false }
734
+ )
735
+ if (
736
+ saved.version !== ctx.doc.version + 1 ||
737
+ !isDeepStrictEqual(
738
+ fields(saved.nodes?.find((n) => n.id === ctx.node.id) || {}),
739
+ expected
740
+ )
741
+ )
742
+ throw new Error('保存响应不完整')
743
+ } catch (error) {
744
+ record.state = 'sync_pending'
745
+ await persist(record)
746
+ throw fail(
747
+ 'DELIVERY_SYNC_PENDING',
748
+ '原最终提示词已完成,画布保存未确认;恢复只同步原结果,不重新生成',
749
+ error
750
+ )
751
+ }
752
+ }
753
+ record.canvasSynced = true
754
+ await persist(record)
755
+ return summary(record, ctx)
756
+ }
757
+ export async function waitDelivery(
758
+ client,
759
+ profile,
760
+ operation,
761
+ interval = 2000,
762
+ timeout = 180000
763
+ ) {
764
+ if (![interval, timeout].every((v) => Number.isSafeInteger(v) && v > 0))
765
+ throw new Error('等待参数必须为正整数')
766
+ const deadline = Date.now() + timeout
767
+ while (Date.now() < deadline) {
768
+ const result = await recoverDelivery(client, profile, operation)
769
+ if (['completed', 'completed_fallback'].includes(result.state))
770
+ return result
771
+ if (result.state === 'failed')
772
+ throw fail('TASK_FAILED', '原最终提示词任务失败,不自动重做')
773
+ if (!result.processing_submitted) return result
774
+ await sleep(
775
+ Math.min(interval, Math.max(1, deadline - Date.now())),
776
+ undefined,
777
+ {
778
+ signal: cancellation.signal
779
+ }
780
+ )
781
+ }
782
+ throw fail('WAIT_TIMEOUT', '最终提示词等待超时;继续原operation,不重新提交')
783
+ }