@walkhi/code-relax 0.1.0-beta.66 → 0.1.0-beta.68

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.
Files changed (120) hide show
  1. package/dist/bin/codex-remote.mjs +4 -2
  2. package/dist/bin/manage-relay-admin.mjs +20 -20
  3. package/dist/bin/migrate-relay-db.mjs +109 -109
  4. package/dist/bin/publish-harmony-release.mjs +46 -46
  5. package/dist/bin/self-relay-admin.mjs +8 -8
  6. package/dist/bin/self-relay-demo.mjs +33 -33
  7. package/dist/bin/self-relay-server.mjs +66 -66
  8. package/dist/bin/set-relay-admin-password.mjs +11 -11
  9. package/dist/build-platform.json +1 -1
  10. package/dist/get-lan-networks.ps1 +40 -40
  11. package/dist/shared/app-server-events.cjs +511 -511
  12. package/dist/shared/p2p-data-channel.cjs +72 -72
  13. package/dist/shared/p2p-diagnostics.cjs +28 -28
  14. package/dist/shared/p2p-ice-policy.cjs +22 -22
  15. package/dist/shared/plan-pricing.cjs +32 -32
  16. package/dist/shared/question-attention.cjs +69 -69
  17. package/dist/shared/release-version.cjs +31 -31
  18. package/dist/src/archived-threads.mjs +22 -22
  19. package/dist/src/background-process.mjs +18 -18
  20. package/dist/src/code-browser.mjs +100 -100
  21. package/dist/src/desktop-launcher.mjs +62 -62
  22. package/dist/src/dots-task-files.mjs +52 -52
  23. package/dist/src/execution-health.mjs +26 -26
  24. package/dist/src/message-images.mjs +9 -9
  25. package/dist/src/plan-pricing.mjs +47 -47
  26. package/dist/src/platform/codex-runtime.mjs +3 -3
  27. package/dist/src/platform/credential-store.mjs +19 -19
  28. package/dist/src/platform/desktop-host.mjs +3 -3
  29. package/dist/src/platform/desktop-permission-mode.mjs +17 -17
  30. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  31. package/dist/src/platform/file-opener.mjs +9 -9
  32. package/dist/src/platform/linux/README.md +3 -3
  33. package/dist/src/platform/linux/codex-runtime.mjs +56 -56
  34. package/dist/src/platform/linux/desktop-installation.mjs +36 -36
  35. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
  36. package/dist/src/platform/linux/network-info.mjs +11 -11
  37. package/dist/src/platform/linux/project-locations.mjs +17 -17
  38. package/dist/src/platform/linux/resident-startup.mjs +52 -52
  39. package/dist/src/platform/linux/retained-runtime.mjs +76 -76
  40. package/dist/src/platform/linux/service-host.mjs +18 -18
  41. package/dist/src/platform/macos/README.md +3 -3
  42. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
  43. package/dist/src/platform/macos/network-info.mjs +8 -8
  44. package/dist/src/platform/macos/project-locations.mjs +8 -8
  45. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  46. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  47. package/dist/src/platform/macos/service-host.mjs +18 -18
  48. package/dist/src/platform/macos/system.mjs +29 -29
  49. package/dist/src/platform/network-info.mjs +3 -3
  50. package/dist/src/platform/path-identity.mjs +5 -5
  51. package/dist/src/platform/posix/network-candidates.mjs +23 -23
  52. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  53. package/dist/src/platform/power-inhibitor.mjs +22 -22
  54. package/dist/src/platform/project-locations.mjs +3 -3
  55. package/dist/src/platform/runtime-paths.mjs +8 -8
  56. package/dist/src/platform/select.mjs +5 -5
  57. package/dist/src/platform/service-host.mjs +3 -3
  58. package/dist/src/platform/user-environment.mjs +3 -3
  59. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  60. package/dist/src/platform/windows/background-process.mjs +30 -30
  61. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  62. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  63. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
  64. package/dist/src/platform/windows/keep-awake.ps1 +20 -20
  65. package/dist/src/platform/windows/network-info.mjs +34 -34
  66. package/dist/src/platform/windows/resident-start.js +20 -20
  67. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  68. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  69. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  70. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  71. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  72. package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
  73. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  74. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  75. package/dist/src/project-create.mjs +121 -121
  76. package/dist/src/project-management.mjs +1 -1
  77. package/dist/src/project-remove.mjs +55 -55
  78. package/dist/src/project-update.mjs +59 -59
  79. package/dist/src/relay-config-import.mjs +45 -45
  80. package/dist/src/self-relay/account-connections.mjs +52 -52
  81. package/dist/src/self-relay/admin-auth.mjs +59 -59
  82. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  83. package/dist/src/self-relay/admin-login-page.mjs +7 -7
  84. package/dist/src/self-relay/admin-server.mjs +105 -105
  85. package/dist/src/self-relay/admin-state.mjs +127 -127
  86. package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
  87. package/dist/src/self-relay/admin-workstations.mjs +143 -143
  88. package/dist/src/self-relay/android-releases.mjs +61 -61
  89. package/dist/src/self-relay/client.mjs +61 -61
  90. package/dist/src/self-relay/connection-reporting.mjs +38 -38
  91. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  92. package/dist/src/self-relay/demo.mjs +6 -6
  93. package/dist/src/self-relay/device-info.mjs +27 -27
  94. package/dist/src/self-relay/device-store.mjs +92 -92
  95. package/dist/src/self-relay/lan-authorizations.mjs +71 -71
  96. package/dist/src/self-relay/lifecycle.mjs +278 -278
  97. package/dist/src/self-relay/long-pairing.mjs +40 -40
  98. package/dist/src/self-relay/mysql-state.mjs +118 -118
  99. package/dist/src/self-relay/phone-connections.mjs +55 -55
  100. package/dist/src/self-relay/server.mjs +1066 -1066
  101. package/dist/src/self-relay/usage-daily.mjs +55 -55
  102. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  103. package/dist/src/self-relay/wire.mjs +36 -36
  104. package/dist/src/server.mjs +1947 -1947
  105. package/dist/src/service-control.mjs +82 -82
  106. package/dist/src/service-health.mjs +33 -33
  107. package/dist/src/service-power.mjs +58 -58
  108. package/dist/src/service-restart.mjs +81 -81
  109. package/dist/src/service-update-worker.mjs +42 -42
  110. package/dist/src/service-update.mjs +80 -80
  111. package/dist/src/shared-recovery.mjs +152 -152
  112. package/dist/src/skill-install.mjs +51 -51
  113. package/dist/src/thread-title-generation.mjs +150 -150
  114. package/dist/src/turn-model-history.mjs +47 -47
  115. package/dist/tools/rtc-route-helper.ps1 +99 -99
  116. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  117. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  118. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  119. package/package.json +4 -4
  120. package/tools/postinstall.mjs +1 -1
@@ -1,82 +1,82 @@
1
- import http from 'node:http';
2
- import { timingSafeEqual } from 'node:crypto';
3
-
4
- function matches(left, right) {
5
- const a = Buffer.from(left || '');
6
- const b = Buffer.from(right || '');
7
- return a.length === b.length && timingSafeEqual(a, b);
8
- }
9
-
10
- // Separate from the LAN listener: mobile credentials never authorize lifecycle operations.
11
- export async function openControl({ instanceId, secret, shutdown, status = () => ({}), actions = {} }) {
12
- const server = http.createServer((request, response) => {
13
- if (!matches(request.headers.authorization, `Bearer ${secret}`)) {
14
- response.writeHead(403).end();
15
- return;
16
- }
17
- const action = Object.keys(actions).find(name => request.url === `/${instanceId}/${name}`);
18
- if ((!action && request.url !== `/${instanceId}`) || !['GET', 'POST'].includes(request.method) || (action && request.method !== 'POST')) {
19
- response.writeHead(404).end();
20
- return;
21
- }
22
- response.setHeader('Content-Type', 'application/json');
23
- response.setHeader('Connection', 'close');
24
- if (action) {
25
- Promise.resolve().then(async () => {
26
- let body = '';
27
- request.setEncoding('utf8');
28
- for await (const chunk of request) {
29
- body += chunk;
30
- if (Buffer.byteLength(body) > 4096) throw new Error('管理请求过大');
31
- }
32
- const details = body ? JSON.parse(body) : {};
33
- if (!details || typeof details !== 'object' || Array.isArray(details)) throw new Error('无效管理请求');
34
- return actions[action](details);
35
- }).then(result => {
36
- response.end(JSON.stringify({ ...result, instanceId, pid: process.pid }));
37
- }).catch(error => { response.writeHead(409).end(JSON.stringify({ error: error.message })); });
38
- return;
39
- }
40
- if (request.method === 'POST') response.once('finish', () => {
41
- Promise.resolve(shutdown()).catch(error => {
42
- console.error('桌面服务关闭失败:', error.message);
43
- process.exitCode = 1;
44
- });
45
- });
46
- response.end(JSON.stringify({ ...status(), instanceId, pid: process.pid }));
47
- });
48
- await new Promise((resolve, reject) => {
49
- server.once('error', reject);
50
- server.listen(0, '127.0.0.1', resolve);
51
- });
52
- return { port: server.address().port, close: () => new Promise((resolve, reject) => {
53
- server.close(error => error ? reject(error) : resolve());
54
- }) };
55
- }
56
-
57
- export function controlRequest(session, method = 'GET', action = '', details) {
58
- return new Promise((resolve, reject) => {
59
- const request = http.request({ hostname: '127.0.0.1', port: session.controlPort,
60
- path: `/${session.instanceId}${action ? '/' + action : ''}`, method, agent: false,
61
- headers: { Authorization: `Bearer ${session.controlSecret}` }, timeout: action ? 25000 : 2000 }, response => {
62
- let body = '';
63
- response.setEncoding('utf8');
64
- response.on('data', chunk => { body += chunk; });
65
- response.on('error', reject);
66
- response.on('end', () => {
67
- try {
68
- if (response.statusCode === 409) throw new Error(JSON.parse(body).error);
69
- if (response.statusCode !== 200) throw new Error('桌面服务管理通道身份验证失败。');
70
- const identity = JSON.parse(body);
71
- if (identity.instanceId !== session.instanceId || identity.pid !== session.pid) {
72
- throw new Error('桌面服务实例身份不匹配。');
73
- }
74
- resolve(identity);
75
- } catch (error) { reject(error); }
76
- });
77
- });
78
- request.on('timeout', () => request.destroy(new Error('桌面服务管理通道响应超时。')));
79
- request.on('error', reject);
80
- request.end(details === undefined ? undefined : JSON.stringify(details));
81
- });
82
- }
1
+ import http from 'node:http';
2
+ import { timingSafeEqual } from 'node:crypto';
3
+
4
+ function matches(left, right) {
5
+ const a = Buffer.from(left || '');
6
+ const b = Buffer.from(right || '');
7
+ return a.length === b.length && timingSafeEqual(a, b);
8
+ }
9
+
10
+ // Separate from the LAN listener: mobile credentials never authorize lifecycle operations.
11
+ export async function openControl({ instanceId, secret, shutdown, status = () => ({}), actions = {} }) {
12
+ const server = http.createServer((request, response) => {
13
+ if (!matches(request.headers.authorization, `Bearer ${secret}`)) {
14
+ response.writeHead(403).end();
15
+ return;
16
+ }
17
+ const action = Object.keys(actions).find(name => request.url === `/${instanceId}/${name}`);
18
+ if ((!action && request.url !== `/${instanceId}`) || !['GET', 'POST'].includes(request.method) || (action && request.method !== 'POST')) {
19
+ response.writeHead(404).end();
20
+ return;
21
+ }
22
+ response.setHeader('Content-Type', 'application/json');
23
+ response.setHeader('Connection', 'close');
24
+ if (action) {
25
+ Promise.resolve().then(async () => {
26
+ let body = '';
27
+ request.setEncoding('utf8');
28
+ for await (const chunk of request) {
29
+ body += chunk;
30
+ if (Buffer.byteLength(body) > 4096) throw new Error('管理请求过大');
31
+ }
32
+ const details = body ? JSON.parse(body) : {};
33
+ if (!details || typeof details !== 'object' || Array.isArray(details)) throw new Error('无效管理请求');
34
+ return actions[action](details);
35
+ }).then(result => {
36
+ response.end(JSON.stringify({ ...result, instanceId, pid: process.pid }));
37
+ }).catch(error => { response.writeHead(409).end(JSON.stringify({ error: error.message })); });
38
+ return;
39
+ }
40
+ if (request.method === 'POST') response.once('finish', () => {
41
+ Promise.resolve(shutdown()).catch(error => {
42
+ console.error('桌面服务关闭失败:', error.message);
43
+ process.exitCode = 1;
44
+ });
45
+ });
46
+ response.end(JSON.stringify({ ...status(), instanceId, pid: process.pid }));
47
+ });
48
+ await new Promise((resolve, reject) => {
49
+ server.once('error', reject);
50
+ server.listen(0, '127.0.0.1', resolve);
51
+ });
52
+ return { port: server.address().port, close: () => new Promise((resolve, reject) => {
53
+ server.close(error => error ? reject(error) : resolve());
54
+ }) };
55
+ }
56
+
57
+ export function controlRequest(session, method = 'GET', action = '', details) {
58
+ return new Promise((resolve, reject) => {
59
+ const request = http.request({ hostname: '127.0.0.1', port: session.controlPort,
60
+ path: `/${session.instanceId}${action ? '/' + action : ''}`, method, agent: false,
61
+ headers: { Authorization: `Bearer ${session.controlSecret}` }, timeout: action ? 25000 : 2000 }, response => {
62
+ let body = '';
63
+ response.setEncoding('utf8');
64
+ response.on('data', chunk => { body += chunk; });
65
+ response.on('error', reject);
66
+ response.on('end', () => {
67
+ try {
68
+ if (response.statusCode === 409) throw new Error(JSON.parse(body).error);
69
+ if (response.statusCode !== 200) throw new Error('桌面服务管理通道身份验证失败。');
70
+ const identity = JSON.parse(body);
71
+ if (identity.instanceId !== session.instanceId || identity.pid !== session.pid) {
72
+ throw new Error('桌面服务实例身份不匹配。');
73
+ }
74
+ resolve(identity);
75
+ } catch (error) { reject(error); }
76
+ });
77
+ });
78
+ request.on('timeout', () => request.destroy(new Error('桌面服务管理通道响应超时。')));
79
+ request.on('error', reject);
80
+ request.end(details === undefined ? undefined : JSON.stringify(details));
81
+ });
82
+ }
@@ -1,33 +1,33 @@
1
- import WebSocket from 'ws';
2
-
3
- // Use the same authenticated LAN protocol as mobile clients; never replay requests.
4
- export function serviceHealth(session) {
5
- if (!session) return Promise.resolve(null);
6
- return new Promise((resolve, reject) => {
7
- const token = new URL(session.accessUrl).searchParams.get('token');
8
- const socket = new WebSocket(`ws://127.0.0.1:${session.port}/api/socket`);
9
- let done = false;
10
- const finish = (error, value = null) => {
11
- if (done) return;
12
- done = true;
13
- clearTimeout(timer);
14
- socket.terminate();
15
- if (error) reject(error); else resolve(value);
16
- };
17
- const timer = setTimeout(() => finish(new Error('桌面服务健康检查超时。')), 6000);
18
- socket.on('open', () => socket.send(JSON.stringify({ type: 'auth', token })));
19
- socket.on('message', data => {
20
- try {
21
- const message = JSON.parse(data.toString());
22
- if (message.type === 'ready') {
23
- socket.send(JSON.stringify({ type: 'request', id: 1, method: 'GET', path: '/api/health' }));
24
- } else if (message.type === 'response' && message.id === 1) {
25
- finish(null, message.status === 200 ? message.body : null);
26
- }
27
- } catch (error) { finish(error); }
28
- });
29
- socket.on('error', error => finish(error.code === 'ECONNREFUSED' ? null : error));
30
- socket.on('close', code => finish(new Error(code === 1008
31
- ? '桌面服务健康检查鉴权失败。' : '桌面服务健康检查连接已断开。')));
32
- });
33
- }
1
+ import WebSocket from 'ws';
2
+
3
+ // Use the same authenticated LAN protocol as mobile clients; never replay requests.
4
+ export function serviceHealth(session) {
5
+ if (!session) return Promise.resolve(null);
6
+ return new Promise((resolve, reject) => {
7
+ const token = new URL(session.accessUrl).searchParams.get('token');
8
+ const socket = new WebSocket(`ws://127.0.0.1:${session.port}/api/socket`);
9
+ let done = false;
10
+ const finish = (error, value = null) => {
11
+ if (done) return;
12
+ done = true;
13
+ clearTimeout(timer);
14
+ socket.terminate();
15
+ if (error) reject(error); else resolve(value);
16
+ };
17
+ const timer = setTimeout(() => finish(new Error('桌面服务健康检查超时。')), 6000);
18
+ socket.on('open', () => socket.send(JSON.stringify({ type: 'auth', token })));
19
+ socket.on('message', data => {
20
+ try {
21
+ const message = JSON.parse(data.toString());
22
+ if (message.type === 'ready') {
23
+ socket.send(JSON.stringify({ type: 'request', id: 1, method: 'GET', path: '/api/health' }));
24
+ } else if (message.type === 'response' && message.id === 1) {
25
+ finish(null, message.status === 200 ? message.body : null);
26
+ }
27
+ } catch (error) { finish(error); }
28
+ });
29
+ socket.on('error', error => finish(error.code === 'ECONNREFUSED' ? null : error));
30
+ socket.on('close', code => finish(new Error(code === 1008
31
+ ? '桌面服务健康检查鉴权失败。' : '桌面服务健康检查连接已断开。')));
32
+ });
33
+ }
@@ -1,58 +1,58 @@
1
- import path from 'node:path';
2
- import { readRecord, writeRecord } from './service-lifecycle.mjs';
3
- import { launchPowerInhibitor } from './platform/power-inhibitor.mjs';
4
-
5
- export const powerConfigPath = stateRoot => path.join(stateRoot, 'keep-awake.json');
6
- export const powerEnabled = stateRoot => readRecord(powerConfigPath(stateRoot))?.enabled !== false;
7
-
8
- export function createPowerManager(stateRoot, { launch = launchPowerInhibitor, report = console.error } = {}) {
9
- let enabled = powerEnabled(stateRoot), active = false, error = '', child = null, closed, stopping = false;
10
- let queue = Promise.resolve();
11
- const snapshot = () => ({ enabled, active, error });
12
- async function release() {
13
- if (!child) return;
14
- stopping = true;
15
- child.stdin.end();
16
- await closed;
17
- }
18
- async function acquire() {
19
- if (child || !enabled) return;
20
- error = '';
21
- try {
22
- const process = launch();
23
- stopping = false;
24
- child = process;
25
- let stderr = '', output = '';
26
- process.stderr.on('data', data => { stderr = (stderr + data).slice(-2048); });
27
- process.stdin.on('error', cause => { if (cause.code !== 'EPIPE') report(`保持唤醒通道异常:${cause.message}`); });
28
- closed = new Promise(resolve => process.once('close', code => {
29
- if (active && !stopping) { error = stderr.trim() || `保持唤醒进程意外退出(${code})`; report(error); }
30
- child = null; active = false; resolve();
31
- }));
32
- await new Promise((resolve, reject) => {
33
- const timer = setTimeout(() => { process.kill(); reject(new Error('保持唤醒申请超时')); }, 8000);
34
- const finish = cause => { clearTimeout(timer); cause ? reject(cause) : resolve(); };
35
- process.once('error', finish);
36
- process.once('close', code => {
37
- finish(new Error(stderr.trim() || `保持唤醒进程已退出(${code})`));
38
- });
39
- process.stdout.on('data', data => {
40
- output += data;
41
- if (output.includes('ready\n') || output.includes('ready\r\n')) { active = true; finish(); }
42
- });
43
- });
44
- } catch (cause) { error = cause.message; report(`保持唤醒未生效:${error}`); }
45
- }
46
- const serial = task => { const result = queue.then(task); queue = result.catch(() => {}); return result; };
47
- return {
48
- snapshot,
49
- start: () => serial(async () => { await acquire(); return snapshot(); }),
50
- setEnabled: value => serial(async () => {
51
- writeRecord(powerConfigPath(stateRoot), { enabled: value });
52
- enabled = value;
53
- if (enabled) await acquire(); else { await release(); error = ''; }
54
- return snapshot();
55
- }),
56
- close: () => serial(async () => { enabled = false; await release(); }),
57
- };
58
- }
1
+ import path from 'node:path';
2
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
3
+ import { launchPowerInhibitor } from './platform/power-inhibitor.mjs';
4
+
5
+ export const powerConfigPath = stateRoot => path.join(stateRoot, 'keep-awake.json');
6
+ export const powerEnabled = stateRoot => readRecord(powerConfigPath(stateRoot))?.enabled !== false;
7
+
8
+ export function createPowerManager(stateRoot, { launch = launchPowerInhibitor, report = console.error } = {}) {
9
+ let enabled = powerEnabled(stateRoot), active = false, error = '', child = null, closed, stopping = false;
10
+ let queue = Promise.resolve();
11
+ const snapshot = () => ({ enabled, active, error });
12
+ async function release() {
13
+ if (!child) return;
14
+ stopping = true;
15
+ child.stdin.end();
16
+ await closed;
17
+ }
18
+ async function acquire() {
19
+ if (child || !enabled) return;
20
+ error = '';
21
+ try {
22
+ const process = launch();
23
+ stopping = false;
24
+ child = process;
25
+ let stderr = '', output = '';
26
+ process.stderr.on('data', data => { stderr = (stderr + data).slice(-2048); });
27
+ process.stdin.on('error', cause => { if (cause.code !== 'EPIPE') report(`保持唤醒通道异常:${cause.message}`); });
28
+ closed = new Promise(resolve => process.once('close', code => {
29
+ if (active && !stopping) { error = stderr.trim() || `保持唤醒进程意外退出(${code})`; report(error); }
30
+ child = null; active = false; resolve();
31
+ }));
32
+ await new Promise((resolve, reject) => {
33
+ const timer = setTimeout(() => { process.kill(); reject(new Error('保持唤醒申请超时')); }, 8000);
34
+ const finish = cause => { clearTimeout(timer); cause ? reject(cause) : resolve(); };
35
+ process.once('error', finish);
36
+ process.once('close', code => {
37
+ finish(new Error(stderr.trim() || `保持唤醒进程已退出(${code})`));
38
+ });
39
+ process.stdout.on('data', data => {
40
+ output += data;
41
+ if (output.includes('ready\n') || output.includes('ready\r\n')) { active = true; finish(); }
42
+ });
43
+ });
44
+ } catch (cause) { error = cause.message; report(`保持唤醒未生效:${error}`); }
45
+ }
46
+ const serial = task => { const result = queue.then(task); queue = result.catch(() => {}); return result; };
47
+ return {
48
+ snapshot,
49
+ start: () => serial(async () => { await acquire(); return snapshot(); }),
50
+ setEnabled: value => serial(async () => {
51
+ writeRecord(powerConfigPath(stateRoot), { enabled: value });
52
+ enabled = value;
53
+ if (enabled) await acquire(); else { await release(); error = ''; }
54
+ return snapshot();
55
+ }),
56
+ close: () => serial(async () => { enabled = false; await release(); }),
57
+ };
58
+ }
@@ -1,81 +1,81 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
- import { fileURLToPath } from 'node:url';
5
- import { setTimeout as delay } from 'node:timers/promises';
6
- import { startBackground } from './background-process.mjs';
7
- import { lifecycle, readRecord, writeRecord } from './service-lifecycle.mjs';
8
- import * as host from './platform/service-host.mjs';
9
- import { desktopHost } from './platform/desktop-host.mjs';
10
-
11
- const script = fileURLToPath(import.meta.url);
12
- const sameProcess = (expected, actual) => expected && actual && expected.pid === actual.pid &&
13
- expected.created === actual.created && expected.executable === actual.executable;
14
- async function processIdentity(pid) {
15
- const current = await desktopHost.process(pid);
16
- return current ? { pid: current.pid, created: current.created, executable: current.executable } : null;
17
- }
18
-
19
- export async function requestServiceRestart(stateRoot) {
20
- const lock = path.join(stateRoot, 'service-restart.lock');
21
- const operationId = randomUUID();
22
- const owner = await processIdentity(process.pid);
23
- if (!owner) throw new Error('无法确认桌面服务重启请求进程。');
24
- const previous = readRecord(lock);
25
- if (previous) {
26
- const actual = previous.owner?.pid ? await processIdentity(previous.owner.pid) : null;
27
- if (sameProcess(previous.owner, actual) || readRecord(lock)?.operationId !== previous.operationId) {
28
- throw new Error('桌面服务已在重启中。');
29
- }
30
- fs.rmSync(lock);
31
- }
32
- const temporary = `${lock}.${operationId}.tmp`;
33
- try {
34
- fs.writeFileSync(temporary, JSON.stringify({ operationId, owner, requestedAt: new Date().toISOString() }),
35
- { flag: 'wx', mode: 0o600 });
36
- // Publish a complete record exclusively; a crash cannot leave a half-written lock.
37
- fs.linkSync(temporary, lock);
38
- } catch (error) {
39
- if (error.code === 'EEXIST') throw new Error('桌面服务已在重启中。');
40
- throw error;
41
- } finally { fs.rmSync(temporary, { force: true }); }
42
- try {
43
- const log = path.join(stateRoot, 'service-restart.log');
44
- const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
45
- const worker = await processIdentity(pid);
46
- if (!worker) throw new Error('桌面服务重启进程未启动。');
47
- const current = readRecord(lock);
48
- if (current?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
49
- writeRecord(lock, { ...current, owner: worker });
50
- return { operationId, status: 'scheduled', message: '桌面服务重启已安排。' };
51
- } catch (error) {
52
- const current = readRecord(lock);
53
- if (current?.operationId === operationId && sameProcess(current.owner, owner)) fs.rmSync(lock, { force: true });
54
- throw error;
55
- }
56
- }
57
-
58
- async function run(stateRoot, operationId) {
59
- const lock = path.join(stateRoot, 'service-restart.lock');
60
- try {
61
- const deadline = Date.now() + 30000;
62
- while (true) {
63
- const record = readRecord(lock);
64
- if (record?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
65
- if (record.owner?.pid === process.pid) break;
66
- if (Date.now() >= deadline) throw new Error('桌面服务重启进程交接超时。');
67
- await delay(100);
68
- }
69
- const service = lifecycle({ stateRoot, host });
70
- await delay(1000);
71
- await service.stop();
72
- await service.start();
73
- } finally {
74
- if (readRecord(lock)?.operationId === operationId) fs.rmSync(lock, { force: true });
75
- }
76
- }
77
-
78
- if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
79
- const [stateRoot, operationId] = process.argv.slice(3);
80
- run(stateRoot, operationId).catch(error => { console.error(error.message); process.exitCode = 1; });
81
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { startBackground } from './background-process.mjs';
7
+ import { lifecycle, readRecord, writeRecord } from './service-lifecycle.mjs';
8
+ import * as host from './platform/service-host.mjs';
9
+ import { desktopHost } from './platform/desktop-host.mjs';
10
+
11
+ const script = fileURLToPath(import.meta.url);
12
+ const sameProcess = (expected, actual) => expected && actual && expected.pid === actual.pid &&
13
+ expected.created === actual.created && expected.executable === actual.executable;
14
+ async function processIdentity(pid) {
15
+ const current = await desktopHost.process(pid);
16
+ return current ? { pid: current.pid, created: current.created, executable: current.executable } : null;
17
+ }
18
+
19
+ export async function requestServiceRestart(stateRoot) {
20
+ const lock = path.join(stateRoot, 'service-restart.lock');
21
+ const operationId = randomUUID();
22
+ const owner = await processIdentity(process.pid);
23
+ if (!owner) throw new Error('无法确认桌面服务重启请求进程。');
24
+ const previous = readRecord(lock);
25
+ if (previous) {
26
+ const actual = previous.owner?.pid ? await processIdentity(previous.owner.pid) : null;
27
+ if (sameProcess(previous.owner, actual) || readRecord(lock)?.operationId !== previous.operationId) {
28
+ throw new Error('桌面服务已在重启中。');
29
+ }
30
+ fs.rmSync(lock);
31
+ }
32
+ const temporary = `${lock}.${operationId}.tmp`;
33
+ try {
34
+ fs.writeFileSync(temporary, JSON.stringify({ operationId, owner, requestedAt: new Date().toISOString() }),
35
+ { flag: 'wx', mode: 0o600 });
36
+ // Publish a complete record exclusively; a crash cannot leave a half-written lock.
37
+ fs.linkSync(temporary, lock);
38
+ } catch (error) {
39
+ if (error.code === 'EEXIST') throw new Error('桌面服务已在重启中。');
40
+ throw error;
41
+ } finally { fs.rmSync(temporary, { force: true }); }
42
+ try {
43
+ const log = path.join(stateRoot, 'service-restart.log');
44
+ const pid = await startBackground(process.execPath, [script, '--worker', stateRoot, operationId], process.env, [log, log]);
45
+ const worker = await processIdentity(pid);
46
+ if (!worker) throw new Error('桌面服务重启进程未启动。');
47
+ const current = readRecord(lock);
48
+ if (current?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
49
+ writeRecord(lock, { ...current, owner: worker });
50
+ return { operationId, status: 'scheduled', message: '桌面服务重启已安排。' };
51
+ } catch (error) {
52
+ const current = readRecord(lock);
53
+ if (current?.operationId === operationId && sameProcess(current.owner, owner)) fs.rmSync(lock, { force: true });
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async function run(stateRoot, operationId) {
59
+ const lock = path.join(stateRoot, 'service-restart.lock');
60
+ try {
61
+ const deadline = Date.now() + 30000;
62
+ while (true) {
63
+ const record = readRecord(lock);
64
+ if (record?.operationId !== operationId) throw new Error('桌面服务重启实例已失效。');
65
+ if (record.owner?.pid === process.pid) break;
66
+ if (Date.now() >= deadline) throw new Error('桌面服务重启进程交接超时。');
67
+ await delay(100);
68
+ }
69
+ const service = lifecycle({ stateRoot, host });
70
+ await delay(1000);
71
+ await service.stop();
72
+ await service.start();
73
+ } finally {
74
+ if (readRecord(lock)?.operationId === operationId) fs.rmSync(lock, { force: true });
75
+ }
76
+ }
77
+
78
+ if (process.argv[1] && path.resolve(process.argv[1]) === script && process.argv[2] === '--worker') {
79
+ const [stateRoot, operationId] = process.argv.slice(3);
80
+ run(stateRoot, operationId).catch(error => { console.error(error.message); process.exitCode = 1; });
81
+ }
@@ -1,42 +1,42 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { spawn } from 'node:child_process';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
-
6
- // Copied outside both npm and service directories before launch. Builtins only.
7
- const jobFile = process.argv[2];
8
- const job = JSON.parse(fs.readFileSync(jobFile, 'utf8'));
9
- process.chdir(path.dirname(jobFile));
10
- const read = () => JSON.parse(fs.readFileSync(job.statusFile, 'utf8'));
11
- function update(values) {
12
- const current = read();
13
- if (current.operationId !== job.operationId) throw new Error('更新操作已被替换');
14
- const temporary = `${job.statusFile}.${process.pid}.tmp`;
15
- fs.writeFileSync(temporary, JSON.stringify({ ...current, ...values }), { mode: 0o600 });
16
- fs.renameSync(temporary, job.statusFile);
17
- }
18
- try {
19
- const deadline = Date.now() + 30000;
20
- while (read().owner?.pid !== process.pid) {
21
- if (Date.now() >= deadline) throw new Error('独立更新进程交接超时');
22
- await delay(100);
23
- }
24
- await delay(1500);
25
- update({ status: 'running', startedAt: new Date().toISOString() });
26
- const args = [job.npmCliPath, 'install', '--global', `@walkhi/code-relax@${job.targetVersion}`,
27
- '--foreground-scripts', '--allow-scripts=@walkhi/code-relax,node-datachannel,sharp', '--registry=https://registry.npmjs.org'];
28
- if (job.npmPrefix) args.push('--prefix', job.npmPrefix);
29
- const child = spawn(job.nodeExecutable, args, { cwd: path.dirname(jobFile), windowsHide: true,
30
- stdio: ['ignore', 'inherit', 'inherit'] });
31
- const code = await new Promise((resolve, reject) => {
32
- child.once('error', reject); child.once('exit', (code, signal) => resolve(code ?? `signal ${signal}`));
33
- });
34
- if (code !== 0) throw new Error(`npm 安装失败(${code}),请查看桌面服务更新日志。`);
35
- const installed = JSON.parse(fs.readFileSync(path.join(job.serviceRoot, 'installation.json'), 'utf8'));
36
- if (installed.version !== job.targetVersion) throw new Error('安装结束,但桌面服务版本与目标版本不一致。');
37
- update({ status: 'completed', finishedAt: new Date().toISOString(), installedVersion: installed.version });
38
- } catch (error) {
39
- console.error(error.stack || error.message);
40
- update({ status: 'failed', finishedAt: new Date().toISOString(), error: error.message });
41
- process.exitCode = 1;
42
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+
6
+ // Copied outside both npm and service directories before launch. Builtins only.
7
+ const jobFile = process.argv[2];
8
+ const job = JSON.parse(fs.readFileSync(jobFile, 'utf8'));
9
+ process.chdir(path.dirname(jobFile));
10
+ const read = () => JSON.parse(fs.readFileSync(job.statusFile, 'utf8'));
11
+ function update(values) {
12
+ const current = read();
13
+ if (current.operationId !== job.operationId) throw new Error('更新操作已被替换');
14
+ const temporary = `${job.statusFile}.${process.pid}.tmp`;
15
+ fs.writeFileSync(temporary, JSON.stringify({ ...current, ...values }), { mode: 0o600 });
16
+ fs.renameSync(temporary, job.statusFile);
17
+ }
18
+ try {
19
+ const deadline = Date.now() + 30000;
20
+ while (read().owner?.pid !== process.pid) {
21
+ if (Date.now() >= deadline) throw new Error('独立更新进程交接超时');
22
+ await delay(100);
23
+ }
24
+ await delay(1500);
25
+ update({ status: 'running', startedAt: new Date().toISOString() });
26
+ const args = [job.npmCliPath, 'install', '--global', `@walkhi/code-relax@${job.targetVersion}`,
27
+ '--foreground-scripts', '--allow-scripts=@walkhi/code-relax,node-datachannel,sharp', '--registry=https://registry.npmjs.org'];
28
+ if (job.npmPrefix) args.push('--prefix', job.npmPrefix);
29
+ const child = spawn(job.nodeExecutable, args, { cwd: path.dirname(jobFile), windowsHide: true,
30
+ stdio: ['ignore', 'inherit', 'inherit'] });
31
+ const code = await new Promise((resolve, reject) => {
32
+ child.once('error', reject); child.once('exit', (code, signal) => resolve(code ?? `signal ${signal}`));
33
+ });
34
+ if (code !== 0) throw new Error(`npm 安装失败(${code}),请查看桌面服务更新日志。`);
35
+ const installed = JSON.parse(fs.readFileSync(path.join(job.serviceRoot, 'installation.json'), 'utf8'));
36
+ if (installed.version !== job.targetVersion) throw new Error('安装结束,但桌面服务版本与目标版本不一致。');
37
+ update({ status: 'completed', finishedAt: new Date().toISOString(), installedVersion: installed.version });
38
+ } catch (error) {
39
+ console.error(error.stack || error.message);
40
+ update({ status: 'failed', finishedAt: new Date().toISOString(), error: error.message });
41
+ process.exitCode = 1;
42
+ }