@walkhi/code-relax 0.1.0-beta.65 → 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 (121) 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/codex-runtime.mjs +0 -4
  43. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
  44. package/dist/src/platform/macos/network-info.mjs +8 -8
  45. package/dist/src/platform/macos/project-locations.mjs +8 -8
  46. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  47. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  48. package/dist/src/platform/macos/service-host.mjs +18 -18
  49. package/dist/src/platform/macos/system.mjs +29 -29
  50. package/dist/src/platform/network-info.mjs +3 -3
  51. package/dist/src/platform/path-identity.mjs +5 -5
  52. package/dist/src/platform/posix/network-candidates.mjs +23 -23
  53. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  54. package/dist/src/platform/power-inhibitor.mjs +22 -22
  55. package/dist/src/platform/project-locations.mjs +3 -3
  56. package/dist/src/platform/runtime-paths.mjs +8 -8
  57. package/dist/src/platform/select.mjs +5 -5
  58. package/dist/src/platform/service-host.mjs +3 -3
  59. package/dist/src/platform/user-environment.mjs +3 -3
  60. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  61. package/dist/src/platform/windows/background-process.mjs +30 -30
  62. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  63. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  64. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
  65. package/dist/src/platform/windows/keep-awake.ps1 +20 -20
  66. package/dist/src/platform/windows/network-info.mjs +34 -34
  67. package/dist/src/platform/windows/resident-start.js +20 -20
  68. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  69. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  70. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  71. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  72. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  73. package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
  74. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  75. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  76. package/dist/src/project-create.mjs +121 -121
  77. package/dist/src/project-management.mjs +1 -1
  78. package/dist/src/project-remove.mjs +55 -55
  79. package/dist/src/project-update.mjs +59 -59
  80. package/dist/src/relay-config-import.mjs +45 -45
  81. package/dist/src/self-relay/account-connections.mjs +52 -52
  82. package/dist/src/self-relay/admin-auth.mjs +59 -59
  83. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  84. package/dist/src/self-relay/admin-login-page.mjs +7 -7
  85. package/dist/src/self-relay/admin-server.mjs +105 -105
  86. package/dist/src/self-relay/admin-state.mjs +127 -127
  87. package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
  88. package/dist/src/self-relay/admin-workstations.mjs +143 -143
  89. package/dist/src/self-relay/android-releases.mjs +61 -61
  90. package/dist/src/self-relay/client.mjs +61 -61
  91. package/dist/src/self-relay/connection-reporting.mjs +38 -38
  92. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  93. package/dist/src/self-relay/demo.mjs +6 -6
  94. package/dist/src/self-relay/device-info.mjs +27 -27
  95. package/dist/src/self-relay/device-store.mjs +92 -92
  96. package/dist/src/self-relay/lan-authorizations.mjs +71 -71
  97. package/dist/src/self-relay/lifecycle.mjs +278 -278
  98. package/dist/src/self-relay/long-pairing.mjs +40 -40
  99. package/dist/src/self-relay/mysql-state.mjs +118 -118
  100. package/dist/src/self-relay/phone-connections.mjs +55 -55
  101. package/dist/src/self-relay/server.mjs +1066 -1066
  102. package/dist/src/self-relay/usage-daily.mjs +55 -55
  103. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  104. package/dist/src/self-relay/wire.mjs +36 -36
  105. package/dist/src/server.mjs +1947 -1947
  106. package/dist/src/service-control.mjs +82 -82
  107. package/dist/src/service-health.mjs +33 -33
  108. package/dist/src/service-power.mjs +58 -58
  109. package/dist/src/service-restart.mjs +81 -81
  110. package/dist/src/service-update-worker.mjs +42 -42
  111. package/dist/src/service-update.mjs +80 -80
  112. package/dist/src/shared-recovery.mjs +152 -152
  113. package/dist/src/skill-install.mjs +51 -51
  114. package/dist/src/thread-title-generation.mjs +150 -150
  115. package/dist/src/turn-model-history.mjs +47 -47
  116. package/dist/tools/rtc-route-helper.ps1 +99 -99
  117. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  118. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  119. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  120. package/package.json +4 -4
  121. package/tools/postinstall.mjs +1 -1
@@ -1,121 +1,121 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { readRecord, writeRecord } from './service-lifecycle.mjs';
5
- import { projectRoots, projectSummary } from './project-management.mjs';
6
- import { projectLocations } from './platform/project-locations.mjs';
7
- import { withDesktopProjectRegistration } from './platform/desktop-project-registration.mjs';
8
-
9
- const pending = new Map();
10
- const uuid = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
11
- const fail = message => { throw Object.assign(new Error(message), { statusCode: 400 }); };
12
- const pathKey = value => process.platform === 'win32' ? path.normalize(value).toLowerCase() : path.normalize(value);
13
-
14
- function defaultName(name) {
15
- let value = path.basename(name).trim().replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').replace(/[ .]+$/g, '') || 'New project';
16
- if (/^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value)) value = `_${value}`;
17
- return value;
18
- }
19
-
20
- export async function createWorkstationProject(server, stateRoot, codexDataRoot, body, dependencies = {}) {
21
- if (!uuid.test(body.idempotencyKey || '')) fail('项目创建标识无效。');
22
- const name = typeof body.name === 'string' ? body.name.trim() : '';
23
- if (!name) fail('请输入项目名称。');
24
- if (!Array.isArray(body.roots) || body.roots.some(root => typeof root?.path !== 'string' || !path.isAbsolute(root.path.trim()))) {
25
- fail('项目文件夹路径无效。');
26
- }
27
- const roots = body.roots.map(root => ({ path: path.normalize(root.path.trim()) }));
28
- const signature = createHash('sha256').update(JSON.stringify({ name, roots })).digest('hex');
29
- const directory = path.join(stateRoot, 'project-create-operations');
30
- const record = path.join(directory, `${body.idempotencyKey}.json`);
31
- const running = pending.get(record);
32
- if (running) {
33
- if (running.signature !== signature) fail('同一个创建请求不能修改项目名称或目录。');
34
- return running.promise;
35
- }
36
- const promise = perform().finally(() => pending.delete(record));
37
- pending.set(record, { signature, promise });
38
- return promise;
39
-
40
- async function perform() {
41
- await fs.mkdir(directory, { recursive: true });
42
- let operation = readRecord(record);
43
- if (operation && operation.signature !== signature) fail('项目创建标识已用于其他内容,请重新打开项目编辑器。');
44
- if (operation?.result) return operation.result;
45
- operation ||= { signature, name, roots };
46
- const save = () => writeRecord(record, operation);
47
- const rpc = (method, params) => server.request(method, params);
48
- const register = dependencies.registration || withDesktopProjectRegistration;
49
- try {
50
- return await register({ stateRoot, codexDataRoot, operationId: body.idempotencyKey,
51
- confirmed: body.confirmDesktopRestart === true, serverUrl: server.url }, async desktop => {
52
- // The restart confirmation and state checks happen before any
53
- // directory or project write. Existing folders are never moved or copied.
54
- let validatedRoots;
55
- if (!operation.resolvedRoots) {
56
- validatedRoots = await projectRoots(rpc, roots);
57
- operation.resolvedRoots = validatedRoots;
58
- if (!operation.resolvedRoots.length) {
59
- const locations = await (dependencies.locations || projectLocations)();
60
- await rpc('fs/createDirectory', { path: locations.defaultParent, recursive: true });
61
- const entries = (await rpc('fs/readDirectory', { path: locations.defaultParent })).entries;
62
- const names = new Set(entries.map(entry => entry.fileName.toLowerCase()));
63
- const base = defaultName(name);
64
- let folder = base;
65
- for (let suffix = 2; names.has(folder.toLowerCase()); suffix++) folder = `${base} ${suffix}`;
66
- operation.defaultDirectory = path.join(locations.defaultParent, folder);
67
- operation.resolvedRoots = [{ path: operation.defaultDirectory }];
68
- }
69
- save();
70
- }
71
- if (operation.defaultDirectory && !operation.directoryCreated) {
72
- if (operation.directoryAttempted) {
73
- // On an explicit retry, keep the originally allocated directory. Never
74
- // allocate another name after an interrupted createDirectory response.
75
- await projectRoots(rpc, operation.resolvedRoots);
76
- } else {
77
- operation.directoryAttempted = true; save();
78
- await rpc('fs/createDirectory', { path: operation.defaultDirectory, recursive: false });
79
- }
80
- operation.directoryCreated = true; save();
81
- try {
82
- const result = await rpc('command/exec', { command: ['git', 'init'], cwd: operation.defaultDirectory, timeoutMs: 10000 });
83
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `退出码 ${result.exitCode}`);
84
- } catch (error) { operation.gitWarning = `项目目录已创建,Git 初始化未完成:${error.message}`; save(); }
85
- }
86
- const resolvedRoots = validatedRoots?.length ? validatedRoots : await projectRoots(rpc, operation.resolvedRoots);
87
- if (!operation.projectId) {
88
- operation.createAttempted = true; save();
89
- // Native project/create owns idempotency; a retry uses this exact key
90
- // and original roots on the same server, never a second transport.
91
- const created = await rpc('project/create', { name, roots: resolvedRoots, idempotencyKey: body.idempotencyKey });
92
- operation.projectId = created.project.id; save();
93
- }
94
- const { project } = await rpc('project/read', { projectId: operation.projectId });
95
- if (project.id !== operation.projectId || project.name !== name || project.roots.length !== resolvedRoots.length
96
- || project.roots.some((root, index) => pathKey(root.path) !== pathKey(resolvedRoots[index].path))) {
97
- throw new Error('已创建项目的名称或目录发生变化,未覆盖其内容。');
98
- }
99
- if (!operation.ordered) {
100
- const first = (await rpc('project/list', { limit: 1, sortKey: 'position', sortDirection: 'asc' })).data[0];
101
- if (first && first.id !== project.id) await rpc('project/move', { projectId: project.id, beforeProjectId: first.id });
102
- operation.ordered = true; save();
103
- }
104
- const desktopRegistration = await desktop.register(project, operation, save);
105
- operation.result = { project: projectSummary(project), desktopRegistration,
106
- ...(operation.gitWarning ? { warning: operation.gitWarning } : {}) };
107
- save();
108
- return operation.result;
109
- });
110
- } catch (error) {
111
- if (operation.projectId || operation.createAttempted || operation.directoryAttempted) {
112
- error.details = { ...error.details, projectCreationPending: true,
113
- ...(operation.projectId ? { createdProjectId: operation.projectId } : {}) };
114
- error.message += operation.projectId
115
- ? ' 项目已创建,请继续完成桌面登记;会沿用同一项目,不会重复创建。'
116
- : ' 请继续核对本次创建;会沿用原请求标识和目录。';
117
- } else error.details = { ...error.details, projectCreationPending: false };
118
- throw error;
119
- }
120
- }
121
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
5
+ import { projectRoots, projectSummary } from './project-management.mjs';
6
+ import { projectLocations } from './platform/project-locations.mjs';
7
+ import { withDesktopProjectRegistration } from './platform/desktop-project-registration.mjs';
8
+
9
+ const pending = new Map();
10
+ const uuid = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
11
+ const fail = message => { throw Object.assign(new Error(message), { statusCode: 400 }); };
12
+ const pathKey = value => process.platform === 'win32' ? path.normalize(value).toLowerCase() : path.normalize(value);
13
+
14
+ function defaultName(name) {
15
+ let value = path.basename(name).trim().replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').replace(/[ .]+$/g, '') || 'New project';
16
+ if (/^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(value)) value = `_${value}`;
17
+ return value;
18
+ }
19
+
20
+ export async function createWorkstationProject(server, stateRoot, codexDataRoot, body, dependencies = {}) {
21
+ if (!uuid.test(body.idempotencyKey || '')) fail('项目创建标识无效。');
22
+ const name = typeof body.name === 'string' ? body.name.trim() : '';
23
+ if (!name) fail('请输入项目名称。');
24
+ if (!Array.isArray(body.roots) || body.roots.some(root => typeof root?.path !== 'string' || !path.isAbsolute(root.path.trim()))) {
25
+ fail('项目文件夹路径无效。');
26
+ }
27
+ const roots = body.roots.map(root => ({ path: path.normalize(root.path.trim()) }));
28
+ const signature = createHash('sha256').update(JSON.stringify({ name, roots })).digest('hex');
29
+ const directory = path.join(stateRoot, 'project-create-operations');
30
+ const record = path.join(directory, `${body.idempotencyKey}.json`);
31
+ const running = pending.get(record);
32
+ if (running) {
33
+ if (running.signature !== signature) fail('同一个创建请求不能修改项目名称或目录。');
34
+ return running.promise;
35
+ }
36
+ const promise = perform().finally(() => pending.delete(record));
37
+ pending.set(record, { signature, promise });
38
+ return promise;
39
+
40
+ async function perform() {
41
+ await fs.mkdir(directory, { recursive: true });
42
+ let operation = readRecord(record);
43
+ if (operation && operation.signature !== signature) fail('项目创建标识已用于其他内容,请重新打开项目编辑器。');
44
+ if (operation?.result) return operation.result;
45
+ operation ||= { signature, name, roots };
46
+ const save = () => writeRecord(record, operation);
47
+ const rpc = (method, params) => server.request(method, params);
48
+ const register = dependencies.registration || withDesktopProjectRegistration;
49
+ try {
50
+ return await register({ stateRoot, codexDataRoot, operationId: body.idempotencyKey,
51
+ confirmed: body.confirmDesktopRestart === true, serverUrl: server.url }, async desktop => {
52
+ // The restart confirmation and state checks happen before any
53
+ // directory or project write. Existing folders are never moved or copied.
54
+ let validatedRoots;
55
+ if (!operation.resolvedRoots) {
56
+ validatedRoots = await projectRoots(rpc, roots);
57
+ operation.resolvedRoots = validatedRoots;
58
+ if (!operation.resolvedRoots.length) {
59
+ const locations = await (dependencies.locations || projectLocations)();
60
+ await rpc('fs/createDirectory', { path: locations.defaultParent, recursive: true });
61
+ const entries = (await rpc('fs/readDirectory', { path: locations.defaultParent })).entries;
62
+ const names = new Set(entries.map(entry => entry.fileName.toLowerCase()));
63
+ const base = defaultName(name);
64
+ let folder = base;
65
+ for (let suffix = 2; names.has(folder.toLowerCase()); suffix++) folder = `${base} ${suffix}`;
66
+ operation.defaultDirectory = path.join(locations.defaultParent, folder);
67
+ operation.resolvedRoots = [{ path: operation.defaultDirectory }];
68
+ }
69
+ save();
70
+ }
71
+ if (operation.defaultDirectory && !operation.directoryCreated) {
72
+ if (operation.directoryAttempted) {
73
+ // On an explicit retry, keep the originally allocated directory. Never
74
+ // allocate another name after an interrupted createDirectory response.
75
+ await projectRoots(rpc, operation.resolvedRoots);
76
+ } else {
77
+ operation.directoryAttempted = true; save();
78
+ await rpc('fs/createDirectory', { path: operation.defaultDirectory, recursive: false });
79
+ }
80
+ operation.directoryCreated = true; save();
81
+ try {
82
+ const result = await rpc('command/exec', { command: ['git', 'init'], cwd: operation.defaultDirectory, timeoutMs: 10000 });
83
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `退出码 ${result.exitCode}`);
84
+ } catch (error) { operation.gitWarning = `项目目录已创建,Git 初始化未完成:${error.message}`; save(); }
85
+ }
86
+ const resolvedRoots = validatedRoots?.length ? validatedRoots : await projectRoots(rpc, operation.resolvedRoots);
87
+ if (!operation.projectId) {
88
+ operation.createAttempted = true; save();
89
+ // Native project/create owns idempotency; a retry uses this exact key
90
+ // and original roots on the same server, never a second transport.
91
+ const created = await rpc('project/create', { name, roots: resolvedRoots, idempotencyKey: body.idempotencyKey });
92
+ operation.projectId = created.project.id; save();
93
+ }
94
+ const { project } = await rpc('project/read', { projectId: operation.projectId });
95
+ if (project.id !== operation.projectId || project.name !== name || project.roots.length !== resolvedRoots.length
96
+ || project.roots.some((root, index) => pathKey(root.path) !== pathKey(resolvedRoots[index].path))) {
97
+ throw new Error('已创建项目的名称或目录发生变化,未覆盖其内容。');
98
+ }
99
+ if (!operation.ordered) {
100
+ const first = (await rpc('project/list', { limit: 1, sortKey: 'position', sortDirection: 'asc' })).data[0];
101
+ if (first && first.id !== project.id) await rpc('project/move', { projectId: project.id, beforeProjectId: first.id });
102
+ operation.ordered = true; save();
103
+ }
104
+ const desktopRegistration = await desktop.register(project, operation, save);
105
+ operation.result = { project: projectSummary(project), desktopRegistration,
106
+ ...(operation.gitWarning ? { warning: operation.gitWarning } : {}) };
107
+ save();
108
+ return operation.result;
109
+ });
110
+ } catch (error) {
111
+ if (operation.projectId || operation.createAttempted || operation.directoryAttempted) {
112
+ error.details = { ...error.details, projectCreationPending: true,
113
+ ...(operation.projectId ? { createdProjectId: operation.projectId } : {}) };
114
+ error.message += operation.projectId
115
+ ? ' 项目已创建,请继续完成桌面登记;会沿用同一项目,不会重复创建。'
116
+ : ' 请继续核对本次创建;会沿用原请求标识和目录。';
117
+ } else error.details = { ...error.details, projectCreationPending: false };
118
+ throw error;
119
+ }
120
+ }
121
+ }
@@ -56,4 +56,4 @@ export async function createProjectFolder(rpc, body) {
56
56
  const target = path.join(parent, name);
57
57
  await rpc('fs/createDirectory', { path: target, recursive: false });
58
58
  return { path: target };
59
- }
59
+ }
@@ -1,55 +1,55 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { readRecord, writeRecord } from './service-lifecycle.mjs';
4
- import { withDesktopProjectRegistration } from './platform/desktop-project-registration.mjs';
5
-
6
- const pending = new Map();
7
- const uuid = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
8
-
9
- export async function removeWorkstationProject(server, stateRoot, codexDataRoot, projectId, body, dependencies = {}) {
10
- if (!projectId || !uuid.test(body.idempotencyKey || '')) throw Object.assign(new Error('项目移除标识无效。'), { statusCode: 400 });
11
- const record = path.join(stateRoot, 'project-remove-operations', `${body.idempotencyKey}.json`);
12
- const running = pending.get(record);
13
- if (running) {
14
- if (running.projectId !== projectId) throw Object.assign(new Error('同一请求不能移除其他项目。'), { statusCode: 409 });
15
- return running.promise;
16
- }
17
- const promise = perform().finally(() => pending.delete(record));
18
- pending.set(record, { projectId, promise });
19
- return promise;
20
-
21
- async function perform() {
22
- await fs.mkdir(path.dirname(record), { recursive: true });
23
- const operation = readRecord(record) || { projectId };
24
- if (operation.projectId !== projectId) throw Object.assign(new Error('移除标识已用于其他项目。'), { statusCode: 409 });
25
- if (operation.result) return operation.result;
26
- const save = () => writeRecord(record, operation);
27
- const registration = dependencies.registration || withDesktopProjectRegistration;
28
- return registration({ stateRoot, codexDataRoot, operationId: body.idempotencyKey, operation: 'remove',
29
- confirmed: body.confirmDesktopRestart === true, serverUrl: server.url }, async desktop => {
30
- if (!operation.project) {
31
- operation.project = (await server.request('project/read', { projectId })).project;
32
- if (operation.project.id !== projectId) throw new Error('项目身份不一致,未执行移除。');
33
- save();
34
- }
35
- const removeNative = async () => {
36
- if (operation.nativeDeleted) return;
37
- if (operation.deleteAttempted) {
38
- // An explicit retry first resolves an earlier unknown response.
39
- try { await server.request('project/read', { projectId }); }
40
- catch (error) {
41
- if (error.message !== `project not found: ${projectId}`) throw error;
42
- operation.nativeDeleted = true; save(); return;
43
- }
44
- }
45
- operation.deleteAttempted = true; save();
46
- await server.request('project/delete', { projectId });
47
- operation.nativeDeleted = true; save();
48
- };
49
- const desktopRemoval = await desktop.remove(operation.project, operation, save, removeNative);
50
- operation.result = { projectId, desktopRemoval };
51
- save();
52
- return operation.result;
53
- });
54
- }
55
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
4
+ import { withDesktopProjectRegistration } from './platform/desktop-project-registration.mjs';
5
+
6
+ const pending = new Map();
7
+ const uuid = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
8
+
9
+ export async function removeWorkstationProject(server, stateRoot, codexDataRoot, projectId, body, dependencies = {}) {
10
+ if (!projectId || !uuid.test(body.idempotencyKey || '')) throw Object.assign(new Error('项目移除标识无效。'), { statusCode: 400 });
11
+ const record = path.join(stateRoot, 'project-remove-operations', `${body.idempotencyKey}.json`);
12
+ const running = pending.get(record);
13
+ if (running) {
14
+ if (running.projectId !== projectId) throw Object.assign(new Error('同一请求不能移除其他项目。'), { statusCode: 409 });
15
+ return running.promise;
16
+ }
17
+ const promise = perform().finally(() => pending.delete(record));
18
+ pending.set(record, { projectId, promise });
19
+ return promise;
20
+
21
+ async function perform() {
22
+ await fs.mkdir(path.dirname(record), { recursive: true });
23
+ const operation = readRecord(record) || { projectId };
24
+ if (operation.projectId !== projectId) throw Object.assign(new Error('移除标识已用于其他项目。'), { statusCode: 409 });
25
+ if (operation.result) return operation.result;
26
+ const save = () => writeRecord(record, operation);
27
+ const registration = dependencies.registration || withDesktopProjectRegistration;
28
+ return registration({ stateRoot, codexDataRoot, operationId: body.idempotencyKey, operation: 'remove',
29
+ confirmed: body.confirmDesktopRestart === true, serverUrl: server.url }, async desktop => {
30
+ if (!operation.project) {
31
+ operation.project = (await server.request('project/read', { projectId })).project;
32
+ if (operation.project.id !== projectId) throw new Error('项目身份不一致,未执行移除。');
33
+ save();
34
+ }
35
+ const removeNative = async () => {
36
+ if (operation.nativeDeleted) return;
37
+ if (operation.deleteAttempted) {
38
+ // An explicit retry first resolves an earlier unknown response.
39
+ try { await server.request('project/read', { projectId }); }
40
+ catch (error) {
41
+ if (error.message !== `project not found: ${projectId}`) throw error;
42
+ operation.nativeDeleted = true; save(); return;
43
+ }
44
+ }
45
+ operation.deleteAttempted = true; save();
46
+ await server.request('project/delete', { projectId });
47
+ operation.nativeDeleted = true; save();
48
+ };
49
+ const desktopRemoval = await desktop.remove(operation.project, operation, save, removeNative);
50
+ operation.result = { projectId, desktopRemoval };
51
+ save();
52
+ return operation.result;
53
+ });
54
+ }
55
+ }
@@ -1,59 +1,59 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { readRecord, writeRecord } from './service-lifecycle.mjs';
5
- import { projectRoots, projectSummary } from './project-management.mjs';
6
- import { withDesktopProjectRegistration } from './platform/desktop-project-registration.mjs';
7
-
8
- const pending = new Map();
9
- const fail = message => { throw Object.assign(new Error(message), { statusCode: 409 }); };
10
- const rootKey = roots => JSON.stringify(roots.map(root => process.platform === 'win32' ? path.normalize(root.path).toLowerCase() : path.normalize(root.path)));
11
- const same = (a, b) => a.name === b.name && rootKey(a.roots) === rootKey(b.roots);
12
-
13
- export async function updateWorkstationProject(server, stateRoot, codexDataRoot, projectId, body, dependencies = {}) {
14
- const name = typeof body.name === 'string' ? body.name.trim() : '';
15
- if (!name || !Array.isArray(body.roots) || body.roots.some(root => typeof root?.path !== 'string' || !path.isAbsolute(root.path.trim()))
16
- || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(body.idempotencyKey || '')) fail('项目名称、文件夹或操作标识无效。');
17
- const target = { name, roots: body.roots.map(root => ({ path: path.normalize(root.path.trim()) })) };
18
- const signature = createHash('sha256').update(JSON.stringify({ projectId, ...target })).digest('hex');
19
- const record = path.join(stateRoot, 'project-update-operations', `${body.idempotencyKey}.json`);
20
- const active = pending.get(record);
21
- if (active) { if (active.signature !== signature) fail('同一请求不能修改其他项目内容。'); return active.promise; }
22
- const promise = perform().finally(() => pending.delete(record));
23
- pending.set(record, { signature, promise }); return promise;
24
-
25
- async function perform() {
26
- await fs.mkdir(path.dirname(record), { recursive: true });
27
- const operation = readRecord(record) || { signature };
28
- if (operation.signature !== signature) fail('项目编辑标识已用于其他内容。');
29
- if (operation.result) return operation.result;
30
- const save = () => writeRecord(record, operation);
31
- const rpc = (method, params) => server.request(method, params);
32
- try {
33
- return await (dependencies.registration || withDesktopProjectRegistration)({ stateRoot, codexDataRoot,
34
- operationId: body.idempotencyKey, operation: 'update', confirmed: body.confirmDesktopRestart === true, serverUrl: server.url }, async desktop => {
35
- const current = (await rpc('project/read', { projectId })).project;
36
- if (current.id !== projectId) fail('项目身份不一致。');
37
- if (current.roots.length && rootKey(current.roots.slice(0, 1)) !== rootKey(target.roots.slice(0, 1))) {
38
- fail('编辑项目不能修改或移除主目录。');
39
- }
40
- operation.previousProject ||= current;
41
- if (!same(current, operation.previousProject) && !same(current, target)) fail('项目已被其他操作修改,请刷新后重新编辑。');
42
- let project = current;
43
- if (!same(current, target)) {
44
- const rootsChanged = rootKey(current.roots) !== rootKey(target.roots);
45
- const roots = rootsChanged ? await projectRoots(rpc, target.roots) : current.roots;
46
- operation.updateAttempted = true; save();
47
- project = (await rpc('project/update', { projectId, name, roots })).project;
48
- }
49
- save();
50
- const desktopRegistration = await desktop.register(project, operation, save);
51
- operation.result = { project: projectSummary(project), desktopRegistration }; save(); return operation.result;
52
- });
53
- } catch (error) {
54
- error.details = { ...error.details, projectUpdatePending: operation.updateAttempted === true };
55
- if (operation.updateAttempted) error.message += ' 请继续完成本次同步,会先核对已有修改。';
56
- throw error;
57
- }
58
- }
59
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { readRecord, writeRecord } from './service-lifecycle.mjs';
5
+ import { projectRoots, projectSummary } from './project-management.mjs';
6
+ import { withDesktopProjectRegistration } from './platform/desktop-project-registration.mjs';
7
+
8
+ const pending = new Map();
9
+ const fail = message => { throw Object.assign(new Error(message), { statusCode: 409 }); };
10
+ const rootKey = roots => JSON.stringify(roots.map(root => process.platform === 'win32' ? path.normalize(root.path).toLowerCase() : path.normalize(root.path)));
11
+ const same = (a, b) => a.name === b.name && rootKey(a.roots) === rootKey(b.roots);
12
+
13
+ export async function updateWorkstationProject(server, stateRoot, codexDataRoot, projectId, body, dependencies = {}) {
14
+ const name = typeof body.name === 'string' ? body.name.trim() : '';
15
+ if (!name || !Array.isArray(body.roots) || body.roots.some(root => typeof root?.path !== 'string' || !path.isAbsolute(root.path.trim()))
16
+ || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(body.idempotencyKey || '')) fail('项目名称、文件夹或操作标识无效。');
17
+ const target = { name, roots: body.roots.map(root => ({ path: path.normalize(root.path.trim()) })) };
18
+ const signature = createHash('sha256').update(JSON.stringify({ projectId, ...target })).digest('hex');
19
+ const record = path.join(stateRoot, 'project-update-operations', `${body.idempotencyKey}.json`);
20
+ const active = pending.get(record);
21
+ if (active) { if (active.signature !== signature) fail('同一请求不能修改其他项目内容。'); return active.promise; }
22
+ const promise = perform().finally(() => pending.delete(record));
23
+ pending.set(record, { signature, promise }); return promise;
24
+
25
+ async function perform() {
26
+ await fs.mkdir(path.dirname(record), { recursive: true });
27
+ const operation = readRecord(record) || { signature };
28
+ if (operation.signature !== signature) fail('项目编辑标识已用于其他内容。');
29
+ if (operation.result) return operation.result;
30
+ const save = () => writeRecord(record, operation);
31
+ const rpc = (method, params) => server.request(method, params);
32
+ try {
33
+ return await (dependencies.registration || withDesktopProjectRegistration)({ stateRoot, codexDataRoot,
34
+ operationId: body.idempotencyKey, operation: 'update', confirmed: body.confirmDesktopRestart === true, serverUrl: server.url }, async desktop => {
35
+ const current = (await rpc('project/read', { projectId })).project;
36
+ if (current.id !== projectId) fail('项目身份不一致。');
37
+ if (current.roots.length && rootKey(current.roots.slice(0, 1)) !== rootKey(target.roots.slice(0, 1))) {
38
+ fail('编辑项目不能修改或移除主目录。');
39
+ }
40
+ operation.previousProject ||= current;
41
+ if (!same(current, operation.previousProject) && !same(current, target)) fail('项目已被其他操作修改,请刷新后重新编辑。');
42
+ let project = current;
43
+ if (!same(current, target)) {
44
+ const rootsChanged = rootKey(current.roots) !== rootKey(target.roots);
45
+ const roots = rootsChanged ? await projectRoots(rpc, target.roots) : current.roots;
46
+ operation.updateAttempted = true; save();
47
+ project = (await rpc('project/update', { projectId, name, roots })).project;
48
+ }
49
+ save();
50
+ const desktopRegistration = await desktop.register(project, operation, save);
51
+ operation.result = { project: projectSummary(project), desktopRegistration }; save(); return operation.result;
52
+ });
53
+ } catch (error) {
54
+ error.details = { ...error.details, projectUpdatePending: operation.updateAttempted === true };
55
+ if (operation.updateAttempted) error.message += ' 请继续完成本次同步,会先核对已有修改。';
56
+ throw error;
57
+ }
58
+ }
59
+ }
@@ -1,45 +1,45 @@
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
- import { credentialStore } from './platform/credential-store.mjs';
6
- import { readRecord } from './service-lifecycle.mjs';
7
-
8
- const exec = promisify(execFile);
9
-
10
- // One-time installation transfer. Execute the previously installed reader at
11
- // its original location before replacing it; the new runtime never uses a vault.
12
- export async function importRelayConfig(serviceRoot, stateRoot, nodeExecutable = process.execPath) {
13
- const configuredId = readRecord(path.join(stateRoot, 'self-relay-config.json'))?.deviceId;
14
- const validate = record => {
15
- if (record?.version !== 2 || !record.devices || typeof record.devices !== 'object' || Array.isArray(record.devices)
16
- || Object.entries(record.devices).some(([id, item]) => !/^[\w-]{16,128}$/.test(id) || item?.id !== id
17
- || !/^[A-Za-z0-9_-]{43}$/.test(item.secret || '') || typeof item.registered !== 'boolean')
18
- || (configuredId && configuredId !== 'new' && !record.devices[configuredId])) {
19
- throw new Error('工作站身份配置不完整或与当前工作站不一致,未停止旧服务。');
20
- }
21
- };
22
- const current = credentialStore('load', undefined, stateRoot).record;
23
- if (current) { validate(current); return { status: 'already-configured' }; }
24
- const legacy = path.join(serviceRoot, 'dist/src/platform', process.platform === 'win32' ? 'windows'
25
- : process.platform === 'darwin' ? 'macos' : 'linux', 'credential-store.mjs');
26
- if (!fs.existsSync(legacy)) {
27
- if (configuredId && configuredId !== 'new') throw new Error('缺少当前工作站身份配置,未停止旧服务;请先完成原配对转存。');
28
- return { status: 'not-needed' };
29
- }
30
- let record;
31
- try {
32
- const script = 'import { pathToFileURL } from "node:url"; const {credentialStore} = await import(pathToFileURL(process.argv[1])); '
33
- + 'process.stdout.write(JSON.stringify((await credentialStore("load")).record));';
34
- const { stdout } = await exec(nodeExecutable, ['--input-type=module', '-e', script, legacy],
35
- { windowsHide: true, timeout: 65000, maxBuffer: 1024 * 1024 });
36
- record = JSON.parse(stdout.replace(/^\uFEFF/, ''));
37
- if (record === null && !configuredId) return { status: 'not-found' };
38
- validate(record);
39
- } catch {
40
- // Never log execFile's error: it can contain stdout with the old identity.
41
- throw new Error('原设备配对配置转存失败,未停止旧服务。无人值守更新已中止,未更换工作站身份;请先完成原配对转存。');
42
- }
43
- credentialStore('save', record, stateRoot);
44
- return { status: 'imported' };
45
- }
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
+ import { credentialStore } from './platform/credential-store.mjs';
6
+ import { readRecord } from './service-lifecycle.mjs';
7
+
8
+ const exec = promisify(execFile);
9
+
10
+ // One-time installation transfer. Execute the previously installed reader at
11
+ // its original location before replacing it; the new runtime never uses a vault.
12
+ export async function importRelayConfig(serviceRoot, stateRoot, nodeExecutable = process.execPath) {
13
+ const configuredId = readRecord(path.join(stateRoot, 'self-relay-config.json'))?.deviceId;
14
+ const validate = record => {
15
+ if (record?.version !== 2 || !record.devices || typeof record.devices !== 'object' || Array.isArray(record.devices)
16
+ || Object.entries(record.devices).some(([id, item]) => !/^[\w-]{16,128}$/.test(id) || item?.id !== id
17
+ || !/^[A-Za-z0-9_-]{43}$/.test(item.secret || '') || typeof item.registered !== 'boolean')
18
+ || (configuredId && configuredId !== 'new' && !record.devices[configuredId])) {
19
+ throw new Error('工作站身份配置不完整或与当前工作站不一致,未停止旧服务。');
20
+ }
21
+ };
22
+ const current = credentialStore('load', undefined, stateRoot).record;
23
+ if (current) { validate(current); return { status: 'already-configured' }; }
24
+ const legacy = path.join(serviceRoot, 'dist/src/platform', process.platform === 'win32' ? 'windows'
25
+ : process.platform === 'darwin' ? 'macos' : 'linux', 'credential-store.mjs');
26
+ if (!fs.existsSync(legacy)) {
27
+ if (configuredId && configuredId !== 'new') throw new Error('缺少当前工作站身份配置,未停止旧服务;请先完成原配对转存。');
28
+ return { status: 'not-needed' };
29
+ }
30
+ let record;
31
+ try {
32
+ const script = 'import { pathToFileURL } from "node:url"; const {credentialStore} = await import(pathToFileURL(process.argv[1])); '
33
+ + 'process.stdout.write(JSON.stringify((await credentialStore("load")).record));';
34
+ const { stdout } = await exec(nodeExecutable, ['--input-type=module', '-e', script, legacy],
35
+ { windowsHide: true, timeout: 65000, maxBuffer: 1024 * 1024 });
36
+ record = JSON.parse(stdout.replace(/^\uFEFF/, ''));
37
+ if (record === null && !configuredId) return { status: 'not-found' };
38
+ validate(record);
39
+ } catch {
40
+ // Never log execFile's error: it can contain stdout with the old identity.
41
+ throw new Error('原设备配对配置转存失败,未停止旧服务。无人值守更新已中止,未更换工作站身份;请先完成原配对转存。');
42
+ }
43
+ credentialStore('save', record, stateRoot);
44
+ return { status: 'imported' };
45
+ }