3x-ui 式共享节点改造:nodes 状态机 + nodeClients 各自凭据
- 移除 VMess 协议 - VLESS/SS2022 按用户流量统计(AnyTLS 日志解析,SS 节点级) - sing-box v2ray_api stats 支持 - 内存优化:面板 128M,agent 64M,sing-box 64M
This commit is contained in:
+38
-29
@@ -1,36 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
// Client URLs are only issued for confirmed, running entry resources.
|
||||
function entries(data, customerId) {
|
||||
const active = data.deployments.filter((item) => item.customerId === customerId && !item.archived && item.status === 'active' &&
|
||||
['relay', 'direct'].includes(item.role) && typeof item.clientUri === 'string' && item.clientUri.length > 0 &&
|
||||
(item.role === 'direct' || data.deployments.some((exit) => exit.id === item.exitDeploymentId && exit.status === 'active' && !exit.archived)));
|
||||
const seen = new Map();
|
||||
return active.map((item) => {
|
||||
const chainName = data.chains?.find((chain) => chain.id === item.chainId)?.name;
|
||||
if (!chainName) return item;
|
||||
// Use the current route name even for links generated by older Agents.
|
||||
// Multiple entries under one route still need distinct names in Clash.
|
||||
const index = (seen.get(chainName) || 0) + 1;
|
||||
seen.set(chainName, index);
|
||||
const name = index === 1 ? chainName : `${chainName} (${index})`;
|
||||
if (item.clientUri.startsWith('vmess://')) {
|
||||
const payload = JSON.parse(Buffer.from(item.clientUri.slice(8), 'base64').toString('utf8'));
|
||||
return { ...item, clientUri:`vmess://${Buffer.from(JSON.stringify({ ...payload, ps:name })).toString('base64')}` };
|
||||
}
|
||||
const separator = item.clientUri.lastIndexOf('#');
|
||||
return { ...item, clientUri:`${item.clientUri.slice(0, separator < 0 ? undefined : separator)}#${encodeURIComponent(name)}` };
|
||||
const { buildClientUri, nodeEmail } = require('./protocols');
|
||||
|
||||
// Client URLs are only issued for confirmed, running nodes. Each link is built
|
||||
// on the fly from the node's current config and the customer's own credentials
|
||||
// for that node (3x-ui style: shared inbound, per-client identity).
|
||||
function nodeClientUri(data, nodeClient) {
|
||||
const node = data.nodes.find((item) => item.id === nodeClient.nodeId);
|
||||
if (!node || node.status !== 'active') return null;
|
||||
const server = data.servers.find((item) => item.id === node.serverId);
|
||||
if (!server) return null;
|
||||
return buildClientUri({
|
||||
protocol: node.protocol, relayServer: server, relayPort: node.port,
|
||||
credentials: { ...nodeClient.credentials, wsPath: node.wsPath, socksUser: nodeEmail(nodeClient.customerId) },
|
||||
reality: node.reality, publicKey: node.artifacts && node.artifacts.realityPublicKey,
|
||||
name: node.name, networkMode: node.networkMode || 'ipv4', tlsDomain: server.tlsDomain,
|
||||
ssServerKey: node.ssServerKey,
|
||||
});
|
||||
}
|
||||
|
||||
function entries(data, customerId) {
|
||||
const seen = new Map();
|
||||
const items = [];
|
||||
for (const nodeClient of data.nodeClients.filter((nc) => nc.customerId === customerId && nc.status === 'active')) {
|
||||
const uri = nodeClientUri(data, nodeClient);
|
||||
if (!uri) continue;
|
||||
const node = data.nodes.find((item) => item.id === nodeClient.nodeId);
|
||||
const nodeName = node ? node.name : nodeClient.nodeId;
|
||||
// Use the current node name even for links generated by older Agents.
|
||||
const index = (seen.get(nodeName) || 0) + 1;
|
||||
seen.set(nodeName, index);
|
||||
const name = index === 1 ? nodeName : `${nodeName} (${index})`;
|
||||
const separator = uri.lastIndexOf('#');
|
||||
const clientUri = `${uri.slice(0, separator < 0 ? undefined : separator)}#${encodeURIComponent(name)}`;
|
||||
items.push({ nodeId: nodeClient.nodeId, clientUri });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function q(value) { return JSON.stringify(String(value)); }
|
||||
|
||||
function clashProxy(item) {
|
||||
const uri = item.clientUri;
|
||||
if (uri.startsWith('vmess://')) {
|
||||
const value = JSON.parse(Buffer.from(uri.slice(8), 'base64').toString('utf8'));
|
||||
return { name:value.ps, type:'vmess', server:value.add, port:Number(value.port), uuid:value.id, alterId:0, cipher:'auto', network:'ws', 'ws-opts':{ path:value.path } };
|
||||
}
|
||||
const name = decodeURIComponent(uri.slice(uri.lastIndexOf('#') + 1));
|
||||
const url = new URL(uri);
|
||||
const base = { name, server:url.hostname.replace(/^\[|\]$/g, ''), port:Number(url.port) };
|
||||
@@ -104,14 +115,13 @@ function makeClash(items, smart = false) {
|
||||
function makeSurge(items) {
|
||||
// Surge does not accept VLESS/Reality policies. Omit unsupported nodes rather
|
||||
// than handing clients an importable profile that cannot establish a tunnel.
|
||||
const proxies = items.map(clashProxy).filter((item) => ['ss','socks5','vmess'].includes(item.type));
|
||||
const proxies = items.map(clashProxy).filter((item) => ['ss','socks5'].includes(item.type));
|
||||
if (!proxies.length) return null;
|
||||
const safe = (value) => String(value).replace(/[\r\n,=]/g, '_');
|
||||
const policies = proxies.map((proxy) => {
|
||||
const prefix = `${safe(proxy.name)} = `;
|
||||
if (proxy.type === 'ss') return `${prefix}ss, ${proxy.server}, ${proxy.port}, encrypt-method=${proxy.cipher}, password=${safe(proxy.password)}`;
|
||||
if (proxy.type === 'socks5') return `${prefix}socks5, ${proxy.server}, ${proxy.port}, ${safe(proxy.username)}, ${safe(proxy.password)}`;
|
||||
return `${prefix}vmess, ${proxy.server}, ${proxy.port}, username=${proxy.uuid}, vmess-aead=true, ws=true, ws-path=${safe(proxy['ws-opts'].path)}`;
|
||||
return `${prefix}socks5, ${proxy.server}, ${proxy.port}, ${safe(proxy.username)}, ${safe(proxy.password)}`;
|
||||
});
|
||||
const names = proxies.map((proxy) => safe(proxy.name)).join(', ');
|
||||
return `[General]\nloglevel = notify\n[Proxy]\n${policies.join('\n')}\n[Proxy Group]\n节点选择 = select, ${names}, DIRECT\n自动选择 = url-test, ${names}, url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n`;
|
||||
@@ -124,7 +134,6 @@ function makeSingBox(items) {
|
||||
if (proxy.type === 'hysteria2') return { type:'hysteria2', ...base, password:proxy.password, tls:{ enabled:true, server_name:proxy.sni, alpn:['h3'] } };
|
||||
if (proxy.type === 'anytls') return { type:'anytls', ...base, password:proxy.password, tls:{ enabled:true, server_name:proxy.sni } };
|
||||
if (proxy.type === 'socks5') return { type:'socks', ...base, version:'5', username:proxy.username, password:proxy.password };
|
||||
if (proxy.type === 'vmess') return { type:'vmess', ...base, uuid:proxy.uuid, security:'auto', alter_id:0, transport:{ type:'ws', path:proxy['ws-opts'].path } };
|
||||
const outbound = { type:'vless', ...base, uuid:proxy.uuid };
|
||||
if (proxy.flow) outbound.flow = proxy.flow;
|
||||
if (proxy.network === 'ws') outbound.transport = { type:'ws', path:proxy['ws-opts'].path };
|
||||
@@ -151,4 +160,4 @@ function formatSubscription(data, customer, format, userAgent = '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
module.exports = { entries, clashProxy, makeClash, makeSingBox, makeSurge, formatSubscription };
|
||||
module.exports = { entries, nodeClientUri, clashProxy, makeClash, makeSingBox, makeSurge, formatSubscription };
|
||||
|
||||
Reference in New Issue
Block a user