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

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 (89) 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/build-platform.json +1 -1
  7. package/dist/get-shared-service-url.ps1 +14 -14
  8. package/dist/shared/code-browser.cjs +159 -159
  9. package/dist/shared/file-changes.cjs +31 -31
  10. package/dist/shared/file-citations.cjs +117 -117
  11. package/dist/shared/file-mentions.cjs +92 -92
  12. package/dist/shared/message-delivery.cjs +115 -115
  13. package/dist/shared/p2p-file-channel.cjs +87 -87
  14. package/dist/shared/server-requests.cjs +311 -311
  15. package/dist/shared/timeline-sync.cjs +173 -173
  16. package/dist/skills/code-relax/SKILL.md +22 -22
  17. package/dist/src/bounded-process-log.mjs +63 -63
  18. package/dist/src/celsius-client.mjs +107 -107
  19. package/dist/src/dots-approval-timeline.mjs +99 -99
  20. package/dist/src/dots-avatar.mjs +42 -42
  21. package/dist/src/dots-chat-projection.mjs +50 -50
  22. package/dist/src/dots-chat-server.mjs +483 -483
  23. package/dist/src/dots-message-images.mjs +319 -319
  24. package/dist/src/dots-profile.mjs +77 -77
  25. package/dist/src/dots-room-activity.mjs +172 -172
  26. package/dist/src/dots-room-client.mjs +135 -135
  27. package/dist/src/dots-room-projection.mjs +94 -94
  28. package/dist/src/dots-schedules.mjs +66 -66
  29. package/dist/src/dots-summary-tasks.mjs +32 -32
  30. package/dist/src/dots-task-catalog.mjs +430 -430
  31. package/dist/src/file-transfer-range.mjs +22 -22
  32. package/dist/src/file-upload-session.mjs +130 -130
  33. package/dist/src/hosted-history-app-server.mjs +103 -103
  34. package/dist/src/hosted-history-cache.mjs +113 -113
  35. package/dist/src/lan-socket-server.mjs +266 -266
  36. package/dist/src/managed-app-server.mjs +373 -373
  37. package/dist/src/managed-endpoint.mjs +25 -25
  38. package/dist/src/message-upload-files.mjs +109 -109
  39. package/dist/src/onboarding.mjs +12 -12
  40. package/dist/src/platform/README.md +1 -1
  41. package/dist/src/platform/codex-protocol.mjs +46 -46
  42. package/dist/src/platform/desktop-project-store.mjs +208 -208
  43. package/dist/src/platform/linux/desktop-host.mjs +87 -87
  44. package/dist/src/platform/linux/process.mjs +53 -53
  45. package/dist/src/platform/linux/system.mjs +50 -50
  46. package/dist/src/platform/linux/user-environment.mjs +9 -9
  47. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  48. package/dist/src/platform/macos/desktop-host.mjs +86 -86
  49. package/dist/src/platform/macos/user-environment.mjs +9 -9
  50. package/dist/src/platform/native-package.mjs +58 -58
  51. package/dist/src/platform/windows/activate-desktop.ps1 +49 -49
  52. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  53. package/dist/src/platform/windows/credential-store.mjs +27 -27
  54. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  55. package/dist/src/platform/windows/desktop-host.mjs +65 -65
  56. package/dist/src/platform/windows/desktop-info.ps1 +38 -38
  57. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  58. package/dist/src/platform/windows/desktop-shortcuts.ps1 +14 -14
  59. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  60. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  61. package/dist/src/platform/windows/project-locations.mjs +16 -16
  62. package/dist/src/platform/windows/service-host.mjs +35 -35
  63. package/dist/src/platform/windows/user-environment.mjs +52 -52
  64. package/dist/src/project-management.mjs +58 -58
  65. package/dist/src/runtime-discovery.mjs +47 -47
  66. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  67. package/dist/src/self-relay/admin-page.mjs +80 -80
  68. package/dist/src/self-relay/admin-style.mjs +14 -14
  69. package/dist/src/self-relay/connector.mjs +439 -439
  70. package/dist/src/self-relay/demo.mjs +31 -31
  71. package/dist/src/self-relay/diagnostics.mjs +54 -54
  72. package/dist/src/self-relay/email-accounts.mjs +183 -183
  73. package/dist/src/self-relay/file-wire.mjs +13 -13
  74. package/dist/src/self-relay/p2p-probe.mjs +168 -168
  75. package/dist/src/self-relay/privacy-page.mjs +26 -26
  76. package/dist/src/self-relay/public-releases.mjs +67 -67
  77. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  78. package/dist/src/self-relay/usage-meter.mjs +141 -141
  79. package/dist/src/server.mjs +5 -4
  80. package/dist/src/service-doctor.mjs +148 -148
  81. package/dist/src/service-install.mjs +97 -97
  82. package/dist/src/service-lifecycle.mjs +139 -139
  83. package/dist/src/shared-app-server.mjs +666 -666
  84. package/dist/src/shared-catalog.mjs +132 -132
  85. package/dist/src/shared-thread-stream.mjs +165 -165
  86. package/dist/src/usage-reset.mjs +17 -17
  87. package/dist/src/workstation-downloads.mjs +120 -120
  88. package/package.json +4 -4
  89. 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
+ }