""" 设置视图 用于配置 API 和模型参数 """ import os import tkinter as tk from tkinter import ttk, messagebox from pathlib import Path from typing import Callable, Optional, Dict, Any from dotenv import load_dotenv, set_key class SettingsView: """ 设置视图 功能: - 配置 API URL 和 Key - 配置各功能使用的模型 - 保存配置到 .env 文件 """ # 预设模型列表 PRESET_MODELS = [ "Qwen/Qwen2.5-7B-Instruct", "Qwen/Qwen2.5-14B-Instruct", "Qwen/Qwen2.5-32B-Instruct", "Qwen/Qwen2.5-72B-Instruct", "Qwen/Qwen2.5-Coder-7B-Instruct", "Qwen/Qwen2.5-Coder-32B-Instruct", "deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-R1", "Pro/deepseek-ai/DeepSeek-R1", "THUDM/glm-4-9b-chat", "01-ai/Yi-1.5-9B-Chat-16K", ] def __init__( self, parent: tk.Widget, env_path: Path, on_save: Optional[Callable[[bool], None]] = None, on_back: Optional[Callable[[], None]] = None ): self.parent = parent self.env_path = env_path self.on_save = on_save self.on_back = on_back # 配置变量 self.vars: Dict[str, tk.StringVar] = {} # 创建主框架 self.frame = tk.Frame(parent, bg='#1e1e1e') self._create_ui() self._load_config() def _create_ui(self) -> None: """创建 UI""" # 标题栏 header = tk.Frame(self.frame, bg='#2d2d2d') header.pack(fill=tk.X, pady=(0, 20)) # 返回按钮 back_btn = tk.Button( header, text="← 返回", font=('Microsoft YaHei UI', 10), bg='#3d3d3d', fg='#ffffff', activebackground='#4d4d4d', activeforeground='#ffffff', relief=tk.FLAT, cursor='hand2', command=self._on_back_click ) back_btn.pack(side=tk.LEFT, padx=10, pady=10) # 标题 title = tk.Label( header, text="⚙️ 设置", font=('Microsoft YaHei UI', 16, 'bold'), bg='#2d2d2d', fg='#ffffff' ) title.pack(side=tk.LEFT, padx=20, pady=10) # 滚动区域 canvas = tk.Canvas(self.frame, bg='#1e1e1e', highlightthickness=0) scrollbar = ttk.Scrollbar(self.frame, orient=tk.VERTICAL, command=canvas.yview) self.content_frame = tk.Frame(canvas, bg='#1e1e1e') canvas.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=20) canvas_window = canvas.create_window((0, 0), window=self.content_frame, anchor=tk.NW) def configure_scroll(event): canvas.configure(scrollregion=canvas.bbox("all")) canvas.itemconfig(canvas_window, width=event.width) self.content_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.bind("", configure_scroll) # 鼠标滚轮支持 def on_mousewheel(event): canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") canvas.bind_all("", on_mousewheel) # API 配置区 self._create_section("API 配置", [ ("LLM_API_URL", "API 地址", "https://api.siliconflow.cn/v1/chat/completions", False), ("LLM_API_KEY", "API Key", "", True), ]) # 模型配置区 self._create_model_section("模型配置", [ ("INTENT_MODEL_NAME", "意图识别模型", "用于判断用户输入是对话还是执行任务(推荐小模型)"), ("CHAT_MODEL_NAME", "对话模型", "用于普通对话回复(推荐中等模型)"), ("GENERATION_MODEL_NAME", "代码生成模型", "用于生成执行计划和代码(推荐大模型)"), ]) # 保存按钮 btn_frame = tk.Frame(self.content_frame, bg='#1e1e1e') btn_frame.pack(fill=tk.X, pady=30) save_btn = tk.Button( btn_frame, text="💾 保存配置", font=('Microsoft YaHei UI', 12, 'bold'), bg='#0e639c', fg='#ffffff', activebackground='#1177bb', activeforeground='#ffffff', relief=tk.FLAT, cursor='hand2', padx=30, pady=10, command=self._save_config ) save_btn.pack() # 提示信息 tip = tk.Label( self.content_frame, text="提示:保存后配置立即生效,无需重启应用", font=('Microsoft YaHei UI', 9), bg='#1e1e1e', fg='#808080' ) tip.pack(pady=(0, 20)) def _create_section(self, title: str, fields: list) -> None: """创建配置区域""" # 区域标题 section_title = tk.Label( self.content_frame, text=title, font=('Microsoft YaHei UI', 12, 'bold'), bg='#1e1e1e', fg='#569cd6', anchor=tk.W ) section_title.pack(fill=tk.X, pady=(20, 10)) # 分隔线 separator = tk.Frame(self.content_frame, bg='#3d3d3d', height=1) separator.pack(fill=tk.X, pady=(0, 15)) # 字段 for key, label, default, is_password in fields: self._create_field(key, label, default, is_password) def _create_field(self, key: str, label: str, default: str, is_password: bool = False) -> None: """创建输入字段""" field_frame = tk.Frame(self.content_frame, bg='#1e1e1e') field_frame.pack(fill=tk.X, pady=8) # 标签 lbl = tk.Label( field_frame, text=label, font=('Microsoft YaHei UI', 10), bg='#1e1e1e', fg='#cccccc', width=12, anchor=tk.W ) lbl.pack(side=tk.LEFT) # 输入框 var = tk.StringVar(value=default) self.vars[key] = var entry = tk.Entry( field_frame, textvariable=var, font=('Consolas', 10), bg='#3c3c3c', fg='#ffffff', insertbackground='#ffffff', relief=tk.FLAT, show='*' if is_password else '' ) entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 0), ipady=5) # 显示/隐藏密码按钮 if is_password: self._is_password_visible = False def toggle_password(): self._is_password_visible = not self._is_password_visible entry.config(show='' if self._is_password_visible else '*') toggle_btn.config(text='🙈' if self._is_password_visible else '👁') toggle_btn = tk.Button( field_frame, text='👁', font=('Segoe UI Emoji', 10), bg='#3c3c3c', fg='#ffffff', activebackground='#4c4c4c', activeforeground='#ffffff', relief=tk.FLAT, cursor='hand2', command=toggle_password ) toggle_btn.pack(side=tk.LEFT, padx=(5, 0)) def _create_model_section(self, title: str, models: list) -> None: """创建模型配置区域""" # 区域标题 section_title = tk.Label( self.content_frame, text=title, font=('Microsoft YaHei UI', 12, 'bold'), bg='#1e1e1e', fg='#569cd6', anchor=tk.W ) section_title.pack(fill=tk.X, pady=(30, 10)) # 分隔线 separator = tk.Frame(self.content_frame, bg='#3d3d3d', height=1) separator.pack(fill=tk.X, pady=(0, 15)) # 模型字段 for key, label, description in models: self._create_model_field(key, label, description) def _create_model_field(self, key: str, label: str, description: str) -> None: """创建模型选择字段""" field_frame = tk.Frame(self.content_frame, bg='#1e1e1e') field_frame.pack(fill=tk.X, pady=10) # 标签和描述 label_frame = tk.Frame(field_frame, bg='#1e1e1e') label_frame.pack(fill=tk.X) lbl = tk.Label( label_frame, text=label, font=('Microsoft YaHei UI', 10, 'bold'), bg='#1e1e1e', fg='#cccccc', anchor=tk.W ) lbl.pack(side=tk.LEFT) desc = tk.Label( label_frame, text=f" ({description})", font=('Microsoft YaHei UI', 9), bg='#1e1e1e', fg='#808080', anchor=tk.W ) desc.pack(side=tk.LEFT) # 下拉框 + 输入框组合 input_frame = tk.Frame(field_frame, bg='#1e1e1e') input_frame.pack(fill=tk.X, pady=(5, 0)) var = tk.StringVar() self.vars[key] = var # 使用 Combobox 支持下拉选择和自定义输入 combo = ttk.Combobox( input_frame, textvariable=var, values=self.PRESET_MODELS, font=('Consolas', 10), state='normal' # 允许自定义输入 ) combo.pack(fill=tk.X, ipady=3) # 设置样式 style = ttk.Style() style.configure('TCombobox', fieldbackground='#3c3c3c', background='#3c3c3c') def _load_config(self) -> None: """从 .env 文件加载配置""" load_dotenv(self.env_path, override=True) # 加载各配置项 config_keys = [ ("LLM_API_URL", "https://api.siliconflow.cn/v1/chat/completions"), ("LLM_API_KEY", ""), ("INTENT_MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct"), ("CHAT_MODEL_NAME", "Qwen/Qwen2.5-32B-Instruct"), ("GENERATION_MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct"), ] for key, default in config_keys: value = os.getenv(key, default) if key in self.vars: self.vars[key].set(value if value else default) def _save_config(self) -> None: """保存配置到 .env 文件""" try: # 验证必填项 api_key = self.vars["LLM_API_KEY"].get().strip() if not api_key or api_key == "your_api_key_here": messagebox.showwarning("提示", "请填写有效的 API Key") return # 确保 .env 文件存在 if not self.env_path.exists(): self.env_path.touch() # 保存各配置项 for key, var in self.vars.items(): value = var.get().strip() set_key(str(self.env_path), key, value) # 同时更新环境变量 os.environ[key] = value # 重置 LLM 客户端单例,强制使用新配置 from llm.client import reset_client, test_connection reset_client() # 进行连通性测试 messagebox.showinfo("提示", "配置已保存,正在测试连接...") success, message = test_connection(timeout=15) # 记录配置变更度量 from llm.config_metrics import get_config_metrics metrics = get_config_metrics(self.env_path.parent / "workspace") metrics.mark_config_changed(connection_test_success=success) if success: messagebox.showinfo("成功", f"配置已保存并生效!\n\n{message}") else: messagebox.showwarning( "配置已保存", f"配置已保存,但连接测试失败:\n\n{message}\n\n请检查配置是否正确。" ) if self.on_save: self.on_save(success) except Exception as e: messagebox.showerror("错误", f"保存配置失败: {str(e)}") def _on_back_click(self) -> None: """返回按钮点击""" if self.on_back: self.on_back() def show(self) -> None: """显示视图""" self._load_config() # 重新加载配置 self.frame.pack(fill=tk.BOTH, expand=True) def hide(self) -> None: """隐藏视图""" self.frame.pack_forget() def get_frame(self) -> tk.Frame: """获取主框架""" return self.frame