Skip to content

AI 应用导入向导中新增 ChatGPT 数据导出 归档源#1373

Open
Liyu199808 wants to merge 4 commits into
tutti-os:mainfrom
Liyu199808:feat/chatgpt-data-exports-issues-1270
Open

AI 应用导入向导中新增 ChatGPT 数据导出 归档源#1373
Liyu199808 wants to merge 4 commits into
tutti-os:mainfrom
Liyu199808:feat/chatgpt-data-exports-issues-1270

Conversation

@Liyu199808

@Liyu199808 Liyu199808 commented Jul 18, 2026

Copy link
Copy Markdown

问题背景

Tutti 已支持导入 Codex、Claude Code 本地记录与 Claude 数据导出 ZIP,但无法
导入用户在 ChatGPT 中的历史对话。手动复制会丢失标题、时间戳、消息结构,且
不适用于大体量归档。(Closes #1270)

解决方式

在现有「从 AI 应用导入」向导中新增 ChatGPT 数据导出 归档源,复用 Claude
导出的本地优先交互(选 ZIP → 本地扫描 → 检索/选择 → 导入)。

  • 契约先行:在 tuttid.v1.yaml 新增 archiveKind 枚举(claude | chatgpt),
    显式区分归档类型,而非把 ChatGPT 伪装成 Claude/Codex provider;已重新生成
    Go/TS 客户端。
  • daemon 解析external_import_chatgpt_export*.go):解析 conversations.json,
    current_node 为叶子回溯 root 再反转得到确定性线性会话;保留标题、
    创建/更新时间、user/assistant 角色、文本与图片占位;用源 UUID/节点 ID 派生
    稳定的 session/message ID 保证幂等。
  • provider 身份:ChatGPT 作为「仅导入」源,不进入可运行 provider 注册表;
    仅在 GUI 目录补充展示标签,并修复导入选择过滤会误删归档源 provider 的问题。
  • 安全:复用 Claude 的归档大小/条目数/解压体积上限与 ZIP/JSON 校验器;
    ChatGPT 错误信息不回传本地绝对路径。
  • desktop:源步骤新增独立卡片,archiveKind 贯穿 wizard/model;新增 en/zh-CN 文案,
    且归档态下的标题、扫描/失败提示、空态、搜索框、结果汇总等文案均按 archiveKind
    切换为 ChatGPT 品牌(不再复用 Claude 文案)。
  • 初版仅支持 Legacy 根目录 conversations.json;手动用官方 OpenAI-export.zip 验证发现需适配 bundle 格式。另修复 linear 追加消息时 session ID 随 current_node 变化导致重复导入的问题。

验证结果

  • go test ./services/tuttid/service/agent/ -run ChatGPT:9/9 PASS
    (解析、分支确定性、幂等、缺失/非数组/重复/超大恶意归档、路径不泄漏、e2e 持久化)
  • go test ./services/tuttid/api/:PASS(archiveKind 转发 + 非法值 400)
  • desktop 模型测试 / typecheck / check:api-generated / check:changed:PASS
  • 已手动冒烟完整导入流程。
  • 验收标准 12 项已逐条对照覆盖(详见测试映射)。

取舍与未完成事项

  • 本次仅对 ChatGPT 导入做路径脱敏;Claude 现状暴露路径的行为按约定不在本 PR 改动。
  • 不支持的单条消息静默跳过(与 Claude 行为一致),仅空会话计入 skipped 汇总。
  • 不做 OAuth/云同步;仅本地归档导入。导入不续接原 ChatGPT 服务端会话。
  • 无面向用户的导入文档,故未新增文档(文档影响已检查)。

. 验收标准 → 测试映射与通过情况

issues 的验收标准逐条对照(全部 ChatGPT 相关测试已通过):

# 验收标准 覆盖的测试 层级 状态
1 无需预处理即可导入官方 ZIP TestImportChatGPTExportArchivePersistsSessionsWithoutLeakingPath(直接读原始 ZIP) daemon e2e
2 导航路径暴露 ChatGPT 源 UI 卡片(ExternalAgentSessionImportSourceStep.tsx)+ i18n key desktop ✅(自动化仅覆盖模型层,UI 呈现需手动)
3 复用扫描/搜索/选择/进度/结果/错误模式 externalAgentSessionImportWizardModel.test.ts(archiveKind 贯穿 scan/import) desktop
4 标题/时间戳/角色/顺序/内容保留 TestScanChatGPTExportArchiveNormalizesRolesTextAndAssetPlaceholders daemon
5 分支/重生消息确定性 + 测试 TestScanChatGPTExportArchiveFollowsCurrentNodeAndDropsAbandonedBranch daemon
6 重复导入不产生重复 TestScanChatGPTExportArchiveIsIdempotentAndBranchScoped + e2e 二次导入 daemon
7 跳过不支持内容且不牵连整段会话 Normalizes 测试(system/tool/隐藏消息被过滤,会话仍导入;空会话计入 skipped) daemon
8 不标记为可对原 ChatGPT 续聊 e2e:AgentTargetID 空、Resumable=false daemon
9 仅本地解析 + 畸形/超大/恶意 ZIP RejectsMissingConversationsWithoutLeakingPathRejectsNonArrayConversationsRejectsDuplicateConversationsEntry新增 TestChatGPTExportConversationStreamRejectsElementBeforeExceedingByteBudget daemon
10 en + zh-CN i18n,中文标点合规 en.ts / zh-CN.ts 新增 key(typecheck 校验键对齐) desktop
11 daemon/API/client/desktop/wizard 全链路 daemon + daemon_agent_sessions_external_import_test.go(forward + 非法 archiveKind 400)+ desktop 模型测试 全层
12 文档影响已检查 无面向用户的导入文档,PR 中声明"无需新增" ✅(声明)

补充说明:

  • 路径不泄漏:ChatGPT 导入把 SourcePath 置空,错误信息不含本地绝对路径,已由 ...WithoutLeakingPath 系列测试断言。Claude 现状保留(本次按你的要求不改)。
  • 超大/解压限制:复用 Claude 的归档大小、条目数、解压体积上限;新增的字节预算测试在 ChatGPT 层显式验证了逐会话尺寸拒绝。
  • 跳过计数粒度:不支持的单条消息静默丢弃(与 Claude 行为一致),仅空会话计入 skipped 汇总——这是刻意与现有实现对齐的取舍。
20ebf958ece12f3ebd07688a8ed7a6b4 image image
Open in Devin Review

Add ChatGPT data export as a new archive source in the external
agent-session import wizard, mirroring the local-first Claude export flow.

The daemon parses conversations.json, linearizes the branch tree by
following current_node back to root, preserves titles, timestamps, roles,
text and asset placeholders, and generates stable session/message IDs so
re-import stays idempotent. Archive kind is threaded explicitly through the
OpenAPI contract (archiveKind: claude | chatgpt) instead of routing ChatGPT
through a fake Claude/Codex provider identity. ChatGPT imports are
non-resumable and never leak local archive paths in user-visible errors.

Reuses Claude's archive size, entry-count and decompressed-size limits and
JSON-stream/ZIP validators. Adds en/zh-CN i18n copy that is branded per
archive kind across the whole wizard (title, scan/scan-failed, empty,
search, selection and result summary), and daemon, API and desktop tests
covering parsing, branching, idempotency, hostile archives and end-to-end
import.

Closes tutti-os#1270

Signed-off-by: liyu <Admin@MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
@tutti-pr-review-bot
tutti-pr-review-bot Bot requested a review from a team July 18, 2026 09:25

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

liyu and others added 3 commits July 18, 2026 20:47
…hives

ChatGPT archive imports intentionally redact the local archive path and
emit an empty sourcePath, but the ExternalAgentImportSession schema marked
sourcePath as required with minLength:1, so the daemon returned data that
violated its own published contract.

Update tuttid.v1.yaml to make sourcePath optional and nullable (mirroring
the existing ExternalAgentImportError.sourcePath), regenerate the Go/TS
clients, and only populate sourcePath when a non-empty path is present.
Claude and Codex imports still surface their real path; archive-only
imports now omit the field instead of sending an empty string.

Signed-off-by: liyu <Admin@MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
OpenAI's current data export nests Conversations__*-chatgpt-*.zip with
sharded conversations-NNN.json files instead of a root conversations.json.
Discover and stream those shards while keeping legacy root conversations.json
support.

Stop keying ProviderSessionID on the current_node leaf id, which changed on
every linear message append and created duplicate imported sessions. Use
fork-decision hashing (matching the Claude export importer) so linear
conversation growth appends to the same session while regenerated branches
remain separate.

Signed-off-by: liyu <Admin@MacBook-Pro.local>
Signed-off-by: liyu <Admin@MacBook-Pro.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: import conversations from ChatGPT data exports

1 participant