@metagptx/deepflow-suite-cli 0.3.1 → 0.3.3
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 +10 -10
- package/bundle/skills/df-agentrt/SKILL.md +13 -17
- package/bundle/skills/df-agentrt-repo/SKILL.md +29 -56
- package/bundle/skills/df-task/SKILL.md +6 -0
- package/bundle/skills/df-task/references/fact-types.md +29 -3
- 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-task/commands.mjs +11 -7
- package/runtime/df-task/df-task.mjs +17 -3
- package/runtime/df-task/protocol.mjs +1 -1
- package/runtime/df-task/read.mjs +1 -1
- package/runtime/df-task/release.json +1 -1
- package/runtime/df-taskdriver/release.json +1 -1
- package/runtime/df-workbench/df-workbench.mjs +127 -34
- package/runtime/df-workbench/release.json +1 -1
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"distribution": "deepflow-suite",
|
|
4
4
|
"package": "@metagptx/deepflow-suite-cli",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.3",
|
|
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.
|
|
11
|
+
"version": "0.3.3",
|
|
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":
|
|
84
|
-
"sha256": "
|
|
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":
|
|
99
|
-
"sha256": "
|
|
98
|
+
"size": 14374,
|
|
99
|
+
"sha256": "357d805b8260d0e67a46a7edaef4f443c7be58dec075bfc2f176a83e88ebb5d1"
|
|
100
100
|
}
|
|
101
101
|
]
|
|
102
102
|
},
|
|
@@ -173,8 +173,8 @@
|
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
"path": "SKILL.md",
|
|
176
|
-
"size":
|
|
177
|
-
"sha256": "
|
|
176
|
+
"size": 12201,
|
|
177
|
+
"sha256": "6af6181c7094ca4217578c8d13938ecfd3df006dacdef6446c30b00fcd8ddfec"
|
|
178
178
|
}
|
|
179
179
|
]
|
|
180
180
|
},
|
|
@@ -184,8 +184,8 @@
|
|
|
184
184
|
"files": [
|
|
185
185
|
{
|
|
186
186
|
"path": "SKILL.md",
|
|
187
|
-
"size":
|
|
188
|
-
"sha256": "
|
|
187
|
+
"size": 3962,
|
|
188
|
+
"sha256": "1b8d4b372f04a78430b96c4a39661a83903c1aefc777a9da10600277497ec205"
|
|
189
189
|
}
|
|
190
190
|
]
|
|
191
191
|
}
|
|
@@ -12,22 +12,20 @@ CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI
|
|
|
12
12
|
你运行在 **DeepFlow Agent Runtime** 上。这是一个把「会话 + 多轮消息 + 代码仓库」
|
|
13
13
|
管起来的服务,你现在执行的是其中一轮。
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## 执行环境与工具范围
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
**这里一个都没有。**
|
|
20
|
-
|
|
21
|
-
简报里那份 `multica` 命令手册,只有一条在本环境真实可用:
|
|
17
|
+
执行引擎的自动简报按完整 multica 产品生成,其中的 issue、chat channel、squad、
|
|
18
|
+
附件、workspace、project 等操作不适用于本 Runtime。当前 `multica` shim 只支持:
|
|
22
19
|
|
|
23
20
|
```bash
|
|
24
21
|
multica repo checkout <URL>
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
这限定的是 `multica` 子命令,不限制普通 Git、shell 或其他已提供的工具。
|
|
25
|
+
简报中的“先用 checkout 获取代码”不是唯一入口要求;已授权仓库可通过普通 Git
|
|
26
|
+
取得或复用工作副本,也可使用托管检出,不设固定的工具调用顺序。
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Runtime 镜像通过标准 PATH 提供 `df-task`、`df-agentctrl`、`df-agentrt` 和 `multica` shim,普通 shell 与登录 shell 均应可直接调用。命令缺失或 `--help` 失败表示对应能力存在执行环境缺口;不在 Run 中安装、编译 CLI 或猜绝对路径反复尝试。保留具体失败与可用能力,是否阻塞当前目标由执行者结合执行定义和任务判断,不能把某个可替代工具不可用直接当作整个任务无法继续。
|
|
31
29
|
|
|
32
30
|
Skill 内容通过 provider 的原生目录交付,以当前会话的 Skill 目录清单为准。清单中的 `r0` 等是根目录别名,打开前先展开,不能拼成 `skills/r0/...`。Codex 的 Runtime Skills 在 `$CODEX_HOME/skills/<slug>/SKILL.md`;Claude 在任务工作目录的 `.claude/skills/<slug>/SKILL.md`。支持文件相对该 Skill 目录读取。`$DEEPFLOW_WORKDIR` 表示任务工作目录,不是通用 Skill 根目录。
|
|
33
31
|
|
|
@@ -64,11 +62,11 @@ Run 中 `df-agentrt` 只使用部署注入的 `MULTICA_SERVER_URL`,忽略 `AGE
|
|
|
64
62
|
|
|
65
63
|
文件仍依赖所属 Worker 工作目录,不是归档或永久可用承诺;未发布或仍在生成的文件不能宣称已交付。
|
|
66
64
|
|
|
67
|
-
##
|
|
65
|
+
## 工作目录与工作副本
|
|
68
66
|
|
|
69
67
|
- 同一个会话的多轮**复用同一个工作目录**,上一轮留下的文件这一轮还在。
|
|
70
|
-
- `repo add` / `repo checkout`
|
|
71
|
-
-
|
|
68
|
+
- 托管 `repo add` / `repo checkout` 检出的仓库位于任务工作目录的同级;用命令返回的绝对路径访问,不猜目录名。这不是所有 Git 工作副本的目录规则。
|
|
69
|
+
- 普通 Git 获取的工作副本放在本 Session 拥有的工作区内;保留已有改动、提交和分支,不把 daemon 缓存或其他 Session 的目录当作自己的工作副本。需要通过文件通道交付的普通文件应位于可读取的任务工作目录内,代码交付使用远端固定引用。
|
|
72
70
|
|
|
73
71
|
## release 之后工作目录的生命周期
|
|
74
72
|
|
|
@@ -160,10 +158,8 @@ release 会停止会话接收新轮次并改变生命周期状态(当前不会
|
|
|
160
158
|
| 0 | 成功 | —— |
|
|
161
159
|
| 1 | 请求发出去了,但失败了 | 看 stderr,它写清了原因 |
|
|
162
160
|
| 2 | 命令行写错了,一个请求都没发 | 改命令 |
|
|
163
|
-
| 3 |
|
|
161
|
+
| 3 | 对应命令所需环境/部署不满足 | 报告具体缺口;不靠改参数反复执行同一命令,不据此推断其他授权方式也不可用 |
|
|
164
162
|
|
|
165
|
-
##
|
|
163
|
+
## 仓库发现与托管检出
|
|
166
164
|
|
|
167
|
-
|
|
168
|
-
见 `df-agentrt-repo` skill。一句话版本:**不要自己拼 `multica repo checkout` 去检出刚加的
|
|
169
|
-
仓库**,daemon 的白名单按 URL 逐字符匹配,你手敲的那个字符串未必是服务端存下来的那个。
|
|
165
|
+
需要按应用名发现仓库、查询或追加 Session 仓库配置、使用托管检出时,见 `df-agentrt-repo`。该能力不是普通 Git 操作的前置条件。
|
|
@@ -1,87 +1,60 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: df-agentrt-repo
|
|
3
|
-
description:
|
|
3
|
+
description: 在 Runtime 会话中查询已配置仓库、按应用名发现仓库,或追加仓库配置并托管检出时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Runtime 仓库发现与托管检出
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI 实现。使用前应已完成 Suite 安装;缺少命令或版本不匹配时,用 `deepflow-suite doctor --json` 核对。Run 内缺少工具属于执行环境异常,不临时安装或编译。
|
|
11
|
-
|
|
12
|
-
会话创建时定下的配置里,只有**代码仓库**这一项是可以在任务进行中追加的。
|
|
13
|
-
|
|
14
|
-
你可能在分析到一半时才发现答案不在手上这几个仓库里——这时不需要中断任务回报、
|
|
15
|
-
等人重建会话,用 `df-agentrt` 自己把仓库加进来,一个回合内就能继续。
|
|
16
|
-
|
|
17
|
-
`df-agentrt` 已经在 PATH 上,不需要定位路径。
|
|
8
|
+
本 Skill 说明 Runtime 的仓库登记与托管检出能力。已知授权仓库及所需 ref 时,也可通过普通 Git 获取或复用工作副本,不要求先查询、登记或托管检出。按当前任务需要选择能力,不设固定调用顺序。
|
|
18
9
|
|
|
19
|
-
|
|
10
|
+
仓库列表、应用搜索结果和登记成功都不增加任务授权;只操作任务范围内的相关仓库。工作目录与复用边界见 `df-agentrt`,Git 认证、提交和 MR 操作见已加载的 Git 能力。
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
df-agentrt repo list
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
每行一个仓库:URL、`@分支`(如果固定了分支)、括号里是说明。
|
|
26
|
-
**先跑这个**——需要的仓库很可能已经在列表里,那就只差一个检出:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
multica repo checkout <上面列出的 URL>
|
|
30
|
-
```
|
|
12
|
+
## CLI 依赖
|
|
31
13
|
|
|
32
|
-
|
|
14
|
+
CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI 实现。缺少命令或版本不匹配时,用 `deepflow-suite doctor --json` 核对。Run 内不临时安装或编译 CLI。
|
|
33
15
|
|
|
34
|
-
|
|
16
|
+
## 按需查询与发现
|
|
35
17
|
|
|
36
|
-
|
|
37
|
-
df-agentrt repo search 结算服务
|
|
38
|
-
```
|
|
18
|
+
`df-agentrt repo list` 查询本 Session 已配置的仓库。每行包含 URL、`@ref`(若已指定)和说明;列表不证明工作副本已存在或远端已刷新。
|
|
39
19
|
|
|
40
|
-
|
|
20
|
+
知道应用名、不知道仓库地址时,可用 `df-agentrt repo search <应用名>`。输出为应用代码、应用名和仓库地址,以 Tab 分隔。搜索无结果只表示此次未匹配到应用;可调整关键词,已有准确地址时无需搜索。
|
|
41
21
|
|
|
42
|
-
|
|
22
|
+
## 登记与托管检出
|
|
43
23
|
|
|
44
|
-
|
|
24
|
+
向本 Session 追加仓库并检出:
|
|
45
25
|
|
|
46
26
|
```bash
|
|
47
27
|
df-agentrt repo add <应用代码或 clone 地址> [...]
|
|
48
28
|
```
|
|
49
29
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
要固定分支:
|
|
30
|
+
参数含 `://` 或 `@` 时按 clone 地址处理,其余按应用代码查询。`--ref` 仅支持单个 clone 地址,可指定任务所需的分支、tag 或提交:
|
|
53
31
|
|
|
54
32
|
```bash
|
|
55
33
|
df-agentrt repo add https://git.example.com/g/settle --ref release/2026-09
|
|
56
34
|
```
|
|
57
35
|
|
|
58
|
-
|
|
59
|
-
真要这么做就分成几条命令。
|
|
36
|
+
对已配置的仓库单独执行托管检出时,使用配置中的 URL,并保留所需 ref:
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## 三件容易做错的事
|
|
65
|
-
|
|
66
|
-
**不要自己拼 `multica repo checkout` 去检出刚加的仓库。** daemon 的白名单按 URL
|
|
67
|
-
**逐字符**匹配,你手敲的地址少一个 `.git`、多一个尾斜杠,就是另一个仓库,
|
|
68
|
-
会得到「repo is not configured」——而 `repo list` 里明明有它。`repo add` 用的是服务端存下来的
|
|
69
|
-
那个字符串,所以它检出得了,你重新敲一遍的不一定。
|
|
38
|
+
```bash
|
|
39
|
+
multica repo checkout <已配置的URL> [--ref <所需ref>]
|
|
40
|
+
```
|
|
70
41
|
|
|
71
|
-
|
|
42
|
+
成功时使用工具返回的工作副本绝对路径。缓存命中或检出成功都不能证明最新远端 ref 已刷新,仍需核对所需版本与实际起始 SHA。
|
|
72
43
|
|
|
73
|
-
|
|
74
|
-
「在列表里」和「在磁盘上」是两回事。
|
|
44
|
+
### 操作语义
|
|
75
45
|
|
|
76
|
-
|
|
46
|
+
- `repo add` 包含登记和检出两个动作。登记已成功但检出失败时,配置仍然保留,不能把整体失败当作登记回滚。
|
|
47
|
+
- 重复添加不重复登记,但仍会执行检出,不是无副作用的查询或重试。已有工作副本时先核对实际目录与改动,不为切换传输方式重新检出。
|
|
48
|
+
- 托管检出按已保存的 URL 精确匹配。使用 `repo list` 或登记结果中的原始 URL;手工改变 `.git`、尾斜杠或 SSH/HTTPS 可能不匹配。`repo add` 已完成检出时,无需再调用 checkout。
|
|
49
|
+
- Session 仓库配置只支持追加,不提供移除接口;这描述的是配置能力,不是所有本地文件操作的规则。
|
|
77
50
|
|
|
78
|
-
|
|
51
|
+
## 失败含义
|
|
79
52
|
|
|
80
|
-
|
|
81
|
-
- 说「remain configured」——仓库加上了,只是检出没成功,可以用 `multica repo checkout <URL>` 重试;
|
|
82
|
-
- 其余的错误信息来自服务端,已经写清楚了原因和下一步。
|
|
53
|
+
根据失败发生的位置解释结果,不把某条命令失败直接当作仓库或整个任务不可用:
|
|
83
54
|
|
|
84
|
-
|
|
55
|
+
- `retrying will not help` 表示该命令缺少所需执行环境;换参数不会修复该缺口,不代表普通 Git 或其他已有能力也不可用。
|
|
56
|
+
- `remain configured` 表示登记保留、检出未成功;按实际错误判断是否适合重试,不能无条件再次检出已有工作副本。
|
|
57
|
+
- Runtime 仓库配置刷新或查询超时,不证明 GitLab 不可达、仓库不存在、凭据缺失或缓存未命中。已知授权仓库和所需 ref 时,普通 Git 不依赖这条托管检出链路。
|
|
58
|
+
- SSH 主机校验或公钥认证失败,表示对应 SSH 连接未成功;Token 与 SSH 私钥是不同凭据,传输方式和凭据绑定按 Git 能力说明处理。
|
|
85
59
|
|
|
86
|
-
|
|
87
|
-
不要猜目录名——仓库目录名来自 URL 最后一段,不一定等于应用名。
|
|
60
|
+
保留具体失败操作、已完成动作、实际目录及未确认项。是否采用其他授权方法、续办或请求人工协助,由执行者结合任务与执行定义判断;不通过扩大仓库范围、切换账号或访问其他 Session 的工作副本绕过限制。
|
|
@@ -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` |
|
|
10
|
-
| `external_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
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
1
|
+
{"version":"0.3.3","project":"deepflow-agent-ctrl","sourceCommit":"dee1a88615b09bd979562902cebc631f81b1859b","sourceDirty":false}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
1
|
+
{"version":"0.3.3","project":"deepflow-agent-runtime","sourceCommit":"592af8311ec783961a98e47377340121b78f2a68","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
|
|
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"];
|
package/runtime/df-task/read.mjs
CHANGED
|
@@ -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
|
+
{"version":"0.3.3","project":"deepflow-task","sourceCommit":"cc53e86834b19e8a4fe73f9e9a08d76853d51e51","sourceDirty":false}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.3.
|
|
1
|
+
{"version":"0.3.3","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
|
-
"--
|
|
86
|
-
"--
|
|
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 === "--
|
|
111
|
-
if (name === "--
|
|
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>] [--
|
|
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.
|
|
485
|
-
if (options.
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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.
|
|
603
|
-
if (![options.
|
|
604
|
-
body.
|
|
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 需要同时指定 --
|
|
608
|
-
if (options.attachmentIds.length && !options.
|
|
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
|
|
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.
|
|
721
|
+
if (options.assigneeId) {
|
|
636
722
|
const task=value?.task,data=task?.metadata?.data;
|
|
637
|
-
if (!task?.id || data?.workspace_id!==context.workspace || data.
|
|
638
|
-
![context.actorId,options.
|
|
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
|
+
{"version":"0.3.3","project":"deepflow-workbench","sourceCommit":"adfb558b1c3532b338b5a8b0d5c11a43739e4ca6","sourceDirty":false}
|