Skip to content

Releases: xwteam/agnes2api

v1.0.0

Choose a tag to compare

@xwteam xwteam released this 14 Sep 03:03

🇨🇳 中文

整个网关用 Rust 重写了一遍。对外契约逐格不变,形态仍然是单一 Docker 镜像。

升级就是换镜像:store.json 的形状一个字节都没改,0.4.0 的数据目录直接挂上去即可。
发版前实测过:把 0.4.0 写下的 store.json 原样交给这一版,网关配置、子密钥、key 池、
注册机的通道与凭据、补池历史全部读得出来。

换了什么

  • 实现语言:TypeScript + Hono → Rust + axum。面板少了一个构建步骤 ——
    admin-ui/ 下的手写文件由 rust-embed编译期直接读原文件嵌进二进制,
    于是「产物与源对不上」在结构上不可能发生。
  • 对外契约没有变:四条协议路由、/admin 子树那 30 余条端点、三种错误信封、
    面板的每一块取数都照旧。发版前用 0.4.0 与这一版同配置、同请求跑了一轮对照,
    16 组请求状态码逐条一致。

新增

  • 注册机:「立即补池」有了两道闸 —— 10 分钟冷却 + 每天 24 次。
    一次点击最多消耗 MINT_BATCH 个临时邮箱名额、每铸一把还是一次真实的上游建号,
    所以连点的代价是真的。状态里也报「今天还剩几次(共几次)」,不必等撞上墙才知道有墙。
  • 注册机:15 个配置项进设置页 —— 通道、目标数量、铸号节奏、两条通道的地址与凭据,
    改完不必重启。此前它们只能靠环境变量配。

修复

  • 🔴 面板改完不生效 / 改完关不掉:此前注册机的配置是启动那一刻的快照
    启动时关着就再也打不开、开着就再也关不掉。现在每一轮与每一次请求都重读。
  • 🔴 关着注册机的部署永远不对账 key 池索引:对账此前挂在补池那条路上,
    而注册机默认是关着的。现在它接在定时轮开头,不受注册机开关门控
  • 打开注册机却没选通道时不再替你挑一条:两条邮箱通道完全平级,
    此前会静默落到其中一条,而后果要到「凭据缺失」那条报错指着一条你从没选过的通道时才浮上来。

完整条目见 CHANGELOG


🇺🇸 English

The whole gateway has been rewritten in Rust. The external contract is unchanged cell for cell, and it still ships as a single Docker image.

Upgrading is swapping the image: the shape of store.json did not move a byte, so the 0.4.0
data directory mounts straight in. Verified before release by handing a store.json written by
0.4.0 to this version: gateway config, API keys, the key pool, the registrar's channel and
credentials, and the top-up history all read back.

What changed

  • Implementation: TypeScript + Hono → Rust + axum. The panel lost a build step —
    the hand-written files under admin-ui/ are read straight from disk by rust-embed at
    compile time, so "the artifact disagrees with the source" cannot happen structurally.
  • The external contract did not change: the four protocol routes, the 30-odd /admin
    endpoints, the three error envelopes and every panel read are as they were. Before release,
    0.4.0 and this version were run side by side with identical config and identical requests:
    16 request groups, status codes identical across the board.

Added

  • Registrar: "top up now" has two guards — a 10-minute cooldown and 24 clicks a day.
    One click consumes up to MINT_BATCH temporary-mailbox slots and every minted key is a real
    upstream registration, so hammering it has a real cost. The status now reports
    "N of M left today" instead of letting you find the wall by walking into it.
  • Registrar: 15 settings on the settings page — channel, target count, minting cadence, and
    both channels' addresses and credentials, with no restart needed. They were
    environment-variable-only before.

Fixed

  • 🔴 Panel edits that never took effect, and a registrar you could not switch off: the
    registrar's config used to be a snapshot taken at start-up — off at boot meant it could
    never be turned on, on at boot meant it could never be turned off. Every round and every
    request now re-reads it.
  • 🔴 A deployment with the registrar off never reconciled the key-pool index: reconciliation
    used to hang off the top-up path, and the registrar is off by default. It now runs at the top
    of the scheduled tick, not gated on the registrar's switch.
  • Enabling the registrar without choosing a channel no longer picks one for you: the two
    mailbox channels are fully equivalent. It used to fall through to one of them silently, and
    you would only find out when a "credentials missing" error pointed at a channel you never chose.

Full entries in the CHANGELOG.


🇯🇵 日本語

ゲートウェイ全体を Rust で書き直しました。外部契約は一つ残らずそのまま、形態も単一の Docker イメージのままです。

アップグレードはイメージの入れ替えだけです:store.json の形は 1 バイトも変わっていないので、
0.4.0 のデータディレクトリをそのままマウントできます。リリース前に実測済み ——
0.4.0 が書いた store.json をこのバージョンに渡したところ、ゲートウェイ設定・API キー・
キープール・レジストラのチャネルと認証情報・補充履歴がすべて読み出せました。

変わったこと

  • 実装言語:TypeScript + Hono → Rust + axum。パネルはビルド手順が 1 つ減りました ——
    admin-ui/ 配下の手書きファイルは rust-embedコンパイル時にそのまま読み込むため、
    「生成物とソースが食い違う」ことが構造上起こりません。
  • 外部契約は変わっていません:4 つのプロトコルルート、/admin の 30 余りのエンドポイント、
    3 種類のエラー封筒、パネルの各取得はすべて従来どおりです。リリース前に 0.4.0 と本版を
    同じ設定・同じリクエストで並べて実行し、16 グループすべてでステータスコードが一致しました。

追加

  • レジストラ:「今すぐ補充」に 2 つの歯止め —— 10 分のクールダウンと 1 日 24 回。
    1 回のクリックで最大 MINT_BATCH 個の使い捨てメール枠を消費し、1 本ごとに実際の
    アカウント登録が走るため、連打のコストは現実のものです。状態には
    「本日の残り N 回(全 M 回)」も出ます。
  • レジストラ:設定 15 項目を設定ページへ —— チャネル、目標本数、作成間隔、
    両チャネルのアドレスと認証情報。再起動は不要です。従来は環境変数でしか設定できませんでした。

修正

  • 🔴 パネルで変更しても効かない/オフにできない:レジストラの設定は起動時のスナップショットでした。
    起動時にオフなら二度とオンにできず、オンなら二度とオフにできませんでした。
    現在は毎ラウンド・毎リクエストで読み直します。
  • 🔴 レジストラがオフのデプロイではキープールのインデックスが永遠に照合されない
    照合が補充の経路にぶら下がっていた一方、レジストラは既定でオフです。
    現在は定期実行の先頭で、レジストラのスイッチに関係なく動きます。
  • チャネルを選ばずにレジストラをオンにしても、こちらで勝手に選ばなくなりました
    2 つのメールチャネルは完全に同格です。

詳細は CHANGELOG を参照してください。


🇰🇷 한국어

게이트웨이 전체를 Rust로 다시 작성했습니다. 외부 계약은 한 칸도 바뀌지 않았고, 형태도 여전히 단일 Docker 이미지입니다.

업그레이드는 이미지 교체가 전부입니다: store.json의 모양은 1바이트도 바뀌지 않아서
0.4.0의 데이터 디렉터리를 그대로 마운트하면 됩니다. 릴리스 전에 실측했습니다 ——
0.4.0이 기록한 store.json을 이 버전에 그대로 넘겼을 때 게이트웨이 설정, API 키,
키 풀, 레지스트라의 채널과 자격 증명, 보충 이력이 모두 읽혔습니다.

바뀐 것

  • 구현 언어: TypeScript + Hono → Rust + axum. 패널은 빌드 단계가 하나 줄었습니다 ——
    admin-ui/ 아래 손으로 쓴 파일을 rust-embed컴파일 시점에 그대로 읽어 넣기 때문에
    "산출물과 소스가 어긋난다"는 일이 구조적으로 일어나지 않습니다.
  • 외부 계약은 그대로입니다: 네 개의 프로토콜 라우트, /admin의 30여 개 엔드포인트,
    세 가지 오류 봉투, 패널의 모든 조회가 종전과 같습니다. 릴리스 전에 0.4.0과 이 버전을
    같은 설정, 같은 요청으로 나란히 돌려 16개 그룹의 상태 코드가 모두 일치함을 확인했습니다.

추가

  • 레지스트라: "지금 채우기"에 두 개의 제동 장치 —— 10분 쿨다운과 하루 24회.
    한 번의 클릭이 최대 MINT_BATCH 개의 임시 메일함 자리를 쓰고, 키 하나마다 실제
    업스트림 계정 등록이 일어나므로 연타의 비용은 실재합니다. 상태에
    "오늘 N회 남음(총 M회)"도 표시됩니다.
  • 레지스트라: 설정 15개를 설정 페이지로 —— 채널, 목표 개수, 생성 주기,
    두 채널의 주소와 자격 증명. 재시작이 필요 없습니다. 이전에는 환경 변수로만 설정할 수 있었습니다.

수정

  • 🔴 패널에서 고쳐도 반영되지 않고, 끌 수도 없던 문제: 레지스트라 설정이
    시작 시점의 스냅샷이었습니다. 시작할 때 꺼져 있으면 다시는 켤 수 없었고,
    켜져 있으면 다시는 끌 수 없었습니다. 이제 매 회차, 매 요청마다 다시 읽습니다.
  • 🔴 레지스트라가 꺼진 배포에서는 키 풀 인덱스가 영영 대조되지 않던 문제:
    대조가 보충 경로에 매달려 있었는데 레지스트라는 기본값이 꺼짐입니다.
    이제 정기 실행의 맨 앞에서, 레지스트라 스위치와 무관하게 동작합니다.
  • 채널을 고르지 않고 레지스트라를 켜도 대신 골라주지 않습니다:
    두 메일 채널은 완전히 동등합니다.

전체 항목은 CHANGELOG를 참고하세요.


🇹🇼 繁體中文

整個網關用 Rust 重寫了一遍。對外契約逐格不變,形態仍然是單一 Docker 映像。

升級就是換映像:store.json 的形狀一個位元組都沒改,0.4.0 的資料目錄直接掛上去即可。
發版前實測過:把 0.4.0 寫下的 store.json 原樣交給這一版,網關設定、子密鑰、key 池、
註冊機的通道與憑據、補池歷史全部讀得出來。

換了什麼

  • 實作語言:TypeScript + Hono → Rust + axum。面板少了一個建置步驟 ——
    admin-ui/ 下的手寫檔案由 rust-embed編譯期直接讀原檔嵌進二進位檔,
    於是「產物與原始碼對不上」在結構上不可能發生。
  • 對外契約沒有變:四條協議路由、/admin 子樹那 30 餘條端點、三種錯誤信封、
    面板的每一塊取數都照舊。發版前用 0.4.0 與這一版同設定、同請求跑了一輪對照,
    16 組請求狀態碼逐條一致。

新增

  • 註冊機:「立即補池」有了兩道閘 —— 10 分鐘冷卻 + 每天 24 次。
    一次點擊最多消耗 MINT_BATCH 個臨時郵箱名額、每鑄一把還是一次真實的上游建號,
    所以連點的代價是真的。狀態裡也報「今天還剩幾次(共幾次)」。
  • 註冊機:15 個設定項進設定頁 —— 通道、目標數量、鑄號節奏、兩條通道的位址與憑據,
    改完不必重啟。此前它們只能靠環境變數設定。

修復

  • 🔴 面板改完不生效 / 改完關不掉:此前註冊機的設定是啟動那一刻的快照
    啟動時關著就再也打不開、開著就再也關不掉。現在每一輪與每一次請求都重讀。
  • 🔴 關著註冊機的部署永遠不對帳 key 池索引:對帳此前掛在補池那條路上,
    而註冊機預設是關著的。現在它接在定時輪開頭,不受註冊機開關門控
  • 打開註冊機卻沒選通道時不再替你挑一條:兩條郵箱通道完全平級。

完整條目見 CHANGELOG

v0.4.0

Choose a tag to compare

@xwteam xwteam released this 11 Sep 00:34

这一版把 Cloudflare Worker 形态整个删掉了,只剩 Docker 一种。这是破坏性变更。

跑在 Worker 上的部署没有升级路径:0.4.0 不认 KV,池数据要先从 KV 导出再导入文件存储。留在 0.3.1 不动是安全的。

为什么砍

Worker 那一侧有两条不是靠写代码能绕开的限制:

  • 注册机在 Worker 上铸不出 key。Agnes 上游的注册限流是按 IP 的,Worker 的出口 IP 池不但没帮上忙,还让同一个池里的别人先把额度用掉。同一份代码在 Docker 上正常出 key。
  • waitUntil 会在响应后约 30 秒静默取消整轮补池(v0.3.0 实测 3/3),取消不抛异常try/catch/finally 两层都拦不住。

顺带修好三处已经变成假话的说法

  • 面板把子密钥吊销上界多报了 1 分钟(6 → 5 分钟),传播上界 120/90 → 60/30 秒。多出来的那一分钟是 KV 的边缘缓存,文件存储上没有这一层。这是安全相关的数:一把泄露的 key 到底多久真正失效。
  • 一条启动期告警的后半句在摘形态之前就是假的
  • TRUST_PROXYCF-Connecting-IP 优先从「由平台保证的事实」降级成有前提的取舍——只挂自建 nginx/Caddy 时,攻击者自带这个头就会压过反代写的 X-Forwarded-For

如实说明:两处判别力下降

摘形态的口径是「只摘不补」,以下两处今天真的没人守:删除持久性契约只剩内存存储在跑;「补池抛错的那一轮也会释放锁」没人钉了。


完整条目见 CHANGELOG

v0.3.1

Choose a tag to compare

@xwteam xwteam released this 10 Sep 09:43

中文

v0.3.0 发出去之后跑了一轮六轴审计 + 对抗式复核:36 条原始发现 → 确认 20 条,本版全修。

审计的标尺是「一个真实用户或运维会不会因为这条而受挫、被误导、或做出错误决定」。
四条被复核驳回——其中一条若照原报去改,反而会把一个正确的行为改坏。

💥 两条 critical

安全文档把一条已经不成立的保证写成了事实。 SECURITY.md 逐字写着「没有 reveal 端点」,
而 v0.3.0 加的正是那两条端点 —— 这句话在端点落地之后又留了一整个版本。现在改写成事实,
并在页面上明写它曾经说反话:读过那句话并据此做安全判断的人是被误导了,这条痕迹不该被抹掉。
当前口径是:握有 ADMIN_TOKEN 的人能读出这台网关存的任何凭据。

/v1/responses 流式让官方 SDK 崩在 SDK 内部。 流里缺两类生命周期事件,而官方累积器要靠
其中一类建出快照条目,拿不到就在第一个增量上抛 IndexError —— 栈全在 openai 包里,
用户第一反应是「我 SDK 装坏了」,排查方向整个被带偏。现在发官方最小事件序列并带齐三个
直接读的属性,断流时发 response.failed 且绝不再发 completed
验收用本地回放把网关吐的真字节喂给官方包,零上游请求。

其余修复

  • Gemini 流式没有终帧:拿不到 finishReason 也拿不到 token 数 ⇒ 一个被截断的半截回答和
    一个完整回答逐字节不可区分
    。上一版修另一条协议时写下的判断,当时只兑现了三分之一。
  • 三条协议把图片等非文本块静默丢掉并回 200:实测同一张图走一条路 400、走另两条 200 加
    一段模型没看见图编出来的答案 —— 同一台网关对同一件事给三种行为
  • 生成参数被静默丢弃:同一份带 tools 的请求,走一条路返回真的工具调用,走另一条返回
    200、正文是模型自己吐的乱码、stop_reason 还写着正常结束。现在按三档裁定
    (同名同义透传 / 上游没那一格报错 / 响应转换不了报错),数值范围刻意不校验。
  • Gemini 路由没有方法白名单:任何拼错的方法名都会被当成对话真发一次上游。
  • 「全部测一遍」实际只测得到第一个,其余全被自己的护栏挡成节流;现在按护栏节奏排队,
    并把「在等节流」显示出来 —— 一颗看起来卡住的按钮和一颗真在工作的按钮,用户分不出来
    就等于坏的。
  • 「显示明文 / 复制」在任何 HTTP 错误下静默无反应。现在四态各有各的一句话,
    其中「这条已经不在了」单独一档:那两张表轮询刷新,记录在别处被删之后屏幕上那行还在。
  • 铸号链路把上游状态码与正文整个丢掉,运维拿到的归因是一句空话。
  • 非 2xx 的上游正文可能回显凭据片段:从前只关了一档。现在一律经流式内容级打码
    (不缓冲全体、取消照常向上游传播),只认那一族前缀 —— 错误体里常带请求编号,
    一并打码会让运维丢掉排障线索。
  • 运维闭环七个缺口:一个自由旋钮与容器卷硬耦合(改一下就在下次升级时静默丢光整池凭据)、
    备份清单漏掉三类键(照它恢复会静默吊销全部已签发的对外密钥)、文档反复让人执行一条
    从未给出的命令、回滚无从下手、升级后的确认在「升级压根没发生」时全部通过。七条全补,五语言同步。

English

A six-axis audit after v0.3.0: 36 raw findings, 20 confirmed under adversarial review, all fixed.

Two critical. SECURITY.md stated "there is no reveal endpoint" as an implemented guarantee —
while v0.3.0 had just added exactly that, and the wording stood for a full release afterwards.
It now states the fact, and says plainly that it used to say the opposite. And /v1/responses
streaming crashed the official SDK from inside the SDK (IndexError): the accumulator needs
lifecycle events the gateway never sent, so users saw a stack full of openai package frames and
reasonably concluded their SDK was broken.

Also fixed: Gemini streaming had no final frame (a truncated answer and a complete one were
byte-for-byte indistinguishable); three protocols silently dropped image blocks and returned 200;
generation parameters were silently discarded; the Gemini route had no method whitelist;
"test every model" only ever tested the first one; reveal/copy buttons did nothing on any HTTP
error; upstream non-2xx bodies could echo credential fragments; and seven gaps in the ops loop —
including one knob that silently loses the whole key pool on the next upgrade, and a backup list
that silently revokes every issued API key on restore.


繁體中文

v0.3.0 之後的六軸稽核:36 條發現,對抗式覆核確認 20 條,本版全修。兩條 critical——安全文件把
一條已經不成立的保證寫成事實;Responses 串流讓官方 SDK 崩在 SDK 內部。另修 Gemini 串流沒有
終幀、三條協議靜默丟棄圖片區塊、生成參數被靜默丟棄、方法白名單缺失、「全部測一遍」只測得到
第一個、顯示明文按鈕在錯誤下靜默無反應、非 2xx 正文可能回顯憑證片段,以及維運閉環七個缺口。

日本語

v0.3.0 後の六軸監査:36 件の指摘のうち対抗的レビューで 20 件を確認し、本版で全て修正。
critical 2 件 —— セキュリティ文書が既に成立しない保証を事実として記載していた件、
Responses のストリーミングが公式 SDK 内部でクラッシュする件。ほか、Gemini ストリーミングに
終端フレームが無い、3 つのプロトコルが画像ブロックを黙って捨てる、生成パラメータが黙って
捨てられる、メソッドのホワイトリストが無い、「全部テスト」が最初の 1 件しか実行できない、
平文表示ボタンがエラー時に無反応、2xx 以外の本文が資格情報の断片を含みうる、運用の穴 7 件。

한국어

v0.3.0 이후 6축 감사: 36건 중 적대적 검증으로 20건을 확인해 이번 버전에서 모두 수정.
critical 2건 — 보안 문서가 이미 성립하지 않는 보장을 사실로 기재하고 있던 문제,
그리고 Responses 스트리밍이 공식 SDK 내부에서 크래시하던 문제. 그 밖에 Gemini 스트리밍의
종료 프레임 부재, 세 프로토콜이 이미지 블록을 조용히 버리던 문제, 생성 파라미터가 조용히
버려지던 문제, 메서드 화이트리스트 부재, '전체 테스트'가 첫 모델만 검사하던 문제,
평문 표시 버튼이 오류 시 무반응이던 문제, 2xx 이외 본문의 자격 증명 조각 노출,
운영 폐루프의 7개 공백을 수정했습니다.


📦 ghcr.io/xwteam/agnes2api:0.3.1 · 📄 CHANGELOG

v0.3.0

Choose a tag to compare

@xwteam xwteam released this 10 Sep 04:16

中文

一次面向使用者的大修,含多项破坏性变更。

修复(都来自真机验收,不是推断)

畸形请求体被原样转发上游。 /v1/chat/completions 是透传路由,从前一次本地校验都没有,于是每个畸形请求都白烧一次整个网关共享的限流额度。上游那层 Cloudflare 约两次快请求就限流 ⇒ 一个客户端 bug 循环重试,几秒内能把所有人的通道打死。而且这条路径不受注册机退避状态保护

结构不对的请求体回 500 而不是 400。 触发条件很现实:{"model":"agnes-2.5-flash"}(模型名合法、只是漏写 messages)就够了,而用户看到「网关内部错误」会以为网关挂了来报障。OpenAI 官方 SDK 对 5xx 默认重试 2 次 ⇒ 一个永远修不好的错误被放大成 3 倍请求。

兜成 500 时一条线索都不留(实测 14 次 500 之后事件流零记录)。现在留痕,但响应体一个字都没多给。

Anthropic 流式usage 两处写死 0(0 比缺字段更坏,它长得像一个真值);上游中途断流会静默退出、照常声称 stop_reason: "end_turn"客户端把「被截断」当成「正常说完」。两条都修了。

/v1/models 漏掉上游 12 个模型里的 8 个,含唯一实测能对话的那个 ⇒ 做模型发现的客户端永远选不到能用的。

面板从来没有过响应式:三份 CSS 里 @media 零命中,而其中一份还写着「本仓不写媒体查询」。侧栏死宽任何宽度都不收、顶栏溢出、六张表全部裸奔。补了 1024 / 900 / 640 三档断点。

404 是纯文本而非 JSON;方法错也回 404。 现在 404 走同族 JSON 信封,方法不匹配回 405 + Allow

新增

逐模型连通性测试:一颗按钮把目录里能对话的模型逐个试一遍。串行发不并发,护栏用常量而不是带模型名的 —— 与单把凭据验活那条刻意相反:那边不该互相挡,这边恰恰互相挡,那就是节流。只测对话模型,图片视频硬拒(测一次图片是真出一张图,测视频是建任务加最多 60 次轮询)。

两族凭据都能在面板上掩码 / 点击显示明文 / 复制。 明文走专门端点、绝不进列表响应(列表是高频无意识调用的),每次取明文留一条只含 id 的审计事件。

💥 破坏性变更

对外 API 密钥改为同时存明文。 这是使用者拍板的、以安全性换便利性的取舍,代价如实登记:面板一旦被打穿,全部客户端密钥明文一次性泄漏;存储介质从「不含可直接使用的客户端凭据」变成「含」,备份与快照的处置级别要跟着升。升级之前签发的没有明文,如实回 null 并说明,不用掩码或空串冒充。鉴权热路径一个字都没动。

结构不对的请求体从 500 变 400(依赖「5xx 会被 SDK 重试」的客户端会受影响)。模型目录 4 → 12。


English

A user-facing overhaul with several breaking changes.

Fixed — malformed request bodies were forwarded upstream verbatim, burning the gateway's shared rate-limit budget (a looping client bug could kill everyone's upstream channel in seconds); shape errors returned 500 instead of 400 (a body missing only messages was enough, and SDKs retry 5xx twice); 500s left no trace at all; Anthropic streaming hardcoded usage to 0 (worse than absent — it looks like a real value) and reported mid-stream upstream failure as a normal end_turn; /v1/models was missing 8 of the 12 upstream models including the only one that actually works; the panel never had any breakpoints; 404 was plain text and method mismatches also returned 404.

Added — per-model connectivity test (serial, throttled, chat models only); reveal & copy for both credential families, via a dedicated audited endpoint that never puts plaintext in list responses.

💥 Breaking — gateway-issued API keys are now stored in plaintext (an owner-decided security-for-convenience trade-off: a compromised panel now leaks every client key; storage media change classification). Shape errors are 400 instead of 500. Model catalog 4 → 12.


繁體中文

一次面向使用者的大修,含多項破壞性變更。 修復:畸形請求體被原樣轉發上游、白燒整個閘道共用的限流額度;結構錯回 500 而非 400;500 不留任何線索;Anthropic 串流 usage 恆 0 且上游斷流被報成正常結束;模型清單漏掉十二個裡的八個;面板從來沒有過響應式。新增:逐模型連通性測試、兩族憑證可在面板顯示明文並複製。💥 破壞性:對外金鑰改為同時存明文(使用者拍板的取捨,代價見 CHANGELOG)。

日本語

利用者向けの大改修(破壊的変更あり)。 不正な形のリクエストボディが上流へそのまま転送され、ゲートウェイ全体で共有するレート制限枠を浪費していた問題、構造エラーが 400 ではなく 500 を返していた問題、500 が痕跡を一切残さない問題、Anthropic ストリーミングの usage が 0 固定で上流の中断が正常終了として報告される問題、モデル一覧が 12 個中 8 個を欠いていた問題、パネルにブレークポイントが一つも無かった問題を修正。モデル別の疎通テストと、資格情報の平文表示・コピーを追加。💥 破壊的:外部向けキーを平文でも保存するようになりました(利用者判断のトレードオフ)。

한국어

사용자 대상 대규모 개편(파괴적 변경 포함). 잘못된 형식의 요청 본문이 업스트림으로 그대로 전달되어 게이트웨이 전체가 공유하는 레이트 리밋을 소모하던 문제, 구조 오류가 400이 아닌 500을 반환하던 문제, 500이 아무 흔적도 남기지 않던 문제, Anthropic 스트리밍의 usage가 0으로 고정되고 업스트림 중단이 정상 종료로 보고되던 문제, 모델 목록이 12개 중 8개를 누락하던 문제, 패널에 브레이크포인트가 하나도 없던 문제를 수정했습니다. 모델별 연결성 테스트와 자격 증명 평문 표시·복사를 추가했습니다. 💥 파괴적: 외부용 키를 평문으로도 저장합니다(사용자가 결정한 절충).


📦 ghcr.io/xwteam/agnes2api:0.3.0 · 📄 CHANGELOG

v0.2.2

Choose a tag to compare

@xwteam xwteam released this 09 Sep 17:11

中文

修一条把「立即补池」整颗按钮废掉的缺陷。

面板点一次补池,回 202「已开始」,然后什么都不会发生 —— 补池历史不加行、事件缓冲空、池子不动,与「压根没点过」逐字节不可区分。

根因是 Cloudflare 的平台行为,日志原话:

waitUntil() tasks did not complete within the allowed time after invocation end and have been cancelled.

线上 3/3 复现,取消发生在响应后 31~33 秒。而这一轮传的预算是照着 Cron Trigger 的 15 分钟墙钟定的 13 分钟 —— 那是 scheduled() 的额度,fetchwaitUntil 没有那么多,差了约 26 倍

🔴 取消不抛异常,整个执行上下文被销毁,于是两层 try/catch/finally 一个都不执行。后果不止「这一轮没记上」:finally 里的释放锁同样不跑,锁泄漏到自然过期,而那份 TTL 当时取的正是 Cron 的 15 分钟 ⇒ 点一次按钮 = 注册机连同定时轮一起停摆一刻钟

💥 破坏性变更

POST /admin/api/registrar/tend 的成功响应 202200,端点等整轮跑完再返回,响应体带上这一轮真实的 outcomedone / skipped / crashed 三态,不许被读成同一种「失败」,处置完全不同)。started: true 保留给旧面板,新增 done: true所有前置拒绝的状态码一个都没变。

其它

  • 手动轮与定时轮的上限彻底分开:单轮 1 把、等码 60 秒、域名 1 次、预算 70 秒(定时轮那份没动)
  • 补池锁 TTL 按路径分岔:定时轮 15 分钟不动,手动轮 180 秒 —— 短于 10 分钟冷却,所以即使 finally 被平台跳过,泄漏的锁也活不到下一次可点
  • 确认弹窗的「本次最多铸几把」跟着压顶,否则弹窗说 5、实际做 1

English

Fixes a defect that made the "Tend now" button do nothing at all.

Clicking it returned 202 ("started") and then nothing happened — no refill-history row, no events, no pool change, byte-for-byte indistinguishable from never having clicked.

The root cause is Cloudflare platform behaviour, quoted verbatim from its own logs:

waitUntil() tasks did not complete within the allowed time after invocation end and have been cancelled.

Reproduced 3/3 in production; cancellation lands 31–33 s after the response. The round was being handed a 13-minute budget derived from the Cron Trigger wall clock — that is the scheduled() allowance, not what a fetch handler's waitUntil gets. Off by roughly 26×.

Cancellation throws nothing, so both layers of try/catch/finally were skipped — including the lock release, which leaked the tend lock for 15 minutes and starved the cron rounds too.

💥 Breaking

POST /admin/api/registrar/tend now returns 200 instead of 202, waiting for the round to finish and carrying the real outcome (done / skipped / crashed — never collapse these into one "failure"). started: true is kept for older panels; done: true is new. No rejection status code changed.


繁體中文

修一條把「立即補池」整顆按鈕廢掉的缺陷。

點一次回 202「已開始」,然後什麼都不會發生。根因是 Cloudflare 在回應結束後約 30 秒取消 waitUntil,而取消不拋例外,兩層 try/catch/finally 都不執行 —— 歷史沒寫、事件沒發、鎖也沒放,洩漏的鎖連定時輪一起擋掉一刻鐘。

💥 破壞性:成功回應 202200,端點等整輪跑完再回應並帶上真實的 outcome。前置拒絕的狀態碼一個都沒變。


日本語

「今すぐ補充」ボタンを丸ごと無効化していた不具合の修正。

クリックすると 202(開始した)が返り、その後何も起きませんでした。原因は Cloudflare がレスポンス終了の約 30 秒後に waitUntil を打ち切ることです。打ち切りは例外を投げないため、2 層の try/catch/finally がいずれも実行されず、履歴も記録されず、ロックも解放されないまま 15 分間漏れ、定時ラウンドまで塞いでいました。

💥 破壊的変更:成功時のレスポンスが 202200 になり、1 巡が終わってから実際の outcome を返します。拒否側のステータスコードは 1 つも変わっていません。


한국어

'지금 보충' 버튼을 통째로 무용지물로 만들던 결함을 고쳤습니다.

누르면 202(시작함)가 돌아온 뒤 아무 일도 일어나지 않았습니다. 원인은 Cloudflare가 응답이 끝나고 약 30초 뒤 waitUntil을 취소하기 때문입니다. 취소는 예외를 던지지 않으므로 두 겹의 try/catch/finally가 모두 실행되지 않았고, 기록도 남지 않고 잠금도 풀리지 않아 15분간 새면서 정시 라운드까지 막았습니다.

💥 파괴적 변경: 성공 응답이 202200으로 바뀌고, 한 라운드를 끝까지 돌린 뒤 실제 outcome을 반환합니다. 거절 쪽 상태 코드는 하나도 바뀌지 않았습니다.


📦 ghcr.io/xwteam/agnes2api:0.2.2 · 📄 CHANGELOG

v0.2.1

Choose a tag to compare

@xwteam xwteam released this 09 Sep 14:24

一次发版后的收口。 v0.2.0 发出去之后做了一轮独立审计,查出来的问题当时修在了 tag 之后 —— 也就是说 v0.2.0 那个 tag、那份 GHCR 镜像、按 tag 读到的代码,全都还带着下面这些毛病。这一版把它们真正发出去。

从 v0.2.0 升级不需要迁移:没有新增或删除配置项,端点与响应形态一处未动。行为面只有面板文案与文档的订正。

🔑 许可:LICENSE 恢复成纯 MIT(做许可扫描的下游请读这条)

尾部那段 Required Notice 只是把 MIT 头部已有的版权声明又抄了一遍,而它让 GitHub 把仓库判成 Other / NOASSERTION —— 连带 GHCR 镜像的 org.opencontainers.image.licenses 也是 NOASSERTION,与 README 徽章、package.json"license": "MIT" 三方对不上。企业 SBOM、docker scout 一类通常把 NOASSERTION 按「禁止使用」处理。v0.2.1 起镜像 label 是 MIT。


🇨🇳 中文

  • 面板板块数:README 说八个,实际是九个。 六份 README 的现状描述与功能清单都还写着旧的八项,而 v0.2.0 发出去的面板多了「API 密钥」。同一棵树里 CHANGELOG 说九、README 说八。(v0.1.0 那条历史行保持八个 —— 它当时确实是八个。)
  • 一批「话说得比事实满」的订正,每一条都能在仓内找到自相矛盾的对手证据:
    • 隐私声明是全称假话:五语言文档写「铸 key 结束后(无论成功还是失败)临时邮箱都会被删除」,而删除是尽力而为 —— 删不掉时会留到上游自己回收,同一份文件往上一百多行就写着这件事。
    • 「近 24 小时」实际只覆盖当前 UTC 日历日。UTC 刚过零点时它只覆盖几分钟却写着「24 小时」,可以少报接近 24 倍;API 密钥板块每张卡上更没有任何区间披露。标签改成「今天(UTC)」,与 3 天 / 7 天 / 30 天 的天数口径一致。
    • 「这一次没验凭据:验凭据那一步要用到一个可用域名」对其中一条通道是假的 —— 那条通道验凭据压根不用域名。跳过是装配层一刀切的策略,不是通道的要求。
    • 「立即补池」确认框把邮箱消耗写死等于 key 数,只在 MAX_DOMAIN_ATTEMPTS=1 的默认值下成立。这颗确认框存在的唯一理由就是给出消耗上界,调大之后上界会偏低。
    • 第三档退避横幅「换一条邮箱通道逃不掉」,把同一段自己说「还开着」的另一种读法一刀切掉了 —— 在那条读法下换通道换的正是整套域名,可能真的有用。
  • 文档里的 /health 示例差了两个版本(26 处仍写 0.1.0),而没有任何东西盯着它。改到现值,并补判据VERSION 现算钉住 —— 手写字面量的话它自己就是下一笔同样的欠账。
  • 「改一处就得几处一起改」那张联动清单,四份互相打架:一份漏了这个文件、另一份漏了那个,照任一份走都会漏掉一个。统一成同一张五处清单。

🇭🇰 繁體中文

  • README 的「八個板塊」改成實際的九個(多了「API 金鑰」)。
  • 一批「話說得比事實滿」的訂正:隱私聲明的全稱承諾(刪除是盡力而為)、「近 24 小時」實為當前 UTC 日曆日、驗憑證被跳過的理由、補池確認框的消耗上界、退避橫幅一刀切掉的另一種讀法。
  • 26 處差了兩個版本的 /health 範例,並補測試從 VERSION 現算釘住。
  • 四份互相打架的聯動清單統一成一張五處清單。

🇺🇸 English

  • README said eight panel sections; there are nine (API keys was added in v0.2.0). CHANGELOG said nine and README said eight in the same tree.
  • A batch of claims that said more than the code did, each with contradicting evidence inside the repo: the privacy note promised the throwaway mailbox is always deleted (deletion is best-effort); "Last 24h" actually covers the current UTC calendar day (just past UTC midnight it can under-report by nearly 24×); the reason given for skipping credential verification is false for one of the channels; the refill confirmation hard-codes mailbox cost to key count (only true at the default MAX_DOMAIN_ATTEMPTS=1); the third-tier backoff banner rules out switching channels using an argument that only holds under one of the two readings it says are still open.
  • 26 /health samples in the docs were two versions stale, with nothing watching them. Fixed, and pinned to VERSION by a gate that computes it from source.
  • The "change one, change all N" roster was written four times and the four lists disagreed — following any one of them would miss a file. Unified.

🇯🇵 日本語

  • README の「8 セクション」を実際の 9 に(v0.2.0 で「API キー」が増えていました)。
  • 実装より多くを主張していた文言を一式訂正:一時メールボックスの削除はベストエフォート(常に削除されるとは限りません)、「直近 24 時間」は実際には現在の UTC 暦日、認証情報の検証をスキップした理由が一方のチャネルでは事実と異なる、補充確認ダイアログのメールボックス消費数、バックオフ第 3 段のバナーが切り捨てていたもう一方の読み方。
  • 2 バージョン古かった /health の例 26 か所を現値に(VERSION から現算するゲートで固定)。
  • 四つに分かれて食い違っていた「まとめて直すべき箇所」の一覧を一本化。

🇰🇷 한국어

  • README의 «여덟 개 섹션»을 실제인 아홉 개로(v0.2.0에서 «API 키»가 추가됨).
  • 구현보다 더 많이 주장하던 문구 일괄 정정: 임시 메일함 삭제는 최선 노력(항상 삭제되는 것은 아님), «최근 24시간»은 실제로 현재 UTC 달력일, 자격 증명 검증을 건너뛴 이유가 한쪽 채널에서는 사실과 다름, 보충 확인창의 메일함 소비량, 세 번째 백오프 배너가 잘라낸 다른 해석.
  • 두 버전 뒤처져 있던 /health 예시 26곳을 현재 값으로(VERSION에서 현산하는 게이트로 고정).
  • 넷으로 갈라져 서로 어긋나던 «함께 고쳐야 할 곳» 목록을 하나로 통합.

完整变更见 CHANGELOG.md 的 [0.2.1] 一节

v0.2.0

Choose a tag to compare

@xwteam xwteam released this 09 Sep 10:07

注册机大修,含破坏性变更 —— 用了注册机的部署升级前请读「破坏性变更」那一节。 两条邮箱通道从「主备自动降级」改成二选一;补池撞上上游限流不再继续尝试,而是当场中止整轮、按档指数退避,并记住被上游屏蔽过的域名;面板「测试连接」现在真的验一次凭据,不再只读域名列表。

没用注册机的部署(只做四协议转发)不受影响:四条转发协议的端点与响应形态一处未动
管理侧则新增了一组端点(见下面「新增」那一节的对外 API 密钥),按 500 报警的监控不用改,
但管理面多了五条路由。镜像照旧由推 v* 标签发到 ghcr.iolinux/amd64 + linux/arm64)。

⚠️ 破坏性变更

registrar.primary / registrar.fallback 合成 registrar.channel 存量配置读得懂,不用手工迁移;被丢掉的那条通道会在面板与事件里点名说出来,不静默。环境变量 REGISTRAR_PRIMARY / REGISTRAR_FALLBACK 的处置与迁移提示见 docs/*/REGISTRAR.md

自动降级整套拆掉。 从前主通道遇到通道级失败会自动切到备通道;现在选中的那条负责收验证码,另一条填了也不会被用到。这是行为变更,不是配置改名。


✨ 新增:对外 API 密钥(本版最大的新增,上一版正文把它整个漏掉了)

面板多了第九个板块「API 密钥」,可以给下游客户端签发 sk- 子密钥,不必再把网关主口令发出去。

  • 五条端点:GET / POST /admin/api/apikeysPATCH / DELETE /admin/api/apikeys/:id,以及清理失效。
  • 只存 SHA-256 摘要,从不存明文;列表里只回掩码与末四位。
  • 支持命名、停用、设到期;写操作要带 version 做乐观并发(过期回 409)。
  • 主口令仍然永远有效,且验证它一次存储读都不产生 —— 密钥表读不出来时它照样能用。
  • ⚠️ 停用或删除之后旧密钥会短暂仍可用(实例缓存 TTL + KV 边缘缓存),这一点在面板与文档里都如实写了。

上一版 Release 正文里 apikey 出现 0 次,五种语言一致地漏掉了这一整块 —— 那是源稿的漏,不是翻译的漏。此处补上。


🇨🇳 中文

  • 两条通道是二选一,不是主备。 「角色」这个词本身就在暗示排名,而两条通道从设计上就完全平级。面板上每条通道现在带「使用中 / 未使用」标记 —— 没被选中的那条即便没配凭据,也不挡你启用注册机。
  • 补池不再把自己锁死。 上游那两道限流(边缘那层与应用层)的惩罚窗口都远比几秒长,而从前撞上之后的处置是「等 5 秒换个域名接着打」—— 每打一次就把窗口续一次,于是永远出不来。现在撞上当场中止整轮、按档指数退避;窗口没过去的那些轮次一次上游请求都不发。
  • 域名结论会被记住。 上游拒绝某个域名时给的是一个干净信号,从前只用来「换下一个」、不留痕,于是每一轮都从头把同一批坏域名再撞一遍。现在它进台账,判死要两跳、一轮最多学一条,避免一次误判把好域名永久排除。
  • 「测试连接」真的验一次凭据。 有的上游在列域名那一步压根不校验凭据 —— 凭据粘错时,那颗按钮以前照样报绿,而绿灯会让人直接排除「凭据有问题」这个方向。现在它在列完域名之后真的拿那把凭据向上游要一次东西,被拒就报失败,归因是「凭据被拒」而不是「连不上」。代价如实登记:有的通道靠「建一个临时邮箱、验完立刻删掉」来验,那种通道上每点一次会占一个活跃邮箱名额;建出来却删不掉时会明说,不假装干净。
  • 一批「话说得比事实满」的订正。 横幅指向的事件在某些分支上根本发不出来、面板说明写着「不消耗任何名额」而实现会建临时邮箱、退避文案把「我们的词表没命中」说成「上游的事实」。每一条都配了会红的判据,不只是改文案。

🇭🇰 繁體中文

  • 兩條通道是二選一,不是主備。 面板上每條通道現在帶「使用中 / 未使用」標記;沒被選中的那條即便沒配憑證,也不擋你啟用註冊機。
  • 補池不再把自己鎖死。 上游兩道限流的懲罰視窗都遠比幾秒長,從前撞上之後「等 5 秒換個網域接著打」等於每打一次就把視窗續一次。現在撞上當場中止整輪、按檔指數退避。
  • 網域結論會被記住:判死要兩跳、一輪最多學一條,避免一次誤判把好網域永久排除。
  • 「測試連線」真的驗一次憑證,不再只讀網域清單(憑證貼錯時它以前照樣報綠)。代價如實登記:有的通道每點一次會佔一個活躍信箱名額。

🇺🇸 English

  • The two mailbox channels are now pick-one, not primary-with-fallback. Each channel carries an "in use / unused" marker in the panel; the one you did not pick does not block you from enabling the registrar even if it has no credentials.
  • Refilling no longer locks itself out. Both upstream rate limiters punish for far longer than a few seconds, and the old handling — wait five seconds, try the next domain — renewed the penalty window on every attempt. Hitting a limit now aborts the round on the spot and backs off exponentially per tier; rounds inside the window make no upstream requests at all.
  • Domain verdicts are remembered. A rejection is a clean signal; it used to be used only to move on, so every round rediscovered the same bad domains. It now goes into a ledger, and ruling a domain out takes two strikes, at most one learned per round, so a single misjudgement cannot permanently exclude a good domain.
  • "Test connection" really verifies the credentials. Some upstreams do not check credentials while listing domains at all — with a mistyped key the button used to report green anyway, and green makes people rule out the credentials. It now really spends the credentials against the upstream and reports failure when they are rejected. The cost is registered honestly: on channels that verify by creating a throwaway mailbox and deleting it, each click holds one active-mailbox slot; if the delete fails it says so rather than pretending it is clean.

🇯🇵 日本語

  • 2 本のメールボックスチャネルは「主系+フォールバック」ではなく、2 つから 1 つを選ぶ方式になりました。 パネルでは各チャネルに「使用中 / 未使用」の印が付きます。
  • 補充が自分自身を締め出すことはなくなりました。 上流の 2 種類のレート制限はいずれも数秒よりはるかに長く罰し、従来の「5 秒待って次のドメインへ」は試すたびに窓を延長していました。今は制限に当たった時点でそのラウンドを打ち切り、段階ごとに指数バックオフします。
  • ドメインの判定を記憶します。 判定を確定させるには 2 回必要で、1 ラウンドで学習するのは最大 1 件です。
  • 「接続テスト」は実際に認証情報を検証します。 貼り間違えた鍵でも以前は緑と表示されていました。使い捨てメールボックスを作って削除する方式のチャネルでは、1 回ごとにアクティブ枠を 1 つ使います。

🇰🇷 한국어

  • 두 메일함 채널은 "주 채널 + 대체"가 아니라 둘 중 하나를 고르는 방식입니다. 패널에서 각 채널에 "사용 중 / 미사용" 표시가 붙습니다.
  • 보충이 스스로를 잠가 버리지 않습니다. 업스트림의 두 속도 제한은 모두 몇 초보다 훨씬 길게 벌하는데, 기존의 "5초 기다렸다 다음 도메인" 방식은 시도할 때마다 창을 연장하고 있었습니다. 이제는 제한에 걸리면 그 라운드를 즉시 중단하고 단계별로 지수 백오프합니다.
  • 도메인 판정을 기억합니다. 확정에는 두 번이 필요하고, 한 라운드에서 배우는 것은 최대 한 건입니다.
  • "연결 테스트"가 실제로 자격 증명을 검증합니다. 키를 잘못 붙여 넣어도 예전에는 초록으로 나왔습니다. 일회용 메일함을 만들고 지우는 방식의 채널에서는 한 번 누를 때마다 활성 정원 하나를 차지합니다.

完整变更见 CHANGELOG.md 的 [0.2.0] 一节

v0.1.1

Choose a tag to compare

@xwteam xwteam released this 31 Aug 13:23

一次整备版,行为面没有改动。 这一版改到的是注释、文案与门禁判据。实测:把 v0.1.0 之后改动过的 91 份源码 / 面板文件(src/admin-ui/ 下全部 .ts / .js / .mjs / .css)逐份抠掉注释再对拍,只有三份仍有差异 —— 两处是字符串里的说明文字(上游事实表里一条「假设」的描述,以及面板产物内嵌的注释本身),第三处是版本号常量本身(src/version.ts,发版本来就要改它)。没有一处逻辑改动。

从 v0.1.0 升级不需要迁移:没有新增或删除配置项,端点与响应形态一处未动。Docker 侧换镜像标签重新拉起即可,Worker 侧重新部署即可。镜像照旧由推 v* 标签发到 ghcr.iolinux/amd64 + linux/arm64)。


🇨🇳 中文

  • 面板资源里那 470 处内部编号清掉了 —— 这是唯一真正外泄的一块。 admin-ui/ 不是只给开发者看的目录:构建脚本把它逐字节烧进产物,服务端再把那份字节当 /admin/js/*.js/admin/css/*.css 的响应体发出去。也就是说这些注释里的内部编号,每一个打开面板的访客都会收到curl 一下就能读到。这 470 处分两轮才清完:第一轮的正则吃不到「数字后面还跟一个小写字母」那种形态,漏下的 4 处第二轮补上,而那四处也都已经烧进了产物。逐处换成描述性说法,一句都没删。今天这棵树上判据真扫的结果是 0。
  • 一条排版上的豁免,被静静升级成了泄漏上的豁免。 「出货文档不许留内部研发轨迹标识符」这条判据,射程一直借用的是排版那根轴的 40 份文档集。而 admin-ui/README.md 之所以不在那 40 份里,唯一依据是一条排版登记 ——「让它套统一的章节骨架毫无意义」。泄漏这根轴一个字都没豁免过它。两根轴共用一份射程的后果是结构性的:那份自述永远不会被检查,它里面 11 处标识符一处都扫不到,而门禁全绿。修法是给泄漏轴单开一份射程(44 份:出货文档全集 + 面板那份自述 + .github 下全部 .md,都从磁盘现算),排版轴那 40 份一个字不动,并各钉一格盯住「哪根轴该用哪份射程」。
  • 仓库其余各处按同一把尺清掉了绝大部分。 网关源码 501 处、测试 1854 处、门禁脚本 121 处、推送前那份逐格表 104 处、仓根四份配置 7 处、40 份出货文档 116 处,以及 222 条提交信息(435 条全扫,只改信息不动树)。这些数是用哪把尺量的:判据里登记的四族零宽前后瞻正则,对每一批清理各自的清理前那棵树逐份现扫,排除控制字符分区名(与发现号同形的真词)与二进制文件;各条提交标题里的处数是当时用别的写法数的,与这里对不上,以这里为准。别把它读成清零:出货文档那一族今天确实是 0(有判据每次推送真扫在守),网关源码与面板资源也是 0,但测试里还剩 142 处、推送前那份逐格表还剩 41 处、提交信息还剩 14 处 —— 剩下的是判据自己用来自证的夹具与同形真词,逐条登记在对应提交里。一处都不是删了事:这些编号绝大多数在做指代,删掉编号那句话就指不到东西了,所以逐处换成说得清那件事本身的说法,行数一行没多一行没少。顺带查实了两件事:那条判据原来的射程只有一份文档,其余 39 份出货文档从来没人验过;而当初报的「91 处」实测是 116 处,差额来自一种会把边界字符一起吃掉的 grep 写法 —— 同一串里连写的两个标识符,它只数得到一个。这条机制已经加了一格夹具钉死。
  • 三格测试一直卡在默认超时的边界上。 它们各自完整跑一遍「注释里的指向必须解析得开」那道门禁,实测要 2.6–5.8 秒,而测试框架的默认超时恰好是 5 秒 —— 也就是说这三格一直在掷骰子,仓库越大越容易红。只给了显式的时间预算,断言一个字没动:一条会因为无关原因变红的判据,会训练人忽略它。

🇺🇸 English

  • 470 internal tracking identifiers removed from the admin panel's assets — the only part that was genuinely leaking. admin-ui/ is not a developer-only directory: the build script inlines it byte for byte into the shipped artifact, and the server then hands those bytes back as the body of /admin/js/*.js and /admin/css/*.css. Every visitor who opens the panel was receiving those identifiers, and a single curl was enough to read them. It took two passes: the first pattern did not match identifiers with a trailing lowercase letter, so four were left behind and picked up on the second pass — and those four had been baked into the artifact as well. Each one was rewritten into a description of the thing it referred to — no sentence was dropped. A real scan of that tree today returns zero.
  • An exemption granted on typography grounds had silently been promoted into an exemption from leak checking. The rule "shipped documents must not carry internal development-tracking identifiers" had been borrowing its scope from the typography axis: a set of 40 documents. admin-ui/README.md is outside that set for exactly one reason — a typography ruling that says forcing it into the shared section skeleton would be pointless. The leak axis had never exempted it from anything. Sharing one scope between two axes has a structural consequence: that document could never be checked, its 11 identifiers were invisible to the rule, and the gate stayed green throughout. The fix gives the leak axis its own scope (44 documents: every shipped document, plus the panel's own README, plus every .md under .github, all enumerated from disk), leaves the typography scope untouched, and pins each axis to its own scope with a dedicated check.
  • Most of the rest of the repository was cleaned with the same ruler. 501 occurrences in the gateway sources, 1854 in the tests, 121 in the gate scripts, 104 in the pre-push checklist, 7 in four repository-root config files, 116 across the 40 shipped documents, and 222 commit messages (all 435 commits were scanned; only messages were rewritten, the trees were left untouched). Which ruler produced those numbers: the four zero-width-lookaround patterns registered in the rule itself, run over the pre-cleanup tree of each batch, file by file, excluding the two control-character block names (real words that share the shape of a finding number) and binary files; the counts in the individual commit subjects were measured with other forms and do not agree with these — these are the ones to trust. Do not read this as zero: the shipped documents really are at zero (a rule scans all of them on every push), and so are the gateway sources and the panel assets, but 142 remain in the tests, 41 in the pre-push checklist, and 14 in the commit messages — those are the fixtures the rule uses to prove itself, plus real words of the same shape, each one logged in the commit that did the work. None of it was a plain deletion: most of these identifiers were acting as cross-references, so deleting the number would have left the sentence pointing at nothing. Each was replaced with wording that names the thing itself, and the line counts came out identical. Two facts fell out of the audit: the rule's original scope was a single document, which means the other 39 shipped documents had never been checked at all; and the "91 occurrences" reported earlier is actually 116 — the difference comes from a grep form that consumes its own boundary characters, so two identifiers written back to back in one string are only counted once. That mechanism now has a fixture holding it down.
  • Three test cases had been sitting right on the default timeout boundary. Each of them runs the "every path referenced in a comment must resolve" gate end to end, which measures at 2.6–5.8 seconds, while the test runner's default timeout is exactly 5 seconds — so those three cells were a coin flip, and the larger the repository grew the more likely they were to fail. They were given an explicit time budget and not one assertion was touched: a check that goes red for reasons unrelated to what it guards trains people to ignore it.

🇯🇵 日本語

  • 管理パネルの配信物に混ざっていた社内向け識別子 470 か所を削除しました —— 実際に外部へ漏れていたのはここだけです。 admin-ui/ は開発者だけが見るディレクトリではありません。ビルドスクリプトがこのディレクトリを1 バイトずつ成果物へ埋め込み、サーバーはそのバイト列をそのまま /admin/js/*.js/admin/css/*.css の本文として返します。つまりパネルを開いた訪問者全員がこれらの識別子を受け取っており、curl 一回で読めてしまう状態でした。清掃は 2 回に分かれました。1 回目の正規表現が「数字のうしろに小文字が 1 つ続く」形を拾えず、取り残した 4 か所を 2 回目で拾っています。その 4 か所も成果物に焼き込まれていました。いずれも指している対象そのものを説明する言い回しへ書き換えており、文を削ってはいません。今日このツリーを実際に走査すると 0 件です。
  • 組版上の除外が、いつの間にか漏洩チェックの除外に格上げされていました。 「出荷ドキュメントに社内の開発履歴を示す識別子を残さない」というルールは、組版軸の適用範囲(40 件のドキュメント)をそのまま借りていました。admin-ui/README.md がその 40 件に入っていない根拠は、「共通の章立てを当てはめても意味がない」という組版側の裁定ただ一つです。漏洩軸はこの文書を一度も除外していません。2 つの軸が 1 つの適用範囲を共有した結果は構造的でした。その文書は永久に検査されず、中にあった 11 か所の識別子はルールから見えないまま、ゲートは緑のままだったのです。修正として漏洩軸に専用の適用範囲(44 件:出荷ドキュメント全部+パネルの README+.github 配下の全 .md、いずれもディスクから都度算出)を与え、組版側の 40 件には一切手を入れず、「どちらの軸がどちらの範囲を使うか」を各軸ごとに 1 ケースで固定しました。
  • リポジトリの残りも同じ物差しでおおむね洗いました。 ゲートウェイのソース 501 か所、テスト 1854 か所、ゲートスクリプト 121 か所、プッシュ前チェックリスト 104 か所、リポジトリ直下の設定 4 ファイル 7 か所、出荷ドキュメント 40 件で 116 か所、そしてコミットメッセージ 222 件(全 435 件を走査し、メッセージのみを書き換え、ツリーには触れていません)。この数はどの物差しで測ったか:ルール自身に登録された 4 族のゼロ幅前後読み正規表現を、各バッチの清掃前ツリーに対してファイル単位で走らせ、制御文字ブロック名(発見番号と同じ形をした実在の語)とバイナリを除外したものです。個々のコミット件名の数値は当時別の書き方で数えたもので、ここの数とは一致しません。ここの数を正としてくださいゼロと読まないでください:出荷ドキュメントは実際に 0 件で(プッシュのたびにルールが全件を走査しています)、ゲートウェイのソースとパネル配信物も 0 件ですが、テストに 142 か所、プッシュ前チェックリストに 41 か所、コミットメッセージに 14 か所が残っています。いずれもルールが自らを検証するためのフィクスチャと、同じ形をした実在の語で、対応するコミットに 1 件ずつ記録してあります。単に消したものは一つもありません。これらの識別子の大半は相互参照として働いていたため、番号を消すと文が何も指さなくなります。そこで対象そのものを名指しする表現へ置き換え、行数は前後で同一になりました。監査の副産物として 2 つの事実が判明しています。このルールの当初の適用範囲は 1 ファイルだけで、残る 39 件の出荷ドキュメントは一度も検査されていなかったこと。そして当初「91 か所」と報告した数は実測 116 か所であったこと —— 差は、境界文字まで一緒に消費してしまう grep の書き方に由来します。1 つの文字列に識別子が続けて 2 つ書かれていると、1 つしか数えられません。この仕組みは専用のフィクスチャで固定済みです。
  • 3 つのテストが既定のタイムアウト境界上に居座っていました。 どれも「コメント中の参照はすべて解決できること」というゲートを丸ごと 1 回走らせるもので、実測 2.6–5.8 秒。テストランナーの既定タイムアウトはちょうど 5 秒です。つまりこの 3 ケースはサイコロを振っている状態で、リポジトリが大きくなるほど赤くなりやすくなっていました。明示的な時間予算を与えただけで、アサーションには一切触れていません。守るべき対象と無関係な理由で赤くなるチェックは、人に無視する癖をつけさせるからです。

🇰🇷 한국어

  • 관리 패널 배포물에 섞여 있던 내부 식별자 470곳을 지웠습니다 — 실제로 밖으로 새고 있던 곳은 여기뿐입니다. admin-ui/는 개발자만 보는 디렉터리가 아닙니다. 빌드 스크립트가 이 디렉터리를 바이트 단위 그대로 산출물에 심고, 서버는 그 바이트를 /admin/js/*.js/admin/css/*.css의 본문으로 그대로 돌려줍니다. 즉 패널을 연 방문자 전원이 이 식별자들을 받아 갔고, curl 한 번이면 읽혔습니다. 청소는 두 번에 나뉘었습니다. 첫 번째 정규식이 "숫자 뒤에 소문자 한 글자가 붙는" 형태를 잡지 못해 남은 4곳을 두 번째에 걷어냈고, 그 4곳도 이미 산출물에 구워져 있었습니다. 모두 가리키던 대상 자체를 설명하는 표현으로 바꿨고, 문장을 지우지는 않았습니다. 오늘 이 트리를 실제로 훑으면 0곳입니다.
  • 조판 축에서 내준 예외가 조용히 유출 축의 예외로 승격돼 있었습니다. "출고 문서에 내부 개발 흔적 식별자를 남기지 않는다"는 판정 기준은 그동안 조판 축의 사정거리(문서 40벌)를 그대로 빌려 쓰고 있었습니다. admin-ui/README.md가 그 40벌에 없는 근거는 "공통 목차 골격을 씌워 봐야 의미가 없다"는 조판 쪽 결정 하나뿐입니다. 유출 축은 이 문서를 단 한 글자도 면제한 적이 없습니다. 두 축이 사정거리 하나를 공유한 결과는 구조적이었습니다. 그 문서는 영원히 검사되지 않았고, 그 안의 식별자 11곳은 기준의 시야 밖에 있었으며, 게이트는 내내 초록이었습니다. 고친 방법은 유출 축에 전용 사정거리(44벌: 출고 문서 전부 + 패널의 README + .github 아래 모든 .md, 전부 디스크에서 즉시 산출)를 주고, 조판 쪽 40벌은 한 글자도 건드리지 않은 뒤, "어느 축이 어느 사정거리를 쓰는지"를 축마다 한 칸씩으로 못 박은 것입니다.
  • 저장소의 나머지도 같은 자로 대부분 훑었습니다. 게이트웨이 소스 501곳, 테스트 1854곳, 게이트 스크립트 121곳, 푸시 전 점검표 104곳, 저장소 최상위 설정 파일 4벌의 7곳, 출고 문서 40벌의 116곳, 그리고 커밋 메시지 222건(전체 435건을 훑었고 메시지만 고쳤을 뿐 트리는 건드리지 않았습니다). 이 숫자는 어떤 자로 쟀는가: 기준 자체에 등록된 네 갈래 제로폭 전후탐색 정규식을, 각 정리 작업의 정리 직전 트리에 대해 파일 단위로 돌리고, 제어문자 블록 이름(발견 번호와 같은 형태의 실제 낱말)과 바이너리를 제외한 값입니다. 개별 커밋 제목의 숫자는 당시 다른 방식으로 센 것이라 여기와 맞지 않으니 여기 숫자를 기준으로 삼아 주세요. 0으로 읽지 마세요: 출고 문서는 실제로 0곳이고(푸시할 때마다 기준이 전부를 훑습니다) 게이트웨이 소스와 패널 배포물도 0곳이지만, 테스트에 142곳, 푸시 전 점검표에 41곳, 커밋 메시지에 14곳이 남아 있습니다. 남은 것들은 기준이 스스로를 증명하는 데 쓰는 픽스처와 같은 형태의 실제 낱말이며, 해당 커밋에 한 건씩 기록해 두었습니다. 그냥 지운 곳은 한 군데도 없습니다. 이 번호들은 대부분 상호 참조 역할을 하고 있어서, 번호를 지우면 그 문장이 아무것도 가리키지 못하게 됩니다. 그래서 대상 자체를 짚어 주는 표현으로 바꿨고 줄 수는 앞뒤가 같습니다. 점검 과정에서 두 가지가 함께 드러났습니다. 이 기준의 원래 사정거리는 문서 한 벌뿐이어서 나머지 39벌은 한 번도 검사된 적이 없었다는 것, 그리고 처음 보고한 "91곳"이 실측 116곳이라는 것입니다. 차이는 경계 문자까지 같이 먹어 버리는 grep 작성법에서 나옵니다. 한 문자열에 식별자 둘이 잇달아 적혀 있으면 하나만 세어집니다. 이 메커니즘은 전용 픽스처로 못 박아 뒀습니다.
  • 테스트 세 칸이 기본 타임아웃 경계에 걸터앉아 있었습니다. 셋 다 "주석 안의 참조는 모두 해석돼야 한다"는...
Read more

v0.1.0

Choose a tag to compare

@xwteam xwteam released this 31 Aug 04:48

首个版本 —— 没有可升级的旧版本,从这一版开始装即可。

这也是本仓第一次推 v* 标签,ghcr.io/xwteam/agnes2api 的镜像随这一版第一次出现(linux/amd64 + linux/arm64)。


🇨🇳 中文

  • 一个服务同时说四种协议openai(Chat Completions)、anthropic(Messages)、responses(OpenAI Responses)、gemini(generateContent)。四条入站协议共用同一套上游调度、同一个 key 池、同一份失败归因,流式在四条上都支持。鉴权闸接受 Authorization: Bearerx-api-keyx-goog-api-key、查询参数 ?key= 四条凭据通道 —— 正好覆盖各协议官方 SDK 默认发送的那一种,只换基址就能直连,要换掉的只是值:任何通道里传的都是本网关的口令,不是上游厂商的密钥。
  • 同一份代码跑两种形态:Cloudflare Worker(池索引落在 KV 上)与 Node / Docker(落在单文件 JSON 上)。Worker 那条路在 README 上有一颗一键部署按钮,按完补上 wrangler.toml 的 KV 命名空间 id 与 GATEWAY_TOKEN 就能起;Docker 那条路直接 docker compose up,镜像在 ghcr.iolinux/amd64linux/arm64 都有。
  • 注册机把「攒 key」这件事自动化了moemailyyds 两条临时邮箱通道,从收码到入池全自动,两条通道在文案与取数顺序上严格平级,谁也不是「主选项」。它默认关闭REGISTRAR_ENABLED 判的是逐字的 true),开了而凭据缺失则直接失败,不会半开着跑。
  • 管理面板零构建/admin 下八个板块 —— 概览、Key 池、注册机、事件、用量、模型、调试台、设置。admin-ui/ 原样挂上去就是可调试的面板,构建脚本只把它逐字节烧进产物。ADMIN_TOKEN 没设时整棵 /admin 都不注册;口令只走 x-admin-key 请求头,不落 Cookie、不进 query。
  • 文档五语言各一份:README / ADMIN / API / DEPLOY / REGISTRAR / SPONSORS / USAGE 都有 zh-CN / zh-TW / en / ja / ko 五份,面板界面同样五语言。仓库零内置凭据,MIT 许可。
  • 一条要先说清楚的限制:本仓至今零份真上游样本。上游事实表里每一条的状态都是「假设」,契约测试里的上游全是桩。所以「示例请求在本仓这份服务上调得通」不等于「上游真的接受它」—— 拿这份协议目录去对接真上游之前,请自行核对。

🇺🇸 English

  • One service, four protocols: openai (Chat Completions), anthropic (Messages), responses (OpenAI Responses) and gemini (generateContent). All four inbound protocols share one upstream scheduler, one key pool and one failure-attribution path, and streaming works on all four. The auth gate accepts four credential channels — Authorization: Bearer, x-api-key, x-goog-api-key and the ?key= query parameter — which covers whatever each vendor's official SDK sends by default, so you only change the base URL. What you do change is the value: every channel expects this gateway's token, never a vendor key.
  • The same code runs in two shapes: a Cloudflare Worker (pool index lives in KV) and Node / Docker (pool index lives in a single JSON file). The Worker path has a one-click deploy button in the README; after clicking it you still supply the KV namespace id in wrangler.toml and the GATEWAY_TOKEN. The Docker path is docker compose up, with images on ghcr.io for both linux/amd64 and linux/arm64.
  • The registrar automates key acquisition: two temporary-mailbox channels, moemail and yyds, take it from receiving the code to landing the key in the pool without a human in the loop. The two channels are strict equals in wording and in read order — neither is "the recommended one". It ships disabled (REGISTRAR_ENABLED must be the literal string true), and enabling it without credentials fails loudly instead of running half-configured.
  • The admin panel needs no build step: eight sections under /admin — overview, key pool, registrar, events, usage, models, playground and settings. admin-ui/ served as-is is the debuggable panel; the build script only inlines it byte-for-byte into the shipped artifact. With no ADMIN_TOKEN set, the entire /admin tree is never registered; the token travels only in the x-admin-key header, never in a cookie and never in the query string.
  • Documentation ships once per language: README / ADMIN / API / DEPLOY / REGISTRAR / SPONSORS / USAGE each exist in zh-CN, zh-TW, en, ja and ko, and the panel UI is translated to the same five. No credentials are baked into the repository. MIT licensed.
  • One limitation to state up front: this repository has no captured samples from a real upstream. Every row in the upstream fact table is marked assumed, and the upstreams in the contract tests are all stubs. So "the example request works against this app" is not the same as "the upstream accepts it" — verify for yourself before you point this protocol catalogue at a real upstream.

🇯🇵 日本語

  • 1 つのサービスが 4 つのプロトコルを話しますopenai(Chat Completions)、anthropic(Messages)、responses(OpenAI Responses)、gemini(generateContent)。4 本の受信プロトコルは同じ上流スケジューラ、同じ key プール、同じ失敗の切り分けを共有し、ストリーミングは 4 本すべてで使えます。認証ゲートは Authorization: Bearerx-api-keyx-goog-api-key、クエリパラメータ ?key= の 4 経路を受け付けます。各プロトコルの公式 SDK が既定で送るものがそのまま通るので、変更はベース URL だけです。ただし値は変わります。どの経路でも渡すのは本ゲートウェイのトークンであって、上流ベンダーのキーではありません。
  • 同じコードが 2 つの形態で動きます:Cloudflare Worker(プール索引は KV)と Node / Docker(プール索引は単一 JSON ファイル)。Worker 側は README にワンクリックデプロイのボタンがあり、押した後に wrangler.toml の KV ネームスペース id と GATEWAY_TOKEN を入れれば起動します。Docker 側は docker compose up で、イメージは ghcr.io にあり linux/amd64linux/arm64 の両方を用意しています。
  • レジストラーが key 集めを自動化しますmoemailyyds の 2 本の一時メールボックス経路が、コード受信からプール投入までを人手なしで完了させます。2 本は文面でも読み出し順でも厳密に対等で、どちらかが「推奨」ということはありません。既定では無効REGISTRAR_ENABLED は文字列 true のみを有効と判定)で、資格情報なしで有効化すると中途半端に動かず即座に失敗します。
  • 管理パネルはビルド手順が要りません/admin の下に 8 つのセクション(概要、key プール、レジストラー、イベント、使用量、モデル、プレイグラウンド、設定)。admin-ui/ はそのまま配信すればデバッグ可能なパネルそのもので、ビルドスクリプトはそれを 1 バイトずつ成果物へ埋め込むだけです。ADMIN_TOKEN が未設定なら /admin の木は一切登録されません。トークンは x-admin-key ヘッダーのみを通り、Cookie にもクエリにも入りません。
  • ドキュメントは 5 言語に各 1 部:README / ADMIN / API / DEPLOY / REGISTRAR / SPONSORS / USAGE がいずれも zh-CN / zh-TW / en / ja / ko で揃い、パネルの UI も同じ 5 言語です。リポジトリに資格情報は一切埋め込まれていません。ライセンスは MIT。
  • 先に述べておく制限が 1 つ:本リポジトリには実際の上流から取った標本が 1 つもありません。上流事実表の各行はすべて「仮定」であり、契約テストの上流もすべてスタブです。したがって「サンプルリクエストがこのアプリで通る」ことは「上流が受け付ける」ことと同じではありません。このプロトコル目録を実際の上流に向ける前に、ご自身で確認してください。

🇰🇷 한국어

  • 서비스 하나가 프로토콜 넷을 말합니다: openai(Chat Completions), anthropic(Messages), responses(OpenAI Responses), gemini(generateContent). 네 갈래 인바운드 프로토콜은 같은 업스트림 스케줄러, 같은 key 풀, 같은 실패 원인 판별을 공유하며 스트리밍도 네 갈래 모두에서 됩니다. 인증 게이트는 Authorization: Bearer, x-api-key, x-goog-api-key, 쿼리 파라미터 ?key= 네 경로를 받습니다. 각 프로토콜 공식 SDK가 기본으로 보내는 방식이 그대로 통하므로 바꿀 것은 베이스 URL 하나뿐입니다. 다만 값은 다릅니다. 어느 경로로 보내든 이 게이트웨이의 토큰이지 업스트림 업체의 키가 아닙니다.
  • 같은 코드가 두 가지 형태로 돕니다: Cloudflare Worker(풀 색인은 KV에)와 Node / Docker(풀 색인은 단일 JSON 파일에). Worker 쪽은 README에 원클릭 배포 버튼이 있고, 누른 뒤 wrangler.toml의 KV 네임스페이스 id와 GATEWAY_TOKEN만 채우면 뜹니다. Docker 쪽은 docker compose up이며 이미지는 ghcr.iolinux/amd64linux/arm64 둘 다 올라갑니다.
  • 레지스트라가 key 확보를 자동화합니다: moemailyyds 두 임시 메일함 경로가 인증 코드 수신부터 풀 투입까지 사람 손 없이 처리합니다. 두 경로는 문구에서도 읽는 순서에서도 엄격히 대등해서 어느 쪽도 "권장"이 아닙니다. 기본값은 꺼짐(REGISTRAR_ENABLED은 문자열 true만 켠 것으로 봅니다)이고, 자격 증명 없이 켜면 어정쩡하게 도는 대신 곧바로 실패합니다.
  • 관리 패널에 빌드 단계가 없습니다: /admin 아래 여덟 개 섹션(개요, key 풀, 레지스트라, 이벤트, 사용량, 모델, 플레이그라운드, 설정). admin-ui/를 그대로 서빙하면 그것이 곧 디버깅 가능한 패널이고, 빌드 스크립트는 그걸 산출물에 바이트 단위로 심을 뿐입니다. ADMIN_TOKEN이 없으면 /admin 트리 전체가 아예 등록되지 않습니다. 토큰은 x-admin-key 헤더로만 다니고 쿠키에도 쿼리에도 남지 않습니다.
  • 문서는 5개 언어로 각각 한 벌씩: README / ADMIN / API / DEPLOY / REGISTRAR / SPONSORS / USAGE 모두 zh-CN / zh-TW / en / ja / ko가 갖춰져 있고 패널 UI도 같은 다섯 언어입니다. 저장소에 내장된 자격 증명은 하나도 없습니다. 라이선스는 MIT.
  • 먼저 밝혀 둘 한계 하나: 이 저장소에는 실제 업스트림에서 받은 표본이 하나도 없습니다. 업스트림 사실 표의 모든 행은 상태가 "가정"이고 계약 테스트의 업스트림도 전부 스텁입니다. 그래서 "예제 요청이 이 앱에서 통한다"는 것은 "업스트림이 받아들인다"와 같지 않습니다. 이 프로토콜 목록을 실제 업스트림에 물리기 전에 직접 확인해 주세요.

🇹🇼 繁體中文

  • 一個服務同時說四種協定openai(Chat Completions)、anthropic(Messages)、responses(OpenAI Responses)、gemini(generateContent)。四條入站協定共用同一套上游排程、同一個 key 池、同一份失敗歸因,串流在四條上都支援。驗證閘接受 Authorization: Bearerx-api-keyx-goog-api-key、查詢參數 ?key= 四條憑證通道 —— 正好蓋住各協定官方 SDK 預設送出的那一種,只換基底網址就能直連,要換掉的只是值:任何通道裡傳的都是本閘道的權杖,不是上游廠商的金鑰。
  • 同一份程式碼跑兩種形態:Cloudflare Worker(池索引落在 KV 上)與 Node / Docker(落在單一 JSON 檔上)。Worker 那條路在 README 上有一顆一鍵部署按鈕,按完補上 wrangler.toml 的 KV 命名空間 id 與 GATEWAY_TOKEN 就能起;Docker 那條路直接 docker compose up,映像檔在 ghcr.iolinux/amd64linux/arm64 都有。
  • 註冊機把「攢 key」這件事自動化了moemailyyds 兩條臨時信箱通道,從收驗證碼到入池全自動,兩條通道在文案與取數順序上嚴格平級,誰都不是「主選項」。它預設關閉REGISTRAR_ENABLED 判的是逐字的 true),開了而憑證缺失則直接失敗,不會半開著跑。
  • 管理面板零建置/admin 底下八個板塊 —— 概覽、key 池、註冊機、事件、用量、模型、除錯台、設定。admin-ui/ 原樣掛上去就是可除錯的面板,建置指令稿只把它逐位元組燒進產出物。ADMIN_TOKEN 沒設時整棵 /admin 都不註冊;權杖只走 x-admin-key 請求標頭,不落 Cookie、不進查詢字串。
  • 文件五語言各一份:README / ADMIN / API / DEPLOY / REGISTRAR / SPONSORS / USAGE 都有 zh-CN / zh-TW / en / ja / ko 五份,面板介面同樣五語言。倉庫零內建憑證,MIT 授權。
  • 一條要先說清楚的限制:本倉至今零份真上游樣本。上游事實表裡每一條的狀態都是「假設」,契約測試裡的上游全是樁。所以「範例請求在本倉這份服務上呼叫得通」不等於「上游真的接受它」—— 拿這份協定目錄去對接真上游之前,請自行核對。