将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 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。
11 KiB
贡献指南(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 层;
只要动过 *.md,第 1.5 层必须跑:
| # | 命令 | 覆盖什么 | 需要什么 |
|---|---|---|---|
| 1 | python -m compileall -q workbuddy_portal manage.py tools |
语法 | — |
| 1.5 | python tools/check_docs.py |
文档自检:内部链接与跨文件锚点、图片引用、绝对路径泄漏、版本一致性、产品名硬编码。改过任何 md 都跑,否则章节重排造成的锚点静默失效会一路漏到线上。文档清单自动发现,不写死文件名 | — |
| 2 | python tools/smoke.py |
215 项离线断言:全页面只读渲染、模板残留检测、多用户隔离与凭证保密、注册与验证码、非管理员越权面全关死、全局键必须落在实例级、历史缺陷防回归、静态资源、CSS 类名对账 | 无(用 Flask test_client,不启服务) |
| 3 | python tools/check_live.py --base http://127.0.0.1:8848 --as demo:admin123 |
122 项真实 HTTP 断言,含登录/CSRF/开放重定向/验证码/安全响应头,--as 追加普通账号越权验收 |
一个运行中的服务 |
| 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。
四、必须遵守的几条不变量
这些是踩过坑之后定下来的,破坏它们会在生产上以很隐蔽的方式出问题:
- SQLite 只允许一个写者。 任何采集动作都要走
collect._Lock()(data/collect.lock)。 不要起多个带调度的进程;多实例部署时其余实例设WB_DISABLE_SCHEDULER=1。 - 列表类接口默认不返回
prompt全文。 它占原始体积约 80%。只有/api/top与/api/records带,且都做截断。新增接口请沿用这个约定。 - 两种字段命名契约不要互相「统一」:
/api/bundle用短键(d/c/k/m/cl/t/px,大屏页依赖),/api/records用可读全名。改错会让大屏静默渲染成空白。 - 流式响应里不要复用
db.get_db()。 Flask 在响应迭代开始前就会关掉请求上下文里的连接, 生成器一读库就Cannot operate on a closed database。要在生成器内部自建连接并finally关闭。 - Docker 部署用命名卷,不要退回绑定挂载。 Windows + Docker Desktop 走 9p, 宿主进程碰过 WAL 库之后容器会永久打不开数据库(详见 docs/ARCHITECTURE.md)。
.gitignore不支持行尾注释——规则后跟# 注释会让整行失效。注释必须单独占一行, 改完用git check-ignore -v <file>逐条确认命中。
多用户与权限相关的六条(v1.2.0 起,v1.3.0 扩充)
uid必须是conn之后的第一个位置参数,且不给默认值。 这是防越权的核心机制:漏传就直接TypeError,而不是静默返回所有人的数据。query.*/collect.*全链路都遵循它。新写一个查询函数时请照做,不要加uid=0这种默认值。- 凭证不参与回落。
db.NO_FALLBACK_KEYS = {"cookie", "user_agent"}: 个人级没有值时不许落回实例级,否则等于拿别人的 Cookie 去采集(串号)。 新增任何「账号身份相关」的配置键,都要考虑是否该进这个集合。 - 验证码答案只能放服务端。 不要图省事塞进
session——Flask 的 session 是 「签名 + base64」而非加密,客户端能直接解开读到答案。下发给浏览器的只有随机captcha_id; 且校验时先删后判(一次性)。同理,验证码图不要用 SVG 渲染,那玩意是文本。 - 写权限只有一个入口:
config.writable_by(key, is_admin)。(v1.3.0) 页面上的置灰 / 隐藏只是「不给误导性按钮」,真正的闸门是服务端判断 —— 所以别在模板里另写一套「哪些键只读」的条件,那必然会和接口判断漂移。 新增一个可配置项时,先决定它的归属:GLOBAL_KEYS(实例级、仅管理员) 还是USER_EDITABLE_KEYS(个人级、人人可写本人那份),然后只改这一处。 - 「键存哪一级」和「谁能写」必须对齐。(v1.3.0)
反例:把采集参数只写进管理员自己的
user_id,其它账号读取时会回落到DEFAULTS, 于是管理员改的值对别人完全不生效 —— 不报错、不进日志,是个纯粹的静默 bug。 所以实例级策略(调度、采集参数、注册策略)一律存user_id=0, 并由db.set_setting()强制重定向(slot:*这类个人簿记键除外,它们本来就该是个人级)。 user_id=0不是孤儿行。(v1.3.0) 实例级配置挂在一个不对应任何真实账号的user_id=0上。任何「清理孤儿行」的语句 都必须排除它 ——DELETE FROM settings WHERE user_id NOT IN (SELECT id FROM users)会把整片实例级配置删掉(历史缺陷:实测把 19 个实例级键清到只剩 1 行)。 现在tools/smoke.py里有「跑完整轮 smoke 后实例级配置一条不少」的防回归断言,别删。- 改主键的迁移必须「删索引 → 改名 → 建新表 → 回填 → 删旧表」。
ALTER TABLE … RENAME TO会把索引一起带走,后续CREATE INDEX IF NOT EXISTS就变成空操作,新表会零索引。本项目在迁移前先调_drop_all_user_indexes(), 并把ALTER TABLE … ADD COLUMN放在executescript之前。
加密与验证码这两块(零依赖约束)
crypto.py与captcha.py只能用标准库。 项目的硬约束是「只要 Flask / waitress / openpyxl」 —— 所以 ChaCha20、HMAC、PNG 编码、点阵字模都是手写的。想引cryptography或Pillow之前先想清楚:这会让「下载即跑」的卖点消失。- 解密失败必须显式报错,不能「失败就返回原值」。
crypto.decrypt()对非v1.前缀 原样返回(兼容历史明文),但校验不过就抛DecryptError。静默降级会让加密形同虚设。
五、代码风格
- 遵循 PEP 8;行宽 100。
- 注释与文档字符串用中文,说明「为什么这么做」而不是「这行在做什么」。
- 提交前用
python -m compileall与python tools/smoke.py自查;动过文档再加一条python tools/check_docs.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 里问,比写完再返工更省事。