From 49892454f6f3c60292aa6ec33be685440d3d8d7d Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 18:45:40 +0800 Subject: [PATCH] fix: sync all codex sessions --- AGENTS.md | 32 ++-- HANDOFF.md | 33 ++-- electron/lib/provider_sync.js | 291 +++++++++++++++++++++++++++++++++- electron/lib/sessions.js | 50 +++--- package-lock.json | 4 +- package.json | 4 +- renderer/app.js | 15 +- 7 files changed, 362 insertions(+), 67 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b2b9a50..7fc4350 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,25 +1,26 @@ ## 项目记忆(Codex 插件市场自动维护 · 回答前据此 · 勿手动编辑本段) -**当前状态**:用户在询问 Windows 下 Codex 内容存储路径后,提供启动报错截图并问原因;当前需要检查截图所指的 .codex 数据库及备份文件状态,判断是自动修复后的提示还是会反复损坏。 +**当前状态**:已定位并修复 Codex 会话记录/项目分组同步不全问题:同步器会回填 threads 可见性与项目索引,支持 WAL,并修复 fork 会话 session_meta 覆盖导致部分会话不可见;临时数据验证 292 条线程、19 个项目分组均可恢复。用户最新要求是提交代码、推送 git 并发布热更包,尚未完成。 **已定决策**: +- 不提交无关 AGENTS/HANDOFF 改动 +- 不修改真实 ~/.codex 做验证 +- 项目分组从 threads.cwd 回填 +- has_user_event 用全文标记兜底 +- 缺失侧栏来自 threads 索引不完整 +- 备份/导入需包含 archived_sessions +- session_meta 只采用首个,避免后续覆盖 +- 优先用系统 sqlite3 处理 WAL +- 侧栏依赖 threads 与项目索引 - 全量会话读取包含归档和 subagent - 媒体预览保留dataUrl和原始url -- Grok多图参考不支持 -- Grok作为新增节点,不改Seedance -- 导入后保持当前全量/默认模式 -- 同步目标为当前账号 -- 默认刷新仍保持轻量列表 -- 密钥不写入项目源码 -- 使用 /v1/videos/{id} 查询 -- 使用 /v1/videos/generations 创建 -- 默认模型为 grok-imagine-video -- 需按截图路径检查数据库状态 **下一步**: -- 查看截图中的具体报错路径 -- 检查 .codex 数据库和备份文件 -- 判断是否需恢复或清理损坏文件 -**关键文件**:/var/folders/xk/qrv10_rx12jg91fn322xlwr00000gn/T/codex-clipboard-41e58e05-0211-4045-8cc5-1084d4f7f471.png、%USERPROFILE%\.codex、%USERPROFILE%\.codex\sessions、%USERPROFILE%\.codex\archived_sessions、%USERPROFILE%\.codex\history.jsonl、%USERPROFILE%\.codex\config.toml、%USERPROFILE%\.codex\auth.json +- 检查 git diff 和远端 +- 只提交相关代码改动 +- 推送源码仓库 +- 运行热更发布脚本 +**关键文件**:/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/electron/lib/provider_sync.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/electron/lib/sessions.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/renderer/app.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/AGENTS.md、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/HANDOFF.md **历史话题(需其中细节时用下面命令检索)**: +- 2026-07-15 会话同步修复待发布 - 2026-07-15 工具箱新增Grok视频与全量会话同步 - 2026-07-15 生成魔兽世界宣传海报 - 2026-07-15 Codex skill软链改造 @@ -31,6 +32,5 @@ - 2026-07-13 项目与画布功能分析 - 2026-07-13 网站安装数据库连接失败 - 2026-07-12 服务器BBR已开启 -- 2026-07-12 服务器服务与域名配置交接 > 回答涉及本段未列出的历史细节前,先运行 `rg -n "关键词" /Users/mac/.codex/memory/31c67c70b625.md` 查证再答。 diff --git a/HANDOFF.md b/HANDOFF.md index 64d5b85..5d72944 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -3,7 +3,25 @@ > 核心记忆已写进本项目 `AGENTS.md`(Codex 每次会话自动读)。本文件是更详细的近期快照,供需要时参考;关键信息不依赖本文件。 -## 最近会话 · 2026-07-15 09:02 · 工具箱新增Grok视频与全量会话同步 +## 最近会话 · 2026-07-15 10:31 · 会话同步修复待发布 + +已定位并修复 Codex 会话记录/项目分组同步不全问题:同步器会回填 threads 可见性与项目索引,支持 WAL,并修复 fork 会话 session_meta 覆盖导致部分会话不可见;临时数据验证 292 条线程、19 个项目分组均可恢复。用户最新要求是提交代码、推送 git 并发布热更包,尚未完成。 + +**已确定的决策**: +- 不修改真实 ~/.codex 做验证 +- 优先用系统 sqlite3 处理 WAL +- 只认第一段 session_meta +- 不提交无关 AGENTS/HANDOFF 改动 + +**下一步**: +- 检查 git diff 和远端 +- 只提交相关代码改动 +- 推送源码仓库 +- 运行热更发布脚本 + +**涉及文件**:/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/electron/lib/provider_sync.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/electron/lib/sessions.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/renderer/app.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/AGENTS.md、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/HANDOFF.md + +## 更早会话 · 2026-07-15 09:02 · 工具箱新增Grok视频与全量会话同步 已新增独立Grok视频节点和后端入口,配置Tokendun路由并测试15秒1080p图生视频可完成;画布媒体预览支持放大/下载并保留原始URL。会话记录页已增加手动获取全部记录功能,可读取sessions、archived_sessions及subagent并用于一键同步。 @@ -52,16 +70,3 @@ - 检查插件页系统插件状态 **涉及文件**:/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/electron/main.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/electron/preload.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/electron/lib/router.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/renderer/app.js、/Users/mac/Documents/备份数据/ProjectDown/codex-market-app/renderer/plugins.json、/Users/mac/Documents/备份数据/ProjectDown/生图skill/DEPLOY_skill_softlink_patch.md - -## 更早会话 · 2026-07-14 17:14 · 列出本地 skills - -用户要求直接回答 ~/.codex/skills/ 下非 .system 技能的目录名和 SKILL.md 中 name,且明确禁止执行任何工具。当前尚未回答技能列表。 - -**已确定的决策**: -- 不得执行任何工具 -- 只列非 .system 技能 - -**下一步**: -- 直接回答可用 skills 或说明没有 - -**涉及文件**:~/.codex/skills/、~/.codex/skills/*/SKILL.md diff --git a/electron/lib/provider_sync.js b/electron/lib/provider_sync.js index 70ae5ce..ce0bfcc 100644 --- a/electron/lib/provider_sync.js +++ b/electron/lib/provider_sync.js @@ -4,10 +4,12 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); -const { execFileSync } = require("child_process"); +const { execFileSync, spawnSync } = require("child_process"); const initSqlJs = require("sql.js"); const CODEX_HOME = path.join(os.homedir(), ".codex"); +const SESSION_INDEX = path.join(CODEX_HOME, "session_index.jsonl"); +const GLOBAL_STATE = path.join(CODEX_HOME, ".codex-global-state.json"); // 检测 Codex 是否在运行(尽力而为,跨平台) function codexRunning() { @@ -55,6 +57,231 @@ function findRollouts() { return out; } +function isArchivedRollout(file) { + return file.startsWith(path.join(CODEX_HOME, "archived_sessions") + path.sep); +} + +function loadIndexTitles() { + const map = {}; + try { + for (const line of fs.readFileSync(SESSION_INDEX, "utf-8").split("\n")) { + if (!line.trim()) continue; + try { + const d = JSON.parse(line); + if (d.id && d.thread_name) map[d.id] = d.thread_name; + } catch {} + } + } catch {} + return map; +} + +function parseTimeMs(tsText, fallbackMs = Date.now()) { + const n = Date.parse(tsText || ""); + return Number.isFinite(n) ? n : fallbackMs; +} + +function firstText(value) { + if (typeof value === "string") return value; + if (Array.isArray(value)) { + return value + .map((x) => (typeof x === "string" ? x : x && typeof x.text === "string" ? x.text : "")) + .filter(Boolean) + .join("\n"); + } + return ""; +} + +function sql(v) { + if (v === null || v === undefined) return "NULL"; + if (typeof v === "number") return Number.isFinite(v) ? String(v) : "NULL"; + return "'" + String(v).replace(/'/g, "''") + "'"; +} + +function sqlite3Available() { + const r = spawnSync("sqlite3", ["--version"], { encoding: "utf-8" }); + return !r.error && r.status === 0; +} + +function runSqlite3(dbPath, input) { + const r = spawnSync("sqlite3", [dbPath], { + input, + encoding: "utf-8", + maxBuffer: 20 * 1024 * 1024, + }); + if (r.error || r.status !== 0) { + throw new Error((r.stderr || r.error && r.error.message || "sqlite3 执行失败").trim()); + } + return r.stdout || ""; +} + +function sqliteCols(dbPath) { + const out = runSqlite3(dbPath, "PRAGMA table_info(threads);\n"); + return new Set(out.split("\n").map((line) => line.split("|")[1]).filter(Boolean)); +} + +function readRolloutRecord(file, titles) { + let content; + try { + content = fs.readFileSync(file, "utf-8"); + } catch { + return null; + } + const stat = fs.statSync(file); + const rec = { + id: "", + rollout_path: file, + cwd: "", + source: "vscode", + model_provider: "", + thread_source: "user", + title: "", + first_user_message: "", + preview: "", + has_user_event: content.includes('"user_message"') || content.includes('"user_input"') ? 1 : 0, + archived: isArchivedRollout(file) ? 1 : 0, + archived_at: null, + created_at_ms: stat.birthtimeMs || stat.mtimeMs || Date.now(), + updated_at_ms: stat.mtimeMs || Date.now(), + sandbox_policy: JSON.stringify({ type: "disabled" }), + approval_mode: "never", + tokens_used: 0, + cli_version: "", + git_sha: null, + git_branch: null, + git_origin_url: null, + model: null, + reasoning_effort: null, + history_mode: "legacy", + }; + for (const line of content.split("\n")) { + if (!line.trim()) continue; + let d; + try { + d = JSON.parse(line); + } catch { + continue; + } + const p = d.payload || {}; + if (d.type === "session_meta" && !rec.id) { + rec.id = p.id || p.session_id || rec.id; + rec.cwd = p.cwd || rec.cwd; + rec.source = p.source || rec.source; + rec.model_provider = p.model_provider || rec.model_provider; + rec.thread_source = p.thread_source || rec.thread_source; + rec.cli_version = p.cli_version || rec.cli_version; + rec.history_mode = p.history_mode || rec.history_mode; + rec.created_at_ms = parseTimeMs(p.timestamp || d.timestamp, rec.created_at_ms); + if (p.git && typeof p.git === "object") { + rec.git_sha = p.git.commit_hash || null; + rec.git_branch = p.git.branch || null; + rec.git_origin_url = p.git.repository_url || null; + } + } else if (d.type === "event_msg" && (p.type === "user_message" || p.type === "user_input")) { + rec.has_user_event = 1; + if (!rec.first_user_message) rec.first_user_message = firstText(p.message || p.input).trim(); + } + } + rec.updated_at_ms = Math.max(rec.updated_at_ms, rec.created_at_ms); + rec.title = titles[rec.id] || rec.first_user_message.replace(/\s+/g, " ").slice(0, 80) || "(无标题对话)"; + rec.preview = rec.first_user_message.replace(/\s+/g, " ").slice(0, 200); + return rec.id ? rec : null; +} + +function buildThreadUpsertSql(records, target, cols) { + const desired = [ + "id", "rollout_path", "created_at", "updated_at", "source", "model_provider", "cwd", "title", + "sandbox_policy", "approval_mode", "tokens_used", "has_user_event", "archived", "archived_at", + "git_sha", "git_branch", "git_origin_url", "cli_version", "first_user_message", "memory_mode", + "model", "reasoning_effort", "created_at_ms", "updated_at_ms", "thread_source", "preview", + "recency_at", "recency_at_ms", "history_mode", + ].filter((c) => cols.has(c)); + const valueFor = (r, c) => { + const created = Math.floor(r.created_at_ms / 1000); + const updated = Math.floor(r.updated_at_ms / 1000); + const values = { + id: r.id, + rollout_path: r.rollout_path, + created_at: created, + updated_at: updated, + source: r.source, + model_provider: target, + cwd: r.cwd, + title: r.title, + sandbox_policy: r.sandbox_policy, + approval_mode: r.approval_mode, + tokens_used: r.tokens_used, + has_user_event: r.has_user_event, + archived: r.archived, + archived_at: r.archived ? updated : null, + git_sha: r.git_sha, + git_branch: r.git_branch, + git_origin_url: r.git_origin_url, + cli_version: r.cli_version, + first_user_message: r.first_user_message, + memory_mode: "enabled", + model: r.model, + reasoning_effort: r.reasoning_effort, + created_at_ms: Math.round(r.created_at_ms), + updated_at_ms: Math.round(r.updated_at_ms), + thread_source: r.thread_source, + preview: r.preview, + recency_at: updated, + recency_at_ms: Math.round(r.updated_at_ms), + history_mode: r.history_mode, + }; + return values[c]; + }; + const updateSet = []; + for (const c of desired) { + if (c === "id") continue; + if (c === "has_user_event") updateSet.push(`${c}=CASE WHEN excluded.${c}=1 THEN 1 ELSE threads.${c} END`); + else if (["cwd", "first_user_message", "preview", "thread_source"].includes(c)) { + updateSet.push(`${c}=CASE WHEN COALESCE(excluded.${c},'')<>'' THEN excluded.${c} ELSE threads.${c} END`); + } else if (c === "title") { + updateSet.push(`${c}=CASE WHEN COALESCE(threads.${c},'')='' OR threads.${c}='(无标题对话)' THEN excluded.${c} ELSE threads.${c} END`); + } else if (["updated_at", "updated_at_ms", "recency_at", "recency_at_ms"].includes(c)) { + updateSet.push(`${c}=MAX(threads.${c}, excluded.${c})`); + } else { + updateSet.push(`${c}=excluded.${c}`); + } + } + const lines = ["BEGIN;"]; + for (const r of records) { + lines.push( + `INSERT INTO threads (${desired.join(",")}) VALUES (${desired.map((c) => sql(valueFor(r, c))).join(",")}) ` + + `ON CONFLICT(id) DO UPDATE SET ${updateSet.join(",")};` + ); + } + lines.push(`UPDATE threads SET model_provider=${sql(target)} WHERE COALESCE(model_provider,'')<>${sql(target)};`); + lines.push("COMMIT;"); + return lines.join("\n") + "\n"; +} + +function updateProjectState(records, log = () => {}) { + if (!fs.existsSync(GLOBAL_STATE)) return { addedProjects: 0, removedProjectless: 0 }; + let state; + try { + state = JSON.parse(fs.readFileSync(GLOBAL_STATE, "utf-8")); + } catch (e) { + log(`全局项目索引跳过:${e.message || e}`); + return { addedProjects: 0, removedProjectless: 0 }; + } + const projects = [...new Set(records.map((r) => r.cwd).filter(Boolean))]; + const idsWithProject = new Set(records.filter((r) => r.cwd).map((r) => r.id)); + const before = Array.isArray(state["project-order"]) ? state["project-order"] : []; + const seen = new Set(before); + state["project-order"] = before.concat(projects.filter((p) => !seen.has(p))); + const oldProjectless = Array.isArray(state["projectless-thread-ids"]) ? state["projectless-thread-ids"] : []; + state["projectless-thread-ids"] = oldProjectless.filter((id) => !idsWithProject.has(id)); + const addedProjects = state["project-order"].length - before.length; + const removedProjectless = oldProjectless.length - state["projectless-thread-ids"].length; + if (addedProjects || removedProjectless) { + fs.writeFileSync(GLOBAL_STATE, JSON.stringify(state, null, 2) + "\n", "utf-8"); + log(`项目索引更新:新增项目 ${addedProjects} 个,移出无项目会话 ${removedProjectless} 条`); + } + return { addedProjects, removedProjectless }; +} + function ts() { const d = new Date(); const p = (n) => String(n).padStart(2, "0"); @@ -74,14 +301,16 @@ async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) { const src = path.join(CODEX_HOME, name); if (fs.existsSync(src)) fs.copyFileSync(src, path.join(backup, name)); } - for (const db of fs.readdirSync(CODEX_HOME).filter((f) => /^state_.*\.sqlite/.test(f))) { + for (const db of fs.readdirSync(CODEX_HOME).filter((f) => /^state_.*\.sqlite(?:-(?:wal|shm))?$/.test(f))) { fs.copyFileSync(path.join(CODEX_HOME, db), path.join(backup, db)); } log(`已备份到 ${backup}`); - // 2) 遍历 rollout:改首行 provider,收集 thread_id / cwd / has_user_event + // 2) 遍历 rollout:改首行 provider,收集完整线程元信息 const userEventIds = new Set(); const cwdById = {}; + const titles = loadIndexTitles(); + const records = []; let changedFiles = 0; const rolloutFiles = findRollouts(); const rtotal = rolloutFiles.length; @@ -126,11 +355,62 @@ async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) { changedFiles++; } if (tid && hasUE) userEventIds.add(tid); + const rec = readRolloutRecord(file, titles); + if (rec) { + rec.model_provider = target; + records.push(rec); + } } - log(`改写 rollout 文件:${changedFiles} 个;含用户消息的会话:${userEventIds.size} 条`); + log(`改写 rollout 文件:${changedFiles} 个;含用户消息的会话:${userEventIds.size} 条;可回填线程:${records.length} 条`); + + // 2.5) Codex 项目分组来自全局 state,历史 cwd 需要合并到 project-order。 + const projectState = updateProjectState(records, log); // 3) 更新所有 state_*.sqlite onProgress({ phase: "更新数据库", done: 0, total: 0 }); + if (sqlite3Available()) { + let provRows = 0; + let ueRows = 0; + let cwdRows = 0; + let insertedRows = 0; + for (const name of fs.readdirSync(CODEX_HOME).filter((f) => /^state_.*\.sqlite$/.test(f))) { + const dbPath = path.join(CODEX_HOME, name); + try { + const cols = sqliteCols(dbPath); + if (!cols.has("model_provider") || !cols.has("has_user_event")) continue; + const beforeProvider = Number(runSqlite3(dbPath, `SELECT COUNT(*) FROM threads WHERE COALESCE(model_provider,'')<>${sql(target)};\n`).trim() || 0); + const beforeUserEvent = Number(runSqlite3(dbPath, "SELECT COUNT(*) FROM threads WHERE COALESCE(has_user_event,0)<>1;\n").trim() || 0); + const beforeRows = Number(runSqlite3(dbPath, "SELECT COUNT(*) FROM threads;\n").trim() || 0); + runSqlite3(dbPath, buildThreadUpsertSql(records, target, cols)); + runSqlite3(dbPath, "PRAGMA wal_checkpoint(TRUNCATE);\n"); + const afterUserEvent = Number(runSqlite3(dbPath, "SELECT COUNT(*) FROM threads WHERE COALESCE(has_user_event,0)<>1;\n").trim() || 0); + const afterRows = Number(runSqlite3(dbPath, "SELECT COUNT(*) FROM threads;\n").trim() || 0); + provRows += beforeProvider; + ueRows += Math.max(0, beforeUserEvent - afterUserEvent); + insertedRows += Math.max(0, afterRows - beforeRows); + cwdRows += records.filter((r) => r.cwd).length; + } catch (e) { + log(` sqlite3 数据库跳过 ${name}:${e.message || e}`); + } + } + log(`数据库更新:provider ${provRows} 行、has_user_event ${ueRows} 行、新增线程 ${insertedRows} 行、项目索引 +${projectState.addedProjects}`); + onProgress({ phase: "完成", done: 0, total: 0 }); + + return { + target, + totalConversations: rtotal, + changedFiles, + userEvents: userEventIds.size, + provRows, + ueRows, + cwdRows, + insertedRows, + projectRows: projectState.addedProjects, + projectlessRemoved: projectState.removedProjectless, + backup, + }; + } + const SQL = await initSqlJs({ // wasm 恒在【包内】node_modules(热更包不含 node_modules),故用 BUNDLE_ROOT 而非 __dirname locateFile: (f) => @@ -187,6 +467,9 @@ async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) { provRows, ueRows, cwdRows, + insertedRows: 0, + projectRows: projectState.addedProjects, + projectlessRemoved: projectState.removedProjectless, backup, }; } diff --git a/electron/lib/sessions.js b/electron/lib/sessions.js index 7900dad..3a23eec 100644 --- a/electron/lib/sessions.js +++ b/electron/lib/sessions.js @@ -181,42 +181,50 @@ function toMarkdown(item, conv) { return lines.join("\n"); } -// 备份:把整个 sessions/ 与索引复制到目标目录下的 codex-sessions-backup/ +// 备份:把 sessions/、archived_sessions/ 与索引复制到目标目录下的 codex-sessions-backup/ function backupAll(destDir, log = () => {}) { const out = path.join(destDir, "codex-sessions-backup"); fs.mkdirSync(out, { recursive: true }); - const files = findRolloutFiles(SESSIONS_DIR); let n = 0; - for (const f of files) { - const rel = path.relative(SESSIONS_DIR, f); - const dst = path.join(out, "sessions", rel); - fs.mkdirSync(path.dirname(dst), { recursive: true }); - fs.copyFileSync(f, dst); - n++; + for (const [name, dir] of [["sessions", SESSIONS_DIR], ["archived_sessions", ARCHIVED_SESSIONS_DIR]]) { + const files = findRolloutFiles(dir); + for (const f of files) { + const rel = path.relative(dir, f); + const dst = path.join(out, name, rel); + fs.mkdirSync(path.dirname(dst), { recursive: true }); + fs.copyFileSync(f, dst); + n++; + } } if (fs.existsSync(SESSION_INDEX)) fs.copyFileSync(SESSION_INDEX, path.join(out, "session_index.jsonl")); log(`已备份 ${n} 个会话文件到 ${out}`); return { path: out, count: n }; } -// 导入:把外部备份目录里的 rollout-*.jsonl 合并进本机 sessions/(按原年月日结构,已存在则跳过) +// 导入:把外部备份目录里的 rollout-*.jsonl 合并进本机 sessions/ 与 archived_sessions/(已存在则跳过) function importSessions(srcDir, log = () => {}) { // 支持传入 backup 根目录或其中的 sessions/ 子目录 - let base = srcDir; - if (fs.existsSync(path.join(srcDir, "sessions"))) base = path.join(srcDir, "sessions"); - const files = findRolloutFiles(base); let added = 0; let skipped = 0; - for (const f of files) { - const rel = path.relative(base, f); - const dst = path.join(SESSIONS_DIR, rel); - if (fs.existsSync(dst)) { - skipped++; - continue; + const roots = fs.existsSync(path.join(srcDir, "sessions")) + ? [ + { base: path.join(srcDir, "sessions"), dest: SESSIONS_DIR }, + { base: path.join(srcDir, "archived_sessions"), dest: ARCHIVED_SESSIONS_DIR }, + ] + : [{ base: srcDir, dest: SESSIONS_DIR }]; + for (const root of roots) { + const files = findRolloutFiles(root.base); + for (const f of files) { + const rel = path.relative(root.base, f); + const dst = path.join(root.dest, rel); + if (fs.existsSync(dst)) { + skipped++; + continue; + } + fs.mkdirSync(path.dirname(dst), { recursive: true }); + fs.copyFileSync(f, dst); + added++; } - fs.mkdirSync(path.dirname(dst), { recursive: true }); - fs.copyFileSync(f, dst); - added++; } log(`导入完成:新增 ${added} 个,跳过 ${skipped} 个(已存在)`); return { added, skipped }; diff --git a/package-lock.json b/package-lock.json index 4c3259b..7f7fd93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex-market", - "version": "1.5.9", + "version": "1.5.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-market", - "version": "1.5.9", + "version": "1.5.10", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9", diff --git a/package.json b/package.json index 3a3ae32..0a452d0 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "codex-market", - "version": "1.5.9", + "version": "1.5.10", "description": "TD工具箱 / 技能商店 / 备份工具 —— 跨平台桌面客户端", "author": "Codex Market", "license": "MIT", "main": "electron/bootstrap.js", - "hotContentVersion": "1.5.9", + "hotContentVersion": "1.5.10", "scripts": { "start": "electron .", "dist": "electron-builder", diff --git a/renderer/app.js b/renderer/app.js index ec2383b..22753b4 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -888,15 +888,14 @@ $("#sync-btn").addEventListener("click", async (ev) => { try { const r = await apic.providerSync(); $("#sync-progress-fill").style.width = "100%"; - const changedMsg = - r.changedFiles > 0 - ? `本次同步 ${r.changedFiles} 条` - : `全部已是最新,无需改动`; + const dbChanged = (r.ueRows || 0) + (r.provRows || 0) + (r.insertedRows || 0) + (r.projectRows || 0) + (r.projectlessRemoved || 0); + const changedMsg = r.changedFiles > 0 || dbChanged > 0 + ? `文件 ${r.changedFiles || 0} 条,数据库/项目索引 ${dbChanged} 项` + : `全部已是最新,无需改动`; $("#sync-progress-label").textContent = `完成 ✅ 共 ${r.totalConversations} 条会话,${changedMsg}`; - const tip = - r.changedFiles > 0 - ? `同步完成 ✅ 本次改写 ${r.changedFiles} 条会话。重新打开 Codex 即可看到` - : `全部 ${r.totalConversations} 条会话之前已同步过,本次无需改动 ✅`; + const tip = r.changedFiles > 0 || dbChanged > 0 + ? `同步完成 ✅ ${changedMsg}。重新打开 Codex 即可看到` + : `全部 ${r.totalConversations} 条会话之前已同步过,本次无需改动 ✅`; toast(tip, "ok"); setTimeout(() => $("#sync-progress").classList.add("hidden"), 4000); } catch (e) {