@walkhi/code-relax 0.1.0-beta.44 → 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.
@@ -1 +1 @@
1
- {"layout":"npm-platform-packages","version":"0.1.0-beta.44"}
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;
@@ -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.44",
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.44",
45
- "@walkhi/code-relax-native-darwin-arm64": "0.1.0-beta.44",
46
- "@walkhi/code-relax-native-darwin-x64": "0.1.0-beta.44"
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
  }