文件
workbuddy-portal/CONTRIBUTING.md
T
wangchuanli df7db3582e feat(multi-user): 多用户化 + 凭证加密 + 自助注册与图形验证码
数据隔离
- settings / usage_records 主键改为 (user_id, key) / (user_id, request_id),
  索引一律以 user_id 打头;collect_runs / audit_log 增加 user_id
- query / collect / scheduler 全链路把 uid 作为 conn 之后的第一个位置参数且无默认值
  (漏传直接 TypeError,不会退化成「返回全量」)
- 配置三级回落 个人→实例→DEFAULTS;NO_FALLBACK_KEYS={cookie,user_agent} 不回落

凭证保密
- 新增 workbuddy_portal/crypto.py:手写 ChaCha20(RFC8439 §2.3) + HMAC-SHA256
  encrypt-then-MAC,零第三方依赖;主密钥 cookie_key 与 SECRET_KEY 分键位存放
- get_secret() 是取明文的唯一通道;get_settings() 把加密键置空;
  secret_state() 只回 {set,chars,tail,broken};升级时自动加密历史明文

注册与验证码
- 新增 /register 与 workbuddy_portal/captcha.py(手写 PNG + 点阵字模 + 干扰线)
- 验证码答案只存服务端表、不进 session,一次性、5 分钟过期、按 purpose 隔离
- allow_register / register_max_per_ip / captcha_policy / captcha_length 四个实例级开关
- 失败限速改为 IP + 用户名双维度;停用账号每请求回查、立即失效

页面
- 新增 /profile(个人中心)与注册页;登录页加验证码与自助注册入口
- /config 增加凭证状态、cookie_broken 告警、实例级设置区;/users 增加邮箱/状态与启停

修复
- base.html 顶层 {% set me %} 覆盖子模板同名变量,导致个人中心「注册于」渲染为空
- WB_COOKIE_SECURE 未写进 compose 的 environment,在 .env 里设了不生效
- 「修改登录密码」提示写「至少 6 位」,与实际策略(≥8 位 + 两类字符)不符
- 「用户管理」删除说明写「可勾选保留」,与页面实际行为不符
- 注册页与 flash 文案里的 **强调** Markdown 字面量

验证与文档
- smoke.py 99 → 165 项断言(多用户隔离 / 凭证保密 / 注册与验证码 / 3 条防回归)
- check_live.py 56 → 83 项断言(新增注册 / 验证码 / 安全响应头一节)
- demo_data.py 造两个账号;shots.py 自动过验证码、重出 11 张截图
- README / SECURITY / ARCHITECTURE / API / DEPLOYMENT / USER-GUIDE / FAQ / CHANGELOG / CONTRIBUTING 同步
2026-09-15 17:32:35 +08:00

8.5 KiB
原始文件 Blame 文件历史

贡献指南(Contributing)

感谢你有兴趣改进 WorkBuddy Portal。这是一个单进程 Flask + SQLite 的轻量项目, 刻意保持了很小的依赖面与很平的目录结构——请先花两分钟读完本文,你的改动会更顺利被接受。


一、开发环境

项 要求
Python 3.13(Docker 镜像用的就是 3.13-slim;3.11+ 一般也可)
操作系统 Linux / macOS / Windows 均可,但注意下面「Windows 特有坑」一节
依赖 pip install -r requirements.txt
可选 Playwright + Chromium(只有跑界面截图才需要)

起步:

git clone <你的仓库地址> workbuddy-portal
cd workbuddy-portal
python -m venv .venv && . .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -r requirements.txt

python manage.py init                              # 建表 + 建管理员(默认 admin/admin123)
python manage.py serve --port 8848                 # 或 docker compose up -d --build

二、用示例数据开发,别用真实数据

tools/demo_data.py 会生成一份完全合成的数据集(假模型名、假 Prompt、假 Cookie 串、 偏斜的积分分布),放在 data/demo/ 下(该目录已在 .gitignore 内)。 它会造两个账号(admin 管理员 + demo 普通用户),好让你顺手验证多用户隔离:

python tools/demo_data.py                          # 默认 data/demo;两个账号口令都是 admin123
WB_DATA_DIR=$PWD/data/demo python manage.py serve --port 8849 --no-scheduler

目录里的 usage.sqlite 在容器里生成再截图才是对的:宿主机跑会让启动日志印出 C:\Users\<用户名>\…,那一行正好会出现在「日志管理」页的截图上。

这样你既能有一个「看起来像真的」的界面来调试,也不会把任何真实用量带进仓库或截图。

三、改动前请先跑一遍验证

项目有一层层递进的验证,代价从低到高,改完至少跑到第 2 层:

# 命令 覆盖什么 需要什么
1 python -m compileall -q workbuddy_portal manage.py tools 语法 —
2 python tools/smoke.py 165 项离线断言:全页面只读渲染、模板残留检测、多用户隔离与凭证保密、注册与验证码、历史缺陷防回归、静态资源、CSS 类名对账 无(用 Flask test_client,不启服务)
3 python tools/check_live.py --base http://127.0.0.1:8848 83 项真实 HTTP 断言,含登录/CSRF/开放重定向/验证码/安全响应头 一个运行中的服务
4 python tools/shots.py --base http://127.0.0.1:8849 登录后逐页截图并收集 console/pageerror Playwright + Chromium
5 docker compose up -d --build && docker compose ps 容器化路径 Docker

tools/shots.py 是最有价值的一层:项目曾经出过「大屏整页全白」的 bug, 只有它抓到了(smoke 与 check_live 都放过了)。改前端务必跑。

check_live.py 与 shots.py 都接受 --db <路径>:给了之后它们会直接从库里读验证码答案, 从而自动过掉登录页与注册页的验证码——不然脚本会被验证码挡在门外。

这三支脚本都会打印 RESULT: ok=N fail=0,fail 不为 0 时退出码是 1,可直接接进 CI。

四、必须遵守的几条不变量

这些是踩过坑之后定下来的,破坏它们会在生产上以很隐蔽的方式出问题:

  1. SQLite 只允许一个写者。 任何采集动作都要走 collect._Lock()(data/collect.lock)。 不要起多个带调度的进程;多实例部署时其余实例设 WB_DISABLE_SCHEDULER=1。
  2. 列表类接口默认不返回 prompt 全文。 它占原始体积约 80%。只有 /api/top 与 /api/records 带,且都做截断。新增接口请沿用这个约定。
  3. 两种字段命名契约不要互相「统一」:/api/bundle 用短键(d/c/k/m/cl/t/px,大屏页依赖), /api/records 用可读全名。改错会让大屏静默渲染成空白。
  4. 流式响应里不要复用 db.get_db()。 Flask 在响应迭代开始前就会关掉请求上下文里的连接, 生成器一读库就 Cannot operate on a closed database。要在生成器内部自建连接并 finally 关闭。
  5. Docker 部署用命名卷,不要退回绑定挂载。 Windows + Docker Desktop 走 9p, 宿主进程碰过 WAL 库之后容器会永久打不开数据库(详见 docs/ARCHITECTURE.md)。
  6. .gitignore 不支持行尾注释——规则后跟 # 注释 会让整行失效。注释必须单独占一行, 改完用 git check-ignore -v <file> 逐条确认命中。

多用户相关的四条(v1.2.0 起)

  1. uid 必须是 conn 之后的第一个位置参数,且不给默认值。 这是防越权的核心机制:漏传就直接 TypeError,而不是静默返回所有人的数据。 query.* / collect.* 全链路都遵循它。新写一个查询函数时请照做,不要加 uid=0 这种默认值。
  2. 凭证不参与回落。 db.NO_FALLBACK_KEYS = {"cookie", "user_agent"}: 个人级没有值时不许落回实例级,否则等于拿别人的 Cookie 去采集(串号)。 新增任何「账号身份相关」的配置键,都要考虑是否该进这个集合。
  3. 验证码答案只能放服务端。 不要图省事塞进 session——Flask 的 session 是 「签名 + base64」而非加密,客户端能直接解开读到答案。下发给浏览器的只有随机 captcha_id; 且校验时先删后判(一次性)。同理,验证码图不要用 SVG 渲染,那玩意是文本。
  4. 改主键的迁移必须「删索引 → 改名 → 建新表 → 回填 → 删旧表」。 ALTER TABLE … RENAME TO 会把索引一起带走,后续 CREATE INDEX IF NOT EXISTS 就变成空操作,新表会零索引。本项目在迁移前先调 _drop_all_user_indexes(), 并把 ALTER TABLE … ADD COLUMN 放在 executescript 之前。

加密与验证码这两块(零依赖约束)

  1. crypto.py 与 captcha.py 只能用标准库。 项目的硬约束是「只要 Flask / waitress / openpyxl」 —— 所以 ChaCha20、HMAC、PNG 编码、点阵字模都是手写的。想引 cryptography 或 Pillow 之前先想清楚:这会让「下载即跑」的卖点消失。
  2. 解密失败必须显式报错,不能「失败就返回原值」。 crypto.decrypt() 对非 v1. 前缀 原样返回(兼容历史明文),但校验不过就抛 DecryptError。静默降级会让加密形同虚设。

五、代码风格

  • 遵循 PEP 8;行宽 100。
  • 注释与文档字符串用中文,说明「为什么这么做」而不是「这行在做什么」。
  • 提交前用 python -m compileall 与 python tools/smoke.py 自查。
  • 不要引入新的第三方依赖,除非有充分理由并在 PR 里说明——这个项目的卖点之一就是依赖少。 如果确实新增了,请同步登记到 THIRD-PARTY-NOTICES.md。

六、提交与 PR

提交信息用 Conventional Commits,一句话说清「改了什么」即可,正文可写动机:

fix(docker): 数据改用 Docker 命名卷,修容器打不开数据库的问题
feat(api): 新增 /api/export 支持按筛选条件导出 CSV
docs: 补充反向代理部署示例

PR 请包含:

  • 动机:解决什么问题,或复现步骤
  • 改动:涉及哪些文件、有没有破坏性变更
  • 验证:贴出 smoke / check_live 的 RESULT 行;改前端再贴截图脚本的输出
  • 若改动影响数据口径(聚合、去重、时区),请额外写一份独立聚合与之比对, 不要只靠肉眼看页面

七、Windows 特有坑(若你在 Windows 上开发)

  • 宿主 Windows 进程访问过 data/usage.sqlite 后,容器内会打不开同一个库。 用命名卷部署时,宿主侧跑 CLI 请一律走 docker compose exec portal python manage.py …。
  • Git 的 /tmp 会被解析成 C:\tmp,git commit -F /tmp/msg.txt 会失败——用仓库内路径。
  • 本机若开着 HTTP 代理,curl http://127.0.0.1:… 会被代理拦成 502,探测本地服务要加 --noproxy '*'。
  • 行尾:仓库用 .gitattributes 锁死 eol=lf,docker/*.sh 若带 CRLF 会在容器里报 exec format error。

有任何不确定的地方,先在 Issue 里问,比写完再返工更省事。