@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.31
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 +95 -89
- package/dist/bin/cleanup-accounts.mjs +63 -63
- package/dist/bin/codex-remote.mjs +374 -367
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-android-release.mjs +62 -58
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/get-shared-service-url.ps1 +6 -6
- package/dist/shared/app-server-events.cjs +422 -412
- package/dist/shared/code-browser.cjs +159 -159
- package/dist/shared/file-changes.cjs +31 -31
- package/dist/shared/file-citations.cjs +117 -117
- package/dist/shared/file-mentions.cjs +92 -92
- package/dist/shared/message-delivery.cjs +54 -54
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-file-channel.cjs +74 -74
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +40 -0
- package/dist/shared/server-requests.cjs +219 -219
- package/dist/shared/timeline-sync.cjs +173 -173
- package/dist/skills/code-relax/SKILL.md +22 -22
- package/dist/src/archived-threads.mjs +22 -0
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/bounded-process-log.mjs +63 -63
- package/dist/src/celsius-client.mjs +107 -107
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +46 -46
- package/dist/src/dots-approval-timeline.mjs +99 -99
- package/dist/src/dots-avatar.mjs +42 -42
- package/dist/src/dots-chat-projection.mjs +50 -50
- package/dist/src/dots-chat-server.mjs +461 -461
- package/dist/src/dots-message-images.mjs +319 -319
- package/dist/src/dots-profile.mjs +77 -77
- package/dist/src/dots-room-activity.mjs +94 -94
- package/dist/src/dots-room-client.mjs +135 -135
- package/dist/src/dots-room-projection.mjs +94 -94
- package/dist/src/dots-schedules.mjs +66 -66
- package/dist/src/dots-summary-tasks.mjs +32 -32
- package/dist/src/dots-task-catalog.mjs +430 -430
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/file-transfer-range.mjs +22 -22
- package/dist/src/file-upload-session.mjs +130 -130
- package/dist/src/hosted-history-app-server.mjs +103 -103
- package/dist/src/hosted-history-cache.mjs +113 -113
- package/dist/src/lan-socket-server.mjs +265 -263
- package/dist/src/managed-app-server.mjs +244 -244
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/message-upload-files.mjs +74 -74
- package/dist/src/onboarding.mjs +12 -12
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/README.md +12 -12
- package/dist/src/platform/codex-protocol.mjs +46 -46
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +3 -3
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/desktop-project-store.mjs +208 -149
- package/dist/src/platform/file-opener.mjs +9 -0
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -0
- package/dist/src/platform/linux/credential-store.mjs +28 -0
- package/dist/src/platform/linux/desktop-host.mjs +83 -0
- package/dist/src/platform/linux/desktop-installation.mjs +36 -0
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
- package/dist/src/platform/linux/network-info.mjs +11 -0
- package/dist/src/platform/linux/process.mjs +42 -0
- package/dist/src/platform/linux/project-locations.mjs +17 -0
- package/dist/src/platform/linux/resident-startup.mjs +52 -0
- package/dist/src/platform/linux/retained-runtime.mjs +76 -0
- package/dist/src/platform/linux/service-host.mjs +18 -0
- package/dist/src/platform/linux/system.mjs +49 -0
- package/dist/src/platform/linux/user-environment.mjs +7 -0
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/codex-runtime.mjs +125 -125
- package/dist/src/platform/macos/credential-store.mjs +10 -10
- package/dist/src/platform/macos/desktop-host.mjs +82 -82
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
- package/dist/src/platform/macos/network-info.mjs +8 -28
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/macos/user-environment.mjs +7 -7
- package/dist/src/platform/native-package.mjs +58 -46
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -0
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/codex-runtime.mjs +215 -215
- package/dist/src/platform/windows/credential-store.mjs +27 -27
- package/dist/src/platform/windows/credential-store.ps1 +49 -49
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-host.mjs +58 -58
- package/dist/src/platform/windows/desktop-info.ps1 +14 -14
- package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
- package/dist/src/platform/windows/dots-profile.mjs +49 -49
- package/dist/src/platform/windows/launch-worker.mjs +41 -41
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/project-locations.mjs +16 -16
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/service-host.mjs +31 -31
- package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.mjs +50 -50
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-archive.mjs +30 -0
- package/dist/src/project-create.mjs +121 -119
- package/dist/src/project-management.mjs +59 -69
- package/dist/src/project-remove.mjs +55 -0
- package/dist/src/project-update.mjs +56 -0
- package/dist/src/runtime-discovery.mjs +47 -47
- package/dist/src/self-relay/account-cleanup.mjs +30 -30
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-server.mjs +74 -74
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-style.mjs +14 -14
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connector.mjs +439 -438
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +37 -37
- package/dist/src/self-relay/device-info.mjs +27 -0
- package/dist/src/self-relay/device-store.mjs +92 -84
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +183 -183
- package/dist/src/self-relay/file-wire.mjs +13 -13
- package/dist/src/self-relay/lan-authorizations.mjs +71 -42
- package/dist/src/self-relay/lifecycle.mjs +264 -250
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -105
- package/dist/src/self-relay/p2p-probe.mjs +159 -159
- package/dist/src/self-relay/privacy-page.mjs +26 -26
- package/dist/src/self-relay/public-releases.mjs +34 -0
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
- package/dist/src/self-relay/server.mjs +1006 -932
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/usage-meter.mjs +141 -141
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +1861 -1778
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-doctor.mjs +110 -110
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-install.mjs +91 -91
- package/dist/src/service-lifecycle.mjs +138 -138
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/shared-app-server.mjs +615 -611
- package/dist/src/shared-catalog.mjs +132 -132
- package/dist/src/shared-recovery.mjs +146 -146
- package/dist/src/shared-thread-stream.mjs +164 -164
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/src/usage-reset.mjs +17 -17
- package/dist/src/workstation-downloads.mjs +120 -120
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +7 -6
- package/tools/postinstall.mjs +127 -127
|
@@ -1,244 +1,244 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { executableKey } from './platform/path-identity.mjs';
|
|
3
|
-
import net from 'node:net';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { EventEmitter } from 'node:events';
|
|
6
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
-
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
8
|
-
import { SharedAppServer } from './shared-app-server.mjs';
|
|
9
|
-
import { pruneRetainedDesktopRuntimes, resolveCodexRuntime, runtimeFilesAvailable } from './platform/codex-runtime.mjs';
|
|
10
|
-
|
|
11
|
-
export const managedAppServerUrl = 'ws://127.0.0.1:45839';
|
|
12
|
-
export const managedAppServerReadyUrl = 'http://127.0.0.1:45839/readyz';
|
|
13
|
-
export const managedAppServerLauncherRevision = 3;
|
|
14
|
-
const retryDelays = [1_000, 2_000, 5_000, 10_000, 20_000];
|
|
15
|
-
|
|
16
|
-
export function managedPortAvailable() {
|
|
17
|
-
return new Promise(resolve => {
|
|
18
|
-
const server = net.createServer();
|
|
19
|
-
server.once('error', () => resolve(false));
|
|
20
|
-
server.listen(45839, '127.0.0.1', () => server.close(() => resolve(true)));
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class ManagedAppServer extends EventEmitter {
|
|
25
|
-
constructor({ stateRoot, host, environment = process.env, available = managedPortAvailable,
|
|
26
|
-
wait = delay, timeout = 30_000, now = Date.now, client = new SharedAppServer(managedAppServerUrl) }) {
|
|
27
|
-
super();
|
|
28
|
-
this.stateRoot = stateRoot;
|
|
29
|
-
this.host = host;
|
|
30
|
-
this.environment = environment;
|
|
31
|
-
this.available = available;
|
|
32
|
-
this.wait = wait;
|
|
33
|
-
this.timeout = timeout;
|
|
34
|
-
this.now = now;
|
|
35
|
-
this.recordPath = path.join(stateRoot, 'managed-app-server.json');
|
|
36
|
-
this.client = client;
|
|
37
|
-
this.state = { state: 'absent', ready: false, error: null };
|
|
38
|
-
this.restartStatus = null;
|
|
39
|
-
this.stopped = true;
|
|
40
|
-
this.restarting = false;
|
|
41
|
-
this.retryAttempt = 0;
|
|
42
|
-
this.retryAfter = 0;
|
|
43
|
-
this.client.on('disconnected', () => {
|
|
44
|
-
if (!this.stopped && !this.restarting) this.failed('Codex 内核连接断开。');
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
snapshot() {
|
|
49
|
-
return { ...this.state,
|
|
50
|
-
version: this.state.ready && this.client.ready ? this.client.serverVersion || null : null,
|
|
51
|
-
...(this.restartStatus ? { restart: { ...this.restartStatus } } : {}) };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async start() {
|
|
55
|
-
this.stopped = false;
|
|
56
|
-
return this.ensure();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async ensure() {
|
|
60
|
-
if (this.stopped) return false;
|
|
61
|
-
if (this.restarting) return false;
|
|
62
|
-
if (this.work) return this.work;
|
|
63
|
-
if (this.now() < this.retryAfter) return false;
|
|
64
|
-
this.work = this.ensureNow().finally(() => { this.work = null; });
|
|
65
|
-
return this.work;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async ensureNow() {
|
|
69
|
-
// Keep an already verified instance available while the periodic identity
|
|
70
|
-
// check runs; publishing "starting" every three seconds makes health flicker.
|
|
71
|
-
if (!this.state.ready) this.publish({ state: 'starting', ready: false, error: null });
|
|
72
|
-
try {
|
|
73
|
-
fs.mkdirSync(this.stateRoot, { recursive: true });
|
|
74
|
-
let record = null, actual = null;
|
|
75
|
-
const candidate = readRecord(this.recordPath);
|
|
76
|
-
const process = candidate?.pid ? await this.host.process(candidate.pid) : null;
|
|
77
|
-
if (sameManagedProcess(candidate, process)) { record = candidate; actual = process; }
|
|
78
|
-
const startedHere = !actual;
|
|
79
|
-
const savedRuntime = actual ? readRecord(path.join(this.stateRoot, 'codex-runtime.json')) : null;
|
|
80
|
-
const runtime = actual && savedRuntime?.executable && runtimeFilesAvailable(savedRuntime.executable)
|
|
81
|
-
? savedRuntime
|
|
82
|
-
: await resolveCodexRuntime(this.stateRoot, { refresh: !actual, environment: this.environment });
|
|
83
|
-
if (actual && !runtimeFilesAvailable(actual.executable)) {
|
|
84
|
-
throw new Error(`运行中的 Codex 内核目录已被移除或缺少配套 code-mode-host:${actual.executable};已找到可用内核 ${runtime.executable},请确认重启内核。`);
|
|
85
|
-
}
|
|
86
|
-
if (!actual) {
|
|
87
|
-
if (!await this.available()) throw new Error('45839 被未识别进程占用,未启动或结束任何进程。');
|
|
88
|
-
const childEnvironment = { ...this.environment };
|
|
89
|
-
childEnvironment.CODEX_HOME = runtime.codexHome;
|
|
90
|
-
// Desktop's inherited INFO tracing can emit enter/exit on every poll.
|
|
91
|
-
// The managed background kernel retains warnings and errors only.
|
|
92
|
-
childEnvironment.RUST_LOG = 'warn';
|
|
93
|
-
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnvironment[key];
|
|
94
|
-
for (const key of Object.keys(childEnvironment)) {
|
|
95
|
-
if (/^npm_/iu.test(key) || key.toUpperCase() === 'INIT_CWD') delete childEnvironment[key];
|
|
96
|
-
}
|
|
97
|
-
const pid = await this.host.start(runtime.executable, ['app-server', '--listen', managedAppServerUrl], childEnvironment,
|
|
98
|
-
[path.join(this.stateRoot, 'managed-app-server.stdout.log'), path.join(this.stateRoot, 'managed-app-server.stderr.log')]);
|
|
99
|
-
const started = await this.host.process(pid);
|
|
100
|
-
if (!sameManagedProcess(started, started)) throw new Error('Codex 内核启动后进程身份无法确认。');
|
|
101
|
-
actual = started;
|
|
102
|
-
record = { ...started, launcherRevision: managedAppServerLauncherRevision };
|
|
103
|
-
writeRecord(this.recordPath, record);
|
|
104
|
-
} else if (!readRecord(this.recordPath)) {
|
|
105
|
-
record = { ...actual, launcherRevision: Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0 };
|
|
106
|
-
writeRecord(this.recordPath, record);
|
|
107
|
-
}
|
|
108
|
-
await this.waitUntilReady(actual);
|
|
109
|
-
await this.client.connect();
|
|
110
|
-
if (!this.client.ready) throw new Error('Codex 内核协议初始化未完成。');
|
|
111
|
-
if (startedHere) {
|
|
112
|
-
try { await pruneRetainedDesktopRuntimes(this.stateRoot, actual.executable); }
|
|
113
|
-
catch (error) { console.error('旧版 Codex 内核文件清理失败:', error); }
|
|
114
|
-
}
|
|
115
|
-
const preferredExecutable = path.resolve(runtime.executable);
|
|
116
|
-
const updatePending = executableKey(actual.executable) !== executableKey(preferredExecutable);
|
|
117
|
-
const launcherRevision = Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0;
|
|
118
|
-
this.retryAttempt = 0;
|
|
119
|
-
this.retryAfter = 0;
|
|
120
|
-
this.publish({ state: 'ready', ready: true, error: null, pid: actual.pid,
|
|
121
|
-
created: actual.created, executable: actual.executable, preferredExecutable,
|
|
122
|
-
updatePending, launcherRevision, selectedVersion: runtime.version, source: runtime.source,
|
|
123
|
-
launcherRestartRequired: launcherRevision !== managedAppServerLauncherRevision,
|
|
124
|
-
url: managedAppServerUrl });
|
|
125
|
-
return true;
|
|
126
|
-
} catch (error) {
|
|
127
|
-
this.failed(error instanceof Error ? error.message : String(error));
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async waitUntilReady(expected) {
|
|
133
|
-
const deadline = this.now() + this.timeout;
|
|
134
|
-
while (true) {
|
|
135
|
-
const actual = await this.host.process(expected.pid);
|
|
136
|
-
if (!sameManagedProcess(expected, actual)) throw new Error('Codex 内核在就绪前退出。');
|
|
137
|
-
if (await this.host.ready(managedAppServerReadyUrl)) return;
|
|
138
|
-
if (this.now() >= deadline) throw new Error('Codex 内核未在 30 秒内就绪,请查看工作站诊断日志。');
|
|
139
|
-
await this.wait(250);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
restart(operationId) {
|
|
144
|
-
if (this.stopped) throw new Error('工作站服务正在停止,不能重启 Codex 内核。');
|
|
145
|
-
if (this.restartWork) {
|
|
146
|
-
if (this.restartStatus?.operationId === operationId) return this.snapshot();
|
|
147
|
-
const error = new Error('Codex 内核已经在重启,请等待当前操作完成。');
|
|
148
|
-
error.code = 'managed_app_server_restarting';
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
this.restartStatus = { operationId, status: 'running', stage: 'checking', error: null,
|
|
152
|
-
startedAt: new Date(this.now()).toISOString() };
|
|
153
|
-
this.emit('state', this.snapshot());
|
|
154
|
-
this.restartWork = this.restartNow().finally(() => { this.restartWork = null; });
|
|
155
|
-
return this.snapshot();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async restartNow() {
|
|
159
|
-
this.restarting = true;
|
|
160
|
-
const previous = this.state;
|
|
161
|
-
this.publish({ ...previous, state: 'restarting', ready: false, error: null });
|
|
162
|
-
try {
|
|
163
|
-
const record = readRecord(this.recordPath);
|
|
164
|
-
const actual = record?.pid ? await this.host.process(record.pid) : null;
|
|
165
|
-
if (actual && !sameManagedProcess(record, actual)) {
|
|
166
|
-
throw new Error('Codex 内核进程身份已经变化,未结束任何进程。');
|
|
167
|
-
}
|
|
168
|
-
if (!actual && !await this.available()) throw new Error('45839 被未识别进程占用,未结束任何进程。');
|
|
169
|
-
this.restartStage('stopping');
|
|
170
|
-
// Resolve the replacement before stopping the current working process.
|
|
171
|
-
const runtime = await resolveCodexRuntime(this.stateRoot, { refresh: true, environment: this.environment });
|
|
172
|
-
const desktop = await this.host.prepareKernelRestart?.();
|
|
173
|
-
this.client.close();
|
|
174
|
-
if (actual) {
|
|
175
|
-
await this.host.stop(actual.pid, actual);
|
|
176
|
-
await this.waitUntilStopped(actual);
|
|
177
|
-
} else if (!await this.available()) {
|
|
178
|
-
throw new Error('45839 被未识别进程占用,未结束任何进程。');
|
|
179
|
-
}
|
|
180
|
-
fs.rmSync(this.recordPath, { force: true });
|
|
181
|
-
this.retryAttempt = 0;
|
|
182
|
-
this.retryAfter = 0;
|
|
183
|
-
this.restartStage('starting');
|
|
184
|
-
const ready = await this.ensureNow();
|
|
185
|
-
if (!ready) throw new Error(this.state.error || 'Codex 内核未能重新启动。');
|
|
186
|
-
const desktopEnvironment = { ...this.environment, CODEX_HOME: runtime.codexHome, CODEX_APP_SERVER_WS_URL: managedAppServerUrl };
|
|
187
|
-
delete desktopEnvironment.CODEX_APP_SERVER_FORCE_CLI;
|
|
188
|
-
delete desktopEnvironment.CODEX_APP_TOOLS_PIPE_PATH;
|
|
189
|
-
await this.host.resumeAfterKernelRestart?.(desktop, desktopEnvironment);
|
|
190
|
-
this.restartStatus = { ...this.restartStatus, status: 'completed', stage: 'ready',
|
|
191
|
-
completedAt: new Date(this.now()).toISOString() };
|
|
192
|
-
this.emit('state', this.snapshot());
|
|
193
|
-
} catch (error) {
|
|
194
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
195
|
-
if (this.state.state === 'restarting') this.failed(message);
|
|
196
|
-
this.restartStatus = { ...this.restartStatus, status: 'failed', stage: 'failed', error: message,
|
|
197
|
-
completedAt: new Date(this.now()).toISOString() };
|
|
198
|
-
this.emit('state', this.snapshot());
|
|
199
|
-
} finally {
|
|
200
|
-
this.restarting = false;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
restartStage(stage) {
|
|
205
|
-
this.restartStatus = { ...this.restartStatus, stage };
|
|
206
|
-
this.emit('state', this.snapshot());
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async waitUntilStopped(expected) {
|
|
210
|
-
const deadline = this.now() + 10_000;
|
|
211
|
-
while (true) {
|
|
212
|
-
const actual = await this.host.process(expected.pid);
|
|
213
|
-
if (!sameManagedProcess(expected, actual) && await this.available()) return;
|
|
214
|
-
if (this.now() >= deadline) throw new Error('Codex 内核未在 10 秒内退出;不会启动第二个实例。');
|
|
215
|
-
await this.wait(100);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
failed(message) {
|
|
220
|
-
const retryMs = retryDelays[Math.min(this.retryAttempt++, retryDelays.length - 1)];
|
|
221
|
-
this.retryAfter = this.now() + retryMs;
|
|
222
|
-
this.publish({ state: 'failed', ready: false, error: message, retryAt: new Date(this.retryAfter).toISOString() });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
publish(value) {
|
|
226
|
-
const changed = JSON.stringify(value) !== JSON.stringify(this.state);
|
|
227
|
-
this.state = value;
|
|
228
|
-
if (changed) this.emit('state', this.snapshot());
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
close() {
|
|
232
|
-
this.stopped = true;
|
|
233
|
-
this.client.close();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function sameManagedProcess(record, actual) {
|
|
238
|
-
if (!record || !actual || record.pid !== actual.pid || record.created !== actual.created) return false;
|
|
239
|
-
if (!record.executable || !actual.executable || executableKey(record.executable) !== executableKey(actual.executable)) return false;
|
|
240
|
-
if (Array.isArray(actual.args)) return JSON.stringify(record.args) === JSON.stringify(actual.args)
|
|
241
|
-
&& JSON.stringify(actual.args.slice(1)) === JSON.stringify(['app-server', '--listen', managedAppServerUrl]);
|
|
242
|
-
const command = String(actual.command || '').toLowerCase();
|
|
243
|
-
return command.includes('app-server') && command.includes(managedAppServerUrl);
|
|
244
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { executableKey } from './platform/path-identity.mjs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
8
|
+
import { SharedAppServer } from './shared-app-server.mjs';
|
|
9
|
+
import { pruneRetainedDesktopRuntimes, resolveCodexRuntime, runtimeFilesAvailable } from './platform/codex-runtime.mjs';
|
|
10
|
+
|
|
11
|
+
export const managedAppServerUrl = 'ws://127.0.0.1:45839';
|
|
12
|
+
export const managedAppServerReadyUrl = 'http://127.0.0.1:45839/readyz';
|
|
13
|
+
export const managedAppServerLauncherRevision = 3;
|
|
14
|
+
const retryDelays = [1_000, 2_000, 5_000, 10_000, 20_000];
|
|
15
|
+
|
|
16
|
+
export function managedPortAvailable() {
|
|
17
|
+
return new Promise(resolve => {
|
|
18
|
+
const server = net.createServer();
|
|
19
|
+
server.once('error', () => resolve(false));
|
|
20
|
+
server.listen(45839, '127.0.0.1', () => server.close(() => resolve(true)));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ManagedAppServer extends EventEmitter {
|
|
25
|
+
constructor({ stateRoot, host, environment = process.env, available = managedPortAvailable,
|
|
26
|
+
wait = delay, timeout = 30_000, now = Date.now, client = new SharedAppServer(managedAppServerUrl) }) {
|
|
27
|
+
super();
|
|
28
|
+
this.stateRoot = stateRoot;
|
|
29
|
+
this.host = host;
|
|
30
|
+
this.environment = environment;
|
|
31
|
+
this.available = available;
|
|
32
|
+
this.wait = wait;
|
|
33
|
+
this.timeout = timeout;
|
|
34
|
+
this.now = now;
|
|
35
|
+
this.recordPath = path.join(stateRoot, 'managed-app-server.json');
|
|
36
|
+
this.client = client;
|
|
37
|
+
this.state = { state: 'absent', ready: false, error: null };
|
|
38
|
+
this.restartStatus = null;
|
|
39
|
+
this.stopped = true;
|
|
40
|
+
this.restarting = false;
|
|
41
|
+
this.retryAttempt = 0;
|
|
42
|
+
this.retryAfter = 0;
|
|
43
|
+
this.client.on('disconnected', () => {
|
|
44
|
+
if (!this.stopped && !this.restarting) this.failed('Codex 内核连接断开。');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
snapshot() {
|
|
49
|
+
return { ...this.state,
|
|
50
|
+
version: this.state.ready && this.client.ready ? this.client.serverVersion || null : null,
|
|
51
|
+
...(this.restartStatus ? { restart: { ...this.restartStatus } } : {}) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async start() {
|
|
55
|
+
this.stopped = false;
|
|
56
|
+
return this.ensure();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async ensure() {
|
|
60
|
+
if (this.stopped) return false;
|
|
61
|
+
if (this.restarting) return false;
|
|
62
|
+
if (this.work) return this.work;
|
|
63
|
+
if (this.now() < this.retryAfter) return false;
|
|
64
|
+
this.work = this.ensureNow().finally(() => { this.work = null; });
|
|
65
|
+
return this.work;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async ensureNow() {
|
|
69
|
+
// Keep an already verified instance available while the periodic identity
|
|
70
|
+
// check runs; publishing "starting" every three seconds makes health flicker.
|
|
71
|
+
if (!this.state.ready) this.publish({ state: 'starting', ready: false, error: null });
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(this.stateRoot, { recursive: true });
|
|
74
|
+
let record = null, actual = null;
|
|
75
|
+
const candidate = readRecord(this.recordPath);
|
|
76
|
+
const process = candidate?.pid ? await this.host.process(candidate.pid) : null;
|
|
77
|
+
if (sameManagedProcess(candidate, process)) { record = candidate; actual = process; }
|
|
78
|
+
const startedHere = !actual;
|
|
79
|
+
const savedRuntime = actual ? readRecord(path.join(this.stateRoot, 'codex-runtime.json')) : null;
|
|
80
|
+
const runtime = actual && savedRuntime?.executable && runtimeFilesAvailable(savedRuntime.executable)
|
|
81
|
+
? savedRuntime
|
|
82
|
+
: await resolveCodexRuntime(this.stateRoot, { refresh: !actual, environment: this.environment });
|
|
83
|
+
if (actual && !runtimeFilesAvailable(actual.executable)) {
|
|
84
|
+
throw new Error(`运行中的 Codex 内核目录已被移除或缺少配套 code-mode-host:${actual.executable};已找到可用内核 ${runtime.executable},请确认重启内核。`);
|
|
85
|
+
}
|
|
86
|
+
if (!actual) {
|
|
87
|
+
if (!await this.available()) throw new Error('45839 被未识别进程占用,未启动或结束任何进程。');
|
|
88
|
+
const childEnvironment = { ...this.environment };
|
|
89
|
+
childEnvironment.CODEX_HOME = runtime.codexHome;
|
|
90
|
+
// Desktop's inherited INFO tracing can emit enter/exit on every poll.
|
|
91
|
+
// The managed background kernel retains warnings and errors only.
|
|
92
|
+
childEnvironment.RUST_LOG = 'warn';
|
|
93
|
+
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnvironment[key];
|
|
94
|
+
for (const key of Object.keys(childEnvironment)) {
|
|
95
|
+
if (/^npm_/iu.test(key) || key.toUpperCase() === 'INIT_CWD') delete childEnvironment[key];
|
|
96
|
+
}
|
|
97
|
+
const pid = await this.host.start(runtime.executable, ['app-server', '--listen', managedAppServerUrl], childEnvironment,
|
|
98
|
+
[path.join(this.stateRoot, 'managed-app-server.stdout.log'), path.join(this.stateRoot, 'managed-app-server.stderr.log')]);
|
|
99
|
+
const started = await this.host.process(pid);
|
|
100
|
+
if (!sameManagedProcess(started, started)) throw new Error('Codex 内核启动后进程身份无法确认。');
|
|
101
|
+
actual = started;
|
|
102
|
+
record = { ...started, launcherRevision: managedAppServerLauncherRevision };
|
|
103
|
+
writeRecord(this.recordPath, record);
|
|
104
|
+
} else if (!readRecord(this.recordPath)) {
|
|
105
|
+
record = { ...actual, launcherRevision: Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0 };
|
|
106
|
+
writeRecord(this.recordPath, record);
|
|
107
|
+
}
|
|
108
|
+
await this.waitUntilReady(actual);
|
|
109
|
+
await this.client.connect();
|
|
110
|
+
if (!this.client.ready) throw new Error('Codex 内核协议初始化未完成。');
|
|
111
|
+
if (startedHere) {
|
|
112
|
+
try { await pruneRetainedDesktopRuntimes(this.stateRoot, actual.executable); }
|
|
113
|
+
catch (error) { console.error('旧版 Codex 内核文件清理失败:', error); }
|
|
114
|
+
}
|
|
115
|
+
const preferredExecutable = path.resolve(runtime.executable);
|
|
116
|
+
const updatePending = executableKey(actual.executable) !== executableKey(preferredExecutable);
|
|
117
|
+
const launcherRevision = Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0;
|
|
118
|
+
this.retryAttempt = 0;
|
|
119
|
+
this.retryAfter = 0;
|
|
120
|
+
this.publish({ state: 'ready', ready: true, error: null, pid: actual.pid,
|
|
121
|
+
created: actual.created, executable: actual.executable, preferredExecutable,
|
|
122
|
+
updatePending, launcherRevision, selectedVersion: runtime.version, source: runtime.source,
|
|
123
|
+
launcherRestartRequired: launcherRevision !== managedAppServerLauncherRevision,
|
|
124
|
+
url: managedAppServerUrl });
|
|
125
|
+
return true;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.failed(error instanceof Error ? error.message : String(error));
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async waitUntilReady(expected) {
|
|
133
|
+
const deadline = this.now() + this.timeout;
|
|
134
|
+
while (true) {
|
|
135
|
+
const actual = await this.host.process(expected.pid);
|
|
136
|
+
if (!sameManagedProcess(expected, actual)) throw new Error('Codex 内核在就绪前退出。');
|
|
137
|
+
if (await this.host.ready(managedAppServerReadyUrl)) return;
|
|
138
|
+
if (this.now() >= deadline) throw new Error('Codex 内核未在 30 秒内就绪,请查看工作站诊断日志。');
|
|
139
|
+
await this.wait(250);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
restart(operationId) {
|
|
144
|
+
if (this.stopped) throw new Error('工作站服务正在停止,不能重启 Codex 内核。');
|
|
145
|
+
if (this.restartWork) {
|
|
146
|
+
if (this.restartStatus?.operationId === operationId) return this.snapshot();
|
|
147
|
+
const error = new Error('Codex 内核已经在重启,请等待当前操作完成。');
|
|
148
|
+
error.code = 'managed_app_server_restarting';
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
this.restartStatus = { operationId, status: 'running', stage: 'checking', error: null,
|
|
152
|
+
startedAt: new Date(this.now()).toISOString() };
|
|
153
|
+
this.emit('state', this.snapshot());
|
|
154
|
+
this.restartWork = this.restartNow().finally(() => { this.restartWork = null; });
|
|
155
|
+
return this.snapshot();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async restartNow() {
|
|
159
|
+
this.restarting = true;
|
|
160
|
+
const previous = this.state;
|
|
161
|
+
this.publish({ ...previous, state: 'restarting', ready: false, error: null });
|
|
162
|
+
try {
|
|
163
|
+
const record = readRecord(this.recordPath);
|
|
164
|
+
const actual = record?.pid ? await this.host.process(record.pid) : null;
|
|
165
|
+
if (actual && !sameManagedProcess(record, actual)) {
|
|
166
|
+
throw new Error('Codex 内核进程身份已经变化,未结束任何进程。');
|
|
167
|
+
}
|
|
168
|
+
if (!actual && !await this.available()) throw new Error('45839 被未识别进程占用,未结束任何进程。');
|
|
169
|
+
this.restartStage('stopping');
|
|
170
|
+
// Resolve the replacement before stopping the current working process.
|
|
171
|
+
const runtime = await resolveCodexRuntime(this.stateRoot, { refresh: true, environment: this.environment });
|
|
172
|
+
const desktop = await this.host.prepareKernelRestart?.();
|
|
173
|
+
this.client.close();
|
|
174
|
+
if (actual) {
|
|
175
|
+
await this.host.stop(actual.pid, actual);
|
|
176
|
+
await this.waitUntilStopped(actual);
|
|
177
|
+
} else if (!await this.available()) {
|
|
178
|
+
throw new Error('45839 被未识别进程占用,未结束任何进程。');
|
|
179
|
+
}
|
|
180
|
+
fs.rmSync(this.recordPath, { force: true });
|
|
181
|
+
this.retryAttempt = 0;
|
|
182
|
+
this.retryAfter = 0;
|
|
183
|
+
this.restartStage('starting');
|
|
184
|
+
const ready = await this.ensureNow();
|
|
185
|
+
if (!ready) throw new Error(this.state.error || 'Codex 内核未能重新启动。');
|
|
186
|
+
const desktopEnvironment = { ...this.environment, CODEX_HOME: runtime.codexHome, CODEX_APP_SERVER_WS_URL: managedAppServerUrl };
|
|
187
|
+
delete desktopEnvironment.CODEX_APP_SERVER_FORCE_CLI;
|
|
188
|
+
delete desktopEnvironment.CODEX_APP_TOOLS_PIPE_PATH;
|
|
189
|
+
await this.host.resumeAfterKernelRestart?.(desktop, desktopEnvironment);
|
|
190
|
+
this.restartStatus = { ...this.restartStatus, status: 'completed', stage: 'ready',
|
|
191
|
+
completedAt: new Date(this.now()).toISOString() };
|
|
192
|
+
this.emit('state', this.snapshot());
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
195
|
+
if (this.state.state === 'restarting') this.failed(message);
|
|
196
|
+
this.restartStatus = { ...this.restartStatus, status: 'failed', stage: 'failed', error: message,
|
|
197
|
+
completedAt: new Date(this.now()).toISOString() };
|
|
198
|
+
this.emit('state', this.snapshot());
|
|
199
|
+
} finally {
|
|
200
|
+
this.restarting = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
restartStage(stage) {
|
|
205
|
+
this.restartStatus = { ...this.restartStatus, stage };
|
|
206
|
+
this.emit('state', this.snapshot());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async waitUntilStopped(expected) {
|
|
210
|
+
const deadline = this.now() + 10_000;
|
|
211
|
+
while (true) {
|
|
212
|
+
const actual = await this.host.process(expected.pid);
|
|
213
|
+
if (!sameManagedProcess(expected, actual) && await this.available()) return;
|
|
214
|
+
if (this.now() >= deadline) throw new Error('Codex 内核未在 10 秒内退出;不会启动第二个实例。');
|
|
215
|
+
await this.wait(100);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
failed(message) {
|
|
220
|
+
const retryMs = retryDelays[Math.min(this.retryAttempt++, retryDelays.length - 1)];
|
|
221
|
+
this.retryAfter = this.now() + retryMs;
|
|
222
|
+
this.publish({ state: 'failed', ready: false, error: message, retryAt: new Date(this.retryAfter).toISOString() });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
publish(value) {
|
|
226
|
+
const changed = JSON.stringify(value) !== JSON.stringify(this.state);
|
|
227
|
+
this.state = value;
|
|
228
|
+
if (changed) this.emit('state', this.snapshot());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
close() {
|
|
232
|
+
this.stopped = true;
|
|
233
|
+
this.client.close();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function sameManagedProcess(record, actual) {
|
|
238
|
+
if (!record || !actual || record.pid !== actual.pid || record.created !== actual.created) return false;
|
|
239
|
+
if (!record.executable || !actual.executable || executableKey(record.executable) !== executableKey(actual.executable)) return false;
|
|
240
|
+
if (Array.isArray(actual.args)) return JSON.stringify(record.args) === JSON.stringify(actual.args)
|
|
241
|
+
&& JSON.stringify(actual.args.slice(1)) === JSON.stringify(['app-server', '--listen', managedAppServerUrl]);
|
|
242
|
+
const command = String(actual.command || '').toLowerCase();
|
|
243
|
+
return command.includes('app-server') && command.includes(managedAppServerUrl);
|
|
244
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export function validateInlineImage(value) {
|
|
2
|
-
const data = typeof value === 'string' ? value.slice(value.indexOf(',') + 1) : '';
|
|
3
|
-
if (typeof value !== 'string' || value.length > Math.ceil(8 * 1024 * 1024 / 3) * 4 + 64 ||
|
|
4
|
-
!/^data:image\/(?:jpeg|png|webp);base64,/.test(value) || !data || data.length % 4 !== 0 ||
|
|
5
|
-
!/^[A-Za-z0-9+/]*={0,2}$/.test(data) || Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) {
|
|
6
|
-
throw new Error('图片必须是 JPEG、PNG 或 WebP 数据,且不超过 8 MiB。');
|
|
7
|
-
}
|
|
8
|
-
return value;
|
|
9
|
-
}
|
|
1
|
+
export function validateInlineImage(value) {
|
|
2
|
+
const data = typeof value === 'string' ? value.slice(value.indexOf(',') + 1) : '';
|
|
3
|
+
if (typeof value !== 'string' || value.length > Math.ceil(8 * 1024 * 1024 / 3) * 4 + 64 ||
|
|
4
|
+
!/^data:image\/(?:jpeg|png|webp);base64,/.test(value) || !data || data.length % 4 !== 0 ||
|
|
5
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(data) || Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) {
|
|
6
|
+
throw new Error('图片必须是 JPEG、PNG 或 WebP 数据,且不超过 8 MiB。');
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|