@omdp/dsh-key-fallback 1.0.1 → 1.0.3

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.js CHANGED
@@ -14,35 +14,97 @@ window.__ModuleLoader__.load({
14
14
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
15
15
  var React = require('react')
16
16
 
17
- function fetchJson(url) {
18
- return fetch(url).then(function (r) { return r.json() })
17
+ function fetchJson(url, signal) {
18
+ return fetch(url, signal ? { signal: signal } : undefined).then(function (r) { return r.json() })
19
19
  }
20
20
 
21
+ // 卡片可见性驱动的"仅在用户观看时轮询":
22
+ // 不看(页面隐藏 / 卡片滚出视口 / 组件卸载)→ 停止轮询,零网络;
23
+ // 重新可见 → 立即刷新并恢复每 2s 轮询,数据最多晚 2s,正常及时。
21
24
  function KeyFallbackCard() {
22
25
  var s = React.useState({ loading: true, data: null })
23
26
  var state = s[0]
24
27
  var setState = s[1]
28
+ var rootRef = React.useRef(null)
29
+ var ioRef = React.useRef(null)
30
+
31
+ // 回调 ref:卡片根元素变化(loading ↔ 列表)时先解除旧元素再观察新元素,
32
+ // 避免旧元素因被移除触发 isIntersecting:false 而错误停止轮询。
33
+ var attachRoot = React.useCallback(function (el) {
34
+ var prev = rootRef.current
35
+ var io = ioRef.current
36
+ if (io && prev && prev !== el) io.unobserve(prev)
37
+ rootRef.current = el
38
+ if (io && el) io.observe(el)
39
+ }, [])
40
+
25
41
  var refresh = React.useCallback(function () {
26
42
  fetchJson('/dsh-key-fallback/pools')
27
43
  .then(function (data) { setState({ loading: false, data: (data && data.pools) || {} }) })
28
44
  .catch(function () { setState({ loading: false, data: null }) })
29
45
  }, [])
46
+
30
47
  React.useEffect(function () {
48
+ var alive = true
49
+ var timer = null
50
+ var pageVisible = !document.hidden
51
+ var cardVisible = true
52
+
53
+ function stop() {
54
+ if (timer !== null) { clearInterval(timer); timer = null }
55
+ }
56
+ function start() {
57
+ if (!alive || timer !== null || !pageVisible || !cardVisible) return
58
+ refresh()
59
+ timer = setInterval(refresh, 2000)
60
+ }
61
+ function sync() {
62
+ if (!alive) return
63
+ if (pageVisible && cardVisible) start()
64
+ else stop()
65
+ }
66
+ function onVisibility() {
67
+ pageVisible = !document.hidden
68
+ sync()
69
+ }
70
+
71
+ document.addEventListener('visibilitychange', onVisibility)
72
+
73
+ if (typeof IntersectionObserver === 'function') {
74
+ var io = new IntersectionObserver(function (entries) {
75
+ if (!alive) return
76
+ for (var i = 0; i < entries.length; i++) {
77
+ cardVisible = entries[i].isIntersecting
78
+ }
79
+ sync()
80
+ }, { threshold: 0.01 })
81
+ ioRef.current = io
82
+ var el = rootRef.current
83
+ if (el) io.observe(el)
84
+ }
85
+
31
86
  refresh()
32
- var t = setInterval(refresh, 2000)
33
- return function () { clearInterval(t) }
87
+ start()
88
+
89
+ return function () {
90
+ alive = false
91
+ stop()
92
+ document.removeEventListener('visibilitychange', onVisibility)
93
+ if (ioRef.current) { ioRef.current.disconnect(); ioRef.current = null }
94
+ ioRef.current = null
95
+ }
34
96
  }, [refresh])
35
97
 
36
98
  if (state.loading) {
37
- return React.createElement('div', { style: { fontSize: '12px', opacity: 0.6 } }, '加载中…')
99
+ return React.createElement('div', { ref: attachRoot, style: { fontSize: '12px', opacity: 0.6 } }, '加载中…')
38
100
  }
39
101
  var data = state.data || {}
40
102
  var providers = Object.keys(data)
41
103
  if (providers.length === 0) {
42
- return React.createElement('p', { style: { fontSize: '12px', opacity: 0.6, margin: 0 } },
104
+ return React.createElement('p', { ref: attachRoot, style: { fontSize: '12px', opacity: 0.6, margin: 0 } },
43
105
  '未在 settings.yaml 配置 keyFallback.providers')
44
106
  }
45
- return React.createElement('div', { style: { fontSize: '12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
107
+ return React.createElement('div', { ref: attachRoot, style: { fontSize: '12px', display: 'flex', flexDirection: 'column', gap: '8px' } },
46
108
  providers.map(function (p) {
47
109
  var pd = data[p]
48
110
  return React.createElement('div', { key: p, style: { padding: '6px 0', borderTop: '1px solid var(--dsw-alias-border-l2, #eee)' } },
@@ -60,14 +122,24 @@ window.__ModuleLoader__.load({
60
122
  }
61
123
 
62
124
  function apply(ctx) {
63
- var slots = ctx.get('slots')
64
- if (slots === undefined) return
65
- // settings.plugin.item 是 root-scope 的 keyed slot(注册只需 key);本卡是只读状态展示,
66
- // 不需要 settingsScope 绑定可编辑配置。与 resume-stream 的 slots.inject 姿势一致。
67
- slots.inject('settings.plugin.item', () => slots.register({
68
- name: 'settings.plugin.item',
69
- key: 'key-fallback',
70
- }, KeyFallbackCard))
125
+ // settings.plugin.item 是 root-scope 的 keyed slot,按 settings 命名空间
126
+ // 分发:host 端 installSettingsSection 注册 key-fallback 命名空间,使它
127
+ // 进入 served 集合后,本卡片才会被 ConfigurablePluginsTab 渲染。
128
+ // 参考 dshmarket:注册通过嵌套 settingsScope inject(保持模块级无
129
+ // settingsScope 硬依赖——旧宿主没有该服务时不至于拖垮整个插件)。
130
+ var scopedInject = ctx.inject
131
+ if (typeof scopedInject === 'function') {
132
+ scopedInject(['settingsScope'], function (scoped) {
133
+ var scopedSlots = scoped.slots
134
+ if (!scopedSlots) return
135
+ scopedSlots.inject('settings.plugin.item', function () {
136
+ return scopedSlots.register({
137
+ name: 'settings.plugin.item',
138
+ key: 'key-fallback',
139
+ }, KeyFallbackCard)
140
+ })
141
+ })
142
+ }
71
143
  }
72
144
 
73
145
  exports.apply = apply
package/lib/index.js CHANGED
@@ -8,11 +8,22 @@ const { load: loadYaml } = (() => {
8
8
  try { return require('js-yaml') }
9
9
  catch { return { load: (s) => { try { return JSON.parse(s) } catch { return {} } } } }
10
10
  })()
11
+ // dsh-settings 官方助手:installSettingsSection 挂在 scoped fiber 上,注册命名空间
12
+ // 后 client 的 settings.plugin.item 键槽才能把它 serve 并渲染状态卡。
13
+ // 参考 dshmarket(同款姿势,实证可显示)。schemastery 用于构造命名空间 schema。
14
+ const dshSettings = (() => {
15
+ try { return require('@deepseek-ai/dsh-settings') } catch { return null }
16
+ })()
17
+ const zs = (() => {
18
+ try { return require('@deepseek-ai/schemastery') } catch { return null }
19
+ })()
11
20
 
12
21
  const name = 'key-fallback'
13
22
  const inject = ['llm', 'settings', 'webServer']
14
23
  const API_BASE = '/dsh-key-fallback'
15
24
  const SETTINGS_FILE = join(homedir(), '.dsh', 'settings.yaml')
25
+ // 与客户端 slots.register key 保持一致的命名空间名。
26
+ const SETTINGS_NS = 'key-fallback'
16
27
 
17
28
  function mask(key) {
18
29
  if (!key) return ''
@@ -41,6 +52,28 @@ function readConfig() {
41
52
  }
42
53
 
43
54
  function apply(ctx) {
55
+ // 注册 settings 命名空间(官方助手 installSettingsSection,与 dshmarket 同款):
56
+ // 让客户端 Settings → 插件的 keyed slot(settings.plugin.item, key='key-fallback')
57
+ // 被 serve 并渲染状态卡。状态卡只读;轮换逻辑仍直接读 settings.yaml(原设计)。
58
+ if (dshSettings && zs) {
59
+ try {
60
+ const ns = dshSettings.settingsNamespace(SETTINGS_NS)
61
+ const schema = zs.object({
62
+ providers: zs.dict(zs.object({
63
+ env: zs.string(),
64
+ keys: zs.array(zs.string()),
65
+ cooldownMs: zs.natural(),
66
+ })),
67
+ })
68
+ dshSettings.installSettingsSection(ctx, ns, schema, {}, {
69
+ setSource: () => {},
70
+ onChange: () => {},
71
+ })
72
+ } catch (e) {
73
+ // 命名空间注册失败不影响核心轮换逻辑
74
+ }
75
+ }
76
+
44
77
  const pools = new Map()
45
78
  const attempts = new Map() // `${turn}:${step}` -> 已尝试次数(防止死循环)
46
79
  const lifetime = new AbortController()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@omdp/dsh-key-fallback",
3
3
  "description": "Minimal API key fallback for DeepSeek Harness: on a retryable request error, silently retry the same call with the next key in the pool.",
4
- "version": "1.0.1",
4
+ "version": "1.0.3",
5
5
  "type": "commonjs",
6
6
  "main": "lib/index.js",
7
7
  "exports": {