From d10fa8f3c080cb06a20cecc7770cd6f7c7bc6467 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 18 May 2026 02:11:30 +0200 Subject: [PATCH] feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/api-docs/endpoints.js | 17 +- .../src/pages/clients/ClientBulkAddModal.vue | 17 +- .../src/pages/clients/ClientFormModal.vue | 46 +- frontend/src/pages/clients/useClients.js | 2 +- web/controller/inbound.go | 14 + web/service/inbound.go | 69 ++ web/service/tgbot.go | 751 +++++++----------- 7 files changed, 401 insertions(+), 515 deletions(-) diff --git a/frontend/src/pages/api-docs/endpoints.js b/frontend/src/pages/api-docs/endpoints.js index 401a2ba7..f6dc9a57 100644 --- a/frontend/src/pages/api-docs/endpoints.js +++ b/frontend/src/pages/api-docs/endpoints.js @@ -80,6 +80,13 @@ export const sections = [ response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "up": 0,\n "down": 0,\n "total": 0,\n "remark": "VLESS-443",\n "enable": true,\n "expiryTime": 0,\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "tag": "inbound-443",\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n },\n "clientStats": []\n }\n ]\n}', }, + { + method: 'GET', + path: '/panel/api/inbounds/options', + summary: 'Lightweight picker projection of the authenticated user’s inbounds. Returns only id, remark, protocol, port, and a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.', + response: + '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "remark": "VLESS-443",\n "protocol": "vless",\n "port": 443,\n "tlsFlowCapable": true\n }\n ]\n}', + }, { method: 'GET', path: '/panel/api/inbounds/get/:id', @@ -392,22 +399,22 @@ export const sections = [ { method: 'POST', path: '/panel/api/clients/add', - summary: 'Create a new client and attach it to one or more inbounds in a single call. Body is JSON.', + summary: 'Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password for Trojan/Shadowsocks, auth for Hysteria) are generated server-side when omitted, so callers can send only the universal fields.', params: [ - { name: 'client', in: 'body (json)', type: 'object', desc: 'Client fields: email, subId, id (uuid), password, auth, totalGB, expiryTime, limitIp, comment, enable.' }, + { name: 'client', in: 'body (json)', type: 'object', desc: 'Client fields: email, subId, id (uuid), password, auth, flow, totalGB, expiryTime, limitIp, tgId (numeric Telegram user ID, 0 = none), comment, enable.' }, { name: 'inboundIds', in: 'body (json)', type: 'integer[]', desc: 'Inbound IDs to attach the client to. At least one required.' }, ], - body: '{\n "client": {\n "email": "alice@example.com",\n "totalGB": 53687091200,\n "expiryTime": 1735689600000\n },\n "inboundIds": [3, 5]\n}', + body: '{\n "client": {\n "email": "alice@example.com",\n "totalGB": 53687091200,\n "expiryTime": 1735689600000,\n "tgId": 0,\n "limitIp": 0,\n "enable": true\n },\n "inboundIds": [3, 5]\n}', response: '{\n "success": true,\n "msg": "Client added"\n}', }, { method: 'POST', path: '/panel/api/clients/update/:email', - summary: 'Update an existing client by email. Changes propagate to every attached inbound. Body is the JSON client payload.', + summary: 'Update an existing client by email. Changes propagate to every attached inbound. Body is the JSON client payload — supply the full set of fields you want to keep (the server replaces the row, it does not patch).', params: [ { name: 'email', in: 'path', type: 'string', desc: 'Current client email (unique identifier).' }, ], - body: '{\n "email": "alice@example.com",\n "totalGB": 107374182400,\n "expiryTime": 1767225600000,\n "enable": true\n}', + body: '{\n "email": "alice@example.com",\n "totalGB": 107374182400,\n "expiryTime": 1767225600000,\n "tgId": 123456789,\n "enable": true\n}', response: '{\n "success": true,\n "msg": "Client updated"\n}', }, { diff --git a/frontend/src/pages/clients/ClientBulkAddModal.vue b/frontend/src/pages/clients/ClientBulkAddModal.vue index 799c1827..f0debc61 100644 --- a/frontend/src/pages/clients/ClientBulkAddModal.vue +++ b/frontend/src/pages/clients/ClientBulkAddModal.vue @@ -7,7 +7,6 @@ import { message } from 'ant-design-vue'; import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils'; import DateTimePicker from '@/components/DateTimePicker.vue'; -import { DBInbound } from '@/models/dbinbound.js'; import { TLS_FLOW_CONTROL } from '@/models/inbound.js'; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); @@ -46,10 +45,7 @@ const form = reactive({ const flowCapableIds = computed(() => { const ids = new Set(); for (const row of props.inbounds || []) { - try { - const parsed = new DBInbound(row).toInbound(); - if (parsed.canEnableTlsFlow?.()) ids.add(row.id); - } catch (_e) { /* ignore */ } + if (row?.tlsFlowCapable) ids.add(row.id); } return ids; }); @@ -176,9 +172,8 @@ async function submit() {