@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.
- package/.agents/skills/uzoo-anime-cli/SKILL.md +83 -0
- package/.agents/skills/uzoo-anime-cli/references/agreement-workflow.md +51 -0
- package/.agents/skills/uzoo-anime-cli/references/canvas-workflow.md +53 -0
- package/.agents/skills/uzoo-anime-cli/references/commands.md +250 -0
- package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +152 -0
- package/.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md +175 -0
- package/.agents/skills/uzoo-anime-cli/references/interaction-workflow.md +67 -0
- package/.agents/skills/uzoo-anime-cli/references/material-graph-workflow.md +123 -0
- package/.agents/skills/uzoo-anime-cli/references/node-capabilities.md +95 -0
- package/.agents/skills/uzoo-anime-cli/references/node-workflow.md +94 -0
- package/.agents/skills/uzoo-anime-cli/references/remake-workflow.md +54 -0
- package/.agents/skills/uzoo-anime-cli/references/repaint-workflow.md +300 -0
- package/.agents/skills/uzoo-anime-cli/references/seedance-review-workflow.md +41 -0
- package/.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md +71 -0
- package/CHANGELOG.md +92 -0
- package/LICENSE +21 -0
- package/README.md +242 -3
- package/assets/README.md +4 -0
- package/assets/favicon-uzoo.png +0 -0
- package/assets/logo-uzoo.svg +7 -0
- 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
- 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
- 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
- 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
- package/docs/M0-M5/345/256/236/346/226/275/344/270/216/351/252/214/346/224/266.md +89 -0
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs//345/274/200/345/217/221/350/256/241/345/210/222.md +127 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs//351/230/266/346/256/2653B/351/252/214/346/224/266/344/272/244/346/216/245.md +44 -0
- package/docs//351/230/266/346/256/265/344/270/200/351/252/214/346/224/266.md +60 -0
- 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
- 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
- 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
- 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
- package/examples/matting.json +5 -0
- package/examples/real-business/image-request.template.json +7 -0
- package/examples/real-business/node-patch.template.json +11 -0
- package/extensions/uzoo-cli-login/manifest.json +9 -0
- package/extensions/uzoo-cli-login/popup.css +1 -0
- package/extensions/uzoo-cli-login/popup.html +14 -0
- package/extensions/uzoo-cli-login/popup.js +65 -0
- package/package.json +56 -6
- package/scripts/check-package-resources.mjs +56 -0
- package/scripts/check-release.mjs +19 -0
- package/scripts/pack-local.mjs +33 -0
- package/scripts/release-policy.mjs +22 -0
- package/src/agreement-page.js +62 -0
- package/src/agreement.js +419 -0
- package/src/annotation-media.js +386 -0
- package/src/api.js +168 -0
- package/src/browser-login.js +13 -0
- package/src/callback-page.js +44 -0
- package/src/canvas.js +105 -0
- package/src/cli-authorization.js +122 -0
- package/src/cli.js +459 -0
- package/src/download-name.js +17 -0
- package/src/draft-finalize.js +229 -0
- package/src/generation-graph.js +124 -0
- package/src/generation-input.js +472 -0
- package/src/graph-disconnect.js +89 -0
- package/src/graph.js +431 -0
- package/src/group-edit.js +53 -0
- package/src/group-layout.js +113 -0
- package/src/group-membership.js +126 -0
- package/src/history-add.js +119 -0
- package/src/image-annotation.js +246 -0
- package/src/image-crop.js +283 -0
- package/src/image-matting.js +82 -0
- package/src/image-media.js +89 -0
- package/src/image-output.js +385 -0
- package/src/local-output.js +23 -0
- package/src/material-download.js +55 -0
- package/src/material-save.js +254 -0
- package/src/materials.js +147 -0
- package/src/matting-background.js +73 -0
- package/src/matting-jobs.js +63 -0
- package/src/matting-worker.js +49 -0
- package/src/matting.js +170 -0
- package/src/media-copy.js +53 -0
- package/src/media.js +343 -0
- package/src/model-options.js +76 -0
- package/src/multipart.js +28 -0
- package/src/node-copy.js +95 -0
- package/src/node-delete.js +49 -0
- package/src/node-history.js +231 -0
- package/src/normal-login.js +118 -0
- package/src/package-info.js +8 -0
- package/src/png-alpha.js +60 -0
- package/src/protocol.js +83 -0
- package/src/remake.js +738 -0
- package/src/repaint-archive.js +53 -0
- package/src/repaint-delivery.js +783 -0
- package/src/repaint-export.js +63 -0
- package/src/repaint-graph.js +214 -0
- package/src/repaint-history.js +267 -0
- package/src/repaint-materials.js +601 -0
- package/src/repaint-reverse.js +614 -0
- package/src/repaint-sequence.js +132 -0
- package/src/repaint-settings.js +576 -0
- package/src/repaint-video.js +798 -0
- package/src/repaint.js +326 -0
- package/src/resource.js +110 -0
- package/src/review.js +435 -0
- package/src/service.js +85 -0
- package/src/store.js +100 -0
- package/src/video-clip.js +381 -0
- package/src/video-frame.js +296 -0
- package/src/workflows.js +214 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { atomic } from './store.js'
|
|
5
|
+
import { scopedCanvas } from './materials.js'
|
|
6
|
+
import { segment, uploadParts } from './workflows.js'
|
|
7
|
+
import { bindUpload } from './resource.js'
|
|
8
|
+
import { hashFile } from './media.js'
|
|
9
|
+
const text = (value) => String(value ?? '').trim()
|
|
10
|
+
const digest = (value) => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
11
|
+
const fail = (code, message, error) =>
|
|
12
|
+
Object.assign(new Error(message), {
|
|
13
|
+
code,
|
|
14
|
+
...(error
|
|
15
|
+
? {
|
|
16
|
+
cause: error,
|
|
17
|
+
diagnostic: {
|
|
18
|
+
...(error.status ? { http_status: error.status } : {}),
|
|
19
|
+
...(error.apiCode ? { api_code: error.apiCode } : {}),
|
|
20
|
+
...(error.requestId ? { request_id: error.requestId } : {})
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
: {})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Shared persistence/upload contract for locally processed image outputs.
|
|
27
|
+
// Callers own source validation, geometry, decoding and operation journals.
|
|
28
|
+
export function imageOutputWorkflow({
|
|
29
|
+
recordPath,
|
|
30
|
+
readContext,
|
|
31
|
+
nodeTitle,
|
|
32
|
+
nodePosition,
|
|
33
|
+
edgeRole,
|
|
34
|
+
edgePrefix,
|
|
35
|
+
codePrefix,
|
|
36
|
+
mime,
|
|
37
|
+
summarize,
|
|
38
|
+
nodeType = () => 'image_upload'
|
|
39
|
+
}) {
|
|
40
|
+
async function persistNodes(client, profile, record, current) {
|
|
41
|
+
const doc = current.doc
|
|
42
|
+
const existing = record.items.map((item) => doc.nodes.find((node) => node.id === item.nodeId))
|
|
43
|
+
if (existing.every(Boolean)) {
|
|
44
|
+
for (let i = 0; i < existing.length; i++) {
|
|
45
|
+
const node = existing[i],
|
|
46
|
+
item = record.items[i]
|
|
47
|
+
if (
|
|
48
|
+
node.type !== nodeType(record, item) ||
|
|
49
|
+
(node.data?.extra?.node_kind &&
|
|
50
|
+
node.data.extra.node_kind !==
|
|
51
|
+
(nodeType(record, item) === 'image' ? 'generation' : 'asset')) ||
|
|
52
|
+
(node.data?.file_name && node.data.file_name !== item.fileName) ||
|
|
53
|
+
!doc.edges.some(
|
|
54
|
+
(e) =>
|
|
55
|
+
e.source === record.input.node_id && e.target === node.id && e.data?.role === edgeRole
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
throw new Error('图片处理结果节点或来源连线已变化,停止保存')
|
|
59
|
+
}
|
|
60
|
+
record.nodesSaved = true
|
|
61
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
if (record.nodesSaved || record.saveAttempted || existing.some(Boolean))
|
|
65
|
+
throw fail(
|
|
66
|
+
codePrefix + '_SYNC_PENDING',
|
|
67
|
+
'图片处理节点/连线缺失或保存结果不明;先核对用户删除/画布内容,不自动重建'
|
|
68
|
+
)
|
|
69
|
+
if (
|
|
70
|
+
!Number.isSafeInteger(doc.version) ||
|
|
71
|
+
!doc.viewport ||
|
|
72
|
+
!doc.meta ||
|
|
73
|
+
!Array.isArray(doc.edges)
|
|
74
|
+
)
|
|
75
|
+
throw new Error('图片处理画布快照不完整')
|
|
76
|
+
const nodes = structuredClone(doc.nodes),
|
|
77
|
+
edges = structuredClone(doc.edges)
|
|
78
|
+
for (const item of record.items) {
|
|
79
|
+
const node = {
|
|
80
|
+
id: item.nodeId,
|
|
81
|
+
type: nodeType(record, item),
|
|
82
|
+
position: nodePosition(nodes, current.node),
|
|
83
|
+
data: {
|
|
84
|
+
title: nodeTitle(item),
|
|
85
|
+
mediaType: 'image',
|
|
86
|
+
url: '',
|
|
87
|
+
file_name: item.fileName,
|
|
88
|
+
file_size: item.local.bytes,
|
|
89
|
+
width: item.local.width,
|
|
90
|
+
height: item.local.height,
|
|
91
|
+
uploading: true,
|
|
92
|
+
...(nodeType(record, item) === 'image'
|
|
93
|
+
? {
|
|
94
|
+
status: 'processing',
|
|
95
|
+
runtime: { status: 'processing', operation: 'upload', progress: 0, error: '' }
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
extra: {
|
|
99
|
+
node_kind: nodeType(record, item) === 'image_upload' ? 'asset' : 'generation',
|
|
100
|
+
aspect_ratio: item.local.width + ':' + item.local.height
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
nodes.push(node)
|
|
105
|
+
edges.push({
|
|
106
|
+
id: edgePrefix + item.nodeId,
|
|
107
|
+
source: record.input.node_id,
|
|
108
|
+
target: item.nodeId,
|
|
109
|
+
sourceHandle: '',
|
|
110
|
+
targetHandle: '',
|
|
111
|
+
type: 'default',
|
|
112
|
+
data: { role: edgeRole }
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
record.saveAttempted = true
|
|
116
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
117
|
+
try {
|
|
118
|
+
const saved = await client.raw(
|
|
119
|
+
'/canvas/docs/' + segment(doc.id),
|
|
120
|
+
{
|
|
121
|
+
method: 'PUT',
|
|
122
|
+
body: {
|
|
123
|
+
version: doc.version,
|
|
124
|
+
name: doc.name,
|
|
125
|
+
viewport: doc.viewport,
|
|
126
|
+
nodes,
|
|
127
|
+
edges,
|
|
128
|
+
meta: doc.meta
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{ retry: false }
|
|
132
|
+
)
|
|
133
|
+
if (
|
|
134
|
+
saved.id !== doc.id ||
|
|
135
|
+
saved.version <= doc.version ||
|
|
136
|
+
record.items.some(
|
|
137
|
+
(item) =>
|
|
138
|
+
!saved.nodes?.some((n) => n.id === item.nodeId) ||
|
|
139
|
+
!saved.edges?.some((e) => e.id === edgePrefix + item.nodeId)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
throw new Error('节点保存响应不完整')
|
|
143
|
+
record.nodesSaved = true
|
|
144
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
record.state = 'sync_pending'
|
|
147
|
+
if (error.status === 409 || error.apiCode === 3002) {
|
|
148
|
+
record.saveAttempted = false
|
|
149
|
+
}
|
|
150
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
151
|
+
throw fail(
|
|
152
|
+
codePrefix + '_SYNC_PENDING',
|
|
153
|
+
'图片处理已在本地导出,节点保存未确认;查询原画布后恢复,不重复处理',
|
|
154
|
+
error
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function validateUpload(response, record, item) {
|
|
159
|
+
if (
|
|
160
|
+
response?.source_doc_id !== record.input.doc_id ||
|
|
161
|
+
response.source_node_id !== item.nodeId ||
|
|
162
|
+
response.segment_type !== 'image' ||
|
|
163
|
+
!response.result?.file_ref ||
|
|
164
|
+
!response.result.url ||
|
|
165
|
+
!/^image\//.test(response.result.mime_type || '') ||
|
|
166
|
+
response.result.width !== item.local.width ||
|
|
167
|
+
response.result.height !== item.local.height ||
|
|
168
|
+
!/^[1-9]\d*$/.test(text(response.task_id))
|
|
169
|
+
)
|
|
170
|
+
throw fail(
|
|
171
|
+
codePrefix + '_UPLOAD_UNKNOWN',
|
|
172
|
+
'上传响应缺少真实图片/任务关联;查询原节点和任务,不重复上传'
|
|
173
|
+
)
|
|
174
|
+
return response
|
|
175
|
+
}
|
|
176
|
+
async function reconcile(client, record, item) {
|
|
177
|
+
const list = await client.raw(
|
|
178
|
+
'/canvas/generations?' +
|
|
179
|
+
new URLSearchParams({
|
|
180
|
+
doc_id: record.input.doc_id,
|
|
181
|
+
node_id: item.nodeId,
|
|
182
|
+
status: 'completed',
|
|
183
|
+
limit: '100'
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
const current = await readContext(client, record.input)
|
|
187
|
+
if (digest(current.binding) !== digest(record.binding))
|
|
188
|
+
throw new Error('恢复上传前源素材已变化,停止')
|
|
189
|
+
const node = current.doc.nodes.find((n) => n.id === item.nodeId)
|
|
190
|
+
const uploadTask = String(node?.data?.task_id || '')
|
|
191
|
+
const uploadRef = node?.data?.file_ref
|
|
192
|
+
const candidates = list.items?.filter(
|
|
193
|
+
(task) =>
|
|
194
|
+
task.doc_id === record.input.doc_id &&
|
|
195
|
+
task.node_id === item.nodeId &&
|
|
196
|
+
task.segment_type === 'image' &&
|
|
197
|
+
(task.model_name === 'local-upload' ||
|
|
198
|
+
(uploadTask &&
|
|
199
|
+
String(task.task_id ?? task.id) === uploadTask &&
|
|
200
|
+
uploadRef &&
|
|
201
|
+
task.result?.file_ref === uploadRef &&
|
|
202
|
+
task.credit_cost === 0 &&
|
|
203
|
+
task.billing_status === 'charged')) &&
|
|
204
|
+
task.status === 'completed' &&
|
|
205
|
+
task.result?.filename === item.fileName &&
|
|
206
|
+
task.result?.file_ref &&
|
|
207
|
+
task.result?.url
|
|
208
|
+
)
|
|
209
|
+
if (candidates?.length !== 1)
|
|
210
|
+
throw fail(
|
|
211
|
+
codePrefix + '_UPLOAD_UNKNOWN',
|
|
212
|
+
'原上传 complete 结果未知;原节点未查到唯一真实任务,不重新 complete 或重传'
|
|
213
|
+
)
|
|
214
|
+
const task = candidates[0]
|
|
215
|
+
item.response = validateUpload(
|
|
216
|
+
{
|
|
217
|
+
source_doc_id: task.doc_id,
|
|
218
|
+
source_node_id: task.node_id,
|
|
219
|
+
segment_type: 'image',
|
|
220
|
+
task_id: task.task_id ?? task.id,
|
|
221
|
+
result: task.result
|
|
222
|
+
},
|
|
223
|
+
record,
|
|
224
|
+
item
|
|
225
|
+
)
|
|
226
|
+
item.state = 'uploaded'
|
|
227
|
+
}
|
|
228
|
+
async function runUploads(client, profile, record) {
|
|
229
|
+
record.state = 'uploading'
|
|
230
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
231
|
+
for (const item of record.items) {
|
|
232
|
+
const current = await readContext(client, record.input)
|
|
233
|
+
if (digest(record.binding) !== digest(current.binding))
|
|
234
|
+
throw new Error('上传前源素材已变化,停止')
|
|
235
|
+
const node = current.doc.nodes.find((node) => node.id === item.nodeId)
|
|
236
|
+
if (
|
|
237
|
+
!node ||
|
|
238
|
+
node.type !== nodeType(record, item) ||
|
|
239
|
+
!current.doc.edges.some(
|
|
240
|
+
(e) =>
|
|
241
|
+
e.source === record.input.node_id &&
|
|
242
|
+
e.target === item.nodeId &&
|
|
243
|
+
e.data?.role === edgeRole
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
throw new Error('图片处理节点或连线已被删除,不自动重建')
|
|
247
|
+
const currentRef =
|
|
248
|
+
node.data?.file_ref || node.data?.extra?.file_ref || node.data?.result?.file_ref
|
|
249
|
+
if (
|
|
250
|
+
currentRef &&
|
|
251
|
+
currentRef !== item.response?.result?.file_ref &&
|
|
252
|
+
!['completing', 'unknown'].includes(item.state)
|
|
253
|
+
)
|
|
254
|
+
throw new Error('图片处理节点已换成其他素材;保留用户编辑')
|
|
255
|
+
if (['completing', 'unknown'].includes(item.state)) {
|
|
256
|
+
record.state = 'upload_unknown'
|
|
257
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
258
|
+
await reconcile(client, record, item)
|
|
259
|
+
record.state = 'uploading'
|
|
260
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
261
|
+
}
|
|
262
|
+
if (item.state === 'session_unknown') {
|
|
263
|
+
record.state = 'upload_unknown'
|
|
264
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
265
|
+
throw fail(
|
|
266
|
+
codePrefix + '_UPLOAD_UNKNOWN',
|
|
267
|
+
'上传会话创建结果未知,停止;不自动创建第二个会话'
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
if (!item.response) {
|
|
271
|
+
const image = path.join(record.directory, item.fileName)
|
|
272
|
+
const local = await hashFile(image)
|
|
273
|
+
if (local.sha256 !== item.local.sha256 || local.bytes !== item.local.bytes)
|
|
274
|
+
throw new Error('本地图片处理产物发生变化,不上传')
|
|
275
|
+
const endpoint =
|
|
276
|
+
'/canvas/docs/' + segment(record.input.doc_id) + '/nodes/' + segment(item.nodeId)
|
|
277
|
+
if (!item.session) {
|
|
278
|
+
item.state = 'session_unknown'
|
|
279
|
+
record.state = 'upload_unknown'
|
|
280
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
281
|
+
try {
|
|
282
|
+
item.session = await client.raw(
|
|
283
|
+
endpoint + '/upload-session',
|
|
284
|
+
{
|
|
285
|
+
method: 'POST',
|
|
286
|
+
body: {
|
|
287
|
+
filename: item.fileName,
|
|
288
|
+
file_size: local.bytes,
|
|
289
|
+
expected_type: 'image',
|
|
290
|
+
mime_type: mime
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
{ retry: false }
|
|
294
|
+
)
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw fail(
|
|
297
|
+
codePrefix + '_UPLOAD_UNKNOWN',
|
|
298
|
+
'创建图片处理上传会话未确认;停止并保留原操作',
|
|
299
|
+
error
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
item.state = 'uploading'
|
|
303
|
+
record.state = 'uploading'
|
|
304
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
305
|
+
}
|
|
306
|
+
if (!item.parts) {
|
|
307
|
+
const handle = await fs.open(image, 'r')
|
|
308
|
+
try {
|
|
309
|
+
item.parts = await uploadParts(client, handle, await handle.stat(), item.session, mime)
|
|
310
|
+
} finally {
|
|
311
|
+
await handle.close()
|
|
312
|
+
}
|
|
313
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
314
|
+
}
|
|
315
|
+
item.state = 'completing'
|
|
316
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
317
|
+
try {
|
|
318
|
+
item.response = validateUpload(
|
|
319
|
+
await client.raw(
|
|
320
|
+
endpoint + '/upload-complete',
|
|
321
|
+
{
|
|
322
|
+
method: 'POST',
|
|
323
|
+
body: {
|
|
324
|
+
upload_id: item.session.upload_id,
|
|
325
|
+
object_key: item.session.object_key,
|
|
326
|
+
expected_parts: item.parts.length,
|
|
327
|
+
parts: item.parts
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
{ retry: false }
|
|
331
|
+
),
|
|
332
|
+
record,
|
|
333
|
+
item
|
|
334
|
+
)
|
|
335
|
+
} catch (error) {
|
|
336
|
+
item.state = 'unknown'
|
|
337
|
+
record.state = 'upload_unknown'
|
|
338
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
339
|
+
throw fail(
|
|
340
|
+
codePrefix + '_UPLOAD_UNKNOWN',
|
|
341
|
+
'图片处理上传完成结果未确认;只查询原节点/任务恢复,不再次 complete',
|
|
342
|
+
error
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
item.state = 'uploaded'
|
|
346
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
347
|
+
}
|
|
348
|
+
if (!item.bound) {
|
|
349
|
+
try {
|
|
350
|
+
await bindUpload(client, {
|
|
351
|
+
docId: record.input.doc_id,
|
|
352
|
+
nodeId: item.nodeId,
|
|
353
|
+
result: item.response,
|
|
354
|
+
imageNode: nodeType(record, item) === 'image'
|
|
355
|
+
})
|
|
356
|
+
// Clear the preparation flag only after real upload binding succeeds.
|
|
357
|
+
const doc = await scopedCanvas(
|
|
358
|
+
client,
|
|
359
|
+
record.input.doc_id,
|
|
360
|
+
record.input.space,
|
|
361
|
+
record.input.team_uid
|
|
362
|
+
)
|
|
363
|
+
const node = doc.nodes.find((n) => n.id === item.nodeId)
|
|
364
|
+
if (node?.data?.extra?.file_ref !== item.response.result.file_ref)
|
|
365
|
+
throw new Error('图片处理引用读回不一致')
|
|
366
|
+
item.bound = true
|
|
367
|
+
item.state = 'completed'
|
|
368
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
369
|
+
} catch (error) {
|
|
370
|
+
record.state = 'sync_pending'
|
|
371
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
372
|
+
throw fail(
|
|
373
|
+
codePrefix + '_SYNC_PENDING',
|
|
374
|
+
'图片已上传,画布绑定未确认;恢复原响应,不重传图片',
|
|
375
|
+
error
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
record.state = 'completed'
|
|
381
|
+
await atomic(recordPath(profile, record.operation), record)
|
|
382
|
+
return summarize(record)
|
|
383
|
+
}
|
|
384
|
+
return { persistNodes, runUploads }
|
|
385
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
// A failed download/write never occupies the requested filename. Linking in the
|
|
6
|
+
// same directory publishes a complete file without replacing an existing file.
|
|
7
|
+
export async function exclusiveOutput(output, write) {
|
|
8
|
+
const target = path.resolve(output)
|
|
9
|
+
const temporary = path.join(path.dirname(target), '.' + crypto.randomUUID() + '.part')
|
|
10
|
+
try {
|
|
11
|
+
try {
|
|
12
|
+
await fs.lstat(target)
|
|
13
|
+
throw Object.assign(new Error('目标文件已存在;请指定新文件,不覆盖'), { code: 'EEXIST' })
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error.code !== 'ENOENT') throw error
|
|
16
|
+
}
|
|
17
|
+
const result = await write(temporary)
|
|
18
|
+
await fs.link(temporary, target)
|
|
19
|
+
return { output: target, ...result }
|
|
20
|
+
} finally {
|
|
21
|
+
await fs.rm(temporary, { force: true })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { materialGet } from './materials.js'
|
|
5
|
+
import { requestFile } from './api.js'
|
|
6
|
+
import { exclusiveOutput } from './local-output.js'
|
|
7
|
+
import { downloadName } from './download-name.js'
|
|
8
|
+
|
|
9
|
+
// Material download is a separate permission gate. A URL returned by list/get
|
|
10
|
+
// never substitutes for a rejected download, even if that URL is still valid.
|
|
11
|
+
export async function downloadMaterial(client, { space, teamUid, assetId, output, expectedHash }) {
|
|
12
|
+
if (typeof output !== 'string' || !output.trim()) throw new Error('素材下载必须提供--output FILE')
|
|
13
|
+
if (expectedHash !== undefined && (typeof expectedHash !== 'string' || !/^[a-fA-F0-9]{64}$/.test(expectedHash)))
|
|
14
|
+
throw new Error('--sha256 必须是64位十六进制')
|
|
15
|
+
// Resolve scope through the actual folder list and asset detail before asking
|
|
16
|
+
// the server for a new original-file link. No filename or format override.
|
|
17
|
+
const asset = await materialGet(client, { space, teamUid, assetId })
|
|
18
|
+
if (String(asset.id) !== String(assetId) || !/^(image|video|audio)\//.test(asset.mime_type || '') || !asset.file_ref)
|
|
19
|
+
throw new Error('素材身份、媒体类型或原文件引用不完整,停止下载')
|
|
20
|
+
const major = asset.mime_type.split('/')[0]
|
|
21
|
+
const link = await client.raw('/material-space/assets/' + encodeURIComponent(String(assetId)) + '/download',
|
|
22
|
+
{}, { retry: false })
|
|
23
|
+
if (!link?.file_ref || typeof link.url !== 'string' || !link.url.trim() ||
|
|
24
|
+
(link.format !== undefined && link.format !== 'original'))
|
|
25
|
+
throw new Error('素材原文件下载接口未返回有效原格式链接;不改用列表地址或转换格式')
|
|
26
|
+
const url = new URL(link.url, new URL(client.baseUrl).origin)
|
|
27
|
+
if (url.searchParams.has('format') && url.searchParams.get('format') !== 'original')
|
|
28
|
+
throw new Error('原文件下载链接包含转换参数;停止,不更改服务端URL或偷偷转换')
|
|
29
|
+
const saved = await exclusiveOutput(output, async temporary => {
|
|
30
|
+
const response = await requestFile(url, client.timeout)
|
|
31
|
+
const mime = (response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
|
|
32
|
+
if (mime !== 'application/octet-stream' && !mime.startsWith(major + '/')) {
|
|
33
|
+
await response.body?.cancel()
|
|
34
|
+
throw new Error('素材下载内容类型与原素材不一致;未保存文件')
|
|
35
|
+
}
|
|
36
|
+
const handle = await fs.open(temporary, 'wx', 0o600)
|
|
37
|
+
const hash = crypto.createHash('sha256')
|
|
38
|
+
let bytes = 0
|
|
39
|
+
try {
|
|
40
|
+
for await (const chunk of response.body) {
|
|
41
|
+
await handle.writeFile(chunk)
|
|
42
|
+
hash.update(chunk)
|
|
43
|
+
bytes += chunk.length
|
|
44
|
+
}
|
|
45
|
+
const sha256 = hash.digest('hex'), length = response.headers.get('content-length')
|
|
46
|
+
if (!bytes || (length !== null && !response.headers.get('content-encoding') && bytes !== Number(length)) ||
|
|
47
|
+
(expectedHash && sha256 !== expectedHash.toLowerCase()))
|
|
48
|
+
throw new Error('素材下载文件完整性校验失败;未保存文件')
|
|
49
|
+
return { bytes, sha256, server_filename: downloadName(response.headers, link.filename) }
|
|
50
|
+
} finally { await handle.close() }
|
|
51
|
+
})
|
|
52
|
+
return { ...saved, output: path.resolve(output), asset_id: asset.id, format: 'original',
|
|
53
|
+
...(typeof link.filename === 'string' ? { filename: link.filename } : {}),
|
|
54
|
+
canvas_written: false, generation_submitted: false, processing_submitted: false, library_saved: false }
|
|
55
|
+
}
|