@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.32

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 (187) hide show
  1. package/README.md +95 -89
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +374 -367
  4. package/dist/bin/manage-relay-admin.mjs +20 -20
  5. package/dist/bin/migrate-relay-db.mjs +109 -109
  6. package/dist/bin/publish-android-release.mjs +63 -58
  7. package/dist/bin/publish-harmony-release.mjs +46 -0
  8. package/dist/bin/release-upload-server.mjs +174 -0
  9. package/dist/bin/self-relay-admin.mjs +8 -8
  10. package/dist/bin/self-relay-demo.mjs +33 -33
  11. package/dist/bin/self-relay-server.mjs +66 -66
  12. package/dist/build-platform.json +1 -1
  13. package/dist/get-lan-networks.ps1 +40 -40
  14. package/dist/get-shared-service-url.ps1 +6 -6
  15. package/dist/shared/app-server-events.cjs +422 -412
  16. package/dist/shared/code-browser.cjs +159 -159
  17. package/dist/shared/file-changes.cjs +31 -31
  18. package/dist/shared/file-citations.cjs +117 -117
  19. package/dist/shared/file-mentions.cjs +92 -92
  20. package/dist/shared/message-delivery.cjs +54 -54
  21. package/dist/shared/p2p-data-channel.cjs +72 -72
  22. package/dist/shared/p2p-diagnostics.cjs +28 -28
  23. package/dist/shared/p2p-file-channel.cjs +78 -74
  24. package/dist/shared/plan-pricing.cjs +32 -32
  25. package/dist/shared/question-attention.cjs +69 -0
  26. package/dist/shared/server-requests.cjs +219 -219
  27. package/dist/shared/timeline-sync.cjs +173 -173
  28. package/dist/skills/code-relax/SKILL.md +22 -22
  29. package/dist/src/archived-threads.mjs +22 -0
  30. package/dist/src/background-process.mjs +18 -18
  31. package/dist/src/bounded-process-log.mjs +63 -63
  32. package/dist/src/celsius-client.mjs +107 -107
  33. package/dist/src/code-browser.mjs +100 -100
  34. package/dist/src/desktop-launcher.mjs +46 -46
  35. package/dist/src/dots-approval-timeline.mjs +99 -99
  36. package/dist/src/dots-avatar.mjs +42 -42
  37. package/dist/src/dots-chat-projection.mjs +50 -50
  38. package/dist/src/dots-chat-server.mjs +461 -461
  39. package/dist/src/dots-message-images.mjs +319 -319
  40. package/dist/src/dots-profile.mjs +77 -77
  41. package/dist/src/dots-room-activity.mjs +94 -94
  42. package/dist/src/dots-room-client.mjs +135 -135
  43. package/dist/src/dots-room-projection.mjs +94 -94
  44. package/dist/src/dots-schedules.mjs +66 -66
  45. package/dist/src/dots-summary-tasks.mjs +32 -32
  46. package/dist/src/dots-task-catalog.mjs +430 -430
  47. package/dist/src/dots-task-files.mjs +52 -52
  48. package/dist/src/execution-health.mjs +26 -26
  49. package/dist/src/file-transfer-range.mjs +22 -22
  50. package/dist/src/file-upload-session.mjs +130 -130
  51. package/dist/src/hosted-history-app-server.mjs +103 -103
  52. package/dist/src/hosted-history-cache.mjs +113 -113
  53. package/dist/src/lan-socket-server.mjs +265 -263
  54. package/dist/src/managed-app-server.mjs +244 -244
  55. package/dist/src/message-images.mjs +9 -9
  56. package/dist/src/message-upload-files.mjs +74 -74
  57. package/dist/src/onboarding.mjs +12 -12
  58. package/dist/src/plan-pricing.mjs +47 -47
  59. package/dist/src/platform/README.md +12 -12
  60. package/dist/src/platform/codex-protocol.mjs +46 -46
  61. package/dist/src/platform/codex-runtime.mjs +3 -3
  62. package/dist/src/platform/credential-store.mjs +3 -3
  63. package/dist/src/platform/desktop-host.mjs +3 -3
  64. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  65. package/dist/src/platform/desktop-project-store.mjs +208 -149
  66. package/dist/src/platform/file-opener.mjs +9 -0
  67. package/dist/src/platform/linux/README.md +3 -3
  68. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  69. package/dist/src/platform/linux/credential-store.mjs +28 -0
  70. package/dist/src/platform/linux/desktop-host.mjs +83 -0
  71. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  72. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  73. package/dist/src/platform/linux/network-info.mjs +11 -0
  74. package/dist/src/platform/linux/process.mjs +42 -0
  75. package/dist/src/platform/linux/project-locations.mjs +17 -0
  76. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  77. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  78. package/dist/src/platform/linux/service-host.mjs +18 -0
  79. package/dist/src/platform/linux/system.mjs +49 -0
  80. package/dist/src/platform/linux/user-environment.mjs +7 -0
  81. package/dist/src/platform/macos/README.md +3 -3
  82. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  83. package/dist/src/platform/macos/credential-store.mjs +10 -10
  84. package/dist/src/platform/macos/desktop-host.mjs +82 -82
  85. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
  86. package/dist/src/platform/macos/network-info.mjs +8 -28
  87. package/dist/src/platform/macos/project-locations.mjs +8 -8
  88. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  89. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  90. package/dist/src/platform/macos/service-host.mjs +18 -18
  91. package/dist/src/platform/macos/system.mjs +29 -29
  92. package/dist/src/platform/macos/user-environment.mjs +7 -7
  93. package/dist/src/platform/native-package.mjs +58 -46
  94. package/dist/src/platform/network-info.mjs +3 -3
  95. package/dist/src/platform/path-identity.mjs +5 -5
  96. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  97. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  98. package/dist/src/platform/project-locations.mjs +3 -3
  99. package/dist/src/platform/runtime-paths.mjs +8 -8
  100. package/dist/src/platform/select.mjs +5 -5
  101. package/dist/src/platform/service-host.mjs +3 -3
  102. package/dist/src/platform/user-environment.mjs +3 -3
  103. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  104. package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
  105. package/dist/src/platform/windows/background-process.mjs +30 -30
  106. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  107. package/dist/src/platform/windows/credential-store.mjs +27 -27
  108. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  109. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  110. package/dist/src/platform/windows/desktop-host.mjs +58 -58
  111. package/dist/src/platform/windows/desktop-info.ps1 +14 -14
  112. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  113. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  114. package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
  115. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  116. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  117. package/dist/src/platform/windows/network-info.mjs +34 -34
  118. package/dist/src/platform/windows/project-locations.mjs +16 -16
  119. package/dist/src/platform/windows/resident-start.js +20 -20
  120. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  121. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  122. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  123. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  124. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  125. package/dist/src/platform/windows/service-host.mjs +31 -31
  126. package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
  127. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  128. package/dist/src/platform/windows/user-environment.mjs +50 -50
  129. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  130. package/dist/src/project-archive.mjs +30 -0
  131. package/dist/src/project-create.mjs +121 -119
  132. package/dist/src/project-management.mjs +59 -69
  133. package/dist/src/project-remove.mjs +55 -0
  134. package/dist/src/project-update.mjs +56 -0
  135. package/dist/src/runtime-discovery.mjs +47 -47
  136. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  137. package/dist/src/self-relay/account-connections.mjs +52 -52
  138. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  139. package/dist/src/self-relay/admin-page.mjs +80 -80
  140. package/dist/src/self-relay/admin-server.mjs +74 -74
  141. package/dist/src/self-relay/admin-state.mjs +127 -127
  142. package/dist/src/self-relay/admin-style.mjs +14 -14
  143. package/dist/src/self-relay/android-releases.mjs +61 -61
  144. package/dist/src/self-relay/client.mjs +61 -61
  145. package/dist/src/self-relay/connector.mjs +439 -438
  146. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  147. package/dist/src/self-relay/demo.mjs +37 -37
  148. package/dist/src/self-relay/device-info.mjs +27 -0
  149. package/dist/src/self-relay/device-store.mjs +92 -84
  150. package/dist/src/self-relay/diagnostics.mjs +54 -54
  151. package/dist/src/self-relay/email-accounts.mjs +183 -183
  152. package/dist/src/self-relay/file-wire.mjs +13 -13
  153. package/dist/src/self-relay/lan-authorizations.mjs +71 -42
  154. package/dist/src/self-relay/lifecycle.mjs +264 -250
  155. package/dist/src/self-relay/long-pairing.mjs +40 -40
  156. package/dist/src/self-relay/mysql-state.mjs +118 -105
  157. package/dist/src/self-relay/p2p-probe.mjs +159 -159
  158. package/dist/src/self-relay/privacy-page.mjs +26 -26
  159. package/dist/src/self-relay/public-releases.mjs +52 -0
  160. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  161. package/dist/src/self-relay/server.mjs +1006 -932
  162. package/dist/src/self-relay/usage-daily.mjs +55 -55
  163. package/dist/src/self-relay/usage-meter.mjs +141 -141
  164. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  165. package/dist/src/self-relay/wire.mjs +36 -36
  166. package/dist/src/server.mjs +1861 -1778
  167. package/dist/src/service-control.mjs +82 -82
  168. package/dist/src/service-doctor.mjs +110 -110
  169. package/dist/src/service-health.mjs +33 -33
  170. package/dist/src/service-install.mjs +91 -91
  171. package/dist/src/service-lifecycle.mjs +138 -138
  172. package/dist/src/service-restart.mjs +81 -81
  173. package/dist/src/shared-app-server.mjs +615 -611
  174. package/dist/src/shared-catalog.mjs +132 -132
  175. package/dist/src/shared-recovery.mjs +146 -146
  176. package/dist/src/shared-thread-stream.mjs +164 -164
  177. package/dist/src/skill-install.mjs +51 -51
  178. package/dist/src/thread-title-generation.mjs +150 -150
  179. package/dist/src/turn-model-history.mjs +47 -47
  180. package/dist/src/usage-reset.mjs +17 -17
  181. package/dist/src/workstation-downloads.mjs +120 -120
  182. package/dist/tools/rtc-route-helper.ps1 +99 -99
  183. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  184. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  185. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  186. package/package.json +7 -6
  187. package/tools/postinstall.mjs +127 -127
@@ -1,120 +1,120 @@
1
- import fs from 'node:fs/promises';
2
- import { constants } from 'node:fs';
3
- import path from 'node:path';
4
- import crypto from 'node:crypto';
5
- import { fileRevision, FILE_PART_BYTES, FILE_READ_LIMIT } from './file-transfer-range.mjs';
6
- import { readRecord, writeRecord } from './service-lifecycle.mjs';
7
-
8
- const fail = (message, statusCode = 400) => { throw Object.assign(new Error(message), { statusCode }); };
9
- const pathKey = value => process.platform === 'win32' ? value.toLowerCase() : value;
10
- const validName = value => typeof value === 'string' && value.length > 0
11
- && !/[<>:"/\\|?*\x00-\x1f]/.test(value) && !/[ .]$/.test(value) && !['.', '..'].includes(value)
12
- && !/^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value);
13
-
14
- // A saved copy belongs to the authorized external asset, rather than its current chat page.
15
- export class WorkstationDownloads {
16
- constructor(stateRoot) {
17
- this.recordPath = path.join(stateRoot, 'external-file-copies.json');
18
- this.copies = readRecord(this.recordPath) || {};
19
- }
20
-
21
- async copy(sourceId) {
22
- const record = this.copies[sourceId];
23
- if (!record) return null;
24
- try {
25
- const stat = await fs.stat(record.path);
26
- return stat.isFile() && fileRevision(stat) === record.revision && stat.size === record.size ? record : null;
27
- } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
28
- }
29
-
30
- async folder(rpc, locations, selection) {
31
- let root;
32
- if (selection.target === 'downloads') root = locations.downloads;
33
- else if (selection.target === 'project' && typeof selection.projectId === 'string' && selection.projectId) {
34
- const { project } = await rpc('project/read', { projectId: selection.projectId });
35
- const selected = selection.root || project.roots[0]?.path;
36
- root = project.roots.find(item => pathKey(item.path) === pathKey(selected))?.path;
37
- }
38
- if (!root || !path.isAbsolute(root)) fail('请选择电脑项目或下载目录。');
39
- const relative = selection.path || '';
40
- if (typeof relative !== 'string' || relative.split('/').some(part => part && !validName(part))) fail('子文件夹路径无效。');
41
- const realRoot = await fs.realpath(root), target = path.join(realRoot, ...relative.split('/'));
42
- const realTarget = await fs.realpath(target), difference = path.relative(realRoot, realTarget);
43
- if (difference === '..' || difference.startsWith('..' + path.sep) || path.isAbsolute(difference)) fail('只能选择当前目录内的子文件夹。');
44
- if (!(await fs.stat(realTarget)).isDirectory()) fail('所选路径不是文件夹。');
45
- return { root, path: relative, absolutePath: target, realPath: realTarget,
46
- parent: relative ? relative.split('/').slice(0, -1).join('/') : null };
47
- }
48
-
49
- async browse(rpc, locations, selection) {
50
- const folder = await this.folder(rpc, locations, selection);
51
- const entries = (await fs.readdir(folder.realPath, { withFileTypes: true })).filter(entry => entry.isDirectory())
52
- .map(entry => ({ name: entry.name, path: [folder.path, entry.name].filter(Boolean).join('/') }))
53
- .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { numeric: true }));
54
- const { realPath, ...visible } = folder;
55
- return { ...visible, entries };
56
- }
57
-
58
- async save(rpc, locations, selection, asset, readPart, signal) {
59
- const folder = await this.folder(rpc, locations, selection);
60
- const partial = path.join(folder.realPath, `.code-relax-${crypto.randomUUID()}.download.part`);
61
- let handle, result, created = false;
62
- try {
63
- signal?.throwIfAborted();
64
- handle = await fs.open(partial, 'wx', 0o600);
65
- created = true;
66
- let offset = 0, size, revision, name, mimeType;
67
- do {
68
- signal?.throwIfAborted();
69
- const query = new URLSearchParams({ offset: String(offset), length: String(FILE_PART_BYTES) });
70
- if (revision) query.set('revision', revision);
71
- const part = await readPart(asset, query);
72
- signal?.throwIfAborted();
73
- if (offset === 0) {
74
- size = part.size; revision = part.revision; name = part.name || asset.name; mimeType = part.mimeType || asset.mimeType;
75
- if (!validName(name)) fail('文件名无效,请修正来源文件的名称。');
76
- if (!Number.isSafeInteger(size) || size < 1 || size > FILE_READ_LIMIT || !revision) fail('文件大小无效或超过 20 MiB 上限。');
77
- }
78
- if (part.size !== size || part.revision !== revision || part.offset !== offset
79
- || !part.bytes?.length || part.bytes.length > Math.min(FILE_PART_BYTES, size - offset)) fail('文件内容发生变化或分片不完整。', 409);
80
- let written = 0;
81
- while (written < part.bytes.length) {
82
- const { bytesWritten } = await handle.write(part.bytes, written, part.bytes.length - written, offset + written);
83
- if (!bytesWritten) fail('电脑文件写入不完整。', 502);
84
- written += bytesWritten;
85
- }
86
- offset += part.bytes.length;
87
- } while (offset < size);
88
- await handle.close(); handle = null;
89
- signal?.throwIfAborted();
90
- // Recheck the destination after the remote read, before creating the final copy.
91
- const current = await this.folder(rpc, locations, selection);
92
- if (pathKey(current.realPath) !== pathKey(folder.realPath)) fail('目标目录发生变化,请重新选择。', 409);
93
- const extension = path.extname(name), body = name.slice(0, name.length - extension.length);
94
- for (let number = 0; ; number++) {
95
- const filename = number ? `${body} (${number})${extension}` : name;
96
- const destination = path.join(folder.realPath, filename);
97
- try {
98
- await fs.copyFile(partial, destination, constants.COPYFILE_EXCL);
99
- const stat = await fs.stat(destination);
100
- if (stat.size !== size) { await fs.unlink(destination); fail('电脑副本大小核对失败。', 502); }
101
- result = { path: destination, name: filename, size, revision: fileRevision(stat), mimeType };
102
- break;
103
- } catch (error) { if (error.code !== 'EEXIST') throw error; }
104
- }
105
- this.copies[asset.id] = result;
106
- try { writeRecord(this.recordPath, this.copies); }
107
- catch (error) { result.warning = '文件已保存,副本记录未能持久保存;重启后请从原文件重新选择。'; console.error('电脑副本记录保存失败', error); }
108
- return result;
109
- } finally {
110
- await handle?.close();
111
- if (created) {
112
- try { await fs.rm(partial, { force: true }); }
113
- catch (error) {
114
- if (!result) throw error;
115
- result.warning = '文件已保存,下载临时文件未能清理。'; console.error('电脑下载临时文件清理失败', error);
116
- }
117
- }
118
- }
119
- }
120
- }
1
+ import fs from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { fileRevision, FILE_PART_BYTES, FILE_READ_LIMIT } from './file-transfer-range.mjs';
6
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
7
+
8
+ const fail = (message, statusCode = 400) => { throw Object.assign(new Error(message), { statusCode }); };
9
+ const pathKey = value => process.platform === 'win32' ? value.toLowerCase() : value;
10
+ const validName = value => typeof value === 'string' && value.length > 0
11
+ && !/[<>:"/\\|?*\x00-\x1f]/.test(value) && !/[ .]$/.test(value) && !['.', '..'].includes(value)
12
+ && !/^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value);
13
+
14
+ // A saved copy belongs to the authorized external asset, rather than its current chat page.
15
+ export class WorkstationDownloads {
16
+ constructor(stateRoot) {
17
+ this.recordPath = path.join(stateRoot, 'external-file-copies.json');
18
+ this.copies = readRecord(this.recordPath) || {};
19
+ }
20
+
21
+ async copy(sourceId) {
22
+ const record = this.copies[sourceId];
23
+ if (!record) return null;
24
+ try {
25
+ const stat = await fs.stat(record.path);
26
+ return stat.isFile() && fileRevision(stat) === record.revision && stat.size === record.size ? record : null;
27
+ } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
28
+ }
29
+
30
+ async folder(rpc, locations, selection) {
31
+ let root;
32
+ if (selection.target === 'downloads') root = locations.downloads;
33
+ else if (selection.target === 'project' && typeof selection.projectId === 'string' && selection.projectId) {
34
+ const { project } = await rpc('project/read', { projectId: selection.projectId });
35
+ const selected = selection.root || project.roots[0]?.path;
36
+ root = project.roots.find(item => pathKey(item.path) === pathKey(selected))?.path;
37
+ }
38
+ if (!root || !path.isAbsolute(root)) fail('请选择电脑项目或下载目录。');
39
+ const relative = selection.path || '';
40
+ if (typeof relative !== 'string' || relative.split('/').some(part => part && !validName(part))) fail('子文件夹路径无效。');
41
+ const realRoot = await fs.realpath(root), target = path.join(realRoot, ...relative.split('/'));
42
+ const realTarget = await fs.realpath(target), difference = path.relative(realRoot, realTarget);
43
+ if (difference === '..' || difference.startsWith('..' + path.sep) || path.isAbsolute(difference)) fail('只能选择当前目录内的子文件夹。');
44
+ if (!(await fs.stat(realTarget)).isDirectory()) fail('所选路径不是文件夹。');
45
+ return { root, path: relative, absolutePath: target, realPath: realTarget,
46
+ parent: relative ? relative.split('/').slice(0, -1).join('/') : null };
47
+ }
48
+
49
+ async browse(rpc, locations, selection) {
50
+ const folder = await this.folder(rpc, locations, selection);
51
+ const entries = (await fs.readdir(folder.realPath, { withFileTypes: true })).filter(entry => entry.isDirectory())
52
+ .map(entry => ({ name: entry.name, path: [folder.path, entry.name].filter(Boolean).join('/') }))
53
+ .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN', { numeric: true }));
54
+ const { realPath, ...visible } = folder;
55
+ return { ...visible, entries };
56
+ }
57
+
58
+ async save(rpc, locations, selection, asset, readPart, signal) {
59
+ const folder = await this.folder(rpc, locations, selection);
60
+ const partial = path.join(folder.realPath, `.code-relax-${crypto.randomUUID()}.download.part`);
61
+ let handle, result, created = false;
62
+ try {
63
+ signal?.throwIfAborted();
64
+ handle = await fs.open(partial, 'wx', 0o600);
65
+ created = true;
66
+ let offset = 0, size, revision, name, mimeType;
67
+ do {
68
+ signal?.throwIfAborted();
69
+ const query = new URLSearchParams({ offset: String(offset), length: String(FILE_PART_BYTES) });
70
+ if (revision) query.set('revision', revision);
71
+ const part = await readPart(asset, query);
72
+ signal?.throwIfAborted();
73
+ if (offset === 0) {
74
+ size = part.size; revision = part.revision; name = part.name || asset.name; mimeType = part.mimeType || asset.mimeType;
75
+ if (!validName(name)) fail('文件名无效,请修正来源文件的名称。');
76
+ if (!Number.isSafeInteger(size) || size < 1 || size > FILE_READ_LIMIT || !revision) fail('文件大小无效或超过 20 MiB 上限。');
77
+ }
78
+ if (part.size !== size || part.revision !== revision || part.offset !== offset
79
+ || !part.bytes?.length || part.bytes.length > Math.min(FILE_PART_BYTES, size - offset)) fail('文件内容发生变化或分片不完整。', 409);
80
+ let written = 0;
81
+ while (written < part.bytes.length) {
82
+ const { bytesWritten } = await handle.write(part.bytes, written, part.bytes.length - written, offset + written);
83
+ if (!bytesWritten) fail('电脑文件写入不完整。', 502);
84
+ written += bytesWritten;
85
+ }
86
+ offset += part.bytes.length;
87
+ } while (offset < size);
88
+ await handle.close(); handle = null;
89
+ signal?.throwIfAborted();
90
+ // Recheck the destination after the remote read, before creating the final copy.
91
+ const current = await this.folder(rpc, locations, selection);
92
+ if (pathKey(current.realPath) !== pathKey(folder.realPath)) fail('目标目录发生变化,请重新选择。', 409);
93
+ const extension = path.extname(name), body = name.slice(0, name.length - extension.length);
94
+ for (let number = 0; ; number++) {
95
+ const filename = number ? `${body} (${number})${extension}` : name;
96
+ const destination = path.join(folder.realPath, filename);
97
+ try {
98
+ await fs.copyFile(partial, destination, constants.COPYFILE_EXCL);
99
+ const stat = await fs.stat(destination);
100
+ if (stat.size !== size) { await fs.unlink(destination); fail('电脑副本大小核对失败。', 502); }
101
+ result = { path: destination, name: filename, size, revision: fileRevision(stat), mimeType };
102
+ break;
103
+ } catch (error) { if (error.code !== 'EEXIST') throw error; }
104
+ }
105
+ this.copies[asset.id] = result;
106
+ try { writeRecord(this.recordPath, this.copies); }
107
+ catch (error) { result.warning = '文件已保存,副本记录未能持久保存;重启后请从原文件重新选择。'; console.error('电脑副本记录保存失败', error); }
108
+ return result;
109
+ } finally {
110
+ await handle?.close();
111
+ if (created) {
112
+ try { await fs.rm(partial, { force: true }); }
113
+ catch (error) {
114
+ if (!result) throw error;
115
+ result.warning = '文件已保存,下载临时文件未能清理。'; console.error('电脑下载临时文件清理失败', error);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
@@ -1,99 +1,99 @@
1
- param(
2
- [Parameter(Mandatory = $true)][string]$Token,
3
- [int]$IdleSeconds = 1800
4
- )
5
-
6
- $ErrorActionPreference = 'Stop'
7
- $pipeName = 'CodeRelaxRtcRouteHelper'
8
- $leases = @{}
9
- $pipeSecurity = [System.IO.Pipes.PipeSecurity]::new()
10
- $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
11
- $pipeSecurity.AddAccessRule([System.IO.Pipes.PipeAccessRule]::new($currentUser, [System.IO.Pipes.PipeAccessRights]::ReadWrite,
12
- [System.Security.AccessControl.AccessControlType]::Allow))
13
- $pipeSecurity.AddAccessRule([System.IO.Pipes.PipeAccessRule]::new([System.Security.Principal.SecurityIdentifier]::new('S-1-5-18'),
14
- [System.IO.Pipes.PipeAccessRights]::FullControl, [System.Security.AccessControl.AccessControlType]::Allow))
15
- $pipeSecurity.AddAccessRule([System.IO.Pipes.PipeAccessRule]::new([System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544'),
16
- [System.IO.Pipes.PipeAccessRights]::FullControl, [System.Security.AccessControl.AccessControlType]::Allow))
17
-
18
- function Test-PublicIpv4([string]$Address) {
19
- $parts = $Address.Split('.')
20
- if ($parts.Count -ne 4) { return $false }
21
- try { $bytes = $parts | ForEach-Object { [byte]$_ } } catch { return $false }
22
- $a = $bytes[0]; $b = $bytes[1]
23
- return $a -ne 0 -and $a -ne 10 -and $a -ne 127 -and $a -lt 224 -and -not ($a -eq 169 -and $b -eq 254) -and
24
- -not ($a -eq 172 -and $b -ge 16 -and $b -le 31) -and -not ($a -eq 192 -and $b -eq 168) -and
25
- -not ($a -eq 100 -and $b -ge 64 -and $b -le 127) -and -not ($a -eq 198 -and ($b -eq 18 -or $b -eq 19))
26
- }
27
-
28
- function Get-PhysicalDefaultRoute {
29
- $routes = Get-NetRoute -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 | Where-Object {
30
- $adapter = Get-NetAdapter -InterfaceIndex $_.InterfaceIndex -ErrorAction SilentlyContinue
31
- $_.NextHop -and $_.NextHop -ne '0.0.0.0' -and $adapter.Status -eq 'Up' -and $adapter.HardwareInterface -eq $true -and $adapter.Virtual -ne $true
32
- } | Sort-Object @{ Expression = { $_.RouteMetric + $_.InterfaceMetric } }
33
- foreach ($route in $routes) {
34
- $adapter = Get-NetAdapter -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue
35
- if ($adapter.Name -notmatch 'Mihomo|Meta|Tunnel|VPN|TAP|Wintun') { return $route }
36
- }
37
- throw '找不到活动物理 IPv4 默认路由'
38
- }
39
-
40
- function Clear-ExpiredRoutes {
41
- $now = Get-Date
42
- foreach ($address in @($leases.Keys)) {
43
- if ($leases[$address].expires -gt $now) { continue }
44
- $lease = $leases[$address]
45
- if ($lease.created) {
46
- Get-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $lease.ifIndex -NextHop $lease.nextHop -ErrorAction SilentlyContinue |
47
- Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
48
- }
49
- $leases.Remove($address)
50
- }
51
- }
52
-
53
- function Clear-AllRoutes {
54
- foreach ($address in @($leases.Keys)) {
55
- $lease = $leases[$address]
56
- if ($lease.created) {
57
- Get-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $lease.ifIndex -NextHop $lease.nextHop -ErrorAction SilentlyContinue |
58
- Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
59
- }
60
- $leases.Remove($address)
61
- }
62
- }
63
-
64
- try {
65
- $deadline = (Get-Date).AddSeconds($IdleSeconds)
66
- while ((Get-Date) -lt $deadline) {
67
- Clear-ExpiredRoutes
68
- $server = [System.IO.Pipes.NamedPipeServerStream]::new($pipeName, [System.IO.Pipes.PipeDirection]::InOut, 1,
69
- [System.IO.Pipes.PipeTransmissionMode]::Byte, [System.IO.Pipes.PipeOptions]::Asynchronous, 0, 0, $pipeSecurity)
70
- $writer = $null
71
- try {
72
- $wait = $server.WaitForConnectionAsync()
73
- if (-not $wait.Wait(1000)) { continue }
74
- $reader = [System.IO.StreamReader]::new($server)
75
- $writer = [System.IO.StreamWriter]::new($server); $writer.AutoFlush = $true
76
- $request = $reader.ReadLine() | ConvertFrom-Json
77
- if ($request.token -cne $Token -or $request.action -ne 'add') { throw '请求未授权' }
78
- $ttl = [Math]::Min([Math]::Max([int]$request.ttlSeconds, 15), 180)
79
- $route = Get-PhysicalDefaultRoute
80
- $rawAddresses = @($request.addresses)
81
- $addresses = @($rawAddresses | Where-Object { Test-PublicIpv4 $_ } | Select-Object -Unique)
82
- if ($addresses.Count -ne $rawAddresses.Count -or $addresses.Count -gt 8) { throw '路由目标无效' }
83
- foreach ($address in $addresses) {
84
- $created = $leases[$address].created -eq $true
85
- if (-not $leases.ContainsKey($address) -and -not (Get-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $route.InterfaceIndex -NextHop $route.NextHop -ErrorAction SilentlyContinue)) {
86
- New-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $route.InterfaceIndex -NextHop $route.NextHop -RouteMetric 5 -PolicyStore ActiveStore | Out-Null
87
- $created = $true
88
- }
89
- $leases[$address] = @{ expires = (Get-Date).AddSeconds($ttl); ifIndex = $route.InterfaceIndex; nextHop = $route.NextHop; created = $created }
90
- }
91
- $writer.WriteLine((@{ ok = $true; count = $addresses.Count } | ConvertTo-Json -Compress))
92
- }
93
- catch {
94
- if ($writer) { $writer.WriteLine((@{ ok = $false; error = $_.Exception.Message } | ConvertTo-Json -Compress)) }
95
- }
96
- finally { $server.Dispose() }
97
- }
98
- }
99
- finally { Clear-AllRoutes }
1
+ param(
2
+ [Parameter(Mandatory = $true)][string]$Token,
3
+ [int]$IdleSeconds = 1800
4
+ )
5
+
6
+ $ErrorActionPreference = 'Stop'
7
+ $pipeName = 'CodeRelaxRtcRouteHelper'
8
+ $leases = @{}
9
+ $pipeSecurity = [System.IO.Pipes.PipeSecurity]::new()
10
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
11
+ $pipeSecurity.AddAccessRule([System.IO.Pipes.PipeAccessRule]::new($currentUser, [System.IO.Pipes.PipeAccessRights]::ReadWrite,
12
+ [System.Security.AccessControl.AccessControlType]::Allow))
13
+ $pipeSecurity.AddAccessRule([System.IO.Pipes.PipeAccessRule]::new([System.Security.Principal.SecurityIdentifier]::new('S-1-5-18'),
14
+ [System.IO.Pipes.PipeAccessRights]::FullControl, [System.Security.AccessControl.AccessControlType]::Allow))
15
+ $pipeSecurity.AddAccessRule([System.IO.Pipes.PipeAccessRule]::new([System.Security.Principal.SecurityIdentifier]::new('S-1-5-32-544'),
16
+ [System.IO.Pipes.PipeAccessRights]::FullControl, [System.Security.AccessControl.AccessControlType]::Allow))
17
+
18
+ function Test-PublicIpv4([string]$Address) {
19
+ $parts = $Address.Split('.')
20
+ if ($parts.Count -ne 4) { return $false }
21
+ try { $bytes = $parts | ForEach-Object { [byte]$_ } } catch { return $false }
22
+ $a = $bytes[0]; $b = $bytes[1]
23
+ return $a -ne 0 -and $a -ne 10 -and $a -ne 127 -and $a -lt 224 -and -not ($a -eq 169 -and $b -eq 254) -and
24
+ -not ($a -eq 172 -and $b -ge 16 -and $b -le 31) -and -not ($a -eq 192 -and $b -eq 168) -and
25
+ -not ($a -eq 100 -and $b -ge 64 -and $b -le 127) -and -not ($a -eq 198 -and ($b -eq 18 -or $b -eq 19))
26
+ }
27
+
28
+ function Get-PhysicalDefaultRoute {
29
+ $routes = Get-NetRoute -DestinationPrefix '0.0.0.0/0' -AddressFamily IPv4 | Where-Object {
30
+ $adapter = Get-NetAdapter -InterfaceIndex $_.InterfaceIndex -ErrorAction SilentlyContinue
31
+ $_.NextHop -and $_.NextHop -ne '0.0.0.0' -and $adapter.Status -eq 'Up' -and $adapter.HardwareInterface -eq $true -and $adapter.Virtual -ne $true
32
+ } | Sort-Object @{ Expression = { $_.RouteMetric + $_.InterfaceMetric } }
33
+ foreach ($route in $routes) {
34
+ $adapter = Get-NetAdapter -InterfaceIndex $route.InterfaceIndex -ErrorAction SilentlyContinue
35
+ if ($adapter.Name -notmatch 'Mihomo|Meta|Tunnel|VPN|TAP|Wintun') { return $route }
36
+ }
37
+ throw '找不到活动物理 IPv4 默认路由'
38
+ }
39
+
40
+ function Clear-ExpiredRoutes {
41
+ $now = Get-Date
42
+ foreach ($address in @($leases.Keys)) {
43
+ if ($leases[$address].expires -gt $now) { continue }
44
+ $lease = $leases[$address]
45
+ if ($lease.created) {
46
+ Get-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $lease.ifIndex -NextHop $lease.nextHop -ErrorAction SilentlyContinue |
47
+ Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
48
+ }
49
+ $leases.Remove($address)
50
+ }
51
+ }
52
+
53
+ function Clear-AllRoutes {
54
+ foreach ($address in @($leases.Keys)) {
55
+ $lease = $leases[$address]
56
+ if ($lease.created) {
57
+ Get-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $lease.ifIndex -NextHop $lease.nextHop -ErrorAction SilentlyContinue |
58
+ Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
59
+ }
60
+ $leases.Remove($address)
61
+ }
62
+ }
63
+
64
+ try {
65
+ $deadline = (Get-Date).AddSeconds($IdleSeconds)
66
+ while ((Get-Date) -lt $deadline) {
67
+ Clear-ExpiredRoutes
68
+ $server = [System.IO.Pipes.NamedPipeServerStream]::new($pipeName, [System.IO.Pipes.PipeDirection]::InOut, 1,
69
+ [System.IO.Pipes.PipeTransmissionMode]::Byte, [System.IO.Pipes.PipeOptions]::Asynchronous, 0, 0, $pipeSecurity)
70
+ $writer = $null
71
+ try {
72
+ $wait = $server.WaitForConnectionAsync()
73
+ if (-not $wait.Wait(1000)) { continue }
74
+ $reader = [System.IO.StreamReader]::new($server)
75
+ $writer = [System.IO.StreamWriter]::new($server); $writer.AutoFlush = $true
76
+ $request = $reader.ReadLine() | ConvertFrom-Json
77
+ if ($request.token -cne $Token -or $request.action -ne 'add') { throw '请求未授权' }
78
+ $ttl = [Math]::Min([Math]::Max([int]$request.ttlSeconds, 15), 180)
79
+ $route = Get-PhysicalDefaultRoute
80
+ $rawAddresses = @($request.addresses)
81
+ $addresses = @($rawAddresses | Where-Object { Test-PublicIpv4 $_ } | Select-Object -Unique)
82
+ if ($addresses.Count -ne $rawAddresses.Count -or $addresses.Count -gt 8) { throw '路由目标无效' }
83
+ foreach ($address in $addresses) {
84
+ $created = $leases[$address].created -eq $true
85
+ if (-not $leases.ContainsKey($address) -and -not (Get-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $route.InterfaceIndex -NextHop $route.NextHop -ErrorAction SilentlyContinue)) {
86
+ New-NetRoute -DestinationPrefix "$address/32" -InterfaceIndex $route.InterfaceIndex -NextHop $route.NextHop -RouteMetric 5 -PolicyStore ActiveStore | Out-Null
87
+ $created = $true
88
+ }
89
+ $leases[$address] = @{ expires = (Get-Date).AddSeconds($ttl); ifIndex = $route.InterfaceIndex; nextHop = $route.NextHop; created = $created }
90
+ }
91
+ $writer.WriteLine((@{ ok = $true; count = $addresses.Count } | ConvertTo-Json -Compress))
92
+ }
93
+ catch {
94
+ if ($writer) { $writer.WriteLine((@{ ok = $false; error = $_.Exception.Message } | ConvertTo-Json -Compress)) }
95
+ }
96
+ finally { $server.Dispose() }
97
+ }
98
+ }
99
+ finally { Clear-AllRoutes }
@@ -1,15 +1,15 @@
1
- $ErrorActionPreference = 'Stop'
2
- $roots = @(
3
- (Join-Path $env:LOCALAPPDATA 'CodexRemote'),
4
- (Join-Path $env:LOCALAPPDATA 'Packages\OpenAI.Codex_2p2nqsd0c76g0\LocalCache\Local\CodexRemote')
5
- )
6
- $config = $null
7
- foreach ($root in $roots) {
8
- $path = Join-Path $root 'self-relay-config.json'
9
- if (-not (Test-Path -LiteralPath $path)) { continue }
10
- $value = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
11
- if ($value.p2pWindowsRouteHelper -eq $true -and $value.p2pWindowsRouteHelperToken) { $config = $value; break }
12
- }
13
- if (-not $config) { throw '未找到已启用的 RTC 路由辅助配置' }
14
- $helper = Join-Path $PSScriptRoot 'rtc-route-helper.ps1'
15
- & $helper -Token $config.p2pWindowsRouteHelperToken -IdleSeconds 1800
1
+ $ErrorActionPreference = 'Stop'
2
+ $roots = @(
3
+ (Join-Path $env:LOCALAPPDATA 'CodexRemote'),
4
+ (Join-Path $env:LOCALAPPDATA 'Packages\OpenAI.Codex_2p2nqsd0c76g0\LocalCache\Local\CodexRemote')
5
+ )
6
+ $config = $null
7
+ foreach ($root in $roots) {
8
+ $path = Join-Path $root 'self-relay-config.json'
9
+ if (-not (Test-Path -LiteralPath $path)) { continue }
10
+ $value = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
11
+ if ($value.p2pWindowsRouteHelper -eq $true -and $value.p2pWindowsRouteHelperToken) { $config = $value; break }
12
+ }
13
+ if (-not $config) { throw '未找到已启用的 RTC 路由辅助配置' }
14
+ $helper = Join-Path $PSScriptRoot 'rtc-route-helper.ps1'
15
+ & $helper -Token $config.p2pWindowsRouteHelperToken -IdleSeconds 1800
@@ -1,22 +1,22 @@
1
- Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.
2
-
3
- Permission is hereby granted, free of charge, to any person
4
- obtaining a copy of this software and associated documentation
5
- files (the "Software"), to deal in the Software without
6
- restriction, including without limitation the rights to use,
7
- copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- copies of the Software, and to permit persons to whom the
9
- Software is furnished to do so, subject to the following
10
- conditions:
11
-
12
- The above copyright notice and this permission notice shall be
13
- included in all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
- OTHER DEALINGS IN THE SOFTWARE.
1
+ Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.