feat(plugins): 新增 AI 监控插件并优化日志管理

- 新增 AI 监控插件 (ai-monitor),支持全链路协议转换监控
  - 捕获 AI 接口请求参数(转换前后)
  - 监控流式和非流式响应(转换前后)
  - 支持内部请求转换监控
- 新增日志清空功能,支持前端和服务器端同时清空当日日志
- 默认禁用 api-potluck 和 ai-monitor 插件
- 更新多语言文档和配置示例
- 优化提供商适配器开发指南
This commit is contained in:
hex2077 2026-01-25 19:40:04 +08:00
parent 245583b96a
commit ce7d78f7d0
17 changed files with 581 additions and 293 deletions

111
OPENCODE_CONFIG_EXAMPLE.md Normal file
View 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它可以为您提供自动补全和实时错误检查。

View file

@ -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` 并在后台异步执行。

View file

@ -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** - テーマ切替機能を追加し、プロバイダープール初期化を最適化、プロバイダーのデフォルト設定を使用するフォールバック戦略を削除

View file

@ -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** - 新增主题切换功能并优化提供商池初始化,移除使用提供商默认配置的降级策略

View file

@ -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

View file

@ -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 来改进这个管理控制台!
## 许可证
本项目使用与主项目相同的许可证。

View file

@ -1,7 +1,7 @@
{
"plugins": {
"api-potluck": {
"enabled": true,
"enabled": false,
"description": "API 大锅饭 - Key 管理和用量统计插件"
},
"default-auth": {

View file

@ -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);

View 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;

View file

@ -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()) {

View file

@ -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);

View file

@ -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验证
*/

View file

@ -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) { /* 静默失败,不影响主流程 */ }
}
}
/**

View file

@ -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;
}
}
}
// 创建单例实例

View file

@ -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');
});
}

View file

@ -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',

View file

@ -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>