第一次提交
This commit is contained in:
709
main.py
Normal file
709
main.py
Normal file
@@ -0,0 +1,709 @@
|
||||
"""
|
||||
FastAPI 主入口
|
||||
提供 RESTful API 和 SSE 流式接口
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crew_factory import CrewFactory, run_multi_agent_task
|
||||
from stream_manager import stream_manager, create_sse_generator
|
||||
|
||||
|
||||
# ==================== 数据模型 ====================
|
||||
|
||||
class RunTaskRequest(BaseModel):
|
||||
"""运行任务请求体"""
|
||||
user_requirement: str = Field(
|
||||
...,
|
||||
description="用户需求描述",
|
||||
example="开发一个在线商城系统,支持用户注册、商品浏览、购物车和支付功能"
|
||||
)
|
||||
skip_confirmation: bool = Field(
|
||||
default=True,
|
||||
description="是否跳过 Coordinator 的人工确认环节"
|
||||
)
|
||||
|
||||
|
||||
class RunTaskResponse(BaseModel):
|
||||
"""运行任务响应体"""
|
||||
task_id: str
|
||||
status: str
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class SSEEvent(BaseModel):
|
||||
"""SSE 事件数据结构"""
|
||||
agent: str
|
||||
type: str
|
||||
content: str
|
||||
timestamp: str
|
||||
task_id: str
|
||||
|
||||
|
||||
# ==================== 生命周期管理 ====================
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时执行
|
||||
print("🚀 Multi-Agent System 启动中...")
|
||||
print("📡 SSE 流服务已就绪")
|
||||
|
||||
# 启动后台任务:定期清理旧流
|
||||
asyncio.create_task(cleanup_streams_periodically())
|
||||
|
||||
yield
|
||||
|
||||
# 关闭时清理
|
||||
print("👋 正在关闭所有 SSE 流...")
|
||||
# 可以在这里添加清理逻辑
|
||||
|
||||
|
||||
# ==================== FastAPI 应用 ====================
|
||||
|
||||
app = FastAPI(
|
||||
title="Multi-Agent Software Delivery System",
|
||||
description="""
|
||||
基于 CrewAI + Qwen3.5-flash 的多智能体软件交付系统
|
||||
|
||||
## 核心功能
|
||||
- **产品需求分析**: ProductManager Agent 分析用户需求,生成 PRD
|
||||
- **测试计划制定**: QAEngineer Agent 设计测试策略和用例
|
||||
- **技术方案设计**: SoftwareDeveloper Agent 输出架构设计和代码框架
|
||||
- **质量审核**: Coordinator Agent 审核所有产出物并生成交付报告
|
||||
|
||||
## 实时通信
|
||||
支持 SSE (Server-Sent Events) 协议,实时推送每个 Agent 的思考过程和任务状态
|
||||
|
||||
## API 端点
|
||||
- `POST /api/run_task` - 启动新任务
|
||||
- `GET /api/stream/{task_id}` - 订阅任务执行日志(SSE)
|
||||
- `GET /api/task/{task_id}/status` - 查询任务状态
|
||||
- `GET /test-ui` - 测试页面
|
||||
""",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# CORS 中间件(允许跨域)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 生产环境应限制具体域名
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ==================== 后台任务 ====================
|
||||
|
||||
async def cleanup_streams_periodically(interval: int = 300):
|
||||
"""定期清理旧的流(每 5 分钟)"""
|
||||
while True:
|
||||
await asyncio.sleep(interval)
|
||||
try:
|
||||
await stream_manager.cleanup_old_streams(max_age_seconds=3600)
|
||||
except Exception as e:
|
||||
print(f"清理流失败:{e}")
|
||||
|
||||
|
||||
# ==================== API 路由 ====================
|
||||
|
||||
@app.post(
|
||||
"/api/run_task",
|
||||
response_model=RunTaskResponse,
|
||||
summary="启动多智能体任务",
|
||||
description="接收用户需求,启动 CrewAI 流程,异步执行并立即返回 task_id"
|
||||
)
|
||||
async def run_task(request: RunTaskRequest):
|
||||
"""
|
||||
启动新的多智能体任务
|
||||
|
||||
- **user_requirement**: 用户需求描述
|
||||
- **skip_confirmation**: 是否跳过人工确认(默认 True)
|
||||
|
||||
返回 task_id 用于后续 SSE 流订阅
|
||||
"""
|
||||
try:
|
||||
# 生成 task_id 并启动任务
|
||||
task_id = await run_multi_agent_task(
|
||||
user_requirement=request.user_requirement,
|
||||
skip_confirmation=request.skip_confirmation
|
||||
)
|
||||
|
||||
return RunTaskResponse(
|
||||
task_id=task_id,
|
||||
status="started",
|
||||
message="任务已启动,请通过 /api/stream/{task_id} 订阅执行日志"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"启动任务失败:{str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@app.get(
|
||||
"/api/stream/{task_id}",
|
||||
summary="订阅任务执行日志 (SSE)",
|
||||
description="建立 SSE 连接,实时接收任务执行过程中的所有事件"
|
||||
)
|
||||
async def stream_task_logs(task_id: str):
|
||||
"""
|
||||
SSE 端点 - 实时推送任务执行日志
|
||||
|
||||
事件类型包括:
|
||||
- **start**: 任务开始
|
||||
- **agent_start**: Agent 开始执行
|
||||
- **thought**: Agent 思考过程
|
||||
- **action**: Agent 执行动作
|
||||
- **output**: Agent 输出结果
|
||||
- **step_end**: 步骤完成
|
||||
- **end**: 任务结束
|
||||
- **error**: 发生错误
|
||||
|
||||
数据格式:
|
||||
```json
|
||||
{
|
||||
"agent": "ProductManager",
|
||||
"type": "thought",
|
||||
"content": "正在分析用户需求...",
|
||||
"timestamp": "2024-01-01T12:00:00",
|
||||
"task_id": "uuid"
|
||||
}
|
||||
```
|
||||
"""
|
||||
# 检查任务是否存在
|
||||
stream = await stream_manager.get_stream(task_id)
|
||||
if stream is None:
|
||||
# 任务不存在,返回错误
|
||||
return StreamingResponse(
|
||||
iter([f"data: {json.dumps({'error': 'Task not found', 'task_id': task_id})}\n\n"]),
|
||||
media_type="text/event-stream"
|
||||
)
|
||||
|
||||
# 创建 SSE 流
|
||||
return StreamingResponse(
|
||||
create_sse_generator(task_id),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no", # Nginx 禁用缓冲
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get(
|
||||
"/api/task/{task_id}/status",
|
||||
summary="查询任务状态",
|
||||
description="获取任务的当前状态和基本信息"
|
||||
)
|
||||
async def get_task_status(task_id: str):
|
||||
"""查询任务状态"""
|
||||
stream = await stream_manager.get_stream(task_id)
|
||||
|
||||
if stream is None:
|
||||
return {"task_id": task_id, "status": "not_found"}
|
||||
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": "closed" if stream.is_closed else "running",
|
||||
"queue_size": stream.queue.qsize() if hasattr(stream, 'queue') else 0,
|
||||
}
|
||||
|
||||
|
||||
@app.get(
|
||||
"/api/streams",
|
||||
summary="列出所有活跃流",
|
||||
description="查看当前所有正在执行的任务"
|
||||
)
|
||||
async def list_active_streams():
|
||||
"""列出所有活跃的 SSE 流"""
|
||||
streams = stream_manager.list_active_streams()
|
||||
return {
|
||||
"total": len(streams),
|
||||
"streams": streams
|
||||
}
|
||||
|
||||
|
||||
@app.get(
|
||||
"/health",
|
||||
summary="健康检查",
|
||||
description="检查服务是否正常运行"
|
||||
)
|
||||
async def health_check():
|
||||
"""健康检查端点"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"service": "Multi-Agent Software Delivery System"
|
||||
}
|
||||
|
||||
|
||||
# ==================== 测试页面 ====================
|
||||
|
||||
@app.get(
|
||||
"/test-ui",
|
||||
response_class=HTMLResponse,
|
||||
summary="测试 UI 页面",
|
||||
description="一个简单的 HTML 页面,用于测试 SSE 流功能"
|
||||
)
|
||||
async def test_ui():
|
||||
"""返回测试页面"""
|
||||
return HTMLResponse(content=get_test_page_html())
|
||||
|
||||
|
||||
def get_test_page_html() -> str:
|
||||
"""生成测试页面 HTML"""
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>多智能体系统 - 测试 UI</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 25px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-idle { background: #ccc; }
|
||||
.status-running { background: #4caf50; animation: pulse 1s infinite; }
|
||||
.status-error { background: #f44336; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.event-log {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.event-item {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-left: 3px solid;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.event-start { border-color: #2196f3; }
|
||||
.event-thought { border-color: #ff9800; }
|
||||
.event-output { border-color: #4caf50; }
|
||||
.event-end { border-color: #9c27b0; }
|
||||
.event-error { border-color: #f44336; background: rgba(244, 67, 54, 0.1); }
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.event-agent {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.event-content {
|
||||
color: #d4d4d4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: #f44336;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.example-requirements {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.example-btn {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.example-btn:hover {
|
||||
background: #e0e0e0;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🤖 多智能体软件交付系统</h1>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="card">
|
||||
<div class="input-group">
|
||||
<label for="requirement">用户需求描述:</label>
|
||||
<textarea
|
||||
id="requirement"
|
||||
placeholder="请输入您的需求,例如:开发一个在线商城系统,支持用户注册、商品浏览、购物车和支付功能..."
|
||||
></textarea>
|
||||
|
||||
<div class="example-requirements">
|
||||
<strong>示例需求:</strong><br>
|
||||
<button class="example-btn" onclick="useExample('mall')">在线商城</button>
|
||||
<button class="example-btn" onclick="useExample('blog')">博客系统</button>
|
||||
<button class="example-btn" onclick="useExample('task')">任务管理</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="skipConfirmation" checked>
|
||||
<label for="skipConfirmation" style="margin: 0;">跳过人工确认环节</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="startBtn" onclick="startTask()">🚀 启动任务</button>
|
||||
</div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div class="card">
|
||||
<div class="status-bar">
|
||||
<div>
|
||||
<span class="status-indicator status-idle" id="statusIndicator"></span>
|
||||
<span id="statusText">等待中...</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Task ID:</strong> <span id="taskIdDisplay">-</span>
|
||||
</div>
|
||||
<button class="clear-btn" onclick="clearLog()">清空日志</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 事件日志 -->
|
||||
<div class="card">
|
||||
<h2 style="margin-bottom: 15px;">📊 实时事件日志</h2>
|
||||
<div class="event-log" id="eventLog">
|
||||
<div style="color: #666; text-align: center; padding: 40px;">
|
||||
暂无事件,点击上方"启动任务"按钮开始
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentTaskId = null;
|
||||
let eventSource = null;
|
||||
|
||||
// 示例需求
|
||||
const examples = {
|
||||
mall: '开发一个在线商城系统,支持用户注册登录、商品分类浏览、搜索功能、购物车管理、订单创建和支付宝/微信支付集成。需要包含后台管理系统用于商品上架和订单处理。',
|
||||
blog: '构建一个个人博客系统,支持 Markdown 编辑器、文章分类和标签、评论功能、SEO 优化。还需要有访客统计、文章阅读量统计,以及响应式设计适配移动端。',
|
||||
task: '创建一个团队协作任务管理工具,类似简化版 Jira。支持创建项目、分解任务、分配负责人、设置优先级和截止日期、进度跟踪。需要有看板视图和甘特图展示。'
|
||||
};
|
||||
|
||||
function useExample(type) {
|
||||
document.getElementById('requirement').value = examples[type];
|
||||
}
|
||||
|
||||
function updateStatus(status, text) {
|
||||
const indicator = document.getElementById('statusIndicator');
|
||||
indicator.className = `status-indicator status-${status}`;
|
||||
document.getElementById('statusText').textContent = text;
|
||||
}
|
||||
|
||||
function addEventToLog(event) {
|
||||
const log = document.getElementById('eventLog');
|
||||
|
||||
// 清除初始提示
|
||||
if (log.querySelector('[style*="text-align: center"]')) {
|
||||
log.innerHTML = '';
|
||||
}
|
||||
|
||||
const eventDiv = document.createElement('div');
|
||||
eventDiv.className = `event-item event-${event.type}`;
|
||||
|
||||
const time = new Date(event.timestamp).toLocaleTimeString();
|
||||
|
||||
eventDiv.innerHTML = `
|
||||
<div class="event-header">
|
||||
<span class="event-agent">${escapeHtml(event.agent)}</span>
|
||||
<span>
|
||||
<span class="event-type">${event.type}</span>
|
||||
<span style="margin-left: 10px;">${time}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="event-content">${escapeHtml(event.content)}</div>
|
||||
`;
|
||||
|
||||
log.appendChild(eventDiv);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function startTask() {
|
||||
const requirement = document.getElementById('requirement').value.trim();
|
||||
if (!requirement) {
|
||||
alert('请输入用户需求描述');
|
||||
return;
|
||||
}
|
||||
|
||||
const skipConfirmation = document.getElementById('skipConfirmation').checked;
|
||||
|
||||
try {
|
||||
updateStatus('running', '任务启动中...');
|
||||
document.getElementById('startBtn').disabled = true;
|
||||
|
||||
const response = await fetch('/api/run_task', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_requirement: requirement,
|
||||
skip_confirmation: skipConfirmation
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
currentTaskId = data.task_id;
|
||||
document.getElementById('taskIdDisplay').textContent = currentTaskId;
|
||||
|
||||
// 连接 SSE
|
||||
connectSSE(currentTaskId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动任务失败:', error);
|
||||
updateStatus('error', '启动失败:' + error.message);
|
||||
document.getElementById('startBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function connectSSE(taskId) {
|
||||
// 关闭旧的连接
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
const eventSourceUrl = `/api/stream/${taskId}`;
|
||||
eventSource = new EventSource(eventSourceUrl);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('SSE 连接已建立');
|
||||
updateStatus('running', '任务执行中...');
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// 忽略连接结束事件
|
||||
if (data.type === 'connection_end') {
|
||||
return;
|
||||
}
|
||||
|
||||
addEventToLog(data);
|
||||
|
||||
// 如果是结束或错误事件,更新状态
|
||||
if (data.type === 'end') {
|
||||
updateStatus('idle', '任务已完成');
|
||||
document.getElementById('startBtn').disabled = false;
|
||||
eventSource.close();
|
||||
} else if (data.type === 'error') {
|
||||
updateStatus('error', '发生错误');
|
||||
document.getElementById('startBtn').disabled = false;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('解析事件数据失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('SSE 错误:', error);
|
||||
// 不立即显示错误,等待服务端发送的 error 事件
|
||||
};
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('eventLog').innerHTML = `
|
||||
<div style="color: #666; text-align: center; padding: 40px;">
|
||||
暂无事件,点击上方"启动任务"按钮开始
|
||||
</div>
|
||||
`;
|
||||
currentTaskId = null;
|
||||
document.getElementById('taskIdDisplay').textContent = '-';
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
updateStatus('idle', '等待中...');
|
||||
document.getElementById('startBtn').disabled = false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
# ==================== 主程序入口 ====================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# 加载环境变量
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
Reference in New Issue
Block a user