app.py 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556
  1. import os
  2. import json
  3. import uuid
  4. import time
  5. import logging
  6. from typing import Dict, Any, Generator, Optional, List
  7. from flask_cors import CORS
  8. import requests
  9. from flask import Flask, request, jsonify, render_template, session, Response, stream_with_context, redirect
  10. from http import HTTPStatus
  11. from dotenv import load_dotenv, find_dotenv
  12. from dashscope import Application
  13. from datetime import datetime
  14. # 先加载环境变量,确保后续模块读取到 .env(支持不同工作目录)
  15. load_dotenv(find_dotenv(), override=False)
  16. # 导入认证模块
  17. from auth_middleware import init_auth
  18. from auth_routes import auth_bp
  19. # 导入配置和工具
  20. from config import config
  21. from utils import (
  22. sanitize_input, validate_user_id, rate_limit,
  23. truncate_conversation_history, log_request_info, get_beijing_time,
  24. generate_session_id
  25. )
  26. # 导入数据库模型
  27. from models import init_database, db, User, Conversation, Message, get_or_create_user
  28. # 配置日志
  29. logging.basicConfig(
  30. level=getattr(logging, config.log_level),
  31. format=config.log_format
  32. )
  33. logger = logging.getLogger(__name__)
  34. # 创建Flask应用
  35. app = Flask(__name__)
  36. CORS(app) # 启用CORS支持
  37. # 配置session
  38. app.secret_key = config.secret_key
  39. app.config['PERMANENT_SESSION_LIFETIME'] = 7 * 24 * 60 * 60 # 7天
  40. app.config['SESSION_COOKIE_SECURE'] = False # 开发环境设为False
  41. app.config['SESSION_COOKIE_HTTPONLY'] = True
  42. app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
  43. # 配置数据库
  44. app.config['SQLALCHEMY_DATABASE_URI'] = config.get_database_url()
  45. app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
  46. app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
  47. 'pool_pre_ping': True,
  48. 'pool_recycle': 300,
  49. 'pool_timeout': 20,
  50. 'max_overflow': 0
  51. }
  52. # 初始化数据库
  53. try:
  54. init_database(app)
  55. except Exception as e:
  56. logger.error(f"数据库初始化失败: {str(e)}", exc_info=True)
  57. raise
  58. # 初始化认证中间件
  59. auth_middleware = init_auth(app)
  60. app.auth_middleware = auth_middleware
  61. # 注册认证蓝图
  62. app.register_blueprint(auth_bp)
  63. # 存储用户当前对话ID的字典(保留用于兼容性)
  64. user_current_conversations: Dict[str, int] = {}
  65. def save_message_to_database(user_id: int, conversation_id: int, role: str, content: str,
  66. thinking_content: str = None, message_order: int = 1) -> Message:
  67. """保存消息到数据库"""
  68. try:
  69. message = Message(
  70. conversation_id=conversation_id,
  71. role=role,
  72. content=content,
  73. thinking_content=thinking_content,
  74. content_type='text',
  75. message_order=message_order
  76. )
  77. db.session.add(message)
  78. # 更新对话的消息计数和最后消息时间
  79. conversation = Conversation.query.get(conversation_id)
  80. if conversation:
  81. conversation.message_count += 1
  82. conversation.last_message_at = get_beijing_time()
  83. conversation.updated_at = get_beijing_time()
  84. # 一次性提交所有更改
  85. db.session.commit()
  86. return message
  87. except Exception as e:
  88. db.session.rollback()
  89. logger.error(f"保存消息到数据库时发生错误: {str(e)}", exc_info=True)
  90. raise
  91. def get_or_create_conversation(user_id: int, title: str = None, conversation_id: int = None, force_new: bool = False) -> Conversation:
  92. """获取或创建对话"""
  93. try:
  94. # 如果强制创建新对话,直接创建
  95. if force_new:
  96. if not title:
  97. title = f"对话 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}"
  98. # 生成新的session_id
  99. session_id = generate_session_id(user_id)
  100. conversation = Conversation(
  101. user_id=user_id,
  102. title=title,
  103. conversation_type='chat',
  104. session_id=session_id,
  105. message_count=0
  106. )
  107. db.session.add(conversation)
  108. db.session.commit()
  109. # 更新用户的当前对话ID
  110. user_current_conversations[str(user_id)] = conversation.conversation_id
  111. return conversation
  112. # 如果指定了conversation_id,验证并返回该对话
  113. if conversation_id:
  114. conversation = Conversation.query.filter_by(
  115. conversation_id=conversation_id,
  116. user_id=user_id,
  117. is_deleted=False
  118. ).first()
  119. if conversation:
  120. # 如果对话没有session_id,生成一个
  121. if not conversation.session_id:
  122. conversation.session_id = generate_session_id(user_id, conversation_id)
  123. db.session.commit()
  124. # 更新用户的当前对话ID
  125. user_current_conversations[str(user_id)] = conversation.conversation_id
  126. return conversation
  127. # 检查用户是否有当前对话映射
  128. current_conv_id = user_current_conversations.get(str(user_id))
  129. if current_conv_id:
  130. conversation = Conversation.query.filter_by(
  131. conversation_id=current_conv_id,
  132. user_id=user_id,
  133. is_deleted=False
  134. ).first()
  135. if conversation:
  136. # 如果对话没有session_id,生成一个
  137. if not conversation.session_id:
  138. conversation.session_id = generate_session_id(user_id, current_conv_id)
  139. db.session.commit()
  140. return conversation
  141. # 创建新对话
  142. if not title:
  143. title = f"对话 {get_beijing_time().strftime('%Y-%m-%d %H:%M')}"
  144. # 生成新的session_id
  145. session_id = generate_session_id(user_id)
  146. conversation = Conversation(
  147. user_id=user_id,
  148. title=title,
  149. conversation_type='chat',
  150. session_id=session_id,
  151. message_count=0
  152. )
  153. db.session.add(conversation)
  154. db.session.commit()
  155. # 更新用户的当前对话ID
  156. user_current_conversations[str(user_id)] = conversation.conversation_id
  157. return conversation
  158. except Exception as e:
  159. db.session.rollback()
  160. logger.error(f"获取或创建对话时发生错误: {str(e)}", exc_info=True)
  161. raise
  162. def switch_to_conversation(user_id: int, conversation_id: int) -> Conversation:
  163. """切换到指定对话"""
  164. try:
  165. # 验证对话是否属于当前用户
  166. conversation = Conversation.query.filter_by(
  167. conversation_id=conversation_id,
  168. user_id=user_id,
  169. is_deleted=False
  170. ).first()
  171. if not conversation:
  172. raise ValueError(f"对话 {conversation_id} 不存在或无权限访问")
  173. # 如果对话没有session_id,生成一个
  174. if not conversation.session_id:
  175. conversation.session_id = generate_session_id(user_id, conversation_id)
  176. db.session.commit()
  177. # 更新用户的当前对话ID
  178. user_current_conversations[str(user_id)] = conversation.conversation_id
  179. return conversation
  180. except Exception as e:
  181. logger.error(f"切换对话时发生错误: {str(e)}", exc_info=True)
  182. raise
  183. def generate_conversation_title(user_message: str) -> str:
  184. """根据用户消息生成对话标题"""
  185. # 取用户消息的前50个字符作为标题
  186. title = user_message.strip()[:50]
  187. if len(user_message) > 50:
  188. title += "..."
  189. return title
  190. @app.route('/')
  191. def index():
  192. """渲染聊天界面"""
  193. # 检查是否是登录成功跳转
  194. login_success = request.args.get('login_success')
  195. token = request.args.get('token')
  196. user_info_json = request.args.get('user_info')
  197. if login_success and token and user_info_json:
  198. # 登录成功,处理用户信息
  199. try:
  200. import urllib.parse
  201. import json
  202. user_info = json.loads(urllib.parse.unquote(user_info_json))
  203. # 保存到session
  204. session['auth_token'] = token
  205. session['user_info'] = user_info
  206. # 将用户信息传递给模板
  207. return render_template('index.html', user_info=user_info, jwt_token=token)
  208. except Exception as e:
  209. logger.error(f'处理登录成功参数失败: {e}')
  210. # 如果处理失败,重定向到登录页面
  211. return redirect('/login')
  212. else:
  213. # 正常访问,检查认证状态
  214. # 优先从session获取
  215. user_info = session.get('user_info', {})
  216. jwt_token = session.get('auth_token', '')
  217. # 如果session中没有,尝试从localStorage获取(通过JavaScript)
  218. if not jwt_token:
  219. # 返回一个页面,让JavaScript检查localStorage
  220. return render_template('index.html', user_info={}, jwt_token='')
  221. # 将用户信息传递给模板
  222. return render_template('index.html', user_info=user_info, jwt_token=jwt_token)
  223. @app.route('/login')
  224. def login():
  225. """登录页面"""
  226. return render_template('login.html')
  227. @app.route('/login_error')
  228. def login_error():
  229. """登录错误页面"""
  230. error_type = request.args.get('error', 'unknown')
  231. error_message = request.args.get('message', '')
  232. error_code = request.args.get('code', '')
  233. return render_template('login_error.html',
  234. error_type=error_type,
  235. error_message=error_message,
  236. error_code=error_code)
  237. @app.route('/api/chat', methods=['POST'])
  238. @auth_middleware.login_required
  239. @rate_limit(config.rate_limit_requests, config.rate_limit_window)
  240. def chat():
  241. """处理聊天请求并调用阿里云百炼API (非流式响应)"""
  242. try:
  243. data = request.json or {}
  244. message = data.get('message', '')
  245. user_id = data.get('user_id', str(uuid.uuid4()))
  246. agent = data.get('agent') or 'kuaishou'
  247. conversation_id = data.get('conversation_id') # 新增:支持指定对话ID
  248. session_id = data.get('session_id') # 新增:支持指定session_id
  249. force_new = data.get('force_new', False) # 新增:强制创建新对话
  250. # 输入验证
  251. message, error = sanitize_input(message, config.max_message_length)
  252. if error:
  253. return jsonify({
  254. 'success': False,
  255. 'error': error
  256. }), HTTPStatus.BAD_REQUEST
  257. user_id, error = validate_user_id(user_id)
  258. if error:
  259. return jsonify({
  260. 'success': False,
  261. 'error': error
  262. }), HTTPStatus.BAD_REQUEST
  263. # 从认证中间件获取用户信息
  264. auth_user_info = getattr(request, 'user_info', {})
  265. auth_user_id = auth_user_info.get('db_user_id') # 使用数据库用户ID
  266. # 选择对应智能体 APP_ID
  267. chosen_app_id, appid_env_key = config.get_app_id_for_agent_with_source(agent)
  268. if not chosen_app_id:
  269. logger.error(f"未找到对应智能体APP_ID,agent={agent}")
  270. return jsonify({
  271. 'success': False,
  272. 'error': f"服务器未配置该智能体的 APP_ID: {agent}"
  273. }), HTTPStatus.INTERNAL_SERVER_ERROR
  274. # 记录请求信息
  275. log_request_info(message, user_id, 'POST')
  276. # 获取或创建对话
  277. conversation = get_or_create_conversation(
  278. auth_user_id,
  279. generate_conversation_title(message),
  280. conversation_id,
  281. force_new
  282. )
  283. # 优先使用前端传来的session_id,否则使用对话的session_id
  284. api_session_id = session_id or conversation.session_id
  285. # 准备API请求参数
  286. headers = {
  287. "Authorization": f"Bearer {config.api_key}",
  288. "Content-Type": "application/json",
  289. "X-DashScope-SSE": "disable" # 禁用流式输出
  290. }
  291. # 从数据库获取对话历史
  292. messages_query = Message.query.filter_by(
  293. conversation_id=conversation.conversation_id,
  294. is_deleted=False
  295. ).order_by(Message.message_order.asc()).all()
  296. # 构建多轮对话历史
  297. messages = []
  298. for msg in messages_query:
  299. messages.append({"role": msg.role, "content": msg.content})
  300. # 添加用户新消息到对话历史
  301. messages.append({"role": "user", "content": message})
  302. # 截断对话历史以控制内存使用
  303. messages = truncate_conversation_history(messages, config.max_conversation_history)
  304. payload = {
  305. "model": "application",
  306. "parameters": {"app_id": chosen_app_id},
  307. "input": {"messages": messages} # 发送完整的对话历史
  308. }
  309. # 如果有会话ID,添加到请求参数中
  310. if api_session_id:
  311. payload["parameters"]["session_id"] = api_session_id
  312. # 发送API请求
  313. response = requests.post(
  314. config.api_endpoint,
  315. headers=headers,
  316. json=payload,
  317. timeout=60
  318. )
  319. # 转换为JSON
  320. response_json = response.json()
  321. if response.status_code != HTTPStatus.OK:
  322. return jsonify({
  323. 'success': False,
  324. 'error': f"错误: {response.status_code} - {response_json.get('message', '未知错误')}",
  325. 'request_id': response_json.get('request_id', ''),
  326. 'agent': agent
  327. }), HTTPStatus.INTERNAL_SERVER_ERROR
  328. # 解析返回结果
  329. output = response_json.get('output', {})
  330. choices = output.get('choices', [{}])
  331. text = choices[0].get('message', {}).get('content', '') if choices else ''
  332. new_session_id = output.get('session_id', '')
  333. thoughts_content = output.get('thoughts', [])
  334. # 更新对话的session_id(如果API返回了新的session_id且与当前不同)
  335. if new_session_id and new_session_id != api_session_id:
  336. conversation.session_id = new_session_id
  337. db.session.commit()
  338. elif not api_session_id and new_session_id:
  339. # 如果对话原本没有session_id,现在有了,更新数据库
  340. conversation.session_id = new_session_id
  341. db.session.commit()
  342. # 保存到数据库
  343. try:
  344. # 获取当前对话的消息数量,用于设置消息顺序
  345. current_message_count = Message.query.filter_by(
  346. conversation_id=conversation.conversation_id,
  347. is_deleted=False
  348. ).count()
  349. # 保存用户消息
  350. save_message_to_database(
  351. auth_user_id,
  352. conversation.conversation_id,
  353. 'user',
  354. message,
  355. message_order=current_message_count + 1
  356. )
  357. # 保存AI回复
  358. if text:
  359. thinking_text = '\n'.join(thoughts_content) if thoughts_content else None
  360. save_message_to_database(
  361. auth_user_id,
  362. conversation.conversation_id,
  363. 'assistant',
  364. text,
  365. thinking_text,
  366. message_order=current_message_count + 2
  367. )
  368. # 更新对话标题(如果是第一条消息)
  369. if conversation.message_count <= 2: # 用户消息 + AI回复
  370. new_title = generate_conversation_title(message)
  371. # 检查是否是默认标题("新对话"或"对话 时间"格式)
  372. is_default_title = (
  373. conversation.title == "新对话" or
  374. conversation.title == f"对话 {conversation.created_at.strftime('%Y-%m-%d %H:%M')}"
  375. )
  376. if is_default_title:
  377. conversation.title = new_title
  378. db.session.commit()
  379. except Exception as db_error:
  380. logger.error(f"保存对话到数据库时发生错误: {str(db_error)}", exc_info=True)
  381. # 不影响主要功能,继续返回结果
  382. return jsonify({
  383. 'success': True,
  384. 'message': text,
  385. 'user_id': user_id,
  386. 'session_id': new_session_id or api_session_id,
  387. 'thoughts': thoughts_content,
  388. 'agent': agent,
  389. 'conversation_id': conversation.conversation_id
  390. })
  391. except requests.exceptions.RequestException as e:
  392. logger.error(f"API请求异常: {str(e)}", exc_info=True)
  393. return jsonify({
  394. 'success': False,
  395. 'error': f"API请求异常: {str(e)}"
  396. }), HTTPStatus.SERVICE_UNAVAILABLE
  397. except Exception as e:
  398. logger.error(f"API调用错误: {str(e)}", exc_info=True)
  399. return jsonify({
  400. 'success': False,
  401. 'error': f"发生错误: {str(e)}"
  402. }), HTTPStatus.INTERNAL_SERVER_ERROR
  403. @app.route('/api/chat/stream', methods=['GET', 'POST'])
  404. @auth_middleware.login_required
  405. @rate_limit(config.rate_limit_requests, config.rate_limit_window)
  406. def chat_stream():
  407. """处理聊天请求并以流式方式返回响应"""
  408. try:
  409. # 从请求中获取数据
  410. if request.method == 'GET':
  411. message = request.args.get('message', '')
  412. user_id = request.args.get('user_id', str(uuid.uuid4()))
  413. agent = request.args.get('agent') or 'kuaishou'
  414. conversation_id = request.args.get('conversation_id') # 支持指定对话ID
  415. else: # POST
  416. data = request.json or {}
  417. message = data.get('message', '')
  418. user_id = data.get('user_id', str(uuid.uuid4()))
  419. agent = data.get('agent') or 'kuaishou'
  420. conversation_id = data.get('conversation_id') # 支持指定对话ID
  421. session_id = data.get('session_id') # 支持指定session_id
  422. force_new = data.get('force_new', False) # 强制创建新对话
  423. # 输入验证
  424. message, error = sanitize_input(message, config.max_message_length)
  425. if error:
  426. return Response(
  427. f"data: {json.dumps({'success': False, 'error': error, 'done': True})}\n\n",
  428. content_type='text/event-stream'
  429. )
  430. user_id, error = validate_user_id(user_id)
  431. if error:
  432. return Response(
  433. f"data: {json.dumps({'success': False, 'error': error, 'done': True})}\n\n",
  434. content_type='text/event-stream'
  435. )
  436. # 选择对应智能体 APP_ID
  437. chosen_app_id, appid_env_key = config.get_app_id_for_agent_with_source(agent)
  438. if not chosen_app_id:
  439. logger.error(f"未找到对应智能体APP_ID,agent={agent}")
  440. return Response(
  441. f"data: {json.dumps({'success': False, 'error': f'服务器未配置该智能体的 APP_ID: {agent}', 'done': True})}\n\n",
  442. content_type='text/event-stream'
  443. )
  444. # 记录请求信息
  445. log_request_info(message, user_id, request.method)
  446. # 从认证中间件获取用户信息
  447. auth_user_info = getattr(request, 'user_info', {})
  448. auth_user_id = auth_user_info.get('db_user_id') # 使用数据库用户ID
  449. # 获取或创建对话
  450. conversation = get_or_create_conversation(
  451. auth_user_id,
  452. generate_conversation_title(message),
  453. conversation_id,
  454. force_new
  455. )
  456. # 优先使用前端传来的session_id,否则使用对话的session_id
  457. api_session_id = session_id or conversation.session_id
  458. # 从数据库获取对话历史
  459. messages_query = Message.query.filter_by(
  460. conversation_id=conversation.conversation_id,
  461. is_deleted=False
  462. ).order_by(Message.message_order.asc()).all()
  463. # 构建多轮对话历史
  464. messages = []
  465. for msg in messages_query:
  466. messages.append({"role": msg.role, "content": msg.content})
  467. # 添加用户新消息到对话历史
  468. messages.append({"role": "user", "content": message})
  469. # 截断对话历史以控制内存使用
  470. messages = truncate_conversation_history(messages, config.max_conversation_history)
  471. # 立即保存用户消息到数据库(在流式响应开始前)
  472. try:
  473. # 获取当前对话的消息数量,用于设置消息顺序
  474. current_message_count = Message.query.filter_by(
  475. conversation_id=conversation.conversation_id,
  476. is_deleted=False
  477. ).count()
  478. # 立即保存用户消息
  479. user_message = save_message_to_database(
  480. auth_user_id,
  481. conversation.conversation_id,
  482. 'user',
  483. message,
  484. message_order=current_message_count + 1
  485. )
  486. except Exception as db_error:
  487. logger.error(f"保存用户消息到数据库时发生错误: {str(db_error)}", exc_info=True)
  488. return Response(
  489. f"data: {json.dumps({'success': False, 'error': f'保存用户消息失败: {str(db_error)}', 'done': True})}\n\n",
  490. content_type='text/event-stream'
  491. )
  492. # 定义流式响应生成器
  493. def generate() -> Generator[str, None, None]:
  494. try:
  495. # 发送初始响应,包含用户消息ID,防止浏览器超时
  496. initial_data = {
  497. 'success': True,
  498. 'chunk': '',
  499. 'done': False,
  500. 'agent': agent,
  501. 'user_message_id': user_message.message_id if user_message else None
  502. }
  503. yield f"data: {json.dumps(initial_data)}\n\n"
  504. try:
  505. # 使用DashScope SDK直接调用 - 多轮对话模式
  506. params = {
  507. "api_key": config.api_key,
  508. "app_id": chosen_app_id,
  509. "messages": messages, # 发送完整的对话历史
  510. "stream": True,
  511. "incremental_output": True,
  512. "has_thoughts": True
  513. }
  514. # 使用对话的session_id
  515. if api_session_id:
  516. params["session_id"] = api_session_id
  517. response = Application.call(**params)
  518. new_session_id: Optional[str] = None
  519. accumulated_text: str = ""
  520. thoughts_content: List[str] = []
  521. # 处理流式响应
  522. for chunk in response:
  523. # 检查客户端是否断开连接
  524. if request.environ.get('werkzeug.server.shutdown') or not request:
  525. break
  526. if chunk.status_code != HTTPStatus.OK:
  527. yield f"data: {json.dumps({'success': False, 'error': f'错误: {chunk.status_code} - {chunk.message}', 'request_id': chunk.request_id, 'done': True, 'agent': agent})}\n\n"
  528. return
  529. # 处理思考过程
  530. if hasattr(chunk.output, 'thoughts') and chunk.output.thoughts:
  531. for it in chunk.output.thoughts:
  532. if it.action_type == 'reasoning':
  533. content = str(it.thought) if not isinstance(it.thought, str) else it.thought
  534. thoughts_content.append(content)
  535. yield f"data: {json.dumps({'success': True, 'thought': content, 'done': False, 'agent': agent})}\n\n"
  536. # 处理回答内容
  537. if hasattr(chunk.output, 'text') and chunk.output.text:
  538. current_text = str(chunk.output.text)
  539. accumulated_text += current_text
  540. yield f"data: {json.dumps({'success': True, 'chunk': current_text, 'done': False, 'agent': agent})}\n\n"
  541. # 检查是否有会话ID
  542. if hasattr(chunk.output, 'session_id') and chunk.output.session_id:
  543. new_session_id = chunk.output.session_id
  544. # 处理结束
  545. if new_session_id and new_session_id != api_session_id:
  546. conversation.session_id = new_session_id
  547. db.session.commit()
  548. elif not api_session_id and new_session_id:
  549. # 如果对话原本没有session_id,现在有了,更新数据库
  550. conversation.session_id = new_session_id
  551. db.session.commit()
  552. # 保存AI回复到数据库
  553. try:
  554. if accumulated_text:
  555. thinking_text = '\n'.join(thoughts_content) if thoughts_content else None
  556. ai_message = save_message_to_database(
  557. auth_user_id,
  558. conversation.conversation_id,
  559. 'assistant',
  560. accumulated_text,
  561. thinking_text,
  562. message_order=current_message_count + 2
  563. )
  564. # 更新对话标题(如果是第一条消息)
  565. if conversation.message_count <= 2: # 用户消息 + AI回复
  566. new_title = generate_conversation_title(message)
  567. # 检查是否是默认标题("新对话"或"对话 时间"格式)
  568. is_default_title = (
  569. conversation.title == "新对话" or
  570. conversation.title == f"对话 {conversation.created_at.strftime('%Y-%m-%d %H:%M')}"
  571. )
  572. if is_default_title:
  573. conversation.title = new_title
  574. db.session.commit()
  575. except Exception as db_error:
  576. logger.error(f"保存AI回复到数据库时发生错误: {str(db_error)}", exc_info=True)
  577. # 不影响主要功能,继续返回结果
  578. # 发送完成标记
  579. final_data = {
  580. 'success': True,
  581. 'done': True,
  582. 'thoughts': thoughts_content,
  583. 'agent': agent,
  584. 'session_id': new_session_id or api_session_id,
  585. 'user_id': user_id,
  586. 'conversation_id': conversation.conversation_id
  587. }
  588. yield f"data: {json.dumps(final_data)}\n\n"
  589. except Exception as e:
  590. logger.error(f"DashScope SDK 调用错误: {str(e)}", exc_info=True)
  591. yield f"data: {json.dumps({'success': False, 'error': f'DashScope API调用错误: {str(e)}', 'done': True, 'agent': agent})}\n\n"
  592. except Exception as e:
  593. logger.error(f"流式API调用错误: {str(e)}", exc_info=True)
  594. yield f"data: {json.dumps({'success': False, 'error': f'发生错误: {str(e)}', 'done': True, 'agent': agent})}\n\n"
  595. # 创建并返回流式响应
  596. response = Response(stream_with_context(generate()), content_type='text/event-stream')
  597. response.headers['Cache-Control'] = 'no-cache'
  598. response.headers['X-Accel-Buffering'] = 'no'
  599. return response
  600. except Exception as e:
  601. logger.error(f"请求处理错误: {str(e)}", exc_info=True)
  602. response = Response(
  603. f"data: {json.dumps({'success': False, 'error': f'发生错误: {str(e)}', 'done': True})}\n\n",
  604. content_type='text/event-stream'
  605. )
  606. response.headers['Cache-Control'] = 'no-cache'
  607. return response
  608. @app.route('/api/reset', methods=['POST'])
  609. @auth_middleware.login_required
  610. @rate_limit(10, 60)
  611. def reset_session():
  612. """重置用户会话"""
  613. try:
  614. data = request.json or {}
  615. user_id = data.get('user_id')
  616. if not user_id:
  617. return jsonify({
  618. 'success': True,
  619. 'message': '没有提供用户ID,但重置请求已处理'
  620. })
  621. # 验证用户ID格式
  622. user_id, error = validate_user_id(user_id)
  623. if error:
  624. return jsonify({
  625. 'success': False,
  626. 'error': error
  627. }), HTTPStatus.BAD_REQUEST
  628. # 从认证中间件获取用户信息
  629. auth_user_info = getattr(request, 'user_info', {})
  630. auth_user_id = auth_user_info.get('db_user_id') # 使用数据库用户ID
  631. # 清除数据库中的当前对话ID(如果有认证用户信息)
  632. if auth_user_id and str(auth_user_id) in user_current_conversations:
  633. del user_current_conversations[str(auth_user_id)]
  634. return jsonify({
  635. 'success': True,
  636. 'message': '会话已重置'
  637. })
  638. except Exception as e:
  639. logger.error(f"重置会话时发生错误: {str(e)}", exc_info=True)
  640. return jsonify({
  641. 'success': False,
  642. 'error': f"发生错误: {str(e)}"
  643. }), HTTPStatus.INTERNAL_SERVER_ERROR
  644. @app.route('/api/status', methods=['GET'])
  645. @auth_middleware.login_required
  646. def get_status():
  647. """获取应用状态信息"""
  648. try:
  649. active_users = len(user_current_conversations)
  650. total_conversations = Conversation.query.filter_by(is_deleted=False).count()
  651. return jsonify({
  652. 'success': True,
  653. 'status': 'running',
  654. 'active_users': active_users,
  655. 'total_conversations': total_conversations,
  656. 'app_version': '2.0.0',
  657. 'python_version': '3.12',
  658. 'config': {
  659. 'max_message_length': config.max_message_length,
  660. 'max_conversation_history': config.max_conversation_history,
  661. 'rate_limit_requests': config.rate_limit_requests,
  662. 'rate_limit_window': config.rate_limit_window
  663. }
  664. })
  665. except Exception as e:
  666. logger.error(f"获取状态时发生错误: {str(e)}", exc_info=True)
  667. return jsonify({
  668. 'success': False,
  669. 'error': f"发生错误: {str(e)}"
  670. }), HTTPStatus.INTERNAL_SERVER_ERROR
  671. # ==================== 历史记录相关API ====================
  672. @app.route('/api/conversations', methods=['GET'])
  673. @auth_middleware.login_required
  674. def get_conversations():
  675. """获取用户的历史对话列表,按时间分组"""
  676. try:
  677. # 从认证中间件获取用户信息
  678. user_info = getattr(request, 'user_info', {})
  679. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  680. if not user_id:
  681. logger.warning('获取历史记录失败 - 用户ID为空')
  682. return jsonify({
  683. 'success': False,
  684. 'error': '用户信息获取失败'
  685. }), HTTPStatus.UNAUTHORIZED
  686. # 获取用户对象
  687. user = User.query.filter_by(user_id=user_id, is_deleted=False).first()
  688. if not user:
  689. return jsonify({
  690. 'success': False,
  691. 'error': '用户不存在'
  692. }), HTTPStatus.NOT_FOUND
  693. # 获取用户的对话列表,按更新时间倒序
  694. conversations = Conversation.query.filter_by(
  695. user_id=user_id,
  696. is_deleted=False
  697. ).order_by(Conversation.updated_at.desc()).all()
  698. # 按时间分组
  699. from datetime import datetime, timedelta
  700. now = get_beijing_time()
  701. today = now.date()
  702. yesterday = today - timedelta(days=1)
  703. week_ago = today - timedelta(days=7)
  704. month_ago = today - timedelta(days=30)
  705. grouped_conversations = {
  706. 'today': [],
  707. 'yesterday': [],
  708. 'week': [],
  709. 'month': []
  710. }
  711. for conv in conversations:
  712. conv_date = conv.updated_at.date()
  713. conv_data = {
  714. 'conversation_id': conv.conversation_id,
  715. 'title': conv.title,
  716. 'message_count': conv.message_count,
  717. 'session_id': conv.session_id,
  718. 'last_message_at': conv.last_message_at.isoformat() if conv.last_message_at else None,
  719. 'created_at': conv.created_at.isoformat(),
  720. 'updated_at': conv.updated_at.isoformat()
  721. }
  722. if conv_date == today:
  723. grouped_conversations['today'].append(conv_data)
  724. elif conv_date == yesterday:
  725. grouped_conversations['yesterday'].append(conv_data)
  726. elif conv_date >= week_ago:
  727. grouped_conversations['week'].append(conv_data)
  728. elif conv_date >= month_ago:
  729. grouped_conversations['month'].append(conv_data)
  730. return jsonify({
  731. 'success': True,
  732. 'conversations': grouped_conversations
  733. })
  734. except Exception as e:
  735. logger.error(f"获取对话列表时发生错误: {str(e)}", exc_info=True)
  736. return jsonify({
  737. 'success': False,
  738. 'error': f"发生错误: {str(e)}"
  739. }), HTTPStatus.INTERNAL_SERVER_ERROR
  740. @app.route('/api/conversations/<int:conversation_id>', methods=['GET'])
  741. @auth_middleware.login_required
  742. def get_conversation_messages(conversation_id):
  743. """获取指定对话的所有消息"""
  744. try:
  745. # 从认证中间件获取用户信息
  746. user_info = getattr(request, 'user_info', {})
  747. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  748. if not user_id:
  749. return jsonify({
  750. 'success': False,
  751. 'error': '用户信息获取失败'
  752. }), HTTPStatus.UNAUTHORIZED
  753. # 验证对话是否属于当前用户
  754. conversation = Conversation.query.filter_by(
  755. conversation_id=conversation_id,
  756. user_id=user_id,
  757. is_deleted=False
  758. ).first()
  759. if not conversation:
  760. return jsonify({
  761. 'success': False,
  762. 'error': '对话不存在或无权限访问'
  763. }), HTTPStatus.NOT_FOUND
  764. # 获取对话的所有消息,按顺序排列
  765. messages = Message.query.filter_by(
  766. conversation_id=conversation_id,
  767. is_deleted=False
  768. ).order_by(Message.message_order.asc()).all()
  769. messages_data = []
  770. for msg in messages:
  771. message_data = {
  772. 'message_id': msg.message_id,
  773. 'role': msg.role,
  774. 'content': msg.content,
  775. 'thinking_content': msg.thinking_content,
  776. 'content_type': msg.content_type,
  777. 'message_order': msg.message_order,
  778. 'created_at': msg.created_at.isoformat()
  779. }
  780. messages_data.append(message_data)
  781. return jsonify({
  782. 'success': True,
  783. 'conversation': {
  784. 'conversation_id': conversation.conversation_id,
  785. 'title': conversation.title,
  786. 'conversation_type': conversation.conversation_type,
  787. 'session_id': conversation.session_id,
  788. 'message_count': conversation.message_count,
  789. 'created_at': conversation.created_at.isoformat(),
  790. 'updated_at': conversation.updated_at.isoformat()
  791. },
  792. 'messages': messages_data
  793. })
  794. except Exception as e:
  795. logger.error(f"获取对话消息时发生错误: {str(e)}", exc_info=True)
  796. return jsonify({
  797. 'success': False,
  798. 'error': f"发生错误: {str(e)}"
  799. }), HTTPStatus.INTERNAL_SERVER_ERROR
  800. @app.route('/api/conversations/<int:conversation_id>/rename', methods=['POST'])
  801. @auth_middleware.login_required
  802. def rename_conversation(conversation_id):
  803. """重命名对话"""
  804. try:
  805. # 从认证中间件获取用户信息
  806. user_info = getattr(request, 'user_info', {})
  807. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  808. if not user_id:
  809. return jsonify({
  810. 'success': False,
  811. 'error': '用户信息获取失败'
  812. }), HTTPStatus.UNAUTHORIZED
  813. data = request.json or {}
  814. new_title = data.get('title', '').strip()
  815. if not new_title:
  816. return jsonify({
  817. 'success': False,
  818. 'error': '新标题不能为空'
  819. }), HTTPStatus.BAD_REQUEST
  820. if len(new_title) > 500:
  821. return jsonify({
  822. 'success': False,
  823. 'error': '标题长度不能超过500个字符'
  824. }), HTTPStatus.BAD_REQUEST
  825. # 验证对话是否属于当前用户
  826. conversation = Conversation.query.filter_by(
  827. conversation_id=conversation_id,
  828. user_id=user_id,
  829. is_deleted=False
  830. ).first()
  831. if not conversation:
  832. return jsonify({
  833. 'success': False,
  834. 'error': '对话不存在或无权限访问'
  835. }), HTTPStatus.NOT_FOUND
  836. # 更新标题
  837. conversation.title = new_title
  838. conversation.updated_at = get_beijing_time()
  839. db.session.commit()
  840. return jsonify({
  841. 'success': True,
  842. 'message': '对话标题已更新',
  843. 'conversation': {
  844. 'conversation_id': conversation.conversation_id,
  845. 'title': conversation.title,
  846. 'updated_at': conversation.updated_at.isoformat()
  847. }
  848. })
  849. except Exception as e:
  850. db.session.rollback()
  851. logger.error(f"重命名对话时发生错误: {str(e)}", exc_info=True)
  852. return jsonify({
  853. 'success': False,
  854. 'error': f"发生错误: {str(e)}"
  855. }), HTTPStatus.INTERNAL_SERVER_ERROR
  856. @app.route('/api/conversations/<int:conversation_id>', methods=['DELETE'])
  857. @auth_middleware.login_required
  858. def delete_conversation(conversation_id):
  859. """删除对话(软删除)"""
  860. try:
  861. # 从认证中间件获取用户信息
  862. user_info = getattr(request, 'user_info', {})
  863. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  864. if not user_id:
  865. return jsonify({
  866. 'success': False,
  867. 'error': '用户信息获取失败'
  868. }), HTTPStatus.UNAUTHORIZED
  869. # 验证对话是否属于当前用户
  870. conversation = Conversation.query.filter_by(
  871. conversation_id=conversation_id,
  872. user_id=user_id,
  873. is_deleted=False
  874. ).first()
  875. if not conversation:
  876. return jsonify({
  877. 'success': False,
  878. 'error': '对话不存在或无权限访问'
  879. }), HTTPStatus.NOT_FOUND
  880. # 软删除对话
  881. conversation.is_deleted = True
  882. conversation.deleted_at = get_beijing_time()
  883. conversation.updated_at = get_beijing_time()
  884. # 同时软删除该对话的所有消息
  885. Message.query.filter_by(
  886. conversation_id=conversation_id,
  887. is_deleted=False
  888. ).update({
  889. 'is_deleted': True,
  890. 'deleted_at': get_beijing_time()
  891. })
  892. db.session.commit()
  893. return jsonify({
  894. 'success': True,
  895. 'message': '对话已删除'
  896. })
  897. except Exception as e:
  898. db.session.rollback()
  899. logger.error(f"删除对话时发生错误: {str(e)}", exc_info=True)
  900. return jsonify({
  901. 'success': False,
  902. 'error': f"发生错误: {str(e)}"
  903. }), HTTPStatus.INTERNAL_SERVER_ERROR
  904. @app.route('/api/conversations/new', methods=['POST'])
  905. @auth_middleware.login_required
  906. def create_new_conversation():
  907. """创建新对话"""
  908. try:
  909. # 从认证中间件获取用户信息
  910. user_info = getattr(request, 'user_info', {})
  911. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  912. if not user_id:
  913. return jsonify({
  914. 'success': False,
  915. 'error': '用户信息获取失败'
  916. }), HTTPStatus.UNAUTHORIZED
  917. # 获取用户对象
  918. user = User.query.filter_by(user_id=user_id, is_deleted=False).first()
  919. if not user:
  920. return jsonify({
  921. 'success': False,
  922. 'error': '用户不存在'
  923. }), HTTPStatus.NOT_FOUND
  924. data = request.json or {}
  925. title = data.get('title', '新对话').strip()
  926. if not title:
  927. title = '新对话'
  928. if len(title) > 500:
  929. return jsonify({
  930. 'success': False,
  931. 'error': '标题长度不能超过500个字符'
  932. }), HTTPStatus.BAD_REQUEST
  933. # 生成新的session_id
  934. session_id = generate_session_id(user_id)
  935. # 创建新对话
  936. conversation = Conversation(
  937. user_id=user_id,
  938. title=title,
  939. conversation_type='chat',
  940. session_id=session_id,
  941. message_count=0
  942. )
  943. db.session.add(conversation)
  944. db.session.commit()
  945. # 更新用户的当前对话ID
  946. user_current_conversations[str(user_id)] = conversation.conversation_id
  947. return jsonify({
  948. 'success': True,
  949. 'message': '新对话已创建',
  950. 'conversation': {
  951. 'conversation_id': conversation.conversation_id,
  952. 'title': conversation.title,
  953. 'created_at': conversation.created_at.isoformat(),
  954. 'updated_at': conversation.updated_at.isoformat()
  955. }
  956. })
  957. except Exception as e:
  958. db.session.rollback()
  959. logger.error(f"创建新对话时发生错误: {str(e)}", exc_info=True)
  960. return jsonify({
  961. 'success': False,
  962. 'error': f"发生错误: {str(e)}"
  963. }), HTTPStatus.INTERNAL_SERVER_ERROR
  964. @app.route('/api/conversations/clear-current', methods=['POST'])
  965. @auth_middleware.login_required
  966. def clear_current_conversation():
  967. """清除用户的当前对话映射,强制下次创建新对话"""
  968. try:
  969. # 从认证中间件获取用户信息
  970. user_info = getattr(request, 'user_info', {})
  971. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  972. if not user_id:
  973. return jsonify({
  974. 'success': False,
  975. 'error': '用户信息获取失败'
  976. }), HTTPStatus.UNAUTHORIZED
  977. # 清除用户的当前对话映射
  978. if str(user_id) in user_current_conversations:
  979. del user_current_conversations[str(user_id)]
  980. return jsonify({
  981. 'success': True,
  982. 'message': '当前对话映射已清除'
  983. })
  984. except Exception as e:
  985. logger.error(f"清除当前对话映射时发生错误: {str(e)}", exc_info=True)
  986. return jsonify({
  987. 'success': False,
  988. 'error': f"发生错误: {str(e)}"
  989. }), HTTPStatus.INTERNAL_SERVER_ERROR
  990. @app.route('/api/conversations/<int:conversation_id>/switch', methods=['POST'])
  991. @auth_middleware.login_required
  992. def switch_conversation(conversation_id):
  993. """切换到指定对话"""
  994. try:
  995. # 从认证中间件获取用户信息
  996. user_info = getattr(request, 'user_info', {})
  997. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  998. if not user_id:
  999. return jsonify({
  1000. 'success': False,
  1001. 'error': '用户信息获取失败'
  1002. }), HTTPStatus.UNAUTHORIZED
  1003. # 切换到指定对话
  1004. conversation = switch_to_conversation(user_id, conversation_id)
  1005. return jsonify({
  1006. 'success': True,
  1007. 'message': '对话切换成功',
  1008. 'conversation': {
  1009. 'conversation_id': conversation.conversation_id,
  1010. 'title': conversation.title,
  1011. 'session_id': conversation.session_id,
  1012. 'message_count': conversation.message_count,
  1013. 'last_message_at': conversation.last_message_at.isoformat() if conversation.last_message_at else None,
  1014. 'created_at': conversation.created_at.isoformat(),
  1015. 'updated_at': conversation.updated_at.isoformat()
  1016. }
  1017. })
  1018. except ValueError as e:
  1019. return jsonify({
  1020. 'success': False,
  1021. 'error': str(e)
  1022. }), HTTPStatus.NOT_FOUND
  1023. except Exception as e:
  1024. logger.error(f"切换对话时发生错误: {str(e)}", exc_info=True)
  1025. return jsonify({
  1026. 'success': False,
  1027. 'error': f"发生错误: {str(e)}"
  1028. }), HTTPStatus.INTERNAL_SERVER_ERROR
  1029. @app.route('/api/conversations/<int:conversation_id>/status', methods=['GET'])
  1030. @auth_middleware.login_required
  1031. def get_conversation_status(conversation_id):
  1032. """获取对话的实时状态信息"""
  1033. try:
  1034. # 从认证中间件获取用户信息
  1035. user_info = getattr(request, 'user_info', {})
  1036. user_id = user_info.get('db_user_id') # 使用数据库用户ID
  1037. if not user_id:
  1038. return jsonify({
  1039. 'success': False,
  1040. 'error': '用户信息获取失败'
  1041. }), HTTPStatus.UNAUTHORIZED
  1042. # 验证对话是否属于当前用户
  1043. conversation = Conversation.query.filter_by(
  1044. conversation_id=conversation_id,
  1045. user_id=user_id,
  1046. is_deleted=False
  1047. ).first()
  1048. if not conversation:
  1049. return jsonify({
  1050. 'success': False,
  1051. 'error': '对话不存在或无权限访问'
  1052. }), HTTPStatus.NOT_FOUND
  1053. # 获取对话的最新消息
  1054. latest_message = Message.query.filter_by(
  1055. conversation_id=conversation_id,
  1056. is_deleted=False
  1057. ).order_by(Message.message_order.desc()).first()
  1058. status_info = {
  1059. 'conversation_id': conversation_id,
  1060. 'title': conversation.title,
  1061. 'message_count': conversation.message_count,
  1062. 'last_message_at': conversation.last_message_at.isoformat() if conversation.last_message_at else None,
  1063. 'updated_at': conversation.updated_at.isoformat(),
  1064. 'has_latest_message': latest_message is not None,
  1065. 'latest_message_role': latest_message.role if latest_message else None,
  1066. 'latest_message_preview': latest_message.content[:100] + '...' if latest_message and len(latest_message.content) > 100 else latest_message.content if latest_message else None
  1067. }
  1068. return jsonify({
  1069. 'success': True,
  1070. 'status': status_info
  1071. })
  1072. except Exception as e:
  1073. logger.error(f"获取对话状态时发生错误: {str(e)}", exc_info=True)
  1074. return jsonify({
  1075. 'success': False,
  1076. 'error': f"发生错误: {str(e)}"
  1077. }), HTTPStatus.INTERNAL_SERVER_ERROR
  1078. @app.route('/api/messages/<int:message_id>/edit', methods=['POST'])
  1079. @auth_middleware.login_required
  1080. @rate_limit(config.rate_limit_requests, config.rate_limit_window)
  1081. def edit_message(message_id):
  1082. """编辑用户消息并重新生成AI回复(流式响应)"""
  1083. try:
  1084. # 从请求中获取数据
  1085. data = request.json or {}
  1086. new_content = data.get('content', '')
  1087. agent = data.get('agent') or 'kuaishou'
  1088. # 输入验证
  1089. new_content, error = sanitize_input(new_content, config.max_message_length)
  1090. if error:
  1091. # 流式响应中的错误返回
  1092. return Response(
  1093. f"data: {json.dumps({'success': False, 'error': error, 'done': True})}\n\n",
  1094. content_type='text/event-stream'
  1095. )
  1096. # 从认证中间件获取用户信息
  1097. auth_user_info = getattr(request, 'user_info', {})
  1098. auth_user_id = auth_user_info.get('db_user_id') # 使用数据库用户ID
  1099. if not auth_user_id:
  1100. return Response(
  1101. f"data: {json.dumps({'success': False, 'error': '用户信息获取失败', 'done': True})}\n\n",
  1102. content_type='text/event-stream'
  1103. )
  1104. # 获取要编辑的消息
  1105. message = Message.query.filter_by(
  1106. message_id=message_id,
  1107. is_deleted=False
  1108. ).first()
  1109. if not message:
  1110. return Response(
  1111. f"data: {json.dumps({'success': False, 'error': '消息不存在', 'done': True})}\n\n",
  1112. content_type='text/event-stream'
  1113. )
  1114. # 验证消息是否是用户消息
  1115. if message.role != 'user':
  1116. return Response(
  1117. f"data: {json.dumps({'success': False, 'error': '只能编辑用户消息', 'done': True})}\n\n",
  1118. content_type='text/event-stream'
  1119. )
  1120. # 获取对话信息
  1121. conversation = Conversation.query.filter_by(
  1122. conversation_id=message.conversation_id,
  1123. user_id=auth_user_id,
  1124. is_deleted=False
  1125. ).first()
  1126. if not conversation:
  1127. return Response(
  1128. f"data: {json.dumps({'success': False, 'error': '对话不存在或无权限访问', 'done': True})}\n\n",
  1129. content_type='text/event-stream'
  1130. )
  1131. # 记录请求信息
  1132. log_request_info(new_content, str(auth_user_id), 'POST')
  1133. # 选择对应智能体 APP_ID
  1134. chosen_app_id, appid_env_key = config.get_app_id_for_agent_with_source(agent)
  1135. if not chosen_app_id:
  1136. logger.error(f"未找到对应智能体APP_ID,agent={agent}")
  1137. return Response(
  1138. f"data: {json.dumps({'success': False, 'error': f'服务器未配置该智能体的 APP_ID: {agent}', 'done': True})}\n\n",
  1139. content_type='text/event-stream'
  1140. )
  1141. # 使用对话的session_id
  1142. api_session_id = conversation.session_id
  1143. # ===== 立即软删除当前消息及之后的所有消息(在流式响应开始前) =====
  1144. try:
  1145. # 计算将被删除的消息数(包括当前消息及之后的所有消息)
  1146. deleted_count = Message.query.filter_by(
  1147. conversation_id=conversation.conversation_id,
  1148. is_deleted=False
  1149. ).filter(
  1150. Message.message_order >= message.message_order
  1151. ).count()
  1152. # 软删除当前消息及之后的所有消息(包括原来的AI回复等)
  1153. Message.query.filter_by(
  1154. conversation_id=conversation.conversation_id,
  1155. is_deleted=False
  1156. ).filter(
  1157. Message.message_order >= message.message_order
  1158. ).update({
  1159. 'is_deleted': True,
  1160. 'deleted_at': get_beijing_time()
  1161. }, synchronize_session=False)
  1162. # 更新消息计数:减去被删除的消息数
  1163. conversation.message_count -= deleted_count
  1164. db.session.commit()
  1165. # 保存编辑后的用户消息(使用相同的message_order)
  1166. new_user_message = save_message_to_database(
  1167. auth_user_id,
  1168. conversation.conversation_id,
  1169. 'user',
  1170. new_content,
  1171. None,
  1172. message_order=message.message_order
  1173. )
  1174. except Exception as db_error:
  1175. db.session.rollback()
  1176. logger.error(f"软删除消息或保存编辑消息时发生错误: {str(db_error)}", exc_info=True)
  1177. return Response(
  1178. f"data: {json.dumps({'success': False, 'error': f'更新消息失败: {str(db_error)}', 'done': True})}\n\n",
  1179. content_type='text/event-stream'
  1180. )
  1181. # 获取该消息之前的所有消息(不包括已删除的消息,现在数据库中已删除的消息不会出现在查询结果中)
  1182. previous_messages = Message.query.filter_by(
  1183. conversation_id=message.conversation_id,
  1184. is_deleted=False
  1185. ).filter(
  1186. Message.message_order < message.message_order
  1187. ).order_by(Message.message_order.asc()).all()
  1188. # 构建对话历史(使用编辑后的消息)
  1189. messages = []
  1190. for msg in previous_messages:
  1191. messages.append({"role": msg.role, "content": msg.content})
  1192. # 添加编辑后的用户消息(刚保存的)
  1193. messages.append({"role": "user", "content": new_content})
  1194. # 截断对话历史以控制内存使用
  1195. messages = truncate_conversation_history(messages, config.max_conversation_history)
  1196. # 定义流式响应生成器
  1197. def generate() -> Generator[str, None, None]:
  1198. try:
  1199. # 发送初始响应
  1200. initial_data = {
  1201. 'success': True,
  1202. 'chunk': '',
  1203. 'done': False,
  1204. 'agent': agent,
  1205. 'message_id': message_id
  1206. }
  1207. yield f"data: {json.dumps(initial_data)}\n\n"
  1208. try:
  1209. # 使用DashScope SDK直接调用 - 多轮对话模式
  1210. params = {
  1211. "api_key": config.api_key,
  1212. "app_id": chosen_app_id,
  1213. "messages": messages, # 发送完整的对话历史
  1214. "stream": True,
  1215. "incremental_output": True,
  1216. "has_thoughts": True
  1217. }
  1218. # 使用对话的session_id
  1219. if api_session_id:
  1220. params["session_id"] = api_session_id
  1221. response = Application.call(**params)
  1222. new_session_id: Optional[str] = None
  1223. accumulated_text: str = ""
  1224. thoughts_content: List[str] = []
  1225. # 处理流式响应
  1226. for chunk in response:
  1227. # 检查客户端是否断开连接
  1228. if request.environ.get('werkzeug.server.shutdown') or not request:
  1229. break
  1230. if chunk.status_code != HTTPStatus.OK:
  1231. yield f"data: {json.dumps({'success': False, 'error': f'错误: {chunk.status_code} - {chunk.message}', 'request_id': chunk.request_id, 'done': True, 'agent': agent})}\n\n"
  1232. return
  1233. # 处理思考过程
  1234. if hasattr(chunk.output, 'thoughts') and chunk.output.thoughts:
  1235. for it in chunk.output.thoughts:
  1236. if it.action_type == 'reasoning':
  1237. content = str(it.thought) if not isinstance(it.thought, str) else it.thought
  1238. thoughts_content.append(content)
  1239. yield f"data: {json.dumps({'success': True, 'thought': content, 'done': False, 'agent': agent})}\n\n"
  1240. # 处理回答内容
  1241. if hasattr(chunk.output, 'text') and chunk.output.text:
  1242. current_text = str(chunk.output.text)
  1243. accumulated_text += current_text
  1244. yield f"data: {json.dumps({'success': True, 'chunk': current_text, 'done': False, 'agent': agent})}\n\n"
  1245. # 检查是否有会话ID
  1246. if hasattr(chunk.output, 'session_id') and chunk.output.session_id:
  1247. new_session_id = chunk.output.session_id
  1248. # 处理结束,保存AI回复到数据库
  1249. try:
  1250. # 更新session_id(如果需要)
  1251. if new_session_id and new_session_id != api_session_id:
  1252. conversation.session_id = new_session_id
  1253. db.session.commit()
  1254. # 保存新的AI回复(如果有)
  1255. if accumulated_text:
  1256. thinking_text = '\n'.join(thoughts_content) if thoughts_content else None
  1257. ai_message = save_message_to_database(
  1258. auth_user_id,
  1259. conversation.conversation_id,
  1260. 'assistant',
  1261. accumulated_text,
  1262. thinking_text,
  1263. message_order=message.message_order + 1
  1264. )
  1265. # save_message_to_database 已经提交,message_count 已更新
  1266. else:
  1267. # 如果没有AI回复,更新对话的更新时间
  1268. conversation.updated_at = get_beijing_time()
  1269. conversation.last_message_at = get_beijing_time()
  1270. db.session.commit()
  1271. except Exception as db_error:
  1272. db.session.rollback()
  1273. logger.error(f"保存AI回复到数据库时发生错误: {str(db_error)}", exc_info=True)
  1274. # 不影响主要功能,继续返回结果
  1275. # 发送完成标记
  1276. final_data = {
  1277. 'success': True,
  1278. 'done': True,
  1279. 'thoughts': thoughts_content,
  1280. 'agent': agent,
  1281. 'session_id': new_session_id or api_session_id,
  1282. 'message_id': message_id,
  1283. 'conversation_id': conversation.conversation_id
  1284. }
  1285. yield f"data: {json.dumps(final_data)}\n\n"
  1286. except Exception as e:
  1287. logger.error(f"DashScope SDK 调用错误: {str(e)}", exc_info=True)
  1288. yield f"data: {json.dumps({'success': False, 'error': f'DashScope API调用错误: {str(e)}', 'done': True, 'agent': agent})}\n\n"
  1289. except Exception as e:
  1290. logger.error(f"流式API调用错误: {str(e)}", exc_info=True)
  1291. yield f"data: {json.dumps({'success': False, 'error': f'发生错误: {str(e)}', 'done': True, 'agent': agent})}\n\n"
  1292. # 创建并返回流式响应
  1293. response = Response(stream_with_context(generate()), content_type='text/event-stream')
  1294. response.headers['Cache-Control'] = 'no-cache'
  1295. response.headers['X-Accel-Buffering'] = 'no'
  1296. return response
  1297. except Exception as e:
  1298. logger.error(f"编辑消息时发生错误: {str(e)}", exc_info=True)
  1299. return jsonify({
  1300. 'success': False,
  1301. 'error': f"发生错误: {str(e)}"
  1302. }), HTTPStatus.INTERNAL_SERVER_ERROR
  1303. # 健康检查端点
  1304. @app.route('/health', methods=['GET'])
  1305. def health_check():
  1306. """健康检查端点"""
  1307. return jsonify({
  1308. 'status': 'ok',
  1309. 'app_version': '2.0.0',
  1310. 'python_version': '3.12',
  1311. 'timestamp': time.time()
  1312. })
  1313. if __name__ == '__main__':
  1314. app.run(host='0.0.0.0', port=config.port, debug=config.debug)