@quecue/dev-notify 0.1.3 → 0.1.4

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.
@@ -3,17 +3,18 @@ import { fileURLToPath } from 'node:url';
3
3
  import path from 'node:path';
4
4
  import { setTimeout as delay } from 'node:timers/promises';
5
5
  import { ConfigError } from './config.js';
6
+ import { t } from './i18n.js';
6
7
  import { claudeCodeStatus } from './claude-code.js';
7
8
  import { bridgeStatus } from './reply-bridge.js';
8
9
  function runCli(args) {
9
10
  const cli = fileURLToPath(new URL('./cli.js', import.meta.url));
10
11
  const result = spawnSync(process.execPath, [cli, ...args], { stdio: 'inherit', timeout: 300000 });
11
12
  if (result.error)
12
- throw new ConfigError('无法运行更新后的 dev-notify。请重新打开终端后运行 dev-notify --version 检查。');
13
+ throw new ConfigError(t('maintenance.runFailed'));
13
14
  return result.status ?? 1;
14
15
  }
15
16
  function reason(error) {
16
- return error instanceof ConfigError ? error.message : '请运行 dev-notify integration claude-code status 或 dev-notify reply status 检查。';
17
+ return error instanceof ConfigError ? error.message : t('maintenance.reasonHint');
17
18
  }
18
19
  const normalize = (file) => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
19
20
  export async function postUpdate(options) {
@@ -27,28 +28,28 @@ export async function postUpdate(options) {
27
28
  hooksStatus = await hooks();
28
29
  }
29
30
  catch (error) {
30
- options.err(`无法检查 Claude Code hooks:${reason(error)}`);
31
+ options.err(t('maintenance.hooksCheckFailed', { reason: reason(error) }));
31
32
  failed = true;
32
33
  }
33
34
  if (hooksStatus) {
34
35
  const count = hooksStatus.stop + hooksStatus.notification + hooksStatus.lifecycle + hooksStatus.interaction;
35
36
  if (!count)
36
- options.out('未检测到本工具的 Claude Code hooks,跳过。');
37
+ options.out(t('maintenance.noHooks'));
37
38
  else if (!hooksStatus.configs.some(file => normalize(file) === normalize(options.config))) {
38
- options.out('Claude Code hooks 绑定的是其他通知配置,跳过自动更新;如需更新,请用原来的 --config 运行 integration claude-code install。');
39
+ options.out(t('maintenance.otherConfig'));
39
40
  }
40
41
  else {
41
- options.out('更新 Claude Code hooks ...');
42
+ options.out(t('maintenance.updatingHooks'));
42
43
  try {
43
44
  if (run(['integration', 'claude-code', 'install', '--yes', '--config', options.config]) === 0)
44
- options.out('Claude Code hooks 已更新。');
45
+ options.out(t('maintenance.hooksUpdated'));
45
46
  else {
46
- options.err('Claude Code hooks 更新失败。');
47
+ options.err(t('maintenance.hooksFailed'));
47
48
  failed = true;
48
49
  }
49
50
  }
50
51
  catch (error) {
51
- options.err(`Claude Code hooks 更新失败:${reason(error)}`);
52
+ options.err(t('maintenance.hooksFailedReason', { reason: reason(error) }));
52
53
  failed = true;
53
54
  }
54
55
  }
@@ -58,11 +59,11 @@ export async function postUpdate(options) {
58
59
  bridgeState = await bridge();
59
60
  }
60
61
  catch (error) {
61
- options.err(`无法检查回复桥接:${reason(error)}`);
62
+ options.err(t('maintenance.bridgeCheckFailed', { reason: reason(error) }));
62
63
  failed = true;
63
64
  }
64
65
  if (bridgeState?.running) {
65
- options.out('重启回复桥接 ...');
66
+ options.out(t('maintenance.restartingBridge'));
66
67
  try {
67
68
  // 0 表示已停止或本来未运行;1 表示停止请求已登记,下面等待进程释放锁。
68
69
  run(['reply', 'stop', '--config', options.config]);
@@ -79,22 +80,22 @@ export async function postUpdate(options) {
79
80
  await wait(500);
80
81
  }
81
82
  if (stillRunning) {
82
- options.err('回复桥接仍在退出,请稍后运行 dev-notify reply start。');
83
+ options.err(t('maintenance.bridgeStillRunning'));
83
84
  failed = true;
84
85
  }
85
86
  else if (run(['reply', 'start', '--config', options.config]) === 0)
86
- options.out('回复桥接已重启。');
87
+ options.out(t('maintenance.bridgeRestarted'));
87
88
  else {
88
- options.err('回复桥接启动失败。');
89
+ options.err(t('maintenance.bridgeStartFailed'));
89
90
  failed = true;
90
91
  }
91
92
  }
92
93
  catch (error) {
93
- options.err(`回复桥接重启失败:${reason(error)}`);
94
+ options.err(t('maintenance.bridgeRestartFailed', { reason: reason(error) }));
94
95
  failed = true;
95
96
  }
96
97
  }
97
98
  else if (bridgeState)
98
- options.out('未检测到运行中的回复桥接,跳过。');
99
+ options.out(t('maintenance.noBridge'));
99
100
  return failed ? 1 : 0;
100
101
  }
package/dist/prompts.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { emitKeypressEvents } from 'node:readline';
2
2
  import { createInterface } from 'node:readline/promises';
3
3
  import { ConfigError } from './config.js';
4
+ import { t } from './i18n.js';
4
5
  export class Cancelled extends Error {
5
6
  }
6
7
  export function terminalPrompts() {
7
8
  if (!process.stdin.isTTY || !process.stdout.isTTY)
8
- throw new ConfigError('配置向导需要在交互终端中运行。自动化场景可以改用 setup --template --yes 搭配环境变量。');
9
+ throw new ConfigError(t('prompts.terminalRequired'));
9
10
  return {
10
11
  out: text => console.log(text),
11
12
  text: label => readText(label),
@@ -22,7 +23,7 @@ export async function readText(label, input = process.stdin, output = process.st
22
23
  return (await terminal.question(label, { signal: controller.signal })).trim();
23
24
  }
24
25
  catch {
25
- throw new Cancelled('已取消。');
26
+ throw new Cancelled(t('prompts.cancelled'));
26
27
  }
27
28
  finally {
28
29
  terminal.off('SIGINT', cancel);
@@ -32,7 +33,7 @@ export async function readText(label, input = process.stdin, output = process.st
32
33
  }
33
34
  export async function readHidden(label, input = process.stdin, output = process.stdout) {
34
35
  if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== 'function')
35
- throw new ConfigError('当前终端不支持隐藏输入。为了不暴露密钥,这里不会改用明文输入;请在支持的终端中运行,或用环境变量配置。');
36
+ throw new ConfigError(t('prompts.hiddenUnsupported'));
36
37
  const wasRaw = input.isRaw, wasPaused = input.isPaused();
37
38
  return new Promise((resolve, reject) => {
38
39
  let value = '';
@@ -58,8 +59,8 @@ export async function readHidden(label, input = process.stdin, output = process.
58
59
  value = '';
59
60
  error ? reject(error) : resolve(result);
60
61
  };
61
- const failed = () => finish(new ConfigError('隐藏输入没有完成,凭据没有保存。'));
62
- const cancelled = () => finish(new Cancelled('已取消。'));
62
+ const failed = () => finish(new ConfigError(t('prompts.hiddenIncomplete')));
63
+ const cancelled = () => finish(new Cancelled(t('prompts.cancelled')));
63
64
  const keypress = (text, key = {}) => {
64
65
  if (key.ctrl && (key.name === 'c' || key.name === 'd'))
65
66
  return cancelled();
@@ -76,7 +77,7 @@ export async function readHidden(label, input = process.stdin, output = process.
76
77
  if (key.ctrl || key.meta || !text || /[\x00-\x1f\x7f]/.test(text))
77
78
  return;
78
79
  if (value.length + text.length > 8192)
79
- return finish(new ConfigError('凭据输入过长,没有保存。'));
80
+ return finish(new ConfigError(t('prompts.inputTooLong')));
80
81
  value += text;
81
82
  };
82
83
  try {
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { randomBytes } from 'node:crypto';
6
6
  import { setTimeout as delay } from 'node:timers/promises';
7
7
  import { appPaths, ConfigError } from './config.js';
8
+ import { t } from './i18n.js';
8
9
  import { accessReplies, atomicJson, readJson, replyDirectory, stateTransaction } from './reply-state.js';
9
10
  import { telegramApi } from './reply-telegram.js';
10
11
  import { drainReplies, processUpdate } from './reply-engine.js';
@@ -44,7 +45,7 @@ export async function stopBridge(file) {
44
45
  export async function startBridge(file, env = process.env) {
45
46
  const access = await accessReplies(file, env);
46
47
  if (!access)
47
- throw new ConfigError('远程回复尚未授权,请先运行 reply setup。');
48
+ throw new ConfigError(t('reply.bridgeAuthFirst'));
48
49
  const existing = await bridgeStatus(file);
49
50
  if (existing.running)
50
51
  return existing.ready;
@@ -72,7 +73,7 @@ export async function runBridge(file, options = {}) {
72
73
  const env = options.env ?? process.env;
73
74
  let access = await accessReplies(file, env);
74
75
  if (!access)
75
- throw new ConfigError('远程回复尚未授权。');
76
+ throw new ConfigError(t('reply.bridgeNotAuthorized'));
76
77
  const directory = replyDirectory(file);
77
78
  await mkdir(directory, { recursive: true, mode: 0o700 });
78
79
  const botDirectory = path.join(appPaths(process.platform, env).data, 'reply-bots');
@@ -83,7 +84,7 @@ export async function runBridge(file, options = {}) {
83
84
  lock = await open(lockPath, 'wx', 0o600);
84
85
  }
85
86
  catch {
86
- throw new ConfigError('当前用户已经有同一个 Bot 的桥接在运行,或残留了锁文件。请确认进程退出后,再处理 reply-bots 中对应的锁。');
87
+ throw new ConfigError(t('reply.bridgeLocked'));
87
88
  }
88
89
  const owner = randomBytes(16).toString('hex'), lease = { pid: process.pid, owner, ready: false, updated: Date.now() };
89
90
  let stopping = false;
@@ -100,7 +101,7 @@ export async function runBridge(file, options = {}) {
100
101
  let api = (options.api ?? (value => telegramApi(value.config.telegram.token)))(access);
101
102
  const webhook = await api('getWebhookInfo', {});
102
103
  if (webhook?.url)
103
- throw new ConfigError('这个 Bot 已经设置了 webhook,不能同时使用 getUpdates。请先移除 webhook;本工具不会自动删除。');
104
+ throw new ConfigError(t('reply.webhookConflict'));
104
105
  await api('getMe', {});
105
106
  const currentState = await stateTransaction(access, state => ({ offset: state.offset, empty: Object.keys(state.entries).length === 0 }));
106
107
  if (!currentState.offset && currentState.empty) {
@@ -137,11 +138,11 @@ export async function runBridge(file, options = {}) {
137
138
  updates = await api('getUpdates', { offset, timeout: 2, allowed_updates: ['message', 'callback_query'] });
138
139
  }
139
140
  catch (error) {
140
- if (error instanceof ConfigError && /冲突/.test(error.message))
141
+ if (error instanceof ConfigError && /冲突|conflict/i.test(error.message))
141
142
  throw error;
142
- log('Telegram 轮询失败;日志中没有记录密钥或服务端正文。');
143
+ log(t('reply.pollFailedLog'));
143
144
  if (++failures >= 5)
144
- throw new ConfigError('Telegram 连续轮询失败 5 次,桥接已经退出。请检查网络、Token 或 API 冲突后重新启动。');
145
+ throw new ConfigError(t('reply.pollFailedFatal'));
145
146
  lease.updated = Date.now();
146
147
  await atomicJson(leasePath(file), lease);
147
148
  await (options.sleep ?? delay)(3000);
@@ -152,7 +153,7 @@ export async function runBridge(file, options = {}) {
152
153
  if (!after || after.identity !== access.identity || await isStopped())
153
154
  break;
154
155
  if (!Array.isArray(updates))
155
- throw new ConfigError('Telegram 更新格式无效。');
156
+ throw new ConfigError(t('reply.updatesInvalid'));
156
157
  for (const update of updates) {
157
158
  if (await isStopped())
158
159
  break;
@@ -160,7 +161,7 @@ export async function runBridge(file, options = {}) {
160
161
  await processUpdate(access, update, context);
161
162
  }
162
163
  catch {
163
- log('回复处理失败。这条更新不会重放;请检查终端,或从最新通知重试。');
164
+ log(t('reply.handleFailedLog'));
164
165
  }
165
166
  }
166
167
  if (updates.length)
@@ -173,7 +174,7 @@ export async function runBridge(file, options = {}) {
173
174
  }
174
175
  catch (error) {
175
176
  lease.ready = false;
176
- lease.error = error instanceof ConfigError ? error.message : '桥接启动或运行失败;为避免泄露密钥,没有显示底层错误。';
177
+ lease.error = error instanceof ConfigError ? error.message : t('reply.bridgeFailed');
177
178
  await atomicJson(leasePath(file), lease).catch(() => { });
178
179
  throw error;
179
180
  }
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { ConfigError } from './config.js';
3
+ import { t } from './i18n.js';
3
4
  import { digest, targetEpoch, stateTransaction, TTL } from './reply-state.js';
4
5
  import { handleInteractionCallback, handleInteractionText, showInteraction } from './reply-interactions.js';
5
6
  import { injectReply, sanitizeReply, validateTarget } from './reply-platform.js';
@@ -27,7 +28,7 @@ export async function resolveEntry(access, key, context) {
27
28
  async function prompt(access, chat, messageId, key, context, autoReply = true) {
28
29
  const resolved = await resolveEntry(access, key, context);
29
30
  if (!resolved) {
30
- await replyHint(context.api, chat, '⚠️ 无法回复', '这个会话已经关闭、切换,或这条通知已过期。请从最新通知进入。', messageId);
31
+ await replyHint(context.api, chat, t('reply.sessionGoneTitle'), t('reply.sessionGoneBody'), messageId);
31
32
  return;
32
33
  }
33
34
  if (resolved.entry.request) {
@@ -36,8 +37,8 @@ async function prompt(access, chat, messageId, key, context, autoReply = true) {
36
37
  }
37
38
  const session = resolved.target.title || resolved.target.session;
38
39
  const sent = autoReply
39
- ? await replyHint(context.api, chat, '💬 回复 Claude', '请回复此消息输入内容。该输入将提交到对应本机会话。\n会话:' + session, messageId, { force_reply: true, selective: true })
40
- : await replyHint(context.api, chat, '💬 回复 Claude', '请长按这条消息并选择「回复」,然后输入内容。该输入将提交到对应本机会话。\n会话:' + session, messageId, { remove_keyboard: true });
40
+ ? await replyHint(context.api, chat, t('reply.replyPromptTitle'), t('reply.replyPromptBody', { session }), messageId, { force_reply: true, selective: true })
41
+ : await replyHint(context.api, chat, t('reply.replyPromptTitle'), t('reply.replyPromptLongPress', { session }), messageId, { remove_keyboard: true });
41
42
  if (Number.isSafeInteger(sent?.message_id))
42
43
  await stateTransaction(access, state => { state.entries[`tg:${chat}:${sent.message_id}`] = resolved.entry; });
43
44
  }
@@ -53,9 +54,9 @@ export async function processUpdate(access, update, context) {
53
54
  if (chat && current?.identity === access.identity && current.policy.users.includes(chat)) {
54
55
  if (typeof update?.callback_query?.id === 'string')
55
56
  await context.api('answerCallbackQuery', {
56
- callback_query_id: update.callback_query.id, text: '处理没有完成,请检查终端;不会自动重复提交。',
57
+ callback_query_id: update.callback_query.id, text: t('reply.processingFailedCallback'),
57
58
  }).catch(() => { });
58
- await replyHint(context.api, chat, '⚠️ 本次提交没有完成', '处理时出现了异常,这次提交可能已经登记,也可能没有生效。请检查终端和后续结果;不会自动重复提交。').catch(() => { });
59
+ await replyHint(context.api, chat, t('reply.processingFailedTitle'), t('reply.processingFailedBody')).catch(() => { });
59
60
  }
60
61
  throw error;
61
62
  }
@@ -83,8 +84,8 @@ async function processAuthorizedUpdate(access, update, context) {
83
84
  if (typeof value === 'string' && value.startsWith('dn:q:')) {
84
85
  const id = value.split(':')[2];
85
86
  if (!/^[a-f0-9]{32}$/.test(id) || !await resolveEntry(access, 'token:' + id, context)) {
86
- await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: '会话已关闭,或这次交互已经失效。' }).catch(() => { });
87
- await replyHint(context.api, chat, '⚠️ 这次提交没有生效', '会话已经关闭、切换,或交互已经失效,答案没有回传。').catch(() => { });
87
+ await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: t('reply.interactionExpiredCallback') }).catch(() => { });
88
+ await replyHint(context.api, chat, t('reply.interactionExpiredTitle'), t('reply.interactionExpiredBody')).catch(() => { });
88
89
  return;
89
90
  }
90
91
  if (await handleInteractionCallback(access, callback, chat, context.api))
@@ -92,7 +93,7 @@ async function processAuthorizedUpdate(access, update, context) {
92
93
  }
93
94
  if (typeof value !== 'string' || !/^dn:r:[a-f0-9]{32}$/.test(value) || typeof callback.id !== 'string')
94
95
  return;
95
- await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: '请在回复框输入内容' });
96
+ await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: t('reply.replyBoxHint') });
96
97
  await prompt(access, chat, callback.message.message_id, 'token:' + value.slice(5), context);
97
98
  return;
98
99
  }
@@ -107,7 +108,7 @@ async function processAuthorizedUpdate(access, update, context) {
107
108
  if (!current.policy.users.includes(chat))
108
109
  return;
109
110
  if (typeof message.date === 'number' && Date.now() / 1000 - message.date > 300) {
110
- await replyHint(context.api, chat, '⚠️ 本次提交没有生效', '这条消息已经超过有效时间,答案没有回传。请查看最新问题或终端状态。');
111
+ await replyHint(context.api, chat, t('reply.staleTitle'), t('reply.staleBody'));
111
112
  return;
112
113
  }
113
114
  if (/^\/start\s+[a-f0-9]{32}$/.test(text)) {
@@ -117,12 +118,12 @@ async function processAuthorizedUpdate(access, update, context) {
117
118
  }
118
119
  const replyTo = message.reply_to_message?.message_id;
119
120
  if (!Number.isSafeInteger(replyTo)) {
120
- await replyHint(context.api, chat, '💬 回复方式', '请直接回复任务完成通知,或点击通知里的「回复 Claude」按钮。', message.message_id);
121
+ await replyHint(context.api, chat, t('reply.howToTitle'), t('reply.howToBody'), message.message_id);
121
122
  return;
122
123
  }
123
124
  const resolved = await resolveEntry(access, `tg:${chat}:${replyTo}`, context);
124
125
  if (!resolved) {
125
- await replyHint(context.api, chat, '⚠️ 无法回复', '通知已过期,或对应会话已经关闭、切换。请从最新通知回复。', message.message_id);
126
+ await replyHint(context.api, chat, t('reply.sessionGoneTitle'), t('reply.expiredReplyBody'), message.message_id);
126
127
  return;
127
128
  }
128
129
  if (resolved.entry.request) {
@@ -134,14 +135,14 @@ async function processAuthorizedUpdate(access, update, context) {
134
135
  safe = sanitizeReply(message.text);
135
136
  }
136
137
  catch (error) {
137
- await replyHint(context.api, chat, '❌ 回复没有提交', error instanceof ConfigError ? error.message : '输入无效。', message.message_id);
138
+ await replyHint(context.api, chat, t('reply.replyFailedTitle'), error instanceof ConfigError ? error.message : t('reply.invalidInput'), message.message_id);
138
139
  return;
139
140
  }
140
141
  const id = randomBytes(16).toString('hex');
141
142
  const queued = await stateTransaction(access, state => {
142
143
  const target = state.sessions[resolved.target.session];
143
144
  if (!target || sessionEpoch(target) !== resolved.entry.epoch)
144
- throw new ConfigError('会话已经变化,这次回复没有提交。');
145
+ throw new ConfigError(t('reply.sessionChanged'));
145
146
  if (state.pending[target.session] && Date.now() - state.pending[target.session].updated < 5 * 60 * 1000)
146
147
  return null;
147
148
  state.pending[target.session] = { hash: textHash(safe), text: safe, epoch: resolved.entry.epoch, id, updated: Date.now(), queued: target.working,
@@ -149,11 +150,11 @@ async function processAuthorizedUpdate(access, update, context) {
149
150
  return target.working;
150
151
  });
151
152
  if (queued === null) {
152
- await replyHint(context.api, chat, '⏳ 新回复没有提交', '这个会话已有一条待确认的回复。请先等待确认,或检查本机终端后再试。', message.message_id);
153
+ await replyHint(context.api, chat, t('reply.pendingTitle'), t('reply.pendingBody'), message.message_id);
153
154
  return;
154
155
  }
155
156
  if (queued)
156
- await replyHint(context.api, chat, '⏳ 回复已排队', 'Claude 正在处理当前任务;本地队列将在本轮结束后尝试提交,不会把文本输入到正在等待权限的弹窗。', message.message_id);
157
+ await replyHint(context.api, chat, t('reply.queuedTitle'), t('reply.queuedBody'), message.message_id);
157
158
  else
158
159
  await drainReplies(access, context);
159
160
  }
@@ -170,9 +171,9 @@ export async function drainReplies(access, context) {
170
171
  if (!current || current.identity !== access.identity || !current.policy.users.includes(pending.chat))
171
172
  return;
172
173
  if (Date.now() - pending.updated > 5 * 60 * 1000)
173
- throw new ConfigError('排队的回复已超过 5 分钟,没有提交。请检查本机后重新回复。');
174
+ throw new ConfigError(t('reply.queueExpired'));
174
175
  if (!target.tracked || pending.epoch !== sessionEpoch(target) || !await (context.validate ?? (value => validateTarget(value)))(target))
175
- throw new ConfigError('会话已经关闭或切换,这次回复没有提交。');
176
+ throw new ConfigError(t('reply.sessionClosed'));
176
177
  const text = pending.text;
177
178
  const claimed = await stateTransaction(access, state => {
178
179
  const actual = state.pending[target.session], active = state.sessions[target.session];
@@ -189,7 +190,7 @@ export async function drainReplies(access, context) {
189
190
  if (!final || final.identity !== access.identity)
190
191
  return;
191
192
  await (context.inject ?? ((value, content) => injectReply(value, content)))(target, text);
192
- await replyHint(context.api, pending.chat, '✅ 已转发给 Claude', '终端输入已提交,正在等待 UserPromptSubmit 确认。', pending.message);
193
+ await replyHint(context.api, pending.chat, t('reply.forwardedTitle'), t('reply.forwardedBody'), pending.message);
193
194
  }
194
195
  catch (error) {
195
196
  await stateTransaction(access, state => {
@@ -199,7 +200,7 @@ export async function drainReplies(access, context) {
199
200
  delete value.text;
200
201
  }
201
202
  });
202
- await replyHint(context.api, pending.chat, '⚠️ 转发状态待确认', error instanceof ConfigError ? error.message : '提交没有完成,请检查本机终端;不会自动重复提交。', pending.message).catch(() => { });
203
+ await replyHint(context.api, pending.chat, t('reply.forwardUnknownTitle'), error instanceof ConfigError ? error.message : t('reply.forwardFailed'), pending.message).catch(() => { });
203
204
  }
204
205
  }
205
206
  }
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { ConfigError } from './config.js';
3
+ import { t } from './i18n.js';
3
4
  import { accessReplies, botKey, digest, stateTransaction, targetEpoch } from './reply-state.js';
4
5
  import { findSession } from './reply-platform.js';
5
6
  import { textHash } from './reply-engine.js';
@@ -27,13 +28,13 @@ export async function lifecycleHook(data, file, options = {}) {
27
28
  if (event === 'SessionEnd' || !target) {
28
29
  delete state.sessions[data.session_id];
29
30
  delete state.pending[data.session_id];
30
- endSessionInteractions(state, data.session_id, '会话已经结束,或无法核验身份');
31
+ endSessionInteractions(state, data.session_id, t('reply.endSessionReason'));
31
32
  return null;
32
33
  }
33
34
  // 同进程 / 同 TTY 已切换会话时,旧映射不再允许输入。
34
35
  for (const [id, other] of Object.entries(state.sessions)) {
35
36
  if (id !== target.session && (other.pid === target.pid || (target.tty && other.tty === target.tty))) {
36
- endSessionInteractions(state, id, '会话已经切换');
37
+ endSessionInteractions(state, id, t('reply.endSwitchedReason'));
37
38
  delete state.sessions[id];
38
39
  delete state.pending[id];
39
40
  }
@@ -52,22 +53,22 @@ export async function lifecycleHook(data, file, options = {}) {
52
53
  if (event === 'UserPromptSubmit' && !match && pending?.phase === 'queued')
53
54
  delete state.pending[target.session];
54
55
  if (event === 'UserPromptSubmit' || event === 'SessionStart' || event === 'Stop') {
55
- endSessionInteractions(state, target.session, event === 'Stop' ? '本轮已结束' : '本机已开始新任务或新会话');
56
+ endSessionInteractions(state, target.session, event === 'Stop' ? t('reply.endTurnReason') : t('reply.endLocalReason'));
56
57
  }
57
58
  return match ? pending : null;
58
59
  });
59
60
  if (receipt) {
60
- await replyHint(apiFor(access, options), receipt.chat, '✅ Claude 已收到回复', 'Claude 已收到你提交的回复,任务完成后会再发通知。', receipt.message).catch(() => options.err?.('Telegram:收到确认没有发送成功;已提交的输入不会重放。'));
61
+ await replyHint(apiFor(access, options), receipt.chat, t('reply.receivedTitle'), t('reply.receivedBody'), receipt.message).catch(() => options.err?.(t('reply.receivedTelegramFailed')));
61
62
  if (access.config.feishu.enabled && access.config.feishu.webhook) {
62
63
  try {
63
- const result = await (options.send ?? deliver)(makePlan('feishu', { title: 'Claude 已收到回复', level: 'info',
64
- content: 'Claude 已收到你提交的回复,任务完成后会再发通知。为减少扩散,这里不转发回复原文。',
65
- footer: '会话:' + data.session_id }, access.config), access.config);
64
+ const result = await (options.send ?? deliver)(makePlan('feishu', { title: t('reply.receivedTitle'), level: 'info',
65
+ content: t('reply.receivedFeishuBody'),
66
+ footer: t('reply.receivedFeishuFooter', { session: data.session_id }) }, access.config), access.config);
66
67
  if (!result.ok)
67
- options.err?.('飞书:收到确认没有完整发送;已提交的输入不会重放。');
68
+ options.err?.(t('reply.receivedFeishuIncomplete'));
68
69
  }
69
70
  catch {
70
- options.err?.('飞书:收到确认发送失败;已提交的输入不会重放。');
71
+ options.err?.(t('reply.receivedFeishuFailed'));
71
72
  }
72
73
  }
73
74
  }
@@ -111,12 +112,12 @@ export async function attachReply(message, data, file, options = {}) {
111
112
  const ready = options.bridge ? await options.bridge(file, options.env ?? process.env)
112
113
  : (await bridgeStatus(file)).ready || await startBridge(file, options.env);
113
114
  if (!ready) {
114
- options.err?.('回复桥接还没有就绪,这条通知不会显示回复按钮。可运行 reply status 查看原因。');
115
+ options.err?.(t('reply.bridgeNotReadyButton'));
115
116
  return null;
116
117
  }
117
118
  const api = apiFor(access, options), bot = await api('getMe', {});
118
119
  if (typeof bot?.username !== 'string' || !/^[A-Za-z0-9_]{5,64}$/.test(bot.username))
119
- throw new ConfigError('Telegram Bot 用户名无效,回复按钮没有启用。');
120
+ throw new ConfigError(t('reply.botUsernameInvalid'));
120
121
  const token = randomBytes(16).toString('hex');
121
122
  await stateTransaction(access, state => { state.entries['token:' + token] = { session: target.session, epoch: targetEpoch(target), updated: Date.now() }; });
122
123
  message.reply = { token, url: `https://t.me/${bot.username}?start=${token}`, users: access.policy.users };
@@ -129,7 +130,7 @@ export function mapReplies(plan, binding) {
129
130
  if (!binding.access.policy.users.includes(item.recipient))
130
131
  return;
131
132
  if (!Number.isSafeInteger(response.result?.message_id))
132
- throw new ConfigError('通知没有返回可关联的 Telegram 消息 ID。');
133
+ throw new ConfigError(t('reply.noMessageId'));
133
134
  await stateTransaction(binding.access, state => {
134
135
  const entry = state.entries['token:' + binding.token];
135
136
  if (entry)
@@ -157,7 +158,7 @@ export async function interactionHook(data, file, options = {}) {
157
158
  // 登记阶段复用生命周期元数据;真正开启远程交互时仍重新核验进程身份。
158
159
  const target = cached ?? await getTarget(data, options);
159
160
  if (!target) {
160
- options.err?.('没有核验到会话,结构化交互将在本机处理。');
161
+ options.err?.(t('reply.noSession'));
161
162
  return;
162
163
  }
163
164
  if (data.hook_event_name === 'PreToolUse') {
@@ -169,13 +170,13 @@ export async function interactionHook(data, file, options = {}) {
169
170
  return;
170
171
  const call = await stateTransaction(access, state => matchingCall(state, target, data));
171
172
  if (!call) {
172
- options.err?.('没有唯一匹配到这次工具调用,远程交互没有启用;本机窗口不受影响。');
173
+ options.err?.(t('reply.noMatchingCall'));
173
174
  return;
174
175
  }
175
176
  const ready = options.bridge ? await options.bridge(file, options.env ?? process.env)
176
177
  : (await bridgeStatus(file)).ready || await startBridge(file, options.env);
177
178
  if (!ready) {
178
- options.err?.('回复桥接还没有就绪,问题将在本机处理。');
179
+ options.err?.(t('reply.bridgeNotReadyQuestion'));
179
180
  return;
180
181
  }
181
182
  const api = apiFor(access, options);
@@ -190,8 +191,8 @@ export async function interactionHook(data, file, options = {}) {
190
191
  if (access.config.feishu.enabled && access.config.feishu.webhook) {
191
192
  const bot = await api('getMe', {});
192
193
  if (typeof bot?.username === 'string' && /^[A-Za-z0-9_]{5,64}$/.test(bot.username)) {
193
- const result = await (options.send ?? deliver)(makePlan('feishu', { title: 'Claude Code 需要你处理', level: 'warn', footer: '',
194
- content: kind === 'question' ? questionsContent(request.questions) : '工具权限请求:' + data.tool_name,
194
+ const result = await (options.send ?? deliver)(makePlan('feishu', { title: t('hook.notificationTitle'), level: 'warn', footer: '',
195
+ content: kind === 'question' ? questionsContent(request.questions) : t('reply.permissionRequest', { tool: String(data.tool_name) }),
195
196
  reply: { token: request.id, url: `https://t.me/${bot.username}?start=${request.id}`, users: access.policy.users } }, access.config), access.config);
196
197
  if (result.ok)
197
198
  await stateTransaction(access, state => {
@@ -207,13 +208,13 @@ export async function interactionHook(data, file, options = {}) {
207
208
  if (response && !controller.signal.aborted)
208
209
  options.response?.(response);
209
210
  else
210
- options.err?.('远程交互已经结束,或答案没有提交。不会自动作答,本机操作不受影响。');
211
+ options.err?.(t('reply.interactionEnded'));
211
212
  }
212
213
  catch (error) {
213
214
  await stateTransaction(access, state => {
214
215
  const current = state.requests[request.id];
215
216
  if (current)
216
- finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '远程交互处理失败,请在本机检查。' });
217
+ finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', reason: 'failed', text: t('reply.interactionFailed') });
217
218
  }).catch(() => { });
218
219
  throw error;
219
220
  }
@@ -224,7 +225,7 @@ export async function interactionHook(data, file, options = {}) {
224
225
  await stateTransaction(access, state => {
225
226
  const current = state.requests[request.id];
226
227
  if (current)
227
- finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '交互等待已取消,远程答案是否生效还没有确认。' });
228
+ finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', reason: 'failed', text: t('reply.interactionCancelled') });
228
229
  }).catch(() => { });
229
230
  }
230
231
  }