Files
fanqie-go/internal/handlers/templates/status.html
T
chenxin 126a96d744 ✨ feat(database): 完善请求日志体系并为全表补充时间字段
- request_log 新增 user_agent 列(含旧表 ALTER 迁移)及 ip/status 索引,记录解码后的明文 query(含 key,不脱敏)
- 请求日志批量写入改为多值 INSERT 单次往返,替代事务逐条 Exec
- 新增按本地时区分天聚合查询,状态页展示今日请求量与近 7 天请求趋势(峰值日高亮)
- 新增 TZ 时区支持:.env 配置 TZ 后日志时间戳与状态页全部时间统一生效,二进制内嵌 tzdata 保证容器可用
- 五张表统一补齐 created_at/updated_at 字段,启动时自动补列并按业务时间回填旧行
- device_pool 的 device_header 增加唯一约束;audio_cache 写入改为 UPSERT 修复同 key 重复报错
- 状态页"最近请求"新增 UA 列与限流状态显示;config.yaml 暴露 rate_limit_qps 配置并默认启用请求日志
2026-10-09 22:08:04 +08:00

807 lines
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>番茄小说 API · 运行状态</title>
<style>
/* finesse · register=product · A=brick-tomato/warm-oat · B=system-sans+mono
* C=single-col-strip · D=none · E=warm-glass · SOUL=5 SPECTACLE=2 DENSITY=7 */
:root {
--page: #f3efe6;
--bg: #ffffff;
--panel-2: #f9f6ef;
--border: rgba(44, 30, 22, 0.07);
--ink-1: #1e1712;
--ink-2: #4a3e35;
--ink-3: #776a5b;
--accent: #a8402c;
--accent-soft: rgba(168, 64, 44, 0.10);
--on-accent: #ffffff;
--accent-2: #5c5a3a;
--accent-2-soft: rgba(92, 90, 58, 0.12);
--up: #35704a;
--up-soft: rgba(62, 122, 82, 0.12);
--up-ring: rgba(62, 122, 82, 0.40);
--down: #c22e22;
--down-soft: rgba(194, 46, 34, 0.10);
--term-bg: #1b1511;
--term-ink: #e6d9c8;
--shadow: 0 1px 3px rgba(56, 36, 24, 0.05);
--border-strong: rgba(44, 30, 22, 0.14);
--accent-line: rgba(168, 64, 44, 0.35);
--accent-shadow: 0 2px 6px rgba(168, 64, 44, 0.28);
--btn-hl: inset 0 1px 0 rgba(255, 255, 255, 0.20);
--glass: rgba(255, 255, 255, 0.62);
--glass-bar: rgba(243, 239, 230, 0.78);
--wash-1: rgba(168, 64, 44, 0.07);
--wash-2: rgba(92, 90, 58, 0.05);
--r: 18px;
--r-sm: 12px;
--mono: "SF Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--page: #16110d;
--bg: #211b16;
--panel-2: #2b241d;
--border: rgba(255, 255, 255, 0.07);
--ink-1: #f2ece3;
--ink-2: #c9bcab;
--ink-3: #9a8d7c;
--accent: #e0705a;
--accent-soft: rgba(224, 112, 90, 0.14);
--on-accent: #200e06;
--accent-2: #b5b37a;
--accent-2-soft: rgba(181, 179, 122, 0.14);
--up: #5fc489;
--up-soft: rgba(95, 196, 137, 0.14);
--up-ring: rgba(95, 196, 137, 0.40);
--down: #ff6b5a;
--down-soft: rgba(255, 107, 90, 0.13);
--term-bg: #120e0a;
--term-ink: #d9cdbd;
--shadow: none;
--border-strong: rgba(255, 255, 255, 0.13);
--accent-line: rgba(224, 112, 90, 0.42);
--accent-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
--btn-hl: inset 0 1px 0 rgba(255, 255, 255, 0.22);
--glass: rgba(33, 27, 22, 0.60);
--glass-bar: rgba(22, 17, 13, 0.74);
--wash-1: rgba(224, 112, 90, 0.08);
--wash-2: rgba(181, 179, 122, 0.045);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { overflow-x: clip; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--page);
color: var(--ink-1);
min-height: 100vh;
padding: 28px 20px 20px;
font-size: 14px;
-webkit-font-smoothing: antialiased;
}
.num { font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
/* 品牌色晕:给玻璃层提供可模糊的内容 */
body::before {
content: '';
position: fixed; inset: 0; z-index: -1; pointer-events: none;
background:
radial-gradient(640px 420px at 85% -5%, var(--wash-1), transparent 65%),
radial-gradient(760px 540px at -10% 105%, var(--wash-2), transparent 65%);
}
.page { max-width: 1120px; margin: 0 auto; }
/* ── 顶部品牌行 ─────────────────────── */
.tophead {
display: flex; align-items: center; gap: 12px;
position: sticky; top: 0; z-index: 10;
margin: -28px -20px 16px; padding: 20px 20px 14px;
background: var(--glass-bar);
-webkit-backdrop-filter: blur(14px) saturate(1.3);
backdrop-filter: blur(14px) saturate(1.3);
border-bottom: 1px solid var(--border);
}
.glyph {
width: 38px; height: 38px; border-radius: 12px; flex-shrink: 0;
background: var(--accent); color: var(--on-accent);
display: grid; place-items: center;
font-size: 18px; font-weight: 800; line-height: 1;
box-shadow: var(--btn-hl), var(--accent-shadow);
}
.tophead h1 { font-size: 18px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.3; }
.tophead .sub { display: block; font-size: 12px; font-weight: 400; color: var(--ink-3); margin-top: 1px; }
.clock { margin-left: auto; font-size: 12.5px; color: var(--ink-3); }
/* ── 卡片基底 ───────────────────────── */
.card {
background: var(--glass);
-webkit-backdrop-filter: blur(14px) saturate(1.35);
backdrop-filter: blur(14px) saturate(1.35);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 18px 20px;
box-shadow: var(--shadow);
}
.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.card-title { font-size: 13px; font-weight: 700; }
.head-side { font-size: 11.5px; color: var(--ink-3); }
/* ── 服务状态条 ─────────────────────── */
.hero { display: flex; flex-wrap: wrap; align-items: center; gap: 16px 28px; padding: 20px 22px; margin-bottom: 14px; }
.hero-status { min-width: 0; }
.live-pill {
display: inline-flex; align-items: center; gap: 7px;
font-size: 12px; font-weight: 600; color: var(--up);
background: var(--up-soft); border-radius: 999px; padding: 4px 11px;
}
.dot { width: 8px; height: 8px; border-radius: 999px; background: var(--up); animation: ripple 2.2s ease-out infinite; }
@keyframes ripple {
0% { box-shadow: 0 0 0 0 var(--up-ring); }
70%, 100% { box-shadow: 0 0 0 7px transparent; }
}
.uptime-label { font-size: 12px; color: var(--ink-3); margin: 10px 0 2px; }
.uptime { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.2; }
.hero-meta { margin-left: auto; min-width: 220px; }
.hero-meta .kv { padding: 4px 0; }
/* ── KPI 卡 ─────────────────────────── */
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 14px; margin-bottom: 14px; }
.kpi-top { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.chip-ic {
width: 30px; height: 30px; border-radius: 9px; flex-shrink: 0;
background: var(--panel-2); color: var(--ink-2);
display: grid; place-items: center;
}
.chip-ic svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 1.8; }
.kpi-label { font-size: 12px; font-weight: 600; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em; line-height: 1.5; }
.big { font-size: 24px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.2; }
.big-aside { font-size: 14px; font-weight: 600; color: var(--ink-3); letter-spacing: 0; }
.big-sub { font-size: 12px; color: var(--ink-3); margin-top: 3px; }
.kpi .kv { border-top: 1px solid var(--border); margin-top: 8px; padding-top: 8px; }
.kpi .kv:first-of-type { margin-top: 14px; }
.kv { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; min-width: 0; }
.kv .k { color: var(--ink-3); flex-shrink: 0; }
.kv .v { font-weight: 600; text-align: right; overflow-wrap: anywhere; }
/* 比例条 */
.meter { margin: 12px 0 2px; }
.meter-cap { display: flex; justify-content: space-between; font-size: 11.5px; color: var(--ink-3); margin-bottom: 5px; }
.track { height: 6px; border-radius: 999px; background: var(--panel-2); overflow: hidden; }
.pfill { display: block; height: 100%; border-radius: inherit; background: var(--accent); width: 0; transition: width 0.7s cubic-bezier(0.22, 0.61, 0.36, 1); }
.no-anim .pfill, .no-anim .sfill { transition: none !important; }
/* 状态小徽章 */
.chip { display: inline-flex; align-items: center; font-size: 11.5px; font-weight: 600; border-radius: 999px; padding: 3px 10px; border: 1px solid var(--border); }
.chip-up { background: var(--up-soft); color: var(--up); }
.chip-off { background: var(--panel-2); color: var(--ink-3); }
.chip-row { margin-top: 10px; }
/* 请求分类细条 */
.srows { margin-top: 12px; display: flex; flex-direction: column; gap: 7px; }
.srow { display: grid; grid-template-columns: 40px 1fr 48px; gap: 8px; align-items: center; }
.slabel { font-size: 11.5px; color: var(--ink-3); }
.strack { height: 5px; border-radius: 999px; background: var(--panel-2); overflow: hidden; }
.sfill { display: block; height: 100%; border-radius: inherit; background: var(--accent); width: 0; transition: width 0.7s cubic-bezier(0.22, 0.61, 0.36, 1); }
.sval { font-size: 12px; font-weight: 600; color: var(--ink-2); text-align: right; }
/* 每日请求行:结构同 srow,宽度服务端算好,不参与 .srow 的 JS 归一化 */
.drows { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border); display: flex; flex-direction: column; gap: 7px; }
.drow { display: grid; grid-template-columns: 44px 1fr 52px; gap: 8px; align-items: center; }
.dlabel { font-size: 11.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.dval { font-size: 12px; font-weight: 600; color: var(--ink-2); text-align: right; }
.drow.is-max .dlabel, .drow.is-max .dval { color: var(--accent); }
/* ── 最近请求表 ─────────────────────── */
.req-card { margin-bottom: 14px; overflow: hidden; }
.twrap { overflow-x: auto; margin: 0 -20px; padding: 0 20px; }
table { width: 100%; min-width: 640px; border-collapse: collapse; font-size: 12.5px; }
th {
text-align: left; font-weight: 600; color: var(--ink-3);
font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; line-height: 1.5;
padding: 7px 10px; border-bottom: 1px solid var(--border); white-space: nowrap;
}
td { padding: 8px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
tbody tr { transition: background 0.12s; }
tbody tr:hover { background: var(--panel-2); }
tbody tr:last-child td { border-bottom: none; }
td.code { font-family: var(--mono); font-size: 12px; }
td.r, th.r { text-align: right; }
.q { color: var(--ink-3); font-size: 11.5px; }
.path-cell { max-width: 380px; overflow: hidden; text-overflow: ellipsis; }
.mth {
font-family: var(--mono); font-size: 10.5px; font-weight: 700; letter-spacing: 0.03em;
background: var(--accent-2-soft); color: var(--accent-2);
padding: 2px 7px; border-radius: 6px;
border: 1px solid var(--border);
}
.stc { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; font-family: var(--mono); font-size: 11.5px; }
.stc i { width: 7px; height: 7px; border-radius: 999px; }
.stc-ok { color: var(--up); } .stc-ok i { background: var(--up); }
.stc-err { color: var(--down); } .stc-err i { background: var(--down); }
.empty { padding: 30px 10px; text-align: center; }
.empty .e-title { font-size: 13px; font-weight: 700; margin-bottom: 6px; }
.empty .e-desc { font-size: 12px; color: var(--ink-3); line-height: 1.6; }
.empty code { font-family: var(--mono); font-size: 11.5px; background: var(--panel-2); border-radius: 6px; padding: 1px 6px; }
/* ── API 文档 ───────────────────────── */
.docs details { border-top: 1px solid var(--border); }
.docs details:first-of-type { border-top: none; }
.docs summary {
list-style: none; display: flex; align-items: center; gap: 10px;
padding: 12px 8px; margin: 0 -8px; border-radius: 10px; cursor: pointer;
}
.docs summary::-webkit-details-marker { display: none; }
.docs summary:hover { background: var(--panel-2); }
.chev { display: grid; place-items: center; color: var(--ink-3); transition: transform 0.2s ease; }
.chev svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
details[open] .chev { transform: rotate(90deg); }
.grp-name { font-size: 13.5px; font-weight: 700; }
.cnt {
font-size: 11px; font-weight: 600; color: var(--ink-3);
background: var(--panel-2); border-radius: 999px; padding: 1px 8px;
border: 1px solid var(--border);
}
.eps { padding: 2px 0 10px; }
.api-endpoint { padding: 10px 10px; margin: 0 -10px 2px; border-top: 1px solid var(--border); border-radius: 10px; }
.api-endpoint:first-child { border-top: none; }
.api-endpoint:hover { background: var(--panel-2); }
.ep-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; min-width: 0; }
.api-path { font-family: var(--mono); font-size: 13px; font-weight: 700; letter-spacing: -0.01em; }
.ep-desc { font-size: 12px; color: var(--ink-3); }
.api-params { display: flex; flex-wrap: wrap; gap: 5px 16px; margin-top: 8px; }
.param-item { display: inline-flex; align-items: baseline; gap: 6px; font-size: 12px; }
.param-name { font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--accent); }
.req, .opt { font-size: 10.5px; font-weight: 700; border-radius: 5px; padding: 1px 6px; border: 1px solid var(--border); }
.req { background: var(--accent-soft); color: var(--accent); }
.opt { background: var(--panel-2); color: var(--ink-3); }
.param-desc { color: var(--ink-3); font-size: 11.5px; }
/* ── 行内测试 ───────────────────────── */
.test-btn {
margin-left: auto; font-size: 11.5px; font-weight: 600; white-space: nowrap;
color: var(--accent); background: var(--bg);
border: 1px solid var(--accent-line); border-radius: 999px;
padding: 4px 13px; cursor: pointer;
box-shadow: var(--shadow);
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s, transform 0.1s;
}
.test-btn:hover, .test-btn.on { background: var(--accent-soft); border-color: var(--accent); }
.test-btn:active { transform: translateY(1px); }
.test-box { margin-top: 10px; padding: 12px; background: var(--panel-2); border: 1px solid var(--border); border-radius: var(--r-sm); }
.test-param { display: flex; align-items: center; gap: 8px; margin: 5px 0; }
.test-param > span {
width: 110px; flex-shrink: 0; font-size: 11.5px; color: var(--ink-3);
font-family: var(--mono);
}
.test-param input {
flex: 1; min-width: 0; padding: 7px 10px; font-size: 12.5px; font-family: var(--mono);
border: 1px solid var(--border); border-radius: 9px;
background: var(--bg); color: var(--ink-1); outline: none;
transition: border-color 0.12s, box-shadow 0.12s;
}
.test-param input:hover:not(:focus) { border-color: var(--border-strong); }
.test-param input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.test-send {
margin-top: 9px; padding: 7px 18px; font-size: 12.5px; font-weight: 700;
background: var(--accent); color: var(--on-accent);
border: none; border-radius: 9px; cursor: pointer;
box-shadow: var(--btn-hl), var(--accent-shadow);
transition: filter 0.15s, box-shadow 0.15s, transform 0.1s;
}
.test-send:hover { filter: brightness(1.07); }
.test-send:active { transform: translateY(1px); box-shadow: var(--btn-hl); }
.test-result {
margin-top: 10px; max-height: 300px; overflow: auto;
background: var(--term-bg); color: var(--term-ink);
padding: 12px; border-radius: 10px;
font-family: var(--mono); font-size: 11.5px; line-height: 1.6;
white-space: pre-wrap; word-break: break-all;
}
.twrap, .test-result { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
.twrap::-webkit-scrollbar, .test-result::-webkit-scrollbar { width: 8px; height: 8px; }
.twrap::-webkit-scrollbar-track, .test-result::-webkit-scrollbar-track { background: transparent; }
.twrap::-webkit-scrollbar-thumb, .test-result::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; }
/* ── 页脚与焦点 ─────────────────────── */
.foot { font-size: 11.5px; color: var(--ink-3); text-align: center; margin: 20px 0 4px; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 640px) {
body { padding: 20px 14px 14px; }
.card { padding: 16px; border-radius: 16px; }
.twrap { margin: 0 -16px; padding: 0 16px; }
.uptime { font-size: 21px; }
.hero-meta { margin-left: 0; width: 100%; }
.ep-desc { display: none; }
.tophead { margin: -20px -14px 14px; padding: 12px 16px 10px; }
.kpis { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
}
/* 不支持毛玻璃时退回实色卡片 */
@supports not (backdrop-filter: blur(1px)) {
:root { --glass: #ffffff; --glass-bar: #f3efe6; }
}
@media (prefers-color-scheme: dark) {
@supports not (backdrop-filter: blur(1px)) {
:root { --glass: #211b16; --glass-bar: #16110d; }
}
}
@media (prefers-reduced-transparency: reduce) {
body::before { display: none; }
.card, .tophead { -webkit-backdrop-filter: none; backdrop-filter: none; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
</style>
<script type="text/javascript">
var RM = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
var firstLoad = true;
window.onload = function () {
enhance();
injectTestButtons();
setInterval(autoRefresh, 3000);
};
// 自动刷新:只更新数据区(状态条/KPI/最近请求),不清空文档区和正在使用的测试表单
function autoRefresh() {
fetch(window.location.href)
.then(function (response) { return response.text(); })
.then(function (html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var fresh = doc.getElementById('stats');
if (!fresh) return;
var stats = document.getElementById('stats');
stats.classList.add('no-anim'); // 刷新时条形图直接落位,不重播生长动画
stats.innerHTML = fresh.innerHTML;
firstLoad = false;
enhance();
requestAnimationFrame(function () {
requestAnimationFrame(function () { stats.classList.remove('no-anim'); });
});
})
.catch(function (err) { console.error('刷新失败:', err); });
}
// 条形图宽度 + 首次载入的数字滚动
function enhance() {
var rows = document.querySelectorAll('.srow');
var max = 1, i;
for (i = 0; i < rows.length; i++) max = Math.max(max, +rows[i].getAttribute('data-count') || 0);
for (i = 0; i < rows.length; i++) {
var v = +rows[i].getAttribute('data-count') || 0;
rows[i].querySelector('.sfill').style.width = (v / max * 100) + '%';
}
document.querySelectorAll('.pfill').forEach(function (el) {
var a = +el.getAttribute('data-ratio'), b = +el.getAttribute('data-of') || 1;
if (isFinite(a)) el.style.width = Math.max(0, Math.min(100, a / b * 100)) + '%';
});
if (firstLoad && !RM) {
document.querySelectorAll('[data-count-num]').forEach(function (el) {
var target = +el.getAttribute('data-count-num') || 0;
if (target <= 0) return;
var t0 = performance.now(), dur = 600;
(function tick(t) {
var p = Math.min(1, (t - t0) / dur);
p = 1 - Math.pow(1 - p, 3);
el.textContent = String(Math.round(target * p));
if (p < 1) requestAnimationFrame(tick);
})(t0);
});
}
}
// 为每个业务接口注入"测试"按钮(状态页自身除外)
function injectTestButtons() {
document.querySelectorAll('.api-endpoint').forEach(function (ep) {
var path = ep.querySelector('.api-path').textContent.trim();
if (path === '/status') return;
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'test-btn';
btn.textContent = '测试';
btn.onclick = function () { toggleTest(ep, path); };
ep.querySelector('.ep-head').appendChild(btn);
});
}
// 展开/收起测试表单;参数清单直接读该接口的参数文档 DOM,零重复维护
function toggleTest(ep, path) {
var existing = ep.querySelector('.test-box');
var btn = ep.querySelector('.test-btn');
if (btn) btn.classList.toggle('on', !existing); // 面板开着时按钮保持点亮
if (existing) { existing.remove(); return; }
var box = document.createElement('div');
box.className = 'test-box';
ep.querySelectorAll('.param-item').forEach(function (p) {
var name = p.querySelector('.param-name').textContent.trim();
var required = p.querySelector('.req') ? ' *' : '';
box.insertAdjacentHTML('beforeend',
'<div class="test-param"><span>' + name + required + '</span>' +
'<input data-name="' + name + '" placeholder="' + name + '"></div>');
});
if (!box.children.length) {
box.insertAdjacentHTML('beforeend', '<div class="test-param">无参数</div>');
}
var send = document.createElement('button');
send.type = 'button';
send.className = 'test-send';
send.textContent = '发送请求';
send.onclick = function () { sendTest(ep, path); };
box.appendChild(send);
var result = document.createElement('pre');
result.className = 'test-result';
box.appendChild(result);
ep.appendChild(box);
}
function sendTest(ep, path) {
var result = ep.querySelector('.test-result');
var sp = new URLSearchParams();
var key = new URLSearchParams(location.search).get('key');
if (key) sp.set('key', key); // protect_all 开启时业务接口需要密钥
ep.querySelectorAll('.test-box input[data-name]').forEach(function (i) {
if (i.value.trim() !== '') sp.set(i.dataset.name, i.value.trim());
});
result.textContent = '请求中...';
fetch(path + '?' + sp.toString())
.then(function (r) { return r.text(); })
.then(function (t) {
try { t = JSON.stringify(JSON.parse(t), null, 2); } catch (e) {}
result.textContent = t; // textContent 写入,无注入面
})
.catch(function (err) { result.textContent = '请求失败: ' + err.message; });
}
</script>
</head>
<body>
<div class="page">
<div id="stats">
<header class="tophead">
<span class="glyph" aria-hidden="true">番</span>
<div>
<h1>番茄小说 API</h1>
<span class="sub">自部署实例 · 运行状态</span>
</div>
<span class="clock num">{{.NowTime}}</span>
</header>
<section class="hero card">
<div class="hero-status">
<span class="live-pill"><i class="dot" aria-hidden="true"></i>服务运行中</span>
<div class="uptime-label">持续运行</div>
<div class="uptime num">{{.Uptime}}</div>
</div>
<div class="hero-meta">
<div class="kv"><span class="k">签名算法</span><span class="v num">{{.Signature}}</span></div>
<div class="kv"><span class="k">启动时间</span><span class="v num">{{.StartTime}}</span></div>
<div class="kv"><span class="k">限流</span><span class="v num">{{.RateLimit}}</span></div>
</div>
</section>
<div class="kpis">
<section class="card kpi">
<div class="kpi-top">
<span class="chip-ic" aria-hidden="true"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3"/></svg></span>
<span class="kpi-label">数据库</span>
</div>
<div class="big num">{{.DBTotal}}</div>
<div class="big-sub">数据库文件大小</div>
<div class="meter">
<div class="meter-cap"><span>空间占用</span><span class="num">{{printf "%.1f" .DBUsedPct}}%</span></div>
<div class="track"><i class="pfill" data-ratio="{{.DBUsedPct}}" data-of="100"></i></div>
</div>
<div class="kv"><span class="k">已用空间</span><span class="v num">{{.DBUsed}}</span></div>
<div class="kv"><span class="k">空闲空间</span><span class="v num">{{.DBFree}}</span></div>
</section>
<section class="card kpi">
<div class="kpi-top">
<span class="chip-ic" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="8" cy="15" r="4.5"/><path d="M11.5 11.5 21 2m-5 1 3 3m-6 0 3 3"/></svg></span>
<span class="kpi-label">设备密钥池</span>
</div>
<div class="big num"><span data-count-num="{{.PoolValid}}">{{.PoolValid}}</span><span class="big-aside"> / {{.PoolTotal}}</span></div>
<div class="big-sub">有效设备 / 总设备数</div>
<div class="chip-row"><span class="chip {{if eq .PoolStatus "已启用"}}chip-up{{else}}chip-off{{end}}">{{.PoolStatus}}</span></div>
<div class="meter">
<div class="meter-cap"><span>有效率</span></div>
<div class="track"><i class="pfill" data-ratio="{{.PoolValid}}" data-of="{{.PoolTotal}}"></i></div>
</div>
<div class="kv"><span class="k">无效设备</span><span class="v num">{{.PoolInvalid}}</span></div>
<div class="kv"><span class="k">当前索引</span><span class="v num">{{.PoolIndex}}</span></div>
</section>
<section class="card kpi">
<div class="kpi-top">
<span class="chip-ic" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg></span>
<span class="kpi-label">请求统计</span>
</div>
<div class="big num" data-count-num="{{.StatTotal}}">{{.StatTotal}}</div>
<div class="big-sub">累计业务请求 · 今日 <span class="num">{{.TodayCount}}</span> 次</div>
<div class="srows">
<div class="srow" data-count="{{.StatSearch}}"><span class="slabel">搜索</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatSearch}}</span></div>
<div class="srow" data-count="{{.StatDetail}}"><span class="slabel">详情</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatDetail}}</span></div>
<div class="srow" data-count="{{.StatCatalog}}"><span class="slabel">目录</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatCatalog}}</span></div>
<div class="srow" data-count="{{.StatContent}}"><span class="slabel">正文</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatContent}}</span></div>
<div class="srow" data-count="{{.StatManga}}"><span class="slabel">漫画</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatManga}}</span></div>
<div class="srow" data-count="{{.StatAudio}}"><span class="slabel">音频</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatAudio}}</span></div>
<div class="srow" data-count="{{.StatVideo}}"><span class="slabel">视频</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatVideo}}</span></div>
<div class="srow" data-count="{{.StatCategory}}"><span class="slabel">分类</span><span class="strack"><i class="sfill"></i></span><span class="sval num">{{.StatCategory}}</span></div>
</div>
{{if .DailyStats}}
<div class="drows">
{{range .DailyStats}}
<div class="drow{{if .MaxCount}} is-max{{end}}"><span class="dlabel">{{.Day}}</span><span class="strack"><i class="sfill" style="width: {{.Pct}}%"></i></span><span class="dval num">{{.Count}}</span></div>
{{end}}
</div>
{{end}}
</section>
<section class="card kpi">
<div class="kpi-top">
<span class="chip-ic" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 8h18v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8z"/><path d="M2 4h20v4H2z"/><path d="M10 12h4"/></svg></span>
<span class="kpi-label">缓存</span>
</div>
<div class="big num" data-count-num="{{.CacheCount}}">{{.CacheCount}}</div>
<div class="big-sub">正文缓存条数</div>
<div class="chip-row"><span class="chip {{if eq .CacheStatus "已启用"}}chip-up{{else}}chip-off{{end}}">{{.CacheStatus}}</span></div>
<div class="kv"><span class="k">自动清理</span><span class="v">{{.CleanupStatus}}</span></div>
<div class="kv"><span class="k">清理间隔</span><span class="v">{{.CleanupInterval}}</span></div>
</section>
</div>
<section class="card req-card">
<div class="card-head">
<span class="card-title">最近请求</span>
<span class="head-side num">20 条 · 3 秒自动刷新</span>
</div>
{{if .RecentRequests}}
<div class="twrap">
<table>
<thead>
<tr>
<th>时间</th>
<th>方法</th>
<th>路径</th>
<th>IP</th>
<th>UA</th>
<th>状态</th>
<th class="r">耗时</th>
</tr>
</thead>
<tbody>
{{range .RecentRequests}}
<tr>
<td class="num">{{.Time}}</td>
<td><span class="mth">{{.Method}}</span></td>
<td class="code path-cell" title="{{.Path}}">{{.Path}}{{if .Query}} <span class="q">?{{.Query}}</span>{{end}}</td>
<td class="code">{{.IP}}</td>
<td class="code" title="{{.UA}}">{{.UA}}</td>
<td><span class="stc {{if .IsErr}}stc-err{{else}}stc-ok{{end}}"><i aria-hidden="true"></i>{{.Status}}</span></td>
<td class="r num">{{.Duration}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="empty">
<div class="e-title">暂无请求记录</div>
<div class="e-desc">未开启请求日志,或服务启动后还没有收到业务请求。<br>可在配置文件中将 <code>server.request_log.enabled</code> 设为 <code>true</code> 后重启。</div>
</div>
{{end}}
</section>
</div>
<section class="docs card">
<div class="card-head">
<span class="card-title">API 接口文档</span>
<span class="head-side">分组可折叠 · 点"测试"直接调试</span>
</div>
<details class="grp" open>
<summary>
<span class="chev" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg></span>
<span class="grp-name">书籍相关</span>
<span class="cnt num">6</span>
</summary>
<div class="eps">
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/search</span><span class="ep-desc">搜索书籍</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">query</span><span class="req">必需</span><span class="param-desc">搜索关键字</span></div>
<div class="param-item"><span class="param-name">offset</span><span class="opt">可选</span><span class="param-desc">偏移量,默认0</span></div>
<div class="param-item"><span class="param-name">tab_type</span><span class="opt">可选</span><span class="param-desc">类型,默认3</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/detail</span><span class="ep-desc">获取书籍详情</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/info</span><span class="ep-desc">获取书籍详情(同 detail)</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/catalog</span><span class="ep-desc">获取书籍目录</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/directory/all_items</span><span class="ep-desc">获取书籍目录(同 catalog)</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/multi-detail</span><span class="ep-desc">批量获取书籍详情</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID列表,逗号分隔</span></div>
</div>
</div>
</div>
</details>
<details class="grp">
<summary>
<span class="chev" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg></span>
<span class="grp-name">内容相关</span>
<span class="cnt num">3</span>
</summary>
<div class="eps">
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/content</span><span class="ep-desc">获取章节正文内容</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">item_id</span><span class="req">必需</span><span class="param-desc">章节ID,支持路径参数或查询参数</span></div>
<div class="param-item"><span class="param-name">text_mode</span><span class="opt">可选</span><span class="param-desc">纯文本模式:1/true=纯文本,0/false=HTML</span></div>
<div class="param-item"><span class="param-name">image_mode</span><span class="opt">可选</span><span class="param-desc">图片处理:1/true=保留图片,0/false=移除图片</span></div>
<div class="param-item"><span class="param-name">review</span><span class="opt">可选</span><span class="param-desc">段评功能:1=启用,0=禁用</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/multi-content</span><span class="ep-desc">批量获取多个章节正文</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
<div class="param-item"><span class="param-name">item_ids</span><span class="req">必需</span><span class="param-desc">章节ID列表,多个ID用逗号分隔,如:123,456,789</span></div>
<div class="param-item"><span class="param-name">text_mode</span><span class="opt">可选</span><span class="param-desc">纯文本模式:1/true=纯文本,0/false=HTML</span></div>
<div class="param-item"><span class="param-name">image_mode</span><span class="opt">可选</span><span class="param-desc">图片处理:1/true=保留图片,0/false=移除图片</span></div>
<div class="param-item"><span class="param-name">ignore_failed</span><span class="opt">可选</span><span class="param-desc">忽略失败章节:1/true=忽略,0/false=不忽略</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/item-summary</span><span class="ep-desc">获取章节摘要信息</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
<div class="param-item"><span class="param-name">item_ids</span><span class="req">必需</span><span class="param-desc">章节ID列表,逗号分隔</span></div>
</div>
</div>
</div>
</details>
<details class="grp">
<summary>
<span class="chev" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg></span>
<span class="grp-name">段评相关</span>
<span class="cnt num">3</span>
</summary>
<div class="eps">
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/chapter_review</span><span class="ep-desc">获取章节评论列表</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
<div class="param-item"><span class="param-name">item_id</span><span class="req">必需</span><span class="param-desc">章节ID</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/review</span><span class="ep-desc">获取段落评论、回复</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">item_id</span><span class="req">必需</span><span class="param-desc">章节ID</span></div>
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
<div class="param-item"><span class="param-name">para</span><span class="req">必需</span><span class="param-desc">段落索引(从0开始)</span></div>
<div class="param-item"><span class="param-name">comment_id</span><span class="opt">可选</span><span class="param-desc">评论ID(获取回复时需要)</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/review_html</span><span class="ep-desc">获取段落评论HTML页面</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">item_id</span><span class="req">必需</span><span class="param-desc">章节ID</span></div>
<div class="param-item"><span class="param-name">book_id</span><span class="req">必需</span><span class="param-desc">书籍ID</span></div>
<div class="param-item"><span class="param-name">para</span><span class="req">必需</span><span class="param-desc">段落索引(从0开始)</span></div>
</div>
</div>
</div>
</details>
<details class="grp">
<summary>
<span class="chev" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg></span>
<span class="grp-name">多媒体</span>
<span class="cnt num">3</span>
</summary>
<div class="eps">
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/manga</span><span class="ep-desc">获取漫画内容</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">item_ids</span><span class="req">必需</span><span class="param-desc">章节ID</span></div>
<div class="param-item"><span class="param-name">output</span><span class="opt">可选</span><span class="param-desc">输出格式:url/proxy/data_uri,默认url</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/audio</span><span class="ep-desc">获取音频信息</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">item_ids</span><span class="req">必需</span><span class="param-desc">章节ID</span></div>
<div class="param-item"><span class="param-name">tone_id</span><span class="opt">可选</span><span class="param-desc">音色ID,默认1</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/video</span><span class="ep-desc">获取视频信息</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">item_ids</span><span class="req">必需</span><span class="param-desc">章节ID</span></div>
</div>
</div>
</div>
</details>
<details class="grp">
<summary>
<span class="chev" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg></span>
<span class="grp-name">分类</span>
<span class="cnt num">2</span>
</summary>
<div class="eps">
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/category-front</span><span class="ep-desc">获取分类首页</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">tab</span><span class="opt">可选</span><span class="param-desc">标签页,默认0</span></div>
</div>
</div>
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/category-landing</span><span class="ep-desc">获取分类详情页</span></div>
<div class="api-params">
<div class="param-item"><span class="param-name">category_id</span><span class="req">必需</span><span class="param-desc">分类ID</span></div>
<div class="param-item"><span class="param-name">offset</span><span class="opt">可选</span><span class="param-desc">偏移量,默认0</span></div>
<div class="param-item"><span class="param-name">limit</span><span class="opt">可选</span><span class="param-desc">每页数量,默认20</span></div>
</div>
</div>
</div>
</details>
<details class="grp">
<summary>
<span class="chev" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg></span>
<span class="grp-name">系统</span>
<span class="cnt num">1</span>
</summary>
<div class="eps">
<div class="api-endpoint">
<div class="ep-head"><span class="mth">GET</span><span class="api-path">/status</span><span class="ep-desc">系统状态页面(当前页面)</span></div>
</div>
</div>
</details>
</section>
<footer class="foot">番茄小说 API · 状态每 3 秒自动刷新 · 测试请求直接发往本实例</footer>
</div>
</body>
</html>