feat: 支持自定义订阅节点并兼容现有数据库

This commit is contained in:
2026-09-29 11:52:41 +08:00
parent 3faf960f35
commit ab7d1c937a
27 changed files with 2203 additions and 449 deletions
+54 -12
View File
@@ -1,69 +1,110 @@
"use server";
import {
CustomProxyValidationError,
parseCustomProxyYaml,
} from "@/lib/custom-proxy";
import { revalidatePath } from "next/cache";
import { requireAdminAccess } from "@/lib/session";
import { createNode, deleteNode, updateNode } from "@/lib/store";
import {
createNode,
deleteNode,
normalizeNodeType,
updateNode,
} from "@/lib/store";
function boolFromForm(value: FormDataEntryValue | null) {
return value === "on";
}
function validateCustomYaml(nodeType: string, formData: FormData) {
if (nodeType !== "custom") return;
try {
parseCustomProxyYaml(String(formData.get("custom_config_yaml") ?? ""));
} catch (error) {
if (error instanceof CustomProxyValidationError)
return { error: error.message };
throw error;
}
}
export async function createNodeAction(formData: FormData) {
await requireAdminAccess();
const nodeType = normalizeNodeType(
String(formData.get("node_type") ?? "hy2"),
);
const validation = validateCustomYaml(nodeType, formData);
if (validation) return validation;
const name = String(formData.get("name") ?? "").trim();
const clientName = String(formData.get("client_name") ?? "").trim();
await createNode({
nodeType,
slug: String(formData.get("slug") ?? "").trim(),
name: String(formData.get("name") ?? "").trim(),
name,
authToken: String(formData.get("auth_token") ?? "").trim() || null,
hy2Listen: String(formData.get("hy2_listen") ?? "").trim() || "",
serverHost: String(formData.get("server_host") ?? "").trim(),
serverPort: Number(formData.get("server_port") ?? 443),
clientName:
String(formData.get("client_name") ?? "").trim() ||
String(formData.get("name") ?? "").trim(),
clientName: clientName || (nodeType === "hy2" ? name : ""),
bandwidthUpMbps: Number(formData.get("bandwidth_up_mbps") ?? 30),
bandwidthDownMbps: Number(formData.get("bandwidth_down_mbps") ?? 30),
skipCertVerify: boolFromForm(formData.get("skip_cert_verify")),
sni: String(formData.get("sni") ?? "").trim() || null,
customConfigYaml: String(formData.get("custom_config_yaml") ?? ""),
trafficStatsUrl: String(formData.get("traffic_stats_url") ?? "").trim(),
trafficStatsListen:
String(formData.get("traffic_stats_listen") ?? "").trim() || ":9999",
trafficStatsSecret:
String(formData.get("traffic_stats_secret") ?? "").trim(),
trafficStatsSecret: String(
formData.get("traffic_stats_secret") ?? "",
).trim(),
enabled: boolFromForm(formData.get("enabled")),
pollIntervalSeconds: Number(formData.get("poll_interval_seconds") ?? 15),
});
revalidatePath("/admin/nodes");
revalidatePath("/admin/subscriptions");
revalidatePath("/admin/dashboard");
}
export async function updateNodeAction(formData: FormData) {
await requireAdminAccess();
const nodeType = normalizeNodeType(
String(formData.get("node_type") ?? "hy2"),
);
const validation = validateCustomYaml(nodeType, formData);
if (validation) return validation;
const name = String(formData.get("name") ?? "").trim();
const clientName = String(formData.get("client_name") ?? "").trim();
await updateNode({
id: Number(formData.get("id")),
nodeType,
slug: String(formData.get("slug") ?? "").trim(),
name: String(formData.get("name") ?? "").trim(),
name,
authToken: String(formData.get("auth_token") ?? "").trim(),
hy2Listen: String(formData.get("hy2_listen") ?? "").trim() || "",
serverHost: String(formData.get("server_host") ?? "").trim(),
serverPort: Number(formData.get("server_port") ?? 443),
clientName:
String(formData.get("client_name") ?? "").trim() ||
String(formData.get("name") ?? "").trim(),
clientName: clientName || (nodeType === "hy2" ? name : ""),
bandwidthUpMbps: Number(formData.get("bandwidth_up_mbps") ?? 30),
bandwidthDownMbps: Number(formData.get("bandwidth_down_mbps") ?? 30),
skipCertVerify: boolFromForm(formData.get("skip_cert_verify")),
sni: String(formData.get("sni") ?? "").trim() || null,
customConfigYaml: String(formData.get("custom_config_yaml") ?? ""),
trafficStatsUrl: String(formData.get("traffic_stats_url") ?? "").trim(),
trafficStatsListen:
String(formData.get("traffic_stats_listen") ?? "").trim() || ":9999",
trafficStatsSecret: String(formData.get("traffic_stats_secret") ?? "").trim(),
trafficStatsSecret: String(
formData.get("traffic_stats_secret") ?? "",
).trim(),
enabled: boolFromForm(formData.get("enabled")),
pollIntervalSeconds: Number(formData.get("poll_interval_seconds") ?? 15),
});
revalidatePath("/admin/nodes");
revalidatePath("/admin/subscriptions");
revalidatePath("/admin/dashboard");
}
@@ -71,5 +112,6 @@ export async function deleteNodeAction(formData: FormData) {
await requireAdminAccess();
await deleteNode(Number(formData.get("id")));
revalidatePath("/admin/nodes");
revalidatePath("/admin/subscriptions");
revalidatePath("/admin/dashboard");
}