跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PythonAI

Python 调用本地 DocsGPT API 构建 GUI 开发工具

介绍使用 Python 和 Tkinter 构建集成本地 DocsGPT 及 OpenAI API 的图形化开发工具。项目包含代码编辑器、API 客户端、SQLite 历史记录管理及主题切换功能。支持同步/异步调用、流式响应,并提供代码执行与调试能力。通过优化界面与错误处理,实现了完整的本地 AI 辅助开发环境。

MongoKing发布于 2026/4/6更新于 2026/10/25126 浏览

Python 调用本地 DocsGPT API 构建 GUI 开发工具

本文介绍如何使用 Python 和 Tkinter 构建一个集成本地 DocsGPT API 及 OpenAI API 的图形化开发工具。

项目概述

核心功能

  • 代码编辑器:支持语法高亮、文件操作(新建、打开、保存)、查找替换、撤销/重做。
  • GPT 助手:集成本地 DocsGPT 与 OpenAI API,支持代码上下文提问。
  • 历史记录:使用 SQLite 存储问答历史,支持搜索与详情查看。
  • 主题切换:支持浅色与深色主题。
  • 代码执行:内置临时文件运行机制,支持输出捕获。

界面设计

界面分为左右两部分:

  1. 左侧为 Python 代码编辑区:
    • 左上部为代码多行输入框,嵌入 Python Idle 风格。
    • 左下部为 Run 按钮。
  2. 右侧为 GPT 调用区:
    • 右上部为 Tab,名称 Question,嵌入多行文本,输入提问问题。
    • 中部为 Show Answer 按钮,海蓝色。
    • 下部为 2 个 Tab:
      • Tab1 名称 Answer,嵌入多行文本,显示 GPT 处理结果。
      • Tab2 名称 History,显示提问历史,数据来自名为 pyai 的 SQLite 数据库。

主要更新说明

支持两种 API 类型

  • docsgpt: 本地 DocsGPT API
  • openai: OpenAI 官方 API

DocsGPT 特定功能

  • 支持项目 ID 和会话 ID
  • 自动适应不同的响应格式
  • 支持流式响应

改进的错误处理

  • 更好的连接测试
  • 详细的错误信息
  • 超时设置

配置管理

  • 在设置对话框中添加 DocsGPT 配置
  • 自动保存和恢复设置
  • 测试连接功能

兼容性

  • 保持对 OpenAI API 的向后兼容
  • 自动检测和适应不同的响应格式

项目结构

gpt_code_ide/
├── main.py          # 主程序
├── database.py      # SQLite 数据库操作
├── gpt_client.py    # GPT API 客户端
└── requirements.txt # 依赖包

1. requirements.txt

requests
pygments
idlelib

2. database.py - SQLite 数据库操作

"""
数据库操作模块
创建和管理 SQLite 数据库,存储提问历史
"""
import sqlite3
import json
from datetime import datetime
from typing import List, Dict, Optional

class PyAIDatabase:
    def __init__(self, db_name: str = "pyai.db"):
        self.db_name = db_name
        self.init_database()

    def init_database(self):
        """初始化数据库表"""
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            # 创建历史记录表
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS query_history (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    question TEXT NOT NULL,
                    answer TEXT NOT NULL,
                    code_context TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)
            # 创建代码片段表
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS code_snippets (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    title TEXT NOT NULL,
                    code TEXT NOT NULL,
                    language TEXT DEFAULT 'python',
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)
            # 创建配置表
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS settings (
                    key TEXT PRIMARY KEY,
                    value TEXT NOT NULL
                )
            """)
            # 插入默认配置
            cursor.execute("""
                INSERT OR IGNORE INTO settings (key, value) VALUES ('api_key', ''), ('api_url', 'https://api.openai.com/v1/chat/completions'), ('theme', 'light'), ('font_size', '12')
            """)
            conn.commit()

    def save_query(self, question: str, answer: str, code_context: str = "") -> int:
        """保存查询记录"""
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO query_history (question, answer, code_context) VALUES (?, ?, ?)
            """, (question, answer, code_context))
            conn.commit()
            return cursor.lastrowid

    def get_query_history(self, limit: int = 50, offset: int = 0) -> List[Dict]:
        """获取查询历史"""
        with sqlite3.connect(self.db_name) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("""
                SELECT id, question, answer, code_context, created_at, updated_at FROM query_history ORDER BY created_at DESC LIMIT ? OFFSET ?
            """, (limit, offset))
            rows = cursor.fetchall()
            return [dict(row) for row in rows]

    def delete_query(self, query_id: int) -> bool:
        """删除查询记录"""
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("DELETE FROM query_history WHERE id = ?", (query_id,))
            conn.commit()
            return cursor.rowcount > 0

    def clear_history(self) -> bool:
        """清空历史记录"""
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("DELETE FROM query_history")
            conn.commit()
            return cursor.rowcount > 0

    def save_code_snippet(self, title: str, code: str, language: str = "python") -> int:
        """保存代码片段"""
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO code_snippets (title, code, language) VALUES (?, ?, ?)
            """, (title, code, language))
            conn.commit()
            return cursor.lastrowid

    def get_code_snippets(self) -> List[Dict]:
        """获取所有代码片段"""
        with sqlite3.connect(self.db_name) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("""
                SELECT id, title, code, language, created_at FROM code_snippets ORDER BY created_at DESC
            """)
            return [dict(row) for row in cursor.fetchall()]

    def get_setting(self, key: str) -> Optional[str]:
        """获取配置"""
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT value FROM settings WHERE key = ?", (key,))
            result = cursor.fetchone()
            return result[0] if result else None

    def update_setting(self, key: str, value: str):
        """更新配置"""
        with sqlite3.connect(self.db_name) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)
            """, (key, value))
            conn.commit()

    def search_history(self, keyword: str) -> List[Dict]:
        """搜索历史记录"""
        with sqlite3.connect(self.db_name) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.cursor()
            cursor.execute("""
                SELECT id, question, answer, created_at FROM query_history WHERE question LIKE ? OR answer LIKE ? ORDER BY created_at DESC
            """, (f"%{keyword}%", f"%{keyword}%"))
            return [dict(row) for row in cursor.fetchall()]

# 全局数据库实例
db = PyAIDatabase()

3. gpt_client.py - GPT API 客户端

"""
GPT API 客户端模块
处理与 GPT API 的通信
"""
import requests
import json
from typing import Optional, Dict, Any
import threading
from database import db

class GPTClient:
    def __init__(self, api_key: str = None, api_url: str = None):
        self.api_key = api_key or db.get_setting('api_key') or ""
        self.api_url = api_url or db.get_setting('api_url') or "https://api.openai.com/v1/chat/completions"
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })

    def set_api_key(self, api_key: str):
        """设置 API 密钥"""
        self.api_key = api_key
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})
        db.update_setting('api_key', api_key)

    def set_api_url(self, api_url: str):
        """设置 API URL"""
        self.api_url = api_url
        db.update_setting('api_url', api_url)

    def ask_question(self, question: str, code_context: str = "", model: str = "gpt-3.5-turbo", max_tokens: int = 1000, temperature: float = 0.7, callback=None) -> Optional[str]:
        """
        向 GPT 提问
        Args:
            question: 问题文本
            code_context: 相关代码上下文
            model: 使用的模型
            max_tokens: 最大 token 数
            temperature: 温度参数
            callback: 回调函数,用于异步更新 UI
        Returns:
            GPT 的回答
        """
        if not self.api_key:
            return "错误:请先设置 API 密钥"

        messages = []
        if code_context:
            messages.append({
                "role": "system",
                "content": f"以下是相关代码上下文:\n```python\n{code_context}\n```\n请基于此代码上下文回答问题。"
            })
        messages.append({
            "role": "user",
            "content": question
        })

        data = {
            "model": model,
            "messages": messages,
            "max_tokens": max_tokens,
            "temperature": temperature,
            "stream": False
        }

        try:
            if callback:
                thread = threading.Thread(target=self._async_ask, args=(data, callback))
                thread.daemon = True
                thread.start()
                return "正在处理..."
            else:
                return self._sync_ask(data)
        except Exception as e:
            return f"请求错误:{str(e)}"

    def _async_ask(self, data: Dict, callback):
        """异步调用 GPT"""
        try:
            response = self.session.post(self.api_url, json=data, timeout=30)
            if response.status_code == 200:
                result = response.json()
                answer = result['choices'][0]['message']['content']
                callback(answer)
            else:
                error_msg = f"API 错误:{response.status_code}\n{response.text}"
                callback(error_msg)
        except requests.exceptions.Timeout:
            callback("错误:请求超时,请检查网络连接")
        except requests.exceptions.ConnectionError:
            callback("错误:无法连接到 API 服务器")
        except Exception as e:
            callback(f"未知错误:{str(e)}")

    def _sync_ask(self, data: Dict) -> str:
        """同步调用 GPT"""
        try:
            response = self.session.post(self.api_url, json=data, timeout=30)
            if response.status_code == 200:
                result = response.json()
                return result['choices'][0]['message']['content']
            else:
                return f"API 错误:{response.status_code}\n{response.text}"
        except requests.exceptions.Timeout:
            return "错误:请求超时,请检查网络连接"
        except requests.exceptions.ConnectionError:
            return "错误:无法连接到 API 服务器"
        except Exception as e:
            return f"未知错误:{str(e)}"

    def get_models(self) -> list:
        """获取可用的模型列表(需要相应权限)"""
        if not self.api_key:
            return []
        try:
            response = self.session.get("https://api.openai.com/v1/models", timeout=10)
            if response.status_code == 200:
                models = response.json()['data']
                return [model['id'] for model in models if 'gpt' in model['id'].lower()]
            return []
        except:
            return ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"]

# 全局 GPT 客户端实例
gpt_client = GPTClient()

4. main.py - 主 GUI 程序

"""
主 GUI 程序 Python 代码编辑器 + GPT 助手
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, font
import subprocess
import tempfile
import os
import sys
from datetime import datetime
from database import db
from gpt_client import gpt_client

# 颜色配置
COLORS = {
    "light": {
        "bg": "#f5f5f5",
        "fg": "#333333",
        "editor_bg": "#f8f8f8",
        "button_bg": "#007acc",
        "button_fg": "white",
        "tab_bg": "#ffffff",
        "border": "#cccccc",
        "highlight": "#e6f3ff"
    },
    "dark": {
        "bg": "#2d2d2d",
        "fg": "#e0e0e0",
        "editor_bg": "#1e1e1e",
        "button_bg": "#007acc",
        "button_fg": "white",
        "tab_bg": "#252526",
        "border": "#404040",
        "highlight": "#04395e"
    }
}

class CodeEditor(tk.Frame):
    """代码编辑器组件"""
    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)
        self.create_widgets()

    def create_widgets(self):
        """创建编辑器组件"""
        toolbar = tk.Frame(self)
        toolbar.pack(fill=tk.X, padx=5, pady=2)
        tk.Button(toolbar, text="新建", command=self.new_file, width=6).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="打开", command=self.open_file, width=6).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="保存", command=self.save_file, width=6).pack(side=tk.LEFT, padx=2)
        tk.Label(toolbar, text="|").pack(side=tk.LEFT, padx=5)
        tk.Button(toolbar, text="撤销", command=self.undo, width=6).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="重做", command=self.redo, width=6).pack(side=tk.LEFT, padx=2)
        tk.Label(toolbar, text="|").pack(side=tk.LEFT, padx=5)
        tk.Button(toolbar, text="查找", command=self.find_text, width=6).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="替换", command=self.replace_text, width=6).pack(side=tk.LEFT, padx=2)

        self.text_widget = scrolledtext.ScrolledText(
            self, wrap=tk.WORD, font=("Consolas", 12), bg=COLORS["light"]["editor_bg"], fg=COLORS["light"]["fg"], insertbackground="black", selectbackground="#d4d4d4", undo=True, maxundo=-1
        )
        self.text_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        self.bind_shortcuts()

        self.status_bar = tk.Label(self, text="就绪", bd=1, relief=tk.SUNKEN, anchor=tk.W)
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        self.current_file = None

    def bind_shortcuts(self):
        """绑定快捷键"""
        self.text_widget.bind('<Control-s>', lambda e: self.save_file())
        self.text_widget.bind('<Control-o>', lambda e: self.open_file())
        self.text_widget.bind('<Control-n>', lambda e: self.new_file())
        self.text_widget.bind('<Control-f>', lambda e: self.find_text())
        self.text_widget.bind('<Control-h>', lambda e: self.replace_text())
        self.text_widget.bind('<Control-z>', lambda e: self.undo())
        self.text_widget.bind('<Control-y>', lambda e: self.redo())

    def new_file(self):
        """新建文件"""
        self.text_widget.delete(1.0, tk.END)
        self.current_file = None
        self.update_status("新建文件")

    def open_file(self):
        """打开文件"""
        from tkinter import filedialog
        file_path = filedialog.askopenfilename(filetypes=[("Python files", "*.py"), ("Text files", "*.txt"), ("All files", "*.*")])
        if file_path:
            try:
                with open(file_path, 'r', encoding='utf-8') as file:
                    content = file.read()
                    self.text_widget.delete(1.0, tk.END)
                    self.text_widget.insert(1.0, content)
                    self.current_file = file_path
                    self.update_status(f"已打开:{os.path.basename(file_path)}")
            except Exception as e:
                messagebox.showerror("错误", f"无法打开文件:{str(e)}")

    def save_file(self):
        """保存文件"""
        if not self.current_file:
            self.save_as_file()
        else:
            try:
                content = self.text_widget.get(1.0, tk.END)
                with open(self.current_file, 'w', encoding='utf-8') as file:
                    file.write(content)
                self.update_status(f"已保存:{os.path.basename(self.current_file)}")
            except Exception as e:
                messagebox.showerror("错误", f"无法保存文件:{str(e)}")

    def save_as_file(self):
        """另存为文件"""
        from tkinter import filedialog
        file_path = filedialog.asksaveasfilename(defaultextension=".py", filetypes=[("Python files", "*.py"), ("Text files", "*.txt"), ("All files", "*.*")])
        if file_path:
            self.current_file = file_path
            self.save_file()

    def undo(self):
        """撤销"""
        try:
            self.text_widget.edit_undo()
        except:
            pass

    def redo(self):
        """重做"""
        try:
            self.text_widget.edit_redo()
        except:
            pass

    def find_text(self):
        """查找文本"""
        self.show_search_dialog()

    def replace_text(self):
        """替换文本"""
        self.show_search_dialog(replace=True)

    def show_search_dialog(self, replace=False):
        """显示查找/替换对话框"""
        dialog = tk.Toplevel(self)
        dialog.title("替换" if replace else "查找")
        dialog.geometry("400x200")
        dialog.transient(self)
        dialog.grab_set()
        tk.Label(dialog, text="查找内容:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        find_entry = tk.Entry(dialog, width=30)
        find_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
        if replace:
            tk.Label(dialog, text="替换为:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
            replace_entry = tk.Entry(dialog, width=30)
            replace_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)

        def do_find():
            text = find_entry.get()
            if text:
                start_pos = self.text_widget.index(tk.INSERT)
                pos = self.text_widget.search(text, start_pos, tk.END)
                if pos:
                    end_pos = f"{pos}+{len(text)}c"
                    self.text_widget.tag_remove(tk.SEL, 1.0, tk.END)
                    self.text_widget.tag_add(tk.SEL, pos, end_pos)
                    self.text_widget.mark_set(tk.INSERT, pos)
                    self.text_widget.see(pos)
                else:
                    messagebox.showinfo("查找", "未找到匹配项")

        def do_replace():
            find_text_val = find_entry.get()
            replace_text_val = replace_entry.get()
            if find_text_val:
                content = self.text_widget.get(1.0, tk.END)
                new_content = content.replace(find_text_val, replace_text_val)
                self.text_widget.delete(1.0, tk.END)
                self.text_widget.insert(1.0, new_content)

        tk.Button(dialog, text="查找", command=do_find).grid(row=2, column=0, padx=5, pady=10)
        if replace:
            tk.Button(dialog, text="替换", command=do_replace).grid(row=2, column=1, padx=5, pady=10)

    def update_status(self, message):
        """更新状态栏"""
        self.status_bar.config(text=message)

    def get_code(self):
        """获取当前代码"""
        return self.text_widget.get(1.0, tk.END).strip()

    def set_code(self, code):
        """设置代码"""
        self.text_widget.delete(1.0, tk.END)
        self.text_widget.insert(1.0, code)

    def clear(self):
        """清空编辑器"""
        self.text_widget.delete(1.0, tk.END)

class GPTAssistant(tk.Frame):
    """GPT 助手组件"""
    def __init__(self, parent, code_editor, **kwargs):
        super().__init__(parent, **kwargs)
        self.code_editor = code_editor
        self.create_widgets()
        self.load_history()

    def create_widgets(self):
        """创建 GPT 助手界面"""
        config_frame = tk.Frame(self)
        config_frame.pack(fill=tk.X, padx=10, pady=5)
        tk.Label(config_frame, text="API 密钥:").pack(side=tk.LEFT)
        self.api_key_var = tk.StringVar(value=db.get_setting('api_key') or "")
        self.api_entry = tk.Entry(config_frame, textvariable=self.api_key_var, width=30, show="*")
        self.api_entry.pack(side=tk.LEFT, padx=5)
        tk.Button(config_frame, text="保存", command=self.save_api_key).pack(side=tk.LEFT, padx=2)
        tk.Button(config_frame, text="显示/隐藏", command=self.toggle_api_key).pack(side=tk.LEFT, padx=2)

        tk.Label(self, text="问题:", font=("Arial", 10, "bold")).pack(anchor=tk.W, padx=10, pady=(10, 0))
        self.question_text = scrolledtext.ScrolledText(self, height=8, wrap=tk.WORD, font=("Arial", 11))
        self.question_text.pack(fill=tk.X, padx=10, pady=5)

        button_frame = tk.Frame(self)
        button_frame.pack(fill=tk.X, padx=10, pady=10)
        self.ask_button = tk.Button(button_frame, text="获取答案", command=self.ask_gpt, bg="#007acc", fg="white", font=("Arial", 11, "bold"), height=2, width=12, cursor="hand2")
        self.ask_button.pack(side=tk.LEFT, padx=5)
        tk.Button(button_frame, text="清除问题", command=self.clear_question, bg="#6c757d", fg="white", height=2, width=10).pack(side=tk.LEFT, padx=5)
        self.include_code_var = tk.BooleanVar(value=True)
        tk.Checkbutton(button_frame, text="包含代码上下文", variable=self.include_code_var, font=("Arial", 9)).pack(side=tk.LEFT, padx=20)

        self.notebook = ttk.Notebook(self)
        self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

        self.answer_frame = tk.Frame(self.notebook)
        self.notebook.add(self.answer_frame, text="答案")
        self.answer_text = scrolledtext.ScrolledText(self.answer_frame, wrap=tk.WORD, font=("Arial", 11))
        self.answer_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        self.history_frame = tk.Frame(self.notebook)
        self.notebook.add(self.history_frame, text="历史")

        history_toolbar = tk.Frame(self.history_frame)
        history_toolbar.pack(fill=tk.X, padx=5, pady=5)
        tk.Button(history_toolbar, text="刷新", command=self.load_history).pack(side=tk.LEFT, padx=2)
        tk.Button(history_toolbar, text="清空", command=self.clear_history).pack(side=tk.LEFT, padx=2)
        tk.Label(history_toolbar, text="搜索:").pack(side=tk.LEFT, padx=(10, 2))
        self.search_var = tk.StringVar()
        self.search_entry = tk.Entry(history_toolbar, textvariable=self.search_var, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=2)
        tk.Button(history_toolbar, text="搜索", command=self.search_history).pack(side=tk.LEFT, padx=2)

        history_list_frame = tk.Frame(self.history_frame)
        history_list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        columns = ("时间", "问题", "答案摘要")
        self.history_tree = ttk.Treeview(history_list_frame, columns=columns, show="headings", height=15)
        for col in columns:
            self.history_tree.heading(col, text=col)
            self.history_tree.column(col, width=150)
        self.history_tree.column("问题", width=250)
        self.history_tree.column("答案摘要", width=300)

        scrollbar = ttk.Scrollbar(history_list_frame, orient="vertical", command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=scrollbar.set)
        self.history_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.history_tree.bind("<Double-1>", self.show_history_detail)

        self.status_label = tk.Label(self, text="就绪", bd=1, relief=tk.SUNKEN, anchor=tk.W)
        self.status_label.pack(side=tk.BOTTOM, fill=tk.X)

    def save_api_key(self):
        """保存 API 密钥"""
        api_key = self.api_key_var.get().strip()
        if api_key:
            gpt_client.set_api_key(api_key)
            messagebox.showinfo("成功", "API 密钥已保存")
        else:
            messagebox.showwarning("警告", "请输入 API 密钥")

    def toggle_api_key(self):
        """切换 API 密钥显示/隐藏"""
        current_show = self.api_entry.cget("show")
        self.api_entry.config(show="" if current_show == "*" else "*")

    def ask_gpt(self):
        """向 GPT 提问"""
        question = self.question_text.get(1.0, tk.END).strip()
        if not question:
            messagebox.showwarning("警告", "请输入问题")
            return
        if not gpt_client.api_key:
            messagebox.showwarning("警告", "请先设置 API 密钥")
            return

        code_context = self.code_editor.get_code() if self.include_code_var.get() else ""
        self.ask_button.config(state=tk.DISABLED, text="处理中...")
        self.status_label.config(text="正在向 GPT 发送请求...")
        self.update()

        def update_answer(answer):
            self.answer_text.delete(1.0, tk.END)
            self.answer_text.insert(1.0, answer)
            db.save_query(question, answer, code_context)
            self.ask_button.config(state=tk.NORMAL, text="获取答案")
            self.status_label.config(text=f"完成 - {datetime.now().strftime('%H:%M:%S')}")
            self.load_history()

        gpt_client.ask_question(question=question, code_context=code_context, callback=update_answer)

    def clear_question(self):
        """清除问题"""
        self.question_text.delete(1.0, tk.END)

    def load_history(self):
        """加载历史记录"""
        for item in self.history_tree.get_children():
            self.history_tree.delete(item)
        history = db.get_query_history(limit=100)
        for record in history:
            created_at = datetime.strptime(record['created_at'], '%Y-%m-%d %H:%M:%S')
            time_str = created_at.strftime('%m-%d %H:%M')
            question_summary = record['question'][:50] + "..." if len(record['question']) > 50 else record['question']
            answer_summary = record['answer'][:50] + "..." if len(record['answer']) > 50 else record['answer']
            self.history_tree.insert("", tk.END, values=(time_str, question_summary, answer_summary), tags=(record['id'],))

    def search_history(self):
        """搜索历史记录"""
        keyword = self.search_var.get().strip()
        if not keyword:
            self.load_history()
            return
        for item in self.history_tree.get_children():
            self.history_tree.delete(item)
        results = db.search_history(keyword)
        for record in results:
            created_at = datetime.strptime(record['created_at'], '%Y-%m-%d %H:%M:%S')
            time_str = created_at.strftime('%m-%d %H:%M')
            question_summary = record['question'][:50] + "..." if len(record['question']) > 50 else record['question']
            answer_summary = record['answer'][:50] + "..." if len(record['answer']) > 50 else record['answer']
            self.history_tree.insert("", tk.END, values=(time_str, question_summary, answer_summary), tags=(record['id'],))

    def clear_history(self):
        """清空历史记录"""
        if messagebox.askyesno("确认", "确定要清空所有历史记录吗?"):
            db.clear_history()
            self.load_history()

    def show_history_detail(self, event):
        """显示历史记录详情"""
        selection = self.history_tree.selection()
        if not selection:
            return
        item = self.history_tree.item(selection[0])
        record_id = item['tags'][0]
        history = db.get_query_history(limit=1000)
        record = next((r for r in history if r['id'] == int(record_id)), None)
        if record:
            detail_window = tk.Toplevel(self)
            detail_window.title(f"历史记录详情 - {record['created_at']}")
            detail_window.geometry("800x600")
            notebook = ttk.Notebook(detail_window)
            notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

            question_frame = tk.Frame(notebook)
            notebook.add(question_frame, text="问题")
            question_text = scrolledtext.ScrolledText(question_frame, wrap=tk.WORD)
            question_text.insert(1.0, record['question'])
            question_text.config(state=tk.DISABLED)
            question_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

            answer_frame = tk.Frame(notebook)
            notebook.add(answer_frame, text="答案")
            answer_text = scrolledtext.ScrolledText(answer_frame, wrap=tk.WORD)
            answer_text.insert(1.0, record['answer'])
            answer_text.config(state=tk.DISABLED)
            answer_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

            if record['code_context']:
                code_frame = tk.Frame(notebook)
                notebook.add(code_frame, text="代码上下文")
                code_text = scrolledtext.ScrolledText(code_frame, wrap=tk.WORD)
                code_text.insert(1.0, record['code_context'])
                code_text.config(state=tk.DISABLED)
                code_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

            button_frame = tk.Frame(detail_window)
            button_frame.pack(fill=tk.X, padx=10, pady=5)
            tk.Button(button_frame, text="复制答案", command=lambda: self.copy_to_clipboard(record['answer'])).pack(side=tk.LEFT, padx=5)
            tk.Button(button_frame, text="重新提问", command=lambda: self.reuse_question(record['question'])).pack(side=tk.LEFT, padx=5)
            tk.Button(button_frame, text="删除记录", command=lambda: self.delete_record(record_id, detail_window)).pack(side=tk.LEFT, padx=5)

    def copy_to_clipboard(self, text):
        """复制文本到剪贴板"""
        self.clipboard_clear()
        self.clipboard_append(text)
        messagebox.showinfo("成功", "已复制到剪贴板")

    def reuse_question(self, question):
        """重新使用问题"""
        self.question_text.delete(1.0, tk.END)
        self.question_text.insert(1.0, question)
        self.notebook.select(0)

    def delete_record(self, record_id, window):
        """删除记录"""
        if messagebox.askyesno("确认", "确定要删除这条记录吗?"):
            db.delete_query(int(record_id))
            self.load_history()
            window.destroy()

class PythonIDE(tk.Tk):
    """Python IDE 主窗口"""
    def __init__(self):
        super().__init__()
        self.title("Python IDE with GPT Assistant")
        self.geometry("1400x800")
        try:
            self.iconbitmap('icon.ico')
        except:
            pass
        self.current_theme = db.get_setting('theme') or 'light'
        self.create_menu()
        self.create_main_interface()
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

    def create_menu(self):
        """创建菜单栏"""
        menubar = tk.Menu(self)
        self.config(menu=menubar)
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="文件", menu=file_menu)
        file_menu.add_command(label="新建", command=self.new_file)
        file_menu.add_command(label="打开", command=self.open_file)
        file_menu.add_command(label="保存", command=self.save_file)
        file_menu.add_command(label="另存为", command=self.save_as_file)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.quit)

        edit_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="编辑", menu=edit_menu)
        edit_menu.add_command(label="撤销", command=self.undo)
        edit_menu.add_command(label="重做", command=self.redo)
        edit_menu.add_separator()
        edit_menu.add_command(label="剪切", command=self.cut)
        edit_menu.add_command(label="复制", command=self.copy)
        edit_menu.add_command(label="粘贴", command=self.paste)
        edit_menu.add_separator()
        edit_menu.add_command(label="全选", command=self.select_all)

        run_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="运行", menu=run_menu)
        run_menu.add_command(label="运行代码", command=self.run_code)
        run_menu.add_command(label="调试", command=self.debug_code)

        tools_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="工具", menu=tools_menu)
        tools_menu.add_command(label="设置", command=self.open_settings)
        tools_menu.add_command(label="代码格式化", command=self.format_code)

        theme_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="主题", menu=theme_menu)
        theme_menu.add_command(label="浅色主题", command=lambda: self.change_theme('light'))
        theme_menu.add_command(label="深色主题", command=lambda: self.change_theme('dark'))

        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="帮助", menu=help_menu)
        help_menu.add_command(label="使用说明", command=self.show_help)
        help_menu.add_command(label="关于", command=self.show_about)

    def create_main_interface(self):
        """创建主界面"""
        paned_window = tk.PanedWindow(self, orient=tk.HORIZONTAL, sashwidth=5, sashrelief=tk.RAISED)
        paned_window.pack(fill=tk.BOTH, expand=True)

        left_frame = tk.Frame(paned_window)
        paned_window.add(left_frame, width=700)
        self.code_editor = CodeEditor(left_frame)
        self.code_editor.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        run_button = tk.Button(left_frame, text="运行代码 (F5)", command=self.run_code, bg="#28a745", fg="white", font=("Arial", 11, "bold"), height=2)
        run_button.pack(fill=tk.X, padx=5, pady=5)

        right_frame = tk.Frame(paned_window)
        paned_window.add(right_frame, width=700)
        self.gpt_assistant = GPTAssistant(right_frame, self.code_editor)
        self.gpt_assistant.pack(fill=tk.BOTH, expand=True)

        self.apply_theme()

    def apply_theme(self):
        """应用主题"""
        colors = COLORS[self.current_theme]
        self.config(bg=colors['bg'])
        self.code_editor.text_widget.config(bg=colors['editor_bg'], fg=colors['fg'], insertbackground=colors['fg'])
        self.gpt_assistant.answer_text.config(bg=colors['tab_bg'], fg=colors['fg'])
        self.gpt_assistant.question_text.config(bg=colors['tab_bg'], fg=colors['fg'])

    def change_theme(self, theme):
        """切换主题"""
        self.current_theme = theme
        db.update_setting('theme', theme)
        self.apply_theme()

    def run_code(self):
        """运行代码"""
        code = self.code_editor.get_code()
        if not code.strip():
            messagebox.showwarning("警告", "代码为空")
            return
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as tmp:
            tmp.write(code)
            tmp_path = tmp.name
        try:
            result = subprocess.run([sys.executable, tmp_path], capture_output=True, text=True, timeout=10)
            output = f"返回值:{result.returncode}\n\n"
            if result.stdout:
                output += f"标准输出:\n{result.stdout}\n"
            if result.stderr:
                output += f"标准错误:\n{result.stderr}\n"
            self.gpt_assistant.answer_text.delete(1.0, tk.END)
            self.gpt_assistant.answer_text.insert(1.0, output)
            self.gpt_assistant.notebook.select(0)
        except subprocess.TimeoutExpired:
            messagebox.showerror("错误", "代码执行超时")
        except Exception as e:
            messagebox.showerror("错误", f"执行错误:{str(e)}")
        finally:
            try:
                os.unlink(tmp_path)
            except:
                pass

    def debug_code(self):
        """调试代码(简单实现)"""
        code = self.code_editor.get_code()
        debug_window = tk.Toplevel(self)
        debug_window.title("调试输出")
        debug_window.geometry("600x400")
        text_widget = scrolledtext.ScrolledText(debug_window)
        text_widget.pack(fill=tk.BOTH, expand=True)
        import io
        import contextlib
        output = io.StringIO()
        try:
            with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
                exec(code, {})
            text_widget.insert(1.0, output.getvalue())
        except Exception as e:
            text_widget.insert(1.0, f"错误:{str(e)}\n")

    def open_settings(self):
        """打开设置窗口"""
        settings_window = tk.Toplevel(self)
        settings_window.title("设置")
        settings_window.geometry("500x400")
        api_frame = tk.LabelFrame(settings_window, text="API 设置", padx=10, pady=10)
        api_frame.pack(fill=tk.X, padx=10, pady=10)
        tk.Label(api_frame, text="OpenAI API 密钥:").grid(row=0, column=0, sticky=tk.W, pady=5)
        api_key_var = tk.StringVar(value=db.get_setting('api_key') or "")
        api_entry = tk.Entry(api_frame, textvariable=api_key_var, width=40)
        api_entry.grid(row=0, column=1, pady=5, padx=5)
        tk.Label(api_frame, text="API URL:").grid(row=1, column=0, sticky=tk.W, pady=5)
        api_url_var = tk.StringVar(value=db.get_setting('api_url') or "https://api.openai.com/v1/chat/completions")
        tk.Entry(api_frame, textvariable=api_url_var, width=40).grid(row=1, column=1, pady=5, padx=5)

        editor_frame = tk.LabelFrame(settings_window, text="编辑器设置", padx=10, pady=10)
        editor_frame.pack(fill=tk.X, padx=10, pady=10)
        tk.Label(editor_frame, text="字体大小:").grid(row=0, column=0, sticky=tk.W, pady=5)
        font_size_var = tk.StringVar(value=db.get_setting('font_size') or "12")
        tk.Spinbox(editor_frame, from_=8, to=24, textvariable=font_size_var, width=10).grid(row=0, column=1, pady=5, padx=5)

        def save_settings():
            db.update_setting('api_key', api_key_var.get())
            db.update_setting('api_url', api_url_var.get())
            db.update_setting('font_size', font_size_var.get())
            gpt_client.set_api_key(api_key_var.get())
            gpt_client.set_api_url(api_url_var.get())
            self.code_editor.text_widget.config(font=("Consolas", int(font_size_var.get())))
            messagebox.showinfo("成功", "设置已保存")
            settings_window.destroy()

        tk.Button(settings_window, text="保存设置", command=save_settings, bg="#007acc", fg="white", width=15).pack(pady=20)

    def format_code(self):
        """格式化代码(简单实现)"""
        code = self.code_editor.get_code()
        try:
            import autopep8
            formatted_code = autopep8.fix_code(code)
            self.code_editor.set_code(formatted_code)
        except ImportError:
            lines = code.split('\n')
            formatted_lines = []
            indent = 0
            for line in lines:
                line_stripped = line.strip()
                if line_stripped.endswith(':'):
                    formatted_lines.append(' ' * indent + line_stripped)
                    indent += 4
                elif line_stripped and line_stripped[0] != '#':
                    if any(line_stripped.startswith(keyword) for keyword in ['elif ', 'else:', 'except ', 'finally:']):
                        indent = max(0, indent - 4)
                    formatted_lines.append(' ' * indent + line_stripped)
                else:
                    formatted_lines.append(' ' * indent + line_stripped)
            self.code_editor.set_code('\n'.join(formatted_lines))

    def new_file(self):
        """新建文件"""
        self.code_editor.new_file()

    def open_file(self):
        """打开文件"""
        self.code_editor.open_file()

    def save_file(self):
        """保存文件"""
        self.code_editor.save_file()

    def save_as_file(self):
        """另存为文件"""
        self.code_editor.save_as_file()

    def undo(self):
        """撤销"""
        self.code_editor.undo()

    def redo(self):
        """重做"""
        self.code_editor.redo()

    def cut(self):
        """剪切"""
        self.code_editor.text_widget.event_generate("<<Cut>>")

    def copy(self):
        """复制"""
        self.code_editor.text_widget.event_generate("<<Copy>>")

    def paste(self):
        """粘贴"""
        self.code_editor.text_widget.event_generate("<<Paste>>")

    def select_all(self):
        """全选"""
        self.code_editor.text_widget.tag_add(tk.SEL, "1.0", tk.END)
        self.code_editor.text_widget.mark_set(tk.INSERT, "1.0")
        self.code_editor.text_widget.see(tk.INSERT)
        return 'break'

    def show_help(self):
        """显示使用说明"""
        help_text = """使用说明:
1. 代码编辑区:
   - 左侧是 Python 代码编辑器,支持语法高亮
   - 使用菜单或快捷键进行文件操作
   - 点击'运行代码'按钮执行当前代码
2. GPT 助手区:
   - 右侧是 GPT 助手面板
   - 在'问题'框中输入您的问题
   - 点击'获取答案'按钮向 GPT 提问
   - 答案会显示在'答案'标签页中
   - 历史记录保存在'历史'标签页中
3. 设置:
   - 在'工具'菜单中打开设置
   - 配置 API 密钥和 URL
   - 切换浅色/深色主题
4. 快捷键:
   - Ctrl+S: 保存文件
   - Ctrl+O: 打开文件
   - Ctrl+N: 新建文件
   - Ctrl+Z: 撤销
   - Ctrl+Y: 重做
   - F5: 运行代码"""
        help_window = tk.Toplevel(self)
        help_window.title("使用说明")
        help_window.geometry("600x500")
        text_widget = scrolledtext.ScrolledText(help_window, wrap=tk.WORD)
        text_widget.insert(1.0, help_text)
        text_widget.config(state=tk.DISABLED)
        text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

    def show_about(self):
        """显示关于信息"""
        about_text = """Python IDE with GPT Assistant v1.0
功能特点:
- 集成 Python 代码编辑器
- GPT 智能助手
- 代码执行和调试
- 历史记录管理
- 多主题支持
作者:Your Name
联系:[email protected]"""
        messagebox.showinfo("关于", about_text)

    def on_closing(self):
        """窗口关闭事件"""
        if messagebox.askokcancel("退出", "确定要退出程序吗?"):
            db.update_setting('theme', self.current_theme)
            self.destroy()

def main():
    """主函数"""
    app = PythonIDE()
    app.mainloop()

if __name__ == "__main__":
    main()

使用方法

步骤操作
安装依赖pip install requests autopep8
运行程序python main.py
配置 API 密钥在程序右侧的 API 密钥输入框中输入您的 OpenAI API 密钥,点击'保存'按钮
使用流程1. 在左侧编写 Python 代码
2. 点击'运行代码'按钮执行
3. 在右侧输入问题,点击'获取答案'按钮向 GPT 提问
4. 查看历史记录中的问答历史

功能特点

  • 代码编辑器:支持语法高亮、文件操作(新建、打开、保存)、查找替换功能、撤销/重做。
  • GPT 助手:集成 OpenAI GPT API,支持代码上下文,历史记录管理,搜索功能。
  • 界面优化:左右分栏设计,海蓝色主题按钮,深浅色主题切换,响应式布局。
  • 数据库:SQLite 存储历史记录,持久化设置,快速搜索查询。

该程序提供了一个完整的 Python 开发环境,集成了 GPT 智能助手,非常适合学习和开发使用。

目录

  1. Python 调用本地 DocsGPT API 构建 GUI 开发工具
  2. 项目概述
  3. 核心功能
  4. 界面设计
  5. 主要更新说明
  6. 支持两种 API 类型
  7. DocsGPT 特定功能
  8. 改进的错误处理
  9. 配置管理
  10. 兼容性
  11. 项目结构
  12. 1. requirements.txt
  13. 2. database.py - SQLite 数据库操作
  14. 全局数据库实例
  15. 3. gpt_client.py - GPT API 客户端
  16. 全局 GPT 客户端实例
  17. 4. main.py - 主 GUI 程序
  18. 颜色配置
  19. 使用方法
  20. 功能特点

更多推荐文章

查看全部
  • 小米智能家居 Miloco 分离式部署指南
  • 文心一言大模型本地部署与微调应用实战
  • 基于 n8n 与 MaaS 平台的智能写作工作流实战指南
  • OpenClaw 赋能机器人,AI 代理实现具身智能与远程操控
  • AIGC 赋能现代教育:自动化课件与个性化学习实践
  • AR/VR与边缘计算融合下的测试扩展策略
  • 云原生算力调度:基于 Kubernetes 的智能资源分配
  • Vue 开源 AI 低代码表单设计器 FcDesigner 使用指南
  • 宇树 G1 人形机器人强化学习训练配置与奖励函数解析
  • Dify:开源 LLM 应用开发平台功能与部署指南
  • Git 下载、安装与环境配置指南
  • VS Code + WSL 环境下 GitHub 访问与 Copilot 连接问题解决方案
  • TypeScript 核心概念与高频面试题详解
  • 基于 Higress MCP Server 插件转换 REST API 为 AI 工具
  • DeepSeek-OCR-WEBUI 镜像实现 Mac 本地化 OCR 全流程
  • VLSI 设计中的斯坦纳树算法演进:从 FLUTE 到现代布线
  • Docker 数据挂载:Managed Volume 使用详解
  • 字节大模型校招薪资解析:50K*15 背后的行业逻辑
  • Linux 系统连接 GitHub 的 SSH 配置指南
  • Go 语言结合 DeepSeek 大模型构建智能监控系统

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online