Skip to content

Commit 00a3584

Browse files
committed
merge: auto-push config to node on settings save
2 parents 0bd05ac + f137461 commit 00a3584

4 files changed

Lines changed: 76 additions & 2 deletions

File tree

src/mcp/tools/nodes.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ async function manageNode(args, emit) {
292292
.populate('groups', 'name color');
293293
if (!node) return { error: `Node '${id}' not found`, code: 404 };
294294
await invalidateNodesCache();
295+
296+
// Auto-push config to the node if any config-affecting field changed.
297+
getSyncService().schedulePush(node._id, updates);
298+
295299
logger.info(`[MCP] Updated node ${node.name}`);
296300
return { success: true, node };
297301
}

src/routes/nodes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,10 @@ router.put('/:id', requireScope('nodes:write'), async (req, res) => {
222222
{ $set: { ssh: node.ssh } }
223223
);
224224
}
225-
225+
226+
// Auto-push config to the node if any config-affecting field changed.
227+
require('../services/syncService').schedulePush(node._id, updates);
228+
226229
// Инвалидируем кэш
227230
await invalidateNodesCache();
228231

src/routes/panel/nodes.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ router.post('/nodes/:id', async (req, res) => {
564564

565565
await HyNode.findByIdAndUpdate(nodeId, { $set: updates });
566566

567+
// Auto-push config to the node if any config-affecting field changed.
568+
syncService.schedulePush(nodeId, updates);
569+
567570
// Sync SSH credentials to sibling node on the same IP (if SSH was part of this update)
568571
const sshChanged = updates['ssh.password'] !== undefined
569572
|| updates['ssh.privateKey'] !== undefined
@@ -898,7 +901,10 @@ router.post('/nodes/:id/outbounds', async (req, res) => {
898901
await HyNode.findByIdAndUpdate(req.params.id, {
899902
$set: { outbounds, aclRules },
900903
});
901-
904+
905+
// Auto-push config so ACL/outbound edits take effect without Auto Setup.
906+
syncService.schedulePush(req.params.id, { outbounds, aclRules });
907+
902908
logger.info(`[Panel] Outbounds updated for node: ${node.name} (${outbounds.length} outbounds, ${aclRules.length} ACL rules)`);
903909

904910
res.redirect(`/panel/nodes/${req.params.id}/outbounds?message=` + encodeURIComponent('Outbounds сохранены'));

src/services/syncService.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,33 @@ const selfSignedAgent = new https.Agent({ rejectUnauthorized: false });
3131
// Mark node offline after this many consecutive health check failures (1 check/min)
3232
const HEALTH_FAILURE_THRESHOLD = 3;
3333

34+
// Fields whose change requires regenerating the runtime config on the node.
35+
// Anything not listed here (name, groups, flag, rankingCoefficient, ssh.*, ...)
36+
// is treated as cosmetic and does not trigger an auto-push.
37+
// Dotted keys (e.g. "xray.realityPrivateKey") are matched via their root.
38+
const CONFIG_AFFECTING_FIELDS = new Set([
39+
// Shared
40+
'domain', 'sni', 'port', 'portRange', 'statsPort', 'statsSecret',
41+
'useCustomConfig', 'customConfig',
42+
// Hysteria
43+
'obfs', 'hopInterval', 'acme', 'masquerade', 'bandwidth',
44+
'ignoreClientBandwidth', 'speedTest', 'disableUDP', 'udpIdleTimeout',
45+
'sniff', 'quic', 'resolver', 'acl', 'aclRules', 'outbounds', 'useTlsFiles',
46+
// Xray (any xray.* sub-path triggers regeneration)
47+
'xray',
48+
]);
49+
50+
function hasConfigRelevantUpdates(updates) {
51+
// null/undefined means "unknown" — err on the side of pushing.
52+
if (!updates) return true;
53+
const keys = Object.keys(updates);
54+
if (keys.length === 0) return false;
55+
return keys.some(k => {
56+
const root = k.split('.')[0];
57+
return CONFIG_AFFECTING_FIELDS.has(k) || CONFIG_AFFECTING_FIELDS.has(root);
58+
});
59+
}
60+
3461
class SyncService {
3562
constructor() {
3663
this.isSyncing = false;
@@ -509,6 +536,40 @@ class SyncService {
509536
return this._updateHysteriaNodeConfig(node);
510537
}
511538

539+
/**
540+
* Fire-and-forget config push after a settings save.
541+
*
542+
* Non-blocking: defers to the next tick via setImmediate so the HTTP
543+
* response is flushed before any SSH/Agent round-trip starts.
544+
*
545+
* Silently skipped when:
546+
* - `updates` contains only cosmetic fields (see CONFIG_AFFECTING_FIELDS);
547+
* - node is inactive or acts as a cascade bridge (cascade deploy owns those);
548+
* - node has neither SSH credentials nor an agent token (never set up yet).
549+
*
550+
* @param {string} nodeId - Node _id
551+
* @param {Object} [updates] - $set payload applied to the node. When omitted,
552+
* the push is assumed relevant and runs unconditionally.
553+
*/
554+
schedulePush(nodeId, updates = null) {
555+
if (!hasConfigRelevantUpdates(updates)) return;
556+
setImmediate(async () => {
557+
try {
558+
const node = await HyNode.findById(nodeId);
559+
if (!node || !node.active) return;
560+
if (node.cascadeRole === 'bridge') return;
561+
562+
const hasSsh = !!(node.ssh?.password || node.ssh?.privateKey);
563+
const hasAgent = !!(node.xray && node.xray.agentToken);
564+
if (!hasSsh && !hasAgent) return;
565+
566+
await this.updateNodeConfig(node);
567+
} catch (error) {
568+
logger.warn(`[AutoPush] node ${nodeId}: ${error.message}`);
569+
}
570+
});
571+
}
572+
512573
/**
513574
* Update Hysteria config on a specific node
514575
*/

0 commit comments

Comments
 (0)