将调度时刻、采集参数等实例级配置收归管理员,普通账号仅可维护本人 Cookie 与 User-Agent。 新增 config.writable_by 作为唯一写权限入口,set_setting 强制全局键落到 user_id=0, 消除「管理员改了只有自己生效」的静默缺陷。新增 tools/check_docs.py 文档自检, smoke 断言扩至 215 项、check_live 扩至 122 项并支持普通账号越权验收, 忽略 backups/、data/*.bak* 与 legacy-v1/,版本升至 v1.3.0。
216 行
11 KiB
HTML
216 行
11 KiB
HTML
{% 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>
|
||
(<b>两者必须取自同一次请求</b>),粘贴到下面。
|
||
<br><b>请粘贴你自己账号的 Cookie</b>:采集只使用本人凭证,各账号的数据互不可见。
|
||
{% if not is_admin %}
|
||
<br><b>这一块是你唯一可以修改的配置</b> —— 其余参数与调度时刻由管理员统一设定。
|
||
{% 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 表示关闭(日常够用)。
|
||
这些是<b>实例级</b>参数,改一次对本机所有账号生效。</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 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">
|
||
<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> 表里(下发到浏览器的只是一个随机 id),
|
||
一次性使用、5 分钟过期 —— 所以答案不会随会话 Cookie 泄漏出去。
|
||
关闭验证码会显著放大被撞库与批量注册的风险,只有在前面已经有可信网关时才考虑。
|
||
</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 %}
|