@walkhi/code-relax 0.1.0-beta.43 → 0.1.0-beta.45
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/dist/bin/codex-remote.mjs +2 -1
- package/dist/build-platform.json +1 -1
- package/dist/src/platform/windows/start-hidden-console.ps1 +11 -2
- package/dist/src/self-relay/server.mjs +1 -1
- package/dist/src/server.mjs +2 -2
- package/dist/src/service-doctor.mjs +17 -6
- package/dist/src/service-update.mjs +9 -3
- package/dist/src/shared-recovery.mjs +6 -3
- package/package.json +4 -4
- package/tools/postinstall.mjs +7 -3
|
@@ -224,7 +224,8 @@ async function main() {
|
|
|
224
224
|
return;
|
|
225
225
|
}
|
|
226
226
|
if (command === 'doctor' || command === 'diag') {
|
|
227
|
-
const
|
|
227
|
+
const cliVersion = readRecord(path.join(packageRoot(path.dirname(fileURLToPath(import.meta.url))), 'package.json'))?.version;
|
|
228
|
+
const result = await doctor({ paths, host, service, ensureAutostart, cliVersion, diagnostics: command === 'diag' });
|
|
228
229
|
console.log(json ? JSON.stringify(result) : formatDoctorOutput(result, command === 'diag'));
|
|
229
230
|
if (!result.ready) process.exitCode = 1;
|
|
230
231
|
return;
|
package/dist/build-platform.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"layout":"npm-platform-packages","version":"0.1.0-beta.
|
|
1
|
+
{"layout":"npm-platform-packages","version":"0.1.0-beta.45"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
$ErrorActionPreference = 'Stop'
|
|
2
2
|
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
3
3
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
+
$brokerStarted = $false
|
|
4
5
|
try {
|
|
5
6
|
$spec = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
6
7
|
# WMI owns this launch, not the caller's kill-on-close Desktop job hierarchy.
|
|
@@ -13,10 +14,15 @@ try {
|
|
|
13
14
|
$request = Join-Path $launchRoot ($instance + '.request.json')
|
|
14
15
|
$diagnostic = Join-Path $launchRoot ($instance + '.broker.log')
|
|
15
16
|
$spec | Add-Member -NotePropertyName instance -NotePropertyValue $instance
|
|
16
|
-
$spec | Add-Member -NotePropertyName diagnostic -NotePropertyValue $diagnostic
|
|
17
17
|
$requestTemporary = $request + '.tmp'
|
|
18
18
|
[IO.File]::WriteAllText($requestTemporary, ($spec | ConvertTo-Json -Compress -Depth 10), (New-Object Text.UTF8Encoding($false)))
|
|
19
19
|
[IO.File]::Move($requestTemporary, $request)
|
|
20
|
+
# MSIX can redirect a newly written file even when its existing parent
|
|
21
|
+
# resolves outside the package. Resolve the file handle, not just the folder.
|
|
22
|
+
$request = [IsolatedProcess]::ResolvePath($request)
|
|
23
|
+
$launchRoot = [IO.Path]::GetDirectoryName($request)
|
|
24
|
+
$ready = Join-Path $launchRoot ($instance + '.json')
|
|
25
|
+
$diagnostic = Join-Path $launchRoot ($instance + '.broker.log')
|
|
20
26
|
$startup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{
|
|
21
27
|
ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
|
|
22
28
|
}
|
|
@@ -26,6 +32,7 @@ try {
|
|
|
26
32
|
CommandLine=$command; CurrentDirectory=$env:USERPROFILE; ProcessStartupInformation=$startup
|
|
27
33
|
}
|
|
28
34
|
if ($broker.ReturnValue -ne 0) { throw ('Windows process broker failed: ' + $broker.ReturnValue) }
|
|
35
|
+
$brokerStarted = $true
|
|
29
36
|
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
|
30
37
|
while (-not (Test-Path -LiteralPath $ready)) {
|
|
31
38
|
if ([DateTime]::UtcNow -gt $deadline) {
|
|
@@ -44,6 +51,8 @@ try {
|
|
|
44
51
|
[Console]::Error.WriteLine($_.Exception.Message)
|
|
45
52
|
exit 1
|
|
46
53
|
} finally {
|
|
47
|
-
|
|
54
|
+
# Once launched, the worker owns consumption. A slow broker must not lose
|
|
55
|
+
# its request merely because this caller timed out; never launch a retry.
|
|
56
|
+
if (-not $brokerStarted -and $request -and (Test-Path -LiteralPath $request)) { Remove-Item -LiteralPath $request -Force }
|
|
48
57
|
if ($requestTemporary -and (Test-Path -LiteralPath $requestTemporary)) { Remove-Item -LiteralPath $requestTemporary -Force }
|
|
49
58
|
}
|
|
@@ -989,7 +989,7 @@ function controlPayload(value) {
|
|
|
989
989
|
const message = JSON.parse(value);
|
|
990
990
|
if (message.type === 'unsubscribe') return true;
|
|
991
991
|
return ['request', 'subscribe'].includes(message.type) && typeof message.path === 'string' &&
|
|
992
|
-
(message.path === '/api/health' || message.path.startsWith('/api/shared-recovery'));
|
|
992
|
+
(message.path === '/api/health' || message.path === '/api/desktop/start' || message.path.startsWith('/api/shared-recovery'));
|
|
993
993
|
} catch { return false; }
|
|
994
994
|
}
|
|
995
995
|
|
package/dist/src/server.mjs
CHANGED
|
@@ -261,10 +261,10 @@ async function handleRequest(request, response) {
|
|
|
261
261
|
if (request.method === 'GET' && url.pathname === '/api/shared-recovery') {
|
|
262
262
|
sendJson(response, 200, await recoveryStatus(stateRoot)); return;
|
|
263
263
|
}
|
|
264
|
-
if (request.method === 'POST' &&
|
|
264
|
+
if (request.method === 'POST' && ['/api/shared-recovery', '/api/desktop/start'].includes(url.pathname)) {
|
|
265
265
|
const body = await readJsonBody(request);
|
|
266
266
|
if (body.confirmed !== true || typeof body.operationId !== 'string') throw new RequestError(400, '请确认重新打开当前 Codex 并接入 Codex 内核后再继续。');
|
|
267
|
-
sendJson(response, 202, await requestRecovery(stateRoot, body)); return;
|
|
267
|
+
sendJson(response, 202, await requestRecovery(stateRoot, { ...body, startOnly: url.pathname === '/api/desktop/start' })); return;
|
|
268
268
|
}
|
|
269
269
|
if (url.pathname === '/api/service/update' && request.method === 'GET') {
|
|
270
270
|
sendJson(response, 200, await checkServiceUpdate(stateRoot)); return;
|
|
@@ -5,7 +5,14 @@ import { readRecord } from './service-lifecycle.mjs';
|
|
|
5
5
|
import { inspectSkill } from './skill-install.mjs';
|
|
6
6
|
import { recoveryStatus } from './shared-recovery.mjs';
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export function packageDeploymentCheck(cliVersion, installedVersion) {
|
|
9
|
+
if (!cliVersion) return { id: 'package-version', status: 'warning', message: '无法读取当前 CLI 包版本,未确认本次部署版本。' };
|
|
10
|
+
if (cliVersion !== installedVersion) return { id: 'package-version', status: 'error',
|
|
11
|
+
message: `当前 CLI 包为 ${cliVersion},固定服务为 ${installedVersion || '未确认'};本次包尚未完整部署。安装脚本可能未执行或执行失败,不能仅凭 npm 警告判断。请查看 install.log;确认允许部署并中断相关任务后,执行 relax setup,再运行 relax doctor。无需放开全局 npm 脚本权限。` };
|
|
12
|
+
return { id: 'package-version', status: 'ok', message: `CLI 与固定服务版本一致:${cliVersion}。这不单独证明本次 postinstall 执行过;当前可用性以下方服务和内核检查为准。` };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function doctor({ paths, host, service, skillTarget, ensureAutostart, cliVersion, diagnostics = false }) {
|
|
9
16
|
const checks = [];
|
|
10
17
|
let installedVersion;
|
|
11
18
|
let kernelUpdate = null;
|
|
@@ -17,11 +24,12 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
|
|
|
17
24
|
};
|
|
18
25
|
await check('installation', async () => {
|
|
19
26
|
const record = readRecord(path.join(paths.serviceRoot, 'installation.json'));
|
|
20
|
-
if (!record || !fs.existsSync(path.join(paths.serviceRoot, 'dist/bin/codex-remote.mjs'))) throw Error('
|
|
27
|
+
if (!record || !fs.existsSync(path.join(paths.serviceRoot, 'dist/bin/codex-remote.mjs'))) throw Error('尚未完整部署服务;安装脚本可能未执行或执行失败。请查看 install.log,确认部署后执行 relax setup,再运行 relax doctor;不能仅凭 npm 警告判断原因。');
|
|
21
28
|
if (!record.nodeExecutable || !fs.existsSync(record.nodeExecutable)) throw Error('记录的 Node 已失效,请用可用的 Node 重新安装 Code Relax。');
|
|
22
29
|
installedVersion = record.version;
|
|
23
30
|
add('installation', 'ok', `已安装版本:${record.version};Node:${record.nodeExecutable}`);
|
|
24
31
|
});
|
|
32
|
+
checks.push(packageDeploymentCheck(cliVersion, installedVersion));
|
|
25
33
|
await check('bridge-runtime', async () => {
|
|
26
34
|
await host.bridgeEnvironment();
|
|
27
35
|
add('bridge-runtime', 'ok', '桌面服务启动环境正常。');
|
|
@@ -46,9 +54,12 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
|
|
|
46
54
|
}
|
|
47
55
|
add('service', state.running ? 'ok' : 'error', state.running
|
|
48
56
|
? `本机服务正常:${state.mode},端口 ${state.port},运行版本:${state.version || '未知'}` : '服务未就绪;安装后执行 relax start。');
|
|
49
|
-
if (state.running && installedVersion && state.version !== installedVersion) add('service-version', '
|
|
57
|
+
if (state.running && installedVersion && state.version !== installedVersion) add('service-version', 'error', state.version
|
|
50
58
|
? `已安装版本 ${installedVersion},但当前运行版本为 ${state.version};请重新执行安装或重启工作站服务。`
|
|
51
|
-
: `已安装版本 ${installedVersion}
|
|
59
|
+
: `已安装版本 ${installedVersion},但无法确认运行服务版本;请保留诊断结果核查,不能据此认定旧进程或自动重启。`);
|
|
60
|
+
if (state.running) add('execution', state.execution?.ready === true && state.execution?.canWrite === true ? 'ok' : 'error',
|
|
61
|
+
state.execution?.ready === true && state.execution?.canWrite === true
|
|
62
|
+
? 'Codex 完整控制已就绪且可写。' : 'Codex 完整控制尚未确认就绪且可写;请查看内核和连接诊断,不能仅凭服务在线判定健康。');
|
|
52
63
|
const currentVersion = state.managedAppServer?.version || null;
|
|
53
64
|
const selectedVersion = state.managedAppServer?.selectedVersion || null;
|
|
54
65
|
if (state.managedAppServer) kernelUpdate = { pending: state.managedAppServer.updatePending === true,
|
|
@@ -86,7 +97,7 @@ export async function doctor({ paths, host, service, skillTarget, ensureAutostar
|
|
|
86
97
|
export function formatDoctorOutput(result, detail = false) {
|
|
87
98
|
const labels = { 'keep-awake': '保持唤醒', installation: '安装文件', 'bridge-runtime': '运行环境', network: '局域网',
|
|
88
99
|
autostart: '登录自启动', service: '工作站服务', 'managed-app-server': 'Codex 内核',
|
|
89
|
-
'service-version': '服务版本', 'managed-app-server-update': 'Codex 更新',
|
|
100
|
+
execution: '完整控制', 'package-version': '安装包版本', 'service-version': '服务版本', 'managed-app-server-update': 'Codex 更新',
|
|
90
101
|
'managed-app-server-launcher': '工作站更新', skill: 'Codex Skill' };
|
|
91
102
|
const byId = id => result.checks.find(item => item.id === id);
|
|
92
103
|
const summary = (id, success) => {
|
|
@@ -95,7 +106,7 @@ export function formatDoctorOutput(result, detail = false) {
|
|
|
95
106
|
};
|
|
96
107
|
const issues = result.checks.filter(item => item.status !== 'ok');
|
|
97
108
|
const lines = [result.ready ? '工作站检查通过。' : '工作站检查发现问题。',
|
|
98
|
-
summary('installation', '完整'), summary('service', '正在运行'), summary('managed-app-server', '已就绪')];
|
|
109
|
+
summary('installation', '完整'), summary('package-version', 'CLI 与固定服务一致'), summary('service', '正在运行'), summary('managed-app-server', '已就绪'), summary('execution', '已就绪且可写')];
|
|
99
110
|
if (issues.length) lines.push('', '需要处理或留意:', ...issues.map(item => `- ${labels[item.id] || item.id}:${item.message}`));
|
|
100
111
|
if (detail) {
|
|
101
112
|
const info = result.diagnostics || {};
|
|
@@ -9,13 +9,19 @@ import { desktopHost } from './platform/desktop-host.mjs';
|
|
|
9
9
|
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
10
10
|
const statusPath = root => path.join(root, 'service-update.json');
|
|
11
11
|
const installation = () => readRecord(path.join(runtimePaths().serviceRoot, 'installation.json'));
|
|
12
|
+
export function publicUpdateOperation(record, installedVersion) {
|
|
13
|
+
if (!record) return null;
|
|
14
|
+
return { operationId: record.operationId, targetVersion: record.targetVersion,
|
|
15
|
+
status: record.status, targetInstalled: Boolean(installedVersion && installedVersion === record.targetVersion),
|
|
16
|
+
error: record.status === 'failed' ? '上次更新未完整完成,请查看工作站更新日志。' : null,
|
|
17
|
+
installedVersion: record.installedVersion || null };
|
|
18
|
+
}
|
|
12
19
|
export function serviceUpdateStatus(root) {
|
|
13
20
|
const record = readRecord(statusPath(root));
|
|
14
21
|
const installed = installation();
|
|
15
22
|
return { supported: Boolean(installed?.npmCliPath && fs.existsSync(installed.npmCliPath)),
|
|
16
23
|
currentVersion: installed?.version || null,
|
|
17
|
-
operation: record
|
|
18
|
-
status: record.status, error: record.error || null, installedVersion: record.installedVersion || null } : null };
|
|
24
|
+
operation: publicUpdateOperation(record, installed?.version) };
|
|
19
25
|
}
|
|
20
26
|
export async function checkServiceUpdate(root) {
|
|
21
27
|
const result = await fetch('https://registry.npmjs.org/@walkhi%2fcode-relax/latest', { signal: AbortSignal.timeout(12000) });
|
|
@@ -64,7 +70,7 @@ export async function requestServiceUpdate(root, body) {
|
|
|
64
70
|
pid: owner.pid, created: owner.created, executable: owner.executable } });
|
|
65
71
|
} catch (error) {
|
|
66
72
|
writeRecord(statusPath(root), { ...readRecord(statusPath(root)), status: 'failed', error: error.message });
|
|
67
|
-
throw
|
|
73
|
+
throw new Error('工作站更新启动未完成,请查看工作站更新日志;结果确认前不要重复提交。');
|
|
68
74
|
}
|
|
69
75
|
return serviceUpdateStatus(root);
|
|
70
76
|
} finally { scheduling = false; }
|
|
@@ -51,7 +51,7 @@ export async function planRecovery(stateRoot, host, available = portAvailable) {
|
|
|
51
51
|
return { desktop: info.desktop, action };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export async function requestRecovery(stateRoot, { confirmed, operationId = randomUUID() } = {}, host = desktopHost) {
|
|
54
|
+
export async function requestRecovery(stateRoot, { confirmed, operationId = randomUUID(), startOnly = false } = {}, host = desktopHost) {
|
|
55
55
|
if (host.desktopIntegration === false) throw new Error('此平台尚不支持 Desktop 自动接入。');
|
|
56
56
|
if (confirmed !== true) throw new Error(recoveryWarning + ' 请明确确认后再执行。');
|
|
57
57
|
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(operationId)) throw new Error('无效的恢复操作编号。');
|
|
@@ -60,12 +60,13 @@ export async function requestRecovery(stateRoot, { confirmed, operationId = rand
|
|
|
60
60
|
if (previous.operationId === operationId) return previous;
|
|
61
61
|
if (active(previous)) throw new Error('已有恢复操作正在执行,请查看恢复状态。');
|
|
62
62
|
const planned = await planRecovery(stateRoot, host);
|
|
63
|
+
if (startOnly && (await host.inspect()).running) throw new Error('Codex 桌面端已经启动,未执行重启。');
|
|
63
64
|
let lock;
|
|
64
65
|
try { lock = fs.openSync(lockPath(stateRoot), 'wx'); }
|
|
65
66
|
catch (error) { if (error.code === 'EEXIST') throw new Error('已有恢复操作,请先查看恢复状态。'); throw error; }
|
|
66
67
|
fs.writeFileSync(lock, JSON.stringify({ operationId })); fs.closeSync(lock);
|
|
67
68
|
const record = { operationId, status: 'scheduled', stage: 'prepared', startedAt: new Date().toISOString(),
|
|
68
|
-
message: 'Codex 连接请求已接受,电脑管理连接保持运行。', steps: recoverySteps(), ...planned };
|
|
69
|
+
message: 'Codex 连接请求已接受,电脑管理连接保持运行。', startOnly, steps: recoverySteps().filter(step => !startOnly || step.id !== 'stopping-desktop'), ...planned };
|
|
69
70
|
try {
|
|
70
71
|
writeRecord(recordPath(stateRoot), record);
|
|
71
72
|
const log = path.join(stateRoot, 'shared-recovery.log');
|
|
@@ -103,7 +104,9 @@ export async function runRecovery(stateRoot, operationId, dependencies = {}) {
|
|
|
103
104
|
const planned = await planRecovery(stateRoot, host, available);
|
|
104
105
|
record = { ...record, ...planned };
|
|
105
106
|
if (fs.existsSync(path.join(stateRoot, 'desktop-launch.lock'))) throw new Error('Desktop 正在启动,未与启动操作并行恢复。');
|
|
106
|
-
|
|
107
|
+
if (record.startOnly) {
|
|
108
|
+
if ((await host.inspect()).running) throw new Error('Codex 桌面端已经启动,未执行重启。');
|
|
109
|
+
} else { stage('stopping-desktop'); await host.closeDesktop(record.desktop); }
|
|
107
110
|
await waitFor(async () => !(await host.inspect()).running, '当前 Codex Desktop 未退出,未继续接入 Codex 内核。');
|
|
108
111
|
stage('starting-app-server'); await start({ stateRoot, host, recoveryId: operationId, progress: stage });
|
|
109
112
|
stage('checking-execution');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walkhi/code-relax",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.45",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"tag": "beta"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.
|
|
45
|
-
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.
|
|
46
|
-
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.
|
|
44
|
+
"@walkhi/code-relax-native-win32-x64": "0.1.0-beta.45",
|
|
45
|
+
"@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.45",
|
|
46
|
+
"@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.45"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/tools/postinstall.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export function formatSetupOutput(output) {
|
|
|
53
53
|
try {
|
|
54
54
|
const result = JSON.parse(output);
|
|
55
55
|
const migration = result.managedAppServerMigration || {};
|
|
56
|
-
return `Code Relax ${result.version || '未知版本'}
|
|
56
|
+
return `Code Relax ${result.version || '未知版本'} setup 已完成;接下来执行安装后健康检查。\n`
|
|
57
57
|
+ '工作站服务和 Codex 内核已就绪,已启用登录自启。\n'
|
|
58
58
|
+ (result.preserveCodex ? '过渡版保留当前 Codex 和内核运行状态;待手机发起下一次完整更新。\n' : migration.restarted ? `Codex 内核已更新:${migration.previousVersion || '未知'} → ${result.managedAppServer?.version || '未知'}(PID ${migration.previousPid} → ${migration.currentPid})。\n` : 'Codex 内核已是当前选定版本,无需切换。\n')
|
|
59
59
|
+ (result.preserveCodex ? '' : result.desktopRestarted ? '电脑版 Codex 已重启,并确认连接到工作站内核。\n' : '安装前未运行电脑版 Codex,保持关闭。\n')
|
|
@@ -152,10 +152,14 @@ if (invokedDirectly && process.env.npm_config_global === 'true') {
|
|
|
152
152
|
prepareNativePackage(fileURLToPath(new URL('../', import.meta.url)));
|
|
153
153
|
setupStarted = true;
|
|
154
154
|
runStage(entry, 'setup', ['setup', '--json'], output => formatSetupOutput(output));
|
|
155
|
-
try {
|
|
155
|
+
try {
|
|
156
|
+
runStage(entry, 'doctor', ['doctor']);
|
|
157
|
+
console.log('Code Relax 自动安装成功:setup 与安装后健康检查均已通过,无需重复执行 setup。');
|
|
158
|
+
console.log('npm 的脚本安全提示与安装结果分别判断;本次安装脚本已实际执行,未修改 npm 安全配置。');
|
|
159
|
+
}
|
|
156
160
|
catch (error) {
|
|
157
161
|
console.error('服务安装已完成,但安装后健康检查未通过;请查看上方诊断与安装日志。');
|
|
158
|
-
process.exitCode = 1;
|
|
162
|
+
process.exitCode = Number.isInteger(error?.status) && error.status > 0 ? error.status : 1;
|
|
159
163
|
}
|
|
160
164
|
console.log(`安装日志:${installLog}`);
|
|
161
165
|
appendLog(process.exitCode ? '[postinstall] failed stage=doctor' : '[postinstall] success');
|