@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.21

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.
Files changed (120) hide show
  1. package/README.md +55 -71
  2. package/dist/bin/codex-remote.mjs +150 -126
  3. package/dist/bin/manage-relay-admin.mjs +20 -0
  4. package/dist/bin/migrate-relay-db.mjs +107 -0
  5. package/dist/bin/self-relay-admin.mjs +8 -0
  6. package/dist/bin/self-relay-demo.mjs +2 -2
  7. package/dist/bin/self-relay-server.mjs +18 -10
  8. package/dist/shared/app-server-events.cjs +42 -15
  9. package/dist/shared/file-mentions.cjs +35 -0
  10. package/dist/shared/server-requests.cjs +86 -58
  11. package/dist/skills/code-relax/SKILL.md +7 -6
  12. package/dist/src/desktop-launcher.mjs +16 -21
  13. package/dist/src/file-upload-session.mjs +124 -0
  14. package/dist/src/lan-socket-server.mjs +52 -9
  15. package/dist/src/managed-app-server.mjs +44 -13
  16. package/dist/src/message-images.mjs +0 -18
  17. package/dist/src/onboarding.mjs +9 -11
  18. package/dist/src/platform/README.md +1 -1
  19. package/dist/src/platform/windows/background-process.mjs +1 -1
  20. package/dist/src/platform/windows/codex-runtime.mjs +251 -0
  21. package/dist/src/platform/windows/desktop-host.mjs +17 -3
  22. package/dist/src/platform/windows/desktop-info.ps1 +2 -4
  23. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
  24. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
  25. package/dist/src/platform/windows/launch-worker.mjs +42 -0
  26. package/dist/src/platform/windows/resident-start.js +20 -0
  27. package/dist/src/platform/windows/resident-startup.ps1 +76 -20
  28. package/dist/src/platform/windows/service-host.mjs +4 -16
  29. package/dist/src/platform/windows/start-hidden-console.ps1 +10 -5
  30. package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
  31. package/dist/src/platform/windows/user-environment.mjs +1 -1
  32. package/dist/src/runtime-discovery.mjs +43 -0
  33. package/dist/src/self-relay/admin-cloud.mjs +101 -0
  34. package/dist/src/self-relay/admin-page.mjs +79 -0
  35. package/dist/src/self-relay/admin-server.mjs +66 -0
  36. package/dist/src/self-relay/admin-state.mjs +82 -19
  37. package/dist/src/self-relay/admin-style.mjs +9 -0
  38. package/dist/src/self-relay/admin-usage-page.mjs +28 -0
  39. package/dist/src/self-relay/connector.mjs +243 -30
  40. package/dist/src/self-relay/demo.mjs +3 -4
  41. package/dist/src/self-relay/email-accounts.mjs +141 -0
  42. package/dist/src/self-relay/lifecycle.mjs +99 -142
  43. package/dist/src/self-relay/mysql-state.mjs +104 -0
  44. package/dist/src/self-relay/p2p-probe.mjs +1 -1
  45. package/dist/src/self-relay/server.mjs +335 -59
  46. package/dist/src/self-relay/usage-diagnostics.mjs +46 -0
  47. package/dist/src/self-relay/usage-meter.mjs +125 -0
  48. package/dist/src/server.mjs +731 -1439
  49. package/dist/src/service-control.mjs +4 -4
  50. package/dist/src/service-doctor.mjs +27 -29
  51. package/dist/src/service-health.mjs +2 -2
  52. package/dist/src/service-install.mjs +1 -4
  53. package/dist/src/service-lifecycle.mjs +19 -19
  54. package/dist/src/shared-app-server.mjs +277 -270
  55. package/dist/src/shared-catalog.mjs +1 -11
  56. package/dist/src/shared-recovery.mjs +11 -9
  57. package/dist/src/shared-thread-stream.mjs +1 -1
  58. package/dist/src/skill-install.mjs +0 -13
  59. package/package.json +17 -18
  60. package/tools/postinstall.mjs +69 -15
  61. package/dist/src/app-server-client.mjs +0 -468
  62. package/dist/src/app-server-tasks.mjs +0 -358
  63. package/dist/src/debug-ui.mjs +0 -131
  64. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  65. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  66. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  67. package/dist/src/platform/windows/launch-worker.ps1 +0 -20
  68. package/dist/src/thread-catalog.mjs +0 -47
  69. package/dist/tools/find-desktop-pipe.mjs +0 -10
  70. package/dist/web/activity-view.js +0 -283
  71. package/dist/web/approval-ui.js +0 -188
  72. package/dist/web/capabilities.js +0 -57
  73. package/dist/web/chat-transport.js +0 -202
  74. package/dist/web/chat.css +0 -782
  75. package/dist/web/chat.js +0 -3830
  76. package/dist/web/client-platform.js +0 -11
  77. package/dist/web/community-view.js +0 -20
  78. package/dist/web/community.css +0 -77
  79. package/dist/web/community.html +0 -37
  80. package/dist/web/composer-controller.js +0 -101
  81. package/dist/web/conversation-controller.js +0 -99
  82. package/dist/web/disclosure-state-controller.js +0 -95
  83. package/dist/web/draft-controller.js +0 -103
  84. package/dist/web/harmony-platform.js +0 -52
  85. package/dist/web/highlight-powershell.min.js +0 -39
  86. package/dist/web/highlight.min.js +0 -1262
  87. package/dist/web/history-cache.js +0 -240
  88. package/dist/web/history-controller.js +0 -167
  89. package/dist/web/index.html +0 -325
  90. package/dist/web/lan-session.js +0 -122
  91. package/dist/web/link-action-controller.js +0 -212
  92. package/dist/web/message-send-controller.js +0 -177
  93. package/dist/web/message-view.js +0 -98
  94. package/dist/web/p2p-data-channel.js +0 -50
  95. package/dist/web/p2p-diagnostics.js +0 -28
  96. package/dist/web/p2p-probe.js +0 -123
  97. package/dist/web/page-resume.js +0 -28
  98. package/dist/web/pending-message-store.js +0 -108
  99. package/dist/web/queue-controller.js +0 -82
  100. package/dist/web/resources.json +0 -1
  101. package/dist/web/self-relay-session.js +0 -129
  102. package/dist/web/server-requests.js +0 -67
  103. package/dist/web/station-connection-controller.js +0 -75
  104. package/dist/web/task-list-view.js +0 -296
  105. package/dist/web/thread-attention-controller.js +0 -124
  106. package/dist/web/thread-context-controller.js +0 -30
  107. package/dist/web/thread-list-controller.js +0 -61
  108. package/dist/web/thread-list-sync.js +0 -86
  109. package/dist/web/thread-title-controller.js +0 -57
  110. package/dist/web/timeline-formatters.js +0 -249
  111. package/dist/web/timeline-reducer.js +0 -81
  112. package/dist/web/timeline-renderer.js +0 -161
  113. package/dist/web/timeline-scroll-controller.js +0 -50
  114. package/dist/web/timeline-sync.js +0 -67
  115. package/dist/web/usage-controller.js +0 -258
  116. package/dist/web/vendor/README.md +0 -14
  117. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  118. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  119. package/dist/web/vendor/highlight.min.js +0 -1262
  120. package/dist/web/vendor/lucide.LICENSE.txt +0 -17
@@ -0,0 +1,251 @@
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 { createHash, randomUUID } from 'node:crypto';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { promisify } from 'node:util';
9
+ import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
10
+
11
+ const exec = promisify(execFile);
12
+ const requiredMethods = ['thread/start', 'thread/resume', 'thread/list', 'thread/turns/list',
13
+ 'thread/items/list', 'turn/start', 'turn/steer', 'turn/interrupt', 'model/list', 'project/list',
14
+ 'thread/settings/update', 'configRequirements/read', 'thread/queue/list', 'thread/queue/add',
15
+ 'thread/queue/delete', 'thread/queue/start', 'thread/read', 'thread/unsubscribe', 'config/read', 'account/rateLimits/read'];
16
+ export const codeModeHostPath = executable => path.join(path.dirname(executable), 'codex-code-mode-host.exe');
17
+ export const runtimeFilesAvailable = executable => fs.existsSync(executable) && fs.existsSync(codeModeHostPath(executable));
18
+ const fingerprint = executable => [executable, codeModeHostPath(executable)]
19
+ .map(file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; }).join('|');
20
+ const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
21
+ const desktopRuntimeScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'desktop-runtime.ps1');
22
+
23
+ async function fileHash(file) {
24
+ const hash = createHash('sha256');
25
+ for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
26
+ return hash.digest('hex');
27
+ }
28
+
29
+ async function installedDesktopRuntime() {
30
+ const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
31
+ '-File', desktopRuntimeScript], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
32
+ return JSON.parse(stdout.trim());
33
+ }
34
+
35
+ async function matchesInstalledDesktop(executable, installed) {
36
+ if (!installed || !runtimeFilesAvailable(executable) || !runtimeFilesAvailable(installed.executable)) return false;
37
+ const files = [[executable, installed.executable], [codeModeHostPath(executable), installed.codeModeHost]];
38
+ for (const [candidate, bundled] of files) {
39
+ if (fs.statSync(candidate).size !== fs.statSync(bundled).size || await fileHash(candidate) !== await fileHash(bundled)) return false;
40
+ }
41
+ return true;
42
+ }
43
+
44
+ function runtimeFiles(directory, relative = '') {
45
+ const files = [];
46
+ for (const entry of fs.readdirSync(path.join(directory, relative), { withFileTypes: true })) {
47
+ const name = path.join(relative, entry.name);
48
+ if (entry.isDirectory()) files.push(...runtimeFiles(directory, name));
49
+ else if (entry.isFile()) files.push(name);
50
+ else throw new Error(`Codex 内核目录包含不支持的文件类型:${name}`);
51
+ }
52
+ return files.sort();
53
+ }
54
+
55
+ function runtimeDirectories(files) {
56
+ const directories = new Set();
57
+ for (const file of files) {
58
+ let directory = path.dirname(file);
59
+ while (directory !== '.') { directories.add(directory); directory = path.dirname(directory); }
60
+ }
61
+ return [...directories].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
62
+ }
63
+
64
+ async function retainDesktopRuntime(executable, installed, stateRoot) {
65
+ if (!/^[\w.-]+$/.test(installed.package)) throw new Error('Codex Desktop 安装包名称无效。');
66
+ const source = path.dirname(executable);
67
+ const files = runtimeFiles(source);
68
+ if (files.includes('runtime.json')) throw new Error('Codex 内核目录与保留清单文件名冲突。');
69
+ const directories = runtimeDirectories(files);
70
+ const root = path.join(stateRoot, 'desktop-runtimes');
71
+ const target = path.join(root, installed.package);
72
+ const retained = path.join(target, 'codex.exe');
73
+ if (fs.existsSync(target)) {
74
+ const manifest = readRecord(path.join(target, 'runtime.json'));
75
+ if (manifest?.package !== installed.package || JSON.stringify(manifest.files) !== JSON.stringify(files)
76
+ || JSON.stringify(runtimeFiles(target)) !== JSON.stringify([...files, 'runtime.json'].sort())
77
+ || !await matchesInstalledDesktop(retained, installed)) {
78
+ throw new Error(`已保留的 Codex 内核文件不完整:${target}`);
79
+ }
80
+ return retained;
81
+ }
82
+ fs.mkdirSync(root, { recursive: true });
83
+ const staging = path.join(root, `.${installed.package}.${randomUUID()}.tmp`);
84
+ fs.mkdirSync(staging);
85
+ const created = [];
86
+ try {
87
+ for (const directory of directories) fs.mkdirSync(path.join(staging, directory));
88
+ for (const name of files) {
89
+ const from = path.join(source, name), to = path.join(staging, name);
90
+ created.push(to);
91
+ try { fs.linkSync(from, to); }
92
+ catch (error) {
93
+ if (error.code !== 'EXDEV') throw error;
94
+ fs.copyFileSync(from, to);
95
+ }
96
+ }
97
+ if (!await matchesInstalledDesktop(path.join(staging, 'codex.exe'), installed)) {
98
+ throw new Error('保留的 Codex 内核与当前安装包不一致。');
99
+ }
100
+ const manifestPath = path.join(staging, 'runtime.json');
101
+ writeRecord(manifestPath, { package: installed.package, files });
102
+ created.push(manifestPath);
103
+ fs.renameSync(staging, target);
104
+ return retained;
105
+ } catch (error) {
106
+ for (const file of created) fs.rmSync(file, { force: true });
107
+ for (const directory of directories.reverse()) {
108
+ const nested = path.join(staging, directory);
109
+ if (fs.existsSync(nested)) fs.rmdirSync(nested);
110
+ }
111
+ fs.rmdirSync(staging);
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ export function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
117
+ const root = path.join(stateRoot, 'desktop-runtimes');
118
+ if (!fs.existsSync(root)) return;
119
+ const keep = path.resolve(path.dirname(keepExecutable)).toLowerCase();
120
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
121
+ if (!entry.isDirectory() || !/^OpenAI\.Codex_[\w.-]+$/.test(entry.name)) continue;
122
+ const directory = path.join(root, entry.name);
123
+ if (path.resolve(directory).toLowerCase() === keep || fs.lstatSync(directory).isSymbolicLink()) continue;
124
+ const manifest = readRecord(path.join(directory, 'runtime.json'));
125
+ const files = manifest?.files;
126
+ if (manifest?.package !== entry.name || !Array.isArray(files) || !files.includes('codex.exe')
127
+ || !files.includes('codex-code-mode-host.exe') || new Set(files).size !== files.length
128
+ || !files.every(name => typeof name === 'string' && name !== 'runtime.json'
129
+ && path.resolve(directory, name).toLowerCase().startsWith(`${path.resolve(directory).toLowerCase()}${path.sep}`))) continue;
130
+ const expected = [...files, 'runtime.json'].sort();
131
+ if (JSON.stringify(runtimeFiles(directory)) !== JSON.stringify(expected)) continue;
132
+ if (!fs.lstatSync(path.join(directory, 'runtime.json')).isFile()
133
+ || !files.every(name => fs.lstatSync(path.join(directory, name)).isFile())) continue;
134
+ for (const name of files) fs.unlinkSync(path.join(directory, name));
135
+ fs.unlinkSync(path.join(directory, 'runtime.json'));
136
+ for (const name of runtimeDirectories(files).reverse()) fs.rmdirSync(path.join(directory, name));
137
+ fs.rmdirSync(directory);
138
+ }
139
+ }
140
+
141
+ // Resolve native binaries, never execute a shell shim from a background task.
142
+ export function discoverCodex(environment = process.env) {
143
+ const found = new Map();
144
+ const add = (file, source) => {
145
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
146
+ const executable = fs.realpathSync(file);
147
+ if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
148
+ };
149
+ if (environment.CODE_RELAX_CODEX_PATH) {
150
+ const file = environment.CODE_RELAX_CODEX_PATH;
151
+ if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
152
+ add(file, 'explicit');
153
+ if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
154
+ return [...found.values()];
155
+ }
156
+ const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
157
+ const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
158
+ const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
159
+ environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
160
+ path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
161
+ for (const directory of directories) {
162
+ add(path.join(directory, 'codex.exe'), 'cli');
163
+ const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
164
+ if (!fs.existsSync(packageFile)) continue;
165
+ const root = path.dirname(fs.realpathSync(packageFile));
166
+ let vendor = path.join(root, 'vendor');
167
+ try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
168
+ catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
169
+ add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
170
+ }
171
+ const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
172
+ add(path.join(desktopRoot, 'codex.exe'), 'desktop');
173
+ if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
174
+ if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
175
+ }
176
+ return [...found.values()];
177
+ }
178
+
179
+ function compareVersions(a, b) {
180
+ const parse = v => v.split(/[.-]/).map(part => /^\d+$/.test(part) ? Number(part) : part);
181
+ const aa = parse(a), bb = parse(b);
182
+ for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
183
+ if (aa[i] === bb[i]) continue;
184
+ if (aa[i] === undefined) return 1;
185
+ if (bb[i] === undefined) return -1;
186
+ return typeof aa[i] === 'number' && typeof bb[i] === 'number' ? aa[i] - bb[i] : String(aa[i]).localeCompare(String(bb[i]));
187
+ }
188
+ return 0;
189
+ }
190
+
191
+ async function verifyProtocol(executable, stateRoot) {
192
+ const directory = fs.mkdtempSync(path.join(stateRoot, 'cli-schema-'));
193
+ try {
194
+ await exec(executable, ['app-server', 'generate-json-schema', '--experimental', '--out', directory],
195
+ { windowsHide: true, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
196
+ const request = fs.readFileSync(path.join(directory, 'ClientRequest.json'), 'utf8');
197
+ const missing = requiredMethods.filter(method => !request.includes(`"${method}"`));
198
+ const turns = fs.readFileSync(path.join(directory, 'v2/ThreadTurnsListParams.json'), 'utf8');
199
+ if (!turns.includes('"itemsView"') || !turns.includes('"summary"')) missing.push('turn summary pagination');
200
+ if (missing.length) throw new Error(`缺少协议能力:${missing.join(', ')}`);
201
+ } finally {
202
+ const remove = dir => {
203
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
204
+ const file = path.join(dir, item.name);
205
+ if (item.isDirectory()) remove(file); else fs.unlinkSync(file);
206
+ }
207
+ fs.rmdirSync(dir);
208
+ };
209
+ remove(directory);
210
+ }
211
+ }
212
+
213
+ export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
214
+ fs.mkdirSync(stateRoot, { recursive: true });
215
+ const recordFile = path.join(stateRoot, 'codex-runtime.json');
216
+ const saved = readRecord(recordFile);
217
+ environment = { ...environment,
218
+ CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
219
+ CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
220
+ if (!refresh && saved?.executable && runtimeFilesAvailable(saved.executable)
221
+ && saved.fingerprint === fingerprint(saved.executable)) return saved;
222
+ const candidates = [], rejected = [];
223
+ let installed = null;
224
+ try { installed = await installedDesktopRuntime(); }
225
+ catch (error) { rejected.push(`无法读取当前 Codex Desktop 安装包:${error.message}`); }
226
+ for (const candidate of discoverCodex(environment)) {
227
+ try {
228
+ if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少同目录的 codex-code-mode-host.exe');
229
+ if (candidate.source === 'desktop' && !await matchesInstalledDesktop(candidate.executable, installed)) {
230
+ throw new Error('不是当前 Codex Desktop 安装包配套的 CLI');
231
+ }
232
+ const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
233
+ const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
234
+ if (!version) throw new Error('无法识别版本');
235
+ candidates.push({ ...candidate, version });
236
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
237
+ }
238
+ candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
239
+ for (const candidate of candidates) {
240
+ try {
241
+ const executable = candidate.source === 'desktop'
242
+ ? await retainDesktopRuntime(candidate.executable, installed, stateRoot) : candidate.executable;
243
+ await verifyProtocol(executable, stateRoot);
244
+ const result = { ...candidate, executable, fingerprint: fingerprint(executable),
245
+ codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
246
+ writeRecord(recordFile, result);
247
+ return result;
248
+ } catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
249
+ }
250
+ throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
251
+ }
@@ -17,15 +17,29 @@ 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]);
24
39
 
25
- export async function desktopShortcuts({ serviceRoot, nodeExecutable }, remove = false, destination) {
40
+ export async function removeDesktopShortcut({ serviceRoot, nodeExecutable }, destination) {
26
41
  const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
27
42
  const args = ['-NodePath', nodeExecutable, '-Entry', entry];
28
- if (remove) args.push('-Remove');
29
43
  if (destination) args.push('-Destination', destination);
30
44
  return JSON.parse(await runScript('desktop-shortcuts.ps1', args));
31
45
  }
@@ -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) { throw '未找到 Codex Desktop,请先安装并登录。' }
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
- $binaryRoot = Join-Path $env:LOCALAPPDATA 'OpenAI/Codex/bin'
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
@@ -0,0 +1,10 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
3
+ $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
4
+ if (-not $package) { 'null'; exit }
5
+ $resources = Join-Path $package.InstallLocation 'app\resources'
6
+ @{
7
+ package = $package.PackageFullName
8
+ executable = Join-Path $resources 'codex.exe'
9
+ codeModeHost = Join-Path $resources 'codex-code-mode-host.exe'
10
+ } | ConvertTo-Json -Compress
@@ -1,28 +1,21 @@
1
- param([string]$NodePath, [string]$Entry, [switch]$Remove, [string]$Destination)
1
+ param([string]$NodePath, [string]$Entry, [string]$Destination)
2
2
  $ErrorActionPreference = 'Stop'
3
3
  [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
4
  $shell = New-Object -ComObject WScript.Shell
5
5
  if (-not $Destination) { $Destination = $shell.SpecialFolders.Item('Desktop') }
6
- $launcher = Join-Path $PSScriptRoot 'desktop-shortcut-run.ps1'
7
- $items = @(@{Name='Code Relax - 完整控制'; Command='shared-start'}, @{Name='Code Relax - 基础控制'; Command='desktop-start'})
6
+ # Only identify and remove the retired shortcut; never create a new launcher.
7
+ $launcher = Join-Path $PSScriptRoot 'desktop-shortcut-run.ps1'
8
+ $items = @(@{Name='Code Relax - Codex 内核'; Command='shared-start'})
8
9
  $results = @()
9
10
  foreach ($item in $items) {
10
11
  $target = Join-Path $Destination ($item.Name + '.lnk')
11
12
  $arguments = '-NoProfile -NoExit -ExecutionPolicy Bypass -File "' + $launcher + '" -NodePath "' + $NodePath + '" -Entry "' + $Entry + '" -Action ' + $item.Command
12
13
  if (Test-Path -LiteralPath $target) {
13
14
  $existing = $shell.CreateShortcut($target)
14
- if ($existing.Arguments -ne $arguments) { throw ('快捷方式不是本安装创建的,未覆盖或删除:' + $target) }
15
- }
16
- if ($Remove) {
17
- if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target }
18
- } else {
19
- $link = $shell.CreateShortcut($target)
20
- $link.TargetPath = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
21
- $link.Arguments = $arguments
22
- $link.WorkingDirectory = Split-Path $Entry -Parent
23
- $link.Description = $item.Name
24
- $link.Save()
15
+ $powershell = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
16
+ if ($existing.Arguments -ne $arguments -or $existing.TargetPath -ne $powershell) { continue }
25
17
  }
18
+ if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target }
26
19
  $results += $target
27
20
  }
28
21
  ConvertTo-Json -InputObject $results -Compress
@@ -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);
@@ -0,0 +1,20 @@
1
+ // Windows Script Host has no console. Keep the login task alive until the
2
+ // short-lived CLI has handed the resident service to the background broker.
3
+ if (WScript.Arguments.length !== 2) WScript.Quit(2);
4
+ var shell = new ActiveXObject('WScript.Shell');
5
+ var files = new ActiveXObject('Scripting.FileSystemObject');
6
+ var node = WScript.Arguments.Item(0);
7
+ var entry = WScript.Arguments.Item(1);
8
+ if (!files.FileExists(node)) {
9
+ var directories = shell.Environment('PROCESS').Item('PATH').split(';');
10
+ node = '';
11
+ for (var i = 0; i < directories.length; i++) {
12
+ var directory = directories[i].replace(/^"|"$/g, '');
13
+ if (!directory) continue;
14
+ var candidate = files.BuildPath(directory, 'node.exe');
15
+ if (files.FileExists(candidate)) { node = candidate; break; }
16
+ }
17
+ if (!node) WScript.Quit(2);
18
+ }
19
+ var command = '"' + node + '" "' + entry + '" start --json';
20
+ WScript.Quit(shell.Run(command, 0, true));
@@ -1,27 +1,83 @@
1
- param([string]$NodePath, [string]$Entry, [ValidateSet('enable','disable','status')][string]$Action='status')
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 桌面服务'
6
+ $legacyTaskName = 'Code Relax Bridge'
7
+ $taskSource = '@walkhi/code-relax'
8
+ $taskPath = '\' + $taskName
9
+ $launcher = Join-Path $PSScriptRoot 'resident-start.js'
10
+ $wscript = Join-Path $env:WINDIR 'System32\wscript.exe'
11
+ $arguments = '//B //Nologo "' + $launcher + '" "' + $NodePath + '" "' + $Entry + '"'
12
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
13
+
14
+ $scheduler = New-Object -ComObject 'Schedule.Service'
15
+ $scheduler.Connect()
16
+ $root = $scheduler.GetFolder('\')
17
+ $registered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
18
+ $legacyRegistered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $legacyTaskName } | Select-Object -First 1)
19
+ if ($registered.Count -gt 0 -and $registered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
20
+ throw 'Scheduled task is not owned by this installation.'
21
+ }
22
+ if ($legacyRegistered.Count -gt 0 -and $legacyRegistered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
23
+ throw 'Legacy scheduled task is not owned by this installation.'
24
+ }
25
+
26
+ # Remove only the legacy shortcut created by this package. A Startup LNK that
27
+ # launches hidden PowerShell with ExecutionPolicy Bypass triggers AV heuristics.
4
28
  $shell = New-Object -ComObject WScript.Shell
5
- $target = Join-Path $shell.SpecialFolders.Item('Startup') 'Code Relax Bridge.lnk'
6
- $launcher = Join-Path $PSScriptRoot 'resident-start.ps1'
7
- $powershell = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
8
- $exists = Test-Path -LiteralPath $target
9
- if ($exists) {
10
- $existing = $shell.CreateShortcut($target)
11
- if ($existing.TargetPath -ne $powershell -or -not $existing.Arguments.Contains('"' + $launcher + '"')) {
12
- throw 'Startup shortcut is not owned by this installation.'
29
+ $legacyPath = Join-Path $shell.SpecialFolders.Item('Startup') 'Code Relax Bridge.lnk'
30
+ $legacyLauncher = Join-Path $PSScriptRoot 'resident-start.ps1'
31
+ $legacyExists = Test-Path -LiteralPath $legacyPath
32
+ if ($Action -ne 'status' -and $legacyExists) {
33
+ $legacy = $shell.CreateShortcut($legacyPath)
34
+ if (-not $legacy.Arguments.Contains('"' + $legacyLauncher + '"')) {
35
+ throw 'Legacy startup shortcut is not owned by this installation.'
13
36
  }
14
37
  }
38
+
15
39
  if ($Action -eq 'enable') {
16
- if (-not (Test-Path -LiteralPath $NodePath) -or -not (Test-Path -LiteralPath $Entry)) { throw 'Installed runtime is missing.' }
17
- $link = $shell.CreateShortcut($target)
18
- $link.TargetPath = $powershell
19
- $link.Arguments = '-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + $launcher + '" -NodePath "' + $NodePath + '" -Entry "' + $Entry + '"'
20
- $link.WorkingDirectory = $env:USERPROFILE
21
- $link.WindowStyle = 7
22
- $link.Description = 'Code Relax resident computer connection'
23
- $link.Save()
24
- } elseif ($Action -eq 'disable' -and $exists) {
25
- Remove-Item -LiteralPath $target
40
+ if (-not (Test-Path -LiteralPath $NodePath) -or -not (Test-Path -LiteralPath $Entry) -or
41
+ -not (Test-Path -LiteralPath $launcher) -or -not (Test-Path -LiteralPath $wscript)) { throw 'Installed runtime is missing.' }
42
+ $definition = $scheduler.NewTask(0)
43
+ $definition.RegistrationInfo.Description = 'Start the Code Relax desktop service when this user signs in.'
44
+ $definition.RegistrationInfo.Source = $taskSource
45
+ $definition.Principal.UserId = $currentUser
46
+ $definition.Principal.LogonType = 3 # TASK_LOGON_INTERACTIVE_TOKEN
47
+ $definition.Principal.RunLevel = 0 # TASK_RUNLEVEL_LUA
48
+ $definition.Settings.Enabled = $true
49
+ $definition.Settings.Hidden = $true
50
+ $definition.Settings.StartWhenAvailable = $true
51
+ $definition.Settings.DisallowStartIfOnBatteries = $false
52
+ $definition.Settings.StopIfGoingOnBatteries = $false
53
+ $definition.Settings.ExecutionTimeLimit = 'PT5M'
54
+ $definition.Settings.MultipleInstances = 2 # TASK_INSTANCES_IGNORE_NEW
55
+ $trigger = $definition.Triggers.Create(9) # TASK_TRIGGER_LOGON
56
+ $trigger.UserId = $currentUser
57
+ $trigger.Enabled = $true
58
+ $exec = $definition.Actions.Create(0) # TASK_ACTION_EXEC
59
+ $exec.Path = $wscript
60
+ $exec.Arguments = $arguments
61
+ $exec.WorkingDirectory = $env:USERPROFILE
62
+ [void]$root.RegisterTaskDefinition($taskName, $definition, 6, $null, $null, 3, $null) # TASK_CREATE_OR_UPDATE
63
+ if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
64
+ } elseif ($Action -eq 'disable') {
65
+ if ($registered.Count -gt 0) { $root.DeleteTask($taskName, 0) }
66
+ if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
67
+ }
68
+ if ($Action -ne 'status' -and $legacyExists) { Remove-Item -LiteralPath $legacyPath }
69
+
70
+ $active = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
71
+ $taskEnabled = $active.Count -gt 0 -and $active[0].Enabled
72
+ $taskCurrent = $false
73
+ if ($taskEnabled) {
74
+ $taskAction = $active[0].Definition.Actions.Item(1)
75
+ $taskCurrent = $taskAction.Path -ieq $wscript -and $taskAction.Arguments -ieq $arguments
26
76
  }
27
- @{ enabled=(Test-Path -LiteralPath $target); path=$target; scope='current-user-logon' } | ConvertTo-Json -Compress
77
+ @{
78
+ enabled=($taskCurrent -and $legacyRegistered.Count -eq 0)
79
+ taskPresent=$taskEnabled
80
+ path=$(if ($taskEnabled) { 'Task Scheduler:' + $taskPath } elseif ($legacyRegistered.Count -gt 0) { 'Task Scheduler:\' + $legacyTaskName } elseif ($legacyExists) { $legacyPath } else { 'Task Scheduler:' + $taskPath })
81
+ scope='current-user-logon'
82
+ mechanism=$(if ($taskCurrent -and $legacyRegistered.Count -eq 0) { 'task-scheduler' } elseif ($taskEnabled -or $legacyRegistered.Count -gt 0) { 'stale-task-scheduler' } elseif ($legacyExists) { 'legacy-startup-shortcut' } else { 'task-scheduler' })
83
+ } | ConvertTo-Json -Compress
@@ -20,24 +20,12 @@ async function detect(script) {
20
20
  export const sharedServiceUrl = () => detect('get-shared-service-url.ps1');
21
21
  export const lanAddress = async () => (await lanNetworks())[0]?.address || '';
22
22
  export { lanNetworks };
23
- export function migrateState(stateRoot) {
24
- fs.mkdirSync(stateRoot, { recursive: true });
25
- for (const name of ['bridge-config.json', 'bridge-session.json']) {
26
- const target = path.join(stateRoot, name);
27
- const old = path.join(bridgeRoot, '../.codex/bridge', name);
28
- if (!fs.existsSync(target) && fs.existsSync(old)) fs.copyFileSync(old, target, fs.constants.COPYFILE_EXCL);
29
- }
30
- }
31
- export async function desktopEnvironment(context, sharedUrl) {
32
- const environment = { ...process.env };
33
- environment.CODEX_THREAD_ID ||= environment.CODEX_SESSION_ID || context?.sourceThreadId;
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'));
23
+ export async function bridgeEnvironment() {
24
+ const environment = { ...process.env };
25
+ const installation = readRecord(path.join(runtimePaths(environment).serviceRoot, 'installation.json'));
37
26
  const executable = installation?.nodeExecutable;
38
27
  if (!executable || !fs.existsSync(executable)) throw new Error('安装时选用的 Node 不存在,请重新安装 Code Relax。');
39
28
  environment.CODEX_MCP_NODE_PATH = executable;
40
- environment.CODEX_REMOTE_LEGACY_UPLOADS_ROOTS = JSON.stringify(installation.legacyUploadsRoots || []);
41
- if (sharedUrl) delete environment.CODEX_APP_TOOLS_PIPE_PATH;
29
+ delete environment.CODEX_APP_TOOLS_PIPE_PATH;
42
30
  return { executable, environment };
43
31
  }
@@ -11,29 +11,34 @@ try {
11
11
  $launchRoot = [IsolatedProcess]::ResolvePath($launchRoot)
12
12
  $ready = Join-Path $launchRoot ($instance + '.json')
13
13
  $request = Join-Path $launchRoot ($instance + '.request.json')
14
+ $diagnostic = Join-Path $launchRoot ($instance + '.broker.log')
14
15
  $spec | Add-Member -NotePropertyName instance -NotePropertyValue $instance
16
+ $spec | Add-Member -NotePropertyName diagnostic -NotePropertyValue $diagnostic
15
17
  $requestTemporary = $request + '.tmp'
16
18
  [IO.File]::WriteAllText($requestTemporary, ($spec | ConvertTo-Json -Compress -Depth 10), (New-Object Text.UTF8Encoding($false)))
17
19
  [IO.File]::Move($requestTemporary, $request)
18
- $environment = @(Get-ChildItem Env: | ForEach-Object { $_.Name + '=' + $_.Value })
19
20
  $startup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{
20
- ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010; EnvironmentVariables=[string[]]$environment
21
+ ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
21
22
  }
22
- $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
23
- $command = '"' + $powershell + '" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + (Join-Path $PSScriptRoot 'launch-worker.ps1') + '" -RequestPath "' + $request + '" -ReadyPath "' + $ready + '" -Instance "' + $instance + '"'
23
+ $worker = Join-Path $PSScriptRoot 'launch-worker.mjs'
24
+ $command = '"' + [string]$spec.brokerExecutable + '" "' + $worker + '" "' + $request + '" "' + $ready + '" "' + $instance + '" "' + $diagnostic + '"'
24
25
  $broker = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
25
26
  CommandLine=$command; CurrentDirectory=$env:USERPROFILE; ProcessStartupInformation=$startup
26
27
  }
27
28
  if ($broker.ReturnValue -ne 0) { throw ('Windows process broker failed: ' + $broker.ReturnValue) }
28
29
  $deadline = [DateTime]::UtcNow.AddSeconds(30)
29
30
  while (-not (Test-Path -LiteralPath $ready)) {
30
- if ([DateTime]::UtcNow -gt $deadline) { throw ('Windows process broker handoff timed out (broker PID ' + $broker.ProcessId + ').') }
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
+ }
31
35
  Start-Sleep -Milliseconds 50
32
36
  }
33
37
  $result = Get-Content -LiteralPath $ready -Raw -Encoding UTF8 | ConvertFrom-Json
34
38
  Remove-Item -LiteralPath $ready
35
39
  if ($result.instance -ne $instance -or $result.brokerPid -ne $broker.ProcessId) { throw 'Windows process broker identity mismatch.' }
36
40
  if ($result.error) { throw [string]$result.error }
41
+ if (Test-Path -LiteralPath $diagnostic) { Remove-Item -LiteralPath $diagnostic -Force }
37
42
  [Console]::WriteLine([int]$result.pid)
38
43
  } catch {
39
44
  [Console]::Error.WriteLine($_.Exception.Message)
@@ -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
- # This action is only reached after explicit whole-workstation recovery confirmation.
9
- # Never kill an app-server process tree: the independent recovery worker must survive.
10
- $workers = @(Get-CimInstance Win32_Process -Filter "Name='ChatGPT.exe'" | Where-Object { $_.ExecutablePath -eq $Executable })
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
  }
@@ -15,7 +15,7 @@ export async function configureManagedEnvironment(stateRoot) {
15
15
  const ownershipPath = path.join(stateRoot, ownershipFile);
16
16
  const ownership = readRecord(ownershipPath);
17
17
  if (current.forceCliUser === '1' || current.forceCliMachine === '1') {
18
- throw new Error('CODEX_APP_SERVER_FORCE_CLI=1 会让 Codex 忽略受管 app-server;未修改该配置。');
18
+ throw new Error('CODEX_APP_SERVER_FORCE_CLI=1 会让 Codex 忽略当前内核地址;未修改该配置。');
19
19
  }
20
20
  if (current.userValue && current.userValue !== managedAppServerUrl) {
21
21
  throw new Error(`用户已有 CODEX_APP_SERVER_WS_URL=${current.userValue};未覆盖该配置。`);