merge: p93-agent-register (注册退避重试+脚本探测服务化+面板地址预填)
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions

This commit is contained in:
Muse
2026-10-22 04:55:19 +00:00
19 changed files with 501 additions and 43 deletions
+24 -5
View File
@@ -20,6 +20,7 @@ import type { Msg } from '@/utils';
import { NodeFormSchema, type NodeFormValues, type ProbeResult } from '@/schemas/node';
import { FormField, rhfZodValidate } from '@/components/form/rhf';
import { useOutboundTagGroups } from '@/api/queries/useOutboundTags';
import { currentPanelMasterUrl } from './agentMasterUrl';
import './NodeFormModal.css';
type Mode = 'add' | 'edit';
@@ -248,8 +249,10 @@ export default function NodeFormModal({
if (agentMode && mode === 'add' && obj?.agentToken) {
setAgentToken(obj.agentToken);
setAgentNodeName(result.data.name.trim());
// Default master URL to the current panel origin; user can edit it.
setAgentMasterUrl(window.location.origin);
// Default the master URL to the address this panel is being
// viewed through — origin AND base path. Dropping the base
// path made the agent's registration 404. User can edit it.
setAgentMasterUrl(currentPanelMasterUrl());
// Keep the modal open state; the token dialog shows on top.
} else {
onOpenChange(false);
@@ -264,6 +267,13 @@ export default function NodeFormModal({
if (!submitting) onOpenChange(false);
}
// The install command is only meaningful with a master URL; an empty
// one would produce a command that can never register, so withhold it
// (and say why) instead of letting the user copy a dead command.
const installCmd = agentMasterUrl
? `bash <(curl -fL https://git.881218.xyz/rogue/3x-ui-agent/raw/branch/main/install-agent.sh) --master ${agentMasterUrl} --token ${agentToken ?? ''} --name ${agentNodeName}`
: '';
return (
<>
{messageContextHolder}
@@ -560,9 +570,9 @@ export default function NodeFormModal({
<Button
key="copy"
type="primary"
disabled={!installCmd}
onClick={() => {
const cmd = `bash <(curl -fL https://git.881218.xyz/rogue/3x-ui-agent/raw/branch/main/install-agent.sh) --master ${agentMasterUrl} --token ${agentToken} --name ${agentNodeName}`;
navigator.clipboard.writeText(cmd).then(
navigator.clipboard.writeText(installCmd).then(
() => messageApi.success(t('pages.nodes.agentTokenCopied')),
() => messageApi.error(t('pages.nodes.agentTokenCopyFailed')),
);
@@ -597,9 +607,18 @@ export default function NodeFormModal({
onChange={(e) => setAgentMasterUrl(e.target.value.trim())}
style={{ marginBottom: 12, fontFamily: 'monospace' }}
/>
{!agentMasterUrl && (
<Alert
type="warning"
showIcon
style={{ marginBottom: 12 }}
title={t('pages.nodes.agentMasterUrlRequired')}
/>
)}
<p>{t('pages.nodes.agentTokenHint')}</p>
<Input.TextArea
value={`bash <(curl -fL https://git.881218.xyz/rogue/3x-ui-agent/raw/branch/main/install-agent.sh) --master ${agentMasterUrl} --token ${agentToken} --name ${agentNodeName}`}
value={installCmd}
placeholder={t('pages.nodes.agentMasterUrlRequired')}
readOnly
rows={4}
style={{ fontFamily: 'monospace' }}
@@ -0,0 +1,20 @@
// Default master URL offered in the agent install command.
//
// The agent registers against {master_url}/panel/api/agent/register, and
// the panel itself is usually served under a base path (webBasePath, e.g.
// "/rogue"). Prefilling the bare origin — as the form used to — produced
// an install command whose registration 404'd, leaving the node stuck at
// "unknown" on the panel. The address the operator is viewing the panel
// through already contains the base path, so derive the default from it:
// origin + window.X_UI_BASE_PATH (injected by the panel server). The
// operator can still override it (e.g. a public address instead of a LAN
// or tunnel one).
export function computeDefaultMasterUrl(origin: string, basePath: string | undefined): string {
const trimmed = (basePath || '/').replace(/\/+$/, '');
return origin + trimmed;
}
export function currentPanelMasterUrl(): string {
if (typeof window === 'undefined') return '';
return computeDefaultMasterUrl(window.location.origin, window.X_UI_BASE_PATH);
}
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { computeDefaultMasterUrl } from '@/pages/nodes/agentMasterUrl';
// The install command's --master must be the address agents can register
// against: origin + panel base path. Dropping the base path was the
// production bug — registration 404'd and the node stayed "unknown".
describe('computeDefaultMasterUrl', () => {
it('keeps a base path with trailing slash', () => {
expect(computeDefaultMasterUrl('https://amb.example.com', '/rogue/')).toBe(
'https://amb.example.com/rogue',
);
});
it('keeps a base path without trailing slash', () => {
expect(computeDefaultMasterUrl('https://amb.example.com', '/rogue')).toBe(
'https://amb.example.com/rogue',
);
});
it('root base path yields the bare origin', () => {
expect(computeDefaultMasterUrl('http://192.168.0.1:55889', '/')).toBe('http://192.168.0.1:55889');
});
it('undefined or empty base path yields the bare origin', () => {
expect(computeDefaultMasterUrl('http://10.0.0.2:2053', undefined)).toBe('http://10.0.0.2:2053');
expect(computeDefaultMasterUrl('http://10.0.0.2:2053', '')).toBe('http://10.0.0.2:2053');
});
it('nested base paths survive', () => {
expect(computeDefaultMasterUrl('https://example.com', '/a/b/')).toBe('https://example.com/a/b');
});
});