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 处问题
这个提交包含在:
2026-09-18 11:13:17 +08:00
父节点 3751dffef9
当前提交 f36149efc3
共修改 29 个文件,包含 1105 行新增和 558 行删除
+8 -18
查看文件
@@ -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>