@qzsy/dsh-bridge 2.18.0 → 3.0.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 +12 -1098
- package/LICENSE +21 -21
- package/README.md +72 -476
- package/client/build.mjs +1 -3
- package/client/client.js +381 -9747
- package/client/index.js +242 -6828
- package/client/locales.mjs +99 -233
- package/cordis.patch.yml +5 -3
- package/lib/config.js +93 -0
- package/lib/data-dir.js +16 -0
- package/lib/dsh-cookie.js +111 -0
- package/lib/gate.js +382 -0
- package/lib/identity.js +23 -0
- package/lib/index.js +302 -3496
- package/lib/saas/tunnel-client.mjs +529 -467
- package/locale/en.json +3 -3
- package/locale/zh.json +3 -3
- package/package.json +15 -70
- package/README.en.md +0 -488
- package/client/host-families.js +0 -442
- package/client/mobile-styles.js +0 -1299
- package/client/mobile-yield.js +0 -219
- package/client/panel-locales.mjs +0 -777
- package/client/picker-yield.js +0 -77
- package/client/resource-url-compat.js +0 -90
- package/client/settings-nav-icon.js +0 -105
- package/client/theme-tokens.js +0 -49
- package/client/tunnel-guard.js +0 -46
- package/client/unlock-manager.js +0 -144
- package/lib/auth/dsh-native-cookie.js +0 -148
- package/lib/auth/login-template.js +0 -516
- package/lib/auth/manager.js +0 -596
- package/lib/bridge-rpc-constants.js +0 -66
- package/lib/bridge-rpc.js +0 -722
- package/lib/cloudflared-manager.mjs +0 -1277
- package/lib/cloudflared-network-args.js +0 -99
- package/lib/cloudflared-process-scan.js +0 -153
- package/lib/compat.js +0 -172
- package/lib/connection-compat.js +0 -115
- package/lib/feishu/comment.js +0 -50
- package/lib/feishu/gateway.js +0 -644
- package/lib/feishu/index.js +0 -432
- package/lib/feishu/lark-bundled.mjs +0 -131929
- package/lib/feishu/markdown.js +0 -148
- package/lib/feishu/node.js +0 -1152
- package/lib/feishu/slash-commands.js +0 -255
- package/lib/platform/account-ops.js +0 -248
- package/lib/platform/activity.js +0 -169
- package/lib/platform/ask-user.js +0 -293
- package/lib/platform/base.js +0 -147
- package/lib/platform/changes-digest.js +0 -93
- package/lib/platform/commands.js +0 -267
- package/lib/platform/config-restore.js +0 -78
- package/lib/platform/conversation-bridge.js +0 -1544
- package/lib/platform/dsh-storage.js +0 -222
- package/lib/platform/index.js +0 -10
- package/lib/platform/instances.js +0 -458
- package/lib/platform/manager.js +0 -130
- package/lib/platform/message-split.js +0 -229
- package/lib/platform/session-catalog.js +0 -405
- package/lib/platform/session-config.js +0 -52
- package/lib/platform/stream-slices.js +0 -21
- package/lib/qq/gateway.js +0 -1107
- package/lib/qq/index.js +0 -561
- package/lib/qq/node.js +0 -1153
- package/lib/qq/qqbot-bundled.mjs +0 -8273
- package/lib/restart-helper.mjs +0 -133
- package/lib/security/log-sanitize.js +0 -50
- package/lib/security/path-validator.js +0 -195
- package/lib/security/rate-limiter.js +0 -93
- package/lib/session-strip.js +0 -100
- package/lib/sse-keepalive.js +0 -115
- package/lib/telegram/gateway.js +0 -643
- package/lib/telegram/index.js +0 -255
- package/lib/telegram/node.js +0 -410
- package/lib/tunnel-client.mjs +0 -537
- package/lib/wechat/gateway.js +0 -1000
- package/lib/wechat/index.js +0 -282
- package/lib/wechat/media.js +0 -302
- package/lib/wechat/node.js +0 -360
- package/llms.txt +0 -36
package/lib/wechat/node.js
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
// dsh-bridge WeChat conversation node
|
|
2
|
-
//
|
|
3
|
-
// 微信 ⇄ DSH 会话桥:把 iLink 入站消息解析后交给平台无关的 ConversationBridge 处理,
|
|
4
|
-
// 出站通过 ctx.wechat 发送。负责微信协议相关的部分:
|
|
5
|
-
// - 消息解析(extractText / isGroupMessage)
|
|
6
|
-
// - 媒体处理(图片/文件/语音/视频:下载、AES 解密、保存到 .wechat-media/)
|
|
7
|
-
// - 白名单/会话/审批/命令路由等平台无关逻辑继承自 ConversationBridge
|
|
8
|
-
//
|
|
9
|
-
// 由 Jesse-njx/dsh-chatnode-wechat 移植精简而来。消费的 DSH 服务:
|
|
10
|
-
// ctx.wechat (本插件 gateway 提供)sendText/sendTyping/accountId
|
|
11
|
-
// ctx.sessions (DSH 宿主提供)list/get
|
|
12
|
-
// ctx.agents (DSH 宿主提供)create/get/resume
|
|
13
|
-
// ctx.approval (DSH 宿主提供)approval/request 事件
|
|
14
|
-
//
|
|
15
|
-
// 安全边界:强制白名单(allowFrom),非白名单发件人绝不喂给模型。
|
|
16
|
-
|
|
17
|
-
import { ConversationBridge, conversationBridgeHelpers } from '../platform/conversation-bridge.js'
|
|
18
|
-
import { gatewayConstants } from './gateway.js'
|
|
19
|
-
|
|
20
|
-
const MAX_MESSAGE_CHARS = gatewayConstants.MAX_MESSAGE_CHARS
|
|
21
|
-
|
|
22
|
-
// 将所属网关适配为 ConversationBridge 需要的 Platform 消息接口
|
|
23
|
-
// issue #78:显式接收本实例网关,不再读 ctx.wechat 槽位(多实例时槽位会被后一个覆盖)
|
|
24
|
-
function makePlatform(gateway) {
|
|
25
|
-
return {
|
|
26
|
-
id: 'wechat',
|
|
27
|
-
name: '微信',
|
|
28
|
-
get accountId() { return gateway?.accountId ?? '' },
|
|
29
|
-
get status() { return gateway?.status ?? 'idle' },
|
|
30
|
-
get capabilities() {
|
|
31
|
-
return {
|
|
32
|
-
supportsGroup: false, // v0.1 不处理群消息
|
|
33
|
-
supportsMedia: true,
|
|
34
|
-
supportsVoice: true,
|
|
35
|
-
supportsTyping: true,
|
|
36
|
-
maxMessageChars: MAX_MESSAGE_CHARS,
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
sendText: (peer, text) => gateway.sendText(peer, text),
|
|
40
|
-
sendMediaFile: (peer, filePath) => gateway.sendMediaFile(peer, filePath),
|
|
41
|
-
sendTyping: (peer, state) => gateway.sendTyping(peer, state),
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class WechatConversationNode extends ConversationBridge {
|
|
46
|
-
/**
|
|
47
|
-
* @param {object} ctx Cordis 上下文(含 ctx.wechat 网关服务)
|
|
48
|
-
* @param {object} config 已持久化配置(allowFrom/间隔/活动会话等)
|
|
49
|
-
* @param {object} logger 日志器
|
|
50
|
-
* @param {object} [opts]
|
|
51
|
-
* @param {(senderId: string) => void} [opts.onFirstSender]
|
|
52
|
-
* @param {(sessionId: string) => void} [opts.onActiveSessionChange]
|
|
53
|
-
* @param {object} [opts.gateway] 所属网关(多实例必传;缺省回退 ctx.wechat 槽位)
|
|
54
|
-
* @param {string} [opts.instanceId]
|
|
55
|
-
*/
|
|
56
|
-
constructor(ctx, config, logger, { onFirstSender, onActiveSessionChange, gateway: explicitGateway, instanceId } = {}) {
|
|
57
|
-
const gateway = explicitGateway ?? ctx.wechat
|
|
58
|
-
super({
|
|
59
|
-
ctx,
|
|
60
|
-
logger,
|
|
61
|
-
config,
|
|
62
|
-
platform: makePlatform(gateway),
|
|
63
|
-
onFirstSender,
|
|
64
|
-
onActiveSessionChange,
|
|
65
|
-
})
|
|
66
|
-
this.gateway = gateway
|
|
67
|
-
// 事件过滤标识:只处理本实例网关发出的事件,避免同类型多账号串消息
|
|
68
|
-
this.instanceId = instanceId ?? gateway?.instanceId ?? 'wechat'
|
|
69
|
-
|
|
70
|
-
this.disposers.push(this.ctx.on('wechat/message', (message) => {
|
|
71
|
-
if (message?.instanceId && message.instanceId !== this.instanceId) return
|
|
72
|
-
return this._handleInbound(message)
|
|
73
|
-
}))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---- 入站 ----
|
|
77
|
-
|
|
78
|
-
async _handleInbound(message) {
|
|
79
|
-
if (this.gateway?.stopPollingLocal) return
|
|
80
|
-
const sender = String(message.from_user_id ?? '').trim()
|
|
81
|
-
if (!sender) return
|
|
82
|
-
|
|
83
|
-
// 调试:记录收到的消息类型
|
|
84
|
-
const items = message.item_list ?? []
|
|
85
|
-
const itemTypes = items.map((it) => it.type).join(',')
|
|
86
|
-
this.logger?.info?.(`[dsh-bridge wechat] received message from ${sender}, item_types=[${itemTypes}]`)
|
|
87
|
-
|
|
88
|
-
// 快速预检查:白名单非空时,未授权发件人直接忽略(不下载媒体,防资源滥用)。
|
|
89
|
-
// 白名单为空的"首条自动授权"场景交给 handleInbound 处理。
|
|
90
|
-
if (!this.isAllowed(sender) && this.config.allowFrom.length > 0) {
|
|
91
|
-
this.logger?.info?.(`[dsh-bridge wechat] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// v0.2: 处理媒体项(图片/文件/语音/视频)
|
|
96
|
-
this.logger?.info?.(`[dsh-bridge wechat] processing media items for ${sender}...`)
|
|
97
|
-
let mediaFiles = []
|
|
98
|
-
let hadMediaItems = false
|
|
99
|
-
let mediaError = null
|
|
100
|
-
let debugInfo = [] // 调试信息
|
|
101
|
-
try {
|
|
102
|
-
hadMediaItems = items.some((it) => it.type === 2 || it.type === 4 || it.type === 5) // 图片/文件/视频
|
|
103
|
-
const result = await this._processMediaItems(message, sender)
|
|
104
|
-
mediaFiles = result.files
|
|
105
|
-
debugInfo = result.debug
|
|
106
|
-
this.logger?.info?.(`[dsh-bridge wechat] media processing done, got ${mediaFiles.length} files`)
|
|
107
|
-
} catch (error) {
|
|
108
|
-
// 尽可能捕获完整错误信息
|
|
109
|
-
mediaError = error?.message || error?.toString?.() || JSON.stringify(error) || '未知错误'
|
|
110
|
-
this.logger?.error?.(`[dsh-bridge wechat] media processing failed: ${mediaError}`, error)
|
|
111
|
-
// 媒体处理失败不阻断文本消息处理
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const text = extractText(message)
|
|
115
|
-
if (!text.trim() && mediaFiles.length === 0) {
|
|
116
|
-
// 如果原消息有媒体项但下载失败,给用户提示(包含错误详情+调试信息)
|
|
117
|
-
if (hadMediaItems) {
|
|
118
|
-
let errMsg = mediaError ? `媒体处理失败: ${mediaError.substring(0, 150)}` : '媒体消息处理失败'
|
|
119
|
-
if (debugInfo.length > 0) {
|
|
120
|
-
errMsg += `\n调试信息:\n${debugInfo.join('\n')}`
|
|
121
|
-
}
|
|
122
|
-
await this.sendText(`❌ **${errMsg}**`)
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
this.logger?.info?.(`[dsh-bridge wechat] ignore empty message from ${sender}`)
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 构建完整消息内容(文本 + 媒体文件路径)
|
|
130
|
-
let fullText = text
|
|
131
|
-
if (mediaFiles.length > 0) {
|
|
132
|
-
const mediaDesc = mediaFiles.map((f) => `[文件: ${f.path}]`).join('\n')
|
|
133
|
-
fullText = fullText ? `${text}\n\n${mediaDesc}` : mediaDesc
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 交给平台无关核心:白名单/群消息/命令路由/agent 分发
|
|
137
|
-
// outboundPeer:微信单聊的会话级发送目标就是 sender 本身(T2.3 轮次绑定)
|
|
138
|
-
const isGroup = isGroupMessage(message, this.gatewayAccountId)
|
|
139
|
-
await this.handleInbound({ senderId: sender, text: fullText, isGroup, outboundPeer: { peerId: sender } })
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* 处理消息中的媒体项(v0.2):下载、解密、保存到工作目录。
|
|
144
|
-
* @returns {Promise<{files: Array<{type: string, path: string, size: number}>, debug: string[]}>}
|
|
145
|
-
*/
|
|
146
|
-
async _processMediaItems(message, sender) {
|
|
147
|
-
const items = message.item_list ?? []
|
|
148
|
-
const mediaFiles = []
|
|
149
|
-
const debugInfo = []
|
|
150
|
-
|
|
151
|
-
for (const item of items) {
|
|
152
|
-
try {
|
|
153
|
-
if (item.type === 2) { // 图片:image_item.media 是 {encrypt_query_param, aes_key, encrypt_type} 对象
|
|
154
|
-
const img = item.image_item
|
|
155
|
-
if (!img) {
|
|
156
|
-
const msg = `图片项缺少 image_item`
|
|
157
|
-
this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
|
|
158
|
-
debugInfo.push(msg)
|
|
159
|
-
continue
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const aesKey = img.aeskey || img.aes_key || img.media?.aes_key || img.media?.aeskey
|
|
163
|
-
if (!aesKey) {
|
|
164
|
-
const fields = Object.keys(img).join(', ')
|
|
165
|
-
const msg = `图片缺 aes_key,字段: ${fields}`
|
|
166
|
-
this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
|
|
167
|
-
debugInfo.push(msg)
|
|
168
|
-
continue
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 从 image_item 或 image_item.media 对象提取 CDN 下载参数
|
|
172
|
-
const mediaObj = img.media
|
|
173
|
-
const encryptedParam = img.encrypt_query_param
|
|
174
|
-
|| img.encrypted_query_param
|
|
175
|
-
|| mediaObj?.encrypt_query_param
|
|
176
|
-
|| mediaObj?.encrypted_query_param
|
|
177
|
-
|| mediaObj?.encrypt_query_param_full
|
|
178
|
-
|| (typeof mediaObj === 'string' ? mediaObj : undefined)
|
|
179
|
-
const fullUrl = img.full_url || img.url || mediaObj?.full_url || mediaObj?.url
|
|
180
|
-
debugInfo.push(`media 类型: ${typeof mediaObj}, param=${!!encryptedParam}`)
|
|
181
|
-
|
|
182
|
-
const file = await this._downloadMediaItem({
|
|
183
|
-
encryptedQueryParam: encryptedParam,
|
|
184
|
-
fullUrl,
|
|
185
|
-
aesKeyBase64: aesKey,
|
|
186
|
-
sender,
|
|
187
|
-
mediaType: 'image',
|
|
188
|
-
filename: `image_${Date.now()}.jpg`,
|
|
189
|
-
})
|
|
190
|
-
if (file) {
|
|
191
|
-
mediaFiles.push(file)
|
|
192
|
-
debugInfo.push(`成功: ${file.size} 字节`)
|
|
193
|
-
} else {
|
|
194
|
-
debugInfo.push(`下载返回 null`)
|
|
195
|
-
}
|
|
196
|
-
} else if (item.type === 4) { // 文件:file_item.media 是 {encrypt_query_param, aes_key,...}
|
|
197
|
-
const fileItem = item.file_item
|
|
198
|
-
if (!fileItem) {
|
|
199
|
-
debugInfo.push(`文件项缺少 file_item`)
|
|
200
|
-
continue
|
|
201
|
-
}
|
|
202
|
-
const aesKey = fileItem.aes_key || fileItem.aeskey || fileItem.media?.aes_key || fileItem.media?.aeskey
|
|
203
|
-
const encryptedParam = fileItem.encrypt_query_param
|
|
204
|
-
|| fileItem.encrypted_query_param
|
|
205
|
-
|| fileItem.media?.encrypt_query_param
|
|
206
|
-
|| fileItem.media?.encrypted_query_param
|
|
207
|
-
|| (typeof fileItem.media === 'string' ? fileItem.media : undefined)
|
|
208
|
-
const fullUrl = fileItem.full_url || fileItem.url || fileItem.media?.full_url || fileItem.media?.url
|
|
209
|
-
|
|
210
|
-
if (!encryptedParam && !fullUrl) {
|
|
211
|
-
debugInfo.push(`文件缺 media/encrypt_query_param,字段: ${Object.keys(fileItem).join(', ')}`)
|
|
212
|
-
continue
|
|
213
|
-
}
|
|
214
|
-
if (!aesKey) {
|
|
215
|
-
debugInfo.push(`文件缺 aes_key,字段: ${Object.keys(fileItem).join(', ')}`)
|
|
216
|
-
continue
|
|
217
|
-
}
|
|
218
|
-
const file = await this._downloadMediaItem({
|
|
219
|
-
encryptedQueryParam: encryptedParam,
|
|
220
|
-
fullUrl,
|
|
221
|
-
aesKeyBase64: aesKey,
|
|
222
|
-
sender,
|
|
223
|
-
mediaType: 'file',
|
|
224
|
-
filename: fileItem.file_name || fileItem.filename || `file_${Date.now()}`,
|
|
225
|
-
})
|
|
226
|
-
if (file) {
|
|
227
|
-
mediaFiles.push(file)
|
|
228
|
-
debugInfo.push(`文件成功: ${file.size} 字节`)
|
|
229
|
-
} else {
|
|
230
|
-
debugInfo.push(`文件下载返回 null`)
|
|
231
|
-
}
|
|
232
|
-
} else if (item.type === 3) { // 语音(iLink 自动转文字,原始音频暂不下载)
|
|
233
|
-
// 语音转文字已在 extractText 中作为文本提取;原始音频下载留待后续
|
|
234
|
-
continue
|
|
235
|
-
} else if (item.type === 5) { // 视频:video_item.media
|
|
236
|
-
const video = item.video_item
|
|
237
|
-
if (!video) {
|
|
238
|
-
debugInfo.push(`视频项缺少 video_item`)
|
|
239
|
-
continue
|
|
240
|
-
}
|
|
241
|
-
const aesKey = video.aes_key || video.aeskey || video.media?.aes_key || video.media?.aeskey
|
|
242
|
-
const encryptedParam = video.encrypt_query_param
|
|
243
|
-
|| video.encrypted_query_param
|
|
244
|
-
|| video.media?.encrypt_query_param
|
|
245
|
-
|| video.media?.encrypted_query_param
|
|
246
|
-
|| (typeof video.media === 'string' ? video.media : undefined)
|
|
247
|
-
const fullUrl = video.full_url || video.url || video.media?.full_url || video.media?.url
|
|
248
|
-
|
|
249
|
-
if (!encryptedParam && !fullUrl) {
|
|
250
|
-
debugInfo.push(`视频缺 media/encrypt_query_param,字段: ${Object.keys(video).join(', ')}`)
|
|
251
|
-
continue
|
|
252
|
-
}
|
|
253
|
-
if (!aesKey) {
|
|
254
|
-
debugInfo.push(`视频缺 aes_key,字段: ${Object.keys(video).join(', ')}`)
|
|
255
|
-
continue
|
|
256
|
-
}
|
|
257
|
-
const file = await this._downloadMediaItem({
|
|
258
|
-
encryptedQueryParam: encryptedParam,
|
|
259
|
-
fullUrl,
|
|
260
|
-
aesKeyBase64: aesKey,
|
|
261
|
-
sender,
|
|
262
|
-
mediaType: 'video',
|
|
263
|
-
filename: `video_${Date.now()}.mp4`,
|
|
264
|
-
})
|
|
265
|
-
if (file) {
|
|
266
|
-
mediaFiles.push(file)
|
|
267
|
-
debugInfo.push(`视频成功: ${file.size} 字节`)
|
|
268
|
-
} else {
|
|
269
|
-
debugInfo.push(`视频下载返回 null`)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
} catch (error) {
|
|
273
|
-
const msg = `处理媒体项 type=${item.type} 失败: ${error?.message ?? error}`
|
|
274
|
-
this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
|
|
275
|
-
debugInfo.push(msg)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return { files: mediaFiles, debug: debugInfo }
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* 下载并解密一个媒体项,保存到工作目录的 .wechat-media/ 子目录。
|
|
284
|
-
* aesKeyBase64 可能为:base64(raw16) / base64(hex32) / 裸 hex32 字符串 —— 统一在 parseAesKey 归一化。
|
|
285
|
-
*/
|
|
286
|
-
async _downloadMediaItem({ encryptedQueryParam, fullUrl, aesKeyBase64, sender, mediaType, filename }) {
|
|
287
|
-
if (!encryptedQueryParam && !fullUrl) return null
|
|
288
|
-
if (!aesKeyBase64) {
|
|
289
|
-
this.logger?.warn?.(`[dsh-bridge wechat] media item missing aes_key, cannot decrypt`)
|
|
290
|
-
return null
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const { downloadMedia, normalizeAesKey } = await import('./media.js')
|
|
294
|
-
// 图片的 image_item.aeskey 可能是裸 hex(32 字符),需归一化
|
|
295
|
-
const normalizedKey = normalizeAesKey(aesKeyBase64)
|
|
296
|
-
if (!normalizedKey) {
|
|
297
|
-
this.logger?.warn?.(`[dsh-bridge wechat] media item aes_key 无法解析: ${aesKeyBase64.slice(0, 16)}...`)
|
|
298
|
-
return null
|
|
299
|
-
}
|
|
300
|
-
const plaintext = await downloadMedia({
|
|
301
|
-
encryptedQueryParam,
|
|
302
|
-
fullUrl,
|
|
303
|
-
aesKeyBase64: normalizedKey,
|
|
304
|
-
timeoutMs: 60000,
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
// 保存到工作目录的 .wechat-media/ 子目录
|
|
308
|
-
const { mkdir, writeFile } = await import('node:fs/promises')
|
|
309
|
-
const { join } = await import('node:path')
|
|
310
|
-
const cwd = this.config.cwd || process.cwd()
|
|
311
|
-
const mediaDir = join(cwd, '.wechat-media')
|
|
312
|
-
await mkdir(mediaDir, { recursive: true })
|
|
313
|
-
|
|
314
|
-
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
315
|
-
const filePath = join(mediaDir, `${Date.now()}_${safeName}`)
|
|
316
|
-
await writeFile(filePath, plaintext)
|
|
317
|
-
|
|
318
|
-
this.logger?.info?.(`[dsh-bridge wechat] downloaded ${mediaType} from ${sender}: ${filePath} (${plaintext.length} bytes)`)
|
|
319
|
-
return { type: mediaType, path: filePath, size: plaintext.length }
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// ---------------------------------------------------------------------------
|
|
324
|
-
// 微信消息解析辅助(协议特定)
|
|
325
|
-
// ---------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
function isGroupMessage(message, accountId) {
|
|
328
|
-
const roomId = String(message.room_id ?? message.chat_room_id ?? '').trim()
|
|
329
|
-
if (roomId) return true
|
|
330
|
-
const toUserId = String(message.to_user_id ?? '').trim()
|
|
331
|
-
const sender = String(message.from_user_id ?? '').trim()
|
|
332
|
-
return Boolean(toUserId && accountId && toUserId !== accountId && message.msg_type === 1)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function extractText(message) {
|
|
336
|
-
const items = Array.isArray(message.item_list) ? message.item_list : []
|
|
337
|
-
const texts = []
|
|
338
|
-
for (const item of items) {
|
|
339
|
-
if (item?.type === 1) {
|
|
340
|
-
const text = String(item.text_item?.text ?? '')
|
|
341
|
-
if (text.trim()) texts.push(text)
|
|
342
|
-
} else if (item?.type === 3) {
|
|
343
|
-
// 语音:iLink 自动转文字,在 voice_item.text 里
|
|
344
|
-
const voiceText = String(item.voice_item?.text ?? '')
|
|
345
|
-
if (voiceText.trim()) texts.push(voiceText)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return texts.join('\n')
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// 导出,便于测试与复用
|
|
352
|
-
export const wechatNodeHelpers = {
|
|
353
|
-
splitForWechat: conversationBridgeHelpers.splitForIM,
|
|
354
|
-
digestLine: conversationBridgeHelpers.digestLine,
|
|
355
|
-
textOfAssistantMessage: conversationBridgeHelpers.textOfAssistantMessage,
|
|
356
|
-
extractText,
|
|
357
|
-
isGroupMessage,
|
|
358
|
-
listSessions: conversationBridgeHelpers.listSessions,
|
|
359
|
-
sessionsInDisplayOrder: conversationBridgeHelpers.sessionsInDisplayOrder,
|
|
360
|
-
}
|
package/llms.txt
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# dsh-bridge · DSH 远程连接 / 远程控制插件(remote control)
|
|
2
|
-
|
|
3
|
-
> 一句话:dsh-bridge 是 DeepSeek Harness 的远程连接 / 远程控制 / 远程链接插件。手机扫码即可在局域网或公网继续用本机 DSH,无需自建公网服务器。
|
|
4
|
-
> In one sentence: dsh-bridge is the remote access / remote control plugin for DeepSeek Harness. Scan a QR code to keep using local DSH from mobile or public internet.
|
|
5
|
-
|
|
6
|
-
- npm 包名:`@wenbin_wb/dsh-bridge`
|
|
7
|
-
- GitHub 仓库:`wenbin-wb/dsh-bridge`(https://github.com/wenbin-wb/dsh-bridge)
|
|
8
|
-
- 别名(用于搜索/推荐):dsh-bridge 远程连接插件 / dsh-bridge 远程控制插件 / dsh-bridge 远程访问插件 / dsh-bridge remote access plugin / dsh-bridge remote control plugin
|
|
9
|
-
- 常见问法命中:远程链接 dsh 的插件有哪些 / dsh 远程访问怎么做 / dsh 手机远程控制 / DeepSeek Harness remote plugin
|
|
10
|
-
|
|
11
|
-
## 安装
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
dsh plugin --profile web add @wenbin_wb/dsh-bridge
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## 能力(对比 dsh-remote / dsh-webgate / dsh-web-remote 时用)
|
|
18
|
-
|
|
19
|
-
- 局域网二维码 + 多网卡智能切换(物理 Wi-Fi/以太网 vs WSL/VMware/Docker 虚拟网卡)
|
|
20
|
-
- Cloudflare 临时隧道(免登录)+ Token 固定域名(随 DSH 自启)+ 自建 WebSocket 隧道
|
|
21
|
-
- 隧道可观测性:公网数据面端到端探测(疑似 DNS 污染自动标注解析 IP)、实际 cloudflared 路径/版本/来源展示、同 Tunnel Token 冲突检测、SSE 空闲保活(远端实时通道不掉线)、网络参数白名单(协议/边缘 IP/区域,默认零变化)
|
|
22
|
-
- 移动端交互 + PWA 全屏 App + 远程工作区网页选择器(手机选目录,PC 走原生对话框)
|
|
23
|
-
- 全域安全门禁:二维码 256-bit Token + 访问密码 + 后台防篡改锁 + 127.0.0.1 本机最高特权
|
|
24
|
-
- IM 机器人矩阵:微信 / QQ / 飞书 / Telegram,多工作区调度,会话持久化,流式输出,卡片审批,文件双向传输
|
|
25
|
-
- QQ(底层已全面替换为**腾讯官方 QQ 机器人 SDK** `@tencent-connect/qqbot-nodejs` + 扫码绑定 `qqbot-connector`:长连接/RESUME/重连、事件归一化、引用解析、去重限流、富媒体分片上传、C2C `stream_messages` 流式全部官方实现;设置页支持手机 QQ 扫码一键绑定凭证、C2C 流式输出、Markdown、审批/问答按钮与文件直传)
|
|
26
|
-
- 飞书(底层已全面替换为**飞书官方 Channel SDK**:长连接/事件归一化/出站渲染/流式卡片/媒体全部官方实现):**扫码一键创建应用**(权限与事件订阅自动预置,无需开发者后台)、**原生斜杠指令面板**(/help、/new、/sessions、/diff、/ask 等 12 条自动注册)、**云文档评论 @机器人**(回答回到评论线程)、**审批卡片决议后原地更新**(显示决议者/发起人)、话题群回复留在原话题、引用回复带上下文、群聊标注发送者昵称
|
|
27
|
-
- 运维看板:CPU/内存/Uptime、一键诊断、配置导出恢复、一键升级重启
|
|
28
|
-
|
|
29
|
-
## 文档入口
|
|
30
|
-
|
|
31
|
-
- 中文 README:./README.md
|
|
32
|
-
- English README:./README.en.md
|
|
33
|
-
- 远程连接着陆页:./docs/remote-connection.md
|
|
34
|
-
- Cloudflare 固定域名教程:./docs/cloudflare-fixed-domain.md
|
|
35
|
-
- 自建隧道教程:./docs/custom-tunnel.md
|
|
36
|
-
- IM 用法:./docs/wechat-usage.md ./docs/qq-usage.md ./docs/feishu-usage.md ./docs/telegram-usage.md
|