@metagptx/deepflow-suite-cli 0.3.0 → 0.3.2

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.
Files changed (32) hide show
  1. package/bundle/deepflow-release.json +61 -16
  2. package/bundle/skills/df-agentctrl/SKILL.md +9 -87
  3. package/bundle/skills/df-agentctrl/references/environment.md +40 -0
  4. package/bundle/skills/df-agentctrl/references/query-scope.md +50 -0
  5. package/bundle/skills/df-agentrt/SKILL.md +25 -61
  6. package/bundle/skills/df-agentrt/references/environment.md +29 -0
  7. package/bundle/skills/df-agentrt/references/invocation-result.md +11 -0
  8. package/bundle/skills/df-agentrt/references/query-scope.md +30 -0
  9. package/bundle/skills/df-agentrt/references/session-create.md +14 -0
  10. package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
  11. package/bundle/skills/df-task/SKILL.md +30 -92
  12. package/bundle/skills/df-task/references/environment.md +50 -0
  13. package/bundle/skills/df-task/references/fact-types.md +29 -3
  14. package/bundle/skills/df-task/references/query-scope.md +49 -0
  15. package/bundle/skills/df-task/references/reading.md +9 -0
  16. package/bundle/skills/df-taskdriver/SKILL.md +3 -3
  17. package/bundle/skills/df-workbench/SKILL.md +13 -1
  18. package/manifest.json +2 -1
  19. package/package.json +1 -1
  20. package/runtime/df-agentctrl/release.json +1 -1
  21. package/runtime/df-agentrt/release.json +1 -1
  22. package/runtime/df-agentrt/result.mjs +44 -0
  23. package/runtime/df-agentrt/runtime.mjs +36 -4
  24. package/runtime/df-agentrt/usage.mjs +2 -2
  25. package/runtime/df-task/commands.mjs +23 -12
  26. package/runtime/df-task/df-task.mjs +34 -8
  27. package/runtime/df-task/protocol.mjs +1 -1
  28. package/runtime/df-task/read.mjs +127 -0
  29. package/runtime/df-task/release.json +1 -1
  30. package/runtime/df-taskdriver/release.json +1 -1
  31. package/runtime/df-workbench/df-workbench.mjs +274 -14
  32. package/runtime/df-workbench/release.json +1 -1
@@ -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: "",
@@ -46,8 +48,11 @@ export function parseArgs(argv, env = process.env) {
46
48
  after: "",
47
49
  limit: "",
48
50
  scope: "mine",
51
+ offset: "",
49
52
  squadId: undefined,
50
53
  file: undefined,
54
+ attachmentIds: [],
55
+ participantIds: [],
51
56
  };
52
57
  const positionals = [];
53
58
  for (let index = 0; index < argv.length; index += 1) {
@@ -56,6 +61,7 @@ export function parseArgs(argv, env = process.env) {
56
61
  options.json = true;
57
62
  continue;
58
63
  }
64
+ if(argument==='--receipt-only'){options.receiptOnly=true;continue;}
59
65
  if (argument === "--help" || argument === "-h") {
60
66
  options.help = true;
61
67
  continue;
@@ -67,29 +73,71 @@ export function parseArgs(argv, env = process.env) {
67
73
  [
68
74
  "--base-url",
69
75
  "--workspace",
76
+ "--expected-actor",
70
77
  "--idempotency-key",
71
78
  "--message",
72
79
  "--title",
73
80
  "--after",
74
81
  "--limit",
75
82
  "--scope",
83
+ "--offset",
76
84
  "--squad-id",
77
85
  "--file",
86
+ "--attachment-id",
87
+ "--member-id",
88
+ "--assignee-id",
89
+ "--expected-assignee",
90
+ "--participant-id",
91
+ "--creation-mode",
92
+ "--request-seq",
93
+ "--followup-context",
78
94
  ].includes(name)
79
95
  ) {
80
96
  const parsed =
81
97
  inline === undefined ? valueAfter(argv, index, name) : inline;
82
98
  if (inline === undefined) index += 1;
83
99
  if (name === "--base-url") options.baseUrl = parsed;
100
+ if (name === "--creation-mode") {
101
+ if(!['atomic','staged'].includes(parsed))throw new UsageError('--creation-mode 需为 atomic 或 staged。');
102
+ options.creationMode=parsed;
103
+ }
84
104
  if (name === "--workspace") options.workspace = parsed;
105
+ if (name === "--expected-actor") {
106
+ if (!parsed.trim() || parsed.length > 160 || /[\r\n]/.test(parsed))
107
+ throw new UsageError("--expected-actor 需要有效的用户 ID。");
108
+ options.expectedActor = parsed;
109
+ }
85
110
  if (name === "--idempotency-key") options.idempotencyKey = parsed;
86
111
  if (name === "--message") options.message = parsed;
87
112
  if (name === "--title") options.title = parsed;
88
113
  if (name === "--after") options.after = parsed;
89
114
  if (name === "--limit") options.limit = parsed;
90
115
  if (name === "--scope") options.scope = parsed;
116
+ if (name === "--offset") options.offset = parsed;
91
117
  if (name === "--squad-id") options.squadId = parsed;
92
118
  if (name === "--file") options.file = parsed;
119
+ if (name === "--attachment-id") options.attachmentIds.push(parsed);
120
+ if (name === "--member-id") options.memberId = parsed;
121
+ if (name === "--request-seq") options.requestSeq=parsed;
122
+ if (name === "--followup-context") {
123
+ let value;try{value=JSON.parse(parsed);}catch{throw new UsageError('--followup-context 需要原提醒关联 JSON。');}
124
+ if(!value||Array.isArray(value)||Object.keys(value).length!==4||Object.keys(value).some(k=>!['task_instance_id','recipient_id','request_seq','version'].includes(k))||
125
+ typeof value.task_instance_id!=='string'||!value.task_instance_id||value.task_instance_id.length>2048||/[\u0000-\u001f\u007f]/.test(value.task_instance_id)||
126
+ typeof value.recipient_id!=='string'||!/^[\w-]{1,160}$/.test(value.recipient_id)||!Number.isSafeInteger(value.request_seq)||value.request_seq<1||typeof value.version!=='string'||!/^[a-f0-9]{64}$/.test(value.version))throw new UsageError('--followup-context 原提醒关联无效。');
127
+ options.followupContext=value;
128
+ }
129
+ if (name === "--assignee-id") options.assigneeId = parsed;
130
+ if (name === "--expected-assignee") {
131
+ let baseline;
132
+ try { baseline=JSON.parse(parsed); } catch { throw new UsageError('--expected-assignee 需要 JSON 原值基线。'); }
133
+ const v=baseline?.value;
134
+ if(!baseline || Array.isArray(baseline) || typeof baseline.present!=='boolean' ||
135
+ Object.keys(baseline).some(k=>!['present','value'].includes(k)) ||
136
+ (baseline.present ? !Object.hasOwn(baseline,'value') || v!==null && (!v || !['human','agent'].includes(v.kind) || !/^[\w-]{1,100}$/.test(v.id||'') || Object.keys(v).some(k=>!['kind','id'].includes(k))) : Object.hasOwn(baseline,'value')))
137
+ throw new UsageError('--expected-assignee 原值基线无效。');
138
+ options.expectedAssignee=baseline;
139
+ }
140
+ if (name === "--participant-id") options.participantIds.push(parsed);
93
141
  continue;
94
142
  }
95
143
  if (argument.startsWith("--"))
@@ -180,6 +228,7 @@ async function request(options, path, requestOptions = {}, context = {}) {
180
228
  const baseUrl = normalizeBaseUrl(options.baseUrl);
181
229
  const url = new URL(path, `${baseUrl}/`);
182
230
  const headers = requestHeaders(options, context, requestOptions.key);
231
+ if(requestOptions.receiptOnly)headers['X-Deepflow-Message-Receipt']='lookup';
183
232
  if (requestOptions.body !== undefined)
184
233
  headers["Content-Type"] = "application/json";
185
234
  let response;
@@ -221,7 +270,7 @@ async function request(options, path, requestOptions = {}, context = {}) {
221
270
  return data;
222
271
  }
223
272
 
224
- async function currentContext(options) {
273
+ async function currentContext(options,requireWorkspace=true) {
225
274
  const me = await request(
226
275
  options,
227
276
  "/api/v1/me",
@@ -229,12 +278,12 @@ async function currentContext(options) {
229
278
  options.workspace ? { workspace: options.workspace } : {},
230
279
  );
231
280
  const workspace = options.workspace || me.workspaceId;
232
- if (!workspace)
281
+ if (!workspace&&requireWorkspace)
233
282
  throw new RequestError("Workbench 没有返回当前工作区。", {
234
283
  code: "workspace_missing",
235
284
  });
236
285
  if (
237
- Array.isArray(me.workspaces) &&
286
+ workspace && Array.isArray(me.workspaces) &&
238
287
  !me.workspaces.some((item) => item.id === workspace)
239
288
  )
240
289
  throw new RequestError(`当前用户无权访问工作区:${workspace}`, {
@@ -245,11 +294,20 @@ async function currentContext(options) {
245
294
  throw new RequestError("Workbench 没有返回当前用户。", {
246
295
  code: "identity_missing",
247
296
  });
297
+ if (
298
+ options.expectedActor !== undefined &&
299
+ options.expectedActor !== me.user.id
300
+ )
301
+ throw new RequestError("Workbench 操作身份已变化;未提交业务请求。", {
302
+ code: "identity_changed",
303
+ });
248
304
  return { me, workspace, actorId: me.user.id };
249
305
  }
250
306
 
251
307
  async function loadConfig(options) {
252
308
  const context = await currentContext(options);
309
+ if(context.me.user.principalKind==='bot')return {baseUrl:normalizeBaseUrl(options.baseUrl),identity:context.me.user,workspaces:context.me.workspaces,
310
+ workspace:context.me.workspaces.find(w=>w.id===context.workspace),authentication:context.me.authentication,capabilities:context.me.capabilities};
253
311
  const [platform, auth, readiness, capabilities] = await Promise.all([
254
312
  request(options, "/api/v1/platform-state"),
255
313
  request(options, "/api/v1/auth/config"),
@@ -423,18 +481,31 @@ function help() {
423
481
  df-workbench squad get <squad-id> [--json]
424
482
  df-workbench squad create --file <配置.json> [--idempotency-key <key>] [--json]
425
483
  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>]
484
+ df-workbench workspace members [--json]
485
+ df-workbench task create --message <消息> [--title <标题>] [--squad-id <squad-id>] [--assignee-id <user-id>] [--creation-mode <atomic|staged>] [--participant-id <user-id>] [--idempotency-key <key>]
486
+ df-workbench task assign <task-id> --member-id <user-id> [--expected-assignee <JSON基线>] [--idempotency-key <key>]
487
+ df-workbench task member-options <task-id>
488
+ df-workbench task invite <task-id> --member-id <user-id> [--idempotency-key <key>]
427
489
  df-workbench task send <task-id> --message <消息> [--idempotency-key <key>]
490
+ df-workbench task followup-context <task-id> --member-id <user-id> [--request-seq <seq>]
491
+ df-workbench task delivery-context <task-id> --member-id <user-id>
492
+ df-workbench task lookup <DF-number> [--json]
428
493
  df-workbench task get <task-id> [--json]
429
494
  df-workbench task facts <task-id> [--after <seq>] [--limit <n>] [--json]
430
- df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--json]
495
+ df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--offset <n>] [--json]
496
+ df-workbench attachment upload --file <文件> [--idempotency-key <key>] [--json]
497
+
498
+ task create/send 可重复传入 --attachment-id <id>,每次最多 10 个。
499
+ task send 可携带 --followup-context <原提醒JSON>;--receipt-only 仅核对原幂等键的消息回执,不创建新消息。
431
500
 
432
501
  全局配置:
433
502
  DF_WORKBENCH_BASE_URL Workbench 根地址,默认 http://127.0.0.1:8793
434
503
  DF_WORKBENCH_TOKEN OIDC 环境使用的 personal token
435
504
  DF_WORKBENCH_WORKSPACE_ID 当前工作区覆盖值
505
+ Token 获取:公司账号登录 → 个人设置 → 个人 Token
436
506
  --base-url <url> 覆盖 Workbench 根地址
437
507
  --workspace <id> 覆盖当前工作区
508
+ --expected-actor <user-id> 固定操作身份;身份变化时停止请求
438
509
  --json 输出机器可读 JSON`;
439
510
  }
440
511
 
@@ -444,6 +515,13 @@ async function run(options) {
444
515
  process.stdout.write(`${help()}\n`);
445
516
  return;
446
517
  }
518
+ if ((options.assigneeId !== undefined || options.participantIds.length) && !(command === "task" && subject === "create")) throw new UsageError("--assignee-id 和 --participant-id 仅用于 task create。");
519
+ if (options.assigneeId !== undefined && !/^[\w-]{1,100}$/.test(options.assigneeId)) throw new UsageError("初始负责人 ID 无效。");
520
+ if (options.memberId !== undefined && !(command === "task" && ["invite","assign","followup-context","delivery-context"].includes(subject))) throw new UsageError("--member-id 仅用于邀请、指定负责人或核对提醒对象。");
521
+ if(options.followupContext && !(command==='task'&&subject==='send'))throw new UsageError('--followup-context 仅用于向原任务回复。');
522
+ if(options.receiptOnly && !(command==='task'&&subject==='send'))throw new UsageError('--receipt-only 仅用于核对原消息回执。');
523
+ if(options.requestSeq!==undefined && !(command==='task'&&subject==='followup-context'))throw new UsageError('--request-seq 仅用于核对原提醒。');
524
+
447
525
  if (
448
526
  options.squadId !== undefined &&
449
527
  !(command === "task" && subject === "create")
@@ -453,9 +531,57 @@ async function run(options) {
453
531
  );
454
532
  if (
455
533
  options.file !== undefined &&
456
- !(command === "squad" && ["create", "update"].includes(subject))
534
+ !(command === "squad" && ["create", "update"].includes(subject)) &&
535
+ !(command === "attachment" && subject === "upload")
457
536
  )
458
- throw new UsageError("--file 仅用于 squad create/update。");
537
+ throw new UsageError(
538
+ "--file 仅用于 squad create/update 或 attachment upload。",
539
+ );
540
+ if (
541
+ options.attachmentIds.length &&
542
+ !(command === "task" && ["create", "send"].includes(subject))
543
+ )
544
+ throw new UsageError("--attachment-id 仅用于 task create/send。");
545
+ if (
546
+ options.attachmentIds.length > 10 ||
547
+ new Set(options.attachmentIds).size !== options.attachmentIds.length ||
548
+ options.attachmentIds.some((id) => !/^[\w-]{1,100}$/.test(id))
549
+ )
550
+ throw new UsageError("附件列表无效,每次最多发送 10 个不同附件。");
551
+ if (command === "attachment" && subject === "upload") {
552
+ if (!options.file) throw new UsageError("attachment upload 需要 --file。");
553
+ const fileInfo = await stat(options.file);
554
+ if (!fileInfo.isFile() || !fileInfo.size || fileInfo.size > 4 * 1024 * 1024)
555
+ throw new UsageError("每个附件需为 1 字节至 4 MiB。");
556
+ const bytes = await readFile(options.file);
557
+ const filename = basename(options.file);
558
+ if (!bytes.length || bytes.length > 4 * 1024 * 1024)
559
+ throw new UsageError("每个附件需为 1 字节至 4 MiB。");
560
+ if (
561
+ !filename.trim() ||
562
+ filename.length > 180 ||
563
+ /[\x00-\x1f\x7f\\]/.test(filename)
564
+ )
565
+ throw new UsageError("附件名称无效。");
566
+ const context = await currentContext(options);
567
+ const value = await request(
568
+ options,
569
+ "/api/v1/task-attachments",
570
+ {
571
+ method: "POST",
572
+ key: idempotencyKey(options),
573
+ body: { filename, bytesBase64: bytes.toString("base64") },
574
+ },
575
+ context,
576
+ );
577
+ writeResult(
578
+ value,
579
+ options,
580
+ (value) =>
581
+ `已上传附件:${value.attachment?.filename} (${value.attachment?.id})`,
582
+ );
583
+ return;
584
+ }
459
585
  if (command === "squad") return squadCommand(options, subject, rest);
460
586
  if (command === "health") {
461
587
  const value = await request(options, "/healthz");
@@ -463,12 +589,12 @@ async function run(options) {
463
589
  return;
464
590
  }
465
591
  if (command === "me") {
466
- const context = await currentContext(options);
592
+ const context = await currentContext(options,false);
467
593
  writeResult(
468
594
  context.me,
469
595
  options,
470
596
  (value) =>
471
- `用户: ${value.user.name} (${value.user.id})\n工作区: ${value.workspaceId}`,
597
+ `身份: ${value.user.principalKind||"human"} · ${value.user.name} (${value.user.id})\n工作区: ${value.workspaceId}`,
472
598
  );
473
599
  return;
474
600
  }
@@ -482,16 +608,84 @@ async function run(options) {
482
608
  );
483
609
  return;
484
610
  }
611
+ if (command === "workspace" && subject === "members") {
612
+ const context = await currentContext(options);
613
+ const value = await request(options,"/api/v1/members",{},context);
614
+ if (!Array.isArray(value.members)) throw new RequestError("工作区成员目录回执无效。",{code:"invalid_member_directory"});
615
+ writeResult(value,options,value => value.members.map(member=>`${member.userId} ${member.name || ""}`).join("\n"));return;
616
+ }
485
617
  if (command !== "task") throw new UsageError(`不支持的命令:${command}`);
618
+ if (subject === 'transfer-owner') throw new UsageError('此任务操作已停用。');
619
+ if(subject==='capabilities') {
620
+ const context=await currentContext(options),value=await request(options,'/api/v1/task-ui/capabilities',{},context);
621
+ writeResult(value,options,value=>JSON.stringify(value));return;
622
+ }
623
+ if(['followup-context','delivery-context'].includes(subject)) {
624
+ const taskId=rest[0];
625
+ if(!/^[\w-]{1,160}$/.test(taskId||'')||!/^[\w-]{1,160}$/.test(options.memberId||''))throw new UsageError('需要任务 ID 和 --member-id 提醒对象。');
626
+ const context=await currentContext(options),query=new URLSearchParams({recipient_id:options.memberId});
627
+ if(options.requestSeq!==undefined)query.set('request_seq',String(integerOption(options.requestSeq,'--request-seq',0,1,Number.MAX_SAFE_INTEGER)));
628
+ const value=await request(options,`/api/v1/task-ui/tasks/${encodeURIComponent(taskId)}/${subject}?${query}`,{},context);
629
+ if(value?.taskId!==taskId||value.recipientId!==options.memberId||typeof value.taskInstanceId!=='string'||!value.taskInstanceId||!Number.isSafeInteger(value.requestSeq)||value.requestSeq<1||!/^[a-f0-9]{64}$/.test(value.version||''))throw new RequestError('原提醒关联回执不匹配。',{code:'task_lookup_mismatch'});
630
+ if(subject==='delivery-context'&&(value.kind!=='delivery'||!Array.isArray(value.artifacts)||!value.artifacts.length||!Number.isSafeInteger(value.startedSeq)||!Number.isSafeInteger(value.endedSeq)))throw new RequestError('交付版本回执不匹配。',{code:'task_lookup_mismatch'});
631
+ writeResult(value,options,value=>`原请求 #${value.requestSeq}:${value.text}`);return;
632
+ }
633
+ if (["member-options", "invite", "assign"].includes(subject)) {
634
+ const taskId = rest[0];
635
+ if (!taskId || !/^[\w-]{1,160}$/.test(taskId)) throw new UsageError("需要有效的 Task ID。");
636
+ const writes = subject !== "member-options";
637
+ if (writes && !/^[\w-]{1,100}$/.test(options.memberId || "")) throw new UsageError("需要 --member-id 指定有效的 DF 用户 ID。");
638
+ const context = await currentContext(options);
639
+ let value;
640
+ if(subject==='assign') {
641
+ const current=(await request(options,`/api/v1/tasks/${encodeURIComponent(taskId)}`,{},context)).task;
642
+ if(current?.id!==taskId||current.metadata?.data?.workspace_id!==context.workspace)throw new RequestError('任务范围未确认。',{code:'task_metadata_mismatch'});
643
+ const presentation=current.metadata.data.presentation||{},key=idempotencyKey(options);
644
+ const assignee=Object.hasOwn(presentation,'assignee')?{present:true,value:presentation.assignee}:{present:false};
645
+ if(options.expectedAssignee) {
646
+ const expected=options.expectedAssignee;
647
+ const same=expected.present===assignee.present && (!expected.present || expected.value===null&&assignee.value===null || expected.value?.kind===assignee.value?.kind&&expected.value?.id===assignee.value?.id);
648
+ const alreadyAssigned=presentation.assignee?.kind==='human' && presentation.assignee.id===options.memberId;
649
+ if(!same&&!alreadyAssigned)throw new RequestError('负责人已变化,请重新核对分配要求。',{code:'task_assignee_changed',status:409});
650
+ }
651
+ // The original baseline is part of the idempotent command, including after
652
+ // a successful write whose response was lost.
653
+ const baselineAssignee=options.expectedAssignee || assignee;
654
+ value=await request(options,`/api/v1/tasks/${encodeURIComponent(taskId)}/presentation`,{method:'POST',key,body:{changes:{assignee:{kind:'human',id:options.memberId}},baseline:{assignee:baselineAssignee}}},context);
655
+ const saved=value?.task,data=saved?.metadata?.data;
656
+ if(saved?.id!==taskId||data?.workspace_id!==context.workspace||data.presentation?.assignee?.kind!=='human'||data.presentation.assignee.id!==options.memberId)
657
+ throw new RequestError('负责人保存回执未确认,请保留原幂等键核对。',{code:'task_metadata_mismatch',outcome:'unknown',taskId,idempotencyKey:key});
658
+ } else {
659
+ const path=subject==='member-options'?`/api/v1/task-ui/tasks/${encodeURIComponent(taskId)}/property-options`:`/api/v1/tasks/${encodeURIComponent(taskId)}/member-invitations`;
660
+ value=await request(options,path,writes?{method:'POST',key:idempotencyKey(options),body:{invitee_ids:[options.memberId]}}:{},context);
661
+ }
662
+ writeResult(value, options, value => JSON.stringify(value));
663
+ return;
664
+ }
486
665
  if (subject === "create") {
487
666
  const body = buildCreateBody(
488
667
  options.message,
489
668
  options.title,
490
669
  options.squadId,
491
670
  );
671
+ if (options.assigneeId) {
672
+ if (![options.assigneeId,...options.participantIds].every(id=>/^[\w-]{1,100}$/.test(id))) throw new UsageError("初始负责人和参与人 ID 无效。");
673
+ if(options.creationMode==='atomic') {body.initial_presentation={assignee:{kind:'human',id:options.assigneeId}};body.first_message=messageValue(options.message);}
674
+ else {body.initial_assignee_id=options.assigneeId;body.first_message="";}
675
+ body.invitee_ids=[...new Set(options.participantIds)].sort();
676
+ } else if(options.participantIds.length) throw new UsageError("--participant-id 需要同时指定 --assignee-id。");
677
+ if (options.attachmentIds.length && (!options.assigneeId || options.creationMode==='atomic'))
678
+ body.attachment_ids = options.attachmentIds;
492
679
  const context = await currentContext(options);
680
+ if(context.me.user.principalKind==='bot') {
681
+ if(options.assigneeId||options.squadId||options.attachmentIds.length)throw new UsageError('Bot 创建任务不支持指定人工负责人、小队或初始附件;附件可在创建后通过 task send 提交。');
682
+ delete body.conversation;
683
+ body.title ||= Array.from(body.description.split('\n')[0]).slice(0,80).join('');
684
+ }
493
685
  const key = idempotencyKey(options);
494
- const value = await request(
686
+ let atomicCreation=options.creationMode==='atomic';
687
+ let value;
688
+ try {value = await request(
495
689
  options,
496
690
  "/api/v1/tasks",
497
691
  {
@@ -500,7 +694,17 @@ async function run(options) {
500
694
  body,
501
695
  },
502
696
  context,
503
- );
697
+ );} catch(error) {
698
+ // A checked validation rejection has not applied the staged command. An
699
+ // idempotency conflict can also be an atomic receipt from before upgrade.
700
+ // Reuse the original key; never fall back after an unknown write outcome.
701
+ const rejected=error instanceof RequestError && (error.status===422 && error.code==='invalid_input' || error.status===409 && error.code==='idempotency_conflict');
702
+ if(!options.assigneeId || atomicCreation || !rejected)throw error;
703
+ const atomicBody={...body,first_message:messageValue(options.message),initial_presentation:{assignee:{kind:'human',id:options.assigneeId}},...(options.attachmentIds.length?{attachment_ids:options.attachmentIds}:{})};
704
+ delete atomicBody.initial_assignee_id;
705
+ value=await request(options,'/api/v1/tasks',{method:'POST',key,body:atomicBody},context);
706
+ atomicCreation=true;
707
+ }
504
708
  if (
505
709
  body.squad_id &&
506
710
  value?.task?.metadata?.data?.squad?.squadId !== body.squad_id
@@ -514,6 +718,27 @@ async function run(options) {
514
718
  idempotencyKey: key,
515
719
  },
516
720
  );
721
+ if (options.assigneeId) {
722
+ const task=value?.task,data=task?.metadata?.data;
723
+ if (!task?.id || data?.workspace_id!==context.workspace || (data.presentation?.assignee?.kind!=='human'||data.presentation.assignee.id!==options.assigneeId) || data.created_by!==context.actorId ||
724
+ ![context.actorId,options.assigneeId,...options.participantIds].every(id=>data.members?.includes(id))) throw new RequestError("已创建的任务初始负责人或成员回执不匹配,请保留原幂等键核对,不要重建。",{code:"task_metadata_mismatch",outcome:"unknown",taskId:task?.id,idempotencyKey:key});
725
+ // Atomic creation freezes the initial message and squad in the server's
726
+ // creation receipt; only the staged protocol needs a second message.
727
+ if(!atomicCreation) {
728
+ // Current squad edits must never change a retry of the original message.
729
+ const agentId=data.initial_message_context?.agentId;
730
+ if (!agentId) throw new RequestError("任务已创建,但创建时的小队快照没有可接收首条消息的伙伴;请保留原任务与幂等键核对。",{code:"initial_agent_unavailable",outcome:"unknown",taskId:task.id,idempotencyKey:key});
731
+ const firstKey="initial-message:"+createHash("sha256").update(key).digest("hex");
732
+ let first;
733
+ try {
734
+ 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);
735
+ } catch(error) {
736
+ throw new RequestError(`任务 ${task.id} 已创建,首条消息回执尚待确认;保留幂等键 ${key} 重试,不换键或重建。`,{code:"initial_message_unconfirmed",outcome:"unknown",taskId:task.id,idempotencyKey:key,causeCode:error.code});
737
+ }
738
+ 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});
739
+ value={...value,first_fact:first.fact,first_fact_seq:first.fact.seq};
740
+ }
741
+ }
517
742
  writeResult(value, options, taskSummary);
518
743
  return;
519
744
  }
@@ -525,7 +750,18 @@ async function run(options) {
525
750
  const value = await request(
526
751
  options,
527
752
  `/api/v1/tasks/${encodeURIComponent(taskId)}/messages`,
528
- { method: "POST", key, body: { text: messageValue(options.message) } },
753
+ {
754
+ method: "POST",
755
+ key,
756
+ receiptOnly:options.receiptOnly===true,
757
+ body: {
758
+ text: messageValue(options.message),
759
+ ...(options.followupContext?{followup_context:options.followupContext}:{}),
760
+ ...(options.attachmentIds.length
761
+ ? { attachment_ids: options.attachmentIds }
762
+ : {}),
763
+ },
764
+ },
529
765
  context,
530
766
  );
531
767
  writeResult(
@@ -535,6 +771,29 @@ async function run(options) {
535
771
  );
536
772
  return;
537
773
  }
774
+ if (subject === "lookup") {
775
+ const number = String(rest[0] || "").toUpperCase();
776
+ if (!/^DF-\d+$/.test(number)) throw new UsageError("task lookup 需要 DF 编号。");
777
+ const context = await currentContext(options);
778
+ for (let page = 0, offset = 0; page < 20; page++) {
779
+ const result = await request(options, `/api/v1/tasks?${new URLSearchParams({scope:"workspace",limit:"200",offset:String(offset)})}`, {}, context);
780
+ if (!Array.isArray(result.tasks) || typeof result.hasMore !== "boolean") throw new RequestError("工作区任务分页回执不完整,尚不能判断该编号是否存在。",{code:"task_lookup_incomplete"});
781
+ const matches = result.tasks.filter(task => task.number === number);
782
+ if (matches.length > 1) throw new RequestError("工作区中出现重复任务编号,请核实目标。",{code:"task_lookup_ambiguous"});
783
+ if (matches.length) {
784
+ const task = matches[0];
785
+ if (task.access === "overview") throw new RequestError(`${number} 存在,但当前账号尚未加入任务,请先获得参与权限。`,{code:"task_participation_required",status:403});
786
+ const value = await request(options, `/api/v1/tasks/${encodeURIComponent(task.id)}`, {}, context);
787
+ if (value.task?.id !== task.id || value.task?.number !== number || value.task?.metadata?.data?.workspace_id !== context.workspace || value.task?.access === "overview")
788
+ throw new RequestError("DF 编号、工作区或内容权限读回不一致,暂停操作。",{code:"task_lookup_mismatch"});
789
+ writeResult(value, options, taskSummary);return;
790
+ }
791
+ if (!result.hasMore) throw new RequestError(`当前工作区未找到 ${number},请确认编号。`,{code:"task_not_found",status:404});
792
+ if (!result.tasks.length) break;
793
+ offset += result.tasks.length;
794
+ }
795
+ throw new RequestError("工作区任务查询尚未完成,请稍后再试;不会新建任务。",{code:"task_lookup_incomplete"});
796
+ }
538
797
  if (subject === "get") {
539
798
  const taskId = rest[0];
540
799
  if (!taskId) throw new UsageError("task get 需要 Task ID。");
@@ -583,6 +842,7 @@ async function run(options) {
583
842
  `/api/v1/tasks?${new URLSearchParams({
584
843
  scope: options.scope,
585
844
  limit: String(limit),
845
+ offset: String(integerOption(options.offset, "--offset", 0, 0, 1048576)),
586
846
  include_state: "1",
587
847
  })}`,
588
848
  {},
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-workbench","sourceCommit":"6afd4dec2000e93a971c94d4f18126c10f74a300","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-workbench","sourceCommit":"adfb558b1c3532b338b5a8b0d5c11a43739e4ca6","sourceDirty":false}