guanghu-ice-heart/modules/guanghu-panel-kit/panel/server.js
冰朔 cf3e106ec9 fix(modules): access 解析器支持 JSON 注册表(守望 registry.json)+ 认知链022 + 守望 default_branch 纠正登记
耳耳蛋反馈 pickup 不认注册编号 → 检查确认守望同样未对接 → 解析器双格式化(JSON 对象遍历 + HDLP 编号行提取)
守望实测 PER-MM-ARCH-001→yaoyan、曜识→yaoshi;胖头鱼 PTS-VA-001-EED 回归通过
2026-08-07 05:51:16 +08:00

359 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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");
let parsedAsJson=false;
try{
const j=JSON.parse(txt);parsedAsJson=true;
const findCanon=(o)=>{for(const [a,c] of Object.entries(map)){if(a&&((o.name||"").toLowerCase()===a||(o.id||"").toLowerCase()===a))return c;}return null;};
(function walk(o){
if(Array.isArray(o)){o.forEach(walk);return;}
if(o&&typeof o==="object"){
if(typeof o.name==="string"&&typeof o.id==="string"){
const canon=findCanon(o);
if(canon){map[o.id.toLowerCase()]=canon;map[o.name.toLowerCase()]=canon;(o.aliases||[]).forEach(x=>{if(typeof x==="string")map[x.toLowerCase()]=canon;});}
}
Object.values(o).forEach(walk);
}
})(j);
}catch(e){}
if(!parsedAsJson){
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 <branch>",
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);});