feat(approval): 审批改用 TG 原生 inline keyboard 按钮
- 审批请求以「✅ 允许一次 / ❌ 拒绝」双按钮消息呈现,点按钮即裁决并 toast 确认 - 按钮发送失败自动降级为原文本提示(回复 A/B),审批绝不丢失 - 文本作答通道保留(handleTgApproval 的 A/B 解析不变),/cancel 语义不变
This commit is contained in:
Vendored
+3
@@ -144,6 +144,9 @@ export declare class TelegramBridge {
|
||||
private handleAskCallback;
|
||||
private installApprovalInterceptor;
|
||||
private aroundApprovalRequest;
|
||||
/** Approval prompt as native inline-keyboard buttons; falls back to a plain-text prompt. */
|
||||
private deliverApprovalPrompt;
|
||||
private handleApprovalCallback;
|
||||
private handleTgApproval;
|
||||
private stopBound;
|
||||
private sendMission;
|
||||
|
||||
+48
-2
@@ -60,8 +60,10 @@ const MODEL_CB = 'mdl:';
|
||||
/** Use eff: (not me:) — short prefix, no collision with other callbacks. */
|
||||
const EFFORT_CB = 'eff:';
|
||||
const BACK_MODEL_CB = 'mb';
|
||||
/** ask:{questionIndex}:{optionIndex|ok} — inline-keyboard taps for ask_user_question. */
|
||||
/** ask:{questionIndex}:{optionIndex|ok|custom} — inline-keyboard taps for ask_user_question. */
|
||||
const ASK_CB = 'ask:';
|
||||
/** apr:allow / apr:deny — inline-keyboard taps for tool approvals. */
|
||||
const APR_CB = 'apr:';
|
||||
const MAX_BUTTONS = 40;
|
||||
/** Marker stored on UserQuestionService.prototype for the ask interceptor. */
|
||||
const kTgAskWrapped = Symbol.for('dsh-telegram-channel.askWrapped');
|
||||
@@ -378,6 +380,10 @@ export class TelegramBridge {
|
||||
await this.handleAskCallback(chatId, cq);
|
||||
return;
|
||||
}
|
||||
if (data.startsWith(APR_CB)) {
|
||||
await this.handleApprovalCallback(chatId, cq, data);
|
||||
return;
|
||||
}
|
||||
if (data === LAST_CB) {
|
||||
await this.client.answerCallbackQuery(cq.id);
|
||||
await this.sendLastTurn(chatId);
|
||||
@@ -1736,7 +1742,9 @@ export class TelegramBridge {
|
||||
const reason = req?.reason ? String(req.reason).slice(0, 200) : '';
|
||||
for (const chatId of targets) {
|
||||
this.pendingApprovalsTG.set(chatId, { toolName, ask });
|
||||
this.enqueueNotice(Number(chatId), `权限审批:${toolName}${reason ? `\n(${reason})` : ''}\n回复 [A] 允许一次 / [B] 拒绝(/cancel 关闭 TG 审批)`);
|
||||
void this.deliverApprovalPrompt(Number(chatId), toolName, reason).catch((err) => {
|
||||
this.ctx.logger.error(this.redact(err));
|
||||
});
|
||||
}
|
||||
}
|
||||
// No bound TG chat: promise never settles — the UI remains the only answer path.
|
||||
@@ -1759,6 +1767,44 @@ export class TelegramBridge {
|
||||
});
|
||||
return Promise.race([promise, gui]);
|
||||
}
|
||||
/** Approval prompt as native inline-keyboard buttons; falls back to a plain-text prompt. */
|
||||
async deliverApprovalPrompt(chatId, toolName, reason) {
|
||||
const keyboard = {
|
||||
inline_keyboard: [[
|
||||
{ text: '✅ 允许一次', callback_data: `${APR_CB}allow` },
|
||||
{ text: '❌ 拒绝', callback_data: `${APR_CB}deny` },
|
||||
]],
|
||||
};
|
||||
try {
|
||||
await this.withRetry(() => this.client.sendMessage(chatId, `权限审批:${toolName}${reason ? `\n(${reason})` : ''}`, undefined, keyboard));
|
||||
}
|
||||
catch (err) {
|
||||
this.ctx.logger.error(this.redact(err));
|
||||
// Never lose an approval: degrade to the text-answer prompt.
|
||||
this.enqueueNotice(chatId, `权限审批:${toolName}${reason ? `\n(${reason})` : ''}\n回复 [A] 允许一次 / [B] 拒绝(/cancel 关闭 TG 审批)`);
|
||||
}
|
||||
}
|
||||
async handleApprovalCallback(chatId, cq, data) {
|
||||
const approval = this.pendingApprovalsTG.get(String(chatId));
|
||||
if (!approval) {
|
||||
await this.client.answerCallbackQuery(cq.id, '该审批已失效(可能已在 Web 端处理)');
|
||||
return;
|
||||
}
|
||||
const allow = data === `${APR_CB}allow`;
|
||||
approval.ask.answered = true;
|
||||
for (const [cid, p] of this.pendingApprovalsTG) {
|
||||
if (p.ask === approval.ask)
|
||||
this.pendingApprovalsTG.delete(cid);
|
||||
}
|
||||
if (allow) {
|
||||
approval.ask.resolve('allowed-once');
|
||||
await this.client.answerCallbackQuery(cq.id, `已允许 ${approval.toolName} 执行一次`);
|
||||
}
|
||||
else {
|
||||
approval.ask.resolve('rejected');
|
||||
await this.client.answerCallbackQuery(cq.id, '已拒绝该工具执行');
|
||||
}
|
||||
}
|
||||
async handleTgApproval(chatId, approval, text) {
|
||||
const cleaned = text.trim().replace(/^\[+/, '').replace(/\]+$/, '').toUpperCase();
|
||||
if (cleaned === 'A' || cleaned === 'B') {
|
||||
|
||||
Reference in New Issue
Block a user