Add multi-server 3x-ui registry with per-panel subscription URLs.

Admins can manage several 3x-ui panels by name and select which one issues invite/user configs via that server's /sub base URL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 03:19:09 +03:00
co-authored by Cursor
parent 83bb73179b
commit 8abe692624
14 changed files with 767 additions and 104 deletions
+50 -11
View File
@@ -47,6 +47,16 @@ def build_subscription_url(settings: dict, sub_id: str) -> str:
return f"{base}/{sub_id}"
def _resolve_settings(settings: dict, panel_id: Optional[str] = None) -> dict:
"""Scope settings to a specific xui panel when panel_id / multi-server registry is used."""
try:
from managers.xui_servers import resolve_panel_settings, ensure_xui_servers
ensure_xui_servers(settings)
return resolve_panel_settings(settings, panel_id)
except Exception:
return settings
class XuiApiError(RuntimeError):
pass
@@ -311,16 +321,18 @@ async def xui_create_vless_config(
name: str,
inbound_id: Optional[int] = None,
expiry_time: int = 0,
panel_id: Optional[str] = None,
) -> dict:
"""Create a VLESS client on 3x-ui and return {client_id, config, subscription_url, links}."""
creds = _settings_creds(settings)
scoped = _resolve_settings(settings, panel_id)
creds = _settings_creds(scoped)
inbound = inbound_id if inbound_id is not None else creds.get('inbound_id')
try:
inbound = int(inbound)
except (TypeError, ValueError):
inbound = 0
async with XuiApi.from_panel_settings(settings) as api:
async with XuiApi.from_panel_settings(scoped) as api:
if not inbound:
vless_inbounds = await api.list_vless_inbounds()
if not vless_inbounds:
@@ -355,7 +367,7 @@ async def xui_create_vless_config(
expiry_time=int(expiry_time or 0),
)
sub_url = build_subscription_url(settings, created.get('sub_id') or '')
sub_url = build_subscription_url(scoped, created.get('sub_id') or '')
# Prefer subscription URL as the main share string when configured
if sub_url:
created['subscription_url'] = sub_url
@@ -363,11 +375,35 @@ async def xui_create_vless_config(
else:
created['subscription_url'] = ''
created['inbound_id'] = inbound
created['panel_id'] = panel_id or ''
return created
async def xui_get_config(settings: dict, email: str) -> str:
async with XuiApi.from_panel_settings(settings) as api:
async def xui_get_config(settings: dict, email: str, panel_id: Optional[str] = None) -> str:
scoped = _resolve_settings(settings, panel_id)
async with XuiApi.from_panel_settings(scoped) as api:
# Prefer subscription URL when subId is available on the client
try:
for inbound in await api.list_inbounds():
if not isinstance(inbound, dict):
continue
settings_raw = inbound.get('settings') or '{}'
if isinstance(settings_raw, str):
try:
settings_obj = json.loads(settings_raw)
except Exception:
settings_obj = {}
else:
settings_obj = settings_raw if isinstance(settings_raw, dict) else {}
for c in settings_obj.get('clients') or []:
if isinstance(c, dict) and (c.get('email') or '') == email:
sub_id = (c.get('subId') or c.get('sub_id') or '').strip()
sub_url = build_subscription_url(scoped, sub_id)
if sub_url:
return sub_url
except Exception as e:
logger.warning('Could not resolve subscription URL for %s: %s', email, e)
links = await api.get_client_links(email)
vless = next((u for u in links if u.startswith('vless://')), None)
if vless:
@@ -377,16 +413,19 @@ async def xui_get_config(settings: dict, email: str) -> str:
raise XuiApiError(f'No share links for 3x-ui client {email}')
async def xui_delete_client(settings: dict, email: str) -> None:
async with XuiApi.from_panel_settings(settings) as api:
async def xui_delete_client(settings: dict, email: str, panel_id: Optional[str] = None) -> None:
scoped = _resolve_settings(settings, panel_id)
async with XuiApi.from_panel_settings(scoped) as api:
await api.delete_client(email)
async def xui_toggle_client(settings: dict, email: str, enable: bool) -> None:
async with XuiApi.from_panel_settings(settings) as api:
async def xui_toggle_client(settings: dict, email: str, enable: bool, panel_id: Optional[str] = None) -> None:
scoped = _resolve_settings(settings, panel_id)
async with XuiApi.from_panel_settings(scoped) as api:
await api.set_client_enabled(email, enable)
async def xui_list_vless_inbounds(settings: dict) -> list:
async with XuiApi.from_panel_settings(settings) as api:
async def xui_list_vless_inbounds(settings: dict, panel_id: Optional[str] = None) -> list:
scoped = _resolve_settings(settings, panel_id)
async with XuiApi.from_panel_settings(scoped) as api:
return await api.list_vless_inbounds()