Releases: ithtelab/workbuddy-manager
Releases · ithtelab/workbuddy-manager
Release list
v1.0.22
安全
-
修复任意文件读取(严重,路径穿越):静态前端的兜底路由直接把请求路径
拼到静态目录上,未做任何越界校验。ASGI 会先对%2f解码,因此
GET /..%2f..%2fdata%2fusers.json这类请求会读到部署目录之外的文件。
实测可读到(均无需登录):users.json—— 内含签发会话 Cookie 的 secret,拿到它即可伪造
{"role":"admin"}的合法会话,等同拿到管理员权限.env——WB_SECRET等- 上游
config.json——api_key,可直接盗用账号池额度 auths/*.json—— 腾讯账号accessToken,等于接管账号
修法:所有静态路径统一经
_safe_static_path(),用resolve()归一化后
强制要求结果仍位于静态目录之内,并对绝对路径先剥离前导分隔符。
实测覆盖..、%2e%2e、双重编码、绝对路径、混合斜杠等变体,
全部 404 且不返回任何内容;正常页面与目录索引不受影响。
另加:越界尝试记 WARN 日志(原先静态路径不记录任何痕迹,被利用了也无从发现)
修复
- 账号文件名校验收紧为白名单:
accounts一组路由共用的_safe_file()
原先只挡/、反斜杠与..。穿越防线本身是够的,但目录内的其他文件
(如.hidden.json)仍可被读到。现只接受workbuddy-<uid>.json这一种形态,
并显式拒绝 NUL 字节 users.json写入权限收紧到 0600:该文件含会话 secret,原先权限跟随
默认 umask,同主机其他用户可能读到。现以 0600 创建(避免"先写后改权限"
的暴露窗口),Windows 上 chmod 无实际意义,静默降级
说明
- 会话签名机制本身没有问题:HMAC-SHA256 + 常数时间比较,且
role编码在
已签名的 payload 内。实测「改 payload 里的 role」「换错误 secret 验签」均被拒绝。
失守的唯一原因是 secret 被上述漏洞读走——修好读取即切断该链
v1.0.21
新增
- 新增「模型中心」页(底栏「模型」):把账号实际可用的模型单列一页,含
显示名、上下文窗口、最大输出、推理档位与系列分组,支持搜索与按能力筛选。- 数据直连腾讯模型接口(
/console/enterprises/personal/models),
因为上游/v1/models会把name(显示名)与reasoning.supportedEfforts
(推理档位)丢掉,只留 id/上下文/最大输出——要做带显示名与能力的展示
只能自己取 - 两级来源 + 如实标注:腾讯接口优先(
source=tencent);取不到时回退上游
/v1/models(source=upstream,字段有限)并明确提示「缺少显示名与推理档位」;
两者都失败则空态 + 列出失败原因,绝不编造数据 - 统计卡(可用模型 / 支持推理 / 大上下文 / 最大上下文)全部由清单真实计算
- 选中账号:优先剩余有效期最长且未过期的账号,单账号失败自动换下一个(最多 3 个)
- 缓存 5 分钟(失败负缓存 60 秒),并提供「重新拉取」绕过缓存
- 口径与上游一致:只收录
cliagent 下的模型,跳过disabled条目
- 数据直连腾讯模型接口(
- 新增「聊天测试台」页(底栏「测试台」):不创建 API 密钥就能直接试调模型,
与下游调用走同一套账号池。内容:- 输入框沿用 HextaUI 的 AIChatInput 形态(失焦时占位符逐字淡入循环、聚焦展开),
但配色改为本项目的主题令牌(原组件写死白底黑字,暗色模式下会瞎),
并把装饰性的「Deep Search」换成真实的模型选择——本项目没有联网检索能力,
与其放个假按钮不如给有用的 - 思考强度接真实参数
reasoning_effort:只列出该模型supportedEfforts
里的档位(来自模型中心同源数据),换模型时自动收敛到受支持的档位,
不会出现「选了一个不支持的值、被上游静默降级」 - 右下角实时显示本次消耗的积分:取自上游末帧
usage.credit,
每条回答下也单独标注本次扣费与 token 数 - 流式输出逐字追加,可中途停止;回答下提供复制与反馈按钮
- 仅管理员可用(
require_admin):测试台会真实消耗积分,而默认部署里
存在弱口令的 guest 账号,放开给所有登录用户等于把额度借出去 - 该通道走管理端登录态,不经过对外密钥与 IP 管控,仅作调试;界面已注明
- 输入框沿用 HextaUI 的 AIChatInput 形态(失焦时占位符逐字淡入循环、聚焦展开),
- 聊天测试台的调用现在会记账:测试台会真实消耗积分,但原先既不写
请求日志也不计用量——用户在「请求日志」与「用量」里查不到这部分开销,账目对不上。
现复用网关的记录逻辑:日志的密钥列为—(测试台没有对外密钥),
据此仍可区分「测试台调用」与「某密钥的调用」;token、实付积分、
首字延迟一并记录,流式与非流式、以及上游不可用(502)都覆盖
修复
- 签到记录分页会丢数据:合并后按时间排序,但候选固定取各表最近 500 条,
第 500 名之后的记录永远翻不到,而total报的是真实全量——界面会显示
「共 810 条」却翻不出后面 300 条。现按offset + limit取足候选,
并把 DB 层 limit 上限从 500 提到 2000 - 「清空签到记录」看着像没生效:面板改为合并视图后,
checkinTotal变成
本端 + 自动的总和,于是只有自动记录时按钮也显示,但清空只删本端那半,
列表仍有内容。现按钮只在确有本端记录时出现,确认框文案也写明范围
(本端 N 条会删、自动 M 条不在此处删) - 筛选「余额查询 / 令牌保活 / 自动签到」后一片空白,且回不去:根因有两处叠加——
- 概览徽标用的是未筛选的区间总数(如「共 62 条」),而列表是按类型筛选的,
于是出现「共 62 条」配空列表的矛盾;底部又写「共 0 条」 - 筛选栏的渲染条件是
taskLogs.length > 0,一旦点到的类型没有记录,
筛选栏连同列表一起消失,用户再也切不回「全部」
现改为:概览数字跟随筛选(用stats.by_kind取该类型的真实条数与积分)、
筛选栏只要区间内有任何记录就渲染、每个类型的按钮直接标出自己的条数(含 0,
这样点之前就知道是空的)、空态按「当前筛选为空」与「区间内无记录」分开提示
- 概览徽标用的是未筛选的区间总数(如「共 62 条」),而列表是按类型筛选的,
- 「签到记录」看不到上游自动签到:签到记录原先只写本端触发的
(手动 / 批量 / 添加账号),而上游定时签到的结果由日志采集器写进另一张表,
所以「自动签到」在这里永远看不到。现把两者按时间归并成统一视图,
来源标注「上游自动」,并分别给出本端/自动的条数。
需要说明的是:上游对签到成功是静默的(只打一行
checkin done: total=.. ok=..汇总 + 失败明细),所以自动侧提供的是每轮
汇总/异常行,不是逐账号成功明细——界面上如实呈现,不假装有更细的数据
说明
- 「系列」按模型 id 前缀推导(glm→智谱 GLM、deepseek→DeepSeek 等),仅为便于分组浏览;
认不出的归入「其他」,不做猜测性归类 - 不展示「倍率 / 单价」:腾讯模型接口不返回该字段(那是第三方面板自维护的价目表),
与其编一个数字,不如不显示。页面底部已注明原因 - 新增
test_model_catalog.py16 例(系列推导、统计口径、三级来源回退、
过期账号跳过、多账号重试、缓存与 force 行为)、test_playground.py8 例
(签到合并、id 不冲突、分页、时间窗、测试台鉴权),全量 212 通过 - 「自动任务与积分记录」的
by_kind计数与列表现在口径一致,避免同一屏出现
多个互相矛盾的数字
v1.0.20
新增
- 可用模型支持手动重新拉取,并如实标注来源(设置 → 上游配置 → 可用模型):
上游/v1/models优先用池里随机一个健康账号去动态拉取(成功缓存 1 小时),
失败则回退到编译进二进制的静态表(另有 5 分钟负缓存)。这带来两个困惑,
这次都处理掉:- 加「重新拉取」按钮:不必再干等上游那 1 小时缓存,也不会因为「刷新页面」
看着没变化而以为坏了 - 来源如实标注:动态时显示「上游实时列表 · N 个」,回退时显示
**「上游内置回退表(非实时) · N 个」**并给出解释——此前一律写
「来自上游实时列表」,回退时用户会误以为是自己账号/配置出了问题 - 判据用上游动态条目才有的
max_output_tokens字段;判不出时不猜,
回退中性文案(unknown)
- 加「重新拉取」按钮:不必再干等上游那 1 小时缓存,也不会因为「刷新页面」
- 说明模型数量为何因人而异:列表由上游随机挑一个账号拉取,取决于该
账号的授权(企业 / 套餐),所以「同样的部署、不同账号看到的模型数量不同」
是上游的正常行为,不是本端少显示。此前有用户反馈「只显示 6 个模型」,
对照上游源码确认静态表固定为 10 个、其列表是你 16 个的真子集,
即确为账号授权差异,而非回退或本端缺陷
修复
- 「重新拉取」按钮不该被「配置文件可读性」禁用:模型列表与 config.json
是否可读是两件事(前者取自上游/v1/models),原先两者耦合会导致
配置读不到时按钮点不动——恰恰是最需要重试的时候。现只在请求进行中禁用
v1.0.19
新增
- 账号列表显示「最后续期」时间,用来区分「刚被保活续期」和「从没刷新过」:
界面上那行「有效期」是剩余时间,而刷新会把它重新拉满,所以单看天数会
读反——7.0 天的账号可能刚刚续期、非常健康;60 天的账号反而可能是
当初扫码签发后再没被刷新过,保活没覆盖到,到期就会掉线。
现在在进度条下方显示「最后续期 N 前」(取 JWT 的iat;刷新会换发新令牌,
故 iat ≈ 最近一次刷新时刻)。实测三种账号一眼可辨:- 刚续期:
7.0 天+最后续期 1 小时前 - 从未刷新:
5.0 天+最后续期 55 天前← 真正要留意的 - 非 JWT:靠文件时间兜底,见下条
- 刚续期:
修复
- 进度条基准的兜底窗口从「一律 60 天」改为按实际推算:JWT 解不出时
(扁平形手写 auth 文件的 accessToken 常不是标准 JWT),原先直接假定
60 天当满格。于是一个 7 天的令牌进度条只显示 12%,看着像快过期,
属于误报。现改为用 auth 文件的mtime推算——上游刷新 token 后会
原子写回该文件,安装器写盘也走同一路径,故exp - mtime即本次
有效期的近似值。实测该场景下进度条从 8.3% 修正为 71.4%。
依然解不出时保持None并回退旧的保守窗口,绝不显示负数或误报。
说明
- 「最后续期」纯展示、不落库:签发时间从令牌自身解出,刷新换发令牌时会
自动更新,不需要额外存储,也不会与令牌不一致
v1.0.18
新增
- 请求日志新增「首字」列(time-to-first-token):原先只有一列「延迟」,
但那是整条响应的耗时——流式请求要等模型把全部内容生成完才结束,所以
回答越长数字越大,13~24 秒很常见,根本看不出上游响应快慢。
现在拆成两列:- 首字:从发起上游请求到收到第一个含正文的 delta。这才是
「上游多久开始回话」,判断上游快慢只看它 - 总耗时(原「延迟」改名):含模型生成全程,用来看一条请求的整体开销
- 非流式请求没有中间过程,首字显示
—;详情面板写「未采集(非流式请求)」 - 阈值也移到首字上:≥3 秒才标琥珀色(总耗时对长回答天然就高,标色只会误报)
- 首字:从发起上游请求到收到第一个含正文的 delta。这才是
说明
- 首字采集刻意不把「收到第一块 SSE」当作首字:OpenAI 流的第一块通常只有
role、content为空,若以它计时,记的是建连时间,数字偏小且失真。因此
要等到真正含content(或推理模型的reasoning_content)的 delta 才算
——这条已写成回归测试锁定 - 数据库新增
first_token_ms列(可空)。已有部署升级时自动ALTER TABLE
补列,历史记录该列为NULL(显示—),不会误报为 0ms
v1.0.17
修复
- 升级后「更新日志」页报「未找到更新日志文件」:v1.0.16 新增的更新日志页
只读部署根目录的CHANGELOG.md,但更新器与安装脚本都不会把该文件放进
部署目录——更新器只替换server/、web/out/、deploy/与.version,
安装脚本也只同步这些。于是发布包里虽然有它,装到机器上却没有,页面直接报错。
更麻烦的是:已经升到 v1.0.16 的部署因为「已是最新版」不会再触发更新,
点多少次更新都补不上这个文件。现改为:- 解析时按优先级找
根目录/CHANGELOG.md→server/CHANGELOG.md - 发布打包时在
server/CHANGELOG.md放一份副本——server/每次更新都会被
整体替换,这条路在任何部署形态下都拿得到 - 安装脚本同时把
CHANGELOG.md、README.md复制到部署目录 - 报错信息列出已尝试的路径,便于自行排查
- 解析时按优先级找
- 顺带说明:v1.0.16 里「更新时同步 CHANGELOG.md/README.md」的改动本身没错,
但它只在下一次更新时才生效,救不了当时已经装上的那份;这次的兜底
副本才是真正覆盖所有情况的做法
v1.0.16
安全
- 修复凭据泄露:上游新增的
upstream.device_token会被明文返回给前端。
上游本次新增了设备风控 token(X-Device-Token),它相当于把一台可信设备的
身份借出去,泄露可被他人复用。而本端的配置接口此前只脱敏了api_key与
upstash.token,新字段被原样下发(实测确认)。
现与 Upstash token 同样处理:只回传「是否已配置」+ 掩码,前端拿不到明文 - 写入语义区分清楚,避免把凭据误清空或写坏:
- 留空 = 保持原值(前端回显的是掩码,不能被当成清空)
- 非空值 = 设为该值
- 显式
null= 清除该键(否则没有办法删掉它) - 拒绝含换行/控制字符、超过 512 字符的值
新增
- 更新提示现在能看出「这批更新做了什么」:原先只显示上游最新一条提交,
而上游常一次累积多个提交——只显示一条会让人以为「就改了这一处」,
也判断不出是功能还是修复、值不值得跟。现在:- 显示落后多少个提交(如「落后 7 个提交」)
- 可展开查看这批判的逐条提交说明(最多 20 条,最新的在前)
- 提交前缀(
feat/fix/refactor)本身就是分类,一眼可辨性质 - 用 GitHub compare API 取,本地提交不在远端(上游 force-push)时静默降级,
不影响版本检测本身
- 出站标识配置补全(设置 → 上游配置 → 出站标识):上游本次新增了几个可配项,
本端一并可视化,便于对齐官方客户端、让官网「使用端」显示正确:upstream.client_name—— 用量归属头(X-Product / X-IDE-Name / X-IDE-Type)upstream.client_version/upstream.cli_version—— 出站 UA 的版本段upstream.device_token_file—— 设备 token 文件路径(留空不读)upstream.passthrough_ip—— 是否透传客户端 IP(默认关闭,不把内网 IP 暴露给上游)- 这些字段均做了控制字符与长度校验;同段其他键不受影响
- 管理端内置「更新日志」(设置 → 更新日志):此前只有仓库里有一份
CHANGELOG.md,要关掉面板、打开 GitHub 才看得到更新了什么。现在:- 直接渲染
CHANGELOG.md,离线可用(文件随发布包分发,不需要联网) - 按版本折叠,默认展开最新一个;当前运行版本标注「当前版本」并可定位
- 未发布内容单独标为「开发中」,与已发布版本区分开
- 分类(安全 / 新增 / 修复 / 改进)带配色标签,一眼看出改动性质
- 不引入 markdown 依赖:解析器只认本项目固定的三级结构,
行内仅处理**加粗**与`代码`,发布包更小 - 顺带修正一个隐患:一键更新原先只替换
server/、web/out/与
.version,不含CHANGELOG.md/README.md——若不同步,
更新后界面里的日志会一直停在旧版内容。现一并同步
- 直接渲染
v1.0.15
修复
- 反代调用报「请求体过大(上限 8 MiB)」,但上游上限其实可调:
网关把上限写死 8 MiB(注释说「与上游对齐」,可上游的
server.max_body_mb是可配置的,管理端设置页也能改)。于是它成了隐性瓶颈——
把上游上限调大后,请求仍在本端先被 413 拦掉,且看不出是谁拦的。
现改为跟随上游server.max_body_mb(默认 8 MiB;读不到配置时回退默认,
不因读不到而拒绝服务),加 10 秒缓存避免每请求读文件 - 大小校验原先只看
Content-Length:分块传输时该头缺失、也可以伪造,
因此超大请求体仍会被整段读进内存(正是当初要防的内存耗尽)。现改为
读取时逐块累计并即时截断,缺头与伪造头都拦得住 - 413 的报错改为可直接照做:说明当前上限、怎么压缩、以及去
「设置 → 上游配置 → 请求上限」(server.max_body_mb)调大
v1.0.14
修复
- 手机上密钥的复制按钮被挤出弹窗、点不到:密钥展示用了
flex-1 + overflow-x-auto但没加min-w-0——flex 子项默认min-width:auto
不肯收缩,长密钥就把复制按钮推到弹窗外面。现改为可换行显示
(min-w-0 break-all),复制按钮带文字标签且固定在弹窗内 - 添加账号的授权链接无法复制:弹窗里只有可点击的链接,想把它发给朋友
让对方扫码授权时没法复制。现新增「复制链接」按钮,并在支持 Web Share API
的浏览器(多为手机)额外提供「分享」按钮,可直接转发到微信等应用 - 设置页「服务信息」的长路径会溢出卡片:
truncate同样因缺min-w-0
不生效(账号目录/上游声明目录)。现补上收缩约束,并给账号目录加复制入口
改进
- 新增通用
CopyButton组件统一复制交互:自身shrink-0不被长文本挤掉、
移动端触控区域 32×32、复制后短暂显示对勾(移动端看不到 toast 时也明确),
并在日志详情的来源 IP / User-Agent / 错误等长字段上补齐复制入口
v1.0.13
修复
- 上游更新失败时能看懂原因,并且有退路:上游提交
4a80249f删掉了一批
脚本却漏改Dockerfile(仍 COPY 已删除的scripts/probe_active.py),
导致docker compose up --build失败——这是上游自身的问题,但当时
界面只报「命令失败」加一串难懂的 checksum 报错,既看不出原因、也没有回退手段。
现在增强三处:- 构建前预检:解析
Dockerfile的COPY,若引用了仓库中不存在的文件,
在构建之前就明确报出原因并列出缺哪个文件,不再让你对着 docker 的长报错猜
(只判定确凿情况,跳过阶段拷贝 / 通配符 / URL,不会误报把正常更新挡下来) - 失败诊断:把 docker 报错归纳成一句人话(Dockerfile 缺文件 / 磁盘不足 /
网络问题 / 权限不足) - 说明当前服务状态:构建失败时 compose 不会动已在运行的容器,
日志里会明确写出「旧容器未被影响,服务正常」,避免误以为服务已挂
- 构建前预检:解析
新增
- 固定上游版本(设置 → 系统更新):可填提交号或标签,之后「更新上游」
会检出该版本而不跟随分支,用于上游某提交有问题时一键回退;
留空即恢复跟随分支。版本值经严格校验后才会拼进 git 参数
改进
- 任务记录改为「固定高度 + 内部滚动」,不再分页:分页虽能控制高度,
但与右侧「上游原始日志」的滚动方式不一致,翻页也不如直接滚直观;
且一页没填满时下方会留一片空白。现三块都用固定高度内滚,页面高度恒定
(实测 7.5 屏 → 1.3 屏)- 单次拉取 200 条(手机上 80 条)以控制 DOM 规模
- 命中上限时底部明确提示「共 N 条 · 仅显示最近 200 条,更早的请缩小时间范围」,
不静默截断 - 两块并排的卡片高度一致(470px),右侧不再有高度不齐的空白
- 滚动条改为细样式:浏览器默认滚动条偏宽、带可见轨道,嵌在卡片里很突兀。
新增.scroll-slim(主题色 30% 透明滑块 + 透明轨道,悬停加深),
并统一应用到任务记录的三个列表、表格容器与更新日志区 - 手机上高度按视口自适应(
calc(100dvh-200px))而非套用桌面固定值:
手机屏更矮,固定高度会明显偏短、卡片下方留一大片空白;
用dvh而非vh是为了在地址栏收起/展开时也算得准