From 219197377571a27963584d4aa5f82ff699871ddc Mon Sep 17 00:00:00 2001 From: Shine Date: Sat, 13 Jun 2026 02:24:20 +0800 Subject: [PATCH 01/32] =?UTF-8?q?feat:=20=E6=8C=89=20IMPLEMENTATION.md=20?= =?UTF-8?q?=E5=85=A8=E9=9D=A2=E9=87=8D=E6=9E=84=E5=89=8D=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - Provider 模型支持 inline 类型(Payload 字段,JSON 序列化节点列表) - Provider handler 新增节点 CRUD 端点(GET/POST/PUT/DELETE /providers/:id/nodes) - IsCacheStale / FetchAndCache 对 inline provider 直接跳过 HTTP 拉取 - 订阅 YAML 生成:inline provider 直接读 Payload,http provider 走原有缓存逻辑 - 新增统一规则集 API /api/rule-sets(source_type=external/hosted,复用原两张表) - 新增 GET /api/subscriptions/:id/components,从 rules 自动推断引用的规则集 - CustomConfig handler 移除 Proxies 字段,代理节点改由 inline provider 承载 - 新增数据库迁移 002_provider_inline 前端: - 侧边栏重构为分组导航(来源素材/配置编排/发布)+ 底部流程提示卡片 - 路由:/rule-sets 替代原 /rule-providers + /hosted-rule-sets - 新增 RuleSets.tsx(tab 切分 全部/订阅/私有) - Providers.tsx 支持 http/inline 双类型,inline 显示节点预览 + 节点管理 Dialog - 新增 ProxyNodeForm 组件(8 种协议字段动态渲染,支持 YAML 粘贴解析) - CustomConfigDetail 删除"代理节点"tab,新增"YAML 编辑"tab - ProxyGroupDialog use[] 支持 inline provider 并显示紫色标识 - Subscriptions.tsx 新增组成要素展开面板(懒加载) - types、API 客户端、i18n 全面更新 --- cmd/server/main.go | 13 + frontend/src/App.tsx | 2 + frontend/src/api/custom-configs.ts | 4 +- frontend/src/api/providers.ts | 63 +- frontend/src/api/rule-sets.ts | 31 + frontend/src/api/subscriptions.ts | 7 +- frontend/src/components/layout/Sidebar.tsx | 172 ++++-- .../src/components/proxy/ProxyNodeForm.tsx | 517 +++++++++++++++++ frontend/src/i18n/locales/en.ts | 41 +- frontend/src/i18n/locales/zh.ts | 64 ++- .../src/pages/CustomConfigDetail/index.tsx | 291 ++++------ .../proxies/ProxyDialog.tsx | 467 --------------- .../proxies/ProxyGroupDialog.tsx | 32 +- .../CustomConfigDetail/proxies/proxy-form.ts | 283 --------- .../CustomConfigDetail/shared/constants.ts | 7 +- frontend/src/pages/CustomConfigs.tsx | 4 - frontend/src/pages/Providers.tsx | 544 ++++++++++++++---- frontend/src/pages/RuleSets.tsx | 461 +++++++++++++++ frontend/src/pages/Subscriptions.tsx | 108 +++- frontend/src/types/index.ts | 38 +- internal/handler/custom_config.go | 17 - internal/handler/provider.go | 185 +++++- internal/handler/rule_set.go | 368 ++++++++++++ internal/handler/subscription.go | 99 ++++ internal/model/provider.go | 39 +- .../migrations/002_provider_inline.go | 19 + internal/service/provider.go | 10 +- internal/service/subscription.go | 26 +- 28 files changed, 2689 insertions(+), 1223 deletions(-) create mode 100644 frontend/src/api/rule-sets.ts create mode 100644 frontend/src/components/proxy/ProxyNodeForm.tsx delete mode 100644 frontend/src/pages/CustomConfigDetail/proxies/ProxyDialog.tsx delete mode 100644 frontend/src/pages/CustomConfigDetail/proxies/proxy-form.ts create mode 100644 frontend/src/pages/RuleSets.tsx create mode 100644 internal/handler/rule_set.go create mode 100644 internal/repository/migrations/002_provider_inline.go diff --git a/cmd/server/main.go b/cmd/server/main.go index d4276fd..295fc77 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -97,6 +97,11 @@ func main() { prov.PUT("/:id", handler.UpdateProvider) prov.DELETE("/:id", handler.DeleteProvider) prov.POST("/:id/refresh", handler.RefreshProvider) + // inline provider 节点管理 + prov.GET("/:id/nodes", handler.GetProviderNodes) + prov.POST("/:id/nodes", handler.AddProviderNode) + prov.PUT("/:id/nodes/:nodeIndex", handler.UpdateProviderNode) + prov.DELETE("/:id/nodes/:nodeIndex", handler.DeleteProviderNode) // 配置模板管理 ct := protected.Group("/config-templates") @@ -134,6 +139,13 @@ func main() { cc.GET("/:id/export", handler.ExportCustomConfig) cc.GET("/:id/preview", handler.PreviewCustomConfig) + // 统一规则集 API + rs := protected.Group("/rule-sets") + rs.GET("", handler.ListRuleSets) + rs.POST("", handler.CreateRuleSet) + rs.PUT("/:id", handler.UpdateRuleSet) + rs.DELETE("/:id", handler.DeleteRuleSet) + // 订阅管理 sub := protected.Group("/subscriptions") sub.GET("", handler.ListSubscriptions) @@ -146,6 +158,7 @@ func main() { sub.GET("/:id/restrictions", handler.ListRestrictions) sub.POST("/:id/restrictions", handler.CreateRestriction) sub.DELETE("/:id/restrictions/:rid", handler.DeleteRestriction) + sub.GET("/:id/components", handler.GetSubscriptionComponents) } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c219b35..c935c61 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import { ConfigTemplates } from '@/pages/ConfigTemplates' import { ConfigTemplateDetail } from '@/pages/ConfigTemplateDetail' import { RuleProviders } from '@/pages/RuleProviders' import { HostedRuleSets } from '@/pages/HostedRuleSets' +import { RuleSets } from '@/pages/RuleSets' import { Subscriptions } from '@/pages/Subscriptions' import { SubscriptionDetail } from '@/pages/SubscriptionDetail' import { AccessLogs } from '@/pages/AccessLogs' @@ -34,6 +35,7 @@ export const router = createBrowserRouter([ { path: 'config-templates/:id', element: }, { path: 'rule-providers', element: }, { path: 'hosted-rule-sets', element: }, + { path: 'rule-sets', element: }, { path: 'subscriptions', element: }, { path: 'subscriptions/:id', element: }, { path: 'subscriptions/:id/logs', element: }, diff --git a/frontend/src/api/custom-configs.ts b/frontend/src/api/custom-configs.ts index aa78221..a082529 100644 --- a/frontend/src/api/custom-configs.ts +++ b/frontend/src/api/custom-configs.ts @@ -1,5 +1,5 @@ import client from './client' -import type { CustomConfig, CustomConfigTransferPayload, ProxyNode, ProxyGroup } from '@/types' +import type { CustomConfig, CustomConfigTransferPayload, ProxyGroup } from '@/types' export const customConfigsApi = { list: async (): Promise => { @@ -14,7 +14,6 @@ export const customConfigsApi = { create: async (data: { name: string - proxies?: ProxyNode[] proxy_groups?: ProxyGroup[] rules?: string[] rule_provider_ids?: number[] @@ -28,7 +27,6 @@ export const customConfigsApi = { id: number, data: { name: string - proxies?: ProxyNode[] proxy_groups?: ProxyGroup[] rules?: string[] rule_provider_ids?: number[] diff --git a/frontend/src/api/providers.ts b/frontend/src/api/providers.ts index 78eb310..e5bc617 100644 --- a/frontend/src/api/providers.ts +++ b/frontend/src/api/providers.ts @@ -1,45 +1,60 @@ import client from './client' import type { Provider } from '@/types' +type HttpProviderData = { + name: string + type: 'http' + url: string + user_agent_id?: number + cache_ttl?: number + filter?: string + exclude_filter?: string + prefix?: string + suffix?: string +} + +type InlineProviderData = { + name: string + type: 'inline' + payload?: Record[] +} + +type CreateProviderData = HttpProviderData | InlineProviderData + export const providersApi = { - // 获取订阅源列表 list: async (): Promise => { const res = await client.get<{ code: number; data: Provider[] }>('/providers') return res.data.data }, - - // 创建订阅源 - create: async (data: { - name: string - url: string - user_agent_id?: number - cache_ttl?: number - }): Promise => { + create: async (data: CreateProviderData): Promise => { const res = await client.post<{ code: number; data: Provider }>('/providers', data) return res.data.data }, - - // 更新订阅源 - update: async ( - id: number, - data: { - name: string - url: string - user_agent_id?: number - cache_ttl?: number - } - ): Promise => { + update: async (id: number, data: CreateProviderData): Promise => { const res = await client.put<{ code: number; data: Provider }>(`/providers/${id}`, data) return res.data.data }, - - // 删除订阅源 delete: async (id: number): Promise => { await client.delete(`/providers/${id}`) }, - - // 手动刷新订阅源 refresh: async (id: number): Promise => { await client.post(`/providers/${id}/refresh`) }, + // inline provider 节点管理 + getNodes: async (id: number): Promise[]> => { + const res = await client.get<{ code: number; data: Record[] | null }>(`/providers/${id}/nodes`) + return res.data.data ?? [] + }, + addNode: async (id: number, node: Record): Promise[]> => { + const res = await client.post<{ code: number; data: Record[] | null }>(`/providers/${id}/nodes`, node) + return res.data.data ?? [] + }, + updateNode: async (id: number, nodeIndex: number, node: Record): Promise[]> => { + const res = await client.put<{ code: number; data: Record[] | null }>(`/providers/${id}/nodes/${nodeIndex}`, node) + return res.data.data ?? [] + }, + deleteNode: async (id: number, nodeIndex: number): Promise[]> => { + const res = await client.delete<{ code: number; data: Record[] | null }>(`/providers/${id}/nodes/${nodeIndex}`) + return res.data.data ?? [] + }, } diff --git a/frontend/src/api/rule-sets.ts b/frontend/src/api/rule-sets.ts new file mode 100644 index 0000000..a9bd90c --- /dev/null +++ b/frontend/src/api/rule-sets.ts @@ -0,0 +1,31 @@ +import client from './client' +import type { RuleSet } from '@/types' + +type CreateRuleSetData = { + source_type: 'external' | 'hosted' + name: string + behavior: string + format: string + url?: string + interval?: number + content?: string +} + +export const ruleSetsApi = { + list: async (sourceType?: 'external' | 'hosted'): Promise => { + const params = sourceType ? { source_type: sourceType } : {} + const res = await client.get<{ code: number; data: RuleSet[] }>('/rule-sets', { params }) + return res.data.data + }, + create: async (data: CreateRuleSetData): Promise => { + const res = await client.post<{ code: number; data: RuleSet }>('/rule-sets', data) + return res.data.data + }, + update: async (id: number, data: CreateRuleSetData): Promise => { + const res = await client.put<{ code: number; data: RuleSet }>(`/rule-sets/${id}`, data) + return res.data.data + }, + delete: async (id: number): Promise => { + await client.delete(`/rule-sets/${id}`) + }, +} diff --git a/frontend/src/api/subscriptions.ts b/frontend/src/api/subscriptions.ts index 8e301d8..b25a951 100644 --- a/frontend/src/api/subscriptions.ts +++ b/frontend/src/api/subscriptions.ts @@ -1,6 +1,6 @@ import client from './client' import { withParsedEnabledProviderIds } from '@/domain/subscription/enabledProviderIds' -import type { Subscription, AccessRestriction, AccessLog } from '@/types' +import type { Subscription, AccessRestriction, AccessLog, SubscriptionComponents } from '@/types' interface SubscriptionPayload { name?: string @@ -101,4 +101,9 @@ export const subscriptionsApi = { deleteRestriction: async (subscriptionId: number, restrictionId: number): Promise => { await client.delete(`/subscriptions/${subscriptionId}/restrictions/${restrictionId}`) }, + + getComponents: async (id: number): Promise => { + const res = await client.get<{ code: number; data: SubscriptionComponents }>(`/subscriptions/${id}/components`) + return res.data.data + }, } diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index c157953..993fc73 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -1,6 +1,6 @@ import { NavLink } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { LayoutDashboard, Globe, Bot, Settings2, Link, Settings, FileCode2, BookOpen, Cloud } from 'lucide-react' +import { LayoutDashboard, Globe, Bot, Settings2, Link, Settings, FileCode2, BookOpen } from 'lucide-react' import { cn } from '@/lib/utils' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' @@ -10,16 +10,44 @@ interface NavItem { labelKey: string } -const navItems: NavItem[] = [ - { path: '/dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' }, - { path: '/providers', icon: Globe, labelKey: 'nav.providers' }, - { path: '/custom-configs', icon: Settings2, labelKey: 'nav.customConfigs' }, - { path: '/config-templates', icon: FileCode2, labelKey: 'nav.configTemplates' }, - { path: '/rule-providers', icon: BookOpen, labelKey: 'nav.ruleProviders' }, - { path: '/hosted-rule-sets', icon: Cloud, labelKey: 'nav.hostedRuleSets' }, - { path: '/subscriptions', icon: Link, labelKey: 'nav.subscriptions' }, - { path: '/user-agents', icon: Bot, labelKey: 'nav.userAgents' }, - { path: '/settings', icon: Settings, labelKey: 'nav.settings' }, +interface NavGroup { + label?: string + labelKey?: string + items: NavItem[] +} + +const navGroups: NavGroup[] = [ + { + items: [ + { path: '/dashboard', icon: LayoutDashboard, labelKey: 'nav.dashboard' }, + ], + }, + { + labelKey: 'nav.groupSource', + items: [ + { path: '/providers', icon: Globe, labelKey: 'nav.providers' }, + { path: '/rule-sets', icon: BookOpen, labelKey: 'nav.ruleSets' }, + ], + }, + { + labelKey: 'nav.groupOrchestration', + items: [ + { path: '/custom-configs', icon: Settings2, labelKey: 'nav.customConfigs' }, + { path: '/config-templates', icon: FileCode2, labelKey: 'nav.configTemplates' }, + ], + }, + { + labelKey: 'nav.groupPublish', + items: [ + { path: '/subscriptions', icon: Link, labelKey: 'nav.subscriptions' }, + ], + }, + { + items: [ + { path: '/user-agents', icon: Bot, labelKey: 'nav.userAgents' }, + { path: '/settings', icon: Settings, labelKey: 'nav.settings' }, + ], + }, ] interface SidebarProps { @@ -31,58 +59,82 @@ interface SidebarProps { export function Sidebar({ onNavClick, collapsed = false, labelsVisible = !collapsed }: SidebarProps) { const { t } = useTranslation() - const navContent = ( -