@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,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