xiaoxiaxia-temp / control.html
sharween's picture
Upload folder using huggingface_hub
8cb8eee verified
Raw
History Blame Contribute Delete
65.6 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>控制面板 - sharween/xiaoxiaxia</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 15px; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
padding: 0;
}
.container { max-width: 1200px; margin: 0 auto; }
/* Header */
.header {
text-align: center;
padding: 32px 20px 24px;
margin-bottom: 28px;
background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.06);
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
.header h1 { font-size: 1.8rem; font-weight: 600; color: #fff; letter-spacing: 1px; }
.header .subtitle {
margin-top: 6px;
font-size: 0.95rem;
color: #8899b4;
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
}
/* Card base */
.card {
background: #16213e;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.06);
padding: 20px 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.2);
}
.card-header, .card-title {
font-size: 1.1rem;
font-weight: 600;
color: #c8d6e5;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
/* Status Cards Row */
.status-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.status-card {
background: #16213e;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.06);
padding: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.2);
transition: border-color 0.2s;
min-height: 100px;
}
.status-card:hover { border-color: rgba(255,255,255,0.12); }
.status-card .service-name {
font-size: 1rem;
font-weight: 600;
color: #c8d6e5;
margin-bottom: 10px;
}
.status-card .status-line {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
transition: background 0.3s;
}
.status-dot.running { background: #4ecca3; box-shadow: 0 0 6px #4ecca3; }
.status-dot.stopped { background: #e23e57; box-shadow: 0 0 6px #e23e57; }
.status-dot.unknown { background: #666; }
.status-dot.loading { background: #f0a500; animation: pulse 0.8s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.status-text {
font-size: 0.85rem;
color: #8899b4;
font-family: "SF Mono", "Fira Code", "Consolas", monospace;
}
.status-label {
font-size: 0.82rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-info { margin-top: 4px; font-size: 0.82rem; color: #667; }
/* Quick Actions */
.actions-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 0.88rem;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
text-decoration: none;
color: #e0e0e0;
background: #0f3460;
}
.btn:hover { background: #1a4a7a; transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-primary { background: #0f3460; }
.btn-primary:hover:not(:disabled) { background: #1a4a7a; }
.btn-success { background: #1b6b4a; }
.btn-success:hover:not(:disabled) { background: #248a5e; }
.btn-warning { background: #a06a00; }
.btn-warning:hover:not(:disabled) { background: #c88200; }
.btn-danger { background: #8a1f2e; }
.btn-danger:hover:not(:disabled) { background: #b0283c; }
.btn-outline {
background: transparent;
border: 1px solid #334;
}
.btn-outline:hover:not(:disabled) { background: rgba(255,255,255,0.05); }
.btn-sm { padding: 6px 14px; font-size: 0.82rem; }
/* Backup Section */
.backup-section { margin-bottom: 20px; }
.backup-section .card-body { display: flex; gap: 12px; flex-wrap: wrap; }
.backup-section .full-backup-warning {
width: 100%;
font-size: 0.82rem;
color: #f0a500;
padding: 8px 12px;
background: rgba(240,165,0,0.08);
border-radius: 6px;
border: 1px solid rgba(240,165,0,0.2);
display: flex;
align-items: center;
gap: 6px;
}
.feedback {
width: 100%;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.85rem;
display: none;
}
.feedback.show { display: block; }
.feedback.success { background: rgba(78,204,163,0.12); color: #4ecca3; border: 1px solid rgba(78,204,163,0.25); }
.feedback.error { background: rgba(226,62,87,0.12); color: #e23e57; border: 1px solid rgba(226,62,87,0.25); }
.feedback.info { background: rgba(15,52,96,0.3); color: #6cb4ee; border: 1px solid rgba(15,52,96,0.4); }
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.2);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Log Section */
.log-section { margin-bottom: 20px; }
.log-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.log-toolbar .log-status {
font-size: 0.82rem;
color: #667;
margin-left: auto;
}
.log-area {
width: 100%;
height: 500px;
overflow-y: auto;
background: #0a0a1a;
border: 1px solid rgba(255,255,255,0.06);
border-radius: 8px;
padding: 14px 16px;
font-family: "SF Mono", "Fira Code", "Consolas", "Courier New", monospace;
font-size: 0.82rem;
line-height: 1.5;
color: #4ecca3;
white-space: pre-wrap;
word-break: break-all;
}
.log-line { display: block; padding: 2px 8px; font-family: "SF Mono", "Fira Code", "Consolas", "Courier New", monospace; white-space: pre-wrap; word-break: break-all; }
.log-divider { display: block; padding: 4px 8px; font-family: "SF Mono", "Fira Code", "Consolas", "Courier New", monospace; color: #6cb4ee; font-weight: bold; border-top: 1px solid #333; margin-top: 4px; }
.log-area .log-empty {
color: #445;
text-align: center;
padding: 30px 0;
}
.log-area .log-timestamp {
color: #6cb4ee;
}
/* Scrollbar */
.log-area::-webkit-scrollbar { width: 6px; }
.log-area::-webkit-scrollbar-track { background: transparent; }
.log-area::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
.log-area::-webkit-scrollbar-thumb:hover { background: #555; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
/* Toggle switch */
.toggle-wrap {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.82rem;
color: #8899b4;
cursor: pointer;
user-select: none;
}
.toggle-track {
width: 36px;
height: 20px;
background: #333;
border-radius: 10px;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
}
.toggle-track.active { background: #0f3460; }
.toggle-track::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #ccc;
transition: all 0.2s;
}
.toggle-track.active::after {
left: 18px;
background: #fff;
}
/* Footer */
.footer {
text-align: center;
padding: 20px;
font-size: 0.8rem;
color: #445;
}
.backup-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
/* Responsive */
@media (max-width: 768px) {
body { padding: 12px; }
.header h1 { font-size: 1.4rem; }
.status-row { grid-template-columns: 1fr; }
.actions-row { flex-direction: column; }
.actions-row .btn { width: 100%; justify-content: center; }
.backup-section .card-body { flex-direction: column; }
.backup-grid { grid-template-columns: 1fr; }
.backup-section .card-body .btn { width: 100%; justify-content: center; }
.log-toolbar .log-status { margin-left: 0; width: 100%; }
}
@media (min-width: 769px) and (max-width: 1024px) {
.status-row { grid-template-columns: repeat(3, 1fr); gap: 12px; }
}
/* Backup Confirm Modal */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.show { display: flex; }
.modal-box {
background: #1a1a2e;
border: 1px solid #0f3460;
border-radius: 12px;
padding: 28px 32px;
max-width: 480px;
width: 90%;
box-shadow: 0 8px 40px rgba(0,0,0,0.5);
}
.modal-title {
font-size: 1.15rem;
font-weight: 600;
color: #f0a500;
margin-bottom: 16px;
}
.modal-body {
font-size: 0.92rem;
color: #c8d6e5;
line-height: 1.6;
margin-bottom: 24px;
}
.modal-body .last-time {
color: #6cb4ee;
font-weight: 500;
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.modal-actions .btn { min-width: 100px; justify-content: center; }
/* Sidebar */
.layout { display: flex; gap: 0; min-height: 100vh; max-width: 1400px; margin: 0 auto; }
.sidebar {
width: 220px; flex-shrink: 0;
background: #111827; border-radius: 12px 0 0 12px;
padding: 20px 0; border: 1px solid rgba(255,255,255,0.06);
}
.sidebar .sbtitle { padding: 0 20px 16px; font-size: 1.1rem; font-weight: 600; color: #fff; border-bottom: 1px solid #1f2937; margin-bottom: 8px; }
.sidebar .sbsub { padding: 0 20px 16px; font-size: 0.75rem; color: #666; margin-top: -8px; border-bottom: 1px solid #1f2937; margin-bottom: 8px; }
.sidebar .sbitem {
display: flex; align-items: center; gap: 10px;
padding: 12px 20px; cursor: pointer; color: #9ca3af;
transition: all 0.2s; font-size: 0.9rem;
}
.sidebar .sbitem:hover { background: #1f2937; color: #e0e0e0; }
.sidebar .sbitem.active { background: #1e3a5f; color: #4fc3f7; border-right: 3px solid #4fc3f7; }
.main-content { flex: 1; padding: 24px; background: #1a1a2e; border-radius: 0 12px 12px 0; border: 1px solid rgba(255,255,255,0.06); border-left: none; overflow: auto; }
.page-content { display: none; }
.page-content.active { display: block; }
@media (max-width: 768px) {
.layout { flex-direction: column; }
.sidebar { width: 100%; border-radius: 12px 12px 0 0; display: flex; flex-wrap: wrap; padding: 10px; }
.sidebar .sbtitle, .sidebar .sbsub { display: none; }
.sidebar .sbitem { padding: 8px 12px; }
.main-content { border-radius: 0 0 12px 12px; border-left: 1px solid rgba(255,255,255,0.06); border-top: none; }
}
</style>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div class="sbtitle">🔧 控制面板</div>
<div class="sbsub" id="subtitleText">sharween/xiaoxiaxia</div>
<div class="sbitem active" data-page="home">📊 主页</div>
<div class="sbitem" data-page="vlm">🧠 OpenViking VLM</div>
<div class="sbitem" data-page="hs">🔮 Hindsight</div>
</div>
<div class="main-content">
<div id="page-home" class="page-content active">
<!-- Status Cards -->
<div class="status-row" id="statusRow">
<div class="status-card" data-service="openviking">
<div class="service-name">OpenViking</div>
<div class="status-line">
<span class="status-dot unknown" id="dotOpenviking"></span>
<span class="status-text" id="infoOpenviking">--</span>
</div>
<div class="status-info" id="metaOpenviking"></div>
</div>
<div class="status-card" data-service="openclaw">
<div class="service-name">OpenClaw Gateway</div>
<div class="status-line">
<span class="status-dot unknown" id="dotOpenclaw"></span>
<span class="status-text" id="infoOpenclaw">--</span>
</div>
<div class="status-info" id="metaOpenclaw"></div>
</div>
<div class="status-card" data-service="hindsight">
<div class="service-name">Hindsight</div>
<div class="status-line">
<span class="status-dot unknown" id="dot-hs"></span>
<span class="status-text" id="info-hs">--</span>
</div>
<div class="status-info" id="meta-hs"></div>
</div>
</div>
<!-- Quick Actions -->
<div class="actions-row">
<a class="btn btn-primary" href="/studio/" target="_blank" rel="noopener">&#128279; OpenViking Studio</a>
<a class="btn btn-primary" href="/hindsight-ui/" target="_blank" rel="noopener">&#128270; Hindsight UI</a>
<a class="btn btn-primary" id="hfDatasetLink" href="#" target="_blank" rel="noopener">&#128193; HF 数据集</a>
<a class="btn btn-primary" id="hfSettingsLink" href="#" target="_blank" rel="noopener">&#9881;&#65039; HF Space 设置</a>
<button class="btn btn-outline" data-action="loadStatus">&#x1F504; 刷新状态</button>
<button class="btn btn-outline" data-action="scrollToLog">&#128203; 查看日志</button>
</div>
<!-- Backup Section -->
<div class="card backup-section">
<div class="card-title">&#128230; 备份管理</div>
<div class="card-body">
<!-- 单行备份按钮,每个按钮下方带说明 -->
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<div style="display:flex;flex-direction:column;align-items:center;">
<button class="btn btn-success backup-btn" data-service="webdav" data-type="quick">&#128179; WEBDAV 增量</button>
<span style="font-size:0.72rem;color:#667;margin-top:4px;">仅上传变更文件</span>
</div>
<div style="display:flex;flex-direction:column;align-items:center;">
<button class="btn btn-warning backup-btn" data-service="webdav" data-type="full">&#128477; WEBDAV 全量</button>
<span style="font-size:0.72rem;color:#667;margin-top:4px;">完整打包+上传</span>
</div>
<div style="display:flex;flex-direction:column;align-items:center;">
<button class="btn btn-success backup-btn" data-service="dataset" data-type="quick">&#128194; DATASET 小</button>
<span style="font-size:0.72rem;color:#667;margin-top:4px;">覆盖式快速备份</span>
</div>
<div style="display:flex;flex-direction:column;align-items:center;">
<button class="btn btn-warning backup-btn" data-service="dataset" data-type="full">&#128477; DATASET 大</button>
<span style="font-size:0.72rem;color:#667;margin-top:4px;">按日期版本保留</span>
</div>
</div>
<div class="full-backup-warning">
&#9888; 完整备份将先停止 OpenViking 服务,备份完成后重新启动
</div>
<div id="backupFeedback" class="feedback"></div>
</div>
</div>
<!-- System Operations -->
<div class="card" style="margin-bottom:20px;">
<div class="card-title">&#9881; 系统操作</div>
<div class="card-body" style="display:flex;gap:12px;flex-wrap:wrap;">
<button class="btn btn-warning" data-action="restartGateway">&#x21BB; 重启 Gateway</button>
<button class="btn btn-danger" data-action="restartSystem">&#9888; 重启系统</button>
<button class="btn btn-danger" data-action="rebuildSystem">&#128260; 重构系统</button>
<div style="width:100%;font-size:0.82rem;color:#667;padding:4px 0;">
重启 Gateway:仅重启网关进程(约10秒)|重启系统:重启容器(约2分钟)|重构系统:重启并清理缓存(约5分钟)
</div>
<div id="systemFeedback" class="feedback"></div>
</div>
</div>
<!-- Restore Section -->
<div class="card backup-section">
<div class="card-title">&#128194; 恢复管理</div>
<div class="card-body">
<div style="width:100%;display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
<select id="restoreSourceSelect" style="flex:1;min-width:150px;padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<option value="webdav">WebDAV</option>
<option value="dataset">HF Dataset</option>
</select>
<select id="restoreFileSelect" style="flex:2;min-width:250px;padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<option value="">-- 请选择备份文件 --</option>
</select>
<button class="btn btn-warning" id="restoreBtn" data-action="restoreBackup">&#128194; 恢复</button>
<button class="btn btn-outline btn-sm" data-action="loadBackupList">&#x1F504; 刷新列表</button>
</div>
<div id="restoreFeedback" class="feedback"></div>
</div>
</div>
</div>
<div id="page-vlm" class="page-content">
<div class="card" style="margin-bottom:20px;">
<div class="card-header">OpenViking VLM 模型配置</div>
<div style="margin-bottom:12px;color:#aaa;font-size:0.85rem;padding:0 24px;">
状态:<span id="ovStatus">加载中...</span>
</div>
<div style="margin-bottom:12px;padding:0 24px;">
<button class="btn btn-warning" data-action="toggleOv">启用/停用</button>
</div>
<div class="card-body">
<div style="margin-bottom:12px;color:#aaa;font-size:0.85rem;">
当前 VLM 模型:<span id="currentVlmModel">-</span>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px;">
<select id="vlmModelSelect" style="flex:1;min-width:200px;padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<option value="">-- 选择模型 --</option>
</select>
<button class="btn btn-primary" id="presetBtn" data-action="selectPresetModel" style="white-space:nowrap;">应用预设置</button>
</div>
<div style="border-top:1px solid #333;padding-top:12px;margin-top:8px;">
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">或自定义模型(填全部4个参数):</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
<input type="text" id="vlmModelId" placeholder="model_id (必填)" style="padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<input type="text" id="vlmBaseUrl" placeholder="base_url (必填)" style="padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<input type="password" id="vlmApiKey" placeholder="api_key (必填)" style="padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<select id="vlmProvider" style="padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<option value="openai">openai</option>
<option value="volcengine">volcengine</option>
<option value="litellm">litellm</option>
</select>
</div>
<div style="margin-top:8px;text-align:right;">
<button class="btn btn-primary" id="customVlmBtn" data-action="applyCustomVLM">应用自定义</button>
</div>
</div>
<div id="vlmFeedback" class="feedback" style="margin-top:8px;font-size:0.85rem;"></div>
</div>
</div>
</div>
<div id="page-hs" class="page-content">
<div class="card" style="margin-bottom:20px;">
<div class="card-header">🔮 Hindsight 记忆引擎</div>
<div class="card-body">
<div style="margin-bottom:12px;color:#aaa;font-size:0.85rem;">
状态:<span id="hsStatus">加载中...</span>
</div>
<div style="margin-bottom:12px;color:#aaa;font-size:0.85rem;">
当前 LLM 模型:<span id="hsCurrentModel">-</span>
</div>
<div style="margin-bottom:12px;">
<button class="btn btn-warning" id="hsToggleBtn" data-action="toggleHs">启用/停用</button>
</div>
<div style="border-top:1px solid #333;padding-top:12px;margin-top:8px;">
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">LLM 配置(Hindsight 专用记忆模型)</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px;">
<select id="hsLlmProvider" style="flex:1;min-width:120px;padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<option value="">-- Provider --</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="gemini">Gemini</option>
<option value="groq">Groq</option>
<option value="claude-code">Claude Code</option>
</select>
<select id="hsModelSelect" style="flex:2;min-width:200px;padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<option value="">-- 选择预设模型 --</option>
</select>
<button class="btn btn-primary" data-action="applyHsPreset" style="white-space:nowrap;">应用预设</button>
</div>
<div style="border-top:1px solid #333;padding-top:12px;">
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">或自定义填入全部参数:</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
<input type="text" id="hsLlmModel" placeholder="Model ID(必填)" style="padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<input type="password" id="hsLlmApiKey" placeholder="API Key(必填)" style="padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<input type="text" id="hsBaseUrl" placeholder="Base URL(必填)" style="padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
</div>
<div style="margin-top:8px;text-align:right;">
<button class="btn btn-primary" data-action="saveHsConfig">保存 LLM 配置</button>
</div>
</div>
</div>
<div style="border-top:1px solid #333;padding-top:12px;margin-top:8px;">
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">云端连接(可选):</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<input type="text" id="hsCloudUrl" placeholder="Hindsight Cloud URL" style="flex:2;min-width:200px;padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<input type="password" id="hsCloudToken" placeholder="Cloud API Token" style="flex:1;min-width:150px;padding:8px 12px;background:#0a0a1a;color:#fff;border:1px solid #333;border-radius:6px;">
<button class="btn btn-outline btn-sm" data-action="saveHsCloudConfig">保存云端配置</button>
</div>
</div>
<div id="hsFeedback" style="margin-top:8px;font-size:0.85rem;"></div>
</div>
</div>
</div>
<div class="card log-section">
<div class="card-title">&#128221; 备份日志</div>
<div class="log-toolbar">
<button class="btn btn-sm btn-outline" data-action="clearLog">&#128465; 清除日志</button>
<label class="toggle-wrap" id="autoScrollToggle">
<span class="toggle-track active" id="toggleTrack"></span>
自动滚动
</label>
<span class="log-status" id="logStatus">就绪</span>
</div>
<div class="log-area" id="logArea">
<span class="log-empty">等待日志加载...</span>
</div>
</div>
</div>
<!-- Backup Confirm Modal -->
<div class="modal-overlay" id="backupModal">
<div class="modal-box">
<div class="modal-title">&#9888; 备份确认</div>
<div class="modal-body">
今天已有备份记录,确定再次备份?<br>
上次备份时间:<span class="last-time" id="lastBackupTime"></span>
</div>
<div class="modal-actions">
<button class="btn btn-outline" data-action="cancelBackup">取消</button>
<button class="btn btn-warning" data-action="confirmBackup">确定备份</button>
</div>
</div>
</div>
</div>
<script>
(function () {
'use strict';
// --- State ---
var statusInterval = null;
var logInterval = null;
var isBackupRunning = false;
var isRestoringRunning = false;
var autoScroll = true;
var prevLogHash = '';
var pendingBackup = null;
// --- Constants ---
var STATUS_POLL_INTERVAL = 5000;
var LOG_POLL_INTERVAL = 3000;
var BACKUP_TIMEOUT = 120000;
var BACKUP_POLL_INTERVAL = 2000;
var FEEDBACK_HIDE_DELAY = 6000;
var backupCheckMap = {
webdav_quick: 'webdav-incremental',
webdav_full: 'webdav-full',
dataset_quick: 'dataset-small',
dataset_full: 'dataset-large'
};
// --- DOM refs ---
var logArea = document.getElementById('logArea');
var logStatus = document.getElementById('logStatus');
var toggleTrack = document.getElementById('toggleTrack');
var backupFeedback = document.getElementById('backupFeedback');
var restoreFeedback = document.getElementById('restoreFeedback');
var systemFeedback = document.getElementById('systemFeedback');
// --- Toggle auto-scroll ---
document.getElementById('autoScrollToggle').addEventListener('click', function () {
autoScroll = !autoScroll;
toggleTrack.classList.toggle('active', autoScroll);
});
// --- Helpers ---
function fmtTime(ts) {
if (!ts) return '--';
var d = new Date(ts);
if (isNaN(d.getTime())) return ts;
var pad = function (n) { return String(n).padStart(2, '0'); };
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
function showFeedback(msg, type) {
if (window._fbTimer) clearTimeout(window._fbTimer);
backupFeedback.textContent = msg;
backupFeedback.className = 'feedback show ' + type;
// 所有状态都不自动隐藏,由下一次 showFeedback 或操作替换
}
function setButtonsDisabled(disabled) {
document.querySelectorAll('.backup-btn').forEach(function(btn) {
btn.disabled = disabled;
});
}
// --- Config: fetch HF Space ID for dynamic links ---
function loadConfig() {
fetch('/control/api/config')
.then(function (r) { return r.json(); })
.then(function (data) {
var spaceId = data.space_id || 'sharween/xiaoxiaxia';
var subtitleEl = document.getElementById('subtitleText');
if (subtitleEl) subtitleEl.textContent = spaceId;
var footerEl = document.getElementById('footerSpaceId');
if (footerEl) footerEl.textContent = spaceId;
var dsLink = document.getElementById('hfDatasetLink');
var stLink = document.getElementById('hfSettingsLink');
if (dsLink) dsLink.href = 'https://huggingface.co/datasets/' + spaceId + '_data/tree/main';
if (stLink) stLink.href = 'https://huggingface.co/spaces/' + spaceId + '/settings';
})
.catch(function () {
console.error('loadConfig: fetch failed (network error)');
// fallback: keep default hrefs pointing to sharween/xiaoxiaxia
var dsLink = document.getElementById('hfDatasetLink');
var stLink = document.getElementById('hfSettingsLink');
if (dsLink) dsLink.href = 'https://huggingface.co/datasets/sharween/xiaoxiaxia_data/tree/main';
if (stLink) stLink.href = 'https://huggingface.co/spaces/sharween/xiaoxiaxia/settings';
});
}
function _renderStatusDisplay(data, dots, infos, metas) {
var services = ['openviking', 'openclaw', 'hindsight'];
services.forEach(function (s) {
var svc = data[s];
var status = 'unknown';
var dot = dots[s];
var info = infos[s];
var meta = metas[s];
if (typeof svc === 'object' && svc !== null) {
status = (svc.status || 'unknown').toLowerCase();
} else if (typeof svc === 'string') {
status = svc.toLowerCase();
}
dot.className = 'status-dot';
if (status === 'running') {
dot.classList.add('running');
info.textContent = '运行中';
} else if (status === 'stopped') {
dot.classList.add('stopped');
info.textContent = '已停止';
} else {
dot.classList.add('unknown');
info.textContent = svc && typeof svc === 'object' ? (svc.status || '未知') : (String(svc) || '未知');
}
var extras = [];
if (typeof svc === 'object' && svc !== null) {
if (svc.pid) extras.push('PID: ' + svc.pid);
if (svc.port) extras.push('Port: ' + svc.port);
if (svc.memory) extras.push('Mem: ' + svc.memory);
if (svc.uptime) extras.push('运行: ' + svc.uptime);
}
meta.textContent = extras.join(' | ') || '';
});
}
// --- Status ---
function loadStatus() {
var dots = {
openviking: document.getElementById('dotOpenviking'),
openclaw: document.getElementById('dotOpenclaw'),
hindsight: document.getElementById('dot-hs')
};
var infos = {
openviking: document.getElementById('infoOpenviking'),
openclaw: document.getElementById('infoOpenclaw'),
hindsight: document.getElementById('info-hs')
};
var metas = {
openviking: document.getElementById('metaOpenviking'),
openclaw: document.getElementById('metaOpenclaw'),
hindsight: document.getElementById('meta-hs')
};
// Show loading
Object.keys(dots).forEach(function (k) {
dots[k].className = 'status-dot loading';
if (infos[k].textContent === '--') infos[k].textContent = '加载中...';
});
fetch('/control/api/status')
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
_renderStatusDisplay(data, dots, infos, metas);
})
.catch(function (err) {
Object.keys(dots).forEach(function (k) {
dots[k].className = 'status-dot unknown';
infos[k].textContent = '连接失败';
});
console.error('Status fetch error:', err);
});
}
// Backup V2 (2x2 grid) — always prompt before backup
function triggerBackupV2(service, type) {
if (isBackupRunning) return;
setButtonsDisabled(true);
showFeedback('检查备份记录...', 'info');
pendingBackup = { service: service, type: type };
fetch('/control/api/backup/check')
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
var modalBody = document.querySelector('#backupModal .modal-body');
var msg = '确定执行备份?';
if (data.backup_today && data.last_backup) {
msg = '今天已有备份记录(' + data.last_backup + '),确定再次备份?<br>此操作将覆盖当天备份。';
} else if (data.last_backup) {
msg = '上次备份时间:' + data.last_backup + '<br>确定执行备份?';
}
modalBody.innerHTML = msg;
document.getElementById('backupModal').classList.add('show');
setButtonsDisabled(false);
})
.catch(function () {
var modalBody = document.querySelector('#backupModal .modal-body');
modalBody.innerHTML = '确定执行备份?';
document.getElementById('backupModal').classList.add('show');
setButtonsDisabled(false);
});
}
function _pollBackupCompletion(label, pollTimer, startTime) {
// 超时检查放在最外层,无论 fetch 成功与否都会执行
if (Date.now() - startTime >= BACKUP_TIMEOUT) {
clearInterval(pollTimer);
showFeedback(label + ' 备份超时,请查看日志确认', 'error');
_resetBackupState();
return;
}
fetch('/control/api/log')
.then(function (r) { return r.json(); })
.then(function (data) {
var lines = data.lines || [];
if (lines.length === 0) return;
var lastLine = (lines[lines.length - 1] || '').toLowerCase();
var recentLines = lines.slice(-10).join('\n').toLowerCase();
// 1) 检查最近日志中是否有明确的完成关键词
var completed =
recentLines.indexOf('completed successfully') >= 0 ||
recentLines.indexOf(' completed') >= 0 ||
recentLines.indexOf('备份完成') >= 0 ||
(recentLines.indexOf('complete') >= 0 &&
recentLines.indexOf('failed') < 0 &&
recentLines.indexOf('error') < 0);
if (completed) {
clearInterval(pollTimer);
showFeedback(label + ' 备份完成', 'success');
_resetBackupState();
loadLog();
return;
}
// 2) 检查最后一行是否有失败迹象(用 lastLine 避免 LiteLLM 警告误触)
if (lastLine.indexOf('failed') >= 0 ||
(lastLine.indexOf('error') >= 0 && lastLine.indexOf('litellm:warning') < 0)) {
clearInterval(pollTimer);
showFeedback(label + ' 备份失败,请查看日志', 'error');
_resetBackupState();
loadLog();
return;
}
})
.catch(function () {
// 网络错误不中断轮询,下次轮询会再试
});
}
function executeBackup(service, type) {
isBackupRunning = true;
setButtonsDisabled(true);
// 修改按钮文字为"备份中..."
document.querySelectorAll('.backup-btn').forEach(function(btn) {
btn.dataset.originalText = btn.textContent.trim();
btn.textContent = '⏳ 备份中...';
});
var specificLabels = {
'webdav_quick': 'WEBDAV 增量备份',
'webdav_full': 'WEBDAV 全量备份',
'dataset_quick': 'DATASET 小备份',
'dataset_full': 'DATASET 大备份'
};
var key = service + '_' + type;
var label = specificLabels[key] || (service.toUpperCase() + ' ' + type);
showFeedback(label + ' 备份中...', 'info');
fetch('/control/api/backup/' + service + '/' + type, { method: 'POST' })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function () {
var startTime = Date.now();
var pollTimer = setInterval(function () {
_pollBackupCompletion(label, pollTimer, startTime);
}, BACKUP_POLL_INTERVAL);
})
.catch(function (err) {
showFeedback(label + ' 备份失败: ' + err, 'error');
console.error('executeBackup error:', err);
_resetBackupState();
});
}
function _resetBackupState() {
isBackupRunning = false;
document.querySelectorAll('.backup-btn').forEach(function(btn) {
btn.disabled = false;
if (btn.dataset.originalText) {
btn.textContent = btn.dataset.originalText;
delete btn.dataset.originalText;
}
});
}
function confirmBackup() {
document.getElementById('backupModal').classList.remove('show');
if (pendingBackup) {
var pb = pendingBackup;
pendingBackup = null;
executeBackup(pb.service, pb.type);
}
}
function cancelBackup() {
document.getElementById('backupModal').classList.remove('show');
pendingBackup = null;
}
// --- Log ---
function loadLog() {
fetch('/control/api/log')
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
var lines = data.lines || [];
var text = lines.join('\n');
var hash = simpleHash(text);
if (hash === prevLogHash && logArea.children.length > 0) {
return; // no change
}
prevLogHash = hash;
if (lines.length === 0) {
logArea.innerHTML = '<span class="log-empty">暂无备份日志</span>';
return;
}
var html = lines.map(function(line) {
if (line.startsWith('===')) {
return '<div class="log-divider">' + escapeHtml(line) + '</div>';
}
return '<div class="log-line">' + escapeHtml(line) + '</div>';
}).join('');
logArea.innerHTML = html;
if (autoScroll) {
logArea.scrollTop = logArea.scrollHeight;
}
logStatus.textContent = '更新于 ' + fmtTime(Date.now());
})
.catch(function (err) {
logStatus.textContent = '日志加载失败';
console.error('Log fetch error:', err);
});
}
function simpleHash(str) {
var h = 0;
for (var i = 0; i < str.length; i++) {
h = ((h << 5) - h) + str.charCodeAt(i);
h |= 0;
}
return h;
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function clearLog() {
logArea.innerHTML = '<span class="log-empty">日志已清除</span>';
prevLogHash = '';
logStatus.textContent = '已清除';
}
function scrollToLog() {
document.querySelector('.log-section').scrollIntoView({ behavior: 'smooth' });
}
// --- VLM Models ---
function loadModels() {
fetch('/control/api/models')
.then(function(r) { return r.json(); })
.then(function(data) {
var select = document.getElementById('vlmModelSelect');
if (!select) return;
select.innerHTML = '<option value="">-- 选择模型 --</option>';
data.models.forEach(function(m) {
var opt = document.createElement('option');
opt.value = m.index;
opt.textContent = m.name + ' (' + m.id + ')';
select.appendChild(opt);
});
var curr = document.getElementById('currentVlmModel');
if (curr && data.current_vlm && data.current_vlm.model) {
curr.textContent = data.current_vlm.model;
}
// 同时填充 HS 模型预设下拉列表
var hsSelect = document.getElementById('hsModelSelect');
if (hsSelect) {
hsSelect.innerHTML = '<option value="">-- 选择预设模型 --</option>';
data.models.forEach(function(m) {
var opt = document.createElement('option');
opt.value = m.index;
opt.setAttribute('data-model', m.id);
opt.setAttribute('data-baseurl', m.base_url || '');
opt.setAttribute('data-haskey', m.has_key ? '1' : '0');
opt.textContent = m.name + ' (' + m.id + ')';
hsSelect.appendChild(opt);
});
}
})
.catch(function(err) {
console.error('Load models error:', err);
});
}
function selectPresetModel() {
var select = document.getElementById('vlmModelSelect');
var feedback = document.getElementById('vlmFeedback');
var presetBtn = document.getElementById('presetBtn');
if (!feedback || !select || !select.value) {
if (feedback) {
feedback.className = 'feedback show error';
feedback.textContent = '请选择一个预设模型';
}
return;
}
if (presetBtn) presetBtn.disabled = true;
feedback.className = 'feedback show info';
feedback.textContent = '正在应用...';
fetch('/control/api/openviking/vlm', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({model_index: parseInt(select.value)})
}).then(function(r) { return r.json(); })
.then(function(d) {
feedback.className = d.status === 'ok' ? 'feedback show success' : 'feedback show error';
feedback.textContent = d.message || 'OK';
if (presetBtn) presetBtn.disabled = false;
if (d.status === 'ok') setTimeout(loadModels, 3000);
})
.catch(function(e) {
feedback.className = 'feedback show error';
feedback.textContent = 'Error: ' + e;
console.error('selectPresetModel error:', e);
if (presetBtn) presetBtn.disabled = false;
});
}
function applyHsPreset() {
var provider = document.getElementById('hsLlmProvider').value;
var select = document.getElementById('hsModelSelect');
if (!provider || !select || !select.value) {
alert('请先选择 Provider 和预设模型');
return;
}
var idx = select.value;
fetch('/control/api/plugins/hindsight/apply-preset', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({model_index: parseInt(idx), llmProvider: provider}),
})
.then(function(r) { return r.json(); })
.then(function(d) {
document.getElementById('hsFeedback').textContent = d.message || '已应用';
document.getElementById('hsFeedback').className = 'feedback show success';
loadHsConfig();
});
}
function applyCustomVLM() {
var feedback = document.getElementById('vlmFeedback');
var customBtn = document.getElementById('customVlmBtn');
if (!feedback) return;
feedback.className = '';
feedback.textContent = '';
var modelId = document.getElementById('vlmModelId').value.trim();
var baseUrl = document.getElementById('vlmBaseUrl').value.trim();
var apiKey = document.getElementById('vlmApiKey').value.trim();
var provider = document.getElementById('vlmProvider').value;
if (!modelId || !baseUrl || !apiKey) {
feedback.className = 'feedback show error';
feedback.textContent = '请填写完整的 4 个参数(model_id、base_url、api_key、provider)';
return;
}
if (customBtn) customBtn.disabled = true;
feedback.className = 'feedback show info';
feedback.textContent = '正在应用...';
fetch('/control/api/openviking/vlm', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({model: modelId, base_url: baseUrl, api_key: apiKey, provider: provider})
}).then(function(r) { return r.json(); })
.then(function(d) {
feedback.className = d.status === 'ok' ? 'feedback show success' : 'feedback show error';
feedback.textContent = d.message || 'OK';
if (customBtn) customBtn.disabled = false;
if (d.status === 'ok') setTimeout(loadModels, 3000);
})
.catch(function(e) {
feedback.className = 'feedback show error';
feedback.textContent = 'Error: ' + e;
console.error('applyCustomVLM error:', e);
if (customBtn) customBtn.disabled = false;
});
}
// --- Restore ---
function loadBackupList() {
if (!restoreFeedback) return;
restoreFeedback.textContent = '正在加载备份列表...';
restoreFeedback.className = 'feedback show info';
fetch('/control/api/backup/list')
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
var select = document.getElementById('restoreFileSelect');
if (!select) return;
select.innerHTML = '<option value="">-- 请选择备份文件 --</option>';
var source = document.getElementById('restoreSourceSelect').value;
var files = data[source] || [];
files.forEach(function(f) {
var opt = document.createElement('option');
opt.value = f;
opt.textContent = f;
select.appendChild(opt);
});
restoreFeedback.textContent = '已加载 ' + files.length + ' 个备份文件';
restoreFeedback.className = 'feedback show success';
setTimeout(function() {
restoreFeedback.className = 'feedback';
}, 3000);
})
.catch(function(err) {
restoreFeedback.textContent = '加载备份列表失败: ' + err;
restoreFeedback.className = 'feedback show error';
console.error('loadBackupList error:', err);
});
}
// Reload file list when source select changes
document.addEventListener('change', function(e) {
if (e.target && e.target.id === 'restoreSourceSelect') {
loadBackupList();
}
});
function _resetRestoreState() {
isRestoringRunning = false;
var restoreBtn = document.getElementById('restoreBtn');
if (restoreBtn) restoreBtn.disabled = false;
}
function restoreBackup() {
if (isRestoringRunning) return;
if (!restoreFeedback) return;
var source = document.getElementById('restoreSourceSelect').value;
var filename = document.getElementById('restoreFileSelect').value;
if (!filename) {
restoreFeedback.textContent = '请先选择一个备份文件';
restoreFeedback.className = 'feedback show error';
return;
}
if (!confirm('确定要从 ' + source.toUpperCase() + ' 恢复备份文件 "' + filename + '" 吗?\n此操作将覆盖当前数据。')) {
return;
}
isRestoringRunning = true;
var restoreBtn = document.getElementById('restoreBtn');
if (restoreBtn) restoreBtn.disabled = true;
restoreFeedback.textContent = '正在恢复...';
restoreFeedback.className = 'feedback show info';
function _resetRestoreState() {
isRestoringRunning = false;
if (restoreBtn) restoreBtn.disabled = false;
}
fetch('/control/api/restore', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({source: source, filename: filename})
})
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
if (data.status === 'started') {
restoreFeedback.textContent = '恢复任务已启动,请查看日志了解进度';
restoreFeedback.className = 'feedback show success';
} else {
restoreFeedback.textContent = data.message || '恢复请求失败';
restoreFeedback.className = 'feedback show error';
}
_resetRestoreState();
})
.catch(function(err) {
restoreFeedback.textContent = '恢复请求失败: ' + err;
restoreFeedback.className = 'feedback show error';
console.error('restoreBackup error:', err);
_resetRestoreState();
});
}
// --- Gateway / System Restart ---
function restartGateway() {
if (!confirm('确定要重启 Gateway 吗?\n服务将短暂中断约 30 秒。')) return;
systemFeedback.textContent = 'Gateway 重启中(约30秒)...';
systemFeedback.className = 'feedback show info';
fetch('/control/api/gateway/restart', { method: 'POST' })
.then(function(r) { return r.json().catch(function(){ return {}; }); })
.then(function(data) {
if (data.status === 'ok') {
systemFeedback.textContent = data.message || 'Gateway 重启成功';
systemFeedback.className = 'feedback show success';
} else {
systemFeedback.textContent = data.message || 'Gateway 重启失败,请查看日志';
systemFeedback.className = 'feedback show error';
}
})
.catch(function(err) {
systemFeedback.textContent = 'Gateway 重启失败,请查看日志';
systemFeedback.className = 'feedback show error';
});
}
function restartSystem() {
if (!confirm('⚠️ 确定要重启整个系统吗?\n所有服务将中断 1-2 分钟。')) return;
if (!confirm('再次确认:系统重启将中断所有正在进行的操作!')) return;
systemFeedback.textContent = '系统重启中,页面将断开连接...';
systemFeedback.className = 'feedback show info';
fetch('/control/api/system/restart', { method: 'POST' }).catch(function(){});
}
function rebuildSystem() {
if (!confirm('⚠️ 确定要重构整个系统吗?\n这将重启容器并清理缓存,约 5 分钟。')) return;
if (!confirm('再次确认:重构将中断所有服务约 5 分钟!')) return;
systemFeedback.textContent = '系统重构中,页面将断开连接...';
systemFeedback.className = 'feedback show info';
fetch('/control/api/system/rebuild', { method: 'POST' }).catch(function(){});
}
function rebuildSystem() {
if (!confirm('⚠️ 确定要重构整个系统吗?\n这将清空构建缓存、重新构建 Docker 镜像,约 5 分钟。')) return;
if (!confirm('再次确认:重构将中断所有服务约 5 分钟!')) return;
showFeedback('系统重构中,页面将断开连接...', 'info');
fetch('/control/api/system/rebuild', { method: 'POST' }).catch(function(){});
}
// --- HS/OV Plugin Toggle ---
function toggleOv() {
if (!confirm('⚠️ 启用/停用 OpenViking 需要手动重启 Gateway 才能生效。\n建议先进行备份,再手动重启 Gateway。\n\n确定继续?')) return;
fetch('/control/api/plugins/openviking/toggle', { method: 'POST' })
.then(function(r) { return r.json(); })
.then(function(data) {
var fb = document.getElementById('vlmFeedback');
fb.textContent = data.message + '。请先备份,再到系统操作中手动重启 Gateway。';
fb.className = 'feedback show info';
});
}
function toggleHs() {
if (!confirm('⚠️ 启用/停用 Hindsight 需要手动重启 Gateway 才能生效。\n建议先进行备份,再手动重启 Gateway。\n\n确定继续?')) return;
fetch('/control/api/plugins/hindsight/toggle', { method: 'POST' })
.then(function(r) { return r.json(); })
.then(function(data) {
var fb = document.getElementById('hsFeedback');
fb.textContent = data.message + '。请先备份,再到系统操作中手动重启 Gateway。';
fb.className = 'feedback show info';
});
}
function loadHsConfig() {
fetch('/control/api/plugins/hindsight/config')
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('hsStatus').textContent = data.enabled ? '✅ 已启用' : '⛔ 已停用';
var cm = document.getElementById('hsCurrentModel');
if (cm) {
var modelName = data.llmModel || '';
cm.textContent = modelName || '未配置';
}
});
}
function loadOvConfig() {
fetch('/control/api/plugins/openviking/config')
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('ovStatus').textContent = data.enabled ? '✅ 已启用' : '⛔ 已停用';
});
}
function saveHsConfig() {
var body = {
llmModel: document.getElementById('hsLlmModel').value.trim(),
llmBaseUrl: document.getElementById('hsBaseUrl').value.trim(),
llmApiKey: document.getElementById('hsLlmApiKey').value,
llmProvider: document.getElementById('hsLlmProvider').value,
};
if (!body.llmModel) { alert('请填写 Model ID'); return; }
if (!body.llmBaseUrl) { alert('请填写 Base URL'); return; }
if (!body.llmApiKey || body.llmApiKey === '••••••••') { alert('请填写 API Key'); return; }
fetch('/control/api/plugins/hindsight/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
})
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('hsFeedback').textContent = data.message || 'OK';
});
}
function saveHsCloudConfig() {
var body = {
hindsightApiUrl: document.getElementById('hsCloudUrl').value,
hindsightApiToken: document.getElementById('hsCloudToken').value,
};
if (!body.hindsightApiToken || body.hindsightApiToken === '••••••••') delete body.hindsightApiToken;
fetch('/control/api/plugins/hindsight/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
})
.then(function(r) { return r.json(); })
.then(function(data) {
document.getElementById('hsFeedback').textContent = data.message || 'OK';
});
}
// --- Polling ---
function startPolling() {
stopPolling();
// Poll status every 5s
statusInterval = setInterval(loadStatus, STATUS_POLL_INTERVAL);
// Poll log every 3s (always, as per requirement: "Auto-refresh every 3 seconds when backup is running"
// we also poll in background to keep it fresh)
loadLog();
logInterval = setInterval(loadLog, LOG_POLL_INTERVAL);
}
function stopPolling() {
if (statusInterval) { clearInterval(statusInterval); statusInterval = null; }
if (logInterval) { clearInterval(logInterval); logInterval = null; }
}
// Expose to window for inline onclick
window.loadStatus = loadStatus;
window.triggerBackupV2 = triggerBackupV2;
window.executeBackup = executeBackup;
window.confirmBackup = confirmBackup;
window.cancelBackup = cancelBackup;
window.loadLog = loadLog;
window.clearLog = clearLog;
window.scrollToLog = scrollToLog;
window.startPolling = startPolling;
window.stopPolling = stopPolling;
window.loadConfig = loadConfig;
window.loadModels = loadModels;
window.selectPresetModel = selectPresetModel;
window.applyCustomVLM = applyCustomVLM;
window.applyHsPreset = applyHsPreset;
window.loadBackupList = loadBackupList;
window.restoreBackup = restoreBackup;
window.showPage = showPage;
window.restartGateway = restartGateway;
window.restartSystem = restartSystem;
window.rebuildSystem = rebuildSystem;
window.toggleOv = toggleOv; window.toggleHs = toggleHs;
window.saveHsConfig = saveHsConfig; window.saveHsCloudConfig = saveHsCloudConfig;
window.loadHsConfig = loadHsConfig; window.loadOvConfig = loadOvConfig;
// --- Bind data-action elements (replacing HTML onclick) ---
var actionMap = {
loadStatus: loadStatus,
scrollToLog: scrollToLog,
restoreBackup: restoreBackup,
loadBackupList: loadBackupList,
clearLog: clearLog,
selectPresetModel: selectPresetModel,
applyCustomVLM: applyCustomVLM,
applyHsPreset: applyHsPreset,
cancelBackup: cancelBackup,
confirmBackup: confirmBackup,
restartGateway: restartGateway,
restartSystem: restartSystem,
rebuildSystem: rebuildSystem,
toggleOv: toggleOv,
toggleHs: toggleHs,
saveHsConfig: saveHsConfig,
saveHsCloudConfig: saveHsCloudConfig,
};
document.querySelectorAll('[data-action]').forEach(function (el) {
var action = el.dataset.action;
if (actionMap[action]) {
el.addEventListener('click', actionMap[action]);
}
});
// --- Bind data-service/data-type backup buttons ---
document.querySelectorAll('[data-service][data-type]').forEach(function (el) {
el.addEventListener('click', function () {
triggerBackupV2(this.dataset.service, this.dataset.type);
});
});
// --- Bind data-page sidebar navigation ---
document.querySelectorAll('[data-page]').forEach(function (el) {
el.addEventListener('click', function () {
showPage(this.dataset.page);
});
});
// --- Init ---
loadConfig();
loadStatus();
loadModels();
loadHsConfig();
loadOvConfig();
startPolling();
function showPage(name) {
// Toggle pages using active class
var pages = ['home', 'vlm', 'hs'];
pages.forEach(function(p) {
var el = document.getElementById('page-' + p);
if (el) el.classList.toggle('active', p === name);
});
// Update sidebar active state
document.querySelectorAll('.sbitem').forEach(function(el) {
el.classList.toggle('active',
el.textContent.includes(name === 'home' ? '主页' : name === 'vlm' ? 'VLM' : 'Hindsight'));
});
}
})();
</script>
</body>
</html>