538 lines
20 KiB
JavaScript
538 lines
20 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;
|
|||
|
|
|
|||
|
|
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 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,
|
|||
|
|
windowsProcessDiagnostics,
|
|||
|
|
killCodex,
|
|||
|
|
};
|