fix(approval): prototype 拦截器替代 approval/request waterfall hook
与 ask_user 同一根因:'approval/request' waterfall 分发在 per-fiber events 注册表,本插件 ctx.on 监听收不到。改为包装 ApprovalService.prototype. request(类跨实例/fiber 共享),TG 审批答案与原请求竞速,保留 dual-path 语义;stop 时恢复原型。已验证运行态拦截器标记就位。
This commit is contained in:
+54
-9
@@ -65,6 +65,8 @@ const ASK_CB = 'ask:';
|
||||
const MAX_BUTTONS = 40;
|
||||
/** Marker stored on UserQuestionService.prototype for the ask interceptor. */
|
||||
const kTgAskWrapped = Symbol.for('dsh-telegram-channel.askWrapped');
|
||||
/** Marker stored on ApprovalService.prototype for the approval interceptor. */
|
||||
const kTgApprovalWrapped = Symbol.for('dsh-telegram-channel.approvalWrapped');
|
||||
function lastContextKeyboard() {
|
||||
return {
|
||||
inline_keyboard: [[{ text: '查看上次对话', callback_data: LAST_CB }]],
|
||||
@@ -132,10 +134,12 @@ export class TelegramBridge {
|
||||
pendingAsks = new Map();
|
||||
hookTimer;
|
||||
pendingApprovalsTG = new Map();
|
||||
disposeApprovalHook;
|
||||
/** Prototype-ask interceptor state (see installAskInterceptor). */
|
||||
askInstalled = false;
|
||||
askRestore;
|
||||
/** Prototype-approval interceptor state (see installApprovalInterceptor). */
|
||||
approvalInstalled = false;
|
||||
approvalRestore;
|
||||
/** callId → tool name (tool/result failure notices) */
|
||||
callNames = new Map();
|
||||
/** sessionId → latest todo snapshot (/mission) */
|
||||
@@ -180,8 +184,7 @@ export class TelegramBridge {
|
||||
// per-fiber events registry in dsh 0.1.5-rc.1), so the prototype wrap —
|
||||
// which sees EVERY ask regardless of fiber/instance — is the reliable seam.
|
||||
this.installAskInterceptor();
|
||||
this.disposeApprovalHook?.();
|
||||
this.disposeApprovalHook = this.ctx.on('approval/request', (req, next) => this.onApprovalRequest(req, next));
|
||||
this.installApprovalInterceptor();
|
||||
void this.client.setMyCommands([
|
||||
{ command: 'start', description: '欢迎与用法' },
|
||||
{ command: 'sessions', description: '按工作区列出并附着会话' },
|
||||
@@ -240,8 +243,9 @@ export class TelegramBridge {
|
||||
this.askRestore?.();
|
||||
this.askRestore = undefined;
|
||||
this.askInstalled = false;
|
||||
this.disposeApprovalHook?.();
|
||||
this.disposeApprovalHook = undefined;
|
||||
this.approvalRestore?.();
|
||||
this.approvalRestore = undefined;
|
||||
this.approvalInstalled = false;
|
||||
if (this.pollPromise) {
|
||||
await this.pollPromise.catch(() => { });
|
||||
this.pollPromise = undefined;
|
||||
@@ -1118,6 +1122,8 @@ export class TelegramBridge {
|
||||
async onSessionEvent(session, event) {
|
||||
if (!this.askInstalled)
|
||||
this.installAskInterceptor();
|
||||
if (!this.approvalInstalled)
|
||||
this.installApprovalInterceptor();
|
||||
const id = String(session.id);
|
||||
const targets = [...this.bindings.values()].filter((b) => b.sessionId === id);
|
||||
if (targets.length === 0)
|
||||
@@ -1661,8 +1667,47 @@ export class TelegramBridge {
|
||||
await this.client.answerCallbackQuery(cq.id, '已提交本题选择,请继续作答其他问题');
|
||||
}
|
||||
}
|
||||
// ── approval: TG answering via dual-path race on the request waterfall ──
|
||||
async onApprovalRequest(req, next) {
|
||||
// ── approval: TG answering via a prototype interceptor ──
|
||||
//
|
||||
// Same root cause and same cure as ask_user: the 'approval/request'
|
||||
// waterfall dispatches on a per-fiber events registry that this plugin's
|
||||
// ctx.on never sees (dsh 0.1.5-rc.1), so ApprovalService.prototype.request
|
||||
// is wrapped instead — the class is shared across instances and fibers.
|
||||
installApprovalInterceptor() {
|
||||
if (this.approvalInstalled)
|
||||
return;
|
||||
let svc;
|
||||
try {
|
||||
svc = this.ctx.get('approval');
|
||||
}
|
||||
catch {
|
||||
return; // service not ready yet — retried lazily from onSessionEvent
|
||||
}
|
||||
if (!svc)
|
||||
return;
|
||||
const proto = svc.constructor.prototype;
|
||||
const prev = proto[kTgApprovalWrapped];
|
||||
if (prev?.bridge === this) {
|
||||
this.approvalInstalled = true;
|
||||
this.approvalRestore = prev.restore;
|
||||
return;
|
||||
}
|
||||
prev?.restore(); // stale wrapper from a disposed bridge instance
|
||||
const orig = proto.request;
|
||||
const bridge = this;
|
||||
const wrapped = function (req) {
|
||||
return bridge.aroundApprovalRequest(this, req, orig);
|
||||
};
|
||||
proto.request = wrapped;
|
||||
const restore = () => {
|
||||
if (proto.request === wrapped)
|
||||
proto.request = orig;
|
||||
};
|
||||
proto[kTgApprovalWrapped] = { restore, bridge: this };
|
||||
this.approvalRestore = restore;
|
||||
this.approvalInstalled = true;
|
||||
}
|
||||
aroundApprovalRequest(service, req, orig) {
|
||||
const sessionId = req?.agent?.id !== undefined ? String(req.agent.id) : undefined;
|
||||
const targets = sessionId !== undefined
|
||||
? [...this.bindings.values()].filter((b) => b.sessionId === sessionId).map((b) => String(b.chatId))
|
||||
@@ -1683,7 +1728,7 @@ export class TelegramBridge {
|
||||
};
|
||||
if (req?.signal?.aborted) {
|
||||
rejectFn(new Error('approval withdrawn'));
|
||||
return next();
|
||||
return orig.call(service, req);
|
||||
}
|
||||
req?.signal?.addEventListener('abort', onAbort, { once: true });
|
||||
if (targets.length > 0) {
|
||||
@@ -1695,7 +1740,7 @@ export class TelegramBridge {
|
||||
}
|
||||
}
|
||||
// No bound TG chat: promise never settles — the UI remains the only answer path.
|
||||
const gui = next();
|
||||
const gui = Promise.resolve().then(() => orig.call(service, req));
|
||||
void gui.then(() => {
|
||||
for (const [chatId, p] of [...this.pendingApprovalsTG]) {
|
||||
if (p.ask !== ask)
|
||||
|
||||
Reference in New Issue
Block a user