戒酒的李白

Initial setup of web app.

  1 +"""
  2 +Flask主应用 - 统一管理三个Streamlit应用
  3 +"""
  4 +
  5 +import os
  6 +import sys
  7 +import subprocess
  8 +import time
  9 +import json
  10 +import threading
  11 +from datetime import datetime
  12 +from queue import Queue, Empty
  13 +from flask import Flask, render_template, request, jsonify, Response
  14 +from flask_socketio import SocketIO, emit
  15 +import signal
  16 +import atexit
  17 +import requests
  18 +
  19 +app = Flask(__name__)
  20 +app.config['SECRET_KEY'] = 'weibo_analysis_system_2024'
  21 +socketio = SocketIO(app, cors_allowed_origins="*")
  22 +
  23 +# 全局变量存储进程信息
  24 +processes = {
  25 + 'insight': {'process': None, 'port': 8501, 'status': 'stopped', 'output': []},
  26 + 'media': {'process': None, 'port': 8502, 'status': 'stopped', 'output': []},
  27 + 'query': {'process': None, 'port': 8503, 'status': 'stopped', 'output': []}
  28 +}
  29 +
  30 +# 输出队列
  31 +output_queues = {
  32 + 'insight': Queue(),
  33 + 'media': Queue(),
  34 + 'query': Queue()
  35 +}
  36 +
  37 +def read_process_output(process, app_name):
  38 + """读取进程输出并放入队列"""
  39 + while True:
  40 + try:
  41 + if process.poll() is not None:
  42 + break
  43 +
  44 + output = process.stdout.readline()
  45 + if output:
  46 + line = output.decode('utf-8', errors='ignore').strip()
  47 + if line:
  48 + timestamp = datetime.now().strftime('%H:%M:%S')
  49 + formatted_line = f"[{timestamp}] {line}"
  50 +
  51 + # 添加到输出列表(保持最近100行)
  52 + processes[app_name]['output'].append(formatted_line)
  53 + if len(processes[app_name]['output']) > 100:
  54 + processes[app_name]['output'].pop(0)
  55 +
  56 + # 发送到前端
  57 + socketio.emit('console_output', {
  58 + 'app': app_name,
  59 + 'line': formatted_line
  60 + })
  61 + except Exception as e:
  62 + print(f"Error reading output for {app_name}: {e}")
  63 + break
  64 +
  65 +def start_streamlit_app(app_name, script_path, port):
  66 + """启动Streamlit应用"""
  67 + try:
  68 + if processes[app_name]['process'] is not None:
  69 + return False, "应用已经在运行"
  70 +
  71 + # 检查文件是否存在
  72 + if not os.path.exists(script_path):
  73 + return False, f"文件不存在: {script_path}"
  74 +
  75 + cmd = [
  76 + sys.executable, '-m', 'streamlit', 'run',
  77 + script_path,
  78 + '--server.port', str(port),
  79 + '--server.headless', 'true',
  80 + '--browser.gatherUsageStats', 'false',
  81 + '--logger.level', 'info'
  82 + ]
  83 +
  84 + # 使用当前工作目录而不是脚本目录
  85 + process = subprocess.Popen(
  86 + cmd,
  87 + stdout=subprocess.PIPE,
  88 + stderr=subprocess.STDOUT,
  89 + bufsize=1,
  90 + universal_newlines=False,
  91 + cwd=os.getcwd()
  92 + )
  93 +
  94 + processes[app_name]['process'] = process
  95 + processes[app_name]['status'] = 'starting'
  96 + processes[app_name]['output'] = []
  97 +
  98 + # 启动输出读取线程
  99 + output_thread = threading.Thread(
  100 + target=read_process_output,
  101 + args=(process, app_name),
  102 + daemon=True
  103 + )
  104 + output_thread.start()
  105 +
  106 + return True, f"{app_name} 应用启动中..."
  107 +
  108 + except Exception as e:
  109 + return False, f"启动失败: {str(e)}"
  110 +
  111 +def stop_streamlit_app(app_name):
  112 + """停止Streamlit应用"""
  113 + try:
  114 + if processes[app_name]['process'] is None:
  115 + return False, "应用未运行"
  116 +
  117 + process = processes[app_name]['process']
  118 + process.terminate()
  119 +
  120 + # 等待进程结束
  121 + try:
  122 + process.wait(timeout=5)
  123 + except subprocess.TimeoutExpired:
  124 + process.kill()
  125 + process.wait()
  126 +
  127 + processes[app_name]['process'] = None
  128 + processes[app_name]['status'] = 'stopped'
  129 +
  130 + return True, f"{app_name} 应用已停止"
  131 +
  132 + except Exception as e:
  133 + return False, f"停止失败: {str(e)}"
  134 +
  135 +def check_app_status():
  136 + """检查应用状态"""
  137 + for app_name, info in processes.items():
  138 + if info['process'] is not None:
  139 + if info['process'].poll() is None:
  140 + # 进程仍在运行,检查端口是否可访问
  141 + try:
  142 + response = requests.get(f"http://localhost:{info['port']}", timeout=2)
  143 + if response.status_code == 200:
  144 + info['status'] = 'running'
  145 + else:
  146 + info['status'] = 'starting'
  147 + except requests.exceptions.RequestException:
  148 + info['status'] = 'starting'
  149 + except Exception:
  150 + info['status'] = 'starting'
  151 + else:
  152 + # 进程已结束
  153 + info['process'] = None
  154 + info['status'] = 'stopped'
  155 +
  156 +def wait_for_app_startup(app_name, max_wait_time=30):
  157 + """等待应用启动完成"""
  158 + import time
  159 + start_time = time.time()
  160 + while time.time() - start_time < max_wait_time:
  161 + info = processes[app_name]
  162 + if info['process'] is None:
  163 + return False, "进程已停止"
  164 +
  165 + if info['process'].poll() is not None:
  166 + return False, "进程启动失败"
  167 +
  168 + try:
  169 + response = requests.get(f"http://localhost:{info['port']}", timeout=2)
  170 + if response.status_code == 200:
  171 + info['status'] = 'running'
  172 + return True, "启动成功"
  173 + except:
  174 + pass
  175 +
  176 + time.sleep(1)
  177 +
  178 + return False, "启动超时"
  179 +
  180 +def cleanup_processes():
  181 + """清理所有进程"""
  182 + for app_name in processes:
  183 + stop_streamlit_app(app_name)
  184 +
  185 +# 注册清理函数
  186 +atexit.register(cleanup_processes)
  187 +
  188 +@app.route('/')
  189 +def index():
  190 + """主页"""
  191 + return render_template('index.html')
  192 +
  193 +@app.route('/api/status')
  194 +def get_status():
  195 + """获取所有应用状态"""
  196 + check_app_status()
  197 + return jsonify({
  198 + app_name: {
  199 + 'status': info['status'],
  200 + 'port': info['port'],
  201 + 'output_lines': len(info['output'])
  202 + }
  203 + for app_name, info in processes.items()
  204 + })
  205 +
  206 +@app.route('/api/start/<app_name>')
  207 +def start_app(app_name):
  208 + """启动指定应用"""
  209 + if app_name not in processes:
  210 + return jsonify({'success': False, 'message': '未知应用'})
  211 +
  212 + script_paths = {
  213 + 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py',
  214 + 'media': 'SingleEngineApp/media_engine_streamlit_app.py',
  215 + 'query': 'SingleEngineApp/query_engine_streamlit_app.py'
  216 + }
  217 +
  218 + success, message = start_streamlit_app(
  219 + app_name,
  220 + script_paths[app_name],
  221 + processes[app_name]['port']
  222 + )
  223 +
  224 + if success:
  225 + # 等待应用启动
  226 + startup_success, startup_message = wait_for_app_startup(app_name, 15)
  227 + if not startup_success:
  228 + message += f" 但启动检查失败: {startup_message}"
  229 +
  230 + return jsonify({'success': success, 'message': message})
  231 +
  232 +@app.route('/api/stop/<app_name>')
  233 +def stop_app(app_name):
  234 + """停止指定应用"""
  235 + if app_name not in processes:
  236 + return jsonify({'success': False, 'message': '未知应用'})
  237 +
  238 + success, message = stop_streamlit_app(app_name)
  239 + return jsonify({'success': success, 'message': message})
  240 +
  241 +@app.route('/api/output/<app_name>')
  242 +def get_output(app_name):
  243 + """获取应用输出"""
  244 + if app_name not in processes:
  245 + return jsonify({'success': False, 'message': '未知应用'})
  246 +
  247 + return jsonify({
  248 + 'success': True,
  249 + 'output': processes[app_name]['output']
  250 + })
  251 +
  252 +@app.route('/api/search', methods=['POST'])
  253 +def search():
  254 + """统一搜索接口"""
  255 + data = request.get_json()
  256 + query = data.get('query', '').strip()
  257 +
  258 + if not query:
  259 + return jsonify({'success': False, 'message': '搜索查询不能为空'})
  260 +
  261 + # 检查哪些应用正在运行
  262 + check_app_status()
  263 + running_apps = [name for name, info in processes.items() if info['status'] == 'running']
  264 +
  265 + if not running_apps:
  266 + return jsonify({'success': False, 'message': '没有运行中的应用'})
  267 +
  268 + # 向运行中的应用发送搜索请求
  269 + results = {}
  270 + api_ports = {'insight': 8601, 'media': 8602, 'query': 8603}
  271 +
  272 + for app_name in running_apps:
  273 + try:
  274 + api_port = api_ports[app_name]
  275 + # 调用Streamlit应用的API端点
  276 + response = requests.post(
  277 + f"http://localhost:{api_port}/api/search",
  278 + json={'query': query},
  279 + timeout=10
  280 + )
  281 + if response.status_code == 200:
  282 + results[app_name] = response.json()
  283 + else:
  284 + results[app_name] = {'success': False, 'message': 'API调用失败'}
  285 + except Exception as e:
  286 + results[app_name] = {'success': False, 'message': str(e)}
  287 +
  288 + return jsonify({
  289 + 'success': True,
  290 + 'query': query,
  291 + 'results': results
  292 + })
  293 +
  294 +@socketio.on('connect')
  295 +def handle_connect():
  296 + """客户端连接"""
  297 + emit('status', 'Connected to Flask server')
  298 +
  299 +@socketio.on('request_status')
  300 +def handle_status_request():
  301 + """请求状态更新"""
  302 + check_app_status()
  303 + emit('status_update', {
  304 + app_name: {
  305 + 'status': info['status'],
  306 + 'port': info['port']
  307 + }
  308 + for app_name, info in processes.items()
  309 + })
  310 +
  311 +if __name__ == '__main__':
  312 + # 启动时自动启动所有Streamlit应用
  313 + print("正在启动Streamlit应用...")
  314 +
  315 + script_paths = {
  316 + 'insight': 'SingleEngineApp/insight_engine_streamlit_app.py',
  317 + 'media': 'SingleEngineApp/media_engine_streamlit_app.py',
  318 + 'query': 'SingleEngineApp/query_engine_streamlit_app.py'
  319 + }
  320 +
  321 + for app_name, script_path in script_paths.items():
  322 + print(f"检查文件: {script_path}")
  323 + if os.path.exists(script_path):
  324 + print(f"启动 {app_name}...")
  325 + success, message = start_streamlit_app(app_name, script_path, processes[app_name]['port'])
  326 + print(f"{app_name}: {message}")
  327 +
  328 + if success:
  329 + print(f"等待 {app_name} 启动完成...")
  330 + startup_success, startup_message = wait_for_app_startup(app_name, 30)
  331 + print(f"{app_name} 启动检查: {startup_message}")
  332 + else:
  333 + print(f"错误: {script_path} 不存在")
  334 +
  335 + print("所有应用启动完成,启动Flask服务器...")
  336 +
  337 + try:
  338 + # 启动Flask应用
  339 + socketio.run(app, host='0.0.0.0', port=5000, debug=False)
  340 + except KeyboardInterrupt:
  341 + print("\n正在关闭应用...")
  342 + cleanup_processes()
@@ -30,3 +30,11 @@ uuid>=1.30 @@ -30,3 +30,11 @@ uuid>=1.30
30 pytest>=7.4.0 30 pytest>=7.4.0
31 black>=23.0.0 31 black>=23.0.0
32 flake8>=6.0.0 32 flake8>=6.0.0
  33 +
  34 +# Flask Web应用
  35 +flask==2.3.3
  36 +flask-socketio==5.3.6
  37 +streamlit==1.28.1
  38 +requests==2.31.0
  39 +python-socketio==5.8.0
  40 +eventlet==0.33.3
  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>微博舆情预测系统</title>
  7 + <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
  8 + <style>
  9 + * {
  10 + margin: 0;
  11 + padding: 0;
  12 + box-sizing: border-box;
  13 + }
  14 +
  15 + body {
  16 + font-family: 'Arial', sans-serif;
  17 + background-color: #ffffff;
  18 + color: #000000;
  19 + line-height: 1.6;
  20 + overflow-x: hidden;
  21 + }
  22 +
  23 + .container {
  24 + max-width: 100vw;
  25 + min-height: 100vh;
  26 + display: flex;
  27 + flex-direction: column;
  28 + border: 2px solid #000000;
  29 + }
  30 +
  31 + /* 搜索框区域 */
  32 + .search-section {
  33 + border-bottom: 2px solid #000000;
  34 + padding: 20px;
  35 + background-color: #ffffff;
  36 + }
  37 +
  38 + .search-title {
  39 + font-size: 24px;
  40 + font-weight: bold;
  41 + text-align: center;
  42 + margin-bottom: 20px;
  43 + letter-spacing: 1px;
  44 + }
  45 +
  46 + .search-box {
  47 + display: flex;
  48 + max-width: 800px;
  49 + margin: 0 auto;
  50 + border: 2px solid #000000;
  51 + }
  52 +
  53 + .search-input {
  54 + flex: 1;
  55 + padding: 15px;
  56 + border: none;
  57 + outline: none;
  58 + font-size: 16px;
  59 + background-color: #ffffff;
  60 + }
  61 +
  62 + .search-button {
  63 + padding: 15px 30px;
  64 + border: none;
  65 + border-left: 2px solid #000000;
  66 + background-color: #000000;
  67 + color: #ffffff;
  68 + cursor: pointer;
  69 + font-size: 16px;
  70 + font-weight: bold;
  71 + transition: all 0.3s ease;
  72 + }
  73 +
  74 + .search-button:hover {
  75 + background-color: #333333;
  76 + }
  77 +
  78 + .search-button:disabled {
  79 + background-color: #666666;
  80 + cursor: not-allowed;
  81 + }
  82 +
  83 + /* 主内容区域 */
  84 + .main-content {
  85 + flex: 1;
  86 + display: flex;
  87 + height: calc(100vh - 140px);
  88 + }
  89 +
  90 + /* 嵌入页面区域 */
  91 + .embedded-section {
  92 + flex: 2;
  93 + border-right: 2px solid #000000;
  94 + background-color: #ffffff;
  95 + position: relative;
  96 + }
  97 +
  98 + .embedded-header {
  99 + padding: 15px;
  100 + border-bottom: 2px solid #000000;
  101 + background-color: #ffffff;
  102 + font-weight: bold;
  103 + text-align: center;
  104 + }
  105 +
  106 + .embedded-content {
  107 + height: calc(100% - 60px);
  108 + position: relative;
  109 + }
  110 +
  111 + /* 控制台输出区域 */
  112 + .console-section {
  113 + flex: 1;
  114 + display: flex;
  115 + flex-direction: column;
  116 + background-color: #ffffff;
  117 + }
  118 +
  119 + /* 应用切换按钮 */
  120 + .app-switcher {
  121 + display: flex;
  122 + border-bottom: 2px solid #000000;
  123 + }
  124 +
  125 + .app-button {
  126 + flex: 1;
  127 + padding: 15px;
  128 + border: none;
  129 + border-right: 2px solid #000000;
  130 + background-color: #ffffff;
  131 + cursor: pointer;
  132 + font-size: 14px;
  133 + font-weight: bold;
  134 + transition: all 0.3s ease;
  135 + position: relative;
  136 + }
  137 +
  138 + .app-button:last-child {
  139 + border-right: none;
  140 + }
  141 +
  142 + .app-button.active {
  143 + background-color: #000000;
  144 + color: #ffffff;
  145 + }
  146 +
  147 + .app-button:not(.active):hover {
  148 + background-color: #f0f0f0;
  149 + }
  150 +
  151 + .status-indicator {
  152 + position: absolute;
  153 + top: 5px;
  154 + right: 5px;
  155 + width: 8px;
  156 + height: 8px;
  157 + border-radius: 50%;
  158 + background-color: #ff0000;
  159 + }
  160 +
  161 + .status-indicator.running {
  162 + background-color: #00ff00;
  163 + }
  164 +
  165 + .status-indicator.starting {
  166 + background-color: #ffff00;
  167 + }
  168 +
  169 + /* 控制台输出 */
  170 + .console-output {
  171 + flex: 1;
  172 + padding: 15px;
  173 + background-color: #000000;
  174 + color: #00ff00;
  175 + font-family: 'Courier New', monospace;
  176 + font-size: 12px;
  177 + overflow-y: auto;
  178 + white-space: pre-wrap;
  179 + word-break: break-all;
  180 + }
  181 +
  182 + .console-line {
  183 + margin-bottom: 2px;
  184 + }
  185 +
  186 + /* 状态信息 */
  187 + .status-bar {
  188 + padding: 10px 20px;
  189 + border-top: 2px solid #000000;
  190 + background-color: #ffffff;
  191 + font-size: 12px;
  192 + display: flex;
  193 + justify-content: space-between;
  194 + align-items: center;
  195 + }
  196 +
  197 + .loading {
  198 + display: inline-block;
  199 + width: 12px;
  200 + height: 12px;
  201 + border: 2px solid #000000;
  202 + border-radius: 50%;
  203 + border-top-color: transparent;
  204 + animation: spin 1s ease-in-out infinite;
  205 + }
  206 +
  207 + @keyframes spin {
  208 + to { transform: rotate(360deg); }
  209 + }
  210 +
  211 + /* 响应式设计 */
  212 + @media (max-width: 768px) {
  213 + .main-content {
  214 + flex-direction: column;
  215 + height: auto;
  216 + }
  217 +
  218 + .embedded-section {
  219 + border-right: none;
  220 + border-bottom: 2px solid #000000;
  221 + height: 400px;
  222 + }
  223 +
  224 + .console-section {
  225 + height: 300px;
  226 + }
  227 + }
  228 +
  229 + /* 消息提示 */
  230 + .message {
  231 + position: fixed;
  232 + top: 20px;
  233 + right: 20px;
  234 + padding: 10px 20px;
  235 + border: 2px solid #000000;
  236 + background-color: #ffffff;
  237 + z-index: 1000;
  238 + opacity: 0;
  239 + transform: translateX(100%);
  240 + transition: all 0.3s ease;
  241 + }
  242 +
  243 + .message.show {
  244 + opacity: 1;
  245 + transform: translateX(0);
  246 + }
  247 +
  248 + .message.error {
  249 + background-color: #ffeeee;
  250 + border-color: #ff0000;
  251 + }
  252 +
  253 + .message.success {
  254 + background-color: #eeffee;
  255 + border-color: #00ff00;
  256 + }
  257 + </style>
  258 +</head>
  259 +<body>
  260 + <div class="container">
  261 + <!-- 搜索框区域 -->
  262 + <div class="search-section">
  263 + <div class="search-title">搜索框</div>
  264 + <div class="search-box">
  265 + <input type="text" class="search-input" id="searchInput" placeholder="请输入搜索内容...">
  266 + <button class="search-button" id="searchButton">搜索</button>
  267 + </div>
  268 + </div>
  269 +
  270 + <!-- 主内容区域 -->
  271 + <div class="main-content">
  272 + <!-- 嵌入页面区域 -->
  273 + <div class="embedded-section">
  274 + <div class="embedded-header" id="embeddedHeader">嵌入的页面</div>
  275 + <div class="embedded-content" id="embeddedContent">
  276 + <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666;">
  277 + <span>只显示一个页面 - 点击按钮切换页面</span>
  278 + </div>
  279 + </div>
  280 + </div>
  281 +
  282 + <!-- 控制台输出区域 -->
  283 + <div class="console-section">
  284 + <!-- 应用切换按钮 -->
  285 + <div class="app-switcher">
  286 + <button class="app-button active" data-app="insight">
  287 + <span class="status-indicator" id="status-insight"></span>
  288 + Insight Engine
  289 + </button>
  290 + <button class="app-button" data-app="media">
  291 + <span class="status-indicator" id="status-media"></span>
  292 + Media Engine
  293 + </button>
  294 + <button class="app-button" data-app="query">
  295 + <span class="status-indicator" id="status-query"></span>
  296 + Query Engine
  297 + </button>
  298 + </div>
  299 +
  300 + <!-- 控制台输出 -->
  301 + <div class="console-output" id="consoleOutput">
  302 + <div class="console-line">[系统] 等待连接...</div>
  303 + </div>
  304 + </div>
  305 + </div>
  306 +
  307 + <!-- 状态栏 -->
  308 + <div class="status-bar">
  309 + <span id="connectionStatus">连接中...</span>
  310 + <span id="systemTime"></span>
  311 + </div>
  312 + </div>
  313 +
  314 + <!-- 消息提示 -->
  315 + <div class="message" id="message"></div>
  316 +
  317 + <script>
  318 + // 全局变量
  319 + let socket;
  320 + let currentApp = 'insight';
  321 + let appStatus = {
  322 + insight: 'stopped',
  323 + media: 'stopped',
  324 + query: 'stopped'
  325 + };
  326 +
  327 + // 初始化
  328 + document.addEventListener('DOMContentLoaded', function() {
  329 + initializeSocket();
  330 + initializeEventListeners();
  331 + updateTime();
  332 + setInterval(updateTime, 1000);
  333 + checkStatus();
  334 + setInterval(checkStatus, 5000);
  335 + });
  336 +
  337 + // Socket.IO连接
  338 + function initializeSocket() {
  339 + socket = io();
  340 +
  341 + socket.on('connect', function() {
  342 + updateConnectionStatus('已连接');
  343 + socket.emit('request_status');
  344 + });
  345 +
  346 + socket.on('disconnect', function() {
  347 + updateConnectionStatus('连接断开');
  348 + });
  349 +
  350 + socket.on('console_output', function(data) {
  351 + if (data.app === currentApp) {
  352 + addConsoleOutput(data.line);
  353 + }
  354 + });
  355 +
  356 + socket.on('status_update', function(data) {
  357 + updateAppStatus(data);
  358 + });
  359 + }
  360 +
  361 + // 事件监听器
  362 + function initializeEventListeners() {
  363 + // 搜索按钮
  364 + document.getElementById('searchButton').addEventListener('click', performSearch);
  365 + document.getElementById('searchInput').addEventListener('keypress', function(e) {
  366 + if (e.key === 'Enter') {
  367 + performSearch();
  368 + }
  369 + });
  370 +
  371 + // 应用切换按钮
  372 + document.querySelectorAll('.app-button').forEach(button => {
  373 + button.addEventListener('click', function() {
  374 + const app = this.dataset.app;
  375 + switchToApp(app);
  376 + });
  377 + });
  378 + }
  379 +
  380 + // 执行搜索
  381 + function performSearch() {
  382 + const query = document.getElementById('searchInput').value.trim();
  383 + if (!query) {
  384 + showMessage('请输入搜索内容', 'error');
  385 + return;
  386 + }
  387 +
  388 + const button = document.getElementById('searchButton');
  389 + button.disabled = true;
  390 + button.innerHTML = '<span class="loading"></span> 搜索中...';
  391 +
  392 + fetch('/api/search', {
  393 + method: 'POST',
  394 + headers: {
  395 + 'Content-Type': 'application/json',
  396 + },
  397 + body: JSON.stringify({ query: query })
  398 + })
  399 + .then(response => response.json())
  400 + .then(data => {
  401 + if (data.success) {
  402 + showMessage('搜索请求已发送到所有运行中的应用', 'success');
  403 + } else {
  404 + showMessage(data.message || '搜索失败', 'error');
  405 + }
  406 + })
  407 + .catch(error => {
  408 + console.error('搜索错误:', error);
  409 + showMessage('搜索请求失败', 'error');
  410 + })
  411 + .finally(() => {
  412 + button.disabled = false;
  413 + button.innerHTML = '搜索';
  414 + });
  415 + }
  416 +
  417 + // 切换应用
  418 + function switchToApp(app) {
  419 + if (app === currentApp) return;
  420 +
  421 + // 更新按钮状态
  422 + document.querySelectorAll('.app-button').forEach(btn => {
  423 + btn.classList.remove('active');
  424 + });
  425 + document.querySelector(`[data-app="${app}"]`).classList.add('active');
  426 +
  427 + currentApp = app;
  428 +
  429 + // 清空并加载新的控制台输出
  430 + document.getElementById('consoleOutput').innerHTML = '<div class="console-line">[系统] 切换到 ' + app + ' 应用</div>';
  431 + loadConsoleOutput(app);
  432 +
  433 + // 更新嵌入页面
  434 + updateEmbeddedPage(app);
  435 + }
  436 +
  437 + // 加载控制台输出
  438 + function loadConsoleOutput(app) {
  439 + fetch(`/api/output/${app}`)
  440 + .then(response => response.json())
  441 + .then(data => {
  442 + if (data.success && data.output.length > 0) {
  443 + const consoleOutput = document.getElementById('consoleOutput');
  444 + data.output.forEach(line => {
  445 + const div = document.createElement('div');
  446 + div.className = 'console-line';
  447 + div.textContent = line;
  448 + consoleOutput.appendChild(div);
  449 + });
  450 + consoleOutput.scrollTop = consoleOutput.scrollHeight;
  451 + }
  452 + })
  453 + .catch(error => {
  454 + console.error('加载输出失败:', error);
  455 + });
  456 + }
  457 +
  458 + // 添加控制台输出
  459 + function addConsoleOutput(line) {
  460 + const consoleOutput = document.getElementById('consoleOutput');
  461 + const div = document.createElement('div');
  462 + div.className = 'console-line';
  463 + div.textContent = line;
  464 + consoleOutput.appendChild(div);
  465 +
  466 + // 保持最近100行
  467 + const lines = consoleOutput.children;
  468 + if (lines.length > 100) {
  469 + consoleOutput.removeChild(lines[0]);
  470 + }
  471 +
  472 + consoleOutput.scrollTop = consoleOutput.scrollHeight;
  473 + }
  474 +
  475 + // 更新嵌入页面
  476 + function updateEmbeddedPage(app) {
  477 + const header = document.getElementById('embeddedHeader');
  478 + const content = document.getElementById('embeddedContent');
  479 +
  480 + const appNames = {
  481 + insight: 'Insight Engine - 私有数据库分析',
  482 + media: 'Media Engine - 多模态能力',
  483 + query: 'Query Engine - 网页搜索'
  484 + };
  485 +
  486 + header.textContent = appNames[app] || app;
  487 +
  488 + // 如果应用正在运行,显示iframe
  489 + if (appStatus[app] === 'running') {
  490 + const ports = { insight: 8501, media: 8502, query: 8503 };
  491 + content.innerHTML = `<iframe src="http://localhost:${ports[app]}" style="width: 100%; height: 100%; border: none;"></iframe>`;
  492 + } else {
  493 + content.innerHTML = `
  494 + <div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; flex-direction: column;">
  495 + <div style="margin-bottom: 10px;">${appNames[app]} 未运行</div>
  496 + <div style="font-size: 12px;">状态: ${appStatus[app]}</div>
  497 + </div>
  498 + `;
  499 + }
  500 + }
  501 +
  502 + // 检查应用状态
  503 + function checkStatus() {
  504 + fetch('/api/status')
  505 + .then(response => response.json())
  506 + .then(data => {
  507 + updateAppStatus(data);
  508 + })
  509 + .catch(error => {
  510 + console.error('状态检查失败:', error);
  511 + });
  512 + }
  513 +
  514 + // 更新应用状态
  515 + function updateAppStatus(data) {
  516 + for (const [app, info] of Object.entries(data)) {
  517 + appStatus[app] = info.status;
  518 + const indicator = document.getElementById(`status-${app}`);
  519 + if (indicator) {
  520 + indicator.className = `status-indicator ${info.status}`;
  521 + }
  522 + }
  523 +
  524 + // 如果当前显示的应用状态发生变化,更新嵌入页面
  525 + updateEmbeddedPage(currentApp);
  526 + }
  527 +
  528 + // 更新连接状态
  529 + function updateConnectionStatus(status) {
  530 + document.getElementById('connectionStatus').textContent = status;
  531 + }
  532 +
  533 + // 更新时间
  534 + function updateTime() {
  535 + const now = new Date();
  536 + const timeString = now.toLocaleTimeString('zh-CN');
  537 + document.getElementById('systemTime').textContent = timeString;
  538 + }
  539 +
  540 + // 显示消息
  541 + function showMessage(text, type = 'info') {
  542 + const message = document.getElementById('message');
  543 + message.textContent = text;
  544 + message.className = `message ${type}`;
  545 + message.classList.add('show');
  546 +
  547 + setTimeout(() => {
  548 + message.classList.remove('show');
  549 + }, 3000);
  550 + }
  551 + </script>
  552 +</body>
  553 +</html>