@quecue/notify-core 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +21 -0
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +28 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +63 -0
- package/README.en.md +216 -0
- package/README.md +215 -2
- package/SECURITY.en.md +40 -0
- package/SECURITY.md +40 -0
- package/dist/channels.d.ts +49 -0
- package/dist/channels.js +123 -0
- package/dist/feishu-sdk.d.ts +5 -0
- package/dist/feishu-sdk.js +44 -0
- package/dist/feishu.d.ts +100 -0
- package/dist/feishu.js +145 -0
- package/dist/formatting.d.ts +24 -0
- package/dist/formatting.js +197 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/messages.d.ts +23 -0
- package/dist/messages.js +20 -0
- package/dist/transport.d.ts +29 -0
- package/dist/transport.js +118 -0
- package/package.json +26 -4
package/CHANGELOG.en.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
English | [简体中文](CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
- Add a separate Feishu self-built app `createFeishuClient`: explicitly start WebSocket reception of `im.message.receive_v1`, with HTTP sending/replies and no card-action callbacks. Existing Webhook `makePlan`/`deliver` and `NotifyConfig` remain unchanged.
|
|
8
|
+
- Synchronous creation is offline and the SDK is lazy-loaded. Startup waits for actual readiness, defaults to a 10000 ms timeout and is idempotent. Support connection states, stopping with pending-start cancellation/reconnection disabled, and restart. Stop does not wait for or cancel callbacks/HTTP requests in progress; sending does not require startup.
|
|
9
|
+
- Add safe `FeishuError` errors and offline/custom `adapterFactory` injection. Message handlers are awaited; failure is safely reported before returning a failure ACK. Isolate `onError` failures and disable SDK logs by default. Handlers must finish within 3 seconds; hosts own deduplication, authorization, self-reply prevention and durable queues, with no exactly-once guarantee across disconnects/redelivery.
|
|
10
|
+
- Add the official MIT-licensed runtime dependency `@larksuiteoapi/node-sdk`, pinned to `1.76.0`, for app tokens, the binary protocol, framing, heartbeats and reconnection. SDK HTTP separately enforces 10 seconds/1 MiB/no redirects; WebSocket frames are outside `createTransport`'s JSON-response guarantee.
|
|
11
|
+
- Synchronize Chinese/English receive/reply examples, security boundaries, contribution and release guidance, including npm registry access when installing the SDK.
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
First standalone release; the npm registry is authoritative for actual publication status and available versions.
|
|
16
|
+
|
|
17
|
+
- Extract independent Telegram/Feishu planning, formatting, splitting and delivery from [dev-notify](https://github.com/QueCue/dev-notify).
|
|
18
|
+
- Provide injectable bounded JSON transport, selected Telegram APIs, safe error types and plain-text splitting.
|
|
19
|
+
- Exclude application configuration, keyrings, polling loops, authorization and business state; hosts can inject translation and transport.
|
|
20
|
+
- Add Chinese/English usage and security documentation plus a publication checklist. There are 13 public API tests using simulated network responses and 8 release-validation tests, not exhaustive platform or real-service acceptance.
|
|
21
|
+
- Add a manually dispatched GitHub Actions npm release workflow: verification-only by default, separate OIDC identity and provenance for publishing, delivery of the same verified artifact, and remote integrity checks. The account owner must first initialize the npm package and configure trust.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
简体中文 | [English](CHANGELOG.en.md)
|
|
4
|
+
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
- 新增独立的飞书自建应用 `createFeishuClient`:显式启动 WebSocket 接收 `im.message.receive_v1`,通过 HTTP 发送/回复,不支持卡片动作回调;现有 Webhook `makePlan`/`deliver` 和 `NotifyConfig` 不变。
|
|
8
|
+
- 同步创建不联网,SDK 懒加载;启动等待实际 ready,默认超时 10000 毫秒,重复启动幂等。支持连接状态、停止取消待启动/禁用重连和重启;停止不等待或取消已执行的回调/HTTP 请求,发送无需先启动。
|
|
9
|
+
- 新增 `FeishuError` 安全错误和离线/自定义 `adapterFactory`;消息回调被 await,失败安全报告后返回失败 ACK,`onError` 失败被隔离,默认不输出 SDK 日志。回调须在 3 秒内结束;宿主负责去重、授权、防自回复和持久队列,不保证断线/重推恰好一次。
|
|
10
|
+
- 新增官方 MIT 许可运行时依赖 `@larksuiteoapi/node-sdk`,固定为 `1.76.0`,负责应用 token、二进制协议、分帧心跳和重连;SDK HTTP 独立限制 10 秒/1 MiB/禁止重定向,WebSocket 帧不适用 `createTransport` 的 JSON 响应保证。
|
|
11
|
+
- 同步中英文接收/回复用法、安全边界、贡献与发布说明,明确安装 SDK 会访问 npm registry。
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
首次独立发行;实际发布状态和可用版本以 npm registry 为准。
|
|
16
|
+
|
|
17
|
+
- 从 [dev-notify](https://github.com/QueCue/dev-notify) 提取独立的双渠道消息规划、格式化、分段和投递能力。
|
|
18
|
+
- 提供可注入的有界 JSON 传输、有限 Telegram API、安全错误类型及纯文本分段。
|
|
19
|
+
- 不包含应用配置、凭据库、更新接收循环、授权或业务状态;允许宿主注入翻译和传输。
|
|
20
|
+
- 补充中文/英文使用说明、安全边界和公开发布检查表;13 项公共 API 测试使用模拟网络,另有 8 项发行校验测试,不代表全平台或真实服务验收。
|
|
21
|
+
- 新增手动触发的 GitHub Actions npm 发行:默认仅验证,正式模式使用独立 OIDC 身份及 provenance,上传同次验证的候选包并核对远端完整性;npm 包初始化和信任配置须先由账户所有者完成。
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 贡献与本地验证
|
|
2
|
+
|
|
3
|
+
本库仅维护 Telegram/飞书通用通讯,不接收应用专属命令、会话业务或授权策略。源码与贡献入口为 [QueCue/notify-core](https://github.com/QueCue/notify-core)。
|
|
4
|
+
|
|
5
|
+
## 开发环境
|
|
6
|
+
|
|
7
|
+
- Node.js 22 或更高版本、npm。
|
|
8
|
+
- 在本仓库执行 `npm ci --ignore-scripts`、`npm test`。
|
|
9
|
+
- 测试默认禁止真实网络,使用虚假凭据和注入的 HTTP/等待函数;飞书客户端通过 `adapterFactory` 注入离线 SDK 适配器,不应要求贡献者提供真实 Bot Token、Webhook、App ID 或 App Secret。依赖安装会访问 npm registry,与运行时测试离线是不同边界。
|
|
10
|
+
- 修改 API 时同步 `.d.ts` 构建、中文/英文 README、错误边界和变更记录。不要手工编辑 `dist`。
|
|
11
|
+
|
|
12
|
+
建议为限流、超时、格式回退、Unicode 分段、部分投递、自定义回调失败和错误脱敏补测试。保持默认导入及客户端创建无网络或配置副作用,SDK 懒加载。传输与授权的边界应写清楚,不要使用测试通过来替代真实平台兼容承诺。
|
|
13
|
+
|
|
14
|
+
飞书客户端修改还应覆盖:实际 WS ready 才完成启动、默认 10000 毫秒启动超时/致命失败、重复启动幂等、同步停止取消待启动并禁用重连、停止后重启,以及 `idle`/`connecting`/`connected`/`reconnecting`/`failed`/`stopped` 状态。验证 SDK 原格式事件与 JSON 字符串 `content`、无需启动的 HTTP 发送/回复、`FeishuError` 的 operation/数值 code 和错误脱敏。消息回调须被 await,失败返回失败 ACK;`onError` 抛错/拒绝须隔离且不输出原日志。`stop()` 不等待或取消已执行的回调/HTTP 请求。
|
|
15
|
+
|
|
16
|
+
回调需在 3 秒内结束;去重、授权、防机器人自回复和耗时任务持久队列属于宿主,不承诺断线/重推恰好一次。现有 Webhook `makePlan`/`deliver` 与 `NotifyConfig` 保持不变,不扩展为卡片动作回调接收器。SDK HTTP 的 10 秒/1 MiB/禁重定向限制应单独验证,不要把 `createTransport` 的 JSON 响应保证套用到 WebSocket 帧。
|
|
17
|
+
|
|
18
|
+
## 提交范围
|
|
19
|
+
|
|
20
|
+
不要提交 `.env`、Token、Webhook、真实接收人、消息原文、日志、node_modules 或包含这些内容的安装包。使用明显的假值,例如 `123:TEST_ONLY`;issue 和截图也需脱敏。
|
|
21
|
+
|
|
22
|
+
MIT 许可证及从 [dev-notify](https://github.com/QueCue/dev-notify) 提取的既有版权声明应保留。飞书客户端使用固定版本 `1.76.0` 的官方 MIT 许可运行时依赖 `@larksuiteoapi/node-sdk`,由其负责应用 token、二进制协议、分帧心跳和重连,默认关闭 SDK 日志;不要再将本库描述为无运行时依赖。新增/更新依赖、复制外部代码或改变发布方式前,请说明来源、许可证、目的及测试范围。
|
|
23
|
+
|
|
24
|
+
## 版本包
|
|
25
|
+
|
|
26
|
+
当前为 0.x API,不承诺跨版本不变;破坏性变化必须在变更记录和版本中明确体现。生成包前执行测试及 `npm pack --dry-run`,检查 [PUBLISHING.md](PUBLISHING.md)。相同版本号的正式发布包不可覆盖,不应以手改 tarball 的方式绕开版本更新。
|
|
27
|
+
|
|
28
|
+
安全问题按 [SECURITY.md](SECURITY.md) 处理,不在公开 issue 中附秘密或完整攻击细节。仓库地址、权限和实际发布状态以维护者公开配置为准。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dev-notify contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PUBLISHING.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# npm 发布流程
|
|
2
|
+
|
|
3
|
+
本说明面向 [QueCue/notify-core](https://github.com/QueCue/notify-core) 维护者。正式 npm 发行使用本仓库的 [GitHub Actions 工作流](https://github.com/QueCue/notify-core/actions/workflows/publish-npm.yml),不共享其他项目的发布身份。普通分支推送或版本标签推送均不会自动发布。
|
|
4
|
+
|
|
5
|
+
## 包和发布边界
|
|
6
|
+
|
|
7
|
+
- 包名 `@quecue/notify-core`,MIT 许可证,公开 npm 包。保留从 [dev-notify](https://github.com/QueCue/dev-notify) 提取代码的版权声明。
|
|
8
|
+
- 飞书客户端新增官方 MIT 许可运行时依赖 `@larksuiteoapi/node-sdk`,固定为 `1.76.0`;本库不再是无运行时依赖。直接版本由 package.json 固定,开发工具及依赖树由 package-lock 固定;发行前核对 SDK 和传递依赖的来源、许可证及锁文件。npm 包仅含 dist 运行时/类型、公开说明、package.json 和许可证,不含测试、CI 脚本或应用凭据。
|
|
9
|
+
- GitHub Actions 使用托管 Ubuntu runner、Node.js 24 和 npm 11.21.0;只有正式发布任务拥有 `id-token: write`。不需要 `NPM_TOKEN`、`NODE_AUTH_TOKEN` secret 或本机 npm 登录。
|
|
10
|
+
- GitHub 仓库公开、工作流存在和测试通过,都不能证明 npm 账户侧信任已配置,也不等于包已经发布。
|
|
11
|
+
|
|
12
|
+
## 首次建立 npm 包与信任
|
|
13
|
+
|
|
14
|
+
**npm 官方要求配置 Trusted Publisher 的包已经存在于 registry。** 尚不存在的包不能仅凭本工作流建立首次信任。账户所有者需先通过 npm 支持的认证流程初始化包,然后在该包的 npm 设置配置以下身份;不要把本机登录凭据或其他包的授权复制到 Actions。
|
|
15
|
+
|
|
16
|
+
| 字段 | 值 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Provider | GitHub Actions |
|
|
19
|
+
| Organization or user | `QueCue` |
|
|
20
|
+
| Repository | `notify-core` |
|
|
21
|
+
| Workflow filename | `publish-npm.yml`,不含目录 |
|
|
22
|
+
| Environment | 留空,本工作流不使用 environment |
|
|
23
|
+
| Publishing permission | 允许直接 `npm publish` |
|
|
24
|
+
|
|
25
|
+
阶段发布也是 npm 提供的初始化方式,但会产生公开的 `0.0.0-stage` 占位版本,并需账户认证和后续审核;本工作流不会擅自创建该额外版本。具体初始化操作应由维护者明确选择及执行。若首次已直接发布 `0.1.0`,Actions 不会覆盖它或把不同 tarball 当作同一次发行,应使用下一版本验证后续 OIDC 发行。
|
|
26
|
+
|
|
27
|
+
新建且尚未成功发布的信任配置会在两天后过期;请在包和工作流准备好后配置。成功发行会验证绑定,不能把它理解为所有已绑定配置每两天失效。本流程不自动改 dist-tag,不要求额外的标签管理权限。
|
|
28
|
+
|
|
29
|
+
## 日常发行
|
|
30
|
+
|
|
31
|
+
1. 检查待公开源码、历史、版本、CHANGELOG、中英文 README 和安全说明,避免 Token、Webhook、真实配置、本机路径及无关内容。
|
|
32
|
+
2. 修改 package.json 和 package-lock 的版本。npm 已有且内容不同的版本必须换版本,不覆盖或重打历史版本。
|
|
33
|
+
3. 本地执行 `npm ci --ignore-scripts`、`npm test`。当前回归包含模拟网络的公共 API 测试及发行校验测试,不代表全部平台、代理或真实服务验收。
|
|
34
|
+
4. 将已审核的源码、锁文件和工作流推送到 GitHub。进入 Actions → **Publish npm** → **Run workflow**,选择待发行分支或提交所在分支,填写与 package.json 完全一致的 `version`。
|
|
35
|
+
5. 保持 `verify_only=true` 先运行验证:安装锁定依赖、编译、测试、生成 tarball、校验精确文件白名单/元数据/SHA512,再隔离安装同一个 tarball,核对 ESM 导出及 TypeScript 类型。下载 `npm-package` artifact 检查。
|
|
36
|
+
6. 确认 npm 信任已建立且版本可发行后,对**同一提交**再次运行,设置 `verify_only=false`。正式运行会重新验证,并只上传本次验证任务保存的同一 tarball;不是直接上传之前某次运行或本机目录中的包。
|
|
37
|
+
|
|
38
|
+
隔离安装本地 tarball **仍会联网 npm registry 下载 SDK 及其传递依赖**,不是完全离线检查;`npm ci --ignore-scripts` 也需要可用的 registry/缓存。安装忽略脚本不等于禁止网络,隔离目录不等于网络沙箱。库导入/客户端创建不联网、SDK 懒加载的承诺仅指运行时,不覆盖 npm 安装;冒烟验证应保持使用虚假凭据和离线适配器,不执行真实 `start()` 或发送。检查新客户端的 ESM 导出和类型、启动/停止/错误边界,以及 Webhook/`NotifyConfig` 未变;SDK HTTP 的 10 秒/1 MiB/禁止重定向保护不代表 WebSocket 帧也有 `createTransport` 的 JSON 响应保证。
|
|
39
|
+
|
|
40
|
+
工作流仅通过 `workflow_dispatch` 触发。正式版本映射 `latest`;`alpha.N`、`beta.N`、`rc.N` 映射对应预发布标签。不会让预发布额外覆盖 `latest`。手动表单通过环境变量传给脚本,不直接拼接到 shell。
|
|
41
|
+
|
|
42
|
+
## 发布与失败核验
|
|
43
|
+
|
|
44
|
+
- 正式任务核对下载包 SHA512,使用 OIDC 执行带 provenance 的 `npm publish`,再只读核对 registry 版本、integrity 和 dist-tag。
|
|
45
|
+
- 包尚未初始化、查询失败或认证失败:停止,不回退到长期 Token,不借用其他仓库身份。
|
|
46
|
+
- 已有版本仅在 SHA512 与本次包一致时跳过上传;不同内容立即停止,不能覆盖。构建环境或文档变化也可能产生不同 tarball。
|
|
47
|
+
- 上传失败或结果未知不自动重发;先查询 registry。上传成功后版本可见性最多等待约 10 分钟,标签最多约 1 分钟;超时不能当作未发布。
|
|
48
|
+
- 不自动修正既有标签。若版本已存在但标签不匹配,应由维护者核实影响后单独处理。
|
|
49
|
+
- 测试或仅验证任务成功不表示发布成功。以正式发布任务结果和 registry 状态为准:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npm view @quecue/notify-core@0.2.0 version dist.integrity --json --registry=https://registry.npmjs.org/
|
|
53
|
+
npm view @quecue/notify-core dist-tags --json --registry=https://registry.npmjs.org/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
应用应声明正常的 npm 运行时依赖,如 `"@quecue/notify-core": "0.2.0"`,在版本可用后更新锁文件并回归;无需保存 vendor tarball、复制通讯运行时或检出兄弟仓库。
|
|
57
|
+
|
|
58
|
+
## 官方参考
|
|
59
|
+
|
|
60
|
+
- [Trusted Publishing](https://docs.npmjs.com/trusted-publishers/)
|
|
61
|
+
- [npm trust:包必须已存在的前置条件](https://docs.npmjs.com/cli/v11/commands/npm-trust/)
|
|
62
|
+
- [阶段发布及首次包占位行为](https://docs.npmjs.com/staged-publishing/)
|
|
63
|
+
- [npm publish](https://docs.npmjs.com/cli/v11/commands/npm-publish)
|
package/README.en.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# notify-core
|
|
2
|
+
|
|
3
|
+
English | [简体中文](README.md)
|
|
4
|
+
|
|
5
|
+
A small Node.js library for Telegram and Feishu: message planning, a Markdown rendering subset, Unicode-safe splitting, bounded JSON transport, and an injectable Telegram API. **It does not implement application-specific workflows.**
|
|
6
|
+
|
|
7
|
+
Originally extracted from [dev-notify](https://github.com/QueCue/dev-notify) and maintained independently at [QueCue/notify-core](https://github.com/QueCue/notify-core).
|
|
8
|
+
|
|
9
|
+
- ESM with TypeScript declarations; Node.js 22 or later.
|
|
10
|
+
- Webhook/Telegram transport uses built-in `fetch` and `crypto`; the Feishu client adds the official MIT-licensed `@larksuiteoapi/node-sdk`, pinned to `1.76.0` as a runtime dependency.
|
|
11
|
+
- Telegram sending and selected query/button APIs; Feishu custom-bot Webhook sending plus self-built app WebSocket message reception and HTTP sending/replies; **no card-action callbacks**.
|
|
12
|
+
- No credential discovery from environment variables, application configuration or keyrings, and no automatic polling or long connections. Importing and creating a Feishu client are offline; the SDK is lazy-loaded, and reception requires an explicit `start()`.
|
|
13
|
+
|
|
14
|
+
## Status and installation
|
|
15
|
+
|
|
16
|
+
MIT-licensed source; npm package name: `@quecue/notify-core`. The Feishu self-built app client below is available starting with `0.2.0`; see the [changelog](CHANGELOG.en.md). The registry is authoritative for available versions:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm view @quecue/notify-core version --registry=https://registry.npmjs.org/
|
|
20
|
+
npm install @quecue/notify-core@0.2.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Applications can declare it as a normal runtime dependency. npm installs it automatically, without a source checkout, vendor archive or embedded copy. Import the ESM API and declarations through `@quecue/notify-core`.
|
|
24
|
+
|
|
25
|
+
For source development or unpublished changes, build a local candidate package:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm ci --ignore-scripts
|
|
29
|
+
npm test
|
|
30
|
+
node -e "require('node:fs').mkdirSync('releases', {recursive: true})"
|
|
31
|
+
npm pack --pack-destination releases
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install the generated file in your own Node.js application, replacing the path:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install /path/to/quecue-notify-core-0.2.0.tgz
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
No other project needs to be checked out. Use the declared ESM entry; CommonJS `require()` and browser compatibility are not guaranteed.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { makePlan, deliver } from '@quecue/notify-core';
|
|
46
|
+
|
|
47
|
+
// The application reads and validates credentials; the library does not discover them.
|
|
48
|
+
const config = {
|
|
49
|
+
telegram: {
|
|
50
|
+
token: process.env.TELEGRAM_BOT_TOKEN ?? '',
|
|
51
|
+
chatIds: [process.env.TELEGRAM_CHAT_ID ?? ''],
|
|
52
|
+
},
|
|
53
|
+
feishu: {
|
|
54
|
+
webhook: process.env.FEISHU_WEBHOOK ?? '',
|
|
55
|
+
secret: process.env.FEISHU_SECRET ?? '',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const message = {
|
|
59
|
+
title: 'Build completed',
|
|
60
|
+
content: '**Result**: passed\n\n' + 'Full output\n'.repeat(1000),
|
|
61
|
+
footer: 'From CI',
|
|
62
|
+
level: 'success',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Planning is offline. Validate credentials and recipients before choosing to send.
|
|
66
|
+
const plan = makePlan('telegram', message, config);
|
|
67
|
+
console.log(`Planned ${plan.items.length} parts`);
|
|
68
|
+
// const result = await deliver(plan, config); // Uncommenting performs a real send.
|
|
69
|
+
// if (!result.ok) console.error(result.error);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For Feishu, use `makePlan('feishu', message, config)`; a non-empty `secret` enables custom-bot signing. Channel enablement, credential precedence and authorization belong to the application, not `NotifyConfig`. A non-empty string does not prove that a credential or destination is valid.
|
|
73
|
+
|
|
74
|
+
### Offline delivery example
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { makePlan, deliver } from '@quecue/notify-core';
|
|
78
|
+
const config = {
|
|
79
|
+
telegram: { token: '123:TEST_ONLY', chatIds: ['111'] },
|
|
80
|
+
feishu: { webhook: '', secret: '' },
|
|
81
|
+
};
|
|
82
|
+
const plan = makePlan('telegram', {
|
|
83
|
+
title: 'Offline example', content: 'No network', footer: '', level: 'info',
|
|
84
|
+
}, config);
|
|
85
|
+
const result = await deliver(plan, config, async () => ({
|
|
86
|
+
ok: true, result: { message_id: 1 },
|
|
87
|
+
}));
|
|
88
|
+
console.log(result.ok); // true; the injected transport performs no real request.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API and boundaries
|
|
92
|
+
|
|
93
|
+
| API | Behavior |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `createChannels(translate?)` | Per-instance message planner and delivery functions |
|
|
96
|
+
| `makePlan(channel, message, config)` | Offline planning, without credential or account validation |
|
|
97
|
+
| `deliver(plan, config, request?, sleep?)` | Sequential delivery returning `{channel,sent,planned,ok,error?}`; stops remaining parts for a failed recipient while continuing others |
|
|
98
|
+
| `createFormatting(translate?)` | Markdown subset parsing and splitting, not full CommonMark |
|
|
99
|
+
| `splitPlainText(text, limit = 4000)` | UTF-16-unit budget without splitting surrogate pairs; not grapheme-cluster or atomic-delivery protection |
|
|
100
|
+
| `createTransport(options?)` | Defaults to a 10-second timeout, 1 MiB response limit, no redirects, strict UTF-8 and JSON-object checks; supports an injected fetcher |
|
|
101
|
+
| `createTelegramApi(token, options?)` | Selected API methods, without a polling loop, offset persistence or authorization |
|
|
102
|
+
| `createFeishuClient({appId,appSecret}, options)` | Synchronously creates a self-built app client; explicit `start()` receives messages, while `sendMessage`/`replyMessage` use HTTP |
|
|
103
|
+
|
|
104
|
+
Markdown supports readable headings, quotations, lists, emphasis, code and tables. Link addresses remain visible. Telegram parse rejection can fall back to plain text; Feishu can retry using compatible card components. Metadata that cannot fit safely is rejected instead of silently truncated.
|
|
105
|
+
|
|
106
|
+
Optional `reply` metadata is `{token,url,users,callbackData?}`: an identifier, a Feishu link, recipients shown Telegram buttons, and optional callback data. The default callback data is `reply:` plus `token`. **This does not grant input permission**; the host must validate identities, callback length, expiry, routing and authorization.
|
|
107
|
+
|
|
108
|
+
### Transport and proxies
|
|
109
|
+
|
|
110
|
+
There is no built-in proxy URL setting. Inject an application-owned `request` or `fetcher` when required, preserving timeout, TLS, redirect and response-size protections. Default `fetch` proxy behavior depends on Node and startup configuration; unconditional direct connectivity is not guaranteed.
|
|
111
|
+
|
|
112
|
+
`createTransport({timeoutMs,maxResponseBytes,translate})` returns `(url,payload,fetcher?)`. This generic transport **does not restrict destination URLs**. Applications must validate schemes, domains and recipients rather than passing untrusted external URLs directly.
|
|
113
|
+
|
|
114
|
+
### Telegram methods
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { createTelegramApi, TelegramApiError } from '@quecue/notify-core';
|
|
118
|
+
const api = createTelegramApi(process.env.TELEGRAM_BOT_TOKEN ?? '', {
|
|
119
|
+
maxRateLimitWaitSeconds: 0, // Let the application schedule retries; errors remain visible.
|
|
120
|
+
});
|
|
121
|
+
// const bot = await api('getMe', {}); // An explicit call performs network I/O.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Allowed methods: `sendMessage`, `editMessageReplyMarkup`, `answerCallbackQuery`, `getMe`, `getUpdates`, `getWebhookInfo`. The library never deletes a Webhook. `getUpdates` and Webhooks are mutually exclusive; only one consumer may receive a Bot's updates. The application owns offsets, deduplication, backlogs and process coordination.
|
|
125
|
+
|
|
126
|
+
### Feishu self-built apps: WebSocket reception and HTTP replies
|
|
127
|
+
|
|
128
|
+
This client is separate from existing Webhook `makePlan`/`deliver`; `NotifyConfig` is unchanged. **Only self-built apps support long connections; custom-bot Webhook URLs and secrets cannot receive events.** WebSocket receives only `im.message.receive_v1` message events; sends/replies use HTTP APIs. Card-action callbacks are unsupported, and the host does not need a public callback endpoint.
|
|
129
|
+
|
|
130
|
+
Configure the app in the Feishu developer console first:
|
|
131
|
+
|
|
132
|
+
1. Create a self-built app and enable its bot capability.
|
|
133
|
+
2. Select long-connection mode under events and callbacks, and subscribe to `im.message.receive_v1`.
|
|
134
|
+
3. Request `im:message:send_as_bot` for sending, and as needed `im:message.p2p_msg:readonly` for private messages and `im:message.group_at_msg:readonly` for group mentions. The platform is authoritative for permissions, approvals and visibility.
|
|
135
|
+
4. Publish an app version and ensure the user account is within its availability scope. The runtime needs outbound access to Feishu services.
|
|
136
|
+
|
|
137
|
+
Minimal receive-and-reply example (host-selected user only; production also needs deduplication and full authorization):
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { createFeishuClient } from '@quecue/notify-core';
|
|
141
|
+
|
|
142
|
+
// The host still reads credentials and the allowlist; never hard-code real values.
|
|
143
|
+
const allowedOpenId = process.env.FEISHU_ALLOWED_OPEN_ID ?? '';
|
|
144
|
+
const client = createFeishuClient({
|
|
145
|
+
appId: process.env.FEISHU_APP_ID ?? '',
|
|
146
|
+
appSecret: process.env.FEISHU_APP_SECRET ?? '',
|
|
147
|
+
}, {
|
|
148
|
+
onMessage: async (event) => {
|
|
149
|
+
// Avoid bot self-replies and check the sender before executing any business action.
|
|
150
|
+
if (event.sender.sender_type !== 'user' || !allowedOpenId ||
|
|
151
|
+
event.sender.sender_id?.open_id !== allowedOpenId) return;
|
|
152
|
+
if (event.message.message_type !== 'text') return;
|
|
153
|
+
const content = JSON.parse(event.message.content);
|
|
154
|
+
if (typeof content.text !== 'string') return;
|
|
155
|
+
await client.replyMessage(event.message.message_id, {
|
|
156
|
+
msgType: 'text',
|
|
157
|
+
content: JSON.stringify({ text: 'Received your text message' }),
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
onError: (error) => console.error('Feishu operation failed', error.operation),
|
|
161
|
+
startTimeoutMs: 10_000,
|
|
162
|
+
}); // Synchronous, offline creation.
|
|
163
|
+
|
|
164
|
+
process.once('SIGINT', () => client.stop());
|
|
165
|
+
await client.start(); // Explicit network access; resolves only when WebSocket is ready.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`onMessage` receives the SDK-shaped `FeishuMessageEvent`, not a wrapper with an extra `event` layer: `event.event_id?`, `event.sender.sender_id.open_id?`, `event.sender.sender_type`, and `event.message` fields `message_id`, `chat_id`, `chat_type`, `message_type`, `content`. `content` is a JSON string suitable for `JSON.parse`; do not assume non-text messages have a `text` field.
|
|
169
|
+
|
|
170
|
+
Client contract:
|
|
171
|
+
|
|
172
|
+
- `createFeishuClient({appId,appSecret}, {onMessage, onError?, startTimeoutMs?, adapterFactory?})` returns synchronously. `adapterFactory` supports offline tests or a custom SDK adapter; custom implementations own their network and redaction boundaries.
|
|
173
|
+
- `await client.start()` waits for actual WS readiness, with a default `10000` ms timeout. Timeout or fatal errors reject the Promise; repeated `start()` is idempotent. `client.state` is `idle`, `connecting`, `connected`, `reconnecting`, `failed` or `stopped`.
|
|
174
|
+
- `client.stop()` synchronously closes the connection and disables reconnection, canceling a pending `start()` by rejecting its Promise; the client can be started again. It **does not wait for or cancel** callbacks or HTTP requests already in progress.
|
|
175
|
+
- `onMessage` is awaited and must finish within **3 seconds**. Failure is safely reported to `onError`, then a safe error is thrown so the SDK returns a failure ACK. Throws/rejections from `onError` are isolated without logging the original error. The 10-second HTTP timeout is not the callback time budget.
|
|
176
|
+
- The host owns `event_id`/`message_id` deduplication, sender/chat authorization, bot self-reply prevention and a durable queue for slow work. Disconnects/redelivery provide no exactly-once guarantee; callback retries can repeat business effects. The example does not implement durable deduplication. Multiple long-connected clients for one app receive in cluster mode, not broadcast mode.
|
|
177
|
+
|
|
178
|
+
Sending does not require `start()` and makes an HTTP request directly:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
const sent = await client.sendMessage({
|
|
182
|
+
receiveId: 'oc_EXAMPLE', // Replace with a host-validated recipient.
|
|
183
|
+
receiveIdType: 'chat_id', // Optional; defaults to chat_id.
|
|
184
|
+
msgType: 'text',
|
|
185
|
+
content: JSON.stringify({ text: 'Proactive message' }),
|
|
186
|
+
// uuid: 'host-provided-request-id',
|
|
187
|
+
});
|
|
188
|
+
// sent: { messageId, chatId? }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`client.replyMessage(messageId, {msgType, content, replyInThread?, uuid?})` also returns `{messageId,chatId?}`; `content` is a JSON string for the selected message type. Neither method uses message planning or automatic splitting.
|
|
192
|
+
|
|
193
|
+
The official MIT-licensed `@larksuiteoapi/node-sdk@1.76.0` handles app tokens, the binary protocol, framing, heartbeats and reconnection; SDK logging is disabled by default. SDK HTTP is independently configured with a 10-second timeout, 1 MiB limit and no redirects. **WebSocket frames are not covered by `createTransport`'s 1 MiB JSON-response guarantee**, nor do they use its `request`/`fetcher` injection.
|
|
194
|
+
|
|
195
|
+
Reference: [official SDK WebSocket event reception](https://github.com/larksuite/node-sdk#receive-events-by-websocket-connection).
|
|
196
|
+
|
|
197
|
+
## Errors and delivery semantics
|
|
198
|
+
|
|
199
|
+
- Built-in transport-generated safe errors omit original URLs, response bodies and underlying network exception text. `NotifyError` is the base class for `TransportError`, `HttpError`, `TelegramApiError` and `FeishuError`. Custom translators, transports and callback-owned output are outside this guarantee and must be sanitized by the caller.
|
|
200
|
+
- `HttpError.status` describes an already-parsed HTTP failure. Invalid JSON or UTF-8 may first produce `TransportError`; not every HTTP 401 is guaranteed to become the same error type.
|
|
201
|
+
- `FeishuError extends NotifyError`: `operation` is `configuration`, `connection`, `stopped`, `send`, `reply` or `handler`; optional `code` is numeric. Generated errors omit SDK text. Handler failures are wrapped as safe errors rather than forwarding the original callback exception to the SDK or `onError`.
|
|
202
|
+
- `TelegramApiError` has `code`, `retryAfterSeconds` and `fatal`. `fatal` is true only for 401/404/409 and does not terminate the host process.
|
|
203
|
+
- `createTelegramApi` retries a 429 at most once within its default 60-second wait threshold. Set `maxRateLimitWaitSeconds: 0` to disable this internal wait. Other network failures are not automatically resent.
|
|
204
|
+
- `deliver` has separate handling for valid Telegram 429 waits up to 60 seconds, format fallback for the current part, and 250 ms spacing between successful Feishu parts. Combined fallback paths do not imply a two-request maximum for the whole operation.
|
|
205
|
+
- A timeout can occur after server acceptance: **no exactly-once delivery, durable offline queue, or multipart atomicity is promised**.
|
|
206
|
+
- `onSent` runs after reported server success. If mapping or the callback fails, `sent` may be positive while `ok` is false. Retrying the entire plan can duplicate messages.
|
|
207
|
+
|
|
208
|
+
Tests use simulated network responses, not exhaustive real-service, proxy or Node-version acceptance. Sharing this library does not share application credentials or permissions.
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
- [Security boundaries and reporting](SECURITY.en.md)
|
|
213
|
+
- [Contribution and local checks](CONTRIBUTING.md)
|
|
214
|
+
- [Release checklist](PUBLISHING.md)
|
|
215
|
+
- [Changes](CHANGELOG.en.md)
|
|
216
|
+
- [MIT license](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,3 +1,216 @@
|
|
|
1
|
-
#
|
|
1
|
+
# notify-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
简体中文 | [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
面向 Node.js 的轻量 Telegram/飞书通讯库:消息规划、Markdown 子集渲染、Unicode 安全分段、HTTP 边界和可注入的 Telegram API。**不绑定具体业务或应用。**
|
|
6
|
+
|
|
7
|
+
代码最初提取自 [dev-notify](https://github.com/QueCue/dev-notify),在 [QueCue/notify-core](https://github.com/QueCue/notify-core) 独立维护。
|
|
8
|
+
|
|
9
|
+
- ESM,提供 TypeScript 类型,要求 Node.js 22 或更新版本。
|
|
10
|
+
- Webhook/Telegram 传输使用 Node 内置 `fetch` 和 `crypto`;飞书客户端新增官方 MIT 许可 SDK `@larksuiteoapi/node-sdk`,运行时依赖固定为 `1.76.0`。
|
|
11
|
+
- Telegram:发送消息,以及有限的查询/按钮 API;飞书:自定义机器人 Webhook 发送,以及自建应用 WebSocket 收消息、HTTP API 发送/回复;**不支持卡片动作回调**。
|
|
12
|
+
- 不读取环境变量或配置文件来取得凭据,不访问系统凭据库,不自动启动轮询或长连接。导入和创建飞书客户端均不联网,SDK 懒加载;显式 `start()` 才启动接收。
|
|
13
|
+
|
|
14
|
+
## 当前状态与安装
|
|
15
|
+
|
|
16
|
+
源码使用 MIT 许可证,npm 包名为 `@quecue/notify-core`。从 `0.2.0` 起提供下述飞书自建应用客户端,版本变化见[变更记录](CHANGELOG.md)。实际可用版本以 registry 为准:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm view @quecue/notify-core version --registry=https://registry.npmjs.org/
|
|
20
|
+
npm install @quecue/notify-core@0.2.0
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
应用可将它声明为普通运行时依赖,npm 会自动安装,不需要用户检出本仓库、复制 vendor 包或手工嵌入通讯源码。ESM/类型入口均通过 `@quecue/notify-core` 导入。
|
|
24
|
+
|
|
25
|
+
如需从源码构建或测试尚未发布的修改,也可生成本地候选包:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm ci --ignore-scripts
|
|
29
|
+
npm test
|
|
30
|
+
node -e "require('node:fs').mkdirSync('releases', {recursive: true})"
|
|
31
|
+
npm pack --pack-destination releases
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
在你的 Node.js 项目中安装生成的文件(将路径换为实际位置):
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install /path/to/quecue-notify-core-0.2.0.tgz
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`npm ci` 需要完整源码检出,包括锁文件;不需要同时检出其他项目。运行时只支持本包声明的 ESM 入口,不保证 `require()` 或浏览器环境兼容性。
|
|
41
|
+
|
|
42
|
+
## 快速开始:双渠道消息规划
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { makePlan, deliver } from '@quecue/notify-core';
|
|
46
|
+
|
|
47
|
+
// 环境变量由应用主动读取;库本身不发现、保存或验证你的凭据来源。
|
|
48
|
+
const config = {
|
|
49
|
+
telegram: {
|
|
50
|
+
token: process.env.TELEGRAM_BOT_TOKEN ?? '',
|
|
51
|
+
chatIds: [process.env.TELEGRAM_CHAT_ID ?? ''],
|
|
52
|
+
},
|
|
53
|
+
feishu: {
|
|
54
|
+
webhook: process.env.FEISHU_WEBHOOK ?? '',
|
|
55
|
+
secret: process.env.FEISHU_SECRET ?? '',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const message = {
|
|
59
|
+
title: '构建完成',
|
|
60
|
+
content: '**结果**:通过\n\n' + '完整输出\n'.repeat(1000),
|
|
61
|
+
footer: '来自 CI',
|
|
62
|
+
level: 'success',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// makePlan 只规划,不联网。由应用先校验渠道凭据和接收人,再明确发送。
|
|
66
|
+
const plan = makePlan('telegram', message, config);
|
|
67
|
+
console.log(`准备发送 ${plan.items.length} 段`);
|
|
68
|
+
// const result = await deliver(plan, config); // 取消注释会向 Telegram 真实发送。
|
|
69
|
+
// if (!result.ok) console.error(result.error);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
飞书改用 `makePlan('feishu', message, config)`;`secret` 非空时按飞书自定义机器人协议签名。`enabled`、默认渠道、环境变量优先级和接收人授权都是应用职责,不是 `NotifyConfig` 的字段。接口使用结构类型;不要把非空字符串误认为已经验证了 Token 或 Webhook。
|
|
73
|
+
|
|
74
|
+
### 离线模拟投递
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { makePlan, deliver } from '@quecue/notify-core';
|
|
78
|
+
const config = {
|
|
79
|
+
telegram: { token: '123:TEST_ONLY', chatIds: ['111'] },
|
|
80
|
+
feishu: { webhook: '', secret: '' },
|
|
81
|
+
};
|
|
82
|
+
const plan = makePlan('telegram', {
|
|
83
|
+
title: '离线示例', content: '不会联网', footer: '', level: 'info',
|
|
84
|
+
}, config);
|
|
85
|
+
const result = await deliver(plan, config, async () => ({
|
|
86
|
+
ok: true, result: { message_id: 1 },
|
|
87
|
+
}));
|
|
88
|
+
console.log(result.ok); // true;使用注入传输,没有真实发送。
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 核心接口
|
|
92
|
+
|
|
93
|
+
| 接口 | 用途与默认边界 |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `createChannels(translate?)` | 创建消息规划/投递实例;翻译只作用于该实例 |
|
|
96
|
+
| `makePlan(channel, message, config)` | 生成发送计划,不联网、不检查账号真实有效性 |
|
|
97
|
+
| `deliver(plan, config, request?, sleep?)` | 顺序投递,返回 `{channel,sent,planned,ok,error?}`;失败后停止该接收人的后续分段,其他接收人继续 |
|
|
98
|
+
| `createFormatting(translate?)` | 创建 Markdown 子集解析与分段函数;不是完整 CommonMark 实现 |
|
|
99
|
+
| `splitPlainText(text, limit = 4000)` | 按 UTF-16 单元预算分段,不切断代理对;保持正文,不保证不拆开组合字形或整条消息原子投递 |
|
|
100
|
+
| `createTransport(options?)` | 10 秒超时、1 MiB 响应上限、禁止重定向、严格 UTF-8/JSON 对象检查;返回可注入 `fetcher` 的请求函数 |
|
|
101
|
+
| `createTelegramApi(token, options?)` | 固定 Telegram 方法集合,不启动轮询、不持久化 offset、不提供身份授权 |
|
|
102
|
+
| `createFeishuClient({appId,appSecret}, options)` | 同步创建自建应用客户端;显式 `start()` 接收消息,`sendMessage`/`replyMessage` 通过 HTTP 发送 |
|
|
103
|
+
|
|
104
|
+
Markdown 支持标题、引用、列表、内联强调、代码和表格的可读渲染,链接地址保留在可见文字中。Telegram 解析失败可回退纯文本;飞书卡片组件被拒绝时可尝试兼容组件。过长标题/footer 等不能安全分段时明确失败,不悄悄截断。
|
|
105
|
+
|
|
106
|
+
`reply` 是可选展示信息:`{token,url,users,callbackData?}`,分别用于按钮标识、飞书跳转 URL、展示 Telegram 按钮的接收人列表及自定义回调数据。默认回调数据为 `reply:` 加 `token`。**这些字段不授予权限**;宿主仍须校验用户、回调长度、过期时间、会话路由和接收端身份。
|
|
107
|
+
|
|
108
|
+
### 传输与代理
|
|
109
|
+
|
|
110
|
+
公共库不内置代理地址配置。需要代理时,应用通过 `request` 或 `fetcher` 注入自己的 HTTP 客户端,并保留超时、TLS 校验、禁止重定向和响应大小限制。默认 `fetch` 的代理行为受 Node 运行时及启动配置影响;库不会承诺无条件直连。
|
|
111
|
+
|
|
112
|
+
`createTransport({timeoutMs,maxResponseBytes,translate})` 返回的函数签名为 `(url,payload,fetcher?)`。它是通用 JSON 传输,**不限制目标 URL**;应用必须验证目标域名、协议和接收人,不能直接接受外部不可信 URL。
|
|
113
|
+
|
|
114
|
+
### Telegram 查询与按钮
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { createTelegramApi, TelegramApiError } from '@quecue/notify-core';
|
|
118
|
+
const api = createTelegramApi(process.env.TELEGRAM_BOT_TOKEN ?? '', {
|
|
119
|
+
maxRateLimitWaitSeconds: 0, // 交由应用决定何时再请求;不会忽略限流错误。
|
|
120
|
+
});
|
|
121
|
+
// const bot = await api('getMe', {}); // 明确调用才联网。
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
允许的方法仅为 `sendMessage`、`editMessageReplyMarkup`、`answerCallbackQuery`、`getMe`、`getUpdates` 和 `getWebhookInfo`。不会删除现有 Webhook。`getUpdates` 与 Webhook 互斥,同一个 Bot 只能有一个更新消费者;offset、去重、积压消息、业务授权和进程互斥由应用管理。
|
|
125
|
+
|
|
126
|
+
### 飞书自建应用:长连接接收与 HTTP 回复
|
|
127
|
+
|
|
128
|
+
此入口独立于现有 Webhook `makePlan`/`deliver`,`NotifyConfig` 不变。**只有自建应用支持长连接;自定义机器人 Webhook 地址和 `secret` 不能用于接收。** WebSocket 仅接收 `im.message.receive_v1` 消息事件,发送/回复走 HTTP API,不支持卡片动作回调,也不需要宿主提供公网回调地址。
|
|
129
|
+
|
|
130
|
+
先在飞书开放平台完成配置:
|
|
131
|
+
|
|
132
|
+
1. 创建自建应用并启用机器人能力。
|
|
133
|
+
2. 在“事件与回调”选择长连接模式,订阅 `im.message.receive_v1`。
|
|
134
|
+
3. 申请发送权限 `im:message:send_as_bot`,按需申请私聊接收权限 `im:message.p2p_msg:readonly`、群聊 @机器人接收权限 `im:message.group_at_msg:readonly`;具体权限、审批及可见范围以平台为准。
|
|
135
|
+
4. 发布应用版本,并确保使用账号在应用可用范围内;运行环境需能访问飞书公网服务。
|
|
136
|
+
|
|
137
|
+
最小接收并回复示例(只允许宿主指定的用户,生产环境还需去重和完整授权):
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import { createFeishuClient } from '@quecue/notify-core';
|
|
141
|
+
|
|
142
|
+
// 凭据及授权名单仍由宿主读取;不要把真实值写进源码。
|
|
143
|
+
const allowedOpenId = process.env.FEISHU_ALLOWED_OPEN_ID ?? '';
|
|
144
|
+
const client = createFeishuClient({
|
|
145
|
+
appId: process.env.FEISHU_APP_ID ?? '',
|
|
146
|
+
appSecret: process.env.FEISHU_APP_SECRET ?? '',
|
|
147
|
+
}, {
|
|
148
|
+
onMessage: async (event) => {
|
|
149
|
+
// 防止机器人自回复,并验证发送者;不要对所有入站消息自动执行业务。
|
|
150
|
+
if (event.sender.sender_type !== 'user' || !allowedOpenId ||
|
|
151
|
+
event.sender.sender_id?.open_id !== allowedOpenId) return;
|
|
152
|
+
if (event.message.message_type !== 'text') return;
|
|
153
|
+
const content = JSON.parse(event.message.content);
|
|
154
|
+
if (typeof content.text !== 'string') return;
|
|
155
|
+
await client.replyMessage(event.message.message_id, {
|
|
156
|
+
msgType: 'text',
|
|
157
|
+
content: JSON.stringify({ text: '收到你的文本消息' }),
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
onError: (error) => console.error('飞书操作失败', error.operation),
|
|
161
|
+
startTimeoutMs: 10_000,
|
|
162
|
+
}); // 同步返回,不联网。
|
|
163
|
+
|
|
164
|
+
process.once('SIGINT', () => client.stop());
|
|
165
|
+
await client.start(); // 主动联网;实际 WebSocket ready 后才完成。
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`onMessage` 收到 SDK 原格式 `FeishuMessageEvent`,不是包含额外 `event` 层的包装:`event.event_id?`、`event.sender.sender_id.open_id?`、`event.sender.sender_type`,以及 `event.message` 的 `message_id`、`chat_id`、`chat_type`、`message_type`、`content`。`content` 是 JSON 字符串,可用 `JSON.parse`;非文本消息不要假设存在 `text` 字段。
|
|
169
|
+
|
|
170
|
+
客户端约定:
|
|
171
|
+
|
|
172
|
+
- `createFeishuClient({appId,appSecret}, {onMessage, onError?, startTimeoutMs?, adapterFactory?})` 同步返回客户端。`adapterFactory` 用于离线测试或自定义 SDK 适配器;自定义实现须自行维护网络和脱敏边界。
|
|
173
|
+
- `await client.start()` 等待实际 WS ready,默认超时 `10000` 毫秒;超时或致命错误使 Promise 拒绝,重复 `start()` 幂等。`client.state` 为 `idle`/`connecting`/`connected`/`reconnecting`/`failed`/`stopped`。
|
|
174
|
+
- `client.stop()` 同步关闭长连接并禁用重连,取消正在等待的 `start()`(其 Promise 拒绝);之后可再次 `start()`。它**不等待或取消**已执行中的消息回调或 HTTP 请求。
|
|
175
|
+
- `onMessage` 会被 await,必须在 **3 秒内结束**。失败会先安全报告给 `onError`,再抛安全错误,让 SDK 返回失败 ACK。`onError` 自身抛错或拒绝会被库隔离,不输出原日志。HTTP 请求的 10 秒超时不是消息回调的时间预算。
|
|
176
|
+
- 宿主负责按 `event_id`/`message_id` 去重、发送者/会话授权、防机器人自回复,并将耗时任务放入持久队列。断线与重推不保证恰好一次,重试回调可能重复业务副作用;上例未实现持久去重。相同应用的多个长连客户端按集群模式接收,不是广播。
|
|
177
|
+
|
|
178
|
+
主动发送不要求先 `start()`,会直接通过 HTTP 联网:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
const sent = await client.sendMessage({
|
|
182
|
+
receiveId: 'oc_EXAMPLE', // 换成宿主验证过的接收人。
|
|
183
|
+
receiveIdType: 'chat_id', // 可省略,默认 chat_id。
|
|
184
|
+
msgType: 'text',
|
|
185
|
+
content: JSON.stringify({ text: '主动发送' }),
|
|
186
|
+
// uuid: '由宿主提供的请求标识',
|
|
187
|
+
});
|
|
188
|
+
// sent: { messageId, chatId? }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`client.replyMessage(messageId, {msgType, content, replyInThread?, uuid?})` 同样返回 `{messageId,chatId?}`,`content` 使用相应消息类型的 JSON 字符串;这两个方法不经过消息规划或自动分段。
|
|
192
|
+
|
|
193
|
+
官方 MIT 许可 SDK `@larksuiteoapi/node-sdk@1.76.0` 负责应用 token、二进制协议、分帧、心跳和重连;默认不输出 SDK 日志。SDK HTTP 独立配置为 10 秒超时、1 MiB 上限、禁止重定向;**WebSocket 帧不适用 `createTransport` 的 1 MiB JSON 响应保证**,也不通过该传输的 `request`/`fetcher` 注入。
|
|
194
|
+
|
|
195
|
+
参考:[官方 SDK 长连接接收事件](https://github.com/larksuite/node-sdk#receive-events-by-websocket-connection)。
|
|
196
|
+
|
|
197
|
+
## 错误、重试与投递语义
|
|
198
|
+
|
|
199
|
+
- 内置传输不把底层 URL、响应原文或网络异常原文放进它生成的安全错误。`NotifyError` 是公共基类,`TransportError`、`HttpError`、`TelegramApiError` 和 `FeishuError` 可按类型处理。自定义翻译/传输及业务回调自身的输出不在此保证内,调用方须自行脱敏。
|
|
200
|
+
- `HttpError.status` 是已解析响应的 HTTP 状态;无效 JSON/UTF-8 等可能先表现为 `TransportError`。不要假设所有 HTTP 401 都必然映射为某一种错误。
|
|
201
|
+
- `FeishuError extends NotifyError`:`operation` 区分 `configuration`/`connection`/`stopped`/`send`/`reply`/`handler`,`code?` 为数值;生成的错误不包含 SDK 原文。消息处理失败会被包装为安全错误,不会把业务回调原异常交给 SDK 或 `onError`。
|
|
202
|
+
- `TelegramApiError` 提供 `code`、`retryAfterSeconds`、`fatal`;`fatal` 只对 401/404/409 为真,是供宿主参考的标记,不会自动退出进程。
|
|
203
|
+
- `createTelegramApi` 默认对一次 429 进行至多一次等待重试,等待阈值为 60 秒;`maxRateLimitWaitSeconds: 0` 关闭内层等待。其余网络失败不自动重发。
|
|
204
|
+
- `deliver` 有自己的策略:Telegram 明确 429 且有效 `retry_after` 在 60 秒内时有限重试;格式兼容回退只处理当前分段;飞书成功分段间等待 250 毫秒。上述回退叠加时不能理解为整个调用只会发生两次 HTTP 请求。
|
|
205
|
+
- 服务端可能已接受消息,但响应丢失,投递结果会未知;**不保证恰好一次、离线补发或整个长消息的原子性**。
|
|
206
|
+
- `onSent` 在服务端报告成功后调用。若映射/回调失败,`sent` 仍可能大于零而 `ok` 为 false;不要因此重发整条计划,否则可能重复通知。
|
|
207
|
+
|
|
208
|
+
## 开发、版本与安全
|
|
209
|
+
|
|
210
|
+
当前自动化测试使用模拟网络,不代表已完成全部 Node 版本、代理环境或真实平台验收。复用库源码不代表共享应用的凭据或系统权限。
|
|
211
|
+
|
|
212
|
+
- [安全边界与报告方式](SECURITY.md)
|
|
213
|
+
- [贡献与本地验证](CONTRIBUTING.md)
|
|
214
|
+
- [发布前检查与版本包更新](PUBLISHING.md)
|
|
215
|
+
- [变更记录](CHANGELOG.md)
|
|
216
|
+
- [MIT 许可证](LICENSE)
|