Trae Assistant commited on
Commit
6ff4e1a
·
0 Parent(s):

Enhance UI, add file upload, localize to Chinese, and optimize agent logic

Browse files
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用轻量级 Python 基础镜像
2
+ FROM python:3.11-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装系统依赖
8
+ RUN apt-get update && apt-get install -y \
9
+ build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # 复制依赖文件
13
+ COPY requirements.txt .
14
+
15
+ # 安装 Python 依赖
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # 复制项目代码
19
+ COPY . .
20
+
21
+ # 暴露端口
22
+ EXPOSE 7860
23
+
24
+ # 启动命令
25
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Stellar Compute Agent
3
+ emoji: 🛰️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ short_description: 星际算力调度智能体 - 低轨卫星边缘计算与任务自动化管理系统
9
+ ---
10
+
11
+ # 星际算力调度智能体 (Stellar Compute Agent)
12
+
13
+ ## 项目简介
14
+ 星际算力调度智能体是一个面向未来“太空算力”趋势的自动化管理系统。它模拟并实现了低轨卫星(LEO)星座的边缘计算资源调度、空间环境监测、任务自动化执行及闭环验证。
15
+
16
+ 该项目采用 **LangGraph** 构建核心决策大脑,通过 **ReAct** (Reasoning and Acting) 模式实现多智能体协同,能够根据复杂的自然语言指令,自主调用卫星状态查询、算力分配、轨道天气检查等工具,确保太空任务的高效与安全。
17
+
18
+ ## 核心特性
19
+ - **🤖 闭环决策系统**:集成“推理/决策 + 工具行动 + 状态/记忆 + 校验/验收 + 迭代/回放”的完整循环。
20
+ - **🛰️ 太空算力管理**:模拟卫星星座的实时电量、负载及轨道位置管理。
21
+ - **⛈️ 空间天气感知**:动态响应太阳风暴、空间碎片等异常环境,调整调度策略。
22
+ - **📱 响应式 UI**:适配移动端,提供现代化的“任务控制中心”交互体验。
23
+ - **🧠 强大的 AI 驱动**:接入硅基流动 (SiliconFlow) 的高性能大模型(如 DeepSeek-V3)。
24
+ - **💾 数据持久化**:所有任务轨迹、卫星状态及审计日志均保存在 SQLite 数据库中。
25
+
26
+ ## 快速启动
27
+
28
+ ### 1. 环境准备
29
+ 确保已安装 Python 3.9+,并获取 SiliconFlow API Key。
30
+
31
+ ### 2. 安装依赖
32
+ ```bash
33
+ pip install -r requirements.txt
34
+ ```
35
+
36
+ ### 3. 运行项目
37
+ ```bash
38
+ python app.py
39
+ ```
40
+ 访问 `http://localhost:8001` 即可进入控制台。
41
+
42
+ ### 4. Docker 运行
43
+ ```bash
44
+ docker build -t stellar-compute-agent .
45
+ docker run -p 8001:8001 stellar-compute-agent
46
+ ```
47
+
48
+ ## 技术架构
49
+ - **后端**: FastAPI, Uvicorn
50
+ - **Agent 框架**: LangChain, LangGraph (StateGraph)
51
+ - **大模型**: DeepSeek-V3 (via SiliconFlow)
52
+ - **前端**: HTML5, Tailwind CSS, Marked.js (Markdown 解析)
53
+ - **存储**: SQLite3
54
+
55
+ ## 开发者
56
+ 由 AI 助手自动构建,专注于未来趋势与生产力工具。
app.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import asyncio
4
+ import shutil
5
+ from typing import List
6
+ from fastapi import FastAPI, Request, Form, UploadFile, File, HTTPException
7
+ from fastapi.responses import HTMLResponse, JSONResponse
8
+ from fastapi.staticfiles import StaticFiles
9
+ from fastapi.templating import Jinja2Templates
10
+ from core.agent import StellarAgent
11
+ from core.database import StellarDB
12
+
13
+ app = FastAPI(title="Stellar Compute Agent")
14
+ db = StellarDB()
15
+ agent = StellarAgent()
16
+
17
+ # 挂载静态文件
18
+ os.makedirs("static", exist_ok=True)
19
+ os.makedirs("uploads", exist_ok=True)
20
+ app.mount("/static", StaticFiles(directory="static"), name="static")
21
+ templates = Jinja2Templates(directory="templates")
22
+
23
+ @app.get("/", response_class=HTMLResponse)
24
+ async def index(request: Request):
25
+ try:
26
+ sats = db.get_satellites()
27
+ tasks = db.get_tasks()
28
+ return templates.TemplateResponse("index.html", {
29
+ "request": request,
30
+ "sats": sats,
31
+ "tasks": tasks
32
+ })
33
+ except Exception as e:
34
+ return HTMLResponse(content=f"<html><body><h1>Internal Server Error</h1><p>{str(e)}</p></body></html>", status_code=500)
35
+
36
+ @app.post("/mission")
37
+ async def create_mission(prompt: str = Form(...)):
38
+ try:
39
+ task_id = str(uuid.uuid4())[:8]
40
+ # 在后台运行任务
41
+ result, logs = await agent.run_mission(task_id, prompt)
42
+ return {"task_id": task_id, "result": result, "logs": logs}
43
+ except Exception as e:
44
+ raise HTTPException(status_code=500, detail=str(e))
45
+
46
+ @app.post("/upload")
47
+ async def upload_file(file: UploadFile = File(...)):
48
+ try:
49
+ file_path = os.path.join("uploads", file.filename)
50
+ with open(file_path, "wb") as buffer:
51
+ shutil.copyfileobj(file.file, buffer)
52
+ return {"filename": file.filename, "status": "success", "path": file_path}
53
+ except Exception as e:
54
+ return JSONResponse(status_code=500, content={"message": f"上传失败: {str(e)}"})
55
+
56
+ @app.get("/api/tasks")
57
+ async def get_tasks():
58
+ return db.get_tasks()
59
+
60
+ @app.get("/api/satellites")
61
+ async def get_satellites():
62
+ return db.get_satellites()
63
+
64
+ @app.get("/api/stats")
65
+ async def get_stats():
66
+ sats = db.get_satellites()
67
+ labels = [s['name'] for s in sats]
68
+ loads = [s['compute_load'] for s in sats]
69
+ batteries = [s['battery'] for s in sats]
70
+ return {
71
+ "labels": labels,
72
+ "loads": loads,
73
+ "batteries": batteries
74
+ }
75
+
76
+ if __name__ == "__main__":
77
+ import uvicorn
78
+ port = int(os.environ.get("PORT", 7860))
79
+ uvicorn.run(app, host="0.0.0.0", port=port)
core/__pycache__/agent.cpython-314.pyc ADDED
Binary file (6.35 kB). View file
 
core/__pycache__/database.cpython-314.pyc ADDED
Binary file (5.77 kB). View file
 
core/__pycache__/tools.cpython-314.pyc ADDED
Binary file (3.14 kB). View file
 
core/agent.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from typing import Annotated, TypedDict, List, Union
4
+ from langchain_openai import ChatOpenAI
5
+ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
6
+ from langgraph.graph import StateGraph, END
7
+ from langgraph.prebuilt import ToolNode
8
+ from core.tools import AVAILABLE_TOOLS, query_satellite_status, allocate_compute_task, check_orbital_weather, verify_mission_result
9
+ from langchain_core.utils.function_calling import convert_to_openai_function
10
+ from core.database import StellarDB
11
+
12
+ # 配置 SiliconFlow API
13
+ os.environ["OPENAI_API_KEY"] = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
14
+ os.environ["OPENAI_API_BASE"] = "https://api.siliconflow.cn/v1"
15
+
16
+ db = StellarDB()
17
+
18
+ class AgentState(TypedDict):
19
+ messages: Annotated[List[BaseMessage], lambda x, y: x + y]
20
+ task_id: str
21
+
22
+ # 初始化模型
23
+ llm = ChatOpenAI(
24
+ model="deepseek-ai/DeepSeek-V3",
25
+ temperature=0.7,
26
+ )
27
+
28
+ # 绑定工具
29
+ tools = [query_satellite_status, allocate_compute_task, check_orbital_weather, verify_mission_result]
30
+ llm_with_tools = llm.bind_tools(tools)
31
+
32
+ def call_model(state: AgentState):
33
+ messages = state["messages"]
34
+ response = llm_with_tools.invoke(messages)
35
+ return {"messages": [response]}
36
+
37
+ def should_continue(state: AgentState):
38
+ messages = state["messages"]
39
+ last_message = messages[-1]
40
+ if last_message.tool_calls:
41
+ return "tools"
42
+ return END
43
+
44
+ # 构建图
45
+ workflow = StateGraph(AgentState)
46
+
47
+ workflow.add_node("agent", call_model)
48
+ workflow.add_node("tools", ToolNode(tools))
49
+
50
+ workflow.set_entry_point("agent")
51
+ workflow.add_conditional_edges(
52
+ "agent",
53
+ should_continue,
54
+ )
55
+ workflow.add_edge("tools", "agent")
56
+
57
+ app_graph = workflow.compile()
58
+
59
+ class StellarAgent:
60
+ def __init__(self):
61
+ self.graph = app_graph
62
+
63
+ async def run_mission(self, task_id: str, prompt: str):
64
+ # 初始系统消息
65
+ system_msg = (
66
+ "你是一个名为 Stellar-Commander 的太空算力调度智能体。你的任务是管理低轨卫星星座的计算任务分配、"
67
+ "监控空间环境以及验证任务结果。你可以使用工具来查询状态、分配任务、检查天气和验证结果。"
68
+ "请始终用中文回答。你的回答应该专业、高效,并始终以保障卫星安全和最大化算力产出为目标。"
69
+ "在执行任务前,先通过工具了解现状。如果有附件信息,请一并考虑。"
70
+ )
71
+
72
+ inputs = {
73
+ "messages": [
74
+ {"role": "system", "content": system_msg},
75
+ {"role": "human", "content": prompt}
76
+ ],
77
+ "task_id": task_id
78
+ }
79
+
80
+ full_log = [f"System: 正在初始化任务序列 #{task_id}...", f"System: 接收到指令: {prompt}"]
81
+ final_result = ""
82
+
83
+ async for event in self.graph.astream(inputs, config={"configurable": {"thread_id": task_id}}):
84
+ for value in event.values():
85
+ if "messages" in value:
86
+ msg = value["messages"][-1]
87
+ if isinstance(msg, AIMessage):
88
+ if msg.content:
89
+ full_log.append(f"Commander: {msg.content}")
90
+ final_result = msg.content
91
+ if msg.tool_calls:
92
+ for tc in msg.tool_calls:
93
+ full_log.append(f"Decision: 调用工具 {tc['name']} 进行分析,参数: {json.dumps(tc['args'], ensure_ascii=False)}")
94
+ elif isinstance(msg, ToolMessage):
95
+ full_log.append(f"Action Result: 工具反馈数据 -> {msg.content}")
96
+
97
+ # 持久化
98
+ db.save_task(task_id, prompt, "Completed", final_result, "\n".join(full_log))
99
+ return final_result, full_log
core/database.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import json
3
+ from datetime import datetime
4
+
5
+ class StellarDB:
6
+ def __init__(self, db_path="stellar.db"):
7
+ self.db_path = db_path
8
+ self._init_db()
9
+
10
+ def _init_db(self):
11
+ with sqlite3.connect(self.db_path) as conn:
12
+ cursor = conn.cursor()
13
+ # 卫星状态表
14
+ cursor.execute('''
15
+ CREATE TABLE IF NOT EXISTS satellites (
16
+ id TEXT PRIMARY KEY,
17
+ name TEXT,
18
+ status TEXT,
19
+ battery INTEGER,
20
+ compute_load INTEGER,
21
+ last_updated TIMESTAMP
22
+ )
23
+ ''')
24
+ # 任务记录表
25
+ cursor.execute('''
26
+ CREATE TABLE IF NOT EXISTS tasks (
27
+ id TEXT PRIMARY KEY,
28
+ description TEXT,
29
+ status TEXT,
30
+ result TEXT,
31
+ agent_log TEXT,
32
+ created_at TIMESTAMP
33
+ )
34
+ ''')
35
+ # 初始卫星数据
36
+ satellites = [
37
+ ('SAT-001', 'Stellar-Alpha (领航者)', 'Orbiting', 85, 10, datetime.now()),
38
+ ('SAT-002', 'Stellar-Beta (守望者)', 'Orbiting', 92, 5, datetime.now()),
39
+ ('SAT-003', 'Stellar-Gamma (开拓者)', 'Maintenance', 45, 0, datetime.now()),
40
+ ('SAT-004', 'Stellar-Delta (中继站)', 'Orbiting', 78, 25, datetime.now()),
41
+ ('SAT-005', 'Stellar-Epsilon (监测站)', 'Orbiting', 60, 40, datetime.now()),
42
+ ]
43
+ cursor.executemany('INSERT OR IGNORE INTO satellites VALUES (?,?,?,?,?,?)', satellites)
44
+ conn.commit()
45
+
46
+ def get_satellites(self):
47
+ with sqlite3.connect(self.db_path) as conn:
48
+ conn.row_factory = sqlite3.Row
49
+ cursor = conn.cursor()
50
+ cursor.execute('SELECT * FROM satellites')
51
+ return [dict(row) for row in cursor.fetchall()]
52
+
53
+ def update_satellite(self, sat_id, status=None, battery=None, compute_load=None):
54
+ with sqlite3.connect(self.db_path) as conn:
55
+ cursor = conn.cursor()
56
+ if status: cursor.execute('UPDATE satellites SET status = ? WHERE id = ?', (status, sat_id))
57
+ if battery is not None: cursor.execute('UPDATE satellites SET battery = ? WHERE id = ?', (battery, sat_id))
58
+ if compute_load is not None: cursor.execute('UPDATE satellites SET compute_load = ? WHERE id = ?', (compute_load, sat_id))
59
+ cursor.execute('UPDATE satellites SET last_updated = ? WHERE id = ?', (datetime.now(), sat_id))
60
+ conn.commit()
61
+
62
+ def save_task(self, task_id, description, status, result="", agent_log=""):
63
+ with sqlite3.connect(self.db_path) as conn:
64
+ cursor = conn.cursor()
65
+ cursor.execute('''
66
+ INSERT OR REPLACE INTO tasks (id, description, status, result, agent_log, created_at)
67
+ VALUES (?, ?, ?, ?, ?, ?)
68
+ ''', (task_id, description, status, result, agent_log, datetime.now()))
69
+ conn.commit()
70
+
71
+ def get_tasks(self):
72
+ with sqlite3.connect(self.db_path) as conn:
73
+ conn.row_factory = sqlite3.Row
74
+ cursor = conn.cursor()
75
+ cursor.execute('SELECT * FROM tasks ORDER BY created_at DESC')
76
+ return [dict(row) for row in cursor.fetchall()]
core/tools.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from core.database import StellarDB
2
+ import random
3
+
4
+ db = StellarDB()
5
+
6
+ def query_satellite_status():
7
+ """查询所有卫星的当前状态、电量和计算负载。"""
8
+ sats = db.get_satellites()
9
+ status_str = "当前卫星星座状态:\n"
10
+ for s in sats:
11
+ status_str += f"- {s['name']} ({s['id']}): 状态={s['status']}, 电量={s['battery']}%, 负载={s['compute_load']}%\n"
12
+ return status_str
13
+
14
+ def allocate_compute_task(task_description, target_sat_id):
15
+ """将计算任务分配给指定的卫星。"""
16
+ sats = db.get_satellites()
17
+ target = next((s for s in sats if s['id'] == target_sat_id), None)
18
+
19
+ if not target:
20
+ return f"错误: 找不到卫星 {target_sat_id}"
21
+
22
+ if target['status'] != 'Orbiting':
23
+ return f"错误: 卫星 {target_sat_id} 当前处于 {target['status']} 状态,无法处理任务。"
24
+
25
+ if target['battery'] < 20:
26
+ return f"警告: 卫星 {target_sat_id} 电量过低 ({target['battery']}%),任务分配失败。"
27
+
28
+ # 模拟分配逻辑
29
+ new_load = min(100, target['compute_load'] + 25)
30
+ db.update_satellite(target_sat_id, compute_load=new_load)
31
+
32
+ return f"成功: 任务 '{task_description}' 已分配给 {target_sat_id}。当前负载增加至 {new_load}%。"
33
+
34
+ def check_orbital_weather():
35
+ """检查当前的轨道空间天气,如太阳活动、碎片风险等。"""
36
+ weathers = ["正常", "强太阳风暴", "空间碎片预警", "电离层干扰"]
37
+ current = random.choice(weathers)
38
+ if current == "正常":
39
+ return "空间天气状况良好,适合进行高强度计算任务。"
40
+ else:
41
+ return f"警告: 当前空间天气为 '{current}',建议推迟非必要任务或降低负载。"
42
+
43
+ def verify_mission_result(task_id):
44
+ """校验任务执行结果。"""
45
+ return f"任务 {task_id} 结果校验完成: 100% 匹配预期数据模型。"
46
+
47
+ # 工具映射表
48
+ AVAILABLE_TOOLS = {
49
+ "query_satellite_status": query_satellite_status,
50
+ "allocate_compute_task": allocate_compute_task,
51
+ "check_orbital_weather": check_orbital_weather,
52
+ "verify_mission_result": verify_mission_result
53
+ }
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ langchain
4
+ langchain-openai
5
+ langgraph
6
+ pydantic
7
+ jinja2
8
+ python-multipart
9
+ requests
stellar.db ADDED
Binary file (20.5 kB). View file
 
templates/index.html ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Stellar Compute - 太空算力调度智能体</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
11
+ <style>
12
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
13
+ body { font-family: 'Space Grotesk', 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: #050505; color: #e5e5e5; }
14
+ .glass { background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); }
15
+ .accent-blue { color: #3b82f6; }
16
+ .bg-accent-blue { background-color: #3b82f6; }
17
+ .prose { color: #d1d5db; }
18
+ .prose h1, .prose h2, .prose h3 { color: #fff; margin-top: 1em; margin-bottom: 0.5em; }
19
+ .prose p { margin-bottom: 1em; line-height: 1.6; }
20
+ .status-dot { height: 8px; width: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
21
+ .orbiting { background-color: #10b981; box-shadow: 0 0 8px #10b981; }
22
+ .maintenance { background-color: #f59e0b; box-shadow: 0 0 8px #f59e0b; }
23
+
24
+ /* 隐藏滚动条但保留功能 */
25
+ .no-scrollbar::-webkit-scrollbar { display: none; }
26
+ .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
27
+ </style>
28
+ </head>
29
+ <body class="min-h-screen">
30
+ <!-- 导航栏 -->
31
+ <nav class="p-4 border-b border-white/10 glass sticky top-0 z-50">
32
+ <div class="max-w-7xl mx-auto flex justify-between items-center">
33
+ <div class="flex items-center space-x-3">
34
+ <i class="fas fa-satellite-dish text-2xl text-blue-500"></i>
35
+ <span class="text-xl font-bold tracking-tighter">STELLAR COMPUTE</span>
36
+ <span class="text-xs bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded border border-blue-500/30">V2.0 升级版</span>
37
+ </div>
38
+ <div class="hidden md:flex space-x-8 text-sm font-medium">
39
+ <a href="#" class="text-blue-400 border-b-2 border-blue-400 pb-1">控制台</a>
40
+ <a href="#" class="hover:text-blue-400 transition">星座网络</a>
41
+ <a href="#" class="hover:text-blue-400 transition">算力市场</a>
42
+ <a href="#" class="hover:text-blue-400 transition">任务审计</a>
43
+ </div>
44
+ <div class="flex items-center space-x-4">
45
+ <span id="system-time" class="text-xs font-mono text-gray-500">2026-02-14 12:00:00 UTC</span>
46
+ <button class="text-xl"><i class="fas fa-user-circle"></i></button>
47
+ </div>
48
+ </div>
49
+ </nav>
50
+
51
+ <main class="max-w-7xl mx-auto p-4 md:p-6 grid grid-cols-1 lg:grid-cols-4 gap-6">
52
+
53
+ <!-- 左侧:状态与图表 -->
54
+ <div class="lg:col-span-1 space-y-6">
55
+ <div class="glass p-5 rounded-2xl">
56
+ <h2 class="text-sm font-bold mb-4 flex items-center text-gray-400 uppercase tracking-widest">
57
+ <i class="fas fa-chart-line mr-2 text-blue-500"></i> 星座负载概览
58
+ </h2>
59
+ <div class="h-48 w-full">
60
+ <canvas id="loadChart"></canvas>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="glass p-5 rounded-2xl">
65
+ <h2 class="text-sm font-bold mb-4 flex items-center text-gray-400 uppercase tracking-widest">
66
+ <i class="fas fa-microchip mr-2 text-blue-500"></i> 卫星节点状态
67
+ </h2>
68
+ <div id="satellite-list" class="space-y-4 max-h-[400px] overflow-y-auto no-scrollbar">
69
+ {% for sat in sats %}
70
+ <div class="p-3 rounded-xl bg-white/5 border border-white/10 hover:border-blue-500/30 transition-all">
71
+ <div class="flex justify-between items-start mb-2">
72
+ <div>
73
+ <h3 class="font-medium text-sm">{{ sat.name }}</h3>
74
+ <p class="text-[10px] text-gray-500 font-mono">{{ sat.id }}</p>
75
+ </div>
76
+ <span class="text-[10px] px-2 py-0.5 rounded-full {% if sat.status == 'Orbiting' %}bg-green-500/20 text-green-400 border border-green-500/30{% else %}bg-yellow-500/20 text-yellow-400 border border-yellow-500/30{% endif %}">
77
+ {{ "运行中" if sat.status == 'Orbiting' else "维护中" }}
78
+ </span>
79
+ </div>
80
+ <div class="space-y-2">
81
+ <div class="flex justify-between text-[10px]">
82
+ <span class="text-gray-400">电量</span>
83
+ <span>{{ sat.battery }}%</span>
84
+ </div>
85
+ <div class="w-full bg-white/10 rounded-full h-1">
86
+ <div class="bg-green-500 h-1 rounded-full transition-all duration-1000" style="width: {{ sat.battery }}%"></div>
87
+ </div>
88
+ <div class="flex justify-between text-[10px] pt-1">
89
+ <span class="text-gray-400">算力负载</span>
90
+ <span>{{ sat.compute_load }}%</span>
91
+ </div>
92
+ <div class="w-full bg-white/10 rounded-full h-1">
93
+ <div class="bg-blue-500 h-1 rounded-full transition-all duration-1000" style="width: {{ sat.compute_load }}%"></div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ {% endfor %}
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- 中间:控制台 -->
103
+ <div class="lg:col-span-2 space-y-6">
104
+ <div class="glass p-6 rounded-2xl relative overflow-hidden">
105
+ <div class="absolute top-0 right-0 p-4">
106
+ <i class="fas fa-shield-alt text-blue-500/20 text-6xl"></i>
107
+ </div>
108
+ <h2 class="text-lg font-bold mb-4 flex items-center">
109
+ <i class="fas fa-terminal mr-2 text-blue-500"></i> 任务控制台 (Mission Control)
110
+ </h2>
111
+ <form id="mission-form" class="space-y-4">
112
+ <div class="relative">
113
+ <textarea
114
+ name="prompt"
115
+ id="mission-input"
116
+ class="w-full bg-white/5 border border-white/10 rounded-xl p-4 text-white focus:outline-none focus:border-blue-500 transition h-40 resize-none text-sm leading-relaxed"
117
+ placeholder="请输入您的指令...
118
+ 例如:'分析 SAT-001 的能耗趋势,并根据当前轨道天气,将非核心计算任务迁移至电力充沛的节点。'"
119
+ ></textarea>
120
+ <div class="absolute bottom-3 right-3 flex space-x-2">
121
+ <button type="button" onclick="triggerUpload()" class="p-2 rounded-lg bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white transition" title="上传附件">
122
+ <i class="fas fa-paperclip"></i>
123
+ </button>
124
+ <input type="file" id="file-upload" class="hidden" onchange="handleFileSelected(this)">
125
+ </div>
126
+ </div>
127
+ <div id="upload-status" class="hidden text-xs text-blue-400 bg-blue-500/10 p-2 rounded-lg border border-blue-500/20">
128
+ <i class="fas fa-file-alt mr-1"></i> <span id="filename-display"></span> 上传成功
129
+ </div>
130
+ <button type="submit" id="submit-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-xl transition shadow-lg shadow-blue-500/20 flex items-center justify-center space-x-3">
131
+ <i class="fas fa-bolt"></i>
132
+ <span>启动智能调度流</span>
133
+ </button>
134
+ </form>
135
+ </div>
136
+
137
+ <!-- 输出区域 -->
138
+ <div id="output-container" class="glass p-6 rounded-2xl hidden transition-all duration-500">
139
+ <div class="flex justify-between items-center mb-6">
140
+ <h2 class="text-lg font-bold flex items-center">
141
+ <i class="fas fa-brain mr-2 text-blue-500"></i> 智能体推理与决策轨迹
142
+ </h2>
143
+ <span id="task-status-badge" class="px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider">等待中</span>
144
+ </div>
145
+
146
+ <div class="space-y-6">
147
+ <div id="agent-logs" class="font-mono text-xs space-y-3 max-h-80 overflow-y-auto p-4 bg-black/40 rounded-xl border border-white/5 no-scrollbar">
148
+ <!-- 日志将在此处动态生成 -->
149
+ </div>
150
+
151
+ <div class="border-t border-white/10 pt-6">
152
+ <div class="flex items-center mb-4">
153
+ <div class="h-1 w-8 bg-blue-500 rounded-full mr-3"></div>
154
+ <h3 class="text-md font-bold">最终执行指令</h3>
155
+ </div>
156
+ <div id="final-result" class="prose prose-invert max-w-none text-sm bg-white/5 p-5 rounded-xl border border-white/10">
157
+ <!-- 结果将在此处动态生成 -->
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <!-- ���侧:任务审计 -->
165
+ <div class="lg:col-span-1 space-y-6">
166
+ <div class="glass p-5 rounded-2xl">
167
+ <h2 class="text-sm font-bold mb-4 flex items-center text-gray-400 uppercase tracking-widest">
168
+ <i class="fas fa-history mr-2 text-blue-500"></i> 历史任务记录
169
+ </h2>
170
+ <div id="task-history" class="space-y-3 overflow-y-auto max-h-[600px] no-scrollbar">
171
+ {% for task in tasks %}
172
+ <div class="p-3 rounded-xl bg-white/5 border border-white/5 text-sm cursor-pointer hover:bg-white/10 hover:border-white/20 transition-all group" onclick="showTaskDetail('{{ task.id }}')">
173
+ <div class="flex justify-between mb-1">
174
+ <span class="font-mono text-[10px] text-blue-400">#{{ task.id }}</span>
175
+ <span class="text-[9px] text-gray-500">{{ task.created_at.strftime('%H:%M:%S') if task.created_at else '' }}</span>
176
+ </div>
177
+ <p class="truncate text-gray-300 group-hover:text-white transition-colors">{{ task.description }}</p>
178
+ <div class="mt-2 flex items-center text-[9px] text-gray-500">
179
+ <span class="status-dot orbiting"></span> 已完成
180
+ </div>
181
+ </div>
182
+ {% else %}
183
+ <div class="text-center py-10 text-gray-600 text-xs">
184
+ <i class="fas fa-inbox text-2xl mb-2 opacity-20"></i>
185
+ <p>暂无任务记录</p>
186
+ </div>
187
+ {% endfor %}
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </main>
192
+
193
+ <!-- 任务详情弹窗 -->
194
+ <div id="detail-modal" class="fixed inset-0 bg-black/90 backdrop-blur-md z-[100] hidden items-center justify-center p-4">
195
+ <div class="glass max-w-4xl w-full max-h-[85vh] rounded-3xl overflow-hidden flex flex-col shadow-2xl border-white/20">
196
+ <div class="p-6 border-b border-white/10 flex justify-between items-center bg-white/5">
197
+ <div class="flex items-center space-x-3">
198
+ <i class="fas fa-file-invoice text-blue-500"></i>
199
+ <h3 class="text-xl font-bold">任务全周期审计报告</h3>
200
+ </div>
201
+ <button onclick="closeModal()" class="w-10 h-10 rounded-full flex items-center justify-center bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white transition">
202
+ <i class="fas fa-times"></i>
203
+ </button>
204
+ </div>
205
+ <div id="modal-content" class="p-8 overflow-y-auto space-y-8 no-scrollbar">
206
+ <!-- 弹窗内容动态生成 -->
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <script>
212
+ // 初始化图表
213
+ let loadChart;
214
+ function initChart(data) {
215
+ const ctx = document.getElementById('loadChart').getContext('2d');
216
+ loadChart = new Chart(ctx, {
217
+ type: 'bar',
218
+ data: {
219
+ labels: data.labels,
220
+ datasets: [
221
+ {
222
+ label: '负载 (%)',
223
+ data: data.loads,
224
+ backgroundColor: 'rgba(59, 130, 246, 0.5)',
225
+ borderColor: '#3b82f6',
226
+ borderWidth: 1,
227
+ borderRadius: 4
228
+ },
229
+ {
230
+ label: '电量 (%)',
231
+ data: data.batteries,
232
+ type: 'line',
233
+ borderColor: '#10b981',
234
+ backgroundColor: '#10b981',
235
+ borderWidth: 2,
236
+ pointRadius: 3,
237
+ fill: false
238
+ }
239
+ ]
240
+ },
241
+ options: {
242
+ responsive: true,
243
+ maintainAspectRatio: false,
244
+ scales: {
245
+ y: {
246
+ beginAtZero: true,
247
+ max: 100,
248
+ grid: { color: 'rgba(255,255,255,0.05)' },
249
+ ticks: { color: '#666', font: { size: 10 } }
250
+ },
251
+ x: {
252
+ grid: { display: false },
253
+ ticks: { color: '#666', font: { size: 10 } }
254
+ }
255
+ },
256
+ plugins: {
257
+ legend: {
258
+ display: true,
259
+ position: 'bottom',
260
+ labels: { color: '#999', boxWidth: 10, font: { size: 10 } }
261
+ }
262
+ }
263
+ }
264
+ });
265
+ }
266
+
267
+ // 定时更新系统时间
268
+ setInterval(() => {
269
+ const now = new Date();
270
+ document.getElementById('system-time').innerText = now.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
271
+ }, 1000);
272
+
273
+ // 表单提交
274
+ const form = document.getElementById('mission-form');
275
+ const submitBtn = document.getElementById('submit-btn');
276
+ const outputContainer = document.getElementById('output-container');
277
+ const agentLogs = document.getElementById('agent-logs');
278
+ const finalResult = document.getElementById('final-result');
279
+ const taskStatusBadge = document.getElementById('task-status-badge');
280
+
281
+ form.onsubmit = async (e) => {
282
+ e.preventDefault();
283
+ const formData = new FormData(form);
284
+ if (!formData.get('prompt')) return;
285
+
286
+ // UI 重置
287
+ submitBtn.disabled = true;
288
+ submitBtn.innerHTML = '<i class="fas fa-satellite animate-pulse"></i> <span>正在建立星地链路...</span>';
289
+ outputContainer.classList.remove('hidden');
290
+ agentLogs.innerHTML = '<div class="text-blue-400/60">[SYSTEM] 链路已建立,开始上传指令...</div>';
291
+ finalResult.innerHTML = '<div class="flex items-center space-x-2 text-gray-500"><div class="w-2 h-2 bg-gray-500 rounded-full animate-bounce"></div><span>计算中...</span></div>';
292
+ taskStatusBadge.className = 'px-3 py-1 rounded-full text-[10px] font-bold bg-yellow-500/20 text-yellow-400 animate-pulse';
293
+ taskStatusBadge.innerText = '执行中';
294
+
295
+ try {
296
+ const response = await fetch('/mission', {
297
+ method: 'POST',
298
+ body: formData
299
+ });
300
+ const data = await response.json();
301
+
302
+ // 渲染日志
303
+ agentLogs.innerHTML = '';
304
+ data.logs.forEach((log, index) => {
305
+ setTimeout(() => {
306
+ const div = document.createElement('div');
307
+ div.className = 'opacity-0 transform translate-x-2 transition-all duration-300';
308
+
309
+ if (log.includes('Decision:')) {
310
+ div.innerHTML = `<span class="text-blue-400 font-bold">◈ 决策:</span> <span class="text-blue-100">${log.split('Decision:')[1]}</span>`;
311
+ } else if (log.includes('Action Result:')) {
312
+ div.innerHTML = `<span class="text-green-400 font-bold">✓ 响应:</span> <span class="text-green-100">${log.split('Action Result:')[1]}</span>`;
313
+ } else if (log.includes('Commander:')) {
314
+ div.innerHTML = `<span class="text-purple-400 font-bold">⌘ 指挥:</span> <span class="text-purple-100">${log.split('Commander:')[1]}</span>`;
315
+ } else {
316
+ div.innerHTML = `<span class="text-gray-500"># ${log}</span>`;
317
+ }
318
+
319
+ agentLogs.appendChild(div);
320
+ setTimeout(() => {
321
+ div.classList.remove('opacity-0', 'translate-x-2');
322
+ agentLogs.scrollTop = agentLogs.scrollHeight;
323
+ }, 50);
324
+ }, index * 100);
325
+ });
326
+
327
+ // 渲染 Markdown 结果
328
+ setTimeout(() => {
329
+ finalResult.innerHTML = marked.parse(data.result);
330
+ taskStatusBadge.className = 'px-3 py-1 rounded-full text-[10px] font-bold bg-green-500/20 text-green-400';
331
+ taskStatusBadge.innerText = '任务完成';
332
+ updateStatus();
333
+ }, data.logs.length * 100 + 500);
334
+
335
+ } catch (err) {
336
+ console.error(err);
337
+ taskStatusBadge.className = 'px-3 py-1 rounded-full text-[10px] font-bold bg-red-500/20 text-red-400';
338
+ taskStatusBadge.innerText = '链路中断';
339
+ finalResult.innerHTML = '<span class="text-red-400">错误: 无法连接到星际计算引擎。请检查您的 API 密钥或网络连接。</span>';
340
+ } finally {
341
+ submitBtn.disabled = false;
342
+ submitBtn.innerHTML = '<i class="fas fa-bolt"></i> <span>启动智能调度流</span>';
343
+ }
344
+ };
345
+
346
+ // 文件上传逻辑
347
+ function triggerUpload() {
348
+ document.getElementById('file-upload').click();
349
+ }
350
+
351
+ async function handleFileSelected(input) {
352
+ if (!input.files || input.files.length === 0) return;
353
+ const file = input.files[0];
354
+ const statusDiv = document.getElementById('upload-status');
355
+ const filenameSpan = document.getElementById('filename-display');
356
+
357
+ statusDiv.classList.remove('hidden');
358
+ statusDiv.innerHTML = `<i class="fas fa-spinner animate-spin mr-1"></i> 正在上传 ${file.name}...`;
359
+
360
+ const formData = new FormData();
361
+ formData.append('file', file);
362
+
363
+ try {
364
+ const res = await fetch('/upload', { method: 'POST', body: formData });
365
+ const data = await res.json();
366
+ if (data.status === 'success') {
367
+ statusDiv.innerHTML = `<i class="fas fa-check-circle text-green-400 mr-1"></i> ${file.name} 已挂载到当前任务上下文`;
368
+ statusDiv.classList.add('bg-green-500/10', 'border-green-500/20', 'text-green-400');
369
+ // 将文件名添加到输入框
370
+ const missionInput = document.getElementById('mission-input');
371
+ missionInput.value += `\n[附件: ${file.name}]`;
372
+ } else {
373
+ throw new Error(data.message);
374
+ }
375
+ } catch (err) {
376
+ statusDiv.innerHTML = `<i class="fas fa-exclamation-triangle text-red-400 mr-1"></i> 上传失败: ${err.message}`;
377
+ statusDiv.classList.add('bg-red-500/10', 'border-red-500/20', 'text-red-400');
378
+ }
379
+ }
380
+
381
+ // 更新状态
382
+ async function updateStatus() {
383
+ try {
384
+ const [satRes, statsRes] = await Promise.all([
385
+ fetch('/api/satellites'),
386
+ fetch('/api/stats')
387
+ ]);
388
+
389
+ const sats = await satRes.json();
390
+ const stats = await statsRes.json();
391
+
392
+ // 更新列表
393
+ const list = document.getElementById('satellite-list');
394
+ list.innerHTML = sats.map(sat => `
395
+ <div class="p-3 rounded-xl bg-white/5 border border-white/10 hover:border-blue-500/30 transition-all">
396
+ <div class="flex justify-between items-start mb-2">
397
+ <div>
398
+ <h3 class="font-medium text-sm">${sat.name}</h3>
399
+ <p class="text-[10px] text-gray-500 font-mono">${sat.id}</p>
400
+ </div>
401
+ <span class="text-[10px] px-2 py-0.5 rounded-full ${sat.status === 'Orbiting' ? 'bg-green-500/20 text-green-400 border border-green-500/30' : 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30'}">
402
+ ${sat.status === 'Orbiting' ? '运行中' : '维护中'}
403
+ </span>
404
+ </div>
405
+ <div class="space-y-2">
406
+ <div class="flex justify-between text-[10px]">
407
+ <span class="text-gray-400">电量</span>
408
+ <span>${sat.battery}%</span>
409
+ </div>
410
+ <div class="w-full bg-white/10 rounded-full h-1">
411
+ <div class="bg-green-500 h-1 rounded-full transition-all duration-1000" style="width: ${sat.battery}%"></div>
412
+ </div>
413
+ <div class="flex justify-between text-[10px] pt-1">
414
+ <span class="text-gray-400">算力负载</span>
415
+ <span>${sat.compute_load}%</span>
416
+ </div>
417
+ <div class="w-full bg-white/10 rounded-full h-1">
418
+ <div class="bg-blue-500 h-1 rounded-full transition-all duration-1000" style="width: ${sat.compute_load}%"></div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ `).join('');
423
+
424
+ // 更新图表
425
+ if (loadChart) {
426
+ loadChart.data.labels = stats.labels;
427
+ loadChart.data.datasets[0].data = stats.loads;
428
+ loadChart.data.datasets[1].data = stats.batteries;
429
+ loadChart.update();
430
+ }
431
+ } catch (err) {
432
+ console.error("更新状态失败:", err);
433
+ }
434
+ }
435
+
436
+ // 显示详情
437
+ async function showTaskDetail(id) {
438
+ try {
439
+ const tasksRes = await fetch('/api/tasks');
440
+ const tasks = await tasksRes.json();
441
+ const task = tasks.find(t => t.id === id);
442
+ if (!task) return;
443
+
444
+ const modal = document.getElementById('detail-modal');
445
+ const content = document.getElementById('modal-content');
446
+
447
+ content.innerHTML = `
448
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
449
+ <div class="space-y-6">
450
+ <div>
451
+ <h4 class="text-xs font-bold text-gray-500 uppercase tracking-widest mb-3 flex items-center">
452
+ <span class="w-1.5 h-1.5 bg-blue-500 rounded-full mr-2"></span> 原始任务指令
453
+ </h4>
454
+ <div class="bg-white/5 p-5 rounded-2xl border border-white/10 text-sm leading-relaxed text-blue-100">
455
+ ${task.description}
456
+ </div>
457
+ </div>
458
+ <div>
459
+ <h4 class="text-xs font-bold text-gray-500 uppercase tracking-widest mb-3 flex items-center">
460
+ <span class="w-1.5 h-1.5 bg-green-500 rounded-full mr-2"></span> 最终决策结果
461
+ </h4>
462
+ <div class="prose prose-invert max-w-none text-sm bg-white/5 p-5 rounded-2xl border border-white/10">
463
+ ${marked.parse(task.result)}
464
+ </div>
465
+ </div>
466
+ </div>
467
+ <div>
468
+ <h4 class="text-xs font-bold text-gray-500 uppercase tracking-widest mb-3 flex items-center">
469
+ <span class="w-1.5 h-1.5 bg-purple-500 rounded-full mr-2"></span> 智能体执行日志 (审计跟踪)
470
+ </h4>
471
+ <div class="bg-black/40 p-5 rounded-2xl border border-white/10 text-[11px] font-mono text-blue-300 h-full overflow-y-auto max-h-[500px] leading-relaxed no-scrollbar">
472
+ ${task.agent_log.split('\n').map(line => `<div class="mb-2 border-l border-white/10 pl-3">${line}</div>`).join('')}
473
+ </div>
474
+ </div>
475
+ </div>
476
+ `;
477
+
478
+ modal.classList.remove('hidden');
479
+ modal.classList.add('flex');
480
+ document.body.style.overflow = 'hidden';
481
+ } catch (err) {
482
+ console.error("加载详情失败:", err);
483
+ }
484
+ }
485
+
486
+ function closeModal() {
487
+ const modal = document.getElementById('detail-modal');
488
+ modal.classList.add('hidden');
489
+ modal.classList.remove('flex');
490
+ document.body.style.overflow = 'auto';
491
+ }
492
+
493
+ // 初始化
494
+ window.onload = async () => {
495
+ const res = await fetch('/api/stats');
496
+ const data = await res.json();
497
+ initChart(data);
498
+
499
+ // 初始动画效果
500
+ document.querySelectorAll('.glass').forEach((el, i) => {
501
+ el.style.opacity = '0';
502
+ el.style.transform = 'translateY(10px)';
503
+ setTimeout(() => {
504
+ el.style.transition = 'all 0.5s ease';
505
+ el.style.opacity = '1';
506
+ el.style.transform = 'translateY(0)';
507
+ }, i * 100);
508
+ });
509
+ };
510
+ </script>
511
+ </body>
512
+ </html>