codex 迁移会话 换成自定义API后找不到原会话 多会话 解决方案

描述:之前使用账号直登的会话现在使用提供商的API之后,会话没了,重新认证又会出现

真实保存在本地,但是没有识别,这是因为

Codex 左侧会话列表不是直接扫 sessions 文件夹来显示的,而是先读 state_5.sqlite 里的 threads 表,再按当前客户端状态做过滤。你这里至少命中了两个过滤条件:

 

1、按当前 model_provider 分桶

你把配置从 openai 改成了别的 provider 之后,左侧更倾向于只显示“当前 provider 对应的线程”。所以旧的 openai 历史虽然还在,但不会自动出现在当前列表里。

 

 

2、按当前工作区 cwd 过滤

当前 provider 下本来只有少量线程,但这些线程在数据库里被写成了 \\?\D:\work\Paper 这种路径,而不是普通的 D:\work\Paper。这样一来,左侧再按工作区筛选时,就会把它们也排除掉,于是看起来像“全没了”。

让ai写了个批处理脚本,这俩文件放在一起,

执行流程:

1、保存俩文件到目录

2、修改python文件的codex目录,只替换自己的电脑名就行,codex默认安装在用户目录下

3、关闭codex进程,包括VScode等IDE codex插件的运行

4、cmd运行.bat 脚本,或者双击.bat

最好在cmd里面运行,这样可以看到报错信息,我下面有运行截图

5、重新打开codex就发现可以了

脚本会先备份当前的配置,不用担心丢失

 2026年6月7日更新 转移多个文件下的会话。感谢*荣、*兄两位老师的投喂!

from __future__ import annotations

import datetime as _dt
import json
import shutil
import sqlite3
import sys
from pathlib import Path

# ==========================
# 配置区 按照文件目录一行一个,python执行,注意要把所有codex进程退出,才能执行
# 聪明的你想必已经发现了修改TARGET_PROVIDER成config.toml里面的model_provider就能回去了!也就是可以自由切换 # ========================== DEFAULT_CODEX_HOME = Path(r"C:\Users\***\.codex") TARGET_PROVIDER = "openai" WORKSPACES = [ r"D:\work\cuda", r"D:\work\Go" ] # ========================== def now_tag() -> str: return _dt.datetime.now().strftime("%Y%m%d-%H%M%S") def backup_paths(paths: list[Path], backup_dir: Path) -> None: backup_dir.mkdir(parents=True, exist_ok=True) for path in paths: if path.exists(): dst = backup_dir / path.name if not dst.exists(): shutil.copy2(path, dst) def normalize_cwd(cwd: str) -> str: if cwd.startswith("\\\\?\\"): return cwd[4:] return cwd def find_rollout(codex_home: Path, thread_id: str) -> Path | None: sessions = codex_home / "sessions" for p in sessions.rglob(f"*{thread_id}*.jsonl"): return p return None def patch_rollout( rollout: Path, target_provider: str, target_cwds: list[str], ) -> bool: changed = False out_lines = [] target_cwd = target_cwds[0] for line in rollout.read_text( encoding="utf-8", errors="ignore", ).splitlines(): original = line try: obj = json.loads(line) except Exception: out_lines.append(original) continue if obj.get("type") == "session_meta": payload = obj.get("payload") or {} if payload.get("model_provider") != target_provider: payload["model_provider"] = target_provider changed = True current_cwd = normalize_cwd( payload.get("cwd", "") ) if current_cwd in target_cwds: payload["cwd"] = current_cwd else: payload["cwd"] = target_cwd changed = True obj["payload"] = payload line = json.dumps( obj, ensure_ascii=False, ) out_lines.append(line) if changed: rollout.write_text( "\n".join(out_lines) + "\n", encoding="utf-8", ) return changed def update_global_state( codex_home: Path, roots: list[str], ) -> bool: path = codex_home / ".codex-global-state.json" if not path.exists(): return False try: data = json.loads( path.read_text( encoding="utf-8", errors="ignore", ) ) except Exception: return False changed = False if ( isinstance( data.get("selected-remote-host-id"), str, ) and data["selected-remote-host-id"].startswith( "remote-ssh-discovered:" ) ): data["selected-remote-host-id"] = None changed = True old_roots = data.get( "active-workspace-roots", [], ) merged_roots = list( dict.fromkeys( old_roots + roots ) ) if data.get("active-workspace-roots") != merged_roots: data["active-workspace-roots"] = merged_roots changed = True if ( data.get( "electron-saved-workspace-roots" ) != merged_roots ): data[ "electron-saved-workspace-roots" ] = merged_roots changed = True if changed: path.write_text( json.dumps( data, ensure_ascii=False, separators=(",", ":"), ), encoding="utf-8", ) return changed def main(): codex_home = DEFAULT_CODEX_HOME db = codex_home / "state_5.sqlite" global_state = ( codex_home / ".codex-global-state.json" ) conn = sqlite3.connect(str(db)) cur = conn.cursor() placeholders = ",".join( "?" * len(WORKSPACES) ) query = f""" select id, model_provider, cwd, title from threads where replace(cwd,'\\\\?\\','') in ({placeholders}) order by updated_at desc """ rows = cur.execute( query, tuple(WORKSPACES), ).fetchall() if not rows: conn.close() print( "No matching threads found." ) return 1 backup_dir = ( codex_home / f"promote-workspace-backup-{now_tag()}" ) backup_paths( [db, global_state], backup_dir, ) db_changed = 0 rollout_changed = 0 touched = 0 for ( thread_id, model_provider, cwd, title, ) in rows: rollout = find_rollout( codex_home, thread_id, ) if rollout is None: continue backup_paths( [rollout], backup_dir, ) if patch_rollout( rollout, TARGET_PROVIDER, WORKSPACES, ): rollout_changed += 1 current_cwd = normalize_cwd(cwd) if ( model_provider != TARGET_PROVIDER ): cur.execute( """ update threads set model_provider=?, has_user_event=1 where id=? """, ( TARGET_PROVIDER, thread_id, ), ) db_changed += 1 touched += 1 print( f"[OK] {title[:60]}" ) conn.commit() conn.close() global_changed = update_global_state( codex_home, WORKSPACES, ) print() print("=" * 60) print( f"Backup folder : {backup_dir}" ) print( f"Provider : {TARGET_PROVIDER}" ) print( f"Workspaces : {len(WORKSPACES)}" ) print( f"Threads found : {len(rows)}" ) print( f"Threads touch : {touched}" ) print( f"Rollouts edit : {rollout_changed}" ) print( f"DB changed : {db_changed}" ) print( f"Global state : {'yes' if global_changed else 'no'}" ) print("=" * 60) print() print( "Completely exit Codex first." ) print( "Restart Codex after script finishes." ) return 0 if __name__ == "__main__": sys.exit(main())

 

原来方案如下,只能迁移单文件  

codex_promote_workspace_threads.bat

@echo off
setlocal

cd /d "%~dp0"

echo [0/2] Checking whether Codex is still running...
tasklist /FI "IMAGENAME eq codex.exe" | find /I "codex.exe" >NUL
if not errorlevel 1 (
  echo Codex is still running. Please quit Codex completely and run this script again.
  exit /b 1
)

echo [1/2] Promoting all workspace threads into the current provider bucket...
python "%~dp0codex_promote_workspace_threads.py"
if errorlevel 1 exit /b 1

echo [2/2] Done.
echo Start Codex again and check the left sidebar.
exit /b 0

  

codex_promote_workspace_threads.py 把标黄的替换为你自己的目录

from __future__ import annotations

import argparse
import datetime as _dt
import json
import shutil
import sqlite3
import sys
from pathlib import Path


DEFAULT_CODEX_HOME = Path(r"C:\Users\*\.codex")
DEFAULT_TARGET_PROVIDER = "chaosai"
DEFAULT_CWD = r"D:\work\Paper"


def now_tag() -> str:
    return _dt.datetime.now().strftime("%Y%m%d-%H%M%S")


def backup_paths(paths: list[Path], backup_dir: Path) -> None:
    backup_dir.mkdir(parents=True, exist_ok=True)
    for path in paths:
        if path.exists():
            dst = backup_dir / path.name
            if not dst.exists():
                shutil.copy2(path, dst)


def normalize_cwd(cwd: str) -> str:
    if cwd.startswith("\\\\?\\"):
        return cwd[4:]
    return cwd


def find_rollout(codex_home: Path, thread_id: str) -> Path | None:
    sessions = codex_home / "sessions"
    for p in sessions.rglob(f"*{thread_id}*.jsonl"):
        return p
    return None


def patch_rollout(rollout: Path, target_provider: str, target_cwd: str) -> bool:
    changed = False
    out_lines: list[str] = []

    for line in rollout.read_text(encoding="utf-8", errors="ignore").splitlines():
        original = line
        try:
            obj = json.loads(line)
        except Exception:
            out_lines.append(original)
            continue

        if obj.get("type") == "session_meta":
            payload = obj.get("payload") or {}
            if payload.get("model_provider") != target_provider:
                payload["model_provider"] = target_provider
                changed = True
            if payload.get("cwd") != target_cwd:
                payload["cwd"] = target_cwd
                changed = True
            obj["payload"] = payload
            line = json.dumps(obj, ensure_ascii=False)

        out_lines.append(line)

    if changed:
        rollout.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
    return changed


def clear_remote_selection(codex_home: Path, target_cwd: str) -> bool:
    path = codex_home / ".codex-global-state.json"
    if not path.exists():
        return False

    try:
        data = json.loads(path.read_text(encoding="utf-8", errors="ignore"))
    except Exception:
        return False

    changed = False

    if isinstance(data.get("selected-remote-host-id"), str) and data["selected-remote-host-id"].startswith(
        "remote-ssh-discovered:"
    ):
        data["selected-remote-host-id"] = None
        changed = True

    roots = [target_cwd]
    if data.get("active-workspace-roots") != roots:
        data["active-workspace-roots"] = roots
        changed = True
    if data.get("electron-saved-workspace-roots") != roots:
        data["electron-saved-workspace-roots"] = roots
        changed = True

    if changed:
        path.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
    return changed


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--codex-home", default=str(DEFAULT_CODEX_HOME))
    parser.add_argument("--target-provider", default=DEFAULT_TARGET_PROVIDER)
    parser.add_argument("--cwd", default=DEFAULT_CWD)
    parser.add_argument("--limit", type=int, default=0)
    args = parser.parse_args()

    codex_home = Path(args.codex_home)
    db = codex_home / "state_5.sqlite"
    global_state = codex_home / ".codex-global-state.json"

    conn = sqlite3.connect(str(db))
    cur = conn.cursor()
    rows = cur.execute(
        """
        select id, model_provider, cwd, title
        from threads
        where replace(cwd, '\\\\?\\', '') = ?
        order by updated_at desc
        """,
        (args.cwd,),
    ).fetchall()

    if args.limit > 0:
        rows = rows[: args.limit]

    if not rows:
        conn.close()
        print("No matching threads found.")
        return 1

    backup_dir = codex_home / f"promote-workspace-backup-{now_tag()}"
    backup_paths([db, global_state], backup_dir)

    db_changed = 0
    rollout_changed = 0
    touched = 0

    for thread_id, model_provider, cwd, _title in rows:
        rollout = find_rollout(codex_home, thread_id)
        if rollout is None:
            continue

        backup_paths([rollout], backup_dir)

        if patch_rollout(rollout, args.target_provider, args.cwd):
            rollout_changed += 1

        new_cwd = args.cwd
        new_provider = args.target_provider
        if model_provider != new_provider or cwd != new_cwd:
            cur.execute(
                """
                update threads
                set model_provider = ?,
                    cwd = ?,
                    has_user_event = 1
                where id = ?
                """,
                (new_provider, new_cwd, thread_id),
            )
            db_changed += 1

        touched += 1

    conn.commit()
    conn.close()

    global_changed = clear_remote_selection(codex_home, args.cwd)

    print(f"Backup folder: {backup_dir}")
    print(f"Workspace cwd: {args.cwd}")
    print(f"Target provider: {args.target_provider}")
    print(f"Threads matched: {len(rows)}")
    print(f"Threads touched: {touched}")
    print(f"Rollouts changed: {rollout_changed}")
    print(f"DB rows changed: {db_changed}")
    print(f"Global state changed: {'yes' if global_changed else 'no'}")
    print("Fully exit Codex before running. Restart Codex after this script finishes.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

 

image

 运行之后,再打开codex就可以了

 

附一个配置提供商API的配置格式

目录:C:\Users\yangxuezhen\.codex

auth.json:写提供商的key

{
"OPENAI_API_KEY": "sk-18a9f0a**7cc06"
}

  

config.toml:

model_provider = "chaosai"
model = "gpt-5.2"
model_reasoning_effort = "medium"
[windows]
sandbox = "elevated"

[model_providers.chaosai]
name = "chaosai"
base_url = "https://*/openai-codex-oauth/v1"
wire_api = "responses"
requires_openai_auth = false
http_headers = { Authorization = "Bearer sk-18a***a7cc06" }

multi_agent = true

  

完毕!

如果对你真有帮助可以赞赏下,我也是搞了半天才弄出来,一两块就行,感谢!

24abab91e9168b6f5d0a0d9290f5bbc6

 

posted @ 2026-04-15 14:11  风起时的悟  阅读(2105)  评论(0)    收藏  举报