Files
LocalAgent/ui/settings_view.py
Mimikko-zeus 68f4f01cd7 feat:增强需求澄清与任务管理功能
更新了 .env.example,新增聊天模型配置,以提升对话处理能力。
增强了 README.md,反映了包括需求澄清、代码复用和自动重试在内的新功能。
重构了 agent.py,以支持多模型交互,并为无法在本地执行的任务新增了引导处理逻辑。
改进了 SandboxRunner,增加了任务执行成功校验,并加入了工作区清理功能。

扩展了 HistoryManager,支持任务摘要生成以及记录的批量删除。
优化了 chat_view.py 和 history_view.py 中的 UI 组件,提升用户体验,包括 Markdown 渲染和任务管理选项。
2026-01-07 12:35:27 +08:00

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