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.
- nonebot_plugin_codex/__init__.py +165 -0
- nonebot_plugin_codex/config.py +84 -0
- nonebot_plugin_codex/native_client.py +419 -0
- nonebot_plugin_codex/service.py +2338 -0
- nonebot_plugin_codex/telegram.py +650 -0
- nonebot_plugin_codex-0.1.0.dist-info/METADATA +167 -0
- nonebot_plugin_codex-0.1.0.dist-info/RECORD +10 -0
- nonebot_plugin_codex-0.1.0.dist-info/WHEEL +4 -0
- nonebot_plugin_codex-0.1.0.dist-info/entry_points.txt +4 -0
- nonebot_plugin_codex-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -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)
|