fix: Codex 自动压缩不触发 —— 接管时链接模型目录(绝对路径)进 config.toml (hot 1.5.83)
This commit is contained in:
@@ -7,17 +7,23 @@
|
||||
// 注意:脚本体用普通模板字符串包裹;内部避免出现 ${(已无)与裸反引号(内部的模板字符串反引号需转义为 \`)。
|
||||
const INJECT_SCRIPT = `
|
||||
(() => {
|
||||
const VER = "voice-inject-v6";
|
||||
const VER = "voice-inject-v13";
|
||||
if (window.__voiceInjectInstalled === VER) return;
|
||||
// 版本变了或首次:清理旧按钮/旧 observer,重新安装
|
||||
if (window.__voiceInjectObserver) { try { window.__voiceInjectObserver.disconnect(); } catch {} }
|
||||
// 版本变了或首次:完整卸载旧实例,避免旧 observer 在新会话继续挂回按钮。
|
||||
if (typeof window.__voiceInjectDispose === "function") {
|
||||
try { window.__voiceInjectDispose(); } catch {}
|
||||
} else if (window.__voiceInjectObserver) {
|
||||
try { window.__voiceInjectObserver.disconnect(); } catch {}
|
||||
}
|
||||
const oldBtn = document.getElementById("codex-voice-input-btn");
|
||||
if (oldBtn) oldBtn.remove();
|
||||
document.querySelectorAll("[data-td-sidebar-delete]").forEach((el) => el.remove());
|
||||
window.__voiceInjectInstalled = VER;
|
||||
|
||||
const TAG = "[VoiceInput]";
|
||||
const BTN_ID = "codex-voice-input-btn";
|
||||
const STYLE_ID = "codex-voice-input-style-v6";
|
||||
const STYLE_ID = "codex-voice-input-style-v13";
|
||||
let disposed = false;
|
||||
|
||||
// —— 样式 ——
|
||||
if (!document.getElementById(STYLE_ID)) {
|
||||
@@ -58,11 +64,18 @@ const INJECT_SCRIPT = `
|
||||
font:13px system-ui,sans-serif;z-index:999999;box-shadow:0 8px 30px rgba(0,0,0,.5);
|
||||
max-width:80vw;
|
||||
}
|
||||
.td-sidebar-delete-btn{color:rgba(113,113,122,.72)!important;}
|
||||
.td-sidebar-delete-btn:hover,.td-sidebar-delete-btn:focus-visible{color:#dc2626!important;}
|
||||
@media (prefers-color-scheme:dark){
|
||||
.td-sidebar-delete-btn{color:rgba(161,161,170,.72)!important;}
|
||||
.td-sidebar-delete-btn:hover,.td-sidebar-delete-btn:focus-visible{color:#f87171!important;}
|
||||
}
|
||||
\`;
|
||||
(document.head || document.documentElement).appendChild(st);
|
||||
}
|
||||
|
||||
function toast(msg, ms = 2400) {
|
||||
if (disposed) return;
|
||||
const t = document.createElement("div");
|
||||
t.className = "codex-voice-toast";
|
||||
t.textContent = msg;
|
||||
@@ -70,6 +83,410 @@ const INJECT_SCRIPT = `
|
||||
setTimeout(() => t.remove(), ms);
|
||||
}
|
||||
|
||||
// 某些中转会把内部思考块作为普通正文推给页面。只处理已明确成对的
|
||||
// 内部标签,保留用户输入框、代码编辑器和工具箱自己注入的节点。
|
||||
function stripVisibleThoughtText(text) {
|
||||
let out = String(text || "");
|
||||
for (const name of ["thinking", "analysis", "reasoning"]) {
|
||||
out = out.replace(new RegExp("<" + name + "\\b[^>]*>[\\s\\S]*?</" + name + "\\s*>", "gi"), "");
|
||||
}
|
||||
return out.replace(/<\\/?(?:thinking|analysis|reasoning)\\b[^>]*>/gi, "");
|
||||
}
|
||||
function sanitizeVisibleThoughts(root) {
|
||||
if (!root || !document.createTreeWalker) return;
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
const nodes = [];
|
||||
let node;
|
||||
while ((node = walker.nextNode())) nodes.push(node);
|
||||
for (const textNode of nodes) {
|
||||
const parent = textNode.parentElement;
|
||||
if (!parent || parent.closest("#codex-voice-input-btn, textarea, input, [contenteditable='true'], script, style")) continue;
|
||||
const next = stripVisibleThoughtText(textNode.nodeValue);
|
||||
if (next !== textNode.nodeValue) textNode.nodeValue = next;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 Codex 任务右键菜单中增加真正删除入口。只从当前菜单关联的
|
||||
// DOM 数据/链接提取 thread id,拿不到 id 时不显示,避免误删。
|
||||
let currentMenuThreadId = "";
|
||||
const THREAD_ID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
||||
function readReactThreadId(node) {
|
||||
for (const key of Object.keys(node || {})) {
|
||||
if (!key.startsWith("__reactProps$") && !key.startsWith("__reactFiber$")) continue;
|
||||
const seen = new Set();
|
||||
const walk = (value, depth, field) => {
|
||||
if (depth > 4 || value == null) return "";
|
||||
if (typeof value === "string") {
|
||||
const hit = value.match(THREAD_ID_RE);
|
||||
return hit && /thread|conversation|session|href|id/i.test(field || "") ? hit[0] : "";
|
||||
}
|
||||
if (typeof value !== "object" || seen.has(value) || value instanceof Node) return "";
|
||||
seen.add(value);
|
||||
for (const [name, child] of Object.entries(value)) {
|
||||
if (/^(children|return|sibling|child|stateNode|alternate|_owner)$/i.test(name)) continue;
|
||||
const hit = walk(child, depth + 1, name);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const hit = walk(node[key], 0, key);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function readThreadId(node) {
|
||||
let cur = node;
|
||||
for (let depth = 0; cur && depth < 8; depth++, cur = cur.parentElement) {
|
||||
for (const key of ["threadId", "conversationId", "sessionId", "id"]) {
|
||||
const value = cur.dataset && cur.dataset[key];
|
||||
if (value && THREAD_ID_RE.test(value)) return value.match(THREAD_ID_RE)[0];
|
||||
}
|
||||
const href = cur.getAttribute && cur.getAttribute("href");
|
||||
const hit = href && href.match(THREAD_ID_RE);
|
||||
if (hit) return hit[0];
|
||||
const text = cur.getAttribute && (cur.getAttribute("data-state") || cur.getAttribute("aria-controls") || "");
|
||||
const textHit = text.match(THREAD_ID_RE);
|
||||
if (textHit) return textHit[0];
|
||||
const nested = cur.querySelector && cur.querySelector("a[href],[data-thread-id],[data-conversation-id],[data-session-id]");
|
||||
if (nested && nested !== cur) {
|
||||
const nestedValue = (nested.getAttribute("href") || "") + " " +
|
||||
(nested.dataset && (nested.dataset.threadId || nested.dataset.conversationId || nested.dataset.sessionId) || "");
|
||||
const nestedHit = nestedValue.match(THREAD_ID_RE);
|
||||
if (nestedHit) return nestedHit[0];
|
||||
}
|
||||
const reactHit = readReactThreadId(cur);
|
||||
if (reactHit) return reactHit;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function sidebarThreadId(row) {
|
||||
const raw = row && row.getAttribute && row.getAttribute("data-app-action-sidebar-thread-id");
|
||||
const hit = String(raw || "").match(THREAD_ID_RE);
|
||||
return hit ? hit[0] : readThreadId(row);
|
||||
}
|
||||
function copySessionIdItem(menu) {
|
||||
if (!menu) return null;
|
||||
const candidates = menu.querySelectorAll("button,[role='menuitem'],[role='button'],li,div");
|
||||
for (const item of candidates) {
|
||||
if (item.dataset && item.dataset.tdPermanentDelete) continue;
|
||||
if (String(item.textContent || "").trim() === "复制会话 ID") return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function menuLooksLikeSession(menu) {
|
||||
const text = String(menu && menu.textContent || "");
|
||||
return !!copySessionIdItem(menu) && /归档任务|复制深度链接|重命名任务/.test(text);
|
||||
}
|
||||
function findMenuThreadId(menu) {
|
||||
const direct = readThreadId(menu);
|
||||
if (direct) return direct;
|
||||
const links = menu ? menu.querySelectorAll("a,button,[role='menuitem'],[data-thread-id],[data-conversation-id]") : [];
|
||||
for (const el of links) {
|
||||
const hit = readThreadId(el);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return currentMenuThreadId;
|
||||
}
|
||||
function sendDeleteRequest(threadId, menu) {
|
||||
if (!threadId || typeof window.__voiceBridge !== "function") {
|
||||
const nativeCopy = copySessionIdItem(menu);
|
||||
if (!nativeCopy || typeof window.__voiceBridge !== "function") {
|
||||
toast("未找到会话 ID,请从工具箱会话记录页删除");
|
||||
return;
|
||||
}
|
||||
const requestId = "td-del-" + Date.now() + "-" + Math.random().toString(16).slice(2);
|
||||
try {
|
||||
// 先让主进程保存剪贴板,随后复用 Codex 原生“复制会话 ID”菜单动作。
|
||||
window.__voiceBridge(JSON.stringify({ kind: "delete-session-clipboard-prepare", requestId }));
|
||||
setTimeout(() => {
|
||||
try { nativeCopy.click(); } catch {}
|
||||
setTimeout(() => {
|
||||
try { window.__voiceBridge(JSON.stringify({ kind: "delete-session-clipboard-read", requestId })); } catch {}
|
||||
}, 90);
|
||||
}, 110);
|
||||
toast("正在读取会话 ID…", 2200);
|
||||
} catch (e) {
|
||||
toast("删除请求发送失败");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
window.__voiceBridge(JSON.stringify({ kind: "delete-session", threadId }));
|
||||
toast("正在执行永久删除,请稍候…", 3200);
|
||||
return false;
|
||||
} catch (e) {
|
||||
toast("删除请求发送失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function addDeleteMenuItem(menu) {
|
||||
if (!menu || menu.querySelector("[data-td-permanent-delete]")) return;
|
||||
if (!menuLooksLikeSession(menu)) return;
|
||||
const threadId = findMenuThreadId(menu);
|
||||
const nativeCopy = copySessionIdItem(menu);
|
||||
if (!nativeCopy) return;
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.dataset.tdPermanentDelete = "1";
|
||||
item.textContent = "彻底删除本地会话";
|
||||
item.title = "退出 Codex 后删除本地会话、SQLite 索引和工具箱记忆";
|
||||
item.style.cssText = "display:block;width:100%;margin-top:6px;padding:8px 12px;border:0;border-top:1px solid rgba(127,127,127,.2);background:transparent;color:#dc2626;text-align:left;font:inherit;cursor:pointer;";
|
||||
item.addEventListener("mouseenter", () => { item.style.background = "rgba(220,38,38,.10)"; });
|
||||
item.addEventListener("mouseleave", () => { item.style.background = "transparent"; });
|
||||
item.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const needsNativeCopy = sendDeleteRequest(threadId, menu);
|
||||
const root = item.closest("[role='menu'],[data-radix-menu-content]") || item.parentElement;
|
||||
if (root) setTimeout(() => root.remove(), needsNativeCopy ? 280 : 0);
|
||||
});
|
||||
(nativeCopy.parentElement || menu).appendChild(item);
|
||||
}
|
||||
function scanSessionMenus() {
|
||||
document.querySelectorAll("[role='menu'],[data-radix-menu-content],div[class*='menu'],div[class*='popover']").forEach((el) => addDeleteMenuItem(el));
|
||||
}
|
||||
function captureMenuTarget(event) {
|
||||
currentMenuThreadId = readThreadId(event.target);
|
||||
setTimeout(scanSessionMenus, 30);
|
||||
setTimeout(scanSessionMenus, 180);
|
||||
}
|
||||
if (window.__tdPermanentDeleteContextHandler) {
|
||||
document.removeEventListener("contextmenu", window.__tdPermanentDeleteContextHandler, true);
|
||||
document.removeEventListener("pointerdown", window.__tdPermanentDeleteContextHandler, true);
|
||||
}
|
||||
// 原生菜单不经过网页 DOM,不再对全局 pointerdown 做昂贵扫描。保留手动
|
||||
// scanner 仅用于未来 Codex 切回网页菜单时诊断。
|
||||
window.__tdPermanentDeleteContextHandler = null;
|
||||
window.__tdScanPermanentDeleteMenu = scanSessionMenus;
|
||||
|
||||
// Codex 的右键菜单是 Electron 原生 Menu,网页脚本无法向其中追加 DOM。
|
||||
// 因此把彻底删除入口挂到任务行已有的“置顶/归档”操作组,macOS/Windows 共用。
|
||||
function makeSidebarDeleteButton(row, actionBox) {
|
||||
const archive = actionBox.querySelector("button[aria-label='归档任务'],button[aria-label='Archive task']");
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.dataset.tdSidebarDelete = "1";
|
||||
btn.className = ((archive && archive.className) || "") + " td-sidebar-delete-btn";
|
||||
btn.setAttribute("aria-label", "彻底删除本地会话");
|
||||
btn.title = "彻底删除本地会话";
|
||||
btn.innerHTML = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M6.25 5.75h7.5m-6.5 0 .45 9h4.6l.45-9M8.1 5.75V4.5h3.8v1.25M8.65 8v4.6M11.35 8v4.6" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
btn.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
btn.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const threadId = sidebarThreadId(row);
|
||||
if (!threadId) {
|
||||
toast("未找到会话 ID,请从工具箱会话记录页删除");
|
||||
return;
|
||||
}
|
||||
sendDeleteRequest(threadId, null);
|
||||
});
|
||||
return btn;
|
||||
}
|
||||
// —— 侧栏行/归档按钮/操作容器 查找:多策略 + 调试日志 ——
|
||||
// 行查找:优先 Codex 内部属性,退化到 aria-label/href/testid 关键字匹配。
|
||||
function findSidebarRows(root) {
|
||||
const scope = root || document;
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const push = (el) => { if (el && !seen.has(el)) { seen.add(el); out.push(el); } };
|
||||
// 1) Codex 内部属性(最准)
|
||||
try {
|
||||
if (scope.nodeType === 1 && scope.matches && scope.matches("[data-app-action-sidebar-thread-row]")) push(scope);
|
||||
if (scope.querySelectorAll) scope.querySelectorAll("[data-app-action-sidebar-thread-row]").forEach(push);
|
||||
} catch (e) { console.log("[TD] findSidebarRows: attr strategy failed", e && e.message); }
|
||||
if (out.length) { console.log("[TD] findSidebarRows: attr strategy hit", out.length); return out; }
|
||||
// 2) aria-label 含 thread/conversation/任务/会话 的按钮或链接
|
||||
try {
|
||||
const ariaEls = scope.querySelectorAll("button[aria-label],a[aria-label],[role='button'][aria-label]");
|
||||
const kwRe = /(thread|conversation|任务|会话)/i;
|
||||
for (const el of ariaEls) {
|
||||
const label = el.getAttribute("aria-label") || "";
|
||||
if (kwRe.test(label)) push(el.closest("li,[data-app-action-sidebar-thread-row],div,section") || el.parentElement || el);
|
||||
}
|
||||
} catch (e) { console.log("[TD] findSidebarRows: aria strategy failed", e && e.message); }
|
||||
if (out.length) { console.log("[TD] findSidebarRows: aria strategy hit", out.length); return out; }
|
||||
// 3) href 含 UUID 的链接
|
||||
try {
|
||||
const hrefEls = scope.querySelectorAll("a[href]");
|
||||
for (const el of hrefEls) {
|
||||
const href = el.getAttribute("href") || "";
|
||||
if (THREAD_ID_RE.test(href)) push(el.closest("li,[data-app-action-sidebar-thread-row],div,section") || el.parentElement || el);
|
||||
}
|
||||
} catch (e) { console.log("[TD] findSidebarRows: href strategy failed", e && e.message); }
|
||||
if (out.length) { console.log("[TD] findSidebarRows: href strategy hit", out.length); return out; }
|
||||
// 4) data-testid 含 thread/conversation
|
||||
try {
|
||||
const testEls = scope.querySelectorAll("[data-testid]");
|
||||
const testRe = /(thread|conversation)/i;
|
||||
for (const el of testEls) {
|
||||
const tid = el.getAttribute("data-testid") || "";
|
||||
if (testRe.test(tid)) push(el.closest("li,[data-app-action-sidebar-thread-row],div,section") || el.parentElement || el);
|
||||
}
|
||||
} catch (e) { console.log("[TD] findSidebarRows: testid strategy failed", e && e.message); }
|
||||
if (out.length) console.log("[TD] findSidebarRows: testid strategy hit", out.length);
|
||||
else console.log("[TD] findSidebarRows: no rows found via any strategy");
|
||||
return out;
|
||||
}
|
||||
// 归档按钮查找:aria-label/title/textContent/SVG 多策略,大小写不敏感、部分匹配。
|
||||
function findArchiveButton(row) {
|
||||
if (!row) return null;
|
||||
const kwRe = /(归档|archive)/i;
|
||||
// 1) aria-label(大小写不敏感 + 部分匹配)
|
||||
try {
|
||||
const byAria = row.querySelector("button[aria-label],a[aria-label],[role='button'][aria-label]");
|
||||
const list = byAria ? [byAria, ...row.querySelectorAll("button[aria-label],a[aria-label],[role='button'][aria-label]")] : row.querySelectorAll("button[aria-label],a[aria-label],[role='button'][aria-label]");
|
||||
for (const el of list) {
|
||||
const label = (el.getAttribute && el.getAttribute("aria-label")) || "";
|
||||
if (kwRe.test(label)) { console.log("[TD] findArchiveButton: aria-label hit", label); return el; }
|
||||
}
|
||||
} catch (e) { console.log("[TD] findArchiveButton: aria strategy failed", e && e.message); }
|
||||
// 2) title 属性
|
||||
try {
|
||||
for (const el of row.querySelectorAll("button[title],a[title],[role='button'][title]")) {
|
||||
const title = (el.getAttribute && el.getAttribute("title")) || "";
|
||||
if (kwRe.test(title)) { console.log("[TD] findArchiveButton: title hit", title); return el; }
|
||||
}
|
||||
} catch (e) { console.log("[TD] findArchiveButton: title strategy failed", e && e.message); }
|
||||
// 3) textContent 匹配
|
||||
try {
|
||||
for (const el of row.querySelectorAll("button,a,[role='button']")) {
|
||||
const text = String(el.textContent || "").trim();
|
||||
if (text && kwRe.test(text)) { console.log("[TD] findArchiveButton: textContent hit", text); return el; }
|
||||
}
|
||||
} catch (e) { console.log("[TD] findArchiveButton: text strategy failed", e && e.message); }
|
||||
// 4) SVG 图标启发:含 archive/pin 风格 path 的按钮
|
||||
try {
|
||||
const svgBtns = row.querySelectorAll("button:has(svg),a:has(svg),[role='button']:has(svg)");
|
||||
for (const el of svgBtns) {
|
||||
const svg = el.querySelector("svg");
|
||||
if (!svg) continue;
|
||||
const d = (svg.querySelector("path") && svg.querySelector("path").getAttribute("d")) || "";
|
||||
// 归档盒常见:横向顶条 + 下方矩形
|
||||
if (/m.*h.*v.*h.*z/i.test(d) && d.length > 30) {
|
||||
console.log("[TD] findArchiveButton: svg heuristics hit");
|
||||
return el;
|
||||
}
|
||||
}
|
||||
} catch (e) { console.log("[TD] findArchiveButton: svg strategy failed", e && e.message); }
|
||||
console.log("[TD] findArchiveButton: no archive button found");
|
||||
return null;
|
||||
}
|
||||
// 操作容器查找:从归档按钮向上试多种深度,或直接定位行内操作组。
|
||||
function findSidebarActionBox(row) {
|
||||
const archive = findArchiveButton(row);
|
||||
if (archive) {
|
||||
// 试 parent.depth 1..4,找到含多个按钮的容器即视为 actionBox
|
||||
let cur = archive.parentElement;
|
||||
for (let depth = 1; cur && depth <= 4; depth++, cur = cur.parentElement) {
|
||||
const btns = cur.querySelectorAll("button,[role='button']");
|
||||
if (btns.length >= 2) {
|
||||
console.log("[TD] findSidebarActionBox: via archive, depth", depth, "btns", btns.length);
|
||||
return { actionBox: cur, anchor: archive.parentElement || archive };
|
||||
}
|
||||
}
|
||||
// 退而求其次:归档按钮的直接父级
|
||||
console.log("[TD] findSidebarActionBox: fallback to archive.parentElement");
|
||||
return { actionBox: archive.parentElement, anchor: archive };
|
||||
}
|
||||
// 不经归档按钮:直接找行内按钮组
|
||||
try {
|
||||
const groups = row.querySelectorAll("div:has(> button),span:has(> button)");
|
||||
for (const g of groups) {
|
||||
if (g.querySelectorAll("button,[role='button']").length >= 2) {
|
||||
console.log("[TD] findSidebarActionBox: direct group hit");
|
||||
return { actionBox: g, anchor: null };
|
||||
}
|
||||
}
|
||||
} catch (e) { console.log("[TD] findSidebarActionBox: direct group strategy failed", e && e.message); }
|
||||
console.log("[TD] findSidebarActionBox: not found");
|
||||
return null;
|
||||
}
|
||||
function mountSidebarDeleteButtons(root = document) {
|
||||
const rows = findSidebarRows(root);
|
||||
console.log("[TD] mountSidebarDeleteButtons: rows", rows.length);
|
||||
let mounted = 0, skipped = 0, failed = 0;
|
||||
for (const row of rows) {
|
||||
if (row.querySelector("[data-td-sidebar-delete]")) { skipped++; continue; }
|
||||
const found = findSidebarActionBox(row);
|
||||
if (!found || !found.actionBox) { console.log("[TD] mountSidebarDeleteButtons: no actionBox for row", row); failed++; continue; }
|
||||
const { actionBox, anchor } = found;
|
||||
try {
|
||||
actionBox.style.width = "76px";
|
||||
actionBox.insertBefore(makeSidebarDeleteButton(row, actionBox), anchor || actionBox.firstChild);
|
||||
mounted++;
|
||||
console.log("[TD] mountSidebarDeleteButtons: mounted on row", row);
|
||||
} catch (e) {
|
||||
console.log("[TD] mountSidebarDeleteButtons: insert failed", e && e.message);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
console.log(\`[TD] mountSidebarDeleteButtons: done mounted=\${mounted} skipped=\${skipped} failed=\${failed}\`);
|
||||
}
|
||||
// —— 诊断:把侧边栏 DOM 结构通过 binding 回传主进程(无需 DevTools)——
|
||||
function dumpSidebarInfo() {
|
||||
if (typeof window.__voiceBridge !== "function") return;
|
||||
const info = { sidebar: null, rows: [], archive: null, actionBox: null };
|
||||
// 1) 侧边栏容器
|
||||
const sidebarCandidates = document.querySelectorAll("[class*='sidebar'],[class*='thread-list'],[class*='conversation-list'],[class*='nav-list'],[class*='task-list']");
|
||||
info.sidebar = sidebarCandidates.length;
|
||||
// 2) 每行采样:取前 3 行看 HTML 结构
|
||||
const allRows = document.querySelectorAll("a[href*='thread'],a[href*='conversation'],a[href*='/c/'],a[href*='/chat/'],[data-app-action-sidebar-thread-row],[class*='thread-row'],[class*='conversation-row'],[class*='session-row']");
|
||||
if (allRows.length) {
|
||||
for (let i = 0; i < Math.min(3, allRows.length); i++) {
|
||||
const el = allRows[i];
|
||||
info.rows.push({
|
||||
tag: el.tagName,
|
||||
id: el.id,
|
||||
classes: (el.className || "").slice(0, 120),
|
||||
attrs: [].map.call(el.attributes || [], function(a) { return a.name + '=\\"' + a.value.slice(0, 60) + '\\"'; }).join(" ").slice(0, 300),
|
||||
inner: el.innerHTML.replace(/\\s+/g, " ").slice(0, 400),
|
||||
});
|
||||
}
|
||||
}
|
||||
// 3) 归档按钮
|
||||
const archiveBtn = document.querySelector("button[aria-label*='归档'],button[aria-label*='Archive'],button[aria-label*='archive'],button[aria-label*='archiv'],button[title*='归档'],button[title*='Archive'],button[title*='archive']");
|
||||
if (archiveBtn) {
|
||||
info.archive = {
|
||||
tag: archiveBtn.tagName,
|
||||
id: archiveBtn.id,
|
||||
ariaLabel: archiveBtn.getAttribute("aria-label") || "",
|
||||
title: archiveBtn.title,
|
||||
classes: (archiveBtn.className || "").slice(0, 120),
|
||||
outer: archiveBtn.outerHTML.slice(0, 300),
|
||||
};
|
||||
}
|
||||
// 4) 侧边栏内所有按钮
|
||||
info.allButtons = [].map.call(document.querySelectorAll("[class*='sidebar'] button,[class*='thread-list'] button"), function(b) {
|
||||
return { ariaLabel: b.getAttribute("aria-label") || "", title: b.title, classes: (b.className || "").slice(0, 60) };
|
||||
});
|
||||
try {
|
||||
window.__voiceBridge(JSON.stringify({ kind: "debug-sidebar-dump", info: info }));
|
||||
} catch {}
|
||||
}
|
||||
window.__tdDumpSidebarInfo = dumpSidebarInfo;
|
||||
// 周期性重试:侧边栏可能是懒加载的,首次注入时 DOM 可能还没渲染出来
|
||||
let retryCount = 0;
|
||||
const MAX_RETRIES = 10;
|
||||
function retryMountSidebar() {
|
||||
if (retryCount >= MAX_RETRIES) return;
|
||||
retryCount++;
|
||||
setTimeout(function() {
|
||||
const rows = findSidebarRows(document);
|
||||
if (rows.length > 0) {
|
||||
mountSidebarDeleteButtons(document);
|
||||
dumpSidebarInfo();
|
||||
} else {
|
||||
console.log("[TD] retryMountSidebar: attempt " + retryCount + " still no rows, retrying...");
|
||||
retryMountSidebar();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
setTimeout(retryMountSidebar, 2000);
|
||||
|
||||
// —— binding 通道(window.__voiceBridge 由主进程 CDP addBinding 注入)——
|
||||
const pending = new Map();
|
||||
let seq = 0;
|
||||
@@ -126,6 +543,7 @@ const INJECT_SCRIPT = `
|
||||
|
||||
// —— 录音 ——
|
||||
let mediaRecorder = null;
|
||||
let activeStream = null;
|
||||
let chunks = [];
|
||||
let curBtn = null;
|
||||
let stopTimer = null;
|
||||
@@ -150,6 +568,7 @@ const INJECT_SCRIPT = `
|
||||
}
|
||||
|
||||
async function startRecording(btn) {
|
||||
if (disposed) return;
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
toast("此环境不支持麦克风");
|
||||
setBtnState(btn, "error");
|
||||
@@ -169,6 +588,7 @@ const INJECT_SCRIPT = `
|
||||
return;
|
||||
}
|
||||
curBtn = btn;
|
||||
activeStream = stream;
|
||||
chunks = [];
|
||||
const mime = MediaRecorder.isTypeSupported("audio/webm")
|
||||
? "audio/webm"
|
||||
@@ -188,6 +608,8 @@ const INJECT_SCRIPT = `
|
||||
};
|
||||
mediaRecorder.onstop = () => {
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
activeStream = null;
|
||||
if (disposed) return;
|
||||
const blob = new Blob(chunks, { type: mime || "audio/webm" });
|
||||
if (blob.size < 800) {
|
||||
toast("未录到声音");
|
||||
@@ -197,6 +619,7 @@ const INJECT_SCRIPT = `
|
||||
setBtnState(btn, "transcribing");
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
if (disposed) return;
|
||||
const dataUrl = String(reader.result || "");
|
||||
const comma = dataUrl.indexOf(",");
|
||||
const b64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
|
||||
@@ -212,6 +635,7 @@ const INJECT_SCRIPT = `
|
||||
setBtnState(btn, "");
|
||||
};
|
||||
reader.onerror = () => {
|
||||
if (disposed) return;
|
||||
toast("音频读取失败");
|
||||
setBtnState(btn, "error");
|
||||
};
|
||||
@@ -270,6 +694,7 @@ const INJECT_SCRIPT = `
|
||||
}
|
||||
|
||||
function onBtnClick(btn) {
|
||||
if (disposed) return;
|
||||
if (mediaRecorder && mediaRecorder.state === "recording") {
|
||||
stopRecording();
|
||||
return;
|
||||
@@ -307,6 +732,7 @@ const INJECT_SCRIPT = `
|
||||
|
||||
let firstMount = true;
|
||||
function tryMount() {
|
||||
if (disposed) return;
|
||||
if (!firstMount && document.getElementById(BTN_ID)) return; // 已在,避免频繁重建丢焦点
|
||||
firstMount = false;
|
||||
const existing = document.getElementById(BTN_ID);
|
||||
@@ -326,23 +752,66 @@ const INJECT_SCRIPT = `
|
||||
// —— MutationObserver:composer/页面重建后重挂 ——
|
||||
let mo = null;
|
||||
function startObserver() {
|
||||
if (mo) return;
|
||||
mo = new MutationObserver(() => {
|
||||
if (disposed || mo) return;
|
||||
mo = new MutationObserver((records) => {
|
||||
if (disposed) return;
|
||||
if (!document.getElementById(BTN_ID)) { firstMount = true; tryMount(); }
|
||||
for (const record of records) {
|
||||
if (record.target && record.target.nodeType === 1) sanitizeVisibleThoughts(record.target);
|
||||
for (const added of record.addedNodes || []) {
|
||||
if (added.nodeType !== 1) continue;
|
||||
sanitizeVisibleThoughts(added);
|
||||
mountSidebarDeleteButtons(added);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.__voiceInjectObserver = mo; // 供重新安装时 disconnect
|
||||
const root = document.body || document.documentElement;
|
||||
if (root) mo.observe(root, { childList: true, subtree: true });
|
||||
// 首次尝试
|
||||
tryMount();
|
||||
mountSidebarDeleteButtons(root);
|
||||
sanitizeVisibleThoughts(root);
|
||||
}
|
||||
function ensureStarted() {
|
||||
if (disposed) return;
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", startObserver, { once: true });
|
||||
} else {
|
||||
startObserver();
|
||||
}
|
||||
}
|
||||
// 由主进程在“关闭语音输入”时调用。必须在断开 CDP 前执行,
|
||||
// 否则页面里的 observer 仍会在输入框重建时重新挂出麦克风按钮。
|
||||
window.__voiceInjectDispose = () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
if (stopTimer) {
|
||||
clearTimeout(stopTimer);
|
||||
stopTimer = null;
|
||||
}
|
||||
if (mediaRecorder && mediaRecorder.state !== "inactive") {
|
||||
try { mediaRecorder.stop(); } catch {}
|
||||
}
|
||||
if (activeStream) {
|
||||
try { activeStream.getTracks().forEach((track) => track.stop()); } catch {}
|
||||
activeStream = null;
|
||||
}
|
||||
for (const entry of pending.values()) {
|
||||
try { entry.reject(new Error("语音输入已关闭")); } catch {}
|
||||
}
|
||||
pending.clear();
|
||||
if (mo) {
|
||||
try { mo.disconnect(); } catch {}
|
||||
mo = null;
|
||||
}
|
||||
if (window.__voiceInjectObserver) delete window.__voiceInjectObserver;
|
||||
document.getElementById(BTN_ID)?.remove();
|
||||
document.getElementById(STYLE_ID)?.remove();
|
||||
document.querySelectorAll(".codex-voice-toast").forEach((el) => el.remove());
|
||||
if (window.__voiceInjectHandle) delete window.__voiceInjectHandle;
|
||||
if (window.__voiceInjectInstalled) delete window.__voiceInjectInstalled;
|
||||
};
|
||||
ensureStarted();
|
||||
})();
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user