## 现象
容器跑着跑着页面全部 500,应用日志:
sqlite3.OperationalError: unable to open database file
(db.py:33, conn.execute("PRAGMA journal_mode=WAL"))
## 根因(已最小复现)
数据原本用绑定挂载(./data:/app/data)。Windows + Docker Desktop 的绑定挂载走 9p
(mount 里是 type 9p, aname=drvfs;path=C:\)。9p 本身支持 WAL(新建库能开 WAL),
但**宿主的 Windows 进程打开过这个 WAL 库之后**,容器侧缓存的 -shm 映射就失效,
下一次连接无法重建共享内存文件 → 打不开数据库,且**不会自愈**。
复现:
docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
docker compose exec portal python -c "..." # OK, 1665 条
python manage.py stats # 宿主侧纯读一次
docker compose exec portal python -c "..." # ERR unable to open database file
# 只有 docker compose restart portal 才恢复
## 处理
- docker-compose.yml 改用命名卷 wb_data / wb_logs(容器独占 /app/data)
- 新增 docker-compose.hostdir.yml 叠加层:需要宿主目录时用
docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
(注明**只建议 Linux**;Linux 的 bind mount 与容器同一文件系统,无此问题)
- 数据迁移:docker run --rm -v workbuddy-portal_wb_data:/to -v "$PWD/data":/from:ro \
alpine:3.20 sh -c 'cp -a /from/. /to/'
- 文档同步:DEPLOYMENT 2.4/5.4/第六节全部改为命名卷 + 备份恢复用 docker run;
新增第九节「Windows 绑定挂载的坑」(含复现步骤);FAQ、USER-GUIDE、README、CHANGELOG 同步
## 验证
- 宿主跑 manage.py stats 与 smoke.py 之后,容器侧仍能正常读写(此前会立刻失效)
- 容器实例 check_live 56/56;离线 smoke 99/99;hostdir 叠加层 config 校验通过
262 行
15 KiB
Markdown
262 行
15 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 / 裸机 `waitress`;镜像可推 Gitea 容器注册表 |
|
||
| 鉴权 | 全站登录 + CSRF + 角色(管理员 / 普通用户),凭证存库、页面只回掩码 |
|
||
| 版本 | v1.1.0 |
|
||
|
||
**目录**:[核心特性](#核心特性) · [架构](#架构一图) · [快速开始](#快速开始) · [命令一览](#命令一览) ·
|
||
[页面一览](#页面一览) · [接口一览](#接口一览) · [文档导航](#文档导航) · [安全须知](#安全须知)
|
||
|
||
---
|
||
|
||
## 核心特性
|
||
|
||
| 能力 | 说明 |
|
||
|---|---|
|
||
| **增量采集** | 按 `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 │
|
||
│ │ ▲ data/usage.sqlite(WAL) │
|
||
└───────────┼──────┘ ▲ │
|
||
client.py(urllib)│ │ │
|
||
│ query.py(聚合全部下推 SQL) │
|
||
│ ▲ ▲ │
|
||
│ web/views.py ──────┘ └──── web/api.py│
|
||
│ (Jinja 后台) (JSON) │
|
||
└───────────────┬───────────────────────────┬──────────────┘
|
||
▼ ▼
|
||
/ /records /tasks /dashboard(ECharts 大屏)
|
||
/config /logs /users
|
||
```
|
||
|
||
四层职责:
|
||
|
||
| 层 | 位置 | 说明 |
|
||
|---|---|---|
|
||
| 采集 | `workbuddy_portal/collect.py` + `scheduler.py` | 纯 `urllib` 调云端;断点、去重、锁、导入导出 |
|
||
| 存储 | `workbuddy_portal/db.py` + `schema.sql` | SQLite WAL,单写者,运行期配置也在库里(`settings` 表) |
|
||
| 聚合 | `workbuddy_portal/query.py` | `daily / dims / top / records / summary / bundle`,全部下推 SQL |
|
||
| 呈现 | `workbuddy_portal/web/` | Jinja 后台(`views.py`)+ JSON API(`api.py`)+ 静态大屏 |
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 方式一:Docker Compose(推荐)
|
||
|
||
```bash
|
||
git clone https://git.iwali.top/wangchuanli/workbuddy-portal.git
|
||
cd workbuddy-portal
|
||
|
||
cp .env.example .env # 至少设好 WB_ADMIN_PASSWORD
|
||
# 编辑 .env: WB_ADMIN_PASSWORD=一个足够强的密码
|
||
|
||
docker compose up -d --build
|
||
docker compose logs -f # Ctrl-C 退出日志跟踪,容器继续跑
|
||
```
|
||
|
||
打开 `http://<本机IP>:8848` → 用 `.env` 里设的账号登录 → 去「配置管理」粘贴 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#windows-绑定挂载的坑容器打不开数据库)。
|
||
|
||
### 方式二:裸机 Python
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
|
||
python manage.py init # 建表 + 默认配置 + 管理员 admin/admin123
|
||
python manage.py import-creds # 可选:把编辑器设置里的 cookie/UA 接管进数据库
|
||
python manage.py migrate-csv # 可选:把旧版 CSV 存档全量导入
|
||
python manage.py serve # 启动,默认 0.0.0.0:8848
|
||
```
|
||
|
||
### 第一次使用必做三件事
|
||
|
||
1. **改密码**——局域网可访问,默认密码等于没锁门(「配置管理 → 修改密码」)。
|
||
2. **填 Cookie**——「配置管理 → 凭证」,否则采集只会记一条 `cookie_expired`。
|
||
获取方式见 [用户手册](docs/USER-GUIDE.md#三获取并填写-cookie)。
|
||
3. **确认调度时刻**——「任务管理」里把 `09:00,17:00` 改成你的习惯时刻,保存即生效。
|
||
|
||
---
|
||
|
||
## 命令一览
|
||
|
||
统一入口是 `manage.py`(Docker 里同样可用:`docker compose exec portal python manage.py stats`)。
|
||
|
||
| 命令 | 作用 |
|
||
|---|---|
|
||
| `init` | 初始化数据库(幂等)。`--user` / `--password` 指定首个管理员 |
|
||
| `serve` | 启动 Web。`--host` `--port` `--debug` `--no-scheduler` |
|
||
| `collect` | 执行一次增量采集后退出(不想开 Web 时可挂系统计划任务) |
|
||
| `migrate-csv [文件]` | 从旧版 CSV 存档导入(默认自动探测旧项目路径) |
|
||
| `import-xlsx <文件>` | 合入官网「用量明细-导出」的 xlsx |
|
||
| `import-creds` | 从 VSCode / Cursor / Trae 的 `settings.json` 读取 `codebuddyUsage.*` 写入数据库 |
|
||
| `fill-prompt` | 回补缺失的 `User Prompt`(官网导出会丢约 22%) |
|
||
| `export-csv [路径]` | 导出与官网 xlsx 同构的 CSV(默认 `data/exports/`) |
|
||
| `vacuum` | `wal_checkpoint(TRUNCATE)` + `VACUUM`,回收空闲页、压缩 WAL |
|
||
| `stats` | 存档概况 + 模型维度表 + 最近采集(不联网) |
|
||
| `status` | 调度开关 / 下次执行 / Cookie 状态 / 最近采集 |
|
||
| `passwd <用户> [新密码]` | 重置或创建登录账号 |
|
||
|
||
### 自检工具
|
||
|
||
| 脚本 | 层 | 说明 |
|
||
|---|---|---|
|
||
| `tools/smoke.py` | 离线回归 | `test_client` 对真实库全页面只读渲染,**99 项断言**(历史缺陷防回归 ①~⑭、CSV 列、class↔CSS 对账、静态资源逐个 200),**不需要先起服务** |
|
||
| `tools/check_live.py` | 真实 HTTP | 对运行中的服务走真实链路(登录 → CSRF → 各页面 → 各 API → 导出 → 安全项),**56 项断言**,基本只读 |
|
||
| `tools/shots.py` | 界面实检 | Playwright 登录后逐页截图并收集 console / pageerror,产物在 `data/shots/` |
|
||
|
||
```bash
|
||
python tools/smoke.py # 离线,随时可跑
|
||
python manage.py serve --port 8849 --no-scheduler # 另开一个终端
|
||
python tools/check_live.py --base http://127.0.0.1:8849 # 真实 HTTP
|
||
python tools/shots.py --base http://127.0.0.1:8849 --full # 逐页截图
|
||
```
|
||
|
||
> `smoke.py` 会写少量 `audit_log` 审计行(被拒的配置写入也留痕),不动业务数据;
|
||
> `check_live.py` 只读,但登录成功会更新 `users.last_login_at` / `login_count`。
|
||
|
||
---
|
||
|
||
## 页面一览
|
||
|
||
| 路径 | 作用 |
|
||
|---|---|
|
||
| `/` | **概览**:KPI(含今日 vs 昨日整日)、采集健康度、调度状态、模型 TOP、最近采集 |
|
||
| `/dashboard` | **ECharts 交互大屏**(独立静态页):日历热力图、趋势、维度分布、单笔 TOP,支持区间/维度/指标联动 |
|
||
| `/records` | **数据明细**:快捷区间、日期/模型/客户端/关键词筛选、排序、分页、展开 Prompt、导出 CSV |
|
||
| `/tasks` | **任务管理**:调度开关与时刻、启动补跑、按区间补采、运行历史 |
|
||
| `/config` | **配置管理**:Cookie / UA、采集参数、TLS 校验、修改密码、维护动作(回补 Prompt / 导出 / 整理库) |
|
||
| `/logs` | **日志管理**:逐次采集详情(含 `[warn]`/`[error]` 原文)、状态筛选、应用日志、操作审计 |
|
||
| `/users` | **用户管理**(仅管理员):新建账号、改显示名/权限/密码、删除、用户操作审计 |
|
||
|
||

|
||
|
||
> 其余页面截图见 [用户手册](docs/USER-GUIDE.md)。
|
||
|
||
---
|
||
|
||
## 接口一览
|
||
|
||
全部需要登录(`/api/*` 未登录返回 `401` JSON);写接口另需 CSRF(请求头 `X-CSRF-Token`,
|
||
页面已注入 `window.WB_CSRF`)。完整参数说明见 [docs/API.md](docs/API.md)。
|
||
|
||
| 方法 | 路径 | 作用 |
|
||
|---|---|---|
|
||
| GET | `/api/manifest` | 存档总量、日期区间、存活日清单、数据源、健康状态 |
|
||
| GET | `/api/bundle` | 大屏一次取齐:全量 `daily` + 窗口 `dims`/`top`/`records`/`totals` |
|
||
| GET | `/api/summary` | KPI + 环比(前一段不在存档内则不给假数字) |
|
||
| GET | `/api/daily` | 逐日聚合(含每日分模型、24 时段) |
|
||
| GET | `/api/dims` | 模型 / 客户端 / 时段汇总 |
|
||
| GET | `/api/top` | 单笔消耗榜(唯一带 Prompt 摘要的接口) |
|
||
| GET | `/api/records` · `/api/records/<id>` | 明细分页 / 单条详情 |
|
||
| GET | `/api/runs` · `/api/runs/<id>` | 采集运行历史 / 单次详情(含逐行日志) |
|
||
| GET | `/api/status` | 调度状态、下次执行、互斥锁、最近采集 |
|
||
| GET | `/api/audit` | 操作审计分页 + 可选动作清单 |
|
||
| POST | `/api/collect` | 手动触发采集(可指定区间补采) |
|
||
| POST | `/api/maintenance/<action>` | `fill-prompt` \| `export-csv` \| `vacuum` \| `recount` |
|
||
| GET/POST | `/api/settings` | 读 / 写配置(非法值 `400` 并列出全部错误) |
|
||
| POST | `/api/password` | 修改自己的登录密码 |
|
||
| GET/POST | `/api/users` · `/api/users/<id>` | 用户管理(仅管理员) |
|
||
| GET | `/logs/tail` · `/records/export` | 应用日志尾部 / 按筛选流式导出 CSV |
|
||
|
||
---
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
workbuddy-portal/
|
||
├── manage.py 统一 CLI(唯一入口)
|
||
├── requirements.txt
|
||
├── Dockerfile 多阶段构建(依赖层 / 运行层)
|
||
├── docker-compose.yml 单服务编排(数据放 Docker 命名卷)
|
||
├── docker-compose.hostdir.yml 可选叠加层:改用宿主机目录(仅建议 Linux)
|
||
├── .env.example 环境变量样例
|
||
├── docker/
|
||
│ ├── entrypoint.sh 幂等初始化 → exec serve(LF 行尾)
|
||
│ └── healthcheck.py 标准库健康检查(免登录页 /login)
|
||
├── docs/ 文档(见下)
|
||
├── tools/
|
||
│ ├── smoke.py 离线回归(99 项断言)
|
||
│ ├── check_live.py 真实 HTTP 验收(56 项断言)
|
||
│ └── shots.py Playwright 逐页截图 + JS 报错收集
|
||
└── workbuddy_portal/
|
||
├── __init__.py create_app:配置 / 日志 / 蓝图 / 错误页 / 启动调度
|
||
├── config.py 路径、项目标识、默认值、写时校验
|
||
├── db.py SQLite 连接、schema、settings 读写、审计
|
||
├── schema.sql 表结构
|
||
├── security.py 密码哈希、session、CSRF、失败限速、safe_next、角色
|
||
├── client.py 云端接口(urllib)+ 编辑器凭证读取
|
||
├── collect.py 增量采集 / 去重入库 / 互斥锁 / xlsx 导入 / CSV 导出
|
||
├── scheduler.py 进程内调度线程(槽位去重 + 启动补跑)
|
||
├── query.py SQL 聚合层
|
||
└── web/
|
||
├── views.py 页面路由
|
||
├── api.py JSON API
|
||
├── templates/ base / login / overview / tasks / config / logs / records / users / error
|
||
└── static/
|
||
├── css/app.css 统一设计令牌
|
||
├── js/app.js 带 CSRF 的请求、表单与维护动作绑定
|
||
├── favicon.svg
|
||
└── dashboard/index.html ECharts 大屏(独立页)
|
||
```
|
||
|
||
---
|
||
|
||
## 文档导航
|
||
|
||
| 文档 | 面向 | 内容 |
|
||
|---|---|---|
|
||
| [docs/USER-GUIDE.md](docs/USER-GUIDE.md) | **使用者** | 用户使用手册:登录、各页面操作、Cookie 获取、导出、常见操作 |
|
||
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | 运维 | 部署与运维:Docker、裸机、反向代理、备份恢复、升级回滚、推镜像到 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) | 所有人 | 变更日志 |
|
||
|
||
---
|
||
|
||
## 安全须知
|
||
|
||
局域网可访问 ⇒ 以下每一条都必要:
|
||
|
||
- **必须改默认密码**;给只读同事发普通账号(`is_admin=0`),不要共用管理员。
|
||
- **Cookie 就是账号凭证**:只以掩码回显,存库不外传;默认开启 TLS 证书校验(`ssl_verify=1`),
|
||
仅在自签 / 企业代理场景临时关闭。
|
||
- **CSRF 全站校验**,退出登录也是 `POST`(GET 型退出能被 `<img src="/logout">` 静默触发)。
|
||
- **开放重定向防护**:登录跳转的 `next` 只接受站内相对路径,`//evil.com` 这类协议相对 URL 一律回落到 `/`。
|
||
- **登录限速**:同 IP 连续失败 5 次锁定 10 分钟;失败计数表有上限与 TTL。
|
||
- **不进版本库的文件**:`data/instance.json`(含 `secret_key`)、`data/usage.sqlite`、`logs/`、`.env`(含明文密码)。
|