feat(modules): panel access 子系统对接仓库人格体注册表(CA-PERSONA-REGISTRY 动态解析+别名归一,pickup 认注册编号)

This commit is contained in:
冰朔 2026-08-07 05:28:49 +08:00
commit 8ec27ec00f
2 changed files with 124 additions and 28 deletions

View file

@ -2,8 +2,13 @@
"node_id": "MM-GZ-001",
"subsystem": "守望语言子系统 · 宠物行业",
"human_name": "毛毛",
"human_accounts": ["maomao"],
"personas": ["yaoyan", "yaoshi"],
"human_accounts": [
"maomao"
],
"personas": [
"yaoyan",
"yaoshi"
],
"repo": "maomao/maomao-fifth-domain",
"default_branch": "guanghu/main",
"deploy_branch": "deploy",
@ -19,6 +24,26 @@
"deploy_runs_dir": "/srv/guanghu/mm-gz-001/deploy-agent/runs",
"deploy_script": "/srv/guanghu/mm-gz-001/deploy-agent/bin/verify-and-deploy.py",
"writer_user": "mm-writer",
"watch_services": ["guanghu-mm-forgejo.service", "guanghu-mm-tunnel.service", "guanghu-mm-panel.service"],
"writeback_paths": ["guanghu-nodes/MM-GZ-001/imports/notion", "guanghu-nodes/MM-GZ-001/contributions", "guanghu-nodes/MM-GZ-001/personas"]
}
"watch_services": [
"guanghu-mm-forgejo.service",
"guanghu-mm-tunnel.service",
"guanghu-mm-panel.service"
],
"writeback_paths": [
"guanghu-nodes/MM-GZ-001/imports/notion",
"guanghu-nodes/MM-GZ-001/contributions",
"guanghu-nodes/MM-GZ-001/personas"
],
"persona_registry": "CA-PERSONA-REGISTRY.hdlp",
"persona_aliases": {
"eererdan": [
"耳耳蛋",
"ICE-GL-耳耳蛋",
"PTS-VA-001-EED"
],
"jianying": [
"鉴影",
"ICE-GL-CA001"
]
}
}

View file

@ -126,9 +126,9 @@ function wakeChain(persona){
{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 与 key"}
action:"完成后 POST "+CFG.domain+"/api/wake-receipt 交回执(报 persona 与 summary 即可"}
],
endpoints:{accessRequest:"POST "+CFG.domain+"/api/access-request",accessPickup:"POST "+CFG.domain+"/api/access-pickup",wakeReceipt:"POST "+CFG.domain+"/api/wake-receipt"}
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(){
@ -161,26 +161,70 @@ async function decide(b){
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}};
}
function accessRequest(b){
if(!personaKeyOk(b.persona,b.key))return {status:403,body:{error:"人格体密钥错误"}};
/* 人格体身份解析:配置别名种子 + 动态读仓库人格体注册表(仓库是注册事实源) */
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:b.persona,reason:(b.reason||"").slice(0,500),ts:new Date().toISOString(),status:"pending"});
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,message:"申请已送达面板,等待人类审批。通过后用同一 id+key 调 /api/access-pickup 领取3小时写入令牌"}};
return {status:200,body:{ok:true,id,persona:canon,message:"申请已送达面板,等人类点一下授权。无需任何密钥——授权后系统自动派发3小时临时凭证真实密钥永不出服务器"}};
}
function accessPickup(b){
if(!personaKeyOk(b.persona,b.key))return {status:403,body:{error:"人格体密钥错误"}};
function grantValid(token){
if(!token)return null;
const grants=jread(CFG.access_dir+"/grants.json",[]);
const g=grants.find(x=>(x.id===b.id||x.requestId===b.id)&&x.persona===b.persona);
if(!g)return {status:404,body:{error:"授权不存在"}};
if(g.status!=="active")return {status:410,body:{error:"授权已失效"}};
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);
let tok="";try{tok=fs.readFileSync(CFG.access_dir+"/writer-token","utf8").trim();}catch(e){}
return {status:200,body:{ok:true,id:g.id,persona:g.persona,expiresAt:g.expiresAt,
gitUrl:"https://"+CFG.writer_user+":"+tok+"@"+CFG.domain_host+"/code/"+REPO+".git",
rules:"⊢ 窗口3小时过期作废 ⊢ 每次推送过自动审核并留痕 ⊢ 部署走 deploy 分支由人类合并"}};
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",[]);
@ -190,9 +234,10 @@ function accessDecision(b,user){
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"});
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);
return {status:200,body:{ok:true,grantId:gid,persona:r.persona,expiresAt:exp.toISOString(),message:"已授予3小时写入窗口"}};
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:"已拒绝该写入申请"}};
@ -204,12 +249,37 @@ function deployApprove(b,user){
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已启动进度实时可见"}};
}
function wakeReceipt(b){
if(!personaKeyOk(b.persona,b.key))return {status:403,body:{error:"人格体密钥错误"}};
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:b.persona,level:(b.level||"").slice(0,80),summary:(b.summary||"").slice(0,1000)});
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 HTTP3 /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");
@ -252,9 +322,10 @@ http.createServer(async (req,res)=>{
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=accessRequest(b||{});return send(res,r.status,r.body);}
if(req.method==="POST"&&p==="/api/access-pickup"){const b=await readBody(req);const r=accessPickup(b||{});return send(res,r.status,r.body);}
if(req.method==="POST"&&p==="/api/wake-receipt"){const b=await readBody(req);const r=wakeReceipt(b||{});return send(res,r.status,r.body);}
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("/")});