@metagptx/deepflow-suite-cli 0.3.1 → 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.
@@ -2,13 +2,13 @@
2
2
  "schemaVersion": 1,
3
3
  "distribution": "deepflow-suite",
4
4
  "package": "@metagptx/deepflow-suite-cli",
5
- "version": "0.3.1",
5
+ "version": "0.3.2",
6
6
  "platform": "any",
7
7
  "arch": "any",
8
8
  "cli": {
9
9
  "name": "deepflow-suite",
10
10
  "package": "@metagptx/deepflow-suite-cli",
11
- "version": "0.3.1",
11
+ "version": "0.3.2",
12
12
  "command": "deepflow-suite"
13
13
  },
14
14
  "skills": [
@@ -80,8 +80,8 @@
80
80
  },
81
81
  {
82
82
  "path": "references/fact-types.md",
83
- "size": 3974,
84
- "sha256": "5dd10c73e652d018ce20617ec8a5e1becb55fb935f15a366831050063a8989fc"
83
+ "size": 6174,
84
+ "sha256": "efab99cb46ad24100e834efd3e7ed4ab5e5004bb4b32320cff08ac14d5fa0070"
85
85
  },
86
86
  {
87
87
  "path": "references/query-scope.md",
@@ -95,8 +95,8 @@
95
95
  },
96
96
  {
97
97
  "path": "SKILL.md",
98
- "size": 13393,
99
- "sha256": "cd1ffc54bf884f02e540ddd4b75c805ed868e32d298d20abcecb3c8af51feb8b"
98
+ "size": 14374,
99
+ "sha256": "357d805b8260d0e67a46a7edaef4f443c7be58dec075bfc2f176a83e88ebb5d1"
100
100
  }
101
101
  ]
102
102
  },
@@ -184,8 +184,8 @@
184
184
  "files": [
185
185
  {
186
186
  "path": "SKILL.md",
187
- "size": 3492,
188
- "sha256": "f04a22e7967727217003071b7ad7c9268fde9058af4e1f8d3de22e28e18ac718"
187
+ "size": 4577,
188
+ "sha256": "8c9020f12674d83e0ea88088c4d556a2dc76c39f8372f0f54a26d95b85277352"
189
189
  }
190
190
  ]
191
191
  }
@@ -81,6 +81,10 @@ stderr 上的话是给你看的,照着做:
81
81
  - 说「remain configured」——仓库加上了,只是检出没成功,可以用 `multica repo checkout <URL>` 重试;
82
82
  - 其余的错误信息来自服务端,已经写清楚了原因和下一步。
83
83
 
84
+ 公司 GitLab 的 SSH 检出因主机校验或 `Permission denied (publickey)` 失败时,个人 Token 和共享 Token 均应通过 HTTPS 使用,不按有没有个人 Token 选择 SSH。若尚未生成工作副本,可将会话中已授权的 `git@gitlab.deepwisdomai.com:<组>/<仓库>.git` 换为同项目的 `https://gitlab.deepwisdomai.com/<组>/<仓库>.git`,保留原 ref,通过 `df-agentrt repo add <HTTPS地址> --ref <原ref>` 注册并检出;原配置未指定 ref 时省略 `--ref`。不要直接把改写后的 URL 交给 `multica repo checkout`,它尚不在精确匹配的白名单中。HTTPS 仍失败时按实际错误报告,不扩大仓库范围或自行提供凭据。
85
+
86
+ 已有工作副本时,不为切换传输方式再次 `repo add` 或 `checkout`,以免重置已有工作;在原目录使用同项目 HTTPS 完成所需 fetch,提交推送遵循已加载的 Git skill。daemon 缓存按仓库 host/path 复用,并不按 SSH/HTTPS 分成两份;检出成功不能证明最新远端 ref 已拉取,按所需 ref 核对远端和实际起点。
87
+
84
88
  ## 加完之后
85
89
 
86
90
  检出目录和你当前的工作目录是同级的。用 `repo add` 打印出来的绝对路径访问,
@@ -31,6 +31,12 @@ metadata 的 `source`、`query_keys` 用于可选软范围过滤,`data` 由业
31
31
 
32
32
  托管 Run 中 `df-task` 由 Runtime 镜像提供。命令缺失或 `--help` 无法执行时,报告执行环境异常并结束;不要在 Run 中安装、编译 CLI 或猜测绝对路径来继续。非托管的本地开发可按项目 README 安装 CLI。**注意不要把 runtime 的地址填成 Task 服务的地址** —— `df-agentrt` 和 `df-task` 是两个服务、两个端口、两套授权。
33
33
 
34
+ ## 创建来源与人工参与请求
35
+
36
+ 涉及人的协作时,从 `get` 的不可变 `creator.kind/id` 核对创建来源;owner、当前成员、经办人及正文不能代替。读取来源未知时保持未知。是否选人、何时求助、授予什么责任,由当前 Agent 定义和本次授权判断。
37
+
38
+ 普通 `handoff --ask` 只写公开交接,不请求成员加入。需要实际人工参与时,读取[人工参与请求与回执](references/fact-types.md#请求实际人工参与),使用 `handoff --participation ensure_member --assignee <真实账号>`;明确授予该人主经办责任时才附加 `--task-assignee`。请求写入成功不等于成员、通知或分配已生效;按请求事实序号核对平台回执,部分成功、失败与未知分别保留。旧 CLI 缺少这些参数时,只有在已加载协议且平台支持的前提下,才可用通用 `append` 提交同一载荷,不能猜字段或把公开答复当作实际邀请。
39
+
34
40
  ## 读日志
35
41
 
36
42
  先确定本轮要作出的判断,再按需选择事实;不要求每轮先通读全部日志。当前描述与版本用上文的 `get` 读取,事实用于补足决策依据。
@@ -6,8 +6,8 @@
6
6
 
7
7
  | 类型 | 谁写 | 必填 | 可选 |
8
8
  | --- | --- | --- | --- |
9
- | `user_message` | 人 | `text` | `wake_policy`、`agent_ids` |
10
- | `external_result` | 外部系统 | `source`、`ref`、`result` | —— |
9
+ | `user_message` | 人或 bot | `text` | `wake_policy`、`agent_ids` |
10
+ | `external_result` | 外部系统或 bot | `source`、`ref`、`result` | —— |
11
11
 
12
12
  `wake_policy` 由产品端写入,缺失时按 `default` 处理。`passive` 表示多人对话里人和人之间的讨论,它**不触发**调度,被别的事件唤醒时也只能当作上下文,不能单独据此派活。`agent_ids` 只是人提到的 Agent,作为提示;派给谁仍由调度根据全局事实决定。
13
13
 
@@ -32,7 +32,7 @@
32
32
  | --- | --- | --- | --- |
33
33
  | `dispatch_intent` | `agent`、`instruction`、`mode`;`mode=message` 时还必须有 `target_run_id` | `agent_version`(Driver 读取的正整数版本) | `new_run` 必须省略 `target_run_id`,由 Driver 根据 `task_id + intent_seq` 派生逻辑 Run;`message` 指向已有逻辑 Run并复用原 Session/AgentData,不切换版本。简写 `df-task dispatch` 不支持版本/幂等键,需固定时用 `append` |
34
34
  | `waiting_reason` | `waiting_on`、`reason` | `waiting_run_id`(实际执行 ID) | 给人和给下一轮调度的上下文,**不是推进器的门** |
35
- | `handoff_to_human` | `ask` | `assignee` | 交球和派活是同一条事实,区别只在 `ask` 的内容 |
35
+ | `handoff_to_human` | `ask` | `assignee`、`participation`、`task_assignee` | 交球和派活是同一条事实,区别只在 `ask` 的内容 |
36
36
  | `adjudication` | `subject`、`verdict` | `reason` | `subject` 是被裁决那条事实的**序号**(数字,不是字符串) |
37
37
  | `run_terminated` | `run_id`、`confirmed_status` | —— | 已经停完并确认之后记录的既成事实 |
38
38
 
@@ -48,3 +48,29 @@
48
48
  | `attempt_failed` | `watermark`、`reason` | `intent_seq` | 推进失败 |
49
49
 
50
50
  **「有 started 没有 accepted」是合法的中间状态**,含义是「正在启动,结果未知」。读日志时不要把它当成异常 —— 推进器会靠幂等键重投把它推进到 accepted。`run_kind` 只能是 `scheduler` 或 `task`;只有调度唤起带 `input_watermark`。
51
+
52
+ ## 请求实际人工参与
53
+
54
+ 公开 handoff 本身不等于邀请。平台支持时,scheduler_run 可用以下命令写入实际参与请求;账号须已由调用方核验,`ask` 向接收人说明具体协助事项和相关成果。
55
+
56
+ ```bash
57
+ # 邀请并定向通知;保留现有经办人
58
+ df-task handoff "$DF_TASK_ID" --participation ensure_member \
59
+ --assignee "$human_id" --ask "$handoff_text" --json
60
+ # 同时明确授予同一人主经办责任
61
+ df-task handoff "$DF_TASK_ID" --participation ensure_member \
62
+ --assignee "$human_id" --task-assignee "$assignment_json" \
63
+ --ask "$handoff_text" --json
64
+ ```
65
+
66
+ `assignment_json` 是 `{"kind":"human","id":"与 assignee 相同的真实账号"}`。省略该参数不改经办人,普通答复、补信息与审核不得因定向交接自动改派。通用 `append --type handoff_to_human --file <JSON>` 可提交同一协议:`ask`、`assignee`、`participation: "ensure_member"`,以及明确需要时的 `task_assignee`;不新增另一套操作语义。
67
+
68
+ 命令成功只确认请求事实已写入。读取 `external_result`,以 `source="human_participation"`、`ref="请求事实序号"` 关联回执;`details.request_seq` 应与该序号一致:
69
+
70
+ - `succeeded`:核对 `member_confirmed`、`member_id`、`notification_id`;请求分配时再核对 `task_assignee`,并读回当前成员与经办人。
71
+ - `failed`:保留错误码及部分成功字段。成员已加入或通知已生成,不代表分配也成功;不得把失败请求当作已交接全部责任。
72
+ - `unknown` 或尚无回执:结果未确认,后续核对同一请求;平台恢复会沿原请求兑现,不因暂未确认而重复邀请。后续真实成功回执可补充先前未知结果,按实际操作及当前状态判断。
73
+
74
+ 通知记录生成不证明人已阅读、回复或批准。邀请、经办人设置、人的实际处理和审核意见是不同事实;本协议不决定何时求助、选择谁或是否接受审核结果。
75
+
76
+ 创建身份从 `df-task get` 的不可变 `creator` 读取,不能从 owner、成员人数或任意 metadata 推断为 bot。未知身份保持未知。
package/manifest.json CHANGED
@@ -56,5 +56,6 @@
56
56
  "project": "deepflow-agent-runtime",
57
57
  "entry": "cli/df-agentrt.mjs"
58
58
  }
59
- ]
59
+ ],
60
+ "version": "0.3.2"
60
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metagptx/deepflow-suite-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Agent-friendly installer and distribution CLI for the DeepFlow suite.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-agent-ctrl","sourceCommit":"10e745773559f4712b04ab61915a018995e30630","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-agent-ctrl","sourceCommit":"dee1a88615b09bd979562902cebc631f81b1859b","sourceDirty":false}
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-agent-runtime","sourceCommit":"420ebb91ed4cf92ca81eb8c295cc111f94b9da2c","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-agent-runtime","sourceCommit":"4492edf7279289b6ca1d29d3d2b444c0757f2b81","sourceDirty":false}
@@ -3,7 +3,7 @@ export const help = `df-task — DeepFlow Task fact log CLI
3
3
  Usage:
4
4
  df-task ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
5
5
  df-task get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]
6
- df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>]
6
+ df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]
7
7
  df-task facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
8
8
  df-task facts <task-id> --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]
9
9
  df-task append <task-id> --type <type> --file payload.json [--idempotency-key <k>]
@@ -14,7 +14,7 @@ Usage:
14
14
  Scheduling shorthands (each one is a wrapper around append):
15
15
  df-task dispatch <task-id> --agent <name> --instruction <text> [--run <run-id>]
16
16
  df-task wait <task-id> --on <what> --reason <text> [--waiting-run <run-id>]
17
- df-task handoff <task-id> --ask <text> [--assignee <who>]
17
+ df-task handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]
18
18
 
19
19
  Global options (before or after the command):
20
20
  --base-url <url> Service URL (or DF_TASK_BASE_URL)
@@ -23,7 +23,7 @@ Global options (before or after the command):
23
23
 
24
24
  Environment:
25
25
  DF_TASK_BASE_URL Service URL, default http://127.0.0.1:8090
26
- DF_TASK_ACTOR_KIND Write identity: human | external | task_run | scheduler_run | driver
26
+ DF_TASK_ACTOR_KIND Write identity: human | bot | external | task_run | scheduler_run | driver
27
27
  DF_TASK_ACTOR_ID Write identity id, e.g. the current run id
28
28
 
29
29
  Facts are append-only. The API has no way to change or remove a fact, so neither does this CLI.
@@ -54,13 +54,14 @@ export const commands = {
54
54
  "writes": false
55
55
  },
56
56
  "create": {
57
- "usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>]",
57
+ "usage": "create --title <text> [--description <text>] [--message <text>] [--metadata <json>] [--idempotency-key <key>]",
58
58
  "description": "Create a task, optionally with its first user message in the same transaction.",
59
59
  "flags": [
60
60
  "--title",
61
61
  "--description",
62
62
  "--message",
63
- "--metadata"
63
+ "--metadata",
64
+ "--idempotency-key"
64
65
  ],
65
66
  "required": [
66
67
  "--title"
@@ -173,11 +174,14 @@ export const commands = {
173
174
  "writes": true
174
175
  },
175
176
  "handoff": {
176
- "usage": "handoff <task-id> --ask <text> [--assignee <who>]",
177
+ "usage": "handoff <task-id> --ask <text> [--assignee <who>] [--participation ensure_member] [--task-assignee <json>]",
177
178
  "description": "Append a handoff to a human. Handing the ball back and asking a human to do a step are the same fact.",
178
179
  "flags": [
179
180
  "--ask",
180
- "--assignee"
181
+ "--assignee",
182
+ "--participation",
183
+ "--task-assignee",
184
+ "--idempotency-key"
181
185
  ],
182
186
  "required": [
183
187
  "--ask"
@@ -58,8 +58,21 @@ export function validate(o, env) {
58
58
  }
59
59
  if (spec.writes) {
60
60
  if (!env.DF_TASK_ACTOR_KIND?.trim() || !env.DF_TASK_ACTOR_ID?.trim()) throw Error('writing a fact requires DF_TASK_ACTOR_KIND and DF_TASK_ACTOR_ID');
61
+ if (o.command === 'create') {
62
+ const key = o.values['--idempotency-key'] || '';
63
+ if (key.length > 200 || /[^\x21-\x7e]/.test(key) || (env.DF_TASK_ACTOR_KIND === 'bot' && key.length < 8)) throw Error('creation idempotency key must be at most 200 visible ASCII bytes; bot creation requires at least 8 bytes');
64
+ }
61
65
  if (!actorKinds.includes(env.DF_TASK_ACTOR_KIND)) throw Error(`unknown DF_TASK_ACTOR_KIND ${env.DF_TASK_ACTOR_KIND}`);
62
- if (o.command === 'metadata' && env.DF_TASK_ACTOR_KIND !== 'human') throw Error('metadata requires DF_TASK_ACTOR_KIND=human');
66
+ if (o.command === 'metadata' && !['human','bot'].includes(env.DF_TASK_ACTOR_KIND)) throw Error('metadata requires DF_TASK_ACTOR_KIND=human or bot');
67
+ }
68
+ if (o.command === 'handoff' && ('--participation' in o.values || '--task-assignee' in o.values)) {
69
+ const target = o.values['--assignee'];
70
+ if (o.values['--participation'] !== 'ensure_member') throw Error('--participation must be ensure_member for member operations');
71
+ if (!target || target.trim() !== target || Buffer.byteLength(target) > 200) throw Error('member operations require a non-empty --assignee of at most 200 bytes without surrounding whitespace');
72
+ if ('--task-assignee' in o.values) {
73
+ const assignment = object(o.values['--task-assignee'], '--task-assignee must be a JSON object');
74
+ if (Object.keys(assignment).length !== 2 || assignment.kind !== 'human' || assignment.id !== target) throw Error('--task-assignee must identify the same human as --assignee');
75
+ }
63
76
  }
64
77
  validateReadOptions(o);
65
78
  }
@@ -95,8 +108,8 @@ export async function requestFor(o) {
95
108
  let type, payload;
96
109
  if (c === 'dispatch') { type = 'dispatch_intent'; payload = { agent: v['--agent'], instruction: v['--instruction'], mode: v['--run'] ? 'message' : 'new_run', ...(v['--run'] ? { target_run_id: v['--run'] } : {}) }; }
97
110
  if (c === 'wait') { type = 'waiting_reason'; payload = { waiting_on: v['--on'], reason: v['--reason'], ...(v['--waiting-run'] ? { waiting_run_id: v['--waiting-run'] } : {}) }; }
98
- if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}) }; }
99
- return post(`${path}/facts`, { type, payload });
111
+ if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}), ...(v['--participation'] ? { participation: v['--participation'] } : {}), ...(v['--task-assignee'] ? { task_assignee: object(v['--task-assignee'], '--task-assignee must be a JSON object') } : {}) }; }
112
+ return post(`${path}/facts`, { type, payload, ...(v['--idempotency-key'] ? {idempotency_key:v['--idempotency-key']} : {}) });
100
113
  }
101
114
  function human(command, data, stdout) {
102
115
  const page = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value) || {};
@@ -149,6 +162,7 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
149
162
  const request = async (options, limit = 0) => {
150
163
  const req = options === o ? initialRequest : await requestFor(options);
151
164
  const headers = req.body ? { 'Content-Type': 'application/json' } : {};
165
+ if (o.command === 'create' && o.values['--idempotency-key']) headers['Idempotency-Key'] = o.values['--idempotency-key'];
152
166
  if (req.method !== 'GET') { headers['X-DF-Actor-Kind'] = env.DF_TASK_ACTOR_KIND; headers['X-DF-Actor-Id'] = env.DF_TASK_ACTOR_ID; }
153
167
  const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
154
168
  const data = await readResponse(response, limit);
@@ -1,3 +1,3 @@
1
1
  // Generated by generate-protocol.mjs from internal/model/fact.go.
2
2
  export const factTypes = ["task_lifecycle","user_message","external_result","artifact_published","summary","help_requested","run_outcome","dispatch_intent","waiting_reason","handoff_to_human","adjudication","run_terminated","invocation_started","invocation_accepted","invocation_ended","attempt_failed"];
3
- export const actorKinds = ["human","external","task_run","scheduler_run","driver"];
3
+ export const actorKinds = ["human","bot","external","task_run","scheduler_run","driver"];
@@ -18,7 +18,7 @@ export function validateReadOptions(o) {
18
18
  const v = o.values;
19
19
  if ('--fields' in v) {
20
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'];
21
+ const roots = o.command === 'get' ? ['id', 'title', 'description', 'description_version', 'creator', 'metadata', 'created_at', 'updated_at'] : ['seq', 'type', 'actor_kind', 'actor_id', 'payload', 'idempotency_key', 'created_at'];
22
22
  const paths = v['--fields'].split(',');
23
23
  paths.forEach((field, i) => {
24
24
  const parts = field.split('.');
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-task","sourceCommit":"a5b9064298158aad2fef2a67f0baa0a638c89f4b","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-task","sourceCommit":"cc53e86834b19e8a4fe73f9e9a08d76853d51e51","sourceDirty":false}
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}
@@ -48,6 +48,7 @@ export function parseArgs(argv, env = process.env) {
48
48
  after: "",
49
49
  limit: "",
50
50
  scope: "mine",
51
+ offset: "",
51
52
  squadId: undefined,
52
53
  file: undefined,
53
54
  attachmentIds: [],
@@ -60,6 +61,7 @@ export function parseArgs(argv, env = process.env) {
60
61
  options.json = true;
61
62
  continue;
62
63
  }
64
+ if(argument==='--receipt-only'){options.receiptOnly=true;continue;}
63
65
  if (argument === "--help" || argument === "-h") {
64
66
  options.help = true;
65
67
  continue;
@@ -78,19 +80,27 @@ export function parseArgs(argv, env = process.env) {
78
80
  "--after",
79
81
  "--limit",
80
82
  "--scope",
83
+ "--offset",
81
84
  "--squad-id",
82
85
  "--file",
83
86
  "--attachment-id",
84
87
  "--member-id",
85
- "--expected-owner",
86
- "--owner-id",
88
+ "--assignee-id",
89
+ "--expected-assignee",
87
90
  "--participant-id",
91
+ "--creation-mode",
92
+ "--request-seq",
93
+ "--followup-context",
88
94
  ].includes(name)
89
95
  ) {
90
96
  const parsed =
91
97
  inline === undefined ? valueAfter(argv, index, name) : inline;
92
98
  if (inline === undefined) index += 1;
93
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
+ }
94
104
  if (name === "--workspace") options.workspace = parsed;
95
105
  if (name === "--expected-actor") {
96
106
  if (!parsed.trim() || parsed.length > 160 || /[\r\n]/.test(parsed))
@@ -103,12 +113,30 @@ export function parseArgs(argv, env = process.env) {
103
113
  if (name === "--after") options.after = parsed;
104
114
  if (name === "--limit") options.limit = parsed;
105
115
  if (name === "--scope") options.scope = parsed;
116
+ if (name === "--offset") options.offset = parsed;
106
117
  if (name === "--squad-id") options.squadId = parsed;
107
118
  if (name === "--file") options.file = parsed;
108
119
  if (name === "--attachment-id") options.attachmentIds.push(parsed);
109
120
  if (name === "--member-id") options.memberId = parsed;
110
- if (name === "--expected-owner") options.expectedOwner = parsed;
111
- if (name === "--owner-id") options.ownerId = 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
+ }
112
140
  if (name === "--participant-id") options.participantIds.push(parsed);
113
141
  continue;
114
142
  }
@@ -200,6 +228,7 @@ async function request(options, path, requestOptions = {}, context = {}) {
200
228
  const baseUrl = normalizeBaseUrl(options.baseUrl);
201
229
  const url = new URL(path, `${baseUrl}/`);
202
230
  const headers = requestHeaders(options, context, requestOptions.key);
231
+ if(requestOptions.receiptOnly)headers['X-Deepflow-Message-Receipt']='lookup';
203
232
  if (requestOptions.body !== undefined)
204
233
  headers["Content-Type"] = "application/json";
205
234
  let response;
@@ -241,7 +270,7 @@ async function request(options, path, requestOptions = {}, context = {}) {
241
270
  return data;
242
271
  }
243
272
 
244
- async function currentContext(options) {
273
+ async function currentContext(options,requireWorkspace=true) {
245
274
  const me = await request(
246
275
  options,
247
276
  "/api/v1/me",
@@ -249,12 +278,12 @@ async function currentContext(options) {
249
278
  options.workspace ? { workspace: options.workspace } : {},
250
279
  );
251
280
  const workspace = options.workspace || me.workspaceId;
252
- if (!workspace)
281
+ if (!workspace&&requireWorkspace)
253
282
  throw new RequestError("Workbench 没有返回当前工作区。", {
254
283
  code: "workspace_missing",
255
284
  });
256
285
  if (
257
- Array.isArray(me.workspaces) &&
286
+ workspace && Array.isArray(me.workspaces) &&
258
287
  !me.workspaces.some((item) => item.id === workspace)
259
288
  )
260
289
  throw new RequestError(`当前用户无权访问工作区:${workspace}`, {
@@ -277,6 +306,8 @@ async function currentContext(options) {
277
306
 
278
307
  async function loadConfig(options) {
279
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};
280
311
  const [platform, auth, readiness, capabilities] = await Promise.all([
281
312
  request(options, "/api/v1/platform-state"),
282
313
  request(options, "/api/v1/auth/config"),
@@ -451,18 +482,21 @@ function help() {
451
482
  df-workbench squad create --file <配置.json> [--idempotency-key <key>] [--json]
452
483
  df-workbench squad update <squad-id> --file <配置.json> [--idempotency-key <key>] [--json]
453
484
  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>]
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>]
455
487
  df-workbench task member-options <task-id>
456
488
  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>]
458
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>
459
492
  df-workbench task lookup <DF-number> [--json]
460
493
  df-workbench task get <task-id> [--json]
461
494
  df-workbench task facts <task-id> [--after <seq>] [--limit <n>] [--json]
462
- df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--json]
495
+ df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--offset <n>] [--json]
463
496
  df-workbench attachment upload --file <文件> [--idempotency-key <key>] [--json]
464
497
 
465
498
  task create/send 可重复传入 --attachment-id <id>,每次最多 10 个。
499
+ task send 可携带 --followup-context <原提醒JSON>;--receipt-only 仅核对原幂等键的消息回执,不创建新消息。
466
500
 
467
501
  全局配置:
468
502
  DF_WORKBENCH_BASE_URL Workbench 根地址,默认 http://127.0.0.1:8793
@@ -481,11 +515,13 @@ async function run(options) {
481
515
  process.stdout.write(`${help()}\n`);
482
516
  return;
483
517
  }
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("成员和负责人参数只用于对应的任务成员命令。");
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
+
489
525
  if (
490
526
  options.squadId !== undefined &&
491
527
  !(command === "task" && subject === "create")
@@ -553,12 +589,12 @@ async function run(options) {
553
589
  return;
554
590
  }
555
591
  if (command === "me") {
556
- const context = await currentContext(options);
592
+ const context = await currentContext(options,false);
557
593
  writeResult(
558
594
  context.me,
559
595
  options,
560
596
  (value) =>
561
- `用户: ${value.user.name} (${value.user.id})\n工作区: ${value.workspaceId}`,
597
+ `身份: ${value.user.principalKind||"human"} · ${value.user.name} (${value.user.id})\n工作区: ${value.workspaceId}`,
562
598
  );
563
599
  return;
564
600
  }
@@ -579,17 +615,50 @@ async function run(options) {
579
615
  writeResult(value,options,value => value.members.map(member=>`${member.userId} ${member.name || ""}`).join("\n"));return;
580
616
  }
581
617
  if (command !== "task") throw new UsageError(`不支持的命令:${command}`);
582
- if (["member-options", "invite", "transfer-owner"].includes(subject)) {
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)) {
583
634
  const taskId = rest[0];
584
635
  if (!taskId || !/^[\w-]{1,160}$/.test(taskId)) throw new UsageError("需要有效的 Task ID。");
585
636
  const writes = subject !== "member-options";
586
637
  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
638
  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);
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
+ }
593
662
  writeResult(value, options, value => JSON.stringify(value));
594
663
  return;
595
664
  }
@@ -599,17 +668,24 @@ async function run(options) {
599
668
  options.title,
600
669
  options.squadId,
601
670
  );
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="";
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="";}
606
675
  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)
676
+ } else if(options.participantIds.length) throw new UsageError("--participant-id 需要同时指定 --assignee-id。");
677
+ if (options.attachmentIds.length && (!options.assigneeId || options.creationMode==='atomic'))
609
678
  body.attachment_ids = options.attachmentIds;
610
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
+ }
611
685
  const key = idempotencyKey(options);
612
- let value = await request(
686
+ let atomicCreation=options.creationMode==='atomic';
687
+ let value;
688
+ try {value = await request(
613
689
  options,
614
690
  "/api/v1/tasks",
615
691
  {
@@ -618,7 +694,17 @@ async function run(options) {
618
694
  body,
619
695
  },
620
696
  context,
621
- );
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
+ }
622
708
  if (
623
709
  body.squad_id &&
624
710
  value?.task?.metadata?.data?.squad?.squadId !== body.squad_id
@@ -632,10 +718,13 @@ async function run(options) {
632
718
  idempotencyKey: key,
633
719
  },
634
720
  );
635
- if (options.ownerId) {
721
+ if (options.assigneeId) {
636
722
  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});
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) {
639
728
  // Current squad edits must never change a retry of the original message.
640
729
  const agentId=data.initial_message_context?.agentId;
641
730
  if (!agentId) throw new RequestError("任务已创建,但创建时的小队快照没有可接收首条消息的伙伴;请保留原任务与幂等键核对。",{code:"initial_agent_unavailable",outcome:"unknown",taskId:task.id,idempotencyKey:key});
@@ -648,6 +737,7 @@ async function run(options) {
648
737
  }
649
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});
650
739
  value={...value,first_fact:first.fact,first_fact_seq:first.fact.seq};
740
+ }
651
741
  }
652
742
  writeResult(value, options, taskSummary);
653
743
  return;
@@ -663,8 +753,10 @@ async function run(options) {
663
753
  {
664
754
  method: "POST",
665
755
  key,
756
+ receiptOnly:options.receiptOnly===true,
666
757
  body: {
667
758
  text: messageValue(options.message),
759
+ ...(options.followupContext?{followup_context:options.followupContext}:{}),
668
760
  ...(options.attachmentIds.length
669
761
  ? { attachment_ids: options.attachmentIds }
670
762
  : {}),
@@ -750,6 +842,7 @@ async function run(options) {
750
842
  `/api/v1/tasks?${new URLSearchParams({
751
843
  scope: options.scope,
752
844
  limit: String(limit),
845
+ offset: String(integerOption(options.offset, "--offset", 0, 0, 1048576)),
753
846
  include_state: "1",
754
847
  })}`,
755
848
  {},
@@ -1 +1 @@
1
- {"version":"0.3.1","project":"deepflow-workbench","sourceCommit":"b83963f15f4ed187164464dddbfd2ea460f52971","sourceDirty":false}
1
+ {"version":"0.3.2","project":"deepflow-workbench","sourceCommit":"adfb558b1c3532b338b5a8b0d5c11a43739e4ca6","sourceDirty":false}