Files
LocalAgent/ui/governance_panel.py
Mimikko-zeus 8a538bb950 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.
2026-02-27 14:32:30 +08:00

339 lines
11 KiB
Python

"""
数据治理监控面板
提供可视化的治理指标展示和管理操作
"""
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