@quecue/dev-notify 0.1.0-alpha.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/README.md +84 -65
- package/SECURITY.md +46 -46
- package/dist/channels.js +9 -9
- package/dist/claude-code.js +35 -36
- package/dist/claude-hook.js +36 -23
- package/dist/cli.js +91 -67
- package/dist/config.js +16 -16
- package/dist/credentials.js +13 -13
- package/dist/formatting.js +4 -4
- package/dist/prompts.js +6 -6
- package/dist/reply-bridge.js +9 -9
- package/dist/reply-engine.js +23 -17
- package/dist/reply-hooks.js +17 -17
- package/dist/reply-interactions.js +26 -19
- package/dist/reply-outcomes.js +13 -13
- package/dist/reply-platform.js +40 -13
- package/dist/reply-state.js +28 -28
- package/dist/reply-telegram.js +1 -1
- package/dist/setup.js +74 -43
- package/dist/update.js +92 -0
- package/package.json +1 -1
package/dist/reply-bridge.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/reply-engine.js
CHANGED
|
@@ -22,17 +22,22 @@ export async function resolveEntry(access, key, context) {
|
|
|
22
22
|
return null;
|
|
23
23
|
return resolved;
|
|
24
24
|
}
|
|
25
|
-
|
|
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, '⚠️ 无法回复', '
|
|
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
|
|
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, '⚠️
|
|
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: '
|
|
82
|
-
await replyHint(context.api, chat, '⚠️
|
|
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
|
-
|
|
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, '⚠️ 无法回复', '
|
|
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, '❌
|
|
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, '⏳
|
|
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('
|
|
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', '
|
|
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, '⚠️
|
|
202
|
+
await replyHint(context.api, pending.chat, '⚠️ 转发状态待确认', error instanceof ConfigError ? error.message : '提交没有完成,请检查本机终端;不会自动重复提交。', pending.message).catch(() => { });
|
|
197
203
|
}
|
|
198
204
|
}
|
|
199
205
|
}
|
package/dist/reply-hooks.js
CHANGED
|
@@ -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 已收到回复', '
|
|
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: '
|
|
64
|
+
content: 'Claude 已收到你提交的回复,任务完成后会再发通知。为减少扩散,这里不转发回复原文。',
|
|
65
65
|
footer: '会话:' + data.session_id }, access.config), access.config);
|
|
66
66
|
if (!result.ok)
|
|
67
|
-
options.err?.('
|
|
67
|
+
options.err?.('飞书:收到确认没有完整发送;已提交的输入不会重放。');
|
|
68
68
|
}
|
|
69
69
|
catch {
|
|
70
|
-
options.err?.('
|
|
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?.('
|
|
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('
|
|
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
|
|
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
|
|
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('
|
|
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' ? '
|
|
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' ? '仍需完成其余问题,之后才回传整份答案。' : '
|
|
187
|
-
+ '\n
|
|
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, '⚠️ 本次提交未生效', '
|
|
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' ? '
|
|
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, '❌
|
|
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
|
-
: '
|
|
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
|
}
|
package/dist/reply-outcomes.js
CHANGED
|
@@ -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 + '
|
|
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: '
|
|
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
|
}
|