@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
@@ -0,0 +1,65 @@
1
+ const SITE = 'https://uzooai.com'
2
+ const $ = id => document.getElementById(id)
3
+ const button = $('action')
4
+ let action
5
+
6
+ function bridgeFromUrl(value) {
7
+ const url = new URL(value)
8
+ const token = url.hash.slice(1)
9
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password || url.pathname !== '/connect' || !/^[A-Za-z0-9_-]{43}$/.test(token)) return null
10
+ return { callback: url.origin, token }
11
+ }
12
+ async function bridgeFetch(pairing, endpoint, body) {
13
+ const response = await fetch(`${pairing.callback}${endpoint}`, {
14
+ method: body === undefined ? 'GET' : 'POST',
15
+ headers: { 'X-Uzoo-Bridge': pairing.token, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
16
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
17
+ redirect: 'error', credentials: 'omit', mode: 'cors', signal: AbortSignal.timeout(10000),
18
+ })
19
+ if (!response.ok) { const data = await response.json().catch(() => ({})); throw new Error(`CLI 连接失败(${data.error || response.status});请重新运行 auth login 并连接`) }
20
+ return response.json()
21
+ }
22
+
23
+ async function init() {
24
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
25
+ if (!tab?.id || !tab.url) throw new Error('无法读取当前标签页,请在 CLI 连接页面或 Uzoo 网站打开扩展')
26
+ const pairing = bridgeFromUrl(tab.url)
27
+ if (pairing) {
28
+ $('description').textContent = '连接当前本机 CLI,然后在正常网站完成登录。'
29
+ button.textContent = '连接 CLI 并打开登录页'
30
+ action = async () => {
31
+ const config = await bridgeFetch(pairing, '/config', {})
32
+ const api = new URL(config.api), login = new URL(config.loginUrl)
33
+ if (config.origin !== SITE || api.origin !== SITE || api.pathname !== '/api/v1' || api.search || api.hash || login.origin !== SITE || login.username || login.password) throw new Error('扩展只允许授权 uzooai.com;CLI 的网站配置不匹配')
34
+ const redirect = login.searchParams.get('redirect')
35
+ if (redirect && new URL(redirect, SITE).origin !== SITE) throw new Error('登录跳转地址不受支持')
36
+ await chrome.storage.session.set({ pairing: { ...pairing, ...config, tabId: tab.id } })
37
+ await chrome.tabs.update(tab.id, { url: login.href })
38
+ $('status').textContent = '请正常登录,进入工作区后再点击本扩展授权。'
39
+ }
40
+ } else {
41
+ const pending = (await chrome.storage.session.get('pairing')).pairing
42
+ if (!pending) throw new Error('尚未连接 CLI。先运行 auth login,再在打开的本机连接页面点击扩展')
43
+ if (new URL(tab.url).origin !== SITE || tab.id !== pending.tabId) throw new Error('请在刚才由 CLI 打开的 Uzoo 标签页完成登录并授权')
44
+ $('description').textContent = '请先完成网站登录。点击下方按钮会将当前账号的网页会话(含刷新 Cookie)交给已连接的本机 CLI。'
45
+ button.textContent = '授权当前会话给 CLI'
46
+ action = async () => {
47
+ const results = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => ({ origin: location.origin, accessToken: localStorage.getItem('access_token') || '', deviceId: localStorage.getItem('sign_v2_device_id') || '' }) })
48
+ const value = results[0]?.result
49
+ if (value?.origin !== SITE || !value.accessToken) throw new Error('当前页面尚未登录,请完成登录后再授权')
50
+ const cookies = await chrome.cookies.getAll({ url: `${pending.api}/auth/refresh` })
51
+ const jar = new Map()
52
+ for (const cookie of cookies.sort((a,b) => b.path.length-a.path.length)) if (!jar.has(cookie.name)) jar.set(cookie.name,cookie.value)
53
+ value.cookie = [...jar].map(([name, data]) => `${name}=${data}`).join('; ')
54
+ await bridgeFetch(pending, '/session', value)
55
+ await chrome.storage.session.remove('pairing')
56
+ $('status').textContent = '会话已交给本机 CLI,请查看终端中的身份验证结果。请关闭使用该会话的网页,避免与 CLI 同时刷新。建议使用独立的普通浏览器用户配置文件。'
57
+ }
58
+ }
59
+ button.disabled = false
60
+ }
61
+ button.addEventListener('click', async () => {
62
+ button.disabled = true; $('status').textContent = '处理中…'
63
+ try { await action() } catch (error) { $('status').textContent = error.message; button.disabled = false }
64
+ })
65
+ init().catch(error => { $('description').textContent = error.message; button.textContent = '请按提示操作' })
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
- {
2
- "name": "@uzooai/anime-ai-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@uzooai/anime-ai-cli",
3
+ "version": "0.6.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "bin": {
10
+ "uzoo-anime": "src/cli.js"
11
+ },
12
+ "scripts": {
13
+ "build": "npm run check && npm run check:resources",
14
+ "pack:local": "node scripts/pack-local.mjs",
15
+ "prepack": "npm run build",
16
+ "start": "node src/cli.js",
17
+ "check": "node --check src/cli.js && node --check src/install.js && node --check src/api.js && node --check src/canvas.js && node --check src/draft-finalize.js && node --check src/media-copy.js && node --check src/group-edit.js && node --check src/group-layout.js && node --check src/node-copy.js && node --check src/node-delete.js && node --check src/materials.js && node --check src/material-save.js && node --check src/service.js && node --check src/video-clip.js && node --check src/media.js && node --check src/image-media.js && node --check src/image-output.js && node --check src/image-crop.js && node --check src/image-annotation.js && node --check src/image-matting.js && node --check src/matting.js && node --check src/png-alpha.js && node --check src/annotation-media.js && node --check src/video-frame.js && node --check src/graph.js && node --check src/graph-disconnect.js && node --check src/group-membership.js && node --check src/model-options.js && node --check src/generation-input.js && node --check src/review.js && node --check src/resource.js && node --check src/generation-graph.js && node --check src/repaint.js && node --check src/repaint-reverse.js && node --check src/repaint-delivery.js && node --check src/repaint-video.js && node --check src/repaint-history.js && node --check src/multipart.js && node --check src/repaint-materials.js && node --check src/repaint-settings.js && node --check src/remake.js && node --check src/repaint-graph.js && node --check src/store.js && node --check src/workflows.js && node --check src/agreement.js && node --check src/agreement-page.js && node --check src/protocol.js && node --check src/cli-authorization.js && node --check src/browser-login.js && node --check src/normal-login.js && node --check extensions/uzoo-cli-login/popup.js && node --check src/local-output.js && node --check src/repaint-archive.js && node --check src/repaint-export.js && node --check src/repaint-sequence.js && node --check src/node-history.js && node --check src/history-add.js && node --check src/material-download.js && node --check src/download-name.js && node --check scripts/pack-local.mjs && node --check src/package-info.js && node --check src/matting-background.js && node --check src/matting-worker.js && node --check src/matting-jobs.js && node --check scripts/release-policy.mjs && node --check scripts/check-release.mjs",
18
+ "test": "node test/smoke.mjs && node test/install.mjs && node test/atomic-file.mjs && node test/phase1.mjs && node test/agreement.mjs && node test/canvas.mjs && node test/material-graph.mjs && node test/graph-rename.mjs && node test/graph-ungroup.mjs && node test/graph-disconnect.mjs && node test/node-copy.mjs && node test/node-copy-batch.mjs && node test/draft-finalize.mjs && node test/media-copy.mjs && node test/group-edit.mjs && node test/node-delete.mjs && node test/material-save.mjs && node test/service.mjs && node test/video-clip.mjs && node test/video-frame.mjs && node test/image-crop.mjs && node test/image-annotation.mjs && node test/image-matting.mjs && node test/matting.mjs && node test/matting-background.mjs && node test/matting-jobs.mjs && node test/model-options.mjs && node test/generation-input.mjs && node test/review.mjs && node test/repaint.mjs && node test/repaint-reverse.mjs && node test/repaint-delivery.mjs && node test/repaint-video.mjs && node test/repaint-settings.mjs && node test/repaint-materials.mjs && node test/remake.mjs && node test/normal-login.mjs && node test/login-extension.mjs && node test/browser-login.mjs && node test/cli-authorization.mjs && node test/store-recovery.mjs && node test/store-concurrency.mjs && node test/repaint-archive.mjs && node test/node-history.mjs && node test/file-transfer.mjs && node test/material-download.mjs && node test/release-policy.mjs",
19
+ "test:browser": "node test/normal-login.mjs && node test/login-extension.mjs && node test/browser-login.mjs",
20
+ "test:media": "node test/media.mjs && node test/image-media.mjs && node test/annotation-media.mjs",
21
+ "check:resources": "node scripts/check-package-resources.mjs",
22
+ "test:package": "node scripts/verify-package.mjs",
23
+ "check:release": "npm run build && node scripts/check-release.mjs",
24
+ "prepublishOnly": "npm run check:release"
25
+ },
26
+ "files": [
27
+ "src/",
28
+ "assets/logo-uzoo.svg",
29
+ "assets/favicon-uzoo.png",
30
+ "!assets/README.md",
31
+ ".agents/skills/uzoo-anime-cli/",
32
+ "extensions/uzoo-cli-login/",
33
+ "examples/matting.json",
34
+ "docs/Agent接入与版本更新.md",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ "LICENSE"
38
+ ],
39
+ "description": "Uzoo Anime CLI for canvas, media references and authorized generation workflows",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/UzooAI/uzooai-cli.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/UzooAI/uzooai-cli/issues"
46
+ },
47
+ "license": "MIT",
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "registry": "https://registry.npmjs.org/",
51
+ "tag": "latest"
52
+ }
53
+ }
@@ -0,0 +1,62 @@
1
+ const escape = (value) =>
2
+ String(value ?? '').replace(
3
+ /[&<>"']/g,
4
+ (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]
5
+ )
6
+
7
+ // Theme only the isolated display copy; document.html and its consent digest stay original.
8
+ function darkDocument(html) {
9
+ const style = `<style data-uzoo-agreement-theme>
10
+ :root{color-scheme:dark}html,body{background:#202020!important;color:#ededed!important}body{padding:16px;font:15px/1.8 system-ui,"Microsoft YaHei",sans-serif;overflow-wrap:anywhere}body *{color:#ededed!important;background-color:transparent!important;background-image:none!important;border-color:#444!important}a,a *{color:#8bc8ff!important}::selection{background:#38566c;color:#fff}
11
+ </style>`
12
+ return html + style
13
+ }
14
+
15
+ // Remote published HTML is isolated in an opaque, scriptless sandbox. Only the
16
+ // local parent page can send a human decision; no API credentials enter HTML.
17
+ export function agreementPage({
18
+ protocol,
19
+ document,
20
+ account,
21
+ actionPath,
22
+ csrf,
23
+ nonce,
24
+ continuation
25
+ }) {
26
+ const script = `const box=document.getElementById('accepted'),yes=document.getElementById('agree'),no=document.getElementById('decline'),status=document.getElementById('status');
27
+ let busy=false,finished=false,timer;
28
+ box.addEventListener('change',()=>yes.disabled=!box.checked||busy||finished);
29
+ function finish(message){finished=true;box.disabled=true;yes.disabled=true;no.disabled=true;status.textContent=message;clearInterval(timer);}
30
+ function receive(r,d){
31
+ if(finished)return;
32
+ if(r.ok&&(d.status==='agreed'||d.status==='not_required')){
33
+ finish(d.status==='agreed'?'协议状态已核验并同步 CLI,可关闭此页。':'当前无需签署协议,状态已同步 CLI,可关闭此页。');
34
+ try{window.close()}catch{}
35
+ }else if(d.status==='declined'||d.status==='changed'||d.status==='cancelled'||d.status==='rejected')finish(d.message);
36
+ else if(d.message)status.textContent=d.message;
37
+ }
38
+ async function decision(agreed){
39
+ if(busy||finished||agreed&&!box.checked)return;
40
+ busy=true;yes.disabled=true;no.disabled=true;
41
+ status.textContent=agreed?'正在核验并记录协议确认…':'已选择不同意,正在结束本次确认…';
42
+ try{
43
+ const r=await fetch(${JSON.stringify(actionPath)},{method:'POST',headers:{'Content-Type':'application/json','X-Uzoo-Agreement':${JSON.stringify(csrf)}},body:JSON.stringify({agreed})});
44
+ receive(r,await r.json());
45
+ }catch{if(!finished)status.textContent='确认结果尚未核实。CLI 将继续只读查询,请勿重复签署。';}
46
+ finally{busy=false;}
47
+ }
48
+ yes.addEventListener('click',()=>decision(true));no.addEventListener('click',()=>decision(false));
49
+ timer=setInterval(async()=>{
50
+ if(finished||busy)return;
51
+ try{const r=await fetch(${JSON.stringify(actionPath.replace('/decision', '/status'))});receive(r,await r.json())}catch{}
52
+ },2500);`
53
+ const resume = continuation
54
+ ? `<aside>确认后 CLI 将重新核验原请求和费用,再继续本次任务:${escape(continuation.operation)} · ${escape(continuation.model)} · 本次已确认 ${escape(continuation.cost)} OO。参数或费用变化时会停止。</aside>`
55
+ : '<aside>本页只记录协议确认,不提交生成,也不扣除生成费用。</aside>'
56
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Uzoo · Seedance 协议确认</title><style>
57
+ *{box-sizing:border-box}html{color-scheme:dark}body{margin:0;background:#111;color:#ededed;font:15px/1.65 system-ui,"Microsoft YaHei",sans-serif;padding:28px}main{max-width:980px;margin:auto;border:1px solid #444;border-radius:12px;background:#1c1c1c;overflow:hidden}header,section,footer{padding:20px 28px}header{border-bottom:1px solid #444}h1{font-size:23px;margin:0}h2{font-size:17px;margin:8px 0}.meta,aside{color:#b9b9b9}.meta{margin-top:10px}aside{background:#252525;padding:12px;border-radius:6px;margin:14px 0}iframe{width:100%;height:45vh;min-height:260px;border:1px solid #444;border-radius:6px;background:#202020}label{display:flex;gap:10px;margin:20px 0;align-items:flex-start}input{width:20px;height:20px;flex-shrink:0;margin-top:3px;accent-color:#2e9fe6}button{border:0;border-radius:6px;padding:11px 20px;font:inherit;cursor:pointer}#agree{background:#2e9fe6;color:white}#decline{background:#333;color:white}button:disabled{background:#444!important;color:#999!important;cursor:not-allowed}footer{display:flex;justify-content:flex-end;gap:12px;border-top:1px solid #444}#status{min-height:26px;color:#b9b9b9}a{color:#8bc8ff}</style></head>
58
+ <body><main><header><h1>《统一协议》</h1><h2>《${escape(document.title)}》</h2><div class="meta">协议版本:${escape(protocol.version || '当前版本')} · 当前 CLI 账号:${escape(account.name)}(${escape(account.uid)})<br>服务:${escape(account.origin)}</div></header><section>
59
+ <p>${escape(protocol.message)}</p>${resume}<p>请阅读下方当前已发布的完整协议,由你本人决定是否同意。</p><iframe title="Seedance 模型使用承诺书正文" sandbox="" referrerpolicy="no-referrer" srcdoc="${escape(darkDocument(document.html))}"></iframe>
60
+ <label><input id="accepted" type="checkbox"><span>我已阅读并同意上述《${escape(document.title)}》(${escape(protocol.version || '当前版本')})。</span></label><p id="status" role="status" aria-live="polite">尚未确认。首次使用或协议版本更新后需要重新确认。</p></section>
61
+ <footer><button id="decline" type="button">不同意</button><button id="agree" type="button" disabled>同意并继续</button></footer></main><script nonce="${escape(nonce)}">${script}</script></body></html>`
62
+ }
@@ -0,0 +1,419 @@
1
+ import crypto from 'node:crypto'
2
+ import http from 'node:http'
3
+ import { request, cancellation } from './api.js'
4
+ import { unwrap } from './protocol.js'
5
+ import { openLoginBrowser } from './normal-login.js'
6
+ import { agreementPage } from './agreement-page.js'
7
+
8
+ export const AGREEMENT_LABEL = '《统一协议》'
9
+ export const AGREEMENT_TITLE = '《Seedance2.0模型使用承诺书》'
10
+ export function agreementError(code, message, protocol) {
11
+ const error = new Error(message)
12
+ Object.assign(error, { code, apiCode: 4401, status: 'awaiting_agreement', protocol })
13
+ return error
14
+ }
15
+ export function validateProtocol(value) {
16
+ if (
17
+ !value ||
18
+ typeof value.required !== 'boolean' ||
19
+ typeof value.agreed !== 'boolean' ||
20
+ typeof value.version !== 'string'
21
+ )
22
+ throw new Error('协议状态响应不完整,停止继续生成')
23
+ return {
24
+ required: value.required,
25
+ agreed: value.agreed,
26
+ version: value.version,
27
+ label: AGREEMENT_LABEL,
28
+ document_title: AGREEMENT_TITLE,
29
+ message: String(value.message || '请阅读并确认协议后继续').slice(0, 2000)
30
+ }
31
+ }
32
+ export async function readProtocol(client) {
33
+ return validateProtocol((await client.raw('/video/protocol-agreement')).protocol)
34
+ }
35
+ async function publicText(url, timeout) {
36
+ const response = await request(
37
+ url,
38
+ { headers: { Accept: 'text/html, application/json' } },
39
+ timeout
40
+ )
41
+ if (!response.ok) {
42
+ await response.body?.cancel()
43
+ throw new Error('协议正文读取失败')
44
+ }
45
+ const chunks = []
46
+ let size = 0
47
+ for await (const chunk of response.body) {
48
+ size += chunk.length
49
+ if (size > 2 * 1024 * 1024) {
50
+ await response.body.cancel().catch(() => {})
51
+ throw new Error('协议正文超过读取限制')
52
+ }
53
+ chunks.push(chunk)
54
+ }
55
+ return {
56
+ text: Buffer.concat(chunks).toString('utf8'),
57
+ mime: response.headers.get('content-type') || ''
58
+ }
59
+ }
60
+ export async function readAgreementDocument(client) {
61
+ const base = new URL(client.baseUrl)
62
+ const list = await publicText(client.baseUrl + '/content', client.timeout)
63
+ const items = unwrap(JSON.parse(list.text)).items
64
+ if (!Array.isArray(items)) throw new Error('已发布协议目录无效')
65
+ const matches = items.filter(
66
+ (i) => i.type === 'terms_of_service' && /seedance/i.test(i.title || '')
67
+ )
68
+ const exact = matches.filter(
69
+ (i) => String(i.title).replace(/\s/g, '') === 'Seedance2.0模型使用承诺书'
70
+ )
71
+ const candidates = exact.length ? exact : matches
72
+ if (candidates.length !== 1) throw new Error('未找到唯一的 Seedance 模型使用承诺书,停止签署')
73
+ const item = candidates[0]
74
+ let html = typeof item.content === 'string' ? item.content : ''
75
+ let source = client.baseUrl + '/content'
76
+ if (!html.trim()) {
77
+ const url = new URL(item.content_url, client.baseUrl + '/')
78
+ const prefix = base.pathname.replace(/\/$/, '')
79
+ if (
80
+ url.origin !== base.origin ||
81
+ url.username ||
82
+ url.password ||
83
+ url.hash ||
84
+ !new RegExp(
85
+ '^' + prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/content/[0-9]+/body$'
86
+ ).test(url.pathname) ||
87
+ url.searchParams.get('kind') !== 'doc' ||
88
+ [...url.searchParams.keys()].some((k) => k !== 'kind')
89
+ )
90
+ throw new Error('协议正文地址不属于当前服务的公开内容接口')
91
+ source = url.href
92
+ const content = await publicText(source, client.timeout)
93
+ if (!/text\/html/i.test(content.mime)) throw new Error('协议正文不是 HTML')
94
+ html = content.text
95
+ }
96
+ if (!html.trim() || html.length > 2 * 1024 * 1024) throw new Error('协议正文为空或过大,不能确认')
97
+ return {
98
+ id: item.id,
99
+ title: String(item.title),
100
+ version: item.version,
101
+ source,
102
+ html,
103
+ digest: crypto
104
+ .createHash('sha256')
105
+ .update(JSON.stringify([item.id, item.title, item.version, html]))
106
+ .digest('hex')
107
+ }
108
+ }
109
+ const identity = (value) => {
110
+ const uid = String(value.uid ?? value.id ?? '')
111
+ if (!uid) throw new Error('无法核对协议签署账号')
112
+ return { uid, name: String(value.name || value.username || uid) }
113
+ }
114
+
115
+ export async function startAgreementBridge({
116
+ client,
117
+ protocol,
118
+ document,
119
+ account,
120
+ continuation,
121
+ timeout = 300000,
122
+ signal = cancellation.signal
123
+ }) {
124
+ const token = crypto.randomBytes(32).toString('base64url'),
125
+ csrf = crypto.randomBytes(32).toString('base64url'),
126
+ nonce = crypto.randomBytes(24).toString('base64url')
127
+ const pathname = '/agreement/' + token
128
+ let origin,
129
+ closed = false,
130
+ settled = false,
131
+ processing = false,
132
+ decisionSent = false,
133
+ timer,
134
+ resolveWait,
135
+ rejectWait
136
+ const wait = new Promise((resolve, reject) => {
137
+ resolveWait = resolve
138
+ rejectWait = reject
139
+ })
140
+ void wait.catch(() => {})
141
+ function fail(code, message, diagnostic) {
142
+ if (!settled) {
143
+ settled = true
144
+ const error=agreementError(code, message, protocol)
145
+ if(diagnostic){error.diagnostic=diagnostic;error.apiCode=diagnostic.api_code;error.requestId=diagnostic.request_id}
146
+ rejectWait(error)
147
+ }
148
+ }
149
+ function agreed(state) {
150
+ if (!settled) {
151
+ settled = true
152
+ resolveWait({
153
+ status: state.agreed ? 'agreed' : 'not_required',
154
+ protocol: state,
155
+ account: { uid: account.uid, name: account.name }
156
+ })
157
+ }
158
+ }
159
+ async function checkAccount() {
160
+ if (identity(await client.raw('/auth/me')).uid !== account.uid)
161
+ throw agreementError(
162
+ 'AGREEMENT_ACCOUNT_CHANGED',
163
+ 'CLI 账号已变化,未继续签署或生成',
164
+ protocol
165
+ )
166
+ }
167
+ async function freshState() {
168
+ await checkAccount()
169
+ const state = await readProtocol(client)
170
+ if (state.version !== protocol.version)
171
+ throw agreementError(
172
+ 'AGREEMENT_CHANGED',
173
+ '协议版本已更新,请重新打开并阅读当前版本后确认',
174
+ state
175
+ )
176
+ return state
177
+ }
178
+ async function freshDocument() {
179
+ const latest = await readAgreementDocument(client)
180
+ if (latest.digest !== document.digest)
181
+ throw agreementError('AGREEMENT_CHANGED', '协议正文已变化,请重新打开并阅读后确认', protocol)
182
+ }
183
+ const server = http.createServer(async (req, res) => {
184
+ res.setHeader('Cache-Control', 'no-store')
185
+ res.setHeader('Referrer-Policy', 'no-referrer')
186
+ res.setHeader('X-Content-Type-Options', 'nosniff')
187
+ const send = (status, data) => {
188
+ if (!res.writableEnded) {
189
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
190
+ res.end(JSON.stringify(data))
191
+ }
192
+ }
193
+ if (closed || req.headers.host !== new URL(origin).host) {
194
+ send(403, { status: 'rejected', message: '本机确认地址不匹配' })
195
+ return
196
+ }
197
+ if (req.method === 'GET' && req.url === pathname) {
198
+ res.setHeader(
199
+ 'Content-Security-Policy',
200
+ `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; frame-src 'self'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'`
201
+ )
202
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
203
+ res.end(
204
+ agreementPage({
205
+ protocol,
206
+ document,
207
+ account: { ...account, origin: new URL(client.baseUrl).origin },
208
+ continuation,
209
+ actionPath: pathname + '/decision',
210
+ csrf,
211
+ nonce
212
+ })
213
+ )
214
+ return
215
+ }
216
+ if (req.method === 'GET' && req.url === pathname + '/status') {
217
+ if (settled) {
218
+ send(200, { status: 'cancelled', message: '本次页面已结束,请查看 CLI 状态' })
219
+ return
220
+ }
221
+ if (processing) {
222
+ send(200, { status: 'waiting', message: '正在确认' })
223
+ return
224
+ }
225
+ try {
226
+ const state = await freshState()
227
+ if (state.agreed || !state.required) {
228
+ if (state.required) await freshDocument()
229
+ res.once('finish', () => agreed(state))
230
+ send(200, {
231
+ status: state.agreed ? 'agreed' : 'not_required',
232
+ message: state.agreed
233
+ ? '当前账号已确认当前版本协议,CLI 将核验后继续'
234
+ : '当前服务不要求协议确认,未记录同意,CLI 将重新核验后继续'
235
+ })
236
+ } else send(200, { status: 'waiting', protocol: state })
237
+ } catch (error) {
238
+ if (error.code?.startsWith('AGREEMENT_')) {
239
+ send(409, { status: 'changed', message: error.message })
240
+ fail(error.code, error.message)
241
+ } else send(503, { status: 'waiting', message: '协议状态查询暂不可用,尚未继续生成' })
242
+ }
243
+ return
244
+ }
245
+ if (req.method !== 'POST' || req.url !== pathname + '/decision') {
246
+ send(404, { status: 'rejected', message: '页面不存在' })
247
+ return
248
+ }
249
+ if (
250
+ req.headers.origin !== origin ||
251
+ req.headers['x-uzoo-agreement'] !== csrf ||
252
+ !/^application\/json(?:;|$)/i.test(req.headers['content-type'] || '')
253
+ ) {
254
+ send(403, { status: 'rejected', message: '需要从本页明确确认' })
255
+ return
256
+ }
257
+ if (settled || processing || decisionSent) {
258
+ send(409, { status: 'waiting', message: '本次确认已处理或结果待核实,请勿重复点击' })
259
+ return
260
+ }
261
+ processing = true
262
+ try {
263
+ const chunks = []
264
+ let size = 0
265
+ for await (const chunk of req) {
266
+ size += chunk.length
267
+ if (size > 1024) {
268
+ send(413, { status: 'rejected', message: '确认请求过大' })
269
+ return
270
+ }
271
+ chunks.push(chunk)
272
+ }
273
+ let body
274
+ try {
275
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
276
+ } catch {
277
+ send(400, { status: 'rejected', message: '确认请求无效' })
278
+ return
279
+ }
280
+ if (!body || typeof body.agreed !== 'boolean' || Object.keys(body).length !== 1) {
281
+ send(400, { status: 'rejected', message: '缺少明确选择' })
282
+ return
283
+ }
284
+ if (!body.agreed) {
285
+ res.once('finish', () =>
286
+ fail(
287
+ 'AGREEMENT_DECLINED',
288
+ '用户未同意《统一协议》《Seedance2.0模型使用承诺书》,本次生成未继续'
289
+ )
290
+ )
291
+ send(200, { status: 'declined', message: '你选择了不同意,本次生成未继续' })
292
+ return
293
+ }
294
+ const state = await freshState()
295
+ if (state.required) await freshDocument()
296
+ if (!state.agreed && state.required) {
297
+ decisionSent = true
298
+ // This authenticated POST only occurs after the human's checkbox + click.
299
+ await client.raw(
300
+ '/video/protocol-agreement',
301
+ { method: 'POST', body: { agreed: true } },
302
+ { retry: false }
303
+ )
304
+ }
305
+ const verified = await freshState()
306
+ if (verified.required) await freshDocument()
307
+ if (verified.required && !verified.agreed) throw new Error('签署记录尚未确认')
308
+ res.once('finish', () => agreed(verified))
309
+ send(200, {
310
+ status: verified.agreed ? 'agreed' : 'not_required',
311
+ message: verified.agreed
312
+ ? '协议状态已核验,CLI 将检查原任务参数与费用后继续'
313
+ : '当前服务不要求协议确认,未记录同意,CLI 将重新核验后继续'
314
+ })
315
+ } catch (error) {
316
+ if (error.code === 'API_ERROR' && (error.apiCode === 2001 || [401,403].includes(error.status))) {
317
+ const requestId=/^[a-zA-Z0-9_-]{1,120}$/.test(error.requestId||'')?error.requestId:undefined
318
+ const message='协议确认被服务端拒绝(HTTP '+error.status+(error.apiCode?',code '+error.apiCode:'')+(requestId?',request_id '+requestId:'')+');尚未核验签署成功,未继续生成。请核对账号和接口权限。'
319
+ res.once('finish',()=>fail('AGREEMENT_REJECTED',message,{http_status:error.status,...(error.apiCode?{api_code:error.apiCode}:{}),...(requestId?{request_id:requestId}:{})}))
320
+ send(403,{status:'rejected',message})
321
+ } else
322
+ if (error.code?.startsWith('AGREEMENT_')) {
323
+ send(409, { status: 'changed', message: error.message })
324
+ fail(error.code, error.message)
325
+ } else
326
+ send(503, {
327
+ status: 'waiting',
328
+ message: '协议签署结果尚未确认,CLI 会只读查询;不会重复提交同意或继续生成'
329
+ })
330
+ } finally {
331
+ processing = false
332
+ }
333
+ })
334
+ server.requestTimeout = 10000
335
+ server.headersTimeout = 10000
336
+ await new Promise((resolve, reject) => {
337
+ server.once('error', reject)
338
+ server.listen(0, '127.0.0.1', resolve)
339
+ })
340
+ origin = `http://127.0.0.1:${server.address().port}`
341
+ const abort = () => fail('AGREEMENT_CANCELLED', '协议确认已取消,原任务未继续')
342
+ signal.addEventListener('abort', abort, { once: true })
343
+ if (signal.aborted) abort()
344
+ timer = setTimeout(
345
+ () =>
346
+ fail(
347
+ 'AGREEMENT_WAIT_TIMEOUT',
348
+ '等待用户确认协议超时,原任务保持等待签署,可恢复原 operation'
349
+ ),
350
+ timeout
351
+ )
352
+ return {
353
+ url: origin + pathname,
354
+ wait,
355
+ close: async () => {
356
+ if (closed) return
357
+ closed = true
358
+ clearTimeout(timer)
359
+ signal.removeEventListener('abort', abort)
360
+ server.closeAllConnections()
361
+ await new Promise((resolve) => server.close(resolve))
362
+ }
363
+ }
364
+ }
365
+
366
+ export async function browserAgreement(
367
+ client,
368
+ {
369
+ browser = 'default',
370
+ timeout = 300000,
371
+ openUrl = openLoginBrowser,
372
+ onReady = () => {},
373
+ onOpenError = () => {},
374
+ continuation,
375
+ signal = cancellation.signal
376
+ } = {}
377
+ ) {
378
+ if (
379
+ !['default', 'msedge', 'chrome'].includes(browser) ||
380
+ !Number.isSafeInteger(timeout) ||
381
+ timeout < 1 ||
382
+ timeout > 1800000
383
+ )
384
+ throw new Error('协议浏览器或等待时间无效')
385
+ const account = identity(await client.raw('/auth/me'))
386
+ const protocol = await readProtocol(client)
387
+ if (!protocol.required || protocol.agreed)
388
+ return { status: protocol.agreed ? 'already_agreed' : 'not_required', protocol, account }
389
+ let document
390
+ try {
391
+ document = await readAgreementDocument(client)
392
+ } catch {
393
+ throw agreementError(
394
+ 'AGREEMENT_DOCUMENT_UNAVAILABLE',
395
+ '《统一协议》《Seedance2.0模型使用承诺书》正文暂不可用,未签署也未继续生成',
396
+ protocol
397
+ )
398
+ }
399
+ const bridge = await startAgreementBridge({
400
+ client,
401
+ protocol,
402
+ document,
403
+ account,
404
+ continuation,
405
+ timeout,
406
+ signal
407
+ })
408
+ try {
409
+ onReady(bridge.url, { protocol, account, document: { title: document.title, id: document.id } })
410
+ try {
411
+ await openUrl(bridge.url, browser)
412
+ } catch (error) {
413
+ onOpenError(error.message)
414
+ }
415
+ return await bridge.wait
416
+ } finally {
417
+ await bridge.close()
418
+ }
419
+ }