chore: 项目定名为 workbuddy-portal,容器化并补齐文档体系
## 项目定名 - 目录 wb_usage_portal → workbuddy-portal - Python 包 wb_usage → workbuddy_portal(含 session cookie 名) - 界面品牌统一为 WorkBuddy Portal;项目标识收敛到 config 单一来源 ## 容器化 - Dockerfile:多阶段构建,依赖层与源码解耦;非 root(uid 1000);内置健康检查 - docker-compose.yml:单服务 + 绑定挂载 data/logs + 日志轮转 + TZ - docker/entrypoint.sh:幂等初始化 → exec serve(LF 行尾,已由 .gitattributes 锁定) - docker/healthcheck.py:纯标准库探活 /login(slim 镜像无 curl) - .dockerignore / .env.example;数据目录可用 WB_DATA_DIR 等环境变量覆盖 ## 文档 - docs/USER-GUIDE.md 用户使用手册(含 9 张真实界面截图) - docs/DEPLOYMENT.md 部署运维(Docker / 裸机 / 反代 / 备份 / 推 Gitea 注册表) - docs/ARCHITECTURE.md 架构与设计说明(含已知坑与红线、验证体系) - docs/API.md 接口参考(路径 / 参数 / 返回结构 / 错误码) - docs/FAQ.md 常见问题;docs/CHANGELOG.md 变更日志 ## 修复缺陷(8) 1. /records/export 必然 500:生成器在请求上下文销毁后才迭代,改用自建连接 2. 大屏页图表全白:相对路径把 echarts.min.js 解析成 /vendor/... → 404 3. /users 500:路由已注册但模板缺失 4. 明细页日期筛选失效:视图传 f.frm、模板读 f.from 5. 配置页维护按钮全死:调用了不存在的 WBU.bindMaint() 6. 审计只能看最近 40 条:LIMIT 写死 7. 明细页多跑一条无用 SELECT:day_list() 取了没人用 8. 登录页锁定阈值未从配置注入 ## 安全加固 - 新增 safe_next():拒绝 //evil.com 等协议相对 URL 的开放重定向 - 缺 CSRF 的写请求统一 400 - 默认开启云端 HTTPS 证书校验(ssl_verify=1);Cookie 是账号凭证 - 登录失败计数表加上限与 TTL - /logout 拆分为 POST(执行) + GET(仅提示),防 <img src=/logout> 静默退出 - settings 内部簿记键 slot:* 读写两侧过滤,不再从 /api/settings 泄漏 ## 内部质量与工具 - 设置项写时校验 + 读时兜底,杜绝「一个手滑的数字让采集整个跑不起来」 - 全局 ValueError → 400:手写 query string 不再暴露 500 页面 - CSV 导出改 csv.writer 流式写入(原手工拼串,字段含逗号会串列) - bundle 明细加 20000 上限并回传 recordsTotal/recordsTruncated,不静默丢数据 - tools/smoke.py 离线回归 99 项;tools/check_live.py 真实 HTTP 56 项 - tools/shots.py Playwright 逐页截图 + JS 报错收集 ## 验证 - compileall 通过;smoke 99/99;对容器实例 check_live 56/56;截图 0 JS 报错 - 容器内采集实测成功(trigger=startup 补跑:新增 11 条)
这个提交包含在:
@@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}配置管理 · {{ project_title }}{% endblock %}
|
||||
{% block body %}
|
||||
|
||||
<div class="pagehead">
|
||||
<div>
|
||||
<h1>配置管理</h1>
|
||||
<p class="lead">凭证、采集参数、维护动作都在这里;所有配置存在数据库,改完立即生效</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<div class="cardhead">
|
||||
<h2>云端凭证</h2>
|
||||
<span class="tag {{ 'ok' if s.cookie_hint else 'bad' }}">{{ '已配置' if s.cookie_hint else '未配置' }}</span>
|
||||
</div>
|
||||
<p class="hint">
|
||||
{% if s.cookie_hint %}当前 Cookie:{{ s.cookie_hint }}(页面与接口都不回传明文){% endif %}
|
||||
<br>获取方式:Chrome 打开 <code>https://www.workbuddy.cn/profile/plans-usage</code> → F12 → Network →
|
||||
任选一个 <code>billing</code> 请求 → 复制 Request Headers 里的 <code>cookie</code> 与 <code>user-agent</code>
|
||||
(<b>两者必须取自同一次请求</b>),粘贴到下面。
|
||||
</p>
|
||||
<form id="formCred">
|
||||
<label class="col">Cookie
|
||||
<textarea name="cookie" rows="4" placeholder="留空表示不修改;填 - 表示清空已保存的 Cookie" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<label class="col">User-Agent
|
||||
<textarea name="user_agent" rows="2" spellcheck="false">{{ s.user_agent }}</textarea>
|
||||
</label>
|
||||
<button class="btn primary" type="submit">保存凭证</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="grid2">
|
||||
<section class="card">
|
||||
<h2>采集参数</h2>
|
||||
<form id="formCollect">
|
||||
<label class="row"><span>接口基址</span><input name="api_base" value="{{ s.api_base }}" spellcheck="false"></label>
|
||||
<label class="row"><span>接口路径</span><input name="api_path" value="{{ s.api_path }}" spellcheck="false"></label>
|
||||
{% set b = num_settings %}
|
||||
<label class="row"><span>分页大小</span>
|
||||
<input name="page_size" type="number" min="{{ b.page_size[0] }}" max="{{ b.page_size[1] }}" value="{{ s.page_size }}">
|
||||
<em class="unit">{{ b.page_size[0] }}~{{ b.page_size[1] }} 条/页</em></label>
|
||||
<label class="row"><span>断点回退</span>
|
||||
<input name="rewind_minutes" type="number" min="{{ b.rewind_minutes[0] }}" max="{{ b.rewind_minutes[1] }}" value="{{ s.rewind_minutes }}">
|
||||
<em class="unit">分钟</em></label>
|
||||
<label class="row"><span>时间漂移容差</span>
|
||||
<input name="drift_tolerance_minutes" type="number" min="{{ b.drift_tolerance_minutes[0] }}" max="{{ b.drift_tolerance_minutes[1] }}" value="{{ s.drift_tolerance_minutes }}">
|
||||
<em class="unit">分钟</em></label>
|
||||
<label class="row"><span>Prompt 截断</span>
|
||||
<input name="max_prompt" type="number" min="{{ b.max_prompt[0] }}" max="{{ b.max_prompt[1] }}" value="{{ s.max_prompt }}">
|
||||
<em class="unit">字符(0 = 不截断)</em></label>
|
||||
<label class="row"><span>整日校验天数</span>
|
||||
<input name="verify_days" type="number" min="{{ b.verify_days[0] }}" max="{{ b.verify_days[1] }}" value="{{ s.verify_days }}">
|
||||
<em class="unit">天</em></label>
|
||||
<label class="row"><span>请求超时</span>
|
||||
<input name="timeout" type="number" min="{{ b.timeout[0] }}" max="{{ b.timeout[1] }}" value="{{ s.timeout }}">
|
||||
<em class="unit">秒</em></label>
|
||||
<label class="row"><span>校验 TLS 证书</span>
|
||||
<select name="ssl_verify">
|
||||
<option value="1" {{ 'selected' if s.ssl_verify != '0' }}>校验(推荐)</option>
|
||||
<option value="0" {{ 'selected' if s.ssl_verify == '0' }}>不校验(仅自签/企业代理时用)</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="hint">Cookie 就是账号凭证,关掉证书校验等于把它暴露给中间人,非必要不要关。</p>
|
||||
<button class="btn primary" type="submit">保存采集参数</button>
|
||||
<p class="hint">整日校验会按天重新拉云端 total 与本地比对,发现缺记录自动补入;设为 0 表示关闭(日常够用)。</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>修改登录密码</h2>
|
||||
<form id="formPwd">
|
||||
<label class="col">原密码<input name="old" type="password" autocomplete="current-password"></label>
|
||||
<label class="col">新密码<input name="new" type="password" autocomplete="new-password"></label>
|
||||
<label class="col">确认新密码<input name="new2" type="password" autocomplete="new-password"></label>
|
||||
<button class="btn primary" type="submit">修改密码</button>
|
||||
<p class="hint">至少 6 位。修改成功后当前会话仍有效,不必重新登录。</p>
|
||||
</form>
|
||||
|
||||
<hr class="sect-divider">
|
||||
<h3>维护动作</h3>
|
||||
<div class="btnrow">
|
||||
<button class="btn" type="button" data-maint="fill-prompt"
|
||||
title="把云端仍保留、但本地为空的 User Prompt 补回来">补全缺失 Prompt</button>
|
||||
<button class="btn" type="button" data-maint="export-csv"
|
||||
title="导出与官网 xlsx 同构的全量 CSV 到 data/exports/">导出全量 CSV</button>
|
||||
<button class="btn" type="button" data-maint="vacuum"
|
||||
title="checkpoint + VACUUM,回收删除后的空闲页">整理数据库</button>
|
||||
</div>
|
||||
<p class="hint">补全 Prompt 需要联网并逐天重拉云端;导出与整理只动本地数据。</p>
|
||||
<div class="btnrow" style="margin-top:14px">
|
||||
<a class="btn ghost" href="{{ url_for('views.records_export') }}">按当前明细页筛选导出</a>
|
||||
{% if current_user().is_admin %}<a class="btn ghost" href="{{ url_for('views.users_page') }}">用户管理</a>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
<script>
|
||||
WBU.bindForm('#formCred', '/api/settings');
|
||||
WBU.bindForm('#formCollect', '/api/settings');
|
||||
WBU.bindForm('#formPwd', '/api/password', {validate: d => d.new === d.new2 ? null : '两次输入的新密码不一致'});
|
||||
WBU.bindMaint('[data-maint]');
|
||||
</script>
|
||||
{% endblock %}
|
||||
在新工单中引用
屏蔽一个用户