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:
+50
-11
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user