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

Initial commit: Enhanced Orbit Ops Agent with file upload and visualization

Browse files
Files changed (6) hide show
  1. Dockerfile +12 -0
  2. README.md +48 -0
  3. app.py +209 -0
  4. instance/orbit_ops.db +0 -0
  5. requirements.txt +4 -0
  6. templates/index.html +487 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ RUN mkdir -p instance && chmod 777 instance
11
+
12
+ CMD ["gunicorn", "-b", "0.0.0.0:7866", "app:app"]
README.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Orbit Ops Agent
3
+ emoji: 🛰️
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7866
8
+ short_description: 商业航天卫星星座运维与任务规划智能体
9
+ ---
10
+
11
+ # 轨道运维智能体 (Orbit Ops Agent)
12
+
13
+ ## 项目简介
14
+ Orbit Ops Agent 是一个专为商业航天设计的卫星运维与任务规划 SaaS 平台。它集成了实时遥测监控、卫星资产管理、任务规划时间轴以及基于 LLM 的智能任务助手。
15
+
16
+ ## 核心功能
17
+ 1. **态势感知仪表盘**:实时监控卫星信号强度、星座能力雷达图及关键指标。
18
+ 2. **卫星资产管理**:全生命周期的卫星数据库(发射、在轨、退役)。
19
+ 3. **任务规划**:可视化的任务时间轴,支持发射、变轨、维护等任务管理。
20
+ 4. **AI 任务助手**:集成 SiliconFlow (Qwen) 大模型,提供专业的轨道力学咨询、异常排查建议和任务规划辅助。
21
+
22
+ ## 技术栈
23
+ - **后端**:Python Flask, SQLite
24
+ - **前端**:Vue 3, Tailwind CSS, ECharts
25
+ - **AI**:SiliconFlow API (Qwen-2.5-7B)
26
+ - **部署**:Docker
27
+
28
+ ## 快速开始
29
+
30
+ ### 本地运行
31
+ ```bash
32
+ # 构建镜像
33
+ docker build -t orbit-ops .
34
+
35
+ # 运行容器
36
+ docker run -p 7865:7865 orbit-ops
37
+ ```
38
+
39
+ 或者直接使用 Python 运行:
40
+ ```bash
41
+ pip install -r requirements.txt
42
+ python app.py
43
+ ```
44
+
45
+ 访问 `http://localhost:7866` 即可使用。
46
+
47
+ ## 许可证
48
+ MIT
app.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ import json
4
+ import requests
5
+ import random
6
+ import time
7
+ from flask import Flask, render_template, request, jsonify, g
8
+ from flask_cors import CORS
9
+ from werkzeug.utils import secure_filename
10
+
11
+ app = Flask(__name__)
12
+ CORS(app)
13
+
14
+ # Configuration
15
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
16
+ app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
17
+ SILICONFLOW_API_KEY = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
18
+ SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
19
+
20
+ # Ensure upload directory exists
21
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
22
+
23
+ # Error Handlers
24
+ @app.errorhandler(413)
25
+ def request_entity_too_large(error):
26
+ return jsonify({'error': 'File too large. Maximum size is 16MB.'}), 413
27
+
28
+ @app.errorhandler(404)
29
+ def not_found_error(error):
30
+ return render_template('index.html'), 200 # SPA fallback or just ignore
31
+
32
+ @app.errorhandler(500)
33
+ def internal_error(error):
34
+ return jsonify({'error': 'Internal Server Error'}), 500
35
+
36
+ # Database Setup
37
+ def get_db():
38
+ db = getattr(g, '_database', None)
39
+ if db is None:
40
+ db_path = os.path.join(app.instance_path, 'orbit_ops.db')
41
+ if not os.path.exists(app.instance_path):
42
+ os.makedirs(app.instance_path)
43
+ db = g._database = sqlite3.connect(db_path)
44
+ db.row_factory = sqlite3.Row
45
+ return db
46
+
47
+ @app.teardown_appcontext
48
+ def close_connection(exception):
49
+ db = getattr(g, '_database', None)
50
+ if db is not None:
51
+ db.close()
52
+
53
+ def init_db():
54
+ with app.app_context():
55
+ db = get_db()
56
+ # Satellites Table
57
+ db.execute('''
58
+ CREATE TABLE IF NOT EXISTS satellites (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ name TEXT NOT NULL,
61
+ type TEXT NOT NULL,
62
+ status TEXT NOT NULL,
63
+ orbit_altitude INTEGER,
64
+ inclination REAL,
65
+ launch_date TEXT
66
+ )
67
+ ''')
68
+ # Missions Table
69
+ db.execute('''
70
+ CREATE TABLE IF NOT EXISTS missions (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ name TEXT NOT NULL,
73
+ description TEXT,
74
+ status TEXT NOT NULL,
75
+ target_date TEXT
76
+ )
77
+ ''')
78
+
79
+ # Seed Data if empty
80
+ cur = db.execute('SELECT count(*) FROM satellites')
81
+ if cur.fetchone()[0] == 0:
82
+ satellites = [
83
+ ('StarLink-X1', 'Comm', 'Active', 550, 53.0, '2024-01-15'),
84
+ ('Sentinel-Prime', 'EarthObs', 'Active', 700, 98.2, '2023-11-20'),
85
+ ('Quantum-Relay', 'Comm', 'Testing', 1200, 45.0, '2025-02-01'),
86
+ ('Debris-Hunter', 'Cleanup', 'Planned', 800, 85.0, '2026-06-10')
87
+ ]
88
+ db.executemany('INSERT INTO satellites (name, type, status, orbit_altitude, inclination, launch_date) VALUES (?, ?, ?, ?, ?, ?)', satellites)
89
+
90
+ missions = [
91
+ ('Polar Orbit Insertion', 'Deploy Sentinel-Prime to polar orbit for ice monitoring.', 'Completed', '2023-11-20'),
92
+ ('Constellation Expansion', 'Launch batch of 60 StarLink-X satellites.', 'Pending', '2026-03-15'),
93
+ ('Debris Removal Demo', 'Test capture mechanism on defunct satellite.', 'Planning', '2026-08-01')
94
+ ]
95
+ db.executemany('INSERT INTO missions (name, description, status, target_date) VALUES (?, ?, ?, ?)', missions)
96
+
97
+ db.commit()
98
+
99
+ # Routes
100
+ @app.route('/')
101
+ def index():
102
+ return render_template('index.html')
103
+
104
+ @app.route('/api/satellites', methods=['GET', 'POST'])
105
+ def handle_satellites():
106
+ db = get_db()
107
+ if request.method == 'POST':
108
+ data = request.json
109
+ db.execute('INSERT INTO satellites (name, type, status, orbit_altitude, inclination, launch_date) VALUES (?, ?, ?, ?, ?, ?)',
110
+ (data['name'], data['type'], data['status'], data['orbit_altitude'], data['inclination'], data['launch_date']))
111
+ db.commit()
112
+ return jsonify({'status': 'success'})
113
+ else:
114
+ cur = db.execute('SELECT * FROM satellites')
115
+ return jsonify([dict(row) for row in cur.fetchall()])
116
+
117
+ @app.route('/api/missions', methods=['GET', 'POST'])
118
+ def handle_missions():
119
+ db = get_db()
120
+ if request.method == 'POST':
121
+ data = request.json
122
+ db.execute('INSERT INTO missions (name, description, status, target_date) VALUES (?, ?, ?, ?)',
123
+ (data['name'], data['description'], data['status'], data['target_date']))
124
+ db.commit()
125
+ return jsonify({'status': 'success'})
126
+ else:
127
+ cur = db.execute('SELECT * FROM missions')
128
+ return jsonify([dict(row) for row in cur.fetchall()])
129
+
130
+ @app.route('/api/upload', methods=['POST'])
131
+ def upload_file():
132
+ if 'file' not in request.files:
133
+ return jsonify({'error': 'No file part'}), 400
134
+ file = request.files['file']
135
+ if file.filename == '':
136
+ return jsonify({'error': 'No selected file'}), 400
137
+ if file:
138
+ filename = secure_filename(file.filename)
139
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
140
+ file.save(filepath)
141
+
142
+ # If it's a JSON file, try to import satellites or missions
143
+ if filename.endswith('.json'):
144
+ try:
145
+ with open(filepath, 'r', encoding='utf-8') as f:
146
+ data = json.load(f)
147
+ db = get_db()
148
+
149
+ if 'satellites' in data:
150
+ for sat in data['satellites']:
151
+ db.execute('INSERT INTO satellites (name, type, status, orbit_altitude, inclination, launch_date) VALUES (?, ?, ?, ?, ?, ?)',
152
+ (sat.get('name'), sat.get('type'), sat.get('status'), sat.get('orbit_altitude'), sat.get('inclination'), sat.get('launch_date')))
153
+
154
+ if 'missions' in data:
155
+ for mission in data['missions']:
156
+ db.execute('INSERT INTO missions (name, description, status, target_date) VALUES (?, ?, ?, ?)',
157
+ (mission.get('name'), mission.get('description'), mission.get('status'), mission.get('target_date')))
158
+
159
+ db.commit()
160
+ return jsonify({'status': 'success', 'message': 'File uploaded and data imported successfully'})
161
+ except Exception as e:
162
+ return jsonify({'status': 'warning', 'message': f'File uploaded but import failed: {str(e)}'})
163
+
164
+ return jsonify({'status': 'success', 'message': 'File uploaded successfully'})
165
+
166
+ @app.route('/api/telemetry')
167
+ def get_telemetry():
168
+ # Mock real-time telemetry
169
+ return jsonify({
170
+ 'timestamp': time.time(),
171
+ 'signal_strength': random.uniform(80, 100),
172
+ 'battery_level': random.uniform(90, 100),
173
+ 'cpu_load': random.uniform(10, 40),
174
+ 'temperature': random.uniform(20, 35)
175
+ })
176
+
177
+ @app.route('/api/chat', methods=['POST'])
178
+ def chat():
179
+ data = request.json
180
+ user_message = data.get('message', '')
181
+
182
+ headers = {
183
+ "Authorization": f"Bearer {SILICONFLOW_API_KEY}",
184
+ "Content-Type": "application/json"
185
+ }
186
+
187
+ payload = {
188
+ "model": "Qwen/Qwen2.5-7B-Instruct",
189
+ "messages": [
190
+ {"role": "system", "content": "你是 Orbit Ops 的 AI 任务规划助手。你是一个专业的航天工程师,负责协助用户进行卫星星座管理、轨道计算、发射任务规划和故障排查。请用中文回答,回答要专业、严谨,并在适当时提供 Markdown 格式的表格或列表。如果涉及数据分析,可以建议用户查看仪表盘。"},
191
+ {"role": "user", "content": user_message}
192
+ ],
193
+ "stream": False
194
+ }
195
+
196
+ try:
197
+ response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=30)
198
+ response.raise_for_status()
199
+ result = response.json()
200
+ content = result['choices'][0]['message']['content']
201
+ return jsonify({'response': content})
202
+ except Exception as e:
203
+ print(f"API Error: {e}")
204
+ # Mock Fallback
205
+ return jsonify({'response': f"AI 服务连接失败 (Mock Mode): 我收到了你的请求 '{user_message}'。作为一个模拟的 AI 助手,我建议你检查卫星 #003 的遥测数据,似乎有异常波动。请在仪表盘中查看详细信息。"})
206
+
207
+ if __name__ == '__main__':
208
+ init_db()
209
+ app.run(host='0.0.0.0', port=7866, debug=True)
instance/orbit_ops.db ADDED
Binary file (16.4 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Flask==3.0.0
2
+ flask-cors==4.0.0
3
+ requests==2.31.0
4
+ gunicorn==21.2.0
templates/index.html ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Orbit Ops Agent - 商业航天运维平台</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
11
+ <style>
12
+ [v-cloak] { display: none; }
13
+ .markdown-body { font-size: 0.95rem; line-height: 1.6; }
14
+ .markdown-body pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }
15
+ .markdown-body code { background: #f3f4f6; padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace; }
16
+ .markdown-body table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
17
+ .markdown-body th, .markdown-body td { border: 1px solid #e5e7eb; padding: 0.5rem; }
18
+ .markdown-body th { background: #f9fafb; }
19
+ </style>
20
+ </head>
21
+ <body class="bg-slate-50 text-slate-900 h-screen flex overflow-hidden">
22
+ <div id="app" v-cloak class="flex w-full h-full">
23
+ <!-- Sidebar -->
24
+ <div class="w-64 bg-slate-900 text-white flex flex-col shrink-0 transition-all duration-300" :class="{'w-20': collapsed}">
25
+ <div class="p-4 flex items-center justify-between border-b border-slate-700">
26
+ <div class="flex items-center gap-3" v-if="!collapsed">
27
+ <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center font-bold">O</div>
28
+ <span class="font-bold text-lg tracking-wide">Orbit Ops</span>
29
+ </div>
30
+ <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center font-bold" v-else>O</div>
31
+ <button @click="collapsed = !collapsed" class="text-slate-400 hover:text-white">
32
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
33
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
34
+ </svg>
35
+ </button>
36
+ </div>
37
+
38
+ <nav class="flex-1 p-4 space-y-2">
39
+ <button @click="currentView = 'dashboard'" :class="{'bg-blue-600 text-white': currentView === 'dashboard', 'text-slate-400 hover:bg-slate-800': currentView !== 'dashboard'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
40
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M8.159 13.39s.009-.089.009-.089a.75.75 0 00-.717.052l-2.613 1.488a.75.75 0 00-.007 1.298l2.613 1.487a.75.75 0 00.724.048s-.008-.09-.008-.09a2.25 2.25 0 01-2.072-3.093zM15 13.5h2.25a2.25 2.25 0 012.25 2.25v2.25a2.25 2.25 0 01-2.25 2.25H15a2.25 2.25 0 01-2.25-2.25V15.75A2.25 2.25 0 0115 13.5zM15 3v2.25a2.25 2.25 0 01-2.25 2.25H10.5a2.25 2.25 0 01-2.25-2.25V3" /></svg>
41
+ <span v-if="!collapsed">态势感知</span>
42
+ </button>
43
+ <button @click="currentView = 'satellites'" :class="{'bg-blue-600 text-white': currentView === 'satellites', 'text-slate-400 hover:bg-slate-800': currentView !== 'satellites'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
44
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" /></svg>
45
+ <span v-if="!collapsed">卫星资产</span>
46
+ </button>
47
+ <button @click="currentView = 'missions'" :class="{'bg-blue-600 text-white': currentView === 'missions', 'text-slate-400 hover:bg-slate-800': currentView !== 'missions'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
48
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0h18M5 10.5h.008v.008H5V10.5zm0 3h.008v.008H5V13.5zm0 3h.008v.008H5V16.5zm3-6h.008v.008H8V10.5zm0 3h.008v.008H8V13.5zm0 3h.008v.008H8V16.5zm3-6h.008v.008H11V10.5zm0 3h.008v.008H11V13.5zm0 3h.008v.008H11V16.5zm3-6h.008v.008H14V10.5zm0 3h.008v.008H14V13.5zm0 3h.008v.008H14V16.5zm3-6h.008v.008H17V10.5zm0 3h.008v.008H17V13.5zm0 3h.008v.008H17V16.5z" /></svg>
49
+ <span v-if="!collapsed">任务规划</span>
50
+ </button>
51
+ <button @click="currentView = 'chat'" :class="{'bg-blue-600 text-white': currentView === 'chat', 'text-slate-400 hover:bg-slate-800': currentView !== 'chat'}" class="w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors">
52
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" /></svg>
53
+ <span v-if="!collapsed">AI 助手</span>
54
+ </button>
55
+ </nav>
56
+ </div>
57
+
58
+ <!-- Main Content -->
59
+ <main class="flex-1 overflow-auto bg-slate-50 relative">
60
+ <!-- Dashboard View -->
61
+ <div v-if="currentView === 'dashboard'" class="p-6 space-y-6">
62
+ <header class="flex justify-between items-center mb-6">
63
+ <h2 class="text-2xl font-bold text-slate-800">态势感知仪表盘</h2>
64
+ <div class="flex items-center gap-2 text-sm text-slate-500">
65
+ <span class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
66
+ 实时数据连接正常
67
+ </div>
68
+ </header>
69
+
70
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
71
+ <div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
72
+ <div class="text-slate-500 text-sm mb-1">在轨卫星</div>
73
+ <div class="text-2xl font-bold text-slate-800">${ satellites.length }</div>
74
+ </div>
75
+ <div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
76
+ <div class="text-slate-500 text-sm mb-1">活跃任务</div>
77
+ <div class="text-2xl font-bold text-blue-600">${ activeMissionsCount }</div>
78
+ </div>
79
+ <div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
80
+ <div class="text-slate-500 text-sm mb-1">平均信号强度</div>
81
+ <div class="text-2xl font-bold text-green-600">${ telemetry.signal_strength.toFixed(1) }%</div>
82
+ </div>
83
+ <div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
84
+ <div class="text-slate-500 text-sm mb-1">碰撞风险</div>
85
+ <div class="text-2xl font-bold text-red-500">低</div>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 h-80">
90
+ <div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200 flex flex-col">
91
+ <h3 class="font-semibold text-slate-700 mb-4">遥测信号趋势</h3>
92
+ <div id="telemetryChart" class="flex-1 w-full h-full"></div>
93
+ </div>
94
+ <div class="bg-white p-4 rounded-xl shadow-sm border border-slate-200 flex flex-col">
95
+ <h3 class="font-semibold text-slate-700 mb-4">星座能力雷达</h3>
96
+ <div id="radarChart" class="flex-1 w-full h-full"></div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Satellites View -->
102
+ <div v-if="currentView === 'satellites'" class="p-6 space-y-6">
103
+ <header class="flex justify-between items-center mb-6">
104
+ <h2 class="text-2xl font-bold text-slate-800">卫星资产管理</h2>
105
+ <div class="flex gap-2">
106
+ <button @click="triggerUpload" class="bg-white border border-slate-300 hover:bg-slate-50 text-slate-700 px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
107
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /></svg>
108
+ ���入数据
109
+ </button>
110
+ <input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept=".json">
111
+ <button @click="showAddSatellite = true" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
112
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
113
+ 添加卫星
114
+ </button>
115
+ </div>
116
+ </header>
117
+
118
+ <div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
119
+ <table class="w-full text-left text-sm text-slate-600">
120
+ <thead class="bg-slate-50 text-slate-800 border-b border-slate-200">
121
+ <tr>
122
+ <th class="px-6 py-4 font-semibold">名称</th>
123
+ <th class="px-6 py-4 font-semibold">类型</th>
124
+ <th class="px-6 py-4 font-semibold">状态</th>
125
+ <th class="px-6 py-4 font-semibold">轨道高度 (km)</th>
126
+ <th class="px-6 py-4 font-semibold">倾角 (deg)</th>
127
+ <th class="px-6 py-4 font-semibold">发射日期</th>
128
+ </tr>
129
+ </thead>
130
+ <tbody class="divide-y divide-slate-100">
131
+ <tr v-for="sat in satellites" :key="sat.id" class="hover:bg-slate-50 transition-colors">
132
+ <td class="px-6 py-4 font-medium text-slate-900">${ sat.name }</td>
133
+ <td class="px-6 py-4">
134
+ <span class="px-2 py-1 bg-slate-100 rounded text-xs">${ sat.type }</span>
135
+ </td>
136
+ <td class="px-6 py-4">
137
+ <span :class="{'bg-green-100 text-green-700': sat.status === 'Active', 'bg-yellow-100 text-yellow-700': sat.status === 'Testing', 'bg-slate-100 text-slate-700': sat.status === 'Planned'}" class="px-2 py-1 rounded-full text-xs font-medium">
138
+ ${ sat.status }
139
+ </span>
140
+ </td>
141
+ <td class="px-6 py-4">${ sat.orbit_altitude }</td>
142
+ <td class="px-6 py-4">${ sat.inclination }°</td>
143
+ <td class="px-6 py-4 text-slate-500">${ sat.launch_date }</td>
144
+ </tr>
145
+ </tbody>
146
+ </table>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Missions View -->
151
+ <div v-if="currentView === 'missions'" class="p-6 space-y-6">
152
+ <header class="flex justify-between items-center mb-6">
153
+ <h2 class="text-2xl font-bold text-slate-800">任务规划时间轴</h2>
154
+ <button @click="showAddMission = true" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2">
155
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
156
+ 新建任务
157
+ </button>
158
+ </header>
159
+
160
+ <div class="space-y-4">
161
+ <div v-for="mission in missions" :key="mission.id" class="bg-white p-6 rounded-xl shadow-sm border border-slate-200 flex gap-6 items-start">
162
+ <div class="flex flex-col items-center gap-1 min-w-[100px]">
163
+ <span class="text-sm font-bold text-slate-400">${ mission.target_date }</span>
164
+ <div class="w-px h-full bg-slate-200 my-2"></div>
165
+ </div>
166
+ <div class="flex-1">
167
+ <div class="flex justify-between items-start mb-2">
168
+ <h3 class="text-lg font-bold text-slate-800">${ mission.name }</h3>
169
+ <span :class="{'bg-blue-100 text-blue-700': mission.status === 'Pending', 'bg-green-100 text-green-700': mission.status === 'Completed', 'bg-purple-100 text-purple-700': mission.status === 'Planning'}" class="px-2 py-1 rounded text-xs font-medium">
170
+ ${ mission.status }
171
+ </span>
172
+ </div>
173
+ <p class="text-slate-600 text-sm">${ mission.description }</p>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Chat View -->
180
+ <div v-if="currentView === 'chat'" class="flex flex-col h-full">
181
+ <div class="flex-1 overflow-y-auto p-6 space-y-4" ref="chatContainer">
182
+ <div v-for="(msg, index) in chatHistory" :key="index" :class="{'flex justify-end': msg.role === 'user', 'flex justify-start': msg.role === 'ai'}">
183
+ <div :class="{'bg-blue-600 text-white': msg.role === 'user', 'bg-white border border-slate-200 text-slate-800': msg.role === 'ai'}" class="max-w-[80%] rounded-2xl px-4 py-3 shadow-sm">
184
+ <div v-if="msg.role === 'ai'" class="markdown-body" v-html="renderMarkdown(msg.content)"></div>
185
+ <div v-else>${ msg.content }</div>
186
+ </div>
187
+ </div>
188
+ <div v-if="isThinking" class="flex justify-start">
189
+ <div class="bg-white border border-slate-200 text-slate-500 rounded-2xl px-4 py-3 shadow-sm flex items-center gap-2">
190
+ <div class="w-2 h-2 bg-slate-400 rounded-full animate-bounce"></div>
191
+ <div class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
192
+ <div class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ <div class="p-4 bg-white border-t border-slate-200">
197
+ <div class="flex gap-2 max-w-4xl mx-auto">
198
+ <input v-model="userQuery" @keyup.enter="sendMessage" type="text" placeholder="输入任务指令,例如:'帮我规划 StarLink-X1 的变轨策略'..." class="flex-1 px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
199
+ <button @click="sendMessage" :disabled="isThinking || !userQuery.trim()" class="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white px-6 py-2 rounded-lg font-medium transition-colors">
200
+ 发送
201
+ </button>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Modals -->
207
+ <div v-if="showAddSatellite" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
208
+ <div class="bg-white rounded-xl shadow-xl w-96 p-6">
209
+ <h3 class="text-xl font-bold mb-4">添加新卫星</h3>
210
+ <div class="space-y-3">
211
+ <input v-model="newSat.name" placeholder="卫星名称" class="w-full border rounded p-2">
212
+ <select v-model="newSat.type" class="w-full border rounded p-2">
213
+ <option value="Comm">通信 (Comm)</option>
214
+ <option value="EarthObs">遥感 (EarthObs)</option>
215
+ <option value="Nav">导航 (Nav)</option>
216
+ </select>
217
+ <select v-model="newSat.status" class="w-full border rounded p-2">
218
+ <option value="Active">Active</option>
219
+ <option value="Testing">Testing</option>
220
+ <option value="Planned">Planned</option>
221
+ </select>
222
+ <input v-model.number="newSat.orbit_altitude" placeholder="轨道高度 (km)" type="number" class="w-full border rounded p-2">
223
+ <input v-model.number="newSat.inclination" placeholder="倾角 (deg)" type="number" class="w-full border rounded p-2">
224
+ <input v-model="newSat.launch_date" type="date" class="w-full border rounded p-2">
225
+ </div>
226
+ <div class="flex justify-end gap-2 mt-6">
227
+ <button @click="showAddSatellite = false" class="text-slate-500 hover:text-slate-700">取消</button>
228
+ <button @click="addSatellite" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">确认添加</button>
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <div v-if="showAddMission" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
234
+ <div class="bg-white rounded-xl shadow-xl w-96 p-6">
235
+ <h3 class="text-xl font-bold mb-4">添加新任务</h3>
236
+ <div class="space-y-3">
237
+ <input v-model="newMission.name" placeholder="任务名称" class="w-full border rounded p-2">
238
+ <textarea v-model="newMission.description" placeholder="任务描述" class="w-full border rounded p-2 h-24"></textarea>
239
+ <select v-model="newMission.status" class="w-full border rounded p-2">
240
+ <option value="Planning">Planning</option>
241
+ <option value="Pending">Pending</option>
242
+ <option value="Completed">Completed</option>
243
+ </select>
244
+ <input v-model="newMission.target_date" type="date" class="w-full border rounded p-2">
245
+ </div>
246
+ <div class="flex justify-end gap-2 mt-6">
247
+ <button @click="showAddMission = false" class="text-slate-500 hover:text-slate-700">取消</button>
248
+ <button @click="addMission" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">确认添加</button>
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ </main>
254
+ </div>
255
+
256
+ <script>
257
+ const { createApp, ref, onMounted, computed, nextTick, watch } = Vue;
258
+
259
+ createApp({
260
+ delimiters: ['${', '}'],
261
+ setup() {
262
+ const collapsed = ref(false);
263
+ const currentView = ref('dashboard');
264
+ const satellites = ref([]);
265
+ const missions = ref([]);
266
+ const telemetry = ref({ signal_strength: 0, battery_level: 0, cpu_load: 0, temperature: 0 });
267
+ const showAddSatellite = ref(false);
268
+ const showAddMission = ref(false);
269
+
270
+ const newSat = ref({ name: '', type: 'Comm', status: 'Planned', orbit_altitude: 500, inclination: 0, launch_date: '' });
271
+ const newMission = ref({ name: '', description: '', status: 'Planning', target_date: '' });
272
+ const fileInput = ref(null);
273
+
274
+ const chatHistory = ref([
275
+ { role: 'ai', content: '你好!我是 Orbit Ops 任务规划助手。我可以帮你计算轨道参数、规划发射窗口或分析遥测数据。' }
276
+ ]);
277
+ const userQuery = ref('');
278
+ const isThinking = ref(false);
279
+ const chatContainer = ref(null);
280
+
281
+ const activeMissionsCount = computed(() => missions.value.filter(m => m.status === 'Pending' || m.status === 'Planning').length);
282
+
283
+ // Fetch Data
284
+ const fetchSatellites = async () => {
285
+ try {
286
+ const res = await fetch('/api/satellites');
287
+ satellites.value = await res.json();
288
+ } catch (e) { console.error(e); }
289
+ };
290
+
291
+ const fetchMissions = async () => {
292
+ try {
293
+ const res = await fetch('/api/missions');
294
+ missions.value = await res.json();
295
+ } catch (e) { console.error(e); }
296
+ };
297
+
298
+ const fetchTelemetry = async () => {
299
+ try {
300
+ const res = await fetch('/api/telemetry');
301
+ const data = await res.json();
302
+ telemetry.value = data;
303
+ updateCharts(data);
304
+ } catch (e) { console.error(e); }
305
+ };
306
+
307
+ // Charts
308
+ let telemetryChart = null;
309
+ let radarChart = null;
310
+ const telemetryHistory = [];
311
+
312
+ const initCharts = () => {
313
+ const tChartDom = document.getElementById('telemetryChart');
314
+ const rChartDom = document.getElementById('radarChart');
315
+
316
+ if (tChartDom) {
317
+ if (telemetryChart) telemetryChart.dispose();
318
+ telemetryChart = echarts.init(tChartDom);
319
+ telemetryChart.setOption({
320
+ tooltip: { trigger: 'axis' },
321
+ grid: { top: 20, right: 20, bottom: 20, left: 40, containLabel: true },
322
+ xAxis: { type: 'category', data: [] },
323
+ yAxis: { type: 'value', min: 60, max: 100 },
324
+ series: [{ data: [], type: 'line', smooth: true, areaStyle: { opacity: 0.1 }, itemStyle: { color: '#3b82f6' } }]
325
+ });
326
+ }
327
+
328
+ if (rChartDom) {
329
+ if (radarChart) radarChart.dispose();
330
+ radarChart = echarts.init(rChartDom);
331
+ radarChart.setOption({
332
+ radar: {
333
+ indicator: [
334
+ { name: '通信覆盖', max: 100 },
335
+ { name: '观测精度', max: 100 },
336
+ { name: '响应速度', max: 100 },
337
+ { name: '能源效率', max: 100 },
338
+ { name: '安全性', max: 100 }
339
+ ]
340
+ },
341
+ series: [{
342
+ type: 'radar',
343
+ data: [
344
+ { value: [85, 90, 75, 95, 80], name: '当���状态', areaStyle: { opacity: 0.2 }, itemStyle: { color: '#10b981' } }
345
+ ]
346
+ }]
347
+ });
348
+ }
349
+ };
350
+
351
+ const updateCharts = (data) => {
352
+ if (!telemetryChart) return;
353
+
354
+ const now = new Date().toLocaleTimeString();
355
+ telemetryHistory.push({ time: now, value: data.signal_strength });
356
+ if (telemetryHistory.length > 20) telemetryHistory.shift();
357
+
358
+ telemetryChart.setOption({
359
+ xAxis: { data: telemetryHistory.map(i => i.time) },
360
+ series: [{ data: telemetryHistory.map(i => i.value) }]
361
+ });
362
+ };
363
+
364
+ // Actions
365
+ const addSatellite = async () => {
366
+ await fetch('/api/satellites', {
367
+ method: 'POST',
368
+ headers: { 'Content-Type': 'application/json' },
369
+ body: JSON.stringify(newSat.value)
370
+ });
371
+ showAddSatellite.value = false;
372
+ fetchSatellites();
373
+ newSat.value = { name: '', type: 'Comm', status: 'Planned', orbit_altitude: 500, inclination: 0, launch_date: '' };
374
+ };
375
+
376
+ const addMission = async () => {
377
+ await fetch('/api/missions', {
378
+ method: 'POST',
379
+ headers: { 'Content-Type': 'application/json' },
380
+ body: JSON.stringify(newMission.value)
381
+ });
382
+ showAddMission.value = false;
383
+ fetchMissions();
384
+ newMission.value = { name: '', description: '', status: 'Planning', target_date: '' };
385
+ };
386
+
387
+ const sendMessage = async () => {
388
+ if (!userQuery.value.trim()) return;
389
+ const msg = userQuery.value;
390
+ userQuery.value = '';
391
+ chatHistory.value.push({ role: 'user', content: msg });
392
+ isThinking.value = true;
393
+ scrollToBottom();
394
+
395
+ try {
396
+ const res = await fetch('/api/chat', {
397
+ method: 'POST',
398
+ headers: { 'Content-Type': 'application/json' },
399
+ body: JSON.stringify({ message: msg })
400
+ });
401
+ const data = await res.json();
402
+ chatHistory.value.push({ role: 'ai', content: data.response });
403
+ } catch (e) {
404
+ chatHistory.value.push({ role: 'ai', content: '连接中断,请稍后再试。' });
405
+ } finally {
406
+ isThinking.value = false;
407
+ scrollToBottom();
408
+ }
409
+ };
410
+
411
+ const triggerUpload = () => {
412
+ if (fileInput.value) fileInput.value.click();
413
+ };
414
+
415
+ const handleFileUpload = async (event) => {
416
+ const file = event.target.files[0];
417
+ if (!file) return;
418
+
419
+ const formData = new FormData();
420
+ formData.append('file', file);
421
+
422
+ try {
423
+ const res = await fetch('/api/upload', {
424
+ method: 'POST',
425
+ body: formData
426
+ });
427
+ const result = await res.json();
428
+ if (result.status === 'success') {
429
+ alert('上传成功!' + (result.message || ''));
430
+ fetchSatellites();
431
+ fetchMissions();
432
+ } else {
433
+ alert('上传失败:' + result.message);
434
+ }
435
+ } catch (e) {
436
+ alert('上传出错:' + e.message);
437
+ }
438
+ // Reset input
439
+ event.target.value = '';
440
+ };
441
+
442
+ const scrollToBottom = () => {
443
+ nextTick(() => {
444
+ if (chatContainer.value) chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
445
+ });
446
+ };
447
+
448
+ const renderMarkdown = (text) => {
449
+ return marked.parse(text);
450
+ };
451
+
452
+ watch(currentView, (newVal) => {
453
+ if (newVal === 'dashboard') {
454
+ nextTick(() => {
455
+ initCharts();
456
+ });
457
+ }
458
+ });
459
+
460
+ onMounted(() => {
461
+ fetchSatellites();
462
+ fetchMissions();
463
+ setInterval(fetchTelemetry, 2000);
464
+
465
+ if (currentView.value === 'dashboard') {
466
+ setTimeout(initCharts, 500);
467
+ }
468
+
469
+ window.addEventListener('resize', () => {
470
+ if (telemetryChart) telemetryChart.resize();
471
+ if (radarChart) radarChart.resize();
472
+ });
473
+ });
474
+
475
+ return {
476
+ collapsed, currentView, satellites, missions, telemetry,
477
+ showAddSatellite, showAddMission, newSat, newMission,
478
+ chatHistory, userQuery, isThinking, chatContainer,
479
+ activeMissionsCount, fileInput,
480
+ addSatellite, addMission, sendMessage, renderMarkdown,
481
+ triggerUpload, handleFileUpload
482
+ };
483
+ }
484
+ }).mount('#app');
485
+ </script>
486
+ </body>
487
+ </html>