diff --git a/airouter-backend.tar b/airouter-backend.tar
new file mode 100644
index 0000000..7fb36b4
Binary files /dev/null and b/airouter-backend.tar differ
diff --git a/airouter-frontend.tar b/airouter-frontend.tar
new file mode 100644
index 0000000..ba86828
Binary files /dev/null and b/airouter-frontend.tar differ
diff --git a/docs/LOG_CLEANER.md b/docs/LOG_CLEANER.md
new file mode 100644
index 0000000..75f95f0
--- /dev/null
+++ b/docs/LOG_CLEANER.md
@@ -0,0 +1,478 @@
+# 日志自动清理功能文档
+
+## 概述
+
+AI Gateway 提供了自动清理请求日志的功能,可以定期删除过期的日志记录,防止数据库文件无限增长。此功能通过配置文件进行管理,支持灵活的定时清理策略。
+
+## 功能特性
+
+### 1. 定时自动清理
+- **自动执行**:在指定时间自动清理过期日志
+- **可配置保留期**:灵活设置日志保留天数(默认7天)
+- **空间回收**:执行SQLite VACUUM操作,真正回收磁盘空间
+- **详细日志**:记录每次清理的详细统计信息
+
+### 2. 手动触发清理
+- 通过API端点手动触发立即清理
+- 返回详细的清理报告
+- 适用于紧急清理或测试场景
+
+### 3. 状态监控
+- 查询清理器当前状态
+- 查看下次执行时间
+- 监控清理配置参数
+
+## 配置说明
+
+### 配置文件位置
+
+配置文件默认位于项目根目录:`config.json`
+
+如果配置文件不存在,系统会自动创建并使用默认配置。
+
+### 配置文件格式
+
+```json
+{
+ "database": {
+ "path": "./gateway.db"
+ },
+ "server": {
+ "port": "8080",
+ "host": "0.0.0.0"
+ },
+ "log_cleaner": {
+ "enabled": true,
+ "execute_time": "02:00",
+ "retention_days": 7,
+ "check_interval": 5
+ },
+ "app": {
+ "name": "AI Gateway",
+ "version": "1.0.0",
+ "environment": "production",
+ "log_level": "info"
+ }
+}
+```
+
+### 日志清理配置参数详解
+
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `enabled` | boolean | `true` | 是否启用自动清理功能 |
+| `execute_time` | string | `"02:00"` | 每天执行清理的时间(24小时制,格式:HH:MM) |
+| `retention_days` | integer | `7` | 日志保留天数,超过此天数的日志将被删除 |
+| `check_interval` | integer | `5` | 检查间隔(分钟),系统每隔此时间检查是否到达执行时间 |
+
+### 配置示例
+
+#### 示例1:每天凌晨2点清理30天前的日志
+```json
+{
+ "log_cleaner": {
+ "enabled": true,
+ "execute_time": "02:00",
+ "retention_days": 30,
+ "check_interval": 5
+ }
+}
+```
+
+#### 示例2:每天中午12点清理3天前的日志
+```json
+{
+ "log_cleaner": {
+ "enabled": true,
+ "execute_time": "12:00",
+ "retention_days": 3,
+ "check_interval": 5
+ }
+}
+```
+
+#### 示例3:禁用自动清理
+```json
+{
+ "log_cleaner": {
+ "enabled": false,
+ "execute_time": "02:00",
+ "retention_days": 7,
+ "check_interval": 5
+ }
+}
+```
+
+## 使用方法
+
+### 1. 启动服务
+
+系统启动时会自动加载配置并启动日志清理器:
+
+```bash
+cd backend
+go run main.go
+```
+
+启动日志会显示清理器状态:
+```
+🚀 启动 AI Gateway 服务...
+✅ 配置文件加载成功: ./config.json
+📋 当前配置:
+ 🗄️ 数据库路径: ./gateway.db
+ 🌐 服务器地址: 0.0.0.0:8080
+ 🧹 日志自动清理: true
+ ⏰ 执行时间: 02:00
+ 📅 保留天数: 7天
+ 🔍 检查间隔: 5分钟
+✅ 数据库连接成功
+🚀 启动日志自动清理器 - 执行时间: 02:00, 保留天数: 7天
+✅ 日志自动清理器已启动
+🌐 HTTP服务器启动在 0.0.0.0:8080
+```
+
+### 2. 查询清理器状态
+
+**API端点**: `GET /api/log-cleaner/status`
+
+**请求示例**:
+```bash
+curl -X GET http://localhost:8080/api/log-cleaner/status \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+**响应示例**:
+```json
+{
+ "enabled": true,
+ "execute_time": "02:00",
+ "retention_days": 7,
+ "check_interval": 5,
+ "next_execute_time": "2025-11-12 02:00:00",
+ "time_until_next": "18h34m22s"
+}
+```
+
+### 3. 手动触发清理
+
+**API端点**: `POST /api/log-cleaner/force-cleanup`
+
+**请求示例**:
+```bash
+curl -X POST http://localhost:8080/api/log-cleaner/force-cleanup \
+ -H "Authorization: Bearer YOUR_API_KEY"
+```
+
+**响应示例**:
+```json
+{
+ "message": "Manual log cleanup completed",
+ "execute_time": "2025-11-11 15:30:45",
+ "duration": "1.234s",
+ "deleted_count": 5000,
+ "total_count_before": 10000,
+ "active_count_before": 10000,
+ "total_count_after": 5000,
+ "cutoff_time": "2025-11-04 15:30:45",
+ "vacuum_duration": "0.567s",
+ "vacuum_error": "",
+ "success": true
+}
+```
+
+### 4. 修改配置
+
+#### 方法1:修改配置文件后重启
+1. 编辑 `config.json` 文件
+2. 修改 `log_cleaner` 配置项
+3. 重启服务
+
+#### 方法2:通过环境变量(未来支持)
+未来版本将支持通过环境变量覆盖配置。
+
+## 清理执行流程
+
+### 自动清理流程
+
+```
+系统启动
+ ↓
+加载配置文件
+ ↓
+初始化日志清理器
+ ↓
+启动定时检查(每5分钟)
+ ↓
+检查当前时间是否匹配执行时间
+ ↓
+【匹配】→ 执行清理任务
+ ↓
+1. 计算截止时间(当前时间 - 保留天数)
+2. 硬删除截止时间之前的日志记录
+3. 执行VACUUM操作回收磁盘空间
+4. 记录清理统计信息
+5. 生成并记录清理报告
+ ↓
+继续定时检查...
+```
+
+### 清理报告说明
+
+每次清理(自动或手动)都会生成详细的清理报告,包含以下信息:
+
+| 字段 | 说明 |
+|------|------|
+| `execute_time` | 清理执行的时间 |
+| `duration` | 清理操作总耗时 |
+| `deleted_count` | 删除的日志记录数 |
+| `total_count_before` | 清理前的总记录数(包括软删除) |
+| `active_count_before` | 清理前的活跃记录数 |
+| `total_count_after` | 清理后的总记录数 |
+| `cutoff_time` | 清理的截止时间(此时间之前的日志被删除) |
+| `vacuum_duration` | VACUUM操作耗时 |
+| `vacuum_error` | VACUUM操作错误信息(如有) |
+| `success` | 清理是否成功 |
+
+### 日志输出示例
+
+```
+⏰ 开始执行定时日志清理任务 - 当前时间: 2025-11-11 02:00:03
+🧹 开始自动清理日志 - 删除 2025-11-04 02:00:03 之前的日志
+✅ 自动日志清理完成:
+ 📊 删除记录数: 5000
+ 📈 删除前总数: 10000 (活跃: 10000)
+ 📉 删除后总数: 5000
+ 🗜️ VACUUM耗时: 0.567s
+ ⏱️ 总耗时: 1.234s
+ 📅 清理截止时间: 2025-11-04 02:00:03
+📋 清理报告已生成 - 删除 5000 条记录,耗时 1.234s
+```
+
+## 性能影响
+
+### 数据库锁定
+- **VACUUM操作**:会锁定整个数据库,期间无法进行写操作
+- **建议**:将执行时间设置在业务低峰期(如凌晨)
+- **耗时估算**:取决于数据库大小,通常几秒到几十秒
+
+### 磁盘I/O
+- DELETE操作和VACUUM操作都会产生磁盘I/O
+- 对于大量数据(百万级),建议分批删除或调整保留策略
+
+### 内存占用
+- 清理过程内存占用较小
+- VACUUM可能临时占用额外磁盘空间(最多为数据库文件大小)
+
+## 故障排查
+
+### 1. 清理器未启动
+
+**症状**:启动日志显示"日志自动清理功能已禁用"
+
+**解决方案**:
+- 检查 `config.json` 中 `log_cleaner.enabled` 是否为 `true`
+- 确认配置文件格式正确
+
+### 2. 配置文件加载失败
+
+**症状**:启动日志显示"加载配置失败,使用默认配置"
+
+**解决方案**:
+- 检查 `config.json` 文件是否存在
+- 验证JSON格式是否正确
+- 检查文件权限是否可读
+
+### 3. VACUUM失败
+
+**症状**:清理报告中显示 `vacuum_error` 不为空
+
+**可能原因**:
+- 磁盘空间不足
+- 数据库文件被其他进程锁定
+- 数据库文件损坏
+
+**解决方案**:
+- 确保有足够的磁盘空间(至少为数据库文件大小的2倍)
+- 关闭其他可能访问数据库的进程
+- 使用SQLite工具检查数据库完整性
+
+### 4. 清理未在预定时间执行
+
+**症状**:过了执行时间但未看到清理日志
+
+**解决方案**:
+- 检查系统时间是否正确
+- 确认 `check_interval` 设置合理(不要太大)
+- 查看应用日志是否有错误信息
+- 验证服务是否正常运行
+
+## 最佳实践
+
+### 1. 保留期设置建议
+
+| 业务场景 | 建议保留期 | 说明 |
+|---------|-----------|------|
+| 开发测试环境 | 1-3天 | 快速清理,节省空间 |
+| 生产环境(高流量) | 7-14天 | 平衡存储和可追溯性 |
+| 生产环境(低流量) | 30-90天 | 长期保留用于分析 |
+| 合规要求 | 根据法规 | 遵守行业规定 |
+
+### 2. 执行时间建议
+
+- **首选**:凌晨2-4点(业务低峰期)
+- **备选**:中午12-14点(如果凌晨有定时任务)
+- **避免**:业务高峰期
+
+### 3. 检查间隔建议
+
+- **推荐值**:5分钟
+- **最小值**:1分钟(频繁检查会增加CPU开销)
+- **最大值**:30分钟(间隔太大可能错过执行时间)
+
+### 4. 监控建议
+
+- 定期查看清理器状态API
+- 监控数据库文件大小变化
+- 关注清理报告中的异常信息
+- 设置告警(如清理失败、VACUUM错误)
+
+### 5. 备份策略
+
+- 在首次启用清理功能前,备份数据库
+- 定期备份配置文件
+- 保留重要日志的导出副本
+
+## 与手动清理的对比
+
+| 特性 | 定时自动清理 | 手动清理(API) | 前端手动清理 |
+|------|------------|----------------|-------------|
+| 触发方式 | 自动定时 | API调用 | 前端操作 |
+| 清理粒度 | 按天数 | 按天数 | 按天数 |
+| 空间回收 | ✅ VACUUM | ✅ VACUUM | ✅ VACUUM |
+| 清理报告 | ✅ 详细 | ✅ 详细 | ✅ 基本 |
+| 适用场景 | 日常维护 | 紧急清理/脚本 | 临时清理 |
+| 需要权限 | 无(系统级) | API认证 | 用户认证 |
+
+## 技术实现细节
+
+### 核心组件
+
+1. **调度器** (`backend/internal/scheduler/log_cleaner.go`)
+ - 定时检查逻辑
+ - 清理执行逻辑
+ - 报告生成
+
+2. **配置管理** (`backend/internal/config/config.go`)
+ - 配置加载和验证
+ - 默认配置管理
+
+3. **API端点** (`backend/api/handlers.go`)
+ - 状态查询接口
+ - 手动触发接口
+
+4. **主程序集成** (`backend/main.go`)
+ - 启动初始化
+ - 优雅关闭
+
+### 数据删除机制
+
+系统使用**硬删除**(Unscoped Delete)而非GORM默认的软删除:
+
+```go
+// 硬删除,真正从数据库中移除记录
+db.Unscoped().Where("created_at < ?", cutoffTime).Delete(&models.RequestLog{})
+
+// 执行VACUUM回收空间
+db.Exec("VACUUM")
+```
+
+这确保:
+- 记录被物理删除
+- 磁盘空间被真正释放
+- 数据库文件大小减小
+
+## 升级和迁移
+
+### 从无配置文件版本升级
+
+如果从旧版本升级,系统会自动创建默认配置文件。
+
+### 配置迁移
+
+1. 备份现有配置(如有)
+2. 参考 `config.example.json` 添加新配置项
+3. 重启服务验证
+
+## 安全考虑
+
+### 1. 权限控制
+- 所有API端点需要认证(Authorization header)
+- 手动触发清理需要管理员权限
+
+### 2. 数据安全
+- 删除操作不可逆
+- 建议定期备份重要日志
+- 清理前检查保留期设置
+
+### 3. 配置安全
+- 配置文件应有适当的文件权限(如644)
+- 不要在配置中存储敏感信息
+- 使用环境变量管理敏感配置(未来版本)
+
+## FAQ
+
+### Q1: 如何临时禁用自动清理?
+A: 修改配置文件,将 `log_cleaner.enabled` 设置为 `false`,然后重启服务。
+
+### Q2: 清理会影响正在运行的服务吗?
+A: VACUUM操作期间会短暂锁定数据库,建议在低峰期执行。正常的DELETE操作影响很小。
+
+### Q3: 如何查看已删除的日志?
+A: 删除后的日志无法恢复。如需保留,请在清理前导出或调整保留期。
+
+### Q4: 可以设置多个清理时间吗?
+A: 当前版本仅支持每天一次。如需多次清理,可以通过cron任务调用手动清理API。
+
+### Q5: 数据库文件大小没有减小?
+A: 确认清理日志中包含VACUUM操作且无错误。如果VACUUM失败,文件大小不会减小。
+
+### Q6: 如何测试清理功能?
+A: 使用手动触发API进行测试,或将执行时间设置为几分钟后。
+
+### Q7: 清理会删除当天的日志吗?
+A: 不会。清理只删除 `retention_days` 天之前的日志。例如保留7天,则只删除7天前的日志。
+
+### Q8: 支持按大小清理吗?
+A: 当前版本仅支持按时间清理。未来可能添加按大小或按数量清理的选项。
+
+## 更新日志
+
+### v1.0.0 (2025-11-11)
+- ✅ 首次发布
+- ✅ 支持定时自动清理
+- ✅ 支持手动触发清理
+- ✅ 集成VACUUM空间回收
+- ✅ 配置文件管理
+- ✅ 详细清理报告
+- ✅ 优雅关闭支持
+
+## 未来计划
+
+- [ ] 支持环境变量配置
+- [ ] 支持按数据库大小触发清理
+- [ ] 支持多个清理时间段
+- [ ] 清理历史记录持久化
+- [ ] 集成Prometheus监控指标
+- [ ] 邮件/webhook通知
+- [ ] 增量清理(分批删除大量数据)
+- [ ] 清理前自动备份选项
+
+## 技术支持
+
+如有问题或建议,请提交Issue或联系开发团队。
+
+---
+
+**文档版本**: 1.0.0
+**最后更新**: 2025-11-11
+**适用版本**: AI Gateway v1.0.0+
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ec6a6cd..231cfee 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,9 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2",
"react": "^19.1.1",
@@ -311,6 +314,55 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
+ "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/core": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
+ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.1.1",
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/sortable": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
+ "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^6.3.0",
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -3518,6 +3570,11 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 71b741b..fd4152d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2",
"react": "^19.1.1",
diff --git a/frontend/src/features/virtual-models/components/DraggableBackendModelCard.jsx b/frontend/src/features/virtual-models/components/DraggableBackendModelCard.jsx
new file mode 100644
index 0000000..2b7aa37
--- /dev/null
+++ b/frontend/src/features/virtual-models/components/DraggableBackendModelCard.jsx
@@ -0,0 +1,176 @@
+import React from 'react';
+import { useSortable } from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+
+const DraggableBackendModelCard = ({ index, bm, providers, onModelChange, onRemove }) => {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id: `backend-model-${index}` });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ };
+
+ return (
+
+
+
+ ≡
+
+
+ 优先级: {index + 1}
+
+
+
+
+ {/* 供应商和后端模型名称一行 */}
+
+
+
+
+
+
+
+
+ onModelChange(index, 'Name', e.target.value)}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
+ />
+
+
+
+ {/* 成本阈值、最大上下文长度、计费方式、Token单价一行 */}
+
+
+
+
+ onModelChange(index, 'CostThreshold', parseFloat(e.target.value) || 0)
+ }
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
+ />
+
+
+
+
+ onModelChange(index, 'MaxContextLength', parseInt(e.target.value))}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
+ />
+
+
+
+
+
+
+
+ {/* Token单价占位符,实际内容在下面条件渲染中 */}
+
+
+
+ {bm.BillingMethod === 'token' ? '见下方详细设置' : '按请求计费'}
+
+
+
+
+ {/* Token单价相关字段 - 按Token计费时显示 */}
+ {bm.BillingMethod === 'token' && (
+
+ )}
+
+ {/* 固定价格 - 按请求计费时显示 */}
+ {bm.BillingMethod === 'request' && (
+
+
+
+ onModelChange(index, 'FixedPrice', parseFloat(e.target.value))}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
+ />
+
+
+ )}
+
+ );
+};
+
+export default DraggableBackendModelCard;
\ No newline at end of file
diff --git a/frontend/src/features/virtual-models/components/VirtualModelForm.jsx b/frontend/src/features/virtual-models/components/VirtualModelForm.jsx
index 1a9152a..1067ef5 100644
--- a/frontend/src/features/virtual-models/components/VirtualModelForm.jsx
+++ b/frontend/src/features/virtual-models/components/VirtualModelForm.jsx
@@ -1,11 +1,33 @@
import React, { useState, useEffect } from 'react';
import { getProviders } from '../../providers/api';
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core';
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import DraggableBackendModelCard from './DraggableBackendModelCard';
const VirtualModelForm = ({ model, onSave, onCancel }) => {
const [name, setName] = useState('');
const [backendModels, setBackendModels] = useState([]);
const [providers, setProviders] = useState([]);
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+
useEffect(() => {
if (model) {
setName(model.Name);
@@ -46,9 +68,28 @@ const VirtualModelForm = ({ model, onSave, onCancel }) => {
setBackendModels(newBackendModels);
};
+ const handleDragEnd = (event) => {
+ const { active, over } = event;
+
+ if (active.id !== over.id) {
+ setBackendModels((items) => {
+ const oldIndex = parseInt(active.id.replace('backend-model-', ''));
+ const newIndex = parseInt(over.id.replace('backend-model-', ''));
+ return arrayMove(items, oldIndex, newIndex);
+ });
+ }
+ };
+
const handleSubmit = (e) => {
e.preventDefault();
- onSave({ Name: name, BackendModels: backendModels });
+
+ // 自动设置优先级为索引+1
+ const backendModelsWithPriority = backendModels.map((model, index) => ({
+ ...model,
+ Priority: index + 1,
+ }));
+
+ onSave({ Name: name, BackendModels: backendModelsWithPriority });
};
return (
@@ -64,126 +105,51 @@ const VirtualModelForm = ({ model, onSave, onCancel }) => {
后端模型
- {backendModels.map((bm, index) => (
-
-
-
-
-
-
-
-
-
-
- handleBackendModelChange(index, 'Name', e.target.value)}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
- />
-
-
-
- handleBackendModelChange(index, 'Priority', parseInt(e.target.value))}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
- />
-
-
-
- handleBackendModelChange(index, 'CostThreshold', parseFloat(e.target.value) || 0)}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
- />
-
-
-
- handleBackendModelChange(index, 'MaxContextLength', parseInt(e.target.value))}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
- />
-
-
-
-
-
- {bm.BillingMethod === 'token' && (
- <>
-
-
- handleBackendModelChange(index, 'PromptTokenPrice', parseFloat(e.target.value))}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
- />
-
-
-
- handleBackendModelChange(index, 'CompletionTokenPrice', parseFloat(e.target.value))}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
- />
-
- >
- )}
- {bm.BillingMethod === 'request' && (
-
-
-
handleBackendModelChange(index, 'FixedPrice', parseFloat(e.target.value))}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
+
+
+ `backend-model-${index}`)}
+ strategy={verticalListSortingStrategy}
+ >
+
+ {backendModels.map((bm, index) => (
+
-
- )}
-
- ))}
+ ))}
+
+
+
-