@quecue/dev-notify 0.1.3 → 0.1.5

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