From 6080558054f7e1d49e4b9fbfa03e3a853a31579b Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Thu, 21 May 2026 22:03:12 +0800 Subject: [PATCH 1/4] debug(proxy): inject apply_patch I/O capture instrumentation (DO NOT MERGE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅用于 PR #236 merge 后回归测试 + DeepSeek / Grok / 其他 provider 验证。 4 个 stage 全量落盘到 ~/.codex-app-transfer/logs/apply-patch-debug/: - inbound: Codex Desktop → adapter (Responses API JSON) - outbound: adapter → upstream (provider Chat JSON) - upstream_raw: upstream raw SSE - downstream_emit: adapter 转换后 SSE → Codex Desktop 无业务逻辑改动,仅 forward.rs 加 90 行 DEBUG ONLY block + 4 处 dump 调用。 Trade-off:延迟微增(每 stream chunk 多 1 次 memcpy)。 **绝不 merge**:完成回归测试后 close PR(不删 branch 以便复用)。 Refs #235 --- crates/proxy/src/forward.rs | 106 +++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/crates/proxy/src/forward.rs b/crates/proxy/src/forward.rs index 6c94ad9..7accac1 100644 --- a/crates/proxy/src/forward.rs +++ b/crates/proxy/src/forward.rs @@ -35,6 +35,92 @@ use crate::diagnostics::{write_upstream_error_bundle, UpstreamErrorBundleInput}; use crate::resolver::{AuthScheme, ResolveError, ResolvedProvider, SharedResolver}; use crate::telemetry::proxy_telemetry; +// ============================================================================= +// !!! DEBUG ONLY — DO NOT MERGE !!! +// ============================================================================= +// 临时 instrumentation: 抓 apply_patch 全链路 I/O 到 +// ~/.codex-app-transfer/logs/apply-patch-debug/--.txt +// 4 stage: inbound / outbound / upstream_raw / downstream_emit +// 完整规则同 issue #235 debug worktree。本 PR 仅用于回归测试 + 多 provider +// 验证,**绝不 merge**。 +use std::sync::atomic::{AtomicU64, Ordering}; + +static DEBUG_APPLY_PATCH_SEQ: AtomicU64 = AtomicU64::new(0); + +fn debug_apply_patch_dir() -> Option { + let home = std::env::var("HOME").ok()?; + let dir = std::path::PathBuf::from(home).join(".codex-app-transfer/logs/apply-patch-debug"); + std::fs::create_dir_all(&dir).ok()?; + Some(dir) +} + +fn debug_apply_patch_dump(stage: &str, request_id: u64, payload: &[u8]) { + let Some(dir) = debug_apply_patch_dir() else { + return; + }; + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + let path = dir.join(format!("{ts:013}-{request_id:04}-{stage}.txt")); + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&path) + { + use std::io::Write as _; + let _ = f.write_all(payload); + } +} + +struct DebugTeeStream { + inner: codex_app_transfer_adapters::ByteStream, + request_id: u64, + stage: &'static str, + accumulator: Vec, +} + +impl DebugTeeStream { + fn new( + inner: codex_app_transfer_adapters::ByteStream, + request_id: u64, + stage: &'static str, + ) -> Self { + Self { + inner, + request_id, + stage, + accumulator: Vec::new(), + } + } +} + +impl Stream for DebugTeeStream { + type Item = Result; + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.as_mut().get_mut(); + match this.inner.as_mut().poll_next(cx) { + Poll::Ready(Some(Ok(chunk))) => { + this.accumulator.extend_from_slice(&chunk); + Poll::Ready(Some(Ok(chunk))) + } + other => other, + } + } +} + +impl Drop for DebugTeeStream { + fn drop(&mut self) { + if !self.accumulator.is_empty() { + debug_apply_patch_dump(self.stage, self.request_id, &self.accumulator); + } + } +} +// ============================================================================= +// !!! END DEBUG ONLY !!! +// ============================================================================= + #[derive(Clone)] pub struct ProxyState { pub http: reqwest::Client, @@ -307,6 +393,10 @@ pub async fn forward_handler( // 1. 收齐入站 body let mut body_bytes: Bytes = axum::body::to_bytes(body, usize::MAX).await?; + // !!! DEBUG ONLY — DO NOT MERGE (post-merge regression test) !!! + let debug_request_id = DEBUG_APPLY_PATCH_SEQ.fetch_add(1, Ordering::Relaxed); + debug_apply_patch_dump("inbound", debug_request_id, &body_bytes); + // 2. 解析(鉴权 + 路由) let client_path = parts .uri @@ -340,6 +430,9 @@ pub async fn forward_handler( let original_body_bytes_for_retry = body_bytes.clone(); let mut plan = adapter.prepare_request(&client_path, body_bytes, &resolved.provider)?; + // !!! DEBUG ONLY — DO NOT MERGE (post-merge regression test) !!! + debug_apply_patch_dump("outbound", debug_request_id, &plan.body); + // 5. 拼上游 URL —— base 末尾去 `/`,plan.upstream_path 必含 `/` let upstream_url = build_upstream_url(&resolved.upstream_base, &plan.upstream_path); let telemetry = proxy_telemetry(); @@ -507,8 +600,11 @@ pub async fn forward_handler( resp.bytes_stream() .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), ); + // !!! DEBUG ONLY — DO NOT MERGE !!! + let raw_tee: codex_app_transfer_adapters::ByteStream = + Box::pin(DebugTeeStream::new(raw, debug_request_id, "upstream_raw")); Box::pin(TracedStream::new( - raw, + raw_tee, t_send, st.as_u16(), upstream_url.clone(), @@ -554,13 +650,19 @@ pub async fn forward_handler( (st, hs, stream) }; - let response_plan = adapter.transform_response_stream( + let mut response_plan = adapter.transform_response_stream( status, upstream_headers, upstream_stream, &resolved.provider, &plan, )?; + // !!! DEBUG ONLY — DO NOT MERGE !!! + response_plan.stream = Box::pin(DebugTeeStream::new( + response_plan.stream, + debug_request_id, + "downstream_emit", + )); let success = response_plan.status.is_success(); telemetry.stats.record(success); telemetry.logs.add( From 0d2306ac5f2ec216f534e2106173f2cd03d5d2ad Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Thu, 21 May 2026 23:06:34 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(adapters):=20apply=5Fpatch=20prompt=20?= =?UTF-8?q?=E4=BF=AE=20Move=20=E7=A9=BA=20hunk=20+=20Begin=20Patch=20first?= =?UTF-8?q?-line(round=207=20Kimi=20=E5=AE=9E=E8=AF=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit post-PR #236 真机 round 7 Kimi 回归(27 turn / 4.9 min / 14 个 apply_patch) 出现 2 个 generation-quality fail,捕获后修 prompt 让其他 provider 不再撞: (1) t0002: 漏写 `*** Begin Patch`,直接以 `*** Add File:` 开头 报 `invalid patch: The first line of the patch must be '*** Begin Patch'` Kimi t0003 一句话自校正:"我忘了加 *** Begin Patch,需要重新调用"。 (2) t0020: `*** Update File: ` + `*** Move to: ` 不带 hunk(纯重命名) 报 `invalid hunk at line 2, Update file hunk for path '' is empty` Kimi t0021 多段推理后摸索出"包含至少一行内容"的 workaround。 prompt 加固 - `tools.rs::APPLY_PATCH_TOOL_DESCRIPTION_FOR_CHAT`: * 顶部加 "MUST start with `*** Begin Patch` as the literal first line" * `Update File: optionally followed by Move to` 加注 "STILL requires at least one hunk — see RENAME / MOVE FILE section" * 新增 RENAME / MOVE FILE 章节:Move to 必须配 ≥1 hunk;纯重命名用 Delete + Add File 替代方案(复制原内容加 `+` 前缀);Update+Move 用 于 rename WITH content change * chat-path gotcha 加第 6 / 7 条:Begin Patch first-line 强调 + Move 非空 hunk - `APPLY_PATCH_INPUT_DESCRIPTION_FOR_CHAT`:紧凑版同步上述 2 条 - `request.rs::APPLY_PATCH_CHAT_PATH_SYSTEM_GUIDANCE`:gotcha 加第 8 / 9 条 回归测试加 5 条断言(guidance 3 + outer 3)覆盖新规则。 PR #240 改定位:从纯 debug-only 转为业务修复 PR(WIP),capture instrumentation 仍保留作为多 provider 调试基础设施,后续轮次发现的 prompt gap / generation quality 问题继续在本 PR 收敛。本 PR 验证完整后 merge 时单独 commit revert capture。 510 tests pass。 Refs #235 --- crates/adapters/src/responses/request.rs | 4 +++ .../adapters/src/responses/request/tests.rs | 27 +++++++++++++++++++ .../adapters/src/responses/request/tools.rs | 16 ++++++++--- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/adapters/src/responses/request.rs b/crates/adapters/src/responses/request.rs index d1fc410..40f8db1 100644 --- a/crates/adapters/src/responses/request.rs +++ b/crates/adapters/src/responses/request.rs @@ -2341,6 +2341,10 @@ const APPLY_PATCH_CHAT_PATH_SYSTEM_GUIDANCE: &str = concat!( "\n", "7. If repeated Update File attempts on the same target fail with `Failed to find context` errors, fall back to a Delete File + Add File pair within the same patch (semantically equivalent to a full rewrite, avoids anchor-matching fragility).\n", "\n", + "8. `*** Begin Patch` MUST be the literal first line of the `input` string — no leading whitespace, no other content before it, never put `*** Add File:` or any operation header directly. Forgetting this causes `invalid patch: The first line of the patch must be '*** Begin Patch'`.\n", + "\n", + "9. `*** Update File: ` + `*** Move to: ` REQUIRES at least one hunk (with `-`/`+` lines or `*** End of File` marker). An empty Update+Move block fails with `Update file hunk for path '' is empty`. **For pure rename without content change**, use `*** Delete File: ` + `*** Add File: ` within the same patch (copy original content with `+` prefix per line). **For rename WITH content change**, keep Update+Move and include the actual `-`/`+` hunks.\n", + "\n", "Following these rules avoids retry storms and improves the success rate on first attempt." ); diff --git a/crates/adapters/src/responses/request/tests.rs b/crates/adapters/src/responses/request/tests.rs index a9a6a0a..12d2fe0 100644 --- a/crates/adapters/src/responses/request/tests.rs +++ b/crates/adapters/src/responses/request/tests.rs @@ -2018,6 +2018,19 @@ fn apply_patch_chat_path_guidance_injected_when_tool_registered() { guidance.contains("Delete File + Add File"), "guidance 必须含 Update 反复失败时 fallback 到 Delete+Add 兜底:{guidance}" ); + // Round 7 实证修复(t0002 漏写 Begin Patch + t0020 Move 空 hunk): + assert!( + guidance.contains("`*** Begin Patch` MUST be the literal first line"), + "guidance 必须显式要求 Begin Patch 是第一行:{guidance}" + ); + assert!( + guidance.contains("Move to:") && guidance.contains("REQUIRES at least one hunk"), + "guidance 必须显式要求 Move to 配非空 hunk:{guidance}" + ); + assert!( + guidance.contains("For pure rename"), + "guidance 必须给纯重命名的 Delete+Add 替代方案:{guidance}" + ); } #[test] @@ -2966,6 +2979,20 @@ fn tools_custom_apply_patch_injects_v4a_format_hint() { outer.contains("Delete File + Add File"), "必须含 Update 反复失败时 fallback 到 Delete+Add 兜底:{outer}" ); + // Round 7 实证修复(t0002 漏写 Begin Patch + t0020 Move 空 hunk): + assert!( + outer.contains("`*** Begin Patch` as the literal first line") + || outer.contains("`*** Begin Patch` MUST be the literal first line"), + "outer description 必须显式要求 Begin Patch 是第一行:{outer}" + ); + assert!( + outer.contains("RENAME / MOVE FILE") && outer.contains("at least one hunk"), + "outer description 必须含 RENAME/MOVE 章节 + Move 配非空 hunk 规则:{outer}" + ); + assert!( + outer.contains("pure rename") && outer.contains("Delete File"), + "outer description 必须给纯重命名的 Delete+Add 替代方案:{outer}" + ); // 参数描述紧凑版必须含同样核心规则(round 4 修复后) assert!( diff --git a/crates/adapters/src/responses/request/tools.rs b/crates/adapters/src/responses/request/tools.rs index 620e140..7341d06 100644 --- a/crates/adapters/src/responses/request/tools.rs +++ b/crates/adapters/src/responses/request/tools.rs @@ -31,9 +31,9 @@ pub(crate) const APPLY_PATCH_TOOL_NAME: &str = "apply_patch"; pub(crate) const APPLY_PATCH_TOOL_DESCRIPTION_FOR_CHAT: &str = concat!( "Edit files using the apply_patch tool. ", "Call this function with a single `input` string containing a V4A patch. ", - "The patch must start with `*** Begin Patch` and end with `*** End Patch`. ", + "**The patch MUST start with `*** Begin Patch` as the literal first line** (no leading whitespace, no other content before it), and end with `*** End Patch`. ", "Each file operation header is one of `*** Add File: `, ", - "`*** Update File: ` (optionally followed by `*** Move to: `), ", + "`*** Update File: ` (optionally followed by `*** Move to: `, but Update with Move STILL requires at least one hunk — see RENAME / MOVE FILE section), ", "or `*** Delete File: `. ", "Within Update hunks, the simplest form is just `-`/`+` lines with no `@@` and ", "no context (suitable when the `-` line is unique in the file). If disambiguation ", @@ -62,6 +62,10 @@ pub(crate) const APPLY_PATCH_TOOL_DESCRIPTION_FOR_CHAT: &str = concat!( "Add File block — they are reserved for Update File. Writing raw source code ", "(e.g. `def main():` with no `+` prefix) directly after `*** Add File:` causes ", "`'def main():' is not a valid hunk header` errors.\n\n", + "RENAME / MOVE FILE (`*** Move to:` always needs ≥1 hunk, never empty):\n", + "`*** Update File: \\n*** Move to: ` followed by **at least one hunk** with `-`/`+` lines (or `*** End of File` marker). An empty Update+Move block fails with `Update file hunk for path '' is empty`. ", + "**For pure rename (no content change)**: use a Delete + Add File pair within the same patch instead — `*** Delete File: ` followed by `*** Add File: ` with every original line prefixed `+`. ", + "**For rename WITH content change**: keep `*** Update File:` + `*** Move to:` and include the actual `-`/`+` hunks for the changes.\n\n", "LINE PREFIX FORMAT (zero whitespace between prefix and content):\n", "Every line in a hunk starts with exactly ONE character followed by content with ", "NO intervening space — `-line_content` (NOT `- line_content`), `+line_content` ", @@ -129,7 +133,9 @@ pub(crate) const APPLY_PATCH_TOOL_DESCRIPTION_FOR_CHAT: &str = concat!( "3. `*** Update File:` cannot operate on a completely empty file. Use shell to write at least one line first, then apply_patch.\n", "4. In a multi-line file, lone `+` lines without a corresponding `-` APPEND below the previous context — they do NOT replace any existing line. ", "To change a line, use `-` to remove the old line AND `+` to add the new one; do not omit the `-`.\n", - "5. If multiple Update attempts on the same file fail with `Failed to find context` errors, fall back to a Delete File + Add File pair within the same patch (semantically equivalent to a full rewrite) — this avoids anchor-matching fragility." + "5. If multiple Update attempts on the same file fail with `Failed to find context` errors, fall back to a Delete File + Add File pair within the same patch (semantically equivalent to a full rewrite) — this avoids anchor-matching fragility.\n", + "6. `*** Begin Patch` MUST be the literal first line of `input` — no preamble, no whitespace, no `*** Add File:` directly. Forgetting it causes `invalid patch: The first line of the patch must be '*** Begin Patch'`.\n", + "7. `*** Update File: ` + `*** Move to: ` requires at least one hunk (rename-only is NOT supported via Move). For pure rename without content change, use `*** Delete File: ` + `*** Add File: ` (copy original content with `+` prefix). Empty Update+Move fails with `Update file hunk for path '' is empty`." ); /// Chat-path replacement for the freeform `input` parameter description. @@ -154,7 +160,9 @@ pub(crate) const APPLY_PATCH_INPUT_DESCRIPTION_FOR_CHAT: &str = concat!( "(read via `cat ` first if unsure) — guessing produces `Failed to find context` errors. ", "Chat-path gotchas: do not Add+Update the same path in one patch; Update cannot ", "operate on a totally empty file; lone `+` without `-` appends instead of replacing. ", - "If Update fails repeatedly, fall back to Delete File + Add File in one patch." + "If Update fails repeatedly, fall back to Delete File + Add File in one patch. ", + "**`*** Begin Patch` MUST be the literal first line of `input`** (no preamble). ", + "**`*** Update File: ` + `*** Move to: ` requires ≥1 hunk** — for pure rename use `*** Delete File:` + `*** Add File:` instead." ); /// Responses tool 定义 → Chat tool 定义. From 6d1b9d12ff127b9ad9dbfc6dc7f3a2cfb53ac1e4 Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Fri, 22 May 2026 00:50:39 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(readme):=20=E7=94=A8=E6=88=B7=E5=AE=9A?= =?UTF-8?q?=E7=A8=BF=E5=90=8C=E6=AD=A5=20+=20=E9=A1=B6=E9=83=A8=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=A6=86=E7=9B=96=E7=BA=A2=E5=AD=97=E5=85=AC=E5=91=8A?= =?UTF-8?q?=20+=20macOS=20=E7=AD=BE=E5=90=8D=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文版用户主导改动(已合并) - 顶部段落合并 + Codex CLI → Codex APP(全大写,用户定稿术语) - 快速开始 6 步 → 3 步(去掉 Full access 切换段 + 桌面窗口兜底段,因为 v2 默认配置 + apply 流程已自动完成) - 常见问题"curl 提权"段重写(明示 macOS 无法触发提权选择 → 默认 full-access 写入) - 免责声明加 "除反馈功能外不涉及第三方联网行为" + "存在封号风险" - 致谢简化(删 Apache-2.0 / verbatim bullet 等详细描述,完整版在 ACKNOWLEDGEMENTS.md) - "日志去哪了" → "日志" 我的补充 - 顶部 [!IMPORTANT] 红字公告:Kimi For Coding + Xiaomi MiMo (Token Plan) 端到端 实测覆盖;其他 chat-completions 兼容 provider(DeepSeek / Kimi 月之暗面 / MiMo Pay for Token / 智谱 GLM / 阿里云百炼 / MiniMax)未做长期真机回归; QQ 3216202644 / 邮箱征集 API key 测试,承诺仅测试用即销毁 - macOS 签名提示加 Apple Developer ID + Apple 公证 (Notarization) + Gatekeeper 术语 + 「右键 → 打开」绕过方式 - 版本号 v2.1.6 → v2.1.12(对齐最新 Latest release) - Codex CLI 0.126 binary 版本引用保留(指特定 binary,非用户视角的 Codex App) 英文版同步 - 顶部 [!IMPORTANT] block 完整翻译 - 段落合并 + 快速开始 3 步 + curl Q&A 段 + 致谢简化 + 免责声明 + Logs 标题 - 英文 product name 用 Codex App(英文 capitalization convention,而非 ALL CAPS) - macOS 签名提示术语对应英文(Apple Developer ID / Notarized / Gatekeeper) --- README.en.md | 76 ++++++++++++++++++++++++++++---------------------- README.md | 79 ++++++++++++++++++++++++++-------------------------- 2 files changed, 83 insertions(+), 72 deletions(-) diff --git a/README.en.md b/README.en.md index 0217d37..3679353 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,14 @@ # Codex App Transfer +> [!IMPORTANT] +> 🔴 **Test coverage notice** +> +> This project has currently completed **end-to-end real-world testing only for Kimi For Coding and Xiaomi MiMo (Token Plan)**. +> +> Other built-in chat-completions-compatible providers (including **DeepSeek, Kimi (Moonshot Platform), Xiaomi MiMo (Pay for Token), Zhipu GLM, Aliyun Bailian (API Key / Token Plan), MiniMax**) **have not undergone long-term real-world regression** — they sit at unit-test + occasional user-report level only. +> +> If you'd be willing to **provide an API key from another provider for testing**, it would be deeply appreciated! Reach out via **QQ: `3216202644`** or email. The author guarantees the **API key will only be used for actual testing of this project**, destroyed immediately after testing, never repurposed. +

简体中文 | English | @@ -14,13 +23,11 @@ Downloads

-Codex App Transfer is a lightweight desktop config + forwarding tool for the **OpenAI Codex CLI**. It runs a local gateway that translates Codex CLI's Responses API requests (HTTP streaming / non-streaming + `/responses` fallback) into Chat Completions / Gemini Native / Anthropic Messages / Grok Web / other upstream formats, then forwards them to your chosen provider. - -Unlike `farion1231/cc-switch` and similar Anthropic-oriented Claude Code tools, this project focuses on **OpenAI Codex CLI**: manage providers, model mapping, forwarding ports, and a logs panel from a desktop UI so Codex CLI can talk to any third-party OpenAI / Gemini / Claude-compatible / Grok inference endpoint. +Codex App Transfer is a lightweight desktop config + forwarding tool for the **OpenAI Codex App**. It runs a local gateway that translates Codex App's Responses API requests (HTTP streaming / non-streaming + `/responses`) into Chat Completions and other upstream formats, then forwards them to your chosen provider. The desktop UI manages providers, model mappings, the forwarding port, and the logs panel, letting Codex App talk to any third-party chat/completions inference service. -After starting forwarding, Codex CLI talks to this tool at `127.0.0.1:18080`. Closing the window minimizes the app to the system tray; right-click the tray icon and choose "Exit" to fully quit. +After starting forwarding, Codex App talks to this tool at `127.0.0.1:18080`. Closing the window minimizes the app to the system tray; right-click the tray icon and choose "Exit" to fully quit. -Current version **v2.1.6** (see [Changelog](docs/CHANGELOG.md) and [Releases](https://github.com/Cmochance/codex-app-transfer/releases)). +Current version **v2.1.12** (see [Changelog](docs/CHANGELOG.md) and [Releases](https://github.com/Cmochance/codex-app-transfer/releases)). ## Preview @@ -30,20 +37,20 @@ Current version **v2.1.6** (see [Changelog](docs/CHANGELOG.md) and [Releases](ht | **Settings** | **Logs** | | ![Settings](docs/img/Settings.png) | ![Logs](docs/img/Logs.png) | -### Codex CLI in action +### Codex App in action -With any provider enabled, Codex CLI's model picker shows ` / `-style real model names. Tool loops / `previous_response_id` history replay / thinking-mode reasoning_content injection are all handled transparently by the local proxy: +With any provider enabled, Codex App's model picker shows ` / `-style real model names. Tool loops / `previous_response_id` history replay / thinking-mode reasoning_content injection are all handled transparently by the local proxy: -![Codex CLI real conversation](docs/img/codex-cli-real-chat.png) +![Codex App real conversation](docs/img/codex-cli-real-chat.png) ## What it does - Manage multiple providers; map OpenAI model names (`gpt-5.5` / `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.3-codex` / `gpt-5.2`) to the provider's real model IDs -- Translate Codex CLI's Responses API streaming / non-streaming requests into upstream protocols: Chat Completions, Gemini Native (`:streamGenerateContent`), Gemini CLI OAuth (Cloud Code Assist), Anthropic Messages (`/v1/messages`), Grok Web (`/rest/app-chat/conversations/new`), Responses passthrough, etc. +- Translate Codex App's Responses API streaming / non-streaming requests into upstream protocols: Chat Completions, Gemini Native (`:streamGenerateContent`), Gemini CLI OAuth (Cloud Code Assist), Anthropic Messages (`/v1/messages`), Grok Web (`/rest/app-chat/conversations/new`), Responses passthrough, etc. - Multi-turn tool conversation context + `previous_response_id` history replay + autocompact expansion + thinking / reasoning_content injection — all aligned with the OpenAI Responses API protocol -- Codex CLI's freeform `apply_patch` tool (edit-file +/- diff UI) works on DeepSeek / Kimi / MiMo and other chat-completions providers: the adapter bridges Responses `custom_tool_call` ↔ chat `function_call` wire forms, the model emits V4A-format patches, Codex CLI renders the diff (issue #235) +- Codex App's freeform `apply_patch` tool (edit-file +/- diff UI) works on chat-completions providers: the adapter bridges Responses `custom_tool_call` ↔ chat `function_call` wire forms, the model emits V4A-format patches, Codex App renders the diff (issue #235) - **Two-layer session history persistence**: L1 in-memory LRU + L2 sqlite with 30-day TTL (`~/.codex-app-transfer/sessions.db`), preserving history across `.app` restarts -- Codex CLI config guardrails: snapshots `~/.codex/{config.toml,auth.json}` before apply; restores via per-key smart merge on exit / next start +- Codex App config guardrails: snapshots `~/.codex/{config.toml,auth.json}` before apply; restores via per-key smart merge on exit / next start - Real-time logs panel auto-refreshing every 2s; unified `tracing::warn!(error_id, detail)` with stable tokens — operators can grep / aggregate - Feedback dialog automatically attaches diagnostic material (environment info, sanitized config, recent error snapshot with full request / response) — fewer back-and-forth follow-ups - Chinese / English UI; light / dark / green / orange / gray / white themes @@ -69,16 +76,13 @@ Each binary ships with `.sha256` and `.sig` (RSA-3072 PKCS#1 v1.5 + SHA-256); th Windows builds are not Authenticode-signed yet, so Windows may show an "unknown publisher" warning — use the `.sha256` / `.sig` to verify download integrity. +macOS builds are **not yet signed with an Apple Developer ID** and **not yet Notarized**, so Gatekeeper blocks first launch with "cannot be opened because it is from an unidentified developer". Workarounds: `right-click → Open` to allow once; or verify download integrity with `.sha256` / `.sig`, then click "Open Anyway" under `System Settings → Privacy & Security`. + ## Quick Start 1. Launch Codex App Transfer; the desktop window opens -2. On the dashboard, click the top-right "+" → pick a preset or add a custom provider; fill in API Base URL, API Key, model mappings -3. On the "Forwarding" page, click "Start" — the local port `18080` begins listening -4. In Codex CLI's config (`~/.codex/config.toml`), point `base_url` to `http://127.0.0.1:18080` and set the API Key to the Gateway API Key shown in this tool -5. Reopen Codex CLI; the model picker auto-lists the current provider's model mappings -6. ⚠️ **Switch Codex CLI to Full access** (`/approvals` → "Full access"): third-party providers will get stuck on the approval prompt under Codex CLI's default `auto` approval mode. Full access lets tool calls through directly — this is a **practical prerequisite** for using third-party providers - -If the desktop window can't open (rare — usually Tauri webview init failed / system webview missing), try restarting first; if it persists, re-download from [Releases](https://github.com/Cmochance/codex-app-transfer/releases) and check `~/.codex-app-transfer/logs/proxy-*.log`, or open an [Issue](https://github.com/Cmochance/codex-app-transfer/issues). v2 has no standalone HTTP admin UI (the admin panel runs in-process via Tauri's `cas://` scheme — **port 18081 is no longer listened on**). +2. On the dashboard, click the top-right "+" → pick a preset or add a custom provider; fill in API Base URL, API Key, then "Fetch models" and add model mappings +3. Click the **Apply** button at the bottom and confirm restarting Codex App to start using it (if a provider is already configured, just click the **Apply** button on its card on the home page) ## Provider compatibility matrix @@ -98,9 +102,9 @@ If the desktop window can't open (rare — usually Tauri webview init failed / s ## Model mapping -Codex CLI prompts by OpenAI model names; third-party providers use real IDs like `deepseek-v4-pro` / `kimi-k2.6` / `glm-5.1` / `gemini-3-pro`. +Codex App prompts by OpenAI model names; third-party providers use real IDs like `deepseek-v4-pro` / `kimi-k2.6` / `glm-5.1` / `gemini-3-pro`. -This tool maps via `provider.models[slot]` (`gpt-5.5` → `deepseek-v4-pro` etc.); Codex CLI's model picker shows ` / ` real names. Upstream `chatcmpl-...` response IDs are auto-rewritten to Codex CLI-validatable `resp_`, preserving deployment-affinity encoding so `previous_response_id` is consistent across turns. +This tool maps via `provider.models[slot]` (`gpt-5.5` → `deepseek-v4-pro` etc.); Codex App's model picker shows ` / ` real names. Upstream `chatcmpl-...` response IDs are auto-rewritten to Codex App-validatable `resp_`, preserving deployment-affinity encoding so `previous_response_id` is consistent across turns. ## Development (v2 / Rust) @@ -153,17 +157,23 @@ To add a new component: create `components/.css` + add a line `@import url ## Troubleshooting -### Codex CLI reports `404 Not Found url: http://127.0.0.1:18080/responses` +### Codex models can't run `curl` and similar network commands / approval prompt stuck + +`curl` requires elevated permissions. Third-party models currently cannot trigger the macOS escalation prompt, so this app writes `sandbox_mode = "danger-full-access"` + `approval_policy = "never"` into `~/.codex/config.toml` by default on apply. On Windows, or if you have other reasons, you can turn this off in Settings → "Allow Codex network tools (full-access mode)" (#215). + +> **⚠️ Security trade-off**: full-access mode lets the model read/write any file and run every command without approval = **fully trust the model** (equivalent to Codex's official "Full access" tier). With the toggle off, Codex falls back to the read-only sandbox + on-request approval. macOS still cannot trigger the elevation prompt, so there is no network access — only the selected model's built-in `web_search` capability is usable. If the model doesn't support `web_search`, all search calls return empty results. + +### Codex App reports `404 Not Found url: http://127.0.0.1:18080/responses` -Old versions only exposed `/v1/responses`; Codex CLI 0.126+ falls back to `/responses` (without `/v1/`). This tool added the route alias — update to v1.0.1+. +Old versions only exposed `/v1/responses`; Codex CLI 0.126+ falls back to `/responses` (without `/v1/`). This tool added the route alias — **v1.0.1 and later all support it** (current v2.x series ships it by default, no extra config needed). -### Codex CLI reports `stream disconnected before completion` +### Codex App reports `stream disconnected before completion` -Usually means `response.id` / `response.model` weren't returned in the shape Codex CLI expects. This tool rewrites upstream `chatcmpl-...` to `resp_` while preserving the requested model name — confirm forwarding logs show `resp_...` instead of `chatcmpl-...`. +Usually means `response.id` / `response.model` weren't returned in the shape Codex App expects. This tool rewrites upstream `chatcmpl-...` to `resp_` while preserving the requested model name — confirm forwarding logs show `resp_...` instead of `chatcmpl-...`. ### Upstream 400: `thinking is enabled but reasoning_content is missing` -Kimi / DeepSeek with thinking enabled require historical assistant messages with `tool_call` to carry `reasoning_content`. v1.0.1+ auto-fills a default empty string and maps reasoning items from Responses input to the corresponding assistant message. +Kimi / DeepSeek with thinking enabled require historical assistant messages with `tool_call` to carry `reasoning_content`. This tool **auto-fills a default empty string since v1.0.1** and maps reasoning items from Responses input to the corresponding assistant message. ### Upstream 400: `'reasoning_effort' does not support 'xhigh'` @@ -194,7 +204,7 @@ From v2.1.12+ the client **enforces** RSA-3072 PKCS#1-v1.5-SHA256 verification o Design intent: the client trusts only the build-time embedded public key and never lets a runtime URL replace it, blocking MITM rewrites of `latest.json` (the public PEM lives in `release/` but pulling it from the same origin as the update URL would dissolve the trust anchor). -### Where are the logs? +### Logs - App UI: real-time panel below the forwarding page, auto-refreshes every 2s - Disk: `~/.codex-app-transfer/logs/proxy-YYYY-MM-DD.log` — click "View Logs" to open directly @@ -206,27 +216,27 @@ Design intent: the client trusts only the build-time embedded public key and nev - **Protocol adapters**: `crates/adapters/` — Responses ↔ Chat / Gemini Native / Gemini CLI OAuth / Anthropic Messages / Grok Web (request body + streaming response state machine + reasoning_content + tool_calls) - **Frontend**: HTML + CSS + vanilla JavaScript + Bootstrap 5.3.3 (localized, no CDN dependency) - **Desktop shell**: Tauri 2 + tray-icon 0.23; the `cas://` URI scheme glues frontend/ and axum in-process, no TCP loopback -- **Storage**: `~/.codex-app-transfer/config.json` (config, compatible with v1.x), `~/.codex-app-transfer/sessions.db` (L2 sqlite session persistence), `~/.codex/{config.toml,auth.json}` (Codex CLI integration) +- **Storage**: `~/.codex-app-transfer/config.json` (config, compatible with v1.x), `~/.codex-app-transfer/sessions.db` (L2 sqlite session persistence), `~/.codex/{config.toml,auth.json}` (Codex App integration) - **Packaging**: `cargo tauri build` single command produces dmg/AppImage/deb/exe/msi; `xtask release-bundle` finalizes sha256 + RSA-3072 sig + latest.json + draft GitHub release ## Disclaimer -This project focuses on **OpenAI Codex CLI** integration; it is **not** an official OpenAI / Anthropic / Google / xAI project and does not reuse their trademarks / logos / release identities. +This project focuses on **OpenAI Codex App** integration; it is **not** an official OpenAI / Anthropic / Google / xAI project and does not reuse their trademarks / logos / release identities. -Upstream API keys / OAuth tokens are stored locally in `~/.codex-app-transfer/` (Unix 0600 + atomic write); the forwarding service only listens on `127.0.0.1` and does not hijack the system proxy. +Upstream API keys / OAuth tokens are stored locally in `~/.codex-app-transfer/` (Unix 0600 + atomic write); the forwarding service only listens on `127.0.0.1` and does not hijack the system proxy. Apart from the feedback feature, this tool performs no third-party network access. -Some experimental providers (Grok Web / Gemini CLI OAuth / Antigravity OAuth) involve upstream TOS gray areas — Grok Web reverse-proxies grok.com's Web backend, Gemini CLI OAuth uses the undocumented internal endpoint `cloudcode-pa.googleapis.com/v1internal` — strictly limited to **personal use**, **must not** be deployed as a public service, **users assume the risk**. +Some experimental providers (Grok Web / Gemini CLI OAuth / Antigravity OAuth) involve upstream TOS gray areas — Grok Web reverse-proxies grok.com's Web backend, Gemini CLI OAuth uses the undocumented internal endpoint `cloudcode-pa.googleapis.com/v1internal` — strictly limited to **personal use**, **must not** be deployed as a public service, **carries a real account-ban risk**, **users assume the risk**. ## Acknowledgements -> One-line summaries below. For the full **borrowing form / itemized list / corresponding file:line in this codebase**, see [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md). +> Overview list below. For the full **borrowing form / itemized list / corresponding file:line in this codebase**, see [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md). - [`farion1231/cc-switch`](https://github.com/farion1231/cc-switch) — provider switching paradigm inspiration - [`lonr-6/cc-desktop-switch`](https://github.com/lonr-6/cc-desktop-switch) — v1.x desktop shell skeleton + README structure reference - [`BerriAI/litellm`](https://github.com/BerriAI/litellm) — bidirectional protocol translation patterns - [`tauri-apps/tauri`](https://tauri.app/) — v2 + `cas://` architecture base -- [`openai/codex`](https://github.com/openai/codex) — autocompact prompt base structure + compact protocol reverse-reference (Apache-2.0) -- [`Piebald-AI/claude-code-system-prompts`](https://github.com/Piebald-AI/claude-code-system-prompts) — autocompact anchor bullets (All user messages verbatim + Next Step verbatim) +- [`openai/codex`](https://github.com/openai/codex) — autocompact prompt base structure + compact protocol reverse-reference +- [`Piebald-AI/claude-code-system-prompts`](https://github.com/Piebald-AI/claude-code-system-prompts) — autocompact anchor bullets - [`7as0nch/mimo2codex`](https://github.com/7as0nch/mimo2codex) — MiMo protocol reference - [`router-for-me/CLIProxyAPI`](https://github.com/router-for-me/CLIProxyAPI) — Gemini OAuth wire-level reference - [`chenyme/grok2api`](https://github.com/chenyme/grok2api) — Grok Web reverse-engineering reference + dynamic statsig algorithm + tool_calls flatten pattern diff --git a/README.md b/README.md index ad9a6a0..87574c9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # Codex App Transfer +> [!IMPORTANT] +> 🔴 **测试覆盖范围说明** +> +> 本项目当前**仅对 Kimi For Coding、Xiaomi MiMo(Token Plan)两家供应商完成了端到端真机实际测试**。 +> +> 其他已内置的 chat-completions 兼容供应商(包括 **DeepSeek、Kimi(月之暗面)、Xiaomi MiMo(Pay for Token)、智谱 GLM、阿里云百炼(API Key / Token Plan)、MiniMax**)**未做长期真机回归**,仅停留在单元测试 + 偶发用户反馈层面。 +> +> 如果你愿意**提供其他供应商的 API key 用于测试**,将万分感激!可通过 **QQ:`3216202644`** 或邮箱联系作者。作者保证 **API key 仅用于本项目实际测试**,测试结束后立即销毁,绝不挪作他用。 +

简体中文 | English | @@ -14,13 +23,11 @@ Downloads

-Codex App Transfer 是一个面向 **OpenAI Codex CLI** 的轻量桌面配置 + 转发工具。它在本机起一个网关,把 Codex CLI 发出的 Responses API 请求(HTTP 流式 / 非流式 + `/responses` 回退)翻译成 Chat Completions / Gemini Native / Anthropic Messages / Grok Web 等格式,转发到你选择的供应商。 - -跟 `farion1231/cc-switch` 这类偏 Claude Code 的 Anthropic 工具不同,本项目专注 **OpenAI Codex CLI** 的接入:用桌面 UI 管理供应商、模型映射、转发端口、日志面板,让 Codex CLI 无缝使用第三方 OpenAI / Gemini / Claude-compatible / Grok 等推理服务。 +Codex App Transfer 是一个面向 **OpenAI Codex APP** 的轻量桌面配置 + 转发工具。它在本机起一个网关,把 Codex APP 发出的 Responses API 请求(HTTP 流式 / 非流式 + `/responses` )翻译成 Chat Completions 等格式,转发到你选择的供应商,用桌面 UI 管理供应商、模型映射、转发端口、日志面板,让 Codex APP 无缝使用第三方 chat/completions 协议的推理服务。 -启动转发后,Codex CLI 通过本机 `127.0.0.1:18080` 与本工具通信。关闭窗口会缩到系统托盘继续运行,右键托盘"退出"才完全退出。 +启动转发后,Codex APP 通过本机 `127.0.0.1:18080` 与本工具通信。关闭窗口会缩到系统托盘继续运行,右键托盘"退出"才完全退出。 -当前版本 **v2.1.6**(详见 [Changelog](docs/CHANGELOG.md) 与 [Releases](https://github.com/Cmochance/codex-app-transfer/releases))。 +当前版本 **v2.1.12**(详见 [Changelog](docs/CHANGELOG.md) 与 [Releases](https://github.com/Cmochance/codex-app-transfer/releases))。 ## 界面预览 @@ -30,20 +37,20 @@ Codex App Transfer 是一个面向 **OpenAI Codex CLI** 的轻量桌面配置 + | **设置** | **日志** | | ![Settings](docs/img/Settings.png) | ![Logs](docs/img/Logs.png) | -### Codex CLI 实际接入 +### Codex APP 实际接入 -启用任意供应商后,Codex CLI 模型选择器会显示「 / 」形式的真实模型名,对话过程中工具循环 / `previous_response_id` 历史回放 / thinking 模式 reasoning_content 注入全部由本地代理透明处理: +启用任意供应商后,Codex APP 模型选择器会显示「 / 」形式的真实模型名,对话过程中工具循环 / `previous_response_id` 历史回放 / thinking 模式 reasoning_content 注入全部由本地代理透明处理: -![Codex CLI 实际对话](docs/img/codex-cli-real-chat.png) +![Codex APP 实际对话](docs/img/codex-cli-real-chat.png) ## 能做什么 - 管理多套供应商,按 OpenAI 模型名(`gpt-5.5` / `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.3-codex` / `gpt-5.2`)映射到供应商真实模型 ID -- 把 Codex CLI 的 Responses API 流式 / 非流式请求转换为上游协议:Chat Completions、Gemini Native(`:streamGenerateContent`)、Gemini CLI OAuth(Cloud Code Assist)、Anthropic Messages(`/v1/messages`)、Grok Web(`/rest/app-chat/conversations/new`)、Responses 透传等 +- 把 Codex APP 的 Responses API 流式 / 非流式请求转换为上游协议:Chat Completions、Gemini Native(`:streamGenerateContent`)、Gemini CLI OAuth(Cloud Code Assist)、Anthropic Messages(`/v1/messages`)、Grok Web(`/rest/app-chat/conversations/new`)、Responses 透传等 - 多轮工具对话上下文 + `previous_response_id` 历史回放 + autocompact 展开 + thinking / reasoning_content 注入全部对齐 OpenAI Responses API 协议 -- Codex CLI 的 freeform `apply_patch` 工具(编辑文件 +/- diff UI)在 DeepSeek / Kimi / MiMo 等 chat-completions provider 上正常工作:adapter 双向桥接 Responses `custom_tool_call` ↔ chat `function_call` 形态,模型按 V4A 格式生成 patch,Codex CLI 渲染为 diff(issue #235) +- Codex APP 的 freeform `apply_patch` 工具(编辑文件 +/- diff UI)在 DeepSeek / Kimi / MiMo 等 chat-completions provider 上正常工作:adapter 双向桥接 Responses `custom_tool_call` ↔ chat `function_call` 形态,模型按 V4A 格式生成 patch,Codex APP 渲染为 diff(issue #235) - 会话历史**两层持久化**:L1 内存 LRU + L2 sqlite 30 天 TTL(`~/.codex-app-transfer/sessions.db`),`.app` 重启不丢历史 -- Codex CLI 原配置守护:apply 前自动快照 `~/.codex/{config.toml,auth.json}`,退出 / 下次启动按 key 智能合并还原 +- Codex APP 原配置守护:apply 前自动快照 `~/.codex/{config.toml,auth.json}`,退出 / 下次启动按 key 智能合并还原 - 实时日志面板,2 秒自动刷新;统一 `tracing::warn!(error_id, detail)` + 稳定 token,operator 可 grep / 聚合 - 反馈弹窗附带诊断材料(环境信息、脱敏配置、最近错误快照及完整请求 / 响应),减少手工补材料 - 中文 / 英文界面,浅色 / 深色 / 绿色 / 橙色 / 灰色 / 白色多种主题 @@ -68,17 +75,13 @@ Codex-App-Transfer-v<版本>-Linux-x86_64.AppImage 通用 Linux x86_64,`ch 每个二进制都附带 `.sha256` 与 `.sig`(RSA-3072 PKCS#1 v1.5 + SHA-256 签名);公钥 `Codex-App-Transfer-release-public.pem` 跟随每个 Release 一起发布,直接从 [Releases](https://github.com/Cmochance/codex-app-transfer/releases) 下载即可验签。 Windows 暂未做 Authenticode 代码签名,系统可能提示未知发布者,可用 `.sha256` / `.sig` 校验下载完整性。 +macOS 暂未做 **Apple Developer ID 代码签名** 与 **Apple 公证(Notarization)**,首次打开会被 Gatekeeper 拦截,提示「无法打开,因为它来自身份不明的开发者」。绕过方式:`右键 → 打开` 一次性放行;或用 `.sha256` / `.sig` 校验下载完整性后,在 `系统设置 → 隐私与安全性` 点「仍要打开」。 ## 快速开始 1. 启动 Codex App Transfer,弹出桌面窗口 -2. 在仪表盘点右上角加号 → 选择 preset 或自定义供应商,填入 API Base URL、API Key、模型映射 -3. 在"转发"页面点"启动转发",本机 `18080` 端口开始监听 -4. 在 Codex CLI 配置文件(`~/.codex/config.toml`)里把 `base_url` 指向 `http://127.0.0.1:18080`,把 API Key 设为本工具显示的 Gateway API Key -5. 重新打开 Codex CLI,模型选项就会自动列出当前供应商的模型映射 -6. ⚠️ **必须把 Codex CLI 切到 Full access**(`/approvals` → "Full access"):第三方 provider 在 Codex CLI 默认 `auto` 审批模式下,工具调用会卡审批弹窗;Full access 直接放行工具调用,这是接入第三方 provider 的**事实必要前提** - -桌面窗口无法打开时(罕见,通常是 Tauri webview 初始化失败 / 系统 webview 缺失),先尝试重启;若仍异常,从 [Releases](https://github.com/Cmochance/codex-app-transfer/releases) 重新下载并查看 `~/.codex-app-transfer/logs/proxy-*.log`,或开 [Issue](https://github.com/Cmochance/codex-app-transfer/issues) 反馈。v2 架构无独立 HTTP admin UI(管理面板走 Tauri 同进程 `cas://`,**不再监听 18081 端口**)。 +2. 在仪表盘点右上角加号 → 选择 preset 或自定义供应商,填入 API Base URL、API Key、获取模型、添加模型映射 +3. 点击页面底部的 应用 按钮并确认重启 Codex APP 即可使用(如果已配置好提供商,直接点击主页面提供商卡片上的 应用 按钮即可) ## 供应商兼容矩阵 @@ -98,9 +101,9 @@ Windows 暂未做 Authenticode 代码签名,系统可能提示未知发布者, ## 模型映射 -Codex CLI 按 OpenAI 模型名提示;第三方 provider 用 `deepseek-v4-pro` / `kimi-k2.6` / `glm-5.1` / `gemini-3-pro` 等真实 ID。 +Codex APP 按 OpenAI 模型名提示;第三方 provider 用 `deepseek-v4-pro` / `kimi-k2.6` / `glm-5.1` / `gemini-3-pro` 等真实 ID。 -本工具用 `provider.models[slot]`(`gpt-5.5` → `deepseek-v4-pro` 等)做槽位映射,Codex CLI 模型选择器看到 ` / ` 形式真实模型名;上游 `chatcmpl-...` 应答 ID 自动重写为 Codex CLI 校验通过的 `resp_`,保留 deployment affinity 编码,`previous_response_id` 跨轮一致。 +本工具用 `provider.models[slot]`(`gpt-5.5` → `deepseek-v4-pro` 等)做槽位映射,Codex APP 模型选择器看到 ` / ` 形式真实模型名;上游 `chatcmpl-...` 应答 ID 自动重写为 Codex APP 校验通过的 `resp_`,保留 deployment affinity 编码,`previous_response_id` 跨轮一致。 ## 本地开发(v2 / Rust) @@ -155,19 +158,17 @@ start frontend/gallery.html # Windows ### Codex 模型不能用 curl 等联网命令 / 弹审批弹窗 -本应用默认在 apply 时把 `sandbox_mode = "danger-full-access"` + `approval_policy = "never"` 同时写入 `~/.codex/config.toml`(Codex 官方推荐的 **"Full access" 配对**,跨平台真正无审批弹窗联网),小白用户开箱即用。可在 设置 → "允许 Codex 联网工具(全权限模式)" 开关里关闭(#215)。 - -> **⚠️ 安全权衡**:full-access 模式模型可读写任何文件 + 所有命令无审批 = **完全信任模型**(等同 Codex 官方文档的 "Full access" 档位)。toggle off 后 Codex 回 read-only 沙箱 + on-request 审批,无网络,仅能用所选模型自带的 `web_search` 能力;若模型不支持 web_search 则完全无法联网搜索。 +curl 命令需要高级权限,目前第三方模型在macOS端无法触发提权选择,因此本应用默认在 apply 时把 `sandbox_mode = "danger-full-access"` + `approval_policy = "never"` 同时写入 `~/.codex/config.toml`,若在Windows端使用或是有其他原因可在 设置 → "允许 Codex 联网工具(全权限模式)" 开关里关闭(#215)。 -v2.1.12 之前尝试用 `workspace-write` + `network_access = true` 路径,但 macOS [seatbelt bug #10390](https://github.com/openai/codex/issues/10390) 跟 Windows `is_safe_command()` 仍触发审批弹窗,都不彻底。#215 改用 Codex 官方 "Full access" 配对作为 toggle on 的语义。 +> **⚠️ 安全权衡**:full-access 模式模型可读写任何文件 + 所有命令无审批 = **完全信任模型**(等同 Codex 官方文档的 "Full access" 档位)。toggle off 后 Codex 回 read-only 沙箱 + on-request 审批。macOS目前无法触发提权选择,无网络,仅能用所选模型自带的 `web_search` 能力;若模型不支持 web_search 则所有搜索操作只会返回空内容。 -### Codex CLI 提示 `404 Not Found url: http://127.0.0.1:18080/responses` +### Codex APP 提示 `404 Not Found url: http://127.0.0.1:18080/responses` 老版本只有 `/v1/responses`,Codex CLI 0.126 起回退到 `/responses`(不带 `/v1/`)。本工具已加路由别名,更新到 v1.0.1+ 即可。 -### Codex CLI 提示 `stream disconnected before completion` +### Codex APP 提示 `stream disconnected before completion` -通常是 `response.id` / `response.model` 没按 Codex CLI 期望填回。本工具把上游 `chatcmpl-...` 重写成 `resp_` 并保留请求模型名,请确认转发日志确实看到 `resp_...` 而不是 `chatcmpl-...`。 +通常是 `response.id` / `response.model` 没按 Codex APP 期望填回。本工具把上游 `chatcmpl-...` 重写成 `resp_` 并保留请求模型名,请确认转发日志确实看到 `resp_...` 而不是 `chatcmpl-...`。 ### 上游 400:`thinking is enabled but reasoning_content is missing` @@ -202,7 +203,7 @@ v2.1.12+ 的客户端 **强制** RSA-3072 PKCS#1-v1.5-SHA256 验签 `latest.json 设计意图: 客户端只信"build-time 嵌入的公钥"产生的签名,运行时不可替换公钥,防 MITM 改 `latest.json` 推任意 installer (公钥 PEM 已在 release/ 目录,但若让客户端动态从 update URL 旁边拉公钥就破坏 trust anchor)。 -### 日志去哪了 +### 日志 - 应用界面:转发页面下方实时面板,2 秒自动刷新 - 磁盘文件:`~/.codex-app-transfer/logs/proxy-YYYY-MM-DD.log`,点"查看日志"按钮直接打开 @@ -214,34 +215,34 @@ v2.1.12+ 的客户端 **强制** RSA-3072 PKCS#1-v1.5-SHA256 验签 `latest.json - **协议适配**:`crates/adapters/` — Responses ↔ Chat / Gemini Native / Gemini CLI OAuth / Anthropic Messages / Grok Web 互转(请求 body + 流式响应状态机 + reasoning_content + tool_calls) - **前端**:HTML + CSS + 原生 JavaScript + Bootstrap 5.3.3(本地化,无 CDN 依赖) - **桌面壳**:Tauri 2 + tray-icon 0.23,通过 `cas://` URI scheme 把 frontend/ 与 axum 同进程串起来,无 TCP loopback -- **存储**:`~/.codex-app-transfer/config.json`(配置,与 v1.x 互通)、`~/.codex-app-transfer/sessions.db`(L2 sqlite 会话持久化)、`~/.codex/{config.toml,auth.json}`(Codex CLI 集成) +- **存储**:`~/.codex-app-transfer/config.json`(配置,与 v1.x 互通)、`~/.codex-app-transfer/sessions.db`(L2 sqlite 会话持久化)、`~/.codex/{config.toml,auth.json}`(Codex APP 集成) - **打包**:`cargo tauri build` 单命令出 dmg/AppImage/deb/exe/msi;`xtask release-bundle` 收口出 sha256 + RSA-3072 sig + latest.json + draft GitHub release ## 免责声明 -本项目专注 **OpenAI Codex CLI** 接入,**不是** OpenAI / Anthropic / Google / xAI 的官方项目,也不复用其商标 / Logo / 发布身份。 +本项目专注 **OpenAI Codex APP** 接入,**不是** OpenAI / Anthropic / Google / xAI 的官方项目,也不复用其商标 / Logo / 发布身份。 -上游 API key / OAuth token 仅保存在本机 `~/.codex-app-transfer/`(Unix 0600 + atomic write);转发服务只监听 `127.0.0.1`,不接管系统代理。 +上游 API key / OAuth token 仅保存在本机 `~/.codex-app-transfer/`(Unix 0600 + atomic write);转发服务只监听 `127.0.0.1`,不接管系统代理,除反馈功能外不涉及第三方联网行为。 -部分实验性 provider(Grok Web / Gemini CLI OAuth / Antigravity OAuth)涉及上游 TOS 灰色地带 — Grok Web 反代 grok.com Web 后端协议、Gemini CLI OAuth 借用 `cloudcode-pa.googleapis.com/v1internal` 内部端点 — 严格限定**个人使用**,**不应**作为对外服务发布,**用户自担风险**。 +部分实验性 provider(Grok Web / Gemini CLI OAuth / Antigravity OAuth)涉及上游 TOS 灰色地带 — Grok Web 反代 grok.com Web 后端协议、Gemini CLI OAuth 借用 `cloudcode-pa.googleapis.com/v1internal` 内部端点 — 严格限定**个人使用**,**不应**作为对外服务发布,且存在封号风险,**用户自担风险**。 ## 致谢 -> 以下列表为概览(每条一句话)。**完整借鉴形式 / 借鉴清单 / 本项目对应 file:line** 见 [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md)。 +> 以下列表为概览。**完整借鉴形式 / 借鉴清单 / 本项目对应 file:line** 见 [ACKNOWLEDGEMENTS.md](./ACKNOWLEDGEMENTS.md)。 - [`farion1231/cc-switch`](https://github.com/farion1231/cc-switch) — provider 切换形态启发 - [`lonr-6/cc-desktop-switch`](https://github.com/lonr-6/cc-desktop-switch) — v1.x 桌面壳骨架 + README 结构参考 - [`BerriAI/litellm`](https://github.com/BerriAI/litellm) — 协议双向转换思路 - [`tauri-apps/tauri`](https://tauri.app/) — v2 + `cas://` 架构基座 -- [`openai/codex`](https://github.com/openai/codex) — autocompact prompt 骨架 + compact 协议结构反查(Apache-2.0) -- [`Piebald-AI/claude-code-system-prompts`](https://github.com/Piebald-AI/claude-code-system-prompts) — autocompact prompt 锚定 bullet(All user messages verbatim + Next Step verbatim) +- [`openai/codex`](https://github.com/openai/codex) — autocompact prompt 骨架 + compact 协议结构反查 +- [`Piebald-AI/claude-code-system-prompts`](https://github.com/Piebald-AI/claude-code-system-prompts) — autocompact prompt 锚定 bullet - [`7as0nch/mimo2codex`](https://github.com/7as0nch/mimo2codex) — MiMo 协议借鉴 - [`router-for-me/CLIProxyAPI`](https://github.com/router-for-me/CLIProxyAPI) — Gemini OAuth wire 参考 - [`chenyme/grok2api`](https://github.com/chenyme/grok2api) — Grok Web 反向工程参考 + dynamic statsig 算法 + tool_calls flatten 模式 -- [`galaxywk223/codex-plugin-unlocker`](https://github.com/galaxywk223/codex-plugin-unlocker) — Codex Desktop Plugins 解锁注入脚本(React Context-value 反查 + DOM enable + MutationObserver,MIT) -- [`QwenLM/qwen-code`](https://github.com/QwenLM/qwen-code) — 阿里官方 Qwen CLI,百炼 Token Plan (`*.maas.aliyuncs.com`) 模型清单硬编码模式(`packages/cli/src/auth/providers/alibaba/tokenPlan.ts` 的 `TOKEN_PLAN_MODELS`,Apache-2.0) -- [`BigPizzaV3/CodexPlusPlus`](https://github.com/BigPizzaV3/CodexPlusPlus) — Windows MSIX Codex Desktop CDP 注入路径(`IApplicationActivationManager` COM + AUMID 自动解析 + cmdline 序列化,`codex_session_delete/launcher.py`,MIT) -- [`borawong/AiMaMi`](https://github.com/borawong/AiMaMi) — Codex 资产管理"受管块"设计:marker + parse/preview/apply/rollback/clear/history 六操作 + Protected 模式(`src-tauri/src/core/custom_instructions.rs:1-130`,MIT)— 本项目 `src-tauri/src/admin/services/managed_block.rs` 借鉴算法,marker prefix 改 `cas:` 项目独立 +- [`galaxywk223/codex-plugin-unlocker`](https://github.com/galaxywk223/codex-plugin-unlocker) — Codex Desktop Plugins 解锁注入脚本 +- [`QwenLM/qwen-code`](https://github.com/QwenLM/qwen-code) — 阿里官方 Qwen CLI,百炼 Token Plan (`*.maas.aliyuncs.com`) 模型清单硬编码模式 +- [`BigPizzaV3/CodexPlusPlus`](https://github.com/BigPizzaV3/CodexPlusPlus) — Windows MSIX Codex Desktop CDP 注入路径 +- [`borawong/AiMaMi`](https://github.com/borawong/AiMaMi) — Codex 资产管理"受管块"设计:Agents.md, MCP, Skills ### 社区贡献者 From cc49ad708cc7fd67573d2714f3f47eae989fe336 Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Fri, 22 May 2026 01:02:35 +0800 Subject: [PATCH 4/4] revert(proxy): remove apply_patch debug capture instrumentation (post-merge cleanup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 7-9 真机 capture 调研已完成,PR 已收敛全部 prompt 微修。 按 memory rule"debug 代码不进 main",在 merge 前 revert forward.rs 回 origin/main 状态。 被 revert 的 commit:6080558 (debug(proxy): inject apply_patch I/O capture instrumentation) - 104 行 DEBUG_APPLY_PATCH_SEQ / debug_apply_patch_dump / DebugTeeStream - 4 个 stage dump 调用(inbound / outbound / upstream_raw / downstream_emit) PR #240 现在剩下的真业务改动: - 0d2306a: prompt 修 Move 空 hunk + Begin Patch first-line (round 7 Kimi 实证) - 6d1b9d1: README 用户定稿同步 + 顶部测试覆盖红字 + macOS 签名提示 510 tests pass + cargo check -p codex-app-transfer-proxy 干净。 Refs #235 --- crates/proxy/src/forward.rs | 106 +----------------------------------- 1 file changed, 2 insertions(+), 104 deletions(-) diff --git a/crates/proxy/src/forward.rs b/crates/proxy/src/forward.rs index 7accac1..6c94ad9 100644 --- a/crates/proxy/src/forward.rs +++ b/crates/proxy/src/forward.rs @@ -35,92 +35,6 @@ use crate::diagnostics::{write_upstream_error_bundle, UpstreamErrorBundleInput}; use crate::resolver::{AuthScheme, ResolveError, ResolvedProvider, SharedResolver}; use crate::telemetry::proxy_telemetry; -// ============================================================================= -// !!! DEBUG ONLY — DO NOT MERGE !!! -// ============================================================================= -// 临时 instrumentation: 抓 apply_patch 全链路 I/O 到 -// ~/.codex-app-transfer/logs/apply-patch-debug/--.txt -// 4 stage: inbound / outbound / upstream_raw / downstream_emit -// 完整规则同 issue #235 debug worktree。本 PR 仅用于回归测试 + 多 provider -// 验证,**绝不 merge**。 -use std::sync::atomic::{AtomicU64, Ordering}; - -static DEBUG_APPLY_PATCH_SEQ: AtomicU64 = AtomicU64::new(0); - -fn debug_apply_patch_dir() -> Option { - let home = std::env::var("HOME").ok()?; - let dir = std::path::PathBuf::from(home).join(".codex-app-transfer/logs/apply-patch-debug"); - std::fs::create_dir_all(&dir).ok()?; - Some(dir) -} - -fn debug_apply_patch_dump(stage: &str, request_id: u64, payload: &[u8]) { - let Some(dir) = debug_apply_patch_dir() else { - return; - }; - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0); - let path = dir.join(format!("{ts:013}-{request_id:04}-{stage}.txt")); - if let Ok(mut f) = std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&path) - { - use std::io::Write as _; - let _ = f.write_all(payload); - } -} - -struct DebugTeeStream { - inner: codex_app_transfer_adapters::ByteStream, - request_id: u64, - stage: &'static str, - accumulator: Vec, -} - -impl DebugTeeStream { - fn new( - inner: codex_app_transfer_adapters::ByteStream, - request_id: u64, - stage: &'static str, - ) -> Self { - Self { - inner, - request_id, - stage, - accumulator: Vec::new(), - } - } -} - -impl Stream for DebugTeeStream { - type Item = Result; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.as_mut().get_mut(); - match this.inner.as_mut().poll_next(cx) { - Poll::Ready(Some(Ok(chunk))) => { - this.accumulator.extend_from_slice(&chunk); - Poll::Ready(Some(Ok(chunk))) - } - other => other, - } - } -} - -impl Drop for DebugTeeStream { - fn drop(&mut self) { - if !self.accumulator.is_empty() { - debug_apply_patch_dump(self.stage, self.request_id, &self.accumulator); - } - } -} -// ============================================================================= -// !!! END DEBUG ONLY !!! -// ============================================================================= - #[derive(Clone)] pub struct ProxyState { pub http: reqwest::Client, @@ -393,10 +307,6 @@ pub async fn forward_handler( // 1. 收齐入站 body let mut body_bytes: Bytes = axum::body::to_bytes(body, usize::MAX).await?; - // !!! DEBUG ONLY — DO NOT MERGE (post-merge regression test) !!! - let debug_request_id = DEBUG_APPLY_PATCH_SEQ.fetch_add(1, Ordering::Relaxed); - debug_apply_patch_dump("inbound", debug_request_id, &body_bytes); - // 2. 解析(鉴权 + 路由) let client_path = parts .uri @@ -430,9 +340,6 @@ pub async fn forward_handler( let original_body_bytes_for_retry = body_bytes.clone(); let mut plan = adapter.prepare_request(&client_path, body_bytes, &resolved.provider)?; - // !!! DEBUG ONLY — DO NOT MERGE (post-merge regression test) !!! - debug_apply_patch_dump("outbound", debug_request_id, &plan.body); - // 5. 拼上游 URL —— base 末尾去 `/`,plan.upstream_path 必含 `/` let upstream_url = build_upstream_url(&resolved.upstream_base, &plan.upstream_path); let telemetry = proxy_telemetry(); @@ -600,11 +507,8 @@ pub async fn forward_handler( resp.bytes_stream() .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)), ); - // !!! DEBUG ONLY — DO NOT MERGE !!! - let raw_tee: codex_app_transfer_adapters::ByteStream = - Box::pin(DebugTeeStream::new(raw, debug_request_id, "upstream_raw")); Box::pin(TracedStream::new( - raw_tee, + raw, t_send, st.as_u16(), upstream_url.clone(), @@ -650,19 +554,13 @@ pub async fn forward_handler( (st, hs, stream) }; - let mut response_plan = adapter.transform_response_stream( + let response_plan = adapter.transform_response_stream( status, upstream_headers, upstream_stream, &resolved.provider, &plan, )?; - // !!! DEBUG ONLY — DO NOT MERGE !!! - response_plan.stream = Box::pin(DebugTeeStream::new( - response_plan.stream, - debug_request_id, - "downstream_emit", - )); let success = response_plan.status.is_success(); telemetry.stats.record(success); telemetry.logs.add(