dsh-model-doctor 0.4.0 → 0.5.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 (2) hide show
  1. package/lib/client/index.js +322 -72
  2. package/package.json +2 -2
@@ -38,6 +38,13 @@ window.__ModuleLoader__.load({
38
38
  '[role="dialog"]',
39
39
  '[data-state="open"][aria-modal]',
40
40
  ].join(',')
41
+ /** 官方菜单(@deepseek-ai/dsh-client-ui-model-selection):类名是 CSS Module 哈希
42
+ * (形如 `wq12jW_option wq12jW_modelOption`),但局部名稳定,用后缀匹配;
43
+ * 模型行是 `button[role="menuitemradio"]`,且 `title` 属性就是模型名。 */
44
+ const OFFICIAL_ROW_SEL = '[role="menuitemradio"]'
45
+ const OFFICIAL_MENU_SUFFIX = '_menu'
46
+ const OFFICIAL_OPTION_SUFFIX = 'modelOption'
47
+ const OFFICIAL_NAME_SUFFIX = 'modelName'
41
48
  const DIRECTORY_TTL_MS = 30000
42
49
  const DIRECTORY_RETRY_MS = 10000
43
50
  /** 一个菜单里至少要有这么多个模型行,才认定它是模型菜单(防误伤其他弹层)。 */
@@ -139,6 +146,10 @@ window.__ModuleLoader__.load({
139
146
  let rowAnchor = new WeakMap()
140
147
  /** 本次扫描的通用路径诊断信息(上报给宿主,便于远程定位 DOM 结构)。 */
141
148
  let lastDiagnostics = null
149
+ /** 通用(文本聚类)路径统计。 */
150
+ let lastGenericStats = null
151
+ /** 官方路径统计。 */
152
+ let lastOfficialStats = null
142
153
 
143
154
  /** 本页会话内已删除的模型键,防目录未刷新时行又冒出来。 */
144
155
  const deletedKeys = new Set()
@@ -178,9 +189,49 @@ window.__ModuleLoader__.load({
178
189
  return String(value ?? '').replace(/\s+/gu, ' ').trim().toLowerCase()
179
190
  }
180
191
 
192
+ /** 折叠文本:去掉一切非字母数字(含空格、连字符、点),用于宽松名字比对。
193
+ * 「gpt-4o mini」与「gpt-4o-mini」折叠后同为 gpt4omini。 */
194
+ function foldText(value) {
195
+ return String(value ?? '').toLowerCase().replace(/[^0-9a-z\u4e00-\u9fff]+/gu, '')
196
+ }
197
+
198
+ /** 元素类名里是否含某个局部名(CSS Module 哈希前缀无关)。 */
199
+ function hasClassToken(el, token) {
200
+ const cls = typeof el.className === 'string' ? el.className : ''
201
+ return cls.toLowerCase().includes(token.toLowerCase())
202
+ }
203
+
204
+ /** 目录里按 名字/ID 找模型:先精确原名,再折叠名。 */
205
+ function findModelsByName(name) {
206
+ const raw = normText(name)
207
+ const folded = foldText(name)
208
+ const hits = []
209
+ if (raw.length === 0) return hits
210
+ for (const provider of directory?.providers ?? []) {
211
+ for (const model of provider.models) {
212
+ const exact = normText(model.name) === raw || normText(model.id) === raw
213
+ const loose = !exact
214
+ && folded.length >= 3
215
+ && (foldText(model.name) === folded || foldText(model.id) === folded)
216
+ if (!exact && !loose) continue
217
+ hits.push({
218
+ providerId: provider.id,
219
+ providerName: provider.name,
220
+ modelId: model.id,
221
+ modelName: model.name,
222
+ })
223
+ }
224
+ }
225
+ return hits
226
+ }
227
+
181
228
  /** 模型名/ID -> 候选条目索引;15 秒内复用。 */
182
229
  let tokenIndex = null
183
230
  let tokenIndexAt = 0
231
+ /** 预编译的 token 正则(先粗筛文本节点,避免对每个节点做全量比对)。 */
232
+ let tokenRegex = null
233
+ /** [token, entries] 按 token 长度降序:优先匹配更具体的名字。 */
234
+ let tokenPairs = []
184
235
 
185
236
  function ensureTokenIndex() {
186
237
  if (tokenIndex !== null && Date.now() - tokenIndexAt < 15000) return tokenIndex
@@ -203,6 +254,10 @@ window.__ModuleLoader__.load({
203
254
  }
204
255
  tokenIndex = index
205
256
  tokenIndexAt = Date.now()
257
+ tokenPairs = Array.from(index.entries()).sort((a, b) => b[0].length - a[0].length)
258
+ tokenRegex = tokenPairs.length === 0
259
+ ? null
260
+ : new RegExp(tokenPairs.map(([token]) => token.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')).join('|'), 'u')
206
261
  return index
207
262
  }
208
263
 
@@ -253,48 +308,117 @@ window.__ModuleLoader__.load({
253
308
  }
254
309
 
255
310
  /**
256
- * 在一个候选根容器里找出模型行:对每个文本节点做已知模型名命中,
257
- * 向上取最近的可点击祖先;若某「行」文本里出现过两个以上不同模型,则它是容器,排除。
311
+ * 全文档找出「命中了已知模型名」的最小行容器。
312
+ * 不再依赖任何菜单根选择器:先用预编译正则粗筛文本节点,再逐一确认 token 命中,
313
+ * 然后向上取最近的可点击祖先。
258
314
  */
259
- function genericRowsIn(root) {
315
+ function minimalMatches(root) {
260
316
  const index = ensureTokenIndex()
261
- if (index.size === 0) return []
262
317
  const found = new Map()
263
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
264
- let node = walker.nextNode()
265
- while (node !== null) {
266
- const raw = node.nodeValue
267
- if (raw !== null && raw.length > 0 && raw.length <= 200) {
268
- const text = normText(raw)
269
- if (text.length >= 2) {
270
- const anchor = node.parentElement
271
- if (anchor !== null) {
272
- for (const [token, entries] of index) {
273
- if (!text.includes(token)) continue
274
- const row = clickableRow(anchor, root)
275
- if (row === null) continue
276
- const current = found.get(row)
277
- if (current === undefined || token.length > current.token.length) {
278
- found.set(row, { token, entries, anchor })
318
+ if (index.size === 0 || tokenRegex === null) return found
319
+ const record = (row, token, entries, anchor) => {
320
+ const current = found.get(row)
321
+ if (current === undefined || token.length > current.token.length) {
322
+ found.set(row, { token, entries, anchor })
323
+ }
324
+ }
325
+ const scan = (pairs, folded) => {
326
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
327
+ let node = walker.nextNode()
328
+ while (node !== null) {
329
+ const raw = node.nodeValue
330
+ if (raw !== null && raw.length > 1 && raw.length <= 200) {
331
+ const text = folded ? foldText(raw) : normText(raw)
332
+ if (text.length >= 2 && (folded || tokenRegex.test(text))) {
333
+ const anchor = node.parentElement
334
+ if (anchor !== null) {
335
+ for (const [token, entries] of pairs) {
336
+ if (token.length > text.length || !text.includes(token)) continue
337
+ const row = clickableRow(anchor, root)
338
+ if (row === null) continue
339
+ record(row, token, entries, anchor)
279
340
  }
280
341
  }
281
342
  }
282
343
  }
344
+ node = walker.nextNode()
283
345
  }
284
- node = walker.nextNode()
346
+ }
347
+ scan(tokenPairs, false)
348
+ // 原名对不上时(菜单显示名与目录写法不同,如空格 vs 连字符)再做一遍折叠匹配
349
+ if (found.size < 2) {
350
+ const foldedPairs = []
351
+ for (const [token, entries] of tokenPairs) {
352
+ const folded = foldText(token)
353
+ if (folded.length >= 3) foldedPairs.push([folded, entries])
354
+ }
355
+ foldedPairs.sort((a, b) => b[0].length - a[0].length)
356
+ if (foldedPairs.length > 0) scan(foldedPairs, true)
285
357
  }
286
358
  // 排除「容器型行」:行文本里出现两个以上不同模型 → 它装的是列表,不是单行。
287
359
  for (const [row] of Array.from(found.entries())) {
288
- const rowText = normText(row.textContent)
289
- const distinct = new Set()
290
- for (const [token, entries] of index) {
291
- if (!rowText.includes(token)) continue
292
- for (const entry of entries) distinct.add(`${entry.providerId}::${entry.modelId}`)
293
- if (distinct.size > 1) break
360
+ if (countDistinctModels(row, index) > 1) found.delete(row)
361
+ }
362
+ return found
363
+ }
364
+
365
+ /** 一个元素里出现了几个不同模型(>1 说明它是容器)。 */
366
+ function countDistinctModels(el, index) {
367
+ const text = normText(el.textContent)
368
+ const distinct = new Set()
369
+ for (const [token, entries] of index) {
370
+ if (!text.includes(token)) continue
371
+ for (const entry of entries) distinct.add(`${entry.providerId}::${entry.modelId}`)
372
+ if (distinct.size > 1) return distinct.size
373
+ }
374
+ return distinct.size
375
+ }
376
+
377
+ /** 容器是否像弹层(绝对定位或带菜单语义),用于排除正文里碰巧出现的模型名。 */
378
+ function isPopoverish(el) {
379
+ let node = el
380
+ let depth = 0
381
+ while (node !== null && node !== document.body && depth < 4) {
382
+ const role = typeof node.getAttribute === 'function' ? node.getAttribute('role') : null
383
+ if (typeof role === 'string' && /^(menu|listbox|dialog|alertdialog)$/u.test(role)) return true
384
+ try {
385
+ const position = window.getComputedStyle(node).position
386
+ if (position === 'absolute' || position === 'fixed') return true
387
+ } catch { /* 忽略计算样式失败 */ }
388
+ node = node.parentElement
389
+ depth += 1
390
+ }
391
+ return false
392
+ }
393
+
394
+ /** 把命中的行按容器聚类:同一父节点下 ≥2 行才算一个菜单。 */
395
+ function clusterRows(found) {
396
+ const byParent = new Map()
397
+ for (const [row, hit] of found.entries()) {
398
+ const parent = row.parentElement ?? document.body
399
+ const list = byParent.get(parent) ?? []
400
+ list.push({ row, ...hit })
401
+ byParent.set(parent, list)
402
+ }
403
+ const clusters = []
404
+ const orphans = []
405
+ for (const [parent, list] of byParent.entries()) {
406
+ if (list.length >= 1) clusters.push({ host: parent, rows: list })
407
+ else orphans.push(...list)
408
+ }
409
+ if (orphans.length >= 1) {
410
+ const byGrand = new Map()
411
+ for (const hit of orphans) {
412
+ const grand = hit.row.parentElement?.parentElement ?? document.body
413
+ const list = byGrand.get(grand) ?? []
414
+ list.push(hit)
415
+ byGrand.set(grand, list)
416
+ }
417
+ for (const [grand, list] of byGrand.entries()) {
418
+ if (list.length >= 1) clusters.push({ host: grand, rows: list })
294
419
  }
295
- if (distinct.size > 1) found.delete(row)
296
420
  }
297
- return Array.from(found.entries()).map(([row, hit]) => ({ row, ...hit }))
421
+ return clusters
298
422
  }
299
423
 
300
424
  /** 根容器是否可见(不可见的弹层跳过,避免误伤收起的菜单)。 */
@@ -630,13 +754,16 @@ window.__ModuleLoader__.load({
630
754
  return { tag: el.tagName, role: el.getAttribute('role') ?? '', cls, text }
631
755
  }
632
756
 
633
- function reportDiagnostics(state) {
757
+ function reportDiagnostics(state, force = false) {
634
758
  if (doctor === null) return
635
- const signature = JSON.stringify(state)
636
- if (signature === reportSignature) return
637
- if (Date.now() - reportAt < 1200) return
759
+ const { at, ...rest } = state
760
+ const signature = JSON.stringify(rest)
761
+ const now = Date.now()
762
+ // 状态没变时:菜单开着也最多 5 秒写一次,避免刷屏
763
+ if (signature === reportSignature && now - reportAt < (force ? 5000 : 1200)) return
764
+ if (now - reportAt < 400) return
638
765
  reportSignature = signature
639
- reportAt = Date.now()
766
+ reportAt = now
640
767
  void doctor.clientReport(state)
641
768
  }
642
769
 
@@ -644,7 +771,65 @@ window.__ModuleLoader__.load({
644
771
  /* 菜单增强主逻辑 */
645
772
  /* ---------------------------------------------------------------- */
646
773
 
647
- /** 兼容路径:dsh-reasoning-effort 的 `.re-model-menu` 结构。 */
774
+ /** 官方模型行的名字:优先 title 属性(官方就是 model.name),再 _modelName 文本。 */
775
+ function officialRowName(row) {
776
+ const title = row.getAttribute('title')
777
+ if (typeof title === 'string' && title.trim().length > 0) return title.trim()
778
+ const nameEl = row.querySelector(`[class*="${OFFICIAL_NAME_SUFFIX}"]`)
779
+ const text = nameEl?.textContent?.trim()
780
+ if (text !== undefined && text.length > 0) return text
781
+ return (row.textContent ?? '').trim()
782
+ }
783
+
784
+ /**
785
+ * 官方路径(首选):直接按官方 DOM 契约定位模型行,不靠文本猜。
786
+ * 行 = `button[role="menuitemradio"]`(类名含 modelOption,或名字能对上目录);
787
+ * 名字取 `title` 属性 → 与权威目录比对得到 provider/model。
788
+ */
789
+ function enhanceOfficial() {
790
+ const rows = Array.from(document.querySelectorAll(OFFICIAL_ROW_SEL)).filter((row) => {
791
+ if (hasClassToken(row, OFFICIAL_OPTION_SUFFIX)) return true
792
+ // 没有 modelOption 类时(契约变化)退一步:名字能对上目录才算模型行,
793
+ // 这样 effort(力度)面板里的 menuitemradio 不会被误加按钮。
794
+ return findModelsByName(officialRowName(row)).length > 0
795
+ })
796
+ if (rows.length === 0) return { rows: 0, wired: 0, samples: [], menuFound: false }
797
+ const samples = []
798
+ let wired = 0
799
+ let unmatched = 0
800
+ for (const row of rows) {
801
+ const name = officialRowName(row)
802
+ const hits = findModelsByName(name)
803
+ if (samples.length < 8) {
804
+ samples.push({ cls: String(row.className ?? '').slice(0, 70), title: name.slice(0, 40), hits: hits.length })
805
+ }
806
+ if (hits.length === 0) {
807
+ unmatched += 1
808
+ continue
809
+ }
810
+ const entry = pickEntry(hits, row)
811
+ const isNew = row.querySelector('.dmd-actions') === null
812
+ rowAnchor.set(row, row.querySelector(`[class*="${OFFICIAL_NAME_SUFFIX}"]`) ?? row)
813
+ enhanceRow(row, entry.providerId, { id: entry.modelId, name: entry.modelName })
814
+ if (isNew) wired += 1
815
+ }
816
+ // 状态条插到菜单顶层(一次),而不是每个供应商分组各插一条
817
+ const menuRoot = rows[0].closest(`[class*="${OFFICIAL_MENU_SUFFIX}"]`)
818
+ ?? rows[0].closest('[role="menu"]')
819
+ ?? rows[0].parentElement
820
+ let hint = ''
821
+ if (directory === null) {
822
+ hint = directoryFailedAt !== 0
823
+ ? `模型医生:目录获取失败 · ${directoryError || '与宿主半通信异常'}`
824
+ : '模型医生:目录获取中…'
825
+ } else if (unmatched > 0) {
826
+ hint = `模型医生:${unmatched} 个模型未匹配上目录`
827
+ }
828
+ ensureAllBar(menuRoot, hint, menuRoot?.firstChild ?? null)
829
+ return { rows: rows.length, wired, samples, unmatched, menuFound: true }
830
+ }
831
+
832
+ /** 官方路径:dsh-reasoning-effort 的 `.re-model-menu` 结构。 */
648
833
  function enhanceLegacy(menu) {
649
834
  let wired = 0
650
835
  const panes = menu.querySelectorAll(PANE_SEL)
@@ -698,56 +883,105 @@ window.__ModuleLoader__.load({
698
883
 
699
884
  /**
700
885
  * 通用路径:任何菜单渲染器(官方原生 / dsh-effort-slider / 其他弹层)。
701
- * 依据「已知模型名」定位行,向上取可点击祖先。
886
+ * 依据「已知模型名」在全文定位行,按容器聚类成菜单,再逐行增强。
702
887
  */
703
888
  function enhanceGeneric(legacyWired) {
704
- const roots = Array.from(document.querySelectorAll(MENU_ROOT_SEL))
705
- .filter((root) => isVisible(root))
706
- .filter((root) => root.closest('.dmd-allbar') === null)
707
- .filter((root) => !root.classList.contains('dmd-allbar'))
708
- const samples = roots.slice(0, 6).map(describe)
889
+ const index = ensureTokenIndex()
890
+ const roots = Array.from(document.querySelectorAll(MENU_ROOT_SEL)).filter((root) => isVisible(root))
891
+ const hits = minimalMatches(document.body)
892
+ const clusters = clusterRows(hits)
709
893
  let wired = 0
710
- const rootStats = []
711
- for (const root of roots) {
712
- const hits = genericRowsIn(root)
713
- rootStats.push({ root: describe(root), rows: hits.length })
714
- if (hits.length < MIN_GENERIC_ROWS) continue
715
- let containerWired = 0
716
- for (const hit of hits) {
894
+ let registered = 0
895
+ let unmatched = 0
896
+ const clusterStats = []
897
+ for (const cluster of clusters) {
898
+ let newlyWired = 0
899
+ for (const hit of cluster.rows) {
717
900
  const entry = pickEntry(hit.entries, hit.row)
718
- if (entry === undefined) continue
901
+ if (entry === undefined) {
902
+ unmatched += 1
903
+ continue
904
+ }
905
+ const isNew = hit.row.querySelector('.dmd-actions') === null
719
906
  rowAnchor.set(hit.row, hit.anchor)
720
907
  enhanceRow(hit.row, entry.providerId, { id: entry.modelId, name: entry.modelName })
721
- containerWired += 1
908
+ registered += 1
909
+ if (isNew) newlyWired += 1
722
910
  }
723
- if (containerWired > 0) {
724
- wired += containerWired
725
- const barHost = hits[0].row.parentElement ?? root
911
+ clusterStats.push({
912
+ host: describe(cluster.host),
913
+ rows: cluster.rows.length,
914
+ wired: newlyWired,
915
+ })
916
+ if (cluster.rows.length >= MIN_GENERIC_ROWS) {
917
+ const first = cluster.rows[0]
726
918
  let hint = ''
727
919
  if (directory === null) {
728
920
  hint = directoryFailedAt !== 0
729
921
  ? `模型医生:目录获取失败 · ${directoryError || '与宿主半通信异常'}`
730
922
  : '模型医生:目录获取中…'
923
+ } else if (newlyWired === 0 && first.row.querySelector('.dmd-actions') === null) {
924
+ hint = '模型医生:模型行未匹配上目录'
731
925
  }
732
- ensureAllBar(barHost, hint, hits[0].row)
926
+ const barHost = first.row.parentElement ?? cluster.host
927
+ ensureAllBar(barHost, hint, first.row)
733
928
  }
734
929
  }
735
- lastDiagnostics = {
736
- at: Date.now(),
737
- mode: legacyWired > 0 ? 'legacy+generic' : 'generic',
738
- tokens: ensureTokenIndex().size,
739
- providerCount: directory?.providers?.length ?? 0,
740
- modelCount: (directory?.providers ?? []).reduce((sum, p) => sum + p.models.length, 0),
930
+ lastGenericStats = {
741
931
  legacyWired,
742
932
  genericWired: wired,
743
- roots: rootStats.slice(0, 8),
744
- samples,
933
+ registered,
934
+ unmatched,
935
+ hitCount: hits.size,
936
+ visibleRoots: roots.length,
937
+ clusters: clusterStats.slice(0, 8),
938
+ samples: roots.slice(0, 5).map(describe),
745
939
  }
746
- return wired
940
+ return registered > 0 ? Math.max(1, wired) : 0
941
+ }
942
+
943
+ /** 菜单结构快照(仅菜单可见时生成,便于远程确认 DOM 契约是否变化)。 */
944
+ function dumpMenu() {
945
+ const menuRoot = document.querySelector(`[class*="${OFFICIAL_MENU_SUFFIX}"]`)
946
+ ?? document.querySelector(MENU_SEL)
947
+ ?? document.querySelector('[role="menu"]')
948
+ if (menuRoot === null || !isVisible(menuRoot)) return null
949
+ const nodes = []
950
+ const walk = (el, depth) => {
951
+ if (nodes.length >= 40 || depth > 4) return
952
+ nodes.push({
953
+ d: depth,
954
+ tag: el.tagName,
955
+ cls: (typeof el.className === 'string' ? el.className : '').slice(0, 70),
956
+ role: el.getAttribute('role') ?? '',
957
+ title: (el.getAttribute('title') ?? '').slice(0, 40),
958
+ text: normText(el.textContent).slice(0, 36),
959
+ })
960
+ for (const child of Array.from(el.children).slice(0, 12)) walk(child, depth + 1)
961
+ }
962
+ walk(menuRoot, 0)
963
+ return { root: describe(menuRoot), nodes }
747
964
  }
748
965
 
749
966
  function enhance() {
750
967
  if (disposed) return
968
+ try {
969
+ enhanceInner()
970
+ } catch (error) {
971
+ // 任何异常都要留痕:否则菜单开着时崩在中间,诊断文件会一直停留在旧状态
972
+ reportDiagnostics({
973
+ at: Date.now(),
974
+ mode: 'error',
975
+ error: error instanceof Error ? error.message : String(error),
976
+ stack: error instanceof Error
977
+ ? String(error.stack ?? '').split('\n').slice(0, 5).join(' | ').slice(0, 500)
978
+ : '',
979
+ href: window.location.href.slice(0, 120),
980
+ }, true)
981
+ }
982
+ }
983
+
984
+ function enhanceInner() {
751
985
  if (directory === null || Date.now() - directoryAt >= DIRECTORY_TTL_MS) {
752
986
  void fetchDirectory()
753
987
  }
@@ -755,18 +989,34 @@ window.__ModuleLoader__.load({
755
989
  rowByModel.clear()
756
990
  rowAnchor = new WeakMap()
757
991
 
992
+ // ① 官方路径(首选,按官方 DOM 契约精确取行)
993
+ const official = enhanceOfficial()
994
+ lastOfficialStats = official
995
+ // ② 兼容路径(dsh-reasoning-effort)
758
996
  const menu = document.querySelector(MENU_SEL)
759
997
  const legacyWired = menu === null ? 0 : enhanceLegacy(menu)
760
- const genericWired = enhanceGeneric(legacyWired)
998
+ // ③ 通用兜底(未知渲染器):前两条都没命中时才跑全文文本聚类
999
+ const genericWired = official.menuFound || legacyWired > 0 ? 0 : enhanceGeneric(legacyWired)
1000
+ if (official.menuFound || legacyWired > 0) lastGenericStats = null
761
1001
 
762
- if (legacyWired + genericWired > 0) paintLastRun('上次结果:')
763
- if (lastDiagnostics !== null) {
764
- reportDiagnostics({
765
- ...lastDiagnostics,
766
- barCount: document.querySelectorAll('.dmd-allbar').length,
767
- actionCount: document.querySelectorAll('.dmd-actions').length,
768
- })
769
- }
1002
+ const wired = official.wired + legacyWired + genericWired
1003
+ if (wired > 0) paintLastRun('上次结果:')
1004
+
1005
+ const menuVisible = official.menuFound || menu !== null
1006
+ reportDiagnostics({
1007
+ at: Date.now(),
1008
+ href: window.location.href.slice(0, 120),
1009
+ tokens: ensureTokenIndex().size,
1010
+ providerCount: directory?.providers?.length ?? 0,
1011
+ modelCount: (directory?.providers ?? []).reduce((sum, p) => sum + p.models.length, 0),
1012
+ directoryError,
1013
+ official: lastOfficialStats,
1014
+ legacyWired,
1015
+ generic: lastGenericStats,
1016
+ menuDump: dumpMenu(),
1017
+ barCount: document.querySelectorAll('.dmd-allbar').length,
1018
+ actionCount: document.querySelectorAll('.dmd-actions').length,
1019
+ }, menuVisible)
770
1020
  }
771
1021
 
772
1022
  function scheduleEnhance() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-model-doctor",
3
- "version": "0.4.0",
4
- "description": "模型医生:在对话输入区的模型选择菜单里,为每个模型选项追加「连通检测」与「删除」按钮——不依赖任何菜单渲染插件(官方原生菜单即可),检测直连各网关探测连通性/欠费/额度,删除直接从 llm-pi-ai 配置移除模型(自动备份配置文档)。",
3
+ "version": "0.5.0",
4
+ "description": "模型医生:在对话输入区的模型选择菜单里,为每个模型选项追加「连通检测」与「删除」按钮——直接适配 DSH 官方模型菜单(按官方 DOM 契约精确定位,不依赖任何菜单渲染插件),检测直连各网关探测连通性/欠费/额度,删除直接从 llm-pi-ai 配置移除模型(自动备份配置文档)。",
5
5
  "keywords": [
6
6
  "deepseek",
7
7
  "deepseek-harness",