@quecue/dev-notify 0.1.0-alpha.1 → 0.1.0

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