aidrama-cli 1.0.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 (3) hide show
  1. package/README.md +135 -0
  2. package/aidrama.js +375 -0
  3. package/package.json +17 -0
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # ai-drama agent CLI(Node 版)
2
+
3
+ 用命令行驱动 **AI 短剧 Agent 流程**的基础能力。**仅依赖 Node 内置能力**(全局 `fetch`,无需 `npm install`),需 **Node >= 18**,尽量通用。
4
+
5
+ ## 运行
6
+
7
+ 无需安装依赖,直接用 Node 运行:
8
+
9
+ ```bash
10
+ node cli/aidrama.js --help
11
+ # 或加可执行权限后直接运行(首行已是 #!/usr/bin/env node)
12
+ chmod +x cli/aidrama.js
13
+ ./cli/aidrama.js --help
14
+ ```
15
+
16
+ 注册为全局命令(可选,零依赖):
17
+
18
+ ```bash
19
+ cd cli && npm link # 之后可直接用 `aidrama ...`
20
+ # 或手动软链: ln -s "$PWD/cli/aidrama.js" /usr/local/bin/aidrama
21
+ ```
22
+
23
+ ## 配置与凭据
24
+
25
+ 优先级:命令行参数 > 环境变量 > 配置文件 > 默认值。
26
+
27
+ | 项 | 命令行 | 环境变量 | 默认 |
28
+ |---|---|---|---|
29
+ | 后端基址 | `--base-url` | `AI_DRAMA_BASE_URL` | `http://localhost:38000` |
30
+ | 凭据 token | (登录后自动保存) | `AI_DRAMA_TOKEN` | 配置文件 |
31
+ | 配置文件 | — | `AI_DRAMA_CLI_CONFIG` | `~/.ai-drama-cli.json` |
32
+
33
+ ```bash
34
+ node cli/aidrama.js config set-base-url http://localhost:38000
35
+ node cli/aidrama.js config show
36
+ ```
37
+
38
+ ## 鉴权(飞书登录为主,兼容密码登录)
39
+
40
+ ### 飞书登录(推荐)
41
+
42
+ 飞书 OAuth 的回调地址固定指向前端页面,CLI 采用「manual-code」流:
43
+
44
+ ```bash
45
+ # 1) 打印授权链接
46
+ node cli/aidrama.js auth feishu
47
+ # 在浏览器打开链接 → 完成飞书授权 → 浏览器跳转到回调页(URL 带 code)
48
+
49
+ # 2) 用回调里的 code 换取 token(state 用上一步打印的)
50
+ node cli/aidrama.js auth feishu --code <code> --state <state>
51
+ # 或直接把整个回调 URL 丢进来,自动解析 code/state:
52
+ node cli/aidrama.js auth feishu --redirect-url 'http://localhost:5193/feishu/callback?code=xxx&state=yyy'
53
+ ```
54
+
55
+ > 需后端启用飞书登录(`AUTH_FEISHU_ENABLED=true` 且配置 FEISHU_APP_ID/SECRET/REDIRECT_URI)。用 `auth methods` 可查当前启用了哪些登录方式。
56
+
57
+ ### 用户名/密码登录(兼容,便于自动化/调试)
58
+
59
+ ```bash
60
+ node cli/aidrama.js auth login -u admin -p admin123
61
+ node cli/aidrama.js auth me # 查看当前用户、角色与权限
62
+ node cli/aidrama.js auth logout
63
+ ```
64
+
65
+ ## Agent 流程能力
66
+
67
+ ```bash
68
+ # 项目
69
+ node cli/aidrama.js projects list
70
+ node cli/aidrama.js projects create "我的项目"
71
+
72
+ # 渠道(仅列启用的;可按能力过滤)
73
+ node cli/aidrama.js providers list --capability text # text | image | video
74
+
75
+ # 原文内容(pipeline 入口)
76
+ node cli/aidrama.js content set 2 --file novel.txt # 或 --text "..."
77
+ node cli/aidrama.js content get 2
78
+
79
+ # 元素推理 / 列表(--wait 触发后轮询到完成)
80
+ node cli/aidrama.js elements run 2 --wait
81
+ node cli/aidrama.js elements list 2
82
+
83
+ # 图片生成(不传 --element-id = 批量基础态;传则单个/状态图)
84
+ node cli/aidrama.js images run 2 --wait
85
+ node cli/aidrama.js images run 2 --element-id 333 --provider-id 5 --wait
86
+
87
+ # 剧集拆解
88
+ node cli/aidrama.js episodes run 2 --wait
89
+ node cli/aidrama.js episodes list 2
90
+
91
+ # 分镜策划
92
+ node cli/aidrama.js storyboards run 2 --episode-id 10 --wait
93
+ node cli/aidrama.js storyboards list 2 --episode-id 10
94
+
95
+ # 视频生成
96
+ node cli/aidrama.js videos run 2 --storyboard-ids 67 68 69 --provider-id 5 --gen-mode multi_ref --wait
97
+ node cli/aidrama.js videos list 2
98
+
99
+ # 直连视频渠道生视频(默认首个启用视频渠道, 如 seedance 2.0)—— 纯提示词→视频, 不依赖分镜
100
+ node cli/aidrama.js videos raw --prompt "一只橘猫在窗台伸懒腰,暖阳,电影质感" --duration 5 --aspect-ratio 9:16 --wait
101
+ # 多模态参考(图/视频/音频);音频不能单独, 须与图或视频同传
102
+ node cli/aidrama.js videos raw --prompt "她转身微笑" --ref-images https://.../a.png --wait
103
+ node cli/aidrama.js videos raw --prompt "从A过渡到B" --gen-mode first_last_frame --ref-images https://.../first.png https://.../last.png --wait
104
+ node cli/aidrama.js videos raw --prompt "镜头缓缓推进" --ref-videos https://.../ref.mp4 --aspect-ratio 9:16 --wait
105
+ node cli/aidrama.js videos raw --prompt "角色说话" --ref-images https://.../face.png --ref-audios https://.../voice.mp3 --wait
106
+ # --out: 自动等待生成完成并把成片下载到本地
107
+ node cli/aidrama.js videos raw --prompt "夜雨霓虹的赛博街道" --aspect-ratio 16:9 --out ./out.mp4
108
+
109
+ # 会话状态
110
+ node cli/aidrama.js session status <session_id>
111
+ node cli/aidrama.js session wait <session_id> --interval 3
112
+ ```
113
+
114
+ ## 通用透传(最大化通用性)
115
+
116
+ 任何后端接口都能直接调(自动带 token):
117
+
118
+ ```bash
119
+ node cli/aidrama.js api GET /api/agent/projects/2/elements
120
+ node cli/aidrama.js api POST /api/agent/elements/run --data '{"project_id":2}'
121
+ node cli/aidrama.js api GET /api/providers --query capability=image
122
+ ```
123
+
124
+ ## 输出
125
+
126
+ 默认美化 JSON;加 `--json` 输出单行紧凑 JSON,便于管道处理(配合 `jq` 等):
127
+
128
+ ```bash
129
+ node cli/aidrama.js --json projects list | jq '.[].id'
130
+ ```
131
+
132
+ ## 退出码
133
+
134
+ - `0` 成功
135
+ - 非 0:HTTP 错误(打印 `HTTP <code>: <detail>`)、连接失败、未登录等
package/aidrama.js ADDED
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * ai-drama agent CLI(Node 版)—— 用命令行驱动 AI 短剧 Agent 流程的基础能力。
5
+ *
6
+ * 特点:
7
+ * - 仅用 Node 内置能力(全局 fetch / fs / os / path),零三方依赖,需 Node >= 18。
8
+ * - 鉴权以「飞书登录」为主(manual-code 流),同时兼容后端用户名/密码登录,便于自动化/调试。
9
+ * - 覆盖 agent 流程基础能力:项目、内容上传、元素推理、图片生成、剧集拆解、分镜、视频、会话轮询、渠道。
10
+ * - 内置通用透传命令 `api`,可直接调任意后端接口,最大化通用性。
11
+ *
12
+ * 配置与凭据(优先级:命令行 > 环境变量 > 配置文件 > 默认):
13
+ * - base_url:--base-url / 环境 AI_DRAMA_BASE_URL / 配置文件 / 默认 http://localhost:38000
14
+ * - token: 环境 AI_DRAMA_TOKEN / 配置文件(登录后自动写入)
15
+ * - 配置文件:环境 AI_DRAMA_CLI_CONFIG / 默认 ~/.ai-drama-cli.json
16
+ */
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const path = require('path');
20
+
21
+ const DEFAULT_BASE_URL = 'http://localhost:38000';
22
+ const CONFIG_PATH = process.env.AI_DRAMA_CLI_CONFIG || path.join(os.homedir(), '.ai-drama-cli.json');
23
+
24
+ // ---------------- 配置 / 凭据 ----------------
25
+ function loadCfg() {
26
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { return {}; }
27
+ }
28
+ function saveCfg(cfg) {
29
+ try {
30
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
31
+ try { fs.chmodSync(CONFIG_PATH, 0o600); } catch { /* 凭据文件权限收紧,失败忽略 */ }
32
+ } catch (e) { eprint(`警告:无法写入配置 ${CONFIG_PATH}: ${e.message}`); }
33
+ }
34
+ function getBase(flags) {
35
+ return (flags['base-url'] || process.env.AI_DRAMA_BASE_URL || loadCfg().base_url || DEFAULT_BASE_URL)
36
+ .replace(/\/+$/, '');
37
+ }
38
+ function getToken() { return process.env.AI_DRAMA_TOKEN || loadCfg().token || null; }
39
+ function setToken(t) { const c = loadCfg(); c.token = t; saveCfg(c); }
40
+
41
+ // ---------------- 输出 ----------------
42
+ function eprint(...a) { console.error(...a); }
43
+ function out(obj, jsonOut) {
44
+ if (obj !== null && typeof obj === 'object') console.log(JSON.stringify(obj, null, jsonOut ? 0 : 2));
45
+ else console.log(obj);
46
+ }
47
+ function fail(msg) { eprint(msg); process.exit(1); }
48
+ function num(v) { return v === undefined || v === null ? undefined : Number(v); }
49
+
50
+ // ---------------- HTTP ----------------
51
+ async function request(method, base, p, { token, body, params } = {}) {
52
+ let url = base.replace(/\/+$/, '') + '/' + String(p).replace(/^\/+/, '');
53
+ if (params) {
54
+ const q = new URLSearchParams();
55
+ for (const [k, v] of Object.entries(params)) {
56
+ if (v === undefined || v === null) continue;
57
+ q.append(k, Array.isArray(v) ? v.join(',') : String(v));
58
+ }
59
+ const s = q.toString();
60
+ if (s) url += '?' + s;
61
+ }
62
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
63
+ if (token) headers['Authorization'] = 'Bearer ' + token;
64
+ let resp;
65
+ try {
66
+ resp = await fetch(url, {
67
+ method: method.toUpperCase(),
68
+ headers,
69
+ body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
70
+ });
71
+ } catch (e) { fail(`连接失败 ${url}: ${e.message}`); }
72
+ const text = await resp.text();
73
+ let data;
74
+ try { data = text ? JSON.parse(text) : {}; } catch { data = text; }
75
+ if (!resp.ok) {
76
+ if (resp.status === 401) fail('HTTP 401 未授权:token 缺失或过期,请重新 `aidrama auth feishu` / `auth login`');
77
+ let detail = (data && typeof data === 'object' && 'detail' in data) ? data.detail : (text || resp.statusText);
78
+ if (typeof detail === 'object') detail = JSON.stringify(detail, null, 0);
79
+ fail(`HTTP ${resp.status}: ${detail}`);
80
+ }
81
+ return data;
82
+ }
83
+ function authToken() {
84
+ const t = getToken();
85
+ if (!t) fail('未登录,请先执行 `aidrama auth feishu` 或 `aidrama auth login -u <user> -p <pass>`');
86
+ return t;
87
+ }
88
+ function authRequest(method, base, p, opt = {}) { return request(method, base, p, { token: authToken(), ...opt }); }
89
+
90
+ // 下载文件到本地(用于 --out:把成片直接存到本地)
91
+ async function downloadFile(url, dest) {
92
+ let resp;
93
+ try { resp = await fetch(url); } catch (e) { fail(`下载失败 ${url}: ${e.message}`); }
94
+ if (!resp.ok) fail(`下载失败 HTTP ${resp.status}: ${url}`);
95
+ const buf = Buffer.from(await resp.arrayBuffer());
96
+ fs.writeFileSync(dest, buf);
97
+ return { path: path.resolve(dest), bytes: buf.length };
98
+ }
99
+
100
+ // ---------------- session 轮询 ----------------
101
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
102
+ async function waitSession(base, sid, interval = 3, maxWait = 1800) {
103
+ const deadline = Date.now() + maxWait * 1000;
104
+ let last = null;
105
+ while (Date.now() < deadline) {
106
+ const st = await authRequest('GET', base, `/api/agent/sessions/${sid}/status`);
107
+ const report = st.validation_report || {};
108
+ const prog = report.progress;
109
+ let line = ` · status=${st.status}`;
110
+ if (prog) line += ` progress=${prog.done}/${prog.total}`;
111
+ if (line !== last) { eprint(line); last = line; }
112
+ if (st.status === 'completed' || st.status === 'failed') return st;
113
+ await sleep(interval * 1000);
114
+ }
115
+ eprint(' · 轮询超时,请稍后用 `aidrama session status` 查看');
116
+ return { status: 'timeout', session_id: sid };
117
+ }
118
+ async function maybeWait(flags, base, res) {
119
+ const sid = res && res.session_id;
120
+ const wantOut = !!flags.out; // --out 需要最终 video_url,故隐含等待完成
121
+ if ((flags.wait || wantOut) && sid) {
122
+ eprint(`已触发 session=${sid},轮询中…`);
123
+ const st = await waitSession(base, sid);
124
+ if (wantOut) {
125
+ const url = st && st.validation_report && st.validation_report.video_url;
126
+ if (url) {
127
+ const info = await downloadFile(url, flags.out);
128
+ eprint(`已下载成片到 ${info.path}(${(info.bytes / 1048576).toFixed(2)} MB)`);
129
+ st.saved_to = info.path;
130
+ } else {
131
+ eprint('注意:会话结果无 video_url,--out 仅适用于产出单个视频的命令(如 videos raw)。');
132
+ }
133
+ }
134
+ out(st, flags.json);
135
+ } else out(res, flags.json);
136
+ }
137
+
138
+ // ---------------- 参数解析 ----------------
139
+ const BOOL_FLAGS = new Set(['json', 'wait', 'help']);
140
+ const MULTI_FLAGS = new Set(['storyboard-ids', 'query', 'ref-images', 'ref-videos', 'ref-audios']);
141
+ const ALIAS = { '-u': '--username', '-p': '--password', '-h': '--help' };
142
+ function parseArgs(argv) {
143
+ const positionals = [];
144
+ const flags = {};
145
+ for (let i = 0; i < argv.length; i++) {
146
+ let a = argv[i];
147
+ if (ALIAS[a]) a = ALIAS[a];
148
+ if (a.startsWith('--')) {
149
+ let key = a.slice(2);
150
+ if (key.includes('=')) { const idx = key.indexOf('='); flags[key.slice(0, idx)] = key.slice(idx + 1); continue; }
151
+ if (BOOL_FLAGS.has(key)) { flags[key] = true; continue; }
152
+ if (MULTI_FLAGS.has(key)) {
153
+ const vals = [];
154
+ while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) vals.push(argv[++i]);
155
+ flags[key] = vals;
156
+ continue;
157
+ }
158
+ flags[key] = argv[++i];
159
+ } else positionals.push(a);
160
+ }
161
+ return { positionals, flags };
162
+ }
163
+
164
+ const USAGE = `aidrama —— AI 短剧 Agent 流程命令行(飞书登录鉴权,尽量通用)
165
+
166
+ 用法: aidrama [--base-url URL] [--json] <分组> <命令> [参数...]
167
+
168
+ 鉴权:
169
+ auth methods 查询后端启用的登录方式
170
+ auth login -u <user> -p <pass> 用户名/密码登录(兼容)
171
+ auth feishu [--code C --state S | --redirect-url URL] 飞书登录
172
+ auth me 当前用户(含 roles/permissions)
173
+ auth logout 清除本地 token
174
+ 配置:
175
+ config show
176
+ config set-base-url <url>
177
+ 项目/渠道/内容:
178
+ projects list | projects create <name>
179
+ providers list [--capability text|image|video]
180
+ content get <pid> | content set <pid> [--text T | --file F] [--content-type novel]
181
+ Agent 步骤(--wait 触发后轮询到完成):
182
+ elements run <pid> [--provider-id N] [--wait] | elements list <pid>
183
+ images run <pid> [--element-id N] [--state-id N] [--provider-id N] [--wait]
184
+ episodes run <pid> [--provider-id N] [--wait] | episodes list <pid>
185
+ storyboards run <pid> [--episode-id N] [--provider-id N] [--wait] | storyboards list <pid> [--episode-id N]
186
+ videos run <pid> --storyboard-ids A B C --provider-id N [--gen-mode multi_ref] [--resolution R] [--wait] | videos list <pid>
187
+ videos raw --prompt "..." [--provider-id N] [--duration 5] [--aspect-ratio 9:16] [--resolution 720p] [--ref-images URL...] [--ref-videos URL...] [--ref-audios URL...] [--gen-mode multi_ref] [--idempotency-key K] [--wait] [--out FILE] 直连视频渠道(默认 seedance 2.0)生视频, 支持多模态参考(图/视频/音频); --out 自动等待并把成片下载到本地
188
+ 会话:
189
+ session status <sid> | session wait <sid> [--interval 3]
190
+ 通用透传:
191
+ api <GET|POST|PUT|DELETE> <path> [--data JSON] [--query k=v ...]
192
+ `;
193
+
194
+ // ---------------- 命令实现 ----------------
195
+ async function dispatch({ positionals, flags }) {
196
+ const [group, cmd] = positionals;
197
+ const base = getBase(flags);
198
+ const A = positionals; // 位置参数简写
199
+
200
+ if (group === 'api') {
201
+ const method = A[1], pth = A[2];
202
+ if (!method || !pth) fail('用法: aidrama api <GET|POST|PUT|DELETE> <path> [--data JSON] [--query k=v ...]');
203
+ const body = flags.data ? JSON.parse(flags.data) : undefined;
204
+ const params = {};
205
+ for (const kv of (flags.query || [])) { const i = kv.indexOf('='); if (i > 0) params[kv.slice(0, i)] = kv.slice(i + 1); }
206
+ return out(await authRequest(method, base, pth, { body, params: Object.keys(params).length ? params : undefined }), flags.json);
207
+ }
208
+
209
+ switch (`${group} ${cmd}`) {
210
+ // auth
211
+ case 'auth methods':
212
+ return out(await request('GET', base, '/api/auth/methods'), flags.json);
213
+ case 'auth login': {
214
+ if (!flags.username || !flags.password) fail('用法: aidrama auth login -u <user> -p <pass>');
215
+ const res = await request('POST', base, '/api/auth/login',
216
+ { body: { username: flags.username, password: flags.password } });
217
+ if (!res.access_token) fail('登录失败:' + JSON.stringify(res));
218
+ setToken(res.access_token);
219
+ eprint('登录成功,token 已保存。');
220
+ return out({ ok: true, token_prefix: res.access_token.slice(0, 12) + '…' }, flags.json);
221
+ }
222
+ case 'auth feishu': {
223
+ let code = flags.code, state = flags.state;
224
+ if (flags['redirect-url']) {
225
+ const u = new URL(flags['redirect-url']);
226
+ code = code || u.searchParams.get('code');
227
+ state = state || u.searchParams.get('state');
228
+ }
229
+ if (!code) {
230
+ const info = await request('GET', base, '/api/auth/feishu/authorize-url');
231
+ eprint('请在浏览器打开下面的飞书授权链接,完成授权后会跳转到回调页:\n');
232
+ eprint(' ' + (info.authorize_url || '(后端未配置飞书登录)') + '\n');
233
+ eprint('授权后,从回调地址取出 code,再执行:');
234
+ eprint(` aidrama auth feishu --code <code> --state ${info.state || ''}`);
235
+ eprint('或直接粘贴整个回调 URL:');
236
+ eprint(" aidrama auth feishu --redirect-url '<回调完整URL>'");
237
+ return out({ authorize_url: info.authorize_url, state: info.state }, flags.json);
238
+ }
239
+ const res = await request('POST', base, '/api/auth/feishu/callback', { body: { code, state } });
240
+ if (!res.access_token) fail('飞书登录失败:' + JSON.stringify(res));
241
+ setToken(res.access_token);
242
+ eprint('飞书登录成功,token 已保存。');
243
+ return out({ ok: true, token_prefix: res.access_token.slice(0, 12) + '…' }, flags.json);
244
+ }
245
+ case 'auth me':
246
+ return out(await authRequest('GET', base, '/api/auth/me'), flags.json);
247
+ case 'auth logout': {
248
+ const c = loadCfg(); delete c.token; saveCfg(c);
249
+ return eprint('已登出,本地 token 已清除。');
250
+ }
251
+
252
+ // config
253
+ case 'config show': {
254
+ const c = loadCfg();
255
+ if (c.token) c.token = c.token.slice(0, 12) + '…';
256
+ return out({ config_path: CONFIG_PATH, base_url: base, ...c }, flags.json);
257
+ }
258
+ case 'config set-base-url': {
259
+ if (!A[2]) fail('用法: aidrama config set-base-url <url>');
260
+ const c = loadCfg(); c.base_url = A[2].replace(/\/+$/, ''); saveCfg(c);
261
+ return eprint(`base_url 已设为 ${c.base_url}`);
262
+ }
263
+
264
+ // projects
265
+ case 'projects list':
266
+ return out(await authRequest('GET', base, '/api/projects'), flags.json);
267
+ case 'projects create':
268
+ if (!A[2]) fail('用法: aidrama projects create <name>');
269
+ return out(await authRequest('POST', base, '/api/projects', { body: { name: A[2] } }), flags.json);
270
+
271
+ // providers
272
+ case 'providers list':
273
+ return out(await authRequest('GET', base, '/api/providers',
274
+ { params: { capability: flags.capability } }), flags.json);
275
+
276
+ // content
277
+ case 'content get':
278
+ return out(await authRequest('GET', base, `/api/agent/projects/${reqId(A[2])}/content`), flags.json);
279
+ case 'content set': {
280
+ let text = flags.text;
281
+ if (flags.file) text = fs.readFileSync(flags.file, 'utf8');
282
+ if (text === undefined) fail('请用 --text 或 --file 提供原文内容');
283
+ return out(await authRequest('POST', base, `/api/agent/projects/${reqId(A[2])}/content`,
284
+ { body: { original_text: text, content_type: flags['content-type'] || 'novel' } }), flags.json);
285
+ }
286
+
287
+ // elements
288
+ case 'elements run':
289
+ return maybeWait(flags, base, await authRequest('POST', base, '/api/agent/elements/run',
290
+ { body: { project_id: reqId(A[2]), provider_id: num(flags['provider-id']) } }));
291
+ case 'elements list':
292
+ return out(await authRequest('GET', base, `/api/agent/projects/${reqId(A[2])}/elements`), flags.json);
293
+
294
+ // images
295
+ case 'images run':
296
+ return maybeWait(flags, base, await authRequest('POST', base, '/api/agent/images/run',
297
+ { body: { project_id: reqId(A[2]), element_id: num(flags['element-id']),
298
+ provider_id: num(flags['provider-id']), state_id: num(flags['state-id']) } }));
299
+
300
+ // episodes
301
+ case 'episodes run':
302
+ return maybeWait(flags, base, await authRequest('POST', base, '/api/agent/episodes/run',
303
+ { body: { project_id: reqId(A[2]), provider_id: num(flags['provider-id']) } }));
304
+ case 'episodes list':
305
+ return out(await authRequest('GET', base, `/api/agent/projects/${reqId(A[2])}/episodes`), flags.json);
306
+
307
+ // storyboards
308
+ case 'storyboards run':
309
+ return maybeWait(flags, base, await authRequest('POST', base, '/api/agent/storyboards/run',
310
+ { body: { project_id: reqId(A[2]), episode_id: num(flags['episode-id']),
311
+ provider_id: num(flags['provider-id']) } }));
312
+ case 'storyboards list':
313
+ return out(await authRequest('GET', base, `/api/agent/projects/${reqId(A[2])}/storyboards`,
314
+ { params: { episode_id: num(flags['episode-id']) } }), flags.json);
315
+
316
+ // videos
317
+ case 'videos run': {
318
+ if (!flags['storyboard-ids'] || !flags['provider-id'])
319
+ fail('用法: aidrama videos run <pid> --storyboard-ids A B C --provider-id N');
320
+ return maybeWait(flags, base, await authRequest('POST', base, '/api/agent/videos/run',
321
+ { body: { project_id: reqId(A[2]), storyboard_ids: flags['storyboard-ids'].map(Number),
322
+ provider_id: num(flags['provider-id']), gen_mode: flags['gen-mode'] || 'multi_ref',
323
+ resolution: flags.resolution } }));
324
+ }
325
+ case 'videos list':
326
+ return out(await authRequest('GET', base, `/api/agent/projects/${reqId(A[2])}/videos`), flags.json);
327
+ case 'videos raw': {
328
+ if (!flags.prompt) fail('用法: aidrama videos raw --prompt "..." [--provider-id N] [--duration 5] [--aspect-ratio 9:16] [--resolution 720p] [--ref-images URL...] [--ref-videos URL...] [--ref-audios URL...] [--gen-mode multi_ref] [--negative-prompt "..."] [--idempotency-key K] [--wait] [--out FILE]');
329
+ const body = {
330
+ prompt: flags.prompt,
331
+ provider_id: num(flags['provider-id']),
332
+ project_id: num(flags['project-id']),
333
+ duration: flags.duration !== undefined ? Number(flags.duration) : undefined,
334
+ aspect_ratio: flags['aspect-ratio'],
335
+ resolution: flags.resolution,
336
+ ref_images: flags['ref-images'],
337
+ ref_videos: flags['ref-videos'],
338
+ ref_audios: flags['ref-audios'],
339
+ gen_mode: flags['gen-mode'],
340
+ negative_prompt: flags['negative-prompt'],
341
+ idempotency_key: flags['idempotency-key'],
342
+ };
343
+ return maybeWait(flags, base, await authRequest('POST', base, '/api/agent/videos/raw', { body }));
344
+ }
345
+
346
+ // session
347
+ case 'session status':
348
+ return out(await authRequest('GET', base, `/api/agent/sessions/${reqStr(A[2])}/status`), flags.json);
349
+ case 'session wait':
350
+ return out(await waitSession(base, reqStr(A[2]), Number(flags.interval) || 3), flags.json);
351
+ }
352
+ // 未匹配
353
+ eprint(`未知命令: ${group || ''} ${cmd || ''}`.trim());
354
+ eprint('\n' + USAGE);
355
+ process.exit(2);
356
+ }
357
+
358
+ function reqId(v) { if (v === undefined) fail('缺少 project_id 参数'); return Number(v); }
359
+ function reqStr(v) { if (v === undefined) fail('缺少 session_id 参数'); return v; }
360
+
361
+ async function main() {
362
+ const argv = process.argv.slice(2);
363
+ const { positionals, flags } = parseArgs(argv);
364
+ if (flags.help || positionals.length === 0) { console.log(USAGE); return 0; }
365
+ await dispatch({ positionals, flags });
366
+ return 0;
367
+ }
368
+
369
+ main().catch((e) => {
370
+ if (e && e.code === 'EPIPE') process.exit(0); // 下游管道(head/jq)提前关闭:静默退出
371
+ eprint(e && e.stack ? e.stack : String(e));
372
+ process.exit(1);
373
+ });
374
+ // 下游管道关闭时 stdout 触发 EPIPE:静默退出,避免 traceback
375
+ process.stdout.on('error', (e) => { if (e.code === 'EPIPE') process.exit(0); });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "aidrama-cli",
3
+ "version": "1.0.0",
4
+ "description": "AI 短剧 Agent 流程命令行(飞书登录鉴权,零三方依赖,Node>=18)",
5
+ "bin": {
6
+ "aidrama": "aidrama.js"
7
+ },
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
11
+ "license": "MIT",
12
+ "files": [
13
+ "aidrama.js",
14
+ "README.md"
15
+ ],
16
+ "private": false
17
+ }