@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.31
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.
- package/README.md +95 -89
- package/dist/bin/cleanup-accounts.mjs +63 -63
- package/dist/bin/codex-remote.mjs +374 -367
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-android-release.mjs +62 -58
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/get-shared-service-url.ps1 +6 -6
- package/dist/shared/app-server-events.cjs +422 -412
- package/dist/shared/code-browser.cjs +159 -159
- package/dist/shared/file-changes.cjs +31 -31
- package/dist/shared/file-citations.cjs +117 -117
- package/dist/shared/file-mentions.cjs +92 -92
- package/dist/shared/message-delivery.cjs +54 -54
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-file-channel.cjs +74 -74
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +40 -0
- package/dist/shared/server-requests.cjs +219 -219
- package/dist/shared/timeline-sync.cjs +173 -173
- package/dist/skills/code-relax/SKILL.md +22 -22
- package/dist/src/archived-threads.mjs +22 -0
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/bounded-process-log.mjs +63 -63
- package/dist/src/celsius-client.mjs +107 -107
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +46 -46
- package/dist/src/dots-approval-timeline.mjs +99 -99
- package/dist/src/dots-avatar.mjs +42 -42
- package/dist/src/dots-chat-projection.mjs +50 -50
- package/dist/src/dots-chat-server.mjs +461 -461
- package/dist/src/dots-message-images.mjs +319 -319
- package/dist/src/dots-profile.mjs +77 -77
- package/dist/src/dots-room-activity.mjs +94 -94
- package/dist/src/dots-room-client.mjs +135 -135
- package/dist/src/dots-room-projection.mjs +94 -94
- package/dist/src/dots-schedules.mjs +66 -66
- package/dist/src/dots-summary-tasks.mjs +32 -32
- package/dist/src/dots-task-catalog.mjs +430 -430
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/file-transfer-range.mjs +22 -22
- package/dist/src/file-upload-session.mjs +130 -130
- package/dist/src/hosted-history-app-server.mjs +103 -103
- package/dist/src/hosted-history-cache.mjs +113 -113
- package/dist/src/lan-socket-server.mjs +265 -263
- package/dist/src/managed-app-server.mjs +244 -244
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/message-upload-files.mjs +74 -74
- package/dist/src/onboarding.mjs +12 -12
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/README.md +12 -12
- package/dist/src/platform/codex-protocol.mjs +46 -46
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +3 -3
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/desktop-project-store.mjs +208 -149
- package/dist/src/platform/file-opener.mjs +9 -0
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -0
- package/dist/src/platform/linux/credential-store.mjs +28 -0
- package/dist/src/platform/linux/desktop-host.mjs +83 -0
- package/dist/src/platform/linux/desktop-installation.mjs +36 -0
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
- package/dist/src/platform/linux/network-info.mjs +11 -0
- package/dist/src/platform/linux/process.mjs +42 -0
- package/dist/src/platform/linux/project-locations.mjs +17 -0
- package/dist/src/platform/linux/resident-startup.mjs +52 -0
- package/dist/src/platform/linux/retained-runtime.mjs +76 -0
- package/dist/src/platform/linux/service-host.mjs +18 -0
- package/dist/src/platform/linux/system.mjs +49 -0
- package/dist/src/platform/linux/user-environment.mjs +7 -0
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/codex-runtime.mjs +125 -125
- package/dist/src/platform/macos/credential-store.mjs +10 -10
- package/dist/src/platform/macos/desktop-host.mjs +82 -82
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
- package/dist/src/platform/macos/network-info.mjs +8 -28
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/macos/user-environment.mjs +7 -7
- package/dist/src/platform/native-package.mjs +58 -46
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -0
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/codex-runtime.mjs +215 -215
- package/dist/src/platform/windows/credential-store.mjs +27 -27
- package/dist/src/platform/windows/credential-store.ps1 +49 -49
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-host.mjs +58 -58
- package/dist/src/platform/windows/desktop-info.ps1 +14 -14
- package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
- package/dist/src/platform/windows/dots-profile.mjs +49 -49
- package/dist/src/platform/windows/launch-worker.mjs +41 -41
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/project-locations.mjs +16 -16
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/service-host.mjs +31 -31
- package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.mjs +50 -50
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-archive.mjs +30 -0
- package/dist/src/project-create.mjs +121 -119
- package/dist/src/project-management.mjs +59 -69
- package/dist/src/project-remove.mjs +55 -0
- package/dist/src/project-update.mjs +56 -0
- package/dist/src/runtime-discovery.mjs +47 -47
- package/dist/src/self-relay/account-cleanup.mjs +30 -30
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-server.mjs +74 -74
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-style.mjs +14 -14
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connector.mjs +439 -438
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +37 -37
- package/dist/src/self-relay/device-info.mjs +27 -0
- package/dist/src/self-relay/device-store.mjs +92 -84
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +183 -183
- package/dist/src/self-relay/file-wire.mjs +13 -13
- package/dist/src/self-relay/lan-authorizations.mjs +71 -42
- package/dist/src/self-relay/lifecycle.mjs +264 -250
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -105
- package/dist/src/self-relay/p2p-probe.mjs +159 -159
- package/dist/src/self-relay/privacy-page.mjs +26 -26
- package/dist/src/self-relay/public-releases.mjs +34 -0
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
- package/dist/src/self-relay/server.mjs +1006 -932
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/usage-meter.mjs +141 -141
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +1861 -1778
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-doctor.mjs +110 -110
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-install.mjs +91 -91
- package/dist/src/service-lifecycle.mjs +138 -138
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/shared-app-server.mjs +615 -611
- package/dist/src/shared-catalog.mjs +132 -132
- package/dist/src/shared-recovery.mjs +146 -146
- package/dist/src/shared-thread-stream.mjs +164 -164
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/src/usage-reset.mjs +17 -17
- package/dist/src/workstation-downloads.mjs +120 -120
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +7 -6
- package/tools/postinstall.mjs +127 -127
|
@@ -1,215 +1,215 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { compareVersions, verifyProtocol } from '../codex-protocol.mjs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
|
-
import { execFile } from 'node:child_process';
|
|
7
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { promisify } from 'node:util';
|
|
10
|
-
import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
|
|
11
|
-
|
|
12
|
-
const exec = promisify(execFile);
|
|
13
|
-
export const codeModeHostPath = executable => path.join(path.dirname(executable), 'codex-code-mode-host.exe');
|
|
14
|
-
export const runtimeFilesAvailable = executable => fs.existsSync(executable) && fs.existsSync(codeModeHostPath(executable));
|
|
15
|
-
const fingerprint = executable => [executable, codeModeHostPath(executable)]
|
|
16
|
-
.map(file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; }).join('|');
|
|
17
|
-
export const runtimeFingerprint = fingerprint;
|
|
18
|
-
const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
19
|
-
const desktopRuntimeScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'desktop-runtime.ps1');
|
|
20
|
-
|
|
21
|
-
async function fileHash(file) {
|
|
22
|
-
const hash = createHash('sha256');
|
|
23
|
-
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
|
|
24
|
-
return hash.digest('hex');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function installedDesktopRuntime() {
|
|
28
|
-
const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
29
|
-
'-File', desktopRuntimeScript], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
|
|
30
|
-
return JSON.parse(stdout.trim());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function matchesInstalledDesktop(executable, installed) {
|
|
34
|
-
if (!installed || !runtimeFilesAvailable(executable) || !runtimeFilesAvailable(installed.executable)) return false;
|
|
35
|
-
const files = [[executable, installed.executable], [codeModeHostPath(executable), installed.codeModeHost]];
|
|
36
|
-
for (const [candidate, bundled] of files) {
|
|
37
|
-
if (fs.statSync(candidate).size !== fs.statSync(bundled).size || await fileHash(candidate) !== await fileHash(bundled)) return false;
|
|
38
|
-
}
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function runtimeFiles(directory, relative = '') {
|
|
43
|
-
const files = [];
|
|
44
|
-
for (const entry of fs.readdirSync(path.join(directory, relative), { withFileTypes: true })) {
|
|
45
|
-
const name = path.join(relative, entry.name);
|
|
46
|
-
if (entry.isDirectory()) files.push(...runtimeFiles(directory, name));
|
|
47
|
-
else if (entry.isFile()) files.push(name);
|
|
48
|
-
else throw new Error(`Codex 内核目录包含不支持的文件类型:${name}`);
|
|
49
|
-
}
|
|
50
|
-
return files.sort();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function runtimeDirectories(files) {
|
|
54
|
-
const directories = new Set();
|
|
55
|
-
for (const file of files) {
|
|
56
|
-
let directory = path.dirname(file);
|
|
57
|
-
while (directory !== '.') { directories.add(directory); directory = path.dirname(directory); }
|
|
58
|
-
}
|
|
59
|
-
return [...directories].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function retainDesktopRuntime(executable, installed, stateRoot) {
|
|
63
|
-
if (!/^[\w.-]+$/.test(installed.package)) throw new Error('Codex Desktop 安装包名称无效。');
|
|
64
|
-
const source = path.dirname(executable);
|
|
65
|
-
const files = runtimeFiles(source);
|
|
66
|
-
if (files.includes('runtime.json')) throw new Error('Codex 内核目录与保留清单文件名冲突。');
|
|
67
|
-
const directories = runtimeDirectories(files);
|
|
68
|
-
const root = path.join(stateRoot, 'desktop-runtimes');
|
|
69
|
-
const target = path.join(root, installed.package);
|
|
70
|
-
const retained = path.join(target, 'codex.exe');
|
|
71
|
-
if (fs.existsSync(target)) {
|
|
72
|
-
const manifest = readRecord(path.join(target, 'runtime.json'));
|
|
73
|
-
if (manifest?.package !== installed.package || JSON.stringify(manifest.files) !== JSON.stringify(files)
|
|
74
|
-
|| JSON.stringify(runtimeFiles(target)) !== JSON.stringify([...files, 'runtime.json'].sort())
|
|
75
|
-
|| !await matchesInstalledDesktop(retained, installed)) {
|
|
76
|
-
throw new Error(`已保留的 Codex 内核文件不完整:${target}`);
|
|
77
|
-
}
|
|
78
|
-
return retained;
|
|
79
|
-
}
|
|
80
|
-
fs.mkdirSync(root, { recursive: true });
|
|
81
|
-
const staging = path.join(root, `.${installed.package}.${randomUUID()}.tmp`);
|
|
82
|
-
fs.mkdirSync(staging);
|
|
83
|
-
const created = [];
|
|
84
|
-
try {
|
|
85
|
-
for (const directory of directories) fs.mkdirSync(path.join(staging, directory));
|
|
86
|
-
for (const name of files) {
|
|
87
|
-
const from = path.join(source, name), to = path.join(staging, name);
|
|
88
|
-
created.push(to);
|
|
89
|
-
try { fs.linkSync(from, to); }
|
|
90
|
-
catch (error) {
|
|
91
|
-
if (error.code !== 'EXDEV') throw error;
|
|
92
|
-
fs.copyFileSync(from, to);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (!await matchesInstalledDesktop(path.join(staging, 'codex.exe'), installed)) {
|
|
96
|
-
throw new Error('保留的 Codex 内核与当前安装包不一致。');
|
|
97
|
-
}
|
|
98
|
-
const manifestPath = path.join(staging, 'runtime.json');
|
|
99
|
-
writeRecord(manifestPath, { package: installed.package, files });
|
|
100
|
-
created.push(manifestPath);
|
|
101
|
-
fs.renameSync(staging, target);
|
|
102
|
-
return retained;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
for (const file of created) fs.rmSync(file, { force: true });
|
|
105
|
-
for (const directory of directories.reverse()) {
|
|
106
|
-
const nested = path.join(staging, directory);
|
|
107
|
-
if (fs.existsSync(nested)) fs.rmdirSync(nested);
|
|
108
|
-
}
|
|
109
|
-
fs.rmdirSync(staging);
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
|
|
115
|
-
const root = path.join(stateRoot, 'desktop-runtimes');
|
|
116
|
-
if (!fs.existsSync(root)) return;
|
|
117
|
-
const keep = path.resolve(path.dirname(keepExecutable)).toLowerCase();
|
|
118
|
-
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
119
|
-
if (!entry.isDirectory() || !/^OpenAI\.Codex_[\w.-]+$/.test(entry.name)) continue;
|
|
120
|
-
const directory = path.join(root, entry.name);
|
|
121
|
-
if (path.resolve(directory).toLowerCase() === keep || fs.lstatSync(directory).isSymbolicLink()) continue;
|
|
122
|
-
const manifest = readRecord(path.join(directory, 'runtime.json'));
|
|
123
|
-
const files = manifest?.files;
|
|
124
|
-
if (manifest?.package !== entry.name || !Array.isArray(files) || !files.includes('codex.exe')
|
|
125
|
-
|| !files.includes('codex-code-mode-host.exe') || new Set(files).size !== files.length
|
|
126
|
-
|| !files.every(name => typeof name === 'string' && name !== 'runtime.json'
|
|
127
|
-
&& path.resolve(directory, name).toLowerCase().startsWith(`${path.resolve(directory).toLowerCase()}${path.sep}`))) continue;
|
|
128
|
-
const expected = [...files, 'runtime.json'].sort();
|
|
129
|
-
if (JSON.stringify(runtimeFiles(directory)) !== JSON.stringify(expected)) continue;
|
|
130
|
-
if (!fs.lstatSync(path.join(directory, 'runtime.json')).isFile()
|
|
131
|
-
|| !files.every(name => fs.lstatSync(path.join(directory, name)).isFile())) continue;
|
|
132
|
-
for (const name of files) fs.unlinkSync(path.join(directory, name));
|
|
133
|
-
fs.unlinkSync(path.join(directory, 'runtime.json'));
|
|
134
|
-
for (const name of runtimeDirectories(files).reverse()) fs.rmdirSync(path.join(directory, name));
|
|
135
|
-
fs.rmdirSync(directory);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Resolve native binaries, never execute a shell shim from a background task.
|
|
140
|
-
export function discoverCodex(environment = process.env) {
|
|
141
|
-
const found = new Map();
|
|
142
|
-
const add = (file, source) => {
|
|
143
|
-
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
|
|
144
|
-
const executable = fs.realpathSync(file);
|
|
145
|
-
if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
|
|
146
|
-
};
|
|
147
|
-
if (environment.CODE_RELAX_CODEX_PATH) {
|
|
148
|
-
const file = environment.CODE_RELAX_CODEX_PATH;
|
|
149
|
-
if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
|
|
150
|
-
add(file, 'explicit');
|
|
151
|
-
if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
|
|
152
|
-
return [...found.values()];
|
|
153
|
-
}
|
|
154
|
-
const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
|
|
155
|
-
const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
156
|
-
const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
|
|
157
|
-
environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
|
|
158
|
-
path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
|
|
159
|
-
for (const directory of directories) {
|
|
160
|
-
add(path.join(directory, 'codex.exe'), 'cli');
|
|
161
|
-
const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
|
|
162
|
-
if (!fs.existsSync(packageFile)) continue;
|
|
163
|
-
const root = path.dirname(fs.realpathSync(packageFile));
|
|
164
|
-
let vendor = path.join(root, 'vendor');
|
|
165
|
-
try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
|
|
166
|
-
catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
|
|
167
|
-
add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
|
|
168
|
-
}
|
|
169
|
-
const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
|
|
170
|
-
add(path.join(desktopRoot, 'codex.exe'), 'desktop');
|
|
171
|
-
if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
|
|
172
|
-
if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
|
|
173
|
-
}
|
|
174
|
-
return [...found.values()];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
|
|
178
|
-
fs.mkdirSync(stateRoot, { recursive: true });
|
|
179
|
-
const recordFile = path.join(stateRoot, 'codex-runtime.json');
|
|
180
|
-
const saved = readRecord(recordFile);
|
|
181
|
-
environment = { ...environment,
|
|
182
|
-
CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
|
|
183
|
-
CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
|
|
184
|
-
if (!refresh && saved?.executable && runtimeFilesAvailable(saved.executable)
|
|
185
|
-
&& saved.fingerprint === fingerprint(saved.executable)) return saved;
|
|
186
|
-
const candidates = [], rejected = [];
|
|
187
|
-
let installed = null;
|
|
188
|
-
try { installed = await installedDesktopRuntime(); }
|
|
189
|
-
catch (error) { rejected.push(`无法读取当前 Codex Desktop 安装包:${error.message}`); }
|
|
190
|
-
for (const candidate of discoverCodex(environment)) {
|
|
191
|
-
try {
|
|
192
|
-
if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少同目录的 codex-code-mode-host.exe');
|
|
193
|
-
if (candidate.source === 'desktop' && !await matchesInstalledDesktop(candidate.executable, installed)) {
|
|
194
|
-
throw new Error('不是当前 Codex Desktop 安装包配套的 CLI');
|
|
195
|
-
}
|
|
196
|
-
const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
|
|
197
|
-
const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
|
|
198
|
-
if (!version) throw new Error('无法识别版本');
|
|
199
|
-
candidates.push({ ...candidate, version });
|
|
200
|
-
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
201
|
-
}
|
|
202
|
-
candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
|
|
203
|
-
for (const candidate of candidates) {
|
|
204
|
-
try {
|
|
205
|
-
const executable = candidate.source === 'desktop'
|
|
206
|
-
? await retainDesktopRuntime(candidate.executable, installed, stateRoot) : candidate.executable;
|
|
207
|
-
await verifyProtocol(executable, stateRoot);
|
|
208
|
-
const result = { ...candidate, executable, fingerprint: fingerprint(executable),
|
|
209
|
-
codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
|
|
210
|
-
writeRecord(recordFile, result);
|
|
211
|
-
return result;
|
|
212
|
-
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
213
|
-
}
|
|
214
|
-
throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
|
|
215
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { compareVersions, verifyProtocol } from '../codex-protocol.mjs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
10
|
+
import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
|
|
11
|
+
|
|
12
|
+
const exec = promisify(execFile);
|
|
13
|
+
export const codeModeHostPath = executable => path.join(path.dirname(executable), 'codex-code-mode-host.exe');
|
|
14
|
+
export const runtimeFilesAvailable = executable => fs.existsSync(executable) && fs.existsSync(codeModeHostPath(executable));
|
|
15
|
+
const fingerprint = executable => [executable, codeModeHostPath(executable)]
|
|
16
|
+
.map(file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; }).join('|');
|
|
17
|
+
export const runtimeFingerprint = fingerprint;
|
|
18
|
+
const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
19
|
+
const desktopRuntimeScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'desktop-runtime.ps1');
|
|
20
|
+
|
|
21
|
+
async function fileHash(file) {
|
|
22
|
+
const hash = createHash('sha256');
|
|
23
|
+
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
|
|
24
|
+
return hash.digest('hex');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function installedDesktopRuntime() {
|
|
28
|
+
const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
29
|
+
'-File', desktopRuntimeScript], { windowsHide: true, encoding: 'utf8', timeout: 30000 });
|
|
30
|
+
return JSON.parse(stdout.trim());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function matchesInstalledDesktop(executable, installed) {
|
|
34
|
+
if (!installed || !runtimeFilesAvailable(executable) || !runtimeFilesAvailable(installed.executable)) return false;
|
|
35
|
+
const files = [[executable, installed.executable], [codeModeHostPath(executable), installed.codeModeHost]];
|
|
36
|
+
for (const [candidate, bundled] of files) {
|
|
37
|
+
if (fs.statSync(candidate).size !== fs.statSync(bundled).size || await fileHash(candidate) !== await fileHash(bundled)) return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runtimeFiles(directory, relative = '') {
|
|
43
|
+
const files = [];
|
|
44
|
+
for (const entry of fs.readdirSync(path.join(directory, relative), { withFileTypes: true })) {
|
|
45
|
+
const name = path.join(relative, entry.name);
|
|
46
|
+
if (entry.isDirectory()) files.push(...runtimeFiles(directory, name));
|
|
47
|
+
else if (entry.isFile()) files.push(name);
|
|
48
|
+
else throw new Error(`Codex 内核目录包含不支持的文件类型:${name}`);
|
|
49
|
+
}
|
|
50
|
+
return files.sort();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function runtimeDirectories(files) {
|
|
54
|
+
const directories = new Set();
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
let directory = path.dirname(file);
|
|
57
|
+
while (directory !== '.') { directories.add(directory); directory = path.dirname(directory); }
|
|
58
|
+
}
|
|
59
|
+
return [...directories].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function retainDesktopRuntime(executable, installed, stateRoot) {
|
|
63
|
+
if (!/^[\w.-]+$/.test(installed.package)) throw new Error('Codex Desktop 安装包名称无效。');
|
|
64
|
+
const source = path.dirname(executable);
|
|
65
|
+
const files = runtimeFiles(source);
|
|
66
|
+
if (files.includes('runtime.json')) throw new Error('Codex 内核目录与保留清单文件名冲突。');
|
|
67
|
+
const directories = runtimeDirectories(files);
|
|
68
|
+
const root = path.join(stateRoot, 'desktop-runtimes');
|
|
69
|
+
const target = path.join(root, installed.package);
|
|
70
|
+
const retained = path.join(target, 'codex.exe');
|
|
71
|
+
if (fs.existsSync(target)) {
|
|
72
|
+
const manifest = readRecord(path.join(target, 'runtime.json'));
|
|
73
|
+
if (manifest?.package !== installed.package || JSON.stringify(manifest.files) !== JSON.stringify(files)
|
|
74
|
+
|| JSON.stringify(runtimeFiles(target)) !== JSON.stringify([...files, 'runtime.json'].sort())
|
|
75
|
+
|| !await matchesInstalledDesktop(retained, installed)) {
|
|
76
|
+
throw new Error(`已保留的 Codex 内核文件不完整:${target}`);
|
|
77
|
+
}
|
|
78
|
+
return retained;
|
|
79
|
+
}
|
|
80
|
+
fs.mkdirSync(root, { recursive: true });
|
|
81
|
+
const staging = path.join(root, `.${installed.package}.${randomUUID()}.tmp`);
|
|
82
|
+
fs.mkdirSync(staging);
|
|
83
|
+
const created = [];
|
|
84
|
+
try {
|
|
85
|
+
for (const directory of directories) fs.mkdirSync(path.join(staging, directory));
|
|
86
|
+
for (const name of files) {
|
|
87
|
+
const from = path.join(source, name), to = path.join(staging, name);
|
|
88
|
+
created.push(to);
|
|
89
|
+
try { fs.linkSync(from, to); }
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code !== 'EXDEV') throw error;
|
|
92
|
+
fs.copyFileSync(from, to);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!await matchesInstalledDesktop(path.join(staging, 'codex.exe'), installed)) {
|
|
96
|
+
throw new Error('保留的 Codex 内核与当前安装包不一致。');
|
|
97
|
+
}
|
|
98
|
+
const manifestPath = path.join(staging, 'runtime.json');
|
|
99
|
+
writeRecord(manifestPath, { package: installed.package, files });
|
|
100
|
+
created.push(manifestPath);
|
|
101
|
+
fs.renameSync(staging, target);
|
|
102
|
+
return retained;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
for (const file of created) fs.rmSync(file, { force: true });
|
|
105
|
+
for (const directory of directories.reverse()) {
|
|
106
|
+
const nested = path.join(staging, directory);
|
|
107
|
+
if (fs.existsSync(nested)) fs.rmdirSync(nested);
|
|
108
|
+
}
|
|
109
|
+
fs.rmdirSync(staging);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function pruneRetainedDesktopRuntimes(stateRoot, keepExecutable) {
|
|
115
|
+
const root = path.join(stateRoot, 'desktop-runtimes');
|
|
116
|
+
if (!fs.existsSync(root)) return;
|
|
117
|
+
const keep = path.resolve(path.dirname(keepExecutable)).toLowerCase();
|
|
118
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
119
|
+
if (!entry.isDirectory() || !/^OpenAI\.Codex_[\w.-]+$/.test(entry.name)) continue;
|
|
120
|
+
const directory = path.join(root, entry.name);
|
|
121
|
+
if (path.resolve(directory).toLowerCase() === keep || fs.lstatSync(directory).isSymbolicLink()) continue;
|
|
122
|
+
const manifest = readRecord(path.join(directory, 'runtime.json'));
|
|
123
|
+
const files = manifest?.files;
|
|
124
|
+
if (manifest?.package !== entry.name || !Array.isArray(files) || !files.includes('codex.exe')
|
|
125
|
+
|| !files.includes('codex-code-mode-host.exe') || new Set(files).size !== files.length
|
|
126
|
+
|| !files.every(name => typeof name === 'string' && name !== 'runtime.json'
|
|
127
|
+
&& path.resolve(directory, name).toLowerCase().startsWith(`${path.resolve(directory).toLowerCase()}${path.sep}`))) continue;
|
|
128
|
+
const expected = [...files, 'runtime.json'].sort();
|
|
129
|
+
if (JSON.stringify(runtimeFiles(directory)) !== JSON.stringify(expected)) continue;
|
|
130
|
+
if (!fs.lstatSync(path.join(directory, 'runtime.json')).isFile()
|
|
131
|
+
|| !files.every(name => fs.lstatSync(path.join(directory, name)).isFile())) continue;
|
|
132
|
+
for (const name of files) fs.unlinkSync(path.join(directory, name));
|
|
133
|
+
fs.unlinkSync(path.join(directory, 'runtime.json'));
|
|
134
|
+
for (const name of runtimeDirectories(files).reverse()) fs.rmdirSync(path.join(directory, name));
|
|
135
|
+
fs.rmdirSync(directory);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Resolve native binaries, never execute a shell shim from a background task.
|
|
140
|
+
export function discoverCodex(environment = process.env) {
|
|
141
|
+
const found = new Map();
|
|
142
|
+
const add = (file, source) => {
|
|
143
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
|
|
144
|
+
const executable = fs.realpathSync(file);
|
|
145
|
+
if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
|
|
146
|
+
};
|
|
147
|
+
if (environment.CODE_RELAX_CODEX_PATH) {
|
|
148
|
+
const file = environment.CODE_RELAX_CODEX_PATH;
|
|
149
|
+
if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
|
|
150
|
+
add(file, 'explicit');
|
|
151
|
+
if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
|
|
152
|
+
return [...found.values()];
|
|
153
|
+
}
|
|
154
|
+
const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
|
|
155
|
+
const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
156
|
+
const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
|
|
157
|
+
environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
|
|
158
|
+
path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
|
|
159
|
+
for (const directory of directories) {
|
|
160
|
+
add(path.join(directory, 'codex.exe'), 'cli');
|
|
161
|
+
const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
|
|
162
|
+
if (!fs.existsSync(packageFile)) continue;
|
|
163
|
+
const root = path.dirname(fs.realpathSync(packageFile));
|
|
164
|
+
let vendor = path.join(root, 'vendor');
|
|
165
|
+
try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
|
|
166
|
+
catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
|
|
167
|
+
add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
|
|
168
|
+
}
|
|
169
|
+
const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
|
|
170
|
+
add(path.join(desktopRoot, 'codex.exe'), 'desktop');
|
|
171
|
+
if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
|
|
172
|
+
if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
|
|
173
|
+
}
|
|
174
|
+
return [...found.values()];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
|
|
178
|
+
fs.mkdirSync(stateRoot, { recursive: true });
|
|
179
|
+
const recordFile = path.join(stateRoot, 'codex-runtime.json');
|
|
180
|
+
const saved = readRecord(recordFile);
|
|
181
|
+
environment = { ...environment,
|
|
182
|
+
CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
|
|
183
|
+
CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
|
|
184
|
+
if (!refresh && saved?.executable && runtimeFilesAvailable(saved.executable)
|
|
185
|
+
&& saved.fingerprint === fingerprint(saved.executable)) return saved;
|
|
186
|
+
const candidates = [], rejected = [];
|
|
187
|
+
let installed = null;
|
|
188
|
+
try { installed = await installedDesktopRuntime(); }
|
|
189
|
+
catch (error) { rejected.push(`无法读取当前 Codex Desktop 安装包:${error.message}`); }
|
|
190
|
+
for (const candidate of discoverCodex(environment)) {
|
|
191
|
+
try {
|
|
192
|
+
if (!runtimeFilesAvailable(candidate.executable)) throw new Error('缺少同目录的 codex-code-mode-host.exe');
|
|
193
|
+
if (candidate.source === 'desktop' && !await matchesInstalledDesktop(candidate.executable, installed)) {
|
|
194
|
+
throw new Error('不是当前 Codex Desktop 安装包配套的 CLI');
|
|
195
|
+
}
|
|
196
|
+
const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
|
|
197
|
+
const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
|
|
198
|
+
if (!version) throw new Error('无法识别版本');
|
|
199
|
+
candidates.push({ ...candidate, version });
|
|
200
|
+
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
201
|
+
}
|
|
202
|
+
candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
try {
|
|
205
|
+
const executable = candidate.source === 'desktop'
|
|
206
|
+
? await retainDesktopRuntime(candidate.executable, installed, stateRoot) : candidate.executable;
|
|
207
|
+
await verifyProtocol(executable, stateRoot);
|
|
208
|
+
const result = { ...candidate, executable, fingerprint: fingerprint(executable),
|
|
209
|
+
codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
|
|
210
|
+
writeRecord(recordFile, result);
|
|
211
|
+
return result;
|
|
212
|
+
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
|
|
215
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { runtimePaths } from './runtime-paths.mjs';
|
|
5
|
-
|
|
6
|
-
// Secrets travel over stdin/stdout only; never PowerShell arguments or errors.
|
|
7
|
-
export function credentialStore(action, record, root = runtimePaths().credentialsRoot) {
|
|
8
|
-
if (process.platform !== 'win32') throw new Error('Credential platform adapter unavailable');
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
const child = spawn(path.join(process.env.SystemRoot, 'System32/WindowsPowerShell/v1.0/powershell.exe'),
|
|
11
|
-
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
12
|
-
fileURLToPath(new URL('./credential-store.ps1', import.meta.url))],
|
|
13
|
-
{ windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
14
|
-
let output = '', diagnostic = '';
|
|
15
|
-
const timer = setTimeout(() => child.kill(), 15000);
|
|
16
|
-
child.stdout.setEncoding('utf8'); child.stdout.on('data', data => { output += data; });
|
|
17
|
-
child.stderr.setEncoding('utf8'); child.stderr.on('data', data => { diagnostic += data; }); child.stdin.on('error', () => {});
|
|
18
|
-
child.once('error', () => { clearTimeout(timer); reject(new Error('无法启动 Windows 凭据保护组件')); });
|
|
19
|
-
child.once('close', code => {
|
|
20
|
-
clearTimeout(timer);
|
|
21
|
-
if (code !== 0) return reject(new Error('Windows 凭据保护失败,未使用明文替代;请检查目录权限或 Windows 用户是否一致;' + diagnostic.trim()));
|
|
22
|
-
try { resolve(JSON.parse(output.replace(/^\uFEFF/, ''))); }
|
|
23
|
-
catch { reject(new Error('Windows 凭据保护返回无效结果')); }
|
|
24
|
-
});
|
|
25
|
-
child.stdin.end(JSON.stringify({ action, root, record }));
|
|
26
|
-
});
|
|
27
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { runtimePaths } from './runtime-paths.mjs';
|
|
5
|
+
|
|
6
|
+
// Secrets travel over stdin/stdout only; never PowerShell arguments or errors.
|
|
7
|
+
export function credentialStore(action, record, root = runtimePaths().credentialsRoot) {
|
|
8
|
+
if (process.platform !== 'win32') throw new Error('Credential platform adapter unavailable');
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = spawn(path.join(process.env.SystemRoot, 'System32/WindowsPowerShell/v1.0/powershell.exe'),
|
|
11
|
+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
12
|
+
fileURLToPath(new URL('./credential-store.ps1', import.meta.url))],
|
|
13
|
+
{ windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
14
|
+
let output = '', diagnostic = '';
|
|
15
|
+
const timer = setTimeout(() => child.kill(), 15000);
|
|
16
|
+
child.stdout.setEncoding('utf8'); child.stdout.on('data', data => { output += data; });
|
|
17
|
+
child.stderr.setEncoding('utf8'); child.stderr.on('data', data => { diagnostic += data; }); child.stdin.on('error', () => {});
|
|
18
|
+
child.once('error', () => { clearTimeout(timer); reject(new Error('无法启动 Windows 凭据保护组件')); });
|
|
19
|
+
child.once('close', code => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
if (code !== 0) return reject(new Error('Windows 凭据保护失败,未使用明文替代;请检查目录权限或 Windows 用户是否一致;' + diagnostic.trim()));
|
|
22
|
+
try { resolve(JSON.parse(output.replace(/^\uFEFF/, ''))); }
|
|
23
|
+
catch { reject(new Error('Windows 凭据保护返回无效结果')); }
|
|
24
|
+
});
|
|
25
|
+
child.stdin.end(JSON.stringify({ action, root, record }));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
$ErrorActionPreference = 'Stop'
|
|
2
|
-
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
3
|
-
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
-
try {
|
|
5
|
-
Add-Type -AssemblyName System.Security
|
|
6
|
-
$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
7
|
-
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
8
|
-
$directory = Join-Path ([System.IO.Path]::GetFullPath($request.root)) $sid.Value
|
|
9
|
-
if (!(Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
|
|
10
|
-
if ((Get-Item -LiteralPath $directory).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential directory' }
|
|
11
|
-
$acl = New-Object System.Security.AccessControl.DirectorySecurity
|
|
12
|
-
$acl.SetAccessRuleProtection($true, $false)
|
|
13
|
-
foreach ($principal in @($sid.Value, 'S-1-5-18', 'S-1-5-32-544')) {
|
|
14
|
-
$identity = New-Object System.Security.Principal.SecurityIdentifier($principal)
|
|
15
|
-
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
16
|
-
$acl.AddAccessRule($rule)
|
|
17
|
-
}
|
|
18
|
-
[System.IO.Directory]::SetAccessControl($directory, $acl)
|
|
19
|
-
$file = Join-Path $directory 'self-relay.json'
|
|
20
|
-
if (Test-Path -LiteralPath $file) {
|
|
21
|
-
if ((Get-Item -LiteralPath $file).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential file' }
|
|
22
|
-
}
|
|
23
|
-
if ($request.action -eq 'save') {
|
|
24
|
-
$plain = [System.Text.Encoding]::UTF8.GetBytes(($request.record | ConvertTo-Json -Depth 12 -Compress))
|
|
25
|
-
$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($plain, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
26
|
-
$json = @{ version = 1; protection = 'DPAPI-CurrentUser'; data = [Convert]::ToBase64String($encrypted) } | ConvertTo-Json -Compress
|
|
27
|
-
$temporary = Join-Path $directory ([Guid]::NewGuid().ToString() + '.tmp')
|
|
28
|
-
$backup = $temporary + '.bak'
|
|
29
|
-
try {
|
|
30
|
-
$stream = [System.IO.File]::Open($temporary, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
|
|
31
|
-
try { $bytes = [System.Text.Encoding]::UTF8.GetBytes($json); $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
|
|
32
|
-
if (Test-Path -LiteralPath $file) { [System.IO.File]::Replace($temporary, $file, $backup) } else { [System.IO.File]::Move($temporary, $file) }
|
|
33
|
-
} finally {
|
|
34
|
-
if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force }
|
|
35
|
-
if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Force }
|
|
36
|
-
}
|
|
37
|
-
} elseif ($request.action -ne 'load') { throw 'Invalid operation' }
|
|
38
|
-
$record = $null
|
|
39
|
-
if (Test-Path -LiteralPath $file) {
|
|
40
|
-
$envelope = [System.IO.File]::ReadAllText($file, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
|
|
41
|
-
if ($envelope.version -ne 1 -or $envelope.protection -ne 'DPAPI-CurrentUser') { throw 'Invalid credential envelope' }
|
|
42
|
-
$plain = [System.Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($envelope.data), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
43
|
-
$record = [System.Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json
|
|
44
|
-
}
|
|
45
|
-
[Console]::Write((@{ path = $file; record = $record } | ConvertTo-Json -Depth 12 -Compress))
|
|
46
|
-
} catch {
|
|
47
|
-
[Console]::Error.WriteLine('Credential protection failed: ' + $_.Exception.GetType().Name + ' at line ' + $_.InvocationInfo.ScriptLineNumber)
|
|
48
|
-
exit 1
|
|
49
|
-
}
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
3
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
+
try {
|
|
5
|
+
Add-Type -AssemblyName System.Security
|
|
6
|
+
$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
7
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
8
|
+
$directory = Join-Path ([System.IO.Path]::GetFullPath($request.root)) $sid.Value
|
|
9
|
+
if (!(Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
|
|
10
|
+
if ((Get-Item -LiteralPath $directory).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential directory' }
|
|
11
|
+
$acl = New-Object System.Security.AccessControl.DirectorySecurity
|
|
12
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
13
|
+
foreach ($principal in @($sid.Value, 'S-1-5-18', 'S-1-5-32-544')) {
|
|
14
|
+
$identity = New-Object System.Security.Principal.SecurityIdentifier($principal)
|
|
15
|
+
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
16
|
+
$acl.AddAccessRule($rule)
|
|
17
|
+
}
|
|
18
|
+
[System.IO.Directory]::SetAccessControl($directory, $acl)
|
|
19
|
+
$file = Join-Path $directory 'self-relay.json'
|
|
20
|
+
if (Test-Path -LiteralPath $file) {
|
|
21
|
+
if ((Get-Item -LiteralPath $file).Attributes -band [System.IO.FileAttributes]::ReparsePoint) { throw 'Invalid credential file' }
|
|
22
|
+
}
|
|
23
|
+
if ($request.action -eq 'save') {
|
|
24
|
+
$plain = [System.Text.Encoding]::UTF8.GetBytes(($request.record | ConvertTo-Json -Depth 12 -Compress))
|
|
25
|
+
$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($plain, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
26
|
+
$json = @{ version = 1; protection = 'DPAPI-CurrentUser'; data = [Convert]::ToBase64String($encrypted) } | ConvertTo-Json -Compress
|
|
27
|
+
$temporary = Join-Path $directory ([Guid]::NewGuid().ToString() + '.tmp')
|
|
28
|
+
$backup = $temporary + '.bak'
|
|
29
|
+
try {
|
|
30
|
+
$stream = [System.IO.File]::Open($temporary, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
|
|
31
|
+
try { $bytes = [System.Text.Encoding]::UTF8.GetBytes($json); $stream.Write($bytes, 0, $bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
|
|
32
|
+
if (Test-Path -LiteralPath $file) { [System.IO.File]::Replace($temporary, $file, $backup) } else { [System.IO.File]::Move($temporary, $file) }
|
|
33
|
+
} finally {
|
|
34
|
+
if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force }
|
|
35
|
+
if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Force }
|
|
36
|
+
}
|
|
37
|
+
} elseif ($request.action -ne 'load') { throw 'Invalid operation' }
|
|
38
|
+
$record = $null
|
|
39
|
+
if (Test-Path -LiteralPath $file) {
|
|
40
|
+
$envelope = [System.IO.File]::ReadAllText($file, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
|
|
41
|
+
if ($envelope.version -ne 1 -or $envelope.protection -ne 'DPAPI-CurrentUser') { throw 'Invalid credential envelope' }
|
|
42
|
+
$plain = [System.Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($envelope.data), $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
43
|
+
$record = [System.Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json
|
|
44
|
+
}
|
|
45
|
+
[Console]::Write((@{ path = $file; record = $record } | ConvertTo-Json -Depth 12 -Compress))
|
|
46
|
+
} catch {
|
|
47
|
+
[Console]::Error.WriteLine('Credential protection failed: ' + $_.Exception.GetType().Name + ' at line ' + $_.InvocationInfo.ScriptLineNumber)
|
|
48
|
+
exit 1
|
|
49
|
+
}
|