文件
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

127 行
6.7 KiB
HTML

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
{% extends "base.html" %}
{% block title %}概览 · {{ project_title }}{% endblock %}
{% block body %}
<div class="pagehead">
<div>
<h1>概览</h1>
<p class="lead">存档 {{ '{:,}'.format(totals.records) }} 条记录,覆盖 {{ totals.firstDay }} ~ {{ totals.lastDay }}</p>
</div>
<div class="actions">
<a class="btn" href="{{ url_for('views.dashboard') }}">打开用量大屏</a>
<button class="btn primary" id="btnCollect" type="button">立即采集一次</button>
</div>
</div>
<div id="collectMsg" class="flash" style="display:none"></div>
<div class="kpis">
<div class="kpi" style="--c:var(--cyan)">
<span>累计积分</span><b>{{ '%.2f'|format(totals.credits) }}</b>
<i>{{ totals.days }} 个活跃日 · 单次均价 {{ '%.2f'|format(totals.credits / totals.calls if totals.calls else 0) }}</i>
</div>
<div class="kpi" style="--c:var(--violet)">
<span>调用次数</span><b>{{ '{:,}'.format(totals.calls) }}</b>
<i>计费 {{ '{:,}'.format(totals.billableCalls) }} · 免费 {{ '{:,}'.format(totals.freeCalls) }}
({{ '%.1f'|format(totals.freeCalls / totals.calls * 100 if totals.calls else 0) }}%)</i>
</div>
<div class="kpi" style="--c:var(--green)">
<span>今日积分</span><b>{{ '%.2f'|format(today_stat.credits) }}</b>
<i>{{ today_stat.calls }} 次调用{% if today_stat.partial %} · 残日截至 {{ today_stat.partial.hhmm }}{% endif %}
{% if yesterday.credits %}
· 昨日整日 {{ '%.2f'|format(yesterday.credits) }}
{% endif %}</i>
</div>
<div class="kpi" style="--c:var(--amber)">
<span>近 30 天积分</span><b>{{ '%.2f'|format(stat30.credits) }}</b>
<i>{% if stat30.delta %}<span class="delta {{ 'dn' if stat30.delta.credits < 0 }}">环比 {{ '%+.1f'|format(stat30.delta.credits) }}%</span>
· 前一区间 {{ '%.0f'|format(stat30.prev.credits) }}{% else %}无可比区间{% endif %}</i>
</div>
<div class="kpi" style="--c:var(--blue)">
<span>模型 / 客户端</span><b>{{ totals.models }} / {{ totals.clients }}</b>
<i>按积分排序见下表</i>
</div>
<div class="kpi" style="--c:{{ 'var(--green)' if mf.health.cookie else 'var(--red)' }}">
<span>采集健康</span><b>{{ '正常' if mf.health.cookie else '缺 Cookie' }}</b>
<i>{% if mf.health.lastRunAt %}最近采集 {{ mf.health.lastRunAt[5:16] }}{% else %}尚无采集记录{% endif %}</i>
</div>
</div>
<div class="grid2">
<section class="card">
<h2>调度状态</h2>
<table class="kv">
<tr><th>调度线程</th><td>{% if sch.running %}<span class="tag ok">运行中</span> 每 {{ sch.interval }}s 轮询{% else %}<span class="tag bad">未运行</span>{% endif %}</td></tr>
<tr><th>调度开关</th><td>{% if sch.enabled %}<span class="tag ok">已启用</span>{% else %}<span class="tag bad">已停用</span>{% endif %}</td></tr>
<tr><th>每日时刻</th><td>{{ sch.times | join(' · ') if sch.times else '—' }}{% if sch.catch_up %} <span class="tag">含启动补跑</span>{% endif %}</td></tr>
<tr><th>下次执行</th><td class="mono">{{ sch.next_run or '—' }}</td></tr>
<tr><th>采集互斥锁</th><td>{% if sch.lock %}<span class="tag warn">采集中</span>{% else %}<span class="tag ok">空闲</span>{% endif %}</td></tr>
<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>
</section>
<section class="card">
<div class="cardhead">
<h2>模型消耗 TOP</h2>
<span class="hint">按积分降序,取前 {{ models|length }} 名</span>
</div>
<div class="tablewrap">
<table class="tbl">
<thead><tr><th>模型</th><th class="num">调用</th><th class="num">积分</th><th class="num">单次均价</th><th class="num">免费占比</th></tr></thead>
<tbody>
{% for m in models %}
<tr><td>{{ m.name }}</td><td class="num">{{ m.calls }}</td>
<td class="num">{{ '%.2f'|format(m.credits) }}</td>
<td class="num">{{ '%.2f'|format(m.avgPerCall) }}</td>
<td class="num">{{ '%.0f%%'|format(m.freeRate * 100) }}</td></tr>
{% else %}
<tr><td colspan="5" class="empty">还没有数据,先点右上角「立即采集一次」</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="hint">客户端构成:{% for c in clients %}{{ c.name }} {{ '%.1f%%'|format(c.credits / totals.credits * 100 if totals.credits else 0) }}{% if not loop.last %} · {% endif %}{% else %}—{% endfor %}</p>
</section>
</div>
<section class="card">
<div class="cardhead">
<h2>最近采集</h2>
<a class="btn ghost sm" href="{{ url_for('views.tasks') }}">查看全部</a>
</div>
<div class="tablewrap">
<table class="tbl">
<thead><tr><th>#</th><th>触发</th><th>开始</th><th class="num">耗时</th><th class="num">云端</th>
<th class="num">新增</th><th class="num">重复</th><th class="num">冲突</th><th class="num">总数</th><th>结论</th></tr></thead>
<tbody>
{% for r in runs %}
<tr>
{# 运行详情在「日志管理」里,而那页仅管理员可达 —— 普通用户不要给死链 #}
<td>{% if current_user().is_admin %}<a href="{{ url_for('views.logs', run=r.id) }}">{{ r.id }}</a>{% else %}{{ r.id }}{% endif %}</td>
<td><span class="tag {{ 'info' if r.trigger=='schedule' else ('accent' if r.trigger=='startup' else 'mute') }}">{{ r.trigger }}</span></td>
<td class="mono">{{ r.started_at[5:] if r.started_at else '—' }}</td>
<td class="num">{{ ((r.duration_ms or 0) / 1000) | round(1) }}s</td>
<td class="num">{{ r.fetched if r.fetched is not none else '—' }}</td>
<td class="num">{{ r.added }}</td>
<td class="num">{{ r.dup }}</td>
<td class="num">{% if r.conflicts %}<span class="tag warn">{{ r.conflicts }}</span>{% else %}0{% endif %}</td>
<td class="num">{{ r.total }}</td>
<td>{{ r.message or '—' }}</td>
</tr>
{% else %}
<tr><td colspan="10" class="empty">还没有采集记录,点右上角「立即采集一次」</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% endblock %}