将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 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。
434 行
32 KiB
Markdown
434 行
32 KiB
Markdown
# WorkBuddy Portal
|
||
|
||
> **workbuddy-portal** —— WorkBuddy 积分用量「采集 / 存储 / 呈现」一体化门户
|
||
|
||
一个独立部署的 Python / Flask 应用:把账号云端的用量明细按时采集下来、按 `request_id`
|
||
去重存档,再以「**管理后台**(配置 / 任务 / 日志 / 明细)+ **ECharts 交互大屏**」两种形态呈现。
|
||
**不依赖任何外部计划任务或自动化**——调度线程就跑在 Web 进程里。
|
||
|
||
| | |
|
||
|---|---|
|
||
| 语言 / 框架 | Python 3.11+ · Flask 3 · Jinja2 · 纯标准库 `urllib` 采集 |
|
||
| 存储 | SQLite(WAL),单文件正本 `data/usage.sqlite` |
|
||
| 前端 | 服务端渲染后台 + 独立 ECharts 大屏(离线自带的 `echarts.min.js`) |
|
||
| 部署 | **三条路径**:裸机 / Docker 自打包 / compose 拉云端镜像;镜像可推 Gitea 容器注册表 |
|
||
| 鉴权 | **多用户**(各自的数据与凭证严格隔离)+ 全站登录 + CSRF + 角色(管理员 / 普通) |
|
||
| 权限 | 普通账号**只能维护本人的 Cookie / User-Agent**;调度频率、采集参数、日志、用户管理都归管理员 |
|
||
| 凭证 | Cookie **ChaCha20 + HMAC 静态加密**入库,页面与接口只回掩码 |
|
||
| 防攻击 | 登录 / 注册**图形验证码**(服务端出题 + 一次性)、失败限速、注册限额 |
|
||
| 版本 | v1.3.0 |
|
||
| **许可证** | **MIT**(第三方组件与再分发资源见 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)) |
|
||
|
||
**目录**:[核心特性](#核心特性) · [架构](#架构一图) · [快速开始](#快速开始) · [命令一览](#命令一览) ·
|
||
[页面一览](#页面一览) · [接口一览](#接口一览) · [目录结构](#目录结构) · [文档导航](#文档导航) · [安全须知](#安全须知) · [开源与许可](#开源与许可)
|
||
|
||
---
|
||
|
||
## 核心特性
|
||
|
||
| 能力 | 说明 |
|
||
|---|---|
|
||
| **多用户隔离** | 每个账号只填**自己的** Cookie、收**自己的**数据、看**自己的**记录。`user_id` 是所有查询的第一个条件,且是**必填位置参数**(漏传直接 `TypeError`,不会静默返回全量) |
|
||
| **两级权限** | 注册出来的账号一律是**普通账号**,能改的只有本人凭证(`cookie` / `user_agent`)。写权限只有一条规则:`config.writable_by(key, is_admin)` —— 页面与接口共用同一个函数,避免「界面置灰但接口还能写」这类规则漂移 |
|
||
| **配置作用域对齐** | 「键存在哪一级」与「谁能改」是一件事:实例级键(调度、采集参数、注册策略…)一律存 `user_id=0` 且仅管理员可写。**不会出现「管理员改了只有自己生效」**(那样别人读时会回落到默认值,是个静默 bug) |
|
||
| **凭证加密** | Cookie 以 ChaCha20(RFC 8439)+ HMAC-SHA256 encrypt-then-MAC 密文入库;主密钥单独放在 `data/instance.json`,与 `SECRET_KEY` 分开。升级时会把历史明文自动加密 |
|
||
| **自助注册 + 验证码** | 开放注册(可关),登录/注册均带**图形验证码**。答案是服务端本地点阵渲染的 PNG,只存库、一次性、5 分钟过期——**不进会话**(Flask 会话是签名不加密的,放进去等于送答案) |
|
||
| **增量采集** | 按 `MAX(ts)` 断点续采 + 回退窗口;主键 `ON CONFLICT` 去重,冲突时以「更早的本地时间」为准 |
|
||
| **进程内调度** | 每天固定时刻(默认 `09:00,17:00`)由内置线程逐个启用账号触发;支持**启动补跑**(程序没开时错过的时刻,开机后在宽限期内补上) |
|
||
| **单写者保证** | 文件锁 `data/collect.lock` 让「调度 / 页面手动触发 / CLI」三处不并发写 SQLite;僵尸锁 30 分钟可抢占 |
|
||
| **全量存档** | 不随官网导出窗口过期而丢数据;官网 xlsx 丢失约 22% 的 `Prompt`,可用 `fill-prompt` 回补 |
|
||
| **大屏去中间层** | 大屏直接走 `/api`,按当前筛选窗口实时聚合;左侧多取等长一段用于算环比,窗口不变不重复请求 |
|
||
| **可观测** | 每次采集落一条 `collect_runs`(含 `[warn]`/`[error]` 逐行原文);另有操作审计与登录审计,均带账号归属 |
|
||
| **一键备份** | 正本就是宿主机上的一个 `.sqlite` 文件,拷走即可;`manage.py vacuum` 回收空闲页 |
|
||
|
||
---
|
||
|
||
## 架构一图
|
||
|
||
```
|
||
┌──────────────── workbuddy-portal(单进程)────────────────┐
|
||
云端用量接口 │ │
|
||
/billing/meter/ │ scheduler.py ──┐ 按实例级时刻表遍历启用账号 │
|
||
get-user-request- │ (20s 轮询槽位) │ │
|
||
usage │ ▼ │
|
||
▲ │ collect.py ─ 文件锁 collect.lock ─ 去重 upsert ─▶ SQLite │
|
||
│ │ ▲ (全部带 user_id) data/usage.sqlite(WAL)│
|
||
└───────────┼──────┘ ▲ │
|
||
client.py(urllib)│ ▲ crypto.py 解密本账号 Cookie │ │
|
||
│ └ captcha.py 出验证码图 │ │
|
||
│ query.py(uid 为第一个查询条件) │
|
||
│ ▲ ▲ │
|
||
│ web/views.py ──────┘ └──── web/api.py│
|
||
│ (Jinja 后台) (JSON) │
|
||
└───────────────┬───────────────────────────┬──────────────┘
|
||
▼ ▼
|
||
/ /records /tasks /config /dashboard(ECharts 大屏)
|
||
/profile [/logs /users:仅管理员] + 未登录:/login /register
|
||
```
|
||
|
||
五层职责:
|
||
|
||
| 层 | 位置 | 说明 |
|
||
|---|---|---|
|
||
| 采集 | `workbuddy_portal/collect.py` + `scheduler.py` | 纯 `urllib` 调云端;断点、去重、锁、导入导出,全部按 `uid` 隔离 |
|
||
| 存储 | `workbuddy_portal/db.py` + `schema.sql` | SQLite WAL,单写者;运行期配置也在库里(`settings` 表,主键 `(user_id, key)`) |
|
||
| 加固 | `workbuddy_portal/crypto.py` + `captcha.py` | 凭证静态加密(零第三方依赖手写 ChaCha20);验证码用**自写 PNG 编码器**出图 |
|
||
| 聚合 | `workbuddy_portal/query.py` | `daily / dims / top / records / summary / bundle`,全部下推 SQL,`uid` 是第一个条件 |
|
||
| 呈现 | `workbuddy_portal/web/` | Jinja 后台(`views.py`)+ JSON API(`api.py`)+ 静态大屏 |
|
||
|
||
> **为什么验证码不用 SVG、也不用第三方库**:SVG 是文本,答案会明文出现在页面源码里;
|
||
> 而本项目坚持 `requirements.txt` 只有 Flask / waitress / openpyxl,所以 PNG 编码器
|
||
> (zlib 压缩 IDAT)与 5×7 点阵字模都是手写的,见 [架构说明](docs/ARCHITECTURE.md#凭证加密与验证码)。
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
三条路径产出的是**同一份代码、同一个数据库格式**,可以互相迁移。完整参数与排错见
|
||
[部署与运维指南](docs/DEPLOYMENT.md)。
|
||
|
||
### 路径 A:裸机 Python
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
|
||
python manage.py init --user admin --password '一个足够强的密码'
|
||
python manage.py import-creds # 可选:把编辑器设置里的 cookie/UA 接管进数据库
|
||
python manage.py migrate-csv # 可选:把旧版 CSV 存档全量导入
|
||
python manage.py serve # 启动,默认 0.0.0.0:8848
|
||
```
|
||
|
||
### 路径 B:Docker 自打包
|
||
|
||
```bash
|
||
git clone https://git.iwali.top/wangchuanli/workbuddy-portal.git
|
||
cd workbuddy-portal
|
||
|
||
cp .env.example .env # 至少设好 WB_ADMIN_PASSWORD
|
||
docker compose up -d --build
|
||
docker compose logs -f # Ctrl-C 退出日志跟踪,容器继续跑
|
||
```
|
||
|
||
### 路径 C:compose 拉云端镜像(不需要 clone 仓库)
|
||
|
||
在任意空目录写一个 `docker-compose.yml`(内容见
|
||
[部署指南第五节](docs/DEPLOYMENT.md#五路径-cdocker-compose-拉云端镜像部署))+ `.env`,然后:
|
||
|
||
```bash
|
||
docker login git.iwali.top -u <用户名> # 密码填 Access Token(公开仓库可跳过)
|
||
docker compose pull
|
||
docker compose up -d
|
||
```
|
||
|
||
三条路径跑起来后都是:打开 `http://<IP>:8848` → 用管理员登录 →
|
||
**先把密码改掉** → 去「配置管理」粘贴 Cookie。
|
||
|
||
> 数据与日志放在 Docker **命名卷**(`workbuddy-portal_wb_data` / `_wb_logs`)里,
|
||
> `docker compose down` 不会删。要用 CLI 就 `docker compose exec portal python manage.py …`。
|
||
> **不要在宿主机上跑 `manage.py` 去连容器的库**——Windows + Docker Desktop 的 9p 挂载下,
|
||
> 宿主进程碰一次 WAL 库就会让容器打不开数据库(纯读也会触发,且不自愈)。
|
||
> 想直接看到数据/日志,用 `docker-compose.hostdir.yml` 叠加层(**仅建议 Linux 宿主机**)。
|
||
> 详见 [部署与运维指南](docs/DEPLOYMENT.md#115-windows-绑定挂载的坑容器打不开数据库)。
|
||
|
||
> **从旧版本升级不需要手工介入**:`init` 由 `PRAGMA user_version` 驱动,自动迁移且幂等。
|
||
> v1.1.0 → v1.2.0 是「单用户 → 多用户」(历史数据归到首个账号、明文 Cookie 就地加密);
|
||
> v1.2.0 → v1.3.0 是「配置作用域收敛」(调度与采集参数从个人级提升到实例级)。
|
||
> 两次都带审计留痕,可重复执行。见 [CHANGELOG](docs/CHANGELOG.md)。
|
||
|
||
### 第一次使用必做
|
||
|
||
**管理员:**
|
||
|
||
1. **改密码**——局域网可访问,默认密码等于没锁门(「个人中心」或「配置管理 → 修改密码」)。
|
||
2. **确认调度时刻**——「任务管理」里把 `09:00,17:00` 改成你的习惯时刻,保存即生效
|
||
(这是**实例级**的,对全站账号生效)。
|
||
3. **填自己的 Cookie**——「配置管理 → 凭证」,否则采集只会记一条 `no_cookie`。
|
||
|
||
**普通账号(注册进来的默认身份):**
|
||
|
||
1. **改密码**——「个人中心 → 修改登录密码」。
|
||
2. **填自己的 Cookie**——这是你**唯一**需要动手的配置。
|
||
3. 想立刻看数据?点「任务管理 → 立即采集一次」,不用等调度时刻。
|
||
|
||
### 想给同事开账号?
|
||
|
||
登录页底部有「**自助注册**」入口(管理员可在「配置管理 → 实例级设置」关掉)。
|
||
注册同样要过验证码,且同一来源每天最多注册 3 个账号(可改)。
|
||
**注册出来的都是普通账号**:只能维护自己的 Cookie、只看自己的数据,看不到日志与用户管理。
|
||
每个账号登录后填**自己的** Cookie——系统不会、也无法把某人的凭证给别人用。
|
||
|
||
> 只想内部开号、不开放注册?管理员在「用户管理」页直接新建即可;
|
||
> 命令行也行:`python manage.py passwd alice 强密码`(默认普通账号,加 `--role admin` 提权)。
|
||
|
||
---
|
||
|
||
## 命令一览
|
||
|
||
统一入口是 `manage.py`(Docker 里同样可用:`docker compose exec portal python manage.py stats`)。
|
||
|
||
**所有涉及数据/凭证的子命令都作用于某一个账号**,用 `-u/--user <用户名>` 指定;
|
||
不指定则取「管理员优先、其次 id 最小」的那个(所以旧习惯的单账号用法仍然成立)。
|
||
唯独 `collect` 不带 `-u` 时会**逐个启用账号**跑一遍,与进程内调度线程的行为一致。
|
||
|
||
| 命令 | 作用 |
|
||
|---|---|
|
||
| `init` | 初始化 / 迁移数据库(幂等)。`--user` / `--password` 指定首个管理员 |
|
||
| `serve` | 启动 Web。`--host` `--port` `--debug` `--no-scheduler` |
|
||
| `collect [-u 账号]` | 执行一次增量采集后退出;**不带 `-u` 则所有启用账号各跑一次** |
|
||
| `migrate-csv [文件] [-u 账号]` | 从旧版 CSV 存档导入(默认自动探测 `legacy-v1/data/usage_records.csv` 等路径),必须说明「算谁的」 |
|
||
| `import-xlsx <文件> [-u 账号]` | 合入官网「用量明细-导出」的 xlsx |
|
||
| `import-creds [-u 账号]` | 从 VSCode / Cursor / Trae 的 `settings.json` 读取 `codebuddyUsage.*` 写入该账号 |
|
||
| `fill-prompt [-u 账号]` | 回补缺失的 `User Prompt`(官网导出会丢约 22%) |
|
||
| `export-csv [路径] [-u 账号]` | 导出 CSV(默认 `data/exports/usage_records_<账号>.csv`,文件名带归属) |
|
||
| `vacuum` | `wal_checkpoint(TRUNCATE)` + `VACUUM`,回收空闲页、压缩 WAL |
|
||
| `stats [-u 账号]` | 先全库概览(每账号多少条 / 多少积分 / Cookie 状态),再给指定账号的维度明细 |
|
||
| `users` | 列出所有账号:角色、状态、数据量、凭证状态、最近登录 IP |
|
||
| `status` | 逐账号显示调度开关 / 下次执行 / Cookie 状态 / 最近采集 |
|
||
| `passwd <用户> [新密码]` | 重置或创建账号;`--role admin` 提权,`--activate` 顺手启用 |
|
||
|
||
### 自检工具
|
||
|
||
| 脚本 | 层 | 说明 |
|
||
|---|---|---|
|
||
| `tools/smoke.py` | 离线回归 | `test_client` 对真实库全页面只读渲染,**215 项断言**:历史缺陷防回归、多用户隔离 / 凭证保密 / 注册与验证码全链路、**非管理员越权面全关死**、**全局键必须落在实例级**、CSV 列、class↔CSS 对账、静态资源逐个 200。**不需要先起服务** |
|
||
| `tools/check_live.py` | 真实 HTTP | 对运行中的服务走真实链路(登录 → CSRF → 各页面 → 各 API → 导出 → 安全项 → 验证码与响应头),**122 项断言**,基本只读。`--as 账号:密码` 追加普通账号越权验收 |
|
||
| `tools/shots.py` | 界面实检 | Playwright 登录后逐页截图并收集 console / pageerror,**含普通账号只读视角**与越权面探测,产物在 `data/shots/` |
|
||
| `tools/check_docs.py` | 文档自检 | 内部链接与**跨文件锚点**、图片引用、**绝对路径泄漏**、版本一致性(`__init__` / `Dockerfile` / `README` / `CHANGELOG` 四处)、模板与 JS 里的产品名硬编码。文档互相引用后章节一重排,锚点会**静默失效**,这层把它变成可执行断言 |
|
||
| `tools/demo_data.py` | 示例数据 | 生成**完全合成**的示例库(管理员 + 普通账号各一份数据与假 Cookie),文档截图基于它 |
|
||
|
||
```bash
|
||
python tools/smoke.py # 离线,随时可跑
|
||
python tools/check_docs.py # 文档自检,有问题退出码 1
|
||
python manage.py serve --port 8849 --no-scheduler # 另开一个终端
|
||
python tools/check_live.py --base http://127.0.0.1:8849 --as demo:admin123
|
||
python tools/shots.py --base http://127.0.0.1:8849 --full # 逐页截图(含普通账号视角)
|
||
```
|
||
|
||
> `check_live.py` 的 `--db` **默认指向 `data/usage.sqlite`**。对示例实例跑时必须显式传
|
||
> `--db data/demo/usage.sqlite`,否则验证码答案从真实库取,会以「登录失败」的形式误导排查。
|
||
|
||
> `smoke.py` **会对实例级配置做写入测试**(管理员写 `schedule_times` 后还原),
|
||
> 并**临时**建两个普通账号用于验证权限边界与注册链路(无论成败都在 `finally` 里删掉),
|
||
> 不动任何用量数据;跑之前建议先备份,或在示例库上跑。
|
||
> `check_live.py` / `shots.py` 只读,但登录成功会更新 `users.last_login_at` / `login_count`。
|
||
> 两者在验证码策略为 `always` 时会**从本地库里取答案**以完成自动登录
|
||
> ——取的是会话里的 captcha id(答案本身只存在于服务端)。
|
||
|
||
---
|
||
|
||
## 页面一览
|
||
|
||
| 路径 | 作用 | 普通账号 |
|
||
|---|---|---|
|
||
| `/login` | **登录**(未登录时的落点):用户名 / 密码 / **图形验证码**,底部有自助注册入口 | ✅ |
|
||
| `/register` | **自助注册**:用户名、显示名、邮箱、密码 + 验证码 | ✅(可被管理员关闭) |
|
||
| `/` | **概览**:KPI(含今日 vs 昨日整日)、采集健康度、调度状态(只读)、模型 TOP、最近采集 | ✅ 只看自己的 |
|
||
| `/dashboard` | **ECharts 交互大屏**(独立静态页):日历热力图、趋势、维度分布、单笔 TOP,支持区间/维度/指标联动 | ✅ |
|
||
| `/records` | **数据明细**:快捷区间、日期/模型/客户端/关键词筛选、排序、分页、展开 Prompt、导出 CSV | ✅ |
|
||
| `/tasks` | **任务管理**:手动采集与按区间补采、运行历史。**调度开关与时刻对普通账号是只读的** | ⚠️ 只读调度 |
|
||
| `/config` | **配置管理**:自己的 Cookie / UA(**唯一可改**)+ 只读的采集参数 + 维护动作;底部实例级设置区 | ⚠️ 仅凭证可改 |
|
||
| `/profile` | **个人中心**:账号概况、我的凭证状态(密文入库)、改密码;点右上角用户名进入 | ✅ |
|
||
| `/logs` | **日志管理**(**仅管理员**):全实例采集详情(含 `[warn]`/`[error]` 原文)、应用日志尾部、操作审计 | ❌ 403 |
|
||
| `/users` | **用户管理**(**仅管理员**):新建账号、改显示名/权限/状态/密码、删除、账号操作审计 | ❌ 403 |
|
||
|
||

|
||
|
||
> 其余页面截图见 [用户手册](docs/USER-GUIDE.md);普通账号的只读视角见
|
||
> `docs/images/03b-tasks-user.png` 与 `04b-config-user.png`。
|
||
|
||
---
|
||
|
||
## 接口一览
|
||
|
||
全部需要登录(`/api/*` 未登录返回 `401` JSON);写接口另需 CSRF(请求头 `X-CSRF-Token`,
|
||
页面已注入 `window.WB_CSRF`)。
|
||
**所有数据接口都只返回当前登录账号的数据** —— `user_id` 由会话决定,不接受客户端传入。
|
||
完整参数说明见 [docs/API.md](docs/API.md)。
|
||
|
||
| 方法 | 路径 | 作用 | 权限 |
|
||
|---|---|---|---|
|
||
| GET | `/api/manifest` | 存档总量、日期区间、存活日清单、数据源、健康状态(含 `cookieChars`/`cookieBroken`/`role`) | 登录 |
|
||
| GET | `/api/bundle` | 大屏一次取齐:全量 `daily` + 窗口 `dims`/`top`/`records`/`totals` | 登录 |
|
||
| GET | `/api/summary` | KPI + 环比(前一段不在存档内则不给假数字) | 登录 |
|
||
| GET | `/api/daily` · `/api/dims` · `/api/top` | 逐日聚合 / 模型·客户端·时段汇总 / 单笔消耗榜 | 登录 |
|
||
| GET | `/api/records` · `/api/records/<id>` | 明细分页 / 单条详情(`<id>` 也受 `user_id` 约束) | 登录 |
|
||
| GET | `/api/runs` · `/api/runs/<id>` | 采集运行历史 / 单次详情(含逐行日志) | 登录 |
|
||
| GET | `/api/status` | 调度状态、下次执行、互斥锁、最近采集;含 `is_admin` / `can_edit_schedule` / `can_view_logs` | 登录 |
|
||
| GET | `/api/audit` | 操作审计分页 + 可选动作清单 | 管理员看全站,普通账号看自己 |
|
||
| POST | `/api/collect` | 手动触发采集(可指定区间补采);未配 Cookie 回 `409 no_cookie`,密文解不开回 `409 cookie_broken` | 登录(只采自己) |
|
||
| POST | `/api/maintenance/<action>` | `fill-prompt` \| `export-csv` \| `vacuum` \| `recount` | `vacuum` 仅管理员 |
|
||
| GET/POST | `/api/settings` | 读 / 写配置。读只回**掩码** `cookie_hint`,并回 `_globalKeys` / `_userKeys` / `_role` / `_canEditGlobal`;写非本人可写的键会整单拒绝(`400` + `denied` 清单) | 登录;写仅本人凭证或管理员 |
|
||
| POST | `/api/profile` · `/api/password` | 改自己的显示名 / 邮箱 / 密码 | 登录 |
|
||
| POST | `/api/captcha` | 验证码机制自述(策略、位数、TTL、图片地址),便于排障自检 | 登录 |
|
||
| GET/POST | `/api/users` · `/api/users/<id>` · `/api/users/<id>/delete` | 用户管理 | **仅管理员** |
|
||
| GET | `/captcha.png?purpose=login\|register` | **图形验证码图片**(唯一无需登录的接口;每次都是新题,带 `no-store`) | 公开 |
|
||
| GET | `/logs/tail` | 应用日志尾部 | **仅管理员** |
|
||
| GET | `/records/export` | 按筛选流式导出 CSV | 登录(只导自己) |
|
||
|
||
---
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
workbuddy-portal/
|
||
├── manage.py 统一 CLI(唯一入口)
|
||
├── requirements.txt 仅 Flask / waitress / openpyxl(零多余依赖)
|
||
├── Dockerfile 多阶段构建(依赖层 / 运行层)
|
||
├── docker-compose.yml 单服务编排(数据放 Docker 命名卷)
|
||
├── docker-compose.hostdir.yml 可选叠加层:改用宿主机目录(仅建议 Linux)
|
||
├── .env.example 环境变量样例
|
||
├── docker/
|
||
│ ├── entrypoint.sh 幂等初始化 → exec serve(LF 行尾)
|
||
│ └── healthcheck.py 标准库健康检查(免登录页 /login)
|
||
├── docs/ 文档 + images/(手册配图,合成数据)
|
||
├── data/ 【运行时】正本 usage.sqlite / instance.json / exports/ / demo/
|
||
├── logs/ 【运行时】app.log(滚动 2 MB × 3)
|
||
├── backups/ 数据库快照统一落点(整目录被 git 忽略)
|
||
├── tools/
|
||
│ ├── smoke.py 离线回归(215 项断言)
|
||
│ ├── check_live.py 真实 HTTP 验收(122 项断言,支持 --as 普通账号)
|
||
│ ├── shots.py Playwright 逐页截图 + JS 报错收集
|
||
│ ├── demo_data.py 生成合成示例库(管理员 + 普通账号)
|
||
│ └── push-all.sh 一条命令推代码 + 推镜像到 Gitea
|
||
└── workbuddy_portal/
|
||
├── __init__.py create_app:配置 / 日志 / 蓝图 / 错误页 / 启动调度
|
||
├── config.py 路径、项目标识、默认值、**配置作用域与写权限**、密钥管理
|
||
├── db.py SQLite 连接、schema、按作用域读写配置、账号、审计
|
||
├── schema.sql 表结构(多用户布局)
|
||
├── crypto.py 凭证静态加密(手写 ChaCha20 + HMAC-SHA256)
|
||
├── captcha.py 图形验证码(手写 PNG 编码器 + 点阵字模)
|
||
├── security.py 密码哈希、会话、CSRF、失败限速、验证码策略、角色、响应头
|
||
├── client.py 云端接口(urllib)+ 编辑器凭证读取
|
||
├── collect.py 增量采集 / 去重入库 / 互斥锁 / xlsx 导入 / CSV 导出
|
||
├── scheduler.py 进程内调度线程(实例级时刻表 + 槽位去重 + 启动补跑)
|
||
├── query.py SQL 聚合层(uid 必填)
|
||
└── web/
|
||
├── views.py 页面路由(含 /login /register /captcha.png /profile)
|
||
├── api.py JSON API
|
||
├── templates/ base / login / register / profile / overview / tasks /
|
||
│ config / logs / records / users / error
|
||
└── static/
|
||
├── css/app.css 统一设计令牌
|
||
├── js/app.js 带 CSRF 的请求、表单与维护动作绑定
|
||
├── favicon.svg
|
||
└── dashboard/index.html ECharts 大屏(独立页)
|
||
```
|
||
|
||
> `backups/` **刻意不放在 `data/`**:`data/` 在 Docker 部署下是命名卷,
|
||
> `docker compose down -v` 会把备份和正本一起删掉——那正好是最需要备份的时刻。
|
||
>
|
||
> 工作区根下另有一个 `legacy-v1/`(v1.0 单文件版归档,仅作 `migrate-csv` 的来源,可安全删除),
|
||
> 它**在仓库之外**,所以含真实数据的 CSV 不会被提交。
|
||
|
||
---
|
||
|
||
## 文档导航
|
||
|
||
| 文档 | 面向 | 内容 |
|
||
|---|---|---|
|
||
| [docs/USER-GUIDE.md](docs/USER-GUIDE.md) | **使用者** | 用户使用手册:注册、登录、配 Cookie、各页面操作、导出、**权限与数据边界**、**信息安全与隐私安全**、常见问题 |
|
||
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | 运维 | 部署与运维:**三条部署路径(裸机 / Docker 自打包 / compose 拉云端镜像)**、反向代理与 HTTPS、推镜像到 Gitea、备份恢复、升级回滚、巡检、排错、配置项速查 |
|
||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | 开发 | 架构与设计说明:数据模型、调度与锁、聚合边界、安全模型、设计取舍 |
|
||
| [docs/API.md](docs/API.md) | 开发 / 集成 | 接口参考:路径、参数、返回结构、错误码 |
|
||
| [docs/FAQ.md](docs/FAQ.md) | 所有人 | 常见问题:采集为空、Cookie 失效、时区、性能、权限 |
|
||
| [docs/CHANGELOG.md](docs/CHANGELOG.md) | 所有人 | 变更日志 |
|
||
| [CONTRIBUTING.md](CONTRIBUTING.md) | 贡献者 | 贡献指南:开发环境、验证分层、必须遵守的不变量、提交规范 |
|
||
| [SECURITY.md](SECURITY.md) | 运维 / 安全 | 安全策略:漏洞私有报告渠道、已有措施、已知非目标 |
|
||
| [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) | 合规 | 第三方组件清单与许可证(含随仓库再分发的 ECharts) |
|
||
| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | 所有人 | 行为准则 |
|
||
| [LICENSE](LICENSE) | 所有人 | MIT 许可证全文 |
|
||
|
||
---
|
||
|
||
## 安全须知
|
||
|
||
局域网可访问 ⇒ 以下每一条都必要:
|
||
|
||
- **必须改默认密码**;给同事发**普通账号**(注册出来的默认身份),不要共用管理员
|
||
——管理员是能停用别人账号的角色。
|
||
- **权限只有两档,且规则只有一条**:`config.writable_by(key, is_admin)`。
|
||
普通账号能写的只有 `USER_EDITABLE_KEYS = {cookie, user_agent}`(且只限本人这份);
|
||
调度频率、采集参数、注册策略、日志、用户管理全部关死。
|
||
**页面上的置灰/隐藏只是「不给误导性按钮」,真正的闸门在 `@admin_required` 与
|
||
`writable_by()` 这两个服务端判断上**——所以直接敲 URL 或构造请求也过不去。
|
||
- **数据按账号隔离**:`uid` 是所有查询的必填位置参数(漏传直接报错,不会静默返回全量);
|
||
`/api/runs/<id>`、`/api/records/<id>` 这类按 id 取的单条接口也带 `user_id` 约束;
|
||
`/logs`、`/logs/tail`、`/users`、`/api/users*` 仅管理员。
|
||
- **Cookie 静态加密**:ChaCha20 + HMAC-SHA256(encrypt-then-MAC)密文入库,主密钥在
|
||
`data/instance.json` 的 `cookie_key`(**与 `SECRET_KEY` 分开**,轮换代价不同)。
|
||
`get_settings()` 把加密键一律置空,要明文只有 `db.get_secret()` 一条路——
|
||
这样任何「顺手打印全部配置」的代码都带不出凭证。升级时历史明文会被自动加密。
|
||
- **Cookie 不跨账号回落**:`NO_FALLBACK_KEYS`(`cookie` / `user_agent`)不参与实例级回落,
|
||
否则新账号会「继承」管理员的凭证,属于最严重的串号越权。
|
||
- **实例级也不存凭证**:`init_db()` 灌默认值时会跳过 `USER_EDITABLE_KEYS` 并显式删除
|
||
实例级的 `cookie` / `user_agent` 行——实例级存凭证等于给所有账号发同一张身份。
|
||
- **验证码先于口令校验**:登录时先验验证码再比密码,避免攻击者拿「密码对不对」当信号,
|
||
在解验证码之前就把字典跑完。答案存服务端 `captchas` 表,**一次性、5 分钟过期、按用途隔离**,
|
||
下发到浏览器的只有随机 id(Flask 会话是签名不加密的,放答案等于送答案)。
|
||
- **注册受双重限制**:验证码 + 同 IP 每日配额(默认 3 个,可改;`allow_register=0` 可整体关闭)。
|
||
- **停用账号立即失效**:`current_user()` 每个请求回查 `users.status`,不必等 12 小时会话过期。
|
||
- **CSRF 全站校验**,退出登录也是 `POST`(GET 型退出能被 `<img src="/logout">` 静默触发)。
|
||
前端 `formData()` 会跳过 `disabled` 控件(含祖先 `fieldset[disabled]`)——
|
||
disabled 的 input 仍在 `form.elements` 里,一起提交会让服务端因「越权修改只读项」拒掉**整单**。
|
||
- **开放重定向防护**:登录跳转的 `next` 只接受站内相对路径,`//evil.com` 这类协议相对 URL 一律回落到 `/`。
|
||
- **登录限速**:按 **IP 与用户名两个维度**分别计数,任一维度连续失败 5 次即锁 10 分钟;
|
||
失败计数表有上限与 TTL;验证码出图另有 60 秒 40 张的限速(不设限就是一条廉价的 CPU 放大路径)。
|
||
- **安全响应头**:CSP(`frame-ancestors 'none'`)、`X-Frame-Options: DENY`、`nosniff`、
|
||
`Referrer-Policy: same-origin`、COOP;`/api/*` 与 `/captcha*` 带 `Cache-Control: no-store`。
|
||
- **不进版本库的文件**:`data/instance.json`(含 `secret_key` 与 `cookie_key`)、
|
||
`data/usage.sqlite`、`data/shots/`、`data/demo/`、`backups/`、`logs/`、`.env`(含明文密码)。
|
||
|
||
---
|
||
|
||
## 开源与许可
|
||
|
||
本项目以 **MIT 许可证**发布,全文见 [LICENSE](LICENSE)。你可以自由使用、修改、商用与再分发,
|
||
只需保留版权声明与许可声明。
|
||
|
||
### 第三方组件(务必看一眼)
|
||
|
||
用量大屏**随仓库再分发了 Apache ECharts 5.6.0**(`workbuddy_portal/web/static/dashboard/vendor/echarts.min.js`,
|
||
Apache-2.0 许可)——之所以内置而不走 CDN,是为了让大屏在局域网内离线可用。
|
||
按 Apache-2.0 第 4 条,再分发时需保留其许可证与版权声明(该文件头部已自带)。
|
||
其余运行期依赖(Flask / waitress / openpyxl)不在本仓库内,由使用方安装时获取。
|
||
|
||
完整的依赖清单、许可证对照表与**合规自查清单**见 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。
|
||
|
||
### 参与贡献
|
||
|
||
- 想改代码?先读 [CONTRIBUTING.md](CONTRIBUTING.md) —— 里面有**必须遵守的几条不变量**
|
||
(SQLite 单写者、列表接口不回 `prompt` 全文、两种字段命名契约不要互相「统一」、
|
||
写权限只能走 `config.writable_by`……),以及从 `compileall` 到容器验证的五层自检该怎么跑。
|
||
- 有想法但手上没有真实数据?`python tools/demo_data.py` 会生成一份**完全合成**的示例库,
|
||
写到 `data/demo/`(已在 `.gitignore` 内),可直接拿来调试界面与截图。
|
||
- 发现安全漏洞?**请不要开公开 Issue**,按 [SECURITY.md](SECURITY.md) 走私有渠道。
|
||
- 参与本项目即表示你同意遵守 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。
|
||
|
||
### 文档里的数据都是合成的
|
||
|
||
`docs/images/` 的全部界面截图与 `docs/` 中的 JSON 示例**均为合成数据**:
|
||
模型名统一为 `demo-*`,客户端为 `vscode` / `webconsole` / `sdk`,Prompt 为通用示例文本,
|
||
Cookie 是 `deadbeef…` / `cafef00d…` 这类一眼可辨的假串,
|
||
审计 IP 取自 RFC 5737 的文档专用网段(`192.0.2.0/24`)。
|
||
生成方式是 `tools/demo_data.py`(会造 `admin` 与 `demo` 两个账号,各有自己的数据),
|
||
所以任何人不需要真实账号就能复现整套文档。截图由 `tools/shots.py` 逐页重出(**13 张**,
|
||
含注册页、个人中心,以及**普通账号视角的只读「任务管理 / 配置管理」**),
|
||
脚本会读示例库里的验证码答案自动过掉登录。
|
||
|
||
> ⚠️ **重出截图时务必用相对路径起示例服务**:
|
||
> ```bash
|
||
> cd workbuddy-portal
|
||
> WB_DATA_DIR=data/demo WB_LOG_DIR=data/demo/logs python manage.py serve --port 8849 --no-scheduler
|
||
> ```
|
||
> 用绝对路径(包括 Git Bash 里的 `$PWD`,它展开成 `/c/...`)会让启动日志印出
|
||
> `C:\Users\<用户名>\…`,而那一行正好会出现在「日志管理」页的截图上。
|
||
> 另外 Git Bash 下 `$PWD` 是 MSYS 风格路径,Python 会把它解析成 `C:\c\Users\…`
|
||
> —— 于是**服务连的是另一个空库**,界面上看不出异常,但截图数据全不对
|
||
> (症状:验证码取不到答案,`shots.py` 报「需要验证码但取不到答案」)。
|
||
|
||
### 致谢
|
||
|
||
- 交互大屏依赖 [Apache ECharts](https://echarts.apache.org/)
|
||
- Web 框架 [Flask](https://flask.palletsprojects.com/),生产服务器 [waitress](https://github.com/Pylons/waitress)
|
||
- 行为准则框架来自 [Contributor Covenant](https://www.contributor-covenant.org/)
|