@qzsy/dsh-bridge 0.0.0-stage → 2.18.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 (77) hide show
  1. package/CHANGELOG.md +1107 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +488 -0
  4. package/README.md +509 -2
  5. package/client/build.mjs +43 -0
  6. package/client/client.js +9864 -0
  7. package/client/host-families.js +442 -0
  8. package/client/index.js +6883 -0
  9. package/client/locales.mjs +242 -0
  10. package/client/mobile-styles.js +1299 -0
  11. package/client/mobile-yield.js +219 -0
  12. package/client/panel-locales.mjs +777 -0
  13. package/client/picker-yield.js +77 -0
  14. package/client/resource-url-compat.js +90 -0
  15. package/client/settings-nav-icon.js +105 -0
  16. package/client/theme-tokens.js +49 -0
  17. package/client/tunnel-guard.js +46 -0
  18. package/client/unlock-manager.js +144 -0
  19. package/cordis.patch.yml +4 -0
  20. package/lib/auth/dsh-native-cookie.js +148 -0
  21. package/lib/auth/login-template.js +516 -0
  22. package/lib/auth/manager.js +596 -0
  23. package/lib/bridge-rpc-constants.js +66 -0
  24. package/lib/bridge-rpc.js +722 -0
  25. package/lib/cloudflared-manager.mjs +1277 -0
  26. package/lib/cloudflared-network-args.js +99 -0
  27. package/lib/cloudflared-process-scan.js +153 -0
  28. package/lib/compat.js +172 -0
  29. package/lib/connection-compat.js +115 -0
  30. package/lib/feishu/comment.js +50 -0
  31. package/lib/feishu/gateway.js +644 -0
  32. package/lib/feishu/index.js +432 -0
  33. package/lib/feishu/lark-bundled.mjs +131929 -0
  34. package/lib/feishu/markdown.js +148 -0
  35. package/lib/feishu/node.js +1152 -0
  36. package/lib/feishu/slash-commands.js +255 -0
  37. package/lib/index.js +3554 -0
  38. package/lib/platform/account-ops.js +248 -0
  39. package/lib/platform/activity.js +169 -0
  40. package/lib/platform/ask-user.js +293 -0
  41. package/lib/platform/base.js +147 -0
  42. package/lib/platform/changes-digest.js +93 -0
  43. package/lib/platform/commands.js +267 -0
  44. package/lib/platform/config-restore.js +78 -0
  45. package/lib/platform/conversation-bridge.js +1544 -0
  46. package/lib/platform/dsh-storage.js +222 -0
  47. package/lib/platform/index.js +10 -0
  48. package/lib/platform/instances.js +458 -0
  49. package/lib/platform/manager.js +130 -0
  50. package/lib/platform/message-split.js +229 -0
  51. package/lib/platform/session-catalog.js +405 -0
  52. package/lib/platform/session-config.js +52 -0
  53. package/lib/platform/stream-slices.js +21 -0
  54. package/lib/qq/gateway.js +1107 -0
  55. package/lib/qq/index.js +561 -0
  56. package/lib/qq/node.js +1153 -0
  57. package/lib/qq/qqbot-bundled.mjs +8273 -0
  58. package/lib/restart-helper.mjs +133 -0
  59. package/lib/saas/device-auth.mjs +366 -0
  60. package/lib/saas/tunnel-client.mjs +695 -0
  61. package/lib/security/log-sanitize.js +50 -0
  62. package/lib/security/path-validator.js +195 -0
  63. package/lib/security/rate-limiter.js +93 -0
  64. package/lib/session-strip.js +100 -0
  65. package/lib/sse-keepalive.js +115 -0
  66. package/lib/telegram/gateway.js +643 -0
  67. package/lib/telegram/index.js +255 -0
  68. package/lib/telegram/node.js +410 -0
  69. package/lib/tunnel-client.mjs +537 -0
  70. package/lib/wechat/gateway.js +1000 -0
  71. package/lib/wechat/index.js +282 -0
  72. package/lib/wechat/media.js +302 -0
  73. package/lib/wechat/node.js +360 -0
  74. package/llms.txt +36 -0
  75. package/locale/en.json +6 -0
  76. package/locale/zh.json +6 -0
  77. package/package.json +135 -4
@@ -0,0 +1,248 @@
1
+ // 平台账号运行时编排(issue #78 / verifier D2、D3、D4)
2
+ //
3
+ // 把「新增 / 移除 / 凭证查重 / 账号实例同步」的编排从 lib/index.js 抽成可注入依赖的
4
+ // factory:装配级单测可以用 fake 的 config 读写 / PlatformManager / 实例构造器覆盖
5
+ // 「查重命中不落库、移除确实删配置、并发同步不重复构造」等关键分支。
6
+ //
7
+ // 约定:
8
+ // - 所有写操作(add / remove / sync)都在**同一个串行队列**里执行,避免
9
+ // "config 已删、管理器又复活" 这类交错;
10
+ // - 每次实例创建都分配**唯一 cordis 服务名**(首次 instanceId,重复创建 instanceId#1、
11
+ // instanceId#2…):cordis 的服务名不会随实例 dispose 释放,复用会抛
12
+ // `service "x" has been registered`(verifier D2 实测);
13
+ // - create 失败不静默:reconcile 报告里的 errors 由调用方检查(importBackup 用
14
+ // assertNoCreateErrors 抛可读错误,RPC 转成 bad-request)。
15
+
16
+ import {
17
+ PLATFORM_TYPES,
18
+ PLATFORM_TYPE_LABELS,
19
+ normalizePlatformSection,
20
+ nextAccount,
21
+ removeAccount,
22
+ findDuplicateCredential,
23
+ displayNameFor,
24
+ platformTypeOf,
25
+ reconcilePlatformInstances,
26
+ } from './instances.js'
27
+
28
+ /**
29
+ * 同步报告里若存在 `create` 阶段的失败,抛出可读错误(避免账号只写日志、面板看不到)。
30
+ *
31
+ * @param {{errors?: Array<{phase: string, instanceId: string, message: string}>}} report
32
+ * @param {{action?: string}} [opts]
33
+ */
34
+ export function assertNoCreateErrors(report, { action = '平台账号同步' } = {}) {
35
+ const failures = (report?.errors ?? []).filter((row) => row?.phase === 'create')
36
+ if (failures.length === 0) return
37
+ const ids = failures.map((row) => row.instanceId).join('、')
38
+ const first = failures[0]?.message ?? ''
39
+ throw new Error(`${action}失败:${failures.length} 个账号未能创建(${ids})${first ? `:${first}` : ''}`)
40
+ }
41
+
42
+ /**
43
+ * @param {object} deps
44
+ * @param {object} [deps.logger]
45
+ * @param {object} [deps.ctx] cordis 上下文(维护 ctx.<platform> 兼容槽位)
46
+ * @param {() => Promise<object>} deps.loadNormalizedConfig 读取并规范化后的 config
47
+ * @param {(mutate: Function) => Promise<object>} deps.updateConfig 读-改-写事务
48
+ * @param {import('./manager.js').PlatformManager} deps.platformManager
49
+ * @param {(type: string, account: object, index: number, opts: {serviceName: string}) => object} deps.createInstance
50
+ * @param {(service: object, type: string, account: object, created: boolean) => any} [deps.restoreInstance]
51
+ * @param {(service: object, type: string, account: object) => any} [deps.startInstance]
52
+ * @param {(service: object) => object} [deps.statusOf]
53
+ * @param {(type: string, firstInstance: object|undefined) => void} [deps.onInstancesChanged]
54
+ */
55
+ export function createPlatformAccountOps({
56
+ logger,
57
+ ctx,
58
+ loadNormalizedConfig,
59
+ updateConfig,
60
+ platformManager,
61
+ createInstance,
62
+ restoreInstance,
63
+ startInstance,
64
+ statusOf,
65
+ onInstancesChanged,
66
+ } = {}) {
67
+ if (typeof loadNormalizedConfig !== 'function' || typeof updateConfig !== 'function'
68
+ || !platformManager || typeof createInstance !== 'function') {
69
+ throw new Error('createPlatformAccountOps: 缺少必需依赖(loadNormalizedConfig/updateConfig/platformManager/createInstance)')
70
+ }
71
+
72
+ let queue = Promise.resolve()
73
+ // instanceId -> 已分配过的 cordis 服务名次数(服务名不复用:cordis 不释放已注册名)
74
+ const serviceNames = new Map()
75
+ // type -> 当前占用 ctx.<platform> 槽位的 instanceId(本模块自己记账,避免误清别人的槽位)
76
+ const slotOwners = new Map()
77
+
78
+ /** 为一次实例创建分配唯一 cordis 服务名。 */
79
+ function allocateServiceName(instanceId) {
80
+ const used = serviceNames.get(instanceId) ?? 0
81
+ serviceNames.set(instanceId, used + 1)
82
+ return used === 0 ? instanceId : `${instanceId}#${used}`
83
+ }
84
+
85
+ /** 串行执行器:所有写操作共用,返回本次 task 的结果(可 reject 给调用方)。 */
86
+ function runExclusive(task) {
87
+ const run = queue.then(task)
88
+ queue = run.then(() => undefined, (err) => {
89
+ logger?.error?.('dsh-bridge: 平台账号操作失败:%s', err?.stack ?? err)
90
+ })
91
+ return run
92
+ }
93
+
94
+ /** 占用 ctx.<platform> 兼容槽位(失败不致命:只影响旧读取方)。 */
95
+ function assignSlot(type, service) {
96
+ const target = service?.gateway ?? service
97
+ try { ctx[type] = target } catch { /* 只读/框架托管时忽略 */ }
98
+ slotOwners.set(type, service?.instanceId ?? type)
99
+ }
100
+
101
+ /** 清空 ctx.<platform> 槽位(实例被移除后不得再指向已 dispose 的网关)。 */
102
+ function clearSlot(type) {
103
+ try { delete ctx[type] } catch { /* 忽略 */ }
104
+ try { if (ctx?.[type]) ctx[type] = undefined } catch { /* 忽略 */ }
105
+ slotOwners.delete(type)
106
+ }
107
+
108
+ /** 同步某个类型的槽位/首实例回调(仅在占位实例变化时写,减少对框架托管的干扰)。 */
109
+ function refreshType(type) {
110
+ const first = platformManager.listByType(type)[0]
111
+ const firstId = first?.instanceId ?? first?.id
112
+ if (!first) {
113
+ if (slotOwners.has(type)) clearSlot(type)
114
+ } else if (slotOwners.get(type) !== firstId) {
115
+ assignSlot(type, first)
116
+ }
117
+ onInstancesChanged?.(type, first)
118
+ }
119
+
120
+ /** 按 config 规范化结果创建/恢复/清理实例(同步核心,调用方负责串行化)。 */
121
+ async function reconcile({ start = true } = {}) {
122
+ const stored = await loadNormalizedConfig()
123
+ const sections = {}
124
+ for (const type of PLATFORM_TYPES) {
125
+ sections[type] = normalizePlatformSection(type, stored?.[type]).section
126
+ }
127
+ const report = await reconcilePlatformInstances({
128
+ platformManager,
129
+ sections,
130
+ logger,
131
+ start,
132
+ createInstance: (type, account, index) => createInstance(type, account, index, {
133
+ serviceName: allocateServiceName(account.instanceId),
134
+ }),
135
+ restoreInstance,
136
+ startInstance,
137
+ })
138
+ for (const type of PLATFORM_TYPES) refreshType(type)
139
+ return report
140
+ }
141
+
142
+ /** 公开的账号同步入口(串行化)。 */
143
+ function syncAccounts(options = {}) {
144
+ return runExclusive(() => reconcile(options))
145
+ }
146
+
147
+ /** 运行期新增账号(RPC platformAddAccount):类型/上限校验 → 追加 → 同步并启动。 */
148
+ function addAccount({ platformId, name } = {}) {
149
+ const type = typeof platformId === 'string' ? platformId : ''
150
+ if (!PLATFORM_TYPES.includes(type)) {
151
+ return Promise.reject(new Error(`不支持的平台类型: ${type || '(空)'}`))
152
+ }
153
+ return runExclusive(async () => {
154
+ let created = null
155
+ await updateConfig((stored) => {
156
+ const { section } = normalizePlatformSection(type, stored[type])
157
+ // 上限校验在 nextAccount 内(D3),超限时 mutate 抛错 → 不落库
158
+ const result = nextAccount({ section, type, name })
159
+ stored[type] = result.section
160
+ created = result.account
161
+ return stored
162
+ })
163
+ await reconcile({ start: true })
164
+ const service = platformManager.getExact(created.instanceId)
165
+ return {
166
+ instanceId: created.instanceId,
167
+ status: service && typeof statusOf === 'function' ? statusOf(service) : null,
168
+ }
169
+ })
170
+ }
171
+
172
+ /** 运行期移除账号(RPC platformRemoveAccount):允许移除最后一个账号(回到 0 账号)。 */
173
+ function removeAccountById({ instanceId } = {}) {
174
+ if (!instanceId || typeof instanceId !== 'string') {
175
+ return Promise.reject(new Error('缺少 instanceId 参数'))
176
+ }
177
+ const type = platformTypeOf(instanceId)
178
+ if (!PLATFORM_TYPES.includes(type)) {
179
+ return Promise.reject(new Error(`账号不存在: ${instanceId}`))
180
+ }
181
+ return runExclusive(async () => {
182
+ const stored = await loadNormalizedConfig()
183
+ const { section } = normalizePlatformSection(type, stored?.[type])
184
+ if (!section.accounts.some((account) => account.instanceId === instanceId)) {
185
+ throw new Error(`账号不存在: ${instanceId}`)
186
+ }
187
+
188
+ const platform = platformManager.getExact(instanceId)
189
+ if (platform) {
190
+ try {
191
+ await platform.stop?.()
192
+ } catch (err) {
193
+ logger?.warn?.('dsh-bridge: 停用平台实例 %s 失败(继续移除):%s', instanceId, err?.message ?? err)
194
+ }
195
+ try {
196
+ platform.dispose?.()
197
+ } catch (err) {
198
+ logger?.warn?.('dsh-bridge: 释放平台实例 %s 失败(继续移除):%s', instanceId, err?.message ?? err)
199
+ }
200
+ platformManager.unregister(instanceId)
201
+ }
202
+
203
+ await updateConfig((current) => {
204
+ const { section: currentSection } = normalizePlatformSection(type, current?.[type])
205
+ current[type] = removeAccount(currentSection, instanceId)
206
+ return current
207
+ })
208
+
209
+ // 槽位顺位/清空:被移除的是当前占位实例时不得留下已 dispose 的网关
210
+ refreshType(type)
211
+ return { ok: true, instanceId }
212
+ })
213
+ }
214
+
215
+ /** 同类型内凭证唯一性校验(platformSetConfig 保存前)。 */
216
+ async function validateAccountConfig(platform, patch) {
217
+ const instanceId = platform?.instanceId ?? platform?.id
218
+ const type = platform?.id ?? platformTypeOf(instanceId)
219
+ if (!PLATFORM_TYPES.includes(type)) return
220
+ const stored = await loadNormalizedConfig()
221
+ const { section } = normalizePlatformSection(type, stored?.[type])
222
+ const candidate = { ...(platform?.config ?? {}), ...(patch ?? {}) }
223
+ const duplicate = findDuplicateCredential({
224
+ type,
225
+ accounts: section.accounts,
226
+ candidate,
227
+ excludeInstanceId: instanceId,
228
+ })
229
+ if (!duplicate) return
230
+ const index = section.accounts.findIndex((account) => account.instanceId === duplicate.instanceId)
231
+ const displayName = displayNameFor({
232
+ type,
233
+ label: PLATFORM_TYPE_LABELS[type] ?? platform?.name,
234
+ account: duplicate,
235
+ index: index + 1,
236
+ })
237
+ throw new Error(`同一应用不能重复添加:该凭证已被「${displayName}」使用`)
238
+ }
239
+
240
+ return {
241
+ syncAccounts,
242
+ addAccount,
243
+ removeAccount: removeAccountById,
244
+ validateAccountConfig,
245
+ allocateServiceName,
246
+ runExclusive,
247
+ }
248
+ }
@@ -0,0 +1,169 @@
1
+ // 轮次进行中的「状态/活动」文案:把宿主工具调用翻译成用户可读的一行状态。
2
+ //
3
+ // 为什么做:Web 端在轮次中会显示「正在分析请求 / 正在运行命令 / 正在编辑文件 …」,
4
+ // 而 IM 侧此前只在心跳里给一句「[处理中] 第 N 轮 | M 次工具调用」——用户看不到
5
+ // agent 到底在干什么,长任务期间体感像"卡住了"。
6
+ //
7
+ // 关键设计:**工具→活动种类的映射直接对齐宿主的实现**,而不是自己另发明一套。
8
+ // 宿主的口径在 `dsh-client-ui-chat` 的 `activity(name)`(本机核实于
9
+ // node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js),
10
+ // 对应文案是 `message.stepProcess.*` 语言键。对齐后:
11
+ // - IM 与 Web 对同一工具给出同一说法,用户不必学两套;
12
+ // - 宿主后续增删映射时,我们只需跟着调整这一处(并有测试锁住当前口径)。
13
+ //
14
+ // 本模块是**纯函数**(无 IO、无状态),便于单测与各平台复用。
15
+
16
+ /** 活动种类 → 进行中文案(对齐宿主 message.stepProcess.<kind>)。 */
17
+ const ACTIVITY_TEXT = {
18
+ thinking: '正在分析请求',
19
+ read: '正在读取文件',
20
+ readImage: '正在读取图片',
21
+ write: '正在写入文件',
22
+ search: '正在搜索代码',
23
+ edit: '正在编辑文件',
24
+ commands: '正在运行命令',
25
+ code: '正在运行代码',
26
+ webSearch: '正在搜索网页',
27
+ webFetch: '正在访问网页',
28
+ subagents: '正在协调子智能体',
29
+ plan: '正在更新计划',
30
+ questions: '等待你的操作',
31
+ tools: '正在调用工具',
32
+ }
33
+
34
+ /**
35
+ * 工具名 → 活动种类(**逐条对齐宿主 `activity()`**)。
36
+ *
37
+ * 保持与宿主同构:宿主判断顺序是先精确名、再前缀/集合匹配,最后兜底 'tools'。
38
+ * @param {string} name 工具名
39
+ * @returns {string} 活动种类
40
+ */
41
+ export function activityOf(name) {
42
+ const n = String(name ?? '')
43
+ if (n === 'read') return 'read'
44
+ if (n === 'read_image') return 'readImage'
45
+ if (n === 'grep' || n === 'glob' || n.endsWith('_inspect')) return 'search'
46
+ if (n === 'write') return 'write'
47
+ if (n === 'edit' || n === 'apply_patch') return 'edit'
48
+ if (['bash', 'pwsh', 'exec_command', 'write_stdin'].includes(n) || n.startsWith('terminal_')) return 'commands'
49
+ if (n === 'run_code') return 'code'
50
+ if (n === 'web_search') return 'webSearch'
51
+ if (n === 'web_fetch') return 'webFetch'
52
+ if (n === 'subagent' || n.startsWith('subagent_')) return 'subagents'
53
+ if (['todo_write', 'create_goal', 'update_goal', 'get_goal'].includes(n)) return 'plan'
54
+ if (n === 'ask_user_question' || n === 'request_user_input') return 'questions'
55
+ return 'tools'
56
+ }
57
+
58
+ /** 活动种类 → 用户可读文案(未知种类兜底为「正在调用工具」)。 */
59
+ export function activityText(kind) {
60
+ return ACTIVITY_TEXT[kind] ?? ACTIVITY_TEXT.tools
61
+ }
62
+
63
+ /** 活动种类对应的图标(IM 里一眼可辨,比纯文字更快读懂)。 */
64
+ const ACTIVITY_ICON = {
65
+ thinking: '💭',
66
+ read: '📖',
67
+ readImage: '🖼',
68
+ write: '✍️',
69
+ search: '🔍',
70
+ edit: '📝',
71
+ commands: '⌨️',
72
+ code: '▶️',
73
+ webSearch: '🔎',
74
+ webFetch: '🌐',
75
+ subagents: '🤖',
76
+ plan: '📋',
77
+ questions: '❓',
78
+ tools: '🔧',
79
+ }
80
+
81
+ export function activityIcon(kind) {
82
+ return ACTIVITY_ICON[kind] ?? ACTIVITY_ICON.tools
83
+ }
84
+
85
+ /** 详情字段的取值优先级(对齐宿主 LIVE_TOOL_DETAIL_KEYS,截取其中信息量最高的前几个)。 */
86
+ const DETAIL_KEYS = [
87
+ 'description', 'command', 'cmd', 'file_path', 'path', 'pattern', 'query', 'queries',
88
+ 'url', 'uri', 'title', 'objective', 'task', 'task_name', 'name', 'question', 'prompt', 'message',
89
+ ]
90
+
91
+ /** 详情最大字符数(对齐宿主 LIVE_TOOL_DETAIL_MAX_CHARS)。 */
92
+ export const DETAIL_MAX_CHARS = 160
93
+
94
+ /**
95
+ * 从工具参数里提取一行「细节」(如命令全文、文件名),供状态行展示。
96
+ *
97
+ * 目的:`⌨️ 正在运行命令(npm test)` 比光说「正在运行命令」有用得多。
98
+ * 参数缺失/非字符串时返回空串(不硬造内容)。
99
+ *
100
+ * @param {object} args 工具调用参数(已解析对象;也容忍 JSON 字符串)
101
+ * @returns {string}
102
+ */
103
+ export function toolDetail(args) {
104
+ let obj = args
105
+ if (typeof args === 'string') {
106
+ try { obj = JSON.parse(args) } catch { return '' }
107
+ }
108
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return ''
109
+ for (const key of DETAIL_KEYS) {
110
+ const v = obj[key]
111
+ const text = typeof v === 'string'
112
+ ? v
113
+ : (Array.isArray(v) && v.every((x) => typeof x === 'string') ? v.join(', ') : '')
114
+ const norm = text.replace(/\s+/g, ' ').trim()
115
+ if (!norm) continue
116
+ return norm.length > DETAIL_MAX_CHARS ? `${norm.slice(0, DETAIL_MAX_CHARS - 1)}…` : norm
117
+ }
118
+ return ''
119
+ }
120
+
121
+ /**
122
+ * 生成轮次进行中的状态行。
123
+ *
124
+ * @param {{ turn?: number, toolName?: string, args?: object|string }} info
125
+ * @returns {string} 形如 `💭 正在分析请求` 或 `⌨️ 正在运行命令(npm test)`
126
+ */
127
+ export function formatActivityLine({ turn, toolName, args } = {}) {
128
+ const s = activityParts({ turn, toolName, args })
129
+ const head = s.detail ? `${s.icon} ${s.text}(${s.detail})` : `${s.icon} ${s.text}`
130
+ return s.turn ? `${head} · 第 ${s.turn} 轮` : head
131
+ }
132
+
133
+ /**
134
+ * 状态行的**结构化**形式:把"受控文案"与"用户可控细节"分开。
135
+ *
136
+ * 为什么需要:`detail` 直接来自工具参数(命令原文、文件名…),其中可能含 markdown
137
+ * 元字符(`*` `_` `` ` `` `[]` 等)。平台若把它整行塞进斜体/引用等样式里,这些字符会
138
+ * **截断样式**(如 `echo *bold*` 会让斜体提前闭合,渲染错乱)。
139
+ * 分开之后,各平台可以只对受控部分加样式、对 detail 做转义或代码包裹。
140
+ *
141
+ * @returns {{ turn:number, kind:string, icon:string, text:string, detail:string, suffix:string }}
142
+ * turn 为 0 表示无轮次信息;suffix 形如 ` · 第 3 轮`(已含前导分隔符,便于直接拼接)
143
+ */
144
+ export function activityParts({ turn, toolName, args } = {}) {
145
+ const kind = toolName ? activityOf(toolName) : 'thinking'
146
+ const n = Number(turn)
147
+ const hasTurn = Number.isFinite(n) && n > 0
148
+ return {
149
+ turn: hasTurn ? n : 0,
150
+ kind,
151
+ icon: activityIcon(kind),
152
+ text: activityText(kind),
153
+ detail: toolDetail(args),
154
+ suffix: hasTurn ? ` · 第 ${n} 轮` : '',
155
+ }
156
+ }
157
+
158
+ /**
159
+ * 转义 markdown 元字符,使其在强调/引用等样式内**原样显示**、不改变结构。
160
+ *
161
+ * 用于状态行的 detail(命令原文等用户可控内容):不转义时 `echo *bold*` 会破坏
162
+ * 外层斜体,甚至把后续正文吞进样式里。
163
+ *
164
+ * @param {string} text
165
+ * @returns {string}
166
+ */
167
+ export function escapeMarkdown(text) {
168
+ return String(text ?? '').replace(/([\\`*_{}[\]()#+\-.!|~>])/g, '\\$1')
169
+ }