feat: establish NekoNest Cloud control and relay

This commit is contained in:
2026-08-12 23:25:43 +08:00
commit f27606b709
222 changed files with 71456 additions and 0 deletions
+363
View File
@@ -0,0 +1,363 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { PairingCancelButton } from "../PairingCancelButton";
import {
buildDaemonRegistrationCommand,
daemonStartCommand,
type SupportedHostOS,
} from "./onboarding";
import type { PairingAccessState } from "../../onboarding";
type PairingResult = { id: string; bootstrapToken: string; expiresAt: string };
type PairingView = PairingResult & {
requestedName: string;
os: SupportedHostOS;
};
type PairingProgress = {
id: string;
status: "waiting" | "claimed" | "expired" | "locked" | "cancelled";
expiresAt: string;
claimedHostId: string | null;
claimedAt: string | null;
claimAttemptState: "not_seen" | "seen" | "invalid";
lastClaimAttemptAt: string | null;
};
type PairingTerminal = Exclude<PairingProgress["status"], "waiting" | "claimed">;
type PairingCompletion = {
hostId: string;
claimedAt: string;
};
type CopyTarget = "command" | "token" | "start";
const unavailableCopy: Record<Exclude<PairingAccessState, "available">, { title: string; detail: string; href: string; action: string; readiness?: boolean }> = {
request_pending: {
title: "免费闭测申请正在审核",
detail: "处理结果会显示在公测权益页;审核期间不需要重复申请。",
href: "/dashboard/billing",
action: "查看申请进度",
},
gated: {
title: "公开接入仍由安全门禁阻止",
detail: "免费政策已经预设,但 P0 证据尚未齐全。可以申请小范围免费闭测资格。",
href: "/dashboard/billing",
action: "申请免费闭测",
readiness: true,
},
inactive: {
title: "公开公测当前未开放",
detail: "该账户没有有效的免费公测或闭测邀请;可以提交闭测申请,不会因此创建订单或要求付款。",
href: "/dashboard/billing",
action: "申请免费闭测",
},
full: {
title: "当前没有可用主机槽位",
detail: "已有主机和等待中的配对请求已经占满当前免费容量,请先取消旧请求或联系管理员调整邀请。",
href: "/dashboard/hosts",
action: "管理主机和配对",
},
};
const terminalCopy: Record<PairingTerminal, { title: string; detail: string }> = {
expired: {
title: "这枚配对码已经过期",
detail: "十分钟有效期已经结束,原码不能恢复。确认 daemon 已准备好后再生成一枚新码。",
},
locked: {
title: "这枚配对码已经锁定",
detail: "错误尝试次数已达到上限,原码不能继续使用。请核对 daemon 和复制步骤后重新生成。",
},
cancelled: {
title: "这枚配对码已经取消",
detail: "原码已失效并释放预留容量;需要接入时可以重新生成。",
},
};
export function PairingForm({
accessState,
releaseAvailable,
minimumDaemonVersion,
connectOrigin,
}: {
accessState: PairingAccessState;
releaseAvailable: boolean;
minimumDaemonVersion: string;
connectOrigin: string;
}) {
const [name, setName] = useState("");
const [os, setOs] = useState<SupportedHostOS>("windows");
const [pairing, setPairing] = useState<PairingView | null>(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState<CopyTarget | null>(null);
const [copyError, setCopyError] = useState("");
const [closedBetaBuildConfirmed, setClosedBetaBuildConfirmed] = useState(false);
const [progressError, setProgressError] = useState("");
const [completion, setCompletion] = useState<PairingCompletion | null>(null);
const [terminal, setTerminal] = useState<PairingTerminal | null>(null);
const [claimAttempt, setClaimAttempt] = useState<Pick<
PairingProgress,
"claimAttemptState" | "lastClaimAttemptAt"
>>({ claimAttemptState: "not_seen", lastClaimAttemptAt: null });
const resultHeading = useRef<HTMLHeadingElement>(null);
const pairingId = pairing?.id ?? null;
useEffect(() => {
if (pairing) resultHeading.current?.focus();
}, [pairing]);
useEffect(() => {
if (!pairingId) return;
let active = true;
let timer: number | undefined;
async function poll() {
try {
const response = await fetch(
`/api/hosts/pairing?pairing_id=${encodeURIComponent(pairingId!)}`,
{ cache: "no-store" },
);
const body = (await response.json()) as {
pairing?: PairingProgress;
message?: string;
};
if (!active) return;
if (!response.ok || !body.pairing) {
throw new Error(body.message || "暂时无法确认配对状态");
}
setProgressError("");
setClaimAttempt({
claimAttemptState: body.pairing.claimAttemptState,
lastClaimAttemptAt: body.pairing.lastClaimAttemptAt,
});
if (body.pairing.status === "claimed" && body.pairing.claimedHostId && body.pairing.claimedAt) {
setCompletion({
hostId: body.pairing.claimedHostId,
claimedAt: body.pairing.claimedAt,
});
setPairing(null);
setCopied(null);
setCopyError("");
return;
}
if (
body.pairing.status === "expired"
|| body.pairing.status === "locked"
|| body.pairing.status === "cancelled"
) {
setTerminal(body.pairing.status);
setPairing(null);
setCopied(null);
setCopyError("");
return;
}
timer = window.setTimeout(poll, 2_000);
} catch (pollError) {
if (!active) return;
setProgressError(
pollError instanceof Error
? `${pollError.message};页面会继续重试。`
: "暂时无法确认配对状态;页面会继续重试。",
);
timer = window.setTimeout(poll, 5_000);
}
}
timer = window.setTimeout(poll, 1_000);
return () => {
active = false;
if (timer !== undefined) window.clearTimeout(timer);
};
}, [pairingId]);
if (accessState !== "available") {
const copy = unavailableCopy[accessState];
return (
<div className="empty-state pairing-access-blocked" role="status">
<span className="empty-symbol">×</span>
<h2>{copy.title}</h2>
<p>{copy.detail}</p>
<div className="pairing-result-actions">
<a className="button button-primary" href={copy.href}>{copy.action}</a>
{copy.readiness && <a className="button button-secondary" href="/readiness"></a>}
<a className="button button-secondary" href="/dashboard"></a>
</div>
</div>
);
}
async function copyText(value: string, target: CopyTarget) {
setCopyError("");
try {
if (!navigator.clipboard?.writeText) throw new Error("clipboard_unavailable");
await navigator.clipboard.writeText(value);
setCopied(target);
} catch {
setCopied(null);
setCopyError("浏览器未允许自动复制。请点入对应文本框,使用 Ctrl+C 或系统复制操作手动复制。");
}
}
async function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setError("");
setTerminal(null);
setCompletion(null);
if (!releaseAvailable && !closedBetaBuildConfirmed) {
setError("公开下载尚未就绪。请先取得并核验兼容的闭测构建,再确认后生成配对码。");
return;
}
setLoading(true);
try {
const response = await fetch("/api/hosts/pairing", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ requestedName: name, os }),
});
const body = (await response.json()) as { pairing?: PairingResult; message?: string };
if (!response.ok || !body.pairing) throw new Error(body.message || "无法创建配对请求");
setPairing({
...body.pairing,
requestedName: name.trim(),
os,
});
setCopied(null);
setCopyError("");
setProgressError("");
setClaimAttempt({ claimAttemptState: "not_seen", lastClaimAttemptAt: null });
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "无法创建配对请求");
} finally {
setLoading(false);
}
}
if (completion) {
return (
<div className="empty-state pairing-complete" role="status">
<span className="empty-symbol"></span>
<h2></h2>
<p> revisiondaemon </p>
<small>{new Date(completion.claimedAt).toLocaleString("zh-CN")} · {completion.hostId}</small>
<div className="pairing-result-actions">
<a className="button button-primary" href="/dashboard/hosts"></a>
<button className="button button-secondary" type="button" onClick={() => setCompletion(null)}></button>
</div>
</div>
);
}
if (terminal) {
const copy = terminalCopy[terminal];
return (
<div className="empty-state pairing-terminal" role="status">
<span className="empty-symbol">×</span>
<h2>{copy.title}</h2>
<p>{copy.detail}</p>
<div className="pairing-result-actions">
<button className="button button-primary" type="button" onClick={() => setTerminal(null)}></button>
<a className="button button-secondary" href="/dashboard/hosts"></a>
</div>
</div>
);
}
if (pairing) {
const registrationCommand = buildDaemonRegistrationCommand({
os: pairing.os,
connectOrigin,
hostName: pairing.requestedName,
});
const startCommand = daemonStartCommand(pairing.os);
return (
<div className="pairing-result">
<span className="eyebrow">PAIRING REQUEST CREATED</span>
<h2 ref={resultHeading} tabIndex={-1}> {pairing.os === "windows" ? "Windows" : "Linux"} </h2>
<p> Cloud Connect URL </p>
<div className="pairing-progress" role="status" aria-live="polite">
<strong>{claimAttempt.claimAttemptState === "seen" ? "Cloud 已收到请求,但尚未认领" : claimAttempt.claimAttemptState === "invalid" ? "认领尝试时间待核实" : "尚未收到注册请求"}</strong>
<span>{claimAttempt.claimAttemptState === "seen"
? `最近一次尝试:${new Date(claimAttempt.lastClaimAttemptAt!).toLocaleString("zh-CN")}。这只证明 Cloud 收到过针对该配对 ID 的请求,不证明来源一定是你的 daemon。请先查看终端错误,并核对系统类型、配对码、daemon 版本和 identity.json;当前码有效时无需反复生成。`
: claimAttempt.claimAttemptState === "invalid"
? "Cloud 找到过针对这枚配对请求的记录,但时间异常,不能判断先后;请查看终端错误或提交反馈。"
: "请先在目标主机运行注册命令并粘贴一次性码。页面每两秒确认一次,尚未收到请求通常表示命令未运行、地址不可达或请求还没发出。"}</span>
</div>
{progressError && <p className="form-error" role="alert">{progressError}</p>}
<a className="text-link pairing-download-link" href="/download"> daemon SHA-256 </a>
<ol className="registration-steps">
<li>
<strong> daemon </strong>
<label className="pairing-copy-field">
<span>{pairing.os === "windows" ? "PowerShell" : "Bash"} </span>
<textarea
readOnly
rows={pairing.os === "windows" ? 12 : 10}
value={registrationCommand}
onFocus={(event) => event.currentTarget.select()}
aria-describedby="registration-command-note"
/>
</label>
<button className="button button-secondary" type="button" onClick={() => copyText(registrationCommand, "command")}>{copied === "command" ? "命令已复制" : "复制注册命令"}</button>
<small id="registration-command-note"></small>
</li>
<li>
<strong></strong>
<label className="pairing-copy-field">
<span></span>
<input readOnly value={pairing.bootstrapToken} onFocus={(event) => event.currentTarget.select()} autoComplete="off" spellCheck={false} />
</label>
<button className="button button-secondary" type="button" onClick={() => copyText(pairing.bootstrapToken, "token")}>{copied === "token" ? "配对码已复制" : "复制一次性配对码"}</button>
<small> {new Date(pairing.expiresAt).toLocaleString("zh-CN")}</small>
</li>
<li>
<strong> daemon</strong>
<label className="pairing-copy-field compact">
<span></span>
<input readOnly value={startCommand} onFocus={(event) => event.currentTarget.select()} />
</label>
<button className="button button-secondary" type="button" onClick={() => copyText(startCommand, "start")}>{copied === "start" ? "启动命令已复制" : "复制启动命令"}</button>
<small> daemon Relay </small>
</li>
</ol>
{copyError && <p className="form-error" role="alert">{copyError}</p>}
{copied && !copyError && <p className="form-success" role="status">使</p>}
<div className="prototype-callout"><strong>使</strong><span></span></div>
<div className="prototype-callout"><strong> daemon</strong><span> identity.json config.json Ed25519 daemon </span></div>
<div className="prototype-callout"><strong></strong><span></span></div>
<div className="pairing-result-actions">
<a className="button button-secondary" href="/dashboard/hosts"></a>
<PairingCancelButton
pairingId={pairing.id}
onCancelled={() => {
setPairing(null);
setTerminal("cancelled");
setCopied(null);
setCopyError("");
setProgressError("");
}}
/>
</div>
</div>
);
}
return (
<form className="cloud-form pairing-form" onSubmit={submit}>
{!releaseAvailable && (
<div className="prototype-callout daemon-preflight" role="group" aria-labelledby="daemon-preflight-title">
<strong id="daemon-preflight-title"> daemon </strong>
<span> SHA-256 </span>
<a className="text-link" href="/download"> v{minimumDaemonVersion} </a>
<label className="switch-row">
<input type="checkbox" checked={closedBetaBuildConfirmed} onChange={(event) => setClosedBetaBuildConfirmed(event.target.checked)} />
<span><strong></strong><small> v{minimumDaemonVersion} SHA-256 </small></span>
</label>
</div>
)}
<label><span></span><input value={name} onChange={(event) => setName(event.target.value)} placeholder="例如:家里工作站" minLength={2} maxLength={48} required /><small></small></label>
<fieldset><legend></legend><div className="choice-grid"><label className={os === "windows" ? "selected" : ""}><input type="radio" name="os" value="windows" checked={os === "windows"} onChange={() => setOs("windows")} /><strong>Windows</strong><small>amd64 · </small></label><label className={os === "linux" ? "selected" : ""}><input type="radio" name="os" value="linux" checked={os === "linux"} onChange={() => setOs("linux")} /><strong>Linux</strong><small>amd64 / arm64 · </small></label></div></fieldset>
{error && <p className="form-error" role="alert">{error}</p>}
<button className="button button-primary" type="submit" disabled={loading || (!releaseAvailable && !closedBetaBuildConfirmed)}>{loading ? "正在创建…" : "生成 10 分钟配对码"}</button>
</form>
);
}
+72
View File
@@ -0,0 +1,72 @@
export type SupportedHostOS = "windows" | "linux";
export function assertSafeConnectOrigin(value: string): string {
const origin = value.trim().replace(/\/$/, "");
const parsed = new URL(origin);
const isLoopback = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
if (parsed.origin !== origin || (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopback))) {
throw new Error("Cloud Connect 服务必须使用 HTTPS origin(本机开发地址除外)");
}
return origin;
}
export function resolveConnectOrigin(
configured: string | undefined,
development = process.env.NODE_ENV !== "production",
): string {
const value = configured?.trim() ?? "";
if (value) return assertSafeConnectOrigin(value);
if (development) return "http://127.0.0.1:3000";
throw new Error("NEKONEST_CLOUD_CONNECT_ORIGIN is required outside development");
}
export function quotePowerShell(value: string): string {
return `'${value.replaceAll("'", "''")}'`;
}
export function quoteBash(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
export function buildDaemonRegistrationCommand(input: {
os: SupportedHostOS;
connectOrigin: string;
hostName: string;
}): string {
const connectOrigin = assertSafeConnectOrigin(input.connectOrigin);
const hostName = input.hostName.trim();
if (!hostName) throw new Error("主机名称不能为空");
if (input.os === "windows") {
return [
`$env:NEKONEST_SERVER = ${quotePowerShell(connectOrigin)}`,
`$env:NEKONEST_TRANSPORT_MODE = 'sealed'`,
`$secureToken = Read-Host '粘贴一次性配对码' -AsSecureString`,
`$tokenPtr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)`,
"try {",
` $env:NEKONEST_BOOTSTRAP_TOKEN = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenPtr)`,
` if ([string]::IsNullOrWhiteSpace($env:NEKONEST_BOOTSTRAP_TOKEN)) { throw '未读取到配对码' }`,
` & '.\\nekonest-daemon.exe' -register -name ${quotePowerShell(hostName)}`,
"} finally {",
" Remove-Item Env:NEKONEST_BOOTSTRAP_TOKEN -ErrorAction SilentlyContinue",
" [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenPtr)",
"}",
].join("\n");
}
return [
"(",
" read -rsp '粘贴一次性配对码: ' NEKONEST_BOOTSTRAP_TOKEN",
" printf '\\n'",
` if [ -z "$NEKONEST_BOOTSTRAP_TOKEN" ]; then printf '未读取到配对码\\n' >&2; exit 1; fi`,
` export NEKONEST_SERVER=${quoteBash(connectOrigin)}`,
" export NEKONEST_TRANSPORT_MODE='sealed'",
" export NEKONEST_BOOTSTRAP_TOKEN",
` ./nekonest-daemon -register -name ${quoteBash(hostName)}`,
")",
].join("\n");
}
export function daemonStartCommand(os: SupportedHostOS): string {
return os === "windows" ? ".\\nekonest-daemon.exe" : "./nekonest-daemon";
}
+39
View File
@@ -0,0 +1,39 @@
import Link from "next/link";
import { requireCloudViewer } from "../../../cloud-auth";
import { DashboardShell, PageHeading } from "../../../components/Shells";
import { getDashboardSnapshot, getOrCreateAccount } from "@/db/repository";
import { getDaemonReleaseState } from "../../../daemon-release";
import { deriveBetaOnboarding } from "../../onboarding";
import { PairingForm } from "./PairingForm";
import { resolveConnectOrigin } from "./onboarding";
import { env } from "cloudflare:workers";
export const dynamic = "force-dynamic";
export default async function NewHostPage() {
const viewer = await requireCloudViewer("/dashboard/hosts/new");
const account = await getOrCreateAccount(viewer);
const [snapshot, daemonRelease] = await Promise.all([
getDashboardSnapshot(account),
getDaemonReleaseState(),
]);
const onboarding = deriveBetaOnboarding({
entitlement: snapshot.entitlement,
hasPendingRequest: snapshot.accessRequests.some((request) => request.status === "requested"),
});
const connectOrigin = resolveConnectOrigin(
env.NEKONEST_CLOUD_CONNECT_ORIGIN,
process.env.NODE_ENV !== "production",
);
return (
<DashboardShell viewer={viewer} active="/dashboard/hosts">
<div className="cloud-page narrow-cloud-page">
<PageHeading eyebrow="ADD OR RECOVER HOST / 添加或恢复主机" title="先创建一次性配对请求。" description="新主机和已撤销主机的安全恢复共用这一步;家里不需要打开入站端口。" actions={<Link className="button button-secondary" href="/download"> daemon</Link>} />
<div className="form-layout">
<section className="panel form-panel"><PairingForm accessState={onboarding.pairingAccessState} releaseAvailable={daemonRelease.available} minimumDaemonVersion={daemonRelease.minimumVersion} connectOrigin={connectOrigin} /></section>
<aside className="form-aside"><span className="eyebrow"></span><ol><li><strong></strong><p> SHA-256 </p></li><li><strong>daemon </strong><p>daemon </p></li><li><strong></strong><p> daemon </p></li><li><strong></strong><p></p></li></ol></aside>
</div>
</div>
</DashboardShell>
);
}