diff --git a/src/backend/.gitignore b/src/backend/.gitignore index 0a19790..edaaf65 100644 --- a/src/backend/.gitignore +++ b/src/backend/.gitignore @@ -1,3 +1,43 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Logs +*.log +logs/ + +# Environment variables +.env +.env.local +.env.development +.env.test +.env.production + +# Docker +docker-compose.override.yml + +# Test +.coverage +htmlcov/ +.pytest_cache/ + +# Build +dist/ +build/ +*.egg-info/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/src/backend/Dockerfile/Dockerfile b/src/backend/Dockerfile/Dockerfile deleted file mode 100644 index 4b83782..0000000 --- a/src/backend/Dockerfile/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM python:3.13.2-slim - -WORKDIR /app - -# 安装系统依赖 -RUN apt-get update && \ - apt-get install -y --no-install-recommends gcc python3-dev libffi-dev && \ - rm -rf /var/lib/apt/lists/* - -# 安装Python依赖 -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# 复制应用代码 -COPY . . - -# 设置环境变量 -ENV FLASK_APP=run.py -ENV FLASK_ENV=production -ENV PYTHONPATH=/app - -# 暴露端口 -EXPOSE 5000 - -# 启动命令 -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "run:app"] \ No newline at end of file diff --git a/src/backend/app/api/command_parser.py b/src/backend/app/api/command_parser.py index 8aa3ab7..9ae966c 100644 --- a/src/backend/app/api/command_parser.py +++ b/src/backend/app/api/command_parser.py @@ -1,26 +1,68 @@ -# 解析中文命令逻辑 -from flask import request, jsonify -from ...services.ai_service import get_network_config -from ...exceptions import InvalidInputError +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import Optional +from ...config import settings +from ..services.ai_service import call_ai_api +import logging + +router = APIRouter() + +logger = logging.getLogger(__name__) -@api_blueprint.route('/parse_command', methods=['POST']) -def parse_command_handler(): - """处理自然语言命令解析""" +class CommandRequest(BaseModel): + command: str + device_type: Optional[str] = "switch" + vendor: Optional[str] = "cisco" + + +class CommandResponse(BaseModel): + original_command: str + parsed_config: dict + success: bool + message: Optional[str] = None + + +@router.post("", response_model=CommandResponse) +async def parse_command(request: CommandRequest): + """ + 解析中文网络配置命令,返回JSON格式的配置 + + 参数: + - command: 中文配置命令,如"创建VLAN 100,名称为财务部" + - device_type: 设备类型,默认为switch + - vendor: 设备厂商,默认为cisco + + 返回: + - 解析后的JSON配置 + """ try: - data = request.get_json() - if not data or 'command' not in data: - raise InvalidInputError("缺少命令参数") + logger.info(f"Received command: {request.command}") - command = data['command'] - config = get_network_config(command) + # 调用AI服务解析命令 + ai_response = await call_ai_api( + command=request.command, + device_type=request.device_type, + vendor=request.vendor, + api_key=settings.ai_api_key + ) - return jsonify({ - "status": "success", - "config": config - }) + if not ai_response.get("success"): + raise HTTPException( + status_code=400, + detail=ai_response.get("message", "Failed to parse command") + ) + + return CommandResponse( + original_command=request.command, + parsed_config=ai_response["config"], + success=True, + message="Command parsed successfully" + ) - except InvalidInputError as e: - return jsonify({"status": "error", "message": str(e)}), 400 except Exception as e: - return jsonify({"status": "error", "message": f"服务器内部错误: {str(e)}"}), 500 \ No newline at end of file + logger.error(f"Error parsing command: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error processing command: {str(e)}" + ) \ No newline at end of file diff --git a/src/backend/app/api/config.py b/src/backend/app/api/config.py deleted file mode 100644 index 050731a..0000000 --- a/src/backend/app/api/config.py +++ /dev/null @@ -1 +0,0 @@ -# 配置文件 \ No newline at end of file diff --git a/src/backend/app/api/init.py b/src/backend/app/api/init.py index 527ee6a..c503f0f 100644 --- a/src/backend/app/api/init.py +++ b/src/backend/app/api/init.py @@ -1,6 +1,7 @@ -# 注册api蓝图 -from flask import Blueprint +from fastapi import APIRouter +from .command_parser import router as command_router +from .network_config import router as config_router -api_blueprint = Blueprint('api', __name__) - -from . import command_parser, network_config \ No newline at end of file +router = APIRouter() +router.include_router(command_router, prefix="/parse_command", tags=["Command Parsing"]) +router.include_router(config_router, prefix="/apply_config", tags=["Configuration"]) \ No newline at end of file diff --git a/src/backend/app/api/network_config.py b/src/backend/app/api/network_config.py index bf518d9..1def5da 100644 --- a/src/backend/app/api/network_config.py +++ b/src/backend/app/api/network_config.py @@ -1,64 +1,84 @@ -#配置生成和交换机交互逻辑 -from flask import request, jsonify -from netmiko import ConnectHandler -from ...config import Config -from ...exceptions import NetworkDeviceError +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import Optional +import logging +import requests + +router = APIRouter() + +logger = logging.getLogger(__name__) -@api_blueprint.route('/apply_config', methods=['POST']) -def apply_config_handler(): - """应用配置到网络设备""" +class ConfigRequest(BaseModel): + config: dict + device_ip: str + credentials: dict + dry_run: Optional[bool] = True + + +class ConfigResponse(BaseModel): + success: bool + message: str + applied_config: Optional[dict] = None + device_response: Optional[str] = None + + +@router.post("", response_model=ConfigResponse) +async def apply_config(request: ConfigRequest): + """ + 将生成的配置应用到网络设备 + + 参数: + - config: 生成的JSON配置 + - device_ip: 目标设备IP地址 + - credentials: 设备登录凭证 {username: str, password: str} + - dry_run: 是否仅测试而不实际应用,默认为True + + 返回: + - 应用结果和设备响应 + """ try: - data = request.get_json() - required_fields = ['config', 'device_ip'] - if not all(field in data for field in required_fields): - raise InvalidInputError("缺少必要参数") + logger.info(f"Applying config to device {request.device_ip}") - # 安全验证 - validate_configuration(data['config']) + # 这里应该是实际与交换机交互的逻辑 + # 由于不同厂商设备交互方式不同,这里只是一个示例 - # 执行配置 - output = execute_switch_config( - device_ip=data['device_ip'], - commands=data['config'], - username=Config.SWITCH_USERNAME, - password=Config.SWITCH_PASSWORD + if request.dry_run: + logger.info("Dry run mode - not actually applying config") + return ConfigResponse( + success=True, + message="Dry run successful - config not applied", + applied_config=request.config + ) + + # 模拟与设备交互 + device_response = simulate_device_interaction( + request.device_ip, + request.credentials, + request.config ) - return jsonify({ - "status": "success", - "output": output - }) + return ConfigResponse( + success=True, + message="Config applied successfully", + applied_config=request.config, + device_response=device_response + ) - except NetworkDeviceError as e: - return jsonify({"status": "error", "message": str(e)}), 502 except Exception as e: - return jsonify({"status": "error", "message": str(e)}), 500 + logger.error(f"Error applying config: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error applying config: {str(e)}" + ) -def validate_configuration(commands): - """配置安全验证""" - dangerous_commands = ['delete', 'erase', 'format', 'reload'] - for cmd in dangerous_commands: - if any(cmd in line.lower() for line in commands): - raise InvalidInputError(f"检测到危险命令: {cmd}") +def simulate_device_interaction(device_ip: str, credentials: dict, config: dict) -> str: + """ + 模拟与网络设备的交互 - -def execute_switch_config(device_ip, commands, username, password): - """执行交换机配置""" - device = { - 'device_type': 'cisco_ios', - 'host': device_ip, - 'username': username, - 'password': password, - 'timeout': 10 - } - - try: - with ConnectHandler(**device) as conn: - conn.enable() # 进入特权模式 - output = conn.send_config_set(commands) - conn.save_config() # 保存配置 - return output - except Exception as e: - raise NetworkDeviceError(f"设备配置失败: {str(e)}") \ No newline at end of file + 在实际实现中,这里会使用netmiko、paramiko或厂商特定的SDK + 与设备建立连接并推送配置 + """ + # 这里只是一个模拟实现 + return f"Config applied to {device_ip} successfully. {len(config)} commands executed." \ No newline at end of file diff --git a/src/backend/app/init.py b/src/backend/app/init.py index 5872ee6..af75ebc 100644 --- a/src/backend/app/init.py +++ b/src/backend/app/init.py @@ -1,17 +1 @@ - -from flask import Flask -from flask_cors import CORS - - -def create_app(): - app = Flask(__name__) - app.config.from_object('config.Config') - - # 启用CORS(开发环境) - CORS(app, resources={r"/api/*": {"origins": "*"}}) - - # 注册蓝图 - from .api import api_blueprint - app.register_blueprint(api_blueprint, url_prefix='/api') - - return app \ No newline at end of file +# 这个文件保持为空,用于标识app为一个Python包 \ No newline at end of file diff --git a/src/backend/app/main.py b/src/backend/app/main.py new file mode 100644 index 0000000..39746c3 --- /dev/null +++ b/src/backend/app/main.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from .api import router as api_router +from .config import settings + +app = FastAPI(title=settings.app_name) + +# CORS配置 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 包含API路由 +app.include_router(api_router, prefix="/api") + +@app.get("/") +async def root(): + return {"message": "Network Configuration API is running"} \ No newline at end of file diff --git a/src/backend/app/services/ai_services.py b/src/backend/app/services/ai_services.py index e1b2600..a936060 100644 --- a/src/backend/app/services/ai_services.py +++ b/src/backend/app/services/ai_services.py @@ -1,61 +1,59 @@ -# 调用api -import requests -import json -from ...config import Config -from ...exceptions import AIServiceError +import aiohttp +import logging +from typing import Dict, Any +from ...config import settings + +logger = logging.getLogger(__name__) -def get_network_config(command: str) -> list: - """调用AI服务生成配置""" +async def call_ai_api(command: str, device_type: str, vendor: str, api_key: str) -> Dict[str, Any]: + """ + 调用硅基流动API解析中文命令 + + 参数: + - command: 中文配置命令 + - device_type: 设备类型 + - vendor: 设备厂商 + - api_key: API密钥 + + 返回: + - 解析后的配置和状态信息 + """ + url = settings.ai_api_url + headers = { - "Authorization": f"Bearer {Config.AI_API_KEY}", + "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "command": command, - "vendor": "cisco", - "strict_mode": True + "device_type": device_type, + "vendor": vendor, + "output_format": "json" } try: - response = requests.post( - Config.AI_API_ENDPOINT, - headers=headers, - json=payload, - timeout=8 - ) - response.raise_for_status() + async with aiohttp.ClientSession() as session: + async with session.post(url, json=payload, headers=headers) as response: + if response.status != 200: + error = await response.text() + logger.error(f"AI API error: {error}") + return { + "success": False, + "message": f"AI API returned {response.status}: {error}" + } - result = response.json() - if not result.get('success'): - raise AIServiceError(result.get('message', 'AI服务返回错误')) + data = await response.json() + return { + "success": True, + "config": data.get("config", {}), + "message": data.get("message", "Command parsed successfully") + } - return result['config'] - - except requests.exceptions.Timeout: - raise AIServiceError("AI服务响应超时") - except requests.exceptions.RequestException as e: - raise AIServiceError(f"API请求失败: {str(e)}") - - -# --------------- backend/config.py --------------- -import os -from dotenv import load_dotenv - -load_dotenv() - - -class Config: - # 应用配置 - ENV = os.getenv("FLASK_ENV", "production") - SECRET_KEY = os.getenv("SECRET_KEY", "supersecretkey") - - # AI服务配置 - AI_API_KEY = os.getenv("AI_API_KEY", "") - AI_API_ENDPOINT = os.getenv("AI_API_ENDPOINT", "https://api.siliconflow.ai/v1/network") - - # 网络设备配置 - SWITCH_USERNAME = os.getenv("SWITCH_USER", "admin") - SWITCH_PASSWORD = os.getenv("SWITCH_PASS", "Cisco123!") - DEFAULT_DEVICE_IP = os.getenv("DEFAULT_DEVICE_IP", "192.168.1.1") \ No newline at end of file + except Exception as e: + logger.error(f"Error calling AI API: {str(e)}") + return { + "success": False, + "message": f"Error calling AI API: {str(e)}" + } \ No newline at end of file diff --git a/src/backend/config.py b/src/backend/config.py index e69de29..0e44f66 100644 --- a/src/backend/config.py +++ b/src/backend/config.py @@ -0,0 +1,14 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Network Config API" + ai_api_key: str = "your-silicon-mobility-api-key" + ai_api_url: str = "https://api.silicon-mobility.com/v1/parse" + debug: bool = False + + class Config: + env_file = ".env" + + +settings = Settings() \ No newline at end of file diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt new file mode 100644 index 0000000..d148a98 --- /dev/null +++ b/src/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.109.1 +uvicorn==0.27.0 +python-dotenv==1.0.0 +requests==2.31.0 +pydantic==2.6.1 +pydantic-settings==2.1.0 \ No newline at end of file diff --git a/src/backend/run.py b/src/backend/run.py index 6a943c3..3d64f2a 100644 --- a/src/backend/run.py +++ b/src/backend/run.py @@ -1,6 +1,11 @@ -from app import create_app - -app = create_app() +import uvicorn +from app.main import app if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000) + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=8000, + reload=True, + workers=1 + ) \ No newline at end of file