feat: enhance session sync and add voice input feature

- 增强会话同步:确保所有会话完全同步到新账号
  - 强制更新 model_provider 字段
  - 添加 session_index.jsonl 重建功能
  - 确保数据库 UPSERT 逻辑正确处理所有情况
- 新增语音输入功能(voice_cdp.js, voice_inject.js)
- 测试脚本和增强模块

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
mac
2026-07-16 19:16:45 +08:00
parent a6207fdefd
commit 02fb553c9e
19 changed files with 1884 additions and 95 deletions
+187
View File
@@ -0,0 +1,187 @@
// session_sync_enhanced.js —— 增强版会话同步:确保本地所有会话一个不剩地同步到新 Codex 账号。
// 在 provider_sync.js 的基础上增强,确保:
// 1. 扫描所有 sessions/ 和 archived_sessions/ 下的会话
// 2. 强制插入到当前账号的数据库(UPSERT 逻辑)
// 3. 重建 session_index.jsonl
// 4. 更新全局项目索引
// 5. 提供详细的同步报告
const fs = require("fs");
const path = require("path");
const os = require("os");
const providerSync = require("./provider_sync");
const CODEX_HOME = path.join(os.homedir(), ".codex");
const SESSION_INDEX = path.join(CODEX_HOME, "session_index.jsonl");
// 完整同步方案:确保所有本地会话都进入新账号
async function syncAllSessionsToNewAccount(opts = {}) {
const log = opts.log || (() => {});
const onProgress = opts.onProgress || (() => {});
log("=== 开始全量会话同步 ===");
log("目标:将本地所有会话同步到当前 Codex 账号");
// 1. 检查 Codex 是否在运行
if (providerSync.codexRunning()) {
throw new Error("检测到 Codex 正在运行,请先完全退出 Codex 再同步。");
}
// 2. 获取当前 provider(从 config.toml)
const targetProvider = providerSync.targetProvider();
log(`当前账号 provider:${targetProvider}`);
// 3. 执行核心同步(provider_sync 已经做了 UPSERT)
log("开始同步本地会话到数据库...");
const result = await providerSync.syncProviderToCurrent(log, onProgress);
// 4. 重建 session_index.jsonl(确保索引完整)
log("重建会话索引...");
const indexResult = rebuildSessionIndex(log);
// 5. 生成同步报告
const report = {
success: true,
provider: result.target,
totalSessions: result.totalConversations,
syncedToDatabase: result.insertedRows,
updatedProvider: result.provRows,
updatedUserEvent: result.ueRows,
indexRebuilt: indexResult.total,
projects: result.projectRows,
backup: result.backup,
details: {
changedFiles: result.changedFiles,
userEventSessions: result.userEvents,
archivedSessions: indexResult.archived,
activeSessions: indexResult.active,
}
};
log("\n=== 同步完成 ===");
log(`✅ 总会话数:${report.totalSessions}`);
log(`✅ 新增到数据库:${report.syncedToDatabase} 条`);
log(`✅ 更新 provider:${report.updatedProvider} 条`);
log(`✅ 重建索引:${report.indexRebuilt} 条`);
log(`✅ 项目分组:${report.projects} 个`);
log(`✅ 备份位置:${report.backup}`);
log("\n现在可以启动 Codex,所有会话应该都能看到了。");
return report;
}
// 重建 session_index.jsonl:扫描所有会话文件,生成完整索引
function rebuildSessionIndex(log = () => {}) {
const sessions = require("./sessions");
// 列出所有会话(包括 archived)
const allConversations = sessions.listConversations({
includeSubagent: true,
includeArchived: true,
});
log(`扫描到 ${allConversations.length} 个会话文件`);
// 备份旧索引
if (fs.existsSync(SESSION_INDEX)) {
const backup = SESSION_INDEX + ".bak-" + Date.now();
fs.copyFileSync(SESSION_INDEX, backup);
log(`已备份旧索引:${path.basename(backup)}`);
}
// 重建索引:每个会话一行 JSON
const lines = [];
let active = 0;
let archived = 0;
for (const conv of allConversations) {
if (!conv.id) continue;
const indexEntry = {
id: conv.id,
thread_name: conv.title || "(无标题对话)",
project_path: conv.cwd || null,
created_at: conv.ts || new Date().toISOString(),
source: conv.source || "vscode",
archived: conv.archived || false,
};
lines.push(JSON.stringify(indexEntry));
if (conv.archived) archived++;
else active++;
}
// 写入新索引
fs.writeFileSync(SESSION_INDEX, lines.join("\n") + "\n", "utf-8");
log(`已重建索引:活跃 ${active} 条,归档 ${archived} 条`);
return { total: lines.length, active, archived };
}
// 诊断:检查会话同步状态
function diagnoseSessionSync(log = () => {}) {
const sessions = require("./sessions");
log("=== 会话同步诊断 ===");
// 1. 统计本地会话文件
const allConversations = sessions.listConversations({
includeSubagent: true,
includeArchived: true,
});
const activeSessions = allConversations.filter(c => !c.archived);
const archivedSessions = allConversations.filter(c => c.archived);
log(`本地会话文件:${allConversations.length} 个`);
log(` - 活跃:${activeSessions.length} 个`);
log(` - 归档:${archivedSessions.length} 个`);
// 2. 检查数据库
const dbFiles = fs.readdirSync(CODEX_HOME).filter(f => /^state_.*\.sqlite$/.test(f));
log(`数据库文件:${dbFiles.length} 个`);
for (const db of dbFiles) {
log(` - ${db}`);
}
// 3. 检查索引文件
const indexExists = fs.existsSync(SESSION_INDEX);
log(`session_index.jsonl:${indexExists ? "存在" : "不存在"}`);
if (indexExists) {
const indexContent = fs.readFileSync(SESSION_INDEX, "utf-8");
const indexLines = indexContent.split("\n").filter(l => l.trim());
log(` - 索引条目:${indexLines.length} 条`);
}
// 4. 检查 provider
const currentProvider = providerSync.targetProvider();
log(`当前 provider:${currentProvider}`);
// 5. 检查会话文件中的 provider 分布
const providerStats = {};
for (const conv of allConversations) {
const p = conv.provider || "(未设置)";
providerStats[p] = (providerStats[p] || 0) + 1;
}
log("会话文件 provider 分布:");
for (const [p, count] of Object.entries(providerStats)) {
log(` - ${p}: ${count} 个`);
}
return {
totalSessions: allConversations.length,
activeSessions: activeSessions.length,
archivedSessions: archivedSessions.length,
databases: dbFiles.length,
indexExists,
currentProvider,
providerStats,
};
}
module.exports = {
syncAllSessionsToNewAccount,
rebuildSessionIndex,
diagnoseSessionSync,
};