mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Compare commits
14 Commits
@@ -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
@@ -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(状态管理)
|
||||
- Axios(HTTP 客户端)
|
||||
- react-router-dom v7(HashRouter)
|
||||
- Chart.js(数据可视化)
|
||||
- Motion(动效)
|
||||
- CodeMirror 6(YAML 编辑器)
|
||||
- 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`)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
Generated
+169
-180
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Булево",
|
||||
|
||||
@@ -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": "",
|
||||
@@ -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": "布尔",
|
||||
|
||||
@@ -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": "",
|
||||
@@ -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": "布林",
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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'
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export type AuthFileType =
|
||||
| 'claude'
|
||||
| 'codex'
|
||||
| 'antigravity'
|
||||
| 'xai'
|
||||
| 'iflow'
|
||||
| 'vertex'
|
||||
| 'empty'
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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 端点
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user