feat: 接入 AnyTLS 节点托管和用户流量同步

This commit is contained in:
2026-09-29 13:35:44 +08:00
parent 586c2399d1
commit ac5fed695c
24 changed files with 1413 additions and 185 deletions
+21
View File
@@ -6,6 +6,7 @@ import {
} from "@/lib/custom-proxy";
import { revalidatePath } from "next/cache";
import { ensureDb } from "@/lib/db";
import { requireAdminAccess } from "@/lib/session";
import {
createNode,
@@ -115,3 +116,23 @@ export async function deleteNodeAction(formData: FormData) {
revalidatePath("/admin/subscriptions");
revalidatePath("/admin/dashboard");
}
export async function updateAnyTLSNodeAction(data: FormData) {
await requireAdminAccess();
const name = String(data.get("name") || "").trim();
if (!name || name.length > 100)
return { error: "请填写 1–100 字的节点名称。" };
const db = await ensureDb();
db.prepare(
"UPDATE hy2_nodes SET name=?,client_name=?,enabled=?,updated_at=? WHERE id=? AND node_type='anytls'",
).run(
name,
name,
data.get("enabled") === "on" ? 1 : 0,
new Date().toISOString(),
Number(data.get("id")),
);
revalidatePath("/admin/nodes");
revalidatePath("/admin/subscriptions");
revalidatePath("/admin/dashboard");
}
+73 -10
View File
@@ -17,14 +17,16 @@ import {
import {
createNodeAction,
updateAnyTLSNodeAction,
deleteNodeAction,
updateNodeAction,
} from "./actions";
function getNodePills(node: {
enabled: number;
node_type: "hy2" | "custom";
node_type: "hy2" | "custom" | "anytls";
last_error_message: string | null;
last_sync_ok_at: string | null;
}) {
return [
{
@@ -33,9 +35,13 @@ function getNodePills(node: {
},
node.node_type === "custom"
? { label: "自定义 YAML", className: "status-pill status-pill--ink" }
: node.last_error_message
? { label: "同步异常", className: "status-pill status-pill--danger" }
: { label: "同步正常", className: "status-pill status-pill--sage" },
: node.node_type === "anytls" &&
(!node.last_sync_ok_at ||
Date.parse(node.last_sync_ok_at) < new Date().getTime() - 90_000)
? { label: "心跳超时", className: "status-pill status-pill--danger" }
: node.last_error_message
? { label: "同步异常", className: "status-pill status-pill--danger" }
: { label: "同步正常", className: "status-pill status-pill--sage" },
];
}
@@ -48,7 +54,12 @@ export default async function NodesPage() {
<div className="hero-copy">
<span className="eyebrow">节点</span>
<h2>节点管理</h2>
<a href="/admin/certificates" className={buttonVariants({ variant: "outline", size: "sm" })}>证书托管 / 自动安装节点</a>
<a
href="/admin/certificates"
className={buttonVariants({ variant: "outline", size: "sm" })}
>
证书托管 / 自动安装节点
</a>
<div className="badge-row">
<span className="status-pill status-pill--ok">
启用 {nodes.filter((node) => node.enabled).length}
@@ -216,6 +227,7 @@ export default async function NodesPage() {
}
items={nodes.map((node) => {
const isCustom = node.node_type === "custom";
const isAnyTLS = node.node_type === "anytls";
const proxyName = getNodeProxyName(node);
const authUrl = buildNodeAuthUrl(
node.slug,
@@ -229,7 +241,7 @@ export default async function NodesPage() {
return {
id: node.id,
text: `${node.name} ${node.slug} ${node.server_host} ${proxyName} ${isCustom ? "自定义 YAML custom" : "HY2"}`,
text: `${node.name} ${node.slug} ${node.server_host} ${proxyName} ${isCustom ? "自定义 YAML custom" : isAnyTLS ? "AnyTLS sing-box TCP" : "HY2"}`,
status: node.enabled ? "enabled" : "disabled",
content: (
<details key={node.id} className="entity-table__item">
@@ -250,20 +262,30 @@ export default async function NodesPage() {
? proxyName
: `${node.server_host}:${node.server_port}`}
<small>
{isCustom ? "自定义订阅节点" : `hy2 ${node.hy2_listen}`}
{isCustom
? "自定义订阅节点"
: `${isAnyTLS ? "AnyTLS TCP" : "HY2 UDP"} ${node.hy2_listen}`}
</small>
</span>
<span className="entity-code-cell break-all">
{isCustom ? "不轮询" : node.traffic_stats_url}
{isCustom
? "不轮询"
: isAnyTLS
? "节点主动上报"
: node.traffic_stats_url}
<small>
{isCustom ? "仅订阅" : node.traffic_stats_listen}
{isCustom
? "仅订阅"
: isAnyTLS
? "管理接口仅本机"
: node.traffic_stats_listen}
</small>
</span>
<span className="entity-number-cell">
<strong>
{isCustom
? "—"
: `${node.last_online_users} / ${node.last_stream_count}`}
: `${isAnyTLS ? "—" : node.last_online_users} / ${node.last_stream_count}`}
</strong>
<span>{isCustom ? "无流量统计" : "用户 / 流"}</span>
</span>
@@ -285,6 +307,47 @@ export default async function NodesPage() {
<div className="entity-table__details">
{isCustom ? (
<CustomNodeForm node={node} />
) : isAnyTLS ? (
<>
<p className="muted">
AnyTLS / sing-box · 每用户独立凭据,约 15
秒同步。用户增删、密码、到期或额度变更会更新配置并重启
sing-box,现有连接会断开。连续失联约 90
秒后停止代理。在线用户数暂不提供,流数量来自本机接口。
</p>
<p className="muted">
{!node.last_sync_ok_at ||
Date.parse(node.last_sync_ok_at) <
new Date().getTime() - 90_000
? "节点未连接或心跳已超时"
: "节点心跳正常"}{" "}
· SNI:{node.sni}
</p>
<ActionForm
action={updateAnyTLSNodeAction}
className="dense-form-grid dense-form-grid--nodes"
>
<input type="hidden" name="id" value={node.id} />
<label className="field">
<span>节点名称 / 订阅名</span>
<Input
name="name"
defaultValue={node.name}
required
maxLength={100}
/>
</label>
<label className="checkbox">
<input
name="enabled"
type="checkbox"
defaultChecked={Boolean(node.enabled)}
/>
<span>启用节点</span>
</label>
<SubmitButton>保存节点</SubmitButton>
</ActionForm>
</>
) : (
<>
<div className="dense-code-grid">