npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

im-notify-kit

v0.3.0

Published

飞书 / 企业微信群机器人通知的发送层:业务码校验、重试、超时、告警去重、卡片构建器。零依赖,跨 Node / Cloudflare Workers / Deno。Send layer for Feishu (Lark) & WeCom bot webhooks — biz-code aware, retrying, timeout-guarded, with alert dedupe.

Readme

im-notify-kit

飞书(Lark)/ 企业微信群机器人通知的发送层。零依赖,跨 Node / Cloudflare Workers / Deno。

npm i im-notify-kit

为什么有这个包

因为同一段推送代码在每个项目里都被重写了一遍,而且每次都漏掉同样的坑:

HTTP 200 不代表消息送达。 机器人被移出群、被停用、触发群安全设置、关键词不匹配——飞书和企微在这些情况下照样返回 HTTP 200,失败信息藏在 body.code / body.errcode 里。只判断 res.ok 的代码会把这些当成推送成功。如果你还顺手做了「推过就 15 分钟内不再推」的去重,那么这一次误判会把整个窗口期的告警全堵死——没人收到,也没人知道没收到

这个包把这类判断收在一个地方,只做一件事:把消息发出去,并诚实告诉你发没发到。

发一条

import { feishu, wecom } from 'im-notify-kit'

await feishu.text(FEISHU_HOOK, '构建失败')
await wecom.markdown(WECOM_HOOK, '**构建失败**')

const r = await feishu.card(FEISHU_HOOK, {
  title: '接口异常',
  template: 'red',
  markdown: '**接口**:`POST /api/feedback`\n**状态**:HTTP 500',
  buttons: [{ text: '看日志', url: 'https://example.com/logs', type: 'primary' }],
})

if (!r.ok) console.error(r.error, r.code, r.response)

一条消息发多个平台

同一份内容,两边都是可交互卡片:飞书渲染成 interactive 卡片,企微渲染成 template_card(主标题 + 键值区 + 跳转按钮)。

两家的卡片结构完全不同——飞书是 {msg_type:'interactive', card:{header, elements}},企微是 {msgtype:'template_card', template_card:{card_type, main_title, ...}}。把飞书那份 JSON POST 给企微 webhook,企微会回 HTTP 200 + errcode 非 0,消息进不去群。这个包按 platform 分别渲染,所以你写一份内容就行。

正文里 **键**:值 形式的行会自动抽成企微卡片的键值区(告警消息本来就是这个形状),抽不出来的行留在副标题,不丢内容。

import { notify } from 'im-notify-kit'

const results = await notify(
  [
    { platform: 'feishu', url: FEISHU_HOOK, name: '告警群' },
    { platform: 'wecom', url: WECOM_HOOK, name: '运维群' },
  ],
  { title: '发版完成', markdown: 'v1.2.0 已上线', template: 'green' },
)

results.filter(r => !r.ok).forEach(r => console.error(r.target.name, r.error))

并发发送,永不抛异常——单个目标失败不影响其它目标,你拿到的是一份完整战报。

接口 5xx 告警

import { apiAlert } from 'im-notify-kit'

await apiAlert(
  [{ platform: 'feishu', url: FEISHU_HOOK }],
  {
    route: '/api/feedback',
    method: 'POST',
    status: 500,
    detail: err.message,
    who: user.email,
    system: '好易美后台',
    logUrl: 'https://manager.example.com/api-logs',
  },
)

自带三条硬规矩:

  • 只推 5xx,不推 4xx。4xx 是调用方的问题(没登录、参数不对、越权),量大且多数是正常拒绝,推了只会淹没真问题。
  • 默认去重,按 route + status 为键、15 分钟窗口。一个坏接口配合前端轮询一分钟能发几十次请求,不去重群会被刷爆,然后所有人把机器人静音——那时告警就彻底失效,比没有告警更糟。
  • 去重的代价你要知道:故障持续期间群里是安静的。收到一条就得当回事,别等第二条。

API

| 函数 | 说明 | |---|---| | feishu.text(url, content, opts?) | 飞书纯文本 | | feishu.card(url, msg, opts?) | 飞书交互卡片 | | feishu.buildCard(msg) | 只拿卡片 payload(要走开放平台 API 而非群机器人时用) | | wecom.text(url, content, opts?) | 企微纯文本 | | wecom.markdown(url, content, opts?) | 企微 markdown | | wecom.templateCard(url, msg, opts?) | 企微模板卡片(可交互,带键值区和跳转) | | wecom.card(url, msg, opts?) | 用平台中立消息发企微,默认走模板卡片 | | wecom.buildTemplateCard(msg, fallbackUrl?) | 只拿企微卡片 payload | | wecom.renderMarkdown(msg) | 只拿 markdown 文本(需要纯文本渲染时用) | | notify(targets, msg, opts?) | 一条消息发多个目标 | | apiAlert(targets, info, opts?) | 接口 5xx 告警 | | postWebhook(platform, url, payload, opts?) | 底层出口,自定义 payload 时用 |

SendOptions

| 字段 | 默认 | 说明 | |---|---|---| | retries | 2 | 重试次数(不含第一次)。只重试网络异常、超时、5xx、429、飞书 9499、企微 45009——参数错、机器人被踢出群这类重试一万次也一样,直接放弃 | | timeoutMs | 10000 | 单次尝试超时。不设超时就是挂在 OS 层 TCP 超时上,不可接受 | | retryBaseMs | 500 | 退避基数,实际等待 base * 2^(n-1) | | fetchImpl | 全局 fetch | 注入 fetch,测试和特殊运行时用 | | dedupe | 无 | { key, windowMs?, store? },不传则不去重 |

SendResult

{
  ok: boolean          // HTTP 2xx 且平台业务码为 0,才是 true
  httpStatus: number   // 网络层就失败时为 0
  code?: number        // 飞书 body.code / 企微 body.errcode
  response: string     // 原始响应体(截断 500 字)
  attempts: number     // 实际尝试次数
  error?: string       // 人话失败原因
  deduped?: boolean    // 被去重拦下——这不是失败,是「刚推过,故意不推」
}

无状态运行时(重要)

去重默认用进程内存。Cloudflare Workers / Pages Functions / Lambda 每次请求都可能是新 isolate,模块级 Map 撑不过一次请求——在那些环境里默认去重等于没开。必须注入外部存储:

import type { DedupeStore } from 'im-notify-kit'

const kvStore: DedupeStore = {
  async shouldSend(key, windowMs) {
    const hit = await env.KV.get(key)
    return !hit
  },
  async markSent(key, windowMs) {
    await env.KV.put(key, '1', { expirationTtl: Math.ceil(windowMs / 1000) })
  },
}

await apiAlert(targets, info, { dedupe: { key: `alert:${route}:${status}`, store: kvStore } })

两条实现约定:

  • shouldSend 查询失败时应返回 true。宁可多推一条,也不要因为存储抖动就静默丢掉告警。
  • 只有发送成功才会调 markSent。失败也打标记的话,一次失败就把整个窗口期堵死了。

在 Cloudflare Pages Functions 里用(有个坑)

wrangler pages deploy 会 bundle functions/ 目录,里面的 import ... from 'im-notify-kit' 要靠 node_modules 解析。如果你的 CI 把「构建」和「部署」拆成两个 job,而部署 job 只继承了 dist/ 产物、没装依赖,部署会失败:

✘ [ERROR] Could not resolve "im-notify-kit"

修法是在部署 job 里补一步装依赖:

deploy:
  needs: [build]
  script:
    - npm ci --ignore-scripts   # ← 少了这行就 Could not resolve
    - npx wrangler pages deploy dist --project-name=xxx --branch=main

值得警惕的是这个失败的形态:typecheck、build 全绿,只有 deploy 红,线上还跑着旧版本、 表现完全正常。很容易被当成偶发的部署抖动,实际是那之后的提交全都没上线。

另外,本地跑 npx wrangler pages functions build 验证会通过——因为本地有 node_modules。本地环境比 CI 多点东西的验证是假验证,别拿它当数。

这个包不做什么

只做发送。读配置、写推送日志、存去重状态一律不碰——那些每个项目的存储都不一样(Supabase / KV / D1 / 内存),塞进来只会逼调用方迁就包的口味。需要持久化的地方走依赖注入(DedupeStore)。

同理,飞书开放平台的 tenant_access_tokenopen_id 解析、图片上传、@提及也不在这里:那些需要应用凭据和租户上下文,跟「群机器人 webhook」是两套东西,混进来会让这个包从「零依赖发送层」变成「飞书 SDK」。

License

MIT