# 从 Claude Code、Codex 与 OpenCode 迁移

> 三种外部会话怎样读进 MiMo 并按修改时间增量重导，“从 Claude Code 导入”搬走哪些凭据，以及哪些 Claude Code 资产是直接沿用的。

- 作者：David（道雾轩）
- 专栏：MiMo Code 源码解读（https://daiw.org/manual/mimo-code.md）
- 最后更新：2026-09-17
- 原文：https://daiw.org/manual/mimo-code/imports
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

从别的编码 Agent 换到 MiMo，要带走的东西分两类。一类是历史会话：Claude Code、Codex、OpenCode 各有自己的本地存储，MiMo 把它们读进自己的 SQLite，在会话列表里和原生会话并排出现。另一类是配置与凭据：API key、`CLAUDE.md`、自定义命令、MCP 服务器、技能。前者靠三个导入器，后者大部分不导入，而是每次启动时直接去读原来的位置。

## 怎么用

| 来源 | 读取位置 | 何时导入 | 入口 |
| --- | --- | --- | --- |
| Claude Code | `~/.claude/projects/*/*.jsonl` | 每次启动 CLI 自动执行 | `mimo session import-claude [--force]` |
| Codex | `~/.codex/sessions/**/*.jsonl` | 只能经 HTTP 接口触发 | `POST /global/import/run` |
| OpenCode | `$XDG_DATA_HOME/opencode/opencode.db`，未设置时为 `~/.local/share/opencode/opencode.db` | 只能经 HTTP 接口触发 | 同上 |

- 自动导入可以用环境变量 `MIMOCODE_DISABLE_CLAUDE_IMPORT` 关掉，设成任何非空值都算。`MIMOCODE_MIMO_ONLY` 管不到它。
- `GET /global/import/scan` 只读地统计三个来源是否存在、各有多少会话、已导入多少；`POST /global/import/run` 的请求体可带 `sources`（`cc`、`codex`、`opencode` 的子集）与 `force`，某个来源失败时错误写进该来源的 `errors`，不影响其他来源（`packages/opencode/src/server/routes/global.ts:298`）。仓库里除了生成的 SDK 方法，没有找到调用这两个接口的前端代码。
- 凭据迁移在 TUI 里：`/login`（或 `/connect`）对话框的“从 Claude Code 导入”。

启动时的自动导入写在 CLI 的全局中间件里，紧跟数据库迁移之后（`packages/opencode/src/index.ts:160`）：

```ts
// Idempotently import Claude Code sessions into SQLite. Runs once per process
// tree (the env guard is inherited by spawned children) and is best-effort:
// a failure here must never block command startup.
if (!process.env.MIMOCODE_DISABLE_CLAUDE_IMPORT && !process.env.MIMOCODE_CLAUDE_IMPORTED) {
  process.env.MIMOCODE_CLAUDE_IMPORTED = "1"
  try {
    await ClaudeImport.run()
  } catch (e) {
    Log.Default.warn("claude-import failed", { e: errorMessage(e) })
  }
}
```

它先把 `MIMOCODE_CLAUDE_IMPORTED` 写进环境变量，子进程继承后就不会再导一遍。每次启动都会遍历全部 JSONL，但没变过的文件只比较一次修改时间就跳过。

## 记账表：external_import

三个导入器共用一张表记录“哪个外部会话对应哪个 MiMo 会话”（`packages/opencode/src/session/external-import.sql.ts:6`）：

```ts
export const ExternalImportTable = sqliteTable(
  "external_import",
  {
    source: text().$type<ExternalSource>().notNull(),
    source_key: text().notNull(),
    session_id: text().$type<SessionID>().notNull(),
    source_path: text().notNull(),
    source_mtime: integer().notNull(),
    time_imported: integer().notNull(),
    message_ids: text({ mode: "json" }).$type<MessageID[]>(),
  },
  (table) => [primaryKey({ columns: [table.source, table.source_key] })],
)
```

这张表最早叫 `claude_import`，只服务 Claude Code；后来的迁移把它推广成三个来源，旧数据的来源一律记为 `cc`（`packages/opencode/migration/20260612000000_external_import/migration.sql:12`）。

每个导入器对单个外部会话的处理顺序相同，以 Claude Code 为例（`packages/opencode/src/session/claude-import.ts:232`）：

1. **跳过未变的**：记账表里已有这条记录、修改时间也没变，且没传 `force`，就跳过（`claude-import.ts:251`）。Claude Code 与 Codex 比较文件的 mtime，OpenCode 比较会话的 `time_updated`。
2. **处理被删的会话**：记录还在，但对应的 MiMo 会话已被用户删掉，就丢掉这条记录当作全新导入，否则后面的更新影响零行、插入消息时外键失败（`claude-import.ts:256`）。
3. **解析并确定项目**：会话的工作目录存在且在 git 仓库里，就按 MiMo 的规则算出项目 ID，否则归到全局项目（`claude-import.ts:226`）。
4. **在一个事务里写库**：项目行不存在才插入；新会话插入会话行；重导时只删除上次导入的那批消息，更新会话的目录与版本（`claude-import.ts:297`）；逐条写入消息与部件；最后更新记账表，连同这次的消息 ID 列表。

第 4 步里重导的那段（`claude-import.ts:297`）：

```ts
if (existing) {
  // Remove only the rows this importer previously created — never the
  // user's mimocode-native continuation messages in the same session.
  // Legacy rows (imported before message_ids tracking) fall back to a
  // full session wipe, matching the original Claude-only contents.
  if (existing.message_ids?.length) {
    for (let i = 0; i < existing.message_ids.length; i += 500)
      tx.delete(MessageTable)
        .where(inArray(MessageTable.id, existing.message_ids.slice(i, i + 500)))
        .run()
  } else {
    tx.delete(MessageTable).where(eq(MessageTable.session_id, sessionId)).run()
  }
  // Preserve mimocode-owned metadata on re-sync: keep any user rename
  // (don't reset title — the Claude title is the immutable first prompt),
  // and never move time_updated backward past native activity.
  tx.update(SessionTable)
    .set({
      project_id: project.id,
      directory: parsed.cwd,
      version: parsed.version ?? "claude-code",
      time_updated: Math.max(existingUpdated ?? 0, parsed.timeUpdated),
    })
    .where(eq(SessionTable.id, sessionId))
    .run()
```

记下 `message_ids` 的意义就在这里：导入的会话可以在 MiMo 里接着聊，之后外部文件又变了（比如又在 Claude Code 里续了几轮），重导只替换导入过来的那批消息，MiMo 里续写的消息保留，用户改过的标题也保留。部件表对消息表设了级联删除，删消息时部件随之删除（`packages/opencode/src/session/session.sql.ts:108`）。

`ExternalImport.runAll` 按 `cc`、`codex`、`opencode` 的顺序串行调用三个导入器，统计扫描、新导入、重导、跳过的数量与错误（`packages/opencode/src/session/external-import.ts:95`）。

## Claude Code：逐行翻译 JSONL

Claude Code 按项目把会话存成 JSONL，每行一条记录。导入器只看 `type` 为 `user` 与 `assistant` 的行（`claude-import.ts:84`），把它们翻译成 MiMo 的 `MessageV2` 消息与部件：

| Claude Code 记录 | MiMo 里变成 |
| --- | --- |
| user 行里的文本块、图片块 | 一条 user 消息，文本部件与 file 部件（base64 图片转成 data URL） |
| user 行里的 `tool_result` 块 | 回填到之前那个工具部件，状态改为 completed 或 error，不单独成消息 |
| assistant 行里的 `thinking` 块 | reasoning 部件 |
| assistant 行里的 `text` 块 | text 部件 |
| assistant 行里的 `tool_use` 块 | tool 部件，先置为 pending，等对应的结果回填 |

几条值得知道的规则：

- **一轮合并成一条 assistant 消息**。Claude Code 一轮里调多次工具会写出多条 assistant 行，中间夹着只含工具结果的 user 行。只含工具结果的 user 行不会结束当前 assistant 消息，于是同一轮的多条 assistant 行归并到同一条消息里（`claude-import.ts:169`）。用量字段每遇到一条 assistant 行就整体覆盖，最终留下的是这一轮最后一条记录的 usage，而不是累加。
- **模型**。`message.model` 不含斜杠时提供方记为 `anthropic`；user 消息沿用上一条 assistant 的模型，会话开头的 user 消息记为 `anthropic/unknown`（`claude-import.ts:25`）。费用记 0，推理 token 记 0。
- **标题**。取第一段不以 `<` 开头的用户文本，压缩空白后截取 100 个字符，这样可以跳过斜杠命令留下的标签；一段都没有时用 `Claude Code session`（`claude-import.ts:144`）。
- **ID**。会话 ID 在首次导入时由 MiMo 生成，之后沿用；消息与部件的 ID 每次解析都重新生成，所以重导替换进来的消息是新 ID，时间戳仍沿用原始记录。
- **范围**。扫描用的是 `*/*.jsonl`，只看项目目录下第一层的文件（`claude-import.ts:237`）。

## Codex：response_item 流

Codex 的会话文件同样是 JSONL，但结构是一串事件。导入器读两类（`packages/opencode/src/session/codex-import.ts:119`）：

- `session_meta`：取工作目录，版本记为 `codex-` 加 CLI 版本号，并以其中的时间戳校正会话创建时间。
- `response_item`：按 `payload.type` 分派。`message` 中 `user` 与 `developer` 角色都变成 user 消息，`assistant` 的输出文本变成 text 部件；`reasoning` 只取摘要文本；`function_call` 与 `custom_tool_call` 变成工具部件，后者的输入原样包成 `raw` 字段；`function_call_output` 与 `custom_tool_call_output` 回填对应工具部件的结果。

Codex 常常先写推理和工具调用、后写回答文本，甚至在任何 assistant 消息之前就出现，所以导入器用 `ensureAssistant` 在需要时补出 user 与 assistant 消息，注释写明了这一点（`codex-import.ts:77`）：

> Codex emits reasoning and tool calls before the assistant text — and sometimes before any assistant message at all — so we must materialize the assistant here rather than dropping the part when nothing is open yet.

与 Claude Code 相比，信息损失更多：模型一律记为 `openai/unknown`，用量全部为 0，工具结果一律记为 completed。`developer` 角色的消息也按 user 消息导入。去重键是文件名（`codex-import.ts:278`），`session_meta` 里的会话 ID 解析出来了，但没有用到。

## OpenCode：直接搬数据库行

MiMo 的会话存储沿用自 OpenCode，这个导入器不做格式翻译，直接用只读方式打开 OpenCode 的 SQLite（`packages/opencode/src/session/opencode-import.ts:82`）。Bun 下用 `bun:sqlite`，Node 下用 `node:sqlite`，由 `package.json` 的条件导入挑选（`packages/opencode/src/storage/read-sqlite.ts:1`）。

- 先确认存在 `session` 表，再按创建时间倒序读出全部会话（`opencode-import.ts:89`）。
- 每个会话的消息整批读出，部件按每批 200 个消息 ID 分批查询（`opencode-import.ts:148`）。
- 会话、消息、部件的 ID、`slug`、`parent_id` 原样保留，消息与部件的 `data` 列解析成 JSON 后原样写入（`opencode-import.ts:192`）。
- 记账表的 `source_mtime` 存会话的 `time_updated`，所以 OpenCode 里续写过的会话下次会被重导（`opencode-import.ts:108`）。

需要注意的是，OpenCode 的父子会话关系被保留，但只搬会话、消息、部件三张表，待办等其他表不在导入范围内；所有消息的 `agent_id` 一律写成 `main`。

## 凭据：“从 Claude Code 导入”

README 说首次启动会自动引导配置（`README.zh.md:41`）。TUI 代码里能找到的是：尚未连接任何提供方时，命令面板把 `/connect` 标为推荐项（`packages/opencode/src/cli/cmd/tui/app.tsx:836`）；没有选中模型又没有任何提供方时，提交输入会弹出上游原有的提供方列表（`packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:433`）。没有找到启动时自动弹出登录框的代码。

“从 Claude Code 导入”是 `/login` 与 `/connect` 对话框里的第三个推荐项，实现在 `packages/opencode/src/cli/cmd/tui/component/dialog-mimo-login.tsx:73`。它依次读取 `~/.claude/` 下的 `settings.json`、`settings.local.json`、`settings_local.json` 里的 `env` 段，后读到的文件优先，都没有时回落到当前进程的环境变量；然后取这几个值（`dialog-mimo-login.tsx:98`）：

```tsx
const key = resolve("ANTHROPIC_API_KEY")
const rawBaseUrl = resolve("ANTHROPIC_BASE_URL")
const baseUrl = rawBaseUrl
  ? rawBaseUrl.replace(/\/+$/, "").replace(/(?<!\/v1)$/, "/v1")
  : undefined
// strip Claude Code context-window suffix e.g. claude-opus-4-6[1m]
const preferredModel = (
  resolve("ANTHROPIC_DEFAULT_OPUS_MODEL") ?? resolve("ANTHROPIC_DEFAULT_SONNET_MODEL")
)?.replace(/\[.*\]$/, "")
```

拿到之后：

1. 把 `ANTHROPIC_API_KEY` 存为 `anthropic` 提供方的 API key；找不到就提示“未找到 Claude Code API Key”并结束。凭据只认这一个变量，Claude Code 自己保存的登录凭据不在读取范围内。
2. 把 `baseURL` 写进全局配置的 `provider.anthropic.options`：没设 `ANTHROPIC_BASE_URL` 时写官方地址 `https://api.anthropic.com/v1`，设了就去掉末尾斜杠，末尾不是 `/v1` 时补上。
3. 重建实例后挑模型：环境里指定的 Opus（其次是 Sonnet）模型不在目录里，就先写进配置；选中顺序是指定模型、`claude-opus-4-6`、按名称排序后最后一个含 opus 的、最后一个含 sonnet 的、第一个模型（`dialog-mimo-login.tsx:141`）。

因为第 2 步总会写入 `baseURL`，之后发往 Anthropic 的请求都会经过 `AnthropicProxyPlugin`：删掉 `anthropic-beta` 请求头，读到 `message_stop` 事件后主动关闭流（`packages/opencode/src/plugin/mimo.ts:205`）。插件与登录的其余部分见[模型、提供方与提示词路由](https://daiw.org/manual/mimo-code/providers)。Claude Code 本身的配置方式见 [Claude Code 手册](https://daiw.org/manual/claude-code)。

## 沿用而非导入的部分

下面这些不经过导入器，MiMo 每次加载时直接读原位置，改动即时生效：

| 内容 | 读取位置 | 关闭开关 | 出处 |
| --- | --- | --- | --- |
| 指令文件 | 项目的 `CLAUDE.md`（没有 `AGENTS.md` 时，或 `AGENTS.md` 不足 500 个字符时一并加载），以及 `~/.claude/CLAUDE.md` | `MIMOCODE_DISABLE_CLAUDE_CODE_PROMPT` | `packages/opencode/src/session/instruction.ts:17` |
| 斜杠命令 | `~/.claude/commands` 与从当前目录到仓库根各级的 `.claude/commands`，同名时 `.mimocode` 的命令覆盖它们 | `MIMOCODE_DISABLE_CLAUDE_CODE_COMMANDS` | `packages/opencode/src/config/paths.ts:45` |
| MCP 服务器 | `~/.claude.json` 与当前目录下 `.claude.json` 里的 `mcpServers`，与 MiMo 原生配置重名时跳过 | `MIMOCODE_DISABLE_CLAUDE_CODE_MCP` | `packages/opencode/src/config/config.ts:771` |
| 技能 | `.claude`、`.agents`、`.codex`、`.opencode` 下的技能目录，见[内置技能包与技能检索](https://daiw.org/manual/mimo-code/skills) | 每个目录各有一个开关 | `packages/opencode/src/skill/index.ts:23` |
| 记忆 | `~/.claude/projects/<项目>/memory`，建索引后可在 `memory` 工具里检索，见[分层记忆](https://daiw.org/manual/mimo-code/memory) | 配置 `memory.cc_index`，默认关闭 | `packages/opencode/src/memory/service.ts:40` |
| Workflow 脚本 | 各级 `.claude/workflows/<名字>.js`，排在 `.mimocode/workflows` 之后，见 [Workflow 运行时](https://daiw.org/manual/mimo-code/workflow-runtime) | 无 | `packages/opencode/src/workflow/resolve.ts:24` |

上游 v1.14.19 已经会读 `CLAUDE.md` 和 `.claude`、`.agents` 下的技能，“`AGENTS.md` 太短时补读 `CLAUDE.md`”以及表里其余几项是 MiMo 加的。总开关是 `MIMOCODE_MIMO_ONLY`：它关掉 `CLAUDE.md` 与 `.claude` 技能的沿用，也不再凭环境变量里的 API key 点亮提供方，但特意保留 MCP 兼容，这样可以继续用 Claude Code 里配好的 MCP 服务器（`packages/opencode/src/flag/flag.ts:206`）。斜杠命令的开关独立于总开关（`flag.ts:212`）。Codex 的配置方式见 [Codex 手册](https://daiw.org/manual/codex)。

下一篇：[终端体验：语音、多语言与视觉](https://daiw.org/manual/mimo-code/tui)——TUI 里新增的语音输入、七种界面语言、星空背景与侧栏插件。
