@quecue/dev-notify 0.0.0-stage → 0.1.0-alpha.3
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.md +39 -0
- package/LICENSE +21 -0
- package/README.md +245 -2
- package/SECURITY.md +76 -0
- package/dist/channels.js +158 -0
- package/dist/claude-code.js +236 -0
- package/dist/claude-hook.js +123 -0
- package/dist/cli.js +278 -0
- package/dist/config.js +141 -0
- package/dist/credentials.js +105 -0
- package/dist/formatting.js +210 -0
- package/dist/prompts.js +96 -0
- package/dist/reply-bridge.js +189 -0
- package/dist/reply-engine.js +199 -0
- package/dist/reply-hooks.js +230 -0
- package/dist/reply-interactions.js +262 -0
- package/dist/reply-outcomes.js +182 -0
- package/dist/reply-platform.js +182 -0
- package/dist/reply-state.js +189 -0
- package/dist/reply-telegram.js +19 -0
- package/dist/setup.js +250 -0
- package/package.json +43 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
## 0.1.0-alpha.3 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- 可选 Claude Code 通知集成:安装、查询和移除用户级 hooks,保留其他配置。
|
|
8
|
+
- Telegram「回复 Claude」、直接回复通知、飞书跳转 Telegram;远程输入需要独立授权具体私聊用户。
|
|
9
|
+
- 会话与进程身份绑定、工作中回复排队、防重放,以及 Claude 实际收到输入后的确认。
|
|
10
|
+
- AskUserQuestion 单选、多选、多题及自定义输入;工具本次允许/拒绝需要额外授权,不创建永久权限规则。
|
|
11
|
+
- npm 包关联 GitHub 源码仓库、问题反馈入口与更新记录。
|
|
12
|
+
|
|
13
|
+
### 修复
|
|
14
|
+
|
|
15
|
+
- PreToolUse 只登记调用,远程等待改用 PermissionRequest;普通交互终端保留 PC 原生问答界面,两端均可操作。
|
|
16
|
+
- Telegram 提交区分「已登记」与实际生效结果;反馈已生效、未生效或结果未知,不自动重放。
|
|
17
|
+
- 两端提交相同答案时只确认最终内容一致,不声称能够判断应答来源。
|
|
18
|
+
- 本机先答、会话切换及超时后使旧按钮失效。
|
|
19
|
+
- 按接收人完整投递结果抑制选择题之后重复的通用权限提醒;发送失败、未覆盖接收人及后续其他工具权限提醒仍保留。
|
|
20
|
+
|
|
21
|
+
### 升级
|
|
22
|
+
|
|
23
|
+
- 已有安装需要重新运行 `dev-notify integration claude-code install`,安装完整的 10 类 hooks,并在 Claude Code `/hooks` 中确认加载;必要时重启会话。
|
|
24
|
+
- 已有回复桥接需要停止后重新启动,使后台进程加载新版。无需重新输入已保存的通知凭据;已有有效回复授权继续使用。
|
|
25
|
+
- 首次使用远程回复须运行 `dev-notify reply setup` 独立授权。自定义通知配置的用户,应在相关命令中继续使用原有 `--config` 路径。
|
|
26
|
+
- npm 安装/升级本身不会改写 Claude Code 设置或启动后台服务。
|
|
27
|
+
|
|
28
|
+
### 验证与限制
|
|
29
|
+
|
|
30
|
+
- Windows x64 / Node 24:PC 优先、Telegram 优先、多选、自定义输入、不同答案竞争及重复提醒修复已由用户完成实机验收。
|
|
31
|
+
- 自动化回归共 157 项,156 通过、0 失败、1 项需要显式启用的原生凭据测试跳过;包含真实 dev-notify CLI/后台进程的模拟网络闭环。
|
|
32
|
+
- 核对的 Claude Code 版本为 2.1.316;其他版本和特殊宿主不保证采用同一并行路径。
|
|
33
|
+
- macOS 终端输入和权限流程、Node 22 最低版本尚未实机验收。工具权限应答的实机验收不包含在上述问答测试中。
|
|
34
|
+
- 网络中断、强杀和撤销授权后不能保证最终回执送达;详情见 SECURITY.md。
|
|
35
|
+
|
|
36
|
+
## 0.1.0-alpha.1
|
|
37
|
+
|
|
38
|
+
- 首个公开 npm Alpha:通用 Telegram/飞书通知、长正文分段、交互渠道配置和系统凭据存储。
|
|
39
|
+
- 不包含 Claude Code hooks 与远程回复交互。
|
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/README.md
CHANGED
|
@@ -1,3 +1,246 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dev-notify
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
跨平台开发任务通知工具,使用 **TypeScript / Node.js**,通过 npm 分发,无需 Python 或独立可执行文件。通用通知不依赖 Claude Code、Unity 或项目目录。
|
|
4
|
+
|
|
5
|
+
**npm 包名为 `@quecue/dev-notify`。`0.1.0-alpha.3` 提供 Claude Code 回复、原生 PC 与 Telegram 双端答题及结果反馈。** 版本变更与升级说明见 [CHANGELOG.md](CHANGELOG.md);公开版本以 npm registry 为准。
|
|
6
|
+
|
|
7
|
+
## 安装与使用
|
|
8
|
+
|
|
9
|
+
要求 Node.js 22 或以上,首发目标 Windows、macOS。运行时依赖锁定版本的 `@napi-rs/keyring`,由 npm 安装对应系统的预编译原生组件,不需要 Python、Rust 或 C++ 编译器。不要省略 npm optional 依赖。
|
|
10
|
+
|
|
11
|
+
从 npm 安装当前已发布的 Alpha:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -g @quecue/dev-notify@alpha
|
|
15
|
+
dev-notify --version
|
|
16
|
+
dev-notify setup
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`0.1.0-alpha.1` 不含 Claude Code hooks;以下回复交互要求 `0.1.0-alpha.3` 或以上。升级后须重新运行 `dev-notify integration claude-code install` 更新完整 hooks,并停止、重新启动已有回复桥接;已有有效授权和通知凭据无需重新配置。使用自定义配置时,继续传入原来的 `--config` 路径。源码开发可在工程目录构建后执行:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm ci --ignore-scripts
|
|
23
|
+
npm run build
|
|
24
|
+
node dist/cli.js setup
|
|
25
|
+
node dist/cli.js doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
配置完成后发送:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
node dist/cli.js send --title "构建完成" --content "安装包已生成"
|
|
32
|
+
node dist/cli.js send --title "测试结果" --content-file result.txt
|
|
33
|
+
node dist/cli.js send --title "任务输出" --stdin
|
|
34
|
+
node dist/cli.js send --title "分段预览" --content-file result.txt --dry-run
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
若需要本地安装为 `dev-notify` 命令,在构建后运行 `npm install --global .`,之后可执行 `dev-notify setup`、`dev-notify send ...`。这是本地源码安装,不是从公开 registry 安装;本工程不会自动进行全局安装或发布。
|
|
38
|
+
|
|
39
|
+
正式包的安装/升级/卸载由 npm 负责。**卸载程序不会自动清除用户凭据**;如需清理,请先交互执行 `dev-notify credentials clear`,再卸载。该命令只删除当前配置引用的 Node 凭据,不删除其他账号、其他配置或 Python 原型凭据。
|
|
40
|
+
|
|
41
|
+
## 已提供的功能
|
|
42
|
+
|
|
43
|
+
- Telegram、飞书双渠道通知,按渲染后的长度分段,不静默截断。
|
|
44
|
+
- 文本、严格 UTF-8 文件或标准输入;支持渠道选择、通知级别、辅助信息。
|
|
45
|
+
- 交互配置、隐藏输入、系统凭据存储、环境变量覆盖。
|
|
46
|
+
- 配置更新冲突检测、失败恢复、凭据清理。
|
|
47
|
+
- 可选测试通知:只有额外确认后才发送,默认不发送。
|
|
48
|
+
- 可选 Claude Code 集成:最终回复、等待通知;确认后合并安装,支持独立查询和移除。
|
|
49
|
+
- 独立授权的 Telegram 按钮回复、直接回复消息、飞书跳转;逐段映射到具体会话。
|
|
50
|
+
- 普通回复本地排队、终端提交与 `UserPromptSubmit` 收到确认,状态不混淆。
|
|
51
|
+
- `AskUserQuestion` 原始选项、单选/多选、自定义答案,通过官方 hook JSON 直接回传。
|
|
52
|
+
- 另外选择启用后,可逐次允许或拒绝工具权限,或填写自定义拒绝说明;不写永久权限规则。
|
|
53
|
+
- `paths`、`doctor`、`status`、`send --dry-run` 离线诊断;`reply start/status/stop/disable/run` 管理桥接。
|
|
54
|
+
|
|
55
|
+
**通用通知不依赖 Claude Code。**npm 安装本身不修改其他应用配置,也不启动服务。通知 hooks 和远程输入授权分开,默认只通知;授权后才允许回复桥接轮询 Telegram。没有自动配对、任意远程 shell 执行器或系统开机自启。
|
|
56
|
+
|
|
57
|
+
## 配置向导
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
node dist/cli.js setup
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
1. 选择 Telegram、飞书或两者。
|
|
64
|
+
2. 隐藏输入 Token、webhook 和可选签名密钥。无法安全隐藏输入时停止,不回退到明文回显。
|
|
65
|
+
3. Telegram 手动填写数字 chat_id,多个用逗号分隔;用户需要先启动 Bot。不会自动启动 getUpdates 配对。
|
|
66
|
+
4. 展示渠道和接收人数,最终确认后保存。输入错误可以重试,Ctrl+C 取消。
|
|
67
|
+
5. 保存后单独询问是否发送固定内容的测试通知。测试失败不会撤销已成功保存的配置。
|
|
68
|
+
6. 另行询问是否配置 Claude Code 自动通知;展示用户级设置路径、正文发送范围后,再次确认才合并 hooks。拒绝时已有集成保持原样,不会被禁用。
|
|
69
|
+
7. Telegram 可用时另外询问是否授权回复;选择具体可信私聊用户、是否允许逐次处理工具权限,再确认。拒绝不改变已有授权,不自动启用全部通知接收人。
|
|
70
|
+
|
|
71
|
+
编辑已有配置需要先确认;密钥留空保留已存值,可选签名输入 `-` 清除。未选渠道会禁用,新条目只保留所选渠道的凭据。环境变量仍然优先;向导会提示存在覆盖,测试使用最终生效值,而不是忽略环境的刚输入值。
|
|
72
|
+
|
|
73
|
+
自动化使用无密钥模板和 CI secrets:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
node dist/cli.js setup --template --yes
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
模板永不覆盖已有配置。`--yes` 只能配合 `--template`,不能跳过交互密钥保存确认。
|
|
80
|
+
|
|
81
|
+
## Claude Code 通知集成
|
|
82
|
+
|
|
83
|
+
要求使用支持命令 hook `command` + `args`(无 shell exec 形式)及 `async` 的新版 Claude Code。实际加载情况在 Claude Code 的 `/hooks` 中确认;旧版本不支持这些字段时应升级,不降级到不安全的路径拼接。
|
|
84
|
+
|
|
85
|
+
已有通知渠道配置时,无需重新输入凭据:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
dev-notify integration claude-code install
|
|
89
|
+
dev-notify integration claude-code status
|
|
90
|
+
dev-notify integration claude-code remove
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
源码验证时将上述 `dev-notify` 替换为 `node dist/cli.js`。安装可用 `--config <绝对或相对路径>` 绑定指定通知配置;执行时将其解析为绝对路径并保存到 hook 参数。自定义 `DEV_NOTIFY_HOME` 的使用者应在安装时设置该变量。
|
|
94
|
+
|
|
95
|
+
- **配置范围**:默认用户级 `~/.claude/settings.json`,设置 `CLAUDE_CONFIG_DIR` 时使用该绝对目录下的 `settings.json`,覆盖该 Claude Code 配置目录下的全部项目。不会扫描或修改每个项目的设置。
|
|
96
|
+
- **发送范围**:Stop 直接使用事件的 `last_assistant_message` 转发最终回复;没有正文则明确发送完成提示,不回读可能滞后的 transcript。不发送思考或工具结果。启用远程回复后还会发送结构化问题、选项,以及另行授权的工具权限请求参数;它们可能含敏感信息,启用前务必检查接收目标。
|
|
97
|
+
- **等待提醒**:转发 `permission_prompt`、`elicitation_dialog`、`elicitation_url_dialog`、`agent_needs_input`;忽略 `idle_prompt`、认证成功等其他通知。当前会话最后一项调用为选择题、没有其他未结束调用,且结构化通知已完整投递时,按接收人抑制重复的 `permission_prompt`;飞书单独核对投递成功及目标。短期保留已结束题目的覆盖状态以处理迟到提醒;新一轮输入、后续其他工具调用、会话切换或交互有效期结束后不再沿用。未覆盖接收人、部分/失败投递及无法核验的情况仍发送通用提醒,其他通知类型不受影响。Notification 本身没有完整选项,不能拿提示猜答案;可靠选项交互见下节。不支持的 MCP 表单/URL 等请求保留本机处理。
|
|
98
|
+
- **合并与移除**:保留权限、环境变量和其他 hooks;重复安装不会累加。同一用户级设置中的本工具 hooks 会在确认后更新到当前 Node、CLI 和通知配置路径。只移除带本工具标记及匹配参数结构的处理器,不删除设置文件或凭据。
|
|
99
|
+
- **与旧工具共存**:原 Python hooks、项目级或插件 hooks 不会自动删除,可能造成重复通知。安装时会提示当前用户级文件中的其他处理器;旧集成需由用户自行确认停用。
|
|
100
|
+
- **事件与时序**:安装 Stop、Notification、SessionStart、UserPromptSubmit、SessionEnd、PreToolUse、PermissionRequest、PostToolUse、PostToolUseFailure、PermissionDenied,共 10 类。Notification 异步;其他事件同步登记状态。PreToolUse 只登记调用身份,不发送通知、不等 Telegram,超时上限 10 秒;PermissionRequest 才等待远程答案。Stop 包含通知发送,可能延迟本地返回;通知/生命周期上限 120 秒。
|
|
101
|
+
- **交互与失败**:PermissionRequest hook 上限 360 秒,远程答题窗口 5 分钟;普通交互终端保留原生窗口,由 Claude 仲裁先到的完整答案。不把 hook 改成 async,也不模拟按键。无回答、撤销、超时或发送失败不默认选择。内部错误输出安全摘要到 stderr 并退出 0,不用会让 Stop 继续的退出码 2。当前已核对 Claude Code 2.1.316 的并行实现,其他版本、后台子任务及特殊宿主需另行验收,不能泛化为所有宿主都并行。
|
|
102
|
+
- **输入边界**:hook stdin 最多 8 MiB,接收超时 15 秒;超限拒绝而非截断。正常长正文沿用双渠道分段。`stop_hook_active: true` 的继续轮次不发送完成通知。
|
|
103
|
+
- **加载限制**:安装后在 `/hooks` 检查并按界面确认,必要时重启会话。不会解除 `disableAllHooks` 或绕过托管策略。`status` 仅检查该用户级文件,不等于运行时已加载或网络验证。
|
|
104
|
+
- **环境和路径**:异步进程使用 Claude Code 的环境,可能与手动终端不同;环境变量优先规则不变。切换 Node 版本或移动源码/包目录后,应重新运行 `install` 更新固定路径。
|
|
105
|
+
- **写入保护**:使用排他 `.dev-notify.lock`、内容快照、同目录临时文件及原子替换;无效 JSON 不覆盖。外部编辑器不遵守锁,安装期间不要并发编辑。强杀/断电可能留下锁或 `.dev-notify-*.tmp`,确认没有安装进程后再清理。
|
|
106
|
+
|
|
107
|
+
启用过回复时先运行 `reply disable` 撤销授权并停止桥接,再运行 `integration claude-code remove` 后卸载 npm 包;npm 卸载不会自动移除 hooks。若先卸载了包,需重新安装后移除,或在 `/hooks` 中手动处理对应条目。单独移除 hooks 不会清除通知凭据。
|
|
108
|
+
|
|
109
|
+
## Claude Code 回复与选项交互
|
|
110
|
+
|
|
111
|
+
已有渠道配置时,使用以下有限流程(源码使用 `node dist/cli.js` 替换 `dev-notify`):
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
dev-notify integration claude-code install
|
|
115
|
+
dev-notify reply setup
|
|
116
|
+
dev-notify reply start
|
|
117
|
+
dev-notify reply status
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
第一条在确认后更新到本版完整 hooks;缺少生命周期事件、Stop 仍异步、绑定其他配置或用户级 hooks 被禁用时拒绝启用/使用回复。普通终端回复还要求实际观察到该会话的 UserPromptSubmit,因此升级中途未登记的旧轮次可能只有通知,下一次正常提交后才开放按钮。第二条**另行授权**具体 Telegram 私聊用户。通知接收人、群聊成员、能看到飞书卡片的人不会自动获得输入权。`reply setup` 默认不授予工具权限批准能力,需单独选 Yes。不会调用 Python、自动迁移旧凭据或默认同意权限。
|
|
121
|
+
|
|
122
|
+
### 普通回复
|
|
123
|
+
|
|
124
|
+
- 完成通知每段都提供「💬 回复 Claude」;也可直接回复对应 Telegram 消息。飞书按钮跳到同一 Bot 的 Telegram 私聊,再进入回复框。通用 `send` 不附带会话回复功能。
|
|
125
|
+
- Bot 启动并核验成功、会话可精确定位时才展示按钮;未就绪仍发送普通通知并报告安全错误。必须从启用后的新通知进入,旧 Python 消息和新授权前的映射不继承。
|
|
126
|
+
- 文本清理换行和控制字符后作为**一次**终端输入提交;最多 4000 Unicode 字符,超限拒绝,不截断、不拆成多个任务。输入可包含 Claude 命令;这等同在本机提交内容,不是只读功能。
|
|
127
|
+
- Claude 工作中收到的普通回复先进入本地队列;只在 Stop 后尝试输入,不拿普通回复填写工具权限弹窗。排队超过 5 分钟取消;本地提交新任务会取消尚未注入的队列,不抢占本地输入。
|
|
128
|
+
- 「已排队」「已转发」「Claude 已收到回复」是不同状态:只有相同会话/进程、匹配文本摘要的 `UserPromptSubmit` 才发收到确认;收到不代表任务完成。确认发回原 Telegram 私聊,并向已配置飞书发送不含回复原文的收到提示。注入失败或结果未知不自动重试。
|
|
129
|
+
- 映射有效期 24 小时;目标必须匹配会话 ID、PID、进程出生标识及 TTY。会话关闭/切换、PID 重用、多个匹配或目标不确定时拒绝。每会话只保留一个未确认远程回复,Telegram update 在处理前登记 offset,崩溃可能丢失,不重放输入。
|
|
130
|
+
|
|
131
|
+
### 等待选项与自定义输入
|
|
132
|
+
|
|
133
|
+
- **AskUserQuestion**:展示实际问题和全部选项正文,长内容分段。单选点击提交;多选逐项选择后提交;每题都有「自定义输入」。多题逐题完成,原始 `questions` 与 `answers` 通过官方 `PermissionRequest.decision.updatedInput` 回传,不模拟方向键或 Enter。旧题按钮和过期自定义输入不回答新题。
|
|
134
|
+
- **工具权限**:仅在 `reply setup` 另外授权后,展示工具名称/参数及「本次允许」「拒绝」「自定义拒绝说明」。通过 `PermissionRequest` 输出一次性的 allow/deny;自定义说明只用于拒绝反馈,不变相批准。不会输出 `updatedPermissions`、修改默认模式或持久写规则,仍受 Claude 托管/deny 规则限制。
|
|
135
|
+
- **不能声称复刻全部本地按钮**:官方明确 `permission_suggestions` 并非本地权限 UI 的完整选项列表,因此不伪造「始终允许」等按钮。MCP Elicitation 表单/URL 等目前只有等待提醒,需回本机处理。
|
|
136
|
+
- 本机原生界面不被替换。Telegram 提供「关闭远程答题(本机仍可操作)」入口;远程超时或关闭不会默认作答。Telegram 是选项点击入口,飞书使用跳转链接,不声称具备飞书回调服务。
|
|
137
|
+
- **两阶段反馈**:每次答案提交先报告「已登记,等待确认」;多题必须全部完成才回传。随后以 PostToolUse 的实际答案反馈「答案内容已生效」或「本次提交未生效」。两端同时提交相同答案时,只确认内容一致,不虚称 Telegram 赢得竞争。权限允许以工具完成作保守确认;拒绝、取消或工具执行失败不足以证明远程决定被采用时,明确报告结果未知。
|
|
138
|
+
- **失效与异常**:本机先答后结束远端请求,桥接尝试把旧选项替换为「已处理/已过期」状态按钮,不再接受答案;编辑失败也拒绝旧回调。回传后最多等待 5 分钟确认,缺少结果时反馈「暂时无法确认」,不会重放答案。反馈需要桥接在线;网络异常、强杀、撤销授权后不保证送达,不为了补通知重新授权或无限重发。最终反馈发送前登记状态,发送失败记为投递未知,防止崩溃后重复发送;若只收到等待提示而没有最终反馈,请检查终端及 reply status。
|
|
139
|
+
- **防误投**:PreToolUse 保存 tool_use_id 与输入摘要,PermissionRequest 仅绑定唯一匹配调用;完成事件按 tool_use_id 结束请求。缺关联、同一 prompt 内重复同内容导致歧义、字段被其他 hook 改写或调用已结束时拒绝远程答题,保留本机处理,不猜测目标。
|
|
140
|
+
|
|
141
|
+
### 平台与后台服务
|
|
142
|
+
|
|
143
|
+
- **Windows**:原生 `claude` 进程,系统 PowerShell 编译控制台帮助类,核验出生标识后使用 AttachConsole / WriteConsoleInputW,不抢前台。共享控制台存在另一个 claude 进程时拒绝。Node 托管的旧 CLI 不在当前可验证输入范围。
|
|
144
|
+
- **macOS**:核验前台 claude 进程及精确 TTY,优先 tmux 字面粘贴缓冲区,否则 Ghostty AppleScript;文本不作为 shell 参数执行。系统可能要求辅助功能/Automation 权限,拒绝时不切到任意窗口。两条适配器已做模拟验证,**未做 macOS 实机输入验收**。
|
|
145
|
+
- 不支持 VS Code 集成终端、WSL、SSH、其他终端的自动回退;元数据缺失、CLI 升级改变内部会话文件格式时拒绝输入,不扫描 transcript 猜目标。原生输入与本地活动之间不具备原子性:回复期间不要在相同终端手工编辑/切换会话。
|
|
146
|
+
- 桥接是本机 Node 后台进程,不监听公网端口、不设置系统自启动。明确授权后可手动 `reply start`,也可由支持的后续 hook 事件按需启动。24 小时没有 Telegram 更新会退出;连续轮询失败 5 次或 409 冲突退出,状态报告安全摘要。
|
|
147
|
+
- 一个 Bot 只能有一个 getUpdates 消费者。请先自行停用旧 Python 桥接和其他机器的同 Bot 实例;同用户的本工具使用 Bot 指纹锁,但无法跨机器或与不遵守锁的程序互斥。不会删除 webhook 或强杀别的进程;共用会导致冲突和消息丢失。
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
dev-notify reply stop # 请求停止本次桥接,不撤销授权;后续 hook 可再启动
|
|
151
|
+
dev-notify reply disable # 确认后撤销授权并请求停止;彻底关闭用此命令
|
|
152
|
+
dev-notify reply run # 前台诊断启动/网络问题,不与 start 实例并行
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
停止通过实例随机标识的控制文件请求,不根据旧 PID 强杀进程。已开始的系统输入不能撤回。`status` 是本地进程/心跳快照,不等于端到端 Telegram 或真实 Claude 验收。
|
|
156
|
+
|
|
157
|
+
授权在 `<通知配置>.reply.json`,消息/会话/offset/队列/交互状态在 `<通知配置>.reply-data/`;Bot 锁在 `paths` 所示 data 目录的 `reply-bots/`。排队回复原文、问题、工具输入和待回传答案会短暂写入本地状态;提交前移除普通回复原文;结构化交互保留到完成或确认超时,再清除问题、参数与答案正文,短期保留结果收据和消息 ID 以反馈及拒绝旧按钮。有效期是逻辑检查,不保证停机后的文件自动擦除。POSIX 使用 0700/0600,Windows 依赖账号目录 ACL;同用户进程仍可读取。不要提交、共享或打包这些文件(已加忽略规则及 npm 白名单)。
|
|
158
|
+
|
|
159
|
+
强杀可能留下锁;先确认相应桥接/写入进程已退出,再手工处理其残留锁,不要在进程运行中删除。`reply disable` 不擅自删除历史状态;需要清除时先禁用/停止,确认后手工删除上述专属文件。清理凭据、移除 hooks 和删除回复状态是独立操作。
|
|
160
|
+
|
|
161
|
+
## 系统凭据与兼容性
|
|
162
|
+
|
|
163
|
+
Node 版使用 **Windows Credential Manager / macOS Keychain**,通过原生 Node API 传递秘密,不调用 Python,不将秘密放进命令行参数,不写入明文 JSON。
|
|
164
|
+
|
|
165
|
+
- 底层库:`@napi-rs/keyring`,固定版本记录于 lockfile。
|
|
166
|
+
- service:`dev-notify.node.credentials.v1`;account:随机 32 位十六进制 ID。
|
|
167
|
+
- 配置引用:`"credentials": {"backend":"keyring","id":"..."}`。
|
|
168
|
+
- 为兼容 Windows 通用凭据 blob 大小,凭据包限制 2400 UTF-8 字节,超限报错,不截断。
|
|
169
|
+
- 原生组件加载失败、存储锁定或拒绝访问时明确报错,不降级为明文文件。macOS 可能要求系统授权;离线不代表一定无系统提示。
|
|
170
|
+
- 默认不枚举系统条目,只访问当前引用或本次新建的随机条目。
|
|
171
|
+
|
|
172
|
+
保留的 Python 原型使用 `backend: "system"`,Windows 是 DPAPI 文件,与 Node Credential Manager 不是同一存储。Node 不会读取或删除它。遇到旧配置可运行 `setup` 重新输入生成 Node 引用;原 Python 凭据保持不变。不要用 Python 原型编辑 Node 的 `keyring` 配置。
|
|
173
|
+
|
|
174
|
+
Python 源码及测试仍在 `src/dev_notify/`、`tests/`、`tools/`,本轮没有删除。历史文档位于 `docs/PYTHON_PROTOTYPE.md`、`docs/PYTHON_SECURITY.md`,不进入 npm 包。
|
|
175
|
+
|
|
176
|
+
## 路径、模板和环境变量
|
|
177
|
+
|
|
178
|
+
| 平台 | 配置 | 数据预留目录 |
|
|
179
|
+
| --- | --- | --- |
|
|
180
|
+
| Windows | `%APPDATA%\DevNotify\config.json` | `%LOCALAPPDATA%\DevNotify\data` |
|
|
181
|
+
| macOS | `~/Library/Application Support/DevNotify/config/config.json` | `~/Library/Application Support/DevNotify/data` |
|
|
182
|
+
|
|
183
|
+
`DEV_NOTIFY_HOME` 必须是绝对路径,配置位于其 `config/config.json` 下。也可用 `--config <路径>` 显式指定文件。显式文件缺失时报错;默认文件缺失时允许仅使用环境变量。
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"version": 1,
|
|
188
|
+
"telegram": {"enabled": true, "chat_ids": [], "token_env": "TELEGRAM_BOT_TOKEN"},
|
|
189
|
+
"feishu": {"enabled": true, "webhook_env": "FEISHU_WEBHOOK", "secret_env": "FEISHU_SECRET"}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- `TELEGRAM_BOT_TOKEN`:Token。
|
|
194
|
+
- `TELEGRAM_CHAT_ID`、`TELEGRAM_CHAT_IDS`:主接收人和逗号分隔接收人,任一显式存在即完全替换文件名单;显式空名单撤销接收人。大整数 chat_id 必须用字符串。
|
|
195
|
+
- `FEISHU_WEBHOOK`:飞书中国站 HTTPS 自定义机器人地址。
|
|
196
|
+
- `FEISHU_SECRET`:可选签名密钥。
|
|
197
|
+
|
|
198
|
+
凭据环境变量显式存在时优先,包括空值;不存在才延迟读取系统引用,同次配置加载只读取一次。禁用渠道不会仅为读取系统秘密而打开后端。Token/webhook/secret 不能直接填写进 JSON。
|
|
199
|
+
|
|
200
|
+
## 更新与清理安全
|
|
201
|
+
|
|
202
|
+
保存流程:写入新随机系统条目 → 校验可读 → 原子替换 JSON → 删除旧 Node 条目。配置切换前失败保留旧配置,并尝试清理本次新凭据;清理失败只报告非敏感引用 ID。旧 Python 条目从不删除。
|
|
203
|
+
|
|
204
|
+
配置写入使用排他 `.setup.lock`,写前及切换前核对原文件。不要在向导保存时用编辑器并发改配置;编辑器不遵守此锁。断电/强杀可能留下锁或孤立条目,存储和 JSON 不是跨系统原子事务。确认没有向导运行后才能处理残留锁;程序不会擅自扫描并删除不明条目。
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
node dist/cli.js credentials clear
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
清理必须交互确认,先去除当前 JSON 引用,再尝试删除其 Node 条目;环境变量保持不变。共享同一旧引用的配置副本也会受影响,操作前会告知。删除失败明确警告,不伪装成已清理。
|
|
211
|
+
|
|
212
|
+
## 格式、发送与退出码
|
|
213
|
+
|
|
214
|
+
支持标题、粗体、斜体、删除线、行内代码、代码块、引用、列表、任务列表、等宽表格。Telegram 按 UTF-16 长度,飞书按 UTF-8 请求预算分段,emoji 不从代理对中切开。跨段标签补齐,标题 `(1/N)`,每段保留辅助信息。链接显示为“文字 (URL)”,纯文本回退保留地址。不是完整 CommonMark 引擎,复杂转义或嵌套不保证完全原样排版。
|
|
215
|
+
|
|
216
|
+
单接收人中途失败停止其后续分段,Telegram 继续其他接收人。HTML/卡片格式错误回退;Telegram 明确 `429` 按服务端要求有限重试。超时不盲目重发,不承诺恰好一次投递。
|
|
217
|
+
|
|
218
|
+
- `send` 会把指定内容上传到第三方,请检查接收人与敏感信息。
|
|
219
|
+
- `--dry-run` 不输出正文/凭据,不联网。
|
|
220
|
+
- `doctor` / `status` 可能读取本地凭据,不验证网络和实际授权。
|
|
221
|
+
- 文件缺失、损坏配置、UTF-8 错误立即报错,不退回发送标题。
|
|
222
|
+
|
|
223
|
+
| 代码 | 含义 |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| 0 | 所选已配置渠道完整发送成功,或配置/只读操作成功 |
|
|
226
|
+
| 1 | 部分/全部投递失败;或 doctor/status 没有可用渠道 |
|
|
227
|
+
| 2 | 参数、配置、文件或凭据操作错误 |
|
|
228
|
+
| 130 | 用户取消 |
|
|
229
|
+
|
|
230
|
+
向导保存成功后测试通知失败,命令仍以保存成功结束,并明确显示测试失败,两者不混淆。保存后取消 hook 配置仍返回 0 并保留渠道;hook 配置出错返回 1,明确提示渠道已保存。集成 `status` 在条目缺失、重复或当前用户级文件禁用 hooks 时返回 1。内部 hook 处理命令始终非阻塞退出,失败通过 stderr 报告。
|
|
231
|
+
|
|
232
|
+
## 开发与验证
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
npm ci --ignore-scripts
|
|
236
|
+
npm test
|
|
237
|
+
npm pack --dry-run
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`node-src/` 是 TypeScript 源码,`dist/` 是编译输出,`test-js/` 是测试。默认测试只用假凭据和模拟后端/网络;本机原生假条目往返测试需显式设置 `DEV_NOTIFY_NATIVE_TEST=1`,且只在 Windows 启用,使用随机 ID,结束后清理。
|
|
241
|
+
|
|
242
|
+
npm 发布白名单只有 `dist/*.js`、README、SECURITY、CHANGELOG、LICENSE 和必需的 package.json;不包含 Python、测试、配置或密文。无 postinstall、自动启动和自写下载脚本;预编译原生依赖由 npm 的平台 optional 包解析,**不应使用 `--omit=optional`**。macOS x64/arm64 和 Windows x64/arm64 预编译包由依赖提供,提供包不等于本项目已实机验收。
|
|
243
|
+
|
|
244
|
+
本机验证环境为 Windows x64、Node 24。自动化回归 157 项:156 通过、0 失败、1 项需显式启用的原生凭据测试跳过。覆盖临时 hooks、回复/授权/过期/防重放、单选/多选/自定义、单次权限 JSON、双端竞争、反馈异常及按接收人的重复提醒去重。实际 CLI 子进程完成后台 start → PreToolUse 登记 → PermissionRequest → 假 Bot 按钮 → JSON 答案 → 模拟 PostToolUse → 最终反馈 → 通知去重/后续工具提醒 → stop;该闭环使用模拟网络和进程元数据,不是实机输入证明。另由用户在 Windows 原生 Claude Code 2.1.316 与真实 Telegram 完成本机优先、远端优先、多选、自定义、不同答案竞争,以及延迟通用提醒去重的实机验收。隔离编译和测试不覆盖真实 hooks 引用的 dist;Windows 原生输入帮助类的自动化测试仅编译/布局验证,不执行真实 Send。macOS 两条终端适配器只有模拟验证;macOS 实机、Node 最低版本、版本管理器切换、工具权限应答实机及完整公开发布审计仍未验收。
|
|
245
|
+
|
|
246
|
+
采用 MIT,见 [LICENSE](LICENSE)。安全边界见 [SECURITY.md](SECURITY.md)。版本更新需单独发布到 npm,GitHub 推送不会自动更新 npm 安装包。
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 安全边界(Node/npm)
|
|
2
|
+
|
|
3
|
+
当前源码为 `0.1.0-alpha.3`,npm 包名为 `@quecue/dev-notify`;源码更新不代表已发布该版本。未宣称完成安全审计或双平台实机验收。Python 原型说明另见 `docs/PYTHON_SECURITY.md`。
|
|
4
|
+
|
|
5
|
+
## 凭据与权限
|
|
6
|
+
|
|
7
|
+
- Node 系统凭据使用固定版本 `@napi-rs/keyring`:Windows Credential Manager / macOS Keychain。秘密通过进程内原生接口传递,不使用 shell、Python、子进程参数或明文落盘。
|
|
8
|
+
- Node service 为 `dev-notify.node.credentials.v1`,每份凭据使用随机 ID。JSON 只保存 `keyring` 引用,不枚举其他系统条目。
|
|
9
|
+
- 环境变量优先且显式空值也覆盖。环境变量和系统凭据都不能防御已被控制的同用户进程;秘密会进入 Node 内存,不能保证所有副本被安全清零。
|
|
10
|
+
- Keychain 可能请求系统授权;锁定、权限不足、原生库加载失败明确报错,不自动降级明文。提供预编译包不代表 macOS 权限流程已验收。
|
|
11
|
+
- Python 的 `system` 引用与 Node 存储隔离,不读取、不删除旧 Python 凭据;重新输入后的 Node 引用不能交给旧 Python 原型编辑。
|
|
12
|
+
- 交互秘密使用 raw mode 隐藏输入,异常/Ctrl+C/EOF 恢复终端状态;无法隐藏输入即拒绝,不回退回显。原生读取不保证无系统弹窗。
|
|
13
|
+
|
|
14
|
+
## 配置事务与清理
|
|
15
|
+
|
|
16
|
+
- 最终确认前不写配置和凭据。确认后先创建新随机条目并校验,再切换 JSON,成功后删除旧 Node 条目。
|
|
17
|
+
- 配置切换失败保留原配置;只尝试清理本次创建的条目,清理失败只输出非敏感 ID。
|
|
18
|
+
- 使用排他 `.setup.lock` 和内容快照防止多个向导互相覆盖;外部编辑器不遵守锁,保存期间不要手工编辑。
|
|
19
|
+
- 存储和 JSON 不是跨系统原子事务。强制结束/断电可能留下锁、临时文件或孤立凭据,程序不扫描并删除未知数据。
|
|
20
|
+
- `credentials clear` 需要交互确认,移除当前引用并删除其 Node 条目;共享引用的配置副本会受影响,操作前告知。环境变量不会清除,删除失败明确警告。
|
|
21
|
+
- POSIX mode 不能代替 Windows ACL,非敏感配置仍可能暴露接收人和本机路径。
|
|
22
|
+
|
|
23
|
+
## 网络与应用边界
|
|
24
|
+
|
|
25
|
+
- 非 dry-run 的 `send`、向导保存后**另行同意**的固定测试消息,以及**明确启用 Claude Code 集成后**收到的 Stop / 指定 Notification 事件会发送通知。
|
|
26
|
+
- 向导测试使用环境覆盖后的最终配置;保存成功与测试失败分别报告,不伪装为全部成功。
|
|
27
|
+
- HTTP 启用系统 TLS 验证、禁止重定向,超时 10 秒,响应大小最多 1 MiB;底层 URL、秘密、服务端原始正文不打印。
|
|
28
|
+
- 网络超时可能发生服务端已接受但客户端未知,不承诺恰好一次,不能为避免失败而无限重发。
|
|
29
|
+
- 接收人环境变量完整替换旧名单,不隐式残留旧授权。
|
|
30
|
+
- npm 安装不修改应用设置或启动服务。通知集成与远程回复是两层独立授权;只安装 hooks 不授予输入权。未授权时只处理最终回复、指定等待通知和辅助元数据。
|
|
31
|
+
- 启用回复后允许可信 Telegram 私聊用户提交本机 Claude 输入,包括 Claude 命令,具有实际控制风险。不是任意 shell 执行器,也不能把这种权限描述为只读。不开放公网监听、不扫描 transcript、项目正文或版本控制信息;读取本地 Claude 会话进程元数据定位目标。
|
|
32
|
+
- `doctor`、`status`、`paths` 和 dry-run 不联网;本地凭据读取可能触发系统授权。
|
|
33
|
+
|
|
34
|
+
## Claude Code 集成
|
|
35
|
+
|
|
36
|
+
- `setup` 提供可选启用入口;安装前展示发送范围、目标配置及用户级设置路径并确认。默认写入 `~/.claude/settings.json`,尊重绝对路径 `CLAUDE_CONFIG_DIR`。启用后作用于该配置目录下的所有项目,最终回复可能含密钥或业务敏感内容,程序不承诺自动脱敏;接收人与发送范围需用户自行确认。
|
|
37
|
+
- hook 直接使用 `last_assistant_message`,不回读 transcript、不上传思考或工具结果;缺少正文只发送明确的完成提示。输入最大 8 MiB、读取超时 15 秒,超限拒绝不截断。权限等 Notification 也可能含敏感内容。
|
|
38
|
+
- 生成 `command` + `args` 的无 shell exec 形式,路径不经 shell 展开;固定安装时 Node/CLI/配置绝对路径,命令参数不含秘密。需要支持该形式与 `async` 的新版 Claude Code;切换 Node 或移动安装目录后重新安装 hooks。
|
|
39
|
+
- 保留其他设置和 hooks,重复安装去重更新,移除仅匹配本工具标记及参数结构。原 Python、项目或插件 hooks 不自动删除,可能重复发送。`status` 不是全局策略或实际事件投递验收;不会解除禁用、降低权限或绕过托管限制。
|
|
40
|
+
- 写入使用专用排他锁、前后快照及同目录临时文件原子替换;拒绝损坏 JSON。外部编辑器不遵守锁,不能宣称无条件消除竞态;强杀可能留锁或临时文件,程序不清理不属于本次创建的锁。
|
|
41
|
+
- Notification 异步,Stop 与会话生命周期同步,状态登记后再发送,可能延迟本地返回。PreToolUse 仅登记调用身份,不发网络通知或等待答案;PermissionRequest 与普通交互终端原生窗口并行等待,最多 5 分钟,不默认选择。新增 PostToolUse、PostToolUseFailure、PermissionDenied 处理结果,完整安装共 10 类事件。当前核对的是 Claude Code 2.1.316 实现,不保证旧版及特殊宿主采用同一并行路径。
|
|
42
|
+
- 无持久通知发送队列、无整条自动重发或跨进程恰好一次保证。继续中的 Stop 和重复空闲提醒被忽略;内部出错仅输出安全摘要并退出 0,不用退出 2 让 Claude 继续。只有明确授权的结构化交互返回对应官方控制 JSON。
|
|
43
|
+
- 移除 hooks、清理通知凭据、卸载 npm 是不同操作。卸载前先单独移除 hooks;不在卸载脚本中擅自修改用户设置。
|
|
44
|
+
|
|
45
|
+
## 回复、交互与本地状态
|
|
46
|
+
|
|
47
|
+
- `reply setup` 单独选择明确的私聊 chat_id,必须在当前通知名单中;群聊、非私聊、发送者与 chat_id 不一致、Bot 消息和未授权通知接收人均不获输入权。飞书只提供 Telegram deep link,知道链接不构成授权。`/chatid` 只返回发件人本人的私聊 ID,不授予权限。
|
|
48
|
+
- 授权绑定 Bot 指纹、随机代次、当前有效用户及可选的权限能力。切换 Token、撤销用户或重新授权不继承旧路由;操作前及提交前重读授权。撤销不能撤回已经开始的原生输入或消除检查与调用之间的全部竞态。
|
|
49
|
+
- 每次读取授权都检查绑定当前通知配置的完整 hooks,缺生命周期、Stop 异步或用户级禁用时拒绝回复;普通输入还要求实际观察到该会话 UserPromptSubmit。用户级文件检查不能证明项目/托管策略或运行会话已加载,须在 /hooks 确认;不承诺消除所有运行时竞态。
|
|
50
|
+
- 消息每段发送成功后保存消息 ID 到会话映射;映射失败报告部分失败,不重发已发送段。会话必须匹配 PID/出生标识/TTY;会话切换、关闭、目标不唯一时拒绝。支持范围仅 Windows 原生 claude 控制台、macOS 前台 claude 的 Ghostty/tmux;不对 WSL/SSH/VS Code 等猜测路由。
|
|
51
|
+
- 普通回复工作中只排队,Stop 后再尝试;5 分钟过期、本地新输入或目标变化取消。单条最多 4000 Unicode 字符,清理控制字符/换行后一次输入;不按 shell 执行、不截断、不自动拆任务。控制台共享且有多个 claude 进程时拒绝;本地用户同时编辑/切换终端仍可能竞争,不能声称原生输入与 UI 活动原子互斥。
|
|
52
|
+
- update offset 在处理前落盘;输入先登记 pending,再调用原生适配器。强杀可能丢消息,但不重放已提交或结果未知的输入。普通回复原文在提交前移除;仅匹配 UserPromptSubmit 才向原私聊及已配置飞书报告收到,飞书确认不附回复原文。不把「转发」当作「收到」或「完成」。
|
|
53
|
+
- AskUserQuestion 使用原始 questions + answers,通过 PermissionRequest decision.updatedInput 回传;保留本机原生界面,单选、多选、逐题自定义不经终端注入。由 Claude 仲裁先到的完整答案。每个按钮带请求/题目序号,过期或旧题不回答新题;超时/撤销不默认作答。
|
|
54
|
+
- PreToolUse 的 tool_use_id 与输入摘要用于关联;PermissionRequest 无该 ID,只接受唯一匹配,歧义、重复同内容、缺失或已结束时拒绝远程处理。完成事件按 tool_use_id 关联,不依靠通知顺序猜目标。
|
|
55
|
+
- 通用 permission_prompt 去重只用于通知投递,不授予输入或批准权限:仅当前会话最后一项调用是选择题、无其他未结束调用、结构化通知完整成功时,按接收人覆盖;飞书另绑定当前 webhook/签名配置的摘要。已结束题目可在原交互有效期内覆盖迟到提醒,新输入及后续工具调用不继承。Notification 缺少精确调用身份时以该保守会话范围判断,不声称跨并发/跨轮次精确去重;状态异常、投递失败或关联不明确时保留通用提醒。
|
|
56
|
+
- Telegram 登记反馈不等于生效。PostToolUse 返回的实际答案用于内容确认;两端同时提交相同答案不能证明来源。拒绝/失败、取消、缺少结果或回传后 5 分钟确认超时,不能证明实际决定时明确结果未知,不重放答案。结果收据在发送前登记,网络失败/强杀可能未送达;不无限重发、不保证恰好一次。撤销授权后不再发送。旧选项替换为已处理/过期状态;编辑失败时仍拒绝旧请求。
|
|
57
|
+
- 问题结束后清除请求输入和答案正文,短期保留消息 ID、结果收据及输入摘要;未收到结果时保留至确认期限,实际清理由活跃桥接或生命周期事件触发。
|
|
58
|
+
- 工具权限必须额外 opt-in;每次点击才输出 PermissionRequest 的一次 allow/deny。自定义文本只作为拒绝 message,不将任意文本变成批准。禁止持久写 updatedPermissions 或更改 bypass/权限模式;不覆盖 Claude deny/托管策略。permission_suggestions 不是完整 UI 列表,不伪造所有本地按钮。MCP 表单/URL 等尚不提供远程应答。
|
|
59
|
+
- 问题、选项、工具参数、回复正文和答案可能包含敏感信息;启用后问题和相应请求会发送到授权私聊及已配置飞书,不能保证自动脱敏。排队文本、请求输入、待回传答案及会话路径会持久写入专属本地状态;0600/0700 不能代替 Windows ACL,也不能防御同用户进程。逻辑 TTL 不等于断电后的自动擦除。
|
|
60
|
+
- 专属文件为 `<config>.reply.json`、`<config>.reply-data/` 与数据目录 `reply-bots/`,已加入忽略规则且不在 npm 白名单中。禁用不擅自删除历史状态;需清除时先禁用并停止,再确认删除专属文件。Bot Token 只在系统凭据/环境/内存中,不写到回复状态。
|
|
61
|
+
- 桥接不监听端口、不配置系统自启动。用户授权后手动或 hook 按需启动,24 小时无更新退出;连续轮询失败 5 次或 409 冲突退出。不删除 webhook,不强杀旧 Python 或其他实例;必须由用户保证跨机器、跨实现的 Bot 独占。本工具锁只能约束遵守相同本地锁目录的实例。
|
|
62
|
+
- stop 用随机实例标识发送控制请求,不依据旧 PID 杀进程;stop 不撤销授权,后续 hook 可重启。彻底关闭使用 disable,原生调用已开始时不能撤回。心跳/PID 是状态快照,不是网络验收;崩溃残留锁需先核实进程退出后人工处理,不自动抢锁。
|
|
63
|
+
|
|
64
|
+
## npm 与测试
|
|
65
|
+
|
|
66
|
+
- 运行时有原生凭据依赖,版本和平台 optional 包锁定于 `package-lock.json`。不要省略 optional 依赖。项目无 postinstall、自写二进制下载、后台启动脚本。
|
|
67
|
+
- npm 白名单仅包含编译 JS、README、SECURITY、CHANGELOG、LICENSE 和 package.json。测试、Python、配置、日志、密文不进入本包。
|
|
68
|
+
- 第三方原生包带有其许可证与 Rust 依赖声明,公开前仍需审查依赖供应链和许可;不能把固定版本等同于完成审计。
|
|
69
|
+
- 默认测试用假凭据/临时目录和模拟系统后端、网络。`DEV_NOTIFY_NATIVE_TEST=1` 显式开启 Windows 原生测试,仅创建本次随机 ID 的假条目,结束清理,不读取用户现有条目。
|
|
70
|
+
- npm 卸载程序不自动删除用户配置/凭据。需要清理时先运行交互清理命令,不在卸载生命周期脚本偷偷操作系统存储。
|
|
71
|
+
|
|
72
|
+
## 公开发布
|
|
73
|
+
|
|
74
|
+
核对包名、源码权属、依赖许可;扫描完整目录、实际 npm 包和 Git 历史。密钥泄漏必须撤销轮换,删除文件不能撤销泄漏。正式公开前应设置私密漏洞报告渠道;当前没有公开安全联络地址。
|
|
75
|
+
|
|
76
|
+
Windows x64 / Node 24 已做隔离测试及真实 CLI 假网络闭环;自动化中的 Windows 原生输入帮助类仅编译/布局验证,不向真实 Claude 会话注入。用户另在 Claude Code 2.1.316 与真实 Telegram 完成了 PC/远端优先、单选/多选/自定义、不同答案竞争及重复提醒去重的实机验收;该问答路径不使用终端输入注入。工具权限应答实机、macOS 终端输入与系统权限流程、其他架构及 Node 最低版本仍未验证,不宣称全面兼容。
|
package/dist/channels.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { escapeHtml, feishuParts, parseMarkdown, partTitle, telegramParts } from './formatting.js';
|
|
4
|
+
export class TransportError extends Error {
|
|
5
|
+
}
|
|
6
|
+
const icons = { info: 'ℹ️', success: '✅', warn: '⚠️', error: '❌' };
|
|
7
|
+
const colors = { info: 'blue', success: 'green', warn: 'yellow', error: 'red' };
|
|
8
|
+
const bytes = (data) => Buffer.byteLength(JSON.stringify(data), 'utf8');
|
|
9
|
+
export async function requestJson(url, payload, fetcher = fetch) {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetcher(url, { method: 'POST', body: JSON.stringify(payload),
|
|
12
|
+
headers: { 'Content-Type': 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(10000) });
|
|
13
|
+
if (!response.body)
|
|
14
|
+
throw new TransportError('服务端响应为空');
|
|
15
|
+
const reader = response.body.getReader();
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let size = 0;
|
|
18
|
+
try {
|
|
19
|
+
while (true) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done)
|
|
22
|
+
break;
|
|
23
|
+
size += value.byteLength;
|
|
24
|
+
if (size > 1024 * 1024) {
|
|
25
|
+
await reader.cancel();
|
|
26
|
+
throw new TransportError('服务端响应超过大小限制');
|
|
27
|
+
}
|
|
28
|
+
chunks.push(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
}
|
|
34
|
+
const result = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
35
|
+
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
36
|
+
throw new TransportError('服务端未返回 JSON 对象');
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof TransportError)
|
|
41
|
+
throw error;
|
|
42
|
+
throw new TransportError('网络请求失败或响应无效;未自动重发,投递状态可能未知');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function text(title, content, footer, icon, html) {
|
|
46
|
+
const sections = [`${icon} ${html ? `<b>${escapeHtml(title)}</b>` : title}`];
|
|
47
|
+
if (content)
|
|
48
|
+
sections.push(content);
|
|
49
|
+
if (footer)
|
|
50
|
+
sections.push('────────\n' + (html ? `<i>${escapeHtml(footer)}</i>` : footer));
|
|
51
|
+
return sections.join('\n\n');
|
|
52
|
+
}
|
|
53
|
+
function card(message, title, content) {
|
|
54
|
+
const elements = [];
|
|
55
|
+
if (content)
|
|
56
|
+
elements.push({ tag: 'markdown', content });
|
|
57
|
+
if (message.footer)
|
|
58
|
+
elements.push({ tag: 'hr' }, { tag: 'note', elements: [{ tag: 'plain_text', content: message.footer }] });
|
|
59
|
+
if (message.reply?.url)
|
|
60
|
+
elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: '💬 在 Telegram 回复 Claude' }, url: message.reply.url, type: 'primary' }] });
|
|
61
|
+
return { msg_type: 'interactive', card: { config: { wide_screen_mode: true },
|
|
62
|
+
header: { template: colors[message.level], title: { tag: 'plain_text', content: title } }, elements } };
|
|
63
|
+
}
|
|
64
|
+
export function makePlan(channel, message, config) {
|
|
65
|
+
if (!message.title.trim())
|
|
66
|
+
throw new Error('通知标题不能为空');
|
|
67
|
+
const blocks = parseMarkdown(message.content);
|
|
68
|
+
if (channel === 'telegram') {
|
|
69
|
+
const icon = icons[message.level];
|
|
70
|
+
const reserve = String(Math.max(1, message.content.length)).length * 2 + 16;
|
|
71
|
+
const budget = 3900 - text(message.title, '', message.footer, icon, false).length - reserve;
|
|
72
|
+
const chunks = telegramParts(blocks, budget);
|
|
73
|
+
const items = [];
|
|
74
|
+
for (const chatId of config.telegram.chatIds)
|
|
75
|
+
chunks.forEach((chunk, index) => {
|
|
76
|
+
const title = partTitle(message.title, index, chunks.length);
|
|
77
|
+
const plain = text(title, chunk.plain, message.footer, icon, false);
|
|
78
|
+
if (plain.length > 4096)
|
|
79
|
+
throw new Error('Telegram 分段超过消息限制');
|
|
80
|
+
items.push({ recipient: chatId, plain, payload: { chat_id: chatId,
|
|
81
|
+
text: text(title, chunk.html, message.footer, icon, true), parse_mode: 'HTML', disable_web_page_preview: true,
|
|
82
|
+
...(message.reply?.users.includes(chatId) ? { reply_markup: { inline_keyboard: [[{ text: '💬 回复 Claude', callback_data: 'dn:r:' + message.reply.token }]] } } : {}) } });
|
|
83
|
+
});
|
|
84
|
+
return { channel, items, recipients: config.telegram.chatIds.length };
|
|
85
|
+
}
|
|
86
|
+
const budget = Math.min(10000, Math.floor((28000 - bytes(card(message, message.title, '')) - 512) / 6));
|
|
87
|
+
const chunks = feishuParts(blocks, budget);
|
|
88
|
+
const items = chunks.map((chunk, index) => {
|
|
89
|
+
const payload = card(message, partTitle(message.title, index, chunks.length), chunk);
|
|
90
|
+
if (bytes(payload) + 128 > 28000)
|
|
91
|
+
throw new Error('飞书分段超过请求大小限制');
|
|
92
|
+
return { recipient: 'feishu', payload };
|
|
93
|
+
});
|
|
94
|
+
return { channel, items, recipients: 1 };
|
|
95
|
+
}
|
|
96
|
+
export async function deliver(plan, config, request = requestJson, sleep = delay) {
|
|
97
|
+
let sent = 0, error;
|
|
98
|
+
const failed = new Set();
|
|
99
|
+
const telegram = plan.channel === 'telegram';
|
|
100
|
+
const url = telegram ? `https://api.telegram.org/bot${config.telegram.token}/sendMessage` : config.feishu.webhook;
|
|
101
|
+
const post = async (payload) => {
|
|
102
|
+
if (!telegram && config.feishu.secret) {
|
|
103
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
104
|
+
payload.timestamp = timestamp;
|
|
105
|
+
payload.sign = createHmac('sha256', `${timestamp}\n${config.feishu.secret}`).update('').digest('base64');
|
|
106
|
+
}
|
|
107
|
+
let response = await request(url, payload);
|
|
108
|
+
if (telegram && response.error_code === 429) {
|
|
109
|
+
const wait = response.parameters?.retry_after;
|
|
110
|
+
if (typeof wait === 'number' && Number.isFinite(wait) && wait > 0 && wait <= 60) {
|
|
111
|
+
await sleep(wait * 1000);
|
|
112
|
+
response = await request(url, payload);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return response;
|
|
116
|
+
};
|
|
117
|
+
for (let index = 0; index < plan.items.length; index++) {
|
|
118
|
+
const item = plan.items[index];
|
|
119
|
+
if (failed.has(item.recipient))
|
|
120
|
+
continue;
|
|
121
|
+
const payload = structuredClone(item.payload);
|
|
122
|
+
try {
|
|
123
|
+
let response = await post(payload);
|
|
124
|
+
if (telegram && response.ok !== true && /parse|entit/i.test(String(response.description ?? ''))) {
|
|
125
|
+
delete payload.parse_mode;
|
|
126
|
+
payload.text = item.plain;
|
|
127
|
+
response = await post(payload);
|
|
128
|
+
}
|
|
129
|
+
else if (!telegram && response.code !== 0) {
|
|
130
|
+
let changed = false;
|
|
131
|
+
payload.card.elements = payload.card.elements.map((element) => {
|
|
132
|
+
if (element.tag !== 'markdown')
|
|
133
|
+
return element;
|
|
134
|
+
changed = true;
|
|
135
|
+
return { tag: 'div', text: { tag: 'lark_md', content: element.content } };
|
|
136
|
+
});
|
|
137
|
+
if (changed)
|
|
138
|
+
response = await post(payload);
|
|
139
|
+
}
|
|
140
|
+
if (telegram ? response.ok === true : response.code === 0) {
|
|
141
|
+
sent++;
|
|
142
|
+
if (plan.onSent)
|
|
143
|
+
await plan.onSent(item, response);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
failed.add(item.recipient);
|
|
147
|
+
error = '渠道拒绝发送,请检查凭据、接收人授权、签名或频率限制';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (cause) {
|
|
151
|
+
failed.add(item.recipient);
|
|
152
|
+
error = cause instanceof TransportError ? cause.message : '发送失败;投递状态可能未知';
|
|
153
|
+
}
|
|
154
|
+
if (!telegram && !failed.has(item.recipient) && index + 1 < plan.items.length)
|
|
155
|
+
await sleep(250);
|
|
156
|
+
}
|
|
157
|
+
return { channel: plan.channel, sent, planned: plan.items.length, ok: sent > 0 && sent === plan.items.length && !error, error };
|
|
158
|
+
}
|