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.
- 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/model-slider/README.md +7 -4
- package/features/notes/README.md +1 -1
- package/features/notes/src/http-server.mjs +31 -0
- package/features/notes/web/app.js +13 -306
- package/features/notes/web/file-icons.mjs +68 -0
- package/features/notes/web/i18n.mjs +237 -0
- package/features/notes/web/todo-drag.mjs +18 -0
- 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/README.md +5 -3
- package/packages/runtime-host/src/codex-cdp.mjs +153 -0
- package/packages/runtime-host/src/codex-design-contract.mjs +116 -0
- package/packages/runtime-host/src/codex-injection.mjs +4586 -0
- package/packages/runtime-host/src/codex-runtime.mjs +63 -4930
- package/packages/runtime-host/src/host-actions.mjs +221 -0
- 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;
|
|
@@ -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
|
-
|
|
136
|
-
|
|
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
|
|
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` 字符串 | 探索证据 | 当前实现不把它们当契约 |
|