Files

1134 lines
47 KiB
Python
Raw Permalink Normal View History

2026-04-17 09:12:38 +08:00
"""
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[再见!]")