@justsilver/opencode-providers 0.3.3 → 0.3.4

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 CHANGED
@@ -7,6 +7,16 @@
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.4] - 2026-10-09
11
+
12
+ ### Added
13
+
14
+ - 插件:**注册表后台自动刷新**——服务端入口 `setup()` 之后每 **30 分钟**做一次**非 force** 的 TTL 检查(TTL 仍 6h,6h 内只读缓存零网络);**只有内容真变了(`source === "network"`)才** `integration.reload()` + `provider.reload()`,未变不碰 `/models`;上游不可达时保留现有注册、下个 tick 重试(**启动时拉不到也能自行恢复,无需重启**)。定时器 `unref()`,且 `setup()` **返回 cleanup**(热重载 / location 关闭时 `clearInterval`,**不泄漏**);间隔可用 `options.refreshIntervalMs` 覆盖(≤0 禁用)。
15
+
16
+ ### Changed
17
+
18
+ - 技能 CLI:`search` 由「任一 token 命中」(OR)改为**全 token AND**——归一化后查询的**全部**显著 token 都命中才算,**无兜底**(AND 零命中直接「没有匹配」,不回退宽松)。专治系列词跨家族误召:搜 `glm-5.3-flash` 不再因共有 token `flash` 把 `deepseek-v4.1-flash` 一并召回;单个系列词查询(如 `flash`)仍召回全部家族;大小写不敏感。
19
+
10
20
  ## [0.3.3] - 2026-10-08
11
21
 
12
22
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justsilver/opencode-providers",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Register providers missing from the models.dev catalog in OpenCode V2 from a self-hosted registry — no opencode.json needed",
6
6
  "license": "MIT",
@@ -35,6 +35,16 @@ const PLUGIN_ID = "opencode-providers"
35
35
  /** Tags integrations so the TUI entry can recognise its own (TUI side keeps its own copy of this string). */
36
36
  const INTEGRATION_SOURCE = PLUGIN_ID
37
37
 
38
+ /**
39
+ * Background revalidation cadence. `loadRegistry` (no `force`) still gates the
40
+ * network on the 6h TTL; this only decides how often we *check*, so a stale
41
+ * cache is refreshed within one interval of expiring instead of only on the next
42
+ * plugin load (opencode start / plugin install / config reload / service restart).
43
+ * Overridable via `options.refreshIntervalMs` (tests / forks); a non-positive
44
+ * value disables the timer.
45
+ */
46
+ const DEFAULT_REFRESH_INTERVAL_MS = 30 * 60 * 1000
47
+
38
48
  interface IntegrationRefLike {
39
49
  id: string
40
50
  name: string
@@ -92,6 +102,14 @@ export default {
92
102
  // Closure-local: repeated `setup` calls (config hot reload) must not share state.
93
103
  const state: { registry?: Registry } = {}
94
104
 
105
+ // Swap the closure state and re-run the transforms. Registration is a full
106
+ // replace, so entries removed upstream also disappear on the next apply.
107
+ const applyRegistry = async (registry: Registry): Promise<void> => {
108
+ state.registry = registry
109
+ await ctx.integration.reload()
110
+ await ctx.provider.reload()
111
+ }
112
+
95
113
  // Force refresh: bypass the TTL, re-register, and tell the TUI what it got.
96
114
  // A `stale-cache` result means upstream was unreachable, so it counts as a
97
115
  // failure and leaves `state` (and the registered providers) untouched.
@@ -101,9 +119,7 @@ export default {
101
119
  if (result.source === "stale-cache") {
102
120
  return { ok: false, errors: ["registry refresh failed; serving the cached copy"] }
103
121
  }
104
- state.registry = result.registry
105
- await ctx.integration.reload()
106
- await ctx.provider.reload()
122
+ await applyRegistry(result.registry)
107
123
  return {
108
124
  ok: true,
109
125
  providers: Object.keys(result.registry.providers).length,
@@ -180,6 +196,43 @@ export default {
180
196
  })
181
197
  }
182
198
  })
199
+
200
+ // Background revalidation. A non-force load runs the same TTL check the boot
201
+ // path used; only a real content change (`source === "network"`) re-registers,
202
+ // so an unchanged registry never nudges `provider.reload()` and `/models` does
203
+ // not flap. A failed check keeps the current registration and retries next tick,
204
+ // so a registry that was unavailable at boot recovers without a restart.
205
+ const intervalMs =
206
+ typeof ctx.options.refreshIntervalMs === "number" ? ctx.options.refreshIntervalMs : DEFAULT_REFRESH_INTERVAL_MS
207
+ let timer: ReturnType<typeof setInterval> | undefined
208
+ if (intervalMs > 0) {
209
+ let ticking = false
210
+ const tick = async (): Promise<void> => {
211
+ if (ticking) return
212
+ ticking = true
213
+ try {
214
+ const result = await loadRegistry({ url, fetch: globalThis.fetch, store })
215
+ if (!result.ok || result.source !== "network") return
216
+ await applyRegistry(result.registry)
217
+ } catch (error) {
218
+ console.warn(
219
+ `[${PLUGIN_ID}] background registry refresh failed: ${error instanceof Error ? error.message : String(error)}`,
220
+ )
221
+ } finally {
222
+ ticking = false
223
+ }
224
+ }
225
+ timer = setInterval(() => void tick(), intervalMs)
226
+ // Never keep the process alive for this: the service owns an HTTP listener,
227
+ // and an uncleared timer would otherwise hang `node --test`.
228
+ unrefTimer(timer)
229
+ }
230
+
231
+ // Teardown: the host runs this on unload / hot reload / location close, so a
232
+ // reloaded plugin never leaves an orphaned interval behind.
233
+ return () => {
234
+ if (timer !== undefined) clearInterval(timer)
235
+ }
183
236
  },
184
237
  }
185
238
 
@@ -189,6 +242,13 @@ function countModels(registry: Registry): number {
189
242
  return total
190
243
  }
191
244
 
245
+ /** Detach a timer from the event loop where the runtime supports it (Node/Bun). */
246
+ function unrefTimer(timer: unknown): void {
247
+ if (typeof timer !== "object" || timer === null) return
248
+ const unref = (timer as { unref?: unknown }).unref
249
+ if (typeof unref === "function") (unref as () => void).call(timer)
250
+ }
251
+
192
252
  function asCacheEntry(value: unknown): RegistryCacheEntry | undefined {
193
253
  if (typeof value !== "object" || value === null) return undefined
194
254
  const entry = value as Partial<RegistryCacheEntry>