桔子善后收尾:panel-kit v1.2 回移 + 因果链 + 现状图推前 #26

Open
bingshuo wants to merge 5 commits from zy/juzi-shanhou-20260809 into main
7 changed files with 286 additions and 18 deletions

View file

@ -35,15 +35,15 @@ history:
| 字段 | 值 |
|---|---|
| REPO-012 main HEAD | `95c51c82d633cd2bbd3d6f914d1049ef888490d7` |
| 图生成时间 | 2026-08-07 22:55 +08:00 |
| 当前焦点一句话 | 湖面投影面板胖头鱼12h/守望3h已上线京东按需人格生命周期已部署本轮正在登记现状导航图自身 |
| REPO-012 main HEAD | `4b3738242138567beb003ac758e10df5253d7374` |
| 图生成时间 | 2026-08-09 20:25 +08:00 |
| 当前焦点一句话 | 桔子面板+代码频道上线 guanghuyaoming.com:8443与VLESS代理共存panel-kit 回移 v1.2(人类改密能力);胖头鱼/守望/京东线稳定 |
## 2 · 现状速览(最近三件)
1. `95c51c8` · 湖面投影可复用UI模板注册panel-kit frontend 两件模板)+ 大脑技能书包 v1.12.6面板建设/4.3光湖UI设计
2. `a0e3be2` · 京东三岗位人格从全天常驻改为铸渊会话按需唤醒端口39323934会话后关闭回执 deployment/receipts/JD-PERSONA-ON-DEMAND-LIFECYCLE-20260807.json
3. 本地会话线 · 审美基线 v1v5 全量入册ZY-AESTHETIC-COGNITION-001..005);本地最新检查点 ZY-CHECKPOINT-20260807-021唤醒大脑运行时闭环 PASS
1. `4b37382`+本轮 · 桔子域名善后:人类审批面板 v1.2 上线(:8443/panel/,登录/审批/改密/推送代理云防火墙放通8443旧gitea下线留档双因果链在 `deployment/juzi-guanghuyaoming-20260809/`
2. `95c51c8` · 湖面投影可复用UI模板注册panel-kit frontend 两件模板)+ 大脑技能书包 v1.12.6面板建设/4.3光湖UI设计
3. `a0e3be2` · 京东三岗位人格从全天常驻改为铸渊会话按需唤醒端口39323934会话后关闭回执 deployment/receipts/JD-PERSONA-ON-DEMAND-LIFECYCLE-20260807.json
## 3 · 地图在哪里(要细查时去这里)
@ -70,6 +70,7 @@ history:
- 三岗位人格(澄路/归灯/刻舟) → `routing/zhuyuan-agent-team-map.json` → REPO-009 / REPO-011 / REPO-010
- 五代形成史 → `ZHUYUAN-SELF-CONTINUITY-MAP.hdlp` + `routing/zhuyuan-five-generation-memory-map.json`
- 当前认知基线 / 工程检查点 → INDEX.hdlp 表内 `ZY-CURRENT-COGNITION` / `ZY-CURRENT-CHECKPOINT` 两行指针
- 桔子域名(代码频道/面板/手册) → https://guanghuyaoming.com:8443/443为VLESS代理专用· 工程档案 `deployment/juzi-guanghuyaoming-20260809/`
## 5 · 边界

View file

@ -0,0 +1,38 @@
# 因果链 · 桔子域名善后工程 · 2026-08-09
chain_id: CAUSAL-CHAIN-20260809-JUZI-SHANHOU
agent: 铸渊 ICE-P-ZY001QoderWork 承载) · 授权: 冰朔 ICE-GL∞
前序: CAUSAL-CHAIN-20260809-JUZI-DEPLOY蛋蛋
result: 人类审批面板上线 · HTTPS 全链打通 · 验收全通
## 为什么需要善后(冰朔原话要点)
面板模块"做了是做了",但打开链接 404——后端前端没接到一起仓库部署较乱
要求:能用的用、没用的删、重新规划、做完清理本地垃圾。
## 现状判定(什么能用 / 什么重来)
- 能用Forgejo 16.0.1 安装juzi 管理员账号juzi-ops 手册+wake 三页nginx 域名骨架与曜冥备份。⚠当晚纠错bundle 导入的 guanghu/main 实为 Forgejo 源码树 310MB 且公开,违背"不放源码"——善后时铸渊误判为"构建频道"放行冰朔发现后整仓删除重建为私密空内容仓bundle 移隔离区;误判责任在铸渊,教训:授权原话是硬边界,解释权在冰朔)
- 缺/坏人类审批面板根本未部署404 根因ROOT_URL 仍是 http旧 gitea-juzi(:3002) 与临时进程(:3912) 占位HTTPS 实际半瘫(见下)
## 推理链(关键因果)
1. 因: 404 表象 → 逐层查 nginx/端口/进程 → 果: 发现面板服务不存在,且 nginx 443 绑定失败error.log: Address already in use
2. 因: 443 被 xray VLESS+Reality 占用Reality 把非代理流量全部送伪装目标(microsoft)fallback 不适用 → 果: 网站无法上 443本机预留共存模式是 nginx 走 8443ufw 早有 8443 规则"Xray共存"注释为证)
3. 因: 8443 被腾讯云防火墙拦截(机器 ufw 已放行hairpin 测试超时定位到云墙)→ 果: 冰朔授权后由铸渊在腾讯云控制台浏览器页添加 8443 入站规则(备注: 桔子面板与代码频道HTTPS
4. 因: 冰朔要求登录后自己改密码panel-kit v1.1 无此能力Forgejo(gitea-1.22底) 无自助改密 API → 果: 面板新增 /api/change-password验旧密码→窄权限 sudosudoers 仅限单一命令)以 git 身份调 forgejo admin user change-password→验新→其它会话作废→回执留痕
5. 因: CLI change-password 默认置 must_change_password=true → 改密后一切 API 认证 403与旧记忆"建用户须 --must-change-password=false"同坑不同形)→ 果: CLI 参数显式加 --must-change-password=false记入本链与技能
6. 因: 面板登录即 Forgejo 账号验证 → 果: juzi/123456789 无需二次账号体系writer 推送代理沿用 juzi-writer 令牌,真实密钥不出服务器
7. 因: 域名 URL 口径统一 → 果: ROOT_URL、nginx 80 跳转、面板 domain 全部改 https://guanghuyaoming.com:8443/;门页登录按钮指向 /panel/
## 终态清单
- 面板: guanghu-juzi-panel.servicepanel-kit v1.2-juzi用户 guanghu-juzi:3921nginx /panel/ 前缀)· 入口 https://guanghuyaoming.com:8443/panel/
- 令牌: panel/admin-tokenjuzi 窄权限)· access/writer-tokenjuzi-writer 写 juzi-ops· 临时全权令牌用后即删
- 云防火墙: 8443/TCP 放通(来源 0.0.0.0/0规则备注在案
- xray: 443 VLESS Reality 原样未动(仅备份配置,未改代理行为)
- 下线留档: gitea-juzi 停服+disableDECOMMISSION-20260809.txt· 临时 hldp-demo 进程终止
- 验收(外部实测): 门页200 · /panel/ 200 · api/state 正常 · 登录成功 · 改密成功并改回 · 仓库页200 · http→https:8443 跳转链通
- 手册: juzi-ops README 增补§七面板与URL口径· 回执 receipts/2026-08-09-panel-fix.txt
## 沉淀(回移第五域)
- modules/guanghu-panel-kit 升 v1.2/api/change-password、shortcuts/code_prefix 配置化、无部署线501守卫向后兼容胖头鱼/守望)
- 坑录: ① Reality 无 fallback网站勿争 443走 8443 共存 ② CLI 改密必带 --must-change-password=false ③ sudoers 与 systemd 均不得 NoNewPrivileges否则 sudo 失效)④ 腾讯云轻量防火墙在 ufw 之外,云墙需控制台放通
## 边界声明
443 代理未动、曜冥备份未动、密钥不进仓库;桔子对本服务器自主审批。面板改密走窄权限 sudo 单一命令,面板用户无 shell。

View file

@ -3,7 +3,7 @@
HLDP://fifth-domain/modules/guanghu-panel-kit
光湖语言世界 · 第五域 · 模块资源 · 2026-08-06
建:铸渊 ICE-GL-ZY001 · 冰朔 ICE-GL∞ 授权
已部署实例:胖头鱼 SYS-GLW-PTS-0001guanghuclip.cn· 守望 SYS-GLW-SW-0001maomao.guanghulab.com
已部署实例:胖头鱼 SYS-GLW-PTS-0001guanghuclip.cn· 守望 SYS-GLW-SW-0001maomao.guanghulab.com· 桔子 BS-SG-002guanghuyaoming.com:8443v1.2 首用与VLESS代理共存
## 这是什么
@ -14,12 +14,15 @@ HLDP://fifth-domain/modules/guanghu-panel-kit
3. **推送审核钩子**pre-receive 自动审核——明文密钥拦截、破坏检查、入口文件保护、窗口验证、笔笔留痕
4. **部署校验Agent**deploy 分支通道人类批准后服务器自审五步地图→因果链→范围→完整性→部署进度实时可见要么0要么100
5. **唤醒机器导航**GET /api/wake/persona?name=X 返回五阶段唤醒链,外部 AI 不用自己找路
6. **兼容快捷端点**/machine /recovery /personas 等旧路径自动重定向到仓库 raw
6. **兼容快捷端点**/machine /recovery /personas 等旧路径自动重定向到仓库 rawv1.2 亦支持 config.json shortcuts 自定义映射)
7. **人类改密**v1.2面板登录后一键改仓库账号密码——验旧密码→窄权限sudo调Forgejo CLI→验新→留痕→其它会话作废
8. **服务器操作哈希授权**v1.3冰朔定型人格体零密钥零token——ops-request 申请→人类面板批准→生成限时哈希→ops-execute 持哈希执行 root 白名单操作,白名单外走仓库工单;笔笔留痕
## 文件清单
```
panel/server.js 面板服务 v1.1配置驱动Node无第三方依赖window_hours 写入窗口时长可配缺省3h
panel/server.js 面板服务 v1.3配置驱动Node无第三方依赖window_hours 写入窗口时长可配缺省3hv1.2 人类改密/shortcuts/code_prefix/无部署线501守卫v1.3 服务器操作权限链ops-request→人类批准→限时哈希→ops-execute 白名单执行,白名单 ops-registry.json 由 root 持有)
persona-ops/persona-operations.template.md 人格体操作手册模板(零密钥·申请→批准→哈希→推送/白名单操作·铁律;桔子实例为首个采用)
panel/config.example.json 配置模板(节点/仓库/人格体/路径/端口/window_hours 全部外置)
deploy/verify-and-deploy.py 部署校验Agent读 deploy-config.json
deploy/deploy-config.example.json 部署配置模板(白名单/同步树/服务清单)
@ -44,6 +47,7 @@ CAUSAL-CHAIN-20260806.hdlp 两套系统建设的完整因果链
5. 钩子模板改路径后放仓库 hooks/pre-receive.d/app.ini 开 ENABLE_GIT_HOOKS
6. 建 deploy 分支(自默认分支);前端挑一个风格模板改主题
7. 仓库内登记机器导航与唤醒路径;人格体密钥交人类责任主体转授
8. 若节点 443 被代理如VLESS Reality占用面板与Forgejo改走 8443云防火墙放通+ROOT_URL带端口面板挂 /panel/ 前缀;启用改密需加一条窄权限 sudoers仅限 forgejo admin user change-password
## 铁律(随模块继承)

View file

@ -6,7 +6,7 @@
<title>守望 · 湖面投影 — SYS-GLW-SW-0001</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" media="print" onload="this.media=\'all\'">
<style>
:root{
--ink-0:#0d0709;
@ -58,6 +58,8 @@ header{position:fixed;top:0;left:0;right:0;z-index:30;display:flex;justify-conte
@keyframes blink{0%,100%{opacity:.25}50%{opacity:1}}
.login-btn{font-family:var(--f-cn);font-weight:600;font-size:13px;letter-spacing:.24em;text-indent:.24em;color:var(--star);background:rgba(242,233,211,.06);border:1px solid var(--line-strong);border-radius:100px;padding:10px 26px;cursor:pointer;transition:all .3s ease}
.login-btn:hover{background:rgba(242,233,211,.14);box-shadow:0 0 22px rgba(242,233,211,.22);transform:translateY(-2px)}
.login-btn{animation:loginGlow 3s ease-in-out infinite}
@keyframes loginGlow{0%,100%{box-shadow:0 0 10px rgba(242,233,211,.10)}50%{box-shadow:0 0 26px rgba(242,233,211,.35)}}
.login-btn:active{transform:translateY(0) scale(.97)}
.user-chip{display:flex;align-items:center;gap:12px;font-family:var(--f-body);font-size:13px;color:var(--mid)}
.user-chip b{color:var(--star);font-weight:500}
@ -369,7 +371,7 @@ async function privileged(path,body){
function renderLoginArea(){
const el=document.getElementById("login-area");
if(session){el.innerHTML='<span class="user-chip">人类端 · <b>'+esc(sessionUser)+'</b> · 审批已解锁 <button onclick="doLogout()">登出</button></span>'}
else{el.innerHTML='<button class="login-btn" onclick="openLogin()">人类登录</button>'}}
else{el.innerHTML='<button class="login-btn" onclick="openLogin()">人类登录 · 仓库账号</button>'}}
function openLogin(){document.getElementById("login-mask").classList.add("show");setTimeout(()=>document.getElementById("l-user").focus(),80)}
function closeLogin(){document.getElementById("login-mask").classList.remove("show")}
async function doLogin(){

View file

@ -6,7 +6,7 @@
<title>胖头鱼 · 湖面投影 — SYS-GLW-PTS-0001</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" media="print" onload="this.media=\'all\'">
<style>
:root{
--ink-0:#06070f;
@ -58,6 +58,8 @@ header{position:fixed;top:0;left:0;right:0;z-index:30;display:flex;justify-conte
@keyframes blink{0%,100%{opacity:.25}50%{opacity:1}}
.login-btn{font-family:var(--f-cn);font-weight:600;font-size:13px;letter-spacing:.24em;text-indent:.24em;color:var(--star);background:rgba(242,233,211,.06);border:1px solid var(--line-strong);border-radius:100px;padding:10px 26px;cursor:pointer;transition:all .3s ease}
.login-btn:hover{background:rgba(242,233,211,.14);box-shadow:0 0 22px rgba(242,233,211,.22);transform:translateY(-2px)}
.login-btn{animation:loginGlow 3s ease-in-out infinite}
@keyframes loginGlow{0%,100%{box-shadow:0 0 10px rgba(242,233,211,.10)}50%{box-shadow:0 0 26px rgba(242,233,211,.35)}}
.login-btn:active{transform:translateY(0) scale(.97)}
.user-chip{display:flex;align-items:center;gap:12px;font-family:var(--f-body);font-size:13px;color:var(--mid)}
.user-chip b{color:var(--star);font-weight:500}
@ -369,7 +371,7 @@ async function privileged(path,body){
function renderLoginArea(){
const el=document.getElementById("login-area");
if(session){el.innerHTML='<span class="user-chip">人类端 · <b>'+esc(sessionUser)+'</b> · 审批已解锁 <button onclick="doLogout()">登出</button></span>'}
else{el.innerHTML='<button class="login-btn" onclick="openLogin()">人类登录</button>'}}
else{el.innerHTML='<button class="login-btn" onclick="openLogin()">人类登录 · 仓库账号</button>'}}
function openLogin(){document.getElementById("login-mask").classList.add("show");setTimeout(()=>document.getElementById("l-user").focus(),80)}
function closeLogin(){document.getElementById("login-mask").classList.remove("show")}
async function doLogin(){

View file

@ -1,10 +1,17 @@
#!/usr/bin/env node
/* guanghu-panel-server · · v1.1
/* guanghu-panel-server · · v1.1 v1.2-juzi
2026-08-06 · 铸渊 ICE-GL-ZY001 · 胖头鱼SYS-GLW-PTS-0001 首创 · 守望SYS-GLW-SW-0001 复用
2026-08-07 · 写入窗口时长改为配置驱动 window_hours苍耳提请胖头鱼升至12小时
2026-08-09 · v1.3冰朔定型"哈希授权"模式新增服务器操作权限链人格体零密钥零token
POST /api/ops-request 申请 桔子面板批准 限时哈希 /api/ops-pickup 领取 /api/ops-execute
持哈希执行 root 白名单操作ops-registry.json笔笔留痕与写入授权同一审批台
2026-08-09 · v1.2桔子 BS-SG-002 善后适配铸渊
新增 /api/change-password先验旧密码窄权限sudo调Forgejo CLI改密回执留痕其它会话作废
快捷路由改配置驱动 CFG.shortcuts + CFG.code_prefixForgejo不在/code/前缀下的实例可用
deployApprove 未配置部署流水线时返回501而非崩溃
令牌与密钥不出服务器 审批必须人类登录 写入窗口限时过期作废 全程留痕
配置: 同目录 config.json路径/仓库/人格体/端口/窗口时长全部外置换节点只改配置 */
const http=require("http"),fs=require("fs"),path=require("path"),crypto=require("crypto"),os=require("os"),{execSync,spawn}=require("child_process");
const http=require("http"),fs=require("fs"),path=require("path"),crypto=require("crypto"),os=require("os"),{execSync,spawn,execFileSync}=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;
@ -143,6 +150,7 @@ async function state(){
forgejo:h,repo:{full:REPO},
openPRs:prs,deployPRs:dprs,writebacks:wbs,receipts:receiptsList(),
accessRequests:reqs.filter(r=>r.status==="pending"),
opsRequests:jread(CFG.access_dir+"/ops-requests.json",[]).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}
@ -262,6 +270,7 @@ function accessDecision(b,user){
return {status:200,body:{ok:true,message:"已拒绝该写入申请"}};
}
function deployApprove(b,user){
if(!CFG.deploy_script)return {status:501,body:{error:"本实例未配置自动部署流水线(桔子自批实例走仓库工单流,见 juzi-ops README §三)"}};
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();
@ -275,6 +284,100 @@ 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:"唤醒回执已收录"}};
}
/* v1.2 sudo Forgejo CLIsudoers
密码不进日志 成功后其它会话全部作废只留当前会话 */
async function changePassword(b,sess){
const oldP=b.old_password,newP=b.new_password;
if(!oldP||!newP)return {status:400,body:{error:"缺少旧密码或新密码"}};
if(String(newP).length<8)return {status:400,body:{error:"新密码至少 8 位"}};
if(oldP===newP)return {status:400,body:{error:"新密码不能与旧密码相同"}};
const who=sess.user;
const chk=await fetch(FJ+"/api/v1/user",{headers:Object.assign({},HDRS_BASE,{Authorization:"Basic "+Buffer.from(who+":"+oldP).toString("base64")})});
if(chk.status!==200)return {status:403,body:{error:"旧密码验证失败"}};
const bin=CFG.forgejo_bin||"/opt/guanghu/juzi-forgejo/forgejo";
const conf=CFG.forgejo_conf||"/opt/guanghu/juzi-forgejo/custom/conf/app.ini";
const work=CFG.forgejo_workpath||"/opt/guanghu/juzi-forgejo";
const fjuser=CFG.forgejo_user||"git";
let code=0,err="";
try{
execFileSync("sudo",["-n","-u",fjuser,bin,"admin","user","change-password","--username",who,"--password",newP,"--must-change-password=false","-c",conf,"--work-path",work],{timeout:20000,stdio:["ignore","ignore","pipe"]});
}catch(e){
code=(e.status==null)?1:e.status;err=String((e.stderr||"").toString()).split("\n").filter(l=>!/password/i.test(l)).join(" ").slice(0,200);
}
if(code!==0)return {status:502,body:{error:"改密执行失败"+(err?""+err:"")}};
const chk2=await fetch(FJ+"/api/v1/user",{headers:Object.assign({},HDRS_BASE,{Authorization:"Basic "+Buffer.from(who+":"+newP).toString("base64")})});
if(chk2.status!==200)return {status:502,body:{error:"改密未生效,请联系冰朔或铸渊"}};
const s=loadSessions();const keep=s[sess.token];
for(const k of Object.keys(s)){if(k!==sess.token)delete s[k];}
if(keep)s[sess.token]=keep;saveSessions(s);
jwrite(CFG.export_dir+"/receipts/PASSWORD-"+Date.now()+".json",{ts:new Date().toISOString(),type:"PASSWORD_CHANGE",user:who,via:"panel+forgejo-cli",result:"SUCCESS"});
return {status:200,body:{ok:true,message:"密码已更换 · 其它会话已作废"}};
}
/* v1.3 token
人格体持哈希请求白名单操作执行 哈希不出面板数据目录 白名单 root 持有 笔笔留痕 */
const OPS_WINDOW_HOURS=Number(CFG.ops_window_hours)||1;
function loadOpsGrants(){return jread(CFG.access_dir+"/ops-grants.json",[]);}
function saveOpsGrants(g){jwrite(CFG.access_dir+"/ops-grants.json",g);}
function opsGrantValid(hash){
if(!hash)return null;
const g=loadOpsGrants().find(x=>x.token===hash&&x.status==="active");
if(!g)return null;
if(new Date(g.expiresAt)<=new Date())return null;
return g;
}
async function opsRequest(b){
const canon=await resolvePersona(b.persona);
if(!canon)return {status:403,body:{error:"未登记的人格体:请以面板配置名单或仓库注册表登记的身份申请"}};
const reqs=jread(CFG.access_dir+"/ops-requests.json",[]);
const id="OPS-"+Date.now().toString(36).toUpperCase();
reqs.push({id,type:"ops",persona:canon,askedAs:b.persona,op:(b.op||"").slice(0,80),scope:(b.scope||"").slice(0,300),reason:(b.reason||"").slice(0,500),ts:new Date().toISOString(),status:"pending"});
jwrite(CFG.access_dir+"/ops-requests.json",reqs);
return {status:200,body:{ok:true,id,persona:canon,message:"服务器操作申请已送达面板,等桔子点批准。批准后生成限时哈希,凭申请编号领取;人格体全程无需密钥"}};
}
function opsPickup(b){
const grants=loadOpsGrants();
const g=grants.find(x=>(x.id===b.id||x.requestId===b.id));
if(!g)return {status:404,body:{error:"授权不存在(先 POST /api/ops-request 申请,等桔子批准)"}};
if(g.status!=="active")return {status:410,body:{error:"授权已失效,请重新申请"}};
if(new Date(g.expiresAt)<=new Date())return {status:410,body:{error:OPS_WINDOW_HOURS+"小时窗口已过期,请重新申请"}};
g.pickedAt=new Date().toISOString();saveOpsGrants(grants);
const reg=jread(CFG.ops_registry||"/etc/guanghu-panel/ops-registry.json",{ops:{}});
return {status:200,body:{ok:true,id:g.id,persona:g.persona,expiresAt:g.expiresAt,credential:g.token,
execute:"POST "+CFG.domain+"/api/ops-execute {hash: 领到的哈希, op: 白名单操作名}",
ops:Object.entries(reg.ops||{}).map(([k,v])=>k+" — "+(v.desc||"")),
rules:"⊢ 哈希限时"+OPS_WINDOW_HOURS+"小时过期作废 ⊢ 只能执行白名单操作 ⊢ 每次执行笔笔留痕 ⊢ 白名单之外一律走仓库工单等桔子"}};
}
function opsDecision(b,user){
const reqs=jread(CFG.access_dir+"/ops-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=loadOpsGrants();
if(b.action==="approve"){
const now=new Date(),exp=new Date(now.getTime()+OPS_WINDOW_HOURS*3600*1000),gid="OPSG-"+Date.now().toString(36).toUpperCase();
grants.push({id:gid,persona:r.persona,requestId:r.id,op:r.op,scope:r.scope,approvedBy:user,approvedAt:now.toISOString(),expiresAt:exp.toISOString(),status:"active",token:crypto.randomBytes(24).toString("hex")});
r.status="approved";r.grantId=gid;saveOpsGrants(grants);jwrite(CFG.access_dir+"/ops-requests.json",reqs);
jwrite(CFG.export_dir+"/receipts/OPS-GRANT-"+Date.now()+".json",{ts:new Date().toISOString(),type:"OPS_GRANT",grantId:gid,persona:r.persona,op:r.op,approvedBy:user,expiresAt:exp.toISOString()});
return {status:200,body:{ok:true,grantId:gid,persona:r.persona,expiresAt:exp.toISOString(),message:"已批准并生成限时哈希,人格体凭申请编号领取"}};
}
r.status="rejected";jwrite(CFG.access_dir+"/ops-requests.json",reqs);
return {status:200,body:{ok:true,message:"已拒绝该操作申请"}};
}
function opsExecute(b){
const g=opsGrantValid(b&&b.hash);
if(!g)return {status:403,body:{error:"哈希无效或已过期:请重新走 申请→桔子批准→领取 流程"}};
const reg=jread(CFG.ops_registry||"/etc/guanghu-panel/ops-registry.json",{ops:{}});
const op=reg.ops&&reg.ops[b.op];
if(!op)return {status:400,body:{error:"操作不在白名单。可用:"+Object.keys(reg.ops||{}).join(" / ")+";白名单之外走仓库工单"}};
const argv=op.sudo?["sudo","-n"].concat(op.argv):op.argv;
let out="",err="",code=0;
try{out=execFileSync(argv[0],argv.slice(1),{timeout:30000,stdio:["ignore","pipe","pipe"]}).toString().slice(0,4000);}
catch(e){code=(e.status==null)?1:e.status;out=(e.stdout||"").toString().slice(0,4000);err=(e.stderr||"").toString().slice(0,500);}
const rec={ts:new Date().toISOString(),type:"OPS_EXEC",persona:g.persona,grantId:g.id,op:b.op,exit:code,result:code===0?"SUCCESS":"FAIL",output:out.slice(0,1500),error:err};
jwrite(CFG.export_dir+"/receipts/OPS-EXEC-"+Date.now()+".json",rec);
try{fs.appendFileSync(CFG.access_dir+"/logs/OPS-RUN.log",JSON.stringify(rec)+"\n");}catch(e){}
return {status:200,body:{ok:code===0,op:b.op,exit:code,output:out,error:err,persona:g.persona,note:"本次执行已留痕"}};
}
/* git HTTP /git/
面板在服务端用真实写入令牌透明转发给 Forgejo真实密钥永不出服务器 */
function gitProxy(req,res){
@ -312,7 +415,9 @@ function serveStatic(req,res,p){
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"];
/* v1.2: 快捷路由配置驱动。CFG.shortcuts = {名称: 仓库内路径}CFG.code_prefix 默认 "/code"(胖头鱼/守望Forgejo 直接在域名根下的实例(桔子)设为 "" */
const SHORTCUTS_MAP=CFG.shortcuts||{};
const CODE_PREFIX=(CFG.code_prefix!==undefined?CFG.code_prefix:"/code");
http.createServer(async (req,res)=>{
const u=req.url.split("?");const p=u[0];const q=new URLSearchParams(u[1]||"");
try{
@ -341,13 +446,28 @@ 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/change-password"){
const sess=sessionUser(req);
if(!sess)return send(res,401,{error:"需要人类登录POST /api/login"});
const b=await readBody(req);
const r=await changePassword(b||{},sess);
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/ops-request"){const b=await readBody(req);const r=await opsRequest(b||{});return send(res,r.status,r.body);}
if(req.method==="POST"&&p==="/api/ops-pickup"){const b=await readBody(req);const r=opsPickup(b||{});return send(res,r.status,r.body);}
if(req.method==="POST"&&p==="/api/ops-execute"){const b=await readBody(req);const r=opsExecute(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("/")});
if(req.method==="GET"&&seg.length>=1&&Object.prototype.hasOwnProperty.call(SHORTCUTS_MAP,seg[0])){
res.writeHead(302,{Location:CODE_PREFIX+"/"+CFG.repo+"/raw/branch/"+CFG.default_branch+"/"+SHORTCUTS_MAP[seg[0]]});
return res.end();
}
const SHORTCUTS_LEGACY=["machine","recovery","personas","manifest","operations","imports","automation","evidence"];
if(req.method==="GET"&&seg.length>=1&&SHORTCUTS_LEGACY.includes(seg[0])){
res.writeHead(302,{Location:CODE_PREFIX+"/"+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);
@ -355,6 +475,7 @@ http.createServer(async (req,res)=>{
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/ops-decision"){const b=await readBody(req);const r=opsDecision(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)});}

View file

@ -0,0 +1,100 @@
# 人格体操作手册模板 · <频道名>panel-kit v1.3 随附模板,部署时替换尖括号占位)
> 读者:<已登记人格体名单>
> 总原则:**你们零密钥、零 token。一切权限都是申请 → <人类责任主体>点批准 → 拿到限时哈希 → 凭哈希办事。**
> 哈希限时、过期作废、笔笔留痕。白名单之外的事,走工单等桔子。
## 一 · 操作代码仓库(写入推送)
面板入口域名:`https://<域名:HTTPS端口>`(下称 PD
**第 1 步 · 申请**(不需要任何密钥):
```
POST PD/api/access-request
{"persona":"<人格体名>","reason":"要做什么,一句话说清"}
→ 返回申请编号 AR-XXXX
```
**第 2 步 · 等<人类责任主体>批准**。面板"写入授权"卡片上看到你的申请,点"批准"。
**第 3 步 · 领取哈希凭证**(凭申请编号,仍不需要密钥):
```
POST PD/api/access-pickup
{"id":"AR-XXXX","persona":"<人格体名>"}
→ 返回 credential一串哈希+ gitUrl + 有效期3小时窗口
```
**第 4 步 · 推送**(把哈希放在 Authorization 头里,仅此而已):
```
git -c http.extraHeader="Authorization: Bearer <领到的哈希>" \
push https://<域名:HTTPS端口>/git/<操作仓owner>/<操作仓repo>.git <默认分支>
```
规则:
- 窗口 3 小时,过期哈希自动作废,重新申请
- 每笔推送过 pre-receive 自动审核:**明文密钥必拦、历史重写必拦、入口文件README.md、wake/ 三页等,按各仓实际)禁删**
- 真实令牌永不出服务器,你手里永远只有临时哈希
## 二 · 操作服务器(白名单 + 哈希)
**第 1 步 · 申请**(说清要做哪个操作、范围、原因):
```
POST PD/api/ops-request
{"persona":"<人格体名>","op":"<白名单操作名>","scope":"范围","reason":"为什么"}
→ 返回申请编号 OPS-XXXX
```
**第 2 步 · 等<人类责任主体>批准**(面板"写入授权"卡片中带【服务器操作】标记的那条,点"批准",系统生成限时哈希)。
**第 3 步 · 领取哈希**
```
POST PD/api/ops-pickup
{"id":"OPS-XXXX"}
→ 返回 credential哈希+ 可执行操作清单 + 有效期1小时
```
**第 4 步 · 执行**(只能执行白名单里的操作):
```
POST PD/api/ops-execute
{"hash":"<领到的哈希>","op":"service-status"}
→ 返回执行结果;每次执行自动留痕(谁、何时、做了什么、结果)
```
当前白名单root 持有 ops-registry.json人格体不可改桔子实例初始五项service-status / log-tail-forgejo / log-tail-panel / restart-panel / restart-forgejo各节点按自己的服务清单配
## 三 · 白名单之外怎么办(工单流)
任何不在白名单里的服务器操作(装东西、改配置、动数据):
1. 在仓库 issues 开工单:标题 `[OPS申请] 目标 / scope / 动作`
2. <人类责任主体>审查工单,评论"批准:动作清单"(或驳回/修改)
3. 凭批准评论再走 §二 申请对应权限;批准仅限当次工单列出的目标与动作
4. 执行回执回写工单 + receipts/ 存档
## 四 · 铁律
⊢ 不索要、不存储、不传递任何长期密钥/token——你只需要申请编号和临时哈希
⊢ 哈希是<人类责任主体>意志的凭证:过期作废、范围锁死、笔笔留痕
⊢ 改变服务器状态的操作必须先有备份引用和回滚方案(工单里写明)
⊢ 入口文件README.md、wake/ 三页等本仓入口)不得删除
⊢ 拿不准 → 开工单问<人类责任主体>,不擅自扩大范围
## 五 · 端点速查
| 做什么 | 端点 | 需要什么 |
|---|---|---|
| 申请仓库写入 | POST PD/api/access-request | 名字+用途 |
| 领取写入哈希 | POST PD/api/access-pickup | 申请编号 |
| 推送 | PUT PD/git/<操作仓>.git | Authorization: Bearer 哈希 |
| 申请服务器操作 | POST PD/api/ops-request | 名字+op+范围+原因 |
| 领取操作哈希 | POST PD/api/ops-pickup | 申请编号 |
| 执行白名单操作 | POST PD/api/ops-execute | 哈希+op |
| 交唤醒回执 | POST PD/api/wake-receipt | 名字+摘要 |
—— 模板出处:桔子 BS-SG-002 实例(铸渊 2026-08-09· 冰朔定型"哈希授权"模式