import type {Conversation, StoredChatMessage, WorkingItem} from "./conversation-types"; import {defaultGenerationSettings, normalizeGenerationSettings, type GenerationSettings} from "./generation-settings"; export type SessionTransferFormat = "xiteng" | "codex" | "claude" | "omp"; export type TransferNode = { sourceId: string; parentSourceId: string | null; role: StoredChatMessage["role"]; parts: StoredChatMessage["parts"]; createdAt: string; completedAt: string; completion?: StoredChatMessage["completion"]; origin?: StoredChatMessage["origin"]; metadata?: StoredChatMessage["metadata"]; }; export type TransferSession = { sourceId: string; name: string; headSourceId: string | null; providerId: string; model: string; generationSettings: GenerationSettings; createdAt: string; updatedAt: string; }; export type TransferDocument = { format: SessionTransferFormat; sessions: TransferSession[]; nodes: TransferNode[]; workingItems?: WorkingItem[]; }; type JsonRecord = Record; function isRecord(value: unknown): value is JsonRecord { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function stringValue(value: unknown, fallback = "") { return typeof value === "string" ? value : fallback; } function isoTimestamp(value: unknown, fallback = new Date().toISOString()) { if (typeof value === "number" && Number.isFinite(value)) { const milliseconds = value < 10_000_000_000 ? value * 1000 : value; return new Date(milliseconds).toISOString(); } if (typeof value === "string" && Number.isFinite(Date.parse(value))) return new Date(value).toISOString(); return fallback; } function jsonLines(text: string) { const values: JsonRecord[] = []; for (const [index, line] of text.split(/\r?\n/).entries()) { if (!line.trim()) continue; try { const value: unknown = JSON.parse(line); if (isRecord(value)) values.push(value); } catch { throw new Error(`JSONL 第 ${index + 1} 行无法解析`); } } return values; } function contentParts(value: unknown, textTypes = ["text", "input_text", "output_text"]): StoredChatMessage["parts"] { if (typeof value === "string") return value ? [{type: "text", text: value}] : []; if (!Array.isArray(value)) return []; const parts: StoredChatMessage["parts"] = []; for (const candidate of value) { if (!isRecord(candidate)) continue; if (textTypes.includes(stringValue(candidate.type)) && typeof candidate.text === "string") { parts.push({type: "text", text: candidate.text}); } else if (candidate.type === "thinking" && typeof candidate.thinking === "string") { parts.push({type: "reasoning", text: candidate.thinking, ...(typeof candidate.signature === "string" ? {signature: candidate.signature} : {})}); } else if (candidate.type === "image" && isRecord(candidate.source)) { const data = stringValue(candidate.source.data); const mediaType = stringValue(candidate.source.media_type); if (data && mediaType) parts.push({type: "image", data, mimeType: mediaType}); } else if (candidate.type === "input_image" && typeof candidate.image_url === "string") { parts.push({type: "image-url", url: candidate.image_url, ...(typeof candidate.detail === "string" ? {detail: candidate.detail} : {})}); } } return parts; } function firstText(nodes: TransferNode[]) { for (const node of nodes) { if (node.role !== "user") continue; const text = node.parts.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => String(part.text)).join("").trim(); if (text && !/^<(environment_context|permissions instructions)>/i.test(text) && !/^# AGENTS\.md instructions/i.test(text)) return text.replace(/\s+/g, " ").slice(0, 80); } return "导入的会话"; } function newestLeaf(nodes: TransferNode[]) { const parents = new Set(nodes.map((node) => node.parentSourceId).filter((id): id is string => Boolean(id))); return [...nodes].filter((node) => !parents.has(node.sourceId)).sort((left, right) => right.completedAt.localeCompare(left.completedAt))[0]?.sourceId || nodes.at(-1)?.sourceId || null; } function transferNode(sourceId: string, parentSourceId: string | null, role: StoredChatMessage["role"], parts: StoredChatMessage["parts"], timestamp: string, extra: Partial = {}): TransferNode { return { sourceId, parentSourceId, role, parts, createdAt: timestamp, completedAt: timestamp, completion: {status: "complete"}, origin: role === "user" ? {type: "user"} : role === "system" ? {type: "system", source: "session-import"} : {type: "legacy"}, ...extra }; } function parseXiteng(value: JsonRecord): TransferDocument { if (value.type !== "xiteng-chat-archive" || value.version !== 1 || !Array.isArray(value.conversations) || !Array.isArray(value.objects)) { throw new Error("不支持的 Xiteng Chat 备份版本"); } const nodes: TransferNode[] = []; for (const candidate of value.objects) { if (!isRecord(candidate) || typeof candidate.id !== "string" || !["system", "user", "assistant"].includes(String(candidate.role)) || !Array.isArray(candidate.parts)) continue; nodes.push({ sourceId: candidate.id, parentSourceId: typeof candidate.parentMessageId === "string" ? candidate.parentMessageId : null, role: candidate.role as StoredChatMessage["role"], parts: candidate.parts as StoredChatMessage["parts"], origin: isRecord(candidate.origin) ? candidate.origin as StoredChatMessage["origin"] : {type: "legacy"}, completion: isRecord(candidate.completion) ? candidate.completion as StoredChatMessage["completion"] : {status: "complete"}, createdAt: isoTimestamp(candidate.createdAt), completedAt: isoTimestamp(candidate.completedAt, isoTimestamp(candidate.createdAt)), ...(isRecord(candidate.metadata) ? {metadata: candidate.metadata as StoredChatMessage["metadata"]} : {}) }); } const sessions: TransferSession[] = value.conversations.flatMap((candidate) => { if (!isRecord(candidate) || typeof candidate.id !== "string") return []; return [{ sourceId: candidate.id, name: stringValue(candidate.name), headSourceId: typeof candidate.headMessageId === "string" ? candidate.headMessageId : null, providerId: stringValue(candidate.providerId, "imported"), model: stringValue(candidate.model, "imported"), generationSettings: normalizeGenerationSettings(candidate.generationSettings), createdAt: isoTimestamp(candidate.createdAt), updatedAt: isoTimestamp(candidate.updatedAt) }]; }); return {format: "xiteng", sessions, nodes, workingItems: Array.isArray(value.workingItems) ? value.workingItems as WorkingItem[] : []}; } function parseCodex(records: JsonRecord[]): TransferDocument { const metadata = records.find((record) => record.type === "session_meta" && isRecord(record.payload)); const sessionPayload = metadata && isRecord(metadata.payload) ? metadata.payload : {}; const sessionId = stringValue(sessionPayload.id, crypto.randomUUID()); let model = "codex"; let name = ""; let parentSourceId: string | null = null; let ordinal = 0; const nodes: TransferNode[] = []; const canonicalUserText = new Set(); const canonicalAssistantText = new Set(); for (const record of records) { if (record.type !== "response_item" || !isRecord(record.payload) || record.payload.type !== "message") continue; const text = contentParts(record.payload.content).filter((part) => part.type === "text").map((part) => String(part.text || "")).join(""); if (record.payload.role === "user") canonicalUserText.add(text); if (record.payload.role === "assistant") canonicalAssistantText.add(text); } const pushNode = (role: StoredChatMessage["role"], parts: StoredChatMessage["parts"], timestamp: string) => { if (!parts.length) return; const sourceId = `codex-${sessionId}-${++ordinal}`; nodes.push(transferNode(sourceId, parentSourceId, role, parts, timestamp)); parentSourceId = sourceId; }; for (const record of records) { const timestamp = isoTimestamp(record.timestamp, isoTimestamp(sessionPayload.timestamp)); if (!isRecord(record.payload)) continue; const payload = record.payload; if (record.type === "turn_context" && typeof payload.model === "string") model = payload.model; if (record.type === "response_item") { if (payload.type === "message" && (payload.role === "user" || payload.role === "assistant")) { const parts = contentParts(payload.content); const text = parts.filter((part) => part.type === "text").map((part) => String(part.text || "")).join("").trim(); if (payload.role !== "user" || !/^<(environment_context|permissions instructions)>/i.test(text)) pushNode(payload.role, parts, timestamp); } else if (payload.type === "reasoning") { const parts = [...(Array.isArray(payload.summary) ? payload.summary : []), ...(Array.isArray(payload.content) ? payload.content : [])] .flatMap((item) => isRecord(item) && typeof item.text === "string" ? [{type: "reasoning", text: item.text}] : []); pushNode("assistant", parts, timestamp); } else if (["function_call", "custom_tool_call", "web_search_call", "tool_search_call"].includes(String(payload.type))) { const callId = stringValue(payload.call_id, stringValue(payload.id, `call-${ordinal + 1}`)); const toolName = stringValue(payload.name, payload.type === "web_search_call" ? "web_search" : payload.type === "tool_search_call" ? "tool_search" : "unknown"); let argumentsValue: unknown = payload.type === "custom_tool_call" ? payload.input : payload.arguments ?? payload.action; if (typeof argumentsValue === "string") { try { argumentsValue = JSON.parse(argumentsValue); } catch { argumentsValue = {input: argumentsValue}; } } pushNode("assistant", [{type: "tool-call", id: callId, name: toolName, arguments: isRecord(argumentsValue) ? argumentsValue : {}}], timestamp); } else if (["function_call_output", "custom_tool_call_output", "tool_search_output"].includes(String(payload.type))) { pushNode("assistant", [{type: "tool-result", toolCallId: stringValue(payload.call_id), content: payload.output ?? payload.tools, isError: payload.status === "failed"}], timestamp); } } else if (record.type === "event_msg") { if (payload.type === "thread_name_updated" && typeof payload.thread_name === "string") name = payload.thread_name; if (payload.type === "user_message" && typeof payload.message === "string" && !canonicalUserText.has(payload.message)) pushNode("user", [{type: "text", text: payload.message}], timestamp); if (payload.type === "agent_message" && typeof payload.message === "string" && !canonicalAssistantText.has(payload.message)) pushNode("assistant", [{type: "text", text: payload.message}], timestamp); } } const createdAt = isoTimestamp(sessionPayload.timestamp, nodes[0]?.createdAt); return { format: "codex", nodes, sessions: [{sourceId: sessionId, name: name || firstText(nodes), headSourceId: parentSourceId, providerId: "openai", model, generationSettings: defaultGenerationSettings, createdAt, updatedAt: nodes.at(-1)?.completedAt || createdAt}] }; } function nearestRetainedParent(sourceParentId: string | null, sourceParents: Map, retained: Map) { const seen = new Set(); let cursor = sourceParentId; while (cursor && !seen.has(cursor)) { seen.add(cursor); const retainedId = retained.get(cursor); if (retainedId) return retainedId; cursor = sourceParents.get(cursor) || null; } return null; } function parseClaude(records: JsonRecord[]): TransferDocument { const sourceParents = new Map(); for (const record of records) if (typeof record.uuid === "string") sourceParents.set(record.uuid, typeof record.parentUuid === "string" ? record.parentUuid : null); const retained = new Map(); const nodes: TransferNode[] = []; let sessionId = ""; let name = ""; let model = "claude"; for (const [index, record] of records.entries()) { sessionId ||= stringValue(record.sessionId); if (record.type === "custom-title") name = stringValue(record.customTitle, name); if (record.type === "ai-title" && !name) name = stringValue(record.aiTitle, name); if ((record.type !== "user" && record.type !== "assistant") || record.isSidechain === true || record.isMeta === true || !isRecord(record.message)) continue; const sourceUuid = stringValue(record.uuid, `claude-line-${index + 1}`); let parentSourceId = nearestRetainedParent(typeof record.parentUuid === "string" ? record.parentUuid : null, sourceParents, retained); const timestamp = isoTimestamp(record.timestamp); const converted: TransferNode[] = []; if (record.type === "assistant") { model = stringValue(record.message.model, model); const parts = contentParts(record.message.content); if (Array.isArray(record.message.content)) { for (const block of record.message.content) { if (!isRecord(block) || block.type !== "tool_use") continue; parts.push({type: "tool-call", id: stringValue(block.id), name: stringValue(block.name, "unknown"), arguments: isRecord(block.input) ? block.input : {}}); } } if (parts.length) converted.push(transferNode(sourceUuid, parentSourceId, "assistant", parts, timestamp)); } else { const toolResults = Array.isArray(record.message.content) ? record.message.content.filter((block) => isRecord(block) && block.type === "tool_result") : []; if (toolResults.length) { for (const block of toolResults) { if (!isRecord(block)) continue; converted.push(transferNode(`${sourceUuid}-tool-result-${converted.length}`, parentSourceId, "assistant", [{type: "tool-result", toolCallId: stringValue(block.tool_use_id), content: block.content, isError: block.is_error === true}], timestamp)); parentSourceId = converted.at(-1)!.sourceId; } } else { const parts = contentParts(record.message.content); if (parts.length) converted.push(transferNode(sourceUuid, parentSourceId, "user", parts, timestamp)); } } for (const node of converted) nodes.push(node); const tail = converted.at(-1)?.sourceId; if (tail) retained.set(sourceUuid, tail); } sessionId ||= crypto.randomUUID(); const createdAt = nodes[0]?.createdAt || new Date().toISOString(); return {format: "claude", nodes, sessions: [{sourceId: sessionId, name: name || firstText(nodes), headSourceId: newestLeaf(nodes), providerId: "anthropic", model, generationSettings: defaultGenerationSettings, createdAt, updatedAt: nodes.at(-1)?.completedAt || createdAt}]}; } function ompMessageParts(message: JsonRecord) { const parts = contentParts(message.content); if (Array.isArray(message.content)) { for (const block of message.content) { if (!isRecord(block)) continue; if (block.type === "toolCall") parts.push({type: "tool-call", id: stringValue(block.id), name: stringValue(block.name, "unknown"), arguments: isRecord(block.arguments) ? block.arguments : {}}); } } return parts; } function parseOmp(records: JsonRecord[]): TransferDocument { const header = records.find((record) => record.type === "session"); if (!header) throw new Error("OMP JSONL 缺少 session header"); const titleSlot = records.find((record) => record.type === "title"); const titleChanges = records.filter((record) => record.type === "title_change"); const sourceParents = new Map(); for (const record of records) if (typeof record.id === "string" && record.type !== "session") sourceParents.set(record.id, typeof record.parentId === "string" ? record.parentId : null); const retained = new Map(); const nodes: TransferNode[] = []; let model = "omp"; for (const record of records) { if (record.type === "model_change" && typeof record.model === "string") model = record.model.includes("/") ? record.model.slice(record.model.indexOf("/") + 1) : record.model; if (record.type !== "message" || typeof record.id !== "string" || !isRecord(record.message)) continue; const parentSourceId = nearestRetainedParent(typeof record.parentId === "string" ? record.parentId : null, sourceParents, retained); const timestamp = isoTimestamp(record.timestamp); const role = record.message.role; let parts: StoredChatMessage["parts"] = []; if (role === "toolResult") parts = [{type: "tool-result", toolCallId: stringValue(record.message.toolCallId), toolName: stringValue(record.message.toolName, "unknown"), content: record.message.content, isError: record.message.isError === true}]; else parts = ompMessageParts(record.message); if (!parts.length || (role !== "user" && role !== "assistant" && role !== "toolResult")) continue; nodes.push(transferNode(record.id, parentSourceId, role === "toolResult" ? "assistant" : role, parts, timestamp)); retained.set(record.id, record.id); } const createdAt = isoTimestamp(header.timestamp, nodes[0]?.createdAt); const latestTitle = titleChanges.at(-1); const name = stringValue(latestTitle?.title, stringValue(titleSlot?.title, stringValue(header.title, firstText(nodes)))); return {format: "omp", nodes, sessions: [{sourceId: stringValue(header.id, crypto.randomUUID()), name, headSourceId: nodes.at(-1)?.sourceId || null, providerId: model.includes("claude") ? "anthropic" : model.includes("gpt") || model.includes("codex") ? "openai" : "imported", model, generationSettings: defaultGenerationSettings, createdAt, updatedAt: nodes.at(-1)?.completedAt || createdAt}]}; } export function detectSessionTransferFormat(text: string, filename = ""): SessionTransferFormat { const trimmed = text.trimStart(); if (trimmed.startsWith("{")) { try { const firstLine = JSON.parse(trimmed.split(/\r?\n/, 1)[0]) as unknown; if (isRecord(firstLine)) { if (firstLine.type === "xiteng-chat-archive") return "xiteng"; if (firstLine.type === "session_meta") return "codex"; if (firstLine.type === "session" || firstLine.type === "title") return "omp"; if (typeof firstLine.sessionId === "string" || typeof firstLine.uuid === "string" || ["user", "assistant", "custom-title", "ai-title"].includes(String(firstLine.type))) return "claude"; } } catch {} } const lower = filename.toLowerCase(); if (lower.endsWith(".xiteng-chat.json")) return "xiteng"; throw new Error("无法识别会话格式;请选择 Xiteng Chat JSON 或 Codex / Claude Code / OMP JSONL"); } export function parseSessionTransfer(text: string, filename = ""): TransferDocument { const format = detectSessionTransferFormat(text, filename); if (format === "xiteng") { const value: unknown = JSON.parse(text); if (!isRecord(value)) throw new Error("Xiteng Chat 备份不是 JSON 对象"); return parseXiteng(value); } const records = jsonLines(text); if (format === "codex") return parseCodex(records); if (format === "claude") return parseClaude(records); return parseOmp(records); } function nodeTextParts(node: TransferNode) { return node.parts.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => ({type: node.role === "assistant" ? "output_text" : "input_text", text: String(part.text)})); } function assistantReasoningParts(node: TransferNode) { return node.parts.filter((part) => part.type === "reasoning" && typeof part.text === "string").map((part) => String(part.text)); } function portableUuid(value: string) { const match = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.exec(value); return match?.[0] || crypto.randomUUID(); } function jsonl(records: JsonRecord[]) { return `${records.map((record) => JSON.stringify(record)).join("\n")}\n`; } function serializeCodex(document: TransferDocument) { const session = document.sessions[0]; if (!session) throw new Error("没有可导出的会话"); const id = portableUuid(session.sourceId); const records: JsonRecord[] = [{type: "session_meta", timestamp: session.createdAt, payload: {id, timestamp: session.createdAt, cwd: "/workspace", originator: "xiteng-chat", cli_version: "0.147.0", source: "cli", model_provider: session.providerId}}]; records.push({type: "turn_context", timestamp: session.createdAt, payload: {cwd: "/workspace", model: session.model}}); for (const node of currentPath(document.nodes, session.headSourceId)) { const text = nodeTextParts(node); if (text.length) records.push({type: "response_item", timestamp: node.createdAt, payload: {type: "message", role: node.role, content: text, ...(node.role === "assistant" ? {phase: "final_answer"} : {})}}); for (const reasoning of assistantReasoningParts(node)) records.push({type: "response_item", timestamp: node.createdAt, payload: {type: "reasoning", summary: [{type: "summary_text", text: reasoning}]}}); for (const part of node.parts) { if (part.type === "tool-call") records.push({type: "response_item", timestamp: node.createdAt, payload: {type: "function_call", call_id: stringValue(part.id, crypto.randomUUID()), name: stringValue(part.name, "unknown"), arguments: JSON.stringify(isRecord(part.arguments) ? part.arguments : {})}}); if (part.type === "tool-result") records.push({type: "response_item", timestamp: node.createdAt, payload: {type: "function_call_output", call_id: stringValue(part.toolCallId), output: part.content ?? ""}}); } } if (session.name) records.push({type: "event_msg", timestamp: session.updatedAt, payload: {type: "thread_name_updated", thread_name: session.name}}); return jsonl(records); } function claudeContent(node: TransferNode) { const content: JsonRecord[] = []; for (const part of node.parts) { if (part.type === "text" && typeof part.text === "string") content.push({type: "text", text: part.text}); if (part.type === "reasoning" && typeof part.text === "string") content.push({type: "thinking", thinking: part.text, signature: stringValue(part.signature)}); if (part.type === "tool-call") content.push({type: "tool_use", id: stringValue(part.id, crypto.randomUUID()), name: stringValue(part.name, "unknown"), input: isRecord(part.arguments) ? part.arguments : {}}); } return content; } function serializeClaude(document: TransferDocument) { const session = document.sessions[0]; if (!session) throw new Error("没有可导出的会话"); const sessionId = portableUuid(session.sourceId); const idMap = new Map(document.nodes.map((node) => [node.sourceId, portableUuid(node.sourceId)])); const records: JsonRecord[] = []; for (const node of document.nodes) { const common = {sessionId, uuid: idMap.get(node.sourceId), parentUuid: node.parentSourceId ? idMap.get(node.parentSourceId) || null : null, timestamp: node.createdAt, cwd: "/workspace", version: "2.1.81", gitBranch: ""}; const toolResults = node.parts.filter((part) => part.type === "tool-result"); const content = claudeContent(node); if (toolResults.length) { records.push({...common, type: "user", message: {role: "user", content: toolResults.map((part) => ({type: "tool_result", tool_use_id: stringValue(part.toolCallId), content: part.content ?? "", is_error: part.isError === true}))}}); } else if (node.role === "user") { records.push({...common, type: "user", message: {role: "user", content}}); } else if (node.role === "assistant" && content.length) { records.push({...common, type: "assistant", message: {id: `msg_${idMap.get(node.sourceId)?.replaceAll("-", "")}`, type: "message", role: "assistant", model: session.model, content, stop_reason: "end_turn", stop_sequence: null, usage: {input_tokens: 0, output_tokens: 0}}}); } } if (session.name) records.push({type: "custom-title", customTitle: session.name, sessionId}); return jsonl(records); } function ompContent(node: TransferNode) { const content: JsonRecord[] = []; for (const part of node.parts) { if (part.type === "text" && typeof part.text === "string") content.push({type: "text", text: part.text}); if (part.type === "reasoning" && typeof part.text === "string") content.push({type: "thinking", thinking: part.text}); if (part.type === "tool-call") content.push({type: "toolCall", id: stringValue(part.id, crypto.randomUUID()), name: stringValue(part.name, "unknown"), arguments: isRecord(part.arguments) ? part.arguments : {}}); } return content; } function serializeOmp(document: TransferDocument) { const session = document.sessions[0]; if (!session) throw new Error("没有可导出的会话"); const idMap = new Map(document.nodes.map((node, index) => [node.sourceId, `xt${(index + 1).toString(36).padStart(6, "0")}`])); const records: JsonRecord[] = [{type: "session", version: 3, id: portableUuid(session.sourceId), timestamp: session.createdAt, cwd: "/workspace", title: session.name, titleSource: "user"}]; let modelParent: string | null = null; if (session.model) { modelParent = "xtmodel0"; records.push({type: "model_change", id: modelParent, parentId: null, timestamp: session.createdAt, model: `${session.providerId}/${session.model}`}); } for (const node of document.nodes) { const parentId = node.parentSourceId ? idMap.get(node.parentSourceId) || null : modelParent; const toolResult = node.parts.find((part) => part.type === "tool-result"); if (toolResult) { records.push({type: "message", id: idMap.get(node.sourceId), parentId, timestamp: node.createdAt, message: {role: "toolResult", toolCallId: stringValue(toolResult.toolCallId), toolName: stringValue(toolResult.toolName, "unknown"), content: toolResult.content ?? "", isError: toolResult.isError === true, timestamp: Date.parse(node.createdAt)}}); } else { records.push({type: "message", id: idMap.get(node.sourceId), parentId, timestamp: node.createdAt, message: {role: node.role, content: ompContent(node), ...(node.role === "assistant" ? {api: "openai-responses", provider: session.providerId, model: session.model, usage: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0}}, stopReason: "stop"} : {}), timestamp: Date.parse(node.createdAt)}}); } } return jsonl(records); } export function currentPath(nodes: TransferNode[], headSourceId: string | null) { const byId = new Map(nodes.map((node) => [node.sourceId, node])); const reversed: TransferNode[] = []; const seen = new Set(); let cursor = headSourceId; while (cursor && !seen.has(cursor)) { seen.add(cursor); const node = byId.get(cursor); if (!node) break; reversed.push(node); cursor = node.parentSourceId; } return reversed.reverse(); } export function serializeSessionTransfer(document: TransferDocument, format: Exclude) { if (format === "codex") return serializeCodex(document); if (format === "claude") return serializeClaude(document); return serializeOmp(document); } export function serializeXitengArchive(conversations: Conversation[], objects: StoredChatMessage[], workingItems: WorkingItem[]) { return JSON.stringify({ type: "xiteng-chat-archive", version: 1, exportedAt: new Date().toISOString(), conversations: conversations.map(({messages: _messages, ...conversation}) => conversation), objects, workingItems }, null, 2); } export function conversationTransferDocument(conversation: Conversation, nodes: StoredChatMessage[]): TransferDocument { return { format: "xiteng", sessions: [{sourceId: conversation.id, name: conversation.name, headSourceId: conversation.headMessageId, providerId: conversation.providerId, model: conversation.model, generationSettings: conversation.generationSettings, createdAt: conversation.createdAt, updatedAt: conversation.updatedAt}], nodes: nodes.map((message) => ({sourceId: message.id, parentSourceId: message.parentMessageId, role: message.role, parts: message.parts, origin: message.origin, completion: message.completion, createdAt: message.createdAt, completedAt: message.completedAt, ...(message.metadata ? {metadata: message.metadata} : {})})) }; }