@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,99 +0,0 @@
1
- // cloudflared 网络参数白名单(issue #71 P1-B)
2
- //
3
- // 取值的一手来源(访问 2026-10-10):Cloudflare 官方 Tunnel run parameters 文档
4
- // https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/tunnel-run-parameters/
5
- // --protocol auto | http2 | quic(默认 auto)
6
- // --edge-ip-version auto | 4 | 6(默认 4)
7
- // --region 官方当前仅收录 us(空/缺省 = 全球默认)
8
- // --ha-connections 被 issue #71 提及,但**官方文档未收录、无官方取值依据**——
9
- // 给用户挖坑的开关不做,故刻意不在本清单内。
10
- //
11
- // 设计约束(三条同时满足才允许把参数拼进启动命令):
12
- // 1. 默认零变化:三项全空 → 不追加任何参数,启动命令与旧版逐字一致;
13
- // 2. 值白名单校验:非法值在保存入口就被拒绝(execFile 数组传参,天然无注入面);
14
- // 3. 能力门控:系统二进制版本不受控,实际二进制不认识的 flag 必须跳过并告警
15
- // ——cloudflared 对未知 flag 是 "Incorrect Usage" 致命错误,绝不带病启动。
16
-
17
- /** 可配置字段(键名进入 cloudflaredConfig 持久化) */
18
- export const CLOUDFLARED_NETWORK_FIELDS = ['protocol', 'edgeIpVersion', 'region'];
19
-
20
- /** 字段 → CLI flag 与官方白名单取值 */
21
- export const CLOUDFLARED_NETWORK_FLAGS = {
22
- protocol: { flag: '--protocol', values: ['auto', 'http2', 'quic'] },
23
- edgeIpVersion: { flag: '--edge-ip-version', values: ['auto', '4', '6'] },
24
- region: { flag: '--region', values: ['us'] },
25
- };
26
-
27
- /**
28
- * 校验网络参数(保存入口用)。
29
- * 语义:undefined = 不修改(不进入 values);空串/纯空白 = 清除(恢复默认);
30
- * 其余必须命中白名单,否则给出可读错误。
31
- *
32
- * @param {Record<string, unknown>} input 形如 { protocol, edgeIpVersion, region }
33
- * @returns {{ ok: boolean, values: Record<string, string>, errors: Record<string, string> }}
34
- */
35
- export function validateCloudflaredNetworkOptions(input = {}) {
36
- const values = {};
37
- const errors = {};
38
- for (const field of CLOUDFLARED_NETWORK_FIELDS) {
39
- const v = input[field];
40
- if (v === undefined) continue;
41
- if (typeof v !== 'string') {
42
- errors[field] = field + ' 必须是文本';
43
- continue;
44
- }
45
- const trimmed = v.trim();
46
- if (trimmed === '') {
47
- values[field] = '';
48
- continue;
49
- }
50
- const spec = CLOUDFLARED_NETWORK_FLAGS[field];
51
- if (!spec.values.includes(trimmed)) {
52
- errors[field] = field + ' 仅支持:' + spec.values.join(' / ');
53
- continue;
54
- }
55
- values[field] = trimmed;
56
- }
57
- return { ok: Object.keys(errors).length === 0, values, errors };
58
- }
59
-
60
- /**
61
- * 按"已配置值 + 实际二进制能力"构造要追加的启动参数。
62
- *
63
- * @param {Record<string, string>} values 已通过 validate 的值(缺省/空 = 不追加)
64
- * @param {Record<string, boolean>|null} supported 能力探测结果;null = 探测失败
65
- * (保守策略:全部跳过并标注原因,隧道照常以默认参数启动)
66
- * @returns {{ args: string[], skipped: { field: string, reason: string }[] }}
67
- */
68
- export function buildCloudflaredNetworkArgs(values = {}, supported = null) {
69
- const args = [];
70
- const skipped = [];
71
- for (const field of CLOUDFLARED_NETWORK_FIELDS) {
72
- const v = values?.[field];
73
- if (typeof v !== 'string' || v === '') continue; // 未配置 = 零变化
74
- const spec = CLOUDFLARED_NETWORK_FLAGS[field];
75
- if (supported === null) {
76
- skipped.push({ field, reason: '能力探测失败,保守跳过(保持默认参数启动)' });
77
- continue;
78
- }
79
- if (supported[field] !== true) {
80
- skipped.push({ field, reason: '当前二进制不支持 ' + spec.flag + ',已跳过' });
81
- continue;
82
- }
83
- args.push(spec.flag, v);
84
- }
85
- return { args, skipped };
86
- }
87
-
88
- /**
89
- * 能力探测判据(纯函数):cloudflared 对未知 flag 的输出是
90
- * "flag provided but not defined"(本机 2024.10.0 实测基线);
91
- * 其余一切输出(接受、枚举校验失败、token 校验失败…)都证明 flag 存在。
92
- * @param {string} commandOutput 探测命令的 stdout+stderr
93
- * @returns {boolean} true = 该二进制认识这个 flag
94
- */
95
- export function parseFlagSupport(commandOutput) {
96
- const out = String(commandOutput ?? '').trim();
97
- if (!out) return false; // 空输出(没跑起来/被吞)→ 保守判为不支持,绝不带病传参
98
- return !/flag provided but not defined/i.test(out);
99
- }
@@ -1,153 +0,0 @@
1
- // "同 token 的另一个 cloudflared" 检测(issue #71 第 4 条)
2
- //
3
- // 场景:用户的系统服务(Windows 服务 / systemd unit)与插件用同一个 tunnel token
4
- // 同时运行——两边互相看不见,面板只显示"半张图"(对方假死/503 时插件无从得知)。
5
- // issue 实测:系统服务 Cloudflared 已 503/readyConnections=0 持续 50 分钟,插件毫不知情。
6
- //
7
- // 设计原则:
8
- // 1. 只在隧道启动时扫一次,异步执行,绝不阻塞启动;
9
- // 2. 比对逻辑全部是纯函数(进程列表与文件读取都可注入,便于单测);
10
- // 3. 全链路 fail-open:进程表读不到、token-file 读不到 → 返回"未确认"而不是报错
11
- // (在部分环境进程扫描会被权限/安全软件拦截,这是预期降级而非故障);
12
- // 4. 绝不打印 token 本体:结果只含 PID 与来源 flag。
13
-
14
- import { readFileSync, readdirSync } from 'node:fs';
15
- import { execFileSync } from 'node:child_process';
16
-
17
- /**
18
- * 从一条进程命令行提取 cloudflared 的鉴权凭据。
19
- * 支持:--token X / --token=X / --token-file X / --token-file=X(值可带引号)。
20
- * @param {string} cmdline
21
- * @returns {{ via: 'token'|'token-file', value: string }|null} 非 cloudflared 或未带凭据 → null
22
- */
23
- export function extractCloudflaredAuth(cmdline) {
24
- const s = String(cmdline ?? '');
25
- if (!s.includes('cloudflared')) return null;
26
- const tokens = s.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
27
- const clean = tokens.map((t) => t.replace(/^"|"$/g, ''));
28
- for (let i = 0; i < clean.length; i++) {
29
- const t = clean[i];
30
- if (t === '--token' || t === '--token-file') {
31
- const v = clean[i + 1];
32
- if (v) return { via: t === '--token' ? 'token' : 'token-file', value: v };
33
- return null;
34
- }
35
- if (t.startsWith('--token=')) {
36
- const v = t.slice('--token='.length);
37
- return v ? { via: 'token', value: v } : null;
38
- }
39
- if (t.startsWith('--token-file=')) {
40
- const v = t.slice('--token-file='.length);
41
- return v ? { via: 'token-file', value: v } : null;
42
- }
43
- }
44
- return null;
45
- }
46
-
47
- /**
48
- * 在候选进程里找与自己 token 相同的 cloudflared。
49
- * @param {object} opts
50
- * @param {string} opts.myToken 本插件正在使用的 tunnel token
51
- * @param {{pid:number, cmdline:string}[]} opts.processes 候选进程(调用方已预筛或不过滤均可)
52
- * @param {number[]} [opts.excludePids] 排除的 PID(自己拉起的 cloudflared)
53
- * @param {(path:string)=>string} [opts.readFileFn] 读 token-file 的注入点
54
- * @returns {{pid:number, via:string}|null} 命中 → 返回;token-file 读不到 → 视为无法确认,不告警
55
- */
56
- export function findSameTokenRunner({ myToken, processes, excludePids = [], readFileFn }) {
57
- const my = String(myToken ?? '').trim();
58
- if (!my || !Array.isArray(processes)) return null;
59
- const read = readFileFn ?? ((p) => readFileSync(p, 'utf8'));
60
- const excluded = new Set(excludePids.map(Number));
61
- for (const p of processes) {
62
- if (!p || excluded.has(Number(p.pid))) continue;
63
- const auth = extractCloudflaredAuth(p.cmdline);
64
- if (!auth) continue;
65
- if (auth.via === 'token') {
66
- if (String(auth.value).trim() === my) return { pid: Number(p.pid), via: 'token' };
67
- } else {
68
- let fileToken;
69
- try { fileToken = String(read(auth.value)).trim(); } catch { fileToken = null; }
70
- // 文件读不到(权限/路径失效)→ 无法确认,fail-open 不告警
71
- if (fileToken && fileToken === my) return { pid: Number(p.pid), via: 'token-file' };
72
- }
73
- }
74
- return null;
75
- }
76
-
77
- /** 解析 `ps -Ao pid=,command=` 输出(macOS 用) */
78
- export function parsePsProcesses(output) {
79
- const out = [];
80
- for (const line of String(output ?? '').split('\n')) {
81
- const m = /^\s*(\d+)\s+(.*)$/.exec(line);
82
- if (m) out.push({ pid: Number(m[1]), cmdline: m[2] });
83
- }
84
- return out;
85
- }
86
-
87
- /** 解析 PowerShell "PID CMDLINE" 行输出(Windows 用) */
88
- export function parsePowerShellProcesses(output) {
89
- const out = [];
90
- for (const line of String(output ?? '').split('\n')) {
91
- const m = /^\s*(\d+)\s+(.*)$/.exec(line);
92
- if (m) out.push({ pid: Number(m[1]), cmdline: m[2] });
93
- }
94
- return out;
95
- }
96
-
97
- /** Linux:直接读 /proc(同机进程 cmdline 通常可读;个别 pid 读失败就跳过) */
98
- export function listLinuxProcesses({ readdirFn = readdirSync, readFileFn = readFileSync } = {}) {
99
- const out = [];
100
- let entries;
101
- try { entries = readdirFn('/proc'); } catch { return out; }
102
- for (const name of entries) {
103
- if (!/^\d+$/.test(name)) continue;
104
- try {
105
- const cmdline = String(readFileFn('/proc/' + name + '/cmdline', 'utf8')).replace(/\0/g, ' ').trim();
106
- if (cmdline.includes('cloudflared')) out.push({ pid: Number(name), cmdline });
107
- } catch { /* 个别进程无权限/已退出:跳过 */ }
108
- }
109
- return out;
110
- }
111
-
112
- /** 平台无关的默认进程枚举:Linux=/proc,macOS=ps,Windows=PowerShell;任何失败 → [](fail-open) */
113
- export function listCloudflaredProcesses({ platformName = process.platform, execFn = execFileSync } = {}) {
114
- try {
115
- if (platformName === 'linux') return listLinuxProcesses();
116
- if (platformName === 'darwin') {
117
- const out = execFn('ps', ['-Ao', 'pid=,command='], { encoding: 'utf8', timeout: 3000 });
118
- return parsePsProcesses(out).filter((p) => p.cmdline.includes('cloudflared'));
119
- }
120
- if (platformName === 'win32') {
121
- const script = '$ErrorActionPreference="SilentlyContinue";'
122
- + 'Get-CimInstance Win32_Process |'
123
- + 'Where-Object { $_.CommandLine -like "*cloudflared*" } |'
124
- + 'ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }';
125
- const out = execFn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script],
126
- { encoding: 'utf8', timeout: 5000 });
127
- return parsePowerShellProcesses(out);
128
- }
129
- return [];
130
- } catch {
131
- return []; // 权限不足 / 安全软件拦截 / 超时:一律静默降级为"未检出",绝不影响隧道启动
132
- }
133
- }
134
-
135
- /**
136
- * 扫描"与本插件同 token 的另一个 cloudflared"(启动时调用一次)。
137
- * @param {object} opts
138
- * @param {string} opts.myToken
139
- * @param {() => number[]} [opts.getExcludePids] 结果计算前才求值(拿到自己子进程的 PID)
140
- * @param {() => {pid:number,cmdline:string}[]} [opts.listProcesses]
141
- * @param {(path:string)=>string} [opts.readFileFn]
142
- * @returns {Promise<{pid:number, via:string}|null>}
143
- */
144
- export async function scanSameTokenRunner({ myToken, getExcludePids, listProcesses, readFileFn } = {}) {
145
- try {
146
- if (!myToken || !String(myToken).trim()) return null;
147
- const processes = (listProcesses ?? listCloudflaredProcesses)();
148
- const excludePids = typeof getExcludePids === 'function' ? (getExcludePids() ?? []) : [];
149
- return findSameTokenRunner({ myToken, processes, excludePids, readFileFn });
150
- } catch {
151
- return null; // fail-open:探测失败等价于"未检出"
152
- }
153
- }
package/lib/compat.js DELETED
@@ -1,172 +0,0 @@
1
- // 运行时兼容垫片(lib/compat.js)
2
- //
3
- // 背景:DSH 核心依赖链(@deepseek-ai/dsh-timeout ← dsh-llm)在每次 agent 请求上
4
- // 调用 AbortSignal.any([...]),该 API 仅存在于 Node 20.3+/22+。运行在低版本 Node
5
- // (如 Node 18)上时,通过桥接发送消息会直接抛
6
- // "AbortSignal.any is not a function ... (internal)"
7
- // 本模块在缺失时安装规范兼容的垫片;环境本身支持时不做任何事(返回 false)。
8
- //
9
- // 浏览器侧(旧 Safari/WebView)由 lib/index.js 的 HTML_HEAD_INJECTIONS 注入同语义垫片。
10
-
11
- function abortWith(controller, reason) {
12
- try {
13
- controller.abort(reason)
14
- } catch {
15
- try { controller.abort() } catch { /* 不可中止的信号:忽略 */ }
16
- }
17
- }
18
-
19
- /**
20
- * 在目标环境中安装 AbortSignal.any / AbortSignal.timeout 垫片(仅缺失时)。
21
- * @param {object} [target] 可注入的环境(默认 globalThis),便于测试
22
- * @returns {boolean} 是否实际安装了垫片
23
- */
24
- export function installAbortSignalCompat(target = globalThis) {
25
- const Signal = target.AbortSignal
26
- const Controller = target.AbortController
27
- if (!Signal || !Controller) return false
28
-
29
- let installed = false
30
-
31
- // ---- AbortSignal.any ----
32
- if (typeof Signal.any !== 'function') {
33
- Signal.any = (signals) => {
34
- const list = Array.from(signals ?? [])
35
- const controller = new Controller()
36
- // 规范语义:任一源信号已中止 → 立即以该原因中止
37
- for (const s of list) {
38
- if (s && s.aborted) {
39
- abortWith(controller, s.reason)
40
- return controller.signal
41
- }
42
- }
43
- const onAbort = (eventOrSignal) => {
44
- // 真实 EventTarget 的 abort 事件参数是 event(event.target = 信号),
45
- // 部分非标准实现直接传信号本身——两者都兼容取 reason
46
- const src = eventOrSignal && eventOrSignal.target ? eventOrSignal.target : eventOrSignal
47
- cleanup()
48
- abortWith(controller, src ? src.reason : undefined)
49
- }
50
- const cleanup = () => {
51
- for (const s of list) {
52
- try { s.removeEventListener('abort', onAbort) } catch { /* 非标准信号:忽略 */ }
53
- }
54
- }
55
- for (const s of list) {
56
- try { s.addEventListener('abort', onAbort, { once: true }) } catch { /* 非标准信号:忽略 */ }
57
- }
58
- return controller.signal
59
- }
60
- installed = true
61
- }
62
-
63
- // ---- AbortSignal.timeout(Node 17.3+ / 较新浏览器才有,顺手补齐)----
64
- if (typeof Signal.timeout !== 'function') {
65
- Signal.timeout = (delayMs) => {
66
- const controller = new Controller()
67
- const delay = Math.max(0, Number(delayMs) || 0)
68
- const reason = typeof target.DOMException === 'function'
69
- ? new target.DOMException('The operation timed out.', 'TimeoutError')
70
- : new Error('The operation timed out.')
71
- const timer = setTimeout(() => abortWith(controller, reason), delay)
72
- // 规范语义:timeout 信号的超时定时器不阻止进程退出
73
- if (typeof timer.unref === 'function') timer.unref()
74
- return controller.signal
75
- }
76
- installed = true
77
- }
78
-
79
- return installed
80
- }
81
-
82
- /**
83
- * 浏览器侧垫片源码:由代理注入到 HTML <head>(在宿主所有脚本之前执行),
84
- * 为 iOS 16 / 旧 Safari / 旧 WebView(无 AbortSignal.any,Safari 17.4 才加入)
85
- * 上的 DSH 网页客户端补齐。多行可读源码,勿手工压缩成单行(配平易错)。
86
- */
87
- export const BROWSER_ABORT_SIGNAL_POLYFILL = `<script data-dsh-bridge-polyfill="2">
88
- !function () {
89
- try {
90
- var S = self.AbortSignal;
91
- if (typeof S !== 'function') return;
92
- if (typeof S.any !== 'function') {
93
- S.any = function (signals) {
94
- var list = Array.prototype.slice.call(signals || []);
95
- var c = new self.AbortController();
96
- var onAbort = function (ev) {
97
- var src = ev && ev.target ? ev.target : ev;
98
- cleanup();
99
- try { c.abort(src ? src.reason : undefined); } catch (e) { c.abort(); }
100
- };
101
- var cleanup = function () {
102
- for (var i = 0; i < list.length; i++) {
103
- try { list[i].removeEventListener('abort', onAbort); } catch (e) {}
104
- }
105
- };
106
- for (var i = 0; i < list.length; i++) {
107
- if (list[i] && list[i].aborted) {
108
- try { c.abort(list[i].reason); } catch (e) { c.abort(); }
109
- return c.signal;
110
- }
111
- }
112
- for (var i = 0; i < list.length; i++) {
113
- try { list[i].addEventListener('abort', onAbort, { once: true }); } catch (e) {}
114
- }
115
- return c.signal;
116
- };
117
- }
118
- if (typeof S.timeout !== 'function') {
119
- S.timeout = function (ms) {
120
- var c = new self.AbortController();
121
- setTimeout(function () {
122
- try { c.abort(new Error('The operation timed out.')); } catch (e) { c.abort(); }
123
- }, Math.max(0, Number(ms) || 0));
124
- return c.signal;
125
- };
126
- }
127
- } catch (e) { /* 环境异常时不影响页面其余脚本 */ }
128
- }();
129
- </script>`
130
-
131
- /**
132
- * 浏览器侧垫片源码(二):iOS 16 / 旧 Safari 缺失的 Promise.withResolvers 与 Iterator。
133
- *
134
- * 背景(两者都是"整个 app 起不来"级别):
135
- * 1. `Promise.withResolvers`:Safari 17.4+ / Chrome 119+ 才有。DSH 宿主 HTML 末尾的内联脚本
136
- * 直接写 `(globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers()).resolve()`;
137
- * 另有多个客户端插件在运行期使用(审批、用户提问、cordis 运行时 ctx.timeout/ctx.interval、
138
- * workspace-files 的文件变更订阅 Follower)。
139
- * 2. `Iterator`:Safari 18.4+ 才有(迭代器辅助方法)。DSH 自带的 pdf.js 在**模块顶层**就执行
140
- * `if (typeof Iterator.prototype.join !== "function") Iterator.prototype.join = ...`
141
- * iOS 16 上没有 Iterator 全局,于是这一行抛 `Can't find variable: Iterator`,
142
- * 导致 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` 导入失败 → 整个插件树加载失败,
143
- * 页面直接显示 "Failed to load plugins"(实测于 iOS 16 真机 + 本机同构模拟)。
144
- * 这里把真实的内置迭代器共享原型(%IteratorPrototype%)挂到 `Iterator.prototype` 上,
145
- * 使 pdf.js 的补丁落到真实迭代器原型,而不是一个无用的空对象。
146
- *
147
- * 两者都在缺失时才安装;已有该能力的引擎不做任何事。多行可读源码,勿手工压缩成单行(配平易错)。
148
- */
149
- export const BROWSER_PROMISE_ITERATOR_POLYFILL = `<script data-dsh-bridge-polyfill="3">
150
- !function () {
151
- try {
152
- if (typeof Promise !== 'undefined' && typeof Promise.withResolvers !== 'function') {
153
- Promise.withResolvers = function () {
154
- var resolve, reject;
155
- var promise = new Promise(function (res, rej) { resolve = res; reject = rej; });
156
- return { promise: promise, resolve: resolve, reject: reject };
157
- };
158
- }
159
- } catch (e) { /* 环境异常时不影响页面其余脚本 */ }
160
- try {
161
- if (typeof self.Iterator === 'undefined') {
162
- var proto = null;
163
- try {
164
- proto = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
165
- } catch (e2) { proto = null; }
166
- var IteratorShim = function Iterator() {};
167
- if (proto && typeof proto === 'object') IteratorShim.prototype = proto;
168
- self.Iterator = IteratorShim;
169
- }
170
- } catch (e) { /* 环境异常时不影响页面其余脚本 */ }
171
- }();
172
- </script>`
@@ -1,115 +0,0 @@
1
- // 运行时兼容垫片:DSH ≥ 0.1.5-alpha.1 的 connection RPC 通道注册回归
2
- // (lib/connection-compat.js)
3
- //
4
- // 背景(上游回归,非本插件声明问题)
5
- // ---------------------------------------------------------------
6
- // @deepseek-ai/dsh-client-connection 在 0.1.5-alpha.1 改了插件级服务声明:
7
- // ≤ 0.1.3-alpha.2 const inject = ["webServer", "credentials"];
8
- // ≥ 0.1.5-alpha.1 const inject = ["credentials"]; // webServer 改为可选
9
- // 同时把 /api 路由改用作用域注入 ctx.inject(["webServer"], (webCtx) => …)。
10
- // 但 HostConnectionService.register() 里那一句没有同步改(各版本行号均为 618):
11
- // return owner.effect(() => owner.webServer.register(route),
12
- // `client-connection: ${channel} rpc channel`);
13
- // 其中 owner === this.ctx,即 connection 插件自己的 ctx。该 ctx 的 fiber 不再把
14
- // webServer 记入 inject,于是 cordis 4 的服务守卫直接抛:
15
- // cannot get property "webServer" without inject
16
- // 注册发生在插件树加载期,异常向上冒泡 → 整棵插件树加载失败,`dsh web` 起不来:
17
- // Error: dsh: plugin tree failed to load: failed to apply loader entry
18
- // dsh-bridge (@wenbin_wb/dsh-bridge): cannot get property "webServer" without inject
19
- //
20
- // 影响面:任何调用 ctx.connection.rpc.handle() 的插件都会踩中(dsh-bridge 只是第一个撞上的)。
21
- // 已逐一核对 npm 上的版本:0.1.5-alpha.1 / alpha.2 / rc.1 / rc.2 全部未修
22
- // (0.1.5-rc.2 为当前已发布的最新版),0.1.3-alpha.2 及更早正常。
23
- //
24
- // 本垫片的做法
25
- // ---------------------------------------------------------------
26
- // 不改上游代码、不猜内部私有方法,只走两个公开面:
27
- // 1. 正常路径:ctx.connection.rpc.handle(channel, handler)(公开 API,未来上游修好即恢复);
28
- // 2. 兜底路径:仅当该调用抛出 "webServer … without inject" 时,在 root 的
29
- // `internal/get` 瀑布事件上临时挂一个监听器 —— 先让默认解析跑,默认解析失败
30
- // (抛出的正是同一个 error 对象)才补上本插件已通过 inject 拿到的 webServer 引用。
31
- // webServer 是单例服务,语义等价;默认解析成功时零干预。
32
- // 监听器只在这一次同步注册调用期间存在,返回后立即摘除(cordis 的 effect 同步执行,
33
- // 注册完成后 owner.webServer 不再被访问),因此对宿主其余部分完全无副作用。
34
- //
35
- // 上游修好后本模块自动空转:首次调用不再抛错,兜底分支根本不会进入。
36
-
37
- const MISSING_INJECT = /without inject/;
38
-
39
- /**
40
- * 判断错误是否为"跨服务取属性缺 inject 作用域"的守卫错误,且对象是 webServer。
41
- * @param {unknown} err 捕获到的异常
42
- * @returns {boolean} 是否命中该回归
43
- */
44
- export function isMissingWebServerInjectError(err) {
45
- if (!(err instanceof Error)) return false;
46
- return MISSING_INJECT.test(err.message) && /webServer/.test(err.message);
47
- }
48
-
49
- /**
50
- * 在 root 的 `internal/get` 瀑布事件上挂一个临时兜底:默认解析失败时提供 webServer。
51
- *
52
- * cordis 4 的 `ctx.events` 是所有子 ctx 共享的同一个实例,且 `internal/get` 派发时
53
- * 不带 context 过滤条件(thisArg 为 null),因此在本插件 ctx 上注册的监听器同样会对
54
- * connection 插件 ctx 的查找生效。监听器由 root fiber 持有,须手工摘除。
55
- *
56
- * @param {object} ctx 本插件的 ctx
57
- * @returns {() => void} 摘除函数
58
- */
59
- export function installWebServerResolutionFallback(ctx) {
60
- const webServer = ctx?.webServer;
61
- if (webServer === undefined) return () => {};
62
-
63
- const listener = function (lookupCtx, prop, error, next) {
64
- if (prop !== 'webServer') return next();
65
- try {
66
- return next();
67
- } catch (err) {
68
- // 默认解析抛的不是本次守卫错误 → 原样上抛,绝不掩盖真实故障
69
- if (err !== error) throw err;
70
- }
71
- // 默认解析确认失败:补上本插件已 inject 的单例引用(语义等价)
72
- return webServer;
73
- };
74
-
75
- const off = ctx.on?.('internal/get', listener, { global: true });
76
- return typeof off === 'function' ? off : () => {};
77
- }
78
-
79
- /**
80
- * 注册 connection RPC 通道,并兼容 DSH ≥ 0.1.5-alpha.1 的 webServer 注入回归。
81
- *
82
- * @param {object} ctx 本插件的 ctx(须已 inject `connection` 与 `webServer`)
83
- * @param {string} channel RPC 通道名(如 `/dsh-bridge`)
84
- * @param {Function} handler 通道处理器
85
- * @param {object} [rpcOptions] 透传给 connection 的通道选项。
86
- * DSH 0.1.0/0.1.1 的 `rpc.handle(channel, handler, options)` 支持
87
- * `{ authority: 'loopback' }`(仅回环可达,非回环请求 403);
88
- * 0.1.2-rc.1 起该第三参数已被移除(更高版本忽略它,传入无副作用)。
89
- * 本插件依赖它来做回环加固,因此必须原样透传、不得丢失。
90
- * @param {{warn?: Function}} [logger] 可选日志器
91
- * @returns {Function} 由 connection 服务返回的处置函数
92
- */
93
- export function registerRpcChannel(ctx, channel, handler, rpcOptions, logger) {
94
- try {
95
- return ctx.connection.rpc.handle(channel, handler, rpcOptions);
96
- } catch (err) {
97
- if (!isMissingWebServerInjectError(err)) throw err;
98
-
99
- // 上游回归:owner(connection 插件自己的 ctx)解析不到 webServer。
100
- // 该调用在 owner.webServer 处、于 webServer.register() 之前就抛,未产生任何副作用,
101
- // 因此可以安全地补上兜底后重试一次。
102
- const dispose = installWebServerResolutionFallback(ctx);
103
- try {
104
- const registered = ctx.connection.rpc.handle(channel, handler, rpcOptions);
105
- logger?.warn?.(
106
- 'dsh-bridge: 检测到宿主 DSH 的 connection RPC 注册回归(cannot get property "webServer" without inject,'
107
- + '见 dsh-client-connection 的 register());已用 webServer 解析兜底完成通道注册。'
108
- + '此为宿主侧问题(DSH 0.1.5-alpha.1 起),插件侧垫片会在上游修复后自动空转。'
109
- );
110
- return registered;
111
- } finally {
112
- dispose();
113
- }
114
- }
115
- }
@@ -1,50 +0,0 @@
1
- // 飞书云文档评论(@机器人)相关纯函数
2
- //
3
- // 官方依据:docs/dev/lark-sdk/README.zh.md「云文档评论 — channel.comments」与 0.7.1 类型定义
4
- // CommentEvent { fileToken, fileType, commentId, replyId?, operator{openId}, mentionedBot, timestamp }
5
- // FetchedComment { commentId, replies[{ reply_id?, content?.elements[] }], quote?, isWhole }
6
- // CommentReplyContentElement = text_run | docs_link | person
7
- //
8
- // 评论回复是**纯文本**(没有卡片/流式/按钮),所以这里把结构化评论拍平成文本,交给会话桥当一轮输入。
9
-
10
- /** 合成评论会话 peer id(与聊天 peer `oc_…` 明显区分,出站据此改走评论回复) */
11
- export function buildCommentPeerId({ fileToken, commentId } = {}) {
12
- return `comment:${fileToken ?? ''}:${commentId ?? ''}`
13
- }
14
-
15
- export function isCommentPeerId(peerId) {
16
- return typeof peerId === 'string' && peerId.startsWith('comment:')
17
- }
18
-
19
- /** 拍平一条评论回复的富文本元素为纯文本(text_run / docs_link / person) */
20
- export function flattenCommentElements(elements) {
21
- if (!Array.isArray(elements)) return ''
22
- return elements
23
- .map((el) => {
24
- if (el?.type === 'text_run') return el.text_run?.text ?? ''
25
- if (el?.type === 'docs_link') return el.docs_link?.url ?? ''
26
- if (el?.type === 'person') return `@${el.person?.user_id ?? ''}`
27
- return ''
28
- })
29
- .join('')
30
- .trim()
31
- }
32
-
33
- /**
34
- * 从 FetchedComment 提取"用户这次说了什么":
35
- * - text:最后一条回复的正文(评论线程里最新那条就是本次触发内容)
36
- * - quote:行内评论选中的文档原文(有则作为上下文)
37
- * - isWhole:整篇文档评论(无行内线程)——回复要直接发顶层评论,避免 SDK 先探一次 in-thread 失败
38
- * - replyId:最后一条回复的 id(当前未用,留给后续"评论打字中"表情等)
39
- */
40
- export function extractCommentInput(fetched) {
41
- const replies = Array.isArray(fetched?.replies) ? fetched.replies : []
42
- const last = replies[replies.length - 1]
43
- return {
44
- text: flattenCommentElements(last?.content?.elements),
45
- quote: String(fetched?.quote ?? '').trim(),
46
- isWhole: Boolean(fetched?.isWhole),
47
- replyId: last?.reply_id ?? '',
48
- replyCount: replies.length,
49
- }
50
- }