Compare commits

..

5 Commits

Author SHA1 Message Date
a57e4d4b46 阿弥诺斯 2025-07-09 18:44:59 +08:00
3
17db64cd02 1 2025-07-09 18:31:15 +08:00
a11e14a367 Merge remote-tracking branch 'origin/main' 2025-07-09 16:48:53 +08:00
adce943152 修改ai模块,使用openai代替httpx 2025-07-09 16:48:44 +08:00
Jerry
7e89ad20fa 修改ai模块,使用openai代替httpx 2025-07-09 16:40:38 +08:00
7 changed files with 110 additions and 60 deletions

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.13" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (AI-powered-switches)" project-jdk-type="Python SDK" />
</project>

View File

@ -126,6 +126,40 @@ 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] # 前端生成的CLI命令列表
is_ensp: bool = False # 是否为eNSP模拟器模式
@router.post("/execute_cli_commands", response_model=dict)
async def execute_cli_commands(request: CLICommandRequest):
"""
执行前端生成的CLI命令
"""
try:
configurator = SwitchConfigurator(
username=settings.SWITCH_USERNAME,
password=settings.SWITCH_PASSWORD,
timeout=settings.SWITCH_TIMEOUT,
ensp_mode=request.is_ensp
)
# 直接发送命令到交换机
result = await configurator.execute_raw_commands(
ip=request.switch_ip,
commands=request.commands
)
return {
"success": True,
"output": result,
"mode": "eNSP" if request.is_ensp else "SSH"
}
except Exception as e:
raise HTTPException(500, detail=str(e))
@router.get("/traffic/interfaces", summary="获取所有网络接口")
async def get_network_interfaces():
return {

View File

@ -179,6 +179,13 @@ class SwitchConfigurator:
except Exception as e:
raise SSHConnectionException(f"连接异常: {str(e)}")
async def execute_raw_commands(self, ip: str, commands: List[str]) -> str:
"""
执行原始CLI命令
"""
return await self._send_commands(ip, commands)
@staticmethod
def _generate_standard_commands(config: SwitchConfig) -> List[str]:
"""生成标准CLI命令"""
@ -242,6 +249,7 @@ class SwitchConfigurator:
logging.error(f"恢复失败: {str(e)}")
return False
@retry(
stop=stop_after_attempt(2),
wait=wait_exponential(multiplier=1, min=4, max=10)

View File

@ -1,64 +1,72 @@
import json
from typing import Dict, Any, Coroutine
import httpx
from typing import Dict, Any
from openai import OpenAI
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.headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
self.client = OpenAI(
api_key=self.api_key,
base_url=self.api_url,
# timeout=httpx.Timeout(30.0)
)
async def parse_command(self, command: str) -> Dict[str, Any]:
async def parse_command(self, command: str) -> Any | None:
"""
调用硅基流动API解析中文命令
"""
prompt = f"""
你是一个网络设备配置专家请将以下中文命令转换为网络设备配置JSON
支持的配置包括VLAN端口路由ACL等
返回格式必须为JSON包含配置类型和详细参数
命令{command}
prompt = """
你是一个网络设备配置专家精通各种类型的路由器的配置,请将以下用户的中文命令转换为网络设备配置JSON
但是请注意由于贪婪的人们追求极高的效率所以你必须严格按照 JSON 格式返回数据不要包含任何额外文本或 Markdown 代码块
返回格式要求
1. 必须包含'type'字段指明配置类型(vlan/interface/acl/route等)
2. 必须包含'commands'字段包含可直接执行的命令列表
3. 其他参数根据配置类型动态添加
4. 不要包含解释性文本步骤说明或注释
示例命令'创建VLAN 100名称为TEST'
示例返回{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["vlan 100", "name TEST"]}
"""
data = {
"model": "deepseek-ai/DeepSeek-V3",
"prompt": prompt,
"max_tokens": 1000,
"temperature": 0.3
}
messages = [
ChatCompletionSystemMessageParam(role="system", content=prompt),
ChatCompletionUserMessageParam(role="user", content=command)
]
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.api_url}/chat/completions",
headers=self.headers,
json=data,
timeout=30
)
response = self.client.chat.completions.create(
model="deepseek-ai/DeepSeek-V3",
messages=messages,
temperature=0.3,
max_tokens=1000,
response_format={"type": "json_object"}
)
if response.status_code != 200:
raise SiliconFlowAPIException(response.text)
logger.debug(response)
result = response.json()
config_str = result["choices"][0]["text"].strip()
config_str = response.choices[0].message.content.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:
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")
except Exception as e:
raise SiliconFlowAPIException(
detail=f"API请求失败: {str(e)}",
status_code=e.response.status_code if hasattr(e, "response") else 500
status_code=getattr(e, "status_code", 500)
)

View File

@ -1,34 +1,26 @@
# 核心依赖 -i https://pypi.tuna.tsinghua.edu.cn/simple
fastapi==0.110.0
uvicorn==0.29.0
python-dotenv==1.0.1
# Pydantic 模型 -i https://pypi.tuna.tsinghua.edu.cn/simple
pysnmp
pydantic==2.6.4
pydantic-settings==2.2.1
# 网络操作 -i https://pypi.tuna.tsinghua.edu.cn/simple
openai==1.93.2
asyncssh==2.14.2
telnetlib3==2.0.3
httpx==0.27.0
python-nmap==0.7.1
# 异步文件操作 -i https://pypi.tuna.tsinghua.edu.cn/simple
pysnmp==7.1.21
aiofiles==23.2.1
# 日志管理 -i https://pypi.tuna.tsinghua.edu.cn/simple
loguru==0.7.2
# 重试机制 -i https://pypi.tuna.tsinghua.edu.cn/simple
tenacity==8.2.3
# 其他工具 -i https://pypi.tuna.tsinghua.edu.cn/simple
asyncio==3.4.3
typing_extensions==4.10.0
#监控依赖 Y
psutil==5.9.8
matplotlib==3.8.3
sqlalchemy==2.0.28
sqlalchemy
openai

View File

@ -96,7 +96,7 @@ const ConfigPage = () => {
};
const handleApply = async () => {
if (!editableConfig.trim()) {
if (!editableConfig) {
Notification.warn({
title: '配置为空',
description: '请先解析或编辑有效配置',
@ -213,13 +213,15 @@ const ConfigPage = () => {
生成配置:
</Text>
<Textarea
value={editableConfig}
value={JSON.stringify(editableConfig)}
rows={12}
onChange={(e) => setEditableConfig(e.target.value)}
fontFamily={'monospace'}
size={'sm'}
bg={'blackAlpha.200'}
whiteSpace="pre-wrap"
/>
<HStack mt={4} spacing={3} justify={'flex-end'}>
<Button
variant={'outline'}
@ -240,7 +242,7 @@ const ConfigPage = () => {
size={'sm'}
onClick={handleApply}
isLoading={applying}
isDisabled={!editableConfig.trim()}
isDisabled={!editableConfig}
>
应用到交换机
</Button>

View File

@ -41,7 +41,13 @@ export const api = {
* @param text 文本
* @returns {Promise<axios.AxiosResponse<any>>}
*/
parseCommand: (text) => axios.post(buildUrl('/api/parse_command'), { command: text }),
//parseCommand: async (text) => await axios.post(buildUrl('/api/parse_command'), { command: text }),
async parseCommand(text) {
const res = await axios.post(buildUrl('/api/parse_command', { command: text }));
if (res) {
return res;
} else return null;
},
/**
* 应用配置