Add OpenClaw skills, platform kit, and template docs
Made-with: Cursor
This commit is contained in:
28
llm-manager/scripts/engines/api_engine.py
Normal file
28
llm-manager/scripts/engines/api_engine.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
OpenAI 兼容 API 引擎:适用于 DeepSeek、通义千问、Kimi、文心一言、豆包(火山方舟)。
|
||||
只要平台提供 OpenAI 兼容接口,均可用此引擎,无需 Playwright。
|
||||
"""
|
||||
|
||||
|
||||
class ApiEngine:
|
||||
def __init__(self, api_base: str, api_key: str, model: str):
|
||||
self.api_base = api_base
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
|
||||
def generate(self, prompt: str) -> str:
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except ImportError:
|
||||
return "ERROR:缺少依赖:pip install openai"
|
||||
|
||||
try:
|
||||
client = OpenAI(base_url=self.api_base, api_key=self.api_key)
|
||||
response = client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
content = response.choices[0].message.content
|
||||
return (content or "").strip()
|
||||
except Exception as e:
|
||||
return f"ERROR:API调用失败:{e}"
|
||||
15
llm-manager/scripts/engines/base.py
Normal file
15
llm-manager/scripts/engines/base.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from playwright.async_api import Page
|
||||
|
||||
|
||||
class BaseEngine(ABC):
|
||||
def __init__(self, page: Page):
|
||||
self.page = page
|
||||
|
||||
@abstractmethod
|
||||
async def generate(self, prompt: str) -> str:
|
||||
"""
|
||||
基于操作页面的具体大模型的生成逻辑。输入文本,抓取并返回文本。
|
||||
返回值:正常结果字符串,或以 "ERROR:" 开头的错误描述。
|
||||
"""
|
||||
pass
|
||||
70
llm-manager/scripts/engines/deepseek.py
Normal file
70
llm-manager/scripts/engines/deepseek.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
DeepSeek 网页版驱动引擎(chat.deepseek.com)。
|
||||
|
||||
选择器说明(如页面改版需更新):
|
||||
- 输入框:#chat-input(textarea)
|
||||
- 发送按钮:[aria-label="发送消息"] 或 div[class*="send"] > button
|
||||
- 停止生成按钮:[aria-label="停止生成"](生成中可见,生成结束消失)
|
||||
- 复制按钮:最后一条回复下的 [aria-label="复制"]
|
||||
"""
|
||||
import asyncio
|
||||
from .base import BaseEngine
|
||||
|
||||
|
||||
class DeepSeekEngine(BaseEngine):
|
||||
async def generate(self, prompt: str) -> str:
|
||||
await self.page.goto("https://chat.deepseek.com")
|
||||
await self.page.wait_for_load_state("networkidle")
|
||||
|
||||
# 登录检测:若能找到输入框则已登录
|
||||
try:
|
||||
editor = self.page.locator("textarea#chat-input").first
|
||||
await editor.wait_for(state="visible", timeout=10000)
|
||||
except Exception:
|
||||
return "ERROR:REQUIRE_LOGIN"
|
||||
|
||||
# 输入提示词
|
||||
await editor.click()
|
||||
await self.page.keyboard.insert_text(prompt)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 发送(优先点按钮,失败则按 Enter)
|
||||
sent = False
|
||||
for sel in ('[aria-label="发送消息"]', 'div[class*="send"] > button', 'button[type="submit"]'):
|
||||
try:
|
||||
btn = self.page.locator(sel).first
|
||||
if await btn.is_visible(timeout=1000):
|
||||
await btn.click()
|
||||
sent = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not sent:
|
||||
await self.page.keyboard.press("Enter")
|
||||
|
||||
print("💡 [llm-manager/deepseek] 已发送提示词,等待 DeepSeek 生成响应...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 等待生成完毕:停止按钮消失即为完成,超时 150 秒
|
||||
stop_sel = '[aria-label="停止生成"]'
|
||||
deadline = asyncio.get_event_loop().time() + 150
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
try:
|
||||
visible = await self.page.locator(stop_sel).first.is_visible(timeout=500)
|
||||
if not visible:
|
||||
break
|
||||
except Exception:
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 通过最后一个复制按钮取结果
|
||||
try:
|
||||
copy_btn = self.page.locator('[aria-label="复制"]').last
|
||||
await copy_btn.click()
|
||||
await asyncio.sleep(0.5)
|
||||
result = await self.page.evaluate("navigator.clipboard.readText()")
|
||||
return result.strip()
|
||||
except Exception as e:
|
||||
return f"ERROR:抓取 DeepSeek 内容时异常:{e}"
|
||||
222
llm-manager/scripts/engines/doubao.py
Normal file
222
llm-manager/scripts/engines/doubao.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
豆包网页版驱动(doubao.com/chat/)。
|
||||
|
||||
生成后不依赖「喇叭瞬时出现/消失」这种不稳定信号。
|
||||
而是轮询判断两种输出是否已进入可复制状态:
|
||||
1) 右侧写作模式:出现「改用对话直接回答」入口并可点右侧复制按钮
|
||||
2) 左侧对话模式:出现可点的消息复制按钮(拿最后一条新增回复)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from .base import BaseEngine
|
||||
|
||||
_DOUBAO_SPEAKER_PATH_SNIPPET = "M19.8628 9.29346C20.3042"
|
||||
|
||||
|
||||
class DoubaoEngine(BaseEngine):
|
||||
async def generate(self, prompt: str) -> str:
|
||||
self.page.set_default_timeout(60_000)
|
||||
await self.page.goto(
|
||||
"https://www.doubao.com/chat/",
|
||||
wait_until="domcontentloaded",
|
||||
timeout=60_000,
|
||||
)
|
||||
|
||||
editor = self.page.locator('textarea[data-testid="chat_input_input"]').first
|
||||
try:
|
||||
await editor.wait_for(state="visible", timeout=30_000)
|
||||
except Exception:
|
||||
return "ERROR:REQUIRE_LOGIN"
|
||||
|
||||
text = (prompt or "").strip()
|
||||
if not text:
|
||||
return "ERROR:PROMPT_EMPTY"
|
||||
|
||||
await editor.click()
|
||||
await editor.fill(text)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
send = self.page.locator(
|
||||
'#flow-end-msg-send, [data-testid="chat_input_send_button"]'
|
||||
).first
|
||||
try:
|
||||
await send.wait_for(state="visible", timeout=15_000)
|
||||
await send.click(timeout=10_000)
|
||||
except Exception as e:
|
||||
return f"ERROR:DOUBAO_SEND_FAILED {e}"
|
||||
|
||||
print("💡 [llm-manager/doubao] 已发送,等待生成完成后再复制(最长 180s)...")
|
||||
|
||||
# 发送前记录:用于确认点的是新增复制按钮,且剪贴板确实变化了
|
||||
left_copy = self.page.locator('[data-testid="message_action_copy"]')
|
||||
right_copy = self.page.locator('[data-testid="container_inner_copy_btn"]')
|
||||
left_prev = await left_copy.count()
|
||||
right_prev = await right_copy.count()
|
||||
clipboard_before = await self._safe_read_clipboard()
|
||||
|
||||
# 稳定检测:避免生成中“喇叭短暂出现”误判(需要连续多次可见)
|
||||
speaker = self.page.locator(
|
||||
f'svg path[d*="{_DOUBAO_SPEAKER_PATH_SNIPPET}"]'
|
||||
).last
|
||||
stable_needed = 3
|
||||
stable = 0
|
||||
interval_sec = 0.5
|
||||
deadline = time.monotonic() + 180.0
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
if await speaker.is_visible():
|
||||
stable += 1
|
||||
else:
|
||||
stable = 0
|
||||
except Exception:
|
||||
stable = 0
|
||||
if stable >= stable_needed:
|
||||
break
|
||||
await asyncio.sleep(interval_sec)
|
||||
else:
|
||||
return "ERROR:DOUBAO_WAIT_COMPLETE_TIMEOUT 180 秒内未检测到“喇叭稳定回显”。"
|
||||
|
||||
# 生成完成后:依据当前页面状态决定点右侧还是左侧复制
|
||||
write_mode = await self._is_write_mode()
|
||||
if write_mode:
|
||||
if await self._wait_count_gt(right_copy, right_prev, timeout_sec=15.0):
|
||||
copy_btn = right_copy.last
|
||||
elif await self._wait_count_gt(left_copy, left_prev, timeout_sec=15.0):
|
||||
copy_btn = left_copy.last
|
||||
else:
|
||||
return "ERROR:DOUBAO_COPY_BUTTON_NOT_READY 右侧/左侧复制按钮均未出现新增。"
|
||||
else:
|
||||
if await self._wait_count_gt(left_copy, left_prev, timeout_sec=15.0):
|
||||
copy_btn = left_copy.last
|
||||
elif await self._wait_count_gt(right_copy, right_prev, timeout_sec=5.0):
|
||||
copy_btn = right_copy.last
|
||||
else:
|
||||
return "ERROR:DOUBAO_COPY_BUTTON_NOT_READY 左侧复制按钮未出现新增。"
|
||||
|
||||
try:
|
||||
await copy_btn.scroll_into_view_if_needed()
|
||||
await copy_btn.click(timeout=15_000, force=True)
|
||||
except Exception as e:
|
||||
return f"ERROR:DOUBAO_COPY_CLICK_FAILED {e}"
|
||||
|
||||
out = await self._wait_clipboard_changed(clipboard_before, timeout_sec=20.0)
|
||||
if out is None:
|
||||
return "ERROR:DOUBAO_CLIPBOARD_NOT_UPDATED 剪贴板未在规定时间内更新。"
|
||||
if self._is_invalid_clipboard(out, clipboard_before=clipboard_before):
|
||||
return f"ERROR:DOUBAO_CLIPBOARD_INVALID {out[:200]}"
|
||||
|
||||
await asyncio.sleep(5)
|
||||
return out
|
||||
|
||||
async def _is_write_mode(self) -> bool:
|
||||
marker = self.page.locator('div.bottom-entry-GSWErB:has-text("改用对话直接回答")').first
|
||||
try:
|
||||
return await marker.is_visible()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _wait_count_gt(self, locator, prev_count: int, timeout_sec: float) -> bool:
|
||||
deadline = time.monotonic() + timeout_sec
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
if await locator.count() > prev_count:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.3)
|
||||
return False
|
||||
|
||||
async def _wait_clipboard_changed(
|
||||
self, clipboard_before: str | None, timeout_sec: float
|
||||
) -> str | None:
|
||||
deadline = time.monotonic() + timeout_sec
|
||||
last = clipboard_before
|
||||
while time.monotonic() < deadline:
|
||||
cur = await self._safe_read_clipboard()
|
||||
if cur and cur != last and not self._is_invalid_clipboard(
|
||||
cur, clipboard_before=clipboard_before
|
||||
):
|
||||
return cur.strip()
|
||||
last = cur
|
||||
await asyncio.sleep(0.4)
|
||||
return None
|
||||
|
||||
async def _try_copy_right(self, right_copy) -> str | None:
|
||||
# 右侧复制按钮
|
||||
try:
|
||||
btn = right_copy.first
|
||||
await btn.wait_for(state="visible", timeout=2000)
|
||||
await btn.scroll_into_view_if_needed()
|
||||
await btn.click(timeout=5000, force=True)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
await asyncio.sleep(0.4)
|
||||
out = await self._safe_read_clipboard()
|
||||
if self._is_invalid_clipboard(out):
|
||||
return None
|
||||
if out == (await self._safe_read_clipboard()):
|
||||
pass
|
||||
return (out or "").strip()
|
||||
|
||||
async def _try_copy_left(self, left_copy, prev_count: int) -> str | None:
|
||||
n = await left_copy.count()
|
||||
if n <= prev_count:
|
||||
return None
|
||||
|
||||
# 从新增区间末尾往前找第一个可点击的复制按钮
|
||||
for idx in range(n - 1, prev_count - 1, -1):
|
||||
btn = left_copy.nth(idx)
|
||||
try:
|
||||
if not await btn.is_visible():
|
||||
continue
|
||||
await btn.scroll_into_view_if_needed()
|
||||
await btn.click(timeout=5000, force=True)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
await asyncio.sleep(0.4)
|
||||
out = await self._safe_read_clipboard()
|
||||
if self._is_invalid_clipboard(out):
|
||||
continue
|
||||
return (out or "").strip()
|
||||
return None
|
||||
|
||||
def _is_invalid_clipboard(
|
||||
self, text: str | None, clipboard_before: str | None = None
|
||||
) -> bool:
|
||||
if not text:
|
||||
return True
|
||||
t = str(text).strip()
|
||||
if not t:
|
||||
return True
|
||||
if clipboard_before is not None and t == str(clipboard_before).strip():
|
||||
return True
|
||||
if t.startswith("ERROR:"):
|
||||
return True
|
||||
# 防止误点拿到 UI 文案/占位文本
|
||||
banned_substrings = (
|
||||
"改用对话直接回答",
|
||||
"新对话",
|
||||
"正在生成",
|
||||
"复制",
|
||||
"下载",
|
||||
"快捷",
|
||||
"发消息",
|
||||
)
|
||||
if any(s in t for s in banned_substrings):
|
||||
return True
|
||||
if "data-testid" in t or "<button" in t:
|
||||
return True
|
||||
# 经验阈值:正常正文不会太短
|
||||
if len(t) < 30:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _safe_read_clipboard(self) -> str | None:
|
||||
try:
|
||||
return await self.page.evaluate("navigator.clipboard.readText()")
|
||||
except Exception:
|
||||
return None
|
||||
48
llm-manager/scripts/engines/kimi.py
Normal file
48
llm-manager/scripts/engines/kimi.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import asyncio
|
||||
from .base import BaseEngine
|
||||
|
||||
|
||||
class KimiEngine(BaseEngine):
|
||||
async def generate(self, prompt: str) -> str:
|
||||
await self.page.goto("https://kimi.moonshot.cn/")
|
||||
await self.page.wait_for_load_state("networkidle")
|
||||
|
||||
# 登录检测:等待输入框出现,超时则未登录
|
||||
try:
|
||||
editor = self.page.locator(".chat-input-editor").first
|
||||
await editor.wait_for(state="visible", timeout=10000)
|
||||
except Exception:
|
||||
return "ERROR:REQUIRE_LOGIN"
|
||||
|
||||
# 输入提示词(insert_text 避免换行符被误触发)
|
||||
await editor.click()
|
||||
await self.page.keyboard.insert_text(prompt)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 点击发送按钮
|
||||
await self.page.locator(".send-button-container").first.click()
|
||||
|
||||
print("💡 [llm-manager/kimi] 已发送提示词,等待 Kimi 生成响应...")
|
||||
|
||||
# 等待 2 秒让发送按钮进入"生成中"状态
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 等待 Send SVG 图标重新出现(表示生成完毕)
|
||||
try:
|
||||
send_icon = self.page.locator('.send-button-container svg[name="Send"]')
|
||||
await send_icon.wait_for(state="visible", timeout=150000)
|
||||
except Exception:
|
||||
return "ERROR:Kimi 生成超时(150秒未见完成标志),可能网络卡住或内容被拦截。"
|
||||
|
||||
# 稳妥起见多等 2 秒,让复制按钮完全渲染
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 点击最后一条回复的复制按钮,通过剪贴板取完整文本
|
||||
try:
|
||||
copy_btn = self.page.locator('svg[name="Copy"]').last
|
||||
await copy_btn.click()
|
||||
await asyncio.sleep(0.5)
|
||||
result = await self.page.evaluate("navigator.clipboard.readText()")
|
||||
return result.strip()
|
||||
except Exception as e:
|
||||
return f"ERROR:抓取 Kimi 内容时异常:{e}"
|
||||
91
llm-manager/scripts/engines/qianwen.py
Normal file
91
llm-manager/scripts/engines/qianwen.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
通义千问网页版驱动引擎(tongyi.aliyun.com/qianwen)。
|
||||
|
||||
选择器说明(如页面改版需更新):
|
||||
- 输入框:#search-input 或 textarea[class*="input"](textarea)
|
||||
- 发送按钮:button[class*="send"] 或 [aria-label="发送"]
|
||||
- 停止生成:button[class*="stop"] 或 [aria-label="停止"]
|
||||
- 复制按钮:最后一条回复下的 [class*="copy"] 或 [aria-label="复制"]
|
||||
"""
|
||||
import asyncio
|
||||
from .base import BaseEngine
|
||||
|
||||
|
||||
class QianwenEngine(BaseEngine):
|
||||
async def generate(self, prompt: str) -> str:
|
||||
await self.page.goto("https://tongyi.aliyun.com/qianwen/")
|
||||
await self.page.wait_for_load_state("networkidle")
|
||||
|
||||
# 登录检测
|
||||
input_selectors = [
|
||||
"#search-input",
|
||||
"textarea[class*='input']",
|
||||
"div[class*='input'][contenteditable='true']",
|
||||
]
|
||||
editor = None
|
||||
for sel in input_selectors:
|
||||
try:
|
||||
loc = self.page.locator(sel).first
|
||||
await loc.wait_for(state="visible", timeout=4000)
|
||||
editor = loc
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if editor is None:
|
||||
return "ERROR:REQUIRE_LOGIN"
|
||||
|
||||
# 输入提示词
|
||||
await editor.click()
|
||||
await self.page.keyboard.insert_text(prompt)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 发送
|
||||
sent = False
|
||||
for sel in (
|
||||
'button[class*="send"]',
|
||||
'[aria-label="发送"]',
|
||||
'button[type="submit"]',
|
||||
):
|
||||
try:
|
||||
btn = self.page.locator(sel).first
|
||||
if await btn.is_visible(timeout=1000):
|
||||
await btn.click()
|
||||
sent = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not sent:
|
||||
await self.page.keyboard.press("Enter")
|
||||
|
||||
print("💡 [llm-manager/qianwen] 已发送提示词,等待通义千问生成响应...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 等待生成完毕
|
||||
stop_selectors = ['button[class*="stop"]', '[aria-label="停止"]', '[aria-label="停止生成"]']
|
||||
deadline = asyncio.get_event_loop().time() + 150
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
stop_visible = False
|
||||
for sel in stop_selectors:
|
||||
try:
|
||||
if await self.page.locator(sel).first.is_visible(timeout=300):
|
||||
stop_visible = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not stop_visible:
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 取结果
|
||||
try:
|
||||
copy_btn = self.page.locator(
|
||||
'[aria-label="复制"], [class*="copy-btn"], button:has(svg[class*="copy"])'
|
||||
).last
|
||||
await copy_btn.click()
|
||||
await asyncio.sleep(0.5)
|
||||
result = await self.page.evaluate("navigator.clipboard.readText()")
|
||||
return result.strip()
|
||||
except Exception as e:
|
||||
return f"ERROR:抓取通义千问内容时异常:{e}"
|
||||
91
llm-manager/scripts/engines/yiyan.py
Normal file
91
llm-manager/scripts/engines/yiyan.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
文心一言网页版驱动引擎(yiyan.baidu.com)。
|
||||
|
||||
选择器说明(如页面改版需更新):
|
||||
- 输入框:[class*="editor"] 或 div[contenteditable="true"](富文本编辑器)
|
||||
- 发送按钮:[class*="send-btn"] 或 [aria-label="发送"]
|
||||
- 停止生成:[class*="stop"] 相关按钮
|
||||
- 复制按钮:最后一条回复下的复制按钮
|
||||
"""
|
||||
import asyncio
|
||||
from .base import BaseEngine
|
||||
|
||||
|
||||
class YiyanEngine(BaseEngine):
|
||||
async def generate(self, prompt: str) -> str:
|
||||
await self.page.goto("https://yiyan.baidu.com")
|
||||
await self.page.wait_for_load_state("networkidle")
|
||||
|
||||
# 登录检测
|
||||
input_selectors = [
|
||||
"div[class*='editor'][contenteditable='true']",
|
||||
"textarea[class*='input']",
|
||||
"[contenteditable='true']",
|
||||
]
|
||||
editor = None
|
||||
for sel in input_selectors:
|
||||
try:
|
||||
loc = self.page.locator(sel).first
|
||||
await loc.wait_for(state="visible", timeout=4000)
|
||||
editor = loc
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if editor is None:
|
||||
return "ERROR:REQUIRE_LOGIN"
|
||||
|
||||
# 输入提示词
|
||||
await editor.click()
|
||||
await self.page.keyboard.insert_text(prompt)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 发送
|
||||
sent = False
|
||||
for sel in (
|
||||
'[class*="send-btn"]',
|
||||
'[aria-label="发送"]',
|
||||
'button[class*="send"]',
|
||||
):
|
||||
try:
|
||||
btn = self.page.locator(sel).first
|
||||
if await btn.is_visible(timeout=1000):
|
||||
await btn.click()
|
||||
sent = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not sent:
|
||||
await self.page.keyboard.press("Enter")
|
||||
|
||||
print("💡 [llm-manager/yiyan] 已发送提示词,等待文心一言生成响应...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 等待生成完毕
|
||||
stop_selectors = ['[class*="stop"]', '[aria-label="停止"]', '[aria-label="停止生成"]']
|
||||
deadline = asyncio.get_event_loop().time() + 150
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
stop_visible = False
|
||||
for sel in stop_selectors:
|
||||
try:
|
||||
if await self.page.locator(sel).first.is_visible(timeout=300):
|
||||
stop_visible = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not stop_visible:
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 取结果
|
||||
try:
|
||||
copy_btn = self.page.locator(
|
||||
'[aria-label="复制"], [class*="copy"], button:has(svg[class*="copy"])'
|
||||
).last
|
||||
await copy_btn.click()
|
||||
await asyncio.sleep(0.5)
|
||||
result = await self.page.evaluate("navigator.clipboard.readText()")
|
||||
return result.strip()
|
||||
except Exception as e:
|
||||
return f"ERROR:抓取文心一言内容时异常:{e}"
|
||||
94
llm-manager/scripts/engines/yuanbao.py
Normal file
94
llm-manager/scripts/engines/yuanbao.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
腾讯元宝网页版驱动引擎(yuanbao.tencent.com)。
|
||||
元宝暂无公开 API,仅支持网页模式。
|
||||
|
||||
选择器说明(如页面改版需更新):
|
||||
- 输入框:[class*="input-area"] textarea 或 [contenteditable="true"]
|
||||
- 发送按钮:[class*="send-btn"] 或 [aria-label="发送"]
|
||||
- 停止生成:[class*="stop"] 相关按钮
|
||||
- 复制按钮:最后一条回复下的复制按钮
|
||||
"""
|
||||
import asyncio
|
||||
from .base import BaseEngine
|
||||
|
||||
|
||||
class YuanbaoEngine(BaseEngine):
|
||||
async def generate(self, prompt: str) -> str:
|
||||
await self.page.goto("https://yuanbao.tencent.com/chat")
|
||||
await self.page.wait_for_load_state("networkidle")
|
||||
|
||||
# 登录检测
|
||||
input_selectors = [
|
||||
"textarea[class*='input']",
|
||||
"div[class*='input-area'] textarea",
|
||||
"[contenteditable='true']",
|
||||
"textarea",
|
||||
]
|
||||
editor = None
|
||||
for sel in input_selectors:
|
||||
try:
|
||||
loc = self.page.locator(sel).first
|
||||
await loc.wait_for(state="visible", timeout=4000)
|
||||
editor = loc
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if editor is None:
|
||||
return "ERROR:REQUIRE_LOGIN"
|
||||
|
||||
# 输入提示词
|
||||
await editor.click()
|
||||
await self.page.keyboard.insert_text(prompt)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 发送
|
||||
sent = False
|
||||
for sel in (
|
||||
'[class*="send-btn"]',
|
||||
'[aria-label="发送"]',
|
||||
'button[class*="send"]',
|
||||
'button[type="submit"]',
|
||||
):
|
||||
try:
|
||||
btn = self.page.locator(sel).first
|
||||
if await btn.is_visible(timeout=1000):
|
||||
await btn.click()
|
||||
sent = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not sent:
|
||||
await self.page.keyboard.press("Enter")
|
||||
|
||||
print("💡 [llm-manager/yuanbao] 已发送提示词,等待腾讯元宝生成响应...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 等待生成完毕
|
||||
stop_selectors = ['[class*="stop"]', '[aria-label="停止"]', '[aria-label="停止生成"]']
|
||||
deadline = asyncio.get_event_loop().time() + 150
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
stop_visible = False
|
||||
for sel in stop_selectors:
|
||||
try:
|
||||
if await self.page.locator(sel).first.is_visible(timeout=300):
|
||||
stop_visible = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not stop_visible:
|
||||
break
|
||||
await asyncio.sleep(2)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# 取结果
|
||||
try:
|
||||
copy_btn = self.page.locator(
|
||||
'[aria-label="复制"], [class*="copy"], button:has(svg[class*="copy"])'
|
||||
).last
|
||||
await copy_btn.click()
|
||||
await asyncio.sleep(0.5)
|
||||
result = await self.page.evaluate("navigator.clipboard.readText()")
|
||||
return result.strip()
|
||||
except Exception as e:
|
||||
return f"ERROR:抓取腾讯元宝内容时异常:{e}"
|
||||
Reference in New Issue
Block a user