@metagptx/deepflow-suite-cli 0.3.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.
@@ -0,0 +1,127 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, mkdtemp, open, link, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ // Retain every numeric token verbatim, including values outside JS precision.
6
+ export const decodeRaw = (raw) => JSON.parse(raw, (_key, value, context) => typeof value === 'number' ? JSON.rawJSON(context.source) : value);
7
+ export const encodeRaw = (value) => JSON.stringify(value);
8
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && !JSON.isRawJSON(value);
9
+ const seq = (value) => {
10
+ if (!JSON.isRawJSON(value) || !/^-?\d+$/.test(value.rawJSON)) throw Error('invalid facts sequence');
11
+ const n = BigInt(value.rawJSON);
12
+ if (n < 0n || n > 9223372036854775807n) throw Error('invalid facts sequence');
13
+ return n;
14
+ };
15
+ const rawSeq = (n) => JSON.rawJSON(BigInt(n).toString());
16
+
17
+ export function validateReadOptions(o) {
18
+ const v = o.values;
19
+ if ('--fields' in v) {
20
+ if (!o.json) throw Error('--fields requires --json');
21
+ const roots = o.command === 'get' ? ['id', 'title', 'description', 'description_version', 'metadata', 'created_at', 'updated_at'] : ['seq', 'type', 'actor_kind', 'actor_id', 'payload', 'idempotency_key', 'created_at'];
22
+ const paths = v['--fields'].split(',');
23
+ paths.forEach((field, i) => {
24
+ const parts = field.split('.');
25
+ if (!roots.includes(parts[0]) || (parts.length > 1 && !['metadata', 'payload'].includes(parts[0]))) throw Error(`unsupported field path ${JSON.stringify(field)} for ${o.command}`);
26
+ if (parts.some((part) => !part || part.trim() !== part || /[\[\]*\/\\\t\r\n]/.test(part))) throw Error(`invalid object field path ${JSON.stringify(field)}`);
27
+ if (paths.slice(0, i).some((earlier) => earlier === field || earlier.startsWith(field + '.') || field.startsWith(earlier + '.'))) throw Error('overlapping field paths');
28
+ });
29
+ }
30
+ const all = '--all' in v;
31
+ if (all && (!o.json || !v['--output-file']?.trim() || '--fields' in v)) throw Error('--all requires --json and --output-file, and cannot be combined with --fields');
32
+ for (const flag of ['--output-file', '--max-pages', '--max-bytes']) if (flag in v && !all) throw Error(`${flag} requires --all`);
33
+ for (const [flag, max] of [['--max-pages', 100], ['--max-bytes', 64 << 20]]) {
34
+ if (flag in v && (!/^\+?\d+$/.test(v[flag]) || BigInt(v[flag]) < 1n || BigInt(v[flag]) > BigInt(max))) throw Error(`${flag} must be between 1 and ${max}`);
35
+ }
36
+ }
37
+
38
+ function projectObject(source, paths) {
39
+ if (!isObject(source)) throw Error('expected a JSON object for field selection');
40
+ const selected = Object.create(null), children = new Map();
41
+ for (const field of paths) {
42
+ const [root, ...parts] = field.split('.');
43
+ if (parts.length) { if (!children.has(root)) children.set(root, []); children.get(root).push(parts.join('.')); }
44
+ else if (Object.hasOwn(source, root)) selected[root] = source[root];
45
+ }
46
+ for (const [root, subpaths] of children) {
47
+ if (!Object.hasOwn(source, root) || !isObject(source[root])) continue;
48
+ const projected = projectObject(source[root], subpaths);
49
+ if (Object.keys(projected).length) selected[root] = projected;
50
+ }
51
+ return selected;
52
+ }
53
+ export function projectResponse(command, raw, paths) {
54
+ const data = decodeRaw(raw);
55
+ if (command === 'get') return encodeRaw(projectObject(data, paths));
56
+ if (!isObject(data) || !Array.isArray(data.facts)) throw Error('expected facts array');
57
+ data.facts = data.facts.map((fact) => projectObject(fact, paths));
58
+ return encodeRaw(data);
59
+ }
60
+ const normalizedID = (id) => typeof id === 'string' ? id.toLowerCase().replace(/^urn:uuid:/, '').replace(/[{}-]/g, '') : '';
61
+ export function validateFactPage(raw, taskID, after) {
62
+ const page = decodeRaw(raw);
63
+ if (!isObject(page) || !Array.isArray(page.facts) || !Object.hasOwn(page, 'next_after_seq')) throw Error('invalid facts page identity, facts or cursor');
64
+ if (!/^[0-9a-f]{32}$/.test(normalizedID(page.task_id)) || normalizedID(page.task_id) !== normalizedID(taskID)) throw Error('facts page task_id does not match query');
65
+ let last = after;
66
+ for (const fact of page.facts) {
67
+ if (!isObject(fact) || typeof fact.type !== 'string' || !fact.type || seq(fact.seq) <= last) throw Error(`facts page is not strictly ordered after ${after}`);
68
+ last = seq(fact.seq);
69
+ }
70
+ if (seq(page.next_after_seq) !== last) throw Error(`facts cursor does not match last seq ${last}`);
71
+ return { facts: page.facts, next: last };
72
+ }
73
+ export async function readResponse(response, limit = 0) {
74
+ if (!limit) return response.text();
75
+ let bytes = 0; const chunks = [];
76
+ for await (const chunk of response.body) {
77
+ bytes += chunk.length;
78
+ if (bytes > limit) throw Object.assign(Error('read byte budget exhausted'), { byteLimit: true });
79
+ chunks.push(chunk);
80
+ }
81
+ return Buffer.concat(chunks).toString('utf8');
82
+ }
83
+
84
+ export async function readAllFacts(o, baseURL, request, stdout, stderr) {
85
+ const output = path.resolve(o.values['--output-file']);
86
+ let temp;
87
+ try {
88
+ try { await lstat(output); throw Error(`output file must not exist: ${output}`); }
89
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
90
+ temp = await mkdtemp(path.join(path.dirname(output), '.task-facts-'));
91
+ } catch (error) { stderr.write(error.message + '\n'); return 2; }
92
+ try {
93
+ const query = { task_id: o.args[0], base_url: baseURL, after_seq: '0', limit: '200' };
94
+ for (const flag of ['--after', '--types', '--limit', '--metadata-source', '--metadata-query-key']) if (flag in o.values) query[flag === '--after' ? 'after_seq' : flag.slice(2).replaceAll('-', '_')] = o.values[flag];
95
+ const manifest = { query, complete: false, stop_reason: 'page_limit', pages: 0, count: 0, next_after_seq: rawSeq(query.after_seq), type_counts: Object.create(null), response_bytes: 0, output_file: output };
96
+ const facts = [], budget = Number(o.values['--max-bytes'] || (8 << 20));
97
+ let cursor = BigInt(query.after_seq);
98
+ for (let i = 0; i < Number(o.values['--max-pages'] || 10); i++) {
99
+ const remaining = budget - manifest.response_bytes;
100
+ if (remaining <= 0) { manifest.stop_reason = 'byte_limit'; break; }
101
+ let raw, page;
102
+ try { raw = await request({ ...o, values: { ...o.values, '--after': String(cursor) } }, remaining); }
103
+ catch (error) { manifest.stop_reason = error.byteLimit ? 'byte_limit' : 'request_error'; stderr.write(error.message + '\n'); break; }
104
+ manifest.response_bytes += Buffer.byteLength(raw);
105
+ try { page = validateFactPage(raw, o.args[0], cursor); }
106
+ catch (error) { manifest.stop_reason = 'invalid_page'; stderr.write(error.message + '\n'); break; }
107
+ manifest.pages++;
108
+ if (!page.facts.length) { manifest.complete = true; manifest.stop_reason = 'empty_page'; break; }
109
+ for (const fact of page.facts) {
110
+ if (!manifest.count) manifest.first_seq = fact.seq;
111
+ manifest.last_seq = fact.seq;
112
+ manifest.type_counts[fact.type] = (manifest.type_counts[fact.type] || 0) + 1;
113
+ manifest.count++;
114
+ }
115
+ facts.push(...page.facts); cursor = page.next; manifest.next_after_seq = rawSeq(cursor);
116
+ }
117
+ const data = Buffer.from(encodeRaw({ query, facts, complete: manifest.complete, stop_reason: manifest.stop_reason, next_after_seq: manifest.next_after_seq }));
118
+ const staging = path.join(temp, 'facts.json'), file = await open(staging, 'wx', 0o600);
119
+ try { await file.writeFile(data); } finally { await file.close(); }
120
+ await link(staging, output);
121
+ manifest.file_bytes = data.length; manifest.file_sha256 = createHash('sha256').update(data).digest('hex');
122
+ stdout.write(encodeRaw(manifest) + '\n');
123
+ if (!manifest.complete) { stderr.write(`incomplete facts read: ${manifest.stop_reason}; next_after_seq=${cursor}\n`); return 1; }
124
+ return 0;
125
+ } catch (error) { stderr.write(error.message + '\n'); return 1; }
126
+ finally { await rm(temp, { recursive: true, force: true }); }
127
+ }
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-task","sourceCommit":"3a8f5c8915cf5df74236eb4da693ac75f4c816a8","sourceDirty":false}
1
+ {"version":"0.3.1","project":"deepflow-task","sourceCommit":"a5b9064298158aad2fef2a67f0baa0a638c89f4b","sourceDirty":false}
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-task-driver","sourceCommit":"ab582505c8d60a99c8d2939c3d58b31803a8a229","sourceDirty":false}
1
+ {"version":"0.3.1","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { randomUUID } from "node:crypto";
4
- import { readFile } from "node:fs/promises";
3
+ import { randomUUID, createHash } from "node:crypto";
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import { basename } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { realpathSync } from "node:fs";
7
8
 
@@ -39,6 +40,7 @@ export function parseArgs(argv, env = process.env) {
39
40
  baseUrl: env.DF_WORKBENCH_BASE_URL || DEFAULT_BASE_URL,
40
41
  token: env.DF_WORKBENCH_TOKEN || "",
41
42
  workspace: env.DF_WORKBENCH_WORKSPACE_ID || "",
43
+ expectedActor: undefined,
42
44
  json: false,
43
45
  idempotencyKey: "",
44
46
  message: "",
@@ -48,6 +50,8 @@ export function parseArgs(argv, env = process.env) {
48
50
  scope: "mine",
49
51
  squadId: undefined,
50
52
  file: undefined,
53
+ attachmentIds: [],
54
+ participantIds: [],
51
55
  };
52
56
  const positionals = [];
53
57
  for (let index = 0; index < argv.length; index += 1) {
@@ -67,6 +71,7 @@ export function parseArgs(argv, env = process.env) {
67
71
  [
68
72
  "--base-url",
69
73
  "--workspace",
74
+ "--expected-actor",
70
75
  "--idempotency-key",
71
76
  "--message",
72
77
  "--title",
@@ -75,6 +80,11 @@ export function parseArgs(argv, env = process.env) {
75
80
  "--scope",
76
81
  "--squad-id",
77
82
  "--file",
83
+ "--attachment-id",
84
+ "--member-id",
85
+ "--expected-owner",
86
+ "--owner-id",
87
+ "--participant-id",
78
88
  ].includes(name)
79
89
  ) {
80
90
  const parsed =
@@ -82,6 +92,11 @@ export function parseArgs(argv, env = process.env) {
82
92
  if (inline === undefined) index += 1;
83
93
  if (name === "--base-url") options.baseUrl = parsed;
84
94
  if (name === "--workspace") options.workspace = parsed;
95
+ if (name === "--expected-actor") {
96
+ if (!parsed.trim() || parsed.length > 160 || /[\r\n]/.test(parsed))
97
+ throw new UsageError("--expected-actor 需要有效的用户 ID。");
98
+ options.expectedActor = parsed;
99
+ }
85
100
  if (name === "--idempotency-key") options.idempotencyKey = parsed;
86
101
  if (name === "--message") options.message = parsed;
87
102
  if (name === "--title") options.title = parsed;
@@ -90,6 +105,11 @@ export function parseArgs(argv, env = process.env) {
90
105
  if (name === "--scope") options.scope = parsed;
91
106
  if (name === "--squad-id") options.squadId = parsed;
92
107
  if (name === "--file") options.file = parsed;
108
+ if (name === "--attachment-id") options.attachmentIds.push(parsed);
109
+ if (name === "--member-id") options.memberId = parsed;
110
+ if (name === "--expected-owner") options.expectedOwner = parsed;
111
+ if (name === "--owner-id") options.ownerId = parsed;
112
+ if (name === "--participant-id") options.participantIds.push(parsed);
93
113
  continue;
94
114
  }
95
115
  if (argument.startsWith("--"))
@@ -245,6 +265,13 @@ async function currentContext(options) {
245
265
  throw new RequestError("Workbench 没有返回当前用户。", {
246
266
  code: "identity_missing",
247
267
  });
268
+ if (
269
+ options.expectedActor !== undefined &&
270
+ options.expectedActor !== me.user.id
271
+ )
272
+ throw new RequestError("Workbench 操作身份已变化;未提交业务请求。", {
273
+ code: "identity_changed",
274
+ });
248
275
  return { me, workspace, actorId: me.user.id };
249
276
  }
250
277
 
@@ -423,18 +450,28 @@ function help() {
423
450
  df-workbench squad get <squad-id> [--json]
424
451
  df-workbench squad create --file <配置.json> [--idempotency-key <key>] [--json]
425
452
  df-workbench squad update <squad-id> --file <配置.json> [--idempotency-key <key>] [--json]
426
- df-workbench task create --message <消息> [--title <标题>] [--squad-id <squad-id>] [--idempotency-key <key>]
453
+ df-workbench workspace members [--json]
454
+ df-workbench task create --message <消息> [--title <标题>] [--squad-id <squad-id>] [--owner-id <user-id>] [--participant-id <user-id>] [--idempotency-key <key>]
455
+ df-workbench task member-options <task-id>
456
+ df-workbench task invite <task-id> --member-id <user-id> [--idempotency-key <key>]
457
+ df-workbench task transfer-owner <task-id> --member-id <user-id> --expected-owner <user-id> [--idempotency-key <key>]
427
458
  df-workbench task send <task-id> --message <消息> [--idempotency-key <key>]
459
+ df-workbench task lookup <DF-number> [--json]
428
460
  df-workbench task get <task-id> [--json]
429
461
  df-workbench task facts <task-id> [--after <seq>] [--limit <n>] [--json]
430
462
  df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--json]
463
+ df-workbench attachment upload --file <文件> [--idempotency-key <key>] [--json]
464
+
465
+ task create/send 可重复传入 --attachment-id <id>,每次最多 10 个。
431
466
 
432
467
  全局配置:
433
468
  DF_WORKBENCH_BASE_URL Workbench 根地址,默认 http://127.0.0.1:8793
434
469
  DF_WORKBENCH_TOKEN OIDC 环境使用的 personal token
435
470
  DF_WORKBENCH_WORKSPACE_ID 当前工作区覆盖值
471
+ Token 获取:公司账号登录 → 个人设置 → 个人 Token
436
472
  --base-url <url> 覆盖 Workbench 根地址
437
473
  --workspace <id> 覆盖当前工作区
474
+ --expected-actor <user-id> 固定操作身份;身份变化时停止请求
438
475
  --json 输出机器可读 JSON`;
439
476
  }
440
477
 
@@ -444,6 +481,11 @@ async function run(options) {
444
481
  process.stdout.write(`${help()}\n`);
445
482
  return;
446
483
  }
484
+ if ((options.ownerId !== undefined || options.participantIds.length) && !(command === "task" && subject === "create")) throw new UsageError("--owner-id 和 --participant-id 仅用于 task create。");
485
+ if (options.ownerId !== undefined && !/^[\w-]{1,100}$/.test(options.ownerId)) throw new UsageError("初始负责人 ID 无效。");
486
+ if ((options.memberId !== undefined && !(command === "task" && ["invite","transfer-owner"].includes(subject))) ||
487
+ (options.expectedOwner !== undefined && !(command === "task" && subject === "transfer-owner")))
488
+ throw new UsageError("成员和负责人参数只用于对应的任务成员命令。");
447
489
  if (
448
490
  options.squadId !== undefined &&
449
491
  !(command === "task" && subject === "create")
@@ -453,9 +495,57 @@ async function run(options) {
453
495
  );
454
496
  if (
455
497
  options.file !== undefined &&
456
- !(command === "squad" && ["create", "update"].includes(subject))
498
+ !(command === "squad" && ["create", "update"].includes(subject)) &&
499
+ !(command === "attachment" && subject === "upload")
457
500
  )
458
- throw new UsageError("--file 仅用于 squad create/update。");
501
+ throw new UsageError(
502
+ "--file 仅用于 squad create/update 或 attachment upload。",
503
+ );
504
+ if (
505
+ options.attachmentIds.length &&
506
+ !(command === "task" && ["create", "send"].includes(subject))
507
+ )
508
+ throw new UsageError("--attachment-id 仅用于 task create/send。");
509
+ if (
510
+ options.attachmentIds.length > 10 ||
511
+ new Set(options.attachmentIds).size !== options.attachmentIds.length ||
512
+ options.attachmentIds.some((id) => !/^[\w-]{1,100}$/.test(id))
513
+ )
514
+ throw new UsageError("附件列表无效,每次最多发送 10 个不同附件。");
515
+ if (command === "attachment" && subject === "upload") {
516
+ if (!options.file) throw new UsageError("attachment upload 需要 --file。");
517
+ const fileInfo = await stat(options.file);
518
+ if (!fileInfo.isFile() || !fileInfo.size || fileInfo.size > 4 * 1024 * 1024)
519
+ throw new UsageError("每个附件需为 1 字节至 4 MiB。");
520
+ const bytes = await readFile(options.file);
521
+ const filename = basename(options.file);
522
+ if (!bytes.length || bytes.length > 4 * 1024 * 1024)
523
+ throw new UsageError("每个附件需为 1 字节至 4 MiB。");
524
+ if (
525
+ !filename.trim() ||
526
+ filename.length > 180 ||
527
+ /[\x00-\x1f\x7f\\]/.test(filename)
528
+ )
529
+ throw new UsageError("附件名称无效。");
530
+ const context = await currentContext(options);
531
+ const value = await request(
532
+ options,
533
+ "/api/v1/task-attachments",
534
+ {
535
+ method: "POST",
536
+ key: idempotencyKey(options),
537
+ body: { filename, bytesBase64: bytes.toString("base64") },
538
+ },
539
+ context,
540
+ );
541
+ writeResult(
542
+ value,
543
+ options,
544
+ (value) =>
545
+ `已上传附件:${value.attachment?.filename} (${value.attachment?.id})`,
546
+ );
547
+ return;
548
+ }
459
549
  if (command === "squad") return squadCommand(options, subject, rest);
460
550
  if (command === "health") {
461
551
  const value = await request(options, "/healthz");
@@ -482,16 +572,44 @@ async function run(options) {
482
572
  );
483
573
  return;
484
574
  }
575
+ if (command === "workspace" && subject === "members") {
576
+ const context = await currentContext(options);
577
+ const value = await request(options,"/api/v1/members",{},context);
578
+ if (!Array.isArray(value.members)) throw new RequestError("工作区成员目录回执无效。",{code:"invalid_member_directory"});
579
+ writeResult(value,options,value => value.members.map(member=>`${member.userId} ${member.name || ""}`).join("\n"));return;
580
+ }
485
581
  if (command !== "task") throw new UsageError(`不支持的命令:${command}`);
582
+ if (["member-options", "invite", "transfer-owner"].includes(subject)) {
583
+ const taskId = rest[0];
584
+ if (!taskId || !/^[\w-]{1,160}$/.test(taskId)) throw new UsageError("需要有效的 Task ID。");
585
+ const writes = subject !== "member-options";
586
+ if (writes && !/^[\w-]{1,100}$/.test(options.memberId || "")) throw new UsageError("需要 --member-id 指定有效的 DF 用户 ID。");
587
+ if (subject === "transfer-owner" && !/^[\w-]{1,100}$/.test(options.expectedOwner || "")) throw new UsageError("需要 --expected-owner 指定当前负责人 ID。");
588
+ const context = await currentContext(options);
589
+ const path = subject === "member-options" ? `/api/v1/task-ui/tasks/${encodeURIComponent(taskId)}/property-options`
590
+ : `/api/v1/tasks/${encodeURIComponent(taskId)}/${subject === "invite" ? "member-invitations" : "owner-transfer"}`;
591
+ const value = await request(options, path, writes ? {method:"POST", key:idempotencyKey(options), body:subject === "invite"
592
+ ? {invitee_ids:[options.memberId]} : {new_owner_id:options.memberId,expected_owner_id:options.expectedOwner}} : {}, context);
593
+ writeResult(value, options, value => JSON.stringify(value));
594
+ return;
595
+ }
486
596
  if (subject === "create") {
487
597
  const body = buildCreateBody(
488
598
  options.message,
489
599
  options.title,
490
600
  options.squadId,
491
601
  );
602
+ if (options.ownerId) {
603
+ if (![options.ownerId,...options.participantIds].every(id=>/^[\w-]{1,100}$/.test(id))) throw new UsageError("初始负责人和参与人 ID 无效。");
604
+ body.initial_owner_id=options.ownerId;
605
+ body.first_message="";
606
+ body.invitee_ids=[...new Set(options.participantIds)].sort();
607
+ } else if(options.participantIds.length) throw new UsageError("--participant-id 需要同时指定 --owner-id。");
608
+ if (options.attachmentIds.length && !options.ownerId)
609
+ body.attachment_ids = options.attachmentIds;
492
610
  const context = await currentContext(options);
493
611
  const key = idempotencyKey(options);
494
- const value = await request(
612
+ let value = await request(
495
613
  options,
496
614
  "/api/v1/tasks",
497
615
  {
@@ -514,6 +632,23 @@ async function run(options) {
514
632
  idempotencyKey: key,
515
633
  },
516
634
  );
635
+ if (options.ownerId) {
636
+ const task=value?.task,data=task?.metadata?.data;
637
+ if (!task?.id || data?.workspace_id!==context.workspace || data.owner_id!==options.ownerId || data.created_by!==context.actorId ||
638
+ ![context.actorId,options.ownerId,...options.participantIds].every(id=>data.members?.includes(id))) throw new RequestError("已创建的任务初始负责人或成员回执不匹配,请保留原幂等键核对,不要重建。",{code:"task_metadata_mismatch",outcome:"unknown",taskId:task?.id,idempotencyKey:key});
639
+ // Current squad edits must never change a retry of the original message.
640
+ const agentId=data.initial_message_context?.agentId;
641
+ if (!agentId) throw new RequestError("任务已创建,但创建时的小队快照没有可接收首条消息的伙伴;请保留原任务与幂等键核对。",{code:"initial_agent_unavailable",outcome:"unknown",taskId:task.id,idempotencyKey:key});
642
+ const firstKey="initial-message:"+createHash("sha256").update(key).digest("hex");
643
+ let first;
644
+ try {
645
+ first=await request(options,`/api/v1/tasks/${encodeURIComponent(task.id)}/messages`,{method:"POST",key:firstKey,body:{text:messageValue(options.message),agentIds:[agentId],...(options.attachmentIds.length?{attachment_ids:options.attachmentIds}:{})}},context);
646
+ } catch(error) {
647
+ throw new RequestError(`任务 ${task.id} 已创建,首条消息回执尚待确认;保留幂等键 ${key} 重试,不换键或重建。`,{code:"initial_message_unconfirmed",outcome:"unknown",taskId:task.id,idempotencyKey:key,causeCode:error.code});
648
+ }
649
+ if (!Number.isSafeInteger(first?.fact?.seq) || first.fact.task_id!==task.id) throw new RequestError("初始消息回执不匹配;保留原任务与幂等键重试。",{code:"initial_message_unconfirmed",outcome:"unknown",taskId:task.id,idempotencyKey:key});
650
+ value={...value,first_fact:first.fact,first_fact_seq:first.fact.seq};
651
+ }
517
652
  writeResult(value, options, taskSummary);
518
653
  return;
519
654
  }
@@ -525,7 +660,16 @@ async function run(options) {
525
660
  const value = await request(
526
661
  options,
527
662
  `/api/v1/tasks/${encodeURIComponent(taskId)}/messages`,
528
- { method: "POST", key, body: { text: messageValue(options.message) } },
663
+ {
664
+ method: "POST",
665
+ key,
666
+ body: {
667
+ text: messageValue(options.message),
668
+ ...(options.attachmentIds.length
669
+ ? { attachment_ids: options.attachmentIds }
670
+ : {}),
671
+ },
672
+ },
529
673
  context,
530
674
  );
531
675
  writeResult(
@@ -535,6 +679,29 @@ async function run(options) {
535
679
  );
536
680
  return;
537
681
  }
682
+ if (subject === "lookup") {
683
+ const number = String(rest[0] || "").toUpperCase();
684
+ if (!/^DF-\d+$/.test(number)) throw new UsageError("task lookup 需要 DF 编号。");
685
+ const context = await currentContext(options);
686
+ for (let page = 0, offset = 0; page < 20; page++) {
687
+ const result = await request(options, `/api/v1/tasks?${new URLSearchParams({scope:"workspace",limit:"200",offset:String(offset)})}`, {}, context);
688
+ if (!Array.isArray(result.tasks) || typeof result.hasMore !== "boolean") throw new RequestError("工作区任务分页回执不完整,尚不能判断该编号是否存在。",{code:"task_lookup_incomplete"});
689
+ const matches = result.tasks.filter(task => task.number === number);
690
+ if (matches.length > 1) throw new RequestError("工作区中出现重复任务编号,请核实目标。",{code:"task_lookup_ambiguous"});
691
+ if (matches.length) {
692
+ const task = matches[0];
693
+ if (task.access === "overview") throw new RequestError(`${number} 存在,但当前账号尚未加入任务,请先获得参与权限。`,{code:"task_participation_required",status:403});
694
+ const value = await request(options, `/api/v1/tasks/${encodeURIComponent(task.id)}`, {}, context);
695
+ if (value.task?.id !== task.id || value.task?.number !== number || value.task?.metadata?.data?.workspace_id !== context.workspace || value.task?.access === "overview")
696
+ throw new RequestError("DF 编号、工作区或内容权限读回不一致,暂停操作。",{code:"task_lookup_mismatch"});
697
+ writeResult(value, options, taskSummary);return;
698
+ }
699
+ if (!result.hasMore) throw new RequestError(`当前工作区未找到 ${number},请确认编号。`,{code:"task_not_found",status:404});
700
+ if (!result.tasks.length) break;
701
+ offset += result.tasks.length;
702
+ }
703
+ throw new RequestError("工作区任务查询尚未完成,请稍后再试;不会新建任务。",{code:"task_lookup_incomplete"});
704
+ }
538
705
  if (subject === "get") {
539
706
  const taskId = rest[0];
540
707
  if (!taskId) throw new UsageError("task get 需要 Task ID。");
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-workbench","sourceCommit":"6afd4dec2000e93a971c94d4f18126c10f74a300","sourceDirty":false}
1
+ {"version":"0.3.1","project":"deepflow-workbench","sourceCommit":"b83963f15f4ed187164464dddbfd2ea460f52971","sourceDirty":false}