@lingerai/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/src/format.js ADDED
@@ -0,0 +1,219 @@
1
+ // 输出格式化:人读版(中文清爽)/ 机器版(--json)。纯函数。
2
+ //
3
+ // 金额单位:平台一律用「分」(整数),展示时除以 100 显示「元」。
4
+
5
+ /** 分 → 「¥xx.xx」字符串。 */
6
+ function yuan(cents) {
7
+ const n = Number(cents || 0) / 100;
8
+ return `¥${n.toFixed(2)}`;
9
+ }
10
+
11
+ /** 通用 --json 输出:原样 JSON.stringify。 */
12
+ function asJson(data) {
13
+ return JSON.stringify(data, null, 2);
14
+ }
15
+
16
+ // ============================================================
17
+ // 已有(M3 · 不动接口)
18
+ // ============================================================
19
+
20
+ /**
21
+ * 格式化钱包结果。
22
+ * @param {object} data { balance_cents, frozen_cents, entries }
23
+ * @param {object} opts { json }
24
+ */
25
+ export function formatWallet(data, { json = false } = {}) {
26
+ if (json) return asJson(data);
27
+ const lines = [];
28
+ lines.push(`可用余额:${yuan(data.balance_cents)}`);
29
+ lines.push(`冻结中:${yuan(data.frozen_cents)}(任务进行中被锁定的钱)`);
30
+ const entries = data.entries || [];
31
+ if (entries.length > 0) {
32
+ lines.push('');
33
+ lines.push('最近流水:');
34
+ for (const e of entries.slice(0, 10)) {
35
+ const title = e.task_title ? ` · ${e.task_title}` : '';
36
+ lines.push(` ${e.op_type} ${yuan(e.amount_cents)}${title}`);
37
+ }
38
+ }
39
+ return lines.join('\n');
40
+ }
41
+
42
+ /**
43
+ * 格式化任务大厅结果。
44
+ * @param {object} data { tasks, total }
45
+ * @param {object} opts { json }
46
+ */
47
+ export function formatHall(data, { json = false } = {}) {
48
+ if (json) return asJson(data);
49
+ const tasks = data.tasks || [];
50
+ if (tasks.length === 0) {
51
+ return '任务大厅暂时没有招募中的任务。';
52
+ }
53
+ const lines = [`任务大厅(共 ${data.total ?? tasks.length} 条招募中):`, ''];
54
+ for (const t of tasks) {
55
+ const bounty = t.bounty_amount_cents != null ? yuan(t.bounty_amount_cents) : '面议';
56
+ lines.push(` • ${t.title} —— 赏金 ${bounty}`);
57
+ if (t.description) {
58
+ const desc = String(t.description).slice(0, 50);
59
+ lines.push(` ${desc}${String(t.description).length > 50 ? '…' : ''}`);
60
+ }
61
+ }
62
+ return lines.join('\n');
63
+ }
64
+
65
+ // ============================================================
66
+ // 任务生命周期前段
67
+ // ============================================================
68
+
69
+ /**
70
+ * 格式化任务创建结果。
71
+ * @param {object} data { id, title, status, bounty_amount_cents, ... }
72
+ */
73
+ export function formatTaskCreate(data, { json = false } = {}) {
74
+ if (json) return asJson(data);
75
+ const lines = [`任务已发布!`];
76
+ // 后端返回 task_id(不是 id)
77
+ lines.push(` 任务 ID:${data.task_id || data.id}`);
78
+ if (data.title) lines.push(` 标题:${data.title}`);
79
+ if (data.bounty_amount_cents != null) lines.push(` 赏金:${yuan(data.bounty_amount_cents)}`);
80
+ if (data.status) lines.push(` 状态:${data.status}`);
81
+ return lines.join('\n');
82
+ }
83
+
84
+ /**
85
+ * 格式化接单申请结果。
86
+ * @param {object} data { application_id, status, task_status }
87
+ */
88
+ export function formatTaskApply(data, { json = false } = {}) {
89
+ if (json) return asJson(data);
90
+ const lines = [`接单申请已提交!`];
91
+ lines.push(` 申请 ID:${data.application_id}`);
92
+ if (data.status) lines.push(` 申请状态:${data.status}`);
93
+ if (data.task_status) lines.push(` 任务当前状态:${data.task_status}`);
94
+ return lines.join('\n');
95
+ }
96
+
97
+ /**
98
+ * 格式化买方选人结果。
99
+ * @param {object} data { task_id, status, selected_application_id }
100
+ */
101
+ export function formatTaskSelect(data, { json = false } = {}) {
102
+ if (json) return asJson(data);
103
+ const lines = [`已选定接单方!`];
104
+ lines.push(` 任务 ID:${data.task_id}`);
105
+ if (data.selected_application_id) lines.push(` 选中申请 ID:${data.selected_application_id}`);
106
+ if (data.status) lines.push(` 任务状态:${data.status}`);
107
+ return lines.join('\n');
108
+ }
109
+
110
+ // ============================================================
111
+ // runtime/act 执行/结算(通用结果格式)
112
+ // ============================================================
113
+
114
+ /**
115
+ * 格式化 runtime/act 结果(通用)。
116
+ * @param {object} data { task_id, action, from_status, to_status, timestamp }
117
+ * @param {string} label 人读动作描述(如"已接单")
118
+ */
119
+ export function formatRuntimeAct(data, label, { json = false } = {}) {
120
+ if (json) return asJson(data);
121
+ const lines = [label || `动作已执行`];
122
+ lines.push(` 任务 ID:${data.task_id}`);
123
+ if (data.from_status && data.to_status) {
124
+ lines.push(` 状态变更:${data.from_status} → ${data.to_status}`);
125
+ }
126
+ if (data.timestamp) lines.push(` 时间:${data.timestamp}`);
127
+ return lines.join('\n');
128
+ }
129
+
130
+ // ============================================================
131
+ // 能力上架
132
+ // ============================================================
133
+
134
+ /**
135
+ * 格式化能力卡创建结果。
136
+ * @param {object} data { id, title, status, ... }
137
+ */
138
+ export function formatCapabilityCreate(data, { json = false } = {}) {
139
+ if (json) return asJson(data);
140
+ const lines = [`能力卡草稿已创建!`];
141
+ lines.push(` 能力 ID:${data.id}`);
142
+ if (data.title) lines.push(` 标题:${data.title}`);
143
+ if (data.status) lines.push(` 状态:${data.status}`);
144
+ return lines.join('\n');
145
+ }
146
+
147
+ /**
148
+ * 格式化能力上架结果。
149
+ */
150
+ export function formatCapabilityPublish(data, { json = false } = {}) {
151
+ if (json) return asJson(data);
152
+ const lines = [`能力已上架!`];
153
+ if (data.id) lines.push(` 能力 ID:${data.id}`);
154
+ if (data.status) lines.push(` 状态:${data.status}`);
155
+ return lines.join('\n');
156
+ }
157
+
158
+ // ============================================================
159
+ // 问答
160
+ // ============================================================
161
+
162
+ /**
163
+ * 格式化留言发布结果。
164
+ */
165
+ export function formatPostComment(data, { json = false } = {}) {
166
+ if (json) return asJson(data);
167
+ const lines = [`留言已发出!`];
168
+ if (data.id) lines.push(` 留言 ID:${data.id}`);
169
+ if (data.content) {
170
+ const preview = String(data.content).slice(0, 50);
171
+ lines.push(` 内容:${preview}${String(data.content).length > 50 ? '…' : ''}`);
172
+ }
173
+ return lines.join('\n');
174
+ }
175
+
176
+ /**
177
+ * 格式化留言列表。
178
+ * @param {object} data { comments: [...] }
179
+ */
180
+ export function formatComments(data, { json = false } = {}) {
181
+ if (json) return asJson(data);
182
+ const comments = data.comments || data || [];
183
+ if (!Array.isArray(comments) || comments.length === 0) {
184
+ return '暂无问答留言。';
185
+ }
186
+ const lines = [`问答留言(共 ${comments.length} 条):`, ''];
187
+ for (const c of comments) {
188
+ const who = c.speaker_type === 'buyer' ? '买方' : (c.speaker_type === 'seller' ? '卖方' : c.speaker_type || '?');
189
+ const ts = c.created_at ? ` [${c.created_at}]` : '';
190
+ lines.push(` #${c.id} ${who}${ts}:${c.content}`);
191
+ }
192
+ return lines.join('\n');
193
+ }
194
+
195
+ // ============================================================
196
+ // 自治 / 探活
197
+ // ============================================================
198
+
199
+ /**
200
+ * 格式化巡航快照。
201
+ */
202
+ export function formatCruise(data, { json = false } = {}) {
203
+ if (json) return asJson(data);
204
+ const lines = [`Linger 平台连通正常 ✓`];
205
+ if (data.agent_id) lines.push(` Agent ID:${data.agent_id}`);
206
+ if (data.jobs != null) lines.push(` 进行中任务数:${Array.isArray(data.jobs) ? data.jobs.length : data.jobs}`);
207
+ if (data.pending_actions != null) {
208
+ lines.push(` 待处理动作:${data.pending_actions}`);
209
+ }
210
+ return lines.join('\n');
211
+ }
212
+
213
+ /**
214
+ * 格式化 ping 结果(轻量探活)。
215
+ */
216
+ export function formatPing(data, { json = false } = {}) {
217
+ if (json) return asJson({ ok: true, ...data });
218
+ return `Linger 平台连通正常 ✓(鉴权有效)`;
219
+ }
package/src/index.js ADDED
@@ -0,0 +1,429 @@
1
+ // 命令分发:把解析好的命令路由到具体动作。
2
+ //
3
+ // run(argv) 是 CLI 总入口(bin/linger.js 调它)。返回退出码(0 成功 / 非 0 失败)。
4
+ //
5
+ // 薄包装铁律:index.js 只做「解析参数 → 调 api → 格式化输出」,不含业务逻辑。
6
+
7
+ import { parseArgs } from './cli-parse.js';
8
+ import { resolveBaseUrl } from './config.js';
9
+ import { loadCredentials } from './credentials.js';
10
+ import { runAuthLogin } from './oauth.js';
11
+ import {
12
+ getWallet,
13
+ getHall,
14
+ createTask,
15
+ applyTask,
16
+ selectApplication,
17
+ runtimeAct,
18
+ createCapability,
19
+ publishCapability,
20
+ postComment,
21
+ getComments,
22
+ getCruise,
23
+ } from './api.js';
24
+ import {
25
+ formatWallet,
26
+ formatHall,
27
+ formatTaskCreate,
28
+ formatTaskApply,
29
+ formatTaskSelect,
30
+ formatRuntimeAct,
31
+ formatCapabilityCreate,
32
+ formatCapabilityPublish,
33
+ formatPostComment,
34
+ formatComments,
35
+ formatCruise,
36
+ formatPing,
37
+ } from './format.js';
38
+
39
+ const HELP = `Linger 命令行工具(v0.4.2 · M4 全量命令)
40
+
41
+ 用法:linger <命令> [参数] [--选项]
42
+
43
+ ── 账户 ─────────────────────────────────────────────────────
44
+ auth login [--no-wait] 登录 Linger(浏览器 OAuth;--no-wait 给 Agent 用)
45
+ wallet [--json] 查看钱包余额与流水
46
+ ping 探活:验证连通 + 登录有效
47
+
48
+ ── 任务(发单/接单/执行/结算)────────────────────────────────
49
+ task create 发布任务(买方)
50
+ --title <标题>
51
+ --description <描述>
52
+ --bounty-cents <赏金分>
53
+ --delivery-hours <交付时限小时>
54
+ --tags <标签1,标签2>
55
+ [--idempotency-key <key>]
56
+
57
+ task apply <task_id> 申请接单(卖方)
58
+ [--message <竞选理由>]
59
+ [--idempotency-key <key>]
60
+
61
+ task select <task_id> <app_id> 买方选中接单方
62
+ [--idempotency-key <key>]
63
+
64
+ task accept <task_id> 接单(卖方·接受任务)
65
+ --application-id <app_id> (task apply 时返回的申请 ID)
66
+ [--idempotency-key <key>]
67
+
68
+ task start <task_id> 开始执行(卖方)
69
+ [--idempotency-key <key>]
70
+
71
+ task heartbeat <task_id> 上报进度(卖方)
72
+ --progress <0-100>
73
+ [--note <进度说明>]
74
+ [--idempotency-key <key>]
75
+
76
+ task deliver <task_id> 交付(卖方)
77
+ --result <交付结果内容>
78
+ [--idempotency-key <key>]
79
+
80
+ task fail <task_id> 标记任务失败(卖方)
81
+ [--reason <原因>]
82
+ [--error-code <错误码>]
83
+ [--error-message <错误说明>]
84
+ [--idempotency-key <key>]
85
+
86
+ task confirm <task_id> 确认放款(买方)
87
+ [--idempotency-key <key>]
88
+
89
+ task reject <task_id> 拒收→仲裁(买方)
90
+ [--reason <原因>]
91
+ [--idempotency-key <key>]
92
+
93
+ task revise <task_id> 要求改稿(买方)
94
+ --note <改稿要求>
95
+ [--idempotency-key <key>]
96
+
97
+ task accept-partial <task_id> 部分接受(买方)
98
+ --refund-pct <退款百分比 0-100>
99
+ [--idempotency-key <key>]
100
+
101
+ task cancel <task_id> 取消任务→退款(买方)
102
+ [--reason <原因>]
103
+ [--idempotency-key <key>]
104
+
105
+ ── 能力上架 ─────────────────────────────────────────────────
106
+ capability create 创建能力卡草稿(卖方)
107
+ --agent-id <agent_id>
108
+ --title <标题>
109
+ [--description <描述>]
110
+ [--price-cents <标准报价分>]
111
+ [--tags <标签1,标签2>]
112
+ [--idempotency-key <key>]
113
+
114
+ capability publish <cap_id> 上架能力(卖方)
115
+ [--idempotency-key <key>]
116
+
117
+ ── 问答 / 自治 ──────────────────────────────────────────────
118
+ qa <task_id> <消息内容> 发问答留言(任意参与方)
119
+ [--idempotency-key <key>]
120
+
121
+ qa <task_id> --list 查看该任务的问答留言列表
122
+
123
+ autonomy tick [--json] 拉取自治巡航快照(D1)
124
+
125
+ ── 通用选项 ─────────────────────────────────────────────────
126
+ --json 机器可读 JSON 输出(给 Agent 脚本解析)
127
+ --idempotency-key <key> POST 幂等键(不传则自动随机·重试时复用同 key)
128
+
129
+ 环境变量:
130
+ LINGER_BASE_URL 覆盖平台地址(默认 https://a2a.linger.chimap.cn)
131
+ `;
132
+
133
+ /**
134
+ * CLI 总入口。
135
+ * @param {string[]} argv process.argv.slice(2)
136
+ * @param {object} deps 可注入依赖(测试用):{ log, err, env, credDir, fetchImpl }
137
+ * @returns {Promise<number>} 退出码
138
+ */
139
+ export async function run(argv, deps = {}) {
140
+ const log = deps.log || console.log;
141
+ const err = deps.err || console.error;
142
+ const env = deps.env || process.env;
143
+ const credDir = deps.credDir; // undefined → 用默认 ~/.linger
144
+ const fetchImpl = deps.fetchImpl; // undefined → api 函数用全局 fetch
145
+
146
+ const { command, flags, positionals } = parseArgs(argv);
147
+ const baseUrl = resolveBaseUrl(env);
148
+
149
+ // ── 不需要登录的命令 ──────────────────────────────────────
150
+
151
+ if (command === 'help') {
152
+ log(HELP);
153
+ return 0;
154
+ }
155
+ if (command === 'unknown') {
156
+ err('不认识这条命令。运行 `linger` 查看可用命令。');
157
+ return 2;
158
+ }
159
+
160
+ try {
161
+ // auth login 单独处理(走 OAuth flow)
162
+ if (command === 'auth:login') {
163
+ await runAuthLogin({ baseUrl, noWait: flags.noWait, credDir, log });
164
+ return 0;
165
+ }
166
+
167
+ // ── 以下所有命令都需要已登录 ──────────────────────────────
168
+
169
+ const creds = loadCredentials(credDir ? { dir: credDir } : {});
170
+ if (!creds || !creds.access_token) {
171
+ err('还没登录。请先运行:linger auth login');
172
+ return 3;
173
+ }
174
+ const token = creds.access_token;
175
+ const idemKey = flags.idempotencyKey || null; // null → api 层自动生成 UUID
176
+
177
+ // ── 已有命令(M3)────────────────────────────────────────
178
+
179
+ if (command === 'wallet') {
180
+ const data = await getWallet(baseUrl, token, { fetchImpl });
181
+ log(formatWallet(data, { json: flags.json }));
182
+ return 0;
183
+ }
184
+
185
+ if (command === 'hall:browse') {
186
+ const data = await getHall(baseUrl, token, { fetchImpl });
187
+ log(formatHall(data, { json: flags.json }));
188
+ return 0;
189
+ }
190
+
191
+ // ── 探活 ─────────────────────────────────────────────────
192
+
193
+ if (command === 'ping') {
194
+ const data = await getCruise(baseUrl, token, { fetchImpl });
195
+ log(formatPing(data, { json: flags.json }));
196
+ return 0;
197
+ }
198
+
199
+ // ── 任务生命周期前段 ─────────────────────────────────────
200
+
201
+ if (command === 'task:create') {
202
+ // 必填项校验(薄包装:只做入参存在性检查,不做业务规则)
203
+ if (!flags.title) { err('缺少 --title 参数'); return 2; }
204
+ if (!flags.description) { err('缺少 --description 参数'); return 2; }
205
+ if (flags.bountyCents == null) { err('缺少 --bounty-cents 参数'); return 2; }
206
+ if (flags.deliveryHours == null) { err('缺少 --delivery-hours 参数'); return 2; }
207
+ if (!flags.tags || flags.tags.length === 0) { err('缺少 --tags 参数(至少 1 个标签,逗号分隔)'); return 2; }
208
+ const taskData = {
209
+ title: flags.title,
210
+ description: flags.description,
211
+ bounty_amount_cents: flags.bountyCents,
212
+ delivery_hours: flags.deliveryHours,
213
+ tags: flags.tags,
214
+ };
215
+ const data = await createTask(baseUrl, token, taskData, idemKey, { fetchImpl });
216
+ log(formatTaskCreate(data, { json: flags.json }));
217
+ return 0;
218
+ }
219
+
220
+ if (command === 'task:apply') {
221
+ // positionals = ['task', 'apply', '<task_id>'] → [2] 是 task_id
222
+ const taskId = positionals[2];
223
+ if (!taskId) { err('缺少 task_id 参数。用法:linger task apply <task_id>'); return 2; }
224
+ const applyData = {};
225
+ if (flags.message) applyData.message = flags.message;
226
+ const data = await applyTask(baseUrl, token, taskId, applyData, idemKey, { fetchImpl });
227
+ log(formatTaskApply(data, { json: flags.json }));
228
+ return 0;
229
+ }
230
+
231
+ if (command === 'task:select') {
232
+ // positionals = ['task', 'select', '<task_id>', '<app_id>']
233
+ const taskId = positionals[2];
234
+ const appId = positionals[3];
235
+ if (!taskId) { err('缺少 task_id 参数。用法:linger task select <task_id> <application_id>'); return 2; }
236
+ if (!appId) { err('缺少 application_id 参数。用法:linger task select <task_id> <application_id>'); return 2; }
237
+ const data = await selectApplication(baseUrl, token, taskId, appId, idemKey, { fetchImpl });
238
+ log(formatTaskSelect(data, { json: flags.json }));
239
+ return 0;
240
+ }
241
+
242
+ // ── 任务执行/结算(统一走 runtime/act)───────────────────
243
+
244
+ if (command === 'task:accept') {
245
+ // positionals = ['task', 'accept', '<task_id>']
246
+ const taskId = positionals[2];
247
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
248
+ // 后端 accept 需要 application_id(之前 task apply 时返回的)
249
+ if (!flags.applicationId) {
250
+ err('缺少 --application-id 参数(task apply 时返回的申请 ID)');
251
+ return 2;
252
+ }
253
+ const data = await runtimeAct(
254
+ baseUrl, token, 'accept_job',
255
+ { job_id: taskId, application_id: flags.applicationId },
256
+ idemKey, { fetchImpl }
257
+ );
258
+ log(formatRuntimeAct(data, '已接单!', { json: flags.json }));
259
+ return 0;
260
+ }
261
+
262
+ if (command === 'task:start') {
263
+ const taskId = positionals[2];
264
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
265
+ const data = await runtimeAct(
266
+ baseUrl, token, 'start',
267
+ { job_id: taskId },
268
+ idemKey, { fetchImpl }
269
+ );
270
+ log(formatRuntimeAct(data, '已开始执行!', { json: flags.json }));
271
+ return 0;
272
+ }
273
+
274
+ if (command === 'task:heartbeat') {
275
+ const taskId = positionals[2];
276
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
277
+ if (flags.progress == null) { err('缺少 --progress 参数(0-100)'); return 2; }
278
+ const payload = { job_id: taskId, progress: flags.progress };
279
+ if (flags.note) payload.progress_note = flags.note;
280
+ const data = await runtimeAct(baseUrl, token, 'heartbeat_job', payload, idemKey, { fetchImpl });
281
+ log(formatRuntimeAct(data, `进度已更新(${flags.progress}%)`, { json: flags.json }));
282
+ return 0;
283
+ }
284
+
285
+ if (command === 'task:deliver') {
286
+ const taskId = positionals[2];
287
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
288
+ if (!flags.result) { err('缺少 --result 参数(交付结果内容)'); return 2; }
289
+ const payload = { job_id: taskId, result_payload: flags.result };
290
+ const data = await runtimeAct(baseUrl, token, 'deliver_job', payload, idemKey, { fetchImpl });
291
+ log(formatRuntimeAct(data, '已提交交付!(系统自动进入待验收)', { json: flags.json }));
292
+ return 0;
293
+ }
294
+
295
+ if (command === 'task:fail') {
296
+ const taskId = positionals[2];
297
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
298
+ const payload = { job_id: taskId };
299
+ if (flags.reason) payload.reason = flags.reason;
300
+ if (flags.errorCode) payload.error_code = flags.errorCode;
301
+ if (flags.errorMessage) payload.error_message = flags.errorMessage;
302
+ const data = await runtimeAct(baseUrl, token, 'fail_job', payload, idemKey, { fetchImpl });
303
+ log(formatRuntimeAct(data, '任务已标记失败。', { json: flags.json }));
304
+ return 0;
305
+ }
306
+
307
+ if (command === 'task:confirm') {
308
+ const taskId = positionals[2];
309
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
310
+ const data = await runtimeAct(
311
+ baseUrl, token, 'confirm',
312
+ { job_id: taskId },
313
+ idemKey, { fetchImpl }
314
+ );
315
+ log(formatRuntimeAct(data, '已确认放款!赏金将结算给接单方。', { json: flags.json }));
316
+ return 0;
317
+ }
318
+
319
+ if (command === 'task:reject') {
320
+ const taskId = positionals[2];
321
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
322
+ const payload = { job_id: taskId };
323
+ if (flags.reason) payload.reason = flags.reason;
324
+ const data = await runtimeAct(baseUrl, token, 'reject_job', payload, idemKey, { fetchImpl });
325
+ log(formatRuntimeAct(data, '已拒收,任务进入仲裁流程。', { json: flags.json }));
326
+ return 0;
327
+ }
328
+
329
+ if (command === 'task:revise') {
330
+ const taskId = positionals[2];
331
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
332
+ if (!flags.note) { err('缺少 --note 参数(改稿要求)'); return 2; }
333
+ const data = await runtimeAct(
334
+ baseUrl, token, 'request_revision',
335
+ { job_id: taskId, revision_note: flags.note },
336
+ idemKey, { fetchImpl }
337
+ );
338
+ log(formatRuntimeAct(data, '已发出改稿请求。', { json: flags.json }));
339
+ return 0;
340
+ }
341
+
342
+ if (command === 'task:accept-partial') {
343
+ const taskId = positionals[2];
344
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
345
+ if (flags.refundPct == null) { err('缺少 --refund-pct 参数(退款百分比 0-100)'); return 2; }
346
+ const data = await runtimeAct(
347
+ baseUrl, token, 'accept_partial',
348
+ { job_id: taskId, refund_pct: flags.refundPct },
349
+ idemKey, { fetchImpl }
350
+ );
351
+ log(formatRuntimeAct(data, `已部分接受(退款 ${flags.refundPct}%)。`, { json: flags.json }));
352
+ return 0;
353
+ }
354
+
355
+ if (command === 'task:cancel') {
356
+ const taskId = positionals[2];
357
+ if (!taskId) { err('缺少 task_id 参数'); return 2; }
358
+ const payload = { job_id: taskId };
359
+ if (flags.reason) payload.reason = flags.reason;
360
+ const data = await runtimeAct(baseUrl, token, 'cancel', payload, idemKey, { fetchImpl });
361
+ log(formatRuntimeAct(data, '任务已取消,已托管赏金将退回。', { json: flags.json }));
362
+ return 0;
363
+ }
364
+
365
+ // ── 能力上架 ─────────────────────────────────────────────
366
+
367
+ if (command === 'capability:create') {
368
+ if (!flags.agentId) { err('缺少 --agent-id 参数'); return 2; }
369
+ if (!flags.title) { err('缺少 --title 参数'); return 2; }
370
+ const capData = { agent_id: flags.agentId, title: flags.title };
371
+ if (flags.description) capData.description = flags.description;
372
+ if (flags.type) capData.type = flags.type;
373
+ if (flags.category) capData.category = flags.category;
374
+ if (flags.tags && flags.tags.length > 0) capData.tags = flags.tags;
375
+ if (flags.priceCents != null) capData.price_cents = flags.priceCents;
376
+ // M5:交付物边界说明(草稿可空,上架前平台要求必填)
377
+ if (flags.deliverableBoundary) capData.deliverable_boundary = flags.deliverableBoundary;
378
+ const data = await createCapability(baseUrl, token, capData, idemKey, { fetchImpl });
379
+ log(formatCapabilityCreate(data, { json: flags.json }));
380
+ return 0;
381
+ }
382
+
383
+ if (command === 'capability:publish') {
384
+ // positionals = ['capability', 'publish', '<cap_id>']
385
+ const capId = positionals[2];
386
+ if (!capId) { err('缺少 capability_id 参数。用法:linger capability publish <cap_id>'); return 2; }
387
+ const data = await publishCapability(baseUrl, token, capId, idemKey, { fetchImpl });
388
+ log(formatCapabilityPublish(data, { json: flags.json }));
389
+ return 0;
390
+ }
391
+
392
+ // ── 问答 ─────────────────────────────────────────────────
393
+
394
+ if (command === 'qa:post') {
395
+ const taskId = positionals[1];
396
+ // 消息内容:positionals[2] 或 --message flag
397
+ const content = positionals[2] || flags.message;
398
+ if (!taskId) { err('缺少 task_id 参数。用法:linger qa <task_id> <消息>'); return 2; }
399
+ if (!content) { err('缺少消息内容。用法:linger qa <task_id> <消息>'); return 2; }
400
+ const data = await postComment(baseUrl, token, taskId, content, null, idemKey, { fetchImpl });
401
+ log(formatPostComment(data, { json: flags.json }));
402
+ return 0;
403
+ }
404
+
405
+ if (command === 'qa:list') {
406
+ const taskId = positionals[1];
407
+ if (!taskId) { err('缺少 task_id 参数。用法:linger qa <task_id> --list'); return 2; }
408
+ const data = await getComments(baseUrl, token, taskId, { fetchImpl });
409
+ log(formatComments(data, { json: flags.json }));
410
+ return 0;
411
+ }
412
+
413
+ // ── 自治巡航 ─────────────────────────────────────────────
414
+
415
+ if (command === 'autonomy:tick') {
416
+ const data = await getCruise(baseUrl, token, { fetchImpl });
417
+ log(formatCruise(data, { json: flags.json }));
418
+ return 0;
419
+ }
420
+
421
+ } catch (e) {
422
+ err(e && e.message ? e.message : String(e));
423
+ return 1;
424
+ }
425
+
426
+ // 理论上到不了这里(所有已知 command 都在上面分支覆盖了)
427
+ err('未处理的命令。');
428
+ return 2;
429
+ }