@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.
@@ -224,7 +224,8 @@ async function main() {
224
224
  return;
225
225
  }
226
226
  if (command === 'doctor' || command === 'diag') {
227
- const result = await doctor({ paths, host, service, ensureAutostart, diagnostics: command === 'diag' });
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;
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.43"}
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
- if ($request -and (Test-Path -LiteralPath $request)) { Remove-Item -LiteralPath $request -Force }
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
 
@@ -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' && url.pathname === '/api/shared-recovery') {
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 async function doctor({ paths, host, service, skillTarget, ensureAutostart, diagnostics = false }) {
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('尚未完整部署服务,请重新安装 Code Relax。');
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', 'warning', state.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 ? { operationId: record.operationId, targetVersion: record.targetVersion,
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 error;
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
- stage('stopping-desktop'); await host.closeDesktop(record.desktop);
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.43",
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.43",
45
- "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.43",
46
- "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.43"
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
  }
@@ -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 || '未知版本'} 服务安装完成。\n`
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 { runStage(entry, 'doctor', ['doctor']); }
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');