feat(权限): 收敛普通账号写权限至本人凭证

将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 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。
这个提交包含在:
2026-09-18 08:46:00 +08:00
父节点 df7db3582e
当前提交 1bf961f6b3
共修改 42 个文件,包含 2615 行新增和 854 行删除
+37 -12
查看文件
@@ -214,10 +214,19 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
"scheduler": { "enabled": true, "times": ["09:00","17:00"], "next": "2026-09-14 17:00:00" },
"running_runs": 0,
"last_run": { "id": 8, "trigger": "startup", "status": "ok", "started_at": "...", "message": "..." },
"cookie_set": true
"cookie_set": true,
"is_admin": false,
"can_edit_schedule": false,
"can_view_logs": false
}
```
> 最后三个字段是**前端显隐的依据**(v1.3.0 起)。
> 调度时刻是**实例级**的 —— 普通账号拿到 `can_edit_schedule: false`,
> 页面据此把「采集调度」渲染成只读表格,而不是给一个点了会被拒的表单。
> 大屏是拿不到 Jinja 上下文的静态页,只能靠 `/api/manifest` 的 `role`
> 或这里的字段决定要不要显示「日志管理」入口。
### GET `/api/audit`
操作审计分页。
@@ -255,9 +264,14 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
"cookie_hint": "92 字符,…c0ffee",
"cookie_broken": false,
"user_agent": "Mozilla/5.0 (...)",
"_globalKeys": ["allow_register", "api_base", "api_path",
"captcha_length", "captcha_policy", "register_max_per_ip"],
"_canEditGlobal": true
"_globalKeys": ["allow_register", "api_base", "api_path", "captcha_length",
"captcha_policy", "catch_up", "catch_up_grace_hours",
"drift_tolerance_minutes", "max_prompt", "page_size",
"register_max_per_ip", "rewind_minutes", "schedule_enabled",
"schedule_times", "ssl_verify", "timeout", "verify_days"],
"_userKeys": ["cookie", "user_agent"],
"_canEditGlobal": true,
"_role": "user"
}
```
@@ -267,8 +281,14 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
| `cookie` | **恒为空串** —— `db.get_settings()` 统一置空,明文只能经 `db.get_secret()` 取 |
| `cookie_hint` | 「N 字符,…尾 4 位」;未配置时为空串 |
| `cookie_broken` | `true` 表示密文解不开(`cookie_key` 换过),需重新粘贴 Cookie |
| `_globalKeys` | 实例级键清单(所有账号共用一份,只有管理员能改) |
| `_globalKeys` | **实例级**键清单(所有账号共用一份,只有管理员能改)。v1.3.0 起含调度与采集参数 |
| `_userKeys` | **个人级**键清单,即 `config.USER_EDITABLE_KEYS`;普通账号唯一能写的两个键 |
| `_canEditGlobal` | 当前账号能否改 `_globalKeys` 里的键 |
| `_role` | `"admin"` / `"user"` —— 前端据此决定显隐(大屏走 `/api/manifest` 的 `role`) |
> **写权限就一条规则**:`config.writable_by(key, is_admin)`。
> 页面上「哪些输入框可以改」与接口「哪些键能写」用的是同一个函数,
> 所以不会出现「界面置灰但接口还能写」的不一致。
### GET `/api/users`(管理员)
@@ -382,19 +402,24 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
|---|---|
| 全部合法 | `200 {"ok": true, "changed": ["page_size"], "ignored": []}` |
| 有非法值 | `400 {"ok": false, "error": "invalid", "errors": ["page_size 需在 20 ~ 1000 条/页 之间"]}` |
| 非管理员改实例级键 | `400 {"ok": false, "error": "invalid", "errors": ["以下为实例级配置,仅管理员可修改:api_base"]}` |
| 不可写的键(普通账号写实例级) | `400 {"ok": false, "error": "invalid", "errors": ["以下配置仅管理员可修改,本账号无法保存:api_base。普通账号可以维护的是本人凭证(Cookie / User-Agent)。"]}` |
要点:
- **写权限判断只有一处**:`config.writable_by(key, is_admin)`。
普通账号能写的**只有** `cookie` 与 `user_agent`(且只限本人这份);
其余(云端接口、注册策略、调度、采集参数)全部仅管理员。
- **越权写是「整单拒绝」而不是「部分生效」**:请求里只要含一个不可写的键,
整个请求 `400`,并在 `errors` 里**点名**是哪些键。这样调用方不会误以为
「既然 `changed` 里没有它就说明写过了」。
- `cookie` **留空 = 不修改**(不会把已有 Cookie 清掉);写 `__clear__` 或 `-` 才是清空;
- `cookie` 落库前会**自动加密**(`db.set_secret`),写进去的永远不是明文;
- **实例级键**(`_globalKeys`:`api_base` / `api_path` / `allow_register` /
`register_max_per_ip` / `captcha_policy` / `captcha_length`)非管理员**写不了**
—— 否则任意注册用户都能把大家的数据采集指向别的服务器;
- 未知键被忽略并在 `ignored` 里列出,**不会被写成任意键**;
- 内部键(`slot:*`)被忽略;
- 改了 `schedule_times` / `schedule_enabled` 会清掉**自己**的槽位标记,新时刻立即生效;
- 每次拒绝都会写一条 `settings_rejected` 审计。
- 内部键(`slot:*`)被忽略 —— 它们是调度簿记,不属于用户可配置项;
- 改 `schedule_times` 会清掉**已不存在时刻**对应的 `slot:*` 标记(所有账号一起清),
新时刻立即生效。刻意**不做全清**:全清会让所有账号在宽限期内一起重采。
- 每次拒绝都会写一条 `settings_rejected` 审计(管理员的「日志管理 → 操作审计」里能看到,
这也是排查「谁的账号在试越权」的入口)。
### POST `/api/password`
+42 -9
查看文件
@@ -88,8 +88,13 @@ audit_log(id, user_id, at, actor, action, detail, ip)
```
**`user_id = 0` 的含义**:在 `settings` / `collect_runs` / `audit_log` 里表示
**实例级**(所有账号共用,例如 `api_base`、系统迁移审计);在 `usage_records` 里
是「尚未归属」的兜底值,正常不会出现。
**实例级**(所有账号共用,例如 `api_base`、`schedule_times`、`page_size`、系统迁移审计);
在 `usage_records` 里是「尚未归属」的兜底值,正常不会出现。
> ⚠️ 实例级**不是**「孤儿行」。清理孤儿数据的 SQL 必须显式排除 `user_id = 0`,
> 例如 `DELETE FROM settings WHERE user_id <> 0 AND user_id NOT IN (SELECT id FROM users)`。
> 写成 `user_id NOT IN (SELECT id FROM users)` 会一次性删光整片实例级配置 ——
> 表现是「所有账号的调度、采集参数、注册策略突然全部回到默认值」。
索引全部以 `user_id` 打头,覆盖六类热点查询:
`(user_id, day)`、`(user_id, day, hour)`、`(user_id, model, day)`、`(user_id, client, day)`、
@@ -298,7 +303,12 @@ records: id c(credits) m(model) cl(client) t(ts) px(prompt)
### 授权与数据隔离
`@login_required`(`/api/*` 未登录返回 401 JSON,页面跳登录)+
`@admin_required`(403)两层。`/users`、`/api/users*`、`/logs/tail`、`vacuum` 要管理员。
`@admin_required`(403)两层。**仅管理员**:`/users`、`/api/users*`、`/logs`、`/logs/tail`、`vacuum`。
两者之间还有一层「同一个页面、两种形态」:`/tasks` 与 `/config` 对普通账号**仍然可达**,
但渲染成**只读形态**(不给表单,换成只读表格 + 一句说明为什么只读),
写接口也会拒绝。页面形态与接口判断**共用 `config.writable_by()`**,
所以不存在「界面上没按钮、构造请求却能改」的空隙 —— 这是本次权限收敛刻意保证的性质。
**多租户隔离靠「显式传参」而不是「隐式全局」**,这是本节最重要的一条设计:
@@ -319,7 +329,7 @@ scheduler.slots(conn, uid=0)
| 项 | 做法 |
|---|---|
| 单条读取也过滤 | `/api/records/<id>`、`/api/runs/<id>` 的 `WHERE` 都带 `user_id` |
| 配置作用域 | 三级回落 `个人 → 实例 → DEFAULTS`;`GLOBAL_KEYS` 只有管理员能改 |
| 配置作用域 | 三级回落 `个人 → 实例 → DEFAULTS`;`GLOBAL_KEYS`(云端接口 + 注册策略 + 调度 + 采集参数)一律存**实例级 `user_id=0`** 且仅管理员可写;普通账号可写的只有 `USER_EDITABLE_KEYS = {cookie, user_agent}` |
| **凭证不回落** | `NO_FALLBACK_KEYS = {cookie, user_agent}` 跳过实例级回落 —— 否则新账号会「继承」管理员的 Cookie,这是最严重的串号越权 |
| 导出隔离 | CSV 文件名带账号名(多用户下同目录同名会互相覆盖) |
| 审计归属 | `audit_log` / `collect_runs` 都带 `user_id`;`/api/audit` 普通账号只看自己 |
@@ -421,9 +431,30 @@ page_size = db.get_int(conn, "page_size", 200) # 任何异常都回落默认
└── NO_FALLBACK_KEYS(cookie / user_agent)到此为止,不回落到实例级
```
`GLOBAL_KEYS`(`api_base` / `api_path` / `allow_register` / `register_max_per_ip` /
`captcha_policy` / `captcha_length`)在读写两侧都被强制折算到 `user_id = 0`,
所以它们天然只有一份,非管理员改不了。
`GLOBAL_KEYS`(共 17 个键,分四组)在读写两侧都被强制折算到 `user_id = 0`,
所以它们天然只有一份,非管理员改不了:
| 分组 | 键 | 为什么归实例级 |
|---|---|---|
| 云端接口(2) | `api_base`、`api_path` | 一台部署连的就是那一个云端,逐账号配没有意义 |
| 注册策略(4) | `allow_register`、`register_max_per_ip`、`captcha_policy`、`captcha_length` | 敞开注册与否是运营决策,不能由任意账号开关 |
| 调度(4) | `schedule_enabled`、`schedule_times`、`catch_up`、`catch_up_grace_hours` | **普通账号不能设置定时任务频率**(本轮需求) |
| 采集参数(7) | `page_size`、`rewind_minutes`、`drift_tolerance_minutes`、`max_prompt`、`verify_days`、`timeout`、`ssl_verify` | 关掉 `ssl_verify` 就能把所有人的 Cookie 发到中间人手里 |
这三样东西必须**对齐**,缺一不可:
1. **键存在哪一级** —— `GLOBAL_KEYS` 一律 `user_id = 0`;
2. **谁能写** —— `config.writable_by(key, is_admin)`:只有 `USER_EDITABLE_KEYS` 对普通账号放行;
3. **读时落哪** —— 三级回落 `个人 → 实例 → DEFAULTS`。
> 🔴 最常见的静默 bug 是「只做到第 2 条」:管理员能改,但值写进了**管理员自己的 `user_id=n`**。
> 别的账号读同一个键时会**落回 `DEFAULTS`**,于是「管理员改了,但只有他自己那边生效」,
> 界面上完全看不出来。所以 `set_setting()` 内部会把全局键**强制重定向**到 `user_id = 0`,
> 从结构上消除这种可能,而不是靠调用方记得传 `0`。
有个 **`slot:*` 例外**:`slot:09:00` 这类簿记键表示「**本账号**今天这个槽位跑过没有」,
它是**个人级**(每个账号各记一份),而它所指向的时刻 `schedule_times` 是**实例级**。
这两者千万别一起改 —— 把 `slot:*` 也搬到实例级,会让两个账号互相以为对方已经跑过当天采集。
有个**容易误判**的细节:`NO_FALLBACK_KEYS` 只拦住「实例级那一行」,不拦 `DEFAULTS`。
所以一个全新账号读 `user_agent` 拿到的是 `DEFAULTS` 里的**通用 Chrome UA**(非空),
@@ -513,13 +544,15 @@ GMT+8 下 `new Date("2026-08-15T00:00:00")` 的 UTC 时刻是前一天 16:00,
| 层 | 手段 | 抓什么 |
|---|---|---|
| 1 | 独立聚合对账(直读 CSV 不走 `query.py`) | 口径错、少算。热力图要**逐格**比,历史上出过「同格覆盖少算 84%」 |
| 2 | `tools/smoke.py`(**165 项断言**,离线) | 模板残留、历史缺陷防回归 ①~⑭、**多用户隔离 / 凭证保密 / 注册与验证码全链路**、静态资源 404、class↔CSS 对账 |
| 3 | `tools/check_live.py`(**83 项断言**,真实 HTTP) | `test_client` 覆盖不到的:waitress、端口、cookie 往返、开放重定向、CSRF、验证码、安全响应头 |
| 2 | `tools/smoke.py`(**215 项断言**,离线) | 模板残留、历史缺陷防回归 ①~⑭、**多用户隔离 / 凭证保密 / 注册与验证码全链路**、**非管理员越权面全关死**、**全局键必须落在实例级**、静态资源 404、class↔CSS 对账 |
| 3 | `tools/check_live.py`(**122 项断言**,真实 HTTP) | `test_client` 覆盖不到的:waitress、端口、cookie 往返、开放重定向、CSRF、验证码、安全响应头;`--as 账号:密码` 追加普通账号越权验收 |
| 4 | Node DOM stub + `vm.runInContext` 跑大屏真实脚本 | 「页面聚合 == 独立算出的聚合」、切区间只发一次请求 |
| 5 | `tools/shots.py`(Playwright 截图 + console/pageerror) | **界面层**。本轮最有价值的 bug(大屏全白)只有它抓到 |
| 附 | `tools/check_docs.py`(**文档层**,不属于上面五层) | 内部链接 / 跨文件锚点 / 图片引用 / **绝对路径泄漏** / 版本一致性 / 产品名硬编码。**章节一重排,锚点就静默失效**,Markdown 自己不报错,只有它抓得到。文档清单自动发现,不写死文件名 |
```bash
python tools/smoke.py # 1~2 层,随时跑
python tools/check_docs.py # 文档层,改过 md 就跑
python manage.py serve --port 8849 --no-scheduler # 另开终端
python tools/check_live.py --base http://127.0.0.1:8849 # 3 层
python tools/shots.py --base http://127.0.0.1:8849 --full # 5 层
+89 -1
查看文件
@@ -8,6 +8,94 @@
---
## [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_hours`
+ `page_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.py` 162 → 215 项断言(新增「非管理员越权面必须全部关死」
与「全局键必须落在实例级」两节);`tools/check_live.py` 83 → 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 加密 · 开放注册与验证码**
@@ -159,7 +247,7 @@
| 容器跑着跑着页面全 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 第九节](DEPLOYMENT.md#windows-绑定挂载的坑容器打不开数据库)。
已写进 [DEPLOYMENT.md 第十一节](DEPLOYMENT.md#115-windows-绑定挂载的坑容器打不开数据库)。
### 安全
+697 -360
查看文件
文件差异内容过多而无法显示 加载差异
+50 -11
查看文件
@@ -74,7 +74,7 @@ docker compose exec portal python manage.py stats
```
完整复现步骤与原理见
[部署与运维指南](DEPLOYMENT.md#windows-绑定挂载的坑容器打不开数据库)。
[部署与运维指南](DEPLOYMENT.md#115-windows-绑定挂载的坑容器打不开数据库)。
### Q:`database is locked` / `disk I/O error`
@@ -104,7 +104,7 @@ python -c "p='docker/entrypoint.sh';d=open(p,'rb').read();open(p,'wb').write(d.r
### Q:采集报 `cookie_expired` / `unauthorized`
Cookie 过期。重新获取(见 [用户手册 3.2](USER-GUIDE.md#32-拿-cookie-的两种办法)),
Cookie 过期。重新获取(见 [用户手册 4.2](USER-GUIDE.md#42-拿-cookie-的两种办法)),
填进「配置管理 → **我的云端凭证**」,保存后按区间补采。
> Cookie 通常是浏览器会话级,**关掉浏览器可能就失效**。从已登录浏览器复制时勾选「保持登录」。
@@ -350,7 +350,7 @@ docker run --rm -v workbuddy-portal_wb_data:/data:ro -v "$PWD/backup":/backup \
alpine:3.20 tar czf /backup/wb-data-$(date +%F).tar.gz -C /data .
```
恢复见 [部署指南 6.3](DEPLOYMENT.md#63-恢复)。
恢复见 [部署指南 8.3](DEPLOYMENT.md#83-恢复)。
**别把新库配旧 WAL 用**——会损坏数据(用上面已 checkpoint 的单文件备份最省心)。
### Q:数据库文件越来越大
@@ -372,10 +372,33 @@ docker compose exec portal python manage.py vacuum
`明文凭证已加密:settings[uid=1].cookie`;
3. 建 `captchas` 表、给各表补 `user_id` 列与索引。
### Q:从 1.2.0 升级到 1.3.0 要做什么
**同样零手工动作**,但会做一次**配置作用域收敛**,值得知道它改了什么:
| 做了什么 | 效果 |
|---|---|
| 把管理员个人名下的**调度与采集参数**提升到实例级(`user_id=0`) | 这些策略从此「一台部署一套」,对所有账号统一生效 |
| 清掉这些键在 `user_id<>0` 下的残留 | 不会再有「某个账号偷偷带着一份自己的旧调度时刻」 |
| 实例级不再保留 `cookie` / `user_agent` | 凭证只属于个人;实例级存凭证等于给所有账号发同一张身份 |
审计里会留一条 `promote_global_settings`,日志里能看到「配置作用域收敛」。
迁移**幂等**,可重复启动。
**升级后建议确认两件事:**
1. 各账号的 Cookie 仍然「已配置」(能解密)——「配置管理 → 我的云端凭证」;
2. 用一个普通账号登录,「任务管理」里的调度时刻应当是**只读**的,
点保存会被拒并点名越权项。
> 如果你的旧部署里**不同账号原本设了不同的采集时刻**,升级后会统一成管理员那一刻。
> 这是刻意的(一台部署一个时刻表,避免多个采集抢同一把写锁),
> 但需要提前知会使用者:他们之后要改时刻得找管理员。
看迁移结果:
```bash
docker compose logs portal | grep -i migrate
docker compose logs portal | grep -iE "迁移|migrat|收敛|promote"
docker compose exec portal python manage.py users
docker compose exec portal python manage.py stats
```
@@ -391,14 +414,25 @@ docker compose exec portal python manage.py stats
> 迁移用 `PRAGMA user_version` 记录版本,**幂等**:重复启动不会重复迁移。
> 改列这种操作现在也由 `init_db()` 自动完成,不再是「需要手工迁移」。
> 想确认当前库结构版本:
> ```bash
> docker compose exec portal python -c "
> from workbuddy_portal import db; c = db.connect()
> print('user_version =', c.execute('PRAGMA user_version').fetchone()[0])"
> ```
> 1.2.0 是 `2`,1.3.0 是 `3`。
### Q:日志在哪、怎么滚动
| 位置 | 内容 |
|---|---|
| `docker compose logs portal` | 容器 stdout(entrypoint + waitress) |
| 命名卷 `workbuddy-portal_wb_logs` 里的 `app.log` | 应用日志,滚动 2 MB × 3 |
| 页面「日志管理」 | 采集逐行日志 + 应用日志尾部 + 操作审计 |
| 位置 | 内容 | 谁能看 |
|---|---|---|
| `docker compose logs portal` | 容器 stdout(entrypoint + waitress) | 能登服务器的人 |
| 命名卷 `workbuddy-portal_wb_logs` 里的 `app.log` | 应用日志,滚动 2 MB × 3 | 能登服务器的人 |
| 页面「日志管理」 | 全实例采集逐行日志 + 应用日志尾部 + 操作审计 | **仅管理员** |
| 页面「任务管理 → 运行历史」 | **你自己**的采集记录(触发方式、耗时、条数) | 所有登录用户 |
> 普通账号看不到「日志管理」页(导航里不显示,直接敲 `/logs` 返回 403)。
> 自己排错先用「任务管理 → 运行历史」,需要逐行日志时找管理员。
### Q:想改采集的接口地址(走镜像/代理)
@@ -415,13 +449,18 @@ docker compose exec portal python manage.py stats
**五层,前两层必须跑绿**:
```bash
python tools/smoke.py # 离线回归 165 项(不需要起服务)
python tools/smoke.py # 离线回归 215 项(不需要起服务)
python tools/check_docs.py # 文档自检(改过任何 md 就跑)
python tools/demo_data.py # 可选:造一份合成示例库
python manage.py serve --port 8849 --no-scheduler # 另开终端
python tools/check_live.py --base http://127.0.0.1:8849 # 真实 HTTP 83 项
python tools/check_live.py --base http://127.0.0.1:8849 # 真实 HTTP 122 项
python tools/shots.py --base http://127.0.0.1:8849 --full # 界面截图 + JS 报错
```
> 对**示例库**跑 `check_live.py` 时记得加 `--db data/demo/usage.sqlite` ——
> 它的 `--db` 默认值是真实的 `data/usage.sqlite`,不传会从错误库里取验证码答案,
> 症状是「登录失败」加一串看不懂的断言失败。
两个新增参数值得一提:
- `check_live.py --db <路径>`:让它直接读库里的验证码答案,从而**自动过验证码**登录;
+439 -218
查看文件
@@ -1,10 +1,13 @@
# WorkBuddy Portal 用户使用手册
# 用户使用指南
> 面向**使用者**(不是开发者)。读完这份就能独立完成日常操作:
> 注册 / 登录 → 配好自己的凭证 → 看用量 → 查明细 → 导数据 → 处理常见异常。
>
> 装服务的人看 [部署与运维指南](DEPLOYMENT.md)。**注册进来的账号是普通账号**,
> 权限范围见 [第三章](#三权限与数据边界) —— 这一章请务必读一遍。
> **关于配图**:本文所有截图都用 `tools/demo_data.py` 生成的**合成示例数据**渲染
> ——模型名统一是 `demo-*`,客户端为 `vscode`/`webconsole`/`sdk`,Prompt 是通用示例文本,
> —— 模型名统一是 `demo-*`,客户端为 `vscode`/`webconsole`/`sdk`,Prompt 是通用示例文本,
> Cookie 是**假串**(`wb_demo_session=…`),账号是 `admin` 与 `demo` 两个。
> 所以你可以照着重现出几乎一样的界面,也不必担心文档里夹带真实账号信息。
> 想自己搭一份这样的环境:`python tools/demo_data.py` 然后按输出的提示起服务即可。
@@ -12,17 +15,19 @@
**目录**
- [一、这个系统是做什么的](#一这个系统是做什么的)
- [二、登录、注册与账号](#二登录注册与账号)
- [三、获取并填写 Cookie](#三获取并填写-cookie)
- [四、概览页:一眼看清家底](#四概览页一眼看清家底)
- [五、用量大屏:交互式分析](#五用量大屏交互式分析)
- [六、数据明细页:查、筛、导](#六数据明细页查筛导)
- [七、任务管理页:定时与补采](#七任务管理页定时与补采)
- [八、配置管理页:参数与维护](#八配置管理页参数与维护)
- [九、日志管理页:出问题先看这里](#九日志管理页出问题先看这里)
- [十、用户管理页(仅管理员)](#十用户管理页仅管理员)
- [十一、常见任务速查](#十一常见任务速查)
- [十二、常见问题](#十二常见问题)
- [二、注册与登录](#二注册与登录)
- [三、权限与数据边界](#三权限与数据边界)
- [四、配置你唯一的配置项:Cookie](#四配置你唯一的配置项cookie)
- [五、概览页:一眼看清家底](#五概览页一眼看清家底)
- [六、用量大屏:交互式分析](#六用量大屏交互式分析)
- [七、数据明细页:查、筛、导](#七数据明细页查筛导)
- [八、任务管理页:手动采集与只读的调度](#八任务管理页手动采集与只读的调度)
- [九、配置管理页](#九配置管理页)
- [十、个人中心](#十个人中心)
- [十一、管理员专属功能](#十一管理员专属功能)
- [十二、信息安全与隐私安全](#十二信息安全与隐私安全)
- [十三、常见任务速查](#十三常见任务速查)
- [十四、常见问题](#十四常见问题)
---
@@ -36,17 +41,21 @@
一次典型的日常是:
```
每个账号按自己配的时刻自动采集(默认 09:00 / 17:00,你什么都不用做)
管理员把采集时刻统一设好(默认 09:00 / 17:00),服务端到点自动采集
↓
你想看看进度 → 打开「概览」看今天用了多少
想深挖 → 打开「用量大屏」按模型/客户端/时段切
要找某条记录 → 「数据明细」搜索 + 展开 Prompt
要拿给别人 → 「数据明细」→ 导出 CSV
你注册 / 登录 后做的第一件事:粘贴自己的 Cookie(否则采集会跳过你)
↓
想看进度 → 「概览」看今天用了多少
想深挖 → 「用量大屏」按模型 / 客户端 / 时段切
要找某条 → 「数据明细」搜索 + 展开 Prompt
要拿给别人 → 「数据明细」→ 导出 CSV
```
**每个账号只看得见自己的数据**,这一点在下面第三章展开。
---
## 二、登录、注册与账号
## 二、注册与登录
### 2.1 登录
@@ -56,21 +65,21 @@
| 项目 | 说明 |
|---|---|
| 默认账号 | `admin` / `admin123`(**只有数据库里一个账号都没有时**才会创建) |
| 默认管理员 | `admin` / `admin123`(**只有数据库里一个账号都没有时**才会创建) |
| 登录保持 | 12 小时 |
| 验证码 | 默认**始终要求**,4 位,不区分大小写,5 分钟内有效、只能用一次 |
| 验证码 | 默认**始终要求**,4 位,不区分大小写,5 分钟内有效、**只能用一次** |
| 失败限制 | 同一 IP、或同一用户名连续错 5 次,锁定 10 分钟 |
| 退出 | 右上角「退出」(走 POST,防被恶意链接静默触发) |
**关于验证码**:
- 图上只有数字与大写字母,并且**去掉了容易看错的 `0 O 1 I L`**;
- 图上只有数字与字母,并且**去掉了容易看错的 `0 O 1 I L`**;
- 看不清就**点图片换一张**,不消耗任何额度;
- 一张验证码**用完即废**:输错要换新的,登录用过之后也不能再拿去注册;
- 答案只存在服务器数据库里,浏览器拿到的只是一个随机编号——**在网页源码里搜不到答案**;
- **答案只存在服务器数据库里**,浏览器拿到的只是一个随机编号 —— 在网页源码里搜不到答案;
- 被锁定期间,即使验证码填对也会被拒,等 10 分钟或换一个来源。
> ⚠️ **首次部署请立刻改密码**:系统是给局域网访问的,默认密码等于没锁门。
> ⚠️ 如果你是**管理员**且是首次部署:立刻改密码(`admin123` 等于没锁门)。
> 改法:「个人中心 → 修改登录密码」,或命令行 `python manage.py passwd admin 新密码`。
### 2.2 自助注册
@@ -93,78 +102,99 @@
2. **来源限额** —— 同一个 IP 每天最多注册 3 个账号(管理员可调);
3. **总开关** —— 管理员可以随时关闭注册入口。
> ### 注册成功后你是什么权限
> **注册出来的账号一律是「普通账号」**。普通账号能做的事只有一件配置相关的:
> **维护你自己的 Cookie 和 User-Agent**。
>
> 定时任务的频率、采集参数、日志查看这些都属于管理员,你看到的是只读的。
> 详细清单见下一章。
> 注册成功后**不会**自动帮你配好采集。你要粘贴的是**你自己账号**的 Cookie,
> 见 [第三章](#三获取并填写-cookie)。在那之前,概览页只会提示「未配置凭证」。
### 2.3 个人中心
点右上角**你自己的名字**,进入个人中心(`/profile`)。
![个人中心](images/10-profile.png)
| 区块 | 能做什么 |
|---|---|
| 四张卡片 | 我的记录数 / 我的积分 / 采集次数 / 我的 Cookie 状态 |
| 修改资料 | 改显示名、邮箱(用户名只读) |
| 修改登录密码 | 需要原密码;改完当前会话仍然有效 |
| 我的采集凭证 | 是否已配置、多少字符、结尾 4 位、最后更新时间、当前调度时刻 |
> 卡片上的「我的积分」只统计**归属你本人的数据**,别人账号的记录不会算进来。
### 2.4 你的数据边界
这是多用户版最要紧的一条:**每个账号只看得到、也只影响自己的数据。**
| 是「你的」 | 是「共用的」 |
|---|---|
| Cookie 与 User-Agent | 接口基址 / 接口路径 |
| 采集参数(分页、超时、截断…) | 是否开放自助注册、注册限额 |
| 调度开关与每日时刻 | 验证码策略与位数 |
| 用量记录、采集历史、导出的 CSV | 数据库文件本身 |
两点值得记牢:
- **管理员也看不到你的 Cookie 和用量明细。** 用户管理页只显示每个账号的记录条数与积分合计,
点不进去看内容;Cookie 在页面上永远只回显「长度 + 结尾 4 位」。
- **采集只使用本人的凭证。** 系统不会拿别人的 Cookie 去替你采集(那会串号),
所以每个账号都必须各自配一次 Cookie。
### 2.5 权限差别
| 能力 | 管理员 | 普通用户 |
|---|---|---|
| 概览 / 大屏 / 明细 / 任务 / 配置 / 日志 / 个人中心 | ✅ | ✅ |
| 改**自己**的采集参数、调度时刻、Cookie | ✅ | ✅ |
| 手动采集、按区间补采 | ✅(只动自己的数据) | ✅(只动自己的数据) |
| 导出 CSV | ✅(只有自己的) | ✅(只有自己的) |
| 整理数据库(VACUUM,整库操作) | ✅ | ❌ |
| 改**实例级**设置(接口地址、开放注册、验证码策略、注册限额) | ✅ | ❌(输入框置灰) |
| 应用日志尾部 | ✅ | ❌(接口 403,页面上该区块为空) |
| **用户管理**(建号 / 停用 / 删号 / 改权限) | ✅ | ❌(导航里不显示,直接访问返回 403) |
> 给同事发普通账号即可,没必要共用管理员——管理员是能停用别人账号的角色。
> 见 [第四章](#四配置你唯一的配置项cookie)。在那之前,概览页只会提示「未配置凭证」,
> 采集到点时会跳过你并记一条 `no_cookie`。
---
## 三、获取并填写 Cookie
## 三、权限与数据边界
**没有 Cookie,采集一定失败。** 这是每个账号**各自**要做一次的手工步骤。
这是本系统最要紧的一章。**先看清自己能用什么,比急着点按钮有用。**
### 3.1 为什么要 Cookie,以及它怎么被保管
### 3.1 两张身份
| | 管理员 | **普通账号(你注册后拿到的)** |
|---|---|---|
| 谁能拿到 | 首个部署账号,或由管理员授权 | 自助注册,或由管理员创建 |
| 配置权限 | 全部 | **只能维护本人的 Cookie / User-Agent** |
| 调度设置 | 可改(实例级,对所有人生效) | **只读**(看不到也改不了频率) |
| 日志查看 | 可看全实例日志与审计 | **无权限**(导航里不显示,直接访问返回 403) |
| 用户管理 | 可建号 / 停用 / 删号 / 改权限 | **无权限**(同上) |
| 看数据 | 只看自己的 | 只看自己的 |
### 3.2 你的权限清单
| 能力 | 普通账号 |
|---|---|
| 登录 / 退出 / 改自己的资料与密码 | ✅ |
| **配置本人的 Cookie 与 User-Agent** | ✅ **这是你唯一可改的配置** |
| 查看概览、用量大屏 | ✅(只有你自己的数据) |
| 查看 / 搜索数据明细、展开 Prompt | ✅(只有你自己的记录) |
| 导出 CSV(当前筛选条件) | ✅(只有你自己的记录) |
| 手动「立即采集一次」、按区间补采 | ✅(只采你自己的) |
| 「补全 Prompt」「导出我的 CSV」 | ✅(只动你自己的) |
| 查看采集运行历史 | ✅(只有你自己的) |
| **设置定时任务频率 / 开关 / 补跑策略** | ❌ **只读** |
| **改采集参数**(分页、超时、截断、证书校验…) | ❌ 只读 |
| **查看日志管理页 / 应用日志** | ❌ 403 |
| **改实例级设置**(接口地址、开放注册、验证码策略) | ❌ |
| **用户管理**(建号 / 停用 / 删号 / 改权限) | ❌ 403 |
| **整理数据库**(`VACUUM`,整库操作) | ❌ |
> **为什么调度不给你改**:采集策略是**整机一套**的(一台部署一个调度时刻表,
> 所有账号在同一时刻被采集)。如果每个账号各定时刻,同一分钟里会有多个采集
> 抢同一把写锁 —— SQLite 是单写者,那样只会互相拖慢。
>
> 你需要「马上采一次」的时候,用「任务管理 → 立即采集一次」,随时可用,不受调度限制。
### 3.3 数据边界:你能看到什么
| 是「你的」 | 是「共用的 / 管理员管」 |
|---|---|
| Cookie 与 User-Agent | 接口基址与路径 |
| 用量记录、采集历史、导出的 CSV | 采集调度时刻表与全部采集参数 |
| 个人资料、登录密码 | 是否开放自助注册、注册限额、验证码策略 |
| — | 数据库文件本身、应用日志 |
三条值得记牢:
- **管理员也看不到你的 Cookie。** 它在数据库里是密文,页面上永远只回显
「长度 + 结尾 4 位」,形如 `1238 字符,结尾 …c0ffe`。
- **管理员也看不到你的用量明细内容。** 用户管理页只显示每个账号的
**记录条数 / 积分合计 / 最后登录时间与 IP** —— 只给「有多少」,不给「是什么」。
- **采集只使用本人的凭证。** 系统不会拿别人的 Cookie 去替你采集(那会串号),
所以每个账号都必须各自配一次 Cookie。
---
## 四、配置你唯一的配置项:Cookie
**没有 Cookie,采集一定失败。** 这是每个账号**各自**要做一次的手工步骤,
也是普通账号唯一需要动手的配置。
### 4.1 为什么要 Cookie,以及它怎么被保管
采集是直接调账号的用量接口,云端靠 Cookie 认人。Cookie 等于账号凭证,所以系统对它:
- **加密后入库**:落库前用 ChaCha20 + HMAC-SHA256 加密(密钥在 `data/instance.json`),
数据库文件被拷走也读不出明文;
- **永不回传明文**:页面与接口只回显「多少字符、结尾 4 位」,形如 `1238 字符,结尾 …c0ffe`;
- **加密后入库**:落库前用 ChaCha20 + HMAC-SHA256 加密(主密钥在 `data/instance.json`,
与数据库文件分开放),数据库被拷走也读不出明文;
- **永不回传明文**:页面与接口只回显「多少字符、结尾 4 位」;
- **只属于你**:存在你的账号名下,别人(包括管理员)看不到、也拿不到;
- **和 User-Agent 绑在一起**:两者必须取自**同一次浏览器请求**,否则云端会认为是另一个客户端。
> 「配置管理 → 我的云端凭证」里如果出现 **无法解密** 的红字提示,说明实例主密钥被换过
> (`data/instance.json` 被删或被替换),重新粘贴一次即可。详见
> [十二、常见问题](#cookie-显示无法解密)。
> [十四、常见问题](#cookie-显示无法解密)。
### 3.2 拿 Cookie 的两种办法
### 4.2 拿 Cookie 的两种办法
**办法 A:让程序自己从编辑器设置里读(最省事)**
@@ -176,69 +206,78 @@ python manage.py import-creds -u alice # 想导给谁就写谁的用户名
```
它会去读编辑器 `settings.json` 里的 `codebuddyUsage.*` 字段,写进数据库。
Docker 部署时对应 `WB_IMPORT_CREDS=1`(需要把设置文件挂进容器)。
> ⚠️ 导入的是**运行这条命令的那台机器上、那个编辑器账号**的 Cookie。
> 如果 A 同事的机器上跑这条命令去给 B 同事的账号导入,采到的就是 A 的数据——
> 所以更稳的做法是让每个人自己登录网页、粘贴自己的 Cookie。
> 如果 A 同事的机器上跑这条命令去给 B 同事的账号导入,采到的就是 A 的数据 ——
> 所以**更稳的做法是自己登录网页、粘贴自己的 Cookie**(就是下面的办法 B)。
> 另外这条命令需要能访问部署机器,普通账号通常直接走办法 B。
**办法 B:手工复制(一定可行)**
**办法 B:手工复制(一定可行,推荐)**
1. 浏览器打开并登录 WorkBuddy 官网;
2. 按 `F12` 打开开发者工具 → 切到 **Network(网络)** 标签;
3. 刷新页面,随便点一个发往 `workbuddy.cn` 的请求;
4. 在 **Request Headers(请求标头)** 里找到 `Cookie:` 一行;
5. **整行值**复制下来(很长,通常几千字符,要复制完整);
6. 到本系统「配置管理 → 凭证 → Cookie」,粘贴,保存。
6. 到本系统「**配置管理 → 我的云端凭证**」,粘贴到 `Cookie` 输入框,保存。
> 顺手把 User-Agent 也填成同一个浏览器的 UA,成功率高一些。
### 3.3 验证 Cookie 是否有效
### 4.3 验证 Cookie 是否有效
保存后到「任务管理 → 立即采集一次」,然后看「日志管理」最新一条:
保存后到「**任务管理 → 立即采集一次**」,然后看该页「运行历史」里那一条的记录:
| 日志里看到 | 含义 | 怎么办 |
| 看到 | 含义 | 怎么办 |
|---|---|---|
| `新增 N 条` 或 `无新增(已是最新)` | ✅ 正常 | — |
| `cookie_expired` / `401` / `403` | Cookie 过期了 | 重新执行 3.2 |
| `no_cookie`(采集被跳过) | 这个账号**还没配** Cookie | 按 3.2 填一份 |
| `cookie_broken` | 密文解不开(实例主密钥被换过) | 重新粘贴一次,见 [十二](#cookie-显示无法解密) |
| `TLS` / `SSLError` | 证书校验失败 | 见「十二、常见问题」 |
| `cookie_expired` / `401` / `403` | Cookie 过期了 | 重新执行 4.2 |
| `no_cookie`(采集被跳过) | 你**还没配** Cookie | 按 4.2 填一份 |
| `cookie_broken` | 密文解不开(实例主密钥被换过) | 重新粘贴一次,见 [十四](#cookie-显示无法解密) |
| `TLS` / `SSLError` | 证书校验失败 | 找管理员(`ssl_verify` 是实例级参数,你改不了) |
> 普通账号看不到「日志管理」页,但**采集运行历史**在「任务管理」页下方就有 ——
> 排错够用了。整机应用日志和操作审计属于管理员。
---
## 四、概览页:一眼看清家底
## 五、概览页:一眼看清家底
登录后的首页。
![概览页](images/01-overview.png)
从上到下四块:
**1. KPI 卡片(6 个)**
**1. KPI 卡片(6 个)** —— 只统计**归属你本人**的数据,别人的记录不会算进来。
| 卡片 | 含义 |
|---|---|
| 存档总量 | 库里一共多少条记录(只增不减) |
| 累计积分 | 全部记录的积分合计 |
| 存档总量 | 你名下有多少条记录(只增不减) |
| 累计积分 | 你名下全部记录的积分合计 |
| 活跃天数 | 有记录的自然日数量 |
| 今日积分 | 今天(本地时区)已消耗 |
| 今日 vs 昨日 | 今日与**昨日整日**对比,带涨跌幅 |
| 采集健康 | 最近若干次采集的成功 / 失败情况 |
**2. 今日 vs 昨日整日**
注意「昨日」是**完整一天**,而「今日」还在进行中——上午看数字偏低是正常的,
注意「昨日」是**完整一天**,而「今日」还在进行中 —— 上午看数字偏低是正常的,
该跟昨天的**同一时段**比才有意义(大屏页能做这个对比)。
**3. 调度状态**
显示调度开关、下次执行时刻、上次采集结果。这里显示「已停用」时采集不会自动跑,
到「任务管理」把调度打开。
显示调度开关、下次执行时刻、上次采集结果。
**普通账号这里显示的时刻是只读的** —— 它由管理员统一设定,
提示会写「时刻由管理员统一设定,你可以在任务管理里查看,也可以随时手动采集本人数据」。
**4. 模型 TOP + 最近采集**
按模型看积分消耗排行;下方是最近几次采集的触发方式(手动 / 调度 / 启动补跑)、
耗时、抓取条数、新增条数、去重条数。
耗时、抓取条数、新增条数、去重条数。**运行号对普通账号是纯文本**(不能点进日志页)。
> 若你还没配 Cookie,页面顶部会有醒目提示引导你去「配置管理」——
> 在那之前采集到点会跳过你。
---
## 五、用量大屏:交互式分析
## 六、用量大屏:交互式分析
左侧导航点「**用量大屏**」,或直接访问 `/dashboard`。
@@ -253,23 +292,24 @@ Docker 部署时对应 `WB_IMPORT_CREDS=1`(需要把设置文件挂进容器
| 趋势折线 | 按天的积分走势,判断是否在加速 |
| 维度分布 | 按**模型**或**客户端**拆分的占比 |
| 时段分布 | 24 小时里集中在哪些时段(配合判断是否有脚本在跑) |
| 单笔 TOP | 最贵的单次请求,含 Prompt 摘要——最值得优化成本的地方 |
| 单笔 TOP | 最贵的单次请求,含 Prompt 摘要 —— 最值得优化成本的地方 |
![大屏交互](images/08-dashboard-interact.png)
> 页面左上角有「← 返回后台」等入口,随时能回管理后台。
> 大屏的数据是**按当前筛选窗口实时取**的,不是预生成的静态图——切区间会重新请求。
> 页面左上角有「← 返回后台」入口,随时能回管理后台。
> 大屏的数据是**按当前筛选窗口实时取**的,不是预生成的静态图 —— 切区间会重新请求。
> 大屏同样是**按账号隔离**的:你只会看到自己的数据。
**怎么用它省钱**:先看「单笔 TOP」抓出最贵的请求类型,再看「时段分布」判断是不是
某个自动化任务在固定时间跑,最后用「数据明细」把那一批记录导出来逐条分析。
---
## 六、数据明细页:查、筛、导
## 七、数据明细页:查、筛、导
![数据明细页](images/02-records.png)
### 6.1 筛选条件
### 7.1 筛选条件
| 条件 | 说明 |
|---|---|
@@ -283,152 +323,209 @@ Docker 部署时对应 `WB_IMPORT_CREDS=1`(需要把设置文件挂进容器
> 日期写错格式(如 `abc`、`2026-13-99`)不会白屏,系统会忽略非法值并提示。
### 6.2 看单条的完整 Prompt
### 7.2 看单条的完整 Prompt
列表默认**不显示 Prompt 全文**(它占数据体积约 80%)。点行首的「展开」看该条的完整 Prompt。
想批量看就导出 CSV。
### 6.3 导出 CSV
### 7.3 导出 CSV
点「导出 CSV」,会把**当前筛选条件下的全部记录**(不是当前页)流式导出,
文件名形如 `usage_2026-09-01_2026-09-14.csv`。
- 编码为 **UTF-8 带 BOM**,Excel 双击直接打开不乱码;
- 列与官网导出的 xlsx **完全同构**:`requestId, credits, prompt, model, client, requestTime`;
- 数据量大时也是边查边吐,不会把服务器内存吃满。
- 数据量大时也是边查边吐,不会把服务器内存吃满;
- **导出的只有你自己的记录**,不含任何别人的数据。
---
## 七、任务管理页:定时与补采
## 八、任务管理页:手动采集与只读的调度
![任务管理页](images/03-tasks.png)
![任务管理页(普通账号视图)](images/03b-tasks-user.png)
### 7.1 调度设置
### 8.1 调度信息(只读)
| 项 | 说明 |
普通账号在「采集调度」这块看到的是**只读表格**,不是表单:
| 项 | 你看到的 |
|---|---|
| 启用调度 | 总开关。关掉后只有手动采集会跑 |
| 每日时刻 | 逗号分隔的本地时刻,如 `09:00,17:00`。**保存即生效,不用重启** |
| 启动补跑 | 打开后,程序启动时会把今天已错过、且还在宽限期内的时刻补采一次 |
| 补跑宽限期 | 超过多少小时就不补了(默认 12 小时) |
| 启用调度 | 开关状态(由管理员设定) |
| 每日时刻 | 如 `09:00,17:00`(由管理员设定) |
| 启动补跑 / 补跑宽限期 | 由管理员设定 |
| 采集参数 | 去「配置管理」看,同样是只读 |
> 调度线程在 Web 进程内,所以「关掉 Web」等于「关掉调度」。
> 如果偶尔忘了开机,靠「启动补跑」把错过的时刻补回来。
页面上会明确写着「**调度时刻与采集参数由管理员统一设定**,普通账号只读」。
要是你需要改时刻,找管理员 —— 这是**整机一套**的策略。
> **调度是按账号配置的**:你在这里改开关与时刻,只影响**你自己**的采集。
> 到达时刻时,系统会逐个账号跑——没配 Cookie 的账号会被跳过并在日志里记一条
> `no_cookie`,不会影响别人。别人也可以在别的时间点采,互不干扰。
> 为什么这么设计:一台部署只有一个调度线程,所有账号在同一时刻被采集。
> 让大家各定时刻只会让多个采集抢同一把写锁(SQLite 单写者),互相拖慢。
>
> **但你随时可以手动采。** 需要「现在就采一次」时用下面的按钮,不受调度限制。
### 7.2 手动采集
### 8.2 手动采集(你可以用)
- **立即采集一次**:按断点续采,最常用的按钮。
- **按区间补采**:填 `起` / `止`,把这几天重新扫一遍。
用途:换了 Cookie 之后回补漏掉的日期;或怀疑某天数据不全时重扫。
重扫**不会产生重复**——主键去重,已存在的记录按「更早的本地时间」保留。
重扫**不会产生重复** —— 主键去重,已存在的记录按「更早的本地时间」保留。
> 这两个动作**只采集你自己的**数据,不会碰到别人的。
>
> **同一时刻只能有一个采集在跑**。重复点击会返回「忙碌」提示,这是设计如此
> (SQLite 是单写者,并发只会互相拖慢)。等它跑完再点。
### 7.3 运行历史
### 8.3 运行历史
每次采集都留一条记录:触发方式、状态、耗时、抓取/新增/去重条数、退出码。
点「详情」看这一次的**逐行日志原文**,包括 `[warn]` 和 `[error]`。
每次采集都留一条记录:触发方式、状态、耗时、抓取 / 新增 / 去重条数、退出码。
你能看到的**只有你自己的运行历史**。
> 「日志」列在普通账号下显示为 `—`。逐行日志原文在「日志管理」页,
> 那是**管理员专属**(你访问会返回 403)。
---
## 八、配置管理页:参数与维护
## 九、配置管理页
![配置管理页](images/04-config.png)
![配置管理页(普通账号视图)](images/04b-config-user.png)
### 8.1 我的云端凭证
### 9.1 我的云端凭证(**这是你唯一能改的配置**)
| 字段 | 说明 |
|---|---|
| Cookie | **你本人账号**的凭证,密文入库。留空保存 = 不修改;填一个 `-` = 清空已保存的 Cookie |
| User-Agent | 与拿 Cookie 的浏览器保持一致更稳(**两者必须取自同一次请求**) |
保存后页面上只显示 `当前 Cookie:1238 字符,结尾 …c0ffe(2026-09-15 10:22 更新)`——
保存后页面上只显示 `当前 Cookie:1238 字符,结尾 …c0ffe(2026-09-15 10:22 更新)` ——
页面上、接口里都拿不到明文。若显示「**无法解密**」的红字横幅,说明实例主密钥被换过,
重新粘贴一次即可。
### 8.2 采集参数
> 凭证卡片上会有一行提示写着「**这一块是你唯一可以修改的配置**」——
> 看到它就找对地方了。
| 参数 | 默认 | 范围 | 说明 |
### 9.2 采集参数(只读)
普通账号在这一块看到的是**只读表格**,输入框不可编辑,页面上还有一张
「**为什么采集参数是只读的**」说明卡:
| 参数 | 默认 | 说明 |
|---|---|---|
| `api_base` | `https://www.workbuddy.cn` | 接口基址(镜像 / 代理时改) |
| `api_path` | `/billing/meter/get-user-request-usage` | 接口路径 |
| `page_size` | 200 | 单页条数(20 ~ 1000) |
| `rewind_minutes` | 2 | 断点回退分钟数(0 ~ 120) |
| `drift_tolerance_minutes` | 5 | 云端时间漂移告警阈值(0 ~ 720) |
| `max_prompt` | 2048 | `Prompt` 入库截断长度,`0` = 不截断 |
| `verify_days` | 0 | 每次采集后做整日完整性校验的天数 |
| `timeout` | 30 | 单次 HTTP 超时(秒) |
| `ssl_verify` | 1(开) | 校验云端 HTTPS 证书,**只在自签 / 企业代理场景才关** |
> **为什么只读**:这些参数决定「一次采集怎么发请求」,属于实例级策略。
> 特别是 `ssl_verify` —— 谁能关掉它,谁就能让这台服务器在不校验证书的情况下
> 把所有人的 Cookie 发出去。这类开关必须收在管理员手里。
>
> 你确实需要调其中某一项时,把需求和理由告诉管理员,由他统一改 ——
> 改完对所有人立即生效,不用重启。
### 9.3 维护动作
| 按钮 | 作用 | 何时用 | 普通账号 |
|---|---|---|---|
| `api_base` | `https://www.workbuddy.cn` | — | 接口基址(镜像 / 代理时改)。**实例级,普通账号只读** |
| `api_path` | `/billing/meter/get-user-request-usage` | — | 接口路径。**实例级,普通账号只读** |
| `page_size` | 200 | 20 ~ 1000 | 单页条数。调大能减少请求次数,但单次更慢 |
| `rewind_minutes` | 2 | 0 ~ 120 | 断点回退分钟数。避免云端写入延迟导致漏数据 |
| `drift_tolerance_minutes` | 5 | 0 ~ 720 | 云端时间比本地早超过该值才告警 |
| `max_prompt` | 2048 | 0 ~ 20000 | `Prompt` 入库截断长度,`0` = 不截断 |
| `verify_days` | 0 | 0 ~ 90 | 每次采集后做整日完整性校验的天数,`0` = 关 |
| `timeout` | 30 | 5 ~ 300 | 单次 HTTP 超时(秒) |
| `ssl_verify` | 1(开) | — | 校验云端 HTTPS 证书。**只在自签/企业代理场景才关** |
> **写错的值会被当场拒绝**并提示原因,不会污染配置(历史版本会因为一个手滑的数字
> 让采集整个跑不起来)。范围外的数、非数字都会在保存时被拦下。
> 上表里除 `api_base` / `api_path` 外,**其余都是「你自己的」配置**——改它只影响你这个账号的
> 采集行为,不影响别人。`schedule_times` 这类调度项同理:每个人可以定自己的采集时刻。
### 8.3 维护动作
| 按钮 | 作用 | 何时用 | 谁能用 |
|---|---|---|---|
| 补全 Prompt | 把缺失的 `Prompt` 从云端回补 | 从官网 xlsx 导入过数据后(xlsx 丢约 22%) | 所有人(只补自己的) |
| 导出我的 CSV | 导出**你自己的**全量数据到 `data/exports/` | 归档 / 交接 | 所有人 |
| 整理数据库 | `wal_checkpoint` + `VACUUM`(**整库操作**) | 删过数据后回收空间,或 WAL 文件偏大时 | **仅管理员** |
| 补全 Prompt | 把缺失的 `Prompt` 从云端回补 | 从官网 xlsx 导入过数据后(xlsx 丢约 22%) | ✅ 只补自己的 |
| 导出我的 CSV | 导出**你自己的**全量数据到 `data/exports/` | 归档 / 交接 | ✅ |
| 整理数据库 | `wal_checkpoint` + `VACUUM`(**整库操作**) | 删过数据后回收空间 | ❌ 仅管理员 |
这些动作**耗时且会占用写权限**,所以有二次确认。执行期间不要重复点击。
### 8.4 修改密码
### 9.4 修改密码
填「当前密码 / 新密码 / 确认新密码」。改完当前会话仍然有效,其他会话需要重新登录。
(同样的表单在「个人中心」也有一份。)
### 8.5 实例级设置(仅管理员可见)
### 9.5 实例级设置(仅管理员可见)
页面最下方这一块,**只有管理员看得到**,改动对**所有账号**生效:
普通账号**看不到这一块**。它包含「开放自助注册 / 同 IP 注册上限 / 验证码策略 /
验证码位数」,改动对**所有账号**生效。见 [第十一章](#十一管理员专属功能)。
---
## 十、个人中心
点右上角**你自己的名字**,进入个人中心(`/profile`)。
![个人中心](images/10-profile.png)
| 区块 | 能做什么 |
|---|---|
| 四张卡片 | 我的记录数 / 我的积分 / 采集次数 / 我的 Cookie 状态 |
| 修改资料 | 改显示名、邮箱(**用户名只读**) |
| 修改登录密码 | 需要原密码;改完当前会话仍然有效 |
| 我的采集凭证 | 是否已配置、多少字符、结尾 4 位、最后更新时间、当前调度时刻(只读) |
顶栏右侧会有一个 **「普通账号」** 小标签(管理员则是「管理员」)——
不确定自己是什么权限时看一眼这里。
> 卡片上的「我的积分」只统计**归属你本人的数据**,别人账号的记录不会算进来。
---
## 十一、管理员专属功能
> 普通账号可以跳过这一章 —— 里面的入口你都看不到(导航里不显示,直接敲地址返回 403)。
### 11.1 调度与采集参数(在「任务管理 / 配置管理」里改)
管理员在这两页看到的是**可编辑表单**,改动是**实例级**的,对**所有账号**生效:
| 项 | 默认 | 说明 |
|---|---|---|
| 启用调度 | 开 | 总开关。关掉后只有手动采集会跑 |
| 每日时刻 | `09:00,17:00` | 逗号分隔的本地时刻。**保存即生效,不用重启** |
| 启动补跑 | 开 | 启动时把今天已错过、且还在宽限期内的时刻补采一次 |
| 补跑宽限期 | 12 小时 | 超过多少小时就不补了 |
| 采集参数 | 见 9.2 | 分页 / 回退 / 超时 / 截断 / 证书校验等 |
> ⚠️ **改 `schedule_times` 会清理槽位簿记**:系统只清掉「不再存在的时刻」对应的
> 槽位标记(所有账号一起清),**不做全清** —— 全清会让全部账号在宽限期内一起重采。
### 11.2 实例级设置
「配置管理」页最下方,改动对**所有账号**生效:
| 设置 | 默认 | 说明 |
|---|---|---|
| 开放自助注册 | 允许 | 关掉后登录页不再显示「自助注册」,只能由管理员建号 |
| 同 IP 每日注册上限 | 3 | 防止一个来源批量刷号;范围 1 ~ 50 |
| 验证码策略 | 始终要求 | `始终要求` / `仅连续失败 2 次后要求` / `关闭` |
| 验证码位数 | 4 | 4 ~ 6 位。位数越多越难被自动识别,也越考验眼力 |
| 验证码位数 | 4 | 4 ~ 6 位 |
> **验证码策略怎么选**:默认的「始终要求」最安全;「仅连续失败后要求」对天天登录的人更友好,
> 但会给机器人留出 2 次免验证码的尝试机会。**「关闭」只有在前面已经有可信网关时才考虑。**
> **验证码策略怎么选**:默认的「始终要求」最安全;「仅连续失败后要求」对天天登录的人
> 更友好,但会给机器人留出 2 次免验证码的尝试机会。**「关闭」只有在前面已经有
> 可信网关时才考虑。**
>
> 验证码的答案只存在服务端 `captchas` 表里,5 分钟过期、用一次就删——
> 验证码的答案只存在服务端 `captchas` 表里,5 分钟过期、用一次就删 ——
> 所以它不会随会话 Cookie 泄漏出去。
---
## 九、日志管理页:出问题先看这里
### 11.3 日志管理页
![日志管理页](images/05-logs.png)
三个区块:
**1. 采集运行历史**(可翻页 + 按状态筛 `ok` / `warn` / `error` / `running`)
每行可展开看**逐行日志原文**——排错时最有用的一块。
1. **采集运行历史**(可翻页 + 按状态筛 `ok` / `warn` / `error` / `running`)
—— 每行可展开看**逐行日志原文**,排错时最有用的一块;表格里多了「账号」列,
能看出是哪个人触发的。
2. **应用日志尾部** —— Web 进程自身的日志(启动、异常栈、调度动作)。
3. **操作审计** —— 谁在什么时候做了什么:登录、登录失败、改配置、触发采集、导出、
建号删号、注册……
**2. 应用日志尾部**
Web 进程自身的日志(启动、异常栈、调度动作)。默认展示尾部若干行。
> 这是**全实例**视角(不是只你自己的)。普通账号访问本页返回 403,
> 所以管理员可以放心把这里当排错入口。
>
> 排错顺序建议:操作审计(有没有人动过)→ 采集历史(采集本身成不成功)→
> 应用日志(程序有没有异常)。
**3. 操作审计**
谁在什么时候做了什么:登录、登录失败、改配置、触发采集、导出、建号删号、注册……
可按**动作**筛选,支持翻页。
> 排错顺序建议:操作审计(有没有人动过) → 采集历史(采集本身成不成功) → 应用日志(程序有没有异常)。
> **多用户下你看到的范围**:「采集运行历史」与「操作审计」只有你**自己的**记录;
> 「应用日志尾部」是整机日志,**仅管理员可见**(普通账号看到的是空区块,接口返回 403)。
---
## 十、用户管理页(仅管理员)
### 11.4 用户管理页
![用户管理页](images/06-users.png)
@@ -441,7 +538,7 @@ Web 进程自身的日志(启动、异常栈、调度动作)。默认展示
| 改密码 | 给忘了密码的同事重置 |
| 删除 | **不可逆**,会连同该账号的用量数据与 Cookie 一起删除 |
列表还给出每个账号的**记录条数 / 积分合计 / 最后登录时间与 IP**——但**看不到内容**:
列表还给出每个账号的**记录条数 / 积分合计 / 最后登录时间与 IP** —— 但**看不到内容**:
管理员能看到的只是「有多少」,看不到「是什么」,也看不到任何人的 Cookie。
内置四条护栏(前端置灰 + 后端再拦一次):
@@ -451,37 +548,130 @@ Web 进程自身的日志(启动、异常栈、调度动作)。默认展示
3. **不能删除自己**;
4. **不能删掉最后一个启用的管理员**(防止系统变成没人能管)。
页面底部是「**账号操作审计**」:最近 20 条账号相关动作,含**注册**与**登录失败**记录——
想知道有没有人在撞你的密码,看这里。
页面底部是「**账号操作审计**」:最近 20 条账号相关动作,含**注册**与**登录失败**记录
—— 想知道有没有人在撞密码,看这里。
> **给同事开账号时默认选「普通」**:管理员是能停用别人账号的角色,没必要扩大。
---
## 十一、常见任务速查
## 十二、信息安全与隐私安全
这一章讲清「系统为你做了什么」和「你该做什么」。前者是设计保证,后者只有你能做到。
### 12.1 系统为你做的(不需要你操心)
**凭证(Cookie)的保护**
| 措施 | 效果 |
|---|---|
| ChaCha20 + HMAC-SHA256 **encrypt-then-MAC** 静态加密 | 数据库文件被拷走也读不出明文;被篡改会解密失败而不是返回垃圾 |
| 加密主密钥与数据库**分开放** | 主密钥在 `data/instance.json`,与 `usage.sqlite` 不同文件;只拿到库 = 解不开 |
| 加密主密钥与会话签名密钥**分键位** | 轮换其中一个不会影响另一个(混用会导致「想换会话密钥却把所有 Cookie 弄坏」) |
| 页面 / 接口**永不回传明文** | 只回「长度 + 结尾 4 位」;`get_settings()` 对加密键一律置空 |
| 凭证**不参与实例级回落** | `NO_FALLBACK_KEYS = {cookie, user_agent}` —— 回落等于「用别人的身份采集」,是最严重的一类越权 |
| 明文取用只有**一条通道** | `db.get_secret()`,只在真正发采集请求时调用 |
**账号与访问控制**
| 措施 | 效果 |
|---|---|
| 密码只存哈希 | 不存明文;管理员也不知道你的密码 |
| 登录失败**双维度限速** | 同 IP、同用户名各自计数,任一连错 5 次锁定 10 分钟 |
| **先验验证码再比口令** | 否则攻击者能拿「密码对不对」当信号,提前跑完字典 |
| 验证码答案**只在服务端** | 浏览器只拿一个随机 id,网页源码里搜不到答案 |
| 会话 Cookie 加固 | `HttpOnly`(JS 读不到)+ `SameSite=Lax`(防跨站携带)+ 可选 `Secure` |
| **停用立即失效** | 每个请求都回查账号状态,不必等 12 小时会话过期 |
| 写请求需 CSRF 令牌 | 防「登录状态下被别人页面静默提交表单」 |
| 安全响应头 | CSP / `X-Frame-Options` / `nosniff` / `Referrer-Policy` |
| 敏感接口 `no-store` | `/api/*` 与 `/captcha*` 不会被浏览器或中间层缓存 |
**数据隔离**
| 措施 | 效果 |
|---|---|
| 全链路按 `user_id` 过滤 | 查询、采集、调度、导出都带 `uid` 参数 |
| **`uid` 是必填位置参数** | 代码里漏传就直接 `TypeError`,不会静默退化成「返回全量」 |
| 索引以 `user_id` 打头 | 隔离既是安全边界,也是查询性能的前提 |
| 越权写**整单拒绝** | 普通账号提交只读项会被点名拒掉,不会「部分生效」 |
### 12.2 你要做的(系统替不了)
**① 自己的 Cookie 自己填,不要经手别人。**
Cookie 等于账号凭证。让同事代填 = 你的账号交到别人手里。
管理员也看不到你的 Cookie,所以**没有任何人需要知道它**。
**② 不要在公共机器上保留登录态。**
系统会话保持 12 小时。借别人的电脑用完就走右上角「退出」,
别只关标签页(会话在服务端仍然有效)。
**③ 密码别和其他系统复用。**
本系统没有邮件通道,忘密码只能找管理员重置,所以请用密码管理器记好。
**④ Cookie 会过期,这是好事。**
Cookie 通常随浏览器会话失效。过期后重新按 [4.2](#42-拿-cookie-的两种办法) 取一份即可,
历史数据完全不受影响。
**⑤ 导出 CSV 后注意存放。**
导出的 CSV 里有你的 `Prompt` 原文 —— 那是你真实的提问内容,可能包含代码、业务描述
甚至敏感信息。**别随手丢在共享目录或聊天群里**。用完删掉。
**⑥ 管理员注意:备份要连密钥一起。**
`data/instance.json` 里存着 Cookie 的加密主密钥。只备份 `usage.sqlite` 而不备它,
恢复后**所有账号的 Cookie 都会变成「无法解密」**,每个人都要重填一遍。
(反过来这也说明:这个文件本身就是高价值目标,权限要收紧。)
### 12.3 边界说明(诚实的那部分)
- **管理员在运维层面能看到比你想象中多的东西**:整机应用日志、所有人账号的
「记录条数与积分合计」、以及部署机器上的数据库文件本身。
管理员**看不到**你的 Cookie 明文、看不到你的 Prompt 内容、也看不到别人的记录内容 ——
但如果管理员对部署机器有 root 权限,理论上能改代码来绕过界面限制。
**这是所有自托管系统的共同前提**:信任部署这台机器的人。
所以如果是团队共用,请让「能登服务器」和「日常使用」的角色分开。
- **传输加密取决于你的部署方式**:纯局域网 `http://` 部署下,流量在网内是明文的。
要防中间人,需要按 [部署指南第六节](DEPLOYMENT.md#六反向代理与-https) 挂 HTTPS 反代,
并把 `WB_COOKIE_SECURE` 设为 `1`。
- **系统不做数据自动清理**:累积的记录会一直留着。要清理只能在数据库层面做,
属于管理员操作。
---
## 十三、常见任务速查
| 我想… | 怎么做 |
|---|---|
| 自己注册一个账号 | 登录页 → **自助注册**(需管理员开放注册) |
| 配好我的采集 | 配置管理 → 我的云端凭证 → 粘贴 Cookie → 保存 → 任务管理「立即采集一次」 |
| 立刻采集一次 | 任务管理 → 立即采集一次(只采我自己的) |
| 回补某几天的数据 | 任务管理 → 按区间补采,填起止日期 |
| 换我自己的 Cookie | 配置管理 → 我的云端凭证 → 粘贴新 Cookie → 保存 → 回补最近几天 |
| 改我的显示名 / 邮箱 / 密码 | 右上角**点自己的名字** → 个人中心 |
| 看不清验证码 | **点验证码图片**换一张 |
| 看清我是什么权限 | 看顶栏右侧的标签:「管理员」还是「普通账号」 |
| 查我自己的采集有没有成功 | 任务管理 → 运行历史(只有我自己的) |
| 看清验证码 | **点验证码图片**换一张 |
| 导出某段时间的数据给别人 | 数据明细 → 选日期 → 导出 CSV |
| 导出我的全量存档 | 配置管理 → 维护动作 → 导出我的 CSV |
| 找出最贵的请求 | 用量大屏 → 单笔 TOP |
| 看某条请求的完整 Prompt | 数据明细 → 该行「展开」 |
| 给同事开账号 | 用户管理 → 新建账号,权限选**普通**(或让同事自助注册) |
| 同事忘记密码 | 用户管理 → 该行「改密」 |
| 临时封掉某个账号 | 用户管理 → 该行「停用」(数据与 Cookie 保留) |
| 拒绝别人自助注册 | 配置管理 → 实例级设置 → 开放自助注册 → 关闭 |
| 把数据备份走 | 让运维按 [部署指南 6.2](DEPLOYMENT.md#62-备份) 备份命名卷,或在「配置管理」导出 CSV |
| 关掉自动采集 | 任务管理 → 关「启用调度」 |
| 改采集时刻 | 任务管理 → 每日时刻,如 `08:30,12:30,18:00` → 保存 |
| 系统变慢了 | 让**管理员**做「配置管理 → 整理数据库」;再不行看「十二」 |
| 改采集时刻 | ❌ 普通账号只读。找管理员改(或说明需求由他统一设) |
| 看日志排错 | ❌ 普通账号无权限。先用「任务管理 → 运行历史」,不够就找管理员 |
| 关掉自动采集 | ❌ 普通账号无权限。找管理员 |
| 系统变慢了 | 找**管理员**做「配置管理 → 整理数据库」;再不行看下一章 |
| 把数据备份走 | 找运维按 [部署指南第八节](DEPLOYMENT.md#八备份与恢复) 备份,或自己导出 CSV |
| 给同事开账号 | 管理员:用户管理 → 新建账号,权限选**普通**(或让同事自助注册) |
| 同事忘记密码 | 管理员:用户管理 → 该行「改密」 |
| 临时封掉某个账号 | 管理员:用户管理 → 该行「停用」(数据与 Cookie 保留) |
---
## 十二、常见问题
## 十四、常见问题
### 验证码看不清
@@ -492,9 +682,9 @@ Web 进程自身的日志(启动、异常栈、调度动作)。默认展示
三种可能,按顺序排查:
1. **这张图已经用过了** —— 验证码是**一次性**的,输错一次、或登录成功之后,它立刻作废,
1. **这张图已经用过了** —— 验证码是**一次性**的,输错一次、或登录成功之后它立刻作废,
必须点图片重新取一张;
2. **超过了 5 分钟** —— 有效期只有 5 分钟,慢慢来的话会过期,换一张即可;
2. **超过了 5 分钟** —— 有效期只有 5 分钟,换一张即可;
3. **跨了页面** —— 登录页取到的图不能拿去注册页用(两边的验证码是分开的)。
> 另外:如果这个来源已被锁定(连续失败 5 次),即使验证码正确也会被拒;等 10 分钟再试。
@@ -509,35 +699,66 @@ Web 进程自身的日志(启动、异常栈、调度动作)。默认展示
同一个 IP 每天默认最多注册 3 个账号。换个网络,或请管理员把
「同 IP 每日注册上限」调大(范围 1 ~ 50)。
### 我为什么改不了定时任务 / 看不到日志
**这是设计如此,不是权限配错了。** 注册出来的账号一律是普通账号,
它只能维护**本人**的 Cookie / User-Agent,然后查看**本人**的数据。
| 你想要的 | 实际可行的 |
|---|---|
| 改采集时刻 | ❌ 找管理员改(整机一套策略) |
| 改采集参数 | ❌ 把需求告诉管理员 |
| 看日志排错 | 先用「任务管理 → 运行历史」;需要逐行日志时找管理员 |
| 立刻采一次 | ✅ 「任务管理 → 立即采集一次」,随时可用 |
如果你确实需要管理员权限(比如要做整库维护),请让管理员在
「用户管理」里把你的权限改成管理员。
### 采集一直没跑(我的数据没更新)
按这个顺序查:
1. **你配 Cookie 了吗** —— 「配置管理 → 我的云端凭证」是否显示「已配置」。
没配的话采集到点会**跳过你**,运行历史里会有一条 `no_cookie`;
2. **Cookie 过期了吗** —— 运行历史里若有 `cookie_expired` / `401`,按
[4.2](#42-拿-cookie-的两种办法) 重新取一份;
3. **调度开着吗** —— 概览页「调度状态」看开关。关着的话就只有手动采集会跑;
4. **服务是不是停了** —— 问管理员。调度线程在服务端进程里,服务停了就不采。
> 采不到也不慌:修好后用「按区间补采」把那几天补回来就行(重扫不会产生重复)。
### 「Cookie 显示无法解密」
「配置管理 → 我的云端凭证」出现红字横幅,或卡片上写着 **无法解密**:这是说数据库里
存的 Cookie 密文,用当前的实例主密钥解不开了。常见原因是 `data/instance.json`
(里面存着 `cookie_key`)被删除、被替换,或者从别的机器拷了一份数据库过来。
**影响**:这个账号的采集会失败,日志里是 `cookie_broken`。
**影响**:你这个账号的采集会失败,运行历史里是 `cookie_broken`。
**怎么办**:重新粘贴一次这个账号的 Cookie 即可,历史数据不受影响。
**怎么避免**:`data/instance.json` 里存着会话签名密钥和加密主密钥——备份数据库时
**把它一起备份**,并且不要在容器之间混用。
**怎么办**:**重新粘贴一次你自己的 Cookie 即可**,历史数据不受影响。
(这个操作只有你本人能做 —— 别人看不到你的 Cookie,也就没法替你恢复。)
**怎么避免**:这是运维的事 —— 备份数据库时要**把 `instance.json` 一起备份**,
并且不要在容器之间混用。
### 我能不能看别人的用量
不能,管理员也不能。「用户管理」页只显示每个账号的记录条数与积分合计,看不到内容。
这是设计如此:Cookie 是账号级凭证,让它跨账号可见等于把别人的账号交出去。
如果确实需要合并统计,正确做法是让每个人各自导出 CSV,再在外部合并。
如果团队确实需要合并统计,正确做法是**每个人各自导出 CSV,再在外部合并**。
### 采集报 `cookie_expired` / `unauthorized`
Cookie 过期。重新按 [3.2](#32-拿-cookie-的两种办法) 拿一份新 Cookie 填进去。
Cookie 有效期通常是浏览器会话级别,**关掉浏览器可能就失效了**——建议用
「办法 B」从已登录的浏览器里复制时,勾选「保持登录」。
Cookie 过期。重新按 [4.2](#42-拿-cookie-的两种办法) 拿一份新 Cookie 填进去。
Cookie 有效期通常是浏览器会话级别,**关掉浏览器可能就失效了** —— 建议从已登录的
浏览器里复制时,勾选「保持登录」。
### 采集成功但「新增 0 条」
大概率是**正常的**:断点续采意味着没有新请求时确实没有新增。
看「日志管理」里那一次的 `抓取` 条数:
看运行历史里那一次的 `抓取` 条数:
- `抓取 > 0,新增 = 0` → 云端返回的都是库里已存在的,正常;
- `抓取 = 0` → 该时段云端确实没有记录。
@@ -545,11 +766,11 @@ Cookie 有效期通常是浏览器会话级别,**关掉浏览器可能就失
所有日期都按**部署机器的本地时区**(容器里由 `TZ` 决定,默认 `Asia/Shanghai`)计算。
如果服务器时区不是东八区,跨日的数据会落到相邻日期上。
Docker 部署请确认 `TZ=Asia/Shanghai`;裸机部署确认系统时区。
这属于部署问题 —— 找管理员确认 `TZ=Asia/Shanghai`(Docker)或系统时区(裸机)。
### 导出的 CSV 在 Excel 里中文乱码
不会——导出已经带 UTF-8 BOM。如果乱码,先确认你打开的是本系统导出的文件,
不会 —— 导出已经带 UTF-8 BOM。如果乱码,先确认你打开的是本系统导出的文件,
而不是手工用记事本另存过的版本。
### 提示「采集正在进行中」
@@ -561,19 +782,19 @@ Docker 部署请确认 `TZ=Asia/Shanghai`;裸机部署确认系统时区。
1. 强制刷新(`Ctrl+F5`)清掉旧缓存;
2. 检查浏览器控制台有没有资源 404;
3. 到「日志管理 → 应用日志」看有没有异常栈。
3. 找管理员看应用日志有没有异常栈(你这边看不到日志页)。
### 关掉浏览器后调度还在跑吗
在的。调度在**服务端进程**里,和浏览器无关。要停就去「任务管理」关调度开关,
或停掉服务。
在的。调度在**服务端进程**里,和浏览器无关。它是整机一套的,
所以你关不关浏览器都不影响它 —— 也正因为如此,它不归你管。
### 忘记密码
**你自己的密码忘了**:网页上没法自助重置(没有邮件通道),找管理员在
**你自己忘了**:网页上没法自助重置(没有邮件通道),找管理员在
「用户管理 → 该行『改密』」给你设一个新的。
**管理员密码忘了**(或者被自己停用了),到部署机器上执行:
**管理员忘了**(或者被自己停用了),到部署机器上执行:
```bash
python manage.py passwd admin 新密码 # 裸机
@@ -596,14 +817,14 @@ python manage.py users
正本是 Docker 命名卷 `workbuddy-portal_wb_data` 里的 `usage.sqlite`(对应容器内 `/app/data`)。
`docker compose down` **不会删数据**;只有显式 `docker compose down -v`
或手动 `docker volume rm` 才会。备份方法见
[部署指南 6.2](DEPLOYMENT.md#62-备份)——对使用者的日常来说,更简单的做法是
「配置管理 → 维护动作 → 导出全量 CSV」留一份快照。
[部署指南第八节](DEPLOYMENT.md#八备份与恢复) —— 对使用者的日常来说,更简单的做法是
「配置管理 → 维护动作 → 导出我的 CSV」留一份快照。
### 能不能同时开多个采集进程
不能,也没必要。SQLite 单写者 + 文件锁的设计就是为了避免并发写。
真要跑多副本,除第一份外都要设 `WB_DISABLE_SCHEDULER=1`,
且只有一份能安全写——所以**不要**横向扩展这个服务。
且只有一份能安全写 —— 所以**不要**横向扩展这个服务。
---
二进制
查看文件
二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 336 KiB

之后

宽度:  |  高度:  |  大小: 334 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 468 KiB

之后

宽度:  |  高度:  |  大小: 643 KiB

二进制
查看文件
二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 535 KiB

之后

宽度:  |  高度:  |  大小: 1.5 MiB

二进制
查看文件
二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 418 KiB

之后

宽度:  |  高度:  |  大小: 810 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 509 KiB

二进制
查看文件
二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 382 KiB

之后

宽度:  |  高度:  |  大小: 708 KiB

二进制文件未显示。

之后

宽度:  |  高度:  |  大小: 638 KiB

二进制
查看文件
二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 487 KiB

之后

宽度:  |  高度:  |  大小: 833 KiB

二进制
查看文件
二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 458 KiB

之后

宽度:  |  高度:  |  大小: 498 KiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 476 KiB

之后

宽度:  |  高度:  |  大小: 1.5 MiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 476 KiB

之后

宽度:  |  高度:  |  大小: 1.5 MiB

二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 339 KiB

之后

宽度:  |  高度:  |  大小: 383 KiB

二进制
查看文件
二进制文件未显示。

之前

宽度:  |  高度:  |  大小: 386 KiB

之后

宽度:  |  高度:  |  大小: 469 KiB