dsh-device-fleet 0.1.0-dev.1 → 0.1.0-dev.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/README.md +38 -0
- package/cordis.patch.yml +3 -1
- package/docs/notes/dev-release.md +36 -0
- package/package.json +45 -45
- package/src/host/store.js +4 -2
- package/src/host/tools.js +35 -0
- package/src/net/fleet-node.js +88 -1
- package/src/net/identity.js +12 -1
- package/src/plugin/index.js +31 -7
package/README.md
CHANGED
|
@@ -9,6 +9,44 @@ DeepSeek Harness 的设备联邦插件:把登录同一账号、且都安装本
|
|
|
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
|
- ⏳ Room / Room Board、委派、审批收件箱 UI、设备页/配对/房间 UI(下一步)
|
|
11
11
|
|
|
12
|
+
## 安装(开发版)
|
|
13
|
+
|
|
14
|
+
已发布到 npm:**`dsh-device-fleet@0.1.0-dev.2`**(dist-tag `dev`)。
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
# CLI / web profile
|
|
18
|
+
dsh plugin --profile web add dsh-device-fleet@dev
|
|
19
|
+
|
|
20
|
+
# 桌面 app:先完全退出 app,再安装,然后重新打开
|
|
21
|
+
dsh plugin --profile desktop add dsh-device-fleet@dev
|
|
22
|
+
|
|
23
|
+
# 离线安装包(拷到另一台设备)
|
|
24
|
+
dsh plugin --profile desktop add C:/path/to/dsh-device-fleet-0.1.0-dev.2.tgz
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
两台设备的 **DSH 版本与插件版本必须一致**(ADR-0024);不一致会在握手时直接拒绝。
|
|
28
|
+
|
|
29
|
+
### 配对(Fleet 入网)
|
|
30
|
+
|
|
31
|
+
1. 设备 A:对 agent 说“生成 Fleet 邀请码” → 模型调用 `fleet_invite`,拿到一次性邀请码(含 fleetKey,注意保密)。
|
|
32
|
+
2. 设备 B:安装并重启后,对 agent 说“加入 Fleet:`<code>`” → `fleet_join` 把邀请排队;**重启 DSH** 后生效。
|
|
33
|
+
3. 两台设备各自在 profile patch 里配置 `policy.allowedRoots` 与 `execution.cwd`(不配时直调读取一律被拒)。
|
|
34
|
+
4. 设备 B:`fleet_list` 看到设备 A → `fleet_read(device=A, path="A 允许根内的文件")` 验证。
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
# 两个 profile 的 cordis.patch.yml 里给 device-fleet 行追加 config:
|
|
38
|
+
- id: device-fleet
|
|
39
|
+
config:
|
|
40
|
+
deviceName: desktop
|
|
41
|
+
execution:
|
|
42
|
+
cwd: D:/work
|
|
43
|
+
agentPreset: standard
|
|
44
|
+
policy:
|
|
45
|
+
allowedRoots: [D:/work]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`fleet_run` 默认需要目标设备本机审批;审批收件箱 UI 尚未实现(当前 fail-closed)。第一阶段建议先用 `fleet_read` / `fleet_fetch` 验证链路。
|
|
49
|
+
|
|
12
50
|
## 设计文档
|
|
13
51
|
|
|
14
52
|
- 术语表:[CONTEXT.md](./CONTEXT.md)
|
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,36 @@
|
|
|
1
|
+
# 开发版发布记录
|
|
2
|
+
|
|
3
|
+
- 包名:`dsh-device-fleet`
|
|
4
|
+
- 版本:`0.1.0-dev.2`
|
|
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.2.tgz>
|
|
8
|
+
- 本地离线包:`dsh-device-fleet-0.1.0-dev.2.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.2`,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.2`(包含上述激活修复);`0.1.0-dev.1` 会激活失败,不要使用。
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
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.3",
|
|
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": ">=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 && 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
|
+
}
|
package/src/host/store.js
CHANGED
|
@@ -64,6 +64,7 @@ export function loadState({ dshHome, dataDir, name = 'device' } = {}) {
|
|
|
64
64
|
joinedAt: Date.now(),
|
|
65
65
|
accountFingerprint: decoded.accountFingerprint ?? null,
|
|
66
66
|
relay: decoded.relay ?? null,
|
|
67
|
+
endpoints: decoded.endpoints ?? [],
|
|
67
68
|
}
|
|
68
69
|
writeJsonAtomic(path.join(dir, 'fleet.json'), fleet)
|
|
69
70
|
writeJsonAtomic(path.join(dir, 'enrollment.json'), enrollment)
|
|
@@ -102,8 +103,8 @@ export function saveConfig(state, config) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
/** Create an invite for this device's fleet. */
|
|
105
|
-
export function createInvite(state, { accountFingerprint = null, relay = null } = {}) {
|
|
106
|
-
return encodeInvite({ fleet: state.fleet, accountFingerprint, relay })
|
|
106
|
+
export function createInvite(state, { accountFingerprint = null, relay = null, endpoints = [] } = {}) {
|
|
107
|
+
return encodeInvite({ fleet: state.fleet, accountFingerprint, relay, endpoints })
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/** Join a fleet from an invite code (overwrites fleet/enrollment, keeps identity). */
|
|
@@ -114,6 +115,7 @@ export function enrollFromInvite(state, code) {
|
|
|
114
115
|
joinedAt: Date.now(),
|
|
115
116
|
accountFingerprint: decoded.accountFingerprint ?? null,
|
|
116
117
|
relay: decoded.relay ?? null,
|
|
118
|
+
endpoints: decoded.endpoints ?? [],
|
|
117
119
|
}
|
|
118
120
|
saveFleet(state)
|
|
119
121
|
writeJsonAtomic(path.join(state.dataDir, 'enrollment.json'), state.enrollment)
|
package/src/host/tools.js
CHANGED
|
@@ -24,6 +24,22 @@ function fail(error) {
|
|
|
24
24
|
/** Pure handlers; wrapped into ToolDefinitions by `createFleetToolDefinitions`. */
|
|
25
25
|
export function createFleetHandlers({ node, client = new DirectCallClient({ node }), pairing = null }) {
|
|
26
26
|
return {
|
|
27
|
+
async fleet_status() {
|
|
28
|
+
if (typeof node.status !== 'function') return { text: 'Error: [unsupported] status is unavailable' }
|
|
29
|
+
return { text: JSON.stringify(node.status(), null, 2) }
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async fleet_connect({ host, port }) {
|
|
33
|
+
if (typeof node.connectAddress !== 'function') return { text: 'Error: [unsupported] manual connect is unavailable' }
|
|
34
|
+
try {
|
|
35
|
+
node.addPeerHint?.({ host, port })
|
|
36
|
+
const peer = await node.connectAddress(host, port)
|
|
37
|
+
return { text: `connected to ${peer.deviceId} (${peer.name ?? 'device'}) at ${host}:${port}` }
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return fail(error)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
27
43
|
async fleet_invite() {
|
|
28
44
|
if (!pairing?.invite) return { text: 'Error: [unsupported] this device cannot create an invite' }
|
|
29
45
|
try {
|
|
@@ -130,6 +146,25 @@ export function createFleetToolDefinitions({ defineTool, handlers }) {
|
|
|
130
146
|
const output = textOutput()
|
|
131
147
|
const commonDevice = { type: 'string', required: true, description: 'Target device id from `fleet_list`.' }
|
|
132
148
|
return [
|
|
149
|
+
defineTool({
|
|
150
|
+
name: 'fleet_status',
|
|
151
|
+
description:
|
|
152
|
+
'Show this device\'s Fleet network status for troubleshooting: device id, direct listener address/port, discovery, connected peers, roster and invite/manual address hints. Use it when discovery does not find the other device.',
|
|
153
|
+
parameters: {},
|
|
154
|
+
output,
|
|
155
|
+
execute: () => handlers.fleet_status(),
|
|
156
|
+
}),
|
|
157
|
+
defineTool({
|
|
158
|
+
name: 'fleet_connect',
|
|
159
|
+
description:
|
|
160
|
+
'Connect to another device by explicit host and port when LAN discovery is unavailable. Use it with the port printed by that device\'s `fleet_status`; the peer id is learned from the authenticated handshake.',
|
|
161
|
+
parameters: {
|
|
162
|
+
host: { type: 'string', required: true, description: 'Target host/IP address on the LAN or VPN.' },
|
|
163
|
+
port: { type: 'number', required: true, description: 'Target device\'s Fleet direct port (from its fleet_status).' },
|
|
164
|
+
},
|
|
165
|
+
output,
|
|
166
|
+
execute: (args) => handlers.fleet_connect(args),
|
|
167
|
+
}),
|
|
133
168
|
defineTool({
|
|
134
169
|
name: 'fleet_invite',
|
|
135
170
|
description:
|
package/src/net/fleet-node.js
CHANGED
|
@@ -45,6 +45,7 @@ export class FleetNode extends EventEmitter {
|
|
|
45
45
|
discovery = false,
|
|
46
46
|
relay = null,
|
|
47
47
|
roster = [],
|
|
48
|
+
peerHints = [],
|
|
48
49
|
trustOnFirstUse = true,
|
|
49
50
|
acceptRelayConnections = true,
|
|
50
51
|
connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
|
|
@@ -74,6 +75,10 @@ export class FleetNode extends EventEmitter {
|
|
|
74
75
|
this.roster = new Map()
|
|
75
76
|
for (const entry of roster) this.roster.set(entry.deviceId, { name: 'device', ...entry })
|
|
76
77
|
this.discovered = new Map()
|
|
78
|
+
this.peerHints = []
|
|
79
|
+
for (const hint of peerHints) this.addPeerHint(hint)
|
|
80
|
+
this.hintTimer = null
|
|
81
|
+
this.hintAttempt = 0
|
|
77
82
|
this.channels = new Map()
|
|
78
83
|
this.pendingRpc = new Map()
|
|
79
84
|
this.requestHandler = null
|
|
@@ -163,9 +168,81 @@ export class FleetNode extends EventEmitter {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
this.emit('started', { direct: this.directAddress ?? null })
|
|
171
|
+
this.scheduleHintConnects()
|
|
166
172
|
return this
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
/** Remember an address hint for a peer (from an invite or a manual `fleet_connect`). */
|
|
176
|
+
addPeerHint(hint) {
|
|
177
|
+
if (!hint || typeof hint.host !== 'string' || !Number.isInteger(hint.port) || hint.port <= 0) return
|
|
178
|
+
const entry = {
|
|
179
|
+
deviceId: typeof hint.deviceId === 'string' ? hint.deviceId : undefined,
|
|
180
|
+
host: hint.host,
|
|
181
|
+
port: hint.port,
|
|
182
|
+
}
|
|
183
|
+
if (entry.deviceId === this.identity.deviceId) return
|
|
184
|
+
const exists = this.peerHints.some(
|
|
185
|
+
(item) => item.host === entry.host && item.port === entry.port && item.deviceId === entry.deviceId,
|
|
186
|
+
)
|
|
187
|
+
if (!exists) this.peerHints.push(entry)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Connect to a raw address; learns the peer id from the handshake. */
|
|
191
|
+
async connectAddress(host, port, { expectedDeviceId = null, timeoutMs = this.connectTimeoutMs } = {}) {
|
|
192
|
+
if (!host || !Number.isInteger(port) || port <= 0) throw new Error('connectAddress needs host and port')
|
|
193
|
+
const expected = expectedDeviceId ? { deviceId: expectedDeviceId } : null
|
|
194
|
+
const socket = await connectDirect({ host, port, timeoutMs })
|
|
195
|
+
const channel = this.createInitiator(socket, expected)
|
|
196
|
+
await this.waitReady(channel, timeoutMs)
|
|
197
|
+
return channel.peer
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Retry the invite/manual hints in the background until one peer connects. */
|
|
201
|
+
scheduleHintConnects() {
|
|
202
|
+
if (this.hintTimer || this.closed) return
|
|
203
|
+
const attempt = async () => {
|
|
204
|
+
this.hintTimer = null
|
|
205
|
+
if (this.closed) return
|
|
206
|
+
if (this.peerHints.length === 0 || this.channels.size > 0) return
|
|
207
|
+
try {
|
|
208
|
+
for (const hint of this.peerHints) {
|
|
209
|
+
if (this.closed || this.channels.size > 0) return
|
|
210
|
+
try {
|
|
211
|
+
const peer = await this.connectAddress(hint.host, hint.port, { expectedDeviceId: hint.deviceId })
|
|
212
|
+
this.emit('hint-connected', { hint, peer })
|
|
213
|
+
return
|
|
214
|
+
} catch {
|
|
215
|
+
// try the next hint; the timer below retries
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
if (!this.closed && this.channels.size === 0) {
|
|
220
|
+
this.hintAttempt += 1
|
|
221
|
+
const delay = Math.min(10_000, 1000 * 2 ** Math.min(this.hintAttempt, 4))
|
|
222
|
+
this.hintTimer = setTimeout(() => void attempt(), delay)
|
|
223
|
+
this.hintTimer.unref?.()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.hintTimer = setTimeout(() => void attempt(), 1500)
|
|
228
|
+
this.hintTimer.unref?.()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
status() {
|
|
232
|
+
return {
|
|
233
|
+
deviceId: this.identity.deviceId,
|
|
234
|
+
name: this.name,
|
|
235
|
+
pluginVersion: this.pluginVersion,
|
|
236
|
+
dshVersion: this.dshVersion,
|
|
237
|
+
direct: this.directAddress ? { host: this.directAddress.host, port: this.directAddress.port } : null,
|
|
238
|
+
relay: this.relayClient ? { online: this.relayClient.online === true } : null,
|
|
239
|
+
discovery: this.discovery ? { enabled: true, peers: [...this.discovered.keys()] } : { enabled: false, peers: [] },
|
|
240
|
+
roster: this.rosterEntries().map((entry) => ({ deviceId: entry.deviceId, name: entry.name ?? 'device' })),
|
|
241
|
+
connected: this.peerIds(),
|
|
242
|
+
peerHints: [...this.peerHints],
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
169
246
|
async acceptIncomingRelay(controller) {
|
|
170
247
|
if (!this.acceptRelayConnections) {
|
|
171
248
|
controller.reject()
|
|
@@ -238,7 +315,15 @@ export class FleetNode extends EventEmitter {
|
|
|
238
315
|
|
|
239
316
|
const expected = this.roster.get(deviceId) ?? { deviceId }
|
|
240
317
|
const discovered = this.discovered.get(deviceId)
|
|
241
|
-
const
|
|
318
|
+
const hinted = this.peerHints.find((entry) => entry.deviceId === deviceId)
|
|
319
|
+
const endpoint =
|
|
320
|
+
host && port
|
|
321
|
+
? { host, port }
|
|
322
|
+
: discovered
|
|
323
|
+
? { host: discovered.host, port: discovered.port }
|
|
324
|
+
: hinted
|
|
325
|
+
? { host: hinted.host, port: hinted.port }
|
|
326
|
+
: null
|
|
242
327
|
let directError = null
|
|
243
328
|
if (endpoint) {
|
|
244
329
|
try {
|
|
@@ -398,6 +483,8 @@ export class FleetNode extends EventEmitter {
|
|
|
398
483
|
|
|
399
484
|
async close() {
|
|
400
485
|
this.closed = true
|
|
486
|
+
clearTimeout(this.hintTimer)
|
|
487
|
+
this.hintTimer = null
|
|
401
488
|
for (const channel of this.channels.values()) channel.close()
|
|
402
489
|
this.channels.clear()
|
|
403
490
|
for (const [id, pending] of this.pendingRpc) {
|
package/src/net/identity.js
CHANGED
|
@@ -70,7 +70,7 @@ export function createFleet({ name = 'fleet' } = {}) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function encodeInvite({ fleet, accountFingerprint = null, relay = null }) {
|
|
73
|
+
export function encodeInvite({ fleet, accountFingerprint = null, relay = null, endpoints = [] }) {
|
|
74
74
|
const payload = {
|
|
75
75
|
v: 1,
|
|
76
76
|
fleetId: fleet.fleetId,
|
|
@@ -78,6 +78,7 @@ export function encodeInvite({ fleet, accountFingerprint = null, relay = null })
|
|
|
78
78
|
name: fleet.name,
|
|
79
79
|
accountFingerprint,
|
|
80
80
|
relay,
|
|
81
|
+
endpoints: Array.isArray(endpoints) ? endpoints : [],
|
|
81
82
|
createdAt: Date.now(),
|
|
82
83
|
}
|
|
83
84
|
return INVITE_PREFIX + Buffer.from(JSON.stringify(payload)).toString('base64url')
|
|
@@ -99,6 +100,16 @@ export function decodeInvite(code) {
|
|
|
99
100
|
fleet: { fleetId: payload.fleetId, fleetKey: payload.fleetKey, name: payload.name ?? 'fleet' },
|
|
100
101
|
accountFingerprint: payload.accountFingerprint ?? null,
|
|
101
102
|
relay: payload.relay ?? null,
|
|
103
|
+
endpoints: Array.isArray(payload.endpoints)
|
|
104
|
+
? payload.endpoints.filter(
|
|
105
|
+
(entry) =>
|
|
106
|
+
entry &&
|
|
107
|
+
typeof entry.host === 'string' &&
|
|
108
|
+
Number.isInteger(entry.port) &&
|
|
109
|
+
entry.port > 0 &&
|
|
110
|
+
(entry.deviceId === undefined || typeof entry.deviceId === 'string'),
|
|
111
|
+
)
|
|
112
|
+
: [],
|
|
102
113
|
createdAt: payload.createdAt ?? null,
|
|
103
114
|
}
|
|
104
115
|
}
|
package/src/plugin/index.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// land now (a prompt section and a first-use manual + orchestration nudge).
|
|
6
6
|
// Room, Room Board, delegation and the UI land in later steps.
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
|
+
import { networkInterfaces } from 'node:os'
|
|
8
9
|
import { fileURLToPath } from 'node:url'
|
|
9
10
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
10
11
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
11
|
-
import
|
|
12
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
12
13
|
import { FleetNode } from '../net/fleet-node.js'
|
|
13
14
|
import { accountFingerprint } from '../net/identity.js'
|
|
14
15
|
import { DirectCallClient, DirectCallHost } from '../host/direct-call.js'
|
|
@@ -115,7 +116,16 @@ function withManual(definitions) {
|
|
|
115
116
|
export function apply(ctx, config) {
|
|
116
117
|
const pluginVersion = packageVersion()
|
|
117
118
|
const dshVersion = process.env.DSH_CLIENT_VERSION ?? 'unknown'
|
|
118
|
-
|
|
119
|
+
// Optional services: Cordis throws on direct property access for services the
|
|
120
|
+
// plugin did not inject, so every optional service goes through get().
|
|
121
|
+
const service = (name) => {
|
|
122
|
+
try {
|
|
123
|
+
return ctx.get?.(name)
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const logger = service('logger') ?? console
|
|
119
129
|
|
|
120
130
|
let state
|
|
121
131
|
try {
|
|
@@ -134,14 +144,15 @@ export function apply(ctx, config) {
|
|
|
134
144
|
discovery: config.discovery ? true : false,
|
|
135
145
|
relay: config.relay ?? state.enrollment?.relay ?? null,
|
|
136
146
|
roster: state.roster,
|
|
147
|
+
peerHints: state.enrollment?.endpoints ?? [],
|
|
137
148
|
pluginVersion,
|
|
138
149
|
dshVersion,
|
|
139
150
|
capabilities: CAPABILITIES,
|
|
140
151
|
})
|
|
141
152
|
|
|
142
153
|
const policy = new ToolPolicy(config.policy)
|
|
143
|
-
const sessionController =
|
|
144
|
-
const fs =
|
|
154
|
+
const sessionController = service('sessionController')
|
|
155
|
+
const fs = service('fs')
|
|
145
156
|
if (!sessionController) logger.warn?.('[device-fleet] sessionController unavailable: direct calls to stateful tools will fail')
|
|
146
157
|
const slot = sessionController
|
|
147
158
|
? new ExecutionSlot({ sessionController, tools: ctx.tools, config: config.execution })
|
|
@@ -186,7 +197,7 @@ export function apply(ctx, config) {
|
|
|
186
197
|
})
|
|
187
198
|
|
|
188
199
|
// Bind the local account fingerprint when the account service is composed.
|
|
189
|
-
const account =
|
|
200
|
+
const account = service('deepseekAccount')
|
|
190
201
|
if (account && typeof account.getDeviceIdentity === 'function') {
|
|
191
202
|
void account
|
|
192
203
|
.getDeviceIdentity()
|
|
@@ -204,6 +215,18 @@ export function apply(ctx, config) {
|
|
|
204
215
|
}
|
|
205
216
|
|
|
206
217
|
const client = new DirectCallClient({ node })
|
|
218
|
+
const localEndpoints = () => {
|
|
219
|
+
const port = node.directAddress?.port
|
|
220
|
+
if (!Number.isInteger(port) || port <= 0) return []
|
|
221
|
+
const endpoints = []
|
|
222
|
+
for (const entries of Object.values(networkInterfaces())) {
|
|
223
|
+
for (const entry of entries ?? []) {
|
|
224
|
+
if (entry.family !== 'IPv4' || entry.internal) continue
|
|
225
|
+
endpoints.push({ deviceId: state.identity.deviceId, host: entry.address, port })
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return endpoints
|
|
229
|
+
}
|
|
207
230
|
const pairing = {
|
|
208
231
|
async invite() {
|
|
209
232
|
const fingerprint =
|
|
@@ -213,6 +236,7 @@ export function apply(ctx, config) {
|
|
|
213
236
|
return createInvite(state, {
|
|
214
237
|
accountFingerprint: fingerprint,
|
|
215
238
|
relay: config.relay ?? state.enrollment?.relay ?? null,
|
|
239
|
+
endpoints: localEndpoints(),
|
|
216
240
|
})
|
|
217
241
|
},
|
|
218
242
|
async join(code) {
|
|
@@ -225,7 +249,7 @@ export function apply(ctx, config) {
|
|
|
225
249
|
for (const definition of wrapped) ctx.tools.register(definition)
|
|
226
250
|
|
|
227
251
|
// Adoption layer 2: one sentence, only when the Fleet has members.
|
|
228
|
-
const systemPrompt =
|
|
252
|
+
const systemPrompt = service('systemPrompt')
|
|
229
253
|
if (systemPrompt && config.promptSection !== false) {
|
|
230
254
|
const order = systemPrompt.getSectionOrder?.('TOOL_SUBAGENT') ?? PROMPT_SECTION_ORDER
|
|
231
255
|
ctx.effect(() =>
|
|
@@ -271,6 +295,6 @@ export function apply(ctx, config) {
|
|
|
271
295
|
void node.close()
|
|
272
296
|
})
|
|
273
297
|
|
|
274
|
-
|
|
298
|
+
logger.info?.(`[device-fleet] v${pluginVersion} device ${state.identity.deviceId} (${config.deviceName})`)
|
|
275
299
|
void ready
|
|
276
300
|
}
|