将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 Cookie 与 User-Agent。 新增 config.writable_by 作为唯一写权限入口,set_setting 强制全局键落到 user_id=0, 消除「管理员改了只有自己生效」的静默缺陷。新增 tools/check_docs.py 文档自检, smoke 断言扩至 215 项、check_live 扩至 122 项并支持普通账号越权验收, 忽略 backups/、data/*.bak* 与 legacy-v1/,版本升至 v1.3.0。
21 KiB
变更日志
本项目遵循 语义化版本:主版本.次版本.修订号。
- 主版本:不兼容的变更(数据库迁移需手工介入、接口契约变化)
- 次版本:向后兼容的功能新增
- 修订号:向后兼容的缺陷修复
[1.3.0] — 2026-09-16
主题:权限收敛 · 配置作用域统一 · 目录规范化
把「配置存在哪一级」与「谁能改它」对齐成一条规则,并收紧普通账号的越权面。
数据不会丢:manage.py init 检测到 PRAGMA user_version 2 → 3 时自动迁移,
把管理员个人名下的调度与采集参数提升到实例级(user_id=0)后清掉个人残留,
全程写一条 promote_global_settings 审计,且可重复执行。
变更(不兼容)
- 调度与采集参数改为实例级,普通账号只读
GLOBAL_KEYS扩容:api_base/api_path/ 注册策略 4 键schedule_enabled/schedule_times/catch_up/catch_up_grace_hourspage_size/rewind_minutes/drift_tolerance_minutes/max_prompt/verify_days/timeout/ssl_verify
- 新增
config.USER_EDITABLE_KEYS = {cookie, user_agent}—— 普通账号唯一可写的两个键 - 新增
config.writable_by(key, is_admin):前后端与测试共用的唯一判断入口, 避免「页面置灰但接口还能写」这类规则漂移 - 为什么必须放实例级(而不是「个人级但只有管理员能写」):若只写在管理员自己的
user_id下,其它账号读取时会回落到DEFAULTS,管理员改的值对别人完全不生效 —— 那是个静默 bug。统一放实例级,语义是「一台部署一套采集与调度策略」。
set_setting()强制把全局键重定向到user_id=0:从结构上消除 「管理员改了只有自己生效」的可能/logs与/logs/tail改为仅管理员(原先普通账号能看到自己的采集历史 + 整机应用日志尾部)。普通账号访问返回 403,导航里不显示入口
新增
manage.py init自动迁移(uv 2 → 3):_promote_personal_to_global(conn)取首个管理员的个人级全局键值提升到user_id=0,再清除user_id<>0的残留; 幂等,可反复执行- 凭证类键刻意不灌实例级:
init_db()灌默认值时continue掉USER_EDITABLE_KEYS,并显式DELETE FROM settings WHERE user_id=0 AND key IN ('cookie','user_agent')—— 实例级存凭证等于给所有账号发同一张身份 /api/settings回传_userKeys/_role,/api/manifest回传role,/api/status新增is_admin/can_edit_schedule/can_view_logs—— 大屏是静态页,拿不到 Jinja 上下文,只能靠这几个字段决定显隐app.js:formData()跳过 disabled 控件(含祖先fieldset[disabled]): disabled 的 input 仍在form.elements里,一起提交会让服务端因「越权修改只读项」 拒掉整单;现在只读项既不显示也不参与提交- 测试:
tools/smoke.py162 → 215 项断言(新增「非管理员越权面必须全部关死」 与「全局键必须落在实例级」两节);tools/check_live.py83 → 122 项, 新增--as USER:PASS参数与第 12 节「普通账号真实 HTTP 越权验收」 - 新增
tools/check_docs.py(文档自检):内部链接与跨文件锚点、图片引用、 绝对路径泄漏(连带会泄漏用户名)、版本一致性(__init__/Dockerfile/README/CHANGELOG四处)、模板与 JS 里的产品名硬编码。 文档互相引用后章节一重排,锚点会静默失效 —— Markdown 自己不报错、CI 也不管, 只能靠这一层。有问题即退出码 1(只想看报告不失败用--no-fail)。 文档清单自动发现,不写死文件名 —— 写死列表的那版曾漏掉THIRD-PARTY-NOTICES.md/CODE_OF_CONDUCT.md(覆盖面 13 → 9 个文件且毫无提示)。 锚点比对刻意不逐字复刻 GitHub/Gitea 的 slug 算法(各家对+/:/连续空格的 处理并不一致,写死一个实现换个托管平台就批量误报),改为只比较「有效字符」 (小写字母 / 数字 / 汉字),既不受标点差异干扰,章节真被改名时又照抓不误
修复
api_status引用了未定义的变量u⇒/api/status稳定 500。 该缺陷由check_live.py新增的真实 HTTP 验收抓到,此前 smoke 完全没有覆盖这个接口tools/smoke.py的清理语句会把实例级配置当孤儿删掉 (DELETE FROM settings WHERE user_id NOT IN (SELECT id FROM users), 而user_id=0不是任何真实账号)⇒ 每跑一轮 smoke 就清空一次实例级配置。 实测曾把 19 个实例级键清到只剩 1 行。已加user_id<>0 AND并补防回归断言tools/smoke.py哨兵 UA 还原会留下多余行:原本实例级无user_agent行时set_setting(..., "")会插一行空串。改为「原本无则 DELETE」,断言也改成比行为而非比行
目录规范化
- 工作区根目录的 v1.0 单文件版(
fetch_usage.py、dashboard/、data/usage_records.csv) 收进工作区级legacy-v1/,附带 README 说明「已被取代、可安全删除」;config.LEGACY_CSV_CANDIDATES第一候选同步指向新位置 - 新增
backups/作为数据库快照的统一落点(刻意不放在data/——data/是 Docker 卷,down -v会把备份和正本一起删掉) .gitignore补backups/*、data/*.bak*、legacy-v1/三条兜底规则- 清理
data/shots/(已被docs/images/取代)与全部__pycache__
文档
docs/DEPLOYMENT.md重写:三条并列的部署路径(裸机 / Docker 自打包 / docker-compose 拉云端镜像),配置项速查表按新作用域重排docs/USER-GUIDE.md重写:新增「权限与数据边界」「信息安全与隐私安全」两章, 截图重出为普通账号视角README.md/SECURITY.md/CONTRIBUTING.md/docs/ARCHITECTURE.md/docs/API.md/docs/FAQ.md同步权限模型、配置作用域与验证层变化; 订正了 CONTRIBUTING / ARCHITECTURE / FAQ 里残留的旧断言数(165/83 → 215/122)
[1.2.0] — 2026-09-15
主题:多用户化 · Cookie 加密 · 开放注册与验证码
从单用户版升级到多用户版。数据不会丢:manage.py init 会自动检测旧表结构并迁移
(PRAGMA user_version 0 → 2),历史用量归到首个账号、明文 Cookie 就地加密,
全程写一条 schema_migrate / encrypt_secrets 审计,且可重复执行。
新增
- 多用户与数据隔离
users表补齐email/status/register_ip/last_login_ip;settings主键改为(user_id, key),usage_records改为(user_id, request_id), 全部索引以user_id打头;collect_runs/audit_log增加user_idquery.py/collect.py/scheduler.py全链路把uid作为conn之后的 第一个位置参数且无默认值 —— 漏传直接TypeError,不会退化成「返回全量」- 配置三级回落:
个人 → 实例(user_id=0) → config.DEFAULTS; 新增NO_FALLBACK_KEYS = {cookie, user_agent},凭证永不回落(回落即串号越权) scheduler.tick()遍历启用账号逐个判断槽位;未配 Cookie 的账号自动跳过- 新增
manage.py users/stats -u/status(逐账号)/ 各子命令的-u/--user
- Cookie 静态加密(
workbuddy_portal/crypto.py,约 190 行,零第三方依赖)- 手写 ChaCha20 块函数(RFC 8439 §2.3)+ HMAC-SHA256 encrypt-then-MAC,
密文格式
v1.<b64salt>.<b64nonce>.<b64ct>.<b64tag>,已用官方测试向量逐字节验证 - 主密钥
cookie_key独立存放在data/instance.json(与SECRET_KEY分开键位) db.get_secret()是取明文的唯一通道;get_settings()把加密键一律置空;db.secret_state()只回{set, chars, tail, broken},绝不含明文decrypt()对非v1.前缀原样返回(兼容历史明文,下次写入自动升级), 校验失败抛异常而不是「失败就返回原值」;升级时自动把历史明文加密
- 手写 ChaCha20 块函数(RFC 8439 §2.3)+ HMAC-SHA256 encrypt-then-MAC,
密文格式
- 开放注册:
/register页 +POST /api/users(管理员);开关allow_register、 同 IP 每日配额register_max_per_ip;用户名/密码强度校验(保留字黑名单、≥8 位且两类字符) - 图形验证码(
workbuddy_portal/captcha.py,约 250 行,零第三方依赖)- 手写 PNG 编码器(zlib 压缩 IDAT)+ 5×7 点阵字模 + Bresenham 干扰线与噪点。 刻意不用 SVG —— SVG 是文本,答案会明文出现在页面源码里
- 答案只写服务端
captchas表;会话里仅存随机 id;一次性、5 分钟过期、按用途隔离 - 策略
captcha_policy:always(默认)/adaptive(同来源失败 2 次后要求)/off - 登录先验验证码再比口令(否则攻击者能拿「密码对不对」当信号提前跑完字典)
- 安全加固
- 失败限速改为 IP + 用户名双维度,任一超限即锁;新增验证码出图限速(60s/40 张)
current_user()每请求回查users.status⇒ 停用账号立即失效,不必等会话过期- 安全响应头:CSP /
X-Frame-Options/nosniff/Referrer-Policy/ COOP;/api/*与/captcha*带no-store - 会话 cookie 显式
HttpOnly+SameSite=Lax+Path=/;WB_COOKIE_SECURE=1可开 Secure /logs/tail改为仅管理员;管理员不能停用/降权/删除自己
- 页面:新增
/login验证码、/register、/profile(个人中心,点右上角用户名进入);/config增加凭证状态与cookie_broken告警、实例级设置区;/users增加邮箱/状态列与启停
变更
- 接口新增:
GET/POST /api/profile、POST /api/captcha(机制自述)、POST /api/users/<id>/delete;GET /api/settings回传_globalKeys/_canEditGlobal /api/collect未配 Cookie 回409 no_cookie,密文解不开回409 cookie_broken(不再静默当成「未配置」)/records/export与 CLIexport-csv的默认文件名带账号名(多用户下同名会互相覆盖)WB_COOKIE环境变量兜底已移除 —— 它会导致串号
修复
db.get_db()在流式响应里被复用导致Cannot operate on a closed database(生成器内部改为自建连接).dockerignore的__pycache__/只匹配上下文根目录,嵌套目录会被打进镜像- 注册成功提示与注册页说明里的
**强调**字面量(HTML 不解析 Markdown) base.html顶层的{% set me = current_user() %}会覆盖子模板传入的同名变量 —— 而current_user()只含id/username/display_name/is_admin,于是个人中心把me.created_at渲染成空(「注册于 ·」)。局部变量改名cur,smoke.py加 3 条防回归断言WB_COOKIE_SECURE没有写进docker-compose.yml的environment:—— 在.env里设了也不生效,文档里的开关实际是哑的(已补上,并加进.env.example)- 「配置管理 → 修改登录密码」提示写「至少 6 位」,与实际策略(≥8 位 + 两类字符)不符
- 「用户管理」删除说明写「可勾选保留」,而页面只有确认框、必删数据,措辞改为与实际一致
升级提示
- 纯 HTTP 局域网部署不要设
WB_COOKIE_SECURE=1,否则浏览器不回传会话 cookie, 表现为「刚登录完又被弹回登录页」 - 迁移后请到「配置管理」确认 Cookie 状态;
secret_state.broken = true说明data/instance.json里的cookie_key与写入时不一致,重新粘贴一次即可
[1.1.0] — 2026-09-14
主题:项目定名 workbuddy-portal · 容器化 · 文档体系
新增
- Docker 化:多阶段
Dockerfile(依赖层与运行层分离,改业务代码不触发重装依赖)、docker-compose.yml(单服务、数据放 Docker 命名卷、健康检查、日志轮转)、docker-compose.hostdir.yml(可选叠加层:把数据/日志放到宿主机目录,仅建议 Linux)、docker/entrypoint.sh(幂等初始化 → exec 交接)、docker/healthcheck.py(纯标准库)、.dockerignore、.env.example - 容器环境变量:
WB_HOSTWB_PORTWB_DATA_DIRWB_LOG_DIRWB_DBWB_ADMIN_USERWB_ADMIN_PASSWORDWB_DISABLE_SCHEDULERWB_IMPORT_CREDSWB_IMPORT_XLSX - 文档体系
docs/: 用户使用手册(含 9 张界面截图,全部用合成示例数据渲染)、 部署与运维指南(含推镜像到 Gitea 注册表的完整流程)、 架构与设计说明、 接口参考、 常见问题 - 开源声明体系(仓库根目录):LICENSE(MIT)、
THIRD-PARTY-NOTICES.md(依赖清单与再分发合规自查)、
CONTRIBUTING.md(含「必须遵守的不变量」与五层自检方法)、
SECURITY.md、CODE_OF_CONDUCT.md、
.github/下的 Issue 表单与 PR 模板、.editorconfig, 以及给全部 Python / Shell 源文件加SPDX-License-Identifier: MIT头 tools/demo_data.py:生成完全合成的示例库(假模型名 / 假 Prompt / 偏斜的积分分布), 写入data/demo/(已在.gitignore内)。文档截图与本地调试都基于它, 任何人不需要真实账号就能复现整套界面.gitattributes:强制*.sh/Dockerfile/ 各类源码为 LF (带 CRLF 的.sh在容器里会报exec format error,极难定位)
变更
-
文档数据脱敏:9 张界面截图全部改用合成示例数据重拍;
docs/API.md、docs/DEPLOYMENT.md示例响应里的真实模型名与真实统计数字一并替换为示例口径。 此前截图中含真实 Prompt 全文、本机路径与本机用户名,属于不该公开的内容 -
.gitignore补强:只写data/*.sqlite会漏掉子目录,改为同时保留data/**/*.sqlite等规则,并新增data/demo/忽略——否则tools/demo_data.py的产物会被误提交 -
项目定名:
wb_usage_portal→workbuddy-portal; Python 包wb_usage→workbuddy_portal;会话 cookiewb_usage_sid→workbuddy_portal_sid(升级后需要重新登录) -
界面品牌统一为 WorkBuddy Portal(此前为「WorkBuddy 用量门户」)
-
项目标识收敛到
config.PROJECT_NAME/PROJECT_TITLE/PROJECT_DESC单一来源, 模板通过app_name等上下文变量引用,不再多处硬编码 -
数据 / 日志目录支持环境变量覆盖(
WB_DATA_DIR/WB_LOG_DIR/WB_DB) -
版本号 1.0.0 → 1.1.0
-
大屏页标题改为「WorkBuddy Portal · 积分消耗大屏」
修复
| # | 症状 | 根因 |
|---|---|---|
| 1 | /records/export 必然 500 |
生成器在请求上下文销毁后才被迭代,复用 db.get_db() 撞「数据库已关闭」。改为生成器内自建连接 |
| 2 | 大屏页图表全白 | /dashboard 无尾斜杠,src="vendor/echarts.min.js" 被解析成 /vendor/… → 404 |
| 3 | /users 500 |
路由已注册但 users.html 不存在 |
| 4 | 明细页日期筛选失效 | 视图传 f.frm、模板读 f.from;导出链接拼 ?frm= 而接口只认 from |
| 5 | 配置页 3 个维护按钮全死 | 模板调 WBU.bindMaint(),app.js 里没有该函数 |
| 6 | 审计只能看最近 40 条 | LIMIT 40 写死 |
| 7 | 明细页多跑一条无用 SELECT |
day_list() 取了没人用 |
| 8 | 登录页锁定阈值写死 | 未从配置注入 |
#2 值得单独一提:前两层测试都没抓到(离线断言只看状态码 +
<html>, 真实 HTTP 只看状态码),是加上 Playwright 截图后才发现的。 据此给tools/smoke.py补了「页面所有src/href资源引用逐个断言 200」一节。
另外在容器实测中发现并修掉一个部署期才暴露的问题:
| 症状 | 根因 | 处理 |
|---|---|---|
容器跑着跑着页面全 500,日志里 sqlite3.OperationalError: unable to open database file |
数据原本用绑定挂载;Windows + Docker Desktop 走 9p,宿主的 Windows 进程只要访问过这个 WAL 库(纯读也会触发),容器侧下一次连接就重建不了 -shm,且不会自愈 |
改为 Docker 命名卷(容器独占数据目录);需要宿主目录时叠加 docker-compose.hostdir.yml(仅建议 Linux) |
最小复现:容器正常 → 宿主跑一次 manage.py stats → 容器立刻打不开库、且重启前不再恢复。
已写进 DEPLOYMENT.md 第十一节。
安全
- 新增
security.safe_next():登录跳转的next拒绝//evil.com(协议相对 URL)、/\evil.com、绝对地址与含 CR/LF 的值 - 缺 CSRF 的写请求统一 400(此前部分路径漏检)
- 默认开启云端 HTTPS 证书校验(
ssl_verify=1)。Cookie 是账号凭证,不该裸奔 - 登录失败计数表加上限(4096 个 IP)与 TTL(1 小时),防内存被大量来源 IP 撑爆
/logout拆分为 POST(执行)+ GET(仅提示),防<img src="/logout">静默退出settings的内部簿记键(slot:*)读写两侧都过滤,不再从/api/settings泄漏
内部质量
- 设置项写时校验 + 读时兜底:
config.normalize_setting()(范围 + 单位,非法值 400 并列出全部错误)+ 采集路径全面改用db.get_int(),杜绝「一个手滑的数字让采集整个跑不起来」 - 全局
ValueError→ 400 处理器:手写 query string 不再能把 500 页面暴露出去 - 日期归一化
norm_day()/norm_window()(容忍2026/09/08、带时间、起止写反) - CSV 导出改用
csv.writer流式写入(此前手工拼字符串,model/client含逗号会串列) bundle明细加下限(BUNDLE_RECORDS_CAP = 20000),并返回recordsTotal/recordsCap/recordsTruncated,不静默丢数据- 轮询日志尾部改用
collections.deque(maxlen=n),不再把整个文件读进内存 - 修掉一条非法 CSS 声明
font: 13px/1.5 inherit(简写里inherit不能当字族, 整条被浏览器丢弃,输入框一直用默认字体)
工具
- 新增
tools/smoke.py:离线回归 99 项断言(全页面只读渲染 + 模板残留 + 历史缺陷防回归 ①~⑭ + 静态资源逐个 200 + CSV 列 + 页面 class ↔app.css选择器对账), 不需要先起服务 tools/check_live.py扩充到 56 项断言,新增用户管理 / 审计 / 流式导出 / 开放重定向 / CSRF 四节- 新增
tools/shots.py:Playwright 登录后逐页截图 + 收集 console / pageerror (内建可执行文件探测,规避驱动与本机浏览器版本错位)
[1.0.0] — 2026-09-14
主题:从「脚本 + CSV + 静态大屏」演化为独立可部署的门户
新增
- 独立 Flask 应用:
create_app工厂 +views/api双蓝图 - SQLite(WAL)作为唯一数据正本,主键去重、断点续采、聚合全部下推 SQL
- 进程内调度线程(20 秒轮询 + 槽位去重 + 启动补跑),不再依赖外部计划任务
- 单写者文件锁
data/collect.lock(含 30 分钟僵尸锁抢占) - 登录鉴权(
pbkdf2:sha256:200000)、全站 CSRF、同 IP 失败限速 - 后台页面:概览 / 数据明细 / 任务管理 / 配置管理 / 日志管理
- 独立 ECharts 交互大屏
/dashboard(离线自带 echarts,不依赖 CDN) - CLI:
initservecollectmigrate-csvimport-xlsximport-credsfill-promptexport-csvstatsstatuspasswdvacuum - 从旧版 CSV / 官网 xlsx / 编辑器设置导入的迁移通道
- 从 VSCode / Cursor / Trae 的
settings.json接管 Cookie 与 User-Agent
变更
- 数据正本从 CSV 改为 SQLite;CSV 降级为导出物(
data/exports/) - 采集从外部脚本改入 Web 进程;删除全部外部自动化与计划任务
- 运行期配置(Cookie、调度、采集参数)从文件搬进数据库,由后台页面维护
版本对照
| 版本 | 数据正本 | 调度 | 部署 | 鉴权 |
|---|---|---|---|---|
| 1.1.0 | SQLite(WAL) | 进程内 | Docker Compose / 裸机 | 登录 + CSRF + 角色 |
| 1.0.0 | SQLite(WAL) | 进程内 | 裸机 | 登录 + CSRF |
| < 1.0 | CSV 文件 | 外部计划任务 | 脚本 | 无(仅局域网) |