feat: refactor API key configuration and enhance application initialization

- Renamed `check_environment` to `check_api_key_configured` for clarity, simplifying the API key validation logic.
- Removed the blocking behavior of the API key check during application startup, allowing the app to run while providing a prompt for configuration.
- Updated `LocalAgentApp` to accept an `api_configured` parameter, enabling conditional messaging for API key setup.
- Enhanced the `SandboxRunner` to support backup management and improved execution result handling with detailed metrics.
- Integrated data governance strategies into the `HistoryManager`, ensuring compliance and improved data management.
- Added privacy settings and metrics tracking across various components to enhance user experience and application safety.
This commit is contained in:
Mimikko-zeus
2026-02-27 14:32:30 +08:00
parent ab5bbff6f7
commit 8a538bb950
58 changed files with 13457 additions and 350 deletions

View File

@@ -396,6 +396,24 @@ class ChatView:
)
self.settings_btn.pack(side=tk.RIGHT, padx=(5, 0))
# 隐私设置按钮(将在外部设置回调)
self.on_show_privacy = None
self.privacy_btn = tk.Button(
btn_container,
text="🔒 隐私",
font=('Microsoft YaHei UI', 10),
bg='#424242',
fg='#a5d6a7',
activebackground='#616161',
activeforeground='#a5d6a7',
relief=tk.FLAT,
padx=10,
pady=3,
cursor='hand2',
command=lambda: self.on_show_privacy() if self.on_show_privacy else None
)
self.privacy_btn.pack(side=tk.RIGHT, padx=(5, 0))
# 历史记录按钮
if self.on_show_history:
self.history_btn = tk.Button(

192
ui/clear_confirm_dialog.py Normal file
View File

@@ -0,0 +1,192 @@
"""
清理确认对话框
在清空工作区前显示确认对话框,支持备份和恢复
"""
import tkinter as tk
from tkinter import ttk
from typing import Callable, Optional
class ClearConfirmDialog:
"""
清理确认对话框
功能:
1. 显示当前工作区内容统计
2. 提供"清空并备份""仅清空""取消"选项
3. 显示最近的备份信息
"""
def __init__(
self,
parent: tk.Tk,
file_count: int,
total_size: str,
has_recent_backup: bool,
on_confirm: Callable[[bool], None], # 参数:是否创建备份
on_cancel: Callable[[], None]
):
self.parent = parent
self.file_count = file_count
self.total_size = total_size
self.has_recent_backup = has_recent_backup
self.on_confirm = on_confirm
self.on_cancel = on_cancel
self.dialog = None
self.result = None
def show(self):
"""显示对话框"""
self.dialog = tk.Toplevel(self.parent)
self.dialog.title("确认清空工作区")
self.dialog.geometry("500x300")
self.dialog.resizable(False, False)
# 居中显示
self.dialog.transient(self.parent)
self.dialog.grab_set()
# 主容器
main_frame = ttk.Frame(self.dialog, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# 警告图标和标题
title_frame = ttk.Frame(main_frame)
title_frame.pack(fill=tk.X, pady=(0, 15))
warning_label = ttk.Label(
title_frame,
text="⚠️",
font=("Segoe UI Emoji", 24)
)
warning_label.pack(side=tk.LEFT, padx=(0, 10))
title_label = ttk.Label(
title_frame,
text="即将清空工作区",
font=("Microsoft YaHei UI", 14, "bold")
)
title_label.pack(side=tk.LEFT)
# 内容统计
info_frame = ttk.LabelFrame(main_frame, text="当前工作区内容", padding="10")
info_frame.pack(fill=tk.X, pady=(0, 15))
info_text = f"• 文件数量:{self.file_count}\n• 总大小:{self.total_size}"
info_label = ttk.Label(
info_frame,
text=info_text,
font=("Microsoft YaHei UI", 10)
)
info_label.pack(anchor=tk.W)
# 备份提示
if self.has_recent_backup:
backup_hint = ttk.Label(
main_frame,
text="💡 提示:检测到最近的备份,您可以随时恢复",
font=("Microsoft YaHei UI", 9),
foreground="#666666"
)
backup_hint.pack(fill=tk.X, pady=(0, 15))
# 说明文字
desc_label = ttk.Label(
main_frame,
text="清空后input 和 output 目录中的所有文件将被删除。\n建议选择\"清空并备份\"以便后续恢复。",
font=("Microsoft YaHei UI", 9),
foreground="#666666",
wraplength=450
)
desc_label.pack(fill=tk.X, pady=(0, 20))
# 按钮区域
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X)
# 取消按钮
cancel_btn = ttk.Button(
button_frame,
text="取消",
command=self._on_cancel,
width=12
)
cancel_btn.pack(side=tk.RIGHT, padx=(5, 0))
# 仅清空按钮
clear_only_btn = ttk.Button(
button_frame,
text="仅清空(不备份)",
command=self._on_clear_only,
width=15
)
clear_only_btn.pack(side=tk.RIGHT, padx=(5, 0))
# 清空并备份按钮(推荐)
clear_backup_btn = ttk.Button(
button_frame,
text="清空并备份(推荐)",
command=self._on_clear_with_backup,
width=18
)
clear_backup_btn.pack(side=tk.RIGHT)
# 设置默认焦点
clear_backup_btn.focus_set()
# 绑定 ESC 键
self.dialog.bind("<Escape>", lambda e: self._on_cancel())
# 等待对话框关闭
self.dialog.wait_window()
def _on_clear_with_backup(self):
"""清空并备份"""
self.result = "backup"
self.dialog.destroy()
self.on_confirm(True)
def _on_clear_only(self):
"""仅清空"""
self.result = "clear"
self.dialog.destroy()
self.on_confirm(False)
def _on_cancel(self):
"""取消"""
self.result = "cancel"
self.dialog.destroy()
self.on_cancel()
def show_clear_confirm_dialog(
parent: tk.Tk,
file_count: int,
total_size: str,
has_recent_backup: bool,
on_confirm: Callable[[bool], None],
on_cancel: Callable[[], None]
):
"""
显示清理确认对话框
Args:
parent: 父窗口
file_count: 文件数量
total_size: 总大小(格式化字符串)
has_recent_backup: 是否有最近的备份
on_confirm: 确认回调(参数:是否创建备份)
on_cancel: 取消回调
"""
dialog = ClearConfirmDialog(
parent=parent,
file_count=file_count,
total_size=total_size,
has_recent_backup=has_recent_backup,
on_confirm=on_confirm,
on_cancel=on_cancel
)
dialog.show()

338
ui/governance_panel.py Normal file
View File

@@ -0,0 +1,338 @@
"""
数据治理监控面板
提供可视化的治理指标展示和管理操作
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from pathlib import Path
from typing import Optional
from history.manager import HistoryManager
from history.data_governance import GovernanceMetrics
class GovernancePanel:
"""
数据治理监控面板
显示治理指标、执行清理操作、导出数据
"""
def __init__(self, parent: tk.Widget, history_manager: HistoryManager):
self.parent = parent
self.history = history_manager
self.frame = None
self._create_widgets()
def _create_widgets(self):
"""创建 UI 组件"""
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
# 标题
title_frame = tk.Frame(self.frame, bg='#1e1e1e')
title_frame.pack(fill=tk.X, padx=10, pady=10)
title_label = tk.Label(
title_frame,
text="🛡️ 数据治理监控",
font=('Microsoft YaHei UI', 14, 'bold'),
fg='#ffd54f',
bg='#1e1e1e'
)
title_label.pack(side=tk.LEFT)
# 刷新按钮
refresh_btn = tk.Button(
title_frame,
text="🔄 刷新",
font=('Microsoft YaHei UI', 10),
bg='#424242',
fg='white',
activebackground='#616161',
activeforeground='white',
relief=tk.FLAT,
padx=10,
cursor='hand2',
command=self._refresh_metrics
)
refresh_btn.pack(side=tk.RIGHT)
# 主内容区域
content_frame = tk.Frame(self.frame, bg='#1e1e1e')
content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# 左侧:指标展示
metrics_frame = tk.LabelFrame(
content_frame,
text=" 治理指标 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#4fc3f7',
bg='#1e1e1e',
relief=tk.GROOVE
)
metrics_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 指标显示区域
self.metrics_text = tk.Text(
metrics_frame,
wrap=tk.WORD,
font=('Consolas', 10),
bg='#2d2d2d',
fg='#d4d4d4',
relief=tk.FLAT,
padx=15,
pady=15,
state=tk.DISABLED,
height=20
)
self.metrics_text.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
# 配置标签样式
self.metrics_text.tag_configure('title', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#ffd54f')
self.metrics_text.tag_configure('label', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#4fc3f7')
self.metrics_text.tag_configure('value', font=('Consolas', 10), foreground='#81c784')
self.metrics_text.tag_configure('warning', font=('Consolas', 10), foreground='#ef5350')
self.metrics_text.tag_configure('normal', font=('Microsoft YaHei UI', 10), foreground='#d4d4d4')
# 右侧:操作面板
action_frame = tk.LabelFrame(
content_frame,
text=" 管理操作 ",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#81c784',
bg='#1e1e1e',
relief=tk.GROOVE
)
action_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(5, 0))
# 操作按钮
btn_config = {
'font': ('Microsoft YaHei UI', 10),
'relief': tk.FLAT,
'cursor': 'hand2',
'width': 18
}
# 手动清理按钮
cleanup_btn = tk.Button(
action_frame,
text="🧹 执行数据清理",
bg='#f57c00',
fg='white',
activebackground='#ff9800',
activeforeground='white',
command=self._manual_cleanup,
**btn_config
)
cleanup_btn.pack(padx=10, pady=(10, 5))
tk.Label(
action_frame,
text="清理过期和敏感数据",
font=('Microsoft YaHei UI', 8),
fg='#888888',
bg='#1e1e1e'
).pack(padx=10, pady=(0, 15))
# 导出脱敏数据按钮
export_btn = tk.Button(
action_frame,
text="📤 导出脱敏数据",
bg='#0e639c',
fg='white',
activebackground='#1177bb',
activeforeground='white',
command=self._export_sanitized,
**btn_config
)
export_btn.pack(padx=10, pady=(0, 5))
tk.Label(
action_frame,
text="导出安全的历史记录",
font=('Microsoft YaHei UI', 8),
fg='#888888',
bg='#1e1e1e'
).pack(padx=10, pady=(0, 15))
# 查看归档按钮
archive_btn = tk.Button(
action_frame,
text="📁 打开归档目录",
bg='#424242',
fg='white',
activebackground='#616161',
activeforeground='white',
command=self._open_archive,
**btn_config
)
archive_btn.pack(padx=10, pady=(0, 5))
tk.Label(
action_frame,
text="查看已归档的记录",
font=('Microsoft YaHei UI', 8),
fg='#888888',
bg='#1e1e1e'
).pack(padx=10, pady=(0, 15))
# 分隔线
ttk.Separator(action_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, padx=10, pady=15)
# 策略说明
policy_label = tk.Label(
action_frame,
text="数据分级策略",
font=('Microsoft YaHei UI', 10, 'bold'),
fg='#ce93d8',
bg='#1e1e1e'
)
policy_label.pack(padx=10, pady=(0, 10))
policy_text = """
• 完整保存 (90天)
敏感度 < 0.3
• 脱敏保存 (30天)
0.3 ≤ 敏感度 < 0.7
• 最小化保存 (7天)
敏感度 ≥ 0.7
• 自动归档
过期数据自动降级或归档
"""
policy_info = tk.Label(
action_frame,
text=policy_text,
font=('Microsoft YaHei UI', 9),
fg='#b0b0b0',
bg='#1e1e1e',
justify=tk.LEFT
)
policy_info.pack(padx=10, pady=(0, 10))
# 加载指标
self._refresh_metrics()
def _refresh_metrics(self):
"""刷新指标显示"""
metrics = self.history.get_governance_metrics()
self.metrics_text.config(state=tk.NORMAL)
self.metrics_text.delete(1.0, tk.END)
if not metrics:
self.metrics_text.insert(tk.END, "暂无治理指标数据\n\n", 'normal')
self.metrics_text.insert(tk.END, "执行任务后将自动收集指标", 'normal')
self.metrics_text.config(state=tk.DISABLED)
return
# 显示指标
self.metrics_text.insert(tk.END, "📊 数据统计\n\n", 'title')
self.metrics_text.insert(tk.END, "总记录数: ", 'label')
self.metrics_text.insert(tk.END, f"{metrics.total_records}\n", 'value')
self.metrics_text.insert(tk.END, "完整保存: ", 'label')
self.metrics_text.insert(tk.END, f"{metrics.full_records}\n", 'value')
self.metrics_text.insert(tk.END, "脱敏保存: ", 'label')
self.metrics_text.insert(tk.END, f"{metrics.sanitized_records}\n", 'value')
self.metrics_text.insert(tk.END, "最小化保存: ", 'label')
self.metrics_text.insert(tk.END, f"{metrics.minimal_records}\n", 'value')
self.metrics_text.insert(tk.END, "已归档: ", 'label')
self.metrics_text.insert(tk.END, f"{metrics.archived_records}\n\n", 'value')
# 存储大小
size_mb = metrics.total_size_bytes / 1024 / 1024
self.metrics_text.insert(tk.END, "存储占用: ", 'label')
self.metrics_text.insert(tk.END, f"{size_mb:.2f} MB\n\n", 'value')
# 过期记录
if metrics.expired_records > 0:
self.metrics_text.insert(tk.END, "⚠️ 待清理: ", 'label')
self.metrics_text.insert(tk.END, f"{metrics.expired_records} 条过期记录\n\n", 'warning')
# 敏感字段命中统计
if metrics.sensitive_field_hits:
self.metrics_text.insert(tk.END, "🔍 敏感字段命中统计\n\n", 'title')
for field, count in sorted(metrics.sensitive_field_hits.items(), key=lambda x: x[1], reverse=True):
self.metrics_text.insert(tk.END, f" {field}: ", 'label')
self.metrics_text.insert(tk.END, f"{count}\n", 'value')
# 最后清理时间
self.metrics_text.insert(tk.END, f"\n\n最后清理: ", 'label')
self.metrics_text.insert(tk.END, f"{metrics.last_cleanup_time}\n", 'normal')
self.metrics_text.config(state=tk.DISABLED)
def _manual_cleanup(self):
"""手动执行数据清理"""
result = messagebox.askyesno(
"确认清理",
"将执行以下操作:\n\n"
"• 完整数据过期 → 降级为脱敏\n"
"• 脱敏数据过期 → 归档\n"
"• 最小化数据过期 → 删除\n\n"
"是否继续?",
icon='question'
)
if result:
try:
stats = self.history.manual_cleanup()
self._refresh_metrics()
messagebox.showinfo(
"清理完成",
f"数据清理完成:\n\n"
f"归档: {stats['archived']}\n"
f"删除: {stats['deleted']}\n"
f"保留: {stats['remaining']}"
)
except Exception as e:
messagebox.showerror("清理失败", f"数据清理失败:\n{e}")
def _export_sanitized(self):
"""导出脱敏数据"""
file_path = filedialog.asksaveasfilename(
title="导出脱敏数据",
defaultextension=".json",
filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
)
if file_path:
try:
count = self.history.export_sanitized(Path(file_path))
messagebox.showinfo("导出成功", f"已导出 {count} 条脱敏记录到:\n{file_path}")
except Exception as e:
messagebox.showerror("导出失败", f"导出失败:\n{e}")
def _open_archive(self):
"""打开归档目录"""
archive_dir = self.history.workspace / "archive"
if archive_dir.exists():
import os
os.startfile(str(archive_dir))
else:
messagebox.showinfo("提示", "归档目录不存在,暂无归档数据")
def show(self):
"""显示面板"""
self._refresh_metrics()
self.frame.pack(fill=tk.BOTH, expand=True)
def hide(self):
"""隐藏面板"""
self.frame.pack_forget()
def get_frame(self) -> tk.Frame:
"""获取主框架"""
return self.frame

View File

@@ -503,7 +503,14 @@ class HistoryView:
# 加载历史记录
records = self.history.get_all()
# 用于跟踪已插入的ID避免重复
inserted_ids = set()
for record in records:
# 如果task_id已存在跳过或使用唯一ID
if record.task_id in inserted_ids:
continue
# 使用任务描述(如果有)或截断的用户输入
description = getattr(record, 'task_summary', None) or record.user_input
if len(description) > 20:
@@ -512,12 +519,27 @@ class HistoryView:
status = "✓ 成功" if record.success else "✗ 失败"
duration = f"{record.duration_ms}ms"
self.tree.insert_with_checkbox('', tk.END, iid=record.task_id, values=(
record.timestamp,
description,
status,
duration
))
try:
self.tree.insert_with_checkbox('', tk.END, iid=record.task_id, values=(
record.timestamp,
description,
status,
duration
))
inserted_ids.add(record.task_id)
except tk.TclError as e:
# 如果ID已存在使用带时间戳的唯一ID
if "already exists" in str(e):
unique_id = f"{record.task_id}_{len(inserted_ids)}"
self.tree.insert_with_checkbox('', tk.END, iid=unique_id, values=(
record.timestamp,
description,
status,
duration
))
inserted_ids.add(unique_id)
else:
raise
# 更新统计信息
self._update_stats()

394
ui/privacy_settings_view.py Normal file
View File

@@ -0,0 +1,394 @@
"""
隐私设置视图
用于配置环境信息采集和脱敏策略
"""
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Callable, Optional
from pathlib import Path
from app.privacy_config import get_privacy_manager, PrivacyManager
class PrivacySettingsView:
"""
隐私设置视图
功能:
- 配置环境信息采集开关
- 配置脱敏策略
- 查看隐私度量指标
"""
def __init__(
self,
parent: tk.Widget,
workspace: Path,
on_back: Optional[Callable[[], None]] = None
):
self.parent = parent
self.workspace = workspace
self.on_back = on_back
self.privacy_manager: PrivacyManager = get_privacy_manager(workspace)
# 配置变量
self.vars = {}
# 创建主框架
self.frame = tk.Frame(parent, bg='#1e1e1e')
self._create_ui()
self._load_settings()
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)
# 说明文本
desc = tk.Label(
self.content_frame,
text="控制向 LLM 发送的环境信息,保护您的隐私安全",
font=('Microsoft YaHei UI', 10),
bg='#1e1e1e',
fg='#808080',
anchor=tk.W
)
desc.pack(fill=tk.X, pady=(10, 20))
# 环境信息采集区
self._create_section("环境信息采集", [
("send_os_info", "操作系统信息", "如 Windows 11、macOS 等"),
("send_python_version", "Python 版本", "如 Python 3.11.0"),
("send_architecture", "系统架构", "如 x86_64、ARM64"),
("send_home_dir", "用户主目录", "⚠️ 敏感信息,建议关闭"),
("send_workspace_path", "工作空间路径", "代码执行所在目录"),
("send_current_dir", "当前工作目录", "⚠️ 敏感信息,建议关闭"),
])
# 脱敏策略区
self._create_section("脱敏策略", [
("anonymize_paths", "路径脱敏", "将路径中的用户名替换为 <USER>"),
("anonymize_username", "用户名脱敏", "隐藏系统用户名"),
])
# 场景化策略区
self._create_section("场景化策略", [
("chat_minimal_info", "对话场景最小化", "对话时仅发送必要信息(推荐)"),
("guidance_full_info", "指导场景完整信息", "操作指导时提供完整环境信息"),
])
# 度量指标区
self._create_metrics_section()
# 按钮区
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_settings
)
save_btn.pack(side=tk.LEFT, padx=5)
export_btn = tk.Button(
btn_frame,
text="📊 导出报告",
font=('Microsoft YaHei UI', 12),
bg='#3d3d3d',
fg='#ffffff',
activebackground='#4d4d4d',
activeforeground='#ffffff',
relief=tk.FLAT,
cursor='hand2',
padx=30,
pady=10,
command=self._export_report
)
export_btn.pack(side=tk.LEFT, padx=5)
# 提示信息
tip = tk.Label(
self.content_frame,
text="💡 提示:关闭敏感信息采集可能影响 AI 回答的准确性,建议开启脱敏策略",
font=('Microsoft YaHei UI', 9),
bg='#1e1e1e',
fg='#808080',
wraplength=600,
justify=tk.LEFT
)
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, description in fields:
self._create_checkbox_field(key, label, description)
def _create_checkbox_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=8)
# 复选框变量
var = tk.BooleanVar()
self.vars[key] = var
# 复选框
checkbox = tk.Checkbutton(
field_frame,
text=label,
variable=var,
font=('Microsoft YaHei UI', 10),
bg='#1e1e1e',
fg='#cccccc',
selectcolor='#2d2d2d',
activebackground='#1e1e1e',
activeforeground='#ffffff',
cursor='hand2'
)
checkbox.pack(side=tk.LEFT, anchor=tk.W)
# 描述
desc = tk.Label(
field_frame,
text=f" ({description})",
font=('Microsoft YaHei UI', 9),
bg='#1e1e1e',
fg='#808080',
anchor=tk.W
)
desc.pack(side=tk.LEFT)
def _create_metrics_section(self) -> None:
"""创建度量指标区域"""
# 区域标题
section_title = tk.Label(
self.content_frame,
text="📊 隐私保护度量",
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))
# 度量指标容器
self.metrics_frame = tk.Frame(self.content_frame, bg='#2d2d2d')
self.metrics_frame.pack(fill=tk.X, pady=10)
self._update_metrics_display()
def _update_metrics_display(self) -> None:
"""更新度量指标显示"""
# 清空现有内容
for widget in self.metrics_frame.winfo_children():
widget.destroy()
metrics = self.privacy_manager.get_metrics()
# 创建指标卡片
metrics_data = [
("总请求次数", metrics['total_requests'], "#3d3d3d"),
("敏感字段上送", metrics['sensitive_fields_sent'], "#8b4513"),
("脱敏处理次数", metrics['anonymized_fields'], "#2e8b57"),
("用户关闭字段", metrics['user_disabled_fields'], "#4169e1"),
]
for i, (label, value, color) in enumerate(metrics_data):
card = tk.Frame(self.metrics_frame, bg=color)
card.grid(row=i//2, column=i%2, padx=10, pady=10, sticky='ew')
value_label = tk.Label(
card,
text=str(value),
font=('Microsoft YaHei UI', 20, 'bold'),
bg=color,
fg='#ffffff'
)
value_label.pack(pady=(10, 0))
name_label = tk.Label(
card,
text=label,
font=('Microsoft YaHei UI', 9),
bg=color,
fg='#cccccc'
)
name_label.pack(pady=(0, 10))
# 配置列权重
self.metrics_frame.columnconfigure(0, weight=1)
self.metrics_frame.columnconfigure(1, weight=1)
# 比率显示
if metrics['total_requests'] > 0:
ratio_frame = tk.Frame(self.metrics_frame, bg='#2d2d2d')
ratio_frame.grid(row=2, column=0, columnspan=2, pady=10, sticky='ew')
sensitive_ratio = tk.Label(
ratio_frame,
text=f"敏感字段上送比率: {metrics['sensitive_ratio']:.1%}",
font=('Microsoft YaHei UI', 10),
bg='#2d2d2d',
fg='#cccccc'
)
sensitive_ratio.pack(pady=5)
anon_ratio = tk.Label(
ratio_frame,
text=f"脱敏处理比率: {metrics['anonymization_ratio']:.1%}",
font=('Microsoft YaHei UI', 10),
bg='#2d2d2d',
fg='#cccccc'
)
anon_ratio.pack(pady=5)
def _load_settings(self) -> None:
"""加载设置"""
settings_dict = self.privacy_manager.settings.to_dict()
for key, var in self.vars.items():
if key in settings_dict:
var.set(settings_dict[key])
def _save_settings(self) -> None:
"""保存设置"""
try:
# 收集设置
settings = {}
for key, var in self.vars.items():
settings[key] = var.get()
# 更新设置
self.privacy_manager.update_settings(**settings)
# 更新度量显示
self._update_metrics_display()
messagebox.showinfo("成功", "隐私设置已保存")
except Exception as e:
messagebox.showerror("错误", f"保存设置失败: {str(e)}")
def _export_report(self) -> None:
"""导出隐私度量报告"""
try:
report = self.privacy_manager.export_metrics()
# 保存到文件
report_file = self.workspace / "privacy_report.txt"
with open(report_file, 'w', encoding='utf-8') as f:
f.write(report)
messagebox.showinfo(
"导出成功",
f"隐私度量报告已导出到:\n{report_file}\n\n是否打开查看?"
)
# 打开文件
import os
os.startfile(str(report_file))
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_settings()
self._update_metrics_display()
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

321
ui/reuse_confirm_dialog.py Normal file
View File

@@ -0,0 +1,321 @@
"""
复用确认对话框
显示任务差异并让用户确认是否复用
"""
import tkinter as tk
from tkinter import ttk
from typing import List, Callable, Optional
from history.task_features import TaskDifference
def show_reuse_confirm_dialog(
parent: tk.Tk,
task_summary: str,
timestamp: str,
similarity_score: float,
differences: List[TaskDifference],
on_confirm: Callable,
on_reject: Callable
):
"""
显示复用确认对话框
Args:
parent: 父窗口
task_summary: 任务摘要
timestamp: 任务时间
similarity_score: 相似度分数
differences: 差异列表
on_confirm: 确认回调
on_reject: 拒绝回调
"""
dialog = tk.Toplevel(parent)
dialog.title("发现相似任务")
dialog.geometry("700x600")
dialog.resizable(False, False)
dialog.configure(bg='#2b2b2b')
# 居中显示
dialog.transient(parent)
dialog.grab_set()
# 主容器
main_frame = tk.Frame(dialog, bg='#2b2b2b')
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# 标题
title_label = tk.Label(
main_frame,
text="🔍 发现相似的成功任务",
font=('Microsoft YaHei UI', 14, 'bold'),
bg='#2b2b2b',
fg='#ffffff'
)
title_label.pack(pady=(0, 15))
# 任务信息框
info_frame = tk.Frame(main_frame, bg='#3c3c3c', relief=tk.FLAT, bd=0)
info_frame.pack(fill=tk.X, pady=(0, 15))
# 任务摘要
task_label = tk.Label(
info_frame,
text=f"任务: {task_summary}",
font=('Microsoft YaHei UI', 10),
bg='#3c3c3c',
fg='#e0e0e0',
anchor='w',
justify='left'
)
task_label.pack(fill=tk.X, padx=15, pady=(10, 5))
# 时间
time_label = tk.Label(
info_frame,
text=f"时间: {timestamp}",
font=('Microsoft YaHei UI', 9),
bg='#3c3c3c',
fg='#a0a0a0',
anchor='w'
)
time_label.pack(fill=tk.X, padx=15, pady=(0, 5))
# 相似度
similarity_percent = int(similarity_score * 100)
similarity_color = '#4caf50' if similarity_score >= 0.8 else '#ff9800' if similarity_score >= 0.6 else '#f44336'
similarity_label = tk.Label(
info_frame,
text=f"相似度: {similarity_percent}%",
font=('Microsoft YaHei UI', 9, 'bold'),
bg='#3c3c3c',
fg=similarity_color,
anchor='w'
)
similarity_label.pack(fill=tk.X, padx=15, pady=(0, 10))
# 差异部分
if differences:
# 统计关键差异
critical_count = sum(1 for d in differences if d.importance == 'critical')
high_count = sum(1 for d in differences if d.importance == 'high')
# 差异标题
diff_title_frame = tk.Frame(main_frame, bg='#2b2b2b')
diff_title_frame.pack(fill=tk.X, pady=(0, 10))
diff_title = tk.Label(
diff_title_frame,
text=f"⚠️ 发现 {len(differences)} 处差异",
font=('Microsoft YaHei UI', 11, 'bold'),
bg='#2b2b2b',
fg='#ff9800'
)
diff_title.pack(side=tk.LEFT)
if critical_count > 0:
critical_badge = tk.Label(
diff_title_frame,
text=f"{critical_count} 关键",
font=('Microsoft YaHei UI', 9),
bg='#f44336',
fg='#ffffff',
padx=8,
pady=2
)
critical_badge.pack(side=tk.LEFT, padx=(10, 5))
if high_count > 0:
high_badge = tk.Label(
diff_title_frame,
text=f"{high_count} 重要",
font=('Microsoft YaHei UI', 9),
bg='#ff9800',
fg='#ffffff',
padx=8,
pady=2
)
high_badge.pack(side=tk.LEFT)
# 差异列表(可滚动)
diff_container = tk.Frame(main_frame, bg='#2b2b2b')
diff_container.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
# 创建 Canvas 和 Scrollbar
canvas = tk.Canvas(diff_container, bg='#2b2b2b', highlightthickness=0)
scrollbar = ttk.Scrollbar(diff_container, orient="vertical", command=canvas.yview)
scrollable_frame = tk.Frame(canvas, bg='#2b2b2b')
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# 显示差异
importance_colors = {
'critical': '#f44336',
'high': '#ff9800',
'medium': '#2196f3',
'low': '#9e9e9e'
}
importance_labels = {
'critical': '关键',
'high': '重要',
'medium': '一般',
'low': '次要'
}
for i, diff in enumerate(differences):
diff_frame = tk.Frame(scrollable_frame, bg='#3c3c3c', relief=tk.FLAT, bd=0)
diff_frame.pack(fill=tk.X, pady=(0, 8), padx=2)
# 差异标题行
header_frame = tk.Frame(diff_frame, bg='#3c3c3c')
header_frame.pack(fill=tk.X, padx=10, pady=(8, 5))
category_label = tk.Label(
header_frame,
text=diff.category,
font=('Microsoft YaHei UI', 9, 'bold'),
bg='#3c3c3c',
fg='#ffffff'
)
category_label.pack(side=tk.LEFT)
importance_badge = tk.Label(
header_frame,
text=importance_labels[diff.importance],
font=('Microsoft YaHei UI', 8),
bg=importance_colors[diff.importance],
fg='#ffffff',
padx=6,
pady=1
)
importance_badge.pack(side=tk.LEFT, padx=(8, 0))
# 当前值
current_frame = tk.Frame(diff_frame, bg='#3c3c3c')
current_frame.pack(fill=tk.X, padx=10, pady=(0, 3))
current_title = tk.Label(
current_frame,
text="当前任务:",
font=('Microsoft YaHei UI', 8),
bg='#3c3c3c',
fg='#a0a0a0'
)
current_title.pack(side=tk.LEFT)
current_value = tk.Label(
current_frame,
text=diff.current_value,
font=('Microsoft YaHei UI', 9),
bg='#3c3c3c',
fg='#4caf50',
wraplength=500,
justify='left'
)
current_value.pack(side=tk.LEFT, padx=(5, 0))
# 历史值
history_frame = tk.Frame(diff_frame, bg='#3c3c3c')
history_frame.pack(fill=tk.X, padx=10, pady=(0, 8))
history_title = tk.Label(
history_frame,
text="历史任务:",
font=('Microsoft YaHei UI', 8),
bg='#3c3c3c',
fg='#a0a0a0'
)
history_title.pack(side=tk.LEFT)
history_value = tk.Label(
history_frame,
text=diff.history_value,
font=('Microsoft YaHei UI', 9),
bg='#3c3c3c',
fg='#ff9800',
wraplength=500,
justify='left'
)
history_value.pack(side=tk.LEFT, padx=(5, 0))
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
else:
# 无差异
no_diff_label = tk.Label(
main_frame,
text="✅ 未发现关键差异",
font=('Microsoft YaHei UI', 10),
bg='#2b2b2b',
fg='#4caf50'
)
no_diff_label.pack(pady=20)
# 提示信息
hint_label = tk.Label(
main_frame,
text="是否直接复用该任务的代码?\n(选择「生成新代码」将根据当前需求重新生成)",
font=('Microsoft YaHei UI', 9),
bg='#2b2b2b',
fg='#a0a0a0',
justify='center'
)
hint_label.pack(pady=(10, 15))
# 按钮区域
button_frame = tk.Frame(main_frame, bg='#2b2b2b')
button_frame.pack(fill=tk.X)
def on_confirm_click():
dialog.destroy()
on_confirm()
def on_reject_click():
dialog.destroy()
on_reject()
# 复用按钮
confirm_btn = tk.Button(
button_frame,
text="✓ 复用代码",
font=('Microsoft YaHei UI', 10, 'bold'),
bg='#4caf50',
fg='#ffffff',
activebackground='#45a049',
activeforeground='#ffffff',
relief=tk.FLAT,
cursor='hand2',
command=on_confirm_click,
padx=30,
pady=10
)
confirm_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 5))
# 拒绝按钮
reject_btn = tk.Button(
button_frame,
text="✗ 生成新代码",
font=('Microsoft YaHei UI', 10),
bg='#555555',
fg='#ffffff',
activebackground='#666666',
activeforeground='#ffffff',
relief=tk.FLAT,
cursor='hand2',
command=on_reject_click,
padx=30,
pady=10
)
reject_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(5, 0))
# 等待对话框关闭
dialog.wait_window()

View File

@@ -40,7 +40,7 @@ class SettingsView:
self,
parent: tk.Widget,
env_path: Path,
on_save: Optional[Callable[[], None]] = None,
on_save: Optional[Callable[[bool], None]] = None,
on_back: Optional[Callable[[], None]] = None
):
self.parent = parent
@@ -342,10 +342,29 @@ class SettingsView:
# 同时更新环境变量
os.environ[key] = value
messagebox.showinfo("成功", "配置已保存!")
# 重置 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()
self.on_save(success)
except Exception as e:
messagebox.showerror("错误", f"保存配置失败: {str(e)}")

View File

@@ -465,7 +465,7 @@ class TaskGuideView:
self.risk_label = tk.Label(
section,
text="• 所有操作仅在 workspace 目录内进行 • 原始文件不会被修改或删除 • 执行代码已通过安全检查",
text="• 所有操作仅在 workspace 目录内进行 • 原始文件不会被修改或删除 • 执行代码已通过当前版本安全复检",
font=('Microsoft YaHei UI', 9),
fg='#d4d4d4',
bg='#1e1e1e',