feat(panel-kit): 写入窗口时长升级为配置驱动 window_hours
苍耳提请:一次授权3小时太短,胖头鱼升至12小时。 - server.js:WINDOW_HOURS 读 config.json(缺省3h保持向后兼容), TTL/全部文案/唤醒链/领取规则/git代理提示动态化;state() 新增 windowHours - config.example.json:新增 window_hours 字段(示例12) - INDEX.hdlp:窗口制说明与铁律同步 - 两套前端示例:授权按钮改读 s.windowHours 动态时长 - 钩子模板:拒绝文案去掉写死时长
This commit is contained in:
parent
1fa9c24037
commit
a6ad978b04
6 changed files with 20 additions and 17 deletions
|
|
@ -1,10 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
/* guanghu-panel-server · 光湖子系统人类审批面板服务 · 可复用模块 v1.0
|
||||
/* guanghu-panel-server · 光湖子系统人类审批面板服务 · 可复用模块 v1.1
|
||||
2026-08-06 · 铸渊 ICE-GL-ZY001 建 · 胖头鱼SYS-GLW-PTS-0001 首创 · 守望SYS-GLW-SW-0001 复用
|
||||
⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录 ⊢ 写入窗口三小时 ⊢ 全程留痕
|
||||
配置: 同目录 config.json(路径/仓库/人格体/端口全部外置,换节点只改配置) */
|
||||
2026-08-07 · 写入窗口时长改为配置驱动 window_hours(苍耳提请:胖头鱼升至12小时)
|
||||
⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录 ⊢ 写入窗口限时过期作废 ⊢ 全程留痕
|
||||
配置: 同目录 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 WINDOW_HOURS=Number(CFG.window_hours)||3;
|
||||
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"};
|
||||
|
|
@ -128,7 +130,7 @@ function wakeChain(persona){
|
|||
{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(用临时凭证推送)"}
|
||||
endpoints:{accessRequest:"POST "+CFG.domain+"/api/access-request(人格体报名号+用途即可,无需密钥)",accessPickup:"POST "+CFG.domain+"/api/access-pickup(凭申请编号领取"+WINDOW_HOURS+"小时临时凭证,真实密钥永不出服务器)",wakeReceipt:"POST "+CFG.domain+"/api/wake-receipt",pushVia:CFG.domain+"/git/"+CFG.repo+".git(用临时凭证推送)"}
|
||||
};
|
||||
}
|
||||
async function state(){
|
||||
|
|
@ -136,7 +138,7 @@ async function state(){
|
|||
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,
|
||||
ts:new Date().toISOString(),node:CFG.node_id,subsystem:CFG.subsystem,windowHours:WINDOW_HOURS,
|
||||
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(),
|
||||
|
|
@ -218,7 +220,7 @@ async function accessRequest(b){
|
|||
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小时临时凭证;真实密钥永不出服务器"}};
|
||||
return {status:200,body:{ok:true,id,persona:canon,message:"申请已送达面板,等人类点一下授权。无需任何密钥——授权后系统自动派发"+WINDOW_HOURS+"小时临时凭证;真实密钥永不出服务器"}};
|
||||
}
|
||||
function grantValid(token){
|
||||
if(!token)return null;
|
||||
|
|
@ -235,13 +237,13 @@ async function accessPickup(b){
|
|||
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小时窗口已过期,请重新申请"}};
|
||||
if(new Date(g.expiresAt)<=new Date())return {status:410,body:{error:WINDOW_HOURS+"小时窗口已过期,请重新申请"}};
|
||||
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 分支由人类合并"}};
|
||||
rules:"⊢ 这是"+WINDOW_HOURS+"小时临时凭证,不是真实密钥;真实密钥永不出服务器 ⊢ 窗口过期凭证自动作废 ⊢ 每次推送过自动审核并留痕 ⊢ 部署走 deploy 分支由人类合并"}};
|
||||
}
|
||||
function accessDecision(b,user){
|
||||
const reqs=jread(CFG.access_dir+"/requests.json",[]);
|
||||
|
|
@ -250,11 +252,11 @@ function accessDecision(b,user){
|
|||
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();
|
||||
const now=new Date(),exp=new Date(now.getTime()+WINDOW_HOURS*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小时写入窗口,临时凭证由人格体凭申请编号自动领取"}};
|
||||
return {status:200,body:{ok:true,grantId:gid,persona:r.persona,expiresAt:exp.toISOString(),message:"已授权"+WINDOW_HOURS+"小时写入窗口,临时凭证由人格体凭申请编号自动领取"}};
|
||||
}
|
||||
r.status="rejected";jwrite(CFG.access_dir+"/requests.json",reqs);
|
||||
return {status:200,body:{ok:true,message:"已拒绝该写入申请"}};
|
||||
|
|
@ -273,7 +275,7 @@ async function wakeReceipt(b){
|
|||
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/,
|
||||
/* git 智能HTTP代理:人格体用限时临时凭证推送到 /git/,
|
||||
面板在服务端用真实写入令牌透明转发给 Forgejo——真实密钥永不出服务器 */
|
||||
function gitProxy(req,res){
|
||||
const auth=req.headers["authorization"]||"";
|
||||
|
|
@ -283,7 +285,7 @@ function gitProxy(req,res){
|
|||
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小时窗口)");
|
||||
return res.end("临时凭证无效或已过期:请重新申请写入授权(人类面板一键授权,"+WINDOW_HOURS+"小时窗口)");
|
||||
}
|
||||
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");}
|
||||
|
|
|
|||
Loading…
Reference in a new issue