- 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.
339 lines
11 KiB
Python
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
|
|
|