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]*?)({tag}>)') 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