1134 lines
47 KiB
Python
1134 lines
47 KiB
Python
"""
|
||||
|
|
DeepSeek Agent - 分离式聊天界面
|
|||
|
|
|
|||
|
|
用户在这个终端界面与 Agent 对话
|
|||
|
|
Agent 后台与 DeepSeek 交互,自动处理 ACTION 标记
|
|||
|
|
|
|||
|
|
使用方式:
|
|||
|
|
1. 启动此脚本
|
|||
|
|
2. 在终端直接输入任务
|
|||
|
|
3. Agent 自动协调 DeepSeek 完成多轮操作
|
|||
|
|
4. 结果返回给用户
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import subprocess
|
|||
|
|
import time
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import threading
|
|||
|
|
import queue
|
|||
|
|
from dataclasses import dataclass, field
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Optional, Any, Callable
|
|||
|
|
from enum import Enum, auto
|
|||
|
|
|
|||
|
|
from playwright.async_api import async_playwright, Page, BrowserContext
|
|||
|
|
|
|||
|
|
DEEPSEEK_URL = "https://chat.deepseek.com"
|
|||
|
|
|
|||
|
|
|
|||
|
|
SOLO_SYSTEM_PROMPT = """【系统指令】你是一个命令执行代理。你的唯一职责是根据用户指令执行本地命令,并将执行结果返回给用户。
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【系统架构 - 重要!】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
本系统采用【双终端架构】:
|
|||
|
|
• 【DP对话终端】- 你当前所在的终端,用于和我(系统)对话
|
|||
|
|
• 【命令执行终端】- 独立的shell进程,专门用于执行所有命令
|
|||
|
|
|
|||
|
|
关键机制:
|
|||
|
|
1. command/powershell 命令会发送到【命令执行终端】执行
|
|||
|
|
2. 命令执行是【非阻塞】的 - 命令扔进去立即返回,不等待完成
|
|||
|
|
3. 命令执行终端的输出会【持续累积】,随时可以获取
|
|||
|
|
4. 你需要用 get_terminal_output 主动查询命令执行结果
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【可用命令】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
• command <命令> - 在命令执行终端执行命令(非阻塞)
|
|||
|
|
• powershell <命令> - 在命令执行终端执行PowerShell(非阻塞)
|
|||
|
|
• get_terminal_output - 获取命令执行终端的所有输出
|
|||
|
|
• send_input <内容> - 向命令执行终端发送输入(用于交互式程序)
|
|||
|
|
• clear_terminal - 清空命令执行终端的输出记录
|
|||
|
|
• read_file <路径> - 读取文件内容
|
|||
|
|
• write_file <路径> - 写入文件(配合 <<<CONTENT 块使用)
|
|||
|
|
• list_dir <路径> - 列出目录内容
|
|||
|
|
• search <关键词> - 搜索文件内容
|
|||
|
|
• complete - 标记任务完成
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【执行流程 - 必须遵守】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
第1步:发送 command 执行命令
|
|||
|
|
↓
|
|||
|
|
第2步:等待系统返回(命令已发送,不等待完成)
|
|||
|
|
↓
|
|||
|
|
第3步:发送 get_terminal_output 查询输出
|
|||
|
|
↓
|
|||
|
|
第4步:根据输出判断:
|
|||
|
|
- 命令已完成 → 继续下一步
|
|||
|
|
- 命令在运行中 → 等待后再次 get_terminal_output
|
|||
|
|
- 命令在等待输入 → 用 send_input 发送输入
|
|||
|
|
- 命令出错 → 处理错误
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【响应格式 - 必须严格遵守】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
每次响应必须包含两部分:
|
|||
|
|
|
|||
|
|
1. ACTION 块(如需执行命令):
|
|||
|
|
<<<ACTION
|
|||
|
|
{"type": "命令类型", "cmd": "具体命令", "explanation": "简要说明"}
|
|||
|
|
ACTION>>>
|
|||
|
|
|
|||
|
|
2. 结束标记(必须!):
|
|||
|
|
<<<END_OF_MESSAGE>>>
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【重要规则】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
1. 【禁止主动执行】初始化阶段严禁执行任何命令,必须等待用户明确指令
|
|||
|
|
2. 【必须加结束标记】每次回复的最后一行必须是 <<<END_OF_MESSAGE>>>
|
|||
|
|
3. 【非阻塞执行】command/powershell 不等待命令完成,立即返回
|
|||
|
|
4. 【必须查询输出】执行命令后,必须用 get_terminal_output 查看结果
|
|||
|
|
5. 【交互式程序】如果程序等待输入,用 send_input 发送输入,不要假设程序已结束
|
|||
|
|
6. 【文件编码】write_file 配合 <<<CONTENT 块使用,内容不经过 JSON 转义
|
|||
|
|
7. 【代码围栏 - 必须!】CONTENT 里的代码必须用 ```python 和 ``` 包裹,否则缩进会丢失
|
|||
|
|
8. 【一轮一指令】每次只输出一个 ACTION,等待系统返回后再继续
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【示例 - 执行命令并获取结果】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
用户:运行一个Python脚本
|
|||
|
|
|
|||
|
|
你(第1轮):
|
|||
|
|
<<<ACTION
|
|||
|
|
{"type": "command", "cmd": "python test.py", "explanation": "运行测试脚本"}
|
|||
|
|
ACTION>>>
|
|||
|
|
<<<END_OF_MESSAGE>>>
|
|||
|
|
|
|||
|
|
系统返回:命令已发送...
|
|||
|
|
|
|||
|
|
你(第2轮):
|
|||
|
|
<<<ACTION
|
|||
|
|
{"type": "get_terminal_output", "cmd": "", "explanation": "查看命令输出"}
|
|||
|
|
ACTION>>>
|
|||
|
|
<<<END_OF_MESSAGE>>>
|
|||
|
|
|
|||
|
|
系统返回:命令终端输出...(显示脚本输出)
|
|||
|
|
|
|||
|
|
你(第3轮,如果程序在等待输入):
|
|||
|
|
<<<ACTION
|
|||
|
|
{"type": "send_input", "cmd": "yes", "explanation": "发送确认输入"}
|
|||
|
|
ACTION>>>
|
|||
|
|
<<<END_OF_MESSAGE>>>
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【示例 - 写入文件(必须用代码围栏)】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
<<<ACTION
|
|||
|
|
{"type": "write_file", "path": "test.py", "explanation": "创建测试文件"}
|
|||
|
|
ACTION>>>
|
|||
|
|
<<<CONTENT
|
|||
|
|
```python
|
|||
|
|
def hello():
|
|||
|
|
print("Hello")
|
|||
|
|
return 42
|
|||
|
|
```
|
|||
|
|
CONTENT>>>
|
|||
|
|
<<<END_OF_MESSAGE>>>
|
|||
|
|
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
【示例 - 简单命令】
|
|||
|
|
═══════════════════════════════════════════════════════
|
|||
|
|
用户:查看当前目录
|
|||
|
|
|
|||
|
|
你:
|
|||
|
|
<<<ACTION
|
|||
|
|
{"type": "list_dir", "cmd": ".", "explanation": "列出当前目录"}
|
|||
|
|
ACTION>>>
|
|||
|
|
<<<END_OF_MESSAGE>>>
|
|||
|
|
|
|||
|
|
任务完成时:
|
|||
|
|
<<<ACTION
|
|||
|
|
{"type": "complete", "explanation": "任务已完成"}
|
|||
|
|
ACTION>>>
|
|||
|
|
<<<END_OF_MESSAGE>>>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ExecutionEngine:
|
|||
|
|
"""本地命令执行引擎 - 双终端架构"""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
# 命令执行终端(独立 shell,非阻塞执行命令)
|
|||
|
|
self.cmd_terminal_proc: Optional[asyncio.subprocess.Process] = None
|
|||
|
|
self.cmd_terminal_output: str = ""
|
|||
|
|
self.cmd_terminal_lock = asyncio.Lock()
|
|||
|
|
self.last_cmd: str = ""
|
|||
|
|
|
|||
|
|
async def _ensure_cmd_terminal(self) -> asyncio.subprocess.Process:
|
|||
|
|
"""确保命令执行终端已启动"""
|
|||
|
|
if self.cmd_terminal_proc is None or self.cmd_terminal_proc.returncode is not None:
|
|||
|
|
if sys.platform == "win32":
|
|||
|
|
self.cmd_terminal_proc = await asyncio.create_subprocess_exec(
|
|||
|
|
"powershell", "-NoLogo",
|
|||
|
|
stdout=asyncio.subprocess.PIPE,
|
|||
|
|
stderr=asyncio.subprocess.STDOUT,
|
|||
|
|
stdin=asyncio.subprocess.PIPE,
|
|||
|
|
creationflags=subprocess.CREATE_NO_WINDOW
|
|||
|
|
)
|
|||
|
|
# 启动后立即设置 UTF-8 编码
|
|||
|
|
await asyncio.sleep(0.2)
|
|||
|
|
self.cmd_terminal_proc.stdin.write(b"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n")
|
|||
|
|
await self.cmd_terminal_proc.stdin.drain()
|
|||
|
|
else:
|
|||
|
|
self.cmd_terminal_proc = await asyncio.create_subprocess_exec(
|
|||
|
|
"bash",
|
|||
|
|
stdout=asyncio.subprocess.PIPE,
|
|||
|
|
stderr=asyncio.subprocess.STDOUT,
|
|||
|
|
stdin=asyncio.subprocess.PIPE
|
|||
|
|
)
|
|||
|
|
# 启动后台任务持续读取输出
|
|||
|
|
asyncio.create_task(self._read_cmd_terminal_output())
|
|||
|
|
print(f"[系统] 命令执行终端已启动 (PID: {self.cmd_terminal_proc.pid})")
|
|||
|
|
return self.cmd_terminal_proc
|
|||
|
|
|
|||
|
|
async def _read_cmd_terminal_output(self):
|
|||
|
|
"""后台任务:持续读取命令终端的输出"""
|
|||
|
|
while self.cmd_terminal_proc and self.cmd_terminal_proc.returncode is None:
|
|||
|
|
try:
|
|||
|
|
line = await asyncio.wait_for(
|
|||
|
|
self.cmd_terminal_proc.stdout.readline(),
|
|||
|
|
timeout=0.1
|
|||
|
|
)
|
|||
|
|
if line:
|
|||
|
|
async with self.cmd_terminal_lock:
|
|||
|
|
decoded = line.decode('utf-8', errors='replace')
|
|||
|
|
self.cmd_terminal_output += decoded
|
|||
|
|
except asyncio.TimeoutError:
|
|||
|
|
continue
|
|||
|
|
except Exception:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
async def execute(self, action_type: str, command: str, working_dir: str = "", content: str = "", agent=None) -> dict:
|
|||
|
|
cwd = Path(working_dir).expanduser() if working_dir else Path.home()
|
|||
|
|
|
|||
|
|
handlers = {
|
|||
|
|
"command": self._exec_in_cmd_terminal,
|
|||
|
|
"powershell": self._exec_in_cmd_terminal,
|
|||
|
|
"read_file": self._read_file,
|
|||
|
|
"write_file": self._write_file,
|
|||
|
|
"list_dir": self._list_dir,
|
|||
|
|
"search": self._search,
|
|||
|
|
"get_terminal_output": self._get_terminal_output,
|
|||
|
|
"send_input": self._send_input_to_terminal,
|
|||
|
|
"clear_terminal": self._clear_terminal_output,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
handler = handlers.get(action_type)
|
|||
|
|
if not handler:
|
|||
|
|
return {"success": False, "output": f"Unknown action type: {action_type}"}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if action_type == "write_file":
|
|||
|
|
# write_file 特殊处理,传入 content 和 agent
|
|||
|
|
if asyncio.iscoroutinefunction(handler):
|
|||
|
|
result = await handler(command, cwd, content, agent)
|
|||
|
|
else:
|
|||
|
|
result = handler(command, cwd, content, agent)
|
|||
|
|
else:
|
|||
|
|
if asyncio.iscoroutinefunction(handler):
|
|||
|
|
result = await handler(command, cwd)
|
|||
|
|
else:
|
|||
|
|
result = handler(command, cwd)
|
|||
|
|
return {"success": True, "output": result}
|
|||
|
|
except Exception as e:
|
|||
|
|
return {"success": False, "output": f"Error: {e}"}
|
|||
|
|
|
|||
|
|
async def _exec_in_cmd_terminal(self, cmd: str, cwd: Path) -> str:
|
|||
|
|
"""在命令执行终端中执行命令(非阻塞,立即返回)"""
|
|||
|
|
proc = await self._ensure_cmd_terminal()
|
|||
|
|
|
|||
|
|
# 记录执行前的输出位置
|
|||
|
|
async with self.cmd_terminal_lock:
|
|||
|
|
output_before_len = len(self.cmd_terminal_output)
|
|||
|
|
|
|||
|
|
# 发送命令到命令终端
|
|||
|
|
self.last_cmd = cmd
|
|||
|
|
if sys.platform == "win32":
|
|||
|
|
# 设置 Python 环境变量强制 UTF-8 输出
|
|||
|
|
if cmd.strip().startswith("python"):
|
|||
|
|
full_cmd = f"cd '{cwd}'; $env:PYTHONIOENCODING='utf-8'; {cmd}\n"
|
|||
|
|
else:
|
|||
|
|
full_cmd = f"cd '{cwd}'; {cmd}\n"
|
|||
|
|
else:
|
|||
|
|
full_cmd = f"cd '{cwd}' && {cmd}\n"
|
|||
|
|
|
|||
|
|
proc.stdin.write(full_cmd.encode())
|
|||
|
|
await proc.stdin.drain()
|
|||
|
|
|
|||
|
|
# 等待一段时间让命令开始执行(不等待完成)
|
|||
|
|
await asyncio.sleep(0.5)
|
|||
|
|
|
|||
|
|
# 获取新产生的输出
|
|||
|
|
async with self.cmd_terminal_lock:
|
|||
|
|
current_output = self.cmd_terminal_output[output_before_len:]
|
|||
|
|
|
|||
|
|
return f"[已执行] {cmd}\n\n命令终端当前输出:\n```\n{current_output[-2000:] if len(current_output) > 2000 else current_output}\n```\n\n提示: 使用 'get_terminal_output' 查看最新输出"
|
|||
|
|
|
|||
|
|
async def _get_terminal_output(self, _: str, cwd: Path) -> str:
|
|||
|
|
"""获取命令执行终端的当前所有输出"""
|
|||
|
|
async with self.cmd_terminal_lock:
|
|||
|
|
output = self.cmd_terminal_output
|
|||
|
|
|
|||
|
|
if len(output) > 5000:
|
|||
|
|
return f"命令终端输出 (共 {len(output)} 字符,显示最后5000字符):\n```\n...\n{output[-5000:]}\n```"
|
|||
|
|
else:
|
|||
|
|
return f"命令终端输出 (共 {len(output)} 字符):\n```\n{output}\n```"
|
|||
|
|
|
|||
|
|
async def _send_input_to_terminal(self, input_text: str, cwd: Path) -> str:
|
|||
|
|
"""向命令执行终端发送输入"""
|
|||
|
|
if not self.cmd_terminal_proc or self.cmd_terminal_proc.returncode is not None:
|
|||
|
|
return "错误: 命令终端未启动"
|
|||
|
|
|
|||
|
|
self.cmd_terminal_proc.stdin.write(input_text.encode() + b'\n')
|
|||
|
|
await self.cmd_terminal_proc.stdin.drain()
|
|||
|
|
|
|||
|
|
# 等待一下让程序处理输入
|
|||
|
|
await asyncio.sleep(0.3)
|
|||
|
|
|
|||
|
|
# 获取新输出
|
|||
|
|
async with self.cmd_terminal_lock:
|
|||
|
|
output = self.cmd_terminal_output
|
|||
|
|
|
|||
|
|
return f"[已发送输入] {input_text}\n\n终端最新输出:\n```\n{output[-1500:] if len(output) > 1500 else output}\n```"
|
|||
|
|
|
|||
|
|
async def _clear_terminal_output(self, _: str, cwd: Path) -> str:
|
|||
|
|
"""清空命令终端输出记录"""
|
|||
|
|
async with self.cmd_terminal_lock:
|
|||
|
|
old_len = len(self.cmd_terminal_output)
|
|||
|
|
self.cmd_terminal_output = ""
|
|||
|
|
return f"已清空命令终端输出记录 (清除了 {old_len} 字符)"
|
|||
|
|
|
|||
|
|
def _read_file(self, path: str, cwd: Path) -> str:
|
|||
|
|
target = cwd / path if not Path(path).is_absolute() else Path(path)
|
|||
|
|
if not target.exists():
|
|||
|
|
return f"File not found: {target}"
|
|||
|
|
try:
|
|||
|
|
return target.read_text(encoding='utf-8', errors='replace')[:5000]
|
|||
|
|
except Exception as e:
|
|||
|
|
return f"Read error: {e}"
|
|||
|
|
|
|||
|
|
def _write_file(self, cmd: str, cwd: Path, content: str = "", agent=None) -> str:
|
|||
|
|
# 新格式: path 直接传入,content 从 action 对象获取
|
|||
|
|
# 兼容旧格式: path::content
|
|||
|
|
if "::" in cmd and not content:
|
|||
|
|
path, content = cmd.split("::", 1)
|
|||
|
|
else:
|
|||
|
|
path = cmd
|
|||
|
|
|
|||
|
|
if agent:
|
|||
|
|
agent.log("WRITE_FILE", f"Path: {path}, Raw content length: {len(content)}")
|
|||
|
|
agent.log("WRITE_FILE", f"Raw content preview: {content[:300].replace(chr(10), '\\n')}")
|
|||
|
|
|
|||
|
|
# 处理代码围栏:去掉 ``` 或 ```python 等标记
|
|||
|
|
content = content.strip()
|
|||
|
|
|
|||
|
|
# 更健壮的围栏处理:找到第一个 ``` 和最后一个 ```
|
|||
|
|
if content.startswith('```'):
|
|||
|
|
# 找到第一个换行符(跳过 ```python 这一行)
|
|||
|
|
first_newline = content.find('\n')
|
|||
|
|
if first_newline != -1:
|
|||
|
|
# 去掉开头的 ``` 或 ```python 行
|
|||
|
|
content = content[first_newline + 1:]
|
|||
|
|
|
|||
|
|
# 去掉结尾的 ```
|
|||
|
|
if content.rstrip().endswith('```'):
|
|||
|
|
last_backticks = content.rstrip().rfind('```')
|
|||
|
|
content = content[:last_backticks].rstrip()
|
|||
|
|
|
|||
|
|
if agent:
|
|||
|
|
agent.log("WRITE_FILE", f"After fence removal: {len(content)} chars")
|
|||
|
|
agent.log("WRITE_FILE", f"Content preview: {content[:300].replace(chr(10), '\\n')}")
|
|||
|
|
|
|||
|
|
# 检查是否还有残留的 ```
|
|||
|
|
if '```' in content:
|
|||
|
|
if agent:
|
|||
|
|
agent.log("WRITE_FILE", f"WARNING: Still contains ``` after removal!")
|
|||
|
|
|
|||
|
|
# 检查缩进
|
|||
|
|
lines_with_indent = [i for i, line in enumerate(content.split('\n')) if line.startswith(' ') or line.startswith('\t')]
|
|||
|
|
if agent:
|
|||
|
|
agent.log("WRITE_FILE", f"Lines with indentation: {lines_with_indent}")
|
|||
|
|
agent.log("WRITE_FILE", f"Final content: {content[:300].replace(chr(10), '\\n')}")
|
|||
|
|
|
|||
|
|
target = cwd / path if not Path(path).is_absolute() else Path(path)
|
|||
|
|
try:
|
|||
|
|
target.write_text(content, encoding='utf-8')
|
|||
|
|
result = f"Written to {target} ({len(content)} chars)"
|
|||
|
|
if agent:
|
|||
|
|
agent.log("WRITE_FILE", f"Success: {result}")
|
|||
|
|
return result
|
|||
|
|
except Exception as e:
|
|||
|
|
if agent:
|
|||
|
|
agent.log("WRITE_FILE", f"Error: {e}")
|
|||
|
|
return f"Write error: {e}"
|
|||
|
|
|
|||
|
|
def _list_dir(self, path: str, cwd: Path) -> str:
|
|||
|
|
target = cwd / path if not Path(path).is_absolute() else Path(path)
|
|||
|
|
if not target.exists():
|
|||
|
|
return f"Directory not found: {target}"
|
|||
|
|
try:
|
|||
|
|
items = []
|
|||
|
|
for item in target.iterdir():
|
|||
|
|
item_type = "📁" if item.is_dir() else "📄"
|
|||
|
|
size = ""
|
|||
|
|
if item.is_file():
|
|||
|
|
try:
|
|||
|
|
size_bytes = item.stat().st_size
|
|||
|
|
if size_bytes < 1024:
|
|||
|
|
size = f" ({size_bytes}B)"
|
|||
|
|
elif size_bytes < 1024*1024:
|
|||
|
|
size = f" ({size_bytes/1024:.1f}KB)"
|
|||
|
|
else:
|
|||
|
|
size = f" ({size_bytes/(1024*1024):.1f}MB)"
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
items.append(f"{item_type} {item.name}{size}")
|
|||
|
|
return "\n".join(items) if items else "(empty directory)"
|
|||
|
|
except Exception as e:
|
|||
|
|
return f"List error: {e}"
|
|||
|
|
|
|||
|
|
def _search(self, pattern: str, cwd: Path) -> str:
|
|||
|
|
results = []
|
|||
|
|
try:
|
|||
|
|
for item in cwd.rglob("*"):
|
|||
|
|
if item.is_file() and pattern.lower() in item.name.lower():
|
|||
|
|
results.append(str(item.relative_to(cwd)))
|
|||
|
|
if len(results) >= 20:
|
|||
|
|
results.append("... (more results)")
|
|||
|
|
break
|
|||
|
|
return "\n".join(results) if results else "No matches found"
|
|||
|
|
except Exception as e:
|
|||
|
|
return f"Search error: {e}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DeepSeekController:
|
|||
|
|
"""DeepSeek 页面控制器"""
|
|||
|
|
|
|||
|
|
def __init__(self, page: Page):
|
|||
|
|
self.page = page
|
|||
|
|
|
|||
|
|
async def send(self, message: str) -> bool:
|
|||
|
|
"""发送消息到 DeepSeek"""
|
|||
|
|
try:
|
|||
|
|
# 查找输入框
|
|||
|
|
input_selectors = [
|
|||
|
|
'textarea[placeholder*="输入"]',
|
|||
|
|
'textarea[class*="input"]',
|
|||
|
|
'[class*="input"] textarea',
|
|||
|
|
'div[contenteditable="true"]',
|
|||
|
|
'textarea',
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
input_box = None
|
|||
|
|
for selector in input_selectors:
|
|||
|
|
try:
|
|||
|
|
el = self.page.locator(selector).first
|
|||
|
|
if await el.count() > 0 and await el.is_visible():
|
|||
|
|
input_box = el
|
|||
|
|
break
|
|||
|
|
except:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if not input_box:
|
|||
|
|
print("[Error] 找不到输入框")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
await input_box.fill(message)
|
|||
|
|
await asyncio.sleep(0.3)
|
|||
|
|
|
|||
|
|
# 点击发送按钮
|
|||
|
|
send_btn = self.page.locator("button[class*='send']").last
|
|||
|
|
if await send_btn.count() > 0 and await send_btn.is_visible():
|
|||
|
|
await send_btn.click()
|
|||
|
|
else:
|
|||
|
|
await input_box.press("Enter")
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[Error] 发送失败: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
async def get_latest_ai_message(self, agent: 'SoloAgent' = None) -> Optional[str]:
|
|||
|
|
"""获取最新 AI 消息 - 适配 DeepSeek DOM 结构"""
|
|||
|
|
try:
|
|||
|
|
text = await self.page.evaluate("""
|
|||
|
|
() => {
|
|||
|
|
// 辅助函数:提取消息内容,正确处理代码块
|
|||
|
|
function extractMessageContent(msg) {
|
|||
|
|
// 找到 markdown 容器
|
|||
|
|
const markdownContent = msg.querySelector('[class*="markdown"], [class*="ds-markdown"]');
|
|||
|
|
if (!markdownContent) {
|
|||
|
|
return msg.textContent.trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let result = '';
|
|||
|
|
|
|||
|
|
// 遍历所有子元素
|
|||
|
|
const children = markdownContent.children;
|
|||
|
|
for (const child of children) {
|
|||
|
|
// 代码块容器
|
|||
|
|
if (child.tagName === 'DIV' && child.querySelector('pre')) {
|
|||
|
|
const pre = child.querySelector('pre');
|
|||
|
|
const code = pre.querySelector('code');
|
|||
|
|
// 只取 code 或 pre 的 textContent,忽略按钮等 UI
|
|||
|
|
const codeText = code ? code.textContent : pre.textContent;
|
|||
|
|
result += '\\n```python\\n' + codeText + '\\n```\\n';
|
|||
|
|
}
|
|||
|
|
// 普通段落
|
|||
|
|
else if (child.tagName === 'P') {
|
|||
|
|
result += child.textContent + '\\n';
|
|||
|
|
}
|
|||
|
|
// 其他元素直接取文本
|
|||
|
|
else {
|
|||
|
|
result += child.textContent + '\\n';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result.trim();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 找所有消息容器
|
|||
|
|
const messageContainers = [
|
|||
|
|
...document.querySelectorAll('[class*="ds-message"]'),
|
|||
|
|
...document.querySelectorAll('[class*="message"]'),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
if (messageContainers.length === 0) return null;
|
|||
|
|
|
|||
|
|
// 遍历消息,找最后一个包含结束标记的 AI 消息
|
|||
|
|
for (let i = messageContainers.length - 1; i >= 0; i--) {
|
|||
|
|
const msg = messageContainers[i];
|
|||
|
|
const content = extractMessageContent(msg);
|
|||
|
|
|
|||
|
|
// 找包含结束标记的消息
|
|||
|
|
if (content.includes('<<<END_OF_MESSAGE>>>')) {
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 兜底:返回最后一个消息
|
|||
|
|
const lastMsg = messageContainers[messageContainers.length - 1];
|
|||
|
|
return extractMessageContent(lastMsg);
|
|||
|
|
}
|
|||
|
|
""")
|
|||
|
|
|
|||
|
|
if agent and text:
|
|||
|
|
agent.log("MSG_EXTRACT", f"Extracted: {text[:300].replace(chr(10), '\\n')}")
|
|||
|
|
|
|||
|
|
return text
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[Error] 获取消息失败: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ChatInterface:
|
|||
|
|
"""用户聊天界面 - 在终端与用户交互"""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self.message_queue = queue.Queue()
|
|||
|
|
self.response_queue = queue.Queue()
|
|||
|
|
self.is_running = False
|
|||
|
|
self.task_in_progress = False
|
|||
|
|
|
|||
|
|
def print_banner(self):
|
|||
|
|
"""打印欢迎信息"""
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print(" [DeepSeek Agent] - 分离式聊天界面")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("\n[使用说明]:")
|
|||
|
|
print(" 1. 在此窗口直接输入任务")
|
|||
|
|
print(" 2. Agent 自动后台协调 DeepSeek 完成")
|
|||
|
|
print(" 3. 多轮操作完成后返回结果")
|
|||
|
|
print(" 4. DeepSeek 窗口保持自动化交互,无需手动操作")
|
|||
|
|
print("\n[示例任务]:")
|
|||
|
|
print(" - 查看桌面有什么文件")
|
|||
|
|
print(" - 统计当前目录的Python文件数量")
|
|||
|
|
print(" - 查找大于100MB的文件")
|
|||
|
|
print("\n[特殊命令]:")
|
|||
|
|
print(" /quit - 退出程序")
|
|||
|
|
print(" /clear - 清屏")
|
|||
|
|
print("=" * 60 + "\n")
|
|||
|
|
|
|||
|
|
def user_input_loop(self):
|
|||
|
|
"""用户输入循环 - 在单独线程运行"""
|
|||
|
|
while self.is_running:
|
|||
|
|
try:
|
|||
|
|
# 显示提示符
|
|||
|
|
if not self.task_in_progress:
|
|||
|
|
user_input = input("\n[你]: ")
|
|||
|
|
|
|||
|
|
if user_input.strip():
|
|||
|
|
if user_input.strip().lower() == '/quit':
|
|||
|
|
self.is_running = False
|
|||
|
|
break
|
|||
|
|
elif user_input.strip().lower() == '/clear':
|
|||
|
|
os.system('cls' if sys.platform == 'win32' else 'clear')
|
|||
|
|
self.print_banner()
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 将用户输入放入队列
|
|||
|
|
self.message_queue.put(user_input.strip())
|
|||
|
|
self.task_in_progress = True
|
|||
|
|
print("\n[正在处理...]")
|
|||
|
|
else:
|
|||
|
|
time.sleep(0.1)
|
|||
|
|
|
|||
|
|
except EOFError:
|
|||
|
|
break
|
|||
|
|
except KeyboardInterrupt:
|
|||
|
|
self.is_running = False
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
def display_result(self, result: str):
|
|||
|
|
"""显示任务结果"""
|
|||
|
|
print(f"\n[DeepSeek]: {result}\n")
|
|||
|
|
self.task_in_progress = False
|
|||
|
|
|
|||
|
|
def display_progress(self, msg: str):
|
|||
|
|
"""显示进度信息"""
|
|||
|
|
print(f" {msg}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
END_MARKER = "<<<END_OF_MESSAGE>>>"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class SoloAgent:
|
|||
|
|
"""主 Agent - 协调用户界面和 DeepSeek"""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self.page: Optional[Page] = None
|
|||
|
|
self.controller: Optional[DeepSeekController] = None
|
|||
|
|
self.engine = ExecutionEngine()
|
|||
|
|
self.chat = ChatInterface()
|
|||
|
|
self.last_ai_msg = ""
|
|||
|
|
self.log_file = Path("agent_debug.log")
|
|||
|
|
self._init_log()
|
|||
|
|
self._playwright = None
|
|||
|
|
self._browser = None
|
|||
|
|
self._context = None
|
|||
|
|
self._initialized = False
|
|||
|
|
|
|||
|
|
async def start(self):
|
|||
|
|
"""启动浏览器和 DeepSeek 连接"""
|
|||
|
|
if self._initialized:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self._playwright = await async_playwright().start()
|
|||
|
|
|
|||
|
|
# 使用持久化上下文,保存登录状态
|
|||
|
|
user_data_dir = Path.home() / ".deepseek_chat_profile"
|
|||
|
|
user_data_dir.mkdir(exist_ok=True)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 尝试启动持久化上下文
|
|||
|
|
self._context = await self._playwright.chromium.launch_persistent_context(
|
|||
|
|
str(user_data_dir),
|
|||
|
|
channel="chrome",
|
|||
|
|
headless=False,
|
|||
|
|
args=["--start-maximized"],
|
|||
|
|
no_viewport=True
|
|||
|
|
)
|
|||
|
|
print("[使用持久化会话,保留登录状态]")
|
|||
|
|
except Exception as e:
|
|||
|
|
# 如果失败,回退到普通模式
|
|||
|
|
print(f"[!] 持久化会话启动失败: {e}")
|
|||
|
|
print("[!] 使用普通模式(需要重新登录)")
|
|||
|
|
self._browser = await self._playwright.chromium.launch(
|
|||
|
|
channel="chrome",
|
|||
|
|
headless=False,
|
|||
|
|
args=["--start-maximized"]
|
|||
|
|
)
|
|||
|
|
self._context = await self._browser.new_context(no_viewport=True)
|
|||
|
|
|
|||
|
|
self.page = self._context.pages[0] if self._context.pages else await self._context.new_page()
|
|||
|
|
await self.page.goto(DEEPSEEK_URL)
|
|||
|
|
|
|||
|
|
print(f"[已打开 DeepSeek]: {DEEPSEEK_URL}")
|
|||
|
|
print("[!] 请先在浏览器中登录 DeepSeek 并创建新对话")
|
|||
|
|
|
|||
|
|
# 等待用户登录(阻塞等待)
|
|||
|
|
input("登录完成后按回车继续...")
|
|||
|
|
|
|||
|
|
self.controller = DeepSeekController(self.page)
|
|||
|
|
|
|||
|
|
# 发送系统提示词
|
|||
|
|
print("[正在初始化系统提示词]...")
|
|||
|
|
await self.controller.send(f"[系统初始化]\n\n{SOLO_SYSTEM_PROMPT}")
|
|||
|
|
print("[系统初始化完成]")
|
|||
|
|
|
|||
|
|
self._initialized = True
|
|||
|
|
|
|||
|
|
async def stop(self):
|
|||
|
|
"""停止浏览器"""
|
|||
|
|
if self._context:
|
|||
|
|
await self._context.close()
|
|||
|
|
self._context = None
|
|||
|
|
if self._browser:
|
|||
|
|
await self._browser.close()
|
|||
|
|
self._browser = None
|
|||
|
|
if self._playwright:
|
|||
|
|
await self._playwright.stop()
|
|||
|
|
self._playwright = None
|
|||
|
|
self._initialized = False
|
|||
|
|
self.page = None
|
|||
|
|
self.controller = None
|
|||
|
|
|
|||
|
|
async def send_message(self, message: str):
|
|||
|
|
"""发送消息给 DeepSeek"""
|
|||
|
|
if not self.controller:
|
|||
|
|
raise RuntimeError("Agent 未启动")
|
|||
|
|
await self.controller.send(f"[用户任务]\n{message}\n\n请开始执行。")
|
|||
|
|
self.last_ai_msg = ""
|
|||
|
|
|
|||
|
|
async def wait_for_response(self, timeout: int = 120) -> Optional[str]:
|
|||
|
|
"""等待 DeepSeek 响应"""
|
|||
|
|
if not self.controller:
|
|||
|
|
raise RuntimeError("Agent 未启动")
|
|||
|
|
|
|||
|
|
start_time = time.time()
|
|||
|
|
last_msg = self.last_ai_msg
|
|||
|
|
|
|||
|
|
while time.time() - start_time < timeout:
|
|||
|
|
current_msg = await self.controller.get_latest_ai_message(self)
|
|||
|
|
if current_msg and current_msg != last_msg:
|
|||
|
|
self.last_ai_msg = current_msg
|
|||
|
|
return current_msg
|
|||
|
|
await asyncio.sleep(0.5)
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
async def send_result(self, result: str):
|
|||
|
|
"""发送执行结果给 DeepSeek"""
|
|||
|
|
if not self.controller:
|
|||
|
|
raise RuntimeError("Agent 未启动")
|
|||
|
|
await self.controller.send(f"[执行结果]\n{result}\n\n请继续下一步,或标记任务完成。")
|
|||
|
|
|
|||
|
|
def _init_log(self):
|
|||
|
|
"""初始化日志文件 - 每次运行清空旧日志"""
|
|||
|
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
# 清空旧日志,只保留当前运行的记录
|
|||
|
|
self.log_file.write_text(f"=== Agent Debug Log - {timestamp} ===\n\n", encoding='utf-8')
|
|||
|
|
self._log_buffer = [] # 内存中的日志缓冲区
|
|||
|
|
|
|||
|
|
def read_log(self, lines: int = 50) -> str:
|
|||
|
|
"""读取最近 N 行日志"""
|
|||
|
|
try:
|
|||
|
|
if self.log_file.exists():
|
|||
|
|
content = self.log_file.read_text(encoding='utf-8')
|
|||
|
|
all_lines = content.split('\n')
|
|||
|
|
return '\n'.join(all_lines[-lines:])
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
return "(日志为空)"
|
|||
|
|
|
|||
|
|
def log(self, category: str, message: str):
|
|||
|
|
"""写入日志"""
|
|||
|
|
timestamp = time.strftime("%H:%M:%S")
|
|||
|
|
log_line = f"[{timestamp}] [{category}] {message}\n"
|
|||
|
|
with open(self.log_file, 'a', encoding='utf-8') as f:
|
|||
|
|
f.write(log_line)
|
|||
|
|
# 同时保存到内存缓冲区
|
|||
|
|
self._log_buffer.append((category, message))
|
|||
|
|
# 只保留最近100条
|
|||
|
|
if len(self._log_buffer) > 100:
|
|||
|
|
self._log_buffer = self._log_buffer[-100:]
|
|||
|
|
|
|||
|
|
def analyze_last_operation(self) -> str:
|
|||
|
|
"""分析最近的操作日志,找出问题"""
|
|||
|
|
analysis = []
|
|||
|
|
|
|||
|
|
# 查找最近的 WRITE_FILE 操作
|
|||
|
|
write_file_logs = [(i, cat, msg) for i, (cat, msg) in enumerate(self._log_buffer) if cat == "WRITE_FILE"]
|
|||
|
|
|
|||
|
|
if write_file_logs:
|
|||
|
|
analysis.append("=== 文件写入分析 ===")
|
|||
|
|
for idx, cat, msg in write_file_logs[-3:]: # 最近3次
|
|||
|
|
analysis.append(f" {msg}")
|
|||
|
|
|
|||
|
|
# 检查缩进问题
|
|||
|
|
if "Lines with indentation" in msg:
|
|||
|
|
if "[]" in msg or " Lines with indentation: 0" in msg:
|
|||
|
|
analysis.append(" ⚠️ 警告:没有检测到缩进!")
|
|||
|
|
|
|||
|
|
# 查找 CONTENT 提取
|
|||
|
|
content_logs = [(i, cat, msg) for i, (cat, msg) in enumerate(self._log_buffer) if cat == "PARSE" and "CONTENT" in msg]
|
|||
|
|
if content_logs:
|
|||
|
|
analysis.append("\n=== CONTENT 提取分析 ===")
|
|||
|
|
for idx, cat, msg in content_logs[-3:]:
|
|||
|
|
analysis.append(f" {msg}")
|
|||
|
|
|
|||
|
|
# 查找原始消息
|
|||
|
|
raw_logs = [(i, cat, msg) for i, (cat, msg) in enumerate(self._log_buffer) if cat == "RAW_MSG"]
|
|||
|
|
if raw_logs:
|
|||
|
|
analysis.append("\n=== 原始消息分析 ===")
|
|||
|
|
for idx, cat, msg in raw_logs[-2:]:
|
|||
|
|
# 检查是否包含 ```
|
|||
|
|
if "```" in msg:
|
|||
|
|
analysis.append(f" ✓ 检测到代码围栏标记")
|
|||
|
|
else:
|
|||
|
|
analysis.append(f" ✗ 未检测到代码围栏标记")
|
|||
|
|
|
|||
|
|
# 检查缩进字符
|
|||
|
|
if "\\n " in msg or "\\n\\t" in msg:
|
|||
|
|
analysis.append(f" ✓ 原始消息包含缩进字符")
|
|||
|
|
else:
|
|||
|
|
analysis.append(f" ⚠️ 原始消息可能没有缩进")
|
|||
|
|
|
|||
|
|
return "\n".join(analysis) if analysis else "(暂无分析数据)"
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def run(self):
|
|||
|
|
"""主运行循环"""
|
|||
|
|
self.chat.print_banner()
|
|||
|
|
|
|||
|
|
async with async_playwright() as p:
|
|||
|
|
# 使用持久化上下文,保存登录状态
|
|||
|
|
from pathlib import Path
|
|||
|
|
user_data_dir = Path.home() / ".deepseek_chat_profile"
|
|||
|
|
user_data_dir.mkdir(exist_ok=True)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 尝试启动持久化上下文
|
|||
|
|
context = await p.chromium.launch_persistent_context(
|
|||
|
|
str(user_data_dir),
|
|||
|
|
channel="chrome",
|
|||
|
|
headless=False,
|
|||
|
|
args=["--start-maximized"],
|
|||
|
|
no_viewport=True
|
|||
|
|
)
|
|||
|
|
print("[使用持久化会话,保留登录状态]")
|
|||
|
|
except Exception as e:
|
|||
|
|
# 如果失败,回退到普通模式
|
|||
|
|
print(f"[!] 持久化会话启动失败: {e}")
|
|||
|
|
print("[!] 使用普通模式(需要重新登录)")
|
|||
|
|
browser = await p.chromium.launch(
|
|||
|
|
channel="chrome",
|
|||
|
|
headless=False,
|
|||
|
|
args=["--start-maximized"]
|
|||
|
|
)
|
|||
|
|
context = await browser.new_context(no_viewport=True)
|
|||
|
|
|
|||
|
|
self.page = context.pages[0] if context.pages else await context.new_page()
|
|||
|
|
await self.page.goto(DEEPSEEK_URL)
|
|||
|
|
|
|||
|
|
print(f"[已打开 DeepSeek]: {DEEPSEEK_URL}")
|
|||
|
|
print("[!] 请先在 DeepSeek 网页中登录并创建新对话\n")
|
|||
|
|
|
|||
|
|
input("登录完成后按回车继续...")
|
|||
|
|
|
|||
|
|
self.controller = DeepSeekController(self.page)
|
|||
|
|
|
|||
|
|
# 发送系统提示词
|
|||
|
|
system_prompt = SOLO_SYSTEM_PROMPT
|
|||
|
|
|
|||
|
|
print("[正在初始化系统提示词]...")
|
|||
|
|
await self.controller.send(f"[系统初始化]\n\n{system_prompt}")
|
|||
|
|
print("[系统初始化完成]\n")
|
|||
|
|
|
|||
|
|
# 启动用户输入线程
|
|||
|
|
self.chat.is_running = True
|
|||
|
|
input_thread = threading.Thread(target=self.chat.user_input_loop, daemon=True)
|
|||
|
|
input_thread.start()
|
|||
|
|
|
|||
|
|
# 主处理循环
|
|||
|
|
await self._main_loop()
|
|||
|
|
|
|||
|
|
async def _main_loop(self):
|
|||
|
|
"""主处理循环"""
|
|||
|
|
while self.chat.is_running:
|
|||
|
|
try:
|
|||
|
|
# 检查是否有用户输入
|
|||
|
|
if not self.chat.message_queue.empty():
|
|||
|
|
user_task = self.chat.message_queue.get()
|
|||
|
|
await self._process_task(user_task)
|
|||
|
|
|
|||
|
|
await asyncio.sleep(0.1)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"\n❌ 错误: {e}\n")
|
|||
|
|
self.chat.task_in_progress = False
|
|||
|
|
|
|||
|
|
async def _process_task(self, user_task: str):
|
|||
|
|
"""处理用户任务 - 与 DeepSeek 多轮交互"""
|
|||
|
|
print(f"\n[任务]: {user_task}")
|
|||
|
|
print("-" * 50)
|
|||
|
|
|
|||
|
|
# 发送用户任务给 DeepSeek
|
|||
|
|
await self.controller.send(f"[用户任务]\n{user_task}\n\n请开始执行。")
|
|||
|
|
|
|||
|
|
# 初始化消息追踪
|
|||
|
|
await asyncio.sleep(2)
|
|||
|
|
self.last_ai_msg = await self.controller.get_latest_ai_message(self) or ""
|
|||
|
|
|
|||
|
|
round_count = 0
|
|||
|
|
|
|||
|
|
while True:
|
|||
|
|
round_count += 1
|
|||
|
|
|
|||
|
|
# 等待 DeepSeek 回复
|
|||
|
|
await asyncio.sleep(2)
|
|||
|
|
current_msg = await self._wait_for_new_message()
|
|||
|
|
|
|||
|
|
# 记录获取到的消息
|
|||
|
|
if current_msg:
|
|||
|
|
self.log("MSG_RECEIVED", f"Length: {len(current_msg)}, Preview: {current_msg[:200].replace(chr(10), '\\n')}")
|
|||
|
|
|
|||
|
|
if not current_msg:
|
|||
|
|
print("[等待回复超时]")
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
print(f"\n[DeepSeek 回复 - 轮次 {round_count}]:")
|
|||
|
|
# 显示清理后的消息(去掉 ACTION 标记)
|
|||
|
|
clean_msg = self._clean_action_markers(current_msg)
|
|||
|
|
if clean_msg.strip():
|
|||
|
|
print(f" {clean_msg[:200]}...")
|
|||
|
|
|
|||
|
|
# 解析 ACTION
|
|||
|
|
action = self._parse_action(current_msg, self)
|
|||
|
|
|
|||
|
|
if action is None and "<<<ACTION" in current_msg and "ACTION>>>" in current_msg:
|
|||
|
|
# 有 ACTION 标记但解析失败,返回错误让 DeepSeek 重新输出
|
|||
|
|
print("\n[!] ACTION 解析失败,返回错误给 DeepSeek")
|
|||
|
|
|
|||
|
|
# 提取原始 ACTION 内容给 DeepSeek 看
|
|||
|
|
import re
|
|||
|
|
action_match = re.search(r'<<<ACTION\s*\n?(.*?)\s*ACTION>>>', current_msg, re.DOTALL)
|
|||
|
|
raw_action = action_match.group(1) if action_match else "无法提取"
|
|||
|
|
|
|||
|
|
# 获取具体的解析错误信息
|
|||
|
|
import json
|
|||
|
|
try:
|
|||
|
|
json.loads(raw_action)
|
|||
|
|
parse_error = "未知错误"
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
parse_error = str(e)
|
|||
|
|
|
|||
|
|
error_msg = f"""[执行结果]
|
|||
|
|
错误: ACTION 格式解析失败
|
|||
|
|
|
|||
|
|
解析错误:{parse_error}
|
|||
|
|
|
|||
|
|
请重新输出正确的 ACTION。
|
|||
|
|
|
|||
|
|
本次回复请以 {END_MARKER} 结尾"""
|
|||
|
|
|
|||
|
|
await asyncio.sleep(1)
|
|||
|
|
await self.controller.send(error_msg)
|
|||
|
|
|
|||
|
|
# 等待 DeepSeek 重新回复
|
|||
|
|
await asyncio.sleep(3)
|
|||
|
|
self.last_ai_msg = ""
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if action:
|
|||
|
|
action_type = action.get('type', '')
|
|||
|
|
|
|||
|
|
if action_type == 'complete':
|
|||
|
|
# 任务完成
|
|||
|
|
explanation = action.get('explanation', '任务已完成')
|
|||
|
|
print(f"\n[任务完成!]")
|
|||
|
|
self.chat.display_result(explanation)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 执行命令
|
|||
|
|
cmd = action.get('cmd', '')
|
|||
|
|
path = action.get('path', '')
|
|||
|
|
content = action.get('content', '')
|
|||
|
|
explanation = action.get('explanation', '')
|
|||
|
|
|
|||
|
|
# write_file 新格式:用 path 而不是 cmd
|
|||
|
|
if action_type == 'write_file' and path:
|
|||
|
|
cmd = path
|
|||
|
|
print(f"\n[执行]: [{action_type}] {path}")
|
|||
|
|
else:
|
|||
|
|
print(f"\n[执行]: [{action_type}] {cmd}")
|
|||
|
|
|
|||
|
|
if explanation:
|
|||
|
|
print(f" 说明: {explanation}")
|
|||
|
|
|
|||
|
|
result = await self.engine.execute(action_type, cmd, "", content, self)
|
|||
|
|
|
|||
|
|
# 显示执行结果摘要
|
|||
|
|
output_preview = result['output'][:200].replace('\n', ' ')
|
|||
|
|
status = "[OK]" if result['success'] else "[ERR]"
|
|||
|
|
print(f" {status} 结果: {output_preview}...")
|
|||
|
|
|
|||
|
|
# 如果是 write_file 操作,自动分析问题
|
|||
|
|
if action_type == "write_file":
|
|||
|
|
print("\n" + "="*50)
|
|||
|
|
print("[自动诊断]")
|
|||
|
|
print(self.analyze_last_operation())
|
|||
|
|
print("="*50)
|
|||
|
|
|
|||
|
|
# 发送结果回 DeepSeek(完整输出,不截断)
|
|||
|
|
display_cmd = path if action_type == 'write_file' and path else cmd
|
|||
|
|
result_msg = f"""[执行结果]
|
|||
|
|
操作: {action_type}
|
|||
|
|
命令: {display_cmd}
|
|||
|
|
成功: {result['success']}
|
|||
|
|
|
|||
|
|
输出:
|
|||
|
|
```
|
|||
|
|
{result['output']}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
请继续下一步或完成任务。"""
|
|||
|
|
|
|||
|
|
await asyncio.sleep(1)
|
|||
|
|
await self.controller.send(result_msg)
|
|||
|
|
|
|||
|
|
# 等待一段时间让 DeepSeek 开始生成回复
|
|||
|
|
print(" [等待 DeepSeek 继续...]")
|
|||
|
|
await asyncio.sleep(3)
|
|||
|
|
|
|||
|
|
# 重置 last_ai_msg,让 _wait_for_new_message 从头检测新消息
|
|||
|
|
self.last_ai_msg = ""
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
# 没有 ACTION 标记,可能是普通回复或任务完成
|
|||
|
|
if "完成" in current_msg or "done" in current_msg.lower():
|
|||
|
|
print(f"\n[任务完成!]")
|
|||
|
|
self.chat.display_result(clean_msg)
|
|||
|
|
return
|
|||
|
|
else:
|
|||
|
|
# 普通回复,显示给用户
|
|||
|
|
print(f"\n[DeepSeek]: {clean_msg}")
|
|||
|
|
self.chat.display_result(clean_msg)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
async def _wait_for_new_message(self) -> Optional[str]:
|
|||
|
|
"""等待新消息完整输出 - 检测结束标记"""
|
|||
|
|
CHECK_INTERVAL = 0.5
|
|||
|
|
|
|||
|
|
print(" [等待 DeepSeek 回复]...", end="", flush=True)
|
|||
|
|
|
|||
|
|
while True:
|
|||
|
|
# 获取当前消息内容
|
|||
|
|
current_msg = await self.controller.get_latest_ai_message()
|
|||
|
|
|
|||
|
|
# 跳过已处理过的消息(完全相同的内容)
|
|||
|
|
if not current_msg or current_msg == self.last_ai_msg:
|
|||
|
|
await asyncio.sleep(CHECK_INTERVAL)
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 检查是否包含结束标记
|
|||
|
|
if END_MARKER in current_msg:
|
|||
|
|
# 找到结束标记,消息完整
|
|||
|
|
print(f"\r [消息完成] ({len(current_msg)} 字符)")
|
|||
|
|
# 去掉结束标记后返回
|
|||
|
|
clean_msg = current_msg.replace(END_MARKER, "").strip()
|
|||
|
|
self.last_ai_msg = current_msg
|
|||
|
|
return clean_msg
|
|||
|
|
|
|||
|
|
# 还在生成中
|
|||
|
|
print(f"\r [生成中...] ({len(current_msg)} 字符)", end="", flush=True)
|
|||
|
|
await asyncio.sleep(CHECK_INTERVAL)
|
|||
|
|
|
|||
|
|
def _parse_action(self, text: str, agent: 'SoloAgent' = None) -> Optional[dict]:
|
|||
|
|
"""解析 ACTION 块和 CONTENT 块"""
|
|||
|
|
if not text:
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", "Empty text input")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# 记录原始消息(用于调试)
|
|||
|
|
if agent:
|
|||
|
|
agent.log("RAW_MSG", text[:500].replace('\n', '\\n'))
|
|||
|
|
|
|||
|
|
# 调试:显示检测到的 ACTION
|
|||
|
|
if "<<<ACTION" in text:
|
|||
|
|
print(f"\n [Debug] Detected ACTION marker in message")
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", "Detected ACTION marker")
|
|||
|
|
|
|||
|
|
# 提取 ACTION 块
|
|||
|
|
action_pattern = r'<<<ACTION\s*\n?(\{[\s\S]*?\})\s*ACTION>>>'
|
|||
|
|
action_match = re.search(action_pattern, text, re.DOTALL)
|
|||
|
|
|
|||
|
|
if not action_match:
|
|||
|
|
# 备用解析
|
|||
|
|
if "<<<ACTION" in text and "ACTION>>>" in text:
|
|||
|
|
try:
|
|||
|
|
start = text.find("<<<ACTION") + len("<<<ACTION")
|
|||
|
|
end = text.find("ACTION>>>")
|
|||
|
|
json_content = text[start:end].strip()
|
|||
|
|
print(f" [Debug] Fallback extraction: {json_content[:100]}...")
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", f"Fallback extraction: {json_content[:200]}")
|
|||
|
|
if json_content.startswith("{") and json_content.endswith("}"):
|
|||
|
|
action_match = type('obj', (object,), {'group': lambda x: json_content})()
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" [Debug] Fallback error: {e}")
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", f"Fallback error: {e}")
|
|||
|
|
return None
|
|||
|
|
else:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
json_str = action_match.group(1).strip()
|
|||
|
|
print(f" [Debug] Extracted JSON: {json_str[:100]}...")
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", f"JSON string: {json_str}")
|
|||
|
|
action = json.loads(json_str)
|
|||
|
|
|
|||
|
|
# 检查是否有配套的 CONTENT 块(用于 write_file)
|
|||
|
|
if action.get("type") == "write_file":
|
|||
|
|
content_pattern = r'<<<CONTENT\s*\n?(.*?)\s*CONTENT>>>'
|
|||
|
|
content_match = re.search(content_pattern, text, re.DOTALL)
|
|||
|
|
if content_match:
|
|||
|
|
# 把 CONTENT 内容放到 action 里
|
|||
|
|
raw_content = content_match.group(1)
|
|||
|
|
action["content"] = raw_content
|
|||
|
|
print(f" [Debug] Extracted CONTENT: {len(action['content'])} chars")
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", f"CONTENT extracted: {len(raw_content)} chars")
|
|||
|
|
agent.log("PARSE", f"CONTENT preview: {raw_content[:200].replace(chr(10), '\\n')}")
|
|||
|
|
elif "path" in action and "cmd" not in action:
|
|||
|
|
# 新格式要求有 CONTENT 块但没有找到
|
|||
|
|
print(f" [Debug] Warning: write_file without CONTENT block")
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", "Warning: write_file without CONTENT block")
|
|||
|
|
action["content"] = ""
|
|||
|
|
|
|||
|
|
return action
|
|||
|
|
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(f" [Debug] JSON parse error: {e}")
|
|||
|
|
if agent:
|
|||
|
|
agent.log("PARSE", f"JSON error: {e}")
|
|||
|
|
agent.log("PARSE", f"Problematic JSON: {json_str[:300] if 'json_str' in dir() else 'N/A'}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def _clean_action_markers(self, text: str) -> str:
|
|||
|
|
"""清理 ACTION 标记,返回可读文本"""
|
|||
|
|
# 移除 ACTION 块
|
|||
|
|
clean = re.sub(r'<<<ACTION\s*[\s\S]*?\s*ACTION>>>', '', text, flags=re.DOTALL)
|
|||
|
|
# 清理多余空行
|
|||
|
|
clean = re.sub(r'\n{3,}', '\n\n', clean)
|
|||
|
|
return clean.strip()
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
agent = SoloAgent()
|
|||
|
|
try:
|
|||
|
|
asyncio.run(agent.run())
|
|||
|
|
except KeyboardInterrupt:
|
|||
|
|
print("\n\n[再见!]")
|