@lingerai/cli 0.1.2 → 0.3.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/package.json +1 -1
- package/src/api.js +17 -0
- package/src/cli-parse.js +36 -7
- package/src/format.js +44 -4
- package/src/index.js +169 -11
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -160,6 +160,23 @@ export async function runtimeAct(baseUrl, token, action, payload, idempotencyKey
|
|
|
160
160
|
// 能力上架(独立端点 · 卖方)
|
|
161
161
|
// ============================================================
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* 上报技能清单(卖方·A 流程第一步)。
|
|
165
|
+
* 把 agent 自己会的技能写进平台 agents.skill_catalog,供网页「AI 识别」挑能力卡。
|
|
166
|
+
* 用 agent token 调用时后端从 token 推导 agent_id(agent 不必知道自己 UUID)。
|
|
167
|
+
* @param {Array<{name:string,description:string}>} skills 技能清单
|
|
168
|
+
*/
|
|
169
|
+
export async function reportSkills(baseUrl, token, skills, idempotencyKey, opts = {}) {
|
|
170
|
+
return authedPost(
|
|
171
|
+
baseUrl,
|
|
172
|
+
'/api/v1/capabilities/skill_catalog',
|
|
173
|
+
token,
|
|
174
|
+
{ skills },
|
|
175
|
+
idempotencyKey,
|
|
176
|
+
opts
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
/**
|
|
164
181
|
* 创建能力卡草稿(卖方)。
|
|
165
182
|
* @param {object} capData CreateCapabilityRequest 字段(agent_id + title 必填,其余草稿可空)
|
package/src/cli-parse.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 命令解析:把 argv 解析成 { command, flags, raw, positionals }。纯函数、无副作用。
|
|
2
2
|
//
|
|
3
|
-
// 支持命令(M4 全量 + M5 deliverable-boundary + M7
|
|
3
|
+
// 支持命令(M4 全量 + M5 deliverable-boundary + M7 任务信息/文件能力 + M2 附件追加):
|
|
4
4
|
//
|
|
5
5
|
// 已实现(M3 · 不动)
|
|
6
6
|
// auth login [--no-wait] → 'auth:login'
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
// --mime-type <mime>
|
|
17
17
|
// --size-bytes <n>
|
|
18
18
|
//
|
|
19
|
+
// 附件追加(M2 新增 · 买方发布后补传需求材料)
|
|
20
|
+
// task add-attachment <task_id> → 'task:add-attachment'
|
|
21
|
+
// --file <本地路径> 必填
|
|
22
|
+
// [--visibility public|accepted-only] 缺省 accepted-only(中标后可见)
|
|
23
|
+
//
|
|
19
24
|
// 任务生命周期前段(独立 REST 端点)
|
|
20
25
|
// task create ... → 'task:create'
|
|
21
26
|
// task apply <task_id> → 'task:apply'
|
|
@@ -34,6 +39,7 @@
|
|
|
34
39
|
// task cancel <task_id> → 'task:cancel'
|
|
35
40
|
//
|
|
36
41
|
// 能力上架
|
|
42
|
+
// capability report-skills ... → 'capability:report-skills'(agent 上报技能清单)
|
|
37
43
|
// capability create ... → 'capability:create'
|
|
38
44
|
// capability publish <cap_id> → 'capability:publish'
|
|
39
45
|
//
|
|
@@ -93,12 +99,18 @@ export function parseArgs(argv) {
|
|
|
93
99
|
type: null, // capability create --type
|
|
94
100
|
category: null, // capability create --category
|
|
95
101
|
tags: null, // capability create --tags (逗号分隔)
|
|
96
|
-
priceCents: null, // capability create --price-cents
|
|
102
|
+
priceCents: null, // capability create --price-cents(兼容保留·已弃用)
|
|
103
|
+
price: null, // capability create --price(元·面向 agent 的友好单位)
|
|
104
|
+
skills: null, // capability report-skills --skills(JSON 数组字符串)
|
|
105
|
+
skillsFile: null, // capability report-skills --skills-file(JSON 文件路径)
|
|
97
106
|
title: null, // task create --title / capability create --title
|
|
98
107
|
taskTitle: null, // task create --title(同 title)
|
|
99
108
|
bountyCents: null, // task create --bounty-cents
|
|
100
109
|
deliveryHours: null, // task create --delivery-hours
|
|
101
110
|
taskTags: null, // task create --tags (逗号分隔)
|
|
111
|
+
// M2 新增 flag
|
|
112
|
+
file: null, // task add-attachment --file(本地文件路径)
|
|
113
|
+
visibility: null, // task add-attachment --visibility(public|accepted-only)
|
|
102
114
|
};
|
|
103
115
|
const positionals = [];
|
|
104
116
|
|
|
@@ -164,6 +176,15 @@ export function parseArgs(argv) {
|
|
|
164
176
|
} else if (a === '--price-cents') {
|
|
165
177
|
const v = args.shift();
|
|
166
178
|
flags.priceCents = v != null ? parseInt(v, 10) : null;
|
|
179
|
+
} else if (a === '--price') {
|
|
180
|
+
// 面向 agent 的友好单位:元(浮点)。index.js 里换算成分。
|
|
181
|
+
const v = args.shift();
|
|
182
|
+
flags.price = v != null ? parseFloat(v) : null;
|
|
183
|
+
} else if (a === '--skills') {
|
|
184
|
+
// capability report-skills:JSON 数组字符串 [{"name","description"}]
|
|
185
|
+
flags.skills = args.shift() ?? null;
|
|
186
|
+
} else if (a === '--skills-file') {
|
|
187
|
+
flags.skillsFile = args.shift() ?? null;
|
|
167
188
|
} else if (a === '--title') {
|
|
168
189
|
flags.title = args.shift() ?? null;
|
|
169
190
|
} else if (a === '--bounty-cents') {
|
|
@@ -172,6 +193,12 @@ export function parseArgs(argv) {
|
|
|
172
193
|
} else if (a === '--delivery-hours') {
|
|
173
194
|
const v = args.shift();
|
|
174
195
|
flags.deliveryHours = v != null ? parseInt(v, 10) : null;
|
|
196
|
+
} else if (a === '--file') {
|
|
197
|
+
// M2:task add-attachment 本地文件路径
|
|
198
|
+
flags.file = args.shift() ?? null;
|
|
199
|
+
} else if (a === '--visibility') {
|
|
200
|
+
// M2:task add-attachment 附件可见性(public|accepted-only)
|
|
201
|
+
flags.visibility = args.shift() ?? null;
|
|
175
202
|
} else if (a.startsWith('--')) {
|
|
176
203
|
// 未知开关:暂忽略
|
|
177
204
|
} else {
|
|
@@ -197,8 +224,9 @@ export function parseArgs(argv) {
|
|
|
197
224
|
// M7 新增:任务信息查询 + 文件能力
|
|
198
225
|
case 'show': command = 'task:show'; break;
|
|
199
226
|
case 'files': command = 'task:files'; break;
|
|
200
|
-
case 'download':
|
|
201
|
-
case 'request-upload':
|
|
227
|
+
case 'download': command = 'task:download'; break;
|
|
228
|
+
case 'request-upload': command = 'task:request-upload'; break;
|
|
229
|
+
case 'add-attachment': command = 'task:add-attachment'; break; // M2 新增
|
|
202
230
|
// 原有生命周期命令
|
|
203
231
|
case 'create': command = 'task:create'; break;
|
|
204
232
|
case 'apply': command = 'task:apply'; break;
|
|
@@ -217,9 +245,10 @@ export function parseArgs(argv) {
|
|
|
217
245
|
}
|
|
218
246
|
} else if (c0 === 'capability') {
|
|
219
247
|
switch (c1) {
|
|
220
|
-
case 'create':
|
|
221
|
-
case 'publish':
|
|
222
|
-
|
|
248
|
+
case 'create': command = 'capability:create'; break;
|
|
249
|
+
case 'publish': command = 'capability:publish'; break;
|
|
250
|
+
case 'report-skills': command = 'capability:report-skills'; break;
|
|
251
|
+
default: command = 'unknown';
|
|
223
252
|
}
|
|
224
253
|
} else if (c0 === 'qa') {
|
|
225
254
|
// qa <task_id> <message> 或 qa <task_id> --list
|
package/src/format.js
CHANGED
|
@@ -69,15 +69,27 @@ export function formatHall(data, { json = false } = {}) {
|
|
|
69
69
|
/**
|
|
70
70
|
* 格式化任务创建结果。
|
|
71
71
|
* @param {object} data { id, title, status, bounty_amount_cents, ... }
|
|
72
|
+
* @param {object} opts { json, webBase }
|
|
73
|
+
* webBase:任务详情页的网页根域名,缺省读 LINGER_WEB_BASE 环境变量,
|
|
74
|
+
* 再缺省用 https://a2a.linger.chimap.cn
|
|
72
75
|
*/
|
|
73
|
-
export function formatTaskCreate(data, { json = false } = {}) {
|
|
74
|
-
|
|
76
|
+
export function formatTaskCreate(data, { json = false, webBase } = {}) {
|
|
77
|
+
const taskId = data.task_id || data.id;
|
|
78
|
+
// 拼任务详情页链接(让 agent 能把链接转给用户)
|
|
79
|
+
const base = webBase || process.env.LINGER_WEB_BASE || 'https://a2a.linger.chimap.cn';
|
|
80
|
+
const taskUrl = `${base}/src/pages/task-detail/index.html?id=${taskId}`;
|
|
81
|
+
|
|
82
|
+
if (json) {
|
|
83
|
+
// json 模式也注入 task_detail_url,方便 agent 程序化读取
|
|
84
|
+
return asJson({ ...data, task_detail_url: taskUrl });
|
|
85
|
+
}
|
|
75
86
|
const lines = [`任务已发布!`];
|
|
76
87
|
// 后端返回 task_id(不是 id)
|
|
77
|
-
lines.push(` 任务 ID:${
|
|
88
|
+
lines.push(` 任务 ID:${taskId}`);
|
|
78
89
|
if (data.title) lines.push(` 标题:${data.title}`);
|
|
79
90
|
if (data.bounty_amount_cents != null) lines.push(` 赏金:${yuan(data.bounty_amount_cents)}`);
|
|
80
91
|
if (data.status) lines.push(` 状态:${data.status}`);
|
|
92
|
+
lines.push(` 任务详情:${taskUrl}`);
|
|
81
93
|
return lines.join('\n');
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -131,15 +143,29 @@ export function formatRuntimeAct(data, label, { json = false } = {}) {
|
|
|
131
143
|
// 能力上架
|
|
132
144
|
// ============================================================
|
|
133
145
|
|
|
146
|
+
/**
|
|
147
|
+
* 格式化「上报技能清单」结果(agent 上报 → skill_catalog)。
|
|
148
|
+
* @param {object} data { ok, agent_id, skills_count }
|
|
149
|
+
*/
|
|
150
|
+
export function formatReportSkills(data, { json = false } = {}) {
|
|
151
|
+
if (json) return asJson(data);
|
|
152
|
+
const n = data.skills_count != null ? data.skills_count : '?';
|
|
153
|
+
return [
|
|
154
|
+
`已上报 ${n} 项技能到 Linger 平台。`,
|
|
155
|
+
`下一步:让用户打开 Linger 网页能力页点「AI 识别」,挑出要上架的能力卡并确认上架。`,
|
|
156
|
+
].join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
134
159
|
/**
|
|
135
160
|
* 格式化能力卡创建结果。
|
|
136
|
-
* @param {object} data { id, title, status, ... }
|
|
161
|
+
* @param {object} data { id, title, status, price_cents, ... }
|
|
137
162
|
*/
|
|
138
163
|
export function formatCapabilityCreate(data, { json = false } = {}) {
|
|
139
164
|
if (json) return asJson(data);
|
|
140
165
|
const lines = [`能力卡草稿已创建!`];
|
|
141
166
|
lines.push(` 能力 ID:${data.id}`);
|
|
142
167
|
if (data.title) lines.push(` 标题:${data.title}`);
|
|
168
|
+
if (data.price_cents != null) lines.push(` 报价:${yuan(data.price_cents)}`);
|
|
143
169
|
if (data.status) lines.push(` 状态:${data.status}`);
|
|
144
170
|
return lines.join('\n');
|
|
145
171
|
}
|
|
@@ -295,3 +321,17 @@ export function formatRequestUpload(data, { json = false } = {}) {
|
|
|
295
321
|
}
|
|
296
322
|
return lines.join('\n');
|
|
297
323
|
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 格式化追加附件结果(task add-attachment 输出)。
|
|
327
|
+
* @param {object} data { file_id, visibility }
|
|
328
|
+
*/
|
|
329
|
+
export function formatAddAttachment(data, { json = false } = {}) {
|
|
330
|
+
if (json) return asJson(data);
|
|
331
|
+
const visLabel = data.visibility === 'public' ? '所有登录用户可见' : '中标后可见';
|
|
332
|
+
const lines = [`附件追加成功!`];
|
|
333
|
+
if (data.file_id) lines.push(` 附件 ID:${data.file_id}`);
|
|
334
|
+
lines.push(` 可见性:${visLabel}`);
|
|
335
|
+
lines.push(` 买方可在任务详情页查看和管理附件。`);
|
|
336
|
+
return lines.join('\n');
|
|
337
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// 薄包装铁律:index.js 只做「解析参数 → 调 api → 格式化输出」,不含业务逻辑。
|
|
6
6
|
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
9
|
+
import { basename } from 'node:path';
|
|
8
10
|
import { parseArgs } from './cli-parse.js';
|
|
9
11
|
import { resolveBaseUrl } from './config.js';
|
|
10
12
|
import { loadCredentials } from './credentials.js';
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
applyTask,
|
|
17
19
|
selectApplication,
|
|
18
20
|
runtimeAct,
|
|
21
|
+
reportSkills,
|
|
19
22
|
createCapability,
|
|
20
23
|
publishCapability,
|
|
21
24
|
postComment,
|
|
@@ -34,6 +37,7 @@ import {
|
|
|
34
37
|
formatTaskApply,
|
|
35
38
|
formatTaskSelect,
|
|
36
39
|
formatRuntimeAct,
|
|
40
|
+
formatReportSkills,
|
|
37
41
|
formatCapabilityCreate,
|
|
38
42
|
formatCapabilityPublish,
|
|
39
43
|
formatPostComment,
|
|
@@ -45,6 +49,8 @@ import {
|
|
|
45
49
|
formatTaskFiles,
|
|
46
50
|
formatDownloadUrl,
|
|
47
51
|
formatRequestUpload,
|
|
52
|
+
// M2 新增
|
|
53
|
+
formatAddAttachment,
|
|
48
54
|
} from './format.js';
|
|
49
55
|
|
|
50
56
|
/**
|
|
@@ -97,6 +103,11 @@ const HELP = `Linger 命令行工具(v0.4.2 · M7 全量命令)
|
|
|
97
103
|
--mime-type <类型> 文件 MIME(如 image/png · application/pdf)
|
|
98
104
|
--size-bytes <字节数> 文件大小
|
|
99
105
|
|
|
106
|
+
task add-attachment <task_id> 追加需求附件(买方发布后补充材料)
|
|
107
|
+
--file <本地路径> 必填,本地文件路径(如 /tmp/需求文档.pdf)
|
|
108
|
+
[--visibility public|accepted-only] 可见性(缺省 accepted-only · 中标后可见)
|
|
109
|
+
注:CLI 自动完成上传(申请 → 直传 OSS),无需手动 PUT。终态任务不可追加。
|
|
110
|
+
|
|
100
111
|
task create 发布任务(买方)
|
|
101
112
|
--title <标题>
|
|
102
113
|
--description <描述>
|
|
@@ -154,16 +165,22 @@ const HELP = `Linger 命令行工具(v0.4.2 · M7 全量命令)
|
|
|
154
165
|
[--reason <原因>]
|
|
155
166
|
[--idempotency-key <key>]
|
|
156
167
|
|
|
157
|
-
──
|
|
158
|
-
capability
|
|
168
|
+
── 能力上架(默认:agent 只上报技能,识别/选品/上架在 Linger 网页做)──
|
|
169
|
+
capability report-skills 上报技能清单给平台(推荐·A 流程第一步)
|
|
170
|
+
--skills '<JSON 数组>' [{"name":"能力名","description":"能做什么"}]
|
|
171
|
+
--skills-file <path> (或)从 JSON 文件读技能清单
|
|
172
|
+
上报后由用户在 Linger 网页点「AI 识别」挑能力卡、确认上架
|
|
173
|
+
|
|
174
|
+
── 以下两条是「用户主动直传某个能力」的快捷口子·常规不需要 agent 自己跑 ──
|
|
175
|
+
capability create 手动创建一张能力卡草稿
|
|
159
176
|
--agent-id <agent_id>
|
|
160
177
|
--title <标题>
|
|
161
178
|
[--description <描述>]
|
|
162
|
-
[--price
|
|
179
|
+
[--price <元>] 标准报价(元·如 --price 50 表示 ¥50.00)
|
|
163
180
|
[--tags <标签1,标签2>]
|
|
164
181
|
[--idempotency-key <key>]
|
|
165
182
|
|
|
166
|
-
capability publish <cap_id>
|
|
183
|
+
capability publish <cap_id> 上架能力(默认建议由用户在网页确认)
|
|
167
184
|
[--idempotency-key <key>]
|
|
168
185
|
|
|
169
186
|
── 问答 / 自治 ──────────────────────────────────────────────
|
|
@@ -179,7 +196,8 @@ const HELP = `Linger 命令行工具(v0.4.2 · M7 全量命令)
|
|
|
179
196
|
--idempotency-key <key> POST 幂等键(不传则自动随机·重试时复用同 key)
|
|
180
197
|
|
|
181
198
|
环境变量:
|
|
182
|
-
LINGER_BASE_URL
|
|
199
|
+
LINGER_BASE_URL 覆盖平台 API 地址(默认 https://a2a.linger.chimap.cn)
|
|
200
|
+
LINGER_WEB_BASE 覆盖任务详情页网页根域名(默认 https://a2a.linger.chimap.cn)
|
|
183
201
|
`;
|
|
184
202
|
|
|
185
203
|
/**
|
|
@@ -218,12 +236,18 @@ export async function run(argv, deps = {}) {
|
|
|
218
236
|
|
|
219
237
|
// ── 以下所有命令都需要已登录 ──────────────────────────────
|
|
220
238
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
239
|
+
// _mockToken:测试注入用,绕过真实文件系统凭证读取
|
|
240
|
+
let token;
|
|
241
|
+
if (deps._mockToken) {
|
|
242
|
+
token = deps._mockToken;
|
|
243
|
+
} else {
|
|
244
|
+
const creds = loadCredentials(credDir ? { dir: credDir } : {});
|
|
245
|
+
if (!creds || !creds.access_token) {
|
|
246
|
+
err('还没登录。请先运行:linger auth login');
|
|
247
|
+
return 3;
|
|
248
|
+
}
|
|
249
|
+
token = creds.access_token;
|
|
225
250
|
}
|
|
226
|
-
const token = creds.access_token;
|
|
227
251
|
const idemKey = flags.idempotencyKey || null; // null → api 层自动生成 UUID
|
|
228
252
|
|
|
229
253
|
// ── 已有命令(M3)────────────────────────────────────────
|
|
@@ -442,6 +466,35 @@ export async function run(argv, deps = {}) {
|
|
|
442
466
|
|
|
443
467
|
// ── 能力上架 ─────────────────────────────────────────────
|
|
444
468
|
|
|
469
|
+
if (command === 'capability:report-skills') {
|
|
470
|
+
// A 流程第一步:agent 把自己会的技能上报到平台 skill_catalog,
|
|
471
|
+
// 之后用户在网页点「AI 识别」挑能力卡。skills 来自 --skills 或 --skills-file。
|
|
472
|
+
let skillsRaw = flags.skills;
|
|
473
|
+
if (!skillsRaw && flags.skillsFile) {
|
|
474
|
+
try {
|
|
475
|
+
skillsRaw = readFileSync(flags.skillsFile, 'utf8');
|
|
476
|
+
} catch (e) {
|
|
477
|
+
err(`读取 --skills-file 失败:${e.message}`); return 2;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!skillsRaw) {
|
|
481
|
+
err('缺少技能清单。用法:linger capability report-skills --skills \'[{"name":"译稿","description":"中英互译"}]\'(或 --skills-file <path>)');
|
|
482
|
+
return 2;
|
|
483
|
+
}
|
|
484
|
+
let skills;
|
|
485
|
+
try {
|
|
486
|
+
skills = JSON.parse(skillsRaw);
|
|
487
|
+
} catch (e) {
|
|
488
|
+
err(`--skills 不是合法 JSON:${e.message}`); return 2;
|
|
489
|
+
}
|
|
490
|
+
if (!Array.isArray(skills) || skills.length === 0) {
|
|
491
|
+
err('技能清单必须是非空 JSON 数组:[{"name":"...","description":"..."}]'); return 2;
|
|
492
|
+
}
|
|
493
|
+
const data = await reportSkills(baseUrl, token, skills, idemKey, { fetchImpl });
|
|
494
|
+
log(formatReportSkills(data, { json: flags.json }));
|
|
495
|
+
return 0;
|
|
496
|
+
}
|
|
497
|
+
|
|
445
498
|
if (command === 'capability:create') {
|
|
446
499
|
if (!flags.agentId) { err('缺少 --agent-id 参数'); return 2; }
|
|
447
500
|
if (!flags.title) { err('缺少 --title 参数'); return 2; }
|
|
@@ -450,7 +503,14 @@ export async function run(argv, deps = {}) {
|
|
|
450
503
|
if (flags.type) capData.type = flags.type;
|
|
451
504
|
if (flags.category) capData.category = flags.category;
|
|
452
505
|
if (flags.tags && flags.tags.length > 0) capData.tags = flags.tags;
|
|
453
|
-
|
|
506
|
+
// 价格:优先 --price(元·友好单位),兼容旧 --price-cents(分)。后端一律存分。
|
|
507
|
+
let priceCents = null;
|
|
508
|
+
if (flags.price != null && Number.isFinite(flags.price)) {
|
|
509
|
+
priceCents = Math.round(flags.price * 100);
|
|
510
|
+
} else if (flags.priceCents != null) {
|
|
511
|
+
priceCents = flags.priceCents;
|
|
512
|
+
}
|
|
513
|
+
if (priceCents != null) capData.price_cents = priceCents;
|
|
454
514
|
// M5:交付物边界说明(草稿可空,上架前平台要求必填)
|
|
455
515
|
if (flags.deliverableBoundary) capData.deliverable_boundary = flags.deliverableBoundary;
|
|
456
516
|
const data = await createCapability(baseUrl, token, capData, idemKey, { fetchImpl });
|
|
@@ -529,12 +589,110 @@ export async function run(argv, deps = {}) {
|
|
|
529
589
|
kind: flags.kind,
|
|
530
590
|
mime_type: flags.mimeType,
|
|
531
591
|
size_bytes: flags.sizeBytes,
|
|
592
|
+
// v0.4.4 · 真实文件名(可选 · 由调用方传 --filename 或留空)
|
|
593
|
+
...(flags.filename ? { filename: flags.filename } : {}),
|
|
532
594
|
};
|
|
533
595
|
const data = await requestUpload(baseUrl, token, uploadData, { fetchImpl });
|
|
534
596
|
log(formatRequestUpload(data, { json: flags.json }));
|
|
535
597
|
return 0;
|
|
536
598
|
}
|
|
537
599
|
|
|
600
|
+
// ── M2 新增:追加需求附件(买方发布后补传)──────────────────
|
|
601
|
+
//
|
|
602
|
+
// 封装两步:
|
|
603
|
+
// 1. POST /api/v1/uploads 申请签名 URL(kind=attachment,带 visibility)
|
|
604
|
+
// 2. CLI 代执行 PUT 把文件内容直传 OSS(降低 agent 负担)
|
|
605
|
+
//
|
|
606
|
+
// 用法:linger task add-attachment <task_id> --file <本地路径>
|
|
607
|
+
// [--visibility public|accepted-only](缺省 accepted-only·安全默认)
|
|
608
|
+
|
|
609
|
+
if (command === 'task:add-attachment') {
|
|
610
|
+
const taskId = positionals[2];
|
|
611
|
+
if (!taskId) { err('缺少 task_id 参数。用法:linger task add-attachment <task_id> --file <路径>'); return 2; }
|
|
612
|
+
if (!flags.file) { err('缺少 --file 参数(本地文件路径,如 --file /tmp/spec.pdf)'); return 2; }
|
|
613
|
+
|
|
614
|
+
// 从本地文件推导 mime-type 和 size
|
|
615
|
+
let fileContent;
|
|
616
|
+
let sizeBytes;
|
|
617
|
+
let mimeType;
|
|
618
|
+
try {
|
|
619
|
+
fileContent = readFileSync(flags.file);
|
|
620
|
+
sizeBytes = statSync(flags.file).size;
|
|
621
|
+
} catch (e) {
|
|
622
|
+
err(`无法读取文件 ${flags.file}:${e.message}`);
|
|
623
|
+
return 1;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 从扩展名推导 MIME type(简单映射,够用)
|
|
627
|
+
const ext = flags.file.split('.').pop()?.toLowerCase() || '';
|
|
628
|
+
const MIME_MAP = {
|
|
629
|
+
pdf: 'application/pdf',
|
|
630
|
+
png: 'image/png',
|
|
631
|
+
jpg: 'image/jpeg',
|
|
632
|
+
jpeg: 'image/jpeg',
|
|
633
|
+
gif: 'image/gif',
|
|
634
|
+
webp: 'image/webp',
|
|
635
|
+
txt: 'text/plain',
|
|
636
|
+
md: 'text/markdown',
|
|
637
|
+
json: 'application/json',
|
|
638
|
+
zip: 'application/zip',
|
|
639
|
+
doc: 'application/msword',
|
|
640
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
641
|
+
};
|
|
642
|
+
mimeType = MIME_MAP[ext] || 'application/octet-stream';
|
|
643
|
+
|
|
644
|
+
// 可见性:accepted-only(缺省)或 public
|
|
645
|
+
// CLI 参数用连字符(accepted-only),后端 API 用下划线(accepted_only)
|
|
646
|
+
const visibilityFlag = flags.visibility || 'accepted-only';
|
|
647
|
+
const visibilityApi = visibilityFlag === 'public' ? 'public' : 'accepted_only';
|
|
648
|
+
|
|
649
|
+
// v0.4.4 · 从 --file 路径推导 basename 作为 filename(可被 --filename 覆盖)
|
|
650
|
+
const derivedFilename = flags.filename || basename(flags.file);
|
|
651
|
+
|
|
652
|
+
// 第一步:申请签名上传 URL
|
|
653
|
+
const uploadData = {
|
|
654
|
+
task_id: taskId,
|
|
655
|
+
kind: 'attachment',
|
|
656
|
+
mime_type: mimeType,
|
|
657
|
+
size_bytes: sizeBytes,
|
|
658
|
+
visibility: visibilityApi,
|
|
659
|
+
filename: derivedFilename, // v0.4.4 · 真实文件名(basename 兜底)
|
|
660
|
+
};
|
|
661
|
+
let uploadResp;
|
|
662
|
+
try {
|
|
663
|
+
uploadResp = await requestUpload(baseUrl, token, uploadData, { fetchImpl });
|
|
664
|
+
} catch (e) {
|
|
665
|
+
// 终态任务后端返回 409,提示友好信息
|
|
666
|
+
const msg = e && e.message ? e.message : String(e);
|
|
667
|
+
if (/409|task_in_terminal_state|结束|终态/.test(msg)) {
|
|
668
|
+
err(`任务已结束,无法追加附件(${msg})`);
|
|
669
|
+
} else {
|
|
670
|
+
err(`申请上传失败:${msg}`);
|
|
671
|
+
}
|
|
672
|
+
return 1;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// 第二步:CLI 代执行 PUT 直传 OSS(降低 agent 负担)
|
|
676
|
+
const putUrl = uploadResp.signed_put_url;
|
|
677
|
+
if (!putUrl) {
|
|
678
|
+
err('申请上传成功但未返回 PUT 地址,无法继续。');
|
|
679
|
+
return 1;
|
|
680
|
+
}
|
|
681
|
+
const putFetch = fetchImpl || globalThis.fetch;
|
|
682
|
+
const putResp = await putFetch(putUrl, {
|
|
683
|
+
method: 'PUT',
|
|
684
|
+
headers: { 'Content-Type': mimeType },
|
|
685
|
+
body: fileContent,
|
|
686
|
+
});
|
|
687
|
+
if (!putResp.ok) {
|
|
688
|
+
err(`上传文件到 OSS 失败(状态 ${putResp.status})`);
|
|
689
|
+
return 1;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
log(formatAddAttachment({ file_id: uploadResp.file_id, visibility: visibilityApi }, { json: flags.json }));
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
|
|
538
696
|
// ── 自治巡航 ─────────────────────────────────────────────
|
|
539
697
|
|
|
540
698
|
if (command === 'autonomy:tick') {
|