diff --git a/agentmain.py b/agentmain.py index 78b1fe7..b78ba60 100644 --- a/agentmain.py +++ b/agentmain.py @@ -102,7 +102,12 @@ class GeneraticAgent: handler.key_info += '\n[SYSTEM] 若开始新任务,先更新或清除工作记忆\n' self.handler = handler self.llmclient.backend = self.llmclient.backends[self.llm_no] - gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, + # 如果有历史记录且来自飞书,注入到首轮 user_input 中(支持/restore恢复上下文) + user_input = raw_query + if source == 'feishu' and len(self.history) > 1: # 飞书场景且有之前的对话记录 + h_str = "\n".join(self.history[-20:]) + user_input = f"### [WORKING MEMORY]\n\n{h_str}\n\n\n### 用户当前消息\n{raw_query}" + gen = agent_runner_loop(self.llmclient, sys_prompt, user_input, handler, TOOLS_SCHEMA, max_turns=40, verbose=self.verbose) try: full_resp = ""; last_pos = 0 diff --git a/assets/SETUP_FEISHU.md b/assets/SETUP_FEISHU.md new file mode 100644 index 0000000..3d132aa --- /dev/null +++ b/assets/SETUP_FEISHU.md @@ -0,0 +1,301 @@ +# 飞书 Agent 配置指南 + +> 让你的个人电脑变成飞书机器人的大脑,随时随地通过飞书对话控制你的电脑。 + +--- + +## 📋 目录 + +1. [前置条件](#前置条件) +2. [方案选择](#方案选择) +3. [企业用户配置](#企业用户配置) +4. [个人用户配置](#个人用户配置) +5. [项目配置](#项目配置) +6. [运行与测试](#运行与测试) +7. [常见问题](#常见问题) + +--- + +## 前置条件 + +### 必需环境 + +- Python 3.8+ +- 本项目完整代码 +- LLM API 密钥(Claude/OpenAI 等,已在 `sidercall/mykeys` 中配置) + +### 安装依赖 + +```bash +pip install lark-oapi +``` + +--- + +## 方案选择 + +| 你的情况 | 推荐方案 | 预计耗时 | +| ------------------ | -------------------------- | --------- | +| 公司已有飞书企业版 | [企业用户配置](#企业用户配置) | 5-10分钟 | +| 个人用户/学习测试 | [个人用户配置](#个人用户配置) | 10-15分钟 | + +--- + +## 企业用户配置 + +> 适用于:你的公司使用飞书,你有权限创建应用或联系管理员审批 + +### 步骤 1:创建应用 + +1. 访问 [飞书开放平台](https://open.feishu.cn/) +2. 登录你的企业飞书账号 +3. 点击右上角「创建应用」→「企业自建应用」 +4. 填写应用信息: + - 应用名称:`我的Agent助手`(可自定义) + - 应用描述:`个人AI助手` + - 应用图标:可选 + +### 步骤 2:添加机器人能力 + +1. 进入应用详情页 +2. 左侧菜单选择「添加应用能力」 +3. 找到「机器人」,点击「添加」 +4. 配置机器人信息(可保持默认) + +### 步骤 3:配置权限 + +1. 左侧菜单「权限管理」→「API 权限」 +2. 搜索并开通以下权限: + - `im:message` - 获取与发送单聊、群组消息 + - `im:message:send_as_bot` - 以应用身份发送消息 + - `contact:user.id:readonly` - 获取用户 ID + +### 步骤 4:获取凭证 + +1. 左侧菜单「凭证与基础信息」 +2. 记录以下信息: + - **App ID**:`cli_xxxxxxxx` + - **App Secret**:`xxxxxxxxxxxxxxxx` + +### 步骤 5:发布应用 + +1. 左侧菜单「版本管理与发布」 +2. 点击「创建版本」 +3. 填写版本信息,提交审核 +4. **联系企业管理员审批**(或自己是管理员直接审批) + +### 步骤 6:获取你的 Open ID + +1. 应用审批通过后,在飞书中搜索你的机器人 +2. 给机器人发送任意消息 +3. 运行以下代码获取你的 Open ID: + +```python +# 临时运行一次,获取 open_id +import lark_oapi as lark +from lark_oapi.api.im.v1 import * + +client = lark.Client.builder().app_id("你的APP_ID").app_secret("你的APP_SECRET").build() + +# 监听消息,打印发送者的 open_id +def handle(data): + print(f"你的 Open ID: {data.event.sender.sender_id.open_id}") + +# ... 或者查看 fsapp.py 运行时的日志输出 +``` + +--- + +## 个人用户配置 + +> 适用于:没有企业飞书账号,想个人测试使用 + +### 步骤 1:创建测试企业 + +1. 访问 [飞书开放平台](https://open.feishu.cn/) +2. 使用个人手机号注册/登录 +3. 点击右上角头像 →「创建测试企业」 +4. 填写企业名称(如:`我的测试工作区`) +5. 创建完成后,你就是这个测试企业的**管理员** + +### 步骤 2:创建应用 + +> 与企业用户步骤相同 + +1. 点击「创建应用」→「企业自建应用」 +2. 填写应用信息 + +### 步骤 3:添加机器人能力 + +1. 进入应用详情页 +2. 「添加应用能力」→「机器人」→「添加」 + +### 步骤 4:配置权限 + +1. 「权限管理」→「API 权限」 +2. 开通权限: + - `im:message` + - `im:message:send_as_bot` + - `contact:user.id:readonly` + +### 步骤 5:获取凭证 + +1. 「凭证与基础信息」 +2. 复制 **App ID** 和 **App Secret** + +### 步骤 6:发布应用(测试企业可自审批) + +1. 「版本管理与发布」→「创建版本」 +2. 提交后,进入 [飞书管理后台](https://feishu.cn/admin) +3. 「工作台」→「应用审核」→ 通过你的应用 + +### 步骤 7:在飞书客户端使用 + +1. 下载 [飞书客户端](https://www.feishu.cn/download) +2. 登录你的测试企业账号 +3. 搜索你创建的机器人名称 +4. 开始对话! + +--- + +## 项目配置 + +### 配置飞书凭证 + +编辑项目根目录的 `mykey.py`,添加: + +```python +# 飞书应用凭证 +fs_app_id = "cli_xxxxxxxxxxxxxxxx" # 替换为你的 App ID +fs_app_secret = "xxxxxxxxxxxxxxxx" # 替换为你的 App Secret + +# 允许使用的用户 Open ID 列表(可选,留空则允许所有人) +fs_allowed_users = [ + "ou_xxxxxxxxxxxxxxxxxxxxxxxx", # 你的 Open ID +] +``` + +### 确认 LLM 配置 + +确保 `sidercall/mykeys` 中已配置 LLM API 密钥: + +```python +# 示例:Claude API +claude_config = { + 'apikey': 'sk-ant-xxxxx', + 'apibase': 'https://api.anthropic.com', + 'model': 'claude-sonnet-4-20250514' +} +``` + +--- + +## 运行与测试 + +### 启动服务 + +```bash +cd /path/to/pc-agent-loop +python fsapp.py +``` + +### 预期输出 + +``` +================================================== +飞书 Agent 已启动(长连接模式) +App ID: cli_xxxxxxxxxxxxxxxx +等待消息... +================================================== +``` + +### 测试对话 + +1. 打开飞书客户端 +2. 找到你的机器人 +3. 发送:`你好` +4. 等待回复(首次可能需要几秒) + +--- + +## 可用命令 + +在与机器人对话时,可以使用以下特殊命令: + +| 命令 | 说明 | +| ---- | ---- | +| `/new` | 开始新对话,清除当前上下文 | +| `/stop` | 中止当前正在执行的任务 | +| `/restore <关键词>` | 恢复之前的对话上下文(根据关键词搜索历史记录) | + +### 命令示例 + +``` +/new # 清空对话,重新开始 +/stop # 停止正在运行的任务 +/restore 昨天的任务 # 恢复包含"昨天的任务"关键词的历史对话 +``` + +### 消息显示说明 + +- ⏳ 表示任务正在执行中 +- 消息会实时更新,无需等待完成 +- 超长回复会自动分段发送 + +--- + +## 常见问题 + +### Q: 提示「应用未发布」或「无权限」 + +**A:** 确保应用已发布且管理员已审批。测试企业用户需要在管理后台手动审批。 + +### Q: 发送消息后没有回复 + +**A:** 检查: + +1. `fsapp.py` 是否在运行 +2. 终端是否有错误日志 +3. LLM API 密钥是否配置正确 + +### Q: 提示「invalid app_id」 + +**A:** 检查 `mykey.py` 中的 `fs_app_id` 是否正确复制(包含 `cli_` 前缀) + +### Q: 如何获取自己的 Open ID? + +**A:** 运行 `fsapp.py` 后给机器人发消息,查看终端日志中的 `open_id` + +### Q: 能否多人同时使用? + +**A:** 不能。一个应用只能有一个长连接,连接到一台电脑。每个人需要创建自己的应用。 + +--- + +## 架构说明 + +``` +你的飞书 ←→ 飞书云 ←→ 长连接 ←→ fsapp.py ←→ Agent ←→ 你的电脑 + ↑ + 运行在你电脑上 +``` + +- 消息通过飞书云转发到你电脑上运行的 `fsapp.py` +- Agent 处理请求后,通过飞书 API 回复消息 +- **你的电脑必须保持运行** `fsapp.py` 才能响应消息 + +--- + +## 下一步 + +- 自定义 Agent 行为:编辑 `assets/sys_prompt.txt` +- 添加新工具:编辑 `assets/tools_schema.json` +- 查看日志:运行时观察终端输出 + +--- + +*文档版本:v1.1 | 更新日期:2026-03-07* + +**v1.1 更新内容:** +- 新增「可用命令」章节(/new, /stop, /restore) +- 新增消息显示说明(⏳ 进行中标记、实时更新等) diff --git a/assets/install_python_windows.bat b/assets/install_python_windows.bat new file mode 100644 index 0000000..b287370 --- /dev/null +++ b/assets/install_python_windows.bat @@ -0,0 +1,111 @@ +@echo off +setlocal enabledelayedexpansion +title Python One-Click Installer +color 0A + +echo. +echo ======================================== +echo Python One-Click Installer (Windows) +echo ======================================== +echo. + +net session >nul 2>&1 +if %errorlevel% neq 0 ( + echo [!] Administrator privileges required. Restarting with elevation... + powershell -Command "Start-Process '%~f0' -Verb RunAs" + exit /b +) + +echo [OK] Administrator privileges confirmed +echo. + +python --version >nul 2>&1 +if %errorlevel% equ 0 ( + echo [OK] Python already installed: + python --version + echo. + choice /C YN /M "Install latest version anyway? (Y=Yes / N=Exit)" + if errorlevel 2 goto :end +) + +set PYTHON_VERSION=3.12.9 +set MIRROR_URL=https://npmmirror.com/mirrors/python/3.12.9/python-3.12.9-amd64.exe +set OFFICIAL_URL=https://www.python.org/ftp/python/3.12.9/python-3.12.9-amd64.exe +set INSTALLER=%TEMP%\python_installer.exe + +echo [*] Preparing to download Python %PYTHON_VERSION% +echo [*] Trying mirror source first... +echo. + +powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '%MIRROR_URL%' -OutFile '%INSTALLER%' -UseBasicParsing" + +if not exist "%INSTALLER%" goto :official +for %%A in ("%INSTALLER%") do if %%~zA lss 1000000 goto :official +echo [OK] Mirror download complete +goto :install + +:official +echo [!] Mirror failed, switching to official source... +powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '%OFFICIAL_URL%' -OutFile '%INSTALLER%' -UseBasicParsing" + +if not exist "%INSTALLER%" ( + echo [x] Download failed. Please check your network connection and retry. + pause + goto :end +) +for %%A in ("%INSTALLER%") do if %%~zA lss 1000000 ( + echo [x] Downloaded file is incomplete. Please check your network and retry. + pause + goto :end +) +echo [OK] Official source download complete + +:install +echo. +echo [*] Installing Python %PYTHON_VERSION% (this may take 2-5 minutes^)... +echo. + +start /wait "" "%INSTALLER%" /passive InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 + +set INSTALL_CODE=%errorlevel% +del /f /q "%INSTALLER%" >nul 2>&1 + +if %INSTALL_CODE% neq 0 ( + echo [x] Installation failed with error code: %INSTALL_CODE% + pause + goto :end +) + +echo [+] Installation complete! +echo. + +timeout /t 3 /nobreak >nul + +set "PATH=C:\Program Files\Python312;C:\Program Files\Python312\Scripts;%PATH%" + +python --version >nul 2>&1 +if %errorlevel% equ 0 ( + echo [OK] Python installed successfully: + python --version + echo. + echo [OK] pip version: + pip --version + echo. + echo [*] Configuring pip mirror (Tsinghua^)... + pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn + echo. + echo [*] Installing requests... + pip install requests + echo. + echo ======================================== + echo All done! Open a new terminal to use + echo python and pip commands. + echo ======================================== +) else ( + echo [!] PATH not yet refreshed. Please close this window and open a new terminal. +) + +:end +echo. +pause diff --git a/fsapp.py b/fsapp.py new file mode 100644 index 0000000..cdaffd1 --- /dev/null +++ b/fsapp.py @@ -0,0 +1,103 @@ +import os, sys, threading, asyncio, time, re, json +import queue as Q +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, PROJECT_ROOT); os.chdir(PROJECT_ROOT) +import lark_oapi as lark +from lark_oapi.api.im.v1 import * +from lark_oapi.api.contact.v3 import * +from agentmain import GeneraticAgent +import mykey +_TAG_PATS = [r'<' + t + r'>.*?' for t in ('thinking', 'summary', 'tool_use', 'file_content')] +def _clean(t): + for p in _TAG_PATS: t = re.sub(p, '', t, flags=re.DOTALL) + return re.sub(r'\n{3,}', '\n\n', t).strip() or '...' +APP_ID, APP_SECRET = getattr(mykey, 'fs_app_id', None), getattr(mykey, 'fs_app_secret', None) +ALLOWED_USERS = set(getattr(mykey, 'fs_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() +_card = lambda t: json.dumps({"config": {"wide_screen_mode": True}, "elements": [{"tag": "markdown", "content": t}]}) +def send_message(open_id, content, msg_type="text", use_card=False): + ct, mt = (_card(content), "interactive") if use_card else (json.dumps({"text": content}), "text") + body = CreateMessageRequest.builder().receive_id_type("open_id").request_body( + CreateMessageRequestBody.builder().receive_id(open_id).msg_type(mt).content(ct).build()).build() + r = client.im.v1.message.create(body) + return r.data.message_id if r.success() else (print(f"发送失败: {r.code}, {r.msg}"), None)[1] +def update_message(message_id, content): + body = PatchMessageRequest.builder().message_id(message_id).request_body( + PatchMessageRequestBody.builder().content(_card(content)).build()).build() + r = client.im.v1.message.patch(body) + if not r.success(): print(f"[ERROR] update_message 失败: {r.code}, {r.msg}") + return r.success() +def handle_message(data): + event, message, sender = data.event, data.event.message, data.event.sender + open_id, msg_type = sender.sender_id.open_id, message.message_type + if ALLOWED_USERS and open_id not in ALLOWED_USERS: return print(f"未授权用户: {open_id}") + if msg_type != "text": return send_message(open_id, "⚠️ 目前只支持文本消息") + text = json.loads(message.content).get("text", "").strip() + if not text: return + print(f"收到消息 [{open_id}]: {text}") + if text.startswith("/"): return handle_command(open_id, text) + 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(text, source='feishu'), "" + while user_tasks.get(open_id, {}).get('running', False): + time.sleep(3) + item = None + try: + while True: item = dq.get_nowait() + except: pass + if item is None: continue + raw, done = item.get("done") or item.get("next", ""), "done" in item + show = _clean(raw) + if len(show) > 3500: + # 智能截断:避免切断代码块 + cut = show[-3000:] + if cut.count('```') % 2 == 1: cut = '```\n' + cut # 补开头 + 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: break + if not user_tasks.get(open_id, {}).get('running', True): 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)}") + finally: user_tasks.pop(open_id, None) + threading.Thread(target=run_agent, daemon=True).start() +def handle_command(open_id, cmd): + import glob + if cmd == "/stop": + if open_id in user_tasks: user_tasks[open_id]['running'] = False + agent.abort(); send_message(open_id, "⏹️ 正在停止...") + elif cmd == "/help": + send_message(open_id, "📖 命令列表:\n/stop - 停止当前任务\n/status - 查看状态\n/restore - 恢复上次对话历史\n/new - 开启新对话\n/help - 显示帮助") + elif cmd == "/status": + send_message(open_id, f"状态: {'🟢 空闲' if not agent.is_busy() else '🔴 运行中'}") + elif cmd == "/restore": + try: + files = glob.glob('./temp/model_responses_*.txt') + if not files: return send_message(open_id, "❌ 没有找到历史记录") + 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() + send_message(open_id, f"✅ 已恢复 {count} 轮对话\n来源: {os.path.basename(latest)}\n(仅恢复上下文,请输入新问题继续)") + except Exception as e: send_message(open_id, f"❌ 恢复失败: {e}") + else: send_message(open_id, f"❓ 未知命令: {cmd}") +def main(): + global client + if not APP_ID or not APP_SECRET: print("错误: 请在 mykey.py 中配置 fs_app_id 和 fs_app_secret"); sys.exit(1) + client = create_client() + handler = lark.EventDispatcherHandler.builder("", "").register_p2_im_message_receive_v1(handle_message).build() + cli = lark.ws.Client(APP_ID, APP_SECRET, event_handler=handler, log_level=lark.LogLevel.INFO) + print("=" * 50 + "\n飞书 Agent 已启动(长连接模式)\n" + f"App ID: {APP_ID}\n等待消息...\n" + "=" * 50) + cli.start() +if __name__ == "__main__": main() \ No newline at end of file