Skip to content
Open
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
> 每个版本一节,**一条一行**,说用户能感知的变化。
> 格式与规矩见 [UPDATING.md](./UPDATING.md)。

## 0.2.45 — 2026-11-13

**修:基础会员没有日额度时额度卡片整张报错「暂时无法获取额度」**

- 平台套餐调整:基础会员改为永久免费、**不赠送每日额度**(官方前端
`dailyQuotaBasic: "不赠送额度"`,Pro / Ultra 仍是 100M / 300M 每天);这类账号钱包里
`daily_token_limit` / `daily_token_balance` 恒为 0,插件照旧画「每日额度」窗口,被额度构造器判
`limit > 0` 不成立而抛错,连「积分余额 / 今日消耗」一起看不到
- 现在上限为 0 时不画这条窗口:卡片照常显示积分余额与今日消耗
- 模型后缀随套餐改名:`monkeycode-basic/…` 标 `· 免费`(平台叫「免费使用」),
`monkeycode-pro` / `monkeycode-ultra` 才是 `· 每日额度`;旧缓存读出即重算,不用重拉目录
- 档位反推表只保留 `pro: 100M / ultra: 300M`:基础会员日额度为 0,不能再靠钱包数值反推档位
- 回归测试锁住 0/0 形态不再冒成 `usage.error`,并锁住 0/0 账号仍能读额度

## 0.2.44 — 2026-11-09

**新增:国内站额度卡片显示「今日消耗」**
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,13 @@

| 后缀 | 含义 | 上游 id 形态 |
|---|---|---|
| `· 每日额度` | 走订阅档位的每日 token 额度(basic / pro / ultra 三档按权限放开) | `monkeycode-basic/…`、`monkeycode-pro/…`、`monkeycode-ultra/…` |
| `· 免费` | 基础会员免费档可用的模型(basic 模型;2026-11-13 起基础会员不再赠送日额度) | `monkeycode-basic/…` |
| `· 每日额度` | 走订阅档位的每日 token 额度(pro / ultra 按权限放开) | `monkeycode-pro/…`、`monkeycode-ultra/…` |
| `· 积分` | 直接扣积分 | 不带前缀 |

- **每日额度用完之后,还是这条模型继续扣积分** —— 不会"跳到另一条模型"上。后缀只说明这条模型**主要**走哪种计费,
账号级开关「额度耗尽后消耗积分」默认就是开的(可在网页控制台关掉)
- **每日额度与积分是同一个账号的两条读数**,不是三个独立额度池:三档的差别是**能调哪些模型**,额度只有一个当日读数
- **每日额度与积分是同一个账号的两条读数**,不是三个独立额度池:档位的差别是**能调哪些模型**,额度只有一个当日读数
- **积分孪生默认隐藏**:同一个模型如果既有档位条目又有积分条目,列表里只留档位那条(它额度用完会自动转积分,能力是超集)。
怀疑两档其实不是同一权重时,设 `MAGPIE_MONKEYCODE_SHOW_CREDIT_TWINS=1` 让两条都出来
- **上下文窗口只声明有实测证据的**:DeepSeek 系 1,048,576(服务端 400 原文);没证据的(如 `glm-5`)不声明 ——
Expand All @@ -153,7 +154,8 @@

| 供应商 | 代表模型 | 备注 |
|---|---|---|
| `monkeycode` | `DeepSeek-Flash · 每日额度` / `GLM-5.3-Flash · 每日额度` | 档位条目按订阅权限放开;basic 调 pro/ultra 前缀会被上游 403 |
| `monkeycode` | `DeepSeek-Flash · 免费` / `GLM-5.3-Flash · 免费` | 基础会员免费档可用;pro / ultra 前缀按订阅权限放开,basic 调会被上游 403 |
| `monkeycode` | `DeepSeek-Flash · 每日额度` / `HY3 · 每日额度` | 走 pro / ultra 的每日 token 额度 |
| `monkeycode` | `DeepSeek-Flash · 积分` / `GLM-5 · 积分` | 按积分计费;有档位孪生的会被隐藏 |
| `baizhi` | `deepseek-v4-pro`、`deepseek-v4-flash` | 按量扣积分,1,048,576 上下文 |
| `baizhi` | `glm-5` / `kimi-k2.6` / `qwen3.5-plus` 等 | 未知能力的模型照常列出,但不声明窗口 / 图片 |
Expand Down Expand Up @@ -236,9 +238,13 @@ magpie plugin list
**国际版点网页登录,界面报「已登录,但 magpie 无法列出它」?** 0.2.36 已修。根因不在国际站:magpie 的插件宿主只在登录结果带
`refresh` 字段时才把 oauth 登录写进 `plugin-auth.json`,缺这一格时界面照旧显示成功、账号却没落盘。升级后重登一次即可。

**为什么模型列表里少了一条「· 积分」的?** 它和某个「· 每日额度」条目是同一个模型,默认只留档位那条。
**为什么模型列表里少了一条「· 积分」的?** 它和某个「· 免费」/「· 每日额度」条目是同一个模型,默认只留档位那条。
想两条都看:设环境变量 `MAGPIE_MONKEYCODE_SHOW_CREDIT_TWINS=1` 再刷新。

**为什么基础会员没有「每日额度」这条读数?** 2026-11-13 平台调整了套餐:基础会员改为永久免费、**不赠送每日额度**
(钱包里 `daily_token_limit` 恒为 0),Pro / Ultra 分别是每天 100M / 300M Token。基础会员的卡片只剩「积分余额 / 今日消耗」,
这是套餐本身,不是读取失败。

**为什么看不到 pro / ultra 档的模型?** 这些模型需要对应订阅。插件按账号当前档位过滤目录,basic 账号不会看到没有权限的 pro / ultra 条目;这不是列表坏了,也不存在一个叫「已锁定」的标记。

**额度条显示「剩余额度未知」?** 平台接口暂时读不到(网络抖动或未登录态查额度)。推理不受影响,下一轮刷新会恢复。
Expand Down
9 changes: 6 additions & 3 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,9 @@ oma_… 裸 token(缺密钥,请

三个细节:

- **每日额度只有上限与剩余都有才画**。缺剩余时把 0 当剩余,会让用户以为今天已经用光。
- **每日额度只有上限与剩余都有、且上限 > 0 才画**。缺剩余时把 0 当剩余,会让用户以为今天已经用光;
上限为 0 也不画——2026-11-13 起基础会员(免费档)钱包恒为 0/0,单独画一条 `limit: 0` 的窗口会被额度契约判不合法,
整张卡片会变成「暂时无法获取额度」(Pro / Ultra 仍是 100M / 300M 每天)。
- **负余额**(真机见过 −8.191)不能直接喂进度条:改成 `balance: 0` + `display: "-8.191 积分(已透支)"`。
- `signIn` 不因 cookie 401 写 `expired`:额度走 cookie、推理走 `oma_`,cookie 失效不影响发消息;标 expired 会把还能用的号判死。

Expand Down Expand Up @@ -457,11 +459,12 @@ POST /api/v1/users/wallet/checkin {captcha_token}

### 显示名:一眼看出按什么计费

`prettyModelName()` 给每条模型加后缀(2026-11-09 起):
`prettyModelName()` 给每条模型加后缀(2026-11-09 起;2026-11-13 按新套餐调整 basic):

| 上游 `model` | 卡片显示名 |
|---|---|
| `monkeycode-basic/deepseek-flash`、`monkeycode-pro/…`、`monkeycode-ultra/…` | `DeepSeek-Flash · 每日额度`(三档同名,差别在权限) |
| `monkeycode-basic/…` | `DeepSeek-Flash · 免费`(基础会员免费档,不赠送日额度) |
| `monkeycode-pro/…`、`monkeycode-ultra/…` | `HY3 · 每日额度`(走 100M / 300M 的当日额度) |
| `deepseek-flash`(无前缀) | `DeepSeek-Flash · 积分` |

`readCatalog()` 读出时会用当前的 `prettyModelName(apiId ?? name)` 重算 `displayName` ——
Expand Down
19 changes: 13 additions & 6 deletions docs/monkeycode-model-billing.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,13 @@ featureValues:{ … dailyQuotaBasic:"每天 10M Token", dailyQuotaPro:"每天 10
features:{ … dailyQuotaBasic:"每日额度:每天 10M Token", dailyQuotaPro:"每日额度:每天 100M Token", dailyQuotaUltra:"每日额度:每天 300M Token" … }
```

与本地常量 **完全一致**:`src/monkeycode/credits.mjs:33`
`PLAN_DAILY_LIMIT = { basic: 10_000_000, pro: 100_000_000, ultra: 300_000_000 }`。
(`resolveTier()` 正是用这个等式反推档位,见 `credits.mjs:179-188` —— 这条交叉验证成立。)
与本地常量 **完全一致**:`src/monkeycode/credits.mjs`

> **2026-11-13 更正**:平台已改套餐 —— 基础会员(`basic`)变成永久免费、**不赠送每日额度**
> (前端 `dailyQuotaBasic: "不赠送额度"`、`status: "unsupported"`;真机 6 个 basic 账号钱包
> `daily_token_limit` / `daily_token_balance` 全部为 0),只有 Pro 100M / Ultra 300M 仍成立。
> 因此 `PLAN_DAILY_LIMIT` 现在只有 `{ pro, ultra }`,`basic` 只能由 `/users/subscription`
> 的 `plan` 字段判定,不能再靠日额度反推。A5 的原文保留为 11-09 的取证快照。

### A6 · 档位与权限**只由 `model` 字符串前缀决定**(平台前端自己就是这么判的)

Expand Down Expand Up @@ -357,9 +361,12 @@ function Xm(e){ const t=e?.remark?.trim(); return t ? jR(t) : Xc(e?.model) }

### 4.5 显示名契约

1. **档位条目**:`<品牌>-<版本> · 每日额度`(把 `基础档/专业档/旗舰档` 换掉:
用户要判断的是「这条吃不吃日额度」,档位名由账号自己决定,写进名字反而每次都要解释)。
若想保留档位信息,可写 `· 每日额度(基础)`,但**必须**先出现「每日额度」。
> **2026-11-13 更新(覆盖下面第 1 条)**:`basic` 前缀改标 `· 免费` —— 基础会员不再赠送日额度,
> 平台模型选择器里 basic 模型标的就是「免费使用」;`· 每日额度` 只留给 `monkeycode-pro` /
> `monkeycode-ultra`。§4.4 表里的 `· 每日额度` 是 11-09 的取证快照,不再作为现行契约。

1. ~~**档位条目**:`<品牌>-<版本> · 每日额度`~~ → 现在只有 pro / ultra 标 `· 每日额度`,
basic 标 `· 免费`。
2. **无前缀条目**:`<品牌>-<版本> · 积分`(现状正确,不动)。
3. **转积分的提示不要塞进模型名**:它是**账号级状态**,读
`GET /api/v1/users/subscription` 的 `enable_credit_consumption`(A4),
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.44",
"version": "0.2.45",
"description": "MonkeyCode 国内站 / 国际站 + 百智云 magpie 插件:百智云「本机导入」「浏览器登录」成功后自动开通并写入国内 MonkeyCode(后台执行、失败不影响百智云登录、按 uid 去重);百智云只保留这两条登录路,国内站只认百智云身份,国际站独立(API Key / 账号密码 / 本机导入 / 浏览器);支持双方邀请码、网关签名与「每日额度 / 积分」计费标识。",
"type": "module",
"main": "./index.mjs",
Expand Down
7 changes: 6 additions & 1 deletion src/monkeycode/catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,14 @@ const BRANDS = Object.freeze({
* 平台前端也是这个口径(`ja()` 取前缀、`Im()` 按 plan 判权限)。
* 日额度耗尽后三档一律默认自动转扣积分(平台「额度耗尽后消耗积分」开关,默认开),
* 所以名字里该说的是「这条吃不吃日额度」,不是「基础/专业/旗舰」。
*
* ⚠ 2026-11-13 起 basic 前缀不再标「每日额度」:平台把基础会员改成**永久免费、不赠送
* 日额度**(官方前端 `dailyQuotaBasic: "不赠送额度"`、`status: "unsupported"`),
* 模型选择器里 basic 模型标的是「免费使用」。所以这条只说明「免费档可用」,
* 真正吃日额度的是 pro / ultra 前缀。
*/
const TIERS = Object.freeze({
"monkeycode-basic": "每日额度",
"monkeycode-basic": "免费",
"monkeycode-pro": "每日额度",
"monkeycode-ultra": "每日额度",
})
Expand Down
16 changes: 12 additions & 4 deletions src/monkeycode/credits.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ const MAX_POW_ATTEMPTS_TOTAL = 5_000_000
/** 订阅档位只认这三个值,其余一律当「档位未知」(不猜)。 */
export const PLAN_LABELS = Object.freeze({ basic: "Basic", pro: "Pro", ultra: "Ultra" })

/** 每日额度的档位 → 计费 token 上限。 */
export const PLAN_DAILY_LIMIT = Object.freeze({ basic: 10_000_000, pro: 100_000_000, ultra: 300_000_000 })
/**
* 每日额度的档位 → 计费 token 上限。
*
* ⚠ 只有 pro / ultra 有日额度。2026-11-13 核实:平台把 basic 改成**永久免费、不赠送额度**
* (官方前端 `dailyQuotaBasic: "不赠送额度"`,`status: "unsupported"`),这类账号钱包里
* `daily_token_limit` / `daily_token_balance` 恒为 0。所以 0 **不能**反推成 basic ——
* basic 只认 `/users/subscription` 的 `plan` 字段。
*/
export const PLAN_DAILY_LIMIT = Object.freeze({ pro: 100_000_000, ultra: 300_000_000 })

/**
* 把用户可能粘贴的 cookie 串归一成可以直接放进 `Cookie` 头的值。
Expand Down Expand Up @@ -276,8 +283,9 @@ export async function fetchPlan(cookie, site, { fetchImpl = fetch, timeoutMs = 1
*
* 1. `GET /api/v1/users/subscription` 的 `plan`(明确的说法,最可信);
* 2. 该端点不给 plan 时(2026-11-04 国内站实测返回 undefined),用钱包的
* `daily_token_limit` 反推 —— 三档的日额度是固定的 10M / 100M / 300M,
* 见 `PLAN_DAILY_LIMIT`。数值对不上就不猜。
* `daily_token_limit` 反推 —— **只有 pro / ultra 能反推**(100M / 300M,见
* `PLAN_DAILY_LIMIT`)。basic 现在没有日额度、钱包恒为 0,反推不出来,
* 只认第 1 条;数值对不上就不猜。
*
* 用途只有一个:把模型目录收窄到这个账号真能吃的额度池(`filterByTier`)。
* 拿不到档位时**宁可不过滤**,也不要把列表清空。
Expand Down
6 changes: 5 additions & 1 deletion src/monkeycode/runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,11 @@ async function readUsage(site, credential, fetchImpl) {
const windows = []
// 每日额度:数量型窗口。span = 一天(上游不下发重置时刻,编一个 resetsAt 就是编造)。
// 只有上限与剩余都有才画(缺剩余时把 0 当剩余会让用户以为今天已用光)。
if (wallet.dailyTokenLimit !== undefined && wallet.dailyTokenBalance !== undefined) {
// ⚠ 上限为 0 也不能画:按积分计费的账号(`enable_credit_consumption: true`)钱包里
// `daily_token_limit` / `daily_token_balance` 恒为 0,画出来会被 `usageWindow` 判
// `limit > 0` 不成立而抛 QuotaShapeError —— 整张额度卡片变成「暂时无法获取额度」
// (2026-11-13 真机:6 个 basic 账号全部 0/0)。没有每日额度就是没有这条窗口。
if (wallet.dailyTokenLimit !== undefined && wallet.dailyTokenLimit > 0 && wallet.dailyTokenBalance !== undefined) {
windows.push(usageWindow({
name: "每日额度",
amount: Math.max(0, wallet.dailyTokenLimit - wallet.dailyTokenBalance),
Expand Down
27 changes: 13 additions & 14 deletions tests/catalog.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,12 @@ test("fetchCatalog:平台没有可用模型时报失败,不返回空表", as
assert.equal(result.ok, false)
})

test("prettyModelName:档位一律标「每日额度」,无前缀才标「积分」", async () => {
test("prettyModelName:basic 标「免费」,pro / ultra 标「每日额度」,无前缀标「积分」", async () => {
const { entryOf, prettyModelName } = await import("../src/monkeycode/catalog.mjs")
// 依据 docs/monkeycode-model-billing.md §4.5:
// - 三档(basic/pro/ultra)吃的都是同一个「当日免费额度」,耗尽后默认自动转扣积分
// (官方前端「额度耗尽后消耗积分」开关,A1/A2/A5)—— 名字里该说的是「这条吃不吃日额度」;
// 档位名(基础/专业/旗舰)由账号自己的订阅决定,写进名字反而每次都要解释。
assert.equal(prettyModelName("monkeycode-basic/glm-5.3-flash"), "GLM-5.3-Flash · 每日额度")
// 2026-11-13 平台口径变更:基础会员「永久免费、不赠送额度」
// (官方前端 `dailyQuotaBasic: "不赠送额度"`,模型选择器里 basic 模型标「免费使用」),
// 所以 basic 前缀不能再标「每日额度」;只有 pro / ultra 吃日额度。
assert.equal(prettyModelName("monkeycode-basic/glm-5.3-flash"), "GLM-5.3-Flash · 免费")
assert.equal(prettyModelName("monkeycode-pro/hy3"), "HY3 · 每日额度")
assert.equal(prettyModelName("monkeycode-ultra/qwen3.7-max"), "Qwen3.7-Max · 每日额度")
// 无前缀 = 官方「付费模型 / 消耗积分使用」分组,直接扣积分(同上 A7)。
Expand All @@ -74,7 +73,7 @@ test("prettyModelName:档位一律标「每日额度」,无前缀才标「
assert.equal(prettyModelName(""), "")
// 缓存条目上的 displayName 是同一套口径(读缓存时算出来的那个字段,用户看到的就是它)。
const entry = entryOf({ id: "u1", model: "monkeycode-basic/deepseek-flash", interface_type: "anthropic" })
assert.equal(entry.displayName, "DeepSeek-Flash · 每日额度")
assert.equal(entry.displayName, "DeepSeek-Flash · 免费")
})

test("probeCatalog:只有 200 才算能用,403/异常一律不可用", async () => {
Expand Down Expand Up @@ -331,7 +330,7 @@ async function withCacheDir(fn) {
}
}

test("readCatalog ⑤:旧缓存的「基础档/专业档/旗舰档」读出即重算成「每日额度」,且只读不改文件", async () => {
test("readCatalog ⑤:旧缓存的「基础档/专业档/旗舰档」读出即重算成「免费 / 每日额度」,且只读不改文件", async () => {
const { readCatalog } = await import("../src/monkeycode/catalog.mjs")
await withCacheDir(({ dir, models }) => {
const file = join(dir, "monkeycode-models-monkeycode.json")
Expand All @@ -342,15 +341,15 @@ test("readCatalog ⑤:旧缓存的「基础档/专业档/旗舰档」读出即
assert.equal(catalog.fetchedAt, 1)
assert.equal(catalog.tier, "basic", "落盘的档位要带回来(runtime 靠它收窄)")

// 三个档位一律「每日额度」;无前缀继续「积分」。
// basic 前缀 = 「免费」,pro / ultra 前缀 = 「每日额度」;无前缀继续「积分」。
assert.deepEqual(
catalog.models.map((model) => model.displayName),
[
"GLM-5.3-Flash · 每日额度",
"GLM-5.3-Flash · 免费",
"HY3 · 每日额度",
"Qwen3.7-Max · 每日额度",
"DeepSeek-Flash · 积分",
"DeepSeek-Flash · 每日额度",
"DeepSeek-Flash · 免费",
],
)
// 除了 displayName,其它字段一个字都不许动(uuid 还要用来区分记录)。
Expand All @@ -359,7 +358,7 @@ test("readCatalog ⑤:旧缓存的「基础档/专业档/旗舰档」读出即
models.map((model) => ({
...model,
displayName: model.displayName
.replace(/ · 基础档$/, " · 每日额度")
.replace(/ · 基础档$/, " · 免费")
.replace(/ · 专业档$/, " · 每日额度")
.replace(/ · 旗舰档$/, " · 每日额度"),
})),
Expand All @@ -381,7 +380,7 @@ test("readCatalog + filterByTier:重算后的目录照常去重(积分孪生
const narrowed = filterByTier(catalog.models, catalog.tier)
assert.deepEqual(idsOf(narrowed), ["monkeycode-basic-glm-5-3-flash", "monkeycode-basic-deepseek-flash"])
// 留下来的那条也必须是重算过的名字(否则「去重对了、名字还是旧的」会漏网)。
assert.equal(narrowed[1].displayName, "DeepSeek-Flash · 每日额度")
assert.equal(narrowed[1].displayName, "DeepSeek-Flash · 免费")

// 逃生阀:只关去重,权限过滤照旧(basic 账号仍然看不到 pro / ultra)。
const previousOptOut = process.env.MAGPIE_MONKEYCODE_SHOW_CREDIT_TWINS
Expand All @@ -394,7 +393,7 @@ test("readCatalog + filterByTier:重算后的目录照常去重(积分孪生
)
// 逃生阀下重算照样生效(改名与去重是两件事)。
assert.equal(escaped.find((model) => model.id === "deepseek-flash").displayName, "DeepSeek-Flash · 积分")
assert.equal(escaped.find((model) => model.id === "monkeycode-basic-deepseek-flash").displayName, "DeepSeek-Flash · 每日额度")
assert.equal(escaped.find((model) => model.id === "monkeycode-basic-deepseek-flash").displayName, "DeepSeek-Flash · 免费")
} finally {
if (previousOptOut === undefined) delete process.env.MAGPIE_MONKEYCODE_SHOW_CREDIT_TWINS
else process.env.MAGPIE_MONKEYCODE_SHOW_CREDIT_TWINS = previousOptOut
Expand Down
Loading