Spaces:
Paused
Paused
icebear0828 commited on
Commit Β·
b94940f
1
Parent(s): c424a99
fix: model store dual index, desktop UI fallback, dynamic port support
Browse files- Model store: replace O(n) plan map with dual index (planTypeβmodels + modelIdβplans), both O(1)
- Desktop UI: fallback to web UI when desktop build missing, add startup path diagnostics
- Server: resolve actual bound port from server.address() to support port=0
- Model fetcher: fix log message (said 5s, actually 1s)
- Update Codex Desktop fingerprint to v26.311.21342 (build 993)
- config/default.yaml +2 -2
- config/prompts/automation-response.md +2 -1
- package-lock.json +2 -2
- scripts/extract-fingerprint.ts +0 -51
- src/index.ts +5 -1
- src/models/model-fetcher.ts +1 -1
- src/models/model-store.ts +26 -19
- src/routes/web.ts +26 -18
config/default.yaml
CHANGED
|
@@ -3,8 +3,8 @@ api:
|
|
| 3 |
timeout_seconds: 60
|
| 4 |
client:
|
| 5 |
originator: Codex Desktop
|
| 6 |
-
app_version: 26.
|
| 7 |
-
build_number: "
|
| 8 |
platform: darwin
|
| 9 |
arch: arm64
|
| 10 |
chromium_version: "144"
|
|
|
|
| 3 |
timeout_seconds: 60
|
| 4 |
client:
|
| 5 |
originator: Codex Desktop
|
| 6 |
+
app_version: 26.311.21342
|
| 7 |
+
build_number: "993"
|
| 8 |
platform: darwin
|
| 9 |
arch: arm64
|
| 10 |
chromium_version: "144"
|
config/prompts/automation-response.md
CHANGED
|
@@ -48,4 +48,5 @@ Response MUST end with a remark-directive block.
|
|
| 48 |
- Waiting on user decision:
|
| 49 |
- \`::inbox-item{title="Choose API shape for filters" summary="Two options drafted; pick A vs B"}\`
|
| 50 |
- Status update with next step:
|
| 51 |
-
- \`::inbox-item{title="PR comments addressed" summary="Ready for re-review; focus on auth edge case"}\
|
|
|
|
|
|
| 48 |
- Waiting on user decision:
|
| 49 |
- \`::inbox-item{title="Choose API shape for filters" summary="Two options drafted; pick A vs B"}\`
|
| 50 |
- Status update with next step:
|
| 51 |
+
- \`::inbox-item{title="PR comments addressed" summary="Ready for re-review; focus on auth edge case"}\`
|
| 52 |
+
`;
|
package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
{
|
| 2 |
"name": "codex-proxy",
|
| 3 |
-
"version": "1.0.
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "codex-proxy",
|
| 9 |
-
"version": "1.0.
|
| 10 |
"hasInstallScript": true,
|
| 11 |
"dependencies": {
|
| 12 |
"@hono/node-server": "^1.0.0",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "codex-proxy",
|
| 3 |
+
"version": "1.0.39",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
"name": "codex-proxy",
|
| 9 |
+
"version": "1.0.39",
|
| 10 |
"hasInstallScript": true,
|
| 11 |
"dependencies": {
|
| 12 |
"@hono/node-server": "^1.0.0",
|
scripts/extract-fingerprint.ts
CHANGED
|
@@ -123,7 +123,6 @@ function extractFromMainJs(
|
|
| 123 |
): {
|
| 124 |
apiBaseUrl: string | null;
|
| 125 |
originator: string | null;
|
| 126 |
-
models: string[];
|
| 127 |
whamEndpoints: string[];
|
| 128 |
userAgentContains: string;
|
| 129 |
} {
|
|
@@ -157,17 +156,6 @@ function extractFromMainJs(
|
|
| 157 |
throw new Error("Failed to extract critical field: originator");
|
| 158 |
}
|
| 159 |
|
| 160 |
-
// Models β deduplicate, use capture group if specified
|
| 161 |
-
const models: Set<string> = new Set();
|
| 162 |
-
const modelPattern = patterns.models;
|
| 163 |
-
if (modelPattern?.pattern) {
|
| 164 |
-
const re = new RegExp(modelPattern.pattern, "g");
|
| 165 |
-
const groupIdx = modelPattern.group ?? 0;
|
| 166 |
-
for (const m of content.matchAll(re)) {
|
| 167 |
-
models.add(m[groupIdx] ?? m[0]);
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
// WHAM endpoints β deduplicate, use capture group if specified
|
| 172 |
const endpoints: Set<string> = new Set();
|
| 173 |
const epPattern = patterns.wham_endpoints;
|
|
@@ -182,7 +170,6 @@ function extractFromMainJs(
|
|
| 182 |
return {
|
| 183 |
apiBaseUrl,
|
| 184 |
originator,
|
| 185 |
-
models: [...models].sort(),
|
| 186 |
whamEndpoints: [...endpoints].sort(),
|
| 187 |
userAgentContains: "Codex Desktop/",
|
| 188 |
};
|
|
@@ -440,7 +427,6 @@ async function main() {
|
|
| 440 |
let mainJsResults = {
|
| 441 |
apiBaseUrl: null as string | null,
|
| 442 |
originator: null as string | null,
|
| 443 |
-
models: [] as string[],
|
| 444 |
whamEndpoints: [] as string[],
|
| 445 |
userAgentContains: "Codex Desktop/",
|
| 446 |
};
|
|
@@ -484,21 +470,10 @@ async function main() {
|
|
| 484 |
const m = mainJs.match(new RegExp(apiPattern.pattern));
|
| 485 |
if (m) mainJsResults.apiBaseUrl = m[0];
|
| 486 |
}
|
| 487 |
-
const modelPattern = patterns.main_js.models;
|
| 488 |
-
if (modelPattern?.pattern) {
|
| 489 |
-
const re = new RegExp(modelPattern.pattern, "g");
|
| 490 |
-
const groupIdx = modelPattern.group ?? 0;
|
| 491 |
-
const modelSet = new Set<string>();
|
| 492 |
-
for (const m of mainJs.matchAll(re)) {
|
| 493 |
-
modelSet.add(m[groupIdx] ?? m[0]);
|
| 494 |
-
}
|
| 495 |
-
mainJsResults.models = [...modelSet].sort();
|
| 496 |
-
}
|
| 497 |
}
|
| 498 |
|
| 499 |
console.log(` API base URL: ${mainJsResults.apiBaseUrl}`);
|
| 500 |
console.log(` originator: ${mainJsResults.originator}`);
|
| 501 |
-
console.log(` models: ${mainJsResults.models.join(", ")}`);
|
| 502 |
console.log(` WHAM endpoints: ${mainJsResults.whamEndpoints.length} found`);
|
| 503 |
|
| 504 |
// Extract system prompts
|
|
@@ -510,31 +485,6 @@ async function main() {
|
|
| 510 |
console.log(` automation-response: ${promptResults.automationResponse ? "found" : "NOT FOUND"}`);
|
| 511 |
}
|
| 512 |
|
| 513 |
-
// Scan webview assets for additional model IDs
|
| 514 |
-
const webviewAssetsDir = join(asarRoot, "webview/assets");
|
| 515 |
-
if (existsSync(webviewAssetsDir)) {
|
| 516 |
-
console.log("[extract] Scanning webview assets for additional models...");
|
| 517 |
-
const modelPattern = patterns.main_js.models;
|
| 518 |
-
if (modelPattern?.pattern) {
|
| 519 |
-
const webviewFiles = readdirSync(webviewAssetsDir).filter((f) => f.endsWith(".js"));
|
| 520 |
-
const webviewModels = new Set<string>();
|
| 521 |
-
for (const file of webviewFiles) {
|
| 522 |
-
const content = readFileSync(join(webviewAssetsDir, file), "utf-8");
|
| 523 |
-
const re = new RegExp(modelPattern.pattern, "g");
|
| 524 |
-
const groupIdx = modelPattern.group ?? 0;
|
| 525 |
-
for (const m of content.matchAll(re)) {
|
| 526 |
-
webviewModels.add(m[groupIdx] ?? m[0]);
|
| 527 |
-
}
|
| 528 |
-
}
|
| 529 |
-
const existingModels = new Set(mainJsResults.models);
|
| 530 |
-
const newFromWebview = [...webviewModels].filter((m) => !existingModels.has(m));
|
| 531 |
-
if (newFromWebview.length > 0) {
|
| 532 |
-
console.log(`[extract] Webview: ${newFromWebview.length} additional models: ${newFromWebview.join(", ")}`);
|
| 533 |
-
mainJsResults.models = [...mainJsResults.models, ...newFromWebview].sort();
|
| 534 |
-
}
|
| 535 |
-
}
|
| 536 |
-
}
|
| 537 |
-
|
| 538 |
// Save extracted prompts
|
| 539 |
const dc = savePrompt("desktop-context", promptResults.desktopContext);
|
| 540 |
const tg = savePrompt("title-generation", promptResults.titleGeneration);
|
|
@@ -549,7 +499,6 @@ async function main() {
|
|
| 549 |
chromium_version: chromiumVersion,
|
| 550 |
api_base_url: mainJsResults.apiBaseUrl,
|
| 551 |
originator: mainJsResults.originator,
|
| 552 |
-
models: mainJsResults.models,
|
| 553 |
wham_endpoints: mainJsResults.whamEndpoints,
|
| 554 |
user_agent_contains: mainJsResults.userAgentContains,
|
| 555 |
sparkle_feed_url: sparkleFeedUrl,
|
|
|
|
| 123 |
): {
|
| 124 |
apiBaseUrl: string | null;
|
| 125 |
originator: string | null;
|
|
|
|
| 126 |
whamEndpoints: string[];
|
| 127 |
userAgentContains: string;
|
| 128 |
} {
|
|
|
|
| 156 |
throw new Error("Failed to extract critical field: originator");
|
| 157 |
}
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
// WHAM endpoints β deduplicate, use capture group if specified
|
| 160 |
const endpoints: Set<string> = new Set();
|
| 161 |
const epPattern = patterns.wham_endpoints;
|
|
|
|
| 170 |
return {
|
| 171 |
apiBaseUrl,
|
| 172 |
originator,
|
|
|
|
| 173 |
whamEndpoints: [...endpoints].sort(),
|
| 174 |
userAgentContains: "Codex Desktop/",
|
| 175 |
};
|
|
|
|
| 427 |
let mainJsResults = {
|
| 428 |
apiBaseUrl: null as string | null,
|
| 429 |
originator: null as string | null,
|
|
|
|
| 430 |
whamEndpoints: [] as string[],
|
| 431 |
userAgentContains: "Codex Desktop/",
|
| 432 |
};
|
|
|
|
| 470 |
const m = mainJs.match(new RegExp(apiPattern.pattern));
|
| 471 |
if (m) mainJsResults.apiBaseUrl = m[0];
|
| 472 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
}
|
| 474 |
|
| 475 |
console.log(` API base URL: ${mainJsResults.apiBaseUrl}`);
|
| 476 |
console.log(` originator: ${mainJsResults.originator}`);
|
|
|
|
| 477 |
console.log(` WHAM endpoints: ${mainJsResults.whamEndpoints.length} found`);
|
| 478 |
|
| 479 |
// Extract system prompts
|
|
|
|
| 485 |
console.log(` automation-response: ${promptResults.automationResponse ? "found" : "NOT FOUND"}`);
|
| 486 |
}
|
| 487 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
// Save extracted prompts
|
| 489 |
const dc = savePrompt("desktop-context", promptResults.desktopContext);
|
| 490 |
const tg = savePrompt("title-generation", promptResults.titleGeneration);
|
|
|
|
| 499 |
chromium_version: chromiumVersion,
|
| 500 |
api_base_url: mainJsResults.apiBaseUrl,
|
| 501 |
originator: mainJsResults.originator,
|
|
|
|
| 502 |
wham_endpoints: mainJsResults.whamEndpoints,
|
| 503 |
user_agent_contains: mainJsResults.userAgentContains,
|
| 504 |
sparkle_feed_url: sparkleFeedUrl,
|
src/index.ts
CHANGED
|
@@ -132,6 +132,10 @@ export async function startServer(options?: StartOptions): Promise<ServerHandle>
|
|
| 132 |
port,
|
| 133 |
});
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
const close = (): Promise<void> => {
|
| 136 |
return new Promise((resolve) => {
|
| 137 |
server.close(() => {
|
|
@@ -150,7 +154,7 @@ export async function startServer(options?: StartOptions): Promise<ServerHandle>
|
|
| 150 |
// Register close handler so self-update can attempt graceful shutdown before restart
|
| 151 |
setCloseHandler(close);
|
| 152 |
|
| 153 |
-
return { close, port };
|
| 154 |
}
|
| 155 |
|
| 156 |
// ββ CLI entry point ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 132 |
port,
|
| 133 |
});
|
| 134 |
|
| 135 |
+
// Resolve actual port (may differ from requested when port=0)
|
| 136 |
+
const addr = server.address();
|
| 137 |
+
const actualPort = (addr && typeof addr === "object") ? addr.port : port;
|
| 138 |
+
|
| 139 |
const close = (): Promise<void> => {
|
| 140 |
return new Promise((resolve) => {
|
| 141 |
server.close(() => {
|
|
|
|
| 154 |
// Register close handler so self-update can attempt graceful shutdown before restart
|
| 155 |
setCloseHandler(close);
|
| 156 |
|
| 157 |
+
return { close, port: actualPort };
|
| 158 |
}
|
| 159 |
|
| 160 |
// ββ CLI entry point ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
src/models/model-fetcher.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function startModelRefresh(
|
|
| 89 |
}
|
| 90 |
}, INITIAL_DELAY_MS);
|
| 91 |
|
| 92 |
-
console.log("[ModelFetcher] Scheduled initial model fetch in
|
| 93 |
}
|
| 94 |
|
| 95 |
function scheduleNext(
|
|
|
|
| 89 |
}
|
| 90 |
}, INITIAL_DELAY_MS);
|
| 91 |
|
| 92 |
+
console.log("[ModelFetcher] Scheduled initial model fetch in 1s");
|
| 93 |
}
|
| 94 |
|
| 95 |
function scheduleNext(
|
src/models/model-store.ts
CHANGED
|
@@ -39,8 +39,10 @@ interface ModelsConfig {
|
|
| 39 |
let _catalog: CodexModelInfo[] = [];
|
| 40 |
let _aliases: Record<string, string> = {};
|
| 41 |
let _lastFetchTime: string | null = null;
|
| 42 |
-
/**
|
| 43 |
-
let
|
|
|
|
|
|
|
| 44 |
|
| 45 |
// ββ Static loading βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
|
|
@@ -55,7 +57,8 @@ export function loadStaticModels(configDir?: string): void {
|
|
| 55 |
|
| 56 |
_catalog = (raw.models ?? []).map((m) => ({ ...m, source: "static" as const }));
|
| 57 |
_aliases = raw.aliases ?? {};
|
| 58 |
-
|
|
|
|
| 59 |
console.log(`[ModelStore] Loaded ${_catalog.length} static models, ${Object.keys(_aliases).length} aliases`);
|
| 60 |
}
|
| 61 |
|
|
@@ -214,30 +217,34 @@ export function applyBackendModels(backendModels: BackendModelEntry[]): void {
|
|
| 214 |
* Clears old records for this planType, applies merge, then records planβmodel mappings.
|
| 215 |
*/
|
| 216 |
export function applyBackendModelsForPlan(planType: string, backendModels: BackendModelEntry[]): void {
|
| 217 |
-
// Clear old planType records
|
| 218 |
-
for (const [modelId, plans] of _modelPlanMap) {
|
| 219 |
-
plans.delete(planType);
|
| 220 |
-
if (plans.size === 0) _modelPlanMap.delete(modelId);
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
// Merge into catalog (existing logic)
|
| 224 |
applyBackendModels(backendModels);
|
| 225 |
|
| 226 |
-
//
|
| 227 |
-
const
|
|
|
|
| 228 |
for (const raw of backendModels) {
|
| 229 |
const id = raw.slug ?? raw.id ?? raw.name ?? "";
|
| 230 |
-
if (
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
if (!plans) {
|
| 233 |
plans = new Set();
|
| 234 |
-
|
| 235 |
}
|
| 236 |
-
plans.add(
|
| 237 |
}
|
| 238 |
}
|
| 239 |
|
| 240 |
-
console.log(`[ModelStore] Plan "${planType}"
|
| 241 |
}
|
| 242 |
|
| 243 |
/**
|
|
@@ -245,7 +252,7 @@ export function applyBackendModelsForPlan(planType: string, backendModels: Backe
|
|
| 245 |
* Empty array means unknown (static-only or not yet fetched).
|
| 246 |
*/
|
| 247 |
export function getModelPlanTypes(modelId: string): string[] {
|
| 248 |
-
return [...(
|
| 249 |
}
|
| 250 |
|
| 251 |
// ββ Model name suffix parsing βββββββββββββββββββββββββββββββββββββββ
|
|
@@ -360,8 +367,8 @@ export function getModelStoreDebug(): {
|
|
| 360 |
} {
|
| 361 |
const backendCount = _catalog.filter((m) => m.source === "backend").length;
|
| 362 |
const planMap: Record<string, string[]> = {};
|
| 363 |
-
for (const [
|
| 364 |
-
planMap[
|
| 365 |
}
|
| 366 |
return {
|
| 367 |
totalModels: _catalog.length,
|
|
|
|
| 39 |
let _catalog: CodexModelInfo[] = [];
|
| 40 |
let _aliases: Record<string, string> = {};
|
| 41 |
let _lastFetchTime: string | null = null;
|
| 42 |
+
/** planType β Set<modelId> β write path: bulk replace per plan */
|
| 43 |
+
let _planModelMap: Map<string, Set<string>> = new Map();
|
| 44 |
+
/** modelId β Set<planType> β read path: O(1) lookup for routing */
|
| 45 |
+
let _modelPlanIndex: Map<string, Set<string>> = new Map();
|
| 46 |
|
| 47 |
// ββ Static loading βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
|
|
|
|
| 57 |
|
| 58 |
_catalog = (raw.models ?? []).map((m) => ({ ...m, source: "static" as const }));
|
| 59 |
_aliases = raw.aliases ?? {};
|
| 60 |
+
_planModelMap = new Map(); // Reset plan maps on reload
|
| 61 |
+
_modelPlanIndex = new Map();
|
| 62 |
console.log(`[ModelStore] Loaded ${_catalog.length} static models, ${Object.keys(_aliases).length} aliases`);
|
| 63 |
}
|
| 64 |
|
|
|
|
| 217 |
* Clears old records for this planType, applies merge, then records planβmodel mappings.
|
| 218 |
*/
|
| 219 |
export function applyBackendModelsForPlan(planType: string, backendModels: BackendModelEntry[]): void {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
// Merge into catalog (existing logic)
|
| 221 |
applyBackendModels(backendModels);
|
| 222 |
|
| 223 |
+
// Build new model set for this plan and replace atomically
|
| 224 |
+
const admittedIds = new Set<string>();
|
| 225 |
+
const catalogIds = new Set(_catalog.map((m) => m.id));
|
| 226 |
for (const raw of backendModels) {
|
| 227 |
const id = raw.slug ?? raw.id ?? raw.name ?? "";
|
| 228 |
+
if (catalogIds.has(id) || isCodexCompatibleId(id)) {
|
| 229 |
+
admittedIds.add(id);
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
_planModelMap.set(planType, admittedIds);
|
| 233 |
+
|
| 234 |
+
// Rebuild reverse index from scratch (plan types are few, this is cheap)
|
| 235 |
+
_modelPlanIndex = new Map();
|
| 236 |
+
for (const [plan, modelIds] of _planModelMap) {
|
| 237 |
+
for (const id of modelIds) {
|
| 238 |
+
let plans = _modelPlanIndex.get(id);
|
| 239 |
if (!plans) {
|
| 240 |
plans = new Set();
|
| 241 |
+
_modelPlanIndex.set(id, plans);
|
| 242 |
}
|
| 243 |
+
plans.add(plan);
|
| 244 |
}
|
| 245 |
}
|
| 246 |
|
| 247 |
+
console.log(`[ModelStore] Plan "${planType}": ${admittedIds.size} admitted models, ${_planModelMap.size} plans tracked`);
|
| 248 |
}
|
| 249 |
|
| 250 |
/**
|
|
|
|
| 252 |
* Empty array means unknown (static-only or not yet fetched).
|
| 253 |
*/
|
| 254 |
export function getModelPlanTypes(modelId: string): string[] {
|
| 255 |
+
return [...(_modelPlanIndex.get(modelId) ?? [])];
|
| 256 |
}
|
| 257 |
|
| 258 |
// ββ Model name suffix parsing βββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 367 |
} {
|
| 368 |
const backendCount = _catalog.filter((m) => m.source === "backend").length;
|
| 369 |
const planMap: Record<string, string[]> = {};
|
| 370 |
+
for (const [planType, modelIds] of _planModelMap) {
|
| 371 |
+
planMap[planType] = [...modelIds];
|
| 372 |
}
|
| 373 |
return {
|
| 374 |
totalModels: _catalog.length,
|
src/routes/web.ts
CHANGED
|
@@ -19,38 +19,46 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
|
|
| 19 |
const publicDir = getPublicDir();
|
| 20 |
const desktopPublicDir = getDesktopPublicDir();
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
// Serve Vite build assets (web)
|
| 23 |
app.use("/assets/*", serveStatic({ root: publicDir }));
|
| 24 |
|
| 25 |
app.get("/", (c) => {
|
| 26 |
try {
|
| 27 |
-
const html = readFileSync(
|
| 28 |
return c.html(html);
|
| 29 |
} catch (err) {
|
| 30 |
const msg = err instanceof Error ? err.message : String(err);
|
| 31 |
console.error(`[Web] Failed to read HTML file: ${msg}`);
|
| 32 |
-
return c.html("<h1>Codex Proxy</h1><p>UI files not found. Run 'npm
|
| 33 |
}
|
| 34 |
});
|
| 35 |
|
| 36 |
// Desktop UI β served at /desktop for Electron
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
try {
|
| 46 |
-
const html = readFileSync(resolve(desktopPublicDir, "index.html"), "utf-8");
|
| 47 |
return c.html(html);
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
| 54 |
|
| 55 |
app.get("/health", async (c) => {
|
| 56 |
const authenticated = accountPool.isAuthenticated();
|
|
|
|
| 19 |
const publicDir = getPublicDir();
|
| 20 |
const desktopPublicDir = getDesktopPublicDir();
|
| 21 |
|
| 22 |
+
const desktopIndexPath = resolve(desktopPublicDir, "index.html");
|
| 23 |
+
const webIndexPath = resolve(publicDir, "index.html");
|
| 24 |
+
const hasDesktopUI = existsSync(desktopIndexPath);
|
| 25 |
+
const hasWebUI = existsSync(webIndexPath);
|
| 26 |
+
|
| 27 |
+
console.log(`[Web] publicDir: ${publicDir} (exists: ${hasWebUI})`);
|
| 28 |
+
console.log(`[Web] desktopPublicDir: ${desktopPublicDir} (exists: ${hasDesktopUI})`);
|
| 29 |
+
|
| 30 |
// Serve Vite build assets (web)
|
| 31 |
app.use("/assets/*", serveStatic({ root: publicDir }));
|
| 32 |
|
| 33 |
app.get("/", (c) => {
|
| 34 |
try {
|
| 35 |
+
const html = readFileSync(webIndexPath, "utf-8");
|
| 36 |
return c.html(html);
|
| 37 |
} catch (err) {
|
| 38 |
const msg = err instanceof Error ? err.message : String(err);
|
| 39 |
console.error(`[Web] Failed to read HTML file: ${msg}`);
|
| 40 |
+
return c.html("<h1>Codex Proxy</h1><p>UI files not found. Run 'npm run build:web' first. The API is still available at /v1/chat/completions</p>");
|
| 41 |
}
|
| 42 |
});
|
| 43 |
|
| 44 |
// Desktop UI β served at /desktop for Electron
|
| 45 |
+
if (hasDesktopUI) {
|
| 46 |
+
app.use("/desktop/assets/*", serveStatic({
|
| 47 |
+
root: desktopPublicDir,
|
| 48 |
+
rewriteRequestPath: (path) => path.replace(/^\/desktop/, ""),
|
| 49 |
+
}));
|
| 50 |
+
|
| 51 |
+
app.get("/desktop", (c) => {
|
| 52 |
+
const html = readFileSync(desktopIndexPath, "utf-8");
|
|
|
|
|
|
|
| 53 |
return c.html(html);
|
| 54 |
+
});
|
| 55 |
+
} else {
|
| 56 |
+
// Fallback: redirect /desktop to web UI so the app is still usable
|
| 57 |
+
app.get("/desktop", (c) => {
|
| 58 |
+
console.warn(`[Web] Desktop UI not found at ${desktopIndexPath}, falling back to web UI`);
|
| 59 |
+
return c.redirect("/");
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
|
| 63 |
app.get("/health", async (c) => {
|
| 64 |
const authenticated = accountPool.isAuthenticated();
|