dsh-lan-relay 1.0.2 → 1.1.0

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
@@ -192,8 +192,7 @@ dsh plugin --profile desktop add dsh-lan-relay
192
192
  ## 11. 开发
193
193
 
194
194
  ```powershell
195
- npm test # 31 项端到端 smoke + 9 项 node:test(hub 内核 / 插件集成 / Web 半结构)
196
- npm run test:unit # 只跑 node:test
195
+ npm test # 9 项 node:test:hub 内核 / 插件集成(含真实 hub + 唤醒投递)/ Web 半结构
197
196
  ```
198
197
 
199
198
  | 路径 | 作用 |
@@ -1,7 +1,7 @@
1
1
  # 两台机器,5 分钟装好(1.0 插件版)
2
2
 
3
3
  前置:两台机器都装 **DSH Desktop(≥ 0.2.1-rc.2)**,在同一局域网,能互相 ping 通。
4
- 不需要预装 Node、不需要复制文件夹、不需要 MCP 连接器、不需要计划任务。
4
+ 不需要预装 Node、不需要复制文件夹、不需要额外配置、不需要计划任务。
5
5
 
6
6
  > 假设:机器 A = `pc1` 当服务端;机器 B = `pc2` 当客户端。名字可以在设置页改。
7
7
 
@@ -20,7 +20,7 @@
20
20
  验证:
21
21
 
22
22
  - 页面「Hub 状态:运行中」,消息数/最新序号正常;
23
- - 同一台机器上 `node relay-cli.mjs status` 显示 🟢(可选,需要仓库里的 CLI)。
23
+ - 页面「在线 peer」里能看到本机(刚启动时可能只显示自己)。
24
24
 
25
25
  ---
26
26
 
@@ -59,4 +59,3 @@
59
59
 
60
60
  - 关闭:**运行角色 → 关闭 → 保存**(hub 与轮询停止,工具仍在但会提示未启用)。
61
61
  - 卸载:**插件 → dsh-lan-relay → 卸载**。配置文件 `~/.dsh/lan-relay/config.json` 会保留,重新装回来即恢复。
62
- - 回退到 0.4.x 旧链路:见 [MIGRATION-0.4-to-1.0.md](MIGRATION-0.4-to-1.0.md)(反向操作)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lan-relay",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "DeepSeek Harness 局域网中继插件:一台机器在 DSH 进程内做 hub,其余机器粘贴加入码即可加入;跨机器 Agent 消息互通、离线补投、自动唤醒会话,标准 DSH bundle,装完即用。",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -11,9 +11,6 @@
11
11
  "./locale/*.json": "./locale/*.json",
12
12
  "./icon": "./icon.svg"
13
13
  },
14
- "bin": {
15
- "dsh-lan-relay": "./relay-cli.mjs"
16
- },
17
14
  "files": [
18
15
  "index.js",
19
16
  "client.js",
@@ -22,12 +19,7 @@
22
19
  "cordis.patch.yml",
23
20
  "icon.svg",
24
21
  "hub.mjs",
25
- "mcp-server.mjs",
26
- "relay-cli.mjs",
27
- "start-hub.cmd",
28
22
  "scripts/allow-firewall.ps1",
29
- "scripts/setup.ps1",
30
- "scripts/install-autostart.ps1",
31
23
  "examples",
32
24
  "docs",
33
25
  "README.md",
@@ -50,13 +42,7 @@
50
42
  },
51
43
  "scripts": {
52
44
  "hub": "node hub.mjs",
53
- "mcp": "node mcp-server.mjs",
54
- "cli": "node relay-cli.mjs",
55
- "test": "node test/smoke.mjs && node --test test/hub-core.test.mjs test/plugin.test.mjs test/client.test.mjs",
56
- "test:unit": "node --test test/hub-core.test.mjs test/plugin.test.mjs test/client.test.mjs",
57
- "test:smoke": "node test/smoke.mjs",
58
- "setup:hub": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/setup.ps1 -Role hub",
59
- "setup:peer": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/setup.ps1 -Role peer"
45
+ "test": "node --test test/hub-core.test.mjs test/plugin.test.mjs test/client.test.mjs"
60
46
  },
61
47
  "engines": {
62
48
  "node": ">=18"
@@ -71,9 +57,7 @@
71
57
  "message-bus",
72
58
  "multi-agent",
73
59
  "agent-to-agent",
74
- "wake-on-message",
75
- "mcp",
76
- "mcp-server"
60
+ "wake-on-message"
77
61
  ],
78
62
  "repository": {
79
63
  "type": "git",
@@ -1,80 +0,0 @@
1
- # 从 0.4.x(push 插件 + MCP 连接器)迁移到 1.0 单包插件
2
-
3
- 1.0 起,**一个 bundle 包做完所有事**:
4
-
5
- | 旧组件 | 1.0 里还需要吗 | 说明 |
6
- |---|---|---|
7
- | `dsh-lan-relay-push` bundle | ❌ 卸载/关闭 | 功能已合并进 `dsh-lan-relay`;同时开会重复唤醒 |
8
- | `lan-relay` MCP 连接(`mcp-server.mjs`) | ❌ 删除连接 | 6 个工具已由插件原生注册(`lan_relay_*`) |
9
- | 独立 `hub.mjs` 进程 | ❌ 不用启动 | 服务端角色在 DSH 进程内起 hub |
10
- | `scripts/install-autostart.ps1` 装的自启 | ❌ 删除 | hub 跟随 DSH 启动 |
11
- | `scripts/setup.ps1` 写配置 | ❌ 不需要 | 设置页写同一份 `config.json` |
12
- | `~/.dsh/lan-relay/config.json` | ✅ 保留 | 字段兼容,多了 `role` |
13
-
14
- 消息协议、`messages.jsonl`、游标文件都没变,所以新旧客户端可以混用(升级过程中不会丢消息)。
15
-
16
- ---
17
-
18
- ## 步骤
19
-
20
- ### 1. 安装新插件
21
-
22
- GUI:**插件 → 添加插件** → `dsh-lan-relay`(npm)或本地目录 / Git 地址。
23
- CLI:
24
-
25
- ```powershell
26
- dsh plugin --profile desktop add "D:\path\to\dsh-lan-relay"
27
- ```
28
-
29
- ### 2. 卸载旧 push 插件
30
-
31
- GUI:**插件 → 已安装插件 → dsh-lan-relay-push → 关闭/卸载**。
32
- CLI:
33
-
34
- ```powershell
35
- dsh plugin --profile desktop remove dsh-lan-relay-push
36
- ```
37
-
38
- > 旧插件用的是独立游标 `state/push-<self>.json`,新插件也用它,所以升级后不会重放历史消息。
39
-
40
- ### 3. 删除旧 MCP 连接
41
-
42
- GUI:**MCP 连接器 → lan-relay → 删除**。
43
- 或者让 Agent 执行 `mcp_connector_disconnect`(连接 key 形如 `json-lan-relay`)。
44
- 删除后旧的 `mcp__lan_relay__relay_*` 工具消失,改用插件原生的 `lan_relay_*`。
45
-
46
- ### 4. 清理开机自启
47
-
48
- 如果之前给独立 hub 装过自启:
49
-
50
- ```powershell
51
- powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-autostart.ps1 -Remove
52
- ```
53
-
54
- 会删除计划任务 `DSH-LAN-Relay-Hub`、启动文件夹快捷方式、`HKCU\...\Run` 键和隐藏启动器;已运行的独立 hub 不受影响(需要的话手动停掉)。
55
-
56
- ### 5. 确认角色与连通性
57
-
58
- 打开 **设置 → 局域网中继**:
59
-
60
- - 旧配置里已经有 `token` + `relay_url`,插件会自动推断为**客户端**,直接可用(顶部应显示「客户端已连接」)。
61
- - 原来的 hub 机器:切到**服务端** → 保存。插件会复用旧 token、在进程内起 hub,并给出新的**加入码**(地址取本机局域网 IP)。
62
- - 其他机器如果换了 token 或地址,粘贴新加入码即可。
63
-
64
- ### 6. 验证
65
-
66
- ```powershell
67
- node relay-cli.mjs status # 🟢 + 可达
68
- node relay-cli.mjs peers
69
- ```
70
-
71
- 或在设置页看「在线 peer」。再让 Agent 用 `lan_relay_status` 确认角色、hub、唤醒统计。
72
-
73
- ---
74
-
75
- ## 常见问题
76
-
77
- - **升级后同一条消息投递两次**:旧 push 插件没关。关掉它即可。
78
- - **服务端保存后报「端口已被占用」**:旧的独立 `hub.mjs` 还在跑。停掉它(或先把它在用的 8787 让出来),再保存一次。
79
- - **对端连不上新 hub**:加入码里的地址可能是多网卡里选错的那个;在服务端「本机地址」里换一个,重新复制加入码。
80
- - **回退**:删掉新 bundle,重新装回 `dsh-lan-relay-push` 并恢复 MCP 连接即可;配置文件一直兼容。
@@ -1,13 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "lan-relay": {
4
- "command": "node",
5
- "args": [
6
- "F:\\work\\dsh\\dsh_workspace\\dsh-lan-relay\\mcp-server.mjs"
7
- ],
8
- "env": {
9
- "LAN_RELAY_CONFIG": "C:\\Users\\you\\.dsh\\lan-relay\\config.json"
10
- }
11
- }
12
- }
13
- }
@@ -1,59 +0,0 @@
1
- ---
2
- name: lan-relay
3
- description: 在没有 MCP 连接器(或 MCP 工具未启用)的机器上,用命令行与局域网内另一台 DSH 互发消息时使用。发送、收件、等消息、查看在线 peer、排障。
4
- ---
5
-
6
- # 局域网 Agent 通信(LAN Relay CLI)
7
-
8
- 通过命令行调用本机的 `dsh-lan-relay` 项目,与另一台机器上的 DSH 互发消息。
9
- 所有命令都读取 `~/.dsh/lan-relay/config.json`(relay_url / token / self)。
10
-
11
- 项目目录(按实际部署改):`F:\work\dsh\dsh_workspace\dsh-lan-relay`
12
-
13
- ## 1. 先确认连通
14
-
15
- ```bash
16
- node "F:/work/dsh/dsh_workspace/dsh-lan-relay/relay-cli.mjs" status
17
- ```
18
-
19
- - `可达=是` 说明 hub 正常;`可达=否` 看输出里的错误与提示(hub 没开 / IP 不对 / 防火墙 / token 不一致)。
20
-
21
- ## 2. 发消息
22
-
23
- ```bash
24
- node "F:/work/dsh/dsh_workspace/dsh-lan-relay/relay-cli.mjs" send --to pc2 --text "帮我看下 X,回我 Y"
25
- ```
26
-
27
- - `--to` 填对方 peer 名;`--to "*"` 广播给所有其他 peer。
28
- - 对方离线也会入队,上线后能补收。
29
-
30
- ## 3. 收消息
31
-
32
- ```bash
33
- node "F:/work/dsh/dsh_workspace/dsh-lan-relay/relay-cli.mjs" inbox --limit 10
34
- ```
35
-
36
- 同一条消息只会出现一次(本地游标);想重读历史加 `--from-seq 0 --no-mark-read`。
37
-
38
- ## 4. 等消息(长轮询)
39
-
40
- ```bash
41
- node "F:/work/dsh/dsh_workspace/dsh-lan-relay/relay-cli.mjs" wait --timeout 25
42
- ```
43
-
44
- - 最多阻塞 25~50 秒,有新消息立即返回;超时返回空。
45
- - 循环调用会持续消耗模型调用,建议只在明确的"等对方回复"场景用。
46
-
47
- ## 5. 查看在线 peer
48
-
49
- ```bash
50
- node "F:/work/dsh/dsh_workspace/dsh-lan-relay/relay-cli.mjs" peers
51
- ```
52
-
53
- 120 秒内有过请求的 peer 视为在线。
54
-
55
- ## 6. 给 Agent 的使用约定
56
-
57
- - 消息里写清:背景、你要对方做什么、期望的回复格式、是否需要 `relay_wait` 继续等。
58
- - 对方是另一个 Agent,不要假设它有你的上下文。
59
- - 任务结束用一句话总结回执,避免两边无止境互相等待。
package/mcp-server.mjs DELETED
@@ -1,284 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * LAN Relay MCP Server —— 把局域网消息总线暴露成 Agent 工具(stdio / JSON-RPC)
4
- *
5
- * 由 DSH 的 MCP 客户端按需拉起,进程生命周期跟随 DSH,不需要手动启动。
6
- * 配置全部来自 ~/.dsh/lan-relay/config.json(或 LAN_RELAY_* 环境变量)。
7
- *
8
- * 工具:
9
- * relay_send 给另一台机器上的 Agent 发消息
10
- * relay_inbox 取自己的新消息(推进游标,同一条只返回一次)
11
- * relay_wait 长轮询等新消息(≤50 秒,避免超出 MCP 工具调用超时)
12
- * relay_peers 看谁在线
13
- * relay_status 看自己的配置与连通性
14
- */
15
- import readline from 'node:readline';
16
- import { loadConfig, RelayError } from './lib/config.mjs';
17
- import { RelayClient } from './lib/relay-client.mjs';
18
-
19
- const VERSION = '0.1.0';
20
- const SERVER_INFO = { name: 'dsh-lan-relay', version: VERSION };
21
-
22
- let client;
23
- let configError = null;
24
- try {
25
- client = new RelayClient(loadConfig());
26
- } catch (error) {
27
- configError = error;
28
- console.error(`[lan-relay] 配置错误:${error.message}`);
29
- }
30
-
31
- function textResult(payload) {
32
- return {
33
- content: [{ type: 'text', text: typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2) }],
34
- };
35
- }
36
-
37
- function errorResult(error) {
38
- const detail = error instanceof RelayError
39
- ? { error: error.message, hint: error.hint || undefined }
40
- : { error: String(error?.message || error) };
41
- return {
42
- content: [{ type: 'text', text: JSON.stringify(detail, null, 2) }],
43
- isError: true,
44
- };
45
- }
46
-
47
- function requireClient() {
48
- if (client) return client;
49
- throw new RelayError(
50
- `LAN relay 配置不可用:${configError?.message || '未知原因'}`,
51
- { hint: '运行 scripts/setup.ps1 生成 ~/.dsh/lan-relay/config.json,然后重启 DSH 或重连 MCP。' },
52
- );
53
- }
54
-
55
- function decorateMessages(result) {
56
- const now = Date.now();
57
- return {
58
- ...result,
59
- messages: result.messages.map((message) => ({
60
- seq: message.seq,
61
- from: message.from,
62
- from_session: message.from_session || '',
63
- to: message.to,
64
- to_session: message.to_session || '',
65
- thread: message.thread || '',
66
- text: message.text,
67
- sent_at: new Date(message.ts).toISOString(),
68
- age_seconds: Math.max(0, Math.round((now - message.ts) / 1000)),
69
- })),
70
- };
71
- }
72
-
73
- const TOOLS = [
74
- {
75
- name: 'relay_send',
76
- description:
77
- '给局域网内另一台机器上的 DSH Agent 发一条消息。to 填对方 peer 名(如 "pc1" / "b"),'
78
- + '或填 "*" 广播给所有其他 peer。to_session 可指定对方某个会话(如 session-a37a161f-...),'
79
- + '不填则由对方的 relay 插件投递到它配置的默认会话。消息会落盘在中继 hub 上,对方离线也会在下次上线时收到。'
80
- + '发送成功返回 seq,可用于引用/排障。',
81
- inputSchema: {
82
- type: 'object',
83
- properties: {
84
- to: { type: 'string', description: '收件 peer 名(小写字母/数字/._-,≤32 字符),或 "*" 表示广播' },
85
- text: { type: 'string', description: '消息正文(≤32000 字符,建议写清背景与期望回复,因为对方是另一个 Agent)' },
86
- thread: { type: 'string', description: '可选话题标签,方便双方按 thread 归档(≤64 字符)' },
87
- to_session: { type: 'string', description: '可选:对方会话 id(如 session-a37a161f-...),用于精确投递/唤醒' },
88
- from_session: { type: 'string', description: '可选:你的会话 id,便于对方回复到同一会话' },
89
- },
90
- required: ['to', 'text'],
91
- additionalProperties: false,
92
- },
93
- },
94
- {
95
- name: 'relay_inbox',
96
- description:
97
- '读取发给自己的新消息。默认会推进本地游标,所以同一条消息不会重复返回;'
98
- + '若想重读历史,可显式传 from_seq=0。返回 messages 数组(含 from/text/seq/age_seconds)和 has_more。',
99
- inputSchema: {
100
- type: 'object',
101
- properties: {
102
- limit: { type: 'integer', minimum: 1, maximum: 100, description: '本次最多取多少条,默认 20' },
103
- from_seq: { type: 'integer', minimum: 0, description: '可选:从这个 seq 之后开始读(不传则用本地游标)' },
104
- mark_read: { type: 'boolean', description: '是否推进游标,默认 true;调试时可传 false' },
105
- },
106
- additionalProperties: false,
107
- },
108
- },
109
- {
110
- name: 'relay_wait',
111
- description:
112
- '长轮询等待新消息:没有消息时会阻塞到超时(最多 50 秒),有新消息立即返回。'
113
- + '典型用法:对方说"等我 30 秒",你就调用 relay_wait(timeout_seconds=30);'
114
- + '想持续在线可以循环调用,但每轮都会消耗一次模型调用。',
115
- inputSchema: {
116
- type: 'object',
117
- properties: {
118
- timeout_seconds: { type: 'number', minimum: 1, maximum: 50, description: '等待秒数,默认 25,上限 50' },
119
- after_seq: { type: 'integer', minimum: 0, description: '可选:从这个 seq 之后等(不传则用本地游标)' },
120
- },
121
- additionalProperties: false,
122
- },
123
- },
124
- {
125
- name: 'relay_peers',
126
- description: '查看同一中继上还有哪些 peer,以及各自是否在线(120 秒内有请求即视为在线)。',
127
- inputSchema: { type: 'object', properties: {}, additionalProperties: false },
128
- },
129
- {
130
- name: 'relay_status',
131
- description:
132
- '查看本机 relay 配置(self、relay_url、游标)与 hub 连通性。首次使用或通信失败时先调它排障。',
133
- inputSchema: { type: 'object', properties: {}, additionalProperties: false },
134
- },
135
- ];
136
-
137
- const HANDLERS = {
138
- relay_send: async (args) => {
139
- const result = await requireClient().send({
140
- to: args?.to,
141
- text: args?.text,
142
- thread: args?.thread || '',
143
- toSession: args?.to_session || '',
144
- fromSession: args?.from_session || '',
145
- });
146
- return {
147
- ok: true,
148
- seq: result.message.seq,
149
- from: result.message.from,
150
- from_session: result.message.from_session || '',
151
- to: result.message.to,
152
- to_session: result.message.to_session || '',
153
- sent_at: new Date(result.message.ts).toISOString(),
154
- latest: result.latest,
155
- hub: result.hub,
156
- };
157
- },
158
- relay_inbox: async (args) => {
159
- const result = await requireClient().inbox({
160
- limit: Number.isInteger(args?.limit) ? args.limit : 20,
161
- fromSeq: Number.isInteger(args?.from_seq) ? args.from_seq : null,
162
- markRead: args?.mark_read !== false,
163
- });
164
- return decorateMessages(result);
165
- },
166
- relay_wait: async (args) => {
167
- const result = await requireClient().wait({
168
- timeoutSeconds: Number.isFinite(args?.timeout_seconds) ? args.timeout_seconds : 25,
169
- afterSeq: Number.isInteger(args?.after_seq) ? args.after_seq : null,
170
- });
171
- return decorateMessages(result);
172
- },
173
- relay_peers: async () => {
174
- const peers = await requireClient().peers();
175
- return {
176
- self: requireClient().self,
177
- relay_url: requireClient().relayUrl,
178
- peers: peers.peers || [],
179
- online_window_seconds: peers.online_window_seconds,
180
- latest: peers.latest,
181
- };
182
- },
183
- relay_status: async () => {
184
- const instance = requireClient();
185
- return instance.status();
186
- },
187
- };
188
-
189
- // ---------- MCP stdio 传输(换行分隔的 JSON-RPC 2.0) ----------
190
- function writeMessage(payload) {
191
- process.stdout.write(`${JSON.stringify(payload)}\n`);
192
- }
193
-
194
- function ok(id, result) {
195
- return { jsonrpc: '2.0', id, result };
196
- }
197
-
198
- function fail(id, code, message, data) {
199
- return { jsonrpc: '2.0', id, error: data === undefined ? { code, message } : { code, message, data } };
200
- }
201
-
202
- const SUPPORTED_PROTOCOLS = ['2024-11-05', '2025-03-26', '2025-06-18'];
203
-
204
- async function handleRequest(message) {
205
- const { id, method, params } = message;
206
- const isNotification = id === undefined || id === null;
207
-
208
- switch (method) {
209
- case 'initialize': {
210
- const requested = params?.protocolVersion;
211
- const protocolVersion = SUPPORTED_PROTOCOLS.includes(requested) ? requested : '2024-11-05';
212
- return ok(id, {
213
- protocolVersion,
214
- capabilities: { tools: { listChanged: false } },
215
- serverInfo: SERVER_INFO,
216
- instructions:
217
- '这是两台机器之间的局域网通信通道。用 relay_send 给对面发消息,用 relay_inbox / relay_wait 收消息。'
218
- + '对方是另一个 DSH Agent,消息要写清楚背景、你要什么、期望的回复格式。',
219
- });
220
- }
221
- case 'notifications/initialized':
222
- case 'notifications/cancelled':
223
- case 'notifications/roots/list_changed':
224
- return undefined;
225
- case 'ping':
226
- return ok(id, {});
227
- case 'tools/list':
228
- return ok(id, { tools: TOOLS });
229
- case 'tools/call': {
230
- const name = params?.name;
231
- const handler = HANDLERS[name];
232
- if (!handler) return fail(id, -32602, `未知工具:${name}`);
233
- try {
234
- const payload = await handler(params?.arguments || {});
235
- return ok(id, textResult(payload));
236
- } catch (error) {
237
- console.error(`[lan-relay] 工具 ${name} 失败:${error?.message || error}`);
238
- return ok(id, errorResult(error));
239
- }
240
- }
241
- case 'resources/list':
242
- return ok(id, { resources: [] });
243
- case 'prompts/list':
244
- return ok(id, { prompts: [] });
245
- default:
246
- if (isNotification) return undefined;
247
- return fail(id, -32601, `未实现的方法:${method}`);
248
- }
249
- }
250
-
251
- const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
252
-
253
- rl.on('line', (line) => {
254
- const trimmed = line.trim();
255
- if (!trimmed) return;
256
- let message;
257
- try {
258
- message = JSON.parse(trimmed);
259
- } catch {
260
- console.error(`[lan-relay] 收到非 JSON 行:${trimmed.slice(0, 200)}`);
261
- return;
262
- }
263
- Promise.resolve()
264
- .then(() => handleRequest(message))
265
- .then((response) => {
266
- if (response) writeMessage(response);
267
- })
268
- .catch((error) => {
269
- console.error(`[lan-relay] 处理请求异常:${error?.stack || error}`);
270
- if (message?.id !== undefined) {
271
- writeMessage(fail(message.id, -32603, String(error?.message || error)));
272
- }
273
- });
274
- });
275
-
276
- rl.on('close', () => process.exit(0));
277
- process.on('uncaughtException', (error) => {
278
- console.error(`[lan-relay] uncaught: ${error?.stack || error}`);
279
- });
280
- process.on('unhandledRejection', (error) => {
281
- console.error(`[lan-relay] unhandledRejection: ${error?.stack || error}`);
282
- });
283
-
284
- console.error(`[lan-relay] MCP server v${VERSION} 已启动(self=${client?.self || '?'},relay=${client?.relayUrl || '未配置'})`);
package/relay-cli.mjs DELETED
@@ -1,169 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * LAN Relay CLI —— 不开 DSH 也能收发/排障(也用于本插件自测)
4
- *
5
- * node relay-cli.mjs status
6
- * node relay-cli.mjs send --to pc2 --text "你好,帮我看下 X"
7
- * node relay-cli.mjs inbox --limit 10
8
- * node relay-cli.mjs wait --timeout 30
9
- * node relay-cli.mjs peers
10
- *
11
- * 通用参数:--config <文件> --url <http://ip:port> --token <token> --self <peer名> --json
12
- */
13
- import { spawn } from 'node:child_process';
14
- import path from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
- import { loadConfig, RelayError } from './lib/config.mjs';
17
- import { RelayClient } from './lib/relay-client.mjs';
18
-
19
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
-
21
- function parseArgs(argv) {
22
- const out = { _: [] };
23
- for (let i = 0; i < argv.length; i += 1) {
24
- const arg = argv[i];
25
- if (!arg.startsWith('--')) {
26
- out._.push(arg);
27
- continue;
28
- }
29
- const eq = arg.indexOf('=');
30
- if (eq > 2) {
31
- out[arg.slice(2, eq)] = arg.slice(eq + 1);
32
- continue;
33
- }
34
- const key = arg.slice(2);
35
- const next = argv[i + 1];
36
- if (next !== undefined && !next.startsWith('--')) {
37
- out[key] = next;
38
- i += 1;
39
- } else {
40
- out[key] = true;
41
- }
42
- }
43
- return out;
44
- }
45
-
46
- function fmtTime(iso) {
47
- if (!iso) return '(未知时间)';
48
- const d = new Date(iso);
49
- return Number.isNaN(d.getTime()) ? iso : d.toLocaleString('zh-CN', { hour12: false });
50
- }
51
-
52
- function decorate(messages) {
53
- const now = Date.now();
54
- return (messages || []).map((message) => ({
55
- ...message,
56
- sent_at: message.sent_at || (message.ts ? new Date(message.ts).toISOString() : ''),
57
- age_seconds: typeof message.age_seconds === 'number'
58
- ? message.age_seconds
59
- : Math.max(0, Math.round((now - (message.ts || now)) / 1000)),
60
- }));
61
- }
62
-
63
- function printInbox(result) {
64
- if (!result.messages.length) {
65
- console.log(result.timed_out ? `⌛ 等待 ${result.waited_seconds}s,没有新消息` : '📭 没有新消息');
66
- return;
67
- }
68
- for (const message of decorate(result.messages)) {
69
- const thread = message.thread ? ` [#${message.thread}]` : '';
70
- console.log(`\n📨 seq=${message.seq} ${message.from} → ${message.to}${thread} ${fmtTime(message.sent_at)}(${message.age_seconds}s 前)`);
71
- console.log(message.text);
72
- }
73
- console.log(`\n— 共 ${result.messages.length} 条,cursor=${result.cursor},latest=${result.latest}${result.has_more ? ',还有更多' : ''}`);
74
- }
75
-
76
- async function main() {
77
- const args = parseArgs(process.argv.slice(2));
78
- const command = args._[0] || 'status';
79
- const json = Boolean(args.json);
80
-
81
- if (command === 'serve' || command === 'hub') {
82
- const hubArgs = [path.join(__dirname, 'hub.mjs')];
83
- if (args.config) hubArgs.push('--config', args.config);
84
- if (args.port) hubArgs.push('--port', String(args.port));
85
- if (args.host) hubArgs.push('--host', String(args.host));
86
- if (args.token) hubArgs.push('--token', String(args.token));
87
- if (args.data) hubArgs.push('--data', String(args.data));
88
- const child = spawn(process.execPath, hubArgs, { stdio: 'inherit' });
89
- child.on('exit', (code) => process.exit(code ?? 0));
90
- return;
91
- }
92
-
93
- const config = loadConfig({
94
- config: typeof args.config === 'string' ? args.config : undefined,
95
- url: typeof args.url === 'string' ? args.url : undefined,
96
- token: typeof args.token === 'string' ? args.token : undefined,
97
- self: typeof args.self === 'string' ? args.self : undefined,
98
- state: typeof args.state === 'string' ? args.state : undefined,
99
- });
100
- const client = new RelayClient(config);
101
-
102
- switch (command) {
103
- case 'status': {
104
- const status = await client.status();
105
- if (json) return console.log(JSON.stringify(status, null, 2));
106
- const dot = status.reachable ? '🟢' : '🔴';
107
- console.log(`${dot} self=${status.self} hub=${status.relay_url} 可达=${status.reachable ? '是' : '否'}`);
108
- if (status.hub) console.log(` hub=${status.hub.name}@${status.hub.hostname}:${status.hub.port} 运行 ${status.hub.uptime_seconds}s,最新 seq=${status.latest}`);
109
- console.log(` 游标 cursor=${status.cursor} 配置=${status.config_path || '(env)'}`);
110
- if (status.peers?.length) console.log(` peers: ${status.peers.map((p) => `${p.name}${p.online ? '(在线)' : '(离线)'}`).join(', ')}`);
111
- if (status.error) console.log(` 错误: ${status.error}\n 提示: ${status.hint}`);
112
- return undefined;
113
- }
114
- case 'send': {
115
- if (!args.to || !args.text) {
116
- throw new RelayError('用法:send --to <peer> --text <内容> [--to-session <session-id>] [--from-session <session-id>] [--thread <标签>]');
117
- }
118
- const result = await client.send({
119
- to: args.to,
120
- text: args.text,
121
- thread: args.thread,
122
- toSession: args['to-session'],
123
- fromSession: args['from-session'],
124
- });
125
- if (json) return console.log(JSON.stringify(result, null, 2));
126
- const sessionHint = result.message.to_session ? `(会话 ${result.message.to_session})` : '';
127
- console.log(`✅ 已发送 #${result.message.seq} → ${result.message.to}${sessionHint}(${result.message.text.length} 字符)`);
128
- return undefined;
129
- }
130
- case 'inbox': {
131
- const result = await client.inbox({
132
- limit: Number(args.limit) || 20,
133
- fromSeq: args['from-seq'] !== undefined ? Number(args['from-seq']) : null,
134
- markRead: args['no-mark-read'] ? false : true,
135
- });
136
- if (json) return console.log(JSON.stringify(result, null, 2));
137
- printInbox(result);
138
- return undefined;
139
- }
140
- case 'wait': {
141
- const result = await client.wait({
142
- timeoutSeconds: Number(args.timeout) || 25,
143
- afterSeq: args['after-seq'] !== undefined ? Number(args['after-seq']) : null,
144
- });
145
- if (json) return console.log(JSON.stringify(result, null, 2));
146
- printInbox(result);
147
- return undefined;
148
- }
149
- case 'peers': {
150
- const result = await client.peers();
151
- if (json) return console.log(JSON.stringify(result, null, 2));
152
- if (!result.peers?.length) console.log('(暂无 peer 记录:只有至少发过一次请求的 peer 才会出现)');
153
- for (const peer of result.peers || []) {
154
- console.log(`${peer.online ? '🟢' : '⚪'} ${peer.name} 最后活跃 ${peer.last_seen_seconds}s 前`);
155
- }
156
- return undefined;
157
- }
158
- default:
159
- throw new RelayError(`未知命令:${command}`, {
160
- hint: '可用命令:status / send / inbox / wait / peers / serve',
161
- });
162
- }
163
- }
164
-
165
- main().catch((error) => {
166
- const message = error instanceof RelayError ? error.toString() : String(error?.stack || error);
167
- console.error(`❌ ${message}`);
168
- process.exit(1);
169
- });
@@ -1,282 +0,0 @@
1
- [CmdletBinding()]
2
- param(
3
- [ValidateSet('auto', 'task', 'startup', 'runkey')][string]$Method = 'auto',
4
- [string]$ConfigPath = '',
5
- [string]$TaskName = 'DSH-LAN-Relay-Hub',
6
- [switch]$Remove,
7
- [switch]$NoStart,
8
- [switch]$Force
9
- )
10
-
11
- $ErrorActionPreference = 'Stop'
12
- $ProjectDir = Split-Path -Parent $PSScriptRoot
13
-
14
- if (-not $ConfigPath) {
15
- $ConfigPath = Join-Path $env:USERPROFILE '.dsh\lan-relay\config.json'
16
- }
17
-
18
- $startupDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Startup'
19
- $startupShortcut = Join-Path $startupDir 'DSH LAN Relay.lnk'
20
- $legacyStartupVbs = Join-Path $startupDir 'DSH-LAN-Relay.vbs'
21
- $runKeyPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
22
- $runKeyName = 'DSH-LAN-Relay'
23
- $launcherVbs = Join-Path (Split-Path -Parent $ConfigPath) 'launch-hub.vbs'
24
-
25
- function Get-LauncherCommand {
26
- return "wscript.exe `"$launcherVbs`""
27
- }
28
-
29
- function New-Launcher {
30
- param([string]$NodePath, [string]$HubPath, [string]$LogPath)
31
- $vbs = @"
32
- ' DSH LAN Relay Hub hidden launcher with auto-restart - generated by dsh-lan-relay
33
- ' Delete the autostart entry (and this file) to disable.
34
- Set sh = CreateObject("WScript.Shell")
35
- cmd = """$NodePath"" ""$HubPath"" --config ""$ConfigPath"" --log ""$LogPath"""
36
- failures = 0
37
- Do
38
- started = Timer
39
- code = sh.Run(cmd, 0, True)
40
- elapsed = Timer - started
41
- If elapsed < 0 Then elapsed = elapsed + 86400
42
- If elapsed > 30 Then failures = 0
43
- If code = 2 Then WScript.Quit 0
44
- If code <> 0 Then
45
- failures = failures + 1
46
- If failures >= 3 Then WScript.Quit code
47
- Else
48
- failures = 0
49
- End If
50
- WScript.Sleep 3000
51
- Loop
52
- "@
53
- try {
54
- [System.IO.File]::WriteAllText($launcherVbs, $vbs, (New-Object System.Text.ASCIIEncoding))
55
- } catch {
56
- Set-Content -Path $launcherVbs -Value $vbs -Encoding Ascii
57
- }
58
- }
59
-
60
- function Remove-All {
61
- $removed = @()
62
-
63
- $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
64
- if ($task) {
65
- try { Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch { }
66
- Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
67
- $removed += "计划任务 $TaskName"
68
- }
69
-
70
- if (Test-Path $startupShortcut) {
71
- Remove-Item $startupShortcut -Force
72
- $removed += "启动文件夹快捷方式 $startupShortcut"
73
- }
74
-
75
- if (Test-Path $legacyStartupVbs) {
76
- Remove-Item $legacyStartupVbs -Force
77
- $removed += "旧启动项 $legacyStartupVbs"
78
- }
79
-
80
- try {
81
- $value = Get-ItemProperty -Path $runKeyPath -Name $runKeyName -ErrorAction SilentlyContinue
82
- if ($value) {
83
- Remove-ItemProperty -Path $runKeyPath -Name $runKeyName -Force
84
- $removed += "注册表自启项 HKCU\...\Run\$runKeyName"
85
- }
86
- } catch { }
87
-
88
- try {
89
- Get-CimInstance Win32_Process -Filter "Name='wscript.exe'" |
90
- Where-Object { $_.CommandLine -like '*launch-hub.vbs*' } |
91
- ForEach-Object {
92
- Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
93
- $removed += "启动器进程 PID $($_.ProcessId)"
94
- }
95
- } catch { }
96
-
97
- if ($removed.Count) {
98
- Write-Host "已移除:$($removed -join ';')" -ForegroundColor Green
99
- } else {
100
- Write-Host '没有找到任何自启项。' -ForegroundColor Yellow
101
- }
102
- Write-Host "hub 本身如果还在运行,需要的话手动结束:Get-NetTCPConnection -LocalPort 8787 -State Listen" -ForegroundColor DarkGray
103
- }
104
-
105
- if ($Remove) {
106
- Remove-All
107
- exit 0
108
- }
109
-
110
- if (-not (Test-Path $ConfigPath)) {
111
- Write-Host "配置文件不存在:$ConfigPath" -ForegroundColor Red
112
- Write-Host '请先运行:scripts\setup.ps1 -Role hub' -ForegroundColor Yellow
113
- exit 1
114
- }
115
-
116
- try {
117
- $nodePath = (Get-Command node -ErrorAction Stop).Source
118
- } catch {
119
- Write-Host '未找到 node.exe,请安装 Node.js 18+ 并加入 PATH 后重试。' -ForegroundColor Red
120
- exit 1
121
- }
122
-
123
- $hubPath = Join-Path $ProjectDir 'hub.mjs'
124
- if (-not (Test-Path $hubPath)) {
125
- Write-Host "找不到 hub.mjs:$hubPath" -ForegroundColor Red
126
- exit 1
127
- }
128
-
129
- $hubData = Join-Path (Split-Path -Parent $ConfigPath) 'hub'
130
- if (-not (Test-Path $hubData)) { New-Item -ItemType Directory -Force -Path $hubData | Out-Null }
131
- $logPath = Join-Path $hubData 'hub.log'
132
-
133
- New-Launcher -NodePath $nodePath -HubPath $hubPath -LogPath $logPath
134
-
135
- # 老版本可能残留启动文件夹 VBS,统一清掉,避免重复启动
136
- if (Test-Path $legacyStartupVbs) { Remove-Item $legacyStartupVbs -Force }
137
-
138
- function Install-Task {
139
- $argument = "`"$hubPath`" --config `"$ConfigPath`" --log `"$logPath`""
140
- $action = New-ScheduledTaskAction -Execute $nodePath -Argument $argument -WorkingDirectory $ProjectDir
141
- $trigger = New-ScheduledTaskTrigger -AtLogOn
142
- $settings = New-ScheduledTaskSettingsSet `
143
- -AllowStartIfOnBatteries `
144
- -DontStopIfGoingOnBatteries `
145
- -StartWhenAvailable `
146
- -RestartCount 3 `
147
- -RestartInterval (New-TimeSpan -Minutes 1) `
148
- -MultipleInstances IgnoreNew `
149
- -ExecutionTimeLimit ([TimeSpan]::Zero)
150
- $description = 'DSH LAN Relay Hub 开机自启(dsh-lan-relay 生成)。删除:scripts\install-autostart.ps1 -Remove'
151
- Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Description $description -Force -ErrorAction Stop | Out-Null
152
- }
153
-
154
- function Install-StartupShortcut {
155
- if (-not (Test-Path $startupDir)) { New-Item -ItemType Directory -Force -Path $startupDir | Out-Null }
156
- $shell = New-Object -ComObject WScript.Shell
157
- $shortcut = $shell.CreateShortcut($startupShortcut)
158
- $shortcut.TargetPath = 'C:\Windows\System32\wscript.exe'
159
- $shortcut.Arguments = "`"$launcherVbs`""
160
- $shortcut.WorkingDirectory = $ProjectDir
161
- $shortcut.WindowStyle = 7
162
- $shortcut.Description = 'DSH LAN Relay Hub(局域网消息总线)'
163
- $shortcut.Save()
164
- if (-not (Test-Path $startupShortcut)) { throw '快捷方式未创建成功' }
165
- }
166
-
167
- function Install-RunKey {
168
- if (-not (Test-Path $runKeyPath)) { New-Item -Path $runKeyPath -Force | Out-Null }
169
- New-ItemProperty -Path $runKeyPath -Name $runKeyName -Value (Get-LauncherCommand) -PropertyType String -Force | Out-Null
170
- }
171
-
172
- $installed = ''
173
- $errors = @()
174
- $order = switch ($Method) {
175
- 'auto' { @('task', 'startup', 'runkey') }
176
- 'task' { @('task') }
177
- 'startup' { @('startup') }
178
- 'runkey' { @('runkey') }
179
- }
180
-
181
- foreach ($candidate in $order) {
182
- try {
183
- switch ($candidate) {
184
- 'task' { Install-Task; $installed = 'task' }
185
- 'startup' { Install-StartupShortcut; $installed = 'startup' }
186
- 'runkey' { Install-RunKey; $installed = 'runkey' }
187
- }
188
- if ($installed) { break }
189
- } catch {
190
- $errors += "$candidate:$($_.Exception.Message)"
191
- }
192
- }
193
-
194
- if (-not $installed) {
195
- Write-Host '所有自启方式都失败:' -ForegroundColor Red
196
- $errors | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
197
- Write-Host ''
198
- Write-Host '可以手动把启动文件夹快捷方式指向:' -ForegroundColor Yellow
199
- Write-Host " wscript.exe `"$launcherVbs`""
200
- exit 1
201
- }
202
-
203
- # 只保留一种方式,避免重复启动
204
- if ($installed -ne 'task') {
205
- $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
206
- if ($task) {
207
- try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch { }
208
- }
209
- }
210
- if ($installed -ne 'runkey') {
211
- try {
212
- if (Get-ItemProperty -Path $runKeyPath -Name $runKeyName -ErrorAction SilentlyContinue) {
213
- Remove-ItemProperty -Path $runKeyPath -Name $runKeyName -Force
214
- }
215
- } catch { }
216
- }
217
- if ($installed -ne 'startup' -and (Test-Path $startupShortcut)) {
218
- Remove-Item $startupShortcut -Force
219
- }
220
-
221
- $methodLabel = switch ($installed) {
222
- 'task' { "计划任务 $TaskName(登录时启动,支持异常重启)" }
223
- 'startup' { "启动文件夹快捷方式 $startupShortcut" }
224
- 'runkey' { "注册表自启项 HKCU\Software\Microsoft\Windows\CurrentVersion\Run\$runKeyName" }
225
- }
226
-
227
- Write-Host ''
228
- Write-Host '============================================================' -ForegroundColor Cyan
229
- Write-Host ' LAN Relay Hub 开机自启已安装' -ForegroundColor Cyan
230
- Write-Host '============================================================' -ForegroundColor Cyan
231
- Write-Host " 方式 : $methodLabel"
232
- Write-Host " 启动器 : $launcherVbs"
233
- Write-Host " 日志 : $logPath"
234
- Write-Host " 配置文件 : $ConfigPath"
235
- if ($errors.Count) {
236
- Write-Host " 回退记录 : $($errors -join ' / ')" -ForegroundColor DarkYellow
237
- }
238
- Write-Host ''
239
-
240
- $existingListener = Get-NetTCPConnection -LocalPort 8787 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
241
- $launcherRunning = @(
242
- Get-CimInstance Win32_Process -Filter "Name='wscript.exe'" -ErrorAction SilentlyContinue |
243
- Where-Object { $_.CommandLine -like '*launch-hub.vbs*' }
244
- ).Count -gt 0
245
-
246
- if (-not $NoStart) {
247
- if ($launcherRunning) {
248
- Write-Host "hub 已由启动器守护运行(PID $($existingListener.OwningProcess)),无需重复启动。" -ForegroundColor Green
249
- } else {
250
- if ($existingListener) {
251
- $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$($existingListener.OwningProcess)" -ErrorAction SilentlyContinue
252
- if ($proc -and $proc.CommandLine -like '*hub.mjs*') {
253
- Write-Host "检测到未受启动器托管的 hub(PID $($existingListener.OwningProcess)),停止后交给自启动器守护…" -ForegroundColor Yellow
254
- Stop-Process -Id $existingListener.OwningProcess -Force -ErrorAction SilentlyContinue
255
- Start-Sleep -Milliseconds 800
256
- }
257
- }
258
- if ($installed -eq 'task') {
259
- Start-ScheduledTask -TaskName $TaskName
260
- } else {
261
- Start-Process -FilePath 'wscript.exe' -ArgumentList "`"$launcherVbs`"" -WindowStyle Hidden
262
- }
263
- }
264
- Start-Sleep -Seconds 2
265
- }
266
-
267
- $listen = Get-NetTCPConnection -LocalPort 8787 -State Listen -ErrorAction SilentlyContinue
268
- Write-Host '当前状态:' -ForegroundColor Cyan
269
- if ($listen) {
270
- Write-Host " ✅ hub 正在监听 8787(PID $($listen[0].OwningProcess))" -ForegroundColor Green
271
- } else {
272
- Write-Host ' ⚠️ 8787 未监听,查看日志:' -ForegroundColor Yellow
273
- Write-Host " Get-Content `"$logPath`" -Tail 30"
274
- }
275
- Write-Host ''
276
- Write-Host '验证:' -ForegroundColor Cyan
277
- Write-Host " node `"$ProjectDir\relay-cli.mjs`" status"
278
- Write-Host '取消自启:' -ForegroundColor Cyan
279
- Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File `"$ProjectDir\scripts\install-autostart.ps1`" -Remove"
280
- Write-Host '说明:' -ForegroundColor DarkGray
281
- Write-Host ' 这是“登录时自启”。DSH 本身需要登录后使用,所以登录自启足够;' -ForegroundColor DarkGray
282
- Write-Host ' 计划任务方式失败通常是因为当前账户没有注册计划任务的权限,脚本会自动退回启动文件夹方式。' -ForegroundColor DarkGray
package/scripts/setup.ps1 DELETED
@@ -1,231 +0,0 @@
1
- [CmdletBinding()]
2
- param(
3
- [ValidateSet('hub', 'peer')][string]$Role = 'hub',
4
- [string]$Self = '',
5
- [string]$HubUrl = '',
6
- [string]$Token = '',
7
- [int]$Port = 8787,
8
- [string]$ConfigPath = '',
9
- [string]$DefaultSession = '',
10
- [switch]$DisablePush,
11
- [switch]$Force
12
- )
13
-
14
- $ErrorActionPreference = 'Stop'
15
- $ProjectDir = Split-Path -Parent $PSScriptRoot
16
-
17
- function New-Token {
18
- $bytes = New-Object byte[] 24
19
- $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
20
- try {
21
- $rng.GetBytes($bytes)
22
- } finally {
23
- $rng.Dispose()
24
- }
25
- return -join ($bytes | ForEach-Object { $_.ToString('x2') })
26
- }
27
-
28
- function Get-LanIps {
29
- try {
30
- Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
31
- Where-Object { $_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.254.*' } |
32
- Select-Object -ExpandProperty IPAddress
33
- } catch {
34
- @()
35
- }
36
- }
37
-
38
- function Pick-LanIp {
39
- param([string[]]$Ips)
40
- $order = @('192.168.*', '10.*', '172.16.*', '172.17.*', '172.18.*', '172.19.*', '172.20.*', '172.21.*', '172.22.*', '172.23.*', '172.24.*', '172.25.*', '172.26.*', '172.27.*', '172.28.*', '172.29.*', '172.30.*', '172.31.*')
41
- foreach ($pattern in $order) {
42
- $hit = $Ips | Where-Object { $_ -like $pattern } | Select-Object -First 1
43
- if ($hit) { return $hit }
44
- }
45
- return ($Ips | Select-Object -First 1)
46
- }
47
-
48
- if (-not $ConfigPath) {
49
- $ConfigPath = Join-Path $env:USERPROFILE '.dsh\lan-relay\config.json'
50
- }
51
- $ConfigDir = Split-Path -Parent $ConfigPath
52
-
53
- $existing = $null
54
- if ((Test-Path $ConfigPath) -and -not $Force) {
55
- try {
56
- $existing = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
57
- } catch {
58
- Write-Warning "已存在的配置无法解析,将按参数重新生成:$ConfigPath"
59
- $existing = $null
60
- }
61
- }
62
-
63
- function Get-ExistingValue {
64
- param([string]$Name, $Default)
65
- if ($existing -and ($existing.PSObject.Properties.Name -contains $Name)) {
66
- $value = $existing.$Name
67
- if ($null -ne $value -and "$value" -ne '') { return $value }
68
- }
69
- return $Default
70
- }
71
-
72
- $hostname = ($env:COMPUTERNAME -as [string])
73
- if (-not $hostname) { $hostname = 'dsh' }
74
- $defaultSelf = $hostname.ToLower() -replace '[^a-z0-9._-]', '-'
75
-
76
- $resolvedSelf = if ($Self) { $Self.ToLower() } else { Get-ExistingValue -Name 'self' -Default $defaultSelf }
77
- $resolvedToken = if ($Token) { $Token } else { Get-ExistingValue -Name 'token' -Default (New-Token) }
78
-
79
- if ($Role -eq 'hub') {
80
- $lanIp = Pick-LanIp -Ips (Get-LanIps)
81
- $defaultUrl = if ($lanIp) { "http://${lanIp}:$Port" } else { "http://127.0.0.1:$Port" }
82
- $resolvedUrl = if ($HubUrl) { $HubUrl } else { Get-ExistingValue -Name 'relay_url' -Default $defaultUrl }
83
- } else {
84
- $resolvedUrl = if ($HubUrl) { $HubUrl } else { Get-ExistingValue -Name 'relay_url' -Default '' }
85
- if (-not $resolvedUrl) {
86
- $resolvedUrl = Read-Host '请输入 hub 机器的局域网地址(例如 http://192.168.0.105:8787)'
87
- if (-not $resolvedUrl) { throw '缺少 -HubUrl:peer 模式必须知道 hub 的地址' }
88
- }
89
- }
90
-
91
- $resolvedUrl = $resolvedUrl.TrimEnd('/')
92
-
93
- $hubConfig = $null
94
- if ($existing -and ($existing.PSObject.Properties.Name -contains 'hub')) { $hubConfig = $existing.hub }
95
-
96
- $dataDir = if ($hubConfig -and ($hubConfig.PSObject.Properties.Name -contains 'data_dir') -and $hubConfig.data_dir) {
97
- $hubConfig.data_dir
98
- } else {
99
- Join-Path $env:USERPROFILE '.dsh\lan-relay\hub'
100
- }
101
-
102
- $stateDir = Get-ExistingValue -Name 'state_dir' -Default (Join-Path $env:USERPROFILE '.dsh\lan-relay\state')
103
-
104
- # push 段:保留已有配置;可用 -DefaultSession 指定默认投递会话,-DisablePush 关闭自动唤醒
105
- $existingPush = $null
106
- if ($existing -and ($existing.PSObject.Properties.Name -contains 'push')) { $existingPush = $existing.push }
107
- $pushDefault = ''
108
- if ($DefaultSession) {
109
- $pushDefault = $DefaultSession
110
- } elseif ($existingPush -and ($existingPush.PSObject.Properties.Name -contains 'default_session')) {
111
- $pushDefault = [string]$existingPush.default_session
112
- }
113
- $pushEnabled = $true
114
- if ($DisablePush) {
115
- $pushEnabled = $false
116
- } elseif ($existingPush -and ($existingPush.PSObject.Properties.Name -contains 'enabled')) {
117
- $pushEnabled = [bool]$existingPush.enabled
118
- }
119
- $pushWake = $true
120
- if ($existingPush -and ($existingPush.PSObject.Properties.Name -contains 'wake')) { $pushWake = [bool]$existingPush.wake }
121
- $pushTimeout = 25
122
- if ($existingPush -and ($existingPush.PSObject.Properties.Name -contains 'poll_timeout_seconds')) { $pushTimeout = [int]$existingPush.poll_timeout_seconds }
123
- # 策略开关:目标会话不在线时是否兜底改投;无 to_session 是否拒收
124
- $pushOfflineFallback = $false
125
- if ($existingPush -and ($existingPush.PSObject.Properties.Name -contains 'offline_fallback')) { $pushOfflineFallback = [bool]$existingPush.offline_fallback }
126
- $pushRequireSession = $false
127
- if ($existingPush -and ($existingPush.PSObject.Properties.Name -contains 'require_session')) { $pushRequireSession = [bool]$existingPush.require_session }
128
- # 目标会话不在线时:prompt = 冷启动投递(sessionController.prompt 自动 resume);drop = 放弃
129
- $pushColdSession = 'prompt'
130
- if ($existingPush -and ($existingPush.PSObject.Properties.Name -contains 'cold_session') -and "$($existingPush.cold_session)" -eq 'drop') { $pushColdSession = 'drop' }
131
-
132
- New-Item -ItemType Directory -Force -Path $ConfigDir, $stateDir | Out-Null
133
-
134
- $config = [ordered]@{
135
- role = $(if ($Role -eq 'hub') { 'server' } else { 'client' })
136
- relay_url = $resolvedUrl
137
- token = $resolvedToken
138
- self = $resolvedSelf
139
- state_dir = $stateDir
140
- request_timeout_ms = 15000
141
- hub = [ordered]@{
142
- enabled = ($Role -eq 'hub')
143
- host = '0.0.0.0'
144
- port = $Port
145
- data_dir = $dataDir
146
- }
147
- push = [ordered]@{
148
- enabled = $pushEnabled
149
- default_session = $pushDefault
150
- wake = $pushWake
151
- poll_timeout_seconds = $pushTimeout
152
- offline_fallback = $pushOfflineFallback
153
- require_session = $pushRequireSession
154
- cold_session = $pushColdSession
155
- }
156
- }
157
-
158
- $json = $config | ConvertTo-Json -Depth 6
159
- [System.IO.File]::WriteAllText($ConfigPath, $json, (New-Object System.Text.UTF8Encoding($false)))
160
-
161
- $nodePath = $null
162
- try { $nodePath = (Get-Command node -ErrorAction Stop).Source } catch { $nodePath = $null }
163
-
164
- $mcpServerPath = Join-Path $ProjectDir 'mcp-server.mjs'
165
- $connectorJson = [ordered]@{
166
- mcpServers = [ordered]@{
167
- 'lan-relay' = [ordered]@{
168
- command = 'node'
169
- args = @($mcpServerPath)
170
- env = [ordered]@{ LAN_RELAY_CONFIG = $ConfigPath }
171
- }
172
- }
173
- } | ConvertTo-Json -Depth 8
174
-
175
- $connectorFile = Join-Path $ConfigDir 'mcp-connector.json'
176
- [System.IO.File]::WriteAllText($connectorFile, $connectorJson, (New-Object System.Text.UTF8Encoding($false)))
177
-
178
- Write-Host ''
179
- Write-Host '============================================================' -ForegroundColor Cyan
180
- Write-Host " LAN Relay 配置完成($Role)" -ForegroundColor Cyan
181
- Write-Host '============================================================' -ForegroundColor Cyan
182
- Write-Host " 角色 : $Role"
183
- Write-Host " peer 名 : $resolvedSelf"
184
- Write-Host " hub 地址 : $resolvedUrl"
185
- Write-Host " token : $resolvedToken"
186
- Write-Host " 配置文件 : $ConfigPath"
187
- Write-Host " 游标目录 : $stateDir"
188
- Write-Host " push 自动唤醒 : $(if ($pushEnabled) { '开启' } else { '关闭' }) 默认会话: $(if ($pushDefault) { $pushDefault } else { '(最近活跃会话)' })"
189
- Write-Host " MCP 路径 : $mcpServerPath"
190
- Write-Host " MCP 配置 : $connectorFile"
191
- Write-Host " Node : $(if ($nodePath) { "$nodePath $(& node --version)" } else { '未找到!请安装 Node.js 18+ 并加入 PATH' })"
192
- Write-Host ''
193
-
194
- if ($Role -eq 'hub') {
195
- $allIps = Get-LanIps
196
- if ($allIps.Count -gt 1) {
197
- Write-Host " 注意:检测到多个局域网 IP:$($allIps -join ', ')" -ForegroundColor Yellow
198
- Write-Host " relay_url 目前用的是 $resolvedUrl,请确认与另一台机器在同一网段;不对就改配置文件或用 -HubUrl 重跑。" -ForegroundColor Yellow
199
- Write-Host ''
200
- }
201
- Write-Host '接下来(hub 机器):' -ForegroundColor Yellow
202
- Write-Host " 1) 启动 hub:"
203
- Write-Host " node `"$ProjectDir\hub.mjs`" --config `"$ConfigPath`""
204
- Write-Host " 或后台常驻:"
205
- Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File `"$ProjectDir\scripts\install-autostart.ps1`""
206
- Write-Host " 2) 放行防火墙(管理员 PowerShell 执行一次):"
207
- Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File `"$ProjectDir\scripts\allow-firewall.ps1`" -Port $Port"
208
- Write-Host ''
209
- } else {
210
- Write-Host '接下来(peer 机器):' -ForegroundColor Yellow
211
- Write-Host " 1) 确认能访问 hub:node `"$ProjectDir\relay-cli.mjs`" status"
212
- Write-Host " 2) 装自动唤醒插件(需要本机 DSH 自带的 CLI,装完重启 DSH):"
213
- Write-Host " & `"<本机 DSH 安装目录>\resources\runtime\cli\bin\dsh.cmd`" plugin --profile desktop add `"file:$ProjectDir\push-plugin`""
214
- Write-Host " 3) 想让消息默认投到某个会话:先拿到会话 id(DSH 会话里执行 echo `$env:DSH_SESSION_ID),再重跑:"
215
- Write-Host " scripts\setup.ps1 -Role peer -Self $resolvedSelf -HubUrl $resolvedUrl -Token $resolvedToken -DefaultSession <session-id>"
216
- Write-Host " 4) 重启 DSH 后验证:node `"$ProjectDir\relay-cli.mjs`" status && node `"$ProjectDir\relay-cli.mjs`" peers"
217
- Write-Host ' 注意:peer 机器不要启动 hub(不要跑 install-autostart.ps1 / start-hub.cmd),hub 只跑在 hub 机器上。'
218
- Write-Host ''
219
- }
220
-
221
- Write-Host '把下面这段作为自定义 stdio MCP 连接加进 DSH(两台机器都要加):' -ForegroundColor Yellow
222
- Write-Host ' - GUI:设置 → MCP 连接器 → 自定义 → 传输方式 stdio,命令填 node,参数填 mcp-server.mjs 的绝对路径'
223
- Write-Host ' - 或把 mcp-connector.json 导入:'
224
- Write-Host ' mcp_connector_import_json,内容如下:' -ForegroundColor DarkGray
225
- Write-Host ''
226
- Write-Host $connectorJson -ForegroundColor Gray
227
- Write-Host ''
228
- Write-Host '另一台机器的配置只需要改 relay_url(指向 hub)和 self(本机名字),token 必须一致。' -ForegroundColor DarkGray
229
- Write-Host '把该值发给另一台机器,在那边执行:' -ForegroundColor DarkGray
230
- Write-Host " scripts\setup.ps1 -Role peer -Self <对方名字> -HubUrl $resolvedUrl -Token $resolvedToken" -ForegroundColor DarkGray
231
- Write-Host ''
package/start-hub.cmd DELETED
@@ -1,13 +0,0 @@
1
- @echo off
2
- setlocal
3
- set CONFIG=%USERPROFILE%\.dsh\lan-relay\config.json
4
- if not exist "%CONFIG%" (
5
- echo 找不到配置:%CONFIG%
6
- echo 请先运行:scripts\setup.ps1 -Role hub
7
- pause
8
- exit /b 1
9
- )
10
- node "%~dp0hub.mjs" --config "%CONFIG%"
11
- echo.
12
- echo hub 已退出(退出码 %ERRORLEVEL%)
13
- pause