nonebot-plugin-codex 0.1.0__py3-none-any.whl

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,650 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import time
5
+ import asyncio
6
+ from typing import Any
7
+ from pathlib import Path
8
+
9
+ from nonebot.adapters.telegram import Bot
10
+ from nonebot.adapters.telegram.message import Message
11
+ from nonebot.adapters.telegram.exception import NetworkError
12
+ from nonebot.adapters.telegram.event import MessageEvent, CallbackQueryEvent
13
+
14
+ from .service import (
15
+ BROWSER_STALE_MESSAGE,
16
+ HISTORY_STALE_MESSAGE,
17
+ BROWSER_CALLBACK_PREFIX,
18
+ HISTORY_CALLBACK_PREFIX,
19
+ CodexBridgeService,
20
+ chunk_text,
21
+ build_chat_key,
22
+ format_result_text,
23
+ decode_browser_callback,
24
+ decode_history_callback,
25
+ should_forward_follow_up,
26
+ format_preferences_summary,
27
+ )
28
+
29
+ RETRY_AFTER_PATTERN = re.compile(r"retry after (\d+(?:\.\d+)?)", re.IGNORECASE)
30
+
31
+
32
+ class TelegramHandlers:
33
+ def __init__(self, service: CodexBridgeService) -> None:
34
+ self.service = service
35
+
36
+ def event_chat(self, event: MessageEvent | CallbackQueryEvent) -> Any:
37
+ chat = getattr(event, "chat", None)
38
+ if chat is not None:
39
+ return chat
40
+ message = getattr(event, "message", None)
41
+ message_chat = getattr(message, "chat", None)
42
+ if message_chat is not None:
43
+ return message_chat
44
+ raise ValueError("无法确定当前聊天上下文。")
45
+
46
+ def chat_key(self, event: MessageEvent | CallbackQueryEvent) -> str:
47
+ chat = self.event_chat(event)
48
+ return build_chat_key(chat.type, chat.id)
49
+
50
+ def telegram_retry_after(self, exc: Exception) -> float | None:
51
+ if not isinstance(exc, NetworkError):
52
+ return None
53
+ message = getattr(exc, "msg", None) or str(exc)
54
+ match = RETRY_AFTER_PATTERN.search(message)
55
+ if match is None:
56
+ return None
57
+ return float(match.group(1))
58
+
59
+ async def retry_telegram_call(self, operation):
60
+ while True:
61
+ try:
62
+ return await operation()
63
+ except Exception as exc:
64
+ retry_after = self.telegram_retry_after(exc)
65
+ if retry_after is None:
66
+ raise
67
+ await asyncio.sleep(retry_after)
68
+
69
+ async def send_event_message(
70
+ self, bot: Bot, event: MessageEvent, text: str, **kwargs: object
71
+ ):
72
+ return await self.retry_telegram_call(lambda: bot.send(event, text, **kwargs))
73
+
74
+ async def send_chat_message(
75
+ self, bot: Bot, chat_id: int, text: str, **kwargs: object
76
+ ):
77
+ return await self.retry_telegram_call(
78
+ lambda: bot.send_message(chat_id=chat_id, text=text, **kwargs)
79
+ )
80
+
81
+ async def edit_message(
82
+ self,
83
+ bot: Bot,
84
+ *,
85
+ chat_id: int,
86
+ message_id: int,
87
+ text: str,
88
+ **kwargs: object,
89
+ ):
90
+ return await self.retry_telegram_call(
91
+ lambda: bot.edit_message_text(
92
+ chat_id=chat_id,
93
+ message_id=message_id,
94
+ text=text,
95
+ **kwargs,
96
+ )
97
+ )
98
+
99
+ async def update_progress(self, bot: Bot, event: MessageEvent, text: str) -> None:
100
+ session = self.service.get_session(self.chat_key(event))
101
+ if session.progress_message_id is None:
102
+ message = await self.send_event_message(bot, event, text)
103
+ session.progress_message_id = getattr(message, "message_id", None)
104
+ return
105
+ try:
106
+ await self.edit_message(
107
+ bot,
108
+ chat_id=event.chat.id,
109
+ message_id=session.progress_message_id,
110
+ text=text,
111
+ )
112
+ except Exception:
113
+ message = await self.send_event_message(bot, event, text)
114
+ session.progress_message_id = getattr(message, "message_id", None)
115
+
116
+ def render_stream_text(self, text: str) -> tuple[str, bool]:
117
+ chunks = chunk_text(text, self.service.settings.chunk_size)
118
+ if not chunks:
119
+ return "", False
120
+ if len(chunks) == 1:
121
+ return chunks[0], False
122
+ return chunks[-1], True
123
+
124
+ async def update_stream_text(self, bot: Bot, event: MessageEvent, text: str) -> None:
125
+ session = self.service.get_session(self.chat_key(event))
126
+ rendered_text, truncated = self.render_stream_text(text)
127
+ if not rendered_text:
128
+ return
129
+ session.stream_message_truncated = truncated
130
+ if rendered_text == session.last_stream_rendered_text:
131
+ return
132
+ if session.stream_message_id is None:
133
+ message = await self.send_event_message(bot, event, rendered_text)
134
+ session.stream_message_id = getattr(message, "message_id", None)
135
+ else:
136
+ try:
137
+ await self.edit_message(
138
+ bot,
139
+ chat_id=event.chat.id,
140
+ message_id=session.stream_message_id,
141
+ text=rendered_text,
142
+ )
143
+ except Exception:
144
+ message = await self.send_event_message(bot, event, rendered_text)
145
+ session.stream_message_id = getattr(message, "message_id", None)
146
+ session.last_stream_rendered_text = rendered_text
147
+
148
+ async def finalize_progress(self, bot: Bot, event: MessageEvent, text: str) -> None:
149
+ session = self.service.get_session(self.chat_key(event))
150
+ if session.progress_message_id is None:
151
+ return
152
+ try:
153
+ await self.edit_message(
154
+ bot,
155
+ chat_id=event.chat.id,
156
+ message_id=session.progress_message_id,
157
+ text=text,
158
+ )
159
+ except Exception:
160
+ pass
161
+ finally:
162
+ session.progress_message_id = None
163
+
164
+ async def send_result(self, bot: Bot, event: MessageEvent, text: str) -> None:
165
+ for chunk in chunk_text(text, self.service.settings.chunk_size):
166
+ await self.send_event_message(bot, event, chunk)
167
+
168
+ def error_text(self, exc: Exception) -> str:
169
+ if (
170
+ isinstance(exc, FileNotFoundError)
171
+ and exc.args
172
+ and exc.args[0] == self.service.settings.binary
173
+ ):
174
+ return "未找到本机 `codex` CLI,请确认它已经安装并且在 PATH 中。"
175
+ if (
176
+ isinstance(exc, RuntimeError)
177
+ and str(exc) == "Codex is already running for this chat"
178
+ ):
179
+ return "Codex 正在运行中,请等待完成或使用 /stop。"
180
+ return str(exc) or "发生了未知错误。"
181
+
182
+ def current_summary(self, chat_key: str) -> str:
183
+ return self.service.describe_preferences(chat_key)
184
+
185
+ def format_models(self, chat_key: str) -> str:
186
+ current_model = self.service.get_preferences(chat_key).model
187
+ lines = [f"当前设置:{self.current_summary(chat_key)}", "可用模型:"]
188
+ for model in self.service.list_models():
189
+ efforts = "/".join(model.supported_reasoning_levels)
190
+ suffix = " (当前)" if model.slug == current_model else ""
191
+ lines.append(f"- {model.slug} [{efforts}]{suffix}")
192
+ return "\n".join(lines)
193
+
194
+ async def execute_prompt(
195
+ self,
196
+ bot: Bot,
197
+ event: MessageEvent,
198
+ prompt: str,
199
+ *,
200
+ mode_override: str | None = None,
201
+ ) -> None:
202
+ chat_key = self.chat_key(event)
203
+ session = self.service.get_session(chat_key)
204
+ session.progress_message_id = None
205
+ session.stream_message_id = None
206
+ session.last_stream_rendered_text = ""
207
+ session.stream_message_truncated = False
208
+ last_stream_update_at = 0.0
209
+ pending_stream_text = ""
210
+
211
+ async def on_progress(text: str) -> None:
212
+ await self.update_progress(bot, event, text)
213
+
214
+ async def flush_stream_text() -> None:
215
+ nonlocal last_stream_update_at, pending_stream_text
216
+ if not pending_stream_text:
217
+ return
218
+ await self.update_stream_text(bot, event, pending_stream_text)
219
+ pending_stream_text = ""
220
+ last_stream_update_at = time.monotonic()
221
+
222
+ async def on_stream_text(text: str) -> None:
223
+ nonlocal pending_stream_text
224
+ pending_stream_text = text
225
+ if time.monotonic() - last_stream_update_at < 0.5:
226
+ return
227
+ await flush_stream_text()
228
+
229
+ try:
230
+ result = await self.service.run_prompt(
231
+ chat_key,
232
+ prompt,
233
+ mode_override=mode_override,
234
+ on_progress=on_progress,
235
+ on_stream_text=on_stream_text,
236
+ )
237
+ except (FileNotFoundError, ValueError) as exc:
238
+ await self.send_event_message(bot, event, self.error_text(exc))
239
+ return
240
+ except RuntimeError:
241
+ await self.send_event_message(
242
+ bot, event, "Codex 正在运行中,请等待完成或使用 /stop。"
243
+ )
244
+ return
245
+
246
+ await flush_stream_text()
247
+ if result.cancelled:
248
+ await self.finalize_progress(bot, event, "Codex 已中断。")
249
+ return
250
+
251
+ status = "Codex 已完成。" if result.exit_code == 0 else "Codex 执行失败。"
252
+ await self.finalize_progress(bot, event, status)
253
+ if (
254
+ result.final_text
255
+ and result.final_text == session.last_stream_text
256
+ and not session.stream_message_truncated
257
+ ):
258
+ if result.notice:
259
+ await self.send_result(bot, event, result.notice)
260
+ return
261
+ await self.send_result(bot, event, format_result_text(result))
262
+
263
+ async def is_active_follow_up(self, event: MessageEvent) -> bool:
264
+ chat_key = self.chat_key(event)
265
+ session = self.service.sessions.get(chat_key)
266
+ text = event.get_plaintext()
267
+ return bool(
268
+ session
269
+ and session.active
270
+ and text.strip()
271
+ and not text.strip().startswith("/")
272
+ )
273
+
274
+ async def is_browser_callback(self, event: CallbackQueryEvent) -> bool:
275
+ return isinstance(event.data, str) and event.data.startswith(
276
+ f"{BROWSER_CALLBACK_PREFIX}:"
277
+ )
278
+
279
+ async def is_history_callback(self, event: CallbackQueryEvent) -> bool:
280
+ return isinstance(event.data, str) and event.data.startswith(
281
+ f"{HISTORY_CALLBACK_PREFIX}:"
282
+ )
283
+
284
+ def callback_message_id(self, event: CallbackQueryEvent) -> int | None:
285
+ message = getattr(event, "message", None)
286
+ return getattr(message, "message_id", None)
287
+
288
+ async def send_browser(self, bot: Bot, event: MessageEvent, chat_key: str) -> None:
289
+ browser = self.service.open_directory_browser(chat_key)
290
+ text, markup = self.service.render_directory_browser(chat_key)
291
+ message = await self.send_event_message(bot, event, text, reply_markup=markup)
292
+ self.service.remember_browser_message(
293
+ chat_key, browser.token, getattr(message, "message_id", None)
294
+ )
295
+
296
+ async def send_history_browser(
297
+ self, bot: Bot, event: MessageEvent, chat_key: str
298
+ ) -> None:
299
+ await self.service.refresh_history_sessions()
300
+ browser = self.service.open_history_browser(chat_key)
301
+ text, markup = self.service.render_history_browser(chat_key)
302
+ message = await self.send_event_message(bot, event, text, reply_markup=markup)
303
+ self.service.remember_history_browser_message(
304
+ chat_key,
305
+ browser.token,
306
+ getattr(message, "message_id", None),
307
+ )
308
+
309
+ async def edit_or_resend_browser(
310
+ self,
311
+ bot: Bot,
312
+ event: CallbackQueryEvent,
313
+ chat_key: str,
314
+ ) -> None:
315
+ browser = self.service.get_browser(chat_key)
316
+ text, markup = self.service.render_directory_browser(chat_key)
317
+ message_id = self.callback_message_id(event) or browser.message_id
318
+ chat_id = self.event_chat(event).id
319
+ try:
320
+ if message_id is None:
321
+ raise ValueError("missing message id")
322
+ await self.edit_message(
323
+ bot,
324
+ chat_id=chat_id,
325
+ message_id=message_id,
326
+ text=text,
327
+ reply_markup=markup,
328
+ )
329
+ self.service.remember_browser_message(chat_key, browser.token, message_id)
330
+ except Exception:
331
+ message = await self.send_chat_message(
332
+ bot, chat_id, text, reply_markup=markup
333
+ )
334
+ self.service.remember_browser_message(
335
+ chat_key,
336
+ browser.token,
337
+ getattr(message, "message_id", None),
338
+ )
339
+
340
+ async def edit_or_resend_history_browser(
341
+ self,
342
+ bot: Bot,
343
+ event: CallbackQueryEvent,
344
+ chat_key: str,
345
+ ) -> None:
346
+ browser = self.service.get_history_browser(chat_key)
347
+ text, markup = self.service.render_history_browser(chat_key)
348
+ message_id = self.callback_message_id(event) or browser.message_id
349
+ chat_id = self.event_chat(event).id
350
+ try:
351
+ if message_id is None:
352
+ raise ValueError("missing message id")
353
+ await self.edit_message(
354
+ bot,
355
+ chat_id=chat_id,
356
+ message_id=message_id,
357
+ text=text,
358
+ reply_markup=markup,
359
+ )
360
+ self.service.remember_history_browser_message(
361
+ chat_key, browser.token, message_id
362
+ )
363
+ except Exception:
364
+ message = await self.send_chat_message(
365
+ bot, chat_id, text, reply_markup=markup
366
+ )
367
+ self.service.remember_history_browser_message(
368
+ chat_key,
369
+ browser.token,
370
+ getattr(message, "message_id", None),
371
+ )
372
+
373
+ async def handle_codex(self, bot: Bot, event: MessageEvent, args: Message) -> None:
374
+ chat_key = self.chat_key(event)
375
+ session = self.service.activate_chat(chat_key)
376
+ if session.running:
377
+ await self.send_event_message(
378
+ bot, event, "Codex 正在运行中,请等待完成或使用 /stop。"
379
+ )
380
+ return
381
+
382
+ prompt = args.extract_plain_text().strip()
383
+ if prompt:
384
+ await self.execute_prompt(bot, event, prompt)
385
+ return
386
+
387
+ await self.send_event_message(
388
+ bot,
389
+ event,
390
+ (
391
+ "Codex 已连接。\n"
392
+ f"当前模式:{self.service.get_session(chat_key).active_mode}\n"
393
+ "普通消息继续当前模式,/mode 切换默认模式,"
394
+ "/exec 执行一次性任务,/new 新开,/stop 退出。\n"
395
+ f"当前设置:{self.current_summary(chat_key)}"
396
+ ),
397
+ )
398
+
399
+ async def handle_mode(self, bot: Bot, event: MessageEvent, args: Message) -> None:
400
+ chat_key = self.chat_key(event)
401
+ mode = args.extract_plain_text().strip()
402
+ if not mode:
403
+ preferences = self.service.get_preferences(chat_key)
404
+ session = self.service.get_session(chat_key)
405
+ await self.send_event_message(
406
+ bot,
407
+ event,
408
+ f"当前默认模式:{preferences.default_mode}\n当前活跃模式:{session.active_mode}",
409
+ )
410
+ return
411
+ try:
412
+ notice = await self.service.update_default_mode(chat_key, mode)
413
+ await self.send_event_message(bot, event, notice)
414
+ except (ValueError, RuntimeError) as exc:
415
+ await self.send_event_message(bot, event, self.error_text(exc))
416
+
417
+ async def handle_exec(self, bot: Bot, event: MessageEvent, args: Message) -> None:
418
+ prompt = args.extract_plain_text().strip()
419
+ if not prompt:
420
+ await self.send_event_message(bot, event, "请在 /exec 后输入要执行的内容。")
421
+ return
422
+ await self.execute_prompt(bot, event, prompt, mode_override="exec")
423
+
424
+ async def handle_new(self, bot: Bot, event: MessageEvent) -> None:
425
+ chat_key = self.chat_key(event)
426
+ await self.service.reset_chat(chat_key, keep_active=True)
427
+ await self.send_event_message(
428
+ bot,
429
+ event,
430
+ (
431
+ "已清空当前 Codex 会话。下一条普通消息会按以下设置新开会话:\n"
432
+ f"{self.current_summary(chat_key)}"
433
+ ),
434
+ )
435
+
436
+ async def handle_stop(self, bot: Bot, event: MessageEvent) -> None:
437
+ await self.service.reset_chat(self.chat_key(event), keep_active=False)
438
+ await self.send_event_message(bot, event, "已断开当前聊天窗口的 Codex 会话。")
439
+
440
+ async def handle_models(self, bot: Bot, event: MessageEvent) -> None:
441
+ chat_key = self.chat_key(event)
442
+ try:
443
+ await self.send_event_message(bot, event, self.format_models(chat_key))
444
+ except (FileNotFoundError, ValueError) as exc:
445
+ await self.send_event_message(bot, event, self.error_text(exc))
446
+
447
+ async def handle_model(self, bot: Bot, event: MessageEvent, args: Message) -> None:
448
+ chat_key = self.chat_key(event)
449
+ slug = args.extract_plain_text().strip()
450
+ try:
451
+ preferences = self.service.get_preferences(chat_key)
452
+ if not slug:
453
+ efforts = "/".join(self.service.get_supported_efforts(preferences.model))
454
+ await self.send_event_message(
455
+ bot,
456
+ event,
457
+ (
458
+ f"当前设置:{format_preferences_summary(preferences)}\n"
459
+ f"当前模型支持推理强度:{efforts}"
460
+ ),
461
+ )
462
+ return
463
+ notice = await self.service.update_model(chat_key, slug)
464
+ await self.send_event_message(bot, event, notice)
465
+ except (FileNotFoundError, ValueError, RuntimeError) as exc:
466
+ await self.send_event_message(bot, event, self.error_text(exc))
467
+
468
+ async def handle_effort(self, bot: Bot, event: MessageEvent, args: Message) -> None:
469
+ chat_key = self.chat_key(event)
470
+ effort = args.extract_plain_text().strip()
471
+ try:
472
+ preferences = self.service.get_preferences(chat_key)
473
+ supported = "/".join(self.service.get_supported_efforts(preferences.model))
474
+ if not effort:
475
+ await self.send_event_message(
476
+ bot,
477
+ event,
478
+ (
479
+ f"当前推理强度:{preferences.reasoning_effort}\n"
480
+ f"当前模型 `{preferences.model}` 支持:{supported}"
481
+ ),
482
+ )
483
+ return
484
+ notice = await self.service.update_reasoning_effort(chat_key, effort)
485
+ await self.send_event_message(bot, event, notice)
486
+ except (FileNotFoundError, ValueError, RuntimeError) as exc:
487
+ await self.send_event_message(bot, event, self.error_text(exc))
488
+
489
+ async def handle_permission(
490
+ self, bot: Bot, event: MessageEvent, args: Message
491
+ ) -> None:
492
+ chat_key = self.chat_key(event)
493
+ permission = args.extract_plain_text().strip()
494
+ try:
495
+ preferences = self.service.get_preferences(chat_key)
496
+ if not permission:
497
+ await self.send_event_message(
498
+ bot,
499
+ event,
500
+ (
501
+ f"当前权限模式:{preferences.permission_mode}\n"
502
+ "safe = workspace-write,danger = 绕过审批与沙箱。"
503
+ ),
504
+ )
505
+ return
506
+ notice = await self.service.update_permission_mode(chat_key, permission)
507
+ await self.send_event_message(bot, event, notice)
508
+ except (FileNotFoundError, ValueError, RuntimeError) as exc:
509
+ await self.send_event_message(bot, event, self.error_text(exc))
510
+
511
+ async def handle_pwd(self, bot: Bot, event: MessageEvent) -> None:
512
+ await self.send_event_message(
513
+ bot, event, self.service.describe_workdir(self.chat_key(event))
514
+ )
515
+
516
+ async def handle_cd(self, bot: Bot, event: MessageEvent, args: Message) -> None:
517
+ chat_key = self.chat_key(event)
518
+ target = args.extract_plain_text().strip()
519
+ try:
520
+ if not target:
521
+ await self.send_browser(bot, event, chat_key)
522
+ return
523
+ await self.send_event_message(
524
+ bot, event, await self.service.update_workdir(chat_key, target)
525
+ )
526
+ except (ValueError, RuntimeError) as exc:
527
+ await self.send_event_message(bot, event, self.error_text(exc))
528
+
529
+ async def handle_home(self, bot: Bot, event: MessageEvent) -> None:
530
+ try:
531
+ notice = await self.service.update_workdir(
532
+ self.chat_key(event), str(Path.home())
533
+ )
534
+ await self.send_event_message(bot, event, notice)
535
+ except (ValueError, RuntimeError) as exc:
536
+ await self.send_event_message(bot, event, self.error_text(exc))
537
+
538
+ async def handle_sessions(self, bot: Bot, event: MessageEvent) -> None:
539
+ try:
540
+ await self.send_history_browser(bot, event, self.chat_key(event))
541
+ except (ValueError, RuntimeError) as exc:
542
+ await self.send_event_message(bot, event, self.error_text(exc))
543
+
544
+ async def handle_browser_callback(self, bot: Bot, event: CallbackQueryEvent) -> None:
545
+ if not isinstance(event.data, str):
546
+ await bot.answer_callback_query(
547
+ event.id, text=BROWSER_STALE_MESSAGE, show_alert=True
548
+ )
549
+ return
550
+
551
+ try:
552
+ chat_key = self.chat_key(event)
553
+ chat_id = self.event_chat(event).id
554
+ token, version, action, index = decode_browser_callback(event.data)
555
+ if action == "apply":
556
+ await self.service.apply_browser_directory(chat_key, token, version)
557
+ await self.edit_or_resend_browser(bot, event, chat_key)
558
+ await bot.answer_callback_query(event.id, text="工作目录已更新。")
559
+ return
560
+ if action == "close":
561
+ self.service.close_directory_browser(chat_key, token, version)
562
+ message_id = self.callback_message_id(event)
563
+ if message_id is not None:
564
+ await self.edit_message(
565
+ bot,
566
+ chat_id=chat_id,
567
+ message_id=message_id,
568
+ text="目录浏览已关闭。",
569
+ reply_markup=None,
570
+ )
571
+ await bot.answer_callback_query(event.id, text="已关闭。")
572
+ return
573
+ self.service.navigate_directory_browser(
574
+ chat_key, token, version, action, index
575
+ )
576
+ await self.edit_or_resend_browser(bot, event, chat_key)
577
+ await bot.answer_callback_query(event.id)
578
+ except ValueError as exc:
579
+ text = str(exc) or BROWSER_STALE_MESSAGE
580
+ await bot.answer_callback_query(
581
+ event.id,
582
+ text=text,
583
+ show_alert=text == BROWSER_STALE_MESSAGE,
584
+ )
585
+ except RuntimeError as exc:
586
+ await bot.answer_callback_query(
587
+ event.id, text=self.error_text(exc), show_alert=True
588
+ )
589
+
590
+ async def handle_history_callback(self, bot: Bot, event: CallbackQueryEvent) -> None:
591
+ if not isinstance(event.data, str):
592
+ await bot.answer_callback_query(
593
+ event.id, text=HISTORY_STALE_MESSAGE, show_alert=True
594
+ )
595
+ return
596
+
597
+ try:
598
+ chat_key = self.chat_key(event)
599
+ chat_id = self.event_chat(event).id
600
+ token, version, action, index = decode_history_callback(event.data)
601
+ if action == "apply":
602
+ notice = await self.service.apply_history_session(
603
+ chat_key, token, version
604
+ )
605
+ await self.edit_or_resend_history_browser(bot, event, chat_key)
606
+ await bot.answer_callback_query(event.id, text="已切换到历史会话。")
607
+ await self.send_chat_message(bot, chat_id, notice)
608
+ return
609
+ if action == "close":
610
+ self.service.close_history_browser(chat_key, token, version)
611
+ message_id = self.callback_message_id(event)
612
+ if message_id is not None:
613
+ await self.edit_message(
614
+ bot,
615
+ chat_id=chat_id,
616
+ message_id=message_id,
617
+ text="历史会话浏览已关闭。",
618
+ reply_markup=None,
619
+ )
620
+ await bot.answer_callback_query(event.id, text="已关闭。")
621
+ return
622
+ if action == "refresh":
623
+ await self.service.refresh_history_sessions()
624
+ self.service.navigate_history_browser(chat_key, token, version, action, index)
625
+ await self.edit_or_resend_history_browser(bot, event, chat_key)
626
+ await bot.answer_callback_query(event.id)
627
+ except ValueError as exc:
628
+ text = str(exc) or HISTORY_STALE_MESSAGE
629
+ await bot.answer_callback_query(
630
+ event.id,
631
+ text=text,
632
+ show_alert=text == HISTORY_STALE_MESSAGE,
633
+ )
634
+ except RuntimeError as exc:
635
+ await bot.answer_callback_query(
636
+ event.id, text=self.error_text(exc), show_alert=True
637
+ )
638
+
639
+ async def handle_follow_up(self, bot: Bot, event: MessageEvent) -> None:
640
+ chat_key = self.chat_key(event)
641
+ session = self.service.get_session(chat_key)
642
+ text = event.get_plaintext().strip()
643
+
644
+ if not should_forward_follow_up(session, text):
645
+ await self.send_event_message(
646
+ bot, event, "Codex 正在运行中,请等待完成或使用 /stop。"
647
+ )
648
+ return
649
+
650
+ await self.execute_prompt(bot, event, text)