今日信号来源
@@ -1280,6 +1334,189 @@ async function renderSignals() {
}
}
+// ── Spec System ──
+let _specCache = {};
+
+function closeSpecModal() {
+ const el = document.getElementById('specModal');
+ if (el) { el.classList.add('hidden'); el.classList.remove('flex'); }
+}
+
+function showModuleHelp(module, type) {
+ const cacheKey = module + ':' + type;
+ const cached = _specCache[cacheKey];
+ const modal = document.getElementById('specModal');
+ modal.classList.remove('hidden');
+ modal.classList.add('flex');
+
+ // Show loading
+ document.getElementById('specModalTitle').textContent = '加载中...';
+ document.getElementById('specModalSubtitle').textContent = '';
+ document.getElementById('specModalBody').innerHTML = '
正在获取规范...
';
+
+ const url = `/api/module-spec/${module}`;
+ const doRender = function(spec) {
+ if (spec.error) {
+ document.getElementById('specModalBody').innerHTML = `
获取失败: ${spec.error}
`;
+ return;
+ }
+ _specCache[cacheKey] = spec;
+ _renderSpecModal(spec, type);
+ };
+
+ if (cached) {
+ doRender(cached);
+ } else {
+ fetchJSON(url).then(spec => {
+ if (spec && typeof spec === 'object') {
+ _specCache[cacheKey] = spec;
+ doRender(spec);
+ } else {
+ document.getElementById('specModalBody').innerHTML = '
无规范数据
';
+ }
+ });
+ }
+}
+
+function _renderSpecModal(spec, type) {
+ const titleEl = document.getElementById('specModalTitle');
+ const subEl = document.getElementById('specModalSubtitle');
+ const bodyEl = document.getElementById('specModalBody');
+
+ let html = '';
+
+ if (type === 'human') {
+ const h = spec.human_help || {};
+ titleEl.textContent = h.title || spec.module || module;
+ subEl.textContent = 'Human Help · 使用指南';
+
+ if (h.description && h.description.length) {
+ html += '
说明
' + h.description.join('
') + '
';
+ }
+ if (h.usage && h.usage.length) {
+ html += '
用法
';
+ h.usage.forEach(u => { html += '- ' + u + '
'; });
+ html += '
';
+ }
+ if (h.troubleshooting && h.troubleshooting.length) {
+ html += '
故障排除
';
+ h.troubleshooting.forEach(t => { html += '- ' + t + '
'; });
+ html += '
';
+ }
+ } else {
+ const a = spec.ai_spec || {};
+ titleEl.textContent = spec.module || module;
+ subEl.textContent = 'AI Spec · 模块规范';
+
+ if (a.apis && a.apis.length) {
+ html += '
APIs
';
+ a.apis.forEach(api => {
+ const method = api.method || 'GET';
+ const color = method === 'GET' ? 'text-[#3fb950]' : method === 'POST' ? 'text-[#d29922]' : method === 'DELETE' ? 'text-[#f85149]' : 'text-[#58a6ff]';
+ html += '
' + method + ' ' + (api.path || '') + '';
+ if (api.description) html += '
' + api.description + '
';
+ html += '
';
+ });
+ html += '
';
+ }
+ if (a.dependencies && a.dependencies.length) {
+ html += '
依赖
';
+ a.dependencies.forEach(d => { html += '- ' + d + '
'; });
+ html += '
';
+ }
+ if (a.constraints && a.constraints.length) {
+ html += '
约束
';
+ a.constraints.forEach(c => { html += '- ' + c + '
'; });
+ html += '
';
+ }
+ if (a.must_not && a.must_not.length) {
+ html += '
禁止
';
+ a.must_not.forEach(m => { html += '- ' + m + '
'; });
+ html += '
';
+ }
+ if (a.related_files && a.related_files.length) {
+ html += '
相关文件
';
+ a.related_files.forEach(f => { html += '- ' + f + '
'; });
+ html += '
';
+ }
+ }
+
+ if (!html) {
+ html = '
该模块暂无规范数据
';
+ }
+
+ bodyEl.innerHTML = html;
+}
+
+// ── Health Tab (native) ──
+let healthRefreshInterval = null;
+
+async function renderHealth() {
+ const el = document.getElementById('tab-health');
+ el.innerHTML = '
加载中...
';
+ try {
+ const [services, monitor] = await Promise.all([
+ fetchJSON('/api/services'),
+ fetchJSON('/api/monitor')
+ ]);
+
+ const svcs = services.services || [];
+ const summary = services.summary || { ok: 0, total: 0 };
+ const tasks = monitor.tasks || [];
+ const tier1 = monitor.tier1 || {};
+ const tier2 = monitor.tier2 || {};
+
+ // Summary cards
+ let html = '
';
+ html += '
' + (summary.ok || 0) + '
正常服务
';
+ html += '
' + (summary.total || 0) + '
总服务数
';
+ html += '
' + (tier1.summary?.total || svcs.length) + '
一级服务
';
+ html += '
' + (tier2.summary?.total || 0) + '
二级服务
';
+ html += '
';
+
+ // Services by layer
+ const layers = {};
+ svcs.forEach(s => {
+ const layer = s.layer || 'other';
+ if (!layers[layer]) layers[layer] = [];
+ layers[layer].push(s);
+ });
+
+ html += '
| 层级 | 服务 | 端口 | 状态 |
';
+ Object.keys(layers).sort().forEach(layer => {
+ layers[layer].forEach((s, i) => {
+ const ok = s.health?.ok;
+ const statusIcon = ok ? '🟢' : (ok === false ? '🔴' : '🟡');
+ const statusColor = ok ? 'text-[#3fb950]' : (ok === false ? 'text-[#f85149]' : 'text-[#d29922]');
+ const label = s.label || s.name || s.type || '';
+ const port = s.port || '-';
+ if (i === 0) {
+ html += '| ' + layer + ' | ' + s.name + ' ' + label + ' | ' + port + ' | ' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + ' |
';
+ } else {
+ html += '| ' + s.name + ' ' + label + ' | ' + port + ' | ' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + ' |
';
+ }
+ });
+ });
+ html += '
';
+
+ // Task status
+ if (tasks.length) {
+ html += '
| 任务 | 状态 | 详情 |
';
+ tasks.forEach(t => {
+ const st = t.status || '';
+ const sc = st === 'ok' || st === 'running' ? 'text-[#3fb950]' : st === 'error' ? 'text-[#f85149]' : st === 'pending' ? 'text-[#d29922]' : 'text-slate-500';
+ html += '| ' + (t.name || '') + ' | ' + st + ' | ' + (t.detail || '') + ' |
';
+ });
+ html += '
';
+ }
+
+ html += '
更新于 ' + (monitor.generated_at || new Date().toLocaleTimeString()) + ' · 每10秒刷新
';
+ el.innerHTML = html;
+ } catch(e) {
+ el.innerHTML = '
加载健康状态失败: ' + e.message + '
';
+ }
+}
+
// ── 盯盘 ──
let watchTimer = null;
async function renderWatch() {