diff --git a/README.md b/README.md index 89ee179..a564e4d 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Discord X License + Desktop downloads Documentation

diff --git a/README_zh.md b/README_zh.md index dc0b436..f7056a5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -5,6 +5,7 @@ Discord X License + 桌面端下载次数 文档

diff --git a/packages/core/src/agents/codex/cli-middleware-runtime.ts b/packages/core/src/agents/codex/cli-middleware-runtime.ts index 47302a4..0c87695 100644 --- a/packages/core/src/agents/codex/cli-middleware-runtime.ts +++ b/packages/core/src/agents/codex/cli-middleware-runtime.ts @@ -2380,8 +2380,12 @@ function configRead(params, values) { model: agentEnv(runtimeAgent, "MODEL") || DEFAULT_MODEL, model_catalog_json: JSON.stringify(modelCatalogConfigValue()), model_provider: agentEnv(runtimeAgent, "MODEL_PROVIDER") || "claude-code", - approval_policy: "default", - sandbox_mode: "workspace-write" + approval_policy: "default" + // sandbox_mode intentionally omitted: let Codex read it from its own + // config.toml (e.g. [windows] sandbox) instead of forcing workspace-write. + // Forcing workspace-write triggers codex-windows-sandbox-setup.exe on every + // command, which fails on systems where the COM+ catalog is unavailable + // (see openai/codex#29332), surfacing as repeated error dialogs. } }; } diff --git a/packages/core/src/gateway/service.ts b/packages/core/src/gateway/service.ts index 382d1ce..a10ff09 100644 --- a/packages/core/src/gateway/service.ts +++ b/packages/core/src/gateway/service.ts @@ -637,7 +637,7 @@ class GatewayService { const client = inferGatewayClient(apiKey, request.headers); const cursorCompatPreparation = prepareCursorOpenAICompatChatBody(this.config, client, method, path, requestBody); if (cursorCompatPreparation) { - headers["x-ccr-cursor-openai-compat"] = cursorCompatPreparation.diagnostic; + headers["x-ccr-cursor-openai-compat"] = sanitizeHeaderValue(cursorCompatPreparation.diagnostic); } let bodyToForward: Buffer | undefined = cursorCompatPreparation?.body ?? requestBody; let routeFallback = this.config.Router.fallback; @@ -645,12 +645,12 @@ class GatewayService { let codexApplyPatchBridgeActive = false; const claudeModelRewrite = prepareClaudeCodeDiscoveredModelRequest(this.config, request.headers, method, path, bodyToForward); if (claudeModelRewrite) { - headers["x-ccr-claude-model-discovery"] = claudeModelRewrite.diagnostic; + headers["x-ccr-claude-model-discovery"] = sanitizeHeaderValue(claudeModelRewrite.diagnostic); bodyToForward = claudeModelRewrite.body; } const claudeAppModelRewrite = prepareClaudeAppFallbackModelRequest(this.config, method, path, bodyToForward); if (claudeAppModelRewrite) { - headers["x-ccr-claude-app-model-rewrite"] = claudeAppModelRewrite.diagnostic; + headers["x-ccr-claude-app-model-rewrite"] = sanitizeHeaderValue(claudeAppModelRewrite.diagnostic); bodyToForward = claudeAppModelRewrite.body; routedModel = claudeAppModelRewrite.routedModel; } @@ -680,6 +680,16 @@ class GatewayService { upstreamAbortController.abort(new Error(clientDisconnectMessage)); onClientDisconnect?.(); }); + response.on("error", (error) => { + // Client-side write failures (EPIPE / ECONNRESET when the client closes + // mid-stream, common during tool execution) must not crash the main + // process as an Uncaught Exception. Swallow them here; the close handler + // above already records the disconnect via writeStreamLog. + if (!clientDisconnected) { + clientDisconnected = true; + upstreamAbortController.abort(new Error(clientDisconnectMessage)); + } + }); const writeRequestLog = ( statusCode: number, @@ -745,10 +755,10 @@ class GatewayService { }); const serialized = Buffer.from(`${JSON.stringify(routed.body)}\n`, "utf8"); headers["content-type"] = "application/json"; - headers["x-ccr-route-reason"] = routed.decision.reason; + headers["x-ccr-route-reason"] = sanitizeHeaderValue(routed.decision.reason); routeFallback = routed.decision.fallback ?? routeFallback; if (routed.decision.model) { - headers["x-ccr-routed-model"] = routed.decision.model; + headers["x-ccr-routed-model"] = sanitizeHeaderValue(routed.decision.model); routedModel = routed.decision.model; } bodyToForward = serialized; @@ -763,10 +773,10 @@ class GatewayService { }); const serialized = Buffer.from(`${JSON.stringify(routed.body)}\n`, "utf8"); headers["content-type"] = "application/json"; - headers["x-ccr-route-reason"] = routed.decision.reason; + headers["x-ccr-route-reason"] = sanitizeHeaderValue(routed.decision.reason); routeFallback = routed.decision.fallback ?? routeFallback; if (routed.decision.model) { - headers["x-ccr-routed-model"] = routed.decision.model; + headers["x-ccr-routed-model"] = sanitizeHeaderValue(routed.decision.model); routedModel = routed.decision.model; } bodyToForward = serialized; @@ -783,7 +793,7 @@ class GatewayService { if (codexApplyPatchBridgeRequest) { bodyToForward = codexApplyPatchBridgeRequest.body; codexApplyPatchBridgeActive = true; - headers["x-ccr-codex-patch-bridge"] = codexApplyPatchBridgeRequest.diagnostic; + headers["x-ccr-codex-patch-bridge"] = sanitizeHeaderValue(codexApplyPatchBridgeRequest.diagnostic); headers["content-type"] = "application/json"; } @@ -908,7 +918,13 @@ class GatewayService { bodyToForward = upstreamResult.attempt.body ?? bodyToForward; routedModel = upstreamResult.attempt.model ?? routedModel; const responseHeaders = rewriteCapabilityResponseHeaders( - mergeFallbackResponseHeaders(upstreamResponseHeaders(upstreamResult), upstreamResult), + // Copy into a mutable Headers instance: upstream fetch Response.headers + // can be immutable (TypeError: immutable on .delete/.set), and + // mergeFallbackResponseHeaders returns the original object as-is when + // no fallback occurred. Codex apply_patch / web-search paths call + // .delete("content-length") below, which would otherwise throw and + // surface as a 502. + new Headers(mergeFallbackResponseHeaders(upstreamResponseHeaders(upstreamResult), upstreamResult)), this.config ); const upstreamResponse = upstreamResult.response; @@ -997,7 +1013,7 @@ class GatewayService { writeStreamLog(); } }); - responseBody.once("error", (error) => { + responseBody.on("error", (error) => { streamDetectedError ??= sseErrorDetector.finish(); writeStreamLog(clientDisconnected ? clientDisconnectMessage : formatError(error)); }); @@ -5627,7 +5643,7 @@ function prepareUpstreamCredentialAttempt(input: { const headers: Record = { ...input.headers, "x-target-providers": selection.credentials.map((candidate) => candidate.internalName).join(","), - "x-ccr-logical-provider": target.provider.name, + "x-ccr-logical-provider": providerRuntimeId(target.provider), "x-ccr-provider-credential-chain": selection.credentials.map((candidate) => candidate.credentialId).join(",") }; delete headers["x-target-provider"]; @@ -6084,7 +6100,7 @@ function mergeFallbackResponseHeaders(headers: Headers, result: UpstreamFetchRes merged.set("x-ccr-fallback-delays-ms", formatFallbackDelays(result.failedAttempts)); } if (result.attempt.model) { - merged.set("x-ccr-fallback-model", result.attempt.model); + merged.set("x-ccr-fallback-model", sanitizeHeaderValue(result.attempt.model)); } } if (credentialIds.length) { @@ -6505,6 +6521,20 @@ function sanitizeProviderHeaderId(value: string | undefined): string | undefined return normalized || undefined; } +function sanitizeHeaderValue(value: unknown): string { + // HTTP header values must be ByteString (code point <= 255). Values derived + // from user-facing names — model selectors like "小米mimo/...", provider + // names, route reasons — can contain non-ASCII characters that crash Node's + // fetch/undici with "Cannot convert argument to a ByteString" (surfaced as + // 502). Normalize to ASCII while preserving case and printable punctuation. + const text = typeof value === "string" && value.trim() ? value : "unknown"; + const sanitized = text + .replace(/[^\x20-\x7E]+/g, "-") + .replace(/-{2,}/g, "-") + .replace(/^-+|-+$/g, ""); + return sanitized || "unknown"; +} + function providerCredentialInternalName( provider: GatewayProviderConfig, protocol: GatewayProviderProtocol,