- 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.
604 lines
20 KiB
Python
604 lines
20 KiB
Python
"""
|
||
任务引导视图组件
|
||
执行任务的引导式 UI - 简化版
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import scrolledtext, messagebox
|
||
from tkinter import ttk
|
||
from typing import Callable, Optional, List
|
||
from pathlib import Path
|
||
import re
|
||
|
||
|
||
class MarkdownText(tk.Text):
|
||
"""支持简单 Markdown 渲染的 Text 组件"""
|
||
|
||
def __init__(self, parent, **kwargs):
|
||
super().__init__(parent, **kwargs)
|
||
self._setup_tags()
|
||
|
||
def _setup_tags(self):
|
||
"""设置 Markdown 样式标签"""
|
||
# 标题样式
|
||
self.tag_configure('h1', font=('Microsoft YaHei UI', 13, 'bold'), foreground='#ffd54f', spacing1=8, spacing3=4)
|
||
self.tag_configure('h2', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#81c784', spacing1=6, spacing3=3)
|
||
self.tag_configure('h3', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#4fc3f7', spacing1=4, spacing3=2)
|
||
|
||
# 列表样式
|
||
self.tag_configure('bullet', foreground='#ce93d8', lmargin1=15, lmargin2=30)
|
||
self.tag_configure('numbered', foreground='#ce93d8', lmargin1=15, lmargin2=30)
|
||
|
||
# 普通文本
|
||
self.tag_configure('normal', font=('Microsoft YaHei UI', 10), foreground='#d4d4d4')
|
||
|
||
def set_markdown(self, text: str):
|
||
"""设置 Markdown 内容并渲染"""
|
||
self.config(state=tk.NORMAL)
|
||
self.delete(1.0, tk.END)
|
||
|
||
lines = text.split('\n')
|
||
for line in lines:
|
||
self._render_line(line)
|
||
|
||
self.config(state=tk.DISABLED)
|
||
|
||
def _render_line(self, line: str):
|
||
"""渲染单行 Markdown"""
|
||
stripped = line.strip()
|
||
|
||
# 标题
|
||
if stripped.startswith('### '):
|
||
self.insert(tk.END, stripped[4:] + '\n', 'h3')
|
||
elif stripped.startswith('## '):
|
||
self.insert(tk.END, stripped[3:] + '\n', 'h2')
|
||
elif stripped.startswith('# '):
|
||
self.insert(tk.END, stripped[2:] + '\n', 'h1')
|
||
# 无序列表
|
||
elif stripped.startswith('- ') or stripped.startswith('* '):
|
||
self.insert(tk.END, '• ' + stripped[2:] + '\n', 'bullet')
|
||
# 有序列表
|
||
elif re.match(r'^\d+\.\s', stripped):
|
||
match = re.match(r'^(\d+\.)\s(.*)$', stripped)
|
||
if match:
|
||
self.insert(tk.END, match.group(1) + ' ' + match.group(2) + '\n', 'numbered')
|
||
# 普通文本
|
||
else:
|
||
self.insert(tk.END, line + '\n', 'normal')
|
||
|
||
|
||
class FileZone(tk.Frame):
|
||
"""简化的文件区域 - 仅显示打开文件夹按钮"""
|
||
|
||
def __init__(
|
||
self,
|
||
parent,
|
||
title: str,
|
||
target_dir: Path,
|
||
is_input: bool = True,
|
||
**kwargs
|
||
):
|
||
super().__init__(parent, **kwargs)
|
||
self.target_dir = target_dir
|
||
self.is_input = is_input
|
||
self.configure(bg='#2d2d2d')
|
||
|
||
# 确保目录存在
|
||
self.target_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
self._create_widgets(title)
|
||
|
||
def _create_widgets(self, title: str):
|
||
"""创建组件"""
|
||
# 打开文件夹按钮
|
||
color = '#4fc3f7' if self.is_input else '#81c784'
|
||
|
||
self.open_btn = tk.Button(
|
||
self,
|
||
text=f"📂 {title}",
|
||
font=('Microsoft YaHei UI', 10),
|
||
bg='#424242',
|
||
fg=color,
|
||
activebackground='#616161',
|
||
activeforeground=color,
|
||
relief=tk.FLAT,
|
||
padx=15,
|
||
pady=8,
|
||
cursor='hand2',
|
||
command=self._open_folder
|
||
)
|
||
self.open_btn.pack(fill=tk.X, padx=5, pady=5)
|
||
|
||
# 文件计数标签
|
||
self.count_label = tk.Label(
|
||
self,
|
||
text="0 个文件",
|
||
font=('Microsoft YaHei UI', 9),
|
||
fg='#888888',
|
||
bg='#2d2d2d'
|
||
)
|
||
self.count_label.pack(pady=(0, 5))
|
||
|
||
self._refresh_count()
|
||
|
||
def _open_folder(self):
|
||
"""打开目标文件夹"""
|
||
import os
|
||
os.startfile(str(self.target_dir))
|
||
|
||
def _refresh_count(self):
|
||
"""刷新文件计数"""
|
||
files = list(self.target_dir.glob('*'))
|
||
files = [f for f in files if f.is_file()]
|
||
self.count_label.config(text=f"{len(files)} 个文件")
|
||
|
||
def refresh(self):
|
||
"""刷新"""
|
||
self._refresh_count()
|
||
|
||
def get_files(self) -> List[Path]:
|
||
"""获取目录中的文件列表"""
|
||
files = list(self.target_dir.glob('*'))
|
||
return [f for f in files if f.is_file()]
|
||
|
||
|
||
class TaskGuideView:
|
||
"""
|
||
任务引导视图 - 简化版
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
parent: tk.Widget,
|
||
on_execute: Callable[[], None],
|
||
on_cancel: Callable[[], None],
|
||
workspace_path: Optional[Path] = None
|
||
):
|
||
self.parent = parent
|
||
self.on_execute = on_execute
|
||
self.on_cancel = on_cancel
|
||
|
||
if workspace_path:
|
||
self.workspace = workspace_path
|
||
else:
|
||
self.workspace = Path(__file__).parent.parent / "workspace"
|
||
|
||
self.input_dir = self.workspace / "input"
|
||
self.output_dir = self.workspace / "output"
|
||
|
||
self._create_widgets()
|
||
|
||
def _create_widgets(self):
|
||
"""创建 UI 组件"""
|
||
# 主框架 - 使用 Canvas 实现滚动
|
||
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
|
||
|
||
# 标题
|
||
title_label = tk.Label(
|
||
self.frame,
|
||
text="执行任务确认",
|
||
font=('Microsoft YaHei UI', 14, 'bold'),
|
||
fg='#ffd54f',
|
||
bg='#1e1e1e'
|
||
)
|
||
title_label.pack(pady=(5, 10))
|
||
|
||
# 文件区域(横向排列)
|
||
file_section = tk.Frame(self.frame, bg='#1e1e1e')
|
||
file_section.pack(fill=tk.X, padx=10, pady=5)
|
||
|
||
# 输入文件区域
|
||
input_frame = tk.LabelFrame(
|
||
file_section,
|
||
text=" 📥 输入 ",
|
||
font=('Microsoft YaHei UI', 10, 'bold'),
|
||
fg='#4fc3f7',
|
||
bg='#1e1e1e',
|
||
relief=tk.GROOVE
|
||
)
|
||
input_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
|
||
|
||
self.input_zone = FileZone(
|
||
input_frame,
|
||
title="打开输入文件夹",
|
||
target_dir=self.input_dir,
|
||
is_input=True,
|
||
bg='#2d2d2d'
|
||
)
|
||
self.input_zone.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
||
|
||
# 箭头
|
||
arrow_label = tk.Label(
|
||
file_section,
|
||
text="→",
|
||
font=('Microsoft YaHei UI', 16, 'bold'),
|
||
fg='#ffd54f',
|
||
bg='#1e1e1e'
|
||
)
|
||
arrow_label.pack(side=tk.LEFT, padx=5)
|
||
|
||
# 输出文件区域
|
||
output_frame = tk.LabelFrame(
|
||
file_section,
|
||
text=" 📤 输出 ",
|
||
font=('Microsoft YaHei UI', 10, 'bold'),
|
||
fg='#81c784',
|
||
bg='#1e1e1e',
|
||
relief=tk.GROOVE
|
||
)
|
||
output_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
||
|
||
self.output_zone = FileZone(
|
||
output_frame,
|
||
title="打开输出文件夹",
|
||
target_dir=self.output_dir,
|
||
is_input=False,
|
||
bg='#2d2d2d'
|
||
)
|
||
self.output_zone.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
||
|
||
# 意图识别结果区域
|
||
self._create_intent_section()
|
||
|
||
# 执行计划区域(Markdown)
|
||
self._create_plan_section()
|
||
|
||
# 代码预览区域(可折叠)
|
||
self._create_code_section()
|
||
|
||
# 风险提示区域
|
||
self._create_risk_section()
|
||
|
||
# 按钮区域
|
||
self._create_button_section()
|
||
|
||
def _create_intent_section(self):
|
||
"""创建意图识别结果区域"""
|
||
section = tk.LabelFrame(
|
||
self.frame,
|
||
text=" 🎯 意图识别 ",
|
||
font=('Microsoft YaHei UI', 10, 'bold'),
|
||
fg='#81c784',
|
||
bg='#1e1e1e',
|
||
relief=tk.GROOVE
|
||
)
|
||
section.pack(fill=tk.X, padx=10, pady=3)
|
||
|
||
self.intent_label = tk.Label(
|
||
section,
|
||
text="",
|
||
font=('Microsoft YaHei UI', 9),
|
||
fg='#d4d4d4',
|
||
bg='#1e1e1e',
|
||
wraplength=650,
|
||
justify=tk.LEFT
|
||
)
|
||
self.intent_label.pack(padx=8, pady=5, anchor=tk.W)
|
||
|
||
def _create_plan_section(self):
|
||
"""创建执行计划区域(支持 Markdown)"""
|
||
section = tk.LabelFrame(
|
||
self.frame,
|
||
text=" 📄 执行计划 ",
|
||
font=('Microsoft YaHei UI', 10, 'bold'),
|
||
fg='#ce93d8',
|
||
bg='#1e1e1e',
|
||
relief=tk.GROOVE
|
||
)
|
||
section.pack(fill=tk.BOTH, expand=True, padx=10, pady=3)
|
||
|
||
# 使用 Markdown 渲染的 Text
|
||
text_frame = tk.Frame(section, bg='#2d2d2d')
|
||
text_frame.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
||
|
||
self.plan_text = MarkdownText(
|
||
text_frame,
|
||
wrap=tk.WORD,
|
||
bg='#2d2d2d',
|
||
fg='#d4d4d4',
|
||
relief=tk.FLAT,
|
||
height=6,
|
||
padx=8,
|
||
pady=5
|
||
)
|
||
|
||
# 添加滚动条
|
||
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.plan_text.yview)
|
||
self.plan_text.configure(yscrollcommand=scrollbar.set)
|
||
|
||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.plan_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
def _create_code_section(self):
|
||
"""创建代码预览区域(可折叠)"""
|
||
# 折叠状态
|
||
self._code_expanded = False
|
||
|
||
# 外层框架
|
||
self.code_section = tk.LabelFrame(
|
||
self.frame,
|
||
text=" 💻 生成的代码 ",
|
||
font=('Microsoft YaHei UI', 10, 'bold'),
|
||
fg='#64b5f6',
|
||
bg='#1e1e1e',
|
||
relief=tk.GROOVE
|
||
)
|
||
self.code_section.pack(fill=tk.X, padx=10, pady=3)
|
||
|
||
# 展开/折叠按钮
|
||
self.toggle_code_btn = tk.Button(
|
||
self.code_section,
|
||
text="▶ 点击展开代码预览",
|
||
font=('Microsoft YaHei UI', 9),
|
||
bg='#2d2d2d',
|
||
fg='#64b5f6',
|
||
activebackground='#3d3d3d',
|
||
activeforeground='#64b5f6',
|
||
relief=tk.FLAT,
|
||
cursor='hand2',
|
||
command=self._toggle_code_view
|
||
)
|
||
self.toggle_code_btn.pack(fill=tk.X, padx=5, pady=5)
|
||
|
||
# 代码显示区域(初始隐藏)
|
||
self.code_frame = tk.Frame(self.code_section, bg='#1e1e1e')
|
||
|
||
# 代码文本框
|
||
self.code_text = tk.Text(
|
||
self.code_frame,
|
||
wrap=tk.NONE,
|
||
font=('Consolas', 10),
|
||
bg='#1e1e1e',
|
||
fg='#d4d4d4',
|
||
insertbackground='white',
|
||
relief=tk.FLAT,
|
||
height=12,
|
||
padx=8,
|
||
pady=5
|
||
)
|
||
|
||
# 配置代码高亮标签
|
||
self.code_text.tag_configure('keyword', foreground='#569cd6')
|
||
self.code_text.tag_configure('string', foreground='#ce9178')
|
||
self.code_text.tag_configure('comment', foreground='#6a9955')
|
||
self.code_text.tag_configure('function', foreground='#dcdcaa')
|
||
self.code_text.tag_configure('number', foreground='#b5cea8')
|
||
|
||
# 滚动条
|
||
code_scrollbar_y = ttk.Scrollbar(self.code_frame, orient=tk.VERTICAL, command=self.code_text.yview)
|
||
code_scrollbar_x = ttk.Scrollbar(self.code_frame, orient=tk.HORIZONTAL, command=self.code_text.xview)
|
||
self.code_text.configure(yscrollcommand=code_scrollbar_y.set, xscrollcommand=code_scrollbar_x.set)
|
||
|
||
code_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
|
||
code_scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
|
||
self.code_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
# 复制按钮
|
||
self.copy_code_btn = tk.Button(
|
||
self.code_frame,
|
||
text="📋 复制代码",
|
||
font=('Microsoft YaHei UI', 9),
|
||
bg='#424242',
|
||
fg='white',
|
||
activebackground='#616161',
|
||
activeforeground='white',
|
||
relief=tk.FLAT,
|
||
cursor='hand2',
|
||
command=self._copy_code
|
||
)
|
||
|
||
def _toggle_code_view(self):
|
||
"""切换代码预览的展开/折叠状态"""
|
||
self._code_expanded = not self._code_expanded
|
||
|
||
if self._code_expanded:
|
||
self.toggle_code_btn.config(text="▼ 点击折叠代码预览")
|
||
self.code_frame.pack(fill=tk.BOTH, expand=True, padx=3, pady=(0, 5))
|
||
self.copy_code_btn.pack(pady=5)
|
||
else:
|
||
self.toggle_code_btn.config(text="▶ 点击展开代码预览")
|
||
self.copy_code_btn.pack_forget()
|
||
self.code_frame.pack_forget()
|
||
|
||
def _copy_code(self):
|
||
"""复制代码到剪贴板"""
|
||
code = self.code_text.get(1.0, tk.END).strip()
|
||
self.frame.clipboard_clear()
|
||
self.frame.clipboard_append(code)
|
||
|
||
# 显示复制成功提示
|
||
original_text = self.copy_code_btn.cget('text')
|
||
self.copy_code_btn.config(text="✓ 已复制!")
|
||
self.frame.after(1500, lambda: self.copy_code_btn.config(text=original_text))
|
||
|
||
def _apply_syntax_highlight(self, code: str):
|
||
"""应用简单的语法高亮"""
|
||
import re
|
||
|
||
# 关键字
|
||
keywords = r'\b(import|from|def|class|if|else|elif|for|while|try|except|finally|with|as|return|yield|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|global|nonlocal)\b'
|
||
# 字符串
|
||
strings = r'(\"\"\"[\s\S]*?\"\"\"|\'\'\'[\s\S]*?\'\'\'|\"[^\"]*\"|\'[^\']*\')'
|
||
# 注释
|
||
comments = r'(#.*$)'
|
||
# 函数调用
|
||
functions = r'\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\('
|
||
# 数字
|
||
numbers = r'\b(\d+\.?\d*)\b'
|
||
|
||
# 先插入纯文本
|
||
self.code_text.delete(1.0, tk.END)
|
||
self.code_text.insert(1.0, code)
|
||
|
||
# 应用高亮
|
||
for match in re.finditer(keywords, code, re.MULTILINE):
|
||
start = f"1.0+{match.start()}c"
|
||
end = f"1.0+{match.end()}c"
|
||
self.code_text.tag_add('keyword', start, end)
|
||
|
||
for match in re.finditer(strings, code, re.MULTILINE):
|
||
start = f"1.0+{match.start()}c"
|
||
end = f"1.0+{match.end()}c"
|
||
self.code_text.tag_add('string', start, end)
|
||
|
||
for match in re.finditer(comments, code, re.MULTILINE):
|
||
start = f"1.0+{match.start()}c"
|
||
end = f"1.0+{match.end()}c"
|
||
self.code_text.tag_add('comment', start, end)
|
||
|
||
for match in re.finditer(numbers, code, re.MULTILINE):
|
||
start = f"1.0+{match.start(1)}c"
|
||
end = f"1.0+{match.end(1)}c"
|
||
self.code_text.tag_add('number', start, end)
|
||
|
||
def _create_risk_section(self):
|
||
"""创建风险提示区域"""
|
||
section = tk.LabelFrame(
|
||
self.frame,
|
||
text=" ⚠️ 安全提示 ",
|
||
font=('Microsoft YaHei UI', 10, 'bold'),
|
||
fg='#ffb74d',
|
||
bg='#1e1e1e',
|
||
relief=tk.GROOVE
|
||
)
|
||
section.pack(fill=tk.X, padx=10, pady=3)
|
||
|
||
self.risk_label = tk.Label(
|
||
section,
|
||
text="• 所有操作仅在 workspace 目录内进行 • 原始文件不会被修改或删除 • 执行代码已通过当前版本安全复检",
|
||
font=('Microsoft YaHei UI', 9),
|
||
fg='#d4d4d4',
|
||
bg='#1e1e1e',
|
||
justify=tk.LEFT
|
||
)
|
||
self.risk_label.pack(padx=8, pady=5, anchor=tk.W)
|
||
|
||
def _create_button_section(self):
|
||
"""创建按钮区域"""
|
||
button_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
||
button_frame.pack(fill=tk.X, padx=10, pady=10)
|
||
|
||
# 统一按钮样式
|
||
btn_font = ('Microsoft YaHei UI', 10)
|
||
btn_width = 12
|
||
btn_height = 1
|
||
|
||
# 刷新文件列表按钮
|
||
self.refresh_btn = tk.Button(
|
||
button_frame,
|
||
text="🔄 刷新",
|
||
font=btn_font,
|
||
width=btn_width,
|
||
height=btn_height,
|
||
bg='#424242',
|
||
fg='white',
|
||
activebackground='#616161',
|
||
activeforeground='white',
|
||
relief=tk.FLAT,
|
||
cursor='hand2',
|
||
command=self._refresh_all
|
||
)
|
||
self.refresh_btn.pack(side=tk.LEFT)
|
||
|
||
# 执行按钮
|
||
self.execute_btn = tk.Button(
|
||
button_frame,
|
||
text="🚀 开始执行",
|
||
font=('Microsoft YaHei UI', 10, 'bold'),
|
||
width=btn_width,
|
||
height=btn_height,
|
||
bg='#4caf50',
|
||
fg='white',
|
||
activebackground='#66bb6a',
|
||
activeforeground='white',
|
||
relief=tk.FLAT,
|
||
cursor='hand2',
|
||
command=self._on_execute_clicked
|
||
)
|
||
self.execute_btn.pack(side=tk.RIGHT)
|
||
|
||
# 取消按钮
|
||
self.cancel_btn = tk.Button(
|
||
button_frame,
|
||
text="取消",
|
||
font=btn_font,
|
||
width=btn_width,
|
||
height=btn_height,
|
||
bg='#616161',
|
||
fg='white',
|
||
activebackground='#757575',
|
||
activeforeground='white',
|
||
relief=tk.FLAT,
|
||
cursor='hand2',
|
||
command=self.on_cancel
|
||
)
|
||
self.cancel_btn.pack(side=tk.RIGHT, padx=(0, 10))
|
||
|
||
def _refresh_all(self):
|
||
"""刷新所有文件列表"""
|
||
self.input_zone.refresh()
|
||
self.output_zone.refresh()
|
||
|
||
def _on_execute_clicked(self):
|
||
"""执行按钮点击"""
|
||
# 刷新文件列表
|
||
self.input_zone.refresh()
|
||
|
||
# 检查 input 目录是否有文件
|
||
files = self.input_zone.get_files()
|
||
|
||
if not files:
|
||
result = messagebox.askyesno(
|
||
"确认执行",
|
||
"输入文件夹为空,确定要继续执行吗?",
|
||
icon='warning'
|
||
)
|
||
if not result:
|
||
return
|
||
|
||
self.on_execute()
|
||
|
||
def set_intent_result(self, reason: str, confidence: float):
|
||
"""设置意图识别结果"""
|
||
self.intent_label.config(
|
||
text=f"识别结果: 执行任务 (置信度: {confidence:.0%}) | 原因: {reason}"
|
||
)
|
||
|
||
def set_execution_plan(self, plan: str):
|
||
"""设置执行计划(Markdown 格式)"""
|
||
self.plan_text.set_markdown(plan)
|
||
|
||
def set_code(self, code: str):
|
||
"""设置生成的代码"""
|
||
self.code_text.config(state=tk.NORMAL)
|
||
self._apply_syntax_highlight(code)
|
||
self.code_text.config(state=tk.DISABLED)
|
||
|
||
def set_risk_info(self, info: str):
|
||
"""设置风险提示"""
|
||
self.risk_label.config(text=info)
|
||
|
||
def set_buttons_enabled(self, enabled: bool):
|
||
"""设置按钮是否可用"""
|
||
state = tk.NORMAL if enabled else tk.DISABLED
|
||
self.execute_btn.config(state=state)
|
||
self.cancel_btn.config(state=state)
|
||
self.refresh_btn.config(state=state)
|
||
|
||
def refresh_output(self):
|
||
"""刷新输出文件列表"""
|
||
self.output_zone.refresh()
|
||
|
||
def show(self):
|
||
"""显示视图"""
|
||
self.frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
self._refresh_all()
|
||
|
||
def hide(self):
|
||
"""隐藏视图"""
|
||
self.frame.pack_forget()
|
||
|
||
def get_frame(self) -> tk.Frame:
|
||
"""获取主框架"""
|
||
return self.frame
|