diff --git a/.gitignore b/.gitignore index 28144b5..8cfd804 100644 --- a/.gitignore +++ b/.gitignore @@ -73,7 +73,11 @@ memory/skill_search/**/__pycache__/ restore_commit.txt sche_tasks/ -QUICK_START.md # CDP Bridge 密钥配置(首次运行自动生成) assets/tmwd_cdp_bridge/config.js **log.* + +# Reflect (ignore new files, whitelist existing) +reflect/* +!reflect/autonomous.py +!reflect/scheduler.py diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 82e07b7..9902969 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -1,409 +1,258 @@ -# GenericAgent 新手上手指南 +# 🚀 新手上手指南 -## 🎯 什么是 GenericAgent? - -GenericAgent 是一个极简、可自我进化的自主 Agent 框架。它的核心只有 **~3,300 行代码**,通过 **7 个原子工具 + 92 行 Agent Loop**,就能赋予任意 LLM 对你本地计算机的系统级控制能力。 - -**它能做什么?** -- 控制浏览器(保留登录态) -- 执行终端命令 -- 读写文件系统 -- 模拟键盘鼠标 -- 控制移动设备(ADB) -- 屏幕视觉识别 - -**最特别的是什么?** - -GenericAgent 不预设技能,而是**靠进化获得能力**。每解决一个新任务,它就会自动将执行路径固化为 Skill,供后续直接调用。使用时间越长,沉淀的技能越多,最终形成一棵完全属于你的专属技能树。 - -> 🤖 **自举实证**: 本项目的所有 Git 操作,从 `git init` 到每一条 commit message,都是 GenericAgent 自主完成的。作者全程未打开过一次终端。 +> 完全没接触过编程也没关系,跟着做就行。Mac / Windows 都适用。 +> +> 如果你已经有 Python 环境,直接跳到[第 2 步](#2-配置-api-key)。 --- -## 🚀 5 分钟快速开始 +## 1. 安装 Python -### 第一步:安装部署 +### Mac -**方法一:标准安装(推荐开发者)** +打开「终端」(启动台搜索 "终端" 或 "Terminal"),粘贴这行命令然后回车: ```bash -# 1. 克隆仓库 -git clone https://github.com/lsdefine/GenericAgent.git -cd GenericAgent - -# 2. 安装最小依赖 -pip install streamlit pywebview - -# 3. 配置 API Key -cp mykey_template.py mykey.py +brew install python ``` -**方法二:Windows 便携版(推荐新手)** - -1. [下载便携版](http://kw.fudan.edu.cn/resources/PC-Agent-Portable.zip) (19MB) -2. 解压到任意目录 -3. 双击运行即可 - -**方法三:Android(Termux)** +如果提示 `brew: command not found`,说明还没装 Homebrew,先粘贴这行: ```bash -cd /sdcard/ga -python agentmain.py +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` -### 第二步:配置 API Key +装完后再执行 `brew install python`。 -编辑 `mykey.py` 文件,填入你的 LLM API Key: +### Windows + +1. 打开 [python.org/downloads](https://www.python.org/downloads/),点黄色大按钮下载 +2. 运行安装包,**底部的 "Add Python to PATH" 一定要勾上** +3. 点 "Install Now" + +### 验证 + +终端 / 命令提示符里输入: + +```bash +python3 --version +``` + +看到 `Python 3.x.x` 就 OK。Windows 上也可以试 `python --version`。 + +> ⚠️ **版本提示**:推荐 **Python 3.11 或 3.12**。不要使用 3.14(与 pywebview 等依赖不兼容)。 + +--- + +## 2. 配置 API Key + +### 下载项目 + +1. 打开 [GitHub 仓库页面](https://github.com/lsdefine/GenericAgent) +2. 点绿色 **Code** 按钮 → **Download ZIP** +3. 解压到你喜欢的位置 + +### 创建配置文件 + +进入项目文件夹,把 `mykey_template.py` 复制一份,重命名为 `mykey.py`。 + +用任意文本编辑器打开 `mykey.py`,填入你的 API 信息。**选一种填就行**,不用的配置删掉或留着不管都行。 + +### 配置示例 + +**最常见的用法:** ```python -# 示例:使用 Claude -api_key = "sk-ant-xxx" # 你的 Anthropic API Key -model = "claude-3-5-sonnet-20241022" - -# 或使用其他模型 -# api_key = "your_openai_key" -# model = "gpt-4" +# 变量名含 'oai' → 走 OpenAI 兼容格式 (/chat/completions) +oai_config = { + 'apikey': 'sk-你的密钥', + 'apibase': 'http://你的API地址:端口', + 'model': '模型名称', +} ``` -支持的模型: -- Claude (Anthropic) -- GPT-4 / GPT-3.5 (OpenAI) -- Gemini (Google) -- Kimi (Moonshot) -- 其他兼容 OpenAI API 的模型 +```python +# 变量名含 'claude'(不含 'native')→ 走 Claude 兼容格式 (/messages) +claude_config = { + 'apikey': 'sk-你的密钥', + 'apibase': 'http://你的API地址:端口', + 'model': 'claude-sonnet-4-20250514', +} +``` -### 第三步:启动界面 +**使用标准工具调用格式(适合较弱模型):** + +```python +# 变量名同时含 'native' 和 'claude' → Claude 标准工具调用格式 +native_claude_config = { + 'apikey': 'sk-ant-你的密钥', + 'apibase': 'https://api.anthropic.com', + 'model': 'claude-sonnet-4-20250514', +} +``` + +> 💡 还支持 `native_oai_config`(OpenAI 标准工具调用)、`xai_config`(Grok)、`sider_cookie`(Sider)等,详见 `mykey_template.py` 中的注释。 + +### 关键规则 + +**变量命名决定接口格式**(不是模型名决定的): + +| 变量名包含 | 触发的 Session | 适用场景 | +|-----------|---------------|---------| +| `oai` | OpenAI 兼容 | 大多数 API 服务、OpenAI 官方 | +| `claude`(不含 `native`) | Claude 兼容 | Claude API 服务 | +| `native` + `claude` | Claude 标准工具调用 | 较弱模型推荐,工具调用更规范 | +| `native` + `oai` | OpenAI 标准工具调用 | 较弱模型推荐,工具调用更规范 | + +> 例:用 Claude 模型,但 API 服务提供的是 OpenAI 兼容接口 → 变量名用 `oai_xxx`。 + +**`apibase` 填写规则**(会自动拼接端点路径): + +| 你填的内容 | 系统行为 | +|-----------|---------| +| `http://host:2001` | 自动补 `/v1/chat/completions` | +| `http://host:2001/v1` | 自动补 `/chat/completions` | +| `http://host:2001/v1/chat/completions` | 直接使用,不拼接 | + +--- + +## 3. 初次启动 + +终端里进入项目文件夹,运行: ```bash -python launch.pyw +cd 你的解压路径 +python3 agentmain.py ``` -启动后会出现一个桌面悬浮窗,你可以直接在里面输入任务指令。 +这就是**命令行模式**,已经可以用了。你会看到一个输入提示符,直接打字发送任务即可。 + +试试你的第一个任务: + +``` +帮我在桌面创建一个 hello.txt,内容是 Hello World +``` + +> 💡 Windows 上如果 `python3` 不识别,换成 `python agentmain.py`。 --- -## 💡 基础使用 +## 4. 让 Agent 自己装依赖 -### 你的第一个任务 - -启动后,试试这些简单的任务: - -**文件操作:** -``` -"帮我在桌面创建一个 hello.txt 文件,内容是 Hello World" -``` - -**网页浏览:** -``` -"打开百度,搜索今天的天气" -``` - -**代码执行:** -``` -"用 Python 计算 1 到 100 的和" -``` - -### 理解 Agent 的工作方式 - -GenericAgent 的工作流程: +Agent 启动后,只需要一句话,它就会自己搞定所有依赖: ``` -你的指令 → Agent 理解任务 → 调用工具执行 → 返回结果 → 沉淀经验 +请查看你的代码,安装所有用得上的 python 依赖 ``` -**核心循环(92 行代码):** -1. **感知**: 读取当前环境状态 -2. **推理**: 分析任务,制定执行计划 -3. **执行**: 调用工具完成操作 -4. **记忆**: 将成功的执行路径写入记忆层 -5. **循环**: 继续下一步,直到任务完成 +Agent 会自己读代码、找出需要的包、全部装好。 ---- +> ⚠️ 如果遇到网络问题导致 Agent 无法调用 API,可能需要先手动装一个包: +> ```bash +> pip install requests +> ``` -## 🧬 进阶:Skill 系统 +### 升级到图形界面 -### 什么是 Skill? +依赖装完后,就可以用 GUI 模式了: -Skill 是 GenericAgent 自动沉淀的任务执行模板。当你第一次完成某个任务时,Agent 会将整个执行流程固化为一个 Skill,下次遇到类似任务就能直接调用。 - -**举个例子:** - -| 你说的话 | 第一次 Agent 做了什么 | 之后每次 | -|---------|---------------------|---------| -| "监控股票并提醒我" | 安装 mootdx → 构建选股流程 → 配置定时任务 → 保存 Skill | **一句话启动** | -| "用 Gmail 发这个文件" | 配置 OAuth → 编写发送脚本 → 保存 Skill | **直接可用** | - -### 如何使用 Skill? - -Skill 会自动保存在 `memory/` 目录下。你不需要手动管理,Agent 会在需要时自动调用。 - -**查看已有的 Skill:** ```bash -ls memory/*.md +python3 launch.pyw ``` -**手动触发 Skill:** +启动后会出现一个桌面悬浮窗,直接在里面输入任务指令。 + +### 可选:让 Agent 帮你做的事 + ``` -"使用之前保存的股票监控 Skill" +请帮我建立 git 连接,方便以后更新代码 ``` -### 培养你的专属 Agent +Agent 会自动配好。如果你电脑上没有 Git,它也会帮你下载 portable 版。 -使用时间越长,你的 Agent 积累的 Skill 越多: +``` +请帮我在桌面创建一个 launch.pyw 的快捷方式 +``` -- **第 1 周**: 基础文件操作、简单网页浏览 -- **第 1 个月**: 自动化工作流、数据处理脚本 -- **第 3 个月**: 复杂的多步骤任务、跨平台操作 -- **第 6 个月**: 一套任何人都没有的专属技能树 +这样以后双击桌面图标就能启动,不用再开终端了。 --- -## 🎮 实用玩法 +## 5. 能力解锁 + +环境跑起来之后,你可以逐步解锁更多能力。每一项都只需要**对 Agent 说一句话**: + +### 基础能力 + +| 能力 | 对 Agent 说 | 说明 | +|------|-----------|------| +| **PowerShell 脚本执行** | `帮我解锁当前用户的 PowerShell ps1 执行权限` | Windows 默认禁止运行 .ps1 脚本 | +| **全局文件搜索** | `安装并配置 Everything 命令行工具进 PATH` | 毫秒级全盘文件搜索 | ### 浏览器自动化 -GenericAgent 注入真实浏览器,保留你的登录态: +| 能力 | 对 Agent 说 | 说明 | +|------|-----------|------| +| **Web 工具解锁** | `执行 web setup sop,解锁 web 工具` | 注入浏览器插件,使 Agent 能直接操控网页 | + +解锁后,Agent 可以在**保留你登录态**的真实浏览器中操作: ``` -"打开淘宝,搜索 iPhone 15,按价格排序" -"登录我的 Gmail,查看未读邮件" -"在美团上帮我点一杯奶茶" +打开淘宝,搜索 iPhone 16,按价格排序 +去 B 站,查看我最近看过的历史视频 ``` -**可用工具:** -- `web_scan`: 读取网页内容 -- `web_execute_js`: 执行 JavaScript 控制页面 +### 进阶能力 -### 文件和代码处理 +| 能力 | 对 Agent 说 | 说明 | +|------|-----------|------| +| **OCR** | `用rapidocr配置你的ocr能力并存入记忆` | 让 Agent 能"看到"屏幕文字 | +| **屏幕视觉** | `仿造你的llmcore,写个调用vision的能力并存入记忆` | 让 Agent 能"看到"屏幕内容 | +| **移动端控制** | `配置 ADB 环境,准备连接安卓设备` | 通过 USB/WiFi 控制 Android 手机 | -``` -"分析这个 Python 项目的代码结构" -"把这个 CSV 文件转换成 Excel" -"批量重命名这个文件夹里的图片" -``` +### 聊天平台接入(可选) -**可用工具:** -- `file_read`: 读取文件 -- `file_write`: 写入文件 -- `file_patch`: 精确修改文件 -- `code_run`: 执行代码 +接入后可以随时随地通过手机给电脑上的 Agent 发指令。 -### 移动设备控制(ADB) +对 Agent 说:`看你的代码,帮我配置 XX 平台的机器人接入` -通过 ADB 控制 Android 设备: +支持的平台:**微信个人Bot** / QQ / 飞书 / 企业微信 / 钉钉 / Telegram -``` -"打开支付宝,查看我的账单" -"在微信上给张三发消息" -"截取手机屏幕并保存" -``` +> Agent 会自动读取代码、引导你完成配置。 -### 定时任务 +### 高级模式 -``` -"每天早上 8 点提醒我查看邮件" -"每小时检查一次股票价格" -"每周一生成上周的工作总结" -``` +以下模式全部**自文档化**——不用查手册,直接问 Agent 即可: + +| 模式 | 对 Agent 说 | +|------|------------| +| **Reflect(反射)** | `查看你的代码,告诉我你的 reflect 模式怎么启用` | +| **计划任务** | `查看你的代码,告诉我你的计划任务模式怎么启用` | +| **Plan(规划)** | `查看你的代码,告诉我你的 plan 模式怎么启用` | +| **SubAgent(子代理)** | `查看你的代码,告诉我你的 subagent 模式怎么启用` | +| **自主探索** | `查看你的代码,告诉我你的自主探索模式怎么启用` | + +> 💡 这就是 GenericAgent 的核心设计理念:**代码即文档**。Agent 能读懂自己的源码,所以任何功能你都可以直接问它。 --- -## 🤖 接入聊天平台(可选) +## 💡 使用越久越强 -### 为什么要接入 Bot? +GenericAgent 不预设技能,而是**靠使用进化**。每完成一个新任务,它会自动将执行路径固化为 Skill,下次遇到类似任务直接调用。 -将 GenericAgent 接入聊天平台后,你可以: -- 随时随地通过手机控制你的电脑 -- 多人协作使用同一个 Agent -- 接收 Agent 的主动通知和提醒 +你不需要管理这些 Skill,Agent 会自动处理。使用时间越长,积累的技能越多,最终形成一棵完全属于你的专属技能树。 -### 支持的平台 +> 💡 如果你觉得某些重要信息 Agent 没有记住,可以直接告诉它:`把这个记到你的记忆里`,它会主动记忆。 -- **QQ Bot**: WebSocket 长连接,无需公网 webhook -- **飞书(Lark)**: 支持富文本、图片、文件、音频 -- **企业微信(WeCom)**: 企业内部使用 -- **钉钉(DingTalk)**: 企业协作 -- **Telegram**: 国际用户 +**其他 Claw 的 Skill 也可以直接复用:** -### 快速配置指南 +- 让 Agent 搜索:`帮我找个做 XXX 的 skill` → 完成后 → `加入你的记忆中` +- 直接指定来源:`访问 XXX 文件夹/URL,按照这个 skill 做 XXX` -**以 QQ Bot 为例:** +**保持更新:** -1. 在 [QQ 开放平台](https://q.qq.com) 创建机器人 -2. 获取 AppID 和 AppSecret -3. 在 `mykey.py` 中配置: +对 Agent 说:`git 更新你的代码,然后看看 commit 有什么新功能` -```python -qq_app_id = "YOUR_APP_ID" -qq_app_secret = "YOUR_APP_SECRET" -qq_allowed_users = ["YOUR_USER_OPENID"] # 或 ['*'] 公开访问 -``` +> Agent 会自动 pull 最新代码并解读 commit log,告诉你新增了什么能力。 -4. 启动 Bot: - -```bash -pip install qq-botpy -python frontends/qqapp.py -# 或与桌面窗口一起启动 -python launch.pyw --qq -``` - -**其他平台配置详见:** -- 飞书: [assets/SETUP_FEISHU.md](assets/SETUP_FEISHU.md) -- 企业微信: `python launch.pyw --wecom` -- 钉钉: `python launch.pyw --dingtalk` -- Telegram: `python frontends/tgapp.py` - ---- - -## 📚 记忆系统详解 - -### 三层记忆架构 - -GenericAgent 的记忆系统分为三层: - -**L0 — 元规则(Meta Rules)** -- Agent 的基础行为规则 -- 系统约束和安全边界 -- 不可修改的核心逻辑 - -**L2 — 全局事实(Global Facts)** -- 长期运行中积累的稳定知识 -- 环境配置、用户偏好 -- 存储在 `memory/global_mem.txt` - -**L3 — 任务 Skills(SOPs)** -- 完成特定任务的操作流程 -- 自动沉淀的执行模板 -- 存储在 `memory/*.md` - -### 如何查看和管理记忆 - -**查看全局记忆:** -```bash -cat memory/global_mem.txt -``` - -**查看所有 Skills:** -```bash -ls memory/*.md -``` - -**手动编辑记忆:** -```bash -# 不建议新手操作,Agent 会自动管理 -vim memory/global_mem.txt -``` - ---- - -## 🔧 常见问题 - -### 安装问题 - -**Q: pip install 失败怎么办?** - -A: 尝试使用国内镜像源: -```bash -pip install -i https://pypi.tuna.tsinghua.edu.cn/simple streamlit pywebview -``` - -**Q: Windows 上启动失败?** - -A: 建议使用便携版,或检查 Python 版本(需要 3.8+) - -### 使用问题 - -**Q: Agent 执行任务失败?** - -A: 检查: -1. API Key 是否正确配置 -2. 网络连接是否正常 -3. 查看 `temp/` 目录下的日志文件 - -**Q: 如何让 Agent 停止当前任务?** - -A: 在界面中输入 "停止" 或 "取消" - -**Q: Agent 的回复太慢?** - -A: 可以尝试: -1. 切换到更快的模型(如 GPT-3.5) -2. 减少任务的复杂度 -3. 检查网络延迟 - -### 进阶问题 - -**Q: 如何自定义 Agent 的行为?** - -A: 编辑 `memory/global_mem.txt`,添加你的偏好设置 - -**Q: 如何备份我的 Skills?** - -A: 直接复制 `memory/` 目录即可 - -**Q: 可以同时运行多个 Agent 实例吗?** - -A: 可以,但需要使用不同的工作目录 - ---- - -## 💪 最佳实践 - -### 新手建议 - -1. **从简单任务开始**: 先尝试文件操作、网页浏览等基础任务 -2. **观察 Agent 的执行过程**: 理解它是如何调用工具的 -3. **让 Agent 自己探索**: 不要过度干预,让它自主学习 -4. **定期查看 Skills**: 了解 Agent 积累了哪些能力 - -### 进阶技巧 - -1. **组合使用多个工具**: 让 Agent 完成复杂的多步骤任务 -2. **利用记忆系统**: 在 `global_mem.txt` 中记录常用的配置和偏好 -3. **编写自定义脚本**: 通过 `code_run` 扩展 Agent 的能力 -4. **接入多个聊天平台**: 实现跨平台的统一控制 - -### 安全注意事项 - -1. **不要在公共环境运行**: Agent 有系统级权限,注意安全 -2. **谨慎授权**: 使用 Bot 时,设置 `allowed_users` 白名单 -3. **定期备份**: 重要的 Skills 和配置要及时备份 -4. **监控执行日志**: 定期检查 `temp/` 目录下的日志 - ---- - -## 🌟 社区和支持 - -### 获取帮助 - -- **GitHub Issues**: [提交问题](https://github.com/lsdefine/GenericAgent/issues) -- **微信交流群**: 扫描下方二维码加入 - -
- -
- -### 贡献指南 - -欢迎贡献代码、文档或 Skills! - -1. Fork 本仓库 -2. 创建你的特性分支 -3. 提交你的改动 -4. 发起 Pull Request - ---- - -## 📖 延伸阅读 - -- [完整 README](README.md) - 项目详细介绍 -- [新用户欢迎文档](WELCOME_NEW_USER.md) - 详细的引导流程 -- [快速开始指南](QUICK_START.pdf) - PDF 版本的快速入门 -- [飞书配置指南](assets/SETUP_FEISHU.md) - 飞书 Bot 详细配置 - ---- - -**🎉 现在,开始你的 GenericAgent 之旅吧!** - -记住: GenericAgent 不是一个预设好的工具,而是一个会随着你的使用不断进化的伙伴。使用时间越长,它就越懂你,越强大。 \ No newline at end of file +> 更多细节请参阅 [README.md](README.md) 或 [详细版图文教程](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)。 \ No newline at end of file diff --git a/QUICK_START.pdf b/QUICK_START.pdf deleted file mode 100644 index c20927a..0000000 Binary files a/QUICK_START.pdf and /dev/null differ diff --git a/README.md b/README.md index cfebd50..188e3b5 100644 --- a/README.md +++ b/README.md @@ -83,94 +83,11 @@ cp mykey_template.py mykey.py python launch.pyw ``` -#### Method 2: Windows Portable Version (Recommended for beginners) +Full guide: [GETTING_STARTED.md](GETTING_STARTED.md) -[Download portable version](http://kw.fudan.edu.cn/resources/PC-Agent-Portable.zip) (19MB, unzip and run) - -Full guide: [WELCOME_NEW_USER.md](WELCOME_NEW_USER.md) - -#### Method 3: Android (Termux) - -```bash -cd /sdcard/ga -python agentmain.py -``` --- -## 🤖 Bot Interfaces (Optional) - -### QQ Bot - -Uses `qq-botpy` WebSocket long connection — **no public webhook required**: - -```bash -pip install qq-botpy -``` - -Add to `mykey.py`: - -```python -qq_app_id = "YOUR_APP_ID" -qq_app_secret = "YOUR_APP_SECRET" -qq_allowed_users = ["YOUR_USER_OPENID"] # or ['*'] for public access -``` - -```bash -python frontends/qqapp.py -# or launch together with the desktop floating window -python launch.pyw --qq -``` - -> Create a bot at the [QQ Open Platform](https://q.qq.com) to get AppID / AppSecret. After the first message, user openid is logged in `temp/qqapp.log`. - - -### Lark (Feishu) - -```bash -pip install lark-oapi -python frontends/fsapp.py # or python launch.pyw --feishu -``` - -```python -fs_app_id = "cli_xxx" -fs_app_secret = "xxx" -fs_allowed_users = ["ou_xxx"] # or ['*'] -``` - -**Inbound support**: text, rich text post, images, files, audio, media, interactive cards / share cards -**Outbound support**: streaming progress cards, image replies, file / media replies -**Vision model**: Images are sent as true multimodal input to OpenAI Vision-compatible backends on the first turn - -Full setup: [assets/SETUP_FEISHU.md](assets/SETUP_FEISHU.md) - - -### WeCom (Enterprise WeChat) - -```bash -pip install wecom_aibot_sdk -python frontends/wecomapp.py # or python launch.pyw --wecom -``` - -```python -wecom_bot_id = "your_bot_id" -wecom_secret = "your_bot_secret" -wecom_allowed_users = ["your_user_id"] -wecom_welcome_message = "Hello, I'm online." -``` - -### DingTalk - -```bash -pip install dingtalk-stream -python frontends/dingtalkapp.py # or python launch.pyw --dingtalk -``` - -```python -dingtalk_client_id = "your_app_key" -dingtalk_client_secret = "your_app_secret" -dingtalk_allowed_users = ["your_staff_id"] # or ['*'] -``` - +## 🤖 Bot Interface (Optional) ### Telegram Bot @@ -335,23 +252,23 @@ cp mykey_template.py mykey.py python launch.pyw ``` -#### 方法二:Windows 便携版(推荐新手) - -[下载便携版](http://kw.fudan.edu.cn/resources/PC-Agent-Portable.zip)(19MB,解压即用) - -完整引导流程见 [WELCOME_NEW_USER.md](WELCOME_NEW_USER.md)。 - -#### 方法三:Android(Termux) - -```bash -cd /sdcard/ga -python agentmain.py -``` +完整引导流程见 [GETTING_STARTED.md](GETTING_STARTED.md)。 --- ## 🤖 Bot 接口(可选) +### 微信 Bot(个人微信) + +无需额外配置,扫码登录即可: + +```bash +pip install pycryptodome qrcode requests +python frontends/wechatapp.py +``` + +> 首次启动会弹出二维码,用微信扫码完成绑定。之后通过微信消息与 Agent 交互。 + ### QQ Bot 使用 `qq-botpy` WebSocket 长连接,**无需公网 webhook**: @@ -370,8 +287,6 @@ qq_allowed_users = ["YOUR_USER_OPENID"] # 或 ['*'] 公开访问 ```bash python frontends/qqapp.py -# 或与桌面悬浮窗一起启动 -python launch.pyw --qq ``` > 在 [QQ 开放平台](https://q.qq.com) 创建机器人获取 AppID / AppSecret。首次消息后,用户 openid 记录于 `temp/qqapp.log`。 @@ -380,7 +295,7 @@ python launch.pyw --qq ```bash pip install lark-oapi -python frontends/fsapp.py # 或 python launch.pyw --feishu +python frontends/fsapp.py ``` ```python @@ -400,7 +315,7 @@ fs_allowed_users = ["ou_xxx"] # 或 ['*'] ```bash pip install wecom_aibot_sdk -python frontends/wecomapp.py # 或 python launch.pyw --wecom +python frontends/wecomapp.py ``` ```python @@ -414,7 +329,7 @@ wecom_welcome_message = "你好,我在线上。" ```bash pip install dingtalk-stream -python frontends/dingtalkapp.py # 或 python launch.pyw --dingtalk +python frontends/dingtalkapp.py ``` ```python @@ -424,19 +339,6 @@ dingtalk_allowed_users = ["your_staff_id"] # 或 ['*'] ``` -### Telegram Bot - -```python -# mykey.py -tg_bot_token = 'YOUR_BOT_TOKEN' -tg_allowed_users = [YOUR_USER_ID] -``` - -```bash -python frontends/tgapp.py -``` - - ## 📊 与同类产品对比 | 特性 | GenericAgent | OpenClaw | Claude Code | diff --git a/WELCOME_NEW_USER.md b/WELCOME_NEW_USER.md deleted file mode 100644 index 322b171..0000000 --- a/WELCOME_NEW_USER.md +++ /dev/null @@ -1,66 +0,0 @@ -# 🚀 欢迎使用物理级全能执行者 - -这是您的 Agent 初始化指引。请按以下阶段操作: - -## 第一阶段:环境启动 (Initial Ignition) - -1. **Python 环境检查** - - 确保安装了 Python 3.10+。几乎零额外依赖。 - - `requests` 是唯一的第三方硬依赖,但多数 Python 发行版已预装。若缺失:`pip install requests` - -2. **配置身份密钥 (Credentials)** - - 复制 `mykey_template.py` 为 `mykey.py` 并填入 API Key。 - -3. **唤醒 Agent(CLI 最小启动)** - - 运行 `python agentmain.py`,进入 CLI 交互模式。此时 Agent 已可工作。 - - 后续所有依赖(包括 GUI 模式所需的包)都可以叫 Agent 帮你安装。 - -4. **(可选)升级为 GUI 模式** - - **指令**:`pip install streamlit pywebview` - - 安装后运行 `launch.pyw`,即可使用悬浮窗 GUI 界面。 - -## 第二阶段:能力激活 (Ability Activation) - -在此阶段,您只需对我发送指令,所有物理操作由我完成: - -1. **解锁 PowerShell 脚本执行权限** - - **指令**:`请帮我当前用户解锁 powershell 的 ps1 执行权限。` - -2. **配置全局文件搜索 (Everything CLI)** - - **指令**:`安装并配置 everything 命令行工具进PATH。` - -3. **Web 自动化环境配置 (Web Setup SOP)** - - **指令**:`执行 web setup sop 解锁 web 工具` - - **物理影响**:我将引导您完成浏览器插件安装,并注入核心脚本,使我能够直接操控您的浏览器页面。 - -4. **补全常用工具库** - - **指令**:`安装常用 Python 自动化包(如 requests, pandas, pyperclip)。` - -5. **配置网络代理 (Proxy Setup)** - - **指令**:`告诉我能用的系统代理` - -6. **激活视觉理解能力 (OCR & Vision)** - - **指令**:`配置截图与 OCR 工具,解锁你的屏幕视觉。` - -7. **移动端自动化准备 (Android/ADB)** - - **指令**:`配置 ADB 环境,准备连接安卓设备。` - -## 第三阶段:记忆与知识体系建造 (Knowledge Architecture) - -当环境就绪后,您可以让我构建您的“数字大脑”: - -1. **自动化 SOP 沉淀** - - **指令**:`记录刚才的操作流程,生成一套自动化 SOP。` - -2. **现有技能挂载 (SOP Retrieval)** - - **指令**:`读取现有 SOP 目录,告诉我你现在掌握的所有技能。` - -3. **物理资产审计 (Asset Audit)** - - **指令**:`帮我建立物理资产清单,扫描并记录我常用的工具路径。` - -4. **Web 调研实战 (Research & Report)** - - **指令**:`搜索 [关键词],并根据网页内容整理一份简易 Markdown 报告保存到当前目录。` - - **物理影响**:我将自动打开浏览器,利用 Web 驱动采集多个页面信息,通过逻辑整合后在您的本地文件夹生成物理文件。 - ---- -**💡 提示**:您可以直接复制上述 `指令` 发送给我,我将立刻执行对应的物理操作。 diff --git a/hub.pyw b/hub.pyw new file mode 100644 index 0000000..97ead53 --- /dev/null +++ b/hub.pyw @@ -0,0 +1,257 @@ +# launcher.pyw - GenericAgent 服务启动器 +# 纯 tkinter + 标准库,零第三方依赖,跨平台 +import os, sys, socket, subprocess, threading +import tkinter as tk +from tkinter import ttk +from collections import deque + +LOCK_PORT = 19735 +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def acquire_singleton(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.bind(('127.0.0.1', LOCK_PORT)) + s.listen(1) + return s + except OSError: + return None + + +def discover_services(): + services = [] + reflect_dir = os.path.join(BASE_DIR, 'reflect') + if os.path.isdir(reflect_dir): + for f in sorted(os.listdir(reflect_dir)): + if f.endswith('.py') and not f.startswith('_'): + services.append({ + 'name': 'reflect/' + f, + 'cmd': [sys.executable, 'agentmain.py', '--reflect', 'reflect/' + f], + }) + frontends_dir = os.path.join(BASE_DIR, 'frontends') + if os.path.isdir(frontends_dir): + for f in sorted(os.listdir(frontends_dir)): + if f.endswith('app.py') and f != 'chatapp_common.py': + if f == 'stapp.py': + cmd = [sys.executable, '-m', 'streamlit', 'run', + 'frontends/' + f, '--server.headless=true'] + else: + cmd = [sys.executable, 'frontends/' + f] + services.append({'name': 'frontends/' + f, 'cmd': cmd}) + return services + + +class ServiceManager: + def __init__(self): + self.procs = {} + self.buffers = {} + + def start(self, name, cmd): + if name in self.procs and self.procs[name].poll() is None: + return + self.buffers[name] = deque(maxlen=500) + env = os.environ.copy() + env['PYTHONUNBUFFERED'] = '1' + kw = dict(cwd=BASE_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, env=env) + if sys.platform == 'win32': + kw['creationflags'] = subprocess.CREATE_NO_WINDOW + proc = subprocess.Popen(cmd, **kw) + self.procs[name] = proc + threading.Thread(target=self._reader, args=(name, proc), daemon=True).start() + + def _reader(self, name, proc): + try: + for line in proc.stdout: + self.buffers[name].append(line) + except Exception: + pass + + def stop(self, name): + proc = self.procs.get(name) + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + + def is_running(self, name): + proc = self.procs.get(name) + return proc is not None and proc.poll() is None + + def stop_all(self): + for name in list(self.procs): + self.stop(name) + + def get_output(self, name): + buf = self.buffers.get(name) + return list(buf) if buf else [] + + +class LauncherApp: + def __init__(self, root): + self.root = root + self.root.title('GenericAgent Launcher') + self.root.geometry('720x740') + self.root.protocol('WM_DELETE_WINDOW', self.on_close) + + self.mgr = ServiceManager() + self.services = discover_services() + self.check_vars = {} + self.selected = None + + self._build_ui() + self._poll() + + def _build_ui(self): + # 标题行:左边标签,右边 Rescan 按钮 + header = ttk.Frame(self.root) + header.pack(fill='x', padx=8, pady=(8, 0)) + ttk.Label(header, text='Services', font=('', 10, 'bold')).pack(side='left') + ttk.Button(header, text='\u27f3 Rescan', width=10, + command=self._rescan).pack(side='right') + + svc_frame = ttk.LabelFrame(self.root, padding=5) + svc_frame.pack(fill='x', padx=8, pady=(2, 4)) + + self.svc_container = ttk.Frame(svc_frame) + self.svc_container.pack(fill='x') + + self.status_labels = {} + self.row_frames = {} + self.name_labels = {} + self._build_service_rows() + + self.output_frame = ttk.LabelFrame(self.root, text='Output', padding=5) + self.output_frame.pack(fill='both', expand=True, padx=8, pady=(4, 8)) + + self.output_text = tk.Text( + self.output_frame, wrap='word', state='disabled', + bg='#1e1e1e', fg='#d4d4d4', + font=('Consolas', 9), insertbackground='white') + sb = ttk.Scrollbar(self.output_frame, command=self.output_text.yview) + self.output_text.configure(yscrollcommand=sb.set) + sb.pack(side='right', fill='y') + self.output_text.pack(fill='both', expand=True) + + def _build_service_rows(self): + for svc in self.services: + name = svc['name'] + row = tk.Frame(self.svc_container, cursor='hand2', padx=4, pady=2) + row.pack(fill='x', pady=1) + self.row_frames[name] = row + + running = self.mgr.is_running(name) + var = self.check_vars.get(name, tk.BooleanVar(value=running)) + if running: + var.set(True) + self.check_vars[name] = var + cb = ttk.Checkbutton( + row, variable=var, + command=lambda n=name, v=var, s=svc: self._toggle(n, v, s)) + cb.pack(side='left') + + name_lbl = tk.Label(row, text=name, anchor='w', cursor='hand2', + bg=row.cget('bg')) + name_lbl.pack(side='left', fill='x', expand=True) + self.name_labels[name] = name_lbl + + st = 'running' if running else 'stopped' + fg = 'green' if running else 'gray' + lbl = ttk.Label(row, text=st, foreground=fg, width=10) + lbl.pack(side='right') + self.status_labels[name] = lbl + + name_lbl.bind('', lambda e, n=name: self._select(n)) + row.bind('', lambda e, n=name: self._select(n)) + + def _rescan(self): + # 记住正在运行的服务 + running_names = {n for n in self.mgr.procs if self.mgr.is_running(n)} + # 清除旧行 + for w in self.svc_container.winfo_children(): + w.destroy() + self.status_labels.clear() + self.row_frames.clear() + self.name_labels.clear() + # 清除不再运行的 check_vars + old_vars = {k: v for k, v in self.check_vars.items() if k in running_names} + self.check_vars.clear() + self.check_vars.update(old_vars) + # 重新扫描 + self.services = discover_services() + self._build_service_rows() + # 如果选中的服务不在新列表中,清除选中 + svc_names = {s['name'] for s in self.services} + if self.selected and self.selected not in svc_names: + self.selected = None + self.output_frame.configure(text='Output') + + def _toggle(self, name, var, svc): + if var.get(): + self.mgr.start(name, svc['cmd']) + self._select(name) + else: + self.mgr.stop(name) + + def _select(self, name): + self.selected = name + # 高亮选中行 + for n, row in self.row_frames.items(): + if n == name: + row.configure(bg='#cce5ff') + self.name_labels[n].configure(bg='#cce5ff') + else: + row.configure(bg='SystemButtonFace') + self.name_labels[n].configure(bg='SystemButtonFace') + self.output_frame.configure(text=f'Output - {name}') + self.root.after(50, self._refresh_output) + + def _refresh_output(self): + if not self.selected: + return + lines = self.mgr.get_output(self.selected) + self.output_text.configure(state='normal') + self.output_text.delete('1.0', 'end') + self.output_text.insert('end', ''.join(lines[-200:])) + self.output_text.configure(state='disabled') + self.output_text.see('end') + + def _poll(self): + for svc in self.services: + name = svc['name'] + running = self.mgr.is_running(name) + lbl = self.status_labels[name] + if running: + lbl.configure(text='running', foreground='green') + else: + lbl.configure(text='stopped', foreground='gray') + if self.check_vars[name].get(): + self.check_vars[name].set(False) + self._refresh_output() + self.root.after(1000, self._poll) + + def on_close(self): + self.mgr.stop_all() + self.root.destroy() + + +if __name__ == '__main__': + lock = acquire_singleton() + if lock is None: + try: + import tkinter.messagebox as mb + r = tk.Tk() + r.withdraw() + mb.showinfo('Launcher', 'Already running.') + r.destroy() + except Exception: + pass + sys.exit(0) + + root = tk.Tk() + app = LauncherApp(root) + root.mainloop() + lock.close() diff --git a/launch.pyw b/launch.pyw index adf668b..12debef 100644 --- a/launch.pyw +++ b/launch.pyw @@ -109,13 +109,9 @@ if __name__ == '__main__': else: print('[Launch] DingTalk Bot not enabled (use --dingtalk to start)') if not args.no_sched: - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM); sock.bind(('127.0.0.1', 45762)); sock.listen(1) - scheduler_proc = subprocess.Popen([sys.executable, os.path.join(script_dir, "agentmain.py"), "--reflect", os.path.join(script_dir, "reflect", "scheduler.py"), "--llm_no", str(args.llm_no)], creationflags=subprocess.CREATE_NO_WINDOW if os.name=='nt' else 0); - atexit.register(lambda: (scheduler_proc.kill(), sock.close())) - print('[Launch] Task Scheduler started') - except OSError: - print('[Launch] Task Scheduler already running (port occupied)') + scheduler_proc = subprocess.Popen([sys.executable, os.path.join(script_dir, "agentmain.py"), "--reflect", os.path.join(script_dir, "reflect", "scheduler.py"), "--llm_no", str(args.llm_no)], creationflags=subprocess.CREATE_NO_WINDOW if os.name=='nt' else 0) + atexit.register(scheduler_proc.kill) + print('[Launch] Task Scheduler started (duplicate prevented by scheduler port lock)') else: print('[Launch] Task Scheduler disabled (--no-sched)') monitor_thread = threading.Thread(target=idle_monitor, daemon=True) diff --git a/llmcore.py b/llmcore.py index 42f48f7..8e812d5 100644 --- a/llmcore.py +++ b/llmcore.py @@ -19,7 +19,7 @@ def compress_history_tags(messages, keep_recent=10, max_len=800): Supports both prompt-style (ClaudeSession/LLMSession) and content-style (NativeClaudeSession) messages.""" compress_history_tags._cd = getattr(compress_history_tags, '_cd', 0) + 1 if compress_history_tags._cd % 5 != 0: return messages - _before = sum(len(json.dumps(m)) for m in messages) + _before = sum(len(json.dumps(m, ensure_ascii=False)) for m in messages) _pats = {tag: re.compile(rf'(<{tag}>)([\s\S]*?)()') for tag in ('thinking', 'tool_use', 'tool_result')} def _trunc(text): for pat in _pats.values(): text = pat.sub(lambda m: m.group(1) + m.group(2)[:max_len] + '...' + m.group(3) if len(m.group(2)) > max_len else m.group(0), text) @@ -34,7 +34,7 @@ def compress_history_tags(messages, keep_recent=10, max_len=800): for block in c: if isinstance(block, dict) and block.get('type') == 'text' and isinstance(block.get('text'), str): block['text'] = _trunc(block['text']) - print(f"[Cut] {_before} -> {sum(len(json.dumps(m)) for m in messages)}") + print(f"[Cut] {_before} -> {sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)}") return messages def auto_make_url(base, path): diff --git a/memory/tmwebdriver_sop.md b/memory/tmwebdriver_sop.md index a004fc2..bdd7c06 100644 --- a/memory/tmwebdriver_sop.md +++ b/memory/tmwebdriver_sop.md @@ -5,6 +5,10 @@ - 非Selenium/Playwright,不需调试浏览器或新数据目录 - 支撑 `web_scan`(只读DOM) / `web_execute_js`(执行JS) 等高层工具 +## 通用限制 +- ⚠web_execute_js中**禁止使用await**,会报SyntaxError(非async上下文)(BBS#34验证) + - 需要异步操作时用`.then()`链式调用或回调 + ## 限制(isTrusted) - JS dispatch的事件`isTrusted=false`,敏感操作(文件上传/部分按钮)会被浏览器拦截 - ⭐**首选绕过:CDP桥**——CDP派发的Input事件是浏览器原生级别(isTrusted=true),且无需前台,见下方CDP章节 @@ -61,12 +65,67 @@ document.body.appendChild(el); // 响应写回el.textContent - ⭐batch混合:`{cmd:'batch', commands:[{cmd:'cookies'},{cmd:'tabs'},{cmd:'cdp',...},...]}` - 返回`{ok:true, results:[...]}`,一次请求多命令,CDP懒attach复用session - `$N.path`引用第N个结果字段(0-indexed),如`"nodeId":"$2.root.nodeId"` - - 典型:文件上传三连 getDocument→querySelector(input[type=file])→setFileInputFiles + - ⚠batch前序命令失败时后续`$N`引用拿到undefined,整条链路**静默失败不报错**,需检查返回results数组中每项的ok状态(未验证,BBS#46) + - 典型:文件上传三连 getDocument(**depth:1**性能优化,200ms+→个位数ms)→querySelector(input[type=file])→setFileInputFiles(未验证,BBS#38) + - ⚠nodeId路径一致性:getDocument+querySelector路径和performSearch+getSearchResults路径的nodeId**不互通**,同一batch内不可混用(未验证,BBS#45) + - ⚠文件上传后前端框架(React/Vue)可能不感知→JS补发**两个事件**(Vue3需input事件而非仅change)(未验证,BBS#35/#39): + ```js + el.dispatchEvent(new Event('input', {bubbles:true})); + el.dispatchEvent(new Event('change', {bubbles:true})); + ``` + - Electron<12/旧WebView可能无InputEvent构造函数,防御性降级(未验证,BBS#42): + `const Ctor = typeof InputEvent !== 'undefined' ? InputEvent : Event; el.dispatchEvent(new Ctor('input', {bubbles:true}));` + - 极端情况(框架仍不响应):Runtime.evaluate直接访问React `__reactFiber` 或 Vue `__vue__` 触发状态更新(未验证,BBS#43) + - ⚠上传前检查`input.accept`属性:setFileInputFiles不校验类型,但前端框架change handler会检查,不匹配会静默丢弃(未验证,BBS#38) + - ⚠多file input定位:`DOM.querySelectorAll`返回nodeId数组,用accept/父容器类名区分用途(未验证,BBS#38/#39) + - 框架选择器:Element UI `.el-upload__input` | Ant Design `.ant-upload input[type=file]` | Naive UI `.n-upload-trigger input[type=file]` | Dropzone `.dz-hidden-input`(未验证,BBS#39) + - ⚠Dropzone拖拽上传:90%底层仍创建隐藏``,先querySelectorAll('input[type=file]')全局扫(未验证,BBS#35/#38) + - ⭐轻量元素存在检测:`DOM.performSearch({query:'input[type=file]'})`返回resultCount,不触发DOM树构建,轮询等待元素时避免重复getDocument(未验证,BBS#39) + - performSearch支持三种语法:CSS选择器 / XPath(`//input[@type='file']`) / 纯文本,自动识别(未验证,BBS#41) + - ⭐瞬态file input处理(Ant Design等框架点击上传按钮时动态创建input,上传完立即销毁)(未验证,BBS#42/#43): + - 方案A(批处理):在同一batch内完成 performSearch→getSearchResults→setFileInputFiles→**discardSearchResults**,缩小input被销毁的时间窗口,discardSearchResults防searchId泄漏(未验证,BBS#46) + - 方案B(事件监听):`DOM.enable`后监听`DOM.childNodeInserted`事件捕获input创建瞬间,零延迟拿到nodeId + - 前提:须先对document.body的nodeId调`DOM.requestChildNodes`,否则CDP不推送子树变更 + - ⚠`DOM.disable`会使所有已获取nodeId失效,setFileInputFiles必须在disable之前。正确时序:DOM.enable→requestChildNodes→[等事件]→setFileInputFiles→DOM.disable(未验证,BBS#45) + - 方案C(猴子补丁兜底):Runtime.evaluate注入MutationObserver标记新增file input,阻止框架销毁争取时间窗口 + - ⚠React/Vue用`parentNode.removeChild(node)`而非`node.remove()`,需patch `Element.prototype.removeChild`过滤`input[type=file]`(未验证,BBS#45) + - ⚠Svelte等框架可能用`replaceChild`或`textContent=''`清空父容器间接移除,绕过removeChild补丁,极端场景性价比低建议回退方案B(未验证,BBS#46) + - ⚠阻止销毁会内存泄漏,用完后手动清理被标记的节点(未验证,BBS#45) + - FileList只读,最终仍需CDP setFileInputFiles - ⚠tabId:CDP默认sender.tab.id(当前注入页),跨tab需显式tabId或先batch内tabs查 - CDP可用任意方法(Input/Network/DOM/Page/Runtime/Emulation等),单条每次attach→send→detach - ⭐跨tab无需前台:指定tabId即可操作后台标签页 - ⭐绕过isTrusted:CDP派发的Input事件是浏览器原生级别 +## CDP点击完整生命周期(未验证,BBS#23) +- 通用点击需**三事件序列**:mouseMoved → mousePressed → mouseReleased(间隔50-100ms) + - 省略mouseMoved会导致MUI Tooltip/Ant Design Dropdown等hover依赖组件失效 + - ⚠autofill释放是特例,只需mousePressed即可(见下方autofill章节) +- 坐标修正(页面有transform:scale/zoom时): + ```js + var scale = window.visualViewport ? window.visualViewport.scale : 1; + var zoom = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; + var realX = x * zoom; var realY = y * zoom; + ``` +- iframe内元素CDP点击:坐标需合成 `finalX = iframeRect.x + elRect.x` + - 跨域iframe拿不到contentDocument:用CDP `Target.getTargets`找iframe targetId → `Target.attachToTarget`建独立会话 + +## CDP文本输入(未验证,BBS#23) +- `Input.insertText({text:'...'})` — 直接插入,快,不触发keydown/keyup +- `Input.dispatchKeyEvent` — 逐键派发,慢但完整模拟 +- React/Vue受控组件:先insertText,再JS手动dispatch `input`事件(input事件不检查isTrusted) +- 简单输入框用insertText够用 + +## CDP DOM域穿透 closed Shadow DOM(未验证,BBS#24/#25) +- `DOM.getDocument({depth:-1, pierce:true})` 穿透所有Shadow边界(含closed) +- `DOM.querySelector({nodeId, selector})` 定位 → `DOM.getBoxModel({nodeId})` 取坐标 +- getBoxModel返回content八值[x1,y1,...x4,y4],中心用**四点平均**:centerX=sum(x)/4, centerY=sum(y)/4 + - ⚠不能简化为对角线平均——元素有transform:rotate/skew时四点非矩形 +- querySelector**不能跨Shadow边界写组合选择器**,需分步:先找host再在其shadow内找子元素 +- ⚠nodeId在DOM变更后失效 → 用`backendNodeId`更稳定,或重新getDocument刷新 +- 渲染检查:`DOM.resolveNode` → `Runtime.callFunctionOn` 检查offsetHeight>0 +- 完整pipeline: getDocument(pierce) → querySelector → getBoxModel → 四点平均坐标 → Input三事件点击 + ## autofill获取 检测:web_scan输出input带`data-autofilled="true"`,value显示为受保护提示(非真实值,Chrome安全保护需点击释放) - ⭐首选CDP单次点击:JS取任一autofill输入框坐标→CDP `Input.dispatchMouseEvent` mousePressed一次即可释放→JS读`.value` diff --git a/reflect/scheduler.py b/reflect/scheduler.py index a485627..11d77f8 100644 --- a/reflect/scheduler.py +++ b/reflect/scheduler.py @@ -1,6 +1,13 @@ -import os, json +import os, json, socket as _socket from datetime import datetime, timedelta +# 端口锁:防止重复启动,bind失败时agentmain会直接崩溃退出 +# reload时mod.__dict__保留_lock,跳过重复绑定 +try: _lock +except NameError: + _lock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) + _lock.bind(('127.0.0.1', 45762)); _lock.listen(1) + INTERVAL = 60 ONCE = False