dsh-model-doctor 0.4.0 → 0.4.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/lib/client/index.js +119 -39
- package/package.json +1 -1
package/lib/client/index.js
CHANGED
|
@@ -181,6 +181,10 @@ window.__ModuleLoader__.load({
|
|
|
181
181
|
/** 模型名/ID -> 候选条目索引;15 秒内复用。 */
|
|
182
182
|
let tokenIndex = null
|
|
183
183
|
let tokenIndexAt = 0
|
|
184
|
+
/** 预编译的 token 正则(先粗筛文本节点,避免对每个节点做全量比对)。 */
|
|
185
|
+
let tokenRegex = null
|
|
186
|
+
/** [token, entries] 按 token 长度降序:优先匹配更具体的名字。 */
|
|
187
|
+
let tokenPairs = []
|
|
184
188
|
|
|
185
189
|
function ensureTokenIndex() {
|
|
186
190
|
if (tokenIndex !== null && Date.now() - tokenIndexAt < 15000) return tokenIndex
|
|
@@ -203,6 +207,10 @@ window.__ModuleLoader__.load({
|
|
|
203
207
|
}
|
|
204
208
|
tokenIndex = index
|
|
205
209
|
tokenIndexAt = Date.now()
|
|
210
|
+
tokenPairs = Array.from(index.entries()).sort((a, b) => b[0].length - a[0].length)
|
|
211
|
+
tokenRegex = tokenPairs.length === 0
|
|
212
|
+
? null
|
|
213
|
+
: new RegExp(tokenPairs.map(([token]) => token.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('|'), 'u')
|
|
206
214
|
return index
|
|
207
215
|
}
|
|
208
216
|
|
|
@@ -253,24 +261,25 @@ window.__ModuleLoader__.load({
|
|
|
253
261
|
}
|
|
254
262
|
|
|
255
263
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
264
|
+
* 全文档找出「命中了已知模型名」的最小行容器。
|
|
265
|
+
* 不再依赖任何菜单根选择器:先用预编译正则粗筛文本节点,再逐一确认 token 命中,
|
|
266
|
+
* 然后向上取最近的可点击祖先。
|
|
258
267
|
*/
|
|
259
|
-
function
|
|
268
|
+
function minimalMatches(root) {
|
|
260
269
|
const index = ensureTokenIndex()
|
|
261
|
-
if (index.size === 0) return []
|
|
262
270
|
const found = new Map()
|
|
271
|
+
if (index.size === 0 || tokenRegex === null) return found
|
|
263
272
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
264
273
|
let node = walker.nextNode()
|
|
265
274
|
while (node !== null) {
|
|
266
275
|
const raw = node.nodeValue
|
|
267
|
-
if (raw !== null && raw.length >
|
|
276
|
+
if (raw !== null && raw.length > 1 && raw.length <= 200) {
|
|
268
277
|
const text = normText(raw)
|
|
269
|
-
if (text.length >= 2) {
|
|
278
|
+
if (text.length >= 2 && tokenRegex.test(text)) {
|
|
270
279
|
const anchor = node.parentElement
|
|
271
280
|
if (anchor !== null) {
|
|
272
|
-
for (const [token, entries] of
|
|
273
|
-
if (!text.includes(token)) continue
|
|
281
|
+
for (const [token, entries] of tokenPairs) {
|
|
282
|
+
if (token.length > text.length || !text.includes(token)) continue
|
|
274
283
|
const row = clickableRow(anchor, root)
|
|
275
284
|
if (row === null) continue
|
|
276
285
|
const current = found.get(row)
|
|
@@ -285,16 +294,69 @@ window.__ModuleLoader__.load({
|
|
|
285
294
|
}
|
|
286
295
|
// 排除「容器型行」:行文本里出现两个以上不同模型 → 它装的是列表,不是单行。
|
|
287
296
|
for (const [row] of Array.from(found.entries())) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
if (countDistinctModels(row, index) > 1) found.delete(row)
|
|
298
|
+
}
|
|
299
|
+
return found
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 一个元素里出现了几个不同模型(>1 说明它是容器)。 */
|
|
303
|
+
function countDistinctModels(el, index) {
|
|
304
|
+
const text = normText(el.textContent)
|
|
305
|
+
const distinct = new Set()
|
|
306
|
+
for (const [token, entries] of index) {
|
|
307
|
+
if (!text.includes(token)) continue
|
|
308
|
+
for (const entry of entries) distinct.add(`${entry.providerId}::${entry.modelId}`)
|
|
309
|
+
if (distinct.size > 1) return distinct.size
|
|
310
|
+
}
|
|
311
|
+
return distinct.size
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 容器是否像弹层(绝对定位或带菜单语义),用于排除正文里碰巧出现的模型名。 */
|
|
315
|
+
function isPopoverish(el) {
|
|
316
|
+
let node = el
|
|
317
|
+
let depth = 0
|
|
318
|
+
while (node !== null && node !== document.body && depth < 4) {
|
|
319
|
+
const role = typeof node.getAttribute === 'function' ? node.getAttribute('role') : null
|
|
320
|
+
if (typeof role === 'string' && /^(menu|listbox|dialog|alertdialog)$/u.test(role)) return true
|
|
321
|
+
try {
|
|
322
|
+
const position = window.getComputedStyle(node).position
|
|
323
|
+
if (position === 'absolute' || position === 'fixed') return true
|
|
324
|
+
} catch { /* 忽略计算样式失败 */ }
|
|
325
|
+
node = node.parentElement
|
|
326
|
+
depth += 1
|
|
327
|
+
}
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** 把命中的行按容器聚类:同一父节点下 ≥2 行才算一个菜单。 */
|
|
332
|
+
function clusterRows(found) {
|
|
333
|
+
const byParent = new Map()
|
|
334
|
+
for (const [row, hit] of found.entries()) {
|
|
335
|
+
const parent = row.parentElement ?? document.body
|
|
336
|
+
const list = byParent.get(parent) ?? []
|
|
337
|
+
list.push({ row, ...hit })
|
|
338
|
+
byParent.set(parent, list)
|
|
339
|
+
}
|
|
340
|
+
const clusters = []
|
|
341
|
+
const orphans = []
|
|
342
|
+
for (const [parent, list] of byParent.entries()) {
|
|
343
|
+
if (list.length >= 2) clusters.push({ host: parent, rows: list })
|
|
344
|
+
else orphans.push(...list)
|
|
345
|
+
}
|
|
346
|
+
// 行被分组标题/包装层隔开时,退一级按祖父容器聚合
|
|
347
|
+
if (orphans.length >= 2) {
|
|
348
|
+
const byGrand = new Map()
|
|
349
|
+
for (const hit of orphans) {
|
|
350
|
+
const grand = hit.row.parentElement?.parentElement ?? document.body
|
|
351
|
+
const list = byGrand.get(grand) ?? []
|
|
352
|
+
list.push(hit)
|
|
353
|
+
byGrand.set(grand, list)
|
|
354
|
+
}
|
|
355
|
+
for (const [grand, list] of byGrand.entries()) {
|
|
356
|
+
if (list.length >= 2) clusters.push({ host: grand, rows: list })
|
|
294
357
|
}
|
|
295
|
-
if (distinct.size > 1) found.delete(row)
|
|
296
358
|
}
|
|
297
|
-
return
|
|
359
|
+
return clusters.filter((cluster) => isPopoverish(cluster.host))
|
|
298
360
|
}
|
|
299
361
|
|
|
300
362
|
/** 根容器是否可见(不可见的弹层跳过,避免误伤收起的菜单)。 */
|
|
@@ -698,52 +760,70 @@ window.__ModuleLoader__.load({
|
|
|
698
760
|
|
|
699
761
|
/**
|
|
700
762
|
* 通用路径:任何菜单渲染器(官方原生 / dsh-effort-slider / 其他弹层)。
|
|
701
|
-
*
|
|
763
|
+
* 依据「已知模型名」在全文定位行,按容器聚类成菜单,再逐行增强。
|
|
702
764
|
*/
|
|
703
765
|
function enhanceGeneric(legacyWired) {
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const samples = roots.slice(0, 6).map(describe)
|
|
766
|
+
const index = ensureTokenIndex()
|
|
767
|
+
const roots = Array.from(document.querySelectorAll(MENU_ROOT_SEL)).filter((root) => isVisible(root))
|
|
768
|
+
const hits = minimalMatches(document.body)
|
|
769
|
+
const clusters = clusterRows(hits)
|
|
709
770
|
let wired = 0
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
for (const hit of hits) {
|
|
771
|
+
let registered = 0
|
|
772
|
+
let unmatched = 0
|
|
773
|
+
const clusterStats = []
|
|
774
|
+
for (const cluster of clusters) {
|
|
775
|
+
let newlyWired = 0
|
|
776
|
+
for (const hit of cluster.rows) {
|
|
717
777
|
const entry = pickEntry(hit.entries, hit.row)
|
|
718
|
-
if (entry === undefined)
|
|
778
|
+
if (entry === undefined) {
|
|
779
|
+
unmatched += 1
|
|
780
|
+
continue
|
|
781
|
+
}
|
|
782
|
+
const isNew = hit.row.querySelector('.dmd-actions') === null
|
|
719
783
|
rowAnchor.set(hit.row, hit.anchor)
|
|
720
784
|
enhanceRow(hit.row, entry.providerId, { id: entry.modelId, name: entry.modelName })
|
|
721
|
-
|
|
785
|
+
registered += 1
|
|
786
|
+
if (isNew) newlyWired += 1
|
|
722
787
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
788
|
+
clusterStats.push({
|
|
789
|
+
host: describe(cluster.host),
|
|
790
|
+
rows: cluster.rows.length,
|
|
791
|
+
wired: newlyWired,
|
|
792
|
+
})
|
|
793
|
+
if (cluster.rows.length >= MIN_GENERIC_ROWS) {
|
|
794
|
+
const first = cluster.rows[0]
|
|
726
795
|
let hint = ''
|
|
727
796
|
if (directory === null) {
|
|
728
797
|
hint = directoryFailedAt !== 0
|
|
729
798
|
? `模型医生:目录获取失败 · ${directoryError || '与宿主半通信异常'}`
|
|
730
799
|
: '模型医生:目录获取中…'
|
|
800
|
+
} else if (newlyWired === 0 && first.row.querySelector('.dmd-actions') === null) {
|
|
801
|
+
hint = '模型医生:模型行未匹配上目录'
|
|
731
802
|
}
|
|
732
|
-
|
|
803
|
+
const barHost = first.row.parentElement ?? cluster.host
|
|
804
|
+
ensureAllBar(barHost, hint, first.row)
|
|
733
805
|
}
|
|
734
806
|
}
|
|
735
807
|
lastDiagnostics = {
|
|
736
808
|
at: Date.now(),
|
|
737
809
|
mode: legacyWired > 0 ? 'legacy+generic' : 'generic',
|
|
738
|
-
|
|
810
|
+
href: window.location.href.slice(0, 120),
|
|
811
|
+
tokens: index.size,
|
|
739
812
|
providerCount: directory?.providers?.length ?? 0,
|
|
740
813
|
modelCount: (directory?.providers ?? []).reduce((sum, p) => sum + p.models.length, 0),
|
|
814
|
+
directoryError,
|
|
741
815
|
legacyWired,
|
|
742
816
|
genericWired: wired,
|
|
743
|
-
|
|
744
|
-
|
|
817
|
+
registered,
|
|
818
|
+
unmatched,
|
|
819
|
+
hitCount: hits.size,
|
|
820
|
+
visibleRoots: roots.length,
|
|
821
|
+
roots: clusterStats.slice(0, 8),
|
|
822
|
+
samples: roots.slice(0, 5).map(describe),
|
|
823
|
+
barCount: document.querySelectorAll('.dmd-allbar').length,
|
|
824
|
+
actionCount: document.querySelectorAll('.dmd-actions').length,
|
|
745
825
|
}
|
|
746
|
-
return wired
|
|
826
|
+
return registered > 0 ? Math.max(1, wired) : 0
|
|
747
827
|
}
|
|
748
828
|
|
|
749
829
|
function enhance() {
|
package/package.json
CHANGED