114 lines
22 KiB
JavaScript

// HLDP-ZY://agents/zhuyuan-dev-agent/server.js
// @guardian: ICE-GL-ZY001 · 铸渊
// @sovereign: TCS-0002∞ · 冰朔
// @copyright: 国作登字-2026-A-00037559
// BD001 常驻Agent v5.0 — 零点原核UI风格
require("dotenv").config();
const express = require("express");
const path = require("path");
const fs = require("fs");
const { handleTask } = require("./modules/task-receiver");
const { sendNotification } = require("./modules/email-notify");
const { expandPlan } = require("./modules/plan-expander");
const { call: rawCall, getStatus: getModelStatus } = require("./modules/model-caller");
const { executePlan } = require("./modules/executor");
const { createLogger, readLatestRun, listRuns } = require("./modules/logger");
const { wrapModelCaller, reset, getCallCount, isBudgetExceeded } = require("./modules/cost-guard");
const { record: writeBrain, recordMilestone, AGENT_ID } = require("./modules/brain-writer");
const { wakeAndCheck, testConnection } = require("./modules/notion-poller");
const safeCall = wrapModelCaller(rawCall);
const app = express();
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();
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"><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>
*,*::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="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="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"></div>
<script>
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);
});
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)});