@walkhi/code-relax 0.1.0-beta.80 → 0.1.0-beta.81

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.80"}
1
+ {"layout":"npm-platform-packages","version":"0.1.0-beta.81"}
@@ -0,0 +1,41 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { startDesktop } from './desktop-launcher.mjs';
3
+
4
+ // The request is acknowledged before native close/start can outlive the phone's
5
+ // request deadline. Health carries the same receipt; a lost reply is not a retry.
6
+ export class DesktopOperation {
7
+ constructor({ stateRoot, host, sample, launch = startDesktop, wait = delay, now = Date.now, timeout = 30000 }) {
8
+ Object.assign(this, { stateRoot, host, sample, launch, wait, now, timeout });
9
+ this.record = null;
10
+ }
11
+ snapshot() { return this.record ? { ...this.record } : null; }
12
+ request({ operationId, restart }) {
13
+ if (this.record?.operationId === operationId) return this.snapshot();
14
+ if (this.record?.status === 'running') {
15
+ throw Object.assign(new Error('桌面端操作仍在进行,请刷新状态核对结果。'), { statusCode: 409 });
16
+ }
17
+ this.record = { operationId, restart, status: 'running', startedAt: new Date(this.now()).toISOString() };
18
+ this.completion = this.run(this.record);
19
+ return this.snapshot();
20
+ }
21
+ async run(record) {
22
+ let failure;
23
+ try {
24
+ await this.launch({ stateRoot: this.stateRoot, host: this.host, restart: record.restart });
25
+ const deadline = this.now() + this.timeout;
26
+ while (!await this.host.desktopReady()) {
27
+ if (this.now() >= deadline) throw new Error('桌面端启动请求已执行,但未确认接入共享内核,请查看工作站诊断日志。');
28
+ await this.wait(500);
29
+ }
30
+ } catch (error) { failure = error; }
31
+ // Publish the final receipt with a fresh desktop sample, including failures.
32
+ try { await this.sample(); }
33
+ catch (error) { failure ||= error; }
34
+ Object.assign(record, { status: failure ? 'failed' : 'completed', finishedAt: new Date(this.now()).toISOString() });
35
+ if (failure) {
36
+ record.error = failure.message;
37
+ console.warn(JSON.stringify({ event: 'desktop-operation-result', operationId: record.operationId,
38
+ status: record.status, message: failure.message }));
39
+ }
40
+ }
41
+ }
@@ -37,14 +37,17 @@ export function prepareNativePackage(packageRoot, { platform = process.platform,
37
37
  const pkg = readJson(path.join(packageRoot, 'package.json'));
38
38
  if (build.layout !== 'npm-platform-packages' || build.version !== pkg.version)
39
39
  throw new Error('安装包构建与当前系统、架构或版本不匹配,请重新安装。');
40
+ const nativeVersion = pkg.optionalDependencies?.[target.packageName];
41
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(nativeVersion || ''))
42
+ throw new Error('安装包未锁定当前平台原生组件的精确版本。');
40
43
  let manifest;
41
44
  try { manifest = createRequire(path.join(packageRoot, 'package.json')).resolve(`${target.packageName}/package.json`); }
42
45
  catch (error) {
43
46
  if (error.code !== 'MODULE_NOT_FOUND') throw error;
44
- throw new Error(`缺少 ${target.packageName}@${pkg.version};请勿省略 optional 依赖,使用 npm install -g @walkhi/code-relax@${pkg.version} --include=optional --allow-scripts=@walkhi/code-relax --foreground-scripts 重新安装。`);
47
+ throw new Error(`缺少 ${target.packageName}@${nativeVersion};请勿省略 optional 依赖,使用 npm install -g @walkhi/code-relax@${pkg.version} --include=optional --allow-scripts=@walkhi/code-relax --foreground-scripts 重新安装。`);
45
48
  }
46
49
  const native = readJson(manifest);
47
- if (native.name !== target.packageName || native.version !== pkg.version
50
+ if (native.name !== target.packageName || native.version !== nativeVersion
48
51
  || native.os?.length !== 1 || native.os[0] !== platform || native.cpu?.length !== 1 || native.cpu[0] !== arch)
49
52
  throw new Error('原生组件包的系统、架构或版本不匹配,未更新工作站。');
50
53
  const source = path.join(path.dirname(manifest), 'native', target.file);
@@ -39,7 +39,7 @@ import { DotsMessageImages } from './dots-message-images.mjs';
39
39
  import { desktopDotsProfiles } from './platform/windows/dots-profile.mjs';
40
40
  import { readPlanPricing } from './plan-pricing.mjs';
41
41
  import { requestRecovery, recoveryStatus } from './shared-recovery.mjs';
42
- import { startDesktop } from './desktop-launcher.mjs';
42
+ import { DesktopOperation } from './desktop-operation.mjs';
43
43
  import { requestServiceRestart } from './service-restart.mjs';
44
44
  import { checkServiceUpdate, requestServiceUpdate, serviceUpdateStatus } from './service-update.mjs';
45
45
  import { TurnModelHistory } from './turn-model-history.mjs';
@@ -128,6 +128,7 @@ const dotsChat = new DotsChatServer(dotsServer, undefined, { uploadsRoot });
128
128
  managedAppServer.on('state', state => { if (state.ready) dotsChat.startBackgroundActivity(); });
129
129
  const runtimeDiscovery = new RuntimeDiscovery({ stateRoot, host: desktopHost,
130
130
  url: () => managedAppServer.snapshot().url, diagnose: runtimeDiagnostic });
131
+ const desktopOperation = new DesktopOperation({ stateRoot, host: desktopHost, sample: () => runtimeDiscovery.sample() });
131
132
  const relayManager = createRelayManager(stateRoot);
132
133
  let sharedCatalog = null, executionMode = 'ordinary';
133
134
  function selectSharedExecution(enabled) {
@@ -159,6 +160,7 @@ function workstationHealth() {
159
160
  mode: executionMode },
160
161
  managedAppServer: managedAppServer.snapshot(),
161
162
  ...runtimeDiscovery.state,
163
+ desktopOperation: desktopOperation.snapshot(),
162
164
  sharedAppServerConnected: sharedServer?.ready === true,
163
165
  sharedRecoveryAvailable: runtimeDiscovery.state.desktop?.installed === true,
164
166
  mode: options.lan ? 'lan' : 'loopback',
@@ -197,8 +199,10 @@ async function handleRequest(request, response) {
197
199
  return;
198
200
  }
199
201
 
200
- if (process.platform !== 'win32' && url.pathname.startsWith('/api/desktop/'))
201
- throw new RequestError(501, 'Mac 版不提供远程桌面控制。', { code: 'desktop_control_unsupported' });
202
+ const remoteDesktopControl = ['/api/desktop/video-probe', '/api/desktop/frame', '/api/desktop/move', '/api/desktop/click'].includes(url.pathname)
203
+ || url.pathname.startsWith('/api/desktop/frames/');
204
+ if (process.platform !== 'win32' && remoteDesktopControl)
205
+ throw new RequestError(501, '此平台不提供远程桌面控制。', { code: 'desktop_control_unsupported' });
202
206
 
203
207
  if (url.pathname === '/api/desktop/video-probe') {
204
208
  if (request.method === 'POST') {
@@ -291,11 +295,13 @@ async function handleRequest(request, response) {
291
295
  }
292
296
  if (request.method === 'POST' && ['/api/desktop/start', '/api/desktop/restart'].includes(url.pathname)) {
293
297
  const body = await readJsonBody(request);
294
- if (body.confirmed !== true) throw new RequestError(400, '请确认启动桌面端。');
295
- let result, observed;
296
- try { result = await startDesktop({ stateRoot, host: desktopHost, restart: url.pathname === '/api/desktop/restart' }); }
297
- finally { observed = await runtimeDiscovery.sample(); }
298
- sendJson(response, 202, { ...result, desktop: observed.desktop, checkedAt: observed.checkedAt }); return;
298
+ if (body.confirmed !== true || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(body.operationId || ''))
299
+ throw new RequestError(400, '请确认启动桌面端并提供操作编号。');
300
+ try {
301
+ sendJson(response, 202, desktopOperation.request({ operationId: body.operationId,
302
+ restart: url.pathname === '/api/desktop/restart' }));
303
+ } catch (error) { throw new RequestError(error.statusCode || 502, error.message); }
304
+ return;
299
305
  }
300
306
  if (request.method === 'POST' && url.pathname === '/api/shared-recovery') {
301
307
  const body = await readJsonBody(request);
@@ -346,6 +352,7 @@ async function handleRequest(request, response) {
346
352
  }
347
353
 
348
354
  if (request.method === 'GET' && url.pathname === '/api/health') {
355
+ if (url.searchParams.get('refresh') === 'desktop') await runtimeDiscovery.sample();
349
356
  sendJson(response, 200, workstationHealth()); return;
350
357
  }
351
358
  if (request.method === 'GET' && url.pathname === '/api/health/events') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkhi/code-relax",
3
- "version": "0.1.0-beta.80",
3
+ "version": "0.1.0-beta.81",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",