@walkhi/code-relax 0.1.0-beta.6 → 0.1.0-beta.8
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 +3 -3
- package/dist/bin/codex-remote.mjs +42 -7
- package/dist/src/managed-app-server.mjs +3 -0
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/server.mjs +18 -6
- package/dist/src/service-doctor.mjs +7 -2
- package/dist/src/service-lifecycle.mjs +1 -1
- package/package.json +1 -1
- package/tools/postinstall.mjs +33 -5
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Bridge 让 HarmonyOS 客户端控制 Windows 上的 Codex Desktop。当前要求
|
|
|
7
7
|
公开 beta 已发布到 npm,安装或更新使用:
|
|
8
8
|
|
|
9
9
|
```powershell
|
|
10
|
-
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax
|
|
10
|
+
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
首次安装前先保存工作,并从 Codex 菜单或 Windows 托盘完全退出 Codex。更新无需先停止正在运行的 Bridge;安装器会短暂停止、替换并恢复服务。npm 全局安装会自动执行 `setup` 和 Skill 安装。`setup` 将完整服务部署到 `%LOCALAPPDATA%\Programs\CodexRemote\service`,记录安装所用 Node 的绝对路径,不复制 Node。配置、日志和二维码以 `installation.json.stateRoot` 为准,通常位于 `%LOCALAPPDATA%\CodexRemote`,MSIX 环境可能解析到包缓存目录;附件位于 `%ProgramData%\CodexRemote\attachments`。
|
|
@@ -64,7 +64,7 @@ relax dev-stop --json
|
|
|
64
64
|
更新固定服务:
|
|
65
65
|
|
|
66
66
|
```powershell
|
|
67
|
-
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax
|
|
67
|
+
npm i -g @walkhi/code-relax@beta --allow-scripts=@walkhi/code-relax --foreground-scripts
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
`setup` 先暂存并校验新版本,再短暂停止 Bridge/中继完成切换;原服务此前正在运行时会自动恢复,新版本启动失败则回滚并启动旧版本。无需提前执行 `stop`。Node 路径变化或原 Node 被删除后需重新 setup。升级可能因 Node 可执行文件路径变化而再次触发 Windows 防火墙授权。
|
|
@@ -80,4 +80,4 @@ npm uninstall -g @walkhi/code-relax
|
|
|
80
80
|
|
|
81
81
|
## 发布边界
|
|
82
82
|
|
|
83
|
-
registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.
|
|
83
|
+
registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.8`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。该版让安装摘要通过 `--foreground-scripts` 直接显示,首次安装或启动链迁移时干净重启 Desktop,分别报告已安装与实际运行版本,并把旧 Startup PowerShell 快捷方式迁移为当前用户任务计划登录触发器。通信协议仍为 7。详细规则见 [npm 分发](../docs/npm-distribution.md)。
|
|
@@ -170,7 +170,9 @@ async function main() {
|
|
|
170
170
|
const source = path.join(stateRoot, name), target = path.join(physicalStateRoot, name);
|
|
171
171
|
if (fs.existsSync(source) && !fs.existsSync(target)) fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
|
|
172
172
|
}
|
|
173
|
-
const
|
|
173
|
+
const hadInstalledService = fs.existsSync(path.join(serviceRoot, 'installation.json'));
|
|
174
|
+
const desktopBeforeSetup = await desktopHost.inspect();
|
|
175
|
+
const previous = hadInstalledService ? await service.status() : { processRunning: false };
|
|
174
176
|
const previousRelay = await selfRelay('relay-status', stateRoot);
|
|
175
177
|
const resumeAfterUpdate = previous.processRunning || previousRelay.running;
|
|
176
178
|
const executable = fs.realpathSync(process.execPath);
|
|
@@ -206,7 +208,18 @@ async function main() {
|
|
|
206
208
|
throw new Error(bridge.managedAppServer?.error || 'Bridge 已安装,但受管 app-server 未就绪;未写入 Desktop 环境变量。');
|
|
207
209
|
}
|
|
208
210
|
let managedAppServerMigrated = false;
|
|
209
|
-
|
|
211
|
+
const managedAppServerMigration = {
|
|
212
|
+
required: bridge.managedAppServer.launcherRestartRequired === true,
|
|
213
|
+
restarted: false,
|
|
214
|
+
previousPid: bridge.managedAppServer.pid || null,
|
|
215
|
+
};
|
|
216
|
+
let desktopRestarted = false;
|
|
217
|
+
let desktopRestartReason = null;
|
|
218
|
+
if (managedAppServerMigration.required) {
|
|
219
|
+
const desktopInfo = await desktopHost.inspect();
|
|
220
|
+
const desktopAttached = desktopInfo.running
|
|
221
|
+
&& (bridge.desktop?.attachedToManagedServer === true || Boolean(await host.sharedServiceUrl()));
|
|
222
|
+
if (desktopAttached) await desktopHost.closeDesktop(desktopInfo.desktop);
|
|
210
223
|
await service.restartManagedAppServer();
|
|
211
224
|
const migrationDeadline = Date.now() + 40_000;
|
|
212
225
|
do {
|
|
@@ -220,18 +233,40 @@ async function main() {
|
|
|
220
233
|
throw new Error('受管 app-server 未在 40 秒内完成启动链迁移。');
|
|
221
234
|
}
|
|
222
235
|
managedAppServerMigrated = true;
|
|
236
|
+
managedAppServerMigration.restarted = true;
|
|
237
|
+
if (desktopAttached) {
|
|
238
|
+
await launchDesktop('shared', { stateRoot: physicalStateRoot, host: desktopHost });
|
|
239
|
+
desktopRestarted = true;
|
|
240
|
+
desktopRestartReason = 'app-server-migration';
|
|
241
|
+
bridge = await service.status();
|
|
242
|
+
}
|
|
223
243
|
}
|
|
224
244
|
const autostart = await residentStartup(installed, 'enable');
|
|
225
245
|
const environment = await configureManagedEnvironment(physicalStateRoot);
|
|
246
|
+
if (!hadInstalledService && desktopBeforeSetup.running && !desktopRestarted) {
|
|
247
|
+
const currentDesktop = await desktopHost.inspect();
|
|
248
|
+
if (currentDesktop.running) await desktopHost.closeDesktop(currentDesktop.desktop);
|
|
249
|
+
await launchDesktop('shared', { stateRoot: physicalStateRoot, host: desktopHost });
|
|
250
|
+
desktopRestarted = true;
|
|
251
|
+
desktopRestartReason = 'first-install';
|
|
252
|
+
bridge = await service.status();
|
|
253
|
+
}
|
|
226
254
|
installed.autostart = autostart;
|
|
227
255
|
installed.restarted = resumeAfterUpdate;
|
|
228
256
|
installed.environment = environment;
|
|
229
257
|
installed.managedAppServer = bridge.managedAppServer;
|
|
230
258
|
installed.managedAppServerMigrated = managedAppServerMigrated;
|
|
259
|
+
installed.managedAppServerMigration = { ...managedAppServerMigration,
|
|
260
|
+
currentPid: bridge.managedAppServer.pid || null,
|
|
261
|
+
launcherRevision: bridge.managedAppServer.launcherRevision ?? null };
|
|
262
|
+
installed.desktopRestarted = desktopRestarted;
|
|
263
|
+
installed.desktopRestartReason = desktopRestartReason;
|
|
231
264
|
installed.desktop = bridge.desktop;
|
|
232
|
-
installed.desktopRestartRecommended = bridge.desktop?.state === 'ordinary';
|
|
265
|
+
installed.desktopRestartRecommended = !desktopRestarted && bridge.desktop?.state === 'ordinary';
|
|
233
266
|
const migrationText = managedAppServerMigrated ? '已自动重建旧启动链的 app-server;安装前尚未完成的任务可能已中断。\n' : '';
|
|
234
|
-
const restartText =
|
|
267
|
+
const restartText = desktopRestarted
|
|
268
|
+
? 'Codex Desktop 已干净重启并重新接入受管 app-server。'
|
|
269
|
+
: installed.desktopRestartRecommended
|
|
235
270
|
? 'Codex 当前仍在使用原连接。可以在手机端选择“立即重启(推荐)”,或稍后自行完全退出再启动。'
|
|
236
271
|
: '下次正常启动 Codex 时会连接受管 app-server。';
|
|
237
272
|
console.log(json ? JSON.stringify({ ...installed, nextSteps: desktopSetupSteps, displayText: `${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}` }) : `服务已安装:${installed.serviceRoot}\n使用已安装的 Node:${installed.nodeExecutable}\n受管 app-server 已就绪;首次联网可能需要 Windows 防火墙授权。\n${migrationText}${desktopSetupSteps.join('\n')}\n${restartText}`);
|
|
@@ -250,9 +285,9 @@ async function main() {
|
|
|
250
285
|
}
|
|
251
286
|
if (!json && (desktop || command === 'start')) console.log('正在检查电脑连接并准备 Bridge…');
|
|
252
287
|
const result = await service[desktop ? 'start' : command]();
|
|
253
|
-
if (command === 'status') {
|
|
254
|
-
console.log(json ? JSON.stringify(result) : result.running
|
|
255
|
-
? `服务正常:${result.mode},端口 ${result.port},PID ${result.pid}` : '服务未就绪。');
|
|
288
|
+
if (command === 'status') {
|
|
289
|
+
console.log(json ? JSON.stringify(result) : result.running
|
|
290
|
+
? `服务正常:${result.mode},版本 ${result.version || '未知'},端口 ${result.port},PID ${result.pid}` : '服务未就绪。');
|
|
256
291
|
return;
|
|
257
292
|
}
|
|
258
293
|
const { accessUrl, qrPath: _legacyQrPath, ...metadata } = result;
|
|
@@ -77,6 +77,9 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
77
77
|
if (!await this.available()) throw new Error('45839 被未识别进程占用,未启动或结束任何进程。');
|
|
78
78
|
const childEnvironment = { ...this.environment };
|
|
79
79
|
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnvironment[key];
|
|
80
|
+
for (const key of Object.keys(childEnvironment)) {
|
|
81
|
+
if (/^npm_/iu.test(key) || key.toUpperCase() === 'INIT_CWD') delete childEnvironment[key];
|
|
82
|
+
}
|
|
80
83
|
const pid = await this.host.start(info.cli, ['app-server', '--listen', managedAppServerUrl], childEnvironment,
|
|
81
84
|
[path.join(this.stateRoot, 'managed-app-server.stdout.log'), path.join(this.stateRoot, 'managed-app-server.stderr.log')]);
|
|
82
85
|
const started = await this.host.process(pid);
|
|
@@ -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
|
|
@@ -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
|
}
|
package/dist/src/server.mjs
CHANGED
|
@@ -47,7 +47,7 @@ const ALLOWED_MODELS = new Set([
|
|
|
47
47
|
'gpt-5.3-codex-spark',
|
|
48
48
|
]);
|
|
49
49
|
const ALLOWED_THINKING = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']);
|
|
50
|
-
const ALLOWED_TOOLS = new Set([
|
|
50
|
+
const ALLOWED_TOOLS = new Set([
|
|
51
51
|
'create_thread',
|
|
52
52
|
'get_usage_limits',
|
|
53
53
|
'list_projects',
|
|
@@ -55,9 +55,21 @@ const ALLOWED_TOOLS = new Set([
|
|
|
55
55
|
'read_thread',
|
|
56
56
|
'send_message_to_thread',
|
|
57
57
|
'wait_threads',
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
function readRuntimeVersion() {
|
|
61
|
+
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
while (true) {
|
|
63
|
+
const manifest = readRecord(path.join(directory, 'package.json'));
|
|
64
|
+
if (manifest?.name === '@walkhi/code-relax') return manifest.version || '未知';
|
|
65
|
+
const parent = path.dirname(directory);
|
|
66
|
+
if (parent === directory) return '未知';
|
|
67
|
+
directory = parent;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const options = parseOptions(process.argv.slice(2));
|
|
72
|
+
const runtimeVersion = readRuntimeVersion();
|
|
61
73
|
const pipePath = process.env.CODEX_APP_TOOLS_PIPE_PATH?.trim();
|
|
62
74
|
let sourceThreadId = (process.env.CODEX_THREAD_ID || process.env.CODEX_SESSION_ID)?.trim();
|
|
63
75
|
|
|
@@ -201,8 +213,8 @@ async function handleRequest(request, response) {
|
|
|
201
213
|
// must not trigger PowerShell, process-identity, or app-server checks.
|
|
202
214
|
const recovery = readRecord(path.join(stateRoot, 'shared-recovery.json'));
|
|
203
215
|
const recovering = ['scheduled', 'running'].includes(recovery?.status);
|
|
204
|
-
sendJson(response, 200, {
|
|
205
|
-
status: 'ok', resident: true,
|
|
216
|
+
sendJson(response, 200, {
|
|
217
|
+
status: 'ok', resident: true, version: runtimeVersion,
|
|
206
218
|
execution: { ...executionHealth.state, ready: executionHealth.state.ready && (!sharedServer || sharedServer.ready),
|
|
207
219
|
...(recovering ? { state: 'recovering' } : {}), mode: executionMode },
|
|
208
220
|
managedAppServer: managedAppServer.snapshot(),
|
|
@@ -7,6 +7,7 @@ import { managedAppServerUrl } from './managed-app-server.mjs';
|
|
|
7
7
|
|
|
8
8
|
export async function doctor({ paths, host, service, skillTarget }) {
|
|
9
9
|
const checks = [];
|
|
10
|
+
let installedVersion;
|
|
10
11
|
const add = (id, status, message) => checks.push({ id, status, message });
|
|
11
12
|
const check = async (id, task) => {
|
|
12
13
|
try { await task(); } catch (error) { add(id, 'error', error.message); }
|
|
@@ -15,7 +16,8 @@ export async function doctor({ paths, host, service, skillTarget }) {
|
|
|
15
16
|
const record = readRecord(path.join(paths.serviceRoot, 'installation.json'));
|
|
16
17
|
if (!record || !fs.existsSync(path.join(paths.serviceRoot, 'dist/bin/codex-remote.mjs'))) throw Error('尚未完整部署服务,请重新安装 Code Relax。');
|
|
17
18
|
if (!record.nodeExecutable || !fs.existsSync(record.nodeExecutable)) throw Error('记录的 Node 已失效,请用可用的 Node 重新安装 Code Relax。');
|
|
18
|
-
|
|
19
|
+
installedVersion = record.version;
|
|
20
|
+
add('installation', 'ok', `已安装版本:${record.version};Node:${record.nodeExecutable}`);
|
|
19
21
|
});
|
|
20
22
|
let sharedUrl;
|
|
21
23
|
await check('shared-service', async () => {
|
|
@@ -34,7 +36,10 @@ export async function doctor({ paths, host, service, skillTarget }) {
|
|
|
34
36
|
await check('service', async () => {
|
|
35
37
|
const state = await service.status();
|
|
36
38
|
add('service', state.running ? 'ok' : 'warning', state.running
|
|
37
|
-
? `本机服务正常:${state.mode},端口 ${state.port}` : '服务未就绪;安装后执行 relax start。');
|
|
39
|
+
? `本机服务正常:${state.mode},端口 ${state.port},运行版本:${state.version || '未知'}` : '服务未就绪;安装后执行 relax start。');
|
|
40
|
+
if (state.running && installedVersion && state.version !== installedVersion) add('service-version', 'warning', state.version
|
|
41
|
+
? `已安装版本 ${installedVersion},但当前运行版本为 ${state.version};请重新执行安装或重启工作站服务。`
|
|
42
|
+
: `已安装版本 ${installedVersion},但当前运行 Bridge 未报告版本,可能仍是旧进程;请重新执行安装或重启工作站服务。`);
|
|
38
43
|
if (state.running) add('managed-app-server', state.managedAppServer?.state === 'ready' ? 'ok' : 'error',
|
|
39
44
|
state.managedAppServer?.state === 'ready' ? '受管 app-server 已就绪。'
|
|
40
45
|
: state.managedAppServer?.error || '受管 app-server 尚未就绪。');
|
|
@@ -56,7 +56,7 @@ export function lifecycle({ stateRoot, host }) {
|
|
|
56
56
|
const running = await alive(session);
|
|
57
57
|
const result = running ? await health(session) : null;
|
|
58
58
|
return { running: running && result?.status === 'ok', processRunning: running, pid: session?.pid,
|
|
59
|
-
port: session?.port, mode: result?.transportMode, execution: result?.execution,
|
|
59
|
+
port: session?.port, mode: result?.transportMode, version: result?.version, execution: result?.execution,
|
|
60
60
|
managedAppServer: result?.managedAppServer, desktop: result?.desktop,
|
|
61
61
|
desktopRestartRecommended: result?.desktopRestartRecommended === true,
|
|
62
62
|
resident: result?.resident === true };
|
package/package.json
CHANGED
package/tools/postinstall.mjs
CHANGED
|
@@ -33,12 +33,12 @@ function systemSummary() {
|
|
|
33
33
|
+ `osArch=${os.arch()} processArch=${process.arch} node=${process.version} npmUserAgent=${npm}`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function runStage(entry, stage, args) {
|
|
36
|
+
function runStage(entry, stage, args, formatOutput = value => value) {
|
|
37
37
|
appendLog(`[${stage}] start`);
|
|
38
38
|
try {
|
|
39
39
|
const output = execFileSync(process.execPath, [entry, ...args], { encoding: 'utf8', windowsHide: true,
|
|
40
40
|
stdio: ['inherit', 'pipe', 'pipe'] });
|
|
41
|
-
if (output) { process.stdout.write(output); appendLog(`[${stage}] stdout\n${output.trimEnd()}`); }
|
|
41
|
+
if (output) { process.stdout.write(formatOutput(output)); appendLog(`[${stage}] stdout\n${output.trimEnd()}`); }
|
|
42
42
|
appendLog(`[${stage}] success`);
|
|
43
43
|
} catch (error) {
|
|
44
44
|
if (error?.stdout) { process.stdout.write(error.stdout); appendLog(`[${stage}] stdout\n${String(error.stdout).trimEnd()}`); }
|
|
@@ -48,6 +48,31 @@ function runStage(entry, stage, args) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export function formatSetupOutput(output, previous = {}) {
|
|
52
|
+
try {
|
|
53
|
+
const result = JSON.parse(output);
|
|
54
|
+
const migration = result.managedAppServerMigration || {};
|
|
55
|
+
const pid = migration.currentPid || result.managedAppServer?.pid || '未知';
|
|
56
|
+
const appServer = migration.restarted
|
|
57
|
+
? `已重启(旧启动链迁移,PID ${migration.previousPid || '未知'} → ${pid})`
|
|
58
|
+
: previous.valid
|
|
59
|
+
? `未重启(PID ${pid},启动代次 ${migration.launcherRevision ?? '未知'} 已是最新)`
|
|
60
|
+
: `已启动(PID ${pid},启动代次 ${migration.launcherRevision ?? '未知'})`;
|
|
61
|
+
return `Code Relax ${result.version || '未知版本'} 安装成功。\n`
|
|
62
|
+
+ `受管 app-server:${appServer}\n`
|
|
63
|
+
+ `Bridge:${result.restarted ? '已恢复并健康' : '已启动并健康'}\n`
|
|
64
|
+
+ `${result.desktopRestarted ? `Codex Desktop:已干净重启并重新接入(${result.desktopRestartReason === 'first-install' ? '首次安装' : 'app-server 迁移'})\n` : ''}`
|
|
65
|
+
+ `${result.desktopRestartRecommended ? 'Codex Desktop:建议完全退出后重新打开。\n' : ''}`;
|
|
66
|
+
} catch { return output; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatSkillOutput(output) {
|
|
70
|
+
try {
|
|
71
|
+
const result = JSON.parse(output);
|
|
72
|
+
return `Code Relax Skill:${result.message || (result.installed ? '已安装' : '已检查')}\n`;
|
|
73
|
+
} catch { return output; }
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
function installationSnapshot() {
|
|
52
77
|
const recordPath = path.join(serviceRoot, 'installation.json');
|
|
53
78
|
if (!fs.existsSync(serviceRoot)) return { exists: false, valid: false };
|
|
@@ -95,15 +120,18 @@ function skillFailureGuidance(previous, current) {
|
|
|
95
120
|
return `${service};Codex Skill 安装失败。\n请执行 relax skill-install,再执行 relax doctor;无需重新卸载服务。`;
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
|
|
123
|
+
const invokedDirectly = process.argv[1]
|
|
124
|
+
&& path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
125
|
+
if (invokedDirectly && process.env.npm_config_global === 'true') {
|
|
99
126
|
const entry = fileURLToPath(new URL('../dist/bin/codex-remote.mjs', import.meta.url));
|
|
100
127
|
const previous = installationSnapshot();
|
|
101
128
|
let stage = 'setup';
|
|
102
129
|
appendLog(`[postinstall] start package=${process.env.npm_package_version || 'unknown'} ${systemSummary()}`);
|
|
103
130
|
try {
|
|
104
|
-
runStage(entry, stage, ['setup', '--json']);
|
|
131
|
+
runStage(entry, stage, ['setup', '--json'], output => formatSetupOutput(output, previous));
|
|
105
132
|
stage = 'skill-install';
|
|
106
|
-
runStage(entry, stage, ['skill-install', '--json']);
|
|
133
|
+
runStage(entry, stage, ['skill-install', '--json'], formatSkillOutput);
|
|
134
|
+
console.log(`安装日志:${installLog}`);
|
|
107
135
|
appendLog('[postinstall] success');
|
|
108
136
|
} catch (error) {
|
|
109
137
|
const current = installationSnapshot();
|