把本机 AStudio(讯飞星辰 / Astron Studio)的上游模型服务,逆向复刻成一个 OpenAI / Anthropic 兼容的 Go 网关。
复用桌面端已登录的会话凭据,无需另申请密钥;纯 Go 标准库实现,单文件约 9 MB, 零外部依赖。
┌──────────────┐ /v1/chat/completions ┌─────────────┐ Bearer <model key> ┌──────────────────────────────┐
│ 客户端 │ /v1/responses │ AStudio2API │ ───────────────────▶ │ maas-api.cn-huabei-1.xf-yun │
│ NewAPI / pi │ ───────────────────────▶ │ (本网关) │ ◀─────────────────── │ /v1/chat/completions │
│ Claude Code │ /v1/messages │ │ SSE 流式 │ /v1/responses │
└──────────────┘ └──────┬──────┘ └──────────────────────────────┘
│ 自动读取
▼
AStudio Data/userdata/astron-session.json
AStudio Data/runtime/acode-home/config.toml
/v1/chat/completions、/v1/responses(Codex)原生透传/v1/messages完整 Anthropic 双向转换,含tool_use流式input_json_delta- 模型支持别名、slug、目录 ID 三种写法,可自定义别名
- 手机号验证码登录(GeeTest 由浏览器解题)
- 桌面端会话一键导入 / 手动录入凭据
- 多账号轮询,按积分余额降权,401 / 429 / 5xx 有界重试换号
- Bearer 凭据定时保鲜,失效即时重取重放
- 积分、会员、待领弹窗总览
- 代领每日签到、运营弹窗、客户端下载奖励、Beta 资格,支持兑换码
- 真实 token 用量,含缓存命中与思考 token
- 请求日志只记元信息,不存对话内容
- 单页控制台:账号 / 密钥 / 模型 / 统计 / 日志 / 权益 / 设置
docker compose up -d即可运行- 状态单文件持久化,配置项可用环境变量覆盖
- 无密钥时拒绝服务,CORS 默认仅放行本机,登录带指数退避限流
网关不替上游做裁决。上游返回什么状态码、什么错误体,就原样回给客户端:
代价是错误文案保持上游原样(包括英文),换取的是“所见即上游”的可诊断性。
AStudio 本身就是一个"Electron 外壳 + 本地 Go/Node 服务 + Acode 内核"的三层结构。
真正调用大模型的是最内层的 Acode 内核(@iflytek/astron-code-prod,一个 Codex 分支),
它只是往一个标准的 OpenAI 兼容上游发请求。
因此不需要伪造签名、不需要破解加密、不需要驱动桌面客户端——
只要拿到那一个 Bearer 凭据(桌面应用本来就以明文存在磁盘上),就能直接调用上游。
| 环节 | 逆向结果 |
|---|---|
| 登录 / 鉴权 | POST https://agent.xfyun.cn/xingchen-studio/...(短信 / 密码 / 微信 SSO) |
| 会话落盘 | <AStudio Data>/userdata/astron-session.json(明文 JSON) |
| 模型凭据换取 | GET {workspace}/bot/models/configs + Cookie → data[].api_key |
| 凭据格式 | <32位hex>:<base64>,即 modelBearerToken |
| 凭据投影 | 写入 <AStudio Data>/runtime/acode-home/config.toml 的 [model_providers.astron-spark] |
| 模型目录 | GET {modelsBase}/models → slug / 上下文窗口 / 思考档位 |
| 推理上游 | https://maas-api.cn-huabei-1.xf-yun.com/v1 |
| 推理协议 | wire_api = "responses",同时兼容 /chat/completions |
| 客户端标识 | clientType: 21(Windows) / 22(macOS)、studioVersion: 3.4.4 |
| 事实 | 文件 | 说明 |
|---|---|---|
| 上游 Base URL | apps/server/dist/index.mjs |
const ASTRON_MAAS_BASE_URL = "https://maas-api.cn-huabei-1.xf-yun.com/v1" |
| 凭据换取 | 同上 | fetchAstronModelCredential() → bot/models/configs |
| Cookie 构造 | 同上 | cookieHeader() → ssoSessionId / sso_sessionid / account_id / token |
| 内核注入 | 同上 | model_providers.astron-spark.* 一串 configOverrides |
| 客户端标识 | 同上 | resolveAstronStudioClientType() |
上游
/v1/chat/completions、/v1/responses、SSE 流式、tools、reasoning_effort、stream_options.include_usage全部已实测可用。/v1/models为空,模型清单要走bot/models/configs或model-manager/models。
模型目录由上游动态返回,随账号权益变化,下表为实测快照。
| 显示名 | Slug | 上下文 | 思考档位 | 来源 |
|---|---|---|---|---|
| Auto | astronclaw-auto |
1M | none / high |
桌面端目录 |
| Spark-X2.5 | spark-x2.5 |
256K | none / high |
桌面端目录 |
| GLM-5.2 | xopglm52 |
1M | none / high / max |
桌面端目录 |
| DeepSeek-V4-Pro | xopdeepseekv4pro0813 |
1M | none / high / max |
桌面端目录 |
| DeepSeek-V4-Flash | xopdsv4flash0731in |
1M | none / high / max |
桌面端目录 |
| DeepSeek-V4-Pro | xopdeepseekv4pro |
— | — | 账号配置 |
前五个来自
model-manager(桌面端实际展示的模型),第六个来自bot/models/configs。 同一模型在两处 slug 不同,网关都接受。
以下模型在 bot/models/configs 里可见,但账号缺少对应权益,上游直接拒绝:
| 显示名 | Slug | 倍率 | 上游响应 |
|---|---|---|---|
| GLM-5.1 | xopglm51 |
×2.0 | 403 账号无有效订单/额度 |
| Kimi-K2.6 | xopkimik26 |
×2.0 | 403 |
| MiniMax-M2.5 | xminimaxm25 |
×1.0 | 403 |
| Qwen3.6-35B-A3B | xopqwen36v35b |
×1.0 | 403 |
| Spark-X2-Agent | xsparkx2agent |
×2.0 | 403 |
| Spark-X2-Flash | spark-x |
×0.5 | 400 no category route found |
网关不替上游裁决,原样透传状态码与错误体,并在 /v1/models 用 astron.source
字段区分来源(目录类模型排序靠后)。购买套餐后即可变为可用。
同一个模型有三种等价写法,/v1/chat/completions 的 model 字段填任意一种都能路由:
| 写法 | 示例 | 来源 |
|---|---|---|
| 显示名 | GLM-5.2 |
桌面端 UI 上看到的名字 |
| Slug | xopglm52 |
上游推理接口的模型 ID |
| 目录 ID | lm_glm52 |
bot/models/configs 的稳定 id |
还可在控制台「设置 → 模型别名」里自定义,格式 别名=目标,例如 glm=xopglm52。
星辰侧没有 Qoder 那种 .../campaigns → claim 的签到接口。真正的「每日登录」
动作是:
POST {workspace}/tenant-app/v2/init-app # 带会话 Cookie,无 body
-> { flag: true, code: 0, data: { banned: false } }
当日首次调用即入账当日奖励(国庆活动为 5000 Spark + 套餐对应积分),并下发一个
DAILY_REWARD_DIALOG 横幅通知;重复调用不会重复发放。官方客户端在启动 / 登录时都会
调用它。
服务端随后用运营弹窗把「到账通知」推给客户端:
GET {workspace}/client-popups/pending
-> [{ componentType: "DAILY_REWARD_DIALOG", popupId, instanceKey, payload, ... }]
POST {workspace}/client-popups/complete {popupId, instanceKey} # 仅关闭横幅
⚠️ 只调用client-popups/complete不会发放积分,它只是把横幅关掉。 实测:complete返回成功、弹窗消失,余额纹丝不动;init-app一调用,余额立刻 +5000 Spark + 积分。所以网关每天的签到动作是init-app,complete只是顺手清理横幅。
componentType 只有三种会被客户端处理(对齐 bundle 里的 SUPPORTED_COMPONENT_TYPES):
| componentType | 含义 |
|---|---|
DAILY_REWARD_DIALOG |
每日奖励到账通知(横幅) |
NEW_USER_DIALOG |
新人奖励 |
CLIENT_DOWNLOAD_REWARD_DIALOG |
客户端下载奖励(对应 client-download-reward/claim) |
网关复刻的是官方客户端在启动 / 登录时执行的同一动作,因此不需要伪造任何签名, 也不需要桌面端常驻。
| 接口 | 方法 | 用途 |
|---|---|---|
points/balance |
GET | 积分余额(通用 + Spark,含各类来源与到期时间) |
points/summary |
GET | 积分摘要 |
membership/me |
GET | 会员套餐(TRIAL/体验版 等) |
client-popups/pending |
GET | 待领取弹窗 |
client-popups/complete |
POST | 完成/领取弹窗 |
client-download-reward/claim |
POST | 领取下载奖励 |
beta/claim/eligibility |
GET | Beta 资格 |
beta/claim |
POST | 领取 Beta(需域账号) |
redeem-codes/redeem |
POST | 兑换码兑换 |
实测数据:
points/balance返回totalBalance/sparkTotalBalance/activityNextExpireTime等;membership/me的uid是数字而非字符串(网关已用宽松标量类型兼容)。
client-download-reward/claim 之类的接口即使今日已领也可能返回成功,
init-app 重复调用也不会有新入账。网关因此在领取前后各取一次余额,用
points_delta 报告真实到账的积分(含每日登录奖励):
- 有增量 →
签到成功:+N 积分 - 无增量但调用成功 →
调用成功但积分无变化(可能今日已领)
不会把「接口返回 200」谎报成「签到成功」。
| 机制 | 说明 |
|---|---|
| 凭据刷新 | 用会话 Cookie 重新换取 modelBearerToken,401 时即时刷新并重放 |
| 状态保活 | 定时拉取积分/会员/弹窗,同时让 Cookie 保持活跃 |
| 余额感知轮转 | 积分与 Spark 均为 0 的账号自动降权;两轮回退策略保证不会硬阻塞请求 |
| 失败转移 | 401 / 429 / 5xx 自动换号重试(有界 3 次),失败账号进入短冷却 |
| 自动签到 | 每日调用 init-app 发奖;过了设定小时即补签(含启动即补签,不必等到整点),按账号分别记录结果 |
调度器每分钟读一次设置,面板改动无需重启即时生效;所有后台任务挂在 一个可取消的根 context 上,关机时立即中止在途请求。
| 键 | 默认 | 说明 |
|---|---|---|
auto_checkin |
false |
每日自动签到(调用 init-app 发奖) |
checkin_hour |
9 |
自动签到时间(0-23);过点后任意时刻补签,启动即补签 |
keepalive_minutes |
0 |
保活间隔分钟数,0 = 关闭 |
balance_aware_rotation |
true |
余额感知轮转 |
checkin_complete_popups |
true |
代领运营弹窗(含每日签到) |
checkin_claim_download_reward |
true |
领取客户端下载奖励 |
checkin_claim_beta |
true |
领取 Beta 资格(需填域账号,否则自动跳过) |
go build -ldflags="-s -w" -o astudio2api .
./astudio2api默认监听 0.0.0.0:10086,控制台在 http://127.0.0.1:10086/。
首次启动不再使用固定默认密码:没有通过 -password / ASTUDIO_ADMIN_PASSWORD
指定时,网关会生成一个随机密码并打印在日志中:
──────────────────────────────────────────────────────────
首次启动:已生成控制台密码 9f2c...(32 位十六进制)
登录 http://127.0.0.1:10086/ 后请在「设置」中修改
──────────────────────────────────────────────────────────
登录后控制台会自动切到「设置」并提示修改;改密后该提示消失。
启动日志会打印自动导入结果:
2026/01/01 09:00:00 imported AStudio account 12345678901 (12345678901)
2026/01/01 09:00:00 AStudio2API listening on http://0.0.0.0:10086
镜像只打包一个静态 Go 二进制(≈9 MB),构建阶段不需要联网拉依赖
(纯标准库,go.mod 无 require)。
# 1) 启动
ASTUDIO_ADMIN_PASSWORD='换成强密码' docker compose up -d --build
# 2) 看日志
docker compose logs -f
# 3) 打开控制台
# http://<服务器IP>:10086/也可以直接 docker run:
docker build -t astudio2api:latest .
docker run -d --name astudio2api --restart unless-stopped \
-p 127.0.0.1:10086:10086 \
-v "$PWD/data:/data" \
-e ASTUDIO_ADMIN_PASSWORD='换成强密码' \
-e TZ=Asia/Shanghai \
astudio2api:latest要点:
| 项 | 说明 |
|---|---|
| 状态卷 | ./data:/data,ASTUDIO_DATA_PATH=/data/astudio2api-data.json |
| 卷里含凭据 | astudio2api-data.json 等同账号密码,不要提交或分享 |
| 端口 | compose 默认绑 127.0.0.1;要局域网访问改成 "10086:10086" |
| 时区 | TZ=Asia/Shanghai,否则自动签到的「小时」会按 UTC 算 |
| 出网 | 容器需能访问 agent.xfyun.cn 与 maas-api.cn-huabei-1.xf-yun.com |
| 健康检查 | GET /ping(免鉴权,不查账号池) |
| 浏览器侧 | GeeTest 脚本由浏览器加载,容器本身不需要访问 static.geetest.com |
| 桌面导入 | 容器里没有 AStudio 桌面端,请用手机号验证码登录添加账号 |
在容器内运行时,
ASTUDIO_ADMIN_PASSWORD每次启动都会覆盖控制台里保存的密码。
三种方式,按场景选:
控制台 → 账号 → 「手机号验证码登录」:填手机号 → 点「发送验证码」 (弹出 GeeTest 滑块,在本页浏览器完成)→ 填短信码 → 「登录并添加账号」。
背后走的是与官方客户端完全一致的链路:
GET {authBase}chat/gee-captcha 取 GeeTest v3 配置
POST {authBase}login/mobile/send-verify-code multipart,需已解题的验证码
POST {authBase}login/phone-quick-login multipart,不需要验证码
POST {authBase}tenant-app/v2/init-app Cookie,返回 banned
GET {authBase}userInfo Cookie,补齐 uid / mobile
GET {workspace}bot/models/configs Cookie,换取 modelBearerToken
关于安全验证的实话:send-verify-code 由服务端强制校验 GeeTest v3——
空验证码会返回 code=20009 geetest verify failed(已实测)。
所以验证码必须由浏览器解题后回传;网关不做也不应做自动解题。
面板镜像了官方客户端的初始化参数(product:'bind'、https:true、
offline:!success、onReady→verify()、30s 超时),
SDK 与官方客户端同源:https://static.geetest.com/static/tools/gt.js。
控制台 → 账号 → 「一键导入」,留空自动探测,或手填数据目录
(如 F:\IDE\AStudio Data)。仅适用于本机有桌面端的场景。
远端 / 容器场景不需要下载桌面端,直接用「方式一」的手机号验证码登录即可。
登录成功后网关保存的是完整会话(token + ssoSessionId);每日签到走的
POST tenant-app/v2/init-app 正是用这个会话 cookie 认证的,所以之后每天的自动
签到都不再需要浏览器,更不需要桌面端。
需要人工做的只有一次:在浏览器里解 GeeTest + 输短信码。按安全性排序:
| 做法 | 说明 |
|---|---|
| SSH 端口转发(推荐) | 面板继续只绑 127.0.0.1,本机 ssh -L 10086:127.0.0.1:10086 user@server,再在本地浏览器打开 http://127.0.0.1:10086/ 登录 |
| 临时开放面板 | compose 端口改为 "10086:10086",登录完立即改回并 docker compose up -d |
| 本地登录后搬运 | 在任意一台能开浏览器的机器上跑一次网关并短信登录,把生成的 data/astudio2api-data.json 拷到服务器(含凭据,注意保密) |
⚠️ runtime/acode-home/config.toml的兜底导入(acode-config)只有 bearer、没有会话 cookie,因此不能用于签到,只能当模型凭据。若账号last_error/status_note出现 会话相关报错,说明 cookie 已失效,回面板重做一次短信登录即可。
# 先在控制台「API 密钥」建一个 sk-...
curl http://127.0.0.1:10086/v1/chat/completions \
-H "Authorization: Bearer sk-xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"model":"GLM-5.2","messages":[{"role":"user","content":"你好"}],"stream":true}'| 客户端 | Base URL | 备注 |
|---|---|---|
| OpenAI SDK / NewAPI | http://127.0.0.1:10086/v1 |
模型填 xopglm52 或 GLM-5.2 |
| Codex / pi | http://127.0.0.1:10086/v1 |
走 /v1/responses,原生支持 |
| Claude Code | http://127.0.0.1:10086 |
ANTHROPIC_BASE_URL,走 /v1/messages |
鉴权:Authorization: Bearer <key> 或 x-api-key。未创建任何密钥时,控制台密码可临时充当密钥。
| 端点 | 方法 | 说明 |
|---|---|---|
/v1/chat/completions |
POST | OpenAI Chat Completions,流式/非流式 |
/v1/responses |
POST | OpenAI Responses API(Codex) |
/v1/messages |
POST | Anthropic Messages API |
/v1/models |
GET | 模型清单(含 astron 扩展:显示名、上下文、思考档位、来源) |
| 端点 | 方法 | 说明 |
|---|---|---|
/health /healthz |
GET | 健康检查,含账号/模型就绪计数(免鉴权) |
/ping |
GET | 纯文本 pong,不查账号池(免鉴权,供容器探针) |
/ |
GET | 控制台页面 |
除 login 外均需面板登录会话。
| 端点 | 方法 | 说明 |
|---|---|---|
/admin/api/login |
POST | 面板密码登录 |
/admin/api/logout |
POST | 退出登录 |
/admin/api/state |
GET | 全量状态(账号 / 密钥 / 模型 / 统计 / 日志 / 权益) |
/admin/api/keys |
GET / POST / DELETE | API 密钥增删与启停 |
/admin/api/accounts |
POST | 账号管理:import / manual / refresh / toggle / domain / remove |
/admin/api/login/geetest |
GET | 取 GeeTest 配置(添加账号用) |
/admin/api/login/sms |
POST | {mobile, geetest_challenge, geetest_validate, geetest_seccode} |
/admin/api/login/verify |
POST | {mobile, verify_code} 登录并加入账号池 |
/admin/api/checkin |
POST | 签到;{id} 指定单账号,空对象则全部账号 |
/admin/api/keepalive |
POST | 保活;同上 |
/admin/api/redeem |
POST | {id?, code} 兑换码 |
/admin/api/benefits |
GET / DELETE | 权益事件日志 / 清空 |
/admin/api/models |
GET / POST | 查看 / 刷新模型目录 |
/admin/api/logs |
GET | 请求日志(?limit=) |
/admin/api/stats |
GET / DELETE | 统计 / 清空 |
/admin/api/settings |
POST | 更新设置 |
/admin/api/password |
POST | 修改面板密码 |
优先级:命令行参数 > 环境变量 > 控制台设置 > 默认值。
| 参数 | 环境变量 | 默认 | 说明 |
|---|---|---|---|
-host |
ASTUDIO_HOST |
0.0.0.0 |
监听地址 |
-port |
ASTUDIO_PORT |
10086 |
监听端口 |
-data |
ASTUDIO_DATA_PATH |
可执行文件同目录 astudio2api-data.json |
状态文件 |
-password |
ASTUDIO_ADMIN_PASSWORD |
首次启动随机生成并打印在日志中 | 控制台密码 |
-astron-dir |
ASTUDIO_DATA_DIR |
自动探测 | AStudio 数据目录 |
-sync-once |
— | — | 只刷新一次模型目录后退出 |
控制台内还可调:上游 Base URL、模型目录 API、工作区 API、studioVersion、
上游并发上限、响应超时、流空闲超时、日志保留天数/条数、CORS 来源、模型别名。
所有状态(账号凭据、API 密钥、统计、日志、权益事件)存在单文件里,
路径由 ASTUDIO_DATA_PATH 决定:
data/astudio2api-data.json # 含账号 token / Bearer / API 密钥
⚠️ 这个文件等同于账号密码。 它已写入.gitignore,请勿提交、勿分享。
仓库里提供了一个结构相同、凭据全部为占位值的样例:
astudio2api-data.example.json
它只是格式参考 —— 正常使用无需手工创建,首次启动会自动生成。
泄露后的处置:删掉 data/ 重新用手机号登录,并在控制台删掉旧 API 密钥。
| 不包含 | 原因 |
|---|---|
extract/ |
AStudio app.asar 解包产物(~148 MB),含第三方版权内容,仅作本地协议比对 |
data/ |
真实账号凭据 |
astudio2api.exe 等二进制 |
构建产物 |
参考的三个 qoder2api 项目(Python hub / Go 桥 / Go 重写版)解决的是 「Qoder 需要 COSY 签名 + 设备指纹」这类主动伪造问题; AStudio 这条链路不需要签名伪造,所以本项目的复杂度集中在别处:
| 维度 | qoder2api 系列 | 本项目 |
|---|---|---|
| 上游鉴权 | COSY 签名 + RSA/AES 包裹 | 直接 Bearer,凭据桌面端已落盘 |
| 设备指纹 | 必须派生稳定指纹防关联 | 无需 |
| 凭据来源 | OAuth 设备流 / PAT | 直接读桌面端会话文件 |
| 协议转换 | Qoder 私有信封 → OpenAI | OpenAI 原生 → OpenAI 透传(+ Anthropic 转换) |
| 多账号 | 有 | 有 |
| 控制台 | 有 | 有 |
- 本项目仅供本机 / 内网自用与协议研究。 上游服务的使用受讯飞星辰服务条款约束, 请勿公开暴露到公网或用于商业转售。
- 凭据即账号。
astron-session.json与config.toml里的modelBearerToken等同于账号密码,不要把状态文件astudio2api-data.json提交到仓库或分享。 - 上游模型清单里会包含当前账号无权调用的模型(存在于
bot/models/configs但不在权益内),调用时上游会返回 403 + 业务码。网关已用astron.source字段 区分来源,目录类模型排序靠后。 - 首次启动的随机密码打印在服务日志里,登录后请立即在「设置」中修改,
或先用
ASTUDIO_ADMIN_PASSWORD指定;并按需收紧 CORS 来源。
astudio2api/
├── main.go # 入口:配置、启动、自动导入、定时维护
├── internal/
│ ├── astron/
│ │ ├── session.go # 会话文件定位与解析、Cookie 构造
│ │ ├── client.go # 上游 HTTP 客户端(凭据换取 / 模型目录 / userInfo)
│ │ ├── account.go # 权益接口(积分/会员/弹窗/兑换码/Beta)
│ │ └── login.go # 手机号验证码登录(GeeTest + 短信 + 换凭据)
│ ├── registry/registry.go # 模型目录合并、别名解析
│ ├── server/
│ │ ├── server.go # 路由、鉴权、CORS、账号选择
│ │ ├── handlers.go # /v1/chat/completions、/v1/responses
│ │ ├── proxy.go # 上游转发、SSE 中继、用量提取、空闲超时
│ │ ├── messages.go # Anthropic /v1/messages 双向转换
│ │ ├── benefits.go # 签到 / 权益 / 多账号保活
│ │ ├── login.go # 手机号登录的 HTTP 面
│ │ └── admin.go # 控制台 API
│ ├── store/store.go # 状态持久化、账号池、统计、日志、权益事件
│ └── web/panel.go # 单文件控制台页面(含 GeeTest 登录)
├── Dockerfile # 多阶段构建,静态二进制 + alpine 运行时
├── docker-compose.yml
├── .dockerignore / .gitignore
├── astudio2api-data.example.json # 脱敏状态文件样例
├── LICENSE
└── README.md