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

538 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};