@cr1ms0n/anon-notify

Pi Agent lifecycle notification extension with steer-based command handoff and a bundled Koishi webhook plugin.

Packages

Package details

extension

Install @cr1ms0n/anon-notify from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@cr1ms0n/anon-notify
Package
@cr1ms0n/anon-notify
Version
0.3.0
Published
Oct 13, 2026
Downloads
799/mo · 26/wk
Author
cr1ms0n
License
UNLICENSED
Types
extension
Size
200.4 KB
Dependencies
0 dependencies · 4 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

anon-notify

Pi Agent 生命周期通知扩展。Agent 完成或失败后,扩展把结构化事件发送到 YesImBot Webhook;YesImBot 将事件注入 Anon 的主对话流。插件不附加模型转告策略,也不代替 Anon 发消息。HTTP 接受、Anon 上下文接收和最终 QQ 发言是三个不同阶段;前两个阶段成功不保证 Anon 一定转告。

默认路径上,扩展和 notify-webhook 插件都不直接向 QQ 发送消息,也不使用通知专用模型、QQ 引用回复轮询或本地 reply journal。插件另有一个显式旁路:请求体带 delivery: "direct" 时,事件不经过 Anon、不经过模型,按固定格式直接发到配置的 Koishi 目标(用于上游模型配额这类无需人格处理的告警)。

安装

需要 Node.js 22.19+ 和提供 agent_settled 等扩展 API 的 Pi(已验证 Pi 0.87.1)。只安装扩展,不升级 Pi 本体:

pi install npm:@cr1ms0n/anon-notify@0.3.0

先按下方配置服务端地址、目标 QQ 与 Token,然后在 Pi 会话中重新加载:

/reload

使用裸命令 /anon-notify 开启当前会话。新安装不带个人目标,未配置有效 QQ 号时不能开启。

若之前通过 ~/.pi/agent/extensions/anon-notify 的源码目录联接加载,请先保存旧 config.json 到下方的稳定用户配置位置,再将旧联接移出自动发现目录,避免和 npm 包重复注册。只移动联接,不删除它指向的源码仓库。Token、journal 和 session activation 无须重建。

源码开发仍可直接使用仓库目录作为 Pi extension;npm 发布包仅包含运行文件、配置示例和说明,不包含本机配置、测试、备份或开发记录。

测试

依赖 Node 内置类型擦除和 node:test,不需要安装依赖:

node --test ./*.test.ts ./server/*.test.mjs

配置

用户配置放在包目录之外,不会被 npm 更新覆盖:

~/.pi/agent/extension-data/anon-notify/config.json

可从 config.example.json 开始,填写自己的 endpoint、recipient.qq 和 recipient.anonQq。如果设置了 PI_CODING_AGENT_DIR,此配置位于对应 Agent 目录的 extension-data/anon-notify/config.json。旧源码目录的 config.json 仍作为兼容后备,稳定用户配置优先于它;该旧文件不发布到 npm。

可信项目可以覆盖配置:

<project>/.pi/anon-notify.json

合并顺序为内置默认值、旧源码目录配置、稳定用户配置、可信项目配置、环境变量;后者覆盖前者。可用环境变量:

  • ANON_NOTIFY_ENABLED
  • ANON_NOTIFY_ENDPOINT
  • ANON_NOTIFY_TOKEN
  • ANON_NOTIFY_SOURCE
  • ANON_NOTIFY_RECIPIENT_QQ
  • ANON_NOTIFY_ANON_QQ

公开包不预设目标 QQ。请将下面的空值填为实际 QQ 号:

"recipient": {
  "qq": "",
  "anonQq": ""
}

服务端仍以 notify-webhook 的 recipientId 为最终投递目标;Pi 不会把目标 QQ 放进每次 webhook 请求,也不会把普通网络错误猜测成“未添加好友”。启用前和 TUI 状态栏会显示本地目标与服务端返回的目标状态;unknown/not_checked 只会显示警告。

Token 默认从这里读取:

~/.pi/agent/extension-data/anon-notify/token

主要配置项

  • enabled、endpoint、source、projectConfig、mainAgentOnly。默认 endpoint 是本机 http://127.0.0.1:5140/api/agent-notify/v1/events,不预设任何个人服务地址。
  • recipient.qq:通知目标 QQ;recipient.anonQq:Anon 的 QQ 号。它们用于明确显示和启用前校验,不允许请求方通过 Webhook 自行指定收件人。

其中 enabled 是扩展的全局配置总开关,不等于当前会话已经启用。没有持久 activation 记录的旧 session 和全新 /new session 默认 inactive;用户显式执行裸命令 /anon-notify 开启后,raw activation 会写入该 session 的 JSONL,之后才会发送生命周期通知、轮询 /commands,并开始发送在线 heartbeat。activation custom entry 不进入模型上下文,也不保存 Token、QQ、endpoint 或其他配置。

  • notify.*:开始、进展、完成、失败、单工具错误通知开关
  • progress.*:首次进展时间、周期、工具数阈值、错误通知冷却
  • delivery.*:HTTP 超时、重试次数、退避时间和非最终通知的最小间隔
  • command.*:指令轮询开关、journal 与空闲轮询间隔。deliveryWhenBusy 默认 steer;旧配置值 followUp 仍可读取,但会归一化为 steer,不会恢复等待整个任务空闲的行为。maxBatchSize 为配置兼容保留,新扩展每次只领取一条。
  • presence.*:heartbeatIntervalMs(本地 heartbeat 发送间隔,默认 10 秒)与 heartbeatTimeoutMs(本地参考上限,默认 30 秒)。服务端以 notify-webhook 的 sessionHeartbeatTimeoutMs 为权威 TTL;Pi 只按 heartbeatIntervalMs 发送,过期判断在服务端。
  • content.*:任务/摘要长度,以及项目、最终回复是否写入摘要。旧 includeModel、includeDuration、includeToolCounts 仅兼容读取并校验 boolean 类型,统一归一化为 false,不能再打开自动附加的模型名称、耗时或工具统计;不会修改用户配置文件。

配置会做严格类型检查。maxTaskChars 最大为 200,maxSummaryChars 最大为 1200,与 Webhook 字段限制一致。

notify.cancelled 仅为旧配置兼容而保留。TUI 中按 Esc、中止活跃低层 Agent,或退出/切换会话时始终静默,不发送 Anon 通知。Pi 默认的单次 Ctrl+C 是清空编辑器,双击 Ctrl+C 是退出,不属于活跃 Agent 的中断键。

source 模板变量

source 支持 {project}、{cwd}、{sessionId}、{model}、{mode}。默认值:

"source": "Pi Agent · {project}"

生命周期判定

  • 子 Agent 会话默认被识别并抑制,不产生通知。
  • 普通 assistant stop 视为正常完成。
  • 完整执行且每个结果都声明 terminate: true 的最终工具批次视为正常完成。
  • length、error、不完整/出错/非终止的最终 toolUse 视为失败。
  • 扩展在每个低层 agent_start 监听当前 AbortSignal,并在 TUI 中观察独立 Esc 输入,以覆盖自动重试、自动压缩和低层 run 之间的间隙。即使中断发生在工具执行期间、最后一条 assistant 仍显示为 toolUse,也会判定为用户中断并静默处理。
  • session_shutdown 的 quit、reload、new、resume、fork 都属于用户或会话生命周期操作,不发送结束通知。
  • 最终通知在 agent_settled 后发送,即 Pi 已没有自动重试、压缩重试或 follow-up。

扩展在未启用通知时仍记录主会话的运行事实,但不发送通知、不启动进展定时器。运行中执行 /anon-notify 后,heartbeat 根据 Pi 的 ctx.isIdle() 立即报告真实状态;正常结束仍发送一次完成事件。开始和结束都会请求立即 heartbeat,在途请求结束后会补发最新状态,不必等待下一个周期。

完成和失败通知会绕过本地 delivery.minIntervalMs,避免最终状态被进展通知的限流窗口吞掉;进展类通知仍受限流控制。服务端事件限额独立于 heartbeat/命令流量。HTTP 429 的 Retry-After 在有限重试内生效,单次附加等待最多 60 秒;无效或显式 failed 回执不算成功。正常 reload/shutdown 会等待实际在途通知结束,但没有跨进程崩溃的本地持久 outbox。

YesImBot 主对话流

Webhook 接收事件后,notify-webhook 插件会把事件转换成带边界标记的 runtime event,并调用 Anon 的主会话 messenger;事件正文直接位于边界标记之间:

[EXTERNAL_AUTOMATION_EVENT]
{event data}
[/EXTERNAL_AUTOMATION_EVENT]

数据块之后没有附加处理策略。payload 里出现的闭合标记会被转义成 JSON 可逆形式([\/EXTERNAL_AUTOMATION_EVENT]),因此模型生成的 summary / details 不能提前结束数据块。

事件使用 trigger: true 写入 Anon 主会话历史,并在主会话空闲时启动模型回合;如果 Anon 正忙,则通过 ifBusy: "defer" 延后到安全边界处理。已经发出的模型请求不会被追加入这条事件。queued / processing 表示服务端接受或正在处理;context_injected 表示 messenger 已接收;只有显式 direct 路径的 delivered 记录 Koishi 发送结果。若 messenger 已成功但回执落盘失败,当前进程只重试保存回执,不立即重复注入,也不移除下一条事件。进程在外部副作用与落盘之间崩溃时仍存在重复投递的不确定性。

这样可以避免两条并行的通知链:不会再出现“插件先生成固定 QQ 摘要,再由 Anon 处理一次”的重复通知,也不会把普通 QQ 消息自动转回原 Pi Agent 会话。

事件正文包含 event_id、session_id、source、task、status、summary、context_usage、details、url 和时间戳。Pi 发送方仍使用 Bearer Token、幂等键和 HTTP 重试;服务端负责队列、幂等去重和主上下文注入状态记录。

上下文占用

每条生命周期通知和当前会话的外部扩展通知都在构造 payload 时通过 Pi 的公开 ctx.getContextUsage() 同步读取一次,作为独立 context_usage 字段发送;它不会因为摘要截断而丢失,HTTP 重试也不会重新采样。

示例字段:

"context_usage": {
  "used_tokens": 48000,
  "context_window": 200000,
  "percent": 24
}

这是通知构造当下 Pi 的当前上下文估计,不是累计 token 消耗、收费量或 provider 的实时精确余量;尚未持久化的流式消息或刚完成的工具结果不保证已计入。百分比是数值,有限的超过 100% 的报告也会保留,不截断到 100%。

压缩后用量暂时未知、但窗口已知时,返回 { "used_tokens": null, "context_window": 200000, "percent": null }。没有可用模型、接口不可用、读取异常或数值非法时,整个字段为 null;未知不会冒充 0%,也不阻止原通知发送。

Koishi 会校验并保存该快照,事件查询和 Anon 的 EXTERNAL_AUTOMATION_EVENT 数据块都包含它。旧请求/旧 state 没有该字段时视为未知,无须迁移;非空对象必须含全部三个字段且无额外属性,tokens/窗口为非负/正安全整数,percent 为非负有限数,tokens 与 percent 必须同时已知或同时为 null。非法字段返回 HTTP 400 invalid_context_usage,不入队。

需要两端都更新才能让 Anon 收到上下文占用:旧 Koishi 插件会忽略新字段;新插件接收旧 Pi 请求时显示未知。本仓库的实现和测试不会自动发布 npm、部署远端插件或更新当前 Pi session。

Anon 显式指挥 Pi Agent

Anon 如果根据主对话上下文明确决定要继续处理某条工作,可以显式调用 agent_command,并同时提供:

  • event_id:通知事件的精确 ID;
  • session_id:产生该通知的 Pi Agent session ID;
  • instruction:长度受限的自然语言指令。
  • images(可选):最多 4 张受限图片,每项使用不带 data: 前缀的 base64 data 与 mime_type(支持 PNG/JPEG/GIF/WebP)。文字字段仍然必填。

服务端会验证通知与 session 的绑定,并把指令写入独立 command queue。工具返回“已入队”只表示 Pi 指令已经进入可靠队列,不表示 Agent 已完成,也不表示用户已经收到 QQ 消息。

待发内容保留在服务端 command queue。Pi 空闲时可以领取一条并立即启动处理;忙时只在当前轮次的 turn_end 边界领取一条,不提前批量交入 Pi 原生队列,也不等整个任务结束。空闲轮询的响应若在 Pi 已变忙后才到达,会推迟到安全边界交付。

扩展用 Pi 的公开 pi.sendMessage(..., { deliverAs: "steer", triggerTurn: true }) 发送类型为 anon-notify:command 的可见 custom message。没有图片时 content 仍是纯 instruction 字符串;有图片时 content 是一个文字块后跟原生 ImageContent 图片块。details 保存 command/event/session ID,扩展包装、ID、JSON、base64 和 marker 不进入模型内容,但指令与图片会正常作为用户级内容显示/发送。该路径不经过异步用户输入预处理,不修改 system prompt。API 返回只表示交付,不表示执行完成。

交付后,指令 ID 先持久写入独立 commands.jsonl journal,再 ACK 服务端,同时保留 session branch marker。租约重试、reload 或重启可通过 journal 去重。该通道不读取普通 QQ 消息,也不管理用户手工排队的内容。

在线会话发现与主动投递

agent_command 仍要求关联一个已处理的通知事件。Anon 若要根据当前对话直接管理仍然在线的 session,可改用另外两个工具:

  • agent_sessions:返回所有 heartbeat 有效的在线 session 安全投影,每项只包含 session_id、project(目录 basename)、model、idle/running、supports_images、last_activity_at 和 last_seen_at。不返回 Token、完整路径、会话正文、journal 内容或 QQ 信息。
  • agent_session_message:向指定在线 session 投递显式内容,参数为 session_id、content 和可选 images。只有服务端 registry 里 heartbeat 未过期的 session 才能接收;离线、过期、未开启或未知 session 直接被拒绝,不会排队等待下次启动。图片要求当前 Pi client 支持 image protocol。

投递复用同一套通道:服务端检查在线状态后写入 command queue,队列记录带 origin: "proactive" 与生成的关联 event_id;目标 session 按上述 steer 边界交付,本地 journal 持久化后才 ACK。相同工具调用重试按 runtime_id:turn_id:tool_call_id 幂等,不会重复创建指令;图片内容也参与 request-key 冲突判断。

图片 command 使用 protocol v2。新 Pi client 的 heartbeat 会声明 command_protocol: 2,轮询时声明 capabilities=images;旧 client 不会领取图片 command,避免把图片静默降级为纯文本。agent_sessions 返回 supports_images 供 Anon 选择目标。

图片限制为每条 command 最多 4 张、单张解码后最多 4 MiB、单条总计最多 8 MiB,服务端 pending/leased 图片总预算默认 32 MiB。MVP 只接受 raw base64,不接受 URL 远程抓图;终态 command 会清理图片原文,只保留数量、字节数和摘要元数据。agent_commands 不返回图片内容。

在线状态来自每个已启用 runtime 自己发送的 heartbeat,而不是扫描磁盘 session 文件:session 文件无法唤醒另一个 live runtime。注册表只存在于服务端内存中,并使用 sessionHeartbeatTimeoutMs TTL 清理;server 重启后由在线 runtime 自然重新登记。每个 runtime 携带随机 runtime_id,旧 runtime 的 offline 请求不能删除同一 session 的新记录。

查询与撤回待发 steer

同一个已配置私聊 scope 内,Anon 可使用:

  • agent_commands({ session_id, limit? }):查询该 session 的指令 ID、来源、状态、cancellable、时间和最多 200 字符的内容预览;最多返回 50 条,不暴露 lease token 或其他 session 的内容。
  • agent_command_cancel({ session_id, command_id }):撤回尚未开始交付的指令。重复撤回幂等;已撤回记录和请求键去重会持久保存,重启后不会复活。

创建工具也会返回 command ID、状态和 cancellable。查询和撤回已持久保存的待发内容不要求 session 当前在线;创建主动消息仍要求有效 heartbeat。

撤回截止点是服务端第一次原子领取、开始交付。 pending 且从未开始交付时可撤回;租约已开始返回 delivery_started,Pi 已 ACK 返回 already_accepted。租约过期或残缺不能证明内容未进入 Pi,因此不会重新变成可撤回。cancelled 是终态,不再领取或计入活跃队列。工具不能删除 Pi 原生 steer/follow-up 队列中的内容,也不会清空其他消息或过滤历史上下文来冒充撤回。

会话级启用与持久化

扩展仍会被 Pi 自动发现和加载,但不会默认作用于所有会话。activation 是具体 Pi session 的属性,不写入全局配置:

/anon-notify
  • /anon-notify 是唯一的命令形式,不再接受任何子命令。再次执行即关闭。带参数的输入不会改变状态,只会提示使用裸命令。
  • 开启和关闭都会先向当前 session JSONL 追加 customType: "anon-notify:activation"、version: 1 的布尔记录,再更新内存状态、TUI、EventBus、polling 和 heartbeat。写入失败时不会声称状态已经改变。
  • 同一 session 在 Pi /reload、退出后 resume、切换后返回和 runtime wrapper 重建时,会从全部 session entries 中恢复最新记录;同一文件内的 /tree branch navigation 不会改变 activation。
  • 没有记录的旧 session 和全新 /new session 默认 inactive,不继承上一个 session。
  • /fork 与 /clone 创建的新 session 会读取源 session 当前的 session-wide activation,并在子 session 写入一条继承快照;此后父子 session 独立启用或停用。
  • 最新匹配记录 malformed 或版本未知时 fail closed 为 inactive,不会回退到更旧的 active 值。
  • 启用前仍会校验目标 QQ 和 Anon QQ;如果服务端明确返回 not_added 或 unreachable,Pi 会显示包含两个 QQ 号的引导,要求先添加 Anon 或确认私聊可达后再重试。
  • 服务端没有可靠好友状态检查时会返回 unknown/not_checked;这只会显示警告,不会被误判为未添加。
  • 再次执行 /anon-notify 会持久化 inactive,停止生命周期通知与 /commands 轮询,并尽力发送一次 offline heartbeat。session_shutdown 同样清理 timer、停止 heartbeat 并 best-effort 注销;它不会写入 inactive。
  • 配置重载使用 Pi 自己的 /reload,不再提供带参数的通知命令;会话和服务端状态分别看 TUI 状态栏 / EventBus 与 agent_sessions。
  • subagent 即使恢复了 active 记录,也始终受 mainAgentOnly 规则抑制,不会发送 heartbeat,也不能通过 activation 绕过。

Pi Web 结构化状态协议

Wrapper 存活时,扩展通过当前 AgentSession 的 EventBus 发布 anon-notify:state version 1 payload,Pi Web 继续以带 runtimeId/revision 的 runtime snapshot 为权威。payload 只包含稳定枚举和布尔事实:sessionId、status、sessionActive、servicesEnabled、tokenConfigured;它不包含 Token、目标 QQ、Webhook 内容或人类展示文本。

Wrapper dormant 时,Pi Web server 从同一 session JSONL 读取 activation custom entry,并返回 source: "dormant" 的只读 projection。Dormant active 会显示为已开启且允许 disable,但不会猜测 Token、全局配置或服务 readiness;新 wrapper 启动后,runtime snapshot 会接管显示。浏览器不直接读取 JSONL,也不使用 localStorage 保存 activation。

Pi Web 工具栏开关继续通过现有 SSE + set_anon_notify RPC 调用 /anon-notify command handler(裸命令 toggle:当前 active 则关闭,否则开启);它不会调用 prompt、创建聊天气泡或模型回合,也不复制 recipient readiness、polling、webhook 或 command queue 逻辑。修改扩展后需要用户自行执行 Pi /reload;修改 Pi Web 源码后需要单独构建并重启对应 server 才会部署,本仓库测试不会自动执行这些运行时操作。

模型上下文边界

扩展不修改 system prompt、不注入扩展状态消息,也不注册模型可见的状态工具。active、inactive 和 subagent 抑制状态仅由扩展内部逻辑、结构化 EventBus 和 TUI 状态栏展示。

实际由 Anon 发回的 Agent command 作为明确标记的 custom-message steer 进入 Pi session,并由 Pi 转为用户级模型消息,因为它本身就是需要 Agent 处理的指令;这不属于扩展运行状态提示词。

命令

/anon-notify            # 当前会话未开启则开启,已开启则关闭

只有这一个裸命令。旧的带参数子命令已全部移除;带任何参数的输入都不会改变状态。

  • 开启前会校验目标 QQ 和 Anon QQ。如果目标配置缺失/无效,开启会被拒绝;如果服务端只返回网络错误,Pi 会允许开启但明确提示“状态未核验”,不会伪造好友状态。
  • 开启或关闭会写入 activation custom entry,并同步 TUI 状态栏、EventBus、/commands 轮询和在线 heartbeat。
  • 需要重读配置时使用 Pi 自带的 /reload;需要查看在线会话时由 Anon 调用服务端 agent_sessions 工具。

扩展间调用

其他 Pi 扩展可以通过事件总线发送通知:

pi.events.emit("anon-notify:send", {
  status: "info",
  source: "另一个扩展",
  task: "索引构建",
  summary: "已处理 500 个文件",
  eventId: "index-build-001"
});

状态可用:completed、failed、warning、info。

更新与回退

npm 包同时附带 server/notify-webhook.js、server/command-queue.js 和 server/session-registry.js。服务端需要已有的 Koishi 和 YesImBot 运行环境,安装 Pi 扩展不会自动部署或重启远端插件。先更新 Koishi 插件,再在 Pi 中 /reload 扩展。服务端 health 提供指令通道能力用于核对版本;本仓库的代码修改和测试不会自动部署、重启服务或更新 Pi 本体。

部署前备份服务端状态文件。旧服务器可能不认识 cancelled 和首次交付字段,回退前必须确认旧代码不会把撤回记录重新解释成可发送状态,不能通过删除 tombstone 回退。已经交入旧版 Pi 原生队列的内容也不能通过升级后撤回。

默认行为

  • 扩展自动加载;没有 activation 记录的 session 默认 inactive,已记录的 session 会恢复其最新 raw activation。
  • 只通知主 Agent,不通知 subagents。
  • 只有 active session 才会发送生命周期通知、轮询 Agent commands,并发送在线 heartbeat。
  • heartbeat 只用于在线发现;离线 session 不会被排队为后续投递目标。
  • 开始、进展和单工具错误即时通知关闭。
  • 正常完成和失败通知开启(仅对 active session 生效)。
  • 用户中断与会话切换始终不通知。
  • 通知通过 Webhook 注入 Anon 主对话流,不直接发送 QQ。
  • Webhook 超时 12 秒,最多尝试 3 次。
  • 非最终通知默认至少间隔 5 分钟;完成和失败不受该间隔限制。

许可

当前 npm 包标记为 UNLICENSED,未授予开源许可。公开下载不表示可按 MIT 等开源许可证使用。