新分支,重新设计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 远光软件微调平台 - Flask 后端 API
from typing import List, Optional """
import uvicorn
import os import os
import sys
import json import json
import re import pymysql
import time 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')
# 请求模型 def load_config():
class UserModel(BaseModel): """加载配置文件"""
username: str with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
password: str return yaml.safe_load(f)
class DatasetModel(BaseModel): CONFIG = load_config()
name: str
description: Optional[str] = None
size: str
class ModelConfigModel(BaseModel): def get_db_connection():
model_name: str """获取数据库连接"""
learning_rate: float db_config = CONFIG['database']
batch_size: int return pymysql.connect(
epochs: int host=db_config['host'],
port=db_config['port'],
user=db_config['username'],
# 响应模型 password=db_config['password'],
class ResponseModel(BaseModel): database=db_config['name'],
code: int charset=db_config.get('charset', 'utf8mb4'),
message: str cursorclass=pymysql.cursors.DictCursor
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) def init_database():
async def list_dataset_files(): """初始化数据库表"""
"""列出data目录中所有保存的数据集文件""" conn = get_db_connection()
try: cursor = conn.cursor()
data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data')
if not os.path.exists(data_dir): tables = [
return ResponseModel( # 精调训练表
code=200, """CREATE TABLE IF NOT EXISTS fine_tune (
message="获取成功", id INT AUTO_INCREMENT PRIMARY KEY,
data={"files": [], "total": 0, "directory": data_dir} 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
)""",
files = [] # 我的模型表
for filename in os.listdir(data_dir): """CREATE TABLE IF NOT EXISTS my_models (
file_path = os.path.join(data_dir, filename) id INT AUTO_INCREMENT PRIMARY KEY,
if os.path.isfile(file_path): name VARCHAR(255) NOT NULL,
stat = os.stat(file_path) type VARCHAR(100),
files.append({ version VARCHAR(50),
"filename": filename, description TEXT,
"size": stat.st_size, create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
"size_human": format_size(stat.st_size), update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
"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) """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
)""",
return ResponseModel( # 模型部署表
code=200, """CREATE TABLE IF NOT EXISTS model_deploy (
message="获取成功", id INT AUTO_INCREMENT PRIMARY KEY,
data={ model_name VARCHAR(255) NOT NULL,
"files": files, endpoint VARCHAR(255),
"total": len(files), instance VARCHAR(100),
"directory": data_dir status VARCHAR(50) DEFAULT 'running',
} create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
) update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
except Exception as e: )""",
raise HTTPException(
status_code=500, # 数据集管理表
detail=f"获取文件列表失败:{str(e)}" """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("数据库初始化完成")
def format_size(size_bytes): app = Flask(__name__)
"""格式化文件大小""" app.config['SECRET_KEY'] = CONFIG['secret_key']
if size_bytes < 1024: CORS(app, resources={r"/api/*": {"origins": "*"}})
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): @app.route('/api/health', methods=['GET'])
"""删除数据集""" def health_check():
global datasets """健康检查接口"""
for i, dataset in enumerate(datasets): return jsonify({'status': 'ok', 'code': 0})
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) # ============ 通用 CRUD 操作 ============
async def get_models(): def generic_get_all(table_name, order_by='create_time DESC'):
"""获取模型列表""" """通用查询所有"""
return ResponseModel(code=200, message="获取成功", data={"models": models}) 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.post("/api/models/config", response_model=ResponseModel) def generic_get_by_id(table_name, id_val):
async def config_model(config: ModelConfigModel): """通用按ID查询"""
"""配置模型参数""" conn = get_db_connection()
return ResponseModel( cursor = conn.cursor()
code=200, cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (id_val,))
message="配置成功", result = cursor.fetchone()
data={ cursor.close()
"model_name": config.model_name, conn.close()
"learning_rate": config.learning_rate, return result
"batch_size": config.batch_size,
"epochs": config.epochs,
"status": "已配置"
}
)
@app.get("/api/training/status") def generic_create(table_name, data):
async def get_training_status(): """通用创建"""
"""获取训练状态""" conn = get_db_connection()
return ResponseModel( cursor = conn.cursor()
code=200, columns = ', '.join(data.keys())
message="获取成功", placeholders = ', '.join(['%s'] * len(data))
data={ sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
"current_task": "GPT-4微调", cursor.execute(sql, list(data.values()))
"progress": 75, conn.commit()
"eta": "2小时", new_id = cursor.lastrowid
"loss": 0.23, cursor.close()
"accuracy": 0.89 conn.close()
} return new_id
)
@app.get("/api/system/stats") def generic_update(table_name, id_val, data):
async def get_system_stats(): """通用更新"""
"""获取系统统计信息""" conn = get_db_connection()
import random cursor = conn.cursor()
return ResponseModel( set_clause = ', '.join([f"{k} = %s" for k in data.keys()])
code=200, sql = f"UPDATE {table_name} SET {set_clause} WHERE id = %s"
message="获取成功", values = list(data.values()) + [id_val]
data={ cursor.execute(sql, values)
"cpu_usage": random.randint(30, 80), conn.commit()
"memory_usage": random.randint(40, 70), cursor.close()
"gpu_usage": random.randint(50, 90), conn.close()
"active_tasks": 5,
"completed_tasks": 158
}
)
@app.post("/api/training/start") def generic_delete(table_name, id_val):
async def start_training(model_name: str, dataset_id: int): """通用删除"""
"""开始训练任务""" conn = get_db_connection()
return ResponseModel( cursor = conn.cursor()
code=200, cursor.execute(f"DELETE FROM {table_name} WHERE id = %s", (id_val,))
message="训练任务已启动", conn.commit()
data={ cursor.close()
"task_id": random.randint(1000, 9999), conn.close()
"model_name": model_name,
"dataset_id": dataset_id,
"status": "running"
}
)
@app.post("/api/training/stop/{task_id}") # ============ 登录接口 ============
async def stop_training(task_id: int): @app.route('/api/login', methods=['POST'])
"""停止训练任务""" def login():
return ResponseModel( data = request.json
code=200, username = data.get('username')
message=f"训练任务 {task_id} 已停止", password = data.get('password')
data={"task_id": task_id, "status": "stopped"}
) 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.get("/api/model/{model_id}/metrics") # ============ 精调训练接口 ============
async def get_model_metrics(model_id: int): @app.route('/api/fine-tune', methods=['GET'])
"""获取模型指标""" def get_fine_tune():
return ResponseModel( return jsonify({'code': 0, 'data': generic_get_all('fine_tune')})
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)
}
)
if __name__ == "__main__": @app.route('/api/fine-tune', methods=['POST'])
uvicorn.run(app, host="0.0.0.0", port=8001) 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 #!/bin/bash
# 启动远光软件微调平台后端服务
echo "🚀 启动 FastAPI 服务器..." cd "$(dirname "$0")"
# 确保在正确的目录中 # 检查并安装依赖
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if ! python3 -c "import flask" 2>/dev/null; then
cd "$SCRIPT_DIR" echo "正在安装依赖..."
pip install -r ../requirements.txt
echo "📂 当前目录: $SCRIPT_DIR"
# 检查Python是否安装
if ! command -v python3 &> /dev/null; then
echo "❌ 错误: Python3 未安装"
echo "请先安装 Python3"
exit 1
fi fi
# 检查pip是否安装 # 启动服务
if ! command -v pip3 &> /dev/null; then echo "启动后端服务..."
echo "❌ 错误: pip3 未安装" python3 main.py
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

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

@@ -3,81 +3,185 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title> <title>远光软件微调平台 - 登录</title>
<link rel="stylesheet" href="../css/login.css"> <script src="https://cdn.tailwindcss.com"></script>
</head> <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<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>
<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>
<section class="container">
<section class="wrapper">
<header>
<div class="logo">
<img src="../assets/logo/logo.png" alt="Logo">
</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>
<script> <script>
function handleLogin(event) { tailwind.config = {
event.preventDefault(); theme: {
extend: {
const username = document.getElementById('username').value; colors: {
const password = document.getElementById('password').value; primary: '#1890ff',
sidebarBg: '#001529',
// 简单的验证:用户名和密码都不为空 sidebarText: '#bfcbd9',
if (username && password) { danger: '#f5222d',
// 登录成功,设置登录状态 success: '#52c41a',
sessionStorage.setItem('isLoggedIn', 'true'); },
// 跳转到主页 fontFamily: {
window.location.href = 'main.html'; sans: ['Inter', 'system-ui', 'sans-serif'],
} else { },
alert('请输入用户名和密码!'); }
} }
} }
</script> </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 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>
<!-- 登录表单 -->
<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>
<!-- 密码输入框 -->
<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>
</div>
<!-- 简单的交互脚本 -->
<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 togglePassword = document.getElementById('togglePassword');
const password = document.getElementById('password');
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> </body>
</html> </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