diff --git a/CHANGELOG.md b/CHANGELOG.md index 4427019..e97e316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ > 每个版本一节,**一条一行**,说用户能感知的变化。 > 格式与规矩见 [UPDATING.md](./UPDATING.md)。 +## 0.2.45 — 2026-11-13 + +**修:平台取消基础用户日额度之后,额度卡片整张变错误态** + +- 平台自 2026-11-11 起不再给基础用户发每日赠送额度([官方公告](https://monkeycode.docs.baizhi.cloud/node/01a012e5-d949-76db-96d0-b9cf63283026)): + 钱包把 `daily_token_limit` 与 `daily_token_balance` 都下发 **0**。插件原先只要这两个字段存在就构造 + 「每日额度」数量型窗口,而窗口构造器硬要求 `limit > 0` ⇒ 抛 `QuotaShapeError` ⇒ **整张额度卡片失败** + (连「积分余额」那条都不显示),这个号在智能路由里也失去额度读数 +- 现在 `limit = 0` 时那条退化成文本窗口「平台未发放(基础用户自 2026-11-11 起无每日赠送额度)」, + 积分余额照常显示、照常可路由;**专业版 / 期间版仍有日额度,走原路,不受影响** +- 实测(2026-11-13,国内站 5 个 + 国际站 1 个 Basic 账号):修复前 6 个号 `usage()` 全部抛错, + 修复后 3 个当场读出的是「每日额度 = 文本 + 积分余额 = 37.949 / 71.124 / 340.481」; + 另外 3 个是平台接口偶发 10 秒级超时(那个另算,与本次改动无关) +- **没改的**:模型名后缀仍写「· 每日额度」—— 它说明的是「这条属于订阅档位的模型池、你的账号有没有权限调」, + 不是「你此刻还有免费额度」。实测 basic 账号调 `monkeycode-basic/deepseek-flash` 照样 200, + 但**扣的是积分**(38 token 的调用扣 0.011 积分);README 里把这条说清楚了 + ## 0.2.44 — 2026-11-09 **新增:国内站额度卡片显示「今日消耗」** diff --git a/README.md b/README.md index 7bd9b20..eeb4478 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - **网关请求签名** —— MonkeyCode 的网关要求每个请求带 `X-OhMyAgent-Signature`;插件改过请求体(补 system、钳 max_tokens)之后会重新算签名 - **额度与积分如实显示** —— 每日额度(计费 token)、订阅档位(Basic / Pro / Ultra)、积分余额各一条读数;查不到的显示未知,不编造 - **额度用完后默认走积分** —— 官方前端的账号级开关「额度耗尽后消耗积分」默认是开的;插件在目录里把两种计费方式标清楚(见「模型」) +- **「基础用户没有日额度」如实说** —— 平台自 2026-11-11 起取消基础用户的每日赠送额度([官方公告](https://monkeycode.docs.baizhi.cloud/node/01a012e5-d949-76db-96d0-b9cf63283026)):这类账号的「每日额度」那条写「平台未发放」,计费全部走积分;专业版 / 期间版仍有额度,那条照常给占比 - **每日签到** —— MonkeyCode 两站支持:自动解开厂商的 PoW 验证题后签到,今天签过就不发写请求 - **多账号按序兜底** —— 勾几个账号就排几个号,前一个额度用完自动换下一个 - **模型目录跟随平台** —— 登录后跟随平台上下架;接口抖动时退回上次快照,不会把列表清空 @@ -131,10 +132,13 @@ | 后缀 | 含义 | 上游 id 形态 | |---|---|---| -| `· 每日额度` | 走订阅档位的每日 token 额度(basic / pro / ultra 三档按权限放开) | `monkeycode-basic/…`、`monkeycode-pro/…`、`monkeycode-ultra/…` | +| `· 每日额度` | 这条属于订阅档位的模型池(basic / pro / ultra 三档按权限放开)。**有日额度时才走额度**:平台自 2026-11-11 起不再给基础用户发每日赠送额度,basic 账号调这些条目**直接扣积分** | `monkeycode-basic/…`、`monkeycode-pro/…`、`monkeycode-ultra/…` | | `· 积分` | 直接扣积分 | 不带前缀 | -- **每日额度用完之后,还是这条模型继续扣积分** —— 不会"跳到另一条模型"上。后缀只说明这条模型**主要**走哪种计费, +- **后缀说的是「这条属于哪个池」,不是「你此刻还有免费额度」**:它同时决定你的账号有没有权限调这条 + (basic 调 pro / ultra 前缀会被上游 403)。基础用户现在一律按积分计费,所以两条后缀在**扣费上没有区别** —— + 实际有没有日额度看卡片上「每日额度」那一条,它写「平台未发放」就是没有 +- **有日额度时(专业版 / 期间版),额度用完之后还是这条模型继续扣积分** —— 不会"跳到另一条模型"上。 账号级开关「额度耗尽后消耗积分」默认就是开的(可在网页控制台关掉) - **每日额度与积分是同一个账号的两条读数**,不是三个独立额度池:三档的差别是**能调哪些模型**,额度只有一个当日读数 - **积分孪生默认隐藏**:同一个模型如果既有档位条目又有积分条目,列表里只留档位那条(它额度用完会自动转积分,能力是超集)。 @@ -241,6 +245,11 @@ magpie plugin list **为什么看不到 pro / ultra 档的模型?** 这些模型需要对应订阅。插件按账号当前档位过滤目录,basic 账号不会看到没有权限的 pro / ultra 条目;这不是列表坏了,也不存在一个叫「已锁定」的标记。 +**额度条写着「平台未发放(基础用户自 2026-11-11 起无每日赠送额度)」?** 这是平台的政策,不是插件读不到: +官方自 2026-11-11 起取消基础用户的每日赠送额度([公告](https://monkeycode.docs.baizhi.cloud/node/01a012e5-d949-76db-96d0-b9cf63283026)), +钱包里的 `daily_token_limit` 与 `daily_token_balance` 从此下发 **0**。这类账号只剩「积分余额」一条读数, +计费全部走积分(签到仍然每天 +100 积分)。专业版 / 期间版账号不受影响,额度那条照常显示占比。 + **额度条显示「剩余额度未知」?** 平台接口暂时读不到(网络抖动或未登录态查额度)。推理不受影响,下一轮刷新会恢复。 --- diff --git a/package.json b/package.json index 5c0dac0..be961c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "magpie-monkeycode", - "version": "0.2.44", + "version": "0.2.45", "description": "MonkeyCode 国内站 / 国际站 + 百智云 magpie 插件:百智云「本机导入」「浏览器登录」成功后自动开通并写入国内 MonkeyCode(后台执行、失败不影响百智云登录、按 uid 去重);百智云只保留这两条登录路,国内站只认百智云身份,国际站独立(API Key / 账号密码 / 本机导入 / 浏览器);支持双方邀请码、网关签名与「每日额度 / 积分」计费标识。", "type": "module", "main": "./index.mjs", diff --git a/registry-entry.json b/registry-entry.json index 42e5feb..c896d70 100644 --- a/registry-entry.json +++ b/registry-entry.json @@ -7,7 +7,7 @@ "baizhi" ], "summary": { - "en": "Signs in to MonkeyCode (China and international) and BaiZhiCloud and serves their models to every agent magpie runs. After a successful BaiZhiCloud desktop import or browser sign-in it provisions the China-side MonkeyCode account in the background — no second click; a background failure never fails the BaiZhiCloud sign-in; deduplicated by uid, verified by read-back, and nothing is written when the auth store cannot be read. BaiZhiCloud keeps only those two sign-in routes, the China site accepts only your BaiZhiCloud identity, and the international site stays independent (API key, password, desktop import, browser). Optional invite codes for both sites, gateway request signing, and per-model “daily quota / credits” billing labels. MonkeyCode supports daily check-in; BaiZhiCloud uses pay-as-you-go credits and has no daily token pool.", - "zh": "签入 MonkeyCode 国内站、国际站与百智云,把它们的模型提供给 magpie 里的每个 agent。「从本机客户端导入」或「浏览器登录」成功后,会在后台用同一个百智云身份自动开通并写入国内 MonkeyCode —— 不用点第二次;后台失败不影响百智云登录;按 uid 去重、写入后回读确认、账号库读不出来就一个字节都不写。百智云只保留这两条登录路,国内站只认百智云身份,国际站保持独立(API Key / 账号密码 / 本机导入 / 浏览器)。支持双方的邀请码、网关请求签名,以及按模型标注「每日额度 / 积分」的计费口径。MonkeyCode 两站支持每日签到;百智云按量扣积分,没有每日 token 池。" + "en": "Signs in to MonkeyCode (China and international) and BaiZhiCloud and serves their models to every agent magpie runs. After a successful BaiZhiCloud desktop import or browser sign-in it provisions the China-side MonkeyCode account in the background — no second click; a background failure never fails the BaiZhiCloud sign-in; deduplicated by uid, verified by read-back, and nothing is written when the auth store cannot be read. BaiZhiCloud keeps only those two sign-in routes, the China site accepts only your BaiZhiCloud identity, and the international site stays independent (API key, password, desktop import, browser). Optional invite codes for both sites, gateway request signing, and per-model “daily quota / credits” billing labels. MonkeyCode supports daily check-in; BaiZhiCloud uses pay-as-you-go credits and has no daily token pool. Since 2026-11-11 the platform grants Basic users no daily free quota: those accounts are shown as “not issued by the platform” and bill entirely from credits, while Pro and period plans keep their daily quota.", + "zh": "签入 MonkeyCode 国内站、国际站与百智云,把它们的模型提供给 magpie 里的每个 agent。「从本机客户端导入」或「浏览器登录」成功后,会在后台用同一个百智云身份自动开通并写入国内 MonkeyCode —— 不用点第二次;后台失败不影响百智云登录;按 uid 去重、写入后回读确认、账号库读不出来就一个字节都不写。百智云只保留这两条登录路,国内站只认百智云身份,国际站保持独立(API Key / 账号密码 / 本机导入 / 浏览器)。支持双方的邀请码、网关请求签名,以及按模型标注「每日额度 / 积分」的计费口径。MonkeyCode 两站支持每日签到;百智云按量扣积分,没有每日 token 池。平台自 2026-11-11 起不再给基础用户发每日赠送额度:这类账号如实显示「平台未发放」并全部按积分计费,专业版 / 期间版额度不受影响。" } } diff --git a/src/monkeycode/runtime.mjs b/src/monkeycode/runtime.mjs index 795f643..c32cb90 100644 --- a/src/monkeycode/runtime.mjs +++ b/src/monkeycode/runtime.mjs @@ -247,8 +247,13 @@ function credentialOf(auth) { return isUsableCredential(credential) ? credential : undefined } -/** 读一次额度与订阅,拼成 magpie 的 usage 形状。 */ -async function readUsage(site, credential, fetchImpl) { +/** 读一次额度与订阅,拼成 magpie 的 usage 形状。 + * + * `options.now` 只是给单测冻时间用的透传口(「今日消耗」按本地日历日算, + * 不注入就只能拿真时钟比 —— 固定日期的 fixture 一旦过了那一天就恒为 0, + * 而这两个用例又会以「读数坏了」的样子红掉)。生产路径不传,行为与以前一致。 + */ +async function readUsage(site, credential, fetchImpl, { now } = {}) { const cookie = cookieOf(credential, site.sessionCookie) if (cookie === undefined) { return { @@ -263,20 +268,39 @@ async function readUsage(site, credential, fetchImpl) { // 两者都不会抛;今日消耗再兜一层 ok:false,失败就当没有这条窗口。 const [plan, todaySpend] = await Promise.all([ fetchPlan(cookie, site, { fetchImpl }), - fetchTodaySpend(cookie, site, { fetchImpl }).catch(() => ({ ok: false })), + fetchTodaySpend(cookie, site, { fetchImpl, ...(now === undefined ? {} : { now }) }).catch(() => ({ ok: false })), ]) const windows = [] // 每日额度:数量型窗口。span = 一天(上游不下发重置时刻,编一个 resetsAt 就是编造)。 // 只有上限与剩余都有才画(缺剩余时把 0 当剩余会让用户以为今天已用光)。 + // + // ⚠ 2026-11-11 起平台取消**基础用户**的每日赠送额度:Basic 账号的钱包把 + // `daily_token_limit` 与 `daily_token_balance` **都下发 0**(官方公告 + // https://monkeycode.docs.baizhi.cloud/node/01a012e5-d949-76db-96d0-b9cf63283026, + // 2026-11-13 在 6 个 Basic 账号上实测确认,国内站与国际站都已生效)。 + // 而 `usageWindow` 的数量型硬要求 `limit > 0` —— 拿 0 去建窗口会抛 QuotaShapeError, + // 把**整张额度卡片**(连积分余额那条)一起弄坏,这个号在路由里也失去额度读数。 + // 所以 `limit = 0` 时退化成文本型(`specs/quota.md` §10.2 的原话),说清「平台没发」, + // 而不是把这条悄悄抹掉 —— 那条不见了,用户会以为插件坏了。 + // pro / ultra / 期间版账号按公告仍有日额度,`limit > 0` 走原路,不受影响。 if (wallet.dailyTokenLimit !== undefined && wallet.dailyTokenBalance !== undefined) { - windows.push(usageWindow({ - name: "每日额度", - amount: Math.max(0, wallet.dailyTokenLimit - wallet.dailyTokenBalance), - limit: wallet.dailyTokenLimit, - unit: "计费 token", - span: 86_400, - })) + if (wallet.dailyTokenLimit > 0) { + windows.push(usageWindow({ + name: "每日额度", + amount: Math.max(0, wallet.dailyTokenLimit - wallet.dailyTokenBalance), + limit: wallet.dailyTokenLimit, + unit: "计费 token", + span: 86_400, + })) + } else { + // textOnly 的窗口路由不计入(它本来就没有占比);可路由的那条是下面的「积分余额」。 + windows.push(usageWindow({ + name: "每日额度", + textOnly: true, + display: "平台未发放(基础用户自 2026-11-11 起无每日赠送额度)", + })) + } } // 积分是钱包型:只给余额,没有上限(上游不给总额,给分母就是假占比)。 // 真机见过负余额(-8.191)—— 负数不能直接当进度条读,改成 0 + 明文字。 diff --git a/tests/monkeycode.test.mjs b/tests/monkeycode.test.mjs index 4c4604b..67eabb5 100644 --- a/tests/monkeycode.test.mjs +++ b/tests/monkeycode.test.mjs @@ -166,6 +166,47 @@ test("额度:钱包型只给余额,数量型给 amount/limit,未配 cookie assert.equal(noCookie.windows.length, 0) }) +test("额度:平台取消基础用户日额度(daily_token_limit = 0)不抛错,退化成文本窗口", async () => { + const plugin = await makePlugin(site)({ client: {} }) + const auth = { type: "api", key: JSON.stringify({ ...credential, session_cookie: "sess", user_id: "u0" }) } + + const original = globalThis.fetch + globalThis.fetch = async (url) => { + const text = String(url) + if (text.endsWith("/api/v1/users/wallet")) { + // 2026-11-11 起基础用户不再有每日赠送额度:两个字段都下发 0(实测形态)。 + return Response.json({ code: 0, data: { balance: 37949, daily_token_balance: 0, daily_token_limit: 0 } }) + } + if (text.endsWith("/api/v1/users/subscription")) { + return Response.json({ code: 0, data: { plan: "basic" } }) + } + throw new Error(`unexpected ${text}`) + } + try { + const usage = await plugin.auth.usage(async () => auth) + // 判据:limit=0 不能把整张卡片变成错误态(原先抛 QuotaShapeError,连积分那条也一起没了)。 + assert.equal(usage.error, undefined) + assert.equal(usage.plan, "MonkeyCode Basic") + + const daily = usage.windows.find((w) => w.name === "每日额度") + assert.ok(daily, "额度那条要在 —— 悄悄抹掉用户会以为插件坏了") + assert.equal(daily.textOnly, true, "limit=0 时不能建数量型窗口(usageWindow 硬要求 limit > 0)") + assert.equal(daily.limit, undefined) + assert.match(daily.display, /基础用户/) + + const credit = usage.windows.find((w) => w.name === "积分余额") + assert.equal(credit.balance, 37.949, "积分余额照常给(钱包单位是千分之一积分)") + + // 至少一条可路由窗口:钱包型的积分窗口参与路由,这个号不能在智能路由里消失。 + assert.ok( + usage.windows.some((w) => w.aside !== true && (w.used !== undefined || w.balance !== undefined)), + "必须留一条可路由窗口", + ) + } finally { + globalThis.fetch = original + } +}) + // ── loader ─────────────────────────────────────────────────────────── test("loader:签名基于最终 body,且不把 SDK 的 x-api-key 带出去", async () => { diff --git a/tests/today-spend.test.mjs b/tests/today-spend.test.mjs index 549d586..3362f81 100644 --- a/tests/today-spend.test.mjs +++ b/tests/today-spend.test.mjs @@ -230,7 +230,7 @@ test("readUsage:成功时在最后追加 textOnly 的「今日消耗」,没 const { fetchImpl } = platformFetch({ pages: [pageBody([record(TODAY + 3600, 98_914, "model_consumption")])], }) - const usage = await readUsage(site, credential, fetchImpl) + const usage = await readUsage(site, credential, fetchImpl, { now }) assert.equal(usage.error, undefined) const today = usage.windows.find((w) => w.name === "今日消耗") assert.ok(today, "读成功就必须有「今日消耗」") @@ -251,10 +251,10 @@ test("readUsage:数字格式与百智云一致(98.79 积分 / 36 次调用 ...Array.from({ length: 35 }, (_, index) => record(TODAY + 100 + index, 2_744)), record(TODAY + 200, 2_750), ] - const ok = await readUsage(site, credential, platformFetch({ pages: [pageBody(transactions)] }).fetchImpl) + const ok = await readUsage(site, credential, platformFetch({ pages: [pageBody(transactions)] }).fetchImpl, { now }) assert.equal(ok.windows.find((w) => w.name === "今日消耗").display, "98.79 积分 / 36 次调用") - const zero = await readUsage(site, credential, platformFetch({ pages: [pageBody([])] }).fetchImpl) + const zero = await readUsage(site, credential, platformFetch({ pages: [pageBody([])] }).fetchImpl, { now }) // 额度内不产生扣费流水:说清楚「没扣」,而不是「0 积分」那种看着像读数坏了的样子。 assert.equal(zero.windows.find((w) => w.name === "今日消耗").display, "今日未扣积分(额度内)") })