@baomihuatop/popcorn-cli 0.1.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 ADDED
@@ -0,0 +1,130 @@
1
+ # popcorn-cli
2
+
3
+ Popcorn 官方命令行客户端。通过本地配置的 API Key 调用 Popcorn 服务,用于查询可用模型、提交生图 / 生视频任务,以及查询任务状态和结果。
4
+
5
+ ## 安装
6
+
7
+ 需要 Node.js 18 或更高版本。
8
+
9
+ ```bash
10
+ npm install -g @baomihuatop/popcorn-cli
11
+ ```
12
+
13
+ 安装后确认命令可用:
14
+
15
+ ```bash
16
+ popcorn-cli --version
17
+ popcorn-cli --help
18
+ ```
19
+
20
+ ## 配置 API Key
21
+
22
+ ```bash
23
+ popcorn-cli config set-key <YOUR_API_KEY>
24
+ ```
25
+
26
+ 查看当前配置:
27
+
28
+ ```bash
29
+ popcorn-cli config show
30
+ ```
31
+
32
+ API Key 会保存在本机配置文件中:`~/.popcorn-cli/config.json`。
33
+
34
+ ## 查询可用模型
35
+
36
+ 提交任务前,先查询当前 API Key 可用的模型和参数 schema。
37
+
38
+ ```bash
39
+ popcorn-cli image models
40
+ popcorn-cli video models
41
+ ```
42
+
43
+ 返回内容包含模型信息、使用限制和提交任务所需的参数结构。
44
+
45
+ ## 提交任务
46
+
47
+ 生图任务:
48
+
49
+ ```bash
50
+ popcorn-cli image submit -p '{"model":"sd-xl-v1","prompt":"a cat"}'
51
+ ```
52
+
53
+ 生视频任务:
54
+
55
+ ```bash
56
+ popcorn-cli video submit -p '{"model":"video-model-id","prompt":"a cinematic cat"}'
57
+ ```
58
+
59
+ 关联会话 ID:
60
+
61
+ ```bash
62
+ popcorn-cli image submit -s <SESSION_ID> -p '{"model":"sd-xl-v1","prompt":"a cat"}'
63
+ ```
64
+
65
+ `-p, --params` 必须是 JSON 对象字符串。具体字段以 `image models` / `video models` 返回的 `params_schema` 为准。
66
+
67
+ ## 查询任务
68
+
69
+ 按任务 ID 查询单个任务:
70
+
71
+ ```bash
72
+ popcorn-cli task list -t <TASK_ID>
73
+ ```
74
+
75
+ 按会话 ID 查询该会话下的任务:
76
+
77
+ ```bash
78
+ popcorn-cli task list -s <SESSION_ID>
79
+ ```
80
+
81
+ ## 常用命令
82
+
83
+ | 命令 | 说明 |
84
+ |------|------|
85
+ | `popcorn-cli config set-key <apiKey>` | 设置 API Key |
86
+ | `popcorn-cli config show` | 查看当前配置 |
87
+ | `popcorn-cli image models` | 查询可用生图模型 |
88
+ | `popcorn-cli image submit -p <JSON>` | 提交生图任务 |
89
+ | `popcorn-cli video models` | 查询可用生视频模型 |
90
+ | `popcorn-cli video submit -p <JSON>` | 提交生视频任务 |
91
+ | `popcorn-cli task list -t <TASK_ID>` | 按任务 ID 查询任务 |
92
+ | `popcorn-cli task list -s <SESSION_ID>` | 按会话 ID 查询任务 |
93
+
94
+ 所有命令都支持 `--help`:
95
+
96
+ ```bash
97
+ popcorn-cli --help
98
+ popcorn-cli image --help
99
+ popcorn-cli image submit --help
100
+ ```
101
+
102
+ ## 升级
103
+
104
+ ```bash
105
+ npm install -g @baomihuatop/popcorn-cli@latest
106
+ ```
107
+
108
+ ## 卸载
109
+
110
+ ```bash
111
+ npm uninstall -g @baomihuatop/popcorn-cli
112
+ ```
113
+
114
+ ## 常见问题
115
+
116
+ `popcorn-cli: command not found`
117
+
118
+ 确认 npm 全局安装目录已加入 `PATH`,或重新打开终端后再试。
119
+
120
+ `未设置 API Key`
121
+
122
+ 先执行:
123
+
124
+ ```bash
125
+ popcorn-cli config set-key <YOUR_API_KEY>
126
+ ```
127
+
128
+ `params 不是合法的 JSON`
129
+
130
+ `-p` 参数必须传入合法 JSON 对象字符串,建议用单引号包裹完整 JSON。
@@ -0,0 +1,192 @@
1
+ ---
2
+ name: popcorn-cli
3
+ description: popcorn-cli 是爆米花系统(Popcorn)的官方 CLI。需要生图、生视频时即可使用本工具:通过本地 API Key 查询可用模型、异步提交任务、按会话/任务 ID 查询最终结果。当要在终端、脚本或 Agent 流程中完成生图/生视频相关能力时使用此 skill。
4
+ version: 0.1.0
5
+ metadata:
6
+ openclaw:
7
+ requires:
8
+ bins:
9
+ - popcorn-cli
10
+ config:
11
+ - ~/.popcorn-cli/config.json
12
+ install:
13
+ - kind: node
14
+ package: "@baomihuatop/popcorn-cli"
15
+ bins:
16
+ - popcorn-cli
17
+ skillKey: popcorn-cli
18
+ emoji: "🎬"
19
+ homepage: https://mangaforge-qa-1255521909.cos.ap-shanghai.myqcloud.com/docs/popcorn-cli/popcorn-cli-installation-guide.html
20
+ ---
21
+
22
+ # popcorn-cli
23
+
24
+ **popcorn-cli 是爆米花(Popcorn)的命令行客户端;需要生图、生视频时,直接使用本 CLI 即可完成模型查询、任务提交与结果查询。** 按任务类型分组组织命令,面向脚本化、自动化和 Agent 场景。
25
+
26
+ ## 概念
27
+
28
+ - **会话(session)**:一次业务上下文,`session_id` 由调用方生成(例如一次剧本推进、一次 Agent 会话)。同一 session 下可提交多个任务,便于统一追踪。
29
+ - **任务(task)**:一次具体的生成请求。`submit` 为异步接口,提交后立刻返回 `task_id`(不含最终结果);须再用 `task list` 按 `task_id` / `session_id` 查询状态与结果。
30
+ - **模型(model)**:每个任务类型下有多个可用模型,模型自带使用限制(时长、分辨率等)。可用模型清单与 API Key 所属租户绑定,仅返回当前租户开通且启用中的模型。提交任务前建议先用 `<group> models` 查询目标场景的可用模型。
31
+
32
+ ## 能力概览
33
+
34
+ **模型查询 / 任务提交**(按任务类型两级分组,当前均为 mock 实现,接口签名一致):
35
+
36
+ | 任务类型 | 查询模型 | 提交任务 |
37
+ |----------|----------|----------|
38
+ | 生图 | `popcorn-cli image models` | `popcorn-cli image submit` |
39
+ | 生视频 | `popcorn-cli video models` | `popcorn-cli video submit` |
40
+
41
+ 所有 `models` / `submit` 子命令的参数与返回结构在各任务类型间完全一致,见下文「命令详情」小节。
42
+
43
+ **任务查询**(跨任务类型统一入口;用于获取 `submit` 异步任务的最终结果):
44
+
45
+ | 命令 | 说明 |
46
+ |------|------|
47
+ | `popcorn-cli task list --sid <id>` | 查询某会话下的所有任务 |
48
+ | `popcorn-cli task list --tid <id>` | 查询单个任务(含 status / result) |
49
+
50
+ **配置管理**:
51
+
52
+ | 命令 | 说明 |
53
+ |------|------|
54
+ | `popcorn-cli config show` | 查看当前生效的配置 |
55
+ | `popcorn-cli config set-key <apiKey>` | 设置 API Key |
56
+
57
+ ## 认证与配置
58
+
59
+ CLI 不接受在命令行显式传入 API Key,统一从本地配置读取。
60
+
61
+ **配置文件位置**:`~/.popcorn-cli/config.json`
62
+
63
+ **配置字段**:
64
+
65
+ | 字段 | 说明 |
66
+ |------|------|
67
+ | `apiKey` | API Key,请求时通过 `X-API-Key` 头传给后端 |
68
+
69
+ ## 命令详情
70
+
71
+ ### 查询模型:`image models` / `video models`
72
+
73
+ 查询某任务类型下当前租户可用的模型(仅返回当前 API Key 所属租户 + 场景匹配 + 启用中 + 非历史版本)。提交任务前应先调用本命令。
74
+
75
+ ```bash
76
+ popcorn-cli <image|video> models
77
+ ```
78
+
79
+ 无参数。返回结构:
80
+
81
+ ```jsonc
82
+ {
83
+ "type": "image",
84
+ "total": 2,
85
+ "models": [
86
+ {
87
+ "model_id": "...", // 模型唯一标识
88
+ "name": "...", // 展示名
89
+ "description": "...", // 简介
90
+ "model_limit": { ... } // 单模型使用限制(不同模型可能不同)
91
+ }
92
+ ],
93
+ "params_schema": { ... } // 该场景 submit 时 --params 的字段结构(同场景所有模型共用)
94
+ }
95
+ ```
96
+
97
+ `params_schema` 是 JSON Schema 风格描述,说明 `submit --params` 可用的字段、类型、默认值。同一场景(image / video)下所有模型共用一份。
98
+
99
+ **推荐流程**:如果不清楚模型跟入参,先 `<group> models` 查看当前租户可用模型清单和 `params_schema`,据此选定 `model_id`,再结合 `params_schema` 的字段构造 `submit` 的 `--params`。
100
+
101
+ ### 提交任务:`image submit` / `video submit`
102
+
103
+ 两类任务的 `submit` 子命令签名一致,仅调用不同后端接口。
104
+
105
+ > **异步接口**:`submit` 仅负责入队,立即返回 `task_id` 等受理信息,**不会等待生成完成,也不包含最终结果**。拿到 `task_id`(或传入的 `session_id`)后,须通过下方 `task list` 轮询任务状态,待 `status` 为终态后再读取 `result` / `error_message`。
106
+
107
+ ```bash
108
+ popcorn-cli <image|video> submit -p <JSON> [-s <SESSION_ID>]
109
+ ```
110
+
111
+ | 参数 | 缩写 | 必填 | 说明 |
112
+ |------|------|------|------|
113
+ | `--params` | `-p` | 是 | 任务参数,必须是 JSON 对象字符串 |
114
+ | `--sid` | `-s` | 否 | 会话 ID,用于将同一会话下的任务关联;不传则该任务不归属任何会话 |
115
+
116
+ 使用示例:
117
+
118
+ ```bash
119
+ popcorn-cli image submit -p '<按 params_schema 构造的 JSON>'
120
+ popcorn-cli video submit -p '<按 params_schema 构造的 JSON>' -s <SESSION_ID>
121
+ ```
122
+
123
+ `-p` 的字段结构由后端 `params_schema` 决定,同场景所有模型共用。使用前请先执行 `popcorn-cli <group> models` 查询当前租户可用的模型清单与 `params_schema`,再选定 `model_id` 并据 schema 构造 `--params`。
124
+
125
+ 返回结构(受理成功,任务已入队):
126
+
127
+ ```jsonc
128
+ {
129
+ "task_id": "...", // 任务唯一标识,用于后续 task list --tid 查询
130
+ "session_id": "...", // 传入的会话 ID;未传则为 null
131
+ "task_type": "image", // 任务类型:image / video
132
+ "created_at": "..." // 任务创建时间(ISO 8601)
133
+ }
134
+ ```
135
+
136
+ ### `task list` — 查询任务
137
+
138
+ 用于获取 `submit` 异步任务的最终状态与结果。提交成功后请用本命令轮询,不要假定 `submit` 返回里已有生成结果。
139
+
140
+ ```bash
141
+ popcorn-cli task list -s <SESSION_ID>
142
+ popcorn-cli task list -t <TASK_ID>
143
+ ```
144
+
145
+ | 参数 | 缩写 | 必填 | 说明 |
146
+ |------|------|------|------|
147
+ | `--sid` | `-s` | 二选一 | 按会话 ID 查询该会话下的所有任务 |
148
+ | `--tid` | `-t` | 二选一 | 按任务 ID 查询单个任务 |
149
+
150
+ `--sid` 与 `--tid` 必须提供其中之一。
151
+
152
+ 返回结构:
153
+
154
+ ```jsonc
155
+ {
156
+ "total": 1,
157
+ "tasks": [
158
+ {
159
+ "task_id": "...", // 任务唯一标识
160
+ "session_id": "...", // 会话 ID;未归属会话则为 null
161
+ "task_type": "image", // 任务类型:image / video
162
+ "created_at": "...", // 创建时间
163
+ "finished_at": "...", // 完成时间;未完成时可能为空
164
+ "status": "succeed", // 见下方状态说明
165
+ "result": "...", // 成功时的结果(如资源 URL 等);未完成或失败可能为空字符串
166
+ "error_message": "" // 失败时的错误信息;成功时通常为空字符串
167
+ }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ `status` 取值:
173
+
174
+ | 状态 | 说明 |
175
+ |------|------|
176
+ | `queued` | 已入队,等待执行 |
177
+ | `running` | 执行中 |
178
+ | `succeed` | 成功(终态,可读 `result`) |
179
+ | `failed` | 失败(终态,可读 `error_message`) |
180
+
181
+ ## 使用场景示例
182
+
183
+ - **CI/自动化脚本**:按 session 提交批量任务,再轮询 `task list` 直到终态
184
+ - **Agent 编排**:Agent 用同一 `session_id` 异步提交生图 / 生视频任务,之后统一 `task list --sid`(或 `--tid`)获取最终结果
185
+
186
+ ## 帮助信息
187
+
188
+ ```bash
189
+ popcorn-cli --help
190
+ popcorn-cli <group> --help # 如 popcorn-cli image --help / popcorn-cli task --help
191
+ popcorn-cli <group> <action> --help
192
+ ```
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Popcorn CLI"
3
+ short_description: "通过 popcorn-cli 命令行提交 Popcorn 任务并查询模型与任务状态"
4
+ default_prompt: "使用 $popcorn-cli 通过命令行提交 Popcorn 任务、查询可用模型或检查任务状态。"
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { buildConfigCommand } from '../src/commands/config.js';
5
+ import { buildImageCommand } from '../src/commands/image.js';
6
+ import { buildVideoCommand } from '../src/commands/video.js';
7
+ import { buildTaskCommand } from '../src/commands/task.js';
8
+
9
+ program
10
+ .name('popcorn-cli')
11
+ .description('Popcorn CLI - 通过 API Key 调用 Popcorn 后端服务')
12
+ .version('0.1.0');
13
+
14
+ program.addCommand(buildConfigCommand());
15
+ program.addCommand(buildImageCommand());
16
+ program.addCommand(buildVideoCommand());
17
+ program.addCommand(buildTaskCommand());
18
+
19
+ program.parseAsync(process.argv).catch((err) => {
20
+ console.error(err.message || err);
21
+ process.exit(1);
22
+ });
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@baomihuatop/popcorn-cli",
3
+ "version": "0.1.0",
4
+ "description": "Popcorn CLI - 通过 API Key 提交生图任务",
5
+ "type": "module",
6
+ "bin": {
7
+ "popcorn-cli": "bin/popcorn-cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/popcorn-cli.js"
11
+ },
12
+ "dependencies": {
13
+ "commander": "^12.1.0",
14
+ "node-fetch": "^3.3.2"
15
+ }
16
+ }
@@ -0,0 +1,52 @@
1
+ # popcorn-cli(爆米花) 安装指南
2
+
3
+ ## 前置条件
4
+
5
+ - Node.js ≥ 18
6
+ - npm ≥ 9
7
+
8
+ 先探测环境:
9
+
10
+ ```bash
11
+ node -v && npm -v
12
+ ```
13
+
14
+ 任一命令缺失或版本不达标时,先安装 / 升级 Node.js(推荐 nvm)后再继续。
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ npm install -g @baomihuatop/popcorn-cli
20
+ ```
21
+
22
+ `EACCES` 权限报错时:macOS/Linux 前加 `sudo`;Windows 使用管理员终端。或改用 nvm 管理的 Node(其全局目录一般在用户目录下,无需 sudo)。
23
+
24
+ ## 校验
25
+
26
+ ```bash
27
+ popcorn-cli --version
28
+ ```
29
+
30
+ 预期输出为版本号(例如 `0.1.0`)。返回该输出即视为安装成功。
31
+
32
+ `command not found` 时,检查 `npm bin -g` 输出的目录是否在 `PATH` 中。
33
+
34
+ ## 安装 Skill
35
+
36
+ CLI 安装并校验通过后,继续安装对应的 Agent Skill,以便在对话中按规范调用 `popcorn-cli` 能力:
37
+
38
+ ```bash
39
+ npx skills add https://clawhub.ai/zeyiy/skills/popcorn-cli
40
+ ```
41
+
42
+ ## 升级
43
+
44
+ ```bash
45
+ npm install -g @baomihuatop/popcorn-cli@latest
46
+ ```
47
+
48
+ ## 卸载
49
+
50
+ ```bash
51
+ npm uninstall -g @baomihuatop/popcorn-cli
52
+ ```
@@ -0,0 +1,75 @@
1
+ import { Command } from 'commander';
2
+ import { loadConfig, requireApiKey } from '../config.js';
3
+ import { requestApi } from '../http.js';
4
+
5
+ function parseParams(input) {
6
+ let value;
7
+ try {
8
+ value = JSON.parse(input);
9
+ } catch (err) {
10
+ throw new Error(`params 不是合法的 JSON: ${err.message}`);
11
+ }
12
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
13
+ throw new Error('params 必须是 JSON 对象');
14
+ }
15
+ return value;
16
+ }
17
+
18
+ /**
19
+ * 构建一个按任务类型组织的命令组,包含 submit / models 两个子命令
20
+ * @param {{
21
+ * group: string, // 命令名,例如 'image'
22
+ * groupDescription: string, // 命令描述
23
+ * taskType: string, // 后端识别用的任务类型,例如 'image'
24
+ * submitPath: string, // 提交任务的 API 路径
25
+ * submitDescription: string, // submit 子命令描述
26
+ * }} options
27
+ */
28
+ export function buildTaskGroup({
29
+ group,
30
+ groupDescription,
31
+ taskType,
32
+ submitPath,
33
+ submitDescription,
34
+ }) {
35
+ const cmd = new Command(group).description(groupDescription);
36
+
37
+ cmd
38
+ .command('submit')
39
+ .description(submitDescription)
40
+ .option('-s, --sid <sessionId>', '会话 ID,用于关联同一会话下的任务')
41
+ .requiredOption('-p, --params <json>', '任务参数(JSON 对象字符串)')
42
+ .action(async (options) => {
43
+ const config = loadConfig();
44
+ requireApiKey(config);
45
+
46
+ const params = parseParams(options.params);
47
+
48
+ const body = await requestApi(config, submitPath, {
49
+ method: 'POST',
50
+ body: {
51
+ ...(options.sid ? { session_id: options.sid } : {}),
52
+ params,
53
+ },
54
+ });
55
+
56
+ console.log(JSON.stringify(body?.data ?? {}, null, 2));
57
+ });
58
+
59
+ cmd
60
+ .command('models')
61
+ .description('查询该任务类型下所有可用模型(含使用限制与入参 schema)')
62
+ .action(async () => {
63
+ const config = loadConfig();
64
+ requireApiKey(config);
65
+
66
+ const body = await requestApi(config, '/api/models', {
67
+ method: 'GET',
68
+ query: { type: taskType },
69
+ });
70
+
71
+ console.log(JSON.stringify(body?.data ?? {}, null, 2));
72
+ });
73
+
74
+ return cmd;
75
+ }
@@ -0,0 +1,37 @@
1
+ import { Command } from 'commander';
2
+ import { loadConfig, saveConfig, getConfigPath } from '../config.js';
3
+
4
+ export function buildConfigCommand() {
5
+ const cmd = new Command('config').description('管理 CLI 配置(API Key、后端地址等)');
6
+
7
+ cmd
8
+ .command('show')
9
+ .description('查看当前生效的配置')
10
+ .action(() => {
11
+ const config = loadConfig();
12
+ const masked = config.apiKey
13
+ ? `${config.apiKey.slice(0, 5)}${'*'.repeat(Math.max(0, config.apiKey.length - 5))}`
14
+ : '(未设置)';
15
+ console.log(`配置文件: ${getConfigPath()}`);
16
+ console.log(`apiKey : ${masked}`);
17
+ console.log(`baseUrl : ${config.baseUrl}`);
18
+ });
19
+
20
+ cmd
21
+ .command('set-key <apiKey>')
22
+ .description('设置 API Key')
23
+ .action((apiKey) => {
24
+ saveConfig({ apiKey });
25
+ console.log('✓ 已保存 API Key');
26
+ });
27
+
28
+ cmd
29
+ .command('set-url <baseUrl>')
30
+ .description('设置后端服务地址')
31
+ .action((baseUrl) => {
32
+ saveConfig({ baseUrl });
33
+ console.log(`✓ 已保存 baseUrl: ${baseUrl}`);
34
+ });
35
+
36
+ return cmd;
37
+ }
@@ -0,0 +1,11 @@
1
+ import { buildTaskGroup } from './_group.js';
2
+
3
+ export function buildImageCommand() {
4
+ return buildTaskGroup({
5
+ group: 'image',
6
+ groupDescription: '生图相关任务',
7
+ taskType: 'image',
8
+ submitPath: '/api/generate/image',
9
+ submitDescription: '提交一个生图任务',
10
+ });
11
+ }
@@ -0,0 +1,33 @@
1
+ import { Command } from 'commander';
2
+ import { loadConfig, requireApiKey } from '../config.js';
3
+ import { requestApi } from '../http.js';
4
+
5
+ export function buildTaskCommand() {
6
+ const cmd = new Command('task').description('任务查询相关命令');
7
+
8
+ cmd
9
+ .command('list')
10
+ .description('按 sid 或 tid 查询任务列表')
11
+ .option('-s, --sid <sessionId>', '按会话 ID 查询该会话下的所有任务')
12
+ .option('-t, --tid <taskId>', '按任务 ID 查询单个任务')
13
+ .action(async (options) => {
14
+ if (!options.sid && !options.tid) {
15
+ throw new Error('必须提供 --sid 或 --tid 中的一个');
16
+ }
17
+
18
+ const config = loadConfig();
19
+ requireApiKey(config);
20
+
21
+ const body = await requestApi(config, '/api/tasks', {
22
+ method: 'GET',
23
+ query: {
24
+ session_id: options.sid,
25
+ task_id: options.tid,
26
+ },
27
+ });
28
+
29
+ console.log(JSON.stringify(body?.data ?? {}, null, 2));
30
+ });
31
+
32
+ return cmd;
33
+ }
@@ -0,0 +1,11 @@
1
+ import { buildTaskGroup } from './_group.js';
2
+
3
+ export function buildVideoCommand() {
4
+ return buildTaskGroup({
5
+ group: 'video',
6
+ groupDescription: '生视频相关任务',
7
+ taskType: 'video',
8
+ submitPath: '/api/generate/video',
9
+ submitDescription: '提交一个生视频任务',
10
+ });
11
+ }
package/src/config.js ADDED
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.popcorn-cli');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULT_CONFIG = {
9
+ apiKey: '',
10
+ baseUrl: 'https://mangaforge.svcs.n1q.cn',
11
+ };
12
+
13
+ export function getConfigPath() {
14
+ return CONFIG_FILE;
15
+ }
16
+
17
+ export function loadConfig() {
18
+ let fileConfig = {};
19
+ if (fs.existsSync(CONFIG_FILE)) {
20
+ try {
21
+ fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
22
+ } catch (err) {
23
+ throw new Error(`配置文件解析失败 (${CONFIG_FILE}): ${err.message}`);
24
+ }
25
+ }
26
+
27
+ return {
28
+ ...DEFAULT_CONFIG,
29
+ ...fileConfig,
30
+ };
31
+ }
32
+
33
+ export function saveConfig(patch) {
34
+ if (!fs.existsSync(CONFIG_DIR)) {
35
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
36
+ }
37
+ const current = fs.existsSync(CONFIG_FILE)
38
+ ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'))
39
+ : {};
40
+ const next = { ...current, ...patch };
41
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
42
+ return next;
43
+ }
44
+
45
+ export function requireApiKey(config) {
46
+ if (!config.apiKey) {
47
+ throw new Error('未配置 API Key。请先运行 `popcorn-cli config set-key <API_KEY>`。');
48
+ }
49
+ }
package/src/http.js ADDED
@@ -0,0 +1,55 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ /**
4
+ * 使用当前配置向后端发起请求
5
+ * @param {{ apiKey: string, baseUrl: string }} config
6
+ * @param {string} path 相对路径(以 / 开头)
7
+ * @param {object} [options] fetch 选项覆盖:method / body / query
8
+ */
9
+ export async function requestApi(config, path, options = {}) {
10
+ const { method = 'GET', body, query, headers = {}, ...rest } = options;
11
+
12
+ let url = `${config.baseUrl.replace(/\/$/, '')}${path}`;
13
+ if (query && Object.keys(query).length > 0) {
14
+ const search = new URLSearchParams();
15
+ for (const [k, v] of Object.entries(query)) {
16
+ if (v !== undefined && v !== null && v !== '') search.append(k, String(v));
17
+ }
18
+ const qs = search.toString();
19
+ if (qs) url += `?${qs}`;
20
+ }
21
+
22
+ const fetchOptions = {
23
+ method,
24
+ ...rest,
25
+ headers: {
26
+ 'X-API-Key': config.apiKey,
27
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
28
+ ...headers,
29
+ },
30
+ };
31
+ if (body !== undefined) {
32
+ fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
33
+ }
34
+
35
+ let res;
36
+ try {
37
+ res = await fetch(url, fetchOptions);
38
+ } catch (err) {
39
+ throw new Error(`请求失败:${err.message}`);
40
+ }
41
+
42
+ const text = await res.text();
43
+ let payload;
44
+ try {
45
+ payload = text ? JSON.parse(text) : {};
46
+ } catch {
47
+ throw new Error(`解析响应失败(HTTP ${res.status}):${text}`);
48
+ }
49
+
50
+ if (!res.ok) {
51
+ throw new Error(`请求失败(HTTP ${res.status}):${payload?.message ?? JSON.stringify(payload)}`);
52
+ }
53
+
54
+ return payload;
55
+ }