利用 Cursor ACP 模式搭建飞书助手机器人,无需任何大模型接口

飞书里 @ 一下就能让 Agent 查日程、读文档、跑脚本,但我不想在 Python 里再养一套 OpenAI / 智谱 / 通义的 SDK 和 API Key。

之前写过一篇 用 GolemBot 把 Codex 接到飞书机器人,思路是在 Node 生态里拼积木:GolemBot 接 IM,Codex CLI 做推理,lark-cli 读飞书资料。那套能跑,但我用下来有两个别扭的地方。一是主链路在 Node 上,和我平时写 Python 脚本、跑 conda 项目的习惯不太贴;二是 Codex 要单独配 API Key 或 OAuth,跟 Cursor 订阅是两套账。

后来我干脆把飞书助手换成了自己写的 lark-assistant-agent,核心改了一层:消息还是 lark-cli 收,智能回复交给 Cursor CLI 的 ACP 模式(agent acp)。Python 这边只做消息路由和 JSON-RPC,requirements.txt 里就 pyyamlpython-dotenv 两个包,一个大模型 SDK 都没有。

下面按这个项目的实际结构,把搭建过程记一遍。

先说清楚「无需大模型接口」是什么意思

这句话容易被理解成「完全不用大模型」。其实不是,准确点说是这样:

  • 项目代码不调用任何第三方大模型的 HTTP API,没有 OPENAI_API_KEY,也没有智谱、通义那类 SDK。
  • 智能回复走本机的 agent acp 子进程,模型和工具都由 Cursor CLI 托管,认证用 cursor_login(本机 Cursor 登录态),需要的话再用 CURSOR_API_KEY 覆盖。
  • 有些消息压根不经过模型:pingpong帮助 回固定文案、部署类触发词直接跑 bash 脚本。

所以「无需大模型接口」说的是你不用自己去接模型 API,不是把机器人退化成纯规则引擎。开放问答该花的模型能力还是要花,只是这笔账记在 Cursor 订阅里,不落到你的 Python 服务上。

和 GolemBot + Codex 那套比,差异大概是这样:

维度GolemBot + Codexlark-assistant-agent + Cursor ACP
主语言Node(GolemBot)Python
模型入口Codex CLI / API KeyCursor CLI agent acp
项目内 LLM SDK无(但 Codex 侧要配)
Agent 规则AGENTS.mdAGENTS.md
飞书读写lark-clilark-cli
无人值守看 Codex approval 配置auto_approve_permissions

整体架构

链路不复杂,Python 就是薄薄一层胶水:

flowchart TB FS[飞书服务器] -->|长连接| LC[lark-cli event consume] LC -->|NDJSON stdout| PY[agent.py] PY --> MF{消息过滤} MF -->|ping / 帮助| BI[builtin_replies 本地] MF -->|部署AI教育项目| WF[deploy_edu 工作流] MF -->|其他文本| RE[ReplyEngine] RE --> ACP[agent acp 子进程] ACP -->|JSON-RPC stdio| CA[Cursor Agent + AGENTS.md] BI --> LR[lark-cli im +messages-reply] WF --> LR RE --> LR LR --> FS

这张图有三个分叉:

  1. 本地规则:健康检查、帮助说明,毫秒级就回了。
  2. 确定性工作流:关键词命中后跑 scripts/deploy-edu.sh,不经过 ACP。
  3. Cursor ACP:剩下的自然语言问题交给本机 Agent,它能 Shell、读写文件、调 lark-cli。

前置条件

组件用途
condaPython 3.11 环境
npx + @larksuite/cli飞书事件消费、消息回复
Cursor CLIagent 命令,启动 ACP
AGENTS.mdAgent 工作区规则(人格、安全边界、lark-cli 约定)

飞书应用侧需要开通 im:messageim:message:send_as_bot 及事件订阅相关 scope,机器人要加入目标群聊。

lark-cli 初始化:

npx --yes @larksuite/cli config init

Cursor CLI 安装参考官方文档:https://cursor.com/docs/cli/overview。装好后确认:

agent --help

克隆与一键初始化

git clone https://github.com/liqinsi/lark-assistant-agent.git
cd lark-assistant-agent
chmod +x scripts/setup.sh
./scripts/setup.sh

setup.sh 会创建 conda 环境 lark-assistant-agent、检查 npx / agent / AGENTS.md、从 example 复制 config/agent.yaml,并跑一遍自检。

激活环境后启动:

conda activate lark-assistant-agent
python -m src.agent

自检命令:

python -m src.agent --check

单条消息联调(30 秒窗口内往群里发消息):

python -m src.agent --once
# 发「ping」→ pong
# 发其他问题 → Cursor Agent 回复

配置说明

config/agent.yaml 是主配置,从 config/agent.yaml.example 复制而来。关键字段:

identity: bot
event_key: im.message.receive_v1

# 群白名单,留空=全部
allowed_chat_ids: []

ignore_bot_messages: true
trigger_prefix: ""

builtin_replies:
  ping: pong
  帮助: |
    我是飞书小助手,基于 lark-cli + Cursor ACP 运行。
    ...

acp:
  enabled: true
  mode: agent          # agent | ask
  auto_approve_permissions: true
  permission_prefer_always: true

几个字段值得单独说一下:

  • allowed_chat_ids:只响应指定群。生产环境建议填上,别让随便哪个群都能遥控你的开发机。
  • trigger_prefix:设成 / 就只响应斜杠命令,能少一些误触发。
  • acp.modeagent 能 Shell、能读写文件;ask 只对话,适合只想问答、不想让 Agent 碰机器的情况。
  • auto_approve_permissions:飞书这边没人去点 Cursor 弹窗,只能自动批准工具权限。反过来说,这等于放开了 Agent 在本机执行命令,所以只在可信环境开。

可选环境变量见 .env.example

# LARK_CLI_NPX_PACKAGE=@larksuite/cli
# CURSOR_API_KEY=    # 一般不填,用本机 cursor_login
LARK_IDENTITY=bot

ACP 是怎么接进去的

src/acp_client.py 里自己写了个轻量 ACP 客户端,没绑定哪个云厂商。核心就几步:

  1. 子进程起 agent acp,工作目录指向项目根,这样 Agent 能读到 AGENTS.md
  2. 走 JSON-RPC 2.0 over stdio:initializeauthenticatecursor_login)→ session/new
  3. 用户消息用 session/prompt 发出去,流式收 session/update,拼成最终回复。
  4. 按飞书 chat_id 复用 ACP 会话,同一个群里的多轮对话就有上下文了。

每次 prompt 会带上飞书上下文,免得 Agent 不知道自己在哪:

你正在作为飞书小助手运行(见仓库 AGENTS.md)。
...
【飞书上下文】
- chat_id: oc_xxx
- chat_type: group
- sender_id: ou_xxx

【用户消息】
(用户原文)

无人值守的时候,Cursor 会弹权限、问问题、给 Plan,这些都由 Python 侧自动应答:

  • session/request_permission → 自动选 allow_alwaysallow_once
  • cursor/ask_question → 自动选第一个选项
  • cursor/create_planaccepted: true

回复引擎 src/reply.py 的分发逻辑很直白,先精确匹配 builtin_replies,匹配不上再走 ACP:

for key, reply in self.builtin_replies.items():
    if text == key or text.lower() == key.lower():
        return reply.strip()
# ...
return self._sessions().ask(text, chat_id=..., ...)

AGENTS.md 比 Python 那点代码更值得花时间

ACP 在 session/new 时用项目根当 cwdAGENTS.md 就是 Agent 的操作手册。项目里带了一份模板,按自己场景改就行,但至少这几件事要写清楚:

  • 角色:飞书 IM 助手,中文,回复尽量简洁。
  • 能做什么:用 Shell、用 lark-cli、读写项目文件。
  • 不能做什么:输出密钥,对 lark-cli 高风险写操作擅自加 --yes
  • 飞书上下文各字段的含义(chat_id / chat_type / sender_id)。
  • lark-cli 的调用约定:npx --yes @larksuite/cli ... --as bot

这跟 GolemBot 那篇的结论是一致的:胶水代码可以很少,但边界得写清楚。否则 Agent 会在飞书空间里乱试命令,或者权限不够时干脆瞎编内容。

飞书应用怎么配

和 GolemBot 方案差不多,飞书开放平台后台按这个顺序走不太容易漏东西:

flowchart TD A[创建企业自建应用] --> B[开启机器人能力] B --> C[复制 App ID / App Secret] C --> D[配置事件订阅] D --> E[订阅 im.message.receive_v1] E --> F[添加 im:message 等权限] F --> G[发布应用版本] G --> H[把机器人拉进群]

这个项目用 lark-cli 长连接消费事件,不是 HTTP 回调,所以不需要公网 IP。event_consumer.py 起的是这条命令:

npx --yes @larksuite/cli event consume im.message.receive_v1 --as bot

stderr 出现 [event] ready event_key=... 后,stdout 开始吐 NDJSON 事件行,agent.py 逐行处理。

回复走原消息线程:

npx --yes @larksuite/cli im +messages-reply --message-id <msg_id> --markdown "..."

内置工作流:不经 LLM 的部署

除了 ping / 帮助,项目还带了个确定性工作流的例子:群里 @ 小助手发「部署AI教育项目」,Python 侧先回一条确认,再跑 scripts/deploy-edu.shgit pull → 同步 → 健康检查),最后把 Markdown 报告贴回原线程。

这条链路根本不会启动 ACP,大概 1–3 分钟跑完,适合那种「运维动作别交给模型瞎猜」的活。Skill 文档在 .agents/skills/lark-deploy-edu/SKILL.md

消息分发在 src/agent.py 里优先级高于普通回复:

过滤 → 部署触发词?→ 跑脚本
     → 否则 → ReplyEngine(内置 / ACP)

生产环境:systemd 守护

开发机验证通过后,可以装用户级 systemd 服务:

./scripts/install-systemd.sh
systemctl --user status lark-assistant-agent
journalctl --user -u lark-assistant-agent -f

scripts/run-agent.sh 会补齐 nvm / ~/.local/binagent 命令)的 PATH。服务配置了 Restart=on-failure,进程挂了会自动拉起。

飞书里怎么测

从简单到复杂,一步步来:

ping

应立刻收到 pong,说明 lark-cli 收发链路是通的。

帮助

应收到固定说明,不经过 Cursor。

@小助手 用一句话介绍你能做什么

走 ACP,验证 Cursor CLI 和 AGENTS.md 是否生效。

@小助手 用 lark-cli 查一下我今天的日程标题

验证 Agent 能否调 lark-cli(需要 user 身份且已 lark-cli auth login)。

常见问题

agent 命令找不到

which agent
ls ~/.local/bin/agent

systemd 环境下 PATH 经常比交互式 shell 短,看 scripts/run-agent.sh 有没有把 ~/.local/bin 加进去。

收到消息但不回复

  1. ignore_bot_messages 是否误过滤;
  2. allowed_chat_ids 白名单是否包含当前群;
  3. trigger_prefix 是否要求消息以特定前缀开头;
  4. 飞书应用权限和版本发布是否完成。

ACP 报错或超时

先在本机单独测 Cursor CLI:

cd lark-assistant-agent
agent

再跑 python -m src.agent --check。如果 --check 里 ACP 项失败,先修 CLI 登录,别在飞书群里反复试。

机器人能聊天,但调不了 lark-cli

lark-cli 读个人日历、任务这类资源要用 user 身份,而且用户本人得先登录:

npx --yes @larksuite/cli auth login --recommend

bot 身份只能收发 IM,代替不了用户去读私有资源。所以 AGENTS.md 里要交代清楚,什么情况下该带 --as user

回复太慢

飞书 IM → Python → 起 ACP 子进程 → Agent 思考 → 可能还要 Shell / lark-cli,整条链路秒级到分钟级都正常。敏感操作可以先用 acp.mode: ask 限制工具,或者把高频问答写进 builtin_replies

别用 kill -9 杀 lark-cli

README 里专门提过:lark-cli event consume 被强杀可能导致飞书侧长连接状态异常。用 Ctrl+C 或 systemd stop 正常退出。

安全提醒

acp.mode: agent 配上 auto_approve_permissions: true,本质上就是允许远程 IM 触发本机命令执行。这件事得当回事,至少做到:

  1. allowed_chat_ids 限制能触发它的群;
  2. AGENTS.md 里把禁止事项写死(密钥、批量删除、擅自 --yes);
  3. .envconfig/agent.yaml 不进 Git;
  4. 用一个专门的低权限飞书应用,别拿个人超级账号长期挂着;
  5. 服务器本身的 SSH 和文件权限也要收紧。

它已经不是「聊天机器人」那个量级的风险了,更接近开了一个「远程 Shell 入口」。

小结

lark-assistant-agent 干的事其实很克制:飞书的收发交给 lark-cli,过滤和路由交给 Python,智能和工具交给 Cursor ACP。自己这边不用维护模型 SDK,不用管 token 计费和重试,也不用再造一个 Agent 运行时,这些都甩给 Cursor CLI 了。

跟 GolemBot 方案比,它更适合「我本机本来就在用 Cursor,顺手把飞书入口接上去」的人。代价也很实在:绑在 Cursor 生态上,ACP 协议还得自己跟着 Cursor CLI 的更新走。

我自己用下来比较舒服的分工是,确定性的事(ping、部署、定时报表)走本地规则或脚本,真正开放的问题才丢给 ACP。模型能力当然可以留着,但没必要让它去碰那些本来一行 bash 就能写死的路径。

项目地址:https://github.com/liqinsi/lark-assistant-agent

参考

版权声明: 本文首发于 指尖魔法屋-利用 Cursor ACP 模式搭建飞书助手机器人,无需任何大模型接口https://blog.thinkmoon.cn/post/996-bot-practice-feishu-cursor-acp/) 转载或引用必须申明原指尖魔法屋来源及源地址!