open-codex-ui 0.1.4__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.
- open_codex_ui-0.1.4.dist-info/METADATA +249 -0
- open_codex_ui-0.1.4.dist-info/RECORD +40 -0
- open_codex_ui-0.1.4.dist-info/WHEEL +5 -0
- open_codex_ui-0.1.4.dist-info/entry_points.txt +7 -0
- open_codex_ui-0.1.4.dist-info/top_level.txt +1 -0
- yier_web/__init__.py +6 -0
- yier_web/app.py +223 -0
- yier_web/auth.py +269 -0
- yier_web/cli.py +172 -0
- yier_web/codex/__init__.py +3 -0
- yier_web/codex/ipc_manager.py +1761 -0
- yier_web/codex/session_events.py +110 -0
- yier_web/codex/ws_commands.py +299 -0
- yier_web/config.py +651 -0
- yier_web/directory_picker.py +93 -0
- yier_web/event_stream.py +29 -0
- yier_web/frontend.py +204 -0
- yier_web/routes/__init__.py +17 -0
- yier_web/routes/codex.py +573 -0
- yier_web/routes/core.py +281 -0
- yier_web/schemas.py +534 -0
- yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js +1 -0
- yier_web/static/assets/CodexView-wpI61iXa.js +606 -0
- yier_web/static/assets/LoginView-CELCom2O.js +101 -0
- yier_web/static/assets/api-CeihACIV.js +1099 -0
- yier_web/static/assets/index-BdFqJ-Kl.css +1 -0
- yier_web/static/assets/index-CjVNk6ja.js +183 -0
- yier_web/static/assets/index-mSBvq1p8.js +79 -0
- yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg +99 -0
- yier_web/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
- yier_web/static/assets/primeicons-DMOk5skT.eot +0 -0
- yier_web/static/assets/primeicons-Dr5RGzOO.svg +345 -0
- yier_web/static/assets/primeicons-MpK4pl85.ttf +0 -0
- yier_web/static/assets/primeicons-WjwUDZjB.woff +0 -0
- yier_web/static/assets/useCodexWorkspace-6QenDzvb.css +1 -0
- yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js +1128 -0
- yier_web/static/brand/open-codex-ui-logo.svg +85 -0
- yier_web/static/favicon.ico +0 -0
- yier_web/static/favicon.svg +99 -0
- yier_web/static/index.html +24 -0
yier_web/routes/core.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, AsyncIterator
|
|
6
|
+
|
|
7
|
+
from litestar import Controller, Request, get, post, put
|
|
8
|
+
from litestar.datastructures import State
|
|
9
|
+
from litestar.exceptions import HTTPException
|
|
10
|
+
from litestar.response import Response, ServerSentEvent
|
|
11
|
+
from litestar.response.sse import ServerSentEventMessage
|
|
12
|
+
from litestar.status_codes import HTTP_400_BAD_REQUEST
|
|
13
|
+
|
|
14
|
+
from yier_web.config import MCPValidationError
|
|
15
|
+
from yier_web.event_stream import EventStreamItem
|
|
16
|
+
from yier_web.schemas import (
|
|
17
|
+
AuthLoginRequest,
|
|
18
|
+
AuthSessionResponse,
|
|
19
|
+
HealthResponse,
|
|
20
|
+
LLMHealth,
|
|
21
|
+
MCPConfigResponse,
|
|
22
|
+
MCPHealth,
|
|
23
|
+
SaveAllowedRootsRequest,
|
|
24
|
+
SaveAppSettingsRequest,
|
|
25
|
+
SaveLLMRequest,
|
|
26
|
+
SaveMCPConfigRequest,
|
|
27
|
+
SelectDirectoryRequest,
|
|
28
|
+
SelectDirectoryResponse,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
EVENT_STREAM_PING_INTERVAL_SECONDS = 15.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _service(state: State, name: str) -> Any:
|
|
35
|
+
return getattr(state, name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _shutdown_event(state: State) -> asyncio.Event | None:
|
|
39
|
+
shutdown_event = getattr(state, "shutdown_event", None)
|
|
40
|
+
if isinstance(shutdown_event, asyncio.Event):
|
|
41
|
+
return shutdown_event
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _wait_for_event_stream_item(
|
|
46
|
+
subscriber: asyncio.Queue[EventStreamItem],
|
|
47
|
+
shutdown_event: asyncio.Event | None,
|
|
48
|
+
timeout: float,
|
|
49
|
+
) -> tuple[EventStreamItem | None, bool]:
|
|
50
|
+
subscriber_task = asyncio.create_task(subscriber.get())
|
|
51
|
+
shutdown_task: asyncio.Task[bool] | None = None
|
|
52
|
+
wait_tasks: list[asyncio.Task[Any]] = [subscriber_task]
|
|
53
|
+
|
|
54
|
+
if shutdown_event is not None:
|
|
55
|
+
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
|
56
|
+
wait_tasks.append(shutdown_task)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
done, pending = await asyncio.wait(
|
|
60
|
+
wait_tasks,
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
63
|
+
)
|
|
64
|
+
for task in pending:
|
|
65
|
+
task.cancel()
|
|
66
|
+
for task in pending:
|
|
67
|
+
try:
|
|
68
|
+
await task
|
|
69
|
+
except asyncio.CancelledError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
if not done:
|
|
73
|
+
return None, False
|
|
74
|
+
|
|
75
|
+
if shutdown_task is not None and shutdown_task in done:
|
|
76
|
+
return None, True
|
|
77
|
+
|
|
78
|
+
return subscriber_task.result(), False
|
|
79
|
+
finally:
|
|
80
|
+
if not subscriber_task.done():
|
|
81
|
+
subscriber_task.cancel()
|
|
82
|
+
try:
|
|
83
|
+
await subscriber_task
|
|
84
|
+
except asyncio.CancelledError:
|
|
85
|
+
pass
|
|
86
|
+
if shutdown_task is not None and not shutdown_task.done():
|
|
87
|
+
shutdown_task.cancel()
|
|
88
|
+
try:
|
|
89
|
+
await shutdown_task
|
|
90
|
+
except asyncio.CancelledError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
wait_for_event_stream_item = _wait_for_event_stream_item
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AuthController(Controller):
|
|
98
|
+
path = "/auth"
|
|
99
|
+
|
|
100
|
+
@get("/session")
|
|
101
|
+
async def get_auth_session(
|
|
102
|
+
self,
|
|
103
|
+
request: Request[Any, Any, Any],
|
|
104
|
+
state: State,
|
|
105
|
+
) -> AuthSessionResponse:
|
|
106
|
+
payload = _service(state, "auth_service").session_payload(request)
|
|
107
|
+
return AuthSessionResponse(**payload)
|
|
108
|
+
|
|
109
|
+
@post("/login")
|
|
110
|
+
async def login(
|
|
111
|
+
self,
|
|
112
|
+
data: AuthLoginRequest,
|
|
113
|
+
request: Request[Any, Any, Any],
|
|
114
|
+
state: State,
|
|
115
|
+
) -> Response:
|
|
116
|
+
auth_service = _service(state, "auth_service")
|
|
117
|
+
if not auth_service.verify_login_password(data.password):
|
|
118
|
+
raise HTTPException(status_code=401, detail="Invalid password.")
|
|
119
|
+
return auth_service.build_login_response(request)
|
|
120
|
+
|
|
121
|
+
@post("/logout")
|
|
122
|
+
async def logout(self, request: Request[Any, Any, Any], state: State) -> Response:
|
|
123
|
+
return _service(state, "auth_service").build_logout_response(request)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class HealthController(Controller):
|
|
127
|
+
@get("/health")
|
|
128
|
+
async def health(self, state: State) -> HealthResponse:
|
|
129
|
+
config_service = _service(state, "config_service")
|
|
130
|
+
frontend_service = _service(state, "frontend_service")
|
|
131
|
+
frontend = await frontend_service.get_status()
|
|
132
|
+
settings = config_service.load_web_settings()
|
|
133
|
+
runtime: dict[str, Any] = {}
|
|
134
|
+
failing_servers = [
|
|
135
|
+
name
|
|
136
|
+
for name, entry in runtime.items()
|
|
137
|
+
if entry.status in {"failed", "needs_auth", "needs_client_registration"}
|
|
138
|
+
]
|
|
139
|
+
mcp_ready = not failing_servers
|
|
140
|
+
return HealthResponse(
|
|
141
|
+
frontend=frontend,
|
|
142
|
+
llm=LLMHealth(
|
|
143
|
+
ready=True,
|
|
144
|
+
detail=None,
|
|
145
|
+
),
|
|
146
|
+
mcp=MCPHealth(
|
|
147
|
+
ready=mcp_ready,
|
|
148
|
+
detail=None
|
|
149
|
+
if mcp_ready
|
|
150
|
+
else f"Problematic MCP servers: {', '.join(failing_servers)}",
|
|
151
|
+
runtime=runtime,
|
|
152
|
+
),
|
|
153
|
+
backends=config_service.build_backend_health(),
|
|
154
|
+
allowed_roots=settings.allowed_roots,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ConfigController(Controller):
|
|
159
|
+
path = "/config"
|
|
160
|
+
|
|
161
|
+
@get()
|
|
162
|
+
async def get_config(self, state: State) -> Response:
|
|
163
|
+
config_service = _service(state, "config_service")
|
|
164
|
+
runtime: dict[str, Any] = {}
|
|
165
|
+
return Response(content=config_service.build_public_config(runtime).model_dump())
|
|
166
|
+
|
|
167
|
+
@put("/llm")
|
|
168
|
+
async def save_llm_config(self, data: SaveLLMRequest, state: State) -> Response:
|
|
169
|
+
config_service = _service(state, "config_service")
|
|
170
|
+
config_service.save_llm_settings(data)
|
|
171
|
+
runtime: dict[str, Any] = {}
|
|
172
|
+
return Response(content=config_service.build_public_config(runtime).model_dump())
|
|
173
|
+
|
|
174
|
+
@put("/roots")
|
|
175
|
+
async def save_allowed_roots_config(
|
|
176
|
+
self,
|
|
177
|
+
data: SaveAllowedRootsRequest,
|
|
178
|
+
state: State,
|
|
179
|
+
) -> Response:
|
|
180
|
+
config_service = _service(state, "config_service")
|
|
181
|
+
config_service.save_allowed_roots(data.allowed_roots)
|
|
182
|
+
runtime: dict[str, Any] = {}
|
|
183
|
+
return Response(content=config_service.build_public_config(runtime).model_dump())
|
|
184
|
+
|
|
185
|
+
@put("/app")
|
|
186
|
+
async def save_app_config(self, data: SaveAppSettingsRequest, state: State) -> Response:
|
|
187
|
+
config_service = _service(state, "config_service")
|
|
188
|
+
config_service.save_app_settings(data)
|
|
189
|
+
runtime: dict[str, Any] = {}
|
|
190
|
+
return Response(content=config_service.build_public_config(runtime).model_dump())
|
|
191
|
+
|
|
192
|
+
@get("/mcp")
|
|
193
|
+
async def get_mcp_config(self, state: State) -> MCPConfigResponse:
|
|
194
|
+
config_service = _service(state, "config_service")
|
|
195
|
+
return MCPConfigResponse(
|
|
196
|
+
mcp_servers=config_service.load_mcp_servers(),
|
|
197
|
+
runtime={},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@put("/mcp")
|
|
201
|
+
async def save_mcp_config(
|
|
202
|
+
self,
|
|
203
|
+
data: SaveMCPConfigRequest,
|
|
204
|
+
state: State,
|
|
205
|
+
) -> MCPConfigResponse:
|
|
206
|
+
config_service = _service(state, "config_service")
|
|
207
|
+
try:
|
|
208
|
+
config_service.save_mcp_servers(data.mcp_servers)
|
|
209
|
+
except MCPValidationError as exc:
|
|
210
|
+
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
211
|
+
return MCPConfigResponse(
|
|
212
|
+
mcp_servers=config_service.load_mcp_servers(),
|
|
213
|
+
runtime={},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
@post("/mcp/reload")
|
|
217
|
+
async def reload_mcp_config(self, state: State) -> MCPConfigResponse:
|
|
218
|
+
config_service = _service(state, "config_service")
|
|
219
|
+
return MCPConfigResponse(
|
|
220
|
+
mcp_servers=config_service.load_mcp_servers(),
|
|
221
|
+
runtime={},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class SystemController(Controller):
|
|
226
|
+
path = "/system"
|
|
227
|
+
|
|
228
|
+
@post("/select-directory")
|
|
229
|
+
async def select_directory(
|
|
230
|
+
self,
|
|
231
|
+
data: SelectDirectoryRequest,
|
|
232
|
+
state: State,
|
|
233
|
+
) -> SelectDirectoryResponse:
|
|
234
|
+
selected_path = await asyncio.to_thread(
|
|
235
|
+
_service(state, "directory_picker_service").select_directory,
|
|
236
|
+
data.initial_path,
|
|
237
|
+
)
|
|
238
|
+
return SelectDirectoryResponse(
|
|
239
|
+
selected=bool(selected_path),
|
|
240
|
+
project_path=selected_path or "",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class EventsController(Controller):
|
|
245
|
+
path = "/events"
|
|
246
|
+
|
|
247
|
+
@get("/stream")
|
|
248
|
+
async def stream_events(self, state: State) -> ServerSentEvent:
|
|
249
|
+
event_broker = _service(state, "event_broker")
|
|
250
|
+
shutdown_event = _shutdown_event(state)
|
|
251
|
+
|
|
252
|
+
async def event_stream() -> AsyncIterator[ServerSentEventMessage]:
|
|
253
|
+
subscriber = event_broker.subscribe()
|
|
254
|
+
try:
|
|
255
|
+
yield ServerSentEventMessage(
|
|
256
|
+
event="connected",
|
|
257
|
+
data=json.dumps({"status": "ok"}, ensure_ascii=False),
|
|
258
|
+
)
|
|
259
|
+
while True:
|
|
260
|
+
item, shutting_down = await _wait_for_event_stream_item(
|
|
261
|
+
subscriber,
|
|
262
|
+
shutdown_event,
|
|
263
|
+
timeout=EVENT_STREAM_PING_INTERVAL_SECONDS,
|
|
264
|
+
)
|
|
265
|
+
if shutting_down:
|
|
266
|
+
break
|
|
267
|
+
if item is None:
|
|
268
|
+
yield ServerSentEventMessage(
|
|
269
|
+
event="ping",
|
|
270
|
+
data=json.dumps({"status": "alive"}, ensure_ascii=False),
|
|
271
|
+
)
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
yield ServerSentEventMessage(
|
|
275
|
+
event=item.event,
|
|
276
|
+
data=json.dumps(item.data, ensure_ascii=False),
|
|
277
|
+
)
|
|
278
|
+
finally:
|
|
279
|
+
event_broker.unsubscribe(subscriber)
|
|
280
|
+
|
|
281
|
+
return ServerSentEvent(event_stream())
|