更新了 .env.example,新增聊天模型配置,以提升对话处理能力。 增强了 README.md,反映了包括需求澄清、代码复用和自动重试在内的新功能。 重构了 agent.py,以支持多模型交互,并为无法在本地执行的任务新增了引导处理逻辑。 改进了 SandboxRunner,增加了任务执行成功校验,并加入了工作区清理功能。 扩展了 HistoryManager,支持任务摘要生成以及记录的批量删除。 优化了 chat_view.py 和 history_view.py 中的 UI 组件,提升用户体验,包括 Markdown 渲染和任务管理选项。
371 lines
12 KiB
Python
371 lines
12 KiB
Python
"""
|
|
设置视图
|
|
用于配置 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[[], 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("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
canvas.bind("<Configure>", configure_scroll)
|
|
|
|
# 鼠标滚轮支持
|
|
def on_mousewheel(event):
|
|
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
|
canvas.bind_all("<MouseWheel>", 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
|
|
|
|
messagebox.showinfo("成功", "配置已保存!")
|
|
|
|
if self.on_save:
|
|
self.on_save()
|
|
|
|
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
|
|
|