Initial commit
This commit is contained in:
4
.env
Normal file
4
.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions
|
||||||
|
LLM_API_KEY=sk-fxsxbgatrjjhsnjpkdfgfngukqoqqgitjpxfqfxifcipaqpc
|
||||||
|
INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
||||||
|
GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
||||||
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions
|
||||||
|
LLM_API_KEY=sk-fxsxbgatrjjhsnjpkdfgfngukqoqqgitjpxfqfxifcipaqpc
|
||||||
|
INTENT_MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
||||||
|
GENERATION_MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
||||||
233
PRD.md
Normal file
233
PRD.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
====================
|
||||||
|
【产品目标】
|
||||||
|
====================
|
||||||
|
- 面向 Windows 小白用户:一句话输入
|
||||||
|
- 自动判断任务类型:
|
||||||
|
- chat:普通对话(如“今天天气怎么样”)
|
||||||
|
- execution:本地执行任务(文件处理)
|
||||||
|
- chat:
|
||||||
|
- 直接调用 LLM 返回文本
|
||||||
|
- execution:
|
||||||
|
- 生成执行计划
|
||||||
|
- 生成 Python 代码
|
||||||
|
- 安全校验
|
||||||
|
- 用户确认
|
||||||
|
- 一次性子进程执行
|
||||||
|
- 强制工作区副本目录:
|
||||||
|
workspace/input
|
||||||
|
workspace/output
|
||||||
|
workspace/logs
|
||||||
|
- MVP 明确不做:
|
||||||
|
- 联网任务(搜索 / 爬取)
|
||||||
|
- 鼠标 / 键盘自动化
|
||||||
|
- 后台常驻
|
||||||
|
- 多任务并行
|
||||||
|
- 核心安全原则:
|
||||||
|
- LLM 可以联网“思考”
|
||||||
|
- Executor(执行器)禁止联网“动手”
|
||||||
|
|
||||||
|
====================
|
||||||
|
【项目结构(必须严格按此生成)】
|
||||||
|
====================
|
||||||
|
LocalAgent/
|
||||||
|
main.py
|
||||||
|
requirements.txt
|
||||||
|
.env.example
|
||||||
|
ui/
|
||||||
|
chat_view.py
|
||||||
|
task_guide_view.py
|
||||||
|
llm/
|
||||||
|
client.py
|
||||||
|
prompts.py
|
||||||
|
intent/
|
||||||
|
classifier.py
|
||||||
|
labels.py
|
||||||
|
safety/
|
||||||
|
rule_checker.py
|
||||||
|
llm_reviewer.py
|
||||||
|
executor/
|
||||||
|
sandbox_runner.py
|
||||||
|
workspace/
|
||||||
|
input/
|
||||||
|
output/
|
||||||
|
logs/
|
||||||
|
|
||||||
|
====================
|
||||||
|
【统一 LLM 调用规则(非常重要)】
|
||||||
|
====================
|
||||||
|
- 所有模型(包括 qwen2.5:7b-instruct)都通过同一个 API:
|
||||||
|
https://api.siliconflow.cn/v1/chat/completions
|
||||||
|
- 不区分“本地 / 云端”客户端
|
||||||
|
- 区分只体现在:
|
||||||
|
- model name
|
||||||
|
- prompt
|
||||||
|
- temperature / max_tokens
|
||||||
|
|
||||||
|
.env.example 中必须包含:
|
||||||
|
|
||||||
|
LLM_API_URL=https://api.siliconflow.cn/v1/chat/completions
|
||||||
|
LLM_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# 用于意图识别的小模型
|
||||||
|
INTENT_MODEL_NAME=qwen2.5:7b-instruct
|
||||||
|
|
||||||
|
# 用于对话 / 计划 / 代码生成的模型(可先用同一个)
|
||||||
|
GENERATION_MODEL_NAME=Pro/zai-org/GLM-4.7
|
||||||
|
|
||||||
|
====================
|
||||||
|
【llm/client.py 要求】
|
||||||
|
====================
|
||||||
|
实现统一的 LLMClient:
|
||||||
|
|
||||||
|
- 使用 requests.post
|
||||||
|
- URL / API KEY 从 .env 读取
|
||||||
|
- 提供方法:
|
||||||
|
chat(
|
||||||
|
messages: list[dict],
|
||||||
|
model: str,
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int
|
||||||
|
) -> str
|
||||||
|
|
||||||
|
- payload 结构参考:
|
||||||
|
{
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"stream": false,
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_tokens": max_tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
- headers:
|
||||||
|
Authorization: Bearer <API_KEY>
|
||||||
|
- 对网络异常 / 非 200 状态码做明确异常抛出
|
||||||
|
- 不要在 client 中写任何业务逻辑
|
||||||
|
|
||||||
|
====================
|
||||||
|
【意图识别(核心修改点)】
|
||||||
|
====================
|
||||||
|
实现 intent/classifier.py:
|
||||||
|
|
||||||
|
- 使用“小参数 LLM”(INTENT_MODEL_NAME,例如 qwen2.5:7b-instruct)
|
||||||
|
- 目标:二分类
|
||||||
|
- chat
|
||||||
|
- execution
|
||||||
|
- 要求输出结构化结果:
|
||||||
|
{
|
||||||
|
"label": "chat" | "execution",
|
||||||
|
"confidence": 0.0 ~ 1.0,
|
||||||
|
"reason": "中文解释,说明为什么这是执行任务/对话任务"
|
||||||
|
}
|
||||||
|
|
||||||
|
- Prompt 必须极短、强约束、可解析
|
||||||
|
- Prompt 模板放在 llm/prompts.py
|
||||||
|
- 对 LLM 输出:
|
||||||
|
- 尝试解析 JSON
|
||||||
|
- 若解析失败 / 字段缺失 → 走兜底逻辑(判为 chat)
|
||||||
|
|
||||||
|
intent/labels.py:
|
||||||
|
- 定义常量:
|
||||||
|
CHAT
|
||||||
|
EXECUTION
|
||||||
|
- 定义阈值:
|
||||||
|
EXECUTION_CONFIDENCE_THRESHOLD = 0.6
|
||||||
|
- 低于阈值一律判定为 chat(宁可少执行,不可误执行)
|
||||||
|
|
||||||
|
====================
|
||||||
|
【Chat Task 流程】
|
||||||
|
====================
|
||||||
|
- 使用 GENERATION_MODEL_NAME
|
||||||
|
- messages = 用户原始输入
|
||||||
|
- 返回文本直接展示
|
||||||
|
- 不触碰本地、不产出文件
|
||||||
|
|
||||||
|
====================
|
||||||
|
【Execution Task 流程】
|
||||||
|
====================
|
||||||
|
1) 生成执行计划
|
||||||
|
- 可用 GENERATION_MODEL_NAME
|
||||||
|
- 输出中文、可读
|
||||||
|
- 明确:
|
||||||
|
- 会做什么
|
||||||
|
- 不会动原文件
|
||||||
|
- 输入 / 输出目录
|
||||||
|
- 可能失败的情况
|
||||||
|
|
||||||
|
2) 生成 Python 执行代码
|
||||||
|
- MVP 先内置“安全示例代码”:
|
||||||
|
- 遍历 workspace/input
|
||||||
|
- 复制文件到 workspace/output
|
||||||
|
- 不依赖第三方库
|
||||||
|
- 不修改原文件
|
||||||
|
- 保存为 workspace/task_<id>.py
|
||||||
|
|
||||||
|
3) safety/rule_checker.py(硬规则)
|
||||||
|
- 静态扫描执行代码:
|
||||||
|
- 禁止 requests / socket / urllib
|
||||||
|
- 禁止访问非 workspace 路径
|
||||||
|
- 禁止危险操作(os.remove, shutil.rmtree, subprocess 等)
|
||||||
|
- 若违反,直接 fail
|
||||||
|
|
||||||
|
4) safety/llm_reviewer.py(软规则)
|
||||||
|
- 使用 GENERATION_MODEL_NAME
|
||||||
|
- 输入:用户需求 + 执行计划 + 代码
|
||||||
|
- 输出:pass / fail + 中文原因
|
||||||
|
|
||||||
|
5) UI(小白引导式,方案 C)
|
||||||
|
- 显示:
|
||||||
|
- 判定原因 reason
|
||||||
|
- 三步引导:
|
||||||
|
1) 把文件复制到 input
|
||||||
|
2) 我来处理
|
||||||
|
3) 去 output 取
|
||||||
|
- 执行计划
|
||||||
|
- 风险提示
|
||||||
|
- 【开始执行】按钮
|
||||||
|
|
||||||
|
6) executor/sandbox_runner.py
|
||||||
|
- 使用 subprocess 启动一次性 Python 子进程
|
||||||
|
- 工作目录限定为 workspace
|
||||||
|
- 捕获 stdout / stderr
|
||||||
|
- 写入 workspace/logs/task_<id>.log
|
||||||
|
- 执行完即退出
|
||||||
|
- 执行器层不允许任何联网能力(由 rule_checker 保证)
|
||||||
|
|
||||||
|
====================
|
||||||
|
【UI(Tkinter)最小可跑要求】
|
||||||
|
====================
|
||||||
|
- main.py 启动 Tkinter 窗口
|
||||||
|
- 顶部:输入框 + 发送按钮
|
||||||
|
- 中部:输出区
|
||||||
|
- 当识别为 execution:
|
||||||
|
- 切换或弹出 task_guide_view
|
||||||
|
- 执行完成后展示:
|
||||||
|
- success / partial / failed
|
||||||
|
- 成功 / 失败数量
|
||||||
|
- 日志路径
|
||||||
|
|
||||||
|
====================
|
||||||
|
【requirements.txt(最小集)】
|
||||||
|
====================
|
||||||
|
- python-dotenv
|
||||||
|
- requests
|
||||||
|
|
||||||
|
====================
|
||||||
|
【最小可跑验收标准】
|
||||||
|
====================
|
||||||
|
- 未配置 LLM_KEY 时给出明确错误提示
|
||||||
|
- 输入“今天天气怎么样” → chat
|
||||||
|
- 输入“把这个文件夹里的图片复制一份” → execution
|
||||||
|
- execution 能生成 task_<id>.py 并真正执行
|
||||||
|
- output / logs 中有真实文件
|
||||||
|
|
||||||
|
====================
|
||||||
|
【Plan 模式输出要求】
|
||||||
|
====================
|
||||||
|
1) 先输出整体实现计划(步骤、模块职责)
|
||||||
|
2) 列出所有文件及其责任说明
|
||||||
|
3) 再按文件路径逐个输出代码内容
|
||||||
|
4) 确保 main.py 可直接运行
|
||||||
|
5) main.py 顶部注释说明:
|
||||||
|
- 如何配置 .env
|
||||||
|
- 如何运行
|
||||||
|
- 如何测试(往 input 放文件)
|
||||||
BIN
__pycache__/main.cpython-313.pyc
Normal file
BIN
__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
25
debug_env.py
Normal file
25
debug_env.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""调试脚本"""
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
ENV_PATH = Path(__file__).parent / ".env"
|
||||||
|
|
||||||
|
print(f"ENV_PATH: {ENV_PATH}")
|
||||||
|
print(f"ENV_PATH exists: {ENV_PATH.exists()}")
|
||||||
|
|
||||||
|
# 读取文件内容
|
||||||
|
if ENV_PATH.exists():
|
||||||
|
print(f"File content:")
|
||||||
|
print(ENV_PATH.read_text(encoding='utf-8'))
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
result = load_dotenv(ENV_PATH)
|
||||||
|
print(f"load_dotenv result: {result}")
|
||||||
|
|
||||||
|
# 检查环境变量
|
||||||
|
print(f"LLM_API_URL: {os.getenv('LLM_API_URL')}")
|
||||||
|
print(f"LLM_API_KEY: {os.getenv('LLM_API_KEY')}")
|
||||||
|
print(f"INTENT_MODEL_NAME: {os.getenv('INTENT_MODEL_NAME')}")
|
||||||
|
print(f"GENERATION_MODEL_NAME: {os.getenv('GENERATION_MODEL_NAME')}")
|
||||||
|
|
||||||
2
executor/__init__.py
Normal file
2
executor/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 执行器模块
|
||||||
|
|
||||||
BIN
executor/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
executor/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
executor/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
executor/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
executor/__pycache__/sandbox_runner.cpython-310.pyc
Normal file
BIN
executor/__pycache__/sandbox_runner.cpython-310.pyc
Normal file
Binary file not shown.
BIN
executor/__pycache__/sandbox_runner.cpython-313.pyc
Normal file
BIN
executor/__pycache__/sandbox_runner.cpython-313.pyc
Normal file
Binary file not shown.
240
executor/sandbox_runner.py
Normal file
240
executor/sandbox_runner.py
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
"""
|
||||||
|
沙箱执行器
|
||||||
|
在受限环境中执行生成的 Python 代码
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ExecutionResult:
|
||||||
|
"""执行结果"""
|
||||||
|
success: bool
|
||||||
|
task_id: str
|
||||||
|
stdout: str
|
||||||
|
stderr: str
|
||||||
|
return_code: int
|
||||||
|
log_path: str
|
||||||
|
duration_ms: int
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxRunner:
|
||||||
|
"""
|
||||||
|
沙箱执行器
|
||||||
|
|
||||||
|
特性:
|
||||||
|
1. 使用 subprocess 启动独立 Python 进程
|
||||||
|
2. 工作目录限定为 workspace
|
||||||
|
3. 捕获所有输出
|
||||||
|
4. 写入日志文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, workspace_path: Optional[str] = None):
|
||||||
|
if workspace_path:
|
||||||
|
self.workspace = Path(workspace_path)
|
||||||
|
else:
|
||||||
|
# 默认使用项目根目录下的 workspace
|
||||||
|
self.workspace = Path(__file__).parent.parent / "workspace"
|
||||||
|
|
||||||
|
self.input_dir = self.workspace / "input"
|
||||||
|
self.output_dir = self.workspace / "output"
|
||||||
|
self.logs_dir = self.workspace / "logs"
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
self.input_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def save_task_code(self, code: str, task_id: Optional[str] = None) -> tuple[str, Path]:
|
||||||
|
"""
|
||||||
|
保存任务代码到文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Python 代码
|
||||||
|
task_id: 任务 ID(可选,自动生成)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(task_id, code_path)
|
||||||
|
"""
|
||||||
|
if not task_id:
|
||||||
|
task_id = self._generate_task_id()
|
||||||
|
|
||||||
|
code_path = self.workspace / f"task_{task_id}.py"
|
||||||
|
code_path.write_text(code, encoding='utf-8')
|
||||||
|
|
||||||
|
return task_id, code_path
|
||||||
|
|
||||||
|
def execute(self, code: str, task_id: Optional[str] = None, timeout: int = 60) -> ExecutionResult:
|
||||||
|
"""
|
||||||
|
执行代码
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Python 代码
|
||||||
|
task_id: 任务 ID
|
||||||
|
timeout: 超时时间(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ExecutionResult: 执行结果
|
||||||
|
"""
|
||||||
|
# 保存代码
|
||||||
|
task_id, code_path = self.save_task_code(code, task_id)
|
||||||
|
|
||||||
|
# 准备日志
|
||||||
|
log_path = self.logs_dir / f"task_{task_id}.log"
|
||||||
|
|
||||||
|
start_time = datetime.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用 subprocess 执行
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(code_path)],
|
||||||
|
cwd=str(self.workspace),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
# 不继承父进程的环境变量中的网络代理等
|
||||||
|
env=self._get_safe_env()
|
||||||
|
)
|
||||||
|
|
||||||
|
end_time = datetime.now()
|
||||||
|
duration_ms = int((end_time - start_time).total_seconds() * 1000)
|
||||||
|
|
||||||
|
# 写入日志
|
||||||
|
self._write_log(
|
||||||
|
log_path=log_path,
|
||||||
|
task_id=task_id,
|
||||||
|
code_path=code_path,
|
||||||
|
stdout=result.stdout,
|
||||||
|
stderr=result.stderr,
|
||||||
|
return_code=result.returncode,
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExecutionResult(
|
||||||
|
success=result.returncode == 0,
|
||||||
|
task_id=task_id,
|
||||||
|
stdout=result.stdout,
|
||||||
|
stderr=result.stderr,
|
||||||
|
return_code=result.returncode,
|
||||||
|
log_path=str(log_path),
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
end_time = datetime.now()
|
||||||
|
duration_ms = int((end_time - start_time).total_seconds() * 1000)
|
||||||
|
|
||||||
|
error_msg = f"执行超时(超过 {timeout} 秒)"
|
||||||
|
|
||||||
|
self._write_log(
|
||||||
|
log_path=log_path,
|
||||||
|
task_id=task_id,
|
||||||
|
code_path=code_path,
|
||||||
|
stdout="",
|
||||||
|
stderr=error_msg,
|
||||||
|
return_code=-1,
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExecutionResult(
|
||||||
|
success=False,
|
||||||
|
task_id=task_id,
|
||||||
|
stdout="",
|
||||||
|
stderr=error_msg,
|
||||||
|
return_code=-1,
|
||||||
|
log_path=str(log_path),
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
end_time = datetime.now()
|
||||||
|
duration_ms = int((end_time - start_time).total_seconds() * 1000)
|
||||||
|
|
||||||
|
error_msg = f"执行异常: {str(e)}"
|
||||||
|
|
||||||
|
self._write_log(
|
||||||
|
log_path=log_path,
|
||||||
|
task_id=task_id,
|
||||||
|
code_path=code_path,
|
||||||
|
stdout="",
|
||||||
|
stderr=error_msg,
|
||||||
|
return_code=-1,
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExecutionResult(
|
||||||
|
success=False,
|
||||||
|
task_id=task_id,
|
||||||
|
stdout="",
|
||||||
|
stderr=error_msg,
|
||||||
|
return_code=-1,
|
||||||
|
log_path=str(log_path),
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
def _generate_task_id(self) -> str:
|
||||||
|
"""生成任务 ID"""
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
short_uuid = uuid.uuid4().hex[:6]
|
||||||
|
return f"{timestamp}_{short_uuid}"
|
||||||
|
|
||||||
|
def _get_safe_env(self) -> dict:
|
||||||
|
"""获取安全的环境变量(移除网络代理等)"""
|
||||||
|
safe_env = os.environ.copy()
|
||||||
|
|
||||||
|
# 移除可能的网络代理设置
|
||||||
|
proxy_vars = [
|
||||||
|
'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy',
|
||||||
|
'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy'
|
||||||
|
]
|
||||||
|
for var in proxy_vars:
|
||||||
|
safe_env.pop(var, None)
|
||||||
|
|
||||||
|
return safe_env
|
||||||
|
|
||||||
|
def _write_log(
|
||||||
|
self,
|
||||||
|
log_path: Path,
|
||||||
|
task_id: str,
|
||||||
|
code_path: Path,
|
||||||
|
stdout: str,
|
||||||
|
stderr: str,
|
||||||
|
return_code: int,
|
||||||
|
duration_ms: int
|
||||||
|
):
|
||||||
|
"""写入执行日志"""
|
||||||
|
log_content = f"""========================================
|
||||||
|
任务执行日志
|
||||||
|
========================================
|
||||||
|
任务 ID: {task_id}
|
||||||
|
代码文件: {code_path}
|
||||||
|
执行时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||||
|
耗时: {duration_ms} ms
|
||||||
|
返回码: {return_code}
|
||||||
|
状态: {"成功" if return_code == 0 else "失败"}
|
||||||
|
|
||||||
|
========================================
|
||||||
|
标准输出 (stdout)
|
||||||
|
========================================
|
||||||
|
{stdout if stdout else "(无输出)"}
|
||||||
|
|
||||||
|
========================================
|
||||||
|
标准错误 (stderr)
|
||||||
|
========================================
|
||||||
|
{stderr if stderr else "(无错误)"}
|
||||||
|
"""
|
||||||
|
log_path.write_text(log_content, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def run_task(code: str, task_id: Optional[str] = None) -> ExecutionResult:
|
||||||
|
"""便捷函数:执行任务"""
|
||||||
|
runner = SandboxRunner()
|
||||||
|
return runner.execute(code, task_id)
|
||||||
|
|
||||||
2
intent/__init__.py
Normal file
2
intent/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 意图识别模块
|
||||||
|
|
||||||
BIN
intent/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
intent/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
intent/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
intent/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intent/__pycache__/classifier.cpython-310.pyc
Normal file
BIN
intent/__pycache__/classifier.cpython-310.pyc
Normal file
Binary file not shown.
BIN
intent/__pycache__/classifier.cpython-313.pyc
Normal file
BIN
intent/__pycache__/classifier.cpython-313.pyc
Normal file
Binary file not shown.
BIN
intent/__pycache__/labels.cpython-310.pyc
Normal file
BIN
intent/__pycache__/labels.cpython-310.pyc
Normal file
Binary file not shown.
BIN
intent/__pycache__/labels.cpython-313.pyc
Normal file
BIN
intent/__pycache__/labels.cpython-313.pyc
Normal file
Binary file not shown.
152
intent/classifier.py
Normal file
152
intent/classifier.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
"""
|
||||||
|
意图识别器
|
||||||
|
使用小参数 LLM 进行意图二分类
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from llm.client import get_client, LLMClientError, ENV_PATH
|
||||||
|
from llm.prompts import INTENT_CLASSIFICATION_SYSTEM, INTENT_CLASSIFICATION_USER
|
||||||
|
from intent.labels import CHAT, EXECUTION, EXECUTION_CONFIDENCE_THRESHOLD, VALID_LABELS
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IntentResult:
|
||||||
|
"""意图识别结果"""
|
||||||
|
label: str # chat 或 execution
|
||||||
|
confidence: float # 0.0 ~ 1.0
|
||||||
|
reason: str # 中文解释
|
||||||
|
raw_response: Optional[str] = None # 原始 LLM 响应(调试用)
|
||||||
|
|
||||||
|
|
||||||
|
class IntentClassifier:
|
||||||
|
"""
|
||||||
|
意图分类器
|
||||||
|
|
||||||
|
使用小参数 LLM(如 qwen2.5:7b-instruct)进行快速意图识别
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
self.model_name = os.getenv("INTENT_MODEL_NAME")
|
||||||
|
|
||||||
|
def classify(self, user_input: str) -> IntentResult:
|
||||||
|
"""
|
||||||
|
对用户输入进行意图分类
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_input: 用户输入的文本
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
IntentResult: 包含 label, confidence, reason 的结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = get_client()
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": INTENT_CLASSIFICATION_SYSTEM},
|
||||||
|
{"role": "user", "content": INTENT_CLASSIFICATION_USER.format(user_input=user_input)}
|
||||||
|
]
|
||||||
|
|
||||||
|
response = client.chat(
|
||||||
|
messages=messages,
|
||||||
|
model=self.model_name,
|
||||||
|
temperature=0.1, # 低温度,更确定性的输出
|
||||||
|
max_tokens=256
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._parse_response(response)
|
||||||
|
|
||||||
|
except LLMClientError as e:
|
||||||
|
# LLM 调用失败,走兜底逻辑
|
||||||
|
return IntentResult(
|
||||||
|
label=CHAT,
|
||||||
|
confidence=0.0,
|
||||||
|
reason=f"意图识别失败({str(e)}),默认为对话模式"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# 其他异常,走兜底逻辑
|
||||||
|
return IntentResult(
|
||||||
|
label=CHAT,
|
||||||
|
confidence=0.0,
|
||||||
|
reason=f"意图识别异常({str(e)}),默认为对话模式"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_response(self, response: str) -> IntentResult:
|
||||||
|
"""
|
||||||
|
解析 LLM 响应
|
||||||
|
|
||||||
|
尝试解析 JSON,若失败则走兜底逻辑
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 尝试提取 JSON(LLM 可能会在 JSON 前后加一些文字)
|
||||||
|
json_str = self._extract_json(response)
|
||||||
|
data = json.loads(json_str)
|
||||||
|
|
||||||
|
# 验证必要字段
|
||||||
|
label = data.get("label", "").lower()
|
||||||
|
confidence = float(data.get("confidence", 0.0))
|
||||||
|
reason = data.get("reason", "无")
|
||||||
|
|
||||||
|
# 验证 label 有效性
|
||||||
|
if label not in VALID_LABELS:
|
||||||
|
return IntentResult(
|
||||||
|
label=CHAT,
|
||||||
|
confidence=0.0,
|
||||||
|
reason=f"无效的意图标签 '{label}',默认为对话模式",
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
# 应用置信度阈值
|
||||||
|
if label == EXECUTION and confidence < EXECUTION_CONFIDENCE_THRESHOLD:
|
||||||
|
return IntentResult(
|
||||||
|
label=CHAT,
|
||||||
|
confidence=confidence,
|
||||||
|
reason=f"执行任务置信度不足({confidence:.2f} < {EXECUTION_CONFIDENCE_THRESHOLD}),降级为对话模式。原因: {reason}",
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
return IntentResult(
|
||||||
|
label=label,
|
||||||
|
confidence=confidence,
|
||||||
|
reason=reason,
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, ValueError, TypeError) as e:
|
||||||
|
# JSON 解析失败,走兜底逻辑
|
||||||
|
return IntentResult(
|
||||||
|
label=CHAT,
|
||||||
|
confidence=0.0,
|
||||||
|
reason=f"响应解析失败,默认为对话模式",
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_json(self, text: str) -> str:
|
||||||
|
"""
|
||||||
|
从文本中提取 JSON 字符串
|
||||||
|
|
||||||
|
LLM 可能会在 JSON 前后添加解释文字,需要提取纯 JSON 部分
|
||||||
|
"""
|
||||||
|
# 尝试找到 JSON 对象的起止位置
|
||||||
|
start = text.find('{')
|
||||||
|
end = text.rfind('}')
|
||||||
|
|
||||||
|
if start != -1 and end != -1 and end > start:
|
||||||
|
return text[start:end + 1]
|
||||||
|
|
||||||
|
# 如果找不到,返回原文本让 json.loads 报错
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# 便捷函数
|
||||||
|
def classify_intent(user_input: str) -> IntentResult:
|
||||||
|
"""快速进行意图分类"""
|
||||||
|
classifier = IntentClassifier()
|
||||||
|
return classifier.classify(user_input)
|
||||||
|
|
||||||
15
intent/labels.py
Normal file
15
intent/labels.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
意图标签定义
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 意图类型常量
|
||||||
|
CHAT = "chat"
|
||||||
|
EXECUTION = "execution"
|
||||||
|
|
||||||
|
# 执行任务置信度阈值
|
||||||
|
# 低于此阈值一律判定为 chat(宁可少执行,不可误执行)
|
||||||
|
EXECUTION_CONFIDENCE_THRESHOLD = 0.6
|
||||||
|
|
||||||
|
# 所有有效标签
|
||||||
|
VALID_LABELS = {CHAT, EXECUTION}
|
||||||
|
|
||||||
2
llm/__init__.py
Normal file
2
llm/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# LLM 模块
|
||||||
|
|
||||||
BIN
llm/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
llm/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
llm/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
llm/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
llm/__pycache__/client.cpython-310.pyc
Normal file
BIN
llm/__pycache__/client.cpython-310.pyc
Normal file
Binary file not shown.
BIN
llm/__pycache__/client.cpython-313.pyc
Normal file
BIN
llm/__pycache__/client.cpython-313.pyc
Normal file
Binary file not shown.
BIN
llm/__pycache__/prompts.cpython-310.pyc
Normal file
BIN
llm/__pycache__/prompts.cpython-310.pyc
Normal file
Binary file not shown.
BIN
llm/__pycache__/prompts.cpython-313.pyc
Normal file
BIN
llm/__pycache__/prompts.cpython-313.pyc
Normal file
Binary file not shown.
124
llm/client.py
Normal file
124
llm/client.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""
|
||||||
|
LLM 统一调用客户端
|
||||||
|
所有模型通过 SiliconFlow API 调用
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 获取项目根目录
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
ENV_PATH = PROJECT_ROOT / ".env"
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClientError(Exception):
|
||||||
|
"""LLM 客户端异常"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClient:
|
||||||
|
"""
|
||||||
|
统一的 LLM 调用客户端
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
client = LLMClient()
|
||||||
|
response = client.chat(
|
||||||
|
messages=[{"role": "user", "content": "你好"}],
|
||||||
|
model="Qwen/Qwen2.5-7B-Instruct",
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=1024
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
|
self.api_url = os.getenv("LLM_API_URL")
|
||||||
|
self.api_key = os.getenv("LLM_API_KEY")
|
||||||
|
|
||||||
|
if not self.api_url:
|
||||||
|
raise LLMClientError("未配置 LLM_API_URL,请检查 .env 文件")
|
||||||
|
if not self.api_key or self.api_key == "your_api_key_here":
|
||||||
|
raise LLMClientError("未配置有效的 LLM_API_KEY,请检查 .env 文件")
|
||||||
|
|
||||||
|
def chat(
|
||||||
|
self,
|
||||||
|
messages: list[dict],
|
||||||
|
model: str,
|
||||||
|
temperature: float = 0.7,
|
||||||
|
max_tokens: int = 1024
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
调用 LLM 进行对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: 消息列表,格式为 [{"role": "user/assistant/system", "content": "..."}]
|
||||||
|
model: 模型名称
|
||||||
|
temperature: 温度参数,控制随机性
|
||||||
|
max_tokens: 最大生成 token 数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LLM 生成的文本内容
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
LLMClientError: 网络异常或 API 返回错误
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"stream": False,
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_tokens": max_tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
self.api_url,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
raise LLMClientError("请求超时,请检查网络连接")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
raise LLMClientError("网络连接失败,请检查网络设置")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
raise LLMClientError(f"网络请求异常: {str(e)}")
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
error_msg = f"API 返回错误 (状态码: {response.status_code})"
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
if "error" in error_detail:
|
||||||
|
error_msg += f": {error_detail['error']}"
|
||||||
|
except:
|
||||||
|
error_msg += f": {response.text[:200]}"
|
||||||
|
raise LLMClientError(error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = response.json()
|
||||||
|
content = result["choices"][0]["message"]["content"]
|
||||||
|
return content
|
||||||
|
except (KeyError, IndexError, TypeError) as e:
|
||||||
|
raise LLMClientError(f"解析 API 响应失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 全局单例(延迟初始化)
|
||||||
|
_client: Optional[LLMClient] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_client() -> LLMClient:
|
||||||
|
"""获取 LLM 客户端单例"""
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = LLMClient()
|
||||||
|
return _client
|
||||||
|
|
||||||
130
llm/prompts.py
Normal file
130
llm/prompts.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
Prompt 模板集合
|
||||||
|
所有与 LLM 交互的 Prompt 统一在此管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 意图识别 Prompt
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
INTENT_CLASSIFICATION_SYSTEM = """你是一个意图分类器。判断用户输入是"普通对话"还是"本地执行任务"。
|
||||||
|
|
||||||
|
规则:
|
||||||
|
- chat: 闲聊、问答、知识查询(如天气、新闻、解释概念)
|
||||||
|
- execution: 需要操作本地文件的任务(如复制、移动、重命名、整理文件)
|
||||||
|
|
||||||
|
只输出JSON,格式:
|
||||||
|
{"label": "chat或execution", "confidence": 0.0到1.0, "reason": "简短中文理由"}"""
|
||||||
|
|
||||||
|
INTENT_CLASSIFICATION_USER = """判断以下输入的意图:
|
||||||
|
{user_input}"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 执行计划生成 Prompt
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
EXECUTION_PLAN_SYSTEM = """你是一个任务规划助手。根据用户需求,生成清晰的执行计划。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
1. 所有操作只在 workspace 目录内进行
|
||||||
|
2. 输入文件来自 workspace/input
|
||||||
|
3. 输出文件保存到 workspace/output
|
||||||
|
4. 绝不修改或删除原始文件
|
||||||
|
5. 不进行任何网络操作
|
||||||
|
|
||||||
|
输出格式(中文):
|
||||||
|
## 任务理解
|
||||||
|
[简述用户想做什么]
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
1. [步骤1]
|
||||||
|
2. [步骤2]
|
||||||
|
...
|
||||||
|
|
||||||
|
## 输入输出
|
||||||
|
- 输入目录: workspace/input
|
||||||
|
- 输出目录: workspace/output
|
||||||
|
|
||||||
|
## 风险提示
|
||||||
|
[可能失败的情况]"""
|
||||||
|
|
||||||
|
EXECUTION_PLAN_USER = """用户需求:{user_input}
|
||||||
|
|
||||||
|
请生成执行计划。"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 代码生成 Prompt
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
CODE_GENERATION_SYSTEM = """你是一个 Python 代码生成器。根据执行计划生成安全的文件处理代码。
|
||||||
|
|
||||||
|
硬性约束:
|
||||||
|
1. 只能操作 workspace/input 和 workspace/output 目录
|
||||||
|
2. 禁止使用: requests, socket, urllib, subprocess, os.system
|
||||||
|
3. 禁止删除文件: os.remove, shutil.rmtree, os.unlink
|
||||||
|
4. 禁止访问 workspace 外的任何路径
|
||||||
|
5. 只使用标准库: os, shutil, pathlib, json, csv 等
|
||||||
|
|
||||||
|
代码模板:
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 工作目录
|
||||||
|
WORKSPACE = Path(__file__).parent
|
||||||
|
INPUT_DIR = WORKSPACE / "input"
|
||||||
|
OUTPUT_DIR = WORKSPACE / "output"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 确保输出目录存在
|
||||||
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# TODO: 实现具体逻辑
|
||||||
|
|
||||||
|
print("任务完成")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
只输出 Python 代码,不要其他解释。"""
|
||||||
|
|
||||||
|
CODE_GENERATION_USER = """执行计划:
|
||||||
|
{execution_plan}
|
||||||
|
|
||||||
|
用户原始需求:{user_input}
|
||||||
|
|
||||||
|
请生成 Python 代码。"""
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 安全审查 Prompt
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
SAFETY_REVIEW_SYSTEM = """你是一个代码安全审查员。检查代码是否符合安全规范。
|
||||||
|
|
||||||
|
检查项:
|
||||||
|
1. 是否只操作 workspace 目录
|
||||||
|
2. 是否有网络请求代码
|
||||||
|
3. 是否有危险的文件删除操作
|
||||||
|
4. 是否有执行外部命令的代码
|
||||||
|
5. 代码逻辑是否与用户需求一致
|
||||||
|
|
||||||
|
输出JSON格式:
|
||||||
|
{"pass": true或false, "reason": "中文审查结论"}"""
|
||||||
|
|
||||||
|
SAFETY_REVIEW_USER = """用户需求:{user_input}
|
||||||
|
|
||||||
|
执行计划:
|
||||||
|
{execution_plan}
|
||||||
|
|
||||||
|
待审查代码:
|
||||||
|
```python
|
||||||
|
{code}
|
||||||
|
```
|
||||||
|
|
||||||
|
请进行安全审查。"""
|
||||||
|
|
||||||
518
main.py
Normal file
518
main.py
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
"""
|
||||||
|
LocalAgent - Windows 本地 AI 执行助手 (MVP)
|
||||||
|
|
||||||
|
========================================
|
||||||
|
配置说明
|
||||||
|
========================================
|
||||||
|
1. 复制 .env.example 为 .env
|
||||||
|
2. 在 .env 中填入你的 SiliconFlow API Key:
|
||||||
|
LLM_API_KEY=sk-xxxxx
|
||||||
|
|
||||||
|
========================================
|
||||||
|
运行方式
|
||||||
|
========================================
|
||||||
|
方式一:使用 Anaconda
|
||||||
|
conda create -n localagent python=3.10
|
||||||
|
conda activate localagent
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
方式二:直接运行(需已安装依赖)
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
========================================
|
||||||
|
测试方法
|
||||||
|
========================================
|
||||||
|
1. 对话测试:输入 "今天天气怎么样" → 应识别为 chat
|
||||||
|
2. 执行测试:
|
||||||
|
- 将测试文件放入 workspace/input 目录
|
||||||
|
- 输入 "把这些文件复制一份" → 应识别为 execution
|
||||||
|
- 确认执行后,检查 workspace/output 目录
|
||||||
|
|
||||||
|
========================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
|
||||||
|
# 确保项目根目录在 Python 路径中
|
||||||
|
PROJECT_ROOT = Path(__file__).parent
|
||||||
|
ENV_PATH = PROJECT_ROOT / ".env"
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
# 在导入其他模块之前先加载环境变量
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
|
from llm.client import get_client, LLMClientError
|
||||||
|
from llm.prompts import (
|
||||||
|
EXECUTION_PLAN_SYSTEM, EXECUTION_PLAN_USER,
|
||||||
|
CODE_GENERATION_SYSTEM, CODE_GENERATION_USER
|
||||||
|
)
|
||||||
|
from intent.classifier import classify_intent, IntentResult
|
||||||
|
from intent.labels import CHAT, EXECUTION
|
||||||
|
from safety.rule_checker import check_code_safety
|
||||||
|
from safety.llm_reviewer import review_code_safety
|
||||||
|
from executor.sandbox_runner import SandboxRunner, ExecutionResult
|
||||||
|
from ui.chat_view import ChatView
|
||||||
|
from ui.task_guide_view import TaskGuideView
|
||||||
|
|
||||||
|
|
||||||
|
class LocalAgentApp:
|
||||||
|
"""
|
||||||
|
LocalAgent 主应用
|
||||||
|
|
||||||
|
职责:
|
||||||
|
1. 管理 UI 状态切换
|
||||||
|
2. 协调各模块工作流程
|
||||||
|
3. 处理用户交互
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.workspace = PROJECT_ROOT / "workspace"
|
||||||
|
self.runner = SandboxRunner(str(self.workspace))
|
||||||
|
|
||||||
|
# 当前任务状态
|
||||||
|
self.current_task: Optional[dict] = None
|
||||||
|
|
||||||
|
# 线程通信队列
|
||||||
|
self.result_queue = queue.Queue()
|
||||||
|
|
||||||
|
# 初始化 UI
|
||||||
|
self._init_ui()
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
"""初始化 UI"""
|
||||||
|
self.root = tk.Tk()
|
||||||
|
self.root.title("LocalAgent - 本地 AI 助手")
|
||||||
|
self.root.geometry("800x700")
|
||||||
|
self.root.configure(bg='#1e1e1e')
|
||||||
|
|
||||||
|
# 设置窗口图标(如果有的话)
|
||||||
|
try:
|
||||||
|
self.root.iconbitmap(PROJECT_ROOT / "icon.ico")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 主容器
|
||||||
|
self.main_container = tk.Frame(self.root, bg='#1e1e1e')
|
||||||
|
self.main_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 聊天视图
|
||||||
|
self.chat_view = ChatView(self.main_container, self._on_user_input)
|
||||||
|
|
||||||
|
# 任务引导视图(初始隐藏)
|
||||||
|
self.task_view: Optional[TaskGuideView] = None
|
||||||
|
|
||||||
|
# 定期检查后台任务结果
|
||||||
|
self._check_queue()
|
||||||
|
|
||||||
|
def _check_queue(self):
|
||||||
|
"""检查后台任务队列"""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
callback, args = self.result_queue.get_nowait()
|
||||||
|
callback(*args)
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 每 100ms 检查一次
|
||||||
|
self.root.after(100, self._check_queue)
|
||||||
|
|
||||||
|
def _run_in_thread(self, func, callback, *args):
|
||||||
|
"""在后台线程运行函数,完成后回调"""
|
||||||
|
def wrapper():
|
||||||
|
try:
|
||||||
|
result = func(*args)
|
||||||
|
self.result_queue.put((callback, (result, None)))
|
||||||
|
except Exception as e:
|
||||||
|
self.result_queue.put((callback, (None, e)))
|
||||||
|
|
||||||
|
thread = threading.Thread(target=wrapper, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _on_user_input(self, user_input: str):
|
||||||
|
"""处理用户输入"""
|
||||||
|
# 显示用户消息
|
||||||
|
self.chat_view.add_message(user_input, 'user')
|
||||||
|
self.chat_view.set_input_enabled(False)
|
||||||
|
self.chat_view.add_message("正在分析您的需求...", 'system')
|
||||||
|
|
||||||
|
# 在后台线程进行意图识别
|
||||||
|
self._run_in_thread(
|
||||||
|
classify_intent,
|
||||||
|
lambda result, error: self._on_intent_result(user_input, result, error),
|
||||||
|
user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_intent_result(self, user_input: str, intent_result: Optional[IntentResult], error: Optional[Exception]):
|
||||||
|
"""意图识别完成回调"""
|
||||||
|
if error:
|
||||||
|
self.chat_view.add_message(f"意图识别失败: {str(error)}", 'error')
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if intent_result.label == CHAT:
|
||||||
|
# 对话模式
|
||||||
|
self._handle_chat(user_input, intent_result)
|
||||||
|
else:
|
||||||
|
# 执行模式
|
||||||
|
self._handle_execution(user_input, intent_result)
|
||||||
|
|
||||||
|
def _handle_chat(self, user_input: str, intent_result: IntentResult):
|
||||||
|
"""处理对话任务"""
|
||||||
|
self.chat_view.add_message(
|
||||||
|
f"识别为对话模式 (原因: {intent_result.reason})",
|
||||||
|
'system'
|
||||||
|
)
|
||||||
|
self.chat_view.add_message("正在生成回复...", 'system')
|
||||||
|
|
||||||
|
# 在后台线程调用 LLM
|
||||||
|
def do_chat():
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
return client.chat(
|
||||||
|
messages=[{"role": "user", "content": user_input}],
|
||||||
|
model=model,
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=2048
|
||||||
|
)
|
||||||
|
|
||||||
|
self._run_in_thread(
|
||||||
|
do_chat,
|
||||||
|
self._on_chat_result
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_chat_result(self, response: Optional[str], error: Optional[Exception]):
|
||||||
|
"""对话完成回调"""
|
||||||
|
if error:
|
||||||
|
self.chat_view.add_message(f"对话失败: {str(error)}", 'error')
|
||||||
|
else:
|
||||||
|
self.chat_view.add_message(response, 'assistant')
|
||||||
|
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
|
||||||
|
def _handle_execution(self, user_input: str, intent_result: IntentResult):
|
||||||
|
"""处理执行任务"""
|
||||||
|
self.chat_view.add_message(
|
||||||
|
f"识别为执行任务 (置信度: {intent_result.confidence:.0%})\n原因: {intent_result.reason}",
|
||||||
|
'system'
|
||||||
|
)
|
||||||
|
self.chat_view.add_message("正在生成执行计划...", 'system')
|
||||||
|
|
||||||
|
# 保存用户输入和意图结果
|
||||||
|
self.current_task = {
|
||||||
|
'user_input': user_input,
|
||||||
|
'intent_result': intent_result
|
||||||
|
}
|
||||||
|
|
||||||
|
# 在后台线程生成执行计划
|
||||||
|
self._run_in_thread(
|
||||||
|
self._generate_execution_plan,
|
||||||
|
self._on_plan_generated,
|
||||||
|
user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_plan_generated(self, plan: Optional[str], error: Optional[Exception]):
|
||||||
|
"""执行计划生成完成回调"""
|
||||||
|
if error:
|
||||||
|
self.chat_view.add_message(f"生成执行计划失败: {str(error)}", 'error')
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_task['execution_plan'] = plan
|
||||||
|
self.chat_view.add_message("正在生成执行代码...", 'system')
|
||||||
|
|
||||||
|
# 在后台线程生成代码
|
||||||
|
self._run_in_thread(
|
||||||
|
self._generate_code,
|
||||||
|
self._on_code_generated,
|
||||||
|
self.current_task['user_input'],
|
||||||
|
plan
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_code_generated(self, code: Optional[str], error: Optional[Exception]):
|
||||||
|
"""代码生成完成回调"""
|
||||||
|
if error:
|
||||||
|
self.chat_view.add_message(f"生成代码失败: {str(error)}", 'error')
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.current_task['code'] = code
|
||||||
|
self.chat_view.add_message("正在进行安全检查...", 'system')
|
||||||
|
|
||||||
|
# 硬规则检查(同步,很快)
|
||||||
|
rule_result = check_code_safety(code)
|
||||||
|
if not rule_result.passed:
|
||||||
|
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
|
||||||
|
|
||||||
|
# 在后台线程进行 LLM 安全审查
|
||||||
|
self._run_in_thread(
|
||||||
|
review_code_safety,
|
||||||
|
self._on_safety_reviewed,
|
||||||
|
self.current_task['user_input'],
|
||||||
|
self.current_task['execution_plan'],
|
||||||
|
code
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_safety_reviewed(self, review_result, error: Optional[Exception]):
|
||||||
|
"""安全审查完成回调"""
|
||||||
|
if error:
|
||||||
|
self.chat_view.add_message(f"安全审查失败: {str(error)}", 'error')
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
return
|
||||||
|
|
||||||
|
if not review_result.passed:
|
||||||
|
self.chat_view.add_message(
|
||||||
|
f"安全审查未通过: {review_result.reason}",
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.chat_view.add_message("安全检查通过,请确认执行", 'system')
|
||||||
|
|
||||||
|
# 显示任务引导视图
|
||||||
|
self._show_task_guide()
|
||||||
|
|
||||||
|
def _generate_execution_plan(self, user_input: str) -> str:
|
||||||
|
"""生成执行计划"""
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
response = client.chat(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": EXECUTION_PLAN_SYSTEM},
|
||||||
|
{"role": "user", "content": EXECUTION_PLAN_USER.format(user_input=user_input)}
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=1024
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _generate_code(self, user_input: str, execution_plan: str) -> str:
|
||||||
|
"""生成执行代码"""
|
||||||
|
client = get_client()
|
||||||
|
model = os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
response = client.chat(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": CODE_GENERATION_SYSTEM},
|
||||||
|
{"role": "user", "content": CODE_GENERATION_USER.format(
|
||||||
|
user_input=user_input,
|
||||||
|
execution_plan=execution_plan
|
||||||
|
)}
|
||||||
|
],
|
||||||
|
model=model,
|
||||||
|
temperature=0.2,
|
||||||
|
max_tokens=2048
|
||||||
|
)
|
||||||
|
|
||||||
|
# 提取代码块
|
||||||
|
code = self._extract_code(response)
|
||||||
|
return code
|
||||||
|
|
||||||
|
def _extract_code(self, response: str) -> str:
|
||||||
|
"""从 LLM 响应中提取代码"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
# 尝试提取 ```python ... ``` 代码块
|
||||||
|
pattern = r'```python\s*(.*?)\s*```'
|
||||||
|
matches = re.findall(pattern, response, re.DOTALL)
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
# 尝试提取 ``` ... ``` 代码块
|
||||||
|
pattern = r'```\s*(.*?)\s*```'
|
||||||
|
matches = re.findall(pattern, response, re.DOTALL)
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
# 如果没有代码块,返回原始响应
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _show_task_guide(self):
|
||||||
|
"""显示任务引导视图"""
|
||||||
|
if not self.current_task:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 隐藏聊天视图
|
||||||
|
self.chat_view.get_frame().pack_forget()
|
||||||
|
|
||||||
|
# 创建任务引导视图
|
||||||
|
self.task_view = TaskGuideView(
|
||||||
|
self.main_container,
|
||||||
|
on_execute=self._on_execute_task,
|
||||||
|
on_cancel=self._on_cancel_task,
|
||||||
|
workspace_path=self.workspace
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置内容
|
||||||
|
self.task_view.set_intent_result(
|
||||||
|
self.current_task['intent_result'].reason,
|
||||||
|
self.current_task['intent_result'].confidence
|
||||||
|
)
|
||||||
|
self.task_view.set_execution_plan(self.current_task['execution_plan'])
|
||||||
|
|
||||||
|
# 显示
|
||||||
|
self.task_view.show()
|
||||||
|
|
||||||
|
def _on_execute_task(self):
|
||||||
|
"""执行任务"""
|
||||||
|
if not self.current_task:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.task_view.set_buttons_enabled(False)
|
||||||
|
|
||||||
|
# 在后台线程执行
|
||||||
|
def do_execute():
|
||||||
|
return self.runner.execute(self.current_task['code'])
|
||||||
|
|
||||||
|
self._run_in_thread(
|
||||||
|
do_execute,
|
||||||
|
self._on_execution_complete
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_execution_complete(self, result: Optional[ExecutionResult], error: Optional[Exception]):
|
||||||
|
"""执行完成回调"""
|
||||||
|
if error:
|
||||||
|
messagebox.showerror("执行错误", f"执行失败: {str(error)}")
|
||||||
|
else:
|
||||||
|
self._show_execution_result(result)
|
||||||
|
# 刷新输出文件列表
|
||||||
|
if self.task_view:
|
||||||
|
self.task_view.refresh_output()
|
||||||
|
|
||||||
|
self._back_to_chat()
|
||||||
|
|
||||||
|
def _show_execution_result(self, result: ExecutionResult):
|
||||||
|
"""显示执行结果"""
|
||||||
|
if result.success:
|
||||||
|
status = "执行成功"
|
||||||
|
else:
|
||||||
|
status = "执行失败"
|
||||||
|
|
||||||
|
message = f"""{status}
|
||||||
|
|
||||||
|
任务 ID: {result.task_id}
|
||||||
|
耗时: {result.duration_ms} ms
|
||||||
|
日志文件: {result.log_path}
|
||||||
|
|
||||||
|
输出:
|
||||||
|
{result.stdout if result.stdout else '(无输出)'}
|
||||||
|
|
||||||
|
{f'错误信息: {result.stderr}' if result.stderr else ''}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
messagebox.showinfo("执行结果", message)
|
||||||
|
# 打开 output 目录
|
||||||
|
os.startfile(str(self.workspace / "output"))
|
||||||
|
else:
|
||||||
|
messagebox.showerror("执行结果", message)
|
||||||
|
|
||||||
|
def _on_cancel_task(self):
|
||||||
|
"""取消任务"""
|
||||||
|
self.current_task = None
|
||||||
|
self._back_to_chat()
|
||||||
|
|
||||||
|
def _back_to_chat(self):
|
||||||
|
"""返回聊天视图"""
|
||||||
|
if self.task_view:
|
||||||
|
self.task_view.hide()
|
||||||
|
self.task_view = None
|
||||||
|
|
||||||
|
self.chat_view.get_frame().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
self.chat_view.set_input_enabled(True)
|
||||||
|
self.current_task = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""运行应用"""
|
||||||
|
self.root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
def check_environment():
|
||||||
|
"""检查运行环境"""
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
|
||||||
|
api_key = os.getenv("LLM_API_KEY")
|
||||||
|
|
||||||
|
if not api_key or api_key == "your_api_key_here":
|
||||||
|
print("=" * 50)
|
||||||
|
print("错误: 未配置 LLM API Key")
|
||||||
|
print("=" * 50)
|
||||||
|
print()
|
||||||
|
print("请按以下步骤配置:")
|
||||||
|
print("1. 复制 .env.example 为 .env")
|
||||||
|
print("2. 在 .env 中设置 LLM_API_KEY=你的API密钥")
|
||||||
|
print()
|
||||||
|
print("获取 API Key: https://siliconflow.cn")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 显示 GUI 错误提示
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
messagebox.showerror(
|
||||||
|
"配置错误",
|
||||||
|
"未配置 LLM API Key\n\n"
|
||||||
|
"请按以下步骤配置:\n"
|
||||||
|
"1. 复制 .env.example 为 .env\n"
|
||||||
|
"2. 在 .env 中设置 LLM_API_KEY=你的API密钥\n\n"
|
||||||
|
"获取 API Key: https://siliconflow.cn"
|
||||||
|
)
|
||||||
|
root.destroy()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主入口"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("LocalAgent - Windows 本地 AI 执行助手")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 检查环境
|
||||||
|
if not check_environment():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 创建工作目录
|
||||||
|
workspace = PROJECT_ROOT / "workspace"
|
||||||
|
(workspace / "input").mkdir(parents=True, exist_ok=True)
|
||||||
|
(workspace / "output").mkdir(parents=True, exist_ok=True)
|
||||||
|
(workspace / "logs").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"工作目录: {workspace}")
|
||||||
|
print(f"输入目录: {workspace / 'input'}")
|
||||||
|
print(f"输出目录: {workspace / 'output'}")
|
||||||
|
print(f"日志目录: {workspace / 'logs'}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
app = LocalAgentApp()
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# LocalAgent MVP 依赖
|
||||||
|
# 使用 Anaconda 创建虚拟环境后安装:
|
||||||
|
# conda create -n localagent python=3.10
|
||||||
|
# conda activate localagent
|
||||||
|
# pip install -r requirements.txt
|
||||||
|
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
requests>=2.31.0
|
||||||
|
|
||||||
2
safety/__init__.py
Normal file
2
safety/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 安全检查模块
|
||||||
|
|
||||||
BIN
safety/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
safety/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
safety/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
safety/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
safety/__pycache__/llm_reviewer.cpython-310.pyc
Normal file
BIN
safety/__pycache__/llm_reviewer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
safety/__pycache__/llm_reviewer.cpython-313.pyc
Normal file
BIN
safety/__pycache__/llm_reviewer.cpython-313.pyc
Normal file
Binary file not shown.
BIN
safety/__pycache__/rule_checker.cpython-310.pyc
Normal file
BIN
safety/__pycache__/rule_checker.cpython-310.pyc
Normal file
Binary file not shown.
BIN
safety/__pycache__/rule_checker.cpython-313.pyc
Normal file
BIN
safety/__pycache__/rule_checker.cpython-313.pyc
Normal file
Binary file not shown.
132
safety/llm_reviewer.py
Normal file
132
safety/llm_reviewer.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"""
|
||||||
|
LLM 软规则审查器
|
||||||
|
使用 LLM 进行代码安全审查
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from llm.client import get_client, LLMClientError, ENV_PATH
|
||||||
|
from llm.prompts import SAFETY_REVIEW_SYSTEM, SAFETY_REVIEW_USER
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LLMReviewResult:
|
||||||
|
"""LLM 审查结果"""
|
||||||
|
passed: bool
|
||||||
|
reason: str
|
||||||
|
raw_response: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LLMReviewer:
|
||||||
|
"""
|
||||||
|
LLM 安全审查器
|
||||||
|
|
||||||
|
使用大模型对代码进行语义级别的安全审查
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
load_dotenv(ENV_PATH)
|
||||||
|
self.model_name = os.getenv("GENERATION_MODEL_NAME")
|
||||||
|
|
||||||
|
def review(
|
||||||
|
self,
|
||||||
|
user_input: str,
|
||||||
|
execution_plan: str,
|
||||||
|
code: str
|
||||||
|
) -> LLMReviewResult:
|
||||||
|
"""
|
||||||
|
审查代码安全性
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_input: 用户原始需求
|
||||||
|
execution_plan: 执行计划
|
||||||
|
code: 待审查的代码
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LLMReviewResult: 审查结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = get_client()
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": SAFETY_REVIEW_SYSTEM},
|
||||||
|
{"role": "user", "content": SAFETY_REVIEW_USER.format(
|
||||||
|
user_input=user_input,
|
||||||
|
execution_plan=execution_plan,
|
||||||
|
code=code
|
||||||
|
)}
|
||||||
|
]
|
||||||
|
|
||||||
|
response = client.chat(
|
||||||
|
messages=messages,
|
||||||
|
model=self.model_name,
|
||||||
|
temperature=0.1,
|
||||||
|
max_tokens=512
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._parse_response(response)
|
||||||
|
|
||||||
|
except LLMClientError as e:
|
||||||
|
# LLM 调用失败,保守起见判定为不通过
|
||||||
|
return LLMReviewResult(
|
||||||
|
passed=False,
|
||||||
|
reason=f"安全审查失败({str(e)}),出于安全考虑拒绝执行"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return LLMReviewResult(
|
||||||
|
passed=False,
|
||||||
|
reason=f"安全审查异常({str(e)}),出于安全考虑拒绝执行"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_response(self, response: str) -> LLMReviewResult:
|
||||||
|
"""解析 LLM 响应"""
|
||||||
|
try:
|
||||||
|
# 提取 JSON
|
||||||
|
json_str = self._extract_json(response)
|
||||||
|
data = json.loads(json_str)
|
||||||
|
|
||||||
|
passed = data.get("pass", False)
|
||||||
|
reason = data.get("reason", "未提供原因")
|
||||||
|
|
||||||
|
# 确保 passed 是布尔值
|
||||||
|
if isinstance(passed, str):
|
||||||
|
passed = passed.lower() in ('true', 'yes', '1', 'pass')
|
||||||
|
|
||||||
|
return LLMReviewResult(
|
||||||
|
passed=bool(passed),
|
||||||
|
reason=reason,
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, ValueError, TypeError):
|
||||||
|
# 解析失败,保守判定
|
||||||
|
return LLMReviewResult(
|
||||||
|
passed=False,
|
||||||
|
reason=f"审查结果解析失败,出于安全考虑拒绝执行",
|
||||||
|
raw_response=response
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_json(self, text: str) -> str:
|
||||||
|
"""从文本中提取 JSON"""
|
||||||
|
start = text.find('{')
|
||||||
|
end = text.rfind('}')
|
||||||
|
|
||||||
|
if start != -1 and end != -1 and end > start:
|
||||||
|
return text[start:end + 1]
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def review_code_safety(
|
||||||
|
user_input: str,
|
||||||
|
execution_plan: str,
|
||||||
|
code: str
|
||||||
|
) -> LLMReviewResult:
|
||||||
|
"""便捷函数:审查代码安全性"""
|
||||||
|
reviewer = LLMReviewer()
|
||||||
|
return reviewer.review(user_input, execution_plan, code)
|
||||||
|
|
||||||
208
safety/rule_checker.py
Normal file
208
safety/rule_checker.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
硬规则安全检查器
|
||||||
|
静态扫描执行代码,检测危险操作
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import ast
|
||||||
|
from typing import List, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RuleCheckResult:
|
||||||
|
"""规则检查结果"""
|
||||||
|
passed: bool
|
||||||
|
violations: List[str] # 违规项列表
|
||||||
|
|
||||||
|
|
||||||
|
class RuleChecker:
|
||||||
|
"""
|
||||||
|
硬规则检查器
|
||||||
|
|
||||||
|
静态扫描代码,检测以下危险操作:
|
||||||
|
1. 网络请求: requests, socket, urllib, http.client
|
||||||
|
2. 危险文件操作: os.remove, shutil.rmtree, os.unlink
|
||||||
|
3. 执行外部命令: subprocess, os.system, os.popen
|
||||||
|
4. 访问非 workspace 路径
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 禁止导入的模块
|
||||||
|
FORBIDDEN_IMPORTS = {
|
||||||
|
'requests',
|
||||||
|
'socket',
|
||||||
|
'urllib',
|
||||||
|
'urllib.request',
|
||||||
|
'urllib.parse',
|
||||||
|
'urllib.error',
|
||||||
|
'http.client',
|
||||||
|
'httplib',
|
||||||
|
'ftplib',
|
||||||
|
'smtplib',
|
||||||
|
'telnetlib',
|
||||||
|
'subprocess',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 禁止调用的函数(模块.函数 或 单独函数名)
|
||||||
|
FORBIDDEN_CALLS = {
|
||||||
|
'os.remove',
|
||||||
|
'os.unlink',
|
||||||
|
'os.rmdir',
|
||||||
|
'os.removedirs',
|
||||||
|
'os.system',
|
||||||
|
'os.popen',
|
||||||
|
'os.spawn',
|
||||||
|
'os.spawnl',
|
||||||
|
'os.spawnle',
|
||||||
|
'os.spawnlp',
|
||||||
|
'os.spawnlpe',
|
||||||
|
'os.spawnv',
|
||||||
|
'os.spawnve',
|
||||||
|
'os.spawnvp',
|
||||||
|
'os.spawnvpe',
|
||||||
|
'os.exec',
|
||||||
|
'os.execl',
|
||||||
|
'os.execle',
|
||||||
|
'os.execlp',
|
||||||
|
'os.execlpe',
|
||||||
|
'os.execv',
|
||||||
|
'os.execve',
|
||||||
|
'os.execvp',
|
||||||
|
'os.execvpe',
|
||||||
|
'shutil.rmtree',
|
||||||
|
'shutil.move', # move 可能导致原文件丢失
|
||||||
|
'eval',
|
||||||
|
'exec',
|
||||||
|
'compile',
|
||||||
|
'__import__',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 危险路径模式(正则)
|
||||||
|
DANGEROUS_PATH_PATTERNS = [
|
||||||
|
r'[A-Za-z]:\\', # Windows 绝对路径
|
||||||
|
r'\\\\', # UNC 路径
|
||||||
|
r'/etc/',
|
||||||
|
r'/usr/',
|
||||||
|
r'/bin/',
|
||||||
|
r'/home/',
|
||||||
|
r'/root/',
|
||||||
|
r'\.\./', # 父目录遍历
|
||||||
|
r'\.\.', # 父目录
|
||||||
|
]
|
||||||
|
|
||||||
|
def check(self, code: str) -> RuleCheckResult:
|
||||||
|
"""
|
||||||
|
检查代码是否符合安全规则
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Python 代码字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RuleCheckResult: 检查结果
|
||||||
|
"""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
# 1. 检查禁止的导入
|
||||||
|
import_violations = self._check_imports(code)
|
||||||
|
violations.extend(import_violations)
|
||||||
|
|
||||||
|
# 2. 检查禁止的函数调用
|
||||||
|
call_violations = self._check_calls(code)
|
||||||
|
violations.extend(call_violations)
|
||||||
|
|
||||||
|
# 3. 检查危险路径
|
||||||
|
path_violations = self._check_paths(code)
|
||||||
|
violations.extend(path_violations)
|
||||||
|
|
||||||
|
return RuleCheckResult(
|
||||||
|
passed=len(violations) == 0,
|
||||||
|
violations=violations
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_imports(self, code: str) -> List[str]:
|
||||||
|
"""检查禁止的导入"""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ast.parse(code)
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
module_name = alias.name.split('.')[0]
|
||||||
|
if alias.name in self.FORBIDDEN_IMPORTS or module_name in self.FORBIDDEN_IMPORTS:
|
||||||
|
violations.append(f"禁止导入模块: {alias.name}")
|
||||||
|
|
||||||
|
elif isinstance(node, ast.ImportFrom):
|
||||||
|
if node.module:
|
||||||
|
module_name = node.module.split('.')[0]
|
||||||
|
if node.module in self.FORBIDDEN_IMPORTS or module_name in self.FORBIDDEN_IMPORTS:
|
||||||
|
violations.append(f"禁止导入模块: {node.module}")
|
||||||
|
|
||||||
|
except SyntaxError:
|
||||||
|
# 如果代码有语法错误,使用正则匹配
|
||||||
|
for module in self.FORBIDDEN_IMPORTS:
|
||||||
|
pattern = rf'\bimport\s+{re.escape(module)}\b|\bfrom\s+{re.escape(module)}\b'
|
||||||
|
if re.search(pattern, code):
|
||||||
|
violations.append(f"禁止导入模块: {module}")
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
def _check_calls(self, code: str) -> List[str]:
|
||||||
|
"""检查禁止的函数调用"""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ast.parse(code)
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Call):
|
||||||
|
call_name = self._get_call_name(node)
|
||||||
|
if call_name in self.FORBIDDEN_CALLS:
|
||||||
|
violations.append(f"禁止调用函数: {call_name}")
|
||||||
|
|
||||||
|
except SyntaxError:
|
||||||
|
# 如果代码有语法错误,使用正则匹配
|
||||||
|
for func in self.FORBIDDEN_CALLS:
|
||||||
|
pattern = rf'\b{re.escape(func)}\s*\('
|
||||||
|
if re.search(pattern, code):
|
||||||
|
violations.append(f"禁止调用函数: {func}")
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
def _get_call_name(self, node: ast.Call) -> str:
|
||||||
|
"""获取函数调用的完整名称"""
|
||||||
|
if isinstance(node.func, ast.Name):
|
||||||
|
return node.func.id
|
||||||
|
elif isinstance(node.func, ast.Attribute):
|
||||||
|
parts = []
|
||||||
|
current = node.func
|
||||||
|
while isinstance(current, ast.Attribute):
|
||||||
|
parts.append(current.attr)
|
||||||
|
current = current.value
|
||||||
|
if isinstance(current, ast.Name):
|
||||||
|
parts.append(current.id)
|
||||||
|
return '.'.join(reversed(parts))
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _check_paths(self, code: str) -> List[str]:
|
||||||
|
"""检查危险路径访问"""
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
for pattern in self.DANGEROUS_PATH_PATTERNS:
|
||||||
|
matches = re.findall(pattern, code, re.IGNORECASE)
|
||||||
|
if matches:
|
||||||
|
# 排除 workspace 相关的合法路径
|
||||||
|
for match in matches:
|
||||||
|
if 'workspace' not in code[max(0, code.find(match)-50):code.find(match)+50].lower():
|
||||||
|
violations.append(f"检测到可疑路径模式: {match}")
|
||||||
|
break
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def check_code_safety(code: str) -> RuleCheckResult:
|
||||||
|
"""便捷函数:检查代码安全性"""
|
||||||
|
checker = RuleChecker()
|
||||||
|
return checker.check(code)
|
||||||
|
|
||||||
2
ui/__init__.py
Normal file
2
ui/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# UI 模块
|
||||||
|
|
||||||
BIN
ui/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
ui/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ui/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/chat_view.cpython-310.pyc
Normal file
BIN
ui/__pycache__/chat_view.cpython-310.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/chat_view.cpython-313.pyc
Normal file
BIN
ui/__pycache__/chat_view.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/task_guide_view.cpython-310.pyc
Normal file
BIN
ui/__pycache__/task_guide_view.cpython-310.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/task_guide_view.cpython-313.pyc
Normal file
BIN
ui/__pycache__/task_guide_view.cpython-313.pyc
Normal file
Binary file not shown.
164
ui/chat_view.py
Normal file
164
ui/chat_view.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"""
|
||||||
|
聊天视图组件
|
||||||
|
处理普通对话的 UI 展示
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import scrolledtext
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ChatView:
|
||||||
|
"""
|
||||||
|
聊天视图
|
||||||
|
|
||||||
|
包含:
|
||||||
|
- 消息显示区域
|
||||||
|
- 输入框
|
||||||
|
- 发送按钮
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: tk.Widget,
|
||||||
|
on_send: Callable[[str], None]
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化聊天视图
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: 父容器
|
||||||
|
on_send: 发送消息回调函数
|
||||||
|
"""
|
||||||
|
self.parent = parent
|
||||||
|
self.on_send = on_send
|
||||||
|
|
||||||
|
self._create_widgets()
|
||||||
|
|
||||||
|
def _create_widgets(self):
|
||||||
|
"""创建 UI 组件"""
|
||||||
|
# 主框架
|
||||||
|
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
|
||||||
|
self.frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title_label = tk.Label(
|
||||||
|
self.frame,
|
||||||
|
text="LocalAgent - 本地 AI 助手",
|
||||||
|
font=('Microsoft YaHei UI', 16, 'bold'),
|
||||||
|
fg='#61dafb',
|
||||||
|
bg='#1e1e1e'
|
||||||
|
)
|
||||||
|
title_label.pack(pady=(0, 10))
|
||||||
|
|
||||||
|
# 消息显示区域
|
||||||
|
self.message_area = scrolledtext.ScrolledText(
|
||||||
|
self.frame,
|
||||||
|
wrap=tk.WORD,
|
||||||
|
font=('Microsoft YaHei UI', 11),
|
||||||
|
bg='#2d2d2d',
|
||||||
|
fg='#d4d4d4',
|
||||||
|
insertbackground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=10,
|
||||||
|
pady=10,
|
||||||
|
state=tk.DISABLED
|
||||||
|
)
|
||||||
|
self.message_area.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
|
|
||||||
|
# 配置消息标签样式
|
||||||
|
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('system', foreground='#ffb74d', font=('Microsoft YaHei UI', 10, 'italic'))
|
||||||
|
self.message_area.tag_configure('error', foreground='#ef5350', font=('Microsoft YaHei UI', 10))
|
||||||
|
|
||||||
|
# 输入区域框架
|
||||||
|
input_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
|
input_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
# 输入框
|
||||||
|
self.input_entry = tk.Entry(
|
||||||
|
input_frame,
|
||||||
|
font=('Microsoft YaHei UI', 12),
|
||||||
|
bg='#3c3c3c',
|
||||||
|
fg='#ffffff',
|
||||||
|
insertbackground='white',
|
||||||
|
relief=tk.FLAT
|
||||||
|
)
|
||||||
|
self.input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=8, padx=(0, 10))
|
||||||
|
self.input_entry.bind('<Return>', self._on_enter_pressed)
|
||||||
|
|
||||||
|
# 发送按钮
|
||||||
|
self.send_button = tk.Button(
|
||||||
|
input_frame,
|
||||||
|
text="发送",
|
||||||
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||||
|
bg='#0078d4',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#106ebe',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=20,
|
||||||
|
pady=5,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._on_send_clicked
|
||||||
|
)
|
||||||
|
self.send_button.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# 显示欢迎消息
|
||||||
|
welcome_msg = (
|
||||||
|
"欢迎使用 LocalAgent!\n"
|
||||||
|
"- 输入问题进行对话\n"
|
||||||
|
"- 输入文件处理需求(如\"复制文件\"、\"整理图片\")将触发执行模式"
|
||||||
|
)
|
||||||
|
self.add_message(welcome_msg, 'system')
|
||||||
|
|
||||||
|
def _on_enter_pressed(self, event):
|
||||||
|
"""回车键处理"""
|
||||||
|
self._on_send_clicked()
|
||||||
|
|
||||||
|
def _on_send_clicked(self):
|
||||||
|
"""发送按钮点击处理"""
|
||||||
|
text = self.input_entry.get().strip()
|
||||||
|
if text:
|
||||||
|
self.input_entry.delete(0, tk.END)
|
||||||
|
self.on_send(text)
|
||||||
|
|
||||||
|
def add_message(self, message: str, tag: str = 'assistant'):
|
||||||
|
"""
|
||||||
|
添加消息到显示区域
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 消息内容
|
||||||
|
tag: 消息类型 (user/assistant/system/error)
|
||||||
|
"""
|
||||||
|
self.message_area.config(state=tk.NORMAL)
|
||||||
|
|
||||||
|
# 添加前缀
|
||||||
|
prefix_map = {
|
||||||
|
'user': '[你] ',
|
||||||
|
'assistant': '[助手] ',
|
||||||
|
'system': '[系统] ',
|
||||||
|
'error': '[错误] '
|
||||||
|
}
|
||||||
|
prefix = prefix_map.get(tag, '')
|
||||||
|
|
||||||
|
self.message_area.insert(tk.END, "\n" + prefix + message + "\n", tag)
|
||||||
|
self.message_area.see(tk.END)
|
||||||
|
self.message_area.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def clear_messages(self):
|
||||||
|
"""清空消息区域"""
|
||||||
|
self.message_area.config(state=tk.NORMAL)
|
||||||
|
self.message_area.delete(1.0, tk.END)
|
||||||
|
self.message_area.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def set_input_enabled(self, enabled: bool):
|
||||||
|
"""设置输入区域是否可用"""
|
||||||
|
state = tk.NORMAL if enabled else tk.DISABLED
|
||||||
|
self.input_entry.config(state=state)
|
||||||
|
self.send_button.config(state=state)
|
||||||
|
|
||||||
|
def get_frame(self) -> tk.Frame:
|
||||||
|
"""获取主框架"""
|
||||||
|
return self.frame
|
||||||
524
ui/task_guide_view.py
Normal file
524
ui/task_guide_view.py
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
"""
|
||||||
|
任务引导视图组件
|
||||||
|
执行任务的引导式 UI - 支持文件拖拽和 Markdown 渲染
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import scrolledtext, messagebox
|
||||||
|
from tkinter import ttk
|
||||||
|
from typing import Callable, Optional, List
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownText(tk.Text):
|
||||||
|
"""支持简单 Markdown 渲染的 Text 组件"""
|
||||||
|
|
||||||
|
def __init__(self, parent, **kwargs):
|
||||||
|
super().__init__(parent, **kwargs)
|
||||||
|
self._setup_tags()
|
||||||
|
|
||||||
|
def _setup_tags(self):
|
||||||
|
"""设置 Markdown 样式标签"""
|
||||||
|
# 标题样式
|
||||||
|
self.tag_configure('h1', font=('Microsoft YaHei UI', 14, 'bold'), foreground='#ffd54f', spacing1=10, spacing3=5)
|
||||||
|
self.tag_configure('h2', font=('Microsoft YaHei UI', 12, 'bold'), foreground='#81c784', spacing1=8, spacing3=4)
|
||||||
|
self.tag_configure('h3', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#4fc3f7', spacing1=6, spacing3=3)
|
||||||
|
|
||||||
|
# 列表样式
|
||||||
|
self.tag_configure('bullet', foreground='#ce93d8', lmargin1=20, lmargin2=35)
|
||||||
|
self.tag_configure('numbered', foreground='#ce93d8', lmargin1=20, lmargin2=35)
|
||||||
|
|
||||||
|
# 代码样式
|
||||||
|
self.tag_configure('code', font=('Consolas', 10), background='#3c3c3c', foreground='#f8f8f2')
|
||||||
|
|
||||||
|
# 粗体和斜体
|
||||||
|
self.tag_configure('bold', font=('Microsoft YaHei UI', 10, 'bold'))
|
||||||
|
self.tag_configure('italic', font=('Microsoft YaHei UI', 10, 'italic'))
|
||||||
|
|
||||||
|
# 普通文本
|
||||||
|
self.tag_configure('normal', font=('Microsoft YaHei UI', 10), foreground='#d4d4d4')
|
||||||
|
|
||||||
|
def set_markdown(self, text: str):
|
||||||
|
"""设置 Markdown 内容并渲染"""
|
||||||
|
self.config(state=tk.NORMAL)
|
||||||
|
self.delete(1.0, tk.END)
|
||||||
|
|
||||||
|
lines = text.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
self._render_line(line)
|
||||||
|
|
||||||
|
self.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def _render_line(self, line: str):
|
||||||
|
"""渲染单行 Markdown"""
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
if stripped.startswith('### '):
|
||||||
|
self.insert(tk.END, stripped[4:] + '\n', 'h3')
|
||||||
|
elif stripped.startswith('## '):
|
||||||
|
self.insert(tk.END, stripped[3:] + '\n', 'h2')
|
||||||
|
elif stripped.startswith('# '):
|
||||||
|
self.insert(tk.END, stripped[2:] + '\n', 'h1')
|
||||||
|
# 无序列表
|
||||||
|
elif stripped.startswith('- ') or stripped.startswith('* '):
|
||||||
|
self.insert(tk.END, ' • ' + stripped[2:] + '\n', 'bullet')
|
||||||
|
# 有序列表
|
||||||
|
elif re.match(r'^\d+\.\s', stripped):
|
||||||
|
match = re.match(r'^(\d+\.)\s(.*)$', stripped)
|
||||||
|
if match:
|
||||||
|
self.insert(tk.END, ' ' + match.group(1) + ' ' + match.group(2) + '\n', 'numbered')
|
||||||
|
# 普通文本
|
||||||
|
else:
|
||||||
|
# 处理行内格式
|
||||||
|
self._render_inline(line + '\n')
|
||||||
|
|
||||||
|
def _render_inline(self, text: str):
|
||||||
|
"""渲染行内 Markdown(粗体、斜体、代码)"""
|
||||||
|
# 简化处理:直接插入普通文本
|
||||||
|
# 完整实现需要更复杂的解析
|
||||||
|
self.insert(tk.END, text, 'normal')
|
||||||
|
|
||||||
|
|
||||||
|
class DropZone(tk.Frame):
|
||||||
|
"""文件拖拽区域"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent,
|
||||||
|
title: str,
|
||||||
|
target_dir: Path,
|
||||||
|
is_input: bool = True,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(parent, **kwargs)
|
||||||
|
self.target_dir = target_dir
|
||||||
|
self.is_input = is_input
|
||||||
|
self.configure(bg='#2d2d2d', relief=tk.GROOVE, bd=2)
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
self.target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self._create_widgets(title)
|
||||||
|
self._setup_drag_drop()
|
||||||
|
self._refresh_file_list()
|
||||||
|
|
||||||
|
def _create_widgets(self, title: str):
|
||||||
|
"""创建组件"""
|
||||||
|
# 标题
|
||||||
|
title_frame = tk.Frame(self, bg='#2d2d2d')
|
||||||
|
title_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
tk.Label(
|
||||||
|
title_frame,
|
||||||
|
text=title,
|
||||||
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||||
|
fg='#4fc3f7' if self.is_input else '#81c784',
|
||||||
|
bg='#2d2d2d'
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 打开文件夹按钮
|
||||||
|
open_btn = tk.Button(
|
||||||
|
title_frame,
|
||||||
|
text="📂",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#424242',
|
||||||
|
fg='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._open_folder
|
||||||
|
)
|
||||||
|
open_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# 刷新按钮
|
||||||
|
refresh_btn = tk.Button(
|
||||||
|
title_frame,
|
||||||
|
text="🔄",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#424242',
|
||||||
|
fg='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._refresh_file_list
|
||||||
|
)
|
||||||
|
refresh_btn.pack(side=tk.RIGHT, padx=(0, 5))
|
||||||
|
|
||||||
|
# 拖拽提示区域
|
||||||
|
self.drop_label = tk.Label(
|
||||||
|
self,
|
||||||
|
text="将文件拖拽到此处\n或点击 📂 打开文件夹",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
fg='#888888',
|
||||||
|
bg='#3c3c3c',
|
||||||
|
relief=tk.SUNKEN,
|
||||||
|
padx=20,
|
||||||
|
pady=15
|
||||||
|
)
|
||||||
|
self.drop_label.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 文件列表
|
||||||
|
self.file_listbox = tk.Listbox(
|
||||||
|
self,
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
bg='#2d2d2d',
|
||||||
|
fg='#d4d4d4',
|
||||||
|
selectbackground='#0078d4',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
height=4
|
||||||
|
)
|
||||||
|
self.file_listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 文件计数
|
||||||
|
self.count_label = tk.Label(
|
||||||
|
self,
|
||||||
|
text="0 个文件",
|
||||||
|
font=('Microsoft YaHei UI', 9),
|
||||||
|
fg='#888888',
|
||||||
|
bg='#2d2d2d'
|
||||||
|
)
|
||||||
|
self.count_label.pack(pady=(0, 5))
|
||||||
|
|
||||||
|
def _setup_drag_drop(self):
|
||||||
|
"""设置拖拽功能(Windows 需要 windnd 库,这里用简化方案)"""
|
||||||
|
# 由于 Tkinter 原生不支持文件拖拽,使用点击打开文件夹的方式
|
||||||
|
self.drop_label.bind('<Button-1>', lambda e: self._open_folder())
|
||||||
|
|
||||||
|
def _open_folder(self):
|
||||||
|
"""打开目标文件夹"""
|
||||||
|
import os
|
||||||
|
os.startfile(str(self.target_dir))
|
||||||
|
|
||||||
|
def _refresh_file_list(self):
|
||||||
|
"""刷新文件列表"""
|
||||||
|
self.file_listbox.delete(0, tk.END)
|
||||||
|
|
||||||
|
files = list(self.target_dir.glob('*'))
|
||||||
|
files = [f for f in files if f.is_file()]
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
self.file_listbox.insert(tk.END, f.name)
|
||||||
|
|
||||||
|
self.count_label.config(text=f"{len(files)} 个文件")
|
||||||
|
|
||||||
|
def get_files(self) -> List[Path]:
|
||||||
|
"""获取目录中的文件列表"""
|
||||||
|
files = list(self.target_dir.glob('*'))
|
||||||
|
return [f for f in files if f.is_file()]
|
||||||
|
|
||||||
|
def clear_files(self):
|
||||||
|
"""清空目录中的文件"""
|
||||||
|
for f in self.target_dir.glob('*'):
|
||||||
|
if f.is_file():
|
||||||
|
f.unlink()
|
||||||
|
self._refresh_file_list()
|
||||||
|
|
||||||
|
|
||||||
|
class TaskGuideView:
|
||||||
|
"""
|
||||||
|
任务引导视图
|
||||||
|
|
||||||
|
小白引导式界面,包含:
|
||||||
|
- 意图识别结果
|
||||||
|
- 文件拖拽区域(输入/输出)
|
||||||
|
- 执行计划展示(Markdown 渲染)
|
||||||
|
- 风险提示
|
||||||
|
- 执行按钮
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: tk.Widget,
|
||||||
|
on_execute: Callable[[], None],
|
||||||
|
on_cancel: Callable[[], None],
|
||||||
|
workspace_path: Optional[Path] = None
|
||||||
|
):
|
||||||
|
self.parent = parent
|
||||||
|
self.on_execute = on_execute
|
||||||
|
self.on_cancel = on_cancel
|
||||||
|
|
||||||
|
if workspace_path:
|
||||||
|
self.workspace = workspace_path
|
||||||
|
else:
|
||||||
|
self.workspace = Path(__file__).parent.parent / "workspace"
|
||||||
|
|
||||||
|
self.input_dir = self.workspace / "input"
|
||||||
|
self.output_dir = self.workspace / "output"
|
||||||
|
|
||||||
|
self._create_widgets()
|
||||||
|
|
||||||
|
def _create_widgets(self):
|
||||||
|
"""创建 UI 组件"""
|
||||||
|
# 主框架
|
||||||
|
self.frame = tk.Frame(self.parent, bg='#1e1e1e')
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title_label = tk.Label(
|
||||||
|
self.frame,
|
||||||
|
text="执行任务确认",
|
||||||
|
font=('Microsoft YaHei UI', 16, 'bold'),
|
||||||
|
fg='#ffd54f',
|
||||||
|
bg='#1e1e1e'
|
||||||
|
)
|
||||||
|
title_label.pack(pady=(10, 15))
|
||||||
|
|
||||||
|
# 上半部分:文件区域
|
||||||
|
file_section = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
|
file_section.pack(fill=tk.X, padx=10, pady=5)
|
||||||
|
|
||||||
|
# 输入文件区域
|
||||||
|
input_frame = tk.LabelFrame(
|
||||||
|
file_section,
|
||||||
|
text=" 📥 输入文件 ",
|
||||||
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||||
|
fg='#4fc3f7',
|
||||||
|
bg='#1e1e1e',
|
||||||
|
relief=tk.GROOVE
|
||||||
|
)
|
||||||
|
input_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
|
||||||
|
|
||||||
|
self.input_zone = DropZone(
|
||||||
|
input_frame,
|
||||||
|
title="待处理文件",
|
||||||
|
target_dir=self.input_dir,
|
||||||
|
is_input=True,
|
||||||
|
bg='#2d2d2d'
|
||||||
|
)
|
||||||
|
self.input_zone.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 箭头
|
||||||
|
arrow_frame = tk.Frame(file_section, bg='#1e1e1e')
|
||||||
|
arrow_frame.pack(side=tk.LEFT, padx=10)
|
||||||
|
tk.Label(
|
||||||
|
arrow_frame,
|
||||||
|
text="➡️",
|
||||||
|
font=('Microsoft YaHei UI', 20),
|
||||||
|
fg='#ffd54f',
|
||||||
|
bg='#1e1e1e'
|
||||||
|
).pack(pady=30)
|
||||||
|
|
||||||
|
# 输出文件区域
|
||||||
|
output_frame = tk.LabelFrame(
|
||||||
|
file_section,
|
||||||
|
text=" 📤 输出文件 ",
|
||||||
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||||
|
fg='#81c784',
|
||||||
|
bg='#1e1e1e',
|
||||||
|
relief=tk.GROOVE
|
||||||
|
)
|
||||||
|
output_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
||||||
|
|
||||||
|
self.output_zone = DropZone(
|
||||||
|
output_frame,
|
||||||
|
title="处理结果",
|
||||||
|
target_dir=self.output_dir,
|
||||||
|
is_input=False,
|
||||||
|
bg='#2d2d2d'
|
||||||
|
)
|
||||||
|
self.output_zone.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 意图识别结果区域
|
||||||
|
self._create_intent_section()
|
||||||
|
|
||||||
|
# 执行计划区域(Markdown)
|
||||||
|
self._create_plan_section()
|
||||||
|
|
||||||
|
# 风险提示区域
|
||||||
|
self._create_risk_section()
|
||||||
|
|
||||||
|
# 按钮区域
|
||||||
|
self._create_button_section()
|
||||||
|
|
||||||
|
def _create_intent_section(self):
|
||||||
|
"""创建意图识别结果区域"""
|
||||||
|
section = tk.LabelFrame(
|
||||||
|
self.frame,
|
||||||
|
text=" 🎯 意图识别 ",
|
||||||
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||||
|
fg='#81c784',
|
||||||
|
bg='#1e1e1e',
|
||||||
|
relief=tk.GROOVE
|
||||||
|
)
|
||||||
|
section.pack(fill=tk.X, padx=10, pady=5)
|
||||||
|
|
||||||
|
self.intent_label = tk.Label(
|
||||||
|
section,
|
||||||
|
text="",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
fg='#d4d4d4',
|
||||||
|
bg='#1e1e1e',
|
||||||
|
wraplength=650,
|
||||||
|
justify=tk.LEFT
|
||||||
|
)
|
||||||
|
self.intent_label.pack(padx=10, pady=8, anchor=tk.W)
|
||||||
|
|
||||||
|
def _create_plan_section(self):
|
||||||
|
"""创建执行计划区域(支持 Markdown)"""
|
||||||
|
section = tk.LabelFrame(
|
||||||
|
self.frame,
|
||||||
|
text=" 📄 执行计划 ",
|
||||||
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||||
|
fg='#ce93d8',
|
||||||
|
bg='#1e1e1e',
|
||||||
|
relief=tk.GROOVE
|
||||||
|
)
|
||||||
|
section.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||||
|
|
||||||
|
# 使用 Markdown 渲染的 Text
|
||||||
|
self.plan_text = MarkdownText(
|
||||||
|
section,
|
||||||
|
wrap=tk.WORD,
|
||||||
|
bg='#2d2d2d',
|
||||||
|
fg='#d4d4d4',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
height=8,
|
||||||
|
padx=10,
|
||||||
|
pady=10
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加滚动条
|
||||||
|
scrollbar = ttk.Scrollbar(section, orient=tk.VERTICAL, command=self.plan_text.yview)
|
||||||
|
self.plan_text.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.plan_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
def _create_risk_section(self):
|
||||||
|
"""创建风险提示区域"""
|
||||||
|
section = tk.LabelFrame(
|
||||||
|
self.frame,
|
||||||
|
text=" ⚠️ 安全提示 ",
|
||||||
|
font=('Microsoft YaHei UI', 11, 'bold'),
|
||||||
|
fg='#ffb74d',
|
||||||
|
bg='#1e1e1e',
|
||||||
|
relief=tk.GROOVE
|
||||||
|
)
|
||||||
|
section.pack(fill=tk.X, padx=10, pady=5)
|
||||||
|
|
||||||
|
self.risk_label = tk.Label(
|
||||||
|
section,
|
||||||
|
text="• 所有操作仅在 workspace 目录内进行\n• 原始文件不会被修改或删除\n• 执行代码已通过安全检查",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
fg='#d4d4d4',
|
||||||
|
bg='#1e1e1e',
|
||||||
|
justify=tk.LEFT
|
||||||
|
)
|
||||||
|
self.risk_label.pack(padx=10, pady=8, anchor=tk.W)
|
||||||
|
|
||||||
|
def _create_button_section(self):
|
||||||
|
"""创建按钮区域"""
|
||||||
|
button_frame = tk.Frame(self.frame, bg='#1e1e1e')
|
||||||
|
button_frame.pack(fill=tk.X, padx=10, pady=15)
|
||||||
|
|
||||||
|
# 刷新文件列表按钮
|
||||||
|
self.refresh_btn = tk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="🔄 刷新文件",
|
||||||
|
font=('Microsoft YaHei UI', 10),
|
||||||
|
bg='#424242',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#616161',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=15,
|
||||||
|
pady=5,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._refresh_all
|
||||||
|
)
|
||||||
|
self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
|
||||||
|
# 取消按钮
|
||||||
|
self.cancel_btn = tk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="取消",
|
||||||
|
font=('Microsoft YaHei UI', 11),
|
||||||
|
bg='#616161',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#757575',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=20,
|
||||||
|
pady=5,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self.on_cancel
|
||||||
|
)
|
||||||
|
self.cancel_btn.pack(side=tk.RIGHT, padx=(10, 0))
|
||||||
|
|
||||||
|
# 执行按钮
|
||||||
|
self.execute_btn = tk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="🚀 开始执行",
|
||||||
|
font=('Microsoft YaHei UI', 12, 'bold'),
|
||||||
|
bg='#4caf50',
|
||||||
|
fg='white',
|
||||||
|
activebackground='#66bb6a',
|
||||||
|
activeforeground='white',
|
||||||
|
relief=tk.FLAT,
|
||||||
|
padx=30,
|
||||||
|
pady=8,
|
||||||
|
cursor='hand2',
|
||||||
|
command=self._on_execute_clicked
|
||||||
|
)
|
||||||
|
self.execute_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def _refresh_all(self):
|
||||||
|
"""刷新所有文件列表"""
|
||||||
|
self.input_zone._refresh_file_list()
|
||||||
|
self.output_zone._refresh_file_list()
|
||||||
|
|
||||||
|
def _on_execute_clicked(self):
|
||||||
|
"""执行按钮点击"""
|
||||||
|
# 刷新文件列表
|
||||||
|
self.input_zone._refresh_file_list()
|
||||||
|
|
||||||
|
# 检查 input 目录是否有文件
|
||||||
|
files = self.input_zone.get_files()
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
result = messagebox.askyesno(
|
||||||
|
"确认执行",
|
||||||
|
"输入文件夹为空,确定要继续执行吗?",
|
||||||
|
icon='warning'
|
||||||
|
)
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_execute()
|
||||||
|
|
||||||
|
def set_intent_result(self, reason: str, confidence: float):
|
||||||
|
"""设置意图识别结果"""
|
||||||
|
self.intent_label.config(
|
||||||
|
text=f"识别结果: 执行任务 (置信度: {confidence:.0%})\n原因: {reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_execution_plan(self, plan: str):
|
||||||
|
"""设置执行计划(Markdown 格式)"""
|
||||||
|
self.plan_text.set_markdown(plan)
|
||||||
|
|
||||||
|
def set_risk_info(self, info: str):
|
||||||
|
"""设置风险提示"""
|
||||||
|
self.risk_label.config(text=info)
|
||||||
|
|
||||||
|
def set_buttons_enabled(self, enabled: bool):
|
||||||
|
"""设置按钮是否可用"""
|
||||||
|
state = tk.NORMAL if enabled else tk.DISABLED
|
||||||
|
self.execute_btn.config(state=state)
|
||||||
|
self.cancel_btn.config(state=state)
|
||||||
|
|
||||||
|
def refresh_output(self):
|
||||||
|
"""刷新输出文件列表"""
|
||||||
|
self.output_zone._refresh_file_list()
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""显示视图"""
|
||||||
|
self.frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
self._refresh_all()
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
"""隐藏视图"""
|
||||||
|
self.frame.pack_forget()
|
||||||
|
|
||||||
|
def get_frame(self) -> tk.Frame:
|
||||||
|
"""获取主框架"""
|
||||||
|
return self.frame
|
||||||
Reference in New Issue
Block a user