比较提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
23799b4ea5 | ||
|
|
9469a61bbc | ||
|
|
342da56d4d |
@@ -9,9 +9,15 @@ data/
|
|||||||
logs/
|
logs/
|
||||||
|
|
||||||
# Python 缓存
|
# Python 缓存
|
||||||
|
# 注意:`__pycache__/` 只能匹配上下文**根目录**下的同名目录,
|
||||||
|
# 嵌套的(如 tools/__pycache__)必须用 `**/` 前缀——否则会被原样打进镜像。
|
||||||
|
# 实测过:只写单条时,镜像里仍有 /app/tools/__pycache__。
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
**/__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
**/*.py[cod]
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
**/*.egg-info/
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)已同步更新
|
||||||
@@ -5,18 +5,27 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# ---- 数据与运行产物:正本不进版本库(体积大、含凭证衍生物)----
|
# ---- 数据与运行产物:正本不进版本库(体积大、含凭证衍生物)----
|
||||||
|
# 同时写 data/* 与 data/**/* 两种:只写前者会漏掉子目录
|
||||||
|
# (曾因此让 data/demo/usage.sqlite 逃过忽略规则)
|
||||||
data/*.sqlite
|
data/*.sqlite
|
||||||
data/*.sqlite-wal
|
data/*.sqlite-wal
|
||||||
data/*.sqlite-shm
|
data/*.sqlite-shm
|
||||||
|
data/**/*.sqlite
|
||||||
|
data/**/*.sqlite-wal
|
||||||
|
data/**/*.sqlite-shm
|
||||||
|
|
||||||
# 含 secret_key,泄露等于会话签名密钥外泄,绝不可提交
|
# 含 secret_key,泄露等于会话签名密钥外泄,绝不可提交
|
||||||
data/instance.json
|
data/instance.json
|
||||||
|
data/**/instance.json
|
||||||
|
|
||||||
data/exports/*.csv
|
data/exports/*.csv
|
||||||
|
|
||||||
# 界面截图(tools/shots.py 生成的临时产物;手册配图在 docs/images/)
|
# 界面截图(tools/shots.py 生成的临时产物;手册配图在 docs/images/)
|
||||||
data/shots/
|
data/shots/
|
||||||
|
|
||||||
|
# 示例数据(tools/demo_data.py 生成,随时可重建,不必入库)
|
||||||
|
data/demo/
|
||||||
|
|
||||||
# ---- 日志 ----
|
# ---- 日志 ----
|
||||||
logs/*
|
logs/*
|
||||||
|
|
||||||
|
|||||||
@@ -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」部分。
|
||||||
@@ -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 里问,比写完再返工更省事。
|
||||||
@@ -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.
|
||||||
@@ -14,9 +14,10 @@
|
|||||||
| 部署 | Docker Compose / 裸机 `waitress`;镜像可推 Gitea 容器注册表 |
|
| 部署 | Docker Compose / 裸机 `waitress`;镜像可推 Gitea 容器注册表 |
|
||||||
| 鉴权 | 全站登录 + CSRF + 角色(管理员 / 普通用户),凭证存库、页面只回掩码 |
|
| 鉴权 | 全站登录 + CSRF + 角色(管理员 / 普通用户),凭证存库、页面只回掩码 |
|
||||||
| 版本 | v1.1.0 |
|
| 版本 | 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/API.md](docs/API.md) | 开发 / 集成 | 接口参考:路径、参数、返回结构、错误码 |
|
||||||
| [docs/FAQ.md](docs/FAQ.md) | 所有人 | 常见问题:采集为空、Cookie 失效、时区、性能、权限 |
|
| [docs/FAQ.md](docs/FAQ.md) | 所有人 | 常见问题:采集为空、Cookie 失效、时区、性能、权限 |
|
||||||
| [docs/CHANGELOG.md](docs/CHANGELOG.md) | 所有人 | 变更日志 |
|
| [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 一律回落到 `/`。
|
- **开放重定向防护**:登录跳转的 `next` 只接受站内相对路径,`//evil.com` 这类协议相对 URL 一律回落到 `/`。
|
||||||
- **登录限速**:同 IP 连续失败 5 次锁定 10 分钟;失败计数表有上限与 TTL。
|
- **登录限速**:同 IP 连续失败 5 次锁定 10 分钟;失败计数表有上限与 TTL。
|
||||||
- **不进版本库的文件**:`data/instance.json`(含 `secret_key`)、`data/usage.sqlite`、`logs/`、`.env`(含明文密码)。
|
- **不进版本库的文件**:`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/)
|
||||||
|
|||||||
@@ -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 库是唯一正本)
|
||||||
@@ -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,同步更新本文档中的版本号
|
||||||
|
- [ ] 若新增了第三方文件到仓库,在此登记其许可证
|
||||||
|
- [ ] 若将本项目的界面截图用于宣传,确认其中不含真实业务数据
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# workbuddy-portal 容器入口
|
# workbuddy-portal 容器入口
|
||||||
# 1) 幂等初始化数据库(建表 + 默认配置 + 首个管理员)
|
# 1) 幂等初始化数据库(建表 + 默认配置 + 首个管理员)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""容器健康检查。
|
"""容器健康检查。
|
||||||
|
|
||||||
只用标准库:slim 镜像里没有 curl。`/login` 是唯一免登录页面,拿到 200 即认为
|
只用标准库:slim 镜像里没有 curl。`/login` 是唯一免登录页面,拿到 200 即认为
|
||||||
|
|||||||
@@ -47,16 +47,16 @@
|
|||||||
"producer": "workbuddy-portal(Flask + SQLite)",
|
"producer": "workbuddy-portal(Flask + SQLite)",
|
||||||
"note": "...",
|
"note": "...",
|
||||||
"totals": {
|
"totals": {
|
||||||
"records": 1665, "credits": 8513.36, "calls": 1086,
|
"records": 944, "credits": 4961.63, "calls": 944,
|
||||||
"freeCalls": 579, "billableCalls": 507,
|
"freeCalls": 328, "billableCalls": 616,
|
||||||
"models": 12, "clients": 3,
|
"models": 7, "clients": 3,
|
||||||
"first": "2026-08-10 00:00:00", "last": "2026-09-14 14:51:00",
|
"first": "2026-08-16 09:12:00", "last": "2026-09-14 15:52:00",
|
||||||
"topCredits": 319.5
|
"topCredits": 412.8
|
||||||
},
|
},
|
||||||
"months": ["2026-08", "2026-09"],
|
"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",
|
"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": { ... 同上 ... },
|
"manifest": { ... 同上 ... },
|
||||||
"daily": [
|
"daily": [
|
||||||
{ "d": "2026-09-08", "c": 1423.5, "k": 88, "fc": 40, "bc": 48,
|
{ "d": "2026-09-08", "c": 168.4, "k": 31, "fc": 12, "bc": 19,
|
||||||
"m": { "deepseek-v4-flash": 800.2, "glm-5.3-flash": 623.3 },
|
"m": { "demo-flash": 62.1, "demo-pro": 41.7 },
|
||||||
"h": [0,0,0,0,0,0,0,0,0,12.5, ...] }
|
"h": [0,0,0,0,0,0,0,0,0,12.5, ...] }
|
||||||
],
|
],
|
||||||
"dims": { "model": [...], "client": [...], "hour": [...] },
|
"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": "..." } ],
|
"records": [ { "id": "...", "c": 5.78, "m": "...", "cl": "...", "t": "...", "px": "..." } ],
|
||||||
"recordsTotal": 1665,
|
"recordsTotal": 944,
|
||||||
"recordsCap": 20000,
|
"recordsCap": 20000,
|
||||||
"recordsTruncated": false,
|
"recordsTruncated": false,
|
||||||
"totals": { ... },
|
"totals": { ... },
|
||||||
@@ -106,13 +106,13 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"records": 428, "credits": 2145.6, "calls": 300,
|
"records": 226, "credits": 1180.4, "calls": 226,
|
||||||
"freeCalls": 120, "billableCalls": 180,
|
"freeCalls": 78, "billableCalls": 148,
|
||||||
"firstDay": "2026-09-08", "lastDay": "2026-09-14", "days": 7,
|
"firstDay": "2026-09-08", "lastDay": "2026-09-14", "days": 7,
|
||||||
"models": 9, "clients": 2,
|
"models": 7, "clients": 3,
|
||||||
"first": "...", "last": "...",
|
"first": "...", "last": "...",
|
||||||
"window": { "from": "2026-09-08", "to": "2026-09-14", "days": 7 },
|
"window": { "from": "2026-09-08", "to": "2026-09-14", "days": 7 },
|
||||||
"avgPerCall": 7.15,
|
"avgPerCall": 5.22,
|
||||||
"prev": { ... 上一段等长窗口的同样结构 ... },
|
"prev": { ... 上一段等长窗口的同样结构 ... },
|
||||||
"delta": { "credits": 12.3, "calls": -4.1, "window": { "from": "...", "to": "..." } },
|
"delta": { "credits": 12.3, "calls": -4.1, "window": { "from": "...", "to": "..." } },
|
||||||
"partial": { "date": "2026-09-14", "hhmm": "14:52" }
|
"partial": { "date": "2026-09-14", "hhmm": "14:52" }
|
||||||
@@ -132,7 +132,7 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
|
|||||||
| `from` / `to` | 筛选窗口 |
|
| `from` / `to` | 筛选窗口 |
|
||||||
|
|
||||||
```json
|
```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 个元素] } ] }
|
"m": {...}, "h": [24 个元素] } ] }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -146,8 +146,8 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model": [ { "name": "deepseek-v4-flash", "credits": 3784.86, "calls": 234, "avg": 16.17, "free": 0 } ],
|
"model": [ { "name": "demo-flash", "credits": 1286.4, "calls": 252, "avg": 5.1, "free": 0 } ],
|
||||||
"client": [ { "name": "VSCode", ... } ],
|
"client": [ { "name": "vscode", ... } ],
|
||||||
"hour": [ { "name": "14", ... } ]
|
"hour": [ { "name": "14", ... } ]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -162,7 +162,7 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
|
|||||||
| `n` | 50 | 返回条数(1~1000) |
|
| `n` | 50 | 返回条数(1~1000) |
|
||||||
|
|
||||||
```json
|
```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 摘要" } ]
|
"t": "2026-09-12 15:04:00", "px": "截断后的 Prompt 摘要" } ]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ KPI + 环比。`from`/`to` 缺省时自动取全量区间。
|
|||||||
{
|
{
|
||||||
"items": [ { "request_id": "…", "credits": 5.78, "model": "…",
|
"items": [ { "request_id": "…", "credits": 5.78, "model": "…",
|
||||||
"client": "…", "ts": "2026-09-14 14:20:00", "prompt": "…" } ],
|
"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": "..." }
|
"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": "..."}` |
|
| 已有采集在跑 | `409 {"ok": false, "error": "busy", "message": "..."}` |
|
||||||
| Cookie 失效 | `401 {"ok": false, "error": "cookie_expired", "message": "..."}` |
|
| Cookie 失效 | `401 {"ok": false, "error": "cookie_expired", "message": "..."}` |
|
||||||
| 云端异常 | `502 {"ok": false, "error": "api", "message": "..."}` |
|
| 云端异常 | `502 {"ok": false, "error": "api", "message": "..."}` |
|
||||||
|
|||||||
@@ -22,16 +22,31 @@
|
|||||||
- **容器环境变量**:`WB_HOST` `WB_PORT` `WB_DATA_DIR` `WB_LOG_DIR` `WB_DB`
|
- **容器环境变量**:`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`
|
`WB_ADMIN_USER` `WB_ADMIN_PASSWORD` `WB_DISABLE_SCHEDULER` `WB_IMPORT_CREDS` `WB_IMPORT_XLSX`
|
||||||
- **文档体系** `docs/`:
|
- **文档体系** `docs/`:
|
||||||
[用户使用手册](USER-GUIDE.md)(含 9 张界面截图)、
|
[用户使用手册](USER-GUIDE.md)(含 9 张界面截图,**全部用合成示例数据渲染**)、
|
||||||
[部署与运维指南](DEPLOYMENT.md)(含推镜像到 Gitea 注册表的完整流程)、
|
[部署与运维指南](DEPLOYMENT.md)(含推镜像到 Gitea 注册表的完整流程)、
|
||||||
[架构与设计说明](ARCHITECTURE.md)、
|
[架构与设计说明](ARCHITECTURE.md)、
|
||||||
[接口参考](API.md)、
|
[接口参考](API.md)、
|
||||||
[常见问题](FAQ.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
|
- **`.gitattributes`**:强制 `*.sh` / `Dockerfile` / 各类源码为 LF
|
||||||
(带 CRLF 的 `.sh` 在容器里会报 `exec format error`,极难定位)
|
(带 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`**;
|
- **项目定名**:`wb_usage_portal` → **`workbuddy-portal`**;
|
||||||
Python 包 `wb_usage` → **`workbuddy_portal`**;会话 cookie
|
Python 包 `wb_usage` → **`workbuddy_portal`**;会话 cookie
|
||||||
`wb_usage_sid` → `workbuddy_portal_sid`(升级后需要重新登录)
|
`wb_usage_sid` → `workbuddy_portal_sid`(升级后需要重新登录)
|
||||||
|
|||||||
@@ -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 -f docker-compose.yml -f docker-compose.hostdir.yml up -d
|
||||||
docker compose exec portal python -c \
|
docker compose exec portal python -c \
|
||||||
"import sqlite3;print(sqlite3.connect('/app/data/usage.sqlite').execute('select count(*) from usage_records').fetchone())"
|
"import sqlite3;print(sqlite3.connect('/app/data/usage.sqlite').execute('select count(*) from usage_records').fetchone())"
|
||||||
# -> (1665,) 容器侧正常
|
# -> (944,) 容器侧正常
|
||||||
|
|
||||||
python manage.py stats # 宿主侧随手跑一次「纯读」的 CLI
|
python manage.py stats # 宿主侧随手跑一次「纯读」的 CLI
|
||||||
# -> 存档:1665 条 …
|
# -> 存档:944 条 …
|
||||||
|
|
||||||
docker compose exec portal python -c \
|
docker compose exec portal python -c \
|
||||||
"import sqlite3;sqlite3.connect('/app/data/usage.sqlite')"
|
"import sqlite3;sqlite3.connect('/app/data/usage.sqlite')"
|
||||||
|
|||||||
@@ -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 |
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""WorkBuddy Portal —— 统一命令行入口。
|
"""WorkBuddy Portal —— 统一命令行入口。
|
||||||
|
|
||||||
采集 / 存储 / 呈现三件事都由本项目承担,不再依赖外部计划任务或自动化。
|
采集 / 存储 / 呈现三件事都由本项目承担,不再依赖外部计划任务或自动化。
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""端到端验收:对**运行中的**服务发真实 HTTP 请求,走完整登录/CSRF/API 链路。
|
"""端到端验收:对**运行中的**服务发真实 HTTP 请求,走完整登录/CSRF/API 链路。
|
||||||
|
|
||||||
与 tests 里用 Flask test_client 的冒烟测试互补——这里验证的是「真的起起来了、
|
与 tests 里用 Flask test_client 的冒烟测试互补——这里验证的是「真的起起来了、
|
||||||
@@ -7,7 +10,7 @@
|
|||||||
|
|
||||||
用法:
|
用法:
|
||||||
python tools/check_live.py # 默认 http://127.0.0.1:8848
|
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 -u admin -p 你的密码
|
||||||
python tools/check_live.py --from 2026-09-08 --to 2026-09-14
|
python tools/check_live.py --from 2026-09-08 --to 2026-09-14
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 用 Gitea Access Token 一次性完成:推代码 + 推镜像
|
# 用 Gitea Access Token 一次性完成:推代码 + 推镜像
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""界面实检:登录后逐页截图,用来目视确认「统一美化」是否真的落地。
|
"""界面实检:登录后逐页截图,用来目视确认「统一美化」是否真的落地。
|
||||||
|
|
||||||
用法:
|
用法:
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""离线回归:用 Flask test_client 对**真实库**做全页面只读渲染 + 缺陷防回归断言。
|
"""离线回归:用 Flask test_client 对**真实库**做全页面只读渲染 + 缺陷防回归断言。
|
||||||
|
|
||||||
与 tools/check_live.py 的分工:
|
与 tools/check_live.py 的分工:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""WorkBuddy Portal —— 独立 Flask 项目。
|
"""WorkBuddy Portal —— 独立 Flask 项目。
|
||||||
|
|
||||||
一个程序管全部:
|
一个程序管全部:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""云端用量接口客户端(纯 urllib,不依赖 requests/浏览器)。
|
"""云端用量接口客户端(纯 urllib,不依赖 requests/浏览器)。
|
||||||
|
|
||||||
接口:POST {api_base}/billing/meter/get-user-request-usage
|
接口:POST {api_base}/billing/meter/get-user-request-usage
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""采集主流程:云端增量 -> SQLite(去重、断点、漂移校验、运行记录)。
|
"""采集主流程:云端增量 -> SQLite(去重、断点、漂移校验、运行记录)。
|
||||||
|
|
||||||
与原 fetch_usage.py 的差别:
|
与原 fetch_usage.py 的差别:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""基础配置。
|
"""基础配置。
|
||||||
|
|
||||||
刻意保持「薄」:凡是运行期要改的东西(cookie、调度周期、采集参数)都放数据库
|
刻意保持「薄」:凡是运行期要改的东西(cookie、调度周期、采集参数)都放数据库
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""SQLite 访问层。
|
"""SQLite 访问层。
|
||||||
|
|
||||||
并发约定(重要):
|
并发约定(重要):
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""SQL 聚合层:所有统计都在 SQLite 里算完再出去,页面不再搬运全量明细。
|
"""SQL 聚合层:所有统计都在 SQLite 里算完再出去,页面不再搬运全量明细。
|
||||||
|
|
||||||
返回结构刻意与旧版 dashboard/data/*.json 的字段保持一致(d/c/k/fc/bc/m/h、
|
返回结构刻意与旧版 dashboard/data/*.json 的字段保持一致(d/c/k/fc/bc/m/h、
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""进程内调度器(手写,不依赖 APScheduler)。
|
"""进程内调度器(手写,不依赖 APScheduler)。
|
||||||
|
|
||||||
为什么不引 APScheduler:
|
为什么不引 APScheduler:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""密码哈希、登录装饰器、CSRF。
|
"""密码哈希、登录装饰器、CSRF。
|
||||||
|
|
||||||
局域网可访问 ⇒ 必须有鉴权。这里用 Werkzeug 自带的 PBKDF2,不引第三方依赖。
|
局域网可访问 ⇒ 必须有鉴权。这里用 Werkzeug 自带的 PBKDF2,不引第三方依赖。
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""Web 层包:蓝图注册。"""
|
"""Web 层包:蓝图注册。"""
|
||||||
from . import api, views
|
from . import api, views
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""JSON API —— ECharts 大屏与后台页面的数据入口。
|
"""JSON API —— ECharts 大屏与后台页面的数据入口。
|
||||||
|
|
||||||
约定:
|
约定:
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2026 Wang Chuanli
|
||||||
|
|
||||||
"""页面路由(Jinja 模板)。
|
"""页面路由(Jinja 模板)。
|
||||||
|
|
||||||
分工:
|
分工:
|
||||||
|
|||||||