文件
workbuddy-portal/workbuddy_portal/web/templates/config.html
T
wangchuanli f36149efc3 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

190 行
10 KiB
HTML
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
{% extends "base.html" %}
{% block title %}配置管理 · {{ project_title }}{% endblock %}
{% block body %}
<div class="pagehead">
<div>
<h1>配置管理</h1>
{% if is_admin %}
<p class="lead">凭证、采集参数、接口地址与注册策略都在这里;改完立即生效</p>
{% else %}
<p class="lead">这里维护<b>你自己账号</b>的云端凭证;采集参数与调度由管理员统一设定,只读展示</p>
{% endif %}
</div>
</div>
{% if s.cookie_broken %}
<div class="flash error" style="margin-bottom:16px">
已保存的 Cookie <b>无法解密</b>(通常是 <code>data/instance.json</code> 里的
<code>cookie_key</code> 被更换或文件丢失)。请重新粘贴一次;在此之前该账号的采集会失败。
</div>
{% endif %}
<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 }}
{% 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>
(两者须取自同一次请求)。
<br>采集只使用本人凭证,各账号数据互不可见。{% if not is_admin %}普通账号只有这一块可改。{% endif %}
</p>
<form id="formCred">
<label class="col">Cookie
<textarea name="cookie" rows="4" autocomplete="off" spellcheck="false"
placeholder="留空表示不修改;填 - 表示清空已保存的 Cookie"></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">
<div class="cardhead">
<h2>采集参数</h2>
{% if is_admin %}
<span class="tag accent">实例级 · 对所有账号生效</span>
{% else %}
<span class="tag mute">只读 · 仅管理员可改</span>
{% endif %}
</div>
{% if is_admin %}
<form id="formCollect">
{% set b = num_settings %}
<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>
<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>
{% else %}
<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>
<tr><th>分页大小</th><td>{{ s.page_size }} 条/页</td></tr>
<tr><th>断点回退</th><td>{{ s.rewind_minutes }} 分钟</td></tr>
<tr><th>时间漂移容差</th><td>{{ s.drift_tolerance_minutes }} 分钟</td></tr>
<tr><th>Prompt 截断</th><td>{{ s.max_prompt }} 字符{% if s.max_prompt == '0' %}(不截断){% endif %}</td></tr>
<tr><th>整日校验天数</th><td>{{ s.verify_days }} 天</td></tr>
<tr><th>请求超时</th><td>{{ s.timeout }} 秒</td></tr>
<tr><th>校验 TLS 证书</th><td>{% if s.ssl_verify != '0' %}<span class="tag ok">校验</span>{% else %}<span class="tag bad">不校验</span>{% endif %}</td></tr>
</table>
{% endif %}
</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">至少 {{ pwd_min }} 位,需包含大写字母、小写字母、数字、符号中的至少两类。修改后当前会话仍有效。</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>
{% if is_admin %}
<button class="btn" type="button" data-maint="vacuum"
title="checkpoint + VACUUM,回收删除后的空闲页(整库操作,仅管理员)">整理数据库</button>
{% endif %}
</div>
<p class="hint">补全 Prompt 需要联网并逐天重拉云端;导出与整理只动本地数据。</p>
<div class="btnrow" style="margin-top:14px">
<a class="btn ghost" href="{{ url_for('views.records_export') }}">按当前明细页筛选导出</a>
<a class="btn ghost" href="{{ url_for('views.profile_page') }}">个人中心</a>
{% if is_admin %}<a class="btn ghost" href="{{ url_for('views.users_page') }}">用户管理</a>{% endif %}
</div>
</section>
</div>
{% if is_admin %}
<section class="card">
<div class="cardhead">
<h2>实例级设置</h2>
<span class="tag accent">仅管理员可改,对所有账号生效</span>
</div>
<form id="formGlobal">
{% set b = num_settings %}
<label class="row"><span>开放自助注册</span>
<select name="allow_register">
<option value="1" {{ 'selected' if s.allow_register != '0' }}>允许任何人注册</option>
<option value="0" {{ 'selected' if s.allow_register == '0' }}>关闭注册(只能由管理员建号)</option>
</select>
</label>
<label class="row"><span>同 IP 每日注册上限</span>
<input name="register_max_per_ip" type="number"
min="{{ b.register_max_per_ip[0] }}" max="{{ b.register_max_per_ip[1] }}"
value="{{ s.register_max_per_ip }}">
<em class="unit">{{ b.register_max_per_ip[0] }}~{{ b.register_max_per_ip[1] }} 个/天</em></label>
<label class="row"><span>验证码策略</span>
<select name="captcha_policy">
<option value="always" {{ 'selected' if s.captcha_policy == 'always' }}>始终要求(推荐)</option>
<option value="adaptive" {{ 'selected' if s.captcha_policy == 'adaptive' }}>仅连续失败后要求(对日常更友好)</option>
<option value="off" {{ 'selected' if s.captcha_policy == 'off' }}>关闭(不推荐)</option>
</select>
</label>
<label class="row"><span>验证码位数</span>
<input name="captcha_length" type="number"
min="{{ b.captcha_length[0] }}" max="{{ b.captcha_length[1] }}"
value="{{ s.captcha_length }}">
<em class="unit">4~6 位</em></label>
<button class="btn primary" type="submit">保存实例设置</button>
<p class="hint">验证码答案只存在服务端 <code>captchas</code> 表,一次性使用、5 分钟过期。关闭会显著放大撞库与批量注册的风险。</p>
</form>
</section>
{% endif %}
{% 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 : '两次输入的新密码不一致'});
var fg = document.querySelector('#formGlobal');
if (fg) WBU.bindForm('#formGlobal', '/api/settings');
WBU.bindMaint('[data-maint]');
</script>
{% endblock %}