feat(安全): 对外暴露面加固 + 界面去 AI 化(v1.5.0)
界面(去 AI 味): - 大屏页清除 114 处生成器残留属性 data-page-node-id - 视觉系统改回工程控制台风格:去 radial/linear-gradient、去辉光、 去标题前彩色装饰条,改为中性灰阶 + 单一蓝色强调色;KPI 色条改状态点 - 精简各页说教式长提示;修掉 profile.html 泄漏到页面上的 Markdown 星号 - 删除登录页过时的「默认账号 admin / admin123」提示(1.4.0 起已无默认口令) 安全与隐私(按「将会被公网访问」收口): - 内部异常只回 8 位事件号,完整堆栈进服务端日志(web/api.py::_internal) - 导出文件名收敛:防响应头注入与路径穿越;manage.py passwd 补用户名校验 - 登录对不存在的账号也走一次哑哈希,抹平用户名枚举的时序差异 - /api/* 读接口限速 240 次 / 60 秒 / 账号(挡住循环调 /api/bundle) - 进程 umask 0077 + 目录 0700 / 文件 0600:对话正文与主密钥的落盘权限 - 表名与库文件路径只对管理员下发;大屏页所有数据插值转义 - --debug 只允许绑定回环地址;新增 Permissions-Policy 与 413 处理器 文档: - DEPLOYMENT 新增第十三节「安全与隐私基线」;迁移表补 1.4.0 → 1.5.0 行 - SECURITY 更新支持范围、新增「信息泄漏收敛」小节与上线检查项 - .codebuddy/ 加入 .gitignore(助手工作记忆不进仓库) 版本:1.4.0 → 1.5.0(无库结构变更,user_version 仍为 4) 验证:python tools/smoke.py → ok=264 fail=0;python tools/check_docs.py → 0 处问题
这个提交包含在:
@@ -39,9 +39,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flash warn" style="margin-bottom:16px">
|
||||
<b>恢复会覆盖当前全部数据</b>(账号、用量、配置一起换成归档里的那一份)。
|
||||
系统会在恢复前<b>自动先备份一次当前库</b>并保留在列表里,恢复错了可以再恢复到那一份。
|
||||
恢复完成后所有既有登录会话失效,需要重新登录。
|
||||
恢复会用归档内容覆盖当前全部数据(账号、用量、配置)。系统会在恢复前自动备份当前库并保留在列表里,
|
||||
恢复错了可以再恢复到那一份。恢复完成后所有登录会话失效,需要重新登录。
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
@@ -69,12 +68,8 @@
|
||||
value="{{ s.backup_keep }}">
|
||||
<em class="unit">份(超出的自动删最旧的)</em></label>
|
||||
<button class="btn primary" type="submit">保存备份设置</button>
|
||||
<p class="hint">
|
||||
自动备份在调度线程里执行,有采集任务在跑时会自动让路、下一轮再打。
|
||||
备份使用 SQLite 的在线备份 API,<b>采集写入期间也能拿到一致快照</b> ——
|
||||
这一点是手工 <code>cp usage.sqlite</code> 做不到的。
|
||||
每份归档打完后按「保留份数」清理最旧的那些。
|
||||
</p>
|
||||
<p class="hint">自动备份在调度线程里执行,有采集在跑时让路到下一轮。走 SQLite 在线备份 API,
|
||||
采集写入期间也能拿到一致快照。打完按「保留份数」清理最旧的。</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -91,18 +86,14 @@
|
||||
<code>manifest.json</code>(时间、行数、积分、逐文件校验值)
|
||||
</td></tr>
|
||||
<tr><th>为什么带上密钥</th><td>
|
||||
没有 <code>instance.json</code> 里的 <code>cookie_key</code>,归档里的凭证密文
|
||||
就永远解不开 —— 那样的「恢复」等于把所有账号的 Cookie 弄丢。
|
||||
没有 <code>instance.json</code> 里的 <code>cookie_key</code>,归档中的凭证密文就解不开。
|
||||
</td></tr>
|
||||
<tr><th>因此</th><td>
|
||||
<b>归档等于全库数据 + 密钥</b>,下载后请当作机密文件保管;
|
||||
它<b>不会</b>进入代码仓库、也不会被打进镜像(见 <code>.gitignore</code> /
|
||||
<code>.dockerignore</code>)。
|
||||
<tr><th>保管要求</th><td>
|
||||
归档 = 全库数据 + 密钥,下载后请按机密文件保管;它不会进入仓库,也不会被打进镜像。
|
||||
</td></tr>
|
||||
<tr><th>目录独立</th><td>
|
||||
备份目录刻意<b>不在 <code>data/</code> 里面</b>:容器里 <code>data/</code> 是数据卷,
|
||||
<code>docker compose down -v</code> 会把正本和副本一起删掉。
|
||||
容器里它挂在独立卷 <code>wb_backups</code> 上。
|
||||
备份目录不在 <code>data/</code> 内,避免容器 <code>down -v</code> 时正本与副本一起被删;
|
||||
容器里挂在独立卷 <code>wb_backups</code> 上。
|
||||
</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
@@ -158,11 +149,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="hint">
|
||||
列表里的「记录 / 积分 / 账号」是归档生成时的计数,用来挑一份合适的恢复 ——
|
||||
恢复前程序还会再校验一次归档的完整性与库结构版本,校验不过不会动你的数据。
|
||||
档案文件名只能由本程序生成(服务端会拒绝任何带路径的文件名)。
|
||||
</p>
|
||||
<p class="hint">「记录 / 积分 / 账号」是归档生成时的计数。恢复前会先校验归档完整性与库结构版本,校验不过不动数据。</p>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -68,8 +68,7 @@
|
||||
</main>
|
||||
|
||||
<footer class="foot">
|
||||
<b>{{ project_title }}</b> · {{ project_name }} · 多用户 · 调度与采集参数由管理员统一设定<br>
|
||||
每个账号只使用并只见得到<b>自己的</b> Cookie 与用量数据;数据正本 <code>data/usage.sqlite</code>
|
||||
{{ project_title }} · {{ project_name }} · 数据正本 <code>data/usage.sqlite</code>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -27,15 +27,11 @@
|
||||
</div>
|
||||
<p class="hint">
|
||||
{% if s.cookie_hint %}当前 Cookie:{{ s.cookie_hint }}
|
||||
{% if s.cookie_at %}({{ s.cookie_at }} 更新){% endif %}
|
||||
—— 页面与接口都不回传明文,数据库里也是密文。{% endif %}
|
||||
{% if s.cookie_at %}({{ s.cookie_at }} 更新){% endif %}。{% 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>),粘贴到下面。
|
||||
<br><b>请粘贴你自己账号的 Cookie</b>:采集只使用本人凭证,各账号的数据互不可见。
|
||||
{% if not is_admin %}
|
||||
<br><b>这一块是你唯一可以修改的配置</b> —— 其余参数与调度时刻由管理员统一设定。
|
||||
{% endif %}
|
||||
任选一个 <code>billing</code> 请求 → 复制 Request Headers 的 <code>cookie</code> 与 <code>user-agent</code>
|
||||
(两者须取自同一次请求)。
|
||||
<br>采集只使用本人凭证,各账号数据互不可见。{% if not is_admin %}普通账号只有这一块可改。{% endif %}
|
||||
</p>
|
||||
<form id="formCred">
|
||||
<label class="col">Cookie
|
||||
@@ -92,11 +88,10 @@
|
||||
</label>
|
||||
<p class="hint">Cookie 就是账号凭证,关掉证书校验等于把它暴露给中间人,非必要不要关。</p>
|
||||
<button class="btn primary" type="submit">保存采集参数</button>
|
||||
<p class="hint">整日校验会按天重新拉云端 total 与本地比对,发现缺记录自动补入;设为 0 表示关闭(日常够用)。
|
||||
这些是<b>实例级</b>参数,改一次对本机所有账号生效。</p>
|
||||
<p class="hint">整日校验按天比对云端 total 与本地记录并补入缺失项,0 表示关闭。实例级参数,对本机所有账号生效。</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="hint">这些参数影响采集行为与云端压力,属于整机策略,普通账号只读。</p>
|
||||
<p class="hint">这些属于整机策略,普通账号只读。</p>
|
||||
<table class="kv">
|
||||
<tr><th>接口基址</th><td class="mono">{{ s.api_base }}</td></tr>
|
||||
<tr><th>接口路径</th><td class="mono">{{ s.api_path }}</td></tr>
|
||||
@@ -118,8 +113,7 @@
|
||||
<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">至少 {{ pwd_min }} 位,且需包含大写字母、小写字母、数字、符号中的至少两类。
|
||||
修改成功后当前会话仍有效,不必重新登录。</p>
|
||||
<p class="hint">至少 {{ pwd_min }} 位,需包含大写字母、小写字母、数字、符号中的至少两类。修改后当前会话仍有效。</p>
|
||||
</form>
|
||||
|
||||
<hr class="sect-divider">
|
||||
@@ -143,22 +137,6 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% if not is_admin %}
|
||||
<section class="card">
|
||||
<div class="cardhead">
|
||||
<h2>为什么采集参数是只读的</h2>
|
||||
<span class="tag mute">普通账号</span>
|
||||
</div>
|
||||
<p class="hint" style="margin:0">
|
||||
你只能维护<b>本人账号的凭证</b>(Cookie 与 User-Agent)—— 采集始终只用你自己的这份凭证,
|
||||
拿回来的数据也只会存在你自己的作用域里,别人看不到、你也看不到别人的。
|
||||
分页大小、超时、接口地址、TLS 校验、调度时刻这些属于<b>整机策略</b>:它们既关系到云端
|
||||
压力,也关系到所有人的采集是否安全,因此统一由管理员设定。
|
||||
<br>如果你确实需要调整,把上面任意一项截图给管理员即可。
|
||||
</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if is_admin %}
|
||||
<section class="card">
|
||||
<div class="cardhead">
|
||||
@@ -191,11 +169,7 @@
|
||||
value="{{ s.captcha_length }}">
|
||||
<em class="unit">4~6 位</em></label>
|
||||
<button class="btn primary" type="submit">保存实例设置</button>
|
||||
<p class="hint">
|
||||
验证码的答案只存在服务端 <code>captchas</code> 表里(下发到浏览器的只是一个随机 id),
|
||||
一次性使用、5 分钟过期 —— 所以答案不会随会话 Cookie 泄漏出去。
|
||||
关闭验证码会显著放大被撞库与批量注册的风险,只有在前面已经有可信网关时才考虑。
|
||||
</p>
|
||||
<p class="hint">验证码答案只存在服务端 <code>captchas</code> 表,一次性使用、5 分钟过期。关闭会显著放大撞库与批量注册的风险。</p>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
<p class="foot-note">还没有账号?<a href="{{ url_for('views.register') }}">自助注册</a></p>
|
||||
{% endif %}
|
||||
<p class="foot-note">
|
||||
首次部署默认账号 <code>admin</code> / <code>admin123</code>,登录后请立即修改密码。<br>
|
||||
连续输错 {{ max_fails }} 次将锁定 {{ lock_minutes }} 分钟;登录状态保持 {{ session_hours }} 小时。
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -58,10 +58,9 @@
|
||||
<tr><th>Cookie</th><td>{% if mf.health.cookie %}<span class="tag ok">已配置</span>{% else %}<span class="tag bad">未配置</span> <a href="{{ url_for('views.config_page') }}">去配置</a>{% endif %}</td></tr>
|
||||
<tr><th>服务器时间</th><td class="mono">{{ sch.now }}</td></tr>
|
||||
</table>
|
||||
<p class="hint">调度在 Web 进程内运行,不再需要计划任务或外部自动化。
|
||||
{% if current_user().is_admin %}所有时刻与开关都在
|
||||
<a href="{{ url_for('views.tasks') }}">任务管理</a>里改。{% else %}时刻由管理员统一设定,
|
||||
你可以在<a href="{{ url_for('views.tasks') }}">任务管理</a>里查看,也可以随时手动采集本人数据。{% endif %}</p>
|
||||
<p class="hint">调度随 Web 进程运行。{% if current_user().is_admin %}时刻与开关在
|
||||
<a href="{{ url_for('views.tasks') }}">任务管理</a>里改。{% else %}时刻由管理员设定,可在
|
||||
<a href="{{ url_for('views.tasks') }}">任务管理</a>查看或手动采集。{% endif %}</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
|
||||
@@ -59,10 +59,8 @@
|
||||
<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">至少 {{ pwd_min }} 位,且需包含大写字母、小写字母、数字、符号中的至少两类,
|
||||
且不能是常见弱口令。
|
||||
<b>修改成功后其他设备上的登录会立刻失效</b>(本机这次会话保留,不必重新登录)——
|
||||
「怀疑被盗所以改密码」才能真正生效。</p>
|
||||
<p class="hint">至少 {{ pwd_min }} 位,需包含大写字母、小写字母、数字、符号中的至少两类。
|
||||
修改成功后其他设备上的登录会立刻失效,本机会话保留。</p>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
@@ -73,16 +71,14 @@
|
||||
<span class="tag mute">数据可携带</span>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:0">
|
||||
打包下载<b>属于你账号</b>的所有内容:用量明细、采集历史、与你相关的操作审计、
|
||||
以及账号信息与对你有效的配置。
|
||||
<br><b>不含</b> Cookie 明文(只写「配没配、多少字符、什么时候更新的」)、
|
||||
也不含其他任何账号的数据与实例级运行日志。
|
||||
下载本账号的用量明细、采集历史、操作审计与有效配置。
|
||||
不含 Cookie 明文,也不含其他账号的数据与实例级运行日志。
|
||||
</p>
|
||||
<div class="btnrow">
|
||||
<a class="btn primary" href="{{ url_for('views.profile_export') }}">导出我的全部数据(zip)</a>
|
||||
<a class="btn ghost" href="{{ url_for('views.records') }}">只导出用量明细(CSV)</a>
|
||||
</div>
|
||||
<p class="hint">导出动作按账号有最小间隔(10 秒)保护,避免脚本反复触发整表扫描。</p>
|
||||
<p class="hint">两次导出之间至少间隔 10 秒。</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -105,10 +101,8 @@
|
||||
</td></tr>
|
||||
</table>
|
||||
<p class="hint">
|
||||
凭证以密文形式存在数据库里(主密钥在 <code>data/instance.json</code>),
|
||||
页面与接口**任何时候都不回传明文**,只显示长度与尾部 4 位。要更换请到
|
||||
<a href="{{ url_for('views.config_page') }}">配置管理</a>粘贴新的 Cookie 与 User-Agent
|
||||
(两者必须取自同一次浏览器请求)。
|
||||
凭证以密文入库(主密钥在 <code>data/instance.json</code>),页面与接口都不回传明文。
|
||||
更换请到<a href="{{ url_for('views.config_page') }}">配置管理</a>重新粘贴 Cookie 与 User-Agent。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -68,9 +68,7 @@
|
||||
<button class="btn primary" type="submit">查询</button>
|
||||
{% if has_filter %}<a class="btn ghost" href="{{ url_for('views.records') }}">重置</a>{% endif %}
|
||||
</form>
|
||||
<p class="hint">
|
||||
日期留空表示不限。有关键词时会同时匹配 User Prompt 与 RequestID(按天全表扫描,区间越大越慢)。
|
||||
</p>
|
||||
<p class="hint">日期留空表示不限。关键词会同时匹配 User Prompt 与 RequestID,区间越大越慢。</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="pagehead">
|
||||
<div>
|
||||
<h1>任务管理</h1>
|
||||
<p class="lead">调度在 Web 进程内执行,采集互斥由文件锁保证;这里能看到本人账号的运行历史,也能手动触发与按区间回填</p>
|
||||
<p class="lead">调度随 Web 进程运行;这里可查看本人运行历史、手动采集与按区间回填</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn primary" id="btnCollect" type="button">立即采集一次</button>
|
||||
@@ -30,11 +30,8 @@
|
||||
<label class="row"><span>每日时刻</span>
|
||||
<input name="schedule_times" value="{{ s_times }}" placeholder="09:00,17:00" spellcheck="false">
|
||||
</label>
|
||||
<p class="hint">本地时区,逗号分隔,支持 <code>HH:MM</code>(也可只写 <code>9</code>)。
|
||||
保存后立即生效;已经过去且不再存在的时刻会被清掉,新的时刻当天就会接管。
|
||||
<br><b>最多 {{ max_slots }} 个时刻</b>(当前 {{ sch.times|length }} 个)—— 时刻数量直接决定
|
||||
采集频次,是对外提供服务时控制云端压力的旋钮。需要更多请先到「配置管理」
|
||||
把「每日调度时刻上限」调大。</p>
|
||||
<p class="hint">本地时区,逗号分隔,支持 <code>HH:MM</code>(也可只写 <code>9</code>),保存后立即生效。
|
||||
最多 {{ max_slots }} 个时刻(当前 {{ sch.times|length }} 个),需要更多请先在「配置管理」调大上限。</p>
|
||||
<label class="row"><span>启动补跑</span>
|
||||
<select name="catch_up">
|
||||
<option value="1" {{ 'selected' if sch.catch_up }}>开启(错过的时刻在宽限期内补跑)</option>
|
||||
@@ -43,10 +40,10 @@
|
||||
</label>
|
||||
<label class="row"><span>补跑宽限</span>
|
||||
<input name="catch_up_grace_hours" value="{{ s_grace }}" type="number" min="1" max="168">
|
||||
<em class="unit">小时(超过就不补,避免开机狂刷)</em>
|
||||
<em class="unit">小时(超过则不补)</em>
|
||||
</label>
|
||||
<button class="btn primary" type="submit">保存调度配置</button>
|
||||
<p class="hint">这套时刻是<b>实例级</b>的:本机上所有账号在同一个时刻各自采集自己的数据。</p>
|
||||
<p class="hint">时刻是实例级的:本机所有账号在此时刻各自采集自己的数据。</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<table class="kv">
|
||||
@@ -55,11 +52,7 @@
|
||||
<tr><th>启动补跑</th><td>{% if sch.catch_up %}<span class="tag">开启</span>(宽限 {{ s_grace }} 小时){% else %}关闭{% endif %}</td></tr>
|
||||
<tr><th>下次执行</th><td class="mono">{{ sch.next_run or '—' }}</td></tr>
|
||||
</table>
|
||||
<p class="hint">
|
||||
调度时刻与采集参数由<b>管理员统一设定</b>,普通账号只读。
|
||||
你随时可以在右上角点「立即采集一次」拉取本人账号的最新用量,
|
||||
也可以在下面按区间补采自己的历史数据 —— 采集始终只用<b>你自己的</b> Cookie。
|
||||
</p>
|
||||
<p class="hint">调度时刻与采集参数由管理员设定,普通账号只读。可在右上角手动采集,或按区间补采本人历史数据。</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -83,11 +76,8 @@
|
||||
data-min-days="{{ max_days }}"></label>
|
||||
<label class="row"><span>结束日期</span><input type="date" name="to" value="{{ sch.now[:10] }}" max="{{ sch.now[:10] }}"></label>
|
||||
<button class="btn" type="submit">按区间补采</button>
|
||||
<p class="hint">指定区间重新拉取云端明细,已存在的记录按 <code>RequestID</code> 去重,不会重复计入。
|
||||
<br><b>单次最长 {{ max_days }} 天</b>(超过会被服务端拒绝,请分批补),
|
||||
且<b>同一账号两次采集之间需间隔 {{ min_gap }} 秒</b>、<b>有任务在跑时不能再发起</b> ——
|
||||
这三条是为了避免把云端接口与本站线程池打满。
|
||||
手动点「立即采集一次」只走增量(从最后一条记录续拉),通常几秒完成。</p>
|
||||
<p class="hint">按 <code>RequestID</code> 去重,已存在的记录不会重复计入。单次最长 {{ max_days }} 天,
|
||||
两次采集间隔需 {{ min_gap }} 秒,有任务在跑时不能发起。「立即采集一次」走增量,通常几秒完成。</p>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="pagehead">
|
||||
<div>
|
||||
<h1>用户管理</h1>
|
||||
<p class="lead">维护账号、状态与管理员身份。单价数据<b>按账号隔离</b>——管理员也看不到别人的用量与凭证</p>
|
||||
<p class="lead">维护账号、状态与管理员身份。用量数据按账号隔离,管理员也看不到别人的用量与凭证</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="tag accent">仅管理员可见</span>
|
||||
@@ -36,8 +36,7 @@
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn primary" type="submit">创建账号</button>
|
||||
<p class="hint">密码至少 8 位且需含两类以上字符。新账号默认是<b>普通账号</b>;
|
||||
管理员身份请显式选择。无论哪种身份,都需要各自配置自己的 Cookie 才能采集。</p>
|
||||
<p class="hint">密码至少 8 位且需含两类以上字符。无论何种身份,都需要各自配置 Cookie 才能采集。</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -92,10 +91,8 @@
|
||||
</table>
|
||||
</div>
|
||||
<p class="hint">
|
||||
「停用」会让该账号<b>立刻</b>失效(每个请求都会校验状态,不必等会话过期),
|
||||
其数据与 Cookie 都保留;「删除」是<b>不可逆</b>的,会连同该账号的用量数据与 Cookie
|
||||
一起删除(接口层另有保留数据的开关,供脚本调用时指定)。
|
||||
管理员不能取消自己的管理员身份、不能停用或删除自己,也不能删掉最后一个启用的管理员。
|
||||
「停用」立即失效,数据与 Cookie 保留;「删除」不可逆,会连同用量数据与 Cookie 一起删除。
|
||||
管理员不能取消自己的管理员身份,也不能停用或删除自己。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
在新工单中引用
屏蔽一个用户