@wenbin_wb/dsh-bridge 2.5.0 → 2.5.2
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.en.md +122 -19
- package/README.md +122 -21
- package/client/client.js +308 -28
- package/client/index.js +283 -32
- package/docs/banner.jpg +0 -0
- package/docs/feishu-usage.md +5 -0
- package/docs/platform-abstraction-design.md +473 -0
- package/docs/screenshots/admin-lock-screen.jpg +0 -0
- package/docs/screenshots/feishu-bot-config.jpg +0 -0
- package/docs/screenshots/feishu-chat.jpg +0 -0
- package/docs/screenshots/lan-access.jpg +0 -0
- package/docs/screenshots/qq-bot-config.jpg +0 -0
- package/docs/screenshots/qq-chat.jpg +0 -0
- package/docs/screenshots/qq-group.jpg +0 -0
- package/docs/screenshots/qr-scan.jpg +0 -0
- package/docs/screenshots/remote-auth-login.jpg +0 -0
- package/docs/screenshots/remote-web-mobile.jpg +0 -0
- package/docs/screenshots/security-auth-config.jpg +0 -0
- package/docs/screenshots/telegram-bot-config.jpg +0 -0
- package/docs/screenshots/tunnel-access.jpg +0 -0
- package/docs/screenshots/wechat-bot-config.jpg +0 -0
- package/docs/screenshots/wechat-chat.jpg +0 -0
- package/docs/telegram-usage.md +2 -0
- package/docs/wechat-bot-plan.md +294 -0
- package/lib/auth/manager.js +63 -18
- package/lib/bridge-rpc.js +95 -8
- package/lib/feishu/node.js +4 -4
- package/lib/index.js +91 -22
- package/lib/qq/node.js +9 -2
- package/lib/telegram/node.js +4 -4
- package/lib/tunnel-client.mjs +11 -2
- package/lib/wechat/media.js +8 -2
- package/lib/wechat/node.js +2 -2
- package/package.json +11 -12
package/lib/bridge-rpc.js
CHANGED
|
@@ -11,13 +11,14 @@ function ok(value) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function fail(code, message, details = {}) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const allowedCodes = new Set([
|
|
15
|
+
'bad-request', 'cancelled', 'internal', 'settings-rejected', 'command-error'
|
|
16
|
+
]);
|
|
17
|
+
const safeCode = allowedCodes.has(code) ? code : 'bad-request';
|
|
17
18
|
return {
|
|
18
19
|
ok: false,
|
|
19
20
|
error: {
|
|
20
|
-
code:
|
|
21
|
+
code: safeCode,
|
|
21
22
|
message,
|
|
22
23
|
details: { issues: [{ message }], ...details },
|
|
23
24
|
},
|
|
@@ -54,6 +55,21 @@ async function wechatStatusValue(wechatService, logger) {
|
|
|
54
55
|
return { ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } };
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function checkAdminAuth(authManager, payload) {
|
|
59
|
+
if (!authManager) return null;
|
|
60
|
+
if (authManager.adminPolicy === 'open') return null;
|
|
61
|
+
// 若系统尚未设置任何管理密码或访客密码,允许免密管理
|
|
62
|
+
const hasAnyPassword = authManager.hasAdminPassword || authManager.hasPassword;
|
|
63
|
+
if (!hasAnyPassword) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// 已设置密码时,必须提供经服务端校验有效的 adminToken(绝不依赖客户端自称的 isLocalhost)
|
|
67
|
+
if (payload?.adminToken && authManager.validateAdminSession(payload.adminToken)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
export function installBridgeRpc(ctx, { service, authManager, wechat, platformManager, logger, saveCustomTunnelConfig }) {
|
|
58
74
|
if (!ctx?.connection?.rpc?.handle) {
|
|
59
75
|
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
@@ -67,7 +83,8 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
67
83
|
|
|
68
84
|
try {
|
|
69
85
|
if (endpoint === BRIDGE_ENDPOINTS.getStatus) {
|
|
70
|
-
const
|
|
86
|
+
const isAdmin = checkAdminAuth(authManager, payload) === null;
|
|
87
|
+
const status = await service.getStatus({ adminAuthValid: isAdmin });
|
|
71
88
|
return ok(status);
|
|
72
89
|
}
|
|
73
90
|
|
|
@@ -75,11 +92,15 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
75
92
|
|
|
76
93
|
if (endpoint === BRIDGE_ENDPOINTS.authGetStatus) {
|
|
77
94
|
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
78
|
-
|
|
95
|
+
const isAdmin = checkAdminAuth(authManager, payload) === null;
|
|
96
|
+
return ok(authManager.getStatus({ masked: !isAdmin }));
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
if (endpoint === BRIDGE_ENDPOINTS.authUpdateConfig) {
|
|
82
100
|
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
101
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
102
|
+
if (adminErr) return adminErr;
|
|
103
|
+
|
|
83
104
|
const { enabled, mode, scope, adminPolicy, password, adminPassword } = payload;
|
|
84
105
|
if (enabled != null) await authManager.setEnabled(enabled);
|
|
85
106
|
if (mode != null) await authManager.setMode(mode);
|
|
@@ -87,13 +108,16 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
87
108
|
if (adminPolicy != null) await authManager.setAdminPolicy(adminPolicy);
|
|
88
109
|
if (password !== undefined) await authManager.setPassword(password);
|
|
89
110
|
if (adminPassword !== undefined) await authManager.setAdminPassword(adminPassword);
|
|
90
|
-
return ok(authManager.getStatus());
|
|
111
|
+
return ok(authManager.getStatus({ masked: false }));
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
if (endpoint === BRIDGE_ENDPOINTS.authRegenerateToken) {
|
|
94
115
|
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
116
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
117
|
+
if (adminErr) return adminErr;
|
|
118
|
+
|
|
95
119
|
await authManager.regenerateSecretToken();
|
|
96
|
-
return ok(authManager.getStatus());
|
|
120
|
+
return ok(authManager.getStatus({ masked: false }));
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
if (endpoint === BRIDGE_ENDPOINTS.authAdminUnlock) {
|
|
@@ -104,7 +128,16 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
104
128
|
return fail('bad-request', res.error || '管理员密码错误');
|
|
105
129
|
}
|
|
106
130
|
|
|
131
|
+
if (endpoint === BRIDGE_ENDPOINTS.authAdminLock) {
|
|
132
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
133
|
+
if (payload?.adminToken) authManager.revokeAdminSession(payload.adminToken);
|
|
134
|
+
return ok({ locked: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
if (endpoint === BRIDGE_ENDPOINTS.saveCustomTunnelConfig) {
|
|
138
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
139
|
+
if (adminErr) return adminErr;
|
|
140
|
+
|
|
108
141
|
const { serverUrl = '', accessToken = '' } = payload;
|
|
109
142
|
await saveCustomTunnelConfig(serverUrl.trim(), accessToken.trim());
|
|
110
143
|
const status = await service.getStatus();
|
|
@@ -112,6 +145,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
112
145
|
}
|
|
113
146
|
|
|
114
147
|
if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
|
|
148
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
149
|
+
if (adminErr) return adminErr;
|
|
150
|
+
|
|
115
151
|
try {
|
|
116
152
|
await service.startCustomTunnel();
|
|
117
153
|
const status = await service.getStatus();
|
|
@@ -123,12 +159,18 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
if (endpoint === BRIDGE_ENDPOINTS.stopCustomTunnel) {
|
|
162
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
163
|
+
if (adminErr) return adminErr;
|
|
164
|
+
|
|
126
165
|
service.stopCustomTunnel();
|
|
127
166
|
const status = await service.getStatus();
|
|
128
167
|
return ok(status);
|
|
129
168
|
}
|
|
130
169
|
|
|
131
170
|
if (endpoint === BRIDGE_ENDPOINTS.startCloudflared) {
|
|
171
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
172
|
+
if (adminErr) return adminErr;
|
|
173
|
+
|
|
132
174
|
try {
|
|
133
175
|
await service.startCloudflared();
|
|
134
176
|
const status = await service.getStatus();
|
|
@@ -140,12 +182,18 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
140
182
|
}
|
|
141
183
|
|
|
142
184
|
if (endpoint === BRIDGE_ENDPOINTS.stopCloudflared) {
|
|
185
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
186
|
+
if (adminErr) return adminErr;
|
|
187
|
+
|
|
143
188
|
service.stopCloudflared();
|
|
144
189
|
const status = await service.getStatus();
|
|
145
190
|
return ok(status);
|
|
146
191
|
}
|
|
147
192
|
|
|
148
193
|
if (endpoint === BRIDGE_ENDPOINTS.resetCloudflared) {
|
|
194
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
195
|
+
if (adminErr) return adminErr;
|
|
196
|
+
|
|
149
197
|
await service.resetCloudflared();
|
|
150
198
|
const status = await service.getStatus();
|
|
151
199
|
return ok(status);
|
|
@@ -157,6 +205,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
157
205
|
}
|
|
158
206
|
|
|
159
207
|
if (endpoint === BRIDGE_ENDPOINTS.upgradePlugin) {
|
|
208
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
209
|
+
if (adminErr) return adminErr;
|
|
210
|
+
|
|
160
211
|
const result = await service.upgradePlugin(payload);
|
|
161
212
|
return ok(result);
|
|
162
213
|
}
|
|
@@ -180,6 +231,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
180
231
|
|
|
181
232
|
if (endpoint === BRIDGE_ENDPOINTS.platformLogin) {
|
|
182
233
|
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
234
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
235
|
+
if (adminErr) return adminErr;
|
|
236
|
+
|
|
183
237
|
const { platformId, qrType } = payload;
|
|
184
238
|
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
185
239
|
const platform = platformManager.get(platformId);
|
|
@@ -193,6 +247,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
193
247
|
|
|
194
248
|
if (endpoint === BRIDGE_ENDPOINTS.platformSetAllowFrom) {
|
|
195
249
|
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
250
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
251
|
+
if (adminErr) return adminErr;
|
|
252
|
+
|
|
196
253
|
const { platformId, allowFrom } = payload;
|
|
197
254
|
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
198
255
|
const platform = platformManager.get(platformId);
|
|
@@ -205,6 +262,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
205
262
|
|
|
206
263
|
if (endpoint === BRIDGE_ENDPOINTS.platformSetConfig) {
|
|
207
264
|
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
265
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
266
|
+
if (adminErr) return adminErr;
|
|
267
|
+
|
|
208
268
|
const { platformId, ...config } = payload;
|
|
209
269
|
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
210
270
|
const platform = platformManager.get(platformId);
|
|
@@ -217,6 +277,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
217
277
|
|
|
218
278
|
if (endpoint === BRIDGE_ENDPOINTS.platformStop) {
|
|
219
279
|
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
280
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
281
|
+
if (adminErr) return adminErr;
|
|
282
|
+
|
|
220
283
|
const { platformId } = payload;
|
|
221
284
|
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
222
285
|
const platform = platformManager.get(platformId);
|
|
@@ -229,6 +292,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
229
292
|
|
|
230
293
|
if (endpoint === BRIDGE_ENDPOINTS.platformStart) {
|
|
231
294
|
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
295
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
296
|
+
if (adminErr) return adminErr;
|
|
297
|
+
|
|
232
298
|
const { platformId } = payload;
|
|
233
299
|
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
234
300
|
const platform = platformManager.get(platformId);
|
|
@@ -241,6 +307,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
241
307
|
|
|
242
308
|
if (endpoint === BRIDGE_ENDPOINTS.platformUnbind) {
|
|
243
309
|
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
310
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
311
|
+
if (adminErr) return adminErr;
|
|
312
|
+
|
|
244
313
|
const { platformId } = payload;
|
|
245
314
|
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
246
315
|
const platform = platformManager.get(platformId);
|
|
@@ -261,6 +330,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
261
330
|
|
|
262
331
|
if (endpoint === BRIDGE_ENDPOINTS.wechatLogin) {
|
|
263
332
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
333
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
334
|
+
if (adminErr) return adminErr;
|
|
335
|
+
|
|
264
336
|
const { qrType } = payload;
|
|
265
337
|
const result = await wechat.login({ qrType });
|
|
266
338
|
if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
|
|
@@ -270,6 +342,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
270
342
|
|
|
271
343
|
if (endpoint === BRIDGE_ENDPOINTS.wechatSetAllowFrom) {
|
|
272
344
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
345
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
346
|
+
if (adminErr) return adminErr;
|
|
347
|
+
|
|
273
348
|
await wechat.setAllowFrom(payload.allowFrom);
|
|
274
349
|
const value = await wechatStatusValue(wechat, logger);
|
|
275
350
|
return ok(value);
|
|
@@ -277,6 +352,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
277
352
|
|
|
278
353
|
if (endpoint === BRIDGE_ENDPOINTS.wechatSetConfig) {
|
|
279
354
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
355
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
356
|
+
if (adminErr) return adminErr;
|
|
357
|
+
|
|
280
358
|
await wechat.setConfig(payload);
|
|
281
359
|
const value = await wechatStatusValue(wechat, logger);
|
|
282
360
|
return ok(value);
|
|
@@ -284,6 +362,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
284
362
|
|
|
285
363
|
if (endpoint === BRIDGE_ENDPOINTS.wechatStop) {
|
|
286
364
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
365
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
366
|
+
if (adminErr) return adminErr;
|
|
367
|
+
|
|
287
368
|
await wechat.stop();
|
|
288
369
|
const value = await wechatStatusValue(wechat, logger);
|
|
289
370
|
return ok(value);
|
|
@@ -291,6 +372,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
291
372
|
|
|
292
373
|
if (endpoint === BRIDGE_ENDPOINTS.wechatStart) {
|
|
293
374
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
375
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
376
|
+
if (adminErr) return adminErr;
|
|
377
|
+
|
|
294
378
|
await wechat.gateway.start().catch((err) => {
|
|
295
379
|
logger.error('wechat start enabled: %s', err?.message ?? err);
|
|
296
380
|
});
|
|
@@ -300,6 +384,9 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
300
384
|
|
|
301
385
|
if (endpoint === BRIDGE_ENDPOINTS.wechatUnbind) {
|
|
302
386
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
387
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
388
|
+
if (adminErr) return adminErr;
|
|
389
|
+
|
|
303
390
|
await wechat.unbind();
|
|
304
391
|
const value = await wechatStatusValue(wechat, logger);
|
|
305
392
|
return ok(value);
|
package/lib/feishu/node.js
CHANGED
|
@@ -75,13 +75,13 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
75
75
|
this._inTurn = false
|
|
76
76
|
|
|
77
77
|
// 订阅网关入站消息事件
|
|
78
|
-
this.ctx.on('feishu/message', (event) => this._handleInbound(event))
|
|
78
|
+
this.disposers.push(this.ctx.on('feishu/message', (event) => this._handleInbound(event)))
|
|
79
79
|
|
|
80
80
|
// 订阅卡片交互事件(审批按钮点击)
|
|
81
|
-
this.ctx.on('feishu/action', (event) => this._handleAction(event))
|
|
81
|
+
this.disposers.push(this.ctx.on('feishu/action', (event) => this._handleAction(event)))
|
|
82
82
|
|
|
83
83
|
// 监听轮次事件:turn/start 开启流式会话,turn/end 最终刷新并重置
|
|
84
|
-
this.ctx.on('session/event', (session, event) => {
|
|
84
|
+
this.disposers.push(this.ctx.on('session/event', (session, event) => {
|
|
85
85
|
if (session.id !== this.activeSessionId) return
|
|
86
86
|
if (event.type === 'turn/start') {
|
|
87
87
|
this._inTurn = true
|
|
@@ -103,7 +103,7 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
103
103
|
this._streamCardId = null
|
|
104
104
|
this._streamContent = ''
|
|
105
105
|
}
|
|
106
|
-
})
|
|
106
|
+
}))
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
async _handleInbound(event) {
|
package/lib/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { join, dirname } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
15
|
-
import {
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
16
|
import QRCode from 'qrcode';
|
|
17
17
|
import { installBridgeRpc } from './bridge-rpc.js';
|
|
18
18
|
import { CustomTunnelClient } from './tunnel-client.mjs';
|
|
@@ -186,10 +186,42 @@ class ProxyServer {
|
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
// 3. 处理鉴权状态 API: GET /__dsh_bridge__/auth-status
|
|
189
|
+
// 3. 处理鉴权状态 API: GET /__dsh_bridge__/auth-status (严格脱敏,不暴露 secretToken)
|
|
190
190
|
if (req.url === '/__dsh_bridge__/auth-status' && req.method === 'GET') {
|
|
191
191
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
192
|
-
res.end(JSON.stringify(this.authManager?.
|
|
192
|
+
res.end(JSON.stringify(this.authManager?.getPublicStatus() ?? { enabled: false }));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 3.1 本机特权 Token 签发:仅限真正物理回环连接(127.0.0.1 / ::1,严禁隧道转发流量伪造)
|
|
197
|
+
if (req.url === '/__dsh_bridge__/loopback-token') {
|
|
198
|
+
const corsHeaders = {
|
|
199
|
+
'Access-Control-Allow-Origin': '*',
|
|
200
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
201
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
202
|
+
};
|
|
203
|
+
if (req.method === 'OPTIONS') {
|
|
204
|
+
res.writeHead(204, corsHeaders);
|
|
205
|
+
res.end();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const remote = req.socket?.remoteAddress || '';
|
|
210
|
+
const isLoopback = (remote === '127.0.0.1' || remote === '::1' || remote === '::ffff:127.0.0.1');
|
|
211
|
+
const internalTunnelHeader = req.headers?.['x-dsh-internal-tunnel'];
|
|
212
|
+
const isCustomTunnel = Boolean(isLoopback && internalTunnelHeader && internalTunnelHeader === this.authManager?.internalTunnelSecret);
|
|
213
|
+
const isCloudflare = Boolean(isLoopback && (req.headers?.['cf-ray'] || req.headers?.['cf-connecting-ip']));
|
|
214
|
+
const isPublicTunnel = isCustomTunnel || isCloudflare;
|
|
215
|
+
|
|
216
|
+
if (isLoopback && !isPublicTunnel && this.authManager) {
|
|
217
|
+
const adminToken = this.authManager.createAdminSession();
|
|
218
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', ...corsHeaders });
|
|
219
|
+
res.end(JSON.stringify({ ok: true, adminToken }));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8', ...corsHeaders });
|
|
224
|
+
res.end(JSON.stringify({ ok: false, error: 'Forbidden: loopback only' }));
|
|
193
225
|
return;
|
|
194
226
|
}
|
|
195
227
|
|
|
@@ -396,9 +428,9 @@ class BridgeService {
|
|
|
396
428
|
return this.proxy;
|
|
397
429
|
}
|
|
398
430
|
|
|
399
|
-
async getStatus() {
|
|
431
|
+
async getStatus({ adminAuthValid = false } = {}) {
|
|
400
432
|
const lanIp = selectLanIPv4();
|
|
401
|
-
const token = this.authManager?.secretToken;
|
|
433
|
+
const token = adminAuthValid ? this.authManager?.secretToken : null;
|
|
402
434
|
const isAuthEnabled = Boolean(this.authManager?.enabled && this.authManager?.mode !== 'password_only' && token);
|
|
403
435
|
|
|
404
436
|
const isLanProtected = isAuthEnabled && this.authManager?.scope !== 'public_only';
|
|
@@ -428,7 +460,7 @@ class BridgeService {
|
|
|
428
460
|
return {
|
|
429
461
|
version: VERSION,
|
|
430
462
|
|
|
431
|
-
auth: this.authManager?.getStatus() ?? { enabled: false },
|
|
463
|
+
auth: this.authManager?.getStatus({ masked: !adminAuthValid }) ?? { enabled: false },
|
|
432
464
|
|
|
433
465
|
proxy: {
|
|
434
466
|
running: !!this.proxy,
|
|
@@ -486,6 +518,7 @@ class BridgeService {
|
|
|
486
518
|
serverUrl,
|
|
487
519
|
accessToken,
|
|
488
520
|
localPort: this.proxyPort,
|
|
521
|
+
internalTunnelSecret: this.authManager?.internalTunnelSecret,
|
|
489
522
|
onStateChange: (state) => {
|
|
490
523
|
this.customTunnelState = state;
|
|
491
524
|
},
|
|
@@ -575,29 +608,57 @@ class BridgeService {
|
|
|
575
608
|
}
|
|
576
609
|
}
|
|
577
610
|
|
|
578
|
-
// 一键直接升级插件(执行 dsh
|
|
611
|
+
// 一键直接升级插件(执行 dsh / npx / npm 自动升级,使用安全的参数数组彻底杜绝 shell 注入)
|
|
579
612
|
async upgradePlugin({ version } = {}) {
|
|
580
613
|
const targetVersion = version ? String(version).trim() : 'latest';
|
|
614
|
+
// 严格 SemVer 白名单正则校验
|
|
615
|
+
if (!/^(latest|\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?)$/.test(targetVersion)) {
|
|
616
|
+
return { ok: false, error: `非法的版本号格式: ${targetVersion}`, version: targetVersion };
|
|
617
|
+
}
|
|
581
618
|
const pkgSpec = `@wenbin_wb/dsh-bridge@${targetVersion}`;
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
619
|
+
const isWin = process.platform === 'win32';
|
|
620
|
+
|
|
621
|
+
const tasks = [
|
|
622
|
+
{ cmd: isWin ? 'dsh.cmd' : 'dsh', fallbackCmd: 'dsh', args: ['plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
623
|
+
{ cmd: isWin ? 'npx.cmd' : 'npx', fallbackCmd: 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
624
|
+
{ cmd: isWin ? 'npm.cmd' : 'npm', fallbackCmd: 'npm', args: ['install', pkgSpec] },
|
|
586
625
|
];
|
|
587
626
|
|
|
588
627
|
let lastError = null;
|
|
589
|
-
let output = '';
|
|
590
628
|
|
|
591
|
-
for (const
|
|
629
|
+
for (const task of tasks) {
|
|
592
630
|
try {
|
|
593
631
|
const res = await new Promise((resolve, reject) => {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
632
|
+
const runExecutable = (executable) => {
|
|
633
|
+
const cp = spawn(executable, task.args, {
|
|
634
|
+
windowsHide: true,
|
|
635
|
+
shell: false,
|
|
636
|
+
timeout: 90000,
|
|
637
|
+
});
|
|
638
|
+
let stdout = '';
|
|
639
|
+
let stderr = '';
|
|
640
|
+
cp.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
641
|
+
cp.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
642
|
+
cp.on('error', (err) => {
|
|
643
|
+
if (executable !== task.fallbackCmd) {
|
|
644
|
+
runExecutable(task.fallbackCmd);
|
|
645
|
+
} else {
|
|
646
|
+
reject(err);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
cp.on('close', (code) => {
|
|
650
|
+
if (code === 0) {
|
|
651
|
+
resolve({ stdout, stderr });
|
|
652
|
+
} else {
|
|
653
|
+
reject(new Error(stderr || stdout || `进程退出码 ${code}`));
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
};
|
|
657
|
+
runExecutable(task.cmd);
|
|
598
658
|
});
|
|
599
|
-
|
|
600
|
-
|
|
659
|
+
|
|
660
|
+
const output = res.stdout || res.stderr || '升级成功';
|
|
661
|
+
return { ok: true, command: `${task.cmd} ${task.args.join(' ')}`, output, version: targetVersion };
|
|
601
662
|
} catch (err) {
|
|
602
663
|
lastError = err;
|
|
603
664
|
}
|
|
@@ -634,6 +695,9 @@ function apply(ctx, config = {}) {
|
|
|
634
695
|
const configFile = join(dshHome, 'dsh-bridge', 'config.json');
|
|
635
696
|
const emergencyResetFile = join(dshHome, 'dsh-bridge', 'reset-auth');
|
|
636
697
|
|
|
698
|
+
// 配置写入互斥锁队列,杜绝多平台并发写入造成文件覆盖损坏
|
|
699
|
+
let configWriteQueue = Promise.resolve();
|
|
700
|
+
|
|
637
701
|
// 从 JSON 文件读取持久化配置
|
|
638
702
|
async function loadConfig() {
|
|
639
703
|
try {
|
|
@@ -644,10 +708,15 @@ function apply(ctx, config = {}) {
|
|
|
644
708
|
}
|
|
645
709
|
}
|
|
646
710
|
|
|
647
|
-
// 持久化配置到 JSON
|
|
711
|
+
// 持久化配置到 JSON 文件(排队原子写入)
|
|
648
712
|
async function saveConfig(data) {
|
|
649
|
-
|
|
650
|
-
|
|
713
|
+
configWriteQueue = configWriteQueue.then(async () => {
|
|
714
|
+
await mkdir(join(dshHome, 'dsh-bridge'), { recursive: true });
|
|
715
|
+
await writeFile(configFile, JSON.stringify(data, null, 2), 'utf8');
|
|
716
|
+
}).catch((err) => {
|
|
717
|
+
logger.error('saveConfig failed: %s', err.message);
|
|
718
|
+
});
|
|
719
|
+
return configWriteQueue;
|
|
651
720
|
}
|
|
652
721
|
|
|
653
722
|
// 访问安全认证管理器
|
package/lib/qq/node.js
CHANGED
|
@@ -147,8 +147,8 @@ export class QqConversationNode extends ConversationBridge {
|
|
|
147
147
|
this._replyMsgId = null // 被动回复用的用户消息 ID(事件 d.id)
|
|
148
148
|
|
|
149
149
|
// 订阅网关入站事件
|
|
150
|
-
this.ctx.on('qq/message', (event) => this._handleInbound(event))
|
|
151
|
-
this.ctx.on('qq/interaction', (event) => this._handleInteraction(event))
|
|
150
|
+
this.disposers.push(this.ctx.on('qq/message', (event) => this._handleInbound(event)))
|
|
151
|
+
this.disposers.push(this.ctx.on('qq/interaction', (event) => this._handleInteraction(event)))
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// ---- 出站:覆盖 sendText / sendTyping,正确处理 scope 与被动回复 ----
|
|
@@ -350,6 +350,13 @@ export class QqConversationNode extends ConversationBridge {
|
|
|
350
350
|
})
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/** 发送多媒体附件,正确传递 c2c 或 group 范围 */
|
|
354
|
+
async sendMediaFile(filePath, opts = {}) {
|
|
355
|
+
const peerInfo = this._currentPeer()
|
|
356
|
+
if (!peerInfo) return
|
|
357
|
+
return this.gateway.sendMediaFile(peerInfo.peerId, filePath, { scope: peerInfo.scope, ...opts })
|
|
358
|
+
}
|
|
359
|
+
|
|
353
360
|
// ---- 入站 ----
|
|
354
361
|
|
|
355
362
|
async _handleInbound(event) {
|
package/lib/telegram/node.js
CHANGED
|
@@ -68,13 +68,13 @@ export class TelegramConversationNode extends ConversationBridge {
|
|
|
68
68
|
this._inTurn = false
|
|
69
69
|
|
|
70
70
|
// 订阅网关入站消息事件
|
|
71
|
-
this.ctx.on('telegram/message', (event) => this._handleInbound(event))
|
|
71
|
+
this.disposers.push(this.ctx.on('telegram/message', (event) => this._handleInbound(event)))
|
|
72
72
|
|
|
73
73
|
// 订阅 Inline 按钮点击交互事件(审批确认)
|
|
74
|
-
this.ctx.on('telegram/action', (event) => this._handleAction(event))
|
|
74
|
+
this.disposers.push(this.ctx.on('telegram/action', (event) => this._handleAction(event)))
|
|
75
75
|
|
|
76
76
|
// 监听轮次事件:turn/start 开启流式打字机,turn/end 最终刷新并重置
|
|
77
|
-
this.ctx.on('session/event', (session, event) => {
|
|
77
|
+
this.disposers.push(this.ctx.on('session/event', (session, event) => {
|
|
78
78
|
if (session.id !== this.activeSessionId) return
|
|
79
79
|
if (event.type === 'turn/start') {
|
|
80
80
|
this._inTurn = true
|
|
@@ -91,7 +91,7 @@ export class TelegramConversationNode extends ConversationBridge {
|
|
|
91
91
|
this._streamMsgId = null
|
|
92
92
|
this._streamContent = ''
|
|
93
93
|
}
|
|
94
|
-
})
|
|
94
|
+
}))
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
async _handleInbound(event) {
|
package/lib/tunnel-client.mjs
CHANGED
|
@@ -8,10 +8,11 @@ const RECONNECT_DELAY = 5000;
|
|
|
8
8
|
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
9
9
|
|
|
10
10
|
export class CustomTunnelClient {
|
|
11
|
-
constructor({ serverUrl, accessToken, localPort, signal, onStateChange, logger }) {
|
|
11
|
+
constructor({ serverUrl, accessToken, localPort, internalTunnelSecret, signal, onStateChange, logger }) {
|
|
12
12
|
this.serverUrl = serverUrl;
|
|
13
13
|
this.accessToken = accessToken;
|
|
14
14
|
this.localPort = localPort;
|
|
15
|
+
this.internalTunnelSecret = internalTunnelSecret;
|
|
15
16
|
this.signal = signal;
|
|
16
17
|
this.onStateChange = onStateChange;
|
|
17
18
|
this.logger = logger;
|
|
@@ -126,10 +127,15 @@ export class CustomTunnelClient {
|
|
|
126
127
|
Object.entries(headers ?? {}).filter(([k]) => !SKIP.has(k.toLowerCase()))
|
|
127
128
|
);
|
|
128
129
|
|
|
130
|
+
const reqHeaders = { ...safeHeaders, host: `127.0.0.1:${this.localPort}` };
|
|
131
|
+
if (this.internalTunnelSecret) {
|
|
132
|
+
reqHeaders['x-dsh-internal-tunnel'] = this.internalTunnelSecret;
|
|
133
|
+
}
|
|
134
|
+
|
|
129
135
|
const req = httpRequest({
|
|
130
136
|
host: '127.0.0.1', port: this.localPort,
|
|
131
137
|
method, path: path || '/',
|
|
132
|
-
headers:
|
|
138
|
+
headers: reqHeaders,
|
|
133
139
|
}, (res) => {
|
|
134
140
|
const chunks = [];
|
|
135
141
|
res.on('data', c => chunks.push(c));
|
|
@@ -168,6 +174,9 @@ export class CustomTunnelClient {
|
|
|
168
174
|
const reqHeaders = { ...headers, host: `127.0.0.1:${this.localPort}` };
|
|
169
175
|
delete reqHeaders['proxy-connection'];
|
|
170
176
|
delete reqHeaders['proxy-authorization'];
|
|
177
|
+
if (this.internalTunnelSecret) {
|
|
178
|
+
reqHeaders['x-dsh-internal-tunnel'] = this.internalTunnelSecret;
|
|
179
|
+
}
|
|
171
180
|
|
|
172
181
|
const lines = [`GET ${path || '/'} HTTP/1.1`];
|
|
173
182
|
for (const [k, v] of Object.entries(reqHeaders)) lines.push(`${k}: ${v}`);
|
package/lib/wechat/media.js
CHANGED
|
@@ -187,9 +187,15 @@ export async function downloadMedia({
|
|
|
187
187
|
const controller = new AbortController()
|
|
188
188
|
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
189
189
|
try {
|
|
190
|
-
const
|
|
191
|
-
|
|
190
|
+
const MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024; // 25MB
|
|
191
|
+
const contentLength = Number(response.headers.get('content-length') || 0);
|
|
192
|
+
if (contentLength > MAX_DOWNLOAD_BYTES) {
|
|
193
|
+
throw new Error(`Media file exceeds maximum allowed size (${contentLength} > 25MB)`);
|
|
194
|
+
}
|
|
192
195
|
const raw = Buffer.from(await response.arrayBuffer())
|
|
196
|
+
if (raw.length > MAX_DOWNLOAD_BYTES) {
|
|
197
|
+
throw new Error(`Media file exceeds maximum allowed size (${raw.length} > 25MB)`);
|
|
198
|
+
}
|
|
193
199
|
if (aesKeyBase64) {
|
|
194
200
|
const key = parseAesKey(aesKeyBase64)
|
|
195
201
|
return aes128EcbDecrypt(raw, key)
|
package/lib/wechat/node.js
CHANGED
|
@@ -59,9 +59,9 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
59
59
|
onActiveSessionChange,
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
this.ctx.on('wechat/message', (message) => {
|
|
62
|
+
this.disposers.push(this.ctx.on('wechat/message', (message) => {
|
|
63
63
|
void this._handleInbound(message)
|
|
64
|
-
})
|
|
64
|
+
}))
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// ---- 入站 ----
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenbin_wb/dsh-bridge",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -18,31 +18,29 @@
|
|
|
18
18
|
"README.md",
|
|
19
19
|
"README.en.md",
|
|
20
20
|
"LICENSE",
|
|
21
|
-
"docs
|
|
22
|
-
"docs/custom-tunnel.md",
|
|
23
|
-
"docs/wechat-usage.md",
|
|
24
|
-
"docs/qq-usage.md",
|
|
25
|
-
"docs/feishu-usage.md",
|
|
26
|
-
"docs/telegram-usage.md"
|
|
21
|
+
"docs"
|
|
27
22
|
],
|
|
28
23
|
"scripts": {
|
|
29
24
|
"build:client": "node client/build.mjs",
|
|
25
|
+
"build:banner": "node scripts/generate-banner.mjs",
|
|
26
|
+
"prepack": "npm run build:client",
|
|
30
27
|
"test": "node --test test/*.test.mjs"
|
|
31
28
|
},
|
|
32
29
|
"dependencies": {
|
|
30
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
33
31
|
"@larksuiteoapi/node-sdk": "^1.73.0",
|
|
34
32
|
"qrcode": "^1.5.3",
|
|
35
|
-
"ws": "^8.18.0"
|
|
36
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.6"
|
|
33
|
+
"ws": "^8.18.0"
|
|
37
34
|
},
|
|
38
35
|
"devDependencies": {
|
|
39
|
-
"esbuild": "^0.25.9"
|
|
36
|
+
"esbuild": "^0.25.9",
|
|
37
|
+
"puppeteer-core": "^25.8.0"
|
|
40
38
|
},
|
|
41
39
|
"peerDependencies": {
|
|
42
40
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
43
41
|
},
|
|
44
42
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
43
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
46
44
|
},
|
|
47
45
|
"license": "MIT",
|
|
48
46
|
"keywords": [
|
|
@@ -62,7 +60,8 @@
|
|
|
62
60
|
"bot",
|
|
63
61
|
"wechat",
|
|
64
62
|
"qq",
|
|
65
|
-
"feishu"
|
|
63
|
+
"feishu",
|
|
64
|
+
"telegram"
|
|
66
65
|
],
|
|
67
66
|
"dsh": {
|
|
68
67
|
"bundle": {
|