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
package/lib/paths.js CHANGED
@@ -82,18 +82,32 @@ export function wechatContextTokenFile() {
82
82
 
83
83
  // ── cloudflared 二进制缓存 ────────────────────────────────────────────────
84
84
  // 与插件数据目录分开存放:它是可重新下载的外部依赖(约 39MB),不是用户数据。
85
- // 新名 ~/.dsh-bridge-cloudflared/;旧名 ~/.dsh-bridge/(上游沿用)。
86
- export const CLOUDFLARED_DIR_NAME = '.dsh-bridge-cloudflared';
87
- export const LEGACY_CLOUDFLARED_DIR_NAME = '.dsh-bridge';
85
+ //
86
+ // 这个名字改过两次,所以迁移是**两级链**:
87
+ // ~/.dsh-bridge/ ← 上游沿用名(fork 前)
88
+ // ~/.dsh-bridge-cloudflared/ ← 第一次改名后
89
+ // ~/.dsh-bridge-gateway-cloudflared/ ← 当前
90
+ // 迁移按"从新到旧"依次尝试,命中第一个存在的旧目录即迁移,避免老用户白等一次
91
+ // 39MB 重下载。
92
+ export const CLOUDFLARED_DIR_NAME = '.dsh-bridge-gateway-cloudflared';
93
+
94
+ /**
95
+ * cloudflared 缓存目录的历史名字,**从新到旧**。
96
+ * 迁移顺序即数组顺序:先找最近一次改名前的名字,再往上追。
97
+ */
98
+ export const LEGACY_CLOUDFLARED_DIR_NAMES = Object.freeze([
99
+ '.dsh-bridge-cloudflared',
100
+ '.dsh-bridge',
101
+ ]);
88
102
 
89
- /** cloudflared 缓存根目录(新名)。 */
103
+ /** cloudflared 缓存根目录(当前名)。 */
90
104
  export function cloudflaredDir() {
91
105
  return join(homedir(), CLOUDFLARED_DIR_NAME);
92
106
  }
93
107
 
94
- /** cloudflared 缓存根目录(旧名)。 */
95
- export function legacyCloudflaredDir() {
96
- return join(homedir(), LEGACY_CLOUDFLARED_DIR_NAME);
108
+ /** 全部历史缓存目录(从新到旧)。 */
109
+ export function legacyCloudflaredDirs() {
110
+ return LEGACY_CLOUDFLARED_DIR_NAMES.map((name) => join(homedir(), name));
97
111
  }
98
112
 
99
113
  /** cloudflared 二进制所在目录。 */
@@ -101,26 +115,41 @@ export function cloudflaredBinDir() {
101
115
  return join(cloudflaredDir(), 'bin');
102
116
  }
103
117
 
118
+ /** 可能存放 cloudflared 二进制的全部目录(当前 + 历史),供清理时一并处理。 */
119
+ export function cloudflaredBinDirs() {
120
+ return [cloudflaredDir(), ...legacyCloudflaredDirs()].map((dir) => join(dir, 'bin'));
121
+ }
122
+
104
123
  /**
105
- * 迁移 cloudflared 二进制缓存目录(旧名 → 新名)。幂等。
124
+ * 迁移 cloudflared 二进制缓存目录(历史名 → 当前名)。幂等。
125
+ *
126
+ * 按 `LEGACY_CLOUDFLARED_DIR_NAMES` 的顺序(从新到旧)尝试,命中第一个存在
127
+ * 且确实是目录的旧目录即迁移,然后返回。
106
128
  *
107
129
  * 与数据目录迁移的区别:这里失败代价只是「重新下载约 39MB」,不是丢数据,
108
130
  * 因此策略更宽松 —— 失败也不抛出,让下载逻辑自愈。
109
131
  *
110
- * @returns {{migrated: boolean, reason: string}}
132
+ * @returns {{migrated: boolean, reason: string, from?: string, to?: string}}
111
133
  */
112
134
  export function migrateLegacyCloudflaredDir({ logger } = {}) {
113
- const from = legacyCloudflaredDir();
114
135
  const to = cloudflaredDir();
115
136
  try {
116
137
  if (existsSync(to)) return { migrated: false, reason: 'already-migrated' };
117
- if (!existsSync(from)) return { migrated: false, reason: 'no-legacy-dir' };
118
- if (!statSync(from).isDirectory()) return { migrated: false, reason: 'legacy-not-a-directory' };
138
+
139
+ // 从新到旧找第一个可用的旧目录
140
+ let from = null;
141
+ for (const candidate of legacyCloudflaredDirs()) {
142
+ if (!existsSync(candidate)) continue;
143
+ if (!statSync(candidate).isDirectory()) continue;
144
+ from = candidate;
145
+ break;
146
+ }
147
+ if (!from) return { migrated: false, reason: 'no-legacy-dir' };
119
148
 
120
149
  try {
121
150
  renameSync(from, to);
122
151
  logger?.info?.('dsh-bridge-gateway: cloudflared 缓存目录已迁移 %s -> %s', from, to);
123
- return { migrated: true, reason: 'renamed' };
152
+ return { migrated: true, reason: 'renamed', from, to };
124
153
  } catch (renameErr) {
125
154
  mkdirSync(to, { recursive: true });
126
155
  try {
@@ -133,7 +162,7 @@ export function migrateLegacyCloudflaredDir({ logger } = {}) {
133
162
  return { migrated: false, reason: `copy-failed:${copyErr?.message ?? copyErr}` };
134
163
  }
135
164
  logger?.info?.('dsh-bridge-gateway: cloudflared 缓存已复制迁移 %s -> %s', from, to);
136
- return { migrated: true, reason: 'copied' };
165
+ return { migrated: true, reason: 'copied', from, to };
137
166
  }
138
167
  } catch (err) {
139
168
  logger?.warn?.('dsh-bridge-gateway: cloudflared 缓存迁移失败(将按需重新下载):%s', err?.message ?? err);
@@ -1,4 +1,4 @@
1
- // dsh-bridge 平台抽象基类
1
+ // dsh-bridge-gateway 平台抽象基类
2
2
  //
3
3
  // 定义 IM 平台适配器的统一接口。每个平台(微信/QQ/飞书/Telegram…)继承本类,
4
4
  // 实现协议层(登录、收发消息、typing)。平台无关的会话桥逻辑在 ConversationBridge
@@ -137,7 +137,7 @@ export class Platform {
137
137
  try {
138
138
  await this.onPersist(patch)
139
139
  } catch (err) {
140
- this.logger?.warn?.(`[dsh-bridge ${this.id}] persist failed: %s`, err?.message ?? err)
140
+ this.logger?.warn?.(`[dsh-bridge-gateway ${this.id}] persist failed: %s`, err?.message ?? err)
141
141
  }
142
142
  }
143
143
 
@@ -1,4 +1,4 @@
1
- // dsh-bridge 平台无关的会话桥
1
+ // dsh-bridge-gateway 平台无关的会话桥
2
2
  //
3
3
  // 承担所有 IM 平台共享的核心逻辑:
4
4
  // - 白名单(allowFrom)与首条自动授权
@@ -385,7 +385,7 @@ export class ConversationBridge {
385
385
  } catch (err) {
386
386
  if (!wanted) throw err
387
387
  const reason = err instanceof Error ? err.message : String(err)
388
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] agent preset "${wanted}" unavailable (${reason}); falling back to the default preset`)
388
+ this.logger?.warn?.(`[dsh-bridge-gateway ${this.platform.id}] agent preset "${wanted}" unavailable (${reason}); falling back to the default preset`)
389
389
  resolvedId = (await presets.resolve(undefined)).id
390
390
  fallbackFrom = wanted
391
391
  }
@@ -446,7 +446,7 @@ export class ConversationBridge {
446
446
 
447
447
  if (prompt) {
448
448
  const platformName = this.platform?.name || 'IM客户端'
449
- const promptWithContext = `${prompt}\n\n<!-- [dsh-bridge 提示] 当前用户正通过【${platformName}】与你对话。若用户明确要求发送、导出或传送文件/图片/报表/代码脚本产物,请在本地生成/准备好文件后,在回复正文中附带明确发送指令:\n[SEND_FILE: <本地文件绝对路径>]\n例如:[SEND_FILE: C:\\path\\to\\report.xlsx]\n网关会自动解析该指令并将该文件直传至用户的聊天窗口,且在聊天文本中自动隐藏该指令。在日常编写代码、回复普通文本或未请求发送文件时,请勿输出此指令。 -->`
449
+ const promptWithContext = `${prompt}\n\n<!-- [dsh-bridge-gateway 提示] 当前用户正通过【${platformName}】与你对话。若用户明确要求发送、导出或传送文件/图片/报表/代码脚本产物,请在本地生成/准备好文件后,在回复正文中附带明确发送指令:\n[SEND_FILE: <本地文件绝对路径>]\n例如:[SEND_FILE: C:\\path\\to\\report.xlsx]\n网关会自动解析该指令并将该文件直传至用户的聊天窗口,且在聊天文本中自动隐藏该指令。在日常编写代码、回复普通文本或未请求发送文件时,请勿输出此指令。 -->`
450
450
  handle.agent.followup(createUserMessage({
451
451
  content: [{ type: 'text', text: promptWithContext }],
452
452
  source: { kind: 'user' },
@@ -571,32 +571,32 @@ export class ConversationBridge {
571
571
  )
572
572
  if (shouldAutoApprove) {
573
573
  this.config.allowFrom = Array.from(new Set([...this.config.allowFrom, sender]))
574
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] auto-approved ${isGroup ? 'group' : 'sender'} ${sender} into allowlist`)
574
+ this.logger?.info?.(`[dsh-bridge-gateway ${this.platform.id}] auto-approved ${isGroup ? 'group' : 'sender'} ${sender} into allowlist`)
575
575
  try {
576
576
  await this.onFirstSender?.(sender)
577
577
  } catch (err) {
578
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to persist first sender: ${err instanceof Error ? err.message : String(err)}`)
578
+ this.logger?.warn?.(`[dsh-bridge-gateway ${this.platform.id}] failed to persist first sender: ${err instanceof Error ? err.message : String(err)}`)
579
579
  }
580
580
  } else {
581
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] media-only first message from ${sender} not auto-approved (waiting for text)`)
581
+ this.logger?.info?.(`[dsh-bridge-gateway ${this.platform.id}] media-only first message from ${sender} not auto-approved (waiting for text)`)
582
582
  }
583
583
 
584
584
  // 如果仍未通过白名单,拒绝处理(防止绕过白名单)
585
585
  if (!this.isAllowed(sender)) {
586
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
586
+ this.logger?.info?.(`[dsh-bridge-gateway ${this.platform.id}] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
587
587
  return 'ignored'
588
588
  }
589
589
  }
590
590
 
591
591
  // 仅在不支持群聊的平台忽略群消息(QQ 等支持群聊的平台放行)
592
592
  if (isGroup && !this.platform?.capabilities?.supportsGroup) {
593
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore group message from ${sender} (no group support)`)
593
+ this.logger?.info?.(`[dsh-bridge-gateway ${this.platform.id}] ignore group message from ${sender} (no group support)`)
594
594
  return 'ignored'
595
595
  }
596
596
 
597
597
  const fullText = text?.trim() ?? ''
598
598
  if (!fullText) {
599
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] ignore empty message from ${sender}`)
599
+ this.logger?.info?.(`[dsh-bridge-gateway ${this.platform.id}] ignore empty message from ${sender}`)
600
600
  return 'ignored'
601
601
  }
602
602
 
@@ -669,11 +669,11 @@ export class ConversationBridge {
669
669
  })
670
670
  }
671
671
  const resumedAgent = handle?.agent
672
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] re-attached agent to session ${sessionId} (${persisted ? 'resume' : 'create'})`)
672
+ this.logger?.info?.(`[dsh-bridge-gateway ${this.platform.id}] re-attached agent to session ${sessionId} (${persisted ? 'resume' : 'create'})`)
673
673
  return resumedAgent
674
674
  } catch (err) {
675
675
  const reason = err instanceof Error ? err.message : String(err)
676
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] failed to re-attach agent: ${reason}`)
676
+ this.logger?.warn?.(`[dsh-bridge-gateway ${this.platform.id}] failed to re-attach agent: ${reason}`)
677
677
  // 清除失效的 activeSessionId,避免用户误以为还在旧会话中
678
678
  if (this.activeSessionId === sessionId) {
679
679
  this.activeSessionId = null
@@ -695,7 +695,7 @@ export class ConversationBridge {
695
695
  }
696
696
 
697
697
  // 针对微信/IM客户端用户,注入上下文提示,规范 Agent 仅在需要向用户发送文件附件时输出 [SEND_FILE: <文件绝对路径>]
698
- const promptWithContext = `${fullText}\n\n<!-- [dsh-bridge 提示] 当前用户正通过【${this.platform.name}】与你对话。若用户明确要求发送、导出或传送文件/图片/报表/代码脚本产物,请在本地生成/准备好文件后,在回复正文中附带明确发送指令:\n[SEND_FILE: <本地文件绝对路径>]\n例如:[SEND_FILE: C:\\path\\to\\report.xlsx]\n网关会自动解析该指令并将该文件直传至用户的聊天窗口,且在聊天文本中自动隐藏该指令。在日常编写代码、回复普通文本或未请求发送文件时,请勿输出此指令。 -->`
698
+ const promptWithContext = `${fullText}\n\n<!-- [dsh-bridge-gateway 提示] 当前用户正通过【${this.platform.name}】与你对话。若用户明确要求发送、导出或传送文件/图片/报表/代码脚本产物,请在本地生成/准备好文件后,在回复正文中附带明确发送指令:\n[SEND_FILE: <本地文件绝对路径>]\n例如:[SEND_FILE: C:\\path\\to\\report.xlsx]\n网关会自动解析该指令并将该文件直传至用户的聊天窗口,且在聊天文本中自动隐藏该指令。在日常编写代码、回复普通文本或未请求发送文件时,请勿输出此指令。 -->`
699
699
 
700
700
  const messageValue = createUserMessage({
701
701
  content: [{ type: 'text', text: promptWithContext }],
@@ -706,7 +706,7 @@ export class ConversationBridge {
706
706
  return 'routed'
707
707
  } catch (err) {
708
708
  const errMsg = err instanceof Error ? err.message : String(err)
709
- this.logger?.error?.(`[dsh-bridge ${this.platform.id}] unhandled error in handleInbound: ${errMsg}`)
709
+ this.logger?.error?.(`[dsh-bridge-gateway ${this.platform.id}] unhandled error in handleInbound: ${errMsg}`)
710
710
  await this.sendText(`❌ **执行出错**:${errMsg}`).catch(() => {})
711
711
  return 'ignored'
712
712
  }
@@ -726,7 +726,7 @@ export class ConversationBridge {
726
726
  for (let i = 0; i < chunks.length; i++) {
727
727
  const result = await this.platform.sendText(peer, chunks[i])
728
728
  if (result && result.success === false) {
729
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] outbound chunk ${i + 1}/${chunks.length} failed: ${result.error}`)
729
+ this.logger?.warn?.(`[dsh-bridge-gateway ${this.platform.id}] outbound chunk ${i + 1}/${chunks.length} failed: ${result.error}`)
730
730
  break
731
731
  }
732
732
  if (i < chunks.length - 1 && this.config.sendChunkDelayMs > 0) {
@@ -847,13 +847,13 @@ export class ConversationBridge {
847
847
  }
848
848
  for (const resolved of uniqueFilesToSend) {
849
849
  try {
850
- this.logger?.info?.(`[dsh-bridge ${this.platform.id}] auto sendMediaFile to peer: ${resolved}`)
850
+ this.logger?.info?.(`[dsh-bridge-gateway ${this.platform.id}] auto sendMediaFile to peer: ${resolved}`)
851
851
  const res = await this.platform.sendMediaFile(targetPeer, resolved)
852
852
  if (res && res.success === false) {
853
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] auto sendMediaFile ${resolved} failed: %s`, res.error)
853
+ this.logger?.warn?.(`[dsh-bridge-gateway ${this.platform.id}] auto sendMediaFile ${resolved} failed: %s`, res.error)
854
854
  }
855
855
  } catch (err) {
856
- this.logger?.warn?.(`[dsh-bridge ${this.platform.id}] auto sendMediaFile ${resolved} failed: %s`, err?.message ?? err)
856
+ this.logger?.warn?.(`[dsh-bridge-gateway ${this.platform.id}] auto sendMediaFile ${resolved} failed: %s`, err?.message ?? err)
857
857
  }
858
858
  }
859
859
  }
@@ -1,4 +1,4 @@
1
- // dsh-bridge 平台抽象层统一导出
1
+ // dsh-bridge-gateway 平台抽象层统一导出
2
2
  //
3
3
  // 提供多平台 IM 接入的基础设施:
4
4
  // Platform 平台适配器基类(协议/连接/登录/收发消息)
@@ -1,4 +1,4 @@
1
- // dsh-bridge 平台管理器
1
+ // dsh-bridge-gateway 平台管理器
2
2
  //
3
3
  // 注册/协调多个 IM 平台适配器(Platform 子类),向主插件与 RPC 层提供:
4
4
  // - 统一的多平台状态聚合(getStatus)
@@ -20,7 +20,7 @@ export class PlatformManager {
20
20
  /** 注册一个平台实例。重复注册同 id 时替换旧实例并 dispose 旧的。 */
21
21
  register(platform) {
22
22
  if (!platform || !platform.id) {
23
- this.logger?.warn?.('[dsh-bridge] PlatformManager.register: 平台缺少 id,已忽略')
23
+ this.logger?.warn?.('[dsh-bridge-gateway] PlatformManager.register: 平台缺少 id,已忽略')
24
24
  return platform
25
25
  }
26
26
  const existing = this.platforms.get(platform.id)
@@ -29,12 +29,12 @@ export class PlatformManager {
29
29
  const result = existing.dispose()
30
30
  if (result instanceof Promise) {
31
31
  // 异步 dispose,记录警告(当前架构假定同步清理)
32
- this.logger?.warn?.(`[dsh-bridge] Platform ${platform.id} dispose is async but not awaited (may leave dangling resources)`)
32
+ this.logger?.warn?.(`[dsh-bridge-gateway] Platform ${platform.id} dispose is async but not awaited (may leave dangling resources)`)
33
33
  }
34
34
  } catch { /* 忽略 */ }
35
35
  }
36
36
  this.platforms.set(platform.id, platform)
37
- this.logger?.info?.(`[dsh-bridge] platform registered: ${platform.id} (${platform.name ?? ''})`)
37
+ this.logger?.info?.(`[dsh-bridge-gateway] platform registered: ${platform.id} (${platform.name ?? ''})`)
38
38
  return platform
39
39
  }
40
40
 
package/lib/qq/gateway.js CHANGED
@@ -193,7 +193,7 @@ export class QqGateway extends Service {
193
193
 
194
194
  async persist(patch) {
195
195
  try { await this.onPersist(patch) } catch (error) {
196
- this.logger?.warn?.('[dsh-bridge qq] persist failed: %s', error?.message ?? error)
196
+ this.logger?.warn?.('[dsh-bridge-gateway qq] persist failed: %s', error?.message ?? error)
197
197
  }
198
198
  }
199
199
 
@@ -246,7 +246,7 @@ export class QqGateway extends Service {
246
246
  } catch (error) {
247
247
  if (this.stopRequested) break
248
248
  this.setStatus('reconnecting')
249
- this.logger?.warn?.('[dsh-bridge qq] gateway disconnected: %s, retrying in %dms...', error?.message ?? error, backoffMs)
249
+ this.logger?.warn?.('[dsh-bridge-gateway qq] gateway disconnected: %s, retrying in %dms...', error?.message ?? error, backoffMs)
250
250
  await sleep(backoffMs)
251
251
  backoffMs = Math.min(Math.round(backoffMs * 1.5), 30_000)
252
252
  }
@@ -278,7 +278,7 @@ export class QqGateway extends Service {
278
278
  finish()
279
279
  return
280
280
  }
281
- this.logger?.info?.('[dsh-bridge qq] WebSocket connected to QQ Open Platform')
281
+ this.logger?.info?.('[dsh-bridge-gateway qq] WebSocket connected to QQ Open Platform')
282
282
  })
283
283
  ws.on('message', (raw) => {
284
284
  if (this.stopRequested) return
@@ -300,7 +300,7 @@ export class QqGateway extends Service {
300
300
  this.startHeartbeat(ws)
301
301
  if (this.sessionId && this.sequence != null) {
302
302
  // 快速恢复模式(Resume):携带 sessionId 与 sequence 避免重新全量鉴权与丢失消息
303
- this.logger?.info?.('[dsh-bridge qq] attempting session resume (sessionId=%s, seq=%s)', this.sessionId, this.sequence)
303
+ this.logger?.info?.('[dsh-bridge-gateway qq] attempting session resume (sessionId=%s, seq=%s)', this.sessionId, this.sequence)
304
304
  ws.send(JSON.stringify({
305
305
  op: 6,
306
306
  d: {
@@ -317,7 +317,7 @@ export class QqGateway extends Service {
317
317
  token: `QQBot ${token}`,
318
318
  intents: this.config.intents,
319
319
  shard: [0, 1],
320
- properties: { $os: process.platform, $browser: 'dsh-bridge', $device: 'dsh-bridge' },
320
+ properties: { $os: process.platform, $browser: 'dsh-bridge-gateway', $device: 'dsh-bridge-gateway' },
321
321
  },
322
322
  }))
323
323
  }
@@ -335,11 +335,11 @@ export class QqGateway extends Service {
335
335
  this.unackedHeartbeats = 0
336
336
  await this.persist({ accountId: this.accountId })
337
337
  this.setStatus('connected')
338
- this.logger?.info?.('[dsh-bridge qq] gateway READY (session_id=%s)', this.sessionId)
338
+ this.logger?.info?.('[dsh-bridge-gateway qq] gateway READY (session_id=%s)', this.sessionId)
339
339
  } else if (payload.t === 'RESUMED') {
340
340
  this.unackedHeartbeats = 0
341
341
  this.setStatus('connected')
342
- this.logger?.info?.('[dsh-bridge qq] gateway RESUMED successfully')
342
+ this.logger?.info?.('[dsh-bridge-gateway qq] gateway RESUMED successfully')
343
343
  }
344
344
  const event = normalizeEvent(payload)
345
345
  if (event.type === 'message' && event.id && !this.seen(event.id)) {
@@ -351,7 +351,7 @@ export class QqGateway extends Service {
351
351
  return
352
352
  }
353
353
  if (op === 7) {
354
- this.logger?.info?.('[dsh-bridge qq] gateway requested reconnect (OpCode 7)')
354
+ this.logger?.info?.('[dsh-bridge-gateway qq] gateway requested reconnect (OpCode 7)')
355
355
  try { ws.close() } catch {}
356
356
  return
357
357
  }
@@ -368,7 +368,7 @@ export class QqGateway extends Service {
368
368
  const beat = () => {
369
369
  if (ws.readyState === WebSocket.OPEN) {
370
370
  if (this.unackedHeartbeats >= 2) {
371
- this.logger?.warn?.('[dsh-bridge qq] heartbeat ACK missed (count=%d), dead link detected, reconnecting...', this.unackedHeartbeats)
371
+ this.logger?.warn?.('[dsh-bridge-gateway qq] heartbeat ACK missed (count=%d), dead link detected, reconnecting...', this.unackedHeartbeats)
372
372
  try { ws.terminate() } catch {}
373
373
  return
374
374
  }
@@ -600,7 +600,7 @@ export class QqGateway extends Service {
600
600
  })
601
601
  return res
602
602
  } catch (err) {
603
- this.logger?.warn?.('[dsh-bridge qq] sendMediaFile error: %s', err?.message ?? err)
603
+ this.logger?.warn?.('[dsh-bridge-gateway qq] sendMediaFile error: %s', err?.message ?? err)
604
604
  return null
605
605
  }
606
606
  }
package/lib/qq/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // dsh-bridge QQ platform adapter
1
+ // dsh-bridge-gateway QQ platform adapter
2
2
  // 编排 QqGateway(OpenAPI v2 网关)+ QqConversationNode(QQ⇄DSH 会话桥)。
3
3
  // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
4
  //
@@ -68,7 +68,7 @@ export class QqService extends Platform {
68
68
 
69
69
  if (this.gateway.configured) {
70
70
  void this.start().catch((err) => {
71
- this.logger.error('[dsh-bridge qq] start failed: %s', err?.message ?? err)
71
+ this.logger.error('[dsh-bridge-gateway qq] start failed: %s', err?.message ?? err)
72
72
  })
73
73
  }
74
74
  }
@@ -184,7 +184,7 @@ export class QqService extends Platform {
184
184
  }
185
185
  this.loginState = { phase: 'done', qrPayload: null, qrKind: null, error: null }
186
186
  void this.gateway.start().catch((err) => {
187
- this.logger.error('[dsh-bridge qq] login start failed: %s', err?.message ?? err)
187
+ this.logger.error('[dsh-bridge-gateway qq] login start failed: %s', err?.message ?? err)
188
188
  })
189
189
  return { ok: true }
190
190
  }
@@ -194,7 +194,7 @@ export class QqService extends Platform {
194
194
  await this.gateway.stop()
195
195
  this.gateway.setCredentials({ appId: '', clientSecret: '', accessToken: '', accessTokenExpiresAt: 0 })
196
196
  await this.persist({ appId: '', clientSecret: '', accessToken: '', accessTokenExpiresAt: 0, accountId: '' })
197
- this.logger.info('[dsh-bridge qq] unbound: credentials cleared')
197
+ this.logger.info('[dsh-bridge-gateway qq] unbound: credentials cleared')
198
198
  }
199
199
 
200
200
  /** 更新白名单并发起持久化。 */
@@ -235,10 +235,10 @@ export class QqService extends Platform {
235
235
  target_type: 'all',
236
236
  panel: { items, remark, version: 1 },
237
237
  })
238
- this.logger.info('[dsh-bridge qq] created %s command panel: %s', scope, res?.panel_id ?? '(no id)')
238
+ this.logger.info('[dsh-bridge-gateway qq] created %s command panel: %s', scope, res?.panel_id ?? '(no id)')
239
239
  }
240
240
  } catch (err) {
241
- this.logger.warn('[dsh-bridge qq] command panel setup for %s skipped: %s', scope, err?.message ?? err)
241
+ this.logger.warn('[dsh-bridge-gateway qq] command panel setup for %s skipped: %s', scope, err?.message ?? err)
242
242
  }
243
243
  }
244
244
 
@@ -256,12 +256,12 @@ export class QqService extends Platform {
256
256
  menuItems.every((item, i) => existingItems[i]?.name === item.name && existingItems[i]?.send_message === item.send_message)
257
257
  if (!same) {
258
258
  await gate.setMenu(menuItems)
259
- this.logger.info('[dsh-bridge qq] custom menu configured')
259
+ this.logger.info('[dsh-bridge-gateway qq] custom menu configured')
260
260
  } else {
261
- this.logger.info('[dsh-bridge qq] custom menu already configured')
261
+ this.logger.info('[dsh-bridge-gateway qq] custom menu already configured')
262
262
  }
263
263
  } catch (err) {
264
- this.logger.warn('[dsh-bridge qq] custom menu setup skipped: %s', err?.message ?? err)
264
+ this.logger.warn('[dsh-bridge-gateway qq] custom menu setup skipped: %s', err?.message ?? err)
265
265
  }
266
266
  }
267
267
 
@@ -298,7 +298,7 @@ export class QqService extends Platform {
298
298
  // 凭证配置完成后自动启动网关(前端「保存并连接」无需二次点击)
299
299
  if (this.gateway.configured && (appId !== undefined || clientSecret !== undefined)) {
300
300
  await this.gateway.start().catch((err) => {
301
- this.logger?.warn?.('[dsh-bridge qq] auto-start failed: %s', err?.message ?? err)
301
+ this.logger?.warn?.('[dsh-bridge-gateway qq] auto-start failed: %s', err?.message ?? err)
302
302
  })
303
303
  }
304
304
  }
@@ -309,4 +309,4 @@ function gatewayMaxChars() {
309
309
  }
310
310
 
311
311
  // 指令面板 remark 标识,用于幂等判断(区分本插件创建的面板)
312
- const PANEL_REMARK = 'dsh-bridge 常用命令'
312
+ const PANEL_REMARK = 'dsh-bridge-gateway 常用命令'
package/lib/qq/node.js CHANGED
@@ -1,4 +1,4 @@
1
- // dsh-bridge QQ conversation node
1
+ // dsh-bridge-gateway QQ conversation node
2
2
  // 把 QQ OpenAPI v2 的入站事件(C2C 私聊 / 群聊 @提及)解析后交给平台无关的
3
3
  // ConversationBridge 处理,出站通过 QqGateway 发送文本 / Markdown / 按钮。
4
4
  // 平台特定部分:
@@ -277,9 +277,9 @@ export class QqConversationNode extends ConversationBridge {
277
277
  streamMsgId = result?.id || result?.message_id || result?.stream_msg_id
278
278
  if (streamMsgId) {
279
279
  this.lastMessageId = streamMsgId
280
- this.logger?.info?.('[dsh-bridge qq] stream started, stream_msg_id=%s, slices=%d', streamMsgId, slices.length)
280
+ this.logger?.info?.('[dsh-bridge-gateway qq] stream started, stream_msg_id=%s, slices=%d', streamMsgId, slices.length)
281
281
  } else {
282
- this.logger?.warn?.('[dsh-bridge qq] stream first chunk returned no id: %o', result)
282
+ this.logger?.warn?.('[dsh-bridge-gateway qq] stream first chunk returned no id: %o', result)
283
283
  }
284
284
  }
285
285
 
@@ -296,7 +296,7 @@ export class QqConversationNode extends ConversationBridge {
296
296
  return { success: true }
297
297
  } catch (err) {
298
298
  // 流式失败 → 补发完整内容 replace 收尾(input_state=10),尽量合并成一条
299
- this.logger?.warn?.('[dsh-bridge qq] stream send failed, patch final replace: %s', err?.message ?? err)
299
+ this.logger?.warn?.('[dsh-bridge-gateway qq] stream send failed, patch final replace: %s', err?.message ?? err)
300
300
  try {
301
301
  const result = await this.gateway.sendStream(peerId, md, {
302
302
  ...streamCommon,
@@ -323,13 +323,13 @@ export class QqConversationNode extends ConversationBridge {
323
323
  return result
324
324
  } catch (fallbackErr1) {
325
325
  // 被动回复失败(msg_id 过期等)→ 主动消息兜底
326
- this.logger?.warn?.('[dsh-bridge qq] markdown send failed with msg_id, retry as active: %s', fallbackErr1?.message ?? fallbackErr1)
326
+ this.logger?.warn?.('[dsh-bridge-gateway qq] markdown send failed with msg_id, retry as active: %s', fallbackErr1?.message ?? fallbackErr1)
327
327
  try {
328
328
  const result = await this.gateway.sendMarkdown(peerId, md, { scope, keyboard })
329
329
  if (result?.id) this.lastMessageId = result.id
330
330
  return result
331
331
  } catch (fallbackErr2) {
332
- this.logger?.error?.('[dsh-bridge qq] markdown send failed: %s', fallbackErr2?.message ?? fallbackErr2)
332
+ this.logger?.error?.('[dsh-bridge-gateway qq] markdown send failed: %s', fallbackErr2?.message ?? fallbackErr2)
333
333
  return { success: false, error: fallbackErr2?.message ?? String(fallbackErr2) }
334
334
  }
335
335
  }
@@ -375,7 +375,7 @@ export class QqConversationNode extends ConversationBridge {
375
375
  // 快速预检查:白名单非空时,未授权单聊发件人直接忽略;
376
376
  // 群聊不在此拦截——交给 handleInbound 自动授权(首次 @机器人 即授权该群)
377
377
  if (!isGroup && !this.isAllowed(authId) && this.config.allowFrom.length > 0) {
378
- this.logger?.info?.(`[dsh-bridge qq] ignore message from non-allowlisted sender ${authId}`)
378
+ this.logger?.info?.(`[dsh-bridge-gateway qq] ignore message from non-allowlisted sender ${authId}`)
379
379
  return
380
380
  }
381
381
 
@@ -384,13 +384,13 @@ export class QqConversationNode extends ConversationBridge {
384
384
 
385
385
  let mediaFiles = []
386
386
  if (attachments.length > 0) {
387
- this.logger?.info?.(`[dsh-bridge qq] processing ${attachments.length} attachment(s) from ${sender}...`)
387
+ this.logger?.info?.(`[dsh-bridge-gateway qq] processing ${attachments.length} attachment(s) from ${sender}...`)
388
388
  mediaFiles = await this._processAttachments(attachments, this.config.cwd)
389
- this.logger?.info?.(`[dsh-bridge qq] downloaded ${mediaFiles.length} file(s)`)
389
+ this.logger?.info?.(`[dsh-bridge-gateway qq] downloaded ${mediaFiles.length} file(s)`)
390
390
  }
391
391
 
392
392
  if (!text && mediaFiles.length === 0) {
393
- this.logger?.info?.(`[dsh-bridge qq] ignore empty message from ${sender}`)
393
+ this.logger?.info?.(`[dsh-bridge-gateway qq] ignore empty message from ${sender}`)
394
394
  return
395
395
  }
396
396
 
@@ -410,7 +410,7 @@ export class QqConversationNode extends ConversationBridge {
410
410
  // 则自动创建新会话并发送消息
411
411
  const messageReference = event.messageReference
412
412
  if (messageReference && !this.activeSessionId && !fullText.startsWith('/')) {
413
- this.logger?.info?.(`[dsh-bridge qq] detected message reference from ${sender}, auto-starting conversation`)
413
+ this.logger?.info?.(`[dsh-bridge-gateway qq] detected message reference from ${sender}, auto-starting conversation`)
414
414
  // 先创建新会话,再处理消息
415
415
  await this.handleInbound({ senderId: authId, text: '/new', isGroup })
416
416
  // 等待一小段时间确保会话创建完成
@@ -441,7 +441,7 @@ export class QqConversationNode extends ConversationBridge {
441
441
  await fs.promises.writeFile(filePath, buf)
442
442
  downloaded.push({ filename: safeName, path: filePath, size: buf.length })
443
443
  } catch (err) {
444
- this.logger?.warn?.('[dsh-bridge qq] download attachment error: %s', err?.message ?? err)
444
+ this.logger?.warn?.('[dsh-bridge-gateway qq] download attachment error: %s', err?.message ?? err)
445
445
  }
446
446
  }
447
447
  return downloaded
@@ -462,7 +462,7 @@ export class QqConversationNode extends ConversationBridge {
462
462
  // 快速预检查:白名单非空时,未授权单聊发件人直接忽略;
463
463
  // 群聊不在此拦截——handleInbound 会按群自动授权
464
464
  if (!isGroup && !this.isAllowed(authId) && this.config.allowFrom.length > 0) {
465
- this.logger?.info?.(`[dsh-bridge qq] ignore interaction from non-allowlisted sender ${authId}`)
465
+ this.logger?.info?.(`[dsh-bridge-gateway qq] ignore interaction from non-allowlisted sender ${authId}`)
466
466
  return
467
467
  }
468
468
 
@@ -481,12 +481,12 @@ export class QqConversationNode extends ConversationBridge {
481
481
  try {
482
482
  await this.gateway.respondInteraction(event.interactionId, { code: 0 })
483
483
  } catch (err) {
484
- this.logger?.warn?.('[dsh-bridge qq] respond interaction failed: %s', err?.message ?? err)
484
+ this.logger?.warn?.('[dsh-bridge-gateway qq] respond interaction failed: %s', err?.message ?? err)
485
485
  }
486
486
  }
487
487
 
488
488
  if (!needsRespond) {
489
- this.logger?.info?.(`[dsh-bridge qq] skip non-button interaction type=${type} from ${authId}`)
489
+ this.logger?.info?.(`[dsh-bridge-gateway qq] skip non-button interaction type=${type} from ${authId}`)
490
490
  return
491
491
  }
492
492
 
@@ -495,7 +495,7 @@ export class QqConversationNode extends ConversationBridge {
495
495
  const buttonId = resolved.button_id || ''
496
496
 
497
497
  // 根据按钮 ID 执行对应操作(消息按钮 11 与快捷菜单 12 都映射到命令)
498
- this.logger?.info?.('[dsh-bridge qq] button interaction type=%s button_id=%s from %s', type, buttonId, authId)
498
+ this.logger?.info?.('[dsh-bridge-gateway qq] button interaction type=%s button_id=%s from %s', type, buttonId, authId)
499
499
  if (buttonId === 'new_conversation') {
500
500
  await this.handleInbound({ senderId: authId, text: '/new', isGroup })
501
501
  } else if (buttonId === 'list_sessions') {
@@ -507,7 +507,7 @@ export class QqConversationNode extends ConversationBridge {
507
507
  } else if (buttonId === 'approve_no') {
508
508
  await this.handleInbound({ senderId: authId, text: '/no', isGroup })
509
509
  } else {
510
- this.logger?.info?.(`[dsh-bridge qq] unknown button interaction: ${buttonId}`)
510
+ this.logger?.info?.(`[dsh-bridge-gateway qq] unknown button interaction: ${buttonId}`)
511
511
  }
512
512
  }
513
513
 
@@ -282,7 +282,7 @@ export class TelegramGateway extends Service {
282
282
  async start() {
283
283
  if (this._startingPromise) return this._startingPromise
284
284
  if (!this.configured) {
285
- this.logger.info?.('[dsh-bridge telegram] not configured (missing botToken); staying idle')
285
+ this.logger.info?.('[dsh-bridge-gateway telegram] not configured (missing botToken); staying idle')
286
286
  this.setStatus('idle')
287
287
  return false
288
288
  }
@@ -296,13 +296,13 @@ export class TelegramGateway extends Service {
296
296
 
297
297
  try {
298
298
  this.botInfo = await this.request('getMe')
299
- this.logger.info?.(`[dsh-bridge telegram] authenticated as @${this.botInfo.username} (id=${this.botInfo.id})`)
299
+ this.logger.info?.(`[dsh-bridge-gateway telegram] authenticated as @${this.botInfo.username} (id=${this.botInfo.id})`)
300
300
  this.setStatus('online')
301
301
  void this.registerCommands().catch(() => {})
302
302
  this._startPollLoop()
303
303
  return true
304
304
  } catch (err) {
305
- this.logger.error?.('[dsh-bridge telegram] getMe error:', err.message)
305
+ this.logger.error?.('[dsh-bridge-gateway telegram] getMe error:', err.message)
306
306
  this.setStatus('error', err.message)
307
307
  return false
308
308
  } finally {
@@ -336,9 +336,9 @@ export class TelegramGateway extends Service {
336
336
  await this.request('setMyCommands', { commands, scope: { type: 'all_group_chats' } })
337
337
  // 4. 设置左下角菜单按键为命令列表
338
338
  await this.request('setChatMenuButton', { menu_button: { type: 'commands' } }).catch(() => {})
339
- this.logger.info?.('[dsh-bridge telegram] bot command menu & menu button registered successfully')
339
+ this.logger.info?.('[dsh-bridge-gateway telegram] bot command menu & menu button registered successfully')
340
340
  } catch (err) {
341
- this.logger.warn?.('[dsh-bridge telegram] setMyCommands failed: %s', err?.message ?? err)
341
+ this.logger.warn?.('[dsh-bridge-gateway telegram] setMyCommands failed: %s', err?.message ?? err)
342
342
  }
343
343
  }
344
344
 
@@ -388,7 +388,7 @@ export class TelegramGateway extends Service {
388
388
  if (this._stopPolling) break
389
389
  consecutiveErrors += 1
390
390
  const backoffSec = Math.min(30, 2 ** Math.min(consecutiveErrors, 5))
391
- this.logger.warn?.(`[dsh-bridge telegram] poll error: ${err.message}, retrying in ${backoffSec}s...`)
391
+ this.logger.warn?.(`[dsh-bridge-gateway telegram] poll error: ${err.message}, retrying in ${backoffSec}s...`)
392
392
  this.setStatus('reconnecting', err.message)
393
393
  await new Promise((r) => setTimeout(r, backoffSec * 1000))
394
394
  }
@@ -470,7 +470,7 @@ export class TelegramGateway extends Service {
470
470
  return await this.request('sendMessage', params)
471
471
  } catch (err) {
472
472
  if (err.errorCode === 400 || String(err.message).includes('can\'t parse')) {
473
- this.logger.warn?.(`[dsh-bridge telegram] HTML parse failed, falling back to plain text for chat ${chatId}`)
473
+ this.logger.warn?.(`[dsh-bridge-gateway telegram] HTML parse failed, falling back to plain text for chat ${chatId}`)
474
474
  return await this.request('sendMessage', {
475
475
  chat_id: chatId,
476
476
  text: String(text).slice(0, MAX_MESSAGE_CHARS),
@@ -545,7 +545,7 @@ export class TelegramGateway extends Service {
545
545
  show_alert: showAlert,
546
546
  })
547
547
  } catch (err) {
548
- this.logger.warn?.('[dsh-bridge telegram] answerCallbackQuery error:', err.message)
548
+ this.logger.warn?.('[dsh-bridge-gateway telegram] answerCallbackQuery error:', err.message)
549
549
  }
550
550
  }
551
551
 
@@ -583,7 +583,7 @@ export class TelegramGateway extends Service {
583
583
 
584
584
  return await this.request(method, {}, { isMultipart: true, formData })
585
585
  } catch (err) {
586
- this.logger.warn?.(`[dsh-bridge telegram] sendMediaFile (${filePath}) error: ${err.message}`)
586
+ this.logger.warn?.(`[dsh-bridge-gateway telegram] sendMediaFile (${filePath}) error: ${err.message}`)
587
587
  return null
588
588
  }
589
589
  }
@@ -611,7 +611,7 @@ export class TelegramGateway extends Service {
611
611
  await fs.promises.writeFile(fullPath, buf)
612
612
  return { path: fullPath, filename: safeName, size: buf.length }
613
613
  } catch (err) {
614
- this.logger.warn?.(`[dsh-bridge telegram] downloadFile error: ${err.message}`)
614
+ this.logger.warn?.(`[dsh-bridge-gateway telegram] downloadFile error: ${err.message}`)
615
615
  return null
616
616
  }
617
617
  }