diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a69558..948368d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ > 每个版本一节,**一条一行**,说用户能感知的变化。 > 格式与规矩见 [UPDATING.md](./UPDATING.md)。 +## 0.2.40 — 2026-11-04 + +**修「收窄两分钟就被打回去」:缓存判过期还在看 `usable`,等于每次 usage 都重拉** + +- 0.2.39 之后真机看到:手工收窄到 2 条,两分钟后缓存又变回 4 条(未收窄) +- 根因:`isStale()` 里还留着「每个模型必须有 `usable` 布尔」那条判据 —— 那是探测时代的规则,探测已经默认关闭、新缓存根本不写 `usable`,于是**每一次** usage 都判定过期、每次都在后台重拉一遍,把收窄结果覆盖回「四类混列」。现在只看时间(TTL 6 小时) +- 顺带把档位**落盘**:`writeCatalog(..., { tier })` 写 `tier`,`readCatalog` 读回来,`configModels` 读缓存时按它再筛一遍(幂等),`refreshCatalog` 没传 tier 时沿用旧值 —— 三道保险,任何一条路径都不会把 pro / ultra 放回来 + ## 0.2.39 — 2026-11-04 **修「模型全都不通」:magpie 发的是它自己的模型 id,上游只认带档位前缀的原值** diff --git a/package.json b/package.json index 17b3ab8..8c68a41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "magpie-monkeycode", - "version": "0.2.39", + "version": "0.2.40", "description": "MonkeyCode 国内站 / 国际站 + 百智云 magpie 插件:三家独立供应商,浏览器登录或 API Key 接入,网关签名与积分额度。", "type": "module", "main": "./index.mjs", diff --git a/src/monkeycode/catalog.mjs b/src/monkeycode/catalog.mjs index 210c5d6..3988976 100644 --- a/src/monkeycode/catalog.mjs +++ b/src/monkeycode/catalog.mjs @@ -193,12 +193,13 @@ export async function fetchCatalog(cookie, site, { fetchImpl = fetch, timeoutMs return { ok: true, models } } -/** 写缓存。 */ -export function writeCatalog(providerId, models) { +/** 写缓存。`tier` 一并落盘:读的时候按它再筛一遍,刷新时也沿用它。 */ +export function writeCatalog(providerId, models, { tier } = {}) { const file = catalogPath(providerId) try { mkdirSync(join(file, ".."), { recursive: true }) - writeFileSync(file, `${JSON.stringify({ fetchedAt: Date.now(), models }, null, 2)}\n`) + const payload = { fetchedAt: Date.now(), models, ...(tier === undefined ? {} : { tier }) } + writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`) return true } catch { return false @@ -210,21 +211,28 @@ export function readCatalog(providerId) { try { const parsed = JSON.parse(readFileSync(catalogPath(providerId), "utf8")) if (!Array.isArray(parsed?.models) || parsed.models.length === 0) return undefined - return { fetchedAt: Number(parsed.fetchedAt) || 0, models: parsed.models } + return { + fetchedAt: Number(parsed.fetchedAt) || 0, + models: parsed.models, + // 落盘时用的档位。读的时候再按它筛一遍(幂等),这样即使某次刷新没传 + // tier,也不会把 pro / ultra 又放回来。 + ...(typeof parsed.tier === "string" ? { tier: parsed.tier } : {}), + } } catch { return undefined } } /** - * 缓存是否过期。 + * 缓存是否过期:只看时间。 * - * 除了时间,还看**探测过没有**:老缓存(0.2.13 之前写的)没有 `usable` 字段, - * 直接拿来用会把「点哪个都 403」的模型全列出来 —— 那种缓存视为过期,重探一次。 + * ⚠ 别再拿「有没有 `usable` 字段」当判据(0.2.39 之前是)。探测已经默认关闭, + * 新缓存根本不写 `usable` —— 那个条件会让**每一次** usage 都判定过期、每次都在 + * 后台重拉一遍,刚写好的收窄结果转眼被覆盖回「四类混列」(2026-11-04 真机: + * 手工收窄到 2 条,两分钟后缓存又变回 4 条)。 */ export function isStale(catalog, now = Date.now()) { if (catalog === undefined) return true - if (!catalog.models.every((model) => typeof model.usable === "boolean")) return true return now - catalog.fetchedAt > CATALOG_TTL_MS } @@ -342,11 +350,12 @@ export function filterByTier(models, tier) { export async function refreshCatalog(cookie, site, options = {}) { const result = await fetchCatalog(cookie, site, options) if (!result.ok) return false - const narrowed = filterByTier(result.models, options.tier) + const tier = options.tier ?? readCatalog(site.id)?.tier + const narrowed = filterByTier(result.models, tier) const models = options.probe === true && options.credential !== undefined ? await probeCatalog(options.credential, site, narrowed, options) : narrowed - return writeCatalog(site.id, models) + return writeCatalog(site.id, models, { tier }) } /** @@ -368,6 +377,6 @@ export async function warmCatalog(cookie, site, { fetchImpl = fetch, timeoutMs = const result = await fetchCatalog(cookie, site, { fetchImpl, timeoutMs }) if (!result.ok) return { ok: false, error: result.error } const models = filterByTier(result.models, tier) - const written = writeCatalog(site.id, models) + const written = writeCatalog(site.id, models, { tier }) return written ? { ok: true, count: models.length } : { ok: false, error: "目录缓存写不进去" } } diff --git a/src/monkeycode/runtime.mjs b/src/monkeycode/runtime.mjs index dfbcdcf..d6d60cb 100644 --- a/src/monkeycode/runtime.mjs +++ b/src/monkeycode/runtime.mjs @@ -26,7 +26,7 @@ import { profileDirFor, readCookiesSilently, startBrowserLogin } from "../sessio import { signInWithBaizhiSession } from "./sso-import.mjs" import { readDesktopMonkeyCodeSession } from "./desktop-session.mjs" import { loginWithPassword } from "./password-login.mjs" -import { KNOWN_CONTEXT, isStale, prettyModelName, readCatalog, refreshCatalog, warmCatalog } from "./catalog.mjs" +import { filterByTier, KNOWN_CONTEXT, isStale, prettyModelName, readCatalog, refreshCatalog, warmCatalog } from "./catalog.mjs" import { AGREE_PROTOCOL_JS } from "../baizhi/runtime.mjs" /** @@ -90,10 +90,11 @@ function configModels(site) { const cached = readCatalog(site.id) if (cached !== undefined && cached.models.length > 0) { const out = {} - // 平台目录给的就是「这个账号能调什么」(按权限 / 订阅过滤过的),原样列出。 - // 不再拿 `usable` 二次筛选:那是 `max_tokens: 1` 的探测结论,会误杀慢的 / - // 被限流的模型,也会让账号看起来「模型怎么这么少」。 - for (const model of cached.models) { + // 平台目录给的就是「这个账号能调什么」(按权限 / 订阅过滤过的)。 + // 落盘时按订阅档位收窄过,这里再按同一条规则筛一遍(幂等):老缓存没存 tier + // 就原样列出,不会因为某次刷新没带档位而把 pro / ultra 放回来。 + const visible = cached.tier === undefined ? cached.models : filterByTier(cached.models, cached.tier) + for (const model of visible) { out[model.id] = { id: model.id, // 显示名在读取时算:老缓存里没有 displayName 也能立刻变好看,不用重拉目录。