@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.10
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 +14 -15
- package/dist/bin/codex-remote.mjs +81 -26
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +1 -1
- package/dist/bin/self-relay-server.mjs +5 -2
- package/dist/shared/app-server-events.cjs +50 -22
- package/dist/shared/p2p-data-channel.cjs +50 -0
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +11 -11
- package/dist/src/lan-socket-server.mjs +8 -2
- package/dist/src/managed-app-server.mjs +27 -7
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +3 -5
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
- package/dist/src/platform/windows/background-process.mjs +7 -2
- package/dist/src/platform/windows/codex-runtime.mjs +117 -0
- package/dist/src/platform/windows/desktop-host.mjs +16 -1
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +1 -1
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +1 -1
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/service-host.mjs +4 -7
- package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +71 -0
- package/dist/src/self-relay/admin-server.mjs +61 -0
- package/dist/src/self-relay/admin-state.mjs +122 -0
- package/dist/src/self-relay/client.mjs +2 -0
- package/dist/src/self-relay/connector.mjs +23 -8
- package/dist/src/self-relay/demo.mjs +4 -5
- package/dist/src/self-relay/lifecycle.mjs +53 -49
- package/dist/src/self-relay/p2p-probe.mjs +123 -83
- package/dist/src/self-relay/server.mjs +157 -17
- package/dist/src/server.mjs +484 -1274
- package/dist/src/service-doctor.mjs +13 -25
- package/dist/src/service-lifecycle.mjs +12 -12
- package/dist/src/shared-app-server.mjs +192 -34
- package/dist/src/shared-catalog.mjs +4 -14
- package/dist/src/shared-recovery.mjs +8 -6
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/web/activity-view.js +283 -0
- package/dist/web/capabilities.js +2 -9
- package/dist/web/chat-transport.js +21 -21
- package/dist/web/chat.css +148 -106
- package/dist/web/chat.js +1454 -2788
- package/dist/web/community.css +82 -0
- package/dist/web/community.html +36 -0
- package/dist/web/composer-controller.js +97 -0
- package/dist/web/conversation-controller.js +99 -0
- package/dist/web/disclosure-state-controller.js +95 -0
- package/dist/web/draft-controller.js +103 -0
- package/dist/web/harmony-platform.js +3 -2
- package/dist/web/history-cache.js +112 -18
- package/dist/web/history-controller.js +167 -0
- package/dist/web/index.html +113 -47
- package/dist/web/link-action-controller.js +212 -0
- package/dist/web/message-send-controller.js +177 -0
- package/dist/web/message-view.js +98 -0
- package/dist/web/p2p-data-channel.js +50 -0
- package/dist/web/p2p-probe.js +67 -27
- package/dist/web/page-resume.js +28 -0
- package/dist/web/pending-message-store.js +108 -0
- package/dist/web/queue-controller.js +82 -0
- package/dist/web/resources.json +1 -1
- package/dist/web/self-relay-session.js +28 -18
- package/dist/web/station-connection-controller.js +75 -0
- package/dist/web/task-list-view.js +289 -0
- package/dist/web/thread-attention-controller.js +124 -0
- package/dist/web/thread-context-controller.js +30 -0
- package/dist/web/thread-list-controller.js +61 -0
- package/dist/web/thread-list-sync.js +86 -0
- package/dist/web/thread-title-controller.js +57 -0
- package/dist/web/timeline-formatters.js +249 -0
- package/dist/web/timeline-reducer.js +77 -0
- package/dist/web/timeline-renderer.js +161 -0
- package/dist/web/timeline-scroll-controller.js +50 -0
- package/dist/web/usage-controller.js +258 -0
- package/dist/web/vendor/lucide.LICENSE.txt +17 -0
- package/package.json +16 -18
- package/tools/postinstall.mjs +140 -3
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -355
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -13
- package/dist/src/thread-catalog.mjs +0 -42
- package/dist/tools/find-desktop-pipe.mjs +0 -10
|
@@ -5,9 +5,11 @@ import { EventEmitter } from 'node:events';
|
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
6
|
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
7
7
|
import { SharedAppServer } from './shared-app-server.mjs';
|
|
8
|
+
import { resolveCodexRuntime } from './platform/windows/codex-runtime.mjs';
|
|
8
9
|
|
|
9
10
|
export const managedAppServerUrl = 'ws://127.0.0.1:45839';
|
|
10
11
|
export const managedAppServerReadyUrl = 'http://127.0.0.1:45839/readyz';
|
|
12
|
+
export const managedAppServerLauncherRevision = 2;
|
|
11
13
|
const retryDelays = [1_000, 2_000, 5_000, 10_000, 20_000];
|
|
12
14
|
|
|
13
15
|
export function managedPortAvailable() {
|
|
@@ -65,36 +67,46 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
65
67
|
if (!this.state.ready) this.publish({ state: 'starting', ready: false, error: null });
|
|
66
68
|
try {
|
|
67
69
|
fs.mkdirSync(this.stateRoot, { recursive: true });
|
|
68
|
-
const info = await this.host.inspect();
|
|
69
|
-
if (!info.cli) throw new Error('未找到 Desktop 管理的 codex.exe,请先安装并登录 Codex Desktop。');
|
|
70
70
|
let record = null, actual = null;
|
|
71
71
|
for (const candidate of [readRecord(this.recordPath), readRecord(this.legacyRecordPath)].filter(Boolean)) {
|
|
72
72
|
const process = candidate.pid ? await this.host.process(candidate.pid) : null;
|
|
73
73
|
if (sameManagedProcess(candidate, process)) { record = candidate; actual = process; break; }
|
|
74
74
|
}
|
|
75
|
+
const runtime = actual
|
|
76
|
+
? readRecord(path.join(this.stateRoot, 'codex-runtime.json')) || { executable: actual.executable }
|
|
77
|
+
: await resolveCodexRuntime(this.stateRoot, { environment: this.environment });
|
|
75
78
|
if (!actual) {
|
|
76
79
|
if (!await this.available()) throw new Error('45839 被未识别进程占用,未启动或结束任何进程。');
|
|
77
80
|
const childEnvironment = { ...this.environment };
|
|
81
|
+
childEnvironment.CODEX_HOME = runtime.codexHome;
|
|
78
82
|
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnvironment[key];
|
|
79
|
-
const
|
|
83
|
+
for (const key of Object.keys(childEnvironment)) {
|
|
84
|
+
if (/^npm_/iu.test(key) || key.toUpperCase() === 'INIT_CWD') delete childEnvironment[key];
|
|
85
|
+
}
|
|
86
|
+
const pid = await this.host.start(runtime.executable, ['app-server', '--listen', managedAppServerUrl], childEnvironment,
|
|
80
87
|
[path.join(this.stateRoot, 'managed-app-server.stdout.log'), path.join(this.stateRoot, 'managed-app-server.stderr.log')]);
|
|
81
88
|
const started = await this.host.process(pid);
|
|
82
89
|
if (!sameManagedProcess(started, started)) throw new Error('Codex 内核启动后进程身份无法确认。');
|
|
83
|
-
|
|
90
|
+
actual = started;
|
|
91
|
+
record = { ...started, launcherRevision: managedAppServerLauncherRevision };
|
|
84
92
|
writeRecord(this.recordPath, record);
|
|
85
93
|
} else if (!readRecord(this.recordPath)) {
|
|
86
|
-
|
|
94
|
+
record = { ...actual, launcherRevision: Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0 };
|
|
95
|
+
writeRecord(this.recordPath, record);
|
|
87
96
|
}
|
|
88
97
|
await this.waitUntilReady(actual);
|
|
89
98
|
await this.client.connect();
|
|
90
99
|
if (!this.client.ready) throw new Error('Codex 内核协议初始化未完成。');
|
|
91
|
-
const preferredExecutable = path.resolve(
|
|
100
|
+
const preferredExecutable = path.resolve(runtime.executable);
|
|
92
101
|
const updatePending = path.resolve(actual.executable).toLowerCase() !== preferredExecutable.toLowerCase();
|
|
102
|
+
const launcherRevision = Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0;
|
|
93
103
|
this.retryAttempt = 0;
|
|
94
104
|
this.retryAfter = 0;
|
|
95
105
|
this.publish({ state: 'ready', ready: true, error: null, pid: actual.pid,
|
|
96
106
|
created: actual.created, executable: actual.executable, preferredExecutable,
|
|
97
|
-
updatePending,
|
|
107
|
+
updatePending, launcherRevision, selectedVersion: runtime.version, source: runtime.source,
|
|
108
|
+
launcherRestartRequired: launcherRevision !== managedAppServerLauncherRevision,
|
|
109
|
+
url: managedAppServerUrl });
|
|
98
110
|
return true;
|
|
99
111
|
} catch (error) {
|
|
100
112
|
this.failed(error instanceof Error ? error.message : String(error));
|
|
@@ -138,7 +150,11 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
138
150
|
if (actual && !sameManagedProcess(record, actual)) {
|
|
139
151
|
throw new Error('Codex 内核进程身份已经变化,未结束任何进程。');
|
|
140
152
|
}
|
|
153
|
+
if (!actual && !await this.available()) throw new Error('45839 被未识别进程占用,未结束任何进程。');
|
|
141
154
|
this.restartStage('stopping');
|
|
155
|
+
// Resolve the replacement before stopping the current working process.
|
|
156
|
+
const runtime = await resolveCodexRuntime(this.stateRoot, { refresh: true, environment: this.environment });
|
|
157
|
+
const desktop = await this.host.prepareKernelRestart?.();
|
|
142
158
|
this.client.close();
|
|
143
159
|
if (actual) {
|
|
144
160
|
await this.host.stop(actual.pid);
|
|
@@ -152,6 +168,10 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
152
168
|
this.restartStage('starting');
|
|
153
169
|
const ready = await this.ensureNow();
|
|
154
170
|
if (!ready) throw new Error(this.state.error || 'Codex 内核未能重新启动。');
|
|
171
|
+
const desktopEnvironment = { ...this.environment, CODEX_HOME: runtime.codexHome, CODEX_APP_SERVER_WS_URL: managedAppServerUrl };
|
|
172
|
+
delete desktopEnvironment.CODEX_APP_SERVER_FORCE_CLI;
|
|
173
|
+
delete desktopEnvironment.CODEX_APP_TOOLS_PIPE_PATH;
|
|
174
|
+
await this.host.resumeAfterKernelRestart?.(desktop, desktopEnvironment);
|
|
155
175
|
this.restartStatus = { ...this.restartStatus, status: 'completed', stage: 'ready',
|
|
156
176
|
completedAt: new Date(this.now()).toISOString() };
|
|
157
177
|
this.emit('state', this.snapshot());
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
import sharp from 'sharp';
|
|
5
|
-
|
|
6
1
|
export function validateInlineImage(value) {
|
|
7
2
|
const data = typeof value === 'string' ? value.slice(value.indexOf(',') + 1) : '';
|
|
8
3
|
if (typeof value !== 'string' || value.length > Math.ceil(8 * 1024 * 1024 / 3) * 4 + 64 ||
|
|
@@ -12,16 +7,3 @@ export function validateInlineImage(value) {
|
|
|
12
7
|
}
|
|
13
8
|
return value;
|
|
14
9
|
}
|
|
15
|
-
|
|
16
|
-
// Only the Desktop tool compatibility path needs a file visible to the host.
|
|
17
|
-
export async function persistLegacyImage(value, directory) {
|
|
18
|
-
validateInlineImage(value);
|
|
19
|
-
const input = Buffer.from(value.slice(value.indexOf(',') + 1), 'base64');
|
|
20
|
-
const buffer = await sharp(input, { limitInputPixels: 40_000_000 }).rotate()
|
|
21
|
-
.resize({ width: 2560, height: 2560, fit: 'inside', withoutEnlargement: true }).webp({ quality: 88 }).toBuffer();
|
|
22
|
-
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
23
|
-
await fs.mkdir(directory, { recursive: true });
|
|
24
|
-
const file = path.join(directory, `${id}.webp`);
|
|
25
|
-
await fs.writeFile(file, buffer);
|
|
26
|
-
return file;
|
|
27
|
-
}
|
package/dist/src/onboarding.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const connectionModesText = [
|
|
2
2
|
'可选连接模式:',
|
|
3
3
|
'• 局域网:启动时会把当前 Wi-Fi/有线网络候选临时发给已配对设备;App 验证后保存,也可手动输入 IP。',
|
|
4
|
-
'•
|
|
4
|
+
'• 私有中继:同一设备授权可跨网络长期重连;Codex 内核就绪后才能进入聊天。',
|
|
5
5
|
'• OpenAI 官方中继:在已配对电脑内使用官方账号和设备授权;认证仍直连 OpenAI。',
|
|
6
6
|
].join('\n');
|
|
7
7
|
|
|
8
8
|
export function phonePairingSteps(transportMode, paired = false) {
|
|
9
|
-
const mode = transportMode === 'lan-shared' ? '局域网 ·
|
|
9
|
+
const mode = transportMode === 'lan-shared' ? '局域网 · Codex 内核已就绪' : '局域网(Codex 内核未就绪)';
|
|
10
10
|
return paired ? [
|
|
11
11
|
`已有移动设备授权。手机可继续使用私有中继;${mode} 会显示验证成功的当前网络和历史记录。`,
|
|
12
12
|
'添加另一台设备时执行 relax pair;每台设备只需扫码配对一次。',
|
|
@@ -19,10 +19,8 @@ export function phonePairingSteps(transportMode, paired = false) {
|
|
|
19
19
|
export const desktopSetupSteps = [
|
|
20
20
|
'Code Relax Skill 已随 npm 包自动安装。',
|
|
21
21
|
'安装完成后 Bridge 和受管 app-server 已启动,并默认在当前用户登录时启动。',
|
|
22
|
-
'
|
|
23
|
-
'基础控制首次聊天需要在 Codex 对话启动一次 Code Relax 绑定上下文;完整控制不需要。',
|
|
22
|
+
'CLI 用户无需安装 Desktop;已安装 Desktop 时,安装器负责将运行中的 Desktop 接入同一内核。',
|
|
24
23
|
'尚无设备授权时执行 relax pair,打开并扫描配对二维码。',
|
|
25
|
-
'若工作站提示 Desktop 尚未接入受管服务,可在手机确认重新打开 Desktop,无需电脑脚本或快捷方式。',
|
|
26
24
|
'统一配对先建立电脑记录;进入该电脑后可另行完成 OpenAI 官方 Remote 配对。',
|
|
27
25
|
];
|
|
28
26
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
当前只实现 Windows Codex:
|
|
4
4
|
|
|
5
5
|
- `windows/runtime-paths.mjs`:固定服务、用户状态、Codex 数据、附件和长期凭据路径。
|
|
6
|
-
- `windows/service-host.mjs
|
|
6
|
+
- `windows/service-host.mjs`:网卡、Bridge 进程环境和旧状态迁移。
|
|
7
7
|
- `windows/desktop-host.mjs`、PowerShell 脚本:普通/共享 Desktop、快捷方式和隐藏控制台启动。
|
|
8
8
|
- `windows/credential-store.mjs/.ps1`:私有中继电脑凭据的 DPAPI 与 ACL。
|
|
9
9
|
|
|
@@ -48,8 +48,8 @@ public static class IsolatedProcess {
|
|
|
48
48
|
if(handle==new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
49
49
|
return handle;
|
|
50
50
|
}
|
|
51
|
-
public static int Start(string executable,string[] arguments,string stdout,string stderr) {
|
|
52
|
-
IntPtr input=IntPtr.Zero,output=IntPtr.Zero,error=IntPtr.Zero,list=IntPtr.Zero,handles=IntPtr.Zero;
|
|
51
|
+
public static int Start(string executable,string[] arguments,string stdout,string stderr,string[] environment) {
|
|
52
|
+
IntPtr input=IntPtr.Zero,output=IntPtr.Zero,error=IntPtr.Zero,list=IntPtr.Zero,handles=IntPtr.Zero,environmentBlock=IntPtr.Zero;
|
|
53
53
|
bool initialized=false;
|
|
54
54
|
try {
|
|
55
55
|
input=File("NUL",true); output=File(String.IsNullOrEmpty(stdout)?"NUL":stdout,false); error=File(String.IsNullOrEmpty(stderr)?"NUL":stderr,false);
|
|
@@ -63,12 +63,16 @@ public static class IsolatedProcess {
|
|
|
63
63
|
startup.startup.flags=0x101; startup.startup.show=0;
|
|
64
64
|
startup.startup.input=input; startup.startup.output=output; startup.startup.error=error; startup.attributes=list;
|
|
65
65
|
var command=new StringBuilder(Quote(executable)); foreach(var argument in arguments) command.Append(' ').Append(Quote(argument));
|
|
66
|
+
Array.Sort(environment,StringComparer.OrdinalIgnoreCase);
|
|
67
|
+
var environmentBytes=Encoding.Unicode.GetBytes(String.Join("\0",environment)+"\0\0");
|
|
68
|
+
environmentBlock=Marshal.AllocHGlobal(environmentBytes.Length); Marshal.Copy(environmentBytes,0,environmentBlock,environmentBytes.Length);
|
|
66
69
|
ProcessInfo info;
|
|
67
|
-
if(!CreateProcessW(executable,command,IntPtr.Zero,IntPtr.Zero,true,
|
|
70
|
+
if(!CreateProcessW(executable,command,IntPtr.Zero,IntPtr.Zero,true,0x80410,environmentBlock,null,ref startup,out info)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
68
71
|
CloseHandle(info.thread); CloseHandle(info.process); return info.pid;
|
|
69
72
|
} finally {
|
|
70
73
|
if(initialized) DeleteProcThreadAttributeList(list);
|
|
71
74
|
if(list!=IntPtr.Zero) Marshal.FreeHGlobal(list); if(handles!=IntPtr.Zero) Marshal.FreeHGlobal(handles);
|
|
75
|
+
if(environmentBlock!=IntPtr.Zero) Marshal.FreeHGlobal(environmentBlock);
|
|
72
76
|
if(input!=IntPtr.Zero) CloseHandle(input); if(output!=IntPtr.Zero) CloseHandle(output); if(error!=IntPtr.Zero) CloseHandle(error);
|
|
73
77
|
}
|
|
74
78
|
}
|
|
@@ -5,9 +5,14 @@ import path from 'node:path';
|
|
|
5
5
|
export function startWindowsProcess(executable, args, environment, logs = []) {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
8
|
+
const helperEnvironment = {};
|
|
9
|
+
for (const name of ['SystemRoot', 'windir', 'LOCALAPPDATA', 'USERPROFILE', 'TEMP', 'TMP', 'PSModulePath']) {
|
|
10
|
+
const key = Object.keys(process.env).find(item => item.toLowerCase() === name.toLowerCase());
|
|
11
|
+
if (key) helperEnvironment[key] = process.env[key];
|
|
12
|
+
}
|
|
8
13
|
const helper = spawn(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
9
14
|
fileURLToPath(new URL('./start-hidden-console.ps1', import.meta.url))], {
|
|
10
|
-
windowsHide: true, env:
|
|
15
|
+
windowsHide: true, env: helperEnvironment, stdio: ['pipe', 'pipe', 'pipe'],
|
|
11
16
|
});
|
|
12
17
|
let output = '', error = '';
|
|
13
18
|
helper.stdout.setEncoding('utf8').on('data', chunk => { output += chunk; });
|
|
@@ -20,6 +25,6 @@ export function startWindowsProcess(executable, args, environment, logs = []) {
|
|
|
20
25
|
if (!Number.isSafeInteger(pid) || pid <= 0) return reject(new Error('后台启动未返回有效 PID'));
|
|
21
26
|
resolve(pid);
|
|
22
27
|
});
|
|
23
|
-
helper.stdin.end(JSON.stringify({ executable, args, logs }));
|
|
28
|
+
helper.stdin.end(JSON.stringify({ brokerExecutable: process.execPath, executable, args, logs, environment }));
|
|
24
29
|
});
|
|
25
30
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
|
|
8
|
+
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
const requiredMethods = ['thread/start', 'thread/resume', 'thread/list', 'thread/turns/list',
|
|
11
|
+
'thread/items/list', 'turn/start', 'turn/steer', 'turn/interrupt', 'model/list', 'project/list',
|
|
12
|
+
'thread/settings/update', 'configRequirements/read', 'thread/queue/list', 'thread/queue/add',
|
|
13
|
+
'thread/queue/delete', 'thread/queue/start', 'thread/read', 'thread/unsubscribe', 'config/read', 'account/rateLimits/read'];
|
|
14
|
+
const fingerprint = file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; };
|
|
15
|
+
|
|
16
|
+
// Resolve native binaries, never execute a shell shim from a background task.
|
|
17
|
+
export function discoverCodex(environment = process.env) {
|
|
18
|
+
const found = new Map();
|
|
19
|
+
const add = (file, source) => {
|
|
20
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
|
|
21
|
+
const executable = fs.realpathSync(file);
|
|
22
|
+
if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
|
|
23
|
+
};
|
|
24
|
+
if (environment.CODE_RELAX_CODEX_PATH) {
|
|
25
|
+
const file = environment.CODE_RELAX_CODEX_PATH;
|
|
26
|
+
if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
|
|
27
|
+
add(file, 'explicit');
|
|
28
|
+
if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
|
|
29
|
+
return [...found.values()];
|
|
30
|
+
}
|
|
31
|
+
const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
|
|
32
|
+
const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
33
|
+
const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
|
|
34
|
+
environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
|
|
35
|
+
path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
|
|
36
|
+
for (const directory of directories) {
|
|
37
|
+
add(path.join(directory, 'codex.exe'), 'cli');
|
|
38
|
+
const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
|
|
39
|
+
if (!fs.existsSync(packageFile)) continue;
|
|
40
|
+
const root = path.dirname(fs.realpathSync(packageFile));
|
|
41
|
+
let vendor = path.join(root, 'vendor');
|
|
42
|
+
try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
|
|
43
|
+
catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
|
|
44
|
+
add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
|
|
45
|
+
}
|
|
46
|
+
const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
|
|
47
|
+
add(path.join(desktopRoot, 'codex.exe'), 'desktop');
|
|
48
|
+
if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
|
|
49
|
+
if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
|
|
50
|
+
}
|
|
51
|
+
return [...found.values()];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compareVersions(a, b) {
|
|
55
|
+
const parse = v => v.split(/[.-]/).map(part => /^\d+$/.test(part) ? Number(part) : part);
|
|
56
|
+
const aa = parse(a), bb = parse(b);
|
|
57
|
+
for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
|
|
58
|
+
if (aa[i] === bb[i]) continue;
|
|
59
|
+
if (aa[i] === undefined) return 1;
|
|
60
|
+
if (bb[i] === undefined) return -1;
|
|
61
|
+
return typeof aa[i] === 'number' && typeof bb[i] === 'number' ? aa[i] - bb[i] : String(aa[i]).localeCompare(String(bb[i]));
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function verifyProtocol(executable, stateRoot) {
|
|
67
|
+
const directory = fs.mkdtempSync(path.join(stateRoot, 'cli-schema-'));
|
|
68
|
+
try {
|
|
69
|
+
await exec(executable, ['app-server', 'generate-json-schema', '--experimental', '--out', directory],
|
|
70
|
+
{ windowsHide: true, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
|
|
71
|
+
const request = fs.readFileSync(path.join(directory, 'ClientRequest.json'), 'utf8');
|
|
72
|
+
const missing = requiredMethods.filter(method => !request.includes(`"${method}"`));
|
|
73
|
+
const turns = fs.readFileSync(path.join(directory, 'v2/ThreadTurnsListParams.json'), 'utf8');
|
|
74
|
+
if (!turns.includes('"itemsView"') || !turns.includes('"summary"')) missing.push('turn summary pagination');
|
|
75
|
+
if (missing.length) throw new Error(`缺少协议能力:${missing.join(', ')}`);
|
|
76
|
+
} finally {
|
|
77
|
+
const remove = dir => {
|
|
78
|
+
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
79
|
+
const file = path.join(dir, item.name);
|
|
80
|
+
if (item.isDirectory()) remove(file); else fs.unlinkSync(file);
|
|
81
|
+
}
|
|
82
|
+
fs.rmdirSync(dir);
|
|
83
|
+
};
|
|
84
|
+
remove(directory);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
|
|
89
|
+
fs.mkdirSync(stateRoot, { recursive: true });
|
|
90
|
+
const recordFile = path.join(stateRoot, 'codex-runtime.json');
|
|
91
|
+
const saved = readRecord(recordFile);
|
|
92
|
+
environment = { ...environment,
|
|
93
|
+
CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
|
|
94
|
+
CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
|
|
95
|
+
if (!refresh && saved?.executable && fs.existsSync(saved.executable)
|
|
96
|
+
&& saved.fingerprint === fingerprint(saved.executable)) return saved;
|
|
97
|
+
const candidates = [], rejected = [];
|
|
98
|
+
for (const candidate of discoverCodex(environment)) {
|
|
99
|
+
try {
|
|
100
|
+
const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
|
|
101
|
+
const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
|
|
102
|
+
if (!version) throw new Error('无法识别版本');
|
|
103
|
+
candidates.push({ ...candidate, version });
|
|
104
|
+
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
105
|
+
}
|
|
106
|
+
candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
try {
|
|
109
|
+
await verifyProtocol(candidate.executable, stateRoot);
|
|
110
|
+
const result = { ...candidate, fingerprint: fingerprint(candidate.executable),
|
|
111
|
+
codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
|
|
112
|
+
writeRecord(recordFile, result);
|
|
113
|
+
return result;
|
|
114
|
+
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
|
|
117
|
+
}
|
|
@@ -17,7 +17,22 @@ export const desktopHost = {
|
|
|
17
17
|
sharedServiceUrl,
|
|
18
18
|
start: startWindowsProcess,
|
|
19
19
|
closeDesktop: executable => runScript('stop-desktop.ps1', ['-Executable', executable]),
|
|
20
|
-
stop: pid => process.kill(pid),
|
|
20
|
+
stop: pid => process.kill(pid),
|
|
21
|
+
async prepareKernelRestart() {
|
|
22
|
+
const info = await this.inspect();
|
|
23
|
+
if (!info.running || !await this.sharedServiceUrl()) return null;
|
|
24
|
+
await this.closeDesktop(info.desktop);
|
|
25
|
+
return info.desktop;
|
|
26
|
+
},
|
|
27
|
+
async resumeAfterKernelRestart(executable, environment) {
|
|
28
|
+
if (!executable) return;
|
|
29
|
+
await this.start(executable, [], environment, []);
|
|
30
|
+
const deadline = Date.now() + 30000;
|
|
31
|
+
while (!await this.sharedServiceUrl()) {
|
|
32
|
+
if (Date.now() >= deadline) throw new Error('Codex 内核已就绪,但 Desktop 未完成重新接入。');
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
21
36
|
ready: async url => { try { return (await fetch(url, { signal: AbortSignal.timeout(1000) })).ok; } catch { return false; } },
|
|
22
37
|
};
|
|
23
38
|
export const resolveStateRoot = stateRoot => runScript('resolve-state-root.ps1', ['-StateRoot', stateRoot]);
|
|
@@ -8,9 +8,7 @@ if ($ProcessId -gt 0) {
|
|
|
8
8
|
exit
|
|
9
9
|
}
|
|
10
10
|
$package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
|
|
11
|
-
if (-not $package) {
|
|
11
|
+
if (-not $package) { @{ desktop=$null; running=$false } | ConvertTo-Json -Compress; exit }
|
|
12
12
|
$desktop = Join-Path $package.InstallLocation 'app/ChatGPT.exe'
|
|
13
13
|
$running = @(Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $desktop })
|
|
14
|
-
|
|
15
|
-
$cli = Get-ChildItem -LiteralPath $binaryRoot -Filter codex.exe -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
16
|
-
@{ desktop=$desktop; cli=$cli.FullName; running=($running.Count -gt 0) } | ConvertTo-Json -Compress
|
|
14
|
+
@{ desktop=$desktop; running=($running.Count -gt 0) } | ConvertTo-Json -Compress
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
param([string]$NodePath, [string]$Entry, [ValidateSet('shared-start'
|
|
1
|
+
param([string]$NodePath, [string]$Entry, [ValidateSet('shared-start')][string]$Action)
|
|
2
2
|
$ErrorActionPreference = 'Stop'
|
|
3
3
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
4
|
& $NodePath $Entry $Action
|
|
@@ -4,7 +4,7 @@ $ErrorActionPreference = 'Stop'
|
|
|
4
4
|
$shell = New-Object -ComObject WScript.Shell
|
|
5
5
|
if (-not $Destination) { $Destination = $shell.SpecialFolders.Item('Desktop') }
|
|
6
6
|
$launcher = Join-Path $PSScriptRoot 'desktop-shortcut-run.ps1'
|
|
7
|
-
$items = @(@{Name='Code Relax -
|
|
7
|
+
$items = @(@{Name='Code Relax - Codex 内核'; Command='shared-start'})
|
|
8
8
|
$results = @()
|
|
9
9
|
foreach ($item in $items) {
|
|
10
10
|
$target = Join-Path $Destination ($item.Name + '.lnk')
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
const [requestPath, readyPath, instance, diagnosticPath] = process.argv.slice(2);
|
|
5
|
+
let result = { instance, brokerPid: process.pid };
|
|
6
|
+
|
|
7
|
+
function diagnostic(message) {
|
|
8
|
+
if (!diagnosticPath) return;
|
|
9
|
+
fs.appendFileSync(diagnosticPath, `${new Date().toISOString()} ${message}\n`, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const spec = JSON.parse(fs.readFileSync(requestPath, 'utf8').replace(/^\uFEFF/u, ''));
|
|
14
|
+
diagnostic('request loaded');
|
|
15
|
+
if (spec.instance !== instance) throw new Error('Windows process broker request identity mismatch.');
|
|
16
|
+
fs.rmSync(requestPath, { force: true });
|
|
17
|
+
const descriptors = (spec.logs || []).map(file => fs.openSync(file, 'a'));
|
|
18
|
+
try {
|
|
19
|
+
const child = spawn(spec.executable, spec.args || [], {
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
env: spec.environment,
|
|
22
|
+
stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'],
|
|
23
|
+
});
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
child.once('spawn', resolve);
|
|
26
|
+
child.once('error', reject);
|
|
27
|
+
});
|
|
28
|
+
// Keep this WMI-owned broker alive with the target so Codex tools inherit
|
|
29
|
+
// its hidden console instead of creating a visible console per command.
|
|
30
|
+
result.pid = child.pid;
|
|
31
|
+
diagnostic(`target started (PID ${child.pid})`);
|
|
32
|
+
} finally {
|
|
33
|
+
descriptors.forEach(descriptor => fs.closeSync(descriptor));
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
result.error = error?.stack || error?.message || String(error);
|
|
37
|
+
try { diagnostic(`failed: ${result.error}`); } catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const temporary = `${readyPath}.tmp`;
|
|
41
|
+
fs.writeFileSync(temporary, JSON.stringify(result), 'utf8');
|
|
42
|
+
fs.renameSync(temporary, readyPath);
|
|
@@ -1,27 +1,67 @@
|
|
|
1
1
|
param([string]$NodePath, [string]$Entry, [ValidateSet('enable','disable','status')][string]$Action='status')
|
|
2
2
|
$ErrorActionPreference = 'Stop'
|
|
3
3
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
+
|
|
5
|
+
$taskName = 'Code Relax Bridge'
|
|
6
|
+
$taskSource = '@walkhi/code-relax'
|
|
7
|
+
$taskPath = '\' + $taskName
|
|
8
|
+
$arguments = '"' + $Entry + '" start --json'
|
|
9
|
+
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
10
|
+
|
|
11
|
+
$scheduler = New-Object -ComObject 'Schedule.Service'
|
|
12
|
+
$scheduler.Connect()
|
|
13
|
+
$root = $scheduler.GetFolder('\')
|
|
14
|
+
$registered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
|
|
15
|
+
if ($registered.Count -gt 0 -and $registered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
|
|
16
|
+
throw 'Scheduled task is not owned by this installation.'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Remove only the legacy shortcut created by this package. A Startup LNK that
|
|
20
|
+
# launches hidden PowerShell with ExecutionPolicy Bypass triggers AV heuristics.
|
|
4
21
|
$shell = New-Object -ComObject WScript.Shell
|
|
5
|
-
$
|
|
6
|
-
$
|
|
7
|
-
$
|
|
8
|
-
$
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
throw 'Startup shortcut is not owned by this installation.'
|
|
22
|
+
$legacyPath = Join-Path $shell.SpecialFolders.Item('Startup') 'Code Relax Bridge.lnk'
|
|
23
|
+
$legacyLauncher = Join-Path $PSScriptRoot 'resident-start.ps1'
|
|
24
|
+
$legacyExists = Test-Path -LiteralPath $legacyPath
|
|
25
|
+
if ($Action -ne 'status' -and $legacyExists) {
|
|
26
|
+
$legacy = $shell.CreateShortcut($legacyPath)
|
|
27
|
+
if (-not $legacy.Arguments.Contains('"' + $legacyLauncher + '"')) {
|
|
28
|
+
throw 'Legacy startup shortcut is not owned by this installation.'
|
|
13
29
|
}
|
|
14
30
|
}
|
|
31
|
+
|
|
15
32
|
if ($Action -eq 'enable') {
|
|
16
33
|
if (-not (Test-Path -LiteralPath $NodePath) -or -not (Test-Path -LiteralPath $Entry)) { throw 'Installed runtime is missing.' }
|
|
17
|
-
$
|
|
18
|
-
$
|
|
19
|
-
$
|
|
20
|
-
$
|
|
21
|
-
$
|
|
22
|
-
$
|
|
23
|
-
$
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
$definition = $scheduler.NewTask(0)
|
|
35
|
+
$definition.RegistrationInfo.Description = 'Start the Code Relax Bridge when this user signs in.'
|
|
36
|
+
$definition.RegistrationInfo.Source = $taskSource
|
|
37
|
+
$definition.Principal.UserId = $currentUser
|
|
38
|
+
$definition.Principal.LogonType = 3 # TASK_LOGON_INTERACTIVE_TOKEN
|
|
39
|
+
$definition.Principal.RunLevel = 0 # TASK_RUNLEVEL_LUA
|
|
40
|
+
$definition.Settings.Enabled = $true
|
|
41
|
+
$definition.Settings.Hidden = $true
|
|
42
|
+
$definition.Settings.StartWhenAvailable = $true
|
|
43
|
+
$definition.Settings.DisallowStartIfOnBatteries = $false
|
|
44
|
+
$definition.Settings.StopIfGoingOnBatteries = $false
|
|
45
|
+
$definition.Settings.ExecutionTimeLimit = 'PT5M'
|
|
46
|
+
$definition.Settings.MultipleInstances = 2 # TASK_INSTANCES_IGNORE_NEW
|
|
47
|
+
$trigger = $definition.Triggers.Create(9) # TASK_TRIGGER_LOGON
|
|
48
|
+
$trigger.UserId = $currentUser
|
|
49
|
+
$trigger.Enabled = $true
|
|
50
|
+
$exec = $definition.Actions.Create(0) # TASK_ACTION_EXEC
|
|
51
|
+
$exec.Path = $NodePath
|
|
52
|
+
$exec.Arguments = $arguments
|
|
53
|
+
$exec.WorkingDirectory = $env:USERPROFILE
|
|
54
|
+
[void]$root.RegisterTaskDefinition($taskName, $definition, 6, $null, $null, 3, $null) # TASK_CREATE_OR_UPDATE
|
|
55
|
+
} elseif ($Action -eq 'disable' -and $registered.Count -gt 0) {
|
|
56
|
+
$root.DeleteTask($taskName, 0)
|
|
26
57
|
}
|
|
27
|
-
|
|
58
|
+
if ($Action -ne 'status' -and $legacyExists) { Remove-Item -LiteralPath $legacyPath }
|
|
59
|
+
|
|
60
|
+
$active = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
|
|
61
|
+
$taskEnabled = $active.Count -gt 0 -and $active[0].Enabled
|
|
62
|
+
@{
|
|
63
|
+
enabled=($taskEnabled -or ($Action -eq 'status' -and $legacyExists))
|
|
64
|
+
path=$(if ($taskEnabled) { 'Task Scheduler:' + $taskPath } elseif ($legacyExists) { $legacyPath } else { 'Task Scheduler:' + $taskPath })
|
|
65
|
+
scope='current-user-logon'
|
|
66
|
+
mechanism=$(if ($taskEnabled) { 'task-scheduler' } elseif ($legacyExists) { 'legacy-startup-shortcut' } else { 'task-scheduler' })
|
|
67
|
+
} | ConvertTo-Json -Compress
|
|
@@ -28,16 +28,13 @@ export function migrateState(stateRoot) {
|
|
|
28
28
|
if (!fs.existsSync(target) && fs.existsSync(old)) fs.copyFileSync(old, target, fs.constants.COPYFILE_EXCL);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
export async function
|
|
32
|
-
const environment = { ...process.env };
|
|
33
|
-
|
|
34
|
-
if (sharedUrl) environment.CODEX_REMOTE_SHARED_WS_URL = sharedUrl;
|
|
35
|
-
else delete environment.CODEX_REMOTE_SHARED_WS_URL;
|
|
36
|
-
const installation = readRecord(path.join(runtimePaths(environment).serviceRoot, 'installation.json'));
|
|
31
|
+
export async function bridgeEnvironment() {
|
|
32
|
+
const environment = { ...process.env };
|
|
33
|
+
const installation = readRecord(path.join(runtimePaths(environment).serviceRoot, 'installation.json'));
|
|
37
34
|
const executable = installation?.nodeExecutable;
|
|
38
35
|
if (!executable || !fs.existsSync(executable)) throw new Error('安装时选用的 Node 不存在,请重新安装 Code Relax。');
|
|
39
36
|
environment.CODEX_MCP_NODE_PATH = executable;
|
|
40
37
|
environment.CODEX_REMOTE_LEGACY_UPLOADS_ROOTS = JSON.stringify(installation.legacyUploadsRoots || []);
|
|
41
|
-
|
|
38
|
+
delete environment.CODEX_APP_TOOLS_PIPE_PATH;
|
|
42
39
|
return { executable, environment };
|
|
43
40
|
}
|
|
@@ -10,30 +10,40 @@ try {
|
|
|
10
10
|
Add-Type -Path (Join-Path $PSScriptRoot 'IsolatedProcess.cs')
|
|
11
11
|
$launchRoot = [IsolatedProcess]::ResolvePath($launchRoot)
|
|
12
12
|
$ready = Join-Path $launchRoot ($instance + '.json')
|
|
13
|
+
$request = Join-Path $launchRoot ($instance + '.request.json')
|
|
14
|
+
$diagnostic = Join-Path $launchRoot ($instance + '.broker.log')
|
|
13
15
|
$spec | Add-Member -NotePropertyName instance -NotePropertyValue $instance
|
|
14
|
-
$spec | Add-Member -NotePropertyName
|
|
15
|
-
$
|
|
16
|
-
$
|
|
16
|
+
$spec | Add-Member -NotePropertyName diagnostic -NotePropertyValue $diagnostic
|
|
17
|
+
$requestTemporary = $request + '.tmp'
|
|
18
|
+
[IO.File]::WriteAllText($requestTemporary, ($spec | ConvertTo-Json -Compress -Depth 10), (New-Object Text.UTF8Encoding($false)))
|
|
19
|
+
[IO.File]::Move($requestTemporary, $request)
|
|
17
20
|
$startup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{
|
|
18
|
-
ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
|
|
21
|
+
ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
|
|
19
22
|
}
|
|
20
|
-
$
|
|
21
|
-
$command = '"' + $
|
|
23
|
+
$worker = Join-Path $PSScriptRoot 'launch-worker.mjs'
|
|
24
|
+
$command = '"' + [string]$spec.brokerExecutable + '" "' + $worker + '" "' + $request + '" "' + $ready + '" "' + $instance + '" "' + $diagnostic + '"'
|
|
22
25
|
$broker = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
|
|
23
26
|
CommandLine=$command; CurrentDirectory=$env:USERPROFILE; ProcessStartupInformation=$startup
|
|
24
27
|
}
|
|
25
28
|
if ($broker.ReturnValue -ne 0) { throw ('Windows process broker failed: ' + $broker.ReturnValue) }
|
|
26
|
-
$deadline = [DateTime]::UtcNow.AddSeconds(
|
|
29
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
|
27
30
|
while (-not (Test-Path -LiteralPath $ready)) {
|
|
28
|
-
if ([DateTime]::UtcNow -gt $deadline) {
|
|
31
|
+
if ([DateTime]::UtcNow -gt $deadline) {
|
|
32
|
+
$detail = if (Test-Path -LiteralPath $diagnostic) { (Get-Content -LiteralPath $diagnostic -Raw -Encoding UTF8).Trim() } else { 'broker did not create a diagnostic log' }
|
|
33
|
+
throw ('Windows process broker handoff timed out (broker PID ' + $broker.ProcessId + '): ' + $detail)
|
|
34
|
+
}
|
|
29
35
|
Start-Sleep -Milliseconds 50
|
|
30
36
|
}
|
|
31
37
|
$result = Get-Content -LiteralPath $ready -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
32
38
|
Remove-Item -LiteralPath $ready
|
|
33
39
|
if ($result.instance -ne $instance -or $result.brokerPid -ne $broker.ProcessId) { throw 'Windows process broker identity mismatch.' }
|
|
34
40
|
if ($result.error) { throw [string]$result.error }
|
|
41
|
+
if (Test-Path -LiteralPath $diagnostic) { Remove-Item -LiteralPath $diagnostic -Force }
|
|
35
42
|
[Console]::WriteLine([int]$result.pid)
|
|
36
43
|
} catch {
|
|
37
44
|
[Console]::Error.WriteLine($_.Exception.Message)
|
|
38
45
|
exit 1
|
|
46
|
+
} finally {
|
|
47
|
+
if ($request -and (Test-Path -LiteralPath $request)) { Remove-Item -LiteralPath $request -Force }
|
|
48
|
+
if ($requestTemporary -and (Test-Path -LiteralPath $requestTemporary)) { Remove-Item -LiteralPath $requestTemporary -Force }
|
|
39
49
|
}
|
|
@@ -5,15 +5,18 @@ $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
|
|
|
5
5
|
if (-not $package -or $Executable -ne (Join-Path $package.InstallLocation 'app/ChatGPT.exe')) {
|
|
6
6
|
throw 'Desktop executable identity changed; recovery stopped.'
|
|
7
7
|
}
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
$
|
|
8
|
+
# Never kill another user's Desktop or an app-server process tree. The independent
|
|
9
|
+
# installer/recovery worker must survive and relaunch only in this interactive session.
|
|
10
|
+
$currentSessionId = (Get-Process -Id $PID -ErrorAction Stop).SessionId
|
|
11
|
+
$workers = @(Get-CimInstance Win32_Process -Filter "Name='ChatGPT.exe'" | Where-Object {
|
|
12
|
+
$_.ExecutablePath -eq $Executable -and $_.SessionId -eq $currentSessionId
|
|
13
|
+
})
|
|
11
14
|
foreach ($worker in $workers) {
|
|
12
15
|
$actual = Get-CimInstance Win32_Process -Filter "ProcessId=$($worker.ProcessId)"
|
|
13
16
|
if ($actual -and $actual.ExecutablePath -eq $worker.ExecutablePath -and $actual.CreationDate -eq $worker.CreationDate) {
|
|
14
17
|
Stop-Process -Id $worker.ProcessId -Force -ErrorAction Stop
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
|
-
if (Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $Executable }) {
|
|
20
|
+
if (Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $Executable -and $_.SessionId -eq $currentSessionId }) {
|
|
18
21
|
throw 'Desktop did not exit; recovery stopped.'
|
|
19
22
|
}
|