新分支,重新设计UI

This commit is contained in:
2026-01-18 19:51:22 +08:00
parent 6e1b4b58ba
commit b2f19d9583
15 changed files with 2060 additions and 1785 deletions

17
config.yaml Normal file
View File

@@ -0,0 +1,17 @@
# 数据库配置
database:
host: "10.10.10.189"
port: 3306
username: "root"
password: "88116142"
name: "ft"
charset: "utf8mb4"
# 应用配置
app:
host: "0.0.0.0"
port: 8080
debug: true
# 密钥配置
secret_key: "yg-ft-platform-secret-key-2024"

3
data/.gitignore vendored
View File

@@ -1,3 +0,0 @@
# 忽略data目录下的所有文件
*
!.gitignore

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
flask==3.0.0
flask-cors==4.0.0
pymysql==1.1.0
pyyaml==6.0.1
cryptography==41.0.7

View File

@@ -1,129 +0,0 @@
# FastAPI 服务器
## 功能特性
这个 FastAPI 服务器为大模型微调平台提供了 RESTful API 接口。
## API 端点
### 基础信息
- `GET /` - 根路径,返回欢迎信息
- `GET /api/health` - 健康检查
### 用户认证
- `POST /api/login` - 用户登录
```json
{
"username": "admin",
"password": "your_password"
}
```
### 数据集管理
- `GET /api/datasets` - 获取数据集列表
- `POST /api/datasets` - 创建新数据集
```json
{
"name": "新数据集名称",
"description": "数据集描述",
"size": "数据集大小"
}
```
- `POST /api/datasets/upload` - 上传数据集文件(支持 JSON 和 JSONL 格式)
```bash
curl -X POST "http://10.10.10.77:8001/api/datasets/upload" \
-F "file=@dataset.json" \
-F "description=数据集描述"
```
**支持的文件格式**: .json, .jsonl
**文件大小限制**: 100MB
- `GET /api/datasets/files` - 获取data目录中保存的文件列表
- `DELETE /api/datasets/{dataset_id}` - 删除数据集
### 模型管理
- `GET /api/models` - 获取模型列表
- `POST /api/models/config` - 配置模型参数
```json
{
"model_name": "GPT-4",
"learning_rate": 0.001,
"batch_size": 32,
"epochs": 100
}
```
### 训练管理
- `GET /api/training/status` - 获取训练状态
- `POST /api/training/start` - 开始训练任务
- `POST /api/training/stop/{task_id}` - 停止训练任务
- `GET /api/model/{model_id}/metrics` - 获取模型指标
### 系统监控
- `GET /api/system/stats` - 获取系统统计信息
## 启动服务器
### 方法 1: 使用启动脚本(推荐)
```bash
cd src
./run.sh
```
### 方法 2: 手动启动
```bash
# 安装依赖
pip3 install -r requirements.txt
# 启动服务器
uvicorn main:app --host 0.0.0.0 --port 8001 --reload
```
## 访问地址
- **服务器**: http://10.10.10.77:8001
- **API 文档**: http://10.10.10.77:8001/docs
- **替代文档**: http://10.10.10.77:8001/redoc
## 示例请求
### 登录
```bash
curl -X POST "http://10.10.10.77:8001/api/login" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "123456"}'
```
### 获取数据集列表
```bash
curl -X GET "http://10.10.10.77:8001/api/datasets"
```
### 上传数据集文件
```bash
curl -X POST "http://10.10.10.77:8001/api/datasets/upload" \
-F "file=@dataset.json" \
-F "description=数据集描述"
```
### 获取data目录文件列表
```bash
curl -X GET "http://10.10.10.77:8001/api/datasets/files"
```
### 获取系统统计
```bash
curl -X GET "http://10.10.10.77:8001/api/system/stats"
```
## 依赖
- Python 3.7+
- FastAPI 0.104.1
- Uvicorn 0.24.0
- Pydantic 2.5.0
## 注意事项
- 服务器默认运行在端口 8001
- 使用 `--reload` 参数启用热重载
- 所有 API 响应都遵循统一格式

View File

@@ -1,381 +1,468 @@
from fastapi import FastAPI, File, UploadFile, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
"""
远光软件微调平台 - Flask 后端 API
"""
import os
import sys
import json
import re
import time
import pymysql
import yaml
from flask import Flask, request, jsonify
from flask_cors import CORS
app = FastAPI(title="大模型微调平台 API", version="1.0.0")
# 获取项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, PROJECT_ROOT)
# 加载配置
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
# 请求模型
class UserModel(BaseModel):
username: str
password: str
def load_config():
"""加载配置文件"""
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
class DatasetModel(BaseModel):
name: str
description: Optional[str] = None
size: str
CONFIG = load_config()
class ModelConfigModel(BaseModel):
model_name: str
learning_rate: float
batch_size: int
epochs: int
# 响应模型
class ResponseModel(BaseModel):
code: int
message: str
data: Optional[dict] = None
# 模拟数据存储
datasets = [
{"id": 1, "name": "中文对话数据集", "size": "1.2GB", "status": "已处理"},
{"id": 2, "name": "英文文本分类数据集", "size": "856MB", "status": "处理中"},
{"id": 3, "name": "图像识别数据集", "size": "2.5GB", "status": "待处理"},
]
models = [
{"id": 1, "name": "GPT-4", "status": "训练中", "accuracy": "92%"},
{"id": 2, "name": "BERT", "status": "已完成", "accuracy": "89%"},
{"id": 3, "name": "LLaMA", "status": "已完成", "accuracy": "95%"},
]
@app.get("/")
async def root():
"""根路径"""
return {"message": "大模型微调平台 API 服务"}
@app.get("/api/health")
async def health_check():
"""健康检查"""
return ResponseModel(code=200, message="服务运行正常", data={"status": "healthy"})
@app.post("/api/login", response_model=ResponseModel)
async def login(user: UserModel):
"""用户登录"""
if user.username == "admin" and user.password:
return ResponseModel(
code=200,
message="登录成功",
data={"token": "mock_token_12345", "user": user.username}
)
else:
return ResponseModel(code=401, message="用户名或密码错误")
@app.get("/api/datasets", response_model=ResponseModel)
async def get_datasets():
"""获取数据集列表"""
return ResponseModel(code=200, message="获取成功", data={"datasets": datasets})
@app.post("/api/datasets", response_model=ResponseModel)
async def create_dataset(dataset: DatasetModel):
"""创建数据集"""
new_dataset = {
"id": len(datasets) + 1,
"name": dataset.name,
"description": dataset.description,
"size": "0MB",
"status": "待处理"
}
datasets.append(new_dataset)
return ResponseModel(code=201, message="创建成功", data={"dataset": new_dataset})
@app.post("/api/datasets/upload", response_model=ResponseModel)
async def upload_dataset(file: UploadFile = File(...), description: Optional[str] = None):
"""上传数据集文件(仅支持 JSON 和 JSONL 格式)"""
# 检查文件类型
allowed_extensions = ['.json', '.jsonl']
file_extension = os.path.splitext(file.filename)[1].lower()
if file_extension not in allowed_extensions:
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型。只能上传 {', '.join(allowed_extensions)} 格式的文件"
)
# 检查文件大小(限制为 100MB
max_size = 100 * 1024 * 1024 # 100MB
contents = await file.read()
file_size = len(contents)
if file_size > max_size:
raise HTTPException(
status_code=400,
detail=f"文件大小超过限制。最大支持 100MB当前文件大小: {file_size / (1024*1024):.2f}MB"
)
try:
# 验证文件内容
if file_extension == '.json':
# 验证 JSON 文件
json.loads(contents.decode('utf-8'))
elif file_extension == '.jsonl':
# 验证 JSONL 文件(每行必须是有效的 JSON
lines = contents.decode('utf-8').strip().split('\n')
for i, line in enumerate(lines):
if line.strip():
try:
json.loads(line)
except json.JSONDecodeError as e:
raise HTTPException(
status_code=400,
detail=f"JSONL 文件格式错误:第 {i+1} 行不是有效的 JSON 格式"
)
# 生成文件大小字符串
if file_size < 1024:
size_str = f"{file_size}B"
elif file_size < 1024 * 1024:
size_str = f"{file_size / 1024:.2f}KB"
else:
size_str = f"{file_size / (1024*1024):.2f}MB"
# 计算行数(用于统计)
lines_count = len(contents.decode('utf-8').strip().split('\n')) if contents else 0
# 保存文件到 data 目录
data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
os.makedirs(data_dir, exist_ok=True)
# 生成唯一文件名(避免冲突)
base_name = os.path.splitext(file.filename)[0]
timestamp = int(time.time())
saved_filename = f"{base_name}_{timestamp}{file_extension}"
saved_path = os.path.join(data_dir, saved_filename)
# 写入文件
with open(saved_path, 'wb') as f:
f.write(contents)
# 创建新数据集记录
new_dataset = {
"id": len(datasets) + 1,
"name": file.filename,
"description": description or f"上传的数据集文件,包含 {lines_count} 行数据",
"size": size_str,
"status": "已处理",
"upload_time": "刚刚",
"file_extension": file_extension,
"records_count": lines_count,
"saved_path": saved_path # 添加保存路径信息
}
# 添加到数据集列表
datasets.append(new_dataset)
return ResponseModel(
code=200,
message="文件上传成功",
data={
"dataset": new_dataset,
"file_info": {
"filename": file.filename,
"size": size_str,
"extension": file_extension,
"records": lines_count
}
}
)
except json.JSONDecodeError:
raise HTTPException(
status_code=400,
detail="JSON 文件格式错误:文件内容不是有效的 JSON 格式"
)
except UnicodeDecodeError:
raise HTTPException(
status_code=400,
detail="文件编码错误:请确保文件使用 UTF-8 编码"
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"文件处理错误:{str(e)}"
)
@app.get("/api/datasets/files", response_model=ResponseModel)
async def list_dataset_files():
"""列出data目录中所有保存的数据集文件"""
try:
data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
if not os.path.exists(data_dir):
return ResponseModel(
code=200,
message="获取成功",
data={"files": [], "total": 0, "directory": data_dir}
)
files = []
for filename in os.listdir(data_dir):
file_path = os.path.join(data_dir, filename)
if os.path.isfile(file_path):
stat = os.stat(file_path)
files.append({
"filename": filename,
"size": stat.st_size,
"size_human": format_size(stat.st_size),
"modified_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(stat.st_mtime)),
"path": file_path
})
# 按修改时间排序(最新的在前)
files.sort(key=lambda x: x["modified_time"], reverse=True)
return ResponseModel(
code=200,
message="获取成功",
data={
"files": files,
"total": len(files),
"directory": data_dir
}
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"获取文件列表失败:{str(e)}"
)
def format_size(size_bytes):
"""格式化文件大小"""
if size_bytes < 1024:
return f"{size_bytes}B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.2f}KB"
else:
return f"{size_bytes / (1024*1024):.2f}MB"
@app.delete("/api/datasets/{dataset_id}", response_model=ResponseModel)
async def delete_dataset(dataset_id: int):
"""删除数据集"""
global datasets
for i, dataset in enumerate(datasets):
if dataset["id"] == dataset_id:
deleted_dataset = datasets.pop(i)
return ResponseModel(
code=200,
message="删除成功",
data={"deleted_dataset": deleted_dataset}
)
raise HTTPException(status_code=404, detail="数据集不存在")
@app.get("/api/models", response_model=ResponseModel)
async def get_models():
"""获取模型列表"""
return ResponseModel(code=200, message="获取成功", data={"models": models})
@app.post("/api/models/config", response_model=ResponseModel)
async def config_model(config: ModelConfigModel):
"""配置模型参数"""
return ResponseModel(
code=200,
message="配置成功",
data={
"model_name": config.model_name,
"learning_rate": config.learning_rate,
"batch_size": config.batch_size,
"epochs": config.epochs,
"status": "已配置"
}
def get_db_connection():
"""获取数据库连接"""
db_config = CONFIG['database']
return pymysql.connect(
host=db_config['host'],
port=db_config['port'],
user=db_config['username'],
password=db_config['password'],
database=db_config['name'],
charset=db_config.get('charset', 'utf8mb4'),
cursorclass=pymysql.cursors.DictCursor
)
@app.get("/api/training/status")
async def get_training_status():
"""获取训练状态"""
return ResponseModel(
code=200,
message="获取成功",
data={
"current_task": "GPT-4微调",
"progress": 75,
"eta": "2小时",
"loss": 0.23,
"accuracy": 0.89
}
)
def init_database():
"""初始化数据库表"""
conn = get_db_connection()
cursor = conn.cursor()
tables = [
# 精调训练表
"""CREATE TABLE IF NOT EXISTS fine_tune (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
base_model VARCHAR(255),
train_type VARCHAR(50),
train_method VARCHAR(50),
dataset_id INT,
valid_split VARCHAR(50),
valid_ratio INT DEFAULT 10,
output_model_name VARCHAR(255),
status VARCHAR(50) DEFAULT 'pending',
progress INT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""",
# 我的模型表
"""CREATE TABLE IF NOT EXISTS my_models (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(100),
version VARCHAR(50),
description TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""",
# 模型评测表
"""CREATE TABLE IF NOT EXISTS model_eval (
id INT AUTO_INCREMENT PRIMARY KEY,
model_name VARCHAR(255) NOT NULL,
dataset VARCHAR(255),
metric VARCHAR(100),
score DECIMAL(10, 4),
status VARCHAR(50) DEFAULT 'completed',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
)""",
# 模型部署表
"""CREATE TABLE IF NOT EXISTS model_deploy (
id INT AUTO_INCREMENT PRIMARY KEY,
model_name VARCHAR(255) NOT NULL,
endpoint VARCHAR(255),
instance VARCHAR(100),
status VARCHAR(50) DEFAULT 'running',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""",
# 数据集管理表
"""CREATE TABLE IF NOT EXISTS dataset_manage (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(100),
size VARCHAR(50),
count INT,
description TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""",
# 数据生成表
"""CREATE TABLE IF NOT EXISTS data_generate (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
template VARCHAR(255),
count INT DEFAULT 0,
status VARCHAR(50) DEFAULT 'pending',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""",
# 权限管理表
"""CREATE TABLE IF NOT EXISTS permission (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
permissions TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""",
# 系统配置表
"""CREATE TABLE IF NOT EXISTS sys_config (
id INT AUTO_INCREMENT PRIMARY KEY,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value TEXT,
description VARCHAR(255),
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)""",
# 用户表
"""CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
)"""
]
for table_sql in tables:
cursor.execute(table_sql)
# 插入默认管理员用户
cursor.execute("SELECT * FROM users WHERE username = 'admin'")
if not cursor.fetchone():
cursor.execute("INSERT INTO users (username, password, role) VALUES ('admin', 'admin', 'admin')")
conn.commit()
cursor.close()
conn.close()
print("数据库初始化完成")
@app.get("/api/system/stats")
async def get_system_stats():
"""获取系统统计信息"""
import random
return ResponseModel(
code=200,
message="获取成功",
data={
"cpu_usage": random.randint(30, 80),
"memory_usage": random.randint(40, 70),
"gpu_usage": random.randint(50, 90),
"active_tasks": 5,
"completed_tasks": 158
}
)
app = Flask(__name__)
app.config['SECRET_KEY'] = CONFIG['secret_key']
CORS(app, resources={r"/api/*": {"origins": "*"}})
@app.post("/api/training/start")
async def start_training(model_name: str, dataset_id: int):
"""开始训练任务"""
return ResponseModel(
code=200,
message="训练任务已启动",
data={
"task_id": random.randint(1000, 9999),
"model_name": model_name,
"dataset_id": dataset_id,
"status": "running"
}
)
# ============ 健康检查 ============
@app.route('/api/health', methods=['GET'])
def health_check():
"""健康检查接口"""
return jsonify({'status': 'ok', 'code': 0})
@app.post("/api/training/stop/{task_id}")
async def stop_training(task_id: int):
"""停止训练任务"""
return ResponseModel(
code=200,
message=f"训练任务 {task_id} 已停止",
data={"task_id": task_id, "status": "stopped"}
)
# ============ 通用 CRUD 操作 ============
def generic_get_all(table_name, order_by='create_time DESC'):
"""通用查询所有"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table_name} ORDER BY {order_by}")
result = cursor.fetchall()
cursor.close()
conn.close()
return result
@app.get("/api/model/{model_id}/metrics")
async def get_model_metrics(model_id: int):
"""获取模型指标"""
return ResponseModel(
code=200,
message="获取成功",
data={
"model_id": model_id,
"accuracy": round(random.uniform(0.85, 0.98), 3),
"precision": round(random.uniform(0.80, 0.95), 3),
"recall": round(random.uniform(0.82, 0.96), 3),
"f1_score": round(random.uniform(0.83, 0.97), 3),
"training_time": f"{random.randint(2, 24)}小时",
"parameters": random.randint(1000000, 100000000)
}
)
def generic_get_by_id(table_name, id_val):
"""通用按ID查询"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (id_val,))
result = cursor.fetchone()
cursor.close()
conn.close()
return result
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8001)
def generic_create(table_name, data):
"""通用创建"""
conn = get_db_connection()
cursor = conn.cursor()
columns = ', '.join(data.keys())
placeholders = ', '.join(['%s'] * len(data))
sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
cursor.execute(sql, list(data.values()))
conn.commit()
new_id = cursor.lastrowid
cursor.close()
conn.close()
return new_id
def generic_update(table_name, id_val, data):
"""通用更新"""
conn = get_db_connection()
cursor = conn.cursor()
set_clause = ', '.join([f"{k} = %s" for k in data.keys()])
sql = f"UPDATE {table_name} SET {set_clause} WHERE id = %s"
values = list(data.values()) + [id_val]
cursor.execute(sql, values)
conn.commit()
cursor.close()
conn.close()
def generic_delete(table_name, id_val):
"""通用删除"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(f"DELETE FROM {table_name} WHERE id = %s", (id_val,))
conn.commit()
cursor.close()
conn.close()
# ============ 登录接口 ============
@app.route('/api/login', methods=['POST'])
def login():
data = request.json
username = data.get('username')
password = data.get('password')
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
user = cursor.fetchone()
cursor.close()
conn.close()
if user:
return jsonify({'code': 0, 'message': '登录成功', 'data': {'username': user['username'], 'role': user['role']}})
return jsonify({'code': 1, 'message': '用户名或密码错误'})
# ============ 精调训练接口 ============
@app.route('/api/fine-tune', methods=['GET'])
def get_fine_tune():
return jsonify({'code': 0, 'data': generic_get_all('fine_tune')})
@app.route('/api/fine-tune', methods=['POST'])
def create_fine_tune():
data = request.json
new_id = generic_create('fine_tune', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/fine-tune/<int:id>', methods=['PUT'])
def update_fine_tune(id):
data = request.json
generic_update('fine_tune', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/fine-tune/<int:id>', methods=['DELETE'])
def delete_fine_tune(id):
generic_delete('fine_tune', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 我的模型接口 ============
@app.route('/api/my-models', methods=['GET'])
def get_my_models():
return jsonify({'code': 0, 'data': generic_get_all('my_models')})
@app.route('/api/my-models', methods=['POST'])
def create_my_model():
data = request.json
new_id = generic_create('my_models', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/my-models/<int:id>', methods=['PUT'])
def update_my_model(id):
data = request.json
generic_update('my_models', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/my-models/<int:id>', methods=['DELETE'])
def delete_my_model(id):
generic_delete('my_models', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 模型评测接口 ============
@app.route('/api/model-eval', methods=['GET'])
def get_model_eval():
return jsonify({'code': 0, 'data': generic_get_all('model_eval')})
@app.route('/api/model-eval', methods=['POST'])
def create_model_eval():
data = request.json
new_id = generic_create('model_eval', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/model-eval/<int:id>', methods=['PUT'])
def update_model_eval(id):
data = request.json
generic_update('model_eval', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/model-eval/<int:id>', methods=['DELETE'])
def delete_model_eval(id):
generic_delete('model_eval', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 模型部署接口 ============
@app.route('/api/model-deploy', methods=['GET'])
def get_model_deploy():
return jsonify({'code': 0, 'data': generic_get_all('model_deploy')})
@app.route('/api/model-deploy', methods=['POST'])
def create_model_deploy():
data = request.json
new_id = generic_create('model_deploy', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/model-deploy/<int:id>', methods=['PUT'])
def update_model_deploy(id):
data = request.json
generic_update('model_deploy', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/model-deploy/<int:id>', methods=['DELETE'])
def delete_model_deploy(id):
generic_delete('model_deploy', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 数据集管理接口 ============
@app.route('/api/dataset-manage', methods=['GET'])
def get_dataset_manage():
return jsonify({'code': 0, 'data': generic_get_all('dataset_manage')})
@app.route('/api/dataset-manage', methods=['POST'])
def create_dataset_manage():
data = request.json
new_id = generic_create('dataset_manage', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/dataset-manage/<int:id>', methods=['PUT'])
def update_dataset_manage(id):
data = request.json
generic_update('dataset_manage', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/dataset-manage/<int:id>', methods=['DELETE'])
def delete_dataset_manage(id):
generic_delete('dataset_manage', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 数据生成接口 ============
@app.route('/api/data-generate', methods=['GET'])
def get_data_generate():
return jsonify({'code': 0, 'data': generic_get_all('data_generate')})
@app.route('/api/data-generate', methods=['POST'])
def create_data_generate():
data = request.json
new_id = generic_create('data_generate', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/data-generate/<int:id>', methods=['PUT'])
def update_data_generate(id):
data = request.json
generic_update('data_generate', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/data-generate/<int:id>', methods=['DELETE'])
def delete_data_generate(id):
generic_delete('data_generate', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 权限管理接口 ============
@app.route('/api/permission', methods=['GET'])
def get_permission():
return jsonify({'code': 0, 'data': generic_get_all('permission')})
@app.route('/api/permission', methods=['POST'])
def create_permission():
data = request.json
new_id = generic_create('permission', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/permission/<int:id>', methods=['PUT'])
def update_permission(id):
data = request.json
generic_update('permission', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/permission/<int:id>', methods=['DELETE'])
def delete_permission(id):
generic_delete('permission', id)
return jsonify({'code': 0, 'message': '删除成功'})
# ============ 系统配置接口 ============
@app.route('/api/sys-config', methods=['GET'])
def get_sys_config():
return jsonify({'code': 0, 'data': generic_get_all('sys_config')})
@app.route('/api/sys-config', methods=['POST'])
def create_sys_config():
data = request.json
new_id = generic_create('sys_config', data)
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
@app.route('/api/sys-config/<int:id>', methods=['PUT'])
def update_sys_config(id):
data = request.json
generic_update('sys_config', id, data)
return jsonify({'code': 0, 'message': '更新成功'})
@app.route('/api/sys-config/<int:id>', methods=['DELETE'])
def delete_sys_config(id):
generic_delete('sys_config', id)
return jsonify({'code': 0, 'message': '删除成功'})
if __name__ == '__main__':
init_database()
app_config = CONFIG['app']
app.run(host=app_config['host'], port=app_config['port'], debug=app_config.get('debug', True))

View File

@@ -1,4 +0,0 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
python-multipart==0.0.6

47
src/run.sh Executable file → Normal file
View File

@@ -1,43 +1,14 @@
#!/bin/bash
# 启动远光软件微调平台后端服务
echo "🚀 启动 FastAPI 服务器..."
cd "$(dirname "$0")"
# 确保在正确的目录中
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "📂 当前目录: $SCRIPT_DIR"
# 检查Python是否安装
if ! command -v python3 &> /dev/null; then
echo "❌ 错误: Python3 未安装"
echo "请先安装 Python3"
exit 1
# 检查并安装依赖
if ! python3 -c "import flask" 2>/dev/null; then
echo "正在安装依赖..."
pip install -r ../requirements.txt
fi
# 检查pip是否安装
if ! command -v pip3 &> /dev/null; then
echo "❌ 错误: pip3 未安装"
echo "请先安装 pip3"
exit 1
fi
# 安装依赖
echo "📦 安装依赖包..."
pip3 install -r requirements.txt
if [ $? -ne 0 ]; then
echo "❌ 依赖安装失败"
exit 1
fi
echo ""
echo "🌐 服务器地址: http://localhost:8001"
echo "📚 API 文档: http://localhost:8001/docs"
echo "🔍 替代文档: http://localhost:8001/redoc"
echo ""
echo "按 Ctrl+C 停止服务器"
echo ""
# 启动服务器
python3 -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
# 启动服务
echo "启动后端服务..."
python3 main.py

View File

@@ -1,47 +0,0 @@
#!/bin/bash
echo "🧪 测试 FastAPI 服务器"
echo "=================================="
echo ""
BASE_URL="http://localhost:8001"
# 测试 1: 根路径
echo "1. 测试根路径..."
curl -s "$BASE_URL/" | python3 -m json.tool
echo ""
# 测试 2: 健康检查
echo "2. 测试健康检查..."
curl -s "$BASE_URL/api/health" | python3 -m json.tool
echo ""
# 测试 3: 用户登录
echo "3. 测试用户登录..."
curl -s -X POST "$BASE_URL/api/login" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "123456"}' | python3 -m json.tool
echo ""
# 测试 4: 获取数据集列表
echo "4. 测试获取数据集列表..."
curl -s "$BASE_URL/api/datasets" | python3 -m json.tool
echo ""
# 测试 5: 获取模型列表
echo "5. 测试获取模型列表..."
curl -s "$BASE_URL/api/models" | python3 -m json.tool
echo ""
# 测试 6: 系统统计
echo "6. 测试系统统计..."
curl -s "$BASE_URL/api/system/stats" | python3 -m json.tool
echo ""
# 测试 7: 训练状态
echo "7. 测试训练状态..."
curl -s "$BASE_URL/api/training/status" | python3 -m json.tool
echo ""
echo "=================================="
echo "✅ 所有测试完成!"

View File

@@ -1,88 +0,0 @@
#!/bin/bash
echo "🧪 测试大模型微调平台 - 所有服务"
echo "=================================="
echo ""
BASE_URL="http://localhost"
# 检查服务是否运行
echo "1. 检查服务状态..."
echo ""
# 检查端口 8000 (Web 前端)
if curl -s "${BASE_URL}:8000" > /dev/null 2>&1; then
echo "✅ Web 前端服务正在运行 (端口 8000)"
else
echo "❌ Web 前端服务未运行 (端口 8000)"
fi
# 检查端口 8001 (FastAPI)
if curl -s "${BASE_URL}:8001" > /dev/null 2>&1; then
echo "✅ FastAPI 服务正在运行 (端口 8001)"
else
echo "❌ FastAPI 服务未运行 (端口 8001)"
fi
echo ""
echo "=================================="
echo ""
# 获取本机IP
SERVER_IP=$(hostname -I | awk '{print $1}')
echo "📱 访问地址:"
echo ""
echo "前端页面:"
echo " - 主页: http://$SERVER_IP:8000/pages/main.html"
echo " - 登录: http://$SERVER_IP:8000/pages/login.html"
echo ""
echo "API 服务:"
echo " - API 根路径: http://$SERVER_IP:8001/"
echo " - API 健康检查: http://$SERVER_IP:8001/api/health"
echo " - API 文档: http://$SERVER_IP:8001/docs"
echo ""
echo "=================================="
echo "2. 测试 API 接口..."
echo ""
# 测试 API
echo "测试根路径:"
curl -s "${BASE_URL}:8001/" | python3 -m json.tool 2>/dev/null || curl -s "${BASE_URL}:8001/"
echo ""
echo "测试健康检查:"
curl -s "${BASE_URL}:8001/api/health" | python3 -m json.tool 2>/dev/null || echo "请求失败"
echo ""
echo "测试数据集 API:"
curl -s "${BASE_URL}:8001/api/datasets" | python3 -m json.tool 2>/dev/null || echo "请求失败"
echo ""
echo "=================================="
echo "3. 测试前端页面..."
echo ""
# 测试前端页面
echo "测试主页:"
if curl -s -I "${BASE_URL}:8000/pages/main.html" | grep -q "200 OK"; then
echo "✅ 主页可访问"
else
echo "❌ 主页无法访问"
fi
echo "测试登录页:"
if curl -s -I "${BASE_URL}:8000/pages/login.html" | grep -q "200 OK"; then
echo "✅ 登录页可访问"
else
echo "❌ 登录页无法访问"
fi
echo ""
echo "=================================="
echo "✅ 测试完成!"
echo ""
echo "💡 如果服务未运行,请使用以下命令启动:"
echo " ./total_start.sh"
echo ""

View File

@@ -1,53 +0,0 @@
#!/bin/bash
echo "🧪 测试data目录功能"
echo "=================================="
echo ""
API_URL="http://10.10.10.77:8001/api"
echo "1. 测试获取文件列表..."
curl -s "${API_URL}/datasets/files" | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f'✅ 共 {data[\"data\"][\"total\"]} 个文件')
print('')
for f in data['data']['files']:
print(f' 📄 {f[\"filename\"]} - {f[\"size_human\"]}')
"
echo ""
echo "2. 测试上传新文件..."
cat > /tmp/test_upload.json << 'INNER_EOF'
[{"text": "测试上传", "label": "test"}]
INNER_EOF
curl -s -X POST "${API_URL}/datasets/upload" \
-F "file=@/tmp/test_upload.json" \
-F "description=测试data目录上传" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if data['code'] == 200:
print('✅ 文件上传成功')
print(f' 保存路径: {data[\"data\"][\"dataset\"][\"saved_path\"]}')
else:
print('❌ 上传失败')
"
echo ""
echo "3. 再次获取文件列表..."
curl -s "${API_URL}/datasets/files" | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f'✅ 当前共 {data[\"data\"][\"total\"]} 个文件')
"
echo ""
echo "=================================="
echo "✅ 测试完成!"
echo ""
echo "📁 data目录位置"
echo " /data/code/FT_Platform/YG_FT_Platform/data/"
echo ""
echo "🔍 查看文件:"
echo " ls -lh /data/code/FT_Platform/YG_FT_Platform/data/"

View File

@@ -1,200 +0,0 @@
#!/bin/bash
echo "🚀 大模型微调平台 - 一键启动"
echo "=================================="
echo ""
# 确保在正确的目录中
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "📂 当前目录: $SCRIPT_DIR"
echo ""
# 获取本机IP地址
SERVER_IP=$(hostname -I | awk '{print $1}')
echo "🌐 本机 IP 地址: $SERVER_IP"
echo ""
# 检查Python是否安装
if ! command -v python3 &> /dev/null; then
echo "❌ 错误: Python3 未安装"
echo "请先安装 Python3"
exit 1
fi
echo "请选择启动方式:"
echo "1) 启动所有服务FastAPI + Web前端"
echo "2) 只启动 FastAPI 服务(端口 8001"
echo "3) 只启动 Web 前端服务(端口 8000"
echo "4) 交互式选择"
echo ""
read -p "请输入选择 (1-4): " choice
case $choice in
1)
echo ""
echo "✅ 启动所有服务..."
echo ""
# 检查并启动 FastAPI 服务
echo "🔧 启动 FastAPI 服务..."
cd "$SCRIPT_DIR/src"
if [ ! -d "node_modules" ] 2>/dev/null && [ ! -f "main.py" ]; then
echo "⚠️ 警告: 未找到 FastAPI 源码文件"
fi
# 启动 FastAPI (后台运行)
cd "$SCRIPT_DIR/src"
python3 -m uvicorn main:app --host 0.0.0.0 --port 8001 &
FASTAPI_PID=$!
echo "✅ FastAPI 服务已启动 (PID: $FASTAPI_PID)"
echo " API 地址: http://localhost:8001"
echo " API 文档: http://localhost:8001/docs"
echo ""
# 等待 FastAPI 启动
sleep 2
# 启动 Web 前端服务
echo "🌐 启动 Web 前端服务..."
cd "$SCRIPT_DIR/web"
python3 -m http.server 8000 &
WEB_PID=$!
echo "✅ Web 前端服务已启动 (PID: $WEB_PID)"
echo " 前端地址: http://localhost:8000"
echo ""
echo "=================================="
echo "🎉 所有服务启动完成!"
echo ""
echo "📱 访问地址:"
echo " - 前端页面: http://$SERVER_IP:8000/pages/main.html"
echo " - 登录页面: http://$SERVER_IP:8000/pages/login.html"
echo " - API 服务: http://$SERVER_IP:8001"
echo " - API 文档: http://$SERVER_IP:8001/docs"
echo ""
echo "⚠️ 按 Ctrl+C 停止所有服务"
echo ""
# 等待用户中断
trap "echo ''; echo '🛑 正在停止服务...'; kill $FASTAPI_PID $WEB_PID 2>/dev/null; echo '✅ 所有服务已停止'; exit 0" INT
# 保持脚本运行
while true; do
sleep 1
done
;;
2)
echo ""
echo "🔧 启动 FastAPI 服务..."
cd "$SCRIPT_DIR/src"
# 检查源码文件是否存在
if [ ! -f "main.py" ]; then
echo "❌ 错误: 未找到 main.py 文件"
echo "请确保在正确的目录中"
exit 1
fi
echo "✅ FastAPI 服务启动中..."
echo " API 地址: http://localhost:8001"
echo " API 文档: http://localhost:8001/docs"
echo ""
echo "⚠️ 按 Ctrl+C 停止服务"
echo ""
python3 -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
;;
3)
echo ""
echo "🌐 启动 Web 前端服务..."
cd "$SCRIPT_DIR/web"
# 检查页面文件是否存在
if [ ! -f "pages/main.html" ]; then
echo "❌ 错误: 未找到 pages/main.html 文件"
echo "请确保在正确的目录中"
exit 1
fi
echo "✅ Web 前端服务启动中..."
echo " 前端地址: http://localhost:8000"
echo ""
echo "⚠️ 按 Ctrl+C 停止服务"
echo ""
python3 -m http.server 8000
;;
4)
echo ""
echo "🔧 检查服务状态..."
echo ""
# 检查 FastAPI
if curl -s http://localhost:8001 > /dev/null 2>&1; then
echo "✅ FastAPI 服务正在运行 (端口 8001)"
else
echo "❌ FastAPI 服务未运行 (端口 8001)"
fi
# 检查 Web 服务
if curl -s http://localhost:8000 > /dev/null 2>&1; then
echo "✅ Web 前端服务正在运行 (端口 8000)"
else
echo "❌ Web 前端服务未运行 (端口 8000)"
fi
echo ""
read -p "是否启动 FastAPI 服务?(y/n): " start_fastapi
if [[ $start_fastapi == "y" || $start_fastapi == "Y" ]]; then
cd "$SCRIPT_DIR/src"
python3 -m uvicorn main:app --host 0.0.0.0 --port 8001 &
FASTAPI_PID=$!
echo "✅ FastAPI 服务已启动 (PID: $FASTAPI_PID)"
fi
echo ""
read -p "是否启动 Web 前端服务?(y/n): " start_web
if [[ $start_web == "y" || $start_web == "Y" ]]; then
cd "$SCRIPT_DIR/web"
python3 -m http.server 8000 &
WEB_PID=$!
echo "✅ Web 前端服务已启动 (PID: $WEB_PID)"
fi
if [[ $start_fastapi == "y" || $start_fastapi == "Y" || $start_web == "y" || $start_web == "Y" ]]; then
echo ""
echo "=================================="
echo "🎉 服务启动完成!"
echo ""
echo "📱 访问地址:"
echo " - 前端页面: http://$SERVER_IP:8000/pages/main.html"
echo " - 登录页面: http://$SERVER_IP:8000/pages/login.html"
echo " - API 服务: http://$SERVER_IP:8001"
echo " - API 文档: http://$SERVER_IP:8001/docs"
echo ""
echo "⚠️ 按 Ctrl+C 停止服务"
echo ""
# 等待用户中断
trap "echo ''; echo '🛑 正在停止服务...'; kill $FASTAPI_PID $WEB_PID 2>/dev/null; echo '✅ 所有服务已停止'; exit 0" INT
# 保持脚本运行
while true; do
sleep 1
done
else
echo "未启动任何服务"
fi
;;
*)
echo "❌ 无效选择,请运行脚本重新选择"
exit 1
;;
esac

View File

@@ -0,0 +1,326 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模型调优 / 创建训练任务</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#1890ff',
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.card-radio {
@apply border border-gray-200 rounded-lg p-4 cursor-pointer transition-all;
}
.card-radio.active {
@apply border-primary bg-blue-50;
}
.card-radio:hover {
@apply border-gray-300;
}
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 顶部导航 -->
<header class="bg-white border-b border-gray-200 shadow-sm">
<div class="flex items-center justify-between px-6 h-14">
<div class="flex items-center space-x-4">
<a href="main.html" class="text-gray-500 hover:text-gray-700">
<i class="fa fa-arrow-left"></i>
<span class="ml-1">返回</span>
</a>
<span class="text-gray-300">|</span>
<span class="text-gray-800 font-medium">创建训练任务</span>
</div>
</div>
</header>
<!-- 页面标题 -->
<div class="px-8 py-4">
<h1 class="text-xl font-medium text-gray-800">模型调优 / 创建训练任务</h1>
</div>
<!-- 表单内容 -->
<div class="px-8 pb-8">
<form id="createForm" class="bg-white rounded-lg shadow-sm p-6 max-w-4xl">
<!-- 基本信息 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">基本信息</h3>
<div class="flex items-center mb-4">
<label class="w-24 text-sm text-gray-600">任务名称</label>
<div class="flex-1 max-w-md">
<input type="text" name="name" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入任务名称" maxlength="50">
<p class="text-xs text-gray-400 mt-1"><span id="nameCount">0</span> / 50</p>
</div>
</div>
</div>
<!-- 训练配置 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练配置</h3>
<!-- 训练方式 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方式</label>
<div class="grid grid-cols-3 gap-4">
<div class="card-radio active" data-value="SFT">
<div class="flex items-start">
<input type="radio" name="train_type" value="SFT" checked class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">SFT 微调训练</div>
<div class="text-xs text-gray-400 mt-1">在监督指令下,增强模型指令跟随的能力,提升全参数微调训练方式</div>
</div>
</div>
</div>
<div class="card-radio" data-value="DPO">
<div class="flex items-start">
<input type="radio" name="train_type" value="DPO" class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">DPO 偏好训练</div>
<div class="text-xs text-gray-400 mt-1">引入人类反馈,降低幻觉,使得模型输出更符合人类偏好</div>
</div>
</div>
</div>
<div class="card-radio" data-value="CPT">
<div class="flex items-start">
<input type="radio" name="train_type" value="CPT" class="mt-1 mr-2">
<div>
<div class="font-medium text-sm">CPT 继续预训练</div>
<div class="text-xs text-gray-400 mt-1">通过无标注数据进行无监督继续训练,强化或新增模型特定能力</div>
</div>
</div>
</div>
</div>
</div>
<!-- 选择模型 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">选择模型</label>
<div class="flex items-center space-x-6 mb-3">
<label class="flex items-center">
<input type="radio" name="model_source" value="preset" checked class="mr-2">
<span class="text-sm">预置模型</span>
</label>
<label class="flex items-center">
<input type="radio" name="model_source" value="custom" class="mr-2">
<span class="text-sm">自定义模型</span>
</label>
</div>
<div class="pl-24">
<select name="base_model" class="w-64 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none">
<option value="">请选择</option>
<option value="qwen-3-4b-instruct">通义千问3-4B-Instruct</option>
<option value="qwen-7b-instruct">通义千问7B-Instruct</option>
<option value="qwen-14b-instruct">通义千问14B-Instruct</option>
<option value="llama2-7b">Llama2-7B</option>
<option value="llama2-13b">Llama2-13B</option>
</select>
</div>
</div>
<!-- 训练方法 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方法</label>
<div class="flex items-center space-x-6">
<label class="flex items-center">
<input type="radio" name="train_method" value="lora" checked class="mr-2">
<span class="text-sm">高效训练</span>
</label>
<label class="flex items-center">
<input type="radio" name="train_method" value="full" class="mr-2">
<span class="text-sm">全参训练</span>
</label>
</div>
</div>
</div>
<!-- 数据配置 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">数据配置</h3>
<!-- 训练集 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练集</label>
<div class="pl-24">
<select name="dataset_id" class="w-64 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none">
<option value="">请选择</option>
</select>
</div>
</div>
<!-- 验证集 -->
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">验证集 <span class="text-red-500">*</span></label>
<div class="flex items-center space-x-6 mb-3">
<label class="flex items-center">
<input type="radio" name="valid_split" value="auto" checked class="mr-2">
<span class="text-sm">自动切分</span>
</label>
<label class="flex items-center">
<input type="radio" name="valid_split" value="custom" class="mr-2">
<span class="text-sm">选择数据集</span>
</label>
</div>
<div class="pl-24 flex items-center">
<span class="text-sm text-gray-600 mr-2">从当前训练集随机分割</span>
<input type="number" name="valid_ratio" value="10" class="w-16 px-2 py-1 border border-gray-300 rounded text-sm text-center focus:border-primary focus:outline-none">
<span class="text-sm text-gray-600 ml-2">% 作为验证集</span>
</div>
</div>
</div>
<!-- 训练产出 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练产出</h3>
<!-- 模型名称 -->
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-3">模型名称</label>
<div class="pl-24">
<input type="text" name="output_model_name" class="w-64 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入模型名称" maxlength="50">
<p class="text-xs text-gray-400 mt-1"><span id="modelNameCount">0</span> / 50</p>
</div>
</div>
<!-- 模型加密 -->
<div class="mb-4">
<div class="flex items-center">
<span class="text-sm text-gray-600 mr-2">模型加密</span>
<span class="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded">安全升级</span>
</div>
<p class="text-xs text-gray-400 mt-1 ml-24">为保障您的数据安全,平台会为导出的模型文件开启 OSS 服务端加密</p>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
<div class="flex items-center space-x-3">
<button type="button" onclick="submitForm()" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90">
开始训练
</button>
<a href="main.html" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300">
取消
</a>
</div>
<div class="flex items-center text-sm">
<a href="#" class="text-primary hover:underline">训练费用 (预估)</a>
<span class="mx-2 text-gray-300">|</span>
<a href="#" class="text-primary hover:underline">计算详情</a>
</div>
</div>
</form>
</div>
<script>
// API 基础地址
const getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:8080/api`;
};
const API_BASE = getApiBase();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 卡片式单选框
document.querySelectorAll('.card-radio').forEach(card => {
card.addEventListener('click', () => {
const parent = card.parentElement;
parent.querySelectorAll('.card-radio').forEach(c => c.classList.remove('active'));
card.classList.add('active');
card.querySelector('input').checked = true;
});
});
// 任务名称字数统计
const nameInput = document.querySelector('input[name="name"]');
nameInput.addEventListener('input', () => {
document.getElementById('nameCount').textContent = nameInput.value.length;
});
// 模型名称字数统计
const modelNameInput = document.querySelector('input[name="output_model_name"]');
modelNameInput.addEventListener('input', () => {
document.getElementById('modelNameCount').textContent = modelNameInput.value.length;
});
// 加载数据集列表
loadDatasets();
});
// 加载数据集列表
async function loadDatasets() {
try {
const response = await fetch(`${API_BASE}/dataset-manage`);
const result = await response.json();
if (result.code === 0) {
const select = document.querySelector('select[name="dataset_id"]');
select.innerHTML = '<option value="">请选择</option>' +
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
}
} catch (e) {
console.error('加载数据集失败:', e);
}
}
// 提交表单
async function submitForm() {
const form = document.getElementById('createForm');
const formData = new FormData(form);
const data = {
name: formData.get('name'),
base_model: formData.get('base_model'),
train_type: formData.get('train_type'),
train_method: formData.get('train_method'),
dataset_id: formData.get('dataset_id'),
valid_split: formData.get('valid_split'),
valid_ratio: parseInt(formData.get('valid_ratio')) || 10,
output_model_name: formData.get('output_model_name'),
status: 'pending',
progress: 0
};
if (!data.name) {
alert('请输入任务名称');
return;
}
if (!data.base_model) {
alert('请选择基础模型');
return;
}
if (!data.dataset_id) {
alert('请选择训练集');
return;
}
try {
const response = await fetch(`${API_BASE}/fine-tune`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.code === 0) {
alert('创建成功!');
window.location.href = 'main.html';
} else {
alert(result.message || '创建失败');
}
} catch (error) {
alert('创建失败: ' + error.message);
}
}
</script>
</body>
</html>

View File

@@ -1,83 +1,187 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<link rel="stylesheet" href="../css/login.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>远光软件微调平台 - 登录</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#1890ff',
sidebarBg: '#001529',
sidebarText: '#bfcbd9',
danger: '#f5222d',
success: '#52c41a',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.input-focus {
@apply focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none;
}
.login-card-shadow {
@apply shadow-lg shadow-primary/10 hover:shadow-xl hover:shadow-primary/15 transition-shadow;
}
}
</style>
</head>
<body>
<div class="svg-top">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="1337" width="1337">
<defs>
<path id="path-1" opacity="1" fill-rule="evenodd" d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" stop-color="rgb(88,62,213)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(23,215,250)" stop-opacity="1"/>
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1"/>
</g>
</svg>
</div>
<body class="antialiased bg-gray-50 min-h-screen flex items-center justify-center p-4">
<!-- 登录主容器 -->
<div class="w-full max-w-md">
<!-- 登录卡片 -->
<div class="bg-white rounded-xl p-8 login-card-shadow">
<!-- 平台LOGO和标题 -->
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 mb-4">
<img src="../assets/logo/logo.png" alt="Logo" class="w-12 h-12 object-contain">
</div>
<h1 class="text-[clamp(1.5rem,3vw,2rem)] font-medium text-gray-800 mb-1">远光软件微调平台</h1>
<p class="text-gray-500">模型管理平台</p>
</div>
<div class="svg-bottom">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="896" width="967.8852157128662">
<defs>
<path id="path-2" opacity="1" fill-rule="evenodd" d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"/>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" stop-color="rgb(40,175,240)" stop-opacity="1"/>
<stop offset="1" stop-color="rgb(18,15,196)" stop-opacity="1"/>
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1"/>
</g>
</svg>
</div>
<!-- 登录表单 -->
<form id="loginForm" class="space-y-5">
<!-- 账号输入框 -->
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">
<i class="fa fa-user-o mr-1 text-gray-400"></i>账号
</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
<i class="fa fa-user-o"></i>
</span>
<input
type="text"
id="username"
class="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg input-focus transition-colors"
placeholder="请输入账号/手机号/邮箱"
value="admin"
required
>
</div>
</div>
<section class="container">
<section class="wrapper">
<header>
<div class="logo">
<img src="../assets/logo/logo.png" alt="Logo">
<!-- 密码输入框 -->
<div>
<div class="flex items-center justify-between mb-1">
<label for="password" class="block text-sm font-medium text-gray-700">
<i class="fa fa-lock-o mr-1 text-gray-400"></i>密码
</label>
<a href="#" class="text-xs text-primary hover:text-primary/80 transition-colors">忘记密码?</a>
</div>
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
<i class="fa fa-lock-o"></i>
</span>
<input
type="password"
id="password"
class="w-full pl-10 pr-10 py-2.5 border border-gray-300 rounded-lg input-focus transition-colors"
placeholder="请输入密码"
value="admin"
required
>
<button type="button" id="togglePassword" class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 transition-colors">
<i class="fa fa-eye-slash"></i>
</button>
</div>
</div>
<!-- 记住密码 & 验证码登录 -->
<div class="flex items-center justify-between">
<label class="flex items-center text-sm text-gray-600 cursor-pointer">
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 focus:ring-primary">
<span class="ml-2">记住密码</span>
</label>
<a href="#" class="text-xs text-primary hover:text-primary/80 transition-colors">验证码登录</a>
</div>
<!-- 错误提示 -->
<div id="errorMsg" class="hidden text-danger text-sm bg-danger/5 p-2 rounded-lg">
<i class="fa fa-exclamation-circle mr-1"></i>
<span>账号或密码错误,请重新输入</span>
</div>
<!-- 登录按钮 -->
<button
type="submit"
class="w-full bg-primary text-white py-2.5 rounded-lg hover:bg-primary/90 active:bg-primary/95 transition-colors font-medium flex items-center justify-center"
>
<i class="fa fa-sign-in mr-2"></i>登 录
</button>
</form>
</div>
<h1>欢迎回来!</h1>
<p>用户登录</p>
</header>
<section class="main-content">
<form id="loginForm" onsubmit="handleLogin(event)">
<input type="text" placeholder="用户名" id="username">
<div class="line"></div>
<input type="password" placeholder="密码" id="password">
<button type="submit">登录</button>
</form>
</section>
<footer>
<p><a href="" title="忘记密码">忘记密码?</a></p>
<p><a href="" title="注册">注册</a></p>
</footer>
</section>
</section>
</div>
<script>
function handleLogin(event) {
event.preventDefault();
<!-- 简单的交互脚本 -->
<script>
// 动态获取 API 基础地址(根据当前访问的 IP 自动调整)
const getApiBase = () => {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
return `${protocol}//${hostname}:8080/api`;
};
const API_BASE = getApiBase();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// 密码显示/隐藏切换
const togglePassword = document.getElementById('togglePassword');
const password = document.getElementById('password');
// 简单的验证:用户名和密码都不为空
if (username && password) {
// 登录成功,设置登录状态
sessionStorage.setItem('isLoggedIn', 'true');
// 跳转到主页
window.location.href = 'main.html';
} else {
alert('请输入用户名和密码!');
}
}
</script>
togglePassword.addEventListener('click', () => {
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
// 切换图标
togglePassword.innerHTML = type === 'password' ? '<i class="fa fa-eye-slash"></i>' : '<i class="fa fa-eye"></i>';
});
// 表单提交处理
const loginForm = document.getElementById('loginForm');
const errorMsg = document.getElementById('errorMsg');
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const passwordVal = document.getElementById('password').value;
if (!username || !passwordVal) {
errorMsg.textContent = '账号和密码不能为空';
errorMsg.classList.remove('hidden');
return;
}
try {
const response = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password: passwordVal })
});
const result = await response.json();
if (result.code === 0) {
errorMsg.classList.add('hidden');
window.location.href = 'main.html';
} else {
errorMsg.textContent = result.message || '账号或密码错误';
errorMsg.classList.remove('hidden');
}
} catch (error) {
errorMsg.textContent = '无法连接到服务器,请确保后端服务已启动';
errorMsg.classList.remove('hidden');
}
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

16
web/run_web.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
# 启动前端 Web 服务器
cd "$(dirname "$0")"
echo "启动前端 Web 服务..."
echo "访问地址: http://localhost:8081"
echo ""
echo "请确保后端服务也在运行 (在另一个终端执行):"
echo " cd /data/code/FT_Platform/YG_FT_Platform/src"
echo " pip install -r ../requirements.txt"
echo " python3 main.py"
echo ""
# 使用 Python 内置 HTTP 服务器
python3 -m http.server 8081