feat: connect Guangzhou front door to JD ops center

This commit is contained in:
冰朔 2026-07-17 13:14:31 +08:00
parent 45c012d6e9
commit 52ac573a28
21 changed files with 640 additions and 1 deletions

View File

@ -126,6 +126,11 @@ GLS-NODE-0001=gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp
LIGHTHOUSE-NODE-ARCHITECTURE=gls/GLS-NODE-0001-LIGHTHOUSE-NODE-ARCHITECTURE.hdlp
LIGHTHOUSE-NODE-BOOTSTRAP=server-tools/light-lake-node-bootstrap/README.md # Ubuntu 22.04 统一节点初始化模板
JD-FD-PRIMARY-BOOTSTRAP-RECEIPT=deployment/receipts/JD-FD-PRIMARY-BOOTSTRAP-20260717.json
GUANGHULAB-FRONT-DOOR=server-tools/guanghulab-front-door/index.html # 广州备案域名轻量前门
JD-APP-HUB=server-tools/jd-app-hub/README.md # 京东主节点首个 Web 应用与广州受限隧道
PERSONAL-NODE-PROBE=server-tools/personal-node-probe/README.md # JD-OPS-CENTER 五分钟只读心跳
BS-GZ-006=deployment/nodes/BS-GZ-006.json # 广州个人节点 · 已接入 JD-OPS-CENTER
BS-GZ-006-CONNECTION-RECEIPT=deployment/receipts/BS-GZ-006-JD-OPS-CONNECTION-20260717.json
NODE-LIGHTHOUSE-WORKORDER=tcs-core/workorders/GLW-WO-20260714-NODE-LIGHTHOUSE-001.hdlp
HLP-LIGHTHOUSE-001=server-tools/enterprise-lighthouse/README.md
ENTERPRISE-LIGHTHOUSE-SERVICE=server-tools/enterprise-lighthouse/lighthouse.py

View File

@ -15,6 +15,21 @@ Nginx: DISABLED_UNTIL_CONFIGURED
下一阶段: TX-PERSONAL-NODE-01 只读样板接入;其余五台不得跳过样板验证批量接入
```
## 个人服务器接入实况 · 2026-07-17
```text
BS-GZ-006广州备案前门: CONNECTED
登记来源: bingshuo/guanghulabREPO-002 历史归档,只读)
当前运行事实源: deployment/nodes/BS-GZ-006.json
JD → 广州管理连接: ONLINE · 独立钥匙 · 广州端限制来源为 JD-OPS-CENTER
广州 → JD Web 隧道: ONLINE · 仅准转发 JD 本机 8088
周期心跳: ONLINE · 每 5 分钟 · 固定只读命令
公开入口: https://guanghulab.com/
京东应用入口: https://guanghulab.com/jd/
历史明文 Gatekeeper token: ROTATED_AND_INVALID
剩余个人节点: 5
```
## 版本
```

View File

@ -0,0 +1,38 @@
{
"schema": "guanghu.managed-node/v1",
"node_id": "BS-GZ-006",
"display_name": "广州备案前门节点",
"owner": "ICE-GL∞",
"provider": "tencent_cloud",
"region": "guangzhou",
"roles": [
"personal-managed-node",
"domain-front-door",
"legacy-service-host"
],
"upstream": "JD-OPS-CENTER",
"resources": {
"cpu_cores": 2,
"memory_gib": 2,
"root_disk_gib": 50
},
"public_surface": {
"domain": "guanghulab.com",
"front_door": "https://guanghulab.com/",
"jd_app_hub": "https://guanghulab.com/jd/",
"icp_record": "陕ICP备2025071211号-1"
},
"connectivity": {
"admin_key_id": "jd_ops_to_bs_gz_006_admin",
"web_proxy_key_id": "bs_gz_006_to_jd_web_proxy",
"heartbeat": "five-minute-fixed-read-only-probe",
"credential_material": "private_only"
},
"source_history": {
"repository": "bingshuo/guanghulab",
"path": "brain/fifth-domain/zero-point/zhuyuan/cloud-compute-pool/bingshuo/BS-GZ-006.hdlp",
"repository_role": "locked_historical_archive",
"runtime_facts_refreshed_at": "2026-07-17"
},
"status": "connected"
}

View File

@ -0,0 +1,41 @@
{
"schema": "guanghu.deployment-receipt/v1",
"receipt_id": "BS-GZ-006-JD-OPS-CONNECTION-20260717",
"recorded_at": "2026-07-17T05:12:00Z",
"node_id": "BS-GZ-006",
"upstream": "JD-OPS-CENTER",
"source": {
"historical_registry": "bingshuo/guanghulab",
"current_repository": "bingshuo/fifth-domain",
"front_door_source": "server-tools/guanghulab-front-door",
"jd_hub_source": "server-tools/jd-app-hub",
"probe_source": "server-tools/personal-node-probe"
},
"verification": {
"jd_to_node_ssh": "passed",
"jd_to_node_key_scope": "dedicated_node_key",
"node_to_jd_web_tunnel": "active",
"tunnel_destination_scope": "jd_loopback_app_hub_only",
"front_door_https": 200,
"jd_hub_https": 200,
"jd_hub_dynamic_status": "passed",
"nginx_config_test": "passed",
"nginx_service": "active",
"heartbeat_timer": "active_every_five_minutes",
"icp_link": "https://beian.miit.gov.cn/",
"icp_record": "陕ICP备2025071211号-1",
"historical_gatekeeper_token": "rotated_and_rejected_with_401",
"current_test_count": 27
},
"rollback": {
"nginx_config_backup": "guanghulab.pre-jd-front-door.20260717-1305",
"authorized_keys_backup": "authorized_keys.pre-jd-ops-20260717",
"legacy_gatekeeper_secret_backup": "secret.pre-jd-ops-20260717"
},
"boundaries": {
"contains_ip": false,
"contains_credentials": false,
"guanghulab_repo_mutated": false,
"remaining_personal_nodes": 5
}
}

View File

@ -0,0 +1,106 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="光湖语言世界国内入口。广州备案前门连接京东云第五域主节点与光湖各项服务。">
<title>光湖 · 国内入口</title>
<link rel="stylesheet" href="styles.css?v=20260717">
</head>
<body>
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<header class="topbar">
<a class="brand" href="/" aria-label="光湖首页">
<svg class="brand-mark" viewBox="0 0 64 40" role="img" aria-label="光湖标志">
<path d="M31.8 20C25.4 8.5 18 5.8 11.7 9.3 2.9 14.2 5.8 29.5 16.3 29.5c6.6 0 11.8-5.4 15.5-9.5Z"/>
<path d="M32.2 20C38.6 8.5 46 5.8 52.3 9.3c8.8 4.9 5.9 20.2-4.6 20.2-6.6 0-11.8-5.4-15.5-9.5Z"/>
</svg>
<span>光湖</span>
</a>
<div class="node-pill"><i></i> 广州备案前门 · 在线</div>
</header>
<main>
<section class="hero">
<p class="eyebrow">GUANGHU · DOMESTIC GATEWAY</p>
<h1>从这里,进入<br><span>光湖语言世界</span></h1>
<p class="lede">广州节点保留备案域名与轻量入口,应用和计算逐步迁往京东云第五域主节点。入口稳定,后端可以继续生长。</p>
<div class="hero-actions">
<a class="primary-button" href="/jd/">进入京东云主节点 <span></span></a>
<a class="text-button" href="https://guanghubingshuo.com/">光湖世界主入口 <span></span></a>
</div>
</section>
<section class="route-section" aria-labelledby="routes-title">
<div class="section-heading">
<div>
<p class="eyebrow">ROUTES</p>
<h2 id="routes-title">选择一条路径</h2>
</div>
<p>页面在广州,重计算在京东。每张卡片都是一个真实入口。</p>
</div>
<div class="card-grid">
<a class="route-card featured" href="/jd/">
<span class="card-number">01</span>
<div class="card-icon"></div>
<div class="card-copy">
<p class="card-kicker">COMPUTE · BEIJING</p>
<h3>京东云主节点</h3>
<p>进入 JD-FD-PRIMARY。新的应用、迁移任务与服务将从这里运行。</p>
</div>
<span class="card-arrow"></span>
</a>
<a class="route-card" href="https://guanghubingshuo.com/">
<span class="card-number">02</span>
<div class="card-icon"></div>
<div class="card-copy">
<p class="card-kicker">WORLD · ENTRY</p>
<h3>光湖世界主入口</h3>
<p>从世界入口进入第五域、永恒湖心与光之湖语言人格系统。</p>
</div>
<span class="card-arrow"></span>
</a>
<a class="route-card" href="https://guanghubingshuo.com/code/">
<span class="card-number">03</span>
<div class="card-icon"></div>
<div class="card-copy">
<p class="card-kicker">SOURCE · TRUTH</p>
<h3>代码仓库</h3>
<p>进入在线事实源查看第五域、Guanghulab 与其他研发仓库。</p>
</div>
<span class="card-arrow"></span>
</a>
<a class="route-card" href="https://guanghubingshuo.com/global-search/">
<span class="card-number">04</span>
<div class="card-icon"></div>
<div class="card-copy">
<p class="card-kicker">SEARCH · ROUTER</p>
<h3>全局路径检索</h3>
<p>跨仓库检索编号、人格路径、系统文件与当前路由。</p>
</div>
<span class="card-arrow"></span>
</a>
</div>
</section>
<section class="architecture-strip" aria-label="节点分工">
<div><span>广州 · BS-GZ-006</span><strong>备案域名 / HTTPS / 前门展示</strong></div>
<b></b>
<div><span>北京 · JD-FD-PRIMARY</span><strong>应用运行 / 计算 / 调度中心</strong></div>
</section>
</main>
<footer>
<div class="footer-brand">光湖语言世界 · 国内备案入口</div>
<div class="footer-links">
<span>© 2026 Guanghu Lab</span>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">陕ICP备2025071211号-1</a>
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,103 @@
:root {
color-scheme: dark;
--bg: #071113;
--panel: rgba(15, 31, 33, .82);
--panel-strong: #10292a;
--line: rgba(186, 238, 223, .15);
--text: #edf8f4;
--muted: #9fb4ae;
--lake: #7de0c7;
--lake-deep: #2d9f8d;
--warm: #f0bd78;
--shadow: 0 24px 70px rgba(0, 0, 0, .3);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
min-height: 100vh;
overflow-x: hidden;
background:
radial-gradient(circle at 12% 18%, rgba(45, 159, 141, .14), transparent 30rem),
radial-gradient(circle at 85% 40%, rgba(240, 189, 120, .08), transparent 26rem),
var(--bg);
color: var(--text);
font-family: Inter, "SF Pro Display", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
}
a { color: inherit; }
.ambient { position: fixed; z-index: -1; border-radius: 999px; filter: blur(90px); opacity: .3; }
.ambient-one { width: 24rem; height: 24rem; left: -10rem; top: 34rem; background: #1f8d7d; }
.ambient-two { width: 20rem; height: 20rem; right: -8rem; top: 8rem; background: #8b6334; }
.topbar, main, footer { width: min(1180px, calc(100% - 40px)); margin-inline: auto; }
.topbar { height: 92px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--line); }
.brand { display: inline-flex; gap: 12px; align-items: center; text-decoration: none; font-size: 22px; font-weight: 750; letter-spacing: .08em; }
.brand-mark { width: 44px; fill: none; stroke: var(--lake); stroke-width: 5; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 12px rgba(125, 224, 199, .28)); }
.node-pill { display: flex; align-items: center; gap: 9px; color: var(--muted); font-size: 13px; padding: 10px 14px; border: 1px solid var(--line); border-radius: 99px; background: rgba(10, 22, 23, .7); }
.node-pill i { width: 7px; height: 7px; border-radius: 50%; background: var(--lake); box-shadow: 0 0 12px var(--lake); }
.hero { padding: 108px 0 92px; max-width: 880px; }
.eyebrow { margin: 0 0 18px; color: var(--lake); font-size: 12px; font-weight: 800; letter-spacing: .22em; }
h1 { margin: 0; max-width: 850px; font-size: clamp(50px, 7.5vw, 92px); line-height: .98; letter-spacing: -.055em; font-weight: 760; }
h1 span { color: transparent; background: linear-gradient(110deg, var(--lake), #bcebdc 52%, var(--warm)); background-clip: text; -webkit-background-clip: text; }
.lede { max-width: 680px; margin: 34px 0 0; color: var(--muted); font-size: 18px; line-height: 1.85; }
.hero-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 24px; margin-top: 42px; }
.primary-button, .text-button { display: inline-flex; align-items: center; gap: 18px; text-decoration: none; font-weight: 720; }
.primary-button { padding: 16px 22px; border-radius: 13px; color: #051211; background: var(--lake); box-shadow: 0 14px 34px rgba(45, 159, 141, .22); }
.primary-button:hover { transform: translateY(-2px); background: #94ead5; }
.text-button { color: var(--muted); }
.text-button:hover { color: var(--text); }
.primary-button, .route-card { transition: transform .22s ease, border-color .22s ease, background .22s ease; }
.route-section { padding: 70px 0; border-top: 1px solid var(--line); }
.section-heading { display: flex; justify-content: space-between; align-items: end; gap: 30px; margin-bottom: 34px; }
.section-heading h2 { margin: 0; font-size: clamp(32px, 5vw, 52px); letter-spacing: -.04em; }
.section-heading > p { max-width: 390px; margin: 0 0 3px; color: var(--muted); line-height: 1.75; }
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.route-card { position: relative; min-height: 250px; display: grid; grid-template-columns: 54px 1fr; gap: 22px; padding: 30px; overflow: hidden; text-decoration: none; border: 1px solid var(--line); border-radius: 20px; background: var(--panel); box-shadow: var(--shadow); backdrop-filter: blur(18px); }
.route-card::after { content: ""; position: absolute; width: 11rem; height: 11rem; right: -6rem; bottom: -7rem; border-radius: 50%; background: rgba(125, 224, 199, .08); transition: transform .25s ease; }
.route-card:hover { transform: translateY(-4px); border-color: rgba(125, 224, 199, .42); background: rgba(18, 42, 42, .93); }
.route-card:hover::after { transform: scale(1.4); }
.route-card.featured { background: linear-gradient(145deg, rgba(23, 62, 58, .96), rgba(14, 34, 35, .9)); border-color: rgba(125, 224, 199, .3); }
.card-number { position: absolute; top: 23px; right: 25px; color: rgba(237, 248, 244, .23); font: 700 12px/1 ui-monospace, monospace; }
.card-icon { width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid rgba(125, 224, 199, .22); border-radius: 16px; color: var(--lake); background: rgba(125, 224, 199, .08); font-weight: 800; }
.card-copy { align-self: center; }
.card-kicker { margin: 0 0 16px; color: var(--lake); font: 700 10px/1 ui-monospace, monospace; letter-spacing: .16em; }
.card-copy h3 { margin: 0 0 13px; font-size: 25px; letter-spacing: -.025em; }
.card-copy > p:last-child { margin: 0; color: var(--muted); line-height: 1.75; font-size: 15px; }
.card-arrow { position: absolute; right: 26px; bottom: 23px; color: var(--warm); font-size: 18px; }
.architecture-strip { margin: 28px 0 88px; padding: 26px 30px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 24px; border-radius: 18px; border: 1px solid var(--line); background: rgba(8, 20, 21, .62); }
.architecture-strip div { display: flex; flex-direction: column; gap: 7px; }
.architecture-strip span { color: var(--lake); font-size: 12px; font-weight: 750; letter-spacing: .08em; }
.architecture-strip strong { font-size: 15px; font-weight: 600; }
.architecture-strip b { color: var(--warm); }
footer { min-height: 120px; display: flex; justify-content: space-between; align-items: center; gap: 28px; border-top: 1px solid var(--line); color: var(--muted); font-size: 13px; }
.footer-brand { color: var(--text); font-weight: 650; }
.footer-links { display: flex; gap: 22px; flex-wrap: wrap; justify-content: flex-end; }
.footer-links a { color: var(--muted); text-underline-offset: 4px; }
.footer-links a:hover { color: var(--lake); }
@media (max-width: 760px) {
.topbar, main, footer { width: min(100% - 28px, 1180px); }
.topbar { height: 76px; }
.node-pill { padding: 8px 10px; font-size: 11px; }
.hero { padding: 76px 0 70px; }
h1 { font-size: clamp(44px, 14vw, 66px); }
.lede { font-size: 16px; }
.section-heading { align-items: start; flex-direction: column; }
.card-grid { grid-template-columns: 1fr; }
.route-card { min-height: 225px; padding: 25px; grid-template-columns: 48px 1fr; gap: 18px; }
.card-icon { width: 48px; height: 48px; }
.architecture-strip { grid-template-columns: 1fr; }
.architecture-strip b { transform: rotate(90deg); justify-self: start; }
footer { padding: 28px 0; align-items: flex-start; flex-direction: column; }
.footer-links { justify-content: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
}

View File

@ -0,0 +1,31 @@
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const html = fs.readFileSync(path.join(__dirname, "..", "index.html"), "utf8");
const styles = fs.readFileSync(path.join(__dirname, "..", "styles.css"), "utf8");
test("front door keeps the legally required ICP link", () => {
assert.match(html, /陕ICP备2025071211号-1/);
assert.match(html, /https:\/\/beian\.miit\.gov\.cn\//);
});
test("front door routes compute work through the JD hub", () => {
assert.match(html, /href="\/jd\/"/);
assert.match(html, /京东云主节点/);
assert.match(html, /href="https:\/\/guanghubingshuo\.com\/code\/"/);
});
test("public page never embeds infrastructure addresses or credentials", () => {
assert.doesNotMatch(html, /(?:\d{1,3}\.){3}\d{1,3}/);
assert.doesNotMatch(html, /zy_gtw_/);
assert.doesNotMatch(html, /password|private[_ -]?key|token/i);
});
test("front door has responsive card layout", () => {
assert.match(styles, /grid-template-columns:\s*repeat\(2/);
assert.match(styles, /@media\s*\(max-width:\s*760px\)/);
});

View File

@ -0,0 +1,7 @@
# JD application hub
The first web application running on `JD-FD-PRIMARY`. It binds only to
`127.0.0.1:8088`; `BS-GZ-006` reaches it through a restricted persistent SSH
tunnel and publishes it below `https://guanghulab.com/jd/`.
No server address, credential or private key belongs in this directory.

View File

@ -0,0 +1,14 @@
fetch("api/status", { cache: "no-store" })
.then((response) => response.ok ? response.json() : Promise.reject(new Error("status unavailable")))
.then((status) => {
const live = document.querySelector("#live-status");
const node = document.querySelector("#node-state");
live.textContent = "运行正常";
live.classList.add("ok");
node.textContent = `${status.node_id} · 在线`;
node.classList.add("ok");
})
.catch(() => {
document.querySelector("#live-status").textContent = "状态暂不可用";
document.querySelector("#node-state").textContent = "节点状态暂不可用";
});

View File

@ -0,0 +1,19 @@
[Unit]
Description=Guanghu BS-GZ-006 to JD application hub tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/ssh -NT -F /etc/guanghu/jd-web-tunnel-ssh-config jd-web-target
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=read-only
ProtectSystem=strict
ReadOnlyPaths=/etc/guanghu/jd-web-tunnel-ssh-config /etc/guanghu/secrets/ssh/bs_gz_006_to_jd_web_proxy /root/.ssh/known_hosts
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>京东云主节点 · 光湖</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main>
<nav><a href="/">← 返回光湖国内入口</a><span id="node-state">正在读取节点状态</span></nav>
<header>
<p class="eyebrow">JD-FD-PRIMARY · BEIJING</p>
<h1>京东云主节点</h1>
<p>这里是光湖第五域的国内运行节点。广州负责备案前门,这里负责应用、计算、迁移与服务器调度。</p>
</header>
<section class="status" aria-label="节点状态">
<div><span>节点</span><strong>JD-FD-PRIMARY</strong></div>
<div><span>角色</span><strong>第五域国内主节点</strong></div>
<div><span>连接</span><strong id="live-status">检查中</strong></div>
</section>
<section class="cards">
<a href="https://guanghubingshuo.com/" target="_top"><small>WORLD</small><h2>光湖世界</h2><p>进入光湖世界主入口与第五域路由。</p><b></b></a>
<a href="https://guanghubingshuo.com/code/bingshuo/fifth-domain" target="_top"><small>SOURCE</small><h2>第五域仓库</h2><p>当前系统结构、人格路径和部署回执事实源。</p><b></b></a>
<a href="https://guanghubingshuo.com/global-search/" target="_top"><small>ROUTER</small><h2>全局检索</h2><p>通过编号和关键词寻找正确的仓库路径。</p><b></b></a>
<div class="coming"><small>NEXT</small><h2>Tolaria 服务</h2><p>桌面软件相关服务迁移完成后从这里接入。</p><b>建设中</b></div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
[Unit]
Description=Guanghu JD Application Hub
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=guanghu
Group=guanghu
WorkingDirectory=/opt/guanghu/jd-app-hub
Environment=NODE_ENV=production
Environment=HUB_HOST=127.0.0.1
Environment=HUB_PORT=8088
Environment=GUANGHU_NODE_ID=JD-FD-PRIMARY
ExecStart=/usr/bin/node /opt/guanghu/jd-app-hub/server.js
Restart=always
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,46 @@
"use strict";
const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");
const os = require("node:os");
const ROOT = __dirname;
const TYPES = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8" };
function json(response, status, value) {
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
response.end(JSON.stringify(value));
}
function createApp() {
return http.createServer((request, response) => {
const url = new URL(request.url, "http://localhost");
if (request.method !== "GET") return json(response, 405, { ok: false, error: "method_not_allowed" });
if (url.pathname === "/api/status") {
return json(response, 200, {
ok: true,
node_id: process.env.GUANGHU_NODE_ID || "JD-FD-PRIMARY",
role: "fifth-domain-primary",
service: "jd-app-hub",
uptime_seconds: Math.floor(process.uptime()),
load_1m: Number(os.loadavg()[0].toFixed(2)),
memory_used_percent: Number((((os.totalmem() - os.freemem()) / os.totalmem()) * 100).toFixed(1)),
timestamp: new Date().toISOString()
});
}
const relative = url.pathname === "/" ? "index.html" : url.pathname.replace(/^\/+/, "");
if (!/^(index\.html|styles\.css|app\.js)$/.test(relative)) return json(response, 404, { ok: false, error: "not_found" });
const file = path.join(ROOT, relative);
response.writeHead(200, { "Content-Type": TYPES[path.extname(file)] || "application/octet-stream", "Cache-Control": relative === "index.html" ? "no-cache" : "public, max-age=3600" });
fs.createReadStream(file).pipe(response);
});
}
if (require.main === module) {
const host = process.env.HUB_HOST || "127.0.0.1";
const port = Number(process.env.HUB_PORT || 8088);
createApp().listen(port, host, () => console.log(`JD app hub listening on ${host}:${port}`));
}
module.exports = { createApp };

View File

@ -0,0 +1,26 @@
:root { color-scheme: dark; --bg:#081112; --panel:#102526; --line:rgba(174,233,216,.16); --text:#eff9f6; --muted:#9cafaa; --lake:#78dfc4; --warm:#efbd79; }
* { box-sizing:border-box; }
body { margin:0; min-height:100vh; color:var(--text); background:radial-gradient(circle at 75% 0,rgba(62,154,136,.18),transparent 32rem),var(--bg); font-family:Inter,"PingFang SC",system-ui,sans-serif; }
main { width:min(1080px,calc(100% - 36px)); margin:auto; padding-bottom:70px; }
nav { height:84px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--line); color:var(--muted); font-size:13px; }
nav a { color:var(--text); text-decoration:none; }
header { padding:90px 0 58px; max-width:780px; }
.eyebrow { color:var(--lake); font:800 11px/1 ui-monospace,monospace; letter-spacing:.18em; }
h1 { margin:20px 0; font-size:clamp(48px,8vw,86px); line-height:1; letter-spacing:-.055em; }
header>p:last-child { max-width:700px; color:var(--muted); font-size:18px; line-height:1.8; }
.status { display:grid; grid-template-columns:repeat(3,1fr); border:1px solid var(--line); border-radius:18px; overflow:hidden; background:rgba(15,35,36,.72); }
.status div { padding:22px; display:flex; flex-direction:column; gap:8px; border-right:1px solid var(--line); }
.status div:last-child { border:0; }
.status span { color:var(--muted); font-size:12px; }
.status strong { font-size:15px; }
.cards { display:grid; grid-template-columns:repeat(2,1fr); gap:16px; padding-top:30px; }
.cards>a,.cards>div { position:relative; min-height:210px; padding:28px; border:1px solid var(--line); border-radius:18px; background:var(--panel); text-decoration:none; color:var(--text); }
.cards>a:hover { border-color:rgba(120,223,196,.48); transform:translateY(-3px); }
.cards>a { transition:.2s ease; }
.cards small { color:var(--lake); font-weight:800; letter-spacing:.16em; }
.cards h2 { margin:34px 0 10px; font-size:25px; }
.cards p { margin:0; color:var(--muted); line-height:1.7; }
.cards b { position:absolute; right:25px; top:25px; color:var(--warm); font-size:13px; }
.coming { opacity:.62; }
#live-status.ok,#node-state.ok { color:var(--lake); }
@media(max-width:700px){ nav{height:70px}.status,.cards{grid-template-columns:1fr}.status div{border-right:0;border-bottom:1px solid var(--line)}header{padding-top:65px}.cards>a,.cards>div{min-height:190px} }

View File

@ -0,0 +1,37 @@
"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const { createApp } = require("../server");
const fs = require("node:fs");
const path = require("node:path");
test("JD hub exposes a minimal non-secret status response", async (t) => {
const server = createApp().listen(0, "127.0.0.1");
t.after(() => server.close());
await new Promise((resolve) => server.once("listening", resolve));
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/api/status`);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.ok, true);
assert.equal(body.node_id, "JD-FD-PRIMARY");
assert.equal("ip" in body, false);
assert.equal("token" in body, false);
});
test("JD hub serves its application page", async (t) => {
const server = createApp().listen(0, "127.0.0.1");
t.after(() => server.close());
await new Promise((resolve) => server.once("listening", resolve));
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/`);
assert.equal(response.status, 200);
assert.match(await response.text(), /京东云主节点/);
});
test("tunnel service keeps private addresses outside the repository", () => {
const unit = fs.readFileSync(path.join(__dirname, "..", "guanghu-jd-web-tunnel.service"), "utf8");
assert.match(unit, /\/etc\/guanghu\/jd-web-tunnel-ssh-config/);
assert.doesNotMatch(unit, /(?:\d{1,3}\.){3}\d{1,3}/);
});

View File

@ -0,0 +1,9 @@
# Personal node heartbeat probe
Runs on `JD-OPS-CENTER` and checks one registered node through a private SSH
alias. The remote action is fixed and read-only: service state, disk usage and
uptime. A JSON receipt is written under `/var/lib/guanghu/receipts/` every five
minutes.
Real addresses and SSH identity paths belong in `/etc/guanghu/nodes/ssh/config`
on the ops center, never in this repository.

View File

@ -0,0 +1,16 @@
[Unit]
Description=Guanghu personal node read-only heartbeat probe
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
EnvironmentFile=-/etc/guanghu/personal-node-probe.env
ExecStart=/usr/local/sbin/guanghu-personal-node-probe
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=read-only
ProtectSystem=strict
ReadWritePaths=/var/lib/guanghu/receipts
ReadOnlyPaths=/etc/guanghu/nodes /etc/guanghu/personal-node-probe.env

View File

@ -0,0 +1,11 @@
[Unit]
Description=Probe the first Guanghu personal node every five minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
Persistent=true
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
NODE_ID=${NODE_ID:-BS-GZ-006}
NODE_ALIAS=${NODE_ALIAS:-bs-gz-006}
SSH_CONFIG=${SSH_CONFIG:-/etc/guanghu/nodes/ssh/config}
RECEIPT_FILE=${RECEIPT_FILE:-/var/lib/guanghu/receipts/BS-GZ-006-latest.json}
FRONT_DOOR_URL=${FRONT_DOOR_URL:-https://guanghulab.com/}
APP_HUB_URL=${APP_HUB_URL:-https://guanghulab.com/jd/api/status}
remote=$(ssh -F "$SSH_CONFIG" -o BatchMode=yes "$NODE_ALIAS" \
"set -e; jq -n \
--arg hostname \"\$(hostname)\" \
--arg nginx \"\$(systemctl is-active nginx)\" \
--arg tunnel \"\$(systemctl is-active guanghu-jd-web-tunnel.service)\" \
--arg disk_used \"\$(df --output=pcent / | tail -1 | tr -d ' ')\" \
--arg uptime_seconds \"\$(cut -d. -f1 /proc/uptime)\" \
'{hostname:\$hostname,nginx:\$nginx,tunnel:\$tunnel,disk_used:\$disk_used,uptime_seconds:(\$uptime_seconds|tonumber)}'")
front_status=$(curl -fsS -o /dev/null -w '%{http_code}' "$FRONT_DOOR_URL")
hub=$(curl -fsS "$APP_HUB_URL")
tmp=$(mktemp "${RECEIPT_FILE}.XXXXXX")
trap 'rm -f "$tmp"' EXIT
jq -n \
--arg node_id "$NODE_ID" \
--arg checked_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg front_status "$front_status" \
--argjson remote "$remote" \
--argjson hub "$hub" \
'{schema:"guanghu.node-heartbeat/v1",ok:true,node_id:$node_id,checked_at:$checked_at,front_door_http:($front_status|tonumber),remote:$remote,jd_hub:{ok:$hub.ok,node_id:$hub.node_id,service:$hub.service}}' \
> "$tmp"
install -m 0640 -o root -g guanghu "$tmp" "$RECEIPT_FILE"
cat "$RECEIPT_FILE"

View File

@ -0,0 +1,24 @@
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const root = path.join(__dirname, "..");
const probe = fs.readFileSync(path.join(root, "probe.sh"), "utf8");
const timer = fs.readFileSync(path.join(root, "guanghu-personal-node-probe.timer"), "utf8");
test("probe uses a private SSH alias and fixed read-only command", () => {
assert.match(probe, /NODE_ALIAS/);
assert.match(probe, /systemctl is-active nginx/);
assert.match(probe, /df --output=pcent/);
assert.doesNotMatch(probe, /(?:\d{1,3}\.){3}\d{1,3}/);
assert.doesNotMatch(probe, /token|password|private[_ -]?key/i);
});
test("probe writes a latest receipt and runs periodically", () => {
assert.match(probe, /RECEIPT_FILE/);
assert.match(timer, /OnUnitActiveSec=5min/);
assert.match(timer, /Persistent=true/);
});

View File

@ -2,7 +2,7 @@
> **HLDP**: `HLDP://fifth-domain/zero-point/core-channel/revive-guard/LL-JD-OPS-CENTER-KEY-AND-NODE-PLAN-20260716`
>
> **状态**: PHASE_0_COMPLETE · WAITING_FOR_TX_PERSONAL_NODE_01
> **状态**: PHASE_1_FIRST_NODE_COMPLETE · FIVE_PERSONAL_NODES_REMAINING
>
> **主权者**: 冰朔 ICE-GL∞
>
@ -12,6 +12,8 @@
> **2026-07-17 实施回写**: 京东主控的 Ubuntu 22.04 统一基线、第五域镜像、受控数据目录、Gatekeeper v3.2、Docker、健康检查与回滚入口已经落地。下一步严格从一台只读样板节点开始不直接批量接入六台。
> **2026-07-17 广州节点回写**: 第一台样板采用历史编号 `BS-GZ-006`,没有另造 `TX-PERSONAL-NODE-01`。京东到广州的独立管理钥匙、广州到京东的受限 Web 隧道、五分钟只读心跳、节点清单与部署回执均已落地。`bingshuo/guanghulab` 只作为历史服务器地图,不再接收新功能提交。
---
## 0 · 冰朔不需要懂这些细节