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
+72 -2
View File
@@ -234,8 +234,12 @@ function buildThreadUpsertSql(records, target, cols) {
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)) {
// 【增强】model_provider 强制更新为目标 provider(切账号核心逻辑)
if (c === "model_provider") {
updateSet.push(`${c}=${sql(target)}`);
} else 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`);
@@ -252,6 +256,7 @@ function buildThreadUpsertSql(records, target, cols) {
`ON CONFLICT(id) DO UPDATE SET ${updateSet.join(",")};`
);
}
// 【增强】确保数据库中所有会话的 provider 都更新为目标(兜底逻辑)
lines.push(`UPDATE threads SET model_provider=${sql(target)} WHERE COALESCE(model_provider,'')<>${sql(target)};`);
lines.push("COMMIT;");
return lines.join("\n") + "\n";
@@ -288,6 +293,59 @@ function ts() {
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
}
// 【增强】重建 session_index.jsonl:确保所有会话都在索引中(切账号后必要)
function rebuildSessionIndex(records, log = () => {}) {
if (!records || !records.length) {
log("索引重建:无会话记录,跳过");
return { rebuilt: 0 };
}
// 备份旧索引
if (fs.existsSync(SESSION_INDEX)) {
const backup = SESSION_INDEX + ".bak-" + ts();
try {
fs.copyFileSync(SESSION_INDEX, backup);
log(`已备份旧索引 → ${path.basename(backup)}`);
} catch (e) {
log(`索引备份失败:${e.message || e}`);
}
}
// 读取现有索引,保留用户重命名的标题
const existingTitles = {};
try {
const lines = fs.readFileSync(SESSION_INDEX, "utf-8").split("\n");
for (const line of lines) {
if (!line.trim()) continue;
try {
const d = JSON.parse(line);
if (d.id && d.thread_name) existingTitles[d.id] = d.thread_name;
} catch {}
}
} catch {}
// 重建索引:每个会话一行 JSON
const lines = [];
for (const r of records) {
if (!r.id) continue;
const indexEntry = {
id: r.id,
thread_name: existingTitles[r.id] || r.title || "(无标题对话)",
project_path: r.cwd || null,
created_at: new Date(r.created_at_ms).toISOString(),
source: r.source || "vscode",
archived: r.archived ? true : false,
};
lines.push(JSON.stringify(indexEntry));
}
// 写入新索引
try {
fs.writeFileSync(SESSION_INDEX, lines.join("\n") + "\n", "utf-8");
log(`索引重建完成:${lines.length} 条会话`);
return { rebuilt: lines.length };
} catch (e) {
log(`索引重建失败:${e.message || e}`);
return { rebuilt: 0, error: e.message || String(e) };
}
}
async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) {
if (codexRunning()) throw new Error("检测到 Codex 正在运行,请先完全退出 Codex 再同步。");
const target = targetProvider();
@@ -394,6 +452,11 @@ async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) {
}
}
log(`数据库更新:provider ${provRows} 行、has_user_event ${ueRows} 行、新增线程 ${insertedRows} 行、项目索引 +${projectState.addedProjects}`);
// 【增强】4) 重建 session_index.jsonl,确保所有会话都在索引中
onProgress({ phase: "重建索引", done: 0, total: 0 });
const indexResult = rebuildSessionIndex(records, log);
onProgress({ phase: "完成", done: 0, total: 0 });
return {
@@ -407,6 +470,7 @@ async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) {
insertedRows,
projectRows: projectState.addedProjects,
projectlessRemoved: projectState.removedProjectless,
indexRebuilt: indexResult.rebuilt || 0,
backup,
};
}
@@ -457,6 +521,11 @@ async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) {
}
}
log(`数据库更新:provider ${provRows} 行、has_user_event ${ueRows} 行、cwd ${cwdRows} 行`);
// 【增强】4) 重建 session_index.jsonl,确保所有会话都在索引中
onProgress({ phase: "重建索引", done: 0, total: 0 });
const indexResult = rebuildSessionIndex(records, log);
onProgress({ phase: "完成", done: 0, total: 0 });
return {
@@ -470,6 +539,7 @@ async function syncProviderToCurrent(log = () => {}, onProgress = () => {}) {
insertedRows: 0,
projectRows: projectState.addedProjects,
projectlessRemoved: projectState.removedProjectless,
indexRebuilt: indexResult.rebuilt || 0,
backup,
};
}