"use strict"; const fs = require("fs"); const os = require("os"); const path = require("path"); const KEEP_VISIBLE_KEY = "global-dictation-keep-visible"; const GLOBAL_DICTATION_ROUTE = "/global-dictation"; const DEFAULT_HTTP_TIMEOUT_MS = 1500; const DEFAULT_OWNER_TIMEOUT_MS = 5000; const DEFAULT_CLOSE_SETTLE_MS = 100; const DEFAULT_CLOSE_RETRIES = 2; const RETRYABLE_IO_CODES = new Set(["EACCES", "EBUSY", "EEXIST", "ENOTEMPTY", "EPERM"]); let codexHome = path.join(os.homedir(), ".codex"); let watchedStateFile = ""; let guardLog = () => {}; let guardBusy = false; let endpointOwnerVerifier = null; const cleanupFlights = new Map(); function sleepSync(ms) { if (!ms) return; Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } function retryIo(fn) { const delays = [0, 10, 25, 50, 100, 200]; let lastError; for (const delay of delays) { if (delay) sleepSync(delay); try { return fn(); } catch (e) { lastError = e; if (!RETRYABLE_IO_CODES.has(e && e.code)) throw e; } } throw lastError; } function setCodexDataDir(dir) { if (typeof dir !== "string" || !dir.trim() || !path.isAbsolute(dir.trim())) { throw new Error("Codex data directory must be an absolute path"); } codexHome = path.resolve(dir.trim()); } function setEndpointOwnerVerifier(verifier) { endpointOwnerVerifier = typeof verifier === "function" ? verifier : null; } function stateFileFor(home = codexHome) { return path.join(home, ".codex-global-state.json"); } function acquireLock(file, waitMs = 600) { const lockFile = file + ".td-dictation.lock"; const deadline = Date.now() + Math.max(0, waitMs); while (true) { try { const fd = fs.openSync(lockFile, "wx", 0o600); return { fd, lockFile }; } catch (e) { if (!e || e.code !== "EEXIST") throw e; try { const ageMs = Date.now() - fs.statSync(lockFile).mtimeMs; if (ageMs > 30000) { fs.rmSync(lockFile, { force: true }); continue; } } catch {} if (Date.now() >= deadline) return null; sleepSync(25); } } } function releaseLock(lock) { if (!lock) return; try { fs.closeSync(lock.fd); } catch {} try { fs.rmSync(lock.lockFile, { force: true }); } catch {} } function writeCompleteTemp(file, content, mode, label) { const tmp = `${file}.${process.pid}.${Date.now()}.${label}.tmp`; const fd = fs.openSync(tmp, "wx", mode || 0o600); try { fs.writeFileSync(fd, content); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } try { fs.chmodSync(tmp, mode || 0o600); } catch {} return tmp; } function writeBackupOnce(file, content, mode) { const backup = file + ".td-before-global-dictation.bak"; let tmp = ""; try { tmp = writeCompleteTemp(backup, content, mode, "backup"); fs.linkSync(tmp, backup); } catch (e) { if (!e || e.code !== "EEXIST") throw e; } finally { if (tmp) { try { fs.rmSync(tmp, { force: true }); } catch {} } } return backup; } function isJsonWhitespaceByte(value) { return value === 0x20 || value === 0x09 || value === 0x0a || value === 0x0d; } function patchKeepVisibleFalse(file, original) { const text = original.toString("latin1"); const pattern = new RegExp(`"${KEEP_VISIBLE_KEY}"\\s*:\\s*true`, "g"); let match; let valueOffset = -1; while ((match = pattern.exec(text))) { valueOffset = match.index + match[0].lastIndexOf("true"); } if (valueOffset < 1 || !isJsonWhitespaceByte(original[valueOffset - 1])) { return { ok: false, changed: false, reason: "unsafe-format" }; } const patchOffset = valueOffset - 1; let fd; try { fd = retryIo(() => fs.openSync(file, "r+")); const current = retryIo(() => fs.readFileSync(fd)); if (!current.equals(original)) { return { ok: false, changed: false, reason: "concurrent-write" }; } const descriptorStat = fs.fstatSync(fd); const pathStat = fs.statSync(file); if (descriptorStat.dev !== pathStat.dev || descriptorStat.ino !== pathStat.ino) { return { ok: false, changed: false, reason: "concurrent-write" }; } const patch = Buffer.from("false", "ascii"); const written = fs.writeSync(fd, patch, 0, patch.length, patchOffset); if (written !== patch.length) throw new Error("global dictation state short write"); fs.fsyncSync(fd); } finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch {} } } const saved = retryIo(() => fs.readFileSync(file)); try { const parsed = JSON.parse(saved.toString("utf8")); if (parsed && parsed[KEEP_VISIBLE_KEY] === false) { return { ok: true, changed: true, reason: "disabled" }; } } catch {} return { ok: false, changed: false, reason: "concurrent-write" }; } function sleepAsync(ms, signal) { return new Promise((resolve, reject) => { if (signal && signal.aborted) { reject(signal.reason instanceof Error ? signal.reason : abortError("request aborted")); return; } const timer = setTimeout(done, Math.max(0, Number(ms) || 0)); function done() { if (signal) signal.removeEventListener("abort", aborted); resolve(); } function aborted() { clearTimeout(timer); signal.removeEventListener("abort", aborted); reject(signal.reason instanceof Error ? signal.reason : abortError("request aborted")); } if (signal) signal.addEventListener("abort", aborted, { once: true }); }); } function createFileIfMissing(file, content, mode) { let tmp = ""; try { tmp = writeCompleteTemp(file, content, mode, "td-dictation-create"); try { fs.linkSync(tmp, file); } catch (e) { if (e && e.code === "EEXIST") return false; throw e; } const written = retryIo(() => fs.readFileSync(file)); return written.equals(content); } finally { if (tmp) { try { fs.rmSync(tmp, { force: true }); } catch {} } } } function rewriteStateAtomically(file, original, state, mode) { const next = Buffer.from(JSON.stringify(state), "utf8"); if (next.equals(original)) { return { ok: true, changed: false, reason: "already-disabled" }; } let tmp = ""; try { const current = retryIo(() => fs.readFileSync(file)); if (!current.equals(original)) { return { ok: false, changed: false, reason: "concurrent-write" }; } tmp = writeCompleteTemp(file, next, mode, "td-dictation-atomic"); const beforeRename = retryIo(() => fs.readFileSync(file)); if (!beforeRename.equals(original)) { return { ok: false, changed: false, reason: "concurrent-write" }; } fs.renameSync(tmp, file); tmp = ""; const saved = retryIo(() => fs.readFileSync(file)); const parsed = JSON.parse(saved.toString("utf8")); if (parsed && parsed[KEEP_VISIBLE_KEY] === false) { return { ok: true, changed: true, reason: "disabled-atomic" }; } return { ok: false, changed: false, reason: "verification-failed" }; } finally { if (tmp) { try { fs.rmSync(tmp, { force: true }); } catch {} } } } function disableKeepVisible(options = {}) { const home = options.codexHome ? path.resolve(options.codexHome) : codexHome; const file = stateFileFor(home); let lock; try { lock = acquireLock(file, options.lockWaitMs); if (!lock) return { ok: false, changed: false, reason: "locked", file }; for (let attempt = 0; attempt < 5; attempt++) { if (!fs.existsSync(file)) { const next = Buffer.from(JSON.stringify({ [KEEP_VISIBLE_KEY]: false }, null, 2) + "\n", "utf8"); if (createFileIfMissing(file, next, 0o600)) { return { ok: true, changed: true, reason: "created-disabled", file }; } continue; } const original = retryIo(() => fs.readFileSync(file)); let state; try { state = JSON.parse(original.toString("utf8")); } catch (e) { return { ok: false, changed: false, reason: "invalid-json", error: e.message, file }; } if (!state || typeof state !== "object" || Array.isArray(state)) { return { ok: false, changed: false, reason: "invalid-state", file }; } if (state[KEEP_VISIBLE_KEY] === false) { return { ok: true, changed: false, reason: "already-disabled", file }; } state[KEEP_VISIBLE_KEY] = false; const mode = fs.statSync(file).mode & 0o777; writeBackupOnce(file, original, mode); const patched = patchKeepVisibleFalse(file, original); if (patched.ok) return { ...patched, file }; if (patched.reason === "unsafe-format") { const rewritten = rewriteStateAtomically(file, original, state, mode); if (rewritten.ok) return { ...rewritten, file }; if (rewritten.reason === "concurrent-write") continue; return { ...rewritten, file }; } if (patched.reason === "concurrent-write") continue; return { ...patched, file }; } return { ok: false, changed: false, reason: "concurrent-write", file }; } catch (e) { return { ok: false, changed: false, reason: "write-failed", error: String((e && e.message) || e), file, }; } finally { releaseLock(lock); } } function decodeRoute(value) { let route = String(value || "").trim(); for (let i = 0; i < 2 && /%[0-9a-f]{2}/i.test(route); i++) { try { route = decodeURIComponent(route); } catch { break; } } if (route && !route.startsWith("/")) route = "/" + route; return route.toLowerCase(); } function parseCodexPageUrl(rawUrl) { try { const url = new URL(String(rawUrl || "")); const pathname = url.pathname.toLowerCase(); if ( url.protocol !== "app:" || url.hostname !== "-" || (pathname !== "/index.html" && pathname !== "/index.html/") ) return null; return url; } catch {} return null; } function routeInfo(rawUrl) { const url = parseCodexPageUrl(rawUrl); if (!url) return { url: null, present: false, route: "" }; for (const [key, value] of url.searchParams.entries()) { if (key.toLowerCase() === "initialroute") { return { url, present: true, route: decodeRoute(value) }; } } return { url, present: false, route: "" }; } function initialRoute(rawUrl) { return routeInfo(rawUrl).route; } function isCodexMainTarget(target) { if (!target || String(target.type || "").toLowerCase() !== "page") return false; const info = routeInfo(target.url); return !!info.url && !info.present; } function isGlobalDictationTarget(target) { if (!target || String(target.type || "").toLowerCase() !== "page") return false; const info = routeInfo(target.url); return !!info.url && info.present && info.route === GLOBAL_DICTATION_ROUTE; } async function fetchWithTimeout(fetchImpl, url, timeoutMs, signal) { return runWithTimeout( (requestSignal) => fetchImpl(url, { signal: requestSignal }), timeoutMs, signal, ); } function abortError(message, code) { const error = new Error(message); error.name = "AbortError"; if (code) error.code = code; return error; } async function runWithTimeout(task, timeoutMs, signal) { const controller = new AbortController(); let timer; let abortListener; const interrupted = new Promise((_, reject) => { timer = setTimeout(() => { const error = abortError("request timed out", "ETIMEDOUT"); controller.abort(error); reject(error); }, Math.max(1, Number(timeoutMs) || DEFAULT_HTTP_TIMEOUT_MS)); if (signal) { abortListener = () => { const reason = signal.reason instanceof Error ? signal.reason : abortError(signal.reason ? String(signal.reason) : "request aborted"); controller.abort(reason); reject(reason); }; if (signal.aborted) abortListener(); else signal.addEventListener("abort", abortListener, { once: true }); } }); try { return await Promise.race([ Promise.resolve().then(() => task(controller.signal)), interrupted, ]); } finally { clearTimeout(timer); if (signal && abortListener) signal.removeEventListener("abort", abortListener); } } async function listTargets(port, options = {}) { const fetchImpl = options.fetchImpl || global.fetch; if (typeof fetchImpl !== "function") { return { available: false, status: "failed", targets: [], error: "fetch unavailable" }; } try { const { response, targets } = await runWithTimeout( async (requestSignal) => { const response = await fetchImpl(`http://127.0.0.1:${Number(port)}/json`, { signal: requestSignal, }); const targets = response && response.ok ? await response.json() : null; return { response, targets }; }, options.timeoutMs || DEFAULT_HTTP_TIMEOUT_MS, options.signal, ); if (!response || !response.ok) { return { available: false, status: "port-unavailable", targets: [], error: response ? `HTTP ${response.status || "error"}` : "empty response", }; } if (!Array.isArray(targets)) { return { available: false, status: "failed", targets: [], error: "invalid target list" }; } return { available: true, status: "available", targets, error: "" }; } catch (e) { const aborted = !!(options.signal && options.signal.aborted); const timedOut = !!(e && e.code === "ETIMEDOUT"); return { available: false, status: aborted ? "aborted" : timedOut ? "timeout" : "port-unavailable", targets: [], error: String((e && e.message) || e), }; } } async function runCloseGlobalDictationTargets(port, options = {}) { const fetchImpl = options.fetchImpl || global.fetch; const listed = Array.isArray(options.targets) ? { available: true, status: "available", targets: options.targets, error: "" } : await listTargets(port, options); const matches = listed.targets.filter(isGlobalDictationTarget); const result = { ok: listed.available, status: listed.status, available: listed.available, matched: matches.length, closed: 0, remaining: listed.available ? matches.length : null, targetIds: matches.map((target) => target.id).filter(Boolean), errors: [], }; if (!listed.available || typeof fetchImpl !== "function") { result.status = listed.status || "port-unavailable"; if (listed.error) result.errors.push(listed.error); return result; } if (!matches.length) { result.status = "not-found"; result.remaining = 0; return result; } if (!listed.targets.some(isCodexMainTarget)) { const verifier = options.endpointOwnerVerifier || endpointOwnerVerifier; let ownership = null; if (typeof verifier === "function") { try { ownership = await runWithTimeout( (requestSignal) => verifier(Number(port), { signal: requestSignal, timeoutMs: options.ownerTimeoutMs || DEFAULT_OWNER_TIMEOUT_MS, }), options.ownerTimeoutMs || DEFAULT_OWNER_TIMEOUT_MS, options.signal, ); } catch (e) { ownership = { verified: false, owned: false, reason: String((e && e.message) || e), }; } } const owned = ownership === true || !!(ownership && ownership.owned); if (!owned) { result.ok = false; result.status = ownership && ownership.verified ? "foreign-endpoint" : "unverified-endpoint"; result.errors.push((ownership && ownership.reason) || "Codex endpoint ownership could not be verified"); return result; } } const closeResults = await Promise.all(matches.map(async (target) => { if (!target.id) { return { closed: false, error: "global dictation target has no id" }; } try { const response = await fetchWithTimeout( fetchImpl, `http://127.0.0.1:${Number(port)}/json/close/${encodeURIComponent(target.id)}`, options.timeoutMs || DEFAULT_HTTP_TIMEOUT_MS, options.signal, ); if (response && response.ok) return { closed: true, error: "" }; return { closed: false, error: `failed to close target ${target.id}` }; } catch (e) { return { closed: false, error: String((e && e.message) || e) }; } })); result.closed = closeResults.filter((item) => item.closed).length; result.errors.push(...closeResults.map((item) => item.error).filter(Boolean)); if (result.errors.length) { result.ok = false; result.status = options.signal && options.signal.aborted ? "aborted" : "failed"; return result; } const settleMs = options.settleMs === undefined ? DEFAULT_CLOSE_SETTLE_MS : Math.max(0, Number(options.settleMs) || 0); if (settleMs) { try { await sleepAsync(settleMs, options.signal); } catch (e) { result.ok = false; result.status = "aborted"; result.remaining = null; result.errors.push(String((e && e.message) || e)); return result; } } const verified = await listTargets(port, options); if (!verified.available) { result.ok = false; result.status = verified.status === "aborted" ? "aborted" : "verification-failed"; result.remaining = null; if (verified.error) result.errors.push(verified.error); return result; } const remaining = verified.targets.filter(isGlobalDictationTarget); result.remaining = remaining.length; if (remaining.length) { result.ok = false; result.status = "still-present"; result.targetIds = remaining.map((target) => target.id).filter(Boolean); return result; } result.status = "closed"; return result; } async function runCloseGlobalDictationWithRetry(port, options = {}) { const retries = options.retries === undefined ? DEFAULT_CLOSE_RETRIES : Math.max(0, Number(options.retries) || 0); const retryDelayMs = options.retryDelayMs === undefined ? 120 : Math.max(0, Number(options.retryDelayMs) || 0); let totalClosed = 0; let lastResult = null; for (let attempt = 0; attempt <= retries; attempt++) { const attemptOptions = attempt === 0 ? options : { ...options, targets: undefined }; lastResult = await runCloseGlobalDictationTargets(port, attemptOptions); totalClosed += lastResult.closed || 0; if (lastResult.status !== "still-present") { return { ...lastResult, closed: totalClosed, attempts: attempt + 1 }; } if (attempt < retries && retryDelayMs) { try { await sleepAsync(retryDelayMs, options.signal); } catch (e) { return { ...lastResult, ok: false, status: "aborted", closed: totalClosed, remaining: null, attempts: attempt + 1, errors: [...lastResult.errors, String((e && e.message) || e)], }; } } } return { ...lastResult, closed: totalClosed, attempts: retries + 1 }; } function closeGlobalDictationTargets(port, options = {}) { const hasCustomOptions = !!options.fetchImpl || !!options.signal || !!options.endpointOwnerVerifier || Array.isArray(options.targets) || options.timeoutMs !== undefined || options.ownerTimeoutMs !== undefined || options.settleMs !== undefined || options.retries !== undefined || options.retryDelayMs !== undefined; if (hasCustomOptions) return runCloseGlobalDictationWithRetry(port, options); const key = String(Number(port)); if (cleanupFlights.has(key)) return cleanupFlights.get(key); const flight = runCloseGlobalDictationWithRetry(port, options).finally(() => { if (cleanupFlights.get(key) === flight) cleanupFlights.delete(key); }); cleanupFlights.set(key, flight); return flight; } function enforceGuard() { if (guardBusy) return; guardBusy = true; try { const result = disableKeepVisible(); if (result.changed) guardLog("已关闭 Codex 全局听写栏常驻显示"); else if (!result.ok && result.reason !== "locked") { guardLog(`关闭 Codex 全局听写栏常驻显示失败:${result.error || result.reason}`); } } finally { guardBusy = false; } } function startGuard(options = {}) { stopGuard(); if (options.codexHome) setCodexDataDir(options.codexHome); guardLog = typeof options.log === "function" ? options.log : () => {}; watchedStateFile = stateFileFor(); const initial = disableKeepVisible(); fs.watchFile(watchedStateFile, { interval: options.intervalMs || 2000, persistent: false }, enforceGuard); return initial; } function stopGuard() { if (watchedStateFile) { try { fs.unwatchFile(watchedStateFile, enforceGuard); } catch {} } watchedStateFile = ""; guardLog = () => {}; } module.exports = { KEEP_VISIBLE_KEY, GLOBAL_DICTATION_ROUTE, setCodexDataDir, setEndpointOwnerVerifier, disableKeepVisible, startGuard, stopGuard, isCodexMainTarget, isGlobalDictationTarget, closeGlobalDictationTargets, _initialRoute: initialRoute, _listTargets: listTargets, _stateFileFor: stateFileFor, };