739 lines
26 KiB
JavaScript
739 lines
26 KiB
JavaScript
"use strict";
|
||
|
||
const fs = require("fs");
|
||
const os = require("os");
|
||
const path = require("path");
|
||
const { execFile, execFileSync } = require("child_process");
|
||
|
||
const EXE_NAMES = process.platform === "win32"
|
||
? ["ChatGPT.exe", "Codex.exe", "OpenAI ChatGPT.exe", "ChatGPT Desktop.exe", "Codex Desktop.exe"]
|
||
: ["ChatGPT", "Codex"];
|
||
|
||
const WIN_PROCESS_NAMES = EXE_NAMES.filter((n) => /\.exe$/i.test(n));
|
||
let lastDiscovery = null;
|
||
|
||
const GLOBAL_DICTATION_HIDE_SCRIPT = `
|
||
set hiddenCount to 0
|
||
tell application "System Events"
|
||
set targetProcesses to every process whose name is "ChatGPT" or name is "Codex"
|
||
repeat with p in targetProcesses
|
||
tell p
|
||
repeat with w in windows
|
||
try
|
||
set titleText to title of w as text
|
||
set roleText to role of w as text
|
||
set subroleText to subrole of w as text
|
||
set windowSize to size of w
|
||
set widthValue to item 1 of windowSize as integer
|
||
set heightValue to item 2 of windowSize as integer
|
||
if titleText is "Codex" and roleText is "AXWindow" and subroleText is "AXDialog" and widthValue >= 500 and widthValue <= 900 and heightValue >= 60 and heightValue <= 120 then
|
||
set position of w to {-10000, -10000}
|
||
set hiddenCount to hiddenCount + 1
|
||
end if
|
||
end try
|
||
end repeat
|
||
end tell
|
||
end repeat
|
||
end tell
|
||
return hiddenCount
|
||
`;
|
||
|
||
function existsFile(p) {
|
||
try {
|
||
return !!p && fs.statSync(p).isFile();
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function isWindowsPackagedPath(p) {
|
||
return process.platform === "win32" && /[\\/]WindowsApps[\\/]|[\\/]Program Files[\\/]WindowsApps[\\/]/i.test(String(p || ""));
|
||
}
|
||
|
||
function isOwnWindowsAppCandidate(name, appId) {
|
||
const hay = [name, appId].filter(Boolean).join(" ");
|
||
return /tokendun|codexmarket|codex-market|td工具箱|td toolbox|插件市场|技能市场/i.test(hay);
|
||
}
|
||
|
||
function isOpenAiDesktopCandidate(name, appId) {
|
||
if (isOwnWindowsAppCandidate(name, appId)) return false;
|
||
const n = String(name || "").trim();
|
||
const hay = [name, appId].filter(Boolean).join(" ");
|
||
return /openai|chatgpt/i.test(hay) || /(^|[^a-z])codex([^a-z]|$)/i.test(n);
|
||
}
|
||
|
||
function addCandidate(out, seen, label, p, opts = {}) {
|
||
if (!p) return;
|
||
if (opts.launchType === "appId") {
|
||
const appId = String(opts.appId || p || "").trim();
|
||
if (!appId || isOwnWindowsAppCandidate(opts.name || "", appId)) return;
|
||
const key = "appid:" + appId.toLowerCase();
|
||
if (seen.has(key)) return;
|
||
seen.add(key);
|
||
out.push({ label, path: appId, appId, name: opts.name || "", launchType: "appId", packaged: true });
|
||
return;
|
||
}
|
||
let abs = "";
|
||
try {
|
||
abs = path.resolve(p);
|
||
} catch {
|
||
return;
|
||
}
|
||
const key = process.platform === "win32" ? abs.toLowerCase() : abs;
|
||
if (seen.has(key) || !existsFile(abs)) return;
|
||
seen.add(key);
|
||
out.push({
|
||
label,
|
||
path: abs,
|
||
launchType: opts.launchType || "exe",
|
||
packaged: !!opts.packaged || isWindowsPackagedPath(abs),
|
||
});
|
||
}
|
||
|
||
function addChecked(report, p) {
|
||
if (!report || !p) return;
|
||
try {
|
||
report.checkedPaths.push(path.resolve(p));
|
||
} catch {}
|
||
}
|
||
|
||
function shouldScanDir(name, depth) {
|
||
if (!name) return false;
|
||
if (/^(programs)$/i.test(name)) return true;
|
||
if (/codex|chatgpt|openai/i.test(name)) return true;
|
||
return depth === 0 && process.platform !== "win32";
|
||
}
|
||
|
||
function scanDirForExecutables(base, out, seen, opts = {}) {
|
||
const maxDepth = opts.maxDepth == null ? 4 : opts.maxDepth;
|
||
const maxEntries = opts.maxEntries == null ? 400 : opts.maxEntries;
|
||
const maxHits = opts.maxHits == null ? 12 : opts.maxHits;
|
||
const report = opts.report || null;
|
||
const targetNames = new Set(EXE_NAMES.map((n) => process.platform === "win32" ? n.toLowerCase() : n));
|
||
let visited = 0;
|
||
if (report && base) report.scannedRoots.push(base);
|
||
|
||
function walk(dir, depth) {
|
||
if (!dir || depth > maxDepth || out.length >= maxHits || visited >= maxEntries) return;
|
||
let entries;
|
||
try {
|
||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
} catch {
|
||
return;
|
||
}
|
||
visited += entries.length;
|
||
for (const ent of entries) {
|
||
if (out.length >= maxHits) return;
|
||
const full = path.join(dir, ent.name);
|
||
const entName = process.platform === "win32" ? ent.name.toLowerCase() : ent.name;
|
||
if (ent.isFile() && targetNames.has(entName)) {
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(扫描)", full);
|
||
continue;
|
||
}
|
||
if (ent.isDirectory() && depth < maxDepth && shouldScanDir(ent.name, depth)) {
|
||
walk(full, depth + 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
walk(base, 0);
|
||
}
|
||
|
||
function psJson(script, timeout = 3500) {
|
||
if (process.platform !== "win32") return null;
|
||
const shells = ["powershell.exe", "pwsh.exe"];
|
||
for (const shell of shells) {
|
||
try {
|
||
const out = execFileSync(shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
||
encoding: "utf-8",
|
||
windowsHide: true,
|
||
timeout,
|
||
}).trim();
|
||
if (!out) return null;
|
||
return JSON.parse(out);
|
||
} catch {}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function resolveWindowsShortcut(lnkPath) {
|
||
if (process.platform !== "win32" || !lnkPath) return null;
|
||
const script = [
|
||
"$p=$env:TD_LNK_PATH",
|
||
"$s=(New-Object -ComObject WScript.Shell).CreateShortcut($p)",
|
||
"$o=[pscustomobject]@{TargetPath=$s.TargetPath;Arguments=$s.Arguments;WorkingDirectory=$s.WorkingDirectory}",
|
||
"$o|ConvertTo-Json -Compress",
|
||
].join(";");
|
||
const shells = ["powershell.exe", "pwsh.exe"];
|
||
for (const shell of shells) {
|
||
try {
|
||
const out = execFileSync(shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
|
||
encoding: "utf-8",
|
||
env: { ...process.env, TD_LNK_PATH: lnkPath },
|
||
windowsHide: true,
|
||
timeout: 2500,
|
||
}).trim();
|
||
if (!out) return null;
|
||
return JSON.parse(out);
|
||
} catch {}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function collectWindowsShortcuts(base, out, opts = {}) {
|
||
if (process.platform !== "win32" || !base) return;
|
||
const maxDepth = opts.maxDepth == null ? 5 : opts.maxDepth;
|
||
const maxEntries = opts.maxEntries == null ? 900 : opts.maxEntries;
|
||
const maxHits = opts.maxHits == null ? 24 : opts.maxHits;
|
||
let visited = 0;
|
||
|
||
function walk(dir, depth) {
|
||
if (!dir || depth > maxDepth || out.length >= maxHits || visited >= maxEntries) return;
|
||
let entries;
|
||
try {
|
||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
} catch {
|
||
return;
|
||
}
|
||
visited += entries.length;
|
||
for (const ent of entries) {
|
||
if (out.length >= maxHits) return;
|
||
const full = path.join(dir, ent.name);
|
||
if (ent.isFile() && /\.lnk$/i.test(ent.name) && /codex|chatgpt|openai/i.test(ent.name)) {
|
||
out.push(full);
|
||
continue;
|
||
}
|
||
if (ent.isDirectory() && depth < maxDepth && shouldScanDir(ent.name, depth)) {
|
||
walk(full, depth + 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
walk(base, 0);
|
||
}
|
||
|
||
function addSquirrelShortcutTargets(out, seen, report, shortcut, targetPath, args) {
|
||
if (!targetPath || !/update\.exe$/i.test(path.basename(targetPath || ""))) return;
|
||
const m = String(args || "").match(/--processStart\s+(?:"([^"]+)"|([^\s]+))/i);
|
||
const exeName = (m && (m[1] || m[2])) || "";
|
||
if (!exeName || !/\.exe$/i.test(exeName)) return;
|
||
const root = path.dirname(targetPath);
|
||
let entries = [];
|
||
try {
|
||
entries = fs.readdirSync(root, { withFileTypes: true });
|
||
} catch {
|
||
return;
|
||
}
|
||
const appDirs = entries
|
||
.filter((ent) => ent.isDirectory() && /^app-/i.test(ent.name))
|
||
.map((ent) => path.join(root, ent.name))
|
||
.sort()
|
||
.reverse();
|
||
for (const dir of appDirs) {
|
||
const exe = path.join(dir, exeName);
|
||
addChecked(report, exe);
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(开始菜单快捷方式)", exe);
|
||
}
|
||
if (report) report.shortcutTargets.push({ shortcut, targetPath, args: args || "", inferred: exeName });
|
||
}
|
||
|
||
function addWindowsShortcutCandidates(out, seen, report, roots) {
|
||
const shortcuts = [];
|
||
for (const root of roots.filter(Boolean)) collectWindowsShortcuts(root, shortcuts);
|
||
for (const lnk of shortcuts) {
|
||
if (report) report.shortcuts.push(lnk);
|
||
const resolved = resolveWindowsShortcut(lnk);
|
||
if (!resolved) continue;
|
||
const targetPath = resolved.TargetPath || resolved.targetPath || "";
|
||
const args = resolved.Arguments || resolved.arguments || "";
|
||
if (report) report.shortcutTargets.push({ shortcut: lnk, targetPath, args });
|
||
if (targetPath && /\.exe$/i.test(targetPath) && !/update\.exe$/i.test(path.basename(targetPath))) {
|
||
addChecked(report, targetPath);
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(开始菜单快捷方式)", targetPath);
|
||
continue;
|
||
}
|
||
addSquirrelShortcutTargets(out, seen, report, lnk, targetPath, args);
|
||
}
|
||
}
|
||
|
||
function addWindowsAppxCandidates(out, seen, report) {
|
||
const script = [
|
||
"$startApps=Get-StartApps | Where-Object { $_.Name -match 'ChatGPT|Codex|OpenAI' -or $_.AppID -match 'ChatGPT|Codex|OpenAI' }",
|
||
"$startRows=@()",
|
||
"foreach($a in $startApps){ $startRows += [pscustomobject]@{Name=$a.Name;AppID=$a.AppID} }",
|
||
"$pkgs=Get-AppxPackage | Where-Object { $_.Name -match 'ChatGPT|Codex|OpenAI' -or $_.PackageFullName -match 'ChatGPT|Codex|OpenAI' }",
|
||
"$rows=@()",
|
||
"$appIds=@()",
|
||
"foreach($p in $pkgs){",
|
||
" $loc=$p.InstallLocation",
|
||
" if($loc){",
|
||
" $manifestPath=Join-Path $loc 'AppxManifest.xml'",
|
||
" if(Test-Path -LiteralPath $manifestPath){",
|
||
" try{",
|
||
" [xml]$manifest=Get-Content -LiteralPath $manifestPath -ErrorAction Stop",
|
||
" foreach($app in @($manifest.Package.Applications.Application)){",
|
||
" $id=[string]$app.Id",
|
||
" if($id){ $appIds += [pscustomobject]@{Package=$p.Name;Family=$p.PackageFamilyName;AppID=($p.PackageFamilyName + '!' + $id);InstallLocation=$loc} }",
|
||
" }",
|
||
" }catch{}",
|
||
" }",
|
||
" $exes=Get-ChildItem -LiteralPath $loc -Recurse -Filter *.exe -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'ChatGPT|Codex|OpenAI' } | Select-Object -First 8",
|
||
" foreach($e in $exes){ $rows += [pscustomobject]@{Package=$p.Name;Path=$e.FullName} }",
|
||
" }",
|
||
"}",
|
||
"[pscustomobject]@{StartApps=$startRows;Packages=$rows;AppIds=$appIds}|ConvertTo-Json -Depth 5 -Compress",
|
||
].join(";");
|
||
const data = psJson(script, 6500);
|
||
const startApps = data && data.StartApps ? (Array.isArray(data.StartApps) ? data.StartApps : [data.StartApps]) : [];
|
||
const arr = data && data.Packages ? (Array.isArray(data.Packages) ? data.Packages : [data.Packages]) : [];
|
||
const appIds = data && data.AppIds ? (Array.isArray(data.AppIds) ? data.AppIds : [data.AppIds]) : [];
|
||
for (const app of startApps) {
|
||
const appId = app && app.AppID ? String(app.AppID) : "";
|
||
const name = app && app.Name ? String(app.Name) : "";
|
||
if (!appId || !isOpenAiDesktopCandidate(name, appId)) continue;
|
||
if (report) report.appx.push({ package: "StartApps", name: app.Name || "", appId });
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(开始菜单 AppID)", appId, {
|
||
launchType: "appId",
|
||
appId,
|
||
name,
|
||
packaged: true,
|
||
});
|
||
}
|
||
for (const app of appIds) {
|
||
const appId = app && app.AppID ? String(app.AppID) : "";
|
||
const name = app && app.Package ? String(app.Package) : "";
|
||
if (!appId || !isOpenAiDesktopCandidate(name, appId)) continue;
|
||
if (report) report.appx.push({ package: name, family: app.Family || "", appId, manifest: true });
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(MSIX AppID)", appId, {
|
||
launchType: "appId",
|
||
appId,
|
||
name,
|
||
packaged: true,
|
||
});
|
||
}
|
||
for (const row of arr) {
|
||
const p = row && row.Path ? String(row.Path) : "";
|
||
if (report && row) report.appx.push({ package: row.Package || "", path: p });
|
||
addChecked(report, p);
|
||
// Program Files\WindowsApps 里的包内 exe 通常不能被普通进程直接启动;
|
||
// 只记录诊断,不当作启动候选,避免弹系统错误框。
|
||
if (p && !isWindowsPackagedPath(p)) {
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(Microsoft Store/MSIX)", p);
|
||
}
|
||
}
|
||
}
|
||
|
||
function desktopExecutableCandidates() {
|
||
const home = os.homedir();
|
||
const out = [];
|
||
const seen = new Set();
|
||
const report = {
|
||
platform: process.platform,
|
||
checkedPaths: [],
|
||
scannedRoots: [],
|
||
shortcuts: [],
|
||
shortcutTargets: [],
|
||
appx: [],
|
||
candidates: [],
|
||
};
|
||
|
||
if (process.platform === "darwin") {
|
||
[
|
||
path.join(home, "Applications/ChatGPT.app/Contents/MacOS/ChatGPT"),
|
||
"/Applications/ChatGPT.app/Contents/MacOS/ChatGPT",
|
||
path.join(home, "Applications/Codex.app/Contents/MacOS/Codex"),
|
||
"/Applications/Codex.app/Contents/MacOS/Codex",
|
||
].forEach((p) => {
|
||
addChecked(report, p);
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序", p);
|
||
});
|
||
const sorted = sortCandidates(out);
|
||
lastDiscovery = { ...report, candidates: sorted };
|
||
return sorted;
|
||
}
|
||
|
||
if (process.platform === "win32") {
|
||
const local = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
||
const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
||
const programData = process.env.ProgramData || "C:\\ProgramData";
|
||
const pf = process.env.ProgramFiles || "C:\\Program Files";
|
||
const pf86 = process.env["ProgramFiles(x86)"] || "";
|
||
const windowsApps = path.join(local, "Microsoft", "WindowsApps");
|
||
const userStartMenu = path.join(appdata, "Microsoft", "Windows", "Start Menu", "Programs");
|
||
const systemStartMenu = path.join(programData, "Microsoft", "Windows", "Start Menu", "Programs");
|
||
const known = [
|
||
[local, "Programs", "ChatGPT", "ChatGPT.exe"],
|
||
[local, "Programs", "OpenAI", "ChatGPT", "ChatGPT.exe"],
|
||
[local, "Programs", "OpenAI ChatGPT", "ChatGPT.exe"],
|
||
[local, "Programs", "ChatGPT Desktop", "ChatGPT.exe"],
|
||
[local, "ChatGPT", "ChatGPT.exe"],
|
||
[local, "OpenAI", "ChatGPT", "ChatGPT.exe"],
|
||
[local, "OpenAI", "ChatGPT", "bin", "ChatGPT.exe"],
|
||
[pf, "ChatGPT", "ChatGPT.exe"],
|
||
[pf, "OpenAI", "ChatGPT", "ChatGPT.exe"],
|
||
[pf, "OpenAI ChatGPT", "ChatGPT.exe"],
|
||
pf86 && [pf86, "ChatGPT", "ChatGPT.exe"],
|
||
pf86 && [pf86, "OpenAI", "ChatGPT", "ChatGPT.exe"],
|
||
[local, "OpenAI", "Codex", "bin", "Codex.exe"],
|
||
[local, "Programs", "Codex", "Codex.exe"],
|
||
[local, "Programs", "codex", "Codex.exe"],
|
||
[local, "Programs", "OpenAI Codex", "Codex.exe"],
|
||
[local, "Programs", "Codex Desktop", "Codex.exe"],
|
||
[local, "Codex", "Codex.exe"],
|
||
[pf, "Codex", "Codex.exe"],
|
||
[pf, "OpenAI Codex", "Codex.exe"],
|
||
pf86 && [pf86, "Codex", "Codex.exe"],
|
||
].filter(Boolean);
|
||
for (const parts of known) {
|
||
const p = path.join(...parts);
|
||
addChecked(report, p);
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序", p);
|
||
}
|
||
for (const p of [path.join(windowsApps, "ChatGPT.exe"), path.join(windowsApps, "Codex.exe")]) {
|
||
addChecked(report, p);
|
||
if (existsFile(p)) {
|
||
report.appx.push({ package: "WindowsApps alias", path: path.resolve(p) });
|
||
addCandidate(out, seen, "ChatGPT/Codex 桌面程序(WindowsApps 别名)", p, {
|
||
launchType: "alias",
|
||
packaged: true,
|
||
});
|
||
}
|
||
}
|
||
addWindowsShortcutCandidates(out, seen, report, [userStartMenu, systemStartMenu]);
|
||
addWindowsAppxCandidates(out, seen, report);
|
||
for (const base of [path.join(local, "Programs"), local, pf, pf86, windowsApps].filter(Boolean)) {
|
||
scanDirForExecutables(base, out, seen, { maxDepth: 5, maxEntries: 1600, maxHits: 24, report });
|
||
}
|
||
const sorted = sortCandidates(out);
|
||
lastDiscovery = { ...report, candidates: sorted };
|
||
return sorted;
|
||
}
|
||
|
||
lastDiscovery = { ...report, candidates: out };
|
||
return out;
|
||
}
|
||
|
||
function sortCandidates(items) {
|
||
return [...items].sort((a, b) => scoreCandidate(b.path) - scoreCandidate(a.path));
|
||
}
|
||
|
||
function scoreCandidate(p) {
|
||
let score = 0;
|
||
if (/chatgpt/i.test(path.basename(p))) score += 100;
|
||
if (/codex/i.test(path.basename(p))) score += 50;
|
||
if (isWindowsPackagedPath(p)) score -= 1000;
|
||
if (/localappdata|AppData[\\/]Local|Applications/i.test(p)) score += 10;
|
||
if (/programs/i.test(p)) score += 5;
|
||
return score;
|
||
}
|
||
|
||
function codexExecutable() {
|
||
const first = desktopExecutableCandidates().find((item) => !item.packaged);
|
||
return first ? first.path : "";
|
||
}
|
||
|
||
function discoveryReport() {
|
||
return lastDiscovery || {
|
||
platform: process.platform,
|
||
checkedPaths: [],
|
||
scannedRoots: [],
|
||
shortcuts: [],
|
||
shortcutTargets: [],
|
||
appx: [],
|
||
candidates: [],
|
||
};
|
||
}
|
||
|
||
function discoverySummary() {
|
||
const r = discoveryReport();
|
||
const parts = [];
|
||
if (r.checkedPaths && r.checkedPaths.length) parts.push(`固定路径 ${r.checkedPaths.length} 个`);
|
||
if (r.scannedRoots && r.scannedRoots.length) parts.push(`目录扫描 ${r.scannedRoots.length} 处`);
|
||
if (r.shortcuts && r.shortcuts.length) parts.push(`快捷方式 ${r.shortcuts.length} 个`);
|
||
if (r.appx && r.appx.length) parts.push(`Store/MSIX 线索 ${r.appx.length} 个`);
|
||
if (r.candidates && r.candidates.length) parts.push(`命中 ${r.candidates.length} 个`);
|
||
return parts.join(",");
|
||
}
|
||
|
||
function codexRunning() {
|
||
try {
|
||
if (process.platform === "darwin") {
|
||
return execFileSync("pgrep", ["-f", "(Codex.app/Contents/MacOS/Codex|ChatGPT.app/Contents/MacOS/ChatGPT)"], { encoding: "utf-8" }).trim().length > 0;
|
||
}
|
||
if (process.platform === "win32") {
|
||
return windowsProcessRows().length > 0;
|
||
}
|
||
} catch {
|
||
return false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function execFileText(file, args, options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
execFile(file, args, {
|
||
encoding: "utf-8",
|
||
windowsHide: true,
|
||
timeout: options.timeout === undefined
|
||
? 2200
|
||
: Math.max(1, Number(options.timeout) || 1),
|
||
signal: options.signal,
|
||
env: options.env || process.env,
|
||
}, (error, stdout) => {
|
||
if (error) {
|
||
error.stdout = stdout || error.stdout || "";
|
||
reject(error);
|
||
return;
|
||
}
|
||
resolve(String(stdout || ""));
|
||
});
|
||
});
|
||
}
|
||
|
||
function isCodexProcessCommand(command) {
|
||
const text = String(command || "").replace(/\s+/g, " ").trim();
|
||
if (!text || isOwnWindowsAppCandidate(text, "")) return false;
|
||
return /(?:^|[\\/])(?:Codex|ChatGPT)\.app[\\/]/i.test(text) ||
|
||
/(?:^|[\\/\s])(?:Codex|ChatGPT)(?:\.exe)?(?:\s|$)/i.test(text) ||
|
||
/WindowsApps[\\/]OpenAI\.(?:Codex|CodexBeta|ChatGPT-Desktop)_/i.test(text);
|
||
}
|
||
|
||
function hideGlobalDictationWindows(options = {}) {
|
||
if (process.platform !== "darwin") {
|
||
return Promise.resolve({
|
||
ok: false,
|
||
status: "unsupported-platform",
|
||
hidden: 0,
|
||
error: "native global dictation window cleanup is only available on macOS",
|
||
});
|
||
}
|
||
|
||
const run = options.execFile || execFile;
|
||
return new Promise((resolve) => {
|
||
run("osascript", ["-e", GLOBAL_DICTATION_HIDE_SCRIPT], {
|
||
encoding: "utf-8",
|
||
windowsHide: true,
|
||
timeout: Math.max(500, Number(options.timeoutMs) || 5000),
|
||
signal: options.signal,
|
||
}, (error, stdout, stderr) => {
|
||
const output = String(stdout || "").trim();
|
||
const hidden = /^\d+$/.test(output) ? Number(output) : 0;
|
||
if (error) {
|
||
resolve({
|
||
ok: false,
|
||
status: "native-command-failed",
|
||
hidden,
|
||
error: String((error && error.message) || stderr || error),
|
||
});
|
||
return;
|
||
}
|
||
resolve({
|
||
ok: true,
|
||
status: hidden ? "hidden" : "not-found",
|
||
hidden,
|
||
error: "",
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
async function debugPortOwner(port, options = {}) {
|
||
const value = Number(port);
|
||
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
||
return { verified: false, owned: false, reason: "invalid-port", processes: [] };
|
||
}
|
||
const signal = options.signal;
|
||
const totalTimeoutMs = Math.max(1, Number(options.timeoutMs) || 5000);
|
||
const deadline = Date.now() + totalTimeoutMs;
|
||
const commandOptions = (maxMs) => {
|
||
if (signal && signal.aborted) {
|
||
throw signal.reason instanceof Error ? signal.reason : new Error("debug port owner check aborted");
|
||
}
|
||
const remaining = deadline - Date.now();
|
||
if (remaining <= 0) {
|
||
const error = new Error("debug port owner check timed out");
|
||
error.code = "ETIMEDOUT";
|
||
throw error;
|
||
}
|
||
return {
|
||
timeout: Math.max(1, Math.min(maxMs, remaining)),
|
||
signal,
|
||
};
|
||
};
|
||
|
||
if (process.platform === "darwin") {
|
||
try {
|
||
const output = await execFileText("lsof", [
|
||
"-nP",
|
||
"-a",
|
||
`-iTCP:${value}`,
|
||
"-sTCP:LISTEN",
|
||
"-Fp",
|
||
], commandOptions(2200));
|
||
const pids = [...new Set(
|
||
output.split(/\r?\n/).filter((line) => /^p\d+$/.test(line)).map((line) => line.slice(1)),
|
||
)];
|
||
const processes = [];
|
||
for (const pid of pids) {
|
||
try {
|
||
const command = (await execFileText(
|
||
"ps",
|
||
["-p", pid, "-o", "command="],
|
||
commandOptions(2200),
|
||
)).trim();
|
||
processes.push({ pid: Number(pid), command });
|
||
} catch {}
|
||
}
|
||
return {
|
||
verified: true,
|
||
owned: processes.some((item) => isCodexProcessCommand(item.command)),
|
||
reason: processes.length ? "process-inspected" : "listener-not-found",
|
||
processes,
|
||
};
|
||
} catch (e) {
|
||
return {
|
||
verified: false,
|
||
owned: false,
|
||
reason: String((e && e.message) || e),
|
||
processes: [],
|
||
};
|
||
}
|
||
}
|
||
|
||
if (process.platform === "win32") {
|
||
const script = [
|
||
"$port=[int]$env:TD_CODEX_DEBUG_PORT",
|
||
"$rows=@()",
|
||
"Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue | ForEach-Object {",
|
||
" $p=Get-CimInstance Win32_Process -Filter ('ProcessId=' + $_.OwningProcess) -ErrorAction SilentlyContinue",
|
||
" if($p){ $rows += [pscustomobject]@{ProcessId=$p.ProcessId;Name=$p.Name;ExecutablePath=$p.ExecutablePath;CommandLine=$p.CommandLine} }",
|
||
"}",
|
||
"$rows|ConvertTo-Json -Depth 4 -Compress",
|
||
].join(";");
|
||
for (const shell of ["powershell.exe", "pwsh.exe"]) {
|
||
try {
|
||
const output = (await execFileText(shell, [
|
||
"-NoProfile",
|
||
"-ExecutionPolicy",
|
||
"Bypass",
|
||
"-Command",
|
||
script,
|
||
], {
|
||
...commandOptions(3500),
|
||
env: { ...process.env, TD_CODEX_DEBUG_PORT: String(value) },
|
||
})).trim();
|
||
const parsed = output ? JSON.parse(output) : [];
|
||
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
||
const processes = rows.map((row) => ({
|
||
pid: Number(row.ProcessId) || 0,
|
||
command: [row.Name, row.ExecutablePath, row.CommandLine].filter(Boolean).join(" "),
|
||
}));
|
||
return {
|
||
verified: true,
|
||
owned: processes.some((item) => isCodexProcessCommand(item.command)),
|
||
reason: processes.length ? "process-inspected" : "listener-not-found",
|
||
processes,
|
||
};
|
||
} catch {}
|
||
}
|
||
return { verified: false, owned: false, reason: "process-inspection-failed", processes: [] };
|
||
}
|
||
|
||
return { verified: false, owned: false, reason: "unsupported-platform", processes: [] };
|
||
}
|
||
|
||
function windowsProcessRows() {
|
||
if (process.platform !== "win32") return [];
|
||
const escapedNames = WIN_PROCESS_NAMES.map((name) => "'" + name.replace(/'/g, "''") + "'").join(",");
|
||
const script = [
|
||
`$names=@(${escapedNames})`,
|
||
"$pkg='WindowsApps\\\\OpenAI\\.(Codex|CodexBeta|ChatGPT-Desktop)_'",
|
||
"$rows=Get-CimInstance Win32_Process | Where-Object { $names -contains $_.Name -or ($_.ExecutablePath -match $pkg) -or ($_.CommandLine -match $pkg) } | ForEach-Object {",
|
||
" [pscustomobject]@{Name=$_.Name;ProcessId=$_.ProcessId;ExecutablePath=$_.ExecutablePath;CommandLine=$_.CommandLine}",
|
||
"}",
|
||
"$rows|ConvertTo-Json -Depth 4 -Compress",
|
||
].join(";");
|
||
const data = psJson(script, 4500);
|
||
if (!data) return [];
|
||
return Array.isArray(data) ? data : [data];
|
||
}
|
||
|
||
function windowsProcessDiagnostics(port) {
|
||
const rows = windowsProcessRows();
|
||
const portRe = new RegExp("--remote-debugging-port(?:=|\\s+)" + String(port || ""), "i");
|
||
const hasDebugArg = rows.some((row) => portRe.test(String((row && row.CommandLine) || "")));
|
||
const summary = rows.map((row) => {
|
||
const cmd = String((row && row.CommandLine) || "").replace(/\s+/g, " ").slice(0, 260);
|
||
const name = (row && row.Name) || "process";
|
||
const pid = (row && row.ProcessId) || "?";
|
||
return `${name}#${pid}${cmd ? " " + cmd : ""}`;
|
||
}).join(" | ");
|
||
return { rows, hasDebugArg, summary };
|
||
}
|
||
|
||
function waitForCodexExit(maxMs = 9000) {
|
||
return new Promise((resolve) => {
|
||
const t0 = Date.now();
|
||
const tick = () => {
|
||
if (!codexRunning()) {
|
||
resolve(true);
|
||
return;
|
||
}
|
||
if (Date.now() - t0 >= maxMs) {
|
||
resolve(false);
|
||
return;
|
||
}
|
||
setTimeout(tick, 300);
|
||
};
|
||
tick();
|
||
});
|
||
}
|
||
|
||
function killCodex() {
|
||
return new Promise((resolve) => {
|
||
if (process.platform === "darwin") {
|
||
execFile("osascript", ["-e", 'tell application "Codex" to quit', "-e", 'tell application "ChatGPT" to quit'], () => {
|
||
execFile("pkill", ["-f", "(Codex.app/Contents/MacOS/Codex|ChatGPT.app/Contents/MacOS/ChatGPT)"], () => {
|
||
waitForCodexExit(12000).then((exited) => resolve({ exited }));
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
if (process.platform === "win32") {
|
||
let pending = WIN_PROCESS_NAMES.length;
|
||
const done = () => {
|
||
pending -= 1;
|
||
if (pending <= 0) {
|
||
waitForCodexExit(9000).then((ok) => {
|
||
if (!ok) {
|
||
try {
|
||
execFileSync("powershell.exe", [
|
||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
|
||
"$pkg='WindowsApps\\\\OpenAI\\.(Codex|CodexBeta|ChatGPT-Desktop)_'; Get-CimInstance Win32_Process | Where-Object { @('ChatGPT.exe','Codex.exe','OpenAI ChatGPT.exe','ChatGPT Desktop.exe','Codex Desktop.exe') -contains $_.Name -or ($_.ExecutablePath -match $pkg) -or ($_.CommandLine -match $pkg) } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
|
||
], { windowsHide: true, timeout: 3500 });
|
||
} catch {}
|
||
}
|
||
waitForCodexExit(5000).then((exited) => resolve({ exited }));
|
||
});
|
||
}
|
||
};
|
||
for (const name of WIN_PROCESS_NAMES) execFile("taskkill", ["/IM", name, "/T", "/F"], done);
|
||
return;
|
||
}
|
||
resolve({ exited: true });
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
desktopExecutableCandidates,
|
||
codexExecutable,
|
||
isWindowsPackagedPath,
|
||
discoveryReport,
|
||
discoverySummary,
|
||
codexRunning,
|
||
debugPortOwner,
|
||
hideGlobalDictationWindows,
|
||
windowsProcessDiagnostics,
|
||
killCodex,
|
||
};
|