@walkhi/code-relax 0.1.0-beta.50 → 0.1.0-beta.59

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 (101) hide show
  1. package/README.md +105 -105
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +403 -403
  4. package/dist/bin/publish-android-release.mjs +63 -63
  5. package/dist/bin/release-upload-server.mjs +190 -190
  6. package/dist/bin/set-relay-admin-password.mjs +11 -0
  7. package/dist/build-platform.json +1 -1
  8. package/dist/get-shared-service-url.ps1 +14 -14
  9. package/dist/shared/code-browser.cjs +159 -159
  10. package/dist/shared/file-changes.cjs +31 -31
  11. package/dist/shared/file-citations.cjs +117 -117
  12. package/dist/shared/file-mentions.cjs +92 -92
  13. package/dist/shared/message-delivery.cjs +115 -115
  14. package/dist/shared/p2p-file-channel.cjs +87 -87
  15. package/dist/shared/release-version.cjs +31 -0
  16. package/dist/shared/server-requests.cjs +311 -311
  17. package/dist/shared/timeline-sync.cjs +173 -173
  18. package/dist/skills/code-relax/SKILL.md +22 -22
  19. package/dist/src/bounded-process-log.mjs +63 -63
  20. package/dist/src/celsius-client.mjs +107 -107
  21. package/dist/src/dots-approval-timeline.mjs +99 -99
  22. package/dist/src/dots-avatar.mjs +42 -42
  23. package/dist/src/dots-chat-projection.mjs +50 -50
  24. package/dist/src/dots-chat-server.mjs +483 -483
  25. package/dist/src/dots-message-images.mjs +319 -319
  26. package/dist/src/dots-profile.mjs +77 -77
  27. package/dist/src/dots-room-activity.mjs +172 -172
  28. package/dist/src/dots-room-client.mjs +137 -135
  29. package/dist/src/dots-room-projection.mjs +94 -94
  30. package/dist/src/dots-schedules.mjs +66 -66
  31. package/dist/src/dots-summary-tasks.mjs +32 -32
  32. package/dist/src/dots-task-catalog.mjs +430 -430
  33. package/dist/src/file-transfer-range.mjs +22 -22
  34. package/dist/src/file-upload-session.mjs +146 -130
  35. package/dist/src/hosted-history-app-server.mjs +103 -103
  36. package/dist/src/hosted-history-cache.mjs +113 -113
  37. package/dist/src/lan-socket-server.mjs +338 -266
  38. package/dist/src/managed-app-server.mjs +373 -373
  39. package/dist/src/managed-endpoint.mjs +25 -25
  40. package/dist/src/message-upload-files.mjs +109 -109
  41. package/dist/src/onboarding.mjs +12 -12
  42. package/dist/src/platform/README.md +1 -1
  43. package/dist/src/platform/codex-protocol.mjs +46 -46
  44. package/dist/src/platform/desktop-project-store.mjs +219 -208
  45. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  46. package/dist/src/platform/linux/process.mjs +53 -53
  47. package/dist/src/platform/linux/system.mjs +50 -50
  48. package/dist/src/platform/linux/user-environment.mjs +9 -9
  49. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  50. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  51. package/dist/src/platform/macos/user-environment.mjs +9 -9
  52. package/dist/src/platform/native-package.mjs +58 -58
  53. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  54. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  55. package/dist/src/platform/windows/credential-store.mjs +27 -27
  56. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  57. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  58. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  59. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  60. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  61. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  62. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  63. package/dist/src/platform/windows/project-locations.mjs +16 -16
  64. package/dist/src/platform/windows/service-host.mjs +35 -35
  65. package/dist/src/platform/windows/user-environment.mjs +52 -52
  66. package/dist/src/project-management.mjs +58 -58
  67. package/dist/src/runtime-discovery.mjs +47 -47
  68. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  69. package/dist/src/self-relay/admin-auth.mjs +59 -0
  70. package/dist/src/self-relay/admin-login-page.mjs +7 -0
  71. package/dist/src/self-relay/admin-page.mjs +87 -80
  72. package/dist/src/self-relay/admin-server.mjs +49 -18
  73. package/dist/src/self-relay/admin-style.mjs +58 -14
  74. package/dist/src/self-relay/admin-workstations-page.mjs +95 -0
  75. package/dist/src/self-relay/admin-workstations.mjs +143 -0
  76. package/dist/src/self-relay/connection-reporting.mjs +38 -0
  77. package/dist/src/self-relay/connector.mjs +444 -439
  78. package/dist/src/self-relay/demo.mjs +31 -31
  79. package/dist/src/self-relay/diagnostics.mjs +54 -54
  80. package/dist/src/self-relay/email-accounts.mjs +183 -183
  81. package/dist/src/self-relay/file-wire.mjs +13 -13
  82. package/dist/src/self-relay/lifecycle.mjs +11 -0
  83. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  84. package/dist/src/self-relay/phone-connections.mjs +55 -0
  85. package/dist/src/self-relay/privacy-page.mjs +26 -26
  86. package/dist/src/self-relay/public-releases.mjs +67 -67
  87. package/dist/src/self-relay/rtc-file-bridge.mjs +46 -41
  88. package/dist/src/self-relay/server.mjs +63 -3
  89. package/dist/src/self-relay/usage-meter.mjs +141 -141
  90. package/dist/src/server.mjs +23 -9
  91. package/dist/src/service-doctor.mjs +148 -148
  92. package/dist/src/service-install.mjs +97 -97
  93. package/dist/src/service-lifecycle.mjs +139 -139
  94. package/dist/src/service-update.mjs +6 -3
  95. package/dist/src/shared-app-server.mjs +683 -666
  96. package/dist/src/shared-catalog.mjs +132 -132
  97. package/dist/src/shared-thread-stream.mjs +165 -165
  98. package/dist/src/usage-reset.mjs +17 -17
  99. package/dist/src/workstation-downloads.mjs +120 -120
  100. package/package.json +4 -4
  101. package/tools/postinstall.mjs +186 -186
@@ -1,25 +1,25 @@
1
- import path from 'node:path';
2
- import { readRecord } from './service-lifecycle.mjs';
3
-
4
- export const defaultManagedPort = 45839;
5
- export function managedEndpointUrl(value) {
6
- if (typeof value !== 'string' || !/^ws:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/.test(value)) return null;
7
- const port = Number(value.slice(value.lastIndexOf(':') + 1));
8
- return port <= 65535 ? `ws://127.0.0.1:${port}` : null;
9
- }
10
- export function managedProcessUrl(record) {
11
- if (!record) return null;
12
- let value;
13
- if (Array.isArray(record.args)) {
14
- if (record.args.length !== 4 || record.args[1] !== 'app-server' || record.args[2] !== '--listen') return null;
15
- value = record.args[3];
16
- } else {
17
- const match = /^\s*(?:"[^"]+"|\S+)\s+app-server\s+--listen\s+(?:"(ws:\/\/127\.0\.0\.1:[0-9]+)"|(ws:\/\/127\.0\.0\.1:[0-9]+))\s*$/i.exec(record.command || '');
18
- value = match?.[1] || match?.[2];
19
- }
20
- const url = managedEndpointUrl(value);
21
- return url && (!record.url || record.url === url) ? url : null;
22
- }
23
- export function readManagedEndpoint(stateRoot) {
24
- return managedProcessUrl(readRecord(path.join(stateRoot, 'managed-app-server.json')));
25
- }
1
+ import path from 'node:path';
2
+ import { readRecord } from './service-lifecycle.mjs';
3
+
4
+ export const defaultManagedPort = 45839;
5
+ export function managedEndpointUrl(value) {
6
+ if (typeof value !== 'string' || !/^ws:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/.test(value)) return null;
7
+ const port = Number(value.slice(value.lastIndexOf(':') + 1));
8
+ return port <= 65535 ? `ws://127.0.0.1:${port}` : null;
9
+ }
10
+ export function managedProcessUrl(record) {
11
+ if (!record) return null;
12
+ let value;
13
+ if (Array.isArray(record.args)) {
14
+ if (record.args.length !== 4 || record.args[1] !== 'app-server' || record.args[2] !== '--listen') return null;
15
+ value = record.args[3];
16
+ } else {
17
+ const match = /^\s*(?:"[^"]+"|\S+)\s+app-server\s+--listen\s+(?:"(ws:\/\/127\.0\.0\.1:[0-9]+)"|(ws:\/\/127\.0\.0\.1:[0-9]+))\s*$/i.exec(record.command || '');
18
+ value = match?.[1] || match?.[2];
19
+ }
20
+ const url = managedEndpointUrl(value);
21
+ return url && (!record.url || record.url === url) ? url : null;
22
+ }
23
+ export function readManagedEndpoint(stateRoot) {
24
+ return managedProcessUrl(readRecord(path.join(stateRoot, 'managed-app-server.json')));
25
+ }
@@ -1,109 +1,109 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import crypto from 'node:crypto';
4
- import sharp from 'sharp';
5
- import media from '../shared/code-browser.cjs';
6
- import { validateInlineImage } from './message-images.mjs';
7
- import { createFileUploadSession, FILE_CHUNK_BYTES, MAX_FILE_BYTES, MAX_FILE_COUNT, MAX_TOTAL_BYTES } from './file-upload-session.mjs';
8
- import { fileRevision } from './file-transfer-range.mjs';
9
-
10
- const mimeTypes = { pdf: 'application/pdf', txt: 'text/plain', md: 'text/markdown', csv: 'text/csv',
11
- json: 'application/json', zip: 'application/zip', doc: 'application/msword',
12
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
13
- xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
14
- ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' };
15
-
16
- // Reuse the incoming-file contract without sending workstation bytes to the phone.
17
- export async function stageWorkstationMessageFile(file, uploadsRoot) {
18
- const handle = await fs.open(file.path, 'r');
19
- const session = createFileUploadSession(uploadsRoot);
20
- try {
21
- const stat = await handle.stat();
22
- if (!stat.isFile() || stat.size < 1 || stat.size > MAX_FILE_BYTES) throw new Error('附件为空或超过 20 MiB。');
23
- const validate = async () => {
24
- if (fileRevision(await handle.stat()) !== file.revision || fileRevision(await fs.stat(file.path)) !== file.revision) {
25
- throw new Error('电脑文件已变化,请重新添加。');
26
- }
27
- };
28
- await validate();
29
- const bytes = Buffer.alloc(stat.size);
30
- for (let offset = 0; offset < bytes.length;) {
31
- const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
32
- if (!bytesRead) throw new Error('电脑文件读取不完整,请重新添加。');
33
- offset += bytesRead;
34
- }
35
- await validate();
36
- const sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
37
- const started = await session.receive({ type: 'start', name: file.name, size: bytes.length, sha256 });
38
- for (let offset = 0; offset < bytes.length; offset += FILE_CHUNK_BYTES) {
39
- await session.receive({ type: 'chunk', id: started.id, offset, data: bytes.subarray(offset, offset + FILE_CHUNK_BYTES) });
40
- }
41
- await validate();
42
- const result = await session.receive({ type: 'finish', id: started.id });
43
- await session.receive({ type: 'commit' });
44
- return { ...result, mediaKind: media.mediaKind(file),
45
- mimeType: media.mediaMime(file) || mimeTypes[path.extname(file.name).slice(1).toLowerCase()] || 'application/octet-stream' };
46
- } finally { await handle.close(); await session.close(); }
47
- }
48
-
49
- async function localFile(file, directory) {
50
- if (typeof file?.path !== 'string' || typeof file.name !== 'string' || !file.name || /[\r\n\0]/.test(file.name)) {
51
- throw new Error('附件信息无效,请重新选择。');
52
- }
53
- const root = await fs.realpath(directory), resolved = await fs.realpath(file.path);
54
- if (path.relative(root, path.dirname(resolved)) !== '') throw new Error('附件必须来自本次工作站上传目录。');
55
- const handle = await fs.open(resolved, 'r');
56
- try {
57
- const stat = await handle.stat();
58
- if (!stat.isFile() || stat.size < 1 || stat.size > MAX_FILE_BYTES) throw new Error('附件为空或超过 20 MiB。');
59
- const bytes = await handle.readFile();
60
- if (bytes.length !== stat.size) throw new Error('附件在读取时发生变化,请重新上传。');
61
- const mimeType = media.mediaMime(file) || mimeTypes[path.extname(file.name).slice(1).toLowerCase()] || 'application/octet-stream';
62
- if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/i.test(mimeType)) throw new Error('附件类型无效。');
63
- return { name: file.name, mimeType, bytes };
64
- } finally { await handle.close(); }
65
- }
66
-
67
- export async function readMessageUploads(body, uploadsRoot) {
68
- const images = body.images ?? [], originals = body.originalImages ?? [], files = body.files ?? [];
69
- if (![images, originals, files].every(Array.isArray) || images.length + originals.length > 4 || files.length + originals.length > MAX_FILE_COUNT) {
70
- throw new Error('每条消息最多 4 张图片、4 个文件。');
71
- }
72
- const uploads = [];
73
- let imageBytes = 0, fileBytes = 0;
74
- for (const image of images) {
75
- let file;
76
- if (typeof image === 'string' && image.startsWith('data:')) {
77
- validateInlineImage(image);
78
- const mimeType = image.slice(5, image.indexOf(';'));
79
- file = { name: `image.${mimeType.split('/')[1]}`, mimeType, bytes: Buffer.from(image.slice(image.indexOf(',') + 1), 'base64') };
80
- } else {
81
- if (typeof image !== 'string' || !/^[a-f0-9]{64}$/.test(image)) throw new Error('图片标识无效。');
82
- let extension = 'webp';
83
- try { await fs.access(path.join(uploadsRoot, `${image}.webp`)); }
84
- catch (error) { if (error.code !== 'ENOENT') throw error; extension = 'png'; }
85
- file = await localFile({ path: path.join(uploadsRoot, `${image}.${extension}`), name: `image.${extension}` }, uploadsRoot);
86
- }
87
- imageBytes += file.bytes.length;
88
- if (imageBytes > 8 * 1024 * 1024) throw new Error('压缩图片合计不能超过 8 MiB。');
89
- uploads.push(file);
90
- }
91
- for (const original of originals) {
92
- if (typeof original !== 'string' || !/\.(png|jpe?g|webp|gif)$/i.test(original)) throw new Error('原图路径无效。');
93
- }
94
- for (const file of [...originals.map(value => ({ path: value, name: path.basename(value) })), ...files]) {
95
- const uploaded = await localFile(file, path.join(uploadsRoot, 'incoming'));
96
- fileBytes += uploaded.bytes.length;
97
- if (fileBytes > MAX_TOTAL_BYTES) throw new Error('文件合计不能超过 40 MiB。');
98
- uploads.push(uploaded);
99
- }
100
- for (const file of uploads) {
101
- if (/^image\/(?:png|jpeg|webp|gif)$/.test(file.mimeType)) {
102
- const metadata = await sharp(file.bytes).metadata();
103
- file.width = metadata.width; file.height = metadata.height;
104
- if (!file.width || !file.height) throw new Error('无法读取 图片尺寸。');
105
- }
106
- }
107
- if (uploads.filter(file => file.width).length > 4) throw new Error('每条消息最多 4 张图片。');
108
- return uploads;
109
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import sharp from 'sharp';
5
+ import media from '../shared/code-browser.cjs';
6
+ import { validateInlineImage } from './message-images.mjs';
7
+ import { createFileUploadSession, FILE_CHUNK_BYTES, MAX_FILE_BYTES, MAX_FILE_COUNT, MAX_TOTAL_BYTES } from './file-upload-session.mjs';
8
+ import { fileRevision } from './file-transfer-range.mjs';
9
+
10
+ const mimeTypes = { pdf: 'application/pdf', txt: 'text/plain', md: 'text/markdown', csv: 'text/csv',
11
+ json: 'application/json', zip: 'application/zip', doc: 'application/msword',
12
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
13
+ xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
14
+ ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' };
15
+
16
+ // Reuse the incoming-file contract without sending workstation bytes to the phone.
17
+ export async function stageWorkstationMessageFile(file, uploadsRoot) {
18
+ const handle = await fs.open(file.path, 'r');
19
+ const session = createFileUploadSession(uploadsRoot);
20
+ try {
21
+ const stat = await handle.stat();
22
+ if (!stat.isFile() || stat.size < 1 || stat.size > MAX_FILE_BYTES) throw new Error('附件为空或超过 20 MiB。');
23
+ const validate = async () => {
24
+ if (fileRevision(await handle.stat()) !== file.revision || fileRevision(await fs.stat(file.path)) !== file.revision) {
25
+ throw new Error('电脑文件已变化,请重新添加。');
26
+ }
27
+ };
28
+ await validate();
29
+ const bytes = Buffer.alloc(stat.size);
30
+ for (let offset = 0; offset < bytes.length;) {
31
+ const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
32
+ if (!bytesRead) throw new Error('电脑文件读取不完整,请重新添加。');
33
+ offset += bytesRead;
34
+ }
35
+ await validate();
36
+ const sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
37
+ const started = await session.receive({ type: 'start', name: file.name, size: bytes.length, sha256 });
38
+ for (let offset = 0; offset < bytes.length; offset += FILE_CHUNK_BYTES) {
39
+ await session.receive({ type: 'chunk', id: started.id, offset, data: bytes.subarray(offset, offset + FILE_CHUNK_BYTES) });
40
+ }
41
+ await validate();
42
+ const result = await session.receive({ type: 'finish', id: started.id });
43
+ await session.receive({ type: 'commit' });
44
+ return { ...result, mediaKind: media.mediaKind(file),
45
+ mimeType: media.mediaMime(file) || mimeTypes[path.extname(file.name).slice(1).toLowerCase()] || 'application/octet-stream' };
46
+ } finally { await handle.close(); await session.close(); }
47
+ }
48
+
49
+ async function localFile(file, directory) {
50
+ if (typeof file?.path !== 'string' || typeof file.name !== 'string' || !file.name || /[\r\n\0]/.test(file.name)) {
51
+ throw new Error('附件信息无效,请重新选择。');
52
+ }
53
+ const root = await fs.realpath(directory), resolved = await fs.realpath(file.path);
54
+ if (path.relative(root, path.dirname(resolved)) !== '') throw new Error('附件必须来自本次工作站上传目录。');
55
+ const handle = await fs.open(resolved, 'r');
56
+ try {
57
+ const stat = await handle.stat();
58
+ if (!stat.isFile() || stat.size < 1 || stat.size > MAX_FILE_BYTES) throw new Error('附件为空或超过 20 MiB。');
59
+ const bytes = await handle.readFile();
60
+ if (bytes.length !== stat.size) throw new Error('附件在读取时发生变化,请重新上传。');
61
+ const mimeType = media.mediaMime(file) || mimeTypes[path.extname(file.name).slice(1).toLowerCase()] || 'application/octet-stream';
62
+ if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/i.test(mimeType)) throw new Error('附件类型无效。');
63
+ return { name: file.name, mimeType, bytes };
64
+ } finally { await handle.close(); }
65
+ }
66
+
67
+ export async function readMessageUploads(body, uploadsRoot) {
68
+ const images = body.images ?? [], originals = body.originalImages ?? [], files = body.files ?? [];
69
+ if (![images, originals, files].every(Array.isArray) || images.length + originals.length > 4 || files.length + originals.length > MAX_FILE_COUNT) {
70
+ throw new Error('每条消息最多 4 张图片、4 个文件。');
71
+ }
72
+ const uploads = [];
73
+ let imageBytes = 0, fileBytes = 0;
74
+ for (const image of images) {
75
+ let file;
76
+ if (typeof image === 'string' && image.startsWith('data:')) {
77
+ validateInlineImage(image);
78
+ const mimeType = image.slice(5, image.indexOf(';'));
79
+ file = { name: `image.${mimeType.split('/')[1]}`, mimeType, bytes: Buffer.from(image.slice(image.indexOf(',') + 1), 'base64') };
80
+ } else {
81
+ if (typeof image !== 'string' || !/^[a-f0-9]{64}$/.test(image)) throw new Error('图片标识无效。');
82
+ let extension = 'webp';
83
+ try { await fs.access(path.join(uploadsRoot, `${image}.webp`)); }
84
+ catch (error) { if (error.code !== 'ENOENT') throw error; extension = 'png'; }
85
+ file = await localFile({ path: path.join(uploadsRoot, `${image}.${extension}`), name: `image.${extension}` }, uploadsRoot);
86
+ }
87
+ imageBytes += file.bytes.length;
88
+ if (imageBytes > 8 * 1024 * 1024) throw new Error('压缩图片合计不能超过 8 MiB。');
89
+ uploads.push(file);
90
+ }
91
+ for (const original of originals) {
92
+ if (typeof original !== 'string' || !/\.(png|jpe?g|webp|gif)$/i.test(original)) throw new Error('原图路径无效。');
93
+ }
94
+ for (const file of [...originals.map(value => ({ path: value, name: path.basename(value) })), ...files]) {
95
+ const uploaded = await localFile(file, path.join(uploadsRoot, 'incoming'));
96
+ fileBytes += uploaded.bytes.length;
97
+ if (fileBytes > MAX_TOTAL_BYTES) throw new Error('文件合计不能超过 40 MiB。');
98
+ uploads.push(uploaded);
99
+ }
100
+ for (const file of uploads) {
101
+ if (/^image\/(?:png|jpeg|webp|gif)$/.test(file.mimeType)) {
102
+ const metadata = await sharp(file.bytes).metadata();
103
+ file.width = metadata.width; file.height = metadata.height;
104
+ if (!file.width || !file.height) throw new Error('无法读取 图片尺寸。');
105
+ }
106
+ }
107
+ if (uploads.filter(file => file.width).length > 4) throw new Error('每条消息最多 4 张图片。');
108
+ return uploads;
109
+ }
@@ -1,12 +1,12 @@
1
- export function phonePairingSteps(_transportMode, paired = false) {
2
- return paired ? ['已配对设备可直接连接。添加设备:relax pair']
3
- : ['生成配对二维码:relax pair', '用 Code Relax App 扫码添加工作站。'];
4
- }
5
- export const desktopSetupSteps = [
6
- '工作站服务和 Codex 内核已就绪,已启用登录自启。',
7
- '生成配对二维码:relax pair',
8
- ];
9
-
10
- export function pairingDisplayText(nextSteps) {
11
- return ['Code Relax 工作站已启动。', ...nextSteps].join('\n');
12
- }
1
+ export function phonePairingSteps(_transportMode, paired = false) {
2
+ return paired ? ['已配对设备可直接连接。添加设备:relax pair']
3
+ : ['生成配对二维码:relax pair', '用 Code Relax App 扫码添加工作站。'];
4
+ }
5
+ export const desktopSetupSteps = [
6
+ '工作站服务和 Codex 内核已就绪,已启用登录自启。',
7
+ '生成配对二维码:relax pair',
8
+ ];
9
+
10
+ export function pairingDisplayText(nextSteps) {
11
+ return ['Code Relax 工作站已启动。', ...nextSteps].join('\n');
12
+ }
@@ -9,4 +9,4 @@
9
9
 
10
10
  Windows 专有脚本兼容系统自带 PowerShell 5.1。通用生命周期使用 Node 接口;系统检测和凭据保护不扩散到业务代码。
11
11
 
12
- macOS、Linux 目录仅保留规划说明,尚无路径、IPC、进程或安装实现。接入排在控制端扩展之后,并需分别验证 Codex Desktop/app-server 的可用连接方式;只有独立 app-server 时不能宣称接管 Desktop 当前任务。
12
+ macOS、Linux 目录仅保留规划说明,尚无路径、IPC、进程或安装实现。接入排在控制端扩展之后,并需分别验证 Codex Desktop/app-server 的可用连接方式;只有独立 app-server 时不能宣称接管 Desktop 当前任务。
@@ -1,46 +1,46 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { execFile } from 'node:child_process';
4
- import { promisify } from 'node:util';
5
-
6
- const exec = promisify(execFile);
7
- const requiredMethods = ['thread/start', 'thread/resume', 'thread/list', 'thread/turns/list',
8
- 'thread/items/list', 'turn/start', 'turn/steer', 'turn/interrupt', 'model/list', 'project/list',
9
- 'project/create', 'project/update', 'project/move',
10
- 'fs/getMetadata', 'fs/readDirectory', 'fs/createDirectory', 'command/exec',
11
- 'thread/settings/update', 'configRequirements/read', 'thread/queue/list', 'thread/queue/add',
12
- 'thread/queue/delete', 'thread/queue/start', 'thread/read', 'thread/unsubscribe', 'config/read', 'account/rateLimits/read'];
13
-
14
- export function compareVersions(a, b) {
15
- const parse = v => v.split(/[.-]/).map(part => /^\d+$/.test(part) ? Number(part) : part);
16
- const aa = parse(a), bb = parse(b);
17
- for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
18
- if (aa[i] === bb[i]) continue;
19
- if (aa[i] === undefined) return 1;
20
- if (bb[i] === undefined) return -1;
21
- return typeof aa[i] === 'number' && typeof bb[i] === 'number' ? aa[i] - bb[i] : String(aa[i]).localeCompare(String(bb[i]));
22
- }
23
- return 0;
24
- }
25
-
26
- export async function verifyProtocol(executable, stateRoot) {
27
- const directory = fs.mkdtempSync(path.join(stateRoot, 'cli-schema-'));
28
- try {
29
- await exec(executable, ['app-server', 'generate-json-schema', '--experimental', '--out', directory],
30
- { windowsHide: true, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
31
- const request = fs.readFileSync(path.join(directory, 'ClientRequest.json'), 'utf8');
32
- const missing = requiredMethods.filter(method => !request.includes(`"${method}"`));
33
- const turns = fs.readFileSync(path.join(directory, 'v2/ThreadTurnsListParams.json'), 'utf8');
34
- if (!turns.includes('"itemsView"') || !turns.includes('"summary"')) missing.push('turn summary pagination');
35
- if (missing.length) throw new Error(`缺少协议能力:${missing.join(', ')}`);
36
- } finally {
37
- const remove = dir => {
38
- for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
39
- const file = path.join(dir, item.name);
40
- if (item.isDirectory()) remove(file); else fs.unlinkSync(file);
41
- }
42
- fs.rmdirSync(dir);
43
- };
44
- remove(directory);
45
- }
46
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+
6
+ const exec = promisify(execFile);
7
+ const requiredMethods = ['thread/start', 'thread/resume', 'thread/list', 'thread/turns/list',
8
+ 'thread/items/list', 'turn/start', 'turn/steer', 'turn/interrupt', 'model/list', 'project/list',
9
+ 'project/create', 'project/update', 'project/move',
10
+ 'fs/getMetadata', 'fs/readDirectory', 'fs/createDirectory', 'command/exec',
11
+ 'thread/settings/update', 'configRequirements/read', 'thread/queue/list', 'thread/queue/add',
12
+ 'thread/queue/delete', 'thread/queue/start', 'thread/read', 'thread/unsubscribe', 'config/read', 'account/rateLimits/read'];
13
+
14
+ export function compareVersions(a, b) {
15
+ const parse = v => v.split(/[.-]/).map(part => /^\d+$/.test(part) ? Number(part) : part);
16
+ const aa = parse(a), bb = parse(b);
17
+ for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
18
+ if (aa[i] === bb[i]) continue;
19
+ if (aa[i] === undefined) return 1;
20
+ if (bb[i] === undefined) return -1;
21
+ return typeof aa[i] === 'number' && typeof bb[i] === 'number' ? aa[i] - bb[i] : String(aa[i]).localeCompare(String(bb[i]));
22
+ }
23
+ return 0;
24
+ }
25
+
26
+ export async function verifyProtocol(executable, stateRoot) {
27
+ const directory = fs.mkdtempSync(path.join(stateRoot, 'cli-schema-'));
28
+ try {
29
+ await exec(executable, ['app-server', 'generate-json-schema', '--experimental', '--out', directory],
30
+ { windowsHide: true, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
31
+ const request = fs.readFileSync(path.join(directory, 'ClientRequest.json'), 'utf8');
32
+ const missing = requiredMethods.filter(method => !request.includes(`"${method}"`));
33
+ const turns = fs.readFileSync(path.join(directory, 'v2/ThreadTurnsListParams.json'), 'utf8');
34
+ if (!turns.includes('"itemsView"') || !turns.includes('"summary"')) missing.push('turn summary pagination');
35
+ if (missing.length) throw new Error(`缺少协议能力:${missing.join(', ')}`);
36
+ } finally {
37
+ const remove = dir => {
38
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
39
+ const file = path.join(dir, item.name);
40
+ if (item.isDirectory()) remove(file); else fs.unlinkSync(file);
41
+ }
42
+ fs.rmdirSync(dir);
43
+ };
44
+ remove(directory);
45
+ }
46
+ }