diff --git a/bun.lock b/bun.lock index e71d0d264..915fa1135 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 1, "workspaces": { "": { "name": "claude-code-hub", @@ -305,9 +306,9 @@ "@hono/swagger-ui": ["@hono/swagger-ui@0.5.2", "", { "peerDependencies": { "hono": "*" } }, "sha512-7wxLKdb8h7JTdZ+K8DJNE3KXQMIpJejkBTQjrYlUWF28Z1PGOKw6kUykARe5NTfueIN37jbyG/sBYsbzXzG53A=="], - "@hono/zod-openapi": ["@hono/zod-openapi@1.1.4", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^8.1.0", "@hono/zod-validator": "^0.7.4", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "^4.0.0" } }, "sha512-4BbOtd6oKg20yo6HLluVbEycBLLIfdKX5o/gUSoKZ2uBmeP4Og/VDfIX3k9pbNEX5W3fRkuPeVjGA+zaQDVY1A=="], + "@hono/zod-openapi": ["@hono/zod-openapi@1.1.5", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^8.1.0", "@hono/zod-validator": "^0.7.4", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "^4.0.0" } }, "sha512-EAnY6ad4yt/MUKHx716BEGGOXSl5d0/FOLozOYB/pmSEFq07qrzefKFtBEMAgd3hlpJXjH+4lwgTtlAo+BGBgQ=="], - "@hono/zod-validator": ["@hono/zod-validator@0.7.4", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.5", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-n4l4hutkfYU07PzRUHBOVzUEn38VSfrh+UVE5d0w4lyfWDOEhzxIupqo5iakRiJL44c3vTuFJBvcmUl8b9agIA=="], "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], @@ -395,7 +396,7 @@ "@lobehub/icons": ["@lobehub/icons@2.43.1", "", { "dependencies": { "@lobehub/ui": "^2.9.3", "antd-style": "^3.7.1", "lucide-react": "^0.469.0", "polished": "^4.3.1", "react-layout-kit": "^2.0.0" }, "peerDependencies": { "antd": "^5.23.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-kva7fS6JwsOxoEqk4T1XZmRdmm9f+C2Enssin54MpTj7fduwYSG6oGgFgyYZ7RQq3eW5b5mnyO2SPIXU39xVsQ=="], - "@lobehub/ui": ["@lobehub/ui@2.16.2", "", { "dependencies": { "@ant-design/cssinjs": "^1.24.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@floating-ui/react": "^0.27.16", "@giscus/react": "^3.1.0", "@lobehub/fluent-emoji": "^2.0.0", "@lobehub/icons": "^2.43.1", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^3.15.0", "@shikijs/transformers": "^3.15.0", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.6", "antd-style": "^3.7.1", "chroma-js": "^3.1.2", "class-variance-authority": "^0.7.1", "dayjs": "^1.11.19", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "framer-motion": "^12.23.24", "immer": "^10.2.0", "katex": "^0.16.25", "leva": "^0.10.1", "lodash-es": "^4.17.21", "lucide-react": "^0.553.0", "marked": "^17.0.0", "mermaid": "^11.12.1", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^13.0.2", "react-error-boundary": "^6.0.0", "react-hotkeys-hook": "^5.2.1", "react-layout-kit": "^2.0.0", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.1.1", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "shiki": "^3.15.0", "shiki-stream": "^0.1.3", "swr": "^2.3.6", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0" }, "peerDependencies": { "antd": "^5.25.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-05niicxyeCE6kExVzE1Y/NCLJXm+i9QqcYnFZSXT0+LnxRuAkPhVEdm3QELws0Da8E2L3Yjs6DnPLqJuubPOJQ=="], + "@lobehub/ui": ["@lobehub/ui@2.16.3", "", { "dependencies": { "@ant-design/cssinjs": "^1.24.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@floating-ui/react": "^0.27.16", "@giscus/react": "^3.1.0", "@lobehub/fluent-emoji": "^2.0.0", "@lobehub/icons": "^2.43.1", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^3.15.0", "@shikijs/transformers": "^3.15.0", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.6", "antd-style": "^3.7.1", "chroma-js": "^3.1.2", "class-variance-authority": "^0.7.1", "dayjs": "^1.11.19", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "framer-motion": "^12.23.24", "immer": "^10.2.0", "katex": "^0.16.25", "leva": "^0.10.1", "lodash-es": "^4.17.21", "lucide-react": "^0.553.0", "marked": "^17.0.0", "mermaid": "^11.12.1", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^13.0.2", "react-error-boundary": "^6.0.0", "react-hotkeys-hook": "^5.2.1", "react-layout-kit": "^2.0.0", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.2", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.1.1", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "shiki": "^3.15.0", "shiki-stream": "^0.1.3", "swr": "^2.3.6", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0" }, "peerDependencies": { "antd": "^5.25.0", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-b/6WY/aLdPI0PAmTCw8bcDGWtdI6/QaQnCuBBsgCtPI0y4b6cpdCN6Qf+z5PLWvI1i46Pv2aXKVERkFVJx97/g=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], @@ -557,13 +558,11 @@ "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="], - "@scalar/core": ["@scalar/core@0.3.22", "", { "dependencies": { "@scalar/types": "0.4.0" } }, "sha512-6lzeRkvgkukSgge35kvxJKiJBny4rdGSaLTNzn/sF1F6JRfUo7I0AgqFxxSZWMD+EG4kGyNxAz0zciDSx2Cjvw=="], + "@scalar/core": ["@scalar/core@0.3.23", "", { "dependencies": { "@scalar/types": "0.5.0" } }, "sha512-hop7LVR3MKB2VpS8dly3gmmbB3lBGxQRtL0pBaC77zFMRHoBv1DuB2bj8l4gxd5grzitJ1LsYduvywLAMY9F6g=="], - "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.24", "", { "dependencies": { "@scalar/core": "0.3.22" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-NjPY3iMm/FqYRXAgr6V7qBhJGbSUQ8hbijFUMuqZo4pIjGEUNLeB5L9U2Gh4cDIPPWeso8mlc16jaX7dV0FrPw=="], + "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.25", "", { "dependencies": { "@scalar/core": "0.3.23" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-ZEQAhvVU/FXdJs8+rVXdfWjwzkE+M6Zr+4W+zNhy8DF17BIpxFXfVL7i3OxK1V/4EtkTplkETjYGTR4ju3RFZw=="], - "@scalar/openapi-types": ["@scalar/openapi-types@0.5.1", "", { "dependencies": { "zod": "4.1.11" } }, "sha512-8g7s9lPolyDFtijyh3Ob459tpezPuZbkXoFgJwBTHjPZ7ap+TvOJTvLk56CFwxVBVz2BxCzWJqxYyy3FUdeLoA=="], - - "@scalar/types": ["@scalar/types@0.4.0", "", { "dependencies": { "@scalar/openapi-types": "0.5.1", "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "4.1.11" } }, "sha512-vOD1GZez7kPdVA+UQit05QE9dbALfevhK9kqRTsqcPX7FvvZ9eQWSNl1GKmKtmRiAZGThv2agM5AvHRxkH2JSw=="], + "@scalar/types": ["@scalar/types@0.5.0", "", { "dependencies": { "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "4.1.11" } }, "sha512-imDMuTieOc5kHM9/Kt/1lmiI5ZtusuaYlzsXTP99IsWvD8mJ7ivF73lPBRj4PKtg4vY+ta5CO/vJpvnCYandRg=="], "@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], @@ -619,9 +618,9 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.9", "", {}, "sha512-UFOCQzi6pRGeVTVlPNwNdnAvT35zugcIydqjvFUzG62dvz2iVjElmNp/hJkUoM5eqbUPfSU/GJIr/wbvD8bTUw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.10", "", {}, "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.9", "", { "dependencies": { "@tanstack/query-core": "5.90.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Zke2AaXiaSfnG8jqPZR52m8SsclKT2d9//AgE/QIzyNvbpj/Q2ln+FsZjb1j69bJZUouBvX2tg9PHirkTm8arw=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.10", "", { "dependencies": { "@tanstack/query-core": "5.90.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -723,7 +722,7 @@ "@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="], - "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="], + "@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -731,25 +730,25 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.4", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/type-utils": "8.46.4", "@typescript-eslint/utils": "8.46.4", "@typescript-eslint/visitor-keys": "8.46.4", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.4", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.47.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/type-utils": "8.47.0", "@typescript-eslint/utils": "8.47.0", "@typescript-eslint/visitor-keys": "8.47.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.47.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.4", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", "@typescript-eslint/typescript-estree": "8.46.4", "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.47.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", "@typescript-eslint/typescript-estree": "8.47.0", "@typescript-eslint/visitor-keys": "8.47.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.4", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.4", "@typescript-eslint/types": "^8.46.4", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.47.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.47.0", "@typescript-eslint/types": "^8.47.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.4", "", { "dependencies": { "@typescript-eslint/types": "8.46.4", "@typescript-eslint/visitor-keys": "8.46.4" } }, "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.47.0", "", { "dependencies": { "@typescript-eslint/types": "8.47.0", "@typescript-eslint/visitor-keys": "8.47.0" } }, "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.4", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.47.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.4", "", { "dependencies": { "@typescript-eslint/types": "8.46.4", "@typescript-eslint/typescript-estree": "8.46.4", "@typescript-eslint/utils": "8.46.4", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.47.0", "", { "dependencies": { "@typescript-eslint/types": "8.47.0", "@typescript-eslint/typescript-estree": "8.47.0", "@typescript-eslint/utils": "8.47.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.46.4", "", {}, "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.47.0", "", {}, "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.4", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.4", "@typescript-eslint/tsconfig-utils": "8.46.4", "@typescript-eslint/types": "8.46.4", "@typescript-eslint/visitor-keys": "8.46.4", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.47.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.47.0", "@typescript-eslint/tsconfig-utils": "8.47.0", "@typescript-eslint/types": "8.47.0", "@typescript-eslint/visitor-keys": "8.47.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", "@typescript-eslint/typescript-estree": "8.46.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.47.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", "@typescript-eslint/typescript-estree": "8.47.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.4", "", { "dependencies": { "@typescript-eslint/types": "8.46.4", "eslint-visitor-keys": "^4.2.1" } }, "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.47.0", "", { "dependencies": { "@typescript-eslint/types": "8.47.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -809,7 +808,7 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "antd": ["antd@5.28.1", "", { "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.1.0", "@rc-component/tour": "~1.15.1", "@rc-component/trigger": "^2.3.0", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", "rc-cascader": "~3.34.0", "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.3.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.1", "rc-image": "~7.12.0", "rc-input": "~1.8.0", "rc-input-number": "~9.5.0", "rc-mentions": "~2.20.0", "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", "rc-notification": "~5.6.4", "rc-pagination": "~5.1.0", "rc-picker": "~4.11.3", "rc-progress": "~4.0.0", "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.8", "rc-slider": "~11.1.9", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.54.0", "rc-tabs": "~15.7.0", "rc-textarea": "~1.10.2", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", "rc-upload": "~4.11.0", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ZfPjbv3pY/jRnBFFn3L1UIRltaW9H4QovokZzNA57EgH3hEhWxQ3wWVfWmU6a3Q1GpbOgWQBJK2vsuoPIYuc9g=="], + "antd": ["antd@5.29.1", "", { "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.1.0", "@rc-component/tour": "~1.15.1", "@rc-component/trigger": "^2.3.0", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", "rc-cascader": "~3.34.0", "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.3.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.1", "rc-image": "~7.12.0", "rc-input": "~1.8.0", "rc-input-number": "~9.5.0", "rc-mentions": "~2.20.0", "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", "rc-notification": "~5.6.4", "rc-pagination": "~5.1.0", "rc-picker": "~4.11.3", "rc-progress": "~4.0.0", "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.8", "rc-slider": "~11.1.9", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.54.0", "rc-tabs": "~15.7.0", "rc-textarea": "~1.10.2", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", "rc-upload": "~4.11.0", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-TTFVbpKbyL6cPfEoKq6Ya3BIjTUr7uDW9+7Z+1oysRv1gpcN7kQ4luH8r/+rXXwz4n6BIz1iBJ1ezKCdsdNW0w=="], "antd-style": ["antd-style@3.7.1", "", { "dependencies": { "@ant-design/cssinjs": "^1.21.1", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=5.8.1", "react": ">=18" } }, "sha512-CQOfddVp4aOvBfCepa+Kj2e7ap+2XBINg1Kn2osdE3oQvrD7KJu/K0sfnLcFLkgCJygbxmuazYdWLKb+drPDYA=="], @@ -869,7 +868,7 @@ "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.8.28", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -895,7 +894,7 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001755", "", {}, "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="], "canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="], @@ -949,7 +948,7 @@ "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -961,9 +960,9 @@ "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="], - "core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="], + "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], - "core-js-compat": ["core-js-compat@3.46.0", "", { "dependencies": { "browserslist": "^4.26.3" } }, "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law=="], + "core-js-compat": ["core-js-compat@3.47.0", "", { "dependencies": { "browserslist": "^4.28.0" } }, "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ=="], "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], @@ -975,7 +974,7 @@ "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], - "csstype": ["csstype@3.2.1", "", {}, "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="], @@ -1113,7 +1112,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.254", "", {}, "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.258", "", {}, "sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg=="], "emoji-mart": ["emoji-mart@5.6.0", "", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="], @@ -1503,7 +1502,7 @@ "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - "jspdf": ["jspdf@3.0.3", "", { "dependencies": { "@babel/runtime": "^7.26.9", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ=="], + "jspdf": ["jspdf@3.0.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ=="], "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], @@ -1989,7 +1988,7 @@ "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], - "react-hook-form": ["react-hook-form@7.66.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw=="], + "react-hook-form": ["react-hook-form@7.66.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA=="], "react-hotkeys-hook": ["react-hotkeys-hook@5.2.1", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xbKh6zJxd/vJHT4Bw4+0pBD662Fk20V+VFhLqciCg+manTVO4qlqRqiwFOYelfHN9dBvWj9vxaPkSS26ZSIJGg=="], @@ -2099,8 +2098,6 @@ "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], @@ -2463,8 +2460,6 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@scalar/openapi-types/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], - "@scalar/types/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], "@scalar/types/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="], diff --git a/drizzle/0021_daily_cost_limits.sql b/drizzle/0021_daily_cost_limits.sql new file mode 100644 index 000000000..69e473de7 --- /dev/null +++ b/drizzle/0021_daily_cost_limits.sql @@ -0,0 +1,22 @@ +-- 每日成本限额功能 - 统一迁移文件 +-- 包含:添加字段、设置约束、添加重置模式 + +-- Step 1: 添加基础字段 +ALTER TABLE "keys" ADD COLUMN "limit_daily_usd" numeric(10, 2);--> statement-breakpoint +ALTER TABLE "keys" ADD COLUMN "daily_reset_time" varchar(5) DEFAULT '00:00';--> statement-breakpoint +ALTER TABLE "keys" ADD COLUMN "daily_reset_mode" varchar(10) DEFAULT 'fixed' NOT NULL;--> statement-breakpoint +ALTER TABLE "providers" ADD COLUMN "limit_daily_usd" numeric(10, 2);--> statement-breakpoint +ALTER TABLE "providers" ADD COLUMN "daily_reset_time" varchar(5) DEFAULT '00:00';--> statement-breakpoint +ALTER TABLE "providers" ADD COLUMN "daily_reset_mode" varchar(10) DEFAULT 'fixed' NOT NULL;--> statement-breakpoint + +-- Step 2: 数据清理和约束设置 +UPDATE "keys" +SET "daily_reset_time" = '00:00' +WHERE "daily_reset_time" IS NULL OR trim("daily_reset_time") = '';--> statement-breakpoint +ALTER TABLE "keys" ALTER COLUMN "daily_reset_time" SET DEFAULT '00:00';--> statement-breakpoint +ALTER TABLE "keys" ALTER COLUMN "daily_reset_time" SET NOT NULL;--> statement-breakpoint +UPDATE "providers" +SET "daily_reset_time" = '00:00' +WHERE "daily_reset_time" IS NULL OR trim("daily_reset_time") = '';--> statement-breakpoint +ALTER TABLE "providers" ALTER COLUMN "daily_reset_time" SET DEFAULT '00:00';--> statement-breakpoint +ALTER TABLE "providers" ALTER COLUMN "daily_reset_time" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0018_snapshot.json b/drizzle/meta/0018_snapshot.json index 6b13d6400..5425a7831 100644 --- a/drizzle/meta/0018_snapshot.json +++ b/drizzle/meta/0018_snapshot.json @@ -204,6 +204,19 @@ "primaryKey": false, "notNull": false }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false, + "default": "'00:00'" + }, "limit_weekly_usd": { "name": "limit_weekly_usd", "type": "numeric(10, 2)", @@ -940,6 +953,19 @@ "primaryKey": false, "notNull": false }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false, + "default": "'00:00'" + }, "limit_weekly_usd": { "name": "limit_weekly_usd", "type": "numeric(10, 2)", @@ -1471,4 +1497,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/0019_snapshot.json b/drizzle/meta/0019_snapshot.json index 14d2397de..cd63888cb 100644 --- a/drizzle/meta/0019_snapshot.json +++ b/drizzle/meta/0019_snapshot.json @@ -204,6 +204,19 @@ "primaryKey": false, "notNull": false }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, "limit_weekly_usd": { "name": "limit_weekly_usd", "type": "numeric(10, 2)", @@ -940,6 +953,19 @@ "primaryKey": false, "notNull": false }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, "limit_weekly_usd": { "name": "limit_weekly_usd", "type": "numeric(10, 2)", @@ -1492,4 +1518,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/0020_snapshot.json b/drizzle/meta/0020_snapshot.json index cd6212845..d46e22ddb 100644 --- a/drizzle/meta/0020_snapshot.json +++ b/drizzle/meta/0020_snapshot.json @@ -204,6 +204,26 @@ "primaryKey": false, "notNull": false }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, "limit_weekly_usd": { "name": "limit_weekly_usd", "type": "numeric(10, 2)", @@ -940,6 +960,26 @@ "primaryKey": false, "notNull": false }, + "limit_daily_usd": { + "name": "limit_daily_usd", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "daily_reset_mode": { + "name": "daily_reset_mode", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'fixed'" + }, + "daily_reset_time": { + "name": "daily_reset_time", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'00:00'" + }, "limit_weekly_usd": { "name": "limit_weekly_usd", "type": "numeric(10, 2)", @@ -1516,4 +1556,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6cd9cac3f..1b9814e88 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1763465177387, "tag": "0020_next_juggernaut", "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1763823720000, + "tag": "0021_daily_cost_limits", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 665915b9f..041a2b7ea 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -150,6 +150,7 @@ "billingDescription": "The system prioritizes billing based on the price of the requested model ({original}). If the model is not found in the price list, the price of the actually called model ({current}) is used." }, "errorMessage": "Error Message", + "filteredProviders": "Filtered Providers", "providerChain": { "title": "Provider Decision Chain Timeline", "totalDuration": "Total Duration: {duration}ms" @@ -477,6 +478,7 @@ "error": "Fetch failed", "networkError": "Network error", "cost5h": "5-Hour Cost", + "costDaily": "Daily Cost", "costWeekly": "Weekly Cost", "costMonthly": "Monthly Cost", "concurrentSessions": "Concurrent Sessions", @@ -505,6 +507,16 @@ "placeholder": "Leave blank for unlimited", "description": "Maximum cost within 5 hours" }, + "limitDailyUsd": { + "label": "Daily Cost Limit (USD)", + "placeholder": "Leave blank for unlimited", + "description": "Maximum cost per day" + }, + "dailyResetTime": { + "label": "Daily Reset Time", + "placeholder": "HH:mm", + "description": "When the daily limit resets (uses system timezone)" + }, "limitWeeklyUsd": { "label": "Weekly Cost Limit (USD)", "placeholder": "Leave blank for unlimited", diff --git a/messages/en/quota.json b/messages/en/quota.json index 094aebbf1..b06112918 100644 --- a/messages/en/quota.json +++ b/messages/en/quota.json @@ -103,6 +103,10 @@ "cost5h": { "label": "5-Hour Cost" }, + "costDaily": { + "label": "Daily Cost", + "resetAt": "Resets at" + }, "costWeekly": { "label": "Weekly Cost", "resetAt": "Resets at" @@ -136,6 +140,7 @@ "keyName": "Key Name", "quotaType": "Quota Type", "cost5h": "5-Hour Quota", + "costDaily": "Daily Quota", "costWeekly": "Weekly Quota", "costMonthly": "Monthly Quota", "concurrentSessions": "Concurrent Limit", @@ -160,6 +165,15 @@ "placeholder": "Unlimited", "current": "Current usage: {currency}{current} / {currency}{limit}" }, + "costDaily": { + "label": "Daily Quota (USD)", + "placeholder": "Unlimited", + "current": "Current usage: {currency}{current} / {currency}{limit}" + }, + "dailyResetTime": { + "label": "Daily Reset Time", + "placeholder": "HH:mm" + }, "costWeekly": { "label": "Weekly Quota (USD)", "placeholder": "Unlimited", @@ -225,6 +239,16 @@ "placeholder": "Leave blank for unlimited", "description": "Maximum cost within 5 hours" }, + "limitDailyUsd": { + "label": "Daily Cost Limit (USD)", + "placeholder": "Leave blank for unlimited", + "description": "Maximum cost per day" + }, + "dailyResetTime": { + "label": "Daily Reset Time", + "placeholder": "HH:mm", + "description": "When the daily limit resets (uses system timezone)" + }, "limitWeeklyUsd": { "label": "Weekly Cost Limit (USD)", "placeholder": "Leave blank for unlimited", diff --git a/messages/en/settings.json b/messages/en/settings.json index f19fc348b..1006b9b29 100644 --- a/messages/en/settings.json +++ b/messages/en/settings.json @@ -967,6 +967,7 @@ "title": "Rate Limit", "summary": { "fiveHour": "5h: ${amount}", + "daily": "Day: ${amount} (reset ${resetTime})", "weekly": "Week: ${amount}", "monthly": "Month: ${amount}", "concurrent": "Concurrent: {count}", @@ -976,6 +977,24 @@ "label": "5h Spend Limit (USD)", "placeholder": "Leave empty for unlimited" }, + "limitDaily": { + "label": "Daily Spend Limit (USD)", + "placeholder": "Leave empty for unlimited" + }, + "dailyResetMode": { + "label": "Daily Reset Mode", + "options": { + "fixed": "Fixed Time Reset", + "rolling": "Rolling Window (24h)" + }, + "desc": { + "fixed": "Reset quota at a fixed time each day", + "rolling": "Reset 24 hours after first API call" + } + }, + "dailyResetTime": { + "label": "Daily Reset Time (HH:mm)" + }, "limitWeekly": { "label": "Weekly Spend Limit (USD)", "placeholder": "Leave empty for unlimited" diff --git a/messages/en/usage.json b/messages/en/usage.json index 684b8ee59..3c2430ab6 100644 --- a/messages/en/usage.json +++ b/messages/en/usage.json @@ -231,6 +231,7 @@ "Create a config.json file in the ~/.claude directory (if it doesn't exist)", "Add the following content:" ], + "configPath": "Configuration file location: {path}", "note": "Note", "notePoints": [ diff --git a/messages/ja/usage.json b/messages/ja/usage.json index a41d2ead1..1f462e9c8 100644 --- a/messages/ja/usage.json +++ b/messages/ja/usage.json @@ -231,6 +231,7 @@ "~/.claude ディレクトリに config.json ファイルを作成 (存在しない場合)", "以下の内容を追加:" ], + "configPath": "設定ファイルの場所:{path}", "note": "注記", "notePoints": [ diff --git a/messages/ru/usage.json b/messages/ru/usage.json index 9e9ec34d8..52acfb686 100644 --- a/messages/ru/usage.json +++ b/messages/ru/usage.json @@ -231,6 +231,7 @@ "Создайте файл config.json в директории ~/.claude (если его нет)", "Добавьте следующее содержимое:" ], + "configPath": "Путь к файлу конфигурации: {path}", "note": "Примечание", "notePoints": [ diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 9df7f59a0..67a8489ce 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -151,6 +151,7 @@ "billingDescription": "系统优先使用请求模型({original})的价格计费。如果价格表中不存在该模型,则使用实际调用模型({current})的价格。" }, "errorMessage": "错误信息", + "filteredProviders": "被过滤的供应商", "providerChain": { "title": "供应商决策链时间线", "totalDuration": "总耗时: {duration}ms" @@ -478,6 +479,7 @@ "error": "获取失败", "networkError": "网络错误", "cost5h": "5小时消费", + "costDaily": "每日消费", "costWeekly": "周消费", "costMonthly": "月消费", "concurrentSessions": "并发 Session", @@ -507,6 +509,16 @@ "description": "5小时内最大消费金额", "descriptionWithUserLimit": "5小时内最大消费金额(用户限额: ${limit})" }, + "limitDailyUsd": { + "label": "每日消费上限 (USD)", + "placeholder": "留空表示无限制", + "description": "每日最大消费金额" + }, + "dailyResetTime": { + "label": "每日重置时间", + "placeholder": "HH:mm", + "description": "每日限额的重置时间(使用系统时区)" + }, "limitWeeklyUsd": { "label": "周消费上限 (USD)", "placeholder": "留空表示无限制", diff --git a/messages/zh-CN/quota.json b/messages/zh-CN/quota.json index 2c499e4cb..3760f5cb4 100644 --- a/messages/zh-CN/quota.json +++ b/messages/zh-CN/quota.json @@ -103,6 +103,10 @@ "cost5h": { "label": "5小时消费" }, + "costDaily": { + "label": "每日消费", + "resetAt": "重置于" + }, "costWeekly": { "label": "周消费", "resetAt": "重置于" @@ -136,6 +140,7 @@ "keyName": "密钥名称", "quotaType": "限额类型", "cost5h": "5小时限额", + "costDaily": "每日限额", "costWeekly": "周限额", "costMonthly": "月限额", "concurrentSessions": "并发限制", @@ -160,6 +165,15 @@ "placeholder": "不限制", "current": "当前已用: {currency}{current} / {currency}{limit}" }, + "costDaily": { + "label": "每日限额(USD)", + "placeholder": "不限制", + "current": "当前已用: {currency}{current} / {currency}{limit}" + }, + "dailyResetTime": { + "label": "每日重置时间", + "placeholder": "HH:mm" + }, "costWeekly": { "label": "周限额(USD)", "placeholder": "不限制", @@ -226,6 +240,16 @@ "description": "5小时内最大消费金额", "descriptionWithUserLimit": "5小时内最大消费金额(用户限额: ${limit})" }, + "limitDailyUsd": { + "label": "每日消费上限 (USD)", + "placeholder": "留空表示无限制", + "description": "每日最大消费金额" + }, + "dailyResetTime": { + "label": "每日重置时间", + "placeholder": "HH:mm", + "description": "每日限额的重置时间(使用系统时区)" + }, "limitWeeklyUsd": { "label": "周消费上限 (USD)", "placeholder": "留空表示无限制", diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json index e3b7df436..fcfbf552b 100644 --- a/messages/zh-CN/settings.json +++ b/messages/zh-CN/settings.json @@ -579,9 +579,10 @@ "rateLimit": { "title": "限流配置", "summary": { - "fiveHour": "5h: ${amount}", - "weekly": "周: ${amount}", - "monthly": "月: ${amount}", + "fiveHour": "5h: {amount}", + "daily": "日: {amount} (重置 {resetTime})", + "weekly": "周: {amount}", + "monthly": "月: {amount}", "concurrent": "并发: {count}", "none": "无限制" }, @@ -589,6 +590,24 @@ "label": "5小时消费上限 (USD)", "placeholder": "留空表示无限制" }, + "limitDaily": { + "label": "每日消费上限 (USD)", + "placeholder": "留空表示无限制" + }, + "dailyResetMode": { + "label": "每日重置模式", + "options": { + "fixed": "固定时间重置", + "rolling": "滚动窗口(24小时)" + }, + "desc": { + "fixed": "每天固定时间点重置配额", + "rolling": "从首次调用开始计算,24小时后重置" + } + }, + "dailyResetTime": { + "label": "每日重置时间 (HH:mm)" + }, "limitWeekly": { "label": "周消费上限 (USD)", "placeholder": "留空表示无限制" diff --git a/messages/zh-CN/usage.json b/messages/zh-CN/usage.json index 1279875ca..3ae101236 100644 --- a/messages/zh-CN/usage.json +++ b/messages/zh-CN/usage.json @@ -227,6 +227,7 @@ "在 ~/.claude 目录下创建 config.json 文件(如果没有)", "添加以下内容:" ], + "configPath": "配置文件路径:{path}", "note": "注意", "notePoints": [ diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index b17fc4042..fa528217f 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -151,6 +151,7 @@ "billingDescription": "系統優先使用請求模型({original})的價格計費。如果價格表中不存在該模型,則使用實際呼叫模型({current})的價格。" }, "errorMessage": "錯誤訊息", + "filteredProviders": "被過濾的供應商", "providerChain": { "title": "供應商決策鏈時間軸", "totalDuration": "總耗時: {duration} 毫秒" diff --git a/messages/zh-TW/usage.json b/messages/zh-TW/usage.json index 0e19ee4ae..1a4d1608a 100644 --- a/messages/zh-TW/usage.json +++ b/messages/zh-TW/usage.json @@ -227,6 +227,7 @@ "在 ~/.claude 目錄下創建 config.json 文件(如果沒有)", "添加以下內容:" ], + "configPath": "設定檔路徑:{path}", "note": "注意", "notePoints": [ diff --git "a/public/readme/\344\276\233\345\272\224\345\225\206\347\256\241\347\220\206.png" "b/public/readme/\344\276\233\345\272\224\345\225\206\347\256\241\347\220\206.png" index 989d28697..868ae3c58 100644 Binary files "a/public/readme/\344\276\233\345\272\224\345\225\206\347\256\241\347\220\206.png" and "b/public/readme/\344\276\233\345\272\224\345\225\206\347\256\241\347\220\206.png" differ diff --git "a/public/readme/\346\216\222\350\241\214\346\246\234.png" "b/public/readme/\346\216\222\350\241\214\346\246\234.png" index af3096e2c..b8516ed2e 100644 Binary files "a/public/readme/\346\216\222\350\241\214\346\246\234.png" and "b/public/readme/\346\216\222\350\241\214\346\246\234.png" differ diff --git "a/public/readme/\346\227\245\345\277\227.png" "b/public/readme/\346\227\245\345\277\227.png" index 1373d889d..63d971d40 100644 Binary files "a/public/readme/\346\227\245\345\277\227.png" and "b/public/readme/\346\227\245\345\277\227.png" differ diff --git "a/public/readme/\351\246\226\351\241\265.png" "b/public/readme/\351\246\226\351\241\265.png" index 9a96c9a42..c6a358bc6 100644 Binary files "a/public/readme/\351\246\226\351\241\265.png" and "b/public/readme/\351\246\226\351\241\265.png" differ diff --git a/src/actions/keys.ts b/src/actions/keys.ts index 95cfed7b9..9f4370a06 100644 --- a/src/actions/keys.ts +++ b/src/actions/keys.ts @@ -27,6 +27,9 @@ export async function addKey(data: { expiresAt?: string; canLoginWebUi?: boolean; limit5hUsd?: number | null; + limitDailyUsd?: number | null; + dailyResetMode?: "fixed" | "rolling"; + dailyResetTime?: string; limitWeeklyUsd?: number | null; limitMonthlyUsd?: number | null; limitConcurrentSessions?: number; @@ -44,6 +47,14 @@ export async function addKey(data: { const validatedData = KeyFormSchema.parse({ name: data.name, expiresAt: data.expiresAt, + canLoginWebUi: data.canLoginWebUi, + limit5hUsd: data.limit5hUsd, + limitDailyUsd: data.limitDailyUsd, + dailyResetMode: data.dailyResetMode, + dailyResetTime: data.dailyResetTime, + limitWeeklyUsd: data.limitWeeklyUsd, + limitMonthlyUsd: data.limitMonthlyUsd, + limitConcurrentSessions: data.limitConcurrentSessions, }); // 检查是否存在同名的生效key @@ -113,6 +124,9 @@ export async function addKey(data: { expires_at: expiresAt, can_login_web_ui: validatedData.canLoginWebUi, limit_5h_usd: validatedData.limit5hUsd, + limit_daily_usd: validatedData.limitDailyUsd, + daily_reset_mode: validatedData.dailyResetMode, + daily_reset_time: validatedData.dailyResetTime, limit_weekly_usd: validatedData.limitWeeklyUsd, limit_monthly_usd: validatedData.limitMonthlyUsd, limit_concurrent_sessions: validatedData.limitConcurrentSessions, @@ -137,6 +151,8 @@ export async function editKey( expiresAt?: string; canLoginWebUi?: boolean; limit5hUsd?: number | null; + limitDailyUsd?: number | null; + dailyResetTime?: string; limitWeeklyUsd?: number | null; limitMonthlyUsd?: number | null; limitConcurrentSessions?: number; @@ -217,6 +233,9 @@ export async function editKey( expires_at: expiresAt, can_login_web_ui: validatedData.canLoginWebUi, limit_5h_usd: validatedData.limit5hUsd, + limit_daily_usd: validatedData.limitDailyUsd, + daily_reset_mode: validatedData.dailyResetMode, + daily_reset_time: validatedData.dailyResetTime, limit_weekly_usd: validatedData.limitWeeklyUsd, limit_monthly_usd: validatedData.limitMonthlyUsd, limit_concurrent_sessions: validatedData.limitConcurrentSessions, @@ -314,6 +333,7 @@ export async function getKeysWithStatistics( export async function getKeyLimitUsage(keyId: number): Promise< ActionResult<{ cost5h: { current: number; limit: number | null; resetAt?: Date }; + costDaily: { current: number; limit: number | null; resetAt?: Date }; costWeekly: { current: number; limit: number | null; resetAt?: Date }; costMonthly: { current: number; limit: number | null; resetAt?: Date }; concurrentSessions: { current: number; limit: number }; @@ -338,11 +358,18 @@ export async function getKeyLimitUsage(keyId: number): Promise< // 动态导入 RateLimitService 避免循环依赖 const { RateLimitService } = await import("@/lib/rate-limit"); const { SessionTracker } = await import("@/lib/session-tracker"); - const { getResetInfo } = await import("@/lib/rate-limit/time-utils"); + const { getResetInfo, getResetInfoWithMode } = await import("@/lib/rate-limit/time-utils"); // 获取金额消费(优先 Redis,降级数据库) - const [cost5h, costWeekly, costMonthly, concurrentSessions] = await Promise.all([ + const [cost5h, costDaily, costWeekly, costMonthly, concurrentSessions] = await Promise.all([ RateLimitService.getCurrentCost(keyId, "key", "5h"), + RateLimitService.getCurrentCost( + keyId, + "key", + "daily", + key.dailyResetTime, + key.dailyResetMode ?? "fixed" + ), RateLimitService.getCurrentCost(keyId, "key", "weekly"), RateLimitService.getCurrentCost(keyId, "key", "monthly"), SessionTracker.getKeySessionCount(keyId), @@ -350,6 +377,11 @@ export async function getKeyLimitUsage(keyId: number): Promise< // 获取重置时间 const resetInfo5h = getResetInfo("5h"); + const resetInfoDaily = getResetInfoWithMode( + "daily", + key.dailyResetTime, + key.dailyResetMode ?? "fixed" + ); const resetInfoWeekly = getResetInfo("weekly"); const resetInfoMonthly = getResetInfo("monthly"); @@ -361,6 +393,11 @@ export async function getKeyLimitUsage(keyId: number): Promise< limit: key.limit5hUsd, resetAt: resetInfo5h.resetAt, // 滚动窗口无 resetAt }, + costDaily: { + current: costDaily, + limit: key.limitDailyUsd, + resetAt: resetInfoDaily.resetAt, + }, costWeekly: { current: costWeekly, limit: key.limitWeeklyUsd, diff --git a/src/actions/providers.ts b/src/actions/providers.ts index 85ae56524..eadd97ea5 100644 --- a/src/actions/providers.ts +++ b/src/actions/providers.ts @@ -136,6 +136,9 @@ export async function getProviders(): Promise { joinClaudePool: provider.joinClaudePool, codexInstructionsStrategy: provider.codexInstructionsStrategy, limit5hUsd: provider.limit5hUsd, + limitDailyUsd: provider.limitDailyUsd, + dailyResetMode: provider.dailyResetMode, + dailyResetTime: provider.dailyResetTime, limitWeeklyUsd: provider.limitWeeklyUsd, limitMonthlyUsd: provider.limitMonthlyUsd, limitConcurrentSessions: provider.limitConcurrentSessions, @@ -190,6 +193,9 @@ export async function addProvider(data: { allowed_models?: string[] | null; join_claude_pool?: boolean; limit_5h_usd?: number | null; + limit_daily_usd?: number | null; + daily_reset_mode?: "fixed" | "rolling"; + daily_reset_time?: string; limit_weekly_usd?: number | null; limit_monthly_usd?: number | null; limit_concurrent_sessions?: number | null; @@ -252,6 +258,9 @@ export async function addProvider(data: { const payload = { ...validated, limit_5h_usd: validated.limit_5h_usd ?? null, + limit_daily_usd: validated.limit_daily_usd ?? null, + daily_reset_mode: validated.daily_reset_mode ?? "fixed", + daily_reset_time: validated.daily_reset_time ?? "00:00", limit_weekly_usd: validated.limit_weekly_usd ?? null, limit_monthly_usd: validated.limit_monthly_usd ?? null, limit_concurrent_sessions: validated.limit_concurrent_sessions ?? 0, @@ -328,6 +337,8 @@ export async function editProvider( allowed_models?: string[] | null; join_claude_pool?: boolean; limit_5h_usd?: number | null; + limit_daily_usd?: number | null; + daily_reset_time?: string; limit_weekly_usd?: number | null; limit_monthly_usd?: number | null; limit_concurrent_sessions?: number | null; @@ -544,6 +555,7 @@ export async function resetProviderCircuit(providerId: number): Promise { canLoginWebUi: key.canLoginWebUi, // 限额配置 limit5hUsd: key.limit5hUsd, + limitDailyUsd: key.limitDailyUsd, + dailyResetTime: key.dailyResetTime, limitWeeklyUsd: key.limitWeeklyUsd, limitMonthlyUsd: key.limitMonthlyUsd, limitConcurrentSessions: key.limitConcurrentSessions || 0, diff --git a/src/app/[locale]/dashboard/_components/statistics/chart.tsx b/src/app/[locale]/dashboard/_components/statistics/chart.tsx index 04846538e..7696a971f 100644 --- a/src/app/[locale]/dashboard/_components/statistics/chart.tsx +++ b/src/app/[locale]/dashboard/_components/statistics/chart.tsx @@ -69,7 +69,7 @@ export function UserStatisticsChart({ // 重置选择状态(当 data.users 变化时) React.useEffect(() => { setSelectedUserIds(new Set(data.users.map((u) => u.id))); - }, [data.users]); + }, [data.users, t]); const isAdminMode = data.mode === "users"; const enableUserFilter = isAdminMode && data.users.length > 1; diff --git a/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx b/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx index 0b305da6a..4b437fddb 100644 --- a/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx +++ b/src/app/[locale]/dashboard/_components/user/forms/add-key-form.tsx @@ -8,6 +8,13 @@ import { DialogFormLayout } from "@/components/form/form-layout"; import { TextField, DateField, NumberField } from "@/components/form/form-field"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useZodForm } from "@/lib/hooks/use-zod-form"; import { KeyFormSchema } from "@/lib/validation/schemas"; import type { User } from "@/types/user"; @@ -30,6 +37,9 @@ export function AddKeyForm({ userId, user, onSuccess }: AddKeyFormProps) { expiresAt: "", canLoginWebUi: true, limit5hUsd: null, + limitDailyUsd: null, + dailyResetMode: "fixed" as const, + dailyResetTime: "00:00", limitWeeklyUsd: null, limitMonthlyUsd: null, limitConcurrentSessions: 0, @@ -46,6 +56,9 @@ export function AddKeyForm({ userId, user, onSuccess }: AddKeyFormProps) { expiresAt: data.expiresAt || undefined, canLoginWebUi: data.canLoginWebUi, limit5hUsd: data.limit5hUsd, + limitDailyUsd: data.limitDailyUsd, + dailyResetMode: data.dailyResetMode, + dailyResetTime: data.dailyResetTime, limitWeeklyUsd: data.limitWeeklyUsd, limitMonthlyUsd: data.limitMonthlyUsd, limitConcurrentSessions: data.limitConcurrentSessions, @@ -131,6 +144,48 @@ export function AddKeyForm({ userId, user, onSuccess }: AddKeyFormProps) { {...form.getFieldProps("limit5hUsd")} /> + + +
+ + +

+ {form.values.dailyResetMode === "fixed" + ? t("dailyResetMode.desc.fixed") + : t("dailyResetMode.desc.rolling")} +

+
+ + {form.values.dailyResetMode === "fixed" && ( + + )} + + + + + 0) || + (record.limitDailyUsd && record.limitDailyUsd > 0) || (record.limitWeeklyUsd && record.limitWeeklyUsd > 0) || (record.limitMonthlyUsd && record.limitMonthlyUsd > 0) || (record.limitConcurrentSessions && record.limitConcurrentSessions > 0); diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx index 8297ccce1..f9467f6b1 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx @@ -295,14 +295,102 @@ export function ErrorDetailsDialog({ {t("logs.details.errorMessage")} -
-
-                  {errorMessage}
-                
-
+ + {/* 尝试解析 JSON 错误 */} + {(() => { + try { + const error = JSON.parse(errorMessage); + + // 检查是否是限流错误 + if (error.code === 'rate_limit_exceeded' || error.code === 'circuit_breaker_open' || error.code === 'mixed_unavailable') { + return ( +
+
+ 💰 {error.message} +
+ {error.details?.filteredProviders && error.details.filteredProviders.length > 0 && ( +
+
+ {t("logs.details.filteredProviders")}: +
+
    + {error.details.filteredProviders + .filter((p: { reason: string }) => p.reason === 'rate_limited' || p.reason === 'circuit_open') + .map((p: { id: number; name: string; details: string }) => ( +
  • + + {p.name} + ({p.details}) +
  • + ))} +
+
+ )} +
+ ); + } + + // 其他 JSON 错误,格式化显示 + return ( +
+
+                        {JSON.stringify(error, null, 2)}
+                      
+
+ ); + } catch { + // 解析失败,显示原始消息 + return ( +
+
+                        {errorMessage}
+                      
+
+ ); + } + })()} )} + {/* 被过滤的供应商(仅在成功请求时显示) */} + {isSuccess && providerChain && providerChain.length > 0 && (() => { + // 从决策链中提取被过滤的供应商 + const filteredProviders = providerChain + .flatMap(item => item.decisionContext?.filteredProviders || []) + .filter(p => p.reason === 'rate_limited' || p.reason === 'circuit_open'); + + if (filteredProviders.length === 0) return null; + + return ( +
+

+ + {t("logs.details.filteredProviders")} +

+
+
    + {filteredProviders.map((p, index) => ( +
  • + 💰 +
    + {p.name} + + ({p.reason === 'rate_limited' ? '供应商费用限制' : '熔断器打开'}) + + {p.details && ( +
    + {p.details} +
    + )} +
    +
  • + ))} +
+
+
+ ); + })()} + {/* 供应商决策链时间线 */} {providerChain && providerChain.length > 0 && (
diff --git a/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx b/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx index c7fcca8f9..360459534 100644 --- a/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx +++ b/src/app/[locale]/dashboard/quotas/keys/_components/edit-key-quota-dialog.tsx @@ -22,6 +22,7 @@ import { useTranslations } from "next-intl"; interface KeyQuota { cost5h: { current: number; limit: number | null }; + costDaily: { current: number; limit: number | null; resetAt?: Date }; costWeekly: { current: number; limit: number | null }; costMonthly: { current: number; limit: number | null }; concurrentSessions: { current: number; limit: number }; @@ -34,6 +35,7 @@ interface EditKeyQuotaDialogProps { currentQuota: KeyQuota | null; currencyCode?: CurrencyCode; trigger?: React.ReactNode; + dailyResetTime?: string; } export function EditKeyQuotaDialog({ @@ -43,6 +45,7 @@ export function EditKeyQuotaDialog({ currentQuota, currencyCode = "USD", trigger, + dailyResetTime = "00:00", }: EditKeyQuotaDialogProps) { const router = useRouter(); const [isPending, startTransition] = useTransition(); @@ -53,6 +56,10 @@ export function EditKeyQuotaDialog({ // 表单状态 const [limit5h, setLimit5h] = useState(currentQuota?.cost5h.limit?.toString() ?? ""); + const [limitDaily, setLimitDaily] = useState( + currentQuota?.costDaily.limit?.toString() ?? "" + ); + const [resetTime, setResetTime] = useState(dailyResetTime); const [limitWeekly, setLimitWeekly] = useState( currentQuota?.costWeekly.limit?.toString() ?? "" ); @@ -72,6 +79,8 @@ export function EditKeyQuotaDialog({ const result = await editKey(keyId, { name: keyName, // 保持名称不变 limit5hUsd: limit5h ? parseFloat(limit5h) : null, + limitDailyUsd: limitDaily ? parseFloat(limitDaily) : null, + dailyResetTime: resetTime, limitWeeklyUsd: limitWeekly ? parseFloat(limitWeekly) : null, limitMonthlyUsd: limitMonthly ? parseFloat(limitMonthly) : null, limitConcurrentSessions: limitConcurrent ? parseInt(limitConcurrent, 10) : 0, @@ -97,6 +106,8 @@ export function EditKeyQuotaDialog({ const result = await editKey(keyId, { name: keyName, limit5hUsd: null, + limitDailyUsd: null, + dailyResetTime: resetTime, limitWeeklyUsd: null, limitMonthlyUsd: null, limitConcurrentSessions: 0, @@ -162,6 +173,47 @@ export function EditKeyQuotaDialog({ )}
+ {/* 每日限额 */} +
+ + setLimitDaily(e.target.value)} + className="h-9" + /> + {currentQuota?.costDaily.limit && ( +

+ {t("costDaily.current", { + currency: currencySymbol, + current: currentQuota.costDaily.current.toFixed(4), + limit: currentQuota.costDaily.limit.toFixed(2), + })} +

+ )} +
+ + {/* 每日重置时间 */} +
+ + setResetTime(e.target.value || "00:00")} + className="h-9" + /> +
+ {/* 周限额 */}
)} + {provider.quota.costDaily.limit && provider.quota.costDaily.limit > 0 && ( +
+
+ {t("costDaily.label")} + {provider.quota.costDaily.resetAt && ( + + {t("costDaily.resetAt")}{" "} + {formatDateDistance(provider.quota.costDaily.resetAt, new Date(), locale)} + + )} +
+
+ + {formatCurrency(provider.quota.costDaily.current, currencyCode)} /{" "} + {formatCurrency(provider.quota.costDaily.limit, currencyCode)} + +
+ +
+ )} + {/* 周消费 */} {provider.quota.costWeekly.limit && provider.quota.costWeekly.limit > 0 && (
diff --git a/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx b/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx index edebe1ed6..bb6b98c0a 100644 --- a/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx +++ b/src/app/[locale]/dashboard/quotas/providers/_components/providers-quota-manager.tsx @@ -9,6 +9,7 @@ import { useTranslations } from "next-intl"; interface ProviderQuota { cost5h: { current: number; limit: number | null; resetInfo: string }; + costDaily: { current: number; limit: number | null; resetAt?: Date }; costWeekly: { current: number; limit: number | null; resetAt: Date }; costMonthly: { current: number; limit: number | null; resetAt: Date }; concurrentSessions: { current: number; limit: number }; diff --git a/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx b/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx index a31243b4e..8ba3d8d9b 100644 --- a/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx +++ b/src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx @@ -21,7 +21,7 @@ export function AddProviderDialog({ enableMultiProviderTypes }: AddProviderDialo 新增服务商 - + (sourceProvider?.limit5hUsd ?? null); + const [limitDailyUsd, setLimitDailyUsd] = useState( + sourceProvider?.limitDailyUsd ?? null + ); + const [dailyResetMode, setDailyResetMode] = useState<"fixed" | "rolling">( + sourceProvider?.dailyResetMode ?? "fixed" + ); + const [dailyResetTime, setDailyResetTime] = useState( + sourceProvider?.dailyResetTime ?? "00:00" + ); const [limitWeeklyUsd, setLimitWeeklyUsd] = useState( sourceProvider?.limitWeeklyUsd ?? null ); @@ -267,6 +276,9 @@ export function ProviderForm({ cost_multiplier?: number; group_tag?: string | null; limit_5h_usd?: number | null; + limit_daily_usd?: number | null; + daily_reset_mode?: "fixed" | "rolling"; + daily_reset_time?: string; limit_weekly_usd?: number | null; limit_monthly_usd?: number | null; limit_concurrent_sessions?: number | null; @@ -296,6 +308,9 @@ export function ProviderForm({ cost_multiplier: costMultiplier, group_tag: groupTag.length > 0 ? groupTag.join(",") : null, limit_5h_usd: limit5hUsd, + limit_daily_usd: limitDailyUsd, + daily_reset_mode: dailyResetMode, + daily_reset_time: dailyResetTime, limit_weekly_usd: limitWeeklyUsd, limit_monthly_usd: limitMonthlyUsd, limit_concurrent_sessions: limitConcurrentSessions, @@ -348,6 +363,9 @@ export function ProviderForm({ cost_multiplier: costMultiplier, group_tag: groupTag.length > 0 ? groupTag.join(",") : null, limit_5h_usd: limit5hUsd, + limit_daily_usd: limitDailyUsd, + daily_reset_mode: dailyResetMode, + daily_reset_time: dailyResetTime, limit_weekly_usd: limitWeeklyUsd, limit_monthly_usd: limitMonthlyUsd, limit_concurrent_sessions: limitConcurrentSessions ?? 0, @@ -398,6 +416,8 @@ export function ProviderForm({ setCostMultiplier(1.0); setGroupTag([]); setLimit5hUsd(null); + setLimitDailyUsd(null); + setDailyResetTime("00:00"); setLimitWeeklyUsd(null); setLimitMonthlyUsd(null); setLimitConcurrentSessions(null); @@ -822,6 +842,13 @@ export function ProviderForm({ const limits: string[] = []; if (limit5hUsd) limits.push(t("sections.rateLimit.summary.fiveHour", { amount: limit5hUsd })); + if (limitDailyUsd) + limits.push( + t("sections.rateLimit.summary.daily", { + amount: limitDailyUsd, + resetTime: dailyResetTime, + }) + ); if (limitWeeklyUsd) limits.push(t("sections.rateLimit.summary.weekly", { amount: limitWeeklyUsd })); if (limitMonthlyUsd) @@ -857,6 +884,70 @@ export function ProviderForm({ step="0.01" />
+
+ + setLimitDailyUsd(validateNumericField(e.target.value))} + placeholder={t("sections.rateLimit.limitDaily.placeholder")} + disabled={isPending} + min="0" + step="0.01" + /> +
+ + +
+
+ + +

+ {dailyResetMode === "fixed" + ? t("sections.rateLimit.dailyResetMode.desc.fixed") + : t("sections.rateLimit.dailyResetMode.desc.rolling")} +

+
+ {dailyResetMode === "fixed" && ( +
+ + setDailyResetTime(e.target.value || "00:00")} + placeholder="00:00" + disabled={isPending} + step="60" + /> +
+ )} +
+ +
@@ -254,7 +259,6 @@ export function ProviderRichListItem({ alt="" className="h-4 w-4 flex-shrink-0" onError={(e) => { - // 隐藏加载失败的图标 (e.target as HTMLImageElement).style.display = "none"; }} /> diff --git a/src/app/[locale]/usage-doc/page.tsx b/src/app/[locale]/usage-doc/page.tsx index 3fb9329fa..bcf48ad62 100644 --- a/src/app/[locale]/usage-doc/page.tsx +++ b/src/app/[locale]/usage-doc/page.tsx @@ -55,7 +55,7 @@ interface CLIConfig { vsCodeExtension?: { name: string; configFile: string; - configPath: { macos: string; windows: string }; + configPath: Record; }; } @@ -78,6 +78,7 @@ function getCLIConfigs(t: (key: string) => string): Record { configPath: { macos: "~/.claude", windows: "C:\\Users\\你的用户名\\.claude", + linux: "~/.claude", }, }, }, @@ -92,6 +93,7 @@ function getCLIConfigs(t: (key: string) => string): Record { configPath: { macos: "~/.codex", windows: "C:\\Users\\你的用户名\\.codex", + linux: "~/.codex", }, }, }, @@ -1008,14 +1010,18 @@ gemini`} /** * 渲染 VS Code 扩展配置 */ - const renderVSCodeExtension = (cli: CLIConfig) => { + const renderVSCodeExtension = (cli: CLIConfig, os: OS) => { const config = cli.vsCodeExtension; if (!config) return null; + const resolvedConfigPath = config.configPath[os]; if (cli.id === "claude-code") { return (

{t("claudeCode.vsCodeExtension.title")}

+

+ {t("claudeCode.vsCodeExtension.configPath", { path: resolvedConfigPath })} +

    {(t.raw("claudeCode.vsCodeExtension.steps") as string[]).map( (step: string, i: number) => ( @@ -1024,8 +1030,9 @@ gemini`} )}
@@ -1294,7 +1301,7 @@ curl -I ${resolvedOrigin}`}
{/* VS Code 扩展配置 */} - {(cli.id === "claude-code" || cli.id === "codex") && renderVSCodeExtension(cli)} + {(cli.id === "claude-code" || cli.id === "codex") && renderVSCodeExtension(cli, os)} {/* 启动与验证 */} {renderStartupVerification(cli, os)} diff --git a/src/app/v1/_lib/codex/chat-completions-handler.ts b/src/app/v1/_lib/codex/chat-completions-handler.ts index 442921ab6..ba715cdff 100644 --- a/src/app/v1/_lib/codex/chat-completions-handler.ts +++ b/src/app/v1/_lib/codex/chat-completions-handler.ts @@ -181,6 +181,25 @@ export async function handleChatCompletions(c: Context): Promise { // 5. 供应商选择(根据模型自动匹配) const providerUnavailable = await ProxyProviderResolver.ensure(session); if (providerUnavailable) { + // 创建失败记录(供应商不可用) + await ProxyMessageService.ensureContext(session); + + // 解析错误响应 + const errorBody = await providerUnavailable + .clone() + .json() + .catch(() => null); + const errorMessage = errorBody?.error?.message || "供应商不可用"; + + // 记录失败消息 + if (session.messageContext) { + const { updateMessageRequestDetails } = await import("@/repository/message"); + await updateMessageRequestDetails(session.messageContext.id, { + statusCode: providerUnavailable.status, + errorMessage: JSON.stringify(errorBody?.error || { message: errorMessage }), + }); + } + return providerUnavailable; } diff --git a/src/app/v1/_lib/proxy/provider-selector.ts b/src/app/v1/_lib/proxy/provider-selector.ts index e7e2bb268..6cbe1ffdd 100644 --- a/src/app/v1/_lib/proxy/provider-selector.ts +++ b/src/app/v1/_lib/proxy/provider-selector.ts @@ -282,15 +282,71 @@ export class ProxyProviderResolver { // 循环结束:所有可用供应商都已尝试或无可用供应商 const status = 503; - const message = - excludedProviders.length > 0 - ? `所有供应商不可用(尝试了 ${excludedProviders.length} 个供应商)` - : "暂无可用的上游服务"; + + // 构建详细的错误消息 + let message = "暂无可用的上游服务"; + let errorType = "no_available_providers"; + + if (excludedProviders.length > 0) { + message = `所有供应商不可用(尝试了 ${excludedProviders.length} 个供应商)`; + errorType = "all_providers_failed"; + } else { + const selectionContext = session.getLastSelectionContext(); + const filteredProviders = selectionContext?.filteredProviders; + + if (filteredProviders && filteredProviders.length > 0) { + // 统计各种原因 + const rateLimited = filteredProviders.filter((p) => p.reason === "rate_limited"); + const circuitOpen = filteredProviders.filter((p) => p.reason === "circuit_open"); + const disabled = filteredProviders.filter((p) => p.reason === "disabled"); + const modelNotAllowed = filteredProviders.filter((p) => p.reason === "model_not_allowed"); + + // 计算可用供应商数量(排除禁用和模型不支持的) + const unavailableCount = rateLimited.length + circuitOpen.length; + const totalEnabled = filteredProviders.length - disabled.length - modelNotAllowed.length; + + if ( + rateLimited.length > 0 && + circuitOpen.length === 0 && + unavailableCount === totalEnabled + ) { + // 全部因为限流 + message = `所有可用供应商已达消费限额(${rateLimited.length} 个供应商)`; + errorType = "rate_limit_exceeded"; + } else if ( + circuitOpen.length > 0 && + rateLimited.length === 0 && + unavailableCount === totalEnabled + ) { + // 全部因为熔断 + message = `所有可用供应商熔断器已打开(${circuitOpen.length} 个供应商)`; + errorType = "circuit_breaker_open"; + } else if (rateLimited.length > 0 && circuitOpen.length > 0) { + // 混合原因 + message = `所有可用供应商不可用(${rateLimited.length} 个达限额,${circuitOpen.length} 个熔断)`; + errorType = "mixed_unavailable"; + } + } + } + logger.error("ProviderSelector: No available providers after trying all candidates", { excludedProviders, totalAttempts: attemptCount, + errorType, + filteredProviders: session.getLastSelectionContext()?.filteredProviders, }); - return ProxyResponses.buildError(status, message); + + // 构建详细的错误响应 + const details: Record = { + totalAttempts: attemptCount, + excludedCount: excludedProviders.length, + }; + + if (session.getLastSelectionContext()?.filteredProviders) { + details.filteredProviders = session.getLastSelectionContext()!.filteredProviders; + } + + return ProxyResponses.buildError(status, message, errorType, details); } /** @@ -551,10 +607,9 @@ export class ProxyProviderResolver { } if (healthyProviders.length === 0) { - logger.warn("ProviderSelector: All providers rate limited, falling back to random"); - // Fail Open:降级到随机选择(让上游拒绝) - const fallback = this.weightedRandom(candidateProviders); - return { provider: fallback, context }; + logger.warn("ProviderSelector: All providers rate limited or unavailable"); + // 所有供应商都被限流或不可用,返回 null 触发 503 错误 + return { provider: null, context }; } // Step 4: 优先级分层(只选择最高优先级的供应商) @@ -621,6 +676,9 @@ export class ProxyProviderResolver { // 1. 检查金额限制 const costCheck = await RateLimitService.checkCostLimits(p.id, "provider", { limit_5h_usd: p.limit5hUsd, + limit_daily_usd: p.limitDailyUsd, + daily_reset_mode: p.dailyResetMode, + daily_reset_time: p.dailyResetTime, limit_weekly_usd: p.limitWeeklyUsd, limit_monthly_usd: p.limitMonthlyUsd, }); diff --git a/src/app/v1/_lib/proxy/rate-limit-guard.ts b/src/app/v1/_lib/proxy/rate-limit-guard.ts index b2c2ffd09..286ab381f 100644 --- a/src/app/v1/_lib/proxy/rate-limit-guard.ts +++ b/src/app/v1/_lib/proxy/rate-limit-guard.ts @@ -85,6 +85,9 @@ export class ProxyRateLimitGuard { // 3. 检查 Key 金额限制 const costCheck = await RateLimitService.checkCostLimits(key.id, "key", { limit_5h_usd: key.limit5hUsd, + limit_daily_usd: key.limitDailyUsd, + daily_reset_mode: key.dailyResetMode, + daily_reset_time: key.dailyResetTime, limit_weekly_usd: key.limitWeeklyUsd, limit_monthly_usd: key.limitMonthlyUsd, }); diff --git a/src/app/v1/_lib/proxy/response-handler.ts b/src/app/v1/_lib/proxy/response-handler.ts index bfc0b34aa..1fae02e55 100644 --- a/src/app/v1/_lib/proxy/response-handler.ts +++ b/src/app/v1/_lib/proxy/response-handler.ts @@ -1280,7 +1280,13 @@ async function trackCostToRedis(session: ProxySession, usage: UsageMetrics | nul key.id, provider.id, session.sessionId, // 直接使用 session.sessionId - costFloat + costFloat, + { + keyResetTime: key.dailyResetTime, + keyResetMode: key.dailyResetMode, + providerResetTime: provider.dailyResetTime, + providerResetMode: provider.dailyResetMode, + } ); // 新增:追踪用户层每日消费 diff --git a/src/app/v1/_lib/proxy/responses.ts b/src/app/v1/_lib/proxy/responses.ts index 930a3e321..3f767b22c 100644 --- a/src/app/v1/_lib/proxy/responses.ts +++ b/src/app/v1/_lib/proxy/responses.ts @@ -1,12 +1,34 @@ export class ProxyResponses { - static buildError(status: number, message: string): Response { - const payload = { + static buildError( + status: number, + message: string, + errorType?: string, + details?: Record + ): Response { + const payload: { + error: { + message: string; + type: string; + code?: string; + details?: Record; + }; + } = { error: { message, - type: String(status), + type: errorType || this.getErrorType(status), }, }; + // 添加错误代码(用于前端识别) + if (errorType) { + payload.error.code = errorType; + } + + // 添加详细信息(可选) + if (details) { + payload.error.details = details; + } + return new Response(JSON.stringify(payload), { status, headers: { @@ -14,4 +36,32 @@ export class ProxyResponses { }, }); } + + /** + * 根据 HTTP 状态码获取默认错误类型 + */ + private static getErrorType(status: number): string { + switch (status) { + case 400: + return "invalid_request_error"; + case 401: + return "authentication_error"; + case 403: + return "permission_error"; + case 404: + return "not_found_error"; + case 429: + return "rate_limit_error"; + case 500: + return "internal_server_error"; + case 502: + return "bad_gateway_error"; + case 503: + return "service_unavailable_error"; + case 504: + return "gateway_timeout_error"; + default: + return "api_error"; + } + } } diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts index 5b4220d14..4367ee831 100644 --- a/src/drizzle/schema.ts +++ b/src/drizzle/schema.ts @@ -53,6 +53,14 @@ export const keys = pgTable('keys', { // 金额限流配置 limit5hUsd: numeric('limit_5h_usd', { precision: 10, scale: 2 }), + limitDailyUsd: numeric('limit_daily_usd', { precision: 10, scale: 2 }), + dailyResetMode: varchar('daily_reset_mode', { length: 10 }) + .default('fixed') + .notNull() + .$type<'fixed' | 'rolling'>(), // fixed: 固定时间重置, rolling: 滚动窗口(24小时) + dailyResetTime: varchar('daily_reset_time', { length: 5 }) + .default('00:00') + .notNull(), // HH:mm 格式,如 "18:00"(仅 fixed 模式使用) limitWeeklyUsd: numeric('limit_weekly_usd', { precision: 10, scale: 2 }), limitMonthlyUsd: numeric('limit_monthly_usd', { precision: 10, scale: 2 }), limitConcurrentSessions: integer('limit_concurrent_sessions').default(0), @@ -117,6 +125,14 @@ export const providers = pgTable('providers', { // 金额限流配置 limit5hUsd: numeric('limit_5h_usd', { precision: 10, scale: 2 }), + limitDailyUsd: numeric('limit_daily_usd', { precision: 10, scale: 2 }), + dailyResetMode: varchar('daily_reset_mode', { length: 10 }) + .default('fixed') + .notNull() + .$type<'fixed' | 'rolling'>(), // fixed: 固定时间重置, rolling: 滚动窗口(24小时) + dailyResetTime: varchar('daily_reset_time', { length: 5 }) + .default('00:00') + .notNull(), // HH:mm 格式,如 "18:00"(仅 fixed 模式使用) limitWeeklyUsd: numeric('limit_weekly_usd', { precision: 10, scale: 2 }), limitMonthlyUsd: numeric('limit_monthly_usd', { precision: 10, scale: 2 }), limitConcurrentSessions: integer('limit_concurrent_sessions').default(0), diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 78dcfb14b..dd48c5c50 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -38,6 +38,9 @@ export async function validateKey(keyString: string): Promise { + const normalizedDailyReset = normalizeResetTime(limits.daily_reset_time); + const dailyResetMode = limits.daily_reset_mode ?? "fixed"; const costLimits: CostLimit[] = [ { amount: limits.limit_5h_usd, period: "5h", name: "5小时" }, + { + amount: limits.limit_daily_usd, + period: "daily", + name: "每日", + resetTime: normalizedDailyReset, + resetMode: dailyResetMode, + }, { amount: limits.limit_weekly_usd, period: "weekly", name: "周" }, { amount: limits.limit_monthly_usd, period: "monthly", name: "月" }, ]; @@ -82,14 +111,48 @@ export class RateLimitService { ); return await this.checkCostLimitsFromDatabase(id, type, costLimits); } + } else if (limit.period === "daily" && limit.resetMode === "rolling") { + // daily 滚动窗口:使用 ZSET + Lua 脚本 + try { + const key = `${type}:${id}:cost_daily_rolling`; + const window24h = 24 * 60 * 60 * 1000; + const result = (await this.redis.eval( + GET_COST_DAILY_ROLLING_WINDOW, + 1, + key, + now.toString(), + window24h.toString() + )) as string; + + current = parseFloat(result || "0"); + + // Cache Miss 检测 + if (current === 0) { + const exists = await this.redis.exists(key); + if (!exists) { + logger.info( + `[RateLimit] Cache miss for ${type}:${id}:cost_daily_rolling, querying database` + ); + return await this.checkCostLimitsFromDatabase(id, type, costLimits); + } + } + } catch (error) { + logger.error( + "[RateLimit] Daily rolling window query failed, fallback to database:", + error + ); + return await this.checkCostLimitsFromDatabase(id, type, costLimits); + } } else { - // 周/月使用普通 GET - const value = await this.redis.get(`${type}:${id}:cost_${limit.period}`); + // daily fixed/周/月使用普通 GET + const { suffix } = this.resolveDailyReset(limit.resetTime); + const periodKey = limit.period === "daily" ? `${limit.period}_${suffix}` : limit.period; + const value = await this.redis.get(`${type}:${id}:cost_${periodKey}`); // Cache Miss 检测 if (value === null && limit.amount > 0) { logger.info( - `[RateLimit] Cache miss for ${type}:${id}:cost_${limit.period}, querying database` + `[RateLimit] Cache miss for ${type}:${id}:cost_${periodKey}, querying database` ); return await this.checkCostLimitsFromDatabase(id, type, costLimits); } @@ -132,8 +195,12 @@ export class RateLimitService { for (const limit of costLimits) { if (!limit.amount || limit.amount <= 0) continue; - // 计算时间范围(使用新的时间工具函数) - const { startTime, endTime } = getTimeRangeForPeriod(limit.period); + // 计算时间范围(使用支持模式的时间工具函数) + const { startTime, endTime } = getTimeRangeForPeriodWithMode( + limit.period, + limit.resetTime, + limit.resetMode + ); // 查询数据库 const current = @@ -162,17 +229,34 @@ export class RateLimitService { logger.info(`[RateLimit] Cache warmed for ${key}, value=${current} (rolling window)`); } + } else if (limit.period === "daily" && limit.resetMode === "rolling") { + // daily 滚动窗口:使用 ZSET + Lua 脚本 + if (current > 0) { + const now = Date.now(); + const window24h = 24 * 60 * 60 * 1000; + const key = `${type}:${id}:cost_daily_rolling`; + + await this.redis.eval( + TRACK_COST_DAILY_ROLLING_WINDOW, + 1, + key, + current.toString(), + now.toString(), + window24h.toString() + ); + + logger.info( + `[RateLimit] Cache warmed for ${key}, value=${current} (daily rolling window)` + ); + } } else { - // 周/月固定窗口:使用 STRING + 动态 TTL - const ttl = getTTLForPeriod(limit.period); - await this.redis.set( - `${type}:${id}:cost_${limit.period}`, - current.toString(), - "EX", - ttl - ); + // daily fixed/周/月固定窗口:使用 STRING + 动态 TTL + const { normalized, suffix } = this.resolveDailyReset(limit.resetTime); + const ttl = getTTLForPeriodWithMode(limit.period, normalized, limit.resetMode); + const periodKey = limit.period === "daily" ? `${limit.period}_${suffix}` : limit.period; + await this.redis.set(`${type}:${id}:cost_${periodKey}`, current.toString(), "EX", ttl); logger.info( - `[RateLimit] Cache warmed for ${type}:${id}:cost_${limit.period}, value=${current}, ttl=${ttl}s` + `[RateLimit] Cache warmed for ${type}:${id}:cost_${periodKey}, value=${current}, ttl=${ttl}s` ); } } catch (error) { @@ -289,21 +373,38 @@ export class RateLimitService { /** * 累加消费(请求结束后调用) - * 5h 使用滚动窗口(ZSET),周/月使用固定窗口(STRING) + * 5h 使用滚动窗口(ZSET),daily 根据模式选择滚动/固定窗口,周/月使用固定窗口(STRING) */ static async trackCost( keyId: number, providerId: number, sessionId: string, - cost: number + cost: number, + options?: { + keyResetTime?: string; + keyResetMode?: DailyResetMode; + providerResetTime?: string; + providerResetMode?: DailyResetMode; + } ): Promise { if (!this.redis || cost <= 0) return; try { + const keyDailyReset = this.resolveDailyReset(options?.keyResetTime); + const providerDailyReset = this.resolveDailyReset(options?.providerResetTime); + const keyDailyMode = options?.keyResetMode ?? "fixed"; + const providerDailyMode = options?.providerResetMode ?? "fixed"; const now = Date.now(); const window5h = 5 * 60 * 60 * 1000; // 5 hours in ms - - // 计算动态 TTL(周/月) + const window24h = 24 * 60 * 60 * 1000; // 24 hours in ms + + // 计算动态 TTL(daily/周/月) + const ttlDailyKey = getTTLForPeriodWithMode("daily", keyDailyReset.normalized, keyDailyMode); + const ttlDailyProvider = + keyDailyReset.normalized === providerDailyReset.normalized && + keyDailyMode === providerDailyMode + ? ttlDailyKey + : getTTLForPeriodWithMode("daily", providerDailyReset.normalized, providerDailyMode); const ttlWeekly = getTTLForPeriod("weekly"); const ttlMonthly = getTTLForPeriod("monthly"); @@ -328,17 +429,52 @@ export class RateLimitService { window5h.toString() ); - // 2. 周/月固定窗口:使用 STRING + 动态 TTL + // 2. daily 滚动窗口:使用 Lua 脚本(ZSET) + if (keyDailyMode === "rolling") { + await this.redis.eval( + TRACK_COST_DAILY_ROLLING_WINDOW, + 1, + `key:${keyId}:cost_daily_rolling`, + cost.toString(), + now.toString(), + window24h.toString() + ); + } + + if (providerDailyMode === "rolling") { + await this.redis.eval( + TRACK_COST_DAILY_ROLLING_WINDOW, + 1, + `provider:${providerId}:cost_daily_rolling`, + cost.toString(), + now.toString(), + window24h.toString() + ); + } + + // 3. daily fixed/周/月固定窗口:使用 STRING + 动态 TTL const pipeline = this.redis.pipeline(); - // Key 的周/月消费 + // Key 的 daily fixed/周/月消费 + if (keyDailyMode === "fixed") { + const keyDailyKey = `key:${keyId}:cost_daily_${keyDailyReset.suffix}`; + pipeline.incrbyfloat(keyDailyKey, cost); + pipeline.expire(keyDailyKey, ttlDailyKey); + } + pipeline.incrbyfloat(`key:${keyId}:cost_weekly`, cost); pipeline.expire(`key:${keyId}:cost_weekly`, ttlWeekly); pipeline.incrbyfloat(`key:${keyId}:cost_monthly`, cost); pipeline.expire(`key:${keyId}:cost_monthly`, ttlMonthly); - // Provider 的周/月消费 + // Provider 的 daily fixed/周/月消费 + if (providerDailyMode === "fixed") { + const providerDailyKey = `provider:${providerId}:cost_daily_${providerDailyReset.suffix}`; + pipeline.incrbyfloat(providerDailyKey, cost); + pipeline.expire(providerDailyKey, ttlDailyProvider); + } + pipeline.incrbyfloat(`provider:${providerId}:cost_weekly`, cost); pipeline.expire(`provider:${providerId}:cost_weekly`, ttlWeekly); @@ -361,9 +497,12 @@ export class RateLimitService { static async getCurrentCost( id: number, type: "key" | "provider", - period: "5h" | "weekly" | "monthly" + period: "5h" | "daily" | "weekly" | "monthly", + resetTime = "00:00", + resetMode: DailyResetMode = "fixed" ): Promise { try { + const dailyResetInfo = this.resolveDailyReset(resetTime); // Fast Path: Redis 查询 if (this.redis && this.redis.status === "ready") { let current = 0; @@ -397,9 +536,40 @@ export class RateLimitService { // Key 存在但值为 0,说明真的是 0 return 0; } + } else if (period === "daily" && resetMode === "rolling") { + // daily 滚动窗口:使用 ZSET + Lua 脚本 + const now = Date.now(); + const window24h = 24 * 60 * 60 * 1000; + const key = `${type}:${id}:cost_daily_rolling`; + + const result = (await this.redis.eval( + GET_COST_DAILY_ROLLING_WINDOW, + 1, + key, + now.toString(), + window24h.toString() + )) as string; + + current = parseFloat(result || "0"); + + // Cache Hit + if (current > 0) { + return current; + } + + // Cache Miss 检测 + const exists = await this.redis.exists(key); + if (!exists) { + logger.info( + `[RateLimit] Cache miss for ${type}:${id}:cost_daily_rolling, querying database` + ); + } else { + return 0; + } } else { - // 周/月使用普通 GET - const value = await this.redis.get(`${type}:${id}:cost_${period}`); + // daily fixed/周/月使用普通 GET + const redisKey = period === "daily" ? `${period}_${dailyResetInfo.suffix}` : period; + const value = await this.redis.get(`${type}:${id}:cost_${redisKey}`); // Cache Hit if (value !== null) { @@ -407,7 +577,9 @@ export class RateLimitService { } // Cache Miss: 从数据库恢复 - logger.info(`[RateLimit] Cache miss for ${type}:${id}:cost_${period}, querying database`); + logger.info( + `[RateLimit] Cache miss for ${type}:${id}:cost_${redisKey}, querying database` + ); } } else { logger.warn(`[RateLimit] Redis unavailable, querying database for ${type} cost`); @@ -418,7 +590,11 @@ export class RateLimitService { "@/repository/statistics" ); - const { startTime, endTime } = getTimeRangeForPeriod(period); + const { startTime, endTime } = getTimeRangeForPeriodWithMode( + period, + dailyResetInfo.normalized, + resetMode + ); const current = type === "key" ? await sumKeyCostInTimeRange(id, startTime, endTime) @@ -447,12 +623,33 @@ export class RateLimitService { logger.info(`[RateLimit] Cache warmed for ${key}, value=${current} (rolling window)`); } + } else if (period === "daily" && resetMode === "rolling") { + // daily 滚动窗口:使用 ZSET + Lua 脚本 + if (current > 0) { + const now = Date.now(); + const window24h = 24 * 60 * 60 * 1000; + const key = `${type}:${id}:cost_daily_rolling`; + + await this.redis.eval( + TRACK_COST_DAILY_ROLLING_WINDOW, + 1, + key, + current.toString(), + now.toString(), + window24h.toString() + ); + + logger.info( + `[RateLimit] Cache warmed for ${key}, value=${current} (daily rolling window)` + ); + } } else { - // 周/月固定窗口:使用 STRING + 动态 TTL - const ttl = getTTLForPeriod(period); - await this.redis.set(`${type}:${id}:cost_${period}`, current.toString(), "EX", ttl); + // daily fixed/周/月固定窗口:使用 STRING + 动态 TTL + const redisKey = period === "daily" ? `${period}_${dailyResetInfo.suffix}` : period; + const ttl = getTTLForPeriodWithMode(period, dailyResetInfo.normalized, resetMode); + await this.redis.set(`${type}:${id}:cost_${redisKey}`, current.toString(), "EX", ttl); logger.info( - `[RateLimit] Cache warmed for ${type}:${id}:cost_${period}, value=${current}, ttl=${ttl}s` + `[RateLimit] Cache warmed for ${type}:${id}:cost_${redisKey}, value=${current}, ttl=${ttl}s` ); } } catch (error) { diff --git a/src/lib/rate-limit/time-utils.ts b/src/lib/rate-limit/time-utils.ts index 8a68f7206..1735927b6 100644 --- a/src/lib/rate-limit/time-utils.ts +++ b/src/lib/rate-limit/time-utils.ts @@ -3,11 +3,22 @@ * 用于计算自然时间窗口(周一/月初)和对应的 TTL */ -import { startOfMonth, startOfWeek, addMonths, addWeeks, addDays } from "date-fns"; +import { + startOfMonth, + startOfWeek, + addMonths, + addWeeks, + addDays, + setHours, + setMinutes, + setSeconds, + setMilliseconds, +} from "date-fns"; import { toZonedTime, fromZonedTime } from "date-fns-tz"; import { getEnvConfig } from "@/lib/config"; -export type TimePeriod = "5h" | "weekly" | "monthly"; +export type TimePeriod = "5h" | "daily" | "weekly" | "monthly"; +export type DailyResetMode = "fixed" | "rolling"; export interface TimeRange { startTime: Date; @@ -15,21 +26,23 @@ export interface TimeRange { } export interface ResetInfo { - type: "rolling" | "natural"; - resetAt?: Date; // 自然时间窗口的重置时间 + type: "rolling" | "natural" | "custom"; + resetAt?: Date; // 自然/自定义时间窗口的重置时间 period?: string; // 滚动窗口的周期描述 } /** * 根据周期计算时间范围 * - 5h: 滚动窗口(过去 5 小时) + * - daily: 自定义每日重置时间到现在(需要额外的 resetTime 参数) * - weekly: 自然周(本周一 00:00 到现在) * - monthly: 自然月(本月 1 号 00:00 到现在) * * 所有自然时间窗口使用配置的时区(Asia/Shanghai) */ -export function getTimeRangeForPeriod(period: TimePeriod): TimeRange { +export function getTimeRangeForPeriod(period: TimePeriod, resetTime = "00:00"): TimeRange { const timezone = getEnvConfig().TZ; // 'Asia/Shanghai' + const normalizedResetTime = normalizeResetTime(resetTime); const now = new Date(); const endTime = now; let startTime: Date; @@ -40,6 +53,12 @@ export function getTimeRangeForPeriod(period: TimePeriod): TimeRange { startTime = new Date(now.getTime() - 5 * 60 * 60 * 1000); break; + case "daily": { + // 自定义每日重置时间(例如:18:00) + startTime = getCustomDailyResetTime(now, normalizedResetTime, timezone); + break; + } + case "weekly": { // 自然周:本周一 00:00 (Asia/Shanghai) const zonedNow = toZonedTime(now, timezone); @@ -60,20 +79,51 @@ export function getTimeRangeForPeriod(period: TimePeriod): TimeRange { return { startTime, endTime }; } +/** + * 根据周期和模式计算时间范围(支持滚动窗口模式) + * - daily + rolling: 滚动窗口(过去 24 小时) + * - daily + fixed: 固定时间重置(使用 resetTime) + * - 其他周期:使用原有逻辑 + */ +export function getTimeRangeForPeriodWithMode( + period: TimePeriod, + resetTime = "00:00", + mode: DailyResetMode = "fixed" +): TimeRange { + if (period === "daily" && mode === "rolling") { + // 滚动窗口:过去 24 小时 + const now = new Date(); + return { + startTime: new Date(now.getTime() - 24 * 60 * 60 * 1000), + endTime: now, + }; + } + + // 其他情况使用原有逻辑 + return getTimeRangeForPeriod(period, resetTime); +} + /** * 根据周期计算 Redis Key 的 TTL(秒) * - 5h: 5 小时(固定) + * - daily: 到下一个自定义重置时间的秒数 * - weekly: 到下周一 00:00 的秒数 * - monthly: 到下月 1 号 00:00 的秒数 */ -export function getTTLForPeriod(period: TimePeriod): number { +export function getTTLForPeriod(period: TimePeriod, resetTime = "00:00"): number { const timezone = getEnvConfig().TZ; const now = new Date(); + const normalizedResetTime = normalizeResetTime(resetTime); switch (period) { case "5h": return 5 * 3600; // 5 小时 + case "daily": { + const nextReset = getNextDailyResetTime(now, normalizedResetTime, timezone); + return Math.max(1, Math.ceil((nextReset.getTime() - now.getTime()) / 1000)); + } + case "weekly": { // 计算到下周一 00:00 的秒数 const zonedNow = toZonedTime(now, timezone); @@ -96,12 +146,31 @@ export function getTTLForPeriod(period: TimePeriod): number { } } +/** + * 根据周期和模式计算 Redis Key 的 TTL(秒) + * - daily + rolling: 24 小时(固定) + * - daily + fixed: 到下一个自定义重置时间的秒数 + * - 其他周期:使用原有逻辑 + */ +export function getTTLForPeriodWithMode( + period: TimePeriod, + resetTime = "00:00", + mode: DailyResetMode = "fixed" +): number { + if (period === "daily" && mode === "rolling") { + return 24 * 3600; // 24 小时 + } + + return getTTLForPeriod(period, resetTime); +} + /** * 获取重置信息(用于前端展示) */ -export function getResetInfo(period: TimePeriod): ResetInfo { +export function getResetInfo(period: TimePeriod, resetTime = "00:00"): ResetInfo { const timezone = getEnvConfig().TZ; const now = new Date(); + const normalizedResetTime = normalizeResetTime(resetTime); switch (period) { case "5h": @@ -110,6 +179,14 @@ export function getResetInfo(period: TimePeriod): ResetInfo { period: "5 小时", }; + case "daily": { + const nextReset = getNextDailyResetTime(now, normalizedResetTime, timezone); + return { + type: "custom", + resetAt: nextReset, + }; + } + case "weekly": { const zonedNow = toZonedTime(now, timezone); const zonedStartOfWeek = startOfWeek(zonedNow, { weekStartsOn: 1 }); @@ -136,6 +213,81 @@ export function getResetInfo(period: TimePeriod): ResetInfo { } } +/** + * 获取重置信息(支持滚动窗口模式) + */ +export function getResetInfoWithMode( + period: TimePeriod, + resetTime = "00:00", + mode: DailyResetMode = "fixed" +): ResetInfo { + if (period === "daily" && mode === "rolling") { + return { + type: "rolling", + period: "24 小时", + }; + } + + return getResetInfo(period, resetTime); +} + +function getCustomDailyResetTime(now: Date, resetTime: string, timezone: string): Date { + const { hours, minutes } = parseResetTime(resetTime); + const zonedNow = toZonedTime(now, timezone); + const zonedResetToday = buildZonedDate(zonedNow, hours, minutes); + const resetToday = fromZonedTime(zonedResetToday, timezone); + + if (now >= resetToday) { + return resetToday; + } + + return addDays(resetToday, -1); +} + +function getNextDailyResetTime(now: Date, resetTime: string, timezone: string): Date { + const { hours, minutes } = parseResetTime(resetTime); + const zonedNow = toZonedTime(now, timezone); + const zonedResetToday = buildZonedDate(zonedNow, hours, minutes); + const resetToday = fromZonedTime(zonedResetToday, timezone); + + if (now < resetToday) { + return resetToday; + } + + const zonedNextDay = addDays(zonedResetToday, 1); + return fromZonedTime(zonedNextDay, timezone); +} + +function buildZonedDate(base: Date, hours: number, minutes: number): Date { + const withHours = setHours(base, hours); + const withMinutes = setMinutes(withHours, minutes); + const withSeconds = setSeconds(withMinutes, 0); + return setMilliseconds(withSeconds, 0); +} + +function parseResetTime(resetTime: string): { hours: number; minutes: number } { + const matches = /^([0-9]{1,2}):([0-9]{2})$/.exec(resetTime.trim()); + if (!matches) { + return { hours: 0, minutes: 0 }; + } + let hours = Number(matches[1]); + let minutes = Number(matches[2]); + + if (Number.isNaN(hours) || hours < 0 || hours > 23) { + hours = 0; + } + if (Number.isNaN(minutes) || minutes < 0 || minutes > 59) { + minutes = 0; + } + + return { hours, minutes }; +} + +export function normalizeResetTime(resetTime?: string): string { + const { hours, minutes } = parseResetTime(resetTime ?? "00:00"); + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; +} + /** * 计算距离午夜的秒数(用于每日限额) * 使用配置的时区(Asia/Shanghai)而非服务器本地时区 diff --git a/src/lib/redis/client.ts b/src/lib/redis/client.ts index d211c94e7..820e87ce7 100644 --- a/src/lib/redis/client.ts +++ b/src/lib/redis/client.ts @@ -3,6 +3,18 @@ import { logger } from "@/lib/logger"; let redisClient: Redis | null = null; +function maskRedisUrl(redisUrl: string) { + try { + const parsed = new URL(redisUrl); + if (parsed.password) { + parsed.password = "****"; + } + return parsed.toString(); + } catch { + return redisUrl.replace(/:\w+@/, ":****@"); + } +} + /** * Build ioredis connection options with protocol-based TLS detection. * - When `rediss://` is used, explicitly enable TLS via `tls: {}` @@ -53,6 +65,8 @@ export function getRedisClient(): Redis | null { return null; } + const safeRedisUrl = maskRedisUrl(redisUrl); + if (redisClient) { return redisClient; } @@ -77,7 +91,7 @@ export function getRedisClient(): Redis | null { // 2. 如果使用 rediss://,则添加显式的 TLS 和 SNI (host) 配置 if (useTls) { - logger.info("[Redis] Using TLS connection (rediss://)"); + logger.info("[Redis] Using TLS connection (rediss://)", { redisUrl: safeRedisUrl }); try { // 从 URL 中解析 hostname,用于 SNI const url = new URL(redisUrl); @@ -99,6 +113,7 @@ export function getRedisClient(): Redis | null { logger.info("[Redis] Connected successfully", { protocol: useTls ? "rediss" : "redis", tlsEnabled: useTls, + redisUrl: safeRedisUrl, }); }); @@ -107,17 +122,18 @@ export function getRedisClient(): Redis | null { error: error instanceof Error ? error.message : String(error), protocol: useTls ? "rediss" : "redis", tlsEnabled: useTls, + redisUrl: safeRedisUrl, }); }); redisClient.on("close", () => { - logger.warn("[Redis] Connection closed"); + logger.warn("[Redis] Connection closed", { redisUrl: safeRedisUrl }); }); // 5. 返回客户端实例 return redisClient; } catch (error) { - logger.error("[Redis] Failed to initialize:", error); + logger.error("[Redis] Failed to initialize:", error, { redisUrl: safeRedisUrl }); return null; } } diff --git a/src/lib/redis/lua-scripts.ts b/src/lib/redis/lua-scripts.ts index bfa5604fb..530132e31 100644 --- a/src/lib/redis/lua-scripts.ts +++ b/src/lib/redis/lua-scripts.ts @@ -180,3 +180,83 @@ end return tostring(total) `; + +/** + * 追踪 24小时滚动窗口消费(使用 ZSET) + * + * 功能: + * 1. 清理 24 小时前的消费记录 + * 2. 添加当前消费记录(带时间戳) + * 3. 计算当前窗口内的总消费 + * 4. 设置兜底 TTL(25 小时) + * + * KEYS[1]: key:${id}:cost_daily_rolling 或 provider:${id}:cost_daily_rolling + * ARGV[1]: cost(本次消费金额) + * ARGV[2]: now(当前时间戳,毫秒) + * ARGV[3]: window(窗口时长,毫秒,默认 86400000 = 24小时) + * + * 返回值:string - 当前窗口内的总消费 + */ +export const TRACK_COST_DAILY_ROLLING_WINDOW = ` +local key = KEYS[1] +local cost = tonumber(ARGV[1]) +local now_ms = tonumber(ARGV[2]) +local window_ms = tonumber(ARGV[3]) -- 24 hours = 86400000 ms + +-- 1. 清理过期记录(24 小时前的数据) +redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms) + +-- 2. 添加当前消费记录(member = timestamp:cost,便于调试和追踪) +local member = now_ms .. ':' .. cost +redis.call('ZADD', key, now_ms, member) + +-- 3. 计算窗口内总消费 +local records = redis.call('ZRANGE', key, 0, -1) +local total = 0 +for _, record in ipairs(records) do + -- 解析 member 格式:"timestamp:cost" + local cost_str = string.match(record, ':(.+)') + if cost_str then + total = total + tonumber(cost_str) + end +end + +-- 4. 设置兜底 TTL(25 小时,防止数据永久堆积) +redis.call('EXPIRE', key, 90000) + +return tostring(total) +`; + +/** + * 查询 24小时滚动窗口当前消费 + * + * 功能: + * 1. 清理 24 小时前的消费记录 + * 2. 计算当前窗口内的总消费 + * + * KEYS[1]: key:${id}:cost_daily_rolling 或 provider:${id}:cost_daily_rolling + * ARGV[1]: now(当前时间戳,毫秒) + * ARGV[2]: window(窗口时长,毫秒,默认 86400000 = 24小时) + * + * 返回值:string - 当前窗口内的总消费 + */ +export const GET_COST_DAILY_ROLLING_WINDOW = ` +local key = KEYS[1] +local now_ms = tonumber(ARGV[1]) +local window_ms = tonumber(ARGV[2]) -- 24 hours = 86400000 ms + +-- 1. 清理过期记录 +redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms) + +-- 2. 计算窗口内总消费 +local records = redis.call('ZRANGE', key, 0, -1) +local total = 0 +for _, record in ipairs(records) do + local cost_str = string.match(record, ':(.+)') + if cost_str then + total = total + tonumber(cost_str) + end +end + +return tostring(total) +`; diff --git a/src/lib/utils/error-messages.ts b/src/lib/utils/error-messages.ts index c4ad9059d..d631ba507 100644 --- a/src/lib/utils/error-messages.ts +++ b/src/lib/utils/error-messages.ts @@ -160,7 +160,8 @@ export function getErrorMessage( ): string { try { return t(code, params); - } catch { + } catch (error) { + console.warn("Translation missing for error code", code, error); // Fallback to generic error message if translation key not found return t("INTERNAL_ERROR"); } @@ -189,7 +190,8 @@ export async function getErrorMessageServer( const { getTranslations } = await import("next-intl/server"); const t = await getTranslations({ locale, namespace: "errors" }); return t(code, params); - } catch { + } catch (error) { + console.error("getErrorMessageServer failed", { locale, code, error }); // Fallback to generic error message return "An error occurred"; } diff --git a/src/lib/utils/quota-helpers.ts b/src/lib/utils/quota-helpers.ts index a609e6f08..d0dd9c568 100644 --- a/src/lib/utils/quota-helpers.ts +++ b/src/lib/utils/quota-helpers.ts @@ -7,6 +7,7 @@ // 类型定义 export type KeyQuota = { cost5h: { current: number; limit: number | null }; + costDaily: { current: number; limit: number | null }; costWeekly: { current: number; limit: number | null }; costMonthly: { current: number; limit: number | null }; concurrentSessions: { current: number; limit: number }; @@ -28,6 +29,7 @@ export function hasKeyQuotaSet(quota: KeyQuota): boolean { return !!( quota.cost5h.limit || + quota.costDaily.limit || quota.costWeekly.limit || quota.costMonthly.limit || (quota.concurrentSessions.limit && quota.concurrentSessions.limit > 0) @@ -75,6 +77,9 @@ export function getMaxUsageRate(quota: KeyQuota): number { if (quota.cost5h.limit) { rates.push(getUsageRate(quota.cost5h.current, quota.cost5h.limit)); } + if (quota.costDaily.limit) { + rates.push(getUsageRate(quota.costDaily.current, quota.costDaily.limit)); + } if (quota.costWeekly.limit) { rates.push(getUsageRate(quota.costWeekly.current, quota.costWeekly.limit)); } diff --git a/src/lib/utils/zod-i18n.ts b/src/lib/utils/zod-i18n.ts index 7837f9bf8..f8a72550e 100644 --- a/src/lib/utils/zod-i18n.ts +++ b/src/lib/utils/zod-i18n.ts @@ -55,7 +55,12 @@ export function setZodErrorMap( try { return { message: t(code, params) }; - } catch { + } catch (error) { + // Only log in development to avoid sensitive data exposure + if (process.env.NODE_ENV === "development") { + console.warn("setZodErrorMap fallback", { code, error }); + // Avoid logging the full issue object which may contain user input + } // Fallback to Zod default message return { message: _ctx.defaultError }; } @@ -91,7 +96,12 @@ export async function getZodErrorMapServer(locale: string) { try { return { message: t(code, params) }; - } catch { + } catch (error) { + // Only log in development to avoid sensitive data exposure + if (process.env.NODE_ENV === "development") { + console.warn("getZodErrorMapServer fallback", { locale, code, error }); + // Avoid logging the full issue object which may contain user input + } return { message: _ctx.defaultError }; } }; diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 227bd0d40..ad9d9f8f8 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -118,6 +118,18 @@ export const KeyFormSchema = z.object({ .max(10000, "5小时消费上限不能超过10000美元") .nullable() .optional(), + limitDailyUsd: z.coerce + .number() + .min(0, "每日消费上限不能为负数") + .max(10000, "每日消费上限不能超过10000美元") + .nullable() + .optional(), + dailyResetMode: z.enum(["fixed", "rolling"]).optional().default("fixed"), + dailyResetTime: z + .string() + .regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, "重置时间格式必须为 HH:mm") + .optional() + .default("00:00"), limitWeeklyUsd: z.coerce .number() .min(0, "周消费上限不能为负数") @@ -184,6 +196,18 @@ export const CreateProviderSchema = z.object({ .max(10000, "5小时消费上限不能超过10000美元") .nullable() .optional(), + limit_daily_usd: z.coerce + .number() + .min(0, "每日消费上限不能为负数") + .max(10000, "每日消费上限不能超过10000美元") + .nullable() + .optional(), + daily_reset_mode: z.enum(["fixed", "rolling"]).optional().default("fixed"), + daily_reset_time: z + .string() + .regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, "重置时间格式必须为 HH:mm") + .optional() + .default("00:00"), limit_weekly_usd: z.coerce .number() .min(0, "周消费上限不能为负数") @@ -322,6 +346,17 @@ export const UpdateProviderSchema = z .max(10000, "5小时消费上限不能超过10000美元") .nullable() .optional(), + limit_daily_usd: z.coerce + .number() + .min(0, "每日消费上限不能为负数") + .max(10000, "每日消费上限不能超过10000美元") + .nullable() + .optional(), + daily_reset_mode: z.enum(["fixed", "rolling"]).optional(), + daily_reset_time: z + .string() + .regex(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/, "重置时间格式必须为 HH:mm") + .optional(), limit_weekly_usd: z.coerce .number() .min(0, "周消费上限不能为负数") diff --git a/src/repository/_shared/transformers.ts b/src/repository/_shared/transformers.ts index 4451b597f..fd2a2c6f8 100644 --- a/src/repository/_shared/transformers.ts +++ b/src/repository/_shared/transformers.ts @@ -27,6 +27,8 @@ export function toKey(dbKey: any): Key { isEnabled: dbKey?.isEnabled ?? true, canLoginWebUi: dbKey?.canLoginWebUi ?? true, limit5hUsd: dbKey?.limit5hUsd ? parseFloat(dbKey.limit5hUsd) : null, + limitDailyUsd: dbKey?.limitDailyUsd ? parseFloat(dbKey.limitDailyUsd) : null, + dailyResetTime: dbKey?.dailyResetTime ?? "00:00", limitWeeklyUsd: dbKey?.limitWeeklyUsd ? parseFloat(dbKey.limitWeeklyUsd) : null, limitMonthlyUsd: dbKey?.limitMonthlyUsd ? parseFloat(dbKey.limitMonthlyUsd) : null, limitConcurrentSessions: dbKey?.limitConcurrentSessions ?? 0, @@ -48,6 +50,8 @@ export function toProvider(dbProvider: any): Provider { modelRedirects: dbProvider?.modelRedirects ?? null, codexInstructionsStrategy: dbProvider?.codexInstructionsStrategy ?? "auto", limit5hUsd: dbProvider?.limit5hUsd ? parseFloat(dbProvider.limit5hUsd) : null, + limitDailyUsd: dbProvider?.limitDailyUsd ? parseFloat(dbProvider.limitDailyUsd) : null, + dailyResetTime: dbProvider?.dailyResetTime ?? "00:00", limitWeeklyUsd: dbProvider?.limitWeeklyUsd ? parseFloat(dbProvider.limitWeeklyUsd) : null, limitMonthlyUsd: dbProvider?.limitMonthlyUsd ? parseFloat(dbProvider.limitMonthlyUsd) : null, limitConcurrentSessions: dbProvider?.limitConcurrentSessions ?? 0, diff --git a/src/repository/key.ts b/src/repository/key.ts index fb0aae9b0..c5090c66d 100644 --- a/src/repository/key.ts +++ b/src/repository/key.ts @@ -19,6 +19,9 @@ export async function findKeyById(id: number): Promise { expiresAt: keys.expiresAt, canLoginWebUi: keys.canLoginWebUi, limit5hUsd: keys.limit5hUsd, + limitDailyUsd: keys.limitDailyUsd, + dailyResetMode: keys.dailyResetMode, + dailyResetTime: keys.dailyResetTime, limitWeeklyUsd: keys.limitWeeklyUsd, limitMonthlyUsd: keys.limitMonthlyUsd, limitConcurrentSessions: keys.limitConcurrentSessions, @@ -44,6 +47,9 @@ export async function findKeyList(userId: number): Promise { expiresAt: keys.expiresAt, canLoginWebUi: keys.canLoginWebUi, limit5hUsd: keys.limit5hUsd, + limitDailyUsd: keys.limitDailyUsd, + dailyResetMode: keys.dailyResetMode, + dailyResetTime: keys.dailyResetTime, limitWeeklyUsd: keys.limitWeeklyUsd, limitMonthlyUsd: keys.limitMonthlyUsd, limitConcurrentSessions: keys.limitConcurrentSessions, @@ -67,6 +73,9 @@ export async function createKey(keyData: CreateKeyData): Promise { expiresAt: keyData.expires_at, canLoginWebUi: keyData.can_login_web_ui ?? true, limit5hUsd: keyData.limit_5h_usd != null ? keyData.limit_5h_usd.toString() : null, + limitDailyUsd: keyData.limit_daily_usd != null ? keyData.limit_daily_usd.toString() : null, + dailyResetMode: keyData.daily_reset_mode ?? "fixed", + dailyResetTime: keyData.daily_reset_time ?? "00:00", limitWeeklyUsd: keyData.limit_weekly_usd != null ? keyData.limit_weekly_usd.toString() : null, limitMonthlyUsd: keyData.limit_monthly_usd != null ? keyData.limit_monthly_usd.toString() : null, @@ -82,6 +91,9 @@ export async function createKey(keyData: CreateKeyData): Promise { expiresAt: keys.expiresAt, canLoginWebUi: keys.canLoginWebUi, limit5hUsd: keys.limit5hUsd, + limitDailyUsd: keys.limitDailyUsd, + dailyResetMode: keys.dailyResetMode, + dailyResetTime: keys.dailyResetTime, limitWeeklyUsd: keys.limitWeeklyUsd, limitMonthlyUsd: keys.limitMonthlyUsd, limitConcurrentSessions: keys.limitConcurrentSessions, @@ -108,6 +120,11 @@ export async function updateKey(id: number, keyData: UpdateKeyData): Promise { joinClaudePool: providers.joinClaudePool, codexInstructionsStrategy: providers.codexInstructionsStrategy, limit5hUsd: providers.limit5hUsd, + limitDailyUsd: providers.limitDailyUsd, + dailyResetMode: providers.dailyResetMode, + dailyResetTime: providers.dailyResetTime, limitWeeklyUsd: providers.limitWeeklyUsd, limitMonthlyUsd: providers.limitMonthlyUsd, limitConcurrentSessions: providers.limitConcurrentSessions, @@ -223,6 +236,13 @@ export async function updateProvider( if (providerData.limit_5h_usd !== undefined) dbData.limit5hUsd = providerData.limit_5h_usd != null ? providerData.limit_5h_usd.toString() : null; + if (providerData.limit_daily_usd !== undefined) + dbData.limitDailyUsd = + providerData.limit_daily_usd != null ? providerData.limit_daily_usd.toString() : null; + if (providerData.daily_reset_mode !== undefined) + dbData.dailyResetMode = providerData.daily_reset_mode; + if (providerData.daily_reset_time !== undefined) + dbData.dailyResetTime = providerData.daily_reset_time; if (providerData.limit_weekly_usd !== undefined) dbData.limitWeeklyUsd = providerData.limit_weekly_usd != null ? providerData.limit_weekly_usd.toString() : null; @@ -274,6 +294,9 @@ export async function updateProvider( joinClaudePool: providers.joinClaudePool, codexInstructionsStrategy: providers.codexInstructionsStrategy, limit5hUsd: providers.limit5hUsd, + limitDailyUsd: providers.limitDailyUsd, + dailyResetMode: providers.dailyResetMode, + dailyResetTime: providers.dailyResetTime, limitWeeklyUsd: providers.limitWeeklyUsd, limitMonthlyUsd: providers.limitMonthlyUsd, limitConcurrentSessions: providers.limitConcurrentSessions, diff --git a/src/types/key.ts b/src/types/key.ts index 7e0c23c9d..86beda207 100644 --- a/src/types/key.ts +++ b/src/types/key.ts @@ -14,6 +14,9 @@ export interface Key { // 金额限流配置 limit5hUsd: number | null; + limitDailyUsd: number | null; + dailyResetMode: "fixed" | "rolling"; + dailyResetTime: string; // HH:mm 格式 limitWeeklyUsd: number | null; limitMonthlyUsd: number | null; limitConcurrentSessions: number; @@ -36,6 +39,9 @@ export interface CreateKeyData { can_login_web_ui?: boolean; // 金额限流配置 limit_5h_usd?: number | null; + limit_daily_usd?: number | null; + daily_reset_mode?: "fixed" | "rolling"; + daily_reset_time?: string; limit_weekly_usd?: number | null; limit_monthly_usd?: number | null; limit_concurrent_sessions?: number; @@ -52,6 +58,9 @@ export interface UpdateKeyData { can_login_web_ui?: boolean; // 金额限流配置 limit_5h_usd?: number | null; + limit_daily_usd?: number | null; + daily_reset_mode?: "fixed" | "rolling"; + daily_reset_time?: string; limit_weekly_usd?: number | null; limit_monthly_usd?: number | null; limit_concurrent_sessions?: number; diff --git a/src/types/provider.ts b/src/types/provider.ts index ecf90104f..532419699 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -44,6 +44,9 @@ export interface Provider { // 金额限流配置 limit5hUsd: number | null; + limitDailyUsd: number | null; + dailyResetMode: "fixed" | "rolling"; + dailyResetTime: string; limitWeeklyUsd: number | null; limitMonthlyUsd: number | null; limitConcurrentSessions: number; @@ -104,6 +107,9 @@ export interface ProviderDisplay { codexInstructionsStrategy: CodexInstructionsStrategy; // 金额限流配置 limit5hUsd: number | null; + limitDailyUsd: number | null; + dailyResetMode: "fixed" | "rolling"; + dailyResetTime: string; limitWeeklyUsd: number | null; limitMonthlyUsd: number | null; limitConcurrentSessions: number; @@ -158,6 +164,9 @@ export interface CreateProviderData { // 金额限流配置 limit_5h_usd?: number | null; + limit_daily_usd?: number | null; + daily_reset_mode?: "fixed" | "rolling"; + daily_reset_time?: string; limit_weekly_usd?: number | null; limit_monthly_usd?: number | null; limit_concurrent_sessions?: number; @@ -214,6 +223,9 @@ export interface UpdateProviderData { // 金额限流配置 limit_5h_usd?: number | null; + limit_daily_usd?: number | null; + daily_reset_mode?: "fixed" | "rolling"; + daily_reset_time?: string; limit_weekly_usd?: number | null; limit_monthly_usd?: number | null; limit_concurrent_sessions?: number; diff --git a/src/types/user.ts b/src/types/user.ts index 3499b1516..b3d3b746d 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -77,6 +77,8 @@ export interface UserKeyDisplay { canLoginWebUi: boolean; // 是否允许使用该 Key 登录 Web UI // 限额配置 limit5hUsd: number | null; // 5小时消费上限(美元) + limitDailyUsd: number | null; // 每日消费上限 + dailyResetTime: string; // 每日重置时间 limitWeeklyUsd: number | null; // 周消费上限(美元) limitMonthlyUsd: number | null; // 月消费上限(美元) limitConcurrentSessions: number; // 并发 Session 上限