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
47 changes: 35 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,55 @@ name: CI

on:
push:
branches: [ main, master ]
branches: [main]
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 20 是 engine 下限,22 是当前 LTS。两条都测 ——
# 只在一条上绿过,就没资格说「支持多版本」。
node-version: ["20", "22"]
steps:
- uses: actions/checkout@v4

- name: Set up Node 20
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
node-version: ${{ matrix.node-version }}

# 只跑单元测试,不装 wrangler —— wrangler 只在部署时需要,
# 而单元测试是零依赖的(node:test + node:assert)
- name: Unit tests
# 单元测试零依赖(node:test + node:assert),所以不装 wrangler。
# 这本身是个需要守住的特性:装依赖会让 CI 变慢,也会引入供应链面。
- name: 单元测试
run: npm test

syntax:
- name: 语法检查(所有源文件,不只 worker.js)
run: |
for f in src/*.js scripts/*.js test/*.mjs; do
node --check "$f" || exit 1
done

docs-sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check worker.js syntax
run: node --check src/worker.js
node-version: "22"
# README 里列出的模型必须和代码里的目录一致。
# 上一版 README 写着「7 个模型」,代码里是 8 个,一个都对不上 ——
# 这条守的就是那个故障:文档与代码各自漂移。
- name: README 与 src/providers.js 的模型清单必须一致
run: node scripts/check-docs.mjs

shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: deploy.sh 语法与静态检查
run: |
bash -n deploy.sh
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
shellcheck -S warning deploy.sh || true
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.wrangler/
.dev.vars
# 本地 OAuth 凭据,等同账号权限,绝不能提交
.iceProxy/
*.log
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Changelog

## 2.0.0

重写。这一版的重点不是「加功能」,而是**修掉上一版里几个「本地绿、线上必挂」的问题**,
并把模型清单收敛到单一真相源。

### 修复(每条都有对应回归测试)

- **流式请求没有真的流式。** OpenAI 兼容分支里 `stream` 被硬编码成 `false`,
于是客户端请求 `stream: true` 时收到一个普通 JSON。聊天类客户端(Cline、Cursor、
各种 Web UI)会一直等一个不会到来的 SSE,表现为卡死。**这是 18 个模型里
13 个的默认路径。**

- **Gemini 完全没有流式。** 上一版只实现了 `generateContent`,没有
`streamGenerateContent`。

- **有效的 Qwen 账号被判定为不可用。** 账号池判断「token 是否有效」时读的是
`acc.token`,而账号存的是 `access_token`。这个分支永远不会命中,所以每个请求
都走「拿 refresh_token 去换」的路径。后果有两层:一是每个请求白白多一次上游往返;
二是 refresh 一旦失败(这个 grant 不是稳定的公开接口,失败很常见),
一个还有一小时寿命的**有效 token 会被冷藏一整天**,用户看到的是
「no Qwen account available」。而 Qwen 是默认模型家族。

- **`README` 声称的「跨 provider 自动故障转移」并不存在。** 代码里只有 Qwen 的
账号轮换。现在真的实现了:按「同模型 → 同 provider 其它模型 → 其它已配置
provider」构建候选链,只在 429/401/403/5xx 时往下走。

- **错误体不是 OpenAI 形状。** 返回的是 `{"error": "unknown model: x"}`(字符串),
而官方 SDK 会按 `{"error": {"message": ...}}` 解析。结果是用户看到客户端 SDK
自己的解析异常,而不是真实原因。

- **Gemini 把多轮历史整段丢弃。** 只取 `messages` 的最后一条,而 Cline/Cursor
每轮都带完整历史 —— 等于每轮都让模型失忆。

- **上游中途断开时,已收到的内容被丢掉。** SSE 拆帧函数在 `read()` 抛错时直接
向上抛,缓冲区里已经读到、还没交付的那部分就被丢了。表现是「模型说到一半
突然没了」,而且断在哪一半是随机的。

### 新增

- **模型目录与 provider 注册表分离**(`src/providers.js`),18 个模型 / 6 个 provider。
新增模型不再需要复制 base/type/auth 三份配置。
- **`Groq`**。至此 6 个 provider。
- **`scripts/check-docs.mjs`** —— CI 里强制 README 与代码的模型清单一致。
上一版 README 标题写 7 个模型、正文表格写 8+、`package.json` 写 5 个、
代码里实际 8 个,四份数字没有一个对得上。
- **响应里标注实际服务的模型**:`_proxy` 字段 + `X-IceProxy-Fallback` 头。
- **`/v1/models` 带 `context_length` / `capabilities` / `available`。**
- **账号池按失败原因分级冷却**:限流一分钟、服务端错误十几秒、认证失败几小时。
不再「一次 429 封一整天」。
- **并发刷新去重**:KV 没有原子操作,同时刷新同一账号会互相覆盖。
- **82 个单测**(上一版 15 个),全部离线、零依赖。

### 变更(可能影响你)

- **模型 ID 有增删**。`gemini/gemini-1.5-flash`、`openrouter/auto`、
`qwen/vision-model` 被替换成实际可用的 ID。README 里那批「Qwen3-Max /
GLM-5.3 / Kimi-K3」从来就不存在于代码里,已从文档中删除。
- **错误响应结构变了**(从字符串改成对象)。这是修 bug,但如果你写了
针对旧格式的解析代码,需要跟着改。
- **`deploy.sh` 不再用 `sed` 改 `wrangler.toml`**。上一版的正则匹配的是
注释行 `# id = "your_kv_id"`,一旦那行被改过格式,`sed` 会静默不匹配
(退出码仍是 0),KV id 留在占位符上,部署出来的 Worker 每个 Qwen 请求都失败。
- **`scripts/auth.js` 改用 `execFileSync`**,不再手工拼 shell 字符串 ——
上一版对含引号的 JSON 做手工转义,既容易出错也是命令注入面。
- **`package.json` 标记 `"type": "module"`**,Node >= 20。
92 changes: 92 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 贡献指南

## 这个项目的底线

1. **运行时零依赖。** `package.json` 的 `dependencies` 必须一直是空的。
`devDependencies` 里只有 `wrangler`(部署工具,不参与运行时)。
单测用 Node 自带的 `node:test`,不引测试框架。
2. **单一真相源。** 模型清单只写在 `src/providers.js`。README 里的表格由
CI(`scripts/check-docs.mjs`)强制与它保持一致。不要在别处再抄一份。
3. **不硬编码会过期的事实。** 免费额度、模型数量这类会变的东西,
要么从代码推导,要么不写。README 里写死的「2000 req/day」是上一版的教训。
4. **每修一个 bug,补一条会红的测试。** 见下文。

## 关于测试

跑:

```bash
npm test
```

写测试时请遵守一条:**先确认你的测试在 bug 存在时会变红。**

做法是把修复临时还原,跑一遍,看它是否失败。例如:

```bash
# 假设你修了一个字段名 bug
cp src/accounts.js /tmp/acc.bak
# 手动把修复改回去
npm test # 这一步必须看到失败
cp /tmp/acc.bak src/accounts.js
npm test # 恢复后必须全绿
```

没做过这一步的测试,很可能是一条**橡皮图章**:断言写得看着合理,
但缺陷存在时它也照样通过。本项目上一版的测试基本都是这个毛病
——测的全是「配置项自洽」(比如「base 是不是 https」),
而真实发生的故障(流式返回 JSON、有效 token 被判失效)一条都没覆盖。

所以测试按**故障现象**组织,不按代码结构组织。文件里的分节标题就是
「这类故障」,每条 `test()` 的名字就是「用户看到的现象」。

## 加一个新模型

1. 在 `src/providers.js` 的 `MODELS` 里加一行:

```js
{ id: "provider/model-name", ctx: 131072, caps: ["tools", "stream"] },
```

如果 provider 已存在,只加这一行就够。新增 provider 才需要在
`PROVIDERS` 里加声明(含 `base` / `protocol` / `auth` / `keyEnv`)。

2. **实测确认这个模型的 id 在真上游存在。** 不要凭印象写。
`unknown model` 这类错误用户是没法自救的。OpenRouter 的模型可以直接查:

```bash
curl -s https://openrouter.ai/api/v1/models | node -e '
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
const ids=JSON.parse(s).data.map(m=>m.id);
console.log(ids.filter(i=>i.includes("你的关键词")).join("\n"));
})'
```

3. 更新 `README.md` 的模型表,然后:

```bash
node scripts/check-docs.mjs # 不一致会直接告诉你差在哪
```

## 改协议相关的东西

`src/openai.js`(SSE 帧与错误形状)和 `src/adapters.js`(各家协议转换)
是兼容性的地基。改动这两处时,请:

- 不要依赖「一次 `read()` 就是一行」。真实网络会把一行切成任意碎片。
测试里有对应的用例,别删。
- 错误体必须是 `{"error": {"message", "type", "code"}}` 对象。
- 流式响应必须以字面量 `data: [DONE]` 收尾,否则客户端会一直等。

## 提交前

```bash
npm run check # 单测 + 语法检查
node scripts/check-docs.mjs # 文档与代码一致性
bash -n deploy.sh
```

## 提交信息

用 `fix:` / `feat:` / `chore:` / `docs:` 开头,正文说清**现象**和**根因**,
不只是结论。「修了流式」是废话,「stream 被硬编码成 false,导致客户端卡死」才有用。
Loading
Loading