@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,472 @@
1
+ // Independent implementation of the ordinary web node input contract.
2
+ // Development oracles: nodeContent.js, generationActions.js and canvas/requests.js.
3
+ export const generationModes = {
4
+ text2image: [['text'], []],
5
+ image2image: [['text', 'image'], ['image']],
6
+ text_to_video: [['text'], []],
7
+ image_reference: [['text', 'image'], ['image']],
8
+ video_reference: [['text', 'video'], ['video']],
9
+ audio_reference: [['text', 'image', 'video', 'audio'], ['audio']],
10
+ combined_reference: [
11
+ ['text', 'image', 'video', 'audio'],
12
+ ['image', 'video', 'audio']
13
+ ],
14
+ video_editing: [['text', 'image', 'video'], ['video']],
15
+ video_extension: [['text', 'video'], ['video']]
16
+ }
17
+ const labels = { image: '图片', video: '视频', audio: '音频' }
18
+ const text = (value) => String(value ?? '').trim()
19
+ const unique = (values) => [...new Set(values)]
20
+ const object = (value) => value && typeof value === 'object' && !Array.isArray(value)
21
+ export function normalizeMode(value) {
22
+ const aliases = {
23
+ 'text-to-image': 'text2image',
24
+ 'image-to-image': 'image2image',
25
+ 'text-to-video': 'text_to_video',
26
+ text2video: 'text_to_video',
27
+ image2video: 'image_reference',
28
+ 'image-to-video': 'image_reference',
29
+ 'image-reference': 'image_reference',
30
+ 'video-reference': 'video_reference',
31
+ 'audio-reference': 'audio_reference',
32
+ 'combined-reference': 'combined_reference',
33
+ omni_reference: 'combined_reference',
34
+ multimodal: 'combined_reference',
35
+ 'video-editing': 'video_editing',
36
+ 'video-extension': 'video_extension'
37
+ }
38
+ const raw = object(value)
39
+ ? value.value || value.code || value.mode || value.name || value.generation_mode
40
+ : value
41
+ return aliases[text(raw)] || text(raw)
42
+ }
43
+ export function storedReferences(doc, node) {
44
+ const meta = doc.meta?.node_references?.[node.id]
45
+ const refs = meta ?? node.data?.references ?? node.data?.extra?.references ?? []
46
+ if (!Array.isArray(refs) || refs.some((ref) => !object(ref) || !text(ref.id)))
47
+ throw new Error('节点持久参考无效;先核对画布引用')
48
+ return structuredClone(refs)
49
+ }
50
+ export function nodeReview(data = {}) {
51
+ const review = data.seedanceReview || data.seedance_review || data.extra?.seedance_review
52
+ return object(review) ? { ...review, status: text(review.status).toLowerCase() } : null
53
+ }
54
+ function resultText(result) {
55
+ const status = result?.structured_meta?.status ?? result?.structuredMeta?.status
56
+ const markdown = result?.structured_render?.markdown ?? result?.structuredRender?.markdown
57
+ if (status !== 'failed' && typeof markdown === 'string' && markdown.trim()) return markdown.trim()
58
+ return typeof result?.text === 'string' ? result.text.trim() : ''
59
+ }
60
+ function plainText(value) {
61
+ // Equivalent to the web plain-text projection; common editor entities are decoded.
62
+ return String(value ?? '')
63
+ .replace(/<[^>]*>/g, ' ')
64
+ .replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (match, entity) => {
65
+ const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }
66
+ if (entity[0] !== '#') return named[entity.toLowerCase()] ?? match
67
+ const n = /^#x/i.test(entity) ? parseInt(entity.slice(2), 16) : Number(entity.slice(1))
68
+ return n > 0 && n <= 0x10ffff ? String.fromCodePoint(n) : match
69
+ })
70
+ .replace(/\s+/g, ' ')
71
+ .trim()
72
+ }
73
+ export function readSource(node) {
74
+ const data = node?.data || {},
75
+ extra = data.extra || {},
76
+ output = data.node_output || {}
77
+ const asset = node?.type?.endsWith('_upload') || extra.node_kind === 'asset'
78
+ const mediaType = asset
79
+ ? data.mediaType || output.media_type || node.type.replace('_upload', '')
80
+ : node?.type
81
+ const result = data.result || {}
82
+ const status = data.status || result.status || (data.last_task_id ? 'processing' : '')
83
+ const content =
84
+ typeof data.output === 'string'
85
+ ? data.output
86
+ : (output.content ?? (status === 'completed' ? resultText(result) : '')) || data.text || ''
87
+ const fileRef =
88
+ data.fileRef || data.file_ref || extra.file_ref || result.file_ref || output.file_ref
89
+ const url = asset
90
+ ? result.url ||
91
+ data.url ||
92
+ output.url ||
93
+ (fileRef ? '/api/v1/files/' + encodeURIComponent(fileRef) : '')
94
+ : result.url || data.resultUrl || output.url || ''
95
+ return {
96
+ id: text(node?.id),
97
+ title: text(data.title || data.name || data.file_name || node?.id),
98
+ mediaType,
99
+ url: text(url),
100
+ text: mediaType === 'text' ? plainText(content) : '',
101
+ uploading:
102
+ data.uploading === true ||
103
+ (data.runtime?.operation === 'upload' &&
104
+ ['queued', 'preparing', 'processing'].includes(data.runtime.status)),
105
+ uploadError: text(
106
+ data.uploadError || (data.runtime?.operation === 'upload' ? data.runtime.error : '')
107
+ ),
108
+ seedanceReview: nodeReview(data)
109
+ }
110
+ }
111
+ export function mergeSources(sources, references) {
112
+ const seen = new Set()
113
+ return [...sources, ...references]
114
+ .filter((ref) => {
115
+ const id = text(ref.id)
116
+ if (!id || seen.has(id)) return false
117
+ seen.add(id)
118
+ return true
119
+ })
120
+ .map((ref) => ({
121
+ ...ref,
122
+ id: text(ref.id),
123
+ title: text(ref.title || ref.id),
124
+ mediaType: text(ref.mediaType || 'image'),
125
+ url: text(ref.url),
126
+ text: text(ref.text)
127
+ }))
128
+ }
129
+ function usable(source) {
130
+ if (!['text', ...Object.keys(labels)].includes(source.mediaType))
131
+ throw new Error(`参考 ${source.id} 的媒体类型不支持`)
132
+ if (source.uploading || source.uploadError)
133
+ throw new Error(`参考 ${source.id} 尚未上传完成或上传失败`)
134
+ if (source.mediaType === 'text' ? !source.text : !source.url)
135
+ throw new Error(`上游 ${source.id} 尚无${source.mediaType === 'text' ? '文本' : '媒体'}输出`)
136
+ if (source.mediaType !== 'text' && !/^(?:https?:\/\/|\/api\/|asset:\/\/)/i.test(source.url))
137
+ throw new Error(`参考 ${source.id} 不是可提交的服务端媒体地址`)
138
+ }
139
+ export function collectGenerationSources(doc, node, mediaTypes = null) {
140
+ const incoming = doc.edges.filter((edge) => edge.target === node.id)
141
+ const connected = []
142
+ for (const id of unique(incoming.map((edge) => edge.source))) {
143
+ const source = doc.nodes.find((candidate) => candidate.id === id)
144
+ if (!source) throw new Error(`上游 ${id} 节点不存在`)
145
+ const resolved = readSource(source)
146
+ if (mediaTypes && !mediaTypes.includes(resolved.mediaType)) continue
147
+ usable(resolved)
148
+ connected.push(resolved)
149
+ }
150
+ const references = storedReferences(doc, node).filter(
151
+ (ref) => !mediaTypes || mediaTypes.includes(ref.mediaType)
152
+ )
153
+ const all = mergeSources(connected, references)
154
+ all.forEach(usable)
155
+ return { connected, references, all }
156
+ }
157
+ function normalizePrompt(value) {
158
+ return value.replace(/@\{ref:([^}]+)\}/g, (marker, encoded) => {
159
+ let id
160
+ try {
161
+ id = decodeURIComponent(encoded)
162
+ } catch {
163
+ return marker
164
+ }
165
+ const match = /^(image|video|audio)-(\d+)$/.exec(id)
166
+ return match ? `@${labels[match[1]]}${match[2]}` : marker
167
+ })
168
+ }
169
+ function serializeMentions(value, sources, media, automatic) {
170
+ const markers = new Map(),
171
+ names = new Map()
172
+ for (const source of sources) {
173
+ const index = media[source.mediaType]?.indexOf(text(source.url)) ?? -1
174
+ if (index < 0) continue
175
+ markers.set(source.id, `@${labels[source.mediaType]}${index + 1}`)
176
+ const name = text(source.displayLabel || source.title)
177
+ if (name) names.set(name, names.has(name) ? null : source)
178
+ }
179
+ if (automatic) {
180
+ const candidates = [...names]
181
+ .filter(([, source]) => source)
182
+ .sort((a, b) => b[0].length - a[0].length)
183
+ const token = /^(?:@\{ref:[^}]+\}|@(?:(?:图片|音频|视频)|(?:image|audio|video))?\d+)/
184
+ let next = '',
185
+ offset = 0
186
+ while (offset < value.length) {
187
+ const marker = token.exec(value.slice(offset))
188
+ const candidate = marker ? null : candidates.find(([name]) => value.startsWith(name, offset))
189
+ if (marker) {
190
+ next += marker[0]
191
+ offset += marker[0].length
192
+ } else if (candidate) {
193
+ next += `@{ref:${encodeURIComponent(candidate[1].id)}}`
194
+ offset += candidate[0].length
195
+ } else next += value[offset++]
196
+ }
197
+ value = next
198
+ }
199
+ const resolved = value.replace(/@@?\{ref:([^}]+)\}/g, (marker, encoded) => {
200
+ try {
201
+ return markers.get(decodeURIComponent(encoded)) || normalizePrompt(marker)
202
+ } catch {
203
+ return marker
204
+ }
205
+ })
206
+ if (/@\{ref:[^}]+\}/.test(resolved))
207
+ throw new Error('提示词包含未解析的素材引用;请先核对参考素材')
208
+ return normalizePrompt(resolved)
209
+ }
210
+ export function promptInput(node, connected, references, sources, media) {
211
+ const contexts = [
212
+ ...connected.filter((s) => s.mediaType === 'text').map((s) => s.text),
213
+ ...references.filter((s) => s.mediaType === 'text').map((s) => text(s.text))
214
+ ]
215
+ .map(text)
216
+ .filter(Boolean)
217
+ const own = text(node.data?.prompt),
218
+ automatic =
219
+ (node.data?.autoRecognizeReferences ?? node.data?.extra?.auto_recognize_references) !== false
220
+ const preparation = Object.entries(labels).flatMap(([type, label]) =>
221
+ (media[type] || []).flatMap((url, index) => {
222
+ const source = sources.find((s) => s.mediaType === type && text(s.url) === url)
223
+ return source
224
+ ? [
225
+ `@${label}${index + 1}是${String(source.title || source.id)
226
+ .replace(/[\r\n]+/g, ' ')
227
+ .trim()}`
228
+ ]
229
+ : []
230
+ })
231
+ )
232
+ const prefix = preparation.length
233
+ ? `<!-- flow:auto-material-preparation:start -->\n素材准备:${preparation.join(';')}。\n<!-- flow:auto-material-preparation:end -->\n\n`
234
+ : ''
235
+ return {
236
+ prompt:
237
+ prefix +
238
+ serializeMentions([...contexts, own].filter(Boolean).join('\n\n'), sources, media, automatic),
239
+ nodePrompt: prefix + serializeMentions(own, sources, media, automatic),
240
+ contexts
241
+ }
242
+ }
243
+ function resolveReferences(doc, connected, configured, includeAll) {
244
+ const ordered = includeAll
245
+ ? mergeSources(connected, configured)
246
+ : configured.length
247
+ ? configured
248
+ : connected
249
+ const media = { image: [], video: [], audio: [] },
250
+ assets = [],
251
+ tasks = [],
252
+ resolved = []
253
+ const seen = new Set()
254
+ for (const reference of ordered) {
255
+ const id = text(reference.id)
256
+ if (seen.has(id) || reference.mediaType === 'text') continue
257
+ seen.add(id)
258
+ const source = doc.nodes.find((n) => n.id === id)
259
+ const current = source ? readSource(source) : null
260
+ const data = source?.data || {},
261
+ extra = data.extra || {}
262
+ const type = current?.mediaType || reference.mediaType || 'image'
263
+ const url = text(current?.url || reference.url)
264
+ const item = { ...reference, id, mediaType: type, url }
265
+ usable(item)
266
+ if (!url.startsWith('asset://')) media[type].push(url)
267
+ if (type !== 'audio') {
268
+ const asset = text(
269
+ reference.assetId || data.sourceAssetId || data.assetId || extra.source_asset_id
270
+ )
271
+ if (asset) assets.push(asset)
272
+ const review = current?.seedanceReview || reference.seedanceReview
273
+ if (text(review?.status).toLowerCase() === 'active' && text(review.taskId)) {
274
+ const reviewedRef = text(review.mediaRef || review.imageRef)
275
+ const currentRef = text(
276
+ data.resultFileRef ||
277
+ data.fileRef ||
278
+ data.file_ref ||
279
+ extra.file_ref ||
280
+ data.result?.file_ref ||
281
+ data.node_output?.file_ref
282
+ )
283
+ if (reviewedRef && currentRef && reviewedRef !== currentRef)
284
+ throw new Error('当前素材与已通过审核的引用不一致;请核对或重新审核,不复用旧关联')
285
+ tasks.push(text(review.taskId))
286
+ }
287
+ }
288
+ resolved.push(item)
289
+ }
290
+ for (const source of connected.filter((s) => s.mediaType === 'audio'))
291
+ media.audio.push(source.url)
292
+ for (const type of Object.keys(media)) media[type] = unique(media[type])
293
+ return {
294
+ media,
295
+ assets: unique(assets),
296
+ tasks: unique(tasks),
297
+ sources: mergeSources(resolved, connected)
298
+ }
299
+ }
300
+ function configuredValue(node, key) {
301
+ const data = node.data || {},
302
+ extra = data.extra || {}
303
+ return data[key] ?? extra[key]
304
+ }
305
+ function setting(node, model, key, listKey, fallback = '') {
306
+ const raw = configuredValue(node, key)
307
+ const choices = Array.isArray(model[listKey]) ? model[listKey] : []
308
+ if (raw !== undefined && raw !== null && raw !== '') {
309
+ if (!choices.includes(String(raw))) throw new Error(`${key} 不在当前模型选项中;请先核对参数`)
310
+ return String(raw)
311
+ }
312
+ return choices.includes(fallback) ? fallback : fallback ? choices[0] || '' : ''
313
+ }
314
+ export function buildGenerationRequest(doc, node, model, inputs) {
315
+ const kind = node.type,
316
+ data = node.data || {},
317
+ extra = data.extra || {}
318
+ const { connected, references, all } = inputs
319
+ let media,
320
+ assets = [],
321
+ tasks = [],
322
+ sources = all
323
+ if (kind === 'image') {
324
+ const referencedImages = references
325
+ .filter((s) => s.mediaType === 'image')
326
+ .map((s) => text(s.url))
327
+ .filter(Boolean)
328
+ media = {
329
+ image: referencedImages.length
330
+ ? referencedImages
331
+ : connected.filter((s) => s.mediaType === 'image').map((s) => s.url),
332
+ video: unique(
333
+ [...connected, ...references].filter((s) => s.mediaType === 'video').map((s) => s.url)
334
+ ),
335
+ audio: unique(
336
+ [...references, ...connected].filter((s) => s.mediaType === 'audio').map((s) => s.url)
337
+ )
338
+ }
339
+ } else {
340
+ const refs = resolveReferences(doc, connected, references, kind === 'video')
341
+ media = refs.media
342
+ assets = refs.assets
343
+ tasks = refs.tasks
344
+ sources = mergeSources(refs.sources, references)
345
+ }
346
+ const input = promptInput(node, connected, references, sources, media)
347
+ if ((!text(data.prompt) && kind === 'text') || !input.prompt.trim()) throw new Error('缺少提示词')
348
+ const body = {
349
+ doc_id: doc.id,
350
+ node_id: node.id,
351
+ segment_type: kind,
352
+ model_name: model.label,
353
+ prompt: (kind === 'text' ? input.nodePrompt : input.prompt).trim(),
354
+ reference_image_urls: media.image,
355
+ reference_video_urls:
356
+ kind === 'text'
357
+ ? unique(
358
+ [...connected, ...references].filter((s) => s.mediaType === 'video').map((s) => s.url)
359
+ )
360
+ : media.video,
361
+ reference_audio_urls: media.audio,
362
+ reference_asset_ids: assets,
363
+ ...(kind !== 'text' ? { reference_canvas_task_ids: tasks } : {}),
364
+ duration: null,
365
+ extra: {}
366
+ }
367
+ if (kind === 'text') {
368
+ const max = Number(data.maxTokens),
369
+ temperature = Number(data.temperature)
370
+ body.extra = {
371
+ context_texts: input.contexts,
372
+ max_tokens: Number.isFinite(max) && max > 0 ? Math.floor(max) : 4096,
373
+ temperature:
374
+ data.temperature != null && Number.isFinite(temperature) && temperature >= 0
375
+ ? temperature
376
+ : 0.7
377
+ }
378
+ } else {
379
+ const modeList = (
380
+ model.generation_modes || (kind === 'image' ? ['text2image', 'image2image'] : [])
381
+ ).map(normalizeMode)
382
+ // The current image run path resolves mode from topology; video also merges
383
+ // persistent references. Keep that distinction observable in the preview.
384
+ const types = (kind === 'image' ? connected : all).map((s) => s.mediaType)
385
+ const compatible = modeList.filter((mode) => {
386
+ const rule = generationModes[mode]
387
+ return (
388
+ rule &&
389
+ types.every((type) => rule[0].includes(type)) &&
390
+ (!rule[1].length || rule[1].some((type) => types.includes(type)))
391
+ )
392
+ })
393
+ const selected = normalizeMode(
394
+ extra.generation_mode ||
395
+ data.generation_mode ||
396
+ data[kind === 'image' ? 'imageMode' : 'videoMode']
397
+ )
398
+ const mode = compatible.includes(selected) ? selected : compatible[0]
399
+ if (!mode) throw new Error('目标模型不支持当前上游类型组合或缺少必需素材')
400
+ body.extra.generation_mode = mode
401
+ const aspect = setting(node, model, 'aspect_ratio', 'aspect_ratios')
402
+ const resolution = setting(node, model, 'resolution', 'resolutions')
403
+ const topLevel = kind === 'video' && /^seedance/i.test(model.label)
404
+ if (aspect) (topLevel ? body : body.extra).aspect_ratio = aspect
405
+ if (resolution) (topLevel ? body : body.extra).resolution = resolution
406
+ if (kind === 'image') {
407
+ if (extra.image_to_image_enabled === false && media.image.length)
408
+ throw new Error('节点已关闭图生图,不能携带图片参考')
409
+ const count = Number(data.image_count ?? data.imageCount ?? extra.image_count) || 1
410
+ if (count !== 1) throw new Error('本轮仅支持单张请求准备;请明确数量为 1')
411
+ body.extra.image_count = 1
412
+ const quality = setting(node, model, 'quality', 'quality_options', 'medium')
413
+ if (quality) body.extra.quality = quality
414
+ body.asset_type = data.asset_type || data.assetType || extra.asset_type || 'character'
415
+ body.enable_asset_review = Boolean(data.seedanceAutoReview)
416
+ } else {
417
+ const duration = Number(data.duration ?? extra.duration)
418
+ if (!Number.isFinite(duration) || duration <= 0) throw new Error('请先设置视频时长')
419
+ if (
420
+ (Number.isFinite(model.durations?.min) && duration < model.durations.min) ||
421
+ (Number.isFinite(model.durations?.max) && duration > model.durations.max)
422
+ )
423
+ throw new Error('视频时长超出当前模型范围')
424
+ body.duration = duration
425
+ if (media.audio.length && model.supports_audio_reference !== true)
426
+ throw new Error('目标模型不支持音频参考')
427
+ if (
428
+ media.audio.length &&
429
+ model.audio_reference_requires_visual === true &&
430
+ !media.image.length &&
431
+ !media.video.length
432
+ )
433
+ throw new Error('音频参考需要至少一个图片或视频输入')
434
+ body.enable_asset_review = (data.seedanceAutoReview ?? extra.seedance_auto_review) === true
435
+ if (model.is_sample_mode === true || extra.draft_preview === true) {
436
+ if (['video_editing', 'video_extension'].includes(mode))
437
+ throw new Error('样片模式不支持视频编辑/延长')
438
+ body.extra.draft_preview = true
439
+ if (topLevel) body.resolution = '480p'
440
+ else body.extra.resolution = '480p'
441
+ }
442
+ }
443
+ }
444
+ for (const type of Object.keys(labels)) {
445
+ const count = all.filter((s) => s.mediaType === type).length
446
+ const limit =
447
+ model[`max_${type}_references`] ?? (type === 'image' ? model.max_reference_count : undefined)
448
+ if (Number.isFinite(limit) && count > limit) throw new Error(`超出模型 ${type} 参考数量限制`)
449
+ }
450
+ if (
451
+ Number.isFinite(model.max_reference_count) &&
452
+ all.filter((s) => s.mediaType !== 'text').length > model.max_reference_count
453
+ )
454
+ throw new Error('超出模型参考素材总数量限制')
455
+ if (kind === 'text')
456
+ for (const type of ['image', 'video']) {
457
+ if (media[type].length && model[`${type}_understanding`] !== true)
458
+ throw new Error(`目标文本模型不支持${type === 'image' ? '图片' : '视频'}理解`)
459
+ }
460
+ if (tasks.some((id) => !/^[1-9]\d*$/.test(id)) || assets.some((id) => !/^[1-9]\d*$/.test(id)))
461
+ throw new Error('素材或审核任务关联 ID 无效')
462
+ return { request: body, sources: all }
463
+ }
464
+
465
+ // Client request order only; do not present this as a provider receipt.
466
+ export function referenceMapping(request, sources) {
467
+ return Object.entries(labels).flatMap(([type, label]) =>
468
+ (request['reference_' + type + '_urls'] || []).map((url, index) => ({
469
+ marker: '@' + label + (index + 1), media_type: type, request_field: 'reference_' + type + '_urls',
470
+ request_index: index, source_node_ids: sources.filter(s => s.mediaType === type && text(s.url) === url).map(s => s.id)
471
+ })))
472
+ }
@@ -0,0 +1,89 @@
1
+ import { isDeepStrictEqual as same } from 'node:util'
2
+ import { scopedCanvas } from './materials.js'
3
+ import { save, sourceType } from './graph.js'
4
+ import { generationModes, normalizeMode, storedReferences, readSource } from './generation-input.js'
5
+
6
+ export function disconnectMentions(value, reference) {
7
+ let result = String(value || '').replace(/@@?{ref:[^}]+}/g, marker => {
8
+ const manual = marker.startsWith('@@'), token = manual ? marker.slice(1) : marker
9
+ let id
10
+ try { id = decodeURIComponent(token.slice(6,-1)) } catch { return marker }
11
+ return id === reference.id ? manual ? '' : String(reference.displayLabel || reference.title || '') : marker
12
+ })
13
+ const legacy = String(reference.legacyMarker || reference.marker || '')
14
+ if (legacy && !/^@{ref:[^}]+}$/.test(legacy)) result = result.split(legacy).join('')
15
+ return result
16
+ }
17
+ const derived = n => n?.type === 'video_repain_begain' || n?.data?.repaint || n?.data?.videoRemake ||
18
+ n?.data?.extra?.repaint || n?.data?.extra?.video_remake || n?.data?.extra?.clip_redraw || n?.data?.draftFinalizeGeneration || n?.data?.extra?.draft_finalize
19
+
20
+ export async function disconnectGraph(client, input, confirmed = false) {
21
+ if (!input || typeof input !== 'object' || Array.isArray(input) ||
22
+ Object.keys(input).some(k=>!['doc_id','space','team_uid','edge_id','expected_version'].includes(k)) ||
23
+ !['doc_id','edge_id'].every(k=>typeof input[k]==='string' && input[k].trim()))
24
+ throw new Error('断线输入需要doc_id/space/team_uid/edge_id,可选expected_version')
25
+ if (input.expected_version !== undefined && (!Number.isSafeInteger(input.expected_version) || input.expected_version < 1))
26
+ throw new Error('expected_version必须为正整数')
27
+ const doc = await scopedCanvas(client,input.doc_id,input.space,input.team_uid)
28
+ if (!Number.isSafeInteger(doc.version) || doc.version < 1 || !Array.isArray(doc.nodes) || !Array.isArray(doc.edges) ||
29
+ !doc.meta || typeof doc.meta !== 'object' || Array.isArray(doc.meta) ||
30
+ !doc.viewport || typeof doc.viewport !== 'object' || Array.isArray(doc.viewport) || typeof doc.name !== 'string' ||
31
+ doc.nodes.some(n=>!n || typeof n.id!=='string') || doc.edges.some(e=>!e || typeof e.id!=='string') ||
32
+ new Set(doc.nodes.map(n=>n.id)).size !== doc.nodes.length || new Set(doc.edges.map(e=>e.id)).size !== doc.edges.length)
33
+ throw new Error('画布快照不完整或ID重复')
34
+ if (input.expected_version !== undefined && input.expected_version !== doc.version) throw new Error('画布已变化,重新预览')
35
+ const edge = doc.edges.find(e=>e.id===input.edge_id)
36
+ if (!edge) throw new Error('连线不存在,先读回核对')
37
+ const source=doc.nodes.find(n=>n.id===edge.source), target=doc.nodes.find(n=>n.id===edge.target)
38
+ if (!source || !target) throw new Error('连线端点缺失')
39
+ if (edge.data?.repaintSystem || edge.data?.role || derived(source) || derived(target))
40
+ throw new Error('仅支持普通连线;系统线与派生流程不支持')
41
+ if (target.data?.generation_config || target.data?.node_output) throw new Error('目标配置协议尚未接入断线')
42
+ const next=structuredClone(doc), nextTarget=next.nodes.find(n=>n.id===target.id)
43
+ next.edges=next.edges.filter(e=>e.id!==edge.id)
44
+ const stillConnected=next.edges.some(e=>e.source===edge.source && e.target===edge.target)
45
+ if (!stillConnected) {
46
+ const refs=storedReferences(doc,target), reference=readSource(source)
47
+ const retained=refs.filter(r=>String(r.id)!==source.id)
48
+ nextTarget.data ||= {}
49
+ nextTarget.data.prompt=disconnectMentions(target.data?.prompt,reference)
50
+ if (Object.hasOwn(target.data||{},'references')) nextTarget.data.references=retained
51
+ if (target.data?.extra && Object.hasOwn(target.data.extra,'references')) nextTarget.data.extra.references=retained
52
+ if (doc.meta.node_references && Object.hasOwn(doc.meta.node_references,target.id)) next.meta.node_references[target.id]=retained
53
+ }
54
+ if (['image','video'].includes(target.type)) {
55
+ const catalog=await client.raw('/canvas/models'+(doc.team_uid?'?team_uid='+encodeURIComponent(doc.team_uid):''))
56
+ const model=catalog.items?.find(m=>m.label===target.data?.model_name && m.content_type===target.type && !m.member_locked)
57
+ if (!model) throw new Error('请先选择目标节点当前空间可用模型')
58
+ const types=next.edges.filter(e=>e.target===target.id).map(e=>sourceType(doc.nodes.find(n=>n.id===e.source)))
59
+ // Match the mounted node's mode normalization after removeEdge: retained
60
+ // persistent material references still constrain the eventual saved mode.
61
+ types.push(...storedReferences(next,nextTarget).map(r=>r.mediaType||sourceType(next.nodes.find(n=>n.id===r.id))).filter(Boolean))
62
+ const supported=(model.generation_modes||[]).map(normalizeMode).filter(m=>generationModes[m])
63
+ const compatible=supported.filter(m=>types.every(t=>generationModes[m][0].includes(t)) &&
64
+ (!generationModes[m][1].length || generationModes[m][1].some(t=>types.includes(t))))
65
+ // Model choice is a user decision: do not silently switch to another model.
66
+ if (!compatible.length) throw new Error('断线后当前模型无兼容模式,请先明确选择模型')
67
+ const current=normalizeMode(target.data?.extra?.generation_mode || target.data?.generation_mode || target.data?.imageMode || target.data?.videoMode)
68
+ const mode=compatible.includes(current)?current:compatible[0]
69
+ nextTarget.data.extra={...nextTarget.data.extra,generation_mode:mode}
70
+ if (Object.hasOwn(nextTarget.data,'generation_mode')) nextTarget.data.generation_mode=mode
71
+ }
72
+ const preview={doc_id:doc.id,edge_id:edge.id,source_node_id:source.id,target_node_id:target.id,
73
+ expected_version:doc.version,still_connected:stillConnected,prompt_before:target.data?.prompt||'',
74
+ prompt_after:nextTarget.data?.prompt||'',references_before:storedReferences(doc,target).map(r=>r.id),
75
+ references_after:storedReferences(next,nextTarget).map(r=>r.id),
76
+ mode_before:target.data?.extra?.generation_mode,mode_after:nextTarget.data?.extra?.generation_mode,
77
+ canvas_written:false,generation_submitted:false}
78
+ if (!confirmed) return preview
79
+ if (input.expected_version===undefined) throw new Error('执行须提供预览expected_version')
80
+ const saved=await save(client,doc,{nodes:next.nodes,edges:next.edges,meta:next.meta})
81
+ let actual
82
+ try { actual=await scopedCanvas(client,input.doc_id,input.space,input.team_uid) }
83
+ catch(cause){throw new Error('断线保存已尝试但读回未确认,不自动重放',{cause})}
84
+ if (!Number.isSafeInteger(actual.version) || actual.version < saved.version ||
85
+ !same(actual.viewport,doc.viewport) || actual.name !== doc.name ||
86
+ !same(actual.nodes,next.nodes) || !same(actual.edges,next.edges) || !same(actual.meta,next.meta))
87
+ throw new Error('断线读回不一致,保留当前画布,不自动重放')
88
+ return {...preview,version:actual.version,canvas_written:true,disconnect_verified:true}
89
+ }