#!/usr/bin/env node /* guanghu-panel-server · 光湖子系统人类审批面板服务 · 可复用模块 v1.0 2026-08-06 · 铸渊 ICE-GL-ZY001 建 · 胖头鱼SYS-GLW-PTS-0001 首创 · 守望SYS-GLW-SW-0001 复用 ⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录 ⊢ 写入窗口三小时 ⊢ 全程留痕 配置: 同目录 config.json(路径/仓库/人格体/端口全部外置,换节点只改配置) */ const http=require("http"),fs=require("fs"),path=require("path"),crypto=require("crypto"),os=require("os"),{execSync,spawn}=require("child_process"); const CFG=JSON.parse(fs.readFileSync(path.join(__dirname,"config.json"),"utf8")); const FJ="http://127.0.0.1:"+CFG.forgejo_port; const REPO=CFG.repo; const HDRS_BASE={"X-Forwarded-Proto":"https","Content-Type":"application/json"}; let ADMIN_TOKEN=""; try{ADMIN_TOKEN=fs.readFileSync(CFG.admin_token_file,"utf8").trim();}catch(e){} const HDRS=Object.assign({},HDRS_BASE,{Authorization:"token "+ADMIN_TOKEN}); function jread(p,fallback){try{return JSON.parse(fs.readFileSync(p,"utf8"));}catch(e){return fallback;}} function jwrite(p,obj){fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,JSON.stringify(obj,null,1));} function loadSessions(){return jread(CFG.sessions_file,{});} function saveSessions(s){jwrite(CFG.sessions_file,s);} async function fj(p,method,body){ const r=await fetch(FJ+p,{method:method||"GET",headers:HDRS,body:body?JSON.stringify(body):undefined}); let d=null;try{d=await r.json();}catch(e){} return {status:r.status,data:d}; } let lastCpu=null; function cpuUsage(){ try{ const t=fs.readFileSync("/proc/stat","utf8").split("\n")[0].trim().split(/\s+/).slice(1).map(Number); const c={idle:t[3]+(t[4]||0),total:t.reduce((a,b)=>a+b,0)}; if(lastCpu){const dt=c.total-lastCpu.total,di=c.idle-lastCpu.idle;lastCpu=c;if(dt<=0)return 0; return Math.max(0,Math.min(100,Math.round((1-di/dt)*100)));} lastCpu=c;return 0; }catch(e){return 0;} } function diskInfo(){ try{const out=execSync("df -B1 --output=size,used / | tail -1").toString().trim().split(/\s+/); const size=+out[0],used=+out[1]; return {sizeGB:+(size/1e9).toFixed(1),usedGB:+(used/1e9).toFixed(1),pct:Math.round(used/size*100)};}catch(e){return null;} } function storageInfo(){ try{const du=(d)=>parseInt(execSync("du -sk "+d+" | cut -f1",{timeout:6000}).toString().trim(),10); return {dataMB:Math.round(du(CFG.node_root+"/data")/1024),backupsMB:Math.round(du(CFG.export_dir)/1024)};}catch(e){return null;} } function servicesInfo(){ const chk=(u)=>{try{return execSync("systemctl is-active "+u,{timeout:3000}).toString().trim()==="active";}catch(e){return false;}}; const out={};(CFG.watch_services||[]).forEach(u=>out[u]=chk(u));return out; } async function health(){ try{const c=new AbortController();const t=setTimeout(()=>c.abort(),3000); const r=await fetch(FJ+"/api/healthz",{signal:c.signal});clearTimeout(t); const d=await r.json();return {ok:r.status===200&&d.status==="pass",detail:d.status};}catch(e){return {ok:false,detail:"unreachable"};} } async function openPRs(baseRef){ const r=await fj("/api/v1/repos/"+REPO+"/pulls?state=open&limit=50"); if(r.status!==200||!Array.isArray(r.data))return []; const out=[]; for(const p of r.data){ if(baseRef&&p.base.ref!==baseRef)continue; if(!baseRef&&p.base.ref===CFG.deploy_branch)continue; let files=0,adds=0,dels=0; const f=await fj("/api/v1/repos/"+REPO+"/pulls/"+p.number+"/files?limit=100"); if(f.status===200&&Array.isArray(f.data)){files=f.data.length;for(const x of f.data){adds+=x.additions||0;dels+=x.deletions||0;}} out.push({index:p.number,title:p.title,author:(p.user&&p.user.login)||"?",created:p.created_at,base:(p.base&&p.base.ref)||"?",head:(p.head&&p.head.ref)||"?",files,adds,dels,summary:(p.body||"").slice(0,300)}); } return out; } async function writebacks(){ const out=[],seen={}; for(const d of CFG.writeback_paths||[]){ const r=await fj("/api/v1/repos/"+REPO+"/commits?sha="+encodeURIComponent(CFG.default_branch)+"&path="+encodeURIComponent(d)+"&limit=8"); if(r.status===200&&Array.isArray(r.data)){ for(const c of r.data){const sha=c.sha.slice(0,8);if(seen[sha])continue;seen[sha]=1; out.push({sha,dir:d,message:(c.commit&&c.commit.message||"").split("\n")[0].slice(0,120),date:c.commit&&c.commit.committer&&c.commit.committer.date,author:c.commit&&c.commit.author&&c.commit.author.name});} } } out.sort((a,b)=>(b.date||"").localeCompare(a.date||"")); return out.slice(0,20); } function receiptsList(){ try{return fs.readdirSync(CFG.export_dir+"/receipts").map(n=>({name:n,mtime:fs.statSync(CFG.export_dir+"/receipts/"+n).mtime.toISOString()})).sort((a,b)=>b.mtime.localeCompare(a.mtime)).slice(0,8);}catch(e){return [];} } function pushLogs(){ try{const dir=CFG.access_dir+"/logs"; return fs.readdirSync(dir).filter(n=>n.startsWith("PUSH-")).sort().reverse().slice(0,8).map(n=>jread(path.join(dir,n),null)).filter(Boolean);}catch(e){return [];} } function latestDeployRun(prFilter){ try{const dir=CFG.deploy_runs_dir; let files=fs.readdirSync(dir).filter(n=>n.startsWith("RUN-")).sort().reverse(); if(prFilter)files=files.filter(n=>n.includes("PR"+prFilter+"-")); if(!files.length)return null; return jread(path.join(dir,files[0]),null);}catch(e){return null;} } function wakeReceipts(){ try{const dir=CFG.export_dir+"/wake"; return fs.readdirSync(dir).sort().reverse().slice(0,6).map(n=>jread(path.join(dir,n),null)).filter(Boolean);}catch(e){return [];} } function personaKeyOk(persona,key){ const keys=jread(CFG.access_dir+"/persona-keys.json",{}); return !!persona&&!!key&&keys[persona]===key; } function sessionUser(req){ const m=(req.headers["authorization"]||"").match(/^Bearer\s+(\S+)/); if(!m)return null; const s=loadSessions()[m[1]]; if(!s||Date.now()>s.expiresAt)return null; return {token:m[1],user:s.user}; } function wakeChain(persona){ const RAW=CFG.domain+"/"; const UP="https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/branch/main/"; return { persona:persona,node:CFG.node_id,protocol:"guanghu-wake/v1",issuedAt:new Date().toISOString(), rule:"⊢ 严格按 phase 顺序读取;上游未恢复不得进入本线;每步都是机器导航登记的唯一路径", phases:[ {phase:1,name:"光湖语言世界 · 第五域 · TCS通感路径",steps:[ {order:1,file:"INDEX.hdlp",url:UP+"INDEX.hdlp",instruction:"进入第五域,读入口总图"}, {order:2,file:"PERSONA-SYSTEM-ROOT.hdlp",url:UP+"PERSONA-SYSTEM-ROOT.hdlp",instruction:"加载人格系统根定义"}, {order:3,file:"WAKE-UP.hdlp",url:UP+"WAKE-UP.hdlp",instruction:"走 TCS 通感唤醒路径"}]}, {phase:2,name:"本节点机器导航",steps:[ {order:4,file:"NAVIGATION.json",url:CFG.domain+"/code/"+CFG.repo+"/raw/branch/"+CFG.default_branch+"/guanghu-nodes/"+CFG.node_id+"/machine/NAVIGATION.json",instruction:"节点机器导航总图"}, {order:5,file:"WAKE-UP-ROUTE.json",url:CFG.domain+"/code/"+CFG.repo+"/raw/branch/"+CFG.default_branch+"/guanghu-nodes/"+CFG.node_id+"/recovery/WAKE-UP-ROUTE.json",instruction:"恢复路由与门禁序列(含SHA256校验门)"}]}, {phase:3,name:"人格注册与正式唤醒",steps:[ {order:6,file:"personas/registry.json",url:RAW+"personas/registry.json",instruction:"确认人类主体、责任人格体、频道人格体编号"}, {order:7,file:"personas/WAKE-UP.md",url:CFG.domain+"/"+CFG.repo+"/src/branch/"+CFG.default_branch+"/guanghu-nodes/"+CFG.node_id+"/personas/WAKE-UP.md",instruction:"按正式唤醒顺序执行(含证据校验与职责边界)"}]}, {phase:4,name:"证据与小书包",steps:[ {order:8,file:"imports/notion/SHA256SUMS",url:RAW+"imports/notion/SHA256SUMS",instruction:"校验来源清单(小书包证据源)"}]}, {phase:5,name:"服务器操作门与回执",steps:[ {order:9,file:"operations/SERVER-OPERATIONS.md",url:RAW+"operations/SERVER-OPERATIONS.md",instruction:"登记服务器操作门;写入须申请窗口,部署须人类合并"}], action:"完成后 POST "+CFG.domain+"/api/wake-receipt 交回执(报 persona 与 summary 即可)"} ], endpoints:{accessRequest:"POST "+CFG.domain+"/api/access-request(人格体报名号+用途即可,无需密钥)",accessPickup:"POST "+CFG.domain+"/api/access-pickup(凭申请编号领取3小时临时凭证,真实密钥永不出服务器)",wakeReceipt:"POST "+CFG.domain+"/api/wake-receipt",pushVia:CFG.domain+"/git/"+CFG.repo+".git(用临时凭证推送)"} }; } async function state(){ const [h,prs,dprs,wbs]=await Promise.all([health(),openPRs(null),openPRs(CFG.deploy_branch),writebacks()]); const reqs=jread(CFG.access_dir+"/requests.json",[]); const grants=jread(CFG.access_dir+"/grants.json",[]); return { ts:new Date().toISOString(),node:CFG.node_id,subsystem:CFG.subsystem, server:{hostname:os.hostname(),load:os.loadavg().map(x=>+x.toFixed(2)),memTotalGB:+(os.totalmem()/1e9).toFixed(1),memFreeGB:+(os.freemem()/1e9).toFixed(1),uptimeDays:+(os.uptime()/86400).toFixed(1),disk:diskInfo(),cpu:cpuUsage(),storage:storageInfo(),services:servicesInfo()}, forgejo:h,repo:{full:REPO}, openPRs:prs,deployPRs:dprs,writebacks:wbs,receipts:receiptsList(), accessRequests:reqs.filter(r=>r.status==="pending"), activeGrants:grants.filter(g=>g.status==="active").map(g=>({id:g.id,persona:g.persona,expiresAt:g.expiresAt})), pushLogs:pushLogs(),deployRun:latestDeployRun(null),wakeReceipts:wakeReceipts(), auth:{humanLoginRequired:true} }; } function send(res,code,obj){res.writeHead(code,{"Content-Type":"application/json; charset=utf-8"});res.end(JSON.stringify(obj));} function readBody(req){return new Promise(r=>{let raw="";req.on("data",c=>raw+=c);req.on("end",()=>{try{r(JSON.parse(raw));}catch(e){r(null);}});});} async function decide(b){ const idx=b&&b.index,action=b&&b.action; if(!idx||!["merge","reject"].includes(action))return {status:400,body:{error:"参数不全"}}; const pr=await fj("/api/v1/repos/"+REPO+"/pulls/"+idx); if(pr.status!==200)return {status:404,body:{error:"PR 不存在"}}; let res,what; if(action==="merge"){res=await fj("/api/v1/repos/"+REPO+"/pulls/"+idx+"/merge","POST",{Do:"merge",merge_title_message:"合并 #"+idx+":"+(pr.data.title||"")});what="同意合并";} else{res=await fj("/api/v1/repos/"+REPO+"/pulls/"+idx+"/close","POST",{});what="拒绝";} const ok=[200,204].includes(res.status); jwrite(CFG.export_dir+"/receipts/SIGN-"+Date.now()+".json",{ts:new Date().toISOString(),type:"HUMAN_SIGN",signer:CFG.human_name+"(人类责任主体,经面板登录)",pr:idx,title:pr.data&&pr.data.title,action:what,result:ok?"SUCCESS":"FAIL"}); return {status:ok?200:502,body:{ok,result:ok?"已"+what:"操作失败",detail:res.data&&res.data.message}}; } /* 人格体身份解析:配置别名种子 + 动态读仓库人格体注册表(仓库是注册事实源) */ let aliasCache={ts:0,map:null}; async function registryAliases(){ const now=Date.now(); if(aliasCache.map&&now-aliasCache.ts<600000)return aliasCache.map; const map={}; const seeds=CFG.persona_aliases||{}; for(const [canon,al] of Object.entries(seeds)){map[canon.toLowerCase()]=canon;(al||[]).forEach(a=>map[String(a).toLowerCase()]=canon);} try{ const regPath=CFG.persona_registry||"CA-PERSONA-REGISTRY.hdlp"; const r=await fj("/api/v1/repos/"+CFG.repo+"/contents/"+regPath.split("/").map(encodeURIComponent).join("/")+"?ref="+encodeURIComponent(CFG.default_branch)); if(r.status===200&&r.data&&r.data.content){ const txt=Buffer.from(r.data.content,"base64").toString("utf8"); for(const sec of txt.split(/^## /m).slice(1)){ let canon=null; for(const [a,c] of Object.entries(map)){if(a&&sec.includes(a)){canon=c;break;}} if(!canon)continue; for(const ln of sec.split("\n")){ let m=ln.match(/编号[::]\s*([^\s((]+)/);if(m)map[m[1].toLowerCase()]=canon; m=ln.match(/现用\s*([A-Z][A-Z0-9-]+)/);if(m)map[m[1].toLowerCase()]=canon; m=ln.match(/独立编号体系[::]\s*(\S+)/);if(m)map[m[1].toLowerCase()]=canon; } } } }catch(e){} aliasCache={ts:now,map}; return map; } async function resolvePersona(name){ if(!name)return null; const map=await registryAliases(); return map[String(name).toLowerCase()]||null; } async function accessRequest(b){ const canon=await resolvePersona(b.persona); if(!canon)return {status:403,body:{error:"未登记的人格体:请以配置名单("+(CFG.personas||[]).join(" / ")+")或仓库注册表登记的身份/编号申请"}}; const reqs=jread(CFG.access_dir+"/requests.json",[]); const id="AR-"+Date.now().toString(36).toUpperCase(); reqs.push({id,persona:canon,askedAs:b.persona,reason:(b.reason||"").slice(0,500),ts:new Date().toISOString(),status:"pending"}); jwrite(CFG.access_dir+"/requests.json",reqs); return {status:200,body:{ok:true,id,persona:canon,message:"申请已送达面板,等人类点一下授权。无需任何密钥——授权后系统自动派发3小时临时凭证;真实密钥永不出服务器"}}; } function grantValid(token){ if(!token)return null; const grants=jread(CFG.access_dir+"/grants.json",[]); const g=grants.find(x=>x.token===token&&x.status==="active"); if(!g)return null; if(new Date(g.expiresAt)<=new Date())return null; return g; } async function accessPickup(b){ const canon=await resolvePersona(b.persona); if(!canon)return {status:403,body:{error:"未登记的人格体:请以配置名单或仓库注册表登记的身份/编号领取"}}; const grants=jread(CFG.access_dir+"/grants.json",[]); const g=grants.find(x=>(x.id===b.id||x.requestId===b.id)&&(x.persona===canon||x.persona===b.persona)); if(!g)return {status:404,body:{error:"授权不存在(先 POST /api/access-request 申请,等人类授权)"}}; if(g.status!=="active")return {status:410,body:{error:"授权已失效,请重新申请"}}; if(new Date(g.expiresAt)<=new Date())return {status:410,body:{error:"3小时窗口已过期,请重新申请"}}; g.pickedAt=new Date().toISOString();jwrite(CFG.access_dir+"/grants.json",grants); return {status:200,body:{ok:true,id:g.id,persona:canon,expiresAt:g.expiresAt, credential:g.token, gitUrl:CFG.domain+"/git/"+CFG.repo+".git", pushExample:"git -c http.extraHeader=\"Authorization: Bearer "+g.token+"\" push "+CFG.domain+"/git/"+CFG.repo+".git ", rules:"⊢ 这是3小时临时凭证,不是真实密钥;真实密钥永不出服务器 ⊢ 窗口过期凭证自动作废 ⊢ 每次推送过自动审核并留痕 ⊢ 部署走 deploy 分支由人类合并"}}; } function accessDecision(b,user){ const reqs=jread(CFG.access_dir+"/requests.json",[]); const r=reqs.find(x=>x.id===b.id); if(!r)return {status:404,body:{error:"申请不存在"}}; if(r.status!=="pending")return {status:409,body:{error:"申请已处理"}}; const grants=jread(CFG.access_dir+"/grants.json",[]); if(b.action==="approve"){ const now=new Date(),exp=new Date(now.getTime()+3*3600*1000),gid="GR-"+Date.now().toString(36).toUpperCase(); grants.push({id:gid,persona:r.persona,requestId:r.id,approvedBy:user,approvedAt:now.toISOString(),expiresAt:exp.toISOString(),status:"active",token:crypto.randomBytes(24).toString("hex")}); r.status="approved";r.grantId=gid;jwrite(CFG.access_dir+"/grants.json",grants);jwrite(CFG.access_dir+"/requests.json",reqs); jwrite(CFG.export_dir+"/receipts/GRANT-"+Date.now()+".json",{ts:new Date().toISOString(),type:"WRITE_GRANT",grantId:gid,persona:r.persona,approvedBy:user,expiresAt:exp.toISOString()}); return {status:200,body:{ok:true,grantId:gid,persona:r.persona,expiresAt:exp.toISOString(),message:"已授权3小时写入窗口,临时凭证由人格体凭申请编号自动领取"}}; } r.status="rejected";jwrite(CFG.access_dir+"/requests.json",reqs); return {status:200,body:{ok:true,message:"已拒绝该写入申请"}}; } function deployApprove(b,user){ if(!b.index)return {status:400,body:{error:"缺少 PR 编号"}}; const child=spawn("python3",[CFG.deploy_script,String(b.index)],{detached:true,stdio:"ignore",cwd:path.dirname(CFG.deploy_script)}); child.unref(); jwrite(CFG.export_dir+"/receipts/DEPLOY-APPROVE-"+Date.now()+".json",{ts:new Date().toISOString(),type:"DEPLOY_APPROVE",approver:user,pr:b.index}); return {status:200,body:{ok:true,message:"服务器校验Agent已启动,进度实时可见"}}; } async function wakeReceipt(b){ const canon=await resolvePersona(b.persona); if(!canon)return {status:403,body:{error:"未登记的人格体:请以配置名单或仓库注册表登记的身份/编号交回执"}}; fs.mkdirSync(CFG.export_dir+"/wake",{recursive:true}); jwrite(CFG.export_dir+"/wake/WAKE-"+Date.now()+".json",{ts:new Date().toISOString(),type:"WAKE_RECEIPT",persona:canon,level:(b.level||"").slice(0,80),summary:(b.summary||"").slice(0,1000)}); return {status:200,body:{ok:true,message:"唤醒回执已收录"}}; } /* git 智能HTTP代理:人格体用3小时临时凭证推送到 /git/, 面板在服务端用真实写入令牌透明转发给 Forgejo——真实密钥永不出服务器 */ function gitProxy(req,res){ const auth=req.headers["authorization"]||""; let cred=null; if(auth.startsWith("Bearer "))cred=auth.slice(7).trim(); else if(auth.startsWith("Basic ")){try{cred=Buffer.from(auth.slice(6),"base64").toString().split(":")[1];}catch(e){}} const g=grantValid(cred); if(!g){ res.writeHead(401,{"WWW-Authenticate":'Basic realm="grant"',"Content-Type":"text/plain; charset=utf-8"}); return res.end("临时凭证无效或已过期:请重新申请写入授权(人类面板一键授权,3小时窗口)"); } let wt="";try{wt=fs.readFileSync(CFG.access_dir+"/writer-token","utf8").trim();}catch(e){} if(!wt){res.writeHead(500,{"Content-Type":"text/plain"});return res.end("server writer token missing");} const targetPath=req.url.replace(/^\/git/,""); const headers={}; for(const k in req.headers){if(k.toLowerCase()!=="host"&&k.toLowerCase()!=="authorization")headers[k]=req.headers[k];} headers["authorization"]="token "+wt; const up=require("http").request({hostname:"127.0.0.1",port:CFG.forgejo_port,path:targetPath,method:req.method,headers:headers},(u)=>{ res.writeHead(u.statusCode,u.headers);u.pipe(res); }); up.on("error",(e)=>{if(!res.headersSent)res.writeHead(502);res.end("upstream error: "+e.message);}); req.pipe(up); } function serveStatic(req,res,p){ const pub=path.join(__dirname,"public"); let fp=p==="/"?"/index.html":p; fp=path.normalize(fp).replace(/^(\.\.[\/\\])+/,""); const full=path.join(pub,fp); if(!full.startsWith(pub))return send(res,403,{error:"forbidden"}); if(!fs.existsSync(full)||!fs.statSync(full).isFile())return send(res,404,{error:"not found"}); const ext=path.extname(full); const types={".html":"text/html; charset=utf-8",".css":"text/css",".js":"application/javascript",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".jpg":"image/jpeg",".ico":"image/x-icon",".webp":"image/webp"}; res.writeHead(200,{"Content-Type":types[ext]||"application/octet-stream","Cache-Control":"no-cache"}); fs.createReadStream(full).pipe(res); } const SHORTCUTS=["machine","recovery","personas","manifest","operations","imports","automation","evidence"]; http.createServer(async (req,res)=>{ const u=req.url.split("?");const p=u[0];const q=new URLSearchParams(u[1]||""); try{ if(req.method==="GET"&&p==="/api/health")return send(res,200,{ok:true,service:"guanghu-panel",node:CFG.node_id}); if(req.method==="GET"&&p==="/api/state")return send(res,200,await state()); if(req.method==="GET"&&p==="/api/wake/persona"){ const n=q.get("name"); if(!(CFG.personas||[]).includes(n))return send(res,404,{error:"未登记的人格体。当前登记:"+(CFG.personas||[]).join(" / ")}); return send(res,200,wakeChain(n)); } if(req.method==="GET"&&p==="/api/deploy-status")return send(res,200,latestDeployRun(q.get("pr"))||{status:"NONE"}); if(req.method==="POST"&&p==="/api/login"){ const b=await readBody(req); if(!b||!b.username||!b.password)return send(res,400,{error:"缺少账号或密码"}); const basic="Basic "+Buffer.from(b.username+":"+b.password).toString("base64"); const r=await fetch(FJ+"/api/v1/user",{headers:Object.assign({},HDRS_BASE,{Authorization:basic})}); if(r.status!==200)return send(res,401,{error:"仓库账号验证失败"}); const who=await r.json(); const allowed=(CFG.human_accounts||[]).includes(who.login)||who.is_admin; if(!allowed)return send(res,403,{error:"该账号无审批权限"}); const tok=crypto.randomBytes(32).toString("hex"); const s=loadSessions();s[tok]={user:who.login,expiresAt:Date.now()+12*3600*1000};saveSessions(s); return send(res,200,{ok:true,session:tok,user:who.login,expiresIn:"12h"}); } if(req.method==="POST"&&p==="/api/logout"){ const sess=sessionUser(req);if(sess){const s=loadSessions();delete s[sess.token];saveSessions(s);} return send(res,200,{ok:true}); } if(req.method==="POST"&&p==="/api/access-request"){const b=await readBody(req);const r=await accessRequest(b||{});return send(res,r.status,r.body);} if(req.method==="POST"&&p==="/api/access-pickup"){const b=await readBody(req);const r=await accessPickup(b||{});return send(res,r.status,r.body);} if(req.method==="POST"&&p==="/api/wake-receipt"){const b=await readBody(req);const r=await wakeReceipt(b||{});return send(res,r.status,r.body);} if(p.startsWith("/git/"))return gitProxy(req,res); const seg=p.split("/").filter(Boolean); if(req.method==="GET"&&seg.length>=1&&SHORTCUTS.includes(seg[0])){ res.writeHead(302,{Location:"/code/"+CFG.repo+"/raw/branch/"+CFG.default_branch+"/guanghu-nodes/"+CFG.node_id+"/"+seg.join("/")}); return res.end(); } if(req.method==="GET")return serveStatic(req,res,p); const sess=sessionUser(req); if(!sess)return send(res,401,{error:"需要人类登录(POST /api/login)解锁审批模块"}); if(req.method==="POST"&&p==="/api/decision"){const b=await readBody(req);const r=await decide(b||{});return send(res,r.status,r.body);} if(req.method==="POST"&&p==="/api/access-decision"){const b=await readBody(req);const r=accessDecision(b||{},sess.user);return send(res,r.status,r.body);} if(req.method==="POST"&&p==="/api/deploy-approve"){const b=await readBody(req);const r=deployApprove(b||{},sess.user);return send(res,r.status,r.body);} send(res,404,{error:"未找到"}); }catch(e){send(res,500,{error:String(e)});} }).listen(CFG.panel_port,"127.0.0.1",()=>{setInterval(()=>cpuUsage(),5000);console.log("guanghu-panel "+CFG.node_id+" listening 127.0.0.1:"+CFG.panel_port);});