Spaces:
Sleeping
Sleeping
| """ | |
| 虫群Swarm Web界面 — 纯FastAPI+HTML,零额外依赖 | |
| 功能: | |
| - 聊天对话(本地记忆+API推理+MOA聚合) | |
| - 系统状态面板 | |
| - 记忆管理 | |
| """ | |
| import os, sys, time, json, asyncio | |
| from typing import Optional | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| app = FastAPI(title="虫群Swarm", version="8.0") | |
| # ============================================================ | |
| # 全局状态 | |
| # ============================================================ | |
| class SwarmAppState: | |
| def __init__(self): | |
| self.node = None | |
| self.initialized = False | |
| self.chat_history = [] | |
| def initialize(self): | |
| if self.initialized: | |
| return True | |
| try: | |
| from core.swarm_node import SwarmNode | |
| from core.aggregation_protocol.types import PermissionLevel | |
| self.node = SwarmNode( | |
| node_id="web_user", | |
| name="Web用户", | |
| permission_level=PermissionLevel.QUEEN, | |
| model_config="tiny", | |
| initial_balance=100.0, | |
| ) | |
| self.node.start() | |
| self.initialized = True | |
| return True | |
| except Exception as e: | |
| print(f"[WebApp] 初始化失败: {e}") | |
| import traceback; traceback.print_exc() | |
| return False | |
| state = SwarmAppState() | |
| # ============================================================ | |
| # HTML页面 | |
| # ============================================================ | |
| HTML_PAGE = """ | |
| <!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>虫群 Swarm</title> | |
| <style> | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; } | |
| .sidebar { width: 240px; background: #111; border-right: 1px solid #222; | |
| display: flex; flex-direction: column; } | |
| .sidebar-header { padding: 16px; border-bottom: 1px solid #222; } | |
| .sidebar-header h2 { font-size: 18px; color: #4ecdc4; } | |
| .sidebar-header p { font-size: 12px; color: #666; margin-top: 4px; } | |
| .stat-item { padding: 10px 16px; border-bottom: 1px solid #1a1a1a; font-size: 13px; } | |
| .stat-label { color: #666; font-size: 11px; } | |
| .stat-value { color: #4ecdc4; font-weight: bold; } | |
| .main { flex:1; display: flex; flex-direction: column; } | |
| .chat-header { padding: 12px 20px; border-bottom: 1px solid #222; | |
| background: #111; display: flex; justify-content: space-between; align-items: center; } | |
| .chat-header h3 { font-size: 15px; } | |
| .mode-select { background: #1a1a1a; color: #e0e0e0; border: 1px solid #333; | |
| padding: 6px 12px; border-radius: 6px; font-size: 12px; } | |
| .messages { flex:1; overflow-y: auto; padding: 20px; } | |
| .msg { margin-bottom: 16px; max-width: 80%; } | |
| .msg.user { margin-left: auto; } | |
| .msg-bubble { padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.5; } | |
| .msg.user .msg-bubble { background: #1a3a4a; color: #e0e0e0; border-bottom-right-radius: 4px; } | |
| .msg.assistant .msg-bubble { background: #1a1a2e; color: #e0e0e0; border-bottom-left-radius: 4px; } | |
| .msg-meta { font-size: 11px; color: #555; margin-top: 4px; } | |
| .msg-source { color: #4ecdc4; } | |
| .input-area { padding: 16px; border-top: 1px solid #222; background: #111; | |
| display: flex; gap: 10px; } | |
| .input-area input { flex:1; background: #1a1a1a; border: 1px solid #333; color: #e0e0e0; | |
| padding: 10px 14px; border-radius: 8px; font-size: 14px; outline: none; } | |
| .input-area input:focus { border-color: #4ecdc4; } | |
| .input-area button { background: #4ecdc4; color: #000; border: none; padding: 10px 20px; | |
| border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 14px; } | |
| .input-area button:hover { background: #3dbdb5; } | |
| .input-area button:disabled { background: #333; color: #666; cursor: not-allowed; } | |
| .loading { display: none; color: #4ecdc4; font-size: 13px; padding: 4px 0; } | |
| .loading.active { display: block; } | |
| .tag { display: inline-block; background: #1a2a2a; color: #4ecdc4; font-size: 11px; | |
| padding: 2px 8px; border-radius: 4px; margin-right: 4px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="sidebar"> | |
| <div class="sidebar-header"> | |
| <h2>🦠 虫群 Swarm</h2> | |
| <p>v8.0 分布式智能体</p> | |
| </div> | |
| <div id="stats"> | |
| <div class="stat-item"><div class="stat-label">状态</div><div class="stat-value" id="st-status">初始化中...</div></div> | |
| <div class="stat-item"><div class="stat-label">本地模型</div><div class="stat-value" id="st-model">-</div></div> | |
| <div class="stat-item"><div class="stat-label">API模型</div><div class="stat-value" id="st-api">-</div></div> | |
| <div class="stat-item"><div class="stat-label">记忆数</div><div class="stat-value" id="st-mem">0</div></div> | |
| <div class="stat-item"><div class="stat-label">余额</div><div class="stat-value" id="st-bal">0 CC</div></div> | |
| <div class="stat-item"><div class="stat-label">查询次数</div><div class="stat-value" id="st-queries">0</div></div> | |
| </div> | |
| </div> | |
| <div class="main"> | |
| <div class="chat-header"> | |
| <h3>💬 对话</h3> | |
| <div> | |
| <select class="mode-select" id="mode"> | |
| <option value="smart">智能模式(记忆+API+MOA)</option> | |
| <option value="local">仅本地记忆</option> | |
| <option value="api">仅API推理</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="messages" id="messages"></div> | |
| <div class="loading" id="loading">🦠 虫群思考中...</div> | |
| <div class="input-area"> | |
| <input id="input" placeholder="输入消息..." autofocus /> | |
| <button id="send" onclick="sendMessage()">发送</button> | |
| </div> | |
| </div> | |
| <script> | |
| const msgs = document.getElementById('messages'); | |
| const inp = document.getElementById('input'); | |
| const loading = document.getElementById('loading'); | |
| const sendBtn = document.getElementById('send'); | |
| let busy = false; | |
| inp.addEventListener('keydown', e => { if(e.key==='Enter') sendMessage(); }); | |
| async function sendMessage() { | |
| const text = inp.value.trim(); | |
| if(!text || busy) return; | |
| busy = true; | |
| inp.value = ''; | |
| sendBtn.disabled = true; | |
| loading.classList.add('active'); | |
| addMsg('user', text); | |
| const mode = document.getElementById('mode').value; | |
| const useApi = mode !== 'local'; | |
| const useMoa = mode === 'smart'; | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 60000); | |
| try { | |
| const resp = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({message: text, use_api: useApi, use_moa: useMoa}), | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeout); | |
| const data = await resp.json(); | |
| let sourceTag = ''; | |
| if(data.source) sourceTag = '<span class="tag">' + data.source + '</span>'; | |
| if(data.contributors && data.contributors.length) | |
| sourceTag += data.contributors.map(c => '<span class="tag">' + c + '</span>').join(''); | |
| const meta = '<div class="msg-meta">' + sourceTag + | |
| ' 置信度:' + (data.confidence||0).toFixed(2) + | |
| ' 延迟:' + Math.round(data.latency_ms||0) + 'ms</div>'; | |
| addMsg('assistant', data.response || '(无回答)', meta); | |
| // 更新统计 | |
| if(data.detail) { | |
| document.getElementById('st-queries').textContent = | |
| (parseInt(document.getElementById('st-queries').textContent) + 1); | |
| } | |
| } catch(e) { | |
| addMsg('assistant', '❌ 请求失败: ' + e.message); | |
| } | |
| busy = false; | |
| sendBtn.disabled = false; | |
| loading.classList.remove('active'); | |
| } | |
| function addMsg(role, text, meta='') { | |
| const div = document.createElement('div'); | |
| div.className = 'msg ' + role; | |
| div.innerHTML = '<div class="msg-bubble">' + escapeHtml(text) + '</div>' + meta; | |
| msgs.appendChild(div); | |
| msgs.scrollTop = msgs.scrollHeight; | |
| } | |
| function escapeHtml(s) { | |
| return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\\n/g,'<br>'); | |
| } | |
| // 初始化状态 | |
| async function loadStatus() { | |
| try { | |
| const r = await fetch('/api/status'); | |
| const d = await r.json(); | |
| document.getElementById('st-status').textContent = d.ready ? '✅ 就绪' : '❌ 未就绪'; | |
| document.getElementById('st-model').textContent = d.model || '-'; | |
| document.getElementById('st-api').textContent = (d.api_models||0) + '个'; | |
| document.getElementById('st-mem').textContent = d.memories || 0; | |
| document.getElementById('st-bal').textContent = (d.balance||0).toFixed(1) + ' CC'; | |
| document.getElementById('st-queries').textContent = d.queries || 0; | |
| } catch(e) { | |
| document.getElementById('st-status').textContent = '❌ 连接失败'; | |
| } | |
| } | |
| loadStatus(); | |
| setInterval(loadStatus, 10000); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # ============================================================ | |
| # API路由 | |
| # ============================================================ | |
| async def index(): | |
| return HTML_PAGE | |
| async def get_status(): | |
| if not state.initialized: | |
| state.initialize() | |
| if not state.initialized or not state.node: | |
| return {"ready": False} | |
| node = state.node | |
| mem_status = node.memory.get_status() | |
| api_count = len(node.api_manager.models) if node._api_models_available else 0 | |
| balance = node.get_balance() | |
| return { | |
| "ready": True, | |
| "model": mem_status.get("model_config", "tiny"), | |
| "api_models": api_count, | |
| "memories": mem_status.get("total_memories", 0), | |
| "balance": balance, | |
| "queries": node.stats.get("queries_processed", 0), | |
| } | |
| async def chat(req: dict): | |
| if not state.initialized: | |
| state.initialize() | |
| if not state.initialized or not state.node: | |
| return {"response": "系统初始化失败", "source": "error", "confidence": 0} | |
| message = req.get("message", "") | |
| use_api = req.get("use_api", True) | |
| use_moa = req.get("use_moa", True) | |
| # 存储用户消息 | |
| state.chat_history.append({"role": "user", "content": message}) | |
| # 智能查询 | |
| result = state.node.smart_query( | |
| message, use_api=use_api, use_moa=use_moa | |
| ) | |
| # 存储AI回答到记忆 | |
| if result.get("response"): | |
| state.node.store_memory(message, result["response"], importance=0.5) | |
| state.chat_history.append({"role": "assistant", "content": result["response"]}) | |
| return result | |
| async def store_memory(req: dict): | |
| """手动存储记忆""" | |
| if not state.initialized or not state.node: | |
| return {"ok": False} | |
| mid = state.node.store_memory( | |
| req.get("question", ""), | |
| req.get("answer", ""), | |
| importance=req.get("importance", 0.5) | |
| ) | |
| return {"ok": True, "memory_id": mid} | |
| async def memory_stats(): | |
| if not state.initialized or not state.node: | |
| return {"total": 0} | |
| return state.node.memory.get_status() | |
| # 启动 | |
| if __name__ == "__main__": | |
| import uvicorn | |
| print("=" * 50) | |
| print("🦠 虫群 Swarm Web界面") | |
| print("=" * 50) | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |