Add OpenClaw skills, platform kit, and template docs

Made-with: Cursor
This commit is contained in:
2026-04-04 10:35:02 +08:00
parent e37b03c00f
commit 35f4758da2
83 changed files with 8971 additions and 0 deletions

View 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