pi-feishu-notify
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
Package details
Install pi-feishu-notify from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-feishu-notify- Package
pi-feishu-notify- Version
0.7.0- Published
- Sep 26, 2026
- Downloads
- 327/mo · 38/wk
- Author
- billy.huang
- License
- MIT
- Types
- extension
- Size
- 125.2 KB
- Dependencies
- 1 dependency · 1 peer
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-feishu-notify
Languages: English · 简体中文
Bidirectional bridge between pi main conversations and Feishu (Lark): when a pi task finishes, a notification is pushed to Feishu — and you can reply to the notification in Feishu to remotely command that pi session to continue.
No lark-cli dependency — it connects directly to Feishu over the official @larksuiteoapi/node-sdk WebSocket long connection. No extra CLI tools, no public callback URL needed.
Features
- ✅ No lark-cli: official SDK direct connection, zero extra toolchain
- ✅ Session-scoped notifications: auto-sends a Feishu notification when a task finishes (
agent_settled) - ✅ Reply-and-inject: reply to the notification in Feishu; your instruction is injected into the corresponding pi session to keep it going
- ✅ Bidirectional bridge: pi → Feishu (notifications) / Feishu → pi (instructions)
- ✅ Markdown formatting: notifications render as Feishu rich text (
post) with clean titles, bold, code blocks, and quotes - ✅ Final result only: follow-up replies send only the filtered final result (markdown notification) — thinking/tool-call content never reaches Feishu
- ✅ Progress feedback: long tasks refresh an elapsed-time progress line (plus project/session) in place on the receipt, so you never wait blindly
- ✅ Cross-process dedup & delivery: the same message is processed only once across multiple pi processes; if the WS event lands on a different pi process than the one owning the target session, the command is relayed via a local inbox so the right session still gets it
- ✅ Crash self-healing: automatically cleans up residual state from dead processes
- ✅ Flexible config: global + project-level overrides, environment variable interpolation, auto-detect
userId/chatId - ✅ i18n: English-first by default; Chinese is automatic in
zh_*environments, or force it vialocale
Install
# from npm (recommended — published on npmjs.org)
pi install npm:pi-feishu-notify
# or via git
pi install git:github.com/xiaohuzai/pi-feishu-notify
# or a local path
pi install ./path/to/pi-feishu-notify
To update later: pi update npm:pi-feishu-notify, or pi update --all to update everything at once. (You can pin a version by appending it to the package name, e.g. npm:pi-feishu-notify@x.y.z — pinned installs are skipped by pi update.)
Prerequisites
- A Feishu enterprise self-built app (Feishu Open Platform → Developer Console → Create Enterprise Self-built App)
- In the app configuration:
- Bot: enable the bot capability
- Event subscription: add the
Receive message im.message.receive_v1event - Permissions:
im:message,im:message:send_as_bot(send messages) — required for markdown notificationsim:message(update messages) — needed by the progress heartbeat to refresh the receipt in place
- Long connection: set the event subscription mode to Receive events via long connection (SDK WebSocket mode — no public URL required)
- Get the app's App ID and App Secret, and add the bot as a contact (DM) or pull it into a group (group notifications).
Note on permission changes: after modifying permissions, you must publish a new version under Version Management for them to take effect. (Many people change settings and see no effect simply because they skipped this step.)
The notification target
userId(open_id) andchatId(group chat_id) don't need to be fetched manually: after configuring and starting, just send the bot a message and it will auto-detect them (see "How to get userId / chatId" below).
Configuration
Add to ~/.pi/agent/settings.json (global) or project .pi/settings.json (project override):
{
"feishu-notify": {
"enabled": true,
"appId": "${FEISHU_APP_ID}",
"appSecret": "${FEISHU_APP_SECRET}",
"domain": "feishu", // 'feishu' (default, China) | 'lark' (international)
"replyEnabled": true, // whether to allow reply-and-inject
"receipt": true, // send a "received" receipt after relaying
"requireMention": false, // in groups, whether @bot is required
"allowedSenderIds": [], // DM whitelist (open_id list); empty = only handle p2p DMs
"allowedChatIds": [], // group whitelist (chat_id list); empty = still allow the "notification target chatId" and "auto-detected groups"
"includeSummary": true, // include the session summary in notifications (default true)
"staleDays": 7, // days to keep stale state (session history / notification routes)
"minDurationMs": 0, // min task duration (ms); only notify when >= this value; 0/absent = no limit
"logLevel": "normal", // 'quiet'|'normal'|'verbose': log verbosity; normal no longer spams notification-sent
"messageFormat": "markdown", // notification/reply format: 'markdown' (default, Feishu rich text) | 'text' (plain)
"locale": "auto" // 'auto' (default, detect via LANG) | 'en' (English) | 'zh' (中文)
}
}
userId/chatIdare not required: when unset, the extension auto-detects the send target — send the bot a DM to detect youruserId, or send a message in a group to detect that group'schatId, and it's used automatically (use/feishu-notify bindto pin the detected values into config). Only configure them manually if you want to lock the send target.
Security tip: use
${ENV_VAR}placeholders forappSecret, injected via environment variables to avoid plaintext in config files. Supports${NAME}and${NAME:-fallback}syntax.
How to get userId / chatId? Auto-detection
userId (open_id) and chatId (group chat_id) don't need to be looked up manually. Just send the bot a message in Feishu and the extension remembers it automatically:
- DM: send the bot a message → your
userId(open_id) is auto-detected - Group: pull the bot into a group, @bot and send a message → that group's
chatIdis auto-detected
Once detected:
- Automatic fallback: if neither
userIdnorchatIdis set in settings, notifications go to the just-detected DM user automatically (first time it hints you can persist with/feishu-notify bind). If you have configured a target (userIdorchatId), it is always used as-is — auto-detected values never override it. /feishu-notify whoami: view the currently detecteduserId/chatId(usually unneeded; use it for troubleshooting or to lock targets manually)./feishu-notify bind: write the detected values into project.pi/settings.json(only fills missing fields, never overwrites existing config); takes effect after restart or/reload.- Survives restarts: detection results are stored in
~/.pi/agent/feishu-notify-discovered.json. On next startup, ifuserIdisn't configured but was detected before, pi shows a one-time hint to use/feishu-notify bind.
Tip: DM auto-fallback is the smoothest onboarding — configure
appId/appSecret, add the bot as a friend, send a message, hand a task to pi, and the notification comes back.
Usage
Start pi after configuring (or /reload) — the extension activates automatically:
Receive a notification: when a pi task finishes (
agent_settled), a notification is sent to Feishu automatically, containing project name, session ID, time and the latest reply summary, so you can tell multi-tasks apart:✅ pi main task completed Project: my-project Session: 01a00b5b Time: 2026/8/17 00:17:00 <task result summary> Reply to this message to keep guiding this session.Reply to command: in Feishu, just reply to the notification with your instruction (no need to @ the bot). The message is injected into the corresponding pi session to continue, and you'll receive a new notification when it finishes.
- Group replies: replies are accepted as long as the group is the "notification target
chatId", the "allowedChatIdswhitelist", or an "auto-detected group" (withrequireMention: false, no @bot needed). - Injection survives restarts & processes: pi generates a new session id after restart, but the reply automatically falls back to a live session in the same project (same cwd) — in this process or any other pi process — to continue. No more "session ended" dead-ends due to an old session id mismatch.
- Group replies: replies are accepted as long as the group is the "notification target
Manual notification: run
/feishu-notify <message>in pi to send a notification to Feishu manually;/feishu-notifywith no args shows extension status (muted state, min duration threshold, auto-detected userId).
Reduce log noise / control notification frequency
- Quiet logs: default
logLevel: 'normal'only outputs warnings and errors — daily[feishu-notify] notification-sent {...}no longer spams. Set'quiet'(errors only) to be quieter, or'verbose'(includes send details) for troubleshooting. - Only notify long tasks: set
minDurationMs(ms); tasks shorter than this betweenagent_startandagent_settleddon't notify. E.g.minDurationMs: 60000= short tasks under 1 minute stay silent. - Per-session mute: run
/feishu-notify off(ormute) to mute the current session — no more automatic notifications after tasks finish;/feishu-notify on(orunmute) to restore. Good for workflows you want to keep local.
How it works
┌────────────┐ agent_settled ┌──────────────┐ SDK WebSocket ┌────────┐
│ pi session │ ─────────────────▶ │ Notification │ long conn │ Feishu │
│ (main) │ ◀───────────────── │ Router │ │ App │
└────────────┘ sendUserMessage └──────────────┘ ◀── reply ───── └────────┘
▲ │ │
│ │ └── lookup target session by replyToMessageId
└────────────────┴── command injection
- Downstream:
agent_settled→ sends a markdown rich-text notification, gets themessage_id, recordsmessage_id → sessionmapping in~/.pi/agent/feishu-notify-router.json - Upstream: the SDK long connection receives a message → if
replyToMessageIdhits the routing table → cross-process dedup claim →pi.sendUserMessageinjects the command - Resident singleton: the WebSocket consumer is a process-level singleton shared across sessions, and doesn't drop when sessions switch
- Dedup:
~/.pi/agent/feishu-notify-dedup.jsonrecords processed messages with cross-process mutual exclusion
Multiple pi processes
The Feishu long connection is cluster-delivery: when several clients of the same app are connected, each event is delivered to exactly one of them at random. So if you run multiple pi processes (e.g. two terminals in different projects), a reply may be received by a process other than the one owning the target session. This extension handles that:
- The process that receives the reply looks up the target session in the session registry (pid + cwd). If the session lives in another live process, the command is written to
~/.pi/agent/feishu-notify-inbox.jsonand the receipt says "handing it to the pi process that owns this session" — the owning process polls the inbox (default every 1s) and injects it there, seamlessly taking over the receipt's progress updates. - If the target session has ended, it falls back to a live session in the same project (same cwd) — across processes — before reporting "session ended".
- If the owning process never picks up the command (e.g. it's frozen), the receipt is updated to say so after ~30s, instead of silently doing nothing.
Notes:
- This works as long as all pi processes run a version of
pi-feishu-notifywith inbox support; an inbox entry that is never picked up expires automatically. - Everything is plain state files with directory locks under
~/.pi/agent/— no daemon, no network beyond Feishu itself.
Markdown formatting & reply behavior
Notification markdown formatting (default)
Task-completion notifications are sent as Feishu rich text (post message + md element) by default, with title, bold, lists, code blocks, and quotes rendered natively by Feishu:
## ✅ pi main task completed
**Project**: my-app
**Session**: a1b2c3d4
**Time**: 2026-08-21 12:00:00
---
(task summary, code blocks/lists preserved as-is)
> Reply to this message to keep guiding this session.
Set messageFormat: "text" to fall back to plain text.
Reply behavior: final result only + progress feedback
When you reply to a notification in Feishu to command pi to continue:
- The reply is received → a "received your reply, processing…" receipt is sent immediately
- Long tasks → the receipt message is updated in place with "⏳ Still working… Ns elapsed — project: xxx / session: a1b2c3d4" (every 15s by default, via
im.v1.message.update), so you know the bot is alive, working, and which project/session it's handling — no blind waiting - Done → the receipt updates to "✅ Done — see the result in the next message", then a new markdown notification is sent containing only the filtered final result (
lastAssistantText: the last assistant message passed throughextractAssistantText, which skipsthinking/toolCallparts) - You can reply to that final result message to keep commanding
Why no live streaming: streaming requires pushing
text_deltatoken by token to Feishu; some providers emit reasoning/tool-calls as plain text deltas, so thinking and tool-call details leak out. That's why we only send the settled final result — consistent with initial task notifications, guaranteeing Feishu always sees a clean final answer.
Progress heartbeat needs the update-message permission: refreshing the receipt uses
im.v1.message.update(sameim:messagepermission); if the app lacks it, progress refresh silently fails without affecting the receipt or the final result.
Reply compatibility
The SDK converts received rich-text (post) messages to plain text, so no matter whether you reply to a markdown notification or a plain text message, the injected command is extracted correctly.
Language (i18n)
User-visible text — Feishu notifications, receipts, /feishu-notify command output, and error messages — follows the locale setting:
auto(default): detects the environment viaLANG/LC_ALL/LC_MESSAGES;zh_*environments get Chinese, everything else gets English.en: force English (the default main language for global developers).zh: force Chinese (简体中文).
Docs (this README) and the gallery description are English-first, with a 简体中文版 available. Log events and developer scripts use English.
File structure
pi-feishu-notify/
├── extensions/
│ └── feishu-notify.ts # pi extension entry (event hooks + commands)
├── src/
│ ├── types.ts # type definitions
│ ├── config.ts # config loading (global + project + env interpolation + legacy migration)
│ ├── feishu.ts # Feishu SDK client (send + long connection, process-level singleton)
│ ├── router.ts # notification routing + cross-process dedup
│ ├── inbox.ts # cross-process command relay (delivery decision + inbox)
│ ├── sessions.ts # session registry (crash self-healing)
│ ├── filter.ts # send/log filtering (minDurationMs, logLevel)
│ ├── notify.ts # notification content building (markdown formatting + reply text extraction)
│ ├── settings.ts # auto-detected ID persistence (/feishu-notify bind)
│ ├── discovery.ts # cross-process persistence of detection results (restart hint)
│ ├── i18n.ts # lightweight internationalization (en/zh messages)
│ └── state.ts # state file atomic read/write + directory lock
├── scripts/ # developer helper scripts (capture open_id, verify credentials)
└── test/ # unit tests (vitest)
Uninstall
# npm install
pi remove npm:pi-feishu-notify
# git install
pi remove git:github.com/xiaohuzai/pi-feishu-notify