@quecue/dev-notify 0.1.0-alpha.3 → 0.1.0

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.
@@ -44,7 +44,7 @@ export async function stopBridge(file) {
44
44
  export async function startBridge(file, env = process.env) {
45
45
  const access = await accessReplies(file, env);
46
46
  if (!access)
47
- throw new ConfigError('远程回复未授权,请先运行 reply setup');
47
+ throw new ConfigError('远程回复尚未授权,请先运行 reply setup。');
48
48
  const existing = await bridgeStatus(file);
49
49
  if (existing.running)
50
50
  return existing.ready;
@@ -72,7 +72,7 @@ export async function runBridge(file, options = {}) {
72
72
  const env = options.env ?? process.env;
73
73
  let access = await accessReplies(file, env);
74
74
  if (!access)
75
- throw new ConfigError('远程回复未授权');
75
+ throw new ConfigError('远程回复尚未授权。');
76
76
  const directory = replyDirectory(file);
77
77
  await mkdir(directory, { recursive: true, mode: 0o700 });
78
78
  const botDirectory = path.join(appPaths(process.platform, env).data, 'reply-bots');
@@ -83,7 +83,7 @@ export async function runBridge(file, options = {}) {
83
83
  lock = await open(lockPath, 'wx', 0o600);
84
84
  }
85
85
  catch {
86
- throw new ConfigError('本用户已有相同 Bot 桥接或残留锁;不会并发轮询。确认进程退出后再处理 reply-bots 中对应锁');
86
+ throw new ConfigError('当前用户已经有同一个 Bot 的桥接在运行,或残留了锁文件。请确认进程退出后,再处理 reply-bots 中对应的锁。');
87
87
  }
88
88
  const owner = randomBytes(16).toString('hex'), lease = { pid: process.pid, owner, ready: false, updated: Date.now() };
89
89
  let stopping = false;
@@ -100,7 +100,7 @@ export async function runBridge(file, options = {}) {
100
100
  let api = (options.api ?? (value => telegramApi(value.config.telegram.token)))(access);
101
101
  const webhook = await api('getWebhookInfo', {});
102
102
  if (webhook?.url)
103
- throw new ConfigError('Bot 已设置 webhook,不能同时使用 getUpdates;不会自动删除 webhook');
103
+ throw new ConfigError('这个 Bot 已经设置了 webhook,不能同时使用 getUpdates。请先移除 webhook;本工具不会自动删除。');
104
104
  await api('getMe', {});
105
105
  const currentState = await stateTransaction(access, state => ({ offset: state.offset, empty: Object.keys(state.entries).length === 0 }));
106
106
  if (!currentState.offset && currentState.empty) {
@@ -139,9 +139,9 @@ export async function runBridge(file, options = {}) {
139
139
  catch (error) {
140
140
  if (error instanceof ConfigError && /冲突/.test(error.message))
141
141
  throw error;
142
- log('Telegram 轮询失败;秘密和服务端正文未记录。');
142
+ log('Telegram 轮询失败;日志中没有记录密钥或服务端正文。');
143
143
  if (++failures >= 5)
144
- throw new ConfigError('Telegram 连续轮询失败 5 次,桥接已退出;请检查网络、Token 或 API 冲突后重新启动');
144
+ throw new ConfigError('Telegram 连续轮询失败 5 次,桥接已经退出。请检查网络、Token 或 API 冲突后重新启动。');
145
145
  lease.updated = Date.now();
146
146
  await atomicJson(leasePath(file), lease);
147
147
  await (options.sleep ?? delay)(3000);
@@ -152,7 +152,7 @@ export async function runBridge(file, options = {}) {
152
152
  if (!after || after.identity !== access.identity || await isStopped())
153
153
  break;
154
154
  if (!Array.isArray(updates))
155
- throw new ConfigError('Telegram 更新格式无效');
155
+ throw new ConfigError('Telegram 更新格式无效。');
156
156
  for (const update of updates) {
157
157
  if (await isStopped())
158
158
  break;
@@ -160,7 +160,7 @@ export async function runBridge(file, options = {}) {
160
160
  await processUpdate(access, update, context);
161
161
  }
162
162
  catch {
163
- log('回复处理失败;更新不重放,请检查终端或从最新通知重试。');
163
+ log('回复处理失败。这条更新不会重放;请检查终端,或从最新通知重试。');
164
164
  }
165
165
  }
166
166
  if (updates.length)
@@ -173,7 +173,7 @@ export async function runBridge(file, options = {}) {
173
173
  }
174
174
  catch (error) {
175
175
  lease.ready = false;
176
- lease.error = error instanceof ConfigError ? error.message : '桥接启动或运行失败,未输出底层错误';
176
+ lease.error = error instanceof ConfigError ? error.message : '桥接启动或运行失败;为避免泄露密钥,没有显示底层错误。';
177
177
  await atomicJson(leasePath(file), lease).catch(() => { });
178
178
  throw error;
179
179
  }
@@ -22,17 +22,22 @@ export async function resolveEntry(access, key, context) {
22
22
  return null;
23
23
  return resolved;
24
24
  }
25
- async function prompt(access, chat, messageId, key, context) {
25
+ // autoReply:Telegram 内联按钮沿用 force_reply,点击后自动进入回复状态。
26
+ // 飞书跳转使用普通提示 + remove_keyboard:清除客户端缓存的旧强制回复目标,但不拉起新的回复框。
27
+ async function prompt(access, chat, messageId, key, context, autoReply = true) {
26
28
  const resolved = await resolveEntry(access, key, context);
27
29
  if (!resolved) {
28
- await replyHint(context.api, chat, '⚠️ 无法回复', '该会话已关闭、切换或通知过期,请从最新通知进入。', messageId);
30
+ await replyHint(context.api, chat, '⚠️ 无法回复', '这个会话已经关闭、切换,或这条通知已过期。请从最新通知进入。', messageId);
29
31
  return;
30
32
  }
31
33
  if (resolved.entry.request) {
32
34
  await showInteraction(access, resolved.entry.request, chat, context.api);
33
35
  return;
34
36
  }
35
- const sent = await replyHint(context.api, chat, '💬 回复 Claude', '请回复此消息输入内容。该输入将提交到对应本机会话。\n会话:' + (resolved.target.title || resolved.target.session), messageId, { force_reply: true, selective: true });
37
+ const session = resolved.target.title || resolved.target.session;
38
+ 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 });
36
41
  if (Number.isSafeInteger(sent?.message_id))
37
42
  await stateTransaction(access, state => { state.entries[`tg:${chat}:${sent.message_id}`] = resolved.entry; });
38
43
  }
@@ -48,9 +53,9 @@ export async function processUpdate(access, update, context) {
48
53
  if (chat && current?.identity === access.identity && current.policy.users.includes(chat)) {
49
54
  if (typeof update?.callback_query?.id === 'string')
50
55
  await context.api('answerCallbackQuery', {
51
- callback_query_id: update.callback_query.id, text: '处理失败或结果未知,请检查终端;不会自动重复提交',
56
+ callback_query_id: update.callback_query.id, text: '处理没有完成,请检查终端;不会自动重复提交。',
52
57
  }).catch(() => { });
53
- await replyHint(context.api, chat, '⚠️ 本次提交结果无法确认', '处理发生异常,无法确认是否已登记或采用。请检查终端及后续结果反馈;不会自动重复提交。').catch(() => { });
58
+ await replyHint(context.api, chat, '⚠️ 本次提交没有完成', '处理时出现了异常,这次提交可能已经登记,也可能没有生效。请检查终端和后续结果;不会自动重复提交。').catch(() => { });
54
59
  }
55
60
  throw error;
56
61
  }
@@ -78,8 +83,8 @@ async function processAuthorizedUpdate(access, update, context) {
78
83
  if (typeof value === 'string' && value.startsWith('dn:q:')) {
79
84
  const id = value.split(':')[2];
80
85
  if (!/^[a-f0-9]{32}$/.test(id) || !await resolveEntry(access, 'token:' + id, context)) {
81
- await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: '会话已关闭或交互已失效' }).catch(() => { });
82
- await replyHint(context.api, chat, '⚠️ 本次提交未生效', '会话已关闭、切换或交互已失效;未回传答案。').catch(() => { });
86
+ await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: '会话已关闭,或这次交互已经失效。' }).catch(() => { });
87
+ await replyHint(context.api, chat, '⚠️ 这次提交没有生效', '会话已经关闭、切换,或交互已经失效,答案没有回传。').catch(() => { });
83
88
  return;
84
89
  }
85
90
  if (await handleInteractionCallback(access, callback, chat, context.api))
@@ -102,11 +107,12 @@ async function processAuthorizedUpdate(access, update, context) {
102
107
  if (!current.policy.users.includes(chat))
103
108
  return;
104
109
  if (typeof message.date === 'number' && Date.now() / 1000 - message.date > 300) {
105
- await replyHint(context.api, chat, '⚠️ 本次提交未生效', '这条输入消息已超过有效时间,未回传;请查看最新问题或终端状态。');
110
+ await replyHint(context.api, chat, '⚠️ 本次提交没有生效', '这条消息已经超过有效时间,答案没有回传。请查看最新问题或终端状态。');
106
111
  return;
107
112
  }
108
113
  if (/^\/start\s+[a-f0-9]{32}$/.test(text)) {
109
- await prompt(access, chat, message.message_id, 'token:' + text.split(/\s+/)[1], context);
114
+ // 飞书跳转:只发普通提示并清除旧键盘状态,不自动进入回复。
115
+ await prompt(access, chat, message.message_id, 'token:' + text.split(/\s+/)[1], context, false);
110
116
  return;
111
117
  }
112
118
  const replyTo = message.reply_to_message?.message_id;
@@ -116,7 +122,7 @@ async function processAuthorizedUpdate(access, update, context) {
116
122
  }
117
123
  const resolved = await resolveEntry(access, `tg:${chat}:${replyTo}`, context);
118
124
  if (!resolved) {
119
- await replyHint(context.api, chat, '⚠️ 无法回复', '通知已过期或对应会话已关闭、切换,请从最新通知回复。', message.message_id);
125
+ await replyHint(context.api, chat, '⚠️ 无法回复', '通知已过期,或对应会话已经关闭、切换。请从最新通知回复。', message.message_id);
120
126
  return;
121
127
  }
122
128
  if (resolved.entry.request) {
@@ -128,14 +134,14 @@ async function processAuthorizedUpdate(access, update, context) {
128
134
  safe = sanitizeReply(message.text);
129
135
  }
130
136
  catch (error) {
131
- await replyHint(context.api, chat, '❌ 未提交回复', error instanceof ConfigError ? error.message : '输入无效', message.message_id);
137
+ await replyHint(context.api, chat, '❌ 回复没有提交', error instanceof ConfigError ? error.message : '输入无效。', message.message_id);
132
138
  return;
133
139
  }
134
140
  const id = randomBytes(16).toString('hex');
135
141
  const queued = await stateTransaction(access, state => {
136
142
  const target = state.sessions[resolved.target.session];
137
143
  if (!target || sessionEpoch(target) !== resolved.entry.epoch)
138
- throw new ConfigError('会话已变化,未提交');
144
+ throw new ConfigError('会话已经变化,这次回复没有提交。');
139
145
  if (state.pending[target.session] && Date.now() - state.pending[target.session].updated < 5 * 60 * 1000)
140
146
  return null;
141
147
  state.pending[target.session] = { hash: textHash(safe), text: safe, epoch: resolved.entry.epoch, id, updated: Date.now(), queued: target.working,
@@ -143,7 +149,7 @@ async function processAuthorizedUpdate(access, update, context) {
143
149
  return target.working;
144
150
  });
145
151
  if (queued === null) {
146
- await replyHint(context.api, chat, '⏳ 未提交新回复', '该会话已有未确认回复,请等待收到确认或检查本机终端后再试。', message.message_id);
152
+ await replyHint(context.api, chat, '⏳ 新回复没有提交', '这个会话已有一条待确认的回复。请先等待确认,或检查本机终端后再试。', message.message_id);
147
153
  return;
148
154
  }
149
155
  if (queued)
@@ -164,9 +170,9 @@ export async function drainReplies(access, context) {
164
170
  if (!current || current.identity !== access.identity || !current.policy.users.includes(pending.chat))
165
171
  return;
166
172
  if (Date.now() - pending.updated > 5 * 60 * 1000)
167
- throw new ConfigError('排队回复已超过 5 分钟,未提交;请检查本机后重新回复');
173
+ throw new ConfigError('排队的回复已超过 5 分钟,没有提交。请检查本机后重新回复。');
168
174
  if (!target.tracked || pending.epoch !== sessionEpoch(target) || !await (context.validate ?? (value => validateTarget(value)))(target))
169
- throw new ConfigError('会话已关闭或切换,未提交');
175
+ throw new ConfigError('会话已经关闭或切换,这次回复没有提交。');
170
176
  const text = pending.text;
171
177
  const claimed = await stateTransaction(access, state => {
172
178
  const actual = state.pending[target.session], active = state.sessions[target.session];
@@ -183,7 +189,7 @@ export async function drainReplies(access, context) {
183
189
  if (!final || final.identity !== access.identity)
184
190
  return;
185
191
  await (context.inject ?? ((value, content) => injectReply(value, content)))(target, text);
186
- await replyHint(context.api, pending.chat, '✅ 已转发给 Claude', '终端输入已提交,等待 UserPromptSubmit 确认;这不等于 Claude 已开始处理。', pending.message);
192
+ await replyHint(context.api, pending.chat, '✅ 已转发给 Claude', '终端输入已提交,正在等待 UserPromptSubmit 确认。', pending.message);
187
193
  }
188
194
  catch (error) {
189
195
  await stateTransaction(access, state => {
@@ -193,7 +199,7 @@ export async function drainReplies(access, context) {
193
199
  delete value.text;
194
200
  }
195
201
  });
196
- await replyHint(context.api, pending.chat, '⚠️ 转发未确认', error instanceof ConfigError ? error.message : '提交失败或结果未知,请检查本机终端;不会自动重复提交。', pending.message).catch(() => { });
202
+ await replyHint(context.api, pending.chat, '⚠️ 转发状态待确认', error instanceof ConfigError ? error.message : '提交没有完成,请检查本机终端;不会自动重复提交。', pending.message).catch(() => { });
197
203
  }
198
204
  }
199
205
  }
@@ -5,7 +5,7 @@ import { findSession } from './reply-platform.js';
5
5
  import { textHash } from './reply-engine.js';
6
6
  import { telegramApi, replyHint } from './reply-telegram.js';
7
7
  import { bridgeStatus, startBridge } from './reply-bridge.js';
8
- import { createInteraction, showInteraction, waitInteraction } from './reply-interactions.js';
8
+ import { createInteraction, questionsContent, showInteraction, waitInteraction } from './reply-interactions.js';
9
9
  import { makePlan, deliver } from './channels.js';
10
10
  import { completeToolCall, endSessionInteractions, finishInteraction, matchingCall, registerToolCall } from './reply-outcomes.js';
11
11
  const apiFor = (access, options) => (options.api ?? (value => telegramApi(value.config.telegram.token)))(access);
@@ -27,13 +27,13 @@ export async function lifecycleHook(data, file, options = {}) {
27
27
  if (event === 'SessionEnd' || !target) {
28
28
  delete state.sessions[data.session_id];
29
29
  delete state.pending[data.session_id];
30
- endSessionInteractions(state, data.session_id, '会话已结束或无法核验');
30
+ endSessionInteractions(state, data.session_id, '会话已经结束,或无法核验身份');
31
31
  return null;
32
32
  }
33
33
  // 同进程 / 同 TTY 已切换会话时,旧映射不再允许输入。
34
34
  for (const [id, other] of Object.entries(state.sessions)) {
35
35
  if (id !== target.session && (other.pid === target.pid || (target.tty && other.tty === target.tty))) {
36
- endSessionInteractions(state, id, '会话已切换');
36
+ endSessionInteractions(state, id, '会话已经切换');
37
37
  delete state.sessions[id];
38
38
  delete state.pending[id];
39
39
  }
@@ -57,17 +57,17 @@ export async function lifecycleHook(data, file, options = {}) {
57
57
  return match ? pending : null;
58
58
  });
59
59
  if (receipt) {
60
- await replyHint(apiFor(access, options), receipt.chat, '✅ Claude 已收到回复', 'UserPromptSubmit 已匹配本次远程输入;这仅确认收到,不代表任务已经完成。', receipt.message).catch(() => options.err?.('Telegram: 收到确认发送失败;不会重放输入。'));
60
+ await replyHint(apiFor(access, options), receipt.chat, '✅ Claude 已收到回复', 'Claude 已收到你提交的回复,任务完成后会再发通知。', receipt.message).catch(() => options.err?.('Telegram:收到确认没有发送成功;已提交的输入不会重放。'));
61
61
  if (access.config.feishu.enabled && access.config.feishu.webhook) {
62
62
  try {
63
63
  const result = await (options.send ?? deliver)(makePlan('feishu', { title: 'Claude 已收到回复', level: 'info',
64
- content: 'UserPromptSubmit 已匹配远程输入;仅确认收到,不代表任务完成。为减少扩散,不转发回复原文。',
64
+ content: 'Claude 已收到你提交的回复,任务完成后会再发通知。为减少扩散,这里不转发回复原文。',
65
65
  footer: '会话:' + data.session_id }, access.config), access.config);
66
66
  if (!result.ok)
67
- options.err?.('feishu: 收到确认未完整发送;不会重放输入。');
67
+ options.err?.('飞书:收到确认没有完整发送;已提交的输入不会重放。');
68
68
  }
69
69
  catch {
70
- options.err?.('feishu: 收到确认发送失败;不会重放输入。');
70
+ options.err?.('飞书:收到确认发送失败;已提交的输入不会重放。');
71
71
  }
72
72
  }
73
73
  }
@@ -111,12 +111,12 @@ export async function attachReply(message, data, file, options = {}) {
111
111
  const ready = options.bridge ? await options.bridge(file, options.env ?? process.env)
112
112
  : (await bridgeStatus(file)).ready || await startBridge(file, options.env);
113
113
  if (!ready) {
114
- options.err?.('远程回复桥接未就绪,本次通知不展示回复按钮;可运行 reply status 检查。');
114
+ options.err?.('回复桥接还没有就绪,这条通知不会显示回复按钮。可运行 reply status 查看原因。');
115
115
  return null;
116
116
  }
117
117
  const api = apiFor(access, options), bot = await api('getMe', {});
118
118
  if (typeof bot?.username !== 'string' || !/^[A-Za-z0-9_]{5,64}$/.test(bot.username))
119
- throw new ConfigError('Telegram Bot 用户名无效,未启用回复按钮');
119
+ throw new ConfigError('Telegram Bot 用户名无效,回复按钮没有启用。');
120
120
  const token = randomBytes(16).toString('hex');
121
121
  await stateTransaction(access, state => { state.entries['token:' + token] = { session: target.session, epoch: targetEpoch(target), updated: Date.now() }; });
122
122
  message.reply = { token, url: `https://t.me/${bot.username}?start=${token}`, users: access.policy.users };
@@ -129,7 +129,7 @@ export function mapReplies(plan, binding) {
129
129
  if (!binding.access.policy.users.includes(item.recipient))
130
130
  return;
131
131
  if (!Number.isSafeInteger(response.result?.message_id))
132
- throw new ConfigError('通知未返回可映射的 Telegram 消息 ID');
132
+ throw new ConfigError('通知没有返回可关联的 Telegram 消息 ID。');
133
133
  await stateTransaction(binding.access, state => {
134
134
  const entry = state.entries['token:' + binding.token];
135
135
  if (entry)
@@ -157,7 +157,7 @@ export async function interactionHook(data, file, options = {}) {
157
157
  // 登记阶段复用生命周期元数据;真正开启远程交互时仍重新核验进程身份。
158
158
  const target = cached ?? await getTarget(data, options);
159
159
  if (!target) {
160
- options.err?.('无法核验会话,结构化交互保留本机处理。');
160
+ options.err?.('没有核验到会话,结构化交互将在本机处理。');
161
161
  return;
162
162
  }
163
163
  if (data.hook_event_name === 'PreToolUse') {
@@ -169,13 +169,13 @@ export async function interactionHook(data, file, options = {}) {
169
169
  return;
170
170
  const call = await stateTransaction(access, state => matchingCall(state, target, data));
171
171
  if (!call) {
172
- options.err?.('无法唯一关联工具调用,远程交互未启用;本机窗口保持正常。');
172
+ options.err?.('没有唯一匹配到这次工具调用,远程交互没有启用;本机窗口不受影响。');
173
173
  return;
174
174
  }
175
175
  const ready = options.bridge ? await options.bridge(file, options.env ?? process.env)
176
176
  : (await bridgeStatus(file)).ready || await startBridge(file, options.env);
177
177
  if (!ready) {
178
- options.err?.('回复桥接未就绪,问题保留本机处理。');
178
+ options.err?.('回复桥接还没有就绪,问题将在本机处理。');
179
179
  return;
180
180
  }
181
181
  const api = apiFor(access, options);
@@ -191,7 +191,7 @@ export async function interactionHook(data, file, options = {}) {
191
191
  const bot = await api('getMe', {});
192
192
  if (typeof bot?.username === 'string' && /^[A-Za-z0-9_]{5,64}$/.test(bot.username)) {
193
193
  const result = await (options.send ?? deliver)(makePlan('feishu', { title: 'Claude Code 需要你处理', level: 'warn', footer: '',
194
- content: kind === 'question' ? request.questions.map(q => q.question + '\n' + q.options.map(o => o.label + ':' + o.description).join('\n')).join('\n\n') : '工具权限请求:' + data.tool_name,
194
+ content: kind === 'question' ? questionsContent(request.questions) : '工具权限请求:' + data.tool_name,
195
195
  reply: { token: request.id, url: `https://t.me/${bot.username}?start=${request.id}`, users: access.policy.users } }, access.config), access.config);
196
196
  if (result.ok)
197
197
  await stateTransaction(access, state => {
@@ -207,13 +207,13 @@ export async function interactionHook(data, file, options = {}) {
207
207
  if (response && !controller.signal.aborted)
208
208
  options.response?.(response);
209
209
  else
210
- options.err?.('远程交互已结束或未提交;不会默认作答,本机操作不受影响。');
210
+ options.err?.('远程交互已经结束,或答案没有提交。不会自动作答,本机操作不受影响。');
211
211
  }
212
212
  catch (error) {
213
213
  await stateTransaction(access, state => {
214
214
  const current = state.requests[request.id];
215
215
  if (current)
216
- finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '远程交互处理失败;请检查本机,不会自动重复提交。' });
216
+ finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '远程交互处理失败,请在本机检查。' });
217
217
  }).catch(() => { });
218
218
  throw error;
219
219
  }
@@ -224,7 +224,7 @@ export async function interactionHook(data, file, options = {}) {
224
224
  await stateTransaction(access, state => {
225
225
  const current = state.requests[request.id];
226
226
  if (current)
227
- finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '交互等待已取消,远程答案未确认生效。' });
227
+ finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '交互等待已取消,远程答案是否生效还没有确认。' });
228
228
  }).catch(() => { });
229
229
  }
230
230
  }
@@ -7,7 +7,7 @@ import { finishInteraction } from './reply-outcomes.js';
7
7
  export const INTERACTION_MS = 5 * 60 * 1000;
8
8
  export function questionsFrom(input) {
9
9
  if (!Array.isArray(input.questions) || input.questions.length < 1 || input.questions.length > 4)
10
- throw new ConfigError('问题结构无效,请在本机回答');
10
+ throw new ConfigError('问题结构无效,请在本机回答。');
11
11
  const questions = input.questions;
12
12
  const seen = new Set();
13
13
  for (const q of questions) {
@@ -15,7 +15,7 @@ export function questionsFrom(input) {
15
15
  || typeof q.header !== 'string' || (q.multiSelect !== undefined && typeof q.multiSelect !== 'boolean') || !Array.isArray(q.options)
16
16
  || q.options.length < 2 || q.options.length > 4 || q.options.some(o => !o || typeof o.label !== 'string' || !o.label.trim()
17
17
  || typeof o.description !== 'string') || new Set(q.options.map(o => o.label)).size !== q.options.length) {
18
- throw new ConfigError('问题结构无效,请在本机回答');
18
+ throw new ConfigError('问题结构无效,请在本机回答。');
19
19
  }
20
20
  seen.add(q.question);
21
21
  }
@@ -23,7 +23,7 @@ export function questionsFrom(input) {
23
23
  }
24
24
  export async function createInteraction(access, target, kind, input, call) {
25
25
  if (kind === 'permission' && access.policy.permissions !== true)
26
- throw new ConfigError('未授权远程工具权限处理');
26
+ throw new ConfigError('未授权远程处理工具权限。');
27
27
  const questions = kind === 'question' ? questionsFrom(input) : [];
28
28
  const value = { id: randomBytes(16).toString('hex'), session: target.session, epoch: targetEpoch(target),
29
29
  updated: Date.now(), expires: Date.now() + INTERACTION_MS, kind, input: structuredClone(input), questions,
@@ -31,11 +31,11 @@ export async function createInteraction(access, target, kind, input, call) {
31
31
  await stateTransaction(access, state => {
32
32
  const current = state.sessions[target.session];
33
33
  if (!current || targetEpoch(current) !== value.epoch)
34
- throw new ConfigError('会话目标已变化');
34
+ throw new ConfigError('会话目标发生了变化。');
35
35
  if (call) {
36
36
  const bound = state.calls[call];
37
37
  if (!bound || bound.ended || bound.request || bound.epoch !== value.epoch)
38
- throw new ConfigError('调用已结束或已绑定;未创建远程问题');
38
+ throw new ConfigError('调用已经结束或已绑定,没有创建远程问题。');
39
39
  bound.request = value.id;
40
40
  }
41
41
  // 不同 tool_use_id 可并行;无绑定的单元级调用仍只保留一个活动交互。
@@ -72,12 +72,19 @@ export function interactionMarkup(value) {
72
72
  export function interactionContent(value) {
73
73
  if (value.kind === 'permission')
74
74
  return '工具:' + String(value.input.tool_name || '未知') + '\n\n' + JSON.stringify(value.input.tool_input ?? {}, null, 2)
75
- + '\n\n仅支持本次允许、拒绝或自定义拒绝说明;官方未提供完整本地按钮列表。不会创建永久权限规则。';
75
+ + '\n\n这里处理本次的允许、拒绝或自定义拒绝说明。Claude 没有提供完整的本地按钮列表,因此这里也不会创建永久权限规则。';
76
76
  const q = value.questions[value.index];
77
77
  return `问题 ${value.index + 1}/${value.questions.length}:${q.question}\n\n`
78
78
  + q.options.map((o, i) => `${i + 1}. ${o.label}\n${o.description}${o.preview ? '\n' + o.preview : ''}`).join('\n\n')
79
79
  + (q.multiSelect ? '\n\n可多选,选好后点击「提交所选项」;也可自定义输入。' : '\n\n点击选项直接回答,或选择自定义输入。')
80
- + '\n本机原生窗口同时可答;Claude 先收到的完整答案生效。远程窗口 5 分钟有效,不会默认选择。';
80
+ + '\n本机原生窗口也可以同时作答;Claude 先收到的完整答案生效。远程窗口有效期为 5 分钟,不会自动选择。';
81
+ }
82
+ // 飞书卡片没有回调按钮,只展示全部题目和跳转入口;逐题编号并空行分隔,避免题目与选项粘连。
83
+ export function questionsContent(questions) {
84
+ return questions.map((q, index) => `问题 ${index + 1}/${questions.length}:${q.question}\n\n`
85
+ + q.options.map((o, i) => `${i + 1}. ${o.label}\n${o.description}${o.preview ? '\n' + o.preview : ''}`).join('\n\n')
86
+ + (q.multiSelect ? '\n\n可多选:进入 Telegram 后逐项选择,再点击「提交所选项」;也可自定义输入。'
87
+ : '\n\n进入 Telegram 后点击选项直接回答,或选择自定义输入。')).join('\n\n────────\n\n');
81
88
  }
82
89
  export async function showInteraction(access, id, chat, api) {
83
90
  const value = await stateTransaction(access, state => state.requests[id]);
@@ -93,7 +100,7 @@ export async function showInteraction(access, id, chat, api) {
93
100
  plan.onSent = async (item, response) => {
94
101
  const message = response.result?.message_id;
95
102
  if (!Number.isSafeInteger(message))
96
- throw new ConfigError('交互通知未返回消息 ID');
103
+ throw new ConfigError('交互通知没有返回消息 ID。');
97
104
  await stateTransaction(access, state => {
98
105
  const current = state.requests[id];
99
106
  if (current) {
@@ -112,7 +119,7 @@ export async function showInteraction(access, id, chat, api) {
112
119
  // API 封装由测试或真实桥接注入,不另建网络连接。
113
120
  const result = await deliver(plan, config, async (_url, payload) => ({ ok: true, result: await api('sendMessage', payload) }));
114
121
  if (!result.ok)
115
- throw new ConfigError('交互通知未完整发送,请回到本机处理');
122
+ throw new ConfigError('交互通知没有完整发送,请回到本机处理。');
116
123
  await stateTransaction(access, state => {
117
124
  const current = state.requests[id];
118
125
  if (!current)
@@ -181,17 +188,17 @@ export async function handleInteractionCallback(access, callback, chat, api) {
181
188
  return operation === 'c' ? 'custom' : value.response ? 'done' : value.index !== previousIndex ? 'answered' : 'next';
182
189
  });
183
190
  await api('answerCallbackQuery', { callback_query_id: callback.id,
184
- text: outcome === 'expired' ? '未提交:按钮已失效或已有答案待确认' : outcome === 'local' ? '已关闭远程答题,本机不受影响' : outcome === 'done' || outcome === 'answered' ? '答案已登记,尚未确认生效' : '已更新' }).catch(() => { });
191
+ text: outcome === 'expired' ? '没有提交:按钮已失效,或已有答案等待确认。' : outcome === 'local' ? '已关闭远程答题,本机不受影响。' : outcome === 'done' || outcome === 'answered' ? '答案已登记,尚未确认生效。' : '已更新。' }).catch(() => { });
185
192
  if (outcome === 'done' || outcome === 'answered')
186
- await replyHint(api, chat, '⏳ 答案已登记,等待确认', (outcome === 'answered' ? '仍需完成其余问题,之后才回传整份答案。' : '已进入回传流程,尚不代表 Claude 采用。')
187
- + '\n稍后反馈生效、未生效或结果未知;若未收到最终反馈,请检查终端。\n请求:' + id.slice(0, 8)).catch(() => { });
193
+ await replyHint(api, chat, '⏳ 答案已登记,等待确认', (outcome === 'answered' ? '仍需完成其余问题,之后才回传整份答案。' : '已进入回传流程,Claude 是否采用会在稍后反馈。')
194
+ + '\n稍后会反馈结果;如果一直没有收到,请检查终端。\n请求:' + id.slice(0, 8)).catch(() => { });
188
195
  if (outcome === 'expired')
189
- await replyHint(api, chat, '⚠️ 本次提交未生效', '此操作未提交:问题已处理、过期、题目已切换或已有答案等待确认。\n请求:' + id.slice(0, 8)).catch(() => { });
196
+ await replyHint(api, chat, '⚠️ 本次提交未生效', '这次操作没有提交:问题已处理、过期、题目已切换,或已有答案等待确认。\n请求:' + id.slice(0, 8)).catch(() => { });
190
197
  if (outcome === 'custom') {
191
198
  const value = await stateTransaction(access, state => state.requests[id]);
192
199
  if (!active(value))
193
200
  return true;
194
- const sent = await replyHint(api, chat, '✍️ 自定义输入', value.kind === 'permission' ? '请回复此消息填写拒绝说明;此输入不会批准权限。' : '请回复此消息输入这个问题的完整答案。', callback.message.message_id, { force_reply: true, selective: true });
201
+ const sent = await replyHint(api, chat, '✍️ 自定义输入', value.kind === 'permission' ? '请回复这条消息,填写拒绝说明。这段说明不会批准权限。' : '请回复这条消息,输入这个问题的完整答案。', callback.message.message_id, { force_reply: true, selective: true });
195
202
  if (Number.isSafeInteger(sent?.message_id))
196
203
  await stateTransaction(access, state => {
197
204
  state.entries[`tg:${chat}:${sent.message_id}`] = { session: value.session, epoch: value.epoch, request: id, question: value.index, updated: Date.now() };
@@ -204,7 +211,7 @@ export async function handleInteractionCallback(access, callback, chat, api) {
204
211
  export async function handleInteractionText(access, id, chat, message, api) {
205
212
  const text = message.text;
206
213
  if (!text.trim() || Array.from(text).length > 4000) {
207
- await replyHint(api, chat, '❌ 未提交答案', '请输入不超过 4000 字符的答案;不会截断。', message.message_id);
214
+ await replyHint(api, chat, '❌ 答案没有提交', '请输入不超过 4000 字符的答案;内容不会被截断。', message.message_id);
208
215
  return;
209
216
  }
210
217
  const outcome = await stateTransaction(access, state => {
@@ -224,9 +231,9 @@ export async function handleInteractionText(access, id, chat, message, api) {
224
231
  question, answer: value.kind === 'question' ? text : 'deny', custom: true });
225
232
  return value.response ? 'done' : 'next';
226
233
  });
227
- await replyHint(api, chat, outcome === 'expired' ? '⚠️ 本次提交未生效' : '⏳ 答案已登记,等待确认', (outcome === 'done' ? '等待 Claude 确认,稍后反馈最终结果;若未收到反馈请检查终端。'
228
- : outcome === 'next' ? '仍需完成其余问题,之后才回传整份答案;尚未生效。'
229
- : '此操作未提交:对应问题已结束、过期、已切换或已有答案等待确认。') + '\n请求:' + id.slice(0, 8));
234
+ await replyHint(api, chat, outcome === 'expired' ? '⚠️ 本次提交未生效' : '⏳ 答案已登记,等待确认', (outcome === 'done' ? '等待 Claude 确认,稍后会反馈最终结果;如果一直没有收到,请检查终端。'
235
+ : outcome === 'next' ? '仍需完成其余问题,之后才会回传整份答案;目前还没有生效。'
236
+ : '这次操作没有提交:对应问题已经结束、过期、切换,或已有答案等待确认。') + '\n请求:' + id.slice(0, 8));
230
237
  if (outcome === 'next')
231
238
  await showInteraction(access, id, chat, api);
232
239
  }
@@ -256,7 +263,7 @@ export async function waitInteraction(access, value, fresh, options = {}) {
256
263
  await stateTransaction(access, state => {
257
264
  const request = state.requests[value.id];
258
265
  if (request && !request.result)
259
- finishInteraction(request, { kind: request.claimed ? 'unknown' : 'not-applied', text: options.signal?.aborted ? '交互等待已取消;远程答案未确认生效。' : '远程交互等待超时;未确认生效,本机仍可继续处理。' });
266
+ finishInteraction(request, { kind: request.claimed ? 'unknown' : 'not-applied', text: options.signal?.aborted ? '交互等待已取消,远程答案是否生效还没有确认。' : '远程交互等待超时,尚未确认生效。你仍可在本机继续处理。' });
260
267
  });
261
268
  return null;
262
269
  }
@@ -35,7 +35,7 @@ export function endSessionInteractions(state, session, reason) {
35
35
  if (value.session === session && !value.result)
36
36
  finishInteraction(value, {
37
37
  kind: value.claimed ? 'unknown' : 'not-applied',
38
- text: value.claimed ? reason + ';没有收到本次答案的生效确认,请检查终端。不会重复提交。' : reason + ';本次远程答案未回传。',
38
+ text: value.claimed ? reason + ';还没有收到本次答案的生效确认,请检查终端。' : reason + ';这次远程答案没有回传。',
39
39
  });
40
40
  }
41
41
  export async function registerToolCall(access, target, data) {
@@ -92,24 +92,24 @@ export async function completeToolCall(access, data) {
92
92
  const q = value.questions.find(q => q.question === receipt.question);
93
93
  const exists = valid && answered && receipt.question !== undefined && Object.hasOwn(answers, receipt.question)
94
94
  && !output.annotations?.[receipt.question]?.unanswered;
95
- receipt.result = !value.claimed ? { kind: 'not-applied', text: '本机或其他应答端已先处理;本次 Telegram 答案未回传。' }
96
- : !valid ? { kind: 'unknown', text: 'Claude 已结束问题,但未提供可核实的答案结果。请检查终端;不会重复提交。' }
95
+ receipt.result = !value.claimed ? { kind: 'not-applied', text: '本机或其他端已经先处理了,这次 Telegram 答案没有提交。' }
96
+ : !valid ? { kind: 'unknown', text: 'Claude 已经结束了这个问题,但没有返回可核实的结果。请检查终端。' }
97
97
  : exists && sameAnswer(answers[receipt.question], receipt.answer, q?.multiSelect === true && receipt.custom !== true && q.options.every(o => !o.label.includes(',')))
98
- ? { kind: 'applied', text: 'Claude 已确认采用与你提交内容一致的答案。若本机同时提交了相同内容,无法仅凭结果区分来源。' }
99
- : { kind: 'not-applied', text: 'Claude 已结束问题,但没有采用你提交的答案;本机或其他应答端可能已先处理。' };
98
+ ? { kind: 'applied', text: 'Claude 已确认采用了与你提交内容一致的答案。如果本机也提交了相同内容,这次结果无法说明是哪一方生效。' }
99
+ : { kind: 'not-applied', text: 'Claude 已经结束问题,但没有采用你提交的答案;本机或其他端可能已经先处理。' };
100
100
  }
101
101
  finishInteraction(value, { kind: 'not-applied', text: '问题已处理,旧按钮已失效。' });
102
102
  }
103
103
  else if (data.hook_event_name === 'PostToolUse' && value.kind === 'permission') {
104
104
  const allow = value.claimed && value.receipts?.some(r => r.answer === 'allow');
105
105
  finishInteraction(value, allow
106
- ? { kind: 'applied', text: '已确认该工具获准并执行完成;无法仅凭完成事件区分是否由本机同时批准。' }
107
- : { kind: 'not-applied', text: '该工具已获准执行;本次远程决定未阻止执行。' });
106
+ ? { kind: 'applied', text: '工具已获准并执行完成。如果本机同时也批准了,完成事件本身无法说明是哪一方放行。' }
107
+ : { kind: 'not-applied', text: '工具已经获准执行,这次远程决定没有阻止执行。' });
108
108
  }
109
109
  else
110
110
  finishInteraction(value, { kind: value.claimed ? 'unknown' : 'not-applied', text: value.claimed
111
- ? '工具失败、拒绝或取消;此事件不能确认远程决定是否被采用。请检查终端,不会重复提交。'
112
- : '请求已失败、拒绝或取消,本次远程决定未回传。' });
111
+ ? '工具执行失败、被拒绝或取消。这个事件无法说明远程决定是否生效,请检查终端。'
112
+ : '请求失败、被拒绝或取消,这次远程决定没有回传。' });
113
113
  });
114
114
  }
115
115
  export async function drainInteractionFeedback(access, api, fresh, log = () => { }) {
@@ -122,8 +122,8 @@ export async function drainInteractionFeedback(access, api, fresh, log = () => {
122
122
  if (!value.result && (!target || targetEpoch(target) !== value.epoch || value.expires <= Date.now()))
123
123
  finishInteraction(value, {
124
124
  kind: value.claimed ? 'unknown' : 'not-applied', text: value.claimed
125
- ? '等待生效确认超时或会话已切换;结果未知,请检查终端。不会自动重复提交。'
126
- : '交互已过期或会话已结束;远程答案未回传,本机仍可按实际界面处理。',
125
+ ? '等待生效确认超时,或会话已经切换。结果暂时未知,请检查终端。'
126
+ : '交互已过期或会话已结束;远程答案没有回传,你仍可在本机界面继续处理。',
127
127
  });
128
128
  }
129
129
  return Object.values(state.requests).filter(value => value.result || value.receipts?.some(r => r.result));
@@ -151,7 +151,7 @@ export async function drainInteractionFeedback(access, api, fresh, log = () => {
151
151
  }
152
152
  catch {
153
153
  delivery = 'unknown';
154
- log('Telegram 交互结果反馈发送失败或结果未知;不自动重发,不影响已采用的答案。');
154
+ log('Telegram 结果反馈没有发送成功。不会自动重发,已经采用的答案不受影响。');
155
155
  }
156
156
  await stateTransaction(access, state => {
157
157
  const actual = state.requests[value.id]?.receipts?.find(r => r.id === receipt.id);
@@ -176,7 +176,7 @@ export async function drainInteractionFeedback(access, api, fresh, log = () => {
176
176
  const label = value.result.kind === 'unknown' ? '已关闭 · 结果待核实' : value.result.text.includes('过期') ? '已过期 · 不再接受答案' : '已处理 · 不再接受答案';
177
177
  await api('editMessageReplyMarkup', { chat_id: message.chat, message_id: message.message,
178
178
  reply_markup: { inline_keyboard: [[{ text: label, callback_data: `dn:q:${value.id}:0:l` }]] } })
179
- .catch(() => log('Telegram 旧按钮移除失败;服务端状态仍拒绝该请求。'));
179
+ .catch(() => log('Telegram 旧按钮没有移除成功。服务端仍会拒绝这个请求。'));
180
180
  }
181
181
  }
182
182
  }