mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/MHSanaei/3x-ui.git
synced 2026-09-20 08:04:05 +08:00
* feat(sub): add SubBalancer model and migration Client-side JSON-subscription balancer row: remark, strategy, member inbound ids, sort order, enabled. Registered in allModels and migrationModels so AutoMigrate and SQLite->Postgres copy pick it up. * feat(sub): add SubBalancer service List/Get/Create/Update/Delete over the sub_balancers table with remark trim, strategy allowlist (leastLoad/leastPing/random) and sort-order floor. Rows are read per request by the subscription builder, so mutations need no xray restart. * feat(sub): add SubBalancer API controller and routes GET/POST /panel/api/sub-balancers, POST /:id (update), DELETE /:id and POST /:id/del alias. inboundIds bind from repeated form keys. Mounted under the /panel/api group so the existing API token + CSRF middleware cover it. * feat(sub): emit client-side balancers in JSON subscription For each enabled balancer, append one config document whose outbounds are the selected inbounds' proxy outbounds retagged under a per-balancer prefix, with routing.balancers + burstObservatory selecting it. Balancer entries interleave with inbound entries by sort order; on equal numbers the balancer follows the inbound. Skipped when disabled or no member outbound is present. * test(sub): cover SubBalancer service and JSON output Service: validation gates (remark/strategy/inbound ids/sort order) and CRUD round-trip. JSON: balancer document shape, sort interleaving with inbounds, disabled/empty skip, and member tag dedup. * feat(sub): add sub-balancers i18n keys pages.settings.subBalancers.* block (menu, title, add, desc, field labels, strategy names, sort-order help, validation messages) added to all 13 locales. * feat(sub): add SubBalancer schema and API queries Zod schema (entity + form, strategy enum, validation messages wired to i18n keys), react-query hooks for list/create/update/delete, and the sub-balancers query key. * feat(sub): add subscription balancers settings tab SubscriptionBalancersTab lists balancers (sort order, remark, strategy, inbound count, enabled toggle, edit/delete) with a form modal (remark, strategy, sort order, multi-select inbounds filtered to multi-client protocols, enabled). Wired into SettingsPage under #subscription-balancers, and the sidebar shows the entry only when JSON subscription is enabled. * test(sub): add SubBalancer form modal test Covers add-mode (no validation errors, confirm with parsed values) and edit-mode (seeds from the balancer, preserves strategy/sort order/enabled). * feat(sub): register sub-balancers in API docs and OpenAPI Adds the sub-balancers endpoint group to endpoints.ts (list/create/update/delete + POST del alias) and regenerates frontend/public/openapi.json from it. * docs: sync openapi.json with frontend docs/public/openapi.json had fallen behind frontend/public/openapi.json (fewer paths/schemas). Copy the current frontend spec so the docs site renders the full API. * docs: add subscription balancers API reference Registers the sub-balancers page (generated MDX) and adds the sub-balancers paths to docs/public/openapi.json so the page renders the list/create/update/delete operations. * feat(sub): accept roundRobin balancer strategy Add roundRobin to the model oneof tag and the service strategy allowlist, alongside leastLoad/leastPing/random. Covered by a service-level create test that fails on the old allowlist. * feat(sub): add roundRobin strategy label pages.settings.subBalancers.strategyRoundRobin added to all 13 locales. * feat(sub): expose roundRobin in balancer form Zod strategy enum, form modal label key, and table strategy colour for roundRobin. * docs(sub): list roundRobin in strategy description The create/update strategy param description now mentions roundRobin alongside the other three. * feat(sub): add subJsonObservatory setting Panel-wide JSON string carrying the burstObservatory ping config (destination, connectivity, interval, sampling, timeout, httpMethod) emitted into client-side balancer docs. Stored like subJsonMux/Rules/FinalMask. * feat(sub): wire observatory config through sub controller WithSUBJsonObservatory option; the controller calls SubJsonService.SetObservatoryConfig after construction. * feat(sub): emit observatory conditionally with configurable probes burstObservatory is emitted only for leastPing/leastLoad; random/roundRobin get none (no fallback, so an observatory would only probe for nothing). Probe params come from the subJsonObservatory setting, falling back to the built-in defaults when empty or partial. Test covers the conditional emit and the override. * feat(sub): add subJsonObservatory to AllSetting model Frontend AllSetting model and Zod schema carry the new panel-wide observatory config string. * feat(sub): add balancer observatory config card New Sub Formats tab editing destination/connectivity/interval/sampling/timeout/httpMethod, stored as JSON in subJsonObservatory. Toggle off clears the setting; the backend then falls back to defaults. * fix(sub): hide save/restart header on sub-balancers tab Sub-balancer mutations are incremental (own CRUD API, no Save, no restart), so the page-wide 'every change needs to be saved / restart the panel' banner is misleading there. The in-tab alert already explains it correctly. * feat(sub): add observatory config i18n keys pages.settings.subBalancers.observatory.* (title, desc, probe field labels and help texts) added to all 13 locales. * feat(sub): regenerate openapi for subJsonObservatory openapigen picks up the new AllSetting field; openapi.json synced into docs. * feat(sub): add observatory tab to sub-balancers Mirrors the Xray Balancers page: two tabs (Balancers + Observatory). Wires allSetting/updateSetting into the tab and adds tabBalancers / tabObservatory labels to all locales. The page Save header is shown again on this tab so the observatory config can be saved. * refactor(sub): drop observatory tab from sub-formats Now that the observatory config lives under sub-balancers, remove the duplicate tab plus its state and defaults from sub-formats. * fix(sub): add missing inboundsCount i18n key The sub-balancers table rendered the raw key path in the Inbounds column because pages.settings.subBalancers.inboundsCount was not defined. Added it to all 13 locales. * test(sub): pin disabled-inbound exclusion from balancer The balancer builds its members from the subscriber's already-filtered entry set, so an inbound disabled for that user can never surface as a member. Adds tests for both shapes (one of several disabled, and the only selected one disabled). * fix(sub): make observatory toggle honest, default connectivity off, add balancer fallback Three coupled defects on the balancer observatory surface, flagged in PR review: - The Observatory Switch wrote '' which the Go side treats as "use built-in defaults", so leastPing/leastLoad still shipped a burstObservatory the admin could no longer see or edit. The observatory is mandatory for these strategies (Xray refuses to start leastPing/leastLoad without one — verified against Xray 26.7), so the switch is relabelled to "customise probe parameters vs built-in defaults" rather than on/off: '' keeps the defaults, a stored JSON overrides them. An info Alert explains this. - Connectivity defaulted to http://www.google.com/generate_204 and an explicit {"connectivity":""} restored it, so the UI's "Leave empty to skip" was unreachable and the direct pre-check was dead on arrival on censored client networks. Default to "" and honour an explicit empty value. - routing.balancers had no fallbackTag, so a leastPing/leastLoad balancer whose probes all fail selects nothing and dispatch fails. Emit fallbackTag pointing at the first member so a probe outage degrades instead of breaking. Also skip balancer entries (kind!=0) in the member scan so a balancer can never match another balancer's row id. Tests cover each fix and fail without it. * fix(sub-balancer): localize controller toasts and reject malformed ids Route the new controller's user-facing messages through I18nWeb so non-English admins get localized toasts like every other controller, and switch parseID to strconv.Atoi rejecting ids < 1 so "12abc" and negative ids no longer coerce to a silent no-op delete that reports success. * fix(sub-balancer): enforce remark length cap server-side The model's validate:"max=256" tag was never enforced (parseSubBalancerForm binds an ad-hoc struct without validate.Struct), so a scripted API client could store an unbounded remark that is emitted verbatim as the remarks field of every affected subscriber's config. Reject len > 256 in validate() to match the frontend Zod cap. * fix(sub-balancer): exclude mtproto from balancer member picker SubJsonService.getConfig has no mtproto case, so an mtproto inbound's first outbound is "direct" and the buildBalancerConfig "tag != proxy" guard drops it — an admin could select it, save without error, and get a balancer that silently omits it (or no document at all). Drop it from the picker and fix the comment. * docs(sub-balancers): add nav entry, fix tab pointer, note mirror scope - Add "subscription-balancers" to the en reference/api meta.json pages array so the new MDX page is reachable from the sidebar (fa/ru/zh have no MDX — gen-openapi.ts emits into en only). - Fix the endpoints.ts section description from "Settings -> Subscription" to "Settings -> Sub Balancers" (the feature's own tab) and regenerate the OpenAPI spec + MDX. - Note in docs/lib/xray/subscription.ts that balancer documents are intentionally out of scope for that mirror. * style(model): trim SubBalancer comment to 2-line cap CLAUDE.md caps committed Go comment blocks at 2 lines; this one was 3. * fix(sub-balancer): parse enabled explicitly and preserve it on partial update parseSubBalancerForm treated any non-"false" value as true (so "bogus" silently enabled) and always overwrote Enabled on update, so a PATCH that omitted the toggle reset a disabled balancer back to enabled. Parse the field with strconv.ParseBool and return *bool: absent means "no change" on update and "true" on create; a malformed value is rejected as 400. Update keeps the stored Enabled when the pointer is nil. * fix(sub-balancer): clear deleted inbound from sub_balancers.InboundIds DelInbound cascaded hosts but left the deleted inbound id in every sub_balancers.InboundIds, so the balancer kept emitting a member no subscriber could resolve — a dangling outbound tag with no proxy behind it. Strip the id inside the existing delete transaction (same shape as the hosts cascade, #5648); with the last member gone the balancer stops emitting. * fix(sub-balancer): return not-found when deleting a missing balancer Delete returned the gorm result error only, which is nil when no row matched, so the controller reported success:true for an id that never existed — a stale UI row looked like a clean delete. Check RowsAffected and return a not-found error on 0 so the toast reflects reality. * style(sub): shorten leastPing/leastLoad observatory comments The observatory-emission guard comment and its test comment ran a few lines long; trim them to a couple of lines each without dropping the invariant that leastPing/leastLoad require a burst observatory. * fix(sub): validate observatory setting instead of silently dropping it SetObservatoryConfig applied whatever survived json.Unmarshal with no checks, so a bad probe URL ("not-a-url"), non-duration interval/timeout, or even unparseable JSON was either silently applied or silently ignored. Validate each field: parse durations with time.ParseDuration, require http(s) URLs for destination/connectivity, and log a warning naming the field and the bad value on every fallback — including the unmarshal error, which was a quiet return. Bad values now keep the built-in defaults instead of leaking into the emitted burstObservatory. * fix(sub): deduplicate burst-observatory defaults across Go and frontend The burst-observatory ping defaults lived in three places that had drifted: Go defaultSubBalancerObservatoryConfig (http probe, sampling 3), the Zod PingConfigSchema, and DEFAULT_BURST_OBSERVATORY (both with a connectivity pre-check URL). Align them to one set: https probe destination, sampling 2, and empty connectivity (skip the direct pre-check). The settings tab now parses the stored JSON through PingConfigSchema and seeds its default from DEFAULT_BURST_OBSERVATORY instead of carrying its own literal. * refactor(sub): extract proxy outbounds once before the balancer loop buildBalancerConfig unmarshalled every inbound document and re-extracted its first outbound on each balancer, so with B balancers and N inbound docs the same document was parsed B*N times. Pull each doc's proxy outbound in a single pre-pass over the entries and cache it per entry; buildBalancerConfig now clones the cached map before retagging, so one parse serves every balancer. Output is byte-for-byte unchanged. * fix(sub): form balancer member tags from the inbound protocol, not tcp→vless balancerTransport derived the bal-N tag suffix from the outbound's transport network and hard-coded tcp→vless, so a vmess/tcp or trojan/tcp member was mislabelled "vless" in every client config — the tag lied about the proxy type. Use the outbound's real protocol as the suffix (bal-1-vmess, bal-1-vless, bal-1-trojan, …) so the tag names the actual proxy; the selector prefix and dedup suffix are unchanged. Update the existing tag assertions and add a vmess case that fails under the old mapping. * fix(sub-balancer): default strategy to random in the create form The create-balancer form seeded strategy to 'leastLoad', but the service validate() defaults an empty strategy to 'random' and the API docs say the default is 'random' — so a freshly opened form showed leastLoad while saving without touching the field silently stored random. Align the form default to 'random' so what the admin sees is what gets persisted. * feat(api-docs): document the SubBalancer response schema The five sub-balancer endpoints carried no responseSchema, so the API docs page rendered them without a typed example. Add example: tags to every SubBalancer field, allow the struct through openapigen, and point the list (responseSchemaArray) and single-row endpoints at 'SubBalancer'. Regenerate the Zod/JSON schemas and OpenAPI doc and mirror openapi.json into docs/. * style(sub-balancer): drop whitespace-only separator lines, add final newline subBalancer.ts and SubBalancerFormModal.tsx used single-space blank lines as separators between statements and had no trailing newline. Replace them with clean empty blank lines and end each file with a newline. * fix(i18n): translate sub-balancer toasts and observatory note The sub-balancer toast messages (list/create/update/delete/invalidId) and the observatory note were left in English across 11 non-English locales (ar, es, fa, id, ja, pt-BR, tr, uk, vi, zh-CN, zh-TW) while every other key in the subBalancers block was already translated. Translate them to match the meaning and terminology of the surrounding keys in each file; the JSON structure and keys are unchanged. * fix(sub-balancer): hide disabled inbounds from the member picker The picker offered every protocol-eligible inbound regardless of its enable flag, but getInboundsBySubId filters `AND inbounds.enable = true`. A disabled member is therefore dropped from every subscriber's entries, and when it was the balancer's only member the balancer document silently stops being emitted — with nothing in the UI explaining why. TestSubJson_BalancerSkippedWhenAll MembersDisabled already documents that backend behavior. Filter the way the sibling client picker has since #5645: hide disabled inbounds, but keep one that is already selected so editing an existing balancer cannot silently drop a member. Drop the `?? []` on the useWatch result so the new useMemo dependency stays referentially stable. * style(sub): trim the balancerMemberSuffix comment to the 2-line cap Comment blocks in committed Go are capped at 2 lines; the name already carries what the function picks, so keep only the why. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
770 lines
23 KiB
TypeScript
770 lines
23 KiB
TypeScript
// Code generated by tools/openapigen. DO NOT EDIT.
|
|
import { z } from 'zod';
|
|
export const GeoKindSchema = z.string();
|
|
export type GeoKind = z.infer<typeof GeoKindSchema>;
|
|
|
|
export const OnlineAPISupportSchema = z.number().int();
|
|
export type OnlineAPISupport = z.infer<typeof OnlineAPISupportSchema>;
|
|
|
|
export const ProcessStateSchema = z.string();
|
|
export type ProcessState = z.infer<typeof ProcessStateSchema>;
|
|
|
|
export const ProtocolSchema = z.string();
|
|
export type Protocol = z.infer<typeof ProtocolSchema>;
|
|
|
|
export const SubLinkProviderSchema = z.unknown();
|
|
export type SubLinkProvider = z.infer<typeof SubLinkProviderSchema>;
|
|
|
|
export const staticEgressResolverSchema = z.string();
|
|
export type staticEgressResolver = z.infer<typeof staticEgressResolverSchema>;
|
|
|
|
export const trafficLocalApplyActionSchema = z.number().int();
|
|
export type trafficLocalApplyAction = z.infer<typeof trafficLocalApplyActionSchema>;
|
|
|
|
export const transportBitsSchema = z.number().int();
|
|
export type transportBits = z.infer<typeof transportBitsSchema>;
|
|
|
|
export const AllSettingSchema = z.object({
|
|
datepicker: z.string(),
|
|
expireDiff: z.number().int().min(0),
|
|
externalTrafficInformEnable: z.boolean(),
|
|
externalTrafficInformURI: z.string(),
|
|
ipLimitAllowlist: z.string(),
|
|
ldapAutoCreate: z.boolean(),
|
|
ldapAutoDelete: z.boolean(),
|
|
ldapBaseDN: z.string(),
|
|
ldapBindDN: z.string(),
|
|
ldapDefaultExpiryDays: z.number().int().min(0),
|
|
ldapDefaultLimitIP: z.number().int().min(0),
|
|
ldapDefaultTotalGB: z.number().int().min(0),
|
|
ldapEnable: z.boolean(),
|
|
ldapFlagField: z.string(),
|
|
ldapHost: z.string(),
|
|
ldapInboundTags: z.string(),
|
|
ldapInsecureSkipVerify: z.boolean(),
|
|
ldapInvertFlag: z.boolean(),
|
|
ldapPassword: z.string(),
|
|
ldapPort: z.number().int().min(0).max(65535),
|
|
ldapSyncCron: z.string(),
|
|
ldapTruthyValues: z.string(),
|
|
ldapUseTLS: z.boolean(),
|
|
ldapUserAttr: z.string(),
|
|
ldapUserFilter: z.string(),
|
|
ldapVlessField: z.string(),
|
|
outboundDownThreshold: z.number().int().min(1).max(100),
|
|
pageSize: z.number().int().min(0).max(1000),
|
|
panelOutbound: z.string(),
|
|
remarkTemplate: z.string(),
|
|
restartXrayOnClientDisable: z.boolean(),
|
|
sessionMaxAge: z.number().int().min(1).max(525600),
|
|
smtpCpu: z.number().int().min(0).max(100),
|
|
smtpEnable: z.boolean(),
|
|
smtpEnabledEvents: z.string(),
|
|
smtpEncryptionType: z.string(),
|
|
smtpFrom: z.string(),
|
|
smtpFromName: z.string(),
|
|
smtpHost: z.string(),
|
|
smtpMemory: z.number().int().min(0).max(100),
|
|
smtpPassword: z.string(),
|
|
smtpPort: z.number().int().min(1).max(65535),
|
|
smtpTo: z.string(),
|
|
smtpUsername: z.string(),
|
|
subAnnounce: z.string(),
|
|
subCertFile: z.string(),
|
|
subClashAutoDetect: z.boolean(),
|
|
subClashEnable: z.boolean(),
|
|
subClashEnableRouting: z.boolean(),
|
|
subClashPath: z.string(),
|
|
subClashRules: z.string(),
|
|
subClashURI: z.string(),
|
|
subClashUserAgentRegex: z.string(),
|
|
subDomain: z.string(),
|
|
subEnable: z.boolean(),
|
|
subEnableRouting: z.boolean(),
|
|
subEncrypt: z.boolean(),
|
|
subHideSettings: z.boolean(),
|
|
subIncyEnableRouting: z.boolean(),
|
|
subIncyRoutingRules: z.string(),
|
|
subJsonAlwaysArray: z.boolean(),
|
|
subJsonAutoDetect: z.boolean(),
|
|
subJsonEnable: z.boolean(),
|
|
subJsonFinalMask: z.string(),
|
|
subJsonMux: z.string(),
|
|
subJsonObservatory: z.string(),
|
|
subJsonPath: z.string(),
|
|
subJsonRules: z.string(),
|
|
subJsonURI: z.string(),
|
|
subJsonUserAgentRegex: z.string(),
|
|
subKeyFile: z.string(),
|
|
subListen: z.string(),
|
|
subPath: z.string(),
|
|
subPort: z.number().int().min(1).max(65535),
|
|
subProfileUrl: z.string(),
|
|
subRoutingRules: z.string(),
|
|
subShowIdentityOnAllLinks: z.boolean(),
|
|
subSupportUrl: z.string(),
|
|
subThemeDir: z.string(),
|
|
subTitle: z.string(),
|
|
subURI: z.string(),
|
|
subUpdates: z.number().int().min(0).max(525600),
|
|
tgBotAPIServer: z.string(),
|
|
tgBotBackup: z.boolean(),
|
|
tgBotChatId: z.string(),
|
|
tgBotEnable: z.boolean(),
|
|
tgBotProxy: z.string(),
|
|
tgBotToken: z.string(),
|
|
tgCpu: z.number().int().min(0).max(100),
|
|
tgEnabledEvents: z.string(),
|
|
tgLang: z.string(),
|
|
tgMemory: z.number().int().min(0).max(100),
|
|
tgRunTime: z.string(),
|
|
timeLocation: z.string(),
|
|
trafficDiff: z.number().int().min(0).max(100),
|
|
trustedProxyCIDRs: z.string(),
|
|
twoFactorEnable: z.boolean(),
|
|
twoFactorToken: z.string(),
|
|
warpUpdateInterval: z.number().int().min(0),
|
|
webBasePath: z.string(),
|
|
webCertFile: z.string(),
|
|
webDomain: z.string(),
|
|
webKeyFile: z.string(),
|
|
webListen: z.string(),
|
|
webPort: z.number().int().min(1).max(65535),
|
|
});
|
|
export type AllSetting = z.infer<typeof AllSettingSchema>;
|
|
|
|
export const AllSettingViewSchema = z.object({
|
|
datepicker: z.string(),
|
|
expireDiff: z.number().int().min(0),
|
|
externalTrafficInformEnable: z.boolean(),
|
|
externalTrafficInformURI: z.string(),
|
|
hasApiToken: z.boolean(),
|
|
hasLdapPassword: z.boolean(),
|
|
hasNordSecret: z.boolean(),
|
|
hasSmtpPassword: z.boolean(),
|
|
hasTgBotToken: z.boolean(),
|
|
hasTwoFactorToken: z.boolean(),
|
|
hasWarpSecret: z.boolean(),
|
|
ipLimitAllowlist: z.string(),
|
|
ldapAutoCreate: z.boolean(),
|
|
ldapAutoDelete: z.boolean(),
|
|
ldapBaseDN: z.string(),
|
|
ldapBindDN: z.string(),
|
|
ldapDefaultExpiryDays: z.number().int().min(0),
|
|
ldapDefaultLimitIP: z.number().int().min(0),
|
|
ldapDefaultTotalGB: z.number().int().min(0),
|
|
ldapEnable: z.boolean(),
|
|
ldapFlagField: z.string(),
|
|
ldapHost: z.string(),
|
|
ldapInboundTags: z.string(),
|
|
ldapInsecureSkipVerify: z.boolean(),
|
|
ldapInvertFlag: z.boolean(),
|
|
ldapPassword: z.string(),
|
|
ldapPort: z.number().int().min(0).max(65535),
|
|
ldapSyncCron: z.string(),
|
|
ldapTruthyValues: z.string(),
|
|
ldapUseTLS: z.boolean(),
|
|
ldapUserAttr: z.string(),
|
|
ldapUserFilter: z.string(),
|
|
ldapVlessField: z.string(),
|
|
outboundDownThreshold: z.number().int().min(1).max(100),
|
|
pageSize: z.number().int().min(0).max(1000),
|
|
panelOutbound: z.string(),
|
|
remarkTemplate: z.string(),
|
|
restartXrayOnClientDisable: z.boolean(),
|
|
sessionMaxAge: z.number().int().min(1).max(525600),
|
|
smtpCpu: z.number().int().min(0).max(100),
|
|
smtpEnable: z.boolean(),
|
|
smtpEnabledEvents: z.string(),
|
|
smtpEncryptionType: z.string(),
|
|
smtpFrom: z.string(),
|
|
smtpFromName: z.string(),
|
|
smtpHost: z.string(),
|
|
smtpMemory: z.number().int().min(0).max(100),
|
|
smtpPassword: z.string(),
|
|
smtpPort: z.number().int().min(1).max(65535),
|
|
smtpTo: z.string(),
|
|
smtpUsername: z.string(),
|
|
subAnnounce: z.string(),
|
|
subCertFile: z.string(),
|
|
subClashAutoDetect: z.boolean(),
|
|
subClashEnable: z.boolean(),
|
|
subClashEnableRouting: z.boolean(),
|
|
subClashPath: z.string(),
|
|
subClashRules: z.string(),
|
|
subClashURI: z.string(),
|
|
subClashUserAgentRegex: z.string(),
|
|
subDomain: z.string(),
|
|
subEnable: z.boolean(),
|
|
subEnableRouting: z.boolean(),
|
|
subEncrypt: z.boolean(),
|
|
subHideSettings: z.boolean(),
|
|
subIncyEnableRouting: z.boolean(),
|
|
subIncyRoutingRules: z.string(),
|
|
subJsonAlwaysArray: z.boolean(),
|
|
subJsonAutoDetect: z.boolean(),
|
|
subJsonEnable: z.boolean(),
|
|
subJsonFinalMask: z.string(),
|
|
subJsonMux: z.string(),
|
|
subJsonObservatory: z.string(),
|
|
subJsonPath: z.string(),
|
|
subJsonRules: z.string(),
|
|
subJsonURI: z.string(),
|
|
subJsonUserAgentRegex: z.string(),
|
|
subKeyFile: z.string(),
|
|
subListen: z.string(),
|
|
subPath: z.string(),
|
|
subPort: z.number().int().min(1).max(65535),
|
|
subProfileUrl: z.string(),
|
|
subRoutingRules: z.string(),
|
|
subShowIdentityOnAllLinks: z.boolean(),
|
|
subSupportUrl: z.string(),
|
|
subThemeDir: z.string(),
|
|
subTitle: z.string(),
|
|
subURI: z.string(),
|
|
subUpdates: z.number().int().min(0).max(525600),
|
|
tgBotAPIServer: z.string(),
|
|
tgBotBackup: z.boolean(),
|
|
tgBotChatId: z.string(),
|
|
tgBotEnable: z.boolean(),
|
|
tgBotProxy: z.string(),
|
|
tgBotToken: z.string(),
|
|
tgCpu: z.number().int().min(0).max(100),
|
|
tgEnabledEvents: z.string(),
|
|
tgLang: z.string(),
|
|
tgMemory: z.number().int().min(0).max(100),
|
|
tgRunTime: z.string(),
|
|
timeLocation: z.string(),
|
|
trafficDiff: z.number().int().min(0).max(100),
|
|
trustedProxyCIDRs: z.string(),
|
|
twoFactorEnable: z.boolean(),
|
|
twoFactorToken: z.string(),
|
|
warpUpdateInterval: z.number().int().min(0),
|
|
webBasePath: z.string(),
|
|
webCertFile: z.string(),
|
|
webDomain: z.string(),
|
|
webKeyFile: z.string(),
|
|
webListen: z.string(),
|
|
webPort: z.number().int().min(1).max(65535),
|
|
});
|
|
export type AllSettingView = z.infer<typeof AllSettingViewSchema>;
|
|
|
|
export const ApiTokenSchema = z.object({
|
|
createdAt: z.number().int(),
|
|
enabled: z.boolean(),
|
|
expiresAt: z.number().int(),
|
|
id: z.number().int(),
|
|
name: z.string(),
|
|
scope: z.string(),
|
|
token: z.string(),
|
|
});
|
|
export type ApiToken = z.infer<typeof ApiTokenSchema>;
|
|
|
|
export const ApiTokenViewSchema = z.object({
|
|
createdAt: z.number().int(),
|
|
enabled: z.boolean(),
|
|
expiresAt: z.number().int(),
|
|
id: z.number().int(),
|
|
name: z.string(),
|
|
scope: z.string(),
|
|
token: z.string().optional(),
|
|
});
|
|
export type ApiTokenView = z.infer<typeof ApiTokenViewSchema>;
|
|
|
|
export const ClientSchema = z.object({
|
|
adTag: z.string().optional(),
|
|
allowedIPs: z.array(z.string()).optional(),
|
|
auth: z.string().optional(),
|
|
comment: z.string(),
|
|
created_at: z.number().int().optional(),
|
|
email: z.string(),
|
|
enable: z.boolean(),
|
|
expiryTime: z.number().int(),
|
|
flow: z.string().optional(),
|
|
group: z.string().optional(),
|
|
id: z.string().optional(),
|
|
keepAlive: z.number().int().optional(),
|
|
limitIp: z.number().int(),
|
|
password: z.string().optional(),
|
|
preSharedKey: z.string().optional(),
|
|
privateKey: z.string().optional(),
|
|
publicKey: z.string().optional(),
|
|
reset: z.number().int(),
|
|
resetDay: z.number().int(),
|
|
resetMax: z.number().int(),
|
|
reverse: z.lazy(() => ClientReverseSchema).nullable().optional(),
|
|
secret: z.string().optional(),
|
|
security: z.string(),
|
|
subId: z.string(),
|
|
tgId: z.number().int(),
|
|
totalGB: z.number().int(),
|
|
trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']).optional(),
|
|
trafficResetDay: z.number().int().min(1).max(31).optional(),
|
|
updated_at: z.number().int().optional(),
|
|
});
|
|
export type Client = z.infer<typeof ClientSchema>;
|
|
|
|
export const ClientInboundSchema = z.object({
|
|
clientId: z.number().int(),
|
|
createdAt: z.number().int(),
|
|
flowOverride: z.string(),
|
|
inboundId: z.number().int(),
|
|
});
|
|
export type ClientInbound = z.infer<typeof ClientInboundSchema>;
|
|
|
|
export const ClientRecordSchema = z.object({
|
|
adTag: z.string(),
|
|
allowedIPs: z.string(),
|
|
auth: z.string(),
|
|
comment: z.string(),
|
|
createdAt: z.number().int(),
|
|
email: z.string(),
|
|
enable: z.boolean(),
|
|
expiryTime: z.number().int(),
|
|
flow: z.string(),
|
|
group: z.string(),
|
|
id: z.number().int(),
|
|
keepAlive: z.number().int(),
|
|
limitHwid: z.number().int(),
|
|
limitIp: z.number().int(),
|
|
password: z.string(),
|
|
preSharedKey: z.string(),
|
|
privateKey: z.string(),
|
|
publicKey: z.string(),
|
|
reset: z.number().int(),
|
|
resetDay: z.number().int(),
|
|
resetMax: z.number().int(),
|
|
reverse: z.unknown(),
|
|
secret: z.string(),
|
|
security: z.string(),
|
|
subId: z.string(),
|
|
tgId: z.number().int(),
|
|
totalGB: z.number().int(),
|
|
trafficReset: z.string(),
|
|
trafficResetDay: z.number().int(),
|
|
updatedAt: z.number().int(),
|
|
uuid: z.string(),
|
|
});
|
|
export type ClientRecord = z.infer<typeof ClientRecordSchema>;
|
|
|
|
export const ClientReverseSchema = z.object({
|
|
tag: z.string(),
|
|
});
|
|
export type ClientReverse = z.infer<typeof ClientReverseSchema>;
|
|
|
|
export const ClientTrafficSchema = z.object({
|
|
down: z.number().int(),
|
|
email: z.string(),
|
|
enable: z.boolean(),
|
|
expiryTime: z.number().int(),
|
|
id: z.number().int(),
|
|
inboundId: z.number().int(),
|
|
lastOnline: z.number().int(),
|
|
lastSubFetch: z.number().int(),
|
|
reset: z.number().int(),
|
|
resetCount: z.number().int(),
|
|
resetDay: z.number().int(),
|
|
resetMax: z.number().int(),
|
|
subId: z.string(),
|
|
total: z.number().int(),
|
|
up: z.number().int(),
|
|
uuid: z.string(),
|
|
});
|
|
export type ClientTraffic = z.infer<typeof ClientTrafficSchema>;
|
|
|
|
export const FallbackParentInfoSchema = z.object({
|
|
masterId: z.number().int(),
|
|
path: z.string().optional(),
|
|
});
|
|
export type FallbackParentInfo = z.infer<typeof FallbackParentInfoSchema>;
|
|
|
|
export const GeoCategorySchema = z.object({
|
|
attributes: z.array(z.string()),
|
|
code: z.string(),
|
|
entries: z.number().int(),
|
|
});
|
|
export type GeoCategory = z.infer<typeof GeoCategorySchema>;
|
|
|
|
export const GeoCategoryPageSchema = z.object({
|
|
items: z.array(z.lazy(() => GeoCategorySchema)),
|
|
total: z.number().int(),
|
|
});
|
|
export type GeoCategoryPage = z.infer<typeof GeoCategoryPageSchema>;
|
|
|
|
export const GeoEntrySchema = z.object({
|
|
kind: z.string(),
|
|
value: z.string(),
|
|
});
|
|
export type GeoEntry = z.infer<typeof GeoEntrySchema>;
|
|
|
|
export const GeoEntryPageSchema = z.object({
|
|
items: z.array(z.lazy(() => GeoEntrySchema)),
|
|
total: z.number().int(),
|
|
});
|
|
export type GeoEntryPage = z.infer<typeof GeoEntryPageSchema>;
|
|
|
|
export const GeoFileSchema = z.object({
|
|
categories: z.number().int(),
|
|
error: z.string().optional(),
|
|
kind: z.lazy(() => GeoKindSchema),
|
|
modifiedAt: z.number().int(),
|
|
name: z.string(),
|
|
size: z.number().int(),
|
|
});
|
|
export type GeoFile = z.infer<typeof GeoFileSchema>;
|
|
|
|
export const GeodataTokenIssueSchema = z.object({
|
|
code: z.string().optional(),
|
|
file: z.string().optional(),
|
|
reason: z.string(),
|
|
token: z.string(),
|
|
});
|
|
export type GeodataTokenIssue = z.infer<typeof GeodataTokenIssueSchema>;
|
|
|
|
export const HistoryOfSeedersSchema = z.object({
|
|
id: z.number().int(),
|
|
seederName: z.string(),
|
|
});
|
|
export type HistoryOfSeeders = z.infer<typeof HistoryOfSeedersSchema>;
|
|
|
|
export const HostSchema = z.object({
|
|
address: z.string(),
|
|
allowInsecure: z.boolean(),
|
|
alpn: z.array(z.string()),
|
|
createdAt: z.number().int(),
|
|
echConfigList: z.string(),
|
|
excludeFromSubTypes: z.array(z.string()),
|
|
finalMask: z.string(),
|
|
fingerprint: z.string(),
|
|
groupId: z.string(),
|
|
hostHeader: z.string(),
|
|
id: z.number().int(),
|
|
inboundId: z.number().int(),
|
|
isDisabled: z.boolean(),
|
|
isHidden: z.boolean(),
|
|
keepSniBlank: z.boolean(),
|
|
mihomoIpVersion: z.enum(['dual', 'ipv4', 'ipv6', 'ipv4-prefer', 'ipv6-prefer']),
|
|
mihomoX25519: z.boolean(),
|
|
muxParams: z.unknown(),
|
|
nodeGuids: z.array(z.string()).optional(),
|
|
overrideSniFromAddress: z.boolean(),
|
|
path: z.string(),
|
|
pinnedPeerCertSha256: z.array(z.string()),
|
|
port: z.number().int().min(0).max(65535),
|
|
remark: z.string().max(256),
|
|
security: z.enum(['same', 'tls', 'none', 'reality']),
|
|
serverDescription: z.string().max(64),
|
|
shuffleHost: z.boolean(),
|
|
sni: z.string(),
|
|
sockoptParams: z.unknown(),
|
|
sortOrder: z.number().int(),
|
|
tags: z.array(z.string()),
|
|
updatedAt: z.number().int(),
|
|
verifyPeerCertByName: z.string(),
|
|
vlessRoute: z.string(),
|
|
});
|
|
export type Host = z.infer<typeof HostSchema>;
|
|
|
|
export const HostGroupSchema = z.object({
|
|
allowInsecure: z.boolean(),
|
|
alpn: z.array(z.string()),
|
|
echConfigList: z.string(),
|
|
excludeFromSubTypes: z.array(z.string()),
|
|
finalMask: z.string(),
|
|
fingerprint: z.string(),
|
|
groupId: z.string(),
|
|
hostHeader: z.string(),
|
|
hosts: z.array(z.string()),
|
|
inboundIds: z.array(z.number().int()),
|
|
isDisabled: z.boolean(),
|
|
isHidden: z.boolean(),
|
|
keepSniBlank: z.boolean(),
|
|
mihomoIpVersion: z.enum(['dual', 'ipv4', 'ipv6', 'ipv4-prefer', 'ipv6-prefer']),
|
|
mihomoX25519: z.boolean(),
|
|
muxParams: z.string(),
|
|
nodeGuids: z.array(z.string()),
|
|
overrideSniFromAddress: z.boolean(),
|
|
path: z.string(),
|
|
pinnedPeerCertSha256: z.array(z.string()),
|
|
port: z.number().int().min(0).max(65535),
|
|
remark: z.string().max(256),
|
|
security: z.enum(['same', 'tls', 'none', 'reality']),
|
|
serverDescription: z.string().max(64),
|
|
shuffleHost: z.boolean(),
|
|
sni: z.string(),
|
|
sockoptParams: z.string(),
|
|
sortOrder: z.number().int(),
|
|
tags: z.array(z.string()),
|
|
verifyPeerCertByName: z.string(),
|
|
vlessRoute: z.string(),
|
|
});
|
|
export type HostGroup = z.infer<typeof HostGroupSchema>;
|
|
|
|
export const InboundSchema = z.object({
|
|
clientStats: z.array(z.lazy(() => ClientTrafficSchema)),
|
|
disableFlow: z.boolean(),
|
|
down: z.number().int(),
|
|
enable: z.boolean(),
|
|
expiryTime: z.number().int(),
|
|
fallbackParent: z.lazy(() => FallbackParentInfoSchema).nullable().optional(),
|
|
id: z.number().int(),
|
|
lastTrafficResetTime: z.number().int(),
|
|
listen: z.string(),
|
|
nodeId: z.number().int().nullable().optional(),
|
|
originNodeGuid: z.string().optional(),
|
|
port: z.number().int().min(0).max(65535),
|
|
protocol: z.enum(['vmess', 'vless', 'trojan', 'shadowsocks', 'wireguard', 'hysteria', 'http', 'mixed', 'tunnel', 'tun', 'mtproto']),
|
|
remark: z.string(),
|
|
settings: z.unknown(),
|
|
shareAddr: z.string(),
|
|
shareAddrStrategy: z.enum(['node', 'listen', 'custom']),
|
|
sniffing: z.unknown(),
|
|
streamSettings: z.unknown(),
|
|
subSortIndex: z.number().int().min(1),
|
|
tag: z.string(),
|
|
total: z.number().int(),
|
|
trafficReset: z.enum(['never', 'hourly', 'daily', 'weekly', 'monthly']),
|
|
trafficResetDay: z.number().int().min(1).max(31),
|
|
up: z.number().int(),
|
|
});
|
|
export type Inbound = z.infer<typeof InboundSchema>;
|
|
|
|
export const InboundClientIpsSchema = z.object({
|
|
clientEmail: z.string(),
|
|
id: z.number().int(),
|
|
ips: z.unknown(),
|
|
});
|
|
export type InboundClientIps = z.infer<typeof InboundClientIpsSchema>;
|
|
|
|
export const InboundFallbackSchema = z.object({
|
|
alpn: z.string(),
|
|
childId: z.number().int(),
|
|
dest: z.string(),
|
|
id: z.number().int(),
|
|
masterId: z.number().int(),
|
|
name: z.string(),
|
|
path: z.string(),
|
|
sortOrder: z.number().int(),
|
|
xver: z.number().int(),
|
|
});
|
|
export type InboundFallback = z.infer<typeof InboundFallbackSchema>;
|
|
|
|
export const InboundOptionSchema = z.object({
|
|
enable: z.boolean(),
|
|
id: z.number().int(),
|
|
listen: z.string().optional(),
|
|
mtprotoDomain: z.string().optional(),
|
|
nodeAddress: z.string().optional(),
|
|
nodeId: z.number().int().nullable().optional(),
|
|
port: z.number().int(),
|
|
protocol: z.string(),
|
|
remark: z.string(),
|
|
shareAddr: z.string().optional(),
|
|
shareAddrStrategy: z.string().optional(),
|
|
ssMethod: z.string(),
|
|
tag: z.string(),
|
|
tlsFlowCapable: z.boolean(),
|
|
wgDns: z.string().optional(),
|
|
wgMtu: z.number().int().optional(),
|
|
wgPublicKey: z.string().optional(),
|
|
});
|
|
export type InboundOption = z.infer<typeof InboundOptionSchema>;
|
|
|
|
export const MsgSchema = z.object({
|
|
msg: z.string(),
|
|
obj: z.unknown(),
|
|
success: z.boolean(),
|
|
});
|
|
export type Msg = z.infer<typeof MsgSchema>;
|
|
|
|
export const NodeSchema = z.object({
|
|
activeCount: z.number().int(),
|
|
address: z.string(),
|
|
allowPrivateAddress: z.boolean(),
|
|
basePath: z.string(),
|
|
clientCount: z.number().int(),
|
|
configDirty: z.boolean(),
|
|
configDirtyAt: z.number().int(),
|
|
cpuPct: z.number(),
|
|
createdAt: z.number().int(),
|
|
depletedCount: z.number().int(),
|
|
disabledCount: z.number().int(),
|
|
enable: z.boolean(),
|
|
guid: z.string(),
|
|
id: z.number().int(),
|
|
inboundCount: z.number().int(),
|
|
inboundSyncMode: z.enum(['all', 'selected']),
|
|
inboundTags: z.array(z.string()),
|
|
lastError: z.string(),
|
|
lastHeartbeat: z.number().int(),
|
|
latencyMs: z.number().int(),
|
|
memPct: z.number(),
|
|
name: z.string(),
|
|
netDown: z.number().int(),
|
|
netUp: z.number().int(),
|
|
onlineCount: z.number().int(),
|
|
outboundTag: z.string(),
|
|
panelVersion: z.string(),
|
|
parentGuid: z.string().optional(),
|
|
pinnedCertSha256: z.string(),
|
|
port: z.number().int().min(1).max(65535),
|
|
remark: z.string(),
|
|
scheme: z.enum(['http', 'https']),
|
|
status: z.string(),
|
|
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
|
|
transitive: z.boolean().optional(),
|
|
updatedAt: z.number().int(),
|
|
uptimeSecs: z.number().int(),
|
|
xrayError: z.string(),
|
|
xrayState: z.string(),
|
|
xrayVersion: z.string(),
|
|
});
|
|
export type Node = z.infer<typeof NodeSchema>;
|
|
|
|
export const NodeMutationRequestSchema = z.object({
|
|
address: z.string(),
|
|
allowPrivateAddress: z.boolean(),
|
|
apiToken: z.string().nullable().optional(),
|
|
basePath: z.string(),
|
|
clearApiToken: z.boolean().optional(),
|
|
enable: z.boolean(),
|
|
id: z.number().int(),
|
|
inboundSyncMode: z.enum(['all', 'selected']),
|
|
inboundTags: z.array(z.string()),
|
|
name: z.string(),
|
|
outboundTag: z.string(),
|
|
pinnedCertSha256: z.string(),
|
|
port: z.number().int().min(1).max(65535),
|
|
remark: z.string(),
|
|
scheme: z.enum(['http', 'https']),
|
|
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
|
|
});
|
|
export type NodeMutationRequest = z.infer<typeof NodeMutationRequestSchema>;
|
|
|
|
export const NodeViewSchema = z.object({
|
|
activeCount: z.number().int(),
|
|
address: z.string(),
|
|
allowPrivateAddress: z.boolean(),
|
|
basePath: z.string(),
|
|
clientCount: z.number().int(),
|
|
configDirty: z.boolean(),
|
|
configDirtyAt: z.number().int(),
|
|
cpuPct: z.number(),
|
|
createdAt: z.number().int(),
|
|
depletedCount: z.number().int(),
|
|
disabledCount: z.number().int(),
|
|
enable: z.boolean(),
|
|
guid: z.string(),
|
|
hasApiToken: z.boolean(),
|
|
id: z.number().int(),
|
|
inboundCount: z.number().int(),
|
|
inboundSyncMode: z.string(),
|
|
inboundTags: z.array(z.string()),
|
|
lastError: z.string(),
|
|
lastHeartbeat: z.number().int(),
|
|
latencyMs: z.number().int(),
|
|
memPct: z.number(),
|
|
name: z.string(),
|
|
netDown: z.number().int(),
|
|
netUp: z.number().int(),
|
|
onlineCount: z.number().int(),
|
|
outboundTag: z.string(),
|
|
panelVersion: z.string(),
|
|
parentGuid: z.string().optional(),
|
|
pinnedCertSha256: z.string(),
|
|
port: z.number().int(),
|
|
remark: z.string(),
|
|
scheme: z.string(),
|
|
status: z.string(),
|
|
tlsVerifyMode: z.string(),
|
|
transitive: z.boolean().optional(),
|
|
updatedAt: z.number().int(),
|
|
uptimeSecs: z.number().int(),
|
|
xrayError: z.string(),
|
|
xrayState: z.string(),
|
|
xrayVersion: z.string(),
|
|
});
|
|
export type NodeView = z.infer<typeof NodeViewSchema>;
|
|
|
|
export const OutboundTrafficsSchema = z.object({
|
|
down: z.number().int(),
|
|
id: z.number().int(),
|
|
tag: z.string(),
|
|
total: z.number().int(),
|
|
up: z.number().int(),
|
|
});
|
|
export type OutboundTraffics = z.infer<typeof OutboundTrafficsSchema>;
|
|
|
|
export const PanelUpdateStatusSchema = z.object({
|
|
exitCode: z.number().int(),
|
|
finishedAt: z.number().int(),
|
|
runId: z.string(),
|
|
state: z.string(),
|
|
});
|
|
export type PanelUpdateStatus = z.infer<typeof PanelUpdateStatusSchema>;
|
|
|
|
export const ProbeResultUISchema = z.object({
|
|
cpuPct: z.number(),
|
|
error: z.string(),
|
|
latencyMs: z.number().int(),
|
|
memPct: z.number(),
|
|
panelVersion: z.string(),
|
|
status: z.string(),
|
|
uptimeSecs: z.number().int(),
|
|
xrayError: z.string(),
|
|
xrayState: z.string(),
|
|
xrayVersion: z.string(),
|
|
});
|
|
export type ProbeResultUI = z.infer<typeof ProbeResultUISchema>;
|
|
|
|
export const RealityScanResultSchema = z.object({
|
|
alpn: z.string(),
|
|
certChainValid: z.boolean(),
|
|
certIssuer: z.string(),
|
|
certSubject: z.string(),
|
|
certValid: z.boolean(),
|
|
curveID: z.string(),
|
|
feasible: z.boolean(),
|
|
h2: z.boolean(),
|
|
host: z.string(),
|
|
ip: z.string(),
|
|
latencyMs: z.number().int(),
|
|
notAfter: z.string(),
|
|
port: z.number().int(),
|
|
privateTarget: z.boolean(),
|
|
reason: z.string(),
|
|
serverNames: z.array(z.string()),
|
|
target: z.string(),
|
|
tls13: z.boolean(),
|
|
tlsVersion: z.string(),
|
|
x25519: z.boolean(),
|
|
});
|
|
export type RealityScanResult = z.infer<typeof RealityScanResultSchema>;
|
|
|
|
export const SettingSchema = z.object({
|
|
id: z.number().int(),
|
|
key: z.string(),
|
|
value: z.string(),
|
|
});
|
|
export type Setting = z.infer<typeof SettingSchema>;
|
|
|
|
export const SubBalancerSchema = z.object({
|
|
createdAt: z.number().int(),
|
|
enabled: z.boolean(),
|
|
id: z.number().int(),
|
|
inboundIds: z.array(z.number().int()),
|
|
remark: z.string().max(256),
|
|
sortOrder: z.number().int().min(1),
|
|
strategy: z.enum(['leastLoad', 'leastPing', 'random', 'roundRobin']),
|
|
updatedAt: z.number().int(),
|
|
});
|
|
export type SubBalancer = z.infer<typeof SubBalancerSchema>;
|
|
|
|
export const UserSchema = z.object({
|
|
id: z.number().int(),
|
|
password: z.string(),
|
|
username: z.string(),
|
|
});
|
|
export type User = z.infer<typeof UserSchema>;
|
|
|