feat:增强需求澄清与任务管理功能
更新了 .env.example,新增聊天模型配置,以提升对话处理能力。 增强了 README.md,反映了包括需求澄清、代码复用和自动重试在内的新功能。 重构了 agent.py,以支持多模型交互,并为无法在本地执行的任务新增了引导处理逻辑。 改进了 SandboxRunner,增加了任务执行成功校验,并加入了工作区清理功能。 扩展了 HistoryManager,支持任务摘要生成以及记录的批量删除。 优化了 chat_view.py 和 history_view.py 中的 UI 组件,提升用户体验,包括 Markdown 渲染和任务管理选项。
This commit is contained in:
@@ -15,5 +15,8 @@ LLM_API_KEY=your_api_key_here
|
|||||||
# Intent recognition model (small model recommended for speed)
|
# Intent recognition model (small model recommended for speed)
|
||||||
INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
||||||
|
|
||||||
|
# Chat model (medium model recommended for conversation)
|
||||||
|
CHAT_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct
|
||||||
|
|
||||||
# Code generation model (large model recommended for quality)
|
# Code generation model (large model recommended for quality)
|
||||||
GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -5,11 +5,16 @@ A Windows-based local AI assistant that can understand natural language commands
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Intent Recognition**: Automatically distinguishes between chat conversations and execution tasks
|
- **Intent Recognition**: Automatically distinguishes between chat conversations and execution tasks
|
||||||
- **Code Generation**: Generates Python code based on user requirements
|
- **Requirement Clarification**: Interactive Q&A to clarify vague requirements before code generation
|
||||||
|
- **Code Generation**: Generates Python code based on structured requirements
|
||||||
- **Safety Checks**: Multi-layer security with static analysis and LLM review
|
- **Safety Checks**: Multi-layer security with static analysis and LLM review
|
||||||
- **Sandbox Execution**: Runs generated code in an isolated environment
|
- **Sandbox Execution**: Runs generated code in an isolated environment
|
||||||
- **Task History**: Records all executed tasks for review
|
- **Task History**: Records all executed tasks with selective deletion
|
||||||
- **Streaming Responses**: Real-time display of LLM responses
|
- **Streaming Responses**: Real-time display of LLM responses
|
||||||
|
- **Settings UI**: Easy configuration of API and models
|
||||||
|
- **Code Reuse**: Automatically finds and reuses successful code for similar tasks
|
||||||
|
- **Auto Retry**: AI-powered code fixing for failed tasks
|
||||||
|
- **Multi-Model Support**: Different models for intent recognition, chat, and code generation
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -32,8 +37,10 @@ LocalAgent/
|
|||||||
│ └── manager.py # History manager
|
│ └── manager.py # History manager
|
||||||
├── ui/ # User interface
|
├── ui/ # User interface
|
||||||
│ ├── chat_view.py # Chat interface
|
│ ├── chat_view.py # Chat interface
|
||||||
|
│ ├── clarify_view.py # Requirement clarification view
|
||||||
│ ├── task_guide_view.py # Task confirmation view
|
│ ├── task_guide_view.py # Task confirmation view
|
||||||
│ └── history_view.py # History view
|
│ ├── history_view.py # History view with Markdown support
|
||||||
|
│ └── settings_view.py # Settings configuration view
|
||||||
├── tests/ # Unit tests
|
├── tests/ # Unit tests
|
||||||
├── workspace/ # Working directory (auto-created)
|
├── workspace/ # Working directory (auto-created)
|
||||||
│ ├── input/ # Input files
|
│ ├── input/ # Input files
|
||||||
@@ -85,7 +92,7 @@ LocalAgent/
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit `.env` file with your settings:
|
Edit `.env` file with your settings (or use the Settings UI in the app):
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# SiliconFlow API Configuration
|
# SiliconFlow API Configuration
|
||||||
@@ -93,7 +100,13 @@ LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions
|
|||||||
LLM_API_KEY=your_api_key_here
|
LLM_API_KEY=your_api_key_here
|
||||||
|
|
||||||
# Model Configuration
|
# Model Configuration
|
||||||
|
# Intent recognition model (small model recommended for speed)
|
||||||
INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
||||||
|
|
||||||
|
# Chat model (medium model recommended for conversation)
|
||||||
|
CHAT_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct
|
||||||
|
|
||||||
|
# Code generation model (large model recommended for quality)
|
||||||
GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,9 +126,26 @@ Describe file processing tasks:
|
|||||||
### Workflow
|
### Workflow
|
||||||
1. Place input files in `workspace/input/`
|
1. Place input files in `workspace/input/`
|
||||||
2. Describe your task in the chat
|
2. Describe your task in the chat
|
||||||
3. Review the execution plan and generated code
|
3. **If the requirement is vague**, the system will ask clarifying questions:
|
||||||
4. Click "Execute" to run
|
- Radio buttons for single-choice options (e.g., watermark type)
|
||||||
5. Find results in `workspace/output/`
|
- Checkboxes for multi-choice options (e.g., watermark positions)
|
||||||
|
- Input fields for custom values (e.g., watermark text, opacity)
|
||||||
|
4. Review the execution plan and generated code
|
||||||
|
5. Click "Execute" to run
|
||||||
|
6. Find results in `workspace/output/`
|
||||||
|
|
||||||
|
### Requirement Clarification Example
|
||||||
|
|
||||||
|
When you input a vague request like "Add watermark to images", the system will:
|
||||||
|
|
||||||
|
1. **Check completeness** - Detect missing information
|
||||||
|
2. **Ask questions** - Present interactive options:
|
||||||
|
- Watermark type: Text / Image (radio)
|
||||||
|
- Position: Top-left / Top-right / Bottom-left / Bottom-right / Center (checkbox)
|
||||||
|
- Text content: [input field]
|
||||||
|
- Opacity: [input field with default 50%]
|
||||||
|
3. **Structure requirement** - Convert answers into a complete specification
|
||||||
|
4. **Generate code** - Create code based on the structured requirement
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
|||||||
696
app/agent.py
696
app/agent.py
@@ -7,23 +7,30 @@ import os
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, Any, Tuple
|
from typing import Optional, Dict, Any, Tuple, List
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
from llm.client import get_client, LLMClientError
|
from llm.client import get_client, LLMClientError
|
||||||
from llm.prompts import (
|
from llm.prompts import (
|
||||||
EXECUTION_PLAN_SYSTEM, EXECUTION_PLAN_USER,
|
EXECUTION_PLAN_SYSTEM, EXECUTION_PLAN_USER,
|
||||||
CODE_GENERATION_SYSTEM, CODE_GENERATION_USER
|
CODE_GENERATION_SYSTEM, CODE_GENERATION_USER,
|
||||||
|
TASK_SUMMARY_SYSTEM, TASK_SUMMARY_USER,
|
||||||
|
CODE_FIX_SYSTEM, CODE_FIX_USER,
|
||||||
|
REQUIREMENT_CHECK_SYSTEM, REQUIREMENT_CHECK_USER,
|
||||||
|
REQUIREMENT_CLARIFY_SYSTEM, REQUIREMENT_CLARIFY_USER,
|
||||||
|
REQUIREMENT_STRUCTURE_SYSTEM, REQUIREMENT_STRUCTURE_USER
|
||||||
)
|
)
|
||||||
from intent.classifier import classify_intent, IntentResult
|
from intent.classifier import classify_intent, IntentResult
|
||||||
from intent.labels import CHAT, EXECUTION
|
from intent.labels import CHAT, EXECUTION, GUIDANCE
|
||||||
from safety.rule_checker import check_code_safety
|
from safety.rule_checker import check_code_safety
|
||||||
from safety.llm_reviewer import review_code_safety, LLMReviewResult
|
from safety.llm_reviewer import review_code_safety, LLMReviewResult
|
||||||
from executor.sandbox_runner import SandboxRunner, ExecutionResult
|
from executor.sandbox_runner import SandboxRunner, ExecutionResult
|
||||||
from ui.chat_view import ChatView
|
from ui.chat_view import ChatView
|
||||||
from ui.task_guide_view import TaskGuideView
|
from ui.task_guide_view import TaskGuideView
|
||||||
from ui.history_view import HistoryView
|
from ui.history_view import HistoryView
|
||||||
|
from ui.settings_view import SettingsView
|
||||||
|
from ui.clarify_view import ClarifyView
|
||||||
from history.manager import get_history_manager, HistoryManager
|
from history.manager import get_history_manager, HistoryManager
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +62,15 @@ class LocalAgentApp:
|
|||||||
self.chat_view: Optional[ChatView] = None
|
self.chat_view: Optional[ChatView] = None
|
||||||
self.task_view: Optional[TaskGuideView] = None
|
self.task_view: Optional[TaskGuideView] = None
|
||||||
self.history_view: Optional[HistoryView] = None
|
self.history_view: Optional[HistoryView] = None
|
||||||
|
self.settings_view: Optional[SettingsView] = None
|
||||||
|
self.clarify_view: Optional[ClarifyView] = None
|
||||||
|
|
||||||
|
# 需求澄清状态
|
||||||
|
self._clarify_state: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
# 对话上下文(用于多轮对话)
|
||||||
|
self._chat_context: List[Dict[str, str]] = []
|
||||||
|
self._max_context_length: int = 10 # 最多保留的对话轮数
|
||||||
|
|
||||||
# 初始化 UI
|
# 初始化 UI
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
@@ -63,7 +79,8 @@ class LocalAgentApp:
|
|||||||
"""初始化 UI"""
|
"""初始化 UI"""
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.title("LocalAgent - 本地 AI 助手")
|
self.root.title("LocalAgent - 本地 AI 助手")
|
||||||
self.root.geometry("800x700")
|
self.root.geometry("1100x750")
|
||||||
|
self.root.minsize(900, 600)
|
||||||
self.root.configure(bg='#1e1e1e')
|
self.root.configure(bg='#1e1e1e')
|
||||||
|
|
||||||
# 设置窗口图标(如果有的话)
|
# 设置窗口图标(如果有的话)
|
||||||
@@ -80,9 +97,13 @@ class LocalAgentApp:
|
|||||||
self.chat_view = ChatView(
|
self.chat_view = ChatView(
|
||||||
self.main_container,
|
self.main_container,
|
||||||
self._on_user_input,
|
self._on_user_input,
|
||||||
on_show_history=self._show_history
|
on_show_history=self._show_history,
|
||||||
|
on_show_settings=self._show_settings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 设置清空上下文的回调
|
||||||
|
self.chat_view.set_clear_context_callback(self._clear_chat_context)
|
||||||
|
|
||||||
# 定期检查后台任务结果
|
# 定期检查后台任务结果
|
||||||
self._check_queue()
|
self._check_queue()
|
||||||
|
|
||||||
@@ -136,6 +157,9 @@ class LocalAgentApp:
|
|||||||
if intent_result.label == CHAT:
|
if intent_result.label == CHAT:
|
||||||
# 对话模式
|
# 对话模式
|
||||||
self._handle_chat(user_input, intent_result)
|
self._handle_chat(user_input, intent_result)
|
||||||
|
elif intent_result.label == GUIDANCE:
|
||||||
|
# 操作指导模式
|
||||||
|
self._handle_guidance(user_input, intent_result)
|
||||||
else:
|
else:
|
||||||
# 执行模式
|
# 执行模式
|
||||||
self._handle_execution(user_input, intent_result)
|
self._handle_execution(user_input, intent_result)
|
||||||
@@ -147,17 +171,24 @@ class LocalAgentApp:
|
|||||||
'system'
|
'system'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 添加用户消息到上下文
|
||||||
|
self._chat_context.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
# 开始流式消息
|
# 开始流式消息
|
||||||
self.chat_view.start_stream_message('assistant')
|
self.chat_view.start_stream_message('assistant')
|
||||||
|
|
||||||
# 在后台线程调用 LLM(流式)
|
# 在后台线程调用 LLM(流式)
|
||||||
def do_chat_stream():
|
def do_chat_stream():
|
||||||
client = get_client()
|
client = get_client()
|
||||||
model = os.getenv("GENERATION_MODEL_NAME")
|
# 使用专门的对话模型,如果未配置则使用代码生成模型
|
||||||
|
model = os.getenv("CHAT_MODEL_NAME") or os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
# 构建带上下文的消息列表
|
||||||
|
messages = self._build_chat_messages()
|
||||||
|
|
||||||
full_response = []
|
full_response = []
|
||||||
for chunk in client.chat_stream(
|
for chunk in client.chat_stream(
|
||||||
messages=[{"role": "user", "content": user_input}],
|
messages=messages,
|
||||||
model=model,
|
model=model,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
max_tokens=2048,
|
max_tokens=2048,
|
||||||
@@ -184,16 +215,94 @@ class LocalAgentApp:
|
|||||||
|
|
||||||
if error:
|
if error:
|
||||||
self.chat_view.add_message(f"对话失败: {str(error)}", 'error')
|
self.chat_view.add_message(f"对话失败: {str(error)}", 'error')
|
||||||
|
elif response:
|
||||||
|
# 保存助手回复到上下文
|
||||||
|
self._chat_context.append({"role": "assistant", "content": response})
|
||||||
|
# 限制上下文长度
|
||||||
|
self._trim_chat_context()
|
||||||
|
|
||||||
self.chat_view.set_input_enabled(True)
|
self.chat_view.set_input_enabled(True)
|
||||||
|
|
||||||
|
def _build_chat_messages(self) -> List[Dict[str, str]]:
|
||||||
|
"""构建带上下文的消息列表"""
|
||||||
|
system_prompt = """你是一个智能助手,可以回答各种问题。请用中文回答。
|
||||||
|
|
||||||
|
如果用户的问题涉及之前的对话内容,请结合上下文进行回答。"""
|
||||||
|
|
||||||
|
messages = [{"role": "system", "content": system_prompt}]
|
||||||
|
messages.extend(self._chat_context)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def _trim_chat_context(self) -> None:
|
||||||
|
"""限制对话上下文长度"""
|
||||||
|
# 每轮对话包含 user 和 assistant 两条消息
|
||||||
|
max_messages = self._max_context_length * 2
|
||||||
|
if len(self._chat_context) > max_messages:
|
||||||
|
# 保留最近的消息
|
||||||
|
self._chat_context = self._chat_context[-max_messages:]
|
||||||
|
|
||||||
|
def _clear_chat_context(self) -> None:
|
||||||
|
"""清空对话上下文"""
|
||||||
|
self._chat_context = []
|
||||||
|
|
||||||
|
def _handle_guidance(self, user_input: str, intent_result: IntentResult) -> None:
|
||||||
|
"""处理操作指导任务(无法通过本地代码完成的任务)"""
|
||||||
|
self.chat_view.add_message(
|
||||||
|
f"识别为操作指导 (原因: {intent_result.reason})\n该任务无法通过本地代码完成,将为您提供操作指导。",
|
||||||
|
'system'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加用户消息到上下文
|
||||||
|
self._chat_context.append({"role": "user", "content": user_input})
|
||||||
|
|
||||||
|
# 开始流式消息
|
||||||
|
self.chat_view.start_stream_message('assistant')
|
||||||
|
|
||||||
|
# 在后台线程调用 LLM(流式)
|
||||||
|
def do_guidance_stream():
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("CHAT_MODEL_NAME") or os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
# 构建专门的操作指导 Prompt
|
||||||
|
system_prompt = """你是一个操作指导助手。用户询问的是一个无法通过本地Python代码完成的任务(如软件设置、系统配置、GUI操作等)。
|
||||||
|
|
||||||
|
请提供清晰、详细的操作步骤指导:
|
||||||
|
1. 使用编号列表,步骤清晰
|
||||||
|
2. 如果有多种方法,列出最常用的1-2种
|
||||||
|
3. 如果涉及不同操作系统/软件版本,说明适用范围
|
||||||
|
4. 可以适当配合说明截图位置或界面元素名称
|
||||||
|
5. 如果操作有风险,给出提醒
|
||||||
|
|
||||||
|
用中文回答。"""
|
||||||
|
|
||||||
|
# 构建带上下文的消息列表
|
||||||
|
messages = [{"role": "system", "content": system_prompt}]
|
||||||
|
messages.extend(self._chat_context)
|
||||||
|
|
||||||
|
full_response = []
|
||||||
|
for chunk in client.chat_stream(
|
||||||
|
messages=messages,
|
||||||
|
model=model,
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=2048,
|
||||||
|
timeout=300
|
||||||
|
):
|
||||||
|
full_response.append(chunk)
|
||||||
|
self.result_queue.put((self._on_chat_chunk, (chunk,)))
|
||||||
|
|
||||||
|
return ''.join(full_response)
|
||||||
|
|
||||||
|
self._run_in_thread(
|
||||||
|
do_guidance_stream,
|
||||||
|
self._on_chat_complete
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_execution(self, user_input: str, intent_result: IntentResult):
|
def _handle_execution(self, user_input: str, intent_result: IntentResult):
|
||||||
"""处理执行任务"""
|
"""处理执行任务"""
|
||||||
self.chat_view.add_message(
|
self.chat_view.add_message(
|
||||||
f"识别为执行任务 (置信度: {intent_result.confidence:.0%})\n原因: {intent_result.reason}",
|
f"识别为执行任务 (置信度: {intent_result.confidence:.0%})\n原因: {intent_result.reason}",
|
||||||
'system'
|
'system'
|
||||||
)
|
)
|
||||||
self.chat_view.show_loading("正在生成执行计划")
|
|
||||||
|
|
||||||
# 保存用户输入和意图结果
|
# 保存用户输入和意图结果
|
||||||
self.current_task = {
|
self.current_task = {
|
||||||
@@ -201,10 +310,36 @@ class LocalAgentApp:
|
|||||||
'intent_result': intent_result
|
'intent_result': intent_result
|
||||||
}
|
}
|
||||||
|
|
||||||
# 在后台线程生成执行计划
|
# 先查找是否有相似的成功任务
|
||||||
|
similar_record = self.history.find_similar_success(user_input)
|
||||||
|
if similar_record:
|
||||||
|
# 询问用户是否复用
|
||||||
|
task_desc = similar_record.task_summary or similar_record.user_input[:50]
|
||||||
|
msg = (
|
||||||
|
f"发现相似的成功任务:\n\n"
|
||||||
|
f"任务: {task_desc}\n"
|
||||||
|
f"时间: {similar_record.timestamp}\n\n"
|
||||||
|
f"是否直接复用该任务的代码?\n"
|
||||||
|
f"(选择[否]将生成新代码)"
|
||||||
|
)
|
||||||
|
result = messagebox.askyesno("发现相似任务", msg, icon='question')
|
||||||
|
if result:
|
||||||
|
# 复用代码
|
||||||
|
self.current_task['execution_plan'] = similar_record.execution_plan
|
||||||
|
self.current_task['code'] = similar_record.code
|
||||||
|
self.current_task['task_summary'] = similar_record.task_summary
|
||||||
|
self.current_task['is_reuse'] = True
|
||||||
|
|
||||||
|
self.chat_view.add_message("复用历史成功代码,请确认执行", 'system')
|
||||||
|
self._show_task_guide()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.chat_view.show_loading("正在分析需求完整性")
|
||||||
|
|
||||||
|
# 检查需求是否完整
|
||||||
self._run_in_thread(
|
self._run_in_thread(
|
||||||
self._generate_execution_plan,
|
self._check_requirement_completeness,
|
||||||
self._on_plan_generated,
|
self._on_requirement_checked,
|
||||||
user_input
|
user_input
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,10 +359,374 @@ class LocalAgentApp:
|
|||||||
self._run_in_thread(
|
self._run_in_thread(
|
||||||
self._generate_code,
|
self._generate_code,
|
||||||
self._on_code_generated,
|
self._on_code_generated,
|
||||||
self.current_task['user_input'],
|
self.current_task.get('structured_requirement') or self.current_task['user_input'],
|
||||||
plan
|
plan
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _check_requirement_completeness(self, user_input: str) -> Dict[str, Any]:
|
||||||
|
"""检查需求是否完整"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("CHAT_MODEL_NAME") or os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
response = client.chat(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": REQUIREMENT_CHECK_SYSTEM},
|
||||||
|
{"role": "user", "content": REQUIREMENT_CHECK_USER.format(user_input=user_input)}
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=500,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
# 解析 JSON 响应
|
||||||
|
try:
|
||||||
|
# 尝试提取 JSON
|
||||||
|
json_match = response
|
||||||
|
if '```' in response:
|
||||||
|
import re
|
||||||
|
match = re.search(r'```(?:json)?\s*(.*?)\s*```', response, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
json_match = match.group(1)
|
||||||
|
|
||||||
|
result = json.loads(json_match)
|
||||||
|
return result
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 解析失败,默认认为需求完整
|
||||||
|
return {
|
||||||
|
"is_complete": True,
|
||||||
|
"confidence": 0.5,
|
||||||
|
"reason": "无法解析完整性检查结果",
|
||||||
|
"suggested_defaults": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _on_requirement_checked(self, result: Optional[Dict], error: Optional[Exception]):
|
||||||
|
"""需求完整性检查完成回调"""
|
||||||
|
if error:
|
||||||
|
# 检查失败,继续正常流程
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
self.chat_view.add_message(f"需求分析失败,将直接生成代码: {str(error)}", 'system')
|
||||||
|
self._continue_to_code_generation()
|
||||||
|
return
|
||||||
|
|
||||||
|
is_complete = result.get('is_complete', True)
|
||||||
|
confidence = result.get('confidence', 1.0)
|
||||||
|
|
||||||
|
# 如果需求完整或置信度较高,直接继续
|
||||||
|
if is_complete and confidence >= 0.7:
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
# 保存建议的默认值
|
||||||
|
self.current_task['suggested_defaults'] = result.get('suggested_defaults', {})
|
||||||
|
self._continue_to_code_generation()
|
||||||
|
else:
|
||||||
|
# 需求不完整,启动澄清流程
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
self.chat_view.add_message(
|
||||||
|
f"需求信息不完整 (原因: {result.get('reason', '缺少关键信息')})\n正在启动需求澄清...",
|
||||||
|
'system'
|
||||||
|
)
|
||||||
|
self._start_clarification()
|
||||||
|
|
||||||
|
def _continue_to_code_generation(self):
|
||||||
|
"""继续代码生成流程"""
|
||||||
|
self.chat_view.show_loading("正在生成任务摘要")
|
||||||
|
|
||||||
|
# 在后台线程生成任务摘要
|
||||||
|
self._run_in_thread(
|
||||||
|
self._generate_task_summary,
|
||||||
|
self._on_summary_generated,
|
||||||
|
self.current_task.get('structured_requirement') or self.current_task['user_input']
|
||||||
|
)
|
||||||
|
|
||||||
|
def _start_clarification(self):
|
||||||
|
"""启动需求澄清流程"""
|
||||||
|
# 初始化澄清状态
|
||||||
|
self._clarify_state = {
|
||||||
|
'original_input': self.current_task['user_input'],
|
||||||
|
'collected_info': {},
|
||||||
|
'history': [],
|
||||||
|
'current_question': None
|
||||||
|
}
|
||||||
|
|
||||||
|
# 重置已显示的历史计数
|
||||||
|
self._displayed_history_count = 0
|
||||||
|
|
||||||
|
self.chat_view.show_loading("正在生成澄清问题")
|
||||||
|
|
||||||
|
# 获取第一个澄清问题
|
||||||
|
self._run_in_thread(
|
||||||
|
self._get_clarify_question,
|
||||||
|
self._on_clarify_question_received,
|
||||||
|
self.current_task['user_input'],
|
||||||
|
{},
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_clarify_question(self, user_input: str, collected_info: Dict, user_answer: str) -> Dict[str, Any]:
|
||||||
|
"""获取澄清问题"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("CHAT_MODEL_NAME") or os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
# 格式化已收集的信息
|
||||||
|
collected_str = json.dumps(collected_info, ensure_ascii=False, indent=2) if collected_info else "{}"
|
||||||
|
|
||||||
|
response = client.chat(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": REQUIREMENT_CLARIFY_SYSTEM},
|
||||||
|
{"role": "user", "content": REQUIREMENT_CLARIFY_USER.format(
|
||||||
|
user_input=user_input,
|
||||||
|
collected_info=collected_str,
|
||||||
|
user_answer=user_answer or "(首次询问)"
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=1000,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
# 解析 JSON 响应
|
||||||
|
try:
|
||||||
|
json_match = response
|
||||||
|
if '```' in response:
|
||||||
|
import re
|
||||||
|
match = re.search(r'```(?:json)?\s*(.*?)\s*```', response, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
json_match = match.group(1)
|
||||||
|
|
||||||
|
result = json.loads(json_match)
|
||||||
|
return result
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 解析失败,认为不需要继续澄清
|
||||||
|
return {
|
||||||
|
"need_clarify": False,
|
||||||
|
"question": "",
|
||||||
|
"options": [],
|
||||||
|
"collected_info": collected_info,
|
||||||
|
"missing_info": []
|
||||||
|
}
|
||||||
|
|
||||||
|
def _on_clarify_question_received(self, result: Optional[Dict], error: Optional[Exception]):
|
||||||
|
"""收到澄清问题回调"""
|
||||||
|
# 隐藏澄清视图的加载状态(如果有)
|
||||||
|
if self.clarify_view:
|
||||||
|
self.clarify_view.hide_loading()
|
||||||
|
|
||||||
|
if error:
|
||||||
|
# 出错时切换回聊天界面
|
||||||
|
if self.clarify_view:
|
||||||
|
self.clarify_view.hide()
|
||||||
|
self.clarify_view = None
|
||||||
|
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
self.chat_view.add_message(f"获取澄清问题失败: {str(error)}", 'error')
|
||||||
|
self._continue_to_code_generation()
|
||||||
|
return
|
||||||
|
|
||||||
|
need_clarify = result.get('need_clarify', False)
|
||||||
|
|
||||||
|
if not need_clarify:
|
||||||
|
# 不需要继续澄清,隐藏澄清视图,进行需求结构化
|
||||||
|
if self.clarify_view:
|
||||||
|
self.clarify_view.hide()
|
||||||
|
self.clarify_view = None
|
||||||
|
self._clarify_state['collected_info'].update(result.get('collected_info', {}))
|
||||||
|
self._structure_requirement()
|
||||||
|
else:
|
||||||
|
# 继续显示/更新澄清视图
|
||||||
|
self._show_clarify_view(result)
|
||||||
|
|
||||||
|
def _show_clarify_view(self, clarify_data: Dict):
|
||||||
|
"""显示或更新需求澄清视图"""
|
||||||
|
# 如果澄清视图不存在,创建新的
|
||||||
|
if not self.clarify_view:
|
||||||
|
# 隐藏聊天视图
|
||||||
|
self.chat_view.get_frame().pack_forget()
|
||||||
|
|
||||||
|
# 创建澄清视图
|
||||||
|
self.clarify_view = ClarifyView(
|
||||||
|
self.main_container,
|
||||||
|
on_submit=self._on_clarify_submit,
|
||||||
|
on_cancel=self._on_clarify_cancel
|
||||||
|
)
|
||||||
|
self.clarify_view.show()
|
||||||
|
|
||||||
|
# 添加上一轮的历史记录(如果有新的)
|
||||||
|
history = self._clarify_state.get('history', [])
|
||||||
|
displayed_count = getattr(self, '_displayed_history_count', 0)
|
||||||
|
|
||||||
|
for item in history[displayed_count:]:
|
||||||
|
self.clarify_view.add_history_item(item['question'], item['answer'])
|
||||||
|
|
||||||
|
self._displayed_history_count = len(history)
|
||||||
|
|
||||||
|
# 设置新问题和选项
|
||||||
|
question = clarify_data.get('question', '请提供更多信息')
|
||||||
|
options = clarify_data.get('options', [])
|
||||||
|
|
||||||
|
self.clarify_view.set_question(question, options)
|
||||||
|
|
||||||
|
# 更新已收集信息提示
|
||||||
|
collected = self._clarify_state.get('collected_info', {})
|
||||||
|
missing = clarify_data.get('missing_info', [])
|
||||||
|
self.clarify_view.update_info_label(len(collected), len(collected) + len(missing))
|
||||||
|
|
||||||
|
# 保存当前问题
|
||||||
|
self._clarify_state['current_question'] = question
|
||||||
|
self._clarify_state['current_options'] = options
|
||||||
|
|
||||||
|
def _on_clarify_submit(self, answers: Dict[str, Any]):
|
||||||
|
"""澄清问题提交回调"""
|
||||||
|
# 格式化答案为字符串
|
||||||
|
answer_parts = []
|
||||||
|
for key, value in answers.items():
|
||||||
|
if isinstance(value, list):
|
||||||
|
answer_parts.append(f"{key}: {', '.join(value)}")
|
||||||
|
else:
|
||||||
|
answer_parts.append(f"{key}: {value}")
|
||||||
|
answer_str = "; ".join(answer_parts)
|
||||||
|
|
||||||
|
# 保存到历史
|
||||||
|
self._clarify_state['history'].append({
|
||||||
|
'question': self._clarify_state.get('current_question', ''),
|
||||||
|
'answer': answer_str
|
||||||
|
})
|
||||||
|
|
||||||
|
# 更新已收集的信息
|
||||||
|
self._clarify_state['collected_info'].update(answers)
|
||||||
|
|
||||||
|
# 在澄清视图中显示加载状态(不切换回聊天界面)
|
||||||
|
if self.clarify_view:
|
||||||
|
self.clarify_view.show_loading("正在分析您的回答...")
|
||||||
|
|
||||||
|
# 继续获取下一个问题
|
||||||
|
self._run_in_thread(
|
||||||
|
self._get_clarify_question,
|
||||||
|
self._on_clarify_question_received,
|
||||||
|
self._clarify_state['original_input'],
|
||||||
|
self._clarify_state['collected_info'],
|
||||||
|
answer_str
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_clarify_cancel(self):
|
||||||
|
"""取消澄清"""
|
||||||
|
if self.clarify_view:
|
||||||
|
self.clarify_view.hide()
|
||||||
|
self.clarify_view = None
|
||||||
|
|
||||||
|
self._clarify_state = None
|
||||||
|
self._displayed_history_count = 0
|
||||||
|
self.current_task = None
|
||||||
|
|
||||||
|
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.chat_view.add_message("已取消需求澄清", 'system')
|
||||||
|
|
||||||
|
def _structure_requirement(self):
|
||||||
|
"""将澄清后的需求结构化"""
|
||||||
|
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
self.chat_view.show_loading("正在整理需求")
|
||||||
|
|
||||||
|
self._run_in_thread(
|
||||||
|
self._do_structure_requirement,
|
||||||
|
self._on_requirement_structured
|
||||||
|
)
|
||||||
|
|
||||||
|
def _do_structure_requirement(self) -> str:
|
||||||
|
"""执行需求结构化"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("CHAT_MODEL_NAME") or os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
collected_str = json.dumps(
|
||||||
|
self._clarify_state['collected_info'],
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat_stream_collect(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": REQUIREMENT_STRUCTURE_SYSTEM},
|
||||||
|
{"role": "user", "content": REQUIREMENT_STRUCTURE_USER.format(
|
||||||
|
user_input=self._clarify_state['original_input'],
|
||||||
|
collected_info=collected_str
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=1500,
|
||||||
|
timeout=120
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _on_requirement_structured(self, result: Optional[str], error: Optional[Exception]):
|
||||||
|
"""需求结构化完成回调"""
|
||||||
|
if error:
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
self.chat_view.add_message(f"需求整理失败: {str(error)}", 'error')
|
||||||
|
# 使用原始输入继续
|
||||||
|
self._continue_to_code_generation()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 保存结构化的需求
|
||||||
|
self.current_task['structured_requirement'] = result
|
||||||
|
self.current_task['collected_info'] = self._clarify_state['collected_info']
|
||||||
|
|
||||||
|
# 清理澄清状态
|
||||||
|
self._clarify_state = None
|
||||||
|
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
self.chat_view.add_message("需求已明确,开始生成代码", 'system')
|
||||||
|
|
||||||
|
# 继续代码生成流程
|
||||||
|
self._continue_to_code_generation()
|
||||||
|
|
||||||
|
def _generate_task_summary(self, user_input: str) -> str:
|
||||||
|
"""生成任务摘要(使用小模型)"""
|
||||||
|
client = get_client()
|
||||||
|
# 使用意图识别模型(小模型)生成摘要
|
||||||
|
model = os.getenv("INTENT_MODEL_NAME") or os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
response = client.chat(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": TASK_SUMMARY_SYSTEM},
|
||||||
|
{"role": "user", "content": TASK_SUMMARY_USER.format(user_input=user_input)}
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=50,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# 清理响应(去除引号、换行等)
|
||||||
|
summary = response.strip().strip('"\'').strip()
|
||||||
|
# 限制长度
|
||||||
|
if len(summary) > 20:
|
||||||
|
summary = summary[:20]
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
def _on_summary_generated(self, summary: Optional[str], error: Optional[Exception]):
|
||||||
|
"""任务摘要生成完成回调"""
|
||||||
|
if error:
|
||||||
|
# 摘要生成失败不影响主流程,使用默认值
|
||||||
|
summary = self.current_task['user_input'][:15] + "..."
|
||||||
|
|
||||||
|
self.current_task['task_summary'] = summary
|
||||||
|
self.chat_view.update_loading_text("正在生成执行计划")
|
||||||
|
|
||||||
|
# 继续生成执行计划
|
||||||
|
self._run_in_thread(
|
||||||
|
self._generate_execution_plan,
|
||||||
|
self._on_plan_generated,
|
||||||
|
self.current_task['user_input']
|
||||||
|
)
|
||||||
|
|
||||||
def _on_code_generated(self, result: tuple, error: Optional[Exception]):
|
def _on_code_generated(self, result: tuple, error: Optional[Exception]):
|
||||||
"""代码生成完成回调"""
|
"""代码生成完成回调"""
|
||||||
if error:
|
if error:
|
||||||
@@ -298,6 +797,9 @@ class LocalAgentApp:
|
|||||||
self.current_task = None
|
self.current_task = None
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 代码生成完成,清空 input 和 output 目录
|
||||||
|
self.runner.clear_workspace(clear_input=True, clear_output=True)
|
||||||
|
|
||||||
self.chat_view.add_message("安全检查通过,请确认执行", 'system')
|
self.chat_view.add_message("安全检查通过,请确认执行", 'system')
|
||||||
|
|
||||||
# 显示任务引导视图
|
# 显示任务引导视图
|
||||||
@@ -439,7 +941,8 @@ class LocalAgentApp:
|
|||||||
duration_ms=result.duration_ms,
|
duration_ms=result.duration_ms,
|
||||||
stdout=result.stdout,
|
stdout=result.stdout,
|
||||||
stderr=result.stderr,
|
stderr=result.stderr,
|
||||||
log_path=result.log_path
|
log_path=result.log_path,
|
||||||
|
task_summary=self.current_task.get('task_summary', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
self._show_execution_result(result)
|
self._show_execution_result(result)
|
||||||
@@ -508,7 +1011,9 @@ class LocalAgentApp:
|
|||||||
self.history_view = HistoryView(
|
self.history_view = HistoryView(
|
||||||
self.main_container,
|
self.main_container,
|
||||||
self.history,
|
self.history,
|
||||||
on_back=self._hide_history
|
on_back=self._hide_history,
|
||||||
|
on_reuse_code=self._on_reuse_code,
|
||||||
|
on_retry_task=self._on_retry_task
|
||||||
)
|
)
|
||||||
self.history_view.show()
|
self.history_view.show()
|
||||||
|
|
||||||
@@ -520,6 +1025,169 @@ class LocalAgentApp:
|
|||||||
|
|
||||||
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
def _on_reuse_code(self, record):
|
||||||
|
"""复用历史记录中的代码"""
|
||||||
|
from history.manager import TaskRecord
|
||||||
|
|
||||||
|
# 隐藏历史视图
|
||||||
|
self._hide_history()
|
||||||
|
|
||||||
|
# 设置当前任务
|
||||||
|
self.current_task = {
|
||||||
|
'user_input': record.user_input,
|
||||||
|
'intent_result': IntentResult(
|
||||||
|
label=record.intent_label,
|
||||||
|
confidence=record.intent_confidence,
|
||||||
|
reason="复用历史任务"
|
||||||
|
),
|
||||||
|
'execution_plan': record.execution_plan,
|
||||||
|
'code': record.code,
|
||||||
|
'task_summary': record.task_summary,
|
||||||
|
'is_reuse': True
|
||||||
|
}
|
||||||
|
|
||||||
|
self.chat_view.add_message(f"复用历史任务: {record.task_summary or record.user_input[:30]}", 'system')
|
||||||
|
self.chat_view.add_message("已加载历史代码,请确认执行", 'system')
|
||||||
|
|
||||||
|
# 直接显示任务引导视图(跳过代码生成)
|
||||||
|
self._show_task_guide()
|
||||||
|
|
||||||
|
def _on_retry_task(self, record):
|
||||||
|
"""重试失败的任务(AI 修复)"""
|
||||||
|
from history.manager import TaskRecord
|
||||||
|
|
||||||
|
# 隐藏历史视图
|
||||||
|
self._hide_history()
|
||||||
|
|
||||||
|
self.chat_view.add_message(f"重试任务: {record.task_summary or record.user_input[:30]}", 'system')
|
||||||
|
self.chat_view.show_loading("正在分析错误并修复代码")
|
||||||
|
self.chat_view.set_input_enabled(False)
|
||||||
|
|
||||||
|
# 保存任务信息
|
||||||
|
self.current_task = {
|
||||||
|
'user_input': record.user_input,
|
||||||
|
'intent_result': IntentResult(
|
||||||
|
label=record.intent_label,
|
||||||
|
confidence=record.intent_confidence,
|
||||||
|
reason="重试失败任务"
|
||||||
|
),
|
||||||
|
'execution_plan': record.execution_plan,
|
||||||
|
'original_code': record.code,
|
||||||
|
'original_stdout': record.stdout,
|
||||||
|
'original_stderr': record.stderr,
|
||||||
|
'task_summary': record.task_summary,
|
||||||
|
'is_retry': True
|
||||||
|
}
|
||||||
|
|
||||||
|
# 在后台线程修复代码
|
||||||
|
self._run_in_thread(
|
||||||
|
self._fix_code,
|
||||||
|
self._on_code_fixed,
|
||||||
|
record
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fix_code(self, record) -> tuple:
|
||||||
|
"""修复失败的代码"""
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
response = client.chat_stream_collect(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": CODE_FIX_SYSTEM},
|
||||||
|
{"role": "user", "content": CODE_FIX_USER.format(
|
||||||
|
user_input=record.user_input,
|
||||||
|
execution_plan=record.execution_plan,
|
||||||
|
code=record.code,
|
||||||
|
stdout=record.stdout or "(无输出)",
|
||||||
|
stderr=record.stderr or "(无错误信息)"
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=0.2,
|
||||||
|
max_tokens=4096,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
code = self._extract_code(response)
|
||||||
|
return (code, None)
|
||||||
|
except ValueError as e:
|
||||||
|
return (None, e)
|
||||||
|
|
||||||
|
def _on_code_fixed(self, result: tuple, error: Optional[Exception]):
|
||||||
|
"""代码修复完成回调"""
|
||||||
|
if error:
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
self.chat_view.add_message(f"代码修复失败: {str(error)}", 'error')
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
return
|
||||||
|
|
||||||
|
code, extract_error = result
|
||||||
|
if extract_error:
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
self.chat_view.add_message(f"代码提取失败: {str(extract_error)}", 'error')
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_task['code'] = code
|
||||||
|
self.chat_view.update_loading_text("正在进行安全检查")
|
||||||
|
|
||||||
|
# 硬规则检查
|
||||||
|
rule_result = check_code_safety(code)
|
||||||
|
if not rule_result.passed:
|
||||||
|
self.chat_view.hide_loading()
|
||||||
|
violations = "\n".join(f" • {v}" for v in rule_result.violations)
|
||||||
|
self.chat_view.add_message(
|
||||||
|
f"修复后的代码安全检查未通过:\n{violations}",
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_task['warnings'] = rule_result.warnings
|
||||||
|
|
||||||
|
# LLM 安全审查
|
||||||
|
self._run_in_thread(
|
||||||
|
lambda: review_code_safety(
|
||||||
|
self.current_task['user_input'],
|
||||||
|
self.current_task['execution_plan'],
|
||||||
|
code,
|
||||||
|
rule_result.warnings
|
||||||
|
),
|
||||||
|
self._on_safety_reviewed
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_settings(self):
|
||||||
|
"""显示设置视图"""
|
||||||
|
# 隐藏聊天视图
|
||||||
|
self.chat_view.get_frame().pack_forget()
|
||||||
|
|
||||||
|
# 创建设置视图
|
||||||
|
self.settings_view = SettingsView(
|
||||||
|
self.main_container,
|
||||||
|
env_path=self.project_root / ".env",
|
||||||
|
on_save=self._on_settings_saved,
|
||||||
|
on_back=self._hide_settings
|
||||||
|
)
|
||||||
|
self.settings_view.show()
|
||||||
|
|
||||||
|
def _hide_settings(self):
|
||||||
|
"""隐藏设置视图,返回聊天"""
|
||||||
|
if self.settings_view:
|
||||||
|
self.settings_view.hide()
|
||||||
|
self.settings_view = None
|
||||||
|
|
||||||
|
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
def _on_settings_saved(self):
|
||||||
|
"""设置保存后的回调"""
|
||||||
|
# 配置已通过 set_key 保存并更新了环境变量
|
||||||
|
# 可以在这里添加额外的处理逻辑
|
||||||
|
pass
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""运行应用"""
|
"""运行应用"""
|
||||||
self.root.mainloop()
|
self.root.mainloop()
|
||||||
|
|||||||
@@ -119,8 +119,15 @@ class SandboxRunner:
|
|||||||
duration_ms=duration_ms
|
duration_ms=duration_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 判断是否成功:return code 为 0 且没有明显的失败迹象
|
||||||
|
success = self._check_execution_success(
|
||||||
|
result.returncode,
|
||||||
|
result.stdout,
|
||||||
|
result.stderr
|
||||||
|
)
|
||||||
|
|
||||||
return ExecutionResult(
|
return ExecutionResult(
|
||||||
success=result.returncode == 0,
|
success=success,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
stdout=result.stdout,
|
stdout=result.stdout,
|
||||||
stderr=result.stderr,
|
stderr=result.stderr,
|
||||||
@@ -187,6 +194,99 @@ class SandboxRunner:
|
|||||||
short_uuid = uuid.uuid4().hex[:6]
|
short_uuid = uuid.uuid4().hex[:6]
|
||||||
return f"{timestamp}_{short_uuid}"
|
return f"{timestamp}_{short_uuid}"
|
||||||
|
|
||||||
|
def clear_workspace(self, clear_input: bool = True, clear_output: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
清空工作目录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
clear_input: 是否清空 input 目录
|
||||||
|
clear_output: 是否清空 output 目录
|
||||||
|
"""
|
||||||
|
if clear_input:
|
||||||
|
self._clear_directory(self.input_dir)
|
||||||
|
if clear_output:
|
||||||
|
self._clear_directory(self.output_dir)
|
||||||
|
|
||||||
|
def _clear_directory(self, directory: Path) -> None:
|
||||||
|
"""
|
||||||
|
清空目录中的所有文件和子目录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: 要清空的目录路径
|
||||||
|
"""
|
||||||
|
if not directory.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
for item in directory.iterdir():
|
||||||
|
try:
|
||||||
|
if item.is_file():
|
||||||
|
item.unlink()
|
||||||
|
elif item.is_dir():
|
||||||
|
shutil.rmtree(item)
|
||||||
|
except Exception as e:
|
||||||
|
# 忽略删除失败的文件(可能被占用)
|
||||||
|
print(f"Warning: Failed to delete {item}: {e}")
|
||||||
|
|
||||||
|
def _check_execution_success(self, return_code: int, stdout: str, stderr: str) -> bool:
|
||||||
|
"""
|
||||||
|
检查执行是否成功
|
||||||
|
|
||||||
|
判断逻辑:
|
||||||
|
1. return code 必须为 0
|
||||||
|
2. 检查输出中是否有失败迹象
|
||||||
|
3. 如果有成功和失败的统计,根据失败数量判断
|
||||||
|
"""
|
||||||
|
# return code 不为 0 直接判定失败
|
||||||
|
if return_code != 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查 stderr 是否有内容(通常表示有错误)
|
||||||
|
if stderr and stderr.strip():
|
||||||
|
# 如果 stderr 有实质内容,可能是失败
|
||||||
|
# 但有些程序会把警告也输出到 stderr,所以不直接判定失败
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 检查 stdout 中的失败迹象
|
||||||
|
output = stdout.lower() if stdout else ""
|
||||||
|
|
||||||
|
# 查找失败统计模式,如 "失败 27 个" 或 "failed: 27"
|
||||||
|
import re
|
||||||
|
|
||||||
|
# 中文模式:成功 X 个, 失败 Y 个
|
||||||
|
pattern_cn = r'成功\s*(\d+)\s*个.*失败\s*(\d+)\s*个'
|
||||||
|
match = re.search(pattern_cn, stdout if stdout else "")
|
||||||
|
if match:
|
||||||
|
success_count = int(match.group(1))
|
||||||
|
fail_count = int(match.group(2))
|
||||||
|
# 如果有失败的,判定为失败
|
||||||
|
if fail_count > 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 英文模式:success: X, failed: Y
|
||||||
|
pattern_en = r'success[:\s]+(\d+).*fail(?:ed)?[:\s]+(\d+)'
|
||||||
|
match = re.search(pattern_en, output)
|
||||||
|
if match:
|
||||||
|
success_count = int(match.group(1))
|
||||||
|
fail_count = int(match.group(2))
|
||||||
|
if fail_count > 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查是否有明显的失败关键词
|
||||||
|
failure_keywords = ['失败', 'error', 'exception', 'traceback', 'failed']
|
||||||
|
for keyword in failure_keywords:
|
||||||
|
if keyword in output:
|
||||||
|
# 如果包含失败关键词,进一步检查是否是统计信息
|
||||||
|
# 如果是 "失败 0 个" 这种,不算失败
|
||||||
|
if '失败 0' in stdout or '失败: 0' in stdout or 'failed: 0' in output or 'failed 0' in output:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _get_safe_env(self) -> dict:
|
def _get_safe_env(self) -> dict:
|
||||||
"""获取安全的环境变量(移除网络代理等)"""
|
"""获取安全的环境变量(移除网络代理等)"""
|
||||||
safe_env = os.environ.copy()
|
safe_env = os.environ.copy()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class TaskRecord:
|
|||||||
stdout: str
|
stdout: str
|
||||||
stderr: str
|
stderr: str
|
||||||
log_path: str
|
log_path: str
|
||||||
|
task_summary: str = "" # 任务摘要(由小模型生成)
|
||||||
|
|
||||||
|
|
||||||
class HistoryManager:
|
class HistoryManager:
|
||||||
@@ -83,7 +84,8 @@ class HistoryManager:
|
|||||||
duration_ms: int,
|
duration_ms: int,
|
||||||
stdout: str = "",
|
stdout: str = "",
|
||||||
stderr: str = "",
|
stderr: str = "",
|
||||||
log_path: str = ""
|
log_path: str = "",
|
||||||
|
task_summary: str = ""
|
||||||
) -> TaskRecord:
|
) -> TaskRecord:
|
||||||
"""
|
"""
|
||||||
添加一条任务记录
|
添加一条任务记录
|
||||||
@@ -100,6 +102,7 @@ class HistoryManager:
|
|||||||
stdout: 标准输出
|
stdout: 标准输出
|
||||||
stderr: 标准错误
|
stderr: 标准错误
|
||||||
log_path: 日志文件路径
|
log_path: 日志文件路径
|
||||||
|
task_summary: 任务摘要
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
TaskRecord: 创建的记录
|
TaskRecord: 创建的记录
|
||||||
@@ -116,7 +119,8 @@ class HistoryManager:
|
|||||||
duration_ms=duration_ms,
|
duration_ms=duration_ms,
|
||||||
stdout=stdout,
|
stdout=stdout,
|
||||||
stderr=stderr,
|
stderr=stderr,
|
||||||
log_path=log_path
|
log_path=log_path,
|
||||||
|
task_summary=task_summary
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到列表开头(最新的在前)
|
# 添加到列表开头(最新的在前)
|
||||||
@@ -146,6 +150,43 @@ class HistoryManager:
|
|||||||
return record
|
return record
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def delete_by_id(self, task_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
根据任务 ID 删除记录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id: 任务 ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否删除成功
|
||||||
|
"""
|
||||||
|
for i, record in enumerate(self._history):
|
||||||
|
if record.task_id == task_id:
|
||||||
|
self._history.pop(i)
|
||||||
|
self._save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_multiple(self, task_ids: List[str]) -> int:
|
||||||
|
"""
|
||||||
|
批量删除记录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_ids: 任务 ID 列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
删除的记录数量
|
||||||
|
"""
|
||||||
|
task_id_set = set(task_ids)
|
||||||
|
original_count = len(self._history)
|
||||||
|
self._history = [r for r in self._history if r.task_id not in task_id_set]
|
||||||
|
deleted_count = original_count - len(self._history)
|
||||||
|
|
||||||
|
if deleted_count > 0:
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
return deleted_count
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""清空历史记录"""
|
"""清空历史记录"""
|
||||||
self._history = []
|
self._history = []
|
||||||
@@ -175,6 +216,57 @@ class HistoryManager:
|
|||||||
'avg_duration_ms': int(avg_duration)
|
'avg_duration_ms': int(avg_duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def find_similar_success(self, user_input: str, threshold: float = 0.6) -> Optional[TaskRecord]:
|
||||||
|
"""
|
||||||
|
查找相似的成功任务
|
||||||
|
|
||||||
|
使用简单的关键词匹配来判断相似度
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_input: 用户输入
|
||||||
|
threshold: 相似度阈值
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
最相似的成功任务记录,如果没有则返回 None
|
||||||
|
"""
|
||||||
|
# 提取关键词
|
||||||
|
def extract_keywords(text: str) -> set:
|
||||||
|
# 简单分词:按空格和标点分割
|
||||||
|
import re
|
||||||
|
words = re.findall(r'[\u4e00-\u9fa5]+|[a-zA-Z]+', text.lower())
|
||||||
|
# 过滤掉太短的词
|
||||||
|
return set(w for w in words if len(w) >= 2)
|
||||||
|
|
||||||
|
input_keywords = extract_keywords(user_input)
|
||||||
|
if not input_keywords:
|
||||||
|
return None
|
||||||
|
|
||||||
|
best_match = None
|
||||||
|
best_score = 0.0
|
||||||
|
|
||||||
|
for record in self._history:
|
||||||
|
if not record.success:
|
||||||
|
continue
|
||||||
|
|
||||||
|
record_keywords = extract_keywords(record.user_input)
|
||||||
|
if not record_keywords:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算 Jaccard 相似度
|
||||||
|
intersection = len(input_keywords & record_keywords)
|
||||||
|
union = len(input_keywords | record_keywords)
|
||||||
|
score = intersection / union if union > 0 else 0
|
||||||
|
|
||||||
|
if score > best_score and score >= threshold:
|
||||||
|
best_score = score
|
||||||
|
best_match = record
|
||||||
|
|
||||||
|
return best_match
|
||||||
|
|
||||||
|
def get_successful_records(self) -> List[TaskRecord]:
|
||||||
|
"""获取所有成功的任务记录"""
|
||||||
|
return [r for r in self._history if r.success]
|
||||||
|
|
||||||
|
|
||||||
# 全局单例
|
# 全局单例
|
||||||
_manager: Optional[HistoryManager] = None
|
_manager: Optional[HistoryManager] = None
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
# 意图类型常量
|
# 意图类型常量
|
||||||
CHAT = "chat"
|
CHAT = "chat"
|
||||||
EXECUTION = "execution"
|
EXECUTION = "execution"
|
||||||
|
GUIDANCE = "guidance" # 操作指导(无法通过本地代码完成的任务)
|
||||||
|
|
||||||
# 执行任务置信度阈值
|
# 执行任务置信度阈值
|
||||||
# 低于此阈值一律判定为 chat(宁可少执行,不可误执行)
|
# 低于此阈值一律判定为 chat(宁可少执行,不可误执行)
|
||||||
EXECUTION_CONFIDENCE_THRESHOLD = 0.6
|
EXECUTION_CONFIDENCE_THRESHOLD = 0.6
|
||||||
|
|
||||||
# 所有有效标签
|
# 所有有效标签
|
||||||
VALID_LABELS = {CHAT, EXECUTION}
|
VALID_LABELS = {CHAT, EXECUTION, GUIDANCE}
|
||||||
|
|
||||||
|
|||||||
313
llm/prompts.py
313
llm/prompts.py
@@ -40,14 +40,26 @@ ALLOWED_LIBRARIES = """
|
|||||||
# 意图识别 Prompt
|
# 意图识别 Prompt
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|
||||||
INTENT_CLASSIFICATION_SYSTEM = """你是一个意图分类器。判断用户输入是"普通对话"还是"本地执行任务"。
|
INTENT_CLASSIFICATION_SYSTEM = """你是一个意图分类器。判断用户输入属于以下哪种类型。
|
||||||
|
|
||||||
规则:
|
【意图类型】
|
||||||
- chat: 闲聊、问答、知识查询(如天气、新闻、解释概念)
|
- chat: 闲聊、问答、知识查询(如天气、新闻、解释概念、编程问题)
|
||||||
- execution: 需要操作本地文件的任务(如复制、移动、重命名、整理、转换文件)
|
- execution: 需要操作本地文件的任务(如复制、移动、重命名、整理、转换文件、图片处理)
|
||||||
|
- guidance: 需要操作指导但无法通过本地Python代码完成的任务
|
||||||
|
|
||||||
|
【guidance 类型示例】
|
||||||
|
- 软件/系统设置类:如何修改浏览器主题、如何设置Windows壁纸、如何更改系统语言
|
||||||
|
- 软件操作类:如何使用Photoshop抠图、如何在Excel中创建透视表
|
||||||
|
- 网络操作类:如何注册某网站账号、如何下载某软件
|
||||||
|
- 硬件操作类:如何连接蓝牙设备、如何设置打印机
|
||||||
|
|
||||||
|
【判断要点】
|
||||||
|
1. 如果任务可以通过Python脚本处理本地文件完成 → execution
|
||||||
|
2. 如果任务需要操作GUI软件、浏览器、系统设置等 → guidance
|
||||||
|
3. 如果是纯粹的知识问答或闲聊 → chat
|
||||||
|
|
||||||
只输出JSON,格式:
|
只输出JSON,格式:
|
||||||
{"label": "chat或execution", "confidence": 0.0到1.0, "reason": "简短中文理由"}"""
|
{"label": "chat或execution或guidance", "confidence": 0.0到1.0, "reason": "简短中文理由"}"""
|
||||||
|
|
||||||
INTENT_CLASSIFICATION_USER = """判断以下输入的意图:
|
INTENT_CLASSIFICATION_USER = """判断以下输入的意图:
|
||||||
{user_input}"""
|
{user_input}"""
|
||||||
@@ -188,3 +200,294 @@ SAFETY_REVIEW_USER = """用户需求:{user_input}
|
|||||||
```
|
```
|
||||||
|
|
||||||
请进行安全审查。"""
|
请进行安全审查。"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 任务摘要生成 Prompt
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
TASK_SUMMARY_SYSTEM = """你是一个任务摘要生成器。根据用户的输入,生成简短的任务描述。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 用中文输出
|
||||||
|
2. 不超过 15 个字
|
||||||
|
3. 只描述任务本身,不要包含"用户想要"等前缀
|
||||||
|
4. 使用动词开头,如"复制"、"转换"、"整理"等
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- 用户输入:"帮我把input里的图片都转成jpg格式" → "图片批量转换为JPG"
|
||||||
|
- 用户输入:"把所有文件按日期分类" → "文件按日期分类整理"
|
||||||
|
- 用户输入:"给图片加水印" → "图片批量添加水印"
|
||||||
|
|
||||||
|
只输出摘要文本,不要其他内容。"""
|
||||||
|
|
||||||
|
TASK_SUMMARY_USER = """用户输入:{user_input}
|
||||||
|
|
||||||
|
请生成任务摘要。"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 代码修复 Prompt(用于失败重试)
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
CODE_FIX_SYSTEM = f"""你是一个 Python 代码修复专家。根据错误信息修复代码。
|
||||||
|
|
||||||
|
【任务】
|
||||||
|
分析代码执行失败的原因,修复代码中的 bug。
|
||||||
|
|
||||||
|
【硬性约束 - 必须遵守】
|
||||||
|
1. 只能操作 workspace/input(读取)和 workspace/output(写入)目录
|
||||||
|
2. 禁止使用: requests, socket, urllib, subprocess, os.system, eval, exec
|
||||||
|
3. 禁止删除文件: os.remove, shutil.rmtree, os.unlink
|
||||||
|
4. 禁止访问 workspace 外的任何路径
|
||||||
|
5. 必须处理异常,打印清晰的错误信息
|
||||||
|
|
||||||
|
{ALLOWED_LIBRARIES}
|
||||||
|
|
||||||
|
【修复要点】
|
||||||
|
1. 仔细分析错误信息,找出根本原因
|
||||||
|
2. 检查 API 使用是否正确(如 PIL 的方法名、参数等)
|
||||||
|
3. 添加必要的错误处理
|
||||||
|
4. 确保代码逻辑正确
|
||||||
|
|
||||||
|
只输出修复后的完整 Python 代码块,不要其他解释。"""
|
||||||
|
|
||||||
|
CODE_FIX_USER = """原始需求:{user_input}
|
||||||
|
|
||||||
|
执行计划:
|
||||||
|
{execution_plan}
|
||||||
|
|
||||||
|
原始代码:
|
||||||
|
```python
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
执行输出:
|
||||||
|
{stdout}
|
||||||
|
|
||||||
|
错误信息:
|
||||||
|
{stderr}
|
||||||
|
|
||||||
|
请分析错误原因并修复代码。"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 需求澄清 Prompt(用于模糊需求的多轮对话)
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
REQUIREMENT_CLARIFY_SYSTEM = """你是一个需求分析助手。你的任务是通过提问来澄清用户模糊的需求。
|
||||||
|
|
||||||
|
【背景】
|
||||||
|
用户提出了一个文件处理任务,但描述不够完整。你需要识别缺失的关键信息,并生成一个问题来询问用户。
|
||||||
|
|
||||||
|
【可处理的任务类型】
|
||||||
|
- 图片处理:添加水印、格式转换、缩放、裁剪、压缩等
|
||||||
|
- 文件整理:按类型/日期/大小分类、重命名、复制、移动等
|
||||||
|
- 文档处理:Excel合并、PDF提取、Word转换等
|
||||||
|
- 压缩打包:批量压缩、解压等
|
||||||
|
|
||||||
|
【输出格式】
|
||||||
|
你必须输出一个 JSON 对象,格式如下:
|
||||||
|
{
|
||||||
|
"need_clarify": true或false,
|
||||||
|
"question": "要问用户的问题(如果need_clarify为false则为空)",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": "选项ID",
|
||||||
|
"type": "radio|checkbox|input",
|
||||||
|
"label": "选项标签/问题描述",
|
||||||
|
"choices": ["选项1", "选项2"], // 仅radio/checkbox需要
|
||||||
|
"default": "默认值", // 可选
|
||||||
|
"placeholder": "输入提示" // 仅input需要
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collected_info": {
|
||||||
|
"已收集的信息键": "值"
|
||||||
|
},
|
||||||
|
"missing_info": ["缺失信息1", "缺失信息2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
【选项类型说明】
|
||||||
|
- radio: 单选,用于互斥选项(如:文字水印/图片水印)
|
||||||
|
- checkbox: 多选,用于可多选的选项(如:水印位置可选多个角落)
|
||||||
|
- input: 输入框,用于自由输入(如:水印文字内容、透明度数值)
|
||||||
|
|
||||||
|
【提问策略】
|
||||||
|
1. 每次只问一个核心问题,不要一次问太多
|
||||||
|
2. 优先问最关键的信息(如水印类型比透明度更重要)
|
||||||
|
3. 提供合理的默认值,减少用户输入负担
|
||||||
|
4. 选项要覆盖常见场景,但不要过于复杂
|
||||||
|
|
||||||
|
【常见需要澄清的信息】
|
||||||
|
图片水印任务:
|
||||||
|
- 水印类型(文字/图片)
|
||||||
|
- 水印内容(文字内容或图片路径)
|
||||||
|
- 水印位置(左上/右上/左下/右下/居中/平铺)
|
||||||
|
- 透明度(0-100%)
|
||||||
|
- 字体大小(仅文字水印)
|
||||||
|
- 水印颜色(仅文字水印)
|
||||||
|
|
||||||
|
图片转换任务:
|
||||||
|
- 目标格式(JPG/PNG/WEBP等)
|
||||||
|
- 质量/压缩率
|
||||||
|
- 是否保持原尺寸
|
||||||
|
|
||||||
|
文件整理任务:
|
||||||
|
- 分类依据(扩展名/日期/大小)
|
||||||
|
- 命名规则
|
||||||
|
- 是否包含子目录
|
||||||
|
|
||||||
|
【示例】
|
||||||
|
用户输入:"给图片加水印"
|
||||||
|
输出:
|
||||||
|
{
|
||||||
|
"need_clarify": true,
|
||||||
|
"question": "请选择水印类型",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"id": "watermark_type",
|
||||||
|
"type": "radio",
|
||||||
|
"label": "水印类型",
|
||||||
|
"choices": ["文字水印", "图片水印"],
|
||||||
|
"default": "文字水印"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collected_info": {},
|
||||||
|
"missing_info": ["水印类型", "水印内容", "水印位置", "透明度"]
|
||||||
|
}
|
||||||
|
|
||||||
|
如果信息已经足够完整,设置 need_clarify 为 false。"""
|
||||||
|
|
||||||
|
REQUIREMENT_CLARIFY_USER = """用户原始需求:{user_input}
|
||||||
|
|
||||||
|
已收集的信息:
|
||||||
|
{collected_info}
|
||||||
|
|
||||||
|
用户最新回答:
|
||||||
|
{user_answer}
|
||||||
|
|
||||||
|
请分析是否还需要继续澄清,如果需要,生成下一个问题。"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 需求结构化 Prompt(将澄清后的需求整理为完整描述)
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
REQUIREMENT_STRUCTURE_SYSTEM = """你是一个需求整理专家。你的任务是将用户的模糊需求和澄清后的信息整理成完整、清晰、无歧义的需求描述。
|
||||||
|
|
||||||
|
【输出要求】
|
||||||
|
生成一段结构化的自然语言描述,必须包含以下要素:
|
||||||
|
|
||||||
|
1. **任务目标**:一句话描述要做什么
|
||||||
|
2. **输入数据**:
|
||||||
|
- 数据来源:workspace/input 目录
|
||||||
|
- 文件类型:具体的文件格式
|
||||||
|
- 数量:单个/批量
|
||||||
|
3. **处理规则**:
|
||||||
|
- 具体的处理逻辑
|
||||||
|
- 所有参数的明确值
|
||||||
|
4. **输出结果**:
|
||||||
|
- 输出位置:workspace/output 目录
|
||||||
|
- 输出格式:文件命名规则、格式等
|
||||||
|
5. **约束条件**:
|
||||||
|
- 不修改原文件
|
||||||
|
- 异常处理方式
|
||||||
|
|
||||||
|
【格式示例】
|
||||||
|
```
|
||||||
|
## 任务目标
|
||||||
|
批量为图片添加文字水印
|
||||||
|
|
||||||
|
## 输入数据
|
||||||
|
- 来源:workspace/input 目录下的所有图片文件
|
||||||
|
- 支持格式:JPG、PNG、WEBP
|
||||||
|
- 处理方式:批量处理所有图片
|
||||||
|
|
||||||
|
## 处理规则
|
||||||
|
- 水印类型:文字水印
|
||||||
|
- 水印内容:"© 2024 MyCompany"
|
||||||
|
- 水印位置:右下角
|
||||||
|
- 透明度:50%
|
||||||
|
- 字体大小:24px
|
||||||
|
- 字体颜色:白色
|
||||||
|
|
||||||
|
## 输出结果
|
||||||
|
- 输出位置:workspace/output 目录
|
||||||
|
- 文件命名:保持原文件名
|
||||||
|
- 输出格式:与原图相同
|
||||||
|
|
||||||
|
## 约束条件
|
||||||
|
- 保持原图不变,输出到新目录
|
||||||
|
- 跳过无法处理的文件并记录错误
|
||||||
|
- 处理完成后输出统计信息
|
||||||
|
```
|
||||||
|
|
||||||
|
只输出整理后的需求描述,不要其他内容。"""
|
||||||
|
|
||||||
|
REQUIREMENT_STRUCTURE_USER = """用户原始需求:{user_input}
|
||||||
|
|
||||||
|
澄清后收集的完整信息:
|
||||||
|
{collected_info}
|
||||||
|
|
||||||
|
请将以上信息整理为完整的需求描述。"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 需求完整性检查 Prompt
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
REQUIREMENT_CHECK_SYSTEM = """你是一个需求完整性检查器。判断用户的需求描述是否足够完整,可以直接生成代码。
|
||||||
|
|
||||||
|
【判断标准】
|
||||||
|
完整的需求应该包含:
|
||||||
|
1. 明确的操作对象(什么类型的文件)
|
||||||
|
2. 明确的操作动作(做什么处理)
|
||||||
|
3. 关键参数已指定或有合理默认值
|
||||||
|
|
||||||
|
【输出格式】
|
||||||
|
{
|
||||||
|
"is_complete": true或false,
|
||||||
|
"confidence": 0.0到1.0,
|
||||||
|
"reason": "判断理由",
|
||||||
|
"suggested_defaults": {
|
||||||
|
"参数名": "建议的默认值"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
【示例】
|
||||||
|
输入:"把图片转成jpg"
|
||||||
|
输出:
|
||||||
|
{
|
||||||
|
"is_complete": true,
|
||||||
|
"confidence": 0.8,
|
||||||
|
"reason": "目标格式明确,质量可使用默认值85%",
|
||||||
|
"suggested_defaults": {
|
||||||
|
"quality": 85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
输入:"给图片加水印"
|
||||||
|
输出:
|
||||||
|
{
|
||||||
|
"is_complete": false,
|
||||||
|
"confidence": 0.3,
|
||||||
|
"reason": "缺少水印类型、内容、位置等关键信息",
|
||||||
|
"suggested_defaults": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
输入:"给图片右下角加上'版权所有'的文字水印"
|
||||||
|
输出:
|
||||||
|
{
|
||||||
|
"is_complete": true,
|
||||||
|
"confidence": 0.9,
|
||||||
|
"reason": "水印类型、内容、位置都已明确,其他参数可用默认值",
|
||||||
|
"suggested_defaults": {
|
||||||
|
"opacity": 50,
|
||||||
|
"font_size": 24,
|
||||||
|
"color": "white"
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
|
||||||
|
REQUIREMENT_CHECK_USER = """用户需求:{user_input}
|
||||||
|
|
||||||
|
请判断这个需求是否足够完整。"""
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
# 添加项目根目录到路径
|
# 添加项目根目录到路径
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
from intent.labels import CHAT, EXECUTION, VALID_LABELS, EXECUTION_CONFIDENCE_THRESHOLD
|
from intent.labels import CHAT, EXECUTION, GUIDANCE, VALID_LABELS, EXECUTION_CONFIDENCE_THRESHOLD
|
||||||
|
|
||||||
|
|
||||||
class TestIntentLabels(unittest.TestCase):
|
class TestIntentLabels(unittest.TestCase):
|
||||||
@@ -19,12 +19,14 @@ class TestIntentLabels(unittest.TestCase):
|
|||||||
"""测试标签已定义"""
|
"""测试标签已定义"""
|
||||||
self.assertEqual(CHAT, "chat")
|
self.assertEqual(CHAT, "chat")
|
||||||
self.assertEqual(EXECUTION, "execution")
|
self.assertEqual(EXECUTION, "execution")
|
||||||
|
self.assertEqual(GUIDANCE, "guidance")
|
||||||
|
|
||||||
def test_valid_labels(self):
|
def test_valid_labels(self):
|
||||||
"""测试有效标签集合"""
|
"""测试有效标签集合"""
|
||||||
self.assertIn(CHAT, VALID_LABELS)
|
self.assertIn(CHAT, VALID_LABELS)
|
||||||
self.assertIn(EXECUTION, VALID_LABELS)
|
self.assertIn(EXECUTION, VALID_LABELS)
|
||||||
self.assertEqual(len(VALID_LABELS), 2)
|
self.assertIn(GUIDANCE, VALID_LABELS)
|
||||||
|
self.assertEqual(len(VALID_LABELS), 3)
|
||||||
|
|
||||||
def test_confidence_threshold(self):
|
def test_confidence_threshold(self):
|
||||||
"""测试置信度阈值"""
|
"""测试置信度阈值"""
|
||||||
|
|||||||
386
ui/chat_view.py
386
ui/chat_view.py
@@ -1,11 +1,243 @@
|
|||||||
"""
|
"""
|
||||||
聊天视图组件
|
聊天视图组件
|
||||||
处理普通对话的 UI 展示 - 支持流式消息和加载动画
|
处理普通对话的 UI 展示 - 支持流式消息、加载动画和 Markdown 渲染
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import scrolledtext
|
from tkinter import scrolledtext
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional, List, Tuple
|
||||||
|
import re
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownRenderer:
|
||||||
|
"""Markdown 渲染器 - 将 Markdown 文本渲染到 Text 组件"""
|
||||||
|
|
||||||
|
# URL 正则表达式
|
||||||
|
URL_PATTERN = re.compile(
|
||||||
|
r'https?://[^\s<>\[\]()()\u4e00-\u9fff]+'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Markdown 链接模式 [text](url)
|
||||||
|
MD_LINK_PATTERN = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
|
||||||
|
|
||||||
|
def __init__(self, text_widget: tk.Text):
|
||||||
|
self.text_widget = text_widget
|
||||||
|
self._link_count = 0
|
||||||
|
self._configure_tags()
|
||||||
|
|
||||||
|
def _configure_tags(self):
|
||||||
|
"""配置 Markdown 样式标签"""
|
||||||
|
# 标题样式
|
||||||
|
self.text_widget.tag_configure('md_h1', font=('Microsoft YaHei UI', 16, 'bold'), foreground='#4fc3f7')
|
||||||
|
self.text_widget.tag_configure('md_h2', font=('Microsoft YaHei UI', 14, 'bold'), foreground='#4fc3f7')
|
||||||
|
self.text_widget.tag_configure('md_h3', font=('Microsoft YaHei UI', 12, 'bold'), foreground='#4fc3f7')
|
||||||
|
|
||||||
|
# 粗体和斜体
|
||||||
|
self.text_widget.tag_configure('md_bold', font=('Microsoft YaHei UI', 11, 'bold'))
|
||||||
|
self.text_widget.tag_configure('md_italic', font=('Microsoft YaHei UI', 11, 'italic'))
|
||||||
|
|
||||||
|
# 代码样式
|
||||||
|
self.text_widget.tag_configure('md_code', font=('Consolas', 10), background='#3c3c3c', foreground='#ce9178')
|
||||||
|
self.text_widget.tag_configure('md_code_block', font=('Consolas', 10), background='#1e1e1e', foreground='#d4d4d4')
|
||||||
|
|
||||||
|
# 列表样式
|
||||||
|
self.text_widget.tag_configure('md_list', foreground='#d4d4d4', lmargin1=20, lmargin2=35)
|
||||||
|
self.text_widget.tag_configure('md_list_bullet', foreground='#ffd54f')
|
||||||
|
|
||||||
|
# 链接样式
|
||||||
|
self.text_widget.tag_configure('md_link', foreground='#64b5f6', underline=True)
|
||||||
|
|
||||||
|
# 引用样式
|
||||||
|
self.text_widget.tag_configure('md_quote', foreground='#9e9e9e', lmargin1=20, lmargin2=20, font=('Microsoft YaHei UI', 11, 'italic'))
|
||||||
|
|
||||||
|
def render(self, text: str, base_tag: str = 'assistant') -> None:
|
||||||
|
"""
|
||||||
|
渲染 Markdown 文本
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Markdown 文本
|
||||||
|
base_tag: 基础样式标签
|
||||||
|
"""
|
||||||
|
lines = text.split('\n')
|
||||||
|
in_code_block = False
|
||||||
|
code_block_content = []
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# 代码块处理
|
||||||
|
if line.strip().startswith('```'):
|
||||||
|
if in_code_block:
|
||||||
|
# 结束代码块
|
||||||
|
self._insert_code_block('\n'.join(code_block_content))
|
||||||
|
code_block_content = []
|
||||||
|
in_code_block = False
|
||||||
|
else:
|
||||||
|
# 开始代码块
|
||||||
|
in_code_block = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_code_block:
|
||||||
|
code_block_content.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 普通行处理
|
||||||
|
self._render_line(line, base_tag)
|
||||||
|
|
||||||
|
# 添加换行(除了最后一行)
|
||||||
|
if i < len(lines) - 1:
|
||||||
|
self.text_widget.insert(tk.END, '\n')
|
||||||
|
|
||||||
|
def _render_line(self, line: str, base_tag: str) -> None:
|
||||||
|
"""渲染单行"""
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# 空行
|
||||||
|
if not stripped:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
if stripped.startswith('### '):
|
||||||
|
self.text_widget.insert(tk.END, stripped[4:], 'md_h3')
|
||||||
|
return
|
||||||
|
elif stripped.startswith('## '):
|
||||||
|
self.text_widget.insert(tk.END, stripped[3:], 'md_h2')
|
||||||
|
return
|
||||||
|
elif stripped.startswith('# '):
|
||||||
|
self.text_widget.insert(tk.END, stripped[2:], 'md_h1')
|
||||||
|
return
|
||||||
|
|
||||||
|
# 引用
|
||||||
|
if stripped.startswith('> '):
|
||||||
|
self.text_widget.insert(tk.END, stripped[2:], 'md_quote')
|
||||||
|
return
|
||||||
|
|
||||||
|
# 无序列表
|
||||||
|
if stripped.startswith('- ') or stripped.startswith('* '):
|
||||||
|
self.text_widget.insert(tk.END, ' • ', 'md_list_bullet')
|
||||||
|
self._render_inline(stripped[2:], base_tag, 'md_list')
|
||||||
|
return
|
||||||
|
|
||||||
|
# 有序列表
|
||||||
|
list_match = re.match(r'^(\d+)\.\s+(.+)$', stripped)
|
||||||
|
if list_match:
|
||||||
|
num = list_match.group(1)
|
||||||
|
content = list_match.group(2)
|
||||||
|
self.text_widget.insert(tk.END, f' {num}. ', 'md_list_bullet')
|
||||||
|
self._render_inline(content, base_tag, 'md_list')
|
||||||
|
return
|
||||||
|
|
||||||
|
# 普通段落
|
||||||
|
self._render_inline(line, base_tag)
|
||||||
|
|
||||||
|
def _render_inline(self, text: str, base_tag: str, extra_tag: str = None) -> None:
|
||||||
|
"""渲染行内元素(粗体、斜体、代码、链接)"""
|
||||||
|
tags = (base_tag, extra_tag) if extra_tag else (base_tag,)
|
||||||
|
|
||||||
|
# 先处理 Markdown 链接 [text](url)
|
||||||
|
last_end = 0
|
||||||
|
for match in self.MD_LINK_PATTERN.finditer(text):
|
||||||
|
# 插入链接前的文本
|
||||||
|
if match.start() > last_end:
|
||||||
|
self._render_inline_formatting(text[last_end:match.start()], tags)
|
||||||
|
|
||||||
|
# 插入链接
|
||||||
|
link_text = match.group(1)
|
||||||
|
link_url = match.group(2)
|
||||||
|
self._insert_link(link_text, link_url)
|
||||||
|
|
||||||
|
last_end = match.end()
|
||||||
|
|
||||||
|
# 处理剩余文本
|
||||||
|
if last_end < len(text):
|
||||||
|
remaining = text[last_end:]
|
||||||
|
self._render_inline_formatting(remaining, tags)
|
||||||
|
|
||||||
|
def _render_inline_formatting(self, text: str, tags: tuple) -> None:
|
||||||
|
"""处理行内格式(粗体、斜体、代码、纯URL)"""
|
||||||
|
# 处理粗体 **text**
|
||||||
|
parts = re.split(r'(\*\*[^*]+\*\*)', text)
|
||||||
|
for part in parts:
|
||||||
|
if part.startswith('**') and part.endswith('**'):
|
||||||
|
self.text_widget.insert(tk.END, part[2:-2], tags + ('md_bold',))
|
||||||
|
else:
|
||||||
|
# 处理斜体 *text*
|
||||||
|
sub_parts = re.split(r'(\*[^*]+\*)', part)
|
||||||
|
for sub_part in sub_parts:
|
||||||
|
if sub_part.startswith('*') and sub_part.endswith('*') and len(sub_part) > 2:
|
||||||
|
self.text_widget.insert(tk.END, sub_part[1:-1], tags + ('md_italic',))
|
||||||
|
else:
|
||||||
|
# 处理行内代码 `code`
|
||||||
|
code_parts = re.split(r'(`[^`]+`)', sub_part)
|
||||||
|
for code_part in code_parts:
|
||||||
|
if code_part.startswith('`') and code_part.endswith('`'):
|
||||||
|
self.text_widget.insert(tk.END, code_part[1:-1], ('md_code',))
|
||||||
|
else:
|
||||||
|
# 处理纯 URL
|
||||||
|
self._render_urls(code_part, tags)
|
||||||
|
|
||||||
|
def _render_urls(self, text: str, tags: tuple) -> None:
|
||||||
|
"""渲染纯 URL 链接"""
|
||||||
|
last_end = 0
|
||||||
|
for match in self.URL_PATTERN.finditer(text):
|
||||||
|
# 插入 URL 前的文本
|
||||||
|
if match.start() > last_end:
|
||||||
|
self.text_widget.insert(tk.END, text[last_end:match.start()], tags)
|
||||||
|
|
||||||
|
# 插入 URL 链接
|
||||||
|
url = match.group(0)
|
||||||
|
# 清理 URL 末尾的标点
|
||||||
|
while url and url[-1] in '.,;:!?。,;:!?':
|
||||||
|
url = url[:-1]
|
||||||
|
self._insert_link(url, url)
|
||||||
|
|
||||||
|
# 如果清理了标点,插入标点
|
||||||
|
original_url = match.group(0)
|
||||||
|
if len(original_url) > len(url):
|
||||||
|
self.text_widget.insert(tk.END, original_url[len(url):], tags)
|
||||||
|
|
||||||
|
last_end = match.end()
|
||||||
|
|
||||||
|
# 插入剩余文本
|
||||||
|
if last_end < len(text):
|
||||||
|
self.text_widget.insert(tk.END, text[last_end:], tags)
|
||||||
|
|
||||||
|
def _insert_link(self, text: str, url: str) -> None:
|
||||||
|
"""插入可点击的链接"""
|
||||||
|
tag_name = f'link_{self._link_count}'
|
||||||
|
self._link_count += 1
|
||||||
|
|
||||||
|
self.text_widget.tag_configure(tag_name, foreground='#64b5f6', underline=True)
|
||||||
|
|
||||||
|
# 绑定点击事件 - 使用 ButtonRelease 而不是 Button-1,更可靠
|
||||||
|
def on_click(event, u=url):
|
||||||
|
self._open_url(u)
|
||||||
|
return "break" # 阻止事件继续传播
|
||||||
|
|
||||||
|
self.text_widget.tag_bind(tag_name, '<ButtonRelease-1>', on_click)
|
||||||
|
self.text_widget.tag_bind(tag_name, '<Enter>', lambda e: self._set_cursor('hand2'))
|
||||||
|
self.text_widget.tag_bind(tag_name, '<Leave>', lambda e: self._set_cursor(''))
|
||||||
|
|
||||||
|
self.text_widget.insert(tk.END, text, (tag_name, 'md_link'))
|
||||||
|
|
||||||
|
def _set_cursor(self, cursor: str) -> None:
|
||||||
|
"""设置鼠标光标"""
|
||||||
|
try:
|
||||||
|
self.text_widget.config(cursor=cursor)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _insert_code_block(self, code: str) -> None:
|
||||||
|
"""插入代码块"""
|
||||||
|
self.text_widget.insert(tk.END, '\n')
|
||||||
|
self.text_widget.insert(tk.END, code, 'md_code_block')
|
||||||
|
self.text_widget.insert(tk.END, '\n')
|
||||||
|
|
||||||
|
def _open_url(self, url: str) -> None:
|
||||||
|
"""打开 URL"""
|
||||||
|
try:
|
||||||
|
webbrowser.open(url)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to open URL: {url}, error: {e}")
|
||||||
|
|
||||||
|
|
||||||
class LoadingIndicator:
|
class LoadingIndicator:
|
||||||
@@ -65,7 +297,7 @@ class ChatView:
|
|||||||
聊天视图
|
聊天视图
|
||||||
|
|
||||||
包含:
|
包含:
|
||||||
- 消息显示区域
|
- 消息显示区域(支持 Markdown 渲染)
|
||||||
- 输入框
|
- 输入框
|
||||||
- 发送按钮
|
- 发送按钮
|
||||||
- 流式消息支持
|
- 流式消息支持
|
||||||
@@ -75,7 +307,8 @@ class ChatView:
|
|||||||
self,
|
self,
|
||||||
parent: tk.Widget,
|
parent: tk.Widget,
|
||||||
on_send: Callable[[str], None],
|
on_send: Callable[[str], None],
|
||||||
on_show_history: Optional[Callable[[], None]] = None
|
on_show_history: Optional[Callable[[], None]] = None,
|
||||||
|
on_show_settings: Optional[Callable[[], None]] = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
初始化聊天视图
|
初始化聊天视图
|
||||||
@@ -84,18 +317,24 @@ class ChatView:
|
|||||||
parent: 父容器
|
parent: 父容器
|
||||||
on_send: 发送消息回调函数
|
on_send: 发送消息回调函数
|
||||||
on_show_history: 显示历史记录回调函数
|
on_show_history: 显示历史记录回调函数
|
||||||
|
on_show_settings: 显示设置页面回调函数
|
||||||
"""
|
"""
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.on_send = on_send
|
self.on_send = on_send
|
||||||
self.on_show_history = on_show_history
|
self.on_show_history = on_show_history
|
||||||
|
self.on_show_settings = on_show_settings
|
||||||
|
|
||||||
# 流式消息状态
|
# 流式消息状态
|
||||||
self._stream_active = False
|
self._stream_active = False
|
||||||
self._stream_tag = None
|
self._stream_tag = None
|
||||||
|
self._stream_buffer = [] # 用于缓存流式内容,最后渲染 Markdown
|
||||||
|
|
||||||
# 加载指示器
|
# 加载指示器
|
||||||
self.loading: Optional[LoadingIndicator] = None
|
self.loading: Optional[LoadingIndicator] = None
|
||||||
|
|
||||||
|
# Markdown 渲染器
|
||||||
|
self.md_renderer: Optional[MarkdownRenderer] = None
|
||||||
|
|
||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
|
|
||||||
def _create_widgets(self):
|
def _create_widgets(self):
|
||||||
@@ -118,10 +357,49 @@ class ChatView:
|
|||||||
)
|
)
|
||||||
title_label.pack(side=tk.LEFT, expand=True)
|
title_label.pack(side=tk.LEFT, expand=True)
|
||||||
|
|
||||||
|
# 按钮容器(右侧)
|
||||||
|
btn_container = tk.Frame(title_frame, bg='#1e1e1e')
|
||||||
|
btn_container.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# 清空对话按钮
|
||||||
|
self.clear_btn = tk.Button(
|
||||||
|
btn_container,
|
||||||
|
text="🗑️ 清空",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#424242',
|
||||||
|
fg='#ef9a9a',
|
||||||
|
activebackground='#616161',
|
||||||
|
activeforeground='#ef9a9a',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=10,
|
||||||
|
pady=3,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._on_clear_chat
|
||||||
|
)
|
||||||
|
self.clear_btn.pack(side=tk.RIGHT, padx=(5, 0))
|
||||||
|
|
||||||
|
# 设置按钮
|
||||||
|
if self.on_show_settings:
|
||||||
|
self.settings_btn = tk.Button(
|
||||||
|
btn_container,
|
||||||
|
text="⚙️ 设置",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#424242',
|
||||||
|
fg='#90caf9',
|
||||||
|
activebackground='#616161',
|
||||||
|
activeforeground='#90caf9',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=10,
|
||||||
|
pady=3,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self.on_show_settings
|
||||||
|
)
|
||||||
|
self.settings_btn.pack(side=tk.RIGHT, padx=(5, 0))
|
||||||
|
|
||||||
# 历史记录按钮
|
# 历史记录按钮
|
||||||
if self.on_show_history:
|
if self.on_show_history:
|
||||||
self.history_btn = tk.Button(
|
self.history_btn = tk.Button(
|
||||||
title_frame,
|
btn_container,
|
||||||
text="📜 历史",
|
text="📜 历史",
|
||||||
font=('Microsoft YaHei UI', 10),
|
font=('Microsoft YaHei UI', 10),
|
||||||
bg='#424242',
|
bg='#424242',
|
||||||
@@ -147,10 +425,14 @@ class ChatView:
|
|||||||
relief=tk.FLAT,
|
relief=tk.FLAT,
|
||||||
padx=10,
|
padx=10,
|
||||||
pady=10,
|
pady=10,
|
||||||
state=tk.DISABLED
|
cursor='arrow'
|
||||||
)
|
)
|
||||||
self.message_area.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
self.message_area.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
|
|
||||||
|
# 禁止编辑但允许选择和点击链接
|
||||||
|
self.message_area.bind('<Key>', lambda e: 'break') # 禁止键盘输入
|
||||||
|
# 允许鼠标操作(选择文本、点击链接)
|
||||||
|
|
||||||
# 配置消息标签样式
|
# 配置消息标签样式
|
||||||
self.message_area.tag_configure('user', foreground='#4fc3f7', font=('Microsoft YaHei UI', 11, 'bold'))
|
self.message_area.tag_configure('user', foreground='#4fc3f7', font=('Microsoft YaHei UI', 11, 'bold'))
|
||||||
self.message_area.tag_configure('assistant', foreground='#81c784', font=('Microsoft YaHei UI', 11))
|
self.message_area.tag_configure('assistant', foreground='#81c784', font=('Microsoft YaHei UI', 11))
|
||||||
@@ -158,6 +440,9 @@ class ChatView:
|
|||||||
self.message_area.tag_configure('error', foreground='#ef5350', font=('Microsoft YaHei UI', 10))
|
self.message_area.tag_configure('error', foreground='#ef5350', font=('Microsoft YaHei UI', 10))
|
||||||
self.message_area.tag_configure('streaming', foreground='#81c784', font=('Microsoft YaHei UI', 11))
|
self.message_area.tag_configure('streaming', foreground='#81c784', font=('Microsoft YaHei UI', 11))
|
||||||
|
|
||||||
|
# 初始化 Markdown 渲染器
|
||||||
|
self.md_renderer = MarkdownRenderer(self.message_area)
|
||||||
|
|
||||||
# 输入区域框架
|
# 输入区域框架
|
||||||
input_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
input_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
input_frame.pack(fill=tk.X)
|
input_frame.pack(fill=tk.X)
|
||||||
@@ -213,28 +498,54 @@ class ChatView:
|
|||||||
self.input_entry.delete(0, tk.END)
|
self.input_entry.delete(0, tk.END)
|
||||||
self.on_send(text)
|
self.on_send(text)
|
||||||
|
|
||||||
def add_message(self, message: str, tag: str = 'assistant'):
|
def _on_clear_chat(self):
|
||||||
|
"""清空对话"""
|
||||||
|
from tkinter import messagebox
|
||||||
|
if messagebox.askyesno("确认", "确定要清空当前对话吗?\n(这将同时清空对话上下文)"):
|
||||||
|
self.clear_messages()
|
||||||
|
# 通知 agent 清空上下文(通过回调)
|
||||||
|
if hasattr(self, 'on_clear_context') and self.on_clear_context:
|
||||||
|
self.on_clear_context()
|
||||||
|
# 重新显示欢迎消息
|
||||||
|
welcome_msg = (
|
||||||
|
"欢迎使用 LocalAgent!\n"
|
||||||
|
"- 输入问题进行对话\n"
|
||||||
|
"- 输入文件处理需求(如\"复制文件\"、\"整理图片\")将触发执行模式"
|
||||||
|
)
|
||||||
|
self.add_message(welcome_msg, 'system')
|
||||||
|
|
||||||
|
def set_clear_context_callback(self, callback: Callable[[], None]):
|
||||||
|
"""设置清空上下文的回调"""
|
||||||
|
self.on_clear_context = callback
|
||||||
|
|
||||||
|
def add_message(self, message: str, tag: str = 'assistant', use_markdown: bool = True):
|
||||||
"""
|
"""
|
||||||
添加消息到显示区域
|
添加消息到显示区域
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: 消息内容
|
message: 消息内容
|
||||||
tag: 消息类型 (user/assistant/system/error)
|
tag: 消息类型 (user/assistant/system/error)
|
||||||
|
use_markdown: 是否使用 Markdown 渲染(assistant 消息默认启用)
|
||||||
"""
|
"""
|
||||||
self.message_area.config(state=tk.NORMAL)
|
|
||||||
|
|
||||||
# 添加前缀
|
# 添加前缀
|
||||||
prefix_map = {
|
prefix_map = {
|
||||||
'user': '[你] ',
|
'user': '\n[你] ',
|
||||||
'assistant': '[助手] ',
|
'assistant': '\n[助手] ',
|
||||||
'system': '[系统] ',
|
'system': '\n[系统] ',
|
||||||
'error': '[错误] '
|
'error': '\n[错误] '
|
||||||
}
|
}
|
||||||
prefix = prefix_map.get(tag, '')
|
prefix = prefix_map.get(tag, '\n')
|
||||||
|
|
||||||
self.message_area.insert(tk.END, "\n" + prefix + message + "\n", tag)
|
self.message_area.insert(tk.END, prefix, tag)
|
||||||
|
|
||||||
|
# 根据消息类型决定是否使用 Markdown 渲染
|
||||||
|
if use_markdown and tag == 'assistant' and self.md_renderer:
|
||||||
|
self.md_renderer.render(message, tag)
|
||||||
|
else:
|
||||||
|
self.message_area.insert(tk.END, message, tag)
|
||||||
|
|
||||||
|
self.message_area.insert(tk.END, '\n')
|
||||||
self.message_area.see(tk.END)
|
self.message_area.see(tk.END)
|
||||||
self.message_area.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def start_stream_message(self, tag: str = 'assistant'):
|
def start_stream_message(self, tag: str = 'assistant'):
|
||||||
"""
|
"""
|
||||||
@@ -245,21 +556,22 @@ class ChatView:
|
|||||||
"""
|
"""
|
||||||
self._stream_active = True
|
self._stream_active = True
|
||||||
self._stream_tag = tag
|
self._stream_tag = tag
|
||||||
|
self._stream_buffer = []
|
||||||
self.message_area.config(state=tk.NORMAL)
|
|
||||||
|
|
||||||
# 添加前缀
|
# 添加前缀
|
||||||
prefix_map = {
|
prefix_map = {
|
||||||
'user': '[你] ',
|
'user': '\n[你] ',
|
||||||
'assistant': '[助手] ',
|
'assistant': '\n[助手] ',
|
||||||
'system': '[系统] ',
|
'system': '\n[系统] ',
|
||||||
'error': '[错误] '
|
'error': '\n[错误] '
|
||||||
}
|
}
|
||||||
prefix = prefix_map.get(tag, '')
|
prefix = prefix_map.get(tag, '\n')
|
||||||
|
|
||||||
self.message_area.insert(tk.END, "\n" + prefix, tag)
|
self.message_area.insert(tk.END, prefix, tag)
|
||||||
|
# 使用 mark 来标记内容开始位置,比索引更可靠
|
||||||
|
self.message_area.mark_set("stream_start", tk.END + "-1c")
|
||||||
|
self.message_area.mark_gravity("stream_start", tk.LEFT)
|
||||||
self.message_area.see(tk.END)
|
self.message_area.see(tk.END)
|
||||||
# 保持 NORMAL 状态以便追加内容
|
|
||||||
|
|
||||||
def append_stream_chunk(self, chunk: str):
|
def append_stream_chunk(self, chunk: str):
|
||||||
"""
|
"""
|
||||||
@@ -271,25 +583,39 @@ class ChatView:
|
|||||||
if not self._stream_active:
|
if not self._stream_active:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._stream_buffer.append(chunk)
|
||||||
self.message_area.insert(tk.END, chunk, self._stream_tag)
|
self.message_area.insert(tk.END, chunk, self._stream_tag)
|
||||||
self.message_area.see(tk.END)
|
self.message_area.see(tk.END)
|
||||||
# 强制更新 UI
|
# 强制更新 UI
|
||||||
self.message_area.update_idletasks()
|
self.message_area.update_idletasks()
|
||||||
|
|
||||||
def end_stream_message(self):
|
def end_stream_message(self):
|
||||||
"""结束流式消息"""
|
"""结束流式消息,重新渲染为 Markdown"""
|
||||||
if self._stream_active:
|
if self._stream_active:
|
||||||
self.message_area.insert(tk.END, "\n")
|
# 获取完整的流式内容
|
||||||
|
full_content = ''.join(self._stream_buffer)
|
||||||
|
|
||||||
|
# 如果是 assistant 消息且有内容,重新渲染为 Markdown
|
||||||
|
if self._stream_tag == 'assistant' and self.md_renderer and full_content.strip():
|
||||||
|
# 删除原来的纯文本内容(从 mark 位置到末尾)
|
||||||
|
try:
|
||||||
|
self.message_area.delete("stream_start", tk.END)
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
# 重新渲染为 Markdown
|
||||||
|
self.md_renderer.render(full_content, self._stream_tag)
|
||||||
|
|
||||||
|
self.message_area.insert(tk.END, '\n')
|
||||||
self.message_area.see(tk.END)
|
self.message_area.see(tk.END)
|
||||||
self.message_area.config(state=tk.DISABLED)
|
|
||||||
|
# 重置状态
|
||||||
self._stream_active = False
|
self._stream_active = False
|
||||||
self._stream_tag = None
|
self._stream_tag = None
|
||||||
|
self._stream_buffer = []
|
||||||
|
|
||||||
def clear_messages(self):
|
def clear_messages(self):
|
||||||
"""清空消息区域"""
|
"""清空消息区域"""
|
||||||
self.message_area.config(state=tk.NORMAL)
|
|
||||||
self.message_area.delete(1.0, tk.END)
|
self.message_area.delete(1.0, tk.END)
|
||||||
self.message_area.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def set_input_enabled(self, enabled: bool):
|
def set_input_enabled(self, enabled: bool):
|
||||||
"""设置输入区域是否可用"""
|
"""设置输入区域是否可用"""
|
||||||
|
|||||||
725
ui/clarify_view.py
Normal file
725
ui/clarify_view.py
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
"""
|
||||||
|
需求澄清视图组件
|
||||||
|
用于通过交互式问答澄清用户的模糊需求
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
from typing import Callable, Optional, Dict, List, Any
|
||||||
|
|
||||||
|
|
||||||
|
class ClarifyOption:
|
||||||
|
"""澄清选项数据类"""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
id: str,
|
||||||
|
type: str, # radio, checkbox, input
|
||||||
|
label: str,
|
||||||
|
choices: List[str] = None,
|
||||||
|
default: str = None,
|
||||||
|
placeholder: str = None
|
||||||
|
):
|
||||||
|
self.id = id
|
||||||
|
self.type = type
|
||||||
|
self.label = label
|
||||||
|
self.choices = choices or []
|
||||||
|
self.default = default
|
||||||
|
self.placeholder = placeholder or ""
|
||||||
|
|
||||||
|
|
||||||
|
class ClarifyView:
|
||||||
|
"""
|
||||||
|
需求澄清视图
|
||||||
|
|
||||||
|
支持:
|
||||||
|
- 单选按钮 (radio)
|
||||||
|
- 复选框 (checkbox)
|
||||||
|
- 输入框 (input)
|
||||||
|
- 多轮对话展示
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: tk.Widget,
|
||||||
|
on_submit: Callable[[Dict[str, Any]], None],
|
||||||
|
on_cancel: Callable[[], None]
|
||||||
|
):
|
||||||
|
self.parent = parent
|
||||||
|
self.on_submit = on_submit
|
||||||
|
self.on_cancel = on_cancel
|
||||||
|
|
||||||
|
# 存储控件变量
|
||||||
|
self._vars: Dict[str, Any] = {}
|
||||||
|
self._option_widgets: List[tk.Widget] = []
|
||||||
|
|
||||||
|
# 对话历史
|
||||||
|
self._history: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
self._create_widgets()
|
||||||
|
|
||||||
|
def _create_widgets(self):
|
||||||
|
"""创建 UI 组件"""
|
||||||
|
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
|
||||||
|
|
||||||
|
# 标题栏
|
||||||
|
title_frame = tk.Frame(self.frame, bg='#2d2d2d')
|
||||||
|
title_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
title_label = tk.Label(
|
||||||
|
title_frame,
|
||||||
|
text="💬 需求澄清",
|
||||||
|
font=('Microsoft YaHei UI', 14, 'bold'),
|
||||||
|
fg='#4fc3f7',
|
||||||
|
bg='#2d2d2d',
|
||||||
|
pady=10
|
||||||
|
)
|
||||||
|
title_label.pack(side=tk.LEFT, padx=15)
|
||||||
|
|
||||||
|
# 提示信息
|
||||||
|
tip_label = tk.Label(
|
||||||
|
title_frame,
|
||||||
|
text="请回答以下问题,帮助我更好地理解您的需求",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
fg='#888888',
|
||||||
|
bg='#2d2d2d'
|
||||||
|
)
|
||||||
|
tip_label.pack(side=tk.RIGHT, padx=15)
|
||||||
|
|
||||||
|
# 主内容区域(可滚动)
|
||||||
|
content_container = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
|
content_container.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
|
||||||
|
|
||||||
|
# 创建 Canvas 和滚动条
|
||||||
|
self.canvas = tk.Canvas(content_container, bg='#1e1e1e', highlightthickness=0)
|
||||||
|
scrollbar = ttk.Scrollbar(content_container, orient=tk.VERTICAL, command=self.canvas.yview)
|
||||||
|
|
||||||
|
self.content_frame = tk.Frame(self.canvas, bg='#1e1e1e')
|
||||||
|
|
||||||
|
self.canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.canvas_window = self.canvas.create_window((0, 0), window=self.content_frame, anchor=tk.NW)
|
||||||
|
|
||||||
|
# 绑定事件
|
||||||
|
self.content_frame.bind("<Configure>", self._on_frame_configure)
|
||||||
|
self.canvas.bind("<Configure>", self._on_canvas_configure)
|
||||||
|
|
||||||
|
# 鼠标滚轮支持
|
||||||
|
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
|
||||||
|
|
||||||
|
# 对话历史区域
|
||||||
|
self.history_frame = tk.Frame(self.content_frame, bg='#1e1e1e')
|
||||||
|
self.history_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
# 当前问题区域
|
||||||
|
self.question_frame = tk.Frame(self.content_frame, bg='#252526', relief=tk.FLAT)
|
||||||
|
self.question_frame.pack(fill=tk.X, pady=10)
|
||||||
|
|
||||||
|
# 问题标签
|
||||||
|
self.question_label = tk.Label(
|
||||||
|
self.question_frame,
|
||||||
|
text="",
|
||||||
|
font=('Microsoft YaHei UI', 11),
|
||||||
|
fg='#ffffff',
|
||||||
|
bg='#252526',
|
||||||
|
wraplength=600,
|
||||||
|
justify=tk.LEFT,
|
||||||
|
padx=15,
|
||||||
|
pady=10
|
||||||
|
)
|
||||||
|
self.question_label.pack(fill=tk.X)
|
||||||
|
|
||||||
|
# 选项区域
|
||||||
|
self.options_frame = tk.Frame(self.question_frame, bg='#252526')
|
||||||
|
self.options_frame.pack(fill=tk.X, padx=15, pady=(0, 15))
|
||||||
|
|
||||||
|
# 底部按钮区域
|
||||||
|
btn_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
|
btn_frame.pack(fill=tk.X, padx=15, pady=15)
|
||||||
|
|
||||||
|
# 取消按钮
|
||||||
|
self.cancel_btn = tk.Button(
|
||||||
|
btn_frame,
|
||||||
|
text="取消",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#424242',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#616161',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=20,
|
||||||
|
pady=5,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._on_cancel
|
||||||
|
)
|
||||||
|
self.cancel_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 已收集信息提示
|
||||||
|
self.info_label = tk.Label(
|
||||||
|
btn_frame,
|
||||||
|
text="",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
fg='#81c784',
|
||||||
|
bg='#1e1e1e'
|
||||||
|
)
|
||||||
|
self.info_label.pack(side=tk.LEFT, padx=20)
|
||||||
|
|
||||||
|
# 确定按钮
|
||||||
|
self.submit_btn = tk.Button(
|
||||||
|
btn_frame,
|
||||||
|
text="确定 →",
|
||||||
|
font=('Microsoft YaHei UI', 10, 'bold'),
|
||||||
|
bg='#0e639c',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#1177bb',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=20,
|
||||||
|
pady=5,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._on_submit
|
||||||
|
)
|
||||||
|
self.submit_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def _on_frame_configure(self, event):
|
||||||
|
"""内容框架大小变化"""
|
||||||
|
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||||
|
|
||||||
|
def _on_canvas_configure(self, event):
|
||||||
|
"""Canvas 大小变化"""
|
||||||
|
self.canvas.itemconfig(self.canvas_window, width=event.width)
|
||||||
|
|
||||||
|
def _on_mousewheel(self, event):
|
||||||
|
"""鼠标滚轮"""
|
||||||
|
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||||
|
|
||||||
|
def set_question(self, question: str, options: List[Dict[str, Any]]):
|
||||||
|
"""
|
||||||
|
设置当前问题和选项
|
||||||
|
|
||||||
|
Args:
|
||||||
|
question: 问题文本
|
||||||
|
options: 选项列表,每个选项是一个字典
|
||||||
|
"""
|
||||||
|
# 更新问题
|
||||||
|
self.question_label.config(text=f"❓ {question}")
|
||||||
|
|
||||||
|
# 清除旧选项
|
||||||
|
for widget in self._option_widgets:
|
||||||
|
widget.destroy()
|
||||||
|
self._option_widgets.clear()
|
||||||
|
self._vars.clear()
|
||||||
|
|
||||||
|
# 创建新选项
|
||||||
|
for opt_data in options:
|
||||||
|
opt = ClarifyOption(
|
||||||
|
id=opt_data.get('id', ''),
|
||||||
|
type=opt_data.get('type', 'input'),
|
||||||
|
label=opt_data.get('label', ''),
|
||||||
|
choices=opt_data.get('choices', []),
|
||||||
|
default=opt_data.get('default'),
|
||||||
|
placeholder=opt_data.get('placeholder', '')
|
||||||
|
)
|
||||||
|
self._create_option_widget(opt)
|
||||||
|
|
||||||
|
def _create_option_widget(self, option: ClarifyOption):
|
||||||
|
"""创建选项控件"""
|
||||||
|
# 选项容器
|
||||||
|
container = tk.Frame(self.options_frame, bg='#252526')
|
||||||
|
container.pack(fill=tk.X, pady=5)
|
||||||
|
self._option_widgets.append(container)
|
||||||
|
|
||||||
|
# 标签
|
||||||
|
if option.label:
|
||||||
|
label = tk.Label(
|
||||||
|
container,
|
||||||
|
text=option.label,
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
fg='#cccccc',
|
||||||
|
bg='#252526'
|
||||||
|
)
|
||||||
|
label.pack(anchor=tk.W, pady=(0, 5))
|
||||||
|
|
||||||
|
if option.type == 'radio':
|
||||||
|
self._create_radio_option(container, option)
|
||||||
|
elif option.type == 'checkbox':
|
||||||
|
self._create_checkbox_option(container, option)
|
||||||
|
elif option.type == 'input':
|
||||||
|
self._create_input_option(container, option)
|
||||||
|
|
||||||
|
def _create_radio_option(self, parent: tk.Widget, option: ClarifyOption):
|
||||||
|
"""创建单选按钮"""
|
||||||
|
var = tk.StringVar(value=option.default or (option.choices[0] if option.choices else ''))
|
||||||
|
self._vars[option.id] = var
|
||||||
|
|
||||||
|
radio_frame = tk.Frame(parent, bg='#252526')
|
||||||
|
radio_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
# 检查是否是位置选项(需要预览)
|
||||||
|
is_position = self._is_position_option(option)
|
||||||
|
|
||||||
|
if is_position:
|
||||||
|
# 使用网格布局显示位置预览
|
||||||
|
self._create_position_radio_with_preview(radio_frame, option, var)
|
||||||
|
else:
|
||||||
|
# 普通单选按钮
|
||||||
|
for choice in option.choices:
|
||||||
|
rb = tk.Radiobutton(
|
||||||
|
radio_frame,
|
||||||
|
text=choice,
|
||||||
|
variable=var,
|
||||||
|
value=choice,
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
fg='#e0e0e0',
|
||||||
|
bg='#252526',
|
||||||
|
activebackground='#252526',
|
||||||
|
activeforeground='#ffffff',
|
||||||
|
selectcolor='#3c3c3c',
|
||||||
|
cursor='hand2'
|
||||||
|
)
|
||||||
|
rb.pack(anchor=tk.W, pady=2)
|
||||||
|
self._option_widgets.append(rb)
|
||||||
|
|
||||||
|
def _is_position_option(self, option: ClarifyOption) -> bool:
|
||||||
|
"""判断是否是位置选项"""
|
||||||
|
position_keywords = ['position', 'pos', '位置', '方位']
|
||||||
|
|
||||||
|
opt_id_lower = option.id.lower()
|
||||||
|
label_lower = option.label.lower()
|
||||||
|
|
||||||
|
for keyword in position_keywords:
|
||||||
|
if keyword in opt_id_lower or keyword in label_lower:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查选项是否包含位置相关词汇
|
||||||
|
position_values = ['左上', '右上', '左下', '右下', '居中', '中心', '顶部', '底部',
|
||||||
|
'top', 'bottom', 'left', 'right', 'center', 'middle']
|
||||||
|
|
||||||
|
for choice in option.choices:
|
||||||
|
choice_lower = choice.lower()
|
||||||
|
for pos in position_values:
|
||||||
|
if pos in choice_lower:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_position_radio_with_preview(self, parent: tk.Widget, option: ClarifyOption, var: tk.StringVar):
|
||||||
|
"""创建带位置预览的单选按钮"""
|
||||||
|
container = tk.Frame(parent, bg='#252526')
|
||||||
|
container.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
# 左侧:单选按钮列表
|
||||||
|
radio_list = tk.Frame(container, bg='#252526')
|
||||||
|
radio_list.pack(side=tk.LEFT, fill=tk.Y)
|
||||||
|
|
||||||
|
for choice in option.choices:
|
||||||
|
rb = tk.Radiobutton(
|
||||||
|
radio_list,
|
||||||
|
text=choice,
|
||||||
|
variable=var,
|
||||||
|
value=choice,
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
fg='#e0e0e0',
|
||||||
|
bg='#252526',
|
||||||
|
activebackground='#252526',
|
||||||
|
activeforeground='#ffffff',
|
||||||
|
selectcolor='#3c3c3c',
|
||||||
|
cursor='hand2',
|
||||||
|
command=lambda: self._update_position_preview(var, preview_canvas)
|
||||||
|
)
|
||||||
|
rb.pack(anchor=tk.W, pady=2)
|
||||||
|
self._option_widgets.append(rb)
|
||||||
|
|
||||||
|
# 右侧:位置预览
|
||||||
|
preview_frame = tk.Frame(container, bg='#3c3c3c', relief=tk.SOLID, borderwidth=1)
|
||||||
|
preview_frame.pack(side=tk.LEFT, padx=(20, 0))
|
||||||
|
|
||||||
|
preview_canvas = tk.Canvas(
|
||||||
|
preview_frame,
|
||||||
|
width=120,
|
||||||
|
height=80,
|
||||||
|
bg='#3c3c3c',
|
||||||
|
highlightthickness=0
|
||||||
|
)
|
||||||
|
preview_canvas.pack(padx=2, pady=2)
|
||||||
|
self._option_widgets.append(preview_canvas)
|
||||||
|
|
||||||
|
# 绘制初始预览
|
||||||
|
self._update_position_preview(var, preview_canvas)
|
||||||
|
|
||||||
|
# 绑定变量变化
|
||||||
|
var.trace_add('write', lambda *args: self._update_position_preview(var, preview_canvas))
|
||||||
|
|
||||||
|
def _update_position_preview(self, var: tk.StringVar, canvas: tk.Canvas):
|
||||||
|
"""更新位置预览"""
|
||||||
|
canvas.delete("all")
|
||||||
|
|
||||||
|
# 绘制背景矩形(代表图片)
|
||||||
|
canvas.create_rectangle(5, 5, 115, 75, outline='#666666', width=1)
|
||||||
|
|
||||||
|
# 获取当前选择的位置
|
||||||
|
position = var.get().lower()
|
||||||
|
|
||||||
|
# 计算标记位置
|
||||||
|
positions_map = {
|
||||||
|
# 中文
|
||||||
|
'左上': (20, 20),
|
||||||
|
'右上': (100, 20),
|
||||||
|
'左下': (20, 60),
|
||||||
|
'右下': (100, 60),
|
||||||
|
'居中': (60, 40),
|
||||||
|
'中心': (60, 40),
|
||||||
|
'顶部居中': (60, 20),
|
||||||
|
'底部居中': (60, 60),
|
||||||
|
'左侧居中': (20, 40),
|
||||||
|
'右侧居中': (100, 40),
|
||||||
|
# 英文
|
||||||
|
'top-left': (20, 20),
|
||||||
|
'top-right': (100, 20),
|
||||||
|
'bottom-left': (20, 60),
|
||||||
|
'bottom-right': (100, 60),
|
||||||
|
'center': (60, 40),
|
||||||
|
'top': (60, 20),
|
||||||
|
'bottom': (60, 60),
|
||||||
|
'left': (20, 40),
|
||||||
|
'right': (100, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查找匹配的位置
|
||||||
|
marker_pos = None
|
||||||
|
for key, pos in positions_map.items():
|
||||||
|
if key in position:
|
||||||
|
marker_pos = pos
|
||||||
|
break
|
||||||
|
|
||||||
|
if not marker_pos:
|
||||||
|
# 默认居中
|
||||||
|
marker_pos = (60, 40)
|
||||||
|
|
||||||
|
# 绘制位置标记
|
||||||
|
x, y = marker_pos
|
||||||
|
canvas.create_oval(x-8, y-8, x+8, y+8, fill='#4fc3f7', outline='#29b6f6', width=2)
|
||||||
|
canvas.create_text(x, y, text="W", fill='white', font=('Arial', 8, 'bold'))
|
||||||
|
|
||||||
|
def _create_checkbox_option(self, parent: tk.Widget, option: ClarifyOption):
|
||||||
|
"""创建复选框"""
|
||||||
|
vars_dict = {}
|
||||||
|
self._vars[option.id] = vars_dict
|
||||||
|
|
||||||
|
checkbox_frame = tk.Frame(parent, bg='#252526')
|
||||||
|
checkbox_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
# 解析默认值
|
||||||
|
default_values = []
|
||||||
|
if option.default:
|
||||||
|
if isinstance(option.default, list):
|
||||||
|
default_values = option.default
|
||||||
|
elif isinstance(option.default, str):
|
||||||
|
default_values = [option.default]
|
||||||
|
|
||||||
|
for choice in option.choices:
|
||||||
|
var = tk.BooleanVar(value=choice in default_values)
|
||||||
|
vars_dict[choice] = var
|
||||||
|
|
||||||
|
cb = tk.Checkbutton(
|
||||||
|
checkbox_frame,
|
||||||
|
text=choice,
|
||||||
|
variable=var,
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
fg='#e0e0e0',
|
||||||
|
bg='#252526',
|
||||||
|
activebackground='#252526',
|
||||||
|
activeforeground='#ffffff',
|
||||||
|
selectcolor='#3c3c3c',
|
||||||
|
cursor='hand2'
|
||||||
|
)
|
||||||
|
cb.pack(anchor=tk.W, pady=2)
|
||||||
|
self._option_widgets.append(cb)
|
||||||
|
|
||||||
|
def _create_input_option(self, parent: tk.Widget, option: ClarifyOption):
|
||||||
|
"""创建输入框"""
|
||||||
|
var = tk.StringVar(value=option.default or '')
|
||||||
|
self._vars[option.id] = var
|
||||||
|
|
||||||
|
input_container = tk.Frame(parent, bg='#252526')
|
||||||
|
input_container.pack(fill=tk.X, pady=2)
|
||||||
|
|
||||||
|
entry = tk.Entry(
|
||||||
|
input_container,
|
||||||
|
textvariable=var,
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#3c3c3c',
|
||||||
|
fg='#ffffff',
|
||||||
|
insertbackground='#ffffff',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
width=40
|
||||||
|
)
|
||||||
|
entry.pack(side=tk.LEFT, ipady=5)
|
||||||
|
self._option_widgets.append(entry)
|
||||||
|
|
||||||
|
# 检查是否是颜色输入(通过 id 或 label 判断)
|
||||||
|
is_color = self._is_color_option(option)
|
||||||
|
|
||||||
|
if is_color:
|
||||||
|
# 添加颜色预览框
|
||||||
|
preview_frame = tk.Frame(input_container, bg='#252526')
|
||||||
|
preview_frame.pack(side=tk.LEFT, padx=(10, 0))
|
||||||
|
|
||||||
|
color_preview = tk.Label(
|
||||||
|
preview_frame,
|
||||||
|
text=" ",
|
||||||
|
bg=option.default or '#000000',
|
||||||
|
width=4,
|
||||||
|
height=1,
|
||||||
|
relief=tk.SOLID,
|
||||||
|
borderwidth=1
|
||||||
|
)
|
||||||
|
color_preview.pack(side=tk.LEFT)
|
||||||
|
self._option_widgets.append(color_preview)
|
||||||
|
|
||||||
|
# 添加颜色选择按钮
|
||||||
|
color_btn = tk.Button(
|
||||||
|
preview_frame,
|
||||||
|
text="选择",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
bg='#424242',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#616161',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=8,
|
||||||
|
cursor='hand2',
|
||||||
|
command=lambda v=var, p=color_preview: self._pick_color(v, p)
|
||||||
|
)
|
||||||
|
color_btn.pack(side=tk.LEFT, padx=(5, 0))
|
||||||
|
self._option_widgets.append(color_btn)
|
||||||
|
|
||||||
|
# 绑定输入变化事件更新预览
|
||||||
|
var.trace_add('write', lambda *args, v=var, p=color_preview: self._update_color_preview(v, p))
|
||||||
|
|
||||||
|
# 占位符提示
|
||||||
|
if option.placeholder:
|
||||||
|
placeholder_label = tk.Label(
|
||||||
|
parent,
|
||||||
|
text=f"💡 {option.placeholder}",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
fg='#666666',
|
||||||
|
bg='#252526'
|
||||||
|
)
|
||||||
|
placeholder_label.pack(anchor=tk.W)
|
||||||
|
self._option_widgets.append(placeholder_label)
|
||||||
|
|
||||||
|
def _is_color_option(self, option: ClarifyOption) -> bool:
|
||||||
|
"""判断是否是颜色选项"""
|
||||||
|
color_keywords = ['color', 'colour', '颜色', '色彩', 'rgb', 'hex']
|
||||||
|
|
||||||
|
# 检查 id
|
||||||
|
opt_id_lower = option.id.lower()
|
||||||
|
for keyword in color_keywords:
|
||||||
|
if keyword in opt_id_lower:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查 label
|
||||||
|
label_lower = option.label.lower()
|
||||||
|
for keyword in color_keywords:
|
||||||
|
if keyword in label_lower:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查默认值是否像颜色值
|
||||||
|
if option.default:
|
||||||
|
default = option.default.strip()
|
||||||
|
if default.startswith('#') and len(default) in [4, 7, 9]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查 placeholder
|
||||||
|
if option.placeholder:
|
||||||
|
placeholder_lower = option.placeholder.lower()
|
||||||
|
for keyword in color_keywords:
|
||||||
|
if keyword in placeholder_lower:
|
||||||
|
return True
|
||||||
|
# 检查是否包含颜色格式提示
|
||||||
|
if '#' in option.placeholder and ('rgb' in placeholder_lower or 'rrggbb' in placeholder_lower):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _update_color_preview(self, var: tk.StringVar, preview: tk.Label):
|
||||||
|
"""更新颜色预览"""
|
||||||
|
color = var.get().strip()
|
||||||
|
|
||||||
|
# 验证颜色格式
|
||||||
|
if self._is_valid_color(color):
|
||||||
|
try:
|
||||||
|
preview.config(bg=color)
|
||||||
|
except tk.TclError:
|
||||||
|
pass # 无效颜色,忽略
|
||||||
|
|
||||||
|
def _is_valid_color(self, color: str) -> bool:
|
||||||
|
"""验证颜色格式是否有效"""
|
||||||
|
if not color:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查十六进制颜色格式
|
||||||
|
if color.startswith('#'):
|
||||||
|
hex_part = color[1:]
|
||||||
|
if len(hex_part) in [3, 6, 8]:
|
||||||
|
try:
|
||||||
|
int(hex_part, 16)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查常见颜色名称
|
||||||
|
common_colors = [
|
||||||
|
'red', 'green', 'blue', 'yellow', 'orange', 'purple', 'pink',
|
||||||
|
'black', 'white', 'gray', 'grey', 'cyan', 'magenta', 'brown'
|
||||||
|
]
|
||||||
|
if color.lower() in common_colors:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _pick_color(self, var: tk.StringVar, preview: tk.Label):
|
||||||
|
"""打开颜色选择器"""
|
||||||
|
from tkinter import colorchooser
|
||||||
|
|
||||||
|
# 获取当前颜色作为初始值
|
||||||
|
current = var.get().strip()
|
||||||
|
initial_color = current if self._is_valid_color(current) else '#000000'
|
||||||
|
|
||||||
|
# 打开颜色选择对话框
|
||||||
|
color = colorchooser.askcolor(
|
||||||
|
color=initial_color,
|
||||||
|
title="选择颜色"
|
||||||
|
)
|
||||||
|
|
||||||
|
if color[1]: # color[1] 是十六进制颜色值
|
||||||
|
var.set(color[1].upper())
|
||||||
|
preview.config(bg=color[1])
|
||||||
|
|
||||||
|
def add_history_item(self, question: str, answer: str):
|
||||||
|
"""
|
||||||
|
添加历史对话项
|
||||||
|
|
||||||
|
Args:
|
||||||
|
question: 问题
|
||||||
|
answer: 用户的回答
|
||||||
|
"""
|
||||||
|
self._history.append({'question': question, 'answer': answer})
|
||||||
|
|
||||||
|
# 创建历史项 UI
|
||||||
|
item_frame = tk.Frame(self.history_frame, bg='#2d2d2d', relief=tk.FLAT)
|
||||||
|
item_frame.pack(fill=tk.X, pady=3)
|
||||||
|
|
||||||
|
# 问题
|
||||||
|
q_label = tk.Label(
|
||||||
|
item_frame,
|
||||||
|
text=f"Q: {question}",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
fg='#888888',
|
||||||
|
bg='#2d2d2d',
|
||||||
|
anchor=tk.W,
|
||||||
|
padx=10,
|
||||||
|
pady=3
|
||||||
|
)
|
||||||
|
q_label.pack(fill=tk.X)
|
||||||
|
|
||||||
|
# 回答
|
||||||
|
a_label = tk.Label(
|
||||||
|
item_frame,
|
||||||
|
text=f"A: {answer}",
|
||||||
|
font=('Microsoft YaHei UI', 9, 'bold'),
|
||||||
|
fg='#81c784',
|
||||||
|
bg='#2d2d2d',
|
||||||
|
anchor=tk.W,
|
||||||
|
padx=10,
|
||||||
|
pady=3
|
||||||
|
)
|
||||||
|
a_label.pack(fill=tk.X)
|
||||||
|
|
||||||
|
def get_current_answers(self) -> Dict[str, Any]:
|
||||||
|
"""获取当前选项的答案"""
|
||||||
|
answers = {}
|
||||||
|
|
||||||
|
for opt_id, var in self._vars.items():
|
||||||
|
if isinstance(var, tk.StringVar):
|
||||||
|
answers[opt_id] = var.get()
|
||||||
|
elif isinstance(var, dict):
|
||||||
|
# checkbox 的情况
|
||||||
|
selected = [k for k, v in var.items() if v.get()]
|
||||||
|
answers[opt_id] = selected
|
||||||
|
|
||||||
|
return answers
|
||||||
|
|
||||||
|
def update_info_label(self, collected_count: int, total_count: int):
|
||||||
|
"""更新已收集信息提示"""
|
||||||
|
if total_count > 0:
|
||||||
|
self.info_label.config(text=f"已收集 {collected_count}/{total_count} 项信息")
|
||||||
|
else:
|
||||||
|
self.info_label.config(text="")
|
||||||
|
|
||||||
|
def set_submit_button_text(self, text: str):
|
||||||
|
"""设置确定按钮文本"""
|
||||||
|
self.submit_btn.config(text=text)
|
||||||
|
|
||||||
|
def _on_submit(self):
|
||||||
|
"""确定按钮点击"""
|
||||||
|
answers = self.get_current_answers()
|
||||||
|
self.on_submit(answers)
|
||||||
|
|
||||||
|
def _on_cancel(self):
|
||||||
|
"""取消按钮点击"""
|
||||||
|
self.on_cancel()
|
||||||
|
|
||||||
|
def show_loading(self, text: str = "加载中..."):
|
||||||
|
"""显示加载状态"""
|
||||||
|
# 禁用按钮
|
||||||
|
self.submit_btn.config(state=tk.DISABLED)
|
||||||
|
self.cancel_btn.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# 更新信息标签显示加载状态
|
||||||
|
self._original_info_text = self.info_label.cget('text')
|
||||||
|
self.info_label.config(text=f"⏳ {text}", fg='#ffa726')
|
||||||
|
|
||||||
|
def hide_loading(self):
|
||||||
|
"""隐藏加载状态"""
|
||||||
|
# 恢复按钮
|
||||||
|
self.submit_btn.config(state=tk.NORMAL)
|
||||||
|
self.cancel_btn.config(state=tk.NORMAL)
|
||||||
|
|
||||||
|
# 恢复信息标签
|
||||||
|
if hasattr(self, '_original_info_text'):
|
||||||
|
self.info_label.config(text=self._original_info_text, fg='#81c784')
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""显示视图"""
|
||||||
|
self.frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
"""隐藏视图"""
|
||||||
|
self.frame.pack_forget()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""重置视图"""
|
||||||
|
# 清除历史
|
||||||
|
self._history.clear()
|
||||||
|
for widget in self.history_frame.winfo_children():
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
|
# 清除选项
|
||||||
|
for widget in self._option_widgets:
|
||||||
|
widget.destroy()
|
||||||
|
self._option_widgets.clear()
|
||||||
|
self._vars.clear()
|
||||||
|
|
||||||
|
# 重置标签
|
||||||
|
self.question_label.config(text="")
|
||||||
|
self.info_label.config(text="")
|
||||||
|
self.submit_btn.config(text="确定 →")
|
||||||
|
|
||||||
|
def get_frame(self) -> tk.Frame:
|
||||||
|
"""获取主框架"""
|
||||||
|
return self.frame
|
||||||
|
|
||||||
@@ -1,32 +1,230 @@
|
|||||||
"""
|
"""
|
||||||
历史记录视图组件
|
历史记录视图组件
|
||||||
显示任务执行历史
|
显示任务执行历史,支持 Markdown 渲染、代码复用、失败重试、勾选删除
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, List, Optional, Set
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from history.manager import TaskRecord, HistoryManager
|
from history.manager import TaskRecord, HistoryManager
|
||||||
|
|
||||||
|
|
||||||
|
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', 14, 'bold'), foreground='#ffd54f', spacing3=10)
|
||||||
|
self.tag_configure('h2', font=('Microsoft YaHei UI', 12, 'bold'), foreground='#4fc3f7', spacing3=8)
|
||||||
|
self.tag_configure('h3', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#81c784', spacing3=6)
|
||||||
|
|
||||||
|
# 普通文本
|
||||||
|
self.tag_configure('normal', font=('Microsoft YaHei UI', 10), foreground='#d4d4d4')
|
||||||
|
|
||||||
|
# 代码块
|
||||||
|
self.tag_configure('code', font=('Consolas', 9), foreground='#ce93d8', background='#1a1a1a')
|
||||||
|
self.tag_configure('code_block', font=('Consolas', 9), foreground='#ce93d8', background='#1a1a1a',
|
||||||
|
lmargin1=20, lmargin2=20, spacing1=5, spacing3=5)
|
||||||
|
|
||||||
|
# 列表
|
||||||
|
self.tag_configure('list_item', font=('Microsoft YaHei UI', 10), foreground='#d4d4d4', lmargin1=20, lmargin2=30)
|
||||||
|
|
||||||
|
# 强调
|
||||||
|
self.tag_configure('bold', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#ffffff')
|
||||||
|
self.tag_configure('italic', font=('Microsoft YaHei UI', 10, 'italic'), foreground='#b0b0b0')
|
||||||
|
|
||||||
|
# 状态
|
||||||
|
self.tag_configure('success', foreground='#81c784')
|
||||||
|
self.tag_configure('error', foreground='#ef5350')
|
||||||
|
self.tag_configure('label', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#4fc3f7')
|
||||||
|
|
||||||
|
def render_markdown(self, text: str):
|
||||||
|
"""渲染 Markdown 文本"""
|
||||||
|
self.config(state=tk.NORMAL)
|
||||||
|
self.delete(1.0, tk.END)
|
||||||
|
|
||||||
|
lines = text.split('\n')
|
||||||
|
in_code_block = False
|
||||||
|
code_block_content = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# 代码块处理
|
||||||
|
if line.strip().startswith('```'):
|
||||||
|
if in_code_block:
|
||||||
|
# 结束代码块
|
||||||
|
code_text = '\n'.join(code_block_content)
|
||||||
|
self.insert(tk.END, code_text + '\n', 'code_block')
|
||||||
|
code_block_content = []
|
||||||
|
in_code_block = False
|
||||||
|
else:
|
||||||
|
# 开始代码块
|
||||||
|
in_code_block = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_code_block:
|
||||||
|
code_block_content.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
if line.startswith('### '):
|
||||||
|
self.insert(tk.END, line[4:] + '\n', 'h3')
|
||||||
|
elif line.startswith('## '):
|
||||||
|
self.insert(tk.END, line[3:] + '\n', 'h2')
|
||||||
|
elif line.startswith('# '):
|
||||||
|
self.insert(tk.END, line[2:] + '\n', 'h1')
|
||||||
|
# 列表项
|
||||||
|
elif line.strip().startswith('- ') or line.strip().startswith('* '):
|
||||||
|
content = line.strip()[2:]
|
||||||
|
self.insert(tk.END, ' • ' + content + '\n', 'list_item')
|
||||||
|
elif re.match(r'^\d+\.\s', line.strip()):
|
||||||
|
self.insert(tk.END, ' ' + line.strip() + '\n', 'list_item')
|
||||||
|
# 普通行
|
||||||
|
else:
|
||||||
|
self._render_inline(line + '\n')
|
||||||
|
|
||||||
|
# 处理未闭合的代码块
|
||||||
|
if code_block_content:
|
||||||
|
code_text = '\n'.join(code_block_content)
|
||||||
|
self.insert(tk.END, code_text + '\n', 'code_block')
|
||||||
|
|
||||||
|
self.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def _render_inline(self, text: str):
|
||||||
|
"""渲染行内元素"""
|
||||||
|
# 简单处理:查找 `code` 和 **bold**
|
||||||
|
pattern = r'(`[^`]+`|\*\*[^*]+\*\*)'
|
||||||
|
parts = re.split(pattern, text)
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
if part.startswith('`') and part.endswith('`'):
|
||||||
|
self.insert(tk.END, part[1:-1], 'code')
|
||||||
|
elif part.startswith('**') and part.endswith('**'):
|
||||||
|
self.insert(tk.END, part[2:-2], 'bold')
|
||||||
|
else:
|
||||||
|
self.insert(tk.END, part, 'normal')
|
||||||
|
|
||||||
|
|
||||||
|
class CheckboxTreeview(ttk.Treeview):
|
||||||
|
"""
|
||||||
|
带勾选框的 Treeview
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent, **kwargs):
|
||||||
|
super().__init__(parent, **kwargs)
|
||||||
|
|
||||||
|
# 勾选状态存储
|
||||||
|
self._checked: Set[str] = set()
|
||||||
|
|
||||||
|
# 勾选变化回调
|
||||||
|
self._on_check_changed: Optional[Callable[[Set[str]], None]] = None
|
||||||
|
|
||||||
|
# 绑定点击事件
|
||||||
|
self.bind('<Button-1>', self._on_click)
|
||||||
|
|
||||||
|
def set_on_check_changed(self, callback: Callable[[Set[str]], None]):
|
||||||
|
"""设置勾选变化回调"""
|
||||||
|
self._on_check_changed = callback
|
||||||
|
|
||||||
|
def _on_click(self, event):
|
||||||
|
"""处理点击事件"""
|
||||||
|
region = self.identify_region(event.x, event.y)
|
||||||
|
|
||||||
|
# 点击在第一列(勾选框区域)
|
||||||
|
if region == 'cell':
|
||||||
|
column = self.identify_column(event.x)
|
||||||
|
if column == '#1': # 第一列是勾选框
|
||||||
|
item = self.identify_row(event.y)
|
||||||
|
if item:
|
||||||
|
self._toggle_check(item)
|
||||||
|
|
||||||
|
def _toggle_check(self, item: str):
|
||||||
|
"""切换勾选状态"""
|
||||||
|
if item in self._checked:
|
||||||
|
self._checked.remove(item)
|
||||||
|
else:
|
||||||
|
self._checked.add(item)
|
||||||
|
|
||||||
|
# 更新显示
|
||||||
|
self._update_check_display(item)
|
||||||
|
|
||||||
|
# 触发回调
|
||||||
|
if self._on_check_changed:
|
||||||
|
self._on_check_changed(self._checked.copy())
|
||||||
|
|
||||||
|
def _update_check_display(self, item: str):
|
||||||
|
"""更新勾选框显示"""
|
||||||
|
values = list(self.item(item, 'values'))
|
||||||
|
if values:
|
||||||
|
values[0] = '☑' if item in self._checked else '☐'
|
||||||
|
self.item(item, values=values)
|
||||||
|
|
||||||
|
def get_checked(self) -> Set[str]:
|
||||||
|
"""获取所有勾选的项"""
|
||||||
|
return self._checked.copy()
|
||||||
|
|
||||||
|
def clear_checked(self):
|
||||||
|
"""清除所有勾选"""
|
||||||
|
for item in list(self._checked):
|
||||||
|
self._checked.remove(item)
|
||||||
|
self._update_check_display(item)
|
||||||
|
|
||||||
|
if self._on_check_changed:
|
||||||
|
self._on_check_changed(set())
|
||||||
|
|
||||||
|
def check_all(self):
|
||||||
|
"""全选"""
|
||||||
|
for item in self.get_children():
|
||||||
|
if item not in self._checked:
|
||||||
|
self._checked.add(item)
|
||||||
|
self._update_check_display(item)
|
||||||
|
|
||||||
|
if self._on_check_changed:
|
||||||
|
self._on_check_changed(self._checked.copy())
|
||||||
|
|
||||||
|
def insert_with_checkbox(self, parent, index, iid=None, **kwargs):
|
||||||
|
"""插入带勾选框的项"""
|
||||||
|
values = list(kwargs.get('values', []))
|
||||||
|
# 在最前面插入勾选框
|
||||||
|
values.insert(0, '☐')
|
||||||
|
kwargs['values'] = values
|
||||||
|
return self.insert(parent, index, iid=iid, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class HistoryView:
|
class HistoryView:
|
||||||
"""
|
"""
|
||||||
历史记录视图
|
历史记录视图
|
||||||
|
|
||||||
显示任务执行历史列表,支持查看详情
|
显示任务执行历史列表,支持:
|
||||||
|
- 查看详情(Markdown 渲染)
|
||||||
|
- 复用成功的代码
|
||||||
|
- 重试失败的任务
|
||||||
|
- 勾选删除
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent: tk.Widget,
|
parent: tk.Widget,
|
||||||
history_manager: HistoryManager,
|
history_manager: HistoryManager,
|
||||||
on_back: Callable[[], None]
|
on_back: Callable[[], None],
|
||||||
|
on_reuse_code: Optional[Callable[[TaskRecord], None]] = None,
|
||||||
|
on_retry_task: Optional[Callable[[TaskRecord], None]] = None
|
||||||
):
|
):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.history = history_manager
|
self.history = history_manager
|
||||||
self.on_back = on_back
|
self.on_back = on_back
|
||||||
|
self.on_reuse_code = on_reuse_code
|
||||||
|
self.on_retry_task = on_retry_task
|
||||||
|
|
||||||
self._selected_record: Optional[TaskRecord] = None
|
self._selected_record: Optional[TaskRecord] = None
|
||||||
self._create_widgets()
|
self._create_widgets()
|
||||||
@@ -68,48 +266,104 @@ class HistoryView:
|
|||||||
# 统计信息
|
# 统计信息
|
||||||
stats = self.history.get_stats()
|
stats = self.history.get_stats()
|
||||||
stats_text = f"共 {stats['total']} 条 | 成功 {stats['success']} | 失败 {stats['failed']} | 成功率 {stats['success_rate']:.0%}"
|
stats_text = f"共 {stats['total']} 条 | 成功 {stats['success']} | 失败 {stats['failed']} | 成功率 {stats['success_rate']:.0%}"
|
||||||
stats_label = tk.Label(
|
self.stats_label = tk.Label(
|
||||||
title_frame,
|
title_frame,
|
||||||
text=stats_text,
|
text=stats_text,
|
||||||
font=('Microsoft YaHei UI', 9),
|
font=('Microsoft YaHei UI', 9),
|
||||||
fg='#888888',
|
fg='#888888',
|
||||||
bg='#1e1e1e'
|
bg='#1e1e1e'
|
||||||
)
|
)
|
||||||
stats_label.pack(side=tk.RIGHT)
|
self.stats_label.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
# 主内容区域(左右分栏)
|
# 主内容区域(左右分栏)
|
||||||
content_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
content_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
# 配置列权重,让右侧详情区域更宽
|
||||||
|
content_frame.columnconfigure(0, weight=2) # 左侧列表
|
||||||
|
content_frame.columnconfigure(1, weight=3) # 右侧详情
|
||||||
|
content_frame.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
# 左侧:历史列表
|
# 左侧:历史列表
|
||||||
list_frame = tk.LabelFrame(
|
list_frame = tk.LabelFrame(
|
||||||
content_frame,
|
content_frame,
|
||||||
text=" 任务列表 ",
|
text=" 任务列表",
|
||||||
font=('Microsoft YaHei UI', 10, 'bold'),
|
font=('Microsoft YaHei UI', 10, 'bold'),
|
||||||
fg='#4fc3f7',
|
fg='#4fc3f7',
|
||||||
bg='#1e1e1e',
|
bg='#1e1e1e',
|
||||||
relief=tk.GROOVE
|
relief=tk.GROOVE
|
||||||
)
|
)
|
||||||
list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
|
list_frame.grid(row=0, column=0, sticky='nsew', padx=(0, 5))
|
||||||
|
|
||||||
|
# 列表操作栏
|
||||||
|
list_toolbar = tk.Frame(list_frame, bg='#2d2d2d')
|
||||||
|
list_toolbar.pack(fill=tk.X, padx=3, pady=(3, 0))
|
||||||
|
|
||||||
|
# 全选按钮
|
||||||
|
self.select_all_btn = tk.Button(
|
||||||
|
list_toolbar,
|
||||||
|
text="☑ 全选",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
bg='#3d3d3d',
|
||||||
|
fg='#aaaaaa',
|
||||||
|
activebackground='#4d4d4d',
|
||||||
|
activeforeground='#ffffff',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=8,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._select_all
|
||||||
|
)
|
||||||
|
self.select_all_btn.pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
|
||||||
|
# 取消全选按钮
|
||||||
|
self.deselect_all_btn = tk.Button(
|
||||||
|
list_toolbar,
|
||||||
|
text="☐ 取消全选",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
bg='#3d3d3d',
|
||||||
|
fg='#aaaaaa',
|
||||||
|
activebackground='#4d4d4d',
|
||||||
|
activeforeground='#ffffff',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=8,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._deselect_all
|
||||||
|
)
|
||||||
|
self.deselect_all_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 已选数量提示
|
||||||
|
self.selected_count_label = tk.Label(
|
||||||
|
list_toolbar,
|
||||||
|
text="",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
fg='#ffd54f',
|
||||||
|
bg='#2d2d2d'
|
||||||
|
)
|
||||||
|
self.selected_count_label.pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
# 列表框
|
# 列表框
|
||||||
list_container = tk.Frame(list_frame, bg='#2d2d2d')
|
list_container = tk.Frame(list_frame, bg='#2d2d2d')
|
||||||
list_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
list_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
||||||
|
|
||||||
# 使用 Treeview 显示列表
|
# 使用带勾选框的 Treeview 显示列表
|
||||||
columns = ('time', 'input', 'status', 'duration')
|
columns = ('check', 'time', 'description', 'status', 'duration')
|
||||||
self.tree = ttk.Treeview(list_container, columns=columns, show='headings', height=15)
|
self.tree = CheckboxTreeview(list_container, columns=columns, show='headings', height=18)
|
||||||
|
|
||||||
# 配置列
|
# 配置列
|
||||||
|
self.tree.heading('check', text='')
|
||||||
self.tree.heading('time', text='时间')
|
self.tree.heading('time', text='时间')
|
||||||
self.tree.heading('input', text='任务描述')
|
self.tree.heading('description', text='任务描述')
|
||||||
self.tree.heading('status', text='状态')
|
self.tree.heading('status', text='状态')
|
||||||
self.tree.heading('duration', text='耗时')
|
self.tree.heading('duration', text='耗时')
|
||||||
|
|
||||||
self.tree.column('time', width=120, minwidth=100)
|
self.tree.column('check', width=30, minwidth=30, anchor='center')
|
||||||
self.tree.column('input', width=250, minwidth=150)
|
self.tree.column('time', width=130, minwidth=110)
|
||||||
self.tree.column('status', width=60, minwidth=50)
|
self.tree.column('description', width=180, minwidth=120)
|
||||||
self.tree.column('duration', width=70, minwidth=50)
|
self.tree.column('status', width=65, minwidth=55)
|
||||||
|
self.tree.column('duration', width=65, minwidth=50)
|
||||||
|
|
||||||
|
# 设置勾选变化回调
|
||||||
|
self.tree.set_on_check_changed(self._on_check_changed)
|
||||||
|
|
||||||
# 滚动条
|
# 滚动条
|
||||||
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.tree.yview)
|
scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.tree.yview)
|
||||||
@@ -130,13 +384,13 @@ class HistoryView:
|
|||||||
bg='#1e1e1e',
|
bg='#1e1e1e',
|
||||||
relief=tk.GROOVE
|
relief=tk.GROOVE
|
||||||
)
|
)
|
||||||
detail_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
detail_frame.grid(row=0, column=1, sticky='nsew', padx=(5, 0))
|
||||||
|
|
||||||
# 详情文本框
|
# 详情文本框(使用 Markdown 渲染)
|
||||||
detail_container = tk.Frame(detail_frame, bg='#2d2d2d')
|
detail_container = tk.Frame(detail_frame, bg='#2d2d2d')
|
||||||
detail_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
detail_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
||||||
|
|
||||||
self.detail_text = tk.Text(
|
self.detail_text = MarkdownText(
|
||||||
detail_container,
|
detail_container,
|
||||||
wrap=tk.WORD,
|
wrap=tk.WORD,
|
||||||
font=('Microsoft YaHei UI', 10),
|
font=('Microsoft YaHei UI', 10),
|
||||||
@@ -154,20 +408,17 @@ class HistoryView:
|
|||||||
detail_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
detail_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
self.detail_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
self.detail_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# 配置详情文本样式
|
|
||||||
self.detail_text.tag_configure('title', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#ffd54f')
|
|
||||||
self.detail_text.tag_configure('label', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#4fc3f7')
|
|
||||||
self.detail_text.tag_configure('success', foreground='#81c784')
|
|
||||||
self.detail_text.tag_configure('error', foreground='#ef5350')
|
|
||||||
self.detail_text.tag_configure('code', font=('Consolas', 9), foreground='#ce93d8')
|
|
||||||
|
|
||||||
# 底部按钮
|
# 底部按钮
|
||||||
btn_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
btn_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
|
# 左侧按钮组
|
||||||
|
left_btn_frame = tk.Frame(btn_frame, bg='#1e1e1e')
|
||||||
|
left_btn_frame.pack(side=tk.LEFT)
|
||||||
|
|
||||||
# 打开日志按钮
|
# 打开日志按钮
|
||||||
self.open_log_btn = tk.Button(
|
self.open_log_btn = tk.Button(
|
||||||
btn_frame,
|
left_btn_frame,
|
||||||
text="📄 打开日志",
|
text="📄 打开日志",
|
||||||
font=('Microsoft YaHei UI', 10),
|
font=('Microsoft YaHei UI', 10),
|
||||||
bg='#424242',
|
bg='#424242',
|
||||||
@@ -180,23 +431,62 @@ class HistoryView:
|
|||||||
state=tk.DISABLED,
|
state=tk.DISABLED,
|
||||||
command=self._open_log
|
command=self._open_log
|
||||||
)
|
)
|
||||||
self.open_log_btn.pack(side=tk.LEFT)
|
self.open_log_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
|
||||||
# 清空历史按钮
|
# 复用代码按钮
|
||||||
clear_btn = tk.Button(
|
self.reuse_btn = tk.Button(
|
||||||
btn_frame,
|
left_btn_frame,
|
||||||
text="🗑️ 清空历史",
|
text="🔄 复用此代码",
|
||||||
font=('Microsoft YaHei UI', 10),
|
font=('Microsoft YaHei UI', 10),
|
||||||
bg='#d32f2f',
|
bg='#0e639c',
|
||||||
fg='white',
|
fg='white',
|
||||||
activebackground='#f44336',
|
activebackground='#1177bb',
|
||||||
activeforeground='white',
|
activeforeground='white',
|
||||||
relief=tk.FLAT,
|
relief=tk.FLAT,
|
||||||
padx=15,
|
padx=15,
|
||||||
cursor='hand2',
|
cursor='hand2',
|
||||||
command=self._clear_history
|
state=tk.DISABLED,
|
||||||
|
command=self._reuse_code
|
||||||
)
|
)
|
||||||
clear_btn.pack(side=tk.RIGHT)
|
self.reuse_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
|
||||||
|
# 重试按钮(仅失败任务可用)
|
||||||
|
self.retry_btn = tk.Button(
|
||||||
|
left_btn_frame,
|
||||||
|
text="🔧 重试(AI修复)",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#f57c00',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#ff9800',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=15,
|
||||||
|
cursor='hand2',
|
||||||
|
state=tk.DISABLED,
|
||||||
|
command=self._retry_task
|
||||||
|
)
|
||||||
|
self.retry_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 右侧按钮组
|
||||||
|
right_btn_frame = tk.Frame(btn_frame, bg='#1e1e1e')
|
||||||
|
right_btn_frame.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# 删除选中按钮(默认禁用)
|
||||||
|
self.delete_btn = tk.Button(
|
||||||
|
right_btn_frame,
|
||||||
|
text="🗑️ 删除选中 (0)",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#5d5d5d',
|
||||||
|
fg='#888888',
|
||||||
|
activebackground='#5d5d5d',
|
||||||
|
activeforeground='#888888',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=15,
|
||||||
|
cursor='arrow',
|
||||||
|
state=tk.DISABLED,
|
||||||
|
command=self._delete_selected
|
||||||
|
)
|
||||||
|
self.delete_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
# 加载数据
|
# 加载数据
|
||||||
self._load_data()
|
self._load_data()
|
||||||
@@ -207,34 +497,116 @@ class HistoryView:
|
|||||||
for item in self.tree.get_children():
|
for item in self.tree.get_children():
|
||||||
self.tree.delete(item)
|
self.tree.delete(item)
|
||||||
|
|
||||||
|
# 清空勾选状态
|
||||||
|
self.tree._checked.clear()
|
||||||
|
|
||||||
# 加载历史记录
|
# 加载历史记录
|
||||||
records = self.history.get_all()
|
records = self.history.get_all()
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
# 截断过长的输入
|
# 使用任务描述(如果有)或截断的用户输入
|
||||||
input_text = record.user_input
|
description = getattr(record, 'task_summary', None) or record.user_input
|
||||||
if len(input_text) > 30:
|
if len(description) > 20:
|
||||||
input_text = input_text[:30] + "..."
|
description = description[:20] + "..."
|
||||||
|
|
||||||
status = "✓ 成功" if record.success else "✗ 失败"
|
status = "✓ 成功" if record.success else "✗ 失败"
|
||||||
duration = f"{record.duration_ms}ms"
|
duration = f"{record.duration_ms}ms"
|
||||||
|
|
||||||
# 提取时间(只显示时分秒)
|
self.tree.insert_with_checkbox('', tk.END, iid=record.task_id, values=(
|
||||||
time_parts = record.timestamp.split(' ')
|
|
||||||
time_str = time_parts[1] if len(time_parts) > 1 else record.timestamp
|
|
||||||
date_str = time_parts[0] if len(time_parts) > 0 else ""
|
|
||||||
display_time = f"{date_str}\n{time_str}"
|
|
||||||
|
|
||||||
self.tree.insert('', tk.END, iid=record.task_id, values=(
|
|
||||||
record.timestamp,
|
record.timestamp,
|
||||||
input_text,
|
description,
|
||||||
status,
|
status,
|
||||||
duration
|
duration
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# 更新统计信息
|
||||||
|
self._update_stats()
|
||||||
|
|
||||||
|
# 更新删除按钮状态
|
||||||
|
self._update_delete_button(set())
|
||||||
|
|
||||||
# 显示空状态提示
|
# 显示空状态提示
|
||||||
if not records:
|
if not records:
|
||||||
self._show_detail("暂无历史记录\n\n执行任务后,记录将显示在这里。")
|
self._show_empty_state()
|
||||||
|
|
||||||
|
def _update_stats(self):
|
||||||
|
"""更新统计信息"""
|
||||||
|
stats = self.history.get_stats()
|
||||||
|
stats_text = f"共 {stats['total']} 条 | 成功 {stats['success']} | 失败 {stats['failed']} | 成功率 {stats['success_rate']:.0%}"
|
||||||
|
self.stats_label.config(text=stats_text)
|
||||||
|
|
||||||
|
def _on_check_changed(self, checked: Set[str]):
|
||||||
|
"""勾选状态变化回调"""
|
||||||
|
self._update_delete_button(checked)
|
||||||
|
|
||||||
|
# 更新已选数量提示
|
||||||
|
count = len(checked)
|
||||||
|
if count > 0:
|
||||||
|
self.selected_count_label.config(text=f"已选 {count} 项")
|
||||||
|
else:
|
||||||
|
self.selected_count_label.config(text="")
|
||||||
|
|
||||||
|
def _update_delete_button(self, checked: Set[str]):
|
||||||
|
"""更新删除按钮状态"""
|
||||||
|
count = len(checked)
|
||||||
|
if count > 0:
|
||||||
|
self.delete_btn.config(
|
||||||
|
text=f"🗑️ 删除选中 ({count})",
|
||||||
|
state=tk.NORMAL,
|
||||||
|
bg='#d32f2f',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#f44336',
|
||||||
|
activeforeground='white',
|
||||||
|
cursor='hand2'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.delete_btn.config(
|
||||||
|
text="🗑️ 删除选中 (0)",
|
||||||
|
state=tk.DISABLED,
|
||||||
|
bg='#5d5d5d',
|
||||||
|
fg='#888888',
|
||||||
|
activebackground='#5d5d5d',
|
||||||
|
activeforeground='#888888',
|
||||||
|
cursor='arrow'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _select_all(self):
|
||||||
|
"""全选"""
|
||||||
|
self.tree.check_all()
|
||||||
|
|
||||||
|
def _deselect_all(self):
|
||||||
|
"""取消全选"""
|
||||||
|
self.tree.clear_checked()
|
||||||
|
|
||||||
|
def _delete_selected(self):
|
||||||
|
"""删除选中的记录"""
|
||||||
|
checked = self.tree.get_checked()
|
||||||
|
if not checked:
|
||||||
|
return
|
||||||
|
|
||||||
|
count = len(checked)
|
||||||
|
result = messagebox.askyesno(
|
||||||
|
"确认删除",
|
||||||
|
f"确定要删除选中的 {count} 条记录吗?\n此操作不可恢复。",
|
||||||
|
icon='warning'
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
# 删除选中的记录
|
||||||
|
for task_id in checked:
|
||||||
|
self.history.delete_by_id(task_id)
|
||||||
|
|
||||||
|
# 重新加载数据
|
||||||
|
self._load_data()
|
||||||
|
self._show_empty_state() if not self.history.get_all() else None
|
||||||
|
|
||||||
|
# 重置按钮状态
|
||||||
|
self.open_log_btn.config(state=tk.DISABLED)
|
||||||
|
self.reuse_btn.config(state=tk.DISABLED)
|
||||||
|
self.retry_btn.config(state=tk.DISABLED)
|
||||||
|
self._selected_record = None
|
||||||
|
|
||||||
|
messagebox.showinfo("删除成功", f"已删除 {count} 条记录")
|
||||||
|
|
||||||
def _on_select(self, event):
|
def _on_select(self, event):
|
||||||
"""选择记录事件"""
|
"""选择记录事件"""
|
||||||
@@ -248,77 +620,84 @@ class HistoryView:
|
|||||||
if record:
|
if record:
|
||||||
self._selected_record = record
|
self._selected_record = record
|
||||||
self._show_record_detail(record)
|
self._show_record_detail(record)
|
||||||
|
|
||||||
|
# 更新按钮状态
|
||||||
self.open_log_btn.config(state=tk.NORMAL)
|
self.open_log_btn.config(state=tk.NORMAL)
|
||||||
|
self.reuse_btn.config(state=tk.NORMAL if record.success else tk.DISABLED)
|
||||||
|
self.retry_btn.config(state=tk.NORMAL if not record.success else tk.DISABLED)
|
||||||
|
|
||||||
def _show_record_detail(self, record: TaskRecord):
|
def _show_record_detail(self, record: TaskRecord):
|
||||||
"""显示记录详情"""
|
"""显示记录详情(Markdown 格式)"""
|
||||||
self.detail_text.config(state=tk.NORMAL)
|
# 构建 Markdown 内容
|
||||||
self.detail_text.delete(1.0, tk.END)
|
status_text = "✓ 成功" if record.success else "✗ 失败"
|
||||||
|
|
||||||
# 标题
|
md_content = f"""## 任务 ID: {record.task_id}
|
||||||
self.detail_text.insert(tk.END, f"任务 ID: {record.task_id}\n", 'title')
|
|
||||||
self.detail_text.insert(tk.END, f"时间: {record.timestamp}\n\n")
|
|
||||||
|
|
||||||
# 用户输入
|
**时间:** {record.timestamp}
|
||||||
self.detail_text.insert(tk.END, "用户输入:\n", 'label')
|
**状态:** {status_text}
|
||||||
self.detail_text.insert(tk.END, f"{record.user_input}\n\n")
|
**耗时:** {record.duration_ms}ms
|
||||||
|
|
||||||
# 执行状态
|
---
|
||||||
self.detail_text.insert(tk.END, "执行状态: ", 'label')
|
|
||||||
if record.success:
|
|
||||||
self.detail_text.insert(tk.END, "成功 ✓\n", 'success')
|
|
||||||
else:
|
|
||||||
self.detail_text.insert(tk.END, "失败 ✗\n", 'error')
|
|
||||||
|
|
||||||
self.detail_text.insert(tk.END, f"耗时: {record.duration_ms}ms\n\n")
|
### 用户输入
|
||||||
|
{record.user_input}
|
||||||
|
|
||||||
# 执行计划
|
---
|
||||||
self.detail_text.insert(tk.END, "执行计划:\n", 'label')
|
|
||||||
plan_preview = record.execution_plan[:500] + "..." if len(record.execution_plan) > 500 else record.execution_plan
|
### 执行计划
|
||||||
self.detail_text.insert(tk.END, f"{plan_preview}\n\n")
|
{record.execution_plan}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 生成的代码
|
||||||
|
```python
|
||||||
|
{record.code}
|
||||||
|
```
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
# 输出
|
|
||||||
if record.stdout:
|
if record.stdout:
|
||||||
self.detail_text.insert(tk.END, "输出:\n", 'label')
|
md_content += f"""---
|
||||||
self.detail_text.insert(tk.END, f"{record.stdout}\n\n")
|
|
||||||
|
### 输出
|
||||||
|
{record.stdout}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
# 错误
|
|
||||||
if record.stderr:
|
if record.stderr:
|
||||||
self.detail_text.insert(tk.END, "错误:\n", 'label')
|
md_content += f"""---
|
||||||
self.detail_text.insert(tk.END, f"{record.stderr}\n", 'error')
|
|
||||||
|
|
||||||
self.detail_text.config(state=tk.DISABLED)
|
### 错误信息
|
||||||
|
{record.stderr}
|
||||||
|
"""
|
||||||
|
|
||||||
def _show_detail(self, text: str):
|
self.detail_text.render_markdown(md_content)
|
||||||
"""显示详情文本"""
|
|
||||||
|
def _show_empty_state(self):
|
||||||
|
"""显示空状态"""
|
||||||
self.detail_text.config(state=tk.NORMAL)
|
self.detail_text.config(state=tk.NORMAL)
|
||||||
self.detail_text.delete(1.0, tk.END)
|
self.detail_text.delete(1.0, tk.END)
|
||||||
self.detail_text.insert(tk.END, text)
|
self.detail_text.insert(tk.END, "暂无历史记录\n\n执行任务后,记录将显示在这里。", 'normal')
|
||||||
self.detail_text.config(state=tk.DISABLED)
|
self.detail_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
def _open_log(self):
|
def _open_log(self):
|
||||||
"""打开日志文件"""
|
"""打开日志文件"""
|
||||||
if self._selected_record and self._selected_record.log_path:
|
if self._selected_record and self._selected_record.log_path:
|
||||||
import os
|
|
||||||
log_path = Path(self._selected_record.log_path)
|
log_path = Path(self._selected_record.log_path)
|
||||||
if log_path.exists():
|
if log_path.exists():
|
||||||
os.startfile(str(log_path))
|
os.startfile(str(log_path))
|
||||||
else:
|
else:
|
||||||
messagebox.showwarning("提示", f"日志文件不存在:\n{log_path}")
|
messagebox.showwarning("提示", f"日志文件不存在:\n{log_path}")
|
||||||
|
|
||||||
def _clear_history(self):
|
def _reuse_code(self):
|
||||||
"""清空历史记录"""
|
"""复用代码"""
|
||||||
result = messagebox.askyesno(
|
if self._selected_record and self.on_reuse_code:
|
||||||
"确认清空",
|
self.on_reuse_code(self._selected_record)
|
||||||
"确定要清空所有历史记录吗?\n此操作不可恢复。",
|
|
||||||
icon='warning'
|
|
||||||
)
|
|
||||||
|
|
||||||
if result:
|
def _retry_task(self):
|
||||||
self.history.clear()
|
"""重试失败的任务"""
|
||||||
self._load_data()
|
if self._selected_record and self.on_retry_task:
|
||||||
self._show_detail("历史记录已清空")
|
self.on_retry_task(self._selected_record)
|
||||||
self.open_log_btn.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
"""显示视图"""
|
"""显示视图"""
|
||||||
@@ -332,4 +711,3 @@ class HistoryView:
|
|||||||
def get_frame(self) -> tk.Frame:
|
def get_frame(self) -> tk.Frame:
|
||||||
"""获取主框架"""
|
"""获取主框架"""
|
||||||
return self.frame
|
return self.frame
|
||||||
|
|
||||||
|
|||||||
370
ui/settings_view.py
Normal file
370
ui/settings_view.py
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
"""
|
||||||
|
设置视图
|
||||||
|
用于配置 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
|
||||||
|
|
||||||
Reference in New Issue
Block a user