初始化 telegram-downloader 并接入群晖 CI/CD
部署到群晖 / deploy (push) Failing after 10m45s

This commit is contained in:
yuming
2026-04-22 21:29:03 +08:00
commit cf40343c51
153 changed files with 33376 additions and 0 deletions
+670
View File
@@ -0,0 +1,670 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>设置 — Telegram 下载器</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0d1117;
--surface: #161b22;
--surface2: #1c2128;
--border: #30363d;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
--text: #e6edf3;
--muted: #8b949e;
--hdr-h: 52px;
}
html, body { height: 100%; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5; }
/* ── Header ── */
.hdr {
position: fixed; top: 0; left: 0; right: 0; height: var(--hdr-h);
background: var(--surface); border-bottom: 1px solid var(--border);
display: flex; align-items: center; padding: 0 20px; gap: 12px; z-index: 200;
}
.hdr-back {
display: flex; align-items: center; gap: 5px; color: var(--muted);
text-decoration: none; font-size: 13px; padding: 4px 10px;
border: 1px solid var(--border); border-radius: 6px; transition: all .15s;
}
.hdr-back:hover { color: var(--accent); border-color: var(--accent); }
.hdr-title { font-size: 15px; font-weight: 700; color: var(--accent); }
/* ── Main Layout ── */
.main { margin-top: var(--hdr-h); display: flex; height: calc(100vh - var(--hdr-h)); }
/* ── Sidebar Tabs ── */
.sidebar {
width: 180px; flex-shrink: 0;
background: var(--surface); border-right: 1px solid var(--border);
padding: 16px 8px; display: flex; flex-direction: column; gap: 4px;
}
.stab {
display: flex; align-items: center; gap: 8px;
padding: 9px 12px; border-radius: 7px; cursor: pointer;
font-size: 13px; color: var(--muted); border: none; background: none;
text-align: left; transition: all .12s; width: 100%;
}
.stab:hover { background: rgba(88,166,255,.07); color: var(--text); }
.stab.active { background: rgba(88,166,255,.12); color: var(--accent); font-weight: 600; }
.stab-icon { font-size: 15px; flex-shrink: 0; }
/* ── Content ── */
.content { flex: 1; overflow-y: auto; padding: 24px; }
.tab-panel { display: none; max-width: 800px; }
.tab-panel.active { display: block; }
/* ── Card ── */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; margin-bottom: 16px;
}
.card-hd {
padding: 12px 16px; border-bottom: 1px solid var(--border);
font-size: 13px; font-weight: 600; display: flex; align-items: center;
justify-content: space-between;
}
.card-bd { padding: 16px; }
/* ── Form Controls ── */
.field { margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field-label { display: block; font-size: 11px; color: var(--muted);
margin-bottom: 5px; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; }
.field-hint { font-size: 11px; color: var(--muted); margin-top: 4px; }
.field-input {
width: 100%; padding: 7px 10px; background: var(--bg);
border: 1px solid var(--border); border-radius: 6px;
color: var(--text); font-size: 13px; transition: border-color .15s;
}
.field-input:focus { outline: none; border-color: var(--accent); }
.field-input-sm { width: 120px; }
select.field-input { cursor: pointer; }
/* ── Buttons ── */
.btn {
padding: 7px 16px; border-radius: 6px; font-size: 13px; font-weight: 500;
cursor: pointer; border: none; display: inline-flex; align-items: center;
gap: 5px; transition: opacity .15s; white-space: nowrap;
}
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn-primary { background: var(--accent); color: #0d1117; }
.btn-primary:hover:not(:disabled) { opacity: .85; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--muted); }
.btn-outline:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.btn-danger { background: transparent; border: 1px solid rgba(248,81,73,.4); color: var(--red); }
.btn-danger:hover:not(:disabled) { background: rgba(248,81,73,.1); }
.btn-sm { padding: 4px 10px; font-size: 12px; }
/* ── Badges ── */
.badge {
display: inline-flex; align-items: center; padding: 2px 7px;
border-radius: 4px; font-size: 11px; font-weight: 600;
}
.badge-green { background: rgba(63,185,80,.15); color: var(--green); }
.badge-orange { background: rgba(210,153,34,.15); color: var(--orange); }
.badge-red { background: rgba(248,81,73,.15); color: var(--red); }
.badge-blue { background: rgba(88,166,255,.15); color: var(--accent); }
.badge-muted { background: rgba(139,148,158,.12); color: var(--muted); }
/* ── Log List ── */
.log-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.log-list {
font-family: 'SFMono-Regular', Consolas, monospace; font-size: 12px;
line-height: 1.6; overflow-x: auto;
}
.log-line { padding: 2px 0; border-bottom: 1px solid rgba(48,54,61,.5); white-space: pre-wrap; word-break: break-all; }
.log-line:last-child { border-bottom: none; }
.log-err { color: var(--red); }
.log-warn { color: var(--orange); }
.log-info { color: var(--text); }
.log-debug { color: var(--muted); }
.log-empty { text-align: center; padding: 24px; color: var(--muted); font-size: 13px; }
/* ── Table ── */
.tbl-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border);
color: var(--muted); font-weight: 600; font-size: 11px; white-space: nowrap; }
td { padding: 7px 10px; border-bottom: 1px solid rgba(48,54,61,.6); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(88,166,255,.04); }
.td-truncate { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ── Pagination ── */
.pager { display: flex; align-items: center; gap: 6px; margin-top: 14px;
font-size: 12px; color: var(--muted); }
.pager-btn {
padding: 4px 10px; border-radius: 5px; background: transparent;
border: 1px solid var(--border); color: var(--muted);
cursor: pointer; font-size: 12px; transition: all .12s;
}
.pager-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.pager-btn:disabled { opacity: .4; cursor: default; }
.pager-info { flex: 1; text-align: center; }
.pager-jump { display: flex; align-items: center; gap: 5px; }
.pager-jump input { width: 54px; padding: 3px 7px; background: var(--bg);
border: 1px solid var(--border); border-radius: 5px; color: var(--text);
font-size: 12px; text-align: center; }
.pager-jump input:focus { outline: none; border-color: var(--accent); }
/* ── DB Search Bar ── */
.search-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.search-bar .field-input { width: auto; flex: 1; min-width: 120px; }
.search-bar select.field-input { width: auto; }
/* ── Status/Session ── */
.info-row { display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid var(--border); }
.info-row:last-child { border-bottom: none; }
.info-k { font-size: 12px; color: var(--muted); }
.info-v { font-size: 12px; }
/* ── Proxy Fields ── */
.proxy-grid { display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px; }
/* ── Toast ── */
.toasts { position: fixed; top: 60px; right: 14px; display: flex;
flex-direction: column; gap: 7px; z-index: 999; pointer-events: none; }
.toast { padding: 9px 16px; border-radius: 7px; font-size: 13px; animation: tin .2s ease; }
.toast-ok { background: rgba(63,185,80,.92); color: #0d1117; }
.toast-err { background: rgba(248,81,73,.92); color: #fff; }
@keyframes tin { from { opacity:0; transform:translateX(16px); } to { opacity:1; transform:none; } }
/* ── Spinner ── */
.spin { display: inline-block; width: 11px; height: 11px;
border: 2px solid rgba(255,255,255,.3); border-radius: 50%;
border-top-color: currentColor; animation: sp 1s linear infinite; }
@keyframes sp { to { transform: rotate(360deg); } }
/* ── Confirm Modal ── */
.modal-mask { display: none; position: fixed; inset: 0;
background: rgba(0,0,0,.7); z-index: 500;
align-items: center; justify-content: center; }
.modal-mask.on { display: flex; }
.modal-box { background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 24px; max-width: 380px; width: 90%; }
.modal-title { font-size: 15px; font-weight: 700; margin-bottom: 8px; }
.modal-body { font-size: 13px; color: var(--muted); margin-bottom: 20px; line-height: 1.6; }
.modal-foot { display: flex; gap: 8px; justify-content: flex-end; }
</style>
</head>
<body>
<!-- Header -->
<header class="hdr">
<a href="/" class="hdr-back">← 返回主页</a>
<span style="color:var(--border);">|</span>
<div class="hdr-title">⚙ 设置</div>
</header>
<div class="toasts" id="toasts"></div>
<!-- 确认弹窗 -->
<div class="modal-mask" id="confirm-modal">
<div class="modal-box">
<div class="modal-title" id="modal-title">确认操作</div>
<div class="modal-body" id="modal-body"></div>
<div class="modal-foot">
<button class="btn btn-outline btn-sm" onclick="closeConfirm()">取消</button>
<button class="btn btn-danger btn-sm" id="modal-ok-btn" onclick="doConfirm()">确认</button>
</div>
</div>
</div>
<div class="main">
<!-- Sidebar -->
<nav class="sidebar">
<button class="stab active" onclick="switchTab('general')"><span class="stab-icon"></span>常规设置</button>
<button class="stab" onclick="switchTab('logs')"><span class="stab-icon">📋</span>运行日志</button>
<button class="stab" onclick="switchTab('db')"><span class="stab-icon">🗄</span>下载记录</button>
<button class="stab" onclick="switchTab('account')"><span class="stab-icon">👤</span>账户管理</button>
</nav>
<!-- Content -->
<div class="content">
<!-- Tab: 常规设置 -->
<div class="tab-panel active" id="tab-general">
<div class="card">
<div class="card-hd">下载设置</div>
<div class="card-bd">
<div class="field">
<label class="field-label">并发下载数量</label>
<input class="field-input field-input-sm" type="number" id="g-max-task" min="1" max="20" value="5">
<div class="field-hint">同时下载的文件数,范围 1–20,修改后自动重启生效</div>
</div>
<div class="field">
<label class="field-label">日志级别</label>
<select class="field-input field-input-sm" id="g-log-level">
<option value="INFO">INFO(推荐)</option>
<option value="DEBUG">DEBUG(详细)</option>
<option value="WARNING">WARNING(仅警告)</option>
</select>
<div class="field-hint">修改后重启生效</div>
</div>
<div class="field">
<label class="field-label">文件保存路径</label>
<input class="field-input" type="text" id="g-save-path" placeholder="/path/to/downloads">
</div>
</div>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-primary" id="g-save-btn" onclick="saveGeneral()">💾 保存并重启</button>
</div>
</div>
<!-- Tab: 运行日志 -->
<div class="tab-panel" id="tab-logs">
<div class="card">
<div class="card-hd">
<span>运行日志</span>
<span id="log-total-badge" style="font-size:11px;color:var(--muted);"></span>
</div>
<div class="card-bd">
<div class="log-toolbar">
<select class="field-input" style="width:130px;" id="log-level-filter">
<option value="">全部级别</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="DEBUG">DEBUG</option>
</select>
<input class="field-input" style="flex:1;min-width:140px;" type="text"
id="log-keyword" placeholder="关键词搜索…" onkeydown="if(event.key==='Enter')loadLogs(1)">
<button class="btn btn-outline btn-sm" onclick="loadLogs(1)">🔍 搜索</button>
<button class="btn btn-outline btn-sm" onclick="loadLogs(logPage)">↺ 刷新</button>
</div>
<div id="log-list" class="log-list"><div class="log-empty">加载中…</div></div>
<div class="pager" id="log-pager" style="display:none;">
<button class="pager-btn" id="log-prev" onclick="loadLogs(logPage-1)"> 上一页</button>
<div class="pager-info" id="log-pager-info"></div>
<div class="pager-jump">
跳转 <input type="number" id="log-jump-input" min="1" onkeydown="if(event.key==='Enter')loadLogs(+this.value)">
</div>
<button class="pager-btn" id="log-next" onclick="loadLogs(logPage+1)">下一页 </button>
</div>
</div>
</div>
</div>
<!-- Tab: 下载记录 -->
<div class="tab-panel" id="tab-db">
<div class="card">
<div class="card-hd">
<span>下载记录</span>
<span id="db-total-badge" style="font-size:11px;color:var(--muted);"></span>
</div>
<div class="card-bd">
<div class="search-bar">
<input class="field-input" type="text" id="db-chat" placeholder="频道ID/名称">
<input class="field-input" type="text" id="db-filename" placeholder="文件名">
<select class="field-input" id="db-status">
<option value="">全部状态</option>
<option value="success">success</option>
<option value="skip">skip</option>
</select>
<select class="field-input" id="db-mediatype">
<option value="">全部类型</option>
<option value="audio">audio</option>
<option value="video">video</option>
<option value="photo">photo</option>
<option value="document">document</option>
</select>
<input class="field-input" type="date" id="db-date-from" title="开始日期">
<input class="field-input" type="date" id="db-date-to" title="结束日期">
<button class="btn btn-primary btn-sm" onclick="loadDb(1)">搜索</button>
<button class="btn btn-outline btn-sm" onclick="clearDbSearch()">重置</button>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>ID</th><th>频道</th><th>消息ID</th><th>文件名</th>
<th>大小</th><th>类型</th><th>时间</th><th>状态</th><th>操作</th>
</tr>
</thead>
<tbody id="db-tbody"><tr><td colspan="9" style="text-align:center;padding:20px;color:var(--muted);">加载中…</td></tr></tbody>
</table>
</div>
<div class="pager" id="db-pager" style="display:none;">
<button class="pager-btn" id="db-prev" onclick="loadDb(dbPage-1)"> 上一页</button>
<div class="pager-info" id="db-pager-info"></div>
<div class="pager-jump">
跳转 <input type="number" id="db-jump-input" min="1" onkeydown="if(event.key==='Enter')loadDb(+this.value)">
</div>
<button class="pager-btn" id="db-next" onclick="loadDb(dbPage+1)">下一页 </button>
</div>
</div>
</div>
</div>
<!-- Tab: 账户管理 -->
<div class="tab-panel" id="tab-account">
<div class="card">
<div class="card-hd">Session 状态</div>
<div class="card-bd" id="session-info">
<div style="color:var(--muted);font-size:13px;">检测中…</div>
</div>
</div>
<div class="card">
<div class="card-hd">代理设置</div>
<div class="card-bd">
<div class="field">
<label class="field-label">代理类型</label>
<select class="field-input field-input-sm" id="p-scheme">
<option value="socks5">SOCKS5</option>
<option value="http">HTTP</option>
</select>
</div>
<div class="proxy-grid">
<div class="field">
<label class="field-label">地址</label>
<input class="field-input" type="text" id="p-host" placeholder="192.168.1.1">
</div>
<div class="field">
<label class="field-label">端口</label>
<input class="field-input" type="number" id="p-port" placeholder="7890">
</div>
<div class="field">
<label class="field-label"> </label>
<button class="btn btn-outline btn-sm" onclick="testProxy()" id="p-test-btn">🔌 测试</button>
<span id="p-test-result" style="font-size:11px;display:block;margin-top:4px;"></span>
</div>
</div>
<div class="field-hint" style="margin-bottom:12px;">留空地址则不使用代理。修改后自动重启。</div>
<button class="btn btn-primary btn-sm" id="p-save-btn" onclick="saveProxy()">💾 保存代理并重启</button>
</div>
</div>
<div class="card">
<div class="card-hd">重新配置账户</div>
<div class="card-bd">
<p style="font-size:13px;color:var(--muted);margin-bottom:14px;line-height:1.7;">
点击下方按钮将删除当前登录 session,服务重启后需在终端重新运行登录命令完成 Telegram 账号验证。<br>
<span style="color:var(--orange);">⚠ 操作不可撤销,当前下载任务将被中断。</span>
</p>
<button class="btn btn-danger" onclick="confirmClearSession()">🔑 清除 Session 并重新登录</button>
</div>
</div>
</div>
</div><!-- /content -->
</div><!-- /main -->
<script>
// ── Toast ──
function toast(msg, type = 'ok') {
const el = document.createElement('div');
el.className = 'toast toast-' + type;
el.textContent = msg;
document.getElementById('toasts').appendChild(el);
setTimeout(() => el.remove(), 3500);
}
function fmtBytes(n) {
if (!n) return '—';
n = +n;
if (n < 1024) return n + ' B';
if (n < 1024**2) return (n/1024).toFixed(1) + ' KB';
if (n < 1024**3) return (n/1024**2).toFixed(1) + ' MB';
return (n/1024**3).toFixed(2) + ' GB';
}
// ── Tab 切换 ──
const TAB_INIT = { logs: false, db: false, account: false };
function switchTab(name) {
document.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelector(`.stab[onclick*="${name}"]`).classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
if (!TAB_INIT[name]) {
TAB_INIT[name] = true;
if (name === 'logs') loadLogs(1);
if (name === 'db') loadDb(1);
if (name === 'account') loadAccountInfo();
}
}
// ════ 常规设置 ════
function loadGeneral() {
fetch('/api/settings/general').then(r => r.json()).then(d => {
document.getElementById('g-max-task').value = d.max_download_task || 5;
document.getElementById('g-log-level').value = d.log_level || 'INFO';
document.getElementById('g-save-path').value = d.save_path || '';
}).catch(() => {});
}
function saveGeneral() {
const btn = document.getElementById('g-save-btn');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 保存中…';
const body = {
max_download_task: +document.getElementById('g-max-task').value,
log_level: document.getElementById('g-log-level').value,
save_path: document.getElementById('g-save-path').value.trim(),
};
fetch('/api/settings/general', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
}).then(r => r.json()).then(d => {
if (d.success) { toast('已保存,5秒后重启…'); setTimeout(() => location.href = '/', 5000); }
else { toast(d.error || '保存失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存并重启'; }
}).catch(() => { toast('请求失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存并重启'; });
}
// ════ 运行日志 ════
let logPage = 1, logPages = 1;
function loadLogs(page) {
page = Math.max(1, Math.min(page, logPages));
logPage = page;
const level = document.getElementById('log-level-filter').value;
const kw = document.getElementById('log-keyword').value.trim();
const url = `/api/settings/logs?page=${page}&page_size=100&level=${encodeURIComponent(level)}&keyword=${encodeURIComponent(kw)}`;
document.getElementById('log-list').innerHTML = '<div class="log-empty">加载中…</div>';
fetch(url).then(r => r.json()).then(d => {
logPages = d.pages || 1;
document.getElementById('log-total-badge').textContent = `${d.total}`;
if (!d.lines || !d.lines.length) {
document.getElementById('log-list').innerHTML = '<div class="log-empty">暂无日志</div>';
document.getElementById('log-pager').style.display = 'none';
return;
}
document.getElementById('log-list').innerHTML = d.lines.map(line => {
let cls = 'log-info';
if (line.includes('| ERROR') || line.includes('|ERROR')) cls = 'log-err';
else if (line.includes('| WARNING') || line.includes('|WARNING')) cls = 'log-warn';
else if (line.includes('| DEBUG') || line.includes('|DEBUG')) cls = 'log-debug';
const escaped = line.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
return `<div class="log-line ${cls}">${escaped}</div>`;
}).join('');
const pager = document.getElementById('log-pager');
pager.style.display = 'flex';
document.getElementById('log-pager-info').textContent = `${logPage} / ${logPages}`;
document.getElementById('log-prev').disabled = logPage <= 1;
document.getElementById('log-next').disabled = logPage >= logPages;
document.getElementById('log-jump-input').value = logPage;
}).catch(() => { document.getElementById('log-list').innerHTML = '<div class="log-empty">加载失败</div>'; });
}
// ════ 下载记录 ════
let dbPage = 1, dbPages = 1;
function loadDb(page) {
page = Math.max(1, Math.min(page, dbPages));
dbPage = page;
const params = new URLSearchParams({
page, page_size: 50,
chat_id: document.getElementById('db-chat').value.trim(),
file_name: document.getElementById('db-filename').value.trim(),
status: document.getElementById('db-status').value,
media_type: document.getElementById('db-mediatype').value,
date_from: document.getElementById('db-date-from').value,
date_to: document.getElementById('db-date-to').value,
});
document.getElementById('db-tbody').innerHTML = '<tr><td colspan="9" style="text-align:center;padding:16px;color:var(--muted);">加载中…</td></tr>';
fetch('/api/settings/db_records?' + params).then(r => r.json()).then(d => {
dbPages = d.pages || 1;
document.getElementById('db-total-badge').textContent = `${d.total}`;
if (!d.records || !d.records.length) {
document.getElementById('db-tbody').innerHTML = '<tr><td colspan="9" style="text-align:center;padding:20px;color:var(--muted);">暂无记录</td></tr>';
document.getElementById('db-pager').style.display = 'none';
return;
}
document.getElementById('db-tbody').innerHTML = d.records.map(r => {
const statusBadge = r.status === 'success'
? `<span class="badge badge-green">成功</span>`
: r.status === 'skip'
? `<span class="badge badge-orange">跳过</span>`
: `<span class="badge badge-muted">${r.status||'—'}</span>`;
const undoBtn = r.status === 'skip'
? `<button class="btn btn-outline btn-sm" onclick="undoSkip('${r.chat_id}',${r.message_id},this)">撤销</button>`
: '';
return `<tr>
<td>${r.id}</td>
<td class="td-truncate" title="${r.chat_title||r.chat_id}">${r.chat_title||r.chat_id||'—'}</td>
<td>${r.message_id}</td>
<td class="td-truncate" title="${r.file_name||''}">${r.file_name||'—'}</td>
<td style="white-space:nowrap">${fmtBytes(r.file_size)}</td>
<td>${r.media_type||'—'}</td>
<td style="white-space:nowrap;font-size:11px;">${(r.download_time||'').slice(0,16)}</td>
<td>${statusBadge}</td>
<td>${undoBtn}</td>
</tr>`;
}).join('');
const pager = document.getElementById('db-pager');
pager.style.display = 'flex';
document.getElementById('db-pager-info').textContent = `${dbPage} / ${dbPages}`;
document.getElementById('db-prev').disabled = dbPage <= 1;
document.getElementById('db-next').disabled = dbPage >= dbPages;
document.getElementById('db-jump-input').value = dbPage;
}).catch(() => { document.getElementById('db-tbody').innerHTML = '<tr><td colspan="9" style="text-align:center;padding:20px;color:var(--red);">加载失败</td></tr>'; });
}
function clearDbSearch() {
['db-chat','db-filename'].forEach(id => document.getElementById(id).value = '');
['db-status','db-mediatype'].forEach(id => document.getElementById(id).value = '');
['db-date-from','db-date-to'].forEach(id => document.getElementById(id).value = '');
loadDb(1);
}
function undoSkip(chatId, msgId, btn) {
btn.disabled = true;
fetch('/api/undo_skip', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({chat_id: chatId, message_id: +msgId})
}).then(r => r.json()).then(d => {
if (d.success) { toast('已撤销跳过'); loadDb(dbPage); }
else { toast(d.error || '撤销失败', 'err'); btn.disabled = false; }
}).catch(() => { toast('请求失败', 'err'); btn.disabled = false; });
}
// ════ 账户管理 ════
function loadAccountInfo() {
fetch('/api/settings/general').then(r => r.json()).then(d => {
const p = d.proxy || {};
document.getElementById('p-scheme').value = p.scheme || 'socks5';
document.getElementById('p-host').value = p.hostname || '';
document.getElementById('p-port').value = p.port || '';
}).catch(() => {});
fetch('/api/setup_status').then(r => r.json()).then(s => {
const el = document.getElementById('session-info');
el.innerHTML = `
<div class="info-row">
<span class="info-k">Session 文件</span>
<span class="info-v">${s.has_session
? '<span class="badge badge-green">✓ 已登录</span>'
: '<span class="badge badge-red">✗ 未登录</span>'}</span>
</div>
<div class="info-row">
<span class="info-k">API 凭证</span>
<span class="info-v">${s.has_api_credentials
? '<span class="badge badge-green">✓ 已配置</span>'
: '<span class="badge badge-red">✗ 未配置</span>'}</span>
</div>`;
}).catch(() => {});
}
function testProxy() {
const btn = document.getElementById('p-test-btn');
const res = document.getElementById('p-test-result');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span>';
res.textContent = '';
fetch('/api/test_proxy', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
scheme: document.getElementById('p-scheme').value,
hostname: document.getElementById('p-host').value.trim(),
port: +document.getElementById('p-port').value || 0,
})
}).then(r => r.json()).then(d => {
res.style.color = d.success ? 'var(--green)' : 'var(--red)';
res.textContent = d.success ? ('✓ ' + d.message) : ('✗ ' + d.error);
}).catch(e => { res.style.color = 'var(--red)'; res.textContent = '✗ ' + e.message; })
.finally(() => { btn.disabled = false; btn.textContent = '🔌 测试'; });
}
function saveProxy() {
const btn = document.getElementById('p-save-btn');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 保存中…';
fetch('/api/settings/general', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
proxy: {
scheme: document.getElementById('p-scheme').value,
hostname: document.getElementById('p-host').value.trim(),
port: +document.getElementById('p-port').value || 7890,
}
})
}).then(r => r.json()).then(d => {
if (d.success) { toast('代理已保存,5秒后重启…'); setTimeout(() => location.href = '/', 5000); }
else { toast(d.error || '保存失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存代理并重启'; }
}).catch(() => { toast('请求失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存代理并重启'; });
}
// ── 确认弹窗 ──
let _confirmCallback = null;
function showConfirm(title, body, cb) {
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-body').textContent = body;
_confirmCallback = cb;
document.getElementById('confirm-modal').classList.add('on');
}
function closeConfirm() {
document.getElementById('confirm-modal').classList.remove('on');
_confirmCallback = null;
}
function doConfirm() {
closeConfirm();
if (_confirmCallback) _confirmCallback();
}
function confirmClearSession() {
showConfirm(
'确认清除 Session',
'此操作将删除当前登录凭证并重启服务。重启后需在终端运行登录命令重新完成 Telegram 账号验证。',
clearSession
);
}
function clearSession() {
fetch('/api/settings/clear_session', { method: 'POST' })
.then(r => r.json()).then(d => {
if (d.success) { toast('Session 已清除,服务即将重启…'); setTimeout(() => location.href = '/', 5000); }
else toast(d.error || '操作失败', 'err');
}).catch(() => toast('请求失败', 'err'));
}
// ── 初始化 ──
loadGeneral();
</script>
</body>
</html>