3 次代码提交
作者 SHA1 备注 提交日期
wangchuanli 23799b4ea5 feat(oss): 补齐开源声明体系(MIT + 第三方声明 + 贡献/安全/行为准则)
* LICENSE              —— MIT
  * THIRD-PARTY-NOTICES  —— 依赖清单、再分发合规说明(含随仓库分发的
                            Apache ECharts 5.6.0 / Apache-2.0)与自查清单
  * CONTRIBUTING.md      —— 开发环境、五层验证、必须遵守的不变量、提交规范
  * SECURITY.md          —— 漏洞私有报告渠道、已有措施、已知非目标
  * CODE_OF_CONDUCT.md   —— 改编自 Contributor Covenant 2.1
  * .github/             —— Bug 报告 / 功能建议表单 + PR 模板
  * .editorconfig        —— 与 .gitattributes 保持一致
  * 全部 Python / Shell 源文件加 SPDX-License-Identifier: MIT 头
  * README 增加「开源与许可」章节与许可标识
2026-09-14 16:15:26 +08:00
wangchuanli 9469a61bbc chore(privacy): 文档与截图改用合成示例数据,移除真实 Prompt 与统计口径
原截图里含有不该公开的内容:
  * docs/images/02-records.png —— 真实 Prompt 全文、请求 ID、真实模型名
  * docs/images/05-logs.png    —— 本机路径与 Windows 用户名
  * docs/images/04-config.png  —— Cookie 尾串
  * 01/03/07/08                —— 真实用量分布与日期

做法:新增 tools/demo_data.py 生成完全合成的示例库(假模型名 demo-*、
通用 Prompt、偏斜的积分分布、RFC 5737 文档专用网段的审计 IP),
在容器里跑它并以 /app 路径截图,再按原规格(1400px + 调色板量化)替换。
同时把 docs/API.md 与 docs/DEPLOYMENT.md 示例响应里的真实模型名与真实
统计数字(1665 条 / 8513.36 积分等)换成示例口径。

顺带修正 .gitignore:只写 data/*.sqlite 会漏掉子目录,补 data/**/*.sqlite
等规则并忽略 data/demo/。
2026-09-14 16:15:23 +08:00
wangchuanli 342da56d4d fix(docker): .dockerignore 漏掉嵌套 __pycache__,字节码混进镜像
只写 `__pycache__/` 时 Docker 仅匹配上下文根目录下的同名目录,
`tools/__pycache__` 等嵌套目录会被原样 COPY 进镜像(实测镜像里确实存在)。
补上 `**/__pycache__/` 与 `**/*.py[cod]`。
2026-09-14 16:15:21 +08:00
共修改 44 个文件,包含 1001 行新增和 26 行删除
+6
查看文件
@@ -9,9 +9,15 @@ data/
logs/
# Python 缓存
# 注意:`__pycache__/` 只能匹配上下文**根目录**下的同名目录,
# 嵌套的(如 tools/__pycache__)必须用 `**/` 前缀——否则会被原样打进镜像。
# 实测过:只写单条时,镜像里仍有 /app/tools/__pycache__。
__pycache__/
**/__pycache__/
*.py[cod]
**/*.py[cod]
*.egg-info/
**/*.egg-info/
.venv/
venv/
+33
查看文件
@@ -0,0 +1,33 @@
# 统一的编辑器约定。与 .gitattributes(* text=auto eol=lf)保持一致。
# 这样无论谁在什么系统上编辑,提交进来的都是 LF 与 UTF-8。
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{html,css,js,json,yml,yaml,svg}]
indent_size = 2
[*.py]
indent_size = 4
max_line_length = 100
[*.md]
# Markdown 里行尾两个空格是有意义的分行,别自动删掉
trim_trailing_whitespace = false
[*.sh]
indent_size = 4
# 容器 entrypoint 必须是 LF,否则报 exec format error / no such file or directory
end_of_line = lf
[Makefile]
indent_style = tab
[*.{png,jpg,jpeg,gif,ico,woff,woff2,sqlite}]
insert_final_newline = false
+83
查看文件
@@ -0,0 +1,83 @@
name: Bug 报告
description: 报告一个可复现的问题
title: "[Bug] "
labels: [bug]
body:
- type: markdown
attributes:
value: |
感谢反馈。提交前请先确认:
1. 你已经读过 [FAQ](https://git.iwali.top/wangchuanli/workbuddy-portal/src/branch/main/docs/FAQ.md) 与 [部署排错](https://git.iwali.top/wangchuanli/workbuddy-portal/src/branch/main/docs/DEPLOYMENT.md);
2. **不要贴真实 Cookie、secret_key 或真实用量数据**——需要复现请用 `python tools/demo_data.py` 生成的示例数据。
- type: input
id: version
attributes:
label: 版本 / 提交号
description: 例如 1.1.0,或 `git rev-parse --short HEAD` 的输出
placeholder: 1.1.0 / 118e27f
validations:
required: true
- type: dropdown
id: deploy
attributes:
label: 部署方式
options:
- Docker Compose(命名卷,推荐)
- Docker Compose + hostdir 叠加层(绑定挂载)
- 裸机 waitress
- 裸机 Flask 开发服务器
- 其他(请在补充说明里写)
validations:
required: true
- type: input
id: env
attributes:
label: 运行环境
description: 操作系统与 Python 版本
placeholder: Windows 11 + Docker Desktop 4.3x / Ubuntu 24.04 + Python 3.13
validations:
required: true
- type: textarea
id: what
attributes:
label: 现象
description: 发生了什么?预期是什么?
validations:
required: true
- type: textarea
id: repro
attributes:
label: 复现步骤
description: 越具体越好。涉及数据口径的问题请说明用的是示例数据还是真实数据。
placeholder: |
1. `docker compose up -d --build`
2. 打开 /dashboard
3. 点击「30 天」
4. 看到 …
validations:
required: true
- type: textarea
id: logs
attributes:
label: 日志 / 报错
description: |
相关日志。**请先脱敏**(抹掉 Cookie、域名、内网 IP、真实模型名)。
大屏类问题请附浏览器 Console 的报错。
render: text
validations:
required: false
- type: textarea
id: checks
attributes:
label: 已做过的自查
description: 例如是否跑过 `tools/check_live.py`、是否换过浏览器、是否重启过容器
validations:
required: false
+8
查看文件
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 使用问题 / 部署排错
url: https://git.iwali.top/wangchuanli/workbuddy-portal/src/branch/main/docs/FAQ.md
about: 常见问题、Cookie 获取、时区与调度、备份恢复等,先看 FAQ 与部署文档
- name: 安全漏洞
url: https://git.iwali.top/wangchuanli/workbuddy-portal/src/branch/main/SECURITY.md
about: 请勿公开提交可直接利用的漏洞细节,按 SECURITY.md 走私有渠道
@@ -0,0 +1,47 @@
name: 功能建议
description: 提出一个新功能或改进
title: "[Feature] "
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
在提建议前,请先看一眼 [架构说明](https://git.iwali.top/wangchuanli/workbuddy-portal/src/branch/main/docs/ARCHITECTURE.md)
里的「已知边界」——有些能力是**刻意不做**的(例如不引入 APScheduler、不新增第三方依赖)。
- type: textarea
id: problem
attributes:
label: 你想解决什么问题
description: 先说场景与痛点,再说方案。这样更容易判断有没有更简单的做法。
placeholder: 我在做 … 的时候,必须手动 … ,很费时。
validations:
required: true
- type: textarea
id: proposal
attributes:
label: 你期望的做法
description: 如果有具体的接口/页面/参数设计,写在这里
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 考虑过的替代方案
description: 以及为什么它们不够好
validations:
required: false
- type: checkboxes
id: constraints
attributes:
label: 约束自查
options:
- label: 该功能不需要新增第三方依赖(或已在下方说明理由)
required: false
- label: 该功能不破坏「SQLite 单写者」这一前提
required: false
- label: 若涉及列表类接口,我不会在其中返回 `prompt` 全文
required: false
+46
查看文件
@@ -0,0 +1,46 @@
## 这个 PR 做了什么
<!-- 一句话说清。若是修 bug,请写清根因,而不是只写「修了个 bug」。 -->
## 关联 Issue
<!-- 例如 Closes #12 -->
## 改动类型
- [ ] Bug 修复
- [ ] 新功能
- [ ] 重构(不改变外部行为)
- [ ] 文档
- [ ] 构建 / 部署 / CI
## 改动清单
<!-- 文件路径 + 做了什么,便于快速 review。例:
- `workbuddy_portal/query.py` —— bundle() 里 daily 改为全量下发
- `docs/API.md` —— 同步说明 daily 的范围语义
-->
## 验证情况
<!-- 请贴出实际输出,不要只写「已测试」。至少覆盖第 2 层。 -->
| 检查 | 结果 |
|---|---|
| `python -m compileall -q workbuddy_portal manage.py tools` | |
| `python tools/smoke.py` | `RESULT: ok=?? fail=0` |
| `python tools/check_live.py`(如起了服务) | `RESULT: ok=?? fail=0` |
| `python tools/shots.py`(如改了前端) | 无 JS 报错 |
| `docker compose up -d --build`(如改了容器相关) | 容器 healthy |
## 破坏性变更 / 需要部署方做的事
<!-- 有就写:环境变量、数据迁移、需要重建镜像、需要改反向代理配置…;没有就写「无」 -->
## 自查确认
- [ ] 没有提交任何凭据、`data/usage.sqlite`、`logs/*`、`instance.json`(已用 `git check-ignore -v` 复核过忽略规则)
- [ ] 没有在列表类接口里新增返回 `prompt` 全文
- [ ] 若涉及数据口径变更,我已用**独立聚合**与页面结果逐项比对
- [ ] 新增的第三方资源已登记到 `THIRD-PARTY-NOTICES.md`
- [ ] 文档(README / docs / CHANGELOG)已同步更新
+9
查看文件
@@ -5,18 +5,27 @@
# =============================================================================
# ---- 数据与运行产物:正本不进版本库(体积大、含凭证衍生物)----
# 同时写 data/* 与 data/**/* 两种:只写前者会漏掉子目录
# (曾因此让 data/demo/usage.sqlite 逃过忽略规则)
data/*.sqlite
data/*.sqlite-wal
data/*.sqlite-shm
data/**/*.sqlite
data/**/*.sqlite-wal
data/**/*.sqlite-shm
# 含 secret_key,泄露等于会话签名密钥外泄,绝不可提交
data/instance.json
data/**/instance.json
data/exports/*.csv
# 界面截图(tools/shots.py 生成的临时产物;手册配图在 docs/images/)
data/shots/
# 示例数据(tools/demo_data.py 生成,随时可重建,不必入库)
data/demo/
# ---- 日志 ----
logs/*
+59
查看文件
@@ -0,0 +1,59 @@
# 行为准则(Code of Conduct)
## 我们的承诺
为了营造开放、友善的协作环境,我们作为贡献者与维护者承诺:**让每个人参与本项目的体验都不受骚扰**,
无论其年龄、体型、可见或不可见的残障、族裔、性别认同与表达、经验水平、教育程度、
社会经济状况、国籍、外貌、种族、宗教,或性取向与身份认同。
## 我们的标准
**有助于营造积极环境的行为:**
- 对他人展现同理心与善意
- 尊重不同的意见、观点与经验
- 给出并优雅地接受建设性的反馈
- 承担责任、向被我们影响的人致歉,并从中学习
- 关注对整个社区最有利的事,而不只是对我们个人
**不可接受的行为:**
- 性化的言语或图像,以及任何形式的性关注或挑逗
- 挑衅、侮辱或贬损性评论,以及人身攻击或政治攻击
- 公开或私下的骚扰
- 未经明确许可,公布他人的私人信息(如真实姓名、住址、邮箱、凭据等)
- 其他在专业场合中可被合理视为不当的行为
> 与本项目技术定位直接相关的一条:**请勿在 Issue、PR、讨论或截图中提交真实的账号凭据、
> 云端 Cookie、`secret_key` 或真实用户的用量数据**。需要复现时请用 `tools/demo_data.py`
> 生成的示例数据,或自行脱敏。这类内容会被立即删除。
## 维护者的责任
维护者负责澄清并执行上述标准,对任何被视为不当、威胁、冒犯或有害的行为,
有权采取适当且公平的纠正措施,包括删除、编辑或拒绝评论、提交、代码与 Issue,
并在必要时临时或永久禁止任何贡献者参与。
## 适用范围
本准则适用于所有项目空间,也适用于个人在**公开场合代表本项目**时的言行。
## 报告与执行
如遇滥用、骚扰或其他不可接受的行为,请通过维护者在代码托管平台上公布的联系方式私下报告
(参见 [SECURITY.md](SECURITY.md) 中的私有渠道)。所有投诉都会被及时、公正地审查与处理。
维护者有义务尊重报告者的隐私与安全。
## 执行准则
维护者将按下述梯度决定后果:
1. **更正** — 私下书面警告,说明违规性质并解释为何不当。必要时要求公开致歉。
2. **警告** — 在一段时间内禁止与相关人员互动;违反将导致临时或永久封禁。
3. **临时封禁** — 在指定期限内禁止任何形式的公开或私下互动。
4. **永久封禁** — 永久禁止在项目内进行任何形式的公开互动。
## 致谢
本准则改编自 [Contributor Covenant](https://www.contributor-covenant.org/) 2.1 版
(原文以 CC BY 4.0 发布)。执行准则参考其「Enforcement Guidelines」部分。
+112
查看文件
@@ -0,0 +1,112 @@
# 贡献指南(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(只有跑界面截图才需要) |
起步:
```bash
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、偏斜的积分分布),
放在 `data/demo/` 下(该目录已在 `.gitignore` 内):
```bash
python tools/demo_data.py # 默认 data/demo,管理员 admin/admin123
WB_DATA_DIR=$PWD/data/demo python manage.py serve --port 8849 --no-scheduler
```
这样你既能有一个「看起来像真的」的界面来调试,也不会把任何真实用量带进仓库或截图。
## 三、改动前请先跑一遍验证
项目有一层层递进的验证,**代价从低到高**,改完至少跑到第 2 层:
| # | 命令 | 覆盖什么 | 需要什么 |
|---|---|---|---|
| 1 | `python -m compileall -q workbuddy_portal manage.py tools` | 语法 | — |
| 2 | `python tools/smoke.py` | **99 项**离线断言:全页面只读渲染、模板残留检测、历史缺陷防回归、静态资源、CSS 类名对账 | 无(用 Flask test_client,不启服务) |
| 3 | `python tools/check_live.py --base http://127.0.0.1:8848` | **56 项**真实 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` 都放过了)。改前端务必跑。
这三支脚本都会打印 `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](docs/ARCHITECTURE.md))。
6. **`.gitignore` 不支持行尾注释**——规则后跟 `# 注释` 会让整行失效。注释必须单独占一行,
改完用 `git check-ignore -v <file>` 逐条确认命中。
## 五、代码风格
- 遵循 PEP 8;行宽 100。
- **注释与文档字符串用中文**,说明「为什么这么做」而不是「这行在做什么」。
- 提交前用 `python -m compileall` 与 `python tools/smoke.py` 自查。
- 不要引入新的第三方依赖,除非有充分理由并在 PR 里说明——这个项目的卖点之一就是依赖少。
如果确实新增了,请同步登记到 [THIRD-PARTY-NOTICES.md](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 里问,比写完再返工更省事。
+21
查看文件
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Wang Chuanli
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+46 -1
查看文件
@@ -14,9 +14,10 @@
| 部署 | Docker Compose / 裸机 `waitress`;镜像可推 Gitea 容器注册表 |
| 鉴权 | 全站登录 + CSRF + 角色(管理员 / 普通用户),凭证存库、页面只回掩码 |
| 版本 | v1.1.0 |
| **许可证** | **MIT**(第三方组件与再分发资源见 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)) |
**目录**:[核心特性](#核心特性) · [架构](#架构一图) · [快速开始](#快速开始) · [命令一览](#命令一览) ·
[页面一览](#页面一览) · [接口一览](#接口一览) · [文档导航](#文档导航) · [安全须知](#安全须知)
[页面一览](#页面一览) · [接口一览](#接口一览) · [文档导航](#文档导航) · [安全须知](#安全须知) · [开源与许可](#开源与许可)
---
@@ -245,6 +246,11 @@ workbuddy-portal/
| [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 许可证全文 |
---
@@ -259,3 +265,42 @@ workbuddy-portal/
- **开放重定向防护**:登录跳转的 `next` 只接受站内相对路径,`//evil.com` 这类协议相对 URL 一律回落到 `/`。
- **登录限速**:同 IP 连续失败 5 次锁定 10 分钟;失败计数表有上限与 TTL。
- **不进版本库的文件**:`data/instance.json`(含 `secret_key`)、`data/usage.sqlite`、`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` 全文、两种字段命名契约不要互相「统一」……),
以及从 `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 为通用示例文本,
审计 IP 取自 RFC 5737 的文档专用网段(`192.0.2.0/24`)。
生成方式是 `tools/demo_data.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/)
+67
查看文件
@@ -0,0 +1,67 @@
# 安全策略(Security Policy)
## 支持范围
本项目按「自托管、局域网内使用」的定位开发。安全修复只针对当前主分支与最新发布版本。
| 版本 | 是否接受安全修复 |
|---|---|
| `1.1.x`(当前) | ✅ |
| `< 1.1` | ❌ 请先升级 |
## 如何报告漏洞
**请不要在公开 Issue 里贴出可直接利用的细节**(含真实 Cookie、`secret_key`、可复现的绕过步骤)。
请通过以下任一私有渠道联系维护者:
<!-- TODO(维护者):首次公开发布前,把下面这行替换为真实可达的安全联系邮箱 -->
- 邮件:`<安全联系邮箱>`(占位,待维护者补全)
- 或通过代码托管平台(Gitea)的站内私信联系仓库管理员
请在报告里尽量包含:
1. 受影响的版本 / 提交号
2. 复现步骤与最小复现(可脱敏)
3. 影响范围(能读到什么、能改到什么)
4. 如果有,你建议的修复方向
我们会在 **7 天内**确认收到,并在修复发布后于 CHANGELOG 里致谢(除非你希望匿名)。
## 设计上已有的安全措施
理解这些边界,有助于你判断某个现象是「设计如此」还是「真的漏洞」:
| 项 | 做法 | 位置 |
|---|---|---|
| 全站鉴权 | 每个页面都有 `@login_required`,每个 `/api/*` 未登录返回 401 JSON | `workbuddy_portal/security.py`、`web/views.py` |
| CSRF | 所有写请求必须带 `X-CSRF-Token`,页面注入 `window.WB_CSRF`,服务端统一拦截 | `security.check_csrf` |
| 会话签名 | Flask `secret_key` 由 `data/instance.json` 持有,首次启动随机生成 | `workbuddy_portal/config.py` |
| 口令存储 | 加盐哈希,不存明文 | `security.hash_password` |
| 凭据脱敏 | 云端 **Cookie 是账号凭证**:入库后页面与接口**都不回传全文**,只给「N 字符,结尾 …xxxx」 | `config.SECRET_KEYS`、`web/views.py` |
| 开放重定向 | 登录后的 `next` 只允许站内相对路径 | `web/views.py` |
| TLS 校验 | 默认开启,**不提供「关掉校验」的快捷开关**(Cookie 不该裸奔) | `settings.ssl_verify` |
| 容器权限 | 运行层非 root(uid/gid 1000 `app`) | `Dockerfile` |
**绝不入库**:`data/instance.json`(含 `secret_key`)、`data/usage.sqlite`、`logs/*`、`.env`。
`.gitignore` 已覆盖;改动忽略规则后请用 `git check-ignore -v <file>` 逐条复核。
## 已知的**非**目标(部署方需自行处理)
本项目刻意不做下面这些,请按你的环境补齐:
- **没有多租户与细粒度权限**:登录用户即为管理员,能改配置、能看全部数据。
- **没有限流与账号锁定**:公网暴露前请置于反向代理的 rate limit 之后。
- **没有强制 HTTPS**:请由反向代理(nginx/Caddy)终止 TLS。
- **没有备份机制**:备份策略需要你自己定(见 `docs/DEPLOYMENT.md`)。
- **不建议直接暴露到公网**:设计前提是局域网或 VPN 内使用。
- **Cookie 的获取方式由使用者负责**:手动从浏览器复制。请勿把该 Cookie 分享给他人,
它的权限等同于你的账号;轮换后记得在「配置管理」页更新。
## 部署前的最小检查清单
- [ ] 已修改默认管理员口令(`WB_ADMIN_PASSWORD`),不再是 `admin123`
- [ ] `data/` 与 `logs/` 目录的权限只对服务账号可读写
- [ ] 前面有反向代理并启用了 HTTPS
- [ ] 确认 `data/instance.json` 没有被提交到任何仓库
- [ ] 已规划备份(SQLite 库是唯一正本)
+78
查看文件
@@ -0,0 +1,78 @@
# 第三方组件与许可声明(Third-Party Notices)
本项目(WorkBuddy Portal)自身以 [MIT 许可证](LICENSE) 发布。
但它**依赖**、并在个别位置**再分发**了若干第三方组件。这些组件的著作权归各自作者所有,
其许可条款独立于本项目的 MIT 条款。本文件汇总这些依赖,供合规审查与二次分发时参考。
---
## 一、运行期依赖(`requirements.txt`)
这些包不在本仓库内,由使用方安装时获取。
| 组件 | 版本要求 | 许可证 | 用途 |
|---|---|---|---|
| [Flask](https://flask.palletsprojects.com/) | `>=3.0` | BSD-3-Clause | Web 框架(路由、Jinja 模板、会话) |
| [waitress](https://github.com/Pylons/waitress) | `>=3.0` | ZPL-2.1 | 生产级纯 Python WSGI 服务器 |
| [openpyxl](https://openpyxl.readthedocs.io/) | `>=3.1` | MIT | 仅 `manage.py import-xlsx` 读 Excel |
Python 标准库(`sqlite3`、`urllib`、`http`、`threading` 等)按 PSF-2.0 许可,随 Python 分发。
> 项目**刻意不依赖** `APScheduler`(调度自实现)与 `requests`(用标准库 `urllib`),
> 因此这两者的许可证与本项目无关。
## 二、随仓库再分发的第三方资源
这是需要特别注意的一类:文件**物理存在于本仓库中**。
### Apache ECharts 5.6.0 — Apache License 2.0
- **位置**:`workbuddy_portal/web/static/dashboard/vendor/echarts.min.js`
- **著作权**:Copyright © 2017-2025 Apache Software Foundation 及 ECharts 贡献者
- **许可证**:Apache License, Version 2.0(全文见 <https://www.apache.org/licenses/LICENSE-2.0>)
- **为何内置**:用量大屏要在局域网内离线可用,不能依赖公网 CDN
- **未修改**:文件按官方发行版原样保留,其头部已包含 Apache 许可证声明与版权信息
按 Apache-2.0 第 4 条要求,再分发时需保留许可证与版权声明——该 `.min.js` 文件头部已自带,
本声明构成附加的显著声明。
> **替换说明**:如需升级,从 <https://echarts.apache.org/> 下载对应版本覆盖同名文件即可,
> 大屏页通过 `/static/dashboard/vendor/echarts.min.js` 引用,无需改代码。
### 项目自有资源
以下文件由本项目创作,同样按 MIT 发布,**不属第三方**:
- `workbuddy_portal/web/static/favicon.svg`
- `workbuddy_portal/web/static/css/app.css`
- `workbuddy_portal/web/static/js/app.js`
- `workbuddy_portal/web/static/dashboard/index.html`
- `docs/images/*.png`(界面截图,**使用合成示例数据**渲染,见下节)
## 三、开发期工具(非运行依赖)
| 组件 | 许可证 | 用途 |
|---|---|---|
| [Playwright for Python](https://playwright.dev/python/) | Apache-2.0 | `tools/shots.py` 登录后逐页截图 |
| [Pillow](https://python-pillow.org/) | MIT-CMU | 人工压缩文档配图时使用,未入库 |
这些工具**不会**被打进 Docker 运行镜像的依赖里,也不影响部署方的义务。
## 四、文档与截图中的数据
`docs/images/` 下的界面截图与 `docs/` 中的 JSON 示例**全部使用合成数据**,
由 `tools/demo_data.py` 生成:模型名统一为 `demo-*`,客户端为 `vscode`/`webconsole`/`sdk`,
Prompt 为通用示例文本,审计 IP 取自 RFC 5737 的文档专用网段(`192.0.2.0/24`)。
**不含任何真实账号、真实用量或第三方受版权保护的内容。**
---
## 五、合规自查清单
二次分发或商用前,建议逐项确认:
- [ ] `LICENSE` 与本文档随发行物一并提供
- [ ] `vendor/echarts.min.js` 的头部许可证声明未被剥离或压缩掉
- [ ] 若替换了 ECharts,同步更新本文档中的版本号
- [ ] 若新增了第三方文件到仓库,在此登记其许可证
- [ ] 若将本项目的界面截图用于宣传,确认其中不含真实业务数据
+3
查看文件
@@ -1,4 +1,7 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
# =============================================================================
# workbuddy-portal 容器入口
# 1) 幂等初始化数据库(建表 + 默认配置 + 首个管理员)
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""容器健康检查。
只用标准库:slim 镜像里没有 curl。`/login` 是唯一免登录页面,拿到 200 即认为
+21 -21
查看文件
@@ -47,16 +47,16 @@
"producer": "workbuddy-portal(Flask + SQLite)",
"note": "...",
"totals": {
"records": 1665, "credits": 8513.36, "calls": 1086,
"freeCalls": 579, "billableCalls": 507,
"models": 12, "clients": 3,
"first": "2026-08-10 00:00:00", "last": "2026-09-14 14:51:00",
"topCredits": 319.5
"records": 944, "credits": 4961.63, "calls": 944,
"freeCalls": 328, "billableCalls": 616,
"models": 7, "clients": 3,
"first": "2026-08-16 09:12:00", "last": "2026-09-14 15:52:00",
"topCredits": 412.8
},
"months": ["2026-08", "2026-09"],
"sources": [{ "path": "usage.sqlite", "role": "primary", "count": 1665, "bytes": 1234567 }],
"sources": [{ "path": "usage.sqlite", "role": "primary", "count": 944, "bytes": 434176 }],
"focusDay": "2026-09-14",
"health": { "cookie": true, "lastRunAt": "2026-09-14 14:51:28", "lastRunStatus": "ok" }
"health": { "cookie": true, "lastRunAt": "2026-09-14 15:52:10", "lastRunStatus": "ok" }
}
```
@@ -73,14 +73,14 @@
{
"manifest": { ... 同上 ... },
"daily": [
{ "d": "2026-09-08", "c": 1423.5, "k": 88, "fc": 40, "bc": 48,
"m": { "deepseek-v4-flash": 800.2, "glm-5.3-flash": 623.3 },
{ "d": "2026-09-08", "c": 168.4, "k": 31, "fc": 12, "bc": 19,
"m": { "demo-flash": 62.1, "demo-pro": 41.7 },
"h": [0,0,0,0,0,0,0,0,0,12.5, ...] }
],
"dims": { "model": [...], "client": [...], "hour": [...] },
"top": [ { "id": "...", "c": 319.5, "m": "kimi-k3-1", "cl": "VSCode", "t": "2026-09-12 15:04:00", "px": "摘要…" } ],
"top": [ { "id": "...", "c": 412.8, "m": "demo-reason", "cl": "vscode", "t": "2026-09-12 15:04:00", "px": "摘要…" } ],
"records": [ { "id": "...", "c": 5.78, "m": "...", "cl": "...", "t": "...", "px": "..." } ],
"recordsTotal": 1665,
"recordsTotal": 944,
"recordsCap": 20000,
"recordsTruncated": false,
"totals": { ... },
@@ -106,13 +106,13 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
```json
{
"records": 428, "credits": 2145.6, "calls": 300,
"freeCalls": 120, "billableCalls": 180,
"records": 226, "credits": 1180.4, "calls": 226,
"freeCalls": 78, "billableCalls": 148,
"firstDay": "2026-09-08", "lastDay": "2026-09-14", "days": 7,
"models": 9, "clients": 2,
"models": 7, "clients": 3,
"first": "...", "last": "...",
"window": { "from": "2026-09-08", "to": "2026-09-14", "days": 7 },
"avgPerCall": 7.15,
"avgPerCall": 5.22,
"prev": { ... 上一段等长窗口的同样结构 ... },
"delta": { "credits": 12.3, "calls": -4.1, "window": { "from": "...", "to": "..." } },
"partial": { "date": "2026-09-14", "hhmm": "14:52" }
@@ -132,7 +132,7 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
| `from` / `to` | 筛选窗口 |
```json
{ "days": [ { "d": "2026-09-08", "c": 1423.5, "k": 88, "fc": 40, "bc": 48,
{ "days": [ { "d": "2026-09-08", "c": 168.4, "k": 31, "fc": 12, "bc": 19,
"m": {...}, "h": [24 个元素] } ] }
```
@@ -146,8 +146,8 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
```json
{
"model": [ { "name": "deepseek-v4-flash", "credits": 3784.86, "calls": 234, "avg": 16.17, "free": 0 } ],
"client": [ { "name": "VSCode", ... } ],
"model": [ { "name": "demo-flash", "credits": 1286.4, "calls": 252, "avg": 5.1, "free": 0 } ],
"client": [ { "name": "vscode", ... } ],
"hour": [ { "name": "14", ... } ]
}
```
@@ -162,7 +162,7 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
| `n` | 50 | 返回条数(1~1000) |
```json
[ { "id": "…", "c": 319.5, "m": "kimi-k3-1", "cl": "VSCode",
[ { "id": "…", "c": 412.8, "m": "demo-reason", "cl": "vscode",
"t": "2026-09-12 15:04:00", "px": "截断后的 Prompt 摘要" } ]
```
@@ -184,7 +184,7 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
{
"items": [ { "request_id": "…", "credits": 5.78, "model": "…",
"client": "…", "ts": "2026-09-14 14:20:00", "prompt": "…" } ],
"total": 1665, "page": 1, "size": 50, "pages": 34,
"total": 944, "page": 1, "size": 50, "pages": 19,
"window": { "from": "...", "to": "..." }
}
```
@@ -292,7 +292,7 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
| 场景 | 响应 |
|---|---|
| 成功 | `200 {"ok": true, "result": {"message": "新增 11 条,重复 6 条,存档共 1665 条", ...}}` |
| 成功 | `200 {"ok": true, "result": {"message": "新增 11 条,重复 6 条,存档共 944 条", ...}}` |
| 已有采集在跑 | `409 {"ok": false, "error": "busy", "message": "..."}` |
| Cookie 失效 | `401 {"ok": false, "error": "cookie_expired", "message": "..."}` |
| 云端异常 | `502 {"ok": false, "error": "api", "message": "..."}` |
+16 -1
查看文件
@@ -22,16 +22,31 @@
- **容器环境变量**:`WB_HOST` `WB_PORT` `WB_DATA_DIR` `WB_LOG_DIR` `WB_DB`
`WB_ADMIN_USER` `WB_ADMIN_PASSWORD` `WB_DISABLE_SCHEDULER` `WB_IMPORT_CREDS` `WB_IMPORT_XLSX`
- **文档体系** `docs/`:
[用户使用手册](USER-GUIDE.md)(含 9 张界面截图)、
[用户使用手册](USER-GUIDE.md)(含 9 张界面截图,**全部用合成示例数据渲染**)、
[部署与运维指南](DEPLOYMENT.md)(含推镜像到 Gitea 注册表的完整流程)、
[架构与设计说明](ARCHITECTURE.md)、
[接口参考](API.md)、
[常见问题](FAQ.md)
- **开源声明体系**(仓库根目录):[LICENSE](../LICENSE)(MIT)、
[THIRD-PARTY-NOTICES.md](../THIRD-PARTY-NOTICES.md)(依赖清单与再分发合规自查)、
[CONTRIBUTING.md](../CONTRIBUTING.md)(含「必须遵守的不变量」与五层自检方法)、
[SECURITY.md](../SECURITY.md)、[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)、
`.github/` 下的 Issue 表单与 PR 模板、`.editorconfig`,
以及给全部 Python / Shell 源文件加 `SPDX-License-Identifier: MIT` 头
- **`tools/demo_data.py`**:生成**完全合成**的示例库(假模型名 / 假 Prompt / 偏斜的积分分布),
写入 `data/demo/`(已在 `.gitignore` 内)。文档截图与本地调试都基于它,
任何人不需要真实账号就能复现整套界面
- **`.gitattributes`**:强制 `*.sh` / `Dockerfile` / 各类源码为 LF
(带 CRLF 的 `.sh` 在容器里会报 `exec format error`,极难定位)
### 变更
- **文档数据脱敏**:9 张界面截图全部改用合成示例数据重拍;
`docs/API.md`、`docs/DEPLOYMENT.md` 示例响应里的真实模型名与真实统计数字一并替换为示例口径。
此前截图中含**真实 Prompt 全文**、本机路径与本机用户名,属于不该公开的内容
- **`.gitignore` 补强**:只写 `data/*.sqlite` 会漏掉子目录,改为同时保留 `data/**/*.sqlite`
等规则,并新增 `data/demo/` 忽略——否则 `tools/demo_data.py` 的产物会被误提交
- **项目定名**:`wb_usage_portal` → **`workbuddy-portal`**;
Python 包 `wb_usage` → **`workbuddy_portal`**;会话 cookie
`wb_usage_sid` → `workbuddy_portal_sid`(升级后需要重新登录)
+2 -2
查看文件
@@ -544,10 +544,10 @@ sqlite3.OperationalError: unable to open database file
docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
docker compose exec portal python -c \
"import sqlite3;print(sqlite3.connect('/app/data/usage.sqlite').execute('select count(*) from usage_records').fetchone())"
# -> (1665,) 容器侧正常
# -> (944,) 容器侧正常
python manage.py stats # 宿主侧随手跑一次「纯读」的 CLI
# -> 存档:1665 条 …
# -> 存档:944 条 …
docker compose exec portal python -c \
"import sqlite3;sqlite3.connect('/app/data/usage.sqlite')"
+5
查看文件
@@ -3,6 +3,11 @@
> 面向**使用者**(不是开发者)。读完这份就能独立完成日常操作:
> 登录 → 看用量 → 配置采集 → 查明细 → 导数据 → 处理常见异常。
> **关于配图**:本文所有截图都用 `tools/demo_data.py` 生成的**合成示例数据**渲染
> ——模型名统一是 `demo-*`,客户端为 `vscode`/`webconsole`/`sdk`,Prompt 是通用示例文本。
> 所以你可以照着重现出几乎一样的界面,也不必担心文档里夹带真实账号信息。
> 想自己搭一份这样的环境:`python tools/demo_data.py` 然后按输出的提示起服务即可。
**目录**
- [一、这个系统是做什么的](#一这个系统是做什么的)
二进制文件未显示。

之前

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

之后

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

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

之前

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

之后

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

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

之前

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

之后

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

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

之前

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

之后

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

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

之前

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

之后

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

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

之前

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

之后

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

二进制文件未显示。

之前

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

之后

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

二进制文件未显示。

之前

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

之后

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

+3
查看文件
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""WorkBuddy Portal —— 统一命令行入口。
采集 / 存储 / 呈现三件事都由本项目承担,不再依赖外部计划任务或自动化。
+4 -1
查看文件
@@ -1,5 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""端到端验收:对**运行中的**服务发真实 HTTP 请求,走完整登录/CSRF/API 链路。
与 tests 里用 Flask test_client 的冒烟测试互补——这里验证的是「真的起起来了、
@@ -7,7 +10,7 @@
用法:
python tools/check_live.py # 默认 http://127.0.0.1:8848
python tools/check_live.py --base http://10.0.0.5:8848
python tools/check_live.py --base http://192.168.1.50:8848 # 换成你的部署主机
python tools/check_live.py -u admin -p 你的密码
python tools/check_live.py --from 2026-09-08 --to 2026-09-14
+287
查看文件
@@ -0,0 +1,287 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""生成**脱敏示例数据**,用于本地体验、界面截图与文档配图。
为什么需要它
------------
`docs/images/` 里的界面截图必须是可公开的,但真实库里的 Prompt 全文、
请求 ID、用量分布与本机路径都属于私有信息。与其手工打码,不如用一份
**完全合成**的数据集重新截图——顺便也让后来者能一键把界面跑起来看。
生成内容
--------
| 表 | 说明 |
|---|---|
| `usage_records` | 约 900 条合成记录,跨 30 天,含假模型名 / 假 Prompt / 偏斜的积分分布 |
| `collect_runs` | 14 条采集历史,含 ok / warn / error 三种状态 |
| `settings` | 走项目默认值(`config.DEFAULTS`),**不写入任何凭据** |
| `audit_log` | 30 条操作审计 |
| `users` | 由 `db.init_db()` 建一个管理员 |
用法
----
# 默认写到 data/demo/(该目录在 .gitignore 内,不会误提交)
python tools/demo_data.py
# 指定目录与管理员口令,然后起服务看效果
python tools/demo_data.py --out data/demo --admin-password demo123
WB_DATA_DIR=$PWD/data/demo python manage.py serve --port 8849 --no-scheduler
注意
----
本脚本**只写 `--out` 指定的目录**,不会读取也不会修改 `data/usage.sqlite`。
已存在的目标库会被拒绝覆盖,除非显式加 `--force`。
"""
from __future__ import annotations
import argparse
import os
import random
import sys
from datetime import datetime, timedelta
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE)
# ---- 合成素材:刻意保持通用,不含任何真实的产品名、Prompt 或业务信息 ----
MODELS = [
# (模型名, 权重, 积分中位数)
("demo-flash", 26, 0.35),
("demo-lite", 20, 0.60),
("demo-pro", 18, 2.20),
("demo-reason", 14, 4.80),
("demo-mini", 12, 0.22),
("demo-vision", 7, 6.50),
("demo-nano", 3, 0.12),
]
CLIENTS = [("vscode", 74), ("webconsole", 18), ("sdk", 8)]
# 写进 settings 的假 Cookie。刻意用重复串,一眼就能看出不是真凭据;
# 作用只是让概览页的健康指示灯是绿的(首装状态是「缺 Cookie」告警)。
DEMO_COOKIE = "wb_demo_session=" + "deadbeef" * 15
PROMPTS_SHORT = [
"帮我解释一下这段代码的作用",
"把这段 SQL 优化一下,避免全表扫描",
"写一个 Python 脚本,把目录里的 CSV 批量转成 JSON",
"这个报错是什么意思:connection refused",
"帮我 review 一下这个接口设计,有什么问题",
"解释一下 JWT 和 Session 的区别",
"生成一份周报模板",
"把下面的需求整理成技术方案",
"这个正则怎么写:匹配 11 位手机号",
"Explain the difference between processes and threads",
"Refactor this function to be more readable",
"What is the time complexity of this algorithm?",
"Write unit tests for this module",
"How do I make this loop faster?",
"Summarize the key points of this document",
]
PROMPTS_LONG = [
"你是资深后端工程师。请审查下面的接口实现,重点看:\n"
"1) 并发写入是否安全;\n2) 异常分支是否都有兜底;\n3) 有没有可以合并的重复查询。\n"
"请按「问题 / 影响 / 建议」三栏输出。",
"下面的表结构要支持按天和按模型两个维度聚合,数据量大约千万级。\n"
"请给出索引设计,并说明每个索引命中的查询模式。",
"把这段代码从回调风格改成 async/await,保持对外行为不变,"
"并补充必要的错误处理。改完给出前后对比。",
"Review the provided module and suggest improvements.\n"
"Focus on readability, error handling, and testability.\n"
"Reply as a short bullet list.",
]
def _weighted(rng: random.Random, pairs):
"""按权重取一项(pairs 为 (值, 权重) 列表)。"""
total = sum(w for _, w in pairs)
pick = rng.uniform(0, total)
acc = 0.0
for val, w in pairs:
acc += w
if pick <= acc:
return val
return pairs[-1][0]
def _credits(rng: random.Random, median: float) -> float:
"""对数正态分布的积分值,偶尔出现大额。"""
val = rng.lognormvariate(0.0, 0.85) * median
if rng.random() < 0.02: # 2% 的大额长任务
val *= rng.uniform(12, 60)
return round(min(val, 900.0), 2)
def _prompt(rng: random.Random) -> str | None:
r = rng.random()
if r < 0.12: # 一部分请求不带 Prompt
return None
if r < 0.55:
return rng.choice(PROMPTS_SHORT)
if r < 0.75:
return rng.choice(PROMPTS_LONG)
# 其余用短句拼接,避免重复得过于整齐
return rng.choice(PROMPTS_SHORT) + ":" + rng.choice(PROMPTS_SHORT)
def build(out_dir: str, days: int, seed: int, admin_password: str,
admin_user: str) -> str:
"""建库并写入合成数据,返回数据库文件路径。"""
out_dir = os.path.abspath(out_dir)
os.makedirs(out_dir, exist_ok=True)
db_file = os.path.join(out_dir, "usage.sqlite")
# 必须在 import 项目模块之前设好环境变量:config 在导入时读取它们。
# 日志目录用 setdefault —— 容器里 WB_LOG_DIR 已由镜像 ENV 指定,
# 不该在数据卷下再凭空建一个 logs/。
os.environ["WB_DATA_DIR"] = out_dir
os.environ.setdefault("WB_LOG_DIR", os.path.join(out_dir, "logs"))
os.environ["WB_DB"] = db_file
from workbuddy_portal import db # noqa: E402
conn = db.connect()
try:
db.init_db(conn, create_admin=True, admin_user=admin_user,
admin_password=admin_password)
# 写入一个**明显是假值**的 Cookie:让概览页的健康状态显示为「正常」
# 而不是首装的「缺 Cookie」告警——演示与截图应当呈现「配置完成」后的样子。
# 这个值不会被任何真实服务接受,也不含任何真实凭据。
db.set_setting(conn, "cookie", DEMO_COOKIE)
rng = random.Random(seed)
now = datetime.now().replace(second=0, microsecond=0)
today0 = now.replace(hour=0, minute=0, second=0)
model_pairs = [(m, w) for m, w, _ in MODELS]
medians = {m: md for m, _, md in MODELS}
client_pairs = list(CLIENTS)
# ---------- usage_records ----------
rows = []
for d in range(days - 1, -1, -1):
day0 = today0 - timedelta(days=d)
for _ in range(rng.randint(14, 46)):
# 工作时间加权:9-19 点更密
hour = _weighted(rng, [(h, 6 if 9 <= h <= 19 else 1) for h in range(24)])
minute = rng.randint(0, 59)
sec = rng.randint(0, 59)
ts = day0 + timedelta(hours=hour, minutes=minute, seconds=sec)
if ts > now:
continue
model = _weighted(rng, model_pairs)
client = _weighted(rng, client_pairs)
rid = "req-%s" % "".join(rng.choice("0123456789abcdef") for _ in range(16))
stamp = ts.strftime("%Y-%m-%d %H:%M:%S")
rows.append((
rid, stamp, ts.strftime("%Y-%m-%d"), hour, model, client,
_credits(rng, medians[model]), _prompt(rng),
stamp, stamp, stamp,
))
conn.execute("BEGIN")
conn.executemany(
"INSERT OR REPLACE INTO usage_records"
"(request_id,ts,day,hour,model,client,credits,prompt,"
" first_seen,last_seen,cloud_ts) VALUES(?,?,?,?,?,?,?,?,?,?,?)", rows)
conn.execute("COMMIT")
# ---------- collect_runs ----------
runs = []
total = 0
for i in range(14, 0, -1):
started = now - timedelta(minutes=i * 37 + rng.randint(0, 9))
fetched = rng.randint(3, 22)
dup = rng.randint(0, max(1, fetched - 2))
added = max(0, fetched - dup)
total += added
status = "ok"
msg = "新增 %d 条,重复 %d 条,存档共 %d 条" % (added, dup, total)
exit_code = 0
if i == 9:
status, exit_code = "warn", 1
msg = "参数错误:from 不是合法日期;abc(正确写法 2026-09-01)"
if i == 5:
status, exit_code = "error", 2
msg = "云端返回 401 Unauthorized:Cookie 可能已过期,请重新粘贴"
fetched = dup = added = 0
trigger = "schedule" if i % 3 else "manual"
runs.append((
trigger, status, started.strftime("%Y-%m-%d %H:%M:%S"),
(started + timedelta(milliseconds=rng.randint(180, 1400))
).strftime("%Y-%m-%d %H:%M:%S"),
rng.randint(180, 1400),
(started - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S"),
started.strftime("%Y-%m-%d %H:%M:%S"),
fetched, added, dup, total, 0, exit_code, msg,
"[%s] %s" % (status, msg),
))
conn.execute("BEGIN")
conn.executemany(
"INSERT INTO collect_runs(trigger,status,started_at,finished_at,duration_ms,"
"win_from,win_to,fetched,added,dup,total,conflicts,exit_code,message,detail)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", runs)
conn.execute("COMMIT")
# ---------- audit_log ----------
audits = []
# IP 用 RFC 5737 的文档专用网段(TEST-NET-1),
# 保证示例里出现的地址永远不可能是真实主机
actions = [
("login", "登录成功", "192.0.2.10"),
("login", "登录成功", "192.0.2.10"),
("settings", "修改:调度时刻 09:00,17:00", "192.0.2.10"),
("settings", "修改:分页大小 200", "192.0.2.10"),
("maintenance.count", "存档当前 %d 条记录" % total, "192.0.2.10"),
("settings_rejected", "参数错误:page_size 必须是数字(条/页)", "192.0.2.10"),
]
for i in range(30):
act, detail, ip = actions[i % len(actions)]
at = now - timedelta(hours=i * 3 + rng.randint(0, 2))
audits.append((at.strftime("%Y-%m-%d %H:%M:%S"), admin_user, act, detail, ip))
conn.execute("BEGIN")
conn.executemany(
"INSERT INTO audit_log(at,actor,action,detail,ip) VALUES(?,?,?,?,?)", audits)
conn.execute("COMMIT")
# 统计一下,便于打印
n = conn.execute("SELECT COUNT(*) FROM usage_records").fetchone()[0]
c = conn.execute("SELECT ROUND(SUM(credits),2) FROM usage_records").fetchone()[0]
d = conn.execute("SELECT COUNT(DISTINCT day) FROM usage_records").fetchone()[0]
print("示例库:%s" % db_file)
print(" 记录 %d 条 / 积分 %s / 覆盖 %d 天" % (n, c, d))
print(" 管理员 %s,口令 %s" % (admin_user, admin_password))
print(" 该目录在 .gitignore 内,不会被提交")
finally:
conn.close()
return db_file
def main() -> int:
ap = argparse.ArgumentParser(description="生成脱敏示例数据(完全合成,不碰真实库)")
ap.add_argument("--out", default=os.path.join(BASE, "data", "demo"),
help="输出目录,默认 data/demo")
ap.add_argument("--days", type=int, default=30, help="覆盖天数,默认 30")
ap.add_argument("--seed", type=int, default=20260914, help="随机种子,保证可复现")
ap.add_argument("--admin-user", default="admin")
ap.add_argument("--admin-password", default="admin123",
help="示例管理员口令,默认 admin123(仅供本地演示)")
ap.add_argument("--force", action="store_true", help="目标库已存在时覆盖")
a = ap.parse_args()
db_file = os.path.join(os.path.abspath(a.out), "usage.sqlite")
if os.path.exists(db_file) and not a.force:
print("目标库已存在:%s" % db_file)
print("如需重建请加 --force")
return 1
build(a.out, a.days, a.seed, a.admin_password, a.admin_user)
return 0
if __name__ == "__main__":
sys.exit(main())
+3
查看文件
@@ -1,4 +1,7 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
# =============================================================================
# 用 Gitea Access Token 一次性完成:推代码 + 推镜像
#
+3
查看文件
@@ -1,5 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""界面实检:登录后逐页截图,用来目视确认「统一美化」是否真的落地。
用法:
+3
查看文件
@@ -1,5 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""离线回归:用 Flask test_client 对**真实库**做全页面只读渲染 + 缺陷防回归断言。
与 tools/check_live.py 的分工:
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""WorkBuddy Portal —— 独立 Flask 项目。
一个程序管全部:
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""云端用量接口客户端(纯 urllib,不依赖 requests/浏览器)。
接口:POST {api_base}/billing/meter/get-user-request-usage
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""采集主流程:云端增量 -> SQLite(去重、断点、漂移校验、运行记录)。
与原 fetch_usage.py 的差别:
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""基础配置。
刻意保持「薄」:凡是运行期要改的东西(cookie、调度周期、采集参数)都放数据库
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""SQLite 访问层。
并发约定(重要):
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""SQL 聚合层:所有统计都在 SQLite 里算完再出去,页面不再搬运全量明细。
返回结构刻意与旧版 dashboard/data/*.json 的字段保持一致(d/c/k/fc/bc/m/h、
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""进程内调度器(手写,不依赖 APScheduler)。
为什么不引 APScheduler:
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""密码哈希、登录装饰器、CSRF。
局域网可访问 ⇒ 必须有鉴权。这里用 Werkzeug 自带的 PBKDF2,不引第三方依赖。
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""Web 层包:蓝图注册。"""
from . import api, views
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""JSON API —— ECharts 大屏与后台页面的数据入口。
约定:
+3
查看文件
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Wang Chuanli
"""页面路由(Jinja 模板)。
分工: