From 5d8efd0dc4a3d5e60bc1cd0e26a64e4d83af08e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Fri, 30 Jan 2026 19:37:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=83=A8=E5=88=86=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/bot_manager.py | 311 ++++++++++++++++++++--------------------- utils/fastapi_utils.py | 106 +++++++------- 2 files changed, 201 insertions(+), 216 deletions(-) diff --git a/routes/bot_manager.py b/routes/bot_manager.py index 30bfcae..72e2247 100644 --- a/routes/bot_manager.py +++ b/routes/bot_manager.py @@ -2,6 +2,7 @@ Bot Manager API 路由 提供模型配置、Bot 管理、设置管理、MCP 服务器等功能的 API """ +import json import logging import uuid import hashlib @@ -160,10 +161,12 @@ class BotSettingsUpdate(BaseModel): model_id: Optional[str] = None language: Optional[str] = None robot_type: Optional[str] = None + description: Optional[str] = None + suggestions: Optional[List[str]] = None dataset_ids: Optional[str] = None system_prompt: Optional[str] = None - user_identifier: Optional[str] = None enable_memori: Optional[bool] = None + enable_thinking: Optional[bool] = None tool_response: Optional[bool] = None skills: Optional[str] = None @@ -185,10 +188,12 @@ class BotSettingsResponse(BaseModel): model: Optional[ModelInfo] # 关联的模型信息 language: str robot_type: Optional[str] + description: Optional[str] + suggestions: Optional[List[str]] dataset_ids: Optional[str] system_prompt: Optional[str] - user_identifier: Optional[str] enable_memori: bool + enable_thinking: bool tool_response: bool skills: Optional[str] updated_at: str @@ -247,12 +252,87 @@ class SuccessResponse(BaseModel): # ============== 数据库表初始化 ============== +async def migrate_bot_settings_to_jsonb(): + """ + 迁移 agent_bot_settings 表数据到 agent_bots.settings JSONB 字段 + 这是一个向后兼容的迁移函数 + """ + pool = get_db_pool_manager().pool + + async with pool.connection() as conn: + async with conn.cursor() as cursor: + # 1. 检查 agent_bots 表是否有 settings 列 + await cursor.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'agent_bots' AND column_name = 'settings' + """) + has_settings_column = await cursor.fetchone() + + if not has_settings_column: + logger.info("Migrating agent_bots table: adding settings column") + + # 添加 settings 列 + await cursor.execute(""" + ALTER TABLE agent_bots + ADD COLUMN settings JSONB DEFAULT '{ + "language": "zh", + "enable_memori": false, + "enable_thinking": false, + "tool_response": false + }'::jsonb + """) + + # 2. 检查旧的 agent_bot_settings 表是否存在 + await cursor.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'agent_bot_settings' + ) + """) + old_table_exists = (await cursor.fetchone())[0] + + if old_table_exists: + logger.info("Migrating data from agent_bot_settings to agent_bots.settings") + + # 3. 迁移旧数据到新字段 + await cursor.execute(""" + UPDATE agent_bots b + SET settings = jsonb_build_object( + 'model_id', s.model_id, + 'language', COALESCE(s.language, 'zh'), + 'robot_type', s.robot_type, + 'dataset_ids', s.dataset_ids, + 'system_prompt', s.system_prompt, + 'enable_memori', COALESCE(s.enable_memori, false), + 'enable_thinking', false, + 'tool_response', COALESCE(s.tool_response, false), + 'skills', s.skills + ) + FROM agent_bot_settings s + WHERE b.id = s.bot_id + """) + + logger.info("Data migration completed, dropping old table") + + # 4. 删除旧的 agent_bot_settings 表 + await cursor.execute("DROP TABLE IF EXISTS agent_bot_settings CASCADE") + + await conn.commit() + logger.info("Bot settings migration completed successfully") + else: + logger.info("Settings column already exists, skipping migration") + + async def init_bot_manager_tables(): """ 初始化 Bot Manager 相关的所有数据库表 """ pool = get_db_pool_manager().pool + # 首先执行迁移(如果需要) + await migrate_bot_settings_to_jsonb() + # SQL 表创建语句 tables_sql = [ # admin_tokens 表(用于存储登录 token) @@ -286,12 +366,18 @@ async def init_bot_manager_tables(): # models 索引 "CREATE INDEX IF NOT EXISTS idx_agent_models_is_default ON agent_models(is_default)", - # bots 表 + # bots 表(合并 settings 为 JSONB 字段) """ CREATE TABLE IF NOT EXISTS agent_bots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, bot_id VARCHAR(255) NOT NULL UNIQUE, + settings JSONB DEFAULT '{ + "language": "zh", + "enable_memori": false, + "enable_thinking": false, + "tool_response": false + }'::jsonb, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) @@ -299,23 +385,6 @@ async def init_bot_manager_tables(): # bots 索引 "CREATE INDEX IF NOT EXISTS idx_agent_bots_bot_id ON agent_bots(bot_id)", - # bot_settings 表 - """ - CREATE TABLE IF NOT EXISTS agent_bot_settings ( - bot_id UUID PRIMARY KEY REFERENCES agent_bots(id) ON DELETE CASCADE, - model_id UUID REFERENCES agent_models(id) ON DELETE SET NULL, - language VARCHAR(10) DEFAULT 'zh', - robot_type VARCHAR(50), - dataset_ids TEXT, - system_prompt TEXT, - user_identifier VARCHAR(255), - enable_memori BOOLEAN DEFAULT FALSE, - tool_response BOOLEAN DEFAULT FALSE, - skills TEXT, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() - ) - """, - # mcp_servers 表 """ CREATE TABLE IF NOT EXISTS agent_mcp_servers ( @@ -684,13 +753,6 @@ async def create_bot(request: BotCreate, authorization: Optional[str] = Header(N RETURNING id, created_at, updated_at """, (request.name, bot_id)) row = await cursor.fetchone() - - # 创建对应的设置记录 - await cursor.execute(""" - INSERT INTO agent_bot_settings (bot_id, language) - VALUES (%s, 'zh') - """, (str(row[0]),)) - await conn.commit() return BotResponse( @@ -818,20 +880,21 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header( async with pool.connection() as conn: async with conn.cursor() as cursor: await cursor.execute(""" - SELECT bot_id, model_id, - language, robot_type, dataset_ids, system_prompt, user_identifier, - enable_memori, tool_response, skills, updated_at - FROM agent_bot_settings - WHERE bot_id = %s + SELECT id, settings, updated_at + FROM agent_bots + WHERE id = %s """, (bot_uuid,)) row = await cursor.fetchone() if not row: - raise HTTPException(status_code=404, detail="Bot settings not found") + raise HTTPException(status_code=404, detail="Bot not found") + + bot_id, settings_json, updated_at = row + settings = settings_json if settings_json else {} # 获取关联的模型信息 model_info = None - model_id = row[1] + model_id = settings.get('model_id') if model_id: await cursor.execute(""" SELECT id, name, provider, model, server, api_key @@ -849,18 +912,20 @@ async def get_bot_settings(bot_uuid: str, authorization: Optional[str] = Header( ) return BotSettingsResponse( - bot_id=str(row[0]), - model_id=str(model_id) if model_id else None, + bot_id=str(bot_id), + model_id=model_id, model=model_info, - language=row[2] or 'zh', - robot_type=row[3], - dataset_ids=row[4], - system_prompt=row[5], - user_identifier=row[6], - enable_memori=row[7] or False, - tool_response=row[8] or False, - skills=row[9], - updated_at=datetime_to_str(row[10]) + language=settings.get('language', 'zh'), + robot_type=settings.get('robot_type'), + description=settings.get('description'), + suggestions=settings.get('suggestions'), + dataset_ids=settings.get('dataset_ids'), + system_prompt=settings.get('system_prompt'), + enable_memori=settings.get('enable_memori', False), + enable_thinking=settings.get('enable_thinking', False), + tool_response=settings.get('tool_response', False), + skills=settings.get('skills'), + updated_at=datetime_to_str(updated_at) ) @@ -885,142 +950,64 @@ async def update_bot_settings( pool = get_db_pool_manager().pool - # 构建更新字段 - update_fields = [] - values = [] - # 处理 model_id:将空字符串转换为 None model_id_value = request.model_id.strip() if request.model_id else None + # 验证 model_id 是否存在 + if model_id_value: + async with pool.connection() as conn: + async with conn.cursor() as cursor: + await cursor.execute("SELECT id FROM agent_models WHERE id = %s", (model_id_value,)) + if not await cursor.fetchone(): + raise HTTPException(status_code=400, detail=f"Model with id '{request.model_id}' not found") + + # 构建 JSONB 更新对象 + update_json = {} if request.model_id is not None: - # 验证 model_id 是否存在 - if model_id_value: - async with pool.connection() as conn: - async with conn.cursor() as cursor: - await cursor.execute("SELECT id FROM agent_models WHERE id = %s", (model_id_value,)) - if not await cursor.fetchone(): - raise HTTPException(status_code=400, detail=f"Model with id '{request.model_id}' not found") - # 使用 NULL 或占位符 - if model_id_value: - update_fields.append("model_id = %s") - values.append(model_id_value) - else: - update_fields.append("model_id = NULL") + update_json['model_id'] = model_id_value if model_id_value else None if request.language is not None: - update_fields.append("language = %s") - values.append(request.language) + update_json['language'] = request.language if request.robot_type is not None: - update_fields.append("robot_type = %s") - values.append(request.robot_type) + update_json['robot_type'] = request.robot_type + if request.description is not None: + update_json['description'] = request.description + if request.suggestions is not None: + update_json['suggestions'] = request.suggestions if request.dataset_ids is not None: - update_fields.append("dataset_ids = %s") - values.append(request.dataset_ids) + update_json['dataset_ids'] = request.dataset_ids if request.system_prompt is not None: - update_fields.append("system_prompt = %s") - values.append(request.system_prompt) - if request.user_identifier is not None: - update_fields.append("user_identifier = %s") - values.append(request.user_identifier) + update_json['system_prompt'] = request.system_prompt if request.enable_memori is not None: - update_fields.append("enable_memori = %s") - values.append(request.enable_memori) + update_json['enable_memori'] = request.enable_memori + if request.enable_thinking is not None: + update_json['enable_thinking'] = request.enable_thinking if request.tool_response is not None: - update_fields.append("tool_response = %s") - values.append(request.tool_response) + update_json['tool_response'] = request.tool_response if request.skills is not None: - update_fields.append("skills = %s") - values.append(request.skills) + update_json['skills'] = request.skills - if not update_fields: + if not update_json: raise HTTPException(status_code=400, detail="No fields to update") - update_fields.append("updated_at = NOW()") - values.append(bot_uuid) - async with pool.connection() as conn: async with conn.cursor() as cursor: - # 检查设置是否存在 - await cursor.execute("SELECT bot_id FROM agent_bot_settings WHERE bot_id = %s", (bot_uuid,)) - if not await cursor.fetchone(): - # 不存在则创建 - # 需要分别处理带占位符和不带占位符的字段 - insert_fields = [] - insert_placeholders = [] - insert_values = [] + # 检查 Bot 是否存在 + await cursor.execute("SELECT id, settings FROM agent_bots WHERE id = %s", (bot_uuid,)) + row = await cursor.fetchone() - for field in update_fields[:-1]: # 去掉 updated_at - if "= NULL" in field: - # 不带占位符的 NULL 值 - insert_fields.append(field.split(" = ")[0]) - insert_placeholders.append("NULL") - elif "= %s" in field: - # 带占位符的字段 - insert_fields.append(field.split(" = ")[0]) - insert_placeholders.append("%s") - # 从 values 中取值(需要追踪索引) - # 这里简化处理:重新解析 + if not row: + raise HTTPException(status_code=404, detail="Bot not found") - # 重新构建:遍历原始请求字段 - if model_id_value is not None: - insert_fields.append("model_id") - if model_id_value: - insert_placeholders.append("%s") - insert_values.append(model_id_value) - else: - insert_placeholders.append("NULL") - if request.language is not None: - insert_fields.append("language") - insert_placeholders.append("%s") - insert_values.append(request.language) - else: - insert_fields.append("language") - insert_placeholders.append("'zh'") - if request.robot_type is not None: - insert_fields.append("robot_type") - insert_placeholders.append("%s") - insert_values.append(request.robot_type) - if request.dataset_ids is not None: - insert_fields.append("dataset_ids") - insert_placeholders.append("%s") - insert_values.append(request.dataset_ids) - if request.system_prompt is not None: - insert_fields.append("system_prompt") - insert_placeholders.append("%s") - insert_values.append(request.system_prompt) - if request.user_identifier is not None: - insert_fields.append("user_identifier") - insert_placeholders.append("%s") - insert_values.append(request.user_identifier) - if request.enable_memori is not None: - insert_fields.append("enable_memori") - insert_placeholders.append("%s") - insert_values.append(request.enable_memori) - if request.tool_response is not None: - insert_fields.append("tool_response") - insert_placeholders.append("%s") - insert_values.append(request.tool_response) - if request.skills is not None: - insert_fields.append("skills") - insert_placeholders.append("%s") - insert_values.append(request.skills) + # 合并现有设置和新设置 + existing_settings = row[1] if row[1] else {} + existing_settings.update(update_json) - # 添加 bot_id - insert_fields.append("bot_id") - insert_placeholders.append("%s") - insert_values.append(bot_uuid) - - # 构建 SQL:混合使用占位符和 NULL - values_clause = ", ".join(insert_placeholders) - sql = f"INSERT INTO agent_bot_settings ({', '.join(insert_fields)}) VALUES ({values_clause})" - - await cursor.execute(sql, insert_values) - else: - # 存在则更新 - await cursor.execute(f""" - UPDATE agent_bot_settings - SET {', '.join(update_fields)} - WHERE bot_id = %s - """, values) + # 更新设置 + await cursor.execute(""" + UPDATE agent_bots + SET settings = %s, updated_at = NOW() + WHERE id = %s + """, (json.dumps(existing_settings), bot_uuid)) await conn.commit() diff --git a/utils/fastapi_utils.py b/utils/fastapi_utils.py index 125a8da..0656ea9 100644 --- a/utils/fastapi_utils.py +++ b/utils/fastapi_utils.py @@ -463,9 +463,12 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]: async with pool.connection() as conn: async with conn.cursor() as cursor: - # 首先根据 bot_user_id 查找 bot 的 UUID + # 从 agent_bots 表获取 bot 信息和 settings await cursor.execute( - "SELECT id, name FROM agent_bots WHERE bot_id = %s", + """ + SELECT id, name, settings + FROM agent_bots WHERE bot_id = %s + """, (bot_user_id,) ) bot_row = await cursor.fetchone() @@ -477,46 +480,44 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]: ) bot_uuid = bot_row[0] + bot_name = bot_row[1] + settings_json = bot_row[2] - # 查询 bot_settings - await cursor.execute( - """ - SELECT model_id, - language, robot_type, dataset_ids, system_prompt, user_identifier, - enable_memori, tool_response, skills - FROM agent_bot_settings WHERE bot_id = %s - """, - (bot_uuid,) - ) - settings_row = await cursor.fetchone() + # 解析 settings JSONB 字段 + if settings_json: + if isinstance(settings_json, str): + try: + settings_data = json.loads(settings_json) + except json.JSONDecodeError: + logger.warning(f"Failed to parse settings JSON for bot {bot_user_id}") + settings_data = {} + else: + settings_data = settings_json + else: + settings_data = {} - if not settings_row: - # 没有设置,使用默认值 - logger.warning(f"No settings found for bot {bot_user_id}, using defaults") - return { - "model": "qwen3-next", - "api_key": "", - "model_server": "", - "language": "zh", - "robot_type": "general_agent", - "dataset_ids": [], - "system_prompt": "", - "user_identifier": "", - "enable_memori": False, - "tool_response": True, - "skills": [] - } + # 获取 model_id + model_id = settings_data.get("model_id", "") - # 解析结果 - columns = [ - 'model_id', - 'language', 'robot_type', 'dataset_ids', 'system_prompt', 'user_identifier', - 'enable_memori', 'tool_response', 'skills' - ] - config = dict(zip(columns, settings_row)) + # 构建 config 字典,使用默认值填充缺失的字段 + config = { + "model": "qwen3-next", + "api_key": "", + "model_server": "", + "language": settings_data.get("language", "zh"), + "robot_type": settings_data.get("robot_type", "general_agent"), + "dataset_ids": settings_data.get("dataset_ids", []), + "system_prompt": settings_data.get("system_prompt", ""), + "user_identifier": settings_data.get("user_identifier", ""), + "enable_memori": settings_data.get("enable_memori", False), + "tool_response": settings_data.get("tool_response", True), + "enable_thinking": settings_data.get("enable_thinking", False), + "skills": settings_data.get("skills", []), + "description": settings_data.get("description", ""), + "suggestions": settings_data.get("suggestions", []) + } # 根据 model_id 查询模型信息 - model_id = config['model_id'] if model_id: await cursor.execute( """ @@ -532,21 +533,14 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]: config['api_key'] = model_row[2] else: logger.warning(f"Model with id {model_id} not found, using defaults") - config['model'] = "qwen3-next" - config['model_server'] = "" - config['api_key'] = "" else: - # 没有选择模型,使用默认值 - config['model'] = "qwen3-next" - config['model_server'] = "" - config['api_key'] = "" + logger.warning(f"No model_id set for bot {bot_user_id}, using defaults") - # 处理 dataset_ids (可能是 JSON 数组字符串或逗号分隔字符串) + # 处理 dataset_ids dataset_ids = config['dataset_ids'] if dataset_ids: if isinstance(dataset_ids, str): if dataset_ids.startswith('['): - import json try: config['dataset_ids'] = json.loads(dataset_ids) except json.JSONDecodeError: @@ -556,16 +550,20 @@ async def fetch_bot_config_from_db(bot_user_id: str) -> Dict[str, Any]: else: config['dataset_ids'] = [] - # 处理 skills (逗号分隔字符串) - skills = config.get('skills', '') - if skills: - if isinstance(skills, str): - config['skills'] = [s.strip() for s in skills.split(',') if s.strip()] - else: - config['skills'] = [] - else: + # 处理 skills + skills = config.get('skills', []) + if isinstance(skills, str): + config['skills'] = [s.strip() for s in skills.split(',') if s.strip()] + elif not isinstance(skills, list): config['skills'] = [] + # 处理 suggestions + suggestions = config.get('suggestions', []) + if isinstance(suggestions, str): + config['suggestions'] = [s.strip() for s in suggestions.split('\n') if s.strip()] + elif not isinstance(suggestions, list): + config['suggestions'] = [] + # 查询 MCP 服务器配置 await cursor.execute( """