From 608e53ed2f58ed52c3040e810730d52700d05628 Mon Sep 17 00:00:00 2001 From: "feifei.xu" <307327147@qq.com> Date: Sun, 15 Mar 2026 10:28:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Step=203=20-=20Bot=20API=20=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=20(v0.9.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 bots.py 路由文件 - 实现 9 个 REST API 端点: - GET /api/bots - 获取列表 - POST /api/bots - 创建 - GET /api/bots/:id - 详情 - PUT /api/bots/:id - 更新 - DELETE /api/bots/:id - 删除 - POST /api/bots/:id/bind - 绑定 Agent - POST /api/bots/:id/unbind - 解绑 Agent - GET /api/bots/:id/status - 状态 - POST /api/bots/:id/heartbeat - 心跳 - POST /api/bots/:id/token - 重新生成 Token - GET /api/bots/:id/stats - 统计 - 实现 JWT 权限检查 - 实现 X-Bot-Token 认证 - 注册蓝图到应用 - 更新版本号到 0.9.2 --- app/__init__.py | 4 +- app/routes/bots.py | 362 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 app/routes/bots.py diff --git a/app/__init__.py b/app/__init__.py index 2d5d58d..329ad0d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -39,7 +39,7 @@ def create_app(config_name='default'): def index(): return { 'service': '智队中枢', - 'version': '0.6.1', + 'version': '0.9.2', 'status': 'running', 'endpoints': { 'health': '/health', @@ -93,6 +93,7 @@ def _register_blueprints(app): from app.routes.gateways import gateways_bp from app.routes.messages import messages_bp from app.routes.stats import stats_bp + from app.routes.bots import bots_bp # Step 3: Bot API from app.web.routes import web_bp from app.web.api import web_api_bp @@ -102,6 +103,7 @@ def _register_blueprints(app): app.register_blueprint(gateways_bp, url_prefix='/api/gateways') app.register_blueprint(messages_bp, url_prefix='/api/messages') app.register_blueprint(stats_bp, url_prefix='/api/stats') + app.register_blueprint(bots_bp, url_prefix='/api/bots') # Step 3: Bot API app.register_blueprint(web_bp, url_prefix='/web') app.register_blueprint(web_api_bp) diff --git a/app/routes/bots.py b/app/routes/bots.py new file mode 100644 index 0000000..7211cba --- /dev/null +++ b/app/routes/bots.py @@ -0,0 +1,362 @@ +""" +Bot 路由 - 机器人管理 API +""" +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from datetime import datetime + +from app.models import User, Agent +from app.services.bot_service import BotService + +bots_bp = Blueprint('bots', __name__) + + +@bots_bp.route('/', methods=['GET']) +@jwt_required() +def get_bots(): + """ + 获取机器人列表 + + Query params: + - owner_only: bool - 只返回自己的 Bot + - include_system: bool - 包含系统 Bot + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + owner_only = request.args.get('owner_only', 'false').lower() == 'true' + + if owner_only: + bots = BotService.get_bots_by_owner(user_id) + else: + bots = BotService.get_available_bots(user) + + return jsonify({ + 'bots': [bot.to_dict() for bot in bots], + 'count': len(bots) + }), 200 + + +@bots_bp.route('/', methods=['POST']) +@jwt_required() +def create_bot(): + """ + 创建机器人 + + Request body: + - name: str - 机器人名称(必填,唯一) + - display_name: str - 显示名称 + - avatar: str - 头像 URL + - description: str - 描述 + - is_system: bool - 是否为系统级 Bot(仅 admin) + - agent_id: str - 绑定的 Agent ID + - capabilities: list - 能力标签 + - config: dict - 配置 + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + data = request.get_json() + + # 验证必填字段 + if not data or 'name' not in data: + return jsonify({'error': 'name is required'}), 400 + + name = data['name'].strip() + + # 检查名称是否已存在 + if BotService.get_bot_by_name(name): + return jsonify({'error': 'Bot name already exists'}), 400 + + # 检查 is_system 权限 + is_system = data.get('is_system', False) + if is_system and user.role != 'admin': + return jsonify({'error': 'Only admin can create system bots'}), 403 + + # 检查 agent_id 是否有效 + agent_id = data.get('agent_id') + if agent_id: + agent = Agent.query.get(agent_id) + if not agent: + return jsonify({'error': 'Agent not found'}), 404 + + # 创建 Bot + bot, token = BotService.create_bot( + name=name, + owner_id=user_id, + display_name=data.get('display_name'), + avatar=data.get('avatar'), + description=data.get('description'), + is_system=is_system, + agent_id=agent_id, + capabilities=data.get('capabilities'), + config=data.get('config') + ) + + return jsonify({ + 'bot': bot.to_dict(), + 'token': token # 只在创建时返回一次 + }), 201 + + +@bots_bp.route('/', methods=['GET']) +@jwt_required() +def get_bot(bot_id): + """获取机器人详情""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'view'): + return jsonify({'error': 'Permission denied'}), 403 + + return jsonify({'bot': bot.to_dict()}), 200 + + +@bots_bp.route('/', methods=['PUT']) +@jwt_required() +def update_bot(bot_id): + """ + 更新机器人配置 + + Request body: + - display_name: str + - avatar: str + - description: str + - capabilities: list + - config: dict + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'edit'): + return jsonify({'error': 'Permission denied'}), 403 + + data = request.get_json() or {} + + bot = BotService.update_bot( + bot=bot, + display_name=data.get('display_name'), + avatar=data.get('avatar'), + description=data.get('description'), + capabilities=data.get('capabilities'), + config=data.get('config') + ) + + return jsonify({'bot': bot.to_dict()}), 200 + + +@bots_bp.route('/', methods=['DELETE']) +@jwt_required() +def delete_bot(bot_id): + """删除机器人""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'delete'): + return jsonify({'error': 'Permission denied'}), 403 + + # 不允许删除系统 Bot(除非是 admin) + if bot.is_system and user.role != 'admin': + return jsonify({'error': 'Cannot delete system bot'}), 403 + + success = BotService.delete_bot(bot) + + if success: + return jsonify({'message': 'Bot deleted'}), 200 + else: + return jsonify({'error': 'Failed to delete bot'}), 500 + + +@bots_bp.route('//bind', methods=['POST']) +@jwt_required() +def bind_agent(bot_id): + """ + 绑定 Agent + + Request body: + - agent_id: str - Agent ID + """ + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'bind'): + return jsonify({'error': 'Permission denied'}), 403 + + data = request.get_json() + if not data or 'agent_id' not in data: + return jsonify({'error': 'agent_id is required'}), 400 + + agent_id = data['agent_id'] + agent = Agent.query.get(agent_id) + if not agent: + return jsonify({'error': 'Agent not found'}), 404 + + bot = BotService.bind_agent(bot, agent_id) + + return jsonify({ + 'bot': bot.to_dict(), + 'message': f'Bot bound to agent {agent.name}' + }), 200 + + +@bots_bp.route('//unbind', methods=['POST']) +@jwt_required() +def unbind_agent(bot_id): + """解绑 Agent""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'bind'): + return jsonify({'error': 'Permission denied'}), 403 + + bot = BotService.bind_agent(bot, None) + + return jsonify({ + 'bot': bot.to_dict(), + 'message': 'Bot unbound from agent' + }), 200 + + +@bots_bp.route('//status', methods=['GET']) +@jwt_required() +def get_bot_status(bot_id): + """获取机器人状态""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'view'): + return jsonify({'error': 'Permission denied'}), 403 + + return jsonify({ + 'bot_id': bot.id, + 'name': bot.name, + 'status': bot.status, + 'agent_id': bot.agent_id, + 'last_active_at': bot.last_active_at.isoformat() if bot.last_active_at else None + }), 200 + + +@bots_bp.route('//heartbeat', methods=['POST']) +def bot_heartbeat(bot_id): + """ + 机器人心跳上报 + + 用于 Bot Token 认证(非 JWT) + Headers: X-Bot-Token: + """ + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # Token 认证 + token = request.headers.get('X-Bot-Token') + if not token or not BotService.verify_token(bot, token): + return jsonify({'error': 'Invalid bot token'}), 401 + + # 更新状态 + BotService.update_status(bot, 'online') + + return jsonify({'status': 'ok'}), 200 + + +@bots_bp.route('//token', methods=['POST']) +@jwt_required() +def regenerate_token(bot_id): + """重新生成 Bot Token""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'edit'): + return jsonify({'error': 'Permission denied'}), 403 + + token = BotService.regenerate_token(bot) + + return jsonify({ + 'message': 'Token regenerated', + 'token': token + }), 200 + + +@bots_bp.route('//stats', methods=['GET']) +@jwt_required() +def get_bot_stats(bot_id): + """获取机器人统计信息""" + user_id = get_jwt_identity() + user = User.query.get(user_id) + + if not user: + return jsonify({'error': 'User not found'}), 404 + + bot = BotService.get_bot_by_id(bot_id) + if not bot: + return jsonify({'error': 'Bot not found'}), 404 + + # 权限检查 + if not BotService.check_permission(user, bot, 'view'): + return jsonify({'error': 'Permission denied'}), 403 + + stats = BotService.get_bot_stats(bot) + + return jsonify(stats), 200