@uzooai/anime-ai-cli 0.0.0-stage → 0.6.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 +89 -0
- package/.agents/skills/uzoo-anime-cli/references/agreement-workflow.md +41 -0
- package/.agents/skills/uzoo-anime-cli/references/canvas-workflow.md +53 -0
- package/.agents/skills/uzoo-anime-cli/references/commands.md +242 -0
- package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +142 -0
- package/.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md +153 -0
- package/.agents/skills/uzoo-anime-cli/references/interaction-workflow.md +67 -0
- package/.agents/skills/uzoo-anime-cli/references/material-graph-workflow.md +113 -0
- package/.agents/skills/uzoo-anime-cli/references/node-capabilities.md +64 -0
- package/.agents/skills/uzoo-anime-cli/references/node-workflow.md +90 -0
- package/.agents/skills/uzoo-anime-cli/references/remake-workflow.md +54 -0
- package/.agents/skills/uzoo-anime-cli/references/repaint-workflow.md +13 -0
- package/.agents/skills/uzoo-anime-cli/references/seedance-review-workflow.md +37 -0
- package/.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md +67 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +46 -3
- package/assets/favicon-uzoo.png +0 -0
- package/assets/logo-uzoo.svg +7 -0
- package/docs/Agent/346/216/245/345/205/245/344/270/216/347/211/210/346/234/254/346/233/264/346/226/260.md +43 -0
- package/examples/matting.json +5 -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 +53 -6
- 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 +464 -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 +125 -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/install.js +237 -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,63 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import { getRepaintHistory } from './repaint-history.js'
|
|
3
|
+
import { reversePath } from './repaint-reverse.js'
|
|
4
|
+
import { deliveryPath } from './repaint-delivery.js'
|
|
5
|
+
import { repaintVideoPath } from './repaint-video.js'
|
|
6
|
+
import { json } from './store.js'
|
|
7
|
+
import { readRepaintArchive, archiveRepaintResult } from './repaint-archive.js'
|
|
8
|
+
import { exclusiveOutput } from './local-output.js'
|
|
9
|
+
import { hashFile, downloadVideo } from './media.js'
|
|
10
|
+
import { jobPath } from './repaint-graph.js'
|
|
11
|
+
|
|
12
|
+
const paths = { reverse: reversePath, delivery: deliveryPath, video: repaintVideoPath }
|
|
13
|
+
const fail = (code, message) => Object.assign(new Error(message), { code })
|
|
14
|
+
export async function exportRepaintResult(client, profile, operation, type, output, source = 'remote') {
|
|
15
|
+
if (!Object.hasOwn(paths, type) || !['remote', 'archive'].includes(source) || !output)
|
|
16
|
+
throw new Error('导出需要type reverse/delivery/video、output及source remote/archive')
|
|
17
|
+
if (type === 'video' && source !== 'remote')
|
|
18
|
+
throw new Error('视频归档不保存可绕过权限的下载资源;请使用remote')
|
|
19
|
+
// This retains account, space, original source, plan and node checks even when
|
|
20
|
+
// settings changed. It never relaxes submission or canvas recovery bindings.
|
|
21
|
+
const history = await getRepaintHistory(client, profile, operation, type)
|
|
22
|
+
const record = await json(paths[type](profile, operation))
|
|
23
|
+
let result = history.result, verifiedAt = new Date().toISOString()
|
|
24
|
+
let details = type === 'delivery' ? { fallback: history.fallback === true, warning: history.warning || null } : {}
|
|
25
|
+
if (source === 'archive') {
|
|
26
|
+
const archive = await readRepaintArchive(record, type)
|
|
27
|
+
if (!archive) throw fail('REPAINT_ARCHIVE_UNAVAILABLE', '没有已核验的本地原结果快照;不补造旧历史')
|
|
28
|
+
result = archive.payload.result
|
|
29
|
+
details = archive.payload.details
|
|
30
|
+
verifiedAt = archive.verified_at
|
|
31
|
+
} else if (!history.original_run_verified || !history.result_available || history.status !== 'completed') {
|
|
32
|
+
throw fail('REPAINT_RESULT_UNAVAILABLE', '原运行未确认完成或原结果不可用;不导出当前其他轮次,不重新生成')
|
|
33
|
+
}
|
|
34
|
+
// Archive only results verified from this remote run, never inferred history.
|
|
35
|
+
if (source === 'remote') await archiveRepaintResult(record, type, result, history.billing,
|
|
36
|
+
details)
|
|
37
|
+
let exported
|
|
38
|
+
if (type === 'video') {
|
|
39
|
+
let link
|
|
40
|
+
try {
|
|
41
|
+
link = await client.raw(jobPath(history.job_id) + '/redraw-tasks/' + encodeURIComponent(history.task_id) + '/download',
|
|
42
|
+
{ headers: { 'X-Client-Source': 'canvas' } }, { retry: false })
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.apiCode !== 2001) throw error
|
|
45
|
+
throw Object.assign(fail('REPAINT_VIDEO_DOWNLOAD_FORBIDDEN', '原转绘下载接口拒绝访问(2001);不推断为白名单问题,核验当前部署、账号及原任务可见性后仅继续原任务下载;生成仍已完成'), {
|
|
46
|
+
diagnostic: { ...(error.status ? { http_status: error.status } : {}), api_code: error.apiCode,
|
|
47
|
+
...(error.requestId ? { request_id: error.requestId } : {}) }
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
if (!link?.file_ref || !link.url) throw new Error('原任务下载接口未返回有效文件引用')
|
|
51
|
+
exported = await exclusiveOutput(output, temporary => downloadVideo(client, link.url, temporary))
|
|
52
|
+
} else {
|
|
53
|
+
if (typeof result?.prompt !== 'string' || !result.prompt.trim())
|
|
54
|
+
throw fail('REPAINT_RESULT_UNAVAILABLE', '原文本结果缺少提示词')
|
|
55
|
+
exported = await exclusiveOutput(output, async temporary => {
|
|
56
|
+
await fs.writeFile(temporary, result.prompt, { flag: 'wx', mode: 0o600 })
|
|
57
|
+
return hashFile(temporary)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
return { operation, type, source, task_id: history.task_id || null, ...exported,
|
|
61
|
+
verified_at: verifiedAt, ...details, current_configuration: history.current_configuration,
|
|
62
|
+
canvas_written: false, generation_submitted: false, processing_submitted: false }
|
|
63
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Persisted canvas protocol, independent of Vue and the frontend runtime.
|
|
2
|
+
import { segment } from './workflows.js'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
+
|
|
5
|
+
export const object = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
6
|
+
export const repaintOf = node => node?.data?.repaint || node?.data?.extra?.repaint || {}
|
|
7
|
+
export const mediaUrl = node => node?.data?.url || node?.data?.result?.url || ''
|
|
8
|
+
export const jobPath = id => `/video-segmentation/${segment(id)}`
|
|
9
|
+
export function assertJob(job, record) {
|
|
10
|
+
const body = record.input
|
|
11
|
+
if (!job || String(job.id) !== record.jobId || job.canvas_doc_id !== body.doc_id ||
|
|
12
|
+
job.canvas_node_id !== record.detailId || String(job.team_uid || '') !== String(body.team_uid || '') ||
|
|
13
|
+
job.segmentation_mode !== body.segmentation_mode) throw new Error('任务与 operation 的画布、节点、团队或分割模式绑定不一致,停止')
|
|
14
|
+
}
|
|
15
|
+
export async function readCanvas(client, input) {
|
|
16
|
+
const doc = await client.raw(`/canvas/docs/${segment(input.doc_id)}`)
|
|
17
|
+
if (doc?.id !== input.doc_id || !Number.isSafeInteger(doc.version) || doc.version < 1 ||
|
|
18
|
+
!Array.isArray(doc.nodes) || !Array.isArray(doc.edges) || !object(doc.meta) || !object(doc.viewport) || typeof doc.name !== 'string') throw new Error('画布快照不完整,停止保存')
|
|
19
|
+
if (!['owner', 'editor'].includes(doc.role)) throw new Error('画布没有可确认的编辑权限')
|
|
20
|
+
if (String(doc.team_uid || '') !== String(input.team_uid || '')) throw new Error('画布归属与已选空间/团队不一致,停止')
|
|
21
|
+
if (new Set(doc.nodes.map(n => n.id)).size !== doc.nodes.length) throw new Error('画布有重复节点 ID,停止')
|
|
22
|
+
const source = doc.nodes.find(n => n.id === input.source_node_id)
|
|
23
|
+
const entry = doc.nodes.find(n => n.id === input.entry_node_id)
|
|
24
|
+
if (!source || !['video', 'video_upload'].includes(source.type) || !mediaUrl(source) || repaintOf(source).role ||
|
|
25
|
+
source.data?.videoRemake || source.data?.clipRedraw || source.data?.extra?.video_remake || source.data?.extra?.clip_redraw) throw new Error('源节点必须是有真实视频地址的普通视频/视频素材节点')
|
|
26
|
+
if (!entry || entry.type !== 'video_repain_begain' || repaintOf(entry).role !== 'entry') throw new Error('entry_node_id 必须指向普通视频转绘入口')
|
|
27
|
+
const inputs = doc.edges.filter(e => e.target === entry.id && e.data?.role === 'video-repaint-input')
|
|
28
|
+
if (inputs.some(e => e.source !== source.id)) throw new Error('视频转绘入口已连接另一源视频,停止')
|
|
29
|
+
return doc
|
|
30
|
+
}
|
|
31
|
+
const promptText = value => typeof value === 'string' ? value : value?.prompt || value?.text || value?.content || ''
|
|
32
|
+
const absoluteMedia = (url, base) => {
|
|
33
|
+
const parsed = new URL(url, base)
|
|
34
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) throw new Error('任务媒体地址无效')
|
|
35
|
+
return parsed.href
|
|
36
|
+
}
|
|
37
|
+
function node(id, type, title, x, y, data, width = 320, height = 260) {
|
|
38
|
+
return { id, type, position: { x, y }, width, height, data: { title, ...data } }
|
|
39
|
+
}
|
|
40
|
+
function edge(id, source, target, role) {
|
|
41
|
+
return { id, source, target, sourceHandle: '', targetHandle: '', type: 'default', data: { repaintSystem: true, role } }
|
|
42
|
+
}
|
|
43
|
+
function addEdge(doc, wanted) {
|
|
44
|
+
const collision = doc.edges.find(e => e.id === wanted.id)
|
|
45
|
+
if (collision && (collision.source !== wanted.source || collision.target !== wanted.target || collision.data?.role !== wanted.data.role)) throw new Error('流程连线 ID 冲突')
|
|
46
|
+
if (!collision && !doc.edges.some(e => e.source === wanted.source && e.target === wanted.target && e.data?.role === wanted.data.role)) doc.edges.push(wanted)
|
|
47
|
+
}
|
|
48
|
+
function segmentsOf(job) {
|
|
49
|
+
const segments = job.plan?.segments
|
|
50
|
+
if (!Array.isArray(segments) || !segments.length || segments.length > 63 || new Set(segments.map(s => s.index)).size !== segments.length ||
|
|
51
|
+
segments.some(s => !Number.isSafeInteger(s.index) || s.index < 1 || !Number.isSafeInteger(s.start_ms) || !Number.isSafeInteger(s.end_ms) || s.end_ms <= s.start_ms || s.duration_ms !== s.end_ms - s.start_ms)) throw new Error('完成任务的真实片段列表无效')
|
|
52
|
+
return segments
|
|
53
|
+
}
|
|
54
|
+
function checkCompletedGroup(doc, group, job) {
|
|
55
|
+
if (!Array.isArray(group.nodeIds) || new Set(group.nodeIds).size !== group.nodeIds.length || !group.nodeIds.every(id => doc.nodes.some(n => n.id === id))) throw new Error('已有流程分组不完整,停止')
|
|
56
|
+
const nodes = doc.nodes.filter(n => group.nodeIds.includes(n.id) && repaintOf(n).jobId === job.id)
|
|
57
|
+
const find = (role, index) => nodes.filter(n => repaintOf(n).role === role && (index === undefined || repaintOf(n).segmentIndex === index))
|
|
58
|
+
if (find('global-prompt').length !== 1 || segmentsOf(job).some(s => ['segment', 'reverse-prompt', 'final-prompt', 'generation'].some(role => find(role, s.index).length !== 1))) throw new Error('已有流程分组缺少完整片段节点链,停止')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function assertAvailable(doc, record) {
|
|
62
|
+
const pending = doc.meta.cli_repaint_pending || {}
|
|
63
|
+
if (!object(pending)) throw new Error('待处理流程元数据无效')
|
|
64
|
+
for (const [operation, claim] of Object.entries(pending)) {
|
|
65
|
+
if (claim?.source_id === record.input.source_node_id && claim?.entry_id === record.input.entry_node_id &&
|
|
66
|
+
(operation !== record.operation || claim.job_id !== record.jobId)) throw new Error('该入口与源视频已有未确认流程;先恢复原 operation,不能重新提交分析')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function claimRepaint(client, record) {
|
|
70
|
+
const doc = await readCanvas(client, record.input)
|
|
71
|
+
assertAvailable(doc, record)
|
|
72
|
+
if (doc.nodes.some(n => repaintOf(n).role === 'split-detail' && repaintOf(n).sourceNodeId === record.input.source_node_id && repaintOf(n).parentNodeId === record.input.entry_node_id)) throw new Error('该源视频已有分段详情,未提交新分析')
|
|
73
|
+
const claim = { job_id: record.jobId, source_id: record.input.source_node_id, entry_id: record.input.entry_node_id, detail_id: record.detailId }
|
|
74
|
+
if (isDeepStrictEqual(doc.meta.cli_repaint_pending?.[record.operation], claim)) return
|
|
75
|
+
const body = { version: doc.version, name: doc.name, viewport: doc.viewport, nodes: doc.nodes, edges: doc.edges,
|
|
76
|
+
meta: { ...doc.meta, cli_repaint_pending: { ...doc.meta.cli_repaint_pending, [record.operation]: claim } } }
|
|
77
|
+
// The reservation contains no fabricated node and prevents a second CLI operation from billing the same flow.
|
|
78
|
+
try {
|
|
79
|
+
const saved = await client.raw(`/canvas/docs/${segment(doc.id)}`, { method: 'PUT', body }, { retry: false })
|
|
80
|
+
if (saved?.meta?.cli_repaint_pending?.[record.operation]?.job_id !== record.jobId) throw new Error('流程占用保存响应不完整')
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.status === 409 || error.apiCode === 3002) throw new Error('流程占用版本冲突,未执行分析提交;读取最新画布后再决定继续原 operation', { cause: error })
|
|
83
|
+
throw new Error('流程占用保存结果未确认,未执行分析提交;先读取画布核对原 operation', { cause: error })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// This private writer only accepts a server job whose binding was verified above.
|
|
88
|
+
// Generic node add/update remains unable to fabricate derived nodes.
|
|
89
|
+
export async function syncRepaintGraph(client, record, job, { restoreDetail = false } = {}) {
|
|
90
|
+
assertJob(job, record)
|
|
91
|
+
const doc = await readCanvas(client, record.input)
|
|
92
|
+
assertAvailable(doc, record)
|
|
93
|
+
const before = structuredClone([doc.nodes, doc.edges, doc.meta])
|
|
94
|
+
const source = doc.nodes.find(n => n.id === record.input.source_node_id)
|
|
95
|
+
const entry = doc.nodes.find(n => n.id === record.input.entry_node_id)
|
|
96
|
+
const aspectRatio = source.data.aspect_ratio || source.data.extra?.aspect_ratio || '16:9'
|
|
97
|
+
const markers = doc.meta.cli_repaint_workflows || {}
|
|
98
|
+
if (!object(markers)) throw new Error('流程元数据格式无效')
|
|
99
|
+
const stored = Object.hasOwn(markers, record.operation) ? markers[record.operation] : null
|
|
100
|
+
if (stored && (stored.job_id !== job.id || stored.detail_id !== record.detailId)) throw new Error('画布流程绑定冲突')
|
|
101
|
+
let detail = doc.nodes.find(n => n.id === record.detailId)
|
|
102
|
+
if (restoreDetail && (job.status !== 'awaiting_review' || stored?.materialized || record.lastStatus === 'completed')) throw new Error('仅可恢复原待审核分析详情,不重建已切片节点链')
|
|
103
|
+
if ((stored || record.lastStatus) && !detail && !restoreDetail) throw new Error('流程详情已被删除,停止;不会自动重建')
|
|
104
|
+
if (detail && (repaintOf(detail).role !== 'split-detail' || repaintOf(detail).jobId !== job.id)) throw new Error('流程详情 ID 已被其他节点占用')
|
|
105
|
+
if (doc.nodes.some(n => n.id !== record.detailId && repaintOf(n).role === 'split-detail' &&
|
|
106
|
+
(repaintOf(n).jobId === job.id || (repaintOf(n).sourceNodeId === source.id && repaintOf(n).parentNodeId === entry.id)))) throw new Error('已有其他分段详情,停止避免重复流程')
|
|
107
|
+
if (!detail) {
|
|
108
|
+
detail = node(record.detailId, 'video_upload', '视频分段详情', (entry.position?.x || 0) + (entry.width || 320) + 100, entry.position?.y || 0,
|
|
109
|
+
{ mediaType: 'video', url: mediaUrl(source), file_name: source.data.file_name || job.source_name || '', aspect_ratio: aspectRatio, extra: {} })
|
|
110
|
+
doc.nodes.push(detail)
|
|
111
|
+
}
|
|
112
|
+
if (!stored) {
|
|
113
|
+
const manual = record.input.segmentation_mode === 'uniform'
|
|
114
|
+
entry.data.extra = { ...entry.data.extra, repaint: { ...repaintOf(entry), role: 'entry',
|
|
115
|
+
splitMode: manual ? 'manual' : 'intelligent', manualSplitExpanded: manual,
|
|
116
|
+
...(manual ? { manualSplitStrategy: 'fixed-duration', manualSplitSeconds: record.input.uniform_interval_seconds } : {}) } }
|
|
117
|
+
if (entry.data.repaint) entry.data.repaint = entry.data.extra.repaint
|
|
118
|
+
}
|
|
119
|
+
const phase = ['failed', 'cancelled'].includes(job.status) ? 'failed' : ['awaiting_review', 'rendering', 'completed'].includes(job.status) || job.confirmed_at ? 'completed' : 'analyzing'
|
|
120
|
+
detail.data.extra = { ...detail.data.extra, repaint_job_id: job.id, repaint_source_node_id: source.id,
|
|
121
|
+
repaint: { ...repaintOf(detail), role: 'split-detail', sourceNodeId: source.id, parentNodeId: entry.id,
|
|
122
|
+
jobId: job.id, analysisStatus: phase, segmentCount: job.plan?.segments?.length || 0,
|
|
123
|
+
progress: Number(job.progress) || 0, stageMessage: job.stage_message || '', error: job.analysis_error_summary || job.error || '',
|
|
124
|
+
splitMode: record.input.segmentation_mode === 'intelligent' ? 'smart' : 'even',
|
|
125
|
+
uniformIntervalSeconds: record.input.uniform_interval_seconds ?? null } }
|
|
126
|
+
// Avoid leaving a stale alternate representation if the server supplied both forms.
|
|
127
|
+
if (detail.data.repaint) detail.data.repaint = detail.data.extra.repaint
|
|
128
|
+
if (detail.data.repaintJobId) detail.data.repaintJobId = job.id
|
|
129
|
+
for (const wanted of [edge(`${record.prefix}-input`, source.id, entry.id, 'video-repaint-input'), edge(`${record.prefix}-detail-link`, entry.id, detail.id, 'split-detail-link')]) {
|
|
130
|
+
if (stored && !restoreDetail && !doc.edges.some(e => e.source === wanted.source && e.target === wanted.target && e.data?.role === wanted.data.role)) throw new Error('流程连线已被删除,停止;不会自动重建')
|
|
131
|
+
addEdge(doc, wanted)
|
|
132
|
+
}
|
|
133
|
+
const groups = doc.meta.canvas_groups || []
|
|
134
|
+
if (!Array.isArray(groups)) throw new Error('画布分组格式无效')
|
|
135
|
+
const linkedGroups = groups.filter(g => (g.repaint?.jobId || g.repaintJobId) === job.id)
|
|
136
|
+
if (linkedGroups.length > 1) throw new Error('任务存在多个分组,停止避免重复物化')
|
|
137
|
+
let group = linkedGroups[0]
|
|
138
|
+
if (stored?.materialized) {
|
|
139
|
+
if (!group || group.id !== stored.group_id || group.repaint?.status !== 'completed' ||
|
|
140
|
+
!Array.isArray(stored.node_ids) || !stored.node_ids.every(id => doc.nodes.some(n => n.id === id))) throw new Error('已保存的流程节点或分组发生删除/变更,停止;不会自动重建')
|
|
141
|
+
if (stored.plan_version !== job.plan_version) throw new Error('分段方案已变化;当前版本不自动覆盖已有节点链')
|
|
142
|
+
checkCompletedGroup(doc, group, job)
|
|
143
|
+
} else if (job.status === 'completed') {
|
|
144
|
+
if (job.confirmed_plan_version !== job.plan_version || !job.confirmed_at) throw new Error('完成任务缺少匹配的已确认方案版本')
|
|
145
|
+
if (record.confirmVersion && record.confirmVersion !== job.plan_version) throw new Error('任务方案版本已变化,停止物化')
|
|
146
|
+
if (group?.repaint?.status === 'completed') {
|
|
147
|
+
// The webpage may already have materialized this real job. Adopt instead of duplicating.
|
|
148
|
+
checkCompletedGroup(doc, group, job)
|
|
149
|
+
} else {
|
|
150
|
+
const foreign = doc.nodes.filter(n => n.id !== detail.id && repaintOf(n).jobId === job.id && repaintOf(n).role !== 'entry')
|
|
151
|
+
if (foreign.length) throw new Error('任务已有未归组派生节点,停止避免重复创建')
|
|
152
|
+
const segments = segmentsOf(job)
|
|
153
|
+
const downloads = await client.raw(`${jobPath(job.id)}/downloads`)
|
|
154
|
+
const files = downloads?.files
|
|
155
|
+
if (!Array.isArray(files) || new Set(files.map(f => f.index)).size !== files.length || segments.some(s => !files.some(f => f.index === s.index && f.url))) throw new Error('片段下载结果不完整,未写入节点链')
|
|
156
|
+
const x = group?.position?.x ?? ((detail.position?.x || 0) + 440)
|
|
157
|
+
const y = group?.position?.y ?? (detail.position?.y || 0)
|
|
158
|
+
const generated = [], links = []
|
|
159
|
+
const textData = (value, role, index) => ({ prompt: value, output: value, extra: { repaint: { jobId: job.id, role, ...(index ? { segmentIndex: index } : {}) } } })
|
|
160
|
+
const globalId = `${record.prefix}-global`
|
|
161
|
+
const globalData = textData(String(group?.repaint?.globalPrompt || ''), 'global-prompt')
|
|
162
|
+
globalData.extra.repaint.attachments = Array.isArray(group?.repaint?.globalPromptAttachments) ? group.repaint.globalPromptAttachments : []
|
|
163
|
+
generated.push(node(globalId, 'text', '全局提示词', x + 408, y + 24, globalData, 320, 240))
|
|
164
|
+
for (const [row, s] of segments.entries()) {
|
|
165
|
+
const f = files.find(item => item.index === s.index)
|
|
166
|
+
const out = (job.outputs || []).find(item => item.index === s.index) || {}
|
|
167
|
+
const delivery = (job.delivery_prompts || []).find(item => item.segment_index === s.index)
|
|
168
|
+
const edit = (job.delivery_prompt_edits || []).find(item => item.segment_index === s.index)
|
|
169
|
+
const reverse = promptText(out.reverse_prompt), final = edit?.prompt ?? (promptText(delivery) || reverse)
|
|
170
|
+
const prefix = `${record.prefix}-${s.index}`, yy = y + 312 + row * 330
|
|
171
|
+
const segmentId = `${prefix}-segment`, reverseId = `${prefix}-reverse`, finalId = `${prefix}-final`, generationId = `${prefix}-generation`
|
|
172
|
+
generated.push(node(segmentId, 'video_upload', `片段 ${s.index}`, x + 24, yy,
|
|
173
|
+
{ mediaType: 'video', url: absoluteMedia(f.url, client.baseUrl), file_name: f.file_name || out.file_name || '',
|
|
174
|
+
aspect_ratio: aspectRatio, duration: Number(s.duration_ms) / 1000, extra: { node_kind: 'asset', repaint: { jobId: job.id, role: 'segment', segmentIndex: s.index } } }))
|
|
175
|
+
generated.push(node(reverseId, 'text', `反推提示词 ${s.index}`, x + 408, yy, textData(reverse, 'reverse-prompt', s.index), 320, 240))
|
|
176
|
+
generated.push(node(finalId, 'text', `最终提示词 ${s.index}`, x + 792, yy, textData(final, 'final-prompt', s.index), 320, 240))
|
|
177
|
+
generated.push(node(generationId, 'video', `生成视频 ${s.index}`, x + 1176, yy,
|
|
178
|
+
{ prompt: '', extra: { aspect_ratio: '16:9', repaint: { jobId: job.id, role: 'generation', segmentIndex: s.index } } }))
|
|
179
|
+
for (const [a, b, role] of [[detail.id, segmentId, 'source-link'], [segmentId, reverseId, 'segment-reverse-link'], [reverseId, finalId, 'reverse-final-link'], [globalId, finalId, 'global-prompt-link'], [finalId, generationId, 'final-generation-link']]) links.push(edge(`${prefix}-${role}`, a, b, role))
|
|
180
|
+
}
|
|
181
|
+
if (generated.some(n => doc.nodes.some(existing => existing.id === n.id))) throw new Error('流程节点 ID 已存在,停止')
|
|
182
|
+
doc.nodes.push(...generated)
|
|
183
|
+
links.forEach(e => addEdge(doc, e))
|
|
184
|
+
if (!group) {
|
|
185
|
+
if (groups.some(g => g.id === record.groupId)) throw new Error('流程分组 ID 冲突')
|
|
186
|
+
group = { id: record.groupId, exitedNodeIds: [], position: { x, y }, padding: 24, color: '#2c2c2c', borderColor: 'rgba(255, 255, 255, 0.28)', label: '视频转绘' }
|
|
187
|
+
groups.push(group)
|
|
188
|
+
}
|
|
189
|
+
Object.assign(group, { nodeIds: generated.map(n => n.id), size: { width: 1520, height: 336 + segments.length * 330 }, repaintJobId: job.id,
|
|
190
|
+
repaint: { ...group.repaint, jobId: job.id, sourceNodeId: detail.id, status: 'completed', layout: { aspectRatio, mediaSize: { width: 320, height: 260 } } } })
|
|
191
|
+
doc.meta.canvas_groups = groups
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const materialized = Boolean(group?.repaint?.status === 'completed')
|
|
195
|
+
doc.meta.cli_repaint_workflows = { ...markers, [record.operation]: {
|
|
196
|
+
job_id: job.id, detail_id: detail.id, source_id: source.id, entry_id: entry.id, status: job.status,
|
|
197
|
+
plan_version: job.plan_version, materialized,
|
|
198
|
+
...(materialized ? { group_id: group.id, node_ids: group.nodeIds } : {}),
|
|
199
|
+
} }
|
|
200
|
+
if (doc.meta.cli_repaint_pending?.[record.operation]) {
|
|
201
|
+
delete doc.meta.cli_repaint_pending[record.operation]
|
|
202
|
+
if (!Object.keys(doc.meta.cli_repaint_pending).length) delete doc.meta.cli_repaint_pending
|
|
203
|
+
}
|
|
204
|
+
if (isDeepStrictEqual([doc.nodes, doc.edges, doc.meta], before)) return { saved: false, version: doc.version, detail_id: detail.id, materialized, group_id: group?.id }
|
|
205
|
+
const body = { version: doc.version, name: doc.name, viewport: doc.viewport, nodes: doc.nodes, edges: doc.edges, meta: doc.meta }
|
|
206
|
+
try {
|
|
207
|
+
const saved = await client.raw(`/canvas/docs/${segment(doc.id)}`, { method: 'PUT', body }, { retry: false })
|
|
208
|
+
if (saved?.version !== doc.version + 1 || !saved.nodes?.some(n => n.id === detail.id) || saved.meta?.cli_repaint_workflows?.[record.operation]?.job_id !== job.id) throw new Error('画布保存响应不完整')
|
|
209
|
+
return { saved: true, version: saved.version, detail_id: detail.id, materialized, group_id: group?.id }
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error.status === 409 || error.apiCode === 3002) throw new Error('画布版本冲突;先读取最新画布,再使用 repaint recover 核对恢复;未自动覆盖', { cause: error })
|
|
212
|
+
throw new Error('任务仍保留;画布保存结果未确认,使用 repaint recover 查询同一任务并核对恢复,勿重新提交分析', { cause: error })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { isDeepStrictEqual as equal } from 'node:util'
|
|
3
|
+
import { json, safeName } from './store.js'
|
|
4
|
+
import {
|
|
5
|
+
reversePath,
|
|
6
|
+
repaintPromptContext,
|
|
7
|
+
validateReverseRun
|
|
8
|
+
} from './repaint-reverse.js'
|
|
9
|
+
import { deliveryPath, validateDeliveryRun } from './repaint-delivery.js'
|
|
10
|
+
import {
|
|
11
|
+
repaintVideoPath,
|
|
12
|
+
validateRepaintVideoTask,
|
|
13
|
+
repaintVideoNodeView
|
|
14
|
+
} from './repaint-video.js'
|
|
15
|
+
import { repaintAttachmentView } from './repaint-settings.js'
|
|
16
|
+
import { repaintOf, jobPath } from './repaint-graph.js'
|
|
17
|
+
import { readSource } from './generation-input.js'
|
|
18
|
+
import { requireServiceReady } from './service.js'
|
|
19
|
+
|
|
20
|
+
const paths = {
|
|
21
|
+
reverse: reversePath,
|
|
22
|
+
delivery: deliveryPath,
|
|
23
|
+
video: repaintVideoPath
|
|
24
|
+
}
|
|
25
|
+
const hash = (v) =>
|
|
26
|
+
crypto.createHash('sha256').update(JSON.stringify(v)).digest('hex')
|
|
27
|
+
const omit = (v, keys) =>
|
|
28
|
+
Object.fromEntries(Object.entries(v).filter(([k]) => !keys.includes(k)))
|
|
29
|
+
const text = (v) => String(v || '').trim()
|
|
30
|
+
|
|
31
|
+
// Read-only path: never relax the binding used by submit/recover. Prompt APIs
|
|
32
|
+
// expose the latest run only; a local journal is not a server archive.
|
|
33
|
+
export async function getRepaintHistory(client, profile, operation, type) {
|
|
34
|
+
safeName(operation)
|
|
35
|
+
if (!Object.hasOwn(paths, type))
|
|
36
|
+
throw new Error('history --type须为reverse、delivery或video')
|
|
37
|
+
const r = await json(paths[type](profile, operation), null)
|
|
38
|
+
if (
|
|
39
|
+
!r ||
|
|
40
|
+
r.schema !== 1 ||
|
|
41
|
+
r.profile !== profile ||
|
|
42
|
+
r.operation !== operation ||
|
|
43
|
+
r.baseUrl !== client.baseUrl ||
|
|
44
|
+
!r.binding ||
|
|
45
|
+
!r.parentOperation
|
|
46
|
+
)
|
|
47
|
+
throw new Error('原历史operation不存在或服务/记录绑定不匹配')
|
|
48
|
+
await requireServiceReady(client)
|
|
49
|
+
const ctx = await repaintPromptContext(
|
|
50
|
+
client,
|
|
51
|
+
profile,
|
|
52
|
+
r.parentOperation,
|
|
53
|
+
r.index,
|
|
54
|
+
{ allowUnsyncedGlobal: true }
|
|
55
|
+
)
|
|
56
|
+
if (r.owner !== ctx.parent.owner) throw new Error('历史operation账号不匹配')
|
|
57
|
+
if (
|
|
58
|
+
!ctx.doc.edges.some(
|
|
59
|
+
(e) =>
|
|
60
|
+
e.source === ctx.global.id &&
|
|
61
|
+
e.target === ctx.final.id &&
|
|
62
|
+
e.data?.role === 'global-prompt-link'
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
throw new Error('原画布全局提示词系统线缺失')
|
|
66
|
+
let node = ctx.node,
|
|
67
|
+
binding = { ...ctx.binding },
|
|
68
|
+
mutable = ['rules']
|
|
69
|
+
if (type === 'delivery') {
|
|
70
|
+
node = ctx.final
|
|
71
|
+
binding = {
|
|
72
|
+
...binding,
|
|
73
|
+
node_id: node.id,
|
|
74
|
+
reverse_hash: hash(ctx.output.reverse_prompt)
|
|
75
|
+
}
|
|
76
|
+
mutable.push('reverse_hash', 'attachments')
|
|
77
|
+
} else if (type === 'video') {
|
|
78
|
+
const nodes = ctx.doc.nodes.filter(
|
|
79
|
+
(n) =>
|
|
80
|
+
ctx.doc.meta.cli_repaint_workflows[r.parentOperation].node_ids.includes(
|
|
81
|
+
n.id
|
|
82
|
+
) &&
|
|
83
|
+
repaintOf(n).jobId === ctx.job.id &&
|
|
84
|
+
repaintOf(n).role === 'generation' &&
|
|
85
|
+
repaintOf(n).segmentIndex === r.index
|
|
86
|
+
)
|
|
87
|
+
if (nodes.length !== 1 || nodes[0].type !== 'video')
|
|
88
|
+
throw new Error('原画布视频节点关联缺失或不唯一;不认领其他节点')
|
|
89
|
+
node = nodes[0]
|
|
90
|
+
if (
|
|
91
|
+
!ctx.doc.edges.some(
|
|
92
|
+
(e) =>
|
|
93
|
+
e.source === ctx.final.id &&
|
|
94
|
+
e.target === node.id &&
|
|
95
|
+
e.data?.role === 'final-generation-link'
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
throw new Error('原画布最终提示词→视频系统线缺失')
|
|
99
|
+
binding = {
|
|
100
|
+
...binding,
|
|
101
|
+
reverse_node_id: ctx.node.id,
|
|
102
|
+
node_id: node.id,
|
|
103
|
+
final_id: ctx.final.id,
|
|
104
|
+
final_text: readSource(ctx.final).text
|
|
105
|
+
}
|
|
106
|
+
mutable.push('final_text')
|
|
107
|
+
}
|
|
108
|
+
if (!equal(omit(binding, mutable), omit(r.binding, mutable)))
|
|
109
|
+
throw new Error('历史任务的job/方案/片段/来源/节点关联变化,停止查询')
|
|
110
|
+
const serverAttachments = repaintAttachmentView(
|
|
111
|
+
ctx.job.delivery_prompt_attachments,
|
|
112
|
+
client.baseUrl
|
|
113
|
+
)
|
|
114
|
+
const canvasAttachments = repaintAttachmentView(
|
|
115
|
+
repaintOf(ctx.global).attachments,
|
|
116
|
+
client.baseUrl
|
|
117
|
+
)
|
|
118
|
+
const canvasRules = text(
|
|
119
|
+
ctx.global.data?.output || ctx.global.data?.prompt || ctx.global.data?.input
|
|
120
|
+
)
|
|
121
|
+
.replace(/<[^>]*>/g, '')
|
|
122
|
+
.trim()
|
|
123
|
+
const changes = {
|
|
124
|
+
global_rules: ctx.binding.rules !== r.binding.rules,
|
|
125
|
+
canvas_global_rules: canvasRules !== r.binding.rules,
|
|
126
|
+
global_settings_synced:
|
|
127
|
+
canvasRules === ctx.binding.rules &&
|
|
128
|
+
equal(canvasAttachments, serverAttachments),
|
|
129
|
+
attachments: Object.hasOwn(r.binding, 'attachments')
|
|
130
|
+
? !equal(serverAttachments, r.binding.attachments)
|
|
131
|
+
: null,
|
|
132
|
+
...(type === 'delivery'
|
|
133
|
+
? { reverse_prompt: binding.reverse_hash !== r.binding.reverse_hash }
|
|
134
|
+
: {}),
|
|
135
|
+
...(type === 'video'
|
|
136
|
+
? { final_prompt: binding.final_text !== r.binding.final_text }
|
|
137
|
+
: {}),
|
|
138
|
+
original_attachment_snapshot_available: Object.hasOwn(
|
|
139
|
+
r.binding,
|
|
140
|
+
'attachments'
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
const out = {
|
|
144
|
+
operation,
|
|
145
|
+
repaint_operation: r.parentOperation,
|
|
146
|
+
type,
|
|
147
|
+
workflow: 'canvas_redraw',
|
|
148
|
+
read_only: true,
|
|
149
|
+
job_id: r.binding.job_id,
|
|
150
|
+
plan_version: r.binding.plan_version,
|
|
151
|
+
segment_index: r.index,
|
|
152
|
+
node_id: r.binding.node_id,
|
|
153
|
+
operation_state: r.state,
|
|
154
|
+
current_configuration: changes,
|
|
155
|
+
original_inputs: {
|
|
156
|
+
global_rules: r.binding.rules,
|
|
157
|
+
...(type === 'video'
|
|
158
|
+
? { parameters: r.input, prompt: r.body?.prompt }
|
|
159
|
+
: {}),
|
|
160
|
+
...(type === 'delivery' ? { rules: r.input } : {}),
|
|
161
|
+
attachment_count: Object.hasOwn(r.binding, 'attachments')
|
|
162
|
+
? r.binding.attachments.length
|
|
163
|
+
: null
|
|
164
|
+
},
|
|
165
|
+
original_run_verified: false,
|
|
166
|
+
result_available: false,
|
|
167
|
+
result: null,
|
|
168
|
+
billing: null,
|
|
169
|
+
canvas_matches_original_result: null,
|
|
170
|
+
canvas_written: false,
|
|
171
|
+
generation_submitted: false,
|
|
172
|
+
processing_submitted: false,
|
|
173
|
+
material_library_saved: false,
|
|
174
|
+
next: '仅查看原运行;配置变化后原结果不代表新配置已生效。写回仍走原recover的严格校验,不重新生成。'
|
|
175
|
+
}
|
|
176
|
+
if (type === 'video') {
|
|
177
|
+
if (!r.taskId)
|
|
178
|
+
return {
|
|
179
|
+
...out,
|
|
180
|
+
status: r.attempted ? 'submission_unknown' : 'not_submitted',
|
|
181
|
+
reason: '没有原接收task_id;不从任务列表自动认领'
|
|
182
|
+
}
|
|
183
|
+
const task = validateRepaintVideoTask(
|
|
184
|
+
r,
|
|
185
|
+
await client.raw(
|
|
186
|
+
jobPath(r.binding.job_id) +
|
|
187
|
+
'/redraw-tasks/' +
|
|
188
|
+
encodeURIComponent(r.taskId),
|
|
189
|
+
{ headers: { 'X-Client-Source': 'canvas' } },
|
|
190
|
+
{ retry: false }
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
const currentTask = text(
|
|
194
|
+
node.data?.last_task_id ||
|
|
195
|
+
node.data?.lastTaskId ||
|
|
196
|
+
node.data?.result?.task_id
|
|
197
|
+
)
|
|
198
|
+
return {
|
|
199
|
+
...out,
|
|
200
|
+
status: task.status,
|
|
201
|
+
original_run_verified: true,
|
|
202
|
+
result_available: task.status === 'completed' && !!task.result,
|
|
203
|
+
task_id: task.id,
|
|
204
|
+
task,
|
|
205
|
+
result: task.result || null,
|
|
206
|
+
billing: {
|
|
207
|
+
estimated_credit_cost: task.estimated_credit_cost,
|
|
208
|
+
credit_cost: task.credit_cost,
|
|
209
|
+
billing_status: task.billing_status
|
|
210
|
+
},
|
|
211
|
+
canvas_matches_original_result:
|
|
212
|
+
task.status === 'completed' &&
|
|
213
|
+
currentTask === String(task.id) &&
|
|
214
|
+
Boolean(r.savedView) &&
|
|
215
|
+
equal(repaintVideoNodeView(node), r.savedView),
|
|
216
|
+
download: '如需下载仍须原任务download权限接口;本命令不下载或请求文件地址'
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!r.accepted)
|
|
220
|
+
return {
|
|
221
|
+
...out,
|
|
222
|
+
status: r.attempted ? 'submission_unknown' : 'not_submitted',
|
|
223
|
+
reason: '没有原运行接收证据;当前job结果不归属于该operation'
|
|
224
|
+
}
|
|
225
|
+
if (type === 'delivery')
|
|
226
|
+
ctx.result =
|
|
227
|
+
(ctx.job.delivery_prompts || []).find(
|
|
228
|
+
(p) => p.segment_index === r.index
|
|
229
|
+
) || null
|
|
230
|
+
let state
|
|
231
|
+
try {
|
|
232
|
+
state =
|
|
233
|
+
type === 'reverse'
|
|
234
|
+
? validateReverseRun(r, ctx)
|
|
235
|
+
: validateDeliveryRun(r, ctx)
|
|
236
|
+
} catch {
|
|
237
|
+
return {
|
|
238
|
+
...out,
|
|
239
|
+
status: 'original_result_unavailable',
|
|
240
|
+
reason:
|
|
241
|
+
'服务端当前运行标记或结果与原记录不匹配;提示词接口仅提供当前轮,不能当作原历史结果或自动重做'
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const prompt = state.prompt?.prompt
|
|
245
|
+
return {
|
|
246
|
+
...out,
|
|
247
|
+
status: state.status,
|
|
248
|
+
original_run_verified: true,
|
|
249
|
+
billing: state.billing,
|
|
250
|
+
result_available: state.status === 'completed' && !!prompt,
|
|
251
|
+
result: state.status === 'completed' ? state.prompt : null,
|
|
252
|
+
canvas_matches_original_result:
|
|
253
|
+
state.status === 'completed' &&
|
|
254
|
+
text(node.data?.output || node.data?.prompt || node.data?.input) ===
|
|
255
|
+
text(prompt),
|
|
256
|
+
...(type === 'delivery'
|
|
257
|
+
? {
|
|
258
|
+
fallback: ctx.output.delivery_prompt_fallback === true,
|
|
259
|
+
warning: ctx.output.delivery_prompt_warning || null
|
|
260
|
+
}
|
|
261
|
+
: {}),
|
|
262
|
+
remote_error:
|
|
263
|
+
ctx.output[
|
|
264
|
+
type === 'reverse' ? 'reverse_prompt_error' : 'delivery_prompt_error'
|
|
265
|
+
] || null
|
|
266
|
+
}
|
|
267
|
+
}
|