Skip to content

Releases: ithtelab/workbuddy-manager

v1.0.22

Choose a tag to compare

@github-actions github-actions released this 14 Sep 02:55

安全

  • 修复任意文件读取(严重,路径穿越):静态前端的兜底路由直接把请求路径
    拼到静态目录上,未做任何越界校验。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

Choose a tag to compare

@github-actions github-actions released this 14 Sep 02:12

新增

  • 新增「模型中心」页(底栏「模型」):把账号实际可用的模型单列一页,含
    显示名、上下文窗口、最大输出、推理档位与系列分组,支持搜索与按能力筛选。
    • 数据直连腾讯模型接口/console/enterprises/personal/models),
      因为上游 /v1/models 会把 name(显示名)与 reasoning.supportedEfforts
      (推理档位)丢掉,只留 id/上下文/最大输出——要做带显示名与能力的展示
      只能自己取
    • 两级来源 + 如实标注:腾讯接口优先(source=tencent);取不到时回退上游
      /v1/modelssource=upstream,字段有限)并明确提示「缺少显示名与推理档位」;
      两者都失败则空态 + 列出失败原因,绝不编造数据
    • 统计卡(可用模型 / 支持推理 / 大上下文 / 最大上下文)全部由清单真实计算
    • 选中账号:优先剩余有效期最长且未过期的账号,单账号失败自动换下一个(最多 3 个)
    • 缓存 5 分钟(失败负缓存 60 秒),并提供「重新拉取」绕过缓存
    • 口径与上游一致:只收录 cli agent 下的模型,跳过 disabled 条目
  • 新增「聊天测试台」页(底栏「测试台」):不创建 API 密钥就能直接试调模型,
    与下游调用走同一套账号池。内容:
    • 输入框沿用 HextaUI 的 AIChatInput 形态(失焦时占位符逐字淡入循环、聚焦展开),
      配色改为本项目的主题令牌(原组件写死白底黑字,暗色模式下会瞎),
      并把装饰性的「Deep Search」换成真实的模型选择——本项目没有联网检索能力,
      与其放个假按钮不如给有用的
    • 思考强度接真实参数 reasoning_effort:只列出该模型 supportedEfforts
      里的档位(来自模型中心同源数据),换模型时自动收敛到受支持的档位,
      不会出现「选了一个不支持的值、被上游静默降级」
    • 右下角实时显示本次消耗的积分:取自上游末帧 usage.credit
      每条回答下也单独标注本次扣费与 token 数
    • 流式输出逐字追加,可中途停止;回答下提供复制与反馈按钮
    • 仅管理员可用require_admin):测试台会真实消耗积分,而默认部署里
      存在弱口令的 guest 账号,放开给所有登录用户等于把额度借出去
    • 该通道走管理端登录态,不经过对外密钥与 IP 管控,仅作调试;界面已注明
  • 聊天测试台的调用现在会记账:测试台会真实消耗积分,但原先既不写
    请求日志也不计用量——用户在「请求日志」与「用量」里查不到这部分开销,账目对不上。
    现复用网关的记录逻辑:日志的密钥列为 (测试台没有对外密钥),
    据此仍可区分「测试台调用」与「某密钥的调用」;token、实付积分、
    首字延迟一并记录,流式与非流式、以及上游不可用(502)都覆盖

修复

  • 签到记录分页会丢数据:合并后按时间排序,但候选固定取各表最近 500 条,
    第 500 名之后的记录永远翻不到,而 total 报的是真实全量——界面会显示
    「共 810 条」却翻不出后面 300 条。现按 offset + limit 取足候选,
    并把 DB 层 limit 上限从 500 提到 2000
  • 「清空签到记录」看着像没生效:面板改为合并视图后,checkinTotal 变成
    本端 + 自动的总和,于是只有自动记录时按钮也显示,但清空只删本端那半,
    列表仍有内容。现按钮只在确有本端记录时出现,确认框文案也写明范围
    (本端 N 条会删、自动 M 条不在此处删)
  • 筛选「余额查询 / 令牌保活 / 自动签到」后一片空白,且回不去:根因有两处叠加——
    1. 概览徽标用的是未筛选的区间总数(如「共 62 条」),而列表是按类型筛选的,
      于是出现「共 62 条」配空列表的矛盾;底部又写「共 0 条」
    2. 筛选栏的渲染条件是 taskLogs.length > 0,一旦点到的类型没有记录,
      筛选栏连同列表一起消失,用户再也切不回「全部」
      现改为:概览数字跟随筛选(用 stats.by_kind 取该类型的真实条数与积分)、
      筛选栏只要区间内有任何记录就渲染、每个类型的按钮直接标出自己的条数(含 0,
      这样点之前就知道是空的)、空态按「当前筛选为空」与「区间内无记录」分开提示
  • 「签到记录」看不到上游自动签到:签到记录原先只写本端触发
    (手动 / 批量 / 添加账号),而上游定时签到的结果由日志采集器写进另一张表,
    所以「自动签到」在这里永远看不到。现把两者按时间归并成统一视图,
    来源标注「上游自动」,并分别给出本端/自动的条数。
    需要说明的是:上游对签到成功是静默的(只打一行
    checkin done: total=.. ok=.. 汇总 + 失败明细),所以自动侧提供的是每轮
    汇总/异常行,不是逐账号成功明细——界面上如实呈现,不假装有更细的数据

说明

  • 「系列」按模型 id 前缀推导(glm→智谱 GLM、deepseek→DeepSeek 等),仅为便于分组浏览;
    认不出的归入「其他」,不做猜测性归类
  • 不展示「倍率 / 单价」:腾讯模型接口不返回该字段(那是第三方面板自维护的价目表),
    与其编一个数字,不如不显示。页面底部已注明原因
  • 新增 test_model_catalog.py 16 例(系列推导、统计口径、三级来源回退、
    过期账号跳过、多账号重试、缓存与 force 行为)、test_playground.py 8 例
    (签到合并、id 不冲突、分页、时间窗、测试台鉴权),全量 212 通过
  • 「自动任务与积分记录」的 by_kind 计数与列表现在口径一致,避免同一屏出现
    多个互相矛盾的数字

v1.0.20

Choose a tag to compare

@github-actions github-actions released this 13 Sep 14:06

新增

  • 可用模型支持手动重新拉取,并如实标注来源(设置 → 上游配置 → 可用模型):
    上游 /v1/models 优先用池里随机一个健康账号去动态拉取(成功缓存 1 小时),
    失败则回退到编译进二进制的静态表(另有 5 分钟负缓存)。这带来两个困惑,
    这次都处理掉:
    • 加「重新拉取」按钮:不必再干等上游那 1 小时缓存,也不会因为「刷新页面」
      看着没变化而以为坏了
    • 来源如实标注:动态时显示「上游实时列表 · N 个」,回退时显示
      **「上游内置回退表(非实时) · N 个」**并给出解释——此前一律写
      「来自上游实时列表」,回退时用户会误以为是自己账号/配置出了问题
    • 判据用上游动态条目才有的 max_output_tokens 字段;判不出时不猜,
      回退中性文案(unknown
  • 说明模型数量为何因人而异:列表由上游随机挑一个账号拉取,取决于该
    账号的授权
    (企业 / 套餐),所以「同样的部署、不同账号看到的模型数量不同」
    是上游的正常行为,不是本端少显示。此前有用户反馈「只显示 6 个模型」,
    对照上游源码确认静态表固定为 10 个、其列表是你 16 个的真子集,
    即确为账号授权差异,而非回退或本端缺陷

修复

  • 「重新拉取」按钮不该被「配置文件可读性」禁用:模型列表与 config.json
    是否可读是两件事(前者取自上游 /v1/models),原先两者耦合会导致
    配置读不到时按钮点不动——恰恰是最需要重试的时候。现只在请求进行中禁用

v1.0.19

Choose a tag to compare

@github-actions github-actions released this 13 Sep 12:43

新增

  • 账号列表显示「最后续期」时间,用来区分「刚被保活续期」和「从没刷新过」:
    界面上那行「有效期」是剩余时间,而刷新会把它重新拉满,所以单看天数会
    读反——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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 09:54

新增

  • 请求日志新增「首字」列(time-to-first-token):原先只有一列「延迟」,
    但那是整条响应的耗时——流式请求要等模型把全部内容生成完才结束,所以
    回答越长数字越大,13~24 秒很常见,根本看不出上游响应快慢。
    现在拆成两列:
    • 首字:从发起上游请求到收到第一个含正文的 delta。这才是
      「上游多久开始回话」,判断上游快慢只看它
    • 总耗时(原「延迟」改名):含模型生成全程,用来看一条请求的整体开销
    • 非流式请求没有中间过程,首字显示 ;详情面板写「未采集(非流式请求)」
    • 阈值也移到首字上:≥3 秒才标琥珀色(总耗时对长回答天然就高,标色只会误报)

说明

  • 首字采集刻意不把「收到第一块 SSE」当作首字:OpenAI 流的第一块通常只有
    rolecontent 为空,若以它计时,记的是建连时间,数字偏小且失真。因此
    要等到真正含 content(或推理模型的 reasoning_content)的 delta 才算
    ——这条已写成回归测试锁定
  • 数据库新增 first_token_ms 列(可空)。已有部署升级时自动 ALTER TABLE
    补列,历史记录该列为 NULL(显示 ),不会误报为 0ms

v1.0.17

Choose a tag to compare

@github-actions github-actions released this 13 Sep 09:24

修复

  • 升级后「更新日志」页报「未找到更新日志文件」:v1.0.16 新增的更新日志页
    只读部署根目录的 CHANGELOG.md,但更新器与安装脚本都不会把该文件放进
    部署目录
    ——更新器只替换 server/web/out/deploy/.version
    安装脚本也只同步这些。于是发布包里虽然有它,装到机器上却没有,页面直接报错。
    更麻烦的是:已经升到 v1.0.16 的部署因为「已是最新版」不会再触发更新,
    点多少次更新都补不上这个文件。现改为:
    • 解析时按优先级找 根目录/CHANGELOG.mdserver/CHANGELOG.md
    • 发布打包时在 server/CHANGELOG.md 放一份副本——server/ 每次更新都会被
      整体替换,这条路在任何部署形态下都拿得到
    • 安装脚本同时把 CHANGELOG.mdREADME.md 复制到部署目录
    • 报错信息列出已尝试的路径,便于自行排查
  • 顺带说明:v1.0.16 里「更新时同步 CHANGELOG.md/README.md」的改动本身没错,
    但它只在下一次更新时才生效,救不了当时已经装上的那份;这次的兜底
    副本才是真正覆盖所有情况的做法

v1.0.16

Choose a tag to compare

@github-actions github-actions released this 13 Sep 09:10

安全

  • 修复凭据泄露:上游新增的 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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 08:22

修复

  • 反代调用报「请求体过大(上限 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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 07:31

修复

  • 手机上密钥的复制按钮被挤出弹窗、点不到:密钥展示用了
    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

Choose a tag to compare

@github-actions github-actions released this 13 Sep 06:27

修复

  • 上游更新失败时能看懂原因,并且有退路:上游提交 4a80249f 删掉了一批
    脚本却漏改 Dockerfile(仍 COPY 已删除的 scripts/probe_active.py),
    导致 docker compose up --build 失败——这是上游自身的问题,但当时
    界面只报「命令失败」加一串难懂的 checksum 报错,既看不出原因、也没有回退手段。
    现在增强三处:
    • 构建前预检:解析 DockerfileCOPY,若引用了仓库中不存在的文件,
      在构建之前就明确报出原因并列出缺哪个文件,不再让你对着 docker 的长报错猜
      (只判定确凿情况,跳过阶段拷贝 / 通配符 / URL,不会误报把正常更新挡下来)
    • 失败诊断:把 docker 报错归纳成一句人话(Dockerfile 缺文件 / 磁盘不足 /
      网络问题 / 权限不足)
    • 说明当前服务状态:构建失败时 compose 不会动已在运行的容器,
      日志里会明确写出「旧容器未被影响,服务正常」,避免误以为服务已挂

新增

  • 固定上游版本(设置 → 系统更新):可填提交号或标签,之后「更新上游」
    会检出该版本而不跟随分支,用于上游某提交有问题时一键回退
    留空即恢复跟随分支。版本值经严格校验后才会拼进 git 参数

改进

  • 任务记录改为「固定高度 + 内部滚动」,不再分页:分页虽能控制高度,
    但与右侧「上游原始日志」的滚动方式不一致,翻页也不如直接滚直观;
    且一页没填满时下方会留一片空白。现三块都用固定高度内滚,页面高度恒定
    (实测 7.5 屏 → 1.3 屏)
    • 单次拉取 200 条(手机上 80 条)以控制 DOM 规模
    • 命中上限时底部明确提示「共 N 条 · 仅显示最近 200 条,更早的请缩小时间范围」,
      不静默截断
    • 两块并排的卡片高度一致(470px),右侧不再有高度不齐的空白
    • 滚动条改为细样式:浏览器默认滚动条偏宽、带可见轨道,嵌在卡片里很突兀。
      新增 .scroll-slim(主题色 30% 透明滑块 + 透明轨道,悬停加深),
      并统一应用到任务记录的三个列表、表格容器与更新日志区
    • 手机上高度按视口自适应calc(100dvh-200px))而非套用桌面固定值:
      手机屏更矮,固定高度会明显偏短、卡片下方留一大片空白;
      dvh 而非 vh 是为了在地址栏收起/展开时也算得准