app.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. """
  2. AI MCP Web UI - Flask 后端
  3. 提供聊天界面与 MCP 工具调用的桥梁
  4. """
  5. import os
  6. import asyncio
  7. from typing import Optional, Dict
  8. from flask import Flask, request, jsonify, send_from_directory
  9. from flask_cors import CORS
  10. import httpx
  11. from anthropic import Anthropic
  12. from config import MCP_SERVERS, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL
  13. from conversation_manager import ConversationManager
  14. app = Flask(__name__)
  15. CORS(app)
  16. app.secret_key = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
  17. # 存储认证会话 (生产环境应使用 Redis 或数据库)
  18. auth_sessions: Dict[str, dict] = {}
  19. @app.route('/')
  20. def index():
  21. return send_from_directory('../frontend', 'index.html')
  22. @app.route('/<path:path>')
  23. def static_files(path):
  24. return send_from_directory('../frontend', path)
  25. # 初始化 Claude 客户端
  26. client = Anthropic(
  27. api_key=ANTHROPIC_API_KEY,
  28. base_url=ANTHROPIC_BASE_URL
  29. )
  30. @app.route('/api/health', methods=['GET'])
  31. def health():
  32. """健康检查端点"""
  33. return jsonify({
  34. "status": "ok",
  35. "model": ANTHROPIC_MODEL,
  36. "mcp_servers": list(MCP_SERVERS.keys())
  37. })
  38. def run_async(coro):
  39. """在同步上下文中运行异步函数"""
  40. loop = asyncio.new_event_loop()
  41. asyncio.set_event_loop(loop)
  42. try:
  43. return loop.run_until_complete(coro)
  44. finally:
  45. loop.close()
  46. @app.route('/api/chat', methods=['POST'])
  47. def chat():
  48. """
  49. 聊天端点 - 接收用户消息,返回 Claude 响应(支持 MCP 工具调用)
  50. """
  51. try:
  52. data = request.json
  53. message = data.get('message', '')
  54. conversation_history = data.get('history', [])
  55. session_id = request.headers.get('X-Session-ID')
  56. if not message:
  57. return jsonify({"error": "Message is required"}), 400
  58. # 创建对话管理器
  59. conv_manager = ConversationManager(
  60. api_key=ANTHROPIC_API_KEY,
  61. base_url=ANTHROPIC_BASE_URL,
  62. model=ANTHROPIC_MODEL,
  63. session_id=session_id
  64. )
  65. # 格式化对话历史
  66. formatted_history = ConversationManager.format_history_for_claude(conversation_history)
  67. # 执行多轮对话(自动处理工具调用)
  68. result = run_async(conv_manager.chat(
  69. user_message=message,
  70. conversation_history=formatted_history,
  71. max_turns=5
  72. ))
  73. # 提取响应文本
  74. response_text = result.get("response", "")
  75. tool_calls = result.get("tool_calls", [])
  76. return jsonify({
  77. "response": response_text,
  78. "model": ANTHROPIC_MODEL,
  79. "tool_calls": tool_calls,
  80. "has_tools": len(tool_calls) > 0
  81. })
  82. except Exception as e:
  83. import traceback
  84. return jsonify({
  85. "error": str(e),
  86. "traceback": traceback.format_exc()
  87. }), 500
  88. @app.route('/api/mcp/servers', methods=['GET'])
  89. def list_mcp_servers():
  90. """获取已配置的 MCP 服务器列表"""
  91. servers = []
  92. for name, server in MCP_SERVERS.items():
  93. servers.append({
  94. "id": name,
  95. "name": server.get("name", name),
  96. "url": server.get("url", ""),
  97. "auth_type": server.get("auth_type", "none"),
  98. "enabled": server.get("enabled", False)
  99. })
  100. return jsonify({"servers": servers})
  101. @app.route('/api/mcp/tools', methods=['GET'])
  102. def list_mcp_tools():
  103. """获取可用的 MCP 工具列表"""
  104. try:
  105. session_id = request.headers.get('X-Session-ID')
  106. # 使用静态方法获取工具
  107. tools = ConversationManager.get_tools(session_id=session_id)
  108. return jsonify({
  109. "tools": tools,
  110. "count": len(tools)
  111. })
  112. except Exception as e:
  113. import traceback
  114. return jsonify({
  115. "error": str(e),
  116. "traceback": traceback.format_exc(),
  117. "tools": []
  118. }), 500
  119. # ========== 认证 API ==========
  120. @app.route('/api/auth/login', methods=['POST'])
  121. def login():
  122. """
  123. Novel Platform 用户登录
  124. 代理到实际的登录端点并返回 JWT Token
  125. """
  126. try:
  127. data = request.json
  128. username = data.get('username')
  129. password = data.get('password')
  130. if not username or not password:
  131. return jsonify({"error": "Username and password are required"}), 400
  132. # 查找需要 JWT 认证的 MCP 服务器
  133. target_server = None
  134. for server_id, config in MCP_SERVERS.items():
  135. if config.get('auth_type') == 'jwt' and 'login_url' in config:
  136. target_server = config
  137. break
  138. if not target_server:
  139. return jsonify({"error": "No JWT-authenticated server configured"}), 400
  140. # 构建登录 URL
  141. base_url = target_server.get('base_url', '')
  142. login_path = target_server.get('login_url', '/api/auth/login')
  143. login_url = f"{base_url}{login_path}"
  144. # 调用实际的登录接口(同步版本)
  145. response = httpx.post(
  146. login_url,
  147. json={"username": username, "password": password},
  148. timeout=30.0
  149. )
  150. if response.status_code == 200:
  151. result = response.json()
  152. import uuid
  153. session_id = str(uuid.uuid4())
  154. # 存储会话信息
  155. auth_sessions[session_id] = {
  156. "username": username,
  157. "token": result.get("token"),
  158. "refresh_token": result.get("refresh_token"),
  159. "server": target_server.get("name")
  160. }
  161. return jsonify({
  162. "success": True,
  163. "session_id": session_id,
  164. "username": username,
  165. "server": target_server.get("name"),
  166. "token": result.get("token")
  167. })
  168. else:
  169. return jsonify({
  170. "error": "Login failed",
  171. "details": response.text
  172. }), response.status_code
  173. except Exception as e:
  174. return jsonify({"error": str(e)}), 500
  175. @app.route('/api/auth/admin-login', methods=['POST'])
  176. def admin_login():
  177. """
  178. Novel Platform 管理员登录
  179. """
  180. try:
  181. data = request.json
  182. username = data.get('username')
  183. password = data.get('password')
  184. if not username or not password:
  185. return jsonify({"error": "Username and password are required"}), 400
  186. # 查找管理员 MCP 服务器
  187. target_server = MCP_SERVERS.get('novel-platform-admin')
  188. if not target_server:
  189. return jsonify({"error": "Admin server not configured"}), 400
  190. # 构建登录 URL
  191. base_url = target_server.get('base_url', '')
  192. login_path = target_server.get('login_url', '/api/auth/admin-login')
  193. login_url = f"{base_url}{login_path}"
  194. # 调用实际的登录接口
  195. response = httpx.post(
  196. login_url,
  197. json={"username": username, "password": password},
  198. timeout=30.0
  199. )
  200. if response.status_code == 200:
  201. result = response.json()
  202. import uuid
  203. session_id = str(uuid.uuid4())
  204. auth_sessions[session_id] = {
  205. "username": username,
  206. "token": result.get("token"),
  207. "refresh_token": result.get("refresh_token"),
  208. "server": target_server.get("name"),
  209. "role": "admin"
  210. }
  211. return jsonify({
  212. "success": True,
  213. "session_id": session_id,
  214. "username": username,
  215. "server": target_server.get("name"),
  216. "role": "admin",
  217. "token": result.get("token")
  218. })
  219. else:
  220. return jsonify({
  221. "error": "Admin login failed",
  222. "details": response.text
  223. }), response.status_code
  224. except Exception as e:
  225. return jsonify({"error": str(e)}), 500
  226. @app.route('/api/auth/logout', methods=['POST'])
  227. def logout():
  228. """登出并清除会话"""
  229. try:
  230. data = request.json
  231. session_id = data.get('session_id')
  232. if session_id and session_id in auth_sessions:
  233. del auth_sessions[session_id]
  234. return jsonify({"success": True})
  235. except Exception as e:
  236. return jsonify({"error": str(e)}), 500
  237. @app.route('/api/auth/status', methods=['GET'])
  238. def auth_status():
  239. """检查认证状态"""
  240. session_id = request.headers.get('X-Session-ID')
  241. if session_id and session_id in auth_sessions:
  242. session = auth_sessions[session_id]
  243. return jsonify({
  244. "authenticated": True,
  245. "username": session.get("username"),
  246. "server": session.get("server"),
  247. "role": session.get("role", "user")
  248. })
  249. return jsonify({
  250. "authenticated": False
  251. })
  252. if __name__ == '__main__':
  253. port = int(os.getenv('PORT', 5000))
  254. debug = os.getenv('DEBUG', 'False').lower() == 'true'
  255. app.run(host='0.0.0.0', port=port, debug=debug)