minecodex 0.1.15 → 0.1.18
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 -0
- package/package.json +1 -1
- package/packages/cli/src/chatgpt-launch-coordinator.mjs +77 -0
- package/packages/cli/src/control-server.mjs +2 -0
- package/packages/cli/src/platform.mjs +232 -29
- package/packages/cli/src/runtime-manager.mjs +202 -101
- package/packages/runtime-host/src/codex-runtime.mjs +116 -117
- package/packages/runtime-host/src/main.mjs +1 -3
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ mcx install
|
|
|
26
26
|
|
|
27
27
|
运行 `mcx install` 不会强制关闭或启动 Codex,也不会影响当前打开的窗口。安装完成后,终端会说明更改将在下一次重启 Codex 时生效,并询问是否立即重启;默认选项为 **No**。只有在您明确确认,或后续手动运行 `mcx restart` 时,才会重新打开 Codex。安装完成后,macOS 的“登录项与扩展”中会添加名为 **MineCodex** 的后台项目。
|
|
28
28
|
|
|
29
|
+
MineCodex 只以后台辅助服务运行,前台应用始终是 OpenAI 签名的官方 `ChatGPT.app`,并继续使用官方默认用户数据。通过 Dock、Finder、Spotlight、登录项或会话恢复打开 ChatGPT 时,后台服务会验证官方应用与对应进程;若该进程尚未开放兼容的本机调试端口,则确认其完整退出后再通过 LaunchServices 重新打开官方应用并注入功能。MineCodex 不修改 `ChatGPT.app`、`app.asar`、Dock 项目或默认打开方式。
|
|
30
|
+
|
|
29
31
|
打开本地控制台:
|
|
30
32
|
|
|
31
33
|
```bash
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function hasExactArgument(command, argument) {
|
|
2
|
+
const value = String(command);
|
|
3
|
+
let offset = value.indexOf(argument);
|
|
4
|
+
while (offset >= 0) {
|
|
5
|
+
const before = offset === 0 || /\s/.test(value[offset - 1]);
|
|
6
|
+
const end = offset + argument.length;
|
|
7
|
+
const after = end === value.length || /\s/.test(value[end]);
|
|
8
|
+
if (before && after) return true;
|
|
9
|
+
offset = value.indexOf(argument, offset + 1);
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasLoopbackCdpArguments(command, cdpPort) {
|
|
15
|
+
return hasExactArgument(command, "--remote-debugging-address=127.0.0.1")
|
|
16
|
+
&& hasExactArgument(command, `--remote-debugging-port=${cdpPort}`)
|
|
17
|
+
&& hasExactArgument(command, `--remote-allow-origins=http://127.0.0.1:${cdpPort}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function requireProcess(processInfo, context) {
|
|
21
|
+
if (!processInfo || !Number.isInteger(processInfo.pid) || processInfo.pid <= 1) {
|
|
22
|
+
throw new Error(`MineCodex could not verify the ${context} ChatGPT process.`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof processInfo.command !== "string" || !processInfo.command.trim()) {
|
|
25
|
+
throw new Error(`MineCodex could not verify the ${context} ChatGPT command line.`);
|
|
26
|
+
}
|
|
27
|
+
if (processInfo.verified === false) {
|
|
28
|
+
throw new Error(`MineCodex rejected an unverified ${context} ChatGPT process.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ChatGPTLaunchCoordinator {
|
|
33
|
+
constructor({ platform, cdpPort }) {
|
|
34
|
+
this.platform = platform;
|
|
35
|
+
this.cdpPort = cdpPort;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async attach(processInfo, owned, { startRuntime, waitForHealthy }) {
|
|
39
|
+
requireProcess(processInfo, owned ? "LaunchServices-created" : "observed");
|
|
40
|
+
await this.platform.assertNativeCodexCdp(processInfo, this.cdpPort);
|
|
41
|
+
const status = await startRuntime();
|
|
42
|
+
const healthy = await waitForHealthy(status);
|
|
43
|
+
await this.platform.assertNativeCodexCdp(processInfo, this.cdpPort);
|
|
44
|
+
return {
|
|
45
|
+
...healthy,
|
|
46
|
+
codex: { pid: processInfo.pid, owned, command: processInfo.command ?? null },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async ensureObservedLaunch(processInfo, { stopRuntime, startRuntime, waitForHealthy }) {
|
|
51
|
+
requireProcess(processInfo, "observed");
|
|
52
|
+
if (typeof this.platform?.terminateNativeCodex !== "function"
|
|
53
|
+
|| typeof this.platform?.openNativeCodex !== "function"
|
|
54
|
+
|| typeof this.platform?.assertNativeCodexCdp !== "function") {
|
|
55
|
+
throw new Error("MineCodex platform adapter cannot coordinate an official ChatGPT launch.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await stopRuntime();
|
|
59
|
+
if (hasLoopbackCdpArguments(processInfo.command, this.cdpPort)) {
|
|
60
|
+
return this.attach(processInfo, false, { startRuntime, waitForHealthy });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const terminated = await this.platform.terminateNativeCodex(processInfo.pid, processInfo.command);
|
|
64
|
+
if (terminated !== 1) {
|
|
65
|
+
throw new Error(`MineCodex refused to replace ChatGPT pid ${processInfo.pid} after identity changed.`);
|
|
66
|
+
}
|
|
67
|
+
const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
|
|
68
|
+
return this.attach(launched, true, { startRuntime, waitForHealthy });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async relaunchOwned({ stopRuntime, terminateOwned, startRuntime, waitForHealthy }) {
|
|
72
|
+
await stopRuntime();
|
|
73
|
+
await terminateOwned();
|
|
74
|
+
const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
|
|
75
|
+
return this.attach(launched, true, { startRuntime, waitForHealthy });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -290,11 +290,13 @@ export async function createControlServer({
|
|
|
290
290
|
service: { running: true },
|
|
291
291
|
runtime: {
|
|
292
292
|
running: runtime.running,
|
|
293
|
+
healthy: runtime.healthy === true,
|
|
293
294
|
pid: runtime.pid ?? null,
|
|
294
295
|
runtimeSessionId: runtime.runtimeSessionId ?? null,
|
|
295
296
|
ready: runtime.ready ?? null,
|
|
296
297
|
plugins: runtime.plugins ?? runtime.pluginStates ?? [],
|
|
297
298
|
renderer: runtime.renderer ?? null,
|
|
299
|
+
codex: runtime.codex ?? null,
|
|
298
300
|
failures: runtime.failures ?? [],
|
|
299
301
|
},
|
|
300
302
|
runtimeSessionId: runtime.runtimeSessionId ?? null,
|
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/pr
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { hasLoopbackCdpArguments } from "./chatgpt-launch-coordinator.mjs";
|
|
6
7
|
|
|
7
8
|
const defaultExecFile = promisify(execFileCallback);
|
|
8
9
|
const defaultFileSystem = Object.freeze({ mkdir, readFile, rename, rm, stat, unlink, writeFile });
|
|
@@ -11,6 +12,8 @@ export const SERVICE_LABEL = "com.vontean.minecodex.background";
|
|
|
11
12
|
export const LEGACY_SERVICE_LABEL = "com.vontean.minecodex";
|
|
12
13
|
export const CODEX_APP_PATH = "/Applications/ChatGPT.app";
|
|
13
14
|
export const CODEX_EXECUTABLE = `${CODEX_APP_PATH}/Contents/MacOS/ChatGPT`;
|
|
15
|
+
export const CODEX_BUNDLE_IDENTIFIER = "com.openai.codex";
|
|
16
|
+
export const CODEX_TEAM_IDENTIFIER = "2DC432GLL2";
|
|
14
17
|
export const MIN_NODE_VERSION = Object.freeze({ major: 22, minor: 5, patch: 0 });
|
|
15
18
|
export const MIN_MACOS_VERSION = Object.freeze({ major: 13, darwinMajor: 22 });
|
|
16
19
|
|
|
@@ -90,6 +93,40 @@ export async function resolveCodexInstallation({
|
|
|
90
93
|
);
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
export async function verifyCodexInstallation({ installation, execFile = defaultExecFile } = {}) {
|
|
97
|
+
if (!installation?.appPath || !installation?.executable) {
|
|
98
|
+
throw new Error("Codex Desktop installation is incomplete.");
|
|
99
|
+
}
|
|
100
|
+
const infoPath = path.join(installation.appPath, "Contents", "Info.plist");
|
|
101
|
+
const { stdout: bundleIdentifier } = await execFile("/usr/bin/plutil", [
|
|
102
|
+
"-extract", "CFBundleIdentifier", "raw", "-o", "-", infoPath,
|
|
103
|
+
]);
|
|
104
|
+
if (String(bundleIdentifier).trim() !== CODEX_BUNDLE_IDENTIFIER) {
|
|
105
|
+
throw new Error(`Refusing non-official ChatGPT bundle: ${String(bundleIdentifier).trim() || "unknown"}`);
|
|
106
|
+
}
|
|
107
|
+
await execFile("/usr/bin/codesign", ["--verify", "--deep", "--strict", installation.appPath]);
|
|
108
|
+
const { stderr: details } = await execFile("/usr/bin/codesign", [
|
|
109
|
+
"-dv", "--verbose=4", installation.appPath,
|
|
110
|
+
]);
|
|
111
|
+
const authority = String(details).match(/^Authority=(.+)$/m)?.[1] ?? "";
|
|
112
|
+
const identifier = String(details).match(/^Identifier=(.+)$/m)?.[1] ?? "";
|
|
113
|
+
const teamIdentifier = String(details).match(/^TeamIdentifier=(.+)$/m)?.[1] ?? "";
|
|
114
|
+
if (
|
|
115
|
+
identifier !== CODEX_BUNDLE_IDENTIFIER
|
|
116
|
+
|| teamIdentifier !== CODEX_TEAM_IDENTIFIER
|
|
117
|
+
|| !/\bOpenAI\b/i.test(authority)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error("Refusing ChatGPT with an unexpected signing identity.");
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
...installation,
|
|
123
|
+
bundleIdentifier: CODEX_BUNDLE_IDENTIFIER,
|
|
124
|
+
signingIdentity: authority,
|
|
125
|
+
teamIdentifier,
|
|
126
|
+
verified: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
93
130
|
export function parseProcessList(output) {
|
|
94
131
|
return String(output).split("\n").flatMap((line) => {
|
|
95
132
|
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
@@ -116,6 +153,22 @@ function hasManagedProfile(command, profileDir) {
|
|
|
116
153
|
return false;
|
|
117
154
|
}
|
|
118
155
|
|
|
156
|
+
function extractCdpPort(command) {
|
|
157
|
+
const match = String(command).match(/(?:^|\s)--remote-debugging-port=(\d+)(?:\s|$)/);
|
|
158
|
+
return match ? Number(match[1]) : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseCodexOwnerMarker(content) {
|
|
162
|
+
try {
|
|
163
|
+
const marker = JSON.parse(String(content));
|
|
164
|
+
if (marker?.version !== 1 || !Number.isInteger(marker.pid) || marker.pid <= 1) return null;
|
|
165
|
+
if (typeof marker.command !== "string" || !marker.command.trim()) return null;
|
|
166
|
+
return marker;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
119
172
|
export function serviceIsAbsent(error) {
|
|
120
173
|
return /could not find service|service not found|no such process|domain does not support specified action/i.test(
|
|
121
174
|
`${error?.stderr ?? ""}\n${error?.message ?? ""}`,
|
|
@@ -136,6 +189,48 @@ async function waitForProcessExit(pid, { processApi, timeoutMs = 8_000, sleep =
|
|
|
136
189
|
throw new Error(`Process ${pid} did not exit in time`);
|
|
137
190
|
}
|
|
138
191
|
|
|
192
|
+
async function waitForProcessesExit(
|
|
193
|
+
pids,
|
|
194
|
+
{ processApi, timeoutMs = 8_000, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) },
|
|
195
|
+
) {
|
|
196
|
+
const pending = new Set(pids);
|
|
197
|
+
const deadline = Date.now() + timeoutMs;
|
|
198
|
+
while (Date.now() < deadline) {
|
|
199
|
+
for (const pid of pending) {
|
|
200
|
+
try {
|
|
201
|
+
processApi.kill(pid, 0);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error.code === "ESRCH") pending.delete(pid);
|
|
204
|
+
else throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (pending.size === 0) return;
|
|
208
|
+
await sleep(100);
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`ChatGPT process tree did not exit in time: ${[...pending].join(", ")}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function descendantPids(output, rootPid) {
|
|
214
|
+
const childrenByParent = new Map();
|
|
215
|
+
for (const line of String(output).split("\n")) {
|
|
216
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+/);
|
|
217
|
+
if (!match) continue;
|
|
218
|
+
const pid = Number(match[1]);
|
|
219
|
+
const parentPid = Number(match[2]);
|
|
220
|
+
const children = childrenByParent.get(parentPid) ?? [];
|
|
221
|
+
children.push(pid);
|
|
222
|
+
childrenByParent.set(parentPid, children);
|
|
223
|
+
}
|
|
224
|
+
const descendants = [];
|
|
225
|
+
const pending = [...(childrenByParent.get(rootPid) ?? [])];
|
|
226
|
+
while (pending.length) {
|
|
227
|
+
const pid = pending.shift();
|
|
228
|
+
descendants.push(pid);
|
|
229
|
+
pending.push(...(childrenByParent.get(pid) ?? []));
|
|
230
|
+
}
|
|
231
|
+
return descendants;
|
|
232
|
+
}
|
|
233
|
+
|
|
139
234
|
function serviceTarget(uid) {
|
|
140
235
|
return `gui/${uid}/${SERVICE_LABEL}`;
|
|
141
236
|
}
|
|
@@ -169,7 +264,7 @@ export class MacPlatformAdapter {
|
|
|
169
264
|
execFile = defaultExecFile,
|
|
170
265
|
fileSystem = defaultFileSystem,
|
|
171
266
|
processApi = process,
|
|
172
|
-
sleep,
|
|
267
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
173
268
|
codexInstallation,
|
|
174
269
|
} = {}) {
|
|
175
270
|
this.uid = uid;
|
|
@@ -181,6 +276,8 @@ export class MacPlatformAdapter {
|
|
|
181
276
|
this.processApi = processApi;
|
|
182
277
|
this.sleep = sleep;
|
|
183
278
|
this.codexInstallation = codexInstallation;
|
|
279
|
+
this.codexInstallationFingerprint = null;
|
|
280
|
+
this.codexInstallationInjected = Boolean(codexInstallation);
|
|
184
281
|
}
|
|
185
282
|
|
|
186
283
|
assertInstallSupported() {
|
|
@@ -194,7 +291,7 @@ export class MacPlatformAdapter {
|
|
|
194
291
|
}
|
|
195
292
|
|
|
196
293
|
async assertCodexInstalled() {
|
|
197
|
-
|
|
294
|
+
const installation = await resolveCodexInstallation({
|
|
198
295
|
homeDir: this.homeDir,
|
|
199
296
|
fileExists: async (filePath) => {
|
|
200
297
|
try {
|
|
@@ -206,11 +303,36 @@ export class MacPlatformAdapter {
|
|
|
206
303
|
}
|
|
207
304
|
},
|
|
208
305
|
});
|
|
306
|
+
this.codexInstallation = await verifyCodexInstallation({ installation, execFile: this.execFile });
|
|
307
|
+
this.codexInstallationFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
|
|
308
|
+
this.codexInstallationInjected = false;
|
|
209
309
|
return this.codexInstallation;
|
|
210
310
|
}
|
|
211
311
|
|
|
212
312
|
async codex() {
|
|
213
|
-
|
|
313
|
+
if (!this.codexInstallation) return this.assertCodexInstalled();
|
|
314
|
+
if (this.codexInstallationInjected) return this.codexInstallation;
|
|
315
|
+
const currentFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
|
|
316
|
+
if (this.codexInstallationFingerprint == null) {
|
|
317
|
+
this.codexInstallationFingerprint = currentFingerprint;
|
|
318
|
+
} else if (currentFingerprint !== this.codexInstallationFingerprint) {
|
|
319
|
+
return this.assertCodexInstalled();
|
|
320
|
+
}
|
|
321
|
+
return this.codexInstallation;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async readCodexInstallationFingerprint(installation) {
|
|
325
|
+
if (typeof this.fileSystem.stat !== "function") return null;
|
|
326
|
+
const paths = [
|
|
327
|
+
installation.executable,
|
|
328
|
+
path.join(installation.appPath, "Contents", "Info.plist"),
|
|
329
|
+
path.join(installation.appPath, "Contents", "_CodeSignature", "CodeResources"),
|
|
330
|
+
];
|
|
331
|
+
const records = await Promise.all(paths.map(async (filePath) => {
|
|
332
|
+
const info = await this.fileSystem.stat(filePath);
|
|
333
|
+
return [filePath, info.dev ?? null, info.ino ?? null, info.size ?? null, info.mtimeMs ?? null];
|
|
334
|
+
}));
|
|
335
|
+
return JSON.stringify(records);
|
|
214
336
|
}
|
|
215
337
|
|
|
216
338
|
async isLaunchdServiceRunning(target) {
|
|
@@ -428,12 +550,34 @@ export class MacPlatformAdapter {
|
|
|
428
550
|
async listCodexProcesses() {
|
|
429
551
|
const installation = await this.codex();
|
|
430
552
|
const { stdout } = await this.execFile("/bin/ps", ["-axo", "pid=,command="]);
|
|
431
|
-
return parseProcessList(stdout)
|
|
553
|
+
return parseProcessList(stdout)
|
|
554
|
+
.filter(({ command }) => isExactCodexProcess(command, installation))
|
|
555
|
+
.map((processInfo) => ({
|
|
556
|
+
...processInfo,
|
|
557
|
+
appPath: installation.appPath,
|
|
558
|
+
executable: installation.executable,
|
|
559
|
+
bundleIdentifier: installation.bundleIdentifier ?? CODEX_BUNDLE_IDENTIFIER,
|
|
560
|
+
signingIdentity: installation.signingIdentity ?? null,
|
|
561
|
+
verified: installation.verified !== false,
|
|
562
|
+
}));
|
|
432
563
|
}
|
|
433
564
|
|
|
434
565
|
async ownedCodexProcesses(paths) {
|
|
435
566
|
const processes = await this.listCodexProcesses();
|
|
436
|
-
|
|
567
|
+
let marker = null;
|
|
568
|
+
try {
|
|
569
|
+
marker = parseCodexOwnerMarker(await this.fileSystem.readFile(paths.codexPidPath, "utf8"));
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error.code !== "ENOENT") throw error;
|
|
572
|
+
}
|
|
573
|
+
return processes.filter(({ pid, command, verified }) => verified === true && (
|
|
574
|
+
hasManagedProfile(command, paths.profileDir)
|
|
575
|
+
|| (
|
|
576
|
+
pid === marker?.pid
|
|
577
|
+
&& command === marker.command
|
|
578
|
+
&& hasLoopbackCdpArguments(command, extractCdpPort(command))
|
|
579
|
+
)
|
|
580
|
+
));
|
|
437
581
|
}
|
|
438
582
|
|
|
439
583
|
async assertOwnedCodexStopped(paths) {
|
|
@@ -457,45 +601,104 @@ export class MacPlatformAdapter {
|
|
|
457
601
|
}
|
|
458
602
|
|
|
459
603
|
async terminateOwnedCodex(paths) {
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
pid = Number((await this.fileSystem.readFile(paths.codexPidPath, "utf8")).trim());
|
|
463
|
-
} catch (error) {
|
|
464
|
-
if (error.code === "ENOENT") {
|
|
465
|
-
await this.assertOwnedCodexStopped(paths);
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
throw error;
|
|
469
|
-
}
|
|
470
|
-
if (!Number.isInteger(pid) || pid <= 1) {
|
|
471
|
-
await this.assertOwnedCodexStopped(paths);
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
const processInfo = (await this.ownedCodexProcesses(paths)).find((candidate) => candidate.pid === pid);
|
|
604
|
+
const processInfo = (await this.ownedCodexProcesses(paths))[0];
|
|
475
605
|
if (!processInfo) {
|
|
476
606
|
await this.assertOwnedCodexStopped(paths);
|
|
477
607
|
return false;
|
|
478
608
|
}
|
|
479
|
-
this.processApi.kill(pid, "SIGTERM");
|
|
480
|
-
await waitForProcessExit(pid, { processApi: this.processApi, sleep: this.sleep });
|
|
609
|
+
this.processApi.kill(processInfo.pid, "SIGTERM");
|
|
610
|
+
await waitForProcessExit(processInfo.pid, { processApi: this.processApi, sleep: this.sleep });
|
|
481
611
|
await this.fileSystem.unlink(paths.codexPidPath).catch(() => {});
|
|
482
612
|
await this.assertOwnedCodexStopped(paths);
|
|
483
613
|
return true;
|
|
484
614
|
}
|
|
485
615
|
|
|
486
|
-
async
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
616
|
+
async codexProcessStartedMsAgo(processInfo) {
|
|
617
|
+
if (!processInfo || !Number.isInteger(processInfo.pid) || processInfo.pid <= 1) return null;
|
|
618
|
+
const { stdout } = await this.execFile("/bin/ps", ["-axo", "pid=,lstart="]);
|
|
619
|
+
const line = String(stdout).split("\n").find((row) => {
|
|
620
|
+
const match = row.match(/\s*(\d+)\s+(.*)/);
|
|
621
|
+
return match?.[1] === String(processInfo.pid);
|
|
622
|
+
});
|
|
623
|
+
const startedAt = line ? Date.parse(line.replace(/^\s*\d+\s+/, "")) : Number.NaN;
|
|
624
|
+
if (!line || !Number.isFinite(startedAt)) return null;
|
|
625
|
+
return Math.max(0, Date.now() - startedAt);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async terminateNativeCodex(pid, expectedCommand = null) {
|
|
629
|
+
if (!Number.isInteger(pid) || pid <= 1) {
|
|
630
|
+
throw new Error("MineCodex requires an exact ChatGPT PID to terminate.");
|
|
631
|
+
}
|
|
632
|
+
const processInfo = (await this.listCodexProcesses()).find((candidate) => (
|
|
633
|
+
candidate.pid === pid
|
|
634
|
+
&& candidate.verified === true
|
|
635
|
+
&& !candidate.command.includes("--user-data-dir=")
|
|
636
|
+
&& (!expectedCommand || candidate.command === expectedCommand)
|
|
637
|
+
));
|
|
638
|
+
if (!processInfo) return 0;
|
|
639
|
+
const { stdout: processTree } = await this.execFile("/bin/ps", ["-axo", "pid=,ppid=,command="]);
|
|
640
|
+
const ownedProcessTree = [pid, ...descendantPids(processTree, pid)];
|
|
641
|
+
this.processApi.kill(pid, "SIGTERM");
|
|
642
|
+
await waitForProcessesExit(ownedProcessTree, { processApi: this.processApi, sleep: this.sleep });
|
|
643
|
+
return 1;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async assertNativeCodexCdp(processInfo, cdpPort) {
|
|
647
|
+
if (!processInfo?.verified || !Number.isInteger(processInfo.pid) || !hasLoopbackCdpArguments(processInfo.command, cdpPort)) {
|
|
648
|
+
throw new Error("MineCodex could not bind the loopback CDP endpoint to a verified ChatGPT process.");
|
|
649
|
+
}
|
|
650
|
+
const { stdout } = await this.execFile("/usr/sbin/lsof", [
|
|
651
|
+
"-nP", "-a", "-p", String(processInfo.pid), `-iTCP:${cdpPort}`, "-sTCP:LISTEN", "-Fn",
|
|
652
|
+
]);
|
|
653
|
+
const fields = String(stdout).split("\n");
|
|
654
|
+
if (!fields.includes(`p${processInfo.pid}`) || !fields.includes(`n127.0.0.1:${cdpPort}`)) {
|
|
655
|
+
throw new Error(`ChatGPT pid ${processInfo.pid} does not own loopback CDP port ${cdpPort}.`);
|
|
656
|
+
}
|
|
657
|
+
return true;
|
|
491
658
|
}
|
|
492
659
|
|
|
493
660
|
async openBrowser(url) {
|
|
494
661
|
await this.execFile("/usr/bin/open", [url]);
|
|
495
662
|
}
|
|
496
663
|
|
|
497
|
-
async openNativeCodex() {
|
|
664
|
+
async openNativeCodex({ cdpPort } = {}) {
|
|
498
665
|
const installation = await this.codex();
|
|
499
|
-
|
|
666
|
+
if (!Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65_535) {
|
|
667
|
+
await this.execFile("/usr/bin/open", ["-a", installation.appPath]);
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const existingPids = new Set((await this.listCodexProcesses()).map(({ pid }) => pid));
|
|
671
|
+
await this.execFile("/usr/bin/open", [
|
|
672
|
+
"-n",
|
|
673
|
+
"-a",
|
|
674
|
+
installation.appPath,
|
|
675
|
+
"--args",
|
|
676
|
+
"--remote-debugging-address=127.0.0.1",
|
|
677
|
+
`--remote-debugging-port=${cdpPort}`,
|
|
678
|
+
`--remote-allow-origins=http://127.0.0.1:${cdpPort}`,
|
|
679
|
+
]);
|
|
680
|
+
return this.waitForNativeCodex({ cdpPort, excludedPids: existingPids });
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async waitForNativeCodex({ cdpPort, excludedPids = new Set(), timeoutMs = 15_000 } = {}) {
|
|
684
|
+
const deadline = Date.now() + timeoutMs;
|
|
685
|
+
while (Date.now() < deadline) {
|
|
686
|
+
const candidates = (await this.listCodexProcesses()).filter(({ pid, command, verified }) => (
|
|
687
|
+
verified === true
|
|
688
|
+
&& !excludedPids.has(pid)
|
|
689
|
+
&& !command.includes("--user-data-dir=")
|
|
690
|
+
&& hasLoopbackCdpArguments(command, cdpPort)
|
|
691
|
+
));
|
|
692
|
+
if (candidates.length > 1) {
|
|
693
|
+
throw new Error(`LaunchServices exposed multiple new ChatGPT processes on CDP port ${cdpPort}.`);
|
|
694
|
+
}
|
|
695
|
+
const processInfo = candidates[0];
|
|
696
|
+
if (processInfo) {
|
|
697
|
+
await this.assertNativeCodexCdp(processInfo, cdpPort);
|
|
698
|
+
return processInfo;
|
|
699
|
+
}
|
|
700
|
+
await this.sleep?.(50);
|
|
701
|
+
}
|
|
702
|
+
throw new Error(`LaunchServices did not expose a loopback CDP ChatGPT process on port ${cdpPort}.`);
|
|
500
703
|
}
|
|
501
704
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { ChatGPTLaunchCoordinator } from "./chatgpt-launch-coordinator.mjs";
|
|
3
4
|
import { readConfig, FEATURE_IDS } from "./config.mjs";
|
|
4
5
|
|
|
5
6
|
export const RUNTIME_PLUGIN_STATES = Object.freeze([
|
|
@@ -19,19 +20,6 @@ function hasExited(child) {
|
|
|
19
20
|
return Boolean(child && (child.exitCode != null || child.signalCode != null));
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function hasExactCommandArgument(command, argument) {
|
|
23
|
-
const value = String(command);
|
|
24
|
-
let offset = value.indexOf(argument);
|
|
25
|
-
while (offset >= 0) {
|
|
26
|
-
const before = offset === 0 || /\s/.test(value[offset - 1]);
|
|
27
|
-
const end = offset + argument.length;
|
|
28
|
-
const after = end === value.length || /\s/.test(value[end]);
|
|
29
|
-
if (before && after) return true;
|
|
30
|
-
offset = value.indexOf(argument, offset + 1);
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
23
|
function normalizedFailure(failure) {
|
|
36
24
|
if (!failure || typeof failure !== "object") return failure ?? null;
|
|
37
25
|
return {
|
|
@@ -132,8 +120,10 @@ export class RuntimeManager {
|
|
|
132
120
|
waitForReady: readyWaiter = waitForReady,
|
|
133
121
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
134
122
|
stopTimeoutMs = 5_000,
|
|
135
|
-
manualLaunchPollMs =
|
|
123
|
+
manualLaunchPollMs = 150,
|
|
124
|
+
manualTakeoverGraceMs = 60_000,
|
|
136
125
|
cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231),
|
|
126
|
+
launchCoordinator = null,
|
|
137
127
|
}) {
|
|
138
128
|
this.paths = paths;
|
|
139
129
|
this.platform = platform;
|
|
@@ -143,6 +133,7 @@ export class RuntimeManager {
|
|
|
143
133
|
this.sleep = sleep;
|
|
144
134
|
this.stopTimeoutMs = stopTimeoutMs;
|
|
145
135
|
this.manualLaunchPollMs = manualLaunchPollMs;
|
|
136
|
+
this.manualTakeoverGraceMs = manualTakeoverGraceMs;
|
|
146
137
|
this.cdpPort = cdpPort;
|
|
147
138
|
this.child = null;
|
|
148
139
|
this.appliedFeatures = [];
|
|
@@ -157,6 +148,13 @@ export class RuntimeManager {
|
|
|
157
148
|
this.manualLaunchWake = null;
|
|
158
149
|
this.manualLaunchBaselinePids = new Set();
|
|
159
150
|
this.manualLaunchArmed = false;
|
|
151
|
+
this.manualLaunchSuppressionDepth = 0;
|
|
152
|
+
this.enhancedCodex = null;
|
|
153
|
+
this.enhancementFailure = null;
|
|
154
|
+
this.launchCoordinator = launchCoordinator ?? new ChatGPTLaunchCoordinator({
|
|
155
|
+
platform,
|
|
156
|
+
cdpPort,
|
|
157
|
+
});
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
enqueue(operation) {
|
|
@@ -165,36 +163,33 @@ export class RuntimeManager {
|
|
|
165
163
|
return next;
|
|
166
164
|
}
|
|
167
165
|
|
|
168
|
-
async startInternal(
|
|
166
|
+
async startInternal() {
|
|
169
167
|
if (this.child && !hasExited(this.child)) return this.status();
|
|
170
168
|
this.clearChildState();
|
|
171
169
|
this.stopping = false;
|
|
172
170
|
const config = await readConfig(this.paths.configPath);
|
|
173
171
|
const features = enabledFeatureIds(config);
|
|
174
|
-
|
|
175
|
-
? await this.platform.codex()
|
|
176
|
-
: null;
|
|
177
|
-
const codexExecutable = codexInstallation?.executable
|
|
178
|
-
?? process.env.MINECODEX_CODEX_EXECUTABLE;
|
|
172
|
+
if (typeof this.platform?.codex === "function") await this.platform.codex();
|
|
179
173
|
await mkdir(this.paths.supportDir, { recursive: true, mode: 0o700 });
|
|
180
174
|
await unlink(this.paths.runtimeReadyPath).catch((error) => {
|
|
181
175
|
if (error.code !== "ENOENT") throw error;
|
|
182
176
|
});
|
|
177
|
+
const runtimeEnvironment = {
|
|
178
|
+
...process.env,
|
|
179
|
+
CODEX_FEATURES_ROOT: this.paths.featuresRoot,
|
|
180
|
+
CODEX_RUNTIME_PROFILE_DIR: this.paths.profileDir,
|
|
181
|
+
CODEX_RUNTIME_CDP_PORT: String(this.cdpPort),
|
|
182
|
+
CODEX_IMAGE_HOST_DATA_DIR: this.paths.imagesDataDir,
|
|
183
|
+
CODEX_NOTES_DATA_DIR: this.paths.notesDataDir,
|
|
184
|
+
MINECODEX_ENABLED_FEATURES: features.join(","),
|
|
185
|
+
MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
|
|
186
|
+
MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
|
|
187
|
+
};
|
|
188
|
+
delete runtimeEnvironment.MINECODEX_LAUNCH_CODEX;
|
|
189
|
+
delete runtimeEnvironment.MINECODEX_CODEX_EXECUTABLE;
|
|
183
190
|
const child = this.spawnProcess(process.execPath, [this.paths.runtimeEntry], {
|
|
184
191
|
cwd: this.paths.packageRoot,
|
|
185
|
-
env:
|
|
186
|
-
...process.env,
|
|
187
|
-
CODEX_FEATURES_ROOT: this.paths.featuresRoot,
|
|
188
|
-
CODEX_RUNTIME_PROFILE_DIR: this.paths.profileDir,
|
|
189
|
-
CODEX_RUNTIME_CDP_PORT: String(this.cdpPort),
|
|
190
|
-
CODEX_IMAGE_HOST_DATA_DIR: this.paths.imagesDataDir,
|
|
191
|
-
CODEX_NOTES_DATA_DIR: this.paths.notesDataDir,
|
|
192
|
-
MINECODEX_ENABLED_FEATURES: features.join(","),
|
|
193
|
-
MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
|
|
194
|
-
MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
|
|
195
|
-
MINECODEX_LAUNCH_CODEX: launchCodex ? "1" : "0",
|
|
196
|
-
...(codexExecutable ? { MINECODEX_CODEX_EXECUTABLE: codexExecutable } : {}),
|
|
197
|
-
},
|
|
192
|
+
env: runtimeEnvironment,
|
|
198
193
|
stdio: "inherit",
|
|
199
194
|
});
|
|
200
195
|
this.child = child;
|
|
@@ -261,9 +256,8 @@ export class RuntimeManager {
|
|
|
261
256
|
async nativeCodexSnapshot() {
|
|
262
257
|
if (typeof this.platform?.snapshotNativeCodexState !== "function") return null;
|
|
263
258
|
const snapshot = await this.platform.snapshotNativeCodexState();
|
|
264
|
-
const cdpArgument = `--remote-debugging-port=${this.cdpPort}`;
|
|
265
259
|
const processes = (Array.isArray(snapshot?.processes) ? snapshot.processes : [])
|
|
266
|
-
.filter(({ command }) => !
|
|
260
|
+
.filter(({ command }) => !String(command).includes("--user-data-dir="));
|
|
267
261
|
return {
|
|
268
262
|
count: processes.length,
|
|
269
263
|
processes,
|
|
@@ -271,11 +265,24 @@ export class RuntimeManager {
|
|
|
271
265
|
};
|
|
272
266
|
}
|
|
273
267
|
|
|
268
|
+
async syncManualLaunchBaseline() {
|
|
269
|
+
const snapshot = await this.nativeCodexSnapshot();
|
|
270
|
+
if (!snapshot) return;
|
|
271
|
+
this.manualLaunchBaselinePids = new Set(snapshot.pids);
|
|
272
|
+
this.manualLaunchArmed = true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async clearOwnedCodexMarker() {
|
|
276
|
+
await unlink(this.paths.codexPidPath).catch((error) => {
|
|
277
|
+
if (error.code !== "ENOENT") this.logger.warn?.("MineCodex could not clear its ChatGPT owner marker", error.message);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
274
281
|
async startManualLaunchMonitor() {
|
|
275
282
|
if (this.manualLaunchMonitorPromise || typeof this.platform?.snapshotNativeCodexState !== "function") return;
|
|
276
|
-
|
|
277
|
-
this.manualLaunchBaselinePids
|
|
278
|
-
this.manualLaunchArmed =
|
|
283
|
+
// 登录恢复时 ChatGPT 可能先于服务启动,首次扫描也必须纳入增强流程。
|
|
284
|
+
this.manualLaunchBaselinePids.clear();
|
|
285
|
+
this.manualLaunchArmed = true;
|
|
279
286
|
this.manualLaunchMonitorStopped = false;
|
|
280
287
|
const monitor = this.monitorManualCodexLaunches().catch((error) => {
|
|
281
288
|
if (!this.manualLaunchMonitorStopped) {
|
|
@@ -322,84 +329,130 @@ export class RuntimeManager {
|
|
|
322
329
|
this.manualLaunchArmed = false;
|
|
323
330
|
}
|
|
324
331
|
|
|
332
|
+
async isManualTakeoverAllowed(processInfo) {
|
|
333
|
+
if (typeof this.platform?.codexProcessStartedMsAgo !== "function") return true;
|
|
334
|
+
let startedMsAgo;
|
|
335
|
+
try {
|
|
336
|
+
startedMsAgo = await this.platform.codexProcessStartedMsAgo(processInfo);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
this.logger.warn?.("MineCodex could not read the ChatGPT start time", error.message);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
if (!Number.isInteger(startedMsAgo) || startedMsAgo < 0) return true;
|
|
342
|
+
return startedMsAgo < this.manualTakeoverGraceMs;
|
|
343
|
+
}
|
|
344
|
+
|
|
325
345
|
async reconcileManualCodexLaunch() {
|
|
326
|
-
if (this.manualLaunchMonitorStopped) return false;
|
|
346
|
+
if (this.manualLaunchMonitorStopped || this.manualLaunchSuppressionDepth > 0) return false;
|
|
327
347
|
const snapshot = await this.nativeCodexSnapshot();
|
|
328
348
|
if (!snapshot) return false;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.some((pid) => snapshot.pids.has(pid));
|
|
333
|
-
if (baselineStillRunning) return false;
|
|
334
|
-
this.manualLaunchBaselinePids.clear();
|
|
335
|
-
if (snapshot.count === 0) {
|
|
336
|
-
this.manualLaunchArmed = true;
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
this.manualLaunchArmed = true;
|
|
349
|
+
if (this.enhancedCodex && !snapshot.pids.has(this.enhancedCodex.pid)) {
|
|
350
|
+
if (this.enhancedCodex.owned) await this.clearOwnedCodexMarker();
|
|
351
|
+
this.enhancedCodex = null;
|
|
340
352
|
}
|
|
341
|
-
|
|
342
353
|
if (snapshot.count === 0) {
|
|
354
|
+
if (this.enhancedCodex?.owned) await this.clearOwnedCodexMarker();
|
|
355
|
+
this.enhancedCodex = null;
|
|
356
|
+
this.manualLaunchBaselinePids.clear();
|
|
343
357
|
this.manualLaunchArmed = true;
|
|
344
358
|
return false;
|
|
345
359
|
}
|
|
346
360
|
if (!this.manualLaunchArmed) return false;
|
|
361
|
+
const newProcesses = snapshot.processes.filter(({ pid }) => !this.manualLaunchBaselinePids.has(pid));
|
|
362
|
+
this.manualLaunchBaselinePids = new Set(snapshot.pids);
|
|
363
|
+
if (newProcesses.length === 0) return false;
|
|
364
|
+
if (newProcesses.length > 1) {
|
|
365
|
+
this.enhancementFailure = {
|
|
366
|
+
code: "MULTIPLE_CHATGPT_LAUNCHES",
|
|
367
|
+
message: "MineCodex found multiple new official ChatGPT processes and did not terminate any of them.",
|
|
368
|
+
phase: "launch",
|
|
369
|
+
};
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
347
372
|
|
|
348
|
-
const
|
|
373
|
+
const observedProcess = newProcesses[0];
|
|
349
374
|
this.manualLaunchArmed = false;
|
|
350
|
-
this.
|
|
375
|
+
this.enhancementFailure = null;
|
|
376
|
+
this.manualLaunchSuppressionDepth += 1;
|
|
351
377
|
return this.enqueue(async () => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
378
|
+
try {
|
|
379
|
+
if (this.manualLaunchMonitorStopped) return false;
|
|
380
|
+
const latest = await this.nativeCodexSnapshot();
|
|
381
|
+
const currentProcess = latest?.processes.find((candidate) => (
|
|
382
|
+
candidate.pid === observedProcess.pid
|
|
383
|
+
&& candidate.command === observedProcess.command
|
|
384
|
+
));
|
|
385
|
+
if (!currentProcess) {
|
|
359
386
|
this.manualLaunchArmed = true;
|
|
387
|
+
return false;
|
|
360
388
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
389
|
+
try {
|
|
390
|
+
if (!(await this.isManualTakeoverAllowed(currentProcess))) {
|
|
391
|
+
this.enhancementFailure = {
|
|
392
|
+
code: "OFFICIAL_CHATGPT_IN_USE",
|
|
393
|
+
message: "MineCodex left the official ChatGPT instance running because it was already in use.",
|
|
394
|
+
phase: "launch",
|
|
395
|
+
};
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
const result = await this.launchCoordinator.ensureObservedLaunch(currentProcess, {
|
|
399
|
+
stopRuntime: () => this.stopInternal(),
|
|
400
|
+
startRuntime: () => this.startInternal(),
|
|
401
|
+
waitForHealthy: () => this.waitForHealthyRuntime(),
|
|
402
|
+
});
|
|
403
|
+
this.enhancedCodex = result.codex;
|
|
404
|
+
this.enhancementFailure = null;
|
|
405
|
+
if (this.enhancedCodex.owned) await this.writeOwnedCodexPid(this.enhancedCodex);
|
|
406
|
+
await this.syncManualLaunchBaseline();
|
|
407
|
+
return result;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
this.enhancementFailure = normalizedFailure({
|
|
410
|
+
code: error.code ?? "CHATGPT_ENHANCEMENT_FAILED",
|
|
411
|
+
message: error.message,
|
|
412
|
+
phase: "launch",
|
|
413
|
+
});
|
|
414
|
+
try {
|
|
415
|
+
await this.startInternal();
|
|
416
|
+
} catch (recoveryError) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`${error.message}; idle RuntimeHost recovery failed: ${recoveryError.message}`,
|
|
419
|
+
{ cause: error },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
} finally {
|
|
387
425
|
this.manualLaunchArmed = true;
|
|
388
|
-
await this.
|
|
389
|
-
|
|
426
|
+
await this.syncManualLaunchBaseline().catch((error) => {
|
|
427
|
+
this.logger.warn?.("MineCodex could not refresh the ChatGPT launch baseline", error.message);
|
|
428
|
+
});
|
|
429
|
+
this.manualLaunchSuppressionDepth -= 1;
|
|
390
430
|
}
|
|
391
431
|
});
|
|
392
432
|
}
|
|
393
433
|
|
|
394
|
-
async
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
434
|
+
async writeOwnedCodexPid(processInfo) {
|
|
435
|
+
if (!this.paths.codexPidPath) return;
|
|
436
|
+
const marker = {
|
|
437
|
+
version: 1,
|
|
438
|
+
pid: processInfo?.pid,
|
|
439
|
+
command: processInfo?.command,
|
|
440
|
+
};
|
|
441
|
+
if (!Number.isInteger(marker.pid) || marker.pid <= 1 || typeof marker.command !== "string" || !marker.command) {
|
|
442
|
+
throw new Error("MineCodex cannot persist incomplete ChatGPT ownership identity.");
|
|
443
|
+
}
|
|
444
|
+
await writeFile(this.paths.codexPidPath, `${JSON.stringify(marker)}\n`, { mode: 0o600 });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async waitForHealthyRuntime(initialStatus = null, timeoutMs = 25_000) {
|
|
448
|
+
const deadline = Date.now() + timeoutMs;
|
|
449
|
+
let status = initialStatus ?? this.status();
|
|
450
|
+
while (Date.now() < deadline) {
|
|
451
|
+
if (!status.healthy) status = await this.refreshStatus();
|
|
452
|
+
if (status.healthy) return status;
|
|
453
|
+
await this.sleep(100);
|
|
402
454
|
}
|
|
455
|
+
throw new Error("MineCodex RuntimeHost did not prove renderer discovery and feature injection.");
|
|
403
456
|
}
|
|
404
457
|
|
|
405
458
|
async stopInternal() {
|
|
@@ -477,11 +530,54 @@ export class RuntimeManager {
|
|
|
477
530
|
|
|
478
531
|
restart() {
|
|
479
532
|
if (this.restartInFlight) return this.restartInFlight;
|
|
533
|
+
this.manualLaunchSuppressionDepth += 1;
|
|
480
534
|
const operation = this.enqueue(async () => {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
535
|
+
try {
|
|
536
|
+
const previousCodex = this.enhancedCodex;
|
|
537
|
+
if (previousCodex?.owned && typeof this.platform?.terminateNativeCodex === "function") {
|
|
538
|
+
const result = await this.launchCoordinator.relaunchOwned({
|
|
539
|
+
stopRuntime: () => this.stopInternal(),
|
|
540
|
+
terminateOwned: async () => {
|
|
541
|
+
const terminated = await this.platform.terminateNativeCodex(previousCodex.pid, previousCodex.command);
|
|
542
|
+
if (terminated !== 1) throw new Error(`MineCodex could not terminate owned ChatGPT pid ${previousCodex.pid}.`);
|
|
543
|
+
},
|
|
544
|
+
startRuntime: () => this.startInternal(),
|
|
545
|
+
waitForHealthy: (status) => this.waitForHealthyRuntime(status),
|
|
546
|
+
});
|
|
547
|
+
this.enhancedCodex = result.codex;
|
|
548
|
+
this.enhancementFailure = null;
|
|
549
|
+
if (this.enhancedCodex.pid) await this.writeOwnedCodexPid(this.enhancedCodex);
|
|
550
|
+
await this.syncManualLaunchBaseline();
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
if (previousCodex && !previousCodex.owned) {
|
|
554
|
+
await this.stopInternal();
|
|
555
|
+
const healthy = await this.waitForHealthyRuntime(await this.startInternal());
|
|
556
|
+
await this.syncManualLaunchBaseline();
|
|
557
|
+
return { ...healthy, codex: previousCodex };
|
|
558
|
+
}
|
|
559
|
+
await this.stopInternal();
|
|
560
|
+
if (!previousCodex) await this.platform.terminateOwnedCodex?.(this.paths);
|
|
561
|
+
this.enhancedCodex = null;
|
|
562
|
+
this.enhancementFailure = null;
|
|
563
|
+
if (typeof this.platform?.openNativeCodex !== "function") return this.startInternal();
|
|
564
|
+
const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
|
|
565
|
+
const status = await this.startInternal();
|
|
566
|
+
const healthy = await this.waitForHealthyRuntime(status);
|
|
567
|
+
this.enhancedCodex = {
|
|
568
|
+
pid: launched?.pid ?? null,
|
|
569
|
+
owned: true,
|
|
570
|
+
command: launched?.command ?? null,
|
|
571
|
+
};
|
|
572
|
+
if (this.enhancedCodex.pid) await this.writeOwnedCodexPid(this.enhancedCodex);
|
|
573
|
+
await this.syncManualLaunchBaseline();
|
|
574
|
+
return { ...healthy, codex: this.enhancedCodex };
|
|
575
|
+
} finally {
|
|
576
|
+
await this.syncManualLaunchBaseline().catch((error) => {
|
|
577
|
+
this.logger.warn?.("MineCodex could not refresh the ChatGPT launch baseline", error.message);
|
|
578
|
+
});
|
|
579
|
+
this.manualLaunchSuppressionDepth -= 1;
|
|
580
|
+
}
|
|
485
581
|
});
|
|
486
582
|
this.restartInFlight = operation;
|
|
487
583
|
const shared = this.restartInFlight.finally(() => {
|
|
@@ -492,8 +588,10 @@ export class RuntimeManager {
|
|
|
492
588
|
|
|
493
589
|
status() {
|
|
494
590
|
const running = Boolean(this.child && !hasExited(this.child));
|
|
591
|
+
const healthy = running && this.ready?.renderer?.active === true;
|
|
495
592
|
return {
|
|
496
593
|
running,
|
|
594
|
+
healthy,
|
|
497
595
|
pid: this.child?.pid ?? null,
|
|
498
596
|
appliedFeatures: running ? [...this.appliedFeatures] : [],
|
|
499
597
|
plugins: running ? [...this.plugins] : [],
|
|
@@ -501,7 +599,10 @@ export class RuntimeManager {
|
|
|
501
599
|
ready: running ? this.ready : null,
|
|
502
600
|
runtimeSessionId: running ? this.ready?.runtimeSessionId ?? null : null,
|
|
503
601
|
renderer: running ? this.ready?.renderer ?? null : null,
|
|
504
|
-
|
|
602
|
+
codex: this.enhancedCodex ? { ...this.enhancedCodex } : null,
|
|
603
|
+
failures: running
|
|
604
|
+
? [...(this.ready?.failures ?? []), ...(this.enhancementFailure ? [this.enhancementFailure] : [])]
|
|
605
|
+
: (this.enhancementFailure ? [this.enhancementFailure] : []),
|
|
505
606
|
};
|
|
506
607
|
}
|
|
507
608
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { mkdir, stat } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
@@ -321,6 +320,9 @@ export function createInjectionSource(features, {
|
|
|
321
320
|
[data-codex-model-slider-trigger-label] {
|
|
322
321
|
display: inline; overflow: visible; text-overflow: clip; white-space: nowrap;
|
|
323
322
|
}
|
|
323
|
+
[data-codex-model-slider-trigger] [class*="_ModelPickerTriggerEffortLabel_"] {
|
|
324
|
+
display: inline-flex; align-items: center; align-self: center;
|
|
325
|
+
}
|
|
324
326
|
html[data-codex-model-slider-selecting-effort] [role="menu"][data-state="open"]:not(:has([data-reasoning-slider])) {
|
|
325
327
|
visibility: hidden !important; opacity: 0 !important; animation: none !important; pointer-events: none !important;
|
|
326
328
|
}
|
|
@@ -374,7 +376,6 @@ export function createInjectionSource(features, {
|
|
|
374
376
|
if (modelSelectorFeature) {
|
|
375
377
|
document.documentElement.setAttribute("data-codex-model-slider-catalog-ready", "");
|
|
376
378
|
document.documentElement.append(modelSelectorStyle);
|
|
377
|
-
void loadNativeFastIcon();
|
|
378
379
|
}
|
|
379
380
|
|
|
380
381
|
for (const feature of pageScriptFeatures) {
|
|
@@ -617,32 +618,6 @@ export function createInjectionSource(features, {
|
|
|
617
618
|
return icon;
|
|
618
619
|
}
|
|
619
620
|
|
|
620
|
-
async function loadNativeFastIcon() {
|
|
621
|
-
const sourceLink = document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
|
|
622
|
-
if (!sourceLink?.href) return;
|
|
623
|
-
try {
|
|
624
|
-
const source = await fetch(sourceLink.href).then((response) => response.text());
|
|
625
|
-
const active = source.match(/d:`(M11\.9125 21\.4125[^`]+)`,fill:`currentColor`/);
|
|
626
|
-
const inactive = source.match(/d:`(M7\.38 16\.2207[^`]+)`,fill:`currentColor`/);
|
|
627
|
-
if (!active?.[1] || !inactive?.[1]) return;
|
|
628
|
-
modelSelectorNativeFastIcons = {
|
|
629
|
-
active: { viewBox: "0 0 24 24", markup: `<path d="${active[1]}" fill="currentColor" />` },
|
|
630
|
-
inactive: {
|
|
631
|
-
viewBox: "0 0 20 20",
|
|
632
|
-
markup: `<g transform="translate(2.43 1.609)"><path d="${inactive[1]}" fill="currentColor" /></g>`,
|
|
633
|
-
},
|
|
634
|
-
};
|
|
635
|
-
document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
|
|
636
|
-
const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
|
|
637
|
-
const content = button.querySelector("[data-codex-model-slider-fast-content]");
|
|
638
|
-
if (icon && content) content.replaceChildren(icon);
|
|
639
|
-
});
|
|
640
|
-
queueEnsure();
|
|
641
|
-
} catch {
|
|
642
|
-
// Leave the native control untouched when this private Codex asset moves.
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
621
|
function nativeCssModuleClass(tokenName, root = document) {
|
|
647
622
|
const pattern = new RegExp(`^_${tokenName}_[A-Za-z0-9_-]+$`);
|
|
648
623
|
const live = Array.from(root.querySelectorAll(`[class*="_${tokenName}_"]`))
|
|
@@ -674,11 +649,33 @@ export function createInjectionSource(features, {
|
|
|
674
649
|
return null;
|
|
675
650
|
}
|
|
676
651
|
|
|
652
|
+
function captureNativeFastIcon() {
|
|
653
|
+
const capture = (state, pathPrefix) => {
|
|
654
|
+
if (modelSelectorNativeFastIcons?.[state]) return;
|
|
655
|
+
const path = Array.from(document.querySelectorAll("svg path")).find(
|
|
656
|
+
(candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
|
|
657
|
+
);
|
|
658
|
+
const svg = path?.closest("svg");
|
|
659
|
+
if (!svg) return;
|
|
660
|
+
const template = svg.cloneNode(true);
|
|
661
|
+
template.removeAttribute("id");
|
|
662
|
+
template.removeAttribute("width");
|
|
663
|
+
template.removeAttribute("height");
|
|
664
|
+
if (!modelSelectorNativeFastIcons) modelSelectorNativeFastIcons = {};
|
|
665
|
+
modelSelectorNativeFastIcons[state] = template;
|
|
666
|
+
};
|
|
667
|
+
capture("active", "M11.9125 21.4125");
|
|
668
|
+
capture("inactive", "M7.38 16.2207");
|
|
669
|
+
return modelSelectorNativeFastIcons;
|
|
670
|
+
}
|
|
671
|
+
|
|
677
672
|
function createNativeFastIcon(active) {
|
|
678
|
-
const
|
|
679
|
-
|
|
673
|
+
const template = captureNativeFastIcon()?.[active ? "active" : "inactive"];
|
|
674
|
+
if (!template) return null;
|
|
675
|
+
const icon = template.cloneNode(true);
|
|
676
|
+
icon.setAttribute("aria-hidden", "true");
|
|
680
677
|
const iconClass = nativeCssModuleClass("FastModeIcon");
|
|
681
|
-
if (
|
|
678
|
+
if (iconClass && !icon.classList.contains(iconClass)) icon.classList.add(iconClass);
|
|
682
679
|
return icon;
|
|
683
680
|
}
|
|
684
681
|
|
|
@@ -1847,8 +1844,14 @@ export function createInjectionSource(features, {
|
|
|
1847
1844
|
const model = modelDefinitionFor(identity, selector);
|
|
1848
1845
|
const efforts = model?.supportedReasoningLevels ?? [];
|
|
1849
1846
|
if (!model || efforts.length < 2) return false;
|
|
1850
|
-
|
|
1851
|
-
|
|
1847
|
+
// 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
|
|
1848
|
+
// 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
|
|
1849
|
+
const nativeModel = nativeCatalogModelFor(identity);
|
|
1850
|
+
const selectedEffort = nativeModel?.defaultReasoningLevel
|
|
1851
|
+
?? document.querySelector("[data-codex-intelligence-trigger]")
|
|
1852
|
+
?.getAttribute("data-selected-reasoning-effort")
|
|
1853
|
+
?? model.defaultReasoningLevel
|
|
1854
|
+
?? efforts[0].effort;
|
|
1852
1855
|
const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
|
|
1853
1856
|
const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
|
|
1854
1857
|
const key = `${identity.raw}:${selectedEffort}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
|
|
@@ -1857,7 +1860,6 @@ export function createInjectionSource(features, {
|
|
|
1857
1860
|
parentMenu.firstElementChild
|
|
1858
1861
|
?.querySelector("[data-model-picker-power-slider] [class*='_TickRail_'] > span"),
|
|
1859
1862
|
);
|
|
1860
|
-
const nativeModel = nativeCatalogModelFor(identity);
|
|
1861
1863
|
const templateWaitCount = Number(parentMenu.getAttribute("data-codex-model-slider-template-wait") ?? 0);
|
|
1862
1864
|
if (nativeModel?.slug.toLowerCase().startsWith("gpt-") && !nativeTemplateReady && templateWaitCount < 3) {
|
|
1863
1865
|
parentMenu.setAttribute("data-codex-model-slider-template-wait", String(templateWaitCount + 1));
|
|
@@ -1953,6 +1955,7 @@ export function createInjectionSource(features, {
|
|
|
1953
1955
|
function ensureModelSelector() {
|
|
1954
1956
|
const selector = modelSelectorFeature?.modelSelector;
|
|
1955
1957
|
if (!selector) return;
|
|
1958
|
+
captureNativeFastIcon();
|
|
1956
1959
|
pruneModelSubmenuPositionObservers();
|
|
1957
1960
|
if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
|
|
1958
1961
|
enhanceComposerModelTrigger(selector);
|
|
@@ -2997,28 +3000,18 @@ export function createInjectionSource(features, {
|
|
|
2997
3000
|
const moduleUrl = await waitForNativeTabModuleUrl();
|
|
2998
3001
|
if (!moduleUrl) throw new Error("Codex app-initial modulepreload was not found");
|
|
2999
3002
|
const appModule = await import(moduleUrl);
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
if (
|
|
3010
|
-
typeof controller?.openTab !== "function"
|
|
3011
|
-
|| typeof controller?.activateTab !== "function"
|
|
3012
|
-
|| typeof controller?.closeTab !== "function"
|
|
3013
|
-
|| !controller?.tabById$
|
|
3014
|
-
|| typeof jsx?.jsx !== "function"
|
|
3015
|
-
|| typeof React?.useLayoutEffect !== "function"
|
|
3016
|
-
|| typeof React?.useRef !== "function"
|
|
3017
|
-
) throw new Error("Codex native tab capability is incomplete");
|
|
3003
|
+
const controller = Object.values(appModule).find((value) => (
|
|
3004
|
+
value
|
|
3005
|
+
&& typeof value === "object"
|
|
3006
|
+
&& typeof value.openTab === "function"
|
|
3007
|
+
&& typeof value.closeTab === "function"
|
|
3008
|
+
&& typeof value.activateTab === "function"
|
|
3009
|
+
&& value.panelId === "right"
|
|
3010
|
+
));
|
|
3011
|
+
if (!controller) throw new Error("Codex right-panel controller is unavailable");
|
|
3012
|
+
if (!controller?.tabById$) throw new Error("Codex native tab capability is incomplete");
|
|
3018
3013
|
nativeTabCapability = {
|
|
3019
3014
|
controller,
|
|
3020
|
-
jsx,
|
|
3021
|
-
React,
|
|
3022
3015
|
moduleAsset: moduleUrl.split("/").pop(),
|
|
3023
3016
|
};
|
|
3024
3017
|
nativeTabCapabilityError = null;
|
|
@@ -3033,10 +3026,20 @@ export function createInjectionSource(features, {
|
|
|
3033
3026
|
return capability;
|
|
3034
3027
|
}
|
|
3035
3028
|
|
|
3036
|
-
function
|
|
3029
|
+
function nativeReactElement(type, props) {
|
|
3030
|
+
return {
|
|
3031
|
+
$$typeof: Symbol.for("react.transitional.element"),
|
|
3032
|
+
type,
|
|
3033
|
+
key: null,
|
|
3034
|
+
ref: null,
|
|
3035
|
+
props: props ?? {},
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
function nativeDetailIcon(feature, detail) {
|
|
3037
3040
|
const icon = detail.icon ?? feature.icon;
|
|
3038
3041
|
if (!icon?.markup) return undefined;
|
|
3039
|
-
return
|
|
3042
|
+
return nativeReactElement("svg", {
|
|
3040
3043
|
width: 16,
|
|
3041
3044
|
height: 16,
|
|
3042
3045
|
viewBox: icon.viewBox ?? "0 0 24 24",
|
|
@@ -3051,42 +3054,13 @@ export function createInjectionSource(features, {
|
|
|
3051
3054
|
});
|
|
3052
3055
|
}
|
|
3053
3056
|
|
|
3054
|
-
function nativeDetailComponent(feature, detail
|
|
3057
|
+
function nativeDetailComponent(feature, detail) {
|
|
3055
3058
|
const detailKey = `${feature.id}:${detail.id}`;
|
|
3056
3059
|
let Component = nativeDetailComponents.get(detailKey);
|
|
3057
3060
|
if (Component) return Component;
|
|
3058
|
-
const
|
|
3061
|
+
const frameName = surfaceFrameName(feature.id);
|
|
3059
3062
|
Component = function CodexPersonalDetailTab() {
|
|
3060
|
-
|
|
3061
|
-
const frameRef = React.useRef(null);
|
|
3062
|
-
const recordKeyRef = React.useRef(null);
|
|
3063
|
-
const frameNameRef = React.useRef(null);
|
|
3064
|
-
frameNameRef.current ??= surfaceFrameName(feature.id);
|
|
3065
|
-
React.useLayoutEffect(() => {
|
|
3066
|
-
const element = elementRef.current;
|
|
3067
|
-
const frame = frameRef.current;
|
|
3068
|
-
if (!element || !frame) return undefined;
|
|
3069
|
-
const instanceId = crypto.randomUUID?.()
|
|
3070
|
-
?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3071
|
-
const recordKey = surfaceKey(feature.id, "native-detail", `${detail.id}:${instanceId}`);
|
|
3072
|
-
recordKeyRef.current = recordKey;
|
|
3073
|
-
registerSurface(recordKey, feature, "detail", detail.surfaceUrl, element, frame, detail.id);
|
|
3074
|
-
let keys = nativeDetailSurfaceKeys.get(detailKey);
|
|
3075
|
-
if (!keys) {
|
|
3076
|
-
keys = new Set();
|
|
3077
|
-
nativeDetailSurfaceKeys.set(detailKey, keys);
|
|
3078
|
-
}
|
|
3079
|
-
keys.add(recordKey);
|
|
3080
|
-
queueTheme();
|
|
3081
|
-
return () => {
|
|
3082
|
-
keys.delete(recordKey);
|
|
3083
|
-
if (keys.size === 0) nativeDetailSurfaceKeys.delete(detailKey);
|
|
3084
|
-
removeSurfaceRecord(recordKey);
|
|
3085
|
-
recordKeyRef.current = null;
|
|
3086
|
-
};
|
|
3087
|
-
}, []);
|
|
3088
|
-
return jsx.jsx("div", {
|
|
3089
|
-
ref: elementRef,
|
|
3063
|
+
return nativeReactElement("div", {
|
|
3090
3064
|
"data-codex-personal-native-detail": detailKey,
|
|
3091
3065
|
style: {
|
|
3092
3066
|
height: "100%",
|
|
@@ -3094,9 +3068,8 @@ export function createInjectionSource(features, {
|
|
|
3094
3068
|
overflow: "hidden",
|
|
3095
3069
|
background: "var(--color-token-main-surface-primary)",
|
|
3096
3070
|
},
|
|
3097
|
-
children:
|
|
3098
|
-
|
|
3099
|
-
name: frameNameRef.current,
|
|
3071
|
+
children: nativeReactElement("iframe", {
|
|
3072
|
+
name: frameName,
|
|
3100
3073
|
src: "about:blank",
|
|
3101
3074
|
title: detail.label,
|
|
3102
3075
|
allow: "clipboard-write",
|
|
@@ -3124,17 +3097,17 @@ export function createInjectionSource(features, {
|
|
|
3124
3097
|
return reloaded;
|
|
3125
3098
|
}
|
|
3126
3099
|
|
|
3127
|
-
async function
|
|
3100
|
+
async function waitForNativeDetailTabElement(detailKey) {
|
|
3128
3101
|
const deadline = performance.now() + 2_000;
|
|
3129
3102
|
do {
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
if (
|
|
3103
|
+
const element = document.querySelector(
|
|
3104
|
+
`[data-codex-personal-native-detail="${detailKey}"]`,
|
|
3105
|
+
);
|
|
3106
|
+
const frame = element?.querySelector("iframe");
|
|
3107
|
+
if (element && frame) return { element, frame };
|
|
3135
3108
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
3136
3109
|
} while (performance.now() < deadline);
|
|
3137
|
-
return
|
|
3110
|
+
return null;
|
|
3138
3111
|
}
|
|
3139
3112
|
|
|
3140
3113
|
async function openNativeDetailTab(feature, detail) {
|
|
@@ -3146,7 +3119,7 @@ export function createInjectionSource(features, {
|
|
|
3146
3119
|
return null;
|
|
3147
3120
|
}
|
|
3148
3121
|
|
|
3149
|
-
const { controller
|
|
3122
|
+
const { controller } = capability;
|
|
3150
3123
|
const tabId = `${feature.id}-${detail.id}`;
|
|
3151
3124
|
const detailKey = `${feature.id}:${detail.id}`;
|
|
3152
3125
|
let session = Array.from(nativeOpenTabSessions).find((candidate) => (
|
|
@@ -3165,15 +3138,22 @@ export function createInjectionSource(features, {
|
|
|
3165
3138
|
}
|
|
3166
3139
|
|
|
3167
3140
|
try {
|
|
3168
|
-
controller.openTab(scope, nativeDetailComponent(feature, detail
|
|
3141
|
+
controller.openTab(scope, nativeDetailComponent(feature, detail), {
|
|
3169
3142
|
id: tabId,
|
|
3170
3143
|
kind: tabId,
|
|
3171
3144
|
title: detail.label,
|
|
3172
3145
|
tooltip: detail.label,
|
|
3173
|
-
icon: nativeDetailIcon(feature, detail
|
|
3146
|
+
icon: nativeDetailIcon(feature, detail),
|
|
3174
3147
|
isClosable: true,
|
|
3175
3148
|
props: {},
|
|
3176
|
-
onClose: () =>
|
|
3149
|
+
onClose: () => {
|
|
3150
|
+
nativeOpenTabSessions.delete(session);
|
|
3151
|
+
const closedKeys = nativeDetailSurfaceKeys.get(detailKey);
|
|
3152
|
+
if (closedKeys) {
|
|
3153
|
+
for (const key of closedKeys) removeSurfaceRecord(key);
|
|
3154
|
+
nativeDetailSurfaceKeys.delete(detailKey);
|
|
3155
|
+
}
|
|
3156
|
+
},
|
|
3177
3157
|
});
|
|
3178
3158
|
} catch (error) {
|
|
3179
3159
|
if (isNewSession) nativeOpenTabSessions.delete(session);
|
|
@@ -3181,15 +3161,29 @@ export function createInjectionSource(features, {
|
|
|
3181
3161
|
return null;
|
|
3182
3162
|
}
|
|
3183
3163
|
|
|
3184
|
-
if (!
|
|
3185
|
-
|
|
3186
|
-
if (!
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3164
|
+
if (!nativeDetailSurfaceKeys.has(detailKey)) {
|
|
3165
|
+
const mounted = await waitForNativeDetailTabElement(detailKey);
|
|
3166
|
+
if (!mounted) {
|
|
3167
|
+
if (isNewSession) nativeOpenTabSessions.delete(session);
|
|
3168
|
+
if (!focusedExisting) {
|
|
3169
|
+
try {
|
|
3170
|
+
controller.closeTab(scope, tabId);
|
|
3171
|
+
} catch {}
|
|
3172
|
+
}
|
|
3173
|
+
nativeTabCapabilityError = new Error("Codex native right-panel Tab did not mount");
|
|
3174
|
+
return null;
|
|
3190
3175
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3176
|
+
const instanceId = crypto.randomUUID?.()
|
|
3177
|
+
?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3178
|
+
const recordKey = surfaceKey(feature.id, "native-detail", `${detail.id}:${instanceId}`);
|
|
3179
|
+
registerSurface(recordKey, feature, "detail", detail.surfaceUrl, mounted.element, mounted.frame, detail.id);
|
|
3180
|
+
let keys = nativeDetailSurfaceKeys.get(detailKey);
|
|
3181
|
+
if (!keys) {
|
|
3182
|
+
keys = new Set();
|
|
3183
|
+
nativeDetailSurfaceKeys.set(detailKey, keys);
|
|
3184
|
+
}
|
|
3185
|
+
keys.add(recordKey);
|
|
3186
|
+
queueTheme();
|
|
3193
3187
|
}
|
|
3194
3188
|
|
|
3195
3189
|
nativeTabCapabilityError = null;
|
|
@@ -3207,6 +3201,9 @@ export function createInjectionSource(features, {
|
|
|
3207
3201
|
}
|
|
3208
3202
|
|
|
3209
3203
|
function closeNativeDetailTabs() {
|
|
3204
|
+
for (const keys of Array.from(nativeDetailSurfaceKeys.values())) {
|
|
3205
|
+
for (const key of keys) removeSurfaceRecord(key);
|
|
3206
|
+
}
|
|
3210
3207
|
for (const session of Array.from(nativeOpenTabSessions)) {
|
|
3211
3208
|
try {
|
|
3212
3209
|
session.controller.closeTab(session.scope, session.tabId);
|
|
@@ -3923,7 +3920,7 @@ export class CodexRuntime {
|
|
|
3923
3920
|
cdpPort = 9231,
|
|
3924
3921
|
logger = console,
|
|
3925
3922
|
fetchImpl = globalThis.fetch,
|
|
3926
|
-
|
|
3923
|
+
launchApplication = null,
|
|
3927
3924
|
connectClient = connect,
|
|
3928
3925
|
sleep = (timeoutMs) => new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
3929
3926
|
availabilityTimeoutMs = 20_000,
|
|
@@ -3942,7 +3939,7 @@ export class CodexRuntime {
|
|
|
3942
3939
|
this.cdpPort = cdpPort;
|
|
3943
3940
|
this.logger = logger;
|
|
3944
3941
|
this.fetchImpl = fetchImpl;
|
|
3945
|
-
this.
|
|
3942
|
+
this.launchApplication = launchApplication;
|
|
3946
3943
|
this.connectClient = connectClient;
|
|
3947
3944
|
this.sleep = sleep;
|
|
3948
3945
|
this.availabilityTimeoutMs = availabilityTimeoutMs;
|
|
@@ -4079,17 +4076,19 @@ export class CodexRuntime {
|
|
|
4079
4076
|
}
|
|
4080
4077
|
|
|
4081
4078
|
launchManagedCodex() {
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4079
|
+
if (typeof this.launchApplication !== "function") {
|
|
4080
|
+
throw new Error("CodexRuntime requires an explicit application launcher for isolated tests.");
|
|
4081
|
+
}
|
|
4082
|
+
const child = this.launchApplication({
|
|
4083
|
+
appPath: this.appPath,
|
|
4084
|
+
args: [
|
|
4085
4085
|
`--user-data-dir=${this.profileDir}`,
|
|
4086
4086
|
"--remote-debugging-address=127.0.0.1",
|
|
4087
4087
|
`--remote-debugging-port=${this.cdpPort}`,
|
|
4088
4088
|
`--remote-allow-origins=http://127.0.0.1:${this.cdpPort}`,
|
|
4089
4089
|
"--no-first-run",
|
|
4090
4090
|
],
|
|
4091
|
-
|
|
4092
|
-
);
|
|
4091
|
+
});
|
|
4093
4092
|
this.managedCodexChild = child;
|
|
4094
4093
|
this.appPid = child.pid ?? null;
|
|
4095
4094
|
this.notifyManagedCodexPidChange(this.appPid);
|
|
@@ -17,7 +17,6 @@ const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot
|
|
|
17
17
|
const legacyDataDir = process.env.CODEX_IMAGE_HOST_DATA_DIR ?? path.join(os.homedir(), ".codex-image-host");
|
|
18
18
|
const profileDir = process.env.CODEX_RUNTIME_PROFILE_DIR ?? path.join(legacyDataDir, "codex-profile");
|
|
19
19
|
const cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231);
|
|
20
|
-
const launchCodex = process.env.MINECODEX_LAUNCH_CODEX === "1";
|
|
21
20
|
|
|
22
21
|
const discoveredFeatures = await discoverFeatures(featuresRoot);
|
|
23
22
|
if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
|
|
@@ -66,13 +65,12 @@ runtime = new CodexRuntime({
|
|
|
66
65
|
featureInstanceId,
|
|
67
66
|
profileDir,
|
|
68
67
|
cdpPort,
|
|
69
|
-
appPath: process.env.MINECODEX_CODEX_EXECUTABLE || undefined,
|
|
70
68
|
onManagedCodexPidChange: updateCodexPidFile,
|
|
71
69
|
onStatusChange: requestReadyFileRefresh,
|
|
72
70
|
});
|
|
73
71
|
|
|
74
72
|
try {
|
|
75
|
-
await runtime.start(
|
|
73
|
+
await runtime.start();
|
|
76
74
|
runtimeReady = true;
|
|
77
75
|
await writeReadyFile();
|
|
78
76
|
} catch (error) {
|