diff --git a/servers/patch_analyzer_mcp/README.md b/servers/patch_analyzer_mcp/README.md new file mode 100644 index 0000000000000000000000000000000000000000..24d578829413e202287ee6b8ab8f9da1e57a5770 --- /dev/null +++ b/servers/patch_analyzer_mcp/README.md @@ -0,0 +1,92 @@ +# patch-analyzer-mcp + +#### 介绍 +补丁回合分析智能体,具有自动拉取上游社区代码生成补丁,分析补丁内容生成excel文件,读取审核后的excel文件按照模块粒度提交到目的仓并创建网页MR合并请求。 + +#### 限制 +目前只支持跟踪一个上游社区代码仓,不支持同时跟踪多个上游仓 + +#### 软件架构 +依赖python的fastmcp,gitpython包 +要求python>=3.10 + + +#### 安装教程 + +1. wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh 下载conda管理工具 +2. conda create --name mcp-server python=3.10 +3. 进入conda环境后安装依赖 pip install fastmcp, gitpython +4. xxxx +5. xxxx + +#### 使用说明 + +mcp server服务端使用方式: +1. 修改src/assistant.conf文件 + +| 参数 | 配置说明 | +|----------------------|----------------------------------------------------| +| kernel_src_url | "kernel"软件的上游代码仓地址 | +| kernel_src_url_proxy | "kernel"软件的上游代码仓代理,影响excel的commit_id列| +| kernel_commitID | "kernel"软件的上游代码仓从指定commitID处开始分析 | +| kernel_src_branch | "kernel"软件的上游代码仓的分支 | +| kernel_dst_branch | "kernel"软件的目的代码仓的分支 | +| kernel_dst_url | "kernel"软件的上游代码仓地址 | +| kernel_project_url | 创建web MR请求的发送地址 | +| kernel_project_token | 创建web MR请求的密钥 | | +| kernel_mr_server | 创建web MR的服务器名(目前支持sangfor和gitee格式的MR)| + +特殊说明:如果要增加openssl软件的配置,复制新增上述字段并将替换"kernel"字段为"openssl"即可,如: +kernel_src_url -》 openssl_src_url +kernel_src_url_proxy -》 openssl_src_url_proxy +依次类推 + +2. python3 patch_assistant 拉起mcp server服务即可,默认会监听0.0.0.0:8100端口 + +--------------------------------------------------------------- + **mcp server客户端使用方式:** + +独立使用agent client客户端使用方式: +1. 修改src/assistant.conf文件 + +| 参数 | 配置说明 | +|----------------------|------------------------------------------------| +| api_key | llm api_key,若无填“EMPTY” | +| base_url | llm base_url | +| model_name | llm 模型名 | +| temperature | llm 模型温度,调节llm输出稳定性和创造性,0.1~0.3输出结果更加稳定 | +| top_p | llm top_p,调节llm输出稳定性,0.7~0.9输出结果更加稳定 | +| mcp_server_ip | mcp server服务端ip | +| mcp_server_port | mcp server服务端port | +| patch_excel_gen_path | 补丁分析结果excel生成的路径,文件名会是 软件-时间戳.xlsx | +| patch_excel_path | 补丁回合导入的excel文件完整路径信息 | +| sse_read_timeout | sse服务端无数据响应超时时间,单位:s,根据服务端最长同步代码块运行时长设置,可以适当调大 | + +按照本地llm模型或者线上llm模型设置模型相关参数,其他按照本地mcp server服务端,本地磁盘路径设置; +2. python client/mcp_client.py 拉起进程,根据程序提示交互输入执行的执行,agent按照software_list提供的列表提示选择软件进行操作,支持 **“软件补丁分析”、“补丁回合”** 两个指令; +3. 完成“软件补丁分析”后,将输出excel供架构师人工审核,需要回填excel中“确认合入”(仅可填 “ **是/否** ” )、“确认理由”,此两项为必填项,若空缺将导致补丁回合步骤失败; +4. 执行“补丁回合”前需要上传评审后的excel文件,按照步骤2执行,等待agent返回结果,若出现补丁冲突问题,需要人工检视修复; + + +客户端接入roo code使用方式: +1. 同样的方式修改client/client.conf文件; +2. python3 patch_assistant.py 拉起mcp server服务即可,默认会监听0.0.0.0:8100端口 +2. 配置client/mcp_config.json,在roo code里监听客户端8100端口; + +``` +{ + "mcpServers": { #标签名,固定 + "patch_analyse": { # mcp server名称 + "type": "streamable-http", # 连接mcp方式,固定 + "url": "http://0.0.0.0:8100/mcp", #mcp 客户端 url + "disabled": false, + "timeout": 3600, # 超时时间 + "alwaysAllow": [ + ] # 常开的接口,置空就可以 + } + } +} +``` + +3. 配置roo code中自定义prompt,看情况根据llm效果来; +4. roo code中进行问答; diff --git a/servers/patch_analyzer_mcp/mcp-rpm.yaml b/servers/patch_analyzer_mcp/mcp-rpm.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3f907f0699ebfed63027354162e62874dc33295c --- /dev/null +++ b/servers/patch_analyzer_mcp/mcp-rpm.yaml @@ -0,0 +1,22 @@ +name: "patch_analyzer_mcp" +summary: "Package dependency analyzer for RPM/DNF/PIP" +description: | + A MCP server that provides tools to analyze package dependencies + for RPM, DNF and PIP packages, including dependency tree generation. + +dependencies: + system: + - python3 + - rpm + - dnf + - python3-pip + packages: + - fastmcp + - gitpython + +files: + required: + - mcp_config.json + - src/patch_assistant.py + optional: + - src/requirements.txt \ No newline at end of file diff --git a/servers/patch_analyzer_mcp/mcp_config.json b/servers/patch_analyzer_mcp/mcp_config.json new file mode 100644 index 0000000000000000000000000000000000000000..2719a10c12f3c08bf80ff7c1eda582d950400a65 --- /dev/null +++ b/servers/patch_analyzer_mcp/mcp_config.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "patch_analyse": { + "type": "streamable-http", + "url": "http://0.0.0.0:8100/mcp", + "disabled": false, + "timeout": 3600, + "alwaysAllow": [ + ] + } + } +} diff --git a/servers/patch_analyzer_mcp/src/assistant.conf b/servers/patch_analyzer_mcp/src/assistant.conf new file mode 100644 index 0000000000000000000000000000000000000000..a5b326e7e98823448b6950e6e5a56bb4369c2ac9 --- /dev/null +++ b/servers/patch_analyzer_mcp/src/assistant.conf @@ -0,0 +1,40 @@ +# tools配置 +kernel_src_url=https://gitee.com/openeuler/kernel.git + +kernel_src_url_proxy=https://proxy/openeuler/kernel.git + +kernel_commitID=860874bd1cca5142eb23d123a0aeac7ec9d73d75 + +kernel_src_branch=openEuler-25.03 + +kernel_dst_branch=personal/openEuler-25.03 + +kernel_dst_url=git@gitee.com:pandongang/kernel.git + +kernel_project_url=https://gitee.com/api/v5/repos/pandongang/kernel/pulls + +kernel_project_token=99ca60a1a66d62067838e70b5295bdac + +kernel_mr_server=sangfor + +#llm参数 +api_key=sk-xxx +base_url=https://dashscope.aliyuncs.com/compatible-mode/v1 +model_name=qwen3-coder-plus-2025-07-22 +temperature=0.2 +top_p=0.8 + +#excel信息 +#补丁分析excel输出路径 +patch_excel_gen_path=/tmp +#补丁回合excel导入路径 +patch_excel_path=/tmp/kernel-20250912120845.xlsx + +#httpx sse服务端无数据响应超时时间,单位:s +sse_read_timeout=1800.0 + +#支持通过agent执行的软件列表,分号分隔 +software_list=kernel;qemu + +#代码分析回合规则 +judge_rules="1、是否是安全性修复,如果补丁修复了安全漏洞,通常需要回合,例如CVE漏洞、权限绕过、远程代码执行;2、严重缺陷修复,如果解决了系统崩溃、数据丢失、功能不可用的bug修复,通常需要回合;3、功能性增强或新特性,一般不回合;4、性能优化,一般不回合;5、本补丁修改自身不满足1、2回合条件,但是属于一个整体系列补丁中一环,暂时评估为需要回合,由人工最终评审,但是需要在判断理由中强调原因;6、如果存在前置补丁或后置补丁依赖,需要列出对应依赖补丁commit id" diff --git a/servers/patch_analyzer_mcp/src/patch_assistant.py b/servers/patch_analyzer_mcp/src/patch_assistant.py new file mode 100644 index 0000000000000000000000000000000000000000..282be0bdd068ebe3696a70b6936457c1d5944a2c --- /dev/null +++ b/servers/patch_analyzer_mcp/src/patch_assistant.py @@ -0,0 +1,488 @@ +from datetime import datetime +import asyncio +import contextvars +import json +import os +import threading +import re +from typing import Any, Dict, List, Optional + +from fastmcp import Client +from fastmcp.client.transports import StreamableHttpTransport +from mcp.server.fastmcp import FastMCP +from openai import OpenAI +from openai import BadRequestError +import openai +import pandas as pd +from pydantic import BaseModel, Field +import patch_tools + +# 创建FastMCP实例,http方式 +mcp = FastMCP("生成补丁、解析补丁、回合补丁", host="0.0.0.0", port=8100) + +#client_config = contextvars.ContextVar("client_config") +client_config = {} +config_lock = threading.Lock() +# 配置OpenAI客户端 +# openai v1.100.1 +client = OpenAI( + api_key="xxxxx", + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" +) + +# 工具调用参数模型 +class ToolCall(BaseModel): + name: str = Field(..., description="要调用的工具名称") + parameters: Dict[str, Any] = Field(..., description="工具所需的参数") + +# 工具调用响应模型 +class ToolResponse(BaseModel): + name: str = Field(..., description="被调用的工具名称") + result: Any = Field(..., description="工具调用结果") + error: Optional[str] = Field(None, description="工具调用错误信息,如果有的话") + +def check_json_output(output: str) -> str: + """判断llm是否包含标签,检查json格式完整性""" + cleaned_output = re.sub(r".*?", "", output, flags=re.DOTALL) + cleaned_output = cleaned_output.lstrip() # 去除多余段首空白 + + # 检查剩余内容是否为JSON格式 + try: + llm_data = json.loads(cleaned_output) + return llm_data + except json.JSONDecodeError: + raise json.JSONDecodeError(f"数据不满足json格式", cleaned_output, 0) + + +def estimate_tokens(text: str) -> int: + """ + 粗略估算长文本转换token后长度,中文按照1token平均1.5字符,英文1token平均4字符 + """ + char_count = len(text) + return int(char_count / 4) + + +def gen_patch_content(software_name: str, system_prompt: str, query: str, commit_id: str) -> List: + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": query} + ] + + patch_count = tools.get_patch_count(software_name, commit_id) + print(f"补丁总数: {patch_count},开始逐条分析") + + patch_list = [] + # 最多进行n轮工具调用 + for idx in range(1, patch_count + 1): + patch_content = tools.read_patch(software_name, idx) + content = patch_content.get("提交描述", None) + diff = patch_content.get("差异", None) + query_new = query[:] + query_new += json.dumps(content, ensure_ascii=False, indent=2) + diff_token = estimate_tokens(json.dumps(diff, ensure_ascii=False, indent=2)) + if diff_token < 8192: + query_new += json.dumps(diff, ensure_ascii=False, indent=2) + + #llm结果不满足预期将重试三次 + max_retries = 3 + retry_count = 0 + terminated = False + while retry_count < max_retries and not terminated: + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": query_new} + ] + + try: + # 调用OpenAI模型 + response = client.chat.completions.create( + model=client_config.get("model_name"), + messages=messages, + temperature=float(client_config.get("temperature")), + top_p=float(client_config.get("top_p")), + ) + response_message = response.choices[0].message.content + except BadRequestError as e: + print(f"{idx}/{patch_count} llm分析报错: {e},将跳过继续下一条") + final_content = {} + final_content.update(patch_content) + patch_list.append(final_content) + break + + try: + llm_data = check_json_output(response_message) + final_content = {} + final_content.update(llm_data) + final_content.update(patch_content) + patch_list.append(final_content) + print(f"分析{idx}/{patch_count}已完成") + terminated = True + except json.JSONDecodeError: + retry_count += 1 + print(f"{idx}/{patch_count}分析输出格式不正确,正在进行第{retry_count}次重试...") + + query_new += f",你输出的内容是:{response_message},格式不符合要求。\n" \ + f"请严格按照要求格式重新生成,确保输出是有效的纯JSON内容。" + + if not terminated: + print(f"{idx}/{patch_count}经过{max_retries}次重试后仍无法解析,跳过这条数据分析") + patch_list.append(patch_content) + + return patch_list + + +def process_with_flow(software_name: str, patch_content: List[Dict]) -> str: + """补丁内容超长时由工作流驱动触发补丁回合动作""" + try: + result = tools.apply_patch(software_name, patch_content) + except Exception as e: + result = f"处理补丁时发生错误:{str(e)}" + + return result + + +def excel_to_json(excel_path: str, sheet_name: str = 0) -> List[Dict]: + """ + 将Excel文件转换为JSON格式(每行作为标题,每列作为数据条目) + + 参数: + excel_path: Excel文件路径 + sheet_name: 工作表名称或索引(默认0,即第一个工作表) + + 返回: + 转换后的JSON数据列表 + """ + try: + # 读取Excel文件,第一行为标题行 + df = pd.read_excel( + excel_path, + sheet_name=sheet_name, + header=0, + index_col=False + ) + + # 检查数据是否为空 + if df.empty: + raise ValueError("Excel文件中没有有效数据") + + # 转换为JSON格式列表(每条记录对应一行数据) + json_data = df.to_dict(orient="records") + # 删除'提交描述'和'差异'字段,占用token影响上下文长度 + fields_to_remove = ['提交描述', '差异'] + for item in json_data: + for field in fields_to_remove: + # 若字段存在则删除,避免KeyError + if field in item: + del item[field] + return json_data + + except FileNotFoundError: + raise FileNotFoundError(f"未找到Excel文件: {excel_path}") + except Exception as e: + raise Exception(f"转换失败: {str(e)}") + + +def load_config(config_filename: str = "assistant.conf"): + """ + 读取配置文件 + 参数: + config_filename: 配置文件名(默认"config.txt") + 返回: + 配置字典(key: 配置项名, value: 配置值(字符串类型)) + """ + script_path = os.path.abspath(__file__) + script_dir = os.path.dirname(script_path) + config_file_path = os.path.join(script_dir, config_filename) + try: + with open(config_file_path, "r", encoding="utf-8") as f: + for line_num, line in enumerate(f, start=1): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + print(f"警告:配置文件第{line_num}行格式错误(缺少'='),已跳过该行:{line}") + continue + key, value = line.split("=", 1) + client_config[key.strip()] = value.strip() + + global client + client = OpenAI( + api_key=client_config.get("api_key"), + base_url=client_config.get("base_url") + ) + return + + except FileNotFoundError: + raise FileNotFoundError(f"未找到配置文件:{config_file_path}") + except Exception as e: + raise Exception(f"读取配置文件失败:{str(e)}") + + +@mcp.tool() +def set_client_config(config_key: str, config_value: str): + """ + 设定指定配置参数的值,可以设置例如 + patch_excel_gen_path:补丁分析结果excel文件输出的根路径; + judge_rules:评审补丁回合规则 + 参数: + config_key: 键(必须为字符串) + config_value: 值(支持任意类型) + 返回: 设置成功返回 True + """ + if not isinstance(config_key, str): + print(f"键必须是字符串类型,当前传入 {type(config_key)}") + return False + with config_lock: + client_config[config_key] = config_value + return True + + +@mcp.tool() +def get_client_config(config_key: str): + """ + 获取系统预设或者用户更新后指定配置参数的值,可以获取到例如 + patch_excel_gen_path:补丁分析结果excel文件输出的根路径; + judge_rules:评审补丁回合规则 + 参数: + config_key: 要查询配置的键(必须为字符串) + 返回: 键对应的值,如果键类型不正确或者找不到键,返回None + """ + if not isinstance(config_key, str): + print(f"键必须是字符串类型,当前传入 {type(config_key)}") + return None + with config_lock: + return client_config.get(config_key) + + +@mcp.tool() +def analyse_software_patch(software_name: str, commit_id: str) -> str: + """ + 通过用户输入的软件名、commit_id分析软件历史补丁详细内容,结果生成excel分析文档 + + 规则: + 需要提前设置的变量-> patch_excel_gen_path补丁分析结果excel文件输出的根路径; + + 参数: + software_name: 需要分析的软件名 + commit_id: 需要分析的软件起始补丁对应的提交信息commit id + + 返回: + 分析结果 + """ + + print("收到用户输入,接下来执行软件漏洞补丁分析...") + system_prompt = f'''你是一个AI辅助补丁分析工具,可以结合提供的工具来帮助回答用户的问题。 + 请根据问题判断是否需要调用工具,如果需要,请选择合适的工具并正确指定参数。 + 工具调用结果返回后,请结合结果逐项分析给出最终回答,分析结果按照要求返回,所有的结果都是有用的,请不要遗漏。 + 如果不需要调用工具,可以直接回答问题。 + 你需要遵守以下规则: + 1、分析结果需要完整,不可以遗漏。 + 2、你给出的最终结果将按照纯json格式被解析,返回结果请严格按照以下json格式,不要包含任何格式(例如MarkDown```json),也不要添加解释。 + 以下是补丁分析举例: + 补丁分析输入:"From 860874bd1cca5142eb23d123a0aeac7ec9d73d75 Mon Sep 17 00:00:00 2001\nFrom: zhaolichang \n + Date: Fri, 21 Mar 2025 01:08:48 +0800\nSubject: [PATCH 1/4] PINCTRL: Fix the issue that CONFIG_PINCTRL_AMD do not\n + support m option\n\nhuawei inclusion\ncategory: bugfix\nbugzilla: https://gitee.com/src-openeuler/calamares/issues/IBS0LG\n + CVE: NA\n\n--------------------------------------\n\nFix the issue that the CONFIG_PINCTRL_ADM configuration do not\n + support the 'm' (module) option, and change it to 'y' (built-in).\n\nFixes: cbba3eb02aa9 ("PINCTRL:ENABLE_CONFIG_PINCTRL_AMD")\n + Signed-off-by: zhaolichang \n---\n arch/arm64/configs/openeuler_defconfig | 2 +-\n + arch/x86/configs/openeuler_defconfig | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\n + diff --git a/arch/arm64/configs/openeuler_defconfig b/arch/arm64/configs/openeuler_defconfig\n + index 531f5f04d8e8..39729694001b 100644\n--- a/arch/arm64/configs/openeuler_defconfig\n+++ b/arch/arm64/configs/openeuler_defconfig\n + @@ -3945,7 +3945,7 @@ CONFIG_PINMUX=y\n CONFIG_PINCONF=y\n CONFIG_GENERIC_PINCONF=y\n # CONFIG_DEBUG_PINCTRL is not set\n + -CONFIG_PINCTRL_AMD=m\n+CONFIG_PINCTRL_AMD=y\n # CONFIG_PINCTRL_CY8C95X0 is not set\n # CONFIG_PINCTRL_MCP23S08 is not set\n + # CONFIG_PINCTRL_MICROCHIP_SGPIO is not set\ndiff --git a/arch/x86/configs/openeuler_defconfig b/arch/x86/configs/openeuler_defconfig\n + index 7f37a9e8b75b..6ece34678378 100644\n--- a/arch/x86/configs/openeuler_defconfig\n+++ b/arch/x86/configs/openeuler_defconfig\n + @@ -4157,7 +4157,7 @@ CONFIG_PINMUX=y\n CONFIG_PINCONF=y\n CONFIG_GENERIC_PINCONF=y\n # CONFIG_DEBUG_PINCTRL is not set\n + -CONFIG_PINCTRL_AMD=m\n+CONFIG_PINCTRL_AMD=y\n # CONFIG_PINCTRL_CY8C95X0 is not set\n # CONFIG_PINCTRL_MCP23S08 is not set\n + # CONFIG_PINCTRL_SX150X is not set\n--\n2.43.0" + 结果输出格式为: + {{ + "提交信息": "860874bd1cca5142eb23d123a0aeac7ec9d73d75", + "提交时间": "Fri, 21 Mar 2025 01:08:48 +0800", + "模块": "PINCTRL", + "改动说明": "修复 CONFIG_PINCTRL_AMD 配置不支持模块('m')选项的问题,将其修改为内置('y')方式", + "判断理由": "xxxx", + "合入策略": "是", + "确认合入": "", + "确认理由": "", + "提交标题": "PINCTRL: Fix the issue that CONFIG_PINCTRL_AMD do not support m option", + "patch名": "补丁的文件名", + "补丁类型": "bugfix" + }} + 参数解释: + 提交信息--补丁信息中对应的补丁编号; + 提交时间--commit修改时间; + 模块--补丁修改涉及的文件所属模块,通常在提交标题中会显示; + 改动说明--补丁信息中对补丁修改点的描述,使用中文生成回答,计算机相关专有名词使用英文表述; + 判断理由--基于以下几点分析:{client_config.get('judge_rules')}; + 合入策略--是/否,根据“判断理由”设置是否需要回合; + 确认合入--填空,由用户回填; + 确认理由--填空,由用户回填; + 提交标题--commit的标题,从补丁信息中提取,格式为“模块:修改内容”; + patch名--补丁的文件名; + 补丁类型--根据合入策略分析结果代码的改动属于哪一类型; + ''' + + user_query = "帮我分析" + software_name + "的补丁,最终结果以json格式输出,补丁内容如下: " + try: + result = gen_patch_content(software_name, system_prompt, user_query, commit_id) + df = pd.DataFrame(result) + now = datetime.now() + output_file = client_config.get("patch_excel_gen_path") + "/" + software_name + "-" + now.strftime("%Y%m%d%H%M%S") + ".xlsx" + df.to_excel(output_file, index=False) + result = f"补丁分析文件已生成到:{output_file}" + except Exception as e: + result = f"处理补丁时发生错误:{str(e)}" + + return result + + +@mcp.tool() +def apply_software_patch(software_name: str, patch_excel_path: str) -> str: + """ + 通过用户输入的软件名、excel评审文档路径,解析excel,回合软件补丁 + + 参数: + software_name: 需要分析的软件名 + commit_id: 软件补丁对应的提交信息commit id + + 返回: + 分析结果,类型为字符串 + """ + + print("收到用户输入,接下来执行软件patch回合...") + patch_list = excel_to_json(patch_excel_path) + filter_list = [item for item in patch_list if item.get("确认合入") == "是"] + if not filter_list: + return f"{patch_excel_path}文件导入后'确认合入'的补丁为空,请确认" + result = process_with_flow(software_name, filter_list) + + final_result = f"补丁回合处理结果:{result}" + return final_result + + +def get_patch_name_by_commit_id(json_data, commit_id) -> str: + """ + 从excel数据中根据commit id查找对应的patch name值,并处理文件名格式 + """ + for item in json_data: + if '提交信息' in item and 'patch名' in item: + if item['提交信息'] == commit_id: + patch_name = item['patch名'] + # 按第一个"-"进行截断 + if '-' in patch_name: + prefix_part = patch_name.split('-', 1)[0] + return prefix_part + + # 未找到匹配项时返回空字符串 + return "" + + +@mcp.tool() +def re_analyse_patch(software_name: str, commit_id: str, patch_excel_path: str) -> str: + """ + 通过用户输入的软件名、excel评审文档路径,指定的commit_id按照新规则重新分析补丁 + + 规则: + 需要提前设置的变量-> judge_rules: 用户自定义的补丁评审规则 + + 参数: + software_name: 需要分析的软件名 + commit_id: 软件补丁对应的提交信息commit id + patch_excel_path: 上一次完整补丁分析报告输出路径 + + 返回: + 分析结果,类型为字符串 + """ + print("收到用户输入,接下来执行单条补丁重分析...") + patch_list = excel_to_json(patch_excel_path) + patch_idx = get_patch_name_by_commit_id(patch_list, commit_id) + if patch_idx == "": + return f"分析原文件{patch_excel_path}中找不到{commit_id}数据,请检查" + system_prompt = f'''你是一个AI辅助补丁分析工具,可以结合提供的工具来帮助回答用户的问题。 + 请根据问题判断是否需要调用工具,如果需要,请选择合适的工具并正确指定参数。 + 工具调用结果返回后,请结合结果逐项分析给出最终回答,分析结果按照要求返回,所有的结果都是有用的,请不要遗漏。 + 如果不需要调用工具,可以直接回答问题。 + 你需要遵守以下规则: + 1、分析结果需要完整,不可以遗漏。 + 2、你给出的最终结果将按照纯json格式被解析,返回结果请严格按照以下json格式,不要包含任何格式(例如MarkDown```json),也不要添加解释。 + 以下是补丁分析举例: + 补丁分析输入:"From 860874bd1cca5142eb23d123a0aeac7ec9d73d75 Mon Sep 17 00:00:00 2001\nFrom: zhaolichang \n + Date: Fri, 21 Mar 2025 01:08:48 +0800\nSubject: [PATCH 1/4] PINCTRL: Fix the issue that CONFIG_PINCTRL_AMD do not\n + support m option\n\nhuawei inclusion\ncategory: bugfix\nbugzilla: https://gitee.com/src-openeuler/calamares/issues/IBS0LG\n + CVE: NA\n\n--------------------------------------\n\nFix the issue that the CONFIG_PINCTRL_ADM configuration do not\n + support the 'm' (module) option, and change it to 'y' (built-in).\n\nFixes: cbba3eb02aa9 ("PINCTRL:ENABLE_CONFIG_PINCTRL_AMD")\n + Signed-off-by: zhaolichang \n---\n arch/arm64/configs/openeuler_defconfig | 2 +-\n + arch/x86/configs/openeuler_defconfig | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\n + diff --git a/arch/arm64/configs/openeuler_defconfig b/arch/arm64/configs/openeuler_defconfig\n + index 531f5f04d8e8..39729694001b 100644\n--- a/arch/arm64/configs/openeuler_defconfig\n+++ b/arch/arm64/configs/openeuler_defconfig\n + @@ -3945,7 +3945,7 @@ CONFIG_PINMUX=y\n CONFIG_PINCONF=y\n CONFIG_GENERIC_PINCONF=y\n # CONFIG_DEBUG_PINCTRL is not set\n + -CONFIG_PINCTRL_AMD=m\n+CONFIG_PINCTRL_AMD=y\n # CONFIG_PINCTRL_CY8C95X0 is not set\n # CONFIG_PINCTRL_MCP23S08 is not set\n + # CONFIG_PINCTRL_MICROCHIP_SGPIO is not set\ndiff --git a/arch/x86/configs/openeuler_defconfig b/arch/x86/configs/openeuler_defconfig\n + index 7f37a9e8b75b..6ece34678378 100644\n--- a/arch/x86/configs/openeuler_defconfig\n+++ b/arch/x86/configs/openeuler_defconfig\n + @@ -4157,7 +4157,7 @@ CONFIG_PINMUX=y\n CONFIG_PINCONF=y\n CONFIG_GENERIC_PINCONF=y\n # CONFIG_DEBUG_PINCTRL is not set\n + -CONFIG_PINCTRL_AMD=m\n+CONFIG_PINCTRL_AMD=y\n # CONFIG_PINCTRL_CY8C95X0 is not set\n # CONFIG_PINCTRL_MCP23S08 is not set\n + # CONFIG_PINCTRL_SX150X is not set\n--\n2.43.0" + 结果输出格式为: + {{ + "提交信息": "860874bd1cca5142eb23d123a0aeac7ec9d73d75", + "模块": "PINCTRL", + "改动说明": "修复 CONFIG_PINCTRL_AMD 配置不支持模块('m')选项的问题,将其修改为内置('y')方式", + "判断理由": "xxxx", + "合入策略": "是" + }} + 参数解释: + 提交信息--补丁信息中对应的补丁编号; + 改动说明--补丁信息中对补丁修改点的描述,使用中文生成回答,计算机相关专有名词使用英文表述; + 判断理由--基于以下几点分析:{client_config.get("judge_rules")}; + 合入策略--是/否,根据“判断理由”设置是否需要回合; + ''' + + user_query = "帮我分析" + software_name + "的补丁,最终结果以json格式输出,补丁内容如下: " + try: + patch_content = tools.read_patch(software_name, int(patch_idx)) + print(f"patch_content: {patch_content}") + content = patch_content.pop("content", None) + user_query += json.dumps(content, ensure_ascii=False, indent=2) + diff = patch_content.get("差异", None) + diff_token = estimate_tokens(json.dumps(diff, ensure_ascii=False, indent=2)) + if diff_token < 8192: + user_query += json.dumps(diff, ensure_ascii=False, indent=2) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_query} + ] + + # 调用OpenAI模型 + response = client.chat.completions.create( + model=client_config.get("model_name"), + messages=messages, + temperature=float(client_config.get("temperature")), + top_p=float(client_config.get("top_p")), + ) + response_message = response.choices[0].message.content + result = response_message + except Exception as e: + result = f"处理补丁分析时发生错误:{str(e)}" + + return result + +def test(): + res = analyse_software_patch("kernel", "cbba3eb02aa940563f9c19aa5c27d19bdca3a194") + print(f"补丁分析结果: {res}") + bool_res = set_client_config("judge_rules", "仅cve漏洞需要回合") + if bool_res: + print("设置judge_rules成功") + res = get_client_config("judge_rules") + print(f"judge_rules {res}") + res = re_analyse_patch("kernel", "860874bd1cca5142eb23d123a0aeac7ec9d73d75", "/tmp/kernel-20250916203141.xlsx") + print(f"补丁重分析结果: {res}") + res = apply_software_patch("kernel", "/tmp/kernel-20250916203141.xlsx") + print(f"补丁回合结果: {res}") + + +if __name__ == "__main__": + load_config() + mcp.run("streamable-http") + #test() # 运行单元自测函数 diff --git a/servers/patch_analyzer_mcp/src/patch_tools.py b/servers/patch_analyzer_mcp/src/patch_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..b1b191635449dddacc5841bee1045fdb45432e61 --- /dev/null +++ b/servers/patch_analyzer_mcp/src/patch_tools.py @@ -0,0 +1,586 @@ +import os +import glob +import re +import subprocess +import json +import requests +from typing import Optional +from typing import Dict +from git import Repo +from pathlib import Path + +last_patch_number = 0 +last_commit_id = "" + +def parse_config_line(line: str) -> Optional[tuple[str, str]]: + """解析配置文件中的一行,提取键值对""" + line = line.strip() + if not line or line.startswith('#'): # 跳过空行和注释 + return None + + if '=' in line: + key, value = line.split('=', 1) + return key.strip(), value.strip() + return None + +def read_config_values(file_path: str) -> Dict[str, str]: + """读取配置文件中的所有键值对""" + config = {} + try: + if not os.path.exists(file_path): + print(f"错误: 文件 '{file_path}' 不存在") + return config + + with open(file_path, 'r', encoding='utf-8') as file: + for line_num, line in enumerate(file, 1): + parsed = parse_config_line(line) + if parsed: + key, value = parsed + config[key] = value + except Exception as e: + print(f"读取文件时出错: {e}") + return config + +def get_value_by_key(file_path: str, key: str) -> Optional[str]: + """根据键获取配置文件中的值""" + config = read_config_values(file_path) + return config.get(key) + +def get_config(app_name: str): + config_file = os.path.dirname(__file__) + '/assistant.conf' + src_url = get_value_by_key(config_file, app_name + "_src_url") + src_url_proxy = get_value_by_key(config_file, app_name + "_src_url_proxy") + commitID = get_value_by_key(config_file, app_name + "_commitID") + src_branch_name = get_value_by_key(config_file, app_name + "_src_branch") + dst_branch_name = get_value_by_key(config_file, app_name + "_dst_branch") + dst_url = get_value_by_key(config_file, app_name + "_dst_url") + project_url = get_value_by_key(config_file, app_name + "_project_url") + project_token = get_value_by_key(config_file, app_name + "_project_token") + mr_server = get_value_by_key(config_file, app_name + "_mr_server") + + return src_url, src_url_proxy, commitID, src_branch_name, dst_branch_name, dst_url, project_url, project_token, mr_server + +def save_config(key, new_value): + """ + 修改配置文件中指定key对应的value值 + + 参数: + key (str): 需要修改的键 + new_value (str): 新的value值 + """ + config_file = os.path.dirname(__file__) + '/assistant.conf' + try: + # 读取文件内容 + with open(config_file, 'r') as file: + lines = file.readlines() + + # 查找并修改目标行 + modified = False + with open(config_file, 'w') as file: + for line in lines: + # 检查是否是目标key所在行 + if line.startswith(f"{key}="): + # 写入新的键值对 + file.write(f"{key}={new_value}\n") + modified = True + else: + # 其他行保持不变 + file.write(line) + + if not modified: + # 如果未找到key,则追加新的键值对 + with open(config_file, 'a') as file: + file.write(f"{key}={new_value}\n") + print(f"未找到键 '{key}',已添加新的键值对") + else: + print(f"成功将键 '{key}' 的值修改为 '{new_value}'") + + except FileNotFoundError: + print(f"错误:文件 '{file_path}' 不存在") + except Exception as e: + print(f"修改文件时发生错误:{str(e)}") + +def generate_app_patches(app_name: str, commit_id: str, save_path: str) -> str: + """ + 拉取kernel或openssl代码仓指定分支的最新代码并从指定的commitID开始生成所有patch。 + + Args: + app_name (str): 应用名字。如果是 "openssl" 或 "kernel",则从指定仓库拉取。 + commit_id (str): 指定commitID生成从该点到最新版本的所有patch。 + save_path (str): 补丁文件的生成路径。 + + Returns: + str: 操作结果描述。 + """ + src_url, _, commitID, src_branch_name, _, _, _, _, _ = get_config(app_name) + if commit_id: #如果用户指定了commit_id则以用户输入的值为基线,如果为指定则使用配置文件中的commitID + commitID = commit_id + + local_repo_path = "/tmp/" + app_name + "-src" + try: + # 1. 克隆或更新仓库 + if not subprocess.run(["test", "-d", f"{local_repo_path}/.git"], capture_output=True).returncode == 0: + # 如果目录不存在或不是git仓库,则克隆 + subprocess.run(["git", "clone", src_url, local_repo_path], check=True) + else: + # 如果是git仓库,则拉取最新代码 + subprocess.run(["git", "-C", local_repo_path, "fetch", "origin"], check=True) + + #下载代码或更新代码后都走一边切换代码分支逻辑 + subprocess.run(["git", "-C", local_repo_path, "checkout", src_branch_name], check=True) + subprocess.run(["git", "-C", local_repo_path, "pull", "origin", src_branch_name], check=True) + # 2. 生成patch + # 使用git format-patch命令,如果commitID为空,则从第一个commit开始 + cmd = ["git", "-C", local_repo_path, "format-patch"] + if commitID: + cmd.append(f"{commitID}..HEAD") + else: + cmd.append("--all") # 从第一个commit开始 + + cmd.extend(["--output-directory", save_path]) + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + return f"成功从commit '{commitID or '第一个commit'}' 开始生成patch文件到 '{save_path}' 目录。" + + except subprocess.CalledProcessError as e: + return f"Git操作失败: {e.stderr}" + except Exception as e: + return f"生成patch时发生错误: {e}" + +def compress_patch(patch_content: str) -> str: + """分析补丁内容,提取MR提交信息""" + lines = patch_content.strip().split('\n') + if not lines: + return '' + + # 提取MR信息(从第一个非空行到签名行之间的所有内容) + mr_lines = [] + in_mr_section = False + signature_pattern = r'^--\s*$' # 签名行通常以"-- "开头 + + # 找到第一个非空行作为开始 + start_line = 0 + for i, line in enumerate(lines): + if line.strip(): + start_line = i + break + + for i, line in enumerate(lines): + if i == start_line: + in_mr_section = True + mr_lines.append(line.strip()) + continue + + if in_mr_section: + # 检查是否是签名行 + if re.match(signature_pattern, line): + in_mr_section = False + break + + # 检查是否是diff开始行 + if line.startswith('diff --git'): + in_mr_section = False + break + + mr_lines.append(line.strip()) + + return '\n'.join(mr_lines) + +def get_diff(input_str): + # 查找"Signed-off-by:"的位置 + marker = "Signed-off-by:" + marker_index = input_str.find(marker) + + if marker_index == -1: + return "" # 如果没有找到标记,返回空字符串 + + # 找到标记所在行的结束位置(换行符) + line_end_index = input_str.find('\n', marker_index) + + if line_end_index == -1: + # 如果标记在最后一行,返回空字符串 + return "" + + # 返回从标记行下一行开始到结尾的所有内容 + return input_str[line_end_index + 1:] + +# 对外使用的接口 +def get_patch_count(app_name: str, commit_id: Optional[str] = None) -> int: + """ + 读取指定软件的补丁数量 + + Args: + app_name (str): 指定的软件 + commit_id (str): (可选参数)指定commitID生成从该点到最新版本的所有patch + + Returns: + int: 补丁数量 + """ + directory = "/tmp/" + app_name + "-patchs" + subprocess.run(["rm", "-rf", directory]) # 清理/tmp/app_name-patchs目录 + if not os.path.exists(directory): + os.makedirs(directory, exist_ok=True) + generate_app_patches(app_name, commit_id, directory) + path = Path(directory) + return len([file for file in path.iterdir() if file.is_file()]) + +def find_patch_file(folder_path: str, number: int): + """ + 根据输入的编号查找对应命名的patch文件 + + 参数: + folder_path: 要搜索的文件夹路径 + number: 要查找的编号(整数) + + 返回: + 找到的文件名,若未找到则返回None + """ + # 检查文件夹是否存在 + if not os.path.exists(folder_path) or not os.path.isdir(folder_path): + raise ValueError(f"文件夹路径不存在或不是一个有效的目录: {folder_path}") + + # 生成目标文件名的前缀(4位数字,前补零) + target_prefix = f"{number:04d}-" + + # 正则表达式匹配模式,确保编号部分完全匹配 + pattern = re.compile(rf"^{target_prefix}.*\.patch$") + + # 遍历文件夹中的所有文件 + for filename in os.listdir(folder_path): + # 检查文件名是否匹配模式 + if pattern.match(filename): + return filename + + # 如果没有找到匹配的文件 + return None + +def concat_commit_url(app_name, commit_id): + """ + 将仓库URL、仓库名称和提交ID拼接成完整的提交链接 + + 参数: + app_name (str): 仓库名称 + commit_id (str): 提交的ID哈希值 + + 返回: + str: 完整的提交链接 + """ + # 从仓库URL中移除可能存在的.git后缀 + _, src_url_proxy, _, _, _, _, _, _, _ = get_config(app_name) + base_url = src_url_proxy.replace(f"/{app_name}.git", f"/{app_name}") + # 拼接成完整的提交链接 + return f"{base_url}/commit/{commit_id}" + +# 对外使用的接口 +def read_patch(app_name: str, patch_number: int) -> dict: + """ + 读取指定软件的补丁并按格式返回 + + Args: + app_name (str): 指定的软件 + patch_number (int): 待返回内容的补丁编号,从1开始 + + Returns: + dict: 格式化的文件内容字符串 + """ + try: + result = {} + # 根据编号找到文件名 + directory = "/tmp/" + app_name + "-patchs" + file_path = find_patch_file(directory, patch_number) + result["patch名"] = file_path + # 如果只获取了文件名,需要拼接完整路径 + if not os.path.isabs(file_path): + full_path = os.path.join(directory, file_path) + else: + full_path = file_path + + with open(full_path, 'r', encoding='utf-8') as f: + data = f.read() + content = compress_patch(data) #返回补丁文件的压缩后的内容 + lines = content.split('\n') + first_line = lines[0].strip() + parts = first_line.split() + if len(parts) >= 2 and parts[0] == 'From': + result["提交信息"] = parts[1] + result["commit_url"] = concat_commit_url(app_name, parts[1]) + result["提交描述"] = content + result["差异"] = get_diff(data) + except Exception as e: + return {"读取补丁失败"} + + global last_patch_number + global last_commit_id + if patch_number > last_patch_number: #读取补丁编号大于上一次读取过的补丁好则记录其commit id,在任务成功后写入app_repo.conf + last_patch_number = patch_number + last_commit_id = result["提交信息"] + return result + +def remove_braces(data, indent=0): + """递归处理JSON数据,去除大括号和中括号""" + result = [] + indent_str = " " * indent # 用于格式化输出的缩进 + + if isinstance(data, dict): + # 处理字典类型 + for key, value in data.items(): + # 递归处理值 + value_str = remove_braces(value, indent + 1) + result.append(f"{key} {value_str}\n") + elif isinstance(data, list): + # 处理列表类型 + for i, item in enumerate(data): + # 递归处理列表项 + item_str = remove_braces(item, indent + 1) + result.append(f"{item_str}") + else: + # 基本类型直接返回 + return str(data) + + return "\n".join(result) + +def get_patch_by_patch_name(patch_name, data): + # 遍历每个补丁信息块 + for patch in data: + # 找到匹配的patch_name + if patch.get("patch名") == patch_name: + # 返回对应的patch json 块 + return patch + + # 如果没有找到匹配项,返回None或其他合适的默认值 + return None + +def extract_patches(data): + # 过滤掉"确认合入"为"否"的补丁 + filtered = [item for item in data if item.get("确认合入") == "是"] + + # 按模块分组 + modules = {} + for item in filtered: + module = item.get("模块") + patch_name = item.get("patch名") + + if module not in modules: + modules[module] = [] + modules[module].append(patch_name) + + # 对每个模块内的补丁名称进行排序(从小到大) + for module in modules: + modules[module].sort() + + # 按模块中最小的补丁名排序模块 + # 先获取每个模块的最小补丁名,然后根据这个最小补丁名排序模块 + sorted_modules = sorted(modules.items(), key=lambda x: min(x[1])) + + # 提取补丁列表 + result = [patches for module, patches in sorted_modules] + + return result + +def create_local_new_branch_name(dst_branch_name: str, model: str) -> str: + """ + 根据给定的目标分支名称和模型名称创建新的本地分支名称 + + 规则: + - 当目标分支包含'release/'或'feature/'时,将其替换为'personal/', + 并在末尾拼接'/ai-patch_{model}' + - 当目标分支包含'personal/'时,直接在末尾拼接'_patch_{model}' + + 参数: + dst_branch_name: 目标分支名称 + model: 模型名称 + + 返回: + 新的本地分支名称 + """ + # 检查是否包含'release/'或'feature/' + if 'release/' in dst_branch_name: + new_branch = dst_branch_name.replace('release/', 'personal/') + return f"{new_branch}/ai-patch_{model}" + elif 'feature/' in dst_branch_name: + new_branch = dst_branch_name.replace('feature/', 'personal/') + return f"{new_branch}/ai-patch_{model}" + # 检查是否包含'personal/' + elif 'personal/' in dst_branch_name: + return f"{dst_branch_name}_patch_{model}" + # 如果都不匹配,按照release/或feature/的规则处理(或者根据需求调整) + else: + # 这里假设其他情况按照release/的规则处理,也可以根据实际需求修改 + return f"personal/{dst_branch_name}/ai-patch_{model}" + +# 对外使用的接口 +def apply_patch(app_name: str, data: list[Dict]) ->str: + """ + 根据补丁分析内容将对应的patch回合到代码仓并提交至远端仓库 + + Args: + app_name (str): 软件名称,如kernel或openssl + data (list[Dict]): 字典列表,内容为json格式的补丁分析结果 + + Returns: + str: 补丁文件合入情况 + """ + patch_name_list = extract_patches(data) + + results = "" + result_failed = "" + + repo_url, _, _, _, dst_branch_name, dst_url, project_url, project_token, mr_server = get_config(app_name) + + patchs_directory = "/tmp/" + app_name + "-patchs" + if not os.path.exists(patchs_directory): #目录不存在则重新生成补丁文件 + os.makedirs(patchs_directory, exist_ok=True) + generate_app_patches(app_name, patchs_directory) + + local_dst_repo_path = "/tmp/" + app_name + "-dst" + try: + # 1. 克隆或更新目标代码仓库 + if not subprocess.run(["test", "-d", f"{local_dst_repo_path}/.git"], capture_output=True).returncode == 0: + # 如果目录不存在或不是git仓库,则克隆 + subprocess.run(["git", "clone", dst_url, local_dst_repo_path], check=True) + else: + # 如果是git仓库,则拉取最新代码 + subprocess.run(["git", "-C", local_dst_repo_path, "fetch", "origin"], check=True) + + #切换分支 + subprocess.run(["git", "-C", local_dst_repo_path, "checkout", dst_branch_name], check=True) + subprocess.run(["git", "-C", local_dst_repo_path, "pull", "origin", dst_branch_name], check=True) + + except subprocess.CalledProcessError as e: + return f"Git操作失败: {e.stderr}" + + # 保存当前工作目录 + original_cwd = os.getcwd() + + try: + # 进入代码仓库目录 + os.chdir(local_dst_repo_path) + + for patch_names in patch_name_list: + # 准备组装mr message + mr_message = {"补丁列表:": ["\r"], "改动分析:": ["\r"], "合入原因:": ["\r"]} + title = "" + title_flag = True + commit_flag = True + # 遍历patch路径列表 + for patch_name in patch_names: + patch_json = get_patch_by_patch_name(patch_name, data) + if title_flag == True: + title = patch_json.get("补丁类型") + "[" + patch_json.get("模块") +"] " + "智能补丁回合\n" + title_flag = False + # 检查patch文件是否存在 + patch_path = patchs_directory + "/" + patch_name + if not os.path.exists(patch_path): #缺少patch为严重错误,终止任务 + result_failed += patch_name + "patch文件不存在" + break + + try: + # 使用git am应用patch + apply_result = subprocess.run(["git", "am", "--3way", patch_path], capture_output=True, text=True, cwd=local_dst_repo_path) + + + if apply_result.returncode == 0: + mr_message["补丁列表:"].append(patch_name[5:]) + mr_message["改动分析:"].append(patch_name[5:] + "
" + patch_json.get("commit_url") + "
" + patch_json.get("改动说明") + "
") + mr_message["合入原因:"].append(patch_name[5:] + "
" + patch_json.get("确认理由") + "
") + else: + subprocess.run(["git", "am", "--abort"], capture_output=True, text=True, cwd=local_dst_repo_path) + result_failed += patch_json.get("提交信息") + " patch冲突\n" + commit_flag = False + except Exception as e: + result_failed += patch_json.get("提交信息") + " patch冲突\n" + commit_flag = False + # 如果发生异常,终止循环 + break + #相同模块的补丁git apply完毕,生成一次MR + if commit_flag == True: + results += title + "合入成功\n" + new_branch_name = create_local_new_branch_name(dst_branch_name, patch_json.get("模块")) #生成模块粒度的新本地分支 + subprocess.run(["git", "branch", "-D", new_branch_name], capture_output=True, text=True, cwd=local_dst_repo_path) #如果目标分支存已经存在,删除目标分支 + subprocess.run(["git", "branch", new_branch_name], capture_output=True, text=True, cwd=local_dst_repo_path) #创建本地模块粒度分支 + subprocess.run(["git", "checkout", "-f", new_branch_name], capture_output=True, text=True, cwd=local_dst_repo_path) #切换进入本地模块粒度分支 + subprocess.run(["git", "push", "--set-upstream", "origin", new_branch_name], capture_output=True, text=True, cwd=local_dst_repo_path) #创建推送到远程 + mr_url = create_mr(mr_server, project_url, project_token, new_branch_name, dst_branch_name, title, remove_braces(mr_message)) + results += "合并请求URL:" + mr_url + "\n" + subprocess.run(["git", "checkout", "-f", dst_branch_name], capture_output=True, text=True, cwd=local_dst_repo_path) #强制切换回本地分支 + subprocess.run(["git", "reset", "--hard", "origin/" + dst_branch_name], capture_output=True, text=True, cwd=local_dst_repo_path) #清理本地分支,准备下一次循环 + else: + print(f"存在合并失败的补丁文件,该模块不能创建MR") + + + except Exception as e: + return {"error": f"切换目录时发生异常: {str(e)}"} + finally: + # 恢复原来的工作目录 + os.chdir(original_cwd) + if len(results) != 0 and len(result_failed) == 0: #所有的补丁都合并成功,则更新app_repo.conf文件的kernel_commitID字段为下一次需要分析的起始commit id + save_config(app_name + "_commitID", last_commit_id) + return results + result_failed + +def create_mr(mr_server: str, project_url: str, project_token: str, src_branch: str, tar_branch: str, title: str, description: str) -> str: + if mr_server == "sangfor": + return create_sangfor_mr(project_url, project_token, src_branch, tar_branch, title, description) + else: # default for gitee + return create_gitee_mr(project_url, project_token, src_branch, tar_branch, title, description) + +def create_sangfor_mr(project_url: str, project_token: str, src_branch: str, tar_branch: str, title: str, description: str) -> str: + """创建深信服远程模块分支的merge request到远程该工程分支 + """ + try: + mr_data = { + "source_branch": src_branch, + "target_branch": tar_branch, + "title": title, + "description": description, + "assignee_id": 27, #固定 + "remove_source_branch": True #固定 + } + headers = {'PRIVATE-TOKEN': project_token} + response = requests.post(project_url, json=mr_data, headers=headers) + if response.status_code != 201: + return f"推送失败: {response.text}" + return f"成功创建MR: " + response.json()["web_url"] + except Exception as e: + return f"推送失败: {str(e)}" + +def create_gitee_mr(project_url: str, project_token: str, src_branch: str, tar_branch: str, title: str, description: str) -> str: + """创建gitee远程模块分支的merge request到远程该工程分支 + """ + try: + mr_data = { + "title": title, + "head": src_branch, + "base": tar_branch, + "body": description + } + headers = { + "Authorization": f"token {project_token}", + "Content-Type": "application/json" + } + response = requests.post(project_url, json=mr_data, headers=headers) + if response.status_code != 201: + return f"推送失败: {response.text}" + return f"成功创建MR: " + response.json()["html_url"] + except Exception as e: + return f"推送失败: {str(e)}" + + +#if __name__ == "__main__": + #测试获取补丁数量 + #res = get_patch_count("kernel", "cbba3eb02aa940563f9c19aa5c27d19bdca3a194") + #res = get_patch_count("kernel") + #print(f"{res}") + + #测试补丁读取 + #res = read_patch("kernel", 2) + #print(f"{res}") + + #测试补丁应用 + #patch_analyze_content = '''[{\"提交信息\":\"860874bd1cca5142eb23d123a0aeac7ec9d73d75\",\"提交时间\":\"Fri, 21 Mar 2025 01:08:48 +0800\",\"模块\":\"PINCTRL\",\"合入策略\":\"是\",\"改动说明\":\"该补丁修复了CONFIG_PINCTRL_AMD配置不支持'm'(模块)选项的问题,并将其改为'y'(内置)。\",\"确认合入\":\"是\",\"确认理由\":\"需要合入\",\"提交标题\":\"[PATCH 1/4] PINCTRL: Fix the issue that CONFIG_PINCTRL_AMD do not support m option\",\"提交描述\":\"huawei inclusion\\ncategory: bugfix\\nbugzilla: https://gitee.com/src-openeuler/calamares/issues/IBS0LG\\nCVE: NA\\n\\n--------------------------------------\\n\\nFix the issue that the CONFIG_PINCTRL_ADM configuration do not support the 'm' (module) option, and change it to 'y' (built-in).\\n\\nFixes: cbba3eb02aa9 (\\\"PINCTRL:ENABLE_CONFIG_PINCTRL_AMD\\\")\\nSigned-off-by: zhaolichang \",\"差异\":\"---\\narch/arm64/configs/openeuler_defconfig | 2 +-\\narch/x86/configs/openeuler_defconfig | 2 +-\\n2 files changed, 2 insertions(+), 2 deletions(-)\",\"patch名\":\"0001-PINCTRL-Fix-the-issue-that-CONFIG_PINCTRL_AMD-do-not.patch\",\"补丁类型\":\"bugfix\",\"commit_url\":\"https://gitee.com/openeuler/kernel/commit/860874bd1cca5142eb23d123a0aeac7ec9d73d75\"},{\"提交信息\":\"056f91a0c5bb0e9afe9cbfc843fd83033d1a9d43\",\"提交时间\":\"Tue, 18 Mar 2025 15:24:52 +0800\",\"模块\":\"TLBI\",\"合入策略\":\"是\",\"改动说明\":\"此提交修复了由于ebfca9b4d3c1提交导致的TLB刷新问题。它移除了TLBI广播强制机制,并基于IPI添加了TLB刷新帮助函数。主要改动包括删除tlbflush.c文件,将相关功能整合到其他文件中,同时优化了相关头文件和Makefile配置。\",\"确认合入\":\"是\",\"确认理由\":\"需要合入\",\"提交标题\":\"[PATCH 2/4] tlbi: fix the problem of incorrect TLB flashing\",\"提交描述\":\"kunpeng inclusion\\ncategory: bugfix\\nbugzilla: https://gitee.com/openeuler/kernel/issues/IBU2Y1\\n\\n--------------------------------\\n\\nfix the problem of incorrect TLB flashing\\n\\nFixes: ebfca9b4d3c1 (\\\"tlbi: Do not force the broadcasting of TLBI and ICache, and add TLB flush helpers based on IPI.\\\")\\nSigned-off-by: zhaolichang \",\"差异\":\"---\\narch/arm64/Kconfig | 6 +-\\narch/arm64/include/asm/mmu_context.h | 14 +--\\narch/arm64/include/asm/pgtable.h | 4 -\\narch/arm64/include/asm/tlbflush.h | 164 +++++++++++----------------\\narch/arm64/kernel/Makefile | 1 -\\narch/arm64/kernel/smp.c | 13 +--\\narch/arm64/kernel/tlbflush.c | 41 -------\\narch/arm64/mm/context.c | 6 -\\n8 files changed, 80 insertions(+), 169 deletions(-)\\ndelete mode 100644 arch/arm64/kernel/tlbflush.c\",\"patch名\":\"0002-tlbi-fix-the-problem-of-incorrect-TLB-flashing.patch\",\"补丁类型\":\"bugfix\",\"commit_url\":\"https://gitee.com/openeuler/kernel/commit/056f91a0c5bb0e9afe9cbfc843fd83033d1a9d43\"},{\"提交信息\":\"f69080d24f34e605bb635e2b4c4baf7fb7504b6f\",\"提交时间\":\"Thu, 20 Mar 2025 17:52:47 +0800\",\"模块\":\"KVM\",\"合入策略\":\"是\",\"改动说明\":\"该补丁引入了一个新的命令行参数\\\"kvm-arm.hcr_nofb\\\",允许虚拟CPU(vcpu)在运行时不使用HCR_EL2.FB位。在鲲鹏920处理器上,当虚拟CPU绑定到物理CPU时,广播无效操作会导致性能问题。通过此补丁,用户可以在启动时设置参数以避免不必要的广播操作,从而提升性能。\",\"确认合入\":\"是\",\"确认理由\":\"需要合入\",\"提交标题\":\"[PATCH 3/4] KVM: arm64: Allow vcpus running without HCR_EL2.FB\",\"提交描述\":\"根据ARM DDI 0487G.a文档,设置HCR_EL2.FB位会导致某些TLBI和IC指令在Inner Shareable域内广播执行。用户反馈在鲲鹏920平台上运行虚拟机时,这种广播机制导致了性能问题。通过引入新的命令行参数\\\"kvm-arm.hcr_nofb\\\",允许用户在启动时禁用HCR_EL2.FB位,从而避免不必要的广播操作。此外,在vcpu迁移到新的物理CPU时,需要清理整个vcpu上下文。\",\"差异\":\"---\\narch/arm64/configs/openeuler_defconfig | 1 +\\narch/arm64/include/asm/kvm_emulate.h | 9 ++++++++\\narch/arm64/kvm/Kconfig | 15 +++++++++++++\\narch/arm64/kvm/arm.c | 29 ++++++++++++++++++++++++++\\ninclude/linux/kvm_host.h | 3 +++\\nvirt/kvm/kvm_main.c | 3 +++\\n6 files changed, 60 insertions(+)\",\"patch名\":\"0003-KVM-arm64-Allow-vcpus-running-without-HCR_EL2.FB.patch\",\"补丁类型\":\"bugfix\",\"commit_url\":\"https://gitee.com/openeuler/kernel/commit/f69080d24f34e605bb635e2b4c4baf7fb7504b6f\"},{\"提交信息\":\"a918a3a7157aeafa723b680ba6c4fb895dd07603\",\"提交时间\":\"Thu, 20 Mar 2025 05:47:01 +0800\",\"模块\":\"PINCTRL\",\"合入策略\":\"是\",\"改动说明\":\"该提交通过启用 CONFIG_PINCTRL 来修复触摸板功能异常问题。\",\"确认合入\":\"是\",\"确认理由\":\"需要合入\",\"提交标题\":\"[PATCH 4/4] PINCTRL: ENABLE_CONFIG_PINCTRL_INTEL\",\"提交描述\":\"huawei inclusion\\ncategory: feature\\nbugzilla: https://gitee.com/src-openeuler/calamares/issues/IBS0LG\\nCVE: NA\\n\\n----------------------------------------------------------\\n\\nThis commit is enable CONFIG_PINCTRL to fix touchpad malfunction\\n\",\"差异\":\"arch/x86/configs/openeuler_defconfig | 18 +++++++++---------\\n1 file changed, 9 insertions(+), 9 deletions(-)\",\"patch名\":\"0004-PINCTRL-ENABLE_CONFIG_PINCTRL_INTEL.patch\",\"补丁类型\":\"bugfix\",\"commit_url\":\"https://gitee.com/openeuler/kernel/commit/a918a3a7157aeafa723b680ba6c4fb895dd07603\"}]''' + #data = json.loads(patch_analyze_content.replace('\n', '\\n')) + #res = apply_patch("kernel", data) + #print(f"{res}") + + #测试创建MR,从远程模块分支到远程工程分支的合并请求 + #res = create_gitee_mr("openEuler-25.03_KVM", "openEuler-25.03", "标题: bugfix[KVM] 智能补丁回合", "合入原因: xxx\n 补丁列表: xxx\n 改动分析:xxx\n") + #print(f"{res}") diff --git a/servers/patch_analyzer_mcp/src/requirements.txt.txt b/servers/patch_analyzer_mcp/src/requirements.txt.txt new file mode 100644 index 0000000000000000000000000000000000000000..27dfdbbbfce09fb971877354791aa71a82aef445 --- /dev/null +++ b/servers/patch_analyzer_mcp/src/requirements.txt.txt @@ -0,0 +1,5 @@ +gitpython==3.1.45 +fastmcp==2.12.0 +pandas==2.3.2 +openai==1.100.1 +openpyxl==3.1.5