فهرست منبع

feat(web): add dashboard,log,model plugin manager (#237)

* add plugin and dashboard

* add log
limbo 6 ماه پیش
والد
کامیت
818217e880
36فایلهای تغییر یافته به همراه4264 افزوده شده و 128 حذف شده
  1. 165 81
      web/openapi.txt
  2. 5 0
      web/package.json
  3. 439 0
      web/pnpm-lock.yaml
  4. 195 3
      web/public/locales/en/translation.json
  5. 195 3
      web/public/locales/zh/translation.json
  6. 67 0
      web/src/api/dashboard.ts
  7. 78 0
      web/src/api/log.ts
  8. 65 0
      web/src/components/common/Date.tsx
  9. 86 0
      web/src/components/common/DateRangePicker.tsx
  10. 64 0
      web/src/components/common/error/validationErrorDisplay.tsx
  11. 2 2
      web/src/components/layout/RootLayOut.tsx
  12. 1 1
      web/src/components/ui/button.tsx
  13. 73 0
      web/src/components/ui/calendar.tsx
  14. 92 0
      web/src/components/ui/echarts.tsx
  15. 46 0
      web/src/components/ui/popover.tsx
  16. 65 0
      web/src/feature/log/components/JsonViewer.tsx
  17. 187 0
      web/src/feature/log/components/LogFilters.tsx
  18. 397 0
      web/src/feature/log/components/LogTable.tsx
  19. 21 0
      web/src/feature/log/hooks.ts
  20. 2 2
      web/src/feature/model/components/ModelDialog.tsx
  21. 532 20
      web/src/feature/model/components/ModelForm.tsx
  22. 82 12
      web/src/feature/model/components/ModelTable.tsx
  23. 160 0
      web/src/feature/monitor/components/MetricsCards.tsx
  24. 519 0
      web/src/feature/monitor/components/MonitorCharts.tsx
  25. 189 0
      web/src/feature/monitor/components/MonitorFilters.tsx
  26. 21 0
      web/src/feature/monitor/hooks.ts
  27. 23 1
      web/src/pages/auth/login.tsx
  28. 91 0
      web/src/pages/log/page.tsx
  29. 88 0
      web/src/pages/monitor/page.tsx
  30. 5 3
      web/src/routes/config.tsx
  31. 46 0
      web/src/types/dashboard.ts
  32. 94 0
      web/src/types/log.ts
  33. 74 0
      web/src/types/model.ts
  34. 13 0
      web/src/validation/dashboard.ts
  35. 16 0
      web/src/validation/log.ts
  36. 66 0
      web/src/validation/model.ts

+ 165 - 81
web/openapi.txt

@@ -1,100 +1,184 @@
-/api/token/${group}?auto_create_group=true
-描述信息:创建 token
-方法:post
-参数:
-path group: 组名
-body
-{
-    "name": "token1"
-}
-
-响应结构:
-{
-    "data":{
-        "key": "xE7Lz",
-        "name": "liangfen",
-        "group": "ns-ovore4kv",
-        "subnets": null,
-        "models": null,
-        "status": 1,
-        "id": 1241,
-        "quota": 0,
-        "used_amount": 0,
-        "request_count": 0,
-        "created_at": 1744798413078,
-        "expired_at": -62135596800000,
-        "accessed_at": -62135596800000
-     },
-    "message": "",
-    "success": true
-}
-
----
-
-/api/tokens/search
-描述信息:获取 token
+/api/log/{group}/search
+描述信息:获取 组级别 log 数据
 方法:get
 参数:
 query
-p 页码
-per_page 每页数量
+page 页码 number
+per_page 每页条数 number
+model_name  model name string
+token_name  token name string 
+start_timestamp 开始时间 timestamp 毫秒级别时间戳
+end_timestamp 结束时间 timestamp 毫秒级别时间戳
+code_type 状态 'all' | 'success' | 'error' string
+
 
 响应结构:
 {
-	"data": {
-		"tokens": [
+  "data": {
+    "channels": [
+      0
+    ],
+    "logs": [
       {
-        "key": "xE7Lz",
-        "name": "liangfen",
-        "group": "ns-ovore4kv",
-        "subnets": null,
-        "models": null,
-        "status": 1,
-        "id": 1241,
-        "quota": 0,
+        "channel": 0,
+        "code": 0,
+        "content": "string",
+        "created_at": "string",
+        "endpoint": "string",
+        "group": "string",
+        "id": 0,
+        "ip": "string",
+        "metadata": {
+          "additionalProp1": "string",
+          "additionalProp2": "string",
+          "additionalProp3": "string"
+        },
+        "mode": 0,
+        "model": "string",
+        "price": {
+          "cache_creation_price": 0,
+          "cache_creation_price_unit": 0,
+          "cached_price": 0,
+          "cached_price_unit": 0,
+          "image_input_price": 0,
+          "image_input_price_unit": 0,
+          "input_price": 0,
+          "input_price_unit": 0,
+          "output_price": 0,
+          "output_price_unit": 0,
+          "per_request_price": 0,
+          "thinking_mode_output_price": 0,
+          "thinking_mode_output_price_unit": 0,
+          "web_search_price": 0,
+          "web_search_price_unit": 0
+        },
+        "request_at": "string",
+        "request_detail": {
+          "id": 0,
+          "log_id": 0,
+          "request_body": "string",
+          "request_body_truncated": true,
+          "response_body": "string",
+          "response_body_truncated": true
+        },
+        "request_id": "string",
+        "retry_at": "string",
+        "retry_times": 0,
+        "token_id": 0,
+        "token_name": "string",
+        "ttfb_milliseconds": 0,
+        "usage": {
+          "cache_creation_tokens": 0,
+          "cached_tokens": 0,
+          "image_input_tokens": 0,
+          "input_tokens": 0,
+          "output_tokens": 0,
+          "reasoning_tokens": 0,
+          "total_tokens": 0,
+          "web_search_count": 0
+        },
         "used_amount": 0,
-        "request_count": 0,
-        "created_at": 1744798413078,
-        "expired_at": -62135596800000,
-        "accessed_at": -62135596800000
-     }
+        "user": "string"
+      }
+    ],
+    "models": [
+      "string"
+    ],
+    "token_names": [
+      "string"
     ],
-		"total": 0
-	},
-	"success": true
+    "total": 0
+  },
+  "message": "string",
+  "success": true
 }
 
 ---
 
-/api/tokens/:id
-描述信息:删除 token
-方法:delete
+/api/logs/search
+描述信息:获取 全部 log 数据
+方法:get
 参数:
-id: token id
-
-响应结构:
-{
-	"data": null,
-	"message": "",
-	"success": true
-}
+query
+page 页码 number
+per_page 每页条数 number
+model_name  model name string
+start_timestamp 开始时间 timestamp 毫秒级别时间戳
+end_timestamp 结束时间 timestamp 毫秒级别时间戳
+code_type 状态 'all' | 'success' | 'error' string
 
----
-
-/api/tokens/:id/status
-描述信息:更新 token 状态
-方法:post
-参数:
-path id: token id
-body
-{
-    "status": 1
-}
-status 状态 1 启用 2 禁用
 
 响应结构:
 {
-	"data": null,
-	"message": "",
-	"success": true
+  "data": {
+    "channels": [
+      0
+    ],
+    "logs": [
+      {
+        "channel": 0,
+        "code": 0,
+        "content": "string",
+        "created_at": "string",
+        "endpoint": "string",
+        "group": "string",
+        "id": 0,
+        "ip": "string",
+        "metadata": {
+          "additionalProp1": "string",
+          "additionalProp2": "string",
+          "additionalProp3": "string"
+        },
+        "mode": 0,
+        "model": "string",
+        "price": {
+          "cache_creation_price": 0,
+          "cache_creation_price_unit": 0,
+          "cached_price": 0,
+          "cached_price_unit": 0,
+          "image_input_price": 0,
+          "image_input_price_unit": 0,
+          "input_price": 0,
+          "input_price_unit": 0,
+          "output_price": 0,
+          "output_price_unit": 0,
+          "per_request_price": 0,
+          "thinking_mode_output_price": 0,
+          "thinking_mode_output_price_unit": 0,
+          "web_search_price": 0,
+          "web_search_price_unit": 0
+        },
+        "request_at": "string",
+        "request_detail": {
+          "id": 0,
+          "log_id": 0,
+          "request_body": "string",
+          "request_body_truncated": true,
+          "response_body": "string",
+          "response_body_truncated": true
+        },
+        "request_id": "string",
+        "retry_at": "string",
+        "retry_times": 0,
+        "token_id": 0,
+        "token_name": "string",
+        "ttfb_milliseconds": 0,
+        "usage": {
+          "cache_creation_tokens": 0,
+          "cached_tokens": 0,
+          "image_input_tokens": 0,
+          "input_tokens": 0,
+          "output_tokens": 0,
+          "reasoning_tokens": 0,
+          "total_tokens": 0,
+          "web_search_count": 0
+        },
+        "used_amount": 0,
+        "user": "string"
+      }
+    ],
+    "total": 0
+  },
+  "message": "string",
+  "success": true
 }

+ 5 - 0
web/package.json

@@ -17,6 +17,7 @@
     "@radix-ui/react-dialog": "^1.1.11",
     "@radix-ui/react-dropdown-menu": "^2.1.12",
     "@radix-ui/react-label": "^2.1.4",
+    "@radix-ui/react-popover": "^1.1.14",
     "@radix-ui/react-select": "^2.2.2",
     "@radix-ui/react-separator": "^1.1.4",
     "@radix-ui/react-slot": "^1.2.0",
@@ -29,7 +30,9 @@
     "axios": "^1.9.0",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
+    "date-fns": "^4.1.0",
     "downshift": "^9.0.9",
+    "echarts": "^5.6.0",
     "i18next": "^25.0.1",
     "i18next-browser-languagedetector": "^8.0.5",
     "i18next-http-backend": "^3.0.2",
@@ -37,9 +40,11 @@
     "motion": "^12.9.1",
     "next-themes": "^0.4.6",
     "react": "^19.0.0",
+    "react-day-picker": "^8.10.1",
     "react-dom": "^19.0.0",
     "react-hook-form": "^7.56.1",
     "react-i18next": "^15.5.1",
+    "react-json-view": "^1.21.3",
     "react-router": "^7.5.1",
     "react-syntax-highlighter": "^15.6.1",
     "sonner": "^2.0.3",

+ 439 - 0
web/pnpm-lock.yaml

@@ -29,6 +29,9 @@ importers:
       '@radix-ui/react-label':
         specifier: ^2.1.4
         version: 2.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-popover':
+        specifier: ^1.1.14
+        version: 1.1.14(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
       '@radix-ui/react-select':
         specifier: ^2.2.2
         version: 2.2.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
@@ -65,9 +68,15 @@ importers:
       clsx:
         specifier: ^2.1.1
         version: 2.1.1
+      date-fns:
+        specifier: ^4.1.0
+        version: 4.1.0
       downshift:
         specifier: ^9.0.9
         version: 9.0.9([email protected])
+      echarts:
+        specifier: ^5.6.0
+        version: 5.6.0
       i18next:
         specifier: ^25.0.1
         version: 25.0.1([email protected])
@@ -89,6 +98,9 @@ importers:
       react:
         specifier: ^19.0.0
         version: 19.1.0
+      react-day-picker:
+        specifier: ^8.10.1
+        version: 8.10.1([email protected])([email protected])
       react-dom:
         specifier: ^19.0.0
         version: 19.1.0([email protected])
@@ -98,6 +110,9 @@ importers:
       react-i18next:
         specifier: ^15.5.1
         version: 15.5.1([email protected]([email protected]))([email protected]([email protected]))([email protected])([email protected])
+      react-json-view:
+        specifier: ^1.21.3
+        version: 1.21.3(@types/[email protected])([email protected]([email protected]))([email protected])
       react-router:
         specifier: ^7.5.1
         version: 7.5.1([email protected]([email protected]))([email protected])
@@ -444,6 +459,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-V7ODUt4mUoJTe3VUxZw6nfURxaPALVqmDQh501YmaQsk3D8AZQrOPRnfKn4H7JGDLBc0KqLhT94H79nV88ppNg==}
     peerDependencies:
@@ -523,6 +551,19 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==}
     peerDependencies:
@@ -571,6 +612,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
     peerDependencies:
@@ -606,6 +660,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==}
     peerDependencies:
@@ -619,6 +686,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==}
     peerDependencies:
@@ -632,6 +712,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
     peerDependencies:
@@ -658,6 +751,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==}
     peerDependencies:
@@ -706,6 +812,15 @@ packages:
       '@types/react':
         optional: true
 
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   '@radix-ui/[email protected]':
     resolution: {integrity: sha512-7Z8n6L+ifMIIYZ83f28qWSceUpkXuslI2FJ34+kDMTiyj91ENdpdQ7VCidrzj5JfwfZTeano/BnGBbu/jqa5rQ==}
     peerDependencies:
@@ -1227,6 +1342,9 @@ packages:
     resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
     engines: {node: '>=10'}
 
+  [email protected]:
+    resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
+
   [email protected]:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
 
@@ -1236,6 +1354,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
+  [email protected]:
+    resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==}
+
   [email protected]:
     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
 
@@ -1298,6 +1419,9 @@ packages:
     resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
     engines: {node: '>=18'}
 
+  [email protected]:
+    resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
+
   [email protected]:
     resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
 
@@ -1308,6 +1432,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
+  [email protected]:
+    resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
+
   [email protected]:
     resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
     engines: {node: '>=6.0'}
@@ -1340,6 +1467,9 @@ packages:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
 
+  [email protected]:
+    resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
+
   [email protected]:
     resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
     engines: {node: '>=10.13.0'}
@@ -1441,6 +1571,15 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
 
+  [email protected]:
+    resolution: {integrity: sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==}
+
   [email protected]:
     resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==}
     peerDependencies:
@@ -1468,6 +1607,11 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==}
+    peerDependencies:
+      react: ^15.0.2 || ^16.0.0 || ^17.0.0
+
   [email protected]:
     resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
     engines: {node: '>=4.0'}
@@ -1724,6 +1868,12 @@ packages:
     resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
     engines: {node: '>=10'}
 
+  [email protected]:
+    resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==}
+
   [email protected]:
     resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
 
@@ -1870,6 +2020,9 @@ packages:
     resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
     engines: {node: '>=6'}
 
+  [email protected]:
+    resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
+
   [email protected]:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
@@ -1883,9 +2036,21 @@ packages:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
 
+  [email protected]:
+    resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==}
+
   [email protected]:
     resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
 
+  [email protected]:
+    resolution: {integrity: sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==}
+    peerDependencies:
+      date-fns: ^2.28.0 || ^3.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
   [email protected]:
     resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
     peerDependencies:
@@ -1919,6 +2084,15 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
 
+  [email protected]:
+    resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==}
+    peerDependencies:
+      react: ^17.0.0 || ^16.3.0 || ^15.5.4
+      react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4
+
+  [email protected]:
+    resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
+
   [email protected]:
     resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
     engines: {node: '>=10'}
@@ -1964,6 +2138,12 @@ packages:
     peerDependencies:
       react: '>= 0.14.0'
 
+  [email protected]:
+    resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
   [email protected]:
     resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
     engines: {node: '>=0.10.0'}
@@ -2001,6 +2181,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
 
+  [email protected]:
+    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
+
   [email protected]:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -2057,6 +2240,9 @@ packages:
     peerDependencies:
       typescript: '>=4.8.4'
 
+  [email protected]:
+    resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
   [email protected]:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
@@ -2082,6 +2268,10 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  [email protected]:
+    resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==}
+    hasBin: true
+
   [email protected]:
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
 
@@ -2098,6 +2288,33 @@ packages:
       '@types/react':
         optional: true
 
+  [email protected]:
+    resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
   [email protected]:
     resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
     engines: {node: '>=10'}
@@ -2189,6 +2406,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
+
   [email protected]:
     resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
     engines: {node: '>=12.20.0'}
@@ -2406,6 +2626,15 @@ snapshots:
       '@types/react': 19.1.2
       '@types/react-dom': 19.1.2(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
@@ -2487,6 +2716,19 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.1.2
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-escape-keydown': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/primitive': 1.1.2
@@ -2532,6 +2774,17 @@ snapshots:
       '@types/react': 19.1.2
       '@types/react-dom': 19.1.2(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected])([email protected])':
     dependencies:
       '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
@@ -2574,6 +2827,29 @@ snapshots:
       '@types/react': 19.1.2
       '@types/react-dom': 19.1.2(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.2
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.10(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-focus-guards': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-focus-scope': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-popper': 1.2.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-portal': 1.1.9(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      aria-hidden: 1.2.4
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-remove-scroll: 2.6.3(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@floating-ui/react-dom': 2.1.2([email protected]([email protected]))([email protected])
@@ -2592,6 +2868,24 @@ snapshots:
       '@types/react': 19.1.2
       '@types/react-dom': 19.1.2(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@floating-ui/react-dom': 2.1.2([email protected]([email protected]))([email protected])
+      '@radix-ui/react-arrow': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-rect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/rect': 1.1.1
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/react-primitive': 2.1.0(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
@@ -2602,6 +2896,16 @@ snapshots:
       '@types/react': 19.1.2
       '@types/react-dom': 19.1.2(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
@@ -2621,6 +2925,15 @@ snapshots:
       '@types/react': 19.1.2
       '@types/react-dom': 19.1.2(@types/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+      '@types/react-dom': 19.1.2(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/primitive': 1.1.2
@@ -2683,6 +2996,13 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.1.2
 
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     dependencies:
       '@radix-ui/primitive': 1.1.2
@@ -3125,6 +3445,8 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -3137,6 +3459,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       balanced-match: 1.0.2
@@ -3192,6 +3516,12 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      node-fetch: 2.7.0
+    transitivePeerDependencies:
+      - encoding
+
   [email protected]:
     dependencies:
       node-fetch: 2.7.0
@@ -3206,6 +3536,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       ms: 2.1.3
@@ -3233,6 +3565,11 @@ snapshots:
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  [email protected]:
+    dependencies:
+      tslib: 2.3.0
+      zrender: 5.6.1
+
   [email protected]:
     dependencies:
       graceful-fs: 4.2.11
@@ -3382,6 +3719,26 @@ snapshots:
     dependencies:
       format: 0.2.2
 
+  [email protected]:
+    dependencies:
+      fbjs: 3.0.5
+    transitivePeerDependencies:
+      - encoding
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      cross-fetch: 3.2.0
+      fbjs-css-vars: 1.0.2
+      loose-envify: 1.4.0
+      object-assign: 4.1.1
+      promise: 7.3.1
+      setimmediate: 1.0.5
+      ua-parser-js: 1.0.40
+    transitivePeerDependencies:
+      - encoding
+
   [email protected]([email protected]):
     optionalDependencies:
       picomatch: 4.0.2
@@ -3406,6 +3763,14 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]([email protected]):
+    dependencies:
+      fbemitter: 3.0.0
+      fbjs: 3.0.5
+      react: 19.1.0
+    transitivePeerDependencies:
+      - encoding
+
   [email protected]: {}
 
   [email protected]:
@@ -3617,6 +3982,10 @@ snapshots:
     dependencies:
       p-locate: 5.0.0
 
+  [email protected]: {}
+
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -3738,6 +4107,10 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      asap: 2.0.6
+
   [email protected]:
     dependencies:
       loose-envify: 1.4.0
@@ -3752,8 +4125,22 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      base16: 1.0.0
+      lodash.curry: 4.1.1
+      lodash.flow: 3.5.0
+      pure-color: 1.3.0
+
+  [email protected]([email protected])([email protected]):
+    dependencies:
+      date-fns: 4.1.0
+      react: 19.1.0
+
   [email protected]([email protected]):
     dependencies:
       react: 19.1.0
@@ -3777,6 +4164,20 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected](@types/[email protected])([email protected]([email protected]))([email protected]):
+    dependencies:
+      flux: 4.0.4([email protected])
+      react: 19.1.0
+      react-base16-styling: 0.6.0
+      react-dom: 19.1.0([email protected])
+      react-lifecycles-compat: 3.0.4
+      react-textarea-autosize: 8.5.9(@types/[email protected])([email protected])
+    transitivePeerDependencies:
+      - '@types/react'
+      - encoding
+
+  [email protected]: {}
+
   [email protected](@types/[email protected])([email protected]):
     dependencies:
       react: 19.1.0
@@ -3823,6 +4224,15 @@ snapshots:
       react: 19.1.0
       refractor: 3.6.0
 
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      '@babel/runtime': 7.27.0
+      react: 19.1.0
+      use-composed-ref: 1.4.0(@types/[email protected])([email protected])
+      use-latest: 1.3.0(@types/[email protected])([email protected])
+    transitivePeerDependencies:
+      - '@types/react'
+
   [email protected]: {}
 
   [email protected]:
@@ -3873,6 +4283,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       shebang-regex: 3.0.0
@@ -3915,6 +4327,8 @@ snapshots:
     dependencies:
       typescript: 5.7.3
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}
@@ -3937,6 +4351,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -3950,6 +4366,25 @@ snapshots:
     optionalDependencies:
       '@types/react': 19.1.2
 
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.2
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+      use-isomorphic-layout-effect: 1.2.1(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.2
+
   [email protected](@types/[email protected])([email protected]):
     dependencies:
       detect-node-es: 1.1.0
@@ -4006,6 +4441,10 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      tslib: 2.3.0
+
   [email protected](@types/[email protected])([email protected])([email protected]([email protected])):
     optionalDependencies:
       '@types/react': 19.1.2

+ 195 - 3
web/public/locales/en/translation.json

@@ -12,7 +12,85 @@
   "common": {
     "noResult": "No Data",
     "copied": "Copied to clipboard",
-    "copyFailed": "Copy failed"
+    "copyFailed": "Copy failed",
+    "validation": {
+      "error": {
+        "title": "Form Validation Error"
+      }
+    },
+    "global": "Global",
+    "loading": "Loading...",
+    "selectDateRange": "Select date range"
+  },
+  "error": {
+    "loading": "Failed to load data",
+    "loadingLogs": "Failed to load logs",
+    "retry": "Retry",
+    "viewDetails": "View Details",
+    "hideDetails": "Hide Details",
+    "message": "Error Message",
+    "code": "Error Code",
+    "network": "Network Error",
+    "networkDescription": "Please check your network connection and try again",
+    "server": "Server Error",
+    "serverDescription": "The server encountered an error while processing your request",
+    "client": "Client Error",
+    "clientDescription": "There was an issue with your request",
+    "validation": "Validation Error",
+    "validationDescription": "Please check your input and try again",
+    "unauthorized": "Unauthorized",
+    "unauthorizedDescription": "You don't have permission to access this resource",
+    "notFound": "Not Found",
+    "notFoundDescription": "The requested resource was not found",
+    "unknown": "Unknown Error",
+    "unknownDescription": "An unexpected error occurred"
+  },
+  "monitor": {
+    "title": "Monitor Dashboard",
+    "noData": "No Data",
+    "noDataDescription": "No data found for the current time range. Please try adjusting the filter conditions or selecting a different time range.",
+    "filters": {
+      "title": "Filters",
+      "key": "API Key",
+      "keyPlaceholder": "Enter API Key name to filter",
+      "model": "Model",
+      "modelPlaceholder": "Enter model name",
+      "dateRange": "Date Range",
+      "dateRangePlaceholder": "Select date range",
+      "timespan": "Time Granularity",
+      "timespanPlaceholder": "Select time granularity",
+      "timespanHour": "Hour",
+      "timespanDay": "Day",
+      "search": "Search",
+      "reset": "Reset"
+    },
+    "metrics": {
+      "totalRequests": "Total Requests",
+      "totalRequestsTooltip": "Total number of requests in the time range",
+      "errorCount": "Error Count",
+      "errorCountTooltip": "Number of exception requests in the time range",
+      "currentRpm": "Current RPM",
+      "currentRpmTooltip": "Requests Per Minute",
+      "currentTpm": "Current TPM",
+      "currentTpmTooltip": "Tokens Per Minute",
+      "outputTokens": "Output Tokens",
+      "outputTokensTooltip": "Total output tokens in the time range",
+      "fullValue": "Full Value"
+    },
+    "charts": {
+      "tokensChart": {
+        "cacheCreationTokens": "Cache Creation Tokens",
+        "cachedTokens": "Cached Tokens",
+        "inputTokens": "Input Tokens",
+        "outputTokens": "Output Tokens",
+        "totalTokens": "Total Tokens",
+        "webSearchCount": "Web Search Count"
+      },
+      "requestsChart": {
+        "requestCount": "Request Count",
+        "exceptionCount": "Exception Count"
+      }
+    }
   },
   "table": {
     "selected": "{{selected}} of {{total}} row(s) selected",
@@ -82,6 +160,13 @@
     "refresh": "Refresh",
     "noResults": "No models found",
     "apiDetails": "API Detail",
+    "pluginInfo": "Plugin Info",
+    "noPluginConfigured": "No plugins configured",
+    "pluginEnabled": "Enabled",
+    "pluginDisabled": "Disabled",
+    "cachePlugin": "Cache",
+    "webSearchPlugin": "Web Search",
+    "thinkSplitPlugin": "Think Split",
     "dialog": {
       "createTitle": "Create Model",
       "updateTitle": "Update Model",
@@ -93,7 +178,65 @@
       "selectType": "Select model type",
       "create": "Create",
       "update": "Update",
-      "submitting": "Submitting..."
+      "submitting": "Submitting...",
+      "modelNameUpdateDisabled": "Model name cannot be modified in update mode",
+      "noSearchEngineConfigured": "Click \"Add Search Engine\" to configure search engines",
+      "pluginConfiguration": "Plugin Configuration",
+      "pluginConfigurationDescription": "Configure optional plugins for this model",
+      "cachePlugin": {
+        "title": "Cache Plugin",
+        "description": "Enable response caching to improve performance",
+        "enable": "Enable Cache",
+        "ttl": "Time to Live (seconds)",
+        "ttlPlaceholder": "Enter cache TTL",
+        "itemMaxSize": "Max Cache Item Size (bytes)",
+        "itemMaxSizePlaceholder": "Enter max item size",
+        "addCacheHitHeader": "Add cache hit header",
+        "cacheHitHeader": "Cache hit header name",
+        "cacheHitHeaderPlaceholder": "Enter header name"
+      },
+      "webSearchPlugin": {
+        "title": "Web Search Plugin",
+        "description": "Enable web search capabilities for enhanced responses",
+        "enable": "Enable Web Search",
+        "searchFrom": "Search Engines",
+        "searchFromPlaceholder": "Select search engines",
+        "addEngine": "Add Search Engine",
+        "removeEngine": "Remove",
+        "engineType": "Engine Type",
+        "engineConfig": "Engine Configuration",
+        "maxResults": "Maximum Results",
+        "maxResultsPlaceholder": "Enter max results",
+        "searchEngines": {
+          "bing": "Bing",
+          "google": "Google",
+          "arxiv": "ArXiv",
+          "searchxng": "SearchXNG"
+        },
+        "engineSpec": {
+          "apiKey": "API Key",
+          "apiKeyPlaceholder": "Enter API key",
+          "cx": "Custom Search Engine ID",
+          "cxPlaceholder": "Enter search engine ID",
+          "baseUrl": "Base URL",
+          "baseUrlPlaceholder": "Enter base URL"
+        },
+        "forceSearch": "Force search on every request",
+        "needReference": "Include references in response",
+        "referenceLocation": "Reference location",
+        "referenceLocationPlaceholder": "Enter reference location",
+        "referenceFormat": "Reference format",
+        "referenceFormatPlaceholder": "Enter reference format",
+        "defaultLanguage": "Default language",
+        "defaultLanguagePlaceholder": "Enter default language",
+        "promptTemplate": "Search prompt template",
+        "promptTemplatePlaceholder": "Enter prompt template"
+      },
+      "thinkSplitPlugin": {
+        "title": "Think Split Plugin",
+        "description": "Split complex thinking into structured responses",
+        "enable": "Enable Think Split"
+      }
     },
     "deleteDialog": {
       "confirmTitle": "Delete Model",
@@ -103,6 +246,54 @@
       "deleting": "Deleting..."
     }
   },
+  "log": {
+    "title": "Log List",
+    "keyName": "Key Name",
+    "model": "Model",
+    "inputTokens": "Input Tokens",
+    "outputTokens": "Output Tokens",
+    "duration": "Duration(s)",
+    "state": "State",
+    "time": "Time",
+    "details": "Details",
+    "success": "Success",
+    "failed": "Failed",
+    "basicInfo": "Basic Info",
+    "tokenInfo": "Token Info",
+    "timeInfo": "Time Info",
+    "requestBody": "Request Body",
+    "responseBody": "Response Body",
+    "noRequestBody": "No request body available",
+    "noResponseBody": "No response body available",
+    "contentTruncated": "Content truncated",
+    "id": "ID",
+    "requestId": "Request ID",
+    "channel": "Channel",
+    "user": "User",
+    "ip": "IP",
+    "endpoint": "Endpoint",
+    "cacheCreation": "Cache Creation",
+    "cached": "Cached",
+    "imageInput": "Image Input",
+    "reasoning": "Reasoning",
+    "total": "Total",
+    "webSearchCount": "Web Search Count",
+    "created": "Created",
+    "request": "Request",
+    "retry": "Retry",
+    "retryTimes": "Retry Times",
+    "ttfb": "TTFB",
+    "filters": {
+      "keyPlaceholder": "Enter key name to filter",
+      "modelPlaceholder": "Enter model name to filter",
+      "statusAll": "All",
+      "statusSuccess": "Success",
+      "statusError": "Error",
+      "dateRangePlaceholder": "Select date range",
+      "search": "Search",
+      "reset": "Reset"
+    }
+  },
   "apiDoc": {
     "requestExample": "Request Example",
     "voice": "required",
@@ -168,6 +359,7 @@
     "8": "STT",
     "9": "Audio",
     "10": "Rerank",
-    "11": "PDF Parsing"
+    "11": "PDF Parsing",
+    "13": "Video Generation"
   }
 }

+ 195 - 3
web/public/locales/zh/translation.json

@@ -12,7 +12,85 @@
   "common": {
     "noResult": "没有数据",
     "copied": "已复制到剪贴板",
-    "copyFailed": "复制失败"
+    "copyFailed": "复制失败",
+    "validation": {
+      "error": {
+        "title": "表单验证错误"
+      }
+    },
+    "global": "全局",
+    "loading": "加载中...",
+    "selectDateRange": "选择日期范围"
+  },
+  "error": {
+    "loading": "数据加载失败",
+    "loadingLogs": "日志加载失败",
+    "retry": "重试",
+    "viewDetails": "查看详情",
+    "hideDetails": "隐藏详情",
+    "message": "错误信息",
+    "code": "错误代码",
+    "network": "网络错误",
+    "networkDescription": "请检查您的网络连接并重试",
+    "server": "服务器错误",
+    "serverDescription": "服务器在处理您的请求时遇到了错误",
+    "client": "客户端错误",
+    "clientDescription": "您的请求存在问题",
+    "validation": "验证错误",
+    "validationDescription": "请检查您的输入并重试",
+    "unauthorized": "未授权",
+    "unauthorizedDescription": "您没有权限访问此资源",
+    "notFound": "未找到",
+    "notFoundDescription": "未找到请求的资源",
+    "unknown": "未知错误",
+    "unknownDescription": "发生了意外错误"
+  },
+  "monitor": {
+    "title": "监控仪表盘",
+    "noData": "暂无数据",
+    "noDataDescription": "当前时间范围内没有找到任何数据。请尝试调整过滤条件或选择不同的时间范围。",
+    "filters": {
+      "title": "过滤条件",
+      "key": "API Key",
+      "keyPlaceholder": "输入 API Key 名称进行过滤",
+      "model": "模型",
+      "modelPlaceholder": "输入模型名称",
+      "dateRange": "日期范围",
+      "dateRangePlaceholder": "选择日期范围",
+      "timespan": "时间粒度",
+      "timespanPlaceholder": "选择时间粒度",
+      "timespanHour": "小时",
+      "timespanDay": "天",
+      "search": "搜索",
+      "reset": "重置"
+    },
+    "metrics": {
+      "totalRequests": "总请求数",
+      "totalRequestsTooltip": "统计时间范围内的总请求次数",
+      "errorCount": "错误数",
+      "errorCountTooltip": "统计时间范围内的异常请求次数",
+      "currentRpm": "当前 RPM",
+      "currentRpmTooltip": "每分钟请求数(Requests Per Minute)",
+      "currentTpm": "当前 TPM",
+      "currentTpmTooltip": "每分钟 Token 数(Tokens Per Minute)",
+      "outputTokens": "输出 Token 数",
+      "outputTokensTooltip": "统计时间范围内的输出 Token 总数",
+      "fullValue": "完整值"
+    },
+    "charts": {
+      "tokensChart": {
+        "cacheCreationTokens": "缓存创建 Tokens",
+        "cachedTokens": "缓存 Tokens",
+        "inputTokens": "输入 Tokens",
+        "outputTokens": "输出 Tokens",
+        "totalTokens": "总 Tokens",
+        "webSearchCount": "搜索次数"
+      },
+      "requestsChart": {
+        "requestCount": "请求数量",
+        "exceptionCount": "异常数量"
+      }
+    }
   },
   "table": {
     "selected": "已选择 {{selected}} 行,共 {{total}} 行",
@@ -82,6 +160,13 @@
     "refresh": "刷新",
     "noResults": "未找到模型",
     "apiDetails": "API 详情",
+    "pluginInfo": "插件信息",
+    "noPluginConfigured": "未配置插件",
+    "pluginEnabled": "已启用",
+    "pluginDisabled": "已禁用",
+    "cachePlugin": "缓存",
+    "webSearchPlugin": "网络搜索",
+    "thinkSplitPlugin": "思考拆分",
     "dialog": {
       "createTitle": "创建模型",
       "updateTitle": "更新模型",
@@ -93,7 +178,65 @@
       "selectType": "选择模型类型",
       "create": "创建",
       "update": "更新",
-      "submitting": "提交中..."
+      "submitting": "提交中...",
+      "modelNameUpdateDisabled": "模型名称在更新模式下不可修改",
+      "noSearchEngineConfigured": "点击\"添加搜索引擎\"来配置搜索引擎",
+      "pluginConfiguration": "插件配置",
+      "pluginConfigurationDescription": "为此模型配置可选插件",
+      "cachePlugin": {
+        "title": "缓存插件",
+        "description": "启用响应缓存以提高性能",
+        "enable": "启用缓存",
+        "ttl": "生存时间(秒)",
+        "ttlPlaceholder": "输入缓存TTL",
+        "itemMaxSize": "最大缓存项大小(字节)",
+        "itemMaxSizePlaceholder": "输入最大项大小",
+        "addCacheHitHeader": "添加缓存命中头",
+        "cacheHitHeader": "缓存命中头名称",
+        "cacheHitHeaderPlaceholder": "输入头名称"
+      },
+      "webSearchPlugin": {
+        "title": "网络搜索插件",
+        "description": "启用网络搜索功能以增强响应",
+        "enable": "启用网络搜索",
+        "searchFrom": "搜索引擎",
+        "searchFromPlaceholder": "选择搜索引擎",
+        "addEngine": "添加搜索引擎",
+        "removeEngine": "移除",
+        "engineType": "引擎类型",
+        "engineConfig": "引擎配置",
+        "maxResults": "最大结果数",
+        "maxResultsPlaceholder": "输入最大结果数",
+        "searchEngines": {
+          "bing": "必应",
+          "google": "谷歌",
+          "arxiv": "ArXiv",
+          "searchxng": "SearchXNG"
+        },
+        "engineSpec": {
+          "apiKey": "API密钥",
+          "apiKeyPlaceholder": "输入API密钥",
+          "cx": "自定义搜索引擎ID",
+          "cxPlaceholder": "输入搜索引擎ID",
+          "baseUrl": "基础URL",
+          "baseUrlPlaceholder": "输入基础URL"
+        },
+        "forceSearch": "每次请求强制搜索",
+        "needReference": "在响应中包含引用",
+        "referenceLocation": "引用位置",
+        "referenceLocationPlaceholder": "输入引用位置",
+        "referenceFormat": "引用格式",
+        "referenceFormatPlaceholder": "输入引用格式",
+        "defaultLanguage": "默认语言",
+        "defaultLanguagePlaceholder": "输入默认语言",
+        "promptTemplate": "搜索提示模板",
+        "promptTemplatePlaceholder": "输入提示模板"
+      },
+      "thinkSplitPlugin": {
+        "title": "思考拆分插件",
+        "description": "将复杂思考拆分为结构化响应",
+        "enable": "启用思考拆分"
+      }
     },
     "deleteDialog": {
       "confirmTitle": "删除模型",
@@ -103,6 +246,54 @@
       "deleting": "删除中..."
     }
   },
+  "log": {
+    "title": "日志列表",
+    "keyName": "密钥名称",
+    "model": "模型",
+    "inputTokens": "输入Tokens",
+    "outputTokens": "输出Tokens",
+    "duration": "持续时间(秒)",
+    "state": "状态",
+    "time": "时间",
+    "details": "详情",
+    "success": "成功",
+    "failed": "失败",
+    "basicInfo": "基本信息",
+    "tokenInfo": "Token信息",
+    "timeInfo": "时间信息",
+    "requestBody": "请求内容",
+    "responseBody": "响应内容",
+    "noRequestBody": "无可用的请求内容",
+    "noResponseBody": "无可用的响应内容",
+    "contentTruncated": "内容已截断",
+    "id": "ID",
+    "requestId": "请求ID",
+    "channel": "渠道",
+    "user": "用户",
+    "ip": "IP地址",
+    "endpoint": "端点",
+    "cacheCreation": "缓存创建",
+    "cached": "已缓存",
+    "imageInput": "图像输入",
+    "reasoning": "推理",
+    "total": "总计",
+    "webSearchCount": "网络搜索次数",
+    "created": "创建时间",
+    "request": "请求时间",
+    "retry": "重试时间",
+    "retryTimes": "重试次数",
+    "ttfb": "首字节时间",
+    "filters": {
+      "keyPlaceholder": "输入密钥名称进行筛选",
+      "modelPlaceholder": "输入模型名称进行筛选",
+      "statusAll": "全部",
+      "statusSuccess": "成功",
+      "statusError": "错误",
+      "dateRangePlaceholder": "选择日期范围",
+      "search": "搜索",
+      "reset": "重置"
+    }
+  },
   "apiDoc": {
     "requestExample": "请求示例",
     "voice": "必填",
@@ -168,6 +359,7 @@
     "8": "语音转录",
     "9": "音频翻译",
     "10": "重排序",
-    "11": "pdf解析"
+    "11": "pdf解析",
+    "13": "视频生成"
   }
 }

+ 67 - 0
web/src/api/dashboard.ts

@@ -0,0 +1,67 @@
+import { get } from './index'
+import { DashboardData, DashboardFilters } from '@/types/dashboard'
+
+export const dashboardApi = {
+    getDashboard: async (filters?: DashboardFilters): Promise<DashboardData> => {
+        const params = new URLSearchParams()
+        
+        if (filters?.model) {
+            params.append('model', filters.model)
+        }
+        if (filters?.start_timestamp) {
+            params.append('start_timestamp', filters.start_timestamp.toString())
+        }
+        if (filters?.end_timestamp) {
+            params.append('end_timestamp', filters.end_timestamp.toString())
+        }
+        if (filters?.timezone) {
+            params.append('timezone', filters.timezone)
+        }
+        if (filters?.timespan) {
+            params.append('timespan', filters.timespan)
+        }
+
+        const queryString = params.toString()
+        const url = queryString ? `dashboard/?${queryString}` : 'dashboard/'
+        
+        const response = await get<DashboardData>(url)
+        return response
+    },
+
+    getDashboardByGroup: async (group: string, filters?: DashboardFilters): Promise<DashboardData> => {
+        const params = new URLSearchParams()
+        
+        if (filters?.keyName) {
+            params.append('token_name', filters.keyName)
+        }
+        if (filters?.model) {
+            params.append('model', filters.model)
+        }
+        if (filters?.start_timestamp) {
+            params.append('start_timestamp', filters.start_timestamp.toString())
+        }
+        if (filters?.end_timestamp) {
+            params.append('end_timestamp', filters.end_timestamp.toString())
+        }
+        if (filters?.timezone) {
+            params.append('timezone', filters.timezone)
+        }
+        if (filters?.timespan) {
+            params.append('timespan', filters.timespan)
+        }
+
+        const queryString = params.toString()
+        const url = queryString ? `dashboard/${group}?${queryString}` : `dashboard/${group}`
+        
+        const response = await get<DashboardData>(url)
+        return response
+    },
+
+    getDashboardData: async (filters?: DashboardFilters): Promise<DashboardData> => {
+        if (filters?.keyName) {
+            return dashboardApi.getDashboardByGroup(filters.keyName, filters)
+        } else {
+            return dashboardApi.getDashboard(filters)
+        }
+    }
+} 

+ 78 - 0
web/src/api/log.ts

@@ -0,0 +1,78 @@
+import { get } from './index'
+import { LogResponse, LogFilters } from '@/types/log'
+
+export const logApi = {
+    // 获取全部日志数据
+    getLogs: async (filters?: LogFilters): Promise<LogResponse> => {
+        const params = new URLSearchParams()
+
+        if (filters?.page) {
+            params.append('page', filters.page.toString())
+        }
+        if (filters?.per_page) {
+            params.append('per_page', filters.per_page.toString())
+        }
+        if (filters?.model) {
+            params.append('model_name', filters.model)
+        }
+        if (filters?.start_timestamp) {
+            params.append('start_timestamp', filters.start_timestamp.toString())
+        }
+        if (filters?.end_timestamp) {
+            params.append('end_timestamp', filters.end_timestamp.toString())
+        }
+        if (filters?.code_type && filters.code_type !== 'all') {
+            params.append('code_type', filters.code_type)
+        }
+
+        const queryString = params.toString()
+        const url = queryString ? `logs/search?${queryString}` : 'logs/search'
+
+        const response = await get<LogResponse>(url)
+        return response
+    },
+
+    // 获取组级别日志数据
+    getLogsByGroup: async (group: string, filters?: LogFilters): Promise<LogResponse> => {
+        const params = new URLSearchParams()
+
+        if (filters?.page) {
+            params.append('page', filters.page.toString())
+        }
+        if (filters?.per_page) {
+            params.append('per_page', filters.per_page.toString())
+        }
+        if (filters?.model) {
+            params.append('model_name', filters.model)
+        }
+        if (filters?.keyName) {
+            params.append('token_name', filters.keyName)
+        }
+        if (filters?.start_timestamp) {
+            params.append('start_timestamp', filters.start_timestamp.toString())
+        }
+        if (filters?.end_timestamp) {
+            params.append('end_timestamp', filters.end_timestamp.toString())
+        }
+        if (filters?.code_type && filters.code_type !== 'all') {
+            params.append('code_type', filters.code_type)
+        }
+
+        const queryString = params.toString()
+        const url = queryString ? `log/${group}/search?${queryString}` : `log/${group}/search`
+
+        const response = await get<LogResponse>(url)
+        return response
+    },
+
+    // 根据条件获取日志数据 - 统一入口
+    getLogData: async (filters?: LogFilters): Promise<LogResponse> => {
+        if (filters?.keyName) {
+            // 有keyName时使用分组API
+            return logApi.getLogsByGroup(filters.keyName, filters)
+        } else {
+            // 没有keyName时使用全局API
+            return logApi.getLogs(filters)
+        }
+    }
+} 

+ 65 - 0
web/src/components/common/Date.tsx

@@ -0,0 +1,65 @@
+"use client"
+
+import * as React from "react"
+import { CalendarIcon } from "lucide-react"
+import { addDays, format } from "date-fns"
+import { DateRange } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Calendar } from "@/components/ui/calendar"
+import {
+    Popover,
+    PopoverContent,
+    PopoverTrigger,
+} from "@/components/ui/popover"
+
+export function DatePickerWithRange({
+    className,
+}: React.HTMLAttributes<HTMLDivElement>) {
+    const [date, setDate] = React.useState<DateRange | undefined>({
+        from: new Date(),
+        to: addDays(new Date(), 20),
+    })
+
+    return (
+        <div className={cn("grid gap-2", className)}>
+            <Popover>
+                <PopoverTrigger asChild>
+                    <Button
+                        id="date"
+                        variant={"outline"}
+                        className={cn(
+                            "w-full justify-start text-left font-normal",
+                            !date && "text-muted-foreground"
+                        )}
+                    >
+                        <CalendarIcon className="mr-2 h-4 w-4" />
+                        {date?.from ? (
+                            date.to ? (
+                                <>
+                                    {format(date.from, "LLL dd, y")} -{" "}
+                                    {format(date.to, "LLL dd, y")}
+                                </>
+                            ) : (
+                                format(date.from, "LLL dd, y")
+                            )
+                        ) : (
+                            <span>Pick a date</span>
+                        )}
+                    </Button>
+                </PopoverTrigger>
+                <PopoverContent className="w-auto p-0" align="start">
+                    <Calendar
+                        initialFocus
+                        mode="range"
+                        defaultMonth={date?.from}
+                        selected={date}
+                        onSelect={setDate}
+                        numberOfMonths={2}
+                    />
+                </PopoverContent>
+            </Popover>
+        </div>
+    )
+}

+ 86 - 0
web/src/components/common/DateRangePicker.tsx

@@ -0,0 +1,86 @@
+"use client"
+
+import * as React from "react"
+import { CalendarIcon } from "lucide-react"
+import { format } from "date-fns"
+import { DateRange } from "react-day-picker"
+import { useTranslation } from "react-i18next"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Calendar } from "@/components/ui/calendar"
+import {
+    Popover,
+    PopoverContent,
+    PopoverTrigger,
+} from "@/components/ui/popover"
+
+interface DateRangePickerProps {
+    value?: DateRange
+    onChange?: (dateRange: DateRange | undefined) => void
+    placeholder?: string
+    className?: string
+    disabled?: boolean
+}
+
+export function DateRangePicker({
+    value,
+    onChange,
+    placeholder,
+    className,
+    disabled = false,
+}: DateRangePickerProps) {
+    const { t } = useTranslation()
+    const [date, setDate] = React.useState<DateRange | undefined>(value)
+
+    // 当外部 value 变化时更新内部状态
+    React.useEffect(() => {
+        setDate(value)
+    }, [value])
+
+    const handleDateChange = (newDate: DateRange | undefined) => {
+        setDate(newDate)
+        onChange?.(newDate)
+    }
+
+    return (
+        <Popover>
+            <PopoverTrigger asChild>
+                <Button
+                    id="date"
+                    variant={"outline"}
+                    disabled={disabled}
+                    className={cn(
+                        "w-full justify-start text-left font-normal",
+                        !date && "text-muted-foreground",
+                        className
+                    )}
+                >
+                    <CalendarIcon className="mr-2 h-4 w-4" />
+                    {date?.from ? (
+                        date.to ? (
+                            <>
+                                {format(date.from, "yyyy-MM-dd")} -{" "}
+                                {format(date.to, "yyyy-MM-dd")}
+                            </>
+                        ) : (
+                            format(date.from, "yyyy-MM-dd")
+                        )
+                    ) : (
+                        <span>{placeholder || t('common.selectDateRange')}</span>
+                    )}
+                </Button>
+            </PopoverTrigger>
+            <PopoverContent className="w-auto p-0" align="start">
+                <Calendar
+                    initialFocus
+                    mode="range"
+                    defaultMonth={date?.from}
+                    selected={date}
+                    onSelect={handleDateChange}
+                    numberOfMonths={2}
+                />
+            </PopoverContent>
+        </Popover>
+    )
+} 

+ 64 - 0
web/src/components/common/error/validationErrorDisplay.tsx

@@ -0,0 +1,64 @@
+import { FieldErrors } from 'react-hook-form'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { AlertCircle } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+
+interface ValidationErrorDisplayProps {
+    errors: FieldErrors<Record<string, unknown>>
+    title?: string
+    className?: string
+}
+
+export function ValidationErrorDisplay({
+    errors,
+    title,
+    className = ""
+}: ValidationErrorDisplayProps) {
+    const { t } = useTranslation()
+
+    // 如果没有错误,不显示组件
+    if (!errors || Object.keys(errors).length === 0) {
+        return null
+    }
+
+    // 递归处理嵌套错误对象
+    const flattenErrors = (obj: Record<string, unknown>, prefix = ''): string[] => {
+        const messages: string[] = []
+
+        for (const [key, value] of Object.entries(obj)) {
+            const fullKey = prefix ? `${prefix}.${key}` : key
+
+            if (value && typeof value === 'object') {
+                if ('message' in value && typeof value.message === 'string') {
+                    // 这是一个错误对象
+                    messages.push(`${fullKey}: ${value.message}`)
+                } else {
+                    // 递归处理嵌套对象
+                    messages.push(...flattenErrors(value as Record<string, unknown>, fullKey))
+                }
+            }
+        }
+
+        return messages
+    }
+
+    const errorMessages = flattenErrors(errors)
+
+    return (
+        <Alert variant="destructive" className={className}>
+            <AlertCircle className="h-4 w-4" />
+            <AlertTitle>
+                {title || t('common.validation.error.title')}
+            </AlertTitle>
+            <AlertDescription>
+                <div className="mt-2 space-y-1">
+                    {errorMessages.map((message, index) => (
+                        <div key={index} className="text-sm">
+                            • {message}
+                        </div>
+                    ))}
+                </div>
+            </AlertDescription>
+        </Alert>
+    )
+}

+ 2 - 2
web/src/components/layout/RootLayOut.tsx

@@ -10,11 +10,11 @@ export function RootLayout() {
     <div className="flex h-screen bg-background">
       <Sidebar
         displayConfig={{
-          monitor: false,
+          monitor: true,
           key: true,
           channel: true,
           model: true,
-          log: false,
+          log: true,
           doc: true,
           github: true,
         }}

+ 1 - 1
web/src/components/ui/button.tsx

@@ -10,7 +10,7 @@ const buttonVariants = cva(
     variants: {
       variant: {
         default:
-          "bg-[#7B7FF6] text-primary-foreground shadow-xs hover:bg-[#6A6DE6]",
+          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
         destructive:
           "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
         outline:

+ 73 - 0
web/src/components/ui/calendar.tsx

@@ -0,0 +1,73 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function Calendar({
+  className,
+  classNames,
+  showOutsideDays = true,
+  ...props
+}: React.ComponentProps<typeof DayPicker>) {
+  return (
+    <DayPicker
+      showOutsideDays={showOutsideDays}
+      className={cn("p-3", className)}
+      classNames={{
+        months: "flex flex-col sm:flex-row gap-2",
+        month: "flex flex-col gap-4",
+        caption: "flex justify-center pt-1 relative items-center w-full",
+        caption_label: "text-sm font-medium",
+        nav: "flex items-center gap-1",
+        nav_button: cn(
+          buttonVariants({ variant: "outline" }),
+          "size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
+        ),
+        nav_button_previous: "absolute left-1",
+        nav_button_next: "absolute right-1",
+        table: "w-full border-collapse space-x-1",
+        head_row: "flex",
+        head_cell:
+          "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
+        row: "flex w-full mt-2",
+        cell: cn(
+          "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
+          props.mode === "range"
+            ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+            : "[&:has([aria-selected])]:rounded-md"
+        ),
+        day: cn(
+          buttonVariants({ variant: "ghost" }),
+          "size-8 p-0 font-normal aria-selected:opacity-100"
+        ),
+        day_range_start:
+          "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
+        day_range_end:
+          "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
+        day_selected:
+          "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+        day_today: "bg-accent text-accent-foreground",
+        day_outside:
+          "day-outside text-muted-foreground aria-selected:text-muted-foreground",
+        day_disabled: "text-muted-foreground opacity-50",
+        day_range_middle:
+          "aria-selected:bg-accent aria-selected:text-accent-foreground",
+        day_hidden: "invisible",
+        ...classNames,
+      }}
+      components={{
+        IconLeft: ({ className, ...props }) => (
+          <ChevronLeft className={cn("size-4", className)} {...props} />
+        ),
+        IconRight: ({ className, ...props }) => (
+          <ChevronRight className={cn("size-4", className)} {...props} />
+        ),
+      }}
+      {...props}
+    />
+  )
+}
+
+export { Calendar }

+ 92 - 0
web/src/components/ui/echarts.tsx

@@ -0,0 +1,92 @@
+import React, { useRef, useEffect, useMemo } from 'react'
+import { init, getInstanceByDom, type EChartsOption, type ECharts } from 'echarts'
+import { cn } from '@/lib/utils'
+
+export interface EChartProps {
+    option: EChartsOption
+    style?: React.CSSProperties
+    className?: string
+    theme?: string | object
+    onChartReady?: (chart: ECharts) => void
+    onClick?: (params: unknown) => void
+}
+
+export const EChart: React.FC<EChartProps> = ({
+    option,
+    style = { width: '100%', height: '350px' },
+    className,
+    theme,
+    onChartReady,
+    onClick,
+}) => {
+    const chartRef = useRef<HTMLDivElement>(null)
+
+    // 防抖的 resize 函数
+    const resizeChart = useMemo(() => {
+        let timeout: NodeJS.Timeout
+        return () => {
+            clearTimeout(timeout)
+            timeout = setTimeout(() => {
+                if (chartRef.current) {
+                    const chart = getInstanceByDom(chartRef.current)
+                    chart?.resize()
+                }
+            }, 300)
+        }
+    }, [])
+
+    useEffect(() => {
+        if (!chartRef.current) return
+
+        // 初始化图表
+        const chart = init(chartRef.current, theme)
+
+        // 设置点击事件
+        if (onClick) {
+            chart.on('click', onClick)
+        }
+
+        // 监听窗口 resize 事件
+        const handleResize = () => resizeChart()
+        window.addEventListener('resize', handleResize)
+
+        // 使用 ResizeObserver 监听容器大小变化
+        const resizeObserver = new ResizeObserver(() => {
+            resizeChart()
+        })
+        resizeObserver.observe(chartRef.current)
+
+        // 图表准备完成回调
+        if (onChartReady) {
+            onChartReady(chart)
+        }
+
+        // 清理函数
+        return () => {
+            chart?.dispose()
+            window.removeEventListener('resize', handleResize)
+            if (chartRef.current) {
+                resizeObserver.unobserve(chartRef.current)
+            }
+            resizeObserver.disconnect()
+        }
+    }, [theme, onChartReady, onClick, resizeChart])
+
+    useEffect(() => {
+        // 更新图表配置
+        if (!chartRef.current) return
+
+        const chart = getInstanceByDom(chartRef.current)
+        if (chart && option) {
+            chart.setOption(option, true)
+        }
+    }, [option])
+
+    return (
+        <div 
+            ref={chartRef} 
+            style={style} 
+            className={cn("w-full", className)}
+        />
+    )
+} 

+ 46 - 0
web/src/components/ui/popover.tsx

@@ -0,0 +1,46 @@
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+function Popover({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
+  return <PopoverPrimitive.Root data-slot="popover" {...props} />
+}
+
+function PopoverTrigger({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
+  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
+}
+
+function PopoverContent({
+  className,
+  align = "center",
+  sideOffset = 4,
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
+  return (
+    <PopoverPrimitive.Portal>
+      <PopoverPrimitive.Content
+        data-slot="popover-content"
+        align={align}
+        sideOffset={sideOffset}
+        className={cn(
+          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+          className
+        )}
+        {...props}
+      />
+    </PopoverPrimitive.Portal>
+  )
+}
+
+function PopoverAnchor({
+  ...props
+}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
+  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

+ 65 - 0
web/src/feature/log/components/JsonViewer.tsx

@@ -0,0 +1,65 @@
+import React, { Suspense } from 'react'
+import { useTheme } from '@/handler/ThemeContext'
+import { Skeleton } from '@/components/ui/skeleton'
+
+// 动态导入 react-json-view 以避免 SSR 问题
+const ReactJson = React.lazy(() => import('react-json-view'))
+
+interface JsonViewerProps {
+    src: unknown
+    name?: string | false
+    collapsed?: boolean | number
+    enableClipboard?: boolean
+    displayDataTypes?: boolean
+    displayObjectSize?: boolean
+    collapseStringsAfterLength?: number
+}
+
+export function JsonViewer({
+    src,
+    name = false,
+    collapsed = 2,
+    enableClipboard = true,
+    displayDataTypes = false,
+    displayObjectSize = false,
+    collapseStringsAfterLength = 100,
+}: JsonViewerProps) {
+    const { theme } = useTheme()
+
+    let parsedSrc = src
+
+    // 尝试解析字符串形式的JSON
+    if (typeof src === 'string') {
+        try {
+            parsedSrc = JSON.parse(src)
+        } catch {
+            // 如果解析失败,显示原始字符串
+            parsedSrc = { value: src }
+        }
+    }
+
+    return (
+        <div className="json-viewer-container">
+            <Suspense fallback={<Skeleton className="h-20 w-full" />}>
+                <ReactJson
+                    src={parsedSrc as object}
+                    theme={theme === 'dark' ? 'tomorrow' : 'rjv-default'}
+                    name={name}
+                    collapsed={collapsed}
+                    enableClipboard={enableClipboard}
+                    displayDataTypes={displayDataTypes}
+                    displayObjectSize={displayObjectSize}
+                    collapseStringsAfterLength={collapseStringsAfterLength}
+                    style={{
+                        backgroundColor: 'transparent',
+                        fontSize: '13px',
+                        fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
+                        padding: '8px',
+                        borderRadius: '6px',
+                        border: '1px solid hsl(var(--border))',
+                    }}
+                />
+            </Suspense>
+        </div>
+    )
+} 

+ 187 - 0
web/src/feature/log/components/LogFilters.tsx

@@ -0,0 +1,187 @@
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import type { DateRange } from 'react-day-picker'
+import { Search, RotateCcw } from 'lucide-react'
+
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import {
+    Select,
+    SelectContent,
+    SelectItem,
+    SelectTrigger,
+    SelectValue,
+} from '@/components/ui/select'
+import {
+    Tooltip,
+    TooltipContent,
+    TooltipProvider,
+    TooltipTrigger,
+} from '@/components/ui/tooltip'
+import { DateRangePicker } from '@/components/common/DateRangePicker'
+import type { LogFilters } from '@/types/log'
+
+interface LogFiltersProps {
+    onFiltersChange: (filters: LogFilters) => void
+    loading?: boolean
+}
+
+export function LogFilters({ onFiltersChange, loading = false }: LogFiltersProps) {
+    const { t } = useTranslation()
+
+    // 计算默认日期范围(当前时间往前7天)
+    const getDefaultDateRange = (): DateRange => {
+        const today = new Date()
+        const sevenDaysAgo = new Date()
+        sevenDaysAgo.setDate(today.getDate() - 7)
+
+        return {
+            from: sevenDaysAgo,
+            to: today
+        }
+    }
+
+    const [keyName, setKeyName] = useState('')
+    const [model, setModel] = useState('')
+    const [dateRange, setDateRange] = useState<DateRange | undefined>(getDefaultDateRange())
+    const [codeType, setCodeType] = useState<'all' | 'success' | 'error'>('all')
+
+    // 处理表单提交
+    const handleSubmit = (e: React.FormEvent) => {
+        e.preventDefault()
+
+        const filters: LogFilters = {
+            keyName: keyName.trim() || undefined,
+            model: model.trim() || undefined,
+            code_type: codeType,
+            page: 1, // 重置到第一页
+            per_page: 10
+        }
+
+        // 处理日期范围
+        if (dateRange?.from) {
+            filters.start_timestamp = dateRange.from.getTime()
+        }
+        if (dateRange?.to) {
+            // 将结束时间设置为当天的 23:59:59
+            const endDate = new Date(dateRange.to)
+            endDate.setHours(23, 59, 59, 999)
+            filters.end_timestamp = endDate.getTime()
+        }
+
+        onFiltersChange(filters)
+    }
+
+    // 重置过滤器
+    const handleReset = () => {
+        setKeyName('')
+        setModel('')
+        const defaultDateRange = getDefaultDateRange()
+        setDateRange(defaultDateRange)
+        setCodeType('all')
+
+        const filters: LogFilters = {
+            code_type: 'all',
+            page: 1,
+            per_page: 10,
+            start_timestamp: defaultDateRange.from!.getTime(),
+            end_timestamp: defaultDateRange.to!.setHours(23, 59, 59, 999)
+        }
+        onFiltersChange(filters)
+    }
+
+    return (
+        <div className="bg-card border border-border rounded-lg p-4 shadow-none">
+            <form onSubmit={handleSubmit}>
+                <div className="flex items-center gap-4">
+                    {/* Key Name 过滤器 */}
+                    <TooltipProvider>
+                        <Tooltip>
+                            <TooltipTrigger asChild>
+                                <div className="flex-1 min-w-0">
+                                    <Input
+                                        placeholder={t('log.filters.keyPlaceholder')}
+                                        value={keyName}
+                                        onChange={(e) => setKeyName(e.target.value)}
+                                        disabled={loading}
+                                        className="h-10"
+                                    />
+                                </div>
+                            </TooltipTrigger>
+                            <TooltipContent>
+                                <p>{t('log.filters.keyPlaceholder')}</p>
+                            </TooltipContent>
+                        </Tooltip>
+                    </TooltipProvider>
+
+                    {/* Model 过滤器 */}
+                    <TooltipProvider>
+                        <Tooltip>
+                            <TooltipTrigger asChild>
+                                <div className="flex-1 min-w-0">
+                                    <Input
+                                        placeholder={t('log.filters.modelPlaceholder')}
+                                        value={model}
+                                        onChange={(e) => setModel(e.target.value)}
+                                        disabled={loading}
+                                        className="h-10"
+                                    />
+                                </div>
+                            </TooltipTrigger>
+                            <TooltipContent>
+                                <p>{t('log.filters.modelPlaceholder')}</p>
+                            </TooltipContent>
+                        </Tooltip>
+                    </TooltipProvider>
+
+                    {/* 状态过滤器 */}
+                    <div className="w-32">
+                        <Select
+                            value={codeType}
+                            onValueChange={(value: 'all' | 'success' | 'error') => setCodeType(value)}
+                            disabled={loading}
+                        >
+                            <SelectTrigger className="h-10">
+                                <SelectValue />
+                            </SelectTrigger>
+                            <SelectContent>
+                                <SelectItem value="all">{t('log.filters.statusAll')}</SelectItem>
+                                <SelectItem value="success">{t('log.filters.statusSuccess')}</SelectItem>
+                                <SelectItem value="error">{t('log.filters.statusError')}</SelectItem>
+                            </SelectContent>
+                        </Select>
+                    </div>
+
+                    {/* 日期范围过滤器 */}
+                    <div className="min-w-48 max-w-72">
+                        <DateRangePicker
+                            value={dateRange}
+                            onChange={setDateRange}
+                            placeholder={t('log.filters.dateRangePlaceholder')}
+                            disabled={loading}
+                            className="h-10"
+                        />
+                    </div>
+
+                    {/* 操作按钮 */}
+                    <div className="flex gap-2 flex-shrink-0">
+                        <Button type="submit" disabled={loading} className="h-10 px-4">
+                            <Search className="h-4 w-4 mr-2" />
+                            {loading ? t('common.loading') : t('log.filters.search')}
+                        </Button>
+                        <Button
+                            type="button"
+                            variant="outline"
+                            onClick={handleReset}
+                            disabled={loading}
+                            className="h-10 px-4"
+                        >
+                            <RotateCcw className="h-4 w-4 mr-2" />
+                            {t('log.filters.reset')}
+                        </Button>
+                    </div>
+                </div>
+            </form>
+        </div>
+    )
+} 

+ 397 - 0
web/src/feature/log/components/LogTable.tsx

@@ -0,0 +1,397 @@
+import React, { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+    createColumnHelper,
+    flexRender,
+    getCoreRowModel,
+    useReactTable,
+} from '@tanstack/react-table'
+import { ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight } from 'lucide-react'
+import { format } from 'date-fns'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Separator } from '@/components/ui/separator'
+import { JsonViewer } from './JsonViewer'
+import type { LogRecord } from '@/types/log'
+
+const columnHelper = createColumnHelper<LogRecord>()
+
+interface LogTableProps {
+    data: LogRecord[]
+    total: number
+    loading?: boolean
+    page: number
+    pageSize: number
+    onPageChange: (page: number) => void
+    onPageSizeChange: (pageSize: number) => void
+}
+
+export function LogTable({
+    data,
+    total,
+    loading = false,
+    page,
+    pageSize,
+    onPageChange,
+    onPageSizeChange,
+}: LogTableProps) {
+    const { t } = useTranslation()
+    const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set())
+
+    const toggleRowExpansion = (rowId: number) => {
+        const newExpanded = new Set(expandedRows)
+        if (newExpanded.has(rowId)) {
+            newExpanded.delete(rowId)
+        } else {
+            newExpanded.add(rowId)
+        }
+        setExpandedRows(newExpanded)
+    }
+
+    const columns = useMemo(
+        () => [
+            columnHelper.display({
+                id: 'details',
+                header: '',
+                cell: ({ row }) => (
+                    <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => toggleRowExpansion(row.original.id)}
+                        className="h-8 w-8 p-0"
+                    >
+                        {expandedRows.has(row.original.id) ? (
+                            <ChevronDown className="h-4 w-4" />
+                        ) : (
+                            <ChevronRight className="h-4 w-4" />
+                        )}
+                    </Button>
+                ),
+                size: 40,
+            }),
+            columnHelper.accessor('token_name', {
+                header: t('log.keyName'),
+                cell: (info) => (
+                    <div className="font-medium text-foreground">
+                        {info.getValue() || '-'}
+                    </div>
+                ),
+                size: 150,
+            }),
+            columnHelper.accessor('model', {
+                header: t('log.model'),
+                cell: (info) => (
+                    <div className="font-mono text-sm">
+                        {info.getValue() || '-'}
+                    </div>
+                ),
+                size: 120,
+            }),
+            columnHelper.display({
+                id: 'input_tokens',
+                header: t('log.inputTokens'),
+                cell: ({ row }) => (
+                    <div className="text-right font-mono">
+                        {row.original.usage?.input_tokens?.toLocaleString() || 0}
+                    </div>
+                ),
+                size: 100,
+            }),
+            columnHelper.display({
+                id: 'output_tokens',
+                header: t('log.outputTokens'),
+                cell: ({ row }) => (
+                    <div className="text-right font-mono">
+                        {row.original.usage?.output_tokens?.toLocaleString() || 0}
+                    </div>
+                ),
+                size: 100,
+            }),
+            columnHelper.display({
+                id: 'duration',
+                header: t('log.duration'),
+                cell: ({ row }) => {
+                    if (!row.original.request_at || !row.original.created_at) {
+                        return (
+                            <div className="text-right font-mono">
+                                -
+                            </div>
+                        )
+                    }
+                    const requestAt = new Date(row.original.request_at)
+                    const createdAt = new Date(row.original.created_at)
+                    const duration = (createdAt.getTime() - requestAt.getTime()) / 1000
+                    return (
+                        <div className="text-right font-mono">
+                            {duration.toFixed(2)}s
+                        </div>
+                    )
+                },
+                size: 80,
+            }),
+            columnHelper.accessor('code', {
+                header: t('log.state'),
+                cell: (info) => {
+                    const code = info.getValue()
+                    const isSuccess = code === 200
+                    return (
+                        <Badge
+                            variant={isSuccess ? 'secondary' : 'destructive'}
+                            className={isSuccess ? 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800' : ''}
+                        >
+                            {isSuccess ? t('log.success') : t('log.failed')}
+                        </Badge>
+                    )
+                },
+                size: 80,
+            }),
+            columnHelper.accessor('created_at', {
+                header: t('log.time'),
+                cell: (info) => (
+                    <div className="text-sm text-muted-foreground">
+                        {info.getValue() ? format(new Date(info.getValue()), 'yyyy-MM-dd HH:mm:ss') : '-'}
+                    </div>
+                ),
+                size: 140,
+            }),
+        ],
+        [t, expandedRows]
+    )
+
+    const table = useReactTable({
+        data: data || [],
+        columns,
+        getCoreRowModel: getCoreRowModel(),
+        manualPagination: true,
+        pageCount: Math.ceil(total / pageSize),
+    })
+
+    const renderExpandedContent = (log: LogRecord) => {
+        return (
+            <div className="p-4 space-y-4 bg-muted/50 border-t">
+                <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+                    {/* 基本信息 */}
+                    <div className="space-y-2">
+                        <h4 className="font-semibold text-sm">{t('log.basicInfo')}</h4>
+                        <div className="space-y-1 text-sm">
+                            <div><span className="font-medium">{t('log.id')}:</span> {log.id}</div>
+                            <div><span className="font-medium">{t('log.requestId')}:</span> {log.request_id}</div>
+                            <div><span className="font-medium">{t('log.channel')}:</span> {log.channel}</div>
+                            <div><span className="font-medium">{t('log.user')}:</span> {log.user || '-'}</div>
+                            <div><span className="font-medium">{t('log.ip')}:</span> {log.ip}</div>
+                            <div><span className="font-medium">{t('log.endpoint')}:</span> {log.endpoint}</div>
+                        </div>
+                    </div>
+
+                    {/* Token信息 */}
+                    <div className="space-y-2">
+                        <h4 className="font-semibold text-sm">{t('log.tokenInfo')}</h4>
+                        <div className="space-y-1 text-sm">
+                            <div><span className="font-medium">{t('log.cacheCreation')}:</span> {log.usage?.cache_creation_tokens || 0}</div>
+                            <div><span className="font-medium">{t('log.cached')}:</span> {log.usage?.cached_tokens || 0}</div>
+                            <div><span className="font-medium">{t('log.imageInput')}:</span> {log.usage?.image_input_tokens || 0}</div>
+                            <div><span className="font-medium">{t('log.reasoning')}:</span> {log.usage?.reasoning_tokens || 0}</div>
+                            <div><span className="font-medium">{t('log.total')}:</span> {log.usage?.total_tokens || 0}</div>
+                            <div><span className="font-medium">{t('log.webSearchCount')}:</span> {log.usage?.web_search_count || 0}</div>
+                        </div>
+                    </div>
+
+                    {/* 时间信息 */}
+                    <div className="space-y-2">
+                        <h4 className="font-semibold text-sm">{t('log.timeInfo')}</h4>
+                        <div className="space-y-1 text-sm">
+                            <div><span className="font-medium">{t('log.created')}:</span> {log.created_at ? format(new Date(log.created_at), 'yyyy-MM-dd HH:mm:ss') : '-'}</div>
+                            <div><span className="font-medium">{t('log.request')}:</span> {log.request_at ? format(new Date(log.request_at), 'yyyy-MM-dd HH:mm:ss') : '-'}</div>
+                            {log.retry_at && <div><span className="font-medium">{t('log.retry')}:</span> {format(new Date(log.retry_at), 'yyyy-MM-dd HH:mm:ss')}</div>}
+                            <div><span className="font-medium">{t('log.retryTimes')}:</span> {log.retry_times || 0}</div>
+                            <div><span className="font-medium">{t('log.ttfb')}:</span> {log.ttfb_milliseconds || 0}ms</div>
+                        </div>
+                    </div>
+                </div>
+
+                <Separator />
+
+                {/* 请求和响应内容 */}
+                <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+                    <div>
+                        <h4 className="font-semibold text-sm mb-2">{t('log.requestBody')}</h4>
+                        {log.request_detail?.request_body ? (
+                            <JsonViewer
+                                src={log.request_detail.request_body}
+                                collapsed={1}
+                                name="request"
+                            />
+                        ) : (
+                            <div className="text-sm text-muted-foreground p-2 border rounded">
+                                {t('log.noRequestBody')}
+                            </div>
+                        )}
+                        {log.request_detail?.request_body_truncated && (
+                            <div className="text-xs text-amber-600 mt-1">⚠️ {t('log.contentTruncated')}</div>
+                        )}
+                    </div>
+                    <div>
+                        <h4 className="font-semibold text-sm mb-2">{t('log.responseBody')}</h4>
+                        {log.request_detail?.response_body ? (
+                            <JsonViewer
+                                src={log.request_detail.response_body}
+                                collapsed={1}
+                                name="response"
+                            />
+                        ) : (
+                            <div className="text-sm text-muted-foreground p-2 border rounded">
+                                {t('log.noResponseBody')}
+                            </div>
+                        )}
+                        {log.request_detail?.response_body_truncated && (
+                            <div className="text-xs text-amber-600 mt-1">⚠️ {t('log.contentTruncated')}</div>
+                        )}
+                    </div>
+                </div>
+            </div>
+        )
+    }
+
+    return (
+        <div className="h-full flex flex-col">
+            <div className="flex-1 min-h-0">
+                <div className="rounded-lg border border-border bg-card shadow-none h-full overflow-hidden">
+                    <div className="overflow-auto h-full">
+                        <table className="w-full table-fixed">
+                            <thead className="sticky top-0 bg-muted/50 backdrop-blur-sm">
+                                <tr className="border-b border-border">
+                                    {table.getHeaderGroups().map((headerGroup) =>
+                                        headerGroup.headers.map((header, index) => (
+                                            <th
+                                                key={header.id}
+                                                className={`px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider ${
+                                                    index === 0 ? 'rounded-tl-lg' : ''
+                                                } ${
+                                                    index === headerGroup.headers.length - 1 ? 'rounded-tr-lg' : ''
+                                                }`}
+                                                style={{ width: header.getSize() }}
+                                            >
+                                                {header.isPlaceholder
+                                                    ? null
+                                                    : flexRender(
+                                                        header.column.columnDef.header,
+                                                        header.getContext()
+                                                    )}
+                                            </th>
+                                        ))
+                                    )}
+                                </tr>
+                            </thead>
+                            <tbody>
+                                {loading ? (
+                                    <tr>
+                                        <td colSpan={columns.length} className="px-4 py-8 text-center">
+                                            <div className="flex items-center justify-center">
+                                                <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
+                                                <span className="ml-2">{t('common.loading')}</span>
+                                            </div>
+                                        </td>
+                                    </tr>
+                                ) : data.length === 0 ? (
+                                    <tr>
+                                        <td colSpan={columns.length} className="px-4 py-8 text-center text-muted-foreground">
+                                            {t('common.noResult')}
+                                        </td>
+                                    </tr>
+                                ) : (
+                                    table.getRowModel().rows.map((row) => (
+                                        <React.Fragment key={row.original.id}>
+                                            <tr className="border-b border-border hover:bg-muted/50 transition-colors">
+                                                {row.getVisibleCells().map((cell) => (
+                                                    <td
+                                                        key={cell.id}
+                                                        className="px-4 py-3 text-sm"
+                                                        style={{ width: cell.column.getSize() }}
+                                                    >
+                                                        {flexRender(
+                                                            cell.column.columnDef.cell,
+                                                            cell.getContext()
+                                                        )}
+                                                    </td>
+                                                ))}
+                                            </tr>
+                                            {expandedRows.has(row.original.id) && (
+                                                <tr>
+                                                    <td colSpan={columns.length} className="p-0">
+                                                        {renderExpandedContent(row.original)}
+                                                    </td>
+                                                </tr>
+                                            )}
+                                        </React.Fragment>
+                                    ))
+                                )}
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+
+            {/* 分页控制 - 固定在底部 */}
+            <div className="flex-shrink-0 pt-4">
+                <div className="flex items-center justify-between px-2">
+                    <div className="flex-1 text-sm text-muted-foreground">
+                        {t('table.pageInfo', {
+                            current: page,
+                            total: Math.ceil(total / pageSize) || 1
+                        })}
+                    </div>
+                    <div className="flex items-center space-x-6 lg:space-x-8">
+                        <div className="flex items-center space-x-2">
+                            <p className="text-sm font-medium">{t('table.rowsPerPage')}</p>
+                            <select
+                                value={pageSize}
+                                onChange={(e) => onPageSizeChange(Number(e.target.value))}
+                                className="h-8 max-w-[80px] rounded border border-input bg-background px-2 text-sm"
+                            >
+                                {[10, 20, 30, 40, 50].map((size) => (
+                                    <option key={size} value={size}>
+                                        {size}
+                                    </option>
+                                ))}
+                            </select>
+                        </div>
+                        <div className="flex items-center space-x-2">
+                            <Button
+                                variant="outline"
+                                className="h-8 w-8 p-0"
+                                onClick={() => onPageChange(1)}
+                                disabled={page <= 1}
+                            >
+                                <ChevronsLeft className="h-4 w-4" />
+                            </Button>
+                            <Button
+                                variant="outline"
+                                className="h-8 w-8 p-0"
+                                onClick={() => onPageChange(Math.max(1, page - 1))}
+                                disabled={page <= 1}
+                            >
+                                <ChevronLeft className="h-4 w-4" />
+                            </Button>
+                            <Button
+                                variant="outline"
+                                className="h-8 w-8 p-0"
+                                onClick={() => onPageChange(Math.min(Math.ceil(total / pageSize), page + 1))}
+                                disabled={page >= Math.ceil(total / pageSize)}
+                            >
+                                <ChevronRight className="h-4 w-4" />
+                            </Button>
+                            <Button
+                                variant="outline"
+                                className="h-8 w-8 p-0"
+                                onClick={() => onPageChange(Math.ceil(total / pageSize))}
+                                disabled={page >= Math.ceil(total / pageSize)}
+                            >
+                                <ChevronsRight className="h-4 w-4" />
+                            </Button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    )
+} 

+ 21 - 0
web/src/feature/log/hooks.ts

@@ -0,0 +1,21 @@
+import { useQuery } from '@tanstack/react-query'
+import { logApi } from '@/api/log'
+import { LogFilters } from '@/types/log'
+
+// 获取日志数据
+export const useLogs = (filters?: LogFilters) => {
+    const query = useQuery({
+        queryKey: ['logs', filters],
+        queryFn: () => logApi.getLogData(filters),
+        // 5分钟刷新一次数据
+        refetchInterval: 5 * 60 * 1000,
+        // 窗口重新获得焦点时刷新
+        refetchOnWindowFocus: true,
+        // 禁用重试,避免错误时过多请求
+        retry: false,
+    })
+
+    return {
+        ...query,
+    }
+} 

+ 2 - 2
web/src/feature/model/components/ModelDialog.tsx

@@ -42,7 +42,8 @@ export function ModelDialog({
     const defaultValues = mode === 'update' && model
         ? {
             model: model.model,
-            type: model.type
+            type: model.type,
+            plugin: model.plugin
         }
         : {
             model: '',
@@ -69,7 +70,6 @@ export function ModelDialog({
                                 >
                                     <ModelForm
                                         mode={mode}
-                                        modelId={model?.model}
                                         defaultValues={defaultValues}
                                         onSuccess={() => onOpenChange(false)}
                                     />

+ 532 - 20
web/src/feature/model/components/ModelForm.tsx

@@ -1,8 +1,11 @@
 // src/feature/model/components/ModelForm.tsx
 import { useForm } from 'react-hook-form'
+import type { FieldErrors } from 'react-hook-form'
 import { zodResolver } from '@hookform/resolvers/zod'
 import { Input } from '@/components/ui/input'
 import { Button } from '@/components/ui/button'
+import { Switch } from '@/components/ui/switch'
+import { Label } from '@/components/ui/label'
 import {
     Form,
     FormControl,
@@ -18,28 +21,31 @@ import {
     SelectTrigger,
     SelectValue,
 } from '@/components/ui/select'
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
+import { ChevronDown, ChevronUp, Plus, X } from 'lucide-react'
 import { modelCreateSchema } from '@/validation/model'
 import { useCreateModel } from '../hooks'
 import { useTranslation } from 'react-i18next'
 import { ModelCreateForm } from '@/validation/model'
+import { Plugin, EngineConfig } from '@/types/model'
 import { AdvancedErrorDisplay } from '@/components/common/error/errorDisplay'
 import { AnimatedButton } from '@/components/ui/animation/components/animated-button'
+import { useState } from 'react'
+import { ENV } from '@/utils/env'
+import { ValidationErrorDisplay } from '@/components/common/error/validationErrorDisplay'
 
 interface ModelFormProps {
     mode?: 'create' | 'update'
-    modelId?: string
     onSuccess?: () => void
     defaultValues?: {
         model: string
         type: number
+        plugin?: Plugin
     }
 }
 
 export function ModelForm({
     mode = 'create',
-    // @ts-expect-error 忽略未使用参数
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    modelId,
     onSuccess,
     defaultValues = {
         model: '',
@@ -48,6 +54,10 @@ export function ModelForm({
 }: ModelFormProps) {
     const { t } = useTranslation()
 
+    // Plugin configuration expanded states
+    const [cachePluginExpanded, setCachePluginExpanded] = useState(false)
+    const [webSearchPluginExpanded, setWebSearchPluginExpanded] = useState(false)
+
     // API hooks
     const {
         createModel,
@@ -56,39 +66,223 @@ export function ModelForm({
         clearError
     } = useCreateModel()
 
-    // Form setup
+    // Form setup with simplified default values
     const form = useForm<ModelCreateForm>({
         resolver: zodResolver(modelCreateSchema),
-        defaultValues,
+        mode: 'onChange', // 启用实时验证
+        defaultValues: {
+            model: defaultValues.model || '',
+            type: defaultValues.type || 1,
+            plugin: {
+                cache: { enable: false, ...defaultValues.plugin?.cache },
+                "web-search": { enable: false, search_from: [], ...defaultValues.plugin?.["web-search"] },
+                "think-split": { enable: false, ...defaultValues.plugin?.["think-split"] },
+            }
+        },
     })
 
+    // Watch plugin enable states
+    const cacheEnabled = form.watch('plugin.cache.enable')
+    const webSearchEnabled = form.watch('plugin.web-search.enable')
+    const searchEngines = form.watch('plugin.web-search.search_from') || []
+
+    // Available search engine types
+    const availableEngineTypes = ['bing', 'google', 'arxiv', 'searchxng'] as const
+
+    // Watch form errors for debugging
+    const formErrors = form.formState.errors
+
+    // Add search engine
+    const addSearchEngine = () => {
+        const currentEngines = form.getValues('plugin.web-search.search_from') || []
+        const newEngine: EngineConfig = {
+            type: 'bing',
+            max_results: undefined,
+            spec: undefined
+        }
+        form.setValue('plugin.web-search.search_from', [...currentEngines, newEngine])
+    }
+
+    // Remove search engine
+    const removeSearchEngine = (index: number) => {
+        const currentEngines = form.getValues('plugin.web-search.search_from') || []
+        const newEngines = currentEngines.filter((_, i) => i !== index)
+        form.setValue('plugin.web-search.search_from', newEngines)
+    }
+
+    // Update search engine
+    const updateSearchEngine = (index: number, updates: Partial<EngineConfig>) => {
+        const currentEngines = form.getValues('plugin.web-search.search_from') || []
+        const newEngines = [...currentEngines]
+        newEngines[index] = { ...newEngines[index], ...updates }
+        form.setValue('plugin.web-search.search_from', newEngines)
+    }
+
+    // Render engine spec fields based on type
+    const renderEngineSpecFields = (engine: EngineConfig, index: number) => {
+        const engineType = engine.type
+        const spec = engine.spec || ({} as Record<string, unknown>)
+
+        switch (engineType) {
+            case 'google': {
+                const googleSpec = spec as { api_key?: string; cx?: string }
+                return (
+                    <div className="space-y-2">
+                        <div>
+                            <Label className="text-xs">{t("model.dialog.webSearchPlugin.engineSpec.apiKey")}</Label>
+                            <Input
+                                placeholder={t("model.dialog.webSearchPlugin.engineSpec.apiKeyPlaceholder")}
+                                value={googleSpec?.api_key || ''}
+                                onChange={(e) => updateSearchEngine(index, {
+                                    spec: { ...spec, api_key: e.target.value }
+                                })}
+                                className="mt-1"
+                            />
+                        </div>
+                        <div>
+                            <Label className="text-xs">{t("model.dialog.webSearchPlugin.engineSpec.cx")}</Label>
+                            <Input
+                                placeholder={t("model.dialog.webSearchPlugin.engineSpec.cxPlaceholder")}
+                                value={googleSpec?.cx || ''}
+                                onChange={(e) => updateSearchEngine(index, {
+                                    spec: { ...spec, cx: e.target.value }
+                                })}
+                                className="mt-1"
+                            />
+                        </div>
+                    </div>
+                )
+            }
+            case 'bing': {
+                const bingSpec = spec as { api_key?: string }
+                return (
+                    <div>
+                        <Label className="text-xs">{t("model.dialog.webSearchPlugin.engineSpec.apiKey")}</Label>
+                        <Input
+                            placeholder={t("model.dialog.webSearchPlugin.engineSpec.apiKeyPlaceholder")}
+                            value={bingSpec?.api_key || ''}
+                            onChange={(e) => updateSearchEngine(index, {
+                                spec: { ...spec, api_key: e.target.value }
+                            })}
+                            className="mt-1"
+                        />
+                    </div>
+                )
+            }
+            case 'searchxng': {
+                const searchxngSpec = spec as { base_url?: string }
+                return (
+                    <div>
+                        <Label className="text-xs">{t("model.dialog.webSearchPlugin.engineSpec.baseUrl")}</Label>
+                        <Input
+                            placeholder={t("model.dialog.webSearchPlugin.engineSpec.baseUrlPlaceholder")}
+                            value={searchxngSpec?.base_url || ''}
+                            onChange={(e) => updateSearchEngine(index, {
+                                spec: { ...spec, base_url: e.target.value }
+                            })}
+                            className="mt-1"
+                        />
+                    </div>
+                )
+            }
+            case 'arxiv':
+            default:
+                return null
+        }
+    }
+
     // Form submission handler
     const handleFormSubmit = (data: ModelCreateForm) => {
+        console.log('Form submitted with data:', data)
+
         // Clear previous errors
         if (clearError) clearError()
 
-        // Prepare data for API
-        const formData = {
-            model: data.model,
-            type: Number(data.type)
+        // Prepare plugin data - only include enabled plugins with their configured values
+        const pluginData = {}
+
+        // Cache plugin - 如果开启,必须有 enable 字段,其他字段可选
+        if (data.plugin?.cache?.enable) {
+            Object.assign(pluginData, {
+                cache: {
+                    enable: true,
+                    ...(data.plugin.cache.ttl && { ttl: data.plugin.cache.ttl }),
+                    ...(data.plugin.cache.item_max_size && { item_max_size: data.plugin.cache.item_max_size }),
+                    ...(data.plugin.cache.add_cache_hit_header !== undefined && { add_cache_hit_header: data.plugin.cache.add_cache_hit_header }),
+                    ...(data.plugin.cache.cache_hit_header && { cache_hit_header: data.plugin.cache.cache_hit_header }),
+                }
+            })
+        }
+
+        // Web search plugin - 如果开启,必须有 enable 和 search_from,其他字段可选
+        if (data.plugin?.["web-search"]?.enable && data.plugin["web-search"].search_from && data.plugin["web-search"].search_from.length > 0) {
+            // Clean up search engines - remove empty spec objects
+            const cleanedSearchFrom = data.plugin["web-search"].search_from.map(engine => ({
+                type: engine.type,
+                ...(engine.max_results && { max_results: engine.max_results }),
+                ...(engine.spec && Object.keys(engine.spec).some(key => (engine.spec as Record<string, unknown>)[key]) && { spec: engine.spec })
+            }))
+
+            Object.assign(pluginData, {
+                "web-search": {
+                    enable: true,
+                    search_from: cleanedSearchFrom,
+                    ...(data.plugin["web-search"].force_search !== undefined && { force_search: data.plugin["web-search"].force_search }),
+                    ...(data.plugin["web-search"].max_results && { max_results: data.plugin["web-search"].max_results }),
+                    ...(data.plugin["web-search"].need_reference !== undefined && { need_reference: data.plugin["web-search"].need_reference }),
+                    ...(data.plugin["web-search"].reference_location && { reference_location: data.plugin["web-search"].reference_location }),
+                    ...(data.plugin["web-search"].reference_format && { reference_format: data.plugin["web-search"].reference_format }),
+                    ...(data.plugin["web-search"].default_language && { default_language: data.plugin["web-search"].default_language }),
+                    ...(data.plugin["web-search"].prompt_template && { prompt_template: data.plugin["web-search"].prompt_template }),
+                }
+            })
         }
 
-        if (mode === 'create') {
-            createModel(formData, {
-                onSuccess: () => {
-                    // Reset form
-                    form.reset()
-                    // Notify parent component
-                    if (onSuccess) onSuccess()
+        // Think split plugin - 如果开启,必须有 enable 字段
+        if (data.plugin?.["think-split"]?.enable) {
+            Object.assign(pluginData, {
+                "think-split": {
+                    enable: true
                 }
             })
         }
+
+        // Prepare data for API - 如果没有启用的插件,则不传递 plugin 字段
+        const formData: { model: string; type: number; plugin?: Plugin } = {
+            model: data.model,
+            type: Number(data.type),
+            ...(Object.keys(pluginData).length > 0 && { plugin: pluginData as Plugin })
+        }
+
+        createModel(formData, {
+            onSuccess: () => {
+                // Reset form
+                form.reset()
+                // Notify parent component
+                if (onSuccess) onSuccess()
+            }
+        })
     }
 
     return (
         <div>
+            {/* 使用简化的验证错误显示组件 */}
+            <ValidationErrorDisplay
+                errors={formErrors as FieldErrors<Record<string, unknown>>}
+                className="mb-4"
+            />
+
             <Form {...form}>
-                <form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-6">
+                <form onSubmit={form.handleSubmit(handleFormSubmit, (errors) => {
+                    // 处理表单验证失败
+                    console.error('Form validation failed:', errors)
+                    if (ENV.isDevelopment) {
+                        console.group('🔴 Form Submission Failed:')
+                        console.log('Validation Errors:', errors)
+                        console.log('Current Form Values:', form.getValues())
+                        console.groupEnd()
+                    }
+                })} className="space-y-6">
                     {/* API error alert */}
                     {error && (
                         <AdvancedErrorDisplay error={error} />
@@ -102,9 +296,19 @@ export function ModelForm({
                             <FormItem>
                                 <FormLabel>{t("model.dialog.modelName")}</FormLabel>
                                 <FormControl>
-                                    <Input placeholder={t("model.dialog.modelNamePlaceholder")} {...field} />
+                                    <Input
+                                        placeholder={t("model.dialog.modelNamePlaceholder")}
+                                        {...field}
+                                        disabled={mode === 'update'}
+                                        className={mode === 'update' ? 'bg-muted' : ''}
+                                    />
                                 </FormControl>
                                 <FormMessage />
+                                {mode === 'update' && (
+                                    <p className="text-xs text-muted-foreground">
+                                        {t("model.dialog.modelNameUpdateDisabled")}
+                                    </p>
+                                )}
                             </FormItem>
                         )}
                     />
@@ -126,7 +330,7 @@ export function ModelForm({
                                         </SelectTrigger>
                                     </FormControl>
                                     <SelectContent>
-                                        {Array.from({ length: 11 }, (_, i) => i + 1).map((type) => (
+                                        {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13].map((type) => (
                                             <SelectItem key={type} value={String(type)}>
                                                 {t(`modeType.${type}` as never)}
                                             </SelectItem>
@@ -138,6 +342,314 @@ export function ModelForm({
                         )}
                     />
 
+                    {/* Plugin Configuration Section */}
+                    <div className="space-y-6">
+                        <div>
+                            <h3 className="text-lg font-medium">{t("model.dialog.pluginConfiguration")}</h3>
+                            <p className="text-sm text-muted-foreground">{t("model.dialog.pluginConfigurationDescription")}</p>
+                        </div>
+                        
+                        <hr className="border-border" />
+
+                        {/* Cache Plugin */}
+                        <div className="space-y-4">
+                            <Collapsible open={cachePluginExpanded} onOpenChange={setCachePluginExpanded}>
+                                <div className="flex items-center justify-between py-2">
+                                    <div className="flex items-center space-x-3">
+                                        <FormField
+                                            control={form.control}
+                                            name="plugin.cache.enable"
+                                            render={({ field }) => (
+                                                <FormItem className="flex items-center space-x-2">
+                                                    <FormControl>
+                                                        <Switch
+                                                            checked={field.value}
+                                                            onCheckedChange={field.onChange}
+                                                        />
+                                                    </FormControl>
+                                                </FormItem>
+                                            )}
+                                        />
+                                        <div>
+                                            <Label className="text-sm font-medium">{t("model.dialog.cachePlugin.title")}</Label>
+                                            <p className="text-xs text-muted-foreground">{t("model.dialog.cachePlugin.description")}</p>
+                                        </div>
+                                    </div>
+                                    {cacheEnabled && (
+                                        <CollapsibleTrigger asChild>
+                                            <Button variant="ghost" size="sm">
+                                                {cachePluginExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
+                                            </Button>
+                                        </CollapsibleTrigger>
+                                    )}
+                                </div>
+                                {cacheEnabled && (
+                                    <CollapsibleContent className="space-y-4 pl-8 pb-4">
+                                        {/* TTL Field */}
+                                        <FormField
+                                            control={form.control}
+                                            name="plugin.cache.ttl"
+                                            render={({ field }) => (
+                                                <FormItem>
+                                                    <FormLabel>{t("model.dialog.cachePlugin.ttl")}</FormLabel>
+                                                    <FormControl>
+                                                        <Input
+                                                            type="number"
+                                                            placeholder={t("model.dialog.cachePlugin.ttlPlaceholder")}
+                                                            {...field}
+                                                            onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
+                                                        />
+                                                    </FormControl>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+
+                                        {/* Item Max Size Field */}
+                                        <FormField
+                                            control={form.control}
+                                            name="plugin.cache.item_max_size"
+                                            render={({ field }) => (
+                                                <FormItem>
+                                                    <FormLabel>{t("model.dialog.cachePlugin.itemMaxSize")}</FormLabel>
+                                                    <FormControl>
+                                                        <Input
+                                                            type="number"
+                                                            placeholder={t("model.dialog.cachePlugin.itemMaxSizePlaceholder")}
+                                                            {...field}
+                                                            onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
+                                                        />
+                                                    </FormControl>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+
+                                        {/* Add Cache Hit Header */}
+                                        <FormField
+                                            control={form.control}
+                                            name="plugin.cache.add_cache_hit_header"
+                                            render={({ field }) => (
+                                                <FormItem className="flex flex-row items-center justify-between py-2">
+                                                    <FormLabel>{t("model.dialog.cachePlugin.addCacheHitHeader")}</FormLabel>
+                                                    <FormControl>
+                                                        <Switch
+                                                            checked={field.value}
+                                                            onCheckedChange={field.onChange}
+                                                        />
+                                                    </FormControl>
+                                                </FormItem>
+                                            )}
+                                        />
+
+                                        {/* Cache Hit Header Name */}
+                                        {form.watch('plugin.cache.add_cache_hit_header') && (
+                                            <FormField
+                                                control={form.control}
+                                                name="plugin.cache.cache_hit_header"
+                                                render={({ field }) => (
+                                                    <FormItem>
+                                                        <FormLabel>{t("model.dialog.cachePlugin.cacheHitHeader")}</FormLabel>
+                                                        <FormControl>
+                                                            <Input placeholder={t("model.dialog.cachePlugin.cacheHitHeaderPlaceholder")} {...field} />
+                                                        </FormControl>
+                                                        <FormMessage />
+                                                    </FormItem>
+                                                )}
+                                            />
+                                        )}
+                                    </CollapsibleContent>
+                                )}
+                            </Collapsible>
+                        </div>
+
+                        <hr className="border-border" />
+
+                        {/* Web Search Plugin */}
+                        <div className="space-y-4">
+                            <Collapsible open={webSearchPluginExpanded} onOpenChange={setWebSearchPluginExpanded}>
+                                <div className="flex items-center justify-between py-2">
+                                    <div className="flex items-center space-x-3">
+                                        <FormField
+                                            control={form.control}
+                                            name="plugin.web-search.enable"
+                                            render={({ field }) => (
+                                                <FormItem className="flex items-center space-x-2">
+                                                    <FormControl>
+                                                        <Switch
+                                                            checked={field.value}
+                                                            onCheckedChange={field.onChange}
+                                                        />
+                                                    </FormControl>
+                                                </FormItem>
+                                            )}
+                                        />
+                                        <div>
+                                            <Label className="text-sm font-medium">{t("model.dialog.webSearchPlugin.title")}</Label>
+                                            <p className="text-xs text-muted-foreground">{t("model.dialog.webSearchPlugin.description")}</p>
+                                        </div>
+                                    </div>
+                                    {webSearchEnabled && (
+                                        <CollapsibleTrigger asChild>
+                                            <Button variant="ghost" size="sm">
+                                                {webSearchPluginExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
+                                            </Button>
+                                        </CollapsibleTrigger>
+                                    )}
+                                </div>
+                                {webSearchEnabled && (
+                                    <CollapsibleContent className="space-y-4 pl-8 pb-4">
+                                        {/* Search Engines Configuration */}
+                                        <div>
+                                            <div className="flex items-center justify-between mb-3">
+                                                <Label className="text-sm font-medium">{t("model.dialog.webSearchPlugin.searchFrom")}</Label>
+                                                <Button
+                                                    type="button"
+                                                    variant="outline"
+                                                    size="sm"
+                                                    onClick={addSearchEngine}
+                                                    className="flex items-center gap-1"
+                                                >
+                                                    <Plus className="h-3 w-3" />
+                                                    {t("model.dialog.webSearchPlugin.addEngine")}
+                                                </Button>
+                                            </div>
+
+                                            <div className="space-y-3">
+                                                {searchEngines.map((engine, index) => (
+                                                    <div key={index} className="p-4 bg-muted/30 rounded-lg">
+                                                        <div className="flex items-start justify-between mb-3">
+                                                            <Label className="text-sm font-medium">
+                                                                {t("model.dialog.webSearchPlugin.engineConfig")} #{index + 1}
+                                                            </Label>
+                                                            <Button
+                                                                type="button"
+                                                                variant="ghost"
+                                                                size="sm"
+                                                                onClick={() => removeSearchEngine(index)}
+                                                                className="h-6 w-6 p-0 text-destructive hover:text-destructive"
+                                                            >
+                                                                <X className="h-3 w-3" />
+                                                            </Button>
+                                                        </div>
+
+                                                        <div className="space-y-3">
+                                                            {/* Engine Type */}
+                                                            <div>
+                                                                <Label className="text-xs">{t("model.dialog.webSearchPlugin.engineType")}</Label>
+                                                                <Select
+                                                                    value={engine.type}
+                                                                    onValueChange={(value) => updateSearchEngine(index, { type: value as 'bing' | 'google' | 'arxiv' | 'searchxng' })}
+                                                                >
+                                                                    <SelectTrigger className="mt-1">
+                                                                        <SelectValue />
+                                                                    </SelectTrigger>
+                                                                    <SelectContent>
+                                                                        {availableEngineTypes.map((type) => (
+                                                                            <SelectItem key={type} value={type}>
+                                                                                {t(`model.dialog.webSearchPlugin.searchEngines.${type}` as 'model.dialog.webSearchPlugin.searchEngines.bing' | 'model.dialog.webSearchPlugin.searchEngines.google' | 'model.dialog.webSearchPlugin.searchEngines.arxiv' | 'model.dialog.webSearchPlugin.searchEngines.searchxng')}
+                                                                            </SelectItem>
+                                                                        ))}
+                                                                    </SelectContent>
+                                                                </Select>
+                                                            </div>
+
+                                                            {/* Max Results */}
+                                                            <div>
+                                                                <Label className="text-xs">{t("model.dialog.webSearchPlugin.maxResults")}</Label>
+                                                                <Input
+                                                                    type="number"
+                                                                    placeholder={t("model.dialog.webSearchPlugin.maxResultsPlaceholder")}
+                                                                    value={engine.max_results || ''}
+                                                                    onChange={(e) => updateSearchEngine(index, {
+                                                                        max_results: e.target.value ? Number(e.target.value) : undefined
+                                                                    })}
+                                                                    className="mt-1"
+                                                                />
+                                                            </div>
+
+                                                            {/* Engine Specific Configuration */}
+                                                            {renderEngineSpecFields(engine, index)}
+                                                        </div>
+                                                    </div>
+                                                ))}
+
+                                                {searchEngines.length === 0 && (
+                                                    <div className="text-center py-8 text-muted-foreground text-sm border-2 border-dashed rounded-lg">
+                                                        {t("model.dialog.noSearchEngineConfigured")}
+                                                    </div>
+                                                )}
+                                            </div>
+                                        </div>
+
+                                        {/* Force Search */}
+                                        <FormField
+                                            control={form.control}
+                                            name="plugin.web-search.force_search"
+                                            render={({ field }) => (
+                                                <FormItem className="flex flex-row items-center justify-between py-2">
+                                                    <FormLabel>{t("model.dialog.webSearchPlugin.forceSearch")}</FormLabel>
+                                                    <FormControl>
+                                                        <Switch
+                                                            checked={field.value}
+                                                            onCheckedChange={field.onChange}
+                                                        />
+                                                    </FormControl>
+                                                </FormItem>
+                                            )}
+                                        />
+
+                                        {/* Global Max Results */}
+                                        <FormField
+                                            control={form.control}
+                                            name="plugin.web-search.max_results"
+                                            render={({ field }) => (
+                                                <FormItem>
+                                                    <FormLabel>{t("model.dialog.webSearchPlugin.maxResults")} ({t("common.global")})</FormLabel>
+                                                    <FormControl>
+                                                        <Input
+                                                            type="number"
+                                                            placeholder={t("model.dialog.webSearchPlugin.maxResultsPlaceholder")}
+                                                            {...field}
+                                                            onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
+                                                        />
+                                                    </FormControl>
+                                                    <FormMessage />
+                                                </FormItem>
+                                            )}
+                                        />
+                                    </CollapsibleContent>
+                                )}
+                            </Collapsible>
+                        </div>
+
+                        <hr className="border-border" />
+
+                        {/* Think Split Plugin */}
+                        <div className="flex items-center justify-between py-2">
+                            <div className="flex items-center space-x-3">
+                                <FormField
+                                    control={form.control}
+                                    name="plugin.think-split.enable"
+                                    render={({ field }) => (
+                                        <FormItem className="flex items-center space-x-2">
+                                            <FormControl>
+                                                <Switch
+                                                    checked={field.value}
+                                                    onCheckedChange={field.onChange}
+                                                />
+                                            </FormControl>
+                                        </FormItem>
+                                    )}
+                                />
+                                <div>
+                                    <Label className="text-sm font-medium">{t("model.dialog.thinkSplitPlugin.title")}</Label>
+                                    <p className="text-xs text-muted-foreground">{t("model.dialog.thinkSplitPlugin.description")}</p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
                     {/* Submit button */}
                     <div className="flex justify-end">
                         <AnimatedButton >

+ 82 - 12
web/src/feature/model/components/ModelTable.tsx

@@ -1,11 +1,9 @@
 // src/feature/model/components/ModelTable.tsx
-import { useState } from 'react'
+import { useState, useMemo } from 'react'
 import { useModels } from '../hooks'
 import { ModelConfig } from '@/types/model'
 import { Button } from '@/components/ui/button'
 import {
-    // @ts-expect-error 忽略未使用参数
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
     MoreHorizontal, Plus, Trash2, RefreshCcw, Pencil, FileText,
 } from 'lucide-react'
 import {
@@ -18,11 +16,12 @@ import { DeleteModelDialog } from './DeleteModelDialog'
 import { useTranslation } from 'react-i18next'
 import { DataTable } from '@/components/table/motion-data-table'
 import { ColumnDef } from '@tanstack/react-table'
-import { useReactTable, getCoreRowModel } from '@tanstack/react-table'
+import { useReactTable, getCoreRowModel, getSortedRowModel } from '@tanstack/react-table'
 import { AdvancedErrorDisplay } from '@/components/common/error/errorDisplay'
 import { AnimatedButton } from '@/components/ui/animation/components/animated-button'
 import { AnimatedIcon } from '@/components/ui/animation/components/animated-icon'
 import ApiDocDrawer from './api-doc/ApiDoc'
+import { Badge } from '@/components/ui/badge'
 
 export function ModelTable() {
     const { t } = useTranslation()
@@ -47,6 +46,18 @@ export function ModelTable() {
         refetch
     } = useModels()
 
+    // Sort models by type for stable sorting
+    const sortedModels = useMemo(() => {
+        if (!models) return []
+        return [...models].sort((a, b) => {
+            if (a.type === b.type) {
+                // Secondary sort by model name for stability
+                return a.model.localeCompare(b.model)
+            }
+            return a.type - b.type
+        })
+    }, [models])
+
     // Create table columns
     const columns: ColumnDef<ModelConfig>[] = [
         {
@@ -64,6 +75,56 @@ export function ModelTable() {
                 </div>
             ),
         },
+        {
+            accessorKey: 'plugin',
+            header: () => <div className="font-medium py-3.5">{t("model.pluginInfo")}</div>,
+            cell: ({ row }) => {
+                const plugin = row.original.plugin
+                if (!plugin) {
+                    return (
+                        <div className="text-muted-foreground text-sm">
+                            {t("model.noPluginConfigured")}
+                        </div>
+                    )
+                }
+
+                const enabledPlugins = []
+                
+                if (plugin.cache?.enable) {
+                    enabledPlugins.push(t("model.cachePlugin"))
+                }
+                
+                if (plugin["web-search"]?.enable) {
+                    enabledPlugins.push(t("model.webSearchPlugin"))
+                }
+                
+                if (plugin["think-split"]?.enable) {
+                    enabledPlugins.push(t("model.thinkSplitPlugin"))
+                }
+
+                if (enabledPlugins.length === 0) {
+                    return (
+                        <div className="text-muted-foreground text-sm">
+                            {t("model.noPluginConfigured")}
+                        </div>
+                    )
+                }
+
+                return (
+                    <div className="flex flex-wrap gap-1">
+                        {enabledPlugins.map((pluginName) => (
+                            <Badge
+                                key={pluginName}
+                                variant="outline"
+                                className="text-xs bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800"
+                            >
+                                {pluginName}
+                            </Badge>
+                        ))}
+                    </div>
+                )
+            },
+        },
         // {
         //     accessorKey: 'owner',
         //     header: () => <div className="font-medium py-3.5">{t("model.owner")}</div>,
@@ -90,12 +151,12 @@ export function ModelTable() {
                             <FileText className="mr-2 h-4 w-4" />
                             {t("model.apiDetails")}
                         </DropdownMenuItem>
-                        {/* <DropdownMenuItem
+                        <DropdownMenuItem
                             onClick={() => openUpdateDialog(row.original)}
                         >
                             <Pencil className="mr-2 h-4 w-4" />
                             {t("model.edit")}
-                        </DropdownMenuItem> */}
+                        </DropdownMenuItem>
                         <DropdownMenuItem
                             onClick={() => openDeleteDialog(row.original.model)}
                         >
@@ -110,9 +171,18 @@ export function ModelTable() {
 
     // Initialize table
     const table = useReactTable({
-        data: models || [],
+        data: sortedModels,
         columns,
         getCoreRowModel: getCoreRowModel(),
+        getSortedRowModel: getSortedRowModel(),
+        initialState: {
+            sorting: [
+                {
+                    id: 'type',
+                    desc: false,
+                },
+            ],
+        },
     })
 
     // Open create model dialog
@@ -123,11 +193,11 @@ export function ModelTable() {
     }
 
     // Open update model dialog
-    // const openUpdateDialog = (model: ModelConfig) => {
-    //     setDialogMode('update')
-    //     setSelectedModel(model)
-    //     setModelDialogOpen(true)
-    // }
+    const openUpdateDialog = (model: ModelConfig) => {
+        setDialogMode('update')
+        setSelectedModel(model)
+        setModelDialogOpen(true)
+    }
 
     // Open delete dialog
     const openDeleteDialog = (id: string) => {

+ 160 - 0
web/src/feature/monitor/components/MetricsCards.tsx

@@ -0,0 +1,160 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+    Activity,
+    AlertTriangle,
+    BarChart3,
+    Zap,
+    MessageSquare
+} from 'lucide-react'
+
+import { Card, CardHeader, CardTitle } from '@/components/ui/card'
+import { Skeleton } from '@/components/ui/skeleton'
+import {
+    Tooltip,
+    TooltipContent,
+    TooltipProvider,
+    TooltipTrigger,
+} from '@/components/ui/tooltip'
+import { DashboardData } from '@/types/dashboard'
+import { cn } from '@/lib/utils'
+import { TFunction } from 'i18next'
+
+interface MetricsCardsProps {
+    data: DashboardData
+    loading?: boolean
+}
+
+interface MetricCardProps {
+    title: string
+    value: number | string
+    icon: React.ReactNode
+    className?: string
+    tooltip?: string
+    bgColor?: string
+    iconColor?: string
+    t?: TFunction
+}
+
+function MetricCard({ title, value, icon, className, tooltip, bgColor, iconColor, t }: MetricCardProps) {
+    const formattedValue = typeof value === 'number' ? value.toLocaleString() : value
+
+    const cardContent = (
+        <Card className={cn(
+            "border-0 shadow-sm hover:shadow-md transition-all duration-200 h-28",
+            "dark:bg-card dark:shadow-lg dark:hover:shadow-xl",
+            bgColor,
+            className
+        )}>
+            <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 pt-4">
+                <div className={cn("p-2 rounded-lg", iconColor)}>
+                    {icon}
+                </div>
+                <div className="text-right flex-1 ml-3">
+                    <CardTitle className="text-xs font-medium text-muted-foreground mb-1 leading-tight">
+                        {title}
+                    </CardTitle>
+                    <div className="text-2xl font-bold text-foreground truncate">
+                        {formattedValue}
+                    </div>
+                </div>
+            </CardHeader>
+        </Card>
+    )
+
+    if (tooltip) {
+        return (
+            <TooltipProvider>
+                <Tooltip>
+                    <TooltipTrigger asChild>
+                        {cardContent}
+                    </TooltipTrigger>
+                    <TooltipContent>
+                        <p>{tooltip}</p>
+                        {t && (
+                            <p className="text-xs text-foreground mt-1">
+                                {t('monitor.metrics.fullValue')}: {formattedValue}
+                            </p>
+                        )}
+                    </TooltipContent>
+                </Tooltip>
+            </TooltipProvider>
+        )
+    }
+
+    return cardContent
+}
+
+export function MetricsCards({ data, loading = false }: MetricsCardsProps) {
+    const { t } = useTranslation()
+
+    if (loading) {
+        return (
+            <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
+                {Array.from({ length: 5 }).map((_, index) => (
+                    <Card key={index} className="border-0 shadow-sm h-28 dark:bg-card">
+                        <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 pt-4">
+                            <div className="p-2">
+                                <Skeleton className="h-5 w-5" />
+                            </div>
+                            <div className="text-right flex-1 ml-3">
+                                <Skeleton className="h-3 w-16 mb-2" />
+                                <Skeleton className="h-6 w-12" />
+                            </div>
+                        </CardHeader>
+                    </Card>
+                ))}
+            </div>
+        )
+    }
+
+    return (
+        <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
+            <MetricCard
+                title={t('monitor.metrics.totalRequests')}
+                value={data.total_count}
+                icon={<Activity className="h-5 w-5 text-blue-600 dark:text-blue-400" />}
+                bgColor="bg-blue-50 dark:bg-blue-950/30"
+                iconColor="bg-blue-100 dark:bg-blue-900/50"
+                tooltip={t('monitor.metrics.totalRequestsTooltip')}
+                t={t}
+            />
+            <MetricCard
+                title={t('monitor.metrics.errorCount')}
+                value={data.exception_count}
+                icon={<AlertTriangle className="h-5 w-5 text-orange-600 dark:text-orange-400" />}
+                bgColor="bg-orange-50 dark:bg-orange-950/30"
+                iconColor="bg-orange-100 dark:bg-orange-900/50"
+                tooltip={t('monitor.metrics.errorCountTooltip')}
+                t={t}
+            />
+            <MetricCard
+                title={t('monitor.metrics.currentRpm')}
+                value={data.rpm}
+                icon={<BarChart3 className="h-5 w-5 text-blue-600 dark:text-blue-400" />}
+                bgColor="bg-blue-50 dark:bg-blue-950/30"
+                iconColor="bg-blue-100 dark:bg-blue-900/50"
+                tooltip={t('monitor.metrics.currentRpmTooltip')}
+                t={t}
+            />
+            <MetricCard
+                title={t('monitor.metrics.currentTpm')}
+                value={data.tpm}
+                icon={<Zap className="h-5 w-5 text-purple-600 dark:text-purple-400" />}
+                bgColor="bg-purple-50 dark:bg-purple-950/30"
+                iconColor="bg-purple-100 dark:bg-purple-900/50"
+                tooltip={t('monitor.metrics.currentTpmTooltip')}
+                t={t}
+            />
+            <MetricCard
+                title={t('monitor.metrics.outputTokens')}
+                value={data.output_tokens}
+                icon={<MessageSquare className="h-5 w-5 text-green-600 dark:text-green-400" />}
+                bgColor="bg-green-50 dark:bg-green-950/30"
+                iconColor="bg-green-100 dark:bg-green-900/50"
+                tooltip={t('monitor.metrics.outputTokensTooltip')}
+                t={t}
+            />
+        </div>
+    )
+} 

+ 519 - 0
web/src/feature/monitor/components/MonitorCharts.tsx

@@ -0,0 +1,519 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import type { EChartsOption } from 'echarts'
+
+import { EChart } from '@/components/ui/echarts'
+import { Skeleton } from '@/components/ui/skeleton'
+import { useTheme } from '@/handler/ThemeContext'
+import { ChartDataPoint } from '@/types/dashboard'
+
+interface MonitorChartsProps {
+    chartData: ChartDataPoint[]
+    loading?: boolean
+}
+
+export function MonitorCharts({ chartData, loading = false }: MonitorChartsProps) {
+    const { t } = useTranslation()
+    const { theme } = useTheme()
+
+    // 清新现代的颜色配置 - 适配暗色模式
+    const colorPalette = [
+        '#3b82f6',  // 蓝色 - 缓存创建 Tokens
+        '#8b5cf6',  // 紫色 - 缓存 Tokens  
+        '#06b6d4',  // 青色 - 输入 Tokens
+        '#10b981',  // 绿色 - 输出 Tokens
+        '#f59e0b',  // 橙色 - 总 Tokens
+        '#ec4899'   // 粉色 - 搜索次数
+    ]
+
+    // 检测暗色模式 - 基于主题设置或系统偏好
+    const isDarkMode = useMemo(() => {
+        if (theme === 'dark') return true
+        if (theme === 'light') return false
+        // 如果是 system,检查系统偏好
+        return window.matchMedia('(prefers-color-scheme: dark)').matches
+    }, [theme])
+
+    // 根据主题模式调整颜色
+    const getThemeColors = () => ({
+        textColor: isDarkMode ? '#e5e7eb' : '#666',
+        axisLineColor: isDarkMode ? '#374151' : '#e1e4e8',
+        splitLineColor: isDarkMode ? '#374151' : '#f0f0f0',
+        tooltipBg: isDarkMode ? 'rgba(31, 41, 55, 0.95)' : 'rgba(255, 255, 255, 0.95)',
+        tooltipBorder: isDarkMode ? '#4b5563' : '#e1e4e8',
+        tooltipTextColor: isDarkMode ? '#f3f4f6' : '#333',
+        crossLabelBg: isDarkMode ? '#4b5563' : '#283042'
+    })
+
+    const themeColors = getThemeColors()
+
+    // Tokens 相关图表配置
+    const tokensChartOption: EChartsOption = useMemo(() => {
+        const timestamps = chartData.map(item => new Date(item.timestamp * 1000).toLocaleString())
+
+        return {
+            backgroundColor: 'transparent',
+            tooltip: {
+                trigger: 'axis',
+                axisPointer: {
+                    type: 'cross',
+                    label: {
+                        backgroundColor: themeColors.crossLabelBg,
+                        borderColor: themeColors.crossLabelBg,
+                        borderWidth: 1,
+                        borderRadius: 4,
+                        color: '#fff'
+                    },
+                    crossStyle: {
+                        color: themeColors.textColor
+                    }
+                },
+                backgroundColor: themeColors.tooltipBg,
+                borderColor: themeColors.tooltipBorder,
+                borderWidth: 1,
+                borderRadius: 8,
+                textStyle: {
+                    color: themeColors.tooltipTextColor,
+                    fontSize: 12
+                },
+                extraCssText: `box-shadow: 0 4px 12px ${isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)'};`
+            },
+            legend: {
+                top: '10px',
+                data: [
+                    t('monitor.charts.tokensChart.cacheCreationTokens'),
+                    t('monitor.charts.tokensChart.cachedTokens'),
+                    t('monitor.charts.tokensChart.inputTokens'),
+                    t('monitor.charts.tokensChart.outputTokens'),
+                    t('monitor.charts.tokensChart.totalTokens'),
+                    t('monitor.charts.tokensChart.webSearchCount')
+                ],
+                textStyle: {
+                    color: themeColors.textColor,
+                    fontSize: 12
+                },
+                itemGap: 20,
+                icon: 'circle'
+            },
+            grid: {
+                left: '12px',
+                right: '12px',
+                bottom: '3%',
+                top: '50px',
+                containLabel: true
+            },
+            xAxis: {
+                type: 'category',
+                boundaryGap: false,
+                data: timestamps,
+                axisLine: {
+                    lineStyle: {
+                        color: themeColors.axisLineColor,
+                        width: 1
+                    }
+                },
+                axisLabel: {
+                    color: themeColors.textColor,
+                    fontSize: 11,
+                    margin: 10
+                },
+                axisTick: {
+                    show: false
+                },
+                splitLine: {
+                    show: false
+                }
+            },
+            yAxis: {
+                type: 'value',
+                axisLine: {
+                    show: false
+                },
+                axisLabel: {
+                    color: themeColors.textColor,
+                    fontSize: 11,
+                    margin: 10
+                },
+                axisTick: {
+                    show: false
+                },
+                splitLine: {
+                    lineStyle: {
+                        color: themeColors.splitLineColor,
+                        type: 'dashed',
+                        width: 1
+                    }
+                }
+            },
+            series: [
+                {
+                    name: t('monitor.charts.tokensChart.cacheCreationTokens'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 5,
+                    lineStyle: {
+                        width: 2,
+                        color: colorPalette[0],
+                        shadowColor: `${colorPalette[0]}20`,
+                        shadowBlur: 4,
+                        shadowOffsetY: 1
+                    },
+                    itemStyle: {
+                        color: colorPalette[0],
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 10,
+                            shadowColor: `${colorPalette[0]}40`,
+                            shadowOffsetY: 2
+                        }
+                    },
+                    data: chartData.map(item => item.cache_creation_tokens)
+                },
+                {
+                    name: t('monitor.charts.tokensChart.cachedTokens'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 5,
+                    lineStyle: {
+                        width: 2,
+                        color: colorPalette[1],
+                        shadowColor: `${colorPalette[1]}20`,
+                        shadowBlur: 4,
+                        shadowOffsetY: 1
+                    },
+                    itemStyle: {
+                        color: colorPalette[1],
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 10,
+                            shadowColor: `${colorPalette[1]}40`,
+                            shadowOffsetY: 2
+                        }
+                    },
+                    data: chartData.map(item => item.cached_tokens)
+                },
+                {
+                    name: t('monitor.charts.tokensChart.inputTokens'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 5,
+                    lineStyle: {
+                        width: 2,
+                        color: colorPalette[2],
+                        shadowColor: `${colorPalette[2]}20`,
+                        shadowBlur: 4,
+                        shadowOffsetY: 1
+                    },
+                    itemStyle: {
+                        color: colorPalette[2],
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 10,
+                            shadowColor: `${colorPalette[2]}40`,
+                            shadowOffsetY: 2
+                        }
+                    },
+                    data: chartData.map(item => item.input_tokens)
+                },
+                {
+                    name: t('monitor.charts.tokensChart.outputTokens'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 5,
+                    lineStyle: {
+                        width: 2,
+                        color: colorPalette[3],
+                        shadowColor: `${colorPalette[3]}20`,
+                        shadowBlur: 4,
+                        shadowOffsetY: 1
+                    },
+                    itemStyle: {
+                        color: colorPalette[3],
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 10,
+                            shadowColor: `${colorPalette[3]}40`,
+                            shadowOffsetY: 2
+                        }
+                    },
+                    data: chartData.map(item => item.output_tokens)
+                },
+                {
+                    name: t('monitor.charts.tokensChart.totalTokens'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 5,
+                    lineStyle: {
+                        width: 2,
+                        color: colorPalette[4],
+                        shadowColor: `${colorPalette[4]}20`,
+                        shadowBlur: 4,
+                        shadowOffsetY: 1
+                    },
+                    itemStyle: {
+                        color: colorPalette[4],
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 10,
+                            shadowColor: `${colorPalette[4]}40`,
+                            shadowOffsetY: 2
+                        }
+                    },
+                    data: chartData.map(item => item.total_tokens)
+                },
+                {
+                    name: t('monitor.charts.tokensChart.webSearchCount'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 5,
+                    lineStyle: {
+                        width: 2,
+                        color: colorPalette[5],
+                        shadowColor: `${colorPalette[5]}20`,
+                        shadowBlur: 4,
+                        shadowOffsetY: 1
+                    },
+                    itemStyle: {
+                        color: colorPalette[5],
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 10,
+                            shadowColor: `${colorPalette[5]}40`,
+                            shadowOffsetY: 2
+                        }
+                    },
+                    data: chartData.map(item => item.web_search_count)
+                }
+            ],
+            animation: true,
+            animationDuration: 1000,
+            animationEasing: 'cubicOut'
+        }
+    }, [chartData, t, isDarkMode, themeColors])
+
+    // 请求和错误图表配置
+    const requestsChartOption: EChartsOption = useMemo(() => {
+        const timestamps = chartData.map(item => new Date(item.timestamp * 1000).toLocaleString())
+
+        return {
+            backgroundColor: 'transparent',
+            tooltip: {
+                trigger: 'axis',
+                axisPointer: {
+                    type: 'cross',
+                    label: {
+                        backgroundColor: themeColors.crossLabelBg,
+                        borderColor: themeColors.crossLabelBg,
+                        borderWidth: 1,
+                        borderRadius: 4,
+                        color: '#fff'
+                    },
+                    crossStyle: {
+                        color: themeColors.textColor
+                    }
+                },
+                backgroundColor: themeColors.tooltipBg,
+                borderColor: themeColors.tooltipBorder,
+                borderWidth: 1,
+                borderRadius: 8,
+                textStyle: {
+                    color: themeColors.tooltipTextColor,
+                    fontSize: 12
+                },
+                extraCssText: `box-shadow: 0 4px 12px ${isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)'};`
+            },
+            legend: {
+                top: '10px',
+                data: [
+                    t('monitor.charts.requestsChart.requestCount'),
+                    t('monitor.charts.requestsChart.exceptionCount')
+                ],
+                textStyle: {
+                    color: themeColors.textColor,
+                    fontSize: 12
+                },
+                itemGap: 20,
+                icon: 'circle'
+            },
+            grid: {
+                left: '12px',
+                right: '12px',
+                bottom: '3%',
+                top: '50px',
+                containLabel: true
+            },
+            xAxis: {
+                type: 'category',
+                boundaryGap: false,
+                data: timestamps,
+                axisLine: {
+                    lineStyle: {
+                        color: themeColors.axisLineColor,
+                        width: 1
+                    }
+                },
+                axisLabel: {
+                    color: themeColors.textColor,
+                    fontSize: 11,
+                    margin: 10
+                },
+                axisTick: {
+                    show: false
+                },
+                splitLine: {
+                    show: false
+                }
+            },
+            yAxis: {
+                type: 'value',
+                axisLine: {
+                    show: false
+                },
+                axisLabel: {
+                    color: themeColors.textColor,
+                    fontSize: 11,
+                    margin: 10
+                },
+                axisTick: {
+                    show: false
+                },
+                splitLine: {
+                    lineStyle: {
+                        color: themeColors.splitLineColor,
+                        type: 'dashed',
+                        width: 1
+                    }
+                }
+            },
+            series: [
+                {
+                    name: t('monitor.charts.requestsChart.requestCount'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 6,
+                    lineStyle: {
+                        width: 2.5,
+                        color: '#3b82f6',
+                        shadowColor: '#3b82f620',
+                        shadowBlur: 6,
+                        shadowOffsetY: 2
+                    },
+                    itemStyle: {
+                        color: '#3b82f6',
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 12,
+                            shadowColor: '#3b82f640',
+                            shadowOffsetY: 3
+                        }
+                    },
+                    data: chartData.map(item => item.request_count)
+                },
+                {
+                    name: t('monitor.charts.requestsChart.exceptionCount'),
+                    type: 'line',
+                    smooth: true,
+                    showSymbol: false,
+                    symbolSize: 6,
+                    lineStyle: {
+                        width: 2.5,
+                        color: '#ef4444',
+                        shadowColor: '#ef444420',
+                        shadowBlur: 6,
+                        shadowOffsetY: 2
+                    },
+                    itemStyle: {
+                        color: '#ef4444',
+                        borderWidth: 2,
+                        borderColor: isDarkMode ? '#1f2937' : '#fff'
+                    },
+                    emphasis: {
+                        focus: 'series',
+                        scale: true,
+                        itemStyle: {
+                            shadowBlur: 12,
+                            shadowColor: '#ef444440',
+                            shadowOffsetY: 3
+                        }
+                    },
+                    data: chartData.map(item => item.exception_count)
+                }
+            ],
+            animation: true,
+            animationDuration: 1000,
+            animationEasing: 'cubicOut'
+        }
+    }, [chartData, t, isDarkMode, themeColors])
+
+    if (loading) {
+        return (
+            <div className="flex flex-col gap-4 h-[calc(100vh-280px)]">
+                <div className="flex-1">
+                    <Skeleton className="w-full h-full rounded-lg" />
+                </div>
+                <div className="flex-1">
+                    <Skeleton className="w-full h-full rounded-lg" />
+                </div>
+            </div>
+        )
+    }
+
+    return (
+        <div className="flex flex-col gap-4 h-[calc(100vh-280px)]">
+            <div className="flex-1">
+                <EChart
+                    option={tokensChartOption}
+                    style={{ width: '100%', height: '100%' }}
+                />
+            </div>
+            <div className="flex-1">
+                <EChart
+                    option={requestsChartOption}
+                    style={{ width: '100%', height: '100%' }}
+                />
+            </div>
+        </div>
+    )
+} 

+ 189 - 0
web/src/feature/monitor/components/MonitorFilters.tsx

@@ -0,0 +1,189 @@
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import type { DateRange } from 'react-day-picker'
+import { Search, RotateCcw } from 'lucide-react'
+
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import {
+    Select,
+    SelectContent,
+    SelectItem,
+    SelectTrigger,
+    SelectValue,
+} from '@/components/ui/select'
+import {
+    Tooltip,
+    TooltipContent,
+    TooltipProvider,
+    TooltipTrigger,
+} from '@/components/ui/tooltip'
+import { DateRangePicker } from '@/components/common/DateRangePicker'
+import { DashboardFilters } from '@/types/dashboard'
+
+interface MonitorFiltersProps {
+    onFiltersChange: (filters: DashboardFilters) => void
+    loading?: boolean
+}
+
+export function MonitorFilters({ onFiltersChange, loading = false }: MonitorFiltersProps) {
+    const { t } = useTranslation()
+
+    // 计算默认日期范围(当前时间往前7天)
+    const getDefaultDateRange = (): DateRange => {
+        const today = new Date()
+        const sevenDaysAgo = new Date()
+        sevenDaysAgo.setDate(today.getDate() - 7)
+
+        return {
+            from: sevenDaysAgo,
+            to: today
+        }
+    }
+
+    const [keyName, setKeyName] = useState('')
+    const [model, setModel] = useState('')
+    const [dateRange, setDateRange] = useState<DateRange | undefined>(getDefaultDateRange())
+    const [timespan, setTimespan] = useState<'day' | 'hour'>('day')
+
+    // 获取客户端时区
+    const getClientTimezone = () => {
+        return Intl.DateTimeFormat().resolvedOptions().timeZone
+    }
+
+    // 处理表单提交
+    const handleSubmit = (e: React.FormEvent) => {
+        e.preventDefault()
+
+        const filters: DashboardFilters = {
+            keyName: keyName.trim() || undefined,
+            model: model.trim() || undefined,
+            timespan,
+            timezone: getClientTimezone(),
+        }
+
+        // 处理日期范围
+        if (dateRange?.from) {
+            filters.start_timestamp = Math.floor(dateRange.from.getTime() / 1000)
+        }
+        if (dateRange?.to) {
+            // 将结束时间设置为当天的 23:59:59
+            const endDate = new Date(dateRange.to)
+            endDate.setHours(23, 59, 59, 999)
+            filters.end_timestamp = Math.floor(endDate.getTime() / 1000)
+        }
+
+        onFiltersChange(filters)
+    }
+
+    // 重置过滤器
+    const handleReset = () => {
+        setKeyName('')
+        setModel('')
+        const defaultDateRange = getDefaultDateRange()
+        setDateRange(defaultDateRange)
+        setTimespan('day')
+
+        const filters: DashboardFilters = {
+            timespan: 'day',
+            timezone: getClientTimezone(),
+            start_timestamp: Math.floor(defaultDateRange.from!.getTime() / 1000),
+            end_timestamp: Math.floor(defaultDateRange.to!.setHours(23, 59, 59, 999) / 1000)
+        }
+        onFiltersChange(filters)
+    }
+
+    return (
+        <div className="bg-card border border-border rounded-lg p-4 shadow-none">
+            <form onSubmit={handleSubmit}>
+                <div className="flex items-center gap-4">
+                    {/* Key 过滤器 */}
+                    <TooltipProvider>
+                        <Tooltip>
+                            <TooltipTrigger asChild>
+                                <div className="flex-1 min-w-0">
+                                    <Input
+                                        placeholder={t('monitor.filters.keyPlaceholder')}
+                                        value={keyName}
+                                        onChange={(e) => setKeyName(e.target.value)}
+                                        disabled={loading}
+                                        className="h-10"
+                                    />
+                                </div>
+                            </TooltipTrigger>
+                            <TooltipContent>
+                                <p>{t('monitor.filters.keyPlaceholder')}</p>
+                            </TooltipContent>
+                        </Tooltip>
+                    </TooltipProvider>
+
+                    {/* Model 过滤器 */}
+                    <TooltipProvider>
+                        <Tooltip>
+                            <TooltipTrigger asChild>
+                                <div className="flex-1 min-w-0">
+                                    <Input
+                                        placeholder={t('monitor.filters.modelPlaceholder')}
+                                        value={model}
+                                        onChange={(e) => setModel(e.target.value)}
+                                        disabled={loading}
+                                        className="h-10"
+                                    />
+                                </div>
+                            </TooltipTrigger>
+                            <TooltipContent>
+                                <p>{t('monitor.filters.modelPlaceholder')}</p>
+                            </TooltipContent>
+                        </Tooltip>
+                    </TooltipProvider>
+
+                    {/* 日期范围过滤器 */}
+                    <div className="min-w-48  max-w-72">
+                        <DateRangePicker
+                            value={dateRange}
+                            onChange={setDateRange}
+                            placeholder={t('monitor.filters.dateRangePlaceholder')}
+                            disabled={loading}
+                            className="h-10"
+                        />
+                    </div>
+
+                    {/* 时间粒度过滤器 */}
+                    <div className="w-24">
+                        <Select
+                            value={timespan}
+                            onValueChange={(value: 'day' | 'hour') => setTimespan(value)}
+                            disabled={loading}
+                        >
+                            <SelectTrigger className="h-10">
+                                <SelectValue />
+                            </SelectTrigger>
+                            <SelectContent>
+                                <SelectItem value="hour">{t('monitor.filters.timespanHour')}</SelectItem>
+                                <SelectItem value="day">{t('monitor.filters.timespanDay')}</SelectItem>
+                            </SelectContent>
+                        </Select>
+                    </div>
+
+                    {/* 操作按钮 */}
+                    <div className="flex gap-2 flex-shrink-0">
+                        <Button type="submit" disabled={loading} className="h-10 px-4">
+                            <Search className="h-4 w-4 mr-2" />
+                            {loading ? t('common.loading') : t('monitor.filters.search')}
+                        </Button>
+                        <Button
+                            type="button"
+                            variant="outline"
+                            onClick={handleReset}
+                            disabled={loading}
+                            className="h-10 px-4"
+                        >
+                            <RotateCcw className="h-4 w-4 mr-2" />
+                            {t('monitor.filters.reset')}
+                        </Button>
+                    </div>
+                </div>
+            </form>
+        </div>
+    )
+} 

+ 21 - 0
web/src/feature/monitor/hooks.ts

@@ -0,0 +1,21 @@
+import { useQuery } from '@tanstack/react-query'
+import { dashboardApi } from '@/api/dashboard'
+import { DashboardFilters } from '@/types/dashboard'
+
+// 获取仪表盘数据
+export const useDashboard = (filters?: DashboardFilters) => {
+    const query = useQuery({
+        queryKey: ['dashboard', filters],
+        queryFn: () => dashboardApi.getDashboardData(filters),
+        // 5分钟刷新一次数据
+        refetchInterval: 5 * 60 * 1000,
+        // 窗口重新获得焦点时刷新
+        refetchOnWindowFocus: true,
+        // 禁用重试,避免错误时过多请求
+        retry: false,
+    })
+
+    return {
+        ...query,
+    }
+} 

+ 23 - 1
web/src/pages/auth/login.tsx

@@ -1,6 +1,6 @@
 import { useForm } from "react-hook-form"
 import { zodResolver } from "@hookform/resolvers/zod"
-import { KeyRound } from 'lucide-react'
+import { KeyRound, Github, FileText } from 'lucide-react'
 import { useTranslation } from "react-i18next"
 
 import { Button } from "@/components/ui/button"
@@ -58,6 +58,28 @@ export default function LoginPage() {
 
             {/* Language Selector and Theme Toggle */}
             <div className="absolute top-4 right-4 z-10 flex items-center gap-4">
+                {/* Github Icon */}
+                <a
+                    href="https://github.com/labring/aiproxy"
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
+                    title="GitHub"
+                >
+                    <Github className="w-4 h-4 text-gray-600 dark:text-gray-400" />
+                </a>
+                
+                {/* Swagger Icon */}
+                <a
+                    href={`${window.location.origin}/swagger/index.html`}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
+                    title="API Documentation"
+                >
+                    <FileText className="w-4 h-4 text-gray-600 dark:text-gray-400" />
+                </a>
+                
                 <ThemeToggle />
                 <LanguageSelector variant="minimal" />
             </div>

+ 91 - 0
web/src/pages/log/page.tsx

@@ -0,0 +1,91 @@
+import { useState } from 'react'
+
+import { useLogs } from '@/feature/log/hooks'
+import { LogFilters } from '@/feature/log/components/LogFilters'
+import { LogTable } from '@/feature/log/components/LogTable'
+import { AdvancedErrorDisplay } from '@/components/common/error/errorDisplay'
+import type { LogFilters as LogFiltersType } from '@/types/log'
+
+export default function LogPage() {
+
+    // 初始化过滤器状态
+    const getDefaultFilters = (): LogFiltersType => {
+        const today = new Date()
+        const sevenDaysAgo = new Date()
+        sevenDaysAgo.setDate(today.getDate() - 7)
+
+        return {
+            code_type: 'all',
+            page: 1,
+            per_page: 10,
+            start_timestamp: sevenDaysAgo.getTime(),
+            end_timestamp: today.setHours(23, 59, 59, 999)
+        }
+    }
+
+    const [filters, setFilters] = useState<LogFiltersType>(getDefaultFilters())
+
+    // 获取日志数据
+    const {
+        data: logData,
+        isLoading,
+        error,
+        refetch
+    } = useLogs(filters)
+
+    // 处理过滤器变化
+    const handleFiltersChange = (newFilters: LogFiltersType) => {
+        setFilters(newFilters)
+    }
+
+    // 处理分页变化
+    const handlePageChange = (page: number) => {
+        setFilters(prev => ({ ...prev, page }))
+    }
+
+    // 处理每页数量变化
+    const handlePageSizeChange = (pageSize: number) => {
+        setFilters(prev => ({ ...prev, per_page: pageSize, page: 1 }))
+    }
+
+    // 处理重试
+    const handleRetry = () => {
+        refetch()
+    }
+
+    return (
+        <div className="h-screen flex flex-col">
+            <div className="flex-shrink-0 p-6 pb-2">
+                {/* 过滤器 */}
+                <LogFilters
+                    onFiltersChange={handleFiltersChange}
+                    loading={isLoading}
+                />
+
+                {/* 错误提示 */}
+                {error && (
+                    <div className="mt-6">
+                        <AdvancedErrorDisplay
+                            error={error}
+                            onRetry={handleRetry}
+                            useCardStyle={true}
+                        />
+                    </div>
+                )}
+            </div>
+
+            {/* 数据表格 - 占据剩余空间 */}
+            <div className="flex-1 px-6 pb-6 min-h-0">
+                <LogTable
+                    data={logData?.logs || []}
+                    total={logData?.total || 0}
+                    loading={isLoading}
+                    page={filters.page || 1}
+                    pageSize={filters.per_page || 10}
+                    onPageChange={handlePageChange}
+                    onPageSizeChange={handlePageSizeChange}
+                />
+            </div>
+        </div>
+    )
+}

+ 88 - 0
web/src/pages/monitor/page.tsx

@@ -0,0 +1,88 @@
+import { useState, useEffect } from 'react'
+import { BarChart3 } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+
+import { useDashboard } from '@/feature/monitor/hooks'
+import { MonitorFilters } from '@/feature/monitor/components/MonitorFilters'
+import { MetricsCards } from '@/feature/monitor/components/MetricsCards'
+import { MonitorCharts } from '@/feature/monitor/components/MonitorCharts'
+import { AdvancedErrorDisplay } from '@/components/common/error/errorDisplay'
+import { DashboardFilters } from '@/types/dashboard'
+
+export default function MonitorPage() {
+    const { t } = useTranslation()
+    
+    // 计算默认日期范围(当前时间往前7天)
+    const getDefaultFilters = (): DashboardFilters => {
+        const today = new Date()
+        const sevenDaysAgo = new Date()
+        sevenDaysAgo.setDate(today.getDate() - 7)
+        
+        return {
+            timespan: 'day',
+            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+            start_timestamp: Math.floor(sevenDaysAgo.getTime() / 1000),
+            end_timestamp: Math.floor(today.setHours(23, 59, 59, 999) / 1000)
+        }
+    }
+
+    const [filters, setFilters] = useState<DashboardFilters>(getDefaultFilters())
+
+    const { data, isLoading, error, refetch } = useDashboard(filters)
+
+    // 自动刷新数据
+    useEffect(() => {
+        const interval = setInterval(() => {
+            refetch()
+        }, 5 * 60 * 1000) // 5分钟刷新一次
+
+        return () => clearInterval(interval)
+    }, [refetch])
+
+    const handleFiltersChange = (newFilters: DashboardFilters) => {
+        setFilters(newFilters)
+    }
+
+    // 安全地获取 chart_data
+    const chartData = data?.chart_data || []
+    const hasChartData = chartData.length > 0
+
+    return (
+        <div className="flex-1 space-y-4 p-6">
+            {/* 过滤器 */}
+            <MonitorFilters onFiltersChange={handleFiltersChange} loading={isLoading} />
+
+            {/* 错误显示 - 使用 AdvancedErrorDisplay 组件 */}
+            {error && (
+                <AdvancedErrorDisplay 
+                    error={error} 
+                    onRetry={refetch}
+                    useCardStyle={true}
+                />
+            )}
+
+            {/* 指标卡片 */}
+            {data && (
+                <MetricsCards data={data} loading={isLoading} />
+            )}
+
+            {/* 图表 */}
+            {data && hasChartData && (
+                <MonitorCharts chartData={chartData} loading={isLoading} />
+            )}
+
+            {/* 空状态 */}
+            {data && !hasChartData && !isLoading && (
+                <div className="flex flex-col items-center justify-center py-12 text-center">
+                    <BarChart3 className="h-12 w-12 text-muted-foreground mb-4" />
+                    <h3 className="text-lg font-medium text-muted-foreground mb-2">
+                        {t('monitor.noData')}
+                    </h3>
+                    <p className="text-sm text-muted-foreground max-w-sm">
+                        {t('monitor.noDataDescription')}
+                    </p>
+                </div>
+            )}
+        </div>
+    )
+}

+ 5 - 3
web/src/routes/config.tsx

@@ -8,6 +8,8 @@ import { ProtectedRoute } from "@/feature/auth/components/ProtectedRoute"
 import ModelPage from "@/pages/model/page"
 import ChannelPage from "@/pages/channel/page"
 import TokenPage from "@/pages/token/page"
+import MonitorPage from "@/pages/monitor/page"
+import LogPage from "@/pages/log/page"
 
 // import layout component directly
 import { RootLayout } from "@/components/layout/RootLayOut"
@@ -41,11 +43,11 @@ export function useRoutes(): RouteObject[] {
             children: [
                 {
                     path: "/",
-                    element: <Navigate to={`${ROUTES.KEY}`} replace />
+                    element: <Navigate to={`${ROUTES.MONITOR}`} replace />
                 },
                 {
                     path: ROUTES.MONITOR,
-                    element: <div>monitor</div>,
+                    element: <MonitorPage />,
                 },
                 {
                     path: ROUTES.KEY,
@@ -61,7 +63,7 @@ export function useRoutes(): RouteObject[] {
                 },
                 {
                     path: ROUTES.LOG,
-                    element: <div>log</div>,
+                    element: <LogPage />,
                 }
             ]
         }]

+ 46 - 0
web/src/types/dashboard.ts

@@ -0,0 +1,46 @@
+export interface ChartDataPoint {
+    cache_creation_tokens: number
+    cached_tokens: number
+    exception_count: number
+    input_tokens: number
+    max_rpm: number
+    max_rps: number
+    max_tpm: number
+    max_tps: number
+    output_tokens: number
+    request_count: number
+    timestamp: number
+    total_tokens: number
+    used_amount: number
+    web_search_count: number
+}
+
+export interface DashboardData {
+    cache_creation_tokens: number
+    cached_tokens: number
+    channels: number[]
+    chart_data: ChartDataPoint[]
+    exception_count: number
+    input_tokens: number
+    max_rpm: number
+    max_rps: number
+    max_tpm: number
+    max_tps: number
+    models: string[]
+    output_tokens: number
+    rpm: number
+    total_count: number
+    total_tokens: number
+    tpm: number
+    used_amount: number
+    web_search_count: number
+}
+
+export interface DashboardFilters {
+    keyName?: string
+    model?: string
+    start_timestamp?: number
+    end_timestamp?: number
+    timezone?: string
+    timespan?: 'day' | 'hour'
+} 

+ 94 - 0
web/src/types/log.ts

@@ -0,0 +1,94 @@
+// src/types/log.ts
+
+// 价格信息
+export interface LogPrice {
+  cache_creation_price: number
+  cache_creation_price_unit: number
+  cached_price: number
+  cached_price_unit: number
+  image_input_price: number
+  image_input_price_unit: number
+  input_price: number
+  input_price_unit: number
+  output_price: number
+  output_price_unit: number
+  per_request_price: number
+  thinking_mode_output_price: number
+  thinking_mode_output_price_unit: number
+  web_search_price: number
+  web_search_price_unit: number
+}
+
+// 使用情况
+export interface LogUsage {
+  cache_creation_tokens: number
+  cached_tokens: number
+  image_input_tokens: number
+  input_tokens: number
+  output_tokens: number
+  reasoning_tokens: number
+  total_tokens: number
+  web_search_count: number
+}
+
+// 请求详情
+export interface LogRequestDetail {
+  id: number
+  log_id: number
+  request_body: string
+  request_body_truncated: boolean
+  response_body: string
+  response_body_truncated: boolean
+}
+
+// 日志记录
+export interface LogRecord {
+  channel: number
+  code: number
+  content: string
+  created_at: string
+  endpoint: string
+  group: string
+  id: number
+  ip: string
+  metadata: Record<string, string>
+  mode: number
+  model: string
+  price: LogPrice
+  request_at: string
+  request_detail: LogRequestDetail
+  request_id: string
+  retry_at: string
+  retry_times: number
+  token_id: number
+  token_name: string
+  ttfb_milliseconds: number
+  usage: LogUsage
+  used_amount: number
+  user: string
+}
+
+// 日志响应数据
+export interface LogResponse {
+  channels: number[]
+  logs: LogRecord[]
+  models: string[]
+  token_names: string[]
+  total: number
+}
+
+// 日志过滤器
+export interface LogFilters {
+  keyName?: string // token name
+  model?: string
+  start_timestamp?: number
+  end_timestamp?: number
+  code_type?: 'all' | 'success' | 'error'
+  page?: number
+  per_page?: number
+}
+
+// 日志列表请求参数
+export interface LogListParams extends LogFilters {
+  group?: string // 当keyName有值时,group = keyName
+} 

+ 74 - 0
web/src/types/model.ts

@@ -37,9 +37,83 @@ export interface ModelConfig {
     price: ModelPrice
     rpm: number
     tpm?: number
+    plugin: Plugin
+}
+
+type Plugin = {
+    cache: CachePlugin // 缓存插件
+    "web-search": WebSearchPlugin // 网络搜索插件
+    "think-split": ThinkSplitPlugin // 思考拆分插件
+}
+
+type CachePlugin = {
+    enable: boolean
+    ttl?: number
+    item_max_size?: number
+    add_cache_hit_header?: boolean
+    cache_hit_header?: string
+}
+
+type WebSearchPlugin = {
+    enable: boolean
+    force_search?: boolean
+    max_results?: number
+    search_rewrite?: {
+        enable?: boolean
+        model_name?: string
+        timeout_millisecond?: number
+        max_count?: number
+        add_rewrite_usage?: boolean
+        rewrite_usage_field?: string
+    }
+    need_reference?: boolean
+    reference_location?: string
+    reference_format?: string
+    default_language?: string
+    prompt_template?: string
+    search_from: EngineConfig[]
+}
+
+type ThinkSplitPlugin = {
+    enable: boolean
+}
+
+type EngineConfig = {
+    type: 'bing' | 'google' | 'arxiv' | 'searchxng'
+    max_results?: number
+    spec?: GoogleSpec | BingSpec | ArxivSpec | SearchXNGSpec
+}
+
+type GoogleSpec = {
+    api_key?: string
+    cx?: string
+}
+
+type BingSpec = {
+    api_key?: string
+}
+
+type ArxivSpec = object
+
+type SearchXNGSpec = {
+    base_url?: string
 }
 
 export interface ModelCreateRequest {
     model: string
     type: number
+    plugin?: Plugin
+}
+
+// Export all types for use in other modules
+export type {
+    Plugin,
+    CachePlugin,
+    WebSearchPlugin,
+    ThinkSplitPlugin,
+    EngineConfig,
+    GoogleSpec,
+    BingSpec,
+    ArxivSpec,
+    SearchXNGSpec
 }

+ 13 - 0
web/src/validation/dashboard.ts

@@ -0,0 +1,13 @@
+import { z } from 'zod'
+
+export const dashboardFiltersSchema = z.object({
+    key: z.string().optional(),
+    model: z.string().optional(),
+    dateRange: z.object({
+        from: z.date().optional(),
+        to: z.date().optional(),
+    }).optional(),
+    timespan: z.enum(['day', 'hour']).default('day'),
+})
+
+export type DashboardFiltersForm = z.infer<typeof dashboardFiltersSchema> 

+ 16 - 0
web/src/validation/log.ts

@@ -0,0 +1,16 @@
+import { z } from 'zod'
+
+// 日志过滤器验证schema
+export const logFilterSchema = z.object({
+    keyName: z.string().optional(),
+    model: z.string().optional(),
+    dateRange: z.object({
+        from: z.date().optional(),
+        to: z.date().optional()
+    }).optional(),
+    code_type: z.enum(['all', 'success', 'error']).default('all'),
+    page: z.number().min(1).default(1),
+    per_page: z.number().min(1).max(100).default(10)
+})
+
+export type LogFilterForm = z.infer<typeof logFilterSchema> 

+ 66 - 0
web/src/validation/model.ts

@@ -1,9 +1,75 @@
 // src/validation/model.ts
 import { z } from 'zod'
 
+// 不同搜索引擎的spec配置
+const googleSpecSchema = z.object({
+    api_key: z.string().optional(),
+    cx: z.string().optional(),
+})
+
+const bingSpecSchema = z.object({
+    api_key: z.string().optional(),
+})
+
+const arxivSpecSchema = z.object({})
+
+const searchXNGSpecSchema = z.object({
+    base_url: z.string().optional(),
+})
+
+// 搜索引擎配置验证
+const engineConfigSchema = z.object({
+    type: z.enum(['bing', 'google', 'arxiv', 'searchxng']),
+    max_results: z.number().optional(),
+    spec: z.union([googleSpecSchema, bingSpecSchema, arxivSpecSchema, searchXNGSpecSchema]).optional()
+})
+
+// 插件配置验证 - 根据用户需求调整
+const pluginSchema = z.object({
+    cache: z.object({
+        enable: z.boolean(),
+        ttl: z.number().optional(),
+        item_max_size: z.number().optional(),
+        add_cache_hit_header: z.boolean().optional(),
+        cache_hit_header: z.string().optional(),
+    }).optional(),
+    "web-search": z.object({
+        enable: z.boolean(),
+        force_search: z.boolean().optional(),
+        max_results: z.number().optional(),
+        search_rewrite: z.object({
+            enable: z.boolean().optional(),
+            model_name: z.string().optional(),
+            timeout_millisecond: z.number().optional(),
+            max_count: z.number().optional(),
+            add_rewrite_usage: z.boolean().optional(),
+            rewrite_usage_field: z.string().optional(),
+        }).optional(),
+        need_reference: z.boolean().optional(),
+        reference_location: z.string().optional(),
+        reference_format: z.string().optional(),
+        default_language: z.string().optional(),
+        prompt_template: z.string().optional(),
+        search_from: z.array(engineConfigSchema).optional()
+    }).refine((data) => {
+        // 如果 web-search 插件启用,则 search_from 必须至少有一个引擎
+        if (data.enable && (!data.search_from || data.search_from.length === 0)) {
+            return false
+        }
+        return true
+    }, {
+        message: '启用网络搜索插件时,必须至少配置一个搜索引擎',
+        path: ['search_from']
+    }).optional(),
+    "think-split": z.object({
+        enable: z.boolean(),
+    }).optional(),
+}).optional()
+
 export const modelCreateSchema = z.object({
     model: z.string().min(1, 'Model name is required'),
     type: z.number().min(0, 'Type is required'),
+    plugin: pluginSchema,
 })
 
 export type ModelCreateForm = z.infer<typeof modelCreateSchema>