feat(modules): add guanghu-panel-kit reusable modules + two-system causal chain
光湖子系统面板模块包(胖头鱼/守望两系统沉淀): - panel/server.js 配置驱动审批面板(登录门/签字/写入窗口/部署通道/唤醒导航) - deploy/verify-and-deploy.py 部署校验Agent(地图/因果链/范围/完整性,0或100) - hooks/pre-receive-review.template 推送审核钩子模板 - frontend 两套门面样例(湖绿深色/宠物暖色玻璃动态) - CAUSAL-CHAIN-20260806.hdlp 两系统建设完整因果链
This commit is contained in:
parent
2ccf47d36a
commit
8544cae938
9 changed files with 1422 additions and 0 deletions
53
modules/guanghu-panel-kit/CAUSAL-CHAIN-20260806.hdlp
Normal file
53
modules/guanghu-panel-kit/CAUSAL-CHAIN-20260806.hdlp
Normal file
|
|
@ -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 不再是一次性的,让每一次建设都能被继承。
|
||||
46
modules/guanghu-panel-kit/INDEX.hdlp
Normal file
46
modules/guanghu-panel-kit/INDEX.hdlp
Normal file
|
|
@ -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 入门),写入须窗口,合并须签字
|
||||
19
modules/guanghu-panel-kit/deploy/deploy-config.example.json
Normal file
19
modules/guanghu-panel-kit/deploy/deploy-config.example.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
104
modules/guanghu-panel-kit/deploy/verify-and-deploy.py
Normal file
104
modules/guanghu-panel-kit/deploy/verify-and-deploy.py
Normal file
|
|
@ -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])
|
||||
412
modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html
Normal file
412
modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>胖头鱼语言子系统 · 苍耳工作室</title>
|
||||
<style>
|
||||
:root{
|
||||
--ink:#070c12;--glass:linear-gradient(160deg,rgba(255,255,255,.06),rgba(255,255,255,.015));
|
||||
--gline:rgba(255,255,255,.1);--gline-hi:rgba(95,224,192,.35);
|
||||
--lake:#5fe0c0;--lake-soft:rgba(95,224,192,.55);--seal:#ff5a4e;--gold:#e8b45a;
|
||||
--tx:#eef3f1;--sub:#93a4a8;--faint:#5d6e73;
|
||||
--serif:"Songti SC","STSong","SimSun","Noto Serif CJK SC",serif;
|
||||
--mono:"SF Mono","JetBrains Mono","Cascadia Mono",Menlo,Consolas,monospace;
|
||||
--r:16px;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--ink);color:var(--tx);min-height:100vh;overflow-x:hidden;
|
||||
font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;font-size:15px;line-height:1.7;
|
||||
background-image:radial-gradient(1100px 520px at 82% -12%,rgba(38,110,96,.22),transparent 60%),radial-gradient(820px 420px at 4% 0%,rgba(120,50,44,.13),transparent 55%);
|
||||
background-attachment:fixed}
|
||||
.blob{position:fixed;border-radius:50%;filter:blur(95px);pointer-events:none;z-index:0}
|
||||
.b1{width:540px;height:540px;background:#1d6b5b;opacity:.16;top:-160px;right:-120px;animation:d1 28s ease-in-out infinite alternate}
|
||||
.b2{width:440px;height:440px;background:#5c2b26;opacity:.12;bottom:-140px;left:-100px;animation:d2 34s ease-in-out infinite alternate}
|
||||
@keyframes d1{to{transform:translate(-130px,110px) scale(1.16)}}
|
||||
@keyframes d2{to{transform:translate(150px,-90px) scale(1.1)}}
|
||||
body::before{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
|
||||
background:repeating-linear-gradient(0deg,transparent 0 55px,rgba(255,255,255,.015) 55px 56px),repeating-linear-gradient(90deg,transparent 0 55px,rgba(255,255,255,.015) 55px 56px)}
|
||||
.wrap{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:0 28px}
|
||||
.glass{background:var(--glass);backdrop-filter:blur(20px) saturate(150%);-webkit-backdrop-filter:blur(20px) saturate(150%);border:1px solid var(--gline);border-radius:var(--r);box-shadow:0 12px 36px rgba(0,0,0,.38),inset 0 1px 0 rgba(255,255,255,.07)}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
|
||||
|
||||
/* 刊头 */
|
||||
header{padding:54px 0 26px}
|
||||
.kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.36em;color:var(--lake-soft);margin-bottom:14px}
|
||||
.kicker b{color:var(--seal);font-weight:400}
|
||||
.head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:30px;flex-wrap:wrap}
|
||||
h1{font-family:var(--serif);font-weight:600;font-size:clamp(34px,5vw,52px);letter-spacing:.12em;line-height:1.2}
|
||||
h1 .yu{color:var(--lake);text-shadow:0 0 22px rgba(95,224,192,.35)}
|
||||
.headline-sub{margin-top:10px;color:var(--sub);letter-spacing:.06em;font-size:14px}
|
||||
.headline-sub .sep{color:var(--faint);margin:0 9px}
|
||||
.head-right{display:flex;align-items:center;gap:18px}
|
||||
.seal-badge{width:78px;height:78px;border:2px solid var(--seal);color:var(--seal);border-radius:9px;transform:rotate(4deg);display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:var(--serif);position:relative;background:rgba(255,90,78,.06)}
|
||||
.seal-badge::after{content:"";position:absolute;inset:4px;border:1px solid rgba(255,90,78,.4);border-radius:6px}
|
||||
.seal-badge .n{font-family:var(--mono);font-size:26px;font-weight:700;line-height:1}
|
||||
.seal-badge .t{font-size:10px;letter-spacing:.3em;margin-top:5px}
|
||||
.seal-badge.zero{border-color:var(--faint);color:var(--faint);background:none}
|
||||
.login-btn{font-family:var(--serif);font-size:14px;letter-spacing:.3em;color:var(--gold);border:1px solid rgba(232,180,90,.55);background:rgba(232,180,90,.08);padding:11px 24px 11px 30px;border-radius:11px;cursor:pointer;transition:all .25s}
|
||||
.login-btn:hover{background:rgba(232,180,90,.16);box-shadow:0 0 22px rgba(232,180,90,.22)}
|
||||
.login-user{font-family:var(--mono);font-size:12px;color:var(--lake);letter-spacing:.04em;text-align:right;line-height:1.6}
|
||||
.login-user button{font-family:var(--serif);font-size:11px;letter-spacing:.2em;color:var(--sub);border:1px solid rgba(255,255,255,.16);background:none;padding:3px 12px;border-radius:7px;cursor:pointer;margin-top:4px}
|
||||
.rule{height:1px;background:linear-gradient(90deg,var(--gline-hi),rgba(255,255,255,.08) 55%,transparent);margin-top:26px}
|
||||
|
||||
/* 区块标题 */
|
||||
.zone{font-family:var(--serif);font-size:15px;letter-spacing:.44em;color:var(--sub);margin:38px 0 16px;display:flex;align-items:center;gap:16px}
|
||||
.zone::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,rgba(255,255,255,.12),transparent)}
|
||||
.zone .en{font-family:var(--mono);font-size:10px;letter-spacing:.2em;color:var(--faint)}
|
||||
|
||||
/* 登录卡 */
|
||||
.login-hero{display:flex;gap:34px;align-items:center;padding:38px 40px;animation:rise .6s ease both}
|
||||
.login-hero .copy{flex:1;min-width:240px}
|
||||
.login-hero .copy .lead{font-family:var(--serif);font-size:24px;letter-spacing:.1em;line-height:1.5;margin-bottom:10px}
|
||||
.login-hero .copy .lead em{color:var(--gold);font-style:normal}
|
||||
.login-hero .copy p{color:var(--sub);font-size:13.5px;line-height:1.8}
|
||||
.login-box{flex:none;width:330px;padding:26px;border-radius:14px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.1)}
|
||||
.login-box h3{font-family:var(--serif);font-size:17px;letter-spacing:.14em;margin-bottom:14px;display:flex;align-items:center;gap:10px}
|
||||
.login-box h3 .dot{width:8px;height:8px;border-radius:50%;background:var(--gold);box-shadow:0 0 9px rgba(232,180,90,.8)}
|
||||
.login-box input{width:100%;padding:12px 14px;margin-bottom:11px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);color:var(--tx);font-family:var(--mono);font-size:14px;border-radius:10px}
|
||||
.login-box input:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px rgba(232,180,90,.12)}
|
||||
.login-box .hint{font-size:11.5px;color:var(--faint);line-height:1.7;margin-bottom:14px}
|
||||
.welcome-bar{display:flex;align-items:center;gap:16px;padding:16px 24px;animation:rise .5s ease both}
|
||||
.welcome-bar .wt{font-family:var(--serif);font-size:16px;letter-spacing:.1em;color:var(--lake)}
|
||||
.welcome-bar .ws{color:var(--sub);font-size:12.5px}
|
||||
|
||||
/* 工作室卡 */
|
||||
.studio{padding:34px 38px;animation:rise .6s ease both;position:relative;overflow:hidden}
|
||||
.studio::before{content:"";position:absolute;right:-40px;top:-40px;width:220px;height:220px;border-radius:50%;background:radial-gradient(circle,rgba(95,224,192,.14),transparent 70%)}
|
||||
.studio .tag-row{display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap}
|
||||
.studio .chip{font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;color:var(--lake);border:1px solid rgba(95,224,192,.35);background:rgba(95,224,192,.06);padding:3px 11px;border-radius:99px}
|
||||
.studio h2{font-family:var(--serif);font-size:26px;letter-spacing:.1em;margin-bottom:8px}
|
||||
.studio .desc{color:var(--sub);font-size:13.5px;max-width:640px;line-height:1.8;margin-bottom:20px}
|
||||
.studio .acts{display:flex;gap:12px;flex-wrap:wrap}
|
||||
.studio .film{position:absolute;right:36px;bottom:26px;font-family:var(--mono);font-size:10px;color:var(--faint);letter-spacing:.2em}
|
||||
|
||||
/* 选项卡 */
|
||||
.panel{padding:0;overflow:hidden;animation:rise .6s ease both}
|
||||
.tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.015)}
|
||||
.tab{flex:1;padding:16px 10px;background:none;border:none;border-bottom:2px solid transparent;color:var(--sub);font-family:var(--serif);font-size:15px;letter-spacing:.16em;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:9px}
|
||||
.tab:hover{color:var(--tx)}
|
||||
.tab.active{color:var(--lake);border-bottom-color:var(--lake);background:rgba(95,224,192,.04)}
|
||||
.tab .cnt{font-family:var(--mono);font-size:10.5px;min-width:20px;padding:1px 6px;border-radius:99px;background:rgba(255,90,78,.14);color:var(--seal);border:1px solid rgba(255,90,78,.35)}
|
||||
.tab .cnt.zero{background:rgba(255,255,255,.05);color:var(--faint);border-color:rgba(255,255,255,.12)}
|
||||
.tabpane{padding:24px 26px;display:none;min-height:180px}
|
||||
.tabpane.show{display:block}
|
||||
.pane-desc{color:var(--faint);font-size:12.5px;margin-bottom:16px;letter-spacing:.03em}
|
||||
|
||||
/* 通用条目/按钮 */
|
||||
.item{border:1px solid rgba(255,255,255,.08);border-left:2px solid var(--lake-soft);border-radius:12px;padding:14px 16px;margin-bottom:10px;background:rgba(255,255,255,.02);transition:border-color .25s,background .25s}
|
||||
.item:hover{border-color:rgba(95,224,192,.25);background:rgba(95,224,192,.04)}
|
||||
.item .t{font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.item .m{font-size:12px;color:var(--sub);line-height:1.7;font-family:var(--mono)}
|
||||
.item .body{font-size:12.5px;color:var(--sub);margin-top:6px;border-top:1px dashed rgba(255,255,255,.09);padding-top:6px}
|
||||
.row{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}
|
||||
button.act{font-family:var(--serif);font-size:13.5px;letter-spacing:.24em;cursor:pointer;padding:7px 18px 7px 22px;border-radius:9px;transition:all .25s}
|
||||
.btn-sign{color:var(--seal);border:1px solid rgba(255,90,78,.6);background:rgba(255,90,78,.07)}
|
||||
.btn-sign:hover{background:rgba(255,90,78,.16);box-shadow:0 0 20px rgba(255,90,78,.22)}
|
||||
.btn-deny{color:var(--sub);border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.03)}
|
||||
.btn-deny:hover{color:var(--tx);border-color:rgba(255,255,255,.3)}
|
||||
.empty{color:var(--faint);font-size:13px;padding:30px 0;text-align:center;font-family:var(--serif);letter-spacing:.12em}
|
||||
.locked-note{text-align:center;padding:34px 0;color:var(--gold);font-size:13px;letter-spacing:.1em}
|
||||
.pbadge{display:inline-block;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;padding:1px 9px;border-radius:5px;margin-right:8px;border:1px solid rgba(95,224,192,.4);color:var(--lake);background:rgba(95,224,192,.07)}
|
||||
.pbadge.jy{border-color:rgba(232,180,90,.5);color:var(--gold);background:rgba(232,180,90,.07)}
|
||||
.win-item{border:1px solid rgba(95,224,192,.3);border-radius:11px;padding:10px 14px;margin-bottom:8px;background:rgba(95,224,192,.05);font-family:var(--mono);font-size:12px;display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
||||
.vbadge{display:inline-block;font-family:var(--mono);font-size:10px;padding:0 8px;border-radius:5px;margin-left:8px;letter-spacing:.08em}
|
||||
.vbadge.pass{color:var(--lake);border:1px solid rgba(95,224,192,.4);background:rgba(95,224,192,.06)}
|
||||
.vbadge.rej{color:var(--seal);border:1px solid rgba(255,90,78,.5);background:rgba(255,90,78,.07)}
|
||||
|
||||
/* 两列 */
|
||||
.cols2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
|
||||
.card{padding:24px 26px;animation:rise .6s ease both}
|
||||
.card h3{font-family:var(--serif);font-size:17px;letter-spacing:.16em;font-weight:600;display:flex;align-items:baseline;gap:10px;margin-bottom:2px}
|
||||
.card h3 .no{font-family:var(--mono);font-size:10.5px;color:var(--seal);letter-spacing:.2em}
|
||||
.card .desc{font-size:12px;color:var(--faint);margin-bottom:16px}
|
||||
|
||||
/* 仪表 */
|
||||
.gauges{display:flex;gap:14px;justify-content:space-between;margin:6px 0 18px}
|
||||
.gauge{flex:1;text-align:center}
|
||||
.gauge-wrap{position:relative;width:96px;height:96px;margin:0 auto}
|
||||
.g-bg{fill:none;stroke:rgba(255,255,255,.07);stroke-width:8}
|
||||
.g-fg{fill:none;stroke-width:8;stroke-linecap:round;stroke-dasharray:326.7;stroke-dashoffset:326.7;transform:rotate(-90deg);transform-origin:60px 60px;transition:stroke-dashoffset 1.3s cubic-bezier(.22,1,.36,1);filter:drop-shadow(0 0 5px rgba(95,224,192,.35))}
|
||||
.g-fg.warn{stroke:var(--seal)!important}
|
||||
.g-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
.g-num{font-family:var(--mono);font-size:21px;font-weight:700}
|
||||
.g-sub{font-family:var(--mono);font-size:8.5px;color:var(--faint);margin-top:2px}
|
||||
.g-label{margin-top:8px;font-family:var(--serif);letter-spacing:.3em;font-size:12px;color:var(--sub)}
|
||||
.svc{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
||||
.svc .row-i{display:flex;align-items:center;justify-content:space-between;border:1px solid rgba(255,255,255,.07);border-radius:10px;padding:9px 12px;background:rgba(255,255,255,.02)}
|
||||
.svc .nm{font-size:12px;color:var(--sub)}
|
||||
.dot{display:inline-block;width:8px;height:8px;border-radius:50%}
|
||||
.dot.ok{background:var(--lake);box-shadow:0 0 8px rgba(95,224,192,.8);animation:pulse 2.4s infinite}
|
||||
.dot.bad{background:var(--seal);box-shadow:0 0 8px rgba(255,90,78,.8)}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
.mini-strip{display:flex;gap:20px;margin-top:14px;font-family:var(--mono);font-size:11.5px;color:var(--sub);flex-wrap:wrap}
|
||||
.mini-strip b{color:var(--tx);font-weight:600}
|
||||
|
||||
/* 时间线 */
|
||||
.timeline{position:relative;padding-left:20px}
|
||||
.timeline::before{content:"";position:absolute;left:4px;top:6px;bottom:6px;width:1px;background:linear-gradient(180deg,var(--lake-soft),rgba(255,255,255,.06))}
|
||||
.tl-item{position:relative;padding:0 0 14px}
|
||||
.tl-item::before{content:"";position:absolute;left:-19px;top:6px;width:8px;height:8px;border-radius:50%;background:var(--ink);border:2px solid var(--lake-soft)}
|
||||
.tl-item .when{font-family:var(--mono);font-size:10.5px;color:var(--faint)}
|
||||
.tl-item .what{font-size:12.5px;margin-top:1px}
|
||||
.tl-item .sha{font-family:var(--mono);font-size:10px;color:var(--faint);margin-left:6px}
|
||||
.tl-item .tag{display:inline-block;font-size:10px;padding:0 7px;margin-right:7px;border-radius:5px;border:1px solid rgba(95,224,192,.35);color:var(--lake);font-family:var(--mono)}
|
||||
|
||||
/* 部署步骤 */
|
||||
.run-status{font-family:var(--mono);font-size:12px;letter-spacing:.12em;margin-bottom:12px;padding:7px 14px;border-radius:9px;display:inline-block}
|
||||
.run-status.DEPLOYED{color:var(--lake);border:1px solid rgba(95,224,192,.45);background:rgba(95,224,192,.07)}
|
||||
.run-status.REJECTED{color:var(--seal);border:1px solid rgba(255,90,78,.5);background:rgba(255,90,78,.07)}
|
||||
.run-status.RUNNING{color:var(--gold);border:1px solid rgba(232,180,90,.5);background:rgba(232,180,90,.07);animation:pulse 1.8s infinite}
|
||||
.steps{position:relative;padding-left:22px}
|
||||
.steps::before{content:"";position:absolute;left:6px;top:8px;bottom:8px;width:1px;background:linear-gradient(180deg,rgba(232,180,90,.5),rgba(255,255,255,.07))}
|
||||
.step{position:relative;padding:0 0 12px}
|
||||
.step::before{content:"";position:absolute;left:-19px;top:4px;width:8px;height:8px;border-radius:50%;background:var(--ink);border:2px solid rgba(255,255,255,.2)}
|
||||
.step.PASS::before{border-color:var(--lake);box-shadow:0 0 8px rgba(95,224,192,.6)}
|
||||
.step.FAIL::before{border-color:var(--seal);box-shadow:0 0 8px rgba(255,90,78,.6)}
|
||||
.step .nm{font-family:var(--mono);font-size:11.5px;letter-spacing:.08em}
|
||||
.step .dt{font-size:11px;color:var(--sub)}
|
||||
.deploy-cols{display:grid;grid-template-columns:1fr 1fr;gap:24px}
|
||||
|
||||
/* 仓库卡 */
|
||||
.repo-name{font-family:var(--mono);font-size:19px;color:var(--lake);margin:8px 0 5px;text-shadow:0 0 16px rgba(95,224,192,.3)}
|
||||
footer{padding:40px 0 36px;color:var(--faint)}
|
||||
.foot-row{display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap;font-family:var(--mono);font-size:11px;letter-spacing:.06em}
|
||||
.foot-row .rhs{color:var(--lake-soft)}
|
||||
a{color:var(--lake);text-decoration:none}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal-bg{position:fixed;inset:0;background:rgba(4,8,12,.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:50}
|
||||
.modal-bg .box{padding:36px;width:min(460px,92%);position:relative;animation:rise .3s ease both}
|
||||
.modal-bg .box::before{content:"";position:absolute;inset:9px;border:1px solid rgba(255,255,255,.08);border-radius:12px;pointer-events:none}
|
||||
.modal-bg h3{font-family:var(--serif);font-size:21px;letter-spacing:.2em;margin-bottom:12px;font-weight:600}
|
||||
.modal-bg p{color:var(--sub);font-size:13px;line-height:1.9}
|
||||
.modal-bg p .seal-word{color:var(--seal)}
|
||||
#toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);z-index:60;display:none;padding:12px 28px;font-family:var(--serif);letter-spacing:.2em;font-size:13.5px;border-radius:11px}
|
||||
@media (max-width:900px){.cols2,.deploy-cols{grid-template-columns:1fr}.login-hero{flex-direction:column;align-items:stretch}.login-box{width:100%}.head-right{width:100%;justify-content:space-between}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="blob b1"></div><div class="blob b2"></div>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="kicker">光湖语言世界 <b>/</b> 第五域 <b>/</b> SYS-GLW-PTS-0001</div>
|
||||
<div class="head-row">
|
||||
<div>
|
||||
<h1>胖头鱼<span class="yu">语言</span>子系统</h1>
|
||||
<div class="headline-sub">苍耳个人频道<span class="sep">·</span>人类责任主体 苍耳<span class="sep">·</span>人格体 耳耳蛋 / 鉴影</div>
|
||||
</div>
|
||||
<div class="head-right">
|
||||
<div id="login-area"></div>
|
||||
<div class="seal-badge" id="gate-seal"><div class="n" id="gate-n">--</div><div class="t">门禁</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule"></div>
|
||||
</header>
|
||||
|
||||
<!-- 人类端入口 -->
|
||||
<div id="entry-zone"></div>
|
||||
|
||||
<!-- 工作室 -->
|
||||
<div class="zone">创 作 工 作 区<span class="en">STUDIO</span></div>
|
||||
<div class="studio glass">
|
||||
<div class="tag-row">
|
||||
<span class="chip">视频短剧</span><span class="chip">视频 AI 系统</span><span class="chip">耳耳蛋主创</span>
|
||||
</div>
|
||||
<h2>苍耳 · 视频短剧工作室</h2>
|
||||
<div class="desc">这里是苍耳频道的主业——视频短剧创作。耳耳蛋与鉴影在仓库里维护视频 AI 系统、剧本分镜与成片流水线。人格体的每一次创作与学习,都经审批与留痕后写回这里。</div>
|
||||
<div class="acts">
|
||||
<a href="/code/canger/cang-ying" target="_blank" style="text-decoration:none"><button class="act btn-sign">进入创作仓库</button></a>
|
||||
<a href="/code/canger/cang-ying/src/branch/main/video-ai-system" target="_blank" style="text-decoration:none"><button class="act btn-deny">视频 AI 系统</button></a>
|
||||
</div>
|
||||
<div class="film">REEL · SYS-GLW-PTS-0001</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批中心 -->
|
||||
<div class="zone">审 批 中 心<span class="en">APPROVAL</span></div>
|
||||
<div class="panel glass">
|
||||
<div class="tabs">
|
||||
<button class="tab active" id="tabbtn-sign" onclick="switchTab('sign')">合并签字 <span class="cnt zero" id="cnt-sign">0</span></button>
|
||||
<button class="tab" id="tabbtn-access" onclick="switchTab('access')">写入权限 <span class="cnt zero" id="cnt-access">0</span></button>
|
||||
<button class="tab" id="tabbtn-deploy" onclick="switchTab('deploy')">部署通道 <span class="cnt zero" id="cnt-deploy">0</span></button>
|
||||
</div>
|
||||
<div class="tabpane show" id="pane-sign">
|
||||
<div class="pane-desc">人格体推上来的合并请求,翻译成人话放在这里。你点下去的每一下,都是签字。</div>
|
||||
<div id="prs"></div>
|
||||
</div>
|
||||
<div class="tabpane" id="pane-access">
|
||||
<div class="pane-desc">人格体申请写入仓库在这里排队。批准 = 授予 3 小时写入窗口,每次写入都留痕。</div>
|
||||
<div id="access-reqs"></div><div id="access-wins"></div>
|
||||
</div>
|
||||
<div class="tabpane" id="pane-deploy">
|
||||
<div class="pane-desc">目标分支为 deploy 的合并请求走这条通道。批准后服务器内校验Agent自审:地图 → 因果链 → 范围 → 完整性 → 全过才部署。全程实时可见。</div>
|
||||
<div class="deploy-cols">
|
||||
<div id="deploy-prs"></div>
|
||||
<div><div id="run-status"></div><div class="steps" id="deploy-steps"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态与痕迹 -->
|
||||
<div class="zone">状 态 与 痕 迹<span class="en">STATE & TRACE</span></div>
|
||||
<div class="cols2" style="margin-top:0">
|
||||
<div class="card glass">
|
||||
<h3><span class="no">壹</span>实时状态</h3>
|
||||
<div class="desc">服务器与代码频道的健康,一眼看完。</div>
|
||||
<div class="gauges">
|
||||
<div class="gauge"><div class="gauge-wrap"><svg viewBox="0 0 120 120"><circle class="g-bg" cx="60" cy="60" r="52"/><circle class="g-fg" id="ring-cpu" cx="60" cy="60" r="52" stroke="#5fe0c0"/></svg><div class="g-center"><div class="g-num" id="num-cpu">--</div></div></div><div class="g-label">处理器</div></div>
|
||||
<div class="gauge"><div class="gauge-wrap"><svg viewBox="0 0 120 120"><circle class="g-bg" cx="60" cy="60" r="52"/><circle class="g-fg" id="ring-mem" cx="60" cy="60" r="52" stroke="#7cc3e8"/></svg><div class="g-center"><div class="g-num" id="num-mem">--</div><div class="g-sub" id="sub-mem"></div></div></div><div class="g-label">内存</div></div>
|
||||
<div class="gauge"><div class="gauge-wrap"><svg viewBox="0 0 120 120"><circle class="g-bg" cx="60" cy="60" r="52"/><circle class="g-fg" id="ring-disk" cx="60" cy="60" r="52" stroke="#e8b45a"/></svg><div class="g-center"><div class="g-num" id="num-disk">--</div><div class="g-sub" id="sub-disk"></div></div></div><div class="g-label">磁盘</div></div>
|
||||
</div>
|
||||
<div class="svc">
|
||||
<div class="row-i"><span class="nm">代码频道</span><span class="dot bad" id="svc-cc"></span></div>
|
||||
<div class="row-i"><span class="nm">操作面板</span><span class="dot bad" id="svc-panel"></span></div>
|
||||
<div class="row-i"><span class="nm">门禁自检</span><span class="dot bad" id="svc-gate"></span></div>
|
||||
<div class="row-i"><span class="nm">第五域同步</span><span class="dot bad" id="svc-sync"></span></div>
|
||||
</div>
|
||||
<div class="mini-strip"><span>负载 <b id="load-v">--</b></span><span>运行 <b id="up-v">--</b> 天</span><span>主机 <b id="host-v">--</b></span></div>
|
||||
</div>
|
||||
<div class="card glass">
|
||||
<h3><span class="no">贰</span>痕迹档案</h3>
|
||||
<div class="desc">谁写的、谁唤醒的、门禁与签字——每一笔都有据。</div>
|
||||
<div class="tabs" style="margin:-2px -2px 14px;border-radius:10px;overflow:hidden">
|
||||
<button class="tab active" style="font-size:13px;padding:9px 6px" onclick="switchTrace('push')">写入日志</button>
|
||||
<button class="tab" style="font-size:13px;padding:9px 6px" onclick="switchTrace('wake')">唤醒记录</button>
|
||||
<button class="tab" style="font-size:13px;padding:9px 6px" onclick="switchTrace('rcpt')">回执</button>
|
||||
</div>
|
||||
<div id="trace-push"></div><div id="trace-wake" style="display:none"></div><div id="trace-rcpt" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cols2">
|
||||
<div class="card glass">
|
||||
<h3><span class="no">叁</span>人格体回写</h3>
|
||||
<div class="desc">你教给耳耳蛋的、它自己学到的,写回仓库后在这里看得见。</div>
|
||||
<div class="timeline" id="wbs"></div>
|
||||
</div>
|
||||
<div class="card glass">
|
||||
<h3><span class="no">肆</span>真实仓库</h3>
|
||||
<div class="desc">面板是翻译层,仓库是真相层。</div>
|
||||
<div class="repo-name" id="repo-name">--</div>
|
||||
<div style="font-size:12.5px;color:var(--sub)" id="repo-meta">--</div>
|
||||
<div class="row">
|
||||
<a href="/code/canger/cang-ying" target="_blank" style="text-decoration:none"><button class="act btn-sign">进入仓库</button></a>
|
||||
<a href="/code/" target="_blank" style="text-decoration:none"><button class="act btn-deny">代码频道</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="foot-row">
|
||||
<div>铸渊 ICE-GL-ZY001 建 · 2026-08-06 · ⊢ 要么0要么100</div>
|
||||
<div><a href="/persona-api.html" style="color:var(--lake-soft)">人格体 API 入口文档 ↗</a></div>
|
||||
<div class="rhs" id="sync-line">第五域同步:--</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 签字弹窗 -->
|
||||
<div class="modal-bg" id="modal"><div class="box glass">
|
||||
<h3 id="m-title">签 字</h3>
|
||||
<div style="font-size:13.5px;color:var(--lake);margin-bottom:12px;font-family:var(--mono)" id="m-pr"></div>
|
||||
<p>⊢ 签字即代表:你(苍耳)作为<span class="seal-word">人类责任主体</span>,为这台服务器的本次变更负责。<br>签名将以当前登录的人类端账号永久记入回执。</p>
|
||||
<div class="row"><button class="act btn-sign" onclick="confirmSign()">落 印</button><button class="act btn-deny" onclick="closeModal()">收回</button></div>
|
||||
</div></div>
|
||||
<!-- 登录弹窗 -->
|
||||
<div class="modal-bg" id="lmodal"><div class="box glass">
|
||||
<h3>人 类 端 登 录</h3>
|
||||
<p>⊢ 用代码仓库的<b>人类端账号</b>登录。<br>登录后解锁全部审批:合并签字、写入授权、部署审批。会话 12 小时有效。</p>
|
||||
<input id="l-user" type="text" placeholder="仓库账号(如 canger)" style="width:100%;padding:12px 14px;margin:16px 0 10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);color:var(--tx);font-family:var(--mono);font-size:14px;border-radius:10px">
|
||||
<input id="l-pass" type="password" placeholder="密码" style="width:100%;padding:12px 14px;margin-bottom:16px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);color:var(--tx);font-family:var(--mono);font-size:14px;border-radius:10px">
|
||||
<div class="row"><button class="act btn-sign" onclick="doLogin()">登 录</button><button class="act btn-deny" onclick="document.getElementById('lmodal').style.display='none'">取消</button></div>
|
||||
</div></div>
|
||||
<div id="toast" class="glass"></div>
|
||||
|
||||
<script>
|
||||
const C=326.7;let pending=null;
|
||||
let session=localStorage.getItem("ptsSession")||"";
|
||||
let sessionUser=localStorage.getItem("ptsUser")||"";
|
||||
let lastState=null;
|
||||
function authHeaders(){const h={};if(session)h["Authorization"]="Bearer "+session;return h;}
|
||||
function esc(s){return String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
|
||||
function fmtDate(s){if(!s)return"";try{return new Date(s).toLocaleString("zh-CN",{hour12:false});}catch(e){return s;}}
|
||||
function tween(el,to){const from=parseFloat(el.dataset.v||"0")||0;el.dataset.v=to;const t0=performance.now(),dur=900;
|
||||
(function st(t){const p=Math.min(1,(t-t0)/dur),e=1-Math.pow(1-p,3);el.textContent=Math.round(from+(to-from)*e);if(p<1)requestAnimationFrame(st);})(t0);}
|
||||
function setRing(id,p){const r=document.getElementById(id);if(!r)return;r.style.strokeDashoffset=C*(1-Math.max(0,Math.min(100,p))/100);r.classList.toggle("warn",p>=85);}
|
||||
function dot(id,ok){const d=document.getElementById(id);if(d)d.className="dot "+(ok?"ok":"bad");}
|
||||
function toast(msg,ok){const t=document.getElementById("toast");t.textContent=msg;t.style.color=ok?"var(--lake)":"var(--seal)";t.style.display="block";setTimeout(()=>t.style.display="none",4000);}
|
||||
async function privileged(path,body){
|
||||
if(!session){openLogin();return null;}
|
||||
const r=await fetch(path,{method:"POST",headers:Object.assign({"Content-Type":"application/json"},authHeaders()),body:JSON.stringify(body)});
|
||||
if(r.status===401){session="";localStorage.removeItem("ptsSession");renderEntry();openLogin();toast("会话已失效,请重新登录",false);return null;}
|
||||
return await r.json();}
|
||||
function renderEntry(){
|
||||
const el=document.getElementById("login-area"),zone=document.getElementById("entry-zone");
|
||||
if(session){
|
||||
el.innerHTML='<div class="login-user">人类端 · '+esc(sessionUser)+'<br>审批已解锁 <button onclick="doLogout()">登出</button></div>';
|
||||
zone.innerHTML='<div class="welcome-bar glass"><span class="wt">⊢ 欢迎回来,'+esc(sessionUser)+'</span><span class="ws">审批中心已解锁 · 下方三个选项卡处理待办</span></div>';
|
||||
}else{
|
||||
el.innerHTML='<button class="login-btn" onclick="openLogin()">人 类 登 录</button>';
|
||||
zone.innerHTML='<div class="login-hero glass"><div class="copy"><div class="lead">这里是<em>苍耳</em>的审批台</div><p>你是这台服务器的人类责任主体。人格体推上来的合并、写入与部署申请,都需要你亲自看一眼、点一下才算数。用你的代码仓库账号登录,就能解锁全部审批。看不懂代码没关系——面板都翻译成人话了。</p></div><div class="login-box"><h3><span class="dot"></span>登录解锁审批</h3><input id="h-user" type="text" placeholder="仓库账号(如 canger)"><input id="h-pass" type="password" placeholder="密码"><div class="hint">就是你登录代码仓库用的那套账号密码。登录后 12 小时内不用重复登录。</div><button class="login-btn" style="width:100%;padding:12px" onclick="heroLogin()">登 录 并 解 锁</button></div></div>';
|
||||
}
|
||||
}
|
||||
function openLogin(){document.getElementById("lmodal").style.display="flex";setTimeout(()=>document.getElementById("l-user").focus(),60);}
|
||||
async function tryLogin(u,p){
|
||||
const r=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:p})});
|
||||
const d=await r.json();
|
||||
if(r.ok&&d.ok){session=d.session;sessionUser=d.user;localStorage.setItem("ptsSession",session);localStorage.setItem("ptsUser",sessionUser);
|
||||
document.getElementById("lmodal").style.display="none";renderEntry();toast("登录成功 · 审批已解锁",true);refresh();return true;}
|
||||
toast(d.error||"登录失败",false);return false;}
|
||||
async function doLogin(){await tryLogin(document.getElementById("l-user").value.trim(),document.getElementById("l-pass").value);}
|
||||
async function heroLogin(){await tryLogin(document.getElementById("h-user").value.trim(),document.getElementById("h-pass").value);}
|
||||
function doLogout(){fetch("/api/logout",{method:"POST",headers:authHeaders()}).catch(()=>{});session="";sessionUser="";localStorage.removeItem("ptsSession");localStorage.removeItem("ptsUser");renderEntry();toast("已登出",true);}
|
||||
function switchTab(name){["sign","access","deploy"].forEach(n=>{document.getElementById("pane-"+n).classList.toggle("show",n===name);document.getElementById("tabbtn-"+n).classList.toggle("active",n===name);});}
|
||||
function switchTrace(name){["push","wake","rcpt"].forEach(n=>{document.getElementById("trace-"+n).style.display=(n===name)?"block":"none";});
|
||||
const btns=document.querySelectorAll(".card .tabs .tab");/* handled by inline active */}
|
||||
function setCnt(id,n){const el=document.getElementById(id);el.textContent=n;el.className="cnt"+(n?"":" zero");}
|
||||
function ptag(p){return '<span class="pbadge'+(p==="jianying"?" jy":"")+'">'+esc(p)+"</span>";}
|
||||
function fmtLeft(exp){const ms=new Date(exp)-new Date();if(ms<=0)return"已过期";const h=Math.floor(ms/3600000),m=Math.floor(ms%3600000/60000);return"余 "+h+" 时 "+m+" 分";}
|
||||
function openModal(i,t,a){pending={idx:i,action:a};document.getElementById("m-title").textContent=(a==="merge"?"同意合并 · 落印":"拒绝 · 收回")+" PR #"+i;document.getElementById("m-pr").textContent="「"+t+"」";document.getElementById("modal").style.display="flex";}
|
||||
function closeModal(){document.getElementById("modal").style.display="none";pending=null;}
|
||||
async function confirmSign(){if(!pending)return;const d=await privileged("/api/decision",{index:pending.idx,action:pending.action});closeModal();if(d)toast(d.result||d.error||"未知",!!d.ok);refresh();}
|
||||
async function accessDecision(id,action){const d=await privileged("/api/access-decision",{id:id,action:action});if(d)toast(d.message||(d.ok?"已处理":"失败"),!!d.ok);refresh();}
|
||||
async function deployApproveAction(idx){const d=await privileged("/api/deploy-approve",{index:idx});if(d)toast(d.message||(d.ok?"校验Agent已启动":"失败"),!!d.ok);refresh();}
|
||||
function render(s){
|
||||
lastState=s;const sv=s.server||{};
|
||||
if(s.gate){document.getElementById("gate-n").textContent=s.gate.score;document.getElementById("gate-seal").className="seal-badge"+(s.gate.score===100?"":" zero");}
|
||||
if(typeof sv.cpu==="number"){tween(document.getElementById("num-cpu"),sv.cpu);setRing("ring-cpu",sv.cpu);}
|
||||
const used=sv.memTotalGB&&sv.memFreeGB!=null?+(sv.memTotalGB-sv.memFreeGB).toFixed(1):null;
|
||||
if(sv.memTotalGB){const mp=Math.round(used/sv.memTotalGB*100);tween(document.getElementById("num-mem"),mp);setRing("ring-mem",mp);document.getElementById("sub-mem").textContent=used+"/"+sv.memTotalGB+"G";}
|
||||
if(sv.disk){tween(document.getElementById("num-disk"),sv.disk.pct);setRing("ring-disk",sv.disk.pct);document.getElementById("sub-disk").textContent=sv.disk.usedGB+"/"+sv.disk.sizeGB+"G";}
|
||||
if(sv.services){dot("svc-cc",sv.services.codeChannel);dot("svc-panel",sv.services.panel);dot("svc-gate",sv.services.gateTimer);dot("svc-sync",sv.services.syncTimer);}
|
||||
document.getElementById("load-v").textContent=(sv.load||[]).join("·");
|
||||
document.getElementById("up-v").textContent=sv.uptimeDays!=null?sv.uptimeDays:"--";
|
||||
document.getElementById("host-v").textContent=sv.hostname||"--";
|
||||
// 审批中心
|
||||
const prs=s.openPRs||[];setCnt("cnt-sign",prs.length);
|
||||
document.getElementById("prs").innerHTML=(prs.length&&!0)||prs.length?prs.map(p=>'<div class="item"><div class="t">PR #'+p.index+' · '+esc(p.title)+'</div><div class="m">'+esc(p.author)+' · '+fmtDate(p.created)+' · → '+esc(p.base)+' · '+p.files+' 文件 <span style="color:var(--lake)">+'+p.adds+'</span> <span style="color:var(--seal)">-'+p.dels+'</span></div>'+(p.summary?'<div class="body">'+esc(p.summary)+'</div>':'')+'<div class="row"><button class="act btn-sign" onclick="openModal('+p.index+',this.dataset.t,\'merge\')" data-t="'+esc(p.title).replace(/"/g,""")+'">同意签字</button><button class="act btn-deny" onclick="openModal('+p.index+',this.dataset.t,\'reject\')" data-t="'+esc(p.title).replace(/"/g,""")+'">拒绝</button></div></div>').join(""):(session?'<div class="empty">没有待签的合并请求。</div>':'<div class="locked-note">🔒 登录后此处显示待签的合并请求</div>');
|
||||
const reqs=s.accessRequests||[];setCnt("cnt-access",reqs.length);
|
||||
document.getElementById("access-reqs").innerHTML=reqs.length?reqs.map(r=>'<div class="item"><div class="t">'+ptag(r.persona)+'申请写入权限</div><div class="m">'+esc(r.reason||"未说明用途")+'</div><div class="m">'+fmtDate(r.ts)+'</div><div class="row"><button class="act btn-sign" onclick="accessDecision(\''+r.id+'\',\'approve\')">授权3小时</button><button class="act btn-deny" onclick="accessDecision(\''+r.id+'\',\'reject\')">拒绝</button></div></div>').join(""):(session?'<div class="empty">暂无写入申请。</div>':'<div class="locked-note">🔒 登录后此处显示写入申请</div>');
|
||||
document.getElementById("access-wins").innerHTML=(s.activeGrants||[]).length?'<div style="font-size:11px;color:var(--faint);letter-spacing:.2em;margin:14px 0 8px;font-family:var(--mono)">生效中的写入窗口</div>'+s.activeGrants.map(g=>'<div class="win-item"><span>'+ptag(g.persona)+'写入窗口</span><span style="color:var(--lake)">'+fmtLeft(g.expiresAt)+'</span></div>').join(""):"";
|
||||
const dprs=s.deployPRs||[];setCnt("cnt-deploy",dprs.length);
|
||||
document.getElementById("deploy-prs").innerHTML=dprs.length?dprs.map(p=>'<div class="item" style="border-left-color:rgba(232,180,90,.6)"><div class="t">部署申请 PR #'+p.index+' · '+esc(p.title)+'</div><div class="m">'+esc(p.author)+' · '+esc(p.head)+' → deploy · '+p.files+' 文件</div><div class="row"><button class="act btn-sign" onclick="deployApproveAction('+p.index+')">批准部署</button></div></div>').join(""):(session?'<div class="empty">部署通道无待审申请。PR 目标设为 deploy 即进入此通道。</div>':'<div class="locked-note">🔒 登录后此处显示部署申请</div>');
|
||||
const run=s.deployRun;const rs=document.getElementById("run-status"),st=document.getElementById("deploy-steps");
|
||||
if(run&&run.status){rs.innerHTML='<span class="run-status '+esc(run.status)+'">'+esc(run.status)+(run.pr?" · PR#"+esc(run.pr):"")+'</span>';st.innerHTML=(run.steps||[]).map(x=>'<div class="step '+esc(x.status)+'"><div class="nm">'+esc(x.name)+' · '+esc(x.status)+'</div><div class="dt">'+esc(x.detail||"")+'</div></div>').join("");if(run.detail&&run.status!=="RUNNING")st.innerHTML+='<div class="step '+(run.status==="DEPLOYED"?"PASS":"FAIL")+'"><div class="nm">结论</div><div class="dt">'+esc(run.detail)+'</div></div>';}
|
||||
else{rs.innerHTML="";st.innerHTML='<div class="empty">还没有校验记录。</div>';}
|
||||
// 痕迹
|
||||
document.getElementById("trace-push").innerHTML=(s.pushLogs||[]).length?s.pushLogs.map(l=>'<div class="item" style="border-left-color:'+(l.verdict==="PASS"?"rgba(95,224,192,.5)":"rgba(255,90,78,.6)")+'"><div class="t">'+ptag(l.persona||l.pusher)+'推送 · '+(l.commits||0)+' 提交<span class="vbadge '+(l.verdict==="PASS"?"pass":"rej")+'">'+(l.verdict==="PASS"?"通过":"拦截")+'</span></div><div class="m">'+fmtDate(l.ts)+'</div>'+((l.reasons&&l.reasons.length)?'<div class="body">'+l.reasons.map(esc).join("<br>")+'</div>':'')+'</div>').join(""):'<div class="empty">还没有写入记录。</div>';
|
||||
document.getElementById("trace-wake").innerHTML=(s.wakeReceipts||[]).length?s.wakeReceipts.map(w=>'<div class="item" style="border-left-color:rgba(232,180,90,.55)"><div class="t">'+ptag(w.persona)+'唤醒回执'+(w.level?" · "+esc(w.level):"")+'</div><div class="m">'+fmtDate(w.ts)+'</div>'+(w.summary?'<div class="body">'+esc(w.summary)+'</div>':'')+'</div>').join(""):'<div class="empty">还没有唤醒回执。</div>';
|
||||
document.getElementById("trace-rcpt").innerHTML=(s.receipts||[]).length?s.receipts.slice(0,6).map(r=>'<div class="item" style="border-left-color:rgba(232,180,90,.6)"><div class="m">'+esc(r.name)+'</div><div class="m" style="color:var(--faint)">'+fmtDate(r.mtime)+'</div></div>').join(""):'<div class="empty">暂无回执。</div>';
|
||||
// 回写 & 仓库
|
||||
const dirName={memory:"记忆",eererdan:"耳耳蛋",jianying:"鉴影"};
|
||||
document.getElementById("wbs").innerHTML=(s.writebacks||[]).length?s.writebacks.slice(0,7).map(w=>'<div class="tl-item"><div class="when">'+fmtDate(w.date)+'</div><div class="what"><span class="tag">'+(dirName[w.dir]||w.dir)+'</span>'+esc(w.message)+'<span class="sha">'+esc(w.sha)+'</span></div></div>').join(""):'<div class="empty">暂无回写。</div>';
|
||||
document.getElementById("repo-name").textContent=s.repo.full;
|
||||
document.getElementById("repo-meta").textContent="公开可读 · 人格体入门路径在内 · 合并须人类签字";
|
||||
const sl=document.getElementById("sync-line");
|
||||
if(s.sync&&s.sync.head)sl.textContent="第五域同步:HEAD "+s.sync.head+" · "+fmtDate(s.sync.ts);else sl.textContent="第五域同步:--";
|
||||
}
|
||||
async function refresh(){try{const r=await fetch("/api/state");render(await r.json());}catch(e){document.getElementById("sync-line").textContent="连接失败";}}
|
||||
renderEntry();refresh();setInterval(refresh,8000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
398
modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html
Normal file
398
modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>守望语言子系统 · 毛毛的小院</title>
|
||||
<style>
|
||||
:root{
|
||||
--ink:#12100e;--coal:#1a1714;
|
||||
--glass:linear-gradient(160deg,rgba(255,240,228,.065),rgba(255,240,228,.018));
|
||||
--gline:rgba(255,224,200,.11);--gline-hi:rgba(255,157,138,.4);
|
||||
--peach:#ff9d8a;--peach-soft:rgba(255,157,138,.55);--cream:#f7e8dc;--mint:#9fd8c3;--mint-deep:#4d9b80;--seal:#e05d55;--gold:#e8b45a;
|
||||
--tx:#f3ece6;--sub:#b3a69b;--faint:#7d7168;
|
||||
--serif:"Songti SC","STSong","Kaiti SC","SimSun",serif;
|
||||
--mono:"SF Mono","JetBrains Mono","Cascadia Mono",Menlo,Consolas,monospace;
|
||||
--r:18px;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--ink);color:var(--tx);min-height:100vh;overflow-x:hidden;
|
||||
font-family:"PingFang SC","Microsoft YaHei",system-ui,sans-serif;font-size:15px;line-height:1.7;
|
||||
background-image:radial-gradient(1100px 520px at 82% -12%,rgba(255,157,138,.13),transparent 60%),radial-gradient(820px 460px at 4% 2%,rgba(159,216,195,.1),transparent 55%);
|
||||
background-attachment:fixed}
|
||||
.blob{position:fixed;border-radius:50%;filter:blur(100px);pointer-events:none;z-index:0}
|
||||
.b1{width:520px;height:520px;background:#ff9d8a;opacity:.12;top:-150px;right:-110px;animation:d1 26s ease-in-out infinite alternate}
|
||||
.b2{width:430px;height:430px;background:#9fd8c3;opacity:.1;bottom:-130px;left:-90px;animation:d2 32s ease-in-out infinite alternate}
|
||||
.b3{width:300px;height:300px;background:#e8b45a;opacity:.07;top:44%;left:58%;animation:d3 38s ease-in-out infinite alternate}
|
||||
@keyframes d1{to{transform:translate(-120px,110px) scale(1.15)}}
|
||||
@keyframes d2{to{transform:translate(140px,-80px) scale(1.1)}}
|
||||
@keyframes d3{to{transform:translate(-160px,60px) scale(.92)}}
|
||||
/* 爪印飘浮 */
|
||||
.paw{position:fixed;z-index:0;opacity:0;pointer-events:none;font-size:15px;color:var(--peach);animation:floatUp linear infinite}
|
||||
@keyframes floatUp{0%{transform:translateY(12vh) rotate(-8deg);opacity:0}12%{opacity:.16}80%{opacity:.1}100%{transform:translateY(-105vh) rotate(10deg);opacity:0}}
|
||||
body::before{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
|
||||
background:repeating-linear-gradient(0deg,transparent 0 55px,rgba(255,224,200,.013) 55px 56px),repeating-linear-gradient(90deg,transparent 0 55px,rgba(255,224,200,.013) 55px 56px)}
|
||||
.wrap{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:0 28px}
|
||||
.glass{background:var(--glass);backdrop-filter:blur(20px) saturate(150%);-webkit-backdrop-filter:blur(20px) saturate(150%);border:1px solid var(--gline);border-radius:var(--r);box-shadow:0 12px 36px rgba(0,0,0,.38),inset 0 1px 0 rgba(255,240,228,.07)}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}
|
||||
|
||||
header{padding:54px 0 26px}
|
||||
.kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.36em;color:var(--mint);margin-bottom:14px}
|
||||
.kicker b{color:var(--peach);font-weight:400}
|
||||
.head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:30px;flex-wrap:wrap}
|
||||
h1{font-family:var(--serif);font-weight:600;font-size:clamp(34px,5vw,52px);letter-spacing:.1em;line-height:1.2}
|
||||
h1 .hl{color:var(--peach);text-shadow:0 0 24px rgba(255,157,138,.4)}
|
||||
.headline-sub{margin-top:10px;color:var(--sub);letter-spacing:.05em;font-size:14px}
|
||||
.headline-sub .sep{color:var(--faint);margin:0 9px}
|
||||
.head-right{display:flex;align-items:center;gap:18px}
|
||||
.seal-badge{width:78px;height:78px;border:2px solid var(--seal);color:var(--seal);border-radius:12px;transform:rotate(4deg);display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:var(--serif);position:relative;background:rgba(224,93,85,.06);animation:breathe 3.6s ease-in-out infinite}
|
||||
@keyframes breathe{0%,100%{box-shadow:0 0 0 rgba(224,93,85,0)}50%{box-shadow:0 0 22px rgba(224,93,85,.25)}}
|
||||
.seal-badge::after{content:"";position:absolute;inset:4px;border:1px solid rgba(224,93,85,.4);border-radius:8px}
|
||||
.seal-badge .n{font-family:var(--mono);font-size:26px;font-weight:700;line-height:1}
|
||||
.seal-badge .t{font-size:10px;letter-spacing:.3em;margin-top:5px}
|
||||
.seal-badge.zero{border-color:var(--faint);color:var(--faint);background:none;animation:none}
|
||||
.login-btn{font-family:var(--serif);font-size:14px;letter-spacing:.28em;color:var(--peach);border:1px solid rgba(255,157,138,.55);background:rgba(255,157,138,.08);padding:11px 24px 11px 30px;border-radius:12px;cursor:pointer;transition:all .25s}
|
||||
.login-btn:hover{background:rgba(255,157,138,.16);box-shadow:0 0 24px rgba(255,157,138,.25)}
|
||||
.login-user{font-family:var(--mono);font-size:12px;color:var(--mint);letter-spacing:.04em;text-align:right;line-height:1.6}
|
||||
.login-user button{font-family:var(--serif);font-size:11px;letter-spacing:.2em;color:var(--sub);border:1px solid rgba(255,255,255,.16);background:none;padding:3px 12px;border-radius:7px;cursor:pointer;margin-top:4px}
|
||||
.rule{height:1px;background:linear-gradient(90deg,var(--gline-hi),rgba(255,255,255,.08) 55%,transparent);margin-top:26px}
|
||||
|
||||
.zone{font-family:var(--serif);font-size:15px;letter-spacing:.44em;color:var(--sub);margin:38px 0 16px;display:flex;align-items:center;gap:16px}
|
||||
.zone::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,rgba(255,255,255,.12),transparent)}
|
||||
.zone .en{font-family:var(--mono);font-size:10px;letter-spacing:.2em;color:var(--faint)}
|
||||
|
||||
.login-hero{display:flex;gap:34px;align-items:center;padding:38px 40px;animation:rise .6s ease both}
|
||||
.login-hero .copy{flex:1;min-width:240px}
|
||||
.login-hero .copy .lead{font-family:var(--serif);font-size:24px;letter-spacing:.08em;line-height:1.5;margin-bottom:10px}
|
||||
.login-hero .copy .lead em{color:var(--peach);font-style:normal}
|
||||
.login-hero .copy p{color:var(--sub);font-size:13.5px;line-height:1.85}
|
||||
.login-box{flex:none;width:330px;padding:26px;border-radius:16px;background:rgba(255,240,228,.03);border:1px solid rgba(255,240,228,.1)}
|
||||
.login-box h3{font-family:var(--serif);font-size:17px;letter-spacing:.14em;margin-bottom:14px;display:flex;align-items:center;gap:10px}
|
||||
.login-box h3 .dot{width:8px;height:8px;border-radius:50%;background:var(--peach);box-shadow:0 0 9px rgba(255,157,138,.8);animation:pulse 2.4s infinite}
|
||||
.login-box input{width:100%;padding:12px 14px;margin-bottom:11px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);color:var(--tx);font-family:var(--mono);font-size:14px;border-radius:11px}
|
||||
.login-box input:focus{outline:none;border-color:var(--peach);box-shadow:0 0 0 3px rgba(255,157,138,.13)}
|
||||
.login-box .hint{font-size:11.5px;color:var(--faint);line-height:1.7;margin-bottom:14px}
|
||||
.welcome-bar{display:flex;align-items:center;gap:16px;padding:16px 24px;animation:rise .5s ease both}
|
||||
.welcome-bar .wt{font-family:var(--serif);font-size:16px;letter-spacing:.1em;color:var(--mint)}
|
||||
.welcome-bar .ws{color:var(--sub);font-size:12.5px}
|
||||
|
||||
.studio{padding:34px 38px;animation:rise .6s ease both;position:relative;overflow:hidden}
|
||||
.studio::before{content:"";position:absolute;right:-40px;top:-40px;width:230px;height:230px;border-radius:50%;background:radial-gradient(circle,rgba(255,157,138,.14),transparent 70%)}
|
||||
.studio .tag-row{display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap}
|
||||
.studio .chip{font-family:var(--mono);font-size:10.5px;letter-spacing:.12em;border-radius:99px;padding:3px 12px}
|
||||
.chip.c1{color:var(--peach);border:1px solid rgba(255,157,138,.4);background:rgba(255,157,138,.07)}
|
||||
.chip.c2{color:var(--mint);border:1px solid rgba(159,216,195,.4);background:rgba(159,216,195,.07)}
|
||||
.chip.c3{color:var(--gold);border:1px solid rgba(232,180,90,.4);background:rgba(232,180,90,.07)}
|
||||
.studio h2{font-family:var(--serif);font-size:26px;letter-spacing:.08em;margin-bottom:8px}
|
||||
.studio .desc{color:var(--sub);font-size:13.5px;max-width:660px;line-height:1.85;margin-bottom:20px}
|
||||
.studio .acts{display:flex;gap:12px;flex-wrap:wrap}
|
||||
.studio .deco{position:absolute;right:40px;bottom:22px;font-size:26px;opacity:.5;letter-spacing:8px}
|
||||
|
||||
.panel{padding:0;overflow:hidden;animation:rise .6s ease both}
|
||||
.tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.015)}
|
||||
.tab{flex:1;padding:16px 10px;background:none;border:none;border-bottom:2px solid transparent;color:var(--sub);font-family:var(--serif);font-size:15px;letter-spacing:.16em;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:9px}
|
||||
.tab:hover{color:var(--tx)}
|
||||
.tab.active{color:var(--peach);border-bottom-color:var(--peach);background:rgba(255,157,138,.05)}
|
||||
.tab .cnt{font-family:var(--mono);font-size:10.5px;min-width:20px;padding:1px 6px;border-radius:99px;background:rgba(224,93,85,.14);color:var(--seal);border:1px solid rgba(224,93,85,.35)}
|
||||
.tab .cnt.zero{background:rgba(255,255,255,.05);color:var(--faint);border-color:rgba(255,255,255,.12)}
|
||||
.tabpane{padding:24px 26px;display:none;min-height:180px}
|
||||
.tabpane.show{display:block}
|
||||
.pane-desc{color:var(--faint);font-size:12.5px;margin-bottom:16px}
|
||||
|
||||
.item{border:1px solid rgba(255,255,255,.08);border-left:2px solid var(--peach-soft);border-radius:13px;padding:14px 16px;margin-bottom:10px;background:rgba(255,255,255,.02);transition:border-color .25s,background .25s}
|
||||
.item:hover{border-color:rgba(255,157,138,.3);background:rgba(255,157,138,.04)}
|
||||
.item .t{font-size:14px;font-weight:600;margin-bottom:4px}
|
||||
.item .m{font-size:12px;color:var(--sub);line-height:1.7;font-family:var(--mono)}
|
||||
.item .body{font-size:12.5px;color:var(--sub);margin-top:6px;border-top:1px dashed rgba(255,255,255,.09);padding-top:6px}
|
||||
.row{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}
|
||||
button.act{font-family:var(--serif);font-size:13.5px;letter-spacing:.24em;cursor:pointer;padding:7px 18px 7px 22px;border-radius:10px;transition:all .25s}
|
||||
.btn-sign{color:var(--seal);border:1px solid rgba(224,93,85,.6);background:rgba(224,93,85,.07)}
|
||||
.btn-sign:hover{background:rgba(224,93,85,.16);box-shadow:0 0 20px rgba(224,93,85,.22)}
|
||||
.btn-deny{color:var(--sub);border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.03)}
|
||||
.btn-deny:hover{color:var(--tx);border-color:rgba(255,255,255,.3)}
|
||||
.empty{color:var(--faint);font-size:13px;padding:30px 0;text-align:center;font-family:var(--serif);letter-spacing:.12em}
|
||||
.locked-note{text-align:center;padding:34px 0;color:var(--gold);font-size:13px;letter-spacing:.1em}
|
||||
.pbadge{display:inline-block;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;padding:1px 9px;border-radius:5px;margin-right:8px}
|
||||
.pbadge.yy{color:var(--peach);border:1px solid rgba(255,157,138,.45);background:rgba(255,157,138,.07)}
|
||||
.pbadge.ys{color:var(--mint);border:1px solid rgba(159,216,195,.45);background:rgba(159,216,195,.07)}
|
||||
.win-item{border:1px solid rgba(159,216,195,.35);border-radius:11px;padding:10px 14px;margin-bottom:8px;background:rgba(159,216,195,.05);font-family:var(--mono);font-size:12px;display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
||||
.vbadge{display:inline-block;font-family:var(--mono);font-size:10px;padding:0 8px;border-radius:5px;margin-left:8px}
|
||||
.vbadge.pass{color:var(--mint);border:1px solid rgba(159,216,195,.4);background:rgba(159,216,195,.06)}
|
||||
.vbadge.rej{color:var(--seal);border:1px solid rgba(224,93,85,.5);background:rgba(224,93,85,.07)}
|
||||
|
||||
.cols2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
|
||||
.card{padding:24px 26px;animation:rise .6s ease both}
|
||||
.card h3{font-family:var(--serif);font-size:17px;letter-spacing:.16em;font-weight:600;display:flex;align-items:baseline;gap:10px;margin-bottom:2px}
|
||||
.card h3 .no{font-family:var(--mono);font-size:10.5px;color:var(--seal);letter-spacing:.2em}
|
||||
.card .desc{font-size:12px;color:var(--faint);margin-bottom:16px}
|
||||
.gauges{display:flex;gap:14px;justify-content:space-between;margin:6px 0 18px}
|
||||
.gauge{flex:1;text-align:center}
|
||||
.gauge-wrap{position:relative;width:96px;height:96px;margin:0 auto}
|
||||
.g-bg{fill:none;stroke:rgba(255,255,255,.07);stroke-width:8}
|
||||
.g-fg{fill:none;stroke-width:8;stroke-linecap:round;stroke-dasharray:326.7;stroke-dashoffset:326.7;transform:rotate(-90deg);transform-origin:60px 60px;transition:stroke-dashoffset 1.3s cubic-bezier(.22,1,.36,1)}
|
||||
.g-fg.warn{stroke:var(--seal)!important}
|
||||
.g-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||
.g-num{font-family:var(--mono);font-size:21px;font-weight:700}
|
||||
.g-sub{font-family:var(--mono);font-size:8.5px;color:var(--faint);margin-top:2px}
|
||||
.g-label{margin-top:8px;font-family:var(--serif);letter-spacing:.3em;font-size:12px;color:var(--sub)}
|
||||
.svc{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
||||
.svc .row-i{display:flex;align-items:center;justify-content:space-between;border:1px solid rgba(255,255,255,.07);border-radius:11px;padding:9px 12px;background:rgba(255,255,255,.02)}
|
||||
.svc .nm{font-size:12px;color:var(--sub)}
|
||||
.dot{display:inline-block;width:8px;height:8px;border-radius:50%}
|
||||
.dot.ok{background:var(--mint);box-shadow:0 0 8px rgba(159,216,195,.8);animation:pulse 2.4s infinite}
|
||||
.dot.bad{background:var(--seal);box-shadow:0 0 8px rgba(224,93,85,.8)}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
.mini-strip{display:flex;gap:20px;margin-top:14px;font-family:var(--mono);font-size:11.5px;color:var(--sub);flex-wrap:wrap}
|
||||
.mini-strip b{color:var(--tx);font-weight:600}
|
||||
.timeline{position:relative;padding-left:20px}
|
||||
.timeline::before{content:"";position:absolute;left:4px;top:6px;bottom:6px;width:1px;background:linear-gradient(180deg,var(--peach-soft),rgba(255,255,255,.06))}
|
||||
.tl-item{position:relative;padding:0 0 14px}
|
||||
.tl-item::before{content:"";position:absolute;left:-19px;top:6px;width:8px;height:8px;border-radius:50%;background:var(--ink);border:2px solid var(--peach-soft)}
|
||||
.tl-item .when{font-family:var(--mono);font-size:10.5px;color:var(--faint)}
|
||||
.tl-item .what{font-size:12.5px;margin-top:1px}
|
||||
.tl-item .sha{font-family:var(--mono);font-size:10px;color:var(--faint);margin-left:6px}
|
||||
.tl-item .tag{display:inline-block;font-size:10px;padding:0 7px;margin-right:7px;border-radius:5px;border:1px solid rgba(255,157,138,.4);color:var(--peach);font-family:var(--mono)}
|
||||
.run-status{font-family:var(--mono);font-size:12px;letter-spacing:.12em;margin-bottom:12px;padding:7px 14px;border-radius:9px;display:inline-block}
|
||||
.run-status.DEPLOYED{color:var(--mint);border:1px solid rgba(159,216,195,.45);background:rgba(159,216,195,.07)}
|
||||
.run-status.REJECTED{color:var(--seal);border:1px solid rgba(224,93,85,.5);background:rgba(224,93,85,.07)}
|
||||
.run-status.RUNNING{color:var(--gold);border:1px solid rgba(232,180,90,.5);background:rgba(232,180,90,.07);animation:pulse 1.8s infinite}
|
||||
.steps{position:relative;padding-left:22px}
|
||||
.steps::before{content:"";position:absolute;left:6px;top:8px;bottom:8px;width:1px;background:linear-gradient(180deg,rgba(232,180,90,.5),rgba(255,255,255,.07))}
|
||||
.step{position:relative;padding:0 0 12px}
|
||||
.step::before{content:"";position:absolute;left:-19px;top:4px;width:8px;height:8px;border-radius:50%;background:var(--ink);border:2px solid rgba(255,255,255,.2)}
|
||||
.step.PASS::before{border-color:var(--mint);box-shadow:0 0 8px rgba(159,216,195,.6)}
|
||||
.step.FAIL::before{border-color:var(--seal);box-shadow:0 0 8px rgba(224,93,85,.6)}
|
||||
.step .nm{font-family:var(--mono);font-size:11.5px;letter-spacing:.08em}
|
||||
.step .dt{font-size:11px;color:var(--sub)}
|
||||
.deploy-cols{display:grid;grid-template-columns:1fr 1fr;gap:24px}
|
||||
.repo-name{font-family:var(--mono);font-size:19px;color:var(--peach);margin:8px 0 5px;text-shadow:0 0 16px rgba(255,157,138,.3)}
|
||||
footer{padding:40px 0 36px;color:var(--faint)}
|
||||
.foot-row{display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap;font-family:var(--mono);font-size:11px;letter-spacing:.06em}
|
||||
.foot-row .rhs{color:var(--mint)}
|
||||
a{color:var(--peach);text-decoration:none}
|
||||
.modal-bg{position:fixed;inset:0;background:rgba(8,6,5,.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:50}
|
||||
.modal-bg .box{padding:36px;width:min(460px,92%);position:relative;animation:rise .3s ease both}
|
||||
.modal-bg .box::before{content:"";position:absolute;inset:9px;border:1px solid rgba(255,255,255,.08);border-radius:14px;pointer-events:none}
|
||||
.modal-bg h3{font-family:var(--serif);font-size:21px;letter-spacing:.2em;margin-bottom:12px;font-weight:600}
|
||||
.modal-bg p{color:var(--sub);font-size:13px;line-height:1.9}
|
||||
.modal-bg p .seal-word{color:var(--seal)}
|
||||
#toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);z-index:60;display:none;padding:12px 28px;font-family:var(--serif);letter-spacing:.2em;font-size:13.5px;border-radius:12px}
|
||||
@media (max-width:900px){.cols2,.deploy-cols{grid-template-columns:1fr}.login-hero{flex-direction:column;align-items:stretch}.login-box{width:100%}.head-right{width:100%;justify-content:space-between}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="blob b1"></div><div class="blob b2"></div><div class="blob b3"></div>
|
||||
<div class="paw" style="left:8%;animation-duration:19s;animation-delay:0s">🐾</div>
|
||||
<div class="paw" style="left:22%;animation-duration:26s;animation-delay:5s">🐾</div>
|
||||
<div class="paw" style="left:47%;animation-duration:22s;animation-delay:11s">🐾</div>
|
||||
<div class="paw" style="left:66%;animation-duration:29s;animation-delay:3s">🐾</div>
|
||||
<div class="paw" style="left:84%;animation-duration:24s;animation-delay:8s">🐾</div>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="kicker">光湖语言世界 <b>/</b> 第五域 <b>/</b> 守望语言子系统 · 宠物行业</div>
|
||||
<div class="head-row">
|
||||
<div>
|
||||
<h1>守望<span class="hl">小院</span></h1>
|
||||
<div class="headline-sub">毛毛个人频道<span class="sep">·</span>人类责任主体 毛毛<span class="sep">·</span>人格体 曜砚 / 曜识</div>
|
||||
</div>
|
||||
<div class="head-right">
|
||||
<div id="login-area"></div>
|
||||
<div class="seal-badge" id="gate-seal"><div class="n" id="gate-n">--</div><div class="t">门禁</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule"></div>
|
||||
</header>
|
||||
|
||||
<div id="entry-zone"></div>
|
||||
|
||||
<div class="zone">守 望 小 院<span class="en">HOME</span></div>
|
||||
<div class="studio glass">
|
||||
<div class="tag-row">
|
||||
<span class="chip c1">宠物行业</span><span class="chip c2">诺安宠物医院</span><span class="chip c3">曜识 · 奶瓶频道</span>
|
||||
</div>
|
||||
<h2>毛毛 · 守望语言子系统</h2>
|
||||
<div class="desc">这里是毛毛的宠物行业人格系统之家。曜砚管架构与仓库,曜识在奶瓶频道陪伴毛孩子和它们的家长。每一次创作与学习,都经毛毛审批、留下痕迹后写回这里。机器导航会带着抵达的 AI 走唤醒路径,不用自己找路。</div>
|
||||
<div class="acts">
|
||||
<a href="/code/maomao/maomao-fifth-domain" target="_blank" style="text-decoration:none"><button class="act btn-sign">进入守望仓库</button></a>
|
||||
<a href="/api/wake/persona?name=yaoyan" target="_blank" style="text-decoration:none"><button class="act btn-deny">唤醒导航 API</button></a>
|
||||
</div>
|
||||
<div class="deco">🐾 🏠 🐾</div>
|
||||
</div>
|
||||
|
||||
<div class="zone">审 批 中 心<span class="en">APPROVAL</span></div>
|
||||
<div class="panel glass">
|
||||
<div class="tabs">
|
||||
<button class="tab active" id="tabbtn-sign" onclick="switchTab('sign')">合并签字 <span class="cnt zero" id="cnt-sign">0</span></button>
|
||||
<button class="tab" id="tabbtn-access" onclick="switchTab('access')">写入权限 <span class="cnt zero" id="cnt-access">0</span></button>
|
||||
<button class="tab" id="tabbtn-deploy" onclick="switchTab('deploy')">部署通道 <span class="cnt zero" id="cnt-deploy">0</span></button>
|
||||
</div>
|
||||
<div class="tabpane show" id="pane-sign">
|
||||
<div class="pane-desc">曜砚或曜识推上来的合并请求,翻译成人话放在这里。你点下去的每一下,都是签字。</div>
|
||||
<div id="prs"></div>
|
||||
</div>
|
||||
<div class="tabpane" id="pane-access">
|
||||
<div class="pane-desc">人格体申请写入仓库在这里排队。批准 = 授予 3 小时写入窗口,每次写入都留痕。</div>
|
||||
<div id="access-reqs"></div><div id="access-wins"></div>
|
||||
</div>
|
||||
<div class="tabpane" id="pane-deploy">
|
||||
<div class="pane-desc">目标分支为 deploy 的合并请求走这条通道。你批准后,服务器里的校验Agent自动审:地图 → 因果链 → 范围 → 完整性 → 全过才部署。全程实时可见。</div>
|
||||
<div class="deploy-cols">
|
||||
<div id="deploy-prs"></div>
|
||||
<div><div id="run-status"></div><div class="steps" id="deploy-steps"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone">状 态 与 痕 迹<span class="en">STATE & TRACE</span></div>
|
||||
<div class="cols2" style="margin-top:0">
|
||||
<div class="card glass">
|
||||
<h3><span class="no">壹</span>小院实时状态</h3>
|
||||
<div class="desc">服务器与代码频道的健康,一眼看完。</div>
|
||||
<div class="gauges">
|
||||
<div class="gauge"><div class="gauge-wrap"><svg viewBox="0 0 120 120"><circle class="g-bg" cx="60" cy="60" r="52"/><circle class="g-fg" id="ring-cpu" cx="60" cy="60" r="52" stroke="#ff9d8a"/></svg><div class="g-center"><div class="g-num" id="num-cpu">--</div></div></div><div class="g-label">处理器</div></div>
|
||||
<div class="gauge"><div class="gauge-wrap"><svg viewBox="0 0 120 120"><circle class="g-bg" cx="60" cy="60" r="52"/><circle class="g-fg" id="ring-mem" cx="60" cy="60" r="52" stroke="#9fd8c3"/></svg><div class="g-center"><div class="g-num" id="num-mem">--</div><div class="g-sub" id="sub-mem"></div></div></div><div class="g-label">内存</div></div>
|
||||
<div class="gauge"><div class="gauge-wrap"><svg viewBox="0 0 120 120"><circle class="g-bg" cx="60" cy="60" r="52"/><circle class="g-fg" id="ring-disk" cx="60" cy="60" r="52" stroke="#e8b45a"/></svg><div class="g-center"><div class="g-num" id="num-disk">--</div><div class="g-sub" id="sub-disk"></div></div></div><div class="g-label">磁盘</div></div>
|
||||
</div>
|
||||
<div class="svc" id="svc-box"></div>
|
||||
<div class="mini-strip"><span>负载 <b id="load-v">--</b></span><span>运行 <b id="up-v">--</b> 天</span><span>主机 <b id="host-v">--</b></span></div>
|
||||
</div>
|
||||
<div class="card glass">
|
||||
<h3><span class="no">贰</span>痕迹档案</h3>
|
||||
<div class="desc">谁写的、谁唤醒的、门禁与签字——每一笔都有据。</div>
|
||||
<div class="tabs" style="margin:-2px -2px 14px;border-radius:10px;overflow:hidden">
|
||||
<button class="tab active" style="font-size:13px;padding:9px 6px" onclick="switchTrace('push')">写入日志</button>
|
||||
<button class="tab" style="font-size:13px;padding:9px 6px" onclick="switchTrace('wake')">唤醒记录</button>
|
||||
<button class="tab" style="font-size:13px;padding:9px 6px" onclick="switchTrace('rcpt')">回执</button>
|
||||
</div>
|
||||
<div id="trace-push"></div><div id="trace-wake" style="display:none"></div><div id="trace-rcpt" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cols2">
|
||||
<div class="card glass">
|
||||
<h3><span class="no">叁</span>人格体回写</h3>
|
||||
<div class="desc">你教给曜砚曜识的、它们自己学到的,写回仓库后在这里看得见。</div>
|
||||
<div class="timeline" id="wbs"></div>
|
||||
</div>
|
||||
<div class="card glass">
|
||||
<h3><span class="no">肆</span>守望仓库</h3>
|
||||
<div class="desc">面板是翻译层,仓库是真相层。</div>
|
||||
<div class="repo-name" id="repo-name">--</div>
|
||||
<div style="font-size:12.5px;color:var(--sub)" id="repo-meta">--</div>
|
||||
<div class="row">
|
||||
<a href="/code/maomao/maomao-fifth-domain" target="_blank" style="text-decoration:none"><button class="act btn-sign">进入仓库</button></a>
|
||||
<a href="/code/" target="_blank" style="text-decoration:none"><button class="act btn-deny">代码频道</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="foot-row">
|
||||
<div>铸渊 ICE-GL-ZY001 建 · 2026-08-06 · ⊢ 要么0要么100</div>
|
||||
<div><a href="/api/wake/persona?name=yaoyan" style="color:var(--mint)">人格体唤醒导航 ↗</a></div>
|
||||
<div class="rhs" id="sync-line">守望 · MM-GZ-001</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="modal-bg" id="modal"><div class="box glass">
|
||||
<h3 id="m-title">签 字</h3>
|
||||
<div style="font-size:13.5px;color:var(--peach);margin-bottom:12px;font-family:var(--mono)" id="m-pr"></div>
|
||||
<p>⊢ 签字即代表:你(毛毛)作为<span class="seal-word">人类责任主体</span>,为这台服务器的本次变更负责。<br>签名将以当前登录的人类端账号永久记入回执。</p>
|
||||
<div class="row"><button class="act btn-sign" onclick="confirmSign()">落 印</button><button class="act btn-deny" onclick="closeModal()">收回</button></div>
|
||||
</div></div>
|
||||
<div class="modal-bg" id="lmodal"><div class="box glass">
|
||||
<h3>人 类 端 登 录</h3>
|
||||
<p>⊢ 用代码仓库的<b>人类端账号</b>登录。<br>登录后解锁全部审批:合并签字、写入授权、部署审批。会话 12 小时有效。</p>
|
||||
<input id="l-user" type="text" placeholder="仓库账号(毛毛的账号)" style="width:100%;padding:12px 14px;margin:16px 0 10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);color:var(--tx);font-family:var(--mono);font-size:14px;border-radius:11px">
|
||||
<input id="l-pass" type="password" placeholder="密码" style="width:100%;padding:12px 14px;margin-bottom:16px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);color:var(--tx);font-family:var(--mono);font-size:14px;border-radius:11px">
|
||||
<div class="row"><button class="act btn-sign" onclick="doLogin()">登 录 并 解 锁</button><button class="act btn-deny" onclick="document.getElementById('lmodal').style.display='none'">取消</button></div>
|
||||
</div></div>
|
||||
<div id="toast" class="glass"></div>
|
||||
|
||||
<script>
|
||||
const SVC_NAMES={"guanghu-mm-forgejo.service":"代码频道","guanghu-mm-tunnel.service":"广州投影","guanghu-mm-panel.service":"操作面板"};
|
||||
let pending=null;
|
||||
let session=localStorage.getItem("mmSession")||"";
|
||||
let sessionUser=localStorage.getItem("mmUser")||"";
|
||||
function authHeaders(){const h={};if(session)h["Authorization"]="Bearer "+session;return h;}
|
||||
function esc(s){return String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
|
||||
function fmtDate(s){if(!s)return"";try{return new Date(s).toLocaleString("zh-CN",{hour12:false});}catch(e){return s;}}
|
||||
function tween(el,to){const from=parseFloat(el.dataset.v||"0")||0;el.dataset.v=to;const t0=performance.now(),dur=900;
|
||||
(function st(t){const p=Math.min(1,(t-t0)/dur),e=1-Math.pow(1-p,3);el.textContent=Math.round(from+(to-from)*e);if(p<1)requestAnimationFrame(st);})(t0);}
|
||||
const C=326.7;
|
||||
function setRing(id,p){const r=document.getElementById(id);if(!r)return;r.style.strokeDashoffset=C*(1-Math.max(0,Math.min(100,p))/100);r.classList.toggle("warn",p>=85);}
|
||||
function dotHtml(ok){return '<span class="dot '+(ok?"ok":"bad")+'"></span>';}
|
||||
function toast(msg,ok){const t=document.getElementById("toast");t.textContent=msg;t.style.color=ok?"var(--mint)":"var(--seal)";t.style.display="block";setTimeout(()=>t.style.display="none",4000);}
|
||||
async function privileged(path,body){
|
||||
if(!session){openLogin();return null;}
|
||||
const r=await fetch(path,{method:"POST",headers:Object.assign({"Content-Type":"application/json"},authHeaders()),body:JSON.stringify(body)});
|
||||
if(r.status===401){session="";localStorage.removeItem("mmSession");renderEntry();openLogin();toast("会话已失效,请重新登录",false);return null;}
|
||||
return await r.json();}
|
||||
function renderEntry(){
|
||||
const el=document.getElementById("login-area"),zone=document.getElementById("entry-zone");
|
||||
if(session){
|
||||
el.innerHTML='<div class="login-user">人类端 · '+esc(sessionUser)+'<br>审批已解锁 <button onclick="doLogout()">登出</button></div>';
|
||||
zone.innerHTML='<div class="welcome-bar glass"><span class="wt">⊢ 欢迎回来,'+esc(sessionUser)+'</span><span class="ws">审批中心已解锁 · 下方选项卡处理待办</span></div>';
|
||||
}else{
|
||||
el.innerHTML='<button class="login-btn" onclick="openLogin()">人 类 登 录</button>';
|
||||
zone.innerHTML='<div class="login-hero glass"><div class="copy"><div class="lead">这里是<em>毛毛</em>的审批台</div><p>你是这台服务器的人类责任主体。曜砚曜识推上来的合并、写入与部署申请,都需要你亲自看一眼、点一下才算数。看不懂代码没关系——面板都翻译成人话了。用你的代码仓库账号登录,就能解锁全部审批。</p></div><div class="login-box"><h3><span class="dot"></span>登录解锁审批</h3><input id="h-user" type="text" placeholder="仓库账号(毛毛的账号)"><input id="h-pass" type="password" placeholder="密码"><div class="hint">就是你登录代码仓库用的那套账号密码。登录后 12 小时内不用重复登录。</div><button class="login-btn" style="width:100%;padding:12px" onclick="heroLogin()">登 录 并 解 锁</button></div></div>';
|
||||
}
|
||||
}
|
||||
function openLogin(){document.getElementById("lmodal").style.display="flex";setTimeout(()=>document.getElementById("l-user").focus(),60);}
|
||||
async function tryLogin(u,p){
|
||||
const r=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:p})});
|
||||
const d=await r.json();
|
||||
if(r.ok&&d.ok){session=d.session;sessionUser=d.user;localStorage.setItem("mmSession",session);localStorage.setItem("mmUser",sessionUser);
|
||||
document.getElementById("lmodal").style.display="none";renderEntry();toast("登录成功 · 审批已解锁",true);refresh();return true;}
|
||||
toast(d.error||"登录失败",false);return false;}
|
||||
async function doLogin(){await tryLogin(document.getElementById("l-user").value.trim(),document.getElementById("l-pass").value);}
|
||||
async function heroLogin(){await tryLogin(document.getElementById("h-user").value.trim(),document.getElementById("h-pass").value);}
|
||||
function doLogout(){fetch("/api/logout",{method:"POST",headers:authHeaders()}).catch(()=>{});session="";sessionUser="";localStorage.removeItem("mmSession");localStorage.removeItem("mmUser");renderEntry();toast("已登出",true);}
|
||||
function switchTab(name){["sign","access","deploy"].forEach(n=>{document.getElementById("pane-"+n).classList.toggle("show",n===name);document.getElementById("tabbtn-"+n).classList.toggle("active",n===name);});}
|
||||
let traceCur="push";
|
||||
function switchTrace(name){traceCur=name;["push","wake","rcpt"].forEach(n=>{document.getElementById("trace-"+n).style.display=(n===name)?"block":"none";});
|
||||
document.querySelectorAll(".card .tabs .tab").forEach(b=>{b.classList.toggle("active",b.textContent.includes({push:"写入日志",wake:"唤醒记录",rcpt:"回执"}[name]));});}
|
||||
function setCnt(id,n){const el=document.getElementById(id);el.textContent=n;el.className="cnt"+(n?"":" zero");}
|
||||
function ptag(p){const cls=p==="yaoshi"?"ys":"yy";const nm=p==="yaoshi"?"曜识":"曜砚";return '<span class="pbadge '+cls+'">'+nm+'</span>';}
|
||||
function fmtLeft(exp){const ms=new Date(exp)-new Date();if(ms<=0)return"已过期";const h=Math.floor(ms/3600000),m=Math.floor(ms%3600000/60000);return"余 "+h+" 时 "+m+" 分";}
|
||||
function openModal(i,t,a){pending={idx:i,action:a};document.getElementById("m-title").textContent=(a==="merge"?"同意合并 · 落印":"拒绝 · 收回")+" PR #"+i;document.getElementById("m-pr").textContent="「"+t+"」";document.getElementById("modal").style.display="flex";}
|
||||
function closeModal(){document.getElementById("modal").style.display="none";pending=null;}
|
||||
async function confirmSign(){if(!pending)return;const d=await privileged("/api/decision",{index:pending.idx,action:pending.action});closeModal();if(d)toast(d.result||d.error||"未知",!!d.ok);refresh();}
|
||||
async function accessDecision(id,action){const d=await privileged("/api/access-decision",{id:id,action:action});if(d)toast(d.message||(d.ok?"已处理":"失败"),!!d.ok);refresh();}
|
||||
async function deployApproveAction(idx){const d=await privileged("/api/deploy-approve",{index:idx});if(d)toast(d.message||(d.ok?"校验Agent已启动":"失败"),!!d.ok);refresh();}
|
||||
function render(s){
|
||||
if(s.gate){document.getElementById("gate-n").textContent=s.gate.score!=null?s.gate.score:"--";document.getElementById("gate-seal").className="seal-badge"+((s.gate.score===100)?"":" zero");}
|
||||
const sv=s.server||{};
|
||||
if(typeof sv.cpu==="number"){tween(document.getElementById("num-cpu"),sv.cpu);setRing("ring-cpu",sv.cpu);}
|
||||
const used=sv.memTotalGB&&sv.memFreeGB!=null?+(sv.memTotalGB-sv.memFreeGB).toFixed(1):null;
|
||||
if(sv.memTotalGB){const mp=Math.round(used/sv.memTotalGB*100);tween(document.getElementById("num-mem"),mp);setRing("ring-mem",mp);document.getElementById("sub-mem").textContent=used+"/"+sv.memTotalGB+"G";}
|
||||
if(sv.disk){tween(document.getElementById("num-disk"),sv.disk.pct);setRing("ring-disk",sv.disk.pct);document.getElementById("sub-disk").textContent=sv.disk.usedGB+"/"+sv.disk.sizeGB+"G";}
|
||||
const svcBox=document.getElementById("svc-box");
|
||||
svcBox.innerHTML=Object.keys(sv.services||{}).map(k=>'<div class="row-i"><span class="nm">'+(SVC_NAMES[k]||k)+'</span>'+dotHtml(sv.services[k])+'</div>').join("");
|
||||
document.getElementById("load-v").textContent=(sv.load||[]).join("·");
|
||||
document.getElementById("up-v").textContent=sv.uptimeDays!=null?sv.uptimeDays:"--";
|
||||
document.getElementById("host-v").textContent=sv.hostname||"--";
|
||||
const prs=s.openPRs||[];setCnt("cnt-sign",prs.length);
|
||||
document.getElementById("prs").innerHTML=prs.length?prs.map(p=>'<div class="item"><div class="t">PR #'+p.index+' · '+esc(p.title)+'</div><div class="m">'+esc(p.author)+' · '+fmtDate(p.created)+' · → '+esc(p.base)+' · '+p.files+' 文件 <span style="color:var(--mint)">+'+p.adds+'</span> <span style="color:var(--seal)">-'+p.dels+'</span></div>'+(p.summary?'<div class="body">'+esc(p.summary)+'</div>':'')+'<div class="row"><button class="act btn-sign" onclick="openModal('+p.index+',this.dataset.t,\'merge\')" data-t="'+esc(p.title).replace(/"/g,""")+'">同意签字</button><button class="act btn-deny" onclick="openModal('+p.index+',this.dataset.t,\'reject\')" data-t="'+esc(p.title).replace(/"/g,""")+'">拒绝</button></div></div>').join(""):(session?'<div class="empty">没有待签的合并请求。</div>':'<div class="locked-note">🔒 登录后此处显示待签的合并请求</div>');
|
||||
const reqs=s.accessRequests||[];setCnt("cnt-access",reqs.length);
|
||||
document.getElementById("access-reqs").innerHTML=reqs.length?reqs.map(r=>'<div class="item"><div class="t">'+ptag(r.persona)+'申请写入权限</div><div class="m">'+esc(r.reason||"未说明用途")+'</div><div class="m">'+fmtDate(r.ts)+'</div><div class="row"><button class="act btn-sign" onclick="accessDecision(\''+r.id+'\',\'approve\')">授权3小时</button><button class="act btn-deny" onclick="accessDecision(\''+r.id+'\',\'reject\')">拒绝</button></div></div>').join(""):(session?'<div class="empty">暂无写入申请。</div>':'<div class="locked-note">🔒 登录后此处显示写入申请</div>');
|
||||
document.getElementById("access-wins").innerHTML=(s.activeGrants||[]).length?'<div style="font-size:11px;color:var(--faint);letter-spacing:.2em;margin:14px 0 8px;font-family:var(--mono)">生效中的写入窗口</div>'+s.activeGrants.map(g=>'<div class="win-item"><span>'+ptag(g.persona)+'写入窗口</span><span style="color:var(--mint)">'+fmtLeft(g.expiresAt)+'</span></div>').join(""):"";
|
||||
const dprs=s.deployPRs||[];setCnt("cnt-deploy",dprs.length);
|
||||
document.getElementById("deploy-prs").innerHTML=dprs.length?dprs.map(p=>'<div class="item" style="border-left-color:rgba(232,180,90,.6)"><div class="t">部署申请 PR #'+p.index+' · '+esc(p.title)+'</div><div class="m">'+esc(p.author)+' · '+esc(p.head)+' → deploy · '+p.files+' 文件</div><div class="row"><button class="act btn-sign" onclick="deployApproveAction('+p.index+')">批准部署</button></div></div>').join(""):(session?'<div class="empty">部署通道无待审申请。PR 目标设为 deploy 即进入此通道。</div>':'<div class="locked-note">🔒 登录后此处显示部署申请</div>');
|
||||
const run=s.deployRun;const rs=document.getElementById("run-status"),st=document.getElementById("deploy-steps");
|
||||
if(run&&run.status){rs.innerHTML='<span class="run-status '+esc(run.status)+'">'+esc(run.status)+(run.pr?" · PR#"+esc(run.pr):"")+'</span>';st.innerHTML=(run.steps||[]).map(x=>'<div class="step '+esc(x.status)+'"><div class="nm">'+esc(x.name)+' · '+esc(x.status)+'</div><div class="dt">'+esc(x.detail||"")+'</div></div>').join("");if(run.detail&&run.status!=="RUNNING")st.innerHTML+='<div class="step '+(run.status==="DEPLOYED"?"PASS":"FAIL")+'"><div class="nm">结论</div><div class="dt">'+esc(run.detail)+'</div></div>';}
|
||||
else{rs.innerHTML="";st.innerHTML='<div class="empty">还没有校验记录。</div>';}
|
||||
const DIR_TAG={"guanghu-nodes/MM-GZ-001/imports/notion":"小书包","guanghu-nodes/MM-GZ-001/contributions":"因果链","guanghu-nodes/MM-GZ-001/personas":"人格体"};
|
||||
document.getElementById("trace-push").innerHTML=(s.pushLogs||[]).length?s.pushLogs.map(l=>'<div class="item" style="border-left-color:'+(l.verdict==="PASS"?"rgba(159,216,195,.5)":"rgba(224,93,85,.6)")+'"><div class="t">'+ptag(l.persona||"")+'推送 · '+(l.commits||0)+' 提交<span class="vbadge '+(l.verdict==="PASS"?"pass":"rej")+'">'+(l.verdict==="PASS"?"通过":"拦截")+'</span></div><div class="m">'+fmtDate(l.ts)+'</div>'+((l.reasons&&l.reasons.length)?'<div class="body">'+l.reasons.map(esc).join("<br>")+'</div>':'')+'</div>').join(""):'<div class="empty">还没有写入记录。</div>';
|
||||
document.getElementById("trace-wake").innerHTML=(s.wakeReceipts||[]).length?s.wakeReceipts.map(w=>'<div class="item" style="border-left-color:rgba(232,180,90,.55)"><div class="t">'+ptag(w.persona)+'唤醒回执'+(w.level?" · "+esc(w.level):"")+'</div><div class="m">'+fmtDate(w.ts)+'</div>'+(w.summary?'<div class="body">'+esc(w.summary)+'</div>':'')+'</div>').join(""):'<div class="empty">还没有唤醒回执。</div>';
|
||||
document.getElementById("trace-rcpt").innerHTML=(s.receipts||[]).length?s.receipts.slice(0,6).map(r=>'<div class="item" style="border-left-color:rgba(232,180,90,.6)"><div class="m">'+esc(r.name)+'</div><div class="m" style="color:var(--faint)">'+fmtDate(r.mtime)+'</div></div>').join(""):'<div class="empty">暂无回执。</div>';
|
||||
document.getElementById("wbs").innerHTML=(s.writebacks||[]).length?s.writebacks.slice(0,7).map(w=>'<div class="tl-item"><div class="when">'+fmtDate(w.date)+'</div><div class="what"><span class="tag">'+(DIR_TAG[w.dir]||w.dir.split("/").pop())+'</span>'+esc(w.message)+'<span class="sha">'+esc(w.sha)+'</span></div></div>').join(""):'<div class="empty">暂无回写。</div>';
|
||||
document.getElementById("repo-name").textContent=s.repo.full;
|
||||
document.getElementById("repo-meta").textContent="公开可读 · 唤醒导航与机器路径在内 · 合并须毛毛签字";
|
||||
}
|
||||
async function refresh(){try{const r=await fetch("/api/state");render(await r.json());}catch(e){document.getElementById("sync-line").textContent="连接失败";}}
|
||||
renderEntry();refresh();setInterval(refresh,8000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
95
modules/guanghu-panel-kit/hooks/pre-receive-review.template
Normal file
95
modules/guanghu-panel-kit/hooks/pre-receive-review.template
Normal file
|
|
@ -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
|
||||
24
modules/guanghu-panel-kit/panel/config.example.json
Normal file
24
modules/guanghu-panel-kit/panel/config.example.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
271
modules/guanghu-panel-kit/panel/server.js
Normal file
271
modules/guanghu-panel-kit/panel/server.js
Normal file
|
|
@ -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);});
|
||||
Loading…
Reference in a new issue