From 2c50a3579576baeb21d5d84a1771125bd1a1544b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 17 Sep 2026 11:21:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(ask):=20=E9=94=AE=E7=9B=98=E5=B8=B8?= =?UTF-8?q?=E6=80=81=E6=8F=90=E4=BE=9B=20=E2=9C=8F=EF=B8=8F=20=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E8=87=AA=E5=AE=9A=E4=B9=89=E7=AD=94=E6=A1=88=20?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 每个带选项的提问键盘固定追加第三行 ✏️ 输入自定义答案 - 点击后该题进入待文本状态(textWanted),丢弃该题已点选的按钮项 - 下一条文本消息自动填充最先待文本的题;带 1:/2: 前缀的作答同样可清除待文本状态 - 已实测:✏️ → 发送 111 → custom 答案正确回流 --- lib/bridge.js | 23 +++++++++++++++++++++-- src/bridge.ts | 24 ++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/bridge.js b/lib/bridge.js index c2b2178..01c2e17 100644 --- a/lib/bridge.js +++ b/lib/bridge.js @@ -1464,6 +1464,7 @@ export class TelegramBridge { at: Date.now(), kbSel: new Map(), textAnswers: new Map(), + textWanted: new Set(), }; this.pendingAsks.set(String(chatId), pending); void this.deliverAskPrompt(Number(chatId), pending); @@ -1492,6 +1493,8 @@ export class TelegramBridge { text: `${j + 1}. ${o.label}`.slice(0, 48), callback_data: `${ASK_CB}${i}:${j}`, }]), + // Standing third choice: free-text answer, always available. + [{ text: '✏️ 输入自定义答案', callback_data: `${ASK_CB}${i}:custom` }], ...(q.multiSelect ? [[{ text: '✅ 提交所选', callback_data: `${ASK_CB}${i}:ok` }]] : []), ], }; @@ -1557,8 +1560,15 @@ export class TelegramBridge { parts.push([null, line]); } if (parts.some(([idx]) => idx === null)) { - await this.enqueueNotice(chatId, `共 ${questions.length} 个问题,请每行用 1:/2: 前缀分别作答`); - return; + // ✏️ free-text route: a question awaiting text absorbs the whole message. + const target = [...pending.textWanted].sort((a, b) => a - b)[0]; + if (target !== undefined) { + parts = [[target, text.trim()]]; + } + else { + await this.enqueueNotice(chatId, `共 ${questions.length} 个问题,请每行用 1:/2: 前缀分别作答`); + return; + } } } for (const [idx, raw] of parts) { @@ -1588,6 +1598,7 @@ export class TelegramBridge { else { pending.textAnswers.set(idx, { id: q.id, selected: [], custom: cleaned }); } + pending.textWanted.delete(idx); } // Keyboard selections take precedence per question; resolve when complete. if (!this.finalizeAskIfComplete(chatId, pending)) { @@ -1612,6 +1623,14 @@ export class TelegramBridge { return; } const opts = Array.isArray(q.options) ? q.options : []; + if (action === 'custom') { + // Free-text path: discard button selections for this question and wait + // for the user's next text message (routed via textWanted). + pending.kbSel.delete(qi); + pending.textWanted.add(qi); + await this.client.answerCallbackQuery(cq.id, '请直接发送你的答案文本'); + return; + } if (action === 'ok') { if (!pending.kbSel.get(qi)?.size) { await this.client.answerCallbackQuery(cq.id, '请先点选选项'); diff --git a/src/bridge.ts b/src/bridge.ts index 4e6725c..1427c5d 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -101,6 +101,8 @@ interface TgPendingAsk { kbSel: Map> /** question index → answer parsed from a text reply (letters or custom). */ textAnswers: Map + /** questions the user chose to answer with free text (✏️ button): the next text reply fills them. */ + textWanted: Set } /** chatId → pending approval request mirrored to Telegram. */ @@ -1636,6 +1638,7 @@ export class TelegramBridge { at: Date.now(), kbSel: new Map(), textAnswers: new Map(), + textWanted: new Set(), } this.pendingAsks.set(String(chatId), pending) void this.deliverAskPrompt(Number(chatId), pending) @@ -1672,6 +1675,8 @@ export class TelegramBridge { text: `${j + 1}. ${o.label}`.slice(0, 48), callback_data: `${ASK_CB}${i}:${j}`, }]), + // Standing third choice: free-text answer, always available. + [{ text: '✏️ 输入自定义答案', callback_data: `${ASK_CB}${i}:custom` }], ...(q.multiSelect ? [[{ text: '✅ 提交所选', callback_data: `${ASK_CB}${i}:ok` }]] : []), ], } @@ -1734,8 +1739,14 @@ export class TelegramBridge { else parts.push([null, line]) } if (parts.some(([idx]) => idx === null)) { - await this.enqueueNotice(chatId, `共 ${questions.length} 个问题,请每行用 1:/2: 前缀分别作答`) - return + // ✏️ free-text route: a question awaiting text absorbs the whole message. + const target = [...pending.textWanted].sort((a, b) => a - b)[0] + if (target !== undefined) { + parts = [[target, text.trim()]] + } else { + await this.enqueueNotice(chatId, `共 ${questions.length} 个问题,请每行用 1:/2: 前缀分别作答`) + return + } } } for (const [idx, raw] of parts) { @@ -1764,6 +1775,7 @@ export class TelegramBridge { } else { pending.textAnswers.set(idx, { id: q.id, selected: [], custom: cleaned }) } + pending.textWanted.delete(idx) } // Keyboard selections take precedence per question; resolve when complete. if (!this.finalizeAskIfComplete(chatId, pending)) { @@ -1789,6 +1801,14 @@ export class TelegramBridge { return } const opts = Array.isArray(q.options) ? q.options : [] + if (action === 'custom') { + // Free-text path: discard button selections for this question and wait + // for the user's next text message (routed via textWanted). + pending.kbSel.delete(qi) + pending.textWanted.add(qi) + await this.client.answerCallbackQuery(cq.id, '请直接发送你的答案文本') + return + } if (action === 'ok') { if (!pending.kbSel.get(qi)?.size) { await this.client.answerCallbackQuery(cq.id, '请先点选选项')