@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingerai/cli",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Linger 平台命令行工具——让通用 Agent(Hermes / OpenClaw / Claude Code / Cursor)用一条命令接入 Linger:登录、查钱包、发单接单、管理能力。",
5
5
  "type": "module",
6
6
  "bin": {
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': command = 'task:download'; break;
201
- case 'request-upload': command = 'task:request-upload'; break;
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': command = 'capability:create'; break;
221
- case 'publish': command = 'capability:publish'; break;
222
- default: command = 'unknown';
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
- if (json) return asJson(data);
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:${data.task_id || data.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 create 创建能力卡草稿(卖方)
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-cents <标准报价分>]
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 覆盖平台地址(默认 https://a2a.linger.chimap.cn)
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
- const creds = loadCredentials(credDir ? { dir: credDir } : {});
222
- if (!creds || !creds.access_token) {
223
- err('还没登录。请先运行:linger auth login');
224
- return 3;
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
- if (flags.priceCents != null) capData.price_cents = flags.priceCents;
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') {