nonebot-plugin-codex 0.1.2__tar.gz → 0.1.4__tar.gz
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-0.1.2 → nonebot_plugin_codex-0.1.4}/PKG-INFO +34 -10
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/README.md +33 -9
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/pyproject.toml +1 -1
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/__init__.py +74 -4
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/service.py +362 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/telegram.py +276 -12
- nonebot_plugin_codex-0.1.4/src/nonebot_plugin_codex/telegram_commands.py +112 -0
- nonebot_plugin_codex-0.1.4/tests/test_plugin_entry.py +70 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_release_notes.py +2 -1
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_service.py +50 -0
- nonebot_plugin_codex-0.1.4/tests/test_telegram_commands.py +57 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_telegram_handlers.py +367 -4
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/LICENSE +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/config.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/native_client.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/runtime.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/telegram_rendering.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/__init__.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/conftest.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_config.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_native_client.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_plugin_meta.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_runtime.py +0 -0
- {nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/tests/test_telegram_rendering.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nonebot-plugin-codex
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Telegram bridge plugin for driving Codex from NoneBot
|
|
5
5
|
Author-Email: ttiee <469784630@qq.com>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -121,19 +121,38 @@ codex_workdir = "/home/yourname"
|
|
|
121
121
|
一个典型工作流通常是这样的:
|
|
122
122
|
|
|
123
123
|
```text
|
|
124
|
-
/codex
|
|
124
|
+
/codex
|
|
125
|
+
/panel
|
|
125
126
|
/cd /home/yourname/projects/demo
|
|
126
|
-
/permission danger
|
|
127
127
|
/mode resume
|
|
128
128
|
然后继续直接发送普通文本消息续聊
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
`/codex` 不带参数时会打开一个 Telegram 内的使用引导面板,方便你直接查看当前模式、工作目录、设置摘要,并进入目录浏览、设置面板或历史会话。
|
|
132
|
+
|
|
133
|
+
`/panel` 和 `/status` 会打开统一的“当前工作台”面板,把模式、模型、推理强度、权限、工作目录、当前会话状态和最近历史摘要放在同一屏里,并提供进入设置、目录、历史、新会话和停止会话的快捷操作。
|
|
134
|
+
|
|
135
|
+
你也可以直接把首条任务跟在 `/codex` 后面:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
/codex 帮我检查当前仓库为什么测试失败
|
|
139
|
+
/permission danger
|
|
140
|
+
```
|
|
141
|
+
|
|
131
142
|
你也可以把一次性任务交给 `exec` 模式:
|
|
132
143
|
|
|
133
144
|
```text
|
|
134
145
|
/exec 用三点总结这个仓库 README 还缺什么
|
|
135
146
|
```
|
|
136
147
|
|
|
148
|
+
如果你希望显式打开引导入口,也可以使用:
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
/help
|
|
152
|
+
/start
|
|
153
|
+
/panel
|
|
154
|
+
```
|
|
155
|
+
|
|
137
156
|
## 配置说明
|
|
138
157
|
|
|
139
158
|
完整配置如下,配置名与当前实现保持一致:
|
|
@@ -178,17 +197,21 @@ codex_stream_read_limit = 1048576
|
|
|
178
197
|
|
|
179
198
|
| 命令 | 说明 |
|
|
180
199
|
| --- | --- |
|
|
181
|
-
| `/codex [prompt]` |
|
|
200
|
+
| `/codex [prompt]` | 打开引导面板,或直接附带首条任务连接 Codex |
|
|
201
|
+
| `/help` | 打开使用引导面板 |
|
|
202
|
+
| `/start` | 打开使用引导面板 |
|
|
203
|
+
| `/panel` | 打开统一工作台面板 |
|
|
204
|
+
| `/status` | 打开统一工作台面板 |
|
|
182
205
|
| `/mode [resume\|exec]` | 查看或切换默认模式 |
|
|
183
206
|
| `/exec <prompt>` | 以一次性 `exec` 模式执行任务 |
|
|
184
|
-
| `/new` |
|
|
185
|
-
| `/stop` |
|
|
186
|
-
| `/models` |
|
|
187
|
-
| `/model [slug]` |
|
|
207
|
+
| `/new` | 新建当前聊天会话 |
|
|
208
|
+
| `/stop` | 停止当前聊天中的 Codex |
|
|
209
|
+
| `/models` | 查看可用模型列表 |
|
|
210
|
+
| `/model [slug]` | 查看或切换当前模型 |
|
|
188
211
|
| `/effort [high\|xhigh]` | 查看或切换推理强度 |
|
|
189
212
|
| `/permission [safe\|danger]` | 查看或切换权限模式 |
|
|
190
|
-
| `/pwd` |
|
|
191
|
-
| `/cd [path]` |
|
|
213
|
+
| `/pwd` | 查看当前工作目录和设置 |
|
|
214
|
+
| `/cd [path]` | 切换目录或打开目录浏览器 |
|
|
192
215
|
| `/home` | 将工作目录重置到 Home |
|
|
193
216
|
| `/sessions` | 打开历史会话浏览器 |
|
|
194
217
|
|
|
@@ -212,6 +235,7 @@ codex_stream_read_limit = 1048576
|
|
|
212
235
|
|
|
213
236
|
## 目录与历史会话
|
|
214
237
|
|
|
238
|
+
- `/panel` 或 `/status` 会打开统一工作台,一屏查看当前设置、工作目录、会话状态和最近历史,并跳转到常用控制面板。
|
|
215
239
|
- `/cd` 可打开目录浏览器,逐级进入目录、切换 Home、显示隐藏目录,并把当前浏览目录设置为工作目录。
|
|
216
240
|
- `/sessions` 会列出 native 与 exec 历史会话,便于恢复此前任务。
|
|
217
241
|
- 历史会话恢复时会尝试切回原始工作目录;如果原目录不存在,会保留当前目录并给出提示。
|
|
@@ -108,19 +108,38 @@ codex_workdir = "/home/yourname"
|
|
|
108
108
|
一个典型工作流通常是这样的:
|
|
109
109
|
|
|
110
110
|
```text
|
|
111
|
-
/codex
|
|
111
|
+
/codex
|
|
112
|
+
/panel
|
|
112
113
|
/cd /home/yourname/projects/demo
|
|
113
|
-
/permission danger
|
|
114
114
|
/mode resume
|
|
115
115
|
然后继续直接发送普通文本消息续聊
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
`/codex` 不带参数时会打开一个 Telegram 内的使用引导面板,方便你直接查看当前模式、工作目录、设置摘要,并进入目录浏览、设置面板或历史会话。
|
|
119
|
+
|
|
120
|
+
`/panel` 和 `/status` 会打开统一的“当前工作台”面板,把模式、模型、推理强度、权限、工作目录、当前会话状态和最近历史摘要放在同一屏里,并提供进入设置、目录、历史、新会话和停止会话的快捷操作。
|
|
121
|
+
|
|
122
|
+
你也可以直接把首条任务跟在 `/codex` 后面:
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
/codex 帮我检查当前仓库为什么测试失败
|
|
126
|
+
/permission danger
|
|
127
|
+
```
|
|
128
|
+
|
|
118
129
|
你也可以把一次性任务交给 `exec` 模式:
|
|
119
130
|
|
|
120
131
|
```text
|
|
121
132
|
/exec 用三点总结这个仓库 README 还缺什么
|
|
122
133
|
```
|
|
123
134
|
|
|
135
|
+
如果你希望显式打开引导入口,也可以使用:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
/help
|
|
139
|
+
/start
|
|
140
|
+
/panel
|
|
141
|
+
```
|
|
142
|
+
|
|
124
143
|
## 配置说明
|
|
125
144
|
|
|
126
145
|
完整配置如下,配置名与当前实现保持一致:
|
|
@@ -165,17 +184,21 @@ codex_stream_read_limit = 1048576
|
|
|
165
184
|
|
|
166
185
|
| 命令 | 说明 |
|
|
167
186
|
| --- | --- |
|
|
168
|
-
| `/codex [prompt]` |
|
|
187
|
+
| `/codex [prompt]` | 打开引导面板,或直接附带首条任务连接 Codex |
|
|
188
|
+
| `/help` | 打开使用引导面板 |
|
|
189
|
+
| `/start` | 打开使用引导面板 |
|
|
190
|
+
| `/panel` | 打开统一工作台面板 |
|
|
191
|
+
| `/status` | 打开统一工作台面板 |
|
|
169
192
|
| `/mode [resume\|exec]` | 查看或切换默认模式 |
|
|
170
193
|
| `/exec <prompt>` | 以一次性 `exec` 模式执行任务 |
|
|
171
|
-
| `/new` |
|
|
172
|
-
| `/stop` |
|
|
173
|
-
| `/models` |
|
|
174
|
-
| `/model [slug]` |
|
|
194
|
+
| `/new` | 新建当前聊天会话 |
|
|
195
|
+
| `/stop` | 停止当前聊天中的 Codex |
|
|
196
|
+
| `/models` | 查看可用模型列表 |
|
|
197
|
+
| `/model [slug]` | 查看或切换当前模型 |
|
|
175
198
|
| `/effort [high\|xhigh]` | 查看或切换推理强度 |
|
|
176
199
|
| `/permission [safe\|danger]` | 查看或切换权限模式 |
|
|
177
|
-
| `/pwd` |
|
|
178
|
-
| `/cd [path]` |
|
|
200
|
+
| `/pwd` | 查看当前工作目录和设置 |
|
|
201
|
+
| `/cd [path]` | 切换目录或打开目录浏览器 |
|
|
179
202
|
| `/home` | 将工作目录重置到 Home |
|
|
180
203
|
| `/sessions` | 打开历史会话浏览器 |
|
|
181
204
|
|
|
@@ -199,6 +222,7 @@ codex_stream_read_limit = 1048576
|
|
|
199
222
|
|
|
200
223
|
## 目录与历史会话
|
|
201
224
|
|
|
225
|
+
- `/panel` 或 `/status` 会打开统一工作台,一屏查看当前设置、工作目录、会话状态和最近历史,并跳转到常用控制面板。
|
|
202
226
|
- `/cd` 可打开目录浏览器,逐级进入目录、切换 Home、显示隐藏目录,并把当前浏览目录设置为工作目录。
|
|
203
227
|
- `/sessions` 会列出 native 与 exec 历史会话,便于恢复此前任务。
|
|
204
228
|
- 历史会话恢复时会尝试切回原始工作目录;如果原目录不存在,会保留当前目录并给出提示。
|
{nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/__init__.py
RENAMED
|
@@ -3,14 +3,21 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from nonebot import get_plugin_config, on_command, on_message, on_type, require
|
|
6
|
+
from nonebot.drivers import Driver
|
|
7
|
+
from nonebot.log import logger
|
|
6
8
|
from nonebot.plugin import PluginMetadata
|
|
7
9
|
from nonebot.params import CommandArg
|
|
8
10
|
from nonebot.adapters.telegram import Bot
|
|
9
11
|
from nonebot.adapters.telegram.message import Message
|
|
10
12
|
from nonebot.adapters.telegram.event import MessageEvent, CallbackQueryEvent
|
|
13
|
+
from nonebot.adapters.telegram.model import (
|
|
14
|
+
BotCommandScopeAllGroupChats,
|
|
15
|
+
BotCommandScopeAllPrivateChats,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
from .config import Config
|
|
13
19
|
from .telegram import TelegramHandlers
|
|
20
|
+
from .telegram_commands import build_plugin_usage, build_telegram_commands
|
|
14
21
|
from .native_client import NativeCodexClient
|
|
15
22
|
from .runtime import build_service_settings
|
|
16
23
|
from .service import CodexBridgeService
|
|
@@ -18,10 +25,7 @@ from .service import CodexBridgeService
|
|
|
18
25
|
__plugin_meta__ = PluginMetadata(
|
|
19
26
|
name="Codex",
|
|
20
27
|
description="Telegram bridge plugin for driving Codex from NoneBot",
|
|
21
|
-
usage=(
|
|
22
|
-
"/codex [prompt], /mode, /exec, /new, /stop, /models, /model, /effort, "
|
|
23
|
-
"/permission, /pwd, /cd, /home, /sessions"
|
|
24
|
-
),
|
|
28
|
+
usage=build_plugin_usage(),
|
|
25
29
|
homepage="https://github.com/ttiee/nonebot-plugin-codex",
|
|
26
30
|
type="application",
|
|
27
31
|
config=Config,
|
|
@@ -58,8 +62,34 @@ service = CodexBridgeService(
|
|
|
58
62
|
)
|
|
59
63
|
handlers = TelegramHandlers(service)
|
|
60
64
|
|
|
65
|
+
|
|
66
|
+
async def sync_telegram_commands(bot: Bot) -> bool:
|
|
67
|
+
synced = True
|
|
68
|
+
scopes = (
|
|
69
|
+
BotCommandScopeAllPrivateChats(),
|
|
70
|
+
BotCommandScopeAllGroupChats(),
|
|
71
|
+
)
|
|
72
|
+
commands = build_telegram_commands()
|
|
73
|
+
for scope in scopes:
|
|
74
|
+
try:
|
|
75
|
+
await bot.set_my_commands(commands, scope=scope)
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
logger.warning(f"Telegram 命令菜单同步失败({scope.type}):{exc}")
|
|
78
|
+
synced = False
|
|
79
|
+
return synced
|
|
80
|
+
|
|
61
81
|
if _runtime_ready:
|
|
82
|
+
@Driver.on_bot_connect
|
|
83
|
+
async def _sync_telegram_commands(bot: Bot) -> None:
|
|
84
|
+
if not isinstance(bot, Bot):
|
|
85
|
+
return
|
|
86
|
+
await sync_telegram_commands(bot)
|
|
87
|
+
|
|
62
88
|
codex_cmd = on_command("codex", priority=10, block=True)
|
|
89
|
+
help_cmd = on_command("help", priority=10, block=True)
|
|
90
|
+
start_cmd = on_command("start", priority=10, block=True)
|
|
91
|
+
panel_cmd = on_command("panel", priority=10, block=True)
|
|
92
|
+
status_cmd = on_command("status", priority=10, block=True)
|
|
63
93
|
mode_cmd = on_command("mode", priority=10, block=True)
|
|
64
94
|
exec_cmd = on_command("exec", priority=10, block=True)
|
|
65
95
|
new_cmd = on_command("new", priority=10, block=True)
|
|
@@ -91,6 +121,18 @@ if _runtime_ready:
|
|
|
91
121
|
block=True,
|
|
92
122
|
rule=handlers.is_setting_callback,
|
|
93
123
|
)
|
|
124
|
+
onboarding_callback = on_type(
|
|
125
|
+
CallbackQueryEvent,
|
|
126
|
+
priority=10,
|
|
127
|
+
block=True,
|
|
128
|
+
rule=handlers.is_onboarding_callback,
|
|
129
|
+
)
|
|
130
|
+
workspace_callback = on_type(
|
|
131
|
+
CallbackQueryEvent,
|
|
132
|
+
priority=10,
|
|
133
|
+
block=True,
|
|
134
|
+
rule=handlers.is_workspace_callback,
|
|
135
|
+
)
|
|
94
136
|
|
|
95
137
|
@codex_cmd.handle()
|
|
96
138
|
async def _handle_codex(
|
|
@@ -98,6 +140,22 @@ if _runtime_ready:
|
|
|
98
140
|
) -> None:
|
|
99
141
|
await handlers.handle_codex(bot, event, args)
|
|
100
142
|
|
|
143
|
+
@help_cmd.handle()
|
|
144
|
+
async def _handle_help(bot: Bot, event: MessageEvent) -> None:
|
|
145
|
+
await handlers.handle_help(bot, event)
|
|
146
|
+
|
|
147
|
+
@start_cmd.handle()
|
|
148
|
+
async def _handle_start(bot: Bot, event: MessageEvent) -> None:
|
|
149
|
+
await handlers.handle_start(bot, event)
|
|
150
|
+
|
|
151
|
+
@panel_cmd.handle()
|
|
152
|
+
async def _handle_panel(bot: Bot, event: MessageEvent) -> None:
|
|
153
|
+
await handlers.handle_panel(bot, event)
|
|
154
|
+
|
|
155
|
+
@status_cmd.handle()
|
|
156
|
+
async def _handle_status(bot: Bot, event: MessageEvent) -> None:
|
|
157
|
+
await handlers.handle_status(bot, event)
|
|
158
|
+
|
|
101
159
|
@mode_cmd.handle()
|
|
102
160
|
async def _handle_mode(
|
|
103
161
|
bot: Bot, event: MessageEvent, args: Message = CommandArg()
|
|
@@ -172,6 +230,18 @@ if _runtime_ready:
|
|
|
172
230
|
async def _handle_setting_callback(bot: Bot, event: CallbackQueryEvent) -> None:
|
|
173
231
|
await handlers.handle_setting_callback(bot, event)
|
|
174
232
|
|
|
233
|
+
@onboarding_callback.handle()
|
|
234
|
+
async def _handle_onboarding_callback(
|
|
235
|
+
bot: Bot, event: CallbackQueryEvent
|
|
236
|
+
) -> None:
|
|
237
|
+
await handlers.handle_onboarding_callback(bot, event)
|
|
238
|
+
|
|
239
|
+
@workspace_callback.handle()
|
|
240
|
+
async def _handle_workspace_callback(
|
|
241
|
+
bot: Bot, event: CallbackQueryEvent
|
|
242
|
+
) -> None:
|
|
243
|
+
await handlers.handle_workspace_callback(bot, event)
|
|
244
|
+
|
|
175
245
|
@follow_up.handle()
|
|
176
246
|
async def _handle_follow_up(bot: Bot, event: MessageEvent) -> None:
|
|
177
247
|
await handlers.handle_follow_up(bot, event)
|
{nonebot_plugin_codex-0.1.2 → nonebot_plugin_codex-0.1.4}/src/nonebot_plugin_codex/service.py
RENAMED
|
@@ -38,6 +38,10 @@ HISTORY_STALE_MESSAGE = "历史会话面板已失效,请重新执行 /sessions
|
|
|
38
38
|
SETTING_CALLBACK_PREFIX = "csp"
|
|
39
39
|
SETTING_STALE_MESSAGE = "设置面板已失效,请重新执行对应命令"
|
|
40
40
|
SUPPORTED_SETTING_PANELS = {"mode", "model", "effort", "permission"}
|
|
41
|
+
ONBOARDING_CALLBACK_PREFIX = "cop"
|
|
42
|
+
ONBOARDING_STALE_MESSAGE = "引导面板已失效,请重新执行 /codex"
|
|
43
|
+
WORKSPACE_CALLBACK_PREFIX = "cwp"
|
|
44
|
+
WORKSPACE_STALE_MESSAGE = "工作台面板已失效,请重新执行 /panel"
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
@dataclass(slots=True)
|
|
@@ -195,6 +199,22 @@ class SettingPanelState:
|
|
|
195
199
|
message_id: int | None = None
|
|
196
200
|
|
|
197
201
|
|
|
202
|
+
@dataclass(slots=True)
|
|
203
|
+
class OnboardingPanelState:
|
|
204
|
+
chat_key: str
|
|
205
|
+
token: str
|
|
206
|
+
version: int
|
|
207
|
+
message_id: int | None = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(slots=True)
|
|
211
|
+
class WorkspacePanelState:
|
|
212
|
+
chat_key: str
|
|
213
|
+
token: str
|
|
214
|
+
version: int
|
|
215
|
+
message_id: int | None = None
|
|
216
|
+
|
|
217
|
+
|
|
198
218
|
def build_chat_key(chat_type: str, chat_id: int) -> str:
|
|
199
219
|
if chat_type == "private":
|
|
200
220
|
return f"private_{chat_id}"
|
|
@@ -330,6 +350,38 @@ def decode_setting_callback(payload: str) -> tuple[str, int, str, str | None]:
|
|
|
330
350
|
return token, version, action, value
|
|
331
351
|
|
|
332
352
|
|
|
353
|
+
def encode_onboarding_callback(token: str, version: int, action: str) -> str:
|
|
354
|
+
return f"{ONBOARDING_CALLBACK_PREFIX}:{token}:{version}:{action}"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def decode_onboarding_callback(payload: str) -> tuple[str, int, str]:
|
|
358
|
+
parts = payload.split(":")
|
|
359
|
+
if len(parts) != 4 or parts[0] != ONBOARDING_CALLBACK_PREFIX:
|
|
360
|
+
raise ValueError("无效的引导回调。")
|
|
361
|
+
token = parts[1]
|
|
362
|
+
try:
|
|
363
|
+
version = int(parts[2])
|
|
364
|
+
except ValueError as exc:
|
|
365
|
+
raise ValueError("无效的引导回调。") from exc
|
|
366
|
+
return token, version, parts[3]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def encode_workspace_callback(token: str, version: int, action: str) -> str:
|
|
370
|
+
return f"{WORKSPACE_CALLBACK_PREFIX}:{token}:{version}:{action}"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def decode_workspace_callback(payload: str) -> tuple[str, int, str]:
|
|
374
|
+
parts = payload.split(":")
|
|
375
|
+
if len(parts) != 4 or parts[0] != WORKSPACE_CALLBACK_PREFIX:
|
|
376
|
+
raise ValueError("无效的工作台回调。")
|
|
377
|
+
token = parts[1]
|
|
378
|
+
try:
|
|
379
|
+
version = int(parts[2])
|
|
380
|
+
except ValueError as exc:
|
|
381
|
+
raise ValueError("无效的工作台回调。") from exc
|
|
382
|
+
return token, version, parts[3]
|
|
383
|
+
|
|
384
|
+
|
|
333
385
|
def parse_event_line(line: str) -> dict[str, Any] | None:
|
|
334
386
|
try:
|
|
335
387
|
payload = json.loads(line)
|
|
@@ -501,6 +553,8 @@ class CodexBridgeService:
|
|
|
501
553
|
self.directory_browsers: dict[str, DirectoryBrowserState] = {}
|
|
502
554
|
self.history_browsers: dict[str, HistoryBrowserState] = {}
|
|
503
555
|
self.setting_panels: dict[str, SettingPanelState] = {}
|
|
556
|
+
self.onboarding_panels: dict[str, OnboardingPanelState] = {}
|
|
557
|
+
self.workspace_panels: dict[str, WorkspacePanelState] = {}
|
|
504
558
|
self._native_history_entries: list[HistoricalSessionSummary] = []
|
|
505
559
|
self._native_history_loaded = False
|
|
506
560
|
self._history_log_cache: dict[str, HistoryLogCacheEntry] = {}
|
|
@@ -1639,6 +1693,314 @@ class CodexBridgeService:
|
|
|
1639
1693
|
self.get_setting_panel(chat_key, token=token, version=version)
|
|
1640
1694
|
self.setting_panels.pop(chat_key, None)
|
|
1641
1695
|
|
|
1696
|
+
def _replace_onboarding_panel_state(
|
|
1697
|
+
self,
|
|
1698
|
+
chat_key: str,
|
|
1699
|
+
*,
|
|
1700
|
+
previous: OnboardingPanelState | None = None,
|
|
1701
|
+
) -> OnboardingPanelState:
|
|
1702
|
+
state = OnboardingPanelState(
|
|
1703
|
+
chat_key=chat_key,
|
|
1704
|
+
token=previous.token if previous else self._make_browser_token(),
|
|
1705
|
+
version=(previous.version + 1) if previous else 1,
|
|
1706
|
+
message_id=previous.message_id if previous else None,
|
|
1707
|
+
)
|
|
1708
|
+
self.onboarding_panels[chat_key] = state
|
|
1709
|
+
return state
|
|
1710
|
+
|
|
1711
|
+
def open_onboarding_panel(self, chat_key: str) -> OnboardingPanelState:
|
|
1712
|
+
self.get_preferences(chat_key)
|
|
1713
|
+
return self._replace_onboarding_panel_state(chat_key)
|
|
1714
|
+
|
|
1715
|
+
def get_onboarding_panel(
|
|
1716
|
+
self,
|
|
1717
|
+
chat_key: str,
|
|
1718
|
+
token: str | None = None,
|
|
1719
|
+
version: int | None = None,
|
|
1720
|
+
) -> OnboardingPanelState:
|
|
1721
|
+
state = self.onboarding_panels.get(chat_key)
|
|
1722
|
+
if state is None:
|
|
1723
|
+
raise ValueError(ONBOARDING_STALE_MESSAGE)
|
|
1724
|
+
if token is not None and state.token != token:
|
|
1725
|
+
raise ValueError(ONBOARDING_STALE_MESSAGE)
|
|
1726
|
+
if version is not None and state.version != version:
|
|
1727
|
+
raise ValueError(ONBOARDING_STALE_MESSAGE)
|
|
1728
|
+
return state
|
|
1729
|
+
|
|
1730
|
+
def remember_onboarding_panel_message(
|
|
1731
|
+
self,
|
|
1732
|
+
chat_key: str,
|
|
1733
|
+
token: str,
|
|
1734
|
+
message_id: int | None,
|
|
1735
|
+
) -> None:
|
|
1736
|
+
if message_id is None:
|
|
1737
|
+
return
|
|
1738
|
+
panel = self.get_onboarding_panel(chat_key, token=token)
|
|
1739
|
+
panel.message_id = message_id
|
|
1740
|
+
|
|
1741
|
+
def close_onboarding_panel(self, chat_key: str, token: str, version: int) -> None:
|
|
1742
|
+
self.get_onboarding_panel(chat_key, token=token, version=version)
|
|
1743
|
+
self.onboarding_panels.pop(chat_key, None)
|
|
1744
|
+
|
|
1745
|
+
def _replace_workspace_panel_state(
|
|
1746
|
+
self,
|
|
1747
|
+
chat_key: str,
|
|
1748
|
+
*,
|
|
1749
|
+
previous: WorkspacePanelState | None = None,
|
|
1750
|
+
) -> WorkspacePanelState:
|
|
1751
|
+
state = WorkspacePanelState(
|
|
1752
|
+
chat_key=chat_key,
|
|
1753
|
+
token=previous.token if previous else self._make_browser_token(),
|
|
1754
|
+
version=(previous.version + 1) if previous else 1,
|
|
1755
|
+
message_id=previous.message_id if previous else None,
|
|
1756
|
+
)
|
|
1757
|
+
self.workspace_panels[chat_key] = state
|
|
1758
|
+
return state
|
|
1759
|
+
|
|
1760
|
+
def open_workspace_panel(self, chat_key: str) -> WorkspacePanelState:
|
|
1761
|
+
self.get_preferences(chat_key)
|
|
1762
|
+
return self._replace_workspace_panel_state(chat_key)
|
|
1763
|
+
|
|
1764
|
+
def get_workspace_panel(
|
|
1765
|
+
self,
|
|
1766
|
+
chat_key: str,
|
|
1767
|
+
token: str | None = None,
|
|
1768
|
+
version: int | None = None,
|
|
1769
|
+
) -> WorkspacePanelState:
|
|
1770
|
+
state = self.workspace_panels.get(chat_key)
|
|
1771
|
+
if state is None:
|
|
1772
|
+
raise ValueError(WORKSPACE_STALE_MESSAGE)
|
|
1773
|
+
if token is not None and state.token != token:
|
|
1774
|
+
raise ValueError(WORKSPACE_STALE_MESSAGE)
|
|
1775
|
+
if version is not None and state.version != version:
|
|
1776
|
+
raise ValueError(WORKSPACE_STALE_MESSAGE)
|
|
1777
|
+
return state
|
|
1778
|
+
|
|
1779
|
+
def remember_workspace_panel_message(
|
|
1780
|
+
self,
|
|
1781
|
+
chat_key: str,
|
|
1782
|
+
token: str,
|
|
1783
|
+
message_id: int | None,
|
|
1784
|
+
) -> None:
|
|
1785
|
+
if message_id is None:
|
|
1786
|
+
return
|
|
1787
|
+
panel = self.get_workspace_panel(chat_key, token=token)
|
|
1788
|
+
panel.message_id = message_id
|
|
1789
|
+
|
|
1790
|
+
def close_workspace_panel(self, chat_key: str, token: str, version: int) -> None:
|
|
1791
|
+
self.get_workspace_panel(chat_key, token=token, version=version)
|
|
1792
|
+
self.workspace_panels.pop(chat_key, None)
|
|
1793
|
+
|
|
1794
|
+
def _workspace_active_mode(self, chat_key: str, preferences: ChatPreferences) -> str:
|
|
1795
|
+
session = self.sessions.get(chat_key)
|
|
1796
|
+
if session is not None and session.active_mode in {"resume", "exec"}:
|
|
1797
|
+
return session.active_mode
|
|
1798
|
+
return preferences.default_mode
|
|
1799
|
+
|
|
1800
|
+
def _workspace_session_summary(self, chat_key: str) -> str:
|
|
1801
|
+
session = self.sessions.get(chat_key)
|
|
1802
|
+
if session is None:
|
|
1803
|
+
return "未开始"
|
|
1804
|
+
active_mode = (
|
|
1805
|
+
session.active_mode
|
|
1806
|
+
if session.active_mode in {"resume", "exec"}
|
|
1807
|
+
else "resume"
|
|
1808
|
+
)
|
|
1809
|
+
thread_id = (
|
|
1810
|
+
self._current_exec_thread_id(session)
|
|
1811
|
+
if active_mode == "exec"
|
|
1812
|
+
else session.native_thread_id or session.thread_id
|
|
1813
|
+
)
|
|
1814
|
+
if not thread_id and not session.active:
|
|
1815
|
+
return "未开始"
|
|
1816
|
+
if not thread_id:
|
|
1817
|
+
return f"{active_mode} | 未绑定"
|
|
1818
|
+
return f"{active_mode} | {thread_id}"
|
|
1819
|
+
|
|
1820
|
+
def _workspace_recent_history_lines(self) -> list[str]:
|
|
1821
|
+
try:
|
|
1822
|
+
entries = self.list_history_sessions()[:2]
|
|
1823
|
+
except ValueError:
|
|
1824
|
+
return ["最近历史:不可用"]
|
|
1825
|
+
if not entries:
|
|
1826
|
+
return ["最近历史:无"]
|
|
1827
|
+
lines = ["最近历史:"]
|
|
1828
|
+
for entry in entries:
|
|
1829
|
+
lines.append(
|
|
1830
|
+
"- "
|
|
1831
|
+
f"{entry.thread_name} | "
|
|
1832
|
+
f"{self._format_history_relative_time(entry.updated_at)}"
|
|
1833
|
+
)
|
|
1834
|
+
return lines
|
|
1835
|
+
|
|
1836
|
+
def navigate_workspace_panel(
|
|
1837
|
+
self,
|
|
1838
|
+
chat_key: str,
|
|
1839
|
+
token: str,
|
|
1840
|
+
version: int,
|
|
1841
|
+
action: str,
|
|
1842
|
+
) -> WorkspacePanelState:
|
|
1843
|
+
panel = self.get_workspace_panel(chat_key, token=token, version=version)
|
|
1844
|
+
if action != "refresh":
|
|
1845
|
+
raise ValueError("未知工作台操作。")
|
|
1846
|
+
return self._replace_workspace_panel_state(chat_key, previous=panel)
|
|
1847
|
+
|
|
1848
|
+
def render_workspace_panel(
|
|
1849
|
+
self, chat_key: str
|
|
1850
|
+
) -> tuple[str, InlineKeyboardMarkup]:
|
|
1851
|
+
panel = self.get_workspace_panel(chat_key)
|
|
1852
|
+
preferences = self.get_preferences(chat_key)
|
|
1853
|
+
lines = [
|
|
1854
|
+
"当前工作台",
|
|
1855
|
+
f"当前模式:{self._workspace_active_mode(chat_key, preferences)}",
|
|
1856
|
+
f"当前设置:{format_preferences_summary(preferences)}",
|
|
1857
|
+
f"当前工作目录:{preferences.workdir}",
|
|
1858
|
+
f"当前会话:{self._workspace_session_summary(chat_key)}",
|
|
1859
|
+
*self._workspace_recent_history_lines(),
|
|
1860
|
+
]
|
|
1861
|
+
keyboard = [
|
|
1862
|
+
[
|
|
1863
|
+
InlineKeyboardButton(
|
|
1864
|
+
text="模式",
|
|
1865
|
+
callback_data=encode_workspace_callback(
|
|
1866
|
+
panel.token, panel.version, "mode"
|
|
1867
|
+
),
|
|
1868
|
+
),
|
|
1869
|
+
InlineKeyboardButton(
|
|
1870
|
+
text="模型",
|
|
1871
|
+
callback_data=encode_workspace_callback(
|
|
1872
|
+
panel.token, panel.version, "model"
|
|
1873
|
+
),
|
|
1874
|
+
),
|
|
1875
|
+
InlineKeyboardButton(
|
|
1876
|
+
text="强度",
|
|
1877
|
+
callback_data=encode_workspace_callback(
|
|
1878
|
+
panel.token, panel.version, "effort"
|
|
1879
|
+
),
|
|
1880
|
+
),
|
|
1881
|
+
InlineKeyboardButton(
|
|
1882
|
+
text="权限",
|
|
1883
|
+
callback_data=encode_workspace_callback(
|
|
1884
|
+
panel.token, panel.version, "permission"
|
|
1885
|
+
),
|
|
1886
|
+
),
|
|
1887
|
+
],
|
|
1888
|
+
[
|
|
1889
|
+
InlineKeyboardButton(
|
|
1890
|
+
text="目录",
|
|
1891
|
+
callback_data=encode_workspace_callback(
|
|
1892
|
+
panel.token, panel.version, "browse"
|
|
1893
|
+
),
|
|
1894
|
+
),
|
|
1895
|
+
InlineKeyboardButton(
|
|
1896
|
+
text="历史",
|
|
1897
|
+
callback_data=encode_workspace_callback(
|
|
1898
|
+
panel.token, panel.version, "history"
|
|
1899
|
+
),
|
|
1900
|
+
),
|
|
1901
|
+
],
|
|
1902
|
+
[
|
|
1903
|
+
InlineKeyboardButton(
|
|
1904
|
+
text="新会话",
|
|
1905
|
+
callback_data=encode_workspace_callback(
|
|
1906
|
+
panel.token, panel.version, "new"
|
|
1907
|
+
),
|
|
1908
|
+
),
|
|
1909
|
+
InlineKeyboardButton(
|
|
1910
|
+
text="停止",
|
|
1911
|
+
callback_data=encode_workspace_callback(
|
|
1912
|
+
panel.token, panel.version, "stop"
|
|
1913
|
+
),
|
|
1914
|
+
),
|
|
1915
|
+
],
|
|
1916
|
+
[
|
|
1917
|
+
InlineKeyboardButton(
|
|
1918
|
+
text="刷新",
|
|
1919
|
+
callback_data=encode_workspace_callback(
|
|
1920
|
+
panel.token, panel.version, "refresh"
|
|
1921
|
+
),
|
|
1922
|
+
),
|
|
1923
|
+
InlineKeyboardButton(
|
|
1924
|
+
text="关闭",
|
|
1925
|
+
callback_data=encode_workspace_callback(
|
|
1926
|
+
panel.token, panel.version, "close"
|
|
1927
|
+
),
|
|
1928
|
+
),
|
|
1929
|
+
],
|
|
1930
|
+
]
|
|
1931
|
+
return "\n".join(lines), InlineKeyboardMarkup(inline_keyboard=keyboard)
|
|
1932
|
+
|
|
1933
|
+
def render_onboarding_panel(
|
|
1934
|
+
self, chat_key: str
|
|
1935
|
+
) -> tuple[str, InlineKeyboardMarkup]:
|
|
1936
|
+
panel = self.get_onboarding_panel(chat_key)
|
|
1937
|
+
preferences = self.get_preferences(chat_key)
|
|
1938
|
+
session = self.sessions.get(chat_key)
|
|
1939
|
+
active_mode = (
|
|
1940
|
+
session.active_mode
|
|
1941
|
+
if session is not None and session.active_mode in {"resume", "exec"}
|
|
1942
|
+
else preferences.default_mode
|
|
1943
|
+
)
|
|
1944
|
+
has_bound_session = bool(
|
|
1945
|
+
session
|
|
1946
|
+
and (
|
|
1947
|
+
session.active
|
|
1948
|
+
or session.thread_id
|
|
1949
|
+
or session.native_thread_id
|
|
1950
|
+
or session.exec_thread_id
|
|
1951
|
+
)
|
|
1952
|
+
)
|
|
1953
|
+
lines = [
|
|
1954
|
+
"开始使用 Codex",
|
|
1955
|
+
f"当前模式:{active_mode}",
|
|
1956
|
+
f"当前工作目录:{preferences.workdir}",
|
|
1957
|
+
f"当前设置:{format_preferences_summary(preferences)}",
|
|
1958
|
+
f"当前会话:{'可继续' if has_bound_session else '未开始'}",
|
|
1959
|
+
(
|
|
1960
|
+
"推荐:直接发送任务,或先切换目录;"
|
|
1961
|
+
"一次性任务用 /exec;恢复上下文看历史会话。"
|
|
1962
|
+
),
|
|
1963
|
+
]
|
|
1964
|
+
keyboard = [
|
|
1965
|
+
[
|
|
1966
|
+
InlineKeyboardButton(
|
|
1967
|
+
text="切换目录",
|
|
1968
|
+
callback_data=encode_onboarding_callback(
|
|
1969
|
+
panel.token, panel.version, "browse"
|
|
1970
|
+
),
|
|
1971
|
+
),
|
|
1972
|
+
InlineKeyboardButton(
|
|
1973
|
+
text="当前设置",
|
|
1974
|
+
callback_data=encode_onboarding_callback(
|
|
1975
|
+
panel.token, panel.version, "settings"
|
|
1976
|
+
),
|
|
1977
|
+
),
|
|
1978
|
+
],
|
|
1979
|
+
[
|
|
1980
|
+
InlineKeyboardButton(
|
|
1981
|
+
text="历史会话",
|
|
1982
|
+
callback_data=encode_onboarding_callback(
|
|
1983
|
+
panel.token, panel.version, "history"
|
|
1984
|
+
),
|
|
1985
|
+
),
|
|
1986
|
+
InlineKeyboardButton(
|
|
1987
|
+
text="新开会话",
|
|
1988
|
+
callback_data=encode_onboarding_callback(
|
|
1989
|
+
panel.token, panel.version, "new"
|
|
1990
|
+
),
|
|
1991
|
+
),
|
|
1992
|
+
],
|
|
1993
|
+
[
|
|
1994
|
+
InlineKeyboardButton(
|
|
1995
|
+
text="关闭",
|
|
1996
|
+
callback_data=encode_onboarding_callback(
|
|
1997
|
+
panel.token, panel.version, "close"
|
|
1998
|
+
),
|
|
1999
|
+
)
|
|
2000
|
+
],
|
|
2001
|
+
]
|
|
2002
|
+
return "\n".join(lines), InlineKeyboardMarkup(inline_keyboard=keyboard)
|
|
2003
|
+
|
|
1642
2004
|
def navigate_setting_panel(
|
|
1643
2005
|
self,
|
|
1644
2006
|
chat_key: str,
|