从 Claude Code、Codex 与 OpenCode 迁移

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

作者 David更新于 第 31 篇(共 33 篇)

从别的编码 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):

// 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):

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):

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):

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)。插件与登录的其余部分见模型、提供方与提示词路由。Claude Code 本身的配置方式见 Claude Code 手册。

沿用而非导入的部分

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

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

下一篇:终端体验:语音、多语言与视觉——TUI 里新增的语音输入、七种界面语言、星空背景与侧栏插件。

本页目录