@qzsy/dsh-bridge 2.18.0 → 3.0.1

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 (81) hide show
  1. package/CHANGELOG.md +15 -1094
  2. package/LICENSE +21 -21
  3. package/README.md +73 -476
  4. package/client/build.mjs +1 -3
  5. package/client/client.js +381 -9747
  6. package/client/index.js +242 -6828
  7. package/client/locales.mjs +99 -233
  8. package/cordis.patch.yml +5 -3
  9. package/lib/config.js +93 -0
  10. package/lib/data-dir.js +16 -0
  11. package/lib/dsh-cookie.js +111 -0
  12. package/lib/gate.js +414 -0
  13. package/lib/identity.js +23 -0
  14. package/lib/index.js +303 -3496
  15. package/lib/saas/tunnel-client.mjs +529 -467
  16. package/locale/en.json +3 -3
  17. package/locale/zh.json +3 -3
  18. package/package.json +15 -70
  19. package/README.en.md +0 -488
  20. package/client/host-families.js +0 -442
  21. package/client/mobile-styles.js +0 -1299
  22. package/client/mobile-yield.js +0 -219
  23. package/client/panel-locales.mjs +0 -777
  24. package/client/picker-yield.js +0 -77
  25. package/client/resource-url-compat.js +0 -90
  26. package/client/settings-nav-icon.js +0 -105
  27. package/client/theme-tokens.js +0 -49
  28. package/client/tunnel-guard.js +0 -46
  29. package/client/unlock-manager.js +0 -144
  30. package/lib/auth/dsh-native-cookie.js +0 -148
  31. package/lib/auth/login-template.js +0 -516
  32. package/lib/auth/manager.js +0 -596
  33. package/lib/bridge-rpc-constants.js +0 -66
  34. package/lib/bridge-rpc.js +0 -722
  35. package/lib/cloudflared-manager.mjs +0 -1277
  36. package/lib/cloudflared-network-args.js +0 -99
  37. package/lib/cloudflared-process-scan.js +0 -153
  38. package/lib/compat.js +0 -172
  39. package/lib/connection-compat.js +0 -115
  40. package/lib/feishu/comment.js +0 -50
  41. package/lib/feishu/gateway.js +0 -644
  42. package/lib/feishu/index.js +0 -432
  43. package/lib/feishu/lark-bundled.mjs +0 -131929
  44. package/lib/feishu/markdown.js +0 -148
  45. package/lib/feishu/node.js +0 -1152
  46. package/lib/feishu/slash-commands.js +0 -255
  47. package/lib/platform/account-ops.js +0 -248
  48. package/lib/platform/activity.js +0 -169
  49. package/lib/platform/ask-user.js +0 -293
  50. package/lib/platform/base.js +0 -147
  51. package/lib/platform/changes-digest.js +0 -93
  52. package/lib/platform/commands.js +0 -267
  53. package/lib/platform/config-restore.js +0 -78
  54. package/lib/platform/conversation-bridge.js +0 -1544
  55. package/lib/platform/dsh-storage.js +0 -222
  56. package/lib/platform/index.js +0 -10
  57. package/lib/platform/instances.js +0 -458
  58. package/lib/platform/manager.js +0 -130
  59. package/lib/platform/message-split.js +0 -229
  60. package/lib/platform/session-catalog.js +0 -405
  61. package/lib/platform/session-config.js +0 -52
  62. package/lib/platform/stream-slices.js +0 -21
  63. package/lib/qq/gateway.js +0 -1107
  64. package/lib/qq/index.js +0 -561
  65. package/lib/qq/node.js +0 -1153
  66. package/lib/qq/qqbot-bundled.mjs +0 -8273
  67. package/lib/restart-helper.mjs +0 -133
  68. package/lib/security/log-sanitize.js +0 -50
  69. package/lib/security/path-validator.js +0 -195
  70. package/lib/security/rate-limiter.js +0 -93
  71. package/lib/session-strip.js +0 -100
  72. package/lib/sse-keepalive.js +0 -115
  73. package/lib/telegram/gateway.js +0 -643
  74. package/lib/telegram/index.js +0 -255
  75. package/lib/telegram/node.js +0 -410
  76. package/lib/tunnel-client.mjs +0 -537
  77. package/lib/wechat/gateway.js +0 -1000
  78. package/lib/wechat/index.js +0 -282
  79. package/lib/wechat/media.js +0 -302
  80. package/lib/wechat/node.js +0 -360
  81. package/llms.txt +0 -36
@@ -1,130 +0,0 @@
1
- // dsh-bridge 平台管理器
2
- //
3
- // 注册/协调多个 IM 平台适配器(Platform 子类),向主插件与 RPC 层提供:
4
- // - 统一的多平台状态聚合(getStatus)
5
- // - 平台实例查找(get / resolve,兼容旧客户端只传 platformId)
6
- // - 按类型枚举(listByType)与注销(unregister)
7
- // - 统一生命周期(dispose)
8
- //
9
- // issue #78 起 Map 的键为 instanceId(`platform.instanceId ?? platform.id`):
10
- // 同一平台类型可注册多个账号实例;旧调用只传 platformId 时回退到该类型第一个实例。
11
- //
12
- // 主插件注入已构造的平台实例列表,本类只负责登记与聚合,不负责构造平台。
13
-
14
- export class PlatformManager {
15
- /**
16
- * @param {object} opts
17
- * @param {object} opts.logger 日志器
18
- */
19
- constructor({ logger } = {}) {
20
- this.logger = logger
21
- this.platforms = new Map() // instanceId -> Platform
22
- }
23
-
24
- /** 实例键:优先显式 instanceId,未设置时回退 platform.id(保持旧单实例语义)。 */
25
- instanceIdOf(platform) {
26
- return platform?.instanceId ?? platform?.id
27
- }
28
-
29
- /** 注册一个平台实例。重复注册同 instanceId 时替换旧实例并 dispose 旧的。 */
30
- register(platform) {
31
- if (!platform || !platform.id) {
32
- this.logger?.warn?.('[dsh-bridge] PlatformManager.register: 平台缺少 id,已忽略')
33
- return platform
34
- }
35
- const instanceId = this.instanceIdOf(platform)
36
- const existing = this.platforms.get(instanceId)
37
- if (existing && existing !== platform) {
38
- try {
39
- const result = existing.dispose()
40
- if (result instanceof Promise) {
41
- // 异步 dispose,记录警告(当前架构假定同步清理)
42
- this.logger?.warn?.(`[dsh-bridge] Platform ${instanceId} dispose is async but not awaited (may leave dangling resources)`)
43
- }
44
- } catch { /* 忽略 */ }
45
- }
46
- this.platforms.set(instanceId, platform)
47
- this.logger?.info?.(`[dsh-bridge] platform registered: ${instanceId} (${platform.name ?? ''})`)
48
- return platform
49
- }
50
-
51
- /**
52
- * 获取平台实例:先按 instanceId 精确命中;未命中时兼容回退到该类型第一个实例
53
- * (`platform.id === platformId`)——旧客户端只传 platformId 时行为不变。
54
- * 不存在返回 undefined。
55
- */
56
- get(platformId) {
57
- if (platformId == null) return undefined
58
- const exact = this.platforms.get(platformId)
59
- if (exact) return exact
60
- for (const platform of this.platforms.values()) {
61
- if (platform?.id === platformId) return platform
62
- }
63
- return undefined
64
- }
65
-
66
- /** get 的语义别名(契约 §7:RPC 统一用 resolve)。 */
67
- resolve(platformIdOrInstanceId) {
68
- return this.get(platformIdOrInstanceId)
69
- }
70
-
71
- /** 所有已注册平台实例(数组,非 id 列表;保持既有返回结构不变)。 */
72
- list() {
73
- return [...this.platforms.values()]
74
- }
75
-
76
- /** 某平台类型下的全部账号实例(按注册顺序)。 */
77
- listByType(type) {
78
- return this.list().filter((platform) => platform?.id === type)
79
- }
80
-
81
- /** 按 instanceId 精确查找(不做类型回退;同步编排用)。 */
82
- getExact(instanceId) {
83
- return this.platforms.get(instanceId)
84
- }
85
-
86
- /** 只从 Map 移除实例(不负责 stop/dispose),返回是否移除成功。 */
87
- unregister(instanceId) {
88
- return this.platforms.delete(instanceId)
89
- }
90
-
91
- /** 聚合所有平台状态:{ [instanceId]: status },status 额外带实例元信息。 */
92
- getStatus() {
93
- const out = {}
94
- for (const platform of this.platforms.values()) {
95
- const instanceId = this.instanceIdOf(platform)
96
- const platformId = platform?.id ?? instanceId
97
- try {
98
- const status = platform.getStatus()
99
- out[instanceId] = {
100
- ...status,
101
- instanceId,
102
- platformId,
103
- displayName: platform.displayName ?? status?.displayName ?? platform.name ?? platformId,
104
- }
105
- } catch (err) {
106
- // 防御性读取 platform.name(getter 可能抛异常)
107
- let name = instanceId
108
- try { name = platform.name ?? instanceId } catch { /* 忽略 */ }
109
- out[instanceId] = {
110
- id: platformId,
111
- name,
112
- status: 'error',
113
- error: err?.message ?? String(err),
114
- instanceId,
115
- platformId,
116
- displayName: platform.displayName ?? name,
117
- }
118
- }
119
- }
120
- return out
121
- }
122
-
123
- /** 释放所有平台。 */
124
- dispose() {
125
- for (const platform of this.platforms.values()) {
126
- try { platform.dispose() } catch { /* 忽略 */ }
127
- }
128
- this.platforms.clear()
129
- }
130
- }
@@ -1,229 +0,0 @@
1
- // 出站消息分块与 SEND_FILE 指令解析(平台无关纯函数)
2
- // 自 conversation-bridge.js 拆出:按平台 maxMessageChars 分块、保留 fenced code block、
3
- // [SEND_FILE: ...] 显式指令提取与路径解析。
4
- import { statSync } from 'node:fs'
5
- import { isAbsolute, normalize, relative, resolve } from 'node:path'
6
-
7
- const FENCE_RE = /^```([^\n`]*)\s*$/
8
-
9
- function normalizeMarkdownBlocks(content) {
10
- const lines = content.split('\n')
11
- const out = []
12
- let blankRun = 0
13
- let inCode = false
14
- for (const raw of lines) {
15
- const line = raw.replace(/\s+$/, '')
16
- if (FENCE_RE.test(line.trim())) {
17
- inCode = !inCode
18
- out.push(line)
19
- blankRun = 0
20
- continue
21
- }
22
- if (inCode) {
23
- out.push(line)
24
- continue
25
- }
26
- if (!line.trim()) {
27
- blankRun += 1
28
- if (blankRun <= 1) out.push('')
29
- continue
30
- }
31
- blankRun = 0
32
- out.push(line)
33
- }
34
- return out.join('\n').trim()
35
- }
36
-
37
- function splitMarkdownBlocks(content) {
38
- const blocks = []
39
- let current = []
40
- let inCode = false
41
- const flush = () => {
42
- const block = current.join('\n').trim()
43
- if (block) blocks.push(block)
44
- current = []
45
- }
46
- for (const raw of content.split('\n')) {
47
- const line = raw.replace(/\s+$/, '')
48
- if (FENCE_RE.test(line.trim())) {
49
- if (!inCode && current.length) flush()
50
- current.push(line)
51
- inCode = !inCode
52
- if (!inCode) flush()
53
- continue
54
- }
55
- if (inCode) {
56
- current.push(line)
57
- continue
58
- }
59
- if (!line.trim()) {
60
- flush()
61
- continue
62
- }
63
- current.push(line)
64
- }
65
- flush()
66
- return blocks
67
- }
68
-
69
- function hardSplit(text, max) {
70
- const chunks = []
71
- let rest = text
72
- while (rest.length > max) {
73
- chunks.push(rest.slice(0, max))
74
- rest = rest.slice(max)
75
- }
76
- if (rest) chunks.push(rest)
77
- return chunks
78
- }
79
-
80
- function packBlocks(blocks, max) {
81
- const units = []
82
- let current = ''
83
- for (const block of blocks) {
84
- const candidate = current ? `${current}\n\n${block}` : block
85
- if (candidate.length <= max) {
86
- current = candidate
87
- continue
88
- }
89
- if (current) units.push(current)
90
- if (block.length <= max) {
91
- current = block
92
- } else {
93
- units.push(...hardSplit(block, max))
94
- current = ''
95
- }
96
- }
97
- if (current) units.push(current)
98
- return units
99
- }
100
-
101
- export function splitForIM(content, max = 2000) {
102
- // 安全检查:防止畸形输入导致无限循环或崩溃
103
- if (typeof content !== 'string' || content.length === 0) return []
104
- if (content.length > 1_000_000) {
105
- content = content.slice(0, 1_000_000) + '\n\n[已截断:内容过长]'
106
- }
107
- const normalized = normalizeMarkdownBlocks(content)
108
- if (!normalized) return []
109
- if (normalized.length <= max) return [normalized]
110
- return packBlocks(splitMarkdownBlocks(normalized), max)
111
- }
112
-
113
- // 协议标记过滤:模型偶尔会把工具调用语法(如 <||DSML||tool_calls>…)当正文
114
- // 输出,这些是协议内容,转发到 IM 只会是乱码噪音。整块移除 tool_calls 段落,
115
- // 再剥掉残余的 DSML 标签;剩余正文照常发送。
116
- export function stripProtocolMarkup(text) {
117
- if (!text || typeof text !== 'string' || !text.includes('DSML')) return text
118
- let out = text.replace(/<[^<>]*DSML[^<>]*tool_calls[^<>]*>[\s\S]*?<[^<>]*DSML[^<>]*tool_calls[^<>]*>/g, '')
119
- out = out.replace(/<[^<>]*DSML[^<>]*>/g, '')
120
- return out
121
- }
122
-
123
- export function textOfAssistantMessage(message) {
124
- const raw = (message.content ?? [])
125
- .filter((block) => block?.type === 'text')
126
- .map((block) => block.text)
127
- .join('\n')
128
- return stripProtocolMarkup(raw)
129
- }
130
-
131
- /**
132
- * 尝试将任意路径(绝对或相对当前工作区)解析为真实存在的本地文件绝对路径
133
- */
134
- export function resolveFilePath(rawPath, cwd = process.cwd()) {
135
- if (typeof rawPath !== 'string') return null
136
- let p = rawPath.trim()
137
- .replace(/^["'`]|["'`]$/g, '')
138
- .replace(/^file:\/\/\/?/, '')
139
- .replace(/^[📁📄📦\s]+/, '')
140
- if (!p) return null
141
- // 排除 HTTP/HTTPS 网址
142
- if (/^https?:\/\//i.test(p)) return null
143
- const resolved = isAbsolute(p) ? normalize(p) : resolve(cwd, p)
144
- try {
145
- if (statSync(resolved).isFile()) {
146
- return resolved
147
- }
148
- } catch {}
149
- return null
150
- }
151
-
152
- /**
153
- * 判断解析后的文件路径是否允许经 [SEND_FILE] 发送给 IM。
154
- * 安全约束(防止模型被诱导后外发任意本地文件):
155
- * 1. 必须位于 allowedRoots 中的某个根目录(含子目录)内 —— 默认仅会话 cwd;
156
- * 2. 路径任何一段不得命中敏感名单(.ssh/.gnupg/.aws/.git/.env/.credentials 等)。
157
- * @param {string} resolvedPath 已解析的绝对路径
158
- * @param {string|string[]} allowedRoots 允许的根目录(绝对路径);默认 process.cwd()
159
- * @returns {boolean}
160
- */
161
- export function isPathAllowedForSend(resolvedPath, allowedRoots = process.cwd()) {
162
- if (typeof resolvedPath !== 'string' || !resolvedPath) return false
163
- const roots = Array.isArray(allowedRoots) ? allowedRoots : [allowedRoots]
164
- if (roots.length === 0) return false
165
-
166
- const normalized = resolve(resolvedPath)
167
- const pathParts = normalized.split(/[\\/]/).filter(Boolean)
168
- const SENSITIVE_PARTS = new Set([
169
- '.ssh', '.gnupg', '.aws', '.azure', '.kube', '.git', '.svn', '.hg',
170
- '.bash_history', '.zsh_history', '.profile', '.bash_profile', '.bashrc',
171
- '.zshrc', '.netrc', '.env', '.npmrc', '.credentials', 'id_rsa', 'id_ed25519',
172
- 'id_ecdsa', 'id_dsa', 'shadow', 'passwd',
173
- ])
174
- for (const part of pathParts) {
175
- if (SENSITIVE_PARTS.has(part.toLowerCase())) return false
176
- }
177
-
178
- for (const root of roots) {
179
- if (typeof root !== 'string' || !root) continue
180
- const normRoot = resolve(root)
181
- if (normalized === normRoot) return true
182
- // 用 relative 判断是否位于根内:越界时 rel 为 '..' 或以 '../' 开头
183
- // (Windows 跨盘则 rel 是绝对路径,isAbsolute 拦截)。跨平台正确处理 \ 与 / 差异。
184
- const rel = relative(normRoot, normalized)
185
- if (rel && !rel.startsWith('..') && !isAbsolute(rel)) return true
186
- }
187
- return false
188
- }
189
-
190
- /**
191
- * 提取并过滤文本中的 [SEND_FILE: <path>] 显式发送指令
192
- * 由 AI 根据用户意图显式决定何时向用户发送文件附件,杜绝底层盲目扫描与误发。
193
- * @param {string} text - 原始助手回复文本
194
- * @param {string} cwd - 会话当前工作目录
195
- * @returns {{ cleanText: string, files: string[] }}
196
- */
197
- export function extractAndStripSendFileDirectives(text, cwd = process.cwd()) {
198
- if (typeof text !== 'string' || !text.trim()) {
199
- return { cleanText: text || '', files: [] }
200
- }
201
-
202
- const files = []
203
- const directiveRegex = /\[(?:SEND_FILE|SEND-FILE|send_file|send-file|SEND_MEDIA|send_media):\s*[`"']?([^\]`"'\r\n]+?)[`"']?\s*\]/gi
204
-
205
- let m
206
- const re = new RegExp(directiveRegex)
207
- while ((m = re.exec(text)) !== null) {
208
- const rawPath = m[1].trim()
209
- const resolved = resolveFilePath(rawPath, cwd)
210
- if (resolved && !files.includes(resolved)) {
211
- files.push(resolved)
212
- }
213
- }
214
-
215
- // 从聊天正文中彻底剔除控制指令(保持 IM 聊天气泡的干净整洁)
216
- const cleanText = text.replace(directiveRegex, '').replace(/\n{3,}/g, '\n\n').trim()
217
-
218
- return { cleanText, files }
219
- }
220
-
221
- /**
222
- * 提取文本中的产物文件路径(基于显式指令)
223
- */
224
- export function extractFilePathsFromText(text, cwd = process.cwd()) {
225
- return extractAndStripSendFileDirectives(text, cwd).files
226
- }
227
-
228
- // ---------------------------------------------------------------------------
229
- // digest 摘要