feat: 发布热更 1.5.94,修复中转历史会话并支持一键清空
This commit is contained in:
+224
-7
@@ -17,6 +17,7 @@ const sessions = require("./lib/sessions");
|
||||
const codexDataDir = require("./lib/codex_data_dir");
|
||||
const providerSync = require("./lib/provider_sync");
|
||||
const router = require("./lib/router");
|
||||
const deeplink = require("./lib/deeplink");
|
||||
const canvas = require("./lib/canvas");
|
||||
const canvasStore = require("./lib/canvas_store");
|
||||
const team = require("./lib/team");
|
||||
@@ -24,6 +25,7 @@ const promptAnalysis = require("./lib/prompt-analysis");
|
||||
const claudedesk = require("./lib/claudedesk");
|
||||
const voice = require("./lib/voice_cdp");
|
||||
const codexDesktop = require("./lib/codex_desktop");
|
||||
const globalDictation = require("./lib/global_dictation");
|
||||
const tokenWatch = require("./lib/token_watch");
|
||||
const proxy = require("./lib/proxy");
|
||||
const memory = require("./lib/memory");
|
||||
@@ -95,6 +97,9 @@ let lastPull = null; // { base, items } —— gh-restore 用上次 pull 的结
|
||||
let voiceSessionDeleteBusy = false;
|
||||
let stateWriteOperation = "";
|
||||
const sessionDeleteClipboard = new Map();
|
||||
let pendingStartupDeepLink = null;
|
||||
let globalDictationCleanupTimer = null;
|
||||
let globalDictationCleanupAttempt = 0;
|
||||
|
||||
async function withStateWriteLock(label, fn) {
|
||||
if (stateWriteOperation) throw new Error(`正在${stateWriteOperation},请完成后再${label}`);
|
||||
@@ -178,6 +183,116 @@ function showWindow() {
|
||||
if (process.platform === "darwin" && app.dock) app.dock.show();
|
||||
}
|
||||
|
||||
function configureGlobalDictation(codexHome) {
|
||||
globalDictation.setEndpointOwnerVerifier((port, options) => codexDesktop.debugPortOwner(port, options));
|
||||
const result = globalDictation.startGuard({ codexHome, log });
|
||||
if (result.changed) log("已关闭 Codex 全局听写栏常驻显示");
|
||||
else if (!result.ok) {
|
||||
log(`关闭 Codex 全局听写栏常驻显示失败:${result.error || result.reason}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function cleanupGlobalDictation(reason = "") {
|
||||
const cdp = await globalDictation.closeGlobalDictationTargets(voice.DEFAULT_PORT);
|
||||
if (cdp.available || !codexDesktop.hideGlobalDictationWindows) {
|
||||
return cdp;
|
||||
}
|
||||
|
||||
const native = await codexDesktop.hideGlobalDictationWindows();
|
||||
if (native && native.hidden) {
|
||||
log(`已将 ChatGPT 全局听写悬浮栏移出屏幕${reason ? `(${reason})` : ""}`);
|
||||
return {
|
||||
...cdp,
|
||||
ok: true,
|
||||
status: "native-hidden",
|
||||
native,
|
||||
closed: native.hidden,
|
||||
remaining: 0,
|
||||
};
|
||||
}
|
||||
return { ...cdp, native };
|
||||
}
|
||||
|
||||
function scheduleGlobalDictationCleanup() {
|
||||
if (globalDictationCleanupTimer) clearTimeout(globalDictationCleanupTimer);
|
||||
globalDictationCleanupTimer = null;
|
||||
globalDictationCleanupAttempt = 0;
|
||||
const delays = [1500, 3000, 6000, 12000, 24000];
|
||||
|
||||
const run = async () => {
|
||||
globalDictationCleanupTimer = null;
|
||||
const result = await cleanupGlobalDictation("启动清理");
|
||||
if (result.status === "closed" || result.status === "native-hidden" || result.status === "not-found") {
|
||||
if (result.closed) log(`已清理 ChatGPT 全局听写悬浮栏 ${result.closed} 个`);
|
||||
return;
|
||||
}
|
||||
if (globalDictationCleanupAttempt >= delays.length || isQuitting) return;
|
||||
const delay = delays[globalDictationCleanupAttempt++];
|
||||
globalDictationCleanupTimer = setTimeout(() => {
|
||||
run().catch((e) => log(`重试清理 ChatGPT 全局听写悬浮栏失败:${e.message || e}`));
|
||||
}, delay);
|
||||
globalDictationCleanupTimer.unref?.();
|
||||
};
|
||||
|
||||
run().catch((e) => log(`清理 ChatGPT 全局听写悬浮栏失败:${e.message || e}`));
|
||||
}
|
||||
|
||||
function notifyRouterChanged(status) {
|
||||
rebuildTrayMenu();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send("router-changed", status);
|
||||
}
|
||||
|
||||
async function handleTdToolboxDeepLink(rawUrl) {
|
||||
const parsed = deeplink.parseTdToolboxImport(rawUrl);
|
||||
if (!parsed.ok) {
|
||||
await dialog.showMessageBox({
|
||||
type: "warning",
|
||||
title: "TD工具箱导入失败",
|
||||
message: "配置深链无法识别",
|
||||
detail: parsed.error,
|
||||
buttons: ["知道了"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showWindow();
|
||||
const input = parsed.input;
|
||||
const answer = await dialog.showMessageBox(mainWindow, {
|
||||
type: "question",
|
||||
title: "导入 Sub2API 分组",
|
||||
message: `导入「${input.name}」到路由中转?`,
|
||||
detail: "将保存 API 地址、密钥和默认模型到本机分组。不会自动启用该分组,也不会修改 ~/.codex 配置。",
|
||||
buttons: ["取消", "导入"],
|
||||
defaultId: 1,
|
||||
cancelId: 0,
|
||||
noLink: true,
|
||||
});
|
||||
if (answer.response !== 1) return;
|
||||
|
||||
try {
|
||||
const status = router.importSub2apiProvider(input);
|
||||
notifyRouterChanged(status);
|
||||
await dialog.showMessageBox(mainWindow, {
|
||||
type: "info",
|
||||
title: "导入完成",
|
||||
message: status._updated ? "已更新 Sub2API 分组" : "已导入 Sub2API 分组",
|
||||
detail: "请到「路由中转」选择该分组后再接入 Codex。",
|
||||
buttons: ["知道了"],
|
||||
});
|
||||
notifyRouterChanged(router.status());
|
||||
} catch (e) {
|
||||
log("Sub2API 分组导入失败:" + ((e && e.message) || e));
|
||||
await dialog.showMessageBox(mainWindow, {
|
||||
type: "error",
|
||||
title: "导入失败",
|
||||
message: "保存 Sub2API 分组失败",
|
||||
detail: String((e && e.message) || e),
|
||||
buttons: ["知道了"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildTrayMenu() {
|
||||
if (!tray) return;
|
||||
const st = router.status();
|
||||
@@ -233,13 +348,54 @@ function setupTray() {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => showWindow());
|
||||
app.on("second-instance", (_e, argv) => {
|
||||
const rawUrl = argv.find((item) => item.startsWith(`${deeplink.SCHEME}://`));
|
||||
if (rawUrl) {
|
||||
handleTdToolboxDeepLink(rawUrl).catch((e) => log("处理深链失败:" + ((e && e.message) || e)));
|
||||
return;
|
||||
}
|
||||
showWindow();
|
||||
});
|
||||
}
|
||||
|
||||
app.on("open-url", (event, rawUrl) => {
|
||||
if (!rawUrl || !rawUrl.startsWith(`${deeplink.SCHEME}://`)) return;
|
||||
event.preventDefault();
|
||||
if (app.isReady()) {
|
||||
handleTdToolboxDeepLink(rawUrl).catch((e) => log("处理深链失败:" + ((e && e.message) || e)));
|
||||
} else {
|
||||
pendingStartupDeepLink = rawUrl;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.defaultApp && process.argv.length > 1) {
|
||||
app.setAsDefaultProtocolClient(deeplink.SCHEME, process.execPath, [path.resolve(process.argv[1])]);
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient(deeplink.SCHEME);
|
||||
}
|
||||
|
||||
const startupDeepLink = process.argv.find((item) => item.startsWith(`${deeplink.SCHEME}://`));
|
||||
if (startupDeepLink) pendingStartupDeepLink = startupDeepLink;
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
providerSync.setCodexDataDir(codexDataDir.get());
|
||||
router.setCodexDataDir(codexDataDir.get());
|
||||
const activeCodexDataDir = codexDataDir.get();
|
||||
providerSync.setCodexDataDir(activeCodexDataDir);
|
||||
router.setCodexDataDir(activeCodexDataDir);
|
||||
configureGlobalDictation(activeCodexDataDir);
|
||||
try {
|
||||
const startupRouter = router.status();
|
||||
if (startupRouter.repair && startupRouter.repair.repaired) {
|
||||
log(`启动修复 Codex provider:${startupRouter.repair.reason || startupRouter.repair.mode}`);
|
||||
}
|
||||
} catch (e) {
|
||||
log(`启动修复 Codex provider 失败:${e.message || e}`);
|
||||
}
|
||||
createWindow();
|
||||
if (pendingStartupDeepLink) {
|
||||
const rawUrl = pendingStartupDeepLink;
|
||||
pendingStartupDeepLink = null;
|
||||
handleTdToolboxDeepLink(rawUrl).catch((e) => log("处理深链失败:" + ((e && e.message) || e)));
|
||||
}
|
||||
setupTray();
|
||||
app.on("activate", () => showWindow());
|
||||
setupAutoUpdate();
|
||||
@@ -275,7 +431,11 @@ app.whenReady().then(async () => {
|
||||
if (r.migrated.length) log(`启动迁移 plugin: ${r.migrated.length} 个转为软链`);
|
||||
if (r.errors.length) log(`启动迁移 plugin 错误: ${JSON.stringify(r.errors)}`);
|
||||
});
|
||||
// 启动自愈:把当前激活分组的全部文本模型先回填进 Codex 模型目录。
|
||||
deferStartupWork("启动修复会话目录", 5000, async () => {
|
||||
await providerSync.repairLegacySessionVisibility(log);
|
||||
});
|
||||
scheduleGlobalDictationCleanup();
|
||||
// 启动自愈:把当前激活分组的同族文本模型回填进 Codex 模型目录。
|
||||
// 目的——用户在 Codex 本体的模型选择器里切模型(不经本 App)时,若目标模型不在目录里,
|
||||
// Codex 会以未知模型启动、侧栏历史还原失败 = "切模型后会话记录消失"。先回填即根治,
|
||||
// 且既有安装无需重新保存分组即可自动修复。异步、失败不动目录(离线安全)。
|
||||
@@ -283,7 +443,9 @@ app.whenReady().then(async () => {
|
||||
const active = router.llmGroup(); // 当前激活且配齐 baseUrl+key+model 的中转分组(排除默认直连)
|
||||
if (!active) return;
|
||||
const r = await router.syncCatalogFromProvider(active);
|
||||
if (r && r.ok && (r.added || r.revealed)) log(`启动同步模型目录:新增 ${r.added} · 显示 ${r.revealed} · 共 ${r.total}`);
|
||||
if (r && r.ok && (r.added || r.revealed || r.hidden)) {
|
||||
log(`启动同步模型目录:新增 ${r.added} · 显示 ${r.revealed} · 隐藏 ${r.hidden} · 共 ${r.total}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,9 +456,28 @@ async function quitApp() {
|
||||
if (quitting) return;
|
||||
quitting = true;
|
||||
isQuitting = true;
|
||||
if (globalDictationCleanupTimer) {
|
||||
clearTimeout(globalDictationCleanupTimer);
|
||||
globalDictationCleanupTimer = null;
|
||||
}
|
||||
try {
|
||||
await Promise.race([
|
||||
voice.disconnect(),
|
||||
new Promise((resolve) => setTimeout(resolve, 2500)),
|
||||
]);
|
||||
} catch {}
|
||||
try {
|
||||
await Promise.race([
|
||||
cleanupGlobalDictation("退出清理"),
|
||||
new Promise((resolve) => setTimeout(resolve, 2500)),
|
||||
]);
|
||||
} catch {}
|
||||
try {
|
||||
memory.stop(); // 停监视 + 记忆库落盘
|
||||
} catch {}
|
||||
try {
|
||||
globalDictation.stopGuard();
|
||||
} catch {}
|
||||
try {
|
||||
// 最多等 5 秒完成网络还原 / 拆 TUN / 杀引擎,超时也继续退出
|
||||
await Promise.race([proxy.shutdown(), new Promise((r) => setTimeout(r, 5000))]);
|
||||
@@ -718,7 +899,16 @@ async function startupUpdateCheck() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (r.needFull) { await checkUpdateManual(); return; } // 有新内容但需完整包
|
||||
if (r.needFull || r.needFullAfterHot) {
|
||||
sendUpdate("update-status", {
|
||||
state: "available-manual",
|
||||
version: r.fullAppVersion || r.contentVersion,
|
||||
url: GITEA_RELEASES_PAGE,
|
||||
notes: `${r.notes || ""}${r.needFullAfterHot ? "\n\n本次更新还需要安装完整包,以注册 TD 工具箱深链协议。" : ""}`.trim(),
|
||||
mandatory: r.mandatory,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch { /* 热更检测失败 → 退回完整包检测 */ }
|
||||
await checkUpdateManual();
|
||||
@@ -1534,6 +1724,7 @@ ipcMain.handle("sessions-data-dir-select", async () => {
|
||||
const next = codexDataDir.set(picked.filePaths[0]);
|
||||
providerSync.setCodexDataDir(next.path);
|
||||
router.setCodexDataDir(next.path);
|
||||
configureGlobalDictation(next.path);
|
||||
memory.restartWatching();
|
||||
log(`会话数据目录已切换:${next.path}`);
|
||||
return next;
|
||||
@@ -1542,6 +1733,7 @@ ipcMain.handle("sessions-data-dir-reset", async () => {
|
||||
const next = codexDataDir.reset();
|
||||
providerSync.setCodexDataDir(next.path);
|
||||
router.setCodexDataDir(next.path);
|
||||
configureGlobalDictation(next.path);
|
||||
memory.restartWatching();
|
||||
log(`会话数据目录已恢复默认:${next.path}`);
|
||||
return next;
|
||||
@@ -1554,6 +1746,31 @@ ipcMain.handle("session-read", async (_e, { path: filePath }) => {
|
||||
return sessions.readConversation(filePath);
|
||||
});
|
||||
|
||||
ipcMain.handle("sessions-clear-all", async () => {
|
||||
return withStateWriteLock("清空全部会话", async () => {
|
||||
if (providerSync.codexRunning()) throw new Error("请先完全退出 Codex,再清空会话记录");
|
||||
const result = await sessions.clearAllConversations();
|
||||
let memoryRemoved = 0;
|
||||
let cleanupWarning = "";
|
||||
try {
|
||||
const memoryResult = await memory.clearAllSessions();
|
||||
memoryRemoved = memoryResult.count || 0;
|
||||
} catch (e) {
|
||||
cleanupWarning = String((e && e.message) || e);
|
||||
log("会话主体已清空,但工具箱会话记忆清理失败:" + cleanupWarning);
|
||||
}
|
||||
try {
|
||||
// 停止待处理的摘要任务,避免已删除 rollout 被重新写回记忆库。
|
||||
memory.restartWatching();
|
||||
} catch (e) {
|
||||
const warning = String((e && e.message) || e);
|
||||
cleanupWarning = cleanupWarning ? cleanupWarning + ";" + warning : warning;
|
||||
log("会话已清空,但重启会话记忆监视器失败:" + warning);
|
||||
}
|
||||
return { ...result, memoryRemoved, cleanupWarning };
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle("session-delete-permanently", async (_e, { path: filePath } = {}) => {
|
||||
return withStateWriteLock("永久删除会话", async () => {
|
||||
if (providerSync.codexRunning()) throw new Error("请先完全退出 Codex,再永久删除会话");
|
||||
@@ -1904,7 +2121,7 @@ voice.onSessionDelete(async (request) => {
|
||||
cancelId: 0,
|
||||
});
|
||||
if (answer.response !== 1) return;
|
||||
voice.disconnect();
|
||||
await voice.disconnect();
|
||||
try {
|
||||
const stopped = await codexDesktop.killCodex();
|
||||
if (!stopped || !stopped.exited) throw new Error("Codex 未完全退出,已停止删除以保护 SQLite");
|
||||
|
||||
Reference in New Issue
Block a user