@quecue/dev-notify 0.1.0-alpha.3 → 0.1.1
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 +23 -1
- package/README.md +84 -65
- package/SECURITY.md +46 -46
- package/dist/channels.js +9 -9
- package/dist/claude-code.js +35 -36
- package/dist/claude-hook.js +36 -23
- package/dist/cli.js +91 -67
- package/dist/config.js +16 -16
- package/dist/credentials.js +13 -13
- package/dist/formatting.js +4 -4
- package/dist/prompts.js +6 -6
- package/dist/reply-bridge.js +9 -9
- package/dist/reply-engine.js +23 -17
- package/dist/reply-hooks.js +17 -17
- package/dist/reply-interactions.js +26 -19
- package/dist/reply-outcomes.js +13 -13
- package/dist/reply-platform.js +40 -13
- package/dist/reply-state.js +28 -28
- package/dist/reply-telegram.js +1 -1
- package/dist/setup.js +74 -43
- package/dist/update.js +92 -0
- package/package.json +1 -1
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('
|
|
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) {
|
|
@@ -63,7 +63,7 @@ function card(message, title, content) {
|
|
|
63
63
|
}
|
|
64
64
|
export function makePlan(channel, message, config) {
|
|
65
65
|
if (!message.title.trim())
|
|
66
|
-
throw new Error('
|
|
66
|
+
throw new Error('通知标题不能为空。');
|
|
67
67
|
const blocks = parseMarkdown(message.content);
|
|
68
68
|
if (channel === 'telegram') {
|
|
69
69
|
const icon = icons[message.level];
|
|
@@ -76,7 +76,7 @@ export function makePlan(channel, message, config) {
|
|
|
76
76
|
const title = partTitle(message.title, index, chunks.length);
|
|
77
77
|
const plain = text(title, chunk.plain, message.footer, icon, false);
|
|
78
78
|
if (plain.length > 4096)
|
|
79
|
-
throw new Error('Telegram
|
|
79
|
+
throw new Error('Telegram 分段后仍然超过消息长度限制。');
|
|
80
80
|
items.push({ recipient: chatId, plain, payload: { chat_id: chatId,
|
|
81
81
|
text: text(title, chunk.html, message.footer, icon, true), parse_mode: 'HTML', disable_web_page_preview: true,
|
|
82
82
|
...(message.reply?.users.includes(chatId) ? { reply_markup: { inline_keyboard: [[{ text: '💬 回复 Claude', callback_data: 'dn:r:' + message.reply.token }]] } } : {}) } });
|
|
@@ -88,7 +88,7 @@ export function makePlan(channel, message, config) {
|
|
|
88
88
|
const items = chunks.map((chunk, index) => {
|
|
89
89
|
const payload = card(message, partTitle(message.title, index, chunks.length), chunk);
|
|
90
90
|
if (bytes(payload) + 128 > 28000)
|
|
91
|
-
throw new Error('
|
|
91
|
+
throw new Error('飞书分段后仍然超过请求大小限制。');
|
|
92
92
|
return { recipient: 'feishu', payload };
|
|
93
93
|
});
|
|
94
94
|
return { channel, items, recipients: 1 };
|
|
@@ -144,12 +144,12 @@ export async function deliver(plan, config, request = requestJson, sleep = delay
|
|
|
144
144
|
}
|
|
145
145
|
else {
|
|
146
146
|
failed.add(item.recipient);
|
|
147
|
-
error = '
|
|
147
|
+
error = '渠道拒绝了这次发送。请检查凭据、接收人授权、签名和频率限制。';
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
catch (cause) {
|
|
151
151
|
failed.add(item.recipient);
|
|
152
|
-
error = cause instanceof TransportError ? cause.message : '
|
|
152
|
+
error = cause instanceof TransportError ? cause.message : '发送失败,这条消息可能没有送达。';
|
|
153
153
|
}
|
|
154
154
|
if (!telegram && !failed.has(item.recipient) && index + 1 < plan.items.length)
|
|
155
155
|
await sleep(250);
|
package/dist/claude-code.js
CHANGED
|
@@ -13,7 +13,7 @@ export function claudeSettingsPath(env = process.env, home = homedir(), platform
|
|
|
13
13
|
const join = platform === 'win32' ? path.win32 : path.posix;
|
|
14
14
|
const root = env.CLAUDE_CONFIG_DIR || join.join(home, '.claude');
|
|
15
15
|
if (!join.isAbsolute(root))
|
|
16
|
-
throw new ConfigError('CLAUDE_CONFIG_DIR
|
|
16
|
+
throw new ConfigError('CLAUDE_CONFIG_DIR 需要是绝对路径,Claude Code 设置没有改动');
|
|
17
17
|
return join.join(root, 'settings.json');
|
|
18
18
|
}
|
|
19
19
|
function settingsFile(options) {
|
|
@@ -23,7 +23,7 @@ async function snapshot(file) {
|
|
|
23
23
|
try {
|
|
24
24
|
const bytes = await readFile(file);
|
|
25
25
|
if (bytes.length > 1024 * 1024)
|
|
26
|
-
throw new ConfigError('Claude Code 设置超过 1 MiB
|
|
26
|
+
throw new ConfigError('Claude Code 设置超过 1 MiB,操作已停止,文件没有改动。');
|
|
27
27
|
return bytes;
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
@@ -31,7 +31,7 @@ async function snapshot(file) {
|
|
|
31
31
|
return null;
|
|
32
32
|
if (error instanceof ConfigError)
|
|
33
33
|
throw error;
|
|
34
|
-
throw new ConfigError('无法读取 Claude Code
|
|
34
|
+
throw new ConfigError('无法读取 Claude Code 设置,操作已停止。');
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
function parseSettings(before) {
|
|
@@ -40,17 +40,17 @@ function parseSettings(before) {
|
|
|
40
40
|
data = before === null ? {} : JSON.parse(decodeUtf8(before));
|
|
41
41
|
}
|
|
42
42
|
catch {
|
|
43
|
-
throw new ConfigError('Claude Code
|
|
43
|
+
throw new ConfigError('Claude Code 设置不是合法的 UTF-8 JSON,没有覆盖。');
|
|
44
44
|
}
|
|
45
45
|
if (!record(data) || (data.hooks !== undefined && !record(data.hooks)))
|
|
46
|
-
throw new ConfigError('Claude Code 设置或 hooks
|
|
46
|
+
throw new ConfigError('Claude Code 设置或 hooks 结构无效,没有覆盖。');
|
|
47
47
|
for (const event of EVENTS) {
|
|
48
48
|
const groups = data.hooks?.[event];
|
|
49
49
|
if (groups === undefined)
|
|
50
50
|
continue;
|
|
51
51
|
if (!Array.isArray(groups) || groups.some(group => !record(group) || !Array.isArray(group.hooks)
|
|
52
52
|
|| group.hooks.some((hook) => !record(hook))))
|
|
53
|
-
throw new ConfigError('Claude Code hook
|
|
53
|
+
throw new ConfigError('Claude Code hook 数组结构无效,没有覆盖。');
|
|
54
54
|
}
|
|
55
55
|
return data;
|
|
56
56
|
}
|
|
@@ -150,14 +150,14 @@ async function commit(file, before, target, prompts, replace = rename) {
|
|
|
150
150
|
lock = await open(lockFile, 'wx', 0o600);
|
|
151
151
|
}
|
|
152
152
|
catch {
|
|
153
|
-
throw new ConfigError('Claude Code
|
|
153
|
+
throw new ConfigError('暂时无法获取 Claude Code 设置写入锁。请确认没有安装进程在运行,再处理残留的 .dev-notify.lock。');
|
|
154
154
|
}
|
|
155
155
|
const temporary = path.join(path.dirname(file), `.dev-notify-${randomBytes(16).toString('hex')}.tmp`);
|
|
156
156
|
let created = false;
|
|
157
157
|
try {
|
|
158
158
|
await lock.close();
|
|
159
159
|
if (!same(before, await snapshot(file)))
|
|
160
|
-
throw new ConfigError('Claude Code
|
|
160
|
+
throw new ConfigError('Claude Code 设置在确认期间发生了变化,没有覆盖,请重试。');
|
|
161
161
|
const stream = await open(temporary, 'wx', 0o600);
|
|
162
162
|
created = true;
|
|
163
163
|
try {
|
|
@@ -168,7 +168,7 @@ async function commit(file, before, target, prompts, replace = rename) {
|
|
|
168
168
|
await stream.close();
|
|
169
169
|
}
|
|
170
170
|
if (!same(before, await snapshot(file)))
|
|
171
|
-
throw new ConfigError('Claude Code
|
|
171
|
+
throw new ConfigError('Claude Code 设置在保存期间发生了变化,没有覆盖,请重试。');
|
|
172
172
|
await replace(temporary, file);
|
|
173
173
|
}
|
|
174
174
|
finally {
|
|
@@ -177,14 +177,14 @@ async function commit(file, before, target, prompts, replace = rename) {
|
|
|
177
177
|
await remove(temporary);
|
|
178
178
|
}
|
|
179
179
|
catch {
|
|
180
|
-
prompts.out('
|
|
180
|
+
prompts.out('临时文件清理失败。安装结束后请删除 .dev-notify-*.tmp。');
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
try {
|
|
184
184
|
await remove(lockFile);
|
|
185
185
|
}
|
|
186
186
|
catch {
|
|
187
|
-
prompts.out('
|
|
187
|
+
prompts.out('锁文件清理失败。安装结束后请删除 .dev-notify.lock。');
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
}
|
|
@@ -193,44 +193,43 @@ export async function configureClaudeCode(config, action, prompts, options = {})
|
|
|
193
193
|
const status = describe(file, data);
|
|
194
194
|
if (action === 'remove' && status.stop + status.notification + status.lifecycle + status.interaction === 0) {
|
|
195
195
|
prompts.out(`Claude Code 用户级设置:${file}`);
|
|
196
|
-
prompts.out('
|
|
196
|
+
prompts.out('没有找到本工具添加的 hooks,设置没有改动。');
|
|
197
197
|
return true;
|
|
198
198
|
}
|
|
199
199
|
const runtime = options.runtime ?? { node: process.execPath, cli: fileURLToPath(new URL('./cli.js', import.meta.url)) };
|
|
200
200
|
const target = merged(data, action === 'install' ? path.resolve(config) : null, runtime);
|
|
201
|
-
|
|
201
|
+
// setup 快速路径已在汇总确认前展示路径和发送范围,避免保存后重复整段说明。
|
|
202
|
+
const quiet = options.confirm === false;
|
|
203
|
+
if (!quiet)
|
|
204
|
+
prompts.out(`Claude Code 用户级设置:${file}`);
|
|
202
205
|
if (JSON.stringify(data) === JSON.stringify(target)) {
|
|
203
|
-
prompts.out(action === 'install' ? '本工具 hooks
|
|
206
|
+
prompts.out(action === 'install' ? '本工具 hooks 已经安装,配置一致,无需重复添加。' : '没有找到本工具添加的 hooks,设置没有改动。');
|
|
204
207
|
return true;
|
|
205
208
|
}
|
|
206
209
|
if (action === 'install') {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
if (!quiet) {
|
|
211
|
+
prompts.out(`通知配置:${path.resolve(config)}`);
|
|
212
|
+
prompts.out('会为这个 Claude Code 配置目录下的所有项目启用通知:任务结束时发送最终回复,需要你确认或输入时也会提醒。');
|
|
213
|
+
prompts.out('最终回复和提醒正文可能包含敏感内容,会发送到配置实际生效的 Telegram / 飞书。环境变量仍然优先。');
|
|
214
|
+
prompts.out('同时安装会话生命周期和结构化问题 hooks。远程输入需要另行运行 reply setup 授权。');
|
|
215
|
+
prompts.out('不会读取整份会话记录。启用回复后,会读取会话的进程元数据;结构化问题或权限请求会发送对应的问题和工具信息。');
|
|
216
|
+
prompts.out('PreToolUse 只登记调用身份;PermissionRequest 会和本机终端窗口同时等待,远程回答的有效期是 5 分钟。安装完成事件只用于反馈结果,不会代替你作答。已核对 Claude Code 2.1.316;其他版本或宿主环境需要另行验证。');
|
|
217
|
+
}
|
|
213
218
|
if (status.others)
|
|
214
|
-
prompts.out(
|
|
215
|
-
prompts.out('
|
|
219
|
+
prompts.out(`当前文件里还有 ${status.others} 个其他相关处理器,可能会重复通知。`);
|
|
220
|
+
prompts.out('项目级或插件的 hooks 也可能同时生效;本工具不会自动迁移或删除旧版 hooks。');
|
|
216
221
|
if (status.disabled)
|
|
217
|
-
prompts.out('
|
|
222
|
+
prompts.out('注意:该文件启用了 disableAllHooks,安装后 hooks 也不会生效;请自行解除禁用。');
|
|
218
223
|
}
|
|
219
|
-
else
|
|
220
|
-
prompts.out('
|
|
221
|
-
if (
|
|
222
|
-
prompts.
|
|
224
|
+
else if (!quiet)
|
|
225
|
+
prompts.out('只会移除本工具添加的通知、生命周期和交互处理器;其他设置、hooks、通知配置和系统凭据都会保留。如需停止回复桥接,请运行 reply disable 或 reply stop。');
|
|
226
|
+
if (options.confirm !== false
|
|
227
|
+
&& !yes(await prompts.text(action === 'install' ? '安装这些 Claude Code hooks?[y/N] ' : '移除本工具的 Claude Code hooks?[y/N] '))) {
|
|
228
|
+
prompts.out('已取消,Claude Code 设置没有改动。');
|
|
223
229
|
return false;
|
|
224
230
|
}
|
|
225
231
|
await commit(file, before, target, prompts, options.replace);
|
|
226
|
-
prompts.out(action === 'install' ? 'Claude Code hooks
|
|
227
|
-
: '本工具的 Claude Code hooks
|
|
232
|
+
prompts.out(action === 'install' ? 'Claude Code hooks 已安装。请在 /hooks 中检查并按界面确认;如果没有生效,请重启会话。'
|
|
233
|
+
: '本工具的 Claude Code hooks 已移除。请在 /hooks 中检查,必要时重启会话。');
|
|
228
234
|
return true;
|
|
229
235
|
}
|
|
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
|
-
}
|
package/dist/claude-hook.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { ConfigError, decodeUtf8, loadConfig, ready } from './config.js';
|
|
3
2
|
import { makePlan } from './channels.js';
|
|
4
3
|
import { NOTIFICATION_MATCHER } from './claude-code.js';
|
|
4
|
+
import { findSessionName } from './reply-platform.js';
|
|
5
5
|
import { lifecycleHook, interactionHook, attachReply, mapReplies, notificationCoverage } from './reply-hooks.js';
|
|
6
6
|
export const MAX_HOOK_BYTES = 8 * 1024 * 1024;
|
|
7
7
|
export async function readHookInput(input = process.stdin, limit = MAX_HOOK_BYTES) {
|
|
8
8
|
const chunks = [];
|
|
9
9
|
let size = 0;
|
|
10
|
-
const timer = setTimeout(() => input.destroy(new ConfigError('Claude Code hook
|
|
10
|
+
const timer = setTimeout(() => input.destroy(new ConfigError('Claude Code hook 输入超时,通知没有发送。')), 15000);
|
|
11
11
|
timer.unref();
|
|
12
12
|
try {
|
|
13
13
|
for await (const chunk of input) {
|
|
14
14
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
15
15
|
size += bytes.length;
|
|
16
16
|
if (size > limit)
|
|
17
|
-
throw new ConfigError('Claude Code hook
|
|
17
|
+
throw new ConfigError('Claude Code hook 输入超过大小限制,通知没有发送;内容不会被截断。');
|
|
18
18
|
chunks.push(bytes);
|
|
19
19
|
}
|
|
20
20
|
return Buffer.concat(chunks);
|
|
@@ -30,39 +30,44 @@ function metadata(value, limit) {
|
|
|
30
30
|
const chars = Array.from(clean);
|
|
31
31
|
return chars.length > limit ? chars.slice(0, limit).join('') + '…' : clean;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
function footer(data, sessionName) {
|
|
34
|
+
const project = typeof data.cwd === 'string' && data.cwd.trim() ? `项目:${metadata(data.cwd, 300)}` : '';
|
|
35
|
+
const topic = typeof sessionName === 'string' && sessionName.trim() ? `主题:${metadata(sessionName.trim(), 120)}` : '';
|
|
36
|
+
const session = typeof data.session_id === 'string' && data.session_id.trim() ? `会话:${metadata(data.session_id, 120)}` : '';
|
|
37
|
+
return [project, topic, session].filter(Boolean).join('\n');
|
|
38
|
+
}
|
|
39
|
+
export function claudeHookMessage(bytes, sessionName = '') {
|
|
34
40
|
if (bytes.byteLength > MAX_HOOK_BYTES)
|
|
35
|
-
throw new ConfigError('Claude Code hook
|
|
41
|
+
throw new ConfigError('Claude Code hook 输入超过大小限制,通知没有发送;内容不会被截断。');
|
|
36
42
|
let data;
|
|
37
43
|
try {
|
|
38
44
|
data = JSON.parse(decodeUtf8(bytes));
|
|
39
45
|
}
|
|
40
46
|
catch {
|
|
41
|
-
throw new ConfigError('Claude Code hook
|
|
47
|
+
throw new ConfigError('Claude Code hook 输入不是合法的 UTF-8 JSON,通知没有发送。');
|
|
42
48
|
}
|
|
43
49
|
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
44
|
-
throw new ConfigError('Claude Code hook
|
|
50
|
+
throw new ConfigError('Claude Code hook 输入需要是 JSON 对象。');
|
|
45
51
|
if (data.hook_event_name !== 'Stop' && data.hook_event_name !== 'Notification')
|
|
46
52
|
return null;
|
|
47
|
-
const
|
|
48
|
-
typeof data.session_id === 'string' ? `会话:${metadata(data.session_id, 120)}` : ''].filter(Boolean).join('\n');
|
|
53
|
+
const details = footer(data, sessionName);
|
|
49
54
|
if (data.hook_event_name === 'Stop') {
|
|
50
55
|
if (data.stop_hook_active !== undefined && typeof data.stop_hook_active !== 'boolean')
|
|
51
|
-
throw new ConfigError('Claude Code Stop
|
|
56
|
+
throw new ConfigError('Claude Code Stop 状态字段无效。');
|
|
52
57
|
if (data.stop_hook_active === true)
|
|
53
58
|
return null;
|
|
54
59
|
if (data.last_assistant_message !== undefined && typeof data.last_assistant_message !== 'string')
|
|
55
|
-
throw new ConfigError('Claude Code
|
|
56
|
-
return { title: 'Claude Code 回复完成', level: 'success', footer,
|
|
60
|
+
throw new ConfigError('Claude Code 最终回复字段无效。');
|
|
61
|
+
return { title: 'Claude Code 回复完成', level: 'success', footer: details,
|
|
57
62
|
content: data.last_assistant_message?.trim() ? data.last_assistant_message
|
|
58
|
-
: '
|
|
63
|
+
: '本轮回复已结束,Claude Code 没有提供最终正文。请回到终端查看;这里不会读取会话文件来补正文,以免显示过期内容。' };
|
|
59
64
|
}
|
|
60
65
|
// 空闲提醒在 Stop 之后重复触发;只转发明确需要用户处理的通知。
|
|
61
66
|
if (!NOTIFICATION_MATCHER.split('|').includes(data.notification_type))
|
|
62
67
|
return null;
|
|
63
68
|
if (data.message !== undefined && typeof data.message !== 'string')
|
|
64
|
-
throw new ConfigError('Claude Code
|
|
65
|
-
return { title: 'Claude Code 需要你处理', level: 'warn', footer,
|
|
69
|
+
throw new ConfigError('Claude Code 通知正文字段无效。');
|
|
70
|
+
return { title: 'Claude Code 需要你处理', level: 'warn', footer: details,
|
|
66
71
|
content: data.message?.trim() ? data.message : 'Claude Code 正在等待你的确认或输入,请回到终端处理。' };
|
|
67
72
|
}
|
|
68
73
|
export async function dispatchClaudeHook(bytes, configFile, context) {
|
|
@@ -74,16 +79,24 @@ export async function dispatchClaudeHook(bytes, configFile, context) {
|
|
|
74
79
|
await interactionHook(data, configFile, options);
|
|
75
80
|
}
|
|
76
81
|
catch {
|
|
77
|
-
context.err('Claude Code
|
|
82
|
+
context.err('Claude Code 的回复状态或交互处理失败。没有自动选择答案或批准权限,请在本机处理。');
|
|
78
83
|
}
|
|
79
84
|
if (!message)
|
|
80
85
|
return;
|
|
86
|
+
let sessionName = '';
|
|
87
|
+
try {
|
|
88
|
+
sessionName = await findSessionName(data.session_id, { env: context.env });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
context.err('读取不到 Claude Code 会话名称,通知底部将省略主题。');
|
|
92
|
+
}
|
|
93
|
+
const notification = sessionName ? claudeHookMessage(bytes, sessionName) ?? message : message;
|
|
81
94
|
let binding = null;
|
|
82
95
|
try {
|
|
83
|
-
binding = await attachReply(
|
|
96
|
+
binding = await attachReply(notification, data, configFile, options);
|
|
84
97
|
}
|
|
85
98
|
catch {
|
|
86
|
-
context.err('
|
|
99
|
+
context.err('远程回复还没有就绪,仍会尝试发送普通通知。');
|
|
87
100
|
}
|
|
88
101
|
const config = await loadConfig(configFile, true, context.env);
|
|
89
102
|
let covered = { telegram: [], feishu: false };
|
|
@@ -91,7 +104,7 @@ export async function dispatchClaudeHook(bytes, configFile, context) {
|
|
|
91
104
|
covered = await notificationCoverage(data, configFile, config, options);
|
|
92
105
|
}
|
|
93
106
|
catch {
|
|
94
|
-
context.err('
|
|
107
|
+
context.err('没有核验到选择题通知的覆盖情况,将保留普通权限提醒。');
|
|
95
108
|
}
|
|
96
109
|
let channels = 0;
|
|
97
110
|
for (const channel of ['telegram', 'feishu']) {
|
|
@@ -101,7 +114,7 @@ export async function dispatchClaudeHook(bytes, configFile, context) {
|
|
|
101
114
|
try {
|
|
102
115
|
if (channel === 'feishu' && covered.feishu)
|
|
103
116
|
continue;
|
|
104
|
-
const plan = makePlan(channel,
|
|
117
|
+
const plan = makePlan(channel, notification, config);
|
|
105
118
|
if (channel === 'telegram' && covered.telegram.length) {
|
|
106
119
|
plan.items = plan.items.filter(item => !covered.telegram.includes(item.recipient));
|
|
107
120
|
plan.recipients = new Set(plan.items.map(item => item.recipient)).size;
|
|
@@ -112,12 +125,12 @@ export async function dispatchClaudeHook(bytes, configFile, context) {
|
|
|
112
125
|
mapReplies(plan, binding);
|
|
113
126
|
const result = await context.send(plan, config);
|
|
114
127
|
if (!result.ok)
|
|
115
|
-
context.err(`${channel}
|
|
128
|
+
context.err(`${channel}:Claude Code 通知没有完整发送。整条消息不会自动重发。`);
|
|
116
129
|
}
|
|
117
130
|
catch {
|
|
118
|
-
context.err(`${channel}
|
|
131
|
+
context.err(`${channel}:Claude Code 通知发送失败。请检查配置、系统凭据和网络;整条消息不会自动重发。`);
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
if (!channels)
|
|
122
|
-
throw new ConfigError('Claude Code 通知没有可用渠道,请运行 dev-notify doctor
|
|
135
|
+
throw new ConfigError('Claude Code 通知没有可用渠道,请运行 dev-notify doctor 检查。');
|
|
123
136
|
}
|