@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.
- package/CHANGELOG.md +15 -1094
- package/LICENSE +21 -21
- package/README.md +73 -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 +414 -0
- package/lib/identity.js +23 -0
- package/lib/index.js +303 -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/client/host-families.js
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
// dsh-bridge 宿主 CSS-module 类名前缀的「运行时探测 + 已知基线 + 显式降级」层
|
|
2
|
-
//
|
|
3
|
-
// 背景(Issue #72,桌面版 0.2.0 不兼容)
|
|
4
|
-
// ---------------------------------------------------------------
|
|
5
|
-
// DSH 每次大版本都会把客户端 CSS-module 的短哈希整代重算:
|
|
6
|
-
// 0.1.x: VOzbGW_overlay / wSkVaW_header / hHd-Xa_root / uV2eYG_card / qDHVXG_root …
|
|
7
|
-
// 0.2.0: wCInkW_overlay / Dc7zOa_header / _2H3hWW_root / RlGAzG_card / _9lTDKa_root …
|
|
8
|
-
// 本插件早期把这些哈希硬编码进选择器,宿主一升级就整片规则**静默空转**:
|
|
9
|
-
// 样式明明注入成功(<style> 在、无报错),却一个元素都匹配不到。
|
|
10
|
-
// 现象就是“手机端打开设置后样式没生效”,而 web 端(旧宿主)正常。
|
|
11
|
-
//
|
|
12
|
-
// 取值顺序(绝不静默)
|
|
13
|
-
// 1) 运行时探测:拿锚点元素 + 本地名反推前缀 —— 适配任意未来版本;
|
|
14
|
-
// 2) 已核实基线:同一 slot 的历代前缀都会做「是否真的命中页面」的验证;
|
|
15
|
-
// 3) 都没有 → 用永不匹配的哨兵占位,并记入 missing / degraded 供上层显式告警。
|
|
16
|
-
//
|
|
17
|
-
// 约定:只认 `<family>_<local>` 这一种 CSS-module 命名(Vite/lightningcss 风格,
|
|
18
|
-
// 0.1.x/0.2.0 的目标组件都是这个风格)。postcss 风格 `_local_hash_id`(如宿主
|
|
19
|
-
// 通用弹窗的 `_root_o6lrb_6`)不在覆盖范围,也不该被覆盖。
|
|
20
|
-
//
|
|
21
|
-
// 与其它模块的关系:mobile-styles.js 只导出**带占位符的模板**,本模块负责把
|
|
22
|
-
// 占位符渲染成真实前缀;client/index.js 负责注入与刷新。
|
|
23
|
-
|
|
24
|
-
/** 探测不到任何前缀时使用的哨兵:保证选择器永不匹配,而不是退化成 `[class*="overlay"]` 这种宽泛命中。 */
|
|
25
|
-
export const ABSENT_FAMILY = '__dshbr_absent_family';
|
|
26
|
-
|
|
27
|
-
/** 占位符语法:__F_<SLOT>__(SLOT 为大写字母/数字/下划线)。 */
|
|
28
|
-
export const FAMILY_TOKEN_RE = /__F_([A-Z][A-Z0-9_]*)__/g;
|
|
29
|
-
|
|
30
|
-
const norm = (v) => (typeof v === 'string' ? v.trim() : '');
|
|
31
|
-
|
|
32
|
-
/** 取元素的 class 列表(对无 classList 的环境 fail-open 成空数组)。 */
|
|
33
|
-
function classListOf(el) {
|
|
34
|
-
if (!el || !el.classList) return [];
|
|
35
|
-
try {
|
|
36
|
-
return [...el.classList];
|
|
37
|
-
} catch {
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 从 class 列表里按本地名反推 CSS-module 前缀(纯函数,便于单测)。
|
|
44
|
-
*
|
|
45
|
-
* @param {string[]} classes 元素上的类名
|
|
46
|
-
* @param {string} local 本地名(如 'overlay'、'navCell')
|
|
47
|
-
* @returns {string|null} 前缀,如 'wCInkW';找不到返回 null
|
|
48
|
-
*/
|
|
49
|
-
export function familyFromClasses(classes, local) {
|
|
50
|
-
const name = norm(local);
|
|
51
|
-
if (!name || !Array.isArray(classes)) return null;
|
|
52
|
-
const suffix = `_${name}`;
|
|
53
|
-
for (const token of classes) {
|
|
54
|
-
if (typeof token !== 'string') continue;
|
|
55
|
-
if (token.length <= suffix.length) continue;
|
|
56
|
-
if (!token.endsWith(suffix)) continue;
|
|
57
|
-
return token.slice(0, token.length - suffix.length);
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 一次性把页面上的 class 拆成 `<前缀> -> Set<本地名>` 索引。
|
|
64
|
-
*
|
|
65
|
-
* 为什么不做成「每个候选前缀一次 querySelector」:流式输出期间 DOM 每几毫秒就变一次,
|
|
66
|
-
* 按 11 个槽位 × 若干候选 × 若干本地名去查,每次要做几十遍带属性子串匹配的全文档扫描;
|
|
67
|
-
* 而一次 `querySelectorAll('[class]')`(走原生 class 属性索引)就够回答所有命中测试。
|
|
68
|
-
*
|
|
69
|
-
* 拆分规则取「最后一个下划线」:`VOzbGW_panel` → (VOzbGW, panel)、
|
|
70
|
-
* `_2H3hWW_newSessionContent` → (_2H3hWW, newSessionContent)、`-ngf_W_triggerLabel` → (-ngf_W, triggerLabel)。
|
|
71
|
-
* 对 `_root_o6lrb_6` 这类 postcss 风格类名会得到无意义键,但只多占一点内存,不会误命中。
|
|
72
|
-
*
|
|
73
|
-
* @returns {Map<string, Set<string>>|null} 索引;拿不到任何节点时返回 null(调用方退回直查)
|
|
74
|
-
*/
|
|
75
|
-
function buildClassIndex(doc) {
|
|
76
|
-
let nodes;
|
|
77
|
-
try {
|
|
78
|
-
nodes = doc?.querySelectorAll?.('[class]');
|
|
79
|
-
} catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
if (!nodes || !nodes.length) return null;
|
|
83
|
-
const index = new Map();
|
|
84
|
-
for (const el of nodes) {
|
|
85
|
-
for (const token of classListOf(el)) {
|
|
86
|
-
const cut = token.lastIndexOf('_');
|
|
87
|
-
if (cut <= 0) continue;
|
|
88
|
-
const family = token.slice(0, cut);
|
|
89
|
-
const local = token.slice(cut + 1);
|
|
90
|
-
let locals = index.get(family);
|
|
91
|
-
if (!locals) {
|
|
92
|
-
locals = new Set();
|
|
93
|
-
index.set(family, locals);
|
|
94
|
-
}
|
|
95
|
-
locals.add(local);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return index.size ? index : null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** 页面里是否存在该前缀 + 任一本地名组成的类名(用于验证基线是否仍然有效)。 */
|
|
102
|
-
function familyHits(doc, family, locals, index) {
|
|
103
|
-
if (!family || !locals || !locals.length) return false;
|
|
104
|
-
if (index) {
|
|
105
|
-
const present = index.get(family);
|
|
106
|
-
return !!present && locals.some((local) => present.has(local));
|
|
107
|
-
}
|
|
108
|
-
for (const local of locals) {
|
|
109
|
-
try {
|
|
110
|
-
if (doc.querySelector(`[class*="${family}_${local}"]`)) return true;
|
|
111
|
-
} catch {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 弱命中:页面里是否存在任意 `<family>_*` 类名。
|
|
120
|
-
*
|
|
121
|
-
* 为什么需要:组件(如设置弹窗)尚未挂载时,按本地名的强命中一定失败,此时若直接取
|
|
122
|
-
* 基线首项就会猜错代次 —— 0.1.x 宿主上会被猜成 0.2.0 的前缀,整片规则反而失效。
|
|
123
|
-
* 而侧栏的「设置」轨道按钮等入口元素在任何代次都常驻,`VOzbGW_trigger` /
|
|
124
|
-
* `wCInkW_trigger` 这类痕迹足以判断该代次是不是活的。
|
|
125
|
-
*/
|
|
126
|
-
function familyPresent(doc, family, index) {
|
|
127
|
-
if (!family) return false;
|
|
128
|
-
if (index) return index.has(family);
|
|
129
|
-
try {
|
|
130
|
-
return !!doc.querySelector(`[class*="${family}_"]`);
|
|
131
|
-
} catch {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const aria = (re) => (doc) => {
|
|
137
|
-
const nodes = doc.querySelectorAll?.('[aria-label]') ?? [];
|
|
138
|
-
for (const el of nodes) if (re.test(el.getAttribute('aria-label') || '')) return el;
|
|
139
|
-
return null;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/** 设置弹窗面板:`[role="dialog"]` 且直接子级含 `<nav>`(0.1.x / 0.2.0 同构)。 */
|
|
143
|
-
const settingsPanel = (doc) => {
|
|
144
|
-
const panels = doc.querySelectorAll?.('div[role="dialog"]') ?? [];
|
|
145
|
-
for (const p of panels) {
|
|
146
|
-
try {
|
|
147
|
-
if (p.querySelector(':scope > nav')) return p;
|
|
148
|
-
} catch {
|
|
149
|
-
/* :scope 不可用时跳过该锚点 */
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const closestClass = (el, local) => {
|
|
156
|
-
let cur = el;
|
|
157
|
-
while (cur && cur !== el.ownerDocument?.body) {
|
|
158
|
-
if (familyFromClasses(classListOf(cur), local)) return cur;
|
|
159
|
-
cur = cur.parentElement;
|
|
160
|
-
}
|
|
161
|
-
return null;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const composerCard = (doc) => {
|
|
165
|
-
const direct = doc.querySelector?.('[data-composer-card="true"]');
|
|
166
|
-
if (direct) return direct;
|
|
167
|
-
const input = doc.querySelector?.('[data-composer-input="true"]')
|
|
168
|
-
|| doc.querySelector?.('div[role="textbox"], textarea');
|
|
169
|
-
return input ? closestClass(input, 'card') || input : null;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* 槽位表:每个槽位=一组同族规则,独立探测、独立降级。
|
|
174
|
-
* `known` 为已核实的历代前缀(顺序=优先级);只在「真的命中页面」或探测失败时才采用。
|
|
175
|
-
* `critical` 的槽位探测失败会触发显式告警(其余只记 debug)。
|
|
176
|
-
*/
|
|
177
|
-
export const FAMILY_SLOTS = {
|
|
178
|
-
// 设置中心弹窗(手机端样式失效的主战场)
|
|
179
|
-
SETTINGS: {
|
|
180
|
-
critical: true,
|
|
181
|
-
locals: ['overlay', 'panel', 'nav', 'navList', 'navCell', 'navLabel', 'navTitle', 'options', 'content', 'close', 'header'],
|
|
182
|
-
known: ['wCInkW', 'VOzbGW'],
|
|
183
|
-
anchors: [{ local: 'panel', find: settingsPanel }],
|
|
184
|
-
},
|
|
185
|
-
// 对话区:头部标题行、面包屑、滚动体、输入座
|
|
186
|
-
CONV: {
|
|
187
|
-
locals: ['header', 'titleRow', 'headerActions', 'headerUtilities', 'crumbs', 'scrollBody', 'viewArea', 'composerSeat'],
|
|
188
|
-
known: ['Dc7zOa', 'wSkVaW'],
|
|
189
|
-
anchors: [{ local: 'header', find: (doc) => doc.querySelector?.('div[class*="_centerCol"] header') || null }],
|
|
190
|
-
},
|
|
191
|
-
// 输入卡片(卡片本体 / 底部工具行 / 模式区 / 尾随区)
|
|
192
|
-
COMPOSER: {
|
|
193
|
-
locals: ['card', 'row', 'tools', 'modes', 'trailing', 'overlayAnchor'],
|
|
194
|
-
known: ['RlGAzG', 'uV2eYG'],
|
|
195
|
-
anchors: [{ local: 'card', find: composerCard }],
|
|
196
|
-
},
|
|
197
|
-
// 左侧边栏(抽屉化的对象)
|
|
198
|
-
SIDEBAR: {
|
|
199
|
-
locals: ['root', 'logoRow', 'toggle', 'collapsed', 'newSession', 'regionArea'],
|
|
200
|
-
known: ['_2H3hWW', 'hHd-Xa'],
|
|
201
|
-
anchors: [{
|
|
202
|
-
local: 'root',
|
|
203
|
-
find: (doc) => {
|
|
204
|
-
const btn = aria(/新建会话|New session/)(doc);
|
|
205
|
-
return btn ? closestClass(btn, 'root') : null;
|
|
206
|
-
},
|
|
207
|
-
}],
|
|
208
|
-
},
|
|
209
|
-
// 工作区列表轨道(搜索/视图选项/列表区)
|
|
210
|
-
WS: {
|
|
211
|
-
locals: ['root', 'searchButton', 'listArea', 'rail'],
|
|
212
|
-
known: ['_9lTDKa', 'qDHVXG'],
|
|
213
|
-
anchors: [{ local: 'searchButton', find: aria(/搜索会话|Search sessions/) }],
|
|
214
|
-
},
|
|
215
|
-
// 模型选择器
|
|
216
|
-
MODEL: {
|
|
217
|
-
locals: ['root', 'trigger', 'triggerLabel'],
|
|
218
|
-
known: ['wq12jW', '_7KE1Ra'],
|
|
219
|
-
anchors: [{ local: 'trigger', find: aria(/选择模型|Select model|模型/) }],
|
|
220
|
-
},
|
|
221
|
-
// 访问模式选择器
|
|
222
|
-
ACCESS: {
|
|
223
|
-
locals: ['trigger'],
|
|
224
|
-
known: ['dlU_AG', 'Sh0Q9G'],
|
|
225
|
-
anchors: [{ local: 'trigger', find: aria(/访问模式|Access mode/) }],
|
|
226
|
-
},
|
|
227
|
-
// Agent 预设胶囊(本地名历代不一:seat / trigger 都收)
|
|
228
|
-
PRESET: {
|
|
229
|
-
locals: ['seat', 'trigger', 'menu'],
|
|
230
|
-
known: ['_oGoKq', 'cubgiG', 'h8S2Va'],
|
|
231
|
-
anchors: [{ local: 'seat', find: aria(/Agent 预设|Agent preset/) }],
|
|
232
|
-
},
|
|
233
|
-
// 会话日志导出按钮
|
|
234
|
-
LOG: {
|
|
235
|
-
locals: ['sessionLogButton'],
|
|
236
|
-
known: ['nL4_yW'],
|
|
237
|
-
anchors: [{ local: 'sessionLogButton', find: aria(/Session Log|会话日志|导出|Export/i) }],
|
|
238
|
-
},
|
|
239
|
-
// Workbench 面板 / Tab 栏(0.2.0 起改为 dockkit,本地名未采全,故只保留基线 + 无锚点)
|
|
240
|
-
WB: {
|
|
241
|
-
locals: ['panel', 'tabBar', 'tabList', 'tab', 'tabActive', 'tabTitle', 'tabClose', 'tabBarPlus'],
|
|
242
|
-
known: ['nArs4W'],
|
|
243
|
-
anchors: [],
|
|
244
|
-
},
|
|
245
|
-
// 折叠簇容器(本地名 `toggleCluster` 历代不变,但族哈希只在 0.1.x 采到过)
|
|
246
|
-
TOGGLE: {
|
|
247
|
-
locals: ['toggleCluster'],
|
|
248
|
-
known: ['W-zNGW'],
|
|
249
|
-
anchors: [{ local: 'toggleCluster', find: (doc) => doc.querySelector?.('div[class*="toggleCluster"]') || null }],
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* 关键判定:设置弹窗是否真的被「注入的 CSS」命中。
|
|
255
|
-
*
|
|
256
|
-
* 刻意用**带标签**的探针,与 CSS 里真正写的结构选择器同形(`div[class*="F_panel"]`、
|
|
257
|
-
* `nav[class*="F_nav"]`、`div[class*="F_options"]`、`div[class*="F_overlay"]`)。
|
|
258
|
-
* 无标签的子串查询会被无关元素满足——独立验收实测:面板类名被整代改掉、只剩
|
|
259
|
-
* `svg.wCInkW_navIcon` 时,`[class*="wCInkW_nav"]` 仍命中,于是 degraded 漏报。
|
|
260
|
-
*/
|
|
261
|
-
function criticalSettingsHit(doc, family) {
|
|
262
|
-
const probes = [
|
|
263
|
-
`div[class*="${family}_panel"]`,
|
|
264
|
-
`nav[class*="${family}_nav"]`,
|
|
265
|
-
`div[class*="${family}_options"]`,
|
|
266
|
-
`div[class*="${family}_overlay"]`,
|
|
267
|
-
];
|
|
268
|
-
for (const sel of probes) {
|
|
269
|
-
try {
|
|
270
|
-
if (doc.querySelector(sel)) return true;
|
|
271
|
-
} catch {
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* 解析各槽位的真实前缀。
|
|
280
|
-
*
|
|
281
|
-
* @param {Document} doc
|
|
282
|
-
* @param {typeof FAMILY_SLOTS} [slots]
|
|
283
|
-
* @returns {{families: Record<string,string>, sources: Record<string,string>, missing: string[], unverified: string[], degraded: boolean, compat: 'ok'|'partial'|'degraded', settingsMounted: boolean}}
|
|
284
|
-
* sources 取值:detected(运行时探测)/ known(基线且实测命中本地名)/
|
|
285
|
-
* known-weak(基线且页面上存在该代次痕迹,但该槽位的本地名尚未挂载)/
|
|
286
|
-
* assumed(无任何痕迹,按基线首项假设)/ absent(无可用前缀)
|
|
287
|
-
* compat:ok=全部槽位有据可依;partial=主链路可用但有槽位只能假设;
|
|
288
|
-
* degraded=设置弹窗已挂载却解析不出前缀(即过去那种静默失效)
|
|
289
|
-
*/
|
|
290
|
-
export function resolveHostFamilies(doc = globalThis.document, slots = FAMILY_SLOTS) {
|
|
291
|
-
const families = {};
|
|
292
|
-
const sources = {};
|
|
293
|
-
const missing = [];
|
|
294
|
-
const unverified = [];
|
|
295
|
-
// 一次扫描建索引,后面所有命中测试都查它(见 buildClassIndex 的性能说明)
|
|
296
|
-
const index = buildClassIndex(doc);
|
|
297
|
-
|
|
298
|
-
for (const [slot, spec] of Object.entries(slots || {})) {
|
|
299
|
-
let family = null;
|
|
300
|
-
let source = null;
|
|
301
|
-
|
|
302
|
-
for (const anchor of spec.anchors ?? []) {
|
|
303
|
-
let el;
|
|
304
|
-
try {
|
|
305
|
-
el = anchor.find(doc);
|
|
306
|
-
} catch {
|
|
307
|
-
el = null;
|
|
308
|
-
}
|
|
309
|
-
const found = familyFromClasses(classListOf(el), anchor.local);
|
|
310
|
-
if (found) {
|
|
311
|
-
family = found;
|
|
312
|
-
source = 'detected';
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (!family) {
|
|
318
|
-
for (const cand of spec.known ?? []) {
|
|
319
|
-
if (familyHits(doc, cand, spec.locals, index)) {
|
|
320
|
-
family = cand;
|
|
321
|
-
source = 'known';
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// 组件还没挂载时的「代次判断」:用弱命中(任意 <cand>_* 痕迹)决定基线取哪一代,
|
|
328
|
-
// 否则会退化成「一律假设最新宿主」,在旧宿主上把整片规则让掉。
|
|
329
|
-
if (!family) {
|
|
330
|
-
for (const cand of spec.known ?? []) {
|
|
331
|
-
if (familyPresent(doc, cand, index)) {
|
|
332
|
-
family = cand;
|
|
333
|
-
source = 'known-weak';
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!family) {
|
|
340
|
-
const cand = norm((spec.known ?? [])[0]);
|
|
341
|
-
if (cand) {
|
|
342
|
-
family = cand;
|
|
343
|
-
source = 'assumed';
|
|
344
|
-
unverified.push(slot);
|
|
345
|
-
} else {
|
|
346
|
-
family = ABSENT_FAMILY;
|
|
347
|
-
source = 'absent';
|
|
348
|
-
missing.push(slot);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
families[slot] = family;
|
|
353
|
-
sources[slot] = source;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// 显式降级判定:设置弹窗已经挂在页面上,却没有解析出可用前缀
|
|
357
|
-
// (即锚点失配 + 基线也匹配不上)——这正是过去会静默失效的状态。
|
|
358
|
-
const mounted = (() => {
|
|
359
|
-
try {
|
|
360
|
-
return !!settingsPanel(doc);
|
|
361
|
-
} catch {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
})();
|
|
365
|
-
const settingsOk = families.SETTINGS
|
|
366
|
-
&& families.SETTINGS !== ABSENT_FAMILY
|
|
367
|
-
&& (!mounted || criticalSettingsHit(doc, families.SETTINGS));
|
|
368
|
-
const degraded = !settingsOk;
|
|
369
|
-
|
|
370
|
-
// 'partial':设置弹窗这一条主链路可用,但有槽位只能"假设"(既没探测到、也没在页面上
|
|
371
|
-
// 留下痕迹)。它们不会误伤(顶多规则空转),但必须给出可见信号 —— 独立验收指出
|
|
372
|
-
// LOG/WB/TOGGLE 过去只进返回值、compat 仍是 ok,等于留了一块静默面。
|
|
373
|
-
const compat = degraded ? 'degraded' : ((unverified.length || missing.length) ? 'partial' : 'ok');
|
|
374
|
-
|
|
375
|
-
return { families, sources, missing, unverified, degraded, compat, settingsMounted: mounted };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* 渲染模板:把 `__F_SLOT__` 换成 `<真实前缀>_`。
|
|
380
|
-
* 注意占位符本身**不含**本地名前的下划线分隔符(`__F_SETTINGS__overlay` 里的 `__`
|
|
381
|
-
* 是定界符),故作替换时必须补回 `_`,否则会渲染成 `wCInkWoverlay` 这种废选择器。
|
|
382
|
-
* 未解析出的槽位换成永不匹配的哨兵,绝不退化成宽泛选择器。
|
|
383
|
-
*/
|
|
384
|
-
export function renderMobileStyles(template, families = {}) {
|
|
385
|
-
return String(template ?? '').replace(FAMILY_TOKEN_RE, (_all, slot) => {
|
|
386
|
-
const fam = norm(families[slot]);
|
|
387
|
-
return `${fam || ABSENT_FAMILY}_`;
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/** 模板里引用到的槽位名(测试用:确保没有拼错的占位符)。 */
|
|
392
|
-
export function referencedSlots(template) {
|
|
393
|
-
const out = new Set();
|
|
394
|
-
for (const m of String(template ?? '').matchAll(FAMILY_TOKEN_RE)) out.add(m[1]);
|
|
395
|
-
return [...out];
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/** 稳定的签名,用于判断「探测结果是否真的变了」,避免无谓重渲染。 */
|
|
399
|
-
export function familiesSignature(families = {}) {
|
|
400
|
-
return Object.keys(families).sort().map((k) => `${k}=${families[k]}`).join('|');
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* 守护宿主 DOM,前缀变化或 DOM 抖动时回调。
|
|
405
|
-
*
|
|
406
|
-
* 刻意**不在本模块内做去重**:调用方(client/index.js)的注入本身幂等(节点复用 +
|
|
407
|
-
* 文本比对),而去重会把"样式表被宿主 HMR 删掉、但族签名没变"这种情况一起吞掉
|
|
408
|
-
* (独立验收实测:把补挂写在"签名变化"分支里时,删表后抖动多次也不会补回)。
|
|
409
|
-
* 本模块只负责"防抖 + 在整棵树(含 <head>)上观察"。
|
|
410
|
-
*
|
|
411
|
-
* @param {(result: ReturnType<typeof resolveHostFamilies>) => void} onChange
|
|
412
|
-
* @param {{doc?: Document, slots?: typeof FAMILY_SLOTS, debounceMs?: number}} [opts]
|
|
413
|
-
* @returns {() => void} 清理函数
|
|
414
|
-
*/
|
|
415
|
-
export function watchHostFamilies(onChange, opts = {}) {
|
|
416
|
-
const doc = opts.doc ?? globalThis.document;
|
|
417
|
-
const slots = opts.slots ?? FAMILY_SLOTS;
|
|
418
|
-
const debounceMs = opts.debounceMs ?? 120;
|
|
419
|
-
if (!doc || typeof doc.addEventListener !== 'function' || typeof MutationObserver === 'undefined') {
|
|
420
|
-
return () => {};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
let timer = null;
|
|
424
|
-
const run = () => {
|
|
425
|
-
timer = null;
|
|
426
|
-
onChange(resolveHostFamilies(doc, slots));
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
const observer = new MutationObserver(() => {
|
|
430
|
-
if (timer) return;
|
|
431
|
-
timer = setTimeout(run, debounceMs);
|
|
432
|
-
});
|
|
433
|
-
// 观察 documentElement(而非 body):<style> 挂在 <head>,宿主 HMR 删表时
|
|
434
|
-
// body 子树根本不会产生变更记录,补挂逻辑就永远不会被唤醒。
|
|
435
|
-
observer.observe(doc.documentElement ?? doc.body, { childList: true, subtree: true });
|
|
436
|
-
|
|
437
|
-
return () => {
|
|
438
|
-
if (timer) clearTimeout(timer);
|
|
439
|
-
timer = null;
|
|
440
|
-
observer.disconnect();
|
|
441
|
-
};
|
|
442
|
-
}
|