@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.
- package/bundle/deepflow-release.json +57 -12
- 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-task/SKILL.md +26 -94
- package/bundle/skills/df-task/references/environment.md +50 -0
- 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/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 +12 -5
- package/runtime/df-task/df-task.mjs +17 -5
- 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 +174 -7
- package/runtime/df-workbench/release.json +1 -1
|
@@ -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.
|
|
1
|
+
{"version":"0.3.1","project":"deepflow-task","sourceCommit":"a5b9064298158aad2fef2a67f0baa0a638c89f4b","sourceDirty":false}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
{
|
|
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.
|
|
1
|
+
{"version":"0.3.1","project":"deepflow-workbench","sourceCommit":"b83963f15f4ed187164464dddbfd2ea460f52971","sourceDirty":false}
|