@wu529778790/open-im 1.11.2-beta.23 → 1.11.2-beta.24

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.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 选择检测器 — 从 AI 输出中检测并提取编号选项
3
+ *
4
+ * 当 AI 问"请选择 1/2/3"时,提取选项并返回结构化数据。
5
+ */
6
+ export interface DetectedChoice {
7
+ number: number;
8
+ text: string;
9
+ }
10
+ export interface DetectionResult {
11
+ /** 是否检测到选择 */
12
+ hasChoices: boolean;
13
+ /** 提取的选项列表 */
14
+ choices: DetectedChoice[];
15
+ /** 去除选项后的纯文本(用于显示) */
16
+ cleanText: string;
17
+ }
18
+ /**
19
+ * 检测 AI 输出中的编号选择
20
+ *
21
+ * 支持格式:
22
+ * - "1. Option A\n2. Option B\n3. Option C"
23
+ * - "1)Option A\n2)Option B"
24
+ * - "**选择 1:** Option A"
25
+ */
26
+ export declare function detectChoices(text: string): DetectionResult;
27
+ /**
28
+ * 构建 Telegram inline keyboard
29
+ */
30
+ export declare function buildChoiceKeyboard(choices: DetectedChoice[], userId: string): {
31
+ inline_keyboard: Array<Array<{
32
+ text: string;
33
+ callback_data: string;
34
+ }>>;
35
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 选择检测器 — 从 AI 输出中检测并提取编号选项
3
+ *
4
+ * 当 AI 问"请选择 1/2/3"时,提取选项并返回结构化数据。
5
+ */
6
+ /**
7
+ * 检测 AI 输出中的编号选择
8
+ *
9
+ * 支持格式:
10
+ * - "1. Option A\n2. Option B\n3. Option C"
11
+ * - "1)Option A\n2)Option B"
12
+ * - "**选择 1:** Option A"
13
+ */
14
+ export function detectChoices(text) {
15
+ // 匹配 "1." 或 "1)" 或 "1:" 开头的行
16
+ const choicePattern = /^\s*(\d+)[.):]\s*(.+)$/gm;
17
+ const matches = [];
18
+ let match;
19
+ while ((match = choicePattern.exec(text)) !== null) {
20
+ const num = parseInt(match[1], 10);
21
+ const content = match[2].trim();
22
+ if (num >= 1 && num <= 9 && content.length > 0) {
23
+ matches.push({ number: num, text: content });
24
+ }
25
+ }
26
+ // 检查是否有选择提示(如"请选择"、"选择哪个"等)
27
+ const hasChoicePrompt = /请选择|选择哪个|选一个|pick|choose|select/i.test(text);
28
+ // 至少需要 2 个选项且有选择提示
29
+ const hasChoices = matches.length >= 2 && hasChoicePrompt;
30
+ if (!hasChoices) {
31
+ return { hasChoices: false, choices: [], cleanText: text };
32
+ }
33
+ // 去除选项行,保留其他内容
34
+ const lines = text.split('\n');
35
+ const cleanLines = lines.filter(line => {
36
+ const trimmed = line.trim();
37
+ return !/^\s*\d+[.):]\s*.+/.test(trimmed);
38
+ });
39
+ return {
40
+ hasChoices: true,
41
+ choices: matches,
42
+ cleanText: cleanLines.join('\n').trim(),
43
+ };
44
+ }
45
+ /**
46
+ * 构建 Telegram inline keyboard
47
+ */
48
+ export function buildChoiceKeyboard(choices, userId) {
49
+ const buttons = [];
50
+ // 每行 1 个按钮(选项通常较长)
51
+ for (const choice of choices) {
52
+ buttons.push([{
53
+ text: `${choice.number}. ${choice.text}`,
54
+ callback_data: `choice:${userId}:${choice.number}`,
55
+ }]);
56
+ }
57
+ return { inline_keyboard: buttons };
58
+ }
@@ -310,6 +310,36 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
310
310
  await ctx.answerCbQuery("任务已完成或不存在");
311
311
  }
312
312
  }
313
+ // 处理选择回调
314
+ if (data.startsWith("choice:")) {
315
+ const parts = data.split(":");
316
+ if (parts.length >= 3) {
317
+ const choiceNum = parts[2];
318
+ const chatId = String(ctx.chat?.id ?? "");
319
+ // 将用户选择作为消息发送给 AI
320
+ const { handleTextFlow } = await import("../platform/handle-text-flow.js");
321
+ await handleTextFlow({
322
+ platform: "telegram",
323
+ userId,
324
+ chatId,
325
+ text: choiceNum,
326
+ ctx: createPlatformEventContext({
327
+ platform: "telegram",
328
+ allowedUserIds: config.telegramAllowedUserIds,
329
+ config,
330
+ sessionManager,
331
+ sender: { sendTextReply: async () => { } },
332
+ }),
333
+ handleAIRequest: async () => { },
334
+ sendTextReply: async (c, t) => {
335
+ await sendTextReply(c, t);
336
+ },
337
+ workDir: sessionManager.getWorkDir(userId),
338
+ convId: sessionManager.getConvId(userId),
339
+ });
340
+ await ctx.answerCbQuery(`已选择 ${choiceNum}`);
341
+ }
342
+ }
313
343
  });
314
344
  bot.on(message("text"), async (tgCtx) => {
315
345
  try {
@@ -7,6 +7,7 @@ import { buildMessageTitle, OPEN_IM_SYSTEM_TITLE } from "../shared/message-title
7
7
  import { buildTextNote } from "../shared/message-note.js";
8
8
  import { MAX_TELEGRAM_MESSAGE_LENGTH } from "../constants.js";
9
9
  import { listDirectories, buildDirectoryKeyboard, } from "../commands/handler.js";
10
+ import { detectChoices, buildChoiceKeyboard } from "../shared/choice-detector.js";
10
11
  const log = createLogger("TgSender");
11
12
  const lastSentByMsg = new Map();
12
13
  // Periodic cleanup of orphaned entries (entries not cleaned by done/error)
@@ -121,13 +122,31 @@ export async function sendTextReply(chatId, text) {
121
122
  const bot = getBot();
122
123
  try {
123
124
  const formatted = formatMessage(text, "done", undefined, OPEN_IM_SYSTEM_TITLE);
124
- // 尝试 Markdown 模式,如果失败则回退到纯文本
125
- try {
126
- await bot.telegram.sendMessage(Number(chatId), formatted, { parse_mode: "Markdown" });
125
+ // 检测是否有编号选择
126
+ const detection = detectChoices(text);
127
+ if (detection.hasChoices) {
128
+ // 有选择:发送纯文本 + 按钮
129
+ const keyboard = buildChoiceKeyboard(detection.choices, chatId);
130
+ try {
131
+ await bot.telegram.sendMessage(Number(chatId), detection.cleanText || formatted, {
132
+ parse_mode: "Markdown",
133
+ reply_markup: keyboard,
134
+ });
135
+ }
136
+ catch {
137
+ await bot.telegram.sendMessage(Number(chatId), detection.cleanText || formatted, {
138
+ reply_markup: keyboard,
139
+ });
140
+ }
127
141
  }
128
- catch {
129
- // Markdown 解析失败,回退到纯文本
130
- await bot.telegram.sendMessage(Number(chatId), formatted);
142
+ else {
143
+ // 无选择:普通消息
144
+ try {
145
+ await bot.telegram.sendMessage(Number(chatId), formatted, { parse_mode: "Markdown" });
146
+ }
147
+ catch {
148
+ await bot.telegram.sendMessage(Number(chatId), formatted);
149
+ }
131
150
  }
132
151
  }
133
152
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.2-beta.23",
3
+ "version": "1.11.2-beta.24",
4
4
  "description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",