dsh-device-fleet 0.0.0-stage → 0.1.0-dev.10
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/CONTEXT.md +123 -0
- package/LICENSE +21 -0
- package/README.md +135 -2
- package/client.js +167 -0
- package/cordis.patch.yml +26 -0
- package/docs/adr/0001-fleet-enrollment-over-account-auto-discovery.md +17 -0
- package/docs/adr/0002-shared-room-as-coordination-unit.md +17 -0
- package/docs/adr/0003-direct-call-and-room-as-two-channels.md +22 -0
- package/docs/adr/0004-direct-call-policy-owned-by-target-device.md +23 -0
- package/docs/adr/0005-upgrade-session-to-room-host.md +22 -0
- package/docs/adr/0006-addressed-messages-and-any-device-speaks.md +16 -0
- package/docs/adr/0007-user-speech-decoupled-from-device.md +21 -0
- package/docs/adr/0008-extend-global-task-list-as-room-board.md +21 -0
- package/docs/adr/0009-room-policy-configured-at-creation.md +21 -0
- package/docs/adr/0010-room-policy-v1-fields-and-custom-room-types.md +24 -0
- package/docs/adr/0011-room-board-lifecycle-and-hooks.md +27 -0
- package/docs/adr/0012-lazy-wake-and-directed-speech.md +22 -0
- package/docs/adr/0013-direct-call-as-device-agnostic-functions.md +26 -0
- package/docs/adr/0014-fleet-call-unified-entry-with-typed-shortcuts.md +22 -0
- package/docs/adr/0015-room-plus-local-teams-and-delegation.md +22 -0
- package/docs/adr/0016-delegation-upgrade-reuses-session.md +20 -0
- package/docs/adr/0017-network-phases-direct-and-relay.md +21 -0
- package/docs/adr/0018-offline-fail-fast-and-durable-room-log.md +21 -0
- package/docs/adr/0019-execution-context-configured-locally.md +24 -0
- package/docs/adr/0020-approval-inbox-with-scoped-grants.md +22 -0
- package/docs/adr/0021-room-visibility-user-full-agents-scoped.md +26 -0
- package/docs/adr/0022-tool-adoption-mechanisms.md +23 -0
- package/docs/adr/0023-reuse-session-trajectory-for-call-details.md +22 -0
- package/docs/adr/0024-strict-version-match-with-reserved-capabilities.md +22 -0
- package/docs/notes/approval-inbox-phase.md +31 -0
- package/docs/notes/design-decisions-summary.md +49 -0
- package/docs/notes/dev-release.md +67 -0
- package/docs/notes/dsh-global-task-list-evaluation.md +54 -0
- package/docs/notes/network-v1-v2-implementation.md +71 -0
- package/docs/notes/pairing-session-2026-11-09.md +30 -0
- package/docs/notes/remote-tests-2026-11-09.md +33 -0
- package/docs/notes/room-logic-phase.md +31 -0
- package/docs/notes/step1-host-plugin-and-direct-call.md +57 -0
- package/package.json +51 -3
- package/src/host/approval-inbox.js +135 -0
- package/src/host/approval-web.js +95 -0
- package/src/host/delegation.js +127 -0
- package/src/host/direct-call.js +209 -0
- package/src/host/execution-slot.js +96 -0
- package/src/host/index.js +10 -0
- package/src/host/room-service.js +324 -0
- package/src/host/store.js +156 -0
- package/src/host/tool-policy.js +151 -0
- package/src/host/tools.js +590 -0
- package/src/index.js +3 -0
- package/src/net/direct.js +69 -0
- package/src/net/discovery.js +105 -0
- package/src/net/fleet-node.js +509 -0
- package/src/net/identity.js +167 -0
- package/src/net/index.js +9 -0
- package/src/net/protocol.js +73 -0
- package/src/net/relay-client.js +258 -0
- package/src/net/relay-server.js +366 -0
- package/src/net/secure-channel.js +538 -0
- package/src/plugin/index.js +380 -0
package/CONTEXT.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# DSH Device Fleet
|
|
2
|
+
|
|
3
|
+
让登录同一 DeepSeek 账号、且都安装互联插件的多台设备上的 agent 与工具互相可用,使用时像在同一台设备上。
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**设备 (Device)**:
|
|
8
|
+
一台运行 DSH 的机器,有自己的 Host、会话、工作区、文件系统与工具集。
|
|
9
|
+
_Avoid_: 节点, 客户端, 实例
|
|
10
|
+
|
|
11
|
+
**Fleet (设备联邦)**:
|
|
12
|
+
一组经显式入网、互相信任的设备的集合;只有同一 Fleet 的成员可以互联。
|
|
13
|
+
_Avoid_: 账号设备组, 集群, 网络
|
|
14
|
+
|
|
15
|
+
**入网 (Enrollment)**:
|
|
16
|
+
把一台新设备加入 Fleet 的一次性动作:由已有成员签发邀请,新设备接受后成为成员。
|
|
17
|
+
_Avoid_: 配对, 关联, 绑定
|
|
18
|
+
|
|
19
|
+
**邀请 (Invite)**:
|
|
20
|
+
一份一次性、会过期的入网凭据,携带 Fleet 标识、会合信息与一次性 token。
|
|
21
|
+
_Avoid_: 配对码, 二维码(后者只是邀请的载体)
|
|
22
|
+
|
|
23
|
+
**成员 (Member)**:
|
|
24
|
+
已入网的一台设备,在 Fleet 成员表中占一条记录,带设备身份、能力档位与撤销状态。
|
|
25
|
+
_Avoid_: 节点, 对端(对端只是单次连接的视角)
|
|
26
|
+
|
|
27
|
+
**账号 (Account)**:
|
|
28
|
+
设备本机登录的 DeepSeek 账号;Fleet 要求成员登录同一账号,用于成员校验与账号切换隔离。
|
|
29
|
+
_Avoid_: 用户, 登录, 身份
|
|
30
|
+
|
|
31
|
+
**账号指纹 (Account fingerprint)**:
|
|
32
|
+
只在 Fleet 内比较、不暴露原始账号 ID 的账号校验值;用于拒绝不同账号的设备,以及账号切换后的旧连接。
|
|
33
|
+
_Avoid_: 账号 ID, 账号哈希, token
|
|
34
|
+
|
|
35
|
+
## Collaboration
|
|
36
|
+
|
|
37
|
+
**Room (协作房间)**:
|
|
38
|
+
一个跨设备的共享协作上下文,等价于群聊:一份权威消息历史、成员表与共享任务板,按序广播给所有成员。
|
|
39
|
+
_Avoid_: 群, 会话, 频道, channel
|
|
40
|
+
|
|
41
|
+
**Host (房主设备)**:
|
|
42
|
+
创建并托管 Room 的设备;房间的权威历史与成员表落在它的一个会话里,Host 只负责排序、广播与持久化,不霸占话语权。
|
|
43
|
+
_Avoid_: 服务端, 服务器, 主控
|
|
44
|
+
|
|
45
|
+
**Participant (参与者)**:
|
|
46
|
+
一台加入 Room 的设备在房间里的身份。Participant 的发言经桥发到 Host 排序后广播;被点名时该设备上的 agent 才被唤起。
|
|
47
|
+
_Avoid_: 成员(成员是 Fleet 层面的概念), 节点, 协作者
|
|
48
|
+
|
|
49
|
+
**Participant Brief (参与者任务简报)**:
|
|
50
|
+
按需唤醒一个 Participant 的 agent 时给它的任务上下文,类似 subagent 的初始 prompt,而不是房间的全部历史。
|
|
51
|
+
_Avoid_: 房间历史, 全量上下文, 会话记录
|
|
52
|
+
|
|
53
|
+
**Room Role (房间角色)**:
|
|
54
|
+
Room 成员的职责标签(`host` / `participant`);角色只决定谁托管权威日志,加入、退出、发言使用同一套成员接口。
|
|
55
|
+
_Avoid_: 主从, 等级, 权限
|
|
56
|
+
|
|
57
|
+
**Room Upgrade (房间升级)**:
|
|
58
|
+
把当前会话显式升级为 Room Host,并可选地把一次既有委派的目标会话绑定为 Participant 的动作;升级保留双方上下文。
|
|
59
|
+
_Avoid_: 转房间, 升格, 建房
|
|
60
|
+
|
|
61
|
+
**Room Policy (房间策略)**:
|
|
62
|
+
房间创建时解析并记录的一组协作规则(邀请权、加入方式、成员上限、任务板写权限等);来自插件配置/预设,创建时确定,Host 可修改并广播。
|
|
63
|
+
_Avoid_: 权限, 设置, 房规
|
|
64
|
+
|
|
65
|
+
**Room Type (房间类型)**:
|
|
66
|
+
Room Policy 的命名预设(内置或用户保存,如私密/团队/只读),创建房间时解析成一份 Room Policy。
|
|
67
|
+
_Avoid_: 模板, 模式
|
|
68
|
+
|
|
69
|
+
**Addressing (点名)**:
|
|
70
|
+
Room 消息的显式收件人集合;只有被点名的成员会被唤醒,未点名成员不跑模型但共享房间日志。
|
|
71
|
+
_Avoid_: @(只是显示形式), 广播(`*` 是点名的广播特例)
|
|
72
|
+
|
|
73
|
+
**User (用户)**:
|
|
74
|
+
房间的人工发言者,是账号所有者本人,与从哪台设备发送无关;设备只作为审计来源。
|
|
75
|
+
_Avoid_: 账号(账号是登录身份), 设备用户, 客户端用户
|
|
76
|
+
|
|
77
|
+
**Room Speaker (发言者)**:
|
|
78
|
+
Room 消息的作者,取值为 `user` 或某个 agent 成员;与 Addressing 一起构成消息的完整路由信息。
|
|
79
|
+
_Avoid_: 发送端, 来源, from 字段
|
|
80
|
+
|
|
81
|
+
**Wake (唤醒)**:
|
|
82
|
+
让某个成员的 agent 在其设备上跑一轮模型的按需动作;只有被点名或被分配任务块时发生。
|
|
83
|
+
_Avoid_: 通知, 启动, 触发
|
|
84
|
+
|
|
85
|
+
**Delegation (委派)**:
|
|
86
|
+
成员 agent 把一次性任务交给另一台成员设备上的 agent 执行并取回结果;被委派方使用独立的一次性子会话,不进入 Room、不共享房间历史。
|
|
87
|
+
_Avoid_: 协作, 派发, 远程调用(那是工具直调)
|
|
88
|
+
|
|
89
|
+
**Local Team (本地团队)**:
|
|
90
|
+
成员 agent 在自己设备上使用的 Agent Team / subagent 集合;对 Room 不可见,不被点名、不写 Room Board,只由代表 agent 把结果带回。
|
|
91
|
+
_Avoid_: 小团队, 下属, 子房间
|
|
92
|
+
|
|
93
|
+
## Shared State
|
|
94
|
+
|
|
95
|
+
**Room Board (房间任务板)**:
|
|
96
|
+
与 Room 同生命周期的共享结构化状态(目标、Task Block、负责人、依赖、产物指针),由 Host 持久化,成员按切片读取。
|
|
97
|
+
_Avoid_: 任务列表, 面板数据, 全局任务库
|
|
98
|
+
|
|
99
|
+
**Task Block (任务块)**:
|
|
100
|
+
Room Board 上分配给某个成员的一段可独立开始与完成的工作单元;串行块完成后触发下一块交接,并行块互不影响。
|
|
101
|
+
_Avoid_: 任务, 子任务, 工单
|
|
102
|
+
|
|
103
|
+
**Mirror (镜像)**:
|
|
104
|
+
参与设备上 Room Board 的本地只读副本,用于面板展示与唤醒时的状态读取;写入一律回到 Host。
|
|
105
|
+
_Avoid_: 副本, 缓存, 同步数据
|
|
106
|
+
|
|
107
|
+
## Execution
|
|
108
|
+
|
|
109
|
+
**Direct Call (直调)**:
|
|
110
|
+
A 的 agent 直接调用目标设备上的一个 tool 并取回结果的一次性调用;不进入 Room、不创建 Participant、不读取目标设备的对话上下文。
|
|
111
|
+
_Avoid_: 远程调用(太泛), RPC, 工具桥
|
|
112
|
+
|
|
113
|
+
**Execution Slot (执行槽)**:
|
|
114
|
+
目标设备上由插件管理的隐藏执行上下文,只提供 cwd、沙箱与审批路由;不跑模型、不进 Room、不保存对话历史。
|
|
115
|
+
_Avoid_: 影子 agent, 执行会话, 沙箱容器
|
|
116
|
+
|
|
117
|
+
**Tool Policy (工具策略)**:
|
|
118
|
+
目标设备插件对"其他设备的直调"的授权规则,按设备与工具粒度决定免审批、需本机审批或拒绝;由资源所有者设备制定。
|
|
119
|
+
_Avoid_: 权限位, ACL, 白名单(白名单只是其中一种手段)
|
|
120
|
+
|
|
121
|
+
**Fleet Tool (设备无关能力函数)**:
|
|
122
|
+
以 `device` 参数指定目标设备的直调工具;统一入口为 `fleet_call(device, tool, parameters)`,另有少量高频 typed 快捷工具(`fleet_read` / `fleet_fetch` / `fleet_run`)。
|
|
123
|
+
_Avoid_: 远程工具, 镜像工具, MCP tool
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-device-fleet contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,136 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-device-fleet
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness 的设备联邦插件:把登录同一账号、且都安装本插件的多台设备连成一个 **Fleet**,让 agent 可以直调其他设备的工具、在共享 **Room** 里协作,或把任务委派给另一台设备的 agent。
|
|
4
|
+
|
|
5
|
+
## 进度
|
|
6
|
+
|
|
7
|
+
- ✅ **网络层 v1**:设备身份、邀请、端到端加密安全通道、直连(手动地址)、UDP 组播发现
|
|
8
|
+
- ✅ **网络层 v2**:自建 Relay(fleetKey 鉴权的信令 + TCP 拼接数据面,不做 NAT 打洞)
|
|
9
|
+
- ✅ **Host 插件 + 直调 MVP(代码完成、单测通过,待真实 DSH 安装联调)**:`fleet_list` / `fleet_tools` / `fleet_call` + `fleet_read` / `fleet_fetch` / `fleet_run`、Tool Policy、Execution Slot、审批 fail-closed
|
|
10
|
+
- ✅ **审批收件箱逻辑层(dev.7)**:`fleet_approvals` / `fleet_approve` / `fleet_deny`,ask 档排队、超时 fail-closed、once/ttl 授权、`fleet_simulate_request` 本机演练;Client 审批面板待做
|
|
11
|
+
- ✅ **Room 逻辑层(dev.8,单机模拟)**:`room_create` / `room_add_local` / `room_send` / `room_messages` / `room_status` / `room_task_*` / `room_close`;本地真实 agent 参与者、寻址唤醒、Room Board 串行交接;单机仿真验证通过
|
|
12
|
+
- ⏳ Client 审批面板 / 房间视图、远程参与者传输、插件重载 rehydrate(下一步)
|
|
13
|
+
|
|
14
|
+
## 安装(开发版)
|
|
15
|
+
|
|
16
|
+
已发布到 npm:**`dsh-device-fleet@0.1.0-dev.3`**(dist-tag `dev`)。
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
# CLI / web profile
|
|
20
|
+
dsh plugin --profile web add dsh-device-fleet@dev
|
|
21
|
+
|
|
22
|
+
# 桌面 app:先完全退出 app,再安装,然后重新打开
|
|
23
|
+
dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
24
|
+
|
|
25
|
+
# 离线安装包(拷到另一台设备)
|
|
26
|
+
dsh plugin --profile desktop add C:/path/to/dsh-device-fleet-0.1.0-dev.3.tgz
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
两台设备的 **DSH 版本与插件版本必须一致**(ADR-0024);不一致会在握手时直接拒绝。
|
|
30
|
+
|
|
31
|
+
### 配对(Fleet 入网)
|
|
32
|
+
|
|
33
|
+
1. 设备 A:对 agent 说“生成 Fleet 邀请码” → 模型调用 `fleet_invite`,拿到一次性邀请码(含 fleetKey,注意保密)。
|
|
34
|
+
2. 设备 B:安装并重启后,对 agent 说“加入 Fleet:`<code>`” → `fleet_join` 把邀请排队;**重启 DSH** 后生效。
|
|
35
|
+
3. 两台设备各自在 profile patch 里配置 `policy.allowedRoots` 与 `execution.cwd`(不配时直调读取一律被拒)。
|
|
36
|
+
4. 设备 B:`fleet_list` 看到设备 A → `fleet_read(device=A, path="A 允许根内的文件")` 验证。
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
# 两个 profile 的 cordis.patch.yml 里给 device-fleet 行追加 config:
|
|
40
|
+
- id: device-fleet
|
|
41
|
+
config:
|
|
42
|
+
deviceName: desktop
|
|
43
|
+
execution:
|
|
44
|
+
cwd: D:/work
|
|
45
|
+
agentPreset: standard
|
|
46
|
+
policy:
|
|
47
|
+
allowedRoots: [D:/work]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`fleet_run` 默认需要目标设备本机审批;审批收件箱 UI 尚未实现(当前 fail-closed)。第一阶段建议先用 `fleet_read` / `fleet_fetch` 验证链路。
|
|
51
|
+
|
|
52
|
+
## 设计文档
|
|
53
|
+
|
|
54
|
+
- 术语表:[CONTEXT.md](./CONTEXT.md)
|
|
55
|
+
- 决策记录:[docs/adr/](./docs/adr/)(17 条,从入网方式到网络分层)
|
|
56
|
+
- 网络实现说明(线格式/Relay 协议/限制):[docs/notes/network-v1-v2-implementation.md](./docs/notes/network-v1-v2-implementation.md)
|
|
57
|
+
- 全局任务列表复用评估:[docs/notes/dsh-global-task-list-evaluation.md](./docs/notes/dsh-global-task-list-evaluation.md)
|
|
58
|
+
|
|
59
|
+
## 快速开始
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import {
|
|
63
|
+
FleetNode, createFleet, decodeInvite, encodeInvite, generateIdentity, accountFingerprint,
|
|
64
|
+
} from './src/net/index.js'
|
|
65
|
+
|
|
66
|
+
// 第一台设备:创建 Fleet,导出邀请
|
|
67
|
+
const fleet = createFleet({ name: 'home' })
|
|
68
|
+
const accountFp = accountFingerprint(fleet.fleetKey, 'account-user-id')
|
|
69
|
+
const invite = encodeInvite({ fleet, accountFingerprint: accountFp })
|
|
70
|
+
|
|
71
|
+
// 第二台设备:解码邀请,加入
|
|
72
|
+
const joined = decodeInvite(invite)
|
|
73
|
+
|
|
74
|
+
const a = new FleetNode({
|
|
75
|
+
identity: generateIdentity({ name: 'desktop' }),
|
|
76
|
+
fleet,
|
|
77
|
+
accountFingerprint: accountFp,
|
|
78
|
+
direct: { host: '0.0.0.0', port: 0 },
|
|
79
|
+
discovery: true,
|
|
80
|
+
})
|
|
81
|
+
const b = new FleetNode({
|
|
82
|
+
identity: generateIdentity({ name: 'laptop' }),
|
|
83
|
+
fleet: joined.fleet,
|
|
84
|
+
accountFingerprint: joined.accountFingerprint,
|
|
85
|
+
direct: { host: '0.0.0.0', port: 0 },
|
|
86
|
+
discovery: true,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
a.onRequest(async ({ method, params }) => params) // 示例:eecho
|
|
90
|
+
await a.start()
|
|
91
|
+
await b.start()
|
|
92
|
+
|
|
93
|
+
// 发现/手动地址均可;双向 RPC 已端到端加密
|
|
94
|
+
const result = await b.request(a.identity.deviceId, 'echo', { path: '/tmp/report.md' })
|
|
95
|
+
console.log(result)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Relay(v2,跨网兜底)
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { createRelayServer } from './src/net/relay-server.js'
|
|
102
|
+
|
|
103
|
+
// 在用户自己的一台机器/VPS/NAS 上启动:
|
|
104
|
+
const relay = createRelayServer({
|
|
105
|
+
host: '0.0.0.0',
|
|
106
|
+
fleets: { [fleet.fleetId]: fleet.fleetKey },
|
|
107
|
+
})
|
|
108
|
+
const { port, dataPort } = await relay.ready
|
|
109
|
+
console.log({ port, dataPort })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
两台设备配置 `relay: { host, port, dataPort }` 后,直连不可达时会自动走 Relay;Relay 只看到密文与连接元数据。
|
|
113
|
+
|
|
114
|
+
## 测试
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
node test/net.test.mjs # 7/7,直连/发现/Relay/鉴权/密钥固定
|
|
118
|
+
node --test test/ # 标准 node:test 入口(受限沙箱可能禁止 spawn)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
覆盖:邀请编解码、直连 RPC 与 roster TOFU、跨 fleet/跨账号拒绝、固定公钥冲突拒绝、Relay 加密 RPC、Relay 鉴权失败、组播发现。
|
|
122
|
+
|
|
123
|
+
## 安全说明
|
|
124
|
+
|
|
125
|
+
- 业务帧端到端加密(X25519 + HKDF + AES-256-GCM);Relay 只能看到密文与元数据;
|
|
126
|
+
- 设备身份在首次接触时用 `fleetKey` 成员证明校验并固定(TOFU),之后公钥变化即拒绝;
|
|
127
|
+
- 账号指纹 `HMAC(fleetKey, accountUserId)` 只做成员校验与隔离,不是传输凭据;
|
|
128
|
+
- 当前为 v1/v2 原型:TOFU 无轮换/吊销、Relay 控制面未加 TLS、无限流与审计,上线前需补齐。
|
|
129
|
+
|
|
130
|
+
## 下一步
|
|
131
|
+
|
|
132
|
+
1. Host 插件接入:身份/邀请/Fleet 配置持久化到 `$DSH_HOME`,把网络层挂到 Cordis `apply`;
|
|
133
|
+
2. 工具面:`fleet_list` / `fleet_tools` / `fleet_call` + `fleet_read` / `fleet_fetch` / `fleet_run`;
|
|
134
|
+
3. Room:会话升级、Room Board(扩展 `dsh-global-task-list`)、点名消息与惰性唤醒;
|
|
135
|
+
4. 委派:`fleet_delegate` + 一次性子会话 + 委派升级为 Room;
|
|
136
|
+
5. UI:设备页/配对/审批队列/房间视图;Host/Client bundle 发布。
|
package/client.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Fleet approval panel (Client half).
|
|
2
|
+
// A floating button under shell.overlay polls the Host route registered by
|
|
3
|
+
// src/host/approval-web.js. Decisions go through ApprovalInbox on the Host.
|
|
4
|
+
window.__ModuleLoader__.load({
|
|
5
|
+
id: 'dsh-device-fleet',
|
|
6
|
+
factory(require) {
|
|
7
|
+
const React = require('react');
|
|
8
|
+
const h = React.createElement;
|
|
9
|
+
const { useEffect, useState } = React;
|
|
10
|
+
|
|
11
|
+
const POLL_MS = 2500;
|
|
12
|
+
const panelStyle = {
|
|
13
|
+
position: 'fixed',
|
|
14
|
+
right: 16,
|
|
15
|
+
bottom: 72,
|
|
16
|
+
width: 360,
|
|
17
|
+
maxHeight: '65vh',
|
|
18
|
+
overflow: 'auto',
|
|
19
|
+
zIndex: 900,
|
|
20
|
+
background: 'var(--dsw-alias-surface-raised, #1f1f1f)',
|
|
21
|
+
color: 'var(--dsw-alias-text, #e8e8e8)',
|
|
22
|
+
border: '1px solid var(--dsw-alias-border, #3a3a3a)',
|
|
23
|
+
borderRadius: 10,
|
|
24
|
+
boxShadow: '0 12px 32px rgba(0,0,0,.35)',
|
|
25
|
+
padding: 12,
|
|
26
|
+
font: '13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
27
|
+
};
|
|
28
|
+
const buttonStyle = {
|
|
29
|
+
position: 'fixed',
|
|
30
|
+
right: 16,
|
|
31
|
+
bottom: 16,
|
|
32
|
+
zIndex: 900,
|
|
33
|
+
display: 'flex',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: 8,
|
|
36
|
+
padding: '8px 12px',
|
|
37
|
+
borderRadius: 999,
|
|
38
|
+
border: '1px solid var(--dsw-alias-border, #3a3a3a)',
|
|
39
|
+
background: 'var(--dsw-alias-surface-raised, #1f1f1f)',
|
|
40
|
+
color: 'var(--dsw-alias-text, #e8e8e8)',
|
|
41
|
+
cursor: 'pointer',
|
|
42
|
+
boxShadow: '0 6px 18px rgba(0,0,0,.3)',
|
|
43
|
+
font: '13px system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function ActionButton({ children, onClick, tone = 'normal', disabled = false }) {
|
|
47
|
+
const colors = {
|
|
48
|
+
normal: { bg: 'var(--dsw-alias-surface, transparent)', border: 'var(--dsw-alias-border, #3a3a3a)' },
|
|
49
|
+
allow: { bg: 'var(--dsw-alias-accent, #2f6fed)', border: 'var(--dsw-alias-accent, #2f6fed)' },
|
|
50
|
+
deny: { bg: 'transparent', border: 'var(--dsw-alias-border, #3a3a3a)' },
|
|
51
|
+
};
|
|
52
|
+
const theme = colors[tone] ?? colors.normal;
|
|
53
|
+
return h(
|
|
54
|
+
'button',
|
|
55
|
+
{
|
|
56
|
+
type: 'button',
|
|
57
|
+
disabled,
|
|
58
|
+
onClick,
|
|
59
|
+
style: {
|
|
60
|
+
padding: '3px 9px',
|
|
61
|
+
borderRadius: 6,
|
|
62
|
+
border: '1px solid ' + theme.border,
|
|
63
|
+
background: theme.bg,
|
|
64
|
+
color: 'var(--dsw-alias-text, #e8e8e8)',
|
|
65
|
+
cursor: disabled ? 'default' : 'pointer',
|
|
66
|
+
opacity: disabled ? 0.55 : 1,
|
|
67
|
+
fontSize: 12,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
children,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ApprovalPanel() {
|
|
75
|
+
const [items, setItems] = useState([]);
|
|
76
|
+
const [open, setOpen] = useState(false);
|
|
77
|
+
const [busy, setBusy] = useState({});
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
let disposed = false;
|
|
81
|
+
let timer;
|
|
82
|
+
const poll = async () => {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch('/device-fleet/approvals', { headers: { accept: 'application/json' } });
|
|
85
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
if (!disposed) setItems(Array.isArray(data.approvals) ? data.approvals : []);
|
|
88
|
+
} catch {
|
|
89
|
+
if (!disposed) setItems([]);
|
|
90
|
+
}
|
|
91
|
+
if (!disposed) timer = setTimeout(poll, POLL_MS);
|
|
92
|
+
};
|
|
93
|
+
poll();
|
|
94
|
+
return () => {
|
|
95
|
+
disposed = true;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const decide = async (id, action, scope) => {
|
|
101
|
+
setBusy((current) => ({ ...current, [id]: true }));
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch('/device-fleet/approvals/decide', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'content-type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({ id, action, ...(scope ? { scope } : {}) }),
|
|
107
|
+
});
|
|
108
|
+
if (response.ok) setItems((current) => current.filter((item) => item.id !== id));
|
|
109
|
+
} finally {
|
|
110
|
+
setBusy((current) => ({ ...current, [id]: false }));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const label = items.length > 0 ? 'Fleet 审批 (' + items.length + ')' : 'Fleet 审批';
|
|
115
|
+
return h(
|
|
116
|
+
'div',
|
|
117
|
+
null,
|
|
118
|
+
open
|
|
119
|
+
? h(
|
|
120
|
+
'div',
|
|
121
|
+
{ style: panelStyle, role: 'dialog', 'aria-label': 'Fleet 待审批请求' },
|
|
122
|
+
h(
|
|
123
|
+
'div',
|
|
124
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 } },
|
|
125
|
+
h('strong', null, '待审批请求'),
|
|
126
|
+
h('button', { type: 'button', onClick: () => setOpen(false), style: { ...buttonStyle, position: 'static', boxShadow: 'none', padding: '3px 9px', fontSize: 12 } }, '收起'),
|
|
127
|
+
),
|
|
128
|
+
items.length === 0
|
|
129
|
+
? h('div', { style: { opacity: 0.7 } }, '暂无待审批请求')
|
|
130
|
+
: items.map((item) =>
|
|
131
|
+
h(
|
|
132
|
+
'div',
|
|
133
|
+
{ key: item.id, style: { borderTop: '1px solid var(--dsw-alias-border, #333)', padding: '8px 0' } },
|
|
134
|
+
h('div', { style: { fontWeight: 600 } }, (item.capability || 'tool') + (item.tool ? ' / ' + item.tool : '')),
|
|
135
|
+
h('div', { style: { opacity: 0.75, fontSize: 12 } }, '来自 ' + item.fromDeviceId + ' · ' + Math.max(0, Math.round(item.expiresInMs / 1000)) + 's 后超时'),
|
|
136
|
+
item.reason ? h('div', { style: { opacity: 0.75, fontSize: 12, marginTop: 2 } }, item.reason) : null,
|
|
137
|
+
h('div', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 11, margin: '4px 0', opacity: 0.8, wordBreak: 'break-all' } }, item.argsSummary),
|
|
138
|
+
h(
|
|
139
|
+
'div',
|
|
140
|
+
{ style: { display: 'flex', gap: 6, marginTop: 4 } },
|
|
141
|
+
h(ActionButton, { tone: 'allow', onClick: () => decide(item.id, 'approve') }, busy[item.id] ? '处理中…' : '允许一次'),
|
|
142
|
+
h(ActionButton, { tone: 'allow', onClick: () => decide(item.id, 'approve', 'ttl') }, '允许 15 分钟'),
|
|
143
|
+
h(ActionButton, { tone: 'deny', onClick: () => decide(item.id, 'deny') }, '拒绝'),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
: null,
|
|
149
|
+
h(
|
|
150
|
+
'button',
|
|
151
|
+
{ type: 'button', style: { ...buttonStyle, borderColor: items.length > 0 ? 'var(--dsw-alias-accent, #2f6fed)' : undefined }, onClick: () => setOpen((value) => !value) },
|
|
152
|
+
h('span', { 'aria-hidden': true }, '🛰️'),
|
|
153
|
+
h('span', null, label),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
inject: ['slots'],
|
|
160
|
+
apply(ctx) {
|
|
161
|
+
ctx.slots.inject('shell.overlay', () =>
|
|
162
|
+
ctx.slots.register({ name: 'shell.overlay', id: 'fleet-approvals', order: 10 }, ApprovalPanel),
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
});
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# dsh-device-fleet bundle patch 鈥?mounts the Host plugin. The settings below are
|
|
2
|
+
# safe defaults; real values (relay address, allowed roots, execution cwd) are
|
|
3
|
+
# meant to be edited in the profile or through the plugin settings page.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: device-fleet
|
|
6
|
+
name: 'dsh-device-fleet'
|
|
7
|
+
config:
|
|
8
|
+
deviceName: !!js process.env.DSH_DEVICE_NAME ?? 'device'
|
|
9
|
+
direct:
|
|
10
|
+
host: '0.0.0.0'
|
|
11
|
+
port: 0
|
|
12
|
+
discovery: true
|
|
13
|
+
policy:
|
|
14
|
+
allowedRoots: []
|
|
15
|
+
capabilities:
|
|
16
|
+
read: auto
|
|
17
|
+
list: auto
|
|
18
|
+
grep: auto
|
|
19
|
+
fetch: auto
|
|
20
|
+
run: ask
|
|
21
|
+
tool: deny
|
|
22
|
+
allowedTools: []
|
|
23
|
+
promptSection: true
|
|
24
|
+
autoApproveAsk: false
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 使用显式入网,而不是"同账号自动互联"
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
DSH 运行时没有"账号 → 设备目录"或"验证对端账号"的端点;插件只能在本机读到自己登录的账号 ID。仅凭"同账号 + 都装插件"无法发现对端,也无法证明对端身份;要实现零配置自动互联必须引入服务端协调。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
采用显式的 Fleet 入网:首台设备创建 Fleet 并签发一次性邀请,其他设备接受邀请后成为成员;账号身份只作为成员校验与隔离条件(账号指纹比对、切换账号即断连),不作为发现或传输通道。
|
|
11
|
+
|
|
12
|
+
## Considered Options
|
|
13
|
+
- 零配置自动互联(需自建或官方服务端做账号-设备绑定)——一期成本与运维/隐私面过大,否决。
|
|
14
|
+
- 局域网内自动出现"待确认设备"、点击确认后入网——作为未来可选增强保留。
|
|
15
|
+
|
|
16
|
+
## Consequences
|
|
17
|
+
一期无需服务端;跨网络发现需要会合点(Hub 设备或 Relay);同账号但未入网的设备不会被自动接纳。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 跨设备协作以共享 Room 为协调单位
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
三个以上 agent 在同一件事上协作时,两两配对会退化成 N(N-1)/2 条互不相干的会话:没有共同历史,协作者之间只能经发起者转发,发起者成为瓶颈与单点;星型转发也同样没有"所有人看到同一份上下文"的群聊感。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
引入 Room 作为跨设备协作的协调单位:由一台设备(Host)托管唯一一份权威消息历史、成员表与共享任务板,所有 Participant 共享并按序广播;任意 Participant 可以直接对其他 Participant 说话,不必经过发起者。Participant 的 agent 按需懒创建,唤醒时只拿任务简报(Participant Brief)与定向消息,不继承房间全量历史。
|
|
11
|
+
|
|
12
|
+
## Considered Options
|
|
13
|
+
- 两两配对的常驻会话——只在 2 台设备时等价于 Room,3 台以上无共同历史,否决作为协调单位(保留为其实现细节:2 人房 = 1:1)。
|
|
14
|
+
- 星型 Lead + workers(仅发起者↔协作者)——实现最简单,但没有群聊感,发起者是单点,否决。
|
|
15
|
+
|
|
16
|
+
## Consequences
|
|
17
|
+
多设备协作需要 Host 角色与广播/排序/持久化;房间历史与各 agent 的上下文解耦,token 成本可控;Host 设备离线时房间需要降级或迁移(一期先按离线只读处理)。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 直调与协作分成两条通道
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
把"只是调用对方一个工具"塞进 Room 或 Participant 会话,会让一次纯工具调用带上对话上下文、会话生命周期与模型唤醒的包袱;反过来,把"多 agent 协作"做成一次性工具调用又丢掉了共同历史与任务状态。两者需要不同的生命周期与上下文预算。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
分成两条通道:
|
|
11
|
+
- **Direct Call(直调)**:A 的 agent 直接调用目标设备上的 tool 并取回结果;不创建 Room/Participant、不读取目标设备对话上下文;目标设备插件按"设备 × 工具"配置决定免审批 / 需本机审批 / 拒绝。
|
|
12
|
+
- **Room(协作)**:只有需要目标设备的 agent 参与时才创建 Participant,给任务简报 + 定向消息,共享房间历史与任务板。
|
|
13
|
+
|
|
14
|
+
直调遇到有状态工具(执行代码、写文件)时,目标设备使用插件管理的隐藏 **Execution Slot** 提供 cwd、沙箱与审批路由;它不跑模型、不进 Room、不保存对话历史。
|
|
15
|
+
|
|
16
|
+
## Considered Options
|
|
17
|
+
- 所有直调都走 Room 的 Participant 会话——上下文与生命周期过重,否决。
|
|
18
|
+
- 直调只允许无状态工具(跑代码必须升级为 agent 任务)——限制过强,否决。
|
|
19
|
+
- 所有直调统一走执行槽——纯读也带上下文,偏重,否决。
|
|
20
|
+
|
|
21
|
+
## Consequences
|
|
22
|
+
直调与协作可以独立演进,token 与上下文成本可控;直调的执行安全由目标设备插件的工具策略负责;执行槽对用户不可见,但会出现在设备内部审计里。
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 直调授权由资源所有者设备决定
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
直调(Direct Call)让 A 的 agent 使用 B 的工具。B 的文件、机器与风险都属于 B,A 不能给自己授权;但逐次弹窗会让一个多步任务反复打断。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
直调的工具策略由**目标设备(资源所有者)**的插件按"设备 × 工具"配置,默认分级:
|
|
11
|
+
- 只读工具:免审批,受目标设备配置的目录根与白名单限制;
|
|
12
|
+
- 执行类(bash/pwsh/terminal/子进程)与写文件:需目标设备本机确认;
|
|
13
|
+
- 危险类(plugin_manager、schedule_*、CUA、交互式终端等):默认拒绝,须在目标设备设备页显式开启。
|
|
14
|
+
|
|
15
|
+
审批在目标设备的插件界面/队列中显示来源设备、agent 会话、工具名、参数预览、cwd 与超时;超时 fail-closed;允许"本次任务内记住(TTL)"。A 侧不能代 B 授权,审批结果记入目标设备审计。
|
|
16
|
+
|
|
17
|
+
## Considered Options
|
|
18
|
+
- 审批转发到 A 的用户——把 B 的资源授权交给 A 侧的人,违背资源所有者原则,否决。
|
|
19
|
+
- 每台设备一个总开关——粒度太粗,表达不了"读可以、跑代码要问",否决。
|
|
20
|
+
- 入网即全授权——最省事也最危险,否决。
|
|
21
|
+
|
|
22
|
+
## Consequences
|
|
23
|
+
"读"与"执行/写"需要分开配置;直调的可用性依赖目标设备有人/有策略回应审批;TTL 授权会短暂扩大目标设备的暴露面,需要审计与撤销。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 会话可就地升级为 Room Host
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
用户往往已经在与本地 agent 对话时才提出"叫上另一台设备一起"的需求。若必须新建会话或切换设备,上下文要搬运、体验割裂。DSH 插件已具备向已有会话日志追加自定义事件并用投影渲染的能力(Agent Team 是现成先例),因此房间状态可以附着在一个已有会话上。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
当前会话可被显式升级为 Room Host:向会话日志追加 `room/*` 事件并挂载 room 投影,保留原有对话上下文。此后以**统一成员接口**添加任意成员(B、C、D……):每台设备对应一个 Participant,Host 只是带 `host` 角色的成员,添加/退出/发言走同一套调用,界面上的名称区别不改变语义。规则:
|
|
11
|
+
- 一个会话最多承担一个 Room 的 `host` 角色;
|
|
12
|
+
- 已是 Participant 的会话不能升级为 Host(另开会话);
|
|
13
|
+
- fork 一个 Host 会话时,子会话不继承 Host 角色或成员关系;
|
|
14
|
+
- Host 会话关闭/归档 → 房间进入只读或结束,Participant 收到通知;
|
|
15
|
+
- 升级与添加成员都是显式动作,不能隐式发生。
|
|
16
|
+
|
|
17
|
+
## Considered Options
|
|
18
|
+
- 总是新建 Room 会话——上下文需搬运,体验割裂,否决。
|
|
19
|
+
- 由目标设备建房、本机以远端身份加入——当前会话不升级,仍有切换感,否决。
|
|
20
|
+
|
|
21
|
+
## Consequences
|
|
22
|
+
房间生命周期绑定 Host 会话,fork/关闭/归档需要额外规则;成员模型保持统一,房间规模与设备命名不影响实现语义。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Room 消息必须点名,且每个成员设备都能发言
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
用户会在不同设备之间切换使用(可能这次对 A 说,下次对 B 说),因此"只有发起者/Host 能发言、其他人只能被动接受"不成立;同时,若每条消息都唤醒所有 agent,会造成 token 浪费、互相打断与重复工作。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
- **每条 Room 消息都带显式收件人**(成员 id 集合;`*` 表示广播,但需显式授权/确认,不是默认行为)。只有被点名的成员被唤醒;未点名成员不跑模型,但仍共享房间权威日志。
|
|
11
|
+
- **Room 视图在所有成员设备上可用**:Host 广播权威日志,成员设备的 UI 订阅展示;从任何设备发言使用同一套接口,从而支持用户"人在这台、下一句换那台"的无感切换。
|
|
12
|
+
- 发言者身份分为两类并都进入权威日志:**用户**(人在某台成员设备上直接发言)与 **agent**(成员设备上的 agent 发言)。
|
|
13
|
+
- Host 的 agent 天然是成员之一,但不再垄断发言;Host 的会话只负责托管日志与排序,未点名时不会因日志追加而被唤醒。
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
成员设备需要房间视图与收发通道;Host 需要广播与排序;"点名"成为协议字段而不是 UI 装饰;未点名成员保持静默。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 用户发言与设备解耦,消息 = 内容 + 发言者 + 点名
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
用户会在 A、B 等设备之间切换发言。如果把"用户"绑定到具体设备,同一人会被拆成多个发言者/接收者,通知、审计与房间状态都会出现无意义的重复;而一条用户消息本质上是无状态的指令或信息,真正决定它被谁处理的是收件人。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
- 用户是跨设备的**单一发言者身份**;设备只是消息的传输来源,仅作审计元数据,不进入房间状态与路由语义。
|
|
11
|
+
- 一条 Room 消息由三部分组成:**内容**、**发言者**(`user` 或某个 agent 成员)、**点名**(收件成员集合)。消息本身不携带设备状态。
|
|
12
|
+
- 用户从任何成员设备直接发言,都以同一 `user` 身份进入权威日志,并直接唤醒被点名的 agent;本机 agent 不参与转发、不消耗本机 token。
|
|
13
|
+
- 用户也可点名本机 agent,让它以该设备上的 agent 成员身份参与。
|
|
14
|
+
|
|
15
|
+
## Considered Options
|
|
16
|
+
- 每个设备一个"用户身份"——同一人被拆分,通知/审计/状态重复,否决。
|
|
17
|
+
- 用户消息一律由本机 agent 代理转发——每次多一次模型调用,也无法精准点名,否决。
|
|
18
|
+
- 只有 Host 设备能发言——违背"任意设备无感交流",否决。
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
房间可以只维护一份人工发言者视角;同一用户的房间视图在各设备一致。未来若支持多人房间,需要引入真正的多用户身份,一期不做。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 复用并扩展 dsh-global-task-list 作为 Room 任务板
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
Room 需要结构化的共享状态(目标、任务、负责人、产物指针),否则被唤醒的 agent 只能反复读聊天记录。`dsh-global-task-list`(v0.4.2,作者自己维护、可改源码)已有高度对齐的数据模型(status/steps/progress/parentId/dependsOn/surface)、持久化 `storageDomain`、成熟的面板 UI 与生成式 surface 渲染;但它刻意设计为**本机、session-independent**,没有共享数据源与 room/owner 维度。
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
走扩展路线(不 fork、不重写):
|
|
11
|
+
- 在 `task_ui` 记录上增加 **room 作用域**(`roomId`)、**负责人成员**(owner member)与**版本号**(用于跨设备 CAS);
|
|
12
|
+
- 增加 **Host 侧程序化 API**(Host service 或 Typert Remote)供 Fleet 插件读写,而不是复用未认证的 `/task-ui` HTTP 路由;
|
|
13
|
+
- **Room Host 权威**;参与设备把房间任务**镜像**到本地 `task_ui`(只读,写入回到 Host);面板按 room 分组并标记只读镜像;
|
|
14
|
+
- agent 工具面沿用现有 `task_*` 并增加可选 `roomId`,或新增 `room_task_*`(命名在工具面设计中再定)。
|
|
15
|
+
|
|
16
|
+
## Considered Options
|
|
17
|
+
- Fork TASK_SCHEMA 与面板组件进 Fleet 插件——要维护 ~48KB 面板与 CSS 的副本,且失去后续改进,否决。
|
|
18
|
+
- Room 板只存 `{deviceId, taskId}` 指针、插件零改动——没有统一视图、状态滞后,仅可作 PoC 过渡,否决。
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
个人任务库与房间任务板共用一份存储与面板,需要明确的 room 过滤与只读镜像规则;schema 增加字段(domain 后端描述符不随 schema 变,无需版本升级);跨设备更新需要 CAS 与冲突提示;扩展后的插件成为 Fleet 的依赖项。
|