@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,614 @@
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 { repaintGet, recordPath } from './repaint.js'
9
+ import { readCanvas, repaintOf, jobPath } from './repaint-graph.js'
10
+ import { requireServiceReady } from './service.js'
11
+ import { cancellation } from './api.js'
12
+
13
+ const hash = (value) =>
14
+ crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
15
+ const text = (value) => String(value ?? '')
16
+ const fail = (code, message, cause) =>
17
+ Object.assign(new Error(message), {
18
+ code,
19
+ ...(cause
20
+ ? {
21
+ cause,
22
+ status: cause.status,
23
+ apiCode: cause.apiCode,
24
+ requestId: cause.requestId
25
+ }
26
+ : {})
27
+ })
28
+ export const reversePath = (profile, operation) =>
29
+ path.join(
30
+ root(),
31
+ 'repaint-reverse',
32
+ safeName(profile),
33
+ safeName(operation) + '.json'
34
+ )
35
+ const persist = (record) =>
36
+ atomic(reversePath(record.profile, record.operation), record)
37
+ const api = (client, endpoint, options = {}, controls) =>
38
+ client.raw(
39
+ endpoint,
40
+ {
41
+ ...options,
42
+ headers: { ...options.headers, 'X-Client-Source': 'canvas' }
43
+ },
44
+ controls
45
+ )
46
+ const fields = (node) =>
47
+ Object.fromEntries(
48
+ ['input', 'prompt', 'output'].map((key) => [key, text(node.data?.[key])])
49
+ )
50
+ const globalText = (node) =>
51
+ text(node.data?.output || node.data?.prompt || node.data?.input)
52
+ .replace(/<[^>]*>/g, '')
53
+ .trim()
54
+ const runState = (output) => ({
55
+ status:
56
+ output.reverse_prompt_status ||
57
+ (output.reverse_prompt ? 'completed' : 'not_started'),
58
+ prompt: output.reverse_prompt || null,
59
+ billing: output.reverse_prompt_billing || null
60
+ })
61
+ const link = (doc, a, b, role) =>
62
+ doc.edges.some(
63
+ (edge) => edge.source === a && edge.target === b && edge.data?.role === role
64
+ )
65
+
66
+ // The parent workflow performs account, service, ownership and original-source checks.
67
+ async function context(
68
+ client,
69
+ profile,
70
+ parentOperation,
71
+ index,
72
+ { allowUnsyncedGlobal = false } = {}
73
+ ) {
74
+ if (!Number.isSafeInteger(index) || index < 1)
75
+ throw new Error('segment-index必须为正整数')
76
+ const parent = await json(recordPath(profile, parentOperation), null)
77
+ if (!parent)
78
+ throw new Error('先查询已有转绘operation,不能自行创建任务或片段')
79
+ const { job } = await repaintGet(client, profile, parentOperation)
80
+ const doc = await readCanvas(client, parent.input)
81
+ const marker = doc.meta.cli_repaint_workflows?.[parentOperation]
82
+ if (
83
+ job.status !== 'completed' ||
84
+ !job.confirmed_at ||
85
+ job.confirmed_plan_version !== job.plan_version ||
86
+ marker?.job_id !== job.id ||
87
+ !marker.materialized ||
88
+ marker.plan_version !== job.plan_version ||
89
+ parent.lastPlanVersion !== job.plan_version
90
+ )
91
+ throw new Error(
92
+ '原分段任务/版本/已物化画布绑定不完整;先恢复原流程,不提交反推'
93
+ )
94
+ const group = doc.meta.canvas_groups?.find(
95
+ (item) => item.id === marker.group_id
96
+ )
97
+ if (
98
+ !group ||
99
+ (group.repaint?.jobId || group.repaintJobId) !== job.id ||
100
+ group.repaint?.status !== 'completed' ||
101
+ !Array.isArray(group.nodeIds) ||
102
+ !Array.isArray(marker.node_ids) ||
103
+ !isDeepStrictEqual(
104
+ [...group.nodeIds].sort(),
105
+ [...marker.node_ids].sort()
106
+ ) ||
107
+ !group.nodeIds.every((id) => doc.nodes.some((node) => node.id === id))
108
+ )
109
+ throw new Error('派生分组/节点被删除或改变;不自动重建')
110
+ const choose = (role, segmentIndex) => {
111
+ const nodes = doc.nodes.filter(
112
+ (node) =>
113
+ group.nodeIds.includes(node.id) &&
114
+ repaintOf(node).jobId === job.id &&
115
+ repaintOf(node).role === role &&
116
+ (segmentIndex === undefined ||
117
+ repaintOf(node).segmentIndex === segmentIndex)
118
+ )
119
+ if (nodes.length !== 1) throw new Error('派生节点角色/片段关联不唯一或缺失')
120
+ return nodes[0]
121
+ }
122
+ const node = choose('reverse-prompt', index),
123
+ source = choose('segment', index),
124
+ final = choose('final-prompt', index),
125
+ global = choose('global-prompt')
126
+ if (
127
+ node.type !== 'text' ||
128
+ !doc.nodes.some(
129
+ (n) => n.id === marker.detail_id && repaintOf(n).jobId === job.id
130
+ ) ||
131
+ !link(
132
+ doc,
133
+ parent.input.source_node_id,
134
+ parent.input.entry_node_id,
135
+ 'video-repaint-input'
136
+ ) ||
137
+ !link(
138
+ doc,
139
+ parent.input.entry_node_id,
140
+ marker.detail_id,
141
+ 'split-detail-link'
142
+ ) ||
143
+ !link(doc, marker.detail_id, source.id, 'source-link') ||
144
+ !link(doc, source.id, node.id, 'segment-reverse-link') ||
145
+ !link(doc, node.id, final.id, 'reverse-final-link')
146
+ )
147
+ throw new Error('原派生链或系统线缺失,不提交或重建')
148
+ const segments = job.plan?.segments?.filter((item) => item.index === index)
149
+ const outputs = job.outputs?.filter(
150
+ (item) => item.index === index && !item.type
151
+ )
152
+ if (segments?.length !== 1 || outputs?.length !== 1 || !outputs[0].object_key)
153
+ throw new Error('原任务缺少唯一真实切片产物')
154
+ if (
155
+ !allowUnsyncedGlobal &&
156
+ globalText(global) !== text(job.delivery_prompt_custom_prompt).trim()
157
+ )
158
+ throw new Error(
159
+ '画布全局规则与后端不同步;先明确并同步规则,不隐式覆盖后提交'
160
+ )
161
+ const segment = segments[0],
162
+ output = outputs[0]
163
+ let previousSegment
164
+ if (index > 1) {
165
+ const previousOutputs = job.outputs.filter(item => item.index === index - 1 && !item.type)
166
+ const previousPlans = job.plan.segments.filter(item => item.index === index - 1)
167
+ if (previousOutputs.length !== 1 || !previousOutputs[0].object_key || previousPlans.length !== 1)
168
+ throw new Error('非首段缺少唯一真实上一段视频上下文,停止反推')
169
+ previousSegment = { index: index - 1, object_key: previousOutputs[0].object_key,
170
+ start_ms: previousPlans[0].start_ms, end_ms: previousPlans[0].end_ms }
171
+ }
172
+ const binding = {
173
+ job_id: job.id,
174
+ plan_version: job.plan_version,
175
+ node_id: node.id,
176
+ source_id: source.id,
177
+ group_id: group.id,
178
+ index,
179
+ start_ms: segment.start_ms,
180
+ end_ms: segment.end_ms,
181
+ object_key: output.object_key,
182
+ ...(previousSegment ? { previous_segment: previousSegment } : {}),
183
+ rules: text(job.delivery_prompt_custom_prompt).trim()
184
+ }
185
+ return { parent, job, doc, node, output, binding, final, global }
186
+ }
187
+ async function quote(client, ctx) {
188
+ const pricing = await api(client, '/video-segmentation/pricing')
189
+ const unit = pricing.reverse_prompt_credits_per_segment,
190
+ benefit = ctx.job.billing?.has_base_benefit
191
+ if (
192
+ pricing.currency !== 'OO' ||
193
+ !Number.isSafeInteger(unit) ||
194
+ unit < 0 ||
195
+ typeof benefit !== 'boolean'
196
+ )
197
+ throw new Error('实时反推价格或首轮权益未知,停止')
198
+ const firstFree = benefit && !ctx.output.reverse_prompt
199
+ const me = await client.raw('/auth/me')
200
+ const team =
201
+ ctx.parent.input.space === 'team'
202
+ ? me.teams?.find(
203
+ (item) =>
204
+ text(item.uid) === ctx.parent.input.team_uid &&
205
+ item.status === 'active'
206
+ )
207
+ : null
208
+ if (
209
+ text(me.uid) !== ctx.parent.owner ||
210
+ (ctx.parent.input.space === 'team' && !team)
211
+ )
212
+ throw new Error('账号/团队成员身份已变化')
213
+ return {
214
+ currency: 'OO',
215
+ credits: firstFree ? 0 : unit,
216
+ unit_price: unit,
217
+ first_run_included: firstFree,
218
+ wallet: {
219
+ space: ctx.parent.input.space,
220
+ team_uid: ctx.parent.input.team_uid || null,
221
+ team_name: team?.name || null,
222
+ shared: Boolean(team),
223
+ balance_snapshot: (team || me).oo_balance ?? null
224
+ }
225
+ }
226
+ }
227
+ const costKey = (estimate) =>
228
+ hash({
229
+ credits: estimate.credits,
230
+ unit_price: estimate.unit_price,
231
+ first_run_included: estimate.first_run_included,
232
+ space: estimate.wallet.space,
233
+ team_uid: estimate.wallet.team_uid
234
+ })
235
+ async function bound(client, profile, operation) {
236
+ const record = await json(reversePath(profile, operation), null)
237
+ if (
238
+ !record ||
239
+ record.schema !== 1 ||
240
+ record.profile !== profile ||
241
+ record.baseUrl !== client.baseUrl
242
+ )
243
+ throw new Error('反推operation不存在或服务不匹配')
244
+ const ctx = await context(
245
+ client,
246
+ profile,
247
+ record.parentOperation,
248
+ record.index
249
+ )
250
+ if (
251
+ record.owner !== ctx.parent.owner ||
252
+ !isDeepStrictEqual(ctx.binding, record.binding)
253
+ )
254
+ throw new Error('反推原任务、片段、规则或版本已变化,停止恢复')
255
+ return { record, ctx }
256
+ }
257
+ async function preventDuplicate(profile, operation, ctx) {
258
+ const directory = path.dirname(reversePath(profile, operation))
259
+ const files = await fs.readdir(directory).catch((error) => {
260
+ if (error.code === 'ENOENT') return []
261
+ throw error
262
+ })
263
+ for (const file of files.filter((file) => file.endsWith('.json'))) {
264
+ const other = await json(path.join(directory, file))
265
+ if (
266
+ other.operation !== operation &&
267
+ other.owner === ctx.parent.owner &&
268
+ other.baseUrl === ctx.parent.baseUrl &&
269
+ other.binding?.job_id === ctx.job.id &&
270
+ other.index === ctx.binding.index &&
271
+ other.attempted &&
272
+ !['completed', 'failed'].includes(other.state)
273
+ )
274
+ throw new Error(
275
+ '该片段已有未结束/未知反推operation;先查询原操作,不换名重提'
276
+ )
277
+ }
278
+ }
279
+ function summary(record, ctx) {
280
+ const state = runState(ctx.output)
281
+ return {
282
+ operation: record.operation,
283
+ repaint_operation: record.parentOperation,
284
+ state: record.state,
285
+ job_id: ctx.job.id,
286
+ plan_version: ctx.job.plan_version,
287
+ segment_index: record.index,
288
+ node_id: ctx.node.id,
289
+ processing_submitted: record.accepted
290
+ ? true
291
+ : record.attempted
292
+ ? null
293
+ : false,
294
+ remote_status: state.status,
295
+ canvas_synced:
296
+ record.canvasSynced === true &&
297
+ Boolean(state.prompt?.prompt) &&
298
+ isDeepStrictEqual(fields(ctx.node), {
299
+ input: state.prompt.prompt,
300
+ prompt: state.prompt.prompt,
301
+ output: state.prompt.prompt
302
+ }),
303
+ billing: state.billing,
304
+ reverse_prompt: state.prompt,
305
+ library_saved: false,
306
+ ...(ctx.output.reverse_prompt_error
307
+ ? { remote_error: text(ctx.output.reverse_prompt_error).slice(0, 1000) }
308
+ : {})
309
+ }
310
+ }
311
+ function validateRun(record, ctx) {
312
+ if (!record.accepted)
313
+ throw fail(
314
+ 'REVERSE_SUBMISSION_UNKNOWN',
315
+ '反推响应未知;仅查询原job/segment,不自动归属结果或重发POST'
316
+ )
317
+ const state = runState(ctx.output),
318
+ billing = state.billing
319
+ if (
320
+ !billing ||
321
+ billing.plan_version !== record.binding.plan_version ||
322
+ billing.amount !== record.approvedCost ||
323
+ (record.reservationKey && billing.reservation_key !== record.reservationKey)
324
+ )
325
+ throw new Error('原反推计费/运行标记变化,停止,不认领另一轮结果')
326
+ if (
327
+ state.status === 'completed' &&
328
+ (!text(state.prompt?.prompt).trim() ||
329
+ state.prompt.generation_number !== record.generationBefore + 1)
330
+ )
331
+ throw new Error('反推完成内容/生成轮次不匹配;不覆盖或重新反推')
332
+ if (
333
+ record.resultHash &&
334
+ (state.status !== 'completed' || hash(state.prompt) !== record.resultHash)
335
+ )
336
+ throw new Error('原反推结果已被替换,不覆盖后续结果')
337
+ return state
338
+ }
339
+ export async function startReverse(
340
+ client,
341
+ profile,
342
+ parentOperation,
343
+ index,
344
+ operation,
345
+ confirmed = false,
346
+ expectedCost
347
+ ) {
348
+ safeName(operation)
349
+ safeName(parentOperation)
350
+ const ctx = await context(client, profile, parentOperation, index)
351
+ let record = await json(reversePath(profile, operation), null)
352
+ if (
353
+ record &&
354
+ (record.parentOperation !== parentOperation ||
355
+ record.index !== index ||
356
+ record.baseUrl !== client.baseUrl ||
357
+ record.owner !== ctx.parent.owner ||
358
+ !isDeepStrictEqual(record.binding, ctx.binding))
359
+ )
360
+ throw new Error('原反推operation不能修改参数、账号或服务')
361
+ if (record?.attempted) return recoverReverse(client, profile, operation)
362
+ await preventDuplicate(profile, operation, ctx)
363
+ if (runState(ctx.output).status === 'running')
364
+ throw new Error('该片段已经在反推,先查询现有状态,不提交另一轮')
365
+ if (runState(ctx.output).status === 'failed')
366
+ throw new Error('该片段已有失败反推;先核对原操作,不自动重做')
367
+ if (!['not_started', 'completed'].includes(runState(ctx.output).status))
368
+ throw new Error('原片段反推状态未知,停止,不提交另一轮')
369
+ const estimate = await quote(client, ctx)
370
+ const baseline = runState(ctx.output)
371
+ if (!confirmed) {
372
+ record = {
373
+ schema: 1,
374
+ profile,
375
+ operation,
376
+ parentOperation,
377
+ index,
378
+ owner: ctx.parent.owner,
379
+ baseUrl: client.baseUrl,
380
+ binding: ctx.binding,
381
+ baseline,
382
+ nodeBefore: fields(ctx.node),
383
+ estimate,
384
+ estimateKey: costKey(estimate),
385
+ generationBefore: ctx.output.reverse_prompt?.generation_number || 0,
386
+ state: 'prepared',
387
+ createdAt: new Date().toISOString(),
388
+ ...(record?.confirmationAttempted ? { confirmationAttempted: true } : {})
389
+ }
390
+ await persist(record)
391
+ return {
392
+ ...summary(record, ctx),
393
+ submitted: false,
394
+ estimate,
395
+ parameters: {
396
+ segment_index: index,
397
+ start_ms: ctx.binding.start_ms,
398
+ end_ms: ctx.binding.end_ms,
399
+ phase: 'reverse_prompt',
400
+ model: 'service_managed',
401
+ global_rules: ctx.binding.rules
402
+ },
403
+ next: '展示原片段、全局规则、付款钱包及本次费用,取得当次确认后同operation加--yes --expected-cost;不会自动处理最终提示词或生成视频'
404
+ }
405
+ }
406
+ if (
407
+ !record ||
408
+ expectedCost === undefined ||
409
+ Number(expectedCost) !== estimate.credits ||
410
+ record.estimateKey !== costKey(estimate) ||
411
+ !isDeepStrictEqual(record.baseline, baseline) ||
412
+ !isDeepStrictEqual(record.nodeBefore, fields(ctx.node))
413
+ )
414
+ throw new Error(
415
+ '先预估并当次确认;费用、节点内容或原反推状态变化需重新核对'
416
+ )
417
+ await requireServiceReady(client)
418
+ if (!ctx.output.reverse_prompt_confirmed_at) {
419
+ if (record.confirmationAttempted)
420
+ throw fail(
421
+ 'REVERSE_SUBMISSION_UNKNOWN',
422
+ '原片段确认响应未知且尚未查明;先reverse-recover,不重发确认'
423
+ )
424
+ record.confirmationAttempted = true
425
+ record.state = 'confirming_segment'
426
+ await persist(record)
427
+ try {
428
+ await api(
429
+ client,
430
+ jobPath(ctx.job.id) + '/segments/' + index + '/confirm-reverse-prompt',
431
+ { method: 'POST', body: {} },
432
+ { retry: false }
433
+ )
434
+ } catch (error) {
435
+ record.state = 'confirmation_unknown'
436
+ await persist(record)
437
+ throw fail(
438
+ 'REVERSE_SUBMISSION_UNKNOWN',
439
+ '片段确认结果未确认,未发送反推;先检查后端并查询原operation',
440
+ error
441
+ )
442
+ }
443
+ }
444
+ const fresh = await context(client, profile, parentOperation, index)
445
+ if (
446
+ !fresh.output.reverse_prompt_confirmed_at ||
447
+ !isDeepStrictEqual(fresh.binding, record.binding) ||
448
+ !isDeepStrictEqual(runState(fresh.output), record.baseline) ||
449
+ !isDeepStrictEqual(fields(fresh.node), record.nodeBefore) ||
450
+ costKey(await quote(client, fresh)) !== record.estimateKey
451
+ )
452
+ throw new Error('确认片段后参数/状态/费用变化,未提交反推,重新预估确认')
453
+ record.attempted = true
454
+ record.approvedCost = estimate.credits
455
+ record.state = 'submitting'
456
+ await persist(record)
457
+ try {
458
+ const response = await api(
459
+ client,
460
+ jobPath(ctx.job.id) + '/segments/' + index + '/reverse-prompt',
461
+ { method: 'POST', body: {} },
462
+ { retry: false }
463
+ )
464
+ const output = response?.outputs?.find(
465
+ (item) => item.index === index && !item.type
466
+ )
467
+ if (
468
+ response?.id === ctx.job.id &&
469
+ response.canvas_doc_id === ctx.doc.id &&
470
+ text(response.team_uid || '') === text(ctx.doc.team_uid || '') &&
471
+ response.plan_version === record.binding.plan_version &&
472
+ output?.reverse_prompt_billing?.plan_version ===
473
+ record.binding.plan_version
474
+ ) {
475
+ record.accepted = true
476
+ record.reservationKey =
477
+ output.reverse_prompt_billing.reservation_key || null
478
+ await persist(record)
479
+ }
480
+ if (!record.accepted) throw new Error('反推响应缺少可核验原任务标记')
481
+ record.state = 'running'
482
+ await persist(record)
483
+ } catch (error) {
484
+ record.state = 'submission_unknown'
485
+ await persist(record)
486
+ throw fail(
487
+ 'REVERSE_SUBMISSION_UNKNOWN',
488
+ '反推提交结果未确认;只查询原job/segment,不能重发或换operation',
489
+ error
490
+ )
491
+ }
492
+ return recoverReverse(client, profile, operation)
493
+ }
494
+ export async function getReverse(client, profile, operation) {
495
+ const { record, ctx } = await bound(client, profile, operation)
496
+ if (record.accepted) validateRun(record, ctx)
497
+ return summary(record, ctx)
498
+ }
499
+ export async function recoverReverse(client, profile, operation) {
500
+ const { record, ctx } = await bound(client, profile, operation)
501
+ if (!record.attempted) {
502
+ if (
503
+ record.confirmationAttempted &&
504
+ ctx.output.reverse_prompt_confirmed_at
505
+ ) {
506
+ record.state = 'segment_confirmed'
507
+ await persist(record)
508
+ }
509
+ return summary(record, ctx)
510
+ }
511
+ const state = validateRun(record, ctx)
512
+ if (state.status === 'failed') {
513
+ record.state = 'failed'
514
+ await persist(record)
515
+ return summary(record, ctx)
516
+ }
517
+ if (state.status !== 'completed') {
518
+ record.state = 'running'
519
+ await persist(record)
520
+ return summary(record, ctx)
521
+ }
522
+ await archiveRepaintResult(record, 'reverse', state.prompt, state.billing)
523
+ const prompt = state.prompt.prompt,
524
+ expected = { input: prompt, prompt, output: prompt },
525
+ current = fields(ctx.node)
526
+ if (
527
+ !isDeepStrictEqual(current, expected) &&
528
+ !isDeepStrictEqual(current, record.nodeBefore)
529
+ )
530
+ throw fail(
531
+ 'REVERSE_SYNC_PENDING',
532
+ '反推节点内容已由用户编辑,不覆盖;原结果仍可查询'
533
+ )
534
+ record.resultHash = hash(state.prompt)
535
+ record.state = 'completed'
536
+ await persist(record)
537
+ if (!isDeepStrictEqual(current, expected)) {
538
+ ctx.node.data = { ...ctx.node.data, ...expected }
539
+ try {
540
+ const saved = await api(
541
+ client,
542
+ '/canvas/docs/' + encodeURIComponent(ctx.doc.id),
543
+ {
544
+ method: 'PUT',
545
+ body: {
546
+ version: ctx.doc.version,
547
+ name: ctx.doc.name,
548
+ viewport: ctx.doc.viewport,
549
+ nodes: ctx.doc.nodes,
550
+ edges: ctx.doc.edges,
551
+ meta: ctx.doc.meta
552
+ }
553
+ },
554
+ { retry: false }
555
+ )
556
+ if (
557
+ saved.version !== ctx.doc.version + 1 ||
558
+ !isDeepStrictEqual(
559
+ fields(saved.nodes?.find((n) => n.id === ctx.node.id) || {}),
560
+ expected
561
+ )
562
+ )
563
+ throw new Error('反推保存响应不完整')
564
+ } catch (error) {
565
+ record.state = 'sync_pending'
566
+ await persist(record)
567
+ throw fail(
568
+ 'REVERSE_SYNC_PENDING',
569
+ '原反推已完成,画布保存未确认;reverse-recover只读回核对并恢复,不重做反推',
570
+ error
571
+ )
572
+ }
573
+ }
574
+ record.canvasSynced = true
575
+ await persist(record)
576
+ return { ...summary(record, ctx), canvas_synced: true }
577
+ }
578
+ export async function waitReverse(
579
+ client,
580
+ profile,
581
+ operation,
582
+ interval = 2000,
583
+ timeout = 180000
584
+ ) {
585
+ if (
586
+ ![interval, timeout].every(
587
+ (value) => Number.isSafeInteger(value) && value > 0
588
+ )
589
+ )
590
+ throw new Error('等待参数必须为正整数')
591
+ const deadline = Date.now() + timeout
592
+ while (Date.now() < deadline) {
593
+ const result = await recoverReverse(client, profile, operation)
594
+ if (result.state === 'completed') return result
595
+ if (result.state === 'failed')
596
+ throw fail('TASK_FAILED', '原片段反推失败;查询原operation,不自动重做')
597
+ if (!result.processing_submitted) return result
598
+ await sleep(
599
+ Math.min(interval, Math.max(1, deadline - Date.now())),
600
+ undefined,
601
+ {
602
+ signal: cancellation.signal
603
+ }
604
+ )
605
+ }
606
+ throw fail(
607
+ 'WAIT_TIMEOUT',
608
+ '等待反推超时,使用原operation的reverse-wait/reverse-recover继续,不重新提交'
609
+ )
610
+ }
611
+
612
+ // Shared read-only validation for the next prompt phase; reverse behavior is unchanged.
613
+ export { context as repaintPromptContext, fields as repaintPromptFields }
614
+ export { validateRun as validateReverseRun }