@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/CHANGELOG.en.md +89 -0
- package/CHANGELOG.md +15 -0
- package/README.en.md +275 -0
- package/README.md +9 -2
- package/SECURITY.en.md +78 -0
- package/SECURITY.md +4 -2
- package/dist/channels.js +14 -12
- package/dist/claude-code.js +30 -30
- package/dist/claude-hook.js +23 -23
- package/dist/cli.js +106 -81
- package/dist/config.js +37 -20
- package/dist/credentials.js +14 -13
- package/dist/formatting.js +6 -4
- package/dist/i18n.js +960 -0
- package/dist/maintenance.js +17 -16
- package/dist/prompts.js +7 -6
- package/dist/reply-bridge.js +11 -10
- package/dist/reply-engine.js +20 -19
- package/dist/reply-hooks.js +21 -20
- package/dist/reply-interactions.js +36 -35
- package/dist/reply-outcomes.js +46 -23
- package/dist/reply-platform.js +14 -13
- package/dist/reply-state.js +31 -30
- package/dist/reply-telegram.js +4 -3
- package/dist/setup.js +69 -51
- package/dist/update.js +23 -22
- package/package.json +4 -1
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
|
|
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('
|
|
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: '
|
|
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('
|
|
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: '
|
|
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);
|
package/dist/claude-code.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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('
|
|
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('
|
|
44
|
+
throw new ConfigError(t('claude.settingsInvalidJson'));
|
|
44
45
|
}
|
|
45
46
|
if (!record(data) || (data.hooks !== undefined && !record(data.hooks)))
|
|
46
|
-
throw new ConfigError('
|
|
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('
|
|
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' ?
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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(
|
|
198
|
-
prompts.out('
|
|
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(
|
|
207
|
+
prompts.out(t('claude.settingsPath', { file }));
|
|
207
208
|
if (JSON.stringify(data) === JSON.stringify(target)) {
|
|
208
|
-
prompts.out(action === 'install' ? '
|
|
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(
|
|
214
|
-
prompts.out('
|
|
215
|
-
prompts.out('
|
|
216
|
-
prompts.out('
|
|
217
|
-
prompts.out('
|
|
218
|
-
prompts.out('
|
|
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(
|
|
222
|
-
prompts.out('
|
|
222
|
+
prompts.out(t('claude.otherHandlers', { count: status.others }));
|
|
223
|
+
prompts.out(t('claude.projectHooks'));
|
|
223
224
|
if (status.disabled)
|
|
224
|
-
prompts.out('
|
|
225
|
+
prompts.out(t('claude.disabledWarning'));
|
|
225
226
|
}
|
|
226
227
|
else if (!quiet)
|
|
227
|
-
prompts.out('
|
|
228
|
+
prompts.out(t('claude.removeScope'));
|
|
228
229
|
if (options.confirm !== false
|
|
229
|
-
&& !yes(await prompts.text(action === 'install' ? '
|
|
230
|
-
prompts.out('
|
|
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' ? '
|
|
235
|
-
: '本工具的 Claude Code hooks 已移除。请在 /hooks 中检查,必要时重启会话。');
|
|
235
|
+
prompts.out(action === 'install' ? t('claude.installed') : t('claude.removed'));
|
|
236
236
|
return true;
|
|
237
237
|
}
|
package/dist/claude-hook.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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() ?
|
|
35
|
-
const topic = typeof sessionName === 'string' && sessionName.trim() ?
|
|
36
|
-
const session = typeof data.session_id === 'string' && data.session_id.trim() ?
|
|
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('
|
|
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('
|
|
48
|
+
throw new ConfigError(t('hook.invalidJson'));
|
|
48
49
|
}
|
|
49
50
|
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
50
|
-
throw new ConfigError('
|
|
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('
|
|
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('
|
|
61
|
-
return { title: '
|
|
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('
|
|
70
|
-
return { title: '
|
|
71
|
-
content: data.message?.trim() ? data.message : '
|
|
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('
|
|
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('
|
|
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(
|
|
128
|
+
context.err(t('hook.sendIncomplete', { channel }));
|
|
129
129
|
}
|
|
130
130
|
catch {
|
|
131
|
-
context.err(
|
|
131
|
+
context.err(t('hook.sendFailed', { channel }));
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
if (!channels)
|
|
135
|
-
throw new ConfigError('
|
|
135
|
+
throw new ConfigError(t('hook.noChannels'));
|
|
136
136
|
}
|