dsh-device-fleet 0.1.0-dev.1 → 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/README.md +41 -1
- package/client.js +167 -0
- package/cordis.patch.yml +3 -1
- package/docs/notes/approval-inbox-phase.md +31 -0
- package/docs/notes/dev-release.md +67 -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/package.json +54 -45
- 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 +27 -8
- package/src/host/execution-slot.js +19 -5
- package/src/host/index.js +4 -0
- package/src/host/room-service.js +324 -0
- package/src/host/store.js +31 -3
- package/src/host/tool-policy.js +2 -1
- package/src/host/tools.js +376 -1
- package/src/net/fleet-node.js +96 -1
- package/src/net/identity.js +12 -1
- package/src/plugin/index.js +121 -17
package/README.md
CHANGED
|
@@ -7,7 +7,47 @@ 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
|
-
-
|
|
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` 验证链路。
|
|
11
51
|
|
|
12
52
|
## 设计文档
|
|
13
53
|
|
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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-device-fleet bundle patch
|
|
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,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.9(稳定化)
|
|
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.9(审批收件箱,逻辑层)
|
|
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.9(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.9(审批面板 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-device-fleet",
|
|
3
|
+
"version": "0.1.0-dev.10",
|
|
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
|
+
}
|