dsh-device-fleet 0.1.0-dev.1 → 0.1.0-dev.11

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/README.md CHANGED
@@ -7,7 +7,48 @@ DeepSeek Harness 的设备联邦插件:把登录同一账号、且都安装本
7
7
  - ✅ **网络层 v1**:设备身份、邀请、端到端加密安全通道、直连(手动地址)、UDP 组播发现
8
8
  - ✅ **网络层 v2**:自建 Relay(fleetKey 鉴权的信令 + TCP 拼接数据面,不做 NAT 打洞)
9
9
  - ✅ **Host 插件 + 直调 MVP(代码完成、单测通过,待真实 DSH 安装联调)**:`fleet_list` / `fleet_tools` / `fleet_call` + `fleet_read` / `fleet_fetch` / `fleet_run`、Tool Policy、Execution Slot、审批 fail-closed
10
- - ⏳ Room / Room Board、委派、审批收件箱 UI、设备页/配对/房间 UI(下一步)
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
+ - ✅ **审批面板 UI(dev.10)**:Host `/device-fleet/approvals` HTTP 数据面 + Client `shell.overlay` 浮动审批按钮/面板(允许一次/15 分钟/拒绝);单测 14/14;刷新页面后可见
13
+ - ⏳ 远程参与者传输、房间视图、插件重载 rehydrate、SSE 推送(下一步)
14
+
15
+ ## 安装(开发版)
16
+
17
+ 已发布到 npm:**`dsh-device-fleet@0.1.0-dev.3`**(dist-tag `dev`)。
18
+
19
+ ```sh
20
+ # CLI / web profile
21
+ dsh plugin --profile web add dsh-device-fleet@dev
22
+
23
+ # 桌面 app:先完全退出 app,再安装,然后重新打开
24
+ dsh plugin --profile desktop add dsh-device-fleet@dev
25
+
26
+ # 离线安装包(拷到另一台设备)
27
+ dsh plugin --profile desktop add C:/path/to/dsh-device-fleet-0.1.0-dev.3.tgz
28
+ ```
29
+
30
+ 两台设备的 **DSH 版本与插件版本必须一致**(ADR-0024);不一致会在握手时直接拒绝。
31
+
32
+ ### 配对(Fleet 入网)
33
+
34
+ 1. 设备 A:对 agent 说“生成 Fleet 邀请码” → 模型调用 `fleet_invite`,拿到一次性邀请码(含 fleetKey,注意保密)。
35
+ 2. 设备 B:安装并重启后,对 agent 说“加入 Fleet:`<code>`” → `fleet_join` 把邀请排队;**重启 DSH** 后生效。
36
+ 3. 两台设备各自在 profile patch 里配置 `policy.allowedRoots` 与 `execution.cwd`(不配时直调读取一律被拒)。
37
+ 4. 设备 B:`fleet_list` 看到设备 A → `fleet_read(device=A, path="A 允许根内的文件")` 验证。
38
+
39
+ ```yaml
40
+ # 两个 profile 的 cordis.patch.yml 里给 device-fleet 行追加 config:
41
+ - id: device-fleet
42
+ config:
43
+ deviceName: desktop
44
+ execution:
45
+ cwd: D:/work
46
+ agentPreset: standard
47
+ policy:
48
+ allowedRoots: [D:/work]
49
+ ```
50
+
51
+ `fleet_run` 默认需要目标设备本机审批;审批收件箱 UI 尚未实现(当前 fail-closed)。第一阶段建议先用 `fleet_read` / `fleet_fetch` 验证链路。
11
52
 
12
53
  ## 设计文档
13
54
 
package/client.js ADDED
@@ -0,0 +1,228 @@
1
+ // Fleet approval panel (Client half).
2
+ // The trigger lives in the native sidebar footer action row (no overlap with
3
+ // content panels); the inbox opens as an intentional centered dialog.
4
+ // Data comes from the Host routes registered by src/host/approval-web.js.
5
+ window.__ModuleLoader__.load({
6
+ id: 'dsh-device-fleet',
7
+ factory(require) {
8
+ const React = require('react');
9
+ const h = React.createElement;
10
+ const { useEffect, useState } = React;
11
+
12
+ const POLL_MS = 2500;
13
+ const store = {
14
+ items: [],
15
+ open: false,
16
+ listeners: new Set(),
17
+ };
18
+ let pollTimer = null;
19
+ let polling = false;
20
+
21
+ const notify = () => {
22
+ for (const listener of store.listeners) listener();
23
+ };
24
+
25
+ const setItems = (items) => {
26
+ const next = Array.isArray(items) ? items : [];
27
+ if (next.length === store.items.length && next.every((item, index) => item.id === store.items[index].id)) return;
28
+ store.items = next;
29
+ notify();
30
+ };
31
+
32
+ const startPolling = () => {
33
+ if (polling) return () => {};
34
+ polling = true;
35
+ const tick = async () => {
36
+ try {
37
+ const response = await fetch('/device-fleet/approvals', { headers: { accept: 'application/json' } });
38
+ if (!response.ok) throw new Error('HTTP ' + response.status);
39
+ const data = await response.json();
40
+ setItems(data.approvals);
41
+ } catch {
42
+ setItems([]);
43
+ }
44
+ if (polling) pollTimer = setTimeout(tick, POLL_MS);
45
+ };
46
+ tick();
47
+ return () => {
48
+ polling = false;
49
+ if (pollTimer) clearTimeout(pollTimer);
50
+ pollTimer = null;
51
+ };
52
+ };
53
+
54
+ const useStore = () => {
55
+ const [, setVersion] = useState(0);
56
+ useEffect(() => {
57
+ const listener = () => setVersion((value) => value + 1);
58
+ store.listeners.add(listener);
59
+ return () => store.listeners.delete(listener);
60
+ }, []);
61
+ return store;
62
+ };
63
+
64
+ const decide = async (id, action, scope) => {
65
+ try {
66
+ await fetch('/device-fleet/approvals/decide', {
67
+ method: 'POST',
68
+ headers: { 'content-type': 'application/json' },
69
+ body: JSON.stringify({ id, action, ...(scope ? { scope } : {}) }),
70
+ });
71
+ } finally {
72
+ store.items = store.items.filter((item) => item.id !== id);
73
+ notify();
74
+ }
75
+ };
76
+
77
+ const dialogStyle = {
78
+ position: 'fixed',
79
+ left: '50%',
80
+ top: '12vh',
81
+ transform: 'translateX(-50%)',
82
+ width: 'min(440px, calc(100vw - 32px))',
83
+ maxHeight: '70vh',
84
+ overflow: 'auto',
85
+ zIndex: 950,
86
+ background: 'var(--dsw-alias-surface-raised, #1f1f1f)',
87
+ color: 'var(--dsw-alias-text, #e8e8e8)',
88
+ border: '1px solid var(--dsw-alias-border, #3a3a3a)',
89
+ borderRadius: 12,
90
+ boxShadow: '0 18px 48px rgba(0,0,0,.45)',
91
+ padding: 14,
92
+ font: '13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif',
93
+ };
94
+
95
+ const smallButton = (label, onClick, tone, disabled) =>
96
+ h(
97
+ 'button',
98
+ {
99
+ type: 'button',
100
+ disabled: disabled === true,
101
+ onClick,
102
+ style: {
103
+ padding: '3px 9px',
104
+ borderRadius: 6,
105
+ border: '1px solid ' + (tone === 'allow' ? 'var(--dsw-alias-accent, #2f6fed)' : 'var(--dsw-alias-border, #3a3a3a)'),
106
+ background: tone === 'allow' ? 'var(--dsw-alias-accent, #2f6fed)' : 'transparent',
107
+ color: 'var(--dsw-alias-text, #e8e8e8)',
108
+ cursor: disabled === true ? 'default' : 'pointer',
109
+ opacity: disabled === true ? 0.55 : 1,
110
+ fontSize: 12,
111
+ },
112
+ },
113
+ label,
114
+ );
115
+
116
+ function Trigger({ wide }) {
117
+ const current = useStore();
118
+ const count = current.items.length;
119
+ const expanded = wide !== false;
120
+ return h(
121
+ 'button',
122
+ {
123
+ type: 'button',
124
+ title: count > 0 ? 'Fleet 审批:' + count + ' 条待处理' : 'Fleet 审批',
125
+ 'aria-label': 'Fleet 审批' + (count > 0 ? '(' + count + ' 条待处理)' : ''),
126
+ onClick: () => {
127
+ store.open = !store.open;
128
+ notify();
129
+ },
130
+ style: {
131
+ display: 'flex',
132
+ alignItems: 'center',
133
+ gap: 6,
134
+ padding: expanded ? '5px 10px' : '5px 7px',
135
+ borderRadius: 8,
136
+ border: '1px solid ' + (count > 0 ? 'var(--dsw-alias-accent, #2f6fed)' : 'var(--dsw-alias-border, #3a3a3a)'),
137
+ background: count > 0 ? 'var(--dsw-alias-accent, #2f6fed)' : 'transparent',
138
+ color: 'var(--dsw-alias-text, #e8e8e8)',
139
+ cursor: 'pointer',
140
+ fontSize: 12,
141
+ whiteSpace: 'nowrap',
142
+ },
143
+ },
144
+ h('span', { 'aria-hidden': true }, '🛰️'),
145
+ expanded ? h('span', null, count > 0 ? '审批 ' + count : '审批') : null,
146
+ );
147
+ }
148
+
149
+ function Panel() {
150
+ const current = useStore();
151
+ if (!current.open) return null;
152
+ useEffect(() => {
153
+ const onKey = (event) => {
154
+ if (event.key === 'Escape') {
155
+ store.open = false;
156
+ notify();
157
+ }
158
+ };
159
+ window.addEventListener('keydown', onKey);
160
+ return () => window.removeEventListener('keydown', onKey);
161
+ }, []);
162
+ return h(
163
+ 'div',
164
+ {
165
+ style: { position: 'fixed', inset: 0, zIndex: 940, background: 'rgba(0,0,0,.35)' },
166
+ onClick: () => {
167
+ store.open = false;
168
+ notify();
169
+ },
170
+ role: 'presentation',
171
+ },
172
+ h(
173
+ 'div',
174
+ { style: dialogStyle, role: 'dialog', 'aria-label': 'Fleet 待审批请求', onClick: (event) => event.stopPropagation() },
175
+ h(
176
+ 'div',
177
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 } },
178
+ h('strong', null, '待审批请求'),
179
+ h(
180
+ 'button',
181
+ {
182
+ type: 'button',
183
+ onClick: () => {
184
+ store.open = false;
185
+ notify();
186
+ },
187
+ style: { background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', fontSize: 12, opacity: 0.8 },
188
+ },
189
+ '收起',
190
+ ),
191
+ ),
192
+ current.items.length === 0
193
+ ? h('div', { style: { opacity: 0.7 } }, '暂无待审批请求')
194
+ : current.items.map((item) =>
195
+ h(
196
+ 'div',
197
+ { key: item.id, style: { borderTop: '1px solid var(--dsw-alias-border, #333)', padding: '9px 0' } },
198
+ h('div', { style: { fontWeight: 600 } }, (item.capability || 'tool') + (item.tool ? ' / ' + item.tool : '')),
199
+ h('div', { style: { opacity: 0.75, fontSize: 12 } }, '来自 ' + item.fromDeviceId + ' · ' + Math.max(0, Math.round(item.expiresInMs / 1000)) + 's 后超时'),
200
+ item.reason ? h('div', { style: { opacity: 0.75, fontSize: 12, marginTop: 2 } }, item.reason) : null,
201
+ h('div', { style: { fontFamily: 'ui-monospace, monospace', fontSize: 11, margin: '5px 0', opacity: 0.8, wordBreak: 'break-all' } }, item.argsSummary),
202
+ h(
203
+ 'div',
204
+ { style: { display: 'flex', gap: 6, marginTop: 4, flexWrap: 'wrap' } },
205
+ smallButton('允许一次', () => decide(item.id, 'approve'), 'allow'),
206
+ smallButton('允许 15 分钟', () => decide(item.id, 'approve', 'ttl'), 'allow'),
207
+ smallButton('拒绝', () => decide(item.id, 'deny'), 'deny'),
208
+ ),
209
+ ),
210
+ ),
211
+ ),
212
+ );
213
+ }
214
+
215
+ return {
216
+ inject: ['slots'],
217
+ apply(ctx) {
218
+ ctx.effect(() => startPolling());
219
+ ctx.slots.inject('sidebar.footer.action', () =>
220
+ ctx.slots.register({ name: 'sidebar.footer.action', id: 'fleet-approvals', order: 5, label: 'Fleet 审批' }, Trigger),
221
+ );
222
+ ctx.slots.inject('shell.overlay', () =>
223
+ ctx.slots.register({ name: 'shell.overlay', id: 'fleet-approvals-panel', order: 10 }, Panel),
224
+ );
225
+ },
226
+ };
227
+ },
228
+ });
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-device-fleet bundle patch — mounts the Host plugin. The settings below are
1
+ # dsh-device-fleet bundle patch 鈥?mounts the Host plugin. The settings below are
2
2
  # safe defaults; real values (relay address, allowed roots, execution cwd) are
3
3
  # meant to be edited in the profile or through the plugin settings page.
4
4
  - insert:
@@ -22,3 +22,5 @@
22
22
  allowedTools: []
23
23
  promptSection: true
24
24
  autoApproveAsk: false
25
+
26
+
@@ -0,0 +1,31 @@
1
+ # 审批收件箱(逻辑层,dev.7)
2
+
3
+ ## 实现
4
+
5
+ - `src/host/approval-inbox.js`:`ApprovalInbox`
6
+ - `request()`:排队、60s(可配)超时 fail-closed、事件 `request/approved/denied/expired/closed`;
7
+ - `list()`:待审列表(来源设备、能力、工具、参数摘要、剩余时间);
8
+ - `approve(id, {scope})`:`once` / `ttl`(TTL 由 Tool Policy 的 `approvalTtlMs` 控制);
9
+ - `deny(id, reason)`;`close()` 时全部失败关闭。
10
+ - `DirectCallHost` / `DelegationHost` 的 `ask` 档改为等待收件箱结果(不再一律拒绝),批准后按 scope 写入 Tool Policy 授权。
11
+ - 新工具:`fleet_approvals` / `fleet_approve` / `fleet_deny`;`fleet_simulate_request`(仅 `approvals.simulation: true`)用于本机无第二台设备时演练。
12
+ - 配置:`approvals: {ttlMs, maxPending, simulation}`。
13
+
14
+ ## 已验证
15
+
16
+ 单元测试(`node test/host.test.mjs`,10/10):
17
+ - 收件箱排队/批准/拒绝/超时/关闭/上限;
18
+ - DirectCallHost 在 `run=ask` 时阻塞等待收件箱批准后才执行;
19
+ - 工具 handler 的列表/批准/拒绝/模拟。
20
+
21
+ 当前 DSH 环境内模拟(dev.7,无需第二台设备):
22
+ 1. `fleet_simulate_request` 创建一条 `run/pwsh` 请求,`fleet_approvals` 显示 `58328ms left`;
23
+ 2. `fleet_approve(id, scope="ttl")` → `approved … (ttl)`,再次 `fleet_approvals` 为 `(no pending approvals)`;
24
+ 3. 再模拟一条 `delegate` 请求,`fleet_deny` → `denied …`;
25
+ 4. 对不存在的 id 调 `fleet_approve` → `Error: [unknown-request]`。
26
+
27
+ ## 未完成
28
+
29
+ - **Client UI 审批面板/通知**(需 Client bundle):当前只能通过工具或后续 UI 应答;Host 侧已发出 `request/approved/denied/expired` 事件,面板可直接消费;
30
+ - `room` 级授权 scope(需 Room 概念落地后再加);
31
+ - 审批结果持久化审计日志(后续与 Room 事件一起做)。
@@ -0,0 +1,38 @@
1
+ # 审批面板 UI(dev.9 / dev.10)
2
+
3
+ ## Host 数据面
4
+
5
+ `src/host/approval-web.js`:
6
+
7
+ - `GET /device-fleet/approvals` → `{ approvals: [...] }`(来自 ApprovalInbox.list);
8
+ - `POST /device-fleet/approvals/decide`,body `{ id, action: "approve"|"deny", scope?, reason? }` → 决策走 ApprovalInbox;
9
+ - 仅注册在 DSH web 载体的本地监听上;决策仍受 ApprovalInbox 的超时/关闭/未知 id 约束;
10
+ - 纯逻辑 `createApprovalApi` 与路由注册分离,便于单测;路由在插件卸载时 dispose。
11
+
12
+ ## Client 半
13
+
14
+ `client.js`(闭包工厂,`window.__ModuleLoader__.load`,id = `dsh-device-fleet`):
15
+
16
+ - 注册 `shell.overlay`(root)列表项 `fleet-approvals`,右下角浮动按钮「Fleet 审批 (n)」;
17
+ - 2.5s 轮询 `/device-fleet/approvals`,有请求时按钮高亮;点开面板显示来源设备/能力/工具/参数摘要/剩余超时;
18
+ - 按钮:允许一次 / 允许 15 分钟(ttl)/ 拒绝,POST 到 decide 后从列表移除;
19
+ - 使用 `--dsw-alias-*` token(带 fallback),不覆盖宿主根、不读其他插件 DOM。
20
+
21
+ `package.json` 增加 `dsh.client = { platform: web, immediately: true, inject: ["@deepseek-ai/dsh-client-runtime"] }` 与 `./client` 导出、`files` 包含 client.js。
22
+
23
+ ## 验证
24
+
25
+ - 单测:`approval HTTP API lists and decides requests, and registers two routes`(列表/决策/路由注册/GET handler);
26
+ - Host 单测总计 **14/14** 通过,网络层 9/9;
27
+ - Host 路由注册:插件 dev.9/10 激活成功(无重复路由错误);
28
+ - Client 视觉:**未验证**(无浏览器控制;需要刷新页面加载 client bundle),`node --check client.js` 通过。
29
+
30
+ ## 使用
31
+
32
+ 刷新当前 Web 页面(F5)后,右下角应出现「Fleet 审批」浮动按钮。用 `fleet_simulate_request` 造一条请求即可看到面板内容;点击允许/拒绝后 Host 端 `fleet_approvals` 同步清空。
33
+
34
+ ## 限制
35
+
36
+ - 面板轮询而非推送,规模大时应换 SSE/事件;
37
+ - 路由未额外鉴权(依赖 web 载体自身的本地/会话边界);
38
+ - 面板不含房间视图(下一版可加 Room 时间线/看板页)。
@@ -0,0 +1,67 @@
1
+ # 开发版发布记录
2
+
3
+ - 包名:`dsh-device-fleet`
4
+ - 版本:`0.1.0-dev.3`
5
+ - dist-tag:`dev`(同时也可用精确版本安装)
6
+ - registry:<https://registry.npmjs.org/dsh-device-fleet>
7
+ - tarball:<https://registry.npmjs.org/dsh-device-fleet/-/dsh-device-fleet-0.1.0-dev.3.tgz>
8
+ - 本地离线包:`dsh-device-fleet-0.1.0-dev.3.tgz`(仓库根目录,可拷贝安装)
9
+ - 发布时间:2026-11-09(Asia/Shanghai)
10
+ - 内容:网络层 v1/v2 + Host 插件 + 直调 MVP(`fleet_invite/join/list/tools/call/read/fetch/run`);Room/Board/委派/审批 UI 未包含。
11
+
12
+ ## 安装
13
+
14
+ ```sh
15
+ dsh plugin --profile web add dsh-device-fleet@dev
16
+ # 桌面 app:完全退出后
17
+ dsh plugin --profile desktop add dsh-device-fleet@dev
18
+ ```
19
+
20
+ 发布时 npm 对新包做了短暂的处理/占位(`0.0.0-stage`),约几分钟后真实版本生效;后续版本号递增、tag 复用 `dev`。
21
+
22
+ ## 已知限制
23
+
24
+ - 严格同版本:两台设备必须安装同一 `0.1.0-dev.3`,DSH 版本也要同一发布线;
25
+ - `fleet_run` 的 `ask` 审批目前 fail-closed(审批收件箱未实现);
26
+ - `fleet_fetch` 仅返回文本;
27
+ - 首次加载会在 `$DSH_HOME/device-fleet/` 自动创建身份与独立 Fleet;加入已有 Fleet 需要先 `fleet_join` 再重启;
28
+ - 默认 `direct.host=0.0.0.0` + UDP 组播发现;Windows 防火墙可能弹窗,需允许。
29
+
30
+ ## 安装激活记录(本机 desktop profile,2026-11-09)
31
+
32
+ - 第一次 install_bundle 被拒:`package.json` 带 UTF-8 BOM → 去掉 BOM 后通过;
33
+ - 第一次 activate 失败:`@deepseek-ai/schemastery` 只有默认导出(`import Schema from ...`),改为默认导入;
34
+ - 第二次 activate 失败:Cordis 直接访问未 inject 的服务会抛错 → 可选服务统一走 `ctx.get(name)`;
35
+ - 最终 `set_plugin include:device-fleet enabled=true` → `application: applied`,8 个 `fleet_*` 工具出现在模型工具表,`fleet_list` 返回本机 device id,`taskui_probe` 仍为 v0.4.2 / tasks=247(与 task-list 无冲突)。
36
+ - 已发布 `0.1.0-dev.3`(包含上述激活修复);`0.1.0-dev.1` 会激活失败,不要使用。
37
+ ## 0.1.0-dev.3(本次)
38
+
39
+ - 邀请里新增 `endpoints`(创建者的局域网/热点 IPv4 + direct 端口),加入方重启后自动向这些地址回连,绕开组播发现失败;
40
+ - 新增 `fleet_status`(deviceId/直连端口/发现/连接/roster/hints)与 `fleet_connect(host, port)`(手动直连,成功后会从握手学到对端 deviceId);
41
+ - `fleet_list` 仍是本地视图;配对成功后双方 roster 会自动固定对端公钥。
42
+ ## 0.1.0-dev.10(稳定化)
43
+
44
+ - 直连端口可固定(profile `direct.port`),插件重载不再换端口;
45
+ - 连接断开后自动重试已知地址(不再只在启动时试一次);
46
+ - `fleet_connect` 手动地址会持久化到 `peer-hints.json`,重启后继续回连;
47
+ - 新增 `fleet_delegate`(dev.5):目标设备用 `SubagentProvider` + `delivery:'caller'` 跑 agent 并返回最终结果。
48
+ ## 0.1.0-dev.10(审批收件箱,逻辑层)
49
+
50
+ - 新增 `ApprovalInbox`:ask 档请求排队、超时 fail-closed、approve(once/ttl)/deny、关闭时全部失败关闭;
51
+ - 直调与委派的 ask 档改为等待本机收件箱结果(不再一律拒绝);
52
+ - 新工具:`fleet_approvals` / `fleet_approve` / `fleet_deny`;`fleet_simulate_request`(仅 `approvals.simulation: true`)用于本机无第二台设备时演练流程;
53
+ - 审批面板(Client UI)与通知尚未实现,当前通过工具/后续 UI 应答。
54
+ ## 0.1.0-dev.10(Room 逻辑层,本机模拟)
55
+
56
+ - `RoomService`:当前会话升级为 Room Host;本地模拟参与者(真实 DSH session/agent);
57
+ - 寻址消息:`room_send` 只唤醒被点名成员,Host 会话写权威 `room/message` 事件;
58
+ - 可见性:`room_messages` 提供完整 transcript;未点名成员不运行;
59
+ - Room Board:`room_task_add/update/list`,`dependsOn` 串行交接(前置完成时唤醒下一个 owner),无依赖任务并行不受影响;
60
+ - 工具:`room_create / room_add_local / room_remove_member / room_send / room_messages / room_status / room_task_* / room_close`;
61
+ - 远程参与者传输与 Client UI 未做(下一步)。
62
+ ## 0.1.0-dev.10(审批面板 UI,第一版)
63
+
64
+ - Host 新增本地 HTTP 数据面:GET `/device-fleet/approvals`、POST `/device-fleet/approvals/decide`(仅本机 web 载体,决策仍走 ApprovalInbox);
65
+ - Client half:`client.js` 闭包工厂,注册 `shell.overlay` 浮动「Fleet 审批」按钮 + 面板,轮询待审批列表,支持允许一次 / 允许 15 分钟 / 拒绝;
66
+ - package.json 增加 `dsh.client` 与 `./client` 导出;`files` 增加 client.js;
67
+ - Client 视觉验证需要刷新页面;当前无浏览器控制,仅验证了 bundle 语法与 Host 路由注册。
@@ -0,0 +1,30 @@
1
+ # 首次配对实录(2026-11-09)
2
+
3
+ ## 环境
4
+
5
+ - 设备 A:本机(Windows,DSH desktop profile,`device-fleet` 0.1.0-dev.3)
6
+ - deviceId:`50fdaf90-f0aa-440e-9fa6-c60ba5051001`,deviceName:`desktop`
7
+ - 直连:`0.0.0.0:60506`(热点网段 `192.168.137.1:60506`)
8
+ - allowedRoots:`E:/Dsh_test`
9
+ - 设备 B:另一台设备(同一热点 `192.168.137.0/24`,plugin 0.1.0-dev.3)
10
+ - deviceId:`c2d42572-e5f0-4d7e-9fff-c28d9a668439`
11
+ - 热点网段内网段:A `192.168.137.1/24`,B 为客户端之一。
12
+
13
+ ## 过程
14
+
15
+ 1. dev.3 邀请携带 `endpoints`(A 的 `210.73.18.88:60506` 与 `192.168.137.1:60506`)。
16
+ 2. B 安装 dev.3 → `fleet_join` 新邀请 → 重启;插件读取 enrollment endpoints,自动回连 A。
17
+ 3. 结果:双方握手成功,roster 互固定公钥;A 侧 `fleet_list` 显示 B `online`,`fleet_status.connected` 含 B。
18
+ 4. `fleet_tools(device=B)` 成功(说明 RPC/加密通道正常)。
19
+ 5. `fleet_read(device=B, path=E:/Dsh_test/hello-from-A.txt)` 返回 `denied: no allowed roots configured on this device` —— 策略按设计拦截,B 侧尚未配置 allowedRoots。
20
+
21
+ ## 结论
22
+
23
+ - **网络层 v1 直连 + 配对流程打通**(组播发现在该热点下不可用,邀请 endpoints 自动回连解决了问题)。
24
+ - **Tool Policy 生效**:目标设备未配置允许根时直调被拒。
25
+ - 待配置 B 的 `policy.allowedRoots` / `execution.cwd` 后可双向直调;`fleet_run` 待审批收件箱 UI。
26
+
27
+ ## 复现要点
28
+
29
+ - 邀请里的 endpoints 由 `networkInterfaces()` 的非回环 IPv4 + 当前 direct port 组成;direct 端口每次插件重载会变,重载后需重新生成邀请或手动 `fleet_connect`。
30
+ - 同一账号指纹:邀请携带 `accountFingerprint`;B 登录不同账号会在加载时被标记 mismatch 并拒绝连接。
@@ -0,0 +1,33 @@
1
+ # 跨设备调用实测(2026-11-09,dev.6)
2
+
3
+ 环境:A `desktop` / `50fdaf90-…`(端口固定 60580);B `laptop` / `c2d42572-…`(dev.6,同一热点)。
4
+ 测试文件:A `E:\Dsh_test\hello-from-A.txt`;B `E:\Dsh_test\hello-from-B.txt`。
5
+
6
+ ## 结果
7
+
8
+ | # | 调用 | 结果 | 耗时 |
9
+ |---|---|---|---|
10
+ | 1 | `fleet_read(B, E:/Dsh_test/hello-from-B.txt)` | ✅ 返回 `hello from device B` | 48ms |
11
+ | 2 | `fleet_call(B, tool="read", {file_path:…hello-from-B.txt})` | ✅ 返回同一内容(原始工具直调) | 4ms |
12
+ | 3 | `fleet_fetch(B, hello-from-B.txt)` | ✅ 返回文本 | 1ms |
13
+ | 4 | `fleet_run(B, "echo hello-from-B")` | ⚠️ `approval_denied`(B 策略 run=ask,审批 UI 未实现,fail-closed 正确) | 即时 |
14
+ | 5 | `fleet_delegate(B, "读取 hello-from-B.txt 并原样返回")` | ✅ B 的 agent(spawn 子会话 `1a768f89…`)读文件并回传最终文本 | 2.7s |
15
+ | 6 | `fleet_delegate(B, "用 fleet_read 读 A 的 hello-from-A.txt 并原样返回")` | ✅ B 的 agent 反向调用 A 的 `read`,回传 `hello from device A (fleet test)`(子会话 `515f8c1b…`) | 2.5s |
16
+
17
+ ## 结论
18
+
19
+ - v1 直连 + 端到端加密 + 配对/roster 固定公钥:**通过**;
20
+ - 直调(typed 快捷工具 / `fleet_call` 原始工具 / fetch):**通过**;
21
+ - Tool Policy 与 fail-closed 审批:**按设计生效**;
22
+ - 跨设备委派(B 的 agent 在本机执行并回传;B 的 agent 反向调 A 工具):**通过**;
23
+ - 遗留:审批收件箱 UI(`run`/`delegate` 的 `ask` 档目前只能拒绝或配置 auto);Room/Room Board/设备页 UI 未开始;委派返回文本里 `[reasoning]` 是结果里的 reasoning block 展示,可后续过滤。
24
+
25
+ ## 复现命令(A 侧)
26
+
27
+ ```
28
+ fleet_list()
29
+ fleet_read(device="c2d42572-…", path="E:/Dsh_test/hello-from-B.txt")
30
+ fleet_call(device="c2d42572-…", tool="read", parameters={file_path:"E:/Dsh_test/hello-from-B.txt"})
31
+ fleet_delegate(device="c2d42572-…", task="读取 E:/Dsh_test/hello-from-B.txt 并原样返回")
32
+ fleet_delegate(device="c2d42572-…", task="用 fleet_read 读设备 50fdaf90-… 的 E:/Dsh_test/hello-from-A.txt 并原样返回")
33
+ ```
@@ -0,0 +1,31 @@
1
+ # Room 逻辑层实测(2026-11-09,dev.8,单机模拟参与者)
2
+
3
+ 在当前这台 DSH 内,用两个**真实本地 session/agent** 充当 Room 参与者,验证寻址唤醒与看板串行交接,不依赖第二台设备。
4
+
5
+ ## 流程与结果
6
+
7
+ 1. `room_create` → roomId = 当前会话 `session-269260c9-…`,host 成员 `host:session-269260c9-…`。
8
+ 2. `room_add_local(name="worker-a", cwd=E:/Dsh_test)` → 真实 session `session-d063026e-…`。
9
+ 3. `room_send(to=[worker-a], text="读取 hello-from-A.txt 并用 room_send 回复 host")` → `woke worker-a`;
10
+ - worker-a 的 agent 真实跑了一轮模型 + 本机 `read`,并在 3.4s 内调用 `room_send` 回传 `hello from device A (fleet test)`;
11
+ - transcript 两条消息,完整可见。
12
+ 4. `room_add_local(name="worker-b")`;看板:
13
+ - taskA `读取 hello-from-A.txt` owner=worker-a;
14
+ - taskB `汇总读取结果` owner=worker-b,`dependsOn=[taskA]`;
15
+ - taskC `并行:独立检查` owner=worker-a,无依赖。
16
+ 5. `room_task_update(taskA, done)` → 返回 `handed off to worker-b`;worker-b 被点名唤醒,30s 内完成汇总并 `room_send` 给 host(内容含前序结果、交叉校验、并行任务仍 pending 的说明),同时把自己 taskB 置为 running。
17
+ 6. `room_task_list`:taskA done、taskB running、taskC pending(并行任务未被串行交接影响)。
18
+ 7. `room_close`。
19
+
20
+ ## 结论
21
+
22
+ - Room 生命周期、成员管理、寻址消息、未点名不唤醒、host 权威日志:单机逻辑通过;
23
+ - Room Board `dependsOn` 串行交接 / 并行任务不阻塞:真实 agent 验证通过;
24
+ - 参与者使用的都是真实 DSH agent(各自 session),不是 mock。
25
+
26
+ ## 限制(下一步)
27
+
28
+ - 远程参与者传输(把 local participant 换成远端设备成员)尚未接入;
29
+ - Client 面板/房间视图未做(用户无法在 GUI 直接看 room 时间线/看板);
30
+ - 房间状态在插件内存 + host session 事件里,插件重载后需要 rehydrate(未实现);
31
+ - 房间策略仅存储,尚未强制(invite/join/maxMembers 中 maxMembers 已生效)。
package/package.json CHANGED
@@ -1,45 +1,54 @@
1
- {
2
- "name": "dsh-device-fleet",
3
- "version": "0.1.0-dev.1",
4
- "description": "DeepSeek Harness device fleet: enroll devices, direct-call remote tools, shared Rooms with lazy-woken agents, and delegated subagents across devices.",
5
- "type": "module",
6
- "main": "src/plugin/index.js",
7
- "exports": {
8
- ".": "./src/plugin/index.js",
9
- "./net": "./src/net/index.js",
10
- "./host": "./src/host/index.js",
11
- "./package.json": "./package.json"
12
- },
13
- "engines": {
14
- "node": "\u003e=22"
15
- },
16
- "dsh": {
17
- "bundle": {
18
- "patch": "./cordis.patch.yml"
19
- }
20
- },
21
- "peerDependencies": {
22
- "@deepseek-ai/cordis": "~4.1.0",
23
- "@deepseek-ai/dsh-tools": "^0.2.2-rc.1",
24
- "@deepseek-ai/dsh-llm": "^0.2.2-rc.1",
25
- "@deepseek-ai/schemastery": "~3.19.0"
26
- },
27
- "scripts": {
28
- "test": "node test/net.test.mjs \u0026\u0026 node test/host.test.mjs",
29
- "test:net": "node test/net.test.mjs",
30
- "test:host": "node test/host.test.mjs"
31
- },
32
- "files": [
33
- "src",
34
- "cordis.patch.yml",
35
- "README.md",
36
- "CONTEXT.md",
37
- "docs",
38
- "LICENSE"
39
- ],
40
- "publishConfig": {
41
- "access": "public",
42
- "tag": "dev"
43
- },
44
- "license": "MIT"
45
- }
1
+ {
2
+ "name": "dsh-device-fleet",
3
+ "version": "0.1.0-dev.11",
4
+ "description": "DeepSeek Harness device fleet: enroll devices, direct-call remote tools, shared Rooms with lazy-woken agents, and delegated subagents across devices.",
5
+ "type": "module",
6
+ "main": "src/plugin/index.js",
7
+ "exports": {
8
+ ".": "./src/plugin/index.js",
9
+ "./client": "./client.js",
10
+ "./net": "./src/net/index.js",
11
+ "./host": "./src/host/index.js",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "engines": {
15
+ "node": ">=22"
16
+ },
17
+ "dsh": {
18
+ "bundle": {
19
+ "patch": "./cordis.patch.yml"
20
+ },
21
+ "client": {
22
+ "platform": "web",
23
+ "immediately": true,
24
+ "inject": [
25
+ "@deepseek-ai/dsh-client-runtime"
26
+ ]
27
+ }
28
+ },
29
+ "peerDependencies": {
30
+ "@deepseek-ai/cordis": "~4.1.0",
31
+ "@deepseek-ai/dsh-tools": "^0.2.2-rc.1",
32
+ "@deepseek-ai/dsh-llm": "^0.2.2-rc.1",
33
+ "@deepseek-ai/schemastery": "~3.19.0"
34
+ },
35
+ "scripts": {
36
+ "test": "node test/net.test.mjs && node test/host.test.mjs",
37
+ "test:net": "node test/net.test.mjs",
38
+ "test:host": "node test/host.test.mjs"
39
+ },
40
+ "files": [
41
+ "src",
42
+ "client.js",
43
+ "cordis.patch.yml",
44
+ "README.md",
45
+ "CONTEXT.md",
46
+ "docs",
47
+ "LICENSE"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "tag": "dev"
52
+ },
53
+ "license": "MIT"
54
+ }