@quecue/dev-notify 0.2.0 → 0.2.2
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 +20 -0
- package/CHANGELOG.md +20 -0
- package/README.en.md +37 -23
- package/README.md +37 -23
- package/SECURITY.en.md +14 -6
- package/SECURITY.md +14 -6
- package/dist/channels.js +10 -157
- package/dist/cli.js +2 -2
- package/dist/config.js +2 -2
- package/dist/formatting.js +4 -211
- package/dist/reply-bridge.js +4 -2
- package/dist/reply-state.js +16 -1
- package/dist/reply-telegram.js +12 -6
- package/package.json +4 -3
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](CHANGELOG.md)
|
|
4
4
|
|
|
5
|
+
## 0.2.2 — 2026-11-07
|
|
6
|
+
|
|
7
|
+
Communication dependency extraction, Windows file-replacement fixes, and documentation updates. Actual version availability is authoritative on the npm registry; the changes below are not included in `0.2.1`.
|
|
8
|
+
|
|
9
|
+
- Extract Telegram/Feishu communication into the standard npm runtime dependency `@quecue/notify-core@0.1.0`, with source at [QueCue/notify-core](https://github.com/QueCue/notify-core). Do not embed the library or link sibling source; npm users do not install this dependency manually. The CLI entry point, languages, and reply button protocol remain compatible. The library's `0.1.0` has been published through GitHub Actions, and this application's lockfile now pins the official registry source and matching SHA512.
|
|
10
|
+
- When a Windows state file is temporarily busy, retry only the same atomic replacement without repeating message or input operations.
|
|
11
|
+
- Update Chinese and English installation, dependency, credential cleanup, and security boundary documentation to distinguish published packages from unreleased source.
|
|
12
|
+
- Share package-file verification between local maintainer checks and CI, covering application runtime, the standard communication dependency declaration and public documentation; remove the obsolete alpha default from the manual publishing form.
|
|
13
|
+
- Regression results for this source revision: 219 tests, 218 passed, 0 failed, and 1 opt-in native credential test skipped. These results do not establish real-machine acceptance on each platform.
|
|
14
|
+
|
|
15
|
+
## 0.2.1 — 2026-11-04
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed mixed Chinese and English Telegram messages after switching the interface language: the reply bridge is a long-running process and previously read the language only once at startup; it now follows the configured language on every polling cycle, so subsequent messages immediately use the new language after `dev-notify language`. An already running bridge must be restarted once (`reply stop` / `reply start`) to load the fix.
|
|
20
|
+
|
|
21
|
+
### Verification
|
|
22
|
+
|
|
23
|
+
- Automated regression totals 204 items: 203 passed, 0 failed, and 1 native credential test requiring explicit enablement was skipped.
|
|
24
|
+
|
|
5
25
|
## 0.2.0 — 2026-11-04
|
|
6
26
|
|
|
7
27
|
### Added
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
简体中文 | [English](CHANGELOG.en.md)
|
|
4
4
|
|
|
5
|
+
## 0.2.2 — 2026-11-07
|
|
6
|
+
|
|
7
|
+
通讯层依赖提取、Windows 文件替换修复与文档更新。实际版本可用性以 npm registry 为准;以下变更不包含在 `0.2.1` 中。
|
|
8
|
+
|
|
9
|
+
- 将 Telegram/飞书通讯层提取为标准 npm 运行时依赖 `@quecue/notify-core@0.1.0`,源码见 [QueCue/notify-core](https://github.com/QueCue/notify-core)。不嵌入通讯库、不链接兄弟仓库源码,npm 用户无需手动安装该依赖。CLI 入口、语言及回复按钮协议保持兼容。通讯库 `0.1.0` 已通过 GitHub Actions 正式发布,本应用锁文件已切换为正式 registry 来源及对应 SHA512。
|
|
10
|
+
- Windows 状态文件被暂态占用时只重试同一次原子替换,不重做消息或输入操作。
|
|
11
|
+
- 更新中英文安装、依赖、凭据清理及安全边界说明,区分已发布安装包和未发布源码。
|
|
12
|
+
- 统一维护者本地与 CI 的发布文件清单校验,覆盖应用运行时、标准通讯依赖声明及公开文档;移除手动发布表单中过时的 alpha 默认标签。
|
|
13
|
+
- 当前源码回归 219 项:218 通过、0 失败、1 项显式原生凭据测试跳过。此结果不代表各平台的实机验收。
|
|
14
|
+
|
|
15
|
+
## 0.2.1 — 2026-11-04
|
|
16
|
+
|
|
17
|
+
### 修复
|
|
18
|
+
|
|
19
|
+
- 修复切换界面语言后 Telegram 消息中英混用的问题:回复桥接是长驻进程,之前只在启动时读取一次语言;现在桥接每轮跟随配置语言,`dev-notify language` 切换后后续消息立即使用新语言。已在运行的桥接需重启一次(`reply stop` / `reply start`)以加载修复。
|
|
20
|
+
|
|
21
|
+
### 验证
|
|
22
|
+
|
|
23
|
+
- 自动化回归共 204 项:203 通过、0 失败、1 项需要显式启用的原生凭据测试跳过。
|
|
24
|
+
|
|
5
25
|
## 0.2.0 — 2026-11-04
|
|
6
26
|
|
|
7
27
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.md)
|
|
4
4
|
|
|
5
|
-
Cross-platform developer task notification tool written in **TypeScript / Node.js** and distributed via npm, with no standalone executable or other script runtime required. General notifications do not depend on Claude Code
|
|
5
|
+
Cross-platform developer task notification tool written in **TypeScript / Node.js** and distributed via npm, with no standalone executable or other script runtime required. General notifications do not depend on Claude Code or a project directory.
|
|
6
6
|
|
|
7
|
-
**The npm package name is `@quecue/dev-notify
|
|
7
|
+
**The npm package name is `@quecue/dev-notify`, with general notifications, Claude Code remote replies, and local/Telegram two-sided question answering and result feedback.** Source repository: [QueCue/dev-notify](https://github.com/QueCue/dev-notify). For changes and upgrade notes, see [CHANGELOG.en.md](CHANGELOG.en.md); published versions and distribution tags are authoritative on the npm registry, and source changes do not constitute a release.
|
|
8
|
+
|
|
9
|
+
### Communication dependency in 0.2.2 source
|
|
10
|
+
|
|
11
|
+
The current 0.2.2 source extracts Telegram/Feishu communication into the standard npm runtime dependency `@quecue/notify-core`, pinned to exactly `0.1.0`. Source: [QueCue/notify-core](https://github.com/QueCue/notify-core). This structure does not describe the published `0.2.1` package.
|
|
12
|
+
|
|
13
|
+
npm installs the dependency; builds compile only this application without copying the communication library or linking sibling source. npm users do not install the library manually. `@quecue/notify-core@0.1.0` has been published through GitHub Actions. The lockfile pins its registry source and integrity; use `npm ci --ignore-scripts` for source development.
|
|
8
14
|
|
|
9
15
|
## Installation and usage
|
|
10
16
|
|
|
@@ -13,6 +19,7 @@ Requires Node.js 22 or later; initial targets are Windows and macOS. Runtime dep
|
|
|
13
19
|
Install from npm:
|
|
14
20
|
|
|
15
21
|
```sh
|
|
22
|
+
npm view @quecue/dev-notify dist-tags
|
|
16
23
|
npm install -g @quecue/dev-notify
|
|
17
24
|
dev-notify --version
|
|
18
25
|
dev-notify setup
|
|
@@ -20,27 +27,22 @@ dev-notify setup
|
|
|
20
27
|
|
|
21
28
|
You can also override credentials and recipients with environment variables: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` / `TELEGRAM_CHAT_IDS`, `FEISHU_WEBHOOK`, `FEISHU_SECRET`; and path variables `DEV_NOTIFY_HOME`, `CLAUDE_CONFIG_DIR`. Explicitly present variables take precedence, and empty values also override; see "Paths, templates, and environment variables" below for complete rules and custom variable names.
|
|
22
29
|
|
|
23
|
-
`
|
|
30
|
+
The default installation uses npm's `latest` tag; `alpha` is a prerelease tag and does not imply a newer version than the stable release. Use the `npm view` command above to check tags and `dev-notify --version` to check the installed version. See the changelog for upgrade notes; after upgrading, update the complete hooks and restart any existing reply bridge. Valid authorizations and notification credentials do not need to be reconfigured; keep passing the original `--config` path for custom configurations.
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
npm ci --ignore-scripts
|
|
27
|
-
npm run build
|
|
28
|
-
node dist/cli.js setup
|
|
29
|
-
node dist/cli.js doctor
|
|
30
|
-
```
|
|
32
|
+
The main instructions below use the npm-installed `dev-notify` command; see "Development and verification" for source builds and `node dist/cli.js` usage.
|
|
31
33
|
|
|
32
34
|
After configuration is complete, send:
|
|
33
35
|
|
|
34
36
|
```sh
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
dev-notify send --title "Build complete" --content "Installer generated"
|
|
38
|
+
dev-notify send --title "Test results" --content-file result.txt
|
|
39
|
+
dev-notify send --title "Task output" --stdin
|
|
40
|
+
dev-notify send --title "Segment preview" --content-file result.txt --dry-run
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
To install locally as the `dev-notify` command, run `npm install --global .` after building, then use `dev-notify setup`, `dev-notify send ...`, and so on. This is a local source install, not an install from the public registry; this project does not automatically install globally or publish.
|
|
42
44
|
|
|
43
|
-
npm handles installation, upgrades, and uninstallation of the official package. **Clearing credentials before uninstalling is recommended**: npm uninstall does not automatically delete user credentials. First run `dev-notify credentials clear` interactively, then uninstall. That command deletes only the Node credential referenced by the current configuration
|
|
45
|
+
npm handles installation, upgrades, and uninstallation of the official package. **Clearing credentials before uninstalling is recommended**: npm uninstall does not automatically delete user credentials. First run `dev-notify credentials clear` interactively, then uninstall. That command deletes only the Node credential referenced by the current configuration; it does not scan other configurations, delete unrelated entries, or handle old-prototype credentials. However, configuration copies sharing the same credential reference will also lose access to that credential.
|
|
44
46
|
|
|
45
47
|
Installed versions can be upgraded with `update`; it only checks the version and invokes npm, and does not download or replace files itself:
|
|
46
48
|
|
|
@@ -73,7 +75,7 @@ Actual integrity verification, registry configuration, permission handling, and
|
|
|
73
75
|
## Configuration wizard
|
|
74
76
|
|
|
75
77
|
```sh
|
|
76
|
-
|
|
78
|
+
dev-notify setup
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
1. Choose Telegram, Feishu, or both; when editing an existing configuration, the current selection is shown and pressing Enter keeps it.
|
|
@@ -84,12 +86,12 @@ node dist/cli.js setup
|
|
|
84
86
|
6. After confirmation, Claude Code notification hooks are merged and installed by default (`--no-hooks` skips it). Existing integrations are left as-is and are not added again.
|
|
85
87
|
7. Remote reply authorization is not requested in setup; when Telegram is available, it prompts you to run `reply setup` separately to choose specific trusted private-chat users and confirm separately.
|
|
86
88
|
|
|
87
|
-
Editing an existing configuration no longer asks a second time whether to continue; leaving a secret blank preserves the stored value, and entering `-` for an optional signature clears it. Channels that are not selected are disabled, and new entries retain credentials only for the selected channels. Environment variables still take precedence (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` / `TELEGRAM_CHAT_IDS`, `FEISHU_WEBHOOK`, `FEISHU_SECRET`); the wizard indicates which variables are overriding configuration
|
|
89
|
+
Editing an existing configuration no longer asks a second time whether to continue; leaving a secret blank preserves the stored value, and entering `-` for an optional signature clears it. Channels that are not selected are disabled, and new entries retain credentials only for the selected channels. Environment variables still take precedence (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` / `TELEGRAM_CHAT_IDS`, `FEISHU_WEBHOOK`, `FEISHU_SECRET`); the wizard indicates which variables are overriding configuration. Tests and subsequent runtime operations use the effective values after environment overrides, but persistence saves the wizard-entered credentials and selected recipients; environment overrides are not automatically written back to configuration or system credentials. See the complete list in "Paths, templates, and environment variables".
|
|
88
90
|
|
|
89
91
|
For automation, use the secret-free template and CI secrets:
|
|
90
92
|
|
|
91
93
|
```sh
|
|
92
|
-
|
|
94
|
+
dev-notify setup --template --yes
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
To save configuration only without sending a test or installing hooks, use `setup --no-test` / `setup --no-hooks` (they can be combined). The template does not overwrite existing configuration. `--yes` can only be used with `--template`; interactive confirmation is still required before saving secrets.
|
|
@@ -187,7 +189,7 @@ The Node version uses **Windows Credential Manager / macOS Keychain**, passing s
|
|
|
187
189
|
- When native component loading fails, storage is locked, or access is denied, it reports an explicit error and does not fall back to a plaintext file. macOS may request system authorization; system prompts may still appear in offline environments.
|
|
188
190
|
- By default it does not enumerate system entries and accesses only the current reference or a random entry created in this run.
|
|
189
191
|
|
|
190
|
-
The old prototype used `backend: "system"`, which on Windows is a DPAPI file and not the same store as Node Credential Manager. Node does not read or delete it. For old configurations, run `setup` to re-enter and generate a Node reference; the original credentials remain unchanged.
|
|
192
|
+
The old prototype used `backend: "system"`, which on Windows is a DPAPI file and not the same store as Node Credential Manager. Node does not read or delete it. For old configurations, run `setup` to re-enter and generate a Node reference; the original credentials remain unchanged. This compatibility restriction does not affect configurations already using `keyring` references.
|
|
191
193
|
|
|
192
194
|
## Paths, templates, and environment variables
|
|
193
195
|
|
|
@@ -269,7 +271,7 @@ Save flow: write a new random system entry → verify readability → atomically
|
|
|
269
271
|
Configuration writes use an exclusive `.setup.lock`, and the original file is checked before writing and before switching. Avoid concurrently editing configuration with an editor while the wizard is saving; editors do not honor this lock. Power loss or force-kill may leave locks or orphaned entries, and storage and JSON are not cross-system atomic transactions. Only after confirming that no wizard is running should residual locks be handled; the program does not proactively scan or delete unknown entries.
|
|
270
272
|
|
|
271
273
|
```sh
|
|
272
|
-
|
|
274
|
+
dev-notify credentials clear
|
|
273
275
|
```
|
|
274
276
|
|
|
275
277
|
Cleanup requires interactive confirmation; it first removes the current JSON reference, then attempts to delete its Node entry; environment variables remain unchanged. Configuration copies sharing the same old reference are also affected, and this is communicated before the operation. Deletion failure is clearly warned and is not shown as cleaned.
|
|
@@ -292,20 +294,32 @@ If one recipient fails midway, its subsequent segments stop, while Telegram cont
|
|
|
292
294
|
| 2 | Argument, configuration, file, or credential operation error |
|
|
293
295
|
| 130 | Canceled by the user |
|
|
294
296
|
|
|
295
|
-
If the test notification fails after the wizard saves successfully, the command still ends as a successful save and clearly shows the test failure; the two are clearly distinguished. Canceling hook configuration after saving still returns 0 and keeps channels; a hook configuration error returns 1 and clearly states that channels were saved. Integration `status` returns 1 when entries are missing, duplicated, or hooks are disabled in the current user-level file. Internal hook
|
|
297
|
+
If the test notification fails after the wizard saves successfully, the command still ends as a successful save and clearly shows the test failure; the two are clearly distinguished. Canceling hook configuration after saving still returns 0 and keeps channels; a hook configuration error returns 1 and clearly states that channels were saved. Integration `status` returns 1 when entries are missing, duplicated, or hooks are disabled in the current user-level file. Internal hook failures are reported via stderr with a non-blocking exit status for the host. This does not mean asynchronous or immediate completion: synchronous hooks awaiting remote answers still wait within their documented timeout.
|
|
296
298
|
|
|
297
299
|
## Development and verification
|
|
298
300
|
|
|
301
|
+
Clone the [GitHub source repository](https://github.com/QueCue/dev-notify) and run these commands from its directory. npm restores pinned dependencies from the lockfile; no communication-library checkout is required:
|
|
302
|
+
|
|
299
303
|
```sh
|
|
300
304
|
npm ci --ignore-scripts
|
|
305
|
+
npm run build
|
|
306
|
+
node dist/cli.js setup
|
|
307
|
+
node dist/cli.js doctor
|
|
308
|
+
node dist/cli.js send --title "Build complete" --content "Installer generated"
|
|
301
309
|
npm test
|
|
302
310
|
npm pack --dry-run
|
|
303
311
|
```
|
|
304
312
|
|
|
313
|
+
For other CLI examples, replace `dev-notify` with `node dist/cli.js`; no global installation is required.
|
|
314
|
+
|
|
305
315
|
`node-src/` is TypeScript source, `dist/` is compiled output, and `test-js/` is tests. Default tests use only fake credentials and mock backends/network; the local native fake-entry round-trip test requires explicitly setting `DEV_NOTIFY_NATIVE_TEST=1`, is enabled only on Windows, uses random IDs, and cleans up afterward.
|
|
306
316
|
|
|
307
|
-
|
|
317
|
+
For the current 0.2.2 source, the package whitelist includes only this application's compiled CLI JavaScript, README, SECURITY, CHANGELOG, LICENSE and package.json. Library copies, tests, real configuration, ciphertext and dependency tarballs are excluded. npm resolves runtime dependencies separately. Published package contents depend on the corresponding version. There is no postinstall, auto-start, or self-written download script; prebuilt native dependencies are resolved by npm's platform optional packages, **do not use `--omit=optional`**. macOS x64/arm64 and Windows x64/arm64 prebuilt packages are provided by dependencies; providing packages does not mean this project has completed real-machine acceptance.
|
|
318
|
+
|
|
319
|
+
See [CHANGELOG.en.md](CHANGELOG.en.md) for verification results of published versions. The regression record for this 0.2.2 source on Windows x64 with Node 24 is 219 tests: 218 passed, 0 failed, and 1 opt-in native credential test skipped. This record does not describe verification of the published npm package.
|
|
320
|
+
|
|
321
|
+
Automated coverage includes temporary hooks, reply/authorization/expiry/anti-replay, single-select/multi-select/custom input, one-time permission JSON, two-sided races, feedback exceptions, and per-recipient duplicate reminder suppression. The actual CLI subprocess completes the loop of background start → PreToolUse registration → PermissionRequest → fake bot button → JSON answer → simulated PostToolUse → final feedback → notification deduplication/subsequent tool reminder → stop; this loop uses mock network and process metadata and cannot replace real-machine input verification. Automated tests for the Windows native input helper class only verify compilation/layout and do not perform a real Send.
|
|
308
322
|
|
|
309
|
-
|
|
323
|
+
Real-machine acceptance is separate from automated results: question-answering acceptance on Windows native Claude Code 2.1.316 with real Telegram covers local-first, remote-first, multi-select, custom input, competing different answers, and delayed general reminder deduplication. Those question-answering tests do not verify terminal input injection for ordinary replies. Both macOS terminal adapters have only simulation verification; macOS real-machine testing, the minimum Node version, version manager switching, and tool permission answering on a real machine have not yet been accepted. This project has not completed a security audit.
|
|
310
324
|
|
|
311
|
-
Licensed under MIT, see [LICENSE](LICENSE). Security boundaries are in [SECURITY.en.md](SECURITY.en.md).
|
|
325
|
+
Licensed under MIT, see [LICENSE](LICENSE). Security boundaries are in [SECURITY.en.md](SECURITY.en.md). Source changes reach installed users only after a new npm release. Ordinary branch pushes do not publish; matching `v*` version tags trigger the repository's publishing workflow. Maintainers should see the [npm publishing guide](https://github.com/QueCue/dev-notify/blob/main/.github/NPM_PUBLISH.en.md).
|
package/README.md
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
简体中文 | [English](README.en.md)
|
|
4
4
|
|
|
5
|
-
跨平台开发任务通知工具,使用 **TypeScript / Node.js**,通过 npm 分发,不需要独立可执行文件或其他脚本运行时。通用通知不依赖 Claude Code
|
|
5
|
+
跨平台开发任务通知工具,使用 **TypeScript / Node.js**,通过 npm 分发,不需要独立可执行文件或其他脚本运行时。通用通知不依赖 Claude Code 或项目目录。
|
|
6
6
|
|
|
7
|
-
**npm 包名为 `@quecue/dev-notify
|
|
7
|
+
**npm 包名为 `@quecue/dev-notify`,支持通用通知、Claude Code 远程回复,以及本机与 Telegram 双端答题和结果反馈。** 源码仓库:[QueCue/dev-notify](https://github.com/QueCue/dev-notify)。版本变更与升级说明见 [CHANGELOG.md](CHANGELOG.md);公开版本和发布标签以 npm registry 为准,源码更新不等同于已发布。
|
|
8
|
+
|
|
9
|
+
### 0.2.2 源码的通讯层依赖
|
|
10
|
+
|
|
11
|
+
当前 0.2.2 源码将 Telegram/飞书通讯层提取为标准 npm 运行时依赖 `@quecue/notify-core`,精确版本为 `0.1.0`,源码见 [QueCue/notify-core](https://github.com/QueCue/notify-core)。此结构不代表已发布的 `0.2.1` 包。
|
|
12
|
+
|
|
13
|
+
依赖由 npm 安装,构建只编译本应用,不复制通讯库或链接兄弟仓库源码。npm 用户无需手动安装通讯库。`@quecue/notify-core@0.1.0` 已通过 GitHub Actions 发布,锁文件固定其 registry 来源和 integrity;源码开发使用 `npm ci --ignore-scripts` 还原依赖。
|
|
8
14
|
|
|
9
15
|
## 安装与使用
|
|
10
16
|
|
|
@@ -13,6 +19,7 @@
|
|
|
13
19
|
从 npm 安装:
|
|
14
20
|
|
|
15
21
|
```sh
|
|
22
|
+
npm view @quecue/dev-notify dist-tags
|
|
16
23
|
npm install -g @quecue/dev-notify
|
|
17
24
|
dev-notify --version
|
|
18
25
|
dev-notify setup
|
|
@@ -20,27 +27,22 @@ dev-notify setup
|
|
|
20
27
|
|
|
21
28
|
也可用环境变量覆盖凭据和接收人:`TELEGRAM_BOT_TOKEN`、`TELEGRAM_CHAT_ID` / `TELEGRAM_CHAT_IDS`、`FEISHU_WEBHOOK`、`FEISHU_SECRET`;路径变量 `DEV_NOTIFY_HOME`、`CLAUDE_CONFIG_DIR`。显式存在的变量优先,空值也会覆盖;完整规则和自定义变量名见下文「路径、模板和环境变量」。
|
|
22
29
|
|
|
23
|
-
`
|
|
30
|
+
默认安装使用 npm 的 `latest` 标签;`alpha` 是预发行标签,不代表比稳定版更新。可用上面的 `npm view` 命令核对标签,使用 `dev-notify --version` 核对已安装版本。升级说明见 changelog;升级后应更新完整 hooks 并重启已有回复桥接。有效授权和通知凭据无需重新配置,自定义配置继续传入原来的 `--config` 路径。
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
npm ci --ignore-scripts
|
|
27
|
-
npm run build
|
|
28
|
-
node dist/cli.js setup
|
|
29
|
-
node dist/cli.js doctor
|
|
30
|
-
```
|
|
32
|
+
以下主流程使用 npm 安装的 `dev-notify` 命令;源码构建与 `node dist/cli.js` 用法见「开发与验证」。
|
|
31
33
|
|
|
32
34
|
配置完成后发送:
|
|
33
35
|
|
|
34
36
|
```sh
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
dev-notify send --title "构建完成" --content "安装包已生成"
|
|
38
|
+
dev-notify send --title "测试结果" --content-file result.txt
|
|
39
|
+
dev-notify send --title "任务输出" --stdin
|
|
40
|
+
dev-notify send --title "分段预览" --content-file result.txt --dry-run
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
若需要本地安装为 `dev-notify` 命令,在构建后运行 `npm install --global .`,之后可执行 `dev-notify setup`、`dev-notify send ...`。这是本地源码安装,不是从公开 registry 安装;本工程不会自动进行全局安装或发布。
|
|
42
44
|
|
|
43
|
-
正式包的安装、升级和卸载由 npm 负责。**卸载前建议先清理凭据**:npm 卸载不会自动删除用户凭据。请先交互执行 `dev-notify credentials clear`,再卸载。该命令只删除当前配置引用的 Node
|
|
45
|
+
正式包的安装、升级和卸载由 npm 负责。**卸载前建议先清理凭据**:npm 卸载不会自动删除用户凭据。请先交互执行 `dev-notify credentials clear`,再卸载。该命令只删除当前配置引用的 Node 凭据,不扫描其他配置或删除无关条目,也不处理旧版原型凭据;但共享同一凭据引用的配置副本也会失去该凭据。
|
|
44
46
|
|
|
45
47
|
已安装的版本可以用 `update` 升级;它只负责检查版本并调用 npm,不会自己下载或替换文件:
|
|
46
48
|
|
|
@@ -73,7 +75,7 @@ dev-notify update --no-post # 只更新包,不执行 hooks/桥接维护
|
|
|
73
75
|
## 配置向导
|
|
74
76
|
|
|
75
77
|
```sh
|
|
76
|
-
|
|
78
|
+
dev-notify setup
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
1. 选择 Telegram、飞书或两者;编辑已有配置时显示当前选择,直接回车保留。
|
|
@@ -84,12 +86,12 @@ node dist/cli.js setup
|
|
|
84
86
|
6. 确认后默认合并安装 Claude Code 通知 hooks(`--no-hooks` 跳过)。已有集成保持原样,不重复添加。
|
|
85
87
|
7. 远程回复授权不在 setup 中询问;Telegram 可用时提示另行运行 `reply setup`,选择具体可信私聊用户并单独确认。
|
|
86
88
|
|
|
87
|
-
编辑已有配置不再二次询问是否继续;密钥留空保留已存值,可选签名输入 `-` 清除。没有选中的渠道会被禁用,新条目只保留所选渠道的凭据。环境变量仍然优先(`TELEGRAM_BOT_TOKEN`、`TELEGRAM_CHAT_ID` / `TELEGRAM_CHAT_IDS`、`FEISHU_WEBHOOK`、`FEISHU_SECRET
|
|
89
|
+
编辑已有配置不再二次询问是否继续;密钥留空保留已存值,可选签名输入 `-` 清除。没有选中的渠道会被禁用,新条目只保留所选渠道的凭据。环境变量仍然优先(`TELEGRAM_BOT_TOKEN`、`TELEGRAM_CHAT_ID` / `TELEGRAM_CHAT_IDS`、`FEISHU_WEBHOOK`、`FEISHU_SECRET`);向导会提示哪些变量正在覆盖配置;测试和后续运行使用环境覆盖后的有效值,但持久保存的是向导输入的凭据与选择的接收人,不会把环境覆盖值自动写回配置或系统凭据。完整列表见「路径、模板和环境变量」。
|
|
88
90
|
|
|
89
91
|
自动化使用无密钥模板和 CI secrets:
|
|
90
92
|
|
|
91
93
|
```sh
|
|
92
|
-
|
|
94
|
+
dev-notify setup --template --yes
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
需要只保存配置、不发送测试或不安装 hooks 时,使用 `setup --no-test` / `setup --no-hooks`(可组合)。模板不会覆盖已有配置。`--yes` 只能配合 `--template` 使用;保存密钥前仍然需要交互确认。
|
|
@@ -187,7 +189,7 @@ Node 版使用 **Windows Credential Manager / macOS Keychain**,通过原生 No
|
|
|
187
189
|
- 原生组件加载失败、存储锁定或拒绝访问时会明确报错,不会降级为明文文件。macOS 可能要求系统授权;离线环境仍可能出现系统提示。
|
|
188
190
|
- 默认不枚举系统条目,只访问当前引用或本次新建的随机条目。
|
|
189
191
|
|
|
190
|
-
旧版原型使用 `backend: "system"`,Windows 上是 DPAPI 文件,与 Node Credential Manager 不是同一存储。Node 不会读取或删除它。遇到旧配置时,可以运行 `setup` 重新输入并生成 Node
|
|
192
|
+
旧版原型使用 `backend: "system"`,Windows 上是 DPAPI 文件,与 Node Credential Manager 不是同一存储。Node 不会读取或删除它。遇到旧配置时,可以运行 `setup` 重新输入并生成 Node 引用,原凭据保持不变。此兼容性限制不影响已使用 `keyring` 引用的配置。
|
|
191
193
|
|
|
192
194
|
## 路径、模板和环境变量
|
|
193
195
|
|
|
@@ -269,7 +271,7 @@ Node 版使用 **Windows Credential Manager / macOS Keychain**,通过原生 No
|
|
|
269
271
|
配置写入使用排他 `.setup.lock`,写入前及切换前都会核对原文件。请在向导保存期间避免用编辑器并发修改配置;编辑器不遵守这个锁。断电或强杀可能留下锁或孤立条目,存储和 JSON 也不是跨系统原子事务。确认没有向导运行后,才能处理残留锁;程序不会主动扫描或删除未知条目。
|
|
270
272
|
|
|
271
273
|
```sh
|
|
272
|
-
|
|
274
|
+
dev-notify credentials clear
|
|
273
275
|
```
|
|
274
276
|
|
|
275
277
|
清理必须交互确认,先去除当前 JSON 引用,再尝试删除其 Node 条目;环境变量保持不变。共享同一旧引用的配置副本也会受影响,操作前会告知。删除失败会明确警告,不会显示为已清理。
|
|
@@ -292,20 +294,32 @@ node dist/cli.js credentials clear
|
|
|
292
294
|
| 2 | 参数、配置、文件或凭据操作错误 |
|
|
293
295
|
| 130 | 用户取消 |
|
|
294
296
|
|
|
295
|
-
向导保存成功后测试通知失败,命令仍以保存成功结束,并明确显示测试失败,两者会清楚区分。保存后取消 hook 配置仍返回 0 并保留渠道;hook 配置出错返回 1,明确提示渠道已保存。集成 `status` 在条目缺失、重复或当前用户级文件禁用 hooks 时返回 1。内部 hook
|
|
297
|
+
向导保存成功后测试通知失败,命令仍以保存成功结束,并明确显示测试失败,两者会清楚区分。保存后取消 hook 配置仍返回 0 并保留渠道;hook 配置出错返回 1,明确提示渠道已保存。集成 `status` 在条目缺失、重复或当前用户级文件禁用 hooks 时返回 1。内部 hook 处理失败通过 stderr 报告,并采用不阻止宿主的退出码;这不代表异步或立即返回,等待远程答案的同步 hook 仍会在约定超时内等待。
|
|
296
298
|
|
|
297
299
|
## 开发与验证
|
|
298
300
|
|
|
301
|
+
从 [GitHub 源码仓库](https://github.com/QueCue/dev-notify)克隆后,在仓库目录执行,npm 将按锁文件安装固定版本依赖,无需检出通讯库仓库:
|
|
302
|
+
|
|
299
303
|
```sh
|
|
300
304
|
npm ci --ignore-scripts
|
|
305
|
+
npm run build
|
|
306
|
+
node dist/cli.js setup
|
|
307
|
+
node dist/cli.js doctor
|
|
308
|
+
node dist/cli.js send --title "构建完成" --content "安装包已生成"
|
|
301
309
|
npm test
|
|
302
310
|
npm pack --dry-run
|
|
303
311
|
```
|
|
304
312
|
|
|
313
|
+
其他 CLI 示例中的 `dev-notify` 也可替换为 `node dist/cli.js`,无须全局安装。
|
|
314
|
+
|
|
305
315
|
`node-src/` 是 TypeScript 源码,`dist/` 是编译输出,`test-js/` 是测试。默认测试只用假凭据和模拟后端/网络;本机原生假条目往返测试需显式设置 `DEV_NOTIFY_NATIVE_TEST=1`,且只在 Windows 启用,使用随机 ID,结束后清理。
|
|
306
316
|
|
|
307
|
-
npm
|
|
317
|
+
当前 0.2.2 源码的 npm 白名单仅包含本应用 CLI 编译 JS、README、SECURITY、CHANGELOG、LICENSE 和 package.json;不包含通讯库副本、测试、真实配置、密文或依赖 tarball。运行时依赖由 npm 单独解析。已发布版本的包内容以对应版本为准。没有 postinstall、自动启动或自写下载脚本;预编译原生依赖由 npm 的平台 optional 包解析,**请勿使用 `--omit=optional`**。macOS x64/arm64 和 Windows x64/arm64 预编译包由依赖提供,提供包并不等同于本项目已完成实机验收。
|
|
318
|
+
|
|
319
|
+
各已发布版本的验证结果见 [CHANGELOG.md](CHANGELOG.md)。当前 0.2.2 源码在 Windows x64、Node 24 下的回归记录为 219 项:218 通过、0 失败、1 项需显式启用的原生凭据测试跳过;该记录不代表已发布 npm 包的验证结果。
|
|
320
|
+
|
|
321
|
+
自动化覆盖临时 hooks、回复/授权/过期/防重放、单选/多选/自定义、单次权限 JSON、双端竞争、反馈异常及按接收人的重复提醒去重。实际 CLI 子进程完成后台 start → PreToolUse 登记 → PermissionRequest → 假 Bot 按钮 → JSON 答案 → 模拟 PostToolUse → 最终反馈 → 通知去重/后续工具提醒 → stop;该闭环使用模拟网络和进程元数据,不能替代实机输入验证。Windows 原生输入帮助类的自动化测试只做编译/布局验证,不会执行真实 Send。
|
|
308
322
|
|
|
309
|
-
|
|
323
|
+
实机验收与自动化结果分别记录:Windows 原生 Claude Code 2.1.316 与真实 Telegram 的问答验收覆盖本机优先、远端优先、多选、自定义、不同答案竞争及延迟通用提醒去重;这些问答测试不验证普通回复的终端输入注入。macOS 两条终端适配器只有模拟验证;macOS 实机、Node 最低版本、版本管理器切换及工具权限应答实机尚未完成验收,本项目也尚未完成安全审计。
|
|
310
324
|
|
|
311
|
-
采用 MIT,见 [LICENSE](LICENSE)。安全边界见 [SECURITY.md](SECURITY.md)
|
|
325
|
+
采用 MIT,见 [LICENSE](LICENSE)。安全边界见 [SECURITY.md](SECURITY.md)。源码变更只有发布新 npm 版本后才提供给安装用户;普通分支推送不发布,符合版本规则的 `v*` 标签会触发仓库的发布工作流。维护者流程见 [npm 发布说明](https://github.com/QueCue/dev-notify/blob/main/.github/NPM_PUBLISH.md)。
|
package/SECURITY.en.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](SECURITY.md)
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The npm package name is `@quecue/dev-notify`, with public source at [QueCue/dev-notify](https://github.com/QueCue/dev-notify). Published versions are authoritative on the npm registry; this document describes the 0.2.2 source, and a source version alone does not establish that an installation package has been published. This project has not completed a security audit or real-machine acceptance on both platforms.
|
|
6
|
+
|
|
7
|
+
## Communication dependency in 0.2.2 source
|
|
8
|
+
|
|
9
|
+
The current 0.2.2 source uses the standard npm runtime dependency `@quecue/notify-core`, pinned to exactly `0.1.0`, with source at [QueCue/notify-core](https://github.com/QueCue/notify-core). Builds do not embed the library or link sibling source; npm installs it automatically. This does not describe the published `0.2.1` package. The library's `0.1.0` release was published through its own GitHub Actions OIDC identity; the lockfile pins the official registry source and SHA512. Neither provenance nor integrity verification constitutes a security audit.
|
|
10
|
+
|
|
11
|
+
Reusing a dependency does not automatically share credentials or authorization, and a bot's `getUpdates` must still have only one consumer. Upgrade dependencies only from trusted sources; tarball integrity checks in the lockfile do not establish source authenticity or constitute a security audit.
|
|
6
12
|
|
|
7
13
|
## Credentials and permissions
|
|
8
14
|
|
|
@@ -66,13 +72,15 @@ The current source is `0.1.3` and the npm package name is `@quecue/dev-notify`;
|
|
|
66
72
|
## npm and testing
|
|
67
73
|
|
|
68
74
|
- Runtime has a native credential dependency, with the version and platform optional packages pinned in `package-lock.json`. Keep optional dependencies. There is no postinstall, self-written binary download, or background startup script.
|
|
69
|
-
-
|
|
70
|
-
- Third-party native packages carry their own licenses and Rust dependency declarations
|
|
75
|
+
- For the current 0.2.2 source, the npm whitelist includes only this application's compiled CLI JS, README, SECURITY, CHANGELOG, LICENSE and package.json. Library copies, tests, real configuration, logs, ciphertext and dependency tarballs are excluded. Published versions are governed by their actual package contents.
|
|
76
|
+
- Third-party native packages carry their own licenses and Rust dependency declarations. Check the supply chain and licenses for releases and dependency upgrades; pinned versions are not equivalent to a completed audit.
|
|
71
77
|
- Default tests use fake credentials, temporary directories, and mock system backends/network. `DEV_NOTIFY_NATIVE_TEST=1` explicitly enables Windows native tests, creates only fake entries with random IDs for this run, cleans up afterward, and does not read the user's existing entries.
|
|
72
78
|
- npm uninstall does not automatically delete user configuration or credentials. To clean up, first run the interactive cleanup command; uninstall lifecycle scripts will not secretly manipulate the user's system storage.
|
|
73
79
|
|
|
74
|
-
##
|
|
80
|
+
## Release security and vulnerability reporting
|
|
81
|
+
|
|
82
|
+
The project is public; see [CHANGELOG.en.md](CHANGELOG.en.md) for release history and the npm registry for published versions. Releases and dependency upgrades still require checking the package name, source ownership, and dependency licenses, and checking source, actual npm packages, and Git history for secrets. Once a key leaks, revoke and rotate it; deleting files cannot undo a leak. This project has not completed a security audit.
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
There is currently no public security contact email. If GitHub private vulnerability reporting is enabled for the repository, submit privately through its [Security page](https://github.com/QueCue/dev-notify/security). Otherwise, use [Issues](https://github.com/QueCue/dev-notify/issues) only to ask for a private contact method, without publishing vulnerability details, credentials, or sensitive logs.
|
|
77
85
|
|
|
78
|
-
Windows x64 / Node 24 has had isolated tests and a real CLI fake-network loop; the Windows native input helper class in automation only
|
|
86
|
+
Windows x64 / Node 24 has had isolated tests and a real CLI fake-network loop; the Windows native input helper class in automation only verifies compilation/layout and does not inject into real Claude sessions. Question-answering acceptance on Claude Code 2.1.316 with real Telegram covers local/remote priority, single-select/multi-select/custom input, competing different answers, and duplicate reminder deduplication; that path does not involve terminal input injection. Tool permission answering on a real machine, macOS terminal input and the system permission flow, other architectures, and the minimum Node version have not been verified, so comprehensive compatibility is not claimed.
|
package/SECURITY.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
简体中文 | [English](SECURITY.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
npm 包名为 `@quecue/dev-notify`,公开源码见 [QueCue/dev-notify](https://github.com/QueCue/dev-notify)。发布版本以 npm registry 为准,本文描述 0.2.2 源码,不能仅依据源码版本认定安装包已发布。本项目尚未完成安全审计或双平台实机验收。
|
|
6
|
+
|
|
7
|
+
## 0.2.2 源码的通讯层依赖
|
|
8
|
+
|
|
9
|
+
当前 0.2.2 源码使用标准 npm 运行时依赖 `@quecue/notify-core`,精确版本 `0.1.0`,源码见 [QueCue/notify-core](https://github.com/QueCue/notify-core)。构建不嵌入通讯库、不链接兄弟仓库源码,npm 自动安装该依赖;这不是已发布 `0.2.1` 的结构说明。通讯库 `0.1.0` 已通过独立 GitHub Actions OIDC 发行,锁文件固定正式 registry 来源和 SHA512;包的 provenance 或完整性校验均不等于安全审计。
|
|
10
|
+
|
|
11
|
+
依赖复用不会自动共享凭据或授权,同一个 Bot 的 `getUpdates` 仍只能由一个消费者使用。依赖只从可信来源升级;锁文件的 tarball integrity 校验不等于来源认证或安全审计。
|
|
6
12
|
|
|
7
13
|
## 凭据与权限
|
|
8
14
|
|
|
@@ -66,13 +72,15 @@
|
|
|
66
72
|
## npm 与测试
|
|
67
73
|
|
|
68
74
|
- 运行时有原生凭据依赖,版本和平台 optional 包锁定于 `package-lock.json`。请保留 optional 依赖。项目没有 postinstall、自写二进制下载或后台启动脚本。
|
|
69
|
-
- npm
|
|
70
|
-
- 第三方原生包带有其许可证与 Rust
|
|
75
|
+
- 当前 0.2.2 源码的 npm 白名单仅包含本应用 CLI 编译 JS、README、SECURITY、CHANGELOG、LICENSE 和 package.json。通讯库副本、测试、真实配置、日志、密文及依赖 tarball 不进入发布包;已发布版本以实际包内容为准。
|
|
76
|
+
- 第三方原生包带有其许可证与 Rust 依赖声明。发布及依赖升级时应核对供应链和许可;固定版本并不等同于完成审计。
|
|
71
77
|
- 默认测试使用假凭据、临时目录和模拟系统后端、网络。`DEV_NOTIFY_NATIVE_TEST=1` 会显式开启 Windows 原生测试,只创建本次随机 ID 的假条目,结束后清理,不读取用户现有条目。
|
|
72
78
|
- npm 卸载不会自动删除用户配置或凭据。需要清理时,请先运行交互清理命令;卸载生命周期脚本不会偷偷操作用户的系统存储。
|
|
73
79
|
|
|
74
|
-
##
|
|
80
|
+
## 发布安全与漏洞报告
|
|
81
|
+
|
|
82
|
+
项目已公开,发布记录见 [CHANGELOG.md](CHANGELOG.md),公开版本以 npm registry 为准。发布及依赖升级仍需核对包名、源码权属和依赖许可,检查源码、实际 npm 包与 Git 历史是否包含秘密。密钥一旦泄漏,必须撤销并轮换;删除文件无法撤销已经发生的泄漏。本项目尚未完成安全审计。
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
目前没有公开安全联络邮箱。如仓库已启用 GitHub 私密漏洞报告,请通过 [Security 页面](https://github.com/QueCue/dev-notify/security)私密提交;否则可先在 [Issues](https://github.com/QueCue/dev-notify/issues) 中仅询问私密联系方式,不要公开漏洞细节、凭据或敏感日志。
|
|
77
85
|
|
|
78
|
-
Windows x64 / Node 24 已做隔离测试及真实 CLI 假网络闭环;自动化中的 Windows 原生输入帮助类只做编译/布局验证,不会向真实 Claude
|
|
86
|
+
Windows x64 / Node 24 已做隔离测试及真实 CLI 假网络闭环;自动化中的 Windows 原生输入帮助类只做编译/布局验证,不会向真实 Claude 会话注入。Claude Code 2.1.316 与真实 Telegram 的问答实机验收覆盖本机/远端优先、单选/多选/自定义、不同答案竞争及重复提醒去重;该问答路径不涉及终端输入注入。工具权限应答实机、macOS 终端输入与系统权限流程、其他架构及 Node 最低版本尚未验证,因此不宣称全面兼容。
|
package/dist/channels.js
CHANGED
|
@@ -1,160 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { ConfigError } from './config.js';
|
|
1
|
+
// 兼容原 CLI 的导出与语言设置,通讯实现由公共包唯一维护。
|
|
2
|
+
import { createChannels } from '@quecue/notify-core';
|
|
4
3
|
import { t } from './i18n.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const colors = { info: 'blue', success: 'green', warn: 'yellow', error: 'red' };
|
|
10
|
-
const bytes = (data) => Buffer.byteLength(JSON.stringify(data), 'utf8');
|
|
11
|
-
export async function requestJson(url, payload, fetcher = fetch) {
|
|
12
|
-
try {
|
|
13
|
-
const response = await fetcher(url, { method: 'POST', body: JSON.stringify(payload),
|
|
14
|
-
headers: { 'Content-Type': 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(10000) });
|
|
15
|
-
if (!response.body)
|
|
16
|
-
throw new TransportError(t('channels.noResponse'));
|
|
17
|
-
const reader = response.body.getReader();
|
|
18
|
-
const chunks = [];
|
|
19
|
-
let size = 0;
|
|
20
|
-
try {
|
|
21
|
-
while (true) {
|
|
22
|
-
const { done, value } = await reader.read();
|
|
23
|
-
if (done)
|
|
24
|
-
break;
|
|
25
|
-
size += value.byteLength;
|
|
26
|
-
if (size > 1024 * 1024) {
|
|
27
|
-
await reader.cancel();
|
|
28
|
-
throw new TransportError(t('channels.responseTooLarge'));
|
|
29
|
-
}
|
|
30
|
-
chunks.push(value);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
reader.releaseLock();
|
|
35
|
-
}
|
|
36
|
-
const result = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
37
|
-
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
38
|
-
throw new TransportError(t('channels.notJsonObject'));
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (error instanceof TransportError)
|
|
43
|
-
throw error;
|
|
44
|
-
throw new TransportError(t('channels.network'));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function text(title, content, footer, icon, html) {
|
|
48
|
-
const sections = [`${icon} ${html ? `<b>${escapeHtml(title)}</b>` : title}`];
|
|
49
|
-
if (content)
|
|
50
|
-
sections.push(content);
|
|
51
|
-
if (footer)
|
|
52
|
-
sections.push('────────\n' + (html ? `<i>${escapeHtml(footer)}</i>` : footer));
|
|
53
|
-
return sections.join('\n\n');
|
|
54
|
-
}
|
|
55
|
-
function card(message, title, content) {
|
|
56
|
-
const elements = [];
|
|
57
|
-
if (content)
|
|
58
|
-
elements.push({ tag: 'markdown', content });
|
|
59
|
-
if (message.footer)
|
|
60
|
-
elements.push({ tag: 'hr' }, { tag: 'note', elements: [{ tag: 'plain_text', content: message.footer }] });
|
|
61
|
-
if (message.reply?.url)
|
|
62
|
-
elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: t('channels.feishuReplyButton') }, url: message.reply.url, type: 'primary' }] });
|
|
63
|
-
return { msg_type: 'interactive', card: { config: { wide_screen_mode: true },
|
|
64
|
-
header: { template: colors[message.level], title: { tag: 'plain_text', content: title } }, elements } };
|
|
65
|
-
}
|
|
4
|
+
export { TransportError } from '@quecue/notify-core';
|
|
5
|
+
const channels = createChannels(key => t(key));
|
|
6
|
+
export const requestJson = channels.requestJson;
|
|
7
|
+
export const deliver = channels.deliver;
|
|
66
8
|
export function makePlan(channel, message, config) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const icon = icons[message.level];
|
|
72
|
-
const reserve = String(Math.max(1, message.content.length)).length * 2 + 16;
|
|
73
|
-
const budget = 3900 - text(message.title, '', message.footer, icon, false).length - reserve;
|
|
74
|
-
const chunks = telegramParts(blocks, budget);
|
|
75
|
-
const items = [];
|
|
76
|
-
for (const chatId of config.telegram.chatIds)
|
|
77
|
-
chunks.forEach((chunk, index) => {
|
|
78
|
-
const title = partTitle(message.title, index, chunks.length);
|
|
79
|
-
const plain = text(title, chunk.plain, message.footer, icon, false);
|
|
80
|
-
if (plain.length > 4096)
|
|
81
|
-
throw new Error(t('channels.telegramTooLong'));
|
|
82
|
-
items.push({ recipient: chatId, plain, payload: { chat_id: chatId,
|
|
83
|
-
text: text(title, chunk.html, message.footer, icon, true), parse_mode: 'HTML', disable_web_page_preview: true,
|
|
84
|
-
...(message.reply?.users.includes(chatId) ? { reply_markup: { inline_keyboard: [[{ text: t('channels.telegramReplyButton'), callback_data: 'dn:r:' + message.reply.token }]] } } : {}) } });
|
|
85
|
-
});
|
|
86
|
-
return { channel, items, recipients: config.telegram.chatIds.length };
|
|
87
|
-
}
|
|
88
|
-
const budget = Math.min(10000, Math.floor((28000 - bytes(card(message, message.title, '')) - 512) / 6));
|
|
89
|
-
const chunks = feishuParts(blocks, budget);
|
|
90
|
-
const items = chunks.map((chunk, index) => {
|
|
91
|
-
const payload = card(message, partTitle(message.title, index, chunks.length), chunk);
|
|
92
|
-
if (bytes(payload) + 128 > 28000)
|
|
93
|
-
throw new Error(t('channels.feishuTooLong'));
|
|
94
|
-
return { recipient: 'feishu', payload };
|
|
95
|
-
});
|
|
96
|
-
return { channel, items, recipients: 1 };
|
|
97
|
-
}
|
|
98
|
-
export async function deliver(plan, config, request = requestJson, sleep = delay) {
|
|
99
|
-
let sent = 0, error;
|
|
100
|
-
const failed = new Set();
|
|
101
|
-
const telegram = plan.channel === 'telegram';
|
|
102
|
-
const url = telegram ? `https://api.telegram.org/bot${config.telegram.token}/sendMessage` : config.feishu.webhook;
|
|
103
|
-
const post = async (payload) => {
|
|
104
|
-
if (!telegram && config.feishu.secret) {
|
|
105
|
-
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
106
|
-
payload.timestamp = timestamp;
|
|
107
|
-
payload.sign = createHmac('sha256', `${timestamp}\n${config.feishu.secret}`).update('').digest('base64');
|
|
108
|
-
}
|
|
109
|
-
let response = await request(url, payload);
|
|
110
|
-
if (telegram && response.error_code === 429) {
|
|
111
|
-
const wait = response.parameters?.retry_after;
|
|
112
|
-
if (typeof wait === 'number' && Number.isFinite(wait) && wait > 0 && wait <= 60) {
|
|
113
|
-
await sleep(wait * 1000);
|
|
114
|
-
response = await request(url, payload);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return response;
|
|
118
|
-
};
|
|
119
|
-
for (let index = 0; index < plan.items.length; index++) {
|
|
120
|
-
const item = plan.items[index];
|
|
121
|
-
if (failed.has(item.recipient))
|
|
122
|
-
continue;
|
|
123
|
-
const payload = structuredClone(item.payload);
|
|
124
|
-
try {
|
|
125
|
-
let response = await post(payload);
|
|
126
|
-
if (telegram && response.ok !== true && /parse|entit/i.test(String(response.description ?? ''))) {
|
|
127
|
-
delete payload.parse_mode;
|
|
128
|
-
payload.text = item.plain;
|
|
129
|
-
response = await post(payload);
|
|
130
|
-
}
|
|
131
|
-
else if (!telegram && response.code !== 0) {
|
|
132
|
-
let changed = false;
|
|
133
|
-
payload.card.elements = payload.card.elements.map((element) => {
|
|
134
|
-
if (element.tag !== 'markdown')
|
|
135
|
-
return element;
|
|
136
|
-
changed = true;
|
|
137
|
-
return { tag: 'div', text: { tag: 'lark_md', content: element.content } };
|
|
138
|
-
});
|
|
139
|
-
if (changed)
|
|
140
|
-
response = await post(payload);
|
|
141
|
-
}
|
|
142
|
-
if (telegram ? response.ok === true : response.code === 0) {
|
|
143
|
-
sent++;
|
|
144
|
-
if (plan.onSent)
|
|
145
|
-
await plan.onSent(item, response);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
failed.add(item.recipient);
|
|
149
|
-
error = t('channels.rejected');
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch (cause) {
|
|
153
|
-
failed.add(item.recipient);
|
|
154
|
-
error = cause instanceof TransportError ? cause.message : t('channels.sendFailed');
|
|
155
|
-
}
|
|
156
|
-
if (!telegram && !failed.has(item.recipient) && index + 1 < plan.items.length)
|
|
157
|
-
await sleep(250);
|
|
158
|
-
}
|
|
159
|
-
return { channel: plan.channel, sent, planned: plan.items.length, ok: sent > 0 && sent === plan.items.length && !error, error };
|
|
9
|
+
// 开发会话的按钮前缀属于业务协议,不写死在通用通讯包中。
|
|
10
|
+
return channels.makePlan(channel, message.reply ? {
|
|
11
|
+
...message, reply: { ...message.reply, callbackData: message.reply.callbackData ?? 'dn:r:' + message.reply.token }
|
|
12
|
+
} : message, config);
|
|
160
13
|
}
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { startBridge, stopBridge, bridgeStatus, runBridge } from './reply-bridge
|
|
|
16
16
|
import { updatePackage } from './update.js';
|
|
17
17
|
import { postUpdate } from './maintenance.js';
|
|
18
18
|
import { localeName, parseLocale, resolveLocale, setLocale, t } from './i18n.js';
|
|
19
|
-
export const VERSION = '0.2.
|
|
19
|
+
export const VERSION = '0.2.2';
|
|
20
20
|
const helpText = () => t('cli.help', { version: VERSION });
|
|
21
21
|
function rawLang(argv) {
|
|
22
22
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -191,7 +191,7 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
|
|
|
191
191
|
return access && status.ready ? 0 : 1;
|
|
192
192
|
}
|
|
193
193
|
if (action === 'run') {
|
|
194
|
-
await runBridge(file, { env: context.env, log: context.err });
|
|
194
|
+
await runBridge(file, { env: context.env, lang: values.lang, log: context.err });
|
|
195
195
|
return 0;
|
|
196
196
|
}
|
|
197
197
|
const ready = await startBridge(file, context.env);
|
package/dist/config.js
CHANGED
|
@@ -3,8 +3,8 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveReference } from './credentials.js';
|
|
5
5
|
import { getLocale, t } from './i18n.js';
|
|
6
|
-
|
|
7
|
-
}
|
|
6
|
+
import { NotifyError as ConfigError } from '@quecue/notify-core';
|
|
7
|
+
export { ConfigError };
|
|
8
8
|
export const example = {
|
|
9
9
|
version: 1,
|
|
10
10
|
language: 'zh',
|
package/dist/formatting.js
CHANGED
|
@@ -1,212 +1,5 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 保留原有导出入口;格式化与分段算法由 notify-core 维护。
|
|
2
|
+
import { createFormatting } from '@quecue/notify-core';
|
|
2
3
|
import { t } from './i18n.js';
|
|
3
|
-
export
|
|
4
|
-
export const
|
|
5
|
-
function inline(text, tags = [], depth = 0) {
|
|
6
|
-
if (depth > 8)
|
|
7
|
-
return [{ text, tags }];
|
|
8
|
-
// 不在生成的 HTML 上执行 Markdown 正则,避免修改链接属性。
|
|
9
|
-
const expression = /`([^`\n]+)`|\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)|\*\*(\S(?:.*?\S)?)\*\*|__(\S(?:.*?\S)?)__|~~(\S(?:.*?\S)?)~~|(?<![\w*])\*([^*\n]+)\*(?![\w*])|(?<![\w_])_([^_\n]+)_(?![\w_])/g;
|
|
10
|
-
const spans = [];
|
|
11
|
-
let cursor = 0;
|
|
12
|
-
for (const match of text.matchAll(expression)) {
|
|
13
|
-
if (match.index > cursor)
|
|
14
|
-
spans.push({ text: text.slice(cursor, match.index), tags });
|
|
15
|
-
if (match[1] !== undefined)
|
|
16
|
-
spans.push({ text: match[1], tags: [...tags, 'code'] });
|
|
17
|
-
else if (match[2] !== undefined) {
|
|
18
|
-
// 链接地址同样保留在可见文本中;任何纯文本回退都不会丢失地址。
|
|
19
|
-
spans.push(...inline(match[2], tags, depth + 1), { text: ` (${match[3]})`, tags });
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
const content = match[4] ?? match[5] ?? match[6] ?? match[7] ?? match[8];
|
|
23
|
-
const tag = match[4] !== undefined || match[5] !== undefined ? 'b' : match[6] !== undefined ? 's' : 'i';
|
|
24
|
-
spans.push(...inline(content, [...tags, tag], depth + 1));
|
|
25
|
-
}
|
|
26
|
-
cursor = match.index + match[0].length;
|
|
27
|
-
}
|
|
28
|
-
if (cursor < text.length)
|
|
29
|
-
spans.push({ text: text.slice(cursor), tags });
|
|
30
|
-
return spans.length ? spans : [{ text: '', tags }];
|
|
31
|
-
}
|
|
32
|
-
function cells(line) {
|
|
33
|
-
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(cell => inline(cell.trim()).map(s => s.text).join(''));
|
|
34
|
-
}
|
|
35
|
-
function separator(line) {
|
|
36
|
-
return line.includes('|') && line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').every(cell => /^:?-{3,}:?$/.test(cell.trim()));
|
|
37
|
-
}
|
|
38
|
-
function width(text) {
|
|
39
|
-
let size = 0;
|
|
40
|
-
for (const ch of text) {
|
|
41
|
-
const code = ch.codePointAt(0);
|
|
42
|
-
size += code >= 0x1100 && (code <= 0x115f || code >= 0x2e80 && code <= 0xa4cf || code >= 0xac00 && code <= 0xd7af || code >= 0xff01 && code <= 0xff60 || code >= 0x1f300) ? 2 : 1;
|
|
43
|
-
}
|
|
44
|
-
return size;
|
|
45
|
-
}
|
|
46
|
-
function table(lines) {
|
|
47
|
-
const rows = lines.filter((_, index) => index !== 1).map(cells);
|
|
48
|
-
const count = Math.max(...rows.map(row => row.length));
|
|
49
|
-
const widths = Array.from({ length: count }, (_, col) => Math.max(3, ...rows.map(row => width(row[col] ?? ''))));
|
|
50
|
-
const output = [];
|
|
51
|
-
rows.forEach((row, index) => {
|
|
52
|
-
output.push(widths.map((size, col) => (row[col] ?? '') + ' '.repeat(size - width(row[col] ?? ''))).join(' | '));
|
|
53
|
-
if (!index)
|
|
54
|
-
output.push(widths.map(size => '-'.repeat(size)).join('-+-'));
|
|
55
|
-
});
|
|
56
|
-
return output.join('\n');
|
|
57
|
-
}
|
|
58
|
-
export function parseMarkdown(content) {
|
|
59
|
-
const lines = content.replace(/\r\n?/g, '\n').replace(/\0/g, '').split('\n');
|
|
60
|
-
const blocks = [];
|
|
61
|
-
let index = 0;
|
|
62
|
-
while (index < lines.length) {
|
|
63
|
-
const line = lines[index];
|
|
64
|
-
const fence = /^\s*(`{3,}|~{3,})(.*)$/.exec(line);
|
|
65
|
-
if (fence) {
|
|
66
|
-
const marker = fence[1][0];
|
|
67
|
-
const closing = new RegExp(`^\\s*${marker === '`' ? '`' : '~'}{${fence[1].length},}\\s*$`);
|
|
68
|
-
const body = [];
|
|
69
|
-
index++;
|
|
70
|
-
while (index < lines.length && !closing.test(lines[index]))
|
|
71
|
-
body.push(lines[index++]);
|
|
72
|
-
if (index < lines.length)
|
|
73
|
-
index++;
|
|
74
|
-
blocks.push({ spans: [{ text: body.join('\n'), tags: [] }], code: true });
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (index + 1 < lines.length && line.includes('|') && separator(lines[index + 1])) {
|
|
78
|
-
const rows = [line, lines[index + 1]];
|
|
79
|
-
index += 2;
|
|
80
|
-
while (index < lines.length && lines[index].trim() && lines[index].includes('|'))
|
|
81
|
-
rows.push(lines[index++]);
|
|
82
|
-
blocks.push({ spans: [{ text: table(rows), tags: [] }], code: true });
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const heading = /^\s{0,3}#{1,6}\s+(.*)$/.exec(line);
|
|
86
|
-
const quote = /^\s*>\s?(.*)$/.exec(line);
|
|
87
|
-
const list = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
88
|
-
let spans;
|
|
89
|
-
if (heading)
|
|
90
|
-
spans = inline(heading[1], ['b']);
|
|
91
|
-
else if (/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line))
|
|
92
|
-
spans = [{ text: '────────', tags: [] }];
|
|
93
|
-
else if (quote)
|
|
94
|
-
spans = inline(quote[1], ['blockquote']);
|
|
95
|
-
else if (list)
|
|
96
|
-
spans = inline(`${list[1]}• ${list[2].replace(/^\[ \] /, '☐ ').replace(/^\[x\] /i, '☑ ')}`);
|
|
97
|
-
else
|
|
98
|
-
spans = inline(line);
|
|
99
|
-
blocks.push({ spans, code: false });
|
|
100
|
-
index++;
|
|
101
|
-
}
|
|
102
|
-
return blocks;
|
|
103
|
-
}
|
|
104
|
-
export function visibleText(blocks) {
|
|
105
|
-
return blocks.map(block => block.spans.map(span => span.text).join('')).join('\n');
|
|
106
|
-
}
|
|
107
|
-
function cut(text, budget, measure) {
|
|
108
|
-
if (budget < 1)
|
|
109
|
-
throw new Error(t('format.noSpace'));
|
|
110
|
-
let used = 0, end = 0;
|
|
111
|
-
for (const ch of text) {
|
|
112
|
-
const size = measure(ch);
|
|
113
|
-
if (used + size > budget)
|
|
114
|
-
break;
|
|
115
|
-
used += size;
|
|
116
|
-
end += ch.length;
|
|
117
|
-
}
|
|
118
|
-
if (!end && text)
|
|
119
|
-
throw new Error(t('format.oneChar'));
|
|
120
|
-
if (end < text.length) {
|
|
121
|
-
for (const boundary of ['\n\n', '\n', ' ']) {
|
|
122
|
-
const offset = text.slice(0, end).lastIndexOf(boundary);
|
|
123
|
-
if (offset >= end / 2) {
|
|
124
|
-
end = offset + boundary.length;
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return [text.slice(0, end), text.slice(end)];
|
|
130
|
-
}
|
|
131
|
-
export function telegramParts(blocks, budget) {
|
|
132
|
-
if (budget < 2)
|
|
133
|
-
throw new Error(t('format.telegramHeader'));
|
|
134
|
-
const output = [];
|
|
135
|
-
let html = '', plain = '';
|
|
136
|
-
const flush = () => { if (plain)
|
|
137
|
-
output.push({ html, plain }); html = ''; plain = ''; };
|
|
138
|
-
blocks.forEach((block, index) => {
|
|
139
|
-
const spans = index ? [{ text: '\n', tags: [] }, ...block.spans] : block.spans;
|
|
140
|
-
for (const span of spans) {
|
|
141
|
-
let rest = span.text;
|
|
142
|
-
while (rest) {
|
|
143
|
-
if (budget - plain.length < [...rest][0].length)
|
|
144
|
-
flush();
|
|
145
|
-
const [piece, remaining] = cut(rest, budget - plain.length, text => text.length);
|
|
146
|
-
// 块间换行不包裹为代码,其余内容每片均使用完整标签。
|
|
147
|
-
const actualTags = block.code && !(index && span === spans[0]) ? ['pre'] : span.tags;
|
|
148
|
-
html += actualTags.map(tag => `<${tag}>`).join('') + escapeHtml(piece) + [...actualTags].reverse().map(tag => `</${tag}>`).join('');
|
|
149
|
-
plain += piece;
|
|
150
|
-
rest = remaining;
|
|
151
|
-
if (rest)
|
|
152
|
-
flush();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
flush();
|
|
157
|
-
return output.length ? output : [{ html: '', plain: '' }];
|
|
158
|
-
}
|
|
159
|
-
export function feishuParts(blocks, budget) {
|
|
160
|
-
if (budget < 128)
|
|
161
|
-
throw new Error(t('format.feishuHeader'));
|
|
162
|
-
const output = [];
|
|
163
|
-
let current = '';
|
|
164
|
-
const size = (text) => Buffer.byteLength(text, 'utf8');
|
|
165
|
-
const flush = () => { if (current)
|
|
166
|
-
output.push(current); current = ''; };
|
|
167
|
-
blocks.forEach((block, index) => {
|
|
168
|
-
if (index) {
|
|
169
|
-
if (size(current) + 1 > budget)
|
|
170
|
-
flush();
|
|
171
|
-
current += '\n';
|
|
172
|
-
}
|
|
173
|
-
for (const span of block.spans) {
|
|
174
|
-
let rest = span.text;
|
|
175
|
-
let prefix = '', suffix = '';
|
|
176
|
-
if (block.code) {
|
|
177
|
-
const runs = rest.match(/`+/g) ?? [];
|
|
178
|
-
const marker = '`'.repeat(Math.max(3, ...runs.map(run => run.length + 1)));
|
|
179
|
-
prefix = marker + 'text\n';
|
|
180
|
-
suffix = '\n' + marker;
|
|
181
|
-
// 极端围栏长度下保留字面正文,不无限增大包装。
|
|
182
|
-
if (size(prefix + suffix) > budget / 2) {
|
|
183
|
-
prefix = '';
|
|
184
|
-
suffix = '';
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
for (const tag of span.tags) {
|
|
189
|
-
const wrapper = tag === 'b' ? '**' : tag === 'i' ? '*' : tag === 's' ? '~~' : tag === 'code' ? '`' : '';
|
|
190
|
-
if (tag === 'blockquote')
|
|
191
|
-
prefix += '▎ ';
|
|
192
|
-
else {
|
|
193
|
-
prefix += wrapper;
|
|
194
|
-
suffix = wrapper + suffix;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
while (rest) {
|
|
199
|
-
const overhead = size(prefix + suffix);
|
|
200
|
-
if (size(current) + overhead + size([...rest][0]) > budget)
|
|
201
|
-
flush();
|
|
202
|
-
const [piece, remaining] = cut(rest, budget - size(current) - overhead, size);
|
|
203
|
-
current += prefix + piece + suffix;
|
|
204
|
-
rest = remaining;
|
|
205
|
-
if (rest)
|
|
206
|
-
flush();
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
flush();
|
|
211
|
-
return output.length ? output : [''];
|
|
212
|
-
}
|
|
4
|
+
export { escapeHtml, partTitle } from '@quecue/notify-core';
|
|
5
|
+
export const { parseMarkdown, visibleText, telegramParts, feishuParts } = createFormatting(key => t(key));
|
package/dist/reply-bridge.js
CHANGED
|
@@ -4,8 +4,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
6
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
-
import { appPaths, ConfigError } from './config.js';
|
|
8
|
-
import { t } from './i18n.js';
|
|
7
|
+
import { appPaths, ConfigError, readConfigLanguage } from './config.js';
|
|
8
|
+
import { resolveLocale, setLocale, t } from './i18n.js';
|
|
9
9
|
import { accessReplies, atomicJson, readJson, replyDirectory, stateTransaction } from './reply-state.js';
|
|
10
10
|
import { telegramApi } from './reply-telegram.js';
|
|
11
11
|
import { drainReplies, processUpdate } from './reply-engine.js';
|
|
@@ -118,6 +118,8 @@ export async function runBridge(file, options = {}) {
|
|
|
118
118
|
await atomicJson(leasePath(file), lease);
|
|
119
119
|
let iteration = 0, failures = 0;
|
|
120
120
|
while (!await isStopped() && iteration++ < (options.maxIterations ?? Infinity)) {
|
|
121
|
+
// 桥接是长驻进程,语言切换后要跟着配置文件走;否则后续消息会和 hook 进程的语言不一致。
|
|
122
|
+
setLocale(resolveLocale(options.lang, env, await readConfigLanguage(file)));
|
|
121
123
|
const current = await fresh();
|
|
122
124
|
if (!current)
|
|
123
125
|
break;
|
package/dist/reply-state.js
CHANGED
|
@@ -24,6 +24,21 @@ export async function readJson(file) {
|
|
|
24
24
|
throw new ConfigError(t('reply.readFailed'));
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
// Windows 防病毒/索引程序可能短暂占用目标文件;只重试原子替换,不重做状态事务或业务操作。
|
|
28
|
+
export async function replaceStateFile(source, target, replace = rename, sleep = delay) {
|
|
29
|
+
for (let attempt = 0;; attempt++) {
|
|
30
|
+
try {
|
|
31
|
+
await replace(source, target);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const code = error.code;
|
|
36
|
+
if (!['EPERM', 'EACCES', 'EBUSY'].includes(code ?? '') || attempt >= 5)
|
|
37
|
+
throw error;
|
|
38
|
+
await sleep(10 * 2 ** attempt);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
export async function atomicJson(file, value) {
|
|
28
43
|
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
29
44
|
const temporary = path.join(path.dirname(file), `.reply-${randomBytes(12).toString('hex')}.tmp`);
|
|
@@ -36,7 +51,7 @@ export async function atomicJson(file, value) {
|
|
|
36
51
|
finally {
|
|
37
52
|
await handle.close();
|
|
38
53
|
}
|
|
39
|
-
await
|
|
54
|
+
await replaceStateFile(temporary, file);
|
|
40
55
|
}
|
|
41
56
|
finally {
|
|
42
57
|
await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
|
package/dist/reply-telegram.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { createTelegramApi, TelegramApiError } from '@quecue/notify-core';
|
|
1
2
|
import { requestJson } from './channels.js';
|
|
2
3
|
import { ConfigError } from './config.js';
|
|
3
4
|
import { t } from './i18n.js';
|
|
4
5
|
export function telegramApi(token, request = requestJson) {
|
|
6
|
+
const call = createTelegramApi(token, { request });
|
|
5
7
|
return async (method, payload) => {
|
|
6
8
|
if (!['sendMessage', 'editMessageReplyMarkup', 'answerCallbackQuery', 'getMe', 'getUpdates', 'getWebhookInfo'].includes(method))
|
|
7
9
|
throw new ConfigError(t('reply.telegramMethod'));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
try {
|
|
11
|
+
return await call(method, payload);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error instanceof TelegramApiError) {
|
|
15
|
+
if (error.code === 409)
|
|
16
|
+
throw new ConfigError(t('reply.telegramConflict'));
|
|
17
|
+
throw new ConfigError(t('reply.telegramRequestFailed'));
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
13
20
|
}
|
|
14
|
-
return response.result;
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
23
|
export async function replyHint(api, chat, title, content, replyTo, markup) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quecue/dev-notify",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "跨平台开发任务通知工具,支持 Telegram 与飞书",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "
|
|
32
|
+
"build": "node scripts/build.mjs",
|
|
33
33
|
"test": "npm run build && node --test test-js/*.test.mjs",
|
|
34
34
|
"start": "node dist/cli.js",
|
|
35
35
|
"prepack": "npm run build"
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"typescript": "^5.9.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@napi-rs/keyring": "2.1.2"
|
|
42
|
+
"@napi-rs/keyring": "2.1.2",
|
|
43
|
+
"@quecue/notify-core": "0.1.0"
|
|
43
44
|
},
|
|
44
45
|
"publishConfig": {
|
|
45
46
|
"access": "public",
|