import fs from 'node:fs/promises'; import path from 'node:path'; import { CODEXMOBILE_STATE_DIR, statePath } from './runtime-paths.js'; const STATE_DIR = CODEXMOBILE_STATE_DIR; const INDEX_PATH = statePath('mobile-sessions.json'); const MAX_SESSIONS = 300; async function readIndexFile() { try { const raw = await fs.readFile(INDEX_PATH, 'utf8'); const parsed = JSON.parse(raw); return Array.isArray(parsed.sessions) ? parsed.sessions : []; } catch (error) { if (error.code !== 'ENOENT') { console.warn('[mobile-sessions] Failed to read index:', error.message); } return []; } } async function writeIndexFile(sessions) { await fs.mkdir(STATE_DIR, { recursive: true }); const trimmed = sessions .filter((session) => session?.id && session?.projectPath) .sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0)) .slice(0, MAX_SESSIONS); await fs.writeFile( INDEX_PATH, JSON.stringify({ version: 1, sessions: trimmed }, null, 2), 'utf8' ); } function fallbackTitle(title, summary) { const value = String(title || summary || '').trim(); return value ? value.slice(0, 52) : '新对话'; } export async function readMobileSessionIndex() { const sessions = await readIndexFile(); return new Map( sessions .filter((session) => session?.id) .map((session) => [session.id, session]) ); } export async function readMobileSessions() { return readIndexFile(); } export async function readMobileSessionMessages(sessionId) { const sessions = await readIndexFile(); const session = sessions.find((item) => item.id === sessionId); return Array.isArray(session?.messages) ? session.messages : []; } export async function deleteMobileSession(sessionId) { const id = String(sessionId || '').trim(); if (!id) { return false; } const sessions = await readIndexFile(); const next = sessions.filter((session) => session.id !== id); if (next.length === sessions.length) { return false; } await writeIndexFile(next); return true; } export async function registerMobileSession({ id, projectPath, title, summary, updatedAt, messages }) { if (!id || !projectPath || String(id).startsWith('draft-') || String(id).startsWith('codex-')) { return null; } const now = updatedAt || new Date().toISOString(); const sessions = await readIndexFile(); const existingIndex = sessions.findIndex((session) => session.id === id); const existing = existingIndex >= 0 ? sessions[existingIndex] : null; const next = { ...(existing || {}), id, projectPath: path.resolve(projectPath), title: existing?.title || fallbackTitle(title, summary), titleLocked: true, summary: summary || title || existing?.summary || 'CodexMobile 对话', updatedAt: now, source: existing?.source || 'codexmobile' }; if (Array.isArray(messages)) { const seenMessageIds = new Set(); next.messages = messages .filter((message) => message && message.role && typeof message.content === 'string') .map((message) => ({ id: String(message.id || `${id}-${Date.now()}`), role: message.role === 'assistant' ? 'assistant' : 'user', content: message.content, timestamp: message.timestamp || now })) .filter((message) => { if (seenMessageIds.has(message.id)) { return false; } seenMessageIds.add(message.id); return true; }); } if (existingIndex >= 0) { sessions[existingIndex] = next; } else { sessions.push(next); } await writeIndexFile(sessions); return next; } export async function renameMobileSession({ id, projectPath, title, updatedAt }) { const sessionId = String(id || '').trim(); if (!sessionId || !projectPath || sessionId.startsWith('draft-')) { return null; } const sessions = await readIndexFile(); const existingIndex = sessions.findIndex((session) => session.id === sessionId); const existing = existingIndex >= 0 ? sessions[existingIndex] : null; const next = { ...(existing || {}), id: sessionId, projectPath: path.resolve(projectPath), title: fallbackTitle(title), titleLocked: true, summary: existing?.summary, updatedAt: existing?.updatedAt || updatedAt || new Date().toISOString(), source: existing?.source || 'codexmobile' }; if (existingIndex >= 0) { sessions[existingIndex] = next; } else { sessions.push(next); } await writeIndexFile(sessions); return next; }