minecodex 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,8 +14,10 @@ export const CODEX_APP_PATH = "/Applications/ChatGPT.app";
14
14
  export const CODEX_EXECUTABLE = `${CODEX_APP_PATH}/Contents/MacOS/ChatGPT`;
15
15
  export const CODEX_BUNDLE_IDENTIFIER = "com.openai.codex";
16
16
  export const CODEX_TEAM_IDENTIFIER = "2DC432GLL2";
17
+ export const CODEX_WINDOWS_PACKAGE_FAMILY = "OpenAI.Codex_2p2nqsd0c76g0";
17
18
  export const MIN_NODE_VERSION = Object.freeze({ major: 22, minor: 5, patch: 0 });
18
19
  export const MIN_MACOS_VERSION = Object.freeze({ major: 13, darwinMajor: 22 });
20
+ export const WINDOWS_SERVICE_TASK_NAME = "MineCodex";
19
21
 
20
22
  function xml(value) {
21
23
  return String(value)
@@ -175,6 +177,59 @@ export function serviceIsAbsent(error) {
175
177
  );
176
178
  }
177
179
 
180
+ // PowerShell 统一经 -EncodedCommand 执行:base64(UTF-16LE) 规避引号与编码问题。
181
+ function encodePowerShellCommand(script) {
182
+ return Buffer.from(String(script), "utf16le").toString("base64");
183
+ }
184
+
185
+ // Windows 控制台默认代码页不是 UTF-8,脚本内强制统一输出编码,
186
+ // 保证中文用户名等路径在 node 侧按 utf8 解码不失真。
187
+ const POWERSHELL_PREAMBLE = "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $ErrorActionPreference = 'Stop';";
188
+
189
+ export function isExactCodexProcessWindows(command, installation) {
190
+ const executable = String(installation?.executable ?? "");
191
+ if (!executable) return false;
192
+ const value = String(command ?? "").trim();
193
+ if (!value) return false;
194
+ // Electron 子进程使用同一 exe,接管列表只能包含主进程。
195
+ if (/(?:^|\s)"?--type(?:=|\s)/.test(value)) return false;
196
+ // Windows 命令行对含空格路径加引号包裹("C:\...\ChatGPT.exe" args),先剥引号再比较。
197
+ if (value.startsWith('"')) {
198
+ const closing = value.indexOf('"', 1);
199
+ if (closing === -1) return false;
200
+ if (value.slice(1, closing).toLowerCase() !== executable.toLowerCase()) return false;
201
+ const rest = value.slice(closing + 1);
202
+ return rest === "" || rest.startsWith(" ");
203
+ }
204
+ const lowerValue = value.toLowerCase();
205
+ const lowerExecutable = executable.toLowerCase();
206
+ return lowerValue === lowerExecutable || lowerValue.startsWith(lowerExecutable + " ");
207
+ }
208
+
209
+ export function buildWindowsServiceCommand({ nodePath, cliPath, paths }) {
210
+ return [
211
+ "@echo off",
212
+ "rem MineCodex background service entry. Managed by mcx install; do not edit.",
213
+ 'cd /d "%~dp0"',
214
+ `${cmdQuote(nodePath)} ${cmdQuote(cliPath)} serve >>${cmdQuote(paths.serviceLogPath)} 2>${cmdQuote(paths.serviceErrorLogPath)}`,
215
+ "",
216
+ ].join("\r\n");
217
+ }
218
+
219
+ // cmd 的引号转义是内部双引号翻倍,与 JSON 转义规则不同,不能混用。
220
+ function cmdQuote(value) {
221
+ return '"' + String(value).replaceAll('"', '""') + '"';
222
+ }
223
+
224
+ export function buildWindowsServiceRuntimeConfig({ nodePath, cliPath, paths }) {
225
+ return JSON.stringify({
226
+ nodePath,
227
+ cliPath,
228
+ serviceLogPath: paths.serviceLogPath,
229
+ serviceErrorLogPath: paths.serviceErrorLogPath,
230
+ });
231
+ }
232
+
178
233
  async function waitForProcessExit(pid, { processApi, timeoutMs = 8_000, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) }) {
179
234
  const deadline = Date.now() + timeoutMs;
180
235
  while (Date.now() < deadline) {
@@ -690,3 +745,465 @@ export class MacPlatformAdapter {
690
745
  throw new Error(`LaunchServices did not expose a loopback CDP ChatGPT process on port ${cdpPort}.`);
691
746
  }
692
747
  }
748
+
749
+ export class WindowsPlatformAdapter {
750
+ constructor({
751
+ platform = process.platform,
752
+ homeDir = os.homedir(),
753
+ appDataDir = process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"),
754
+ localAppDataDir = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local"),
755
+ execFile = defaultExecFile,
756
+ fileSystem = defaultFileSystem,
757
+ processApi = process,
758
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
759
+ codexInstallation,
760
+ } = {}) {
761
+ this.platform = platform;
762
+ this.homeDir = homeDir;
763
+ this.appDataDir = appDataDir;
764
+ this.localAppDataDir = localAppDataDir;
765
+ this.execFile = execFile;
766
+ this.fileSystem = fileSystem;
767
+ this.processApi = processApi;
768
+ this.sleep = sleep;
769
+ this.codexInstallation = codexInstallation;
770
+ this.codexInstallationFingerprint = null;
771
+ this.codexInstallationInjected = Boolean(codexInstallation);
772
+ }
773
+
774
+ async powershell(script) {
775
+ const { stdout } = await this.execFile("powershell.exe", [
776
+ "-NoProfile", "-NonInteractive", "-EncodedCommand",
777
+ encodePowerShellCommand(POWERSHELL_PREAMBLE + "\n" + String(script)),
778
+ ]);
779
+ return String(stdout ?? "");
780
+ }
781
+
782
+ assertInstallSupported() {
783
+ if (this.platform !== "win32") {
784
+ throw new Error("Windows installation requires Windows 10 or newer (win32).");
785
+ }
786
+ }
787
+
788
+ async assertCodexInstalled() {
789
+ const script = [
790
+ "$pkg = Get-AppxPackage -Name 'OpenAI.Codex' -ErrorAction Stop | Select-Object -First 1;",
791
+ "if (-not $pkg) { throw 'Codex Desktop (OpenAI.Codex) was not found for the current user.' }",
792
+ "$exe = Join-Path $pkg.InstallLocation 'app\\ChatGPT.exe';",
793
+ "if (-not (Test-Path $exe)) { throw 'ChatGPT.exe is missing from the OpenAI.Codex package.' }",
794
+ "$sig = Get-AuthenticodeSignature -FilePath $exe;",
795
+ "[pscustomobject]@{",
796
+ " installLocation = [string]$pkg.InstallLocation;",
797
+ " packageFamilyName = [string]$pkg.PackageFamilyName;",
798
+ " version = [string]$pkg.Version;",
799
+ " exePath = $exe;",
800
+ " signatureStatus = [string]$sig.Status;",
801
+ " signerSubject = [string]$sig.SignerCertificate.Subject;",
802
+ "} | ConvertTo-Json -Compress",
803
+ ].join("\n");
804
+ const info = JSON.parse(await this.powershell(script));
805
+ if (info.packageFamilyName !== CODEX_WINDOWS_PACKAGE_FAMILY) {
806
+ throw new Error(`Refusing non-official ChatGPT package: ${info.packageFamilyName || "unknown"}`);
807
+ }
808
+ if (info.signatureStatus !== "Valid" || !/openai/i.test(info.signerSubject ?? "")) {
809
+ throw new Error("Refusing ChatGPT with an unexpected signing identity.");
810
+ }
811
+ this.codexInstallation = Object.freeze({
812
+ appPath: info.installLocation,
813
+ executable: info.exePath,
814
+ packageFamilyName: info.packageFamilyName,
815
+ version: info.version,
816
+ signerSubject: info.signerSubject,
817
+ verified: true,
818
+ });
819
+ this.codexInstallationFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
820
+ this.codexInstallationInjected = false;
821
+ return this.codexInstallation;
822
+ }
823
+
824
+ async codex() {
825
+ if (!this.codexInstallation) return this.assertCodexInstalled();
826
+ if (this.codexInstallationInjected) return this.codexInstallation;
827
+ const currentFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
828
+ if (this.codexInstallationFingerprint == null) {
829
+ this.codexInstallationFingerprint = currentFingerprint;
830
+ } else if (currentFingerprint !== this.codexInstallationFingerprint) {
831
+ return this.assertCodexInstalled();
832
+ }
833
+ return this.codexInstallation;
834
+ }
835
+
836
+ async readCodexInstallationFingerprint(installation) {
837
+ if (typeof this.fileSystem.stat !== "function") return null;
838
+ const paths = [
839
+ installation.executable,
840
+ path.join(installation.appPath, "AppxManifest.xml"),
841
+ ];
842
+ const records = await Promise.all(paths.map(async (filePath) => {
843
+ const info = await this.fileSystem.stat(filePath);
844
+ return [filePath, info.dev ?? null, info.ino ?? null, info.size ?? null, info.mtimeMs ?? null];
845
+ }));
846
+ return JSON.stringify(records);
847
+ }
848
+
849
+ async pathExists(filePath) {
850
+ try {
851
+ if (this.fileSystem.stat) await this.fileSystem.stat(filePath);
852
+ else await this.fileSystem.readFile(filePath);
853
+ return true;
854
+ } catch (error) {
855
+ if (error.code === "ENOENT") return false;
856
+ throw error;
857
+ }
858
+ }
859
+
860
+ async queryScheduledTaskState() {
861
+ const stdout = await this.powershell([
862
+ // Get-ScheduledTask 对不存在的任务抛 terminating error,SilentlyContinue 拦不住,必须 try/catch。
863
+ "try { $task = Get-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "' -ErrorAction Stop } catch { $task = $null }",
864
+ "if ($task) { Write-Output ([string]$task.State) } else { Write-Output 'Missing' }",
865
+ ].join("\n"));
866
+ return String(stdout).trim();
867
+ }
868
+
869
+ async isServiceRunning() {
870
+ return (await this.queryScheduledTaskState()) === "Running";
871
+ }
872
+
873
+ async isServiceInstalled(paths) {
874
+ return (await this.pathExists(paths.serviceAppPath)) || (await this.queryScheduledTaskState()) !== "Missing";
875
+ }
876
+
877
+ async serviceManagerStatus(paths) {
878
+ if (!(await this.pathExists(paths.serviceAppPath)) && (await this.queryScheduledTaskState()) === "Missing") {
879
+ return "not-found";
880
+ }
881
+ const state = await this.queryScheduledTaskState();
882
+ if (state === "Running" || state === "Ready") return "enabled";
883
+ if (state === "Disabled") return "disabled";
884
+ return "unknown";
885
+ }
886
+
887
+ async writeServiceRuntimeConfig(paths, content, mode = 0o600) {
888
+ const temporaryPath = paths.serviceRuntimeConfigPath + ".installing";
889
+ await this.fileSystem.writeFile(temporaryPath, content, { mode });
890
+ await this.fileSystem.rename(temporaryPath, paths.serviceRuntimeConfigPath);
891
+ }
892
+
893
+ async writeServiceCommand(paths, content) {
894
+ await this.fileSystem.writeFile(paths.serviceAppPath, content, { mode: 0o700 });
895
+ }
896
+
897
+ async snapshotServiceState(paths) {
898
+ const state = await this.queryScheduledTaskState();
899
+ const appExists = await this.pathExists(paths.serviceAppPath);
900
+ let runtimeConfig = { exists: false, content: null, mode: null };
901
+ if (appExists && paths.serviceRuntimeConfigPath) {
902
+ try {
903
+ const metadataPromise = this.fileSystem.stat
904
+ ? this.fileSystem.stat(paths.serviceRuntimeConfigPath).catch(() => null)
905
+ : Promise.resolve(null);
906
+ const [content, metadata] = await Promise.all([
907
+ this.fileSystem.readFile(paths.serviceRuntimeConfigPath, "utf8"),
908
+ metadataPromise,
909
+ ]);
910
+ runtimeConfig = { exists: true, content, mode: metadata?.mode };
911
+ } catch (error) {
912
+ if (error.code !== "ENOENT") throw error;
913
+ }
914
+ }
915
+ let taskDefinition = { exists: false, content: null, mode: null };
916
+ if (state !== "Missing") {
917
+ try {
918
+ taskDefinition = { exists: true, content: await this.powershell("Export-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "'"), mode: null };
919
+ } catch {
920
+ taskDefinition = { exists: true, content: null, mode: null };
921
+ }
922
+ }
923
+ return {
924
+ app: { exists: appExists, status: appExists ? await this.serviceManagerStatus(paths) : "not-found", runtimeConfig },
925
+ // 字段名沿用 mac 快照契约(plist),Windows 下内容为计划任务定义。
926
+ plist: taskDefinition,
927
+ running: state === "Running",
928
+ };
929
+ }
930
+
931
+ async installService({ cliPath, paths, start = true }) {
932
+ await this.stopService(paths, { ignoreErrors: false });
933
+ await this.fileSystem.mkdir(paths.serviceDir, { recursive: true, mode: 0o700 });
934
+ await this.fileSystem.mkdir(paths.logsDir, { recursive: true, mode: 0o700 });
935
+ await this.writeServiceRuntimeConfig(paths, buildWindowsServiceRuntimeConfig({ nodePath: process.execPath, cliPath, paths }));
936
+ await this.writeServiceCommand(paths, buildWindowsServiceCommand({ nodePath: process.execPath, cliPath, paths }));
937
+ await this.registerServiceTask(paths);
938
+ if (start) await this.startService(paths);
939
+ }
940
+
941
+ async registerServiceTask(paths) {
942
+ // ExecutionTimeLimit 0 表示不限时:服务是常驻进程,不能被默认 72h 限制回收。
943
+ const script = [
944
+ "$serviceCmd = '" + String(paths.serviceAppPath).replaceAll("'", "''") + "';",
945
+ '$action = New-ScheduledTaskAction -Execute \'cmd.exe\' -Argument (\'/c ""\' + $serviceCmd + \'""\');',
946
+ "$trigger = New-ScheduledTaskTrigger -AtLogOn;",
947
+ "$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1);",
948
+ "Register-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null;",
949
+ "Write-Output 'registered'",
950
+ ].join("\n");
951
+ await this.powershell(script);
952
+ }
953
+
954
+ async startService(paths) {
955
+ const state = await this.queryScheduledTaskState();
956
+ if (state === "Missing") throw new Error("MineCodex background task is not installed. Run mcx install first.");
957
+ if (state === "Disabled") throw new Error("MineCodex background task is disabled. Enable it in Task Scheduler, then run mcx install again.");
958
+ await this.powershell("Start-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "'");
959
+ }
960
+
961
+ async stopService(paths, { ignoreErrors = false } = {}) {
962
+ try {
963
+ await this.powershell([
964
+ "try { $task = Get-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "' -ErrorAction Stop } catch { $task = $null }",
965
+ "if ($task) { Stop-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "' -ErrorAction Stop }",
966
+ ].join("\n"));
967
+ } catch (error) {
968
+ if (!ignoreErrors) throw error;
969
+ }
970
+ }
971
+
972
+ async assertServiceStopped() {
973
+ if (await this.isServiceRunning()) throw new Error("MineCodex scheduled task is still running.");
974
+ }
975
+
976
+ async restoreServiceState(snapshot, { cliPath, paths } = {}) {
977
+ await this.removeService(paths);
978
+ await this.assertServiceStopped();
979
+ if (snapshot?.app?.exists) {
980
+ await this.fileSystem.mkdir(paths.serviceDir, { recursive: true, mode: 0o700 });
981
+ await this.fileSystem.mkdir(paths.logsDir, { recursive: true, mode: 0o700 });
982
+ if (snapshot.app.runtimeConfig?.exists) {
983
+ await this.writeServiceRuntimeConfig(
984
+ paths,
985
+ snapshot.app.runtimeConfig.content,
986
+ snapshot.app.runtimeConfig.mode ? snapshot.app.runtimeConfig.mode & 0o777 : 0o600,
987
+ );
988
+ }
989
+ await this.writeServiceCommand(paths, buildWindowsServiceCommand({ nodePath: process.execPath, cliPath, paths }));
990
+ await this.registerServiceTask(paths);
991
+ if (snapshot.app.status === "enabled" || snapshot.running) await this.startService(paths);
992
+ return snapshot;
993
+ }
994
+ return snapshot;
995
+ }
996
+
997
+ async removeService(paths) {
998
+ await this.stopService(paths, { ignoreErrors: false });
999
+ await this.assertServiceStopped();
1000
+ // Unregister 对不存在的任务抛 terminating error,try/catch 视为已卸载。
1001
+ await this.powershell(
1002
+ "try { Unregister-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "' -Confirm:$false -ErrorAction Stop } catch { }",
1003
+ );
1004
+ if (paths?.serviceRuntimeConfigPath) {
1005
+ await this.fileSystem.unlink(paths.serviceRuntimeConfigPath).catch((error) => {
1006
+ if (error.code !== "ENOENT") throw error;
1007
+ });
1008
+ await this.fileSystem.unlink(paths.serviceRuntimeConfigPath + ".installing").catch((error) => {
1009
+ if (error.code !== "ENOENT") throw error;
1010
+ });
1011
+ }
1012
+ await this.fileSystem.rm(paths.serviceDir, { recursive: true, force: true });
1013
+ }
1014
+
1015
+ async listCodexProcesses() {
1016
+ const installation = await this.codex();
1017
+ const stdout = await this.powershell([
1018
+ "$procs = Get-CimInstance Win32_Process -Filter \"Name='ChatGPT.exe'\" |",
1019
+ " Where-Object { $_.CommandLine } |",
1020
+ " ForEach-Object { [pscustomobject]@{ pid = $_.ProcessId; command = $_.CommandLine } };",
1021
+ "ConvertTo-Json -Compress -InputObject @($procs)",
1022
+ ].join("\n"));
1023
+ let records;
1024
+ try {
1025
+ records = JSON.parse(String(stdout).trim() || "[]");
1026
+ } catch {
1027
+ records = [];
1028
+ }
1029
+ if (!Array.isArray(records)) records = [];
1030
+ return records
1031
+ .filter(({ command }) => isExactCodexProcessWindows(command, installation))
1032
+ .map((processInfo) => ({
1033
+ ...processInfo,
1034
+ appPath: installation.appPath,
1035
+ executable: installation.executable,
1036
+ bundleIdentifier: installation.packageFamilyName ?? CODEX_WINDOWS_PACKAGE_FAMILY,
1037
+ signingIdentity: installation.signerSubject ?? null,
1038
+ verified: installation.verified !== false,
1039
+ }));
1040
+ }
1041
+
1042
+ async ownedCodexProcesses(paths) {
1043
+ const processes = await this.listCodexProcesses();
1044
+ let marker = null;
1045
+ try {
1046
+ marker = parseCodexOwnerMarker(await this.fileSystem.readFile(paths.codexPidPath, "utf8"));
1047
+ } catch (error) {
1048
+ if (error.code !== "ENOENT") throw error;
1049
+ }
1050
+ return processes.filter(({ pid, command, verified }) => verified === true && (
1051
+ hasManagedProfile(command, paths.profileDir)
1052
+ || (
1053
+ pid === marker?.pid
1054
+ && command === marker.command
1055
+ && hasLoopbackCdpArguments(command, extractCdpPort(command))
1056
+ )
1057
+ ));
1058
+ }
1059
+
1060
+ async assertOwnedCodexStopped(paths) {
1061
+ const processes = await this.ownedCodexProcesses(paths);
1062
+ if (processes.length) {
1063
+ throw new Error(`MineCodex-owned Codex processes are still running: ${processes.map(({ pid }) => pid).join(", ")}`);
1064
+ }
1065
+ }
1066
+
1067
+ async snapshotNativeCodexState() {
1068
+ const processes = (await this.listCodexProcesses()).filter(({ command }) => !command.includes("--user-data-dir="));
1069
+ return { count: processes.length, processes };
1070
+ }
1071
+
1072
+ async restoreNativeCodexState(snapshot) {
1073
+ if (!snapshot?.count) return 0;
1074
+ const current = (await this.listCodexProcesses()).filter(({ command }) => !command.includes("--user-data-dir="));
1075
+ if (current.length) return current.length;
1076
+ await this.openNativeCodex();
1077
+ return snapshot.count;
1078
+ }
1079
+
1080
+ async terminateOwnedCodex(paths) {
1081
+ const processInfo = (await this.ownedCodexProcesses(paths))[0];
1082
+ if (!processInfo) {
1083
+ await this.assertOwnedCodexStopped(paths);
1084
+ return false;
1085
+ }
1086
+ this.processApi.kill(processInfo.pid);
1087
+ await waitForProcessExit(processInfo.pid, { processApi: this.processApi, sleep: this.sleep });
1088
+ await this.fileSystem.unlink(paths.codexPidPath).catch(() => {});
1089
+ await this.assertOwnedCodexStopped(paths);
1090
+ return true;
1091
+ }
1092
+
1093
+ async terminateNativeCodex(pid, expectedCommand = null) {
1094
+ if (!Number.isInteger(pid) || pid <= 1) {
1095
+ throw new Error("MineCodex requires an exact ChatGPT PID to terminate.");
1096
+ }
1097
+ const processInfo = (await this.listCodexProcesses()).find((candidate) => (
1098
+ candidate.pid === pid
1099
+ && candidate.verified === true
1100
+ && !candidate.command.includes("--user-data-dir=")
1101
+ && (!expectedCommand || candidate.command === expectedCommand)
1102
+ ));
1103
+ if (!processInfo) return 0;
1104
+ const ownedProcessTree = await this.collectProcessTree(pid);
1105
+ for (const treePid of ownedProcessTree) {
1106
+ try {
1107
+ this.processApi.kill(treePid);
1108
+ } catch {
1109
+ // 进程可能已自行退出,终止阶段容忍缺失。
1110
+ }
1111
+ }
1112
+ await waitForProcessesExit(ownedProcessTree, { processApi: this.processApi, sleep: this.sleep });
1113
+ return 1;
1114
+ }
1115
+
1116
+ async collectProcessTree(rootPid) {
1117
+ const stdout = await this.powershell([
1118
+ "$procs = Get-CimInstance Win32_Process -Filter \"Name='ChatGPT.exe'\" |",
1119
+ " ForEach-Object { [pscustomobject]@{ pid = $_.ProcessId; ppid = $_.ParentProcessId } };",
1120
+ "ConvertTo-Json -Compress -InputObject @($procs)",
1121
+ ].join("\n"));
1122
+ let records;
1123
+ try {
1124
+ records = JSON.parse(String(stdout).trim() || "[]");
1125
+ } catch {
1126
+ records = [];
1127
+ }
1128
+ if (!Array.isArray(records)) records = [];
1129
+ const childrenByParent = new Map();
1130
+ for (const { pid, ppid } of records) {
1131
+ const children = childrenByParent.get(ppid) ?? [];
1132
+ children.push(pid);
1133
+ childrenByParent.set(ppid, children);
1134
+ }
1135
+ const descendants = [];
1136
+ const pending = [...(childrenByParent.get(rootPid) ?? [])];
1137
+ while (pending.length) {
1138
+ const current = pending.shift();
1139
+ descendants.push(current);
1140
+ pending.push(...(childrenByParent.get(current) ?? []));
1141
+ }
1142
+ return [rootPid, ...descendants];
1143
+ }
1144
+
1145
+ async assertNativeCodexCdp(processInfo, cdpPort) {
1146
+ if (!processInfo?.verified || !Number.isInteger(processInfo.pid) || !hasLoopbackCdpArguments(processInfo.command, cdpPort)) {
1147
+ throw new Error("MineCodex could not bind the loopback CDP endpoint to a verified ChatGPT process.");
1148
+ }
1149
+ const stdout = await this.powershell([
1150
+ "$owned = Get-NetTCPConnection -State Listen -LocalPort " + cdpPort + " -ErrorAction SilentlyContinue |",
1151
+ " Where-Object { $_.OwningProcess -eq " + processInfo.pid + " -and ($_.LocalAddress -eq '127.0.0.1' -or $_.LocalAddress -eq '::1') };",
1152
+ "if ($owned) { Write-Output 'OWNED' } else { throw ('ChatGPT pid ' + " + processInfo.pid + " + ' does not own loopback CDP port " + cdpPort + ".') }",
1153
+ ].join("\n"));
1154
+ if (!String(stdout).includes("OWNED")) {
1155
+ throw new Error(`ChatGPT pid ${processInfo.pid} does not own loopback CDP port ${cdpPort}.`);
1156
+ }
1157
+ return true;
1158
+ }
1159
+
1160
+ async openBrowser(url) {
1161
+ const safeUrl = String(url).replaceAll("'", "''");
1162
+ await this.powershell("Start-Process -FilePath '" + safeUrl + "'");
1163
+ }
1164
+
1165
+ async openNativeCodex({ cdpPort } = {}) {
1166
+ const installation = await this.codex();
1167
+ if (!Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65_535) {
1168
+ await this.powershell("Start-Process -FilePath '" + String(installation.executable).replaceAll("'", "''") + "'");
1169
+ return null;
1170
+ }
1171
+ const existingPids = new Set((await this.listCodexProcesses()).map(({ pid }) => pid));
1172
+ const safeExecutable = String(installation.executable).replaceAll("'", "''");
1173
+ await this.powershell([
1174
+ "Start-Process -FilePath '" + safeExecutable + "' -ArgumentList @(",
1175
+ " '--remote-debugging-address=127.0.0.1',",
1176
+ " '--remote-debugging-port=" + cdpPort + "',",
1177
+ " '--remote-allow-origins=http://127.0.0.1:" + cdpPort + "');",
1178
+ ].join("\n"));
1179
+ return this.waitForNativeCodex({ cdpPort, excludedPids: existingPids });
1180
+ }
1181
+
1182
+ async waitForNativeCodex({ cdpPort, excludedPids = new Set(), timeoutMs = 15_000 } = {}) {
1183
+ const deadline = Date.now() + timeoutMs;
1184
+ while (Date.now() < deadline) {
1185
+ const candidates = (await this.listCodexProcesses()).filter(({ pid, command, verified }) => (
1186
+ verified === true
1187
+ && !excludedPids.has(pid)
1188
+ && !command.includes("--user-data-dir=")
1189
+ && hasLoopbackCdpArguments(command, cdpPort)
1190
+ ));
1191
+ if (candidates.length > 1) {
1192
+ throw new Error(`Windows exposed multiple new ChatGPT processes on CDP port ${cdpPort}.`);
1193
+ }
1194
+ const processInfo = candidates[0];
1195
+ if (processInfo) {
1196
+ await this.assertNativeCodexCdp(processInfo, cdpPort);
1197
+ return processInfo;
1198
+ }
1199
+ await this.sleep?.(50);
1200
+ }
1201
+ throw new Error(`Windows did not expose a loopback CDP ChatGPT process on port ${cdpPort}.`);
1202
+ }
1203
+ }
1204
+
1205
+ export function createPlatformAdapter(options = {}) {
1206
+ const platform = options.platform ?? process.platform;
1207
+ if (platform === "win32") return new WindowsPlatformAdapter(options);
1208
+ return new MacPlatformAdapter(options);
1209
+ }
@@ -190,7 +190,7 @@ export class RuntimeManager {
190
190
  const child = this.spawnProcess(process.execPath, [this.paths.runtimeEntry], {
191
191
  cwd: this.paths.packageRoot,
192
192
  env: runtimeEnvironment,
193
- stdio: "inherit",
193
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
194
194
  });
195
195
  this.child = child;
196
196
  if (hasExited(child)) {
@@ -479,7 +479,14 @@ export class RuntimeManager {
479
479
  child.once("exit", onExit);
480
480
  try {
481
481
  try {
482
- child.kill(signal);
482
+ // Windows 的 SIGTERM 会强杀;先请求宿主清理插件子进程。
483
+ if (signal === "SIGTERM" && child.connected && typeof child.send === "function") {
484
+ child.send({ type: "minecodex:shutdown" }, (error) => {
485
+ if (error && !hasExited(child)) this.logger.warn?.("Runtime shutdown request failed", error.message);
486
+ });
487
+ } else {
488
+ child.kill(signal);
489
+ }
483
490
  } catch (error) {
484
491
  if (error?.code === "ESRCH") return true;
485
492
  throw error;
@@ -2722,7 +2722,7 @@ export function createInjectionSource(features, {
2722
2722
  record.frame.contentWindow?.postMessage({
2723
2723
  type: "codex-personal:surface-active",
2724
2724
  active: Boolean(active),
2725
- }, record.origin);
2725
+ }, "/");
2726
2726
  }
2727
2727
 
2728
2728
  function nativeSidebarTrigger() {
@@ -2749,7 +2749,7 @@ export function createInjectionSource(features, {
2749
2749
  record.frame.contentWindow?.postMessage({
2750
2750
  type: "codex-personal:sidebar-state",
2751
2751
  ...state,
2752
- }, record.origin);
2752
+ }, "/");
2753
2753
  }
2754
2754
 
2755
2755
  function registerSurface(key, feature, kind, surfaceUrl, element, frame, detailId = null) {
@@ -2863,7 +2863,7 @@ export function createInjectionSource(features, {
2863
2863
  : tokens["--color-token-main-surface-primary"] ?? style.backgroundColor;
2864
2864
  record.frame.contentWindow?.postMessage(
2865
2865
  { type: "codex-personal:theme", theme: mode, mode, locale, tokens: surfaceTokens },
2866
- record.origin,
2866
+ "/",
2867
2867
  );
2868
2868
  }
2869
2869
  }
@@ -2879,11 +2879,26 @@ export function createInjectionSource(features, {
2879
2879
  return sidebar?.getBoundingClientRect().right ?? 0;
2880
2880
  }
2881
2881
 
2882
+ // Windows 版 Codex 在 renderer 顶部渲染横贯全宽的应用菜单条
2883
+ // (_ApplicationMenuTopBar),page surface 必须避开该条并以内嵌卡片
2884
+ // 形式呈现(左上圆角)。macOS 无此元素,inset 为 0,布局保持不变。
2885
+ function applicationMenuTopInset() {
2886
+ const menuBar = document.querySelector('[class*="_ApplicationMenuTopBar_"]');
2887
+ if (!menuBar?.isConnected) return 0;
2888
+ const bottom = menuBar.getBoundingClientRect().bottom;
2889
+ return Number.isFinite(bottom) && bottom > 0 ? Math.round(bottom) : 0;
2890
+ }
2891
+
2882
2892
  function updatePageSurfacePositions() {
2883
2893
  const state = sidebarState();
2884
2894
  const left = `${state.open ? Math.max(0, sidebarRight()) : 0}px`;
2895
+ const topInset = applicationMenuTopInset();
2896
+ const embeddedCard = topInset > 0;
2885
2897
  for (const [featureId, surface] of pageSurfaces) {
2886
2898
  surface.style.left = left;
2899
+ surface.style.top = `${topInset}px`;
2900
+ surface.style.borderTopLeftRadius = embeddedCard ? "12px" : "";
2901
+ surface.style.overflow = embeddedCard ? "hidden" : "";
2887
2902
  postSidebarState(surfaceRecords.get(surfaceKey(featureId, "page")), state);
2888
2903
  }
2889
2904
  }
@@ -4173,7 +4188,10 @@ export function createInjectionSource(features, {
4173
4188
 
4174
4189
  function findMessageSurface(event) {
4175
4190
  for (const record of surfaceRecords.values()) {
4176
- if (event.origin === record.origin && event.source === record.frame.contentWindow) return record;
4191
+ // 身份以 event.source(WindowProxy 不可伪造)为准;Windows
4192
+ // setDocumentContent 的 iframe 消息可能报告 null origin,
4193
+ // 因此用已注册 frame 的 WindowProxy 精确匹配来源。
4194
+ if (event.source === record.frame.contentWindow) return record;
4177
4195
  }
4178
4196
  return null;
4179
4197
  }
@@ -4186,7 +4204,7 @@ export function createInjectionSource(features, {
4186
4204
  ok,
4187
4205
  result,
4188
4206
  error,
4189
- }, record.origin);
4207
+ }, "/");
4190
4208
  }
4191
4209
 
4192
4210
  function hostActionFailure(error) {
@@ -4243,8 +4261,9 @@ export function createInjectionSource(features, {
4243
4261
  if (!files.length || files.some((file) => !(file instanceof File))) {
4244
4262
  throw new Error("Real dropped File objects are required");
4245
4263
  }
4264
+ // Windows 盘符路径(C:\...)不以 / 开头,需同时接受 POSIX 与 Windows 形态。
4246
4265
  const paths = files.map((file) => getPathForFile(file)).filter((value) => (
4247
- typeof value === "string" && value.startsWith("/")
4266
+ typeof value === "string" && (/^[/\\]/.test(value) || /^[a-zA-Z]:[/\\]/.test(value))
4248
4267
  ));
4249
4268
  if (!paths.length) throw new Error("Codex could not resolve the dropped file paths");
4250
4269
  respondToSurface(record, requestId, { ok: true, result: { paths } });
@@ -4366,7 +4385,7 @@ export function createInjectionSource(features, {
4366
4385
  record.frame.contentWindow?.postMessage({
4367
4386
  type: "codex-personal:focus",
4368
4387
  feature: record.featureId,
4369
- }, record.origin);
4388
+ }, "/");
4370
4389
  }
4371
4390
  }
4372
4391
  if (event.data.type === "codex-personal:surface-metrics" && record.kind === "pinned") {
@@ -93,14 +93,22 @@ async function shutdown() {
93
93
  await featureProcesses.stop();
94
94
  }
95
95
 
96
+ let shuttingDown = false;
97
+ async function requestShutdown() {
98
+ if (shuttingDown) return;
99
+ shuttingDown = true;
100
+ try {
101
+ await shutdown();
102
+ process.exit(0);
103
+ } catch (error) {
104
+ console.error(`Runtime shutdown failed: ${error.message}`);
105
+ process.exit(1);
106
+ }
107
+ }
96
108
  for (const signal of ["SIGINT", "SIGTERM"]) {
97
- process.once(signal, async () => {
98
- try {
99
- await shutdown();
100
- process.exit(0);
101
- } catch (error) {
102
- console.error(`Runtime shutdown failed: ${error.message}`);
103
- process.exit(1);
104
- }
105
- });
109
+ process.once(signal, requestShutdown);
106
110
  }
111
+ process.on("message", (message) => {
112
+ if (message?.type === "minecodex:shutdown") void requestShutdown();
113
+ });
114
+ if (process.connected) process.once("disconnect", requestShutdown);