@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.
- package/bundle/deepflow-release.json +61 -16
- package/bundle/skills/df-agentctrl/SKILL.md +9 -87
- package/bundle/skills/df-agentctrl/references/environment.md +40 -0
- package/bundle/skills/df-agentctrl/references/query-scope.md +50 -0
- package/bundle/skills/df-agentrt/SKILL.md +25 -61
- package/bundle/skills/df-agentrt/references/environment.md +29 -0
- package/bundle/skills/df-agentrt/references/invocation-result.md +11 -0
- package/bundle/skills/df-agentrt/references/query-scope.md +30 -0
- package/bundle/skills/df-agentrt/references/session-create.md +14 -0
- package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
- package/bundle/skills/df-task/SKILL.md +30 -92
- package/bundle/skills/df-task/references/environment.md +50 -0
- package/bundle/skills/df-task/references/fact-types.md +29 -3
- package/bundle/skills/df-task/references/query-scope.md +49 -0
- package/bundle/skills/df-task/references/reading.md +9 -0
- package/bundle/skills/df-taskdriver/SKILL.md +3 -3
- package/bundle/skills/df-workbench/SKILL.md +13 -1
- package/manifest.json +2 -1
- package/package.json +1 -1
- package/runtime/df-agentctrl/release.json +1 -1
- package/runtime/df-agentrt/release.json +1 -1
- package/runtime/df-agentrt/result.mjs +44 -0
- package/runtime/df-agentrt/runtime.mjs +36 -4
- package/runtime/df-agentrt/usage.mjs +2 -2
- package/runtime/df-task/commands.mjs +23 -12
- package/runtime/df-task/df-task.mjs +34 -8
- package/runtime/df-task/protocol.mjs +1 -1
- package/runtime/df-task/read.mjs +127 -0
- package/runtime/df-task/release.json +1 -1
- package/runtime/df-taskdriver/release.json +1 -1
- package/runtime/df-workbench/df-workbench.mjs +274 -14
- 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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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.
|
|
1
|
+
{"version":"0.3.2","project":"deepflow-workbench","sourceCommit":"adfb558b1c3532b338b5a8b0d5c11a43739e4ca6","sourceDirty":false}
|