@quecue/dev-notify 0.1.0-alpha.1 → 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/README.md +103 -8
- package/SECURITY.md +36 -5
- package/dist/channels.js +8 -2
- package/dist/claude-code.js +236 -0
- package/dist/claude-hook.js +123 -0
- package/dist/cli.js +110 -17
- 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 +1 -1
- package/package.json +10 -1
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/README.md
CHANGED
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
跨平台开发任务通知工具,使用 **TypeScript / Node.js**,通过 npm 分发,无需 Python 或独立可执行文件。通用通知不依赖 Claude Code、Unity 或项目目录。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**npm 包名为 `@quecue/dev-notify`。`0.1.0-alpha.3` 提供 Claude Code 回复、原生 PC 与 Telegram 双端答题及结果反馈。** 版本变更与升级说明见 [CHANGELOG.md](CHANGELOG.md);公开版本以 npm registry 为准。
|
|
6
6
|
|
|
7
7
|
## 安装与使用
|
|
8
8
|
|
|
9
9
|
要求 Node.js 22 或以上,首发目标 Windows、macOS。运行时依赖锁定版本的 `@napi-rs/keyring`,由 npm 安装对应系统的预编译原生组件,不需要 Python、Rust 或 C++ 编译器。不要省略 npm optional 依赖。
|
|
10
10
|
|
|
11
|
-
|
|
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` 路径。源码开发可在工程目录构建后执行:
|
|
12
20
|
|
|
13
21
|
```sh
|
|
14
22
|
npm ci --ignore-scripts
|
|
@@ -37,9 +45,14 @@ node dist/cli.js send --title "分段预览" --content-file result.txt --dry-run
|
|
|
37
45
|
- 交互配置、隐藏输入、系统凭据存储、环境变量覆盖。
|
|
38
46
|
- 配置更新冲突检测、失败恢复、凭据清理。
|
|
39
47
|
- 可选测试通知:只有额外确认后才发送,默认不发送。
|
|
40
|
-
-
|
|
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` 管理桥接。
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
**通用通知不依赖 Claude Code。**npm 安装本身不修改其他应用配置,也不启动服务。通知 hooks 和远程输入授权分开,默认只通知;授权后才允许回复桥接轮询 Telegram。没有自动配对、任意远程 shell 执行器或系统开机自启。
|
|
43
56
|
|
|
44
57
|
## 配置向导
|
|
45
58
|
|
|
@@ -52,6 +65,8 @@ node dist/cli.js setup
|
|
|
52
65
|
3. Telegram 手动填写数字 chat_id,多个用逗号分隔;用户需要先启动 Bot。不会自动启动 getUpdates 配对。
|
|
53
66
|
4. 展示渠道和接收人数,最终确认后保存。输入错误可以重试,Ctrl+C 取消。
|
|
54
67
|
5. 保存后单独询问是否发送固定内容的测试通知。测试失败不会撤销已成功保存的配置。
|
|
68
|
+
6. 另行询问是否配置 Claude Code 自动通知;展示用户级设置路径、正文发送范围后,再次确认才合并 hooks。拒绝时已有集成保持原样,不会被禁用。
|
|
69
|
+
7. Telegram 可用时另外询问是否授权回复;选择具体可信私聊用户、是否允许逐次处理工具权限,再确认。拒绝不改变已有授权,不自动启用全部通知接收人。
|
|
55
70
|
|
|
56
71
|
编辑已有配置需要先确认;密钥留空保留已存值,可选签名输入 `-` 清除。未选渠道会禁用,新条目只保留所选渠道的凭据。环境变量仍然优先;向导会提示存在覆盖,测试使用最终生效值,而不是忽略环境的刚输入值。
|
|
57
72
|
|
|
@@ -63,6 +78,86 @@ node dist/cli.js setup --template --yes
|
|
|
63
78
|
|
|
64
79
|
模板永不覆盖已有配置。`--yes` 只能配合 `--template`,不能跳过交互密钥保存确认。
|
|
65
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
|
+
|
|
66
161
|
## 系统凭据与兼容性
|
|
67
162
|
|
|
68
163
|
Node 版使用 **Windows Credential Manager / macOS Keychain**,通过原生 Node API 传递秘密,不调用 Python,不将秘密放进命令行参数,不写入明文 JSON。
|
|
@@ -132,7 +227,7 @@ node dist/cli.js credentials clear
|
|
|
132
227
|
| 2 | 参数、配置、文件或凭据操作错误 |
|
|
133
228
|
| 130 | 用户取消 |
|
|
134
229
|
|
|
135
|
-
|
|
230
|
+
向导保存成功后测试通知失败,命令仍以保存成功结束,并明确显示测试失败,两者不混淆。保存后取消 hook 配置仍返回 0 并保留渠道;hook 配置出错返回 1,明确提示渠道已保存。集成 `status` 在条目缺失、重复或当前用户级文件禁用 hooks 时返回 1。内部 hook 处理命令始终非阻塞退出,失败通过 stderr 报告。
|
|
136
231
|
|
|
137
232
|
## 开发与验证
|
|
138
233
|
|
|
@@ -144,8 +239,8 @@ npm pack --dry-run
|
|
|
144
239
|
|
|
145
240
|
`node-src/` 是 TypeScript 源码,`dist/` 是编译输出,`test-js/` 是测试。默认测试只用假凭据和模拟后端/网络;本机原生假条目往返测试需显式设置 `DEV_NOTIFY_NATIVE_TEST=1`,且只在 Windows 启用,使用随机 ID,结束后清理。
|
|
146
241
|
|
|
147
|
-
npm 发布白名单只有 `dist/*.js`、README、SECURITY、LICENSE 和必需的 package.json;不包含 Python、测试、配置或密文。无 postinstall、自动启动和自写下载脚本;预编译原生依赖由 npm 的平台 optional 包解析,**不应使用 `--omit=optional`**。macOS x64/arm64 和 Windows x64/arm64 预编译包由依赖提供,提供包不等于本项目已实机验收。
|
|
242
|
+
npm 发布白名单只有 `dist/*.js`、README、SECURITY、CHANGELOG、LICENSE 和必需的 package.json;不包含 Python、测试、配置或密文。无 postinstall、自动启动和自写下载脚本;预编译原生依赖由 npm 的平台 optional 包解析,**不应使用 `--omit=optional`**。macOS x64/arm64 和 Windows x64/arm64 预编译包由依赖提供,提供包不等于本项目已实机验收。
|
|
148
243
|
|
|
149
|
-
本机验证环境为 Windows x64、Node 24。macOS 实机、Node
|
|
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 最低版本、版本管理器切换、工具权限应答实机及完整公开发布审计仍未验收。
|
|
150
245
|
|
|
151
|
-
采用 MIT,见 [LICENSE](LICENSE)。安全边界见 [SECURITY.md](SECURITY.md)
|
|
246
|
+
采用 MIT,见 [LICENSE](LICENSE)。安全边界见 [SECURITY.md](SECURITY.md)。版本更新需单独发布到 npm,GitHub 推送不会自动更新 npm 安装包。
|
package/SECURITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 安全边界(Node/npm)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
当前源码为 `0.1.0-alpha.3`,npm 包名为 `@quecue/dev-notify`;源码更新不代表已发布该版本。未宣称完成安全审计或双平台实机验收。Python 原型说明另见 `docs/PYTHON_SECURITY.md`。
|
|
4
4
|
|
|
5
5
|
## 凭据与权限
|
|
6
6
|
|
|
@@ -22,18 +22,49 @@
|
|
|
22
22
|
|
|
23
23
|
## 网络与应用边界
|
|
24
24
|
|
|
25
|
-
-
|
|
25
|
+
- 非 dry-run 的 `send`、向导保存后**另行同意**的固定测试消息,以及**明确启用 Claude Code 集成后**收到的 Stop / 指定 Notification 事件会发送通知。
|
|
26
26
|
- 向导测试使用环境覆盖后的最终配置;保存成功与测试失败分别报告,不伪装为全部成功。
|
|
27
27
|
- HTTP 启用系统 TLS 验证、禁止重定向,超时 10 秒,响应大小最多 1 MiB;底层 URL、秘密、服务端原始正文不打印。
|
|
28
28
|
- 网络超时可能发生服务端已接受但客户端未知,不承诺恰好一次,不能为避免失败而无限重发。
|
|
29
29
|
- 接收人环境变量完整替换旧名单,不隐式残留旧授权。
|
|
30
|
-
-
|
|
30
|
+
- npm 安装不修改应用设置或启动服务。通知集成与远程回复是两层独立授权;只安装 hooks 不授予输入权。未授权时只处理最终回复、指定等待通知和辅助元数据。
|
|
31
|
+
- 启用回复后允许可信 Telegram 私聊用户提交本机 Claude 输入,包括 Claude 命令,具有实际控制风险。不是任意 shell 执行器,也不能把这种权限描述为只读。不开放公网监听、不扫描 transcript、项目正文或版本控制信息;读取本地 Claude 会话进程元数据定位目标。
|
|
31
32
|
- `doctor`、`status`、`paths` 和 dry-run 不联网;本地凭据读取可能触发系统授权。
|
|
32
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
|
+
|
|
33
64
|
## npm 与测试
|
|
34
65
|
|
|
35
66
|
- 运行时有原生凭据依赖,版本和平台 optional 包锁定于 `package-lock.json`。不要省略 optional 依赖。项目无 postinstall、自写二进制下载、后台启动脚本。
|
|
36
|
-
- npm 白名单仅包含编译 JS、README、SECURITY、LICENSE 和 package.json。测试、Python、配置、日志、密文不进入本包。
|
|
67
|
+
- npm 白名单仅包含编译 JS、README、SECURITY、CHANGELOG、LICENSE 和 package.json。测试、Python、配置、日志、密文不进入本包。
|
|
37
68
|
- 第三方原生包带有其许可证与 Rust 依赖声明,公开前仍需审查依赖供应链和许可;不能把固定版本等同于完成审计。
|
|
38
69
|
- 默认测试用假凭据/临时目录和模拟系统后端、网络。`DEV_NOTIFY_NATIVE_TEST=1` 显式开启 Windows 原生测试,仅创建本次随机 ID 的假条目,结束清理,不读取用户现有条目。
|
|
39
70
|
- npm 卸载程序不自动删除用户配置/凭据。需要清理时先运行交互清理命令,不在卸载生命周期脚本偷偷操作系统存储。
|
|
@@ -42,4 +73,4 @@
|
|
|
42
73
|
|
|
43
74
|
核对包名、源码权属、依赖许可;扫描完整目录、实际 npm 包和 Git 历史。密钥泄漏必须撤销轮换,删除文件不能撤销泄漏。正式公开前应设置私密漏洞报告渠道;当前没有公开安全联络地址。
|
|
44
75
|
|
|
45
|
-
|
|
76
|
+
Windows x64 / Node 24 已做隔离测试及真实 CLI 假网络闭环;自动化中的 Windows 原生输入帮助类仅编译/布局验证,不向真实 Claude 会话注入。用户另在 Claude Code 2.1.316 与真实 Telegram 完成了 PC/远端优先、单选/多选/自定义、不同答案竞争及重复提醒去重的实机验收;该问答路径不使用终端输入注入。工具权限应答实机、macOS 终端输入与系统权限流程、其他架构及 Node 最低版本仍未验证,不宣称全面兼容。
|
package/dist/channels.js
CHANGED
|
@@ -56,6 +56,8 @@ function card(message, title, content) {
|
|
|
56
56
|
elements.push({ tag: 'markdown', content });
|
|
57
57
|
if (message.footer)
|
|
58
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' }] });
|
|
59
61
|
return { msg_type: 'interactive', card: { config: { wide_screen_mode: true },
|
|
60
62
|
header: { template: colors[message.level], title: { tag: 'plain_text', content: title } }, elements } };
|
|
61
63
|
}
|
|
@@ -76,7 +78,8 @@ export function makePlan(channel, message, config) {
|
|
|
76
78
|
if (plain.length > 4096)
|
|
77
79
|
throw new Error('Telegram 分段超过消息限制');
|
|
78
80
|
items.push({ recipient: chatId, plain, payload: { chat_id: chatId,
|
|
79
|
-
text: text(title, chunk.html, message.footer, icon, true), parse_mode: 'HTML', disable_web_page_preview: true
|
|
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 }]] } } : {}) } });
|
|
80
83
|
});
|
|
81
84
|
return { channel, items, recipients: config.telegram.chatIds.length };
|
|
82
85
|
}
|
|
@@ -134,8 +137,11 @@ export async function deliver(plan, config, request = requestJson, sleep = delay
|
|
|
134
137
|
if (changed)
|
|
135
138
|
response = await post(payload);
|
|
136
139
|
}
|
|
137
|
-
if (telegram ? response.ok === true : response.code === 0)
|
|
140
|
+
if (telegram ? response.ok === true : response.code === 0) {
|
|
138
141
|
sent++;
|
|
142
|
+
if (plan.onSent)
|
|
143
|
+
await plan.onSent(item, response);
|
|
144
|
+
}
|
|
139
145
|
else {
|
|
140
146
|
failed.add(item.recipient);
|
|
141
147
|
error = '渠道拒绝发送,请检查凭据、接收人授权、签名或频率限制';
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { readFile, mkdir, open, rename, unlink } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { ConfigError, decodeUtf8 } from './config.js';
|
|
7
|
+
export const HOOK_MARKER = 'dev-notify:claude-code:v1';
|
|
8
|
+
export const NOTIFICATION_MATCHER = 'permission_prompt|elicitation_dialog|elicitation_url_dialog|agent_needs_input';
|
|
9
|
+
export const EVENTS = ['Stop', 'Notification', 'SessionStart', 'UserPromptSubmit', 'SessionEnd', 'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PostToolUseFailure', 'PermissionDenied'];
|
|
10
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
const yes = (value) => /^(y|yes)$/i.test(value.trim());
|
|
12
|
+
export function claudeSettingsPath(env = process.env, home = homedir(), platform = process.platform) {
|
|
13
|
+
const join = platform === 'win32' ? path.win32 : path.posix;
|
|
14
|
+
const root = env.CLAUDE_CONFIG_DIR || join.join(home, '.claude');
|
|
15
|
+
if (!join.isAbsolute(root))
|
|
16
|
+
throw new ConfigError('CLAUDE_CONFIG_DIR 必须是绝对路径,未修改 Claude Code 设置');
|
|
17
|
+
return join.join(root, 'settings.json');
|
|
18
|
+
}
|
|
19
|
+
function settingsFile(options) {
|
|
20
|
+
return claudeSettingsPath(options.env, options.home, options.platform);
|
|
21
|
+
}
|
|
22
|
+
async function snapshot(file) {
|
|
23
|
+
try {
|
|
24
|
+
const bytes = await readFile(file);
|
|
25
|
+
if (bytes.length > 1024 * 1024)
|
|
26
|
+
throw new ConfigError('Claude Code 设置超过 1 MiB,未修改');
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT')
|
|
31
|
+
return null;
|
|
32
|
+
if (error instanceof ConfigError)
|
|
33
|
+
throw error;
|
|
34
|
+
throw new ConfigError('无法读取 Claude Code 设置,未修改');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parseSettings(before) {
|
|
38
|
+
let data;
|
|
39
|
+
try {
|
|
40
|
+
data = before === null ? {} : JSON.parse(decodeUtf8(before));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new ConfigError('Claude Code 设置不是合法 UTF-8 JSON,未覆盖');
|
|
44
|
+
}
|
|
45
|
+
if (!record(data) || (data.hooks !== undefined && !record(data.hooks)))
|
|
46
|
+
throw new ConfigError('Claude Code 设置或 hooks 结构无效,未覆盖');
|
|
47
|
+
for (const event of EVENTS) {
|
|
48
|
+
const groups = data.hooks?.[event];
|
|
49
|
+
if (groups === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (!Array.isArray(groups) || groups.some(group => !record(group) || !Array.isArray(group.hooks)
|
|
52
|
+
|| group.hooks.some((hook) => !record(hook))))
|
|
53
|
+
throw new ConfigError('Claude Code hook 数组结构无效,未覆盖');
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
function owned(hook) {
|
|
58
|
+
return hook.type === 'command' && hook.statusMessage === HOOK_MARKER && Array.isArray(hook.args)
|
|
59
|
+
&& hook.args.length === 6 && typeof hook.args[0] === 'string'
|
|
60
|
+
&& hook.args[1] === 'integration' && hook.args[2] === 'claude-code' && hook.args[3] === 'hook'
|
|
61
|
+
&& hook.args[4] === '--config' && typeof hook.args[5] === 'string';
|
|
62
|
+
}
|
|
63
|
+
function describe(file, data) {
|
|
64
|
+
const result = { file, stop: 0, notification: 0, lifecycle: 0, interaction: 0, others: 0, disabled: data.disableAllHooks === true };
|
|
65
|
+
for (const event of EVENTS)
|
|
66
|
+
for (const group of data.hooks?.[event] ?? [])
|
|
67
|
+
for (const hook of group.hooks) {
|
|
68
|
+
if (owned(hook)) {
|
|
69
|
+
if (event === 'Stop')
|
|
70
|
+
result.stop++;
|
|
71
|
+
else if (event === 'Notification')
|
|
72
|
+
result.notification++;
|
|
73
|
+
else if (['PreToolUse', 'PermissionRequest', 'PostToolUse', 'PostToolUseFailure', 'PermissionDenied'].includes(event))
|
|
74
|
+
result.interaction++;
|
|
75
|
+
else
|
|
76
|
+
result.lifecycle++;
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
result.others++;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
export async function claudeCodeStatus(options = {}) {
|
|
84
|
+
const file = settingsFile(options);
|
|
85
|
+
return describe(file, parseSettings(await snapshot(file)));
|
|
86
|
+
}
|
|
87
|
+
export async function replyHooksReady(config, options = {}) {
|
|
88
|
+
const data = parseSettings(await snapshot(settingsFile(options)));
|
|
89
|
+
if (data.disableAllHooks === true)
|
|
90
|
+
return false;
|
|
91
|
+
const normalize = (file) => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
|
|
92
|
+
for (const event of EVENTS) {
|
|
93
|
+
const groups = data.hooks?.[event] ?? [];
|
|
94
|
+
const matches = groups.flatMap((group) => group.hooks)
|
|
95
|
+
.filter((hook) => owned(hook) && normalize(hook.args[5]) === normalize(config));
|
|
96
|
+
if (matches.length !== 1 || (event !== 'Notification' && matches[0].async === true))
|
|
97
|
+
return false;
|
|
98
|
+
if (event !== 'Notification' && groups.some((group) => group.matcher && group.matcher !== '*'
|
|
99
|
+
&& group.hooks.includes(matches[0])))
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
function merged(data, config, runtime) {
|
|
105
|
+
const target = structuredClone(data);
|
|
106
|
+
for (const event of EVENTS) {
|
|
107
|
+
const groups = [];
|
|
108
|
+
for (const group of target.hooks?.[event] ?? []) {
|
|
109
|
+
const remaining = group.hooks.filter((hook) => !owned(hook));
|
|
110
|
+
if (remaining.length === group.hooks.length)
|
|
111
|
+
groups.push(group);
|
|
112
|
+
else if (remaining.length)
|
|
113
|
+
groups.push({ ...group, hooks: remaining });
|
|
114
|
+
}
|
|
115
|
+
if (config !== null) {
|
|
116
|
+
const handler = { type: 'command', command: runtime.node,
|
|
117
|
+
args: [runtime.cli, 'integration', 'claude-code', 'hook', '--config', config],
|
|
118
|
+
...(event === 'Notification' ? { async: true } : { timeout: event === 'PermissionRequest' ? 360 : event === 'PreToolUse' ? 10 : 120 }), statusMessage: HOOK_MARKER };
|
|
119
|
+
const matcher = event === 'Notification' ? NOTIFICATION_MATCHER : null;
|
|
120
|
+
groups.push(matcher ? { matcher, hooks: [handler] } : { hooks: [handler] });
|
|
121
|
+
}
|
|
122
|
+
if (groups.length) {
|
|
123
|
+
target.hooks ??= {};
|
|
124
|
+
target.hooks[event] = groups;
|
|
125
|
+
}
|
|
126
|
+
else if (target.hooks)
|
|
127
|
+
delete target.hooks[event];
|
|
128
|
+
}
|
|
129
|
+
if (target.hooks && !Object.keys(target.hooks).length)
|
|
130
|
+
delete target.hooks;
|
|
131
|
+
return target;
|
|
132
|
+
}
|
|
133
|
+
function same(left, right) {
|
|
134
|
+
return left === null ? right === null : right !== null && left.equals(right);
|
|
135
|
+
}
|
|
136
|
+
async function remove(file) {
|
|
137
|
+
try {
|
|
138
|
+
await unlink(file);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error.code !== 'ENOENT')
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function commit(file, before, target, prompts, replace = rename) {
|
|
146
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
147
|
+
const lockFile = file + '.dev-notify.lock';
|
|
148
|
+
let lock;
|
|
149
|
+
try {
|
|
150
|
+
lock = await open(lockFile, 'wx', 0o600);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new ConfigError('Claude Code 设置写入锁不可用;确认没有安装进程后才能处理残留 .dev-notify.lock');
|
|
154
|
+
}
|
|
155
|
+
const temporary = path.join(path.dirname(file), `.dev-notify-${randomBytes(16).toString('hex')}.tmp`);
|
|
156
|
+
let created = false;
|
|
157
|
+
try {
|
|
158
|
+
await lock.close();
|
|
159
|
+
if (!same(before, await snapshot(file)))
|
|
160
|
+
throw new ConfigError('Claude Code 设置在确认期间已变化,未覆盖,请重试');
|
|
161
|
+
const stream = await open(temporary, 'wx', 0o600);
|
|
162
|
+
created = true;
|
|
163
|
+
try {
|
|
164
|
+
await stream.writeFile(JSON.stringify(target, null, 2) + '\n', 'utf8');
|
|
165
|
+
await stream.sync();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await stream.close();
|
|
169
|
+
}
|
|
170
|
+
if (!same(before, await snapshot(file)))
|
|
171
|
+
throw new ConfigError('Claude Code 设置在保存期间已变化,未覆盖,请重试');
|
|
172
|
+
await replace(temporary, file);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (created) {
|
|
176
|
+
try {
|
|
177
|
+
await remove(temporary);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
prompts.out('临时 Claude Code 设置清理失败,请确认安装结束后清理 .dev-notify-*.tmp');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await remove(lockFile);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
prompts.out('Claude Code 设置锁清理失败,请确认安装结束后处理 .dev-notify.lock');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export async function configureClaudeCode(config, action, prompts, options = {}) {
|
|
192
|
+
const file = settingsFile(options), before = await snapshot(file), data = parseSettings(before);
|
|
193
|
+
const status = describe(file, data);
|
|
194
|
+
if (action === 'remove' && status.stop + status.notification + status.lifecycle + status.interaction === 0) {
|
|
195
|
+
prompts.out(`Claude Code 用户级设置:${file}`);
|
|
196
|
+
prompts.out('没有本工具标记的 hooks,未修改。');
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
const runtime = options.runtime ?? { node: process.execPath, cli: fileURLToPath(new URL('./cli.js', import.meta.url)) };
|
|
200
|
+
const target = merged(data, action === 'install' ? path.resolve(config) : null, runtime);
|
|
201
|
+
prompts.out(`Claude Code 用户级设置:${file}`);
|
|
202
|
+
if (JSON.stringify(data) === JSON.stringify(target)) {
|
|
203
|
+
prompts.out(action === 'install' ? '本工具 hooks 已安装且配置一致,未重复添加。' : '没有本工具标记的 hooks,未修改。');
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (action === 'install') {
|
|
207
|
+
prompts.out(`通知配置:${path.resolve(config)}`);
|
|
208
|
+
prompts.out('将为该 Claude Code 配置目录下的所有项目启用:最终回复通知,以及权限等待等需要输入的通知。');
|
|
209
|
+
prompts.out('最终回复和通知正文可能包含敏感内容,将发送到上述配置实际生效的 Telegram / 飞书目标;环境变量仍可覆盖。');
|
|
210
|
+
prompts.out('同时安装会话生命周期和结构化问题 hooks;未独立运行 reply setup 授权时不接收远程输入、不启动桥接。');
|
|
211
|
+
prompts.out('不读取整份会话记录;启用回复后读取会话进程元数据,结构化问题/权限请求会发送相应问题或工具输入。');
|
|
212
|
+
prompts.out('PreToolUse 只登记身份;PermissionRequest 与普通交互终端原生窗口并行等待,远程 5 分钟有效。安装完成事件用于结果反馈,不默认作答。当前已核对 Claude Code 2.1.316 实现;旧版和特殊宿主需另行验收。');
|
|
213
|
+
if (status.others)
|
|
214
|
+
prompts.out(`保留当前文件中另外 ${status.others} 个相关处理器;可能出现重复通知。`);
|
|
215
|
+
prompts.out('项目级或插件 hooks 也可能同时生效;不会自动迁移或删除原 Python hooks。');
|
|
216
|
+
if (status.disabled)
|
|
217
|
+
prompts.out('注意:该文件设置了 disableAllHooks,安装后仍不会生效;本工具不擅自解除禁用。');
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
prompts.out('只移除本工具标记的通知、生命周期和交互处理器;保留其他设置、hooks、通知配置和系统凭据。如需停止回复桥接,请运行 reply disable / stop。');
|
|
221
|
+
if (!yes(await prompts.text(action === 'install' ? '确认合并安装 Claude Code 通知 hooks?[y/N] ' : '确认移除本工具的 Claude Code hooks?[y/N] '))) {
|
|
222
|
+
prompts.out('已取消,Claude Code 设置未修改。');
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
await commit(file, before, target, prompts, options.replace);
|
|
226
|
+
prompts.out(action === 'install' ? 'Claude Code hooks 已写入。请在 /hooks 中检查并按界面确认;未生效时重启会话。'
|
|
227
|
+
: '本工具的 Claude Code hooks 已移除;请在 /hooks 中检查或重启会话。');
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
export async function offerClaudeCode(config, prompts, options = {}) {
|
|
231
|
+
if (!yes(await prompts.text('是否配置 Claude Code 自动通知?将另行展示用户级设置路径及发送范围。[y/N] '))) {
|
|
232
|
+
prompts.out('未更改 Claude Code hooks;已有集成保持原样。');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await configureClaudeCode(config, 'install', prompts, options);
|
|
236
|
+
}
|