UserControl的Unique ID

本文探讨了ASP.NET中动态UserControl的绑定问题,尤其是在postback情况下是否需要重新绑定数据。结论指出,在添加控件前后调用其方法均可,但避免使用IsPostBack条件判断。若必须使用,则应在子控件生成UniqueID后进行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://www.richbox.net/libray/html/riadoc/dotnet/ASP.NET/150952749.htm

以及其中的

http://www.cnblogs.com/king_astar/archive/2004/09/30/48126.html

提到的Unique ID和postback后到底需不需要再绑定一次动态UserControl的数据的问题。

总结------

所以,
在this.someCtl.Controls.Add(ctl); 之前调用该control的方法或者在之后调用控件方法都可以,
不过不要加If(!ispostback)的判断,
如果一定要加postback的判断,则需要在子控件的UniqueID生成之后,再调用控件的方法(来绑定数据等)。

 

D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:60: error: no matching function for call to 'service::DatabaseManager::executeNonQuery(QString&)' D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp: In function 'void data::UserControl::Login::createUserTable()': D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:60:35: error: no matching function for call to 'service::DatabaseManager::executeNonQuery(QString&)' 60 | db.executeNonQuery(createTableQuery); | ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~ D:\QT_PROGRAM\LabNexus\pch.h:27: In file included from D:/QT_PROGRAM/LabNexus/pch.h:27, In file included from D:/QT_PROGRAM/LabNexus/pch.h:27, from D:/QT_PROGRAM/LabNexus/build/Desktop_Qt_6_8_2_MinGW_64_bit-Debug/CMakeFiles/LabNexus.dir/cmake_pch.hxx:5, from <command-line>: D:/QT_PROGRAM/LabNexus/service/database/databaseManager.h:41:14: note: candidate: 'bool service::DatabaseManager::executeNonQuery(const QString&, const QMap<QString, QVariant>&)' 41 | bool executeNonQuery(const QString &queryString,const QMap<QString, QVariant> &params); | ^~~~~~~~~~~~~~~ D:/QT_PROGRAM/LabNexus/service/database/databaseManager.h:41:14: note: candidate expects 2 arguments, 1 provided D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:218: error: no matching function for call to 'service::DatabaseManager::executeNonQuery(QString&)' D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp: In function 'void data::UserControl::permission::createGroupTable()': D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:218:35: error: no matching function for call to 'service::DatabaseManager::executeNonQuery(QString&)' 218 | db.executeNonQuery(createTableQuery); | ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~ D:/QT_PROGRAM/LabNexus/service/database/databaseManager.h:41:14: note: candidate: 'bool service::DatabaseManager::executeNonQuery(const QString&, const QMap<QString, QVariant>&)' 41 | bool executeNonQuery(const QString &queryString,const QMap<QString, QVariant> &params); | ^~~~~~~~~~~~~~~ D:/QT_PROGRAM/LabNexus/service/database/databaseManager.h:41:14: note: candidate expects 2 arguments, 1 provided D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:235: error: no matching function for call to 'service::DatabaseManager::executeNonQuery(QString&)' D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp: In function 'void data::UserControl::permission::createUserGroupTable()': D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:235:35: error: no matching function for call to 'service::DatabaseManager::executeNonQuery(QString&)' 235 | db.executeNonQuery(createUserGroupTableQuery); | ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~ D:/QT_PROGRAM/LabNexus/service/database/databaseManager.h:41:14: note: candidate: 'bool service::DatabaseManager::executeNonQuery(const QString&, const QMap<QString, QVariant>&)' 41 | bool executeNonQuery(const QString &queryString,const QMap<QString, QVariant> &params); | ^~~~~~~~~~~~~~~ D:/QT_PROGRAM/LabNexus/service/database/databaseManager.h:41:14: note: candidate expects 2 arguments, 1 provided mingw32-make.exe[2]:-1: *** [CMakeFiles\LabNexus.dir\build.make:429: CMakeFiles/LabNexus.dir/module/data/data_UserControl.cpp.obj] Error 1 mingw32-make.exe[1]:-1: *** [CMakeFiles\Makefile2:114: CMakeFiles/LabNexus.dir/all] Error 2 mingw32-make.exe:-1: *** [Makefile:100: all] Error 2 D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:60: error: Too few arguments to function call, expected 2, have 1 D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:218: error: Too few arguments to function call, expected 2, have 1 D:\QT_PROGRAM\LabNexus\module\data\data_UserControl.cpp:235: error: Too few arguments to function call, expected 2, have 1 关于该.cpp文件报错原因? // // Created by gouzuang on 25-7-6. // #include<pch.h> #include "module/data/data_UserControl.h" namespace data::UserControl { void dropDB() { QFile dbFile(path); if (dbFile.exists()) { if (dbFile.remove()) { log(LogLevel::INFO) << "数据库文件删除成功"; } else { log(LogLevel::ERR) << "数据库文件删除失败"; } } else { log(LogLevel::INFO) << "数据库文件不存在"; } } void buildDB() { QFile dbFile(path); if (!dbFile.exists()) { if (dbFile.open(QIODevice::WriteOnly)) { dbFile.close(); log(service::LogLevel::INFO) << "数据库文件创建成功"; } else { log(service::LogLevel::ERR) << "数据库文件创建失败"; } Login::createUserTable(); permission::createGroupTable(); permission::createUserGroupTable(); // 创建用户组 if (auto r = permission::createGroup("Student", ""); !r) { log(LogLevel::ERR) << "创建组 Student 失败, 错误码:" << static_cast<int>(r.error()); } if (auto r = permission::createGroup("Teacher", ""); !r) { log(LogLevel::ERR) << "创建组 Teacher 失败, 错误码:" << static_cast<int>(r.error()); } } else { log(service::LogLevel::INFO) << "数据库文件已存在"; } } namespace Login { void createUserTable() { service::DatabaseManager db(path); if (!db.tableExists("users")) { QString createTableQuery = R"( CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, id_number TEXT UNIQUE NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, status Text NOT NULL DEFAULT 'AllRight' ) )"; db.executeNonQuery(createTableQuery); } } std::expected<int, UserControlError> isUserPasswordValid(const QString &idNumber, const QString &password) { service::DatabaseManager db(path); log(service::LogLevel::INFO) << "开始验证用户密码: " << idNumber; QString query = R"( SELECT id, password, status FROM users WHERE id_number = ? )"; auto results = db.executePreparedQueryAndFetchAll(query, {idNumber}); if (results.isEmpty()) { log(service::LogLevel::ERR) << "登录失败。用户不存在: " << idNumber; return std::unexpected(UserControlError::UserNotFound); } const auto& row = results.first(); int userId = row["id"].toInt(); QString storedPassword = row["password"].toString(); QString status = row["status"].toString(); if (status != "AllRight") { log(service::LogLevel::ERR) << "登录失败。用户状态异常: " << idNumber << " status: " << status; return std::unexpected(UserControlError::UserNotFound); } if (storedPassword != password) { log(service::LogLevel::ERR) << "登录失败。密码错误: " << idNumber; return std::unexpected(UserControlError::IncorrectPassword); } log(service::LogLevel::INFO) << "登录成功。密码验证成功: " << idNumber; currentUserId = userId; return userId; } std::expected<int, UserControlError> createNewUser(const QString &idNumber, const QString &username, const QString &password) { service::DatabaseManager db(path); if (!db.tableExists("users")) { log(service::LogLevel::ERR) << "用户表不存在"; throw std::runtime_error("User table does not exist."); } auto userId = foundUserIdByIdNumber(idNumber); if (userId.has_value()) { log(service::LogLevel::INFO) << "用户已存在: " << idNumber; return std::unexpected(UserControlError::UserAlreadyExists); } QString insertQuery = R"( INSERT INTO users(id_number, username, password, status) VALUES(?, ?, ?, 'AllRight') )"; if (!db.executePreparedNonQuery(insertQuery, {idNumber, username, password})) { log(service::LogLevel::ERR) << "新用户创建失败: " << idNumber; throw std::runtime_error("Failed to create new user."); } log(service::LogLevel::DATA) << "新用户创建成功: " << idNumber; auto newUserId = foundUserIdByIdNumber(idNumber); if (!newUserId.has_value()) { throw std::runtime_error("Failed to retrieve new user ID after creation."); } return newUserId.value(); } std::expected<int, UserControlError> createNewUser(const QString &idNumber, const QString &username, const QString &password, const QString group) { auto newUserResult = createNewUser(idNumber, username, password); if (!newUserResult) { return std::unexpected(newUserResult.error()); } if (group.isEmpty()) { log(service::LogLevel::DATA) << "未指定组,用户创建成功但未添加到任何组"; return newUserResult.value(); } log(service::LogLevel::DATA) << "尝试将用户添加到组: " << group; auto addToGroupResult = permission::addUserToGroup(newUserResult.value(), group); if (!addToGroupResult) { log(service::LogLevel::ERR) << "用户 " << newUserResult.value() << " 添加到组 " << group << " 失败"; return std::unexpected(addToGroupResult.error()); } return newUserResult.value(); } std::expected<int, UserControlError> foundUserIdByIdNumber(const QString &idNumber) { service::DatabaseManager db(path); QString query = "SELECT id FROM users WHERE id_number = ?"; auto results = db.executePreparedQueryAndFetchAll(query, {idNumber}); if (results.isEmpty()) { return std::unexpected(UserControlError::UserNotFound); } return results.first()["id"].toInt(); } std::expected<bool, UserControlError> deleteUserById(int userId) { service::DatabaseManager db(path); // 检查用户是否存在 QString checkQuery = R"( SELECT id FROM users WHERE id = ? )"; auto results = db.executePreparedQueryAndFetchAll(checkQuery, {userId}); if (results.isEmpty()) { log(service::LogLevel::INFO) << "删除失败。用户不存在: " << userId; return std::unexpected(UserControlError::UserNotFound); } // 标记用户在 user_groups 中的关联为 Deleted QString updateGroupsStatus = R"( UPDATE user_groups SET status = 'Deleted' WHERE user_id = ? )"; db.executePreparedNonQuery(updateGroupsStatus, {userId}); // 标记用户为 Deleted QString updateUserStatus = R"( UPDATE users SET status = 'Deleted' WHERE id = ? )"; if (db.executePreparedNonQuery(updateUserStatus, {userId})) { log(service::LogLevel::DATA) << "用户标记为已删除: " << userId; return true; } log(service::LogLevel::ERR) << "标记用户删除失败: " << userId; return std::unexpected(UserControlError::DatabaseError); } std::expected<bool, UserControlError> updateUserPassword(int userId, const QString &newPassword) { service::DatabaseManager db(path); QString updateQuery = R"( UPDATE users SET password = ? WHERE id = ? )"; if (!db.executePreparedNonQuery(updateQuery, {newPassword, userId})) { return std::unexpected(UserControlError::DatabaseError); } log(service::LogLevel::DATA) << "用户" << userId << "密码更新成功: " << userId; return true; } } namespace permission { void createGroupTable() { service::DatabaseManager db(path); if (!db.tableExists("groups")) { QString createTableQuery = R"( CREATE TABLE groups( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT NOT NULL DEFAULT 'AllRight' ) )"; db.executeNonQuery(createTableQuery); } } void createUserGroupTable() { service::DatabaseManager db(path); if (!db.tableExists("user_groups")) { QString createUserGroupTableQuery = R"( CREATE TABLE user_groups( user_id INTEGER NOT NULL, group_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'AllRight', FOREIGN KEY(user_id) REFERENCES users(id), FOREIGN KEY(group_id) REFERENCES groups(id), PRIMARY KEY(user_id, group_id) ) )"; db.executeNonQuery(createUserGroupTableQuery); } } std::expected<bool, UserControlError> createGroup(const QString &name, const QString &description) { service::DatabaseManager db(path); if (!db.tableExists("groups")) { throw std::runtime_error("Group table does not exist."); } // 检查组是否已存在 QString checkGroupQuery = R"( SELECT id FROM groups WHERE name = ? )"; auto results = db.executePreparedQueryAndFetchAll(checkGroupQuery, {name}); if (!results.isEmpty()) { log(service::LogLevel::INFO) << "组已存在: " << name; return std::unexpected(UserControlError::GroupAlreadyExists); } QString insertQuery = R"( INSERT INTO groups(name, description, status) VALUES(?, ?, 'AllRight') )"; if (db.executePreparedNonQuery(insertQuery, {name, description})) { log(service::LogLevel::DATA) << "新组创建成功: " << name; return true; } log(service::LogLevel::ERR) << "新组创建失败: " << name; return std::unexpected(UserControlError::DatabaseError); } std::expected<bool, UserControlError> addUserToGroup(int userId, const QString &groupName) { service::DatabaseManager db(path); // 检查组是否存在 QString checkGroupQuery = R"( SELECT id FROM groups WHERE name = ? )"; auto groupResults = db.executePreparedQueryAndFetchAll(checkGroupQuery, {groupName}); if (groupResults.isEmpty()) { log(service::LogLevel::INFO) << "组不存在: " << groupName; return std::unexpected(UserControlError::GroupNotFound); } int groupId = groupResults.first()["id"].toInt(); // 检查用户是否已经在该组中 QString checkUserInGroupQuery = R"( SELECT user_id FROM user_groups WHERE user_id = ? AND group_id = ? )"; auto userInGroupResults = db.executePreparedQueryAndFetchAll(checkUserInGroupQuery, {userId, groupId}); if (!userInGroupResults.isEmpty()) { log(service::LogLevel::INFO) << "用户 " << userId << " 已经在组 " << groupName << " 中"; return std::unexpected(UserControlError::UserAlreadyInGroup); } // 将用户添加到组 QString insertUserGroupQuery = R"( INSERT INTO user_groups(user_id, group_id, status) VALUES(?, ?, 'AllRight') )"; if (db.executePreparedNonQuery(insertUserGroupQuery, {userId, groupId})) { log(service::LogLevel::DATA) << "用户 " << userId << " 添加到组 " << groupName << " 成功"; return true; } log(service::LogLevel::ERR) << "用户 " << userId << " 添加到组 " << groupName << " 失败"; return std::unexpected(UserControlError::DatabaseError); } QString getUserInWhichGroup(int userId) { service::DatabaseManager db(path); QString query = R"( SELECT g.name FROM groups g JOIN user_groups ug ON g.id = ug.group_id WHERE ug.user_id = ? )"; auto results = db.executePreparedQueryAndFetchAll(query, {userId}); QStringList groupNames; for (const auto &row : results) { groupNames.append(row["name"].toString()); } return groupNames.join(", "); } bool isUserInGroup(int userId, const QString &groupName) { service::DatabaseManager db(path); QString query = R"( SELECT COUNT(*) FROM user_groups ug JOIN groups g ON ug.group_id = g.id WHERE ug.user_id = ? AND g.name = ? )"; auto results = db.executePreparedQueryAndFetchAll(query, {userId, groupName}); if (results.isEmpty()) { return false; } return results.first()["COUNT(*)"].toInt() > 0; } } namespace UserInfo { std::expected<QString,UserInfoError> getUserNameById(int userId) { service::DatabaseManager db(path); QString query = "SELECT username FROM users WHERE id = ?"; auto results = db.executePreparedQueryAndFetchAll(query, {userId}); if (results.isEmpty()) { return std::unexpected(UserInfoError::UserNotFound); } return results.first()["username"].toString(); } } }
07-11
我正在编写一个闲鱼客服,以上是XianyuAgent.py代码,以下是两个子代码文件:context_manager.py:import sqlite3 import os import json from datetime import datetime from loguru import logger class ChatContextManager: """ 聊天上下文管理器 负责存储和检索用户与商品之间的对话历史,使用SQLite数据库进行持久化存储。 支持按会话ID检索对话历史,以及议价次数统计。 """ def __init__(self, max_history=100, db_path="data/chat_history.db"): """ 初始化聊天上下文管理器 Args: max_history: 每个对话保留的最大消息数 db_path: SQLite数据库文件路径 """ self.max_history = max_history self.db_path = db_path self._init_db() def _init_db(self): """初始化数据库表结构""" # 确保数据库目录存在 db_dir = os.path.dirname(self.db_path) if db_dir and not os.path.exists(db_dir): os.makedirs(db_dir) conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # 创建消息表 cursor.execute(''' CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, item_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, chat_id TEXT ) ''') # 检查是否需要添加chat_id字段(兼容旧数据库) cursor.execute("PRAGMA table_info(messages)") columns = [column[1] for column in cursor.fetchall()] if 'chat_id' not in columns: cursor.execute('ALTER TABLE messages ADD COLUMN chat_id TEXT') logger.info("已为messages表添加chat_id字段") # 创建索引以加速查询 cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_user_item ON messages (user_id, item_id) ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_chat_id ON messages (chat_id) ''') cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp) ''') # 创建基于会话ID的议价次数表 cursor.execute(''' CREATE TABLE IF NOT EXISTS chat_bargain_counts ( chat_id TEXT PRIMARY KEY, count INTEGER DEFAULT 0, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') # 创建商品信息表 cursor.execute(''' CREATE TABLE IF NOT EXISTS items ( item_id TEXT PRIMARY KEY, data TEXT NOT NULL, price REAL, description TEXT, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') # 创建人工标准回答表 cursor.execute(''' CREATE TABLE IF NOT EXISTS manual_answers ( id INTEGER PRIMARY KEY AUTOINCREMENT, question TEXT NOT NULL, answer TEXT NOT NULL, item_id TEXT NOT NULL, chat_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(question, item_id) ) ''') # 创建索引加速查询 cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_manual_answers_question ON manual_answers (question) ''') # 创建索引加速查询 cursor.execute(''' CREATE INDEX IF NOT EXISTS idx_manual_answers_item_id ON manual_answers (item_id) ''') conn.commit() conn.close() logger.info(f"聊天历史数据库初始化完成: {self.db_path}") def save_item_info(self, item_id, item_data): """ 保存商品信息到数据库 Args: item_id: 商品ID item_data: 商品信息字典 """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: # 从商品数据中提取有用信息 price = float(item_data.get('soldPrice', 0)) description = item_data.get('desc', '') # 将整个商品数据转换为JSON字符串 data_json = json.dumps(item_data, ensure_ascii=False) cursor.execute( """ INSERT INTO items (item_id, data, price, description, last_updated) VALUES (?, ?, ?, ?, ?) ON CONFLICT(item_id) DO UPDATE SET data = ?, price = ?, description = ?, last_updated = ? """, ( item_id, data_json, price, description, datetime.now().isoformat(), data_json, price, description, datetime.now().isoformat() ) ) conn.commit() logger.debug(f"商品信息已保存: {item_id}") except Exception as e: logger.error(f"保存商品信息时出错: {e}") conn.rollback() finally: conn.close() def get_item_info(self, item_id): """ 从数据库获取商品信息 Args: item_id: 商品ID Returns: dict: 商品信息字典,如果不存在返回None """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: cursor.execute( "SELECT data FROM items WHERE item_id = ?", (item_id,) ) result = cursor.fetchone() if result: return json.loads(result[0]) return None except Exception as e: logger.error(f"获取商品信息时出错: {e}") return None finally: conn.close() def add_message_by_chat(self, chat_id, user_id, item_id, role, content): """ 基于会话ID添加新消息到对话历史 Args: chat_id: 会话ID user_id: 用户ID (用户消息存真实user_id,助手消息存卖家ID) item_id: 商品ID role: 消息角色 (user/assistant) content: 消息内容 """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: # 插入新消息,使用chat_id作为额外标识 cursor.execute( "INSERT INTO messages (user_id, item_id, role, content, timestamp, chat_id) VALUES (?, ?, ?, ?, ?, ?)", (user_id, item_id, role, content, datetime.now().isoformat(), chat_id) ) # 检查是否需要清理旧消息(基于chat_id) cursor.execute( """ SELECT id FROM messages WHERE chat_id = ? ORDER BY timestamp DESC LIMIT ?, 1 """, (chat_id, self.max_history) ) oldest_to_keep = cursor.fetchone() if oldest_to_keep: cursor.execute( "DELETE FROM messages WHERE chat_id = ? AND id < ?", (chat_id, oldest_to_keep[0]) ) conn.commit() except Exception as e: logger.error(f"添加消息到数据库时出错: {e}") conn.rollback() finally: conn.close() def get_context_by_chat(self, chat_id): """ 基于会话ID获取对话历史 Args: chat_id: 会话ID Returns: list: 包含对话历史的列表 """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: cursor.execute( """ SELECT role, content FROM messages WHERE chat_id = ? ORDER BY timestamp ASC LIMIT ? """, (chat_id, self.max_history) ) messages = [{"role": role, "content": content} for role, content in cursor.fetchall()] # 获取议价次数并添加到上下文中 bargain_count = self.get_bargain_count_by_chat(chat_id) if bargain_count > 0: messages.append({ "role": "system", "content": f"议价次数: {bargain_count}" }) except Exception as e: logger.error(f"获取对话历史时出错: {e}") messages = [] finally: conn.close() return messages def increment_bargain_count_by_chat(self, chat_id): """ 基于会话ID增加议价次数 Args: chat_id: 会话ID """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: # 使用UPSERT语法直接基于chat_id增加议价次数 cursor.execute( """ INSERT INTO chat_bargain_counts (chat_id, count, last_updated) VALUES (?, 1, ?) ON CONFLICT(chat_id) DO UPDATE SET count = count + 1, last_updated = ? """, (chat_id, datetime.now().isoformat(), datetime.now().isoformat()) ) conn.commit() logger.debug(f"会话 {chat_id} 议价次数已增加") except Exception as e: logger.error(f"增加议价次数时出错: {e}") conn.rollback() finally: conn.close() def get_bargain_count_by_chat(self, chat_id): """ 基于会话ID获取议价次数 Args: chat_id: 会话ID Returns: int: 议价次数 """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: cursor.execute( "SELECT count FROM chat_bargain_counts WHERE chat_id = ?", (chat_id,) ) result = cursor.fetchone() return result[0] if result else 0 except Exception as e: logger.error(f"获取议价次数时出错: {e}") return 0 finally: conn.close() def update_manual_answer(self, question, new_answer, item_id, chat_id=None): """ 更新已保存的人工标准回答 Args: question: 原始问题 new_answer: 新的回答内容 item_id: 商品ID chat_id: 会话ID(可选) Returns: bool: 更新是否成功 """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: cursor.execute( "UPDATE manual_answers SET answer = ?, created_at = ? WHERE question = ? AND item_id = ?", (new_answer, datetime.now().isoformat(), question, item_id) ) conn.commit() if cursor.rowcount > 0: logger.info(f"人工标准回答已更新: {question[:30]}...") return True else: logger.warning(f"未找到匹配的人工标准回答: {question[:30]}...") return False except Exception as e: logger.error(f"更新人工标准回答时出错: {e}") conn.rollback() return False finally: conn.close() def save_manual_answer(self, question, answer, item_id, chat_id=None): """ 保存人工标准回答到数据库 Args: question: 用户问题 answer: 人工回答 item_id: 商品ID chat_id: 会话ID(可选) """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: cursor.execute( "INSERT OR IGNORE INTO manual_answers (question, answer, item_id, chat_id, created_at) VALUES (?, ?, ?, ?, ?)", (question, answer, item_id, chat_id, datetime.now().isoformat()) ) conn.commit() logger.info(f"人工标准回答已保存: {question[:30]}...") except Exception as e: logger.error(f"保存人工标准回答时出错: {e}") conn.rollback() finally: conn.close() def get_manual_answer(self, question, item_id): """ 从数据库获取人工标准回答 Args: question: 用户问题 item_id: 商品ID Returns: str: 匹配的人工回答,如果没有找到则返回None """ conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: # 使用LIKE进行模糊匹配 cursor.execute( "SELECT answer FROM manual_answers WHERE item_id = ? AND question LIKE ? ORDER BY created_at DESC LIMIT 1", (item_id, f"%{question}%") ) result = cursor.fetchone() return result[0] if result else None except Exception as e: logger.error(f"获取人工标准回答时出错: {e}") return None finally: conn.close() main.py: import base64 import json import asyncio import time import os import websockets from loguru import logger from dotenv import load_dotenv from XianyuApis import XianyuApis import sys from utils.xianyu_utils import generate_mid, generate_uuid, trans_cookies, generate_device_id, decrypt from XianyuAgent import XianyuReplyBot from context_manager import ChatContextManager class XianyuLive: def __init__(self, cookies_str): self.xianyu = XianyuApis() self.base_url = 'wss://wss-goofish.dingtalk.com/' self.cookies_str = cookies_str self.cookies = trans_cookies(cookies_str) self.xianyu.session.cookies.update(self.cookies) # 直接使用 session.cookies.update self.myid = self.cookies['unb'] self.device_id = generate_device_id(self.myid) self.context_manager = ChatContextManager() # 心跳相关配置 self.heartbeat_interval = int(os.getenv("HEARTBEAT_INTERVAL", "15")) # 心跳间隔,默认15秒 self.heartbeat_timeout = int(os.getenv("HEARTBEAT_TIMEOUT", "5")) # 心跳超时,默认5秒 self.last_heartbeat_time = 0 self.last_heartbeat_response = 0 self.heartbeat_task = None self.ws = None # Token刷新相关配置 self.token_refresh_interval = int(os.getenv("TOKEN_REFRESH_INTERVAL", "3600")) # Token刷新间隔,默认1小时 self.token_retry_interval = int(os.getenv("TOKEN_RETRY_INTERVAL", "300")) # Token重试间隔,默认5分钟 self.last_token_refresh_time = 0 self.current_token = None self.token_refresh_task = None self.connection_restart_flag = False # 连接重启标志 # 人工接管相关配置 self.manual_mode_conversations = set() # 存储处于人工接管模式的会话ID self.manual_mode_timeout = int(os.getenv("MANUAL_MODE_TIMEOUT", "3600")) # 人工接管超时时间,默认1小时 self.manual_mode_timestamps = {} # 记录进入人工模式的时间 # 消息过期时间配置 self.message_expire_time = int(os.getenv("MESSAGE_EXPIRE_TIME", "300000")) # 消息过期时间,默认5分钟 # 人工接管关键词,从环境变量读取 self.toggle_keywords = os.getenv("TOGGLE_KEYWORDS", "。") async def refresh_token(self): """刷新token""" try: logger.info("开始刷新token...") # 获取新token(如果Cookie失效,get_token会直接退出程序) token_result = self.xianyu.get_token(self.device_id) if 'data' in token_result and 'accessToken' in token_result['data']: new_token = token_result['data']['accessToken'] self.current_token = new_token self.last_token_refresh_time = time.time() logger.info("Token刷新成功") return new_token else: logger.error(f"Token刷新失败: {token_result}") return None except Exception as e: logger.error(f"Token刷新异常: {str(e)}") return None async def token_refresh_loop(self): """Token刷新循环""" while True: try: current_time = time.time() # 检查是否需要刷新token if current_time - self.last_token_refresh_time >= self.token_refresh_interval: logger.info("Token即将过期,准备刷新...") new_token = await self.refresh_token() if new_token: logger.info("Token刷新成功,准备重新建立连接...") # 设置连接重启标志 self.connection_restart_flag = True # 关闭当前WebSocket连接,触发重连 if self.ws: await self.ws.close() break else: logger.error("Token刷新失败,将在{}分钟后重试".format(self.token_retry_interval // 60)) await asyncio.sleep(self.token_retry_interval) # 使用配置的重试间隔 continue # 每分钟检查一次 await asyncio.sleep(60) except Exception as e: logger.error(f"Token刷新循环出错: {e}") await asyncio.sleep(60) async def send_msg(self, ws, cid, toid, text): text = { "contentType": 4, "html": { "html": text } } text_base64 = str(base64.b64encode(json.dumps(text).encode('utf-8')), 'utf-8') msg = { "lwp": "/r/MessageSend/sendByReceiverScope", "headers": { "mid": generate_mid() }, "body": [ { "uuid": generate_uuid(), "cid": f"{cid}@goofish", "conversationType": 1, "content": { "contentType": 101, "custom": { "type": 1, "data": text_base64 } }, "redPointPolicy": 0, "extension": { "extJson": "{}" }, "ctx": { "appVersion": "1.0", "platform": "web" }, "mtags": {}, "msgReadStatusSetting": 1 }, { "actualReceivers": [ f"{toid}@goofish", f"{self.myid}@goofish" ] } ] } await ws.send(json.dumps(msg)) async def init(self, ws): # 如果没有token或者token过期,获取新token if not self.current_token or (time.time() - self.last_token_refresh_time) >= self.token_refresh_interval: logger.info("获取初始token...") await self.refresh_token() if not self.current_token: logger.error("无法获取有效token,初始化失败") raise Exception("Token获取失败") msg = { "lwp": "/reg", "headers": { "cache-header": "app-key token ua wv", "app-key": "444e9908a51d1cb236a27862abc769c9", "token": self.current_token, "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 DingTalk(2.1.5) OS(Windows/10) Browser(Chrome/133.0.0.0) DingWeb/2.1.5 IMPaaS DingWeb/2.1.5", "dt": "j", "wv": "im:3,au:3,sy:6", "sync": "0,0;0;0;", "did": self.device_id, "mid": generate_mid() } } await ws.send(json.dumps(msg)) # 等待一段时间,确保连接注册完成 await asyncio.sleep(1) msg = {"lwp": "/r/SyncStatus/ackDiff", "headers": {"mid": "5701741704675979 0"}, "body": [ {"pipeline": "sync", "tooLong2Tag": "PNM,1", "channel": "sync", "topic": "sync", "highPts": 0, "pts": int(time.time() * 1000) * 1000, "seq": 0, "timestamp": int(time.time() * 1000)}]} await ws.send(json.dumps(msg)) logger.info('连接注册完成') def is_chat_message(self, message): """判断是否为用户聊天消息""" try: return ( isinstance(message, dict) and "1" in message and isinstance(message["1"], dict) # 确保是字典类型 and "10" in message["1"] and isinstance(message["1"]["10"], dict) # 确保是字典类型 and "reminderContent" in message["1"]["10"] ) except Exception: return False def is_sync_package(self, message_data): """判断是否为同步包消息""" try: return ( isinstance(message_data, dict) and "body" in message_data and "syncPushPackage" in message_data["body"] and "data" in message_data["body"]["syncPushPackage"] and len(message_data["body"]["syncPushPackage"]["data"]) > 0 ) except Exception: return False def is_typing_status(self, message): """判断是否为用户正在输入状态消息""" try: return ( isinstance(message, dict) and "1" in message and isinstance(message["1"], list) and len(message["1"]) > 0 and isinstance(message["1"][0], dict) and "1" in message["1"][0] and isinstance(message["1"][0]["1"], str) and "@goofish" in message["1"][0]["1"] ) except Exception: return False def is_system_message(self, message): """判断是否为系统消息""" try: return ( isinstance(message, dict) and "3" in message and isinstance(message["3"], dict) and "needPush" in message["3"] and message["3"]["needPush"] == "false" ) except Exception: return False def check_toggle_keywords(self, message): """检查消息是否包含切换关键词""" message_stripped = message.strip() return message_stripped in self.toggle_keywords def is_manual_mode(self, chat_id): """检查特定会话是否处于人工接管模式""" if chat_id not in self.manual_mode_conversations: return False # 检查是否超时 current_time = time.time() if chat_id in self.manual_mode_timestamps: if current_time - self.manual_mode_timestamps[chat_id] > self.manual_mode_timeout: # 超时,自动退出人工模式 self.exit_manual_mode(chat_id) return False return True def enter_manual_mode(self, chat_id): """进入人工接管模式""" self.manual_mode_conversations.add(chat_id) self.manual_mode_timestamps[chat_id] = time.time() def exit_manual_mode(self, chat_id): """退出人工接管模式""" self.manual_mode_conversations.discard(chat_id) if chat_id in self.manual_mode_timestamps: del self.manual_mode_timestamps[chat_id] def toggle_manual_mode(self, chat_id): """切换人工接管模式""" if self.is_manual_mode(chat_id): self.exit_manual_mode(chat_id) return "auto" else: self.enter_manual_mode(chat_id) return "manual" async def handle_message(self, message_data, websocket): """处理所有类型的消息""" try: try: message = message_data ack = { "code": 200, "headers": { "mid": message["headers"]["mid"] if "mid" in message["headers"] else generate_mid(), "sid": message["headers"]["sid"] if "sid" in message["headers"] else '', } } if 'app-key' in message["headers"]: ack["headers"]["app-key"] = message["headers"]["app-key"] if 'ua' in message["headers"]: ack["headers"]["ua"] = message["headers"]["ua"] if 'dt' in message["headers"]: ack["headers"]["dt"] = message["headers"]["dt"] await websocket.send(json.dumps(ack)) except Exception as e: pass # 如果不是同步包消息,直接返回 if not self.is_sync_package(message_data): return # 获取并解密数据 sync_data = message_data["body"]["syncPushPackage"]["data"][0] # 检查是否有必要的字段 if "data" not in sync_data: logger.debug("同步包中无data字段") return # 解密数据 try: data = sync_data["data"] try: data = base64.b64decode(data).decode("utf-8") data = json.loads(data) # logger.info(f"无需解密 message: {data}") return except Exception as e: # logger.info(f'加密数据: {data}') decrypted_data = decrypt(data) message = json.loads(decrypted_data) except Exception as e: logger.error(f"消息解密失败: {e}") return try: # 判断是否为订单消息,需要自行编写付款后的逻辑 if message['3']['redReminder'] == '等待买家付款': user_id = message['1'].split('@')[0] user_url = f'https://www.goofish.com/personal?userId={user_id}' logger.info(f'等待买家 {user_url} 付款') return elif message['3']['redReminder'] == '交易关闭': user_id = message['1'].split('@')[0] user_url = f'https://www.goofish.com/personal?userId={user_id}' logger.info(f'买家 {user_url} 交易关闭') return elif message['3']['redReminder'] == '等待卖家发货': user_id = message['1'].split('@')[0] user_url = f'https://www.goofish.com/personal?userId={user_id}' logger.info(f'交易成功 {user_url} 等待卖家发货') return except: pass # 判断消息类型 if self.is_typing_status(message): logger.debug("用户正在输入") return elif not self.is_chat_message(message): logger.debug("其他非聊天消息") logger.debug(f"原始消息: {message}") return # 处理聊天消息 create_time = int(message["1"]["5"]) send_user_name = message["1"]["10"]["reminderTitle"] send_user_id = message["1"]["10"]["senderUserId"] send_message = message["1"]["10"]["reminderContent"] # 时效性验证(过滤5分钟前消息) if (time.time() * 1000 - create_time) > self.message_expire_time: logger.debug("过期消息丢弃") return # 获取商品ID和会话ID url_info = message["1"]["10"]["reminderUrl"] item_id = url_info.split("itemId=")[1].split("&")[0] if "itemId=" in url_info else None chat_id = message["1"]["2"].split('@')[0] if not item_id: logger.warning("无法获取商品ID") return # 检查是否为卖家(自己)发送的控制命令 if send_user_id == self.myid: logger.debug("检测到卖家消息,检查是否为控制命令") # 检查切换命令 if self.check_toggle_keywords(send_message): mode = self.toggle_manual_mode(chat_id) if mode == "manual": logger.info(f"🔴 已接管会话 {chat_id} (商品: {item_id})") else: logger.info(f"🟢 已恢复会话 {chat_id} 的自动回复 (商品: {item_id})") return # 记录卖家人工回复 self.context_manager.add_message_by_chat(chat_id, self.myid, item_id, "assistant", send_message) logger.info(f"卖家人工回复 (会话: {chat_id}, 商品: {item_id}): {send_message}") # 获取用户问题并保存人工标准回答 context = self.context_manager.get_context_by_chat(chat_id) user_question = None for msg in reversed(context): if msg['role'] == 'user': user_question = msg['content'] break if user_question: self.context_manager.save_manual_answer(user_question, send_message, item_id) logger.info(f"已保存人工标准回答 (商品: {item_id}): 问题: {user_question}, 回答: {send_message}") return logger.info(f"用户: {send_user_name} (ID: {send_user_id}), 商品: {item_id}, 会话: {chat_id}, 消息: {send_message}") # 添加用户消息到上下文 self.context_manager.add_message_by_chat(chat_id, send_user_id, item_id, "user", send_message) # 如果当前会话处于人工接管模式,不进行自动回复 if self.is_manual_mode(chat_id): logger.info(f"🔴 会话 {chat_id} 处于人工接管模式,跳过自动回复") return if self.is_system_message(message): logger.debug("系统消息,跳过处理") return # 从数据库中获取商品信息,如果不存在则从API获取并保存 item_info = self.context_manager.get_item_info(item_id) if not item_info: logger.info(f"从API获取商品信息: {item_id}") api_result = self.xianyu.get_item_info(item_id) if 'data' in api_result and 'itemDO' in api_result['data']: item_info = api_result['data']['itemDO'] # 保存商品信息到数据库 self.context_manager.save_item_info(item_id, item_info) else: logger.warning(f"获取商品信息失败: {api_result}") return else: logger.info(f"从数据库获取商品信息: {item_id}") item_description = f"{item_info['desc']};当前商品售卖价格为:{str(item_info['soldPrice'])}" # 获取完整的对话上下文 context = self.context_manager.get_context_by_chat(chat_id) # 生成回复 bot_reply = bot.generate_reply( send_message, item_description, context=context ) # 检查是否为价格意图,如果是则增加议价次数 if bot.last_intent == "price": self.context_manager.increment_bargain_count_by_chat(chat_id) bargain_count = self.context_manager.get_bargain_count_by_chat(chat_id) logger.info(f"用户 {send_user_name} 对商品 {item_id} 的议价次数: {bargain_count}") # 添加机器人回复到上下文 self.context_manager.add_message_by_chat(chat_id, self.myid, item_id, "assistant", bot_reply) logger.info(f"机器人回复: {bot_reply}") await self.send_msg(websocket, chat_id, send_user_id, bot_reply) except Exception as e: logger.error(f"处理消息时发生错误: {str(e)}") logger.debug(f"原始消息: {message_data}") async def send_heartbeat(self, ws): """发送心跳包并等待响应""" try: heartbeat_mid = generate_mid() heartbeat_msg = { "lwp": "/!", "headers": { "mid": heartbeat_mid } } await ws.send(json.dumps(heartbeat_msg)) self.last_heartbeat_time = time.time() logger.debug("心跳包已发送") return heartbeat_mid except Exception as e: logger.error(f"发送心跳包失败: {e}") raise async def heartbeat_loop(self, ws): """心跳维护循环""" while True: try: current_time = time.time() # 检查是否需要发送心跳 if current_time - self.last_heartbeat_time >= self.heartbeat_interval: await self.send_heartbeat(ws) # 检查上次心跳响应时间,如果超时则认为连接已断开 if (current_time - self.last_heartbeat_response) > (self.heartbeat_interval + self.heartbeat_timeout): logger.warning("心跳响应超时,可能连接已断开") break await asyncio.sleep(1) except Exception as e: logger.error(f"心跳循环出错: {e}") break async def handle_heartbeat_response(self, message_data): """处理心跳响应""" try: if ( isinstance(message_data, dict) and "headers" in message_data and "mid" in message_data["headers"] and "code" in message_data and message_data["code"] == 200 ): self.last_heartbeat_response = time.time() logger.debug("收到心跳响应") return True except Exception as e: logger.error(f"处理心跳响应出错: {e}") return False async def main(self): while True: try: # 重置连接重启标志 self.connection_restart_flag = False headers = { "Cookie": self.cookies_str, "Host": "wss-goofish.dingtalk.com", "Connection": "Upgrade", "Pragma": "no-cache", "Cache-Control": "no-cache", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", "Origin": "https://www.goofish.com", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "zh-CN,zh;q=0.9", } async with websockets.connect(self.base_url, extra_headers=headers) as websocket: self.ws = websocket await self.init(websocket) # 初始化心跳时间 self.last_heartbeat_time = time.time() self.last_heartbeat_response = time.time() # 启动心跳任务 self.heartbeat_task = asyncio.create_task(self.heartbeat_loop(websocket)) # 启动token刷新任务 self.token_refresh_task = asyncio.create_task(self.token_refresh_loop()) async for message in websocket: try: # 检查是否需要重启连接 if self.connection_restart_flag: logger.info("检测到连接重启标志,准备重新建立连接...") break message_data = json.loads(message) # 处理心跳响应 if await self.handle_heartbeat_response(message_data): continue # 发送通用ACK响应 if "headers" in message_data and "mid" in message_data["headers"]: ack = { "code": 200, "headers": { "mid": message_data["headers"]["mid"], "sid": message_data["headers"].get("sid", "") } } # 复制其他可能的header字段 for key in ["app-key", "ua", "dt"]: if key in message_data["headers"]: ack["headers"][key] = message_data["headers"][key] await websocket.send(json.dumps(ack)) # 处理其他消息 await self.handle_message(message_data, websocket) except json.JSONDecodeError: logger.error("消息解析失败") except Exception as e: logger.error(f"处理消息时发生错误: {str(e)}") logger.debug(f"原始消息: {message}") except websockets.exceptions.ConnectionClosed: logger.warning("WebSocket连接已关闭") except Exception as e: logger.error(f"连接发生错误: {e}") finally: # 清理任务 if self.heartbeat_task: self.heartbeat_task.cancel() try: await self.heartbeat_task except asyncio.CancelledError: pass if self.token_refresh_task: self.token_refresh_task.cancel() try: await self.token_refresh_task except asyncio.CancelledError: pass # 如果是主动重启,立即重连;否则等待5秒 if self.connection_restart_flag: logger.info("主动重启连接,立即重连...") else: logger.info("等待5秒后重连...") await asyncio.sleep(5) if __name__ == '__main__': # 加载环境变量 load_dotenv() # 配置日志级别 log_level = os.getenv("LOG_LEVEL", "DEBUG").upper() logger.remove() # 移除默认handler logger.add( sys.stderr, level=log_level, format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>" ) logger.info(f"日志级别设置为: {log_level}") cookies_str = os.getenv("COOKIES_STR") bot = XianyuReplyBot() xianyuLive = XianyuLive(cookies_str) # 常驻进程 asyncio.run(xianyuLive.main()) 现在在我加了techagent知识库检索(knowledge_base中product_summary.excel)的功能之后,客服机器人的消息发布出去了,只是可以把消息回复记录进日志
08-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值