224 lines
10 KiB
JavaScript
224 lines
10 KiB
JavaScript
/* Search Hub — 管理面板 */
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
let currentEditSource = null;
|
||
|
||
loadSources();
|
||
loadStatus();
|
||
loadApiDocs();
|
||
loadUsage();
|
||
|
||
// ===== 搜索源 =====
|
||
function loadSources() {
|
||
fetch('/api/sources')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
var sources = data.sources || [];
|
||
var html = '';
|
||
sources.forEach(function (s) {
|
||
var badge = s.available
|
||
? '<span class="source-badge badge-ok">可用</span>'
|
||
: '<span class="source-badge badge-fail">不可用</span>';
|
||
var keyInfo = s.needs_api_key
|
||
? (s.available ? ' | 密钥已配置' : ' | 需配置密钥')
|
||
: ' | 无需密钥';
|
||
var editBtn = s.needs_api_key
|
||
? '<button class="btn-ghost" onclick="openEditModal(\'' + s.name + '\')">编辑</button>'
|
||
: '';
|
||
html += '<div class="source-card">';
|
||
html += ' <div class="d-flex justify-content-between align-items-center">';
|
||
html += ' <div><span class="source-name">' + escapeHtml(s.display_name || s.name) + '</span></div>';
|
||
html += ' <div class="d-flex gap-3 align-items-center">' + badge + editBtn + '</div>';
|
||
html += ' </div>';
|
||
html += ' <div class="small text-muted mt-1">' + s.name + keyInfo + '</div>';
|
||
html += '</div>';
|
||
});
|
||
document.getElementById('sources-list').innerHTML = html;
|
||
});
|
||
}
|
||
|
||
// ===== 服务状态 =====
|
||
function loadStatus() {
|
||
fetch('/api/status')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
document.getElementById('hub-status').textContent = data.status === 'ok' ? '运行中' : '异常';
|
||
document.getElementById('hub-version').textContent = data.version || '-';
|
||
document.getElementById('hub-history').textContent = data.history_count || 0;
|
||
});
|
||
}
|
||
|
||
// ===== API 文档 =====
|
||
function loadApiDocs() {
|
||
fetch('/api/docs')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
var endpoints = data.endpoints || [];
|
||
var html = '';
|
||
endpoints.forEach(function (ep) {
|
||
var methodClass = ep.method === 'GET' ? 'method-get' : 'method-post';
|
||
html += '<div class="api-endpoint">';
|
||
html += ' <span class="method ' + methodClass + '">' + ep.method + '</span>';
|
||
html += ' <span class="path">' + ep.path + '</span>';
|
||
html += ' <div class="desc">' + escapeHtml(ep.desc) + '</div>';
|
||
if (ep.example) {
|
||
html += ' <button class="show-example-btn" onclick="toggleExample(this)">显示示例</button>';
|
||
html += ' <div class="example">' + escapeHtml(ep.example) + '</div>';
|
||
}
|
||
html += '</div>';
|
||
});
|
||
document.getElementById('api-docs-list').innerHTML = html;
|
||
});
|
||
}
|
||
|
||
// ===== 服务用量(异步加载,不阻塞页面) =====
|
||
function loadUsage() {
|
||
var el = document.getElementById('usage-list');
|
||
fetch('/api/admin/usage')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
var html = '';
|
||
|
||
// Tavily(优先显示 account 级别用量)
|
||
var t = data.tavily || {};
|
||
if (t.available) {
|
||
var usage = t.key && t.key.limit > 0 ? t.key.usage : (t.account ? t.account.search_usage : 0);
|
||
var limit = t.key && t.key.limit > 0 ? t.key.limit : (t.account ? t.account.plan_limit : 0);
|
||
var remaining = t.key && t.key.limit > 0 ? t.key.remaining : (limit - usage);
|
||
var pct = limit > 0 ? Math.round(usage / limit * 100) : 0;
|
||
html += '<div class="info-chip">';
|
||
html += ' <strong>Tavily</strong>';
|
||
html += ' <div style="margin-top:4px">已用: ' + usage + ' / ' + limit;
|
||
html += ' <span style="margin-left:8px;color:' + (pct > 80 ? '#ef4444' : '#10b981') + '">剩余: ' + remaining + '</span></div>';
|
||
if (t.account) {
|
||
html += ' <div class="small text-muted" style="margin-top:2px">账户搜索: ' + t.account.search_usage + ' / ' + t.account.plan_limit;
|
||
if (t.account.paygo_usage > 0) html += ' | 按量: ' + t.account.paygo_usage;
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
} else {
|
||
html += '<div class="info-chip"><strong>Tavily</strong><br><span class="text-muted">' + (t.error || '未配置') + '</span></div>';
|
||
}
|
||
|
||
// 百度 VDB
|
||
var b = data.baidu_vdb || {};
|
||
if (b.available) {
|
||
html += '<div class="info-chip" style="margin-top:6px">';
|
||
html += ' <strong>百度 VDB</strong>';
|
||
html += ' <div style="margin-top:4px">免费额度: ' + b.freeQuota + '</div>';
|
||
html += '</div>';
|
||
} else {
|
||
html += '<div class="info-chip" style="margin-top:6px"><strong>百度 VDB</strong><br><span class="text-muted">' + (b.error || '未配置') + '</span></div>';
|
||
}
|
||
|
||
el.innerHTML = html;
|
||
})
|
||
.catch(function () {
|
||
el.innerHTML = '<div class="info-chip text-muted">用量查询暂不可用</div>';
|
||
});
|
||
}
|
||
|
||
window.toggleExample = function (btn) {
|
||
var example = btn.nextElementSibling;
|
||
if (example) {
|
||
example.classList.toggle('visible');
|
||
btn.textContent = example.classList.contains('visible') ? '收起' : '显示示例';
|
||
}
|
||
};
|
||
|
||
// ===== 编辑弹窗 =====
|
||
window.openEditModal = function (name) {
|
||
currentEditSource = name;
|
||
var modal = document.getElementById('edit-modal');
|
||
var title = document.getElementById('edit-modal-title');
|
||
var body = document.getElementById('edit-modal-body');
|
||
title.textContent = '加载中...';
|
||
body.innerHTML = '<div class="form-hint">加载中...</div>';
|
||
modal.style.display = 'flex';
|
||
|
||
fetch('/api/admin/sources/' + name)
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
title.textContent = '编辑: ' + (data.display_name || name);
|
||
var html = '';
|
||
if (data.fields.length === 0) {
|
||
html = '<div class="form-hint">该搜索源无需配置</div>';
|
||
} else {
|
||
data.fields.forEach(function (f) {
|
||
html += '<div class="form-group">';
|
||
html += ' <label>' + escapeHtml(f.label) + (f.required ? ' <span class="text-danger">*</span>' : '') + '</label>';
|
||
if (f.type === 'password') {
|
||
html += ' <input type="password" class="form-control" id="cfg-' + f.key + '"';
|
||
html += ' placeholder="' + (f.has_value ? '已配置,输入新值覆盖' : '请输入') + '">';
|
||
if (f.has_value) {
|
||
html += ' <div class="form-hint">当前: ' + escapeHtml(f.value) + '</div>';
|
||
}
|
||
} else {
|
||
html += ' <input type="text" class="form-control" id="cfg-' + f.key + '"';
|
||
html += ' value="' + escapeHtml(f.value || '') + '">';
|
||
}
|
||
html += '</div>';
|
||
});
|
||
}
|
||
body.innerHTML = html;
|
||
})
|
||
.catch(function (err) {
|
||
body.innerHTML = '<div class="text-danger">加载失败: ' + err.message + '</div>';
|
||
});
|
||
};
|
||
|
||
window.closeEditModal = function () {
|
||
document.getElementById('edit-modal').style.display = 'none';
|
||
currentEditSource = null;
|
||
};
|
||
|
||
window.saveConfig = function () {
|
||
if (!currentEditSource) return;
|
||
var body = document.getElementById('edit-modal-body');
|
||
var inputs = body.querySelectorAll('input[id^="cfg-"]');
|
||
var data = {};
|
||
inputs.forEach(function (input) {
|
||
var key = input.id.replace('cfg-', '');
|
||
var val = input.value.trim();
|
||
if (val) data[key] = val;
|
||
});
|
||
if (Object.keys(data).length === 0) {
|
||
alert('请至少填写一个字段');
|
||
return;
|
||
}
|
||
var saveBtn = document.getElementById('save-btn');
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = '保存中...';
|
||
fetch('/api/admin/sources/' + currentEditSource, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (result) {
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = '保存';
|
||
if (result.success) {
|
||
closeEditModal();
|
||
loadSources();
|
||
loadStatus();
|
||
} else {
|
||
alert('保存失败: ' + (result.error || '未知错误'));
|
||
}
|
||
})
|
||
.catch(function (err) {
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = '保存';
|
||
alert('请求失败: ' + err.message);
|
||
});
|
||
};
|
||
|
||
// ===== 辅助 =====
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
})();
|