feat: 接入 AnyTLS 节点托管和用户流量同步
This commit is contained in:
+73
-10
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user