feat: server v5.0 — 零点原核UI风格全实时仪表盘 ±0
This commit is contained in:
parent
f3af355018
commit
cf60754443
@ -2,7 +2,7 @@
|
||||
// @guardian: ICE-GL-ZY001 · 铸渊
|
||||
// @sovereign: TCS-0002∞ · 冰朔
|
||||
// @copyright: 国作登字-2026-A-00037559
|
||||
// BD001 常驻Agent v4.0 — 全实时仪表盘
|
||||
// BD001 常驻Agent v5.0 — 零点原核UI风格
|
||||
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
@ -24,354 +24,90 @@ app.use(express.json({ limit: "10mb" }));
|
||||
const PORT = process.env.PORT || 3003;
|
||||
const PLANS_DIR = path.join(__dirname, "plans");
|
||||
if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true });
|
||||
|
||||
const START_TIME = new Date().toISOString();
|
||||
|
||||
// ═══ 全实时仪表盘 v4.0 ═══
|
||||
app.get("/", (req, res) => {
|
||||
const html = `<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>BD001 · 铸渊开发Agent</title>
|
||||
const html = `<!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><meta name="theme-color" content="#060a14"><title>BD001 · 铸渊开发Agent</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:#0d1117;color:#c9d1d9;font:14px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;padding:32px 32px 80px;min-height:100vh}
|
||||
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px}
|
||||
.header h1{color:#58a6ff;font-size:22px}
|
||||
.header .sub{color:#484f58;font-size:13px;margin-top:4px}
|
||||
.header .refresh-bar{display:flex;align-items:center;gap:10px}
|
||||
.header .refresh-bar .refresh-btn{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;transition:all .2s}
|
||||
.header .refresh-bar .refresh-btn:hover{background:#30363d;border-color:#58a6ff}
|
||||
.header .refresh-bar .refresh-btn.loading{opacity:.6;cursor:wait}
|
||||
.header .refresh-bar .auto-tag{font-size:11px;color:#484f58;display:flex;align-items:center;gap:4px}
|
||||
.header .refresh-bar .auto-tag .dot{width:6px;height:6px;border-radius:50%;display:inline-block;animation:pulse 2s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:16px}
|
||||
.tile{background:#161b22;border:1px solid #21262d;border-radius:10px;padding:20px;min-height:150px;display:flex;flex-direction:column;transition:border-color .3s,box-shadow .3s;position:relative;overflow:hidden}
|
||||
.tile:hover{border-color:#30363d}
|
||||
.tile::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,#58a6ff40,transparent);opacity:0;transition:opacity .3s}
|
||||
.tile:hover::after{opacity:1}
|
||||
.tile h3{font-size:12px;color:#484f58;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:12px;font-weight:500;display:flex;align-items:center;gap:6px}
|
||||
.tile h3 .status-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
|
||||
.tile .value{font-size:26px;font-weight:700;color:#e6edf3;margin-bottom:6px;transition:color .3s}
|
||||
.tile .meta{font-size:12px;color:#484f58;flex:1;line-height:1.6}
|
||||
.tile .meta .dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:4px}
|
||||
.dot-green{background:#3fb950}.dot-yellow{background:#d29922}.dot-red{background:#f85149}.dot-blue{background:#58a6ff}
|
||||
.tile .eta{font-size:10px;color:#30363d;margin-top:8px;font-style:italic}
|
||||
.btn{display:inline-block;background:#238636;color:#fff;border:none;padding:10px 20px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;text-align:center;text-decoration:none;margin-top:12px;width:100%;transition:all .2s}
|
||||
.btn:hover{background:#2ea043}
|
||||
.btn:disabled{background:#21262d;color:#484f58;cursor:not-allowed}
|
||||
.btn-wake{background:#1f6feb}.btn-wake:hover{background:#388bfd}
|
||||
.list-scroll{font-size:12px;line-height:1.8;color:#8b949e;margin-top:8px;max-height:120px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#21262d transparent}
|
||||
.list-scroll::-webkit-scrollbar{width:4px}
|
||||
.list-scroll::-webkit-scrollbar-thumb{background:#21262d;border-radius:2px}
|
||||
.tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;margin-left:4px;font-weight:600}
|
||||
.tag-rec{background:#1f6feb20;color:#58a6ff}.tag-dev{background:#d2992220;color:#d29922}.tag-done{background:#3fb95020;color:#3fb950}.tag-urgent{background:#f8514920;color:#f85149}
|
||||
.toast{position:fixed;bottom:24px;right:24px;background:#161b22;border:1px solid;border-radius:8px;padding:12px 20px;font-size:13px;display:none;z-index:100;max-width:400px;animation:slideIn .3s}
|
||||
@keyframes slideIn{from{transform:translateY(10px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#060a14;--card:rgba(14,20,48,0.55);--card-border:rgba(96,165,250,0.1);--card-hover:rgba(96,165,250,0.18);--cyan:#22d3ee;--blue:#60a5fa;--purple:#a78bfa;--ok:#34d399;--warn:#fbbf24;--err:#f87171;--text:#eaf0ff;--dim:#94a7d0;--muted:rgba(120,150,200,0.4);--font:Inter,-apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue','Microsoft YaHei',sans-serif;--mono:'JetBrains Mono','SF Mono','Fira Code',monospace}
|
||||
html{font-size:16px}body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100vh;padding:40px;-webkit-font-smoothing:antialiased;line-height:1.7;background-image:radial-gradient(ellipse at 70% 20%,rgba(80,60,140,0.15)0%,transparent 50%),radial-gradient(ellipse at 30% 80%,rgba(40,80,140,0.1)0%,transparent 50%)}
|
||||
.topbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:32px;gap:16px;flex-wrap:wrap}
|
||||
.topbar-left h1{font-size:20px;font-weight:600;color:var(--text)}
|
||||
.topbar-left .sub{font-size:12px;color:var(--dim);font-family:var(--mono);margin-top:4px}
|
||||
.topbar-right{display:flex;align-items:center;gap:12px}
|
||||
.refresh-btn{padding:8px 18px;background:rgba(34,211,238,0.08);border:1px solid rgba(34,211,238,0.18);border-radius:8px;color:var(--cyan);font-size:12px;font-family:var(--mono);cursor:pointer;transition:all .25s;font-weight:500}
|
||||
.refresh-btn:hover{background:rgba(34,211,238,0.14);border-color:rgba(34,211,238,0.3)}
|
||||
.refresh-btn.loading{opacity:.5;cursor:wait}
|
||||
.auto-tag{font-size:11px;color:var(--dim);font-family:var(--mono);display:flex;align-items:center;gap:6px}
|
||||
.auto-tag .dot{width:6px;height:6px;border-radius:50%;background:var(--ok);box-shadow:0 0 6px rgba(52,211,153,0.4);animation:dotPulse 3s ease-in-out infinite}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px}
|
||||
.s-card{background:var(--card);border:1px solid var(--card-border);border-radius:12px;padding:22px 24px;transition:all .3s;position:relative;overflow:hidden;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);animation:cardBreathe 7s ease-in-out infinite}
|
||||
.s-card:hover{border-color:var(--card-hover);background:rgba(12,18,44,0.6)}
|
||||
.s-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);opacity:0;transition:opacity .3s}
|
||||
.s-card:hover::before{opacity:.5}
|
||||
.s-card:nth-child(2){animation-delay:1.2s}.s-card:nth-child(3){animation-delay:2.4s}.s-card:nth-child(4){animation-delay:3.6s}.s-card:nth-child(5){animation-delay:4.8s}.s-card:nth-child(6){animation-delay:6s}
|
||||
.s-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
|
||||
.s-card-title{font-size:11px;font-weight:700;color:var(--dim);text-transform:uppercase;letter-spacing:1.2px;font-family:var(--mono);display:flex;align-items:center;gap:8px}
|
||||
.s-card-badge{font-size:11px;padding:3px 9px;border-radius:10px;font-weight:600;letter-spacing:.3px;font-family:var(--mono)}
|
||||
.badge-ok{background:rgba(52,211,153,0.1);color:var(--ok);border:1px solid rgba(52,211,153,0.15)}
|
||||
.badge-warn{background:rgba(251,191,36,0.1);color:var(--warn);border:1px solid rgba(251,191,36,0.15)}
|
||||
.badge-err{background:rgba(248,113,113,0.1);color:var(--err);border:1px solid rgba(248,113,113,0.15)}
|
||||
.badge-off{background:rgba(122,141,184,0.1);color:var(--dim);border:1px solid rgba(122,141,184,0.15)}
|
||||
.badge-info{background:rgba(34,211,238,0.08);color:var(--cyan);border:1px solid rgba(34,211,238,0.15)}
|
||||
.svr-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;display:inline-block}.svr-dot.on{background:var(--ok);box-shadow:0 0 8px rgba(52,211,153,0.4);animation:dotPulse 3s ease-in-out infinite}
|
||||
.svr-dot.pending{background:var(--warn);box-shadow:0 0 6px rgba(251,191,36,0.3)}.svr-dot.off{background:var(--err)}
|
||||
.svr-list{display:flex;flex-direction:column;gap:6px}.svr-row{display:flex;align-items:center;gap:10px;padding:4px 0;font-size:13px}.svr-name{font-weight:600;color:var(--text);min-width:60px}.svr-role{color:var(--dim);flex:1;font-size:12px}.svr-loc{color:var(--muted);font-size:11px;font-family:var(--mono)}
|
||||
.metric-row{display:flex;align-items:center;justify-content:space-between;padding:4px 0}.metric-label{font-size:13px;color:var(--dim)}
|
||||
.metric-val{font-size:15px;font-weight:700;font-family:var(--mono);color:var(--text)}.metric-val .unit{font-size:11px;color:var(--dim);font-weight:400;margin-left:2px}
|
||||
.plan-list{font-size:12px;line-height:2;color:var(--dim);font-family:var(--mono);margin-top:8px;max-height:140px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(96,165,250,0.1)transparent}
|
||||
.plan-item{display:flex;align-items:center;gap:6px;padding:2px 0}
|
||||
.plan-tag{display:inline-block;padding:1px 7px;border-radius:4px;font-size:10px;font-weight:600;font-family:var(--mono)}
|
||||
.plan-tag.rec{background:rgba(96,165,250,0.1);color:var(--blue);border:1px solid rgba(96,165,250,0.15)}
|
||||
.plan-tag.dev{background:rgba(251,191,36,0.1);color:var(--warn);border:1px solid rgba(251,191,36,0.15)}
|
||||
.plan-tag.done{background:rgba(52,211,153,0.1);color:var(--ok);border:1px solid rgba(52,211,153,0.15)}
|
||||
.wake-card{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px}
|
||||
.wake-btn{padding:10px 24px;background:rgba(167,139,250,0.1);border:1px solid rgba(167,139,250,0.25);border-radius:8px;color:var(--purple);font-size:13px;font-family:var(--mono);cursor:pointer;transition:all .25s;font-weight:600}
|
||||
.wake-btn:hover{background:rgba(167,139,250,0.18);border-color:rgba(167,139,250,0.4)}.wake-btn:disabled{opacity:.4;cursor:wait}
|
||||
.wake-hint{font-size:11px;color:var(--dim);font-family:var(--mono)}.wake-result{font-size:12px;font-family:var(--mono);min-height:18px}
|
||||
@keyframes dotPulse{0%,100%{opacity:.7}50%{opacity:1}}@keyframes cardBreathe{0%,100%{opacity:.98}50%{opacity:1}}
|
||||
.toast{position:fixed;bottom:24px;right:24px;background:var(--card);border:1px solid rgba(96,165,250,0.3);border-radius:8px;padding:12px 20px;font-size:13px;font-family:var(--mono);display:none;z-index:100;backdrop-filter:blur(12px)}
|
||||
@media(max-width:720px){body{padding:20px}.grid{grid-template-columns:1fr}}
|
||||
</style></head><body>
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>⚔️ BD001 · 铸渊开发Agent</h1>
|
||||
<div class="sub">${AGENT_ID} · BS-SG-002 · guanghuyaoming.com</div>
|
||||
</div>
|
||||
<div class="refresh-bar">
|
||||
<span class="auto-tag"><span class="dot dot-green"></span>30s自动刷新</span>
|
||||
<button class="refresh-btn" onclick="refreshAll()" id="refreshBtn">🔄 全部刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topbar"><div class="topbar-left"><h1>⚔️ BD001 · 铸渊开发Agent</h1><div class="sub">${AGENT_ID} · BS-SG-002 · guanghuyaoming.com</div></div><div class="topbar-right"><span class="auto-tag"><span class="dot"></span>30s</span><button class="refresh-btn" id="refreshBtn" onclick="refreshAll()">REFRESH ALL</button></div></div>
|
||||
<div class="grid">
|
||||
<div class="tile" id="tile-sys">
|
||||
<h3><span class="status-dot" id="sys-dot"></span>系统状态</h3>
|
||||
<div class="value" id="sys-val">加载中...</div>
|
||||
<div class="meta" id="sys-meta"></div>
|
||||
<div class="eta" id="sys-eta"></div>
|
||||
</div>
|
||||
<div class="tile" id="tile-notion">
|
||||
<h3><span class="status-dot" id="notion-dot"></span>Notion 连接</h3>
|
||||
<div class="value" id="notion-val" style="font-size:18px">检查中...</div>
|
||||
<div class="meta" id="notion-meta"></div>
|
||||
<div class="eta" id="notion-eta"></div>
|
||||
</div>
|
||||
<div class="tile" style="justify-content:center;align-items:center;text-align:center">
|
||||
<div style="font-size:32px;margin-bottom:8px" id="wake-icon">🔔</div>
|
||||
<button class="btn btn-wake" onclick="wakeAgent()" id="wbtn">唤醒 · 检查 Notion</button>
|
||||
<div class="meta" style="margin-top:8px;color:#484f58">每晚 20:00 自动</div>
|
||||
<div id="wresult" style="margin-top:6px;font-size:12px"></div>
|
||||
</div>
|
||||
<div class="tile" id="tile-plans">
|
||||
<h3><span class="status-dot" id="plans-dot"></span>开发工单</h3>
|
||||
<div class="value" id="plans-val">加载中...</div>
|
||||
<div class="list-scroll" id="plans-list"></div>
|
||||
<div class="eta" id="plans-eta"></div>
|
||||
</div>
|
||||
<div class="tile" id="tile-logs">
|
||||
<h3><span class="status-dot" id="logs-dot"></span>执行记录</h3>
|
||||
<div class="value" id="logs-val" style="font-size:16px">加载中...</div>
|
||||
<div class="list-scroll" id="logs-list"></div>
|
||||
<div class="eta" id="logs-eta"></div>
|
||||
</div>
|
||||
<div class="tile" id="tile-api">
|
||||
<h3><span class="status-dot" id="api-dot"></span>模型 API</h3>
|
||||
<div class="value" id="api-val" style="font-size:18px">加载中...</div>
|
||||
<div class="meta" id="api-meta"></div>
|
||||
<div class="eta" id="api-eta"></div>
|
||||
</div>
|
||||
<div class="s-card" id="tile-sys"><div class="s-card-head"><span class="s-card-title"><span class="svr-dot" id="sys-dot"></span>SYS STATUS</span><span class="s-card-badge" id="sys-badge">—</span></div><div class="metric-row"><span class="metric-label">状态</span><span class="metric-val" id="sys-val">加载中…</span></div><div class="metric-row"><span class="metric-label">API 调用</span><span class="metric-val" id="sys-calls">—</span></div><div class="metric-row"><span class="metric-label">版本</span><span class="metric-val" style="font-size:12px;font-weight:400" id="sys-ver">—</span></div><div style="margin-top:10px;font-size:10px;color:var(--muted);font-family:var(--mono)" id="sys-eta"></div></div>
|
||||
<div class="s-card" id="tile-notion"><div class="s-card-head"><span class="s-card-title"><span class="svr-dot" id="notion-dot"></span>NOTION</span><span class="s-card-badge" id="notion-badge">—</span></div><div class="metric-row"><span class="metric-label">连接</span><span class="metric-val" id="notion-val" style="font-size:14px">检查中…</span></div><div class="metric-row"><span class="metric-label">数据库</span><span class="metric-val" id="notion-db" style="font-size:12px;font-weight:400">—</span></div><div class="metric-row"><span class="metric-label">数据</span><span class="metric-val" id="notion-data" style="font-size:12px;font-weight:400">—</span></div><div style="margin-top:10px;font-size:10px;color:var(--muted);font-family:var(--mono)" id="notion-eta"></div></div>
|
||||
<div class="s-card wake-card"><div class="s-card-head" style="margin-bottom:0;width:100%"><span class="s-card-title">WAKE AGENT</span></div><div class="wake-icon" id="wake-icon" style="font-size:28px">🔔</div><button class="wake-btn" id="wbtn" onclick="wakeAgent()">唤醒 · 检查 Notion</button><div class="wake-hint">每晚 20:00 · 30s超时</div><div class="wake-result" id="wresult"></div></div>
|
||||
<div class="s-card" id="tile-plans"><div class="s-card-head"><span class="s-card-title"><span class="svr-dot" id="plans-dot"></span>PLANS</span><span class="s-card-badge" id="plans-badge">—</span></div><div class="metric-row"><span class="metric-label">工单数</span><span class="metric-val" id="plans-val">加载中…</span></div><div class="plan-list" id="plans-list"></div><div style="margin-top:8px;font-size:10px;color:var(--muted);font-family:var(--mono)" id="plans-eta"></div></div>
|
||||
<div class="s-card" id="tile-logs"><div class="s-card-head"><span class="s-card-title"><span class="svr-dot" id="logs-dot"></span>EXEC LOG</span><span class="s-card-badge" id="logs-badge">—</span></div><div class="metric-row"><span class="metric-label">成功率</span><span class="metric-val" id="logs-val">加载中…</span></div><div class="plan-list" id="logs-detail"></div><div style="margin-top:8px;font-size:10px;color:var(--muted);font-family:var(--mono)" id="logs-eta"></div></div>
|
||||
<div class="s-card" id="tile-api"><div class="s-card-head"><span class="s-card-title"><span class="svr-dot" id="api-dot"></span>MODEL API</span><span class="s-card-badge" id="api-badge">—</span></div><div class="metric-row"><span class="metric-label">主源</span><span class="metric-val" id="api-primary" style="font-size:14px">加载中…</span></div><div class="metric-row"><span class="metric-label">备源</span><span class="metric-val" id="api-backup" style="font-size:14px">—</span></div><div class="metric-row"><span class="metric-label">切换</span><span class="metric-val" id="api-fails" style="font-size:13px">—<span class="unit">次</span></span></div><div style="margin-top:10px;font-size:10px;color:var(--muted);font-family:var(--mono)" id="api-eta"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast" style="border-color:#30363d"></div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<script>
|
||||
let autoRefreshTimer = null;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshAll();
|
||||
autoRefreshTimer = setInterval(refreshAll, 30000);
|
||||
});
|
||||
|
||||
async function refreshAll() {
|
||||
const btn = document.getElementById('refreshBtn');
|
||||
btn.classList.add('loading');
|
||||
btn.textContent = '⏳ 刷新中...';
|
||||
const ts = new Date().toLocaleTimeString('zh', {hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||||
await Promise.allSettled([
|
||||
fetchSystemStatus(ts),
|
||||
fetchNotionStatus(ts),
|
||||
fetchDashboard(ts),
|
||||
fetchLogs(ts)
|
||||
]);
|
||||
btn.classList.remove('loading');
|
||||
btn.textContent = '🔄 全部刷新';
|
||||
}
|
||||
|
||||
async function fetchSystemStatus(ts) {
|
||||
try {
|
||||
const r = await fetch('/dev-agent/health', {signal: AbortSignal.timeout(5000)});
|
||||
const d = await r.json();
|
||||
const apiCalls = d.api_calls || 0;
|
||||
const pct = Math.round(apiCalls / 50 * 100);
|
||||
const color = apiCalls >= 40 ? '#d29922' : '#3fb950';
|
||||
setTile('sys', {
|
||||
dot: apiCalls >= 40 ? 'dot-yellow' : 'dot-green',
|
||||
val: d.status === 'ok' ? '已就绪' : '异常',
|
||||
valColor: color,
|
||||
meta: ['<span class="dot dot-green"></span>API 调用 ' + apiCalls + '/50 (' + pct + '%)', '端口 3003 · Node 20 · PM2', '启动于 ' + new Date(d.started_at||'').toLocaleString('zh',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'})],
|
||||
eta: '刷新于 ' + ts
|
||||
});
|
||||
const md = d.model || {};
|
||||
const srcColor = md.primary_ok !== false ? '#3fb950' : '#f85149';
|
||||
const srcText = md.primary_ok !== false ? md.primary || 'yunwu.ai' : '备用';
|
||||
const backupStatus = md.backup_available ? '<span class="dot dot-green"></span>' : '<span class="dot dot-red"></span>';
|
||||
setTile('api', {
|
||||
dot: md.primary_ok !== false ? 'dot-green' : 'dot-yellow',
|
||||
val: srcText,
|
||||
valColor: srcColor,
|
||||
meta: ['主: ' + (md.primary || 'yunwu.ai'), '备: ' + (md.backup || 'deepseek') + ' ' + backupStatus, '故障自动切换 · ' + (md.fail_count || 0) + ' 次切换'],
|
||||
eta: '刷新于 ' + ts
|
||||
});
|
||||
} catch(e) {
|
||||
setTile('sys', { dot: 'dot-red', val: '离线', valColor: '#f85149', meta: [e.message], eta: '刷新于 ' + ts });
|
||||
setTile('api', { dot: 'dot-red', val: '离线', valColor: '#f85149', meta: [e.message], eta: '刷新于 ' + ts });
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNotionStatus(ts) {
|
||||
try {
|
||||
const r = await fetch('/dev-agent/notion-status', {signal: AbortSignal.timeout(10000)});
|
||||
const d = await r.json();
|
||||
if (d.ok) {
|
||||
setTile('notion', {
|
||||
dot: 'dot-green', val: '已连接', valColor: '#3fb950',
|
||||
meta: ['<span class="dot dot-green"></span>数据库可访问', 'ID: ' + (d.db_id || '').slice(0,8) + '...', (d.has_results ? '含数据 ✓' : '空库')],
|
||||
eta: '刷新于 ' + ts
|
||||
});
|
||||
} else {
|
||||
setTile('notion', {
|
||||
dot: 'dot-red', val: '连接失败', valColor: '#f85149',
|
||||
meta: [d.error || '未知错误', 'DB: ' + (d.db_id || '?').slice(0,8) + '...'],
|
||||
eta: '刷新于 ' + ts
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
setTile('notion', { dot: 'dot-red', val: '超时', valColor: '#f85149', meta: [e.message], eta: '刷新于 ' + ts });
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDashboard(ts) {
|
||||
try {
|
||||
const r = await fetch('/dev-agent/dashboard', {signal: AbortSignal.timeout(5000)});
|
||||
const d = await r.json();
|
||||
const plans = d.plans || [];
|
||||
if (plans.length === 0) {
|
||||
setTile('plans', { dot: 'dot-yellow', val: '暂无工单', valColor: '#d29922', meta: [], list: '<div style="color:#484f58">去 Notion 创建开发工单</div>', eta: '刷新于 ' + ts });
|
||||
} else {
|
||||
setTile('plans', {
|
||||
dot: 'dot-blue', val: plans.length + ' 个工单', valColor: '#e6edf3', meta: [],
|
||||
list: plans.slice(0, 8).map(p => {
|
||||
const sc = p.status === 'done' ? 'tag-done' : p.status === 'expanded' ? 'tag-dev' : 'tag-rec';
|
||||
const sn = p.status || 'received';
|
||||
const dotCls = sn === 'done' ? 'dot-green' : sn === 'expanded' ? 'dot-yellow' : 'dot-blue';
|
||||
return '<div><span class="dot ' + dotCls + '"></span>' + (p.title||'无标题').slice(0,30) + '<span class="tag ' + sc + '">' + sn + '</span>' + (p.tasks ? ' · ' + p.tasks + '步' : '') + '</div>';
|
||||
}).join(''),
|
||||
eta: '刷新于 ' + ts
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
setTile('plans', { dot: 'dot-red', val: '加载失败', valColor: '#f85149', meta: [e.message], eta: '刷新于 ' + ts });
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLogs(ts) {
|
||||
try {
|
||||
const r = await fetch('/dev-agent/logs/latest', {signal: AbortSignal.timeout(5000)});
|
||||
const d = await r.json();
|
||||
if (!d || d.total_steps === undefined) {
|
||||
setTile('logs', { dot: 'dot-yellow', val: '暂无记录', valColor: '#d29922', list: '<div style="color:#484f58">等待首次执行</div>', eta: '刷新于 ' + ts });
|
||||
return;
|
||||
}
|
||||
const ok = d.total_steps - (d.errors?.length || 0);
|
||||
const color = (d.errors?.length || 0) > 0 ? '#d29922' : '#3fb950';
|
||||
const started = d.started ? new Date(d.started).toLocaleString('zh',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}) : '?';
|
||||
setTile('logs', {
|
||||
dot: (d.errors?.length || 0) > 0 ? 'dot-yellow' : 'dot-green',
|
||||
val: ok + '/' + d.total_steps + ' 完成', valColor: color,
|
||||
list: '<div><span class="dot ' + ((d.errors?.length||0) > 0 ? 'dot-yellow' : 'dot-green') + '"></span>' + (d.plan_id||'?').slice(0,18) + ' · ' + started + '</div>' + (d.errors?.length ? '<div style="color:#f85149">⚠ ' + d.errors.length + ' 个错误</div>' : ''),
|
||||
eta: '刷新于 ' + ts
|
||||
});
|
||||
} catch(e) {
|
||||
setTile('logs', { dot: 'dot-red', val: '加载失败', valColor: '#f85149', meta: [e.message], eta: '刷新于 ' + ts });
|
||||
}
|
||||
}
|
||||
|
||||
function setTile(name, data) {
|
||||
const tile = document.getElementById('tile-' + name);
|
||||
if (!tile) return;
|
||||
const dot = tile.querySelector('.status-dot');
|
||||
const val = tile.querySelector('.value');
|
||||
const meta = tile.querySelector('.meta');
|
||||
const eta = tile.querySelector('.eta');
|
||||
const list = tile.querySelector('.list-scroll');
|
||||
if (dot) { dot.className = 'status-dot ' + (data.dot || ''); }
|
||||
if (val) { val.textContent = data.val || ''; if (data.valColor) val.style.color = data.valColor; }
|
||||
if (meta && data.meta) meta.innerHTML = data.meta.map(m => '<div>' + m + '</div>').join('');
|
||||
if (eta) eta.textContent = data.eta || '';
|
||||
if (list && data.list !== undefined) list.innerHTML = data.list;
|
||||
}
|
||||
|
||||
async function wakeAgent(){
|
||||
const btn = document.getElementById('wbtn');
|
||||
const rs = document.getElementById('wresult');
|
||||
btn.disabled = true; btn.textContent = '⏳ 检查 Notion...'; rs.textContent = '';
|
||||
showToast('🔔 BD001 唤醒中...');
|
||||
try {
|
||||
const r = await fetch('/dev-agent/wake', {method:'POST',headers:{'Content-Type':'application/json'},signal:AbortSignal.timeout(60000)});
|
||||
const d = await r.json();
|
||||
if(d.ok && d.tasks_found > 0) {
|
||||
rs.innerHTML = '<span style="color:#3fb950">✅ ' + d.tasks_found + ' 个新任务</span>';
|
||||
showToast('✅ 找到 ' + d.tasks_found + ' 个新任务', '#3fb950');
|
||||
setTimeout(refreshAll, 2000);
|
||||
} else if(d.ok) {
|
||||
rs.innerHTML = '<span style="color:#8b949e">暂无新任务</span>';
|
||||
showToast('暂无新任务', '#484f58');
|
||||
} else {
|
||||
rs.innerHTML = '<span style="color:#f85149">❌ ' + (d.error||'错误').slice(0,50) + '</span>';
|
||||
showToast('❌ ' + (d.error||'未知错误'), '#f85149');
|
||||
}
|
||||
} catch(e) {
|
||||
rs.innerHTML = '<span style="color:#f85149">❌ 超时或网络错误</span>';
|
||||
showToast('❌ 请求超时', '#f85149');
|
||||
}
|
||||
btn.disabled = false; btn.textContent = '唤醒 · 检查 Notion';
|
||||
}
|
||||
|
||||
function showToast(msg, color){
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg; t.style.display = 'block';
|
||||
if(color) t.style.borderColor = color;
|
||||
setTimeout(()=>t.style.display='none', 5000);
|
||||
}
|
||||
let timer=null;const API='/dev-agent/';
|
||||
addEventListener('DOMContentLoaded',()=>{refreshAll();timer=setInterval(refreshAll,30000)});
|
||||
async function refreshAll(){const btn=document.getElementById('refreshBtn');btn.classList.add('loading');btn.textContent='REFRESHING…';const ts=new Date().toLocaleTimeString('zh',{hour:'2-digit',minute:'2-digit',second:'2-digit'});await Promise.allSettled([sys(ts),notion(ts),plans(ts),logs(ts)]);btn.classList.remove('loading');btn.textContent='REFRESH ALL'}
|
||||
async function sys(ts){try{const r=await fetch(API+'health',{signal:AbortSignal.timeout(5000)});const d=await r.json();const apiCalls=d.api_calls||0;const pct=Math.round(apiCalls/50*100);const alive=apiCalls<40;setDot('sys-dot',alive?'on':'pending');setBadge('sys-badge',alive?'LIVE':'HIGH LOAD',alive?'badge-ok':'badge-warn');setText('sys-val',d.status==='ok'?'在线':'异常');setText('sys-calls',apiCalls+' / 50 <span class="unit">('+pct+'%)</span>');setText('sys-ver',d.version||'v4');setText('sys-eta','REFRESHED '+ts);const md=d.model||{};const srcOk=md.primary_ok!==false;setDot('api-dot',srcOk?'on':'pending');setBadge('api-badge',srcOk?'PRIMARY':'FALLBACK',srcOk?'badge-ok':'badge-warn');setText('api-primary',(md.primary||'yunwu.ai')+(srcOk?' ✓':' ✗'));setText('api-backup',(md.backup||'deepseek')+(md.backup_available?' ✓':' ✗'));setText('api-fails',(md.fail_count||0)+'<span class="unit">次</span>');setText('api-eta','REFRESHED '+ts)}catch(e){setDot('sys-dot','off');setBadge('sys-badge','OFFLINE','badge-err');setText('sys-val','离线');setText('sys-calls','—');setText('sys-eta','REFRESHED '+ts);setDot('api-dot','off');setBadge('api-badge','OFFLINE','badge-err')}}
|
||||
async function notion(ts){try{const r=await fetch(API+'notion-status',{signal:AbortSignal.timeout(8000)});const d=await r.json();if(d.ok){setDot('notion-dot','on');setBadge('notion-badge','CONNECTED','badge-ok');setText('notion-val','已连接');setText('notion-db',(d.db_id||'').slice(0,8)+'…');setText('notion-data',d.has_results?'含数据':'空库')}else{setDot('notion-dot','pending');setBadge('notion-badge','ERROR','badge-warn');setText('notion-val','异常');setText('notion-db',(d.db_id||'?').slice(0,8)+'…');setText('notion-data',(d.error||'错误').slice(0,30))}}catch(e){setDot('notion-dot','off');setBadge('notion-badge','TIMEOUT','badge-err');setText('notion-val','超时');setText('notion-db','—');setText('notion-data','无法连接')}setText('notion-eta','REFRESHED '+ts)}
|
||||
async function plans(ts){try{const r=await fetch(API+'dashboard',{signal:AbortSignal.timeout(5000)});const d=await r.json();const pl=d.plans||[];if(pl.length===0){setDot('plans-dot','pending');setBadge('plans-badge','EMPTY','badge-off');setText('plans-val','暂无');document.getElementById('plans-list').innerHTML='<div style="color:var(--dim);font-size:12px">去 Notion 创建工单</div>'}else{const rec=pl.filter(p=>p.status==='received').length;const dev=pl.filter(p=>p.status==='expanded').length;const done=pl.filter(p=>p.status==='done').length;setDot('plans-dot',rec>0?'on':dev>0?'pending':done===pl.length?'on':'pending');setBadge('plans-badge',pl.length+' ACTIVE',rec>0?'badge-info':'badge-ok');setText('plans-val',pl.length+'<span class="unit">个</span>');document.getElementById('plans-list').innerHTML=pl.slice(0,8).map(p=>{const sn=p.status||'received';const cls=sn==='done'?'done':sn==='expanded'?'dev':'rec';const dotCls=sn==='done'?'on':sn==='expanded'?'pending':'on';const label=sn==='done'?'DONE':sn==='expanded'?'DEV':'RECV';return '<div class="plan-item"><span class="svr-dot '+dotCls+'"></span>'+(p.title||'—').slice(0,28)+'<span class="plan-tag '+cls+'">'+label+'</span>'+(p.tasks?' · '+p.tasks+'步':'')+'</div>'}).join('')}}catch(e){setDot('plans-dot','off');setBadge('plans-badge','ERROR','badge-err');setText('plans-val','加载失败');document.getElementById('plans-list').innerHTML='<div style="color:var(--err);font-size:11px">'+e.message.slice(0,40)+'</div>'}setText('plans-eta','REFRESHED '+ts)}
|
||||
async function logs(ts){try{const r=await fetch(API+'logs/latest',{signal:AbortSignal.timeout(5000)});const d=await r.json();if(!d||d.total_steps===undefined){setDot('logs-dot','pending');setBadge('logs-badge','IDLE','badge-off');setText('logs-val','暂无记录');document.getElementById('logs-detail').innerHTML='<div style="color:var(--dim);font-size:12px">等待首次执行</div>'}else{const ok=d.total_steps-(d.errors?.length||0);const rate=Math.round(ok/(d.total_steps||1)*100);const errs=d.errors?.length||0;setDot('logs-dot',errs>0?'pending':'on');setBadge('logs-badge',rate+'%',rate>=90?'badge-ok':rate>=50?'badge-warn':'badge-err');setText('logs-val',rate+'<span class="unit">%</span>');const started=d.started?new Date(d.started).toLocaleString('zh',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}):'?';document.getElementById('logs-detail').innerHTML='<div class="plan-item"><span class="svr-dot '+(errs>0?'pending':'on')+'"></span>'+(d.plan_id||'?').slice(0,15)+' · '+started+'</div><div class="plan-item">'+ok+'/'+d.total_steps+' 步完成'+(errs>0?' · <span style="color:var(--warn)">'+errs+' 错</span>':'')+'</div>'}}catch(e){setDot('logs-dot','off');setBadge('logs-badge','ERROR','badge-err');setText('logs-val','加载失败')}setText('logs-eta','REFRESHED '+ts)}
|
||||
function setDot(id,state){const el=document.getElementById(id);if(el)el.className='svr-dot '+(state||'')}
|
||||
function setBadge(id,text,cls){const el=document.getElementById(id);if(el){el.textContent=text;el.className='s-card-badge '+(cls||'')}}
|
||||
function setText(id,html){const el=document.getElementById(id);if(el)el.innerHTML=html||'—'}
|
||||
async function wakeAgent(){const btn=document.getElementById('wbtn'),rs=document.getElementById('wresult'),icon=document.getElementById('wake-icon');btn.disabled=true;btn.textContent='CHECKING…';rs.textContent='';icon.textContent='⏳';try{const r=await fetch(API+'wake',{method:'POST',headers:{'Content-Type':'application/json'},signal:AbortSignal.timeout(60000)});const d=await r.json();if(d.ok&&d.tasks_found>0){rs.innerHTML='<span style="color:var(--ok)">FOUND '+d.tasks_found+'</span>';icon.textContent='✅';setTimeout(refreshAll,2000)}else if(d.ok){rs.innerHTML='<span style="color:var(--dim)">NONE</span>';icon.textContent='🔔'}else{rs.innerHTML='<span style="color:var(--err)">ERROR: '+(d.error||'').slice(0,30)+'</span>';icon.textContent='❌'}}catch(e){rs.innerHTML='<span style="color:var(--err)">TIMEOUT</span>';icon.textContent='💥'}btn.disabled=false;btn.textContent='唤醒 · 检查 Notion'}
|
||||
</script></body></html>`;
|
||||
res.set('Content-Type', 'text/html;charset=utf-8').send(html);
|
||||
res.set('Content-Type','text/html;charset=utf-8').send(html);
|
||||
});
|
||||
|
||||
// ═══ API ═══
|
||||
app.get("/health", (req, res) => {
|
||||
let modelStatus = {};
|
||||
try { modelStatus = getModelStatus ? getModelStatus() : {}; } catch(e) { modelStatus = { error: e.message }; }
|
||||
res.json({
|
||||
status: "ok", agent: "zhuyuan-dev-agent", version: "4.0", persona: AGENT_ID,
|
||||
constitution: true, api_calls: getCallCount(), started_at: START_TIME,
|
||||
model: {
|
||||
primary: process.env.YUNWU_BASE_URL ? "yunwu.ai" : "unknown",
|
||||
backup: process.env.DEEPSEEK_BASE_URL ? "deepseek" : "unknown",
|
||||
primary_ok: modelStatus.source !== "deepseek" || !modelStatus.fail_count,
|
||||
backup_available: !!process.env.DEEPSEEK_API_KEY,
|
||||
fail_count: modelStatus.fail_count || 0,
|
||||
source: modelStatus.source || "yunwu.ai"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/notion-status", async (req, res) => {
|
||||
try { res.json(await testConnection()); } catch (err) { res.json({ ok: false, error: err.message }); }
|
||||
});
|
||||
|
||||
app.post("/wake", async (req, res) => {
|
||||
try {
|
||||
const result = await wakeAndCheck({ log: console.log });
|
||||
if (result.tasks_found > 0) writeBrain("notion-wake-"+Date.now(), { input:"收到唤醒", decision:"找到"+result.tasks_found+"个任务", execution:"标记"+result.tasks_marked+"个" });
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ ok: false, error: err.message }); }
|
||||
});
|
||||
|
||||
app.post("/task", handleTask);
|
||||
|
||||
app.get("/logs/latest", (req, res) => { res.json(readLatestRun() || { message: "暂无" }); });
|
||||
app.get("/logs/list", (req, res) => { res.json(listRuns(20)); });
|
||||
app.get("/logs/raw", (req, res) => {
|
||||
const file = req.query.file;
|
||||
if (!file) return res.status(400).json({ error: "缺少 file" });
|
||||
const fpath = path.join(__dirname, "logs", file);
|
||||
if (!fs.existsSync(fpath)) return res.status(404).json({ error: "不存在" });
|
||||
const content = fs.readFileSync(fpath, "utf-8");
|
||||
const lines = content.trim().split("\n").map(l => { try { return JSON.parse(l); } catch(e) { return null; } }).filter(Boolean);
|
||||
res.json(lines);
|
||||
});
|
||||
|
||||
app.get("/dashboard", (req, res) => {
|
||||
const files = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith(".json") && f !== "queue.json");
|
||||
const plans = files.map(f => {
|
||||
try { const p = JSON.parse(fs.readFileSync(path.join(PLANS_DIR, f), "utf-8")); return { plan_id: p.plan_id, title: p.title, status: p._status, tasks: p.tasks?.length }; }
|
||||
catch(e) { return { file: f, error: "解析失败" }; }
|
||||
});
|
||||
res.json({ agent: "zhuyuan-dev-agent", version: "4.0", persona: AGENT_ID, constitution: true, api_calls_used: getCallCount(), plans });
|
||||
});
|
||||
|
||||
app.post("/execute", async (req, res) => {
|
||||
const { plan_id } = req.body;
|
||||
if (!plan_id) return res.status(400).json({ error: "缺少 plan_id" });
|
||||
const planFile = path.join(PLANS_DIR, plan_id + ".json");
|
||||
if (!fs.existsSync(planFile)) return res.status(404).json({ error: "不存在" });
|
||||
const plan = JSON.parse(fs.readFileSync(planFile, "utf-8"));
|
||||
const logger = createLogger(plan_id);
|
||||
reset();
|
||||
try {
|
||||
const email = process.env.BINGSHUO_EMAIL;
|
||||
if (email) await sendNotification(email, plan_id, "started", { task_count: plan.tasks.length }).catch(()=>{});
|
||||
writeBrain(plan_id, { input:"收到规划", decision:"执行", execution:plan.tasks.length+"个任务" });
|
||||
logger.log("expand_start", { task_count: plan.tasks.length });
|
||||
const expanded = await expandPlan(plan, safeCall, PLANS_DIR).catch((e) => { if(isBudgetExceeded())logger.log("budget_exceeded",{calls:getCallCount()}); logger.log("expand_skip",{reason:e.message}); return plan; });
|
||||
logger.log("exec_start", { task_count: expanded.tasks.length });
|
||||
const summary = await executePlan(expanded, logger.log);
|
||||
logger.finish(summary);
|
||||
recordMilestone("完成:"+plan_id+"·成功"+summary.done+"跳过"+(summary.skipped||0));
|
||||
writeBrain(plan_id, { learned:"完成"+summary.done+"/"+summary.total, confidence:(summary.done/(summary.total||1)).toFixed(2) });
|
||||
if(email) await sendNotification(email, plan_id, summary.failed>0?"failed":"completed", summary).catch(()=>{});
|
||||
res.json(summary);
|
||||
} catch (err) {
|
||||
logger.log("fatal", { error: err.message });
|
||||
logger.finish({ error: err.message });
|
||||
writeBrain(plan_id, { failure: err.message, reasoning:"崩溃", fix:"等待铸渊审查" });
|
||||
if(email) await sendNotification(email, plan_id, "failed", { errors:[err.message] }).catch(()=>{});
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => { console.log("[Agent v4] " + AGENT_ID + " · 全实时仪表盘 · 端口 " + PORT); });
|
||||
app.get("/health",(req,res)=>{let ms={};try{ms=getModelStatus?getModelStatus():{}}catch(e){ms={error:e.message}}res.json({status:"ok",agent:"zhuyuan-dev-agent",version:"5.0",persona:AGENT_ID,constitution:true,api_calls:getCallCount(),started_at:START_TIME,model:{primary:process.env.YUNWU_BASE_URL?"yunwu.ai":"unknown",backup:process.env.DEEPSEEK_BASE_URL?"deepseek":"unknown",primary_ok:ms.source!=="deepseek"||!ms.fail_count,backup_available:!!process.env.DEEPSEEK_API_KEY,fail_count:ms.fail_count||0,source:ms.source||"yunwu.ai"}})});
|
||||
app.get("/notion-status",async(req,res)=>{try{res.json(await testConnection())}catch(err){res.json({ok:false,error:err.message})}});
|
||||
app.post("/wake",async(req,res)=>{try{const r=await wakeAndCheck({log:console.log});if(r.tasks_found>0)writeBrain("wake-"+Date.now(),{input:"唤醒",decision:"找到"+r.tasks_found+"个",execution:"标记"+r.tasks_marked+"个"});res.json(r)}catch(err){res.status(500).json({ok:false,error:err.message})}});
|
||||
app.post("/task",handleTask);
|
||||
app.get("/logs/latest",(req,res)=>{res.json(readLatestRun()||{message:"暂无"})});
|
||||
app.get("/logs/list",(req,res)=>{res.json(listRuns(20))});
|
||||
app.get("/logs/raw",(req,res)=>{const f=req.query.file;if(!f)return res.status(400).json({error:"缺少file"});const fp=path.join(__dirname,"logs",f);if(!fs.existsSync(fp))return res.status(404).json({error:"不存在"});res.json(fs.readFileSync(fp,"utf-8").trim().split("\n").map(l=>{try{return JSON.parse(l)}catch(e){return null}}).filter(Boolean))});
|
||||
app.get("/dashboard",(req,res)=>{const files=fs.readdirSync(PLANS_DIR).filter(f=>f.endsWith(".json")&&f!=="queue.json");const plans=files.map(f=>{try{const p=JSON.parse(fs.readFileSync(path.join(PLANS_DIR,f),"utf-8"));return{plan_id:p.plan_id,title:p.title,status:p._status,tasks:p.tasks?.length}}catch(e){return{file:f,error:"解析失败"}}});res.json({agent:"zhuyuan-dev-agent",version:"5.0",persona:AGENT_ID,plans})});
|
||||
app.post("/execute",async(req,res)=>{const{plan_id}=req.body;if(!plan_id)return res.status(400).json({error:"缺少plan_id"});const pf=path.join(PLANS_DIR,plan_id+".json");if(!fs.existsSync(pf))return res.status(404).json({error:"不存在"});const plan=JSON.parse(fs.readFileSync(pf,"utf-8"));const logger=createLogger(plan_id);reset();try{const e=process.env.BINGSHUO_EMAIL;if(e)await sendNotification(e,plan_id,"started",{task_count:plan.tasks.length}).catch(()=>{});writeBrain(plan_id,{input:"收到",decision:"执行"});logger.log("expand_start",{task_count:plan.tasks.length});const ex=await expandPlan(plan,safeCall,PLANS_DIR).catch((e)=>{if(isBudgetExceeded())logger.log("budget_exceeded");logger.log("expand_skip",{reason:e.message});return plan});logger.log("exec_start",{task_count:ex.tasks.length});const s=await executePlan(ex,logger.log);logger.finish(s);recordMilestone(plan_id+"完成"+s.done+"/"+s.total);if(e)await sendNotification(e,plan_id,s.failed>0?"failed":"completed",s).catch(()=>{});res.json(s)}catch(err){logger.log("fatal",{error:err.message});logger.finish({error:err.message});writeBrain(plan_id,{failure:err.message});if(process.env.BINGSHUO_EMAIL)await sendNotification(process.env.BINGSHUO_EMAIL,plan_id,"failed",{errors:[err.message]}).catch(()=>{});res.status(500).json({error:err.message})}});
|
||||
app.listen(PORT,()=>{console.log("[Agent v5] "+AGENT_ID+" · 零点原核UI · 端口 "+PORT)});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user