Files
deepseek-web/yuanbao_agent_mvp.py
T

374 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
元宝全自动代理 MVP
验证思路:通过 Playwright 拦截元宝对话,识别本地调用意图,
执行本地操作后将结果注入回对话。
核心机制:
1. 监听元宝输入框,检测特殊指令标记(如 /local 或 @agent)
2. 拦截用户输入,在本地执行
3. 将执行结果作为"用户消息"注入回对话
4. 元宝基于结果继续回复
"""
import asyncio
import json
import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional, Callable
from playwright.async_api import async_playwright, Page, BrowserContext
YUANBAO_URL = "https://yuanbao.tencent.com/chat"
# 本地工具注册表
TOOLS: dict[str, Callable] = {}
def register_tool(name: str):
"""工具装饰器"""
def decorator(func: Callable):
TOOLS[name] = func
return func
return decorator
# ============ 本地工具实现 ============
@register_tool("read_file")
def tool_read_file(path: str) -> str:
"""读取本地文件内容"""
try:
file_path = Path(path).expanduser().resolve()
if not file_path.exists():
return f"[错误] 文件不存在: {path}"
content = file_path.read_text(encoding="utf-8", errors="ignore")
# 截断过长内容
if len(content) > 5000:
content = content[:5000] + f"\n... (已截断,共 {len(content)} 字符)"
return f"[文件内容: {path}]\n```\n{content}\n```"
except Exception as e:
return f"[错误] 读取文件失败: {e}"
@register_tool("list_dir")
def tool_list_dir(path: str = ".") -> str:
"""列出目录内容"""
try:
dir_path = Path(path).expanduser().resolve()
if not dir_path.exists():
return f"[错误] 目录不存在: {path}"
items = []
for item in sorted(dir_path.iterdir()):
item_type = "📁" if item.is_dir() else "📄"
items.append(f"{item_type} {item.name}")
return f"[目录列表: {path}]\n" + "\n".join(items[:50])
except Exception as e:
return f"[错误] 列出目录失败: {e}"
@register_tool("run_command")
def tool_run_command(command: str) -> str:
"""执行本地命令"""
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=30,
encoding="utf-8",
errors="ignore"
)
output = result.stdout
if result.stderr:
output += f"\n[stderr]\n{result.stderr}"
# 截断过长输出
if len(output) > 5000:
output = output[:5000] + f"\n... (已截断,共 {len(output)} 字符)"
return f"[命令执行: {command}]\n返回码: {result.returncode}\n```\n{output}\n```"
except subprocess.TimeoutExpired:
return f"[错误] 命令执行超时: {command}"
except Exception as e:
return f"[错误] 执行命令失败: {e}"
@register_tool("web_search")
def tool_web_search(query: str) -> str:
"""模拟网络搜索(MVP 阶段返回提示)"""
return f"[搜索: {query}]\nMVP 阶段暂未接入真实搜索,请直接询问元宝。"
# ============ 指令解析器 ============
class CommandParser:
"""解析用户输入中的本地调用指令"""
# 指令模式:@agent:tool_name(args) 或 /local tool_name args
PATTERNS = [
# @agent:read_file("/path/to/file")
r'@agent:(\w+)\s*\(\s*["\']?([^"\')]*)["\']?\s*\)',
# /local read_file /path/to/file
r'/local\s+(\w+)\s+(.+?)(?:\n|$)',
# ```local
# read_file /path/to/file
# ```
r'```local\s*\n(\w+)\s+(.+?)\n```',
]
def parse(self, text: str) -> Optional[dict]:
"""解析指令,返回工具调用信息或 None"""
text = text.strip()
# 模式 1: @agent:tool_name(args)
match = re.search(self.PATTERNS[0], text)
if match:
return {
"tool": match.group(1),
"args": [match.group(2)],
"raw": match.group(0)
}
# 模式 2: /local tool_name args
match = re.search(self.PATTERNS[1], text)
if match:
return {
"tool": match.group(1),
"args": [match.group(2).strip()],
"raw": match.group(0)
}
# 模式 3: ```local ... ```
match = re.search(self.PATTERNS[2], text, re.DOTALL)
if match:
return {
"tool": match.group(1),
"args": [match.group(2).strip()],
"raw": match.group(0)
}
return None
# ============ 元宝页面操作 ============
class YuanbaoController:
"""控制元宝网页的输入输出"""
def __init__(self, page: Page):
self.page = page
self.parser = CommandParser()
async def find_input_box(self):
"""查找输入框"""
selectors = [
"div[contenteditable='true'][data-placeholder]",
"div[contenteditable='true']",
"textarea[placeholder]",
"textarea",
]
for sel in selectors:
try:
el = await self.page.wait_for_selector(sel, timeout=3000)
if el:
return el
except Exception:
continue
return None
async def inject_message(self, content: str):
"""将内容注入到输入框并发送"""
input_box = await self.find_input_box()
if not input_box:
print("[MVP] 未找到输入框")
return False
# 点击输入框
await input_box.click()
await asyncio.sleep(0.3)
# 清空现有内容
await self.page.keyboard.press("Control+a")
await self.page.keyboard.press("Delete")
await asyncio.sleep(0.2)
# 输入内容
# 分段输入避免过长
max_chunk = 500
for i in range(0, len(content), max_chunk):
chunk = content[i:i+max_chunk]
await input_box.type(chunk, delay=5)
await asyncio.sleep(0.5)
# 发送
await self._click_send(input_box)
return True
async def _click_send(self, input_box):
"""点击发送按钮"""
send_selectors = [
"button[class*='send']",
"button[aria-label*='发送']",
"button[title*='发送']",
"div[class*='send-btn']",
]
for sel in send_selectors:
try:
btn = self.page.locator(sel).last
if await btn.is_visible():
await btn.click()
return
except Exception:
continue
# 回退到 Enter
await input_box.press("Enter")
async def get_last_user_message(self) -> Optional[str]:
"""获取最后一条用户消息"""
try:
# 查找用户消息
messages = await self.page.evaluate("""
() => {
const items = document.querySelectorAll('.agent-chat__list__item--user, [class*="user"]');
const last = items[items.length - 1];
return last ? last.innerText?.trim() : null;
}
""")
return messages
except Exception as e:
print(f"[MVP] 获取消息失败: {e}")
return None
async def intercept_and_execute(self, user_input: str) -> Optional[str]:
"""
拦截用户输入,如果包含本地指令则执行并返回结果
返回: 要注入回对话的内容,或 None(无需拦截)
"""
cmd = self.parser.parse(user_input)
if not cmd:
return None
tool_name = cmd["tool"]
args = cmd["args"]
print(f"[MVP] 检测到本地指令: {tool_name}({args})")
# 执行工具
if tool_name not in TOOLS:
return f"[错误] 未知工具: {tool_name}。可用工具: {list(TOOLS.keys())}"
try:
result = TOOLS[tool_name](*args)
# 包装成自然对话格式
return f"[本地执行结果]\n{result}\n\n请基于以上结果继续回答。"
except Exception as e:
return f"[错误] 工具执行失败: {e}"
# ============ 主循环 ============
class YuanbaoAgentMVP:
"""元宝全自动代理 MVP"""
def __init__(self):
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
self.controller: Optional[YuanbaoController] = None
self._last_processed: Optional[str] = None
async def run(self):
"""启动 MVP"""
print("=" * 60)
print(" 元宝全自动代理 MVP")
print("=" * 60)
print("\n使用说明:")
print(" 1. 在元宝对话中输入特殊指令触发本地调用:")
print(" - @agent:read_file('/path/to/file')")
print(" - /local list_dir /path/to/dir")
print(" - ```local\nrun_command ls -la\n```")
print(" 2. 系统将拦截指令,执行本地操作")
print(" 3. 执行结果将注入回对话,元宝基于结果继续")
print("\n按 Ctrl+C 退出\n")
async with async_playwright() as p:
# 启动浏览器
print("[MVP] 启动浏览器...")
user_data_dir = str(Path.home() / ".yuanbao_agent_profile")
self.context = await p.chromium.launch_persistent_context(
user_data_dir=user_data_dir,
channel="chrome",
headless=False,
args=["--start-maximized"],
no_viewport=True,
)
self.page = self.context.pages[0] if self.context.pages else await self.context.new_page()
await self.page.goto(YUANBAO_URL)
print(f"[MVP] 已打开元宝: {YUANBAO_URL}")
print("[MVP] 请手动登录元宝,然后按回车继续...")
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, input)
self.controller = YuanbaoController(self.page)
print("[MVP] 开始监听对话...")
await self._monitor_loop()
async def _monitor_loop(self):
"""监听循环"""
while True:
try:
await asyncio.sleep(2) # 每 2 秒检查一次
# 获取最后一条用户消息
last_msg = await self.controller.get_last_user_message()
if not last_msg:
continue
# 避免重复处理
if last_msg == self._last_processed:
continue
# 检查是否包含本地指令
result = await self.controller.intercept_and_execute(last_msg)
if result:
print(f"[MVP] 准备注入执行结果...")
# 等待元宝开始回复(可选)
await asyncio.sleep(1)
# 注入结果
success = await self.controller.inject_message(result)
if success:
print(f"[MVP] 结果已注入对话")
self._last_processed = last_msg
else:
print(f"[MVP] 注入失败")
else:
# 无指令,只是记录
self._last_processed = last_msg
except KeyboardInterrupt:
print("\n[MVP] 用户中断")
break
except Exception as e:
print(f"[MVP] 监听错误: {e}")
await asyncio.sleep(5)
async def main():
agent = YuanbaoAgentMVP()
try:
await agent.run()
except Exception as e:
print(f"[MVP] 错误: {e}")
raise
if __name__ == "__main__":
asyncio.run(main())