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

Packages

Package details

extension

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 · 简体中文

npm version License: MIT

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 via locale

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

  1. A Feishu enterprise self-built app (Feishu Open Platform → Developer Console → Create Enterprise Self-built App)
  2. In the app configuration:
    • Bot: enable the bot capability
    • Event subscription: add the Receive message im.message.receive_v1 event
    • Permissions:
      • im:message, im:message:send_as_bot (send messages) — required for markdown notifications
      • im: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)
  3. 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) and chatId (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 / chatId are not required: when unset, the extension auto-detects the send target — send the bot a DM to detect your userId, or send a message in a group to detect that group's chatId, and it's used automatically (use /feishu-notify bind to pin the detected values into config). Only configure them manually if you want to lock the send target.

Security tip: use ${ENV_VAR} placeholders for appSecret, 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 chatId is auto-detected

Once detected:

  • Automatic fallback: if neither userId nor chatId is 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 (userId or chatId), it is always used as-is — auto-detected values never override it.
  • /feishu-notify whoami: view the currently detected userId / 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, if userId isn'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:

  1. 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.
  2. 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 "allowedChatIds whitelist", or an "auto-detected group" (with requireMention: 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.
  3. Manual notification: run /feishu-notify <message> in pi to send a notification to Feishu manually; /feishu-notify with 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 between agent_start and agent_settled don't notify. E.g. minDurationMs: 60000 = short tasks under 1 minute stay silent.
  • Per-session mute: run /feishu-notify off (or mute) to mute the current session — no more automatic notifications after tasks finish; /feishu-notify on (or unmute) 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 the message_id, records message_id → session mapping in ~/.pi/agent/feishu-notify-router.json
  • Upstream: the SDK long connection receives a message → if replyToMessageId hits the routing table → cross-process dedup claim → pi.sendUserMessage injects 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.json records 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.json and 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-notify with 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:

  1. The reply is received → a "received your reply, processing…" receipt is sent immediately
  2. 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
  3. 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 through extractAssistantText, which skips thinking/toolCall parts)
  4. You can reply to that final result message to keep commanding

Why no live streaming: streaming requires pushing text_delta token 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 (same im:message permission); 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 via LANG / 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

License

MIT