Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,上游只认带档位前缀的原值**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
31 changes: 20 additions & 11 deletions src/monkeycode/catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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 })
}

/**
Expand All @@ -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: "目录缓存写不进去" }
}
11 changes: 6 additions & 5 deletions src/monkeycode/runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"

/**
Expand Down Expand Up @@ -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 也能立刻变好看,不用重拉目录。
Expand Down
Loading