feat(plugins): 新增 AI 监控插件并优化日志管理
- 新增 AI 监控插件 (ai-monitor),支持全链路协议转换监控 - 捕获 AI 接口请求参数(转换前后) - 监控流式和非流式响应(转换前后) - 支持内部请求转换监控 - 新增日志清空功能,支持前端和服务器端同时清空当日日志 - 默认禁用 api-potluck 和 ai-monitor 插件 - 更新多语言文档和配置示例 - 优化提供商适配器开发指南
This commit is contained in:
parent
245583b96a
commit
ce7d78f7d0
17 changed files with 581 additions and 293 deletions
111
OPENCODE_CONFIG_EXAMPLE.md
Normal file
111
OPENCODE_CONFIG_EXAMPLE.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# OpenCode 配置示例及重点解释
|
||||
|
||||
本文档提供了一个典型的 `opencode` 配置文件示例,并对其中的关键配置项进行了详细解释,帮助您快速理解如何配置不同的 AI 服务提供商。
|
||||
|
||||
## 配置示例 (`config.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": [],
|
||||
"provider": {
|
||||
"kiro": {
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"name": "AIClient2API-kiro",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:3000/claude-kiro-oauth/v1",
|
||||
"apiKey": "123456"
|
||||
},
|
||||
"models": {
|
||||
"claude-opus-4-5": {
|
||||
"name": "Claude Opus 4.5 Kiro"
|
||||
},
|
||||
"claude-sonnet-4-5-20250929": {
|
||||
"name": "Claude Sonnet 4.5 Kiro"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qwen": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "AIClient2API-qwen",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:3000/openai-qwen-oauth/v1",
|
||||
"apiKey": "123456"
|
||||
},
|
||||
"models": {
|
||||
"qwen3-coder-plus": {
|
||||
"name": "Qwen3 Coder Plus Openai "
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini-antigravity": {
|
||||
"npm": "@ai-sdk/google",
|
||||
"name": "AIClient2API-antigravity",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:3000/gemini-antigravity/v1beta",
|
||||
"apiKey": "123456"
|
||||
},
|
||||
"models": {
|
||||
"gemini-2.5-flash-preview": {
|
||||
"name": "gemini-2.5-flash-antigravity"
|
||||
},
|
||||
"gemini-3-flash-preview": {
|
||||
"name": "gemini-3-flash-antigravity"
|
||||
},
|
||||
"gemini-3-pro-preview": {
|
||||
"name": "gemini-3-pro-antigravity"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gemini-cli": {
|
||||
"npm": "@ai-sdk/google",
|
||||
"name": "AIClient2API-geminicli",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:3000/v1beta",
|
||||
"apiKey": "123456"
|
||||
},
|
||||
"models": {
|
||||
"gemini-2.5-flash-preview": {
|
||||
"name": "gemini-2.5-flash-geminicli"
|
||||
},
|
||||
"gemini-3-flash-preview": {
|
||||
"name": "gemini-3-flash-geminicli"
|
||||
},
|
||||
"gemini-3-pro-preview": {
|
||||
"name": "gemini-3-pro-geminicli"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "https://opencode.ai/config.json"
|
||||
}
|
||||
```
|
||||
|
||||
## 配置重点解释
|
||||
|
||||
### 1. `provider` (服务提供商配置)
|
||||
这是配置的核心部分,每个键(如 `kiro`, `qwen`, `gemini-cli`)代表一个独立的服务提供商实例。
|
||||
|
||||
* **`npm` (SDK 适配器)**:
|
||||
* 指定底层使用的 AI SDK。例如:
|
||||
* `@ai-sdk/anthropic`: 用于 Anthropic (Claude) 系列模型。
|
||||
* `@ai-sdk/openai-compatible`: 用于兼容 OpenAI 接口标准的模型(如通义千问 Qwen)。
|
||||
* `@ai-sdk/google`: 用于 Google Gemini 系列模型。
|
||||
* **重点**: 必须确保 `npm` 字段与您要使用的模型协议匹配,否则会导致连接失败。
|
||||
|
||||
* **`options` (连接参数)**:
|
||||
* **`baseURL`**: API 的访问地址。在示例中,许多是内网或中转地址(如 `http://localhost:3000/...`)。
|
||||
* **`apiKey`**: 访问 API 所需的身份验证密钥。
|
||||
|
||||
* **`models` (模型映射)**:
|
||||
* 定义该提供商下可用的模型列表。
|
||||
* **键名 (ID)**: 实际调用时使用的模型 ID(例如 `claude-opus-4-5`)。
|
||||
* **`name`**: 在 UI 界面上显示的友好名称。
|
||||
* **重点**: 这里的键名必须与服务端实际支持的模型标识符一致。
|
||||
|
||||
### 2. 区分同类型的不同实例
|
||||
在示例中,有两个 `gemini` 相关的配置:`gemini-antigravity` 和 `gemini-cli`。
|
||||
* 它们虽然都使用 `@ai-sdk/google`,但通过不同的 `baseURL` 区分。
|
||||
* 这允许您在同一配置中接入来自不同网关或环境的同类模型,并通过自定义的 `name`(如 `gemini-2.5-flash-antigravity` vs `gemini-2.5-flash-geminicli`)在前端进行区分。
|
||||
|
||||
### 3. `$schema`
|
||||
* 用于提供 JSON 模式验证。在支持的编辑器(如 VS Code)中,它可以为您提供自动补全和实时错误检查。
|
||||
|
|
@ -6,13 +6,17 @@
|
|||
|
||||
1. **后端常量定义**:在 `src/utils/common.js` 中添加标识。
|
||||
2. **核心 Service 开发**:在 `src/providers/` 实现 API 请求逻辑。
|
||||
3. **适配器注册**:在 `src/providers/adapter.js` 注册。
|
||||
3. **适配器注册**:在 `src/providers/adapter.js` 注册并实现适配器类。
|
||||
4. **模型与号池配置**:在 `src/providers/provider-models.js` 和 `src/providers/provider-pool-manager.js` 配置。
|
||||
5. **前端 UI 全方位调整**:
|
||||
* `static/app/provider-manager.js`:号池显示与顺序。
|
||||
* `static/app/file-upload.js`:上传路径映射。
|
||||
* `static/app/modal.js`:配置字段显示顺序。
|
||||
* `static/app/utils.js`:定义配置字段元数据。
|
||||
* `static/components/section-config.html`:配置按钮。
|
||||
* `static/components/section-guide.html`:使用指南。
|
||||
* `static/app/routing-examples.js`:路由调用示例。
|
||||
* `src/handlers/ollama-handler.js`:Ollama 协议前缀与支持映射。
|
||||
6. **系统级映射(必做)**:在 OAuth 处理器、凭据关联工具、用量统计等模块中建立映射。
|
||||
|
||||
---
|
||||
|
|
@ -25,36 +29,49 @@
|
|||
### 2.2 核心 Service (Core)
|
||||
在 `src/providers/` 下创建新目录并实现 `NewProviderApiService` 类。
|
||||
**必选方法**:`constructor(config)`, `initialize()`, `listModels()`, `generateContent()`, `generateContentStream()`。
|
||||
**可选功能**:若支持用量查询,需实现 `getUsageLimits()`;若支持 Token 统计,需实现 `countTokens()`。
|
||||
|
||||
### 2.3 注册适配器
|
||||
在 [`src/providers/adapter.js`](src/providers/adapter.js) 中继承 `ApiServiceAdapter`,并在 `getServiceAdapter` 工厂方法中添加 `switch` 分支。
|
||||
在 [`src/providers/adapter.js`](src/providers/adapter.js) 中:
|
||||
1. 继承 `ApiServiceAdapter` 实现特定提供商的适配器类。
|
||||
2. 适配器类需按需重写 `generateContent`, `generateContentStream`, `listModels`, `getUsageLimits`, `countTokens`, `refreshToken` 等方法,并转发给核心 Service。
|
||||
3. 在 `getServiceAdapter` 工厂方法中添加对应的 `switch` 分支,根据 `MODEL_PROVIDER` 返回实例。
|
||||
|
||||
### 2.4 模型与号池默认配置
|
||||
* **模型列表**:在 [`src/providers/provider-models.js`](src/providers/provider-models.js) 的 `PROVIDER_MODELS` 对象中添加默认支持的模型 ID。
|
||||
* **健康检查默认值**:在 [`src/providers/provider-pool-manager.js`](src/providers/provider-pool-manager.js) 的 `DEFAULT_HEALTH_CHECK_MODELS` 中指定用于健康检查的默认模型。
|
||||
* **健康检查默认值**:在 [`src/providers/provider-pool-manager.js`](src/providers/provider-pool-manager.js) 的以下位置配置:
|
||||
* `DEFAULT_HEALTH_CHECK_MODELS`:指定用于健康检查的默认模型。
|
||||
* `checkAndRefreshExpiringNodes`:指定凭据文件路径键名。
|
||||
* `_buildHealthCheckRequests`:若有特殊请求格式需求,需在此添加逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 3. 前端界面调整
|
||||
|
||||
### 3.1 号池显示逻辑 ([`static/app/provider-manager.js`](static/app/provider-manager.js))
|
||||
* **显示顺序**:将新标识添加到 `providerDisplayOrder` 数组。
|
||||
* **授权按钮**:若支持 OAuth,在 `generateAuthButton` 的 `oauthProviders` 数组中添加标识。
|
||||
* **路径提示**:在 `getAuthFilePath` 中返回凭据文件的默认建议路径。
|
||||
### 3.1 字段定义与元数据 ([`static/app/utils.js`](static/app/utils.js))
|
||||
在 `getProviderTypeFields` 函数中定义该提供商所需的配置字段(如 API Key, Base URL, 凭据路径等),指定字段类型和占位符。
|
||||
|
||||
### 3.2 凭据上传路由 ([`static/app/file-upload.js`](static/app/file-upload.js))
|
||||
### 3.2 字段显示顺序 ([`static/app/modal.js`](static/app/modal.js))
|
||||
在 `getFieldOrder` 函数的 `fieldOrderMap` 中添加新提供商的字段显示顺序。
|
||||
|
||||
### 3.3 号池显示逻辑 ([`static/app/provider-manager.js`](static/app/provider-manager.js))
|
||||
* **显示顺序**:将新标识和显示名称添加到 `providerConfigs` 数组。
|
||||
* **授权按钮**:若支持 OAuth,在 `generateAuthButton` 的 `oauthProviders` 数组中添加标识。
|
||||
* **认证逻辑**:若支持 OAuth 或批量导入,需在 `handleGenerateAuthUrl` 中实现相应的触发逻辑(如弹出认证方式选择器)。
|
||||
|
||||
### 3.4 凭据上传路由 ([`static/app/file-upload.js`](static/app/file-upload.js))
|
||||
* 修改 `getProviderKey`,建立提供商标识与 `configs/` 子目录名的映射(例如:`new-provider-api` -> `new-provider`)。
|
||||
|
||||
### 3.3 凭据文件管理筛选器
|
||||
### 3.5 凭据文件管理筛选器
|
||||
需要在以下三个位置添加新提供商的筛选支持:
|
||||
|
||||
#### 3.3.1 HTML 筛选器选项 ([`static/components/section-upload-config.html`](static/components/section-upload-config.html))
|
||||
#### 3.5.1 HTML 筛选器选项 ([`static/components/section-upload-config.html`](static/components/section-upload-config.html))
|
||||
在 `id="configProviderFilter"` 的 `<select>` 元素中添加新的 `<option>`:
|
||||
```html
|
||||
<option value="new-provider-type" data-i18n="upload.providerFilter.newProvider">New Provider OAuth</option>
|
||||
```
|
||||
|
||||
#### 3.3.2 JavaScript 提供商映射 ([`static/app/upload-config-manager.js`](static/app/upload-config-manager.js))
|
||||
#### 3.5.2 JavaScript 提供商映射 ([`static/app/upload-config-manager.js`](static/app/upload-config-manager.js))
|
||||
在 `detectProviderFromPath()` 函数的 `providerMappings` 数组中添加映射关系:
|
||||
```javascript
|
||||
{
|
||||
|
|
@ -65,22 +82,28 @@
|
|||
}
|
||||
```
|
||||
|
||||
#### 3.3.3 多语言文案 ([`static/app/i18n.js`](static/app/i18n.js))
|
||||
在中文和英文的翻译对象中添加筛选器文案:
|
||||
#### 3.5.3 多语言文案 ([`static/app/i18n.js`](static/app/i18n.js))
|
||||
在中文和英文的翻译对象中添加筛选器、配置项、认证步骤等相关文案:
|
||||
```javascript
|
||||
// 中文版本 (zh-CN)
|
||||
'upload.providerFilter.newProvider': 'New Provider OAuth',
|
||||
'config.newProvider.apiKey': 'API 密钥',
|
||||
|
||||
// 英文版本 (en-US)
|
||||
'upload.providerFilter.newProvider': 'New Provider OAuth',
|
||||
'config.newProvider.apiKey': 'API Key',
|
||||
```
|
||||
|
||||
### 3.4 配置管理界面 ([`static/components/section-config.html`](static/components/section-config.html))
|
||||
### 3.6 配置管理界面 ([`static/components/section-config.html`](static/components/section-config.html))
|
||||
* **必须添加**:在 `id="modelProvider"`(初始化提供商选择)容器中添加对应的 `provider-tag` 按钮。
|
||||
* **可选添加**:在 `id="proxyProviders"`(代理开关)中同步添加。
|
||||
|
||||
### 3.5 指南与教程 ([`static/components/section-guide.html`](static/components/section-guide.html))
|
||||
* 在"项目简介"和"客户端配置指南"中添加新提供商的调用示例(如 `{provider}/v1/chat/completions`)。
|
||||
### 3.7 路由调用示例 ([`static/app/routing-examples.js`](static/app/routing-examples.js))
|
||||
在 `routingConfigs` 数组中添加该提供商的路径定义,并在 `generateCurlExample` 中处理协议转换逻辑说明。
|
||||
|
||||
### 3.8 指南与教程 ([`static/components/section-guide.html`](static/components/section-guide.html))
|
||||
* 在"支持的模型提供商"中添加新提供商的介绍和支持情况(Badge)。
|
||||
* 在"客户端配置指南"中补充该提供商的调用路径提示。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -104,13 +127,21 @@
|
|||
### 4.3 用量统计映射 ([`src/ui-modules/usage-api.js`](src/ui-modules/usage-api.js))
|
||||
* 将标识添加到 `supportedProviders` 数组。
|
||||
* 在 `credPathKey` 映射中添加路径键名,以便前端能展示每个账号的配额/用量。
|
||||
* 在 `getAdapterUsage` 中根据需要处理原始数据的格式化。
|
||||
|
||||
### 4.4 OAuth 处理器 ([`src/ui-modules/oauth-api.js`](src/ui-modules/oauth-api.js))
|
||||
若支持 OAuth,需在 `handleGenerateAuthUrl` 中分发到相应的认证处理器。
|
||||
### 4.4 OAuth 处理器
|
||||
* **处理器逻辑**:在 `src/auth/oauth-handlers.js` 中导出处理函数。
|
||||
* **路由分发**:在 [`src/ui-modules/oauth-api.js`](src/ui-modules/oauth-api.js) 的 `handleGenerateAuthUrl` 中分发到相应的处理器。
|
||||
* **回调处理**:若涉及 HTTP 回调,需在 `src/auth/` 下实现回调服务器逻辑。
|
||||
|
||||
### 4.5 Ollama 协议映射 ([`src/handlers/ollama-handler.js`](src/handlers/ollama-handler.js))
|
||||
* 在 `MODEL_PREFIX_MAP` 中添加该提供商对应的日志/显示前缀。
|
||||
* 在 `supportedProviders` 数组中添加该提供商标识,以支持 Ollama 协议转换。
|
||||
|
||||
---
|
||||
|
||||
## 5. 注意事项
|
||||
1. **协议对齐**:本项目内部默认使用 Gemini 协议。若上游为 OpenAI 协议,需在 `src/convert/` 实现转换。
|
||||
2. **安全**:不要在 Core 代码中硬编码 Key,始终从 `config` 中读取动态注入的凭据。
|
||||
3. **异常捕获**:Core 代码必须抛出标准错误(包含 status),以便号池管理器识别并自动隔离失效账号。
|
||||
1. **协议对齐**:本项目内部默认使用 Gemini 协议。若上游为 OpenAI 协议,需在 `src/convert/` 实现转换,或在 Core Service 中自行处理。
|
||||
2. **安全性**:不要在 Core 代码中硬编码 Key,始终从 `config` 中读取动态注入的凭据。
|
||||
3. **异常捕获**:Core 代码必须抛出标准错误(包含 status),以便号池管理器识别并自动隔离失效账号。401/403 错误通常触发 UUID 刷新或凭据切换。
|
||||
4. **异步刷新**:利用 V2 架构的读写分离,耗时的认证逻辑应放入 `refreshToken` 并在后台异步执行。
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
> <details>
|
||||
> <summary>クリックして詳細なバージョン履歴を展開</summary>
|
||||
>
|
||||
> - **2026.01.25** - AI 監視プラグインの強化:AI プロトコル変換前後のリクエストパラメータとレスポンスの監視をサポート。ログ管理の最適化:統一されたログ形式、ビジュアル設定
|
||||
> - **2026.01.15** - プロバイダープールマネージャーの最適化:非同期リフレッシュキューメカニズム、バッファキュー重複排除、グローバル並行制御、ノードウォームアップと自動期限切れ検出を追加
|
||||
> - **2026.01.07** - iFlowプロトコルサポートの追加、OAuth認証方式でQwen、Kimi、DeepSeek、GLMシリーズモデルにアクセス可能、自動トークンリフレッシュ機能をサポート
|
||||
> - **2026.01.03** - テーマ切替機能を追加し、プロバイダープール初期化を最適化、プロバイダーのデフォルト設定を使用するフォールバック戦略を削除
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
> <details>
|
||||
> <summary>点击展开查看详细版本历史</summary>
|
||||
>
|
||||
> - **2026.01.25** - 增强 AI 监控插件:支持监控 AI 协议转换前后的请求参数和响应。优化日志管理:统一日志格式,可视化配置
|
||||
> - **2026.01.15** - 优化提供商池管理器:新增异步刷新队列机制、缓冲队列去重、全局并发控制,支持节点预热和自动过期检测
|
||||
> - **2026.01.07** - 新增 iFlow 协议支持,通过 OAuth 认证方式访问 Qwen、Kimi、DeepSeek 和 GLM 系列模型,支持自动 token 刷新功能
|
||||
> - **2026.01.03** - 新增主题切换功能并优化提供商池初始化,移除使用提供商默认配置的降级策略
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
> <details>
|
||||
> <summary>Click to expand detailed version history</summary>
|
||||
>
|
||||
> - **2026.01.25** - Enhanced AI Monitor plugin: supports monitoring request parameters and responses before and after AI protocol conversion. Optimized log management: unified log format, visual configuration
|
||||
> - **2026.01.15** - Optimized provider pool manager: added async refresh queue mechanism, buffer queue deduplication, global concurrency control, node warmup and automatic expiry detection
|
||||
> - **2026.01.07** - Added iFlow protocol support, enabling access to Qwen, Kimi, DeepSeek, and GLM series models via OAuth authentication with automatic token refresh
|
||||
> - **2026.01.03** - Added theme switching functionality and optimized provider pool initialization, removed the fallback strategy of using provider default configuration
|
||||
|
|
|
|||
238
UI_README.md
238
UI_README.md
|
|
@ -1,238 +0,0 @@
|
|||
# AIClient2API 可视化管理控制台
|
||||
|
||||
## 概述
|
||||
|
||||
AIClient2API 现在包含一个功能完整的可视化 Web UI 管理控制台,允许您通过浏览器轻松管理配置、监控提供商池状态、查看实时日志、配置AI模型提供商等。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🎨 现代化界面
|
||||
- 响应式设计,支持桌面和移动设备
|
||||
- 直观的仪表盘展示系统统计信息
|
||||
- 侧边栏导航,方便快速切换功能模块
|
||||
- 协议标签切换(OpenAI协议/Claude协议)
|
||||
|
||||
### 📊 实时监控
|
||||
- **仪表盘**:显示运行时间、系统信息、Node.js版本、服务器时间、内存使用
|
||||
- **提供商池管理**:查看各提供商账户状态、使用统计、错误率
|
||||
- **活动统计**:活动连接、活跃提供商、健康提供商数量
|
||||
|
||||
### ⚙️ 配置管理
|
||||
- 在线修改 API 密钥、监听地址、端口
|
||||
- 支持多种模型提供商:
|
||||
- **Gemini CLI OAuth** - 支持突破限制的Gemini访问
|
||||
- **OpenAI Custom** - 自定义OpenAI API配置
|
||||
- **Claude Custom** - 自定义Claude API配置
|
||||
- **Claude Kiro OAuth** - 突破限制/免费使用的Claude服务
|
||||
- **Qwen OAuth** - 通义千问OAuth认证
|
||||
- **OpenAI Responses** - OpenAI新版本API
|
||||
- 编辑系统提示词
|
||||
- 高级配置选项:
|
||||
- 系统提示文件路径和模式
|
||||
- 提示日志配置
|
||||
- 请求重试机制(最大重试次数、基础延迟)
|
||||
- OAuth令牌自动刷新设置
|
||||
- 提供商池配置文件路径
|
||||
|
||||
### 🔧 配置管理
|
||||
- 搜索配置文件功能
|
||||
- 按关联状态过滤(已关联/未关联)
|
||||
- 配置文件列表展示
|
||||
- 配置统计信息(总数、已关联数、未关联数)
|
||||
- 实时刷新配置列表
|
||||
|
||||
### 🛣️ 路径路由调用示例
|
||||
- **即时切换**:通过修改URL路径即可切换不同的AI模型提供商
|
||||
- **跨协议调用**:支持OpenAI协议调用Claude模型,或Claude协议调用OpenAI模型
|
||||
- **客户端配置指导**:为Cherry-Studio、NextChat、Cline等客户端提供配置示例
|
||||
- 提供完整的curl使用示例
|
||||
- 一键复制端点路径功能
|
||||
|
||||
支持的路由路径示例:
|
||||
- `/gemini-cli-oauth/v1/chat/completions` - Gemini CLI OAuth (OpenAI协议)
|
||||
- `/gemini-cli-oauth/v1/messages` - Gemini CLI OAuth (Claude协议)
|
||||
- `/openai-qwen-oauth/v1/chat/completions` - Qwen OAuth (OpenAI协议)
|
||||
- `/openai-qwen-oauth/v1/messages` - Qwen OAuth (Claude协议)
|
||||
- `/claude-custom/v1/chat/completions` - Claude Custom (OpenAI协议)
|
||||
- `/claude-custom/v1/messages` - Claude Custom (Claude协议)
|
||||
- `/claude-kiro-oauth/v1/chat/completions` - Claude Kiro OAuth (OpenAI协议)
|
||||
- `/claude-kiro-oauth/v1/messages` - Claude Kiro OAuth (Claude协议)
|
||||
- `/openai-custom/v1/chat/completions` - OpenAI Custom (OpenAI协议)
|
||||
- `/openai-custom/v1/messages` - OpenAI Custom (Claude协议)
|
||||
|
||||
### 📜 实时日志
|
||||
- 实时显示服务器输出日志
|
||||
- 清空日志功能
|
||||
- 自动滚动和手动滚动切换
|
||||
- 日志缓冲区管理
|
||||
|
||||
### 🔔 通知系统
|
||||
- 操作成功/失败提示
|
||||
- 优雅的 Toast 通知
|
||||
- 3秒自动消失
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 启动服务器
|
||||
|
||||
服务器启动时会自动打开浏览器到管理控制台:
|
||||
|
||||
```bash
|
||||
node src/api-server.js --port 3000 --api-key 123456
|
||||
```
|
||||
|
||||
访问地址:http://127.0.0.1:3000/
|
||||
|
||||
### 界面导航
|
||||
|
||||
1. **仪表盘** - 系统概览、统计信息和路径路由示例
|
||||
2. **配置管理** - 修改服务器配置和提供商设置
|
||||
3. **提供商池管理** - 管理多个API提供商账户
|
||||
4. **配置管理** - 管理配置文件和搜索过滤
|
||||
5. **实时日志** - 查看服务器运行日志
|
||||
|
||||
## API 端点
|
||||
|
||||
管理控制台使用以下 RESTful API 端点:
|
||||
|
||||
### 配置管理
|
||||
- `GET /api/config` - 获取当前配置
|
||||
- `POST /api/config` - 更新配置
|
||||
- `GET /api/configs` - 获取配置文件列表
|
||||
- `POST /api/configs/search` - 搜索配置文件
|
||||
|
||||
### 系统信息
|
||||
- `GET /api/system` - 获取系统信息
|
||||
- `GET /api/providers` - 获取提供商池信息
|
||||
|
||||
### 实时数据
|
||||
- `GET /api/events` - Server-Sent Events 流,用于实时更新
|
||||
|
||||
### 静态文件
|
||||
- `GET /` - 主页面
|
||||
- `GET /index.html` - HTML页面
|
||||
- `GET /app/styles.css` - 样式文件
|
||||
- `GET /app/mobile.css` - 移动端样式
|
||||
- `GET /app/app.js` - JavaScript主逻辑
|
||||
- `GET /app/utils.js` - 工具函数
|
||||
- `GET /app/config-manager.js` - 配置管理
|
||||
- `GET /app/provider-manager.js` - 提供商管理
|
||||
- `GET /app/event-stream.js` - 事件流处理
|
||||
- `GET /app/event-handlers.js` - 事件处理器
|
||||
- `GET /app/navigation.js` - 导航逻辑
|
||||
- `GET /app/modal.js` - 模态框组件
|
||||
- `GET /app/file-upload.js` - 文件上传
|
||||
- `GET /app/upload-config-manager.js` - 配置管理
|
||||
- `GET /app/routing-examples.js` - 路由示例
|
||||
- `GET /app/constants.js` - 常量定义
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 前端技术栈
|
||||
- **HTML5** - 语义化结构,支持无障碍访问
|
||||
- **CSS3** - 现代化样式(CSS Variables、Flexbox、Grid)
|
||||
- **JavaScript (ES6+)** - 模块化交互逻辑
|
||||
- **Server-Sent Events** - 实时数据推送
|
||||
- **Font Awesome 6.4.0** - 图标库
|
||||
|
||||
### 后端集成
|
||||
- 集成到现有 `api-server.js` 中
|
||||
- 零额外依赖
|
||||
- ES 模块语法
|
||||
- 异步处理
|
||||
|
||||
### 实时通信
|
||||
使用 Server-Sent Events 实现实时双向通信:
|
||||
- 自动广播日志到所有连接的客户端
|
||||
- 每5秒发送统计更新
|
||||
- 轻量级实现,无需 WebSocket
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
static/
|
||||
├── index.html # 主页面
|
||||
└── app/
|
||||
├── styles.css # 主样式文件
|
||||
├── mobile.css # 移动端样式
|
||||
├── app.js # 主应用逻辑
|
||||
├── constants.js # 常量定义
|
||||
├── utils.js # 工具函数
|
||||
├── config-manager.js # 配置管理
|
||||
├── provider-manager.js # 提供商管理
|
||||
├── upload-config-manager.js # 上传配置管理
|
||||
├── event-stream.js # 事件流处理
|
||||
├── event-handlers.js # 事件处理器
|
||||
├── navigation.js # 导航逻辑
|
||||
├── modal.js # 模态框组件
|
||||
├── file-upload.js # 文件上传
|
||||
└── routing-examples.js # 路由示例
|
||||
```
|
||||
|
||||
## 支持的提供商
|
||||
|
||||
### 突破限制类型
|
||||
- **Gemini CLI OAuth** - 通过OAuth突破Gemini API限制
|
||||
- **Claude Kiro OAuth** - 免费使用的Claude服务
|
||||
- **Qwen OAuth** - 通义千问OAuth认证
|
||||
|
||||
### 官方API/三方类型
|
||||
- **OpenAI Custom** - 自定义OpenAI API端点
|
||||
- **Claude Custom** - 自定义Claude API端点
|
||||
- **OpenAI Responses** - OpenAI最新版本API
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- Chrome 60+
|
||||
- Firefox 55+
|
||||
- Safari 11+
|
||||
- Edge 79+
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 服务器启动后浏览器没有自动打开?
|
||||
A: 检查是否使用 localhost 或 127.0.0.1 启动服务器。自动打开仅在本地部署时启用。
|
||||
|
||||
### Q: 如何使用路径路由功能?
|
||||
A: 在仪表盘的"路径路由调用示例"中,查看不同提供商的使用示例。修改客户端的API端点URL即可切换不同的AI模型。
|
||||
|
||||
### Q: 如何配置OAuth提供商?
|
||||
A: 在配置管理页面选择对应的OAuth提供商,填写项目ID和OAuth凭据(支持文件路径或Base64编码)。
|
||||
|
||||
### Q: 上传配置文件有什么作用?
|
||||
A: 上传配置文件可以将本地配置文件上传到服务器,方便管理和在不同环境间同步配置。
|
||||
|
||||
### Q: 如何查看更详细的提供商信息?
|
||||
A: 在"提供商池"页面可以看到每个提供商的使用次数、错误次数、最后使用时间等详细信息。
|
||||
|
||||
### Q: 配置修改后需要重启服务器吗?
|
||||
A: 大部分配置(如系统提示、API密钥)会立即生效,但网络端口等更改需要重启服务器。
|
||||
|
||||
### Q: 支持哪些客户端配置?
|
||||
A: 支持Cherry-Studio、NextChat、Cline等主流AI客户端,只需将API端点设置为对应的路由路径即可。
|
||||
|
||||
## 路由使用指南
|
||||
|
||||
### 1. 选择提供商
|
||||
根据需要访问的AI模型选择对应的提供商路径。
|
||||
|
||||
### 2. 选择协议格式
|
||||
- **OpenAI协议**:使用 `/v1/chat/completions` 端点
|
||||
- **Claude协议**:使用 `/v1/messages` 端点
|
||||
|
||||
### 3. 配置客户端
|
||||
在AI客户端中设置:
|
||||
- **API端点**:`http://localhost:3000/{提供商路径}/{协议路径}`
|
||||
- **API密钥**:对应提供商的密钥
|
||||
- **模型名称**:使用提供商支持的具体模型
|
||||
|
||||
### 4. 发送请求
|
||||
使用对应的协议格式发送请求,可以实现跨协议调用。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个管理控制台!
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目使用与主项目相同的许可证。
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"plugins": {
|
||||
"api-potluck": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"description": "API 大锅饭 - Key 管理和用量统计插件"
|
||||
},
|
||||
"default-auth": {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ import path from 'path';
|
|||
// 插件配置文件路径
|
||||
const PLUGINS_CONFIG_FILE = path.join(process.cwd(), 'configs', 'plugins.json');
|
||||
|
||||
// 默认禁用的插件列表
|
||||
const DEFAULT_DISABLED_PLUGINS = ['api-potluck', 'ai-monitor'];
|
||||
|
||||
/**
|
||||
* 插件类型常量
|
||||
*/
|
||||
|
|
@ -105,16 +108,20 @@ class PluginManager {
|
|||
const plugin = pluginModule.default || pluginModule;
|
||||
|
||||
if (plugin && plugin.name) {
|
||||
// 检查是否在默认禁用列表中
|
||||
const enabled = !DEFAULT_DISABLED_PLUGINS.includes(plugin.name);
|
||||
defaultConfig.plugins[plugin.name] = {
|
||||
enabled: true,
|
||||
enabled: enabled,
|
||||
description: plugin.description || ''
|
||||
};
|
||||
logger.info(`[PluginManager] Found plugin for default config: ${plugin.name}`);
|
||||
}
|
||||
} catch (importError) {
|
||||
// 如果导入失败,使用目录名作为插件名
|
||||
// 检查是否在默认禁用列表中
|
||||
const enabled = !DEFAULT_DISABLED_PLUGINS.includes(entry.name);
|
||||
defaultConfig.plugins[entry.name] = {
|
||||
enabled: true,
|
||||
enabled: enabled,
|
||||
description: ''
|
||||
};
|
||||
logger.warn(`[PluginManager] Could not import plugin ${entry.name}, using directory name:`, importError.message);
|
||||
|
|
|
|||
130
src/plugins/ai-monitor/index.js
Normal file
130
src/plugins/ai-monitor/index.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import logger from '../../utils/logger.js';
|
||||
|
||||
/**
|
||||
* AI 接口监控插件
|
||||
* 功能:
|
||||
* 1. 捕获 AI 接口的请求参数(转换前和转换后)
|
||||
* 2. 捕获 AI 接口的响应结果(转换前和转换后,流式响应聚合输出)
|
||||
*/
|
||||
const aiMonitorPlugin = {
|
||||
name: 'ai-monitor',
|
||||
version: '1.0.0',
|
||||
description: 'AI 接口监控插件 - 捕获请求和响应参数(全链路协议转换监控,流式聚合输出,用于调试和分析)',
|
||||
type: 'middleware',
|
||||
_priority: 100,
|
||||
|
||||
// 用于存储流式响应的中间状态
|
||||
streamCache: new Map(),
|
||||
|
||||
async init(config) {
|
||||
logger.info('[AI Monitor Plugin] Initialized');
|
||||
},
|
||||
|
||||
/**
|
||||
* 中间件:初始化请求上下文
|
||||
*/
|
||||
async middleware(req, res, requestUrl, config) {
|
||||
const aiPaths = ['/v1/chat/completions', '/v1/responses', '/v1/messages', '/v1beta/models'];
|
||||
const isAiPath = aiPaths.some(path => requestUrl.pathname.includes(path));
|
||||
|
||||
if (isAiPath && req.method === 'POST') {
|
||||
// 在监控插件中生成请求标识,并存入 config 以供全链路追踪
|
||||
const requestId = Date.now() + Math.random().toString(36).substring(2, 10);
|
||||
config._monitorRequestId = requestId;
|
||||
}
|
||||
|
||||
return { handled: false };
|
||||
},
|
||||
|
||||
hooks: {
|
||||
/**
|
||||
* 请求转换后的钩子
|
||||
*/
|
||||
async onContentGenerated(config) {
|
||||
const { originalRequestBody, processedRequestBody, fromProvider, toProvider, model, _monitorRequestId, isStream } = config;
|
||||
if (!originalRequestBody) return;
|
||||
|
||||
setImmediate(() => {
|
||||
const hasConversion = JSON.stringify(originalRequestBody) !== JSON.stringify(processedRequestBody);
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] >>> Req Protocol: ${fromProvider}${hasConversion ? ' -> ' + toProvider : ''} | Model: ${model}`);
|
||||
|
||||
if (hasConversion) {
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] [Req Original]: ${JSON.stringify(originalRequestBody)}`);
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] [Req Processed]: ${JSON.stringify(processedRequestBody)}`);
|
||||
} else {
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] [Req]: ${JSON.stringify(originalRequestBody)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理流式响应的聚合输出
|
||||
if (isStream && _monitorRequestId) {
|
||||
setTimeout(() => {
|
||||
const cache = aiMonitorPlugin.streamCache.get(_monitorRequestId);
|
||||
if (cache) {
|
||||
const hasConversion = JSON.stringify(cache.nativeChunks) !== JSON.stringify(cache.convertedChunks);
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] <<< Stream Response Aggregated: ${hasConversion ? cache.toProvider + ' -> ' : ''}${cache.fromProvider}`);
|
||||
|
||||
if (hasConversion) {
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] [Res Native Full]: ${JSON.stringify(cache.nativeChunks)}`);
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] [Res Converted Full]: ${JSON.stringify(cache.convertedChunks)}`);
|
||||
} else {
|
||||
logger.info(`[AI Monitor][${_monitorRequestId}] [Res Full]: ${JSON.stringify(cache.nativeChunks)}`);
|
||||
}
|
||||
|
||||
aiMonitorPlugin.streamCache.delete(_monitorRequestId);
|
||||
}
|
||||
}, 2000); // 等待流传输完成
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 非流式响应转换监控
|
||||
*/
|
||||
async onUnaryResponse({ nativeResponse, clientResponse, fromProvider, toProvider, requestId }) {
|
||||
setImmediate(() => {
|
||||
const reqId = requestId || 'N/A';
|
||||
const hasConversion = JSON.stringify(nativeResponse) !== JSON.stringify(clientResponse);
|
||||
logger.info(`[AI Monitor][${reqId}] <<< Res Protocol: ${hasConversion ? toProvider + ' -> ' : ''}${fromProvider} (Unary)`);
|
||||
|
||||
if (hasConversion) {
|
||||
logger.info(`[AI Monitor][${reqId}] [Res Native]: ${JSON.stringify(nativeResponse)}`);
|
||||
logger.info(`[AI Monitor][${reqId}] [Res Converted]: ${JSON.stringify(clientResponse)}`);
|
||||
} else {
|
||||
logger.info(`[AI Monitor][${reqId}] [Res]: ${JSON.stringify(nativeResponse)}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 流式响应分块转换监控 - 聚合数据
|
||||
*/
|
||||
async onStreamChunk({ nativeChunk, chunkToSend, fromProvider, toProvider, requestId }) {
|
||||
if (!requestId) return;
|
||||
|
||||
if (!aiMonitorPlugin.streamCache.has(requestId)) {
|
||||
aiMonitorPlugin.streamCache.set(requestId, {
|
||||
nativeChunks: [],
|
||||
convertedChunks: [],
|
||||
fromProvider,
|
||||
toProvider
|
||||
});
|
||||
}
|
||||
|
||||
const cache = aiMonitorPlugin.streamCache.get(requestId);
|
||||
cache.nativeChunks.push(nativeChunk);
|
||||
cache.convertedChunks.push(chunkToSend);
|
||||
},
|
||||
|
||||
/**
|
||||
* 内部请求转换监控
|
||||
*/
|
||||
async onInternalRequestConverted({ requestId, internalRequest, converterName }) {
|
||||
setImmediate(() => {
|
||||
const reqId = requestId || 'N/A';
|
||||
logger.info(`[AI Monitor][${reqId}] >>> Internal Req Converted [${converterName}]: ${JSON.stringify(internalRequest)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default aiMonitorPlugin;
|
||||
|
|
@ -765,7 +765,7 @@ async saveCredentialsToFile(filePath, newData) {
|
|||
/**
|
||||
* Build CodeWhisperer request from OpenAI messages
|
||||
*/
|
||||
buildCodewhispererRequest(messages, model, tools = null, inSystemPrompt = null, thinking = null) {
|
||||
async buildCodewhispererRequest(messages, model, tools = null, inSystemPrompt = null, thinking = null) {
|
||||
const conversationId = uuidv4();
|
||||
|
||||
let systemPrompt = this.getContentText(inSystemPrompt);
|
||||
|
|
@ -1181,6 +1181,23 @@ async saveCredentialsToFile(filePath, newData) {
|
|||
request.profileArn = this.profileArn;
|
||||
}
|
||||
|
||||
// 监控钩子:内部请求转换
|
||||
if (this.config?._monitorRequestId) {
|
||||
try {
|
||||
const { getPluginManager } = await import('../../core/plugin-manager.js');
|
||||
const pluginManager = getPluginManager();
|
||||
if (pluginManager) {
|
||||
await pluginManager.executeHook('onInternalRequestConverted', {
|
||||
requestId: this.config._monitorRequestId,
|
||||
internalRequest: request,
|
||||
converterName: 'buildCodewhispererRequest'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('[Kiro] Error calling onInternalRequestConverted hook:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// fs.writeFile('claude-kiro-request'+Date.now()+'.json', JSON.stringify(request));
|
||||
return request;
|
||||
}
|
||||
|
|
@ -1307,7 +1324,7 @@ async saveCredentialsToFile(filePath, newData) {
|
|||
throw new Error('No messages found in request body');
|
||||
}
|
||||
|
||||
const requestData = this.buildCodewhispererRequest(messages, model, body.tools, body.system, body.thinking);
|
||||
const requestData = await this.buildCodewhispererRequest(messages, model, body.tools, body.system, body.thinking);
|
||||
|
||||
try {
|
||||
const token = this.accessToken; // Use the already initialized token
|
||||
|
|
@ -1596,6 +1613,11 @@ async saveCredentialsToFile(filePath, newData) {
|
|||
async generateContent(model, requestBody) {
|
||||
if (!this.isInitialized) await this.initialize();
|
||||
|
||||
// 临时存储 monitorRequestId
|
||||
if (requestBody._monitorRequestId) {
|
||||
this.config._monitorRequestId = requestBody._monitorRequestId;
|
||||
}
|
||||
|
||||
// 检查 token 是否即将过期,如果是则推送到刷新队列
|
||||
if (this.isExpiryDateNear()) {
|
||||
logger.info('[Kiro] Token is near expiry, marking credential as need refresh...');
|
||||
|
|
@ -1787,7 +1809,7 @@ async saveCredentialsToFile(filePath, newData) {
|
|||
throw new Error('No messages found in request body');
|
||||
}
|
||||
|
||||
const requestData = this.buildCodewhispererRequest(messages, model, body.tools, body.system, body.thinking);
|
||||
const requestData = await this.buildCodewhispererRequest(messages, model, body.tools, body.system, body.thinking);
|
||||
|
||||
const token = this.accessToken;
|
||||
const headers = {
|
||||
|
|
@ -1952,6 +1974,11 @@ async saveCredentialsToFile(filePath, newData) {
|
|||
// 真正的流式传输实现
|
||||
async * generateContentStream(model, requestBody) {
|
||||
if (!this.isInitialized) await this.initialize();
|
||||
|
||||
// 临时存储 monitorRequestId
|
||||
if (requestBody._monitorRequestId) {
|
||||
this.config._monitorRequestId = requestBody._monitorRequestId;
|
||||
}
|
||||
|
||||
// 检查 token 是否即将过期,如果是则推送到刷新队列
|
||||
if (this.isExpiryDateNear()) {
|
||||
|
|
|
|||
|
|
@ -113,6 +113,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
return await systemApi.handleDownloadTodayLog(req, res);
|
||||
}
|
||||
|
||||
// Clear today's log file
|
||||
if (method === 'POST' && pathParam === '/api/system/clear-log') {
|
||||
return await systemApi.handleClearTodayLog(req, res);
|
||||
}
|
||||
|
||||
// Get provider pools summary
|
||||
if (method === 'GET' && pathParam === '/api/providers') {
|
||||
return await providerApi.handleGetProviders(req, res, currentConfig, providerPoolManager);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,46 @@ export async function handleDownloadTodayLog(req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当日日志
|
||||
*/
|
||||
export async function handleClearTodayLog(req, res) {
|
||||
try {
|
||||
const success = logger.clearTodayLog();
|
||||
|
||||
if (success) {
|
||||
// 广播日志清空事件
|
||||
const { broadcastEvent } = await import('./event-broadcast.js');
|
||||
broadcastEvent('log_cleared', {
|
||||
action: 'log_cleared',
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'Today\'s log file has been cleared'
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: '当日日志已清空'
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: { message: '清空日志失败' }
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('[UI API] Failed to clear log:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: { message: 'Failed to clear log: ' + error.message }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查接口(用于前端token验证)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -312,6 +312,21 @@ export async function handleStreamRequest(res, service, model, requestBody, from
|
|||
? convertData(nativeChunk, 'streamChunk', toProvider, fromProvider, model)
|
||||
: nativeChunk;
|
||||
|
||||
// 监控钩子:流式响应分块
|
||||
if (CONFIG?._monitorRequestId) {
|
||||
try {
|
||||
const pluginManager = getPluginManager();
|
||||
await pluginManager.executeHook('onStreamChunk', {
|
||||
nativeChunk,
|
||||
chunkToSend,
|
||||
fromProvider,
|
||||
toProvider,
|
||||
model,
|
||||
requestId: CONFIG._monitorRequestId
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!chunkToSend) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -481,6 +496,21 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP
|
|||
clientResponse = convertData(nativeResponse, 'response', toProvider, fromProvider, model);
|
||||
}
|
||||
|
||||
// 监控钩子:非流式响应
|
||||
if (CONFIG?._monitorRequestId) {
|
||||
try {
|
||||
const pluginManager = getPluginManager();
|
||||
await pluginManager.executeHook('onUnaryResponse', {
|
||||
nativeResponse,
|
||||
clientResponse,
|
||||
fromProvider,
|
||||
toProvider,
|
||||
model,
|
||||
requestId: CONFIG._monitorRequestId
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
//logger.info(`[Response] Sending response to client: ${JSON.stringify(clientResponse)}`);
|
||||
await handleUnifiedResponse(res, JSON.stringify(clientResponse), false);
|
||||
await logConversation('output', responseText, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME);
|
||||
|
|
@ -704,6 +734,11 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
|
|||
|
||||
// 1. Convert request body from client format to backend format, if necessary.
|
||||
let processedRequestBody = originalRequestBody;
|
||||
// 将 _monitorRequestId 注入到 requestBody 中,以便在 service 内部访问
|
||||
if (CONFIG._monitorRequestId) {
|
||||
processedRequestBody._monitorRequestId = CONFIG._monitorRequestId;
|
||||
}
|
||||
|
||||
// fs.writeFile('originalRequestBody'+Date.now()+'.json', JSON.stringify(originalRequestBody));
|
||||
if (getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider)) {
|
||||
logger.info(`[Request Convert] Converting request from ${fromProvider} to ${toProvider}`);
|
||||
|
|
@ -742,11 +777,21 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
|
|||
await handleUnaryRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName, retryContext);
|
||||
}
|
||||
|
||||
// 执行插件钩子:内容生成后
|
||||
try {
|
||||
const pluginManager = getPluginManager();
|
||||
await pluginManager.executeHook('onContentGenerated', CONFIG);
|
||||
} catch (e) { /* 静默失败,不影响主流程 */ }
|
||||
if (CONFIG?._monitorRequestId) {
|
||||
// 执行插件钩子:内容生成后
|
||||
try {
|
||||
const pluginManager = getPluginManager();
|
||||
await pluginManager.executeHook('onContentGenerated', {
|
||||
...CONFIG,
|
||||
originalRequestBody,
|
||||
processedRequestBody,
|
||||
fromProvider,
|
||||
toProvider,
|
||||
model,
|
||||
isStream
|
||||
});
|
||||
} catch (e) { /* 静默失败,不影响主流程 */ }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -343,6 +343,39 @@ class Logger {
|
|||
console.error('[Logger] Failed to cleanup old logs:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当日日志文件
|
||||
* @returns {boolean} 是否成功清空
|
||||
*/
|
||||
clearTodayLog() {
|
||||
try {
|
||||
if (!this.currentLogFile || !fs.existsSync(this.currentLogFile)) {
|
||||
console.warn('[Logger] No current log file to clear');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 关闭当前日志流
|
||||
if (this.logStream && !this.logStream.destroyed) {
|
||||
this.logStream.end();
|
||||
}
|
||||
|
||||
// 清空文件内容
|
||||
fs.writeFileSync(this.currentLogFile, '');
|
||||
|
||||
// 重新创建日志流
|
||||
this.logStream = fs.createWriteStream(this.currentLogFile, { flags: 'a' });
|
||||
this.logStream.on('error', (err) => {
|
||||
console.error('[Logger] Failed to write to log file:', err.message);
|
||||
});
|
||||
|
||||
console.log('[Logger] Today\'s log file cleared successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Logger] Failed to clear today\'s log file:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
|
|
|
|||
|
|
@ -17,12 +17,60 @@ function initEventListeners() {
|
|||
|
||||
// 清空日志
|
||||
if (elements.clearLogsBtn) {
|
||||
elements.clearLogsBtn.addEventListener('click', () => {
|
||||
clearLogs();
|
||||
if (elements.logsContainer) {
|
||||
elements.logsContainer.innerHTML = '';
|
||||
elements.clearLogsBtn.addEventListener('click', async () => {
|
||||
// 显示确认对话框,明确提示会清空本地日志文件
|
||||
const confirmed = confirm(t('logs.clear.confirm.msg'));
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = window.authManager.getToken();
|
||||
if (!token) {
|
||||
showToast(t('common.error'), '请先登录', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用后端 API 清空日志文件
|
||||
const response = await fetch(`${window.location.origin}/api/system/clear-log`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
showToast(t('common.error'), '认证失败,请重新登录', 'error');
|
||||
window.authManager.clearToken();
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// 清空前端日志显示
|
||||
clearLogs();
|
||||
if (elements.logsContainer) {
|
||||
elements.logsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// 显示成功提示,明确说明已清空本地日志文件
|
||||
showToast(
|
||||
t('logs.clear.success.title'),
|
||||
t('logs.clear.success.msg'),
|
||||
'success',
|
||||
5000 // 显示 5 秒
|
||||
);
|
||||
} else {
|
||||
showToast(t('common.error'), t('logs.clear.failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清空日志失败:', error);
|
||||
showToast(t('common.error'), t('logs.clear.failed') + ': ' + error.message, 'error');
|
||||
}
|
||||
showToast(t('common.success'), t('common.refresh.success'), 'success');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -540,6 +540,11 @@ const translations = {
|
|||
'logs.autoScroll': '自动滚动',
|
||||
'logs.autoScroll.on': '自动滚动: 开',
|
||||
'logs.autoScroll.off': '自动滚动: 关',
|
||||
'logs.clear.confirm.title': '警告',
|
||||
'logs.clear.confirm.msg': '此操作将清空当日的本地日志文件!\n\n• 前端页面的实时日志将被清空\n• 服务器上当日的日志文件也将被清空\n• 此操作不可恢复\n\n确定要继续吗?',
|
||||
'logs.clear.success.title': '清空成功',
|
||||
'logs.clear.success.msg': '前端实时日志和服务器当日日志文件已全部清空',
|
||||
'logs.clear.failed': '清空日志失败',
|
||||
|
||||
// Plugins
|
||||
'plugins.title': '插件管理',
|
||||
|
|
@ -624,12 +629,30 @@ const translations = {
|
|||
'guide.flow.title': '操作流程图',
|
||||
'guide.flow.step1.title': '配置管理',
|
||||
'guide.flow.step1.desc': '在「配置管理」页面设置基本参数',
|
||||
'guide.flow.step1.item1': '设置 API Key',
|
||||
'guide.flow.step1.item2': '选择启动时初始化的模型提供商',
|
||||
'guide.flow.step1.item3': '配置高级选项',
|
||||
'guide.flow.step2.title': '生成授权',
|
||||
'guide.flow.step2.desc': '在「提供商池管理」页面生成 OAuth 授权',
|
||||
'guide.flow.step2.method1': '方式一:OAuth 授权',
|
||||
'guide.flow.step2.method1.item1': '点击「生成授权」按钮',
|
||||
'guide.flow.step2.method1.item2': '在弹窗中完成 OAuth 登录',
|
||||
'guide.flow.step2.method1.item3': '凭据自动保存',
|
||||
'guide.flow.step2.or': '或',
|
||||
'guide.flow.step2.method2': '方式二:手动上传',
|
||||
'guide.flow.step2.method2.item1': '新增提供商节点',
|
||||
'guide.flow.step2.method2.item2': '上传已有的授权文件',
|
||||
'guide.flow.step2.method2.item3': '手动关联凭据路径',
|
||||
'guide.flow.step3.title': '管理凭据',
|
||||
'guide.flow.step3.desc': '在「凭据文件管理」页面查看和管理凭据',
|
||||
'guide.flow.step3.item1': '查看已生成的凭据文件',
|
||||
'guide.flow.step3.item2': '自动关联到提供商池',
|
||||
'guide.flow.step3.item3': '删除无效凭据',
|
||||
'guide.flow.step4.title': '开始使用',
|
||||
'guide.flow.step4.desc': '在「仪表盘」查看路由示例并开始调用 API',
|
||||
'guide.flow.step4.item1': '查看路由调用示例',
|
||||
'guide.flow.step4.item2': '复制 API 端点地址',
|
||||
'guide.flow.step4.item3': '在客户端中配置使用',
|
||||
|
||||
// Tutorial
|
||||
'tutorial.title': '配置教程',
|
||||
|
|
@ -1275,6 +1298,11 @@ const translations = {
|
|||
'logs.autoScroll': 'Auto Scroll',
|
||||
'logs.autoScroll.on': 'Auto Scroll: On',
|
||||
'logs.autoScroll.off': 'Auto Scroll: Off',
|
||||
'logs.clear.confirm.title': 'Warning',
|
||||
'logs.clear.confirm.msg': 'This action will clear today\'s local log file!\n\n• Real-time logs on frontend will be cleared\n• Today\'s log file on server will also be cleared\n• This action cannot be undone\n\nAre you sure you want to continue?',
|
||||
'logs.clear.success.title': 'Success',
|
||||
'logs.clear.success.msg': 'Both real-time logs and today\'s log file on server have been cleared',
|
||||
'logs.clear.failed': 'Failed to clear logs',
|
||||
|
||||
// Plugins
|
||||
'plugins.title': 'Plugin Management',
|
||||
|
|
@ -1359,12 +1387,30 @@ const translations = {
|
|||
'guide.flow.title': 'Operation Flowchart',
|
||||
'guide.flow.step1.title': 'Configuration',
|
||||
'guide.flow.step1.desc': 'Set basic parameters in "Configuration" page',
|
||||
'guide.flow.step1.item1': 'Set API Key',
|
||||
'guide.flow.step1.item2': 'Select model providers to initialize on startup',
|
||||
'guide.flow.step1.item3': 'Configure advanced options',
|
||||
'guide.flow.step2.title': 'Generate Auth',
|
||||
'guide.flow.step2.desc': 'Generate OAuth authorization in "Provider Pools" page',
|
||||
'guide.flow.step2.method1': 'Method 1: OAuth Authorization',
|
||||
'guide.flow.step2.method1.item1': 'Click "Generate Auth" button',
|
||||
'guide.flow.step2.method1.item2': 'Complete OAuth login in popup',
|
||||
'guide.flow.step2.method1.item3': 'Credentials saved automatically',
|
||||
'guide.flow.step2.or': 'or',
|
||||
'guide.flow.step2.method2': 'Method 2: Manual Upload',
|
||||
'guide.flow.step2.method2.item1': 'Add new provider node',
|
||||
'guide.flow.step2.method2.item2': 'Upload existing authorization file',
|
||||
'guide.flow.step2.method2.item3': 'Manually link credential path',
|
||||
'guide.flow.step3.title': 'Manage Credentials',
|
||||
'guide.flow.step3.desc': 'View and manage credentials in "Credential Files" page',
|
||||
'guide.flow.step3.item1': 'View generated credential files',
|
||||
'guide.flow.step3.item2': 'Auto-link to provider pool',
|
||||
'guide.flow.step3.item3': 'Delete invalid credentials',
|
||||
'guide.flow.step4.title': 'Start Using',
|
||||
'guide.flow.step4.desc': 'View routing examples in "Dashboard" and start calling API',
|
||||
'guide.flow.step4.item1': 'View routing call examples',
|
||||
'guide.flow.step4.item2': 'Copy API endpoint address',
|
||||
'guide.flow.step4.item3': 'Configure in client application',
|
||||
|
||||
// Tutorial
|
||||
'tutorial.title': 'Configuration Tutorial',
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@
|
|||
<h4 data-i18n="guide.flow.step1.title">配置管理</h4>
|
||||
<p data-i18n="guide.flow.step1.desc">在「配置管理」页面设置基本参数</p>
|
||||
<ul>
|
||||
<li>设置 API Key</li>
|
||||
<li>选择启动时初始化的模型提供商</li>
|
||||
<li>配置高级选项</li>
|
||||
<li data-i18n="guide.flow.step1.item1">设置 API Key</li>
|
||||
<li data-i18n="guide.flow.step1.item2">选择启动时初始化的模型提供商</li>
|
||||
<li data-i18n="guide.flow.step1.item3">配置高级选项</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -60,20 +60,20 @@
|
|||
<p data-i18n="guide.flow.step2.desc">在「提供商池管理」页面生成 OAuth 授权</p>
|
||||
<div class="branch-options">
|
||||
<div class="branch-option">
|
||||
<div class="branch-label">方式一:OAuth 授权</div>
|
||||
<div class="branch-label" data-i18n="guide.flow.step2.method1">方式一:OAuth 授权</div>
|
||||
<ul>
|
||||
<li>点击「生成授权」按钮</li>
|
||||
<li>在弹窗中完成 OAuth 登录</li>
|
||||
<li>凭据自动保存</li>
|
||||
<li data-i18n="guide.flow.step2.method1.item1">点击「生成授权」按钮</li>
|
||||
<li data-i18n="guide.flow.step2.method1.item2">在弹窗中完成 OAuth 登录</li>
|
||||
<li data-i18n="guide.flow.step2.method1.item3">凭据自动保存</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="branch-divider">或</div>
|
||||
<div class="branch-divider" data-i18n="guide.flow.step2.or">或</div>
|
||||
<div class="branch-option">
|
||||
<div class="branch-label">方式二:手动上传</div>
|
||||
<div class="branch-label" data-i18n="guide.flow.step2.method2">方式二:手动上传</div>
|
||||
<ul>
|
||||
<li>新增提供商节点</li>
|
||||
<li>上传已有的授权文件</li>
|
||||
<li>手动关联凭据路径</li>
|
||||
<li data-i18n="guide.flow.step2.method2.item1">新增提供商节点</li>
|
||||
<li data-i18n="guide.flow.step2.method2.item2">上传已有的授权文件</li>
|
||||
<li data-i18n="guide.flow.step2.method2.item3">手动关联凭据路径</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -88,9 +88,9 @@
|
|||
<h4 data-i18n="guide.flow.step3.title">管理凭据</h4>
|
||||
<p data-i18n="guide.flow.step3.desc">在「凭据文件管理」页面查看和管理凭据</p>
|
||||
<ul>
|
||||
<li>查看已生成的凭据文件</li>
|
||||
<li>自动关联到提供商池</li>
|
||||
<li>删除无效凭据</li>
|
||||
<li data-i18n="guide.flow.step3.item1">查看已生成的凭据文件</li>
|
||||
<li data-i18n="guide.flow.step3.item2">自动关联到提供商池</li>
|
||||
<li data-i18n="guide.flow.step3.item3">删除无效凭据</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -103,9 +103,9 @@
|
|||
<h4 data-i18n="guide.flow.step4.title">开始使用</h4>
|
||||
<p data-i18n="guide.flow.step4.desc">在「仪表盘」查看路由示例并开始调用 API</p>
|
||||
<ul>
|
||||
<li>查看路由调用示例</li>
|
||||
<li>复制 API 端点地址</li>
|
||||
<li>在客户端中配置使用</li>
|
||||
<li data-i18n="guide.flow.step4.item1">查看路由调用示例</li>
|
||||
<li data-i18n="guide.flow.step4.item2">复制 API 端点地址</li>
|
||||
<li data-i18n="guide.flow.step4.item3">在客户端中配置使用</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue