Compare commits

..

14 Commits

31 changed files with 2660 additions and 1600 deletions
+15 -16
View File
@@ -6,7 +6,7 @@ A single-file Web UI (React + TypeScript) for operating and troubleshooting the
**Main Project**: https://github.com/router-for-me/CLIProxyAPI
**Example URL**: https://remote.router-for.me/
**Minimum Required Version**: ≥ 6.8.0 (recommended ≥ 6.8.15)
**Minimum Required Version**: ≥ 7.1.0 (recommended latest)
Since version 6.0.19, the Web UI ships with the main program; access it via `/management.html` on the API port once the service is running.
@@ -28,7 +28,7 @@ The address is auto-detected from the current page URL; manual override is suppo
### Option B: Run the dev server
```bash
npm install
npm ci
npm run dev
```
@@ -37,7 +37,7 @@ Open `http://localhost:5173`, then connect to your CLI Proxy API backend instanc
### Option C: Build a single HTML file
```bash
npm install
npm ci
npm run build
```
@@ -69,45 +69,44 @@ This is different from the proxy `api-keys` you manage inside the UI (those are
### Remote management
If you connect from a non-localhost browser, the server must allow remote management (e.g. `allow-remote-management: true`).
See `api.md` for the full authentication rules, server-side limits, and edge cases.
Check the CLI Proxy API server documentation/config comments for the full authentication rules, server-side limits, and edge cases.
## What you can manage (mapped to the UI pages)
- **Dashboard**: connection status, server version/build date, quick counts, model availability snapshot.
- **Basic Settings**: debug, proxy URL, request retry, quota fallback (switch project or preview models when limits reached), request logging, file logging, WebSocket auth.
- **API Keys**: manage proxy `api-keys` (add/edit/delete).
- **Config Panel**: visual editor for common `config.yaml` fields, basic settings, proxy `api-keys`, and source editing with YAML highlighting/search plus a save diff preview.
- **AI Providers**:
- Gemini/Codex/Claude/Vertex key entries (base URL, headers, proxy, model aliases, excluded models, prefix).
- OpenAI-compatible providers (multiple API keys, custom headers, model alias import via `/v1/models`, optional browser-side "chat/completions" test).
- Ampcode integration (upstream URL/key, force mappings, model mapping table).
- **Auth Files**: upload/download/delete JSON credentials, filter/search/pagination, runtime-only indicators, view supported models per credential (when the server supports it), manage OAuth excluded models (supports `*` wildcards), configure OAuth model alias mappings.
- **OAuth**: start OAuth/device flows for supported providers, poll status, optionally submit callback `redirect_url`; includes iFlow cookie import.
- **OAuth**: start OAuth/device flows for Codex, Anthropic/Claude, Antigravity, Gemini CLI, Kimi, and xAI/Grok; poll status; submit callback URLs or xAI/Grok displayed codes; import Vertex JSON credentials and iFlow cookies.
- **Quota Management**: manage quota limits and usage for Claude, Antigravity, Codex, Gemini CLI, and other providers.
- **Config**: edit `/config.yaml` in-browser with YAML highlighting + search, then save/reload.
- **Logs**: tail logs with incremental polling, auto-refresh, search, hide management traffic, clear logs; download request error log files.
- **System**: quick links + fetch `/v1/models` (grouped view). Requires at least one proxy API key to query models.
- **System**: quick links, update check, request logging toggle, local login data cleanup, and fetch `/v1/models` (grouped view). Requires at least one proxy API key to query models.
## Tech Stack
- React 19 + TypeScript 5.9
- Vite 7 (single-file build)
- React 19 + TypeScript 6.0
- Vite 8 (single-file build)
- Zustand (state management)
- Axios (HTTP client)
- react-router-dom v7 (HashRouter)
- Chart.js (data visualization)
- Motion (animations)
- CodeMirror 6 (YAML editor)
- SCSS Modules (styling)
- i18next (internationalization)
## Internationalization
Currently supports three languages:
Currently supports four languages:
- English (en)
- Simplified Chinese (zh-CN)
- Traditional Chinese (zh-TW)
- Russian (ru)
The UI language is automatically detected from browser settings and can be manually switched at the bottom of the page.
The UI language is automatically detected from browser settings and can be manually switched from the login page or header language menu.
## Browser Compatibility
@@ -119,7 +118,7 @@ The UI language is automatically detected from browser settings and can be manua
- Vite produces a **single HTML** output (`dist/index.html`) with all assets inlined (via `vite-plugin-singlefile`).
- Tagging `vX.Y.Z` triggers `.github/workflows/release.yml` to publish `dist/management.html`.
- The UI version shown in the footer is injected at build time (env `VERSION`, git tag, or `package.json` fallback).
- The UI version shown on the System page is injected at build time (env `VERSION`, git tag, or `package.json` fallback).
## Security notes
@@ -151,7 +150,7 @@ Issues and PRs are welcome. Please include:
- Reproduction steps (server version + UI version)
- Screenshots for UI changes
- Verification notes (`npm run lint`, `npm run type-check`)
- Verification notes (`npm run lint`, `npm run type-check`, `npm run build`)
## License
+15 -15
View File
@@ -6,7 +6,7 @@
**主项目**: https://github.com/router-for-me/CLIProxyAPI
**示例地址**: https://remote.router-for.me/
**最低版本要求**: ≥ 6.8.0(推荐 ≥ 6.8.15
**最低版本要求**: ≥ 7.1.0(推荐最新
从6.0.19版本开始,Web UI 随主程序一起提供;服务运行后,通过 API 端口上的"/management.html"访问它。
@@ -28,7 +28,7 @@
### 方式 B:开发调试
```bash
npm install
npm ci
npm run dev
```
@@ -37,7 +37,7 @@ npm run dev
### 方式 C:构建单文件 HTML
```bash
npm install
npm ci
npm run build
```
@@ -69,44 +69,44 @@ npm run build
### 远程管理
当你从非 localhost 的浏览器访问时,服务端通常需要开启远程管理(例如 `allow-remote-management: true`)。
完整鉴权规则、服务端限制与边界情况请参考 CLI Proxy API 服务端文档或配置注释。
## 功能一览(按页面对应)
- **仪表盘**:连接状态、服务版本/构建时间、关键数量概览、可用模型概览。
- **基础设置**:调试开关、代理 URL、请求重试、配额回退(达到上限时切换项目或预览模型)、请求日志、文件日志、WebSocket 鉴权
- **API Keys**:管理代理 `api-keys`(增/改/删)。
- **配置面板**:可视化编辑常用 `config.yaml` 字段、基础设置与代理 `api-keys`;也支持源码编辑、YAML 高亮/搜索与保存前差异预览
- **AI 提供商**
- Gemini/Codex/Claude/Vertex 配置(Base URL、Headers、代理、模型别名、排除模型、Prefix)。
- OpenAI 兼容提供商(多 Key、Header、自助从 `/v1/models` 拉取并导入模型别名、可选浏览器侧 `chat/completions` 测试)。
- Ampcode 集成(上游地址/密钥、强制映射、模型映射表)。
- **认证文件**:上传/下载/删除 JSON 凭据,筛选/搜索/分页,标记 runtime-only;查看单个凭据可用模型(依赖后端支持);管理 OAuth 排除模型(支持 `*` 通配符);配置 OAuth 模型别名映射。
- **OAuth**:对支持的提供商发起 OAuth/设备码流程轮询状态;可选提交回调 `redirect_url`;包含 iFlow Cookie 导入。
- **OAuth**:对 Codex、Anthropic/Claude、Antigravity、Gemini CLI、Kimi、xAI/Grok 发起 OAuth/设备码流程轮询状态;支持提交回调 URL 或 xAI/Grok 页面显示的 code;包含 Vertex JSON 凭据导入与 iFlow Cookie 导入。
- **配额管理**:管理 Claude、Antigravity、Codex、Gemini CLI 等提供商的配额上限与使用情况。
- **配置文件**:浏览器内编辑 `/config.yaml`(YAML 高亮 + 搜索),保存/重载。
- **日志**:增量拉取日志、自动刷新、搜索、隐藏管理端流量、清空日志;下载请求错误日志文件。
- **系统信息**:快捷链接 + 拉取 `/v1/models` 并分组展示(需要至少一个代理 API Key 才能查询模型)。
- **系统信息**:快捷链接、版本检查、请求日志开关、本地登录信息清理,以及拉取 `/v1/models` 并分组展示(需要至少一个代理 API Key 才能查询模型)。
## 技术栈
- React 19 + TypeScript 5.9
- Vite 7(单文件构建)
- React 19 + TypeScript 6.0
- Vite 8(单文件构建)
- Zustand(状态管理)
- AxiosHTTP 客户端)
- react-router-dom v7HashRouter
- Chart.js(数据可视化
- Motion(动效
- CodeMirror 6YAML 编辑器)
- SCSS Modules(样式)
- i18next(国际化)
## 多语言支持
目前支持种语言:
目前支持种语言:
- 英文 (en)
- 简体中文 (zh-CN)
- 繁体中文 (zh-TW)
- 俄文 (ru)
界面语言会根据浏览器设置自动切换,也可在页面底部手动切换。
界面语言会根据浏览器设置自动切换,也可在登录页或顶部语言菜单手动切换。
## 浏览器兼容性
@@ -118,7 +118,7 @@ npm run build
- 使用 Vite 输出 **单文件 HTML**`dist/index.html`),资源全部内联(`vite-plugin-singlefile`)。
-`vX.Y.Z` 标签会触发 `.github/workflows/release.yml`,发布 `dist/management.html`
-显示的 UI 版本在构建期注入(优先使用环境变量 `VERSION`,否则使用 git tag / `package.json`)。
- 系统信息页显示的 UI 版本在构建期注入(优先使用环境变量 `VERSION`,否则使用 git tag / `package.json`)。
## 安全提示
@@ -150,7 +150,7 @@ npm run type-check # tsc --noEmit
- 复现步骤(服务端版本 + UI 版本)
- UI 改动截图
- 验证记录(`npm run lint``npm run type-check`
- 验证记录(`npm run lint``npm run type-check``npm run build`
## 许可证
+169 -180
View File
@@ -12,11 +12,11 @@
"@codemirror/merge": "^6.12.0",
"@uiw/react-codemirror": "^4.25.3",
"axios": "^1.13.2",
"i18next": "^25.7.1",
"i18next": "^26.2.0",
"motion": "^12.34.3",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-i18next": "^16.4.0",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.10.1",
"yaml": "^2.8.2",
"zustand": "^5.0.9"
@@ -25,8 +25,8 @@
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
@@ -34,8 +34,8 @@
"globals": "^16.5.0",
"prettier": "^3.7.4",
"sass": "^1.94.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"vite": "^8.0.10",
"vite-plugin-singlefile": "^2.3.3"
}
@@ -266,9 +266,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -478,9 +478,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1453,21 +1453,20 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
"integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.48.1",
"@typescript-eslint/type-utils": "8.48.1",
"@typescript-eslint/utils": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/type-utils": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1477,18 +1476,11 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"@typescript-eslint/parser": "^8.59.3",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -1499,31 +1491,18 @@
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz",
"integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.1",
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/typescript-estree": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1",
"debug": "^4.3.4"
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1533,20 +1512,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz",
"integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.48.1",
"@typescript-eslint/types": "^8.48.1",
"debug": "^4.3.4"
"@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1556,18 +1535,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz",
"integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1578,9 +1557,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz",
"integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1591,21 +1570,21 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz",
"integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/typescript-estree": "8.48.1",
"@typescript-eslint/utils": "8.48.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1615,27 +1594,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz",
"integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1647,21 +1613,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz",
"integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.48.1",
"@typescript-eslint/tsconfig-utils": "8.48.1",
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/visitor-keys": "8.48.1",
"debug": "^4.3.4",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1671,59 +1637,59 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^2.0.2"
"brace-expansion": "^5.0.5"
},
"engines": {
"node": ">=16 || 14 >=14.17"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz",
"integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.48.1",
"@typescript-eslint/types": "8.48.1",
"@typescript-eslint/typescript-estree": "8.48.1"
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1733,19 +1699,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz",
"integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.1",
"eslint-visitor-keys": "^4.2.1"
"@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1755,6 +1721,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@uiw/codemirror-extensions-basic-setup": {
"version": "4.25.3",
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.3.tgz",
@@ -2746,29 +2725,26 @@
}
},
"node_modules/i18next": {
"version": "25.7.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.1.tgz",
"integrity": "sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw==",
"version": "26.2.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.2.0.tgz",
"integrity": "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"typescript": "^5"
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"typescript": {
@@ -3535,19 +3511,19 @@
}
},
"node_modules/react-i18next": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.4.0.tgz",
"integrity": "sha512-bxVeBA8Ky2UeItNhF4JRxHCFIrpEJHGFG/mOAa4CR0JkqaDEYSLmlEgmC4Os63SBlZ+E5U0YyrNJOSVl2mtVqQ==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@babel/runtime": "^7.29.2",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"i18next": ">= 26.2.0",
"react": ">= 16.8.0",
"typescript": "^5"
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"react-dom": {
@@ -3700,9 +3676,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3793,6 +3769,19 @@
"node": ">=8.0"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -3811,9 +3800,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@@ -3825,16 +3814,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.48.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz",
"integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==",
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
"integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.48.1",
"@typescript-eslint/parser": "8.48.1",
"@typescript-eslint/typescript-estree": "8.48.1",
"@typescript-eslint/utils": "8.48.1"
"@typescript-eslint/eslint-plugin": "8.59.3",
"@typescript-eslint/parser": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3844,8 +3833,8 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/update-browserslist-db": {
+6 -6
View File
@@ -16,11 +16,11 @@
"@codemirror/merge": "^6.12.0",
"@uiw/react-codemirror": "^4.25.3",
"axios": "^1.13.2",
"i18next": "^25.7.1",
"i18next": "^26.2.0",
"motion": "^12.34.3",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-i18next": "^16.4.0",
"react-i18next": "^17.0.8",
"react-router-dom": "^7.10.1",
"yaml": "^2.8.2",
"zustand": "^5.0.9"
@@ -29,8 +29,8 @@
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
@@ -38,8 +38,8 @@
"globals": "^16.5.0",
"prettier": "^3.7.4",
"sass": "^1.94.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.3",
"vite": "^8.0.10",
"vite-plugin-singlefile": "^2.3.3"
}
+1
View File
@@ -0,0 +1 @@
<svg fill="#ffffff" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>

After

Width:  |  Height:  |  Size: 752 B

+51 -46
View File
@@ -1,44 +1,52 @@
@use '../../styles/mixins' as *;
@use '../../styles/variables' as *;
.section {
display: grid;
grid-template-columns: minmax(0, 220px) minmax(0, 1fr);
gap: 24px;
padding: 26px 0;
border-top: 1px solid color-mix(in srgb, var(--border-color) 76%, transparent);
scroll-margin-top: 112px;
&:first-child {
border-top: none;
padding-top: 0;
}
display: flex;
flex-direction: column;
gap: clamp(16px, 2vw, 22px);
height: clamp(520px, calc(100dvh - var(--header-height, 64px) - 250px), 780px);
min-width: 0;
box-sizing: border-box;
overflow-y: auto;
overscroll-behavior: auto;
padding: clamp(20px, 2.4vw, 28px);
border: 1px solid var(--border-color);
border-radius: 8px;
background: color-mix(in srgb, var(--bg-primary) 82%, transparent);
scroll-margin-top: 104px;
scroll-snap-align: start;
scroll-snap-stop: always;
scrollbar-width: thin;
@include mobile {
grid-template-columns: minmax(0, 1fr);
gap: 18px;
padding: 22px 0;
scroll-margin-top: 88px;
gap: 14px;
height: clamp(420px, calc(100dvh - var(--header-height, 64px) - 260px), 680px);
padding: 16px;
scroll-margin-top: 92px;
}
}
.header {
position: sticky;
top: 104px;
align-self: start;
display: flex;
flex-direction: column;
gap: 14px;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 10px;
align-items: start;
padding-bottom: 14px;
border-bottom: 1px solid var(--border-color);
background: color-mix(in srgb, var(--bg-primary) 88%, transparent);
@include mobile {
position: static;
grid-template-columns: minmax(0, 1fr);
gap: 10px;
padding-bottom: 12px;
background: transparent;
}
}
.titleRow {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
min-width: 0;
}
@@ -46,17 +54,14 @@
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 42px;
height: 42px;
padding: 0 12px;
border-radius: 14px;
background:
linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 92%, transparent), transparent),
color-mix(in srgb, var(--primary-color) 11%, var(--bg-secondary));
border: 1px solid color-mix(in srgb, var(--primary-color) 18%, var(--border-color));
color: var(--primary-hover);
font-size: 12px;
font-weight: 700;
min-width: 32px;
height: 28px;
padding: 0 8px;
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-secondary);
font-size: 11px;
font-weight: 750;
letter-spacing: 0.08em;
}
@@ -64,11 +69,9 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 14px;
background: color-mix(in srgb, var(--bg-secondary) 88%, transparent);
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
width: 28px;
height: 28px;
border-radius: 6px;
color: var(--text-secondary);
flex: 0 0 auto;
}
@@ -82,18 +85,19 @@
.title {
margin: 0;
font-size: clamp(20px, 2vw, 25px);
line-height: 1.08;
font-weight: 700;
color: var(--text-primary);
font-size: clamp(18px, 1.6vw, 22px);
font-weight: 680;
line-height: 1.18;
letter-spacing: 0;
}
.description {
margin: 0;
max-width: 34ch;
max-width: 72ch;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.7;
font-size: 13px;
line-height: 1.65;
@include mobile {
max-width: none;
@@ -104,5 +108,6 @@
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
min-width: 0;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,7 @@ import styles from './VisualConfigEditor.module.scss';
import { copyToClipboard } from '@/utils/clipboard';
import type {
PayloadFilterRule,
PayloadHeaderEntry,
PayloadModelEntry,
PayloadParamEntry,
PayloadParamValidationErrorCode,
@@ -449,6 +450,17 @@ const StringListEditor = memo(function StringListEditor({
);
});
function hasPayloadModelAdvancedSettings(model: PayloadModelEntry) {
return Boolean(
model.fromProtocol ||
(model.headers?.length ?? 0) > 0 ||
(model.match?.length ?? 0) > 0 ||
(model.notMatch?.length ?? 0) > 0 ||
(model.exist?.length ?? 0) > 0 ||
(model.notExist?.length ?? 0) > 0
);
}
export const PayloadRulesEditor = memo(function PayloadRulesEditor({
value,
disabled,
@@ -465,6 +477,31 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
const { t } = useTranslation();
const rules = value;
const protocolOptions = useMemo(() => buildProtocolOptions(t, rules), [rules, t]);
const fromProtocolOptions = useMemo(
() => [
{
value: '',
label: t('config_management.visual.payload_rules.provider_default'),
},
{
value: 'openai',
label: t('config_management.visual.payload_rules.provider_openai'),
},
{
value: 'responses',
label: t('config_management.visual.payload_rules.provider_responses'),
},
{
value: 'gemini',
label: t('config_management.visual.payload_rules.provider_gemini'),
},
{
value: 'claude',
label: t('config_management.visual.payload_rules.provider_claude'),
},
],
[t]
);
const payloadValueTypeOptions = useMemo(
() =>
VISUAL_CONFIG_PAYLOAD_VALUE_TYPE_OPTIONS.map((option) => ({
@@ -480,6 +517,7 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
],
[t]
);
const [modelAdvancedOverrides, setModelAdvancedOverrides] = useState<Record<string, boolean>>({});
const addRule = () => onChange([...rules, { id: makeClientId(), models: [], params: [] }]);
const removeRule = (ruleIndex: number) => onChange(rules.filter((_, i) => i !== ruleIndex));
@@ -509,6 +547,79 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
});
};
const toggleModelAdvanced = (modelId: string, defaultExpanded: boolean) => {
setModelAdvancedOverrides((current) => ({
...current,
[modelId]: !(current[modelId] ?? defaultExpanded),
}));
};
const addHeader = (ruleIndex: number, modelIndex: number) => {
const rule = rules[ruleIndex];
const model = rule.models[modelIndex];
updateModel(ruleIndex, modelIndex, {
headers: [...(model.headers ?? []), { id: makeClientId(), name: '', value: '' }],
});
};
const updateHeader = (
ruleIndex: number,
modelIndex: number,
headerIndex: number,
patch: Partial<PayloadHeaderEntry>
) => {
const model = rules[ruleIndex].models[modelIndex];
updateModel(ruleIndex, modelIndex, {
headers: (model.headers ?? []).map((header, i) =>
i === headerIndex ? { ...header, ...patch } : header
),
});
};
const removeHeader = (ruleIndex: number, modelIndex: number, headerIndex: number) => {
const model = rules[ruleIndex].models[modelIndex];
updateModel(ruleIndex, modelIndex, {
headers: (model.headers ?? []).filter((_, i) => i !== headerIndex),
});
};
const addCondition = (ruleIndex: number, modelIndex: number, key: 'match' | 'notMatch') => {
const model = rules[ruleIndex].models[modelIndex];
updateModel(ruleIndex, modelIndex, {
[key]: [
...(model[key] ?? []),
{ id: makeClientId(), path: '', valueType: 'string', value: '' },
],
});
};
const updateCondition = (
ruleIndex: number,
modelIndex: number,
key: 'match' | 'notMatch',
conditionIndex: number,
patch: Partial<PayloadParamEntry>
) => {
const model = rules[ruleIndex].models[modelIndex];
updateModel(ruleIndex, modelIndex, {
[key]: (model[key] ?? []).map((condition, i) =>
i === conditionIndex ? { ...condition, ...patch } : condition
),
});
};
const removeCondition = (
ruleIndex: number,
modelIndex: number,
key: 'match' | 'notMatch',
conditionIndex: number
) => {
const model = rules[ruleIndex].models[modelIndex];
updateModel(ruleIndex, modelIndex, {
[key]: (model[key] ?? []).filter((_, i) => i !== conditionIndex),
});
};
const addParam = (ruleIndex: number) => {
const rule = rules[ruleIndex];
const nextParam: PayloadParamEntry = {
@@ -558,6 +669,62 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
return getValidationMessage(t, errorCode);
};
const renderConditionValueEditor = (
ruleIndex: number,
modelIndex: number,
key: 'match' | 'notMatch',
conditionIndex: number,
condition: PayloadParamEntry
) => {
if (condition.valueType === 'boolean') {
return (
<Select
value={
condition.value.toLowerCase() === 'true' || condition.value.toLowerCase() === 'false'
? condition.value.toLowerCase()
: ''
}
options={booleanValueOptions}
placeholder={t('config_management.visual.payload_rules.value_boolean')}
disabled={disabled}
ariaLabel={t('config_management.visual.payload_rules.condition_value')}
onChange={(nextValue) =>
updateCondition(ruleIndex, modelIndex, key, conditionIndex, { value: nextValue })
}
/>
);
}
if (condition.valueType === 'json') {
return (
<textarea
className={`input ${styles.payloadJsonInput}`}
placeholder={getValuePlaceholder(condition.valueType)}
aria-label={t('config_management.visual.payload_rules.condition_value')}
value={condition.value}
onChange={(e) =>
updateCondition(ruleIndex, modelIndex, key, conditionIndex, {
value: e.target.value,
})
}
disabled={disabled}
/>
);
}
return (
<ExpandableInput
placeholder={getValuePlaceholder(condition.valueType)}
ariaLabel={t('config_management.visual.payload_rules.condition_value')}
value={condition.value}
onChange={(nextValue) =>
updateCondition(ruleIndex, modelIndex, key, conditionIndex, { value: nextValue })
}
disabled={disabled}
/>
);
};
const renderParamValueEditor = (
ruleIndex: number,
paramIndex: number,
@@ -641,70 +808,303 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
<div className={styles.blockLabel}>
{t('config_management.visual.payload_rules.models')}
</div>
{(rule.models.length ? rule.models : []).map((model, modelIndex) => (
<div
key={model.id}
className={[
styles.payloadRuleModelRow,
protocolFirst ? styles.payloadRuleModelRowProtocolFirst : '',
]
.filter(Boolean)
.join(' ')}
>
{protocolFirst ? (
<>
<Select
value={model.protocol ?? ''}
options={protocolOptions}
{(rule.models.length ? rule.models : []).map((model, modelIndex) => {
const hasAdvancedSettings = hasPayloadModelAdvancedSettings(model);
const advancedExpanded = modelAdvancedOverrides[model.id] ?? hasAdvancedSettings;
return (
<div key={model.id} className={styles.payloadModelGroup}>
<div
className={[
styles.payloadRuleModelRow,
protocolFirst ? styles.payloadRuleModelRowProtocolFirst : '',
]
.filter(Boolean)
.join(' ')}
>
{protocolFirst ? (
<>
<Select
value={model.protocol ?? ''}
options={protocolOptions}
disabled={disabled}
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
onChange={(nextValue) =>
updateModel(ruleIndex, modelIndex, {
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
})
}
/>
<ExpandableInput
placeholder={t('config_management.visual.payload_rules.model_name')}
ariaLabel={t('config_management.visual.payload_rules.model_name')}
value={model.name}
onChange={(nextValue) =>
updateModel(ruleIndex, modelIndex, { name: nextValue })
}
disabled={disabled}
/>
</>
) : (
<>
<ExpandableInput
placeholder={t('config_management.visual.payload_rules.model_name')}
ariaLabel={t('config_management.visual.payload_rules.model_name')}
value={model.name}
onChange={(nextValue) =>
updateModel(ruleIndex, modelIndex, { name: nextValue })
}
disabled={disabled}
/>
<Select
value={model.protocol ?? ''}
options={protocolOptions}
disabled={disabled}
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
onChange={(nextValue) =>
updateModel(ruleIndex, modelIndex, {
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
})
}
/>
</>
)}
<Button
variant="secondary"
size="sm"
className={styles.payloadRowActionButton}
onClick={() => toggleModelAdvanced(model.id, hasAdvancedSettings)}
disabled={disabled}
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
onChange={(nextValue) =>
updateModel(ruleIndex, modelIndex, {
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
})
}
/>
<ExpandableInput
placeholder={t('config_management.visual.payload_rules.model_name')}
ariaLabel={t('config_management.visual.payload_rules.model_name')}
value={model.name}
onChange={(nextValue) => updateModel(ruleIndex, modelIndex, { name: nextValue })}
>
{advancedExpanded
? t('config_management.visual.payload_rules.hide_advanced')
: t('config_management.visual.payload_rules.advanced')}
</Button>
<Button
variant="ghost"
size="sm"
className={styles.payloadRowActionButton}
onClick={() => removeModel(ruleIndex, modelIndex)}
disabled={disabled}
/>
</>
) : (
<>
<ExpandableInput
placeholder={t('config_management.visual.payload_rules.model_name')}
ariaLabel={t('config_management.visual.payload_rules.model_name')}
value={model.name}
onChange={(nextValue) => updateModel(ruleIndex, modelIndex, { name: nextValue })}
disabled={disabled}
/>
<Select
value={model.protocol ?? ''}
options={protocolOptions}
disabled={disabled}
ariaLabel={t('config_management.visual.payload_rules.provider_type')}
onChange={(nextValue) =>
updateModel(ruleIndex, modelIndex, {
protocol: (nextValue || undefined) as PayloadModelEntry['protocol'],
})
}
/>
</>
)}
<Button
variant="ghost"
size="sm"
className={styles.payloadRowActionButton}
onClick={() => removeModel(ruleIndex, modelIndex)}
disabled={disabled}
>
{t('config_management.visual.common.delete')}
</Button>
</div>
))}
>
{t('config_management.visual.common.delete')}
</Button>
</div>
{advancedExpanded ? (
<div className={styles.payloadModelAdvanced}>
<div className={styles.payloadAdvancedGrid}>
<div className={styles.fieldShell}>
<label className={styles.fieldLabel}>
{t('config_management.visual.payload_rules.from_protocol')}
</label>
<Select
value={model.fromProtocol ?? ''}
options={fromProtocolOptions}
disabled={disabled}
ariaLabel={t('config_management.visual.payload_rules.from_protocol')}
onChange={(nextValue) =>
updateModel(ruleIndex, modelIndex, {
fromProtocol: (nextValue ||
undefined) as PayloadModelEntry['fromProtocol'],
})
}
/>
</div>
</div>
<div className={styles.blockStack}>
<div className={styles.blockLabel}>
{t('config_management.visual.payload_rules.headers')}
</div>
{(model.headers ?? []).map((header, headerIndex) => (
<div key={header.id} className={styles.payloadHeaderRow}>
<ExpandableInput
placeholder={t('config_management.visual.payload_rules.header_name')}
ariaLabel={t('config_management.visual.payload_rules.header_name')}
value={header.name}
onChange={(nextValue) =>
updateHeader(ruleIndex, modelIndex, headerIndex, {
name: nextValue,
})
}
disabled={disabled}
/>
<ExpandableInput
placeholder={t('config_management.visual.payload_rules.header_value')}
ariaLabel={t('config_management.visual.payload_rules.header_value')}
value={header.value}
onChange={(nextValue) =>
updateHeader(ruleIndex, modelIndex, headerIndex, {
value: nextValue,
})
}
disabled={disabled}
/>
<Button
variant="ghost"
size="sm"
className={styles.payloadRowActionButton}
onClick={() => removeHeader(ruleIndex, modelIndex, headerIndex)}
disabled={disabled}
>
{t('config_management.visual.common.delete')}
</Button>
</div>
))}
<div className={styles.actionRow}>
<Button
variant="secondary"
size="sm"
onClick={() => addHeader(ruleIndex, modelIndex)}
disabled={disabled}
>
{t('config_management.visual.payload_rules.add_header')}
</Button>
</div>
</div>
{(['match', 'notMatch'] as const).map((conditionKey) => (
<div key={conditionKey} className={styles.blockStack}>
<div className={styles.blockLabel}>
{t(`config_management.visual.payload_rules.${conditionKey}`)}
</div>
{(model[conditionKey] ?? []).map((condition, conditionIndex) => {
const conditionError = getValidationMessage(
t,
getPayloadParamValidationError(condition)
);
return (
<div key={condition.id} className={styles.payloadRuleParamGroup}>
<div className={styles.payloadRuleParamRow}>
<ExpandableInput
placeholder={t(
'config_management.visual.payload_rules.condition_path'
)}
ariaLabel={t(
'config_management.visual.payload_rules.condition_path'
)}
value={condition.path}
onChange={(nextValue) =>
updateCondition(
ruleIndex,
modelIndex,
conditionKey,
conditionIndex,
{ path: nextValue }
)
}
disabled={disabled}
/>
<Select
value={condition.valueType}
options={payloadValueTypeOptions}
disabled={disabled}
ariaLabel={t(
'config_management.visual.payload_rules.param_type'
)}
onChange={(nextValue) =>
updateCondition(
ruleIndex,
modelIndex,
conditionKey,
conditionIndex,
{
valueType: nextValue as PayloadParamValueType,
value:
nextValue === 'boolean'
? 'true'
: nextValue === 'json' &&
condition.value.trim() === ''
? '{}'
: condition.value,
}
)
}
/>
{renderConditionValueEditor(
ruleIndex,
modelIndex,
conditionKey,
conditionIndex,
condition
)}
<Button
variant="ghost"
size="sm"
className={styles.payloadRowActionButton}
onClick={() =>
removeCondition(
ruleIndex,
modelIndex,
conditionKey,
conditionIndex
)
}
disabled={disabled}
>
{t('config_management.visual.common.delete')}
</Button>
</div>
{conditionError ? (
<div className={`error-box ${styles.payloadParamError}`}>
{conditionError}
</div>
) : null}
</div>
);
})}
<div className={styles.actionRow}>
<Button
variant="secondary"
size="sm"
onClick={() => addCondition(ruleIndex, modelIndex, conditionKey)}
disabled={disabled}
>
{t('config_management.visual.payload_rules.add_condition')}
</Button>
</div>
</div>
))}
<div className={styles.payloadAdvancedGrid}>
<div className={styles.blockStack}>
<div className={styles.blockLabel}>
{t('config_management.visual.payload_rules.exist')}
</div>
<StringListEditor
value={model.exist ?? []}
disabled={disabled}
placeholder={t('config_management.visual.payload_rules.condition_path')}
inputAriaLabel={t(
'config_management.visual.payload_rules.condition_path'
)}
onChange={(exist) => updateModel(ruleIndex, modelIndex, { exist })}
/>
</div>
<div className={styles.blockStack}>
<div className={styles.blockLabel}>
{t('config_management.visual.payload_rules.notExist')}
</div>
<StringListEditor
value={model.notExist ?? []}
disabled={disabled}
placeholder={t('config_management.visual.payload_rules.condition_path')}
inputAriaLabel={t(
'config_management.visual.payload_rules.condition_path'
)}
onChange={(notExist) =>
updateModel(ruleIndex, modelIndex, { notExist })
}
/>
</div>
</div>
</div>
) : null}
</div>
);
})}
<div className={styles.actionRow}>
<Button
variant="secondary"
@@ -731,7 +1131,9 @@ export const PayloadRulesEditor = memo(function PayloadRulesEditor({
placeholder={t('config_management.visual.payload_rules.json_path')}
ariaLabel={t('config_management.visual.payload_rules.json_path')}
value={param.path}
onChange={(nextValue) => updateParam(ruleIndex, paramIndex, { path: nextValue })}
onChange={(nextValue) =>
updateParam(ruleIndex, paramIndex, { path: nextValue })
}
disabled={disabled}
/>
{rawJsonValues ? null : (
@@ -28,6 +28,7 @@ import {
getTypeColor,
getTypeLabel,
isRuntimeOnlyAuthFile,
normalizeProviderKey,
parsePriorityValue,
type QuotaProviderType,
type ResolvedTheme,
@@ -88,11 +89,12 @@ export function AuthFileCard(props: AuthFileCardProps) {
failure: normalizeUsageTotal(file.failed),
};
const isRuntimeOnly = isRuntimeOnlyAuthFile(file);
const isAistudio = (file.type || '').toLowerCase() === 'aistudio';
const providerKey = normalizeProviderKey(String(file.type ?? file.provider ?? 'unknown'));
const isAistudio = providerKey === 'aistudio';
const showModelsButton = !isRuntimeOnly || isAistudio;
const typeColor = getTypeColor(file.type || 'unknown', resolvedTheme);
const typeLabel = getTypeLabel(t, file.type || 'unknown');
const providerIcon = getAuthFileIcon(file.type || 'unknown', resolvedTheme);
const typeColor = getTypeColor(providerKey, resolvedTheme);
const typeLabel = getTypeLabel(t, providerKey);
const providerIcon = getAuthFileIcon(providerKey, resolvedTheme);
const quotaType =
quotaFilterType && resolveQuotaType(file) === quotaFilterType ? quotaFilterType : null;
@@ -34,6 +34,7 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
}
};
const previewText = formatJsonText(updatedText);
const invalidContentPreview = editor?.invalidContentPreview ?? '';
return (
<Modal
@@ -100,60 +101,70 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
</div>
<div className={styles.prefixProxyJsonWrapper}>
<label className={styles.prefixProxyLabel}>
{t('auth_files.prefix_proxy_source_label')}
{editor.json
? t('auth_files.prefix_proxy_source_label')
: t('auth_files.prefix_proxy_invalid_content_label')}
</label>
<textarea
className={styles.prefixProxyTextarea}
rows={10}
readOnly
value={previewText}
/>
</div>
<div className={styles.prefixProxyFields}>
<Input
label={t('auth_files.prefix_label')}
value={editor.prefix}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('prefix', e.target.value)}
/>
<Input
label={t('auth_files.proxy_url_label')}
value={editor.proxyUrl}
placeholder={t('auth_files.proxy_url_placeholder')}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('proxyUrl', e.target.value)}
/>
<Input
label={t('auth_files.priority_label')}
value={editor.priority}
placeholder={t('auth_files.priority_placeholder')}
hint={t('auth_files.priority_hint')}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('priority', e.target.value)}
/>
<div className="form-group">
<label>{t('auth_files.headers_label')}</label>
{editor.json ? (
<textarea
className={`input ${editor.headersError ? styles.prefixProxyTextareaInvalid : ''}`}
value={editor.headersText}
placeholder={t('auth_files.headers_placeholder')}
rows={4}
aria-invalid={Boolean(editor.headersError)}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('headersText', e.target.value)}
className={styles.prefixProxyTextarea}
rows={10}
readOnly
value={previewText}
/>
{editor.headersError && <div className="error-box">{editor.headersError}</div>}
<div className="hint">{t('auth_files.headers_hint')}</div>
</div>
<Input
label={t('auth_files.note_label')}
value={editor.note}
placeholder={t('auth_files.note_placeholder')}
hint={t('auth_files.note_hint')}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('note', e.target.value)}
/>
) : (
<pre className={styles.prefixProxyInvalidContentPreview}>
{invalidContentPreview}
</pre>
)}
</div>
{editor.json && (
<div className={styles.prefixProxyFields}>
<Input
label={t('auth_files.prefix_label')}
value={editor.prefix}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('prefix', e.target.value)}
/>
<Input
label={t('auth_files.proxy_url_label')}
value={editor.proxyUrl}
placeholder={t('auth_files.proxy_url_placeholder')}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('proxyUrl', e.target.value)}
/>
<Input
label={t('auth_files.priority_label')}
value={editor.priority}
placeholder={t('auth_files.priority_placeholder')}
hint={t('auth_files.priority_hint')}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('priority', e.target.value)}
/>
<div className="form-group">
<label>{t('auth_files.headers_label')}</label>
<textarea
className={`input ${editor.headersError ? styles.prefixProxyTextareaInvalid : ''}`}
value={editor.headersText}
placeholder={t('auth_files.headers_placeholder')}
rows={4}
aria-invalid={Boolean(editor.headersError)}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('headersText', e.target.value)}
/>
{editor.headersError && <div className="error-box">{editor.headersError}</div>}
<div className="hint">{t('auth_files.headers_hint')}</div>
</div>
<Input
label={t('auth_files.note_label')}
value={editor.note}
placeholder={t('auth_files.note_placeholder')}
hint={t('auth_files.note_hint')}
disabled={disableControls || editor.saving || !editor.json}
onChange={(e) => onChange('note', e.target.value)}
/>
</div>
)}
</>
)}
</div>
+17 -4
View File
@@ -3,6 +3,8 @@ import iconAntigravity from '@/assets/icons/antigravity.svg';
import iconClaude from '@/assets/icons/claude.svg';
import iconCodex from '@/assets/icons/codex.svg';
import iconGemini from '@/assets/icons/gemini.svg';
import iconGrok from '@/assets/icons/grok.svg';
import iconGrokDark from '@/assets/icons/grok-dark.svg';
import iconIflow from '@/assets/icons/iflow.svg';
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
import iconKimiLight from '@/assets/icons/kimi-light.svg';
@@ -82,6 +84,11 @@ export const TYPE_COLORS: Record<string, TypeColorSet> = {
light: { bg: '#e0f7fa', text: '#006064' },
dark: { bg: '#004d40', text: '#80deea' },
},
// xAI / Grok: graphite brand treatment, distinct from blue and purple providers
xai: {
light: { bg: '#f3f4f6', text: '#111827', border: '1px solid #d1d5db' },
dark: { bg: '#111827', text: '#f9fafb', border: '1px solid #374151' },
},
// iFlow logo: 品红紫渐变 #5C5CFF → #AE5CFF,偏品红以区别于 Qwen 的紫罗兰
iflow: {
light: { bg: '#f5e3fc', text: '#9025c8' },
@@ -109,6 +116,7 @@ export const AUTH_FILE_ICONS: Record<string, AuthFileIconAsset> = {
codex: iconCodex,
gemini: iconGemini,
'gemini-cli': iconGemini,
xai: { light: iconGrok, dark: iconGrokDark },
iflow: iconIflow,
kimi: { light: iconKimiLight, dark: iconKimiDark },
qwen: iconQwen,
@@ -128,7 +136,11 @@ export const resolveQuotaErrorMessage = (
return fallback;
};
export const normalizeProviderKey = (value: string) => value.trim().toLowerCase();
export const normalizeProviderKey = (value: string) => {
const key = value.trim().toLowerCase().replace(/_/g, '-');
if (key === 'x-ai' || key === 'grok') return 'xai';
return key;
};
export const getAuthFileStatusMessage = (file: AuthFileItem): string => {
const raw = file['status_message'] ?? file.statusMessage;
@@ -141,15 +153,16 @@ export const hasAuthFileStatusMessage = (file: AuthFileItem): boolean =>
getAuthFileStatusMessage(file).length > 0;
export const getTypeLabel = (t: TFunction, type: string): string => {
const key = `auth_files.filter_${type}`;
const providerKey = normalizeProviderKey(type);
const key = `auth_files.filter_${providerKey}`;
const translated = t(key);
if (translated !== key) return translated;
if (type.toLowerCase() === 'iflow') return 'iFlow';
if (providerKey === 'iflow') return 'iFlow';
return type.charAt(0).toUpperCase() + type.slice(1);
};
export const getTypeColor = (type: string, resolvedTheme: ResolvedTheme): ThemeColors => {
const set = TYPE_COLORS[type] || TYPE_COLORS.unknown;
const set = TYPE_COLORS[normalizeProviderKey(type)] || TYPE_COLORS.unknown;
return resolvedTheme === 'dark' && set.dark ? set.dark : set.light;
};
@@ -11,6 +11,7 @@ import {
getTypeLabel,
hasAuthFileStatusMessage,
isRuntimeOnlyAuthFile,
normalizeProviderKey,
} from '@/features/authFiles/constants';
type DeleteAllOptions = {
@@ -310,7 +311,12 @@ export function useAuthFilesData(): UseAuthFilesDataResult {
} else {
const filesToDelete = files.filter((file) => {
if (isRuntimeOnlyAuthFile(file)) return false;
if (isFiltered && file.type !== filter) return false;
if (
isFiltered &&
normalizeProviderKey(String(file.type ?? file.provider ?? '')) !== filter
) {
return false;
}
if (isProblemOnly && !hasAuthFileStatusMessage(file)) return false;
if (isDisabledOnly && file.disabled !== true) return false;
return true;
@@ -10,6 +10,9 @@ type AuthFileHeadersErrorKey =
| 'auth_files.headers_invalid_json'
| 'auth_files.headers_invalid_object'
| 'auth_files.headers_invalid_value';
type AuthFileContentErrorKey =
| 'auth_files.prefix_proxy_invalid_json'
| 'auth_files.prefix_proxy_html_challenge';
export type PrefixProxyEditorField = 'prefix' | 'proxyUrl' | 'priority' | 'note' | 'headersText';
@@ -23,6 +26,7 @@ export type PrefixProxyEditorState = {
error: string | null;
originalText: string;
rawText: string;
invalidContentPreview: string;
json: Record<string, unknown> | null;
prefix: string;
proxyUrl: string;
@@ -90,6 +94,47 @@ const parseHeadersText = (
const normalizeTextField = (value: unknown): string =>
typeof value === 'string' ? value.trim() : '';
const INVALID_CONTENT_PREVIEW_LIMIT = 1000;
const buildInvalidContentPreview = (text: string): string => {
const trimmed = text.trim();
if (!trimmed) return '';
if (trimmed.length <= INVALID_CONTENT_PREVIEW_LIMIT) return trimmed;
return `${trimmed.slice(0, INVALID_CONTENT_PREVIEW_LIMIT)}\n...`;
};
const buildInvalidAuthFileContentState = (
text: string,
resolveError: (key: AuthFileContentErrorKey) => string
): Pick<
PrefixProxyEditorState,
'loading' | 'error' | 'rawText' | 'originalText' | 'invalidContentPreview'
> => ({
loading: false,
error: resolveError(getAuthFileContentErrorKey(text)),
rawText: text,
originalText: text,
invalidContentPreview: buildInvalidContentPreview(text),
});
const getAuthFileContentErrorKey = (text: string): AuthFileContentErrorKey => {
const head = text.trimStart().slice(0, 4096).toLowerCase();
const looksLikeHtml =
head.startsWith('<!doctype html') ||
head.startsWith('<html') ||
head.includes('<head') ||
head.includes('<body');
const looksLikeChallenge =
head.includes('cf_chl') ||
head.includes('__cf_chl_tk') ||
head.includes('challenge-platform') ||
head.includes('cloudflare');
return looksLikeHtml || looksLikeChallenge
? 'auth_files.prefix_proxy_html_challenge'
: 'auth_files.prefix_proxy_invalid_json';
};
const hasKeys = (value: Record<string, unknown> | AuthFileFieldsPatch | null): boolean =>
Boolean(value && Object.keys(value).length > 0);
@@ -270,7 +315,7 @@ export function useAuthFilesPrefixProxyEditor(
prefixProxyEditor?.headersTouched && prefixProxyEditor.headersError
);
const prefixProxyUpdatedText =
prefixProxyEditor?.json && !hasBlockingValidationError
prefixProxyEditor && !hasBlockingValidationError
? buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key))
: '';
@@ -302,6 +347,7 @@ export function useAuthFilesPrefixProxyEditor(
error: null,
originalText: '',
rawText: '',
invalidContentPreview: '',
json: null,
prefix: '',
proxyUrl: '',
@@ -325,10 +371,7 @@ export function useAuthFilesPrefixProxyEditor(
if (!prev || prev.fileName !== name) return prev;
return {
...prev,
loading: false,
error: t('auth_files.prefix_proxy_invalid_json'),
rawText: trimmed,
originalText: trimmed,
...buildInvalidAuthFileContentState(rawText, (key) => t(key)),
};
});
return;
@@ -339,10 +382,7 @@ export function useAuthFilesPrefixProxyEditor(
if (!prev || prev.fileName !== name) return prev;
return {
...prev,
loading: false,
error: t('auth_files.prefix_proxy_invalid_json'),
rawText: trimmed,
originalText: trimmed,
...buildInvalidAuthFileContentState(rawText, (key) => t(key)),
};
});
return;
@@ -370,6 +410,7 @@ export function useAuthFilesPrefixProxyEditor(
loading: false,
originalText,
rawText: originalText,
invalidContentPreview: '',
json,
prefix,
proxyUrl,
+401 -87
View File
@@ -1,7 +1,9 @@
import { useCallback, useMemo, useReducer } from 'react';
import { isMap, parse as parseYaml, parseDocument } from 'yaml';
import type {
DisableImageGenerationMode,
PayloadFilterRule,
PayloadHeaderEntry,
PayloadParamEntry,
PayloadParamValueType,
PayloadRule,
@@ -137,6 +139,24 @@ function setIntFromStringInDoc(doc: YamlDocument, path: YamlPath, value: unknown
}
}
function setDisableImageGenerationInDoc(
doc: YamlDocument,
path: YamlPath,
value: DisableImageGenerationMode
): void {
if (value === 'chat') {
doc.setIn(path, 'chat');
return;
}
if (value === 'true') {
doc.setIn(path, true);
return;
}
if (docHas(doc, path)) doc.setIn(path, false);
}
function getNonNegativeIntegerError(value: string): 'non_negative_integer' | undefined {
const trimmed = value.trim();
if (!trimmed) return undefined;
@@ -157,10 +177,15 @@ export function getVisualConfigValidationErrors(
): VisualConfigValidationErrors {
return {
port: getPortError(values.port),
errorLogsMaxFiles: getNonNegativeIntegerError(values.errorLogsMaxFiles),
logsMaxTotalSizeMb: getNonNegativeIntegerError(values.logsMaxTotalSizeMb),
redisUsageQueueRetentionSeconds: getNonNegativeIntegerError(
values.redisUsageQueueRetentionSeconds
),
requestRetry: getNonNegativeIntegerError(values.requestRetry),
maxRetryCredentials: getNonNegativeIntegerError(values.maxRetryCredentials),
maxRetryInterval: getNonNegativeIntegerError(values.maxRetryInterval),
authAutoRefreshWorkers: getNonNegativeIntegerError(values.authAutoRefreshWorkers),
'streaming.keepaliveSeconds': getNonNegativeIntegerError(values.streaming.keepaliveSeconds),
'streaming.bootstrapRetries': getNonNegativeIntegerError(values.streaming.bootstrapRetries),
'streaming.nonstreamKeepaliveInterval': getNonNegativeIntegerError(
@@ -201,8 +226,14 @@ export function getPayloadParamValidationError(
}
function hasPayloadParamValidationErrors(rules: PayloadRule[]): boolean {
return rules.some((rule) =>
rule.params.some((param) => Boolean(getPayloadParamValidationError(param)))
return rules.some(
(rule) =>
rule.params.some((param) => Boolean(getPayloadParamValidationError(param))) ||
rule.models.some(
(model) =>
(model.match ?? []).some((param) => Boolean(getPayloadParamValidationError(param))) ||
(model.notMatch ?? []).some((param) => Boolean(getPayloadParamValidationError(param)))
)
);
}
@@ -221,7 +252,19 @@ function arePayloadModelEntriesEqual(
const a = left[i];
const b = right[i];
if (!a || !b) return false;
if (a.id !== b.id || a.name !== b.name || a.protocol !== b.protocol) return false;
if (
a.id !== b.id ||
a.name !== b.name ||
a.protocol !== b.protocol ||
a.fromProtocol !== b.fromProtocol
) {
return false;
}
if (!arePayloadHeaderEntriesEqual(a.headers, b.headers)) return false;
if (!arePayloadParamEntriesEqual(a.match ?? [], b.match ?? [])) return false;
if (!arePayloadParamEntriesEqual(a.notMatch ?? [], b.notMatch ?? [])) return false;
if (!areStringArraysEqual(a.exist, b.exist)) return false;
if (!areStringArraysEqual(a.notExist, b.notExist)) return false;
}
return true;
}
@@ -243,6 +286,34 @@ function arePayloadParamEntriesEqual(
return true;
}
function arePayloadHeaderEntriesEqual(
left: PayloadHeaderEntry[] | undefined,
right: PayloadHeaderEntry[] | undefined
): boolean {
const leftEntries = left ?? [];
const rightEntries = right ?? [];
if (leftEntries === rightEntries) return true;
if (leftEntries.length !== rightEntries.length) return false;
for (let i = 0; i < leftEntries.length; i += 1) {
const a = leftEntries[i];
const b = rightEntries[i];
if (!a || !b) return false;
if (a.id !== b.id || a.name !== b.name || a.value !== b.value) return false;
}
return true;
}
function areStringArraysEqual(left: string[] | undefined, right: string[] | undefined): boolean {
const leftItems = left ?? [];
const rightItems = right ?? [];
if (leftItems === rightItems) return true;
if (leftItems.length !== rightItems.length) return false;
for (let i = 0; i < leftItems.length; i += 1) {
if (leftItems[i] !== rightItems[i]) return false;
}
return true;
}
function arePayloadRulesEqual(left: PayloadRule[], right: PayloadRule[]): boolean {
if (left === right) return true;
if (left.length !== right.length) return false;
@@ -314,6 +385,63 @@ function parsePayloadProtocol(raw: unknown): string | undefined {
return raw.trim() ? raw : undefined;
}
function parseDisableImageGenerationMode(raw: unknown): DisableImageGenerationMode {
if (raw === true) return 'true';
if (typeof raw === 'string') {
const normalized = raw.trim().toLowerCase();
if (normalized === 'true') return 'true';
if (normalized === 'chat') return 'chat';
}
return 'false';
}
function parsePayloadHeaders(raw: unknown, idPrefix: string): PayloadHeaderEntry[] {
const record = asRecord(raw);
if (!record) return [];
return Object.entries(record).map(([name, value], index) => ({
id: `${idPrefix}-header-${index}`,
name,
value: String(value ?? ''),
}));
}
function parsePayloadConditions(raw: unknown, idPrefix: string): PayloadParamEntry[] {
if (!Array.isArray(raw)) return [];
const entries: PayloadParamEntry[] = [];
raw.forEach((item, itemIndex) => {
const record = asRecord(item);
if (!record) {
if (typeof item === 'string') {
entries.push({
id: `${idPrefix}-condition-${itemIndex}-0`,
path: item,
valueType: 'string',
value: '',
});
}
return;
}
Object.entries(record).forEach(([path, value], valueIndex) => {
const parsedValue = parsePayloadParamValue(value);
entries.push({
id: `${idPrefix}-condition-${itemIndex}-${valueIndex}`,
path,
valueType: parsedValue.valueType,
value: parsedValue.value,
});
});
});
return entries;
}
function parseStringList(raw: unknown): string[] {
return Array.isArray(raw) ? raw.map((item) => String(item ?? '').trim()).filter(Boolean) : [];
}
function deleteLegacyApiKeysProvider(doc: YamlDocument): void {
if (docHas(doc, ['auth', 'providers', 'config-api-key', 'api-key-entries'])) {
doc.deleteIn(['auth', 'providers', 'config-api-key', 'api-key-entries']);
@@ -326,26 +454,37 @@ function deleteLegacyApiKeysProvider(doc: YamlDocument): void {
deleteIfMapEmpty(doc, ['auth']);
}
function parsePayloadModelEntries(raw: unknown, idPrefix: string): PayloadRule['models'] {
if (!Array.isArray(raw)) return [];
return raw.map((model, modelIndex) => {
const modelRecord = asRecord(model);
const nameRaw =
typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
const modelId = `${idPrefix}-${modelIndex}`;
return {
id: modelId,
name,
protocol: parsePayloadProtocol(modelRecord?.protocol),
fromProtocol: parsePayloadProtocol(modelRecord?.['from-protocol']),
headers: parsePayloadHeaders(modelRecord?.headers, modelId),
match: parsePayloadConditions(modelRecord?.match, `${modelId}-match`),
notMatch: parsePayloadConditions(modelRecord?.['not-match'], `${modelId}-not-match`),
exist: parseStringList(modelRecord?.exist),
notExist: parseStringList(modelRecord?.['not-exist']),
};
});
}
function parsePayloadRules(rules: unknown): PayloadRule[] {
if (!Array.isArray(rules)) return [];
return rules.map((rule, index) => {
const record = asRecord(rule) ?? {};
const modelsRaw = record.models;
const models = Array.isArray(modelsRaw)
? modelsRaw.map((model, modelIndex) => {
const modelRecord = asRecord(model);
const nameRaw =
typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
return {
id: `model-${index}-${modelIndex}`,
name,
protocol: parsePayloadProtocol(modelRecord?.protocol),
};
})
: [];
const models = parsePayloadModelEntries(record.models, `model-${index}`);
const paramsRecord = asRecord(record.params);
const params = paramsRecord
@@ -370,20 +509,7 @@ function parsePayloadFilterRules(rules: unknown): PayloadFilterRule[] {
return rules.map((rule, index) => {
const record = asRecord(rule) ?? {};
const modelsRaw = record.models;
const models = Array.isArray(modelsRaw)
? modelsRaw.map((model, modelIndex) => {
const modelRecord = asRecord(model);
const nameRaw =
typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
return {
id: `filter-model-${index}-${modelIndex}`,
name,
protocol: parsePayloadProtocol(modelRecord?.protocol),
};
})
: [];
const models = parsePayloadModelEntries(record.models, `filter-model-${index}`);
const paramsRaw = record.params;
const params = Array.isArray(paramsRaw) ? paramsRaw.map(String) : [];
@@ -398,20 +524,7 @@ function parseRawPayloadRules(rules: unknown): PayloadRule[] {
return rules.map((rule, index) => {
const record = asRecord(rule) ?? {};
const modelsRaw = record.models;
const models = Array.isArray(modelsRaw)
? modelsRaw.map((model, modelIndex) => {
const modelRecord = asRecord(model);
const nameRaw =
typeof model === 'string' ? model : (modelRecord?.name ?? modelRecord?.id ?? '');
const name = typeof nameRaw === 'string' ? nameRaw : String(nameRaw ?? '');
return {
id: `raw-model-${index}-${modelIndex}`,
name,
protocol: parsePayloadProtocol(modelRecord?.protocol),
};
})
: [];
const models = parsePayloadModelEntries(record.models, `raw-model-${index}`);
const paramsRecord = asRecord(record.params);
const params = paramsRecord
@@ -427,34 +540,88 @@ function parseRawPayloadRules(rules: unknown): PayloadRule[] {
});
}
function serializePayloadParamEntryValue(param: PayloadParamEntry): unknown {
if (param.valueType === 'number') {
const num = Number(param.value);
return Number.isFinite(num) ? num : param.value;
}
if (param.valueType === 'boolean') {
return param.value === 'true';
}
if (param.valueType === 'json') {
try {
return JSON.parse(param.value);
} catch {
return param.value;
}
}
return param.value;
}
function serializePayloadHeadersForYaml(headers?: PayloadHeaderEntry[]): Record<string, string> {
const result: Record<string, string> = {};
for (const header of headers ?? []) {
const name = header.name.trim();
if (!name) continue;
result[name] = header.value;
}
return result;
}
function serializePayloadConditionsForYaml(
conditions?: PayloadParamEntry[]
): Array<Record<string, unknown>> {
const result: Array<Record<string, unknown>> = [];
for (const condition of conditions ?? []) {
const path = condition.path.trim();
if (!path) continue;
result.push({ [path]: serializePayloadParamEntryValue(condition) });
}
return result;
}
function serializeStringListForYaml(items?: string[]): string[] {
return (items ?? []).map((item) => item.trim()).filter(Boolean);
}
function serializePayloadModelsForYaml(
models: PayloadRule['models']
): Array<Record<string, unknown>> {
return (models || [])
.filter((m) => m.name?.trim())
.map((m) => {
const obj: Record<string, unknown> = { name: m.name.trim() };
if (m.protocol) obj.protocol = m.protocol;
if (m.fromProtocol) obj['from-protocol'] = m.fromProtocol;
const headers = serializePayloadHeadersForYaml(m.headers);
if (Object.keys(headers).length) obj.headers = headers;
const match = serializePayloadConditionsForYaml(m.match);
if (match.length) obj.match = match;
const notMatch = serializePayloadConditionsForYaml(m.notMatch);
if (notMatch.length) obj['not-match'] = notMatch;
const exist = serializeStringListForYaml(m.exist);
if (exist.length) obj.exist = exist;
const notExist = serializeStringListForYaml(m.notExist);
if (notExist.length) obj['not-exist'] = notExist;
return obj;
});
}
function serializePayloadRulesForYaml(rules: PayloadRule[]): Array<Record<string, unknown>> {
return rules
.map((rule) => {
const models = (rule.models || [])
.filter((m) => m.name?.trim())
.map((m) => {
const obj: Record<string, unknown> = { name: m.name.trim() };
if (m.protocol) obj.protocol = m.protocol;
return obj;
});
const models = serializePayloadModelsForYaml(rule.models);
const params: Record<string, unknown> = {};
for (const param of rule.params || []) {
if (!param.path?.trim()) continue;
let value: unknown = param.value;
if (param.valueType === 'number') {
const num = Number(param.value);
value = Number.isFinite(num) ? num : param.value;
} else if (param.valueType === 'boolean') {
value = param.value === 'true';
} else if (param.valueType === 'json') {
try {
value = JSON.parse(param.value);
} catch {
value = param.value;
}
}
params[param.path.trim()] = value;
params[param.path.trim()] = serializePayloadParamEntryValue(param);
}
return { models, params };
@@ -467,13 +634,7 @@ function serializePayloadFilterRulesForYaml(
): Array<Record<string, unknown>> {
return rules
.map((rule) => {
const models = (rule.models || [])
.filter((m) => m.name?.trim())
.map((m) => {
const obj: Record<string, unknown> = { name: m.name.trim() };
if (m.protocol) obj.protocol = m.protocol;
return obj;
});
const models = serializePayloadModelsForYaml(rule.models);
const params = (Array.isArray(rule.params) ? rule.params : [])
.map((path) => String(path).trim())
@@ -487,13 +648,7 @@ function serializePayloadFilterRulesForYaml(
function serializeRawPayloadRulesForYaml(rules: PayloadRule[]): Array<Record<string, unknown>> {
return rules
.map((rule) => {
const models = (rule.models || [])
.filter((m) => m.name?.trim())
.map((m) => {
const obj: Record<string, unknown> = { name: m.name.trim() };
if (m.protocol) obj.protocol = m.protocol;
return obj;
});
const models = serializePayloadModelsForYaml(rule.models);
const params: Record<string, unknown> = {};
for (const param of rule.params || []) {
@@ -562,6 +717,36 @@ function getNextDirtyFields(
nextDirtyFields.add(key);
}
};
const updateScalarDirty = (key: keyof VisualConfigValues) => {
if (Object.prototype.hasOwnProperty.call(patch, key)) {
updateDirty(key, nextValues[key] === baselineValues[key]);
}
};
(
[
'rmDisableAutoUpdatePanel',
'errorLogsMaxFiles',
'usageStatisticsEnabled',
'redisUsageQueueRetentionSeconds',
'passthroughHeaders',
'disableCooling',
'disableImageGeneration',
'authAutoRefreshWorkers',
'enableGeminiCliEndpoint',
'antigravitySignatureCacheEnabled',
'antigravitySignatureBypassStrict',
'claudeHeaderUserAgent',
'claudeHeaderPackageVersion',
'claudeHeaderRuntimeVersion',
'claudeHeaderOs',
'claudeHeaderArch',
'claudeHeaderTimeout',
'claudeHeaderStabilizeDeviceProfile',
'codexHeaderUserAgent',
'codexHeaderBetaFeatures',
] as Array<keyof VisualConfigValues>
).forEach(updateScalarDirty);
if (Object.prototype.hasOwnProperty.call(patch, 'host')) {
updateDirty('host', nextValues.host === baselineValues.host);
@@ -811,6 +996,8 @@ export function useVisualConfig() {
const routing = asRecord(parsed.routing);
const payload = asRecord(parsed.payload);
const streaming = asRecord(parsed.streaming);
const claudeHeaderDefaults = asRecord(parsed['claude-header-defaults']);
const codexHeaderDefaults = asRecord(parsed['codex-header-defaults']);
const newValues: VisualConfigValues = {
host: typeof parsed.host === 'string' ? parsed.host : '',
@@ -826,6 +1013,7 @@ export function useVisualConfig() {
? remoteManagement['secret-key']
: '',
rmDisableControlPanel: Boolean(remoteManagement?.['disable-control-panel']),
rmDisableAutoUpdatePanel: Boolean(remoteManagement?.['disable-auto-update-panel']),
rmPanelRepo:
typeof remoteManagement?.['panel-github-repository'] === 'string'
? remoteManagement['panel-github-repository']
@@ -840,13 +1028,56 @@ export function useVisualConfig() {
commercialMode: Boolean(parsed['commercial-mode']),
loggingToFile: Boolean(parsed['logging-to-file']),
logsMaxTotalSizeMb: String(parsed['logs-max-total-size-mb'] ?? ''),
errorLogsMaxFiles: String(parsed['error-logs-max-files'] ?? ''),
usageStatisticsEnabled: Boolean(parsed['usage-statistics-enabled']),
redisUsageQueueRetentionSeconds: String(
parsed['redis-usage-queue-retention-seconds'] ?? ''
),
proxyUrl: typeof parsed['proxy-url'] === 'string' ? parsed['proxy-url'] : '',
forceModelPrefix: Boolean(parsed['force-model-prefix']),
passthroughHeaders: Boolean(parsed['passthrough-headers']),
requestRetry: String(parsed['request-retry'] ?? ''),
maxRetryCredentials: String(parsed['max-retry-credentials'] ?? ''),
maxRetryInterval: String(parsed['max-retry-interval'] ?? ''),
disableCooling: Boolean(parsed['disable-cooling']),
disableImageGeneration: parseDisableImageGenerationMode(parsed['disable-image-generation']),
authAutoRefreshWorkers: String(parsed['auth-auto-refresh-workers'] ?? ''),
wsAuth: Boolean(parsed['ws-auth']),
enableGeminiCliEndpoint: Boolean(parsed['enable-gemini-cli-endpoint']),
antigravitySignatureCacheEnabled: Boolean(
parsed['antigravity-signature-cache-enabled'] ?? true
),
antigravitySignatureBypassStrict: Boolean(parsed['antigravity-signature-bypass-strict']),
claudeHeaderUserAgent:
typeof claudeHeaderDefaults?.['user-agent'] === 'string'
? claudeHeaderDefaults['user-agent']
: '',
claudeHeaderPackageVersion:
typeof claudeHeaderDefaults?.['package-version'] === 'string'
? claudeHeaderDefaults['package-version']
: '',
claudeHeaderRuntimeVersion:
typeof claudeHeaderDefaults?.['runtime-version'] === 'string'
? claudeHeaderDefaults['runtime-version']
: '',
claudeHeaderOs: typeof claudeHeaderDefaults?.os === 'string' ? claudeHeaderDefaults.os : '',
claudeHeaderArch:
typeof claudeHeaderDefaults?.arch === 'string' ? claudeHeaderDefaults.arch : '',
claudeHeaderTimeout:
typeof claudeHeaderDefaults?.timeout === 'string' ? claudeHeaderDefaults.timeout : '',
claudeHeaderStabilizeDeviceProfile: Boolean(
claudeHeaderDefaults?.['stabilize-device-profile']
),
codexHeaderUserAgent:
typeof codexHeaderDefaults?.['user-agent'] === 'string'
? codexHeaderDefaults['user-agent']
: '',
codexHeaderBetaFeatures:
typeof codexHeaderDefaults?.['beta-features'] === 'string'
? codexHeaderDefaults['beta-features']
: '',
quotaSwitchProject: Boolean(quotaExceeded?.['switch-project'] ?? true),
quotaSwitchPreviewModel: Boolean(quotaExceeded?.['switch-preview-model'] ?? true),
@@ -854,9 +1085,7 @@ export function useVisualConfig() {
routingStrategy: routing?.strategy === 'fill-first' ? 'fill-first' : 'round-robin',
routingSessionAffinity: Boolean(
routing?.['session-affinity'] ??
routing?.sessionAffinity ??
routing?.['sessionAffinity']
routing?.['session-affinity'] ?? routing?.sessionAffinity ?? routing?.['sessionAffinity']
),
routingSessionAffinityTTL:
typeof routing?.['session-affinity-ttl'] === 'string'
@@ -920,6 +1149,7 @@ export function useVisualConfig() {
values.rmAllowRemote ||
values.rmSecretKey.trim() ||
values.rmDisableControlPanel ||
values.rmDisableAutoUpdatePanel ||
values.rmPanelRepo.trim()
) {
ensureMapInDoc(doc, ['remote-management']);
@@ -930,6 +1160,11 @@ export function useVisualConfig() {
['remote-management', 'disable-control-panel'],
values.rmDisableControlPanel
);
setBooleanInDoc(
doc,
['remote-management', 'disable-auto-update-panel'],
values.rmDisableAutoUpdatePanel
);
setStringInDoc(doc, ['remote-management', 'panel-github-repository'], values.rmPanelRepo);
if (docHas(doc, ['remote-management', 'panel-repo'])) {
doc.deleteIn(['remote-management', 'panel-repo']);
@@ -954,13 +1189,95 @@ export function useVisualConfig() {
setBooleanInDoc(doc, ['commercial-mode'], values.commercialMode);
setBooleanInDoc(doc, ['logging-to-file'], values.loggingToFile);
setIntFromStringInDoc(doc, ['logs-max-total-size-mb'], values.logsMaxTotalSizeMb);
setIntFromStringInDoc(doc, ['error-logs-max-files'], values.errorLogsMaxFiles);
setBooleanInDoc(doc, ['usage-statistics-enabled'], values.usageStatisticsEnabled);
setIntFromStringInDoc(
doc,
['redis-usage-queue-retention-seconds'],
values.redisUsageQueueRetentionSeconds
);
setStringInDoc(doc, ['proxy-url'], values.proxyUrl);
setBooleanInDoc(doc, ['force-model-prefix'], values.forceModelPrefix);
setBooleanInDoc(doc, ['passthrough-headers'], values.passthroughHeaders);
setIntFromStringInDoc(doc, ['request-retry'], values.requestRetry);
setIntFromStringInDoc(doc, ['max-retry-credentials'], values.maxRetryCredentials);
setIntFromStringInDoc(doc, ['max-retry-interval'], values.maxRetryInterval);
setBooleanInDoc(doc, ['disable-cooling'], values.disableCooling);
setDisableImageGenerationInDoc(
doc,
['disable-image-generation'],
values.disableImageGeneration
);
setIntFromStringInDoc(doc, ['auth-auto-refresh-workers'], values.authAutoRefreshWorkers);
setBooleanInDoc(doc, ['ws-auth'], values.wsAuth);
setBooleanInDoc(doc, ['enable-gemini-cli-endpoint'], values.enableGeminiCliEndpoint);
if (
docHas(doc, ['antigravity-signature-cache-enabled']) ||
!values.antigravitySignatureCacheEnabled
) {
doc.setIn(
['antigravity-signature-cache-enabled'],
values.antigravitySignatureCacheEnabled
);
}
setBooleanInDoc(
doc,
['antigravity-signature-bypass-strict'],
values.antigravitySignatureBypassStrict
);
if (
docHas(doc, ['claude-header-defaults']) ||
values.claudeHeaderUserAgent.trim() ||
values.claudeHeaderPackageVersion.trim() ||
values.claudeHeaderRuntimeVersion.trim() ||
values.claudeHeaderOs.trim() ||
values.claudeHeaderArch.trim() ||
values.claudeHeaderTimeout.trim() ||
values.claudeHeaderStabilizeDeviceProfile
) {
ensureMapInDoc(doc, ['claude-header-defaults']);
setStringInDoc(
doc,
['claude-header-defaults', 'user-agent'],
values.claudeHeaderUserAgent
);
setStringInDoc(
doc,
['claude-header-defaults', 'package-version'],
values.claudeHeaderPackageVersion
);
setStringInDoc(
doc,
['claude-header-defaults', 'runtime-version'],
values.claudeHeaderRuntimeVersion
);
setStringInDoc(doc, ['claude-header-defaults', 'os'], values.claudeHeaderOs);
setStringInDoc(doc, ['claude-header-defaults', 'arch'], values.claudeHeaderArch);
setStringInDoc(doc, ['claude-header-defaults', 'timeout'], values.claudeHeaderTimeout);
setBooleanInDoc(
doc,
['claude-header-defaults', 'stabilize-device-profile'],
values.claudeHeaderStabilizeDeviceProfile
);
deleteIfMapEmpty(doc, ['claude-header-defaults']);
}
if (
docHas(doc, ['codex-header-defaults']) ||
values.codexHeaderUserAgent.trim() ||
values.codexHeaderBetaFeatures.trim()
) {
ensureMapInDoc(doc, ['codex-header-defaults']);
setStringInDoc(doc, ['codex-header-defaults', 'user-agent'], values.codexHeaderUserAgent);
setStringInDoc(
doc,
['codex-header-defaults', 'beta-features'],
values.codexHeaderBetaFeatures
);
deleteIfMapEmpty(doc, ['codex-header-defaults']);
}
if (
docHas(doc, ['quota-exceeded']) ||
@@ -983,10 +1300,7 @@ export function useVisualConfig() {
doc.setIn(['quota-exceeded', 'switch-project'], values.quotaSwitchProject);
doc.setIn(['quota-exceeded', 'switch-preview-model'], values.quotaSwitchPreviewModel);
if (writeQuotaAntigravityCredits) {
doc.setIn(
['quota-exceeded', 'antigravity-credits'],
values.quotaAntigravityCredits
);
doc.setIn(['quota-exceeded', 'antigravity-credits'], values.quotaAntigravityCredits);
}
deleteIfMapEmpty(doc, ['quota-exceeded']);
}
+77 -4
View File
@@ -540,6 +540,7 @@
"filter_claude": "Claude",
"filter_codex": "Codex",
"filter_antigravity": "Antigravity",
"filter_xai": "xAI",
"filter_iflow": "iFlow",
"filter_vertex": "Vertex",
"filter_empty": "Empty",
@@ -552,6 +553,7 @@
"type_claude": "Claude",
"type_codex": "Codex",
"type_antigravity": "Antigravity",
"type_xai": "xAI",
"type_iflow": "iFlow",
"type_vertex": "Vertex",
"type_empty": "Empty",
@@ -589,6 +591,7 @@
"prefix_proxy_loading": "Loading auth file...",
"prefix_proxy_info_label": "Auth file info (info)",
"prefix_proxy_source_label": "Auth file JSON (preview)",
"prefix_proxy_invalid_content_label": "Downloaded content (truncated)",
"prefix_label": "Prefix (prefix)",
"proxy_url_label": "Proxy URL (proxy_url)",
"prefix_placeholder": "",
@@ -613,6 +616,7 @@
"headers_invalid_object": "Custom headers must be a JSON object.",
"headers_invalid_value": "Each custom header value must be a string.",
"prefix_proxy_invalid_json": "This auth file is not a JSON object, so fields cannot be edited.",
"prefix_proxy_html_challenge": "Downloaded content is an HTML challenge page, not an auth JSON object. Re-authenticate or replace this auth file before editing fields.",
"prefix_proxy_saved_success": "Updated auth file \"{{name}}\" successfully",
"quota_refresh_success": "Quota refreshed for \"{{name}}\"",
"quota_refresh_failed": "Failed to refresh quota for \"{{name}}\": {{message}}"
@@ -901,6 +905,22 @@
"kimi_oauth_status_error": "Authentication failed:",
"kimi_oauth_start_error": "Failed to start Kimi OAuth:",
"kimi_oauth_polling_error": "Failed to check authentication status:",
"xai_oauth_title": "xAI OAuth",
"xai_oauth_button": "Start xAI Login",
"xai_oauth_hint": "Login to xAI Grok through OAuth flow, automatically obtain and save authentication files.",
"xai_oauth_url_label": "Authorization URL:",
"xai_open_link": "Open Link",
"xai_copy_link": "Copy Link",
"xai_oauth_status_waiting": "Waiting for authentication...",
"xai_oauth_status_success": "Authentication successful!",
"xai_oauth_status_error": "Authentication failed:",
"xai_oauth_start_error": "Failed to start xAI OAuth:",
"xai_oauth_polling_error": "Failed to check authentication status:",
"xai_callback_label": "Callback URL or code",
"xai_callback_placeholder": "Paste the code shown by Grok, or the full callback URL",
"xai_callback_hint": "Grok may only show a code on the page. Paste that code directly and the app will submit it as http://127.0.0.1:56121/callback?... automatically.",
"xai_callback_required": "Please paste the code shown by Grok or the full callback URL first.",
"xai_callback_state_missing": "Missing the state for this xAI login. Start xAI login again, then submit the code.",
"oauth_callback_label": "Callback URL",
"oauth_callback_placeholder": "http://localhost:1455/auth/callback?code=...&state=...",
"oauth_callback_hint": "Remote browser mode: after the provider redirects to http://localhost:..., copy the full URL and submit it here.",
@@ -1074,6 +1094,8 @@
"allow_remote_desc": "Allow management access from other hosts",
"disable_panel": "Disable Control Panel",
"disable_panel_desc": "Disable the built-in web control panel",
"disable_auto_update_panel": "Disable Panel Auto Updates",
"disable_auto_update_panel_desc": "Download the panel when first missing, but never auto-update it from GitHub",
"secret_key": "Management Key",
"secret_key_placeholder": "Set management key",
"panel_repo": "Panel Repository"
@@ -1093,7 +1115,31 @@
"commercial_mode_desc": "Disable high-overhead middleware to support high concurrency",
"logging_to_file": "Log to File",
"logging_to_file_desc": "Save logs to files",
"logs_max_size": "Log File Size Limit (MB)"
"logs_max_size": "Log File Size Limit (MB)",
"error_logs_max_files": "Retained Error Log Files",
"usage_statistics_enabled": "Enable In-memory Usage Statistics",
"usage_statistics_enabled_desc": "Aggregate request usage statistics in memory",
"redis_usage_retention": "Redis Usage Queue Retention (seconds)",
"redis_usage_retention_hint": "In-memory retention for RESP LPOP/RPOP usage output, max 3600 seconds",
"antigravity_signature_cache": "Enable Antigravity Signature Cache",
"antigravity_signature_cache_desc": "Prefer and validate cached thinking-block signatures",
"antigravity_signature_strict": "Strict Bypass Signature Validation",
"antigravity_signature_strict_desc": "Only applies when the signature cache is disabled; validates the full Claude protobuf tree"
},
"headers": {
"title": "Header Defaults",
"description": "Default headers for Claude and Codex OAuth requests when the client omits them",
"claude_title": "Claude Header Defaults",
"codex_title": "Codex Header Defaults",
"user_agent": "User-Agent",
"package_version": "Package Version",
"runtime_version": "Runtime Version",
"os": "OS",
"arch": "Arch",
"timeout": "Timeout",
"stabilize_device": "Stabilize Device Profile",
"stabilize_device_desc": "Pin OS/Arch and stabilize the software fingerprint per credential/API key",
"beta_features": "Beta Features"
},
"network": {
"title": "Network Configuration",
@@ -1103,6 +1149,13 @@
"max_retry_credentials": "Max Retry Credentials",
"max_retry_credentials_hint": "Leave empty to keep it unset. Set to 0 to preserve legacy behavior and try all available credentials.",
"max_retry_interval": "Max Retry Interval (seconds)",
"auth_auto_refresh_workers": "Auth Auto-refresh Workers",
"auth_auto_refresh_workers_hint": "When greater than 0, overrides the default worker count (16)",
"disable_image_generation": "Disable Image Generation",
"disable_image_generation_hint": "false enables it; true disables all image generation; chat only disables non-image endpoint injection",
"disable_image_generation_false": "false (enabled)",
"disable_image_generation_true": "true (disabled everywhere)",
"disable_image_generation_chat": "chat (disable chat injection only)",
"routing_strategy": "Routing Strategy",
"routing_strategy_hint": "Select credential selection strategy",
"strategy_round_robin": "Round Robin",
@@ -1110,9 +1163,15 @@
"session_affinity_ttl": "Session Affinity TTL",
"force_model_prefix": "Force Model Prefix",
"force_model_prefix_desc": "Unprefixed model requests only use credentials without prefix",
"passthrough_headers": "Pass Through Upstream Headers",
"passthrough_headers_desc": "Forward filtered upstream response headers to downstream clients",
"disable_cooling": "Disable Cooling",
"disable_cooling_desc": "Globally disable auth/model cooldown windows after failures",
"session_affinity": "Session Affinity Routing",
"ws_auth": "WebSocket Authentication",
"ws_auth_desc": "Enable WebSocket authentication (/v1/ws)"
"ws_auth_desc": "Enable WebSocket authentication (/v1/ws)",
"enable_gemini_cli_endpoint": "Enable Gemini CLI Internal Endpoint",
"enable_gemini_cli_endpoint_desc": "Enable /v1internal:* compatibility endpoints"
},
"quota": {
"title": "Quota Fallback",
@@ -1121,8 +1180,7 @@
"switch_project_desc": "Automatically switch to another project when quota is exceeded",
"switch_preview_model": "Switch to Preview Model",
"switch_preview_model_desc": "Switch to preview model version when quota is exceeded",
"antigravity_credits": "Antigravity Credits Retry",
"antigravity_credits_desc": "Retry once with enabledCreditTypes=[\"GOOGLE_ONE_AI\"] when Antigravity returns quota_exhausted 429"
"antigravity_credits": "Use Antigravity Credits"
},
"streaming": {
"title": "Streaming Configuration",
@@ -1182,10 +1240,25 @@
"provider_default": "Default",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
"provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
"advanced": "Advanced",
"hide_advanced": "Hide Advanced",
"from_protocol": "Source Protocol (from-protocol)",
"headers": "Request Header Matches",
"header_name": "Header Name",
"header_value": "Header Value",
"add_header": "Add Header",
"match": "Required Matches (match)",
"notMatch": "Forbidden Matches (not-match)",
"exist": "Required Paths (exist)",
"notExist": "Forbidden Paths (not-exist)",
"condition_path": "Match Path",
"condition_value": "Match Value",
"add_condition": "Add Condition",
"value_type_string": "String",
"value_type_number": "Number",
"value_type_boolean": "Boolean",
+77 -4
View File
@@ -540,6 +540,7 @@
"filter_claude": "Claude",
"filter_codex": "Codex",
"filter_antigravity": "Antigravity",
"filter_xai": "xAI",
"filter_iflow": "iFlow",
"filter_vertex": "Vertex",
"filter_empty": "Пусто",
@@ -552,6 +553,7 @@
"type_claude": "Claude",
"type_codex": "Codex",
"type_antigravity": "Antigravity",
"type_xai": "xAI",
"type_iflow": "iFlow",
"type_vertex": "Vertex",
"type_empty": "Пусто",
@@ -589,6 +591,7 @@
"prefix_proxy_loading": "Загрузка файла авторизации...",
"prefix_proxy_info_label": "Информация о файле авторизации (info)",
"prefix_proxy_source_label": "JSON файла авторизации (предпросмотр)",
"prefix_proxy_invalid_content_label": "Скачанное содержимое (сокращено)",
"prefix_label": "Префикс (prefix)",
"proxy_url_label": "URL прокси (proxy_url)",
"prefix_placeholder": "",
@@ -607,6 +610,7 @@
"note_hint": "Необязательно. Используется для описания назначения или владельца учётных данных; оставьте пустым, чтобы не записывать.",
"note_display": "Заметка",
"prefix_proxy_invalid_json": "Этот файл авторизации не является JSON-объектом, поэтому поля нельзя редактировать.",
"prefix_proxy_html_challenge": "Скачанное содержимое является HTML-страницей проверки, а не JSON-объектом авторизации. Повторно авторизуйтесь или замените файл перед редактированием полей.",
"prefix_proxy_saved_success": "Файл авторизации \"{{name}}\" успешно обновлён",
"card_tools_title": "Инструменты",
"quota_refresh_single": "Обновить квоту",
@@ -898,6 +902,22 @@
"kimi_oauth_status_error": "Ошибка аутентификации:",
"kimi_oauth_start_error": "Не удалось запустить Kimi OAuth:",
"kimi_oauth_polling_error": "Не удалось проверить статус аутентификации:",
"xai_oauth_title": "xAI OAuth",
"xai_oauth_button": "Начать вход xAI",
"xai_oauth_hint": "Выполните вход в xAI Grok через OAuth и автоматически получите/сохраните файлы авторизации.",
"xai_oauth_url_label": "URL авторизации:",
"xai_open_link": "Открыть ссылку",
"xai_copy_link": "Скопировать ссылку",
"xai_oauth_status_waiting": "Ожидание аутентификации...",
"xai_oauth_status_success": "Аутентификация успешна!",
"xai_oauth_status_error": "Ошибка аутентификации:",
"xai_oauth_start_error": "Не удалось запустить xAI OAuth:",
"xai_oauth_polling_error": "Не удалось проверить статус аутентификации:",
"xai_callback_label": "Callback URL или код",
"xai_callback_placeholder": "Вставьте код со страницы Grok или полный callback URL",
"xai_callback_hint": "Grok иногда показывает только код на странице. Вставьте этот код напрямую, приложение автоматически отправит его как http://127.0.0.1:56121/callback?...",
"xai_callback_required": "Сначала вставьте код со страницы Grok или полный callback URL.",
"xai_callback_state_missing": "Отсутствует state для этого входа xAI. Запустите вход xAI заново и затем отправьте код.",
"oauth_callback_label": "Callback URL",
"oauth_callback_placeholder": "http://localhost:1455/auth/callback?code=...&state=...",
"oauth_callback_hint": "Режим удалённого браузера: после перенаправления провайдера на http://localhost:... скопируйте полный URL и отправьте его здесь.",
@@ -1071,6 +1091,8 @@
"allow_remote_desc": "Разрешить управление с других хостов",
"disable_panel": "Отключить панель",
"disable_panel_desc": "Отключить встроенную веб-панель управления",
"disable_auto_update_panel": "Отключить автообновление панели",
"disable_auto_update_panel_desc": "Скачивать панель при первом отсутствии, но не обновлять её автоматически с GitHub",
"secret_key": "Ключ управления",
"secret_key_placeholder": "Задайте ключ управления",
"panel_repo": "Репозиторий панели"
@@ -1090,7 +1112,31 @@
"commercial_mode_desc": "Отключить тяжёлое промежуточное ПО для поддержки высокой нагрузки",
"logging_to_file": "Журналировать в файл",
"logging_to_file_desc": "Сохранять журналы в файлы",
"logs_max_size": "Максимальный размер файла журнала (МБ)"
"logs_max_size": "Максимальный размер файла журнала (МБ)",
"error_logs_max_files": "Файлов журнала ошибок",
"usage_statistics_enabled": "Включить статистику использования в памяти",
"usage_statistics_enabled_desc": "Агрегировать статистику использования запросов в памяти",
"redis_usage_retention": "Хранение очереди Redis usage (сек)",
"redis_usage_retention_hint": "Время хранения вывода RESP LPOP/RPOP в памяти, максимум 3600 секунд",
"antigravity_signature_cache": "Включить кэш подписей Antigravity",
"antigravity_signature_cache_desc": "Предпочитать и проверять кэшированные подписи thinking-блоков",
"antigravity_signature_strict": "Строгая проверка bypass-подписи",
"antigravity_signature_strict_desc": "Применяется только при отключённом кэше подписей; проверяет полное дерево Claude protobuf"
},
"headers": {
"title": "Заголовки по умолчанию",
"description": "Заголовки по умолчанию для OAuth-запросов Claude и Codex, когда клиент их не отправил",
"claude_title": "Claude Header Defaults",
"codex_title": "Codex Header Defaults",
"user_agent": "User-Agent",
"package_version": "Package Version",
"runtime_version": "Runtime Version",
"os": "OS",
"arch": "Arch",
"timeout": "Timeout",
"stabilize_device": "Стабилизировать профиль устройства",
"stabilize_device_desc": "Фиксировать OS/Arch и стабилизировать программный отпечаток для учётных данных/API-ключа",
"beta_features": "Beta Features"
},
"network": {
"title": "Сетевые настройки",
@@ -1100,6 +1146,13 @@
"max_retry_credentials": "Максимум учётных данных для повторов",
"max_retry_credentials_hint": "Оставьте пустым, чтобы не задавать поле. Значение 0 сохраняет legacy-поведение и позволяет перебрать все доступные учётные данные.",
"max_retry_interval": "Максимальный интервал повтора (сек)",
"auth_auto_refresh_workers": "Workers автообновления auth",
"auth_auto_refresh_workers_hint": "Если больше 0, переопределяет число workers по умолчанию (16)",
"disable_image_generation": "Отключить генерацию изображений",
"disable_image_generation_hint": "false включает; true отключает везде; chat отключает только инъекцию на не-image endpoints",
"disable_image_generation_false": "false (включено)",
"disable_image_generation_true": "true (отключено везде)",
"disable_image_generation_chat": "chat (только отключить chat-инъекцию)",
"routing_strategy": "Стратегия маршрутизации",
"routing_strategy_hint": "Выберите стратегию подбора учётных данных",
"strategy_round_robin": "По кругу",
@@ -1107,9 +1160,15 @@
"session_affinity_ttl": "TTL привязки сессии",
"force_model_prefix": "Принудительный префикс модели",
"force_model_prefix_desc": "Запросы к моделям без префикса используют только учётные данные без префикса",
"passthrough_headers": "Передавать upstream-заголовки",
"passthrough_headers_desc": "Передавать отфильтрованные upstream-заголовки ответа downstream-клиентам",
"disable_cooling": "Отключить cooldown",
"disable_cooling_desc": "Глобально отключить окна охлаждения auth/model после ошибок",
"session_affinity": "Маршрутизация с привязкой к сессии",
"ws_auth": "Аутентификация WebSocket",
"ws_auth_desc": "Включить аутентификацию WebSocket (/v1/ws)"
"ws_auth_desc": "Включить аутентификацию WebSocket (/v1/ws)",
"enable_gemini_cli_endpoint": "Включить внутренний endpoint Gemini CLI",
"enable_gemini_cli_endpoint_desc": "Включить совместимые endpoints /v1internal:*"
},
"quota": {
"title": "Резерв по квоте",
@@ -1118,8 +1177,7 @@
"switch_project_desc": "Автоматически переходить на другой проект при превышении квоты",
"switch_preview_model": "Переключить на preview-модель",
"switch_preview_model_desc": "Переключаться на preview-версию модели при превышении квоты",
"antigravity_credits": "Повтор Antigravity Credits",
"antigravity_credits_desc": "При ответе Antigravity quota_exhausted 429 повторять запрос один раз с enabledCreditTypes=[\"GOOGLE_ONE_AI\"]"
"antigravity_credits": "Использовать Antigravity Credits"
},
"streaming": {
"title": "Настройки стриминга",
@@ -1179,10 +1237,25 @@
"provider_default": "По умолчанию",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
"provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
"advanced": "Дополнительно",
"hide_advanced": "Скрыть дополнительно",
"from_protocol": "Исходный протокол (from-protocol)",
"headers": "Совпадения заголовков запроса",
"header_name": "Имя заголовка",
"header_value": "Значение заголовка",
"add_header": "Добавить заголовок",
"match": "Обязательные совпадения (match)",
"notMatch": "Запрещённые совпадения (not-match)",
"exist": "Обязательные пути (exist)",
"notExist": "Запрещённые пути (not-exist)",
"condition_path": "Путь условия",
"condition_value": "Значение условия",
"add_condition": "Добавить условие",
"value_type_string": "Строка",
"value_type_number": "Число",
"value_type_boolean": "Булево",
+77 -4
View File
@@ -540,6 +540,7 @@
"filter_claude": "Claude",
"filter_codex": "Codex",
"filter_antigravity": "Antigravity",
"filter_xai": "xAI",
"filter_iflow": "iFlow",
"filter_vertex": "Vertex",
"filter_empty": "空文件",
@@ -552,6 +553,7 @@
"type_claude": "Claude",
"type_codex": "Codex",
"type_antigravity": "Antigravity",
"type_xai": "xAI",
"type_iflow": "iFlow",
"type_vertex": "Vertex",
"type_empty": "空文件",
@@ -589,6 +591,7 @@
"prefix_proxy_loading": "正在加载认证文件...",
"prefix_proxy_info_label": "认证文件信息(info",
"prefix_proxy_source_label": "认证文件 JSON(预览)",
"prefix_proxy_invalid_content_label": "下载内容(已截断)",
"prefix_label": "前缀(prefix",
"proxy_url_label": "代理 URLproxy_url",
"prefix_placeholder": "",
@@ -613,6 +616,7 @@
"headers_invalid_object": "自定义请求头必须是 JSON 对象。",
"headers_invalid_value": "每个自定义请求头的值都必须是字符串。",
"prefix_proxy_invalid_json": "该认证文件不是 JSON 对象,无法编辑字段。",
"prefix_proxy_html_challenge": "下载到的是 HTML 验证页面,不是认证 JSON 对象。请重新认证或替换该认证文件后再编辑字段。",
"prefix_proxy_saved_success": "已更新认证文件 \"{{name}}\"",
"quota_refresh_success": "已刷新 \"{{name}}\" 的额度",
"quota_refresh_failed": "刷新 \"{{name}}\" 的额度失败:{{message}}"
@@ -901,6 +905,22 @@
"kimi_oauth_status_error": "认证失败:",
"kimi_oauth_start_error": "启动 Kimi OAuth 失败:",
"kimi_oauth_polling_error": "检查认证状态失败:",
"xai_oauth_title": "xAI OAuth",
"xai_oauth_button": "开始 xAI 登录",
"xai_oauth_hint": "通过 OAuth 流程登录 xAI Grok 服务,自动获取并保存认证文件。",
"xai_oauth_url_label": "授权链接:",
"xai_open_link": "打开链接",
"xai_copy_link": "复制链接",
"xai_oauth_status_waiting": "等待认证中...",
"xai_oauth_status_success": "认证成功!",
"xai_oauth_status_error": "认证失败:",
"xai_oauth_start_error": "启动 xAI OAuth 失败:",
"xai_oauth_polling_error": "检查认证状态失败:",
"xai_callback_label": "回调 URL 或授权码",
"xai_callback_placeholder": "粘贴页面显示的 code,或完整 callback URL",
"xai_callback_hint": "Grok 有时只在页面显示 code。可直接粘贴 code,系统会自动拼接为 http://127.0.0.1:56121/callback?... 后提交。",
"xai_callback_required": "请先粘贴 Grok 页面显示的 code 或完整回调 URL。",
"xai_callback_state_missing": "缺少本次 xAI 登录的 state,请重新开始 xAI 登录后再提交 code。",
"oauth_callback_label": "回调 URL",
"oauth_callback_placeholder": "http://localhost:1455/auth/callback?code=...&state=...",
"oauth_callback_hint": "远程浏览器模式:当授权跳转到 http://localhost:... 后,复制完整 URL 并提交到这里。",
@@ -1074,6 +1094,8 @@
"allow_remote_desc": "允许从其他主机访问管理接口",
"disable_panel": "禁用控制面板",
"disable_panel_desc": "禁用内置的 Web 控制面板",
"disable_auto_update_panel": "禁用面板自动更新",
"disable_auto_update_panel_desc": "首次缺失时仍可下载,但不再从 GitHub 后台自动更新",
"secret_key": "管理密钥",
"secret_key_placeholder": "设置管理密钥",
"panel_repo": "面板仓库"
@@ -1093,7 +1115,31 @@
"commercial_mode_desc": "禁用高开销中间件以支持高并发",
"logging_to_file": "写入日志文件",
"logging_to_file_desc": "将日志保存到文件",
"logs_max_size": "日志文件大小限制 (MB)"
"logs_max_size": "日志文件大小限制 (MB)",
"error_logs_max_files": "错误日志保留文件数",
"usage_statistics_enabled": "启用内存用量统计",
"usage_statistics_enabled_desc": "启用内存中的请求用量聚合",
"redis_usage_retention": "Redis 用量队列保留秒数",
"redis_usage_retention_hint": "RESP 接口 LPOP/RPOP 用量队列的内存保留时间,最大 3600 秒",
"antigravity_signature_cache": "启用 Antigravity 签名缓存",
"antigravity_signature_cache_desc": "优先使用并校验缓存的 thinking block 签名",
"antigravity_signature_strict": "严格校验旁路签名",
"antigravity_signature_strict_desc": "仅在关闭签名缓存时生效,按完整 Claude protobuf 树校验"
},
"headers": {
"title": "Header 默认值",
"description": "Claude 与 Codex OAuth 请求在客户端缺少 Header 时使用的默认值",
"claude_title": "Claude Header Defaults",
"codex_title": "Codex Header Defaults",
"user_agent": "User-Agent",
"package_version": "Package Version",
"runtime_version": "Runtime Version",
"os": "OS",
"arch": "Arch",
"timeout": "Timeout",
"stabilize_device": "稳定设备指纹",
"stabilize_device_desc": "固定 OS/Arch,并按凭据/API Key 稳定软件指纹",
"beta_features": "Beta Features"
},
"network": {
"title": "网络配置",
@@ -1103,6 +1149,13 @@
"max_retry_credentials": "最大重试凭据数",
"max_retry_credentials_hint": "留空表示不设置;设为 0 表示保留 legacy 行为,并尝试所有可用凭据。",
"max_retry_interval": "最大重试间隔 (秒)",
"auth_auto_refresh_workers": "认证自动刷新 Worker 数",
"auth_auto_refresh_workers_hint": "大于 0 时覆盖默认 Worker 数(16",
"disable_image_generation": "禁用图像生成",
"disable_image_generation_hint": "false 为启用;true 为全部禁用;chat 仅禁用非图片端点注入",
"disable_image_generation_false": "false(启用)",
"disable_image_generation_true": "true(全部禁用)",
"disable_image_generation_chat": "chat(仅禁用聊天注入)",
"routing_strategy": "路由策略",
"routing_strategy_hint": "选择凭据选择策略",
"strategy_round_robin": "轮询 (Round Robin)",
@@ -1110,9 +1163,15 @@
"session_affinity_ttl": "会话粘性 TTL",
"force_model_prefix": "强制模型前缀",
"force_model_prefix_desc": "未带前缀的模型请求只使用无前缀凭据",
"passthrough_headers": "透传上游响应 Header",
"passthrough_headers_desc": "将过滤后的上游响应 Header 转发给下游客户端",
"disable_cooling": "禁用冷却调度",
"disable_cooling_desc": "全局禁用认证/模型失败后的冷却窗口",
"session_affinity": "会话粘性路由",
"ws_auth": "WebSocket 认证",
"ws_auth_desc": "启用 WebSocket 连接认证 (/v1/ws)"
"ws_auth_desc": "启用 WebSocket 连接认证 (/v1/ws)",
"enable_gemini_cli_endpoint": "启用 Gemini CLI 内部端点",
"enable_gemini_cli_endpoint_desc": "启用 /v1internal:* 兼容端点"
},
"quota": {
"title": "配额回退",
@@ -1121,8 +1180,7 @@
"switch_project_desc": "配额耗尽时自动切换到其他项目",
"switch_preview_model": "切换预览模型",
"switch_preview_model_desc": "配额耗尽时切换到预览版本模型",
"antigravity_credits": "Antigravity Credits 重试",
"antigravity_credits_desc": "Antigravity 返回 quota_exhausted 429 时,使用 enabledCreditTypes=[\"GOOGLE_ONE_AI\"] 重试一次"
"antigravity_credits": "使用Antigravity Credits"
},
"streaming": {
"title": "流式传输配置",
@@ -1182,10 +1240,25 @@
"provider_default": "默认",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
"provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
"advanced": "高级",
"hide_advanced": "收起高级",
"from_protocol": "来源协议 (from-protocol)",
"headers": "请求 Header 匹配",
"header_name": "Header 名称",
"header_value": "Header 值",
"add_header": "添加 Header",
"match": "必须匹配 (match)",
"notMatch": "不得匹配 (not-match)",
"exist": "必须存在 (exist)",
"notExist": "不得存在 (not-exist)",
"condition_path": "匹配路径",
"condition_value": "匹配值",
"add_condition": "添加条件",
"value_type_string": "字符串",
"value_type_number": "数字",
"value_type_boolean": "布尔",
+77 -4
View File
@@ -540,6 +540,7 @@
"filter_claude": "Claude",
"filter_codex": "Codex",
"filter_antigravity": "Antigravity",
"filter_xai": "xAI",
"filter_iflow": "iFlow",
"filter_vertex": "Vertex",
"filter_empty": "空檔案",
@@ -552,6 +553,7 @@
"type_claude": "Claude",
"type_codex": "Codex",
"type_antigravity": "Antigravity",
"type_xai": "xAI",
"type_iflow": "iFlow",
"type_vertex": "Vertex",
"type_empty": "空檔案",
@@ -589,6 +591,7 @@
"prefix_proxy_loading": "正在載入驗證檔案...",
"prefix_proxy_info_label": "驗證檔案資訊(info",
"prefix_proxy_source_label": "驗證檔案 JSON(預覽)",
"prefix_proxy_invalid_content_label": "下載內容(已截斷)",
"prefix_label": "前綴(prefix",
"proxy_url_label": "代理 URLproxy_url",
"prefix_placeholder": "",
@@ -613,6 +616,7 @@
"headers_invalid_object": "自訂請求標頭必須是 JSON 物件。",
"headers_invalid_value": "每個自訂請求標頭的值都必須是字串。",
"prefix_proxy_invalid_json": "該驗證檔案不是 JSON 物件,無法編輯欄位。",
"prefix_proxy_html_challenge": "下載到的是 HTML 驗證頁面,不是驗證 JSON 物件。請重新驗證或替換該驗證檔案後再編輯欄位。",
"prefix_proxy_saved_success": "已更新驗證檔案「{{name}}」",
"quota_refresh_success": "已重新整理「{{name}}」的配額",
"quota_refresh_failed": "重新整理「{{name}}」的配額失敗:{{message}}"
@@ -901,6 +905,22 @@
"kimi_oauth_status_error": "驗證失敗:",
"kimi_oauth_start_error": "啟動 Kimi OAuth 失敗:",
"kimi_oauth_polling_error": "檢查驗證狀態失敗:",
"xai_oauth_title": "xAI OAuth",
"xai_oauth_button": "開始 xAI 登入",
"xai_oauth_hint": "透過 OAuth 流程登入 xAI Grok 服務,自動取得並儲存驗證檔案。",
"xai_oauth_url_label": "授權連結:",
"xai_open_link": "開啟連結",
"xai_copy_link": "複製連結",
"xai_oauth_status_waiting": "等待驗證中...",
"xai_oauth_status_success": "驗證成功!",
"xai_oauth_status_error": "驗證失敗:",
"xai_oauth_start_error": "啟動 xAI OAuth 失敗:",
"xai_oauth_polling_error": "檢查驗證狀態失敗:",
"xai_callback_label": "回調 URL 或授權碼",
"xai_callback_placeholder": "貼上頁面顯示的 code,或完整 callback URL",
"xai_callback_hint": "Grok 有時只在頁面顯示 code。可直接貼上 code,系統會自動拼接為 http://127.0.0.1:56121/callback?... 後提交。",
"xai_callback_required": "請先貼上 Grok 頁面顯示的 code 或完整回調 URL。",
"xai_callback_state_missing": "缺少本次 xAI 登入的 state,請重新開始 xAI 登入後再提交 code。",
"qwen_oauth_title": "Qwen OAuth",
"qwen_oauth_button": "開始 Qwen 登入",
"qwen_oauth_hint": "透過裝置授權流程登入 Qwen 服務,自動取得並儲存驗證檔案。",
@@ -1100,6 +1120,8 @@
"allow_remote_desc": "允許從其他主機存取管理介面",
"disable_panel": "停用控制面板",
"disable_panel_desc": "停用內建的 Web 控制面板",
"disable_auto_update_panel": "停用面板自動更新",
"disable_auto_update_panel_desc": "首次缺失時仍可下載,但不再從 GitHub 背景自動更新",
"secret_key": "管理金鑰",
"secret_key_placeholder": "設定管理金鑰",
"panel_repo": "面板儲存庫"
@@ -1119,7 +1141,31 @@
"commercial_mode_desc": "停用高開銷中介軟體以支援高並行",
"logging_to_file": "寫入記錄檔",
"logging_to_file_desc": "將記錄儲存到檔案",
"logs_max_size": "記錄檔大小限制(MB"
"logs_max_size": "記錄檔大小限制(MB",
"error_logs_max_files": "錯誤記錄保留檔案數",
"usage_statistics_enabled": "啟用記憶體用量統計",
"usage_statistics_enabled_desc": "啟用記憶體中的請求用量聚合",
"redis_usage_retention": "Redis 用量佇列保留秒數",
"redis_usage_retention_hint": "RESP 介面 LPOP/RPOP 用量佇列的記憶體保留時間,最大 3600 秒",
"antigravity_signature_cache": "啟用 Antigravity 簽名快取",
"antigravity_signature_cache_desc": "優先使用並校驗快取的 thinking block 簽名",
"antigravity_signature_strict": "嚴格校驗旁路簽名",
"antigravity_signature_strict_desc": "僅在關閉簽名快取時生效,按完整 Claude protobuf 樹校驗"
},
"headers": {
"title": "Header 預設值",
"description": "Claude 與 Codex OAuth 請求在客戶端缺少 Header 時使用的預設值",
"claude_title": "Claude Header Defaults",
"codex_title": "Codex Header Defaults",
"user_agent": "User-Agent",
"package_version": "Package Version",
"runtime_version": "Runtime Version",
"os": "OS",
"arch": "Arch",
"timeout": "Timeout",
"stabilize_device": "穩定設備指紋",
"stabilize_device_desc": "固定 OS/Arch,並按憑證/API Key 穩定軟體指紋",
"beta_features": "Beta Features"
},
"network": {
"title": "網路設定",
@@ -1129,6 +1175,13 @@
"max_retry_credentials": "最大重試憑證數",
"max_retry_credentials_hint": "留空表示不設定;設為 0 表示保留 legacy 行為,並嘗試所有可用憑證。",
"max_retry_interval": "最大重試間隔(秒)",
"auth_auto_refresh_workers": "驗證自動刷新 Worker 數",
"auth_auto_refresh_workers_hint": "大於 0 時覆蓋預設 Worker 數(16",
"disable_image_generation": "停用圖像生成",
"disable_image_generation_hint": "false 為啟用;true 為全部停用;chat 僅停用非圖片端點注入",
"disable_image_generation_false": "false(啟用)",
"disable_image_generation_true": "true(全部停用)",
"disable_image_generation_chat": "chat(僅停用聊天注入)",
"routing_strategy": "路由策略",
"routing_strategy_hint": "選擇憑證選擇策略",
"strategy_round_robin": "輪詢(Round Robin",
@@ -1136,9 +1189,15 @@
"session_affinity_ttl": "會話黏性 TTL",
"force_model_prefix": "強制模型前綴",
"force_model_prefix_desc": "未帶前綴的模型請求只使用無前綴憑證",
"passthrough_headers": "透傳上游回應 Header",
"passthrough_headers_desc": "將過濾後的上游回應 Header 轉發給下游客戶端",
"disable_cooling": "停用冷卻調度",
"disable_cooling_desc": "全域停用驗證/模型失敗後的冷卻視窗",
"session_affinity": "會話黏性路由",
"ws_auth": "WebSocket 驗證",
"ws_auth_desc": "啟用 WebSocket 連線驗證(/v1/ws"
"ws_auth_desc": "啟用 WebSocket 連線驗證(/v1/ws",
"enable_gemini_cli_endpoint": "啟用 Gemini CLI 內部端點",
"enable_gemini_cli_endpoint_desc": "啟用 /v1internal:* 相容端點"
},
"quota": {
"title": "配額回退",
@@ -1147,8 +1206,7 @@
"switch_project_desc": "配額耗盡時自動切換到其他專案",
"switch_preview_model": "切換預覽模型",
"switch_preview_model_desc": "配額耗盡時切換到預覽版本模型",
"antigravity_credits": "Antigravity Credits 重試",
"antigravity_credits_desc": "Antigravity 回傳 quota_exhausted 429 時,使用 enabledCreditTypes=[\"GOOGLE_ONE_AI\"] 重試一次"
"antigravity_credits": "使用Antigravity Credits"
},
"streaming": {
"title": "串流傳輸設定",
@@ -1208,10 +1266,25 @@
"provider_default": "預設",
"provider_openai": "OpenAI",
"provider_openai_response": "OpenAI Response",
"provider_responses": "Responses",
"provider_gemini": "Gemini",
"provider_claude": "Claude",
"provider_codex": "Codex",
"provider_antigravity": "Antigravity",
"advanced": "進階",
"hide_advanced": "收起進階",
"from_protocol": "來源協議(from-protocol",
"headers": "請求 Header 匹配",
"header_name": "Header 名稱",
"header_value": "Header 值",
"add_header": "新增 Header",
"match": "必須匹配(match",
"notMatch": "不得匹配(not-match",
"exist": "必須存在(exist",
"notExist": "不得存在(not-exist",
"condition_path": "匹配路徑",
"condition_value": "匹配值",
"add_condition": "新增條件",
"value_type_string": "字串",
"value_type_number": "數字",
"value_type_boolean": "布林",
+70 -16
View File
@@ -100,7 +100,6 @@
}
}
.filterAllIconWrap {
position: relative;
border-color: color-mix(in srgb, var(--primary-color) 20%, var(--border-color));
@@ -119,7 +118,6 @@
color: color-mix(in srgb, var(--primary-color) 70%, var(--text-primary));
}
.filterContent {
display: flex;
flex-direction: column;
@@ -232,16 +230,31 @@
}
.filterControlsPanel {
padding: 14px;
position: relative;
overflow: hidden;
padding: 16px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent);
background: color-mix(in srgb, var(--bg-secondary) 50%, transparent);
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--primary-color) 8%, transparent),
transparent 46%
),
color-mix(in srgb, var(--bg-secondary) 56%, transparent);
@include mobile {
padding: 12px;
}
}
.filterControls {
display: grid;
grid-template-columns: minmax(240px, 1.4fr) repeat(2, minmax(120px, 0.72fr)) minmax(220px, auto);
gap: $spacing-md;
grid-template-columns:
minmax(260px, 1fr)
minmax(108px, 0.35fr)
minmax(148px, 0.45fr);
gap: 12px;
align-items: flex-end;
// 修复 Input 组件在 filterItem 中的嵌套样式
@@ -250,7 +263,7 @@
}
:global(.input) {
height: 40px;
height: 42px;
box-sizing: border-box;
}
@@ -263,6 +276,10 @@
}
}
.filterSearchItem {
min-width: 0;
}
.filterItem {
display: flex;
flex-direction: column;
@@ -273,14 +290,14 @@
font-size: 11px;
color: var(--text-tertiary);
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
letter-spacing: 0;
white-space: nowrap;
}
}
.filterToggleItem {
min-width: 0;
grid-column: 1 / -1;
@include tablet {
grid-column: 1 / -1;
@@ -289,19 +306,29 @@
.filterToggleGroup {
display: grid;
grid-template-columns: 1fr;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
min-height: 40px;
min-height: 42px;
@include mobile {
grid-template-columns: 1fr;
}
}
.filterToggleCard {
display: flex;
align-items: center;
min-height: 40px;
padding: 0 2px;
min-width: 0;
min-height: 42px;
padding: 0 10px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent);
background: color-mix(in srgb, var(--bg-secondary) 50%, transparent);
background: color-mix(in srgb, var(--bg-primary) 56%, transparent);
> label {
width: 100%;
min-width: 0;
}
}
.filterToggle {
@@ -317,7 +344,17 @@
color: var(--text-primary);
font-size: 13px;
font-weight: 600;
white-space: nowrap;
line-height: 1.25;
}
.searchInput {
padding-right: 40px;
}
.searchIcon {
display: block;
color: var(--text-tertiary);
pointer-events: none;
}
.pageSizeSelect {
@@ -329,7 +366,7 @@
color: var(--text-primary);
font-size: 14px;
cursor: text;
height: 40px;
height: 42px;
box-sizing: border-box;
&:focus {
@@ -1388,6 +1425,23 @@
}
}
.prefixProxyInvalidContentPreview {
width: 100%;
max-height: 220px;
margin: 0;
padding: $spacing-sm $spacing-md;
border: 1px solid var(--border-color);
border-radius: $radius-md;
background-color: var(--bg-secondary);
color: var(--text-secondary);
font-size: 12px;
font-family: monospace;
line-height: 1.5;
white-space: pre-wrap;
overflow: auto;
box-sizing: border-box;
}
.prefixProxyFields {
display: grid;
grid-template-columns: 1fr;
+20 -15
View File
@@ -20,7 +20,7 @@ import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select';
import { IconFilterAll } from '@/components/ui/icons';
import { IconFilterAll, IconSearch } from '@/components/ui/icons';
import { EmptyState } from '@/components/ui/EmptyState';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import { copyToClipboard } from '@/utils/clipboard';
@@ -194,7 +194,7 @@ export function AuthFilesPage() {
const persisted = readAuthFilesUiState();
if (persisted) {
if (typeof persisted.filter === 'string' && persisted.filter.trim()) {
setFilter(persisted.filter);
setFilter(normalizeProviderKey(persisted.filter));
}
if (typeof persisted.problemOnly === 'boolean') {
setProblemOnly(persisted.problemOnly);
@@ -349,9 +349,8 @@ export function AuthFilesPage() {
const existingTypes = useMemo(() => {
const types = new Set<string>(['all']);
files.forEach((file) => {
if (file.type) {
types.add(file.type);
}
const type = normalizeProviderKey(String(file.type ?? file.provider ?? ''));
if (type) types.add(type);
});
return Array.from(types);
}, [files]);
@@ -378,8 +377,9 @@ export function AuthFilesPage() {
const typeCounts = useMemo(() => {
const counts: Record<string, number> = { all: filesMatchingStatusFilters.length };
filesMatchingStatusFilters.forEach((file) => {
if (!file.type) return;
counts[file.type] = (counts[file.type] || 0) + 1;
const type = normalizeProviderKey(String(file.type ?? file.provider ?? ''));
if (!type) return;
counts[type] = (counts[type] || 0) + 1;
});
return counts;
}, [filesMatchingStatusFilters]);
@@ -391,7 +391,8 @@ export function AuthFilesPage() {
const normalizedTerm = normalizedSearch.toLowerCase();
return filesMatchingStatusFilters.filter((item) => {
const matchType = filter === 'all' || item.type === filter;
const type = normalizeProviderKey(String(item.type ?? item.provider ?? ''));
const matchType = normalizedFilter === 'all' || type === normalizedFilter;
const matchSearch =
!normalizedSearch ||
[item.name, item.type, item.provider].some((value) => {
@@ -402,7 +403,7 @@ export function AuthFilesPage() {
});
return matchType && matchSearch;
});
}, [filesMatchingStatusFilters, filter, normalizedSearch, wildcardSearch]);
}, [filesMatchingStatusFilters, normalizedFilter, normalizedSearch, wildcardSearch]);
const sorted = useMemo(() => {
const copy = [...filtered];
@@ -586,7 +587,7 @@ export function AuthFilesPage() {
<div className={styles.filterRail}>
<div className={styles.filterTags}>
{existingTypes.map((type) => {
const isActive = filter === type;
const isActive = normalizedFilter === type;
const iconSrc = getAuthFileIcon(type, resolvedTheme);
const color =
type === 'all'
@@ -646,13 +647,15 @@ export function AuthFilesPage() {
return t('auth_files.delete_filtered_result_button');
}
if (problemOnly) {
return filter === 'all'
return normalizedFilter === 'all'
? t('auth_files.delete_problem_button')
: t('auth_files.delete_problem_button_with_type', { type: getTypeLabel(t, filter) });
: t('auth_files.delete_problem_button_with_type', {
type: getTypeLabel(t, normalizedFilter),
});
}
return filter === 'all'
return normalizedFilter === 'all'
? t('auth_files.delete_all_button')
: `${t('common.delete')} ${getTypeLabel(t, filter)}`;
: `${t('common.delete')} ${getTypeLabel(t, normalizedFilter)}`;
})();
return (
@@ -714,15 +717,17 @@ export function AuthFilesPage() {
<div className={styles.filterContent}>
<div className={styles.filterControlsPanel}>
<div className={styles.filterControls}>
<div className={styles.filterItem}>
<div className={`${styles.filterItem} ${styles.filterSearchItem}`}>
<label>{t('auth_files.search_label')}</label>
<Input
className={styles.searchInput}
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
placeholder={t('auth_files.search_placeholder')}
rightElement={<IconSearch className={styles.searchIcon} size={18} />}
/>
</div>
<div className={styles.filterItem}>
+102 -250
View File
@@ -2,11 +2,12 @@
@use '../styles/variables' as *;
.container {
width: 100%;
width: min(100%, 1480px);
min-height: 100%;
display: flex;
flex-direction: column;
gap: $spacing-lg;
gap: clamp(18px, 2.4vw, 28px);
margin: 0 auto;
overflow-y: auto;
padding-bottom: calc(
var(--config-action-bar-height, 0px) + 16px + env(safe-area-inset-bottom) + #{$spacing-md}
@@ -14,189 +15,73 @@
}
.pageHeader {
position: relative;
overflow: hidden;
display: grid;
grid-template-columns: minmax(0, 1.2fr) auto;
gap: 24px;
align-items: end;
padding: clamp(22px, 3vw, 30px);
border-radius: 30px;
border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
background:
radial-gradient(
circle at top right,
color-mix(in srgb, var(--primary-color) 14%, transparent),
transparent 34%
),
linear-gradient(
135deg,
color-mix(in srgb, var(--bg-primary) 94%, transparent),
color-mix(in srgb, var(--bg-secondary) 96%, transparent)
);
&::before {
content: '';
position: absolute;
inset: 0 auto auto 0;
width: min(260px, 48vw);
height: min(260px, 48vw);
background: radial-gradient(
circle,
color-mix(in srgb, var(--primary-color) 12%, transparent),
transparent 70%
);
pointer-events: none;
opacity: 0.7;
}
@include mobile {
grid-template-columns: minmax(0, 1fr);
align-items: stretch;
gap: 16px;
}
display: flex;
align-items: flex-start;
}
.pageHeaderCopy {
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
gap: 10px;
min-width: 0;
}
.pageEyebrow {
color: var(--text-secondary);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
width: min(100%, 360px);
}
.pageTitle {
margin: 0;
color: var(--text-primary);
font-size: clamp(32px, 4vw, 52px);
line-height: 0.95;
letter-spacing: -0.04em;
}
.description {
margin: 0;
max-width: 46rem;
color: var(--text-secondary);
font-size: 15px;
line-height: 1.7;
}
.pageMeta {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
min-width: 0;
@include mobile {
align-items: stretch;
}
}
.statusBadge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 0 14px;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
background: color-mix(in srgb, var(--bg-primary) 86%, transparent);
color: var(--text-primary);
font-size: 13px;
font-size: 28px;
font-weight: 700;
text-align: center;
@include mobile {
width: 100%;
}
color: var(--text-primary);
margin: 0;
}
.tabBar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 999px;
background: color-mix(in srgb, var(--bg-primary) 82%, transparent);
border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
width: fit-content;
max-width: 100%;
overflow-x: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
@include mobile {
width: 100%;
}
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2px;
padding: 2px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: color-mix(in srgb, var(--bg-primary) 72%, transparent);
}
.tabItem {
@include button-reset;
padding: 10px 16px;
border-radius: 999px;
min-height: 38px;
padding: 0 12px;
border-radius: 6px;
border: 1px solid transparent;
color: var(--text-secondary);
font-size: 14px;
font-weight: 700;
font-size: 13px;
font-weight: 650;
line-height: 1.25;
white-space: nowrap;
transition:
color 0.15s ease,
background-color 0.15s ease,
border-color 0.15s ease,
transform 0.15s ease;
border-color 0.15s ease;
&:hover:not(:disabled) {
color: var(--text-primary);
background: color-mix(in srgb, var(--bg-secondary) 92%, transparent);
border-color: color-mix(in srgb, var(--border-color) 88%, transparent);
background: color-mix(in srgb, var(--text-primary) 5%, transparent);
}
&:disabled {
opacity: 0.6;
opacity: 0.58;
cursor: not-allowed;
transform: none;
}
@include mobile {
flex: 1;
text-align: center;
}
}
.tabActive {
color: var(--text-primary);
background: var(--bg-primary);
border-color: color-mix(in srgb, var(--primary-color) 24%, var(--border-color));
box-shadow: 0 16px 32px -28px rgba(0, 0, 0, 0.3);
color: var(--bg-primary);
background: var(--text-primary);
border-color: var(--text-primary);
}
.workspaceShell {
display: flex;
flex-direction: column;
gap: $spacing-lg;
padding: clamp(18px, 3vw, 28px);
border-radius: 32px;
border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
background: color-mix(in srgb, var(--bg-primary) 74%, transparent);
box-shadow: var(--shadow);
@include mobile {
padding: 16px;
border-radius: 28px;
}
min-width: 0;
}
.content {
@@ -209,25 +94,26 @@
.sourceWorkspace {
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
min-height: 0;
}
.sourceToolbar {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
gap: $spacing-sm;
padding-bottom: 16px;
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: color-mix(in srgb, var(--bg-primary) 76%, transparent);
@include mobile {
flex-direction: column;
align-items: stretch;
grid-template-columns: minmax(0, 1fr);
}
}
.searchInputWrapper {
flex: 1;
min-width: 0;
position: relative;
display: flex;
@@ -240,28 +126,28 @@
}
.searchInput {
flex: 1;
border-radius: 16px !important;
padding-right: 132px !important;
min-height: 38px !important;
border-radius: 6px !important;
padding-right: 128px !important;
background: var(--bg-secondary) !important;
}
.searchRight {
display: inline-flex;
align-items: center;
gap: 8px;
gap: 6px;
}
.searchCount {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
background: color-mix(in srgb, var(--bg-primary) 88%, transparent);
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
min-height: 26px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: 12px;
font-weight: 700;
font-weight: 650;
pointer-events: none;
white-space: nowrap;
}
@@ -271,12 +157,12 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 999px;
background: var(--primary-color);
border: 1px solid var(--primary-color);
color: #fff;
width: 30px;
height: 30px;
border-radius: 6px;
border: 1px solid var(--text-primary);
background: var(--text-primary);
color: var(--bg-primary);
transition:
background-color $transition-fast,
border-color $transition-fast,
@@ -288,7 +174,7 @@
}
&:disabled {
opacity: 0.5;
opacity: 0.45;
cursor: not-allowed;
}
}
@@ -299,11 +185,11 @@
flex-shrink: 0;
button {
min-width: 36px;
width: 36px;
height: 36px;
min-width: 38px;
width: 38px;
height: 38px;
padding: 0 !important;
border-radius: 999px;
border-radius: 6px;
}
@include mobile {
@@ -319,21 +205,23 @@
.editorWrapper {
width: 100%;
flex: 0 0 auto;
height: clamp(420px, 64vh, 980px);
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
border-radius: 28px;
height: clamp(500px, 70vh, 1040px);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
background: color-mix(in srgb, var(--bg-primary) 90%, transparent);
background: var(--bg-primary);
@supports (height: 100dvh) {
height: clamp(420px, 64dvh, 980px);
height: clamp(500px, 70dvh, 1040px);
}
:global {
.cm-editor {
height: 100%;
font-size: 14px;
font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
font-size: 13px;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
background: transparent;
}
@@ -345,8 +233,8 @@
}
.cm-gutters {
border-right: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
background: color-mix(in srgb, var(--bg-secondary) 88%, transparent);
border-right: 1px solid var(--border-color);
background: color-mix(in srgb, var(--bg-secondary) 86%, transparent);
}
.cm-lineNumbers .cm-gutterElement {
@@ -355,35 +243,22 @@
color: var(--text-muted);
}
.cm-activeLine {
background: var(--bg-hover);
}
.cm-activeLine,
.cm-activeLineGutter {
background: var(--bg-hover);
background: color-mix(in srgb, var(--text-primary) 5%, transparent);
}
.cm-selectionMatch {
background: rgba(255, 193, 7, 0.3);
background: rgba(224, 170, 20, 0.24);
}
.cm-searchMatch {
background: rgba(255, 193, 7, 0.4);
outline: 1px solid rgba(255, 193, 7, 0.6);
background: rgba(224, 170, 20, 0.32);
outline: 1px solid rgba(224, 170, 20, 0.48);
}
.cm-searchMatch-selected {
background: rgba(255, 152, 0, 0.5);
}
[data-theme='dark'] & {
.cm-gutters {
background: var(--bg-tertiary);
}
.cm-selectionMatch {
background: rgba(255, 193, 7, 0.2);
}
background: rgba(198, 87, 70, 0.32);
}
}
}
@@ -396,8 +271,8 @@
.saved {
color: var(--success-color);
background: color-mix(in srgb, var(--success-color) 12%, var(--bg-primary));
border-color: color-mix(in srgb, var(--success-color) 28%, var(--border-color));
background: color-mix(in srgb, var(--success-color) 10%, transparent);
border-color: color-mix(in srgb, var(--success-color) 34%, var(--border-color));
}
.error {
@@ -420,17 +295,14 @@
.floatingActionList {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
--glass-blur: 12px;
background: var(--glass-bg);
backdrop-filter: var(--glass-backdrop-filter);
-webkit-backdrop-filter: var(--glass-backdrop-filter);
border: 1px solid var(--glass-border);
border-radius: 999px;
box-shadow: var(--shadow-lg);
gap: 6px;
padding: 6px;
max-width: inherit;
overflow-x: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
background: color-mix(in srgb, var(--bg-primary) 92%, transparent);
box-shadow: var(--shadow-lg);
scrollbar-width: none;
&::-webkit-scrollbar {
@@ -442,16 +314,16 @@
display: inline-flex;
align-items: center;
min-width: 0;
min-height: 28px;
min-height: 34px;
padding: 0 10px;
border-radius: 999px;
border-radius: 6px;
border: 1px solid transparent;
background: color-mix(in srgb, var(--text-primary) 6%, transparent);
color: var(--text-primary);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
text-align: center;
max-width: min(280px, 46vw);
max-width: min(300px, 46vw);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -469,28 +341,24 @@
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 999px;
cursor: pointer;
width: 38px;
height: 38px;
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
transition:
background-color 0.2s ease,
transform 0.15s ease;
background-color 0.15s ease,
color 0.15s ease,
opacity 0.15s ease;
&:hover:not(:disabled) {
background: color-mix(in srgb, var(--text-primary) 10%, transparent);
transform: scale(1.08);
}
&:active:not(:disabled) {
transform: scale(0.95);
background: var(--text-primary);
color: var(--bg-primary);
}
&:disabled {
opacity: 0.5;
opacity: 0.45;
cursor: not-allowed;
transform: none;
}
}
@@ -502,7 +370,7 @@
height: 7px;
border-radius: 999px;
background: var(--warning-color);
box-shadow: 0 0 0 2px rgba($warning-color, 0.25);
box-shadow: 0 0 0 2px var(--bg-primary);
}
@media (max-width: 1200px) {
@@ -511,21 +379,8 @@
max-width: calc(100vw - 16px);
}
.floatingActionList {
gap: 6px;
padding: 8px 10px;
}
.floatingStatus {
max-width: min(180px, 40vw);
font-size: 10px;
padding: 0 8px;
}
.floatingActionButton {
width: 38px;
height: 38px;
flex: 0 0 auto;
}
}
@@ -533,11 +388,8 @@
.floatingStatus {
max-width: min(132px, 38vw);
}
}
@media (max-width: 380px) {
.pageHeader,
.workspaceShell {
padding: 14px;
.searchInput {
padding-right: 108px !important;
}
}
-15
View File
@@ -500,26 +500,11 @@ export function ConfigPage() {
</div>
);
const pageEyebrow =
activeTab === 'visual'
? t('config_management.tabs.visual', { defaultValue: '可视化编辑' })
: t('config_management.tabs.source', { defaultValue: '源文件编辑' });
const pageDescription =
activeTab === 'visual'
? t('config_management.visual.notice')
: t('config_management.description');
return (
<div className={styles.container}>
<div className={styles.pageHeader}>
<div className={styles.pageHeaderCopy}>
<span className={styles.pageEyebrow}>{pageEyebrow}</span>
<h1 className={styles.pageTitle}>{t('config_management.title')}</h1>
<p className={styles.description}>{pageDescription}</p>
</div>
<div className={styles.pageMeta}>
<div className={`${styles.statusBadge} ${getStatusClass()}`}>{getStatusText()}</div>
<div className={styles.tabBar}>
<button
type="button"
+108 -7
View File
@@ -16,6 +16,8 @@ import iconGemini from '@/assets/icons/gemini.svg';
import iconKimiLight from '@/assets/icons/kimi-light.svg';
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
import iconVertex from '@/assets/icons/vertex.svg';
import iconGrok from '@/assets/icons/grok.svg';
import iconGrokDark from '@/assets/icons/grok-dark.svg';
interface ProviderState {
url?: string;
@@ -67,10 +69,18 @@ const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabe
{ id: 'anthropic', titleKey: 'auth_login.anthropic_oauth_title', hintKey: 'auth_login.anthropic_oauth_hint', urlLabelKey: 'auth_login.anthropic_oauth_url_label', icon: iconClaude },
{ id: 'antigravity', titleKey: 'auth_login.antigravity_oauth_title', hintKey: 'auth_login.antigravity_oauth_hint', urlLabelKey: 'auth_login.antigravity_oauth_url_label', icon: iconAntigravity },
{ id: 'gemini-cli', titleKey: 'auth_login.gemini_cli_oauth_title', hintKey: 'auth_login.gemini_cli_oauth_hint', urlLabelKey: 'auth_login.gemini_cli_oauth_url_label', icon: iconGemini },
{ id: 'kimi', titleKey: 'auth_login.kimi_oauth_title', hintKey: 'auth_login.kimi_oauth_hint', urlLabelKey: 'auth_login.kimi_oauth_url_label', icon: { light: iconKimiLight, dark: iconKimiDark } }
{ id: 'kimi', titleKey: 'auth_login.kimi_oauth_title', hintKey: 'auth_login.kimi_oauth_hint', urlLabelKey: 'auth_login.kimi_oauth_url_label', icon: { light: iconKimiLight, dark: iconKimiDark } },
{ id: 'xai', titleKey: 'auth_login.xai_oauth_title', hintKey: 'auth_login.xai_oauth_hint', urlLabelKey: 'auth_login.xai_oauth_url_label', icon: { light: iconGrok, dark: iconGrokDark } }
];
const CALLBACK_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli'];
const CALLBACK_SUPPORTED: OAuthProvider[] = [
'codex',
'anthropic',
'antigravity',
'gemini-cli',
'xai'
];
const XAI_CALLBACK_URL = 'http://127.0.0.1:56121/callback';
const SUCCESS_RESET_DELAY_MS = 5000;
const getProviderI18nPrefix = (provider: OAuthProvider) => provider.replace('-', '_');
const getAuthKey = (provider: OAuthProvider, suffix: string) =>
@@ -80,6 +90,77 @@ const getIcon = (icon: string | { light: string; dark: string }, theme: 'light'
return typeof icon === 'string' ? icon : icon[theme];
};
const isAbsoluteUrl = (value: string): boolean => {
try {
new URL(value);
return true;
} catch {
return false;
}
};
const readQueryLikeCallbackInput = (value: string) => {
const trimmed = value.trim();
if (!trimmed) return null;
const queryStart = trimmed.indexOf('?');
const hashStart = trimmed.indexOf('#');
const rawParams =
queryStart >= 0
? trimmed.slice(queryStart + 1)
: hashStart >= 0
? trimmed.slice(hashStart + 1)
: trimmed;
if (!/(^|[&#?])(code|state|error)=/i.test(rawParams)) return null;
return new URLSearchParams(rawParams.replace(/^[?#]/, ''));
};
const extractDisplayedXaiCode = (value: string): string => {
const trimmed = value.trim();
const codeMatch = trimmed.match(/\bcode\s*[:=]\s*([^\s&]+)/i);
return (codeMatch?.[1] ?? trimmed).trim();
};
const buildXaiCallbackUrl = (input: string, state?: string): string | null => {
const trimmed = input.trim();
if (!trimmed) return null;
if (isAbsoluteUrl(trimmed)) return trimmed;
const params = readQueryLikeCallbackInput(trimmed);
if (params) {
const code = params.get('code')?.trim();
const error = params.get('error')?.trim();
const errorDescription = params.get('error_description')?.trim();
const callbackState = params.get('state')?.trim() || state?.trim();
if (!callbackState) return null;
const callbackUrl = new URL(XAI_CALLBACK_URL);
callbackUrl.searchParams.set('state', callbackState);
if (code) callbackUrl.searchParams.set('code', code);
if (error) callbackUrl.searchParams.set('error', error);
if (errorDescription) callbackUrl.searchParams.set('error_description', errorDescription);
return callbackUrl.toString();
}
const code = extractDisplayedXaiCode(trimmed);
const callbackState = state?.trim();
if (!code || !callbackState) return null;
const callbackUrl = new URL(XAI_CALLBACK_URL);
callbackUrl.searchParams.set('code', code);
callbackUrl.searchParams.set('state', callbackState);
return callbackUrl.toString();
};
const resolveCallbackUrl = (
provider: OAuthProvider,
input: string,
state?: string
): string | null => {
if (provider !== 'xai') return input.trim();
return buildXaiCallbackUrl(input, state);
};
export function OAuthPage() {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -262,9 +343,17 @@ export function OAuthPage() {
};
const submitCallback = async (provider: OAuthProvider) => {
const redirectUrl = (states[provider]?.callbackUrl || '').trim();
const callbackInput = (states[provider]?.callbackUrl || '').trim();
if (!callbackInput) {
showNotification(
t(provider === 'xai' ? 'auth_login.xai_callback_required' : 'auth_login.oauth_callback_required'),
'warning'
);
return;
}
const redirectUrl = resolveCallbackUrl(provider, callbackInput, states[provider]?.state);
if (!redirectUrl) {
showNotification(t('auth_login.oauth_callback_required'), 'warning');
showNotification(t(provider === 'xai' ? 'auth_login.xai_callback_state_missing' : 'auth_login.missing_state'), 'warning');
return;
}
updateProviderState(provider, {
@@ -434,8 +523,16 @@ export function OAuthPage() {
{canSubmitCallback && (
<div className={styles.callbackSection}>
<Input
label={t('auth_login.oauth_callback_label')}
hint={t('auth_login.oauth_callback_hint')}
label={t(
provider.id === 'xai'
? 'auth_login.xai_callback_label'
: 'auth_login.oauth_callback_label'
)}
hint={t(
provider.id === 'xai'
? 'auth_login.xai_callback_hint'
: 'auth_login.oauth_callback_hint'
)}
value={state.callbackUrl || ''}
onChange={(e) =>
updateProviderState(provider.id, {
@@ -444,7 +541,11 @@ export function OAuthPage() {
callbackError: undefined
})
}
placeholder={t('auth_login.oauth_callback_placeholder')}
placeholder={t(
provider.id === 'xai'
? 'auth_login.xai_callback_placeholder'
: 'auth_login.oauth_callback_placeholder'
)}
/>
<div className={styles.callbackActions}>
<Button
+37
View File
@@ -591,6 +591,43 @@
}
}
@include mobile {
.premiumPlanValue {
overflow: hidden;
background:
radial-gradient(
circle at 18% 24%,
rgba(255, 255, 255, 0.88) 0%,
rgba(255, 255, 255, 0.58) 18%,
rgba(255, 255, 255, 0) 42%
),
linear-gradient(135deg, #fff9e3 0%, #ffe07f 52%, #e0aa14 100%);
box-shadow:
0 1px 3px rgba(133, 92, 0, 0.16),
0 0 0 1px rgba(255, 255, 255, 0.22) inset,
0 0 10px rgba(255, 214, 98, 0.22);
&::before {
content: none;
}
}
:global([data-theme='dark']) .premiumPlanValue {
background:
radial-gradient(
circle at 18% 24%,
rgba(255, 229, 138, 0.28) 0%,
rgba(255, 214, 98, 0.14) 18%,
rgba(255, 214, 98, 0) 44%
),
linear-gradient(135deg, #4f3d0b 0%, #6e5510 48%, #8f6d10 100%);
box-shadow:
0 1px 6px rgba(0, 0, 0, 0.28),
0 0 0 1px rgba(255, 220, 120, 0.16) inset,
0 0 10px rgba(255, 196, 44, 0.18);
}
}
.fileCard {
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
+2 -1
View File
@@ -26,6 +26,7 @@ import iconKimiLight from '@/assets/icons/kimi-light.svg';
import iconKimiDark from '@/assets/icons/kimi-dark.svg';
import iconGlm from '@/assets/icons/glm.svg';
import iconGrok from '@/assets/icons/grok.svg';
import iconGrokDark from '@/assets/icons/grok-dark.svg';
import iconDeepseek from '@/assets/icons/deepseek.svg';
import iconMinimax from '@/assets/icons/minimax.svg';
import styles from './SystemPage.module.scss';
@@ -37,7 +38,7 @@ const MODEL_CATEGORY_ICONS: Record<string, string | { light: string; dark: strin
qwen: iconQwen,
kimi: { light: iconKimiLight, dark: iconKimiDark },
glm: iconGlm,
grok: iconGrok,
grok: { light: iconGrok, dark: iconGrokDark },
deepseek: iconDeepseek,
minimax: iconMinimax,
};
+9 -2
View File
@@ -9,7 +9,8 @@ export type OAuthProvider =
| 'anthropic'
| 'antigravity'
| 'gemini-cli'
| 'kimi';
| 'kimi'
| 'xai';
export interface OAuthStartResponse {
url: string;
@@ -20,7 +21,13 @@ export interface OAuthCallbackResponse {
status: 'ok';
}
const WEBUI_SUPPORTED: OAuthProvider[] = ['codex', 'anthropic', 'antigravity', 'gemini-cli'];
const WEBUI_SUPPORTED: OAuthProvider[] = [
'codex',
'anthropic',
'antigravity',
'gemini-cli',
'xai'
];
const CALLBACK_PROVIDER_MAP: Partial<Record<OAuthProvider, string>> = {
'gemini-cli': 'gemini'
};
+1
View File
@@ -14,6 +14,7 @@ export type AuthFileType =
| 'claude'
| 'codex'
| 'antigravity'
| 'xai'
| 'iflow'
| 'vertex'
| 'empty'
+56
View File
@@ -1,4 +1,5 @@
export type PayloadParamValueType = 'string' | 'number' | 'boolean' | 'json';
export type DisableImageGenerationMode = 'false' | 'true' | 'chat';
export type PayloadParamValidationErrorCode =
| 'payload_invalid_number'
| 'payload_invalid_boolean'
@@ -6,10 +7,13 @@ export type PayloadParamValidationErrorCode =
export type VisualConfigFieldPath =
| 'port'
| 'errorLogsMaxFiles'
| 'logsMaxTotalSizeMb'
| 'redisUsageQueueRetentionSeconds'
| 'requestRetry'
| 'maxRetryCredentials'
| 'maxRetryInterval'
| 'authAutoRefreshWorkers'
| 'streaming.keepaliveSeconds'
| 'streaming.bootstrapRetries'
| 'streaming.nonstreamKeepaliveInterval';
@@ -27,10 +31,22 @@ export type PayloadParamEntry = {
value: string;
};
export type PayloadHeaderEntry = {
id: string;
name: string;
value: string;
};
export type PayloadModelEntry = {
id: string;
name: string;
protocol?: string;
fromProtocol?: string;
headers?: PayloadHeaderEntry[];
match?: PayloadParamEntry[];
notMatch?: PayloadParamEntry[];
exist?: string[];
notExist?: string[];
};
export type PayloadRule = {
@@ -60,6 +76,7 @@ export type VisualConfigValues = {
rmAllowRemote: boolean;
rmSecretKey: string;
rmDisableControlPanel: boolean;
rmDisableAutoUpdatePanel: boolean;
rmPanelRepo: string;
authDir: string;
apiKeysText: string;
@@ -67,11 +84,18 @@ export type VisualConfigValues = {
commercialMode: boolean;
loggingToFile: boolean;
logsMaxTotalSizeMb: string;
errorLogsMaxFiles: string;
usageStatisticsEnabled: boolean;
redisUsageQueueRetentionSeconds: string;
proxyUrl: string;
forceModelPrefix: boolean;
passthroughHeaders: boolean;
requestRetry: string;
maxRetryCredentials: string;
maxRetryInterval: string;
disableCooling: boolean;
disableImageGeneration: DisableImageGenerationMode;
authAutoRefreshWorkers: string;
quotaSwitchProject: boolean;
quotaSwitchPreviewModel: boolean;
quotaAntigravityCredits: boolean;
@@ -79,6 +103,18 @@ export type VisualConfigValues = {
routingSessionAffinity: boolean;
routingSessionAffinityTTL: string;
wsAuth: boolean;
enableGeminiCliEndpoint: boolean;
antigravitySignatureCacheEnabled: boolean;
antigravitySignatureBypassStrict: boolean;
claudeHeaderUserAgent: string;
claudeHeaderPackageVersion: string;
claudeHeaderRuntimeVersion: string;
claudeHeaderOs: string;
claudeHeaderArch: string;
claudeHeaderTimeout: string;
claudeHeaderStabilizeDeviceProfile: boolean;
codexHeaderUserAgent: string;
codexHeaderBetaFeatures: string;
payloadDefaultRules: PayloadRule[];
payloadDefaultRawRules: PayloadRule[];
payloadOverrideRules: PayloadRule[];
@@ -101,6 +137,7 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
rmAllowRemote: false,
rmSecretKey: '',
rmDisableControlPanel: false,
rmDisableAutoUpdatePanel: false,
rmPanelRepo: '',
authDir: '',
apiKeysText: '',
@@ -108,11 +145,18 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
commercialMode: false,
loggingToFile: false,
logsMaxTotalSizeMb: '',
errorLogsMaxFiles: '',
usageStatisticsEnabled: false,
redisUsageQueueRetentionSeconds: '',
proxyUrl: '',
forceModelPrefix: false,
passthroughHeaders: false,
requestRetry: '',
maxRetryCredentials: '',
maxRetryInterval: '',
disableCooling: false,
disableImageGeneration: 'false',
authAutoRefreshWorkers: '',
quotaSwitchProject: true,
quotaSwitchPreviewModel: true,
quotaAntigravityCredits: false,
@@ -120,6 +164,18 @@ export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
routingSessionAffinity: false,
routingSessionAffinityTTL: '',
wsAuth: false,
enableGeminiCliEndpoint: false,
antigravitySignatureCacheEnabled: true,
antigravitySignatureBypassStrict: false,
claudeHeaderUserAgent: '',
claudeHeaderPackageVersion: '',
claudeHeaderRuntimeVersion: '',
claudeHeaderOs: '',
claudeHeaderArch: '',
claudeHeaderTimeout: '',
claudeHeaderStabilizeDeviceProfile: false,
codexHeaderUserAgent: '',
codexHeaderBetaFeatures: '',
payloadDefaultRules: [],
payloadDefaultRawRules: [],
payloadOverrideRules: [],
+4 -2
View File
@@ -58,14 +58,16 @@ export const OAUTH_CARD_IDS = [
'anthropic-oauth-card',
'antigravity-oauth-card',
'gemini-cli-oauth-card',
'kimi-oauth-card'
'kimi-oauth-card',
'xai-oauth-card'
];
export const OAUTH_PROVIDERS = {
CODEX: 'codex',
ANTHROPIC: 'anthropic',
ANTIGRAVITY: 'antigravity',
GEMINI_CLI: 'gemini-cli',
KIMI: 'kimi'
KIMI: 'kimi',
XAI: 'xai'
} as const;
// API 端点
-1
View File
@@ -21,7 +21,6 @@
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}