diff --git a/src/backend/.gitignore b/src/backend/.gitignore
index edaaf65..e5effa1 100644
--- a/src/backend/.gitignore
+++ b/src/backend/.gitignore
@@ -1,52 +1,7 @@
-# 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]
-*$py.class
-
-# C extensions
*.so
-
-# Distribution / packaging
.Python
build/
develop-eggs/
@@ -60,155 +15,23 @@ parts/
sdist/
var/
wheels/
-share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
+# IDE
+.idea/
+.vscode/
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# UV
-# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-#uv.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
+# Environment
.env
-.venv
+.venv/
env/
venv/
ENV/
env.bak/
venv.bak/
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-# Ruff stuff:
-.ruff_cache/
-
-# PyPI configuration file
-.pypirc
+# Logs
+*.log
\ No newline at end of file
diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile
index a47b4ad..5fe4682 100644
--- a/src/backend/Dockerfile
+++ b/src/backend/Dockerfile
@@ -1,22 +1,15 @@
-version: '3.13.2'
+FROM python:3.13-slim
-services:
- app:
- build: .
- ports:
- - "8000:8000"
- depends_on:
- - redis
- environment:
- - REDIS_URL=redis://redis:6379
+WORKDIR /app
- redis:
- image: redis:alpine
- ports:
- - "6379:6379"
+COPY ./requirements.txt /app/requirements.txt
- worker:
- build: .
- command: celery -A app.services.task_service worker --loglevel=info
- depends_on:
- - ONBUILD
\ No newline at end of file
+RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
+
+COPY . /app
+
+ENV PYTHONPATH=/app
+ENV PORT=8000
+ENV HOST=0.0.0.0
+
+CMD ["uvicorn", "run:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
\ No newline at end of file
diff --git a/src/backend/__init__.py b/src/backend/__init__.py
new file mode 100644
index 0000000..5dd8fd0
--- /dev/null
+++ b/src/backend/__init__.py
@@ -0,0 +1 @@
+#empty
\ No newline at end of file
diff --git a/src/backend/app/__init__.py b/src/backend/app/__init__.py
new file mode 100644
index 0000000..ccadf06
--- /dev/null
+++ b/src/backend/app/__init__.py
@@ -0,0 +1,23 @@
+from fastapi import FastAPI
+from src.backend.app.api.endpoints import router as api_router
+from src.backend.app.utils.logger import setup_logging
+from src.backend.config import settings
+
+
+def create_app() -> FastAPI:
+ # 设置日志
+ setup_logging()
+
+ # 创建FastAPI应用
+ app = FastAPI(
+ title=settings.APP_NAME,
+ debug=settings.DEBUG,
+ docs_url=f"{settings.API_PREFIX}/docs",
+ redoc_url=f"{settings.API_PREFIX}/redoc",
+ openapi_url=f"{settings.API_PREFIX}/openapi.json"
+ )
+
+ # 添加API路由
+ app.include_router(api_router, prefix=settings.API_PREFIX)
+
+ return app
\ No newline at end of file
diff --git a/src/backend/app/adapters/__init__.py b/src/backend/app/adapters/__init__.py
deleted file mode 100644
index 16faff2..0000000
--- a/src/backend/app/adapters/__init__.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from .base import BaseAdapter
-from .cisco import CiscoAdapter
-from .huawei import HuaweiAdapter
-from .factory import AdapterFactory
-
-# 自动注册所有适配器类
-__all_adapters__ = {
- 'cisco': CiscoAdapter,
- 'huawei': HuaweiAdapter
-}
-
-def get_supported_vendors() -> list:
- """获取当前支持的设备厂商列表"""
- return list(__all_adapters__.keys())
-
-def init_adapters():
- """初始化适配器工厂"""
- AdapterFactory.register_adapters(__all_adapters__)
-
-# 应用启动时自动初始化
-init_adapters()
-
-__all__ = [
- 'BaseAdapter',
- 'CiscoAdapter',
- 'HuaweiAdapter',
- 'AdapterFactory',
- 'get_supported_vendors'
-]
\ No newline at end of file
diff --git a/src/backend/app/adapters/base.py b/src/backend/app/adapters/base.py
deleted file mode 100644
index 74814d3..0000000
--- a/src/backend/app/adapters/base.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# /backend/app/adapters/base.py
-from abc import ABC, abstractmethod
-from typing import Dict, Any
-
-class BaseAdapter(ABC):
- @abstractmethod
- async def connect(self, ip: str, credentials: Dict[str, str]):
- pass
-
- @abstractmethod
- async def deploy_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
- pass
-
- @abstractmethod
- async def get_status(self) -> Dict[str, Any]:
- pass
diff --git a/src/backend/app/adapters/cisco.py b/src/backend/app/adapters/cisco.py
deleted file mode 100644
index 3186cac..0000000
--- a/src/backend/app/adapters/cisco.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# /backend/app/adapters/cisco.py
-from netmiko import ConnectHandler
-from .base import BaseAdapter
-
-class CiscoAdapter(BaseAdapter):
- def __init__(self):
- self.connection = None
-
- async def connect(self, ip: str, credentials: Dict[str, str]):
- self.connection = ConnectHandler(
- device_type='cisco_ios',
- host=ip,
- username=credentials['username'],
- password=credentials['password'],
- timeout=10
- )
-
- async def deploy_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
- commands = self._generate_commands(config)
- output = self.connection.send_config_set(commands)
- return {'success': True, 'output': output}
-
- def _generate_commands(self, config: Dict[str, Any]) -> list:
- # 实际生产中应使用Jinja2模板
- commands = []
- if 'vlans' in config:
- for vlan in config['vlans']:
- commands.extend([
- f"vlan {vlan['id']}",
- f"name {vlan['name']}"
- ])
- return commands
\ No newline at end of file
diff --git a/src/backend/app/adapters/factory.py b/src/backend/app/adapters/factory.py
deleted file mode 100644
index ea1c243..0000000
--- a/src/backend/app/adapters/factory.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from . import BaseAdapter
-from .cisco import CiscoAdapter
-from .huawei import HuaweiAdapter
-
-class AdapterFactory:
- _adapters = {}
-
- @classmethod
- def register_adapters(cls, adapters: dict):
- """注册适配器字典"""
- cls._adapters.update(adapters)
-
- @classmethod
- def get_adapter(vendor: str)->BaseAdapter:
- adapters = {
- 'cisco': CiscoAdapter,
- 'huawei': HuaweiAdapter
- }
- if vendor not in cls._adapters:
- raise ValueError(f"Unsupported vendor: {vendor}")
- return cls._adapters[vendor]()
diff --git a/src/backend/app/adapters/huawei.py b/src/backend/app/adapters/huawei.py
deleted file mode 100644
index e495e96..0000000
--- a/src/backend/app/adapters/huawei.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import httpx
-from .base import BaseAdapter
-
-class HuaweiAdapter(BaseAdapter):
- def __init__(self):
- self.client = None
- self.base_url = None
-
- async def connect(self, ip: str, credentials: dict):
- self.base_url = f"https://{ip}/restconf"
- self.client = httpx.AsyncClient(
- auth=(credentials['username'], credentials['password']),
- verify=False,
- timeout=30.0
- )
-
- async def deploy_config(self, config: dict):
- headers = {"Content-Type": "application/yang-data+json"}
- url = f"{self.base_url}/data/ietf-restconf:operations/network-topology:deploy"
- response = await self.client.post(url, json=config, headers=headers)
- response.raise_for_status()
- return response.json()
-
- async def disconnect(self):
- if self.client:
- await self.client.aclose()
\ No newline at end of file
diff --git a/src/backend/app/api/__init__.py b/src/backend/app/api/__init__.py
index 6d50dc5..9b2cbfd 100644
--- a/src/backend/app/api/__init__.py
+++ b/src/backend/app/api/__init__.py
@@ -1,7 +1,4 @@
from fastapi import APIRouter
-from src.backend.app.api.command_parser import router as command_router
-from src.backend.app.api.network_config import router as config_router
+from .endpoints import router
-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
+__all__ = ["router"]
\ No newline at end of file
diff --git a/src/backend/app/api/bulk.py b/src/backend/app/api/bulk.py
deleted file mode 100644
index ac5d422..0000000
--- a/src/backend/app/api/bulk.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from fastapi import APIRouter, HTTPException, BackgroundTasks
-from pydantic import BaseModel
-from typing import List
-from app.services.batch import BatchService
-from app.utils.decorators import async_retry
-
-router = APIRouter()
-
-class BulkDeviceConfig(BaseModel):
- device_ips: List[str]
- config: dict
- credentials: dict
- vendor: str = "cisco"
- timeout: int = 30
-
-@router.post("/config")
-@async_retry(max_attempts=3, delay=1)
-async def bulk_apply_config(request: BulkDeviceConfig, bg_tasks: BackgroundTasks):
- """
- 批量配置设备接口
- 示例请求体:
- {
- "device_ips": ["192.168.1.1", "192.168.1.2"],
- "config": {"vlans": [{"id": 100, "name": "test"}]},
- "credentials": {"username": "admin", "password": "secret"},
- "vendor": "cisco"
- }
- """
- devices = [{
- "ip": ip,
- "credentials": request.credentials,
- "vendor": request.vendor
- } for ip in request.device_ips]
-
- try:
- batch = BatchService()
- bg_tasks.add_task(batch.deploy_batch, devices, request.config)
- return {"message": "Batch job started", "device_count": len(devices)}
- except Exception as e:
- raise HTTPException(500, detail=str(e))
\ 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 3a8045d..071473e 100644
--- a/src/backend/app/api/command_parser.py
+++ b/src/backend/app/api/command_parser.py
@@ -1,68 +1,69 @@
-from fastapi import APIRouter, HTTPException
-from pydantic import BaseModel
-from typing import Optional
+from typing import Dict, Any
+from src.backend.app.services.ai_services import AIService
from src.backend.config import settings
-from src.backend.app.services.ai_service import call_ai_api
-import logging
-
-router = APIRouter()
-
-logger = logging.getLogger(__name__)
-class CommandRequest(BaseModel):
- command: str
- device_type: Optional[str] = "switch"
- vendor: Optional[str] = "cisco"
+class CommandParser:
+ def __init__(self):
+ self.ai_service = AIService(settings.SILICONFLOW_API_KEY, settings.SILICONFLOW_API_URL)
+ async def parse(self, command: str) -> Dict[str, Any]:
+ """
+ 解析中文命令并返回配置
+ """
+ # 首先尝试本地简单解析
+ local_parsed = self._try_local_parse(command)
+ if local_parsed:
+ return local_parsed
-class CommandResponse(BaseModel):
- original_command: str
- parsed_config: dict
- success: bool
- message: Optional[str] = None
+ # 本地无法解析则调用AI服务
+ return await self.ai_service.parse_command(command)
+ def _try_local_parse(self, command: str) -> Dict[str, Any]:
+ """
+ 尝试本地解析常见命令
+ """
+ command = command.lower().strip()
-@router.post("", response_model=CommandResponse)
-async def parse_command(request: CommandRequest):
- """
- 解析中文网络配置命令,返回JSON格式的配置
+ # VLAN配置
+ if "vlan" in command and "创建" in command:
+ parts = command.split()
+ vlan_id = next((p for p in parts if p.isdigit()), None)
+ if vlan_id:
+ return {
+ "type": "vlan",
+ "vlan_id": int(vlan_id),
+ "name": f"VLAN{vlan_id}",
+ "interfaces": []
+ }
- 参数:
- - command: 中文配置命令,如"创建VLAN 100,名称为财务部"
- - device_type: 设备类型,默认为switch
- - vendor: 设备厂商,默认为cisco
+ # 接口配置
+ if any(word in command for word in ["接口", "端口"]) and any(
+ word in command for word in ["启用", "关闭", "配置"]):
+ parts = command.split()
+ interface = next((p for p in parts if p.startswith(("gi", "fa", "te", "eth"))), None)
+ if interface:
+ config = {
+ "type": "interface",
+ "interface": interface,
+ "state": "up" if "启用" in command else "down"
+ }
- 返回:
- - 解析后的JSON配置
- """
- try:
- logger.info(f"Received command: {request.command}")
+ if "ip" in command and "地址" in command:
+ ip_part = next((p for p in parts if "." in p and p.count(".") == 3), None)
+ if ip_part:
+ config["ip_address"] = ip_part
- # 调用AI服务解析命令
- ai_response = await call_ai_api(
- command=request.command,
- device_type=request.device_type,
- vendor=request.vendor,
- api_key=settings.ai_api_key
- )
+ if "描述" in command:
+ desc_start = command.find("描述") + 2
+ description = command[desc_start:].strip()
+ config["description"] = description
- if not ai_response.get("success"):
- raise HTTPException(
- status_code=400,
- detail=ai_response.get("message", "Failed to parse command")
- )
+ if "vlan" in command:
+ vlan_id = next((p for p in parts if p.isdigit()), None)
+ if vlan_id:
+ config["vlan"] = int(vlan_id)
- return CommandResponse(
- original_command=request.command,
- parsed_config=ai_response["config"],
- success=True,
- message="Command parsed successfully"
- )
+ return config
- except Exception as e:
- 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
+ return None
\ No newline at end of file
diff --git a/src/backend/app/api/endpoints.py b/src/backend/app/api/endpoints.py
new file mode 100644
index 0000000..0234d42
--- /dev/null
+++ b/src/backend/app/api/endpoints.py
@@ -0,0 +1,50 @@
+from fastapi import APIRouter, Depends, HTTPException
+from typing import Any
+from pydantic import BaseModel
+
+from src.backend.app.services.ai_services import AIService
+from src.backend.app.api.network_config import SwitchConfigurator
+from src.backend.config import settings
+
+router = APIRouter()
+
+class CommandRequest(BaseModel):
+ command: str
+
+class ConfigRequest(BaseModel):
+ config: dict
+ switch_ip: str
+
+@router.post("/parse_command", response_model=dict)
+async def parse_command(request: CommandRequest):
+ """
+ 解析中文命令并返回JSON配置
+ """
+ try:
+ ai_service = AIService(settings.SILICONFLOW_API_KEY, settings.SILICONFLOW_API_URL)
+ config = await ai_service.parse_command(request.command)
+ return {"success": True, "config": config}
+ except Exception as e:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Failed to parse command: {str(e)}"
+ )
+
+@router.post("/apply_config", response_model=dict)
+async def apply_config(request: ConfigRequest):
+ """
+ 应用配置到交换机
+ """
+ try:
+ configurator = SwitchConfigurator(
+ username=settings.SWITCH_USERNAME,
+ password=settings.SWITCH_PASSWORD,
+ timeout=settings.SWITCH_TIMEOUT
+ )
+ result = await configurator.apply_config(request.switch_ip, request.config)
+ return {"success": True, "result": result}
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to apply config: {str(e)}"
+ )
\ No newline at end of file
diff --git a/src/backend/app/api/health.py b/src/backend/app/api/health.py
deleted file mode 100644
index dde2155..0000000
--- a/src/backend/app/api/health.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from fastapi import APIRouter
-from ...monitoring.healthcheck import check_redis, check_ai_service
-
-router = APIRouter()
-
-@router.get("/live")
-async def liveness_check():
- return {"status": "alive"}
-
-@router.get("/ready")
-async def readiness_check():
- redis_ok = await check_redis()
- ai_ok = await check_ai_service()
- return {
- "redis": redis_ok,
- "ai_service": ai_ok
- }
\ 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 1def5da..e8f85e0 100644
--- a/src/backend/app/api/network_config.py
+++ b/src/backend/app/api/network_config.py
@@ -1,84 +1,174 @@
-from fastapi import APIRouter, HTTPException
-from pydantic import BaseModel
-from typing import Optional
-import logging
-import requests
-
-router = APIRouter()
-
-logger = logging.getLogger(__name__)
+import paramiko
+import asyncio
+from typing import Dict, Any
+from ..utils.exceptions import SwitchConfigException
-class ConfigRequest(BaseModel):
- config: dict
- device_ip: str
- credentials: dict
- dry_run: Optional[bool] = True
+class SwitchConfigurator:
+ def __init__(self, username: str, password: str, timeout: int = 10):
+ self.username = username
+ self.password = password
+ self.timeout = timeout
+ async def apply_config(self, switch_ip: str, config: Dict[str, Any]) -> str:
+ """
+ 应用配置到交换机
+ """
+ try:
+ # 根据配置类型调用不同的方法
+ config_type = config.get("type", "").lower()
-class ConfigResponse(BaseModel):
- success: bool
- message: str
- applied_config: Optional[dict] = None
- device_response: Optional[str] = None
+ if config_type == "vlan":
+ return await self._configure_vlan(switch_ip, config)
+ elif config_type == "interface":
+ return await self._configure_interface(switch_ip, config)
+ elif config_type == "acl":
+ return await self._configure_acl(switch_ip, config)
+ elif config_type == "route":
+ return await self._configure_route(switch_ip, config)
+ else:
+ raise SwitchConfigException(f"Unsupported config type: {config_type}")
+ except Exception as e:
+ raise SwitchConfigException(str(e))
+ async def _send_commands(self, switch_ip: str, commands: list) -> str:
+ """
+ 发送命令到交换机
+ """
+ try:
+ # 使用Paramiko建立SSH连接
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-@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:
- logger.info(f"Applying config to device {request.device_ip}")
-
- # 这里应该是实际与交换机交互的逻辑
- # 由于不同厂商设备交互方式不同,这里只是一个示例
-
- 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
+ # 在异步上下文中运行阻塞操作
+ loop = asyncio.get_event_loop()
+ await loop.run_in_executor(
+ None,
+ lambda: ssh.connect(
+ switch_ip,
+ username=self.username,
+ password=self.password,
+ timeout=self.timeout
+ )
)
- # 模拟与设备交互
- device_response = simulate_device_interaction(
- request.device_ip,
- request.credentials,
- request.config
- )
+ # 获取SSH shell
+ shell = await loop.run_in_executor(None, ssh.invoke_shell)
- return ConfigResponse(
- success=True,
- message="Config applied successfully",
- applied_config=request.config,
- device_response=device_response
- )
+ # 发送配置命令
+ output = ""
+ for cmd in commands:
+ await loop.run_in_executor(None, shell.send, cmd + "\n")
+ await asyncio.sleep(0.5)
+ while shell.recv_ready():
+ output += (await loop.run_in_executor(None, shell.recv, 1024)).decode("utf-8")
- except Exception as e:
- logger.error(f"Error applying config: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=f"Error applying config: {str(e)}"
- )
+ # 关闭连接
+ await loop.run_in_executor(None, ssh.close)
+ return output
+ except Exception as e:
+ raise SwitchConfigException(f"SSH connection failed: {str(e)}")
-def simulate_device_interaction(device_ip: str, credentials: dict, config: dict) -> str:
- """
- 模拟与网络设备的交互
+ async def _configure_vlan(self, switch_ip: str, config: Dict[str, Any]) -> str:
+ """
+ 配置VLAN
+ """
+ vlan_id = config["vlan_id"]
+ vlan_name = config.get("name", f"VLAN{vlan_id}")
+ interfaces = config.get("interfaces", [])
- 在实际实现中,这里会使用netmiko、paramiko或厂商特定的SDK
- 与设备建立连接并推送配置
- """
- # 这里只是一个模拟实现
- return f"Config applied to {device_ip} successfully. {len(config)} commands executed."
\ No newline at end of file
+ commands = [
+ "configure terminal",
+ f"vlan {vlan_id}",
+ f"name {vlan_name}",
+ ]
+
+ # 配置接口
+ for intf in interfaces:
+ commands.extend([
+ f"interface {intf['interface']}",
+ f"switchport access vlan {vlan_id}",
+ "exit"
+ ])
+
+ commands.append("end")
+
+ return await self._send_commands(switch_ip, commands)
+
+ async def _configure_interface(self, switch_ip: str, config: Dict[str, Any]) -> str:
+ """
+ 配置接口
+ """
+ interface = config["interface"]
+ ip_address = config.get("ip_address")
+ description = config.get("description", "")
+ vlan = config.get("vlan")
+ state = config.get("state", "up")
+
+ commands = [
+ "configure terminal",
+ f"interface {interface}",
+ f"description {description}",
+ ]
+
+ if ip_address:
+ commands.append(f"ip address {ip_address}")
+
+ if vlan:
+ commands.append(f"switchport access vlan {vlan}")
+
+ if state.lower() == "up":
+ commands.append("no shutdown")
+ else:
+ commands.append("shutdown")
+
+ commands.extend(["exit", "end"])
+
+ return await self._send_commands(switch_ip, commands)
+
+ async def _configure_acl(self, switch_ip: str, config: Dict[str, Any]) -> str:
+ """
+ 配置ACL
+ """
+ acl_id = config["acl_id"]
+ acl_type = config.get("type", "standard")
+ rules = config.get("rules", [])
+
+ commands = ["configure terminal"]
+
+ if acl_type == "standard":
+ commands.append(f"access-list {acl_id} standard")
+ else:
+ commands.append(f"access-list {acl_id} extended")
+
+ for rule in rules:
+ action = rule.get("action", "permit")
+ source = rule.get("source", "any")
+ destination = rule.get("destination", "any")
+ protocol = rule.get("protocol", "ip")
+
+ if acl_type == "standard":
+ commands.append(f"{action} {source}")
+ else:
+ commands.append(f"{action} {protocol} {source} {destination}")
+
+ commands.append("end")
+
+ return await self._send_commands(switch_ip, commands)
+
+ async def _configure_route(self, switch_ip: str, config: Dict[str, Any]) -> str:
+ """
+ 配置路由
+ """
+ network = config["network"]
+ mask = config["mask"]
+ next_hop = config["next_hop"]
+
+ commands = [
+ "configure terminal",
+ f"ip route {network} {mask} {next_hop}",
+ "end"
+ ]
+
+ return await self._send_commands(switch_ip, commands)
\ No newline at end of file
diff --git a/src/backend/app/api/topology.py b/src/backend/app/api/topology.py
deleted file mode 100644
index 5be8809..0000000
--- a/src/backend/app/api/topology.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from fastapi import APIRouter, BackgroundTasks
-from pydantic import BaseModel
-from ...services.task_service import deploy_to_device
-
-router = APIRouter()
-
-class TopologyRequest(BaseModel):
- devices: list
- config: dict
-
-@router.post("/deploy")
-async def deploy_topology(
- request: TopologyRequest,
- bg_tasks: BackgroundTasks
-):
- task_ids = []
- for device in request.devices:
- task = deploy_to_device.delay(device, request.config)
- task_ids.append(task.id)
- return {"task_ids": task_ids}
diff --git a/src/backend/app/init.py b/src/backend/app/init.py
deleted file mode 100644
index af75ebc..0000000
--- a/src/backend/app/init.py
+++ /dev/null
@@ -1 +0,0 @@
-# 这个文件保持为空,用于标识app为一个Python包
\ No newline at end of file
diff --git a/src/backend/app/main.py b/src/backend/app/main.py
deleted file mode 100644
index 3696104..0000000
--- a/src/backend/app/main.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from fastapi import FastAPI
-from fastapi.middleware.cors import CORSMiddleware
-from src.backend.app.api.command_parser import router as api_router
-from src.backend.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/models/__init__.py b/src/backend/app/models/__init__.py
index b945124..dc4e74f 100644
--- a/src/backend/app/models/__init__.py
+++ b/src/backend/app/models/__init__.py
@@ -1,10 +1,30 @@
-from .device import DeviceCredentials, DeviceInfo
-from .topology import TopologyType, DeviceRole, NetworkTopology
+# 数据模型模块初始化文件
+# 目前项目不使用数据库,此文件保持为空
+# 未来如果需要添加数据库模型,可以在这里定义
-__all__ = [
- 'DeviceCredentials',
- 'DeviceInfo',
- 'TopologyType',
- 'DeviceRole',
- 'NetworkTopology'
-]
\ No newline at end of file
+from pydantic import BaseModel
+from typing import Optional
+
+# 示例:基础响应模型
+class BaseResponse(BaseModel):
+ success: bool
+ message: Optional[str] = None
+ data: Optional[dict] = None
+
+# 示例:交换机连接信息模型(如果需要存储交换机信息)
+class SwitchInfo(BaseModel):
+ ip: str
+ username: str
+ password: str
+ model: Optional[str] = None
+ description: Optional[str] = None
+
+# 示例:配置历史记录模型
+class ConfigHistory(BaseModel):
+ command: str
+ config: dict
+ timestamp: float
+ status: str # success/failed
+ error: Optional[str] = None
+
+__all__ = ["BaseResponse", "SwitchInfo", "ConfigHistory"]
\ No newline at end of file
diff --git a/src/backend/app/models/devices.py b/src/backend/app/models/devices.py
deleted file mode 100644
index 803d448..0000000
--- a/src/backend/app/models/devices.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from pydantic import BaseModel
-from typing import Optional
-
-class DeviceCredentials(BaseModel):
- username: str
- password: str
- enable_password: Optional[str] = None
-
-class DeviceInfo(BaseModel):
- ip: str
- vendor: str
- model: Optional[str] = None
- os_version: Optional[str] = None
- credentials: DeviceCredentials
\ No newline at end of file
diff --git a/src/backend/app/models/topology.py b/src/backend/app/models/topology.py
deleted file mode 100644
index b944604..0000000
--- a/src/backend/app/models/topology.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#拓补数据结构
-from enum import Enum
-from typing import Dict, List
-from pydantic import BaseModel
-
-class TopologyType(str, Enum):
- SPINE_LEAF = "spine-leaf"
- CORE_ACCESS = "core-access"
- RING = "ring"
-
-class DeviceRole(str, Enum):
- CORE = "core"
- SPINE = "spine"
- LEAF = "leaf"
- ACCESS = "access"
-
-class NetworkTopology(BaseModel):
- type: TopologyType
- devices: Dict[DeviceRole, List[str]]
- links: Dict[str, List[str]]
\ No newline at end of file
diff --git a/src/backend/app/monitoring/metrics.py b/src/backend/app/monitoring/metrics.py
deleted file mode 100644
index c39adf4..0000000
--- a/src/backend/app/monitoring/metrics.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from prometheus_client import (
- Counter,
- Gauge,
- Histogram,
- Summary
-)
-
-# API Metrics
-API_REQUESTS = Counter(
- 'api_requests_total',
- 'Total API requests',
- ['method', 'endpoint', 'status']
-)
-
-API_LATENCY = Histogram(
- 'api_request_latency_seconds',
- 'API request latency',
- ['endpoint']
-)
-
-# Device Metrics
-DEVICE_CONNECTIONS = Gauge(
- 'network_device_connections',
- 'Active device connections',
- ['vendor']
-)
-
-CONFIG_APPLY_TIME = Summary(
- 'config_apply_seconds',
- 'Time spent applying configurations'
-)
-
-# Error Metrics
-CONFIG_ERRORS = Counter(
- 'config_errors_total',
- 'Configuration errors',
- ['error_type']
-)
-
-def observe_api_request(method: str, endpoint: str, status: int, duration: float):
- API_REQUESTS.labels(method, endpoint, status).inc()
- API_LATENCY.labels(endpoint).observe(duration)
\ No newline at end of file
diff --git a/src/backend/app/monitoring/middleware.py b/src/backend/app/monitoring/middleware.py
deleted file mode 100644
index 2c7e718..0000000
--- a/src/backend/app/monitoring/middleware.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from prometheus_client import Counter, Histogram
-from fastapi import Request
-
-REQUESTS = Counter(
- 'api_requests_total',
- 'Total API Requests',
- ['method', 'endpoint']
-)
-
-LATENCY = Histogram(
- 'api_request_latency_seconds',
- 'API Request Latency',
- ['endpoint']
-)
-
-
-async def monitor_requests(request: Request, call_next):
- start_time = time.time()
- response = await call_next(request)
- latency = time.time() - start_time
-
- REQUESTS.labels(
- method=request.method,
- endpoint=request.url.path
- ).inc()
-
- LATENCY.labels(
- endpoint=request.url.path
- ).observe(latency)
-
- return response
\ No newline at end of file
diff --git a/src/backend/app/services/__init__.py b/src/backend/app/services/__init__.py
deleted file mode 100644
index 5ae93d7..0000000
--- a/src/backend/app/services/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from .task_service import celery_app
-from .ai_service import AIService
-from .topology import TopologyService
-from .batch import BatchService
-
-# 单例服务实例
-ai_service = AIService()
-topology_service = TopologyService()
-batch_service = BatchService()
-
-__all__ = [
- 'celery_app',
- 'ai_service',
- 'topology_service',
- 'batch_service'
-]
\ 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 bc02f6c..e4d0712 100644
--- a/src/backend/app/services/ai_services.py
+++ b/src/backend/app/services/ai_services.py
@@ -1,59 +1,61 @@
-import aiohttp
-import logging
+import json
+import httpx
from typing import Dict, Any
-from src.backend.config import settings
-
-logger = logging.getLogger(__name__)
+from src.backend.app.utils.exceptions import SiliconFlowAPIException
-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 {api_key}",
- "Content-Type": "application/json"
- }
-
- payload = {
- "command": command,
- "device_type": device_type,
- "vendor": vendor,
- "output_format": "json"
- }
-
- try:
- 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}"
- }
-
- data = await response.json()
- return {
- "success": True,
- "config": data.get("config", {}),
- "message": data.get("message", "Command parsed successfully")
- }
-
- except Exception as e:
- logger.error(f"Error calling AI API: {str(e)}")
- return {
- "success": False,
- "message": f"Error calling AI API: {str(e)}"
+class AIService:
+ def __init__(self, api_key: str, api_url: str):
+ self.api_key = api_key
+ self.api_url = api_url
+ self.headers = {
+ "Authorization": f"Bearer {self.api_key}",
+ "Content-Type": "application/json"
}
+
+ async def parse_command(self, command: str) -> Dict[str, Any]:
+ """
+ 调用硅基流动API解析中文命令
+ """
+ prompt = f"""
+ 你是一个网络设备配置专家,请将以下中文命令转换为网络设备配置JSON。
+ 支持的配置包括:VLAN、端口、路由、ACL等。
+ 返回格式必须为JSON,包含配置类型和详细参数。
+
+ 命令:{command}
+ """
+
+ data = {
+ "model": "text-davinci-003",
+ "prompt": prompt,
+ "max_tokens": 1000,
+ "temperature": 0.3
+ }
+
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.post(
+ f"{self.api_url}/completions",
+ headers=self.headers,
+ json=data,
+ timeout=30
+ )
+
+ if response.status_code != 200:
+ raise SiliconFlowAPIException(response.text)
+
+ result = response.json()
+ config_str = result["choices"][0]["text"].strip()
+
+ # 确保返回的是有效的JSON
+ try:
+ config = json.loads(config_str)
+ return config
+ except json.JSONDecodeError:
+ # 尝试修复可能的多余字符
+ if config_str.startswith("```json"):
+ config_str = config_str[7:-3].strip()
+ return json.loads(config_str)
+ raise SiliconFlowAPIException("Invalid JSON format returned from AI")
+ except httpx.HTTPError as e:
+ raise SiliconFlowAPIException(str(e))
\ No newline at end of file
diff --git a/src/backend/app/services/batch.py b/src/backend/app/services/batch.py
deleted file mode 100644
index 9339583..0000000
--- a/src/backend/app/services/batch.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import asyncio
-from typing import List, Dict, Any
-from app.adapters.factory import AdapterFactory
-from app.utils.connection_pool import ConnectionPool
-from app.monitoring.metrics import (
- DEVICE_CONNECTIONS,
- CONFIG_APPLY_TIME,
- CONFIG_ERRORS
-)
-
-
-class BatchService:
- def __init__(self, max_workers: int = 10):
- self.semaphore = asyncio.Semaphore(max_workers)
- self.pool = ConnectionPool()
-
- @CONFIG_APPLY_TIME.time()
- async def deploy_batch(self, devices: List[Dict], config: Dict[str, Any]):
- async def _deploy(device):
- vendor = device.get('vendor', 'cisco')
- async with self.semaphore:
- try:
- adapter = AdapterFactory.get_adapter(vendor)
- await adapter.connect(device['ip'], device['credentials'])
- DEVICE_CONNECTIONS.labels(vendor).inc()
-
- result = await adapter.deploy_config(config)
- return {
- "device": device['ip'],
- "status": "success",
- "result": result
- }
- except ConnectionError as e:
- CONFIG_ERRORS.labels("connection").inc()
- return {
- "device": device['ip'],
- "status": "failed",
- "error": str(e)
- }
- finally:
- if adapter:
- await adapter.disconnect()
- DEVICE_CONNECTIONS.labels(vendor).dec()
-
- return await asyncio.gather(
- *[_deploy(device) for device in devices],
- return_exceptions=True
- )
\ No newline at end of file
diff --git a/src/backend/app/services/task_service.py b/src/backend/app/services/task_service.py
deleted file mode 100644
index 4b3985a..0000000
--- a/src/backend/app/services/task_service.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#Celery任务定义
-from celery import Celery
-from src.backend.app.utils.connection_pool import ConnectionPool
-from src.backend.config import settings
-
-celery = Celery(__name__, broker=settings.REDIS_URL)
-pool = ConnectionPool(max_size=settings.MAX_CONNECTIONS)
-
-@celery.task
-async def deploy_to_device(device_info: dict, config: dict):
- adapter = await pool.get(device_info['vendor'])
- try:
- await adapter.connect(device_info['ip'], device_info['credentials'])
- result = await adapter.deploy_config(config)
- await pool.release(adapter)
- return {'device': device_info['ip'], 'result': result}
- except Exception as e:
- return {'device': device_info['ip'], 'error': str(e)}
\ No newline at end of file
diff --git a/src/backend/app/services/topology_service.py b/src/backend/app/services/topology_service.py
deleted file mode 100644
index 7b61cb5..0000000
--- a/src/backend/app/services/topology_service.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#拓补处理逻辑
-def generate_multi_device_config(topology):
- """
- topology示例:
- {
- "core_switches": [sw1, sw2],
- "access_switches": {
- "sw1": [sw3, sw4],
- "sw2": [sw5, sw6]
- }
- }
- """
- configs = {}
- # 生成核心层配置(如MSTP根桥选举)
- for sw in topology['core_switches']:
- configs[sw] = generate_core_config(sw)
-
- # 生成接入层配置(如端口绑定)
- for core_sw, access_sws in topology['access_switches'].items():
- for sw in access_sws:
- configs[sw] = generate_access_config(sw, uplink=core_sw)
-
- return configs
\ No newline at end of file
diff --git a/src/backend/app/utils/cli_templates.py b/src/backend/app/utils/cli_templates.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/backend/app/utils/connection_pool.py b/src/backend/app/utils/connection_pool.py
deleted file mode 100644
index d601f93..0000000
--- a/src/backend/app/utils/connection_pool.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#连接池
-# /backend/app/utils/connection_pool.py
-import asyncio
-from collections import deque
-from ..adapters import cisco, huawei
-
-class ConnectionPool:
- def __init__(self, max_size=10):
- self.max_size = max_size
- self.pool = deque(maxlen=max_size)
- self.lock = asyncio.Lock()
-
- async def get(self, vendor: str):
- async with self.lock:
- if self.pool:
- return self.pool.pop()
- return CiscoAdapter() if vendor == 'cisco' else HuaweiAdapter()
-
- async def release(self, adapter):
- async with self.lock:
- if len(self.pool) < self.max_size:
- self.pool.append(adapter)
\ No newline at end of file
diff --git a/src/backend/app/utils/exceptions.py b/src/backend/app/utils/exceptions.py
new file mode 100644
index 0000000..4108d08
--- /dev/null
+++ b/src/backend/app/utils/exceptions.py
@@ -0,0 +1,22 @@
+from fastapi import HTTPException, status
+
+class AICommandParseException(HTTPException):
+ def __init__(self, detail: str):
+ super().__init__(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=f"AI command parse error: {detail}"
+ )
+
+class SwitchConfigException(HTTPException):
+ def __init__(self, detail: str):
+ super().__init__(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Switch configuration error: {detail}"
+ )
+
+class SiliconFlowAPIException(HTTPException):
+ def __init__(self, detail: str):
+ super().__init__(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail=f"SiliconFlow API error: {detail}"
+ )
\ No newline at end of file
diff --git a/src/backend/app/utils/logger.py b/src/backend/app/utils/logger.py
new file mode 100644
index 0000000..3021c0e
--- /dev/null
+++ b/src/backend/app/utils/logger.py
@@ -0,0 +1,33 @@
+import logging
+from loguru import logger
+import sys
+
+
+class InterceptHandler(logging.Handler):
+ def emit(self, record):
+ # Get corresponding Loguru level if it exists
+ try:
+ level = logger.level(record.levelname).name
+ except ValueError:
+ level = record.levelno
+
+ # Find caller from where originated the logged message
+ frame, depth = logging.currentframe(), 2
+ while frame.f_code.co_filename == logging.__file__:
+ frame = frame.f_back
+ depth += 1
+
+ logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
+
+
+def setup_logging():
+ # 拦截标准logging
+ logging.basicConfig(handlers=[InterceptHandler()], level=0)
+
+ # 配置loguru
+ logger.configure(
+ handlers=[
+ {"sink": sys.stdout,
+ "format": "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}"}
+ ]
+ )
\ No newline at end of file
diff --git a/src/backend/celeryconfig.py b/src/backend/celeryconfig.py
deleted file mode 100644
index 8cd8365..0000000
--- a/src/backend/celeryconfig.py
+++ /dev/null
@@ -1,9 +0,0 @@
-broker_url = 'redis://redis:6379/0'
-result_backend = 'redis://redis:6379/1'
-task_serializer = 'json'
-result_serializer = 'json'
-accept_content = ['json']
-timezone = 'UTC'
-enable_utc = True
-task_track_started = True
-task_time_limit = 300
\ No newline at end of file
diff --git a/src/backend/config.py b/src/backend/config.py
index 68d0fd8..93590eb 100644
--- a/src/backend/config.py
+++ b/src/backend/config.py
@@ -1,17 +1,23 @@
-from pydantic_settings import BaseSettings
-from pydantic import Field
+import os
+from pydantic import BaseSettings
class Settings(BaseSettings):
- app_name: str = "Network Automation API"
- redis_url: str = Field("redis://localhost:6379", env="REDIS_URL")
- ai_api_key: str = Field(..., env="AI_API_KEY")
- max_connections: int = Field(50, env="MAX_CONNECTIONS")
- default_timeout: int = Field(30, env="DEFAULT_TIMEOUT")
+ APP_NAME: str = "AI Network Configurator"
+ DEBUG: bool = True
+ API_PREFIX: str = "/api"
+
+ # 硅基流动API配置
+ SILICONFLOW_API_KEY: str = os.getenv("SILICONFLOW_API_KEY", "")
+ SILICONFLOW_API_URL: str = os.getenv("SILICONFLOW_API_URL", "https://api.siliconflow.ai/v1")
+
+ # 交换机配置
+ SWITCH_USERNAME: str = os.getenv("SWITCH_USERNAME", "admin")
+ SWITCH_PASSWORD: str = os.getenv("SWITCH_PASSWORD", "admin")
+ SWITCH_TIMEOUT: int = os.getenv("SWITCH_TIMEOUT", 10)
class Config:
env_file = ".env"
- extra = "ignore"
settings = Settings()
\ No newline at end of file
diff --git a/src/backend/exceptions.py b/src/backend/exceptions.py
deleted file mode 100644
index a4d1b58..0000000
--- a/src/backend/exceptions.py
+++ /dev/null
@@ -1,11 +0,0 @@
-class InvalidInputError(Exception):
- """输入验证异常"""
- pass
-
-class NetworkDeviceError(Exception):
- """网络设备通信异常"""
- pass
-
-class AIServiceError(Exception):
- """AI服务异常"""
- pass
diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt
index 5122206..7f2f48d 100644
--- a/src/backend/requirements.txt
+++ b/src/backend/requirements.txt
@@ -1,8 +1,7 @@
-fastapi==0.109.1
-uvicorn==0.27.0
+fastapi==0.95.2
+uvicorn==0.22.0
python-dotenv==1.0.0
-celery==5.3.6
-redis==4.6.0
-netmiko==4.2.0
-asyncssh==2.14.10.0
-prometheus-client==0.2
\ No newline at end of file
+requests==2.28.2
+paramiko==3.1.0
+pydantic==1.10.7
+loguru==0.7.0
\ No newline at end of file
diff --git a/src/backend/run.py b/src/backend/run.py
index 3d64f2a..b1847f3 100644
--- a/src/backend/run.py
+++ b/src/backend/run.py
@@ -1,11 +1,13 @@
import uvicorn
-from app.main import app
+from src.backend.app import create_app
+
+app = create_app()
if __name__ == "__main__":
uvicorn.run(
- "app.main:app",
+ app,
host="0.0.0.0",
port=8000,
- reload=True,
- workers=1
+ log_level="info",
+ reload=True
)
\ No newline at end of file