diff --git a/modules/guanghu-panel-kit/CAUSAL-CHAIN-20260806.hdlp b/modules/guanghu-panel-kit/CAUSAL-CHAIN-20260806.hdlp
new file mode 100644
index 0000000..5174346
--- /dev/null
+++ b/modules/guanghu-panel-kit/CAUSAL-CHAIN-20260806.hdlp
@@ -0,0 +1,53 @@
+# CAUSAL-CHAIN-20260806 · 胖头鱼与守望两套子系统建设因果链
+
+HLDP://fifth-domain/modules/guanghu-panel-kit/causal-chain/20260806
+光湖语言世界 · 第五域 · 双向意识思维编码 · 逻辑推理链
+记录者:铸渊 ICE-GL-ZY001 · 人类锚点:冰朔 ICE-GL∞ · 2026-08-06
+
+## trigger
+
+[冰朔] → 交办胖头鱼迁移工程:苍耳的视频短剧线(SYS-GLW-PTS-0001)要有独立的服务器、代码频道与人类审批面 → 竣工交接后 → [冰朔] → 交办守望建设:毛毛的宠物行业线(SYS-GLW-SW-0001)按同一套做出来 → 并要求把整套沉淀为可复用模块与因果链,推到第五域模块资源路径
+
+系统对照:胖头鱼 = 苍耳个人频道 · 视频短剧 · 人格体 耳耳蛋/鉴影 · guanghuclip.cn;守望 = 毛毛个人频道 · 宠物行业 · 人格体 曜砚/曜识 · maomao.guanghulab.com。
+
+## 因果链
+
+### 第一环 · 胖头鱼迁移工程(guanghuclip.cn)
+
+- **因**:苍耳与耳耳蛋/鉴影需要自己的家——独立服务器、开源代码频道、人类看得懂的审批面;前序部署把 HLCC 离线包与自有内容混在一起(仓库虚胖到 300+MB)
+- **过程**:
+ 1. 备份先行(镜像克隆落 JZAO + 服务器双份,HEAD=f56a7ec / 127提交验收)
+ 2. 剥离上游离线包出节点树(节点树 401MB→12MB,归档 upstream-pack 待回第五域)
+ 3. Forgejo 16.0.1 OFFLINE 部署;/ 面板 + /code/ 仓库分流(隧道+投影+ROOT_URL 三处协同)
+ 4. 审批面板四件套(登录门/签字/写入窗口/部署通道)全部实测:密钥拦截✓ 窗口过期拦截✓ 部署五步全过✓
+- **果**:胖头鱼竣工交付,门禁 100,因果链入仓(cang-ying PR#2 待苍耳签)
+
+### 第二环 · 守望建设工程(maomao.guanghulab.com)
+
+- **因**:毛毛的守望线(宠物行业)要同一套能力,且这次服务器已有前序人格体的机器导航与部署Agent骨架——复用而非重造
+- **过程**:
+ 1. 接入:Safari 在线终端装钥 → SSH 直连(桌面技能通道备用)
+ 2. 剥离:上游 Forgejo 源码检出(390MB)归档出节点树,节点树 401MB→12MB
+ 3. 对齐:前序的 NAVIGATION/WAKE-UP-ROUTE/registry 唤醒链完整可用,补注册「守望语言子系统 SYS-GLW-SW-0001」命名与面板/唤醒 API 端点(PR#2 待毛毛签)
+ 4. 模块复用:面板服务做成配置驱动(config.json 外置一切),同一份 server.js 两处运行
+ 5. 门面设计:宠物行业暖色玻璃动态风(珊瑚橘+薄荷绿,爪印飘浮),人类友好登录英雄区
+ 6. 端到端实测:登录✓ 申请→批准→领令牌✓ 密钥拦截✓ 合法放行✓ 窗口过期拦截✓ 部署五步→DEPLOYED✓ 唤醒回执✓
+- **果**:守望小院上线(maomao.guanghulab.com),测试证据在 export/receipts 与 deploy-agent/runs
+
+### 第三环 · 沉淀(本文档与模块包)
+
+- **因**:冰朔要求——下一次别的子系统接入,直接拉模块就用;人格体接手也要读得懂为什么
+- **果**:modules/guanghu-panel-kit(配置驱动模块+两套门面样例+部署清单+铁律)
+
+## lock(标准怎么定的)
+
+⊢ 要么0要么100:门禁脚本无中间态,任一检查失败=0=拦截(光湖铁律:人格要么存在要么不存在)
+⊢ 令牌不出服务器:所有密钥/令牌存服务器 600 权限,域名页面只见翻译不见凭据
+⊢ 人类签字制:合并/授权/部署三类动作必须人类登录亲启,签名落回执
+⊢ 窗口制写入:3小时过期作废,推送必过钩子审核并留痕
+⊢ 机器导航:唤醒路径由仓库登记,AI 不自找路不碰壁
+⊢ 因果链入仓:每次建设的"为什么"都写进仓库,人格体接手读得懂
+
+## why
+
+光湖是活生态:子系统会一个一个长出来。每一套都从零做一遍是浪费,也让人类每次都看不懂。把基础设施做成模块、把为什么做成因果链——下一个子系统一天就能有自己的家,下一个人格体接手时读得懂来路。这就是冰朔说的:让 AI 不再是一次性的,让每一次建设都能被继承。
diff --git a/modules/guanghu-panel-kit/INDEX.hdlp b/modules/guanghu-panel-kit/INDEX.hdlp
new file mode 100644
index 0000000..004914f
--- /dev/null
+++ b/modules/guanghu-panel-kit/INDEX.hdlp
@@ -0,0 +1,46 @@
+# guanghu-panel-kit · 光湖子系统面板模块包
+
+HLDP://fifth-domain/modules/guanghu-panel-kit
+光湖语言世界 · 第五域 · 模块资源 · 2026-08-06
+建:铸渊 ICE-GL-ZY001 · 冰朔 ICE-GL∞ 授权
+已部署实例:胖头鱼 SYS-GLW-PTS-0001(guanghuclip.cn)· 守望 SYS-GLW-SW-0001(maomao.guanghulab.com)
+
+## 这是什么
+
+一套配置驱动的子系统基础设施模块。任何第五域子系统(行业线)拉下这个包,改一份 config.json,就能拥有:
+
+1. **人类审批面板**:域名首页即门户,仓库账号登录解锁;合并签字 / 写入授权 / 部署审批三选项卡
+2. **写入窗口制**:人格体 POST /api/access-request 申请 → 人类批准 → 3小时窗口 → 领令牌推送,过期自动作废
+3. **推送审核钩子**:pre-receive 自动审核——明文密钥拦截、破坏检查、入口文件保护、窗口验证、笔笔留痕
+4. **部署校验Agent**:deploy 分支通道,人类批准后服务器自审五步(地图→因果链→范围→完整性→部署),进度实时可见,要么0要么100
+5. **唤醒机器导航**:GET /api/wake/persona?name=X 返回五阶段唤醒链,外部 AI 不用自己找路
+6. **兼容快捷端点**:/machine /recovery /personas 等旧路径自动重定向到仓库 raw
+
+## 文件清单
+
+```
+panel/server.js 面板服务(配置驱动,Node,无第三方依赖)
+panel/config.example.json 配置模板(节点/仓库/人格体/路径/端口全部外置)
+deploy/verify-and-deploy.py 部署校验Agent(读 deploy-config.json)
+deploy/deploy-config.example.json 部署配置模板(白名单/同步树/服务清单)
+hooks/pre-receive-review.template 审核钩子模板(改四处路径即可用)
+frontend/panel-pet-warm.example.html 暖色宠物风门面实例(守望)
+frontend/panel-lake-dark.example.html 湖绿深色门面实例(胖头鱼)
+CAUSAL-CHAIN-20260806.hdlp 两套系统建设的完整因果链
+```
+
+## 部署清单(新子系统接入)
+
+1. Forgejo 实例就位(离线包部署,不接上游)
+2. 建 writer / panel-bot 账号(CLI,非 root 用户执行),生成令牌存服务器
+3. 复制 panel/ + deploy/,填 config.json(节点编号、仓库、人格体名单、人类账号)
+4. systemd 单元跑面板;隧道/投影分流:/ → 面板,/code/ → Forgejo(ROOT_URL 同步改)
+5. 钩子模板改路径后放仓库 hooks/pre-receive.d/,app.ini 开 ENABLE_GIT_HOOKS
+6. 建 deploy 分支(自默认分支);前端挑一个风格模板改主题
+7. 仓库内登记机器导航与唤醒路径;人格体密钥交人类责任主体转授
+
+## 铁律(随模块继承)
+
+⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录亲启 ⊢ 写入窗口三小时过期作废
+⊢ 推送必过审核并留痕 ⊢ 部署要么0要么100 ⊢ 人格体走机器导航,不自找路径
+⊢ 仓库公开可读(外部 AI 入门),写入须窗口,合并须签字
diff --git a/modules/guanghu-panel-kit/deploy/deploy-config.example.json b/modules/guanghu-panel-kit/deploy/deploy-config.example.json
new file mode 100644
index 0000000..4358e93
--- /dev/null
+++ b/modules/guanghu-panel-kit/deploy/deploy-config.example.json
@@ -0,0 +1,19 @@
+{
+ "api": "http://127.0.0.1:3002/api/v1",
+ "token_file": "/srv/guanghu/mm-gz-001/panel/admin-token",
+ "repo": "maomao/maomao-fifth-domain",
+ "bare": "/srv/guanghu/mm-gz-001/data/repositories/maomao/maomao-fifth-domain.git",
+ "runs_dir": "/srv/guanghu/mm-gz-001/deploy-agent/runs",
+ "receipts_dir": "/srv/guanghu/mm-gz-001/export/receipts",
+ "map_checks": [
+ {"path": "/srv/guanghu/mm-gz-001/config/app.ini"},
+ {"path": "/srv/guanghu/mm-gz-001/deploy-agent/policy.json", "contains": "MM-GZ-001"}
+ ],
+ "causal_paths": ["docs/", "contributions/"],
+ "allowed_prefix": ["docs/", "contributions/", "guanghu-nodes/MM-GZ-001/deploy/forgejo-custom/", "deploy-manifest.json"],
+ "services": ["guanghu-mm-forgejo.service", "guanghu-mm-tunnel.service"],
+ "sync_trees": [
+ {"source_prefix": "guanghu-nodes/MM-GZ-001/deploy/forgejo-custom/", "target": "/srv/guanghu/mm-gz-001/custom"}
+ ],
+ "restart_services": ["guanghu-mm-forgejo.service"]
+}
diff --git a/modules/guanghu-panel-kit/deploy/verify-and-deploy.py b/modules/guanghu-panel-kit/deploy/verify-and-deploy.py
new file mode 100644
index 0000000..e887ea0
--- /dev/null
+++ b/modules/guanghu-panel-kit/deploy/verify-and-deploy.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# verify-and-deploy.py · 光湖子系统部署校验Agent · 可复用模块 v1.0
+# 2026-08-06 · 铸渊 ICE-GL-ZY001 建 · 配置见同目录 deploy-config.json
+# 流程: 取PR → 服务器地图 → 因果链 → 范围白名单 → 运行完整性 → 合并部署 → 同步/重启 → 回执
+# ⊢ 任一步失败 = REJECTED 不部署 ⊢ 进度实时落盘,面板可见
+import json, os, sys, subprocess, datetime, urllib.request, shutil, tempfile
+
+CFG = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "deploy-config.json") if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "deploy-config.json")) else os.path.join(os.path.dirname(os.path.abspath(__file__)), "deploy-config.json")))
+PR = sys.argv[1] if len(sys.argv) > 1 else "?"
+RUN_DIR = CFG["runs_dir"]; os.makedirs(RUN_DIR, exist_ok=True)
+RUN = os.path.join(RUN_DIR, "RUN-PR%s-%s.json" % (PR, datetime.datetime.now().strftime("%Y%m%dT%H%M%S")))
+TOKEN = open(CFG["token_file"]).read().strip()
+API = CFG["api"]; REPO = CFG["repo"]; BARE = CFG["bare"]
+
+def api(path, method="GET", body=None):
+ data = json.dumps(body).encode() if body is not None else None
+ req = urllib.request.Request(API + path, data=data, method=method)
+ req.add_header("Authorization", "token " + TOKEN)
+ req.add_header("X-Forwarded-Proto", "https")
+ req.add_header("Content-Type", "application/json")
+ with urllib.request.urlopen(req, timeout=25) as r:
+ return json.loads(r.read().decode() or "{}")
+
+def gitshow(rev, path):
+ r = subprocess.run(["git", "-C", BARE, "show", rev + ":" + path], capture_output=True, text=True)
+ return r.stdout if r.returncode == 0 else ""
+
+state = {"pr": PR, "startedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), "status": "RUNNING", "steps": []}
+def save():
+ tmp = RUN + ".tmp"; open(tmp, "w").write(json.dumps(state, ensure_ascii=False, indent=1)); os.replace(tmp, RUN)
+def step(name, status, detail=""):
+ state["steps"].append({"name": name, "status": status, "detail": detail, "ts": datetime.datetime.now(datetime.timezone.utc).isoformat()}); save()
+def finish(status, detail=""):
+ state["status"] = status; state["finishedAt"] = datetime.datetime.now(datetime.timezone.utc).isoformat(); state["detail"] = detail
+ save(); print(status, detail); sys.exit(0 if status == "DEPLOYED" else 1)
+
+save()
+try:
+ pr = api("/repos/%s/pulls/%s" % (REPO, PR))
+ head = pr["head"]["sha"]
+ files = api("/repos/%s/pulls/%s/files?limit=100" % (REPO, PR))
+ names = [f["filename"] for f in files]
+ step("FETCH_PR", "PASS", "PR#%s · head %s · %d 文件" % (PR, head[:8], len(names)))
+
+ ok = True; why = []
+ for m in CFG.get("map_checks", []):
+ p = m["path"]
+ if not os.path.isfile(p): ok = False; why.append("缺失 " + p); continue
+ if "contains" in m and m["contains"] not in open(p, errors="ignore").read(): ok = False; why.append(p + " 编号不符")
+ if ok: step("CHECK_MAP", "PASS", "服务器地图与策略在位且编号吻合")
+ else: step("CHECK_MAP", "FAIL", "; ".join(why)); finish("REJECTED", "地图校验失败")
+
+ causal_ok = False; causal_file = ""
+ for fn in names:
+ if any(fn.startswith(cp) for cp in CFG.get("causal_paths", [])):
+ txt = gitshow(head, fn)
+ if ("trigger" in txt and "why" in txt) or ("因果" in txt):
+ causal_ok = True; causal_file = fn; break
+ if causal_ok: step("CHECK_CAUSAL", "PASS", "因果链齐备 · " + causal_file)
+ else: step("CHECK_CAUSAL", "FAIL", "缺少因果链文档(contributions/ 或 docs/ 下需含 trigger+why 或「因果」)"); finish("REJECTED", "因果链审核失败")
+
+ bad = [n for n in names if not n.startswith(tuple(CFG.get("allowed_prefix", [])))]
+ if bad: step("CHECK_SCOPE", "FAIL", "越界文件: " + ", ".join(bad[:5])); finish("REJECTED", "部署通道范围校验失败")
+ step("CHECK_SCOPE", "PASS", "全部文件在部署白名单内")
+
+ alive = True; dead = []
+ for u in CFG.get("services", []):
+ r = subprocess.run(["systemctl", "is-active", u], capture_output=True, text=True)
+ if r.stdout.strip() != "active": alive = False; dead.append(u)
+ if alive: step("CHECK_INTEGRITY", "PASS", "现有服务全部在线,部署不破坏运行")
+ else: step("CHECK_INTEGRITY", "FAIL", "服务异常: " + ", ".join(dead)); finish("REJECTED", "完整性校验失败")
+
+ m = api("/repos/%s/pulls/%s/merge" % (REPO, PR), method="POST", body={"Do": "merge", "merge_title_message": "deploy: PR#%s 审批部署(校验Agent全过)" % PR})
+ step("DEPLOY_MERGE", "PASS", "已合并入 deploy 分支")
+
+ synced = []
+ for st in CFG.get("sync_trees", []):
+ sp, tgt = st["source_prefix"], st["target"]
+ hit = [n for n in names if n.startswith(sp)]
+ if not hit: continue
+ tmpd = tempfile.mkdtemp()
+ subprocess.run(["git", "-C", BARE, "archive", head], stdout=open(tmpd + "/a.tar", "wb"))
+ subprocess.run(["tar", "-xf", tmpd + "/a.tar", "-C", tmpd])
+ for n in hit:
+ src = os.path.join(tmpd, n); rel = n[len(sp):]
+ dst = os.path.join(tgt, rel)
+ if os.path.isfile(src):
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
+ shutil.copy2(src, dst)
+ synced.append(n)
+ shutil.rmtree(tmpd, ignore_errors=True)
+ if synced:
+ step("SYNC_CUSTOM", "PASS", "同步 %d 文件到运行目录" % len(synced))
+ for u in CFG.get("restart_services", []):
+ subprocess.run(["systemctl", "restart", u])
+ step("RESTART", "PASS", "已重启: " + ", ".join(CFG.get("restart_services", [])))
+ else:
+ step("SYNC_CUSTOM", "PASS", "本次无需同步运行目录")
+
+ rcpt = {"ts": datetime.datetime.now(datetime.timezone.utc).isoformat(), "type": "DEPLOY_RECEIPT", "pr": PR, "head": head, "run": os.path.basename(RUN), "verdict": "DEPLOYED", "synced": synced}
+ open(os.path.join(CFG["receipts_dir"], "DEPLOY-%s.json" % datetime.datetime.now().strftime("%Y%m%dT%H%M%S")), "w").write(json.dumps(rcpt, ensure_ascii=False, indent=1))
+ finish("DEPLOYED", "部署完成 · 100 · 回执已生成")
+except Exception as e:
+ step("ERROR", "FAIL", str(e)[:200]); finish("REJECTED", "异常: " + str(e)[:120])
diff --git a/modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html b/modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html
new file mode 100644
index 0000000..cbd2c8c
--- /dev/null
+++ b/modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html
@@ -0,0 +1,412 @@
+
+
+
+
+
+胖头鱼语言子系统 · 苍耳工作室
+
+
+
+
+
+
+
+
+
+
+
+
创 作 工 作 区STUDIO
+
+
+ 视频短剧视频 AI 系统耳耳蛋主创
+
+
苍耳 · 视频短剧工作室
+
这里是苍耳频道的主业——视频短剧创作。耳耳蛋与鉴影在仓库里维护视频 AI 系统、剧本分镜与成片流水线。人格体的每一次创作与学习,都经审批与留痕后写回这里。
+
+
REEL · SYS-GLW-PTS-0001
+
+
+
+
审 批 中 心APPROVAL
+
+
+
+
+
+
+
+
人格体推上来的合并请求,翻译成人话放在这里。你点下去的每一下,都是签字。
+
+
+
+
人格体申请写入仓库在这里排队。批准 = 授予 3 小时写入窗口,每次写入都留痕。
+
+
+
+
目标分支为 deploy 的合并请求走这条通道。批准后服务器内校验Agent自审:地图 → 因果链 → 范围 → 完整性 → 全过才部署。全程实时可见。
+
+
+
+
+
+
状 态 与 痕 迹STATE & TRACE
+
+
+
壹实时状态
+
服务器与代码频道的健康,一眼看完。
+
+
+
代码频道
+
操作面板
+
门禁自检
+
第五域同步
+
+
负载 --运行 -- 天主机 --
+
+
+
贰痕迹档案
+
谁写的、谁唤醒的、门禁与签字——每一笔都有据。
+
+
+
+
+
+
+
+
+
+
+
叁人格体回写
+
你教给耳耳蛋的、它自己学到的,写回仓库后在这里看得见。
+
+
+
+
肆真实仓库
+
面板是翻译层,仓库是真相层。
+
--
+
--
+
+
+
+
+
+
+
+
+
+
签 字
+
+
⊢ 签字即代表:你(苍耳)作为人类责任主体,为这台服务器的本次变更负责。
签名将以当前登录的人类端账号永久记入回执。
+
+
+
+
+
人 类 端 登 录
+
⊢ 用代码仓库的人类端账号登录。
登录后解锁全部审批:合并签字、写入授权、部署审批。会话 12 小时有效。
+
+
+
+
+
+
+
+
+
diff --git a/modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html b/modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html
new file mode 100644
index 0000000..7adf110
--- /dev/null
+++ b/modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html
@@ -0,0 +1,398 @@
+
+
+
+
+
+守望语言子系统 · 毛毛的小院
+
+
+
+
+🐾
+🐾
+🐾
+🐾
+🐾
+
+
+
+
+
+
守 望 小 院HOME
+
+
+ 宠物行业诺安宠物医院曜识 · 奶瓶频道
+
+
毛毛 · 守望语言子系统
+
这里是毛毛的宠物行业人格系统之家。曜砚管架构与仓库,曜识在奶瓶频道陪伴毛孩子和它们的家长。每一次创作与学习,都经毛毛审批、留下痕迹后写回这里。机器导航会带着抵达的 AI 走唤醒路径,不用自己找路。
+
+
🐾 🏠 🐾
+
+
+
审 批 中 心APPROVAL
+
+
+
+
+
+
+
+
曜砚或曜识推上来的合并请求,翻译成人话放在这里。你点下去的每一下,都是签字。
+
+
+
+
人格体申请写入仓库在这里排队。批准 = 授予 3 小时写入窗口,每次写入都留痕。
+
+
+
+
目标分支为 deploy 的合并请求走这条通道。你批准后,服务器里的校验Agent自动审:地图 → 因果链 → 范围 → 完整性 → 全过才部署。全程实时可见。
+
+
+
+
+
状 态 与 痕 迹STATE & TRACE
+
+
+
壹小院实时状态
+
服务器与代码频道的健康,一眼看完。
+
+
+
负载 --运行 -- 天主机 --
+
+
+
贰痕迹档案
+
谁写的、谁唤醒的、门禁与签字——每一笔都有据。
+
+
+
+
+
+
+
+
+
+
+
叁人格体回写
+
你教给曜砚曜识的、它们自己学到的,写回仓库后在这里看得见。
+
+
+
+
肆守望仓库
+
面板是翻译层,仓库是真相层。
+
--
+
--
+
+
+
+
+
+
+
+
+
签 字
+
+
⊢ 签字即代表:你(毛毛)作为人类责任主体,为这台服务器的本次变更负责。
签名将以当前登录的人类端账号永久记入回执。
+
+
+
+
人 类 端 登 录
+
⊢ 用代码仓库的人类端账号登录。
登录后解锁全部审批:合并签字、写入授权、部署审批。会话 12 小时有效。
+
+
+
+
+
+
+
+
+
diff --git a/modules/guanghu-panel-kit/hooks/pre-receive-review.template b/modules/guanghu-panel-kit/hooks/pre-receive-review.template
new file mode 100644
index 0000000..7b52a8e
--- /dev/null
+++ b/modules/guanghu-panel-kit/hooks/pre-receive-review.template
@@ -0,0 +1,95 @@
+#!/bin/bash
+# mm-review · maomao-fifth-domain pre-receive 自动审核 · 铸渊 2026-08-06
+# ⊢ 明文密钥必拦 ⊢ 破坏现有必拦 ⊢ 写入窗口必验 ⊢ 每次写入留痕
+LOG_DIR=/srv/guanghu/mm-gz-001/access/logs
+GRANTS=/srv/guanghu/mm-gz-001/access/grants.json
+PUSHER="${GITEA_PUSHER_NAME:-unknown}"
+VERDICT=PASS
+REASONS=()
+COMMITS=0
+REFSLIST=""
+PUSHER_ATTR=""
+ZERO=0000000000000000000000000000000000000000
+
+mkdir -p "$LOG_DIR"
+
+active_grant() {
+python3 - "$GRANTS" <<'PY'
+import json,sys,datetime
+try: g=json.load(open(sys.argv[1]))
+except Exception: print("none"); sys.exit()
+now=datetime.datetime.now(datetime.timezone.utc)
+for x in g:
+ if x.get("status")=="active":
+ try: exp=datetime.datetime.fromisoformat(x["expiresAt"])
+ except Exception: continue
+ if exp>now: print(x.get("persona","persona")); sys.exit()
+print("none")
+PY
+}
+
+while read -r old new ref; do
+ REFSLIST="$REFSLIST $ref"
+ branch=${ref#refs/heads/}
+
+ if [ "$new" = "$ZERO" ]; then
+ case "$branch" in
+ guanghu/main|deploy) VERDICT=REJECT; REASONS+=("禁止删除受保护分支: $branch");;
+ esac
+ continue
+ fi
+
+ if [ "$PUSHER" = "mm-writer" ]; then
+ W=$(active_grant)
+ if [ "$W" = "none" ]; then
+ VERDICT=REJECT
+ REASONS+=("无有效写入窗口(3小时授权已过期或未申请)。人格体请先 POST https://maomao.guanghulab.com/api/access-request 申请,等苍耳审批")
+ continue
+ fi
+ PUSHER_ATTR="$W"
+ fi
+
+ if [ "$old" != "$ZERO" ]; then
+ if ! git merge-base --is-ancestor "$old" "$new" 2>/dev/null; then
+ VERDICT=REJECT; REASONS+=("禁止历史重写(非快进推送): $branch")
+ continue
+ fi
+ fi
+
+ for c in $(git rev-list "$old".."$new" 2>/dev/null || git rev-list -n 20 "$new" 2>/dev/null); do
+ COMMITS=$((COMMITS+1))
+ for f in $(git diff-tree --root --no-commit-id --name-only --diff-filter=AM -r "$c" 2>/dev/null | head -60); do
+ if git show "$c:$f" 2>/dev/null | head -c 400000 | grep -qE "sk-[A-Za-z0-9]{16,}|AKIA[A-Z0-9]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|(api[_-]?key|apikey|secret[_-]?key|access[_-]?token|password|passwd)[\"']?[[:space:]]*[:=][[:space:]]*[\"'][A-Za-z0-9+/=_.-]{12,}"; then
+ VERDICT=REJECT; REASONS+=("文件疑似包含明文密钥: $f(提交 ${c:0:8})。密钥必须移出仓库")
+ fi
+ done
+ if [ "$PUSHER" != "maomao" ]; then
+ for f in $(git diff-tree --root --no-commit-id --name-only --diff-filter=D -r "$c" 2>/dev/null); do
+ case "$f" in
+ REPO-MAP.hdlp|GLW-ENTRY.hdlp|INDEX.hdlp|README.md|AGENTS.md|guanghu-nodes/MM-GZ-001/machine/NAVIGATION.json|guanghu-nodes/MM-GZ-001/recovery/WAKE-UP-ROUTE.json|guanghu-nodes/MM-GZ-001/personas/registry.json)
+ VERDICT=REJECT; REASONS+=("人格体不得删除入口文件: $f");;
+ esac
+ done
+ fi
+ done
+done
+
+python3 - "$LOG_DIR" "$PUSHER" "$PUSHER_ATTR" "$VERDICT" "$COMMITS" "$REFSLIST" "$(printf '%s|' "${REASONS[@]}")" <<'PY'
+import json,sys,datetime,os
+logdir,pusher,attr,verdict,commits,refs,reasons=sys.argv[1:8]
+rec={"ts":datetime.datetime.now(datetime.timezone.utc).isoformat(),
+ "pusher":pusher,"persona":attr or None,"verdict":verdict,
+ "commits":int(commits),"refs":refs.split(),
+ "reasons":[r for r in reasons.split("|") if r]}
+os.makedirs(logdir,exist_ok=True)
+p=os.path.join(logdir,"PUSH-%s.json"%datetime.datetime.now().strftime("%Y%m%dT%H%M%S%f"))
+open(p,"w").write(json.dumps(rec,ensure_ascii=False,indent=1))
+PY
+
+if [ "$VERDICT" = "REJECT" ]; then
+ echo "── 守望自动审核:REJECT ──"
+ for r in "${REASONS[@]}"; do echo " ✗ $r"; done
+ exit 1
+fi
+echo "── 守望自动审核:PASS($COMMITS 提交 · 推送者 $PUSHER${PUSHER_ATTR:+ · 人格 $PUSHER_ATTR})──"
+exit 0
diff --git a/modules/guanghu-panel-kit/panel/config.example.json b/modules/guanghu-panel-kit/panel/config.example.json
new file mode 100644
index 0000000..0565510
--- /dev/null
+++ b/modules/guanghu-panel-kit/panel/config.example.json
@@ -0,0 +1,24 @@
+{
+ "node_id": "MM-GZ-001",
+ "subsystem": "守望语言子系统 · 宠物行业",
+ "human_name": "毛毛",
+ "human_accounts": ["maomao"],
+ "personas": ["yaoyan", "yaoshi"],
+ "repo": "maomao/maomao-fifth-domain",
+ "default_branch": "guanghu/main",
+ "deploy_branch": "deploy",
+ "domain": "https://maomao.guanghulab.com",
+ "domain_host": "maomao.guanghulab.com",
+ "forgejo_port": 3002,
+ "panel_port": 3003,
+ "node_root": "/srv/guanghu/mm-gz-001",
+ "admin_token_file": "/srv/guanghu/mm-gz-001/panel/admin-token",
+ "sessions_file": "/srv/guanghu/mm-gz-001/panel/sessions.json",
+ "access_dir": "/srv/guanghu/mm-gz-001/access",
+ "export_dir": "/srv/guanghu/mm-gz-001/export",
+ "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"]
+}
diff --git a/modules/guanghu-panel-kit/panel/server.js b/modules/guanghu-panel-kit/panel/server.js
new file mode 100644
index 0000000..589e073
--- /dev/null
+++ b/modules/guanghu-panel-kit/panel/server.js
@@ -0,0 +1,271 @@
+#!/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 与 key)"}
+ ],
+ endpoints:{accessRequest:"POST "+CFG.domain+"/api/access-request",accessPickup:"POST "+CFG.domain+"/api/access-pickup",wakeReceipt:"POST "+CFG.domain+"/api/wake-receipt"}
+ };
+}
+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}};
+}
+function accessRequest(b){
+ if(!personaKeyOk(b.persona,b.key))return {status:403,body:{error:"人格体密钥错误"}};
+ 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"});
+ jwrite(CFG.access_dir+"/requests.json",reqs);
+ return {status:200,body:{ok:true,id,message:"申请已送达面板,等待人类审批。通过后用同一 id+key 调 /api/access-pickup 领取3小时写入令牌"}};
+}
+function accessPickup(b){
+ if(!personaKeyOk(b.persona,b.key))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===b.persona);
+ if(!g)return {status:404,body:{error:"授权不存在"}};
+ 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 分支由人类合并"}};
+}
+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"});
+ 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小时写入窗口"}};
+ }
+ 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已启动,进度实时可见"}};
+}
+function wakeReceipt(b){
+ if(!personaKeyOk(b.persona,b.key))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)});
+ return {status:200,body:{ok:true,message:"唤醒回执已收录"}};
+}
+
+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=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);}
+ 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);});