good version for 算法注册
This commit is contained in:
@@ -1,207 +0,0 @@
|
||||
# 智能算法展示平台 - 故障排除指南
|
||||
|
||||
## Docker镜像拉取问题
|
||||
|
||||
### 问题描述
|
||||
当尝试使用Docker Compose部署时,可能会遇到镜像拉取缓慢或失败的问题,特别是在网络受限的环境中。
|
||||
|
||||
### 解决方案
|
||||
|
||||
#### 1. 配置Docker镜像加速器
|
||||
|
||||
对于中国大陆用户,可以配置Docker镜像加速器:
|
||||
|
||||
```bash
|
||||
# 编辑Docker守护进程配置
|
||||
sudo mkdir -p /etc/docker
|
||||
sudo tee /etc/docker/daemon.json <<-'EOF'
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.mirrors.ustc.edu.cn",
|
||||
"https://hub-mirror.c.163.com",
|
||||
"https://mirror.baidubce.com"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
# 重启Docker服务
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
#### 2. 手动拉取镜像
|
||||
|
||||
如果自动拉取失败,可以手动拉取所需镜像:
|
||||
|
||||
```bash
|
||||
# 拉取所有必需的镜像
|
||||
docker pull postgres:14-alpine
|
||||
docker pull redis:7-alpine
|
||||
docker pull minio/minio:latest
|
||||
docker pull nginx:alpine
|
||||
docker pull python:3.9-slim
|
||||
docker pull node:18-alpine
|
||||
```
|
||||
|
||||
#### 3. 使用不同的Compose文件
|
||||
|
||||
我们提供了两个Compose文件:
|
||||
|
||||
- `docker-compose-full.yml` - 适用于网络良好的环境,包含构建步骤
|
||||
- `compose-without-build.yml` - 适用于网络受限的环境,使用预拉取的镜像
|
||||
|
||||
#### 4. 本地开发模式
|
||||
|
||||
如果Docker部署遇到困难,可以使用本地开发模式:
|
||||
|
||||
```bash
|
||||
# 启动后端服务
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload
|
||||
|
||||
# 启动前端服务
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 端口冲突问题
|
||||
|
||||
### 问题描述
|
||||
部署时可能遇到端口已被占用的情况。
|
||||
|
||||
### 解决方案
|
||||
|
||||
检查并释放被占用的端口:
|
||||
|
||||
```bash
|
||||
# 检查端口占用情况
|
||||
lsof -i :8000
|
||||
lsof -i :3000
|
||||
lsof -i :5432
|
||||
lsof -i :6379
|
||||
lsof -i :9000
|
||||
lsof -i :9001
|
||||
|
||||
# 终止占用端口的进程
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
## 数据库连接问题
|
||||
|
||||
### 问题描述
|
||||
后端服务启动后无法连接到数据库。
|
||||
|
||||
### 解决方案
|
||||
|
||||
确保PostgreSQL服务已完全启动后再启动后端服务:
|
||||
|
||||
```bash
|
||||
# 等待PostgreSQL准备就绪
|
||||
docker exec -it algorithm-showcase-postgres pg_isready
|
||||
```
|
||||
|
||||
## MinIO连接问题
|
||||
|
||||
### 问题描述
|
||||
后端服务无法连接到MinIO对象存储。
|
||||
|
||||
### 解决方案
|
||||
|
||||
MinIO服务在首次启动时需要一些时间初始化存储桶。如果遇到连接问题,可以:
|
||||
|
||||
1. 等待MinIO服务完全启动
|
||||
2. 检查MinIO控制台 http://localhost:9001
|
||||
3. 验证凭据是否正确(admin/minioadmin)
|
||||
|
||||
## 前端API连接问题
|
||||
|
||||
### 问题描述
|
||||
前端无法连接到后端API。
|
||||
|
||||
### 解决方案
|
||||
|
||||
确保环境变量正确设置:
|
||||
|
||||
```bash
|
||||
# 检查前端环境变量
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
```
|
||||
|
||||
## 部署验证
|
||||
|
||||
部署完成后,可以通过以下方式验证服务是否正常运行:
|
||||
|
||||
```bash
|
||||
# 检查所有服务状态
|
||||
docker-compose -f docker-compose-full.yml ps
|
||||
|
||||
# 测试后端API
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# 访问前端
|
||||
open http://localhost:3000
|
||||
|
||||
# 访问API文档
|
||||
open http://localhost:8000/docs
|
||||
```
|
||||
|
||||
## 常见错误及解决方案
|
||||
|
||||
### 错误:`connection refused`
|
||||
- 检查相应服务是否已启动
|
||||
- 检查防火墙设置
|
||||
|
||||
### 错误:`permission denied`
|
||||
- 检查文件权限
|
||||
- 确保有足够的磁盘空间
|
||||
|
||||
### 错误:`port already allocated`
|
||||
- 检查端口占用情况
|
||||
- 终止占用端口的进程
|
||||
|
||||
### 错误:`image not found`
|
||||
- 手动拉取缺失的镜像
|
||||
- 检查镜像名称和标签是否正确
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 查看服务日志
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker-compose -f docker-compose-full.yml logs
|
||||
|
||||
# 查看特定服务日志
|
||||
docker-compose -f docker-compose-full.yml logs backend
|
||||
docker-compose -f docker-compose-full.yml logs frontend
|
||||
```
|
||||
|
||||
### 进入容器调试
|
||||
```bash
|
||||
# 进入后端容器
|
||||
docker exec -it algorithm-showcase-backend bash
|
||||
|
||||
# 进入前端容器
|
||||
docker exec -it algorithm-showcase-frontend sh
|
||||
```
|
||||
|
||||
### 清理部署
|
||||
```bash
|
||||
# 停止并删除所有服务
|
||||
docker-compose -f docker-compose-full.yml down
|
||||
|
||||
# 删除卷(谨慎使用,会删除所有数据)
|
||||
docker-compose -f docker-compose-full.yml down -v
|
||||
|
||||
# 清理孤立容器
|
||||
docker container prune
|
||||
```
|
||||
|
||||
## 联系支持
|
||||
|
||||
如果遇到无法解决的问题,请联系技术支持并提供以下信息:
|
||||
|
||||
1. 操作系统版本
|
||||
2. Docker和Docker Compose版本
|
||||
3. 完整的错误日志
|
||||
4. 已尝试的解决方案
|
||||
11
api-gateway/Dockerfile
Normal file
11
api-gateway/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM nginx:latest
|
||||
|
||||
# 复制nginx配置文件
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY ai-services.conf /etc/nginx/conf.d/ai-services.conf
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
# 启动nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
60
api-gateway/ai-services.conf
Normal file
60
api-gateway/ai-services.conf
Normal file
@@ -0,0 +1,60 @@
|
||||
# /etc/nginx/conf.d/ai-services.conf
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 服务1:文本分类(网关路径/ai/text-classify)
|
||||
location /ai/text-classify/ {
|
||||
proxy_pass http://text-classification:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# 服务2:图像识别(网关路径/ai/image-recog)
|
||||
location /ai/image-recog/ {
|
||||
proxy_pass http://image-recognition:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# 服务3:语音转文字(网关路径/ai/speech-to-text)
|
||||
location /ai/speech-to-text/ {
|
||||
proxy_pass http://speech-to-text:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# OpenAI代理服务(网关路径/ai/openai)
|
||||
location /ai/openai/ {
|
||||
proxy_pass http://openai-proxy:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# 后端服务(网关路径/api)
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
return 200 'OK';
|
||||
}
|
||||
}
|
||||
32
api-gateway/nginx.conf
Normal file
32
api-gateway/nginx.conf
Normal file
@@ -0,0 +1,32 @@
|
||||
# nginx.conf
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
# Load dynamic modules.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
# 智能算法展示平台架构设计
|
||||
|
||||
## 1. 架构设计理念
|
||||
|
||||
智能算法展示平台的架构设计参考了MLflow的核心思想,采用分层架构和模块化设计,确保系统的可扩展性、可维护性和易用性。平台以算法为中心,围绕算法的注册、管理、调用和展示构建完整的生态系统。
|
||||
|
||||
**设计理念:**
|
||||
- **分层架构:** 清晰的层次划分,各层职责明确,便于独立开发和维护
|
||||
- **模块化设计:** 功能模块解耦,便于扩展和重用
|
||||
- **标准化接口:** 统一的API接口设计,确保系统内部和外部集成的一致性
|
||||
- **简洁实用:** 核心功能紧凑实现,满足内部使用需求
|
||||
- **可观测性:** 基本的监控和日志系统,确保系统的可靠性和可维护性
|
||||
|
||||
## 2. 整体架构图
|
||||
|
||||
```
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | | | | | |
|
||||
| 前端客户展示层 | | 后端核心服务层 | | 算法API层 | | 开发SDK和工具模块 |
|
||||
| (Vue3 + TypeScript) | | (Python Web Framework) | | (算法封装与执行) | | (SDK开发与工具) |
|
||||
| | | | | | | |
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | | | | | |
|
||||
| 1. 仿真输入获取模块 | | 1. API网关 | | 1. 算法注册 | | 1. SDK开发 |
|
||||
| 2. 算法调用模块 | | 2. 服务管理 | | 2. 版本管理 | | 2. 命令行工具 |
|
||||
| 3. 效果展示模块 | | 3. 数据管理 | | 3. 权限配置 | | 3. API文档 |
|
||||
| | | 4. 监控与日志 | | 4. 算法执行 | | 4. 示例代码 |
|
||||
| | | | | | | |
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | | | | | |
|
||||
| 用户交互界面 | | 核心业务逻辑 | | 算法封装与执行 | | 系统集成与二次开发 |
|
||||
| | | | | | | |
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | | | | | |
|
||||
| 调用前端API | | 调用后端服务 | | 调用算法实现 | | 调用SDK和工具 |
|
||||
| | | | | | | |
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | |
|
||||
+--------------------------+--------------------------+--------------------------+
|
||||
|
|
||||
v
|
||||
+------------------------+
|
||||
| |
|
||||
| 基础设施层 |
|
||||
| |
|
||||
+------------------------+
|
||||
| |
|
||||
| 1. 数据存储 |
|
||||
| - PostgreSQL |
|
||||
| - Redis |
|
||||
| - MinIO |
|
||||
| 2. 容器化部署 |
|
||||
| - Docker |
|
||||
| - Docker Compose |
|
||||
| 3. 第三方服务 |
|
||||
| - OpenAI API |
|
||||
| 4. 监控与日志 |
|
||||
| - 简单日志管理 |
|
||||
| - 基本监控指标 |
|
||||
| |
|
||||
+------------------------+
|
||||
```
|
||||
|
||||
## 3. 各层详细设计
|
||||
|
||||
### 3.1 前端客户展示层
|
||||
|
||||
**职责:**
|
||||
- 负责用户交互和效果展示
|
||||
- 提供直观、友好的用户界面
|
||||
- 处理用户输入和请求
|
||||
- 展示算法执行结果和可视化效果
|
||||
|
||||
**核心组件:**
|
||||
- **仿真输入获取组件:** 集成OpenAI API,支持多种类型的输入数据
|
||||
- **算法调用组件:** 提供算法目录和参数配置界面
|
||||
- **效果展示组件:** 提供多维度的可视化效果展示
|
||||
- **历史记录组件:** 管理用户的测试历史
|
||||
|
||||
**技术实现:**
|
||||
- **框架:** Vue 3 + TypeScript
|
||||
- **构建工具:** Vite
|
||||
- **状态管理:** Pinia
|
||||
- **UI组件库:** Element Plus
|
||||
- **可视化库:** ECharts
|
||||
- **HTTP客户端:** Axios
|
||||
- **路由:** Vue Router
|
||||
|
||||
### 3.2 后端核心服务层
|
||||
|
||||
**职责:**
|
||||
- 处理前端请求和业务逻辑
|
||||
- 管理算法服务的基本配置和状态
|
||||
- 存储和管理数据
|
||||
- 监控系统运行状态
|
||||
|
||||
**核心组件:**
|
||||
- **API网关:** 请求路由、认证授权、流量控制
|
||||
- **服务管理:** 服务基本配置和状态管理
|
||||
- **数据管理:** 输入数据存储、输出结果存储、元数据管理
|
||||
- **监控与日志:** 基本的调用监控、日志管理和告警
|
||||
|
||||
**技术实现:**
|
||||
- **Web框架:** FastAPI(推荐)或 Flask
|
||||
- **数据库:** PostgreSQL
|
||||
- **缓存:** Redis
|
||||
- **消息队列:** RabbitMQ
|
||||
- **认证:** JWT
|
||||
- **API文档:** OpenAPI
|
||||
|
||||
### 3.3 算法API层
|
||||
|
||||
**职责:**
|
||||
- 封装算法实现
|
||||
- 提供标准化的API接口
|
||||
- 执行算法并返回结果
|
||||
- 管理算法版本和权限
|
||||
|
||||
**核心组件:**
|
||||
- **算法注册:** 管理算法信息和API规范
|
||||
- **版本管理:** 支持算法多版本管理和切换
|
||||
- **权限配置:** 控制算法的访问权限
|
||||
- **算法执行:** 处理输入数据,执行算法逻辑
|
||||
|
||||
**技术实现:**
|
||||
- **Web框架:** FastAPI或Flask
|
||||
- **容器化:** Docker
|
||||
- **部署管理:** Docker Compose
|
||||
- **版本控制:** Git
|
||||
- **权限管理:** RBAC
|
||||
|
||||
### 3.4 基础设施层
|
||||
|
||||
**职责:**
|
||||
- 提供系统运行所需的基础设施
|
||||
- 支持系统的扩展和部署
|
||||
- 确保系统的可靠性和安全性
|
||||
|
||||
**核心组件:**
|
||||
- **数据存储:** PostgreSQL(结构化数据)、Redis(缓存)、MinIO(非结构化数据)
|
||||
- **容器化部署:** Docker(容器化)、Docker Compose(部署管理)
|
||||
- **第三方服务:** OpenAI API(用于生成仿真输入数据)
|
||||
- **监控与日志:** 简单日志管理、基本监控指标
|
||||
|
||||
**技术实现:**
|
||||
- **容器技术:** Docker
|
||||
- **部署工具:** Docker Compose
|
||||
- **存储服务:** PostgreSQL、Redis、MinIO
|
||||
- **监控工具:** 轻量级监控方案(如内置监控功能或简单日志)
|
||||
|
||||
### 3.5 开发SDK和工具模块
|
||||
|
||||
**职责:**
|
||||
- 提供开发SDK,便于系统集成和二次开发
|
||||
- 开发命令行工具,支持算法管理和调用测试
|
||||
- 生成API文档,便于开发者理解和使用系统
|
||||
- 提供示例代码,便于开发者快速上手
|
||||
|
||||
**核心组件:**
|
||||
- **SDK开发:** 提供Python、JavaScript等多种语言的SDK
|
||||
- **命令行工具:** 支持算法管理、调用测试等功能
|
||||
- **API文档:** 基于OpenAPI规范生成详细的API文档
|
||||
- **示例代码:** 提供丰富的示例代码,覆盖常见使用场景
|
||||
|
||||
**技术实现:**
|
||||
- **SDK开发:** 使用Python包管理工具(如pip)发布Python SDK,使用npm发布JavaScript SDK
|
||||
- **命令行工具:** 使用Click或argparse实现命令行工具
|
||||
- **API文档:** 使用OpenAPI规范生成API文档
|
||||
- **示例代码:** 提供Python、JavaScript等多种语言的示例代码
|
||||
|
||||
## 4. 组件交互流程
|
||||
|
||||
### 4.1 算法调用流程
|
||||
|
||||
```
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | | | | | |
|
||||
| 前端客户展示层 | | 后端核心服务层 | | 算法API层 | | 基础设施层 |
|
||||
| | | | | | | |
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | |
|
||||
| 1. 发送算法调用请求 | | |
|
||||
+---------------------------> | | |
|
||||
| | 2. 验证用户身份和权限 | |
|
||||
| |----------------------------> | |
|
||||
| | | 3. 发现对应的算法服务 |
|
||||
| | |----------------------------> |
|
||||
| | | 4. 执行算法 |
|
||||
| | |----------------------------> |
|
||||
| | | 5. 返回执行结果 |
|
||||
| | 6. 存储调用记录 | <---------------------------|
|
||||
| 7. 展示算法执行结果 | <---------------------------|
|
||||
| <---------------------------|
|
||||
```
|
||||
|
||||
### 4.2 算法注册流程
|
||||
|
||||
```
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | | | | | |
|
||||
| 前端客户展示层 | | 后端核心服务层 | | 算法API层 | | 基础设施层 |
|
||||
| | | | | | | |
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | |
|
||||
| 1. 填写算法注册信息 | | |
|
||||
+---------------------------> | | |
|
||||
| | 2. 验证管理员权限 | |
|
||||
| |----------------------------> | |
|
||||
| | 3. 存储算法信息 | |
|
||||
| |----------------------------> | |
|
||||
| | 4. 配置算法部署 | |
|
||||
| |----------------------------> | |
|
||||
| | 5. 测试算法API | |
|
||||
| |----------------------------> | |
|
||||
| | 6. 发布算法 | |
|
||||
| 7. 算法注册成功提示 | <---------------------------|
|
||||
| <---------------------------|
|
||||
```
|
||||
|
||||
### 4.3 效果对比流程
|
||||
|
||||
```
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | | | | | |
|
||||
| 前端客户展示层 | | 后端核心服务层 | | 算法API层 | | 基础设施层 |
|
||||
| | | | | | | |
|
||||
+------------------------+ +------------------------+ +------------------------+ +------------------------+
|
||||
| | | |
|
||||
| 1. 选择对比算法和参数 | | |
|
||||
+---------------------------> | | |
|
||||
| | 2. 验证用户身份 | |
|
||||
| |----------------------------> | |
|
||||
| | 3. 依次执行每个算法 | |
|
||||
| |----------------------------> | |
|
||||
| | | 4. 执行算法1 |
|
||||
| | |----------------------------> |
|
||||
| | | 5. 返回执行结果1 |
|
||||
| | 6. 存储执行结果1 | <---------------------------|
|
||||
| | 7. 执行算法2 | |
|
||||
| |----------------------------> | |
|
||||
| | | 8. 执行算法2 |
|
||||
| | |----------------------------> |
|
||||
| | | 9. 返回执行结果2 |
|
||||
| | 10. 存储执行结果2 | <---------------------------|
|
||||
| | 11. 生成对比结果 | |
|
||||
| 12. 展示对比结果 | <---------------------------|
|
||||
| <---------------------------|
|
||||
```
|
||||
|
||||
## 5. 技术选型理由
|
||||
|
||||
### 5.1 前端技术选型
|
||||
|
||||
- **Vue 3 + TypeScript:** 提供强类型支持和组件化开发能力,提高代码质量和开发效率
|
||||
- **Vite:** 快速的构建工具,提供更好的开发体验和构建性能
|
||||
- **Pinia:** 轻量级状态管理库,替代Vuex,提供更好的TypeScript支持
|
||||
- **Element Plus:** 功能丰富的UI组件库,提供良好的用户体验
|
||||
- **Three.js:** 3D可视化库,用于复杂数据的三维展示
|
||||
- **ECharts:** 强大的图表库,用于数据的二维可视化
|
||||
|
||||
### 5.2 后端技术选型
|
||||
|
||||
- **FastAPI:** 高性能的Python Web框架,提供自动API文档生成和类型提示
|
||||
- **PostgreSQL:** 功能强大的关系型数据库,支持复杂查询和事务
|
||||
- **Redis:** 高性能的缓存数据库,用于缓存热点数据和管理会话
|
||||
- **RabbitMQ:** 可靠的消息队列,用于异步任务处理和服务解耦
|
||||
- **JWT:** 无状态的认证机制,便于水平扩展
|
||||
|
||||
### 5.3 部署技术选型
|
||||
|
||||
- **Docker:** 容器化技术,确保环境一致性和部署效率
|
||||
- **Docker Compose:** 简化多容器应用的部署和管理,适合内部使用的小型系统
|
||||
|
||||
### 5.4 数据存储选型
|
||||
|
||||
- **PostgreSQL:** 功能强大的关系型数据库,支持复杂查询和事务,适合存储结构化数据
|
||||
- **Redis:** 高性能的缓存数据库,用于缓存热点数据和管理会话,提高系统性能
|
||||
- **MinIO:** 兼容S3的对象存储服务,适合存储非结构化数据(如图片、视频),部署简单,适合内部使用
|
||||
|
||||
### 5.5 第三方服务选型
|
||||
|
||||
- **OpenAI API:** 用于生成仿真输入数据,支持通过文本描述生成各种类型的输入数据,提高系统的灵活性和用户体验
|
||||
|
||||
## 6. 部署和扩展方案
|
||||
|
||||
### 6.1 部署方案
|
||||
|
||||
**开发环境:**
|
||||
- 本地Docker Compose部署,包含所有必要的服务
|
||||
- 前端使用Vite开发服务器,支持热重载
|
||||
- 后端使用FastAPI开发服务器,支持自动重载
|
||||
|
||||
**测试环境:**
|
||||
- 本地或内网服务器Docker Compose部署
|
||||
- 模拟真实用户流量进行测试
|
||||
- 基本的监控和日志系统
|
||||
|
||||
**生产环境:**
|
||||
- 内网服务器Docker Compose部署
|
||||
- 单节点或少量节点部署,满足内部使用需求
|
||||
- 手动部署,通过Docker Compose命令进行服务管理
|
||||
|
||||
### 6.2 扩展方案
|
||||
|
||||
**功能扩展:**
|
||||
- **功能模块扩展:** 通过模块化设计,支持新功能的快速集成
|
||||
- **算法类型扩展:** 通过标准化的API接口,支持新算法类型的集成
|
||||
- **数据源扩展:** 通过适配器模式,支持新数据源的集成
|
||||
|
||||
**技术栈扩展:**
|
||||
- **框架升级:** 支持框架版本的平滑升级,确保系统的稳定性和安全性
|
||||
- **数据库迁移:** 支持数据库的平滑迁移和升级,确保数据的一致性和可靠性
|
||||
|
||||
## 7. 架构优势
|
||||
|
||||
### 7.1 可扩展性
|
||||
|
||||
- **分层架构:** 各层独立扩展,互不影响
|
||||
- **模块化设计:** 功能模块解耦,便于新功能的快速集成
|
||||
- **标准化接口:** 统一的API接口,便于集成新的算法和服务
|
||||
|
||||
### 7.2 可维护性
|
||||
|
||||
- **模块化设计:** 功能模块解耦,便于独立开发和维护
|
||||
- **标准化代码:** 统一的代码风格和规范,提高代码可读性
|
||||
- **完善的文档:** 详细的系统文档和API文档,便于理解和使用
|
||||
- **可观测性:** 基本的监控和日志系统,便于问题排查和系统优化
|
||||
|
||||
### 7.3 易用性
|
||||
|
||||
- **直观的用户界面:** 友好的前端界面,便于用户操作和使用
|
||||
- **标准化API:** 统一的API接口设计,便于系统集成和二次开发
|
||||
- **简化部署:** 使用Docker Compose本地部署,简化部署流程
|
||||
- **集中管理:** 集中的管理界面,便于系统管理和监控
|
||||
|
||||
### 7.4 可靠性
|
||||
|
||||
- **容错机制:** 完善的错误处理和容错机制,确保系统的稳定性
|
||||
- **数据备份:** 定期数据备份,确保数据的安全性和可靠性
|
||||
- **安全措施:** 基本的安全措施,确保系统和数据的安全
|
||||
|
||||
## 8. 架构演进路线
|
||||
|
||||
### 8.1 第一阶段:基础架构搭建
|
||||
|
||||
- 搭建前端客户展示层、后端核心服务层和算法API层的基础架构
|
||||
- 实现核心功能模块,包括算法注册、调用和展示
|
||||
- 配置基础设施层,包括数据存储和Docker Compose部署
|
||||
- 完成系统集成和测试
|
||||
|
||||
### 8.2 第二阶段:功能完善
|
||||
|
||||
- 完善前端客户展示层的功能,包括效果展示和对比
|
||||
- 增强后端核心服务层的能力,包括服务管理和基本监控
|
||||
- 扩展算法API层的功能,包括版本管理和权限配置
|
||||
- 优化系统性能和用户体验
|
||||
|
||||
### 8.3 第三阶段:功能扩展
|
||||
|
||||
- 支持更多类型的算法和输入数据格式
|
||||
- 集成必要的第三方服务,如OpenAI(如果需要)
|
||||
- 开发简单的工具,便于系统集成和使用
|
||||
- 完善系统文档,促进内部使用和维护
|
||||
|
||||
### 8.4 第四阶段:优化升级
|
||||
|
||||
- 优化系统性能,提高算法执行效率
|
||||
- 增强系统稳定性和可靠性
|
||||
- 根据内部使用反馈,持续优化系统功能
|
||||
- 简化运维流程,减少人工干预
|
||||
|
||||
## 9. 结论
|
||||
|
||||
智能算法展示平台的架构设计参考了MLflow的核心思想,采用分层架构和模块化设计,确保系统的可扩展性、可维护性和易用性。平台以算法为中心,围绕算法的注册、管理、调用和展示构建完整的生态系统,为用户提供直观、友好的算法测试和展示体验。
|
||||
|
||||
通过合理的技术选型和架构设计,平台能够支持多种类型的算法和部署方式,满足不同用户的需求。同时,平台的可扩展性和可维护性确保了系统能够随着业务需求的变化而不断演进和优化。
|
||||
|
||||
智能算法展示平台的架构设计为系统的开发和部署提供了清晰的指导,确保系统能够高质量、高效率地完成开发和上线,为用户提供优质的算法展示和测试服务。
|
||||
0
backend/algorithm_showcase.db
Normal file
0
backend/algorithm_showcase.db
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -46,11 +47,56 @@ class Settings(BaseSettings):
|
||||
GITEA_DEFAULT_OWNER: str = ""
|
||||
GITEA_REPO_PREFIX: str = "AI"
|
||||
|
||||
# 服务管理配置
|
||||
SERVICE_MANAGEMENT: Dict[str, Any] = {
|
||||
"mode": "supervisor", # 服务管理模式:local, docker, supervisor
|
||||
"service_root_dir": "/opt/ai-services",
|
||||
"supervisor_config_dir": "/etc/supervisor/conf.d",
|
||||
}
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
extra = "allow" # 允许额外的环境变量
|
||||
|
||||
def get_config(self, config_key: str, default: Any = None) -> Any:
|
||||
"""获取配置,优先级:环境变量 > 数据库 > 文件默认值
|
||||
|
||||
Args:
|
||||
config_key: 配置键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
配置值
|
||||
"""
|
||||
# 1. 先从环境变量获取
|
||||
env_key = config_key.upper().replace('.', '_')
|
||||
env_value = getattr(self, env_key, None)
|
||||
if env_value is not None:
|
||||
return env_value
|
||||
|
||||
# 2. 从数据库获取
|
||||
try:
|
||||
from app.models.database import SessionLocal
|
||||
from app.models.models import ServiceConfig
|
||||
|
||||
db: Session = SessionLocal()
|
||||
try:
|
||||
config = db.query(ServiceConfig).filter_by(
|
||||
config_key=config_key,
|
||||
status="active"
|
||||
).first()
|
||||
if config:
|
||||
return config.config_value
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
# 数据库连接失败时,返回默认值
|
||||
print(f"Failed to load config from database: {str(e)}")
|
||||
|
||||
# 3. 返回默认值
|
||||
return default
|
||||
|
||||
|
||||
# 创建全局配置实例
|
||||
settings = Settings()
|
||||
|
||||
@@ -72,9 +72,24 @@ class APIGateway:
|
||||
if not version_info:
|
||||
raise HTTPException(status_code=404, detail="Algorithm version not found")
|
||||
|
||||
# 在实际实现中,这里会根据version_info.url将请求转发到对应的算法服务
|
||||
# 现在我们模拟调用过程
|
||||
algorithm_url = version_info.url if hasattr(version_info, 'url') else f"http://localhost:8001/algorithms/{algorithm_id}/execute"
|
||||
# 尝试从算法版本获取URL,如果没有则尝试从服务表获取
|
||||
algorithm_url = None
|
||||
|
||||
# 首先检查版本信息中是否有URL
|
||||
if hasattr(version_info, 'url') and version_info.url:
|
||||
algorithm_url = version_info.url
|
||||
else:
|
||||
# 如果版本信息中没有URL,尝试从服务表获取
|
||||
from app.models.models import AlgorithmService
|
||||
service = db.query(AlgorithmService).filter(
|
||||
AlgorithmService.algorithm_name == algorithm_id
|
||||
).first()
|
||||
|
||||
if service and service.api_url:
|
||||
algorithm_url = service.api_url
|
||||
else:
|
||||
# 如果都没有,使用默认的本地端点
|
||||
algorithm_url = f"http://localhost:8001/algorithms/{algorithm_id}/execute"
|
||||
|
||||
# 使用httpx调用算法服务
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
|
||||
@@ -4,7 +4,8 @@ from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
|
||||
from app.config.settings import settings
|
||||
from app.models.database import engine, Base
|
||||
from app.routes import user, algorithm, openai, gateway, services, data_management, monitoring, permissions, history, deployment, gitea, repositories
|
||||
from app.models import models, api # 导入所有模型以确保表被创建
|
||||
from app.routes import user, algorithm, openai, gateway, services, data_management, monitoring, permissions, history, deployment, gitea, repositories, config, comparison, api_management
|
||||
|
||||
# 创建数据库表
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@@ -48,6 +49,9 @@ app.include_router(history.router, prefix=settings.API_V1_STR)
|
||||
app.include_router(deployment.router)
|
||||
app.include_router(gitea.router, prefix=settings.API_V1_STR)
|
||||
app.include_router(repositories.router, prefix=settings.API_V1_STR)
|
||||
app.include_router(config.router, prefix=settings.API_V1_STR)
|
||||
app.include_router(comparison.router, prefix=settings.API_V1_STR)
|
||||
app.include_router(api_management.router, prefix=settings.API_V1_STR)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
||||
BIN
backend/app/models/__pycache__/api.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
74
backend/app/models/api.py
Normal file
74
backend/app/models/api.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""API封装模型"""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Text, Boolean, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from app.models.database import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class ApiEndpoint(Base):
|
||||
"""API端点模型"""
|
||||
__tablename__ = "api_endpoints"
|
||||
|
||||
id = Column(String, primary_key=True, index=True, default=lambda: str(uuid.uuid4()))
|
||||
name = Column(String, nullable=False, index=True) # API名称
|
||||
description = Column(Text, default="") # API描述
|
||||
path = Column(String, nullable=False, unique=True, index=True) # API路径,如 /api/image-classification
|
||||
method = Column(String, default="POST") # HTTP方法:GET, POST, PUT, DELETE
|
||||
algorithm_id = Column(String, ForeignKey("algorithms.id"), nullable=False, index=True) # 关联的算法ID
|
||||
version_id = Column(String, ForeignKey("algorithm_versions.id"), nullable=False, index=True) # 关联的算法版本ID
|
||||
service_id = Column(String, ForeignKey("algorithm_services.service_id"), nullable=True, index=True) # 关联的服务ID
|
||||
|
||||
# API配置
|
||||
config = Column(JSON, nullable=False, default={}) # API配置(超时、重试等)
|
||||
request_schema = Column(JSON, nullable=True) # 请求参数schema
|
||||
response_schema = Column(JSON, nullable=True) # 响应参数schema
|
||||
|
||||
# 权限配置
|
||||
requires_auth = Column(Boolean, default=True) # 是否需要认证
|
||||
allowed_roles = Column(JSON, default=[]) # 允许的角色列表
|
||||
rate_limit = Column(JSON, nullable=True) # 限流配置,如 {"max_requests": 100, "window": 60}
|
||||
|
||||
# 状态
|
||||
status = Column(String, default="active", index=True) # 状态:active, inactive, deprecated
|
||||
is_public = Column(Boolean, default=False) # 是否公开
|
||||
|
||||
# 统计信息
|
||||
call_count = Column(String, default="0") # 调用次数
|
||||
success_count = Column(String, default="0") # 成功次数
|
||||
error_count = Column(String, default="0") # 错误次数
|
||||
avg_response_time = Column(String, default="0.0") # 平均响应时间
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=datetime.utcnow)
|
||||
last_called_at = Column(DateTime(timezone=True), nullable=True) # 最后调用时间
|
||||
|
||||
|
||||
class ApiCallLog(Base):
|
||||
"""API调用日志模型"""
|
||||
__tablename__ = "api_call_logs"
|
||||
|
||||
id = Column(String, primary_key=True, index=True, default=lambda: str(uuid.uuid4()))
|
||||
api_endpoint_id = Column(String, ForeignKey("api_endpoints.id"), nullable=False, index=True) # API端点ID
|
||||
user_id = Column(String, ForeignKey("users.id"), nullable=True, index=True) # 调用用户ID
|
||||
|
||||
# 请求信息
|
||||
request_method = Column(String, nullable=False) # 请求方法
|
||||
request_path = Column(String, nullable=False) # 请求路径
|
||||
request_headers = Column(JSON, nullable=True) # 请求头
|
||||
request_body = Column(JSON, nullable=True) # 请求体
|
||||
|
||||
# 响应信息
|
||||
response_status = Column(String, nullable=False) # 响应状态码
|
||||
response_body = Column(JSON, nullable=True) # 响应体
|
||||
response_time = Column(String, nullable=False) # 响应时间(秒)
|
||||
|
||||
# 错误信息
|
||||
error_message = Column(Text, nullable=True) # 错误信息
|
||||
error_type = Column(String, nullable=True) # 错误类型
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow, index=True) # 调用时间
|
||||
@@ -13,6 +13,8 @@ class Algorithm(Base):
|
||||
name = Column(String, nullable=False, index=True)
|
||||
description = Column(Text, nullable=False)
|
||||
type = Column(String, nullable=False, index=True) # computer_vision, nlp, ml, edge_computing, medical, autonomous_driving等
|
||||
tech_category = Column(String, nullable=False, default="computer_vision") # 技术分类:计算机视觉、视频处理、自然语言处理等
|
||||
output_type = Column(String, nullable=False, default="image") # 输出类型:图片、视频、文本、JSON等
|
||||
status = Column(String, default="active", index=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
@@ -159,6 +161,8 @@ class AlgorithmService(Base):
|
||||
name = Column(String, nullable=False, index=True) # 服务名称
|
||||
algorithm_name = Column(String, nullable=False) # 算法名称
|
||||
version = Column(String, nullable=False) # 版本
|
||||
tech_category = Column(String, nullable=False, default="computer_vision") # 技术分类:计算机视觉、视频处理、自然语言处理等
|
||||
output_type = Column(String, nullable=False, default="image") # 输出类型:图片、视频、文本、JSON等
|
||||
host = Column(String, nullable=False) # 主机地址
|
||||
port = Column(Integer, nullable=False) # 端口
|
||||
api_url = Column(String, nullable=False) # API地址
|
||||
@@ -172,3 +176,18 @@ class AlgorithmService(Base):
|
||||
|
||||
# 添加Algorithm模型的repository关系
|
||||
Algorithm.repository = relationship("AlgorithmRepository", back_populates="algorithm", uselist=False)
|
||||
|
||||
|
||||
class ServiceConfig(Base):
|
||||
"""服务配置模型"""
|
||||
__tablename__ = "service_configs"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
config_key = Column(String, nullable=False, unique=True, index=True) # 配置键
|
||||
config_value = Column(JSON, nullable=False) # 配置值(JSON格式)
|
||||
config_type = Column(String, nullable=False) # 配置类型:system, service, user
|
||||
service_id = Column(String, nullable=True, index=True) # 服务ID(可为空,系统配置)
|
||||
description = Column(Text, default="") # 配置描述
|
||||
status = Column(String, default="active") # 状态
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from app.routes import user, algorithm, history, gateway, monitoring, openai, deployment
|
||||
from app.routes import user, algorithm, history, gateway, monitoring, openai, deployment, config, comparison
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -11,3 +11,5 @@ api_router.include_router(gateway.router, prefix="/gateway", tags=["gateway"])
|
||||
api_router.include_router(monitoring.router, prefix="/monitoring", tags=["monitoring"])
|
||||
api_router.include_router(openai.router, prefix="/openai", tags=["openai"])
|
||||
api_router.include_router(deployment.router, tags=["deployment"])
|
||||
api_router.include_router(config.router, tags=["config"])
|
||||
api_router.include_router(comparison.router, tags=["comparison"])
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/app/routes/__pycache__/api_management.cpython-312.pyc
Normal file
BIN
backend/app/routes/__pycache__/api_management.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routes/__pycache__/comparison.cpython-312.pyc
Normal file
BIN
backend/app/routes/__pycache__/comparison.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/routes/__pycache__/config.cpython-312.pyc
Normal file
BIN
backend/app/routes/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -33,6 +33,18 @@ async def create_algorithm(
|
||||
|
||||
|
||||
@router.get("", response_model=AlgorithmListResponse)
|
||||
async def get_algorithms_no_slash(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
type: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取算法列表(不带末尾斜杠)"""
|
||||
algorithms = AlgorithmService.get_algorithms(db, skip=skip, limit=limit, algorithm_type=type)
|
||||
return {"algorithms": algorithms, "total": len(algorithms)}
|
||||
|
||||
|
||||
@router.get("/", response_model=AlgorithmListResponse)
|
||||
async def get_algorithms(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
|
||||
510
backend/app/routes/api_management.py
Normal file
510
backend/app/routes/api_management.py
Normal file
@@ -0,0 +1,510 @@
|
||||
"""API管理路由,处理API端点的封装和管理"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from app.models.database import get_db
|
||||
from app.models.models import Algorithm, AlgorithmVersion, AlgorithmService, User
|
||||
from app.models.api import ApiEndpoint, ApiCallLog
|
||||
from app.schemas.user import UserResponse
|
||||
from app.routes.user import get_current_active_user
|
||||
|
||||
router = APIRouter(prefix="/api-management", tags=["api-management"])
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiEndpointCreate(BaseModel):
|
||||
"""创建API端点请求模型"""
|
||||
name: str
|
||||
description: str = ""
|
||||
path: str
|
||||
method: str = "POST"
|
||||
algorithm_id: str
|
||||
version_id: str
|
||||
service_id: Optional[str] = None
|
||||
requires_auth: bool = True
|
||||
allowed_roles: List[str] = []
|
||||
rate_limit: Optional[Dict[str, Any]] = None
|
||||
is_public: bool = False
|
||||
config: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class ApiEndpointUpdate(BaseModel):
|
||||
"""更新API端点请求模型"""
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
path: Optional[str] = None
|
||||
method: Optional[str] = None
|
||||
requires_auth: Optional[bool] = None
|
||||
allowed_roles: Optional[List[str]] = None
|
||||
rate_limit: Optional[Dict[str, Any]] = None
|
||||
is_public: Optional[bool] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class ApiEndpointResponse(BaseModel):
|
||||
"""API端点响应模型"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
path: str
|
||||
method: str
|
||||
algorithm_id: str
|
||||
algorithm_name: str
|
||||
version_id: str
|
||||
version: str
|
||||
service_id: Optional[str]
|
||||
status: str
|
||||
is_public: bool
|
||||
call_count: str
|
||||
success_count: str
|
||||
error_count: str
|
||||
avg_response_time: str
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
last_called_at: Optional[datetime]
|
||||
|
||||
|
||||
class ApiEndpointListResponse(BaseModel):
|
||||
"""API端点列表响应模型"""
|
||||
endpoints: List[ApiEndpointResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class ApiStatsResponse(BaseModel):
|
||||
"""API统计响应模型"""
|
||||
total_endpoints: int
|
||||
active_endpoints: int
|
||||
total_calls: str
|
||||
total_success: str
|
||||
total_errors: str
|
||||
avg_response_time: str
|
||||
|
||||
|
||||
@router.get("/endpoints", response_model=ApiEndpointListResponse)
|
||||
async def get_api_endpoints(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
algorithm_id: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""获取API端点列表"""
|
||||
try:
|
||||
# 构建查询
|
||||
query = db.query(ApiEndpoint)
|
||||
|
||||
# 筛选条件
|
||||
if algorithm_id:
|
||||
query = query.filter(ApiEndpoint.algorithm_id == algorithm_id)
|
||||
if status:
|
||||
query = query.filter(ApiEndpoint.status == status)
|
||||
|
||||
# 分页
|
||||
endpoints = query.offset(skip).limit(limit).all()
|
||||
total = query.count()
|
||||
|
||||
# 构建响应
|
||||
endpoint_responses = []
|
||||
for endpoint in endpoints:
|
||||
# 获取关联的算法和版本信息
|
||||
algorithm = db.query(Algorithm).filter(Algorithm.id == endpoint.algorithm_id).first()
|
||||
version = db.query(AlgorithmVersion).filter(AlgorithmVersion.id == endpoint.version_id).first()
|
||||
|
||||
endpoint_responses.append({
|
||||
"id": endpoint.id,
|
||||
"name": endpoint.name,
|
||||
"description": endpoint.description,
|
||||
"path": endpoint.path,
|
||||
"method": endpoint.method,
|
||||
"algorithm_id": endpoint.algorithm_id,
|
||||
"algorithm_name": algorithm.name if algorithm else "",
|
||||
"version_id": endpoint.version_id,
|
||||
"version": version.version if version else "",
|
||||
"service_id": endpoint.service_id,
|
||||
"status": endpoint.status,
|
||||
"is_public": endpoint.is_public,
|
||||
"call_count": endpoint.call_count,
|
||||
"success_count": endpoint.success_count,
|
||||
"error_count": endpoint.error_count,
|
||||
"avg_response_time": endpoint.avg_response_time,
|
||||
"created_at": endpoint.created_at,
|
||||
"updated_at": endpoint.updated_at,
|
||||
"last_called_at": endpoint.last_called_at
|
||||
})
|
||||
|
||||
return {
|
||||
"endpoints": endpoint_responses,
|
||||
"total": total
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取API端点列表失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取API端点列表失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/endpoints/{endpoint_id}", response_model=ApiEndpointResponse)
|
||||
async def get_api_endpoint(
|
||||
endpoint_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""获取API端点详情"""
|
||||
try:
|
||||
endpoint = db.query(ApiEndpoint).filter(ApiEndpoint.id == endpoint_id).first()
|
||||
|
||||
if not endpoint:
|
||||
raise HTTPException(status_code=404, detail="API端点不存在")
|
||||
|
||||
# 获取关联的算法和版本信息
|
||||
algorithm = db.query(Algorithm).filter(Algorithm.id == endpoint.algorithm_id).first()
|
||||
version = db.query(AlgorithmVersion).filter(AlgorithmVersion.id == endpoint.version_id).first()
|
||||
|
||||
return {
|
||||
"id": endpoint.id,
|
||||
"name": endpoint.name,
|
||||
"description": endpoint.description,
|
||||
"path": endpoint.path,
|
||||
"method": endpoint.method,
|
||||
"algorithm_id": endpoint.algorithm_id,
|
||||
"algorithm_name": algorithm.name if algorithm else "",
|
||||
"version_id": endpoint.version_id,
|
||||
"version": version.version if version else "",
|
||||
"service_id": endpoint.service_id,
|
||||
"status": endpoint.status,
|
||||
"is_public": endpoint.is_public,
|
||||
"call_count": endpoint.call_count,
|
||||
"success_count": endpoint.success_count,
|
||||
"error_count": endpoint.error_count,
|
||||
"avg_response_time": endpoint.avg_response_time,
|
||||
"created_at": endpoint.created_at,
|
||||
"updated_at": endpoint.updated_at,
|
||||
"last_called_at": endpoint.last_called_at
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取API端点详情失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取API端点详情失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/endpoints", response_model=ApiEndpointResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_api_endpoint(
|
||||
request: ApiEndpointCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""创建API端点"""
|
||||
try:
|
||||
# 检查用户权限
|
||||
if not hasattr(current_user, 'role_name') or current_user.role_name != "admin":
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
# 验证算法和版本是否存在
|
||||
algorithm = db.query(Algorithm).filter(Algorithm.id == request.algorithm_id).first()
|
||||
if not algorithm:
|
||||
raise HTTPException(status_code=404, detail="算法不存在")
|
||||
|
||||
version = db.query(AlgorithmVersion).filter(
|
||||
AlgorithmVersion.id == request.version_id
|
||||
).first()
|
||||
if not version or version.algorithm_id != request.algorithm_id:
|
||||
raise HTTPException(status_code=404, detail="算法版本不存在")
|
||||
|
||||
# 如果指定了服务ID,验证服务是否存在
|
||||
if request.service_id:
|
||||
service = db.query(AlgorithmService).filter(
|
||||
AlgorithmService.service_id == request.service_id
|
||||
).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
|
||||
# 检查API路径是否已存在
|
||||
existing_endpoint = db.query(ApiEndpoint).filter(
|
||||
ApiEndpoint.path == request.path
|
||||
).first()
|
||||
if existing_endpoint:
|
||||
raise HTTPException(status_code=400, detail="API路径已存在")
|
||||
|
||||
# 创建API端点
|
||||
new_endpoint = ApiEndpoint(
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
path=request.path,
|
||||
method=request.method,
|
||||
algorithm_id=request.algorithm_id,
|
||||
version_id=request.version_id,
|
||||
service_id=request.service_id,
|
||||
requires_auth=request.requires_auth,
|
||||
allowed_roles=request.allowed_roles,
|
||||
rate_limit=request.rate_limit,
|
||||
is_public=request.is_public,
|
||||
config=request.config,
|
||||
status="active",
|
||||
call_count="0",
|
||||
success_count="0",
|
||||
error_count="0",
|
||||
avg_response_time="0.0"
|
||||
)
|
||||
|
||||
db.add(new_endpoint)
|
||||
db.commit()
|
||||
db.refresh(new_endpoint)
|
||||
|
||||
# 返回创建的API端点
|
||||
return {
|
||||
"id": new_endpoint.id,
|
||||
"name": new_endpoint.name,
|
||||
"description": new_endpoint.description,
|
||||
"path": new_endpoint.path,
|
||||
"method": new_endpoint.method,
|
||||
"algorithm_id": new_endpoint.algorithm_id,
|
||||
"algorithm_name": algorithm.name,
|
||||
"version_id": new_endpoint.version_id,
|
||||
"version": version.version,
|
||||
"service_id": new_endpoint.service_id,
|
||||
"status": new_endpoint.status,
|
||||
"is_public": new_endpoint.is_public,
|
||||
"call_count": new_endpoint.call_count,
|
||||
"success_count": new_endpoint.success_count,
|
||||
"error_count": new_endpoint.error_count,
|
||||
"avg_response_time": new_endpoint.avg_response_time,
|
||||
"created_at": new_endpoint.created_at,
|
||||
"updated_at": new_endpoint.updated_at,
|
||||
"last_called_at": new_endpoint.last_called_at
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"创建API端点失败: {str(e)}")
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"创建API端点失败: {str(e)}")
|
||||
|
||||
|
||||
@router.put("/endpoints/{endpoint_id}", response_model=ApiEndpointResponse)
|
||||
async def update_api_endpoint(
|
||||
endpoint_id: str,
|
||||
request: ApiEndpointUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""更新API端点"""
|
||||
try:
|
||||
# 检查用户权限
|
||||
if not hasattr(current_user, 'role_name') or current_user.role_name != "admin":
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
# 查询API端点
|
||||
endpoint = db.query(ApiEndpoint).filter(ApiEndpoint.id == endpoint_id).first()
|
||||
if not endpoint:
|
||||
raise HTTPException(status_code=404, detail="API端点不存在")
|
||||
|
||||
# 更新字段
|
||||
if request.name is not None:
|
||||
endpoint.name = request.name
|
||||
if request.description is not None:
|
||||
endpoint.description = request.description
|
||||
if request.path is not None:
|
||||
# 检查新路径是否已被其他端点使用
|
||||
existing_endpoint = db.query(ApiEndpoint).filter(
|
||||
ApiEndpoint.path == request.path,
|
||||
ApiEndpoint.id != endpoint_id
|
||||
).first()
|
||||
if existing_endpoint:
|
||||
raise HTTPException(status_code=400, detail="API路径已存在")
|
||||
endpoint.path = request.path
|
||||
if request.method is not None:
|
||||
endpoint.method = request.method
|
||||
if request.requires_auth is not None:
|
||||
endpoint.requires_auth = request.requires_auth
|
||||
if request.allowed_roles is not None:
|
||||
endpoint.allowed_roles = request.allowed_roles
|
||||
if request.rate_limit is not None:
|
||||
endpoint.rate_limit = request.rate_limit
|
||||
if request.is_public is not None:
|
||||
endpoint.is_public = request.is_public
|
||||
if request.config is not None:
|
||||
endpoint.config = request.config
|
||||
if request.status is not None:
|
||||
endpoint.status = request.status
|
||||
|
||||
db.commit()
|
||||
db.refresh(endpoint)
|
||||
|
||||
# 获取关联的算法和版本信息
|
||||
algorithm = db.query(Algorithm).filter(Algorithm.id == endpoint.algorithm_id).first()
|
||||
version = db.query(AlgorithmVersion).filter(AlgorithmVersion.id == endpoint.version_id).first()
|
||||
|
||||
return {
|
||||
"id": endpoint.id,
|
||||
"name": endpoint.name,
|
||||
"description": endpoint.description,
|
||||
"path": endpoint.path,
|
||||
"method": endpoint.method,
|
||||
"algorithm_id": endpoint.algorithm_id,
|
||||
"algorithm_name": algorithm.name if algorithm else "",
|
||||
"version_id": endpoint.version_id,
|
||||
"version": version.version if version else "",
|
||||
"service_id": endpoint.service_id,
|
||||
"status": endpoint.status,
|
||||
"is_public": endpoint.is_public,
|
||||
"call_count": endpoint.call_count,
|
||||
"success_count": endpoint.success_count,
|
||||
"error_count": endpoint.error_count,
|
||||
"avg_response_time": endpoint.avg_response_time,
|
||||
"created_at": endpoint.created_at,
|
||||
"updated_at": endpoint.updated_at,
|
||||
"last_called_at": endpoint.last_called_at
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"更新API端点失败: {str(e)}")
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"更新API端点失败: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/endpoints/{endpoint_id}")
|
||||
async def delete_api_endpoint(
|
||||
endpoint_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""删除API端点"""
|
||||
try:
|
||||
# 检查用户权限
|
||||
if not hasattr(current_user, 'role_name') or current_user.role_name != "admin":
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
# 查询API端点
|
||||
endpoint = db.query(ApiEndpoint).filter(ApiEndpoint.id == endpoint_id).first()
|
||||
if not endpoint:
|
||||
raise HTTPException(status_code=404, detail="API端点不存在")
|
||||
|
||||
# 删除API端点
|
||||
db.delete(endpoint)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "API端点删除成功"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"删除API端点失败: {str(e)}")
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"删除API端点失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/stats", response_model=ApiStatsResponse)
|
||||
async def get_api_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""获取API统计信息"""
|
||||
try:
|
||||
# 检查用户权限
|
||||
if not hasattr(current_user, 'role_name') or current_user.role_name != "admin":
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
|
||||
# 统计API端点
|
||||
total_endpoints = db.query(ApiEndpoint).count()
|
||||
active_endpoints = db.query(ApiEndpoint).filter(ApiEndpoint.status == "active").count()
|
||||
|
||||
# 统计调用次数
|
||||
endpoints = db.query(ApiEndpoint).all()
|
||||
total_calls = sum(int(e.call_count or 0) for e in endpoints)
|
||||
total_success = sum(int(e.success_count or 0) for e in endpoints)
|
||||
total_errors = sum(int(e.error_count or 0) for e in endpoints)
|
||||
|
||||
# 计算平均响应时间
|
||||
avg_response_times = [float(e.avg_response_time or 0) for e in endpoints if float(e.avg_response_time or 0) > 0]
|
||||
avg_response_time = sum(avg_response_times) / len(avg_response_times) if avg_response_times else 0.0
|
||||
|
||||
return {
|
||||
"total_endpoints": total_endpoints,
|
||||
"active_endpoints": active_endpoints,
|
||||
"total_calls": str(total_calls),
|
||||
"total_success": str(total_success),
|
||||
"total_errors": str(total_errors),
|
||||
"avg_response_time": f"{avg_response_time:.2f}"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取API统计信息失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取API统计信息失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/endpoints/{endpoint_id}/test")
|
||||
async def test_api_endpoint(
|
||||
endpoint_id: str,
|
||||
payload: Dict[str, Any],
|
||||
db: Session = Depends(get_db),
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""测试API端点"""
|
||||
try:
|
||||
# 查询API端点
|
||||
endpoint = db.query(ApiEndpoint).filter(ApiEndpoint.id == endpoint_id).first()
|
||||
if not endpoint:
|
||||
raise HTTPException(status_code=404, detail="API端点不存在")
|
||||
|
||||
# 检查API端点状态
|
||||
if endpoint.status != "active":
|
||||
raise HTTPException(status_code=400, detail="API端点未激活")
|
||||
|
||||
# 查询关联的服务
|
||||
if endpoint.service_id:
|
||||
service = db.query(AlgorithmService).filter(
|
||||
AlgorithmService.service_id == endpoint.service_id
|
||||
).first()
|
||||
if not service or service.status != "running":
|
||||
raise HTTPException(status_code=400, detail="关联服务未运行")
|
||||
|
||||
# 调用服务
|
||||
import httpx
|
||||
import time
|
||||
|
||||
service_url = service.api_url
|
||||
if not service_url.endswith("/"):
|
||||
service_url += "/"
|
||||
|
||||
call_url = f"{service_url}predict"
|
||||
|
||||
start_time = time.time()
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
call_url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
response_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
return {
|
||||
"success": True,
|
||||
"result": response.json(),
|
||||
"response_time": response_time,
|
||||
"message": "API调用成功"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"服务返回错误: HTTP {response.status_code}",
|
||||
"response_time": response_time
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="API端点未关联服务")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"测试API端点失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"测试API端点失败: {str(e)}")
|
||||
64
backend/app/routes/comparison.py
Normal file
64
backend/app/routes/comparison.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from app.services.comparison_service import ComparisonService
|
||||
from app.routes.user import get_current_active_user
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter(prefix="/comparison", tags=["comparison"])
|
||||
|
||||
# 创建对比服务实例
|
||||
comparison_service = ComparisonService()
|
||||
|
||||
|
||||
@router.post("/compare-algorithms", response_model=dict)
|
||||
async def compare_algorithms(
|
||||
request_data: Dict[str, Any],
|
||||
current_user: dict = Depends(get_current_active_user)
|
||||
):
|
||||
"""比较多个算法的效果
|
||||
|
||||
Args:
|
||||
request_data: 请求数据,包含input_data和algorithm_configs
|
||||
current_user: 当前活跃用户
|
||||
|
||||
Returns:
|
||||
对比结果
|
||||
"""
|
||||
input_data = request_data.get("input_data")
|
||||
algorithm_configs = request_data.get("algorithm_configs")
|
||||
|
||||
if not input_data:
|
||||
raise HTTPException(status_code=400, detail="缺少 input_data 参数")
|
||||
|
||||
if not algorithm_configs or not isinstance(algorithm_configs, list):
|
||||
raise HTTPException(status_code=400, detail="缺少 algorithm_configs 参数或格式错误")
|
||||
|
||||
result = await comparison_service.compare_algorithms(input_data, algorithm_configs)
|
||||
|
||||
if not result["success"]:
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "对比失败"))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/generate-report", response_model=dict)
|
||||
async def generate_comparison_report(
|
||||
comparison_results: Dict[str, Any],
|
||||
current_user: dict = Depends(get_current_active_user)
|
||||
):
|
||||
"""生成对比报告
|
||||
|
||||
Args:
|
||||
comparison_results: 对比结果
|
||||
current_user: 当前活跃用户
|
||||
|
||||
Returns:
|
||||
对比报告
|
||||
"""
|
||||
report = comparison_service.generate_comparison_report(comparison_results)
|
||||
|
||||
if not report["success"]:
|
||||
raise HTTPException(status_code=500, detail=report.get("error", "生成报告失败"))
|
||||
|
||||
return report
|
||||
124
backend/app/routes/config.py
Normal file
124
backend/app/routes/config.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from app.models.database import get_db
|
||||
from app.services.config_service import ConfigService
|
||||
from app.routes.user import get_current_active_user
|
||||
|
||||
router = APIRouter(prefix="/config", tags=["config"])
|
||||
|
||||
|
||||
@router.get("/{config_key}")
|
||||
async def get_config(
|
||||
config_key: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_active_user)
|
||||
):
|
||||
"""获取配置
|
||||
|
||||
Args:
|
||||
config_key: 配置键
|
||||
db: 数据库会话
|
||||
current_user: 当前活跃用户
|
||||
|
||||
Returns:
|
||||
配置信息
|
||||
"""
|
||||
config = ConfigService.get_config(db, config_key)
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
return {"key": config_key, "value": config}
|
||||
|
||||
|
||||
@router.post("/{config_key}")
|
||||
async def set_config(
|
||||
config_key: str,
|
||||
config_data: Dict[str, Any],
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_active_user)
|
||||
):
|
||||
"""设置配置
|
||||
|
||||
Args:
|
||||
config_key: 配置键
|
||||
config_data: 配置数据,包含value、type、service_id、description等字段
|
||||
db: 数据库会话
|
||||
current_user: 当前活跃用户
|
||||
|
||||
Returns:
|
||||
设置结果
|
||||
"""
|
||||
success = ConfigService.set_config(
|
||||
db=db,
|
||||
config_key=config_key,
|
||||
config_value=config_data.get("value"),
|
||||
config_type=config_data.get("type", "system"),
|
||||
service_id=config_data.get("service_id"),
|
||||
description=config_data.get("description", "")
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="设置配置失败")
|
||||
return {"message": "设置配置成功"}
|
||||
|
||||
|
||||
@router.get("/service/{service_id}")
|
||||
async def get_service_configs(
|
||||
service_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_active_user)
|
||||
):
|
||||
"""获取服务配置
|
||||
|
||||
Args:
|
||||
service_id: 服务ID
|
||||
db: 数据库会话
|
||||
current_user: 当前活跃用户
|
||||
|
||||
Returns:
|
||||
服务配置列表
|
||||
"""
|
||||
configs = ConfigService.get_service_configs(db, service_id)
|
||||
return {"service_id": service_id, "configs": configs}
|
||||
|
||||
|
||||
@router.delete("/{config_key}")
|
||||
async def delete_config(
|
||||
config_key: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_active_user)
|
||||
):
|
||||
"""删除配置
|
||||
|
||||
Args:
|
||||
config_key: 配置键
|
||||
db: 数据库会话
|
||||
current_user: 当前活跃用户
|
||||
|
||||
Returns:
|
||||
删除结果
|
||||
"""
|
||||
success = ConfigService.delete_config(db, config_key)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="删除配置失败")
|
||||
return {"message": "删除配置成功"}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_configs(
|
||||
config_type: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_active_user)
|
||||
):
|
||||
"""获取所有配置
|
||||
|
||||
Args:
|
||||
config_type: 配置类型,可选
|
||||
db: 数据库会话
|
||||
current_user: 当前活跃用户
|
||||
|
||||
Returns:
|
||||
配置列表
|
||||
"""
|
||||
configs = ConfigService.get_all_configs(db, config_type)
|
||||
return {"configs": configs}
|
||||
@@ -14,6 +14,7 @@ from app.schemas.user import UserResponse
|
||||
from app.services.project_analyzer import ProjectAnalyzer
|
||||
from app.services.service_generator import ServiceGenerator
|
||||
from app.services.service_orchestrator import ServiceOrchestrator
|
||||
from app.gitea.service import gitea_service
|
||||
|
||||
router = APIRouter(prefix="/services", tags=["services"])
|
||||
|
||||
@@ -23,6 +24,9 @@ class RegisterServiceRequest(BaseModel):
|
||||
repository_id: str
|
||||
name: str
|
||||
version: str = "1.0.0"
|
||||
description: Optional[str] = ""
|
||||
tech_category: str = "computer_vision"
|
||||
output_type: str = "image"
|
||||
service_type: str = "http"
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
@@ -154,31 +158,24 @@ async def register_service(
|
||||
# 记录仓库信息
|
||||
print(f"仓库信息: {repo.name}, {repo.description}, {repo.repo_url}")
|
||||
|
||||
# 2. 分析项目
|
||||
repo_path = f"/tmp/repository_{request.repository_id}"
|
||||
# 注意:在实际实现中,应该从算法仓库中获取项目文件
|
||||
# 这里简化处理,创建一个模拟的项目结构
|
||||
os.makedirs(repo_path, exist_ok=True)
|
||||
# 2. 从Gitea仓库克隆代码到本地
|
||||
repo_path = f"/tmp/algorithms/{request.repository_id}"
|
||||
|
||||
# 创建模拟的算法文件
|
||||
with open(os.path.join(repo_path, "algorithm.py"), "w") as f:
|
||||
f.write("""
|
||||
def predict(data):
|
||||
return {"result": "Prediction result", "input": data}
|
||||
# 使用Gitea服务克隆仓库
|
||||
clone_success = gitea_service.clone_repository(repo.repo_url, request.repository_id, repo.branch or "main")
|
||||
if not clone_success:
|
||||
raise HTTPException(status_code=400, detail=f"克隆仓库失败: {repo.repo_url}")
|
||||
|
||||
def run(data):
|
||||
return {"result": "Run result", "input": data}
|
||||
print(f"仓库克隆成功: {repo_path}")
|
||||
|
||||
def main(data):
|
||||
return {"result": "Main result", "input": data}
|
||||
""")
|
||||
|
||||
# 分析项目
|
||||
# 3. 分析项目
|
||||
project_info = project_analyzer.analyze_project(repo_path)
|
||||
if not project_info["success"]:
|
||||
raise HTTPException(status_code=400, detail=f"项目分析失败: {project_info['error']}")
|
||||
|
||||
# 3. 生成服务包装器
|
||||
print(f"项目分析成功: {project_info}")
|
||||
|
||||
# 4. 生成服务包装器
|
||||
service_config = {
|
||||
"name": request.name,
|
||||
"version": request.version,
|
||||
@@ -194,24 +191,31 @@ def main(data):
|
||||
if not generate_result["success"]:
|
||||
raise HTTPException(status_code=400, detail=f"服务生成失败: {generate_result['error']}")
|
||||
|
||||
# 4. 部署服务
|
||||
print(f"服务生成成功: {generate_result}")
|
||||
|
||||
# 5. 部署服务
|
||||
service_id = str(uuid.uuid4())
|
||||
deploy_result = service_orchestrator.deploy_service(service_id, service_config, project_info)
|
||||
deploy_result = service_orchestrator.deploy_service(service_id, service_config, project_info, repo_path)
|
||||
if not deploy_result["success"]:
|
||||
raise HTTPException(status_code=400, detail=f"服务部署失败: {deploy_result['error']}")
|
||||
|
||||
# 5. 保存服务信息到数据库
|
||||
print(f"服务部署成功: {deploy_result}")
|
||||
|
||||
# 6. 保存服务信息到数据库
|
||||
new_service = AlgorithmService(
|
||||
id=str(uuid.uuid4()),
|
||||
service_id=service_id,
|
||||
name=request.name,
|
||||
algorithm_name=repo.name, # 使用仓库名称作为算法名称
|
||||
version=request.version,
|
||||
tech_category=request.tech_category,
|
||||
output_type=request.output_type,
|
||||
host=request.host,
|
||||
port=request.port,
|
||||
api_url=deploy_result["api_url"],
|
||||
status=deploy_result["status"],
|
||||
config={
|
||||
"repository_id": request.repository_id, # 保存仓库ID
|
||||
"service_type": request.service_type,
|
||||
"timeout": request.timeout,
|
||||
"health_check_path": request.health_check_path,
|
||||
@@ -352,8 +356,64 @@ async def start_service(
|
||||
|
||||
# 启动服务
|
||||
start_result = service_orchestrator.start_service(service_id, container_id)
|
||||
|
||||
# 如果启动失败,尝试从数据库重新注册服务
|
||||
if not start_result["success"]:
|
||||
raise HTTPException(status_code=400, detail=f"服务启动失败: {start_result['error']}")
|
||||
print(f"服务启动失败: {start_result['error']},尝试从数据库重新注册服务")
|
||||
|
||||
# 获取仓库信息
|
||||
repository_id = service.config.get("repository_id")
|
||||
if not repository_id:
|
||||
raise HTTPException(status_code=400, detail="Repository ID not found in service config")
|
||||
|
||||
repository = db.query(AlgorithmRepository).filter(AlgorithmRepository.id == repository_id).first()
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
# 从Gitea克隆仓库
|
||||
clone_success = gitea_service.clone_repository(
|
||||
repository.repo_url,
|
||||
service_id,
|
||||
repository.branch or "main"
|
||||
)
|
||||
if not clone_success:
|
||||
raise HTTPException(status_code=400, detail="Failed to clone repository")
|
||||
|
||||
# 仓库路径
|
||||
repo_path = f"/tmp/algorithms/{service_id}"
|
||||
|
||||
# 分析项目
|
||||
project_info = project_analyzer.analyze_project(repo_path)
|
||||
if not project_info:
|
||||
raise HTTPException(status_code=400, detail="Failed to analyze project")
|
||||
|
||||
# 生成服务
|
||||
service_config = {
|
||||
"name": service.name,
|
||||
"version": service.version,
|
||||
"host": service.host,
|
||||
"port": service.port,
|
||||
"timeout": service.config.get("timeout", 30),
|
||||
"health_check_path": service.config.get("health_check_path", "/health"),
|
||||
"environment": service.config.get("environment", {})
|
||||
}
|
||||
|
||||
# 部署服务
|
||||
deploy_result = service_orchestrator.deploy_service(service_id, project_info, service_config, repo_path)
|
||||
if not deploy_result["success"]:
|
||||
raise HTTPException(status_code=400, detail=f"服务部署失败: {deploy_result['error']}")
|
||||
|
||||
# 更新服务配置
|
||||
service.config["container_id"] = deploy_result["container_id"]
|
||||
service.api_url = deploy_result["api_url"]
|
||||
db.commit()
|
||||
|
||||
start_result = {
|
||||
"success": True,
|
||||
"service_id": service_id,
|
||||
"status": "running",
|
||||
"error": None
|
||||
}
|
||||
|
||||
# 更新服务状态
|
||||
service.status = start_result["status"]
|
||||
@@ -1065,3 +1125,108 @@ async def batch_delete_services(
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
class ServiceCallRequest(BaseModel):
|
||||
"""服务调用请求"""
|
||||
service_id: str
|
||||
payload: Dict[str, Any]
|
||||
|
||||
|
||||
class ServiceCallResponse(BaseModel):
|
||||
"""服务调用响应"""
|
||||
success: bool
|
||||
result: Dict[str, Any]
|
||||
service_id: str
|
||||
execution_time: float
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/call")
|
||||
async def call_service(
|
||||
request: ServiceCallRequest,
|
||||
current_user: UserResponse = Depends(get_current_active_user)
|
||||
):
|
||||
"""直接调用注册的服务"""
|
||||
import time
|
||||
import httpx
|
||||
|
||||
# 创建数据库会话
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 查询服务
|
||||
service = db.query(AlgorithmService).filter(
|
||||
AlgorithmService.service_id == request.service_id
|
||||
).first()
|
||||
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="服务不存在")
|
||||
|
||||
# 检查服务状态
|
||||
if service.status != "running":
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail=f"服务未运行,当前状态: {service.status}"
|
||||
)
|
||||
|
||||
# 调用服务
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 构建服务URL
|
||||
service_url = service.api_url
|
||||
|
||||
# 如果URL没有路径,添加默认路径
|
||||
if not service_url.endswith("/"):
|
||||
service_url += "/"
|
||||
|
||||
# 添加调用端点
|
||||
call_url = f"{service_url}predict"
|
||||
|
||||
# 使用httpx调用服务
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
call_url,
|
||||
json=request.payload,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
if response.status_code == 200:
|
||||
return ServiceCallResponse(
|
||||
success=True,
|
||||
result=response.json(),
|
||||
service_id=request.service_id,
|
||||
execution_time=execution_time
|
||||
)
|
||||
else:
|
||||
return ServiceCallResponse(
|
||||
success=False,
|
||||
result={},
|
||||
service_id=request.service_id,
|
||||
execution_time=execution_time,
|
||||
error=f"服务返回错误: HTTP {response.status_code} - {response.text}"
|
||||
)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
execution_time = time.time() - start_time
|
||||
return ServiceCallResponse(
|
||||
success=False,
|
||||
result={},
|
||||
service_id=request.service_id,
|
||||
execution_time=execution_time,
|
||||
error=f"无法连接到服务: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
return ServiceCallResponse(
|
||||
success=False,
|
||||
result={},
|
||||
service_id=request.service_id,
|
||||
execution_time=execution_time,
|
||||
error=f"服务调用异常: {str(e)}"
|
||||
)
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -167,6 +167,12 @@ async def read_users_me(current_user: UserResponse = Depends(get_current_active_
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/me/", response_model=UserResponse)
|
||||
async def read_users_me_with_slash(current_user: UserResponse = Depends(get_current_active_user)):
|
||||
"""获取当前用户信息(带末尾斜杠)"""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/", response_model=UserListResponse)
|
||||
async def get_users(
|
||||
skip: int = 0,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -9,6 +9,8 @@ class AlgorithmBase(BaseModel):
|
||||
name: str = Field(..., description="算法名称")
|
||||
description: str = Field(..., description="算法描述")
|
||||
type: str = Field(..., description="算法类型")
|
||||
tech_category: str = Field(default="computer_vision", description="技术分类")
|
||||
output_type: str = Field(default="image", description="输出类型")
|
||||
|
||||
|
||||
class AlgorithmCreate(AlgorithmBase):
|
||||
|
||||
Binary file not shown.
BIN
backend/app/services/__pycache__/config_service.cpython-312.pyc
Normal file
BIN
backend/app/services/__pycache__/config_service.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
165
backend/app/services/comparison_service.py
Normal file
165
backend/app/services/comparison_service.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from typing import Dict, Any, List
|
||||
import asyncio
|
||||
import httpx
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComparisonService:
|
||||
"""效果对比服务"""
|
||||
|
||||
async def compare_algorithms(
|
||||
self,
|
||||
input_data: Dict[str, Any],
|
||||
algorithm_configs: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""比较多个算法的效果
|
||||
|
||||
Args:
|
||||
input_data: 输入数据
|
||||
algorithm_configs: 算法配置列表,每个配置包含服务URL、参数等
|
||||
|
||||
Returns:
|
||||
对比结果
|
||||
"""
|
||||
try:
|
||||
# 异步执行所有算法
|
||||
tasks = []
|
||||
for config in algorithm_configs:
|
||||
task = self._execute_algorithm(config, input_data)
|
||||
tasks.append(task)
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 处理结果
|
||||
comparison_results = []
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
comparison_results.append({
|
||||
"algorithm_id": algorithm_configs[i].get("id"),
|
||||
"algorithm_name": algorithm_configs[i].get("name"),
|
||||
"success": False,
|
||||
"error": str(result),
|
||||
"output": None,
|
||||
"execution_time": 0
|
||||
})
|
||||
else:
|
||||
comparison_results.append({
|
||||
"algorithm_id": algorithm_configs[i].get("id"),
|
||||
"algorithm_name": algorithm_configs[i].get("name"),
|
||||
"success": True,
|
||||
"error": None,
|
||||
"output": result.get("output"),
|
||||
"execution_time": result.get("execution_time", 0)
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"results": comparison_results,
|
||||
"input_data": input_data
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Comparison error: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"results": []
|
||||
}
|
||||
|
||||
async def _execute_algorithm(
|
||||
self,
|
||||
config: Dict[str, Any],
|
||||
input_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""执行单个算法
|
||||
|
||||
Args:
|
||||
config: 算法配置
|
||||
input_data: 输入数据
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
url = config.get("url")
|
||||
params = config.get("params", {})
|
||||
|
||||
if not url:
|
||||
raise ValueError("缺少算法服务URL")
|
||||
|
||||
# 构建请求数据
|
||||
request_data = {
|
||||
"input_data": input_data.get("input_data", input_data),
|
||||
"params": params
|
||||
}
|
||||
|
||||
# 发送请求
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(f"{url}/predict", json=request_data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
return {
|
||||
"output": result,
|
||||
"execution_time": execution_time
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Algorithm execution error: {str(e)}")
|
||||
raise e
|
||||
|
||||
def generate_comparison_report(self, comparison_results: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""生成对比报告
|
||||
|
||||
Args:
|
||||
comparison_results: 对比结果
|
||||
|
||||
Returns:
|
||||
对比报告
|
||||
"""
|
||||
try:
|
||||
if not comparison_results.get("success"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": comparison_results.get("error", "对比失败")
|
||||
}
|
||||
|
||||
results = comparison_results.get("results", [])
|
||||
|
||||
# 分析结果
|
||||
successful_algorithms = [r for r in results if r.get("success")]
|
||||
failed_algorithms = [r for r in results if not r.get("success")]
|
||||
|
||||
# 计算平均执行时间
|
||||
if successful_algorithms:
|
||||
avg_execution_time = sum(r.get("execution_time", 0) for r in successful_algorithms) / len(successful_algorithms)
|
||||
else:
|
||||
avg_execution_time = 0
|
||||
|
||||
# 生成报告
|
||||
report = {
|
||||
"summary": {
|
||||
"total_algorithms": len(results),
|
||||
"successful_algorithms": len(successful_algorithms),
|
||||
"failed_algorithms": len(failed_algorithms),
|
||||
"average_execution_time": round(avg_execution_time, 2)
|
||||
},
|
||||
"details": results,
|
||||
"input_data": comparison_results.get("input_data")
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"report": report
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Report generation error: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
165
backend/app/services/config_service.py
Normal file
165
backend/app/services/config_service.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from typing import Optional, Dict, Any, List
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.models import ServiceConfig
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""配置服务"""
|
||||
|
||||
@staticmethod
|
||||
def get_config(db: Session, config_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_key: 配置键
|
||||
|
||||
Returns:
|
||||
配置值,如果不存在返回None
|
||||
"""
|
||||
config = db.query(ServiceConfig).filter_by(
|
||||
config_key=config_key,
|
||||
status="active"
|
||||
).first()
|
||||
|
||||
if config:
|
||||
return config.config_value
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def set_config(db: Session, config_key: str, config_value: Dict[str, Any],
|
||||
config_type: str = "system", service_id: Optional[str] = None,
|
||||
description: str = "") -> bool:
|
||||
"""设置配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_key: 配置键
|
||||
config_value: 配置值
|
||||
config_type: 配置类型,默认为"system"
|
||||
service_id: 服务ID,系统配置可为None
|
||||
description: 配置描述
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
try:
|
||||
# 检查是否存在
|
||||
existing_config = db.query(ServiceConfig).filter_by(
|
||||
config_key=config_key
|
||||
).first()
|
||||
|
||||
if existing_config:
|
||||
# 更新现有配置
|
||||
existing_config.config_value = config_value
|
||||
existing_config.config_type = config_type
|
||||
existing_config.service_id = service_id
|
||||
existing_config.description = description
|
||||
existing_config.status = "active"
|
||||
else:
|
||||
# 创建新配置
|
||||
new_config = ServiceConfig(
|
||||
id=f"config-{uuid.uuid4()}",
|
||||
config_key=config_key,
|
||||
config_value=config_value,
|
||||
config_type=config_type,
|
||||
service_id=service_id,
|
||||
description=description,
|
||||
status="active"
|
||||
)
|
||||
db.add(new_config)
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set config: {str(e)}")
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_service_configs(db: Session, service_id: str) -> List[Dict[str, Any]]:
|
||||
"""获取服务的所有配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
service_id: 服务ID
|
||||
|
||||
Returns:
|
||||
服务配置列表
|
||||
"""
|
||||
configs = db.query(ServiceConfig).filter_by(
|
||||
service_id=service_id,
|
||||
status="active"
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"key": config.config_key,
|
||||
"value": config.config_value,
|
||||
"type": config.config_type,
|
||||
"description": config.description
|
||||
}
|
||||
for config in configs
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def delete_config(db: Session, config_key: str) -> bool:
|
||||
"""删除配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_key: 配置键
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
config = db.query(ServiceConfig).filter_by(
|
||||
config_key=config_key
|
||||
).first()
|
||||
|
||||
if config:
|
||||
config.status = "inactive"
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete config: {str(e)}")
|
||||
db.rollback()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_all_configs(db: Session, config_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""获取所有配置
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
config_type: 配置类型,可选
|
||||
|
||||
Returns:
|
||||
配置列表
|
||||
"""
|
||||
query = db.query(ServiceConfig).filter_by(status="active")
|
||||
|
||||
if config_type:
|
||||
query = query.filter_by(config_type=config_type)
|
||||
|
||||
configs = query.all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": config.id,
|
||||
"key": config.config_key,
|
||||
"value": config.config_value,
|
||||
"type": config.config_type,
|
||||
"service_id": config.service_id,
|
||||
"description": config.description,
|
||||
"created_at": config.created_at,
|
||||
"updated_at": config.updated_at
|
||||
}
|
||||
for config in configs
|
||||
]
|
||||
@@ -63,22 +63,41 @@ class ProjectAnalyzer:
|
||||
Returns:
|
||||
项目类型,如 "python", "java", "nodejs" 等
|
||||
"""
|
||||
# 检查Python项目
|
||||
# 检查Python项目 - 先检查根目录
|
||||
if os.path.exists(os.path.join(repo_path, "requirements.txt")) or \
|
||||
os.path.exists(os.path.join(repo_path, "pyproject.toml")) or \
|
||||
any(file.endswith(".py") for file in os.listdir(repo_path)):
|
||||
return "python"
|
||||
|
||||
# 检查Java项目
|
||||
# 检查Python项目 - 递归检查子目录
|
||||
for root, dirs, files in os.walk(repo_path):
|
||||
if "requirements.txt" in files or "pyproject.toml" in files:
|
||||
return "python"
|
||||
if any(file.endswith(".py") for file in files):
|
||||
return "python"
|
||||
|
||||
# 检查Java项目 - 先检查根目录
|
||||
if os.path.exists(os.path.join(repo_path, "pom.xml")) or \
|
||||
os.path.exists(os.path.join(repo_path, "build.gradle")) or \
|
||||
os.path.exists(os.path.join(repo_path, "src")):
|
||||
return "java"
|
||||
|
||||
# 检查Node.js项目
|
||||
# 检查Java项目 - 递归检查子目录
|
||||
for root, dirs, files in os.walk(repo_path):
|
||||
if "pom.xml" in files or "build.gradle" in files:
|
||||
return "java"
|
||||
if "src" in dirs:
|
||||
return "java"
|
||||
|
||||
# 检查Node.js项目 - 先检查根目录
|
||||
if os.path.exists(os.path.join(repo_path, "package.json")):
|
||||
return "nodejs"
|
||||
|
||||
# 检查Node.js项目 - 递归检查子目录
|
||||
for root, dirs, files in os.walk(repo_path):
|
||||
if "package.json" in files:
|
||||
return "nodejs"
|
||||
|
||||
# 检查其他项目类型
|
||||
if os.path.exists(os.path.join(repo_path, "CMakeLists.txt")):
|
||||
return "c++"
|
||||
|
||||
@@ -38,13 +38,14 @@ class ServiceOrchestrator:
|
||||
self.client = None
|
||||
print("使用本地进程部署模式")
|
||||
|
||||
def deploy_service(self, service_id: str, service_config: Dict[str, Any], project_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def deploy_service(self, service_id: str, service_config: Dict[str, Any], project_info: Dict[str, Any], repo_path: str = None) -> Dict[str, Any]:
|
||||
"""部署服务
|
||||
|
||||
Args:
|
||||
service_id: 服务ID
|
||||
service_config: 服务配置
|
||||
project_info: 项目信息
|
||||
repo_path: 仓库路径(用于复制真实的算法文件)
|
||||
|
||||
Returns:
|
||||
部署结果
|
||||
@@ -95,7 +96,7 @@ class ServiceOrchestrator:
|
||||
service_dir = self._create_service_directory(service_id)
|
||||
|
||||
# 2. 生成服务包装器
|
||||
self._generate_local_service_wrapper(service_dir, project_info, service_config)
|
||||
self._generate_local_service_wrapper(service_dir, project_info, service_config, repo_path)
|
||||
|
||||
# 3. 启动服务进程
|
||||
process_info = self._start_local_service_process(service_id, service_dir, project_info, service_config)
|
||||
@@ -176,9 +177,13 @@ class ServiceOrchestrator:
|
||||
else:
|
||||
# 本地进程启动
|
||||
if service_id not in self.processes:
|
||||
# 服务不在进程列表中,可能是服务重启导致的
|
||||
# 这种情况下,需要从外部重新注册服务
|
||||
# 暂时返回错误,建议用户重新注册服务
|
||||
print(f"服务 {service_id} 不在进程列表中,无法启动")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "服务不存在",
|
||||
"error": "服务不存在,请重新注册服务",
|
||||
"service_id": service_id,
|
||||
"status": "error"
|
||||
}
|
||||
@@ -272,11 +277,18 @@ class ServiceOrchestrator:
|
||||
else:
|
||||
# 本地进程停止
|
||||
if service_id not in self.processes:
|
||||
# 服务不在进程列表中,可能是服务重启导致的
|
||||
# 尝试通过端口查找并停止进程
|
||||
print(f"服务 {service_id} 不在进程列表中,尝试通过端口查找进程")
|
||||
|
||||
# 从服务配置中获取端口信息
|
||||
# 这里需要从外部传入服务配置,或者从数据库查询
|
||||
# 暂时返回成功,因为服务可能已经停止了
|
||||
return {
|
||||
"success": False,
|
||||
"error": "服务不存在",
|
||||
"success": True,
|
||||
"service_id": service_id,
|
||||
"status": "error"
|
||||
"status": "stopped",
|
||||
"error": None
|
||||
}
|
||||
|
||||
process_info = self.processes[service_id]
|
||||
@@ -1271,13 +1283,14 @@ json
|
||||
os.makedirs(service_dir, exist_ok=True)
|
||||
return service_dir
|
||||
|
||||
def _generate_local_service_wrapper(self, service_dir: str, project_info: Dict[str, Any], service_config: Dict[str, Any]):
|
||||
def _generate_local_service_wrapper(self, service_dir: str, project_info: Dict[str, Any], service_config: Dict[str, Any], repo_path: str = None):
|
||||
"""生成本地服务包装器
|
||||
|
||||
Args:
|
||||
service_dir: 服务目录
|
||||
project_info: 项目信息
|
||||
service_config: 服务配置
|
||||
repo_path: 仓库路径(用于复制真实的算法文件)
|
||||
"""
|
||||
# 生成服务包装器
|
||||
service_wrapper_content = self._generate_service_wrapper(project_info, service_config)
|
||||
@@ -1285,7 +1298,44 @@ json
|
||||
with open(os.path.join(service_dir, f"service_wrapper{wrapper_extension}"), "w") as f:
|
||||
f.write(service_wrapper_content)
|
||||
|
||||
# 创建模拟的算法文件
|
||||
# 复制真实的算法文件
|
||||
if repo_path and project_info["project_type"] == "python":
|
||||
# 尝试找到并复制主要的算法文件
|
||||
entry_point = project_info.get("entry_point")
|
||||
if entry_point:
|
||||
source_file = os.path.join(repo_path, entry_point)
|
||||
if os.path.exists(source_file):
|
||||
# 复制算法文件到服务目录
|
||||
import shutil
|
||||
shutil.copy2(source_file, os.path.join(service_dir, "algorithm.py"))
|
||||
print(f"已复制算法文件: {source_file} -> {os.path.join(service_dir, 'algorithm.py')}")
|
||||
return
|
||||
|
||||
# 如果没有找到入口点,尝试复制所有Python文件
|
||||
if os.path.exists(repo_path):
|
||||
import shutil
|
||||
for root, dirs, files in os.walk(repo_path):
|
||||
for file in files:
|
||||
if file.endswith(".py") and not file.startswith("_"):
|
||||
source_file = os.path.join(root, file)
|
||||
dest_file = os.path.join(service_dir, file)
|
||||
shutil.copy2(source_file, dest_file)
|
||||
print(f"已复制Python文件: {source_file} -> {dest_file}")
|
||||
|
||||
# 如果有algorithm.py,就使用它,否则创建一个模拟的
|
||||
if not os.path.exists(os.path.join(service_dir, "algorithm.py")):
|
||||
print("未找到algorithm.py,创建模拟算法文件")
|
||||
self._create_mock_algorithm(service_dir)
|
||||
else:
|
||||
# 创建模拟的算法文件
|
||||
self._create_mock_algorithm(service_dir)
|
||||
|
||||
def _create_mock_algorithm(self, service_dir: str):
|
||||
"""创建模拟的算法文件
|
||||
|
||||
Args:
|
||||
service_dir: 服务目录
|
||||
"""
|
||||
algorithm_content = """
|
||||
def predict(data):
|
||||
return {"result": "Prediction result", "input": data}
|
||||
@@ -1316,9 +1366,9 @@ def main(data):
|
||||
|
||||
# 构建启动命令
|
||||
if project_info["project_type"] == "python":
|
||||
cmd = ["python", f"service_wrapper.py"]
|
||||
cmd = ["python", "service_wrapper.py"]
|
||||
else:
|
||||
cmd = ["node", f"service_wrapper.js"]
|
||||
cmd = ["node", "service_wrapper.js"]
|
||||
|
||||
# 设置环境变量
|
||||
env = os.environ.copy()
|
||||
|
||||
2
backend/backend.log
Normal file
2
backend/backend.log
Normal file
@@ -0,0 +1,2 @@
|
||||
INFO: Will watch for changes in these directories: ['/Users/duguoyou/MLFlow/algorithm-showcase/backend']
|
||||
ERROR: [Errno 48] Address already in use
|
||||
38
backend/check_algorithms.py
Normal file
38
backend/check_algorithms.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检查算法数据"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.models.database import SessionLocal
|
||||
from app.models.models import Algorithm
|
||||
|
||||
def check_algorithms():
|
||||
"""检查算法数据"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
algorithms = db.query(Algorithm).all()
|
||||
|
||||
print(f"数据库中共有 {len(algorithms)} 个算法:\n")
|
||||
|
||||
for algo in algorithms:
|
||||
print(f"算法名称: {algo.name}")
|
||||
print(f" ID: {algo.id}")
|
||||
print(f" 类型: {algo.type}")
|
||||
print(f" 技术分类: {algo.tech_category}")
|
||||
print(f" 输出类型: {algo.output_type}")
|
||||
print(f" 描述: {algo.description}")
|
||||
print(f" 状态: {algo.status}")
|
||||
print(f" 版本数: {len(algo.versions)}")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
print(f"检查算法数据失败: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_algorithms()
|
||||
57
backend/check_user_role.py
Normal file
57
backend/check_user_role.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检查用户角色信息"""
|
||||
|
||||
import requests
|
||||
|
||||
def check_user_role():
|
||||
"""检查用户角色"""
|
||||
base_url = "http://localhost:8001/api/v1"
|
||||
|
||||
# 登录
|
||||
print("步骤1: 登录")
|
||||
login_data = {
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{base_url}/users/login", json=login_data)
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"登录失败: {response.text}")
|
||||
return
|
||||
|
||||
data = response.json()
|
||||
access_token = data.get('access_token')
|
||||
print(f"登录成功!")
|
||||
|
||||
# 获取用户信息
|
||||
print("\n步骤2: 获取用户信息")
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
user_response = requests.get(f"{base_url}/users/me", headers=headers)
|
||||
print(f"状态码: {user_response.status_code}")
|
||||
|
||||
if user_response.status_code == 200:
|
||||
user_data = user_response.json()
|
||||
print(f"\n用户信息:")
|
||||
print(f" 用户名: {user_data.get('username', 'N/A')}")
|
||||
print(f" 邮箱: {user_data.get('email', 'N/A')}")
|
||||
print(f" 角色ID: {user_data.get('role_id', 'N/A')}")
|
||||
print(f" 角色名称: {user_data.get('role_name', 'N/A')}")
|
||||
print(f" 角色对象: {user_data.get('role', 'N/A')}")
|
||||
|
||||
# 检查是否是管理员
|
||||
role_name = user_data.get('role_name')
|
||||
if role_name == 'admin':
|
||||
print(f"\n✅ 用户是管理员,应该显示后台管理页面")
|
||||
else:
|
||||
print(f"\n❌ 用户不是管理员,角色名称是: {role_name}")
|
||||
else:
|
||||
print(f"获取用户信息失败: {user_response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_user_role()
|
||||
@@ -1,39 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
检查数据库中的用户信息
|
||||
"""
|
||||
"""检查数据库中的用户信息"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '/Users/duguoyou/MLFlow/algorithm-showcase/backend')
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.database import SessionLocal
|
||||
from app.models.models import User
|
||||
|
||||
from app.services.user import UserService
|
||||
|
||||
def check_users():
|
||||
"""检查数据库中的用户信息"""
|
||||
"""检查用户"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# 获取所有用户
|
||||
users = db.query(User).all()
|
||||
|
||||
if users:
|
||||
print("数据库中的用户信息:")
|
||||
print("-" * 50)
|
||||
print(f"数据库中的用户数量: {len(users)}")
|
||||
|
||||
for user in users:
|
||||
print(f"用户ID: {user.id}")
|
||||
print(f"用户名: {user.username}")
|
||||
print(f"邮箱: {user.email}")
|
||||
print(f"角色: {user.role}")
|
||||
print(f"状态: {user.status}")
|
||||
print(f"创建时间: {user.created_at}")
|
||||
print("-" * 50)
|
||||
for user in users:
|
||||
print(f"\n用户ID: {user.id}")
|
||||
print(f"用户名: {user.username}")
|
||||
print(f"邮箱: {user.email}")
|
||||
print(f"状态: {user.status}")
|
||||
print(f"角色ID: {user.role_id}")
|
||||
print(f"密码哈希: {user.password_hash[:50]}...")
|
||||
|
||||
# 测试admin用户认证
|
||||
print("\n\n测试admin用户认证:")
|
||||
admin_user = UserService.get_user_by_username(db, 'admin')
|
||||
if admin_user:
|
||||
print(f"找到admin用户: {admin_user.id}")
|
||||
print(f"密码哈希: {admin_user.password_hash[:50]}...")
|
||||
|
||||
# 测试密码验证
|
||||
test_password = 'admin123'
|
||||
is_valid = UserService.verify_password(test_password, admin_user.password_hash)
|
||||
print(f"密码 '{test_password}' 验证结果: {is_valid}")
|
||||
|
||||
# 尝试认证
|
||||
authenticated_user = UserService.authenticate_user(db, 'admin', test_password)
|
||||
if authenticated_user:
|
||||
print(f"认证成功: {authenticated_user.id}")
|
||||
else:
|
||||
print("认证失败")
|
||||
else:
|
||||
print("数据库中没有用户信息")
|
||||
print("未找到admin用户")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_users()
|
||||
151
backend/create_sample_algorithms.py
Normal file
151
backend/create_sample_algorithms.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""创建示例算法数据"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.models.database import SessionLocal
|
||||
from app.models.models import Algorithm, AlgorithmVersion
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
def create_sample_algorithms():
|
||||
"""创建示例算法"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# 示例算法数据
|
||||
algorithms_data = [
|
||||
{
|
||||
"name": "目标检测",
|
||||
"description": "识别图像中的物体位置和类别,支持人脸、车辆、物品等多种目标检测",
|
||||
"type": "computer_vision",
|
||||
"tech_category": "computer_vision",
|
||||
"output_type": "image",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"url": "http://0.0.0.0:8001",
|
||||
"is_default": True
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "视频分析",
|
||||
"description": "分析视频内容,提取关键帧、识别动作、追踪物体等",
|
||||
"type": "computer_vision",
|
||||
"tech_category": "video_processing",
|
||||
"output_type": "video",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"url": "http://0.0.0.0:8002",
|
||||
"is_default": True
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "图像增强",
|
||||
"description": "提升图像质量,包括去噪、超分辨率、色彩校正等功能",
|
||||
"type": "computer_vision",
|
||||
"tech_category": "computer_vision",
|
||||
"output_type": "image",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"url": "http://0.0.0.0:8003",
|
||||
"is_default": True
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "文本分类",
|
||||
"description": "对文本内容进行分类,支持新闻分类、情感分析、垃圾邮件识别等",
|
||||
"type": "nlp",
|
||||
"tech_category": "nlp",
|
||||
"output_type": "text",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"url": "http://0.0.0.0:8004",
|
||||
"is_default": True
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "异常检测",
|
||||
"description": "检测数据中的异常模式,适用于工业监控、金融风控等场景",
|
||||
"type": "ml",
|
||||
"tech_category": "ml",
|
||||
"output_type": "json",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"url": "http://0.0.0.0:8005",
|
||||
"is_default": True
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "医学影像分析",
|
||||
"description": "分析医学影像,辅助医生进行疾病诊断,支持CT、MRI等多种影像格式",
|
||||
"type": "medical",
|
||||
"tech_category": "computer_vision",
|
||||
"output_type": "image",
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"url": "http://0.0.0.0:8006",
|
||||
"is_default": True
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# 创建算法
|
||||
for algo_data in algorithms_data:
|
||||
# 检查算法是否已存在
|
||||
existing_algo = db.query(Algorithm).filter(Algorithm.name == algo_data["name"]).first()
|
||||
if existing_algo:
|
||||
print(f"✓ 算法 '{algo_data['name']}' 已存在,跳过")
|
||||
continue
|
||||
|
||||
# 创建算法
|
||||
algorithm = Algorithm(
|
||||
id=str(uuid.uuid4()),
|
||||
name=algo_data["name"],
|
||||
description=algo_data["description"],
|
||||
type=algo_data["type"],
|
||||
tech_category=algo_data["tech_category"],
|
||||
output_type=algo_data["output_type"],
|
||||
status="active"
|
||||
)
|
||||
db.add(algorithm)
|
||||
db.flush() # 获取算法ID
|
||||
|
||||
# 创建版本
|
||||
for version_data in algo_data["versions"]:
|
||||
version = AlgorithmVersion(
|
||||
id=str(uuid.uuid4()),
|
||||
algorithm_id=algorithm.id,
|
||||
version=version_data["version"],
|
||||
url=version_data["url"],
|
||||
is_default=version_data["is_default"]
|
||||
)
|
||||
db.add(version)
|
||||
|
||||
print(f"✓ 已创建算法: {algo_data['name']}")
|
||||
|
||||
db.commit()
|
||||
print("\n示例算法创建完成!")
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"创建示例算法失败: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_sample_algorithms()
|
||||
45
backend/migrate_add_algorithm_fields.py
Normal file
45
backend/migrate_add_algorithm_fields.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""数据库迁移脚本:添加技术分类和输出类型字段"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.models.database import engine
|
||||
|
||||
def migrate():
|
||||
"""执行数据库迁移"""
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
# 检查字段是否已存在(PostgreSQL语法)
|
||||
result = conn.execute(text("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'algorithms'
|
||||
"""))
|
||||
columns = [row[0] for row in result.fetchall()]
|
||||
|
||||
# 添加 tech_category 字段
|
||||
if 'tech_category' not in columns:
|
||||
conn.execute(text("ALTER TABLE algorithms ADD COLUMN tech_category VARCHAR(50) DEFAULT 'computer_vision'"))
|
||||
print("✓ 已添加 tech_category 字段")
|
||||
else:
|
||||
print("✓ tech_category 字段已存在")
|
||||
|
||||
# 添加 output_type 字段
|
||||
if 'output_type' not in columns:
|
||||
conn.execute(text("ALTER TABLE algorithms ADD COLUMN output_type VARCHAR(50) DEFAULT 'image'"))
|
||||
print("✓ 已添加 output_type 字段")
|
||||
else:
|
||||
print("✓ output_type 字段已存在")
|
||||
|
||||
conn.commit()
|
||||
print("\n数据库迁移完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"数据库迁移失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
45
backend/migrate_add_service_fields.py
Normal file
45
backend/migrate_add_service_fields.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""数据库迁移脚本:为algorithm_services表添加技术分类和输出类型字段"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.models.database import engine
|
||||
|
||||
def migrate():
|
||||
"""执行数据库迁移"""
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
# 检查字段是否已存在(PostgreSQL语法)
|
||||
result = conn.execute(text("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'algorithm_services'
|
||||
"""))
|
||||
columns = [row[0] for row in result.fetchall()]
|
||||
|
||||
# 添加 tech_category 字段
|
||||
if 'tech_category' not in columns:
|
||||
conn.execute(text("ALTER TABLE algorithm_services ADD COLUMN tech_category VARCHAR(50) DEFAULT 'computer_vision'"))
|
||||
print("✓ 已添加 tech_category 字段到 algorithm_services 表")
|
||||
else:
|
||||
print("✓ tech_category 字段已存在于 algorithm_services 表")
|
||||
|
||||
# 添加 output_type 字段
|
||||
if 'output_type' not in columns:
|
||||
conn.execute(text("ALTER TABLE algorithm_services ADD COLUMN output_type VARCHAR(50) DEFAULT 'image'"))
|
||||
print("✓ 已添加 output_type 字段到 algorithm_services 表")
|
||||
else:
|
||||
print("✓ output_type 字段已存在于 algorithm_services 表")
|
||||
|
||||
conn.commit()
|
||||
print("\n数据库迁移完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"数据库迁移失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
48
backend/test_all_apis.py
Normal file
48
backend/test_all_apis.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试所有API端点"""
|
||||
|
||||
import requests
|
||||
|
||||
def test_apis():
|
||||
"""测试API端点"""
|
||||
base_url = "http://localhost:8001/api/v1"
|
||||
|
||||
# 测试算法列表(不需要认证)
|
||||
print("1. 测试算法列表(不需要认证):")
|
||||
try:
|
||||
response = requests.get(f"{base_url}/algorithms/")
|
||||
print(f" 状态码: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" 成功获取 {len(data.get('algorithms', []))} 个算法")
|
||||
else:
|
||||
print(f" 失败: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" 错误: {e}")
|
||||
|
||||
# 测试用户信息(需要认证)
|
||||
print("\n2. 测试用户信息(需要认证):")
|
||||
try:
|
||||
response = requests.get(f"{base_url}/users/me")
|
||||
print(f" 状态码: {response.status_code}")
|
||||
if response.status_code == 401:
|
||||
print(f" 需要认证(正常)")
|
||||
else:
|
||||
print(f" 响应: {response.text}")
|
||||
except Exception as e:
|
||||
print(f" 错误: {e}")
|
||||
|
||||
# 测试服务列表(需要认证)
|
||||
print("\n3. 测试服务列表(需要认证):")
|
||||
try:
|
||||
response = requests.get(f"{base_url}/services")
|
||||
print(f" 状态码: {response.status_code}")
|
||||
if response.status_code == 401:
|
||||
print(f" 需要认证(正常)")
|
||||
else:
|
||||
print(f" 响应: {response.text[:200]}")
|
||||
except Exception as e:
|
||||
print(f" 错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_apis()
|
||||
53
backend/test_api.py
Normal file
53
backend/test_api.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试前端API调用"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import requests
|
||||
|
||||
def test_api():
|
||||
"""测试API"""
|
||||
try:
|
||||
# 调用算法列表API
|
||||
response = requests.get('http://localhost:8001/api/v1/algorithms/')
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
algorithms = data.get('algorithms', [])
|
||||
|
||||
print(f"成功获取 {len(algorithms)} 个算法\n")
|
||||
|
||||
# 检查每个算法的字段
|
||||
for algo in algorithms:
|
||||
print(f"算法: {algo['name']}")
|
||||
print(f" 技术分类: {algo.get('tech_category', 'N/A')}")
|
||||
print(f" 输出类型: {algo.get('output_type', 'N/A')}")
|
||||
print()
|
||||
|
||||
# 测试筛选
|
||||
print("测试筛选功能:")
|
||||
|
||||
# 按技术分类筛选
|
||||
cv_algorithms = [a for a in algorithms if a.get('tech_category') == 'computer_vision']
|
||||
print(f" 计算机视觉算法: {len(cv_algorithms)} 个")
|
||||
|
||||
# 按输出类型筛选
|
||||
image_algorithms = [a for a in algorithms if a.get('output_type') == 'image']
|
||||
print(f" 图片输出算法: {len(image_algorithms)} 个")
|
||||
|
||||
# 按名称搜索
|
||||
search_results = [a for a in algorithms if '视频' in a.get('name', '')]
|
||||
print(f" 包含'视频'的算法: {len(search_results)} 个")
|
||||
|
||||
else:
|
||||
print(f"API调用失败: {response.status_code}")
|
||||
print(response.text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"测试失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_api()
|
||||
32
backend/test_frontend_proxy.py
Normal file
32
backend/test_frontend_proxy.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试前端代理配置"""
|
||||
|
||||
import requests
|
||||
|
||||
def test_frontend_proxy():
|
||||
"""测试前端代理"""
|
||||
try:
|
||||
# 测试前端代理
|
||||
response = requests.get('http://localhost:3000/api/algorithms')
|
||||
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"成功获取 {len(data.get('algorithms', []))} 个算法")
|
||||
|
||||
# 检查第一个算法的字段
|
||||
if data.get('algorithms'):
|
||||
first_algo = data['algorithms'][0]
|
||||
print(f"\n第一个算法:")
|
||||
print(f" 名称: {first_algo.get('name')}")
|
||||
print(f" 技术分类: {first_algo.get('tech_category')}")
|
||||
print(f" 输出类型: {first_algo.get('output_type')}")
|
||||
else:
|
||||
print(f"请求失败: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"测试失败: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_frontend_proxy()
|
||||
48
backend/test_full_login.py
Normal file
48
backend/test_full_login.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试完整的登录流程"""
|
||||
|
||||
import requests
|
||||
|
||||
def test_full_login_flow():
|
||||
"""测试完整的登录流程"""
|
||||
base_url = "http://localhost:8001/api/v1"
|
||||
|
||||
# 步骤1: 登录
|
||||
print("步骤1: 登录")
|
||||
login_data = {
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{base_url}/users/login", json=login_data)
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"登录失败: {response.text}")
|
||||
return
|
||||
|
||||
data = response.json()
|
||||
access_token = data.get('access_token')
|
||||
print(f"登录成功!")
|
||||
print(f"Token: {access_token[:50]}...")
|
||||
|
||||
# 步骤2: 使用token获取用户信息
|
||||
print("\n步骤2: 获取用户信息")
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
user_response = requests.get(f"{base_url}/users/me", headers=headers)
|
||||
print(f"状态码: {user_response.status_code}")
|
||||
|
||||
if user_response.status_code == 200:
|
||||
user_data = user_response.json()
|
||||
print(f"用户名: {user_data.get('username', 'N/A')}")
|
||||
print(f"邮箱: {user_data.get('email', 'N/A')}")
|
||||
print(f"角色: {user_data.get('role_name', 'N/A')}")
|
||||
else:
|
||||
print(f"获取用户信息失败: {user_response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_full_login_flow()
|
||||
45
backend/test_login.py
Normal file
45
backend/test_login.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""测试登录功能"""
|
||||
|
||||
import requests
|
||||
|
||||
def test_login():
|
||||
"""测试登录"""
|
||||
base_url = "http://localhost:8001/api/v1"
|
||||
|
||||
# 测试登录
|
||||
print("测试登录功能:")
|
||||
login_data = {
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{base_url}/users/login", json=login_data)
|
||||
print(f"状态码: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"登录成功!")
|
||||
print(f"访问令牌: {data.get('access_token', 'N/A')[:50]}...")
|
||||
print(f"令牌类型: {data.get('token_type', 'N/A')}")
|
||||
|
||||
# 测试使用令牌访问受保护的API
|
||||
if data.get('access_token'):
|
||||
headers = {"Authorization": f"Bearer {data['access_token']}"}
|
||||
user_response = requests.get(f"{base_url}/users/me", headers=headers)
|
||||
print(f"\n测试用户信息API:")
|
||||
print(f"状态码: {user_response.status_code}")
|
||||
if user_response.status_code == 200:
|
||||
user_data = user_response.json()
|
||||
print(f"用户名: {user_data.get('username', 'N/A')}")
|
||||
print(f"邮箱: {user_data.get('email', 'N/A')}")
|
||||
else:
|
||||
print(f"失败: {user_response.text}")
|
||||
else:
|
||||
print(f"登录失败: {response.text}")
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_login()
|
||||
53
backend/test_login_api.py
Normal file
53
backend/test_login_api.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""直接测试登录API"""
|
||||
|
||||
import requests
|
||||
|
||||
def test_login_api():
|
||||
"""测试登录API"""
|
||||
base_url = "http://localhost:8001/api/v1"
|
||||
|
||||
# 测试1: 使用JSON格式
|
||||
print("测试1: 使用JSON格式")
|
||||
login_data = {
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{base_url}/users/login", json=login_data)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应头: {dict(response.headers)}")
|
||||
print(f"响应内容: {response.text[:500]}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ 登录成功!")
|
||||
print(f"Token: {data.get('access_token', 'N/A')[:50]}...")
|
||||
else:
|
||||
print(f"❌ 登录失败")
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
# 测试2: 使用form-data格式
|
||||
print("\n\n测试2: 使用form-data格式")
|
||||
form_data = {
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(f"{base_url}/users/login", data=form_data)
|
||||
print(f"状态码: {response.status_code}")
|
||||
print(f"响应内容: {response.text[:500]}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ 登录成功!")
|
||||
else:
|
||||
print(f"❌ 登录失败")
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_login_api()
|
||||
232
backend/test_system.py
Normal file
232
backend/test_system.py
Normal file
@@ -0,0 +1,232 @@
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from typing import Dict, Any, List
|
||||
|
||||
class SystemTester:
|
||||
def __init__(self, base_url: str = "http://localhost:8001/api/v1"):
|
||||
self.base_url = base_url
|
||||
self.session = requests.Session()
|
||||
self.token = None
|
||||
self.user_id = None
|
||||
|
||||
def login(self, username: str = "admin", password: str = "admin123") -> bool:
|
||||
"""登录系统"""
|
||||
try:
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/users/login",
|
||||
json={"username": username, "password": password}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self.token = data.get("access_token")
|
||||
self.user_id = data.get("user_id")
|
||||
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
|
||||
print(f"✓ 登录成功: {username}")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 登录失败: {response.status_code} - {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ 登录异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def test_config_endpoints(self) -> bool:
|
||||
"""测试配置管理API"""
|
||||
print("\n=== 测试配置管理API ===")
|
||||
success = True
|
||||
|
||||
try:
|
||||
# 测试获取所有配置
|
||||
response = self.session.get(f"{self.base_url}/config/")
|
||||
if response.status_code == 200:
|
||||
print("✓ 获取所有配置成功")
|
||||
configs = response.json().get("configs", [])
|
||||
print(f" 当前配置数量: {len(configs)}")
|
||||
else:
|
||||
print(f"✗ 获取所有配置失败: {response.status_code}")
|
||||
success = False
|
||||
|
||||
# 测试添加配置
|
||||
test_config = {
|
||||
"value": "test_value_123",
|
||||
"type": "system",
|
||||
"service_id": None,
|
||||
"description": "测试配置"
|
||||
}
|
||||
response = self.session.post(f"{self.base_url}/config/test_config_key", json=test_config)
|
||||
if response.status_code == 200:
|
||||
print("✓ 添加配置成功")
|
||||
else:
|
||||
print(f"✗ 添加配置失败: {response.status_code} - {response.text}")
|
||||
success = False
|
||||
|
||||
# 测试获取单个配置
|
||||
response = self.session.get(f"{self.base_url}/config/test_config_key")
|
||||
if response.status_code == 200:
|
||||
print("✓ 获取单个配置成功")
|
||||
config_data = response.json()
|
||||
print(f" 配置值: {config_data.get('value')}")
|
||||
else:
|
||||
print(f"✗ 获取单个配置失败: {response.status_code}")
|
||||
success = False
|
||||
|
||||
# 测试删除配置
|
||||
response = self.session.delete(f"{self.base_url}/config/test_config_key")
|
||||
if response.status_code == 200:
|
||||
print("✓ 删除配置成功")
|
||||
else:
|
||||
print(f"✗ 删除配置失败: {response.status_code}")
|
||||
success = False
|
||||
|
||||
return success
|
||||
except Exception as e:
|
||||
print(f"✗ 配置管理API测试异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def test_comparison_endpoints(self) -> bool:
|
||||
"""测试算法比较API"""
|
||||
print("\n=== 测试算法比较API ===")
|
||||
success = True
|
||||
|
||||
try:
|
||||
# 测试算法比较(使用模拟数据)
|
||||
test_data = {
|
||||
"input_data": {"text": "这是一段测试文本"},
|
||||
"algorithm_configs": [
|
||||
{
|
||||
"algorithm_id": "test_algo_1",
|
||||
"algorithm_name": "测试算法1",
|
||||
"version": "1.0.0",
|
||||
"config": "{}"
|
||||
},
|
||||
{
|
||||
"algorithm_id": "test_algo_2",
|
||||
"algorithm_name": "测试算法2",
|
||||
"version": "1.0.0",
|
||||
"config": "{}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = self.session.post(f"{self.base_url}/comparison/compare-algorithms", json=test_data)
|
||||
if response.status_code == 200:
|
||||
print("✓ 算法比较API调用成功")
|
||||
result = response.json()
|
||||
print(f" 比较状态: {result.get('success')}")
|
||||
if result.get('results'):
|
||||
print(f" 结果数量: {len(result.get('results'))}")
|
||||
else:
|
||||
print(f"✗ 算法比较失败: {response.status_code} - {response.text}")
|
||||
success = False
|
||||
|
||||
return success
|
||||
except Exception as e:
|
||||
print(f"✗ 算法比较API测试异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def test_existing_endpoints(self) -> bool:
|
||||
"""测试现有API端点"""
|
||||
print("\n=== 测试现有API端点 ===")
|
||||
success = True
|
||||
|
||||
try:
|
||||
# 测试健康检查
|
||||
response = self.session.get(f"{self.base_url.replace('/api/v1', '')}/health")
|
||||
if response.status_code == 200:
|
||||
print("✓ 健康检查通过")
|
||||
else:
|
||||
print(f"✗ 健康检查失败: {response.status_code}")
|
||||
success = False
|
||||
|
||||
# 测试获取当前用户
|
||||
response = self.session.get(f"{self.base_url}/users/me")
|
||||
if response.status_code == 200:
|
||||
print("✓ 获取当前用户成功")
|
||||
user_data = response.json()
|
||||
print(f" 用户名: {user_data.get('username')}")
|
||||
else:
|
||||
print(f"✗ 获取当前用户失败: {response.status_code}")
|
||||
success = False
|
||||
|
||||
# 测试获取算法列表
|
||||
response = self.session.get(f"{self.base_url}/algorithms/")
|
||||
if response.status_code == 200:
|
||||
print("✓ 获取算法列表成功")
|
||||
algorithms = response.json()
|
||||
print(f" 算法数量: {len(algorithms) if isinstance(algorithms, list) else 0}")
|
||||
else:
|
||||
print(f"✗ 获取算法列表失败: {response.status_code}")
|
||||
success = False
|
||||
|
||||
# 测试获取服务列表
|
||||
response = self.session.get(f"{self.base_url}/services")
|
||||
if response.status_code == 200:
|
||||
print("✓ 获取服务列表成功")
|
||||
services = response.json()
|
||||
print(f" 服务数量: {len(services) if isinstance(services, list) else 0}")
|
||||
else:
|
||||
print(f"✗ 获取服务列表失败: {response.status_code}")
|
||||
success = False
|
||||
|
||||
return success
|
||||
except Exception as e:
|
||||
print(f"✗ 现有API端点测试异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def run_all_tests(self) -> Dict[str, bool]:
|
||||
"""运行所有测试"""
|
||||
print("=" * 50)
|
||||
print("开始系统自动化测试")
|
||||
print("=" * 50)
|
||||
|
||||
results = {}
|
||||
|
||||
# 登录
|
||||
if not self.login():
|
||||
print("\n✗ 登录失败,无法继续测试")
|
||||
return {"login": False}
|
||||
|
||||
results["login"] = True
|
||||
|
||||
# 测试现有端点
|
||||
results["existing_endpoints"] = self.test_existing_endpoints()
|
||||
|
||||
# 测试配置管理API
|
||||
results["config_endpoints"] = self.test_config_endpoints()
|
||||
|
||||
# 测试算法比较API
|
||||
results["comparison_endpoints"] = self.test_comparison_endpoints()
|
||||
|
||||
# 输出测试结果
|
||||
print("\n" + "=" * 50)
|
||||
print("测试结果汇总")
|
||||
print("=" * 50)
|
||||
|
||||
for test_name, result in results.items():
|
||||
status = "✓ 通过" if result else "✗ 失败"
|
||||
print(f"{test_name}: {status}")
|
||||
|
||||
total_tests = len(results)
|
||||
passed_tests = sum(1 for result in results.values() if result)
|
||||
|
||||
print(f"\n总计: {passed_tests}/{total_tests} 测试通过")
|
||||
|
||||
if passed_tests == total_tests:
|
||||
print("🎉 所有测试通过!")
|
||||
else:
|
||||
print("⚠️ 部分测试失败,请检查日志")
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
tester = SystemTester()
|
||||
results = tester.run_all_tests()
|
||||
|
||||
# 返回退出码
|
||||
exit_code = 0 if all(results.values()) else 1
|
||||
return exit_code
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
181
docker-compose.yml
Normal file
181
docker-compose.yml
Normal file
@@ -0,0 +1,181 @@
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
ai-services-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
minio-data:
|
||||
gitea-data:
|
||||
algorithm-logs:
|
||||
|
||||
services:
|
||||
# API 网关
|
||||
api-gateway:
|
||||
build:
|
||||
context: ./api-gateway
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "80:80"
|
||||
networks:
|
||||
- ai-services-network
|
||||
depends_on:
|
||||
- backend
|
||||
- text-classification
|
||||
- image-recognition
|
||||
- speech-to-text
|
||||
- openai-proxy
|
||||
restart: always
|
||||
|
||||
# 后端服务
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8000:8000"
|
||||
networks:
|
||||
- ai-services-network
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- minio
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://admin:password@postgres:5432/algorithm_db
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- MINIO_URL=http://minio:9000
|
||||
- MINIO_ACCESS_KEY=minioadmin
|
||||
- MINIO_SECRET_KEY=minioadmin
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- DEPLOYMENT_MODE=docker
|
||||
volumes:
|
||||
- algorithm-logs:/app/logs
|
||||
restart: always
|
||||
|
||||
# 算法服务1:文本分类
|
||||
text-classification:
|
||||
build:
|
||||
context: ./services/text-classification
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8001:8000"
|
||||
networks:
|
||||
- ai-services-network
|
||||
environment:
|
||||
- SERVICE_NAME=text-classification
|
||||
- LOG_LEVEL=info
|
||||
volumes:
|
||||
- algorithm-logs:/app/logs
|
||||
restart: always
|
||||
|
||||
# 算法服务2:图像识别
|
||||
image-recognition:
|
||||
build:
|
||||
context: ./services/image-recognition
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8002:8000"
|
||||
networks:
|
||||
- ai-services-network
|
||||
environment:
|
||||
- SERVICE_NAME=image-recognition
|
||||
- LOG_LEVEL=info
|
||||
volumes:
|
||||
- algorithm-logs:/app/logs
|
||||
restart: always
|
||||
|
||||
# 算法服务3:语音转文字
|
||||
speech-to-text:
|
||||
build:
|
||||
context: ./services/speech-to-text
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8003:8000"
|
||||
networks:
|
||||
- ai-services-network
|
||||
environment:
|
||||
- SERVICE_NAME=speech-to-text
|
||||
- LOG_LEVEL=info
|
||||
volumes:
|
||||
- algorithm-logs:/app/logs
|
||||
restart: always
|
||||
|
||||
# OpenAI 代理服务
|
||||
openai-proxy:
|
||||
build:
|
||||
context: ./services/openai-proxy
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8004:8000"
|
||||
networks:
|
||||
- ai-services-network
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- LOG_LEVEL=info
|
||||
volumes:
|
||||
- algorithm-logs:/app/logs
|
||||
restart: always
|
||||
|
||||
# 数据库
|
||||
postgres:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- ai-services-network
|
||||
environment:
|
||||
- POSTGRES_DB=algorithm_db
|
||||
- POSTGRES_USER=admin
|
||||
- POSTGRES_PASSWORD=password
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
# 缓存
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- ai-services-network
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
restart: always
|
||||
|
||||
# 对象存储
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
networks:
|
||||
- ai-services-network
|
||||
environment:
|
||||
- MINIO_ROOT_USER=minioadmin
|
||||
- MINIO_ROOT_PASSWORD=minioadmin
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
restart: always
|
||||
|
||||
# 代码管理
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "222:22"
|
||||
networks:
|
||||
- ai-services-network
|
||||
environment:
|
||||
- GITEA__database__TYPE=postgres
|
||||
- GITEA__database__HOST=postgres:5432
|
||||
- GITEA__database__NAME=gitea_db
|
||||
- GITEA__database__USER=admin
|
||||
- GITEA__database__PASSWD=password
|
||||
volumes:
|
||||
- gitea-data:/data
|
||||
depends_on:
|
||||
- postgres
|
||||
restart: always
|
||||
117
frontend/public/test_algorithm_api.html
Normal file
117
frontend/public/test_algorithm_api.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>测试算法API</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
.info {
|
||||
color: blue;
|
||||
}
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>测试算法API</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. 测试GET /api/algorithms</h2>
|
||||
<button onclick="testAlgorithmsApi()">测试算法API</button>
|
||||
<div id="algorithmsResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. 测试GET /api/v1/algorithms</h2>
|
||||
<button onclick="testAlgorithmsApiV1()">测试算法API V1</button>
|
||||
<div id="algorithmsResultV1"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testAlgorithmsApi() {
|
||||
const resultDiv = document.getElementById('algorithmsResult');
|
||||
resultDiv.innerHTML = '<p class="info">正在测试...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/algorithms', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = '<p class="success">✅ 算法API调用成功!</p>';
|
||||
resultDiv.innerHTML += '<p>算法数量: ' + data.algorithms.length + '</p>';
|
||||
resultDiv.innerHTML += '<pre>' + JSON.stringify(data.algorithms.map(a => ({name: a.name, id: a.id})), null, 2) + '</pre>';
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API调用失败!</p>';
|
||||
resultDiv.innerHTML += '<p>状态码: ' + response.status + '</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + JSON.stringify(data) + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API请求失败!</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function testAlgorithmsApiV1() {
|
||||
const resultDiv = document.getElementById('algorithmsResultV1');
|
||||
resultDiv.innerHTML = '<p class="info">正在测试...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/algorithms', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = '<p class="success">✅ 算法API V1调用成功!</p>';
|
||||
resultDiv.innerHTML += '<p>算法数量: ' + data.algorithms.length + '</p>';
|
||||
resultDiv.innerHTML += '<pre>' + JSON.stringify(data.algorithms.map(a => ({name: a.name, id: a.id})), null, 2) + '</pre>';
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API V1调用失败!</p>';
|
||||
resultDiv.innerHTML += '<p>状态码: ' + response.status + '</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + JSON.stringify(data) + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API V1请求失败!</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,8 +13,8 @@
|
||||
<el-menu-item index="/">
|
||||
<router-link to="/">首页</router-link>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/algorithms">
|
||||
<router-link to="/algorithms">算法列表</router-link>
|
||||
<el-menu-item v-if="userStore.isLoggedIn" index="/algorithms">
|
||||
<router-link to="/algorithms">算法展示平台</router-link>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/admin">
|
||||
<router-link to="/admin">管理员中心</router-link>
|
||||
@@ -84,6 +84,14 @@ const handleLogout = () => {
|
||||
// 初始化用户状态
|
||||
onMounted(() => {
|
||||
userStore.init()
|
||||
|
||||
// 调试信息
|
||||
console.log('App.vue - userStore初始化完成')
|
||||
console.log('isLoggedIn:', userStore.isLoggedIn)
|
||||
console.log('user:', userStore.user)
|
||||
console.log('user.role?.name:', userStore.user?.role?.name)
|
||||
console.log('user.role_name:', userStore.user?.role_name)
|
||||
console.log('isAdmin:', userStore.isAdmin)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import { ElMessage } from 'element-plus'
|
||||
// 配置axios
|
||||
// 移除baseURL配置,使用Vite代理处理API路径映射
|
||||
axios.defaults.headers.common['Content-Type'] = 'application/json'
|
||||
axios.defaults.withCredentials = true
|
||||
|
||||
// 添加请求拦截器,用于添加认证token和调试日志
|
||||
axios.interceptors.request.use(
|
||||
@@ -39,11 +38,18 @@ axios.interceptors.request.use(
|
||||
console.log('发送请求:', config.method?.toUpperCase(), config.url)
|
||||
console.log('完整URL:', (axios.defaults.baseURL || '') + (config.url || ''))
|
||||
|
||||
// 添加认证token
|
||||
// 添加认证token(登录请求除外)
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
console.log('请求拦截器 - localStorage中的token:', token ? token.substring(0, 50) + '...' : 'null')
|
||||
|
||||
// 如果是登录请求,不添加token
|
||||
if (token && !config.url?.includes('/login')) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
console.log('请求拦截器 - 已添加Authorization头:', config.headers.Authorization.substring(0, 50) + '...')
|
||||
} else {
|
||||
console.log('请求拦截器 - 未添加Authorization头')
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
@@ -61,9 +67,39 @@ axios.interceptors.response.use(
|
||||
console.error('请求错误:', error)
|
||||
console.error('错误状态:', error.response?.status)
|
||||
console.error('错误数据:', error.response?.data)
|
||||
console.error('请求URL:', error.config?.url)
|
||||
|
||||
// 处理401错误,跳转到登录页
|
||||
// 处理401错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 如果是登录请求失败,不显示"登录已过期"的错误
|
||||
if (error.config?.url?.includes('/login')) {
|
||||
// 登录失败,让登录页面自己处理错误
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 如果是获取用户信息失败,清除token但不显示错误消息
|
||||
if (error.config?.url?.includes('/users/me')) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 如果是Gitea配置请求失败,不显示错误消息
|
||||
if (error.config?.url?.includes('/gitea/config')) {
|
||||
// Gitea配置可能不存在,不显示错误
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 如果是仓库列表请求失败,不显示错误消息并记录详细日志
|
||||
if (error.config?.url?.includes('/repositories')) {
|
||||
console.error('仓库列表请求失败:', error)
|
||||
console.error('错误详情:', error.response?.data)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 其他401错误,跳转到登录页
|
||||
console.error('401错误 - 请求URL:', error.config?.url)
|
||||
console.error('401错误 - 响应数据:', error.response?.data)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
|
||||
@@ -16,7 +16,8 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'Algorithms',
|
||||
component: () => import('../views/AlgorithmsView.vue'),
|
||||
meta: {
|
||||
title: '算法列表'
|
||||
title: '算法展示平台',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -93,6 +94,22 @@ const routes: Array<RouteRecordRaw> = [
|
||||
meta: {
|
||||
title: '服务注册'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'AdminConfigManagement',
|
||||
component: () => import('../views/admin/AdminConfigManagementView.vue'),
|
||||
meta: {
|
||||
title: '配置管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'comparison',
|
||||
name: 'AdminAlgorithmComparison',
|
||||
component: () => import('../views/admin/AdminAlgorithmComparisonView.vue'),
|
||||
meta: {
|
||||
title: '算法效果比较'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
141
frontend/src/services/admin.ts
Normal file
141
frontend/src/services/admin.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export interface ConfigItem {
|
||||
id: string
|
||||
config_key: string
|
||||
config_value: string
|
||||
config_type: string
|
||||
service_id: string | null
|
||||
description: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AlgorithmConfig {
|
||||
algorithm_id: string
|
||||
algorithm_name: string
|
||||
version: string
|
||||
config: string
|
||||
}
|
||||
|
||||
export interface ComparisonResult {
|
||||
success: boolean
|
||||
comparison_time: string
|
||||
results: Array<{
|
||||
algorithm_name: string
|
||||
algorithm_id: string
|
||||
version: string
|
||||
execution_time: number
|
||||
success: boolean
|
||||
output: any
|
||||
error: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ComparisonReport {
|
||||
success: boolean
|
||||
report_time: string
|
||||
summary: string
|
||||
performance_analysis: string
|
||||
recommendations: string
|
||||
}
|
||||
|
||||
class ConfigService {
|
||||
private readonly baseUrl = '/api/config'
|
||||
|
||||
async getConfig(configKey: string): Promise<ConfigItem | null> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/${configKey}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async setConfig(configKey: string, configData: {
|
||||
value: string
|
||||
type?: string
|
||||
service_id?: string | null
|
||||
description?: string
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/${configKey}`, {
|
||||
value: configData.value,
|
||||
type: configData.type || 'system',
|
||||
service_id: configData.service_id || null,
|
||||
description: configData.description || ''
|
||||
})
|
||||
return response.data.message === '设置配置成功'
|
||||
} catch (error) {
|
||||
console.error('设置配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getServiceConfigs(serviceId: string): Promise<ConfigItem[]> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/service/${serviceId}`)
|
||||
return response.data.configs || []
|
||||
} catch (error) {
|
||||
console.error('获取服务配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteConfig(configKey: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.delete(`${this.baseUrl}/${configKey}`)
|
||||
return response.data.message === '删除配置成功'
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getAllConfigs(configType?: string): Promise<ConfigItem[]> {
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
if (configType) {
|
||||
params.config_type = configType
|
||||
}
|
||||
|
||||
const response = await axios.get(`${this.baseUrl}/`, { params })
|
||||
return response.data.configs || []
|
||||
} catch (error) {
|
||||
console.error('获取所有配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ComparisonService {
|
||||
private readonly baseUrl = '/api/comparison'
|
||||
|
||||
async compareAlgorithms(inputData: any, algorithmConfigs: AlgorithmConfig[]): Promise<ComparisonResult> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/compare-algorithms`, {
|
||||
input_data: inputData,
|
||||
algorithm_configs: algorithmConfigs
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('算法比较失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async generateComparisonReport(comparisonResults: ComparisonResult): Promise<ComparisonReport> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/generate-report`, comparisonResults)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('生成比较报告失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const configService = new ConfigService()
|
||||
export const comparisonService = new ComparisonService()
|
||||
224
frontend/src/services/apiManagement.ts
Normal file
224
frontend/src/services/apiManagement.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const BASE_URL = 'http://0.0.0.0:8001/api/v1/api-management'
|
||||
|
||||
export interface ApiEndpoint {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
path: string
|
||||
method: string
|
||||
algorithm_id: string
|
||||
algorithm_name: string
|
||||
version_id: string
|
||||
version: string
|
||||
service_id: string | null
|
||||
status: string
|
||||
is_public: boolean
|
||||
call_count: string
|
||||
success_count: string
|
||||
error_count: string
|
||||
avg_response_time: string
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
last_called_at: string | null
|
||||
}
|
||||
|
||||
export interface ApiStats {
|
||||
total_endpoints: number
|
||||
active_endpoints: number
|
||||
total_calls: string
|
||||
total_success: string
|
||||
total_errors: string
|
||||
avg_response_time: string
|
||||
}
|
||||
|
||||
export const apiManagementService = {
|
||||
async getApiEndpoints(algorithmId?: string, status?: string): Promise<{ endpoints: ApiEndpoint[], total: number }> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (algorithmId) params.append('algorithm_id', algorithmId)
|
||||
if (status) params.append('status', status)
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取API端点列表失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async getApiEndpoint(endpointId: string): Promise<ApiEndpoint> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取API端点详情失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async createApiEndpoint(data: {
|
||||
name: string
|
||||
description: string
|
||||
path: string
|
||||
method: string
|
||||
algorithm_id: string
|
||||
version_id: string
|
||||
service_id?: string
|
||||
requires_auth: boolean
|
||||
allowed_roles: string[]
|
||||
rate_limit?: Record<string, any>
|
||||
is_public: boolean
|
||||
config: Record<string, any>
|
||||
}): Promise<ApiEndpoint> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '创建API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async updateApiEndpoint(endpointId: string, data: Partial<{
|
||||
name: string
|
||||
description: string
|
||||
path: string
|
||||
method: string
|
||||
requires_auth: boolean
|
||||
allowed_roles: string[]
|
||||
rate_limit?: Record<string, any>
|
||||
is_public: boolean
|
||||
config: Record<string, any>
|
||||
status: string
|
||||
}>): Promise<ApiEndpoint> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '更新API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async deleteApiEndpoint(endpointId: string): Promise<{ success: boolean; message: string }> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '删除API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async getApiStats(): Promise<ApiStats> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/stats`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取API统计信息失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async testApiEndpoint(endpointId: string, payload: Record<string, any>): Promise<{
|
||||
success: boolean
|
||||
result?: any
|
||||
response_time: number
|
||||
message?: string
|
||||
error?: string
|
||||
}> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '测试API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
}
|
||||
94
frontend/src/services/serviceManagement.ts
Normal file
94
frontend/src/services/serviceManagement.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export interface Service {
|
||||
id: string
|
||||
service_id: string
|
||||
name: string
|
||||
algorithm_name: string
|
||||
version: string
|
||||
host: string
|
||||
port: number
|
||||
api_url: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface ServiceOperationResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
service_id: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ServiceListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
services: Service[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface RegisterServiceRequest {
|
||||
repository_id: string
|
||||
name: string
|
||||
version: string
|
||||
service_type: string
|
||||
host: string
|
||||
port: number
|
||||
timeout: number
|
||||
health_check_path: string
|
||||
environment: Record<string, any>
|
||||
}
|
||||
|
||||
export interface RegisterServiceResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
service: Service
|
||||
}
|
||||
|
||||
export interface DeleteServiceResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
service_id: string
|
||||
}
|
||||
|
||||
const BASE_URL = '/api/v1/services'
|
||||
|
||||
export const serviceManagementApi = {
|
||||
getServices: async (): Promise<ServiceListResponse> => {
|
||||
const response = await axios.get<ServiceListResponse>(BASE_URL)
|
||||
return response.data
|
||||
},
|
||||
|
||||
registerService: async (data: RegisterServiceRequest): Promise<RegisterServiceResponse> => {
|
||||
const response = await axios.post<RegisterServiceResponse>(`${BASE_URL}/register`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
startService: async (serviceId: string): Promise<ServiceOperationResponse> => {
|
||||
const response = await axios.post<ServiceOperationResponse>(`${BASE_URL}/${serviceId}/start`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
stopService: async (serviceId: string): Promise<ServiceOperationResponse> => {
|
||||
const response = await axios.post<ServiceOperationResponse>(`${BASE_URL}/${serviceId}/stop`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
restartService: async (serviceId: string): Promise<ServiceOperationResponse> => {
|
||||
const response = await axios.post<ServiceOperationResponse>(`${BASE_URL}/${serviceId}/restart`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
deleteService: async (serviceId: string): Promise<DeleteServiceResponse> => {
|
||||
const response = await axios.delete<DeleteServiceResponse>(`${BASE_URL}/${serviceId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
getServiceDetail: async (serviceId: string): Promise<Service> => {
|
||||
const response = await axios.get<{ success: boolean; message: string; service: Service }>(`${BASE_URL}/${serviceId}`)
|
||||
return response.data.service
|
||||
}
|
||||
}
|
||||
|
||||
export default serviceManagementApi
|
||||
@@ -7,6 +7,8 @@ interface Algorithm {
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
tech_category: string // 技术分类:计算机视觉、视频处理、自然语言处理等
|
||||
output_type: string // 输出类型:图片、视频、文本、JSON等
|
||||
status: string
|
||||
versions: AlgorithmVersion[]
|
||||
created_at: string
|
||||
@@ -70,7 +72,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
|
||||
try {
|
||||
const params = type ? { type } : {}
|
||||
const response = await axios.get('/algorithms', { params })
|
||||
const response = await axios.get('/api/algorithms', { params })
|
||||
this.algorithms = response.data.algorithms
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -87,7 +89,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/${id}`)
|
||||
const response = await axios.get(`/api/algorithms/${id}`)
|
||||
this.currentAlgorithm = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -104,7 +106,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/${algorithmId}/versions`)
|
||||
const response = await axios.get(`/api/algorithms/${algorithmId}/versions`)
|
||||
if (this.currentAlgorithm) {
|
||||
this.currentAlgorithm.versions = response.data
|
||||
}
|
||||
@@ -123,7 +125,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/algorithms/call', request)
|
||||
const response = await axios.post('/api/algorithms/call', request)
|
||||
this.callResult = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -140,7 +142,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/calls/${callId}`)
|
||||
const response = await axios.get(`/api/algorithms/calls/${callId}`)
|
||||
this.callResult = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -157,7 +159,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/openai/generate-data', { prompt, data_type: dataType })
|
||||
const response = await axios.post('/api/openai/generate-data', { prompt, data_type: dataType })
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '生成仿真数据失败'
|
||||
@@ -173,7 +175,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/algorithms', algorithmData)
|
||||
const response = await axios.post('/api/algorithms', algorithmData)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '创建算法失败'
|
||||
|
||||
@@ -15,6 +15,7 @@ interface User {
|
||||
email: string
|
||||
role_id: string
|
||||
role?: Role
|
||||
role_name?: string
|
||||
status: string
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token,
|
||||
isAdmin: (state) => state.user?.role?.name === 'admin'
|
||||
isAdmin: (state) => state.user?.role?.name === 'admin' || state.user?.role_name === 'admin'
|
||||
},
|
||||
|
||||
actions: {
|
||||
@@ -54,16 +55,27 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
try {
|
||||
console.log('开始登录请求...', credentials)
|
||||
console.log('登录前的token:', this.token ? this.token.substring(0, 50) + '...' : 'null')
|
||||
console.log('登录前的localStorage:', {
|
||||
token: localStorage.getItem('token')?.substring(0, 50) + '...',
|
||||
user: localStorage.getItem('user')
|
||||
})
|
||||
|
||||
const response = await axios.post('/api/users/login', credentials)
|
||||
console.log('登录响应:', response.data)
|
||||
const { access_token } = response.data
|
||||
|
||||
// 保存token到本地存储
|
||||
// 保存token到本地存储(这一步必须在fetchUser之前)
|
||||
localStorage.setItem('token', access_token)
|
||||
this.token = access_token
|
||||
|
||||
// 获取用户信息
|
||||
await this.fetchUser()
|
||||
// 获取用户信息(即使失败也不影响登录)
|
||||
try {
|
||||
await this.fetchUser()
|
||||
} catch (error) {
|
||||
console.warn('获取用户信息失败,但登录已成功:', error)
|
||||
// fetchUser失败不影响登录,用户信息可以在后续请求中获取
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -113,14 +125,21 @@ export const useUserStore = defineStore('user', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('fetchUser - 开始获取用户信息')
|
||||
console.log('fetchUser - 当前token:', this.token ? this.token.substring(0, 50) + '...' : 'null')
|
||||
console.log('fetchUser - localStorage中的token:', localStorage.getItem('token')?.substring(0, 50) + '...')
|
||||
|
||||
const response = await axios.get('/api/users/me')
|
||||
console.log('fetchUser - 获取用户信息成功:', response.data)
|
||||
this.user = response.data
|
||||
|
||||
// 保存用户信息到本地存储
|
||||
localStorage.setItem('user', JSON.stringify(response.data))
|
||||
console.log('fetchUser - 用户信息已保存到localStorage')
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error('fetchUser - 获取用户信息失败:', error)
|
||||
this.error = error.response?.data?.detail || '获取用户信息失败'
|
||||
return false
|
||||
} finally {
|
||||
|
||||
@@ -38,6 +38,18 @@
|
||||
</template>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/config">
|
||||
<template #icon>
|
||||
<el-icon><setting /></el-icon>
|
||||
</template>
|
||||
<span>配置管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/comparison">
|
||||
<template #icon>
|
||||
<el-icon><trend-charts /></el-icon>
|
||||
</template>
|
||||
<span>算法效果比较</span>
|
||||
</el-menu-item>
|
||||
|
||||
</el-menu>
|
||||
</aside>
|
||||
@@ -57,7 +69,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { DataAnalysis, User, Link, Cpu } from '@element-plus/icons-vue'
|
||||
import { DataAnalysis, User, Link, Cpu, Setting, TrendCharts } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取路由和路由器
|
||||
const route = useRoute()
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
<template>
|
||||
<div class="algorithms-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>算法列表</h1>
|
||||
<h1>算法能力展示</h1>
|
||||
<p class="subtitle">探索我们强大的AI算法能力,支持多种应用场景</p>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" class="filter-form">
|
||||
<el-form-item label="算法类型">
|
||||
<el-select v-model="selectedType" placeholder="选择算法类型" @change="handleTypeChange">
|
||||
<el-form-item label="技术分类">
|
||||
<el-select v-model="selectedTechCategory" placeholder="选择技术分类" @change="handleTypeChange">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="边缘计算" value="edge_computing" />
|
||||
<el-option label="医疗算法" value="medical" />
|
||||
<el-option label="计算机视觉" value="computer_vision" />
|
||||
<el-option label="视频处理" value="video_processing" />
|
||||
<el-option label="自然语言处理" value="nlp" />
|
||||
<el-option label="机器学习" value="ml" />
|
||||
<el-option label="强化学习" value="reinforcement_learning" />
|
||||
<el-option label="边缘计算" value="edge_computing" />
|
||||
<el-option label="医疗算法" value="medical" />
|
||||
<el-option label="自动驾驶算法" value="autonomous_driving" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="输出类型">
|
||||
<el-select v-model="selectedOutputType" placeholder="选择输出类型" @change="handleTypeChange">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="图片" value="image" />
|
||||
<el-option label="视频" value="video" />
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="JSON" value="json" />
|
||||
<el-option label="音频" value="audio" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="搜索">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
@@ -46,7 +57,10 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>{{ algorithm.name }}</h2>
|
||||
<span class="algorithm-type">{{ getAlgorithmTypeName(algorithm.type) }}</span>
|
||||
<div class="tags">
|
||||
<el-tag type="primary" size="small">{{ getTechCategoryName(algorithm.tech_category) }}</el-tag>
|
||||
<el-tag type="success" size="small">{{ getOutputTypeName(algorithm.output_type) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,8 +83,9 @@
|
||||
<el-button type="primary" size="small">查看详情</el-button>
|
||||
</router-link>
|
||||
<router-link :to="`/algorithm/${algorithm.id}/call`" class="call-btn">
|
||||
<el-button type="success" size="small">立即调用</el-button>
|
||||
<el-button type="success" size="small">在线演示</el-button>
|
||||
</router-link>
|
||||
<el-button type="info" size="small" @click="showApiDocs(algorithm)">API文档</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -87,46 +102,115 @@
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- API文档对话框 -->
|
||||
<el-dialog
|
||||
v-model="showApiDialog"
|
||||
title="API文档"
|
||||
width="60%"
|
||||
>
|
||||
<div v-if="selectedAlgorithm" class="api-docs">
|
||||
<h3>{{ selectedAlgorithm.name }}</h3>
|
||||
<p>{{ selectedAlgorithm.description }}</p>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>API端点</h4>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="基础URL">
|
||||
http://0.0.0.0:8001/api/v1
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="算法ID">
|
||||
{{ selectedAlgorithm.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="技术分类">
|
||||
{{ getTechCategoryName(selectedAlgorithm.tech_category) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="输出类型">
|
||||
{{ getOutputTypeName(selectedAlgorithm.output_type) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>调用示例</h4>
|
||||
<pre class="code-example">
|
||||
POST /api/v1/algorithms/{{ selectedAlgorithm.id }}/call
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"input_data": {
|
||||
"data": "your input data"
|
||||
},
|
||||
"params": {}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
import { Search, Time, Folder, Document } from '@element-plus/icons-vue'
|
||||
import { Search, Timer, Folder, Document } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取算法存储
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 筛选和分页
|
||||
const selectedType = ref('')
|
||||
const selectedTechCategory = ref('')
|
||||
const selectedOutputType = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 算法类型映射
|
||||
const algorithmTypes = {
|
||||
// API文档对话框
|
||||
const showApiDialog = ref(false)
|
||||
const selectedAlgorithm = ref<any>(null)
|
||||
|
||||
// 技术分类映射
|
||||
const techCategories = {
|
||||
computer_vision: '计算机视觉',
|
||||
video_processing: '视频处理',
|
||||
nlp: '自然语言处理',
|
||||
ml: '机器学习',
|
||||
reinforcement_learning: '强化学习',
|
||||
edge_computing: '边缘计算',
|
||||
medical: '医疗算法',
|
||||
autonomous_driving: '自动驾驶算法'
|
||||
}
|
||||
|
||||
// 获取算法类型的中文名称
|
||||
const getAlgorithmTypeName = (type: string) => {
|
||||
return algorithmTypes[type as keyof typeof algorithmTypes] || type
|
||||
// 输出类型映射
|
||||
const outputTypes = {
|
||||
image: '图片',
|
||||
video: '视频',
|
||||
text: '文本',
|
||||
json: 'JSON',
|
||||
audio: '音频'
|
||||
}
|
||||
|
||||
// 获取技术分类的中文名称
|
||||
const getTechCategoryName = (type: string) => {
|
||||
return techCategories[type as keyof typeof techCategories] || type
|
||||
}
|
||||
|
||||
// 获取输出类型的中文名称
|
||||
const getOutputTypeName = (type: string) => {
|
||||
return outputTypes[type as keyof typeof outputTypes] || type
|
||||
}
|
||||
|
||||
// 筛选算法
|
||||
const filteredAlgorithms = computed(() => {
|
||||
let algorithms = algorithmStore.algorithms
|
||||
|
||||
// 按类型筛选
|
||||
if (selectedType.value) {
|
||||
algorithms = algorithms.filter(algorithm => algorithm.type === selectedType.value)
|
||||
// 按技术分类筛选
|
||||
if (selectedTechCategory.value) {
|
||||
algorithms = algorithms.filter(algorithm => algorithm.tech_category === selectedTechCategory.value)
|
||||
}
|
||||
|
||||
// 按输出类型筛选
|
||||
if (selectedOutputType.value) {
|
||||
algorithms = algorithms.filter(algorithm => algorithm.output_type === selectedOutputType.value)
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
@@ -159,6 +243,12 @@ const handlePageChange = (page: number) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 显示API文档
|
||||
const showApiDocs = (algorithm: any) => {
|
||||
selectedAlgorithm.value = algorithm
|
||||
showApiDialog.value = true
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
@@ -175,12 +265,21 @@ onMounted(async () => {
|
||||
.algorithms-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.algorithms-container h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
@@ -195,6 +294,20 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-form .el-form-item {
|
||||
margin-bottom: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-form .el-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.filter-form .el-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.algorithms-list {
|
||||
@@ -215,31 +328,39 @@ onMounted(async () => {
|
||||
.algorithm-card {
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.algorithm-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.algorithm-type {
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tags .el-tag {
|
||||
padding: 4px 12px;
|
||||
background-color: #ecf5ff;
|
||||
color: #409EFF;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -248,8 +369,9 @@ onMounted(async () => {
|
||||
|
||||
.algorithm-description {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.algorithm-meta {
|
||||
@@ -258,6 +380,7 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
@@ -270,6 +393,11 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
@@ -278,6 +406,30 @@ onMounted(async () => {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.api-docs h3 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.api-docs h4 {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.api-docs p {
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background-color: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
@@ -293,8 +445,14 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-actions a {
|
||||
.card-actions a,
|
||||
.card-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,25 +59,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<section class="tech-stack-section">
|
||||
<h2>技术栈</h2>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-item">
|
||||
<h3>前端</h3>
|
||||
<p>Vue 3 + TypeScript + Vite + Pinia + Element Plus</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h3>后端</h3>
|
||||
<p>FastAPI + SQLAlchemy + PostgreSQL + Redis + MinIO</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h3>部署</h3>
|
||||
<p>Docker + Docker Compose</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -96,15 +77,15 @@ const popularAlgorithms = ref([
|
||||
},
|
||||
{
|
||||
id: 'algorithm-002',
|
||||
name: '文本情感分析算法',
|
||||
type: 'nlp',
|
||||
description: '分析文本的情感倾向,支持积极、消极、中性三种情感分类'
|
||||
name: '目标识别',
|
||||
type: 'computer_vision',
|
||||
description: '识别图像中的物体位置和类别,支持人脸、车辆、物品等多种目标检测'
|
||||
},
|
||||
{
|
||||
id: 'algorithm-003',
|
||||
name: '推荐算法',
|
||||
type: 'ml',
|
||||
description: '基于用户行为的推荐算法,提供个性化推荐结果'
|
||||
name: '视频分析',
|
||||
type: 'video_processing',
|
||||
description: '分析视频内容,提取关键帧、识别动作、追踪物体等'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -341,43 +322,6 @@ const popularAlgorithms = ref([
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
|
||||
/* 技术栈区域 */
|
||||
.tech-stack-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tech-stack-section h2 {
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
margin-bottom: 40px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.tech-item {
|
||||
background-color: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tech-item h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tech-item p {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.hero-content h1 {
|
||||
@@ -395,8 +339,7 @@ const popularAlgorithms = ref([
|
||||
}
|
||||
|
||||
.features-grid,
|
||||
.algorithms-grid,
|
||||
.tech-grid {
|
||||
.algorithms-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
619
frontend/src/views/admin/AdminAlgorithmComparisonView.vue
Normal file
619
frontend/src/views/admin/AdminAlgorithmComparisonView.vue
Normal file
@@ -0,0 +1,619 @@
|
||||
<template>
|
||||
<div class="algorithm-comparison-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>算法效果比较</h1>
|
||||
|
||||
<!-- 比较配置区域 -->
|
||||
<el-card class="comparison-config-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>比较配置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 输入数据配置 -->
|
||||
<el-form :model="comparisonForm" label-width="120px">
|
||||
<el-form-item label="输入数据">
|
||||
<el-input
|
||||
v-model="comparisonForm.input_data"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入测试数据(JSON格式)"
|
||||
/>
|
||||
<div class="hint">
|
||||
提示:输入JSON格式的测试数据,例如:{"text": "这是一段测试文本"}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 算法配置列表 -->
|
||||
<el-form-item label="选择算法">
|
||||
<div class="algorithm-configs">
|
||||
<div
|
||||
v-for="(config, index) in comparisonForm.algorithm_configs"
|
||||
:key="index"
|
||||
class="algorithm-config-item"
|
||||
>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="config-header">
|
||||
<span>算法 {{ index + 1 }}</span>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeAlgorithmConfig(index)"
|
||||
:disabled="comparisonForm.algorithm_configs.length <= 2"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="config" label-width="100px">
|
||||
<el-form-item label="算法ID">
|
||||
<el-input
|
||||
v-model="config.algorithm_id"
|
||||
placeholder="请输入算法ID"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="算法名称">
|
||||
<el-input
|
||||
v-model="config.algorithm_name"
|
||||
placeholder="请输入算法名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本">
|
||||
<el-input
|
||||
v-model="config.version"
|
||||
placeholder="请输入版本号"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置参数">
|
||||
<el-input
|
||||
v-model="config.config"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置参数(JSON格式)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="addAlgorithmConfig"
|
||||
class="add-algorithm-btn"
|
||||
>
|
||||
<el-icon><plus /></el-icon>
|
||||
添加算法
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" @click="compareAlgorithms" :loading="comparing">
|
||||
<el-icon><loading v-if="comparing" /></el-icon>
|
||||
开始比较
|
||||
</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 比较结果区域 -->
|
||||
<el-card v-if="comparisonResult" class="comparison-result-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>比较结果</span>
|
||||
<el-button type="success" @click="generateReport">
|
||||
<el-icon><document /></el-icon>
|
||||
生成报告
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 结果概览 -->
|
||||
<div class="result-overview">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="比较时间">
|
||||
{{ formatDate(comparisonResult.comparison_time) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="算法数量">
|
||||
{{ comparisonResult.results?.length || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="comparisonResult.success ? 'success' : 'danger'">
|
||||
{{ comparisonResult.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 详细结果 -->
|
||||
<div class="detailed-results">
|
||||
<h3>详细结果</h3>
|
||||
<el-table :data="comparisonResult.results" style="width: 100%">
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="150" />
|
||||
<el-table-column prop="algorithm_id" label="算法ID" width="200" />
|
||||
<el-table-column prop="version" label="版本" width="120" />
|
||||
<el-table-column prop="execution_time" label="执行时间(ms)" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.execution_time?.toFixed(2) || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="success" label="执行状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.success ? 'success' : 'danger'">
|
||||
{{ scope.row.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="output" label="输出结果" min-width="200">
|
||||
<template #default="scope">
|
||||
<div class="output-cell">
|
||||
{{ formatOutput(scope.row.output) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="error" label="错误信息" width="200">
|
||||
<template #default="scope">
|
||||
<span class="error-text">{{ scope.row.error || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 性能对比图表 -->
|
||||
<div v-if="comparisonResult.results && comparisonResult.results.length > 0" class="performance-chart">
|
||||
<h3>性能对比</h3>
|
||||
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 报告展示区域 -->
|
||||
<el-card v-if="comparisonReport" class="comparison-report-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>比较报告</span>
|
||||
<el-button @click="downloadReport">
|
||||
<el-icon><download /></el-icon>
|
||||
下载报告
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="report-content">
|
||||
<div class="report-header">
|
||||
<h2>算法效果比较报告</h2>
|
||||
<p>生成时间:{{ formatDate(comparisonReport.report_time) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-section">
|
||||
<h3>执行摘要</h3>
|
||||
<p>{{ comparisonReport.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-section">
|
||||
<h3>性能分析</h3>
|
||||
<div v-html="comparisonReport.performance_analysis"></div>
|
||||
</div>
|
||||
|
||||
<div class="report-section">
|
||||
<h3>建议</h3>
|
||||
<div v-html="comparisonReport.recommendations"></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { Plus, Loading, Document, Download } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
import { comparisonService, type ComparisonResult, type ComparisonReport, type AlgorithmConfig } from '../../services/admin'
|
||||
|
||||
// 状态管理
|
||||
const comparing = ref(false)
|
||||
const comparisonResult = ref<any>(null)
|
||||
const comparisonReport = ref<any>(null)
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 比较表单
|
||||
const comparisonForm = ref({
|
||||
input_data: '{"text": "这是一段测试文本"}',
|
||||
algorithm_configs: <AlgorithmConfig[]>[
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法1',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
},
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法2',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 添加算法配置
|
||||
const addAlgorithmConfig = () => {
|
||||
comparisonForm.value.algorithm_configs.push({
|
||||
algorithm_id: '',
|
||||
algorithm_name: `算法${comparisonForm.value.algorithm_configs.length + 1}`,
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除算法配置
|
||||
const removeAlgorithmConfig = (index: number) => {
|
||||
if (comparisonForm.value.algorithm_configs.length > 2) {
|
||||
comparisonForm.value.algorithm_configs.splice(index, 1)
|
||||
} else {
|
||||
ElMessage.warning('至少需要保留两个算法进行比较')
|
||||
}
|
||||
}
|
||||
|
||||
// 比较算法
|
||||
const compareAlgorithms = async () => {
|
||||
try {
|
||||
// 验证输入数据
|
||||
let inputData
|
||||
try {
|
||||
inputData = JSON.parse(comparisonForm.value.input_data)
|
||||
} catch (error) {
|
||||
ElMessage.error('输入数据格式错误,请输入有效的JSON格式')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证算法配置
|
||||
const validConfigs = comparisonForm.value.algorithm_configs.filter(config => {
|
||||
return config.algorithm_id && config.algorithm_name
|
||||
})
|
||||
|
||||
if (validConfigs.length < 2) {
|
||||
ElMessage.error('至少需要配置两个有效的算法进行比较')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证配置参数
|
||||
validConfigs.forEach(config => {
|
||||
try {
|
||||
if (config.config) {
|
||||
JSON.parse(config.config)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(`算法 ${config.algorithm_name} 的配置参数格式错误`)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
comparing.value = true
|
||||
comparisonResult.value = null
|
||||
comparisonReport.value = null
|
||||
|
||||
comparisonResult.value = await comparisonService.compareAlgorithms(inputData, validConfigs)
|
||||
|
||||
if (comparisonResult.value.success) {
|
||||
ElMessage.success('算法比较完成')
|
||||
// 渲染图表
|
||||
nextTick(() => {
|
||||
renderChart()
|
||||
})
|
||||
} else {
|
||||
ElMessage.error('算法比较失败:' + (comparisonResult.value.error || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('算法比较失败:', error)
|
||||
ElMessage.error('算法比较失败')
|
||||
} finally {
|
||||
comparing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成报告
|
||||
const generateReport = async () => {
|
||||
try {
|
||||
comparisonReport.value = await comparisonService.generateComparisonReport(comparisonResult.value)
|
||||
|
||||
if (comparisonReport.value.success) {
|
||||
ElMessage.success('报告生成成功')
|
||||
} else {
|
||||
ElMessage.error('报告生成失败:' + (comparisonReport.value.error || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成报告失败:', error)
|
||||
ElMessage.error('生成报告失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value || !comparisonResult.value.results) return
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
|
||||
const results = comparisonResult.value.results
|
||||
const algorithmNames = results.map((r: any) => r.algorithm_name)
|
||||
const executionTimes = results.map((r: any) => r.execution_time || 0)
|
||||
const successRates = results.map((r: any) => r.success ? 100 : 0)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '算法性能对比'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['执行时间(ms)', '成功率(%)']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: algorithmNames
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '执行时间(ms)',
|
||||
position: 'left'
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '成功率(%)',
|
||||
position: 'right',
|
||||
max: 100
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '执行时间(ms)',
|
||||
type: 'bar',
|
||||
data: executionTimes,
|
||||
yAxisIndex: 0
|
||||
},
|
||||
{
|
||||
name: '成功率(%)',
|
||||
type: 'line',
|
||||
data: successRates,
|
||||
yAxisIndex: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 下载报告
|
||||
const downloadReport = () => {
|
||||
if (!comparisonReport.value) return
|
||||
|
||||
const reportContent = `
|
||||
算法效果比较报告
|
||||
生成时间:${formatDate(comparisonReport.value.report_time)}
|
||||
|
||||
执行摘要:
|
||||
${comparisonReport.value.summary}
|
||||
|
||||
性能分析:
|
||||
${comparisonReport.value.performance_analysis}
|
||||
|
||||
建议:
|
||||
${comparisonReport.value.recommendations}
|
||||
`
|
||||
|
||||
const blob = new Blob([reportContent], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `算法比较报告_${new Date().getTime()}.txt`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('报告下载成功')
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
comparisonForm.value = {
|
||||
input_data: '{"text": "这是一段测试文本"}',
|
||||
algorithm_configs: <AlgorithmConfig[]>[
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法1',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
},
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法2',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
}
|
||||
]
|
||||
}
|
||||
comparisonResult.value = null
|
||||
comparisonReport.value = null
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 格式化输出
|
||||
const formatOutput = (output: any) => {
|
||||
if (typeof output === 'object') {
|
||||
return JSON.stringify(output).substring(0, 100) + (JSON.stringify(output).length > 100 ? '...' : '')
|
||||
}
|
||||
return String(output).substring(0, 100) + (String(output).length > 100 ? '...' : '')
|
||||
}
|
||||
|
||||
// 响应式图表
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithm-comparison-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.algorithm-comparison-container h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comparison-config-card,
|
||||
.comparison-result-card,
|
||||
.comparison-report-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.algorithm-configs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.algorithm-config-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-algorithm-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-overview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detailed-results {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detailed-results h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.output-cell {
|
||||
max-height: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.performance-chart {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.performance-chart h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.report-header h2 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.report-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.report-section h3 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 2px solid #409eff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.algorithm-configs {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,9 +13,9 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
注册新服务
|
||||
</el-button>
|
||||
<el-button type="info" @click="showServiceDetailDialog = false">
|
||||
<el-icon><View /></el-icon>
|
||||
查看服务详情
|
||||
<el-button type="info" @click="showHelpDialog = true">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
帮助说明
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -66,9 +66,11 @@
|
||||
<el-table-column prop="host" label="主机" width="150" />
|
||||
<el-table-column prop="port" label="端口" width="80" />
|
||||
<el-table-column prop="api_url" label="API地址" />
|
||||
<el-table-column prop="last_heartbeat" label="最后心跳" width="180">
|
||||
<el-table-column prop="health_check" label="健康检查" width="200">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.last_heartbeat) }}
|
||||
<el-link :href="scope.row.api_url + '/health'" target="_blank" type="primary">
|
||||
{{ scope.row.api_url }}/health
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
@@ -97,8 +99,17 @@
|
||||
<el-descriptions-item label="主机">{{ selectedService.host }}</el-descriptions-item>
|
||||
<el-descriptions-item label="端口">{{ selectedService.port }}</el-descriptions-item>
|
||||
<el-descriptions-item label="API地址">{{ selectedService.api_url }}</el-descriptions-item>
|
||||
<el-descriptions-item label="健康检查">
|
||||
<el-link :href="selectedService.api_url + '/health'" target="_blank" type="primary">
|
||||
{{ selectedService.api_url }}/health
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务信息">
|
||||
<el-link :href="selectedService.api_url + '/info'" target="_blank" type="primary">
|
||||
{{ selectedService.api_url }}/info
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="启动时间">{{ formatDate(selectedService.start_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后心跳">{{ formatDate(selectedService.last_heartbeat) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务描述" :span="2">{{ selectedService.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
@@ -131,14 +142,169 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 帮助说明对话框 -->
|
||||
<el-dialog
|
||||
v-model="showHelpDialog"
|
||||
title="算法服务管理说明"
|
||||
width="70%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="help-dialog-content">
|
||||
<el-collapse v-model="activeHelpSections">
|
||||
<el-collapse-item title="什么是算法服务管理?" name="what">
|
||||
<div class="help-section">
|
||||
<p><strong>算法服务管理</strong>是智能算法展示平台的核心功能,用于管理从算法仓库部署的算法服务。</p>
|
||||
<p>它将您的算法代码转换为可调用的API服务,让算法能够通过HTTP接口被其他应用调用。</p>
|
||||
<el-divider />
|
||||
<h4>主要功能:</h4>
|
||||
<ul>
|
||||
<li><strong>服务注册</strong>:将算法仓库中的算法部署为独立的服务</li>
|
||||
<li><strong>服务监控</strong>:实时监控服务的运行状态和健康情况</li>
|
||||
<li><strong>服务控制</strong>:启动、停止、重启已部署的服务</li>
|
||||
<li><strong>服务调用</strong>:通过统一接口调用算法服务</li>
|
||||
<li><strong>日志查看</strong>:查看服务运行日志,便于问题排查</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="为什么需要服务管理?" name="why">
|
||||
<div class="help-section">
|
||||
<h4>解决的问题:</h4>
|
||||
<ul>
|
||||
<li><strong>算法隔离</strong>:每个算法运行在独立的环境中,避免相互干扰</li>
|
||||
<li><strong>资源管理</strong>:统一管理CPU、内存等资源分配</li>
|
||||
<li><strong>高可用性</strong>:支持服务的自动重启和故障恢复</li>
|
||||
<li><strong>负载均衡</strong>:支持多实例部署,提高并发处理能力</li>
|
||||
<li><strong>版本管理</strong>:同时运行不同版本的算法,便于A/B测试</li>
|
||||
</ul>
|
||||
<el-divider />
|
||||
<h4>应用场景:</h4>
|
||||
<ul>
|
||||
<li><strong>算法展示</strong>:为客户演示算法效果和能力</li>
|
||||
<li><strong>API服务</strong>:为其他应用提供算法调用接口</li>
|
||||
<li><strong>性能测试</strong>:对比不同算法的效果和性能</li>
|
||||
<li><strong>生产部署</strong>:将算法部署到生产环境提供服务</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何使用服务管理?" name="how">
|
||||
<div class="help-section">
|
||||
<h4>使用流程:</h4>
|
||||
<el-steps :active="currentStep" finish-status="success" align-center>
|
||||
<el-step title="上传算法代码" description="将算法代码上传到Gitea仓库" />
|
||||
<el-step title="注册服务" description="从仓库选择算法并注册为服务" />
|
||||
<el-step title="部署服务" description="系统自动部署服务并启动" />
|
||||
<el-step title="调用服务" description="通过API或前端页面调用算法" />
|
||||
</el-steps>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>详细步骤:</h4>
|
||||
<ol>
|
||||
<li><strong>上传算法代码</strong>:在"算法仓库管理"中上传您的算法项目代码</li>
|
||||
<li><strong>注册服务</strong>:点击"注册新服务"按钮,选择要部署的算法</li>
|
||||
<li><strong>配置服务</strong>:设置服务名称、端口、环境变量等配置</li>
|
||||
<li><strong>启动服务</strong>:系统会自动部署并启动服务</li>
|
||||
<li><strong>测试调用</strong>:使用服务调用功能测试算法是否正常工作</li>
|
||||
<li><strong>监控服务</strong>:查看服务状态、日志和性能指标</li>
|
||||
</ol>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="服务状态说明" name="status">
|
||||
<div class="help-section">
|
||||
<h4>服务状态类型:</h4>
|
||||
<el-table :data="statusDescriptions" style="width: 100%">
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" />
|
||||
<el-table-column prop="action" label="建议操作" width="150" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="常见问题" name="faq">
|
||||
<div class="help-section">
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="服务无法启动怎么办?">
|
||||
<p>检查以下几点:</p>
|
||||
<ul>
|
||||
<li>算法代码是否有语法错误</li>
|
||||
<li>依赖包是否正确安装</li>
|
||||
<li>端口是否被占用</li>
|
||||
<li>查看服务日志获取详细错误信息</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何调用已部署的服务?">
|
||||
<p>有三种调用方式:</p>
|
||||
<ul>
|
||||
<li><strong>前端调用</strong>:在算法调用页面选择服务进行调用</li>
|
||||
<li><strong>API调用</strong>:使用POST /api/v1/services/call接口调用</li>
|
||||
<li><strong>网关调用</strong>:通过API网关统一调用所有算法</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何验证服务是否正常运行?">
|
||||
<p>可以通过以下方式验证服务状态:</p>
|
||||
<ul>
|
||||
<li><strong>健康检查</strong>:访问服务的 /health 端点</li>
|
||||
<li><strong>服务信息</strong>:访问服务的 /info 端点</li>
|
||||
<li><strong>直接访问</strong>:在浏览器中访问服务的API地址</li>
|
||||
</ul>
|
||||
<p>例如,如果服务的API地址是 http://0.0.0.0:8005,可以访问:</p>
|
||||
<ul>
|
||||
<li>http://0.0.0.0:8005/health - 健康检查端点</li>
|
||||
<li>http://0.0.0.0:8005/info - 服务信息端点</li>
|
||||
</ul>
|
||||
<p>正常情况下,/health 端点会返回 {"status": "healthy", "service": "服务名"}</p>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="服务性能如何优化?">
|
||||
<p>优化建议:</p>
|
||||
<ul>
|
||||
<li>调整服务配置中的超时时间</li>
|
||||
<li>增加服务的并发处理能力</li>
|
||||
<li>使用更高效的算法实现</li>
|
||||
<li>考虑使用GPU加速</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何查看服务日志?">
|
||||
<p>点击服务列表中的"详情"按钮,在弹出的对话框中可以查看:</p>
|
||||
<ul>
|
||||
<li>服务基本信息</li>
|
||||
<li>服务配置详情</li>
|
||||
<li>实时运行日志</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showHelpDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Refresh, View, Plus } from '@element-plus/icons-vue'
|
||||
import { Refresh, View, Plus, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { serviceManagementApi } from '../../services/serviceManagement'
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
@@ -146,8 +312,42 @@ const router = useRouter()
|
||||
// 状态管理
|
||||
const services = ref<any[]>([])
|
||||
const showServiceDetailDialog = ref(false)
|
||||
const showHelpDialog = ref(false)
|
||||
const selectedService = ref<any>(null)
|
||||
|
||||
// 帮助说明相关状态
|
||||
const activeHelpSections = ref<string[]>(['what'])
|
||||
const currentStep = ref(0)
|
||||
|
||||
// 服务状态说明数据
|
||||
const statusDescriptions = ref([
|
||||
{
|
||||
status: 'running',
|
||||
description: '服务正在正常运行,可以接收和处理请求',
|
||||
action: '可以调用服务'
|
||||
},
|
||||
{
|
||||
status: 'stopped',
|
||||
description: '服务已停止,无法接收请求',
|
||||
action: '点击启动按钮启动服务'
|
||||
},
|
||||
{
|
||||
status: 'error',
|
||||
description: '服务运行出错,需要检查日志排查问题',
|
||||
action: '查看日志并修复问题后重启'
|
||||
},
|
||||
{
|
||||
status: 'starting',
|
||||
description: '服务正在启动中,请稍候',
|
||||
action: '等待启动完成'
|
||||
},
|
||||
{
|
||||
status: 'stopping',
|
||||
description: '服务正在停止中,请稍候',
|
||||
action: '等待停止完成'
|
||||
}
|
||||
])
|
||||
|
||||
// 计算服务统计信息
|
||||
const totalServices = computed(() => services.value.length)
|
||||
const runningServices = computed(() => services.value.filter(s => s.status === 'running').length)
|
||||
@@ -178,30 +378,10 @@ const formatDate = (dateString: string) => {
|
||||
// 加载服务列表
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
// 从本地存储获取token
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
ElMessage.error('未登录,请重新登录')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用后端API获取服务列表
|
||||
const response = await fetch('http://0.0.0.0:8001/api/v1/services', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取服务列表失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
const result = await serviceManagementApi.getServices()
|
||||
if (result.success) {
|
||||
// 处理服务数据,添加缺失的字段
|
||||
services.value = data.services.map((service: any) => ({
|
||||
services.value = result.services.map((service: any) => ({
|
||||
...service,
|
||||
last_heartbeat: service.last_heartbeat || null,
|
||||
start_time: service.start_time || null,
|
||||
@@ -211,7 +391,7 @@ const loadServices = async () => {
|
||||
}))
|
||||
console.log('服务列表加载完成', services.value)
|
||||
} else {
|
||||
throw new Error(data.message || '获取服务列表失败')
|
||||
throw new Error(result.message || '获取服务列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载服务列表失败:', error)
|
||||
@@ -229,14 +409,15 @@ const refreshServices = async () => {
|
||||
const startService = async (service: any) => {
|
||||
try {
|
||||
console.log('启动服务:', service.name)
|
||||
// 这里应该调用后端API启动服务
|
||||
|
||||
// 模拟启动服务
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const result = await serviceManagementApi.startService(service.service_id)
|
||||
|
||||
// 更新服务状态
|
||||
service.status = 'running'
|
||||
ElMessage.success('服务启动成功')
|
||||
if (result.success) {
|
||||
ElMessage.success('服务启动成功')
|
||||
await refreshServices()
|
||||
} else {
|
||||
ElMessage.error(`服务启动失败: ${result.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动服务失败:', error)
|
||||
ElMessage.error('启动服务失败')
|
||||
@@ -253,16 +434,20 @@ const stopService = async (service: any) => {
|
||||
})
|
||||
|
||||
console.log('停止服务:', service.name)
|
||||
// 这里应该调用后端API停止服务
|
||||
|
||||
// 模拟停止服务
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const result = await serviceManagementApi.stopService(service.service_id)
|
||||
|
||||
// 更新服务状态
|
||||
service.status = 'stopped'
|
||||
ElMessage.success('服务停止成功')
|
||||
if (result.success) {
|
||||
ElMessage.success('服务停止成功')
|
||||
await refreshServices()
|
||||
} else {
|
||||
ElMessage.error(`服务停止失败: ${result.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
if (error !== 'cancel') {
|
||||
console.error('停止服务失败:', error)
|
||||
ElMessage.error('停止服务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,14 +455,15 @@ const stopService = async (service: any) => {
|
||||
const restartService = async (service: any) => {
|
||||
try {
|
||||
console.log('重启服务:', service.name)
|
||||
// 这里应该调用后端API重启服务
|
||||
|
||||
// 模拟重启服务
|
||||
service.status = 'restarting'
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
service.status = 'running'
|
||||
const result = await serviceManagementApi.restartService(service.service_id)
|
||||
|
||||
ElMessage.success('服务重启成功')
|
||||
if (result.success) {
|
||||
ElMessage.success('服务重启成功')
|
||||
await refreshServices()
|
||||
} else {
|
||||
ElMessage.error(`服务重启失败: ${result.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重启服务失败:', error)
|
||||
ElMessage.error('重启服务失败')
|
||||
@@ -315,6 +501,45 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.help-section h4 {
|
||||
margin: 15px 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.help-section p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section ul,
|
||||
.help-section ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-section li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.status-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
@@ -240,6 +240,7 @@ import { ref, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Plus, Setting, ArrowDown, UploadFilled, Edit, Folder } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
// 获取路由
|
||||
const router = useRouter()
|
||||
@@ -465,8 +466,6 @@ const uploadFilesToServer = async (files: File[], algorithmId?: string) => {
|
||||
return await uploadFilesInBatches(files, algorithmId, MAX_BATCH_SIZE);
|
||||
}
|
||||
|
||||
const axios = await import('axios')
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
// 添加所有文件到 FormData
|
||||
@@ -493,9 +492,11 @@ const uploadFilesToServer = async (files: File[], algorithmId?: string) => {
|
||||
console.log('=== 发送上传请求 ===')
|
||||
console.log('Sending request to /api/gitea/repos/upload')
|
||||
|
||||
const response = await axios.default.post('/api/gitea/repos/upload', formData, {
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const response = await axios.post('/api/gitea/repos/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
// 监控上传进度
|
||||
onUploadProgress: (progressEvent) => {
|
||||
@@ -540,70 +541,74 @@ const uploadFilesToServer = async (files: File[], algorithmId?: string) => {
|
||||
|
||||
// 分批上传文件
|
||||
const uploadFilesInBatches = async (files: File[], algorithmId?: string, limit: number = 50 * 1024 * 1024) => {
|
||||
console.log('开始分批上传文件...');
|
||||
try {
|
||||
console.log('开始分批上传文件...');
|
||||
|
||||
// 判断是基于文件数量还是文件大小分批
|
||||
const MAX_FILES_PER_BATCH = 1000 // Chrome 浏览器限制为 1000 个文件
|
||||
const isFileCountBatch = files.length > MAX_FILES_PER_BATCH
|
||||
// 判断是基于文件数量还是文件大小分批
|
||||
const MAX_FILES_PER_BATCH = 1000 // Chrome 浏览器限制为 1000 个文件
|
||||
const isFileCountBatch = files.length > MAX_FILES_PER_BATCH
|
||||
|
||||
// 按批次分割文件
|
||||
const batches: File[][] = [];
|
||||
let currentBatch: File[] = [];
|
||||
let currentBatchSize = 0;
|
||||
// 按批次分割文件
|
||||
const batches: File[][] = [];
|
||||
let currentBatch: File[] = [];
|
||||
let currentBatchSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (isFileCountBatch) {
|
||||
// 基于文件数量分批 (Chrome 限制 1000)
|
||||
if (currentBatch.length >= MAX_FILES_PER_BATCH) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
for (const file of files) {
|
||||
if (isFileCountBatch) {
|
||||
// 基于文件数量分批 (Chrome 限制 1000)
|
||||
if (currentBatch.length >= MAX_FILES_PER_BATCH) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
}
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
}
|
||||
} else {
|
||||
// 基于文件大小分批
|
||||
if (currentBatchSize + file.size > limit && currentBatch.length > 0) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
currentBatchSize = file.size;
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
currentBatchSize += file.size;
|
||||
// 基于文件大小分批
|
||||
if (currentBatchSize + file.size > limit && currentBatch.length > 0) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
currentBatchSize = file.size;
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
currentBatchSize += file.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加最后一个批次
|
||||
if (currentBatch.length > 0) {
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
|
||||
console.log(`总共 ${files.length} 个文件分为 ${batches.length} 批`);
|
||||
console.log(`分批模式: ${isFileCountBatch ? '基于文件数量' : '基于文件大小'}`);
|
||||
|
||||
// 逐批上传
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
console.log(`上传第 ${i + 1} 批文件,包含 ${batches[i].length} 个文件`);
|
||||
|
||||
const batchResult = await uploadSingleBatch(batches[i], algorithmId);
|
||||
if (!batchResult) {
|
||||
console.error(`第 ${i + 1} 批上传失败`);
|
||||
return false;
|
||||
// 添加最后一个批次
|
||||
if (currentBatch.length > 0) {
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
const progressPerBatch = Math.floor(100 / batches.length);
|
||||
uploadProgress.value = Math.min(progressPerBatch * (i + 1), 90); // 最多到90%,保留10%给最后的推送
|
||||
}
|
||||
console.log(`总共 ${files.length} 个文件分为 ${batches.length} 批`);
|
||||
console.log(`分批模式: ${isFileCountBatch ? '基于文件数量' : '基于文件大小'}`);
|
||||
|
||||
console.log('所有批次上传完成');
|
||||
return true;
|
||||
// 逐批上传
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
console.log(`上传第 ${i + 1} 批文件,包含 ${batches[i].length} 个文件`);
|
||||
|
||||
const batchResult = await uploadSingleBatch(batches[i], algorithmId);
|
||||
if (!batchResult) {
|
||||
console.error(`第 ${i + 1} 批上传失败`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
const progressPerBatch = Math.floor(100 / batches.length);
|
||||
uploadProgress.value = Math.min(progressPerBatch * (i + 1), 90); // 最多到90%,保留10%给最后的推送
|
||||
}
|
||||
|
||||
console.log('所有批次上传完成');
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('分批上传失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 上传单个批次
|
||||
const uploadSingleBatch = async (batch: File[], algorithmId?: string) => {
|
||||
try {
|
||||
const axios = await import('axios');
|
||||
const formData = new FormData();
|
||||
|
||||
for (let i = 0; i < batch.length; i++) {
|
||||
@@ -617,9 +622,11 @@ const uploadSingleBatch = async (batch: File[], algorithmId?: string) => {
|
||||
|
||||
console.log(`上传批次,包含 ${batch.length} 个文件`);
|
||||
|
||||
const response = await axios.default.post('/api/gitea/repos/upload', formData, {
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const response = await axios.post('/api/gitea/repos/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 600000 // 10分钟超时
|
||||
});
|
||||
@@ -732,16 +739,32 @@ const formatDate = (dateString: string) => {
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 获取带认证的axios实例
|
||||
const getAxiosWithAuth = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return {
|
||||
axios: axios,
|
||||
token: token || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 加载仓库列表
|
||||
const loadRepos = async () => {
|
||||
// 检查用户是否已登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
console.log('用户未登录,跳过加载仓库列表')
|
||||
ElMessage.warning('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
|
||||
console.log('开始加载仓库列表...')
|
||||
console.log('Token:', token ? `${token.substring(0, 20)}...` : 'none')
|
||||
console.log('Token长度:', token?.length)
|
||||
|
||||
// 调用后端API获取仓库列表
|
||||
const response = await axios.default.get('/api/repositories')
|
||||
// 调用后端API获取仓库列表,认证头会自动添加
|
||||
const response = await axios.get('/api/repositories')
|
||||
|
||||
console.log('仓库列表API响应:', response)
|
||||
console.log('响应状态:', response.status)
|
||||
@@ -758,11 +781,20 @@ const loadRepos = async () => {
|
||||
console.error('加载仓库列表失败:success为false')
|
||||
ElMessage.error('加载仓库列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('加载仓库列表失败:', error)
|
||||
console.error('错误类型:', typeof error)
|
||||
console.error('错误详情:', error)
|
||||
ElMessage.error('加载仓库列表失败')
|
||||
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
console.error('认证失败,请重新登录')
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
// 清除无效的token
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
} else {
|
||||
ElMessage.error('加载仓库列表失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,19 +811,23 @@ const addRepo = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API添加仓库
|
||||
const response = await axios.default.post('/api/repositories', {
|
||||
const response = await axios.post('/api/repositories', {
|
||||
name: repoForm.value.name,
|
||||
description: repoForm.value.description,
|
||||
type: repoForm.value.type,
|
||||
repo_url: repoForm.value.repo_url,
|
||||
branch: repoForm.value.branch,
|
||||
local_path: repoForm.value.local_path
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (response.data.success) {
|
||||
// 显示上传进度对话框
|
||||
isUploading.value = true
|
||||
@@ -804,20 +840,28 @@ const addRepo = async () => {
|
||||
const fullRepoName = repoPrefix + repoForm.value.gitRepoName
|
||||
|
||||
// 首先在Gitea上创建仓库
|
||||
const createResponse = await axios.default.post('/api/gitea/repos/create', {
|
||||
const createResponse = await axios.post('/api/gitea/repos/create', {
|
||||
algorithm_id: fullRepoName,
|
||||
algorithm_name: repoForm.value.name,
|
||||
description: repoForm.value.description
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
console.log('仓库创建响应:', createResponse.data)
|
||||
|
||||
// 然后克隆仓库(如果需要)
|
||||
try {
|
||||
const cloneResponse = await axios.default.post('/api/gitea/repos/clone', {
|
||||
const cloneResponse = await axios.post('/api/gitea/repos/clone', {
|
||||
repo_url: repoForm.value.repo_url,
|
||||
algorithm_id: fullRepoName,
|
||||
branch: repoForm.value.branch
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
console.log('仓库克隆响应:', cloneResponse.data)
|
||||
} catch (cloneError: any) {
|
||||
@@ -839,9 +883,13 @@ const addRepo = async () => {
|
||||
}
|
||||
|
||||
// 最后推送代码到Gitea仓库
|
||||
const pushResponse = await axios.default.post('/api/gitea/repos/push', {
|
||||
const pushResponse = await axios.post('/api/gitea/repos/push', {
|
||||
algorithm_id: fullRepoName,
|
||||
message: `Initial commit for ${repoForm.value.name}`
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (pushResponse.data.success) {
|
||||
@@ -896,9 +944,14 @@ const editRepo = async (repo: any) => {
|
||||
try {
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API获取仓库详细信息
|
||||
const response = await axios.default.get(`/api/repositories/${repo.id}`)
|
||||
const response = await axios.get(`/api/repositories/${repo.id}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
const repoDetails = response.data.repository
|
||||
@@ -949,8 +1002,7 @@ const updateRepo = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
console.log('正在更新仓库:', {
|
||||
repoId: currentEditingRepo.value.id,
|
||||
@@ -960,10 +1012,14 @@ const updateRepo = async () => {
|
||||
})
|
||||
|
||||
// 调用后端API更新仓库 - 只更新名称、描述和类型
|
||||
const response = await axios.default.put(`/api/repositories/${currentEditingRepo.value.id}`, {
|
||||
const response = await axios.put(`/api/repositories/${currentEditingRepo.value.id}`, {
|
||||
name: editRepoForm.value.name,
|
||||
description: editRepoForm.value.description,
|
||||
type: editRepoForm.value.type
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -976,7 +1032,7 @@ const updateRepo = async () => {
|
||||
console.log('更新Gitea仓库信息,使用algorithm_id:', algorithmId)
|
||||
|
||||
// 调用 Gitea API 更新仓库信息 - 只更新描述,不更新名称
|
||||
const giteaResponse = await axios.default.patch('/api/gitea/repos/update', {
|
||||
const giteaResponse = await axios.patch('/api/gitea/repos/update', {
|
||||
algorithm_id: algorithmId,
|
||||
description: editRepoForm.value.description,
|
||||
private: false // 暂时默认为公开仓库,后续可以添加专门的私有仓库选项
|
||||
@@ -996,7 +1052,7 @@ const updateRepo = async () => {
|
||||
} else {
|
||||
console.log('Gitea仓库信息更新失败')
|
||||
}
|
||||
} catch (giteaError) {
|
||||
} catch (giteaError: any) {
|
||||
console.error('更新Gitea仓库信息失败:', giteaError)
|
||||
console.error('错误详情:', giteaError.response?.data || giteaError.message)
|
||||
// Gitea更新失败不影响本地更新的成功状态
|
||||
@@ -1041,9 +1097,14 @@ const deleteRepo = async (repoId: string) => {
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API删除仓库
|
||||
const response = await axios.default.delete(`/api/repositories/${repoId}`)
|
||||
const response = await axios.delete(`/api/repositories/${repoId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('仓库删除成功')
|
||||
@@ -1096,13 +1157,18 @@ const saveGiteaConfig = async () => {
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API保存配置
|
||||
const response = await axios.default.post('/api/gitea/config', {
|
||||
const response = await axios.post('/api/gitea/config', {
|
||||
server_url: giteaConfigForm.value.serverUrl,
|
||||
access_token: giteaConfigForm.value.accessToken,
|
||||
default_owner: giteaConfigForm.value.defaultOwner,
|
||||
repo_prefix: giteaConfigForm.value.repoPrefix || ''
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -1159,11 +1225,22 @@ const cancelEditGiteaConfig = () => {
|
||||
|
||||
// 加载已保存的Gitea配置
|
||||
const loadGiteaConfig = async () => {
|
||||
// 检查用户是否已登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
console.log('用户未登录,跳过加载Gitea配置')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
const { default: axios } = await import('axios')
|
||||
|
||||
const response = await axios.default.get('/api/gitea/config')
|
||||
const response = await axios.get('/api/gitea/config', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
const config = response.data
|
||||
@@ -1174,9 +1251,13 @@ const loadGiteaConfig = async () => {
|
||||
repoPrefix: config.repo_prefix || ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载Gitea配置失败:', error)
|
||||
// 配置不存在时不显示错误
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 401 || error.response?.status === 404) {
|
||||
// 配置不存在或未认证,不显示错误
|
||||
console.log('Gitea配置不存在或需要认证')
|
||||
} else {
|
||||
console.error('加载Gitea配置失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,54 @@
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="showAddDialog = true">
|
||||
<el-icon><plus /></el-icon>
|
||||
<el-icon><Plus /></el-icon>
|
||||
封装新API
|
||||
</el-button>
|
||||
<el-button @click="loadApis">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="info" @click="showHelpDialog = true">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
帮助说明
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- API统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.total_endpoints }}</div>
|
||||
<div class="stat-label">总API数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.active_endpoints }}</div>
|
||||
<div class="stat-label">激活API</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.total_calls }}</div>
|
||||
<div class="stat-label">总调用次数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.avg_response_time }}s</div>
|
||||
<div class="stat-label">平均响应时间</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="filterStatus" placeholder="API状态" clearable @change="loadApis">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="激活" value="active" />
|
||||
<el-option label="停用" value="inactive" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- API列表 -->
|
||||
@@ -16,6 +61,7 @@
|
||||
<el-table-column prop="name" label="API名称" width="200" />
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="180" />
|
||||
<el-table-column prop="version" label="版本" width="120" />
|
||||
<el-table-column prop="path" label="API路径" width="200" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
@@ -23,13 +69,16 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="call_count" label="调用次数" width="100" />
|
||||
<el-table-column prop="avg_response_time" label="平均响应时间" width="120" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="testApi(scope.row)">测试</el-button>
|
||||
<el-button size="small" @click="editApi(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteApi(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
@@ -39,15 +88,26 @@
|
||||
<!-- 封装API对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
title="封装新API"
|
||||
:title="isEditMode ? '编辑API' : '封装新API'"
|
||||
width="70%"
|
||||
>
|
||||
<el-form :model="apiForm" :rules="apiRules" ref="apiFormRef">
|
||||
<el-form-item label="API名称" prop="name">
|
||||
<el-input v-model="apiForm.name" placeholder="请输入API名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API路径" prop="path">
|
||||
<el-input v-model="apiForm.path" placeholder="请输入API路径,例如:/api/image-classification" />
|
||||
</el-form-item>
|
||||
<el-form-item label="HTTP方法" prop="method">
|
||||
<el-select v-model="apiForm.method" placeholder="选择HTTP方法">
|
||||
<el-option label="GET" value="GET" />
|
||||
<el-option label="POST" value="POST" />
|
||||
<el-option label="PUT" value="PUT" />
|
||||
<el-option label="DELETE" value="DELETE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择算法" prop="algorithm_id">
|
||||
<el-select v-model="apiForm.algorithm_id" placeholder="选择算法">
|
||||
<el-select v-model="apiForm.algorithm_id" placeholder="选择算法" @change="handleAlgorithmChange">
|
||||
<el-option
|
||||
v-for="algorithm in algorithms"
|
||||
:key="algorithm.id"
|
||||
@@ -66,8 +126,11 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="API路径" prop="path">
|
||||
<el-input v-model="apiForm.path" placeholder="请输入API路径,例如:/api/predict" />
|
||||
<el-form-item label="是否公开">
|
||||
<el-switch v-model="apiForm.is_public" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要认证">
|
||||
<el-switch v-model="apiForm.requires_auth" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
@@ -80,8 +143,172 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showAddDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="addApi">保存</el-button>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveApi">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 测试API对话框 -->
|
||||
<el-dialog
|
||||
v-model="showTestDialog"
|
||||
title="测试API"
|
||||
width="60%"
|
||||
>
|
||||
<el-form :model="testForm" ref="testFormRef">
|
||||
<el-form-item label="API信息">
|
||||
<div class="api-info">
|
||||
<p><strong>API名称:</strong>{{ testingApi?.name }}</p>
|
||||
<p><strong>API路径:</strong>{{ testingApi?.path }}</p>
|
||||
<p><strong>HTTP方法:</strong>{{ testingApi?.method }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求数据">
|
||||
<el-input
|
||||
v-model="testForm.payload"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder='请输入JSON格式的请求数据,例如:{"text":"测试数据","number":123}'
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showTestDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="executeTest" :loading="testing">测试</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 帮助说明对话框 -->
|
||||
<el-dialog
|
||||
v-model="showHelpDialog"
|
||||
title="API管理说明"
|
||||
width="70%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="help-dialog-content">
|
||||
<el-collapse v-model="activeHelpSections">
|
||||
<el-collapse-item title="什么是API管理?" name="what">
|
||||
<div class="help-section">
|
||||
<p><strong>API管理</strong>是智能算法展示平台的核心功能,用于封装和管理算法API端点。</p>
|
||||
<p>它将算法服务封装成统一的API接口,提供标准化的调用方式,支持权限控制、流量限制等功能。</p>
|
||||
<el-divider />
|
||||
<h4>主要功能:</h4>
|
||||
<ul>
|
||||
<li><strong>API封装</strong>:将算法服务封装成标准化的API端点</li>
|
||||
<li><strong>权限控制</strong>:配置API的访问权限和角色限制</li>
|
||||
<li><strong>流量限制</strong>:设置API的调用频率限制</li>
|
||||
<li><strong>API测试</strong>:在线测试API是否正常工作</li>
|
||||
<li><strong>调用统计</strong>:统计API的调用次数和成功率</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="为什么需要API管理?" name="why">
|
||||
<div class="help-section">
|
||||
<h4>解决的问题:</h4>
|
||||
<ul>
|
||||
<li><strong>接口标准化</strong>:统一API调用方式,降低集成复杂度</li>
|
||||
<li><strong>权限管理</strong>:精细控制API的访问权限</li>
|
||||
<li><strong>流量控制</strong>:防止API被滥用,保护服务稳定性</li>
|
||||
<li><strong>监控统计</strong>:实时监控API调用情况</li>
|
||||
<li><strong>文档生成</strong>:自动生成API文档</li>
|
||||
</ul>
|
||||
<el-divider />
|
||||
<h4>应用场景:</h4>
|
||||
<ul>
|
||||
<li><strong>对外开放</strong>:为外部应用提供算法API</li>
|
||||
<li><strong>内部集成</strong>:为其他系统提供算法服务</li>
|
||||
<li><strong>API市场</strong>:发布API到API市场供他人使用</li>
|
||||
<li><strong>微服务架构</strong>:构建基于API的微服务架构</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何使用API管理?" name="how">
|
||||
<div class="help-section">
|
||||
<h4>使用流程:</h4>
|
||||
<el-steps :active="currentStep" finish-status="success" align-center>
|
||||
<el-step title="选择算法" description="选择要封装的算法和版本" />
|
||||
<el-step title="配置API" description="设置API路径、权限等配置" />
|
||||
<el-step title="测试API" description="测试API是否正常工作" />
|
||||
<el-step title="发布API" description="发布API供他人使用" />
|
||||
</el-steps>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>详细步骤:</h4>
|
||||
<ol>
|
||||
<li><strong>选择算法</strong>:从算法列表中选择要封装的算法和版本</li>
|
||||
<li><strong>配置API</strong>:设置API名称、路径、HTTP方法等</li>
|
||||
<li><strong>设置权限</strong>:配置是否需要认证、允许的角色等</li>
|
||||
<li><strong>流量限制</strong>:设置API的调用频率限制</li>
|
||||
<li><strong>测试API</strong>:使用测试功能验证API是否正常</li>
|
||||
<li><strong>发布API</strong>:将API设置为公开状态供他人使用</li>
|
||||
</ol>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="API配置说明" name="config">
|
||||
<div class="help-section">
|
||||
<h4>配置项说明:</h4>
|
||||
<el-table :data="configDescriptions" style="width: 100%">
|
||||
<el-table-column prop="item" label="配置项" width="150" />
|
||||
<el-table-column prop="description" label="说明" />
|
||||
<el-table-column prop="example" label="示例" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="常见问题" name="faq">
|
||||
<div class="help-section">
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="API调用失败怎么办?">
|
||||
<p>检查以下几点:</p>
|
||||
<ul>
|
||||
<li>关联的算法服务是否正在运行</li>
|
||||
<li>API路径是否正确</li>
|
||||
<li>请求参数格式是否正确</li>
|
||||
<li>是否有足够的权限调用API</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何设置API权限?">
|
||||
<p>权限配置包括:</p>
|
||||
<ul>
|
||||
<li><strong>是否需要认证</strong>:控制API是否需要用户登录</li>
|
||||
<li><strong>允许的角色</strong>:限制只有特定角色可以调用</li>
|
||||
<li><strong>是否公开</strong>:公开API不需要认证即可访问</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何设置流量限制?">
|
||||
<p>流量限制配置:</p>
|
||||
<ul>
|
||||
<li><strong>最大请求数</strong>:在时间窗口内的最大请求数</li>
|
||||
<li><strong>时间窗口</strong>:限制的时间范围(秒)</li>
|
||||
<li>示例:{"max_requests": 100, "window": 60} 表示每分钟最多100次请求</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何查看API调用统计?">
|
||||
<p>统计信息包括:</p>
|
||||
<ul>
|
||||
<li><strong>总调用次数</strong>:API被调用的总次数</li>
|
||||
<li><strong>成功次数</strong>:调用成功的次数</li>
|
||||
<li><strong>错误次数</strong>:调用失败的次数</li>
|
||||
<li><strong>平均响应时间</strong>:API的平均响应时间</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showHelpDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -92,83 +319,146 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAlgorithmStore } from '../../stores/algorithm'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Refresh, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { apiManagementService, type ApiEndpoint } from '../../services/apiManagement'
|
||||
|
||||
// 获取路由和存储
|
||||
const router = useRouter()
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 状态管理
|
||||
const apis = ref([])
|
||||
const algorithms = ref([])
|
||||
const versions = ref([])
|
||||
const apis = ref<ApiEndpoint[]>([])
|
||||
const algorithms = ref<any[]>([])
|
||||
const versions = ref<any[]>([])
|
||||
const showAddDialog = ref(false)
|
||||
const showTestDialog = ref(false)
|
||||
const showHelpDialog = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const filterStatus = ref('')
|
||||
const apiFormRef = ref()
|
||||
const testFormRef = ref()
|
||||
const testingApi = ref<ApiEndpoint | null>(null)
|
||||
const testing = ref(false)
|
||||
|
||||
const apiForm = ref({
|
||||
name: '',
|
||||
path: '',
|
||||
method: 'POST',
|
||||
algorithm_id: '',
|
||||
version_id: '',
|
||||
path: '',
|
||||
is_public: false,
|
||||
requires_auth: true,
|
||||
description: ''
|
||||
})
|
||||
|
||||
const testForm = ref({
|
||||
payload: '{"text":"测试数据","number":123}'
|
||||
})
|
||||
|
||||
const apiStats = ref({
|
||||
total_endpoints: 0,
|
||||
active_endpoints: 0,
|
||||
total_calls: '0',
|
||||
total_success: '0',
|
||||
total_errors: '0',
|
||||
avg_response_time: '0.00'
|
||||
})
|
||||
|
||||
const configDescriptions = ref([
|
||||
{
|
||||
item: 'API名称',
|
||||
description: 'API的显示名称,用于标识和区分不同的API',
|
||||
example: '图像分类API'
|
||||
},
|
||||
{
|
||||
item: 'API路径',
|
||||
description: 'API的访问路径,客户端通过此路径调用API',
|
||||
example: '/api/image-classification'
|
||||
},
|
||||
{
|
||||
item: 'HTTP方法',
|
||||
description: 'API支持的HTTP方法',
|
||||
example: 'POST'
|
||||
},
|
||||
{
|
||||
item: '是否公开',
|
||||
description: 'API是否对外公开,公开API不需要认证即可访问',
|
||||
example: 'true'
|
||||
},
|
||||
{
|
||||
item: '是否需要认证',
|
||||
description: 'API调用是否需要用户认证',
|
||||
example: 'true'
|
||||
}
|
||||
])
|
||||
|
||||
// 帮助说明相关状态
|
||||
const activeHelpSections = ref<string[]>(['what'])
|
||||
const currentStep = ref(0)
|
||||
|
||||
// 表单验证规则
|
||||
const apiRules = ref({
|
||||
name: [
|
||||
{ required: true, message: '请输入API名称', trigger: 'blur' }
|
||||
],
|
||||
path: [
|
||||
{ required: true, message: '请输入API路径', trigger: 'blur' }
|
||||
],
|
||||
method: [
|
||||
{ required: true, message: '请选择HTTP方法', trigger: 'blur' }
|
||||
],
|
||||
algorithm_id: [
|
||||
{ required: true, message: '请选择算法', trigger: 'blur' }
|
||||
],
|
||||
version_id: [
|
||||
{ required: true, message: '请选择版本', trigger: 'blur' }
|
||||
],
|
||||
path: [
|
||||
{ required: true, message: '请输入API路径', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 加载API列表
|
||||
const loadApis = async () => {
|
||||
// 这里应该调用后端API获取API列表
|
||||
// 暂时模拟数据
|
||||
apis.value = [
|
||||
{
|
||||
id: 'api-1',
|
||||
name: '图像分类API',
|
||||
algorithm_name: 'ResNet50',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'api-2',
|
||||
name: '文本分类API',
|
||||
algorithm_name: 'BERT',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
try {
|
||||
const result = await apiManagementService.getApiEndpoints(filterStatus.value)
|
||||
apis.value = result.endpoints
|
||||
} catch (error) {
|
||||
console.error('加载API列表失败:', error)
|
||||
ElMessage.error('加载API列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载API统计
|
||||
const loadApiStats = async () => {
|
||||
try {
|
||||
const stats = await apiManagementService.getApiStats()
|
||||
apiStats.value = stats
|
||||
} catch (error) {
|
||||
console.error('加载API统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载算法列表
|
||||
const loadAlgorithms = async () => {
|
||||
await algorithmStore.fetchAlgorithms()
|
||||
algorithms.value = algorithmStore.algorithms
|
||||
try {
|
||||
await algorithmStore.fetchAlgorithms()
|
||||
algorithms.value = algorithmStore.algorithms
|
||||
} catch (error) {
|
||||
console.error('加载算法列表失败:', error)
|
||||
ElMessage.error('加载算法列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载版本列表
|
||||
const loadVersions = async (algorithmId: string) => {
|
||||
if (algorithmId) {
|
||||
const algorithm = algorithmStore.algorithms.find(a => a.id === algorithmId)
|
||||
const algorithm = algorithmStore.algorithms.find((a: any) => a.id === algorithmId)
|
||||
if (algorithm && algorithm.versions) {
|
||||
versions.value = algorithm.versions
|
||||
}
|
||||
@@ -184,51 +474,141 @@ const handleAlgorithmChange = (algorithmId: string) => {
|
||||
const addApi = async () => {
|
||||
if (!apiFormRef.value) return
|
||||
|
||||
// 验证表单
|
||||
await apiFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
// 这里应该调用后端API添加API
|
||||
// 暂时模拟添加
|
||||
console.log('添加API:', apiForm.value)
|
||||
ElMessage.success('API封装成功')
|
||||
|
||||
// 关闭对话框
|
||||
showAddDialog.value = false
|
||||
|
||||
// 重置表单
|
||||
apiForm.value = {
|
||||
name: '',
|
||||
algorithm_id: '',
|
||||
version_id: '',
|
||||
path: '',
|
||||
description: ''
|
||||
try {
|
||||
await apiManagementService.createApiEndpoint(apiForm.value)
|
||||
ElMessage.success('API封装成功')
|
||||
closeDialog()
|
||||
await loadApis()
|
||||
await loadApiStats()
|
||||
} catch (error: any) {
|
||||
console.error('添加API失败:', error)
|
||||
ElMessage.error(error.message || '添加API失败')
|
||||
}
|
||||
|
||||
// 重新加载API列表
|
||||
await loadApis()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑API
|
||||
const editApi = (api: any) => {
|
||||
// 这里应该打开编辑对话框,暂时打印
|
||||
console.log('编辑API:', api)
|
||||
const editApi = (api: ApiEndpoint) => {
|
||||
isEditMode.value = true
|
||||
apiForm.value = {
|
||||
name: api.name,
|
||||
path: api.path,
|
||||
method: api.method,
|
||||
algorithm_id: api.algorithm_id,
|
||||
version_id: api.version_id,
|
||||
is_public: api.is_public,
|
||||
requires_auth: true,
|
||||
description: api.description
|
||||
}
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
// 更新API
|
||||
const updateApi = async () => {
|
||||
if (!apiFormRef.value) return
|
||||
|
||||
await apiFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const apiId = apis.value.find((a: ApiEndpoint) => a.name === apiForm.value.name)?.id || ''
|
||||
await apiManagementService.updateApiEndpoint(apiId, apiForm.value)
|
||||
ElMessage.success('API更新成功')
|
||||
closeDialog()
|
||||
await loadApis()
|
||||
} catch (error: any) {
|
||||
console.error('更新API失败:', error)
|
||||
ElMessage.error(error.message || '更新API失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存API
|
||||
const saveApi = async () => {
|
||||
if (isEditMode.value) {
|
||||
await updateApi()
|
||||
} else {
|
||||
await addApi()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除API
|
||||
const deleteApi = (apiId: string) => {
|
||||
// 这里应该调用后端API删除API,暂时打印
|
||||
console.log('删除API:', apiId)
|
||||
ElMessage.success('API删除成功')
|
||||
// 重新加载API列表
|
||||
loadApis()
|
||||
const deleteApi = async (apiId: string) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除此API吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await apiManagementService.deleteApiEndpoint(apiId)
|
||||
ElMessage.success('API删除成功')
|
||||
await loadApis()
|
||||
await loadApiStats()
|
||||
} catch (error: any) {
|
||||
console.error('删除API失败:', error)
|
||||
ElMessage.error(error.message || '删除API失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 测试API
|
||||
const testApi = (api: ApiEndpoint) => {
|
||||
testingApi.value = api
|
||||
testForm.value.payload = '{"text":"测试数据","number":123}'
|
||||
showTestDialog.value = true
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
const executeTest = async () => {
|
||||
if (!testFormRef.value || !testingApi.value) return
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(testForm.value.payload)
|
||||
testing.value = true
|
||||
|
||||
const result = await apiManagementService.testApiEndpoint(testingApi.value.id, payload)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success(`API测试成功,响应时间:${result.response_time.toFixed(2)}秒`)
|
||||
ElMessage.info(`响应结果:${JSON.stringify(result.result)}`)
|
||||
} else {
|
||||
ElMessage.error(`API测试失败:${result.error}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('测试API失败:', error)
|
||||
ElMessage.error(error.message || '测试API失败')
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const closeDialog = () => {
|
||||
showAddDialog.value = false
|
||||
isEditMode.value = false
|
||||
apiForm.value = {
|
||||
name: '',
|
||||
path: '',
|
||||
method: 'POST',
|
||||
algorithm_id: '',
|
||||
version_id: '',
|
||||
is_public: false,
|
||||
requires_auth: true,
|
||||
description: ''
|
||||
}
|
||||
if (apiFormRef.value) {
|
||||
apiFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(async () => {
|
||||
await loadAlgorithms()
|
||||
await loadApis()
|
||||
await loadApiStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -247,7 +627,100 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.api-info {
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.api-info p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.help-section h4 {
|
||||
margin: 15px 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.help-section p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section ul,
|
||||
.help-section ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-section li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-cards {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -259,5 +732,13 @@ onMounted(async () => {
|
||||
.action-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
618
frontend/src/views/admin/AdminConfigManagementView.vue
Normal file
618
frontend/src/views/admin/AdminConfigManagementView.vue
Normal file
@@ -0,0 +1,618 @@
|
||||
<template>
|
||||
<div class="config-management-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>配置管理</h1>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="showAddDialog = true">
|
||||
<el-icon><plus /></el-icon>
|
||||
添加配置
|
||||
</el-button>
|
||||
<el-button @click="loadConfigs">刷新</el-button>
|
||||
<el-button type="info" @click="showHelpDialog = true">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
帮助说明
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="filterType" placeholder="配置类型" clearable @change="loadConfigs">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="系统配置" value="system" />
|
||||
<el-option label="服务配置" value="service" />
|
||||
<el-option label="用户配置" value="user" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 配置列表 -->
|
||||
<el-table :data="configs" style="width: 100%">
|
||||
<el-table-column prop="config_key" label="配置键" width="250" />
|
||||
<el-table-column prop="config_value" label="配置值" width="300" />
|
||||
<el-table-column prop="config_type" label="类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getConfigTypeTag(scope.row.config_type)">
|
||||
{{ getConfigTypeName(scope.row.config_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="service_id" label="服务ID" width="150" />
|
||||
<el-table-column prop="description" label="描述" width="200" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editConfig(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteConfig(scope.row.config_key)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑配置对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
:title="isEditMode ? '编辑配置' : '添加配置'"
|
||||
width="60%"
|
||||
>
|
||||
<el-form :model="configForm" :rules="configRules" ref="configFormRef">
|
||||
<el-form-item label="配置键" prop="config_key">
|
||||
<el-input
|
||||
v-model="configForm.config_key"
|
||||
placeholder="请输入配置键"
|
||||
:disabled="isEditMode"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置值" prop="config_value">
|
||||
<el-input
|
||||
v-model="configForm.config_value"
|
||||
placeholder="请输入配置值"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置类型" prop="config_type">
|
||||
<el-select v-model="configForm.config_type" placeholder="选择配置类型">
|
||||
<el-option label="系统配置" value="system" />
|
||||
<el-option label="服务配置" value="service" />
|
||||
<el-option label="用户配置" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="服务ID" prop="service_id">
|
||||
<el-input
|
||||
v-model="configForm.service_id"
|
||||
placeholder="请输入服务ID(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="configForm.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入配置描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveConfig">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 帮助说明对话框 -->
|
||||
<el-dialog
|
||||
v-model="showHelpDialog"
|
||||
title="配置管理说明"
|
||||
width="70%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="help-dialog-content">
|
||||
<el-collapse v-model="activeHelpSections">
|
||||
<el-collapse-item title="什么是配置管理?" name="what">
|
||||
<div class="help-section">
|
||||
<p><strong>配置管理</strong>是智能算法展示平台的核心功能,用于管理系统、服务和用户的各种配置项。</p>
|
||||
<p>它提供了统一的配置管理界面,支持不同层级的配置,采用三层配置架构,确保配置的灵活性和安全性。</p>
|
||||
<el-divider />
|
||||
<h4>主要功能:</h4>
|
||||
<ul>
|
||||
<li><strong>配置存储</strong>:将配置存储在数据库中,便于管理和查询</li>
|
||||
<li><strong>配置分层</strong>:支持系统、服务、用户三层配置管理</li>
|
||||
<li><strong>动态更新</strong>:配置修改后立即生效,无需重启服务</li>
|
||||
<li><strong>配置优先级</strong>:环境变量 > 数据库 > 文件默认值</li>
|
||||
<li><strong>配置历史</strong>:记录配置的创建和更新时间</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="为什么需要配置管理?" name="why">
|
||||
<div class="help-section">
|
||||
<h4>解决的问题:</h4>
|
||||
<ul>
|
||||
<li><strong>配置集中管理</strong>:避免配置分散在多个文件中</li>
|
||||
<li><strong>动态配置</strong>:无需重启服务即可修改配置</li>
|
||||
<li><strong>配置安全</strong>:敏感配置可以单独管理,不暴露在代码中</li>
|
||||
<li><strong>环境适配</strong>:不同环境使用不同配置,便于部署</li>
|
||||
<li><strong>配置版本</strong>:记录配置变更历史,便于追溯</li>
|
||||
</ul>
|
||||
<el-divider />
|
||||
<h4>应用场景:</h4>
|
||||
<ul>
|
||||
<li><strong>系统配置</strong>:数据库连接、Redis配置、API密钥等</li>
|
||||
<li><strong>服务配置</strong>:算法服务的超时时间、并发数等</li>
|
||||
<li><strong>用户配置</strong>:用户偏好、权限限制等</li>
|
||||
<li><strong>环境切换</strong>:开发、测试、生产环境配置切换</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何使用配置管理?" name="how">
|
||||
<div class="help-section">
|
||||
<h4>使用流程:</h4>
|
||||
<el-steps :active="currentStep" finish-status="success" align-center>
|
||||
<el-step title="确定配置类型" description="选择系统/服务/用户配置" />
|
||||
<el-step title="添加配置" description="填写配置键、值和描述" />
|
||||
<el-step title="应用配置" description="配置立即生效" />
|
||||
<el-step title="管理配置" description="编辑、删除或查看配置" />
|
||||
</el-steps>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>详细步骤:</h4>
|
||||
<ol>
|
||||
<li><strong>确定配置类型</strong>:根据配置的用途选择合适的类型(system/service/user)</li>
|
||||
<li><strong>添加配置</strong>:点击"添加配置"按钮,填写配置信息</li>
|
||||
<li><strong>配置键命名</strong>:使用命名空间规范,如"openai.api_config"</li>
|
||||
<li><strong>配置值格式</strong>:使用JSON格式,便于存储复杂配置</li>
|
||||
<li><strong>应用配置</strong>:配置保存后立即生效,无需重启</li>
|
||||
<li><strong>管理配置</strong>:可以随时编辑、删除或查看配置历史</li>
|
||||
</ol>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置类型说明" name="types">
|
||||
<div class="help-section">
|
||||
<h4>配置类型:</h4>
|
||||
<el-table :data="configTypeDescriptions" style="width: 100%">
|
||||
<el-table-column prop="type" label="类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getConfigTypeTag(scope.row.type)">
|
||||
{{ getConfigTypeName(scope.row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" />
|
||||
<el-table-column prop="examples" label="示例" />
|
||||
<el-table-column prop="scope" label="作用范围" width="120" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置命名规范" name="naming">
|
||||
<div class="help-section">
|
||||
<h4>命名规范:</h4>
|
||||
<ul>
|
||||
<li><strong>使用小写字母</strong>:配置键使用小写字母和点号分隔</li>
|
||||
<li><strong>使用命名空间</strong>:按功能模块组织配置</li>
|
||||
<li><strong>使用下划线</strong>:单词之间使用下划线连接</li>
|
||||
<li><strong>避免特殊字符</strong>:不要使用空格和特殊符号</li>
|
||||
</ul>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>命名示例:</h4>
|
||||
<el-table :data="namingExamples" style="width: 100%">
|
||||
<el-table-column prop="category" label="类别" width="150" />
|
||||
<el-table-column prop="example" label="示例" />
|
||||
<el-table-column prop="description" label="说明" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="常见问题" name="faq">
|
||||
<div class="help-section">
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="配置修改后多久生效?">
|
||||
<p>配置修改后<strong>立即生效</strong>,无需重启服务。系统会自动从数据库读取最新配置。</p>
|
||||
<p>注意:某些配置可能需要服务重新加载才能生效,具体取决于配置的使用方式。</p>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何备份配置?">
|
||||
<p>配置存储在数据库中,建议:</p>
|
||||
<ul>
|
||||
<li>定期备份数据库</li>
|
||||
<li>记录重要配置的变更历史</li>
|
||||
<li>使用版本控制管理配置文件</li>
|
||||
<li>在修改配置前先导出现有配置</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置值支持哪些格式?">
|
||||
<p>配置值支持<strong>JSON格式</strong>,可以存储:</p>
|
||||
<ul>
|
||||
<li>字符串:`"value"`</li>
|
||||
<li>数字:`123` 或 `123.45`</li>
|
||||
<li>布尔值:`true` 或 `false`</li>
|
||||
<li>数组:`["item1", "item2"]`</li>
|
||||
<li>对象:`{"key": "value", "num": 123}`</li>
|
||||
<li>嵌套结构:`{"config": {"nested": {"value": "test"}}}`</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何处理敏感配置?">
|
||||
<p>对于敏感配置(如API密钥、密码等),建议:</p>
|
||||
<ul>
|
||||
<li>使用环境变量存储,不要存储在数据库中</li>
|
||||
<li>在代码中使用占位符,运行时从环境变量读取</li>
|
||||
<li>定期轮换敏感配置</li>
|
||||
<li>限制敏感配置的访问权限</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置优先级是如何工作的?">
|
||||
<p>系统采用三层配置架构,优先级从高到低:</p>
|
||||
<ol>
|
||||
<li><strong>环境变量</strong>(最高优先级):适合敏感信息和部署环境配置</li>
|
||||
<li><strong>数据库配置</strong>(中等优先级):适合运行时动态配置</li>
|
||||
<li><strong>文件默认值</strong>(最低优先级):代码中的默认配置</li>
|
||||
</ol>
|
||||
<p>当多个配置源存在相同配置键时,优先级高的会覆盖优先级低的。</p>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showHelpDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Plus, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { configService, type ConfigItem } from '../../services/admin'
|
||||
|
||||
// 状态管理
|
||||
const configs = ref<ConfigItem[]>([])
|
||||
const filterType = ref('')
|
||||
const showAddDialog = ref(false)
|
||||
const showHelpDialog = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const configFormRef = ref()
|
||||
const configForm = ref({
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
config_type: 'system',
|
||||
service_id: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 帮助说明相关状态
|
||||
const activeHelpSections = ref<string[]>(['what'])
|
||||
const currentStep = ref(0)
|
||||
|
||||
// 配置类型说明数据
|
||||
const configTypeDescriptions = ref([
|
||||
{
|
||||
type: 'system',
|
||||
description: '系统级别的全局配置,影响整个平台',
|
||||
examples: '数据库连接、Redis配置、API密钥等',
|
||||
scope: '全局'
|
||||
},
|
||||
{
|
||||
type: 'service',
|
||||
description: '特定算法服务的配置,只影响单个服务',
|
||||
examples: '超时时间、并发数、模型参数等',
|
||||
scope: '服务'
|
||||
},
|
||||
{
|
||||
type: 'user',
|
||||
description: '用户级别的配置,个性化设置',
|
||||
examples: '用户偏好、权限限制、存储配额等',
|
||||
scope: '用户'
|
||||
}
|
||||
])
|
||||
|
||||
// 命名示例数据
|
||||
const namingExamples = ref([
|
||||
{
|
||||
category: '系统配置',
|
||||
example: 'database.connection_pool',
|
||||
description: '数据库连接池配置'
|
||||
},
|
||||
{
|
||||
category: '系统配置',
|
||||
example: 'redis.cache_timeout',
|
||||
description: 'Redis缓存超时时间'
|
||||
},
|
||||
{
|
||||
category: '服务配置',
|
||||
example: 'service.{service_id}.timeout',
|
||||
description: '特定服务的超时配置'
|
||||
},
|
||||
{
|
||||
category: '服务配置',
|
||||
example: 'service.{service_id}.max_concurrent',
|
||||
description: '特定服务的最大并发数'
|
||||
},
|
||||
{
|
||||
category: '用户配置',
|
||||
example: 'user.{user_id}.preferences',
|
||||
description: '用户偏好设置'
|
||||
},
|
||||
{
|
||||
category: '用户配置',
|
||||
example: 'user.{user_id}.permissions',
|
||||
description: '用户权限配置'
|
||||
}
|
||||
])
|
||||
|
||||
// 表单验证规则
|
||||
const configRules = ref({
|
||||
config_key: [
|
||||
{ required: true, message: '请输入配置键', trigger: 'blur' }
|
||||
],
|
||||
config_value: [
|
||||
{ required: true, message: '请输入配置值', trigger: 'blur' }
|
||||
],
|
||||
config_type: [
|
||||
{ required: true, message: '请选择配置类型', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 获取配置类型标签样式
|
||||
const getConfigTypeTag = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'system': 'danger',
|
||||
'service': 'warning',
|
||||
'user': 'success'
|
||||
}
|
||||
return typeMap[type] || 'info'
|
||||
}
|
||||
|
||||
// 获取配置类型名称
|
||||
const getConfigTypeName = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'system': '系统配置',
|
||||
'service': '服务配置',
|
||||
'user': '用户配置'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 加载配置列表
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
configs.value = await configService.getAllConfigs(filterType.value)
|
||||
} catch (error) {
|
||||
console.error('加载配置列表失败:', error)
|
||||
ElMessage.error('加载配置列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加配置
|
||||
const addConfig = async () => {
|
||||
try {
|
||||
const success = await configService.setConfig(configForm.value.config_key, {
|
||||
value: configForm.value.config_value,
|
||||
type: configForm.value.config_type,
|
||||
service_id: configForm.value.service_id || null,
|
||||
description: configForm.value.description
|
||||
})
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('配置添加成功')
|
||||
closeDialog()
|
||||
loadConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加配置失败:', error)
|
||||
ElMessage.error('添加配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
const editConfig = (config: any) => {
|
||||
isEditMode.value = true
|
||||
configForm.value = {
|
||||
config_key: config.config_key,
|
||||
config_value: config.config_value,
|
||||
config_type: config.config_type,
|
||||
service_id: config.service_id || '',
|
||||
description: config.description || ''
|
||||
}
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
const updateConfig = async () => {
|
||||
try {
|
||||
const success = await configService.setConfig(configForm.value.config_key, {
|
||||
value: configForm.value.config_value,
|
||||
type: configForm.value.config_type,
|
||||
service_id: configForm.value.service_id || null,
|
||||
description: configForm.value.description
|
||||
})
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('配置更新成功')
|
||||
closeDialog()
|
||||
loadConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新配置失败:', error)
|
||||
ElMessage.error('更新配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
if (!configFormRef.value) return
|
||||
|
||||
await configFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
if (isEditMode.value) {
|
||||
await updateConfig()
|
||||
} else {
|
||||
await addConfig()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const deleteConfig = (configKey: string) => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要删除此配置吗?',
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const success = await configService.deleteConfig(configKey)
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('配置删除成功')
|
||||
loadConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
ElMessage.error('删除配置失败')
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const closeDialog = () => {
|
||||
showAddDialog.value = false
|
||||
isEditMode.value = false
|
||||
configForm.value = {
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
config_type: 'system',
|
||||
service_id: '',
|
||||
description: ''
|
||||
}
|
||||
if (configFormRef.value) {
|
||||
configFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadConfigs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-management-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-management-container h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.help-section h4 {
|
||||
margin: 15px 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.help-section p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section ul,
|
||||
.help-section ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-section li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-table-column {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -104,6 +104,38 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 算法技术分类 -->
|
||||
<el-form-item label="技术分类" prop="tech_category">
|
||||
<el-select
|
||||
v-model="serviceForm.tech_category"
|
||||
placeholder="请选择技术分类"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option label="计算机视觉" value="computer_vision" />
|
||||
<el-option label="视频处理" value="video_processing" />
|
||||
<el-option label="自然语言处理" value="nlp" />
|
||||
<el-option label="机器学习" value="ml" />
|
||||
<el-option label="边缘计算" value="edge_computing" />
|
||||
<el-option label="医疗算法" value="medical" />
|
||||
<el-option label="自动驾驶算法" value="autonomous_driving" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 输出类型 -->
|
||||
<el-form-item label="输出类型" prop="output_type">
|
||||
<el-select
|
||||
v-model="serviceForm.output_type"
|
||||
placeholder="请选择输出类型"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option label="图片" value="image" />
|
||||
<el-option label="视频" value="video" />
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="JSON" value="json" />
|
||||
<el-option label="音频" value="audio" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 服务配置 -->
|
||||
<el-form-item label="服务类型" prop="service_type">
|
||||
<el-select
|
||||
@@ -292,6 +324,8 @@ const serviceForm = reactive({
|
||||
name: '',
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
tech_category: 'computer_vision',
|
||||
output_type: 'image',
|
||||
service_type: 'http',
|
||||
host: '0.0.0.0',
|
||||
port: 8000,
|
||||
@@ -333,7 +367,7 @@ const rules = {
|
||||
const showResultDialog = ref(false)
|
||||
const registrationResult = reactive({
|
||||
success: false,
|
||||
service: null,
|
||||
service: null as any,
|
||||
error: ''
|
||||
})
|
||||
|
||||
@@ -434,6 +468,9 @@ const submitForm = async () => {
|
||||
repository_id: serviceForm.repository_id,
|
||||
name: serviceForm.name,
|
||||
version: serviceForm.version,
|
||||
description: serviceForm.description,
|
||||
tech_category: serviceForm.tech_category,
|
||||
output_type: serviceForm.output_type,
|
||||
service_type: serviceForm.service_type,
|
||||
host: serviceForm.host,
|
||||
port: serviceForm.port,
|
||||
@@ -472,6 +509,8 @@ const resetForm = () => {
|
||||
}
|
||||
// 重置默认值
|
||||
serviceForm.version = '1.0.0'
|
||||
serviceForm.tech_category = 'computer_vision'
|
||||
serviceForm.output_type = 'image'
|
||||
serviceForm.service_type = 'http'
|
||||
serviceForm.host = '0.0.0.0'
|
||||
serviceForm.port = 8000
|
||||
|
||||
@@ -144,14 +144,26 @@ const fetchRoles = async () => {
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
// 检查用户是否已登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
console.log('用户未登录,跳过加载用户列表')
|
||||
error.value = '请先登录'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const response = await axios.get('/api/users/')
|
||||
users.value = response.data.users
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('获取用户列表失败:', err)
|
||||
error.value = '获取用户列表失败'
|
||||
if (err.response?.status === 401 || err.response?.status === 403) {
|
||||
error.value = '权限不足,请重新登录'
|
||||
} else {
|
||||
error.value = '获取用户列表失败'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
467
frontend/test.html
Normal file
467
frontend/test.html
Normal file
@@ -0,0 +1,467 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>系统功能测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #409eff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.test-section h2 {
|
||||
color: #409eff;
|
||||
margin-top: 0;
|
||||
}
|
||||
.test-item {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.test-item.success {
|
||||
border-left: 4px solid #67c23a;
|
||||
}
|
||||
.test-item.error {
|
||||
border-left: 4px solid #f56c6c;
|
||||
}
|
||||
.test-item.pending {
|
||||
border-left: 4px solid #e6a23c;
|
||||
}
|
||||
.status {
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.success .status {
|
||||
color: #67c23a;
|
||||
}
|
||||
.error .status {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.pending .status {
|
||||
color: #e6a23c;
|
||||
}
|
||||
button {
|
||||
background-color: #409eff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.result {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.summary {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #e6f7ff;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.summary.success {
|
||||
background-color: #f0f9ff;
|
||||
color: #67c23a;
|
||||
}
|
||||
.summary.error {
|
||||
background-color: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>系统功能自动化测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>测试控制</h2>
|
||||
<button id="runAllTests" onclick="runAllTests()">运行所有测试</button>
|
||||
<button id="resetTests" onclick="resetTests()">重置测试</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>登录测试</h2>
|
||||
<div class="test-item pending" id="loginTest">
|
||||
<span>测试用户登录</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="loginResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>配置管理API测试</h2>
|
||||
<div class="test-item pending" id="getConfigsTest">
|
||||
<span>获取所有配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getConfigsResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="addConfigTest">
|
||||
<span>添加测试配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="addConfigResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getConfigTest">
|
||||
<span>获取单个配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getConfigResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="deleteConfigTest">
|
||||
<span>删除测试配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="deleteConfigResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>算法比较API测试</h2>
|
||||
<div class="test-item pending" id="compareAlgorithmsTest">
|
||||
<span>算法效果比较</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="compareAlgorithmsResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>现有API测试</h2>
|
||||
<div class="test-item pending" id="healthCheckTest">
|
||||
<span>健康检查</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="healthCheckResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getCurrentUserTest">
|
||||
<span>获取当前用户</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getCurrentUserResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getAlgorithmsTest">
|
||||
<span>获取算法列表</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getAlgorithmsResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getServicesTest">
|
||||
<span>获取服务列表</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getServicesResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary" id="testSummary">
|
||||
点击"运行所有测试"开始测试
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = 'http://localhost:8001/api/v1';
|
||||
let authToken = null;
|
||||
let testResults = {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0
|
||||
};
|
||||
|
||||
async function makeRequest(url, options = {}) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json();
|
||||
return { success: response.ok, status: response.status, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function updateTestItem(testId, success, message) {
|
||||
const testItem = document.getElementById(testId);
|
||||
const resultDiv = document.getElementById(testId + 'Result');
|
||||
|
||||
testItem.classList.remove('pending', 'success', 'error');
|
||||
testItem.classList.add(success ? 'success' : 'error');
|
||||
|
||||
const statusSpan = testItem.querySelector('.status');
|
||||
statusSpan.textContent = success ? '✓ 通过' : '✗ 失败';
|
||||
|
||||
resultDiv.textContent = message;
|
||||
|
||||
testResults.total++;
|
||||
if (success) {
|
||||
testResults.passed++;
|
||||
} else {
|
||||
testResults.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
async function testLogin() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/users/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'admin123' })
|
||||
});
|
||||
|
||||
if (result.success && result.data.access_token) {
|
||||
authToken = result.data.access_token;
|
||||
updateTestItem('loginTest', true, `登录成功,用户ID: ${result.data.user_id}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('loginTest', false, `登录失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetConfigs() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const configs = result.data.configs || [];
|
||||
updateTestItem('getConfigsTest', true, `获取成功,配置数量: ${configs.length}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getConfigsTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAddConfig() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/test_frontend_config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
value: 'frontend_test_value',
|
||||
type: 'system',
|
||||
service_id: null,
|
||||
description: '前端测试配置'
|
||||
})
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('addConfigTest', true, '添加配置成功');
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('addConfigTest', false, `添加失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetConfig() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/test_frontend_config`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('getConfigTest', true, `获取成功,配置值: ${result.data.value}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getConfigTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDeleteConfig() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/test_frontend_config`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('deleteConfigTest', true, '删除配置成功');
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('deleteConfigTest', false, `删除失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCompareAlgorithms() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/comparison/compare-algorithms`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input_data: { text: '前端测试文本' },
|
||||
algorithm_configs: [
|
||||
{
|
||||
algorithm_id: 'frontend_test_1',
|
||||
algorithm_name: '前端测试算法1',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
},
|
||||
{
|
||||
algorithm_id: 'frontend_test_2',
|
||||
algorithm_name: '前端测试算法2',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (result.success && result.data.success) {
|
||||
const resultsCount = result.data.results ? result.data.results.length : 0;
|
||||
updateTestItem('compareAlgorithmsTest', true, `比较成功,结果数量: ${resultsCount}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('compareAlgorithmsTest', false, `比较失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testHealthCheck() {
|
||||
const result = await makeRequest(`${API_BASE_URL.replace('/api/v1', '')}/health`);
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('healthCheckTest', true, `健康检查通过: ${result.data.status}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('healthCheckTest', false, `健康检查失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetCurrentUser() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/users/me`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('getCurrentUserTest', true, `获取成功,用户名: ${result.data.username}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getCurrentUserTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetAlgorithms() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/algorithms/`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const count = Array.isArray(result.data) ? result.data.length : 0;
|
||||
updateTestItem('getAlgorithmsTest', true, `获取成功,算法数量: ${count}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getAlgorithmsTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetServices() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/services`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const count = Array.isArray(result.data) ? result.data.length : 0;
|
||||
updateTestItem('getServicesTest', true, `获取成功,服务数量: ${count}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getServicesTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
// 重置测试结果
|
||||
testResults = { total: 0, passed: 0, failed: 0 };
|
||||
|
||||
// 禁用按钮
|
||||
document.getElementById('runAllTests').disabled = true;
|
||||
document.getElementById('resetTests').disabled = true;
|
||||
|
||||
// 运行测试
|
||||
await testLogin();
|
||||
|
||||
if (authToken) {
|
||||
await testHealthCheck();
|
||||
await testGetCurrentUser();
|
||||
await testGetAlgorithms();
|
||||
await testGetServices();
|
||||
await testGetConfigs();
|
||||
await testAddConfig();
|
||||
await testGetConfig();
|
||||
await testDeleteConfig();
|
||||
await testCompareAlgorithms();
|
||||
}
|
||||
|
||||
// 更新摘要
|
||||
const summaryDiv = document.getElementById('testSummary');
|
||||
summaryDiv.classList.remove('success', 'error');
|
||||
|
||||
if (testResults.failed === 0) {
|
||||
summaryDiv.classList.add('success');
|
||||
summaryDiv.textContent = `🎉 所有测试通过!总计: ${testResults.passed}/${testResults.total}`;
|
||||
} else {
|
||||
summaryDiv.classList.add('error');
|
||||
summaryDiv.textContent = `⚠️ 部分测试失败!总计: ${testResults.passed}/${testResults.total},失败: ${testResults.failed}`;
|
||||
}
|
||||
|
||||
// 启用按钮
|
||||
document.getElementById('runAllTests').disabled = false;
|
||||
document.getElementById('resetTests').disabled = false;
|
||||
}
|
||||
|
||||
function resetTests() {
|
||||
// 重置所有测试项
|
||||
const testItems = document.querySelectorAll('.test-item');
|
||||
testItems.forEach(item => {
|
||||
item.classList.remove('success', 'error');
|
||||
item.classList.add('pending');
|
||||
const statusSpan = item.querySelector('.status');
|
||||
statusSpan.textContent = '待测试';
|
||||
const resultDiv = item.querySelector('.result');
|
||||
resultDiv.textContent = '';
|
||||
});
|
||||
|
||||
// 重置摘要
|
||||
const summaryDiv = document.getElementById('testSummary');
|
||||
summaryDiv.classList.remove('success', 'error');
|
||||
summaryDiv.textContent = '点击"运行所有测试"开始测试';
|
||||
|
||||
// 重置结果
|
||||
testResults = { total: 0, passed: 0, failed: 0 };
|
||||
authToken = null;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,8 +14,24 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api/v1'),
|
||||
timeout: 600000 // 10分钟超时
|
||||
rewrite: (path) => {
|
||||
// 如果路径已经是 /api/v1/ 开头,则不重写
|
||||
if (path.startsWith('/api/v1/')) {
|
||||
return path
|
||||
}
|
||||
// 否则将 /api/ 重写为 /api/v1/
|
||||
return path.replace(/^\/api\//, '/api/v1/')
|
||||
},
|
||||
timeout: 600000, // 10分钟超时
|
||||
// 确保认证头在重定向时被保留
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
// 确保认证头被正确传递
|
||||
if (req.headers.authorization) {
|
||||
proxyReq.setHeader('Authorization', req.headers.authorization)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
169
frontend_access_test.html
Normal file
169
frontend_access_test.html
Normal file
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>前端API测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
border-color: #28a745;
|
||||
}
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#result {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>前端API测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>1. 测试登录</h2>
|
||||
<input type="text" id="username" value="admin" placeholder="用户名">
|
||||
<input type="password" id="password" value="admin123" placeholder="密码">
|
||||
<button onclick="testLogin()">登录</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>2. 测试获取用户信息</h2>
|
||||
<button onclick="testGetUser()">获取用户信息</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>3. 测试算法列表</h2>
|
||||
<button onclick="testAlgorithms()">获取算法列表</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>4. 测试仓库列表</h2>
|
||||
<button onclick="testRepositories()">获取仓库列表</button>
|
||||
</div>
|
||||
|
||||
<div id="result">点击按钮开始测试...</div>
|
||||
|
||||
<script>
|
||||
let token = null;
|
||||
|
||||
function log(message, isError = false) {
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.textContent = message;
|
||||
resultDiv.className = isError ? 'error' : 'success';
|
||||
}
|
||||
|
||||
async function testLogin() {
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
log(`登录结果:\n状态码: ${response.status}\n响应: ${JSON.stringify(data, null, 2)}`);
|
||||
|
||||
if (response.ok && data.access_token) {
|
||||
token = data.access_token;
|
||||
localStorage.setItem('token', token);
|
||||
log(`登录成功!\nToken: ${token.substring(0, 50)}...`);
|
||||
}
|
||||
} catch (error) {
|
||||
log(`登录失败: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetUser() {
|
||||
token = token || localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users/me', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
log(`获取用户信息结果:\n状态码: ${response.status}\n响应: ${JSON.stringify(data, null, 2)}`);
|
||||
} catch (error) {
|
||||
log(`获取用户信息失败: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function testAlgorithms() {
|
||||
token = token || localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/algorithms', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
log(`获取算法列表结果:\n状态码: ${response.status}\n响应: ${JSON.stringify(data, null, 2).substring(0, 500)}...`);
|
||||
} catch (error) {
|
||||
log(`获取算法列表失败: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function testRepositories() {
|
||||
token = token || localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/repositories', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
log(`获取仓库列表结果:\n状态码: ${response.status}\n响应: ${JSON.stringify(data, null, 2)}`);
|
||||
} catch (error) {
|
||||
log(`获取仓库列表失败: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查是否有token
|
||||
window.onload = function() {
|
||||
const savedToken = localStorage.getItem('token');
|
||||
if (savedToken) {
|
||||
token = savedToken;
|
||||
log(`已找到保存的Token: ${token.substring(0, 50)}...`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,142 +0,0 @@
|
||||
# 智能算法展示平台需求分析
|
||||
|
||||
## 1. 产品概述
|
||||
|
||||
智能算法展示平台是一个面向客户的算法能力可视化呈现系统,同时兼顾内部算法管理。平台通过「仿真输入 - 一键调用 - 效果可视化」的核心链路,让客户快速感知算法价值,同时为内部团队提供算法API管理能力。
|
||||
|
||||
**核心定位:**
|
||||
- **对外:** 算法能力展示窗口,适配ML/强化学习/计算机视觉等全类型算法
|
||||
- **对内:** 算法API管理台,支持算法API的注册、版本管理、调用监控、权限配置
|
||||
|
||||
## 2. 核心功能需求
|
||||
|
||||
### 2.1 前端客户展示层
|
||||
|
||||
#### 2.1.1 仿真输入获取模块
|
||||
- **OpenAI集成:** 接入OpenAI API,支持通过文本描述生成仿真输入数据
|
||||
- **多类型数据输入:** 支持图片、文本、结构化数据等多种类型的输入
|
||||
- **输入模板:** 提供预设的输入模板,方便客户快速测试
|
||||
|
||||
#### 2.1.2 算法调用模块
|
||||
- **算法目录:** 展示可用的算法列表,包含算法描述、适用场景等信息
|
||||
- **一键调用:** 支持选择算法和输入数据后一键执行
|
||||
- **参数配置:** 允许客户调整算法参数,测试不同参数下的效果
|
||||
|
||||
#### 2.1.3 效果展示模块
|
||||
- **多维度可视化:** 支持图表、图像对比、数值分析等多种展示方式
|
||||
- **效果对比:** 支持同输入下不同算法的效果对比,或同算法不同参数的效果对比
|
||||
- **历史记录:** 保存客户的测试历史,方便查看和比较
|
||||
|
||||
### 2.2 后端核心服务层
|
||||
|
||||
#### 2.2.1 API网关
|
||||
- **请求路由:** 根据请求路径和参数,将请求路由到对应的算法服务
|
||||
- **认证授权:** 验证用户身份和权限,确保API调用安全
|
||||
- **流量控制:** 限制API调用频率,防止系统过载
|
||||
|
||||
#### 2.2.2 服务管理
|
||||
- **服务管理:** 管理算法服务的基本配置和状态
|
||||
|
||||
#### 2.2.3 数据管理
|
||||
- **输入数据存储:** 存储客户上传的输入数据
|
||||
- **输出结果存储:** 存储算法执行的结果数据
|
||||
- **元数据管理:** 管理算法、输入、输出的元数据信息
|
||||
|
||||
#### 2.2.4 监控与日志
|
||||
- **调用监控:** 监控API调用情况,包括调用次数、响应时间、成功率等
|
||||
- **日志管理:** 记录系统运行日志,方便问题排查
|
||||
|
||||
### 2.3 算法API层
|
||||
|
||||
#### 2.3.1 算法注册
|
||||
- **算法信息管理:** 管理算法的基本信息,如名称、描述、版本等
|
||||
- **API规范定义:** 定义算法API的请求和响应格式
|
||||
- **部署配置:** 配置算法的部署方式和运行环境
|
||||
|
||||
#### 2.3.2 版本管理
|
||||
- **版本控制:** 支持算法的多版本管理,允许回滚到历史版本
|
||||
- **版本切换:** 允许在不同版本间切换,测试不同版本的效果
|
||||
|
||||
#### 2.3.3 权限配置
|
||||
- **访问控制:** 配置不同用户对算法的访问权限
|
||||
- **密钥管理:** 管理API调用所需的密钥
|
||||
|
||||
## 3. 非功能需求
|
||||
|
||||
### 3.1 性能需求
|
||||
- **响应时间:** 算法调用响应时间不超过5秒(不含算法执行时间)
|
||||
- **并发处理:** 支持至少100个并发请求
|
||||
- **可扩展性:** 系统架构支持水平扩展,以应对增长的用户量和算法数量
|
||||
|
||||
### 3.2 安全需求
|
||||
- **认证机制:** 实现基于JWT的认证机制
|
||||
- **数据加密:** 对敏感数据进行加密存储
|
||||
- **API安全:** 防止API滥用和恶意攻击
|
||||
|
||||
### 3.3 可用性需求
|
||||
- **系统可用性:** 系统可用性达到99.9%
|
||||
- **故障恢复:** 系统具备故障自动恢复能力
|
||||
|
||||
### 3.4 可维护性需求
|
||||
- **模块化设计:** 系统采用模块化设计,便于维护和升级
|
||||
- **日志管理:** 完善的日志系统,便于问题排查
|
||||
- **文档完整:** 提供完整的系统文档和API文档
|
||||
|
||||
## 4. 用户场景
|
||||
|
||||
### 4.1 客户场景
|
||||
1. **场景一:新客户了解算法能力**
|
||||
- 客户访问平台,浏览可用的算法列表
|
||||
- 选择感兴趣的算法,查看算法描述和适用场景
|
||||
- 使用平台提供的输入模板或通过OpenAI生成仿真输入
|
||||
- 一键调用算法,查看执行结果和可视化效果
|
||||
- 对比不同算法或不同参数下的效果
|
||||
|
||||
2. **场景二:潜在客户测试定制需求**
|
||||
- 客户上传自定义的输入数据
|
||||
- 选择相关算法进行测试
|
||||
- 调整算法参数,测试不同配置下的效果
|
||||
- 保存测试历史,与平台管理员沟通定制需求
|
||||
|
||||
### 4.2 内部管理员场景
|
||||
1. **场景一:算法注册与管理**
|
||||
- 管理员登录后台,注册新的算法
|
||||
- 配置算法的基本信息、API规范和部署参数
|
||||
- 管理算法的版本,发布新版本或回滚到历史版本
|
||||
- 配置算法的访问权限,控制谁可以访问该算法
|
||||
|
||||
2. **场景二:系统监控与分析**
|
||||
- 管理员查看API调用监控面板,了解系统运行状态
|
||||
- 分析算法调用情况,识别热门算法和潜在问题
|
||||
- 查看系统日志,排查和解决问题
|
||||
|
||||
## 5. 业务目标
|
||||
|
||||
### 5.1 对外目标
|
||||
- **提升算法可见性:** 通过可视化展示,让客户直观了解算法能力
|
||||
- **加速销售周期:** 减少客户评估算法的时间,加速销售决策
|
||||
- **扩大市场覆盖:** 通过在线展示,扩大算法的市场覆盖范围
|
||||
- **收集客户反馈:** 通过客户测试,收集算法改进的反馈
|
||||
|
||||
### 5.2 对内目标
|
||||
- **统一算法管理:** 集中管理所有算法API,提高管理效率
|
||||
- **优化资源分配:** 基于调用情况,优化算法资源分配
|
||||
- **促进算法迭代:** 通过监控和反馈,促进算法的持续迭代
|
||||
- **降低运营成本:** 自动化算法管理流程,降低运营成本
|
||||
|
||||
## 6. 范围限定
|
||||
|
||||
### 6.1 功能范围
|
||||
- **包含:** 算法展示、API管理、仿真输入、效果可视化、调用监控、开发SDK和工具
|
||||
- **不包含:** 算法开发环境、模型训练、数据标注工具
|
||||
|
||||
### 6.2 技术范围
|
||||
- **前端:** Vue3 + TypeScript + Vite + Pinia + Element Plus
|
||||
- **后端:** 基于Python的Web框架
|
||||
- **API管理:** OpenAPI规范,独立维护API文档
|
||||
- **数据存储:** PostgreSQL(结构化数据)、Redis(缓存)、MinIO(非结构化数据)
|
||||
|
||||
### 6.3 业务范围
|
||||
- **目标用户:** 外部客户、内部算法团队、销售团队
|
||||
- **适用算法:** ML算法、强化学习算法、计算机视觉算法等
|
||||
- **不适用:** 实时控制算法、需要特殊硬件的算法
|
||||
16
services/image-recognition/Dockerfile
Normal file
16
services/image-recognition/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动服务
|
||||
CMD ["python", "main.py"]
|
||||
71
services/image-recognition/ai_algorithm.py
Normal file
71
services/image-recognition/ai_algorithm.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import logging
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from typing import List, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageRecognizer:
|
||||
"""图像识别器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化图像识别器"""
|
||||
logger.info("初始化图像识别器")
|
||||
# 这里可以加载预训练模型
|
||||
# 示例中使用简单的规则识别
|
||||
|
||||
def recognize(self, images: List[str], params: Dict[str, Any] = None) -> List[Dict[str, Any]]:
|
||||
"""识别图像
|
||||
|
||||
Args:
|
||||
images: 图像列表,每个图像为base64编码字符串
|
||||
params: 识别参数
|
||||
|
||||
Returns:
|
||||
识别结果列表
|
||||
"""
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
threshold = params.get("threshold", 0.5)
|
||||
|
||||
results = []
|
||||
for image_base64 in images:
|
||||
# 简单的规则识别示例
|
||||
recognition = self._simple_recognize(image_base64)
|
||||
results.append({
|
||||
"image": image_base64[:100] + "..." if len(image_base64) > 100 else image_base64,
|
||||
"label": recognition["label"],
|
||||
"confidence": recognition["confidence"]
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def _simple_recognize(self, image_base64: str) -> Dict[str, Any]:
|
||||
"""简单的图像识别实现
|
||||
|
||||
Args:
|
||||
image_base64: base64编码的图像
|
||||
|
||||
Returns:
|
||||
识别结果
|
||||
"""
|
||||
# 简单的规则识别(基于图像大小和内容特征)
|
||||
try:
|
||||
# 解码base64
|
||||
image_data = base64.b64decode(image_base64)
|
||||
|
||||
# 计算图像大小特征
|
||||
image_size = len(image_data)
|
||||
|
||||
# 基于大小的简单分类
|
||||
if image_size < 10240: # 小于10KB
|
||||
return {"label": "小图像", "confidence": 0.8}
|
||||
elif image_size < 102400: # 小于100KB
|
||||
return {"label": "中等图像", "confidence": 0.85}
|
||||
else: # 大于100KB
|
||||
return {"label": "大图像", "confidence": 0.9}
|
||||
except Exception as e:
|
||||
logger.error(f"Image recognition error: {str(e)}")
|
||||
return {"label": "未知", "confidence": 0.5}
|
||||
27
services/image-recognition/config.py
Normal file
27
services/image-recognition/config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""服务配置"""
|
||||
# 服务基本配置
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8002
|
||||
DEBUG: bool = True
|
||||
|
||||
# 服务名称
|
||||
SERVICE_NAME: str = "image-recognition"
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL: str = "info"
|
||||
|
||||
# 算法配置
|
||||
ALGORITHM_THRESHOLD: float = 0.5
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
# 创建全局配置实例
|
||||
settings = Settings()
|
||||
108
services/image-recognition/main.py
Normal file
108
services/image-recognition/main.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
import json
|
||||
import logging
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from .ai_algorithm import ImageRecognizer
|
||||
from .config import settings
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 初始化FastAPI应用
|
||||
app = FastAPI(
|
||||
title="图像识别服务",
|
||||
description="提供图像识别功能的AI服务",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 初始化识别器
|
||||
recognizer = ImageRecognizer()
|
||||
|
||||
# 定义请求模型
|
||||
class PredictRequest(BaseModel):
|
||||
input_data: list
|
||||
params: dict = {}
|
||||
|
||||
# 定义响应模型
|
||||
class PredictResponse(BaseModel):
|
||||
predictions: list
|
||||
status: str
|
||||
|
||||
@app.post("/predict", response_model=PredictResponse)
|
||||
async def predict(request: PredictRequest):
|
||||
"""算法预测接口"""
|
||||
try:
|
||||
logger.info(f"Received prediction request for {len(request.input_data)} images")
|
||||
predictions = recognizer.recognize(request.input_data, request.params)
|
||||
logger.info(f"Prediction completed: {predictions}")
|
||||
return PredictResponse(
|
||||
predictions=predictions,
|
||||
status="success"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Prediction error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/predict/file")
|
||||
async def predict_file(file: UploadFile = File(...)):
|
||||
"""通过文件上传进行预测"""
|
||||
try:
|
||||
logger.info(f"Received file upload: {file.filename}")
|
||||
|
||||
# 读取文件内容
|
||||
contents = await file.read()
|
||||
|
||||
# 转换为base64
|
||||
image_base64 = base64.b64encode(contents).decode('utf-8')
|
||||
|
||||
# 调用识别器
|
||||
predictions = recognizer.recognize([image_base64])
|
||||
|
||||
logger.info(f"File prediction completed: {predictions}")
|
||||
return {
|
||||
"predictions": predictions,
|
||||
"status": "success",
|
||||
"filename": file.filename
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"File prediction error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "image-recognition",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
@app.get("/info")
|
||||
async def service_info():
|
||||
"""服务信息接口"""
|
||||
return {
|
||||
"name": "图像识别服务",
|
||||
"description": "提供图像识别功能的AI服务",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"/predict": "POST - 图像识别预测",
|
||||
"/predict/file": "POST - 通过文件上传进行预测",
|
||||
"/health": "GET - 健康检查",
|
||||
"/info": "GET - 服务信息"
|
||||
}
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
5
services/image-recognition/requirements.txt
Normal file
5
services/image-recognition/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0.post1
|
||||
pydantic==2.5.2
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
24
services/image-recognition/start.sh
Normal file
24
services/image-recognition/start.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 启动图像识别服务
|
||||
|
||||
# 进入服务目录
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 检查虚拟环境是否存在
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "创建虚拟环境..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
echo "激活虚拟环境..."
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
echo "安装依赖..."
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 启动服务
|
||||
echo "启动图像识别服务..."
|
||||
python main.py
|
||||
16
services/openai-proxy/Dockerfile
Normal file
16
services/openai-proxy/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动服务
|
||||
CMD ["python", "main.py"]
|
||||
174
services/openai-proxy/ai_algorithm.py
Normal file
174
services/openai-proxy/ai_algorithm.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
import openai
|
||||
from .config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenAIProxy:
|
||||
"""OpenAI代理"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化OpenAI代理"""
|
||||
logger.info("初始化OpenAI代理")
|
||||
# 设置API密钥
|
||||
openai.api_key = settings.API_KEY
|
||||
if settings.API_BASE:
|
||||
openai.api_base = settings.API_BASE
|
||||
|
||||
def complete(self, model: str, messages: list, temperature: float = 0.7,
|
||||
max_tokens: int = 1000) -> Dict[str, Any]:
|
||||
"""完成聊天请求
|
||||
|
||||
Args:
|
||||
model: 模型名称
|
||||
messages: 消息列表
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大令牌数
|
||||
|
||||
Returns:
|
||||
完成结果
|
||||
"""
|
||||
try:
|
||||
response = openai.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens
|
||||
)
|
||||
|
||||
# 转换为字典格式
|
||||
return {
|
||||
"id": response.id,
|
||||
"object": response.object,
|
||||
"created": response.created,
|
||||
"model": response.model,
|
||||
"choices": [
|
||||
{
|
||||
"index": choice.index,
|
||||
"message": {
|
||||
"role": choice.message.role,
|
||||
"content": choice.message.content
|
||||
},
|
||||
"finish_reason": choice.finish_reason
|
||||
}
|
||||
for choice in response.choices
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": response.usage.prompt_tokens,
|
||||
"completion_tokens": response.usage.completion_tokens,
|
||||
"total_tokens": response.usage.total_tokens
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"OpenAI completion error: {str(e)}")
|
||||
# 返回模拟响应,用于演示
|
||||
return self._mock_completion(messages, model)
|
||||
|
||||
def generate_simulation_input(self, prompt: str, input_type: str = "text") -> Dict[str, Any]:
|
||||
"""生成仿真输入数据
|
||||
|
||||
Args:
|
||||
prompt: 用户描述的场景
|
||||
input_type: 输入类型,支持 "text", "image", "table"
|
||||
|
||||
Returns:
|
||||
生成的仿真输入数据
|
||||
"""
|
||||
try:
|
||||
# 根据输入类型构建不同的提示词
|
||||
if input_type == "text":
|
||||
system_prompt = "你是一个文本数据生成器,根据用户描述生成相应的文本数据"
|
||||
user_prompt = f"请根据以下描述生成文本数据:{prompt}"
|
||||
elif input_type == "image":
|
||||
system_prompt = "你是一个图像描述生成器,根据用户描述生成详细的图像描述"
|
||||
user_prompt = f"请根据以下描述生成详细的图像描述:{prompt}"
|
||||
elif input_type == "table":
|
||||
system_prompt = "你是一个表格数据生成器,根据用户描述生成结构化的表格数据"
|
||||
user_prompt = f"请根据以下描述生成结构化的表格数据:{prompt}"
|
||||
else:
|
||||
system_prompt = "你是一个数据生成器,根据用户描述生成相应的数据"
|
||||
user_prompt = f"请根据以下描述生成数据:{prompt}"
|
||||
|
||||
# 调用OpenAI API
|
||||
response = openai.chat.completions.create(
|
||||
model=settings.MODEL,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
temperature=settings.TEMPERATURE,
|
||||
max_tokens=settings.MAX_TOKENS
|
||||
)
|
||||
|
||||
# 处理响应
|
||||
generated_content = response.choices[0].message.content
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": generated_content,
|
||||
"input_type": input_type
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"OpenAI simulation input generation error: {str(e)}")
|
||||
# 返回模拟响应,用于演示
|
||||
return self._mock_simulation_input(prompt, input_type)
|
||||
|
||||
def _mock_completion(self, messages: list, model: str) -> Dict[str, Any]:
|
||||
"""模拟完成响应,用于演示
|
||||
|
||||
Args:
|
||||
messages: 消息列表
|
||||
model: 模型名称
|
||||
|
||||
Returns:
|
||||
模拟的完成结果
|
||||
"""
|
||||
return {
|
||||
"id": "chat-mock-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677825464,
|
||||
"model": model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "这是一个模拟的响应,用于演示OpenAI代理服务"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 20,
|
||||
"total_tokens": 30
|
||||
}
|
||||
}
|
||||
|
||||
def _mock_simulation_input(self, prompt: str, input_type: str) -> Dict[str, Any]:
|
||||
"""模拟生成仿真输入数据,用于演示
|
||||
|
||||
Args:
|
||||
prompt: 用户描述的场景
|
||||
input_type: 输入类型
|
||||
|
||||
Returns:
|
||||
模拟的生成结果
|
||||
"""
|
||||
if input_type == "text":
|
||||
data = f"这是根据描述生成的文本数据:{prompt}"
|
||||
elif input_type == "image":
|
||||
data = f"这是根据描述生成的图像描述:{prompt}"
|
||||
elif input_type == "table":
|
||||
data = f"这是根据描述生成的表格数据:{prompt}"
|
||||
else:
|
||||
data = f"这是根据描述生成的数据:{prompt}"
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": data,
|
||||
"input_type": input_type
|
||||
}
|
||||
31
services/openai-proxy/config.py
Normal file
31
services/openai-proxy/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""服务配置"""
|
||||
# 服务基本配置
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8004
|
||||
DEBUG: bool = True
|
||||
|
||||
# 服务名称
|
||||
SERVICE_NAME: str = "openai-proxy"
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL: str = "info"
|
||||
|
||||
# OpenAI配置
|
||||
API_KEY: Optional[str] = None
|
||||
API_BASE: str = "https://api.openai.com/v1"
|
||||
MODEL: str = "gpt-3.5-turbo"
|
||||
TEMPERATURE: float = 0.7
|
||||
MAX_TOKENS: int = 1000
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
# 创建全局配置实例
|
||||
settings = Settings()
|
||||
109
services/openai-proxy/main.py
Normal file
109
services/openai-proxy/main.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
import json
|
||||
import logging
|
||||
from .ai_algorithm import OpenAIProxy
|
||||
from .config import settings
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 初始化FastAPI应用
|
||||
app = FastAPI(
|
||||
title="OpenAI代理服务",
|
||||
description="提供OpenAI API代理功能的服务",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 初始化代理
|
||||
openai_proxy = OpenAIProxy()
|
||||
|
||||
# 定义请求模型
|
||||
class CompletionRequest(BaseModel):
|
||||
model: str = "gpt-3.5-turbo"
|
||||
messages: list
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 1000
|
||||
|
||||
# 定义响应模型
|
||||
class CompletionResponse(BaseModel):
|
||||
id: str
|
||||
object: str
|
||||
created: int
|
||||
model: str
|
||||
choices: list
|
||||
usage: dict
|
||||
|
||||
# 定义生成仿真输入请求模型
|
||||
class GenerateSimulationInputRequest(BaseModel):
|
||||
prompt: str
|
||||
input_type: str = "text"
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completions(request: CompletionRequest):
|
||||
"""OpenAI聊天完成接口"""
|
||||
try:
|
||||
logger.info(f"Received chat completion request for model: {request.model}")
|
||||
response = openai_proxy.complete(
|
||||
model=request.model,
|
||||
messages=request.messages,
|
||||
temperature=request.temperature,
|
||||
max_tokens=request.max_tokens
|
||||
)
|
||||
logger.info(f"Chat completion completed")
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Chat completion error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/generate-simulation-input")
|
||||
async def generate_simulation_input(request: GenerateSimulationInputRequest):
|
||||
"""生成仿真输入数据"""
|
||||
try:
|
||||
logger.info(f"Received simulation input generation request")
|
||||
response = openai_proxy.generate_simulation_input(
|
||||
prompt=request.prompt,
|
||||
input_type=request.input_type
|
||||
)
|
||||
logger.info(f"Simulation input generation completed")
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Simulation input generation error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "openai-proxy",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
@app.get("/info")
|
||||
async def service_info():
|
||||
"""服务信息接口"""
|
||||
return {
|
||||
"name": "OpenAI代理服务",
|
||||
"description": "提供OpenAI API代理功能的服务",
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"/v1/chat/completions": "POST - OpenAI聊天完成",
|
||||
"/generate-simulation-input": "POST - 生成仿真输入数据",
|
||||
"/health": "GET - 健康检查",
|
||||
"/info": "GET - 服务信息"
|
||||
}
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
6
services/openai-proxy/requirements.txt
Normal file
6
services/openai-proxy/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0.post1
|
||||
pydantic==2.5.2
|
||||
pydantic-settings==2.1.0
|
||||
openai==1.3.5
|
||||
python-dotenv==1.0.0
|
||||
24
services/openai-proxy/start.sh
Normal file
24
services/openai-proxy/start.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 启动OpenAI代理服务
|
||||
|
||||
# 进入服务目录
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 检查虚拟环境是否存在
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "创建虚拟环境..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
echo "激活虚拟环境..."
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
echo "安装依赖..."
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 启动服务
|
||||
echo "启动OpenAI代理服务..."
|
||||
python main.py
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user