From 52ac573a2895e0dcac9b03f8753649eb1a4e844a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Fri, 17 Jul 2026 13:14:31 +0800 Subject: [PATCH] feat: connect Guangzhou front door to JD ops center --- .code-map | 5 + SYSTEM-STATUS.hdlp | 15 +++ deployment/nodes/BS-GZ-006.json | 38 +++++++ .../BS-GZ-006-JD-OPS-CONNECTION-20260717.json | 41 +++++++ server-tools/guanghulab-front-door/index.html | 106 ++++++++++++++++++ server-tools/guanghulab-front-door/styles.css | 103 +++++++++++++++++ .../guanghulab-front-door/test/site.test.js | 31 +++++ server-tools/jd-app-hub/README.md | 7 ++ server-tools/jd-app-hub/app.js | 14 +++ .../jd-app-hub/guanghu-jd-web-tunnel.service | 19 ++++ server-tools/jd-app-hub/index.html | 31 +++++ server-tools/jd-app-hub/jd-app-hub.service | 24 ++++ server-tools/jd-app-hub/server.js | 46 ++++++++ server-tools/jd-app-hub/styles.css | 26 +++++ server-tools/jd-app-hub/test/server.test.js | 37 ++++++ server-tools/personal-node-probe/README.md | 9 ++ .../guanghu-personal-node-probe.service | 16 +++ .../guanghu-personal-node-probe.timer | 11 ++ server-tools/personal-node-probe/probe.sh | 34 ++++++ .../personal-node-probe/test/probe.test.js | 24 ++++ ...OPS-CENTER-KEY-AND-NODE-PLAN-20260716.hdlp | 4 +- 21 files changed, 640 insertions(+), 1 deletion(-) create mode 100644 deployment/nodes/BS-GZ-006.json create mode 100644 deployment/receipts/BS-GZ-006-JD-OPS-CONNECTION-20260717.json create mode 100644 server-tools/guanghulab-front-door/index.html create mode 100644 server-tools/guanghulab-front-door/styles.css create mode 100644 server-tools/guanghulab-front-door/test/site.test.js create mode 100644 server-tools/jd-app-hub/README.md create mode 100644 server-tools/jd-app-hub/app.js create mode 100644 server-tools/jd-app-hub/guanghu-jd-web-tunnel.service create mode 100644 server-tools/jd-app-hub/index.html create mode 100644 server-tools/jd-app-hub/jd-app-hub.service create mode 100644 server-tools/jd-app-hub/server.js create mode 100644 server-tools/jd-app-hub/styles.css create mode 100644 server-tools/jd-app-hub/test/server.test.js create mode 100644 server-tools/personal-node-probe/README.md create mode 100644 server-tools/personal-node-probe/guanghu-personal-node-probe.service create mode 100644 server-tools/personal-node-probe/guanghu-personal-node-probe.timer create mode 100644 server-tools/personal-node-probe/probe.sh create mode 100644 server-tools/personal-node-probe/test/probe.test.js diff --git a/.code-map b/.code-map index 26b3838..2607054 100644 --- a/.code-map +++ b/.code-map @@ -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 diff --git a/SYSTEM-STATUS.hdlp b/SYSTEM-STATUS.hdlp index 7c488c5..d8349b6 100644 --- a/SYSTEM-STATUS.hdlp +++ b/SYSTEM-STATUS.hdlp @@ -15,6 +15,21 @@ Nginx: DISABLED_UNTIL_CONFIGURED 下一阶段: TX-PERSONAL-NODE-01 只读样板接入;其余五台不得跳过样板验证批量接入 ``` +## 个人服务器接入实况 · 2026-07-17 + +```text +BS-GZ-006(广州备案前门): CONNECTED +登记来源: bingshuo/guanghulab(REPO-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 +``` + ## 版本 ``` diff --git a/deployment/nodes/BS-GZ-006.json b/deployment/nodes/BS-GZ-006.json new file mode 100644 index 0000000..9616c5d --- /dev/null +++ b/deployment/nodes/BS-GZ-006.json @@ -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" +} diff --git a/deployment/receipts/BS-GZ-006-JD-OPS-CONNECTION-20260717.json b/deployment/receipts/BS-GZ-006-JD-OPS-CONNECTION-20260717.json new file mode 100644 index 0000000..afd3196 --- /dev/null +++ b/deployment/receipts/BS-GZ-006-JD-OPS-CONNECTION-20260717.json @@ -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 + } +} diff --git a/server-tools/guanghulab-front-door/index.html b/server-tools/guanghulab-front-door/index.html new file mode 100644 index 0000000..2883585 --- /dev/null +++ b/server-tools/guanghulab-front-door/index.html @@ -0,0 +1,106 @@ + + + + + + + 光湖 · 国内入口 + + + +
+
+
+ + + + + + 光湖 + +
广州备案前门 · 在线
+
+ +
+
+

GUANGHU · DOMESTIC GATEWAY

+

从这里,进入
光湖语言世界

+

广州节点保留备案域名与轻量入口,应用和计算逐步迁往京东云第五域主节点。入口稳定,后端可以继续生长。

+ +
+ +
+
+
+

ROUTES

+

选择一条路径

+
+

页面在广州,重计算在京东。每张卡片都是一个真实入口。

+
+ + +
+ +
+
广州 · BS-GZ-006备案域名 / HTTPS / 前门展示
+ +
北京 · JD-FD-PRIMARY应用运行 / 计算 / 调度中心
+
+
+ + + + diff --git a/server-tools/guanghulab-front-door/styles.css b/server-tools/guanghulab-front-door/styles.css new file mode 100644 index 0000000..bd5f926 --- /dev/null +++ b/server-tools/guanghulab-front-door/styles.css @@ -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; } +} diff --git a/server-tools/guanghulab-front-door/test/site.test.js b/server-tools/guanghulab-front-door/test/site.test.js new file mode 100644 index 0000000..2111213 --- /dev/null +++ b/server-tools/guanghulab-front-door/test/site.test.js @@ -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\)/); +}); diff --git a/server-tools/jd-app-hub/README.md b/server-tools/jd-app-hub/README.md new file mode 100644 index 0000000..63bb941 --- /dev/null +++ b/server-tools/jd-app-hub/README.md @@ -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. diff --git a/server-tools/jd-app-hub/app.js b/server-tools/jd-app-hub/app.js new file mode 100644 index 0000000..30901c8 --- /dev/null +++ b/server-tools/jd-app-hub/app.js @@ -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 = "节点状态暂不可用"; + }); diff --git a/server-tools/jd-app-hub/guanghu-jd-web-tunnel.service b/server-tools/jd-app-hub/guanghu-jd-web-tunnel.service new file mode 100644 index 0000000..8ac38f7 --- /dev/null +++ b/server-tools/jd-app-hub/guanghu-jd-web-tunnel.service @@ -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 diff --git a/server-tools/jd-app-hub/index.html b/server-tools/jd-app-hub/index.html new file mode 100644 index 0000000..9003b5b --- /dev/null +++ b/server-tools/jd-app-hub/index.html @@ -0,0 +1,31 @@ + + + + + + 京东云主节点 · 光湖 + + + +
+ +
+

JD-FD-PRIMARY · BEIJING

+

京东云主节点

+

这里是光湖第五域的国内运行节点。广州负责备案前门,这里负责应用、计算、迁移与服务器调度。

+
+
+
节点JD-FD-PRIMARY
+
角色第五域国内主节点
+
连接检查中
+
+
+ WORLD

光湖世界

进入光湖世界主入口与第五域路由。

+ SOURCE

第五域仓库

当前系统结构、人格路径和部署回执事实源。

+ ROUTER

全局检索

通过编号和关键词寻找正确的仓库路径。

+
NEXT

Tolaria 服务

桌面软件相关服务迁移完成后从这里接入。

建设中
+
+
+ + + diff --git a/server-tools/jd-app-hub/jd-app-hub.service b/server-tools/jd-app-hub/jd-app-hub.service new file mode 100644 index 0000000..0ae693a --- /dev/null +++ b/server-tools/jd-app-hub/jd-app-hub.service @@ -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 diff --git a/server-tools/jd-app-hub/server.js b/server-tools/jd-app-hub/server.js new file mode 100644 index 0000000..4d67d00 --- /dev/null +++ b/server-tools/jd-app-hub/server.js @@ -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 }; diff --git a/server-tools/jd-app-hub/styles.css b/server-tools/jd-app-hub/styles.css new file mode 100644 index 0000000..c048493 --- /dev/null +++ b/server-tools/jd-app-hub/styles.css @@ -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} } diff --git a/server-tools/jd-app-hub/test/server.test.js b/server-tools/jd-app-hub/test/server.test.js new file mode 100644 index 0000000..2f581d2 --- /dev/null +++ b/server-tools/jd-app-hub/test/server.test.js @@ -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}/); +}); diff --git a/server-tools/personal-node-probe/README.md b/server-tools/personal-node-probe/README.md new file mode 100644 index 0000000..db576e4 --- /dev/null +++ b/server-tools/personal-node-probe/README.md @@ -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. diff --git a/server-tools/personal-node-probe/guanghu-personal-node-probe.service b/server-tools/personal-node-probe/guanghu-personal-node-probe.service new file mode 100644 index 0000000..c02c2f7 --- /dev/null +++ b/server-tools/personal-node-probe/guanghu-personal-node-probe.service @@ -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 diff --git a/server-tools/personal-node-probe/guanghu-personal-node-probe.timer b/server-tools/personal-node-probe/guanghu-personal-node-probe.timer new file mode 100644 index 0000000..9ede845 --- /dev/null +++ b/server-tools/personal-node-probe/guanghu-personal-node-probe.timer @@ -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 diff --git a/server-tools/personal-node-probe/probe.sh b/server-tools/personal-node-probe/probe.sh new file mode 100644 index 0000000..1031e2e --- /dev/null +++ b/server-tools/personal-node-probe/probe.sh @@ -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" diff --git a/server-tools/personal-node-probe/test/probe.test.js b/server-tools/personal-node-probe/test/probe.test.js new file mode 100644 index 0000000..de15f8c --- /dev/null +++ b/server-tools/personal-node-probe/test/probe.test.js @@ -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/); +}); diff --git a/zero-point/core-channel/revive-guard/LL-JD-OPS-CENTER-KEY-AND-NODE-PLAN-20260716.hdlp b/zero-point/core-channel/revive-guard/LL-JD-OPS-CENTER-KEY-AND-NODE-PLAN-20260716.hdlp index ce54d2d..7d36368 100644 --- a/zero-point/core-channel/revive-guard/LL-JD-OPS-CENTER-KEY-AND-NODE-PLAN-20260716.hdlp +++ b/zero-point/core-channel/revive-guard/LL-JD-OPS-CENTER-KEY-AND-NODE-PLAN-20260716.hdlp @@ -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 · 冰朔不需要懂这些细节