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

## 0.2.39 — 2026-11-04

**修「模型全都不通」:magpie 发的是它自己的模型 id,上游只认带档位前缀的原值**

- 真机现象:国际站 2 条模型全部 `403 Forbidden`。直连上游实测——**同一份凭据、同一个模型**,`monkeycode-basic/deepseek-flash` 回 **200**,而 `monkeycode-basic-deepseek-flash`(slug)、UUID、裸名 `deepseek-flash` 全部 **403 Forbidden**
- 根因在 magpie 侧:`api: { id: m.id ?? was?.api.id ?? key }` —— 它用**我们返回的 `id`**(斜杠被压成连字符)当上游 model,**无视** config 里给的 `api.id`。而模型 id 又不能带斜杠(会被 magpie 当成 provider 边界,长出 `monkeycode-basic` / `monkeycode-pro` 三个假供应商)
- 所以只能在自己的 `loader.fetch` 里换回来:读目录缓存拿 `id → apiId` 映射,把请求体里的 `model` 从 magpie 的 id 换回平台原值,再补 system、再算签名
- 单测锁住:`loader.fetch` 收到 `monkeycode-basic-deepseek-flash` 时,真正发出去的是 `monkeycode-basic/deepseek-flash`

## 0.2.38 — 2026-11-04

**模型列表再按订阅档位收窄:basic 账号不再看到 pro / ultra 的池**
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.38",
"version": "0.2.39",
"description": "MonkeyCode 国内站 / 国际站 + 百智云 magpie 插件:三家独立供应商,浏览器登录或 API Key 接入,网关签名与积分额度。",
"type": "module",
"main": "./index.mjs",
Expand Down
30 changes: 30 additions & 0 deletions src/monkeycode/runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,36 @@ export function makePlugin(site) {
if (body === undefined && request) body = await request.clone().text()
let text = typeof body === "string" ? body : undefined

// ── 把 magpie 的模型 id 换回上游认得的那串 ──────────────────
//
// magpie 发过来的 `model` 是**它自己的模型 id**(我们返回的 `id`,
// 斜杠被压成连字符,例如 `monkeycode-basic-deepseek-flash`)——
// 它不会用我们在 config 里给的 `api.id`:
// magpie: `api: { id: m.id ?? was?.api.id ?? key }`(m 是 config 的条目)
// 而上游只认**平台原值**(带档位前缀的 `monkeycode-basic/deepseek-flash`):
// 2026-11-04 实测 —— 原值 200,slug 回 `403 Forbidden`,
// UUID 回 `403 Forbidden`,裸名 `deepseek-flash` 也 403。
//
// 所以这一层必须换回来。映射来源就是目录缓存(`id → apiId`)。
// 换完**再**补 system、**再**算签名:签名覆盖的是 prompt(system),
// 与 model 无关,但顺序上仍以「最终要发出去的 body」为准最稳。
if (text !== undefined) {
const catalog = readCatalog(site.id)
if (catalog !== undefined && catalog.models.length > 0) {
const byId = new Map(catalog.models.map((model) => [model.id, model.apiId]))
try {
const parsed = JSON.parse(text)
const key = typeof parsed.model === "string" ? parsed.model : ""
const mapped = byId.get(key)
if (mapped !== undefined && mapped !== key && mapped.length > 0) {
parsed.model = mapped
text = JSON.stringify(parsed)
body = text
}
} catch {}
}
}

// 网关要求被签名的 prompt 非空:没有 system 的请求它会一律回
// `403 invalid ohmyagent request`(2026-11-04 实测:不带 system 的
// 探测 403、带 system 的同样请求 200)。用户直接对话时 magpie 经常
Expand Down
111 changes: 111 additions & 0 deletions tests/loader-model-map.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// tests/loader-model-map.test.mjs —— 发出去的 `model` 必须是上游认得的那串
//
// 2026-11-04 真机:国际站两条模型全部 `403 Forbidden`。直连上游用同一份凭据试:
//
// monkeycode-basic/deepseek-flash → 200
// monkeycode-basic-deepseek-flash(slug) → 403 Forbidden
// <uuid> → 403 Forbidden
// deepseek-flash(裸名) → 403 Forbidden
//
// 根因在 magpie 侧:它构造上游请求时用的是 `api: { id: m.id ?? … }` —— **我们返回的
// `id`**(斜杠被压成连字符),config 里给的 `api.id` 被无视。而 magpie 的模型 id 又
// 不能带斜杠(会被当成 provider 边界,长出 `monkeycode-basic` / `monkeycode-pro` /
// `monkeycode-ultra` 三个不存在的供应商)。
//
// 所以只能在 loader.fetch 里换回来。这个测试锁的就是这一步。

import test from "node:test"
import assert from "node:assert/strict"
import { mkdtempSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"

const dir = mkdtempSync(join(tmpdir(), "magpie-modelmap-"))
process.env.MAGPIE_MONKEYCODE_CACHE_DIR = dir
process.env.MAGPIE_PLUGIN_DISABLE_BROWSER = "1"

writeFileSync(join(dir, "monkeycode-models-monkeycodeintl.json"), JSON.stringify({
fetchedAt: Date.now(),
models: [{
id: "monkeycode-basic-deepseek-flash",
apiId: "monkeycode-basic/deepseek-flash",
name: "monkeycode-basic/deepseek-flash",
displayName: "DeepSeek-Flash · 基础档",
context: 1048576,
output: 32000,
image: true,
free: false,
}],
}), "utf8")

const { MonkeyCodeIntlAuthPlugin } = await import("../index.mjs")

const AUTH = {
type: "api",
key: JSON.stringify({ access_token: "oma_testtesttesttest", signing_secret: "secret-secret-secret" }),
}

/** 跑一次 loader.fetch,返回真正发出去的东西。 */
async function send(body, initExtra = {}) {
const plugin = await MonkeyCodeIntlAuthPlugin({}, {})
const loader = await plugin.auth.loader(async () => AUTH)
let captured
const realFetch = globalThis.fetch
globalThis.fetch = async (url, init) => {
captured = { url, headers: init?.headers, body: init?.body }
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } })
}
try {
await loader.fetch(`${loader.baseURL}/messages`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
...initExtra,
})
} finally {
globalThis.fetch = realFetch
}
return captured
}

test("magpie 的模型 id 被换回平台原值(带档位前缀),不是 slug", async () => {
const captured = await send({
model: "monkeycode-basic-deepseek-flash",
max_tokens: 8,
messages: [{ role: "user", content: "hi" }],
})
const sent = JSON.parse(captured.body)
assert.equal(sent.model, "monkeycode-basic/deepseek-flash")
})

test("已经是上游原值时不动它(幂等)", async () => {
const captured = await send({
model: "monkeycode-basic/deepseek-flash",
max_tokens: 8,
messages: [{ role: "user", content: "hi" }],
})
assert.equal(JSON.parse(captured.body).model, "monkeycode-basic/deepseek-flash")
})

test("目录里没有的 model 原样放行(不猜、不改)", async () => {
const captured = await send({
model: "some-unknown-model",
max_tokens: 8,
messages: [{ role: "user", content: "hi" }],
})
assert.equal(JSON.parse(captured.body).model, "some-unknown-model")
})

test("改完 model 之后补 system、并按最终 body 签名", async () => {
const captured = await send({
model: "monkeycode-basic-deepseek-flash",
max_tokens: 8,
messages: [{ role: "user", content: "hi" }],
})
const sent = JSON.parse(captured.body)
assert.equal(typeof sent.system, "string")
assert.ok(sent.system.length > 0, "没 system 的请求上游一律 403 invalid ohmyagent request")
const headers = captured.headers instanceof Headers ? Object.fromEntries(captured.headers) : captured.headers
const signature = headers["x-ohmyagent-signature"] ?? headers["X-OhmyAgent-Signature"]
assert.ok(String(signature).startsWith("v1="), "签名头必须存在")
})
Loading