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.
- package/README.md +2 -2
- package/features/images/src/http-server.mjs +2 -1
- package/features/images/src/save-as.mjs +37 -8
- package/features/images/web/app.js +91 -80
- package/features/images/web/i18n.mjs +127 -0
- package/features/images/web/index.html +33 -33
- package/features/notes/src/http-server.mjs +28 -0
- package/features/notes/web/app.js +4 -3
- package/package.json +4 -2
- package/packages/cli/src/commands.mjs +3 -3
- package/packages/cli/src/npm-adapter.mjs +12 -4
- package/packages/cli/src/paths.mjs +36 -1
- package/packages/cli/src/platform.mjs +517 -0
- package/packages/cli/src/runtime-manager.mjs +9 -2
- package/packages/runtime-host/src/codex-injection.mjs +26 -7
- package/packages/runtime-host/src/main.mjs +17 -9
|
@@ -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
|
-
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
-
},
|
|
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,
|
|
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);
|