@remixmate/cli 0.9.72 → 0.9.73

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 (179) hide show
  1. package/LICENSE +45 -21
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/dist/argv.js +1 -51
  5. package/dist/auth/auth-lock.js +1 -100
  6. package/dist/auth/auto-login.js +7 -75
  7. package/dist/auth/commands.js +40 -290
  8. package/dist/auth/credential-store.js +1 -143
  9. package/dist/auth/device-flow-runner.js +13 -110
  10. package/dist/auth/device-flow.js +1 -132
  11. package/dist/auth/ensure.js +1 -62
  12. package/dist/auth/environment.js +1 -69
  13. package/dist/auth/pending-store.js +1 -45
  14. package/dist/auth/resolve.js +1 -44
  15. package/dist/billing.js +3 -76
  16. package/dist/capabilities.js +1 -75
  17. package/dist/cli.js +39 -211
  18. package/dist/diagnostics.js +2 -70
  19. package/dist/doctor.js +9 -188
  20. package/dist/errors.js +1 -33
  21. package/dist/exec.js +4 -47
  22. package/dist/handlers/gen-digital-human.js +33 -264
  23. package/dist/handlers/gen-image.js +9 -113
  24. package/dist/handlers/gen-post.js +15 -192
  25. package/dist/handlers/gen-video.js +7 -185
  26. package/dist/handlers/gen-voice.js +19 -133
  27. package/dist/handlers/index.js +1 -20
  28. package/dist/handlers/shared.js +1 -66
  29. package/dist/handlers/template-init.js +1 -21
  30. package/dist/http.js +3 -155
  31. package/dist/index.js +1 -8
  32. package/dist/install.js +40 -448
  33. package/dist/manifest.json +44 -3
  34. package/dist/progress.js +2 -15
  35. package/dist/project/commands.js +34 -156
  36. package/dist/project/host.js +1 -151
  37. package/dist/project/resolve.js +1 -65
  38. package/dist/project/store.js +2 -97
  39. package/dist/project/take.js +1 -106
  40. package/dist/project/trace.js +5 -278
  41. package/dist/registry.js +3 -70
  42. package/dist/runner.js +7 -225
  43. package/dist/skill-schema.js +1 -194
  44. package/dist/template/args.js +1 -69
  45. package/dist/template/check.js +2 -30
  46. package/dist/template/commands.js +34 -163
  47. package/dist/template/constants.js +1 -46
  48. package/dist/template/init.js +5 -133
  49. package/dist/template/list.js +1 -46
  50. package/dist/template/preview.js +7 -318
  51. package/dist/template/proc.js +1 -43
  52. package/dist/template/project.js +6 -161
  53. package/dist/template/publish.js +5 -185
  54. package/dist/template/render-client.js +2 -79
  55. package/dist/template/upgrade.js +4 -123
  56. package/dist/text.js +1 -15
  57. package/package.json +2 -35
  58. package/skills/doc-parse/scripts/extract_via_render.py +32 -32
  59. package/skills/export-jianying/scripts/gen_jianying_draft.py +188 -188
  60. package/skills/gen-script/SKILL.md +23 -1
  61. package/skills/gen-script/scripts/gen_script.py +644 -551
  62. package/skills/gen-script/skill.json +12 -0
  63. package/skills/gen-script/version.json +1 -1
  64. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +21 -21
  65. package/skills/render-video/scripts/_chrome_vendor.py +17 -17
  66. package/skills/render-video/scripts/_digital_human_segments.py +34 -34
  67. package/skills/render-video/scripts/_video_probe.py +33 -33
  68. package/skills/render-video/scripts/_vod_polling.py +13 -13
  69. package/skills/render-video/scripts/remote_renderer_client.py +42 -42
  70. package/skills/render-video/scripts/render_video.py +639 -639
  71. package/skills/render-video/scripts/required_props.py +37 -37
  72. package/skills/render-video/scripts/upload_video.py +35 -35
  73. package/skills/render-video/version.json +1 -1
  74. package/skills/template-registry/SKILL.md +16 -0
  75. package/skills/template-registry/scripts/countdown_spec.py +92 -92
  76. package/skills/template-registry/scripts/doc_extract.py +40 -40
  77. package/skills/template-registry/scripts/list_templates.py +303 -191
  78. package/skills/template-registry/scripts/log_diagnostics.py +1 -1
  79. package/skills/template-registry/scripts/match_template.py +87 -87
  80. package/skills/template-registry/scripts/payload_hydrator.py +89 -89
  81. package/skills/template-registry/scripts/registry_loader.py +159 -159
  82. package/skills/template-registry/scripts/render_job_client.py +75 -75
  83. package/skills/template-registry/scripts/showcase_loader.py +44 -44
  84. package/skills/template-registry/skill.json +4 -1
  85. package/skills/template-registry/version.json +1 -1
  86. package/skills/template-registry/video_dsl/runtime/__init__.py +8 -8
  87. package/skills/template-registry/video_dsl/runtime/dsl_validator.py +329 -329
  88. package/skills/template-registry/video_dsl/runtime/narration_budget.py +36 -36
  89. package/skills/template-registry/video_dsl/runtime/payload_contract.py +7 -7
  90. package/skills/template-registry/video_dsl/runtime/prompt_enhancer.py +98 -98
  91. package/skills/template-registry/video_dsl/runtime/timeline_compiler.py +127 -127
  92. package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +14 -1
  93. package/skills/video-parser/scripts/analyze_video.py +173 -173
  94. package/skills/video-parser/scripts/deconstruct_video.py +91 -91
  95. package/skills/video-parser/scripts/parse_via_render.py +21 -21
  96. package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +4 -4
  97. package/skills/web-screenshot/scripts/_media_screenshot/bootstrap.py +7 -7
  98. package/skills/web-screenshot/scripts/_media_screenshot/browser.py +4 -4
  99. package/skills/web-screenshot/scripts/_media_screenshot/cli_args.py +4 -4
  100. package/skills/web-screenshot/scripts/_media_screenshot/js_loader.py +2 -2
  101. package/skills/web-screenshot/scripts/_media_screenshot/overlay.py +1 -1
  102. package/skills/web-screenshot/scripts/_media_screenshot/reader.py +40 -40
  103. package/skills/web-screenshot/scripts/_media_screenshot/recording.py +11 -11
  104. package/skills/web-screenshot/scripts/_media_screenshot/scenes/__init__.py +8 -8
  105. package/skills/web-screenshot/scripts/_media_screenshot/scenes/arrow.py +2 -2
  106. package/skills/web-screenshot/scripts/_media_screenshot/scenes/caption.py +1 -1
  107. package/skills/web-screenshot/scripts/_media_screenshot/scenes/click.py +1 -1
  108. package/skills/web-screenshot/scripts/_media_screenshot/scenes/code_lines.py +5 -5
  109. package/skills/web-screenshot/scripts/_media_screenshot/scenes/focus.py +1 -1
  110. package/skills/web-screenshot/scripts/_media_screenshot/scenes/highlight.py +3 -3
  111. package/skills/web-screenshot/scripts/_media_screenshot/scenes/hover.py +1 -1
  112. package/skills/web-screenshot/scripts/_media_screenshot/scenes/redact.py +1 -1
  113. package/skills/web-screenshot/scripts/_media_screenshot/scenes/scroll.py +1 -1
  114. package/skills/web-screenshot/scripts/_media_screenshot/scenes/sequence.py +1 -1
  115. package/skills/web-screenshot/scripts/_media_screenshot/scenes/title_card.py +1 -1
  116. package/skills/web-screenshot/scripts/_media_screenshot/scenes/type.py +1 -1
  117. package/skills/web-screenshot/scripts/_media_screenshot/scenes/wait.py +1 -1
  118. package/skills/web-screenshot/scripts/_media_screenshot/scenes/zoom.py +2 -2
  119. package/skills/web-screenshot/scripts/_media_screenshot/screenshot.py +15 -15
  120. package/skills/web-screenshot/scripts/_media_screenshot/storyboard.py +2 -2
  121. package/skills/web-screenshot/scripts/_media_screenshot/template.py +11 -11
  122. package/skills/web-screenshot/scripts/_media_screenshot/trim.py +10 -10
  123. package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +22 -22
  124. package/skills/web-screenshot/scripts/_media_upload.py +33 -33
  125. package/skills/web-screenshot/scripts/read_page.py +15 -15
  126. package/skills/web-screenshot/scripts/record.py +26 -26
  127. package/skills/web-screenshot/scripts/screenshot.py +5 -5
  128. package/dist/argv.d.ts +0 -15
  129. package/dist/auth/auth-lock.d.ts +0 -26
  130. package/dist/auth/auto-login.d.ts +0 -20
  131. package/dist/auth/commands.d.ts +0 -8
  132. package/dist/auth/credential-store.d.ts +0 -60
  133. package/dist/auth/device-flow-runner.d.ts +0 -58
  134. package/dist/auth/device-flow.d.ts +0 -52
  135. package/dist/auth/ensure.d.ts +0 -55
  136. package/dist/auth/environment.d.ts +0 -38
  137. package/dist/auth/pending-store.d.ts +0 -33
  138. package/dist/auth/resolve.d.ts +0 -30
  139. package/dist/billing.d.ts +0 -44
  140. package/dist/capabilities.d.ts +0 -63
  141. package/dist/cli.d.ts +0 -13
  142. package/dist/diagnostics.d.ts +0 -24
  143. package/dist/doctor.d.ts +0 -22
  144. package/dist/errors.d.ts +0 -29
  145. package/dist/exec.d.ts +0 -18
  146. package/dist/handlers/gen-digital-human.d.ts +0 -17
  147. package/dist/handlers/gen-image.d.ts +0 -20
  148. package/dist/handlers/gen-post.d.ts +0 -22
  149. package/dist/handlers/gen-video.d.ts +0 -19
  150. package/dist/handlers/gen-voice.d.ts +0 -26
  151. package/dist/handlers/index.d.ts +0 -20
  152. package/dist/handlers/shared.d.ts +0 -26
  153. package/dist/handlers/template-init.d.ts +0 -10
  154. package/dist/http.d.ts +0 -72
  155. package/dist/index.d.ts +0 -9
  156. package/dist/install.d.ts +0 -71
  157. package/dist/progress.d.ts +0 -23
  158. package/dist/project/commands.d.ts +0 -19
  159. package/dist/project/host.d.ts +0 -91
  160. package/dist/project/resolve.d.ts +0 -43
  161. package/dist/project/store.d.ts +0 -31
  162. package/dist/project/take.d.ts +0 -50
  163. package/dist/project/trace.d.ts +0 -98
  164. package/dist/registry.d.ts +0 -30
  165. package/dist/runner.d.ts +0 -39
  166. package/dist/skill-schema.d.ts +0 -138
  167. package/dist/template/args.d.ts +0 -25
  168. package/dist/template/check.d.ts +0 -23
  169. package/dist/template/commands.d.ts +0 -25
  170. package/dist/template/constants.d.ts +0 -39
  171. package/dist/template/init.d.ts +0 -28
  172. package/dist/template/list.d.ts +0 -23
  173. package/dist/template/preview.d.ts +0 -60
  174. package/dist/template/proc.d.ts +0 -26
  175. package/dist/template/project.d.ts +0 -47
  176. package/dist/template/publish.d.ts +0 -43
  177. package/dist/template/render-client.d.ts +0 -39
  178. package/dist/template/upgrade.d.ts +0 -44
  179. package/dist/text.d.ts +0 -8
@@ -1,133 +1,5 @@
1
- /**
2
- * `remixmate template init <dir> [--force]` — scaffold a starter project.
3
- *
4
- * Only Node + npm are required (requirement 3.1): the starter is fetched with
5
- * `npm pack` (works with whatever registry/proxy npm is configured for), unpacked
6
- * with the system `tar` (bsdtar ships with Windows 10+), then `npm install`ed.
7
- * No login is needed (requirement 3.5).
8
- */
9
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
10
- import os from 'node:os';
11
- import path from 'node:path';
12
- import { EXIT, SkillError } from '../errors.js';
13
- import { emitProgress } from '../progress.js';
14
- import { STARTER_PACKAGE, STARTER_TARBALL_ENV, STARTER_VERSION } from './constants.js';
15
- import { Output } from './project.js';
16
- import { runNpm, runProcess } from './proc.js';
17
- import { skillInstallHint } from '../install.js';
18
- function isNonEmptyDir(dir) {
19
- if (!existsSync(dir))
20
- return false;
21
- if (!statSync(dir).isDirectory()) {
22
- throw new SkillError(`❌ ${dir} 已存在且不是目录`, EXIT.USAGE);
23
- }
24
- return readdirSync(dir).length > 0;
25
- }
26
- /** Obtain the starter tarball: local override, else `npm pack` into `tmp`. Shared with `upgrade`. */
27
- export async function obtainStarterTarball(tmp, out) {
28
- const override = (process.env[STARTER_TARBALL_ENV] ?? '').trim();
29
- if (override) {
30
- const file = path.resolve(override);
31
- if (!existsSync(file)) {
32
- throw new SkillError(`❌ ${STARTER_TARBALL_ENV} 指向的文件不存在: ${file}`, EXIT.USAGE);
33
- }
34
- out.info(`使用本地 starter 包: ${file}`);
35
- return { file, label: `local:${path.basename(file)}` };
36
- }
37
- const spec = `${STARTER_PACKAGE}@${STARTER_VERSION}`;
38
- out.info(`下载 ${spec} …`);
39
- const res = await runNpm(['pack', spec, '--json', '--silent'], { cwd: tmp, stdio: 'pipe' });
40
- if (res.code !== 0) {
41
- const tail = res.stderr.trim().split('\n').slice(-5).join('\n');
42
- throw new SkillError(`❌ npm pack ${spec} 失败(退出码 ${res.code})。检查网络与 npm registry 配置。\n${tail}`, EXIT.BACKEND_UNREACHABLE);
43
- }
44
- let file;
45
- try {
46
- const parsed = JSON.parse(res.stdout);
47
- if (parsed[0]?.filename)
48
- file = path.join(tmp, parsed[0].filename);
49
- }
50
- catch {
51
- // Older npm without --json support: fall back to whatever .tgz landed in tmp.
52
- }
53
- file ??= readdirSync(tmp).filter((f) => f.endsWith('.tgz')).map((f) => path.join(tmp, f))[0];
54
- if (!file || !existsSync(file))
55
- throw new SkillError('❌ npm pack 未产出 tarball');
56
- return { file, label: spec };
57
- }
58
- export async function runInit(opts) {
59
- const out = new Output(Boolean(opts.json));
60
- if (!opts.dir)
61
- throw new SkillError('❌ 缺少目标目录:remixmate template init <dir>', EXIT.USAGE);
62
- // agent 常把 `~/remixmate-templates/x` 加引号传进来,shell 不会展开 —— 这里补上,
63
- // 否则会在 cwd 下建一个字面量 `~` 目录。
64
- const raw = opts.dir === '~' || /^~[\\/]/.test(opts.dir) ? path.join(os.homedir(), opts.dir.slice(1)) : opts.dir;
65
- const dir = path.resolve(raw);
66
- if (isNonEmptyDir(dir) && !opts.force) {
67
- throw new SkillError(`❌ 目标目录非空: ${dir}\n 换一个空目录,或确认可以覆盖后加 --force。`, EXIT.USAGE);
68
- }
69
- const tmp = mkdtempSync(path.join(os.tmpdir(), 'remixmate-starter-'));
70
- try {
71
- emitProgress({ phase: 'template-init:fetch', progress: 0.1 });
72
- const { file, label } = await obtainStarterTarball(tmp, out);
73
- emitProgress({ phase: 'template-init:extract', progress: 0.4 });
74
- mkdirSync(dir, { recursive: true });
75
- // npm tarballs are rooted at `package/` — strip it so files land directly in <dir>.
76
- const tar = await runProcess('tar', ['-xzf', file, '-C', dir, '--strip-components=1'], { stdio: 'pipe' });
77
- if (tar.code !== 0) {
78
- throw new SkillError(`❌ 解压 starter 失败(tar 退出码 ${tar.code}): ${tar.stderr.trim().slice(0, 300)}`);
79
- }
80
- let installed = false;
81
- if (!opts.skipInstall) {
82
- emitProgress({ phase: 'template-init:install', progress: 0.6 });
83
- out.info('安装依赖(npm install)…');
84
- const inst = await runNpm(['install', '--no-audit', '--no-fund'], { cwd: dir, stdio: 'stderr' });
85
- if (inst.code !== 0) {
86
- throw new SkillError(`❌ npm install 失败(退出码 ${inst.code})。工程文件已解压到 ${dir},修复后在该目录重跑 npm install。`);
87
- }
88
- installed = true;
89
- }
90
- emitProgress({ phase: 'template-init:done', progress: 1 });
91
- printNextSteps(out, dir, installed);
92
- const result = { ok: true, dir, starter: label, installed };
93
- out.result({ ...result });
94
- return result;
95
- }
96
- finally {
97
- rmSync(tmp, { recursive: true, force: true });
98
- }
99
- }
100
- function printNextSteps(out, dir, installed) {
101
- // 一律给绝对路径:agent 会话的 cwd 往往和用户终端不同,相对路径(尤其 ../../..)既难读又容易 cd 错。
102
- // 含空格等字符时加引号,保证可以原样复制执行。
103
- const cdTarget = /^[\w@%+=:,./\\~-]+$/.test(dir) ? dir : `"${dir}"`;
104
- out.info('');
105
- out.info(`✅ 模板工程已创建: ${dir}`);
106
- out.info('');
107
- out.info('下一步:');
108
- out.info(` cd ${cdTarget}`);
109
- if (!installed)
110
- out.info(' npm install');
111
- out.info(' npm run dev # 打开 Remotion Studio 本地预览');
112
- out.info(' remixmate template check <name> # 本地校验(无需登录)');
113
- out.info(' remixmate template preview <name> # 远程渲染预览 mp4(需登录)');
114
- out.info(' remixmate template publish <name> # 发布为你的私有模板(需登录)');
115
- out.info('');
116
- out.info('在本地 agent 里使用:');
117
- out.info(' Codex / WorkBuddy 在工程目录打开,agent 会读取 AGENTS.md');
118
- out.info(' Claude Code 在工程目录打开,agent 会读取 CLAUDE.md(与 AGENTS.md 内容相同)');
119
- out.info(' TraeWork 只认项目级 skill:在工程目录执行一次 remixmate install --project');
120
- out.info(' 其它宿主若还没装 skill:remixmate install');
121
- out.info(' 当前会话不在工程目录里时:先读 ' + path.join(dir, 'AGENTS.md') + ',或在该目录重开会话');
122
- let hint = null;
123
- try {
124
- hint = skillInstallHint();
125
- }
126
- catch {
127
- // 提示只是锦上添花,读回执失败不能让 init 失败。
128
- }
129
- if (hint) {
130
- out.info('');
131
- out.info(hint);
132
- }
133
- }
1
+ var k=Object.defineProperty;var m=(e,i)=>k(e,"name",{value:i,configurable:!0});import{existsSync as w,mkdirSync as u,mkdtempSync as x,readdirSync as E,rmSync as T,statSync as g}from"node:fs";import $ from"node:os";import o from"node:path";import{EXIT as f,SkillError as a}from"../errors.js";import{emitProgress as c}from"../progress.js";import{STARTER_PACKAGE as N,STARTER_TARBALL_ENV as A,STARTER_VERSION as R}from"./constants.js";import{Output as j}from"./project.js";import{runNpm as y,runProcess as b}from"./proc.js";import{skillInstallHint as v}from"../install.js";function C(e){if(!w(e))return!1;if(!g(e).isDirectory())throw new a(`❌ ${e} 已存在且不是目录`,f.USAGE);return E(e).length>0}m(C,"isNonEmptyDir");async function G(e,i){const l=(process.env[A]??"").trim();if(l){const t=o.resolve(l);if(!w(t))throw new a(`❌ ${A} 指向的文件不存在: ${t}`,f.USAGE);return i.info(`使用本地 starter 包: ${t}`),{file:t,label:`local:${o.basename(t)}`}}const n=`${N}@${R}`;i.info(`下载 ${n} …`);const r=await y(["pack",n,"--json","--silent"],{cwd:e,stdio:"pipe"});if(r.code!==0){const t=r.stderr.trim().split(`
2
+ `).slice(-5).join(`
3
+ `);throw new a(`❌ npm pack ${n} 失败(退出码 ${r.code})。检查网络与 npm registry 配置。
4
+ ${t}`,f.BACKEND_UNREACHABLE)}let s;try{const t=JSON.parse(r.stdout);t[0]?.filename&&(s=o.join(e,t[0].filename))}catch{}if(s??=E(e).filter(t=>t.endsWith(".tgz")).map(t=>o.join(e,t))[0],!s||!w(s))throw new a("❌ npm pack 未产出 tarball");return{file:s,label:n}}m(G,"obtainStarterTarball");async function K(e){const i=new j(!!e.json);if(!e.dir)throw new a("❌ 缺少目标目录:remixmate template init <dir>",f.USAGE);const l=e.dir==="~"||/^~[\\/]/.test(e.dir)?o.join($.homedir(),e.dir.slice(1)):e.dir,n=o.resolve(l);if(C(n)&&!e.force)throw new a(`❌ 目标目录非空: ${n}
5
+ 换一个空目录,或确认可以覆盖后加 --force。`,f.USAGE);const r=x(o.join($.tmpdir(),"remixmate-starter-"));try{c({phase:"template-init:fetch",progress:.1});const{file:s,label:t}=await G(r,i);c({phase:"template-init:extract",progress:.4}),u(n,{recursive:!0});const p=await b("tar",["-xzf",s,"-C",n,"--strip-components=1"],{stdio:"pipe"});if(p.code!==0)throw new a(`❌ 解压 starter 失败(tar 退出码 ${p.code}): ${p.stderr.trim().slice(0,300)}`);let d=!1;if(!e.skipInstall){c({phase:"template-init:install",progress:.6}),i.info("安装依赖(npm install)…");const S=await y(["install","--no-audit","--no-fund"],{cwd:n,stdio:"stderr"});if(S.code!==0)throw new a(`❌ npm install 失败(退出码 ${S.code})。工程文件已解压到 ${n},修复后在该目录重跑 npm install。`);d=!0}c({phase:"template-init:done",progress:1}),U(i,n,d);const h={ok:!0,dir:n,starter:t,installed:d};return i.result({...h}),h}finally{T(r,{recursive:!0,force:!0})}}m(K,"runInit");function U(e,i,l){const n=/^[\w@%+=:,./\\~-]+$/.test(i)?i:`"${i}"`;e.info(""),e.info(`✅ 模板工程已创建: ${i}`),e.info(""),e.info("下一步:"),e.info(` cd ${n}`),l||e.info(" npm install"),e.info(" npm run dev # 打开 Remotion Studio 本地预览"),e.info(" remixmate template check <name> # 本地校验(无需登录)"),e.info(" remixmate template preview <name> # 远程渲染预览 mp4(需登录)"),e.info(" remixmate template publish <name> # 发布为你的私有模板(需登录)"),e.info(""),e.info("在本地 agent 里使用:"),e.info(" Codex / WorkBuddy 在工程目录打开,agent 会读取 AGENTS.md"),e.info(" Claude Code 在工程目录打开,agent 会读取 CLAUDE.md(与 AGENTS.md 内容相同)"),e.info(" TraeWork 只认项目级 skill:在工程目录执行一次 remixmate install --project"),e.info(" 其它宿主若还没装 skill:remixmate install"),e.info(" 当前会话不在工程目录里时:先读 "+o.join(i,"AGENTS.md")+",或在该目录重开会话");let r=null;try{r=v()}catch{}r&&(e.info(""),e.info(r))}m(U,"printNextSteps");export{G as obtainStarterTarball,K as runInit};
@@ -1,46 +1 @@
1
- /**
2
- * `remixmate template list` — the caller's own (non-builtin) templates.
3
- *
4
- * Uses ab-api `/remotionTemplate/myPage` (the "mine" view: user_id = caller,
5
- * is_builtin = 0, not deleted). The builtin filter is re-applied client side
6
- * so a backend change can never make this list show platform templates.
7
- */
8
- import { mmPost, resolveHttpContext } from '../http.js';
9
- import { padDisplay } from '../text.js';
10
- import { Output } from './project.js';
11
- const PAGE_SIZE = 100;
12
- const MAX_PAGES = 20;
13
- export async function runList(opts) {
14
- const out = new Output(Boolean(opts.json));
15
- const ctx = await resolveHttpContext('template-list', { privateToken: opts.token, apiBaseUrl: opts.apiBaseUrl });
16
- const templates = [];
17
- for (let current = 1; current <= MAX_PAGES; current++) {
18
- const page = await mmPost(ctx, '/remotionTemplate/myPage', { current, pageSize: PAGE_SIZE }, { timeoutMs: 30_000 });
19
- const list = page?.list ?? [];
20
- for (const t of list) {
21
- if (t.isBuiltin === true)
22
- continue;
23
- templates.push({
24
- templateKey: String(t.templateKey ?? ''),
25
- name: String(t.name ?? ''),
26
- lifecycleStatus: typeof t.lifecycleStatus === 'string' ? t.lifecycleStatus : undefined,
27
- coreVersion: typeof t.coreVersion === 'string' && t.coreVersion ? t.coreVersion : undefined,
28
- updateTime: typeof t.updateTime === 'string' ? t.updateTime : undefined,
29
- });
30
- }
31
- const total = page?.pager?.total ?? 0;
32
- if (list.length < PAGE_SIZE || current * PAGE_SIZE >= total)
33
- break;
34
- }
35
- if (templates.length === 0) {
36
- out.info('还没有私有模板。用 remixmate template publish <name> 发布一个。');
37
- }
38
- else {
39
- for (const t of templates) {
40
- out.info(` ${padDisplay(t.templateKey, 32)}${padDisplay(t.name, 24)}${padDisplay(t.lifecycleStatus ?? '', 12)}${t.updateTime ?? ''}`);
41
- }
42
- }
43
- const result = { ok: true, templates };
44
- out.result(result);
45
- return result;
46
- }
1
+ var f=Object.defineProperty;var m=(o,n)=>f(o,"name",{value:n,configurable:!0});import{mmPost as y,resolveHttpContext as d}from"../http.js";import{padDisplay as a}from"../text.js";import{Output as g}from"./project.js";const r=100,S=20;async function B(o){const n=new g(!!o.json),p=await d("template-list",{privateToken:o.token,apiBaseUrl:o.apiBaseUrl}),i=[];for(let t=1;t<=S;t++){const l=await y(p,"/remotionTemplate/myPage",{current:t,pageSize:r},{timeoutMs:3e4}),c=l?.list??[];for(const e of c)e.isBuiltin!==!0&&i.push({templateKey:String(e.templateKey??""),name:String(e.name??""),lifecycleStatus:typeof e.lifecycleStatus=="string"?e.lifecycleStatus:void 0,coreVersion:typeof e.coreVersion=="string"&&e.coreVersion?e.coreVersion:void 0,updateTime:typeof e.updateTime=="string"?e.updateTime:void 0});const u=l?.pager?.total??0;if(c.length<r||t*r>=u)break}if(i.length===0)n.info("还没有私有模板。用 remixmate template publish <name> 发布一个。");else for(const t of i)n.info(` ${a(t.templateKey,32)}${a(t.name,24)}${a(t.lifecycleStatus??"",12)}${t.updateTime??""}`);const s={ok:!0,templates:i};return n.result(s),s}m(B,"runList");export{B as runList};
@@ -1,318 +1,7 @@
1
- /**
2
- * `remixmate template preview <name>` — render a draft mp4 on the real ab-render.
3
- *
4
- * Ported from template-library `scripts/preview-remote.mjs` (same HTTP contract,
5
- * same file-enumeration and sample-props rules). It deliberately does NOT
6
- * reimplement the DSL → TemplateBinding pipeline: preview answers "does this
7
- * code run in the real renderer"; binding correctness is `render-video`'s job.
8
- *
9
- * Sample props are contract-driven: they come from the template's own
10
- * `dsl-example.json` through the `slotMapping[slot].propExtractors` it declares,
11
- * plus two global conventions (scene.customPayload → props.templateData,
12
- * dsl.assets → resolvedAssets). Nothing here knows any specific template.
13
- */
14
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
15
- import path from 'node:path';
16
- import { EXIT, SkillError } from '../errors.js';
17
- import { resolveHttpContext } from '../http.js';
18
- import { emitProgress } from '../progress.js';
19
- import { IDENTITY_FILE, PREVIEW_MAX_LONG_EDGE, PREVIEW_MAX_SECONDS, PREVIEW_MAX_SHORT_EDGE, } from './constants.js';
20
- import { runCheck } from './check.js';
21
- import { Output, readJsonFile, requireProjectRoot, requireTemplateDir, validateTemplateName } from './project.js';
22
- import { RenderBusinessError, renderPost, resolveRenderBaseUrl } from './render-client.js';
23
- const FPS = 30;
24
- const DEFAULT_DURATION_SEC = 5;
25
- const DEFAULT_TIMEOUT_SEC = 180;
26
- // Same enumeration rules as preview-remote.mjs.
27
- const SKIP_DIRS = new Set(['node_modules', 'dist', '__pycache__', '.git']);
28
- const TEXT_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.json', '.css', '.scss', '.md']);
29
- const MAX_FILE_BYTES = 512 * 1024;
30
- const WARN_TOTAL_BYTES = 20 * 1024 * 1024;
31
- /** Keys must stay in sync with the metadata schema's supportedAspectRatios enum. */
32
- const ASPECT_RATIOS = {
33
- '16:9': [16, 9],
34
- '9:16': [9, 16],
35
- '1:1': [1, 1],
36
- '3:4': [3, 4],
37
- '4:3': [4, 3],
38
- };
39
- export const PREVIEW_LIMIT_MESSAGE = `草稿预览超出上限:长边 ≤ ${PREVIEW_MAX_LONG_EDGE}px、短边 ≤ ${PREVIEW_MAX_SHORT_EDGE}px、时长 ≤ ${PREVIEW_MAX_SECONDS} 秒。` +
40
- '请缩短 --duration-sec,或减少 --props-json 的页数;正式出片用 remixmate render-video,不受此限。';
41
- // ── files ────────────────────────────────────────────────────────────────────
42
- export function collectFiles(root, warn = () => { }) {
43
- const files = {};
44
- let bytes = 0;
45
- const walk = (dir) => {
46
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
47
- const full = path.join(dir, entry.name);
48
- if (entry.isDirectory()) {
49
- if (!SKIP_DIRS.has(entry.name))
50
- walk(full);
51
- continue;
52
- }
53
- if (!TEXT_EXTS.has(path.extname(entry.name)))
54
- continue;
55
- if (/\.(test|spec)\./.test(entry.name))
56
- continue;
57
- // Local publish identity — not template source.
58
- if (dir === root && entry.name === IDENTITY_FILE)
59
- continue;
60
- const size = statSync(full).size;
61
- if (size > MAX_FILE_BYTES) {
62
- warn(` ⚠ 跳过超大文件(${(size / 1024).toFixed(0)} KB):${path.relative(root, full)}`);
63
- continue;
64
- }
65
- bytes += size;
66
- files[path.relative(root, full).split(path.sep).join('/')] = readFileSync(full, 'utf8');
67
- }
68
- };
69
- walk(root);
70
- return { files, bytes };
71
- }
72
- // ── sample props ─────────────────────────────────────────────────────────────
73
- function applyExtractors(scene, extractors) {
74
- const out = {};
75
- for (const [propName, spec] of Object.entries(extractors ?? {})) {
76
- if (!spec || typeof spec.from !== 'string')
77
- continue;
78
- const from = spec.from;
79
- if (from === 'textLayers') {
80
- const layer = (scene.textLayers ?? []).find((l) => l.role === spec.role);
81
- if (layer?.content !== undefined)
82
- out[propName] = layer.content;
83
- continue;
84
- }
85
- const value = from.split('.').reduce((acc, k) => (acc == null ? acc : acc[k]), scene); // eslint-disable-line @typescript-eslint/no-explicit-any
86
- if (value !== undefined)
87
- out[propName] = value;
88
- }
89
- return out;
90
- }
91
- export function buildSampleProps(tplDir, tpl, compositionId, propsJson, warn = () => { }) {
92
- const examplePath = path.join(tplDir, 'dsl-example.json');
93
- const dsl = existsSync(examplePath) ? readJsonFile(examplePath) : null;
94
- const assets = Array.isArray(dsl?.assets) ? dsl.assets : [];
95
- // --props-json overrides props but keeps the example's asset table: assetRefs
96
- // in the override still need something to resolve against.
97
- if (propsJson !== undefined) {
98
- let props;
99
- try {
100
- props = JSON.parse(propsJson);
101
- }
102
- catch (err) {
103
- throw new SkillError(`❌ --props-json 解析失败: ${err.message}`, EXIT.USAGE);
104
- }
105
- return { props, assets, durationSec: null };
106
- }
107
- const scene = (dsl?.scenes ?? [])[0];
108
- if (!scene)
109
- return { props: {}, assets, durationSec: null };
110
- const slot = (tpl.compositions ?? []).find((c) => c.compositionId === compositionId)?.slot;
111
- const extractors = slot ? tpl.slotMapping?.[slot]?.propExtractors : undefined;
112
- // Covers render through a dedicated channel and never declare slotMapping.cover.
113
- if (slot && slot !== 'cover' && !extractors) {
114
- warn(` ⚠ template.json 的 slotMapping.${slot} 未声明 propExtractors —— 预览将只带 templateData。`);
115
- }
116
- const props = applyExtractors(scene, extractors);
117
- if (scene.customPayload)
118
- props.templateData = scene.customPayload;
119
- return { props, assets, durationSec: typeof scene.duration === 'number' ? scene.duration : null, scene };
120
- }
121
- /** Narration → subtitle segments (exact from items[].atSec, else proportional by sentence). */
122
- export function buildSubtitleSegments(scene, totalFrames) {
123
- const narration = scene?.audio?.narration;
124
- if (!narration)
125
- return [];
126
- const finish = (segs) => segs
127
- .filter((s) => s.text)
128
- .map((s, i, arr) => ({
129
- text: s.text,
130
- startFrame: Math.max(0, Math.round(s.startFrame)),
131
- endFrame: i === arr.length - 1 ? totalFrames : Math.round(arr[i + 1].startFrame),
132
- }))
133
- .filter((s) => s.endFrame > s.startFrame);
134
- const items = Array.isArray(narration.items) ? narration.items : null;
135
- if (items && items.length > 0 && items.some((it) => typeof it?.atSec === 'number')) {
136
- let cursor = 0;
137
- return finish(items.map((it) => {
138
- const at = typeof it?.atSec === 'number' ? it.atSec * FPS : cursor;
139
- cursor = at;
140
- return { text: it?.text ?? '', startFrame: at };
141
- }));
142
- }
143
- const text = typeof narration.text === 'string' ? narration.text.trim() : '';
144
- if (!text)
145
- return [];
146
- const sentences = text.split(/(?<=[。!?!?])/).map((s) => s.trim()).filter(Boolean);
147
- if (sentences.length === 0)
148
- return [];
149
- const weights = sentences.map((s) => Math.max(1, s.length));
150
- const total = weights.reduce((a, b) => a + b, 0);
151
- let acc = 0;
152
- return finish(sentences.map((s, i) => {
153
- const startFrame = (acc / total) * totalFrames;
154
- acc += weights[i];
155
- return { text: s, startFrame };
156
- }));
157
- }
158
- /** Largest even-pixel frame of the given aspect that fits the draft preview limits. */
159
- export function previewDimensions(aspect) {
160
- const [aw, ah] = ASPECT_RATIOS[aspect] ?? ASPECT_RATIOS['16:9'];
161
- const landscape = aw >= ah;
162
- const long = landscape ? aw : ah;
163
- const short = landscape ? ah : aw;
164
- const scale = Math.min(PREVIEW_MAX_LONG_EDGE / long, PREVIEW_MAX_SHORT_EDGE / short);
165
- const even = (n) => Math.floor(n / 2) * 2;
166
- const w = even(aw * scale);
167
- const h = even(ah * scale);
168
- return [w, h];
169
- }
170
- export function buildPreviewPlan(tplDir, opts, warn = () => { }) {
171
- const tpl = readJsonFile(path.join(tplDir, 'template.json'));
172
- const comps = Array.isArray(tpl.compositions) ? tpl.compositions : [];
173
- const compositionId = opts.compositionId ?? comps[0]?.compositionId;
174
- if (!compositionId)
175
- throw new SkillError('❌ template.json 未声明 compositions', EXIT.USAGE);
176
- if (opts.compositionId && !comps.some((c) => c.compositionId === compositionId)) {
177
- throw new SkillError(`❌ composition "${compositionId}" 不在该模板中。可选: ${comps.map((c) => c.compositionId).join(', ')}`, EXIT.USAGE);
178
- }
179
- const { files, bytes } = collectFiles(tplDir, warn);
180
- if (Object.keys(files).length === 0)
181
- throw new SkillError('❌ 模板目录没有可提交的源文件');
182
- if (bytes > WARN_TOTAL_BYTES)
183
- warn(` ⚠ 模板总体积 ${(bytes / 1024 / 1024).toFixed(1)} MB,接近 ab-render 的请求上限`);
184
- const sample = buildSampleProps(tplDir, tpl, compositionId, opts.propsJson, warn);
185
- const aspect = (tpl.supportedAspectRatios ?? [])[0] ?? '16:9';
186
- const durationSec = Number(opts.durationSec ?? sample.durationSec ?? tpl.supportedDurations?.min ?? DEFAULT_DURATION_SEC);
187
- const [width, height] = previewDimensions(aspect);
188
- // An array of props = one page each, chained into a multi-entry timeline.
189
- const propsList = Array.isArray(sample.props) ? sample.props : null;
190
- const perSceneFrames = Math.max(1, Math.round(durationSec * FPS));
191
- const totalFrames = propsList ? perSceneFrames * propsList.length : perSceneFrames;
192
- const totalSeconds = totalFrames / FPS;
193
- if (totalSeconds > PREVIEW_MAX_SECONDS) {
194
- throw new SkillError(`❌ ${PREVIEW_LIMIT_MESSAGE}(本次 ${totalSeconds.toFixed(1)} 秒)`, EXIT.USAGE);
195
- }
196
- const subtitleSegments = propsList ? [] : buildSubtitleSegments(sample.scene, perSceneFrames);
197
- const timeline = (propsList ?? [sample.props]).map((props, i) => {
198
- const startFrame = i * perSceneFrames;
199
- const endFrame = startFrame + perSceneFrames;
200
- return {
201
- sceneId: propsList ? `preview-${String(i + 1).padStart(2, '0')}` : 'preview',
202
- compositionId,
203
- startFrame,
204
- endFrame,
205
- durationFrames: perSceneFrames,
206
- startTime: Number((startFrame / FPS).toFixed(2)),
207
- endTime: Number((endFrame / FPS).toFixed(2)),
208
- props,
209
- layers: [],
210
- subtitleSegments,
211
- transition: { type: 'none', durationFrames: 0 },
212
- };
213
- });
214
- const inputProps = {
215
- timeline,
216
- renderConfig: {
217
- width,
218
- height,
219
- fps: FPS,
220
- totalFrames,
221
- totalDuration: Number(totalSeconds.toFixed(2)),
222
- codec: 'h264',
223
- crf: 18,
224
- outputFormat: 'mp4',
225
- },
226
- resolvedAssets: sample.assets,
227
- globalTypography: tpl.defaultTypography,
228
- colorScheme: tpl.defaultColorScheme,
229
- motionPreset: tpl.defaultMotionPreset,
230
- };
231
- return { files, inputProps, compositionId, totalSeconds };
232
- }
233
- function isLimitError(err) {
234
- return JSON.stringify(err.payload ?? {}).includes('draft_preview_limit_exceeded');
235
- }
236
- function printScanIssues(out, err) {
237
- const data = err.payload?.data;
238
- const issues = data?.issues ?? err.payload?.issues;
239
- if (!Array.isArray(issues) || issues.length === 0)
240
- return false;
241
- out.warn(`\n✗ 被 ab-render 的安全扫描拒绝(${issues.length} 处):`);
242
- for (const i of issues) {
243
- out.warn(typeof i === 'string' ? ` · ${i}` : ` · ${i.file}\n ${i.reason}${i.detail ? `: ${i.detail}` : ''}`);
244
- }
245
- out.warn('\n 先在本地运行 remixmate template check,它与平台使用同一份安全策略。');
246
- return true;
247
- }
248
- export async function runPreview(opts) {
249
- const out = new Output(Boolean(opts.json));
250
- const name = validateTemplateName(opts.name);
251
- const root = requireProjectRoot(opts.cwd);
252
- const tplDir = requireTemplateDir(root, name);
253
- emitProgress({ phase: 'template-preview:check', progress: 0.05 });
254
- const checkCode = await runCheck({ name, cwd: root, stdio: opts.json ? 'stderr' : 'inherit' });
255
- if (checkCode !== 0) {
256
- throw new SkillError(`❌ 本地校验未通过(check 退出码 ${checkCode}),已中止预览。`, checkCode === 2 ? EXIT.USAGE : EXIT.ERROR);
257
- }
258
- const plan = buildPreviewPlan(tplDir, opts, (m) => out.warn(m));
259
- const http = await resolveHttpContext('template-preview', { privateToken: opts.token, apiBaseUrl: opts.apiBaseUrl });
260
- const base = resolveRenderBaseUrl();
261
- const { width, height } = plan.inputProps.renderConfig;
262
- out.info(`模板 ${name}`);
263
- out.info(`composition ${plan.compositionId}`);
264
- out.info(`画幅 ${width}×${height} ${plan.totalSeconds.toFixed(1)}s @${FPS}fps`);
265
- out.info(`文件 ${Object.keys(plan.files).length} 个`);
266
- out.info(`ab-render ${base}`);
267
- const post = (route, body) => renderPost(base, route, body, { token: http.privateToken, skillName: 'template-preview' });
268
- let taskId;
269
- try {
270
- emitProgress({ phase: 'template-preview:submit', progress: 0.1 });
271
- const start = await post('/renderDraft', { files: plan.files, inputProps: plan.inputProps, upload: false });
272
- if (!start.taskId)
273
- throw new SkillError('❌ /renderDraft 未返回 taskId');
274
- taskId = start.taskId;
275
- }
276
- catch (err) {
277
- if (err instanceof RenderBusinessError) {
278
- if (isLimitError(err))
279
- throw new SkillError(`❌ ${PREVIEW_LIMIT_MESSAGE}`, EXIT.ERROR);
280
- if (printScanIssues(out, err))
281
- throw new SkillError('❌ 预览被安全扫描拒绝', EXIT.ERROR);
282
- }
283
- throw err;
284
- }
285
- out.info(`taskId ${taskId}`);
286
- const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
287
- const interval = opts.pollIntervalMs ?? 1500;
288
- const deadline = Date.now() + timeoutSec * 1000;
289
- let lastStatus = '';
290
- for (;;) {
291
- if (Date.now() > deadline) {
292
- // A timeout doesn't mean the task is gone — it's usually still queued.
293
- throw new SkillError(`❌ 轮询超时(${timeoutSec}s),任务 ${taskId} 仍在 ab-render 排队/渲染。\n` +
294
- ' 加长等待:--timeout-sec 900');
295
- }
296
- await new Promise((r) => setTimeout(r, interval));
297
- const st = await post('/renderDraftStatus', { taskId });
298
- if (st.status !== lastStatus) {
299
- lastStatus = st.status ?? '';
300
- out.info(st.status === 'pending' ? '排队中…(队列前面还有任务)' : `状态: ${st.status}`);
301
- }
302
- emitProgress({ phase: `template-preview:${st.status ?? 'unknown'}`, progress: 0.1 + 0.9 * (st.progress ?? 0), taskId });
303
- if (st.status === 'succeeded') {
304
- const url = st.previewUrl?.startsWith('http') ? st.previewUrl : `${base}${st.previewUrl ?? ''}`;
305
- out.info(`\n✅ 预览就绪\n ${url}`);
306
- out.info(' 临时产物,最长保留 24 小时(服务重启后可能更早失效)。');
307
- const result = { ok: true, name, taskId, url, compositionId: plan.compositionId, durationSec: plan.totalSeconds };
308
- out.result({ ...result });
309
- return result;
310
- }
311
- if (st.status === 'failed') {
312
- const detail = st.error ?? '(无错误详情)';
313
- if (detail.includes('draft_preview_limit_exceeded'))
314
- throw new SkillError(`❌ ${PREVIEW_LIMIT_MESSAGE}`);
315
- throw new SkillError(`❌ 渲染失败: ${detail}`);
316
- }
317
- }
318
- }
1
+ var O=Object.defineProperty;var f=(e,t)=>O(e,"name",{value:t,configurable:!0});import{existsSync as U,readdirSync as N,readFileSync as L,statSync as B}from"node:fs";import $ from"node:path";import{EXIT as x,SkillError as w}from"../errors.js";import{resolveHttpContext as C}from"../http.js";import{emitProgress as A}from"../progress.js";import{IDENTITY_FILE as G,PREVIEW_MAX_LONG_EDGE as R,PREVIEW_MAX_SECONDS as k,PREVIEW_MAX_SHORT_EDGE as j}from"./constants.js";import{runCheck as X}from"./check.js";import{Output as W,readJsonFile as M,requireProjectRoot as V,requireTemplateDir as J,validateTemplateName as Y}from"./project.js";import{RenderBusinessError as q,renderPost as z,resolveRenderBaseUrl as H}from"./render-client.js";const g=30,K=5,Q=180,Z=new Set(["node_modules","dist","__pycache__",".git"]),tt=new Set([".ts",".tsx",".js",".jsx",".mts",".cts",".json",".css",".scss",".md"]),et=512*1024,ot=20*1024*1024,D={"16:9":[16,9],"9:16":[9,16],"1:1":[1,1],"3:4":[3,4],"4:3":[4,3]},T=`草稿预览超出上限:长边 ≤ ${R}px、短边 ≤ ${j}px、时长 ≤ ${k} 秒。请缩短 --duration-sec,或减少 --props-json 的页数;正式出片用 remixmate render-video,不受此限。`;function nt(e,t=()=>{}){const r={};let n=0;const i=f(p=>{for(const o of N(p,{withFileTypes:!0})){const a=$.join(p,o.name);if(o.isDirectory()){Z.has(o.name)||i(a);continue}if(!tt.has($.extname(o.name))||/\.(test|spec)\./.test(o.name)||p===e&&o.name===G)continue;const s=B(a).size;if(s>et){t(` ⚠ 跳过超大文件(${(s/1024).toFixed(0)} KB):${$.relative(e,a)}`);continue}n+=s,r[$.relative(e,a).split($.sep).join("/")]=L(a,"utf8")}},"walk");return i(e),{files:r,bytes:n}}f(nt,"collectFiles");function st(e,t){const r={};for(const[n,i]of Object.entries(t??{})){if(!i||typeof i.from!="string")continue;const p=i.from;if(p==="textLayers"){const a=(e.textLayers??[]).find(s=>s.role===i.role);a?.content!==void 0&&(r[n]=a.content);continue}const o=p.split(".").reduce((a,s)=>a==null?a:a[s],e);o!==void 0&&(r[n]=o)}return r}f(st,"applyExtractors");function rt(e,t,r,n,i=()=>{}){const p=$.join(e,"dsl-example.json"),o=U(p)?M(p):null,a=Array.isArray(o?.assets)?o.assets:[];if(n!==void 0){let u;try{u=JSON.parse(n)}catch(h){throw new w(`❌ --props-json 解析失败: ${h.message}`,x.USAGE)}return{props:u,assets:a,durationSec:null}}const s=(o?.scenes??[])[0];if(!s)return{props:{},assets:a,durationSec:null};const d=(t.compositions??[]).find(u=>u.compositionId===r)?.slot,l=d?t.slotMapping?.[d]?.propExtractors:void 0;d&&d!=="cover"&&!l&&i(` ⚠ template.json 的 slotMapping.${d} 未声明 propExtractors —— 预览将只带 templateData。`);const m=st(s,l);return s.customPayload&&(m.templateData=s.customPayload),{props:m,assets:a,durationSec:typeof s.duration=="number"?s.duration:null,scene:s}}f(rt,"buildSampleProps");function it(e,t){const r=e?.audio?.narration;if(!r)return[];const n=f(l=>l.filter(m=>m.text).map((m,u,h)=>({text:m.text,startFrame:Math.max(0,Math.round(m.startFrame)),endFrame:u===h.length-1?t:Math.round(h[u+1].startFrame)})).filter(m=>m.endFrame>m.startFrame),"finish"),i=Array.isArray(r.items)?r.items:null;if(i&&i.length>0&&i.some(l=>typeof l?.atSec=="number")){let l=0;return n(i.map(m=>{const u=typeof m?.atSec=="number"?m.atSec*g:l;return l=u,{text:m?.text??"",startFrame:u}}))}const p=typeof r.text=="string"?r.text.trim():"";if(!p)return[];const o=p.split(/(?<=[。!?!?])/).map(l=>l.trim()).filter(Boolean);if(o.length===0)return[];const a=o.map(l=>Math.max(1,l.length)),s=a.reduce((l,m)=>l+m,0);let d=0;return n(o.map((l,m)=>{const u=d/s*t;return d+=a[m],{text:l,startFrame:u}}))}f(it,"buildSubtitleSegments");function at(e){const[t,r]=D[e]??D["16:9"],n=t>=r,i=n?t:r,p=n?r:t,o=Math.min(R/i,j/p),a=f(l=>Math.floor(l/2)*2,"even"),s=a(t*o),d=a(r*o);return[s,d]}f(at,"previewDimensions");function ct(e,t,r=()=>{}){const n=M($.join(e,"template.json")),i=Array.isArray(n.compositions)?n.compositions:[],p=t.compositionId??i[0]?.compositionId;if(!p)throw new w("❌ template.json 未声明 compositions",x.USAGE);if(t.compositionId&&!i.some(I=>I.compositionId===p))throw new w(`❌ composition "${p}" 不在该模板中。可选: ${i.map(I=>I.compositionId).join(", ")}`,x.USAGE);const{files:o,bytes:a}=nt(e,r);if(Object.keys(o).length===0)throw new w("❌ 模板目录没有可提交的源文件");a>ot&&r(` ⚠ 模板总体积 ${(a/1024/1024).toFixed(1)} MB,接近 ab-render 的请求上限`);const s=rt(e,n,p,t.propsJson,r),d=(n.supportedAspectRatios??[])[0]??"16:9",l=Number(t.durationSec??s.durationSec??n.supportedDurations?.min??K),[m,u]=at(d),h=Array.isArray(s.props)?s.props:null,y=Math.max(1,Math.round(l*g)),_=h?y*h.length:y,E=_/g;if(E>k)throw new w(`❌ ${T}(本次 ${E.toFixed(1)} 秒)`,x.USAGE);const c=h?[]:it(s.scene,y),F={timeline:(h??[s.props]).map((I,b)=>{const v=b*y,P=v+y;return{sceneId:h?`preview-${String(b+1).padStart(2,"0")}`:"preview",compositionId:p,startFrame:v,endFrame:P,durationFrames:y,startTime:Number((v/g).toFixed(2)),endTime:Number((P/g).toFixed(2)),props:I,layers:[],subtitleSegments:c,transition:{type:"none",durationFrames:0}}}),renderConfig:{width:m,height:u,fps:g,totalFrames:_,totalDuration:Number(E.toFixed(2)),codec:"h264",crf:18,outputFormat:"mp4"},resolvedAssets:s.assets,globalTypography:n.defaultTypography,colorScheme:n.defaultColorScheme,motionPreset:n.defaultMotionPreset};return{files:o,inputProps:F,compositionId:p,totalSeconds:E}}f(ct,"buildPreviewPlan");function pt(e){return JSON.stringify(e.payload??{}).includes("draft_preview_limit_exceeded")}f(pt,"isLimitError");function lt(e,t){const n=t.payload?.data?.issues??t.payload?.issues;if(!Array.isArray(n)||n.length===0)return!1;e.warn(`
2
+ ✗ 被 ab-render 的安全扫描拒绝(${n.length} 处):`);for(const i of n)e.warn(typeof i=="string"?` · ${i}`:` · ${i.file}
3
+ ${i.reason}${i.detail?`: ${i.detail}`:""}`);return e.warn(`
4
+ 先在本地运行 remixmate template check,它与平台使用同一份安全策略。`),!0}f(lt,"printScanIssues");async function $t(e){const t=new W(!!e.json),r=Y(e.name),n=V(e.cwd),i=J(n,r);A({phase:"template-preview:check",progress:.05});const p=await X({name:r,cwd:n,stdio:e.json?"stderr":"inherit"});if(p!==0)throw new w(`❌ 本地校验未通过(check 退出码 ${p}),已中止预览。`,p===2?x.USAGE:x.ERROR);const o=ct(i,e,c=>t.warn(c)),a=await C("template-preview",{privateToken:e.token,apiBaseUrl:e.apiBaseUrl}),s=H(),{width:d,height:l}=o.inputProps.renderConfig;t.info(`模板 ${r}`),t.info(`composition ${o.compositionId}`),t.info(`画幅 ${d}×${l} ${o.totalSeconds.toFixed(1)}s @${g}fps`),t.info(`文件 ${Object.keys(o.files).length} 个`),t.info(`ab-render ${s}`);const m=f((c,S)=>z(s,c,S,{token:a.privateToken,skillName:"template-preview"}),"post");let u;try{A({phase:"template-preview:submit",progress:.1});const c=await m("/renderDraft",{files:o.files,inputProps:o.inputProps,upload:!1});if(!c.taskId)throw new w("❌ /renderDraft 未返回 taskId");u=c.taskId}catch(c){if(c instanceof q){if(pt(c))throw new w(`❌ ${T}`,x.ERROR);if(lt(t,c))throw new w("❌ 预览被安全扫描拒绝",x.ERROR)}throw c}t.info(`taskId ${u}`);const h=e.timeoutSec??Q,y=e.pollIntervalMs??1500,_=Date.now()+h*1e3;let E="";for(;;){if(Date.now()>_)throw new w(`❌ 轮询超时(${h}s),任务 ${u} 仍在 ab-render 排队/渲染。
5
+ 加长等待:--timeout-sec 900`);await new Promise(S=>setTimeout(S,y));const c=await m("/renderDraftStatus",{taskId:u});if(c.status!==E&&(E=c.status??"",t.info(c.status==="pending"?"排队中…(队列前面还有任务)":`状态: ${c.status}`)),A({phase:`template-preview:${c.status??"unknown"}`,progress:.1+.9*(c.progress??0),taskId:u}),c.status==="succeeded"){const S=c.previewUrl?.startsWith("http")?c.previewUrl:`${s}${c.previewUrl??""}`;t.info(`
6
+ ✅ 预览就绪
7
+ ${S}`),t.info(" 临时产物,最长保留 24 小时(服务重启后可能更早失效)。");const F={ok:!0,name:r,taskId:u,url:S,compositionId:o.compositionId,durationSec:o.totalSeconds};return t.result({...F}),F}if(c.status==="failed"){const S=c.error??"(无错误详情)";throw S.includes("draft_preview_limit_exceeded")?new w(`❌ ${T}`):new w(`❌ 渲染失败: ${S}`)}}}f($t,"runPreview");export{T as PREVIEW_LIMIT_MESSAGE,ct as buildPreviewPlan,rt as buildSampleProps,it as buildSubtitleSegments,nt as collectFiles,at as previewDimensions,$t as runPreview};
@@ -1,43 +1 @@
1
- /**
2
- * Child-process helpers for `remixmate template`.
3
- *
4
- * Every command is spawned with an argv array — never a shell string — so a
5
- * directory or package spec can't be interpreted by a shell. The one exception
6
- * is npm on Windows: `npm` is `npm.cmd` there, and since Node's CVE-2024-27980
7
- * fix a .cmd can only be spawned with `shell: true`. For that path we only ever
8
- * pass fixed tokens or validated package specs (paths travel via `cwd`), and
9
- * refuse anything containing shell metacharacters.
10
- */
11
- import { spawn } from 'node:child_process';
12
- import { EXIT, SkillError } from '../errors.js';
13
- export function runProcess(command, args, opts = {}) {
14
- const mode = opts.stdio ?? 'inherit';
15
- // 'stderr' = child output is human progress (npm install chatter), route it
16
- // to our stderr so our stdout stays the result/progress protocol.
17
- const stdio = mode === 'inherit' ? 'inherit' : mode === 'pipe' ? ['ignore', 'pipe', 'pipe'] : ['ignore', 'pipe', 'inherit'];
18
- return new Promise((resolve, reject) => {
19
- let stdout = '';
20
- let stderr = '';
21
- const child = spawn(command, args, { cwd: opts.cwd, stdio, env: opts.env ?? process.env, shell: opts.shell ?? false });
22
- child.stdout?.on('data', (d) => {
23
- if (mode === 'stderr')
24
- process.stderr.write(d);
25
- else
26
- stdout += d.toString();
27
- });
28
- child.stderr?.on('data', (d) => (stderr += d.toString()));
29
- child.on('error', (err) => reject(new SkillError(`❌ 无法启动 ${command}: ${err.message}`, EXIT.SPAWN_FAILED)));
30
- child.on('close', (code, signal) => resolve({ code: signal ? EXIT.ERROR : code ?? EXIT.ERROR, stdout, stderr }));
31
- });
32
- }
33
- const SAFE_SHELL_ARG = /^[A-Za-z0-9@._/:=+-]+$/;
34
- /** Run npm portably. `args` must be plain tokens; paths go through `cwd`. */
35
- export function runNpm(args, opts = {}) {
36
- if (process.platform !== 'win32')
37
- return runProcess('npm', args, opts);
38
- for (const a of args) {
39
- if (!SAFE_SHELL_ARG.test(a))
40
- throw new SkillError(`❌ 拒绝把不安全的参数交给 npm.cmd: ${a}`);
41
- }
42
- return runProcess('npm.cmd', args, { ...opts, shell: true });
43
- }
1
+ var a=Object.defineProperty;var s=(e,n)=>a(e,"name",{value:n,configurable:!0});import{spawn as E}from"node:child_process";import{EXIT as c,SkillError as l}from"../errors.js";function m(e,n,t={}){const i=t.stdio??"inherit",f=i==="inherit"?"inherit":i==="pipe"?["ignore","pipe","pipe"]:["ignore","pipe","inherit"];return new Promise((u,h)=>{let p="",d="";const o=E(e,n,{cwd:t.cwd,stdio:f,env:t.env??process.env,shell:t.shell??!1});o.stdout?.on("data",r=>{i==="stderr"?process.stderr.write(r):p+=r.toString()}),o.stderr?.on("data",r=>d+=r.toString()),o.on("error",r=>h(new l(`❌ 无法启动 ${e}: ${r.message}`,c.SPAWN_FAILED))),o.on("close",(r,w)=>u({code:w?c.ERROR:r??c.ERROR,stdout:p,stderr:d}))})}s(m,"runProcess");const R=/^[A-Za-z0-9@._/:=+-]+$/;function $(e,n={}){if(process.platform!=="win32")return m("npm",e,n);for(const t of e)if(!R.test(t))throw new l(`❌ 拒绝把不安全的参数交给 npm.cmd: ${t}`);return m("npm.cmd",e,{...n,shell:!0})}s($,"runNpm");export{$ as runNpm,m as runProcess};