dsh-bridge-gateway 0.2.2 → 0.3.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/CHANGELOG.md +74 -0
- package/README.md +17 -3
- package/README.zh.md +18 -2
- package/assets/apple-touch-icon-180.png +0 -0
- package/assets/icon-192.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/icon-maskable-192.png +0 -0
- package/assets/icon-maskable-512.png +0 -0
- package/client/client.js +302 -41
- package/client/index.js +438 -42
- package/docs/banner.jpg +0 -0
- package/docs/custom-tunnel.md +5 -5
- package/docs/platform-abstraction-design.md +1 -1
- package/docs/qq-usage.md +6 -6
- package/docs/screenshots/remote-auth-login-light.jpg +0 -0
- package/docs/screenshots/remote-auth-login.jpg +0 -0
- package/docs/wechat-usage.md +1 -1
- package/lib/auth/brand-assets.js +131 -0
- package/lib/auth/login-template.js +503 -361
- package/lib/auth/manager.js +47 -12
- package/lib/auth/theme-tokens.js +142 -0
- package/lib/bridge-rpc.js +2 -2
- package/lib/cloudflared-manager.mjs +1 -1
- package/lib/feishu/gateway.js +17 -17
- package/lib/feishu/index.js +3 -3
- package/lib/feishu/lark-bundled.mjs +288 -469
- package/lib/feishu/node.js +7 -7
- package/lib/gateway/cert.js +1 -1
- package/lib/index.js +210 -80
- package/lib/paths.js +43 -14
- package/lib/platform/base.js +2 -2
- package/lib/platform/conversation-bridge.js +17 -17
- package/lib/platform/index.js +1 -1
- package/lib/platform/manager.js +4 -4
- package/lib/qq/gateway.js +10 -10
- package/lib/qq/index.js +11 -11
- package/lib/qq/node.js +17 -17
- package/lib/telegram/gateway.js +10 -10
- package/lib/telegram/index.js +3 -3
- package/lib/telegram/node.js +5 -5
- package/lib/wechat/gateway.js +6 -6
- package/lib/wechat/index.js +4 -4
- package/lib/wechat/media.js +1 -1
- package/lib/wechat/node.js +13 -13
- package/package.json +6 -3
package/lib/feishu/node.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bridge Feishu / Lark conversation node
|
|
1
|
+
// dsh-bridge-gateway Feishu / Lark conversation node
|
|
2
2
|
// 把飞书 OpenAPI / WebSocket 长连接入站事件解析后交给平台无关的
|
|
3
3
|
// ConversationBridge 处理,出站通过 FeishuGateway 发送文本 / 卡片。
|
|
4
4
|
|
|
@@ -117,7 +117,7 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
if (!text && mediaFiles.length === 0) {
|
|
120
|
-
this.logger.debug?.(`[dsh-bridge feishu] ignore empty message from ${peerId}`)
|
|
120
|
+
this.logger.debug?.(`[dsh-bridge-gateway feishu] ignore empty message from ${peerId}`)
|
|
121
121
|
return
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -127,7 +127,7 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
127
127
|
fullText = fullText ? `${fullText}\n\n${mediaDesc}` : mediaDesc
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
this.logger.debug?.(`[dsh-bridge feishu] handling inbound message from ${peerId} (group=${isGroup}): ${fullText}`)
|
|
130
|
+
this.logger.debug?.(`[dsh-bridge-gateway feishu] handling inbound message from ${peerId} (group=${isGroup}): ${fullText}`)
|
|
131
131
|
|
|
132
132
|
// 如果还没有活动会话,且是首条消息,自动尝试选择最新已有会话
|
|
133
133
|
if (!this.activeSessionId) {
|
|
@@ -162,10 +162,10 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
162
162
|
const filePath = path.join(mediaDir, safeName)
|
|
163
163
|
await fs.promises.writeFile(filePath, buf)
|
|
164
164
|
downloaded.push({ filename: safeName, path: filePath, size: buf.length })
|
|
165
|
-
this.logger.info?.(`[dsh-bridge feishu] downloaded media ${safeName} (${buf.length} bytes)`)
|
|
165
|
+
this.logger.info?.(`[dsh-bridge-gateway feishu] downloaded media ${safeName} (${buf.length} bytes)`)
|
|
166
166
|
}
|
|
167
167
|
} catch (err) {
|
|
168
|
-
this.logger.warn?.(`[dsh-bridge feishu] download media failed: ${err.message}`)
|
|
168
|
+
this.logger.warn?.(`[dsh-bridge-gateway feishu] download media failed: ${err.message}`)
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
return downloaded
|
|
@@ -207,7 +207,7 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
207
207
|
this._streamCardId = res?.message_id || null
|
|
208
208
|
this._streamContent = firstSlice
|
|
209
209
|
} catch (err) {
|
|
210
|
-
this.logger.warn?.('[dsh-bridge feishu] sendMarkdownCard failed, fallback to sendText:', err?.message ?? err)
|
|
210
|
+
this.logger.warn?.('[dsh-bridge-gateway feishu] sendMarkdownCard failed, fallback to sendText:', err?.message ?? err)
|
|
211
211
|
await this.gateway.sendText(peerId, content, { isGroup })
|
|
212
212
|
return
|
|
213
213
|
}
|
|
@@ -239,7 +239,7 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
} catch (err) {
|
|
242
|
-
this.logger.warn?.('[dsh-bridge feishu] sendMarkdownCard failed, fallback to plain text:', err?.message ?? err)
|
|
242
|
+
this.logger.warn?.('[dsh-bridge-gateway feishu] sendMarkdownCard failed, fallback to plain text:', err?.message ?? err)
|
|
243
243
|
await this.gateway.sendText(peerId, content, { isGroup })
|
|
244
244
|
}
|
|
245
245
|
}
|
package/lib/gateway/cert.js
CHANGED
|
@@ -208,7 +208,7 @@ export async function getOrCreateSelfSignedCert({ certFile, keyFile, logger }) {
|
|
|
208
208
|
writeFile(keyFile, key, 'utf8'),
|
|
209
209
|
]);
|
|
210
210
|
} catch (err) {
|
|
211
|
-
logger?.warn?.('dsh-bridge: 无法持久化自签证书: %s', err?.message ?? err);
|
|
211
|
+
logger?.warn?.('dsh-bridge-gateway: 无法持久化自签证书: %s', err?.message ?? err);
|
|
212
212
|
}
|
|
213
213
|
return { cert, key };
|
|
214
214
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bridge 主插件(Host)
|
|
1
|
+
// dsh-bridge-gateway 主插件(Host)
|
|
2
2
|
//
|
|
3
3
|
// 多渠道访问桥:
|
|
4
4
|
// 1. 局域网访问代理(自动启动,零配置)
|
|
@@ -25,6 +25,7 @@ import { FeishuService } from './feishu/index.js';
|
|
|
25
25
|
import { TelegramService } from './telegram/index.js';
|
|
26
26
|
import { AuthManager } from './auth/manager.js';
|
|
27
27
|
import { renderLoginPage } from './auth/login-template.js';
|
|
28
|
+
import { pwaIconSvg } from './auth/brand-assets.js';
|
|
28
29
|
import { isSafeWorkspacePath, isSensitiveFolderName } from './security/path-validator.js';
|
|
29
30
|
import { applyRestoredPlatformConfig, RESTORED_STRING_FIELDS, PLATFORM_TIMING_FIELDS } from './platform/config-restore.js';
|
|
30
31
|
import {
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
certDir as certDirFor,
|
|
35
36
|
migrateLegacyDataDir,
|
|
36
37
|
migrateLegacyCloudflaredDir,
|
|
38
|
+
cloudflaredBinDirs,
|
|
37
39
|
} from './paths.js';
|
|
38
40
|
import { getOrCreateSelfSignedCert } from './gateway/cert.js';
|
|
39
41
|
|
|
@@ -235,6 +237,11 @@ class QrCache {
|
|
|
235
237
|
|
|
236
238
|
/**
|
|
237
239
|
* PWA Web App Manifest 与 App 启动图标
|
|
240
|
+
*
|
|
241
|
+
* 图标与配色统一到 DSH 本体:此前这里是一个蓝色机器人脸(#4f6ef7),而登录页是
|
|
242
|
+
* 绿色盾牌、DSH 本体是鲸鱼 —— 手机用户把网关加到主屏后,会经历「机器人图标 →
|
|
243
|
+
* 绿色盾牌页 → 鲸鱼 DSH」三方品牌跳变。现在三处同源同一枚鲸鱼(brand-assets.js),
|
|
244
|
+
* 底色用 DSH 暗色画布 --dsw-alias-bg-base = #151517。
|
|
238
245
|
*/
|
|
239
246
|
const PWA_MANIFEST = JSON.stringify({
|
|
240
247
|
name: 'DeepSeek Harness',
|
|
@@ -242,34 +249,52 @@ const PWA_MANIFEST = JSON.stringify({
|
|
|
242
249
|
description: 'DeepSeek Harness Remote & Mobile Workspace',
|
|
243
250
|
start_url: '/',
|
|
244
251
|
display: 'standalone',
|
|
245
|
-
background_color: '#
|
|
246
|
-
theme_color: '#
|
|
252
|
+
background_color: '#151517',
|
|
253
|
+
theme_color: '#151517',
|
|
247
254
|
orientation: 'any',
|
|
248
255
|
icons: [
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
256
|
+
// PNG 是主屏图标的主力:HarmonyOS 与多数 Android 启动器**不支持 SVG 图标**,
|
|
257
|
+
// 只声明 SVG 会导致"添加到桌面"后图标一片空白(2026-10-16 华为 Mate 80 实测)。
|
|
258
|
+
// any 与 maskable 分开声明:maskable 必须满幅,启动器会按自己的形状裁切。
|
|
259
|
+
{ src: '/__dsh_bridge__/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
|
260
|
+
{ src: '/__dsh_bridge__/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
|
261
|
+
{ src: '/__dsh_bridge__/icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' },
|
|
262
|
+
{ src: '/__dsh_bridge__/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
|
263
|
+
{ src: '/__dsh_bridge__/pwa-icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }
|
|
255
264
|
]
|
|
256
265
|
}, null, 2);
|
|
257
266
|
|
|
258
|
-
const PWA_ICON_SVG =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
const PWA_ICON_SVG = pwaIconSvg();
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 主屏图标的 PNG 路由表。文件由 scripts/generate-icons.mjs 生成并入库(assets/)。
|
|
271
|
+
* 运行期只读文件、不做图形转换。
|
|
272
|
+
*/
|
|
273
|
+
const PWA_ICON_ROUTES = {
|
|
274
|
+
'/__dsh_bridge__/icon-192.png': 'icon-192.png',
|
|
275
|
+
'/__dsh_bridge__/icon-512.png': 'icon-512.png',
|
|
276
|
+
'/__dsh_bridge__/icon-maskable-192.png': 'icon-maskable-192.png',
|
|
277
|
+
'/__dsh_bridge__/icon-maskable-512.png': 'icon-maskable-512.png',
|
|
278
|
+
// iOS 的约定路径:必须是 PNG(iOS 自己加圆角,所以用圆角版即可)
|
|
279
|
+
'/apple-touch-icon.png': 'apple-touch-icon-180.png',
|
|
280
|
+
'/apple-touch-icon-precomposed.png': 'apple-touch-icon-180.png',
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** assets 目录(图标 PNG 所在)。 */
|
|
284
|
+
const PWA_ASSETS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets');
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 读取一个图标 PNG。缺失时返回 null(说明没跑过 generate-icons),由调用方降级。
|
|
288
|
+
* @param {string} fileName assets 下的文件名
|
|
289
|
+
* @returns {Buffer|null}
|
|
290
|
+
*/
|
|
291
|
+
function readPwaIconPng(fileName) {
|
|
292
|
+
try {
|
|
293
|
+
return readFileSync(join(PWA_ASSETS_DIR, fileName));
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
273
298
|
|
|
274
299
|
|
|
275
300
|
function encodeBase64Url(value) {
|
|
@@ -349,6 +374,58 @@ function normalizeClientIp(raw) {
|
|
|
349
374
|
return ip.startsWith('::ffff:') ? ip.slice(7) : ip;
|
|
350
375
|
}
|
|
351
376
|
|
|
377
|
+
/**
|
|
378
|
+
* 判定 TCP 对端是否属于私网地址段(**不含环回**)。
|
|
379
|
+
*
|
|
380
|
+
* 用途:与 `auth.genuineLocal` 组合,决定登录页是否展示「忘记密码」救急指引
|
|
381
|
+
* (其中含宿主内部路径 ~/.dsh/dsh-bridge-gateway/reset-auth)。该指引对能登上
|
|
382
|
+
* 宿主 shell 的人有用,对匿名公网访客只是不必要的信息披露。
|
|
383
|
+
*
|
|
384
|
+
* 为什么环回不在这里判:TCP 对端是 127.0.0.1 有两种截然不同的情形 ——
|
|
385
|
+
* ① 真的在宿主机上打开 127.0.0.1(应展示)
|
|
386
|
+
* ② 经隧道或同机反向代理转发的公网流量(**绝不能展示**)
|
|
387
|
+
* 二者只能靠 Host 头与隧道标头区分,那段判定在 AuthManager.verifyRequest 里
|
|
388
|
+
* 已经做过(`genuineLocal`),这里只负责"非环回的私网直连"这一种情形。
|
|
389
|
+
*
|
|
390
|
+
* 注意:这是**展示用**判定,不参与任何鉴权决策。
|
|
391
|
+
*
|
|
392
|
+
* @param {string} rawIp 原始 remoteAddress(可能带 ::ffff: 前缀)
|
|
393
|
+
* @returns {boolean}
|
|
394
|
+
*/
|
|
395
|
+
function isPrivateLanAddress(rawIp) {
|
|
396
|
+
const ip = normalizeClientIp(rawIp);
|
|
397
|
+
if (!ip) return false;
|
|
398
|
+
// IPv6 唯一本地地址 fc00::/7 与链路本地 fe80::/10
|
|
399
|
+
if (/^f[cd][0-9a-f]{2}:/i.test(ip) || /^fe[89ab][0-9a-f]:/i.test(ip)) return true;
|
|
400
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip);
|
|
401
|
+
if (!m) return false;
|
|
402
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
403
|
+
if (a === 10) return true; // 10.0.0.0/8
|
|
404
|
+
if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
|
|
405
|
+
if (a === 192 && b === 168) return true; // 192.168.0.0/16
|
|
406
|
+
if (a === 169 && b === 254) return true; // 169.254.0.0/16 链路本地
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 组合出登录页的 `isLocal`(是否本机/局域网来源)—— **纯展示用,不参与鉴权**。
|
|
412
|
+
*
|
|
413
|
+
* 两个来源缺一不可:
|
|
414
|
+
* - `auth.genuineLocal`:AuthManager 判定的"真·宿主机物理访问"
|
|
415
|
+
* (环回 + 非隧道 + Host 指向本机)。隧道与同机反向代理虽然 TCP 对端也是
|
|
416
|
+
* 127.0.0.1,但这里会判为 false —— 这正是不能只用 IP 判断的原因。
|
|
417
|
+
* - `isPrivateLanAddress`:非环回的私网直连(192.168/10/172.16-31)。
|
|
418
|
+
*
|
|
419
|
+
* 单独成函数是为了可单测:这里判错会导致向匿名公网访客泄露宿主内部路径。
|
|
420
|
+
*
|
|
421
|
+
* @param {{genuineLocal?: boolean}} auth verifyRequest 的返回值
|
|
422
|
+
* @param {string} remoteAddress TCP 对端地址
|
|
423
|
+
* @returns {boolean}
|
|
424
|
+
*/
|
|
425
|
+
function resolveLoginPageIsLocal(auth, remoteAddress) {
|
|
426
|
+
return Boolean(auth?.genuineLocal) || isPrivateLanAddress(remoteAddress);
|
|
427
|
+
}
|
|
428
|
+
|
|
352
429
|
/** 网关数据目录(与 config.json 同处) */
|
|
353
430
|
function gatewayDataDir() {
|
|
354
431
|
return dataDir();
|
|
@@ -430,11 +507,26 @@ class ProxyServer {
|
|
|
430
507
|
res.end(PWA_MANIFEST);
|
|
431
508
|
return;
|
|
432
509
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
510
|
+
// 主屏图标:PNG 优先(HarmonyOS / Android / iOS 都只认 PNG),
|
|
511
|
+
// 资源缺失时降级到内联 SVG,至少不是 404。
|
|
512
|
+
if (PWA_ICON_ROUTES[pathname]) {
|
|
513
|
+
const png = readPwaIconPng(PWA_ICON_ROUTES[pathname]);
|
|
514
|
+
if (png) {
|
|
515
|
+
res.writeHead(200, {
|
|
516
|
+
'Content-Type': 'image/png',
|
|
517
|
+
'Content-Length': png.length,
|
|
518
|
+
'Cache-Control': 'public, max-age=86400',
|
|
519
|
+
'Access-Control-Allow-Origin': '*',
|
|
520
|
+
});
|
|
521
|
+
res.end(png);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (pathname === '/__dsh_bridge__/pwa-icon.svg') {
|
|
526
|
+
res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
|
|
527
|
+
res.end(PWA_ICON_SVG);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
438
530
|
|
|
439
531
|
// 1. 处理登录 API: POST /__dsh_bridge__/login
|
|
440
532
|
if (pathname === '/__dsh_bridge__/login' && req.method === 'POST') {
|
|
@@ -558,13 +650,47 @@ class ProxyServer {
|
|
|
558
650
|
const isHtml = accept.includes('text/html') || (!req.url.startsWith('/api/') && !req.url.includes('.'));
|
|
559
651
|
if (isHtml) {
|
|
560
652
|
const clientIp = req.socket?.remoteAddress || '';
|
|
561
|
-
|
|
653
|
+
// 锁定状态只查一次:isIpBlocked() 与 lockRemainingMs() 语义等价
|
|
654
|
+
// (后者顺带清理已过期记录),分两次调用会在"锁定刚过期"的瞬间出现竞态
|
|
655
|
+
// —— 拿到 locked=true 却拿到剩余 0,页面会显示一个凭空的倒计时。
|
|
656
|
+
const lockRemainingMs = this.authManager?.lockRemainingMs(clientIp) ?? 0;
|
|
657
|
+
const isLocked = lockRemainingMs > 0;
|
|
658
|
+
// 认证模式与防护范围透传给模板:token_only 下不再渲染必然失败的密码框,
|
|
659
|
+
// 副标题也能说清"这次为什么要求认证"。
|
|
660
|
+
const publicStatus = this.authManager?.getPublicStatus() ?? {};
|
|
562
661
|
const html = renderLoginPage({
|
|
563
|
-
|
|
662
|
+
mode: publicStatus.mode,
|
|
663
|
+
scope: publicStatus.scope,
|
|
664
|
+
hasPassword: publicStatus.hasPassword,
|
|
564
665
|
locked: isLocked,
|
|
565
|
-
|
|
666
|
+
lockRemainingMs,
|
|
667
|
+
isLocal: resolveLoginPageIsLocal(auth, clientIp),
|
|
668
|
+
version: VERSION,
|
|
669
|
+
});
|
|
670
|
+
// 登录页内嵌错误文案、锁定态与认证模式,必须禁缓存 ——
|
|
671
|
+
// 与本仓库对代理 DSH HTML 的处理保持一致(见下方 index.html 注入处)。
|
|
672
|
+
//
|
|
673
|
+
// CSP 是纵深防御:本页全部资源都是内联的(零外部请求,有测试钉死),
|
|
674
|
+
// 因此可以收到极紧。**connect-src 'self' 不能漏** —— 登录靠
|
|
675
|
+
// fetch('/__dsh_bridge__/login') 提交,default-src 'none' 下漏掉它会
|
|
676
|
+
// 直接让认证失效。form-action 'none' 兜住"JS 不可用时原生提交"这条路径。
|
|
677
|
+
res.writeHead(401, {
|
|
678
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
679
|
+
'Cache-Control': 'no-store, must-revalidate',
|
|
680
|
+
'Pragma': 'no-cache',
|
|
681
|
+
'X-Content-Type-Options': 'nosniff',
|
|
682
|
+
'Referrer-Policy': 'no-referrer',
|
|
683
|
+
'Content-Security-Policy': [
|
|
684
|
+
"default-src 'none'",
|
|
685
|
+
"style-src 'unsafe-inline'",
|
|
686
|
+
"script-src 'unsafe-inline'",
|
|
687
|
+
"connect-src 'self'",
|
|
688
|
+
"img-src 'self' data:",
|
|
689
|
+
"form-action 'none'",
|
|
690
|
+
"base-uri 'none'",
|
|
691
|
+
"frame-ancestors 'none'",
|
|
692
|
+
].join('; '),
|
|
566
693
|
});
|
|
567
|
-
res.writeHead(401, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
568
694
|
res.end(html);
|
|
569
695
|
return;
|
|
570
696
|
} else {
|
|
@@ -612,7 +738,7 @@ class ProxyServer {
|
|
|
612
738
|
if (!res.headersSent) {
|
|
613
739
|
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
614
740
|
res.end(
|
|
615
|
-
`dsh-bridge: 无法连接插件 host 服务 (127.0.0.1:${extra.targetPort}) — ${err.message}`
|
|
741
|
+
`dsh-bridge-gateway: 无法连接插件 host 服务 (127.0.0.1:${extra.targetPort}) — ${err.message}`
|
|
616
742
|
);
|
|
617
743
|
} else {
|
|
618
744
|
res.destroy();
|
|
@@ -650,7 +776,7 @@ class ProxyServer {
|
|
|
650
776
|
proxyReq.on('error', (err) => {
|
|
651
777
|
this.logger.error('代理请求失败: %s', err.message);
|
|
652
778
|
if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
653
|
-
res.end(`dsh-bridge: 无法连接 dsh web (127.0.0.1:${this.targetPort}) — ${err.message}`);
|
|
779
|
+
res.end(`dsh-bridge-gateway: 无法连接 dsh web (127.0.0.1:${this.targetPort}) — ${err.message}`);
|
|
654
780
|
});
|
|
655
781
|
req.pipe(proxyReq);
|
|
656
782
|
};
|
|
@@ -778,7 +904,7 @@ class ProxyServer {
|
|
|
778
904
|
this.server.once('error', reject);
|
|
779
905
|
this.server.listen(this.localPort, '0.0.0.0', () => {
|
|
780
906
|
const proto = this.tls ? 'https' : 'http';
|
|
781
|
-
this.logger.info('dsh-bridge: %s代理已启动 %s://0.0.0.0:%d -> 127.0.0.1:%d', this.tag, proto, this.localPort, this.targetPort);
|
|
907
|
+
this.logger.info('dsh-bridge-gateway: %s代理已启动 %s://0.0.0.0:%d -> 127.0.0.1:%d', this.tag, proto, this.localPort, this.targetPort);
|
|
782
908
|
resolve();
|
|
783
909
|
});
|
|
784
910
|
});
|
|
@@ -823,7 +949,7 @@ class ProxyServer {
|
|
|
823
949
|
const conn = this.wsSockets.get(socket);
|
|
824
950
|
if (conn) conn.closeReason = reason;
|
|
825
951
|
this.logger?.warn?.(
|
|
826
|
-
'dsh-bridge: %s回收外网长连接 %s(%s,静默 %ds,鉴权方式 %s)',
|
|
952
|
+
'dsh-bridge-gateway: %s回收外网长连接 %s(%s,静默 %ds,鉴权方式 %s)',
|
|
827
953
|
this.tag, conn?.ip ?? '未知', reason, Math.round((idleMs ?? 0) / 1000), conn?.via ?? '未知',
|
|
828
954
|
);
|
|
829
955
|
try { socket.destroy(); } catch {}
|
|
@@ -877,7 +1003,7 @@ class ProxyServer {
|
|
|
877
1003
|
kicked += 1;
|
|
878
1004
|
}
|
|
879
1005
|
}
|
|
880
|
-
this.logger?.warn?.(`dsh-bridge: 管理员强制断开 ${target} 的 ${kicked} 条连接`);
|
|
1006
|
+
this.logger?.warn?.(`dsh-bridge-gateway: 管理员强制断开 ${target} 的 ${kicked} 条连接`);
|
|
881
1007
|
return { kicked };
|
|
882
1008
|
}
|
|
883
1009
|
|
|
@@ -1214,14 +1340,18 @@ class BridgeService {
|
|
|
1214
1340
|
await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
|
|
1215
1341
|
}
|
|
1216
1342
|
|
|
1217
|
-
// 重置 Cloudflare 隧道:关闭隧道 + 删除已下载的 cloudflared
|
|
1343
|
+
// 重置 Cloudflare 隧道:关闭隧道 + 删除已下载的 cloudflared 二进制。
|
|
1344
|
+
// 当前目录与全部历史目录都要删 —— 迁移失败时二进制可能留在旧目录里,
|
|
1345
|
+
// 只删当前目录会让"重置"看起来没生效(下次仍从旧目录捡到那个二进制)。
|
|
1346
|
+
// 目录一律取自 lib/paths.js,不在这里硬编码字面量(这正是 paths.js 存在的理由)。
|
|
1218
1347
|
async resetCloudflared() {
|
|
1219
1348
|
await this.stopCloudflared();
|
|
1220
|
-
const
|
|
1349
|
+
const binDirs = this.home ? [join(this.home, 'bin')] : cloudflaredBinDirs();
|
|
1221
1350
|
const candidates = ['cloudflared.exe', 'cloudflared'];
|
|
1222
|
-
for (const
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1351
|
+
for (const binDir of binDirs) {
|
|
1352
|
+
for (const binName of candidates) {
|
|
1353
|
+
try { await unlink(join(binDir, binName)); } catch {}
|
|
1354
|
+
}
|
|
1225
1355
|
}
|
|
1226
1356
|
this.cloudflaredState = { phase: 'idle', detail: '' };
|
|
1227
1357
|
}
|
|
@@ -1436,7 +1566,7 @@ class BridgeService {
|
|
|
1436
1566
|
});
|
|
1437
1567
|
|
|
1438
1568
|
await this.gateway.start();
|
|
1439
|
-
this.logger?.info?.('dsh-bridge: 公网直连网关已启用 0.0.0.0:%d (HTTPS + 强制登录门禁)', port);
|
|
1569
|
+
this.logger?.info?.('dsh-bridge-gateway: 公网直连网关已启用 0.0.0.0:%d (HTTPS + 强制登录门禁)', port);
|
|
1440
1570
|
return this.gateway;
|
|
1441
1571
|
}
|
|
1442
1572
|
|
|
@@ -2147,7 +2277,7 @@ function apply(ctx, config = {}) {
|
|
|
2147
2277
|
'<meta name="theme-color" content="#1e1e2e">',
|
|
2148
2278
|
'<link rel="manifest" href="/manifest.webmanifest">',
|
|
2149
2279
|
'<link rel="icon" type="image/svg+xml" href="/__dsh_bridge__/pwa-icon.svg">',
|
|
2150
|
-
'<link rel="apple-touch-icon" href="/
|
|
2280
|
+
'<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">',
|
|
2151
2281
|
].join(''),
|
|
2152
2282
|
},
|
|
2153
2283
|
{
|
|
@@ -2180,24 +2310,24 @@ function apply(ctx, config = {}) {
|
|
|
2180
2310
|
try {
|
|
2181
2311
|
await ctx.loader.remove(id);
|
|
2182
2312
|
} catch (err) {
|
|
2183
|
-
logger.warn('dsh-bridge: unmount %s failed: %s', id, err?.message ?? err);
|
|
2313
|
+
logger.warn('dsh-bridge-gateway: unmount %s failed: %s', id, err?.message ?? err);
|
|
2184
2314
|
}
|
|
2185
2315
|
}
|
|
2186
2316
|
};
|
|
2187
2317
|
try {
|
|
2188
|
-
logger.info('dsh-bridge: mounting directory picker (browse backend + web surface)');
|
|
2318
|
+
logger.info('dsh-bridge-gateway: mounting directory picker (browse backend + web surface)');
|
|
2189
2319
|
for (const name of [DIRECTORY_PICKER_BACKEND, DIRECTORY_PICKER_SURFACE]) {
|
|
2190
2320
|
ids.push(await ctx.loader.create({ name }));
|
|
2191
2321
|
}
|
|
2192
2322
|
} catch (err) {
|
|
2193
|
-
logger.error('dsh-bridge: directory picker mount failed: %s', err?.message ?? err);
|
|
2323
|
+
logger.error('dsh-bridge-gateway: directory picker mount failed: %s', err?.message ?? err);
|
|
2194
2324
|
await unmount();
|
|
2195
2325
|
return () => {};
|
|
2196
2326
|
}
|
|
2197
2327
|
return unmount;
|
|
2198
|
-
}, 'dsh-bridge: directory picker (browse backend + surface)');
|
|
2328
|
+
}, 'dsh-bridge-gateway: directory picker (browse backend + surface)');
|
|
2199
2329
|
} else {
|
|
2200
|
-
logger.warn('dsh-bridge: loader unavailable, directory picker left to DSH native composition');
|
|
2330
|
+
logger.warn('dsh-bridge-gateway: loader unavailable, directory picker left to DSH native composition');
|
|
2201
2331
|
}
|
|
2202
2332
|
|
|
2203
2333
|
const proxyPort = config.port ?? 3082;
|
|
@@ -2262,7 +2392,7 @@ function apply(ctx, config = {}) {
|
|
|
2262
2392
|
const stored = await loadConfig();
|
|
2263
2393
|
delete stored.auth;
|
|
2264
2394
|
await saveConfig(stored);
|
|
2265
|
-
logger.warn('dsh-bridge: [保命救急] 检测到 reset-auth 标记文件,已成功重置所有访问密码与安全策略!');
|
|
2395
|
+
logger.warn('dsh-bridge-gateway: [保命救急] 检测到 reset-auth 标记文件,已成功重置所有访问密码与安全策略!');
|
|
2266
2396
|
} catch {}
|
|
2267
2397
|
}
|
|
2268
2398
|
|
|
@@ -2279,7 +2409,7 @@ function apply(ctx, config = {}) {
|
|
|
2279
2409
|
if (stored.auth.adminPasswordSalt) authManager.adminPasswordSalt = stored.auth.adminPasswordSalt;
|
|
2280
2410
|
if (stored.auth.secretToken) authManager.secretToken = stored.auth.secretToken;
|
|
2281
2411
|
if (stored.auth.allowLoopback != null) authManager.allowLoopback = Boolean(stored.auth.allowLoopback);
|
|
2282
|
-
logger.info('dsh-bridge: loaded saved auth config (enabled=%s, mode=%s, adminPolicy=%s)', authManager.enabled, authManager.mode, authManager.adminPolicy);
|
|
2412
|
+
logger.info('dsh-bridge-gateway: loaded saved auth config (enabled=%s, mode=%s, adminPolicy=%s)', authManager.enabled, authManager.mode, authManager.adminPolicy);
|
|
2283
2413
|
}
|
|
2284
2414
|
}).catch(() => {});
|
|
2285
2415
|
|
|
@@ -2305,43 +2435,43 @@ function apply(ctx, config = {}) {
|
|
|
2305
2435
|
loadConfig().then(async (stored) => {
|
|
2306
2436
|
if (stored?.lan?.selectedIp) {
|
|
2307
2437
|
service.selectedLanIp = stored.lan.selectedIp;
|
|
2308
|
-
logger.info('dsh-bridge: loaded saved lan config (selectedIp=%s)', service.selectedLanIp);
|
|
2438
|
+
logger.info('dsh-bridge-gateway: loaded saved lan config (selectedIp=%s)', service.selectedLanIp);
|
|
2309
2439
|
}
|
|
2310
2440
|
if (stored?.gateway) {
|
|
2311
2441
|
service.gatewayConfig = stored.gateway;
|
|
2312
|
-
logger.info('dsh-bridge: loaded saved gateway config (port=%s, autoStart=%s)', service.gatewayConfig.port ?? 7443, Boolean(service.gatewayConfig.autoStart));
|
|
2442
|
+
logger.info('dsh-bridge-gateway: loaded saved gateway config (port=%s, autoStart=%s)', service.gatewayConfig.port ?? 7443, Boolean(service.gatewayConfig.autoStart));
|
|
2313
2443
|
if (service.gatewayConfig.autoStart) {
|
|
2314
|
-
logger.info('dsh-bridge: auto-starting public direct gateway...');
|
|
2444
|
+
logger.info('dsh-bridge-gateway: auto-starting public direct gateway...');
|
|
2315
2445
|
service.startGateway().catch((err) => {
|
|
2316
|
-
logger.error('dsh-bridge: gateway auto-start failed: %s', err?.message ?? err);
|
|
2446
|
+
logger.error('dsh-bridge-gateway: gateway auto-start failed: %s', err?.message ?? err);
|
|
2317
2447
|
});
|
|
2318
2448
|
}
|
|
2319
2449
|
}
|
|
2320
2450
|
if (stored?.cloudflared) {
|
|
2321
2451
|
service.cloudflaredConfig = stored.cloudflared;
|
|
2322
|
-
logger.info('dsh-bridge: loaded saved cloudflared config (autoStart=%s, tokenConfigured=%s)', Boolean(service.cloudflaredConfig.autoStart), Boolean(service.cloudflaredConfig.token));
|
|
2452
|
+
logger.info('dsh-bridge-gateway: loaded saved cloudflared config (autoStart=%s, tokenConfigured=%s)', Boolean(service.cloudflaredConfig.autoStart), Boolean(service.cloudflaredConfig.token));
|
|
2323
2453
|
if (service.cloudflaredConfig.autoStart) {
|
|
2324
|
-
logger.info('dsh-bridge: auto-starting cloudflared tunnel...');
|
|
2454
|
+
logger.info('dsh-bridge-gateway: auto-starting cloudflared tunnel...');
|
|
2325
2455
|
service.startCloudflared({ autoStart: true }).catch((err) => {
|
|
2326
|
-
logger.error('dsh-bridge: cloudflared auto-start failed: %s', err?.message ?? err);
|
|
2456
|
+
logger.error('dsh-bridge-gateway: cloudflared auto-start failed: %s', err?.message ?? err);
|
|
2327
2457
|
});
|
|
2328
2458
|
}
|
|
2329
2459
|
}
|
|
2330
2460
|
|
|
2331
2461
|
if (stored?.customTunnel) {
|
|
2332
2462
|
service.customTunnelConfig = stored.customTunnel;
|
|
2333
|
-
logger.info('dsh-bridge: loaded saved custom tunnel config (autoStart=%s)', Boolean(service.customTunnelConfig.autoStart));
|
|
2463
|
+
logger.info('dsh-bridge-gateway: loaded saved custom tunnel config (autoStart=%s)', Boolean(service.customTunnelConfig.autoStart));
|
|
2334
2464
|
if (service.customTunnelConfig.autoStart && service.customTunnelConfig.serverUrl) {
|
|
2335
|
-
logger.info('dsh-bridge: auto-starting custom tunnel...');
|
|
2465
|
+
logger.info('dsh-bridge-gateway: auto-starting custom tunnel...');
|
|
2336
2466
|
service.startCustomTunnel({ autoStart: true }).catch((err) => {
|
|
2337
|
-
logger.error('dsh-bridge: custom tunnel auto-start failed: %s', err?.message ?? err);
|
|
2467
|
+
logger.error('dsh-bridge-gateway: custom tunnel auto-start failed: %s', err?.message ?? err);
|
|
2338
2468
|
});
|
|
2339
2469
|
}
|
|
2340
2470
|
}
|
|
2341
2471
|
|
|
2342
2472
|
if (stored?.tailscale?.url) {
|
|
2343
2473
|
service.tailscaleConfig = stored.tailscale;
|
|
2344
|
-
logger.info('dsh-bridge: loaded saved tailscale url (%s)', service.tailscaleConfig.url);
|
|
2474
|
+
logger.info('dsh-bridge-gateway: loaded saved tailscale url (%s)', service.tailscaleConfig.url);
|
|
2345
2475
|
}
|
|
2346
2476
|
}).catch(() => {});
|
|
2347
2477
|
|
|
@@ -2353,7 +2483,7 @@ function apply(ctx, config = {}) {
|
|
|
2353
2483
|
const timer = setInterval(() => { void service.refreshTailnet(); }, TAILNET_TTL_MS);
|
|
2354
2484
|
if (timer.unref) timer.unref();
|
|
2355
2485
|
return () => clearInterval(timer);
|
|
2356
|
-
}, 'dsh-bridge: tailnet probe refresh');
|
|
2486
|
+
}, 'dsh-bridge-gateway: tailnet probe refresh');
|
|
2357
2487
|
|
|
2358
2488
|
// 平台管理器:注册/协调所有 IM 平台适配器
|
|
2359
2489
|
const platformManager = new PlatformManager({ logger });
|
|
@@ -2426,7 +2556,7 @@ function apply(ctx, config = {}) {
|
|
|
2426
2556
|
try {
|
|
2427
2557
|
if (cfg.activeSessionId) {
|
|
2428
2558
|
wechat.node.activeSessionId = cfg.activeSessionId;
|
|
2429
|
-
logger.info('dsh-bridge: restored wechat active session: %s', cfg.activeSessionId);
|
|
2559
|
+
logger.info('dsh-bridge-gateway: restored wechat active session: %s', cfg.activeSessionId);
|
|
2430
2560
|
} else {
|
|
2431
2561
|
await wechat.node._pickDefaultSession().catch(() => {});
|
|
2432
2562
|
}
|
|
@@ -2443,9 +2573,9 @@ function apply(ctx, config = {}) {
|
|
|
2443
2573
|
accountId: cfg.accountId,
|
|
2444
2574
|
baseUrl: cfg.baseUrl,
|
|
2445
2575
|
});
|
|
2446
|
-
logger.info('dsh-bridge: loaded saved wechat bot config, starting gateway');
|
|
2576
|
+
logger.info('dsh-bridge-gateway: loaded saved wechat bot config, starting gateway');
|
|
2447
2577
|
await wechat.start().catch((err) => {
|
|
2448
|
-
logger.error('dsh-bridge: wechat auto-start failed: %s', err?.message ?? err);
|
|
2578
|
+
logger.error('dsh-bridge-gateway: wechat auto-start failed: %s', err?.message ?? err);
|
|
2449
2579
|
});
|
|
2450
2580
|
}
|
|
2451
2581
|
}
|
|
@@ -2465,7 +2595,7 @@ function apply(ctx, config = {}) {
|
|
|
2465
2595
|
try {
|
|
2466
2596
|
if (cfg.activeSessionId) {
|
|
2467
2597
|
qq.node.activeSessionId = cfg.activeSessionId;
|
|
2468
|
-
logger.info('dsh-bridge: restored qq active session: %s', cfg.activeSessionId);
|
|
2598
|
+
logger.info('dsh-bridge-gateway: restored qq active session: %s', cfg.activeSessionId);
|
|
2469
2599
|
} else {
|
|
2470
2600
|
await qq.node._pickDefaultSession().catch(() => {});
|
|
2471
2601
|
}
|
|
@@ -2485,9 +2615,9 @@ function apply(ctx, config = {}) {
|
|
|
2485
2615
|
gatewayUrl: cfg.gatewayUrl,
|
|
2486
2616
|
accountId: cfg.accountId,
|
|
2487
2617
|
});
|
|
2488
|
-
logger.info('dsh-bridge: loaded saved qq bot config, starting gateway');
|
|
2618
|
+
logger.info('dsh-bridge-gateway: loaded saved qq bot config, starting gateway');
|
|
2489
2619
|
await qq.start().catch((err) => {
|
|
2490
|
-
logger.error('dsh-bridge: qq auto-start failed: %s', err?.message ?? err);
|
|
2620
|
+
logger.error('dsh-bridge-gateway: qq auto-start failed: %s', err?.message ?? err);
|
|
2491
2621
|
});
|
|
2492
2622
|
}
|
|
2493
2623
|
}
|
|
@@ -2507,7 +2637,7 @@ function apply(ctx, config = {}) {
|
|
|
2507
2637
|
try {
|
|
2508
2638
|
if (cfg.activeSessionId) {
|
|
2509
2639
|
feishu.node.activeSessionId = cfg.activeSessionId;
|
|
2510
|
-
logger.info('dsh-bridge: restored feishu active session: %s', cfg.activeSessionId);
|
|
2640
|
+
logger.info('dsh-bridge-gateway: restored feishu active session: %s', cfg.activeSessionId);
|
|
2511
2641
|
} else {
|
|
2512
2642
|
await feishu.node._pickDefaultSession().catch(() => {});
|
|
2513
2643
|
}
|
|
@@ -2524,9 +2654,9 @@ function apply(ctx, config = {}) {
|
|
|
2524
2654
|
appSecret: cfg.appSecret,
|
|
2525
2655
|
domain: cfg.domain || 'feishu',
|
|
2526
2656
|
});
|
|
2527
|
-
logger.info('dsh-bridge: loaded saved feishu bot config, starting gateway');
|
|
2657
|
+
logger.info('dsh-bridge-gateway: loaded saved feishu bot config, starting gateway');
|
|
2528
2658
|
await feishu.start().catch((err) => {
|
|
2529
|
-
logger.error('dsh-bridge: feishu auto-start failed: %s', err?.message ?? err);
|
|
2659
|
+
logger.error('dsh-bridge-gateway: feishu auto-start failed: %s', err?.message ?? err);
|
|
2530
2660
|
});
|
|
2531
2661
|
}
|
|
2532
2662
|
}
|
|
@@ -2546,7 +2676,7 @@ function apply(ctx, config = {}) {
|
|
|
2546
2676
|
try {
|
|
2547
2677
|
if (cfg.activeSessionId) {
|
|
2548
2678
|
telegram.node.activeSessionId = cfg.activeSessionId;
|
|
2549
|
-
logger.info('dsh-bridge: restored telegram active session: %s', cfg.activeSessionId);
|
|
2679
|
+
logger.info('dsh-bridge-gateway: restored telegram active session: %s', cfg.activeSessionId);
|
|
2550
2680
|
} else {
|
|
2551
2681
|
await telegram.node._pickDefaultSession().catch(() => {});
|
|
2552
2682
|
}
|
|
@@ -2562,9 +2692,9 @@ function apply(ctx, config = {}) {
|
|
|
2562
2692
|
botToken: cfg.botToken,
|
|
2563
2693
|
proxy: cfg.proxy || '',
|
|
2564
2694
|
});
|
|
2565
|
-
logger.info('dsh-bridge: loaded saved telegram bot config, starting gateway');
|
|
2695
|
+
logger.info('dsh-bridge-gateway: loaded saved telegram bot config, starting gateway');
|
|
2566
2696
|
await telegram.start().catch((err) => {
|
|
2567
|
-
logger.error('dsh-bridge: telegram auto-start failed: %s', err?.message ?? err);
|
|
2697
|
+
logger.error('dsh-bridge-gateway: telegram auto-start failed: %s', err?.message ?? err);
|
|
2568
2698
|
});
|
|
2569
2699
|
}
|
|
2570
2700
|
}
|
|
@@ -2670,11 +2800,11 @@ function apply(ctx, config = {}) {
|
|
|
2670
2800
|
.catch(() => false)
|
|
2671
2801
|
.then((lanAutoStart) => {
|
|
2672
2802
|
if (!lanAutoStart) {
|
|
2673
|
-
logger.info('dsh-bridge: LAN proxy auto-start disabled (default off; enable via lan.autoStart)');
|
|
2803
|
+
logger.info('dsh-bridge-gateway: LAN proxy auto-start disabled (default off; enable via lan.autoStart)');
|
|
2674
2804
|
return;
|
|
2675
2805
|
}
|
|
2676
2806
|
void service.startProxy().catch((err) => {
|
|
2677
|
-
logger.error('dsh-bridge: proxy start failed: %s', err?.message ?? err);
|
|
2807
|
+
logger.error('dsh-bridge-gateway: proxy start failed: %s', err?.message ?? err);
|
|
2678
2808
|
});
|
|
2679
2809
|
});
|
|
2680
2810
|
|
|
@@ -2687,7 +2817,7 @@ function apply(ctx, config = {}) {
|
|
|
2687
2817
|
platformManager.dispose();
|
|
2688
2818
|
authManager.dispose();
|
|
2689
2819
|
await service.dispose();
|
|
2690
|
-
}, 'dsh-bridge: stop wechat, qq, feishu, telegram, proxy, auth and tunnels');
|
|
2820
|
+
}, 'dsh-bridge-gateway: stop wechat, qq, feishu, telegram, proxy, auth and tunnels');
|
|
2691
2821
|
}
|
|
2692
2822
|
|
|
2693
|
-
export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4, listAllLanIPv4, LEGACY_BROWSER_POLYFILL };
|
|
2823
|
+
export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4, listAllLanIPv4, LEGACY_BROWSER_POLYFILL, resolveLoginPageIsLocal };
|