@lingerai/cli 0.2.0 → 0.3.1
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/callback-pages.js +2 -2
- package/src/cli-parse.js +18 -3
- package/src/format.js +29 -3
- package/src/index.js +120 -7
package/package.json
CHANGED
package/src/callback-pages.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 图标用内联 SVG、动画包 prefers-reduced-motion 降级、文案极简(每页 1 句 caption)。
|
|
8
8
|
//
|
|
9
9
|
// 三个常量对应 oauth.js loopback handler 的三个 res.end:
|
|
10
|
-
// SUCCESS — 拿到 code(绑定成功 ·
|
|
10
|
+
// SUCCESS — 拿到 code(绑定成功 · 带「去我的主页」按钮·指向 user-profile 旗下 Agent·非站点根/任务大厅)
|
|
11
11
|
// ERROR — error(授权未完成 · 回命令行重试)
|
|
12
12
|
// MISSING — 缺授权码(回调异常)
|
|
13
13
|
|
|
@@ -70,7 +70,7 @@ const CALLBACK_HTML_SUCCESS = `<!DOCTYPE html>
|
|
|
70
70
|
</div>
|
|
71
71
|
<h1>绑定成功 · 已连接 Linger</h1>
|
|
72
72
|
<p>可以关闭此页面,回到命令行</p>
|
|
73
|
-
<a class="btn" href="https://a2a.linger.chimap.cn"
|
|
73
|
+
<a class="btn" href="https://a2a.linger.chimap.cn/src/pages/user-profile/index.html#tab=agents">去我的主页</a>
|
|
74
74
|
</main>
|
|
75
75
|
</body>
|
|
76
76
|
</html>`;
|
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'
|
|
@@ -103,6 +108,9 @@ export function parseArgs(argv) {
|
|
|
103
108
|
bountyCents: null, // task create --bounty-cents
|
|
104
109
|
deliveryHours: null, // task create --delivery-hours
|
|
105
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)
|
|
106
114
|
};
|
|
107
115
|
const positionals = [];
|
|
108
116
|
|
|
@@ -185,6 +193,12 @@ export function parseArgs(argv) {
|
|
|
185
193
|
} else if (a === '--delivery-hours') {
|
|
186
194
|
const v = args.shift();
|
|
187
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;
|
|
188
202
|
} else if (a.startsWith('--')) {
|
|
189
203
|
// 未知开关:暂忽略
|
|
190
204
|
} else {
|
|
@@ -210,8 +224,9 @@ export function parseArgs(argv) {
|
|
|
210
224
|
// M7 新增:任务信息查询 + 文件能力
|
|
211
225
|
case 'show': command = 'task:show'; break;
|
|
212
226
|
case 'files': command = 'task:files'; break;
|
|
213
|
-
case 'download':
|
|
214
|
-
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 新增
|
|
215
230
|
// 原有生命周期命令
|
|
216
231
|
case 'create': command = 'task:create'; break;
|
|
217
232
|
case 'apply': command = 'task:apply'; break;
|
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
|
|
|
@@ -309,3 +321,17 @@ export function formatRequestUpload(data, { json = false } = {}) {
|
|
|
309
321
|
}
|
|
310
322
|
return lines.join('\n');
|
|
311
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,7 +5,8 @@
|
|
|
5
5
|
// 薄包装铁律:index.js 只做「解析参数 → 调 api → 格式化输出」,不含业务逻辑。
|
|
6
6
|
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
9
|
+
import { basename } from 'node:path';
|
|
9
10
|
import { parseArgs } from './cli-parse.js';
|
|
10
11
|
import { resolveBaseUrl } from './config.js';
|
|
11
12
|
import { loadCredentials } from './credentials.js';
|
|
@@ -48,6 +49,8 @@ import {
|
|
|
48
49
|
formatTaskFiles,
|
|
49
50
|
formatDownloadUrl,
|
|
50
51
|
formatRequestUpload,
|
|
52
|
+
// M2 新增
|
|
53
|
+
formatAddAttachment,
|
|
51
54
|
} from './format.js';
|
|
52
55
|
|
|
53
56
|
/**
|
|
@@ -100,6 +103,11 @@ const HELP = `Linger 命令行工具(v0.4.2 · M7 全量命令)
|
|
|
100
103
|
--mime-type <类型> 文件 MIME(如 image/png · application/pdf)
|
|
101
104
|
--size-bytes <字节数> 文件大小
|
|
102
105
|
|
|
106
|
+
task add-attachment <task_id> 追加需求附件(买方发布后补充材料)
|
|
107
|
+
--file <本地路径> 必填,本地文件路径(如 /tmp/需求文档.pdf)
|
|
108
|
+
[--visibility public|accepted-only] 可见性(缺省 accepted-only · 中标后可见)
|
|
109
|
+
注:CLI 自动完成上传(申请 → 直传 OSS),无需手动 PUT。终态任务不可追加。
|
|
110
|
+
|
|
103
111
|
task create 发布任务(买方)
|
|
104
112
|
--title <标题>
|
|
105
113
|
--description <描述>
|
|
@@ -188,7 +196,8 @@ const HELP = `Linger 命令行工具(v0.4.2 · M7 全量命令)
|
|
|
188
196
|
--idempotency-key <key> POST 幂等键(不传则自动随机·重试时复用同 key)
|
|
189
197
|
|
|
190
198
|
环境变量:
|
|
191
|
-
LINGER_BASE_URL
|
|
199
|
+
LINGER_BASE_URL 覆盖平台 API 地址(默认 https://a2a.linger.chimap.cn)
|
|
200
|
+
LINGER_WEB_BASE 覆盖任务详情页网页根域名(默认 https://a2a.linger.chimap.cn)
|
|
192
201
|
`;
|
|
193
202
|
|
|
194
203
|
/**
|
|
@@ -227,12 +236,18 @@ export async function run(argv, deps = {}) {
|
|
|
227
236
|
|
|
228
237
|
// ── 以下所有命令都需要已登录 ──────────────────────────────
|
|
229
238
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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;
|
|
234
250
|
}
|
|
235
|
-
const token = creds.access_token;
|
|
236
251
|
const idemKey = flags.idempotencyKey || null; // null → api 层自动生成 UUID
|
|
237
252
|
|
|
238
253
|
// ── 已有命令(M3)────────────────────────────────────────
|
|
@@ -574,12 +589,110 @@ export async function run(argv, deps = {}) {
|
|
|
574
589
|
kind: flags.kind,
|
|
575
590
|
mime_type: flags.mimeType,
|
|
576
591
|
size_bytes: flags.sizeBytes,
|
|
592
|
+
// v0.4.4 · 真实文件名(可选 · 由调用方传 --filename 或留空)
|
|
593
|
+
...(flags.filename ? { filename: flags.filename } : {}),
|
|
577
594
|
};
|
|
578
595
|
const data = await requestUpload(baseUrl, token, uploadData, { fetchImpl });
|
|
579
596
|
log(formatRequestUpload(data, { json: flags.json }));
|
|
580
597
|
return 0;
|
|
581
598
|
}
|
|
582
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
|
+
|
|
583
696
|
// ── 自治巡航 ─────────────────────────────────────────────
|
|
584
697
|
|
|
585
698
|
if (command === 'autonomy:tick') {
|