feat(panel): reskin backend — node sortIndex, presence/speed, /api/system, credentials, subscription aggregation (Node+Go parity)
This commit is contained in:
+54
-3
@@ -1,6 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const { buildClientUri, nodeEmail } = require('./protocols');
|
||||
const { dedupKey, withName, nameFromUri } = require('./extsub');
|
||||
|
||||
function nodeSortIndex(data, nodeId) {
|
||||
const node = data.nodes.find((item) => item.id === nodeId);
|
||||
return node && Number.isFinite(node.sortIndex) ? node.sortIndex : Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -22,7 +28,11 @@ function nodeClientUri(data, nodeClient) {
|
||||
function entries(data, customerId) {
|
||||
const seen = new Map();
|
||||
const items = [];
|
||||
for (const nodeClient of data.nodeClients.filter((nc) => nc.customerId === customerId && nc.status === 'active')) {
|
||||
// Own nodes follow the panel-wide custom order (sortIndex); the sort is
|
||||
// stable, so equal/unset indexes keep the binding creation order.
|
||||
const bindings = data.nodeClients.filter((nc) => nc.customerId === customerId && nc.status === 'active');
|
||||
bindings.sort((a, b) => nodeSortIndex(data, a.nodeId) - nodeSortIndex(data, b.nodeId));
|
||||
for (const nodeClient of bindings) {
|
||||
const uri = nodeClientUri(data, nodeClient);
|
||||
if (!uri) continue;
|
||||
const node = data.nodes.find((item) => item.id === nodeClient.nodeId);
|
||||
@@ -38,6 +48,47 @@ function entries(data, customerId) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Full subscription item list: own nodes (sortIndex order), then manual
|
||||
// links (list order), then external sources (source order, cached order).
|
||||
// Global dedup by scheme+host+port+credential fingerprint, own first.
|
||||
// Name collisions across segments get the " (n)" suffix treatment.
|
||||
function aggregateEntries(data, customer) {
|
||||
const items = entries(data, customer.id);
|
||||
const usedKeys = new Set();
|
||||
const usedNames = new Set();
|
||||
for (const item of items) {
|
||||
const key = dedupKey(item.clientUri);
|
||||
if (key) usedKeys.add(key);
|
||||
usedNames.add(nameFromUri(item.clientUri));
|
||||
}
|
||||
const append = (uri, rawName) => {
|
||||
const key = dedupKey(uri);
|
||||
if (key && usedKeys.has(key)) return;
|
||||
let name = rawName || '未命名节点';
|
||||
if (usedNames.has(name)) {
|
||||
let index = 2;
|
||||
while (usedNames.has(`${name} (${index})`)) index += 1;
|
||||
name = `${name} (${index})`;
|
||||
}
|
||||
if (key) usedKeys.add(key);
|
||||
usedNames.add(name);
|
||||
items.push({ nodeId: '', clientUri: withName(uri, name) });
|
||||
};
|
||||
(customer.manualLinks || []).forEach((link, index) => {
|
||||
if (!link.enabled) return;
|
||||
append(link.uri, link.remark || nameFromUri(link.uri) || `手动节点 ${index + 1}`);
|
||||
});
|
||||
for (const source of customer.externalSources || []) {
|
||||
if (!source.enabled) continue;
|
||||
const cache = (data.subSourceCache || {})[source.id];
|
||||
if (!cache || !Array.isArray(cache.items)) continue;
|
||||
for (const cached of cache.items) {
|
||||
append(cached.uri, `${source.remark ? `[${source.remark}] ` : ''}${cached.name || '未命名节点'}`);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function q(value) { return JSON.stringify(String(value)); }
|
||||
|
||||
function clashProxy(item) {
|
||||
@@ -149,7 +200,7 @@ function makeSingBox(items) {
|
||||
}
|
||||
|
||||
function formatSubscription(data, customer, format, userAgent = '') {
|
||||
const items = entries(data, customer.id);
|
||||
const items = aggregateEntries(data, customer);
|
||||
if (!items.length) return null;
|
||||
const selected = format === 'auto' ? (/clash|mihomo/i.test(userAgent) ? 'clash' : 'base64') : format;
|
||||
if (selected === 'raw') return { body:items.map((item) => item.clientUri).join('\n') + '\n', contentType:'text/plain; charset=utf-8' };
|
||||
@@ -160,4 +211,4 @@ function formatSubscription(data, customer, format, userAgent = '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
module.exports = { entries, nodeClientUri, clashProxy, makeClash, makeSingBox, makeSurge, formatSubscription };
|
||||
module.exports = { entries, aggregateEntries, nodeClientUri, clashProxy, makeClash, makeSingBox, makeSurge, formatSubscription };
|
||||
|
||||
Reference in New Issue
Block a user