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:
+142
-2
@@ -71,6 +71,7 @@ $$("[data-open-panel]").forEach((btn) => {
|
||||
activatePanel(name);
|
||||
if (name === "sessions" && !sessLoaded) sessLoad();
|
||||
if (name === "memory" && !memLoaded) memLoad();
|
||||
if (name === "voice" && !voiceLoaded) voiceLoad();
|
||||
});
|
||||
});
|
||||
// 内置工具面板「← 返回插件市场」
|
||||
@@ -98,6 +99,140 @@ apic.onLog((msg) => {
|
||||
$("#log-panel").scrollTop = $("#log-panel").scrollHeight;
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// 语音输入(CDP 注入 Codex 麦克风按钮)
|
||||
// ===========================================================================
|
||||
let voiceLoaded = false;
|
||||
const PHASE_TEXT = {
|
||||
unknown: "未连接",
|
||||
checking: "检测中…",
|
||||
not_running: "Codex 未运行",
|
||||
running_no_port: "需重启 Codex",
|
||||
restarting: "正在重启 Codex…",
|
||||
connecting: "正在连接…",
|
||||
injecting: "正在注入…",
|
||||
connected: "已连接",
|
||||
disconnected: "已断开",
|
||||
transcribing: "转写中…",
|
||||
recording: "录音中",
|
||||
error: "出错",
|
||||
};
|
||||
function renderVoiceStatus(s) {
|
||||
const dot = $("#voice-dot");
|
||||
const phaseText = $("#voice-phase-text");
|
||||
const detail = $("#voice-detail");
|
||||
const connectBtn = $("#voice-connect-btn");
|
||||
const discBtn = $("#voice-disconnect-btn");
|
||||
const phase = s && s.phase ? s.phase : "unknown";
|
||||
if (dot) dot.dataset.state = phase;
|
||||
const txt = PHASE_TEXT[phase] || phase;
|
||||
if (phaseText) phaseText.textContent = txt;
|
||||
const connected = phase === "connected" || phase === "transcribing" || phase === "recording";
|
||||
if (connectBtn) connectBtn.classList.toggle("hidden", connected);
|
||||
if (discBtn) discBtn.classList.toggle("hidden", !connected);
|
||||
let detailText = txt;
|
||||
if (s && s.lastError) detailText += " " + s.lastError;
|
||||
if (s && s.codexPath) detailText += " 路径:" + s.codexPath;
|
||||
if (s && s.target && s.target.title) detailText += " 目标:" + s.target.title;
|
||||
if (detail) detail.textContent = detailText;
|
||||
}
|
||||
async function voiceLoad() {
|
||||
if (voiceLoaded) return;
|
||||
voiceLoaded = true;
|
||||
let unsub = null;
|
||||
try {
|
||||
const s0 = await apic.voiceStatus();
|
||||
renderVoiceStatus(s0);
|
||||
} catch {}
|
||||
unsub = apic.onVoiceStatus((s) => renderVoiceStatus(s));
|
||||
|
||||
const connectBtn = $("#voice-connect-btn");
|
||||
const discBtn = $("#voice-disconnect-btn");
|
||||
const refreshSttBtn = $("#voice-refresh-stt");
|
||||
|
||||
async function loadSttInfo() {
|
||||
try {
|
||||
const info = await apic.voiceSttInfo();
|
||||
const nameEl = $("#voice-stt-name");
|
||||
const modelEl = $("#voice-stt-model");
|
||||
if (info && info.ready) {
|
||||
if (nameEl) nameEl.textContent = info.groupName || "(未命名)";
|
||||
if (modelEl) modelEl.textContent = info.model || "whisper-1";
|
||||
} else {
|
||||
if (nameEl) nameEl.textContent = "未配置";
|
||||
if (modelEl) modelEl.textContent = "—";
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
loadSttInfo();
|
||||
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
const cur = await apic.voiceStatus().catch(() => ({}));
|
||||
// 若 Codex 在跑但没端口,需用户确认重启
|
||||
if (cur.phase === "running_no_port") {
|
||||
const ok = confirm(
|
||||
"需要重启 Codex 以启用语音输入调试通道。\n\n当前 Codex 窗口会关闭一次后重新打开,登录态与历史会话保留。\n是否继续?"
|
||||
);
|
||||
if (!ok) return;
|
||||
busy(connectBtn, true, "重启中…");
|
||||
try {
|
||||
const s = await apic.voiceConnect({ forceRestart: true });
|
||||
renderVoiceStatus(s);
|
||||
if (s.phase === "error") toast(s.lastError || "连接失败", "err");
|
||||
} catch (e) {
|
||||
toast("连接失败:" + (e && e.message ? e.message : e), "err");
|
||||
} finally {
|
||||
busy(connectBtn, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
busy(connectBtn, true, "连接中…");
|
||||
try {
|
||||
const s = await apic.voiceConnect({});
|
||||
renderVoiceStatus(s);
|
||||
if (s.phase === "running_no_port") {
|
||||
toast("Codex 在运行,需重启以启用调试端口,请再次点击连接", "");
|
||||
} else if (s.phase === "error") {
|
||||
toast(s.lastError || "连接失败", "err");
|
||||
} else if (s.phase === "connected") {
|
||||
toast("已连接,Codex 输入框旁应出现 🎤 按钮");
|
||||
}
|
||||
} catch (e) {
|
||||
toast("连接失败:" + (e && e.message ? e.message : e), "err");
|
||||
} finally {
|
||||
busy(connectBtn, false);
|
||||
}
|
||||
});
|
||||
discBtn.addEventListener("click", async () => {
|
||||
busy(discBtn, true, "断开中…");
|
||||
try {
|
||||
const s = await apic.voiceDisconnect();
|
||||
renderVoiceStatus(s);
|
||||
} finally {
|
||||
busy(discBtn, false);
|
||||
}
|
||||
});
|
||||
if (refreshSttBtn) {
|
||||
refreshSttBtn.addEventListener("click", () => loadSttInfo());
|
||||
}
|
||||
const probeBtn = $("#voice-probe-stt");
|
||||
if (probeBtn) {
|
||||
probeBtn.addEventListener("click", async () => {
|
||||
const out = $("#voice-probe-result");
|
||||
if (out) out.textContent = "诊断中…";
|
||||
try {
|
||||
const arr = await apic.voiceProbeStt();
|
||||
const lines = (arr || []).map((x) =>
|
||||
(x.supported ? "✅" : "❌") + " " + (x.name || "") + " " + (x.baseUrl || "") + " HTTP " + x.status + (x.note ? " " + x.note : "")
|
||||
);
|
||||
if (out) out.innerHTML = "各分组 /v1/audio/transcriptions 支持情况(✅=路径存在,可转写):\n" + esc(lines.join("\n")).replace(/\n/g, "<br>");
|
||||
} catch (e) {
|
||||
if (out) out.textContent = "诊断失败:" + (e && e.message ? e.message : e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// ① 插件市场
|
||||
// ===========================================================================
|
||||
@@ -110,9 +245,10 @@ const installed = new Set(JSON.parse(localStorage.getItem("installedSkills") ||
|
||||
let installedSkills = []; // 磁盘实况(~/.codex/skills 扫描结果)
|
||||
let installedDirs = new Set(); // 已安装技能的目录名,用于精确标注“已安装”
|
||||
|
||||
// 与主进程 skillInstall 的目录命名保持一致(安装到 ~/.codex/skills/<此名>)
|
||||
// 与主进程 skillInstall 的目录命名保持一致(安装到 ~/.codex/skills/<此名>)。统一小写,避免大小写变体导致"已安装"判断错位。
|
||||
function safeSkillDir(name) {
|
||||
return (String(name || "skill")
|
||||
.toLowerCase()
|
||||
.replace(/[^\w一-龥.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80)) || "skill";
|
||||
}
|
||||
async function loadInstalledSkills() {
|
||||
@@ -1187,7 +1323,7 @@ function renderRouter(st) {
|
||||
<div class="cl-item">
|
||||
<input type="radio" name="active-grp" ${g.id === st.activeId ? "checked" : ""} data-active="${g.id}" />
|
||||
<div>
|
||||
<div class="cl-name">${esc(g.name)} ${badges}${g.codexImage ? ' <span class="agent-badge mm">🎨 生图</span>' : ""}</div>
|
||||
<div class="cl-name">${esc(g.name)} ${badges}${g.codexImage ? ' <span class="agent-badge mm">🎨 生图</span>' : ""}${g.stt ? ' <span class="agent-badge mm">🎤 转文字' + (g.sttModel ? " " + esc(g.sttModel) : "") + '</span>' : ""}</div>
|
||||
<div class="cl-src">${esc(g.baseUrl)}${g.model ? " · 模型 " + esc(g.model) : ""}${!g.isDefault && g.mode === "chat" ? " · 🔄转换" : ""}${g.hasKey ? "" : " · ⚠️无密钥"}</div>
|
||||
</div>
|
||||
<div class="btn-group" style="margin-left:auto">${actions}</div>
|
||||
@@ -1231,6 +1367,8 @@ function openGrpModal(g) {
|
||||
$("#grp-key").placeholder = g && g.hasKey ? "(已保存,留空则不修改)" : "sk-…";
|
||||
$("#grp-model").value = g ? g.model || "" : "";
|
||||
$("#grp-codex-image").checked = !!(g && g.codexImage);
|
||||
$("#grp-stt").checked = !!(g && g.stt);
|
||||
$("#grp-stt-model").value = g && g.sttModel ? g.sttModel : "whisper-1";
|
||||
$("#grp-ctx").value = g && g.contextWindow ? g.contextWindow : "";
|
||||
const sel = $("#grp-model-list");
|
||||
sel.classList.add("hidden");
|
||||
@@ -1266,6 +1404,8 @@ $("#grp-save").addEventListener("click", async () => {
|
||||
mode: $("#grp-mode").value,
|
||||
contextWindow: $("#grp-ctx").value.trim(),
|
||||
codexImage: $("#grp-codex-image").checked,
|
||||
stt: $("#grp-stt").checked,
|
||||
sttModel: $("#grp-stt").checked ? ($("#grp-stt-model").value.trim() || "whisper-1") : "",
|
||||
};
|
||||
if (grpEditing) group.id = grpEditing.id;
|
||||
const key = $("#grp-key").value;
|
||||
|
||||
Reference in New Issue
Block a user