From 85bbc1f47109ba7dac8cc8a7395f9f55af19e7b7 Mon Sep 17 00:00:00 2001 From: zstar <65890619+zstar1003@users.noreply.github.com> Date: Sat, 17 May 2025 11:57:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=9A=E8=AF=9D=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E4=BC=9A=E8=AF=9D=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + management/server/app.py | 3 +- management/server/routes/__init__.py | 3 + .../server/routes/conversation/__init__.py | 0 .../server/routes/conversation/routes.py | 63 ++ .../server/services/conversation/service.py | 267 +++++++ .../src/common/assets/icons/conversation.svg | 21 + .../web/src/pages/conversation/index.vue | 709 ++++++++++++++++++ management/web/src/router/index.ts | 18 + .../web/types/auto/svg-component-global.d.ts | 6 +- management/web/types/auto/svg-component.d.ts | 10 +- 11 files changed, 1091 insertions(+), 10 deletions(-) create mode 100644 management/server/routes/conversation/__init__.py create mode 100644 management/server/routes/conversation/routes.py create mode 100644 management/server/services/conversation/service.py create mode 100644 management/web/src/common/assets/icons/conversation.svg create mode 100644 management/web/src/pages/conversation/index.vue diff --git a/.gitignore b/.gitignore index 33ed442..6b4c150 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ management/models--opendatalab--PDF-Extract-Kit-1.0 management/models--hantian--layoutreader docker/models management/web/types/auto +node_modules/.cache/logger/umi.log diff --git a/management/server/app.py b/management/server/app.py index 029645e..d221d7d 100644 --- a/management/server/app.py +++ b/management/server/app.py @@ -1,7 +1,6 @@ -import database import jwt import os -from flask import Flask, jsonify, request +from flask import Flask, request from flask_cors import CORS from datetime import datetime, timedelta from routes import register_routes diff --git a/management/server/routes/__init__.py b/management/server/routes/__init__.py index 89825b6..d287592 100644 --- a/management/server/routes/__init__.py +++ b/management/server/routes/__init__.py @@ -7,6 +7,7 @@ teams_bp = Blueprint('teams', __name__, url_prefix='/api/v1/teams') tenants_bp = Blueprint('tenants', __name__, url_prefix='/api/v1/tenants') files_bp = Blueprint('files', __name__, url_prefix='/api/v1/files') knowledgebase_bp = Blueprint('knowledgebases', __name__, url_prefix='/api/v1/knowledgebases') +conversation_bp = Blueprint('conversation', __name__, url_prefix='/api/v1/conversation') # 导入路由 from .users.routes import * @@ -14,6 +15,7 @@ from .teams.routes import * from .tenants.routes import * from .files.routes import * from .knowledgebases.routes import * +from .conversation.routes import * def register_routes(app): @@ -23,3 +25,4 @@ def register_routes(app): app.register_blueprint(tenants_bp) app.register_blueprint(files_bp) app.register_blueprint(knowledgebase_bp) + app.register_blueprint(conversation_bp) \ No newline at end of file diff --git a/management/server/routes/conversation/__init__.py b/management/server/routes/conversation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/server/routes/conversation/routes.py b/management/server/routes/conversation/routes.py new file mode 100644 index 0000000..ac750f6 --- /dev/null +++ b/management/server/routes/conversation/routes.py @@ -0,0 +1,63 @@ +from flask import jsonify, request +from services.conversation.service import get_conversations_by_user_id, get_messages_by_conversation_id, get_conversation_detail +from .. import conversation_bp + + +@conversation_bp.route("", methods=["GET"]) +def get_conversations(): + """获取对话列表的API端点,支持分页和条件查询""" + try: + # 获取查询参数 + user_id = request.args.get("user_id") + page = int(request.args.get("page", 1)) + size = int(request.args.get("size", 20)) + sort_by = request.args.get("sort_by", "update_time") + sort_order = request.args.get("sort_order", "desc") + + # 参数验证 + if not user_id: + return jsonify({"code": 400, "message": "用户ID不能为空"}), 400 + + # 调用服务函数获取分页和筛选后的对话数据 + conversations, total = get_conversations_by_user_id(user_id, page, size, sort_by, sort_order) + + # 返回符合前端期望格式的数据 + return jsonify({"code": 0, "data": {"list": conversations, "total": total}, "message": "获取对话列表成功"}) + except Exception as e: + # 错误处理 + return jsonify({"code": 500, "message": f"获取对话列表失败: {str(e)}"}), 500 + + +@conversation_bp.route("//messages", methods=["GET"]) +def get_messages(conversation_id): + """获取特定对话的消息列表""" + try: + # 获取查询参数 + page = int(request.args.get("page", 1)) + size = int(request.args.get("size", 30)) + + # 调用服务函数获取消息数据 + messages, total = get_messages_by_conversation_id(conversation_id, page, size) + + # 返回符合前端期望格式的数据 + return jsonify({"code": 0, "data": {"list": messages, "total": total}, "message": "获取消息列表成功"}) + except Exception as e: + # 错误处理 + return jsonify({"code": 500, "message": f"获取消息列表失败: {str(e)}"}), 500 + + +@conversation_bp.route("/", methods=["GET"]) +def get_conversation(conversation_id): + """获取特定对话的详细信息""" + try: + # 调用服务函数获取对话详情 + conversation = get_conversation_detail(conversation_id) + + if not conversation: + return jsonify({"code": 404, "message": "对话不存在"}), 404 + + # 返回符合前端期望格式的数据 + return jsonify({"code": 0, "data": conversation, "message": "获取对话详情成功"}) + except Exception as e: + # 错误处理 + return jsonify({"code": 500, "message": f"获取对话详情失败: {str(e)}"}), 500 diff --git a/management/server/services/conversation/service.py b/management/server/services/conversation/service.py new file mode 100644 index 0000000..b1d886e --- /dev/null +++ b/management/server/services/conversation/service.py @@ -0,0 +1,267 @@ +import mysql.connector +from database import DB_CONFIG + + +def get_conversations_by_user_id(user_id, page=1, size=20, sort_by="update_time", sort_order="desc"): + """ + 根据用户ID获取对话列表 + + 参数: + user_id (str): 用户ID + page (int): 当前页码 + size (int): 每页大小 + sort_by (str): 排序字段 + sort_order (str): 排序方式 (asc/desc) + + 返回: + tuple: (对话列表, 总数) + """ + try: + conn = mysql.connector.connect(**DB_CONFIG) + cursor = conn.cursor(dictionary=True) + + # 直接使用user_id作为tenant_id + tenant_id = user_id + + print(f"查询用户ID: {user_id}, 租户ID: {tenant_id}") + + # 查询总记录数 + count_sql = """ + SELECT COUNT(*) as total + FROM dialog d + WHERE d.tenant_id = %s + """ + cursor.execute(count_sql, (tenant_id,)) + total = cursor.fetchone()["total"] + + print(f"查询到总记录数: {total}") + + # 计算分页偏移量 + offset = (page - 1) * size + + # 确定排序方向 + sort_direction = "DESC" if sort_order.lower() == "desc" else "ASC" + + # 执行分页查询 + query = f""" + SELECT + d.id, + d.name, + d.create_date, + d.update_date, + d.tenant_id + FROM + dialog d + WHERE + d.tenant_id = %s + ORDER BY + d.{sort_by} {sort_direction} + LIMIT %s OFFSET %s + """ + + print(f"执行查询: {query}") + print(f"参数: tenant_id={tenant_id}, size={size}, offset={offset}") + + cursor.execute(query, (tenant_id, size, offset)) + results = cursor.fetchall() + + print(f"查询结果数量: {len(results)}") + + # 获取每个对话的最新消息 + conversations = [] + for dialog in results: + # 查询对话的所有消息 + conv_query = """ + SELECT id, message, name + FROM conversation + WHERE dialog_id = %s + ORDER BY create_date DESC + """ + cursor.execute(conv_query, (dialog["id"],)) + conv_results = cursor.fetchall() + + latest_message = "" + conversation_name = dialog["name"] # 默认使用dialog的name + if conv_results and len(conv_results) > 0: + # 获取最新的一条对话记录 + latest_conv = conv_results[0] + # 如果conversation有name,优先使用conversation的name + if latest_conv and latest_conv.get("name"): + conversation_name = latest_conv["name"] + + if latest_conv and latest_conv["message"]: + # 获取最后一条消息内容 + messages = latest_conv["message"] + if messages and len(messages) > 0: + # 检查消息类型,处理字符串和字典两种情况 + if isinstance(messages[-1], dict): + latest_message = messages[-1].get("content", "") + elif isinstance(messages[-1], str): + latest_message = messages[-1] + else: + latest_message = str(messages[-1]) + + conversations.append( + { + "id": dialog["id"], + "name": conversation_name, + "latestMessage": latest_message[:100] + "..." if len(latest_message) > 100 else latest_message, + "createTime": dialog["create_date"].strftime("%Y-%m-%d %H:%M:%S") if dialog["create_date"] else "", + "updateTime": dialog["update_date"].strftime("%Y-%m-%d %H:%M:%S") if dialog["update_date"] else "", + } + ) + + # 关闭连接 + cursor.close() + conn.close() + + return conversations, total + + except mysql.connector.Error as err: + print(f"数据库错误: {err}") + # 更详细的错误日志 + import traceback + + traceback.print_exc() + return [], 0 + except Exception as e: + print(f"未知错误: {e}") + import traceback + + traceback.print_exc() + return [], 0 + + +def get_messages_by_conversation_id(conversation_id, page=1, size=30): + """ + 获取特定对话的详细信息 + + 参数: + conversation_id (str): 对话ID + page (int): 当前页码 + size (int): 每页大小 + + 返回: + tuple: (对话详情, 总数) + """ + try: + conn = mysql.connector.connect(**DB_CONFIG) + cursor = conn.cursor(dictionary=True) + + # 查询对话信息 + query = """ + SELECT * + FROM conversation + WHERE dialog_id = %s + ORDER BY create_date DESC + """ + cursor.execute(query, (conversation_id,)) + result = cursor.fetchall() # 确保读取所有结果 + + if not result: + print(f"未找到对话ID: {conversation_id}") + cursor.close() + conn.close() + return None, 0 + + # 获取第一条记录作为对话详情 + conversation = None + if len(result) > 0: + conversation = { + "id": result[0]["id"], + "dialogId": result[0].get("dialog_id", ""), + "createTime": result[0]["create_date"].strftime("%Y-%m-%d %H:%M:%S") if result[0].get("create_date") else "", + "updateTime": result[0]["update_date"].strftime("%Y-%m-%d %H:%M:%S") if result[0].get("update_date") else "", + "messages": result[0].get("message", []), + } + + # 打印调试信息 + print(f"获取到对话详情: ID={conversation_id}") + print(f"消息长度: {len(conversation['messages']) if conversation and conversation.get('messages') else 0}") + + # 关闭连接 + cursor.close() + conn.close() + + # 返回对话详情和消息总数 + total = len(conversation["messages"]) if conversation and conversation.get("messages") else 0 + return conversation, total + + except mysql.connector.Error as err: + print(f"数据库错误: {err}") + # 更详细的错误日志 + import traceback + + traceback.print_exc() + return None, 0 + except Exception as e: + print(f"未知错误: {e}") + import traceback + + traceback.print_exc() + return None, 0 + + +def get_conversation_detail(conversation_id): + """ + 获取特定对话的详细信息 + + 参数: + conversation_id (str): 对话ID + + 返回: + dict: 对话详情 + """ + try: + conn = mysql.connector.connect(**DB_CONFIG) + cursor = conn.cursor(dictionary=True) + + # 查询对话信息 + query = """ + SELECT c.*, d.name as dialog_name, d.icon as dialog_icon + FROM conversation c + LEFT JOIN dialog d ON c.dialog_id = d.id + WHERE c.id = %s + """ + cursor.execute(query, (conversation_id,)) + result = cursor.fetchone() + + if not result: + print(f"未找到对话ID: {conversation_id}") + return None + + # 格式化对话详情 + conversation = { + "id": result["id"], + "name": result.get("name", ""), + "dialogId": result.get("dialog_id", ""), + "dialogName": result.get("dialog_name", ""), + "dialogIcon": result.get("dialog_icon", ""), + "createTime": result["create_date"].strftime("%Y-%m-%d %H:%M:%S") if result.get("create_date") else "", + "updateTime": result["update_date"].strftime("%Y-%m-%d %H:%M:%S") if result.get("update_date") else "", + "messages": result.get("message", []), + } + + # 打印调试信息 + print(f"获取到对话详情: ID={conversation_id}") + print(f"消息数量: {len(conversation['messages']) if conversation['messages'] else 0}") + + # 关闭连接 + cursor.close() + conn.close() + + return conversation + + except mysql.connector.Error as err: + print(f"数据库错误: {err}") + # 更详细的错误日志 + import traceback + + traceback.print_exc() + return None + except Exception as e: + print(f"未知错误: {e}") + import traceback + + traceback.print_exc() + return None diff --git a/management/web/src/common/assets/icons/conversation.svg b/management/web/src/common/assets/icons/conversation.svg new file mode 100644 index 0000000..d46fc13 --- /dev/null +++ b/management/web/src/common/assets/icons/conversation.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/management/web/src/pages/conversation/index.vue b/management/web/src/pages/conversation/index.vue new file mode 100644 index 0000000..c4bf074 --- /dev/null +++ b/management/web/src/pages/conversation/index.vue @@ -0,0 +1,709 @@ + + + + + + + + diff --git a/management/web/src/router/index.ts b/management/web/src/router/index.ts index dd4207e..b0bb10e 100644 --- a/management/web/src/router/index.ts +++ b/management/web/src/router/index.ts @@ -134,6 +134,24 @@ export const constantRoutes: RouteRecordRaw[] = [ } } ] + }, + { + path: "/conversation", + component: Layouts, + redirect: "/conversation/index", + children: [ + { + path: "index", + component: () => import("@/pages/conversation/index.vue"), + name: "conversation", + meta: { + title: "用户会话管理", + svgIcon: "conversation", + affix: false, + keepAlive: true + } + } + ] } ] diff --git a/management/web/types/auto/svg-component-global.d.ts b/management/web/types/auto/svg-component-global.d.ts index cd5da82..d457a12 100644 --- a/management/web/types/auto/svg-component-global.d.ts +++ b/management/web/types/auto/svg-component-global.d.ts @@ -9,18 +9,18 @@ declare module 'vue' { export interface GlobalComponents { SvgIcon: import("vue").DefineComponent<{ name: { - type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; + type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; default: string; required: true; }; }, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly; + type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; default: string; required: true; }; }>>, { - name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; + name: "conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; }>; } } diff --git a/management/web/types/auto/svg-component.d.ts b/management/web/types/auto/svg-component.d.ts index 4dc999b..a3c9d59 100644 --- a/management/web/types/auto/svg-component.d.ts +++ b/management/web/types/auto/svg-component.d.ts @@ -7,20 +7,20 @@ declare module '~virtual/svg-component' { const SvgIcon: import("vue").DefineComponent<{ name: { - type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; + type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; default: string; required: true; }; }, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly; + type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">; default: string; required: true; }; }>>, { - name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; + name: "conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; }>; - export const svgNames: ["dashboard", "file", "fullscreen-exit", "fullscreen", "kb", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"]; - export type SvgName = "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; + export const svgNames: ["conversation", "dashboard", "file", "fullscreen-exit", "fullscreen", "kb", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"]; + export type SvgName = "conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management"; export default SvgIcon; }