feat: 新增纯坐标模式开关与文本输入优化

1. 新增WOC_DISABLE_OPENCV环境变量支持纯坐标定位模式
2. 优化xdotool文本输入:替换换行符为空格、添加30ms输入延迟
3. 新增输入框清空逻辑,避免内容残留
This commit is contained in:
Kris 2026-07-18 05:23:06 +08:00
parent be93906635
commit 3effe1338c
8 changed files with 71 additions and 1106 deletions

View File

@ -31,6 +31,11 @@ _STEP_MIN_TIMEOUT_SEC = 7.0
_FLOW_TIMEOUT_SEC = 30.0
_HTTP_TIMEOUT_SEC = 60.0
# xdotool type 每字符间隔(毫秒):默认值 12ms 对中文 IME 过快,
# 容易导致字符丢失;提高到 30ms 给微信 UI 足够处理时间。
# 可通过环境变量 WOC_TYPE_DELAY_MS 覆盖。
_TYPE_DELAY_MS = int(os.environ.get("WOC_TYPE_DELAY_MS", "30"))
class XdotoolBackend(BackendProtocol):
"""xdotool + scrot 后端实现。
@ -170,15 +175,27 @@ class XdotoolBackend(BackendProtocol):
)
async def type_text(self, text: str) -> None:
"""xdotool type -- <text>:原样输入文本,不解析修饰键组合。"""
"""xdotool type --delay <ms> -- <text>:原样输入文本,不解析修饰键组合。
使用 --delay 控制每字符间隔避免中文 IME 处理不及导致字符丢失
"""
text_len = len(text)
logger.info(
"[ui-backend] type_text start len=%d preview=%r",
text_len, text[:30],
"[ui-backend] type_text start len=%d delay=%dms preview=%r",
text_len, _TYPE_DELAY_MS, text[:30],
)
t0 = time.perf_counter()
async with self._ui_lock:
await self._run(["xdotool", "type", "--", text])
await self._run(
[
"xdotool",
"type",
"--delay",
str(_TYPE_DELAY_MS),
"--",
text,
]
)
logger.info(
"[ui-backend] type_text done len=%d (%.0fms)",
text_len, (time.perf_counter() - t0) * 1000,

View File

@ -164,11 +164,20 @@ class ContactDriver(BaseDriver):
微信 4.x Linux 自绘 UI 不响应 Ctrl+V改用 xdotool type 逐字符输入
方法名保留 _paste_via_xclip 以维持调用点稳定
注意
- \n / \r 替换为空格避免 xdotool 把换行解析为 Return
- 使用 --delay 30ms 降低输入速度减少中文 IME 字符丢失
Raises:
BridgeError(SEND_FAILED): xdotool type 执行失败
"""
normalized = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
normalized = " ".join(normalized.split())
if normalized != text:
logger.info("[ui] type text normalized: original_len=%d len=%d", len(text), len(normalized))
text = normalized
logger.info("[ui] type text: len=%d", len(text))
rc, _, stderr = await self._run(["xdotool", "type", text])
rc, _, stderr = await self._run(["xdotool", "type", "--delay", "30", "--", text])
if rc != 0:
err = stderr.decode(errors="ignore").strip() if stderr else "unknown"
raise BridgeError(

View File

@ -117,11 +117,21 @@ class MessageDriver(BaseDriver):
xdotool type逐字符 XSendEvent能正常输入中英文因此改用
xdotool type 直接打字方法名保留 _paste_via_xclip 以维持调用点稳定
注意
- \n / \r 替换为空格避免 xdotool 把换行解析为 Return 键触发发送
- 使用 --delay 30ms 降低输入速度减少中文 IME 字符丢失
Raises:
BridgeError(SEND_FAILED): xdotool type 执行失败
"""
# 规范化:去除换行,避免 Return 误触发
normalized = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
normalized = " ".join(normalized.split())
if normalized != text:
logger.info("[ui] type text normalized: original_len=%d len=%d", len(text), len(normalized))
text = normalized
logger.info("[ui] type text: len=%d", len(text))
rc, _, stderr = await self._run(["xdotool", "type", text])
rc, _, stderr = await self._run(["xdotool", "type", "--delay", "30", "--", text])
if rc != 0:
err = stderr.decode(errors="ignore").strip() if stderr else "unknown"
raise BridgeError(

View File

@ -92,11 +92,20 @@ class MomentDriver(BaseDriver):
微信 4.x Linux 自绘 UI 不响应 Ctrl+V改用 xdotool type 逐字符输入
方法名保留 _paste_via_xclip 以维持调用点稳定
注意
- \n / \r 替换为空格避免 xdotool 把换行解析为 Return
- 使用 --delay 30ms 降低输入速度减少中文 IME 字符丢失
Raises:
BridgeError(SEND_FAILED): xdotool type 执行失败
"""
normalized = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
normalized = " ".join(normalized.split())
if normalized != text:
logger.info("[ui] type text normalized: original_len=%d len=%d", len(text), len(normalized))
text = normalized
logger.info("[ui] type text: len=%d", len(text))
rc, _, stderr = await self._run(["xdotool", "type", text])
rc, _, stderr = await self._run(["xdotool", "type", "--delay", "30", "--", text])
if rc != 0:
err = stderr.decode(errors="ignore").strip() if stderr else "unknown"
raise BridgeError(

View File

@ -270,19 +270,29 @@ class SendTextFlow(Flow):
async def _type_content(self, ctx: FlowContext) -> None:
"""输入消息内容。"""
t0 = time.perf_counter()
# 清理开头/结尾的空白和换行,避免输入框视觉为空、发送按钮灰色
# 1. 清理首尾空白
cleaned_content = ctx.content.strip()
if cleaned_content != ctx.content:
# 2. 把换行符(含 \r\n / \n / \r替换为空格避免 xdotool type 把 \n
# 解析为 Return 键,导致消息在输入过程中被提前分段发送。
# 微信输入框本身支持 Shift+Enter 换行,但 xdotool type 无法区分。
normalized_content = cleaned_content.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
# 3. 合并连续空格为一个,保持内容整洁
normalized_content = " ".join(normalized_content.split())
if normalized_content != ctx.content:
logger.info(
"[flow:send_text] step=type_content stripped leading/trailing whitespace: "
"[flow:send_text] step=type_content normalized: "
"original_len=%d cleaned_len=%d",
len(ctx.content), len(cleaned_content),
len(ctx.content), len(normalized_content),
)
ctx.content = cleaned_content
ctx.content = normalized_content
logger.info(
"[flow:send_text] step=type_content start content_len=%d",
len(ctx.content),
)
# 4. 输入前清空输入框,避免上次残留内容与本次内容混合
await self.actions.backend.key_press("ctrl+a")
await self.actions.backend.key_press("Delete")
await asyncio.sleep(0.05)
await self.actions.backend.type_text(ctx.content)
ctx.current_state = FlowState.TEXT_TYPED
logger.info(

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,9 @@ services:
- WOC_AUTO_ACCEPT_POLL_INTERVAL=${WOC_AUTO_ACCEPT_POLL_INTERVAL:-3}
- WOC_UI_BACKEND=${WOC_UI_BACKEND:-flow}
- WOC_UI_DEBUG_SHOTS=${WOC_UI_DEBUG_SHOTS:-false}
# 纯坐标模式:=1 时 bridge 跳过 OpenCV 模板匹配,直接用窗口比例坐标定位 UI 元素。
# 适用于模板图像不可靠的场景,避免假匹配/假阳性。留空=用 OpenCV需正确模板
- WOC_DISABLE_OPENCV=${WOC_DISABLE_OPENCV:-1}
volumes:
# 面板账号数据(用户、实例元信息、密码哈希)

View File

@ -201,6 +201,7 @@ function envList(inst: Instance): string[] {
'WOC_AUTO_ACCEPT_POLL_INTERVAL',
'WOC_UI_BACKEND',
'WOC_UI_DEBUG_SHOTS',
'WOC_DISABLE_OPENCV',
]) {
const v = process.env[k];
if (v) env.push(`${k}=${v}`);