@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/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) {
@@ -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);
@@ -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 必须是绝对路径,未修改 Claude Code 设置');
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 设置不是合法 UTF-8 JSON,未覆盖');
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 设置写入锁不可用;确认没有安装进程后才能处理残留 .dev-notify.lock');
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('临时 Claude Code 设置清理失败,请确认安装结束后清理 .dev-notify-*.tmp');
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('Claude Code 设置锁清理失败,请确认安装结束后处理 .dev-notify.lock');
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('没有本工具标记的 hooks,未修改。');
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
- prompts.out(`Claude Code 用户级设置:${file}`);
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 已安装且配置一致,未重复添加。' : '没有本工具标记的 hooks,未修改。');
206
+ prompts.out(action === 'install' ? '本工具 hooks 已经安装,配置一致,无需重复添加。' : '没有找到本工具添加的 hooks,设置没有改动。');
204
207
  return true;
205
208
  }
206
209
  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 实现;旧版和特殊宿主需另行验收。');
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(`保留当前文件中另外 ${status.others} 个相关处理器;可能出现重复通知。`);
215
- prompts.out('项目级或插件 hooks 也可能同时生效;不会自动迁移或删除原 Python hooks。');
219
+ prompts.out(`当前文件里还有 ${status.others} 个其他相关处理器,可能会重复通知。`);
220
+ prompts.out('项目级或插件的 hooks 也可能同时生效;本工具不会自动迁移或删除旧版 hooks。');
216
221
  if (status.disabled)
217
- prompts.out('注意:该文件设置了 disableAllHooks,安装后仍不会生效;本工具不擅自解除禁用。');
222
+ prompts.out('注意:该文件启用了 disableAllHooks,安装后 hooks 也不会生效;请自行解除禁用。');
218
223
  }
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 设置未修改。');
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 已写入。请在 /hooks 中检查并按界面确认;未生效时重启会话。'
227
- : '本工具的 Claude Code hooks 已移除;请在 /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
- }
@@ -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 输入超时,未发送通知')), 15000);
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
- export function claudeHookMessage(bytes) {
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 输入不是合法 UTF-8 JSON,未发送通知');
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 输入必须是 JSON 对象');
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 footer = [typeof data.cwd === 'string' ? `项目:${metadata(path.win32.basename(data.cwd), 120)}` : '',
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
- : '本轮回复已结束,但 Claude Code 未提供最终正文。请在终端查看回复;不会读取可能滞后的会话文件来代替正文。' };
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(message, data, configFile, options);
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, message, config);
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}: Claude Code 通知未完整发送;不自动重发整条消息。`);
128
+ context.err(`${channel}:Claude Code 通知没有完整发送。整条消息不会自动重发。`);
116
129
  }
117
130
  catch {
118
- context.err(`${channel}: Claude Code 通知发送失败;请检查配置、系统凭据及网络,不自动重发整条消息。`);
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
  }