[{"content":" 在平时处理影刀RPA问题的时候，会找到一些好用的技巧，在这篇文章中做个记录，也方便以后自己查找。\n网页 获取网页弹窗（JavaScript Alerts）的内容 测试页面（需要翻墙）\n类似下图这样的弹窗，这种事 JS 原生的 Alert 弹窗，不再 HTML 标签里，无法选中弹窗的 DOM\n对于获取这类弹窗的内容，可以用以下思路解决\n向页面注入JS劫持 window.alert 方法，将 msg 保存到全局变量 RPA点击按钮触发弹窗，然后关闭弹窗 向页面注入JS，读取全局变量中的 msg 劫持 windw.alert 的 js\nfunction (element, input) { // 保存原始 alert window._originalAlert = window.alert; // 重写 alert，劫持弹窗内容 window.alert = function (msg) { window._lastAlertMsg = msg; // 存到全局变量 console.log(\u0026#34;捕获到弹窗：\u0026#34;, msg); // 调试打印 window._originalAlert(msg); // 继续弹出原弹窗 }; return null; } 读取页面全局变量的 js\nfunction (element, input) { // 读取劫持到的弹窗文本 let msg = window._lastAlertMsg || \u0026#34;\u0026#34;; console.log(\u0026#34;最终弹窗内容：\u0026#34;, msg); return msg; } 在影刀中的使用如下图\n","permalink":"https://blog.levitan.top/posts/shadowbot-notes/","summary":"\u003cblockquote\u003e\n\u003cp\u003e在平时处理影刀RPA问题的时候，会找到一些好用的技巧，在这篇文章中做个记录，也方便以后自己查找。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"网页\"\u003e网页\u003c/h1\u003e\n\u003ch2 id=\"获取网页弹窗javascript-alerts的内容\"\u003e获取网页弹窗（JavaScript Alerts）的内容\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://the-internet.herokuapp.com/javascript_alerts\"\u003e测试页面（需要翻墙）\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e类似下图这样的弹窗，这种事 JS 原生的 Alert 弹窗，不再 HTML 标签里，无法选中弹窗的 DOM\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/posts/shadowbot-notes/images/javaScript-Alerts.png\"\u003e\u003c/p\u003e\n\u003cp\u003e对于获取这类弹窗的内容，可以用以下思路解决\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e向页面注入JS劫持 \u003ccode\u003ewindow.alert\u003c/code\u003e 方法，将 msg 保存到全局变量\u003c/li\u003e\n\u003cli\u003eRPA点击按钮触发弹窗，然后关闭弹窗\u003c/li\u003e\n\u003cli\u003e向页面注入JS，读取全局变量中的 msg\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e劫持 \u003ccode\u003ewindw.alert\u003c/code\u003e 的 js\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eelement\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003einput\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 保存原始 alert\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    window.\u003cspan style=\"color:#a6e22e\"\u003e_originalAlert\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e window.\u003cspan style=\"color:#a6e22e\"\u003ealert\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 重写 alert，劫持弹窗内容\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    window.\u003cspan style=\"color:#a6e22e\"\u003ealert\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003emsg\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        window.\u003cspan style=\"color:#a6e22e\"\u003e_lastAlertMsg\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emsg\u003c/span\u003e;       \u003cspan style=\"color:#75715e\"\u003e// 存到全局变量\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003econsole\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elog\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;捕获到弹窗：\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003emsg\u003c/span\u003e);   \u003cspan style=\"color:#75715e\"\u003e// 调试打印\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        window.\u003cspan style=\"color:#a6e22e\"\u003e_originalAlert\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003emsg\u003c/span\u003e);       \u003cspan style=\"color:#75715e\"\u003e// 继续弹出原弹窗\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    };\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e读取页面全局变量的 js\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eelement\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003einput\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 读取劫持到的弹窗文本\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003elet\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emsg\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e window.\u003cspan style=\"color:#a6e22e\"\u003e_lastAlertMsg\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e||\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003econsole\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003elog\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;最终弹窗内容：\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003emsg\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emsg\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e在影刀中的使用如下图\u003c/p\u003e","title":"影刀使用小记"},{"content":"1. 前言 最近在公司做了个内部提效的飞书机器人，公司有台公共的mac mini，之前一直是手动打包好代码上传到mac mini上用 uv run python main.py 运行，可以说是非常不优雅了。\n以前都是在 Linux 上用 systemctl 部署服务，没在 mac 弄过，查了一下 mac 系统上有个 launchd 用于管理服务，这篇文档记录一下第一次在 mac 上用 launchd 部署服务\n2. launchctl 与 systemctl 的区别 功能 systemctl launchctl 主要配置文件 .service .plist 启动服务 systemctl start [name] launchctl bootstrap gui/$(id -u) [path] 停止服务 systemctl stop [name] launchctl bootout gui/$(id -u) [path] 开机自启 systemctl enable [name] 只要 .plist 文件放到特定目录，默认加载 查看状态 systemctl status [name] launchctl list [label] 服务配置文件位置\nmacOS 使用 Property List (.plist) 文件来定义服务。根据你想让服务以什么权限运行，存放路径也不同：\n用户级 Agent (~/Library/LaunchAgents): 只有当前用户登录后才运行，以该用户权限运行。\n系统级 Agent (/Library/LaunchAgents): 任何用户登录后都会运行。\n系统级 Daemon (/Library/LaunchDaemons): 开机即运行，通常以 root 权限运行（适合 Web 服务器、数据库等）。\n3. 配置文件的编写 创建 ~/Library/LaunchAgents/com.yingdao-fj.cyber-monkey.plist 文件，写入以下内容。\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE plist PUBLIC \u0026#34;-//Apple//DTD PLIST 1.0//EN\u0026#34; \u0026#34;http://www.apple.com/DTDs/PropertyList-1.0.dtd\u0026#34;\u0026gt; \u0026lt;plist version=\u0026#34;1.0\u0026#34;\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;Label\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;com.yingdao-fj.cyber-monkey\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;ProgramArguments\u0026lt;/key\u0026gt; \u0026lt;array\u0026gt; \u0026lt;string\u0026gt;/opt/homebrew/bin/uv\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;run\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;python\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;main.py\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;--config\u0026lt;/string\u0026gt; \u0026lt;string\u0026gt;/Users/shadowbotfj/.cyber-monkey/config.yaml\u0026lt;/string\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;key\u0026gt;WorkingDirectory\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/Users/shadowbotfj/project/cyber-monkey\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;RunAtLoad\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;KeepAlive\u0026lt;/key\u0026gt; \u0026lt;true/\u0026gt; \u0026lt;key\u0026gt;StandardOutPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/dev/null\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;StandardErrorPath\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/dev/null\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;EnvironmentVariables\u0026lt;/key\u0026gt; \u0026lt;dict\u0026gt; \u0026lt;key\u0026gt;PATH\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin\u0026lt;/string\u0026gt; \u0026lt;key\u0026gt;PYTHONUNBUFFERED\u0026lt;/key\u0026gt; \u0026lt;string\u0026gt;1\u0026lt;/string\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/dict\u0026gt; \u0026lt;/plist\u0026gt; ","permalink":"https://blog.levitan.top/posts/mac-launchctl-deploy/","summary":"\u003ch1 id=\"1-前言\"\u003e1. 前言\u003c/h1\u003e\n\u003cp\u003e最近在公司做了个内部提效的飞书机器人，公司有台公共的mac mini，之前一直是手动打包好代码上传到mac mini上用 \u003ccode\u003euv run python main.py\u003c/code\u003e 运行，可以说是非常不优雅了。\u003c/p\u003e\n\u003cp\u003e以前都是在 Linux 上用 systemctl 部署服务，没在 mac 弄过，查了一下 mac 系统上有个 launchd 用于管理服务，这篇文档记录一下第一次在 mac 上用 launchd 部署服务\u003c/p\u003e\n\u003ch1 id=\"2-launchctl-与-systemctl-的区别\"\u003e2. launchctl 与 systemctl 的区别\u003c/h1\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e功能\u003c/th\u003e\n          \u003cth\u003esystemctl\u003c/th\u003e\n          \u003cth\u003elaunchctl\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e主要配置文件\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e.service\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003e.plist\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e启动服务\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003esystemctl start [name]\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003elaunchctl bootstrap gui/$(id -u) [path]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e停止服务\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003esystemctl stop [name]\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003elaunchctl bootout gui/$(id -u) [path]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e开机自启\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003esystemctl enable [name]\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e只要 .plist 文件放到特定目录，默认加载\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e查看状态\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003esystemctl status [name]\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003elaunchctl list [label]\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e服务配置文件位置\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003emacOS 使用 Property List (.plist) 文件来定义服务。根据你想让服务以什么权限运行，存放路径也不同：\u003c/p\u003e","title":"在 Mac 上使用 launchtcl 部署服务"},{"content":" OpenClaw 最近很火，正好公司有台空闲的 Mac Mini，可以在上面捣鼓一下这个新东西。\n1. 安装 Lume Lume 是 MacOS 上的原生虚拟化框架，让 OpenClaw 在虚拟环境下运行不会搞乱我们的系统。\n1.1 安装 Lume 方式一：脚本安装\n/bin/bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)\u0026#34; 方式二：Homebrew 安装\nbrew tap trycua/lume brew install lume 验证安装\n使用以下命令验证是否安装成功：\nlume --version 1.2 下载 MacOS 镜像 lume 安装成功后，需要获取 MacOS 镜像，使用下面指令将镜像下载到 Downloads 文件夹中：\ncurl -L -o ~/Downloads/macos.ipsw \u0026#34;$(lume ipsw 2\u0026gt;/dev/null | tail -1)\u0026#34; 1.3 运行虚拟机 使用下面指令即可运行虚拟机\nlume create openclaw --os macos --ipsw ~/Downloads/macos.ipsw 2. 安装 OpenClaw openclaw 中文文档\nopenclaw 的安装方式在其官方文档中已经很清楚了，我这里只列出几个我安装时遇到的问题\n2.1 踩坑记录 2.1.1 对接飞书频道 安装飞书插件\nopenclaw plugins install @openclaw/feishu 使用下面命令配置飞书通道\nopenclaw channels add 2.1.2 配置其他模型 在 ～/.openclaw/openclaw.json 文件中添加 models 块\n\u0026#34;models\u0026#34;: { \u0026#34;providers\u0026#34;: { // 这里配置供应商 \u0026#34;bailian\u0026#34;: { \u0026#34;baseUrl\u0026#34;: \u0026#34;https://coding.dashscope.aliyuncs.com/v1\u0026#34;,\t// 供应商接口链接 \u0026#34;apiKey\u0026#34;: \u0026#34;sk-sp-xxxxxxxxx\u0026#34;, \u0026#34;api\u0026#34;: \u0026#34;openai-completions\u0026#34;, \u0026#34;models\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;qwen3.5-plus\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;qwen3.5-plus\u0026#34;, \u0026#34;reasoning\u0026#34;: false, \u0026#34;input\u0026#34;: [ \u0026#34;text\u0026#34;, \u0026#34;image\u0026#34;\t// 这里添加 image 才有办法识别图片 ] } ] } } } 添加完 models 块后，在修改 agents 块\n\u0026#34;agents\u0026#34;: { \u0026#34;defaults\u0026#34;: { \u0026#34;model\u0026#34;: { \u0026#34;primary\u0026#34;: \u0026#34;bailian/qwen3.5-plus\u0026#34; // mdels中配置的供应商名称和模型名称 } } } 最后执行 openclaw gateway restart 即可生效\n2.1.3 配置浏览器 默认情况下 openclaw 会尝试使用扩展中继的方式控制用户的浏览器，这会导致运行不顺畅，所以直接让openclaw使用自托管的方式去运行更方便。\n\u0026#34;browser\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;headless\u0026#34;: false, \u0026#34;noSandbox\u0026#34;: false, \u0026#34;attachOnly\u0026#34;: false, \u0026#34;defaultProfile\u0026#34;: \u0026#34;openclaw\u0026#34; } 3. 调优 根据 OpenClaw 的官方文档，OpenClaw 有一些可以优化的地方，这里总结一下\n需要注意的是，下面调优的参数都是和使用的模型有关\n3.1 历史的工具调用结果裁剪 可以让 OpenClaw 在请求发到 LLM 之前对历史的工具调用结果进行裁剪\n裁剪有 硬裁剪 和 软裁剪 两种\n硬裁剪： 直接把工具结果替换为 hardClear.placeholder 中的内容\n软裁剪： 只正对过大的工具结果，并且保留开头和结尾，在中间插入 hardClear.placeholder 中的内容\n配置示例：\n{ \u0026#34;agents\u0026#34;: { \u0026#34;defaults\u0026#34;: { // 工具裁剪配置 \u0026#34;contextPruning\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;cache-ttl\u0026#34;, // 有 off 和 cache-ttl \u0026#34;ttl\u0026#34;: \u0026#34;1h\u0026#34;, // tool result 保留时间，单位 ms/s/m/h，默认是m \u0026#34;keepLastAssistants\u0026#34;: 5, // 保留最近几条工具调用信息 \u0026#34;softTrimRatio\u0026#34;: 0.1, // 触发软裁剪时，尝试清理的目标比例 \u0026#34;hardClearRatio\u0026#34;: 0.3, // 触发硬裁剪是，尝试清理的目标比例 \u0026#34;minPrunableToolChars\u0026#34;: 50000, // 单个工具结果超过多少字符才会进入可裁剪名单 \u0026#34;softTrim\u0026#34;: { \u0026#34;maxChars\u0026#34;: 4000, // 最多保留字符书 \u0026#34;headChars\u0026#34;: 1500, // 开头保留的字符数 \u0026#34;tailChars\u0026#34;: 1500 // 结尾保留的字符数 }, \u0026#34;hardClear\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;placeholder\u0026#34;: \u0026#34;[Old tool result content cleared]\u0026#34; // 硬裁剪时的占位符 }, // 永远不执行 pruning 的工具 \u0026#34;tools\u0026#34;: { \u0026#34;deny\u0026#34;: [\u0026#34;browser\u0026#34;, \u0026#34;canvas\u0026#34;] }, }, }, }, } 3.2 记忆压缩 当模型的上下文达到阈值时压缩较早的历史记录。\n另外openclaw还支持配置专门的模型做压缩动作\n配置示例：\n{ \u0026#34;agents\u0026#34;: { \u0026#34;defaults\u0026#34;: { \u0026#34;compaction\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;safeguard\u0026#34;, // default | safeguard \u0026#34;reserveTokensFloor\u0026#34;: 24000, // 保证在 compaction 前预留的token空间 // 压缩时对关键ID的保护策略，strict严格保护（保留原样）；off 不保护； custom 使用自己规则 \u0026#34;identifierPolicy\u0026#34;: \u0026#34;strict\u0026#34;, // strict | off | custom \u0026#34;identifierInstructions\u0026#34;: \u0026#34;Preserve deployment IDs, ticket IDs, and host:port pairs exactly.\u0026#34;, // used when identifierPolicy=custom // 在 compaction 后重新注入一些关键提示词 \u0026#34;postCompactionSections\u0026#34;: [\u0026#34;Session Startup\u0026#34;, \u0026#34;Red Lines\u0026#34;], // [] disables reinjection // 压缩专用模型配置 \u0026#34;model\u0026#34;: \u0026#34;openrouter/anthropic/claude-sonnet-4-5\u0026#34;, // optional compaction-only model override \u0026#34;memoryFlush\u0026#34;: { \u0026#34;enabled\u0026#34;: true, // compaction 前多少 token 触发 memory flush // 触发条件：session_tokens \u0026gt; context_window - reserveTokensFloor - softThresholdTokens \u0026#34;softThresholdTokens\u0026#34;: 6000, \u0026#34;systemPrompt\u0026#34;: \u0026#34;Session nearing compaction. Store durable memories now.\u0026#34;, \u0026#34;prompt\u0026#34;: \u0026#34;Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.\u0026#34;, }, }, }, }, } ","permalink":"https://blog.levitan.top/posts/openclaw/","summary":"\u003cblockquote\u003e\n\u003cp\u003eOpenClaw 最近很火，正好公司有台空闲的 Mac Mini，可以在上面捣鼓一下这个新东西。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"1-安装-lume\"\u003e1. 安装 Lume\u003c/h1\u003e\n\u003cp\u003eLume 是 MacOS 上的原生虚拟化框架，让 OpenClaw 在虚拟环境下运行不会搞乱我们的系统。\u003c/p\u003e\n\u003ch2 id=\"11-安装-lume\"\u003e1.1 安装 Lume\u003c/h2\u003e\n\u003cp\u003e方式一：脚本安装\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e/bin/bash -c \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e$(\u003c/span\u003ecurl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh\u003cspan style=\"color:#66d9ef\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e方式二：Homebrew 安装\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ebrew tap trycua/lume\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ebrew install lume\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e验证安装\u003c/p\u003e\n\u003cp\u003e使用以下命令验证是否安装成功：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elume --version\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"12-下载-macos-镜像\"\u003e1.2 下载 MacOS 镜像\u003c/h2\u003e\n\u003cp\u003elume 安装成功后，需要获取 MacOS 镜像，使用下面指令将镜像下载到 Downloads 文件夹中：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -L -o ~/Downloads/macos.ipsw \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#66d9ef\"\u003e$(\u003c/span\u003elume ipsw 2\u0026gt;/dev/null | tail -1\u003cspan style=\"color:#66d9ef\"\u003e)\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"13-运行虚拟机\"\u003e1.3 运行虚拟机\u003c/h2\u003e\n\u003cp\u003e使用下面指令即可运行虚拟机\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elume create openclaw --os macos --ipsw ~/Downloads/macos.ipsw\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"2-安装-openclaw\"\u003e2. 安装 OpenClaw\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://docs.openclaw.ai/zh-CN\"\u003eopenclaw 中文文档\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eopenclaw 的安装方式在其官方文档中已经很清楚了，我这里只列出几个我安装时遇到的问题\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"21-踩坑记录\"\u003e2.1 踩坑记录\u003c/h2\u003e\n\u003ch3 id=\"211-对接飞书频道\"\u003e2.1.1 对接飞书频道\u003c/h3\u003e\n\u003cp\u003e安装飞书插件\u003c/p\u003e","title":"使用 OpenClaw + Lume 搭建 MacOS 自动化环境"},{"content":"1. 前言 在之前尝试过 AI 驱动的RPA程序完成给定的任务，但是整个运行下来给人的感觉就是太慢了，当时跑一个淘宝搜索产品并获取产品信息的任务跑了快10分钟，总结下来慢的原因无非下面几点。\nAI返回内容慢，通过OpenRouter调用gemini模型，一个请求要1-2分钟才能返回结果。 要实现程序 感知-决策-在感知-判断 就要调用多次模型，模型调用次数越多就越慢。 所以，之前AI驱动RPA属于对技术的探索，但没有实际意义， 这次我尝试用AI来处理在自动化中会遇到的一些复杂验证码问题\n下面是这次需要解决的验证码的例子，这三个验证码都来自Temu平台。\n2. 核心思路 整体思路很简单，把验证码截图给AI，让AI按要求返回坐标，程序解析坐标并点击\n整体的代码设计如下图，首先是三个抽象类下面逐个解释一下。\nAgent：用于调用AI接口，封装了图片解析、AI结果处理等功能，需要子类实现具体的AI接口\nCapability：表示一种能力，就是给AI的提示词。\nHandle：使用AI返回的结果处理问题\nAgent有3个子类，分别是调用OpenAI规范的接口、调用影刀内置AI接口和调用影刀AIPower接口。\nCapability的3个子类分别表示点选验证码处理、拖动验证码处理以及复选框处理\n在做软件自动化时很容易遇到复选框无法判断是否勾选，这种情况就可以借助AI\nHandle有2个子类分表用于，处理点击操作和处理拖动操作\n3. 代码实现 ability.py\nfrom abc import ABC, abstractmethod class Capability(ABC): \u0026#34;\u0026#34;\u0026#34; 表示一种能力, AI能做什么操作 \u0026#34;\u0026#34;\u0026#34; @property @abstractmethod def prompt(self): pass class ClickVerifier(Capability): @property def prompt(self): prompt = \u0026#34;\u0026#34;\u0026#34; 请严格按照图片中给出的操作要求执行。 图片中已经明确标注了需要点击的目标及点击顺序， 你只需要根据图片内容，依次返回每一步需要点击的位置坐标。 规则： 1. 只返回图片中明确要求点击的内容，不要增加或省略步骤 2. 坐标基于当前输入图片的像素坐标 3. 坐标原点为图片左上角 (0, 0)，x 向右，y 向下 4. 每个坐标应尽量位于对应目标的可点击区域中心 5. 如果图片中某一步无法明确定位，请不要猜测，在 summary 中说明 输出要求： - 仅返回 JSON，不要输出任何多余文本 - 不要使用 markdown - JSON 结构必须严格如下： { \u0026#34;positions\u0026#34;: [ { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx }, { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx } ], \u0026#34;summary\u0026#34;: \u0026#34;简要说明这些坐标如何对应图片中的点击要求\u0026#34; } \u0026#34;\u0026#34;\u0026#34; return prompt class DragVerifier(Capability): @property def prompt(self): prompt = \u0026#34;\u0026#34;\u0026#34; 图片中已经明确标注了需要如何拖拽内容 你只需要根据图片内容，依次返回开始拖拽和结束拖拽的位置坐标 规则： 1. 坐标基于当前输入图片的像素坐标 2. 坐标原点为图片左上角 (0, 0)，x 向右，y 向下 3. 每个坐标应尽量位于对应目标的可点击区域中心 4. 如果图片中某一步无法明确定位，请不要猜测，在 summary 中说明 输出要求： - 仅返回 JSON，不要输出任何多余文本 - 不要使用 markdown - JSON 结构必须严格如下： { \u0026#34;positions\u0026#34;: [ { \u0026#34;start\u0026#34;: { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx, }, \u0026#34;end\u0026#34;: { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx, } }, { \u0026#34;start\u0026#34;: { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx, }, \u0026#34;end\u0026#34;: { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx, } }, ], \u0026#34;summary\u0026#34;: \u0026#34;简要说明理由\u0026#34; } \u0026#34;\u0026#34;\u0026#34; return prompt class CheckboxProcess(Capability): @property def prompt(self): prompt = \u0026#34;\u0026#34;\u0026#34; 给你一张界面截图，你需要根据任务要求返回需要点击的复选框坐标 如果一个复选框已经被点击则跳过该复选框 规则 1. 坐标基于当前输入图片的像素坐标 2. 坐标原点为图片左上角 (0, 0)，x 向右，y 向下 3. 每个坐标应尽量位于对应目标的可点击区域中心 4. 如果图片中某一步无法明确定位，请不要猜测，在 summary 中说明 输出要求: - 仅返回 JSON，不要输出任何多余文本 - 不要使用 markdown - JSON 结构必须严格如下： { \u0026#34;positions\u0026#34;: [ { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx }, { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx } ], \u0026#34;summary\u0026#34;: \u0026#34;简要说明这些坐标如何对应图片中的点击要求\u0026#34; } \u0026#34;\u0026#34;\u0026#34; return prompt agent.py\nimport xbot_visual import base64 import json import re import mimetypes from abc import ABC, abstractmethod from typing import List, Dict from openai import OpenAI from .ability import * class Agent(ABC): def __init__(self, ability: Capability, params: Dict): \u0026#34;\u0026#34;\u0026#34; 对于不同实现方式需要的参数可用params传入 \u0026#34;\u0026#34;\u0026#34; self.params = params self.prompt = ability.prompt if \u0026#34;job\u0026#34; in params: self.prompt = self.prompt + f\u0026#34;\\n你的任务是: {params[\u0026#39;job\u0026#39;]}\u0026#34; @abstractmethod def find_positions(self, img_path: str): \u0026#34;\u0026#34;\u0026#34; 识别图片，按顺序输出需要点击的坐标 \u0026#34;\u0026#34;\u0026#34; pass def handle_result(self, result): pattern = r\u0026#34;\\{.*\\}\u0026#34; match_result = re.search(pattern, result, re.DOTALL) if match_result: try: result_json = json.loads(match_result.group(0)) except Exception as e: raise Exception(f\u0026#34;AI返回结果无法转为JSON格式, 内容: {result_json}\u0026#34;) return result_json else: raise Exception(f\u0026#34;AI返回内容无法提取JSON数据, 内容: {result}\u0026#34;) def encode_image_to_url(self, img_path): mime_type, _ = mimetypes.guess_type(img_path) if not mime_type: raise Exception(\u0026#34;无法识别图片类型\u0026#34;) with open(img_path, \u0026#34;rb\u0026#34;) as image_file: base64_data = base64.b64encode(image_file.read()).decode(\u0026#39;utf-8\u0026#39;) return f\u0026#34;data:{mime_type};base64,{base64_data}\u0026#34; class OpenaiAPI(Agent): \u0026#34;\u0026#34;\u0026#34; 调用兼容OpenAI接口的模型 :params: {\u0026#34;base_url\u0026#34;: \u0026#34;https://xxxxxx\u0026#34;, \u0026#34;API_KEY\u0026#34;:\u0026#34;xxxxxx\u0026#34;, model=\u0026#34;xxxx\u0026#34;} \u0026#34;\u0026#34;\u0026#34; def __init__(self, ability, params): super().__init__(ability, params) self.client = OpenAI( base_url=self.params[\u0026#34;base_url\u0026#34;], api_key=self.params[\u0026#34;API_KEY\u0026#34;] ) def find_positions(self, img_path): img_url = self.encode_image_to_url(img_path) content = [ {\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: self.prompt}, {\u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: {\u0026#34;url\u0026#34;: img_url}} ] messages = [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: content}] try: response = self.client.chat.completions.create(model=self.params[\u0026#34;model\u0026#34;], messages=messages) except Exception as e: raise Exception(f\u0026#34;访问 {self.params[\u0026#39;base_url\u0026#39;]} 出现异常: {e}\u0026#34;) return self.handle_result(response.choices[0].message.content) class ShadowAPI(Agent): \u0026#34;\u0026#34;\u0026#34; 使用影刀的接口调用其他模型识别验证码 :params: {\u0026#34;ai_engine\u0026#34;: \u0026#34;xxxx\u0026#34;, model=\u0026#34;xxxx\u0026#34;} \u0026#34;\u0026#34;\u0026#34; def __init__(self, ability, params): super().__init__(ability, params) def find_positions(self, img_path): result = xbot_visual.chatgpt.completions( ai_engine=self.params[\u0026#34;ai_engine\u0026#34;], model=self.params[\u0026#34;model\u0026#34;], use_multiModal=True, knowledge=\u0026#34;\u0026#34;, images=lambda: [img_path], prompt=\u0026#34;\u0026#34;, question=self.prompt ) return self.handle_result(result) class AIPowerAPI(Agent): \u0026#34;\u0026#34;\u0026#34; 使用AIPower识别验证码 :params: {\u0026#34;flow_id\u0026#34;: \u0026#34;xxxx\u0026#34;, \u0026#34;input_name\u0026#34;: \u0026#34;xxx\u0026#34;, \u0026#34;output_name\u0026#34;: \u0026#34;xxxx\u0026#34;} \u0026#34;\u0026#34;\u0026#34; def __init__(self, ability, params): super().__init__(ability, params) def find_positions(self, img_path): result = xbot_visual.ai_power.run( flow_id=self.params[\u0026#34;flow_id\u0026#34;], inputs=[ {self.params[\u0026#34;input_name\u0026#34;]: img_path, \u0026#34;type\u0026#34;: \u0026#34;IMAGE\u0026#34;, }, ], outputs=[ self.params[\u0026#34;output_name\u0026#34;], ] ) result = getattr(result, self.params[\u0026#34;output_name\u0026#34;]) return self.handle_result(result) def create_agent(api_type, ability_name, params): ability_obj = globals().get(ability_name) if ability_obj: capability = ability_obj() else: raise ValueError(f\u0026#34;ability.py 中找不到 {ability}\u0026#34;) mapping = { \u0026#34;OpenAI\u0026#34;: OpenaiAPI, \u0026#34;影刀\u0026#34;: ShadowAPI, \u0026#34;AIPower\u0026#34;: AIPowerAPI } return mapping[api_type](capability, params) action.py\nimport xbot_visual from xbot.web import WebBrowser, WebElement from xbot.win32 import Win32Window, Win32Element from xbot.selector import Selector import tempfile import os from PIL import Image from .agent import create_agent, Agent from abc import ABC, abstractmethod class Handle(ABC): def __init__(self, api_type, ability_name, params): self.agent = create_agent(api_type, ability_name, params) def _call_agent(self, container, element, is_web): if hasattr(element, \u0026#34;find\u0026#34;) is False and hasattr(container, \u0026#34;find\u0026#34;): element = container.find(element) bounding = element.get_bounding() with tempfile.TemporaryDirectory() as temp_dir: screenshot_func = xbot_visual.web.element.screenshot if is_web else xbot_visual.win32.element.screenshot kwargs = {\u0026#34;browser\u0026#34;: container, \u0026#34;capture_area\u0026#34;: \u0026#34;Element\u0026#34;} if is_web else {\u0026#34;window\u0026#34;: container} img_path = screenshot_func( element=element, folder_path=temp_dir, random_filename=True, filename=None, **kwargs ) with Image.open(img_path) as img: size = {\u0026#34;width\u0026#34;: img.size[0], \u0026#34;height\u0026#34;: img.size[1]} raw_result = self.agent.find_positions(img_path) return { \u0026#34;element_bounding\u0026#34;: bounding, \u0026#34;img_size\u0026#34;: size, \u0026#34;agent_result\u0026#34;: raw_result } @abstractmethod def _action(self, call_agent_result: dict): pass def run(self, container, element, is_web): call_agent_result = self._call_agent(container, element, is_web) self.action(call_agent_result) class HandleClick(Handle): def __init__(self, api_type, ability_name, params): super().__init__(api_type, ability_name, params) def _action(self, call_agent_result): bounding = call_agent_result[\u0026#34;element_bounding\u0026#34;] agent_result = call_agent_result[\u0026#34;agent_result\u0026#34;] img_size = call_agent_result[\u0026#34;img_size\u0026#34;] for item in agent_result[\u0026#34;positions\u0026#34;]: x = item[\u0026#34;x\u0026#34;] / 1000 * img_size[\u0026#34;width\u0026#34;] y = item[\u0026#34;y\u0026#34;] / 1000 * img_size[\u0026#34;height\u0026#34;] xbot.win32.mouse_move( point_x=bounding[0]+x, point_y=bounding[1]+y, move_speed=\u0026#34;middle\u0026#34;, delay_after=0 ) xbot.win32.mouse_click() class HandleDrag(Handle): def __init__(self, api_type, ability_name, params): super().__init__(api_type, ability_name, params) def _action(self, call_agent_result: dict): bounding = call_agent_result[\u0026#34;element_bounding\u0026#34;] agent_result = call_agent_result[\u0026#34;agent_result\u0026#34;] img_size = call_agent_result[\u0026#34;img_size\u0026#34;] for item in agent_result[\u0026#34;positions\u0026#34;]: start_x = item[\u0026#34;start\u0026#34;][\u0026#34;x\u0026#34;] / 1000 * img_size[\u0026#34;width\u0026#34;] start_y = item[\u0026#34;start\u0026#34;][\u0026#34;y\u0026#34;] / 1000 * img_size[\u0026#34;height\u0026#34;] end_x = item[\u0026#34;end\u0026#34;][\u0026#34;x\u0026#34;] / 1000 * img_size[\u0026#34;width\u0026#34;] end_y = item[\u0026#34;end\u0026#34;][\u0026#34;y\u0026#34;] / 1000 * img_size[\u0026#34;height\u0026#34;] xbot.win32.mouse_move( point_x=bounding[0]+start_x, point_y=bounding[1]+start_y, move_speed=\u0026#34;middle\u0026#34;, delay_after=0 ) xbot.win32.mouse_click(click_type=\u0026#34;down\u0026#34;) xbot.win32.mouse_move( point_x=bounding[0]+end_x, point_y=bounding[1]+end_y, move_speed=\u0026#34;middle\u0026#34;, delay_after=0 ) xbot.win32.mouse_click(click_type=\u0026#34;up\u0026#34;) ","permalink":"https://blog.levitan.top/posts/ai-driven-captcha-automation-solution/","summary":"\u003ch1 id=\"1-前言\"\u003e1. 前言\u003c/h1\u003e\n\u003cp\u003e在之前尝试过 AI 驱动的RPA程序完成给定的任务，但是整个运行下来给人的感觉就是太慢了，当时跑一个淘宝搜索产品并获取产品信息的任务跑了快10分钟，总结下来慢的原因无非下面几点。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eAI返回内容慢，通过OpenRouter调用gemini模型，一个请求要1-2分钟才能返回结果。\u003c/li\u003e\n\u003cli\u003e要实现程序 \u003ccode\u003e感知-决策-在感知-判断\u003c/code\u003e 就要调用多次模型，模型调用次数越多就越慢。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cblockquote\u003e\n\u003cp\u003e所以，之前AI驱动RPA属于对技术的探索，但没有实际意义，\n这次我尝试用AI来处理在自动化中会遇到的一些复杂验证码问题\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e下面是这次需要解决的验证码的例子，这三个验证码都来自Temu平台。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/posts/ai-driven-captcha-automation-solution/images/%E9%AA%8C%E8%AF%81%E7%A0%811.png\"\u003e\n\u003cimg loading=\"lazy\" src=\"/posts/ai-driven-captcha-automation-solution/images/%E9%AA%8C%E8%AF%81%E7%A0%812.png\"\u003e\n\u003cimg loading=\"lazy\" src=\"/posts/ai-driven-captcha-automation-solution/images/%E9%AA%8C%E8%AF%81%E7%A0%813.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"2-核心思路\"\u003e2. 核心思路\u003c/h1\u003e\n\u003cp\u003e整体思路很简单，把验证码截图给AI，让AI按要求返回坐标，程序解析坐标并点击\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/posts/ai-driven-captcha-automation-solution/images/%E8%AE%BE%E8%AE%A11.png\"\u003e\u003c/p\u003e\n\u003cp\u003e整体的代码设计如下图，首先是三个抽象类下面逐个解释一下。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eAgent\u003c/code\u003e：用于调用AI接口，封装了图片解析、AI结果处理等功能，需要子类实现具体的AI接口\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eCapability\u003c/code\u003e：表示一种能力，就是给AI的提示词。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ccode\u003eHandle\u003c/code\u003e：使用AI返回的结果处理问题\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ccode\u003eAgent\u003c/code\u003e有3个子类，分别是调用OpenAI规范的接口、调用影刀内置AI接口和调用影刀AIPower接口。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003eCapability\u003c/code\u003e的3个子类分别表示点选验证码处理、拖动验证码处理以及复选框处理\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e在做软件自动化时很容易遇到复选框无法判断是否勾选，这种情况就可以借助AI\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ccode\u003eHandle\u003c/code\u003e有2个子类分表用于，处理点击操作和处理拖动操作\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/posts/ai-driven-captcha-automation-solution/images/UML.png\"\u003e\u003c/p\u003e\n\u003ch1 id=\"3-代码实现\"\u003e3. 代码实现\u003c/h1\u003e\n\u003cp\u003e\u003ccode\u003eability.py\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003efrom\u003c/span\u003e abc \u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e ABC, abstractmethod\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eCapability\u003c/span\u003e(ABC):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    表示一种能力, AI能做什么操作\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003e@property\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003e@abstractmethod\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eprompt\u003c/span\u003e(self):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003epass\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eClickVerifier\u003c/span\u003e(Capability):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003e@property\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eprompt\u003c/span\u003e(self):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        prompt \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        请严格按照图片中给出的操作要求执行。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        图片中已经明确标注了需要点击的目标及点击顺序，\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        你只需要根据图片内容，依次返回每一步需要点击的位置坐标。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        规则：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        1. 只返回图片中明确要求点击的内容，不要增加或省略步骤\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        2. 坐标基于当前输入图片的像素坐标\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        3. 坐标原点为图片左上角 (0, 0)，x 向右，y 向下\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        4. 每个坐标应尽量位于对应目标的可点击区域中心\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        5. 如果图片中某一步无法明确定位，请不要猜测，在 summary 中说明\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        输出要求：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - 仅返回 JSON，不要输出任何多余文本\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - 不要使用 markdown\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - JSON 结构必须严格如下：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            \u0026#34;positions\u0026#34;: [\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            ],\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            \u0026#34;summary\u0026#34;: \u0026#34;简要说明这些坐标如何对应图片中的点击要求\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e prompt\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eDragVerifier\u003c/span\u003e(Capability):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003e@property\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eprompt\u003c/span\u003e(self):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        prompt \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        图片中已经明确标注了需要如何拖拽内容\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        你只需要根据图片内容，依次返回开始拖拽和结束拖拽的位置坐标\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        规则：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        1. 坐标基于当前输入图片的像素坐标\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        2. 坐标原点为图片左上角 (0, 0)，x 向右，y 向下\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        3. 每个坐标应尽量位于对应目标的可点击区域中心\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        4. 如果图片中某一步无法明确定位，请不要猜测，在 summary 中说明\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        输出要求：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - 仅返回 JSON，不要输出任何多余文本\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - 不要使用 markdown\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - JSON 结构必须严格如下：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            \u0026#34;positions\u0026#34;: [\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    \u0026#34;start\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;x\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;y\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    \u0026#34;end\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;x\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;y\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    \u0026#34;start\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;x\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;y\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    \u0026#34;end\u0026#34;: {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;x\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                        \u0026#34;y\u0026#34;: xxx,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                    }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            ],\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            \u0026#34;summary\u0026#34;: \u0026#34;简要说明理由\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e prompt\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eCheckboxProcess\u003c/span\u003e(Capability):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003e@property\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eprompt\u003c/span\u003e(self):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        prompt \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        给你一张界面截图，你需要根据任务要求返回需要点击的复选框坐标\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        如果一个复选框已经被点击则跳过该复选框\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        规则\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        1. 坐标基于当前输入图片的像素坐标\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        2. 坐标原点为图片左上角 (0, 0)，x 向右，y 向下\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        3. 每个坐标应尽量位于对应目标的可点击区域中心\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        4. 如果图片中某一步无法明确定位，请不要猜测，在 summary 中说明\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        输出要求:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - 仅返回 JSON，不要输出任何多余文本\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - 不要使用 markdown\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        - JSON 结构必须严格如下：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        {\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            \u0026#34;positions\u0026#34;: [\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx },\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e                { \u0026#34;x\u0026#34;: xxx, \u0026#34;y\u0026#34;: xxx }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            ],\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e            \u0026#34;summary\u0026#34;: \u0026#34;简要说明这些坐标如何对应图片中的点击要求\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e prompt\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003eagent.py\u003c/code\u003e\u003c/p\u003e","title":"AI驱动验证码自动化解决方案"},{"content":" 这篇文章也是用来记录我自己搭建博客博客的经历\n1. 项目 1.1 实现链路 下面是整个博客的链路，因为我要考虑服务器迁移的问题，为了方便迁移所以会使用容器化方案。\n最终实现我写好一篇文章，在本地使用 git push命令即可实现服务器自动更新内容。\n1.2 文件结构 hugo-blog/ ├── blog/ │ ├── source/ │ ├── repo.git/ └── docker-compose.yaml source：存放hugo原始数据以及hugo编译后的public文件夹\nrepo.git：Git仓库\n2. 博客搭建 2.1 创建Git仓库 在服务器上创建Git裸仓库，用于接收推送的代码\n# 创建 git 文件夹 mkdir -p blog/repo.git cd blog/repo.git # 初始化裸仓库 git init --bare 配置自动化钩子，当本地终端通过git push推送文章时，服务器自动触发这个脚本\nvim hooks/post-receive #!/bin/bash # 定义路径 GIT_DIR=$HOME/docker/hugo-blog/blog/repo.git SRC_DIR=$HOME/docker/hugo-blog/blog/source # 1. 检出源码到临时目录 git --work-tree=$SRC_DIR --git-dir=$GIT_DIR checkout -f main echo \u0026#34;迁出分支成功\u0026#34; # 2. 使用 Docker 运行 Hugo 构建 # --rm 表示构建完后自动删除容器 docker run --rm \\ -u $(id -u):$(id -g) \\ -v $SRC_DIR:/src \\ hugomods/hugo:latest build echo \u0026#34;Hugo构建成功\u0026#34; 给脚本执行权限chmod +x hooks/post-receive\n2.2 本地终端Hugo配置 在Hugo项目根目录添加Git远程端\ngit remote add blog levitan@levitan.top:/home/levitan/docker/hugo-blog/blog/repo.git 2.3 docker-compose配置 services: nginx: image: nginx:alpine container_name: hugo-blog ports: - \u0026#34;8002:80\u0026#34; volumes: - ./blog/source/public:/usr/share/nginx/html:ro restart: always 2.4 主机Nginx配置 server { listen 8443 ssl; server_name blog.levitan.top; # 域名证书位置 ssl_certificate /etc/nginx/ssl/blog/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/blog/key.pem; location / { proxy_pass http://127.0.0.1:8002; # 重要的头部设置 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; # 告知后端协议是 HTTPS } } ","permalink":"https://blog.levitan.top/posts/build-blog-using-hugo/","summary":"\u003cblockquote\u003e\n\u003cp\u003e这篇文章也是用来记录我自己搭建博客博客的经历\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"1-项目\"\u003e1. 项目\u003c/h1\u003e\n\u003ch2 id=\"11-实现链路\"\u003e1.1 实现链路\u003c/h2\u003e\n\u003cp\u003e下面是整个博客的链路，因为我要考虑服务器迁移的问题，为了方便迁移所以会使用容器化方案。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/posts/build-blog-using-hugo/images/design-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e最终实现我写好一篇文章，在本地使用 \u003ccode\u003egit push\u003c/code\u003e命令即可实现服务器自动更新内容。\u003c/p\u003e\n\u003ch2 id=\"12-文件结构\"\u003e1.2 文件结构\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehugo-blog/\n├── blog/\n│   ├── source/\n│   ├── repo.git/\n└── docker-compose.yaml\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003ccode\u003esource\u003c/code\u003e：存放hugo原始数据以及hugo编译后的public文件夹\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003erepo.git\u003c/code\u003e：Git仓库\u003c/p\u003e\n\u003ch1 id=\"2-博客搭建\"\u003e2. 博客搭建\u003c/h1\u003e\n\u003ch2 id=\"21-创建git仓库\"\u003e2.1 创建Git仓库\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e在服务器上创建Git裸仓库，用于接收推送的代码\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 创建 git 文件夹\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emkdir -p blog/repo.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecd blog/repo.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 初始化裸仓库\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit init --bare\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e配置自动化钩子，当本地终端通过\u003ccode\u003egit push\u003c/code\u003e推送文章时，服务器自动触发这个脚本\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003evim hooks/post-receive\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#!/bin/bash\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 定义路径\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eGIT_DIR\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e$HOME/docker/hugo-blog/blog/repo.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eSRC_DIR\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e$HOME/docker/hugo-blog/blog/source\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 1. 检出源码到临时目录\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit --work-tree\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e$SRC_DIR --git-dir\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e$GIT_DIR checkout -f main\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;迁出分支成功\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# 2. 使用 Docker 运行 Hugo 构建\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# --rm 表示构建完后自动删除容器\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker run --rm \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t-u \u003cspan style=\"color:#66d9ef\"\u003e$(\u003c/span\u003eid -u\u003cspan style=\"color:#66d9ef\"\u003e)\u003c/span\u003e:\u003cspan style=\"color:#66d9ef\"\u003e$(\u003c/span\u003eid -g\u003cspan style=\"color:#66d9ef\"\u003e)\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\t-v $SRC_DIR:/src \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\thugomods/hugo:latest build\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eecho \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Hugo构建成功\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e给脚本执行权限\u003ccode\u003echmod +x hooks/post-receive\u003c/code\u003e\u003c/p\u003e","title":"使用Hugo搭建博客"},{"content":" 这次尝试的宗旨是\nAI可替换，不和特定AI绑定 不考虑Token消耗 设计出通用的 AI 驱动框架，并不局限于 RPA 这个方面 1. 浅尝 最开始的想法是实现两个Agent分别是·、任务规划Agent和任务执行Agent\n任务规划Agent根据用户需求列出步骤清单，任务执行Agent负责循环步骤清单执行步骤\n1.1 任务规划Agent 我在实现完任务规划Agent后就发现，任务规划和执行分离不是一个好设计，规划的任务准确性会越来越低，所以这个方案我就没有继续实现执行Agent\nrask_planner_agent.py\nimport base64 from langchain_deepseek import ChatDeepSeek from langchain_openai import ChatOpenAI from langchain.agents import create_agent from .output_schema import TaskSchema class TaskPlannerAgent: def __init__(self): self.llm = ChatOpenAI( openai_api_key=\u0026#34;sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, openai_api_base=\u0026#34;https://openrouter.ai/api/v1\u0026#34;, model_name=\u0026#34;google/gemini-3-flash-preview\u0026#34; ) # System prompt self.system_prompt = \u0026#34;\u0026#34;\u0026#34; 你是一个专业的任务规划助手。用户将提供任务描述或图片，你需要结合这些信息将其拆解为结构化步骤。 \u0026#34;\u0026#34;\u0026#34; self.agent = create_agent( model=self.llm, system_prompt=self.system_prompt, response_format=TaskSchema ) def plan(self, instruction: str, image_path: str = None) -\u0026gt; TaskSchema: content = [{\u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: instruction}] if image_path: image_data = self._encode_image(image_path) content.append({ \u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: { \u0026#34;url\u0026#34;: f\u0026#34;data:image/jpeg;base64,{image_data}\u0026#34; } }) result = self.agent.invoke( {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: content}]} ) return result[\u0026#34;structured_response\u0026#34;] def _encode_image(self, image_path: str): \u0026#34;\u0026#34;\u0026#34;将本地图片转为 base64编码\u0026#34;\u0026#34;\u0026#34; with open(image_path, \u0026#39;rb\u0026#39;) as image_file: return base64.b64encode(image_file.read()).decode(\u0026#39;utf-8\u0026#39;) def main(args): agent = TaskPlannerAgent() for step in agent.plan(\u0026#34;搜索熊猫，并进入搜索结果的第一项内容\u0026#34;, r\u0026#34;C:\\Users\\Levit\\Desktop\\1.jpg\u0026#34;).steps: print(step) output_schema.py 规范Agent输出格式\nfrom typing import List, Optional, Literal, Union from pydantic import BaseModel, Field class Point(BaseModel): x: int = Field(..., description=\u0026#34;目标终点横坐标\u0026#34;) y: int = Field(..., description=\u0026#34;目标终点纵坐标\u0026#34;) class Click(BaseModel): type: Literal[\u0026#39;click\u0026#39;] point: Point = Field(..., description=\u0026#34;鼠标点击的位置\u0026#34;) button: Literal[\u0026#34;left\u0026#34;, \u0026#34;middle\u0026#34;, \u0026#34;right\u0026#34;] = Field( ..., description=\u0026#34;鼠标按键，可选值为 left（左键）、middle（中键）、right（右键）\u0026#34; ) clicks: int = Field(..., description=\u0026#34;表示按下次数\u0026#34;) class Input(BaseModel): point: Point = Field(..., description=\u0026#34;输入框所在位置\u0026#34;) value: str = Field(..., description=\u0026#34;需要输入的文本内容\u0026#34;) class Step(BaseModel): id: int = Field(..., description=\u0026#34;步骤的唯一序号\u0026#34;) description: str = Field(..., description=\u0026#34;描述该步骤的具体意图\u0026#34;) action: Union[Click, Input] = Field(..., description=\u0026#34;具体操作类型\u0026#34;) class TaskSchema(BaseModel): \u0026#34;\u0026#34;\u0026#34;任务执行计划的根结构\u0026#34;\u0026#34;\u0026#34; goal: str = Field(..., description=\u0026#34;任务的最终目标描述\u0026#34;) steps: List[Step] = Field(..., description=\u0026#34;拆解后的执行步骤列表\u0026#34;) 1.2 结论 这个方案有个问题，规划和后续遇到的问题可能会不一样。\n目前的逻辑是规划 → 执行\n实际优秀的逻辑是：感知 → 决策 → 行动 → 再感知\n2. 改进模型 这个方案，每轮 Agent 都会感知当前界面并做出下一步的决策 ，同时 Agent 会记住之前自己做过的决策。\n2.1 感知决策Agent 这个 Agent 负责感知并做出决策，同时Agent包含了简单的消息管理，Agent能知道之前自己做过什么操作。\nagent.py\nimport base64 from langchain_deepseek import ChatDeepSeek from langchain_openai import ChatOpenAI from langchain.agents import create_agent from .output_schema import StepSchema class AgentMemory: def __init__(self): self.steps = [] def add_step(self, step: StepSchema): self.steps.append(step.model_dump_json()) def summary(self) -\u0026gt; str: if not self.steps: return \u0026#34;尚未执行任何操作\u0026#34; return \u0026#39;\\n\u0026#39;.join(self.steps) class DecisionAgent: def __init__(self, purpose): self.llm = ChatOpenAI( openai_api_key=\u0026#34;sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxx\u0026#34;, openai_api_base=\u0026#34;https://openrouter.ai/api/v1\u0026#34;, model_name=\u0026#34;google/gemini-3-flash-preview\u0026#34; ) # System prompt self.system_prompt = f\u0026#34;\u0026#34;\u0026#34; 你是一个桌面RPA的决策Agent，你需要根据客户目的与当前电脑页面分析下一步需要执行的动作并以结构化的方式输出下一步动作。 客户的目标是：{purpose} 请参考之前的操作历史，不要重复已经失败的动作。 \u0026#34;\u0026#34;\u0026#34; self.memory = AgentMemory() self.agent = create_agent( model=self.llm, system_prompt=self.system_prompt, response_format=StepSchema ) def next_step(self, image_path) -\u0026gt; StepSchema: content = [] content.append({ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: f\u0026#34;你之前已经执行过的步骤如下:\\n{self.memory.summary()}\u0026#34; }) image_data = self._encode_image(image_path) content.append({ \u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: { \u0026#34;url\u0026#34;: f\u0026#34;data:image/jpeg;base64,{image_data}\u0026#34; } }) result = self.agent.invoke( {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: content}]} ) self.memory.add_step(result[\u0026#34;structured_response\u0026#34;]) return result[\u0026#34;structured_response\u0026#34;] def _encode_image(self, image_path: str): \u0026#34;\u0026#34;\u0026#34;将本地图片转为 base64编码\u0026#34;\u0026#34;\u0026#34; with open(image_path, \u0026#39;rb\u0026#39;) as image_file: return base64.b64encode(image_file.read()).decode(\u0026#39;utf-8\u0026#39;) output_schema.py\nfrom typing import List, Optional, Literal, Union, Any from pydantic import BaseModel, Field class Point(BaseModel): x: int = Field(..., description=\u0026#34;目标终点横坐标\u0026#34;) y: int = Field(..., description=\u0026#34;目标终点纵坐标\u0026#34;) class Click(BaseModel): type: Literal[\u0026#34;click\u0026#34;] point: Point = Field(..., description=\u0026#34;鼠标点击的位置\u0026#34;) button: Literal[\u0026#34;left\u0026#34;, \u0026#34;middle\u0026#34;, \u0026#34;right\u0026#34;] = Field( ..., description=\u0026#34;鼠标按键，可选值为 left（左键）、middle（中键）、right（右键）\u0026#34; ) clicks: int = Field(..., description=\u0026#34;表示按下次数\u0026#34;) class Input(BaseModel): type: Literal[\u0026#34;input\u0026#34;] point: Point = Field(..., description=\u0026#34;输入框所在位置\u0026#34;) value: str = Field(..., description=\u0026#34;需要输入的文本内容\u0026#34;) class Finish(BaseModel): type: Literal[\u0026#34;finish\u0026#34;] output: dict[str, Any] | None = Field(..., description=\u0026#34;当任务需要返回内容时返回结果放这里\u0026#34;) class StepSchema(BaseModel): thought: str = Field(..., description=\u0026#34;表示当前需要做的事情\u0026#34;) action: Union[Click, Input, Finish] = Field(..., description=\u0026#34;具体操作类型\u0026#34;) 2.2 执行器 指令负责执行的模块已经不带Agent了，因为执行模块不用带脑子，只需要根据决策Agent的结果执行对应动作。\nexecutor.py\nimport xbot from xbot import print, sleep from .import package from .package import variables as glv from .output_schema import StepSchema class Executor: def __init__(self): # 截图的宽高 self.width = 1920 self.height = 1080 def execute(self, step: StepSchema): action = step.action if action.type == \u0026#34;finish\u0026#34;: return x, y = self.get_pixel_coords(action.point.x, action.point.y) if action.type == \u0026#34;click\u0026#34;: xbot.win32.mouse_move(x, y) xbot.win32.mouse_click() elif action.type == \u0026#34;input\u0026#34;: point = action.point xbot.win32.mouse_move(x, y) xbot.win32.mouse_click() xbot.win32.send_keys(\u0026#34;^{a}{BACKSPACE}\u0026#34;) xbot.win32.send_keys(action.value) def get_pixel_coords(self, normalized_x, normalized_y): pixel_x = (normalized_x / 1000) * self.width pixel_y = (normalized_y / 1000) * self.height return int(pixel_x), int(pixel_y) 2.3 效果 我这里是把代码丢在影刀里面运行的，所以会有一些影刀的Python包\nmain.py\nimport xbot from xbot import print, sleep from .import package from .package import variables as glv from agent import DecisionAgent from excutor import Excutor def main(args): agent = DecisionAgent(\u0026#34;打开浏览器，下载当前页面的订单数据，需要将文件的名字重命名为“测试.xlsx”\u0026#34;) excutor = Executor() while True: xbot.win32.screenshot.save_screen_to_file(r\u0026#34;C:\\Users\\Levit\\Desktop\\测试.jpg\u0026#34;, \u0026#34;jpg\u0026#34;) step = agent.next_step(r\u0026#34;C:\\Users\\Levit\\Desktop\\测试.jpg\u0026#34;) print(step) if step.action.type == \u0026#34;finish\u0026#34;: break excutor.execute(step) sleep(2) 2.4 结论 这个方案存在一下几个问题\nDecision Agent 同时承担了「感知 + 决策」：短期 OK，长期会出三个问题：prompt 越来越长、agent 开始“看不清重点”、无法复用 perception 能力 没有「结果校验 / 失败感知」：需要识别当前操作是否执行正确并且在 memory 中记录出每次操作是否正确 将感知和决策分离的优势是，可以换任意的感知，换上不同的感知配合上不同的执行器就是不同的Agent\n3. 进一步改进模型 根据 2 中的结论，进一步优化的 Agent 模型如下\nPerceptionAgent：负责感知世界，相当于人眼，使用有多模态能力的AI模型\nDecisionAgent：负责做决策，相当于人脑，使用推理类的大语言模型\nExecutor：负责执行操作，不需要接入AI模型，纯代码实现\nResultObserverAgent：负责校验实际运行结果与预期运行结果是否相符，可以对操作分层，一部分操作可机械判断结果，一部分操作可通过多模态的AI模型判断结果\n3.1. 感知 Agent perception_agent.py\nimport base64 from langchain_openai import ChatOpenAI from langchain.agents import create_agent from .perception_output_schema import ViewSchema class PerceptionAgent: def __init__(self): self.llm = ChatOpenAI( openai_api_key=\u0026#34;sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\u0026#34;, openai_api_base=\u0026#34;https://openrouter.ai/api/v1\u0026#34;, model_name=\u0026#34;google/gemini-3-flash-preview\u0026#34; ) self.system_prompt = \u0026#34;\u0026#34;\u0026#34; 你是一个 Perception Agent，负责感知当前电脑屏幕的状态， 返回图片中所有可以操作的元素 你的任务是： - 描述当前屏幕的可见内容 - 如果提供了“期望结果”，判断当前画面是否满足该期望 你智能基于当前的截图判断 如果无法判断是否满足期望，请返回“不确定” \u0026#34;\u0026#34;\u0026#34; self.agent = create_agent( model=self.llm, system_prompt=self.system_prompt, response_format=ViewSchema ) def view(self, image_path: str, expectation: str=None) -\u0026gt; ViewSchema: content = [] if expectation: content.append({ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: expectation }) image_data = self._encode_image(image_path) content.append({ \u0026#34;type\u0026#34;: \u0026#34;image_url\u0026#34;, \u0026#34;image_url\u0026#34;: { \u0026#34;url\u0026#34;: f\u0026#34;data:image/jpeg;base64,{image_data}\u0026#34; } }) result = self.agent.invoke( {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: content}]} ) return result[\u0026#34;structured_response\u0026#34;] def _encode_image(self, image_path: str): \u0026#34;\u0026#34;\u0026#34;将本地图片转为 base64编码\u0026#34;\u0026#34;\u0026#34; with open(image_path, \u0026#39;rb\u0026#39;) as image_file: return base64.b64encode(image_file.read()).decode(\u0026#39;utf-8\u0026#39;) perception_agent_schema.py\nfrom typing import List, Optional, Literal, Union, Any from pydantic import BaseModel, Field class Position(BaseModel): x: int = Field(..., description=\u0026#34;横坐标，范围 0-1000，左上角为原点\u0026#34;) y: int = Field(..., description=\u0026#34;纵坐标，范围 0-1000，左上角为原点\u0026#34;) class Element(BaseModel): label: str = Field(..., description=\u0026#34;元素名称\u0026#34;) focus: Optional[bool] = Field(..., description=\u0026#34;元素是否获取到输入焦点；无法判断则为 null\u0026#34;) value:Optional[str] = Field(..., description=\u0026#34;输入框或文本元素当前显示的内容，如无法识别则为 null\u0026#34;) elem_type: Literal[\u0026#34;button\u0026#34;, \u0026#34;input\u0026#34;, \u0026#34;text\u0026#34;, \u0026#34;icon\u0026#34;, \u0026#34;link\u0026#34;] = Field(..., description=\u0026#34;元素的类型\u0026#34;) position: Position = Field(..., description=\u0026#34;元素的坐标\u0026#34;) class ViewSchema(BaseModel): summary: str = Field( ..., description=\u0026#34;一句话描述当前界面状态\u0026#34; ) elements: List[Element] = Field( ..., description=\u0026#34;当前界面中可以操作的元素\u0026#34; ) notes: str = Field( ..., description=\u0026#34;任何你认为对决策有帮助的客观信息\u0026#34; ) 3.2 决策 Agent decision_agent.py\nfrom langchain_openai import ChatOpenAI from langchain_deepseek import ChatDeepSeek from langchain.agents import create_agent from .perception_output_schema import ViewSchema from .decision_output_schema import StepSchema from .result_observer_output_schema import ObservationResultSchema class AgentMemory: def __init__(self): self.steps = [] def add_step(self, step: StepSchema): self.steps.append(step.model_dump_json()) def summary(self) -\u0026gt; str: if not self.steps: return \u0026#34;尚未执行任何操作\u0026#34; return \u0026#39;\\n\u0026#39;.join(self.steps) class DecisionAgent: def __init__(self, purpose): self.llm = ChatDeepSeek( model=\u0026#39;deepseek-chat\u0026#39;, api_key=\u0026#39;sk-xxxxxxxxxxxxxxxx\u0026#39;, temperature=0.7 ) self.system_prompt = f\u0026#34;\u0026#34;\u0026#34; 你是一个桌面自动化系统中的 Decision Agent 你的职责是： - 根据任务目标、与当前界面状态、历史执行记录以及上一步执行结果决定下一步要执行的动作。 - 明确该动作执行后“期望出现的结果”和“结果观察策略”。 注意： - 只输出 JSON 格式，不要包含任何前导或后续文本 前任务目标是：{purpose} \u0026#34;\u0026#34;\u0026#34; self.memory = AgentMemory() self.agent = create_agent( model=self.llm, system_prompt=self.system_prompt, response_format=StepSchema ) def step(self, view: ViewSchema, observer: ObservationResultSchema = None): content = [] content.append({ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: f\u0026#34;当前页面感知结果：\\n{view.model_dump_json()}\u0026#34; }) content.append({ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: f\u0026#34;你之前已经执行过的步骤如下：\\n{self.memory.summary()}\u0026#34; }) if observer: content.append({ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: f\u0026#34;上一步执行后的验证结果：\\n{observer.model_dump_json()}\u0026#34; }) result = self.agent.invoke( {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: content}]} ) step = result[\u0026#34;structured_response\u0026#34;] self.memory.add_step(step) return step decision_agent.py\nfrom typing import List, Optional, Literal, Union, Any from pydantic import BaseModel, Field class Position(BaseModel): x: int = Field(..., description=\u0026#34;横坐标，范围 0-1000，左上角为原点\u0026#34;) y: int = Field(..., description=\u0026#34;纵坐标，范围 0-1000，左上角为原点\u0026#34;) class Element(BaseModel): label: str = Field(..., description=\u0026#34;元素名称\u0026#34;) focus: Optional[bool] = Field(..., description=\u0026#34;元素是否获取到输入焦点；无法判断则为 null\u0026#34;) value:Optional[str] = Field(..., description=\u0026#34;输入框或文本元素当前显示的内容，如无法识别则为 null\u0026#34;) elem_type: Literal[\u0026#34;button\u0026#34;, \u0026#34;input\u0026#34;, \u0026#34;text\u0026#34;, \u0026#34;icon\u0026#34;, \u0026#34;link\u0026#34;] = Field(..., description=\u0026#34;元素的类型\u0026#34;) position: Position = Field(..., description=\u0026#34;元素的坐标\u0026#34;) class ViewSchema(BaseModel): summary: str = Field( ..., description=\u0026#34;一句话描述当前界面状态\u0026#34; ) elements: List[Element] = Field( ..., description=\u0026#34;当前界面中可以操作的元素\u0026#34; ) notes: str = Field( ..., description=\u0026#34;任何你认为对决策有帮助的客观信息\u0026#34; ) 3.3 执行器 executor.py\nimport xbot from xbot import print, sleep from .import package from .package import variables as glv from .decision_output_schema import StepSchema class Executor: def __init__(self): # 截图的宽高 self.width = 1920 self.height = 1080 def execute(self, step: StepSchema): action = step.action if action.type == \u0026#34;finish\u0026#34;: return x, y = self.get_pixel_coords(action.point.x, action.point.y) if action.type == \u0026#34;click\u0026#34;: xbot.win32.mouse_move(x, y) xbot.win32.mouse_click() elif action.type == \u0026#34;input\u0026#34;: point = action.point xbot.win32.mouse_move(x, y) xbot.win32.mouse_click() xbot.win32.send_keys(\u0026#34;^{a}{BACKSPACE}\u0026#34;) xbot.win32.send_keys(action.value) def get_pixel_coords(self, normalized_x, normalized_y): pixel_x = (normalized_x / 1000) * self.width pixel_y = (normalized_y / 1000) * self.height return int(pixel_x), int(pixel_y) 3.4 结果检测Agent result_observer_agent.py\nimport base64 from langchain_openai import ChatOpenAI from langchain_deepseek import ChatDeepSeek from langchain.agents import create_agent from .perception_output_schema import ViewSchema from .decision_output_schema import StepSchema from .result_observer_output_schema import ObservationResultSchema class ResultObserverAgent: def __init__(self): self.llm = ChatDeepSeek( model=\u0026#39;deepseek-chat\u0026#39;, api_key=\u0026#39;sk-xxxxxxxxxxxxxxxxxxxxxxxxxx\u0026#39;, temperature=0.7 ) self.system_prompt = \u0026#34;\u0026#34;\u0026#34; 你是一个左面自动化系统中的 ResultObserver Agent。 你的职责是： - 对比 “动作执行后的期望” 与 “执行动作后的界面感知结果” - 判断期望是否被满足 - 提供一个清晰、客观、的观察结论 \u0026#34;\u0026#34;\u0026#34; self.agent = create_agent( model=self.llm, system_prompt=self.system_prompt, response_format=ObservationResultSchema ) def observe(self, view: ViewSchema, step: StepSchema) -\u0026gt; ObservationResultSchema: content = [] content.append({ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: f\u0026#34;当前界面感知结果：\\n{view.model_dump_json(indent=2)}\u0026#34; }) content.append({ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: f\u0026#34;动作执行后的预期：{step.expectation}\u0026#34; }) result = self.agent.invoke( {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: content}]} ) return result[\u0026#34;structured_response\u0026#34;] result_observer_output_schema.py\nfrom typing import List, Optional, Literal, Union, Any from pydantic import BaseModel, Field class ObservationResultSchema(BaseModel): matches_expectation: Literal[\u0026#34;yes\u0026#34;, \u0026#34;no\u0026#34;, \u0026#34;uncertain\u0026#34;] = Field( ..., description=\u0026#34;当前界面是否满足预期\u0026#34; ) reason: str = Field( ..., description=\u0026#34;解释为何满足或不满足预期\u0026#34; ) 3.5 效果 main.py\nfrom .perception_agent import PerceptionAgent from .decision_agent import DecisionAgent from .result_observer_agent import ResultObserverAgent from .executor import Executor def main(args): sleep(5) job = \u0026#34;使用浏览器，下载当前页面的订单数据，并将文件命名为“测试.xlsx\u0026#34; perception_agent = PerceptionAgent() decision_agent = DecisionAgent(job) observer_agent = ResultObserverAgent() executor = Executor() count = 1 pre_step = None while True: xbot.win32.screenshot.save_screen_to_file(r\u0026#34;C:\\Users\\Levit\\Desktop\\测试.jpg\u0026#34;, \u0026#34;jpg\u0026#34;) view = perception_agent.view(r\u0026#34;C:\\Users\\Levit\\Desktop\\测试.jpg\u0026#34;) print(f\u0026#34;世界：{view.model_dump_json()}\u0026#34;) if count != 1: observer = observer_agent.observe(view, pre_step) print(f\u0026#34;验证：{observer.model_dump_json()}\u0026#34;) step = decision_agent.step(view, observer) else: step = decision_agent.step(view) print(f\u0026#34;决策：{step.model_dump_json()}\u0026#34;) if step.action.type == \u0026#34;finish\u0026#34;: break executor.execute(step) pre_step = step sleep(3) count += 1 4. 更进一步优化模型 3中的模型运行已经满足我的要求，这个模型我就没有实际实现，理论上肯定这个模型更优秀。\n增加 Tracker Agent Perception Agent 增加一个反馈，让Perception Agent在下一次观察世界时候应该着重注意什么 ","permalink":"https://blog.levitan.top/posts/rpa-agent/","summary":"\u003cblockquote\u003e\n\u003cp\u003e这次尝试的宗旨是\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eAI可替换，不和特定AI绑定\u003c/li\u003e\n\u003cli\u003e不考虑Token消耗\u003c/li\u003e\n\u003cli\u003e设计出通用的 AI 驱动框架，并不局限于 RPA 这个方面\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"1-浅尝\"\u003e1. 浅尝\u003c/h1\u003e\n\u003cp\u003e最开始的想法是实现两个Agent分别是·、\u003ccode\u003e任务规划Agent\u003c/code\u003e和\u003ccode\u003e任务执行Agent\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e任务规划Agent\u003c/code\u003e根据用户需求列出步骤清单，\u003ccode\u003e任务执行Agent\u003c/code\u003e负责循环步骤清单执行步骤\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/posts/rpa-agent/images/design-1.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"11-任务规划agent\"\u003e1.1 任务规划Agent\u003c/h2\u003e\n\u003cp\u003e我在实现完\u003ccode\u003e任务规划Agent\u003c/code\u003e后就发现，任务规划和执行分离不是一个好设计，规划的任务准确性会越来越低，所以这个方案我就没有继续实现\u003ccode\u003e执行Agent\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003erask_planner_agent.py\u003c/code\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e base64\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003efrom\u003c/span\u003e langchain_deepseek \u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e ChatDeepSeek\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003efrom\u003c/span\u003e langchain_openai \u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e ChatOpenAI\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003efrom\u003c/span\u003e langchain.agents \u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e create_agent\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003efrom\u003c/span\u003e .output_schema \u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e TaskSchema\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eTaskPlannerAgent\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003e__init__\u003c/span\u003e(self):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        self\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003ellm \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e ChatOpenAI(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            openai_api_key\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            openai_api_base\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://openrouter.ai/api/v1\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            model_name\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;google/gemini-3-flash-preview\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        )\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e# System prompt\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        self\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003esystem_prompt \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        你是一个专业的任务规划助手。用户将提供任务描述或图片，你需要结合这些信息将其拆解为结构化步骤。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e        \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        self\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eagent \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e create_agent(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            model\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003eself\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003ellm,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            system_prompt\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003eself\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003esystem_prompt,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            response_format\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003eTaskSchema\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        )\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eplan\u003c/span\u003e(self, instruction: str, image_path: str \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eNone\u003c/span\u003e) \u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003e TaskSchema:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        content \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e [{\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;text\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;text\u0026#34;\u003c/span\u003e: instruction}]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e image_path:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            image_data \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e self\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003e_encode_image(image_path)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            content\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eappend({\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;type\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;image_url\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;image_url\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                    \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;url\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003ef\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;data:image/jpeg;base64,\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e{\u003c/span\u003eimage_data\u003cspan style=\"color:#e6db74\"\u003e}\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            })\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        result \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e self\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eagent\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003einvoke(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            {\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;messages\u0026#34;\u003c/span\u003e: [{\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;role\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;content\u0026#34;\u003c/span\u003e: content}]}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        )\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e result[\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;structured_response\u0026#34;\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003e_encode_image\u003c/span\u003e(self, image_path: str):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\u0026#34;\u0026#34;将本地图片转为 base64编码\u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ewith\u003c/span\u003e open(image_path, \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;rb\u0026#39;\u003c/span\u003e) \u003cspan style=\"color:#66d9ef\"\u003eas\u003c/span\u003e image_file:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e base64\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eb64encode(image_file\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eread())\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003edecode(\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;utf-8\u0026#39;\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e(args):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    agent \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e TaskPlannerAgent()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e step \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e agent\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eplan(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;搜索熊猫，并进入搜索结果的第一项内容\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003er\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;C:\\Users\\Levit\\Desktop\\1.jpg\u0026#34;\u003c/span\u003e)\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003esteps:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        print(step)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003eoutput_schema.py\u003c/code\u003e 规范Agent输出格式\u003c/p\u003e","title":"RPA Agent 探索"}]