回滚部分提交

This commit is contained in:
Jerry 2025-09-10 14:53:57 +08:00
parent 842e562b91
commit d11decae6a
6 changed files with 8600 additions and 5960 deletions

View File

@ -9,7 +9,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
.idea/misc.xml generated
View File

@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.13" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
</project>

View File

@ -4,13 +4,14 @@ from fastapi import (APIRouter, HTTPException, Response, WebSocket, WebSocketDis
from typing import List
from pydantic import BaseModel
import asyncio
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, JSONResponse
import matplotlib.pyplot as plt
import io
import base64
import psutil
import ipaddress
from ..models.requests import CLICommandRequest, ConfigRequest
from ..services.switch_traffic_monitor import get_switch_monitor
from ..utils import logger
from ...app.services.ai_services import AIService
@ -55,22 +56,6 @@ class BatchConfigRequest(BaseModel):
password: str = None
timeout: int = None
@router.post("/batch_apply_config")
async def batch_apply_config(request: BatchConfigRequest):
results = {}
for ip in request.switch_ips:
try:
configurator = SwitchConfigurator(
username=request.username,
password=request.password,
timeout=request.timeout)
results[ip] = await configurator.apply_config(ip, request.config)
except Exception as e:
results[ip] = str(e)
return {"results": results}
@router.get("/test")
async def test_endpoint():
return {"message": "Hello World"}
@ -96,27 +81,29 @@ async def list_devices():
}
class DeviceItem(BaseModel):
name: str
ip: str
vendor: str
class CommandRequest(BaseModel):
command: str
vendor: str = "huawei"
class ConfigRequest(BaseModel):
config: dict
switch_ip: str
username: str = None
password: str = None
timeout: int = None
vendor: str = "huawei"
devices: List[DeviceItem]
@router.post("/parse_command", response_model=dict)
async def parse_command(request: CommandRequest):
"""解析中文命令并返回JSON配置"""
"""解析中文命令并返回每台设备的配置 JSON"""
missing_vendor = [d for d in request.devices if not d.vendor or d.vendor.strip() == ""]
if missing_vendor:
names = ", ".join([d.name for d in missing_vendor])
raise HTTPException(
status_code=400,
detail=f"以下设备未配置厂商: {names}"
)
try:
ai_service = AIService(settings.SILICONFLOW_API_KEY, settings.SILICONFLOW_API_URL)
config = await ai_service.parse_command(request.command, request.vendor)
return {"success": True, "config": config}
config = await ai_service.parse_command(request.command, [d.dict() for d in request.devices])
return {"success": True, "config": config.get("results", [])}
except Exception as e:
raise HTTPException(
status_code=400,
@ -141,44 +128,16 @@ async def apply_config(request: ConfigRequest):
status_code=500,
detail=f"Failed to apply config: {str(e)}"
)
class CLICommandRequest(BaseModel):
switch_ip: str
commands: List[str]
is_ensp: bool = False
def extract_credentials(self) -> tuple:
"""从commands中提取用户名和密码"""
username = None
password = None
for cmd in self.commands:
if cmd.startswith("!username="):
username = cmd.split("=")[1]
elif cmd.startswith("!password="):
password = cmd.split("=")[1]
return username, password
def get_clean_commands(self) -> List[str]:
"""获取去除凭据后的实际命令"""
return [cmd for cmd in self.commands
if not (cmd.startswith("!username=") or cmd.startswith("!password="))]
@router.post("/execute_cli_commands", response_model=dict)
async def execute_cli_commands(request: CLICommandRequest):
"""执行前端生成的CLI命令"""
try:
username, password = request.extract_credentials()
clean_commands = request.get_clean_commands()
configurator = SwitchConfigurator(
username=username,
password=password,
timeout=settings.SWITCH_TIMEOUT,
ensp_mode=request.is_ensp
)
result = await configurator.execute_raw_commands(
@ -188,7 +147,6 @@ async def execute_cli_commands(request: CLICommandRequest):
return {
"success": True,
"output": result,
"mode": "eNSP" if request.is_ensp else "SSH"
}
except Exception as e:
raise HTTPException(500, detail=str(e))
@ -444,4 +402,4 @@ async def get_network_adapters():
networks = await asyncio.to_thread(sync_get_adapters)
return {"networks": networks}
except Exception as e:
return {"error": f"获取网络适配器信息失败: {str(e)}"}
return {"error": f"获取网络适配器信息失败: {str(e)}"}

View File

@ -1,56 +1,47 @@
from typing import Dict, Any, Coroutine
import httpx
from typing import Any, List, Dict
from openai import AsyncOpenAI
import json
from src.backend.app.utils.exceptions import SiliconFlowAPIException
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from src.backend.app.utils.logger import logger
class AIService:
def __init__(self, api_key: str, api_url: str):
self.api_key = api_key
self.api_url = api_url
self.client = AsyncOpenAI(
api_key=self.api_key,
base_url=self.api_url,
timeout=httpx.Timeout(30.0)
)
self.client = AsyncOpenAI(api_key=api_key, base_url=api_url)
async def parse_command(self, command: str, vendor: str = "huawei") -> Any | None:
async def parse_command(self, command: str, devices: List[Dict]) -> Dict[str, Any]:
"""
调用硅基流动API解析中文命令
针对一组设备和一条自然语言命令生成每台设备的配置 JSON
"""
vendor_prompts = {
"huawei": "华为交换机配置命令",
"cisco": "思科交换机配置命令",
"h3c": "H3C交换机配置命令",
"ruijie": "锐捷交换机配置命令",
"zte": "中兴交换机配置命令"
}
devices_str = json.dumps(devices, ensure_ascii=False, indent=2)
example = """[{"device": {"name": "sw1","ip": "192.168.1.10","vendor": "huawei","username": "NONE", "password": "Huawei"},"config": {"type": "vlan","vlan_id": 300,"name": "Sales","commands": ["system-view","vlan 300","name Sales","quit","quit","save","Y"]}}]"""
prompt = f"""
你是一个网络设备配置专家精通各种类型的路由器的配置请将以下用户的中文命令转换为{vendor_prompts.get(vendor, '网络设备')}配置JSON
但是请注意由于贪婪的人们追求极高的效率所以你必须严格按照 JSON 格式返回数据不要包含任何额外文本或 Markdown 代码块
返回格式要求
1. 必须包含'type'字段指明配置类型(vlan/interface/acl/route等)
2. 必须包含'commands'字段包含可直接执行的命令列表
3. 其他参数根据配置类型动态添加
4. 不要包含解释性文本步骤说明或注释
5. 要包含使用ssh连接交换机后的完整命令包括但不完全包括system-view退出保存等完整操作注意保存还需要输入Y
你是一个网络设备配置专家现在有以下设备
{devices_str}
根据厂商{vendor}的不同命令格式如下
- 华为: system-view quit save Y
- 思科: enable configure terminal exit write memory
- H3C: system-view quit save
- 锐捷: enable configure terminal exit write
- 中兴: enable configure terminal exit write memory
用户输入了一条命令{command}
示例命令'创建VLAN 100名称为TEST'
华为示例返回{{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["system-view","vlan 100", "name TEST","quit","quit","save","Y"]}}
思科示例返回{{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["enable","configure terminal","vlan 100", "name TEST","exit","exit","write memory"]}}
"""
你的任务
- 为每台设备分别生成配置
- 输出一个 JSON 数组每个元素对应一台设备
- 每个对象必须包含:
- device: 原始设备信息 (name, ip, vendor,username,password)
- config: 配置详情
- type: 配置类型 ( vlan/interface/acl/route)
- commands: 可直接执行的命令数组 (必须包含进入配置退出保存命令)
- 其他字段: 根据配置类型动态添加
- 严格返回 JSON不要包含解释说明或 markdown
各厂商保存命令规则
- 华为: system-view quit save Y
- 思科: enable configure terminal exit write memory
- H3C: system-view quit save
- 锐捷: enable configure terminal exit write
- 中兴: enable configure terminal exit write memory
返回示例仅作为格式参考不要照抄 VLAN ID 和命令内容请根据实际命令生成{example}
"""
messages = [
ChatCompletionSystemMessageParam(role="system", content=prompt),
@ -61,29 +52,18 @@ class AIService:
response = await self.client.chat.completions.create(
model="deepseek-ai/DeepSeek-V3",
messages=messages,
temperature=0.3,
max_tokens=1000,
temperature=0.2,
max_tokens=1500,
response_format={"type": "json_object"}
)
logger.debug(response)
config_str = response.choices[0].message.content.strip()
configs = json.loads(config_str)
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 KeyError:
logger.error(KeyError)
raise SiliconFlowAPIException("errrrrrrro")
return {"success": True, "results": configs}
except Exception as e:
raise SiliconFlowAPIException(
detail=f"API请求失败: {str(e)}",
detail=f"AI 解析配置失败: {str(e)}",
status_code=getattr(e, "status_code", 500)
)

14385
src/frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -90,6 +90,7 @@ const ConfigPage = () => {
let result = await resultWrapper.unwrap();
if (result?.data?.config) {
const configMap = {};
console.log(JSON.stringify(result.data.config)); //{}
result.data.config.forEach((item) => {
if (item.device?.ip) {
configMap[item.device.ip] = item;