diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000..1bc428a --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,409 @@ +# GenericAgent 新手上手指南 + +## 🎯 什么是 GenericAgent? + +GenericAgent 是一个极简、可自我进化的自主 Agent 框架。它的核心只有 **~3,300 行代码**,通过 **7 个原子工具 + 92 行 Agent Loop**,就能赋予任意 LLM 对你本地计算机的系统级控制能力。 + +**它能做什么?** +- 控制浏览器(保留登录态) +- 执行终端命令 +- 读写文件系统 +- 模拟键盘鼠标 +- 控制移动设备(ADB) +- 屏幕视觉识别 + +**最特别的是什么?** + +GenericAgent 不预设技能,而是**靠进化获得能力**。每解决一个新任务,它就会自动将执行路径固化为 Skill,供后续直接调用。使用时间越长,沉淀的技能越多,最终形成一棵完全属于你的专属技能树。 + +> 🤖 **自举实证**: 本项目的所有 Git 操作,从 `git init` 到每一条 commit message,都是 GenericAgent 自主完成的。作者全程未打开过一次终端。 + +--- + +## 🚀 5 分钟快速开始 + +### 第一步:安装部署 + +**方法一:标准安装(推荐开发者)** + +```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 +``` + +**方法二:Windows 便携版(推荐新手)** + +1. [下载便携版](http://kw.fudan.edu.cn/resources/PC-Agent-Portable.zip) (19MB) +2. 解压到任意目录 +3. 双击运行即可 + +**方法三:Android(Termux)** + +```bash +cd /sdcard/ga +python agentmain.py +``` + +### 第二步:配置 API Key + +编辑 `mykey.py` 文件,填入你的 LLM API Key: + +```python +# 示例:使用 Claude +api_key = "sk-ant-xxx" # 你的 Anthropic API Key +model = "claude-3-5-sonnet-20241022" + +# 或使用其他模型 +# api_key = "your_openai_key" +# model = "gpt-4" +``` + +支持的模型: +- Claude (Anthropic) +- GPT-4 / GPT-3.5 (OpenAI) +- Gemini (Google) +- Kimi (Moonshot) +- 其他兼容 OpenAI API 的模型 + +### 第三步:启动界面 + +```bash +python launch.pyw +``` + +启动后会出现一个桌面悬浮窗,你可以直接在里面输入任务指令。 + +--- + +## 💡 基础使用 + +### 你的第一个任务 + +启动后,试试这些简单的任务: + +**文件操作:** +``` +"帮我在桌面创建一个 hello.txt 文件,内容是 Hello World" +``` + +**网页浏览:** +``` +"打开百度,搜索今天的天气" +``` + +**代码执行:** +``` +"用 Python 计算 1 到 100 的和" +``` + +### 理解 Agent 的工作方式 + +GenericAgent 的工作流程: + +``` +你的指令 → Agent 理解任务 → 调用工具执行 → 返回结果 → 沉淀经验 +``` + +**核心循环(92 行代码):** +1. **感知**: 读取当前环境状态 +2. **推理**: 分析任务,制定执行计划 +3. **执行**: 调用工具完成操作 +4. **记忆**: 将成功的执行路径写入记忆层 +5. **循环**: 继续下一步,直到任务完成 + +--- + +## 🧬 进阶:Skill 系统 + +### 什么是 Skill? + +Skill 是 GenericAgent 自动沉淀的任务执行模板。当你第一次完成某个任务时,Agent 会将整个执行流程固化为一个 Skill,下次遇到类似任务就能直接调用。 + +**举个例子:** + +| 你说的话 | 第一次 Agent 做了什么 | 之后每次 | +|---------|---------------------|---------| +| "监控股票并提醒我" | 安装 mootdx → 构建选股流程 → 配置定时任务 → 保存 Skill | **一句话启动** | +| "用 Gmail 发这个文件" | 配置 OAuth → 编写发送脚本 → 保存 Skill | **直接可用** | + +### 如何使用 Skill? + +Skill 会自动保存在 `memory/` 目录下。你不需要手动管理,Agent 会在需要时自动调用。 + +**查看已有的 Skill:** +```bash +ls memory/*.md +``` + +**手动触发 Skill:** +``` +"使用之前保存的股票监控 Skill" +``` + +### 培养你的专属 Agent + +使用时间越长,你的 Agent 积累的 Skill 越多: + +- **第 1 周**: 基础文件操作、简单网页浏览 +- **第 1 个月**: 自动化工作流、数据处理脚本 +- **第 3 个月**: 复杂的多步骤任务、跨平台操作 +- **第 6 个月**: 一套任何人都没有的专属技能树 + +--- + +## 🎮 实用玩法 + +### 浏览器自动化 + +GenericAgent 注入真实浏览器,保留你的登录态: + +``` +"打开淘宝,搜索 iPhone 15,按价格排序" +"登录我的 Gmail,查看未读邮件" +"在美团上帮我点一杯奶茶" +``` + +**可用工具:** +- `web_scan`: 读取网页内容 +- `web_execute_js`: 执行 JavaScript 控制页面 + +### 文件和代码处理 + +``` +"分析这个 Python 项目的代码结构" +"把这个 CSV 文件转换成 Excel" +"批量重命名这个文件夹里的图片" +``` + +**可用工具:** +- `file_read`: 读取文件 +- `file_write`: 写入文件 +- `file_patch`: 精确修改文件 +- `code_run`: 执行代码 + +### 移动设备控制(ADB) + +通过 ADB 控制 Android 设备: + +``` +"打开支付宝,查看我的账单" +"在微信上给张三发消息" +"截取手机屏幕并保存" +``` + +### 定时任务 + +``` +"每天早上 8 点提醒我查看邮件" +"每小时检查一次股票价格" +"每周一生成上周的工作总结" +``` + +--- + +## 🤖 接入聊天平台(可选) + +### 为什么要接入 Bot? + +将 GenericAgent 接入聊天平台后,你可以: +- 随时随地通过手机控制你的电脑 +- 多人协作使用同一个 Agent +- 接收 Agent 的主动通知和提醒 + +### 支持的平台 + +- **QQ Bot**: WebSocket 长连接,无需公网 webhook +- **飞书(Lark)**: 支持富文本、图片、文件、音频 +- **企业微信(WeCom)**: 企业内部使用 +- **钉钉(DingTalk)**: 企业协作 +- **Telegram**: 国际用户 + +### 快速配置指南 + +**以 QQ Bot 为例:** + +1. 在 [QQ 开放平台](https://q.qq.com) 创建机器人 +2. 获取 AppID 和 AppSecret +3. 在 `mykey.py` 中配置: + +```python +qq_app_id = "YOUR_APP_ID" +qq_app_secret = "YOUR_APP_SECRET" +qq_allowed_users = ["YOUR_USER_OPENID"] # 或 ['*'] 公开访问 +``` + +4. 启动 Bot: + +```bash +pip install qq-botpy +python qqapp.py +# 或与桌面窗口一起启动 +python launch.pyw --qq +``` + +**其他平台配置详见:** +- 飞书: [assets/SETUP_FEISHU.md](assets/SETUP_FEISHU.md) +- 企业微信: `python launch.pyw --wecom` +- 钉钉: `python launch.pyw --dingtalk` +- Telegram: `python 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 diff --git a/fsapp.py b/fsapp.py index ec53b99..062f915 100644 --- a/fsapp.py +++ b/fsapp.py @@ -1,4 +1,11 @@ -import json, os, re, sys, threading, time +import glob +import json +import os +import queue as Q +import re +import sys +import threading +import time PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, PROJECT_ROOT) @@ -8,9 +15,9 @@ import lark_oapi as lark from lark_oapi.api.im.v1 import * from agentmain import GeneraticAgent -from chatapp_common import clean_reply, extract_files, format_restore, public_access, strip_files, to_allowed_set from llmcore import mykeys +_TAG_PATS = [r"<" + t + r">.*?" for t in ("thinking", "summary", "tool_use", "file_content")] _IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".ico", ".tiff", ".tif"} _AUDIO_EXTS = {".opus", ".mp3", ".wav", ".m4a", ".aac"} _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm"} @@ -32,13 +39,37 @@ MEDIA_DIR = os.path.join(TEMP_DIR, "feishu_media") os.makedirs(MEDIA_DIR, exist_ok=True) +def _clean(text): + for pat in _TAG_PATS: + text = re.sub(pat, "", text, flags=re.DOTALL) + return re.sub(r"\n{3,}", "\n\n", text).strip() or "..." + + +def _extract_files(text): + return re.findall(r"\[FILE:([^\]]+)\]", text or "") + + +def _strip_files(text): + return re.sub(r"\[FILE:[^\]]+\]", "", text or "").strip() + + def _display_text(text): - return strip_files(clean_reply(text)) or "..." + return _strip_files(_clean(text)) or "..." + + +def _to_allowed_set(value): + if value is None: + return set() + if isinstance(value, str): + value = [value] + return {str(x).strip() for x in value if str(x).strip()} def _parse_json(raw): + if not raw: + return {} try: - return json.loads(raw) if raw else {} + return json.loads(raw) except Exception: return {} @@ -198,27 +229,31 @@ def _extract_post_content(content_json): APP_ID = str(mykeys.get("fs_app_id", "") or "").strip() APP_SECRET = str(mykeys.get("fs_app_secret", "") or "").strip() -ALLOWED_USERS = to_allowed_set(mykeys.get("fs_allowed_users", [])) -PUBLIC_ACCESS = public_access(ALLOWED_USERS) +ALLOWED_USERS = _to_allowed_set(mykeys.get("fs_allowed_users", [])) +PUBLIC_ACCESS = not ALLOWED_USERS or "*" in ALLOWED_USERS agent = GeneraticAgent() threading.Thread(target=agent.run, daemon=True).start() client, user_tasks = None, {} -def create_client(): return lark.Client.builder().app_id(APP_ID).app_secret(APP_SECRET).log_level(lark.LogLevel.INFO).build() -def _card(text): return json.dumps({"config": {"wide_screen_mode": True}, "elements": [{"tag": "markdown", "content": text}]}, ensure_ascii=False) +def create_client(): + return lark.Client.builder().app_id(APP_ID).app_secret(APP_SECRET).log_level(lark.LogLevel.INFO).build() -def send_message(open_id, content, msg_type="text", use_card=False): +def _card(text): + return json.dumps({"config": {"wide_screen_mode": True}, "elements": [{"tag": "markdown", "content": text}]}, ensure_ascii=False) + + +def send_message(receive_id, content, msg_type="text", use_card=False, receive_id_type="open_id"): if use_card: payload, real_type = _card(content), "interactive" elif msg_type == "text": payload, real_type = json.dumps({"text": content}, ensure_ascii=False), "text" else: payload, real_type = content, msg_type - body = CreateMessageRequest.builder().receive_id_type("open_id").request_body( - CreateMessageRequestBody.builder().receive_id(open_id).msg_type(real_type).content(payload).build() + body = CreateMessageRequest.builder().receive_id_type(receive_id_type).request_body( + CreateMessageRequestBody.builder().receive_id(receive_id).msg_type(real_type).content(payload).build() ).build() response = client.im.v1.message.create(body) if response.success(): @@ -237,10 +272,6 @@ def update_message(message_id, content): return response.success() -def _read_file_obj(response): - return response.file.read() if hasattr(response.file, "read") else response.file - - def _upload_image_sync(file_path): try: with open(file_path, "rb") as f: @@ -274,29 +305,51 @@ def _upload_file_sync(file_path): return None -def _download_resource(message_id, file_key, resource_type, label): +def _download_image_sync(message_id, image_key): + try: + request = GetMessageResourceRequest.builder().message_id(message_id).file_key(image_key).type("image").build() + response = client.im.v1.message_resource.get(request) + if response.success(): + data = response.file.read() if hasattr(response.file, "read") else response.file + return data, response.file_name + print(f"[ERROR] download image failed: {response.code}, {response.msg}") + except Exception as e: + print(f"[ERROR] download image failed {image_key}: {e}") + return None, None + + +def _download_file_sync(message_id, file_key, resource_type="file"): + if resource_type == "audio": + resource_type = "file" try: request = GetMessageResourceRequest.builder().message_id(message_id).file_key(file_key).type(resource_type).build() response = client.im.v1.message_resource.get(request) if response.success(): - return _read_file_obj(response), response.file_name - print(f"[ERROR] download {label} failed: {response.code}, {response.msg}") + data = response.file.read() if hasattr(response.file, "read") else response.file + return data, response.file_name + print(f"[ERROR] download {resource_type} failed: {response.code}, {response.msg}") except Exception as e: - print(f"[ERROR] download {label} failed {file_key}: {e}") + print(f"[ERROR] download {resource_type} failed {file_key}: {e}") return None, None def _download_and_save_media(msg_type, content_json, message_id): - file_key = content_json.get("image_key") if msg_type == "image" else content_json.get("file_key") - resource_type = "image" if msg_type == "image" else ("file" if msg_type == "audio" else msg_type) - default_ext = ".jpg" if msg_type == "image" else ".opus" if msg_type == "audio" else "" - if not (file_key and message_id): - return None, None - data, filename = _download_resource(message_id, file_key, resource_type, msg_type) - if data: - filename = filename or f"{file_key[:16]}{default_ext}" - if msg_type == "audio" and filename and not filename.endswith(".opus"): - filename += ".opus" + data, filename = None, None + if msg_type == "image": + image_key = content_json.get("image_key") + if image_key and message_id: + data, filename = _download_image_sync(message_id, image_key) + if not filename: + filename = f"{image_key[:16]}.jpg" + elif msg_type in ("audio", "file", "media"): + file_key = content_json.get("file_key") + if file_key and message_id: + data, filename = _download_file_sync(message_id, file_key, msg_type) + if not filename: + filename = file_key[:16] + if msg_type == "audio" and filename and not filename.endswith(".opus"): + filename = f"{filename}.opus" + if data and filename: file_path = os.path.join(MEDIA_DIR, os.path.basename(filename)) with open(file_path, "wb") as f: f.write(data) @@ -305,36 +358,38 @@ def _download_and_save_media(msg_type, content_json, message_id): def _describe_media(msg_type, file_path, filename): - return f"[{msg_type}: {filename}]\n[{'Image' if msg_type == 'image' else 'File'}: source: {file_path}]" + if msg_type == "image": + return f"[image: {filename}]\n[Image: source: {file_path}]" + if msg_type == "audio": + return f"[audio: {filename}]\n[File: source: {file_path}]" + if msg_type in ("file", "media"): + return f"[{msg_type}: {filename}]\n[File: source: {file_path}]" + return f"[{msg_type}]\n[File: source: {file_path}]" -def _send_local_file(open_id, file_path): +def _send_local_file(receive_id, file_path, receive_id_type="open_id"): if not os.path.isfile(file_path): - send_message(open_id, f"⚠️ 文件不存在: {file_path}") + send_message(receive_id, f"⚠️ 文件不存在: {file_path}", receive_id_type=receive_id_type) return False ext = os.path.splitext(file_path)[1].lower() - is_image = ext in _IMAGE_EXTS - file_key = _upload_image_sync(file_path) if is_image else _upload_file_sync(file_path) - if file_key: - key_name, msg_type = ("image_key", "image") if is_image else ("file_key", "media" if ext in _AUDIO_EXTS or ext in _VIDEO_EXTS else "file") - send_message(open_id, json.dumps({key_name: file_key}, ensure_ascii=False), msg_type=msg_type) - return True - send_message(open_id, f"⚠️ 文件发送失败: {os.path.basename(file_path)}") + if ext in _IMAGE_EXTS: + image_key = _upload_image_sync(file_path) + if image_key: + send_message(receive_id, json.dumps({"image_key": image_key}, ensure_ascii=False), msg_type="image", receive_id_type=receive_id_type) + return True + else: + file_key = _upload_file_sync(file_path) + if file_key: + msg_type = "media" if ext in _AUDIO_EXTS or ext in _VIDEO_EXTS else "file" + send_message(receive_id, json.dumps({"file_key": file_key}, ensure_ascii=False), msg_type=msg_type, receive_id_type=receive_id_type) + return True + send_message(receive_id, f"⚠️ 文件发送失败: {os.path.basename(file_path)}", receive_id_type=receive_id_type) return False -def _send_generated_files(open_id, raw_text): - for file_path in extract_files(raw_text): - _send_local_file(open_id, file_path) - - -def _append_media(parts, image_paths, msg_type, file_path, filename): - if not (file_path and filename): - parts.append(f"[{msg_type}: download failed]") - return - parts.append(_describe_media(msg_type, file_path, filename)) - if msg_type == "image": - image_paths.append(file_path) +def _send_generated_files(receive_id, raw_text, receive_id_type="open_id"): + for file_path in _extract_files(raw_text): + _send_local_file(receive_id, file_path, receive_id_type) def _build_user_message(message): @@ -352,10 +407,19 @@ def _build_user_message(message): parts.append(text) for image_key in image_keys: file_path, filename = _download_and_save_media("image", {"image_key": image_key}, message_id) - _append_media(parts, image_paths, "image", file_path, filename) + if file_path and filename: + parts.append(_describe_media("image", file_path, filename)) + image_paths.append(file_path) + else: + parts.append("[image: download failed]") elif msg_type in ("image", "audio", "file", "media"): file_path, filename = _download_and_save_media(msg_type, content_json, message_id) - _append_media(parts, image_paths, msg_type, file_path, filename) + if file_path and filename: + parts.append(_describe_media(msg_type, file_path, filename)) + if msg_type == "image": + image_paths.append(file_path) + else: + parts.append(f"[{msg_type}: download failed]") elif msg_type in ("share_chat", "share_user", "interactive", "share_calendar_event", "system", "merge_forward"): parts.append(_extract_share_card_content(content_json, msg_type)) else: @@ -366,21 +430,28 @@ def _build_user_message(message): def handle_message(data): event, message, sender = data.event, data.event.message, data.event.sender open_id = sender.sender_id.open_id + chat_id = message.chat_id if not PUBLIC_ACCESS and open_id not in ALLOWED_USERS: print(f"未授权用户: {open_id}") return user_input, image_paths = _build_user_message(message) if not user_input: - send_message(open_id, f"⚠️ 暂不支持处理此类飞书消息:{message.message_type}") + if chat_id: + send_message(chat_id, f"⚠️ 暂不支持处理此类飞书消息:{message.message_type}", receive_id_type="chat_id") + else: + send_message(open_id, f"⚠️ 暂不支持处理此类飞书消息:{message.message_type}") return print(f"收到消息 [{open_id}] ({message.message_type}, {len(image_paths)} images): {user_input[:200]}") if message.message_type == "text" and user_input.startswith("/"): - return handle_command(open_id, user_input) + return handle_command(open_id, user_input, chat_id) def run_agent(): user_tasks[open_id] = {"running": True} try: - msg_id, dq, last_text = send_message(open_id, "思考中...", use_card=True), agent.put_task(user_input, source="feishu", images=image_paths), "" + if chat_id: + msg_id, dq, last_text = send_message(chat_id, "思考中...", use_card=True, receive_id_type="chat_id"), agent.put_task(user_input, source="feishu", images=image_paths), "" + else: + msg_id, dq, last_text = send_message(open_id, "思考中...", use_card=True), agent.put_task(user_input, source="feishu", images=image_paths), "" while user_tasks.get(open_id, {}).get("running", False): time.sleep(3) item = None @@ -398,55 +469,81 @@ def handle_message(data): cut = show[-3000:] if cut.count("```") % 2 == 1: cut = "```\n" + cut - msg_id, last_text, show = send_message(open_id, "(继续...)", use_card=True), "", cut + if chat_id: + msg_id, last_text, show = send_message(chat_id, "(继续...)", use_card=True, receive_id_type="chat_id"), "", cut + else: + msg_id, last_text, show = send_message(open_id, "(继续...)", use_card=True), "", cut display = show if done else show + " ⏳" if display != last_text and msg_id: update_message(msg_id, display) last_text = display if done: - _send_generated_files(open_id, raw) + if chat_id: + _send_generated_files(chat_id, raw, receive_id_type="chat_id") + else: + _send_generated_files(open_id, raw) break if not user_tasks.get(open_id, {}).get("running", True): - send_message(open_id, "⏹️ 已停止") + if chat_id: + send_message(chat_id, "已停止", receive_id_type="chat_id") + else: + send_message(open_id, "已停止") except Exception as e: import traceback print(f"[ERROR] run_agent 异常: {e}") traceback.print_exc() - send_message(open_id, f"❌ 错误: {str(e)}") + if chat_id: + send_message(chat_id, f"错误: {str(e)}", receive_id_type="chat_id") + else: + send_message(open_id, f"错误: {str(e)}") finally: user_tasks.pop(open_id, None) threading.Thread(target=run_agent, daemon=True).start() -def handle_command(open_id, cmd): +def handle_command(open_id, cmd, chat_id=None): + def _send_cmd_response(content): + if chat_id: + send_message(chat_id, content, receive_id_type="chat_id") + else: + send_message(open_id, content) if cmd == "/stop": if open_id in user_tasks: user_tasks[open_id]["running"] = False agent.abort() - send_message(open_id, "⏹️ 正在停止...") + _send_cmd_response("正在停止...") elif cmd == "/new": agent.abort() agent.history = [] - send_message(open_id, "🆕 已清空当前共享上下文") + _send_cmd_response("已清空当前共享上下文") elif cmd == "/help": - send_message(open_id, "📖 命令列表:\n/stop - 停止当前任务\n/status - 查看状态\n/restore - 恢复上次对话历史\n/new - 开启新对话\n/help - 显示帮助") + _send_cmd_response("命令列表:\n/stop - 停止当前任务\n/status - 查看状态\n/restore - 恢复上次对话历史\n/new - 开启新对话\n/help - 显示帮助") elif cmd == "/status": - send_message(open_id, f"状态: {'🟢 空闲' if not agent.is_running else '🔴 运行中'}") + _send_cmd_response(f"状态: {'空闲' if not agent.is_running else '运行中'}") elif cmd == "/restore": try: - restored_info, err = format_restore() - if err: - return send_message(open_id, err) - restored, fname, count = restored_info + files = glob.glob("./temp/model_responses_*.txt") + if not files: + return _send_cmd_response("没有找到历史记录") + latest = max(files, key=os.path.getmtime) + with open(latest, "r", encoding="utf-8") as f: + content = f.read() + users = re.findall(r"=== USER ===\n(.+?)(?==== |$)", content, re.DOTALL) + resps = re.findall(r"=== Response ===.*?\n(.+?)(?==== Prompt|$)", content, re.DOTALL) + count = 0 + for u, r in zip(users, resps): + u, r = u.strip(), r.strip()[:500] + if u and r: + agent.history.extend([f"[USER]: {u}", f"[Agent] {r}"]) + count += 1 agent.abort() - agent.history.extend(restored) - send_message(open_id, f"✅ 已恢复 {count} 轮对话\n来源: {fname}\n(仅恢复上下文,请输入新问题继续)") + _send_cmd_response(f"已恢复 {count} 轮对话\n来源: {os.path.basename(latest)}\n(仅恢复上下文,请输入新问题继续)") except Exception as e: - send_message(open_id, f"❌ 恢复失败: {e}") + _send_cmd_response(f"恢复失败: {e}") else: - send_message(open_id, f"❓ 未知命令: {cmd}") + _send_cmd_response(f"未知命令: {cmd}") def main():