minecodex 1.0.82 → 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;
@@ -132,8 +132,8 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
132
132
  - `attach-file`:用 CDP 填充临时 file input,再把真实 `File` drop 给 Composer;只有
133
133
  观察到原生附件 chip/preview 才报告 `attach`,否则明确返回 `insert-path`。
134
134
  - `open-detail-tab`:动态发现当前 Codex React right-panel controller 与当前 Task scope,
135
- 用稳定的 `featureId-detailId` 聚焦或创建唯一原生 Tab;capability 不可用时才回退到
136
- RuntimeHost 自建的兼容 Tab host。
135
+ 传入包含 `Component`、`isAvailable`、`kind` `dropDestinations` 的标签描述对象,
136
+ 用稳定的 `featureId-detailId` 聚焦或创建唯一原生 Tab;capability 不可用时返回明确错误。
137
137
  - `resolve-file-paths`:通过 Codex preload 暴露的 Electron file bridge 解析 Finder
138
138
  drop 的真实 `File` 对象。
139
139
 
@@ -145,6 +145,8 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
145
145
  本地 Surface 不直接导航到 loopback URL:Host 先创建 `about:blank` iframe,再校验
146
146
  manifest URL、health service/protocol/instance,抓取 HTML 并用 `Page.setDocumentContent`
147
147
  写入目标 frame。Images/Notes 只向精确的 `app://-` Origin 开放 CORS。
148
+ - CDP 重连时同步轮换后的 binding token;保留已就绪的 Surface 和编辑草稿,
149
+ 使用新 token 重试未就绪页面,避免弹窗停留在空白 iframe。
148
150
  - 入口由 Renderer 内的 MutationObserver 幂等挂载,React 重绘不会产生重复入口。
149
151
  - Summary 根据对话主区域宽度连续派生 `overlay / shift / gutter`:小于 1096px
150
152
  使用临时 Popover;1096–1535px 预留 316px 并把对话内容左移 158px;更宽时
@@ -166,7 +168,7 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
166
168
  | --- | --- | --- |
167
169
  | loopback HTTP、manifest、Surface message schema | 项目自有稳定接口 | 可由测试直接覆盖 |
168
170
  | `window.electronBridge.getPathForFile` | 当前稳定 internal/preload | 仅用于 Finder `File` 路径解析 |
169
- | React right-panel `openTab`、Task scope、JSX runtime | private internal capability | 动态发现 bundle 并做能力检测;失败时回退自建 Tab host |
171
+ | React right-panel `openTab`、Task scope、JSX runtime | private internal capability | 动态发现 bundle 并做能力检测;失败时返回明确的不可用错误 |
170
172
  | toolbar、summary、tabs、Composer selectors | DOM inference | 统一留在本 Adapter |
171
173
  | `Runtime.addBinding`、`DOM.setFileInputFiles`、注入脚本 | high-risk CDP | 必须使用隔离 profile 验收 |
172
174
  | `togglePinnedSummary`、`composer.addFiles` 字符串 | 探索证据 | 当前实现不把它们当契约 |