Files
codex-market-app/electron/lib/codex_desktop.js
T

538 lines
20 KiB
JavaScript
Raw Normal View History

"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,
};