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.
- package/README.md +135 -0
- package/aidrama.js +375 -0
- 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
|
+
}
|