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.
Files changed (45) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +17 -3
  3. package/README.zh.md +18 -2
  4. package/assets/apple-touch-icon-180.png +0 -0
  5. package/assets/icon-192.png +0 -0
  6. package/assets/icon-512.png +0 -0
  7. package/assets/icon-maskable-192.png +0 -0
  8. package/assets/icon-maskable-512.png +0 -0
  9. package/client/client.js +302 -41
  10. package/client/index.js +438 -42
  11. package/docs/banner.jpg +0 -0
  12. package/docs/custom-tunnel.md +5 -5
  13. package/docs/platform-abstraction-design.md +1 -1
  14. package/docs/qq-usage.md +6 -6
  15. package/docs/screenshots/remote-auth-login-light.jpg +0 -0
  16. package/docs/screenshots/remote-auth-login.jpg +0 -0
  17. package/docs/wechat-usage.md +1 -1
  18. package/lib/auth/brand-assets.js +131 -0
  19. package/lib/auth/login-template.js +503 -361
  20. package/lib/auth/manager.js +47 -12
  21. package/lib/auth/theme-tokens.js +142 -0
  22. package/lib/bridge-rpc.js +2 -2
  23. package/lib/cloudflared-manager.mjs +1 -1
  24. package/lib/feishu/gateway.js +17 -17
  25. package/lib/feishu/index.js +3 -3
  26. package/lib/feishu/lark-bundled.mjs +288 -469
  27. package/lib/feishu/node.js +7 -7
  28. package/lib/gateway/cert.js +1 -1
  29. package/lib/index.js +210 -80
  30. package/lib/paths.js +43 -14
  31. package/lib/platform/base.js +2 -2
  32. package/lib/platform/conversation-bridge.js +17 -17
  33. package/lib/platform/index.js +1 -1
  34. package/lib/platform/manager.js +4 -4
  35. package/lib/qq/gateway.js +10 -10
  36. package/lib/qq/index.js +11 -11
  37. package/lib/qq/node.js +17 -17
  38. package/lib/telegram/gateway.js +10 -10
  39. package/lib/telegram/index.js +3 -3
  40. package/lib/telegram/node.js +5 -5
  41. package/lib/wechat/gateway.js +6 -6
  42. package/lib/wechat/index.js +4 -4
  43. package/lib/wechat/media.js +1 -1
  44. package/lib/wechat/node.js +13 -13
  45. package/package.json +6 -3
@@ -1,4 +1,4 @@
1
- // dsh-bridge Telegram platform adapter
1
+ // dsh-bridge-gateway Telegram platform adapter
2
2
  // 编排 TelegramGateway(长轮询 + 代理支持)+ TelegramConversationNode(Telegram⇄DSH 会话桥)。
3
3
  // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
4
 
@@ -45,7 +45,7 @@ export class TelegramService extends Platform {
45
45
 
46
46
  if (this.gateway.configured) {
47
47
  void this.start().catch((err) => {
48
- this.logger.error?.('[dsh-bridge telegram] start failed:', err?.message ?? err)
48
+ this.logger.error?.('[dsh-bridge-gateway telegram] start failed:', err?.message ?? err)
49
49
  })
50
50
  }
51
51
  }
@@ -203,7 +203,7 @@ export class TelegramService extends Platform {
203
203
 
204
204
  if (this.gateway.configured && (botToken !== undefined || proxy !== undefined)) {
205
205
  await this.start().catch((err) => {
206
- this.logger?.warn?.('[dsh-bridge telegram] auto-start failed: %s', err?.message ?? err)
206
+ this.logger?.warn?.('[dsh-bridge-gateway telegram] auto-start failed: %s', err?.message ?? err)
207
207
  })
208
208
  }
209
209
  return { success: true }
@@ -1,4 +1,4 @@
1
- // dsh-bridge Telegram conversation node
1
+ // dsh-bridge-gateway Telegram conversation node
2
2
  // 把 Telegram OpenAPI / 长轮询入站事件解析后交给平台无关的
3
3
  // ConversationBridge 处理,出站通过 TelegramGateway 发送文本 / Inline Keyboard 按钮 / 媒体。
4
4
 
@@ -107,7 +107,7 @@ export class TelegramConversationNode extends ConversationBridge {
107
107
  }
108
108
 
109
109
  if (!text && mediaFiles.length === 0) {
110
- this.logger.debug?.(`[dsh-bridge telegram] ignore empty message from ${chatId}`)
110
+ this.logger.debug?.(`[dsh-bridge-gateway telegram] ignore empty message from ${chatId}`)
111
111
  return
112
112
  }
113
113
 
@@ -117,7 +117,7 @@ export class TelegramConversationNode extends ConversationBridge {
117
117
  fullText = fullText ? `${fullText}\n\n${mediaDesc}` : mediaDesc
118
118
  }
119
119
 
120
- this.logger.debug?.(`[dsh-bridge telegram] handling inbound message from ${chatId} (group=${isGroup}): ${fullText}`)
120
+ this.logger.debug?.(`[dsh-bridge-gateway telegram] handling inbound message from ${chatId} (group=${isGroup}): ${fullText}`)
121
121
 
122
122
  // 如果还没有活动会话,且是首条消息,自动尝试选择最新已有会话
123
123
  if (!this.activeSessionId) {
@@ -160,7 +160,7 @@ export class TelegramConversationNode extends ConversationBridge {
160
160
  if (res) downloaded.push(res)
161
161
  }
162
162
  } catch (err) {
163
- this.logger.warn?.(`[dsh-bridge telegram] process media error: ${err.message}`)
163
+ this.logger.warn?.(`[dsh-bridge-gateway telegram] process media error: ${err.message}`)
164
164
  }
165
165
 
166
166
  return downloaded
@@ -256,7 +256,7 @@ export class TelegramConversationNode extends ConversationBridge {
256
256
  this._streamMsgId = res?.message_id || null
257
257
  this._streamContent = firstSlice
258
258
  } catch (err) {
259
- this.logger.warn?.('[dsh-bridge telegram] sendText stream initial failed:', err?.message ?? err)
259
+ this.logger.warn?.('[dsh-bridge-gateway telegram] sendText stream initial failed:', err?.message ?? err)
260
260
  await this.gateway.sendText(peerId, content).catch(() => {})
261
261
  return
262
262
  }
@@ -1,4 +1,4 @@
1
- // dsh-bridge WeChat iLink gateway
1
+ // dsh-bridge-gateway WeChat iLink gateway
2
2
  //
3
3
  // 微信 ClawBot(iLink Bot API)网关:扫码登录 + 长轮询收消息 + 发送 + typing。
4
4
  // 由 Jesse-njx/dsh-chatnode-wechat(MIT)移植精简而来,协议细节与 hermes-agent
@@ -448,7 +448,7 @@ export class WechatGateway extends Service {
448
448
  if (!text.trim()) return { success: false, error: 'empty message' }
449
449
  if (!this.configured) return { success: false, error: 'not configured' }
450
450
  let contextToken = this.contextTokens.get(to)
451
- const id = clientId ?? `dsh-bridge-wechat-${randomId()}`
451
+ const id = clientId ?? `dsh-bridge-gateway-wechat-${randomId()}`
452
452
  let lastError
453
453
  let retriedWithoutToken = false
454
454
 
@@ -575,7 +575,7 @@ export class WechatGateway extends Service {
575
575
  }) {
576
576
  if (!this.configured) return { success: false, error: 'not configured' }
577
577
  const contextToken = this.contextTokens.get(to)
578
- const id = clientId ?? `dsh-bridge-wechat-${randomId()}`
578
+ const id = clientId ?? `dsh-bridge-gateway-wechat-${randomId()}`
579
579
  const hexKey = aesKeyHex || (aesKeyBase64 ? Buffer.from(aesKeyBase64, 'base64').toString('hex') : '')
580
580
 
581
581
  // 构建媒体项(全字段兼容各端微信客户端解析)
@@ -724,7 +724,7 @@ export class WechatGateway extends Service {
724
724
  rawFileMd5,
725
725
  })
726
726
  } catch (err) {
727
- this.logger?.warn?.('[dsh-bridge wechat] sendMediaFile failed: %s', err?.message ?? err)
727
+ this.logger?.warn?.('[dsh-bridge-gateway wechat] sendMediaFile failed: %s', err?.message ?? err)
728
728
  return { success: false, error: err?.message }
729
729
  }
730
730
  }
@@ -844,7 +844,7 @@ export class WechatGateway extends Service {
844
844
  consecutiveFailures = 0
845
845
  if (batch.syncBuf) this.syncBuf = batch.syncBuf
846
846
  if (this.statusValue !== 'connected') {
847
- this.logger?.info?.('[dsh-bridge wechat] connected to iLink platform')
847
+ this.logger?.info?.('[dsh-bridge-gateway wechat] connected to iLink platform')
848
848
  }
849
849
  if (this.stopPollingLocal) break
850
850
  this.setStatus('connected')
@@ -859,7 +859,7 @@ export class WechatGateway extends Service {
859
859
  // iLink 独占锁:同 token 已有别的 poller。响亮报错并停止。
860
860
  this.setStatus('error')
861
861
  this.ctx.emit('wechat/fatal', new Error(
862
- 'iLink returned HTTP 403: another poller (hermes-agent, OpenClaw, or a duplicate dsh-bridge WeChat bot) is already polling this account. ' +
862
+ 'iLink returned HTTP 403: another poller (hermes-agent, OpenClaw, or a duplicate dsh-bridge-gateway WeChat bot) is already polling this account. ' +
863
863
  'iLink allows exactly one authenticated poller per token. Stop the other gateway or use a dedicated WeChat account.',
864
864
  ))
865
865
  fatal = true
@@ -1,4 +1,4 @@
1
- // dsh-bridge WeChat platform adapter
1
+ // dsh-bridge-gateway WeChat platform adapter
2
2
  //
3
3
  // 编排 WechatGateway(iLink 网关,注册为 ctx.wechat 服务)+ WechatConversationNode
4
4
  // (微信⇄DSH 会话桥)。作为 Platform 子类,可注册进 PlatformManager 统一管理。
@@ -52,7 +52,7 @@ export class WechatService extends Platform {
52
52
 
53
53
  if (this.gateway.configured) {
54
54
  void this.start().catch((err) => {
55
- this.logger.error('[dsh-bridge wechat] start failed: %s', err?.message ?? err)
55
+ this.logger.error('[dsh-bridge-gateway wechat] start failed: %s', err?.message ?? err)
56
56
  })
57
57
  }
58
58
  }
@@ -203,7 +203,7 @@ export class WechatService extends Platform {
203
203
  baseUrl: creds.baseUrl,
204
204
  })
205
205
  void this.gateway.start().catch((err) => {
206
- this.logger.error('[dsh-bridge wechat] login start failed: %s', err?.message ?? err)
206
+ this.logger.error('[dsh-bridge-gateway wechat] login start failed: %s', err?.message ?? err)
207
207
  })
208
208
  this.loginState = { phase: 'done', qrPayload: null, qrKind: null, error: null }
209
209
  }
@@ -213,7 +213,7 @@ export class WechatService extends Platform {
213
213
  await this.gateway.stop()
214
214
  this.gateway.setCredentials({ token: '', accountId: '', baseUrl: undefined })
215
215
  await this.persist({ token: '', accountId: '', baseUrl: '' })
216
- this.logger.info('[dsh-bridge wechat] unbound: credentials cleared')
216
+ this.logger.info('[dsh-bridge-gateway wechat] unbound: credentials cleared')
217
217
  }
218
218
 
219
219
  /** 更新白名单并发起持久化。 */
@@ -1,4 +1,4 @@
1
- // dsh-bridge WeChat media download/upload + AES-128-ECB encryption
1
+ // dsh-bridge-gateway WeChat media download/upload + AES-128-ECB encryption
2
2
  //
3
3
  // iLink 媒体项(图片/文件/语音/视频)通过 CDN 加密传输:
4
4
  // - 下载:CDN URL + encrypted_query_param → 下载密文 → AES-128-ECB 解密
@@ -1,4 +1,4 @@
1
- // dsh-bridge WeChat conversation node
1
+ // dsh-bridge-gateway WeChat conversation node
2
2
  //
3
3
  // 微信 ⇄ DSH 会话桥:把 iLink 入站消息解析后交给平台无关的 ConversationBridge 处理,
4
4
  // 出站通过 ctx.wechat 发送。负责微信协议相关的部分:
@@ -74,17 +74,17 @@ export class WechatConversationNode extends ConversationBridge {
74
74
  // 调试:记录收到的消息类型
75
75
  const items = message.item_list ?? []
76
76
  const itemTypes = items.map((it) => it.type).join(',')
77
- this.logger?.info?.(`[dsh-bridge wechat] received message from ${sender}, item_types=[${itemTypes}]`)
77
+ this.logger?.info?.(`[dsh-bridge-gateway wechat] received message from ${sender}, item_types=[${itemTypes}]`)
78
78
 
79
79
  // 快速预检查:白名单非空时,未授权发件人直接忽略(不下载媒体,防资源滥用)。
80
80
  // 白名单为空的"首条自动授权"场景交给 handleInbound 处理。
81
81
  if (!this.isAllowed(sender) && this.config.allowFrom.length > 0) {
82
- this.logger?.info?.(`[dsh-bridge wechat] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
82
+ this.logger?.info?.(`[dsh-bridge-gateway wechat] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
83
83
  return
84
84
  }
85
85
 
86
86
  // v0.2: 处理媒体项(图片/文件/语音/视频)
87
- this.logger?.info?.(`[dsh-bridge wechat] processing media items for ${sender}...`)
87
+ this.logger?.info?.(`[dsh-bridge-gateway wechat] processing media items for ${sender}...`)
88
88
  let mediaFiles = []
89
89
  let hadMediaItems = false
90
90
  let mediaError = null
@@ -94,11 +94,11 @@ export class WechatConversationNode extends ConversationBridge {
94
94
  const result = await this._processMediaItems(message, sender)
95
95
  mediaFiles = result.files
96
96
  debugInfo = result.debug
97
- this.logger?.info?.(`[dsh-bridge wechat] media processing done, got ${mediaFiles.length} files`)
97
+ this.logger?.info?.(`[dsh-bridge-gateway wechat] media processing done, got ${mediaFiles.length} files`)
98
98
  } catch (error) {
99
99
  // 尽可能捕获完整错误信息
100
100
  mediaError = error?.message || error?.toString?.() || JSON.stringify(error) || '未知错误'
101
- this.logger?.error?.(`[dsh-bridge wechat] media processing failed: ${mediaError}`, error)
101
+ this.logger?.error?.(`[dsh-bridge-gateway wechat] media processing failed: ${mediaError}`, error)
102
102
  // 媒体处理失败不阻断文本消息处理
103
103
  }
104
104
 
@@ -113,7 +113,7 @@ export class WechatConversationNode extends ConversationBridge {
113
113
  await this.sendText(`❌ **${errMsg}**`)
114
114
  return
115
115
  }
116
- this.logger?.info?.(`[dsh-bridge wechat] ignore empty message from ${sender}`)
116
+ this.logger?.info?.(`[dsh-bridge-gateway wechat] ignore empty message from ${sender}`)
117
117
  return
118
118
  }
119
119
 
@@ -144,7 +144,7 @@ export class WechatConversationNode extends ConversationBridge {
144
144
  const img = item.image_item
145
145
  if (!img) {
146
146
  const msg = `图片项缺少 image_item`
147
- this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
147
+ this.logger?.warn?.(`[dsh-bridge-gateway wechat] ${msg}`)
148
148
  debugInfo.push(msg)
149
149
  continue
150
150
  }
@@ -153,7 +153,7 @@ export class WechatConversationNode extends ConversationBridge {
153
153
  if (!aesKey) {
154
154
  const fields = Object.keys(img).join(', ')
155
155
  const msg = `图片缺 aes_key,字段: ${fields}`
156
- this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
156
+ this.logger?.warn?.(`[dsh-bridge-gateway wechat] ${msg}`)
157
157
  debugInfo.push(msg)
158
158
  continue
159
159
  }
@@ -261,7 +261,7 @@ export class WechatConversationNode extends ConversationBridge {
261
261
  }
262
262
  } catch (error) {
263
263
  const msg = `处理媒体项 type=${item.type} 失败: ${error?.message ?? error}`
264
- this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
264
+ this.logger?.warn?.(`[dsh-bridge-gateway wechat] ${msg}`)
265
265
  debugInfo.push(msg)
266
266
  }
267
267
  }
@@ -276,7 +276,7 @@ export class WechatConversationNode extends ConversationBridge {
276
276
  async _downloadMediaItem({ encryptedQueryParam, fullUrl, aesKeyBase64, sender, mediaType, filename }) {
277
277
  if (!encryptedQueryParam && !fullUrl) return null
278
278
  if (!aesKeyBase64) {
279
- this.logger?.warn?.(`[dsh-bridge wechat] media item missing aes_key, cannot decrypt`)
279
+ this.logger?.warn?.(`[dsh-bridge-gateway wechat] media item missing aes_key, cannot decrypt`)
280
280
  return null
281
281
  }
282
282
 
@@ -284,7 +284,7 @@ export class WechatConversationNode extends ConversationBridge {
284
284
  // 图片的 image_item.aeskey 可能是裸 hex(32 字符),需归一化
285
285
  const normalizedKey = normalizeAesKey(aesKeyBase64)
286
286
  if (!normalizedKey) {
287
- this.logger?.warn?.(`[dsh-bridge wechat] media item aes_key 无法解析: ${aesKeyBase64.slice(0, 16)}...`)
287
+ this.logger?.warn?.(`[dsh-bridge-gateway wechat] media item aes_key 无法解析: ${aesKeyBase64.slice(0, 16)}...`)
288
288
  return null
289
289
  }
290
290
  const plaintext = await downloadMedia({
@@ -305,7 +305,7 @@ export class WechatConversationNode extends ConversationBridge {
305
305
  const filePath = join(mediaDir, `${Date.now()}_${safeName}`)
306
306
  await writeFile(filePath, plaintext)
307
307
 
308
- this.logger?.info?.(`[dsh-bridge wechat] downloaded ${mediaType} from ${sender}: ${filePath} (${plaintext.length} bytes)`)
308
+ this.logger?.info?.(`[dsh-bridge-gateway wechat] downloaded ${mediaType} from ${sender}: ${filePath} (${plaintext.length} bytes)`)
309
309
  return { type: mediaType, path: filePath, size: plaintext.length }
310
310
  }
311
311
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "dsh-bridge-gateway",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "以 dsh-bridge 为根基的 DSH 远程访问网关:局域网 / Cloudflare 隧道 / Tailscale 隧道 / 自建隧道 / 公网直连网关 / IM Bot,全部在 Web 面板中配置。",
5
- "releaseNotes": "【dsh-bridge-gateway 0.2.2】\n• 🐛 修复旧内核浏览器(统信 UOS / 360 浏览器等 Chromium 108)打开后工作区永远加载不了:经官方 `webserver/index-inject` 注入 `Promise.withResolvers` / `AbortSignal.any` / `Array.prototype.toSorted` 三个 polyfill,全部带 `typeof` 守卫且幂等,现代浏览器上原样跳过、不覆盖原生实现\n• ✅ 新增 10 条兼容性断言(含「不得覆盖原生实现」「幂等」两条反向断言),全套 216 项测试通过",
5
+ "releaseNotes": "【dsh-bridge-gateway 0.3.0】\n• 📱 修复「滚不到最新会话」:插件给主框架注入的 height:100dvh + overflow:hidden 组合使 DSH 的布局测量算错,会话只渲染约 4 条消息(scrollHeight 996 vs 正常 11295)。去掉这两个属性组后恢复\n• 📱 移动端共修 8 项:主屏图标空白、渠道中心面板标题被顶栏盖住、会话名恒为「新会话」、轨迹视图内容被裁、顶部两层头过高、视图下拉被裁\n• 🖼️ PWA 主屏图标改为真 PNG(192/512 的 any 与 maskable + iOS 180)并用 DSH 业务蓝 #4176e6 —— 华为 HarmonyOS 与多数 Android 启动器不支持 SVG 图标,此前「添加到桌面」得到的是空白图标\n• 🎨 远程登录页改版为门厅式设计,与 DSH 本体同源同一枚鲸鱼\n• 🔍 新增真机诊断能力:?dsh-diag=1 只读指标叠加层 + ?dsh-mobile=off/no-new/no-<分组> 二分开关\n• 🔧 品牌名统一为 dsh-bridge-gateway,旧数据目录与 cloudflared 缓存目录两级链自动迁移\n• ⚠️ 已知问题:输入框仍会压住少量内容(两次修复尝试都破坏 DSH 滚动锚定,已回退),须由 DSH 侧解决\n• ✅ 269 项测试通过,新增 12 条护栏(逐条用注入 bug 验证过会变红)",
6
6
  "type": "module",
7
+ "packageManager": "pnpm@11.24.0",
7
8
  "main": "lib/index.js",
8
9
  "exports": {
9
10
  ".": {
@@ -20,12 +21,14 @@
20
21
  "README.zh.md",
21
22
  "CHANGELOG.md",
22
23
  "LICENSE",
23
- "docs"
24
+ "docs",
25
+ "assets"
24
26
  ],
25
27
  "scripts": {
26
28
  "build:client": "node client/build.mjs",
27
29
  "build:lark": "node scripts/bundle-lark.mjs",
28
30
  "build:banner": "node scripts/generate-banner.mjs",
31
+ "verify:mobile-header": "node scripts/verify-mobile-header.mjs",
29
32
  "prepack": "npm run build:lark && npm run build:client",
30
33
  "release:github": "node scripts/create-github-release.mjs",
31
34
  "test": "node --import ./test/isolate-home.mjs --test test/*.test.mjs"