@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.
Files changed (89) hide show
  1. package/.agents/skills/uzoo-anime-cli/SKILL.md +89 -0
  2. package/.agents/skills/uzoo-anime-cli/references/agreement-workflow.md +41 -0
  3. package/.agents/skills/uzoo-anime-cli/references/canvas-workflow.md +53 -0
  4. package/.agents/skills/uzoo-anime-cli/references/commands.md +242 -0
  5. package/.agents/skills/uzoo-anime-cli/references/generation-workflow.md +142 -0
  6. package/.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md +153 -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 +113 -0
  9. package/.agents/skills/uzoo-anime-cli/references/node-capabilities.md +64 -0
  10. package/.agents/skills/uzoo-anime-cli/references/node-workflow.md +90 -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 +13 -0
  13. package/.agents/skills/uzoo-anime-cli/references/seedance-review-workflow.md +37 -0
  14. package/.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md +67 -0
  15. package/CHANGELOG.md +13 -0
  16. package/LICENSE +21 -0
  17. package/README.md +46 -3
  18. package/assets/favicon-uzoo.png +0 -0
  19. package/assets/logo-uzoo.svg +7 -0
  20. 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
  21. package/examples/matting.json +5 -0
  22. package/extensions/uzoo-cli-login/manifest.json +9 -0
  23. package/extensions/uzoo-cli-login/popup.css +1 -0
  24. package/extensions/uzoo-cli-login/popup.html +14 -0
  25. package/extensions/uzoo-cli-login/popup.js +65 -0
  26. package/package.json +53 -6
  27. package/src/agreement-page.js +62 -0
  28. package/src/agreement.js +419 -0
  29. package/src/annotation-media.js +386 -0
  30. package/src/api.js +168 -0
  31. package/src/browser-login.js +13 -0
  32. package/src/callback-page.js +44 -0
  33. package/src/canvas.js +105 -0
  34. package/src/cli-authorization.js +122 -0
  35. package/src/cli.js +464 -0
  36. package/src/download-name.js +17 -0
  37. package/src/draft-finalize.js +229 -0
  38. package/src/generation-graph.js +124 -0
  39. package/src/generation-input.js +472 -0
  40. package/src/graph-disconnect.js +89 -0
  41. package/src/graph.js +431 -0
  42. package/src/group-edit.js +53 -0
  43. package/src/group-layout.js +113 -0
  44. package/src/group-membership.js +125 -0
  45. package/src/history-add.js +119 -0
  46. package/src/image-annotation.js +246 -0
  47. package/src/image-crop.js +283 -0
  48. package/src/image-matting.js +82 -0
  49. package/src/image-media.js +89 -0
  50. package/src/image-output.js +385 -0
  51. package/src/install.js +237 -0
  52. package/src/local-output.js +23 -0
  53. package/src/material-download.js +55 -0
  54. package/src/material-save.js +254 -0
  55. package/src/materials.js +147 -0
  56. package/src/matting-background.js +73 -0
  57. package/src/matting-jobs.js +63 -0
  58. package/src/matting-worker.js +49 -0
  59. package/src/matting.js +170 -0
  60. package/src/media-copy.js +53 -0
  61. package/src/media.js +343 -0
  62. package/src/model-options.js +76 -0
  63. package/src/multipart.js +28 -0
  64. package/src/node-copy.js +95 -0
  65. package/src/node-delete.js +49 -0
  66. package/src/node-history.js +231 -0
  67. package/src/normal-login.js +118 -0
  68. package/src/package-info.js +8 -0
  69. package/src/png-alpha.js +60 -0
  70. package/src/protocol.js +83 -0
  71. package/src/remake.js +738 -0
  72. package/src/repaint-archive.js +53 -0
  73. package/src/repaint-delivery.js +783 -0
  74. package/src/repaint-export.js +63 -0
  75. package/src/repaint-graph.js +214 -0
  76. package/src/repaint-history.js +267 -0
  77. package/src/repaint-materials.js +601 -0
  78. package/src/repaint-reverse.js +614 -0
  79. package/src/repaint-sequence.js +132 -0
  80. package/src/repaint-settings.js +576 -0
  81. package/src/repaint-video.js +798 -0
  82. package/src/repaint.js +326 -0
  83. package/src/resource.js +110 -0
  84. package/src/review.js +435 -0
  85. package/src/service.js +85 -0
  86. package/src/store.js +100 -0
  87. package/src/video-clip.js +381 -0
  88. package/src/video-frame.js +296 -0
  89. package/src/workflows.js +214 -0
package/src/canvas.js ADDED
@@ -0,0 +1,105 @@
1
+ import crypto from 'node:crypto'
2
+ import { segment } from './workflows.js'
3
+
4
+ // Public CLI names map to the persisted API types and roles used by the web canvas.
5
+ export const NODE_TYPES = ['text', 'image', 'video', 'audio', 'image_upload', 'video_upload', 'audio_upload', 'video-repaint']
6
+ function createNode(id, type) {
7
+ let apiType = type, data = {}
8
+ if (type.endsWith('_upload')) data = { mediaType: type.slice(0, -7), extra: { node_kind: 'asset' } }
9
+ if (type === 'video-repaint') {
10
+ apiType = 'video_repain_begain'
11
+ data = { extra: { repaint: { role: 'entry', splitMode: 'intelligent' } } }
12
+ }
13
+ return { id, type: apiType, position: { x: 120, y: 130 }, data }
14
+ }
15
+
16
+ const object = v => v !== null && typeof v === 'object' && !Array.isArray(v)
17
+ function isDerivedNode(node) {
18
+ const data = node.data || {}, extra = data.extra || {}
19
+ return Boolean(data.videoRemake || extra.video_remake || data.clipRedraw || extra.clip_redraw || data.draftFinalizeGeneration || extra.draft_finalize ||
20
+ [data.repaint, extra.repaint].some(marker => marker &&
21
+ (node.type !== 'video_repain_begain' || marker.role !== 'entry')))
22
+ }
23
+ const derivedNodeMessage = '派生节点必须由真实业务流程创建;node add/update 不能独立创建或将普通节点改为视频重拍、分段详情等派生节点'
24
+ function text(value, label, max = Infinity) {
25
+ if (typeof value !== 'string' || !value.trim() || value.length > max) throw new Error(`${label} 必须为非空字符串(最长 ${max})`)
26
+ return value
27
+ }
28
+ function position(value) {
29
+ if (!object(value) || !Number.isFinite(value.x) || !Number.isFinite(value.y)) throw new Error('position.x/y 必须为有限数字')
30
+ return value
31
+ }
32
+ function patchNode(node, patch) {
33
+ if (!object(patch)) throw new Error('节点输入必须是 JSON 对象')
34
+ for (const key of Object.keys(patch)) if (!['data', 'position', 'width', 'height'].includes(key)) throw new Error(`节点输入不支持 ${key};id/type 不可修改`)
35
+ const next = { ...node }
36
+ if (patch.position !== undefined) next.position = position(patch.position)
37
+ for (const key of ['width', 'height']) if (patch[key] !== undefined) {
38
+ if (!Number.isFinite(patch[key]) || patch[key] <= 0) throw new Error(`${key} 必须为正数`)
39
+ next[key] = patch[key]
40
+ }
41
+ if (patch.data !== undefined) {
42
+ if (!object(patch.data)) throw new Error('data 必须是 JSON 对象')
43
+ if (patch.data.extra !== undefined && !object(patch.data.extra)) throw new Error('data.extra 必须是 JSON 对象')
44
+ next.data = { ...node.data, ...patch.data,
45
+ ...(patch.data.extra ? { extra: { ...node.data.extra, ...patch.data.extra } } : {}) }
46
+ }
47
+ return next
48
+ }
49
+ export function spaceContext({ space, teamUid, projectId }) {
50
+ if (!['personal', 'team'].includes(space)) throw new Error('请先确认使用个人还是团队空间,并传 --space personal 或 --space team --team-uid UID')
51
+ if (space === 'personal' && (teamUid !== undefined || projectId !== undefined)) throw new Error('个人空间不能传 --team-uid/--project-id;团队空间请显式选择 --space team')
52
+ if (space === 'team' && (typeof teamUid !== 'string' || !/^\d{12}$/.test(teamUid))) throw new Error('团队空间必须传 --team-uid(账号返回的 12 位团队 UID)')
53
+ return space === 'team' ? { team_uid: teamUid } : {}
54
+ }
55
+ export async function createCanvas(client, { name, space, teamUid, projectId }) {
56
+ const context = spaceContext({ space, teamUid, projectId })
57
+ const body = { name: text(name, '--name', 200), viewport: { x: 0, y: 0, zoom: 1 }, nodes: [], edges: [], meta: {} }
58
+ Object.assign(body, context)
59
+ if (projectId !== undefined) {
60
+ if (!Number.isSafeInteger(projectId) || projectId < 1) throw new Error('--project-id 必须为正整数')
61
+ body.canvas_project_id = projectId
62
+ }
63
+ // Refresh expired credentials using a read before the non-retriable creation.
64
+ await client.raw('/auth/me')
65
+ // Creation has no server idempotency contract. Never automatically repeat it.
66
+ try { return await client.raw('/canvas/docs', { method: 'POST', body }, { retry: false }) }
67
+ catch (error) {
68
+ if ((!error.apiCode && !error.status && error.code !== 'API_ERROR') || error.status >= 500) throw new Error('创建画布结果未确认;先 canvas list 核对,勿直接重复创建', { cause: error })
69
+ throw error
70
+ }
71
+ }
72
+ export async function writeNode(client, { docId, nodeId, type, patch = {}, update = false, expectedSpace, expectedTeamUid, expectedVersion }) {
73
+ text(docId, '--doc')
74
+ if (!update && ['video-remake', 'video-split-detail'].includes(type)) throw new Error(derivedNodeMessage)
75
+ if (!update && !NODE_TYPES.includes(type)) throw new Error(`--type 必须为 ${NODE_TYPES.join('/')}`)
76
+ const id = nodeId === undefined && !update ? `${type}-${crypto.randomUUID()}` : text(nodeId, '--node-id')
77
+ // Validate local input before reading or writing the document.
78
+ const candidate = patchNode(update ? { id, data: {} } : createNode(id, type), patch)
79
+ if (!update && isDerivedNode(candidate)) throw new Error(derivedNodeMessage)
80
+ const endpoint = `/canvas/docs/${segment(docId)}`
81
+ const doc = await client.raw(endpoint)
82
+ if(expectedVersion !== undefined && doc.version!==expectedVersion) throw new Error('画布已变化;先读取核对,再恢复原任务')
83
+ if (expectedSpace !== undefined && String(doc.team_uid || '') !== String(expectedSpace === 'team' ? expectedTeamUid : '')) throw new Error('画布归属与确认空间不一致')
84
+ if (doc.id !== docId || !Number.isSafeInteger(doc.version) || doc.version < 1 || !Array.isArray(doc.nodes) || !Array.isArray(doc.edges) || !object(doc.viewport) || !object(doc.meta) || typeof doc.name !== 'string') throw new Error('画布快照不完整,停止保存以避免覆盖')
85
+ if (doc.role && !['owner', 'editor'].includes(doc.role)) throw new Error('画布仅可读,不能写入节点')
86
+ const matches = doc.nodes.filter(n => n.id === id)
87
+ if (matches.length > 1) throw new Error('画布已有重复节点 id,停止保存')
88
+ if (update && !matches.length) throw new Error('节点不存在')
89
+ if (!update && matches.length) throw new Error('节点 id 已存在;请读取节点确认,勿重复创建')
90
+ const node = update ? patchNode(matches[0], patch) : candidate
91
+ if (update && !isDerivedNode(matches[0]) && isDerivedNode(node)) throw new Error(derivedNodeMessage)
92
+ const nodes = update ? doc.nodes.map(n => n.id === id ? node : n) : [...doc.nodes, node]
93
+ const body = { version: doc.version, name: doc.name, viewport: doc.viewport, nodes, edges: doc.edges, meta: doc.meta }
94
+ // Backend CAS protects concurrent website edits. Do not reload and overwrite on conflict.
95
+ let saved
96
+ try { saved = await client.raw(endpoint, { method: 'PUT', body }, { retry: false }) }
97
+ catch (error) {
98
+ if (error.apiCode === 3002 || error.status === 409) error.message = '画布版本冲突,未自动覆盖;先 canvas get 检查最新内容,再决定是否重新操作'
99
+ else if ((!error.apiCode && !error.status && error.code !== 'API_ERROR') || error.status >= 500) throw new Error(`节点保存结果未确认;先 canvas get ${docId} 核对节点 ${id},勿直接重复创建`, { cause: error })
100
+ throw error
101
+ }
102
+ const savedNode = saved?.nodes?.find(n => n.id === id)
103
+ if (!savedNode || !Number.isSafeInteger(saved.version) || saved.version <= doc.version) throw new Error(`节点保存响应不完整;先 canvas get ${docId} 核对节点 ${id}`)
104
+ return { doc_id: docId, node_id: id, version: saved.version, node: savedNode }
105
+ }
@@ -0,0 +1,122 @@
1
+ import crypto from 'node:crypto'
2
+ import http from 'node:http'
3
+ import os from 'node:os'
4
+ import { baseUrl, cancellation, request } from './api.js'
5
+ import { unwrap } from './protocol.js'
6
+ import { openLoginBrowser } from './normal-login.js'
7
+ import { callbackPage } from './callback-page.js'
8
+
9
+ export const CLIENT_ID = 'uzoo-anime-cli'
10
+ const b64 = value => Buffer.from(value).toString('base64url')
11
+
12
+ export async function e1Request(api, endpoint, payload, timeout = 30000) {
13
+ const root = baseUrl(api)
14
+ const publicResponse = await request(`${root}/auth/public-key`, {}, timeout)
15
+ const key = unwrap(await publicResponse.json())
16
+ if (!publicResponse.ok || !key?.pkid || !key?.public_key) throw new Error('无法获取授权加密公钥')
17
+ const url = new URL(`${root}${endpoint}`)
18
+ const requestKey = crypto.randomBytes(32)
19
+ const enc = Buffer.from(crypto.hkdfSync('sha256', requestKey, Buffer.alloc(0), Buffer.from('ENC'), 32))
20
+ const mac = Buffer.from(crypto.hkdfSync('sha256', requestKey, Buffer.alloc(0), Buffer.from('MAC'), 32))
21
+ const wrapped = crypto.publicEncrypt({ key: key.public_key, oaepHash: 'sha256', padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, requestKey)
22
+ const t = String(Date.now()), k = `E1.${b64(wrapped)}`
23
+ const aad = Buffer.from(['POST', url.pathname, t, k].join('\n'))
24
+ const nonce = crypto.randomBytes(12)
25
+ const cipher = crypto.createCipheriv('aes-256-gcm', enc, nonce)
26
+ cipher.setAAD(aad)
27
+ const encrypted = Buffer.concat([cipher.update(JSON.stringify(payload), 'utf8'), cipher.final(), cipher.getAuthTag()])
28
+ const body = JSON.stringify({ pkid: key.pkid, data: b64(Buffer.concat([nonce, encrypted])) })
29
+ const bodyHash = crypto.createHash('sha256').update(body).digest('hex')
30
+ const canonical = ['POST', url.pathname, '', bodyHash, t, k].join('\n')
31
+ const response = await request(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-T': t, 'X-K': k, 'X-Sign': crypto.createHmac('sha256', mac).update(canonical).digest('base64url'), 'X-Device-Id': payload.device_id }, body }, timeout)
32
+ const result = await response.json().catch(() => null)
33
+ if (!result) throw new Error(`授权服务请求失败(HTTP ${response.status})`)
34
+ try {
35
+ const data = unwrap(result)
36
+ if (!response.ok) throw new Error(`授权服务请求失败(HTTP ${response.status})`)
37
+ return data
38
+ } catch (error) {
39
+ const messages = { 2010: 'CLI 令牌已失效,请重新运行 auth login', 2011: 'CLI 会话已过期,请重新运行 auth login', 2012: 'CLI 会话已被新的 CLI 登录替换,请重新运行 auth login' }
40
+ const symbol = error.code === 2110 && /^[a-z_]{1,64}$/.test(result?.data?.error || '') ? result.data.error : ''
41
+ const e = new Error(messages[error.code] || `授权服务拒绝请求(code ${error.code || 'unknown'}${symbol ? ',' + symbol : ''})`)
42
+ e.code = error.code
43
+ if (symbol) e.protocolError = symbol
44
+ throw e
45
+ }
46
+ }
47
+
48
+ export async function startCallback({ state, timeout = 300000, signal = cancellation.signal, waitForCompletion = false }) {
49
+ const path = `/cli/callback/${b64(crypto.randomBytes(24))}`
50
+ let resolveWait, rejectWait, settled = false
51
+ let pendingResponse, language = ''
52
+ function sendPage(res, status) {
53
+ if (res.destroyed || res.writableEnded) return Promise.resolve()
54
+ const page = callbackPage(status, language)
55
+ return new Promise(resolve => {
56
+ res.once('finish', resolve)
57
+ res.once('close', resolve)
58
+ res.writeHead(200, page.headers).end(page.html)
59
+ })
60
+ }
61
+ async function complete(status) {
62
+ if (!pendingResponse) return
63
+ const res = pendingResponse
64
+ pendingResponse = null
65
+ await sendPage(res, status)
66
+ }
67
+ const wait = new Promise((resolve, reject) => { resolveWait = resolve; rejectWait = reject })
68
+ // A timeout can occur while authorization creation is still in flight.
69
+ wait.catch(() => {})
70
+ const server = http.createServer((req, res) => {
71
+ let url
72
+ try { url = new URL(req.url, 'http://127.0.0.1') } catch { res.writeHead(400).end(); return }
73
+ if (req.method !== 'GET' || req.headers.host !== `127.0.0.1:${server.address().port}` || url.pathname !== path) { res.writeHead(404).end(); return }
74
+ const states = url.searchParams.getAll('state'), codes = url.searchParams.getAll('code'), errors = url.searchParams.getAll('error')
75
+ if ([...url.searchParams.keys()].some(key => !['state', 'code', 'error'].includes(key)) || states.length !== 1 || states[0] !== state || codes.length + errors.length !== 1 || codes.length > 1 || errors.length > 1 || (codes.length && !/^[A-Za-z0-9_-]{43}$/.test(codes[0])) || (errors.length && errors[0] !== 'access_denied')) { res.writeHead(400).end(); return }
76
+ if (settled) { res.writeHead(409).end(); return }
77
+ settled = true
78
+ language = req.headers['accept-language'] || ''
79
+ if (waitForCompletion && !errors.length) pendingResponse = res
80
+ else void sendPage(res, errors.length ? 'denied' : 'received')
81
+ if (errors.length) rejectWait(new Error('用户取消了 CLI 授权'))
82
+ else resolveWait(codes[0])
83
+ })
84
+ server.requestTimeout = 10000
85
+ server.headersTimeout = 10000
86
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve) })
87
+ const abort = () => { if (!settled) { settled = true; rejectWait(new Error('授权已取消')) } }
88
+ signal.addEventListener('abort', abort, { once: true })
89
+ if (signal.aborted) abort()
90
+ const timer = setTimeout(() => { if (!settled) { settled = true; rejectWait(new Error('等待网页授权超时,请重新登录')) } }, timeout)
91
+ return { redirectUri: `http://127.0.0.1:${server.address().port}${path}`, wait, complete, close: async () => { clearTimeout(timer); signal.removeEventListener('abort', abort); await complete('failure'); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)) } }
92
+ }
93
+
94
+ export async function browserAuthorization({ profile, store, webUrl, browser = 'default', timeout = 300000, requestTimeout = 30000, openUrl = openLoginBrowser, onReady = () => {}, onOpenError = () => {} }) {
95
+ if (!['default', 'msedge', 'chrome'].includes(browser)) throw new Error('--browser 必须是 default、msedge 或 chrome')
96
+ const api = baseUrl(profile.baseUrl)
97
+ const website = new URL(webUrl || new URL(api).origin)
98
+ if (website.username || website.password || website.search || website.hash || website.pathname !== '/') throw new Error('网页登录地址必须是网站根地址')
99
+ if (website.protocol !== 'https:' && !(website.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(website.hostname))) throw new Error('远程网页登录必须使用 HTTPS')
100
+ const deviceId = profile.deviceId || `cli-${crypto.randomUUID()}`
101
+ const state = b64(crypto.randomBytes(32)), verifier = b64(crypto.randomBytes(48))
102
+ const bridge = await startCallback({ state, timeout, waitForCompletion: true })
103
+ try {
104
+ const authorization = await e1Request(api, '/auth/cli/authorizations', { client_id: CLIENT_ID, request_id: crypto.randomUUID(), redirect_uri: bridge.redirectUri, state, code_challenge: b64(crypto.createHash('sha256').update(verifier).digest()), code_challenge_method: 'S256', device_id: deviceId, device_name: (os.hostname() || 'CLI').replace(/[\x00-\x1f\x7f]/g, '').slice(0, 100) || 'CLI', session_policy: 'web_cli_independent' }, requestTimeout)
105
+ const target = new URL(authorization.authorize_url)
106
+ if (target.origin !== website.origin) throw new Error(`授权页面地址与配置的网站不一致:服务端 origin=${target.origin},当前网站 origin=${website.origin};请核对部署的 CLI_AUTH_ORIGIN,不会转发凭据或自动切换服务`)
107
+ if (target.pathname !== '/auth/login' || target.searchParams.get('request_id') !== authorization.authorization_id || !/^car_[A-Za-z0-9_-]{43}$/.test(authorization.authorization_id)) throw new Error('授权页面地址与配置的网站不一致')
108
+ onReady(target.href)
109
+ try { await openUrl(target.href, browser) } catch (error) { onOpenError(error.message) }
110
+ const code = await bridge.wait
111
+ const grant = { grant_type: 'authorization_code', client_id: CLIENT_ID, authorization_id: authorization.authorization_id, code, code_verifier: verifier, redirect_uri: bridge.redirectUri, device_id: deviceId, exchange_id: crypto.randomUUID() }
112
+ let pair
113
+ try { pair = await e1Request(api, '/auth/cli/token', grant, requestTimeout) } catch (error) {
114
+ if (error.code) throw error
115
+ pair = await e1Request(api, '/auth/cli/token', grant, requestTimeout)
116
+ }
117
+ if (!pair.access_token || !pair.refresh_token || pair.session?.client_type !== 'cli' || pair.session?.device_id !== deviceId) throw new Error('授权令牌响应无效')
118
+ await store.save({ baseUrl: api, deviceId, accessToken: pair.access_token, refreshToken: pair.refresh_token, authMode: 'cli' })
119
+ await bridge.complete('success')
120
+ return { profile: store.name, loggedIn: true, mode: 'cli', cliSessionReplaced: Boolean(pair.cli_session_replaced) }
121
+ } finally { await bridge.close() }
122
+ }