omp packages

pi-feishu-notify0.7.0

Bidirectional bridge between pi main conversations and Feishu (Lark): sends a Feishu notification when a pi task finishes, and lets you reply to the notification in Feishu to remotely command that pi session to continue. No lark-cli dependency, built on t

billy.huang Published 23 days ago MIT ~327 downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

pi-feishu-notify

Languages: English · 简体中文

npm version License: MIT

pi 主对话 ⇄ 飞书双向桥:pi 任务完成时推送飞书通知,在飞书里回复通知即可远程指挥对应 pi 会话继续执行。

不依赖 lark-cli —— 基于官方 @larksuiteoapi/node-sdk 的 WebSocket 长连接直连飞书,无需安装任何额外 CLI 工具,也无需公网回调。

特性

  • ✅ 不依赖 lark-cli:官方 SDK 直连,零额外工具链
  • ✅ session 级通知:任务结束(agent_settled)自动推送飞书通知
  • ✅ 回复回注:在飞书里回复通知,指令自动注入对应 pi 会话继续执行
  • ✅ 双向桥:pi → 飞书(通知)/ 飞书 → pi(指令)
  • ✅ markdown 美化:通知默认以飞书富文本(post)渲染,标题/加粗/代码块/引用清晰美观
  • ✅ 只发最终结果:回复回注后同样只发过滤后的最终结果(markdown 通知),thinking/工具调用内容不会推给飞书
  • ✅ 进度反馈:长任务期间在回执消息上原地刷新「已用时 Xs + 项目/会话」,不会在飞书侧干等
  • ✅ 跨进程去重 & 转达:多 pi 进程场景下同一消息只处理一次;若长连接事件落在别的 pi 进程,指令会经本地收件箱转达给目标会话所在的进程回注
  • ✅ 崩溃自愈:自动清理死进程残留状态
  • ✅ 配置灵活:全局 + 项目级覆盖,支持环境变量插值,自动识别 userId/chatId

安装

# 通过 npm 安装(推荐,发布在 npmjs.org)
pi install npm:pi-feishu-notify

# 或通过 git 安装
pi install git:github.com/xiaohuzai/pi-feishu-notify

# 或本地路径
pi install ./path/to/pi-feishu-notify

后续更新:pi update npm:pi-feishu-notify,或用 pi update --all 一并更新。(也可以在包名后追加版本号锁定版本,如 npm:pi-feishu-notify@x.y.z —— 锁定版本的安装不会被 pi update 更新。)

前置条件

  1. 一个飞书企业自建应用(飞书开放平台 → 开发者后台 → 创建企业自建应用)

  2. 在应用配置里:

    • 机器人:启用机器人能力
    • 事件订阅:添加 接收消息 im.message.receive_v1 事件
    • 权限:
      • im:message、im:message:send_as_bot(发送消息)—— markdown 通知必需
      • im:message(更新消息)—— 进度心跳原地刷新回执消息需要(同一条权限,部分场景需 im:message:send_as_bot 已具备)
    • 长连接:事件订阅方式选择 使用长连接接收事件(SDK WebSocket 方式,无需公网 URL)
  3. 拿到应用的 App ID 和 App Secret,并把机器人加为联系人(私聊)或拉进群聊(群通知)。

注意权限变更生效时机:修改权限后,需到 版本管理 发布一个新版本,权限才会真正生效(很多人改完配置后不生效,是因为漏了这步)。

通知目标 userId(open_id)和 chatId(群 chat_id)无需手动获取:配置好并启动后,给机器人发一条消息即可自动识别(见下文「userId / chatId 怎么获取」)。

配置

在 ~/.pi/agent/settings.json(全局)或项目 .pi/settings.json(项目覆盖)中添加:

{
  "feishu-notify": {
    "enabled": true,
    "appId": "${FEISHU_APP_ID}",
    "appSecret": "${FEISHU_APP_SECRET}",
    "domain": "feishu",             // 'feishu'(默认,国内)| 'lark'(国际版)
    "replyEnabled": true,           // 是否允许回复回注
    "receipt": true,                // 转达后回执一条"已收到"
    "requireMention": false,        // 群聊时是否要求 @ 机器人
    "allowedSenderIds": [],         // 私聊白名单(open_id 列表),不填则只处理 p2p 单聊
    "allowedChatIds": [],           // 群聊白名单(chat_id 列表);不填时仍会放行"通知目标 chatId"与"自动识别过的群"里的回复
    "includeSummary": true,         // 通知中包含会话摘要(默认 true)
    "staleDays": 7,                 // 残留状态(会话历史/通知路由)保留天数
    "minDurationMs": 0,             // 任务最短时长(毫秒),>= 该值才发通知;0/缺省=不限制
    "logLevel": "normal",           // 'quiet'|'normal'|'verbose':日志详细度,normal 默认不再刷 notification-sent
    "messageFormat": "markdown",    // 通知/回复格式:'markdown'(默认,飞书富文本)| 'text'(纯文本)
  }
}

userId / chatId 不是必填:不配置时,扩展会自动识别通知目标——给机器人发一条私聊消息即识别出 userId,群里发消息即识别出 chatId,并自动用于发送(可用 /feishu-notify bind 把识别结果固定写入配置)。只有想锁定发送目标时才手动配置。

安全建议:appSecret 使用 ${ENV_VAR} 占位符,通过环境变量注入,避免明文写入配置文件。支持 ${NAME} 和 ${NAME:-fallback} 语法。

userId / chatId 怎么获取?可以自动识别

userId(open_id)和 chatId(群 chat_id)不需要手动查。只要你在飞书里给机器人发一条消息,扩展就会自动记住:

  • 私聊:你给机器人发一条消息 → 自动识别出你的 userId(open_id)
  • 群聊:把机器人拉进群,在群里 @ 机器人发一条消息 → 自动识别出该群的 chatId

识别到后:

  • 自动回退生效:如果 settings 里 userId / chatId 都没配,通知会自动发到刚识别出来的私聊用户(首次会自动提示可用 /feishu-notify bind 持久化)。只要配置了发送目标(userId 或 chatId),就始终按配置发送,自动识别值不会覆盖它。
  • /feishu-notify whoami:查看当前已识别的 userId / chatId(一般不需要,排查或想手动锁定目标时用)。
  • /feishu-notify bind:把识别到的值自动写入项目 .pi/settings.json(只补写缺失字段,不覆盖已有配置),重启或 /reload 后固定生效。
  • 重启也能记住:识别结果会存到 ~/.pi/agent/feishu-notify-discovered.json。下次启动时若 settings 仍未配置 userId 但已识别过,pi 里会弹一条一次性提示,提醒你用 /feishu-notify bind 一键写入。

提示:私聊自动回退是最顺滑的入门方式——配好 appId/appSecret、机器人加好友后发条消息,再把 task 交给 pi,通知就会发回来。

使用

配置完成后启动 pi(或 /reload),扩展自动生效:

  1. 收到通知:pi 每次任务结束(agent_settled)自动向飞书发送一条通知,包含项目名、会话 ID、时间与最近一次回复摘要,方便多任务区分:

    ✅ pi 主对话已完成
    项目: my-project
    会话: 01a00b5b
    时间: 2026/8/17 00:17:00
    
    <任务结果摘要>
    
    回复本消息可继续指挥该会话。
    
  2. 回复指挥:在飞书里直接回复这条通知,输入你要的指令(无需 @ 机器人)。消息会注入到对应的 pi 会话继续执行,执行完成后会再收到新的通知。

    • 群聊回复:只要该群是「通知目标 chatId」或「allowedChatIds 白名单」或「自动识别过的群」,回复都会被接收(requireMention: false 时无需 @ 机器人)。
    • 重启/跨进程后仍可回注:pi 重启会生成新的 session id,但回复会自动回退到同一项目(cwd 相同)的存活会话继续执行(本进程或其它 pi 进程均可)——不再因为旧会话 id 对不上而只回一句"会话已结束"。
  3. 手动通知:在 pi 里执行 /feishu-notify <消息> 可手动发送一条通知到飞书;/feishu-notify 无参数时查看扩展状态(含是否静音、最短时长阈值、是否已自动识别 userId)。

减少日志打扰 / 控制通知频率

  • 日志安静:默认 logLevel: 'normal' 只输出警告和错误,日常任务完成的 [feishu-notify] notification-sent {...} 不再刷屏。想更安静设 'quiet'(只报错),需要排查设 'verbose'(含发送细节)。
  • 长任务才通知:设置 minDurationMs(毫秒),任务从 agent_start 到 agent_settled 不足该时长则不发送。例如 minDurationMs: 60000 表示 1 分钟以内的短任务不打扰。
  • 按会话静音:在 pi 里执行 /feishu-notify off(或 mute)静音当前会话,任务完成后不再自动发通知;/feishu-notify on(或 unmute)恢复。适合某些只想本地、不想推飞书的工作场景。

工作原理

┌────────────┐   agent_settled    ┌──────────────┐  SDK WebSocket  ┌────────┐
│  pi session │ ─────────────────▶ │  Notification │ 长连接(直连)    │  飞书   │
│  (主对话)    │ ◀───────────────── │     Router    │                │  App   │
└────────────┘   sendUserMessage   └──────────────┘  ◀── 回复通知 ──  └────────┘
      ▲                │               │
      │                │               └── 按 replyToMessageId 反查目标 session
      └────────────────┴── 指令回注
  • 下行:agent_settled → 发送 markdown 富文本通知,拿到 message_id,记录到 ~/.pi/agent/feishu-notify-router.json(message_id → session 映射)
  • 上行:SDK 长连接收到消息 → 若 replyToMessageId 命中路由表 → 跨进程去重认领 → pi.sendUserMessage 回注指令
  • 常驻单例:WebSocket consumer 是进程级单例,跨 session 共享,不随 session 切换断开
  • 去重:~/.pi/agent/feishu-notify-dedup.json 记录已处理消息,跨进程互斥

多 pi 进程

飞书长连接是集群投递:同一应用多个 client 同时在线时,一条事件只会随机投给其中一个。因此开了多个 pi 进程(比如两个终端两个项目)时,回复可能被非目标会话所在的进程收到。本扩展对此做了处理:

  • 收到回复的进程先查会话注册表(pid + cwd)。若目标 session 在另一个存活进程,就把指令写入 ~/.pi/agent/feishu-notify-inbox.json,回执提示「正在转达给该会话所在的 pi 进程」——目标进程轮询收件箱(默认 1s)后在本地回注,并无缝接管回执上的进度刷新。
  • 若目标会话已结束,先回退到同项目(cwd 相同)的存活会话(跨进程同样生效),实在没有才回执「会话已结束」。
  • 若目标进程一直没取走(如卡死),约 30s 后回执会更新为「仍在等待」提示,而不是无声无息。

注意:

  • 该机制要求所有 pi 进程运行带收件箱支持的 pi-feishu-notify 版本;未被取走的条目会自动过期清理。
  • 全部状态都是 ~/.pi/agent/ 下带目录锁的普通文件,无需常驻守护进程。

markdown 美化 & 回复行为

通知 markdown 美化(默认)

任务完成通知默认以飞书富文本(post 消息 + md 元素)发送,由飞书原生渲染标题、加粗、列表、代码块、引用:

## ✅ pi 主对话已完成

**项目**:my-app
**会话**:a1b2c3d4
**时间**:2026-08-21 12:00:00

---

(任务摘要,代码块/列表原样保留)

> 回复本消息可继续指挥该会话

设为 messageFormat: "text" 可回退为纯文本。

回复回注:只发最终结果 + 进度反馈

当你在飞书回复通知指挥 pi 继续时:

  1. 收到回复 → 先发一条「已收到你的回复,正在处理…」回执
  2. 长任务期间 → 该回执消息会原地刷新「⏳ 仍在处理中,已用时 Xs… 项目「xxx」/ 会话 a1b2c3d4」(默认每 15s 一次,im.v1.message.update),让你知道 bot 还活着、在干活、处理的是哪个项目/会话,不会傻等
  3. 处理完成 → 回执更新为「✅ 处理完成,结果见下一条消息」,随后新发一条 markdown 通知,只含过滤后的最终结果(lastAssistantText,对最后一条 assistant 消息做 extractAssistantText,跳过 thinking / toolCall 部分)
  4. 你仍可回复这条最终结果消息继续指挥

为什么不做实时流式:流式需要把 text_delta 实时逐条推给飞书;部分 provider 会把推理/工具调用也以普通文本增量下发,导致 thinking、工具调用细节跟着流出去。因此默认只发「结算后」的最终结果,与初始任务通知行为一致,保证飞书看到的永远是干净的最终答案。

进度心跳需要更新消息权限:刷新回执用 im.v1.message.update(同 im:message 权限);若应用未开通该权限,进度刷新会静默失败,不影响回执和最终结果发送。

回复兼容

SDK 会把收到的富文本(post)消息转成纯文本,因此无论你回复的是 markdown 通知还是普通文本消息,回注指令都能正确提取。

文件结构

pi-feishu-notify/
├── extensions/
│   └── feishu-notify.ts     # pi 扩展入口(事件钩子 + 命令)
├── src/
│   ├── types.ts             # 类型定义
│   ├── config.ts            # 配置加载(全局+项目+env 插值+旧配置迁移)
│   ├── feishu.ts            # 飞书 SDK 客户端(发送 + 长连接,进程级单例)
│   ├── router.ts            # 通知路由 + 跨进程去重
│   ├── inbox.ts             # 跨进程指令转达(投递决策 + 收件箱)
│   ├── sessions.ts          # 会话注册表(崩溃自愈)
│   ├── filter.ts            # 发送/日志过滤(minDurationMs、logLevel)
│   ├── notify.ts            # 通知内容构建(markdown 美化 + 回复文本提取)
│   ├── settings.ts          # 自动识别 ID 持久化(/feishu-notify bind)
│   ├── discovery.ts         # 识别结果跨进程持久化(重启提示用)
│   └── state.ts             # 状态文件原子读写 + 目录锁
└── test/                    # 单元测试(vitest)

卸载

# npm 安装方式
pi remove npm:pi-feishu-notify

# git 安装方式
pi remove git:github.com/xiaohuzai/pi-feishu-notify

License

MIT