commit 88a0b40cdc9be9a7becbbf0e031edfa9c46d5037 Author: sairate Date: Sun Apr 27 14:40:20 2025 +0800 Signed-off-by: sairate diff --git a/.env b/.env new file mode 100644 index 0000000..b56584d --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +BAIDU_API_KEY = 4icZSO1OlMCU2ZiRMhgGCXFu +BAIDU_SECRET_KEY = 6wJldJ08m1jIX9hb0ULcJrIJ9D1OJW3c +DEEPSEEK_API_KEY = sk-f15b44b6b3344cdd820e59acebce9d2c diff --git a/README.md b/README.md new file mode 100644 index 0000000..74d2b4a --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# AI代码分析工具 v4.3 + +该工具基于人工智能,能够分析 Python 代码,提供详细的语法检查、逻辑分析和优化建议,并能自动生成流程图与类图。通过 Graphviz 和 PlantUML,用户可以更好地理解代码的结构和流程。 + +## 安装与配置 + +### 1. 安装 Python 环境 + +- 请确保您的机器上已安装 Python 3.x。若尚未安装,请访问 [Python 官方网站](https://www.python.org/downloads/) 下载并安装。 + +- 在安装 Python 后,确保 `pip` 工具已正确配置。您可以通过以下命令检查 Python 和 pip 是否安装成功: + ```bash + python --version + pip --version + ``` + +- 安装必要的 Python 库: + ```bash + pip install -r requirements.txt + ``` + + `requirements.txt` 文件包含了工具所依赖的所有 Python 库,包括 `openai`, `tkinter`, `Pillow`, `graphviz`, `python-dotenv` 等。 + +### 2. 安装 Java 环境 + +- 本工具依赖 PlantUML 生成 UML 图,因此需要安装 Java 环境。请访问 [Java 官网](https://www.java.com/en/download/) 下载并安装适用于您操作系统的 Java 版本。 + +- 安装完成后,检查 Java 环境是否配置成功: + ```bash + java -version + ``` + + 确保显示的版本是您已安装的 Java 版本。 + +### 3. 配置 PlantUML + +- 确保目录下有 `plantuml.jar` 文件,该文件是 PlantUML 的核心文件。 + +### 4. 配置环境变量 + +- 创建一个 `.env` 文件,并在其中添加 `DEEPSEEK_API_KEY`,这是您用于 DeepSeek API 的密钥。格式如下: + ``` + DEEPSEEK_API_KEY=your_api_key_here + ``` + +### 5. 运行配置脚本 `config.py` + +在项目根目录中找到并运行 `config.py` 配置文件。该脚本会帮助您检查并配置工具所需的环境设置,确保所有依赖项正确安装。运行命令如下: +```bash +python config.py +``` + +该脚本将自动检查 Java 和 Python 环境是否配置正确,并确保 PlantUML 配置无误。 + +### 6. 运行工具 + +配置完成后,您可以启动工具: +```bash +python app.py +``` + +该命令将启动应用程序,您可以通过界面输入代码并开始分析。 + +## 使用方法 + +1. **输入代码**:在界面中输入 Python 代码。 +2. **开始分析**:点击“开始分析”,工具会返回分析结果,包括: + - 语法检查与修正建议 + - 逻辑分析和优化建议 + - 生成流程图和类图 + +## 贡献 + +欢迎大家贡献代码,提供 Bug 修复或功能增强!请通过 Pull Requests 提交您的修改。 + +## 许可 + +本项目采用 MIT 许可协议,详情请见 [LICENSE]。 + diff --git a/app.py b/app.py new file mode 100644 index 0000000..7583037 --- /dev/null +++ b/app.py @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- +import os +import re +import queue +import threading +import tkinter as tk +import tempfile +import subprocess +from tkinter import ttk, messagebox, scrolledtext +from graphviz import Source +from openai import OpenAI +from dotenv import load_dotenv +from PIL import Image, ImageTk + +# 兼容性处理 +try: + resample_mode = Image.Resampling.LANCZOS +except AttributeError: + resample_mode = Image.ANTIALIAS + +load_dotenv() + +# 配置常量 +DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +PLANTUML_JAR = os.path.join(PROJECT_ROOT, "plantuml-1.2025.2.jar") +FONT_NAME = "Microsoft YaHei" + +# 线程安全队列 +analysis_queue = queue.Queue() +diagram_queue = queue.Queue() + +# 临时文件目录 +TEMP_DIR = tempfile.mkdtemp() + +# 界面样式配置 +STYLE_CONFIG = { + "font_family": FONT_NAME, + "font_size": 11, + "dark_bg": "#2d2d2d", + "light_bg": "#f0f0f0", + "primary": "#007acc", + "success": "#4CAF50", + "danger": "#f44336", + "text_primary": "#ffffff", + "text_secondary": "#333333", + "code_bg": "#f8f9fa" +} + + +class EnhancedCanvas(tk.Canvas): + """支持缩放和平移的画布组件""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.scale_factor = 1.0 + self.last_x = 0 + self.last_y = 0 + self.image_item = None + + # 事件绑定 + self.bind("", self.zoom) + self.bind("", self.start_pan) + self.bind("", self.pan) + self.bind("", self.zoom) # Linux支持 + self.bind("", self.zoom) + + def zoom(self, event): + """鼠标滚轮缩放""" + scale = 1.1 if event.delta > 0 or event.num == 4 else 0.9 + self.scale_factor *= scale + self.scale_factor = max(0.5, min(3.0, self.scale_factor)) + + x = self.canvasx(event.x) + y = self.canvasy(event.y) + self.scale(tk.ALL, x, y, scale, scale) + self.configure(scrollregion=self.bbox(tk.ALL)) + + def start_pan(self, event): + """开始平移""" + self.last_x = event.x + self.last_y = event.y + self.scan_mark(event.x, event.y) + + def pan(self, event): + """平移视图""" + self.scan_dragto(event.x, event.y, gain=1) + self.configure(scrollregion=self.bbox(tk.ALL)) + + def update_image(self, img_path): + """更新画布图片""" + if self.image_item: + self.delete(self.image_item) + + try: + img = Image.open(img_path) + self.tk_img = ImageTk.PhotoImage(img) + self.image_item = self.create_image(0, 0, anchor=tk.NW, image=self.tk_img) + self.configure(scrollregion=self.bbox(tk.ALL)) + except Exception as e: + messagebox.showerror("图片加载失败", str(e)) + + +class MarkdownRenderer(scrolledtext.ScrolledText): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.configure( + state='disabled', + wrap=tk.WORD, + padx=15, + pady=15, + font=(STYLE_CONFIG['font_family'], STYLE_CONFIG['font_size']) + ) + self._init_tags() + + def _init_tags(self): + self.tag_configure('h1', font=(FONT_NAME, 16, 'bold'), spacing3=10) + self.tag_configure('h2', font=(FONT_NAME, 14, 'bold'), spacing2=8) + self.tag_configure('bold', font=(FONT_NAME, STYLE_CONFIG['font_size'], 'bold')) + self.tag_configure('italic', font=(FONT_NAME, STYLE_CONFIG['font_size'], 'italic')) + self.tag_configure('code', + background=STYLE_CONFIG['code_bg'], + relief='sunken', + borderwidth=1, + font='Consolas 10') + + def render(self, markdown_text): + self.configure(state='normal') + self.delete(1.0, tk.END) + + lines = markdown_text.split('\n') + in_code_block = False + + for line in lines: + if line.strip().startswith('```'): + in_code_block = not in_code_block + continue + + if in_code_block: + self.insert(tk.END, line + '\n', 'code') + continue + + if line.startswith('# '): + self.insert(tk.END, line[2:] + '\n', 'h1') + elif line.startswith('## '): + self.insert(tk.END, line[3:] + '\n', 'h2') + else: + processed_line = self._process_inline_formatting(line) + self.insert(tk.END, processed_line + '\n') + + self.configure(state='disabled') + + def _process_inline_formatting(self, line): + line = re.sub(r'\*\*(.*?)\*\*', lambda m: self._apply_tag(m.group(1), 'bold'), line) + line = re.sub(r'\*(.*?)\*', lambda m: self._apply_tag(m.group(1), 'italic'), line) + return line + + def _apply_tag(self, text, tag): + self.insert(tk.END, text, tag) + return '' + + +class AsyncAnalyzer(threading.Thread): + """异步分析线程""" + + def __init__(self, code, callback): + super().__init__() + self.code = code + self.callback = callback + self.daemon = True + + def run(self): + try: + client = OpenAI( + api_key=DEEPSEEK_API_KEY, + base_url="https://api.deepseek.com" + ) + response = client.chat.completions.create( + model="deepseek-chat", + messages=[ + {"role": "system", "content": "请严格按以下Markdown格式分析代码:\n" + "## 语法检查\n- 错误列表\n- 修正建议\n\n" + "## 逻辑分析\n1. 步骤说明\n2. 流程图(必须使用标准DOT语法,用```dot标记)\n" + "3. 类图(用```plantuml标记)\n\n" + "## 优化建议\n- 性能优化\n- 可读性建议\n\n" + "DOT语法要求:\n" + "1. 使用有意义的节点名称\n" + "2. 所有边使用 -> 符号\n" + "3. 节点属性用方括号包裹"}, + {"role": "user", "content": f"分析代码:\n```python\n{self.code}\n```"} + ], + temperature=0.3 + ) + self.callback(response.choices[0].message.content, None) + except Exception as e: + self.callback(None, str(e)) + + +class DiagramGenerator: + @staticmethod + def generate_flow(dot_code, filename): + """生成中文流程图""" + try: + font_config = f''' + graph [fontname="{FONT_NAME}"]; + node [fontname="{FONT_NAME}"]; + edge [fontname="{FONT_NAME}"]; + ''' + dot_code = re.sub(r'(digraph\s*\w*\s*{)', f'\\1\n{font_config}', dot_code) + + filepath = os.path.join(TEMP_DIR, filename) + src = Source(dot_code, format='png', engine='dot', encoding='utf-8') + output_path = src.render(filepath, cleanup=True) + return output_path + except Exception as e: + return f"流程图错误:{str(e)}" + + @staticmethod + def generate_class(plantuml_code, filename): + """生成类图(英文)""" + try: + plantuml_code = re.sub(r'^\s*```\s*plantuml\s*\n', '', plantuml_code, flags=re.IGNORECASE) + plantuml_code = re.sub(r'\n\s*```\s*$', '', plantuml_code) + + uml_path = os.path.join(TEMP_DIR, f"{filename}.puml") + img_path = os.path.join(TEMP_DIR, f"{filename}.png") + + with open(uml_path, 'w', encoding='utf-8') as f: + f.write(plantuml_code) + + cmd = [ + 'java', + '-Djava.awt.headless=true', + '-jar', PLANTUML_JAR, + '-tpng', + uml_path, + '-o', TEMP_DIR + ] + + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=30 + ) + + if result.returncode != 0: + error_msg = result.stdout.strip() + return f"类图生成失败:{error_msg.split('ERROR')[-1].strip()}" + + return img_path + except subprocess.TimeoutExpired: + return "错误:生成超时(30秒)" + except Exception as e: + return f"系统错误:{str(e)}" + finally: + if os.path.exists(uml_path): + os.remove(uml_path) + + +class CodeAnalyzerApp: + def __init__(self, root): + self.root = root + self._init_ui() + self._init_state() + self._start_queue_processor() + + def _init_ui(self): + """初始化界面""" + self.root.title("AI代码分析工具 v4.3") + self.root.geometry("1440x960") + + # 主布局 + main_paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL) + main_paned.pack(fill=tk.BOTH, expand=True) + + # 上部区域 + top_pane = ttk.PanedWindow(main_paned, orient=tk.HORIZONTAL) + main_paned.add(top_pane) + + # 代码输入区 + input_frame = ttk.LabelFrame(top_pane, text=" 代码输入 ", padding=10) + top_pane.add(input_frame, weight=1) + self.code_input = scrolledtext.ScrolledText( + input_frame, + font=('Consolas', 11), + wrap=tk.WORD, + padx=10, + pady=10 + ) + self.code_input.pack(fill=tk.BOTH, expand=True) + + # 分析结果区 + result_frame = ttk.Frame(top_pane) + top_pane.add(result_frame, weight=1) + self.md_view = MarkdownRenderer(result_frame) + self.md_view.pack(fill=tk.BOTH, expand=True) + + # 下部图表区 + bottom_pane = ttk.PanedWindow(main_paned, orient=tk.HORIZONTAL) + main_paned.add(bottom_pane, weight=1) + + # 流程图面板 + flow_frame = ttk.LabelFrame(bottom_pane, text=" 流程图 ", padding=5) + bottom_pane.add(flow_frame, weight=1) + self.flow_canvas = EnhancedCanvas( + flow_frame, + bg="white", + bd=2, + relief=tk.GROOVE + ) + self.flow_canvas.pack(fill=tk.BOTH, expand=True) + + # 类图面板 + class_frame = ttk.LabelFrame(bottom_pane, text=" 类图 ", padding=5) + bottom_pane.add(class_frame, weight=1) + self.class_canvas = EnhancedCanvas( + class_frame, + bg="white", + bd=2, + relief=tk.GROOVE + ) + self.class_canvas.pack(fill=tk.BOTH, expand=True) + + # 控制栏 + self._init_controls() + + def _init_controls(self): + """初始化控制组件""" + control_frame = ttk.Frame(self.root) + control_frame.pack(fill=tk.X, pady=5) + + self.analyze_btn = ttk.Button( + control_frame, + text="开始分析", + command=self._start_analysis + ) + self.analyze_btn.pack(side=tk.LEFT, padx=10) + + ttk.Button( + control_frame, + text="重置视图", + command=self._reset_view + ).pack(side=tk.RIGHT, padx=10) + + # 状态栏 + self.status_var = tk.StringVar() + status_bar = ttk.Label( + self.root, + textvariable=self.status_var, + relief=tk.SUNKEN, + anchor=tk.W + ) + status_bar.pack(side=tk.BOTTOM, fill=tk.X) + + def _init_state(self): + """初始化状态""" + self.is_processing = False + + def _start_analysis(self): + """启动分析任务""" + if self.is_processing: + return + + code = self.code_input.get("1.0", tk.END).strip() + if len(code) < 20: + messagebox.showwarning("输入错误", "请输入有效的代码内容") + return + + self.is_processing = True + self.analyze_btn.config(state=tk.DISABLED) + self.status_var.set("正在分析代码...") + + AsyncAnalyzer( + code=code, + callback=self._handle_analysis_result + ).start() + + def _handle_analysis_result(self, result, error): + """处理分析结果""" + self.is_processing = False + self.analyze_btn.config(state=tk.NORMAL) + + if error: + self.status_var.set(f"分析失败:{error}") + messagebox.showerror("分析错误", error) + return + + self.md_view.render(result) + self.status_var.set("正在生成图表...") + self._process_diagrams(result) + + def _process_diagrams(self, analysis_result): + """处理图表生成""" + dot_code = self._extract_code_block(analysis_result, 'dot') + plantuml_code = self._extract_code_block(analysis_result, 'plantuml') + + if dot_code: + threading.Thread( + target=self._generate_diagram, + args=('flow', dot_code, 'flow_diagram'), + daemon=True + ).start() + + if plantuml_code: + threading.Thread( + target=self._generate_diagram, + args=('class', plantuml_code, 'class_diagram'), + daemon=True + ).start() + + def _generate_diagram(self, diagram_type, code, filename): + """生成并更新图表""" + try: + if diagram_type == 'flow': + path = DiagramGenerator.generate_flow(code, filename) + else: + path = DiagramGenerator.generate_class(code, filename) + + diagram_queue.put((diagram_type, path)) + except Exception as e: + diagram_queue.put((diagram_type, f"生成错误:{str(e)}")) + + def _start_queue_processor(self): + """启动队列处理""" + + def process(): + if not diagram_queue.empty(): + diagram_type, path = diagram_queue.get() + if diagram_type == 'flow': + self._update_flow_diagram(path) + else: + self._update_class_diagram(path) + + self.root.after(100, process) + + process() + + def _update_flow_diagram(self, img_path): + """更新流程图""" + if img_path.startswith(("错误:", "流程图错误:")): + self.status_var.set(img_path) + messagebox.showerror("流程图错误", img_path.split(":", 1)[-1]) + return + + try: + self.flow_canvas.update_image(img_path) + self.status_var.set("流程图已更新") + except Exception as e: + messagebox.showerror("流程图加载失败", str(e)) + + def _update_class_diagram(self, img_path): + """更新类图""" + if img_path.startswith(("错误:", "类图生成失败:")): + messagebox.showerror("类图错误", img_path.split(":", 1)[-1]) + return + + try: + self.class_canvas.update_image(img_path) + self.status_var.set("类图已更新") + except Exception as e: + messagebox.showerror("类图加载失败", str(e)) + + def _reset_view(self): + """重置视图""" + for canvas in [self.flow_canvas, self.class_canvas]: + canvas.scale_factor = 1.0 + canvas.scale(tk.ALL, 0, 0, 1, 1) + canvas.configure(scrollregion=canvas.bbox(tk.ALL)) + + @staticmethod + def _extract_code_block(text, lang): + """提取代码块""" + pattern = rf'```{lang}\n(.*?)\n```' + match = re.search(pattern, text, re.DOTALL) + return match.group(1).strip() if match else None + + +def check_environment(): + """环境检查""" + try: + subprocess.run( + ['java', '-version'], + check=True, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + timeout=10, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 + ) + + if not os.path.exists(PLANTUML_JAR): + messagebox.showerror("配置错误", f"未找到PlantUML JAR文件:\n{PLANTUML_JAR}") + return False + + return True + except Exception as e: + messagebox.showerror("环境错误", f"Java环境异常:{str(e)}") + return False + + +if __name__ == "__main__": + if not check_environment(): + exit(1) + + root = tk.Tk() + app = CodeAnalyzerApp(root) + root.mainloop() \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..e231cf5 --- /dev/null +++ b/config.py @@ -0,0 +1,79 @@ +import os +import subprocess +import requests +from tqdm import tqdm + +# 保存到脚本所在目录 +download_dir = os.path.dirname(os.path.abspath(__file__)) + +# 配置文件信息 +files = { + "graphviz": { + "url": "https://blog.sairate.top/upload/app/windows_10_cmake_Release_graphviz-install-12.2.1-win64.exe", + "filename": "graphviz-install-12.2.1-win64.exe", + "install_cmd": lambda path: [path, '/S', '/AddToPath=1'] + }, + "cmake": { + "url": "https://blog.sairate.top/upload/app/cmake-4.0.0-rc4-windows-x86_64.msi", + "filename": "cmake-4.0.0-rc4-windows-x86_64.msi", + "install_cmd": lambda path: [ + "msiexec", "/i", path, "/qn", "ADD_CMAKE_TO_PATH=System" + ] + } +} + +# 单独下载但不安装的文件 +optional_files = { + "plantuml": { + "url": "https://blog.sairate.top/upload/app/plantuml-1.2025.2.jar", + "filename": "plantuml-1.2025.2.jar" + } +} + + +def download_file(url, filename): + file_path = os.path.join(download_dir, filename) + print(f"开始下载 {filename} ...") + response = requests.get(url, stream=True) + total = int(response.headers.get('content-length', 0)) + + with open(file_path, 'wb') as file, tqdm( + desc=filename, + total=total, + unit='iB', + unit_scale=True, + unit_divisor=1024, + ) as bar: + for data in response.iter_content(chunk_size=1024): + size = file.write(data) + bar.update(size) + + print(f"下载完成:{filename}") + return file_path + + +def install_file(file_path, install_command): + print(f"开始安装 {file_path} ...") + subprocess.run(install_command(file_path), check=True) + print(f"安装完成:{file_path}") + + +def main(): + # 安装需要安装的文件 + for name, info in files.items(): + try: + file_path = download_file(info["url"], info["filename"]) + install_file(file_path, info["install_cmd"]) + except Exception as e: + print(f"处理 {name} 时出错: {e}") + + # 下载可选文件 + for name, info in optional_files.items(): + try: + download_file(info["url"], info["filename"]) + except Exception as e: + print(f"下载 {name} 时出错: {e}") + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9dc8a63 Binary files /dev/null and b/requirements.txt differ