@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.
- package/dist/bin/codex-remote.mjs +4 -2
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-harmony-release.mjs +46 -46
- 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/bin/set-relay-admin-password.mjs +11 -11
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/shared/app-server-events.cjs +511 -511
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-ice-policy.cjs +22 -22
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -69
- package/dist/shared/release-version.cjs +31 -31
- package/dist/src/archived-threads.mjs +22 -22
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +62 -62
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +19 -19
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-permission-mode.mjs +17 -17
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/file-opener.mjs +9 -9
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -56
- package/dist/src/platform/linux/desktop-installation.mjs +36 -36
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/linux/network-info.mjs +11 -11
- package/dist/src/platform/linux/project-locations.mjs +17 -17
- package/dist/src/platform/linux/resident-startup.mjs +52 -52
- package/dist/src/platform/linux/retained-runtime.mjs +76 -76
- package/dist/src/platform/linux/service-host.mjs +18 -18
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/codex-runtime.mjs +0 -4
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/macos/network-info.mjs +8 -8
- 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/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -23
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/power-inhibitor.mjs +22 -22
- 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/background-process.mjs +30 -30
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
- package/dist/src/platform/windows/keep-awake.ps1 +20 -20
- package/dist/src/platform/windows/network-info.mjs +34 -34
- 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/start-hidden-console.ps1 +58 -58
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-create.mjs +121 -121
- package/dist/src/project-management.mjs +1 -1
- package/dist/src/project-remove.mjs +55 -55
- package/dist/src/project-update.mjs +59 -59
- package/dist/src/relay-config-import.mjs +45 -45
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-auth.mjs +59 -59
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-login-page.mjs +7 -7
- package/dist/src/self-relay/admin-server.mjs +105 -105
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
- package/dist/src/self-relay/admin-workstations.mjs +143 -143
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connection-reporting.mjs +38 -38
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +6 -6
- package/dist/src/self-relay/device-info.mjs +27 -27
- package/dist/src/self-relay/device-store.mjs +92 -92
- package/dist/src/self-relay/lan-authorizations.mjs +71 -71
- package/dist/src/self-relay/lifecycle.mjs +278 -278
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -118
- package/dist/src/self-relay/phone-connections.mjs +55 -55
- package/dist/src/self-relay/server.mjs +1066 -1066
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- 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 +1947 -1947
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-power.mjs +58 -58
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/service-update-worker.mjs +42 -42
- package/dist/src/service-update.mjs +80 -80
- package/dist/src/shared-recovery.mjs +152 -152
- 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/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 +4 -4
- package/tools/postinstall.mjs +1 -1
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import protocol from '../shared/code-browser.cjs';
|
|
5
|
-
import { fileRange, fileRevision } from './file-transfer-range.mjs';
|
|
6
|
-
|
|
7
|
-
async function checkedPath(root, relative) {
|
|
8
|
-
const target = protocol.child(root, relative);
|
|
9
|
-
return fs.realpath(target);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function git(cwd, args) {
|
|
13
|
-
return new Promise((resolve, reject) => execFile('git', ['--no-optional-locks', '--literal-pathspecs', '-c', 'core.quotePath=false', ...args],
|
|
14
|
-
{ cwd, encoding: 'utf8', maxBuffer: protocol.maxBytes, timeout: 15000, windowsHide: true },
|
|
15
|
-
(error, stdout, stderr) => {
|
|
16
|
-
if (error) reject(Object.assign(new Error(error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER'
|
|
17
|
-
? 'Git 输出超过预览上限(1 MiB)。' : String(stderr || error.message).trim()), { statusCode: 422 }));
|
|
18
|
-
else resolve(stdout);
|
|
19
|
-
}));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function browseCode(rpc, operation, query) {
|
|
23
|
-
if (operation === 'change') return protocol.change(rpc, query);
|
|
24
|
-
if (operation === 'changes') return protocol.changes(rpc, query);
|
|
25
|
-
if (operation === 'roots') return { ...await protocol.context(rpc, query), workspaceDiff: true, sizeCheckedBeforeRead: true };
|
|
26
|
-
const root = query.get('root');
|
|
27
|
-
if (typeof root !== 'string' || !path.isAbsolute(root)) protocol.fail('请输入电脑上的绝对目录路径。');
|
|
28
|
-
if (operation === 'workspace' || operation === 'workspace-file') {
|
|
29
|
-
const realRoot = await checkedPath(root, '');
|
|
30
|
-
const repository = (await git(realRoot, ['rev-parse', '--show-toplevel'])).trim();
|
|
31
|
-
const scope = path.relative(repository, realRoot).replaceAll('\\', '/') || '.';
|
|
32
|
-
if (scope.startsWith('..') || path.isAbsolute(scope)) protocol.fail('无法确认 Git 仓库范围。', 403);
|
|
33
|
-
const toRelative = name => protocol.relative(realRoot, path.resolve(repository, name));
|
|
34
|
-
if (operation === 'workspace-file') {
|
|
35
|
-
const group = query.get('group');
|
|
36
|
-
if (!['staged', 'unstaged'].includes(group)) protocol.fail('未知的变更来源。');
|
|
37
|
-
const names = [query.get('path'), query.get('oldPath')].filter(Boolean);
|
|
38
|
-
if (!names.length) protocol.fail('请选择变更文件。');
|
|
39
|
-
const paths = names.map(name => path.relative(repository, protocol.child(realRoot, name)).replaceAll('\\', '/'));
|
|
40
|
-
const diff = await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--no-ext-diff', '--no-textconv', '--no-color', names.length > 1 ? '--find-renames' : '--no-renames', '--', ...paths]);
|
|
41
|
-
return { diff, scope: group === 'staged' ? '当前工作区 · 已暂存' : '当前工作区 · 未暂存' };
|
|
42
|
-
}
|
|
43
|
-
const entries = [];
|
|
44
|
-
for (const group of ['unstaged', 'staged']) {
|
|
45
|
-
const fields = (await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--name-status', '-z', '--find-renames', '--no-ext-diff', '--no-textconv', '--', scope])).split('\0');
|
|
46
|
-
for (let i = 0; i < fields.length && fields[i];) {
|
|
47
|
-
const status = fields[i++], first = fields[i++];
|
|
48
|
-
const renamed = /^[RC]/.test(status), last = renamed ? fields[i++] : first;
|
|
49
|
-
const name = toRelative(last), oldPath = renamed ? toRelative(first) : null;
|
|
50
|
-
if (name == null) { if (oldPath != null) entries.push({ path: oldPath, oldPath: null, status: 'D', group }); continue; }
|
|
51
|
-
entries.push({ path: name, oldPath, status: renamed && oldPath == null ? 'A' : status, group });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
for (const name of (await git(repository, ['ls-files', '--others', '--exclude-standard', '-z', '--', scope])).split('\0').filter(Boolean)) {
|
|
55
|
-
const relative = toRelative(name);
|
|
56
|
-
if (relative != null) entries.push({ path: relative, status: '?', group: 'untracked' });
|
|
57
|
-
}
|
|
58
|
-
if (entries.length > protocol.maxEntries) protocol.fail('变更文件超过 4000 项,暂不支持展示。', 413);
|
|
59
|
-
return { root, entries, scope: '当前工作区快照 · 非本次任务归属' };
|
|
60
|
-
}
|
|
61
|
-
const relative = query.get('path') || '';
|
|
62
|
-
const target = await checkedPath(root, relative);
|
|
63
|
-
if ((operation === 'image' || operation === 'binary')) {
|
|
64
|
-
const handle = await fs.open(target, 'r');
|
|
65
|
-
try {
|
|
66
|
-
const before = await handle.stat();
|
|
67
|
-
if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
|
|
68
|
-
if (before.size > protocol.maxFileBytes) protocol.fail('文件超过预览上限(20 MiB)。', 413);
|
|
69
|
-
if (!before.size) protocol.fail('文件为空。', 415);
|
|
70
|
-
const revision = fileRevision(before);
|
|
71
|
-
const { offset, length } = fileRange(query, before.size, revision);
|
|
72
|
-
const header = Buffer.alloc(12);
|
|
73
|
-
await handle.read(header, 0, header.length, 0);
|
|
74
|
-
const mimeType = operation === 'binary' ? 'application/octet-stream' : protocol.imageMime(header);
|
|
75
|
-
const data = Buffer.alloc(length);
|
|
76
|
-
const { bytesRead } = await handle.read(data, 0, data.length, offset);
|
|
77
|
-
const real = await checkedPath(root, relative), after = await handle.stat(), onDisk = await fs.stat(real);
|
|
78
|
-
if (real !== target || bytesRead !== data.length || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || after.size !== before.size || onDisk.ino !== before.ino || onDisk.dev !== before.dev || onDisk.mtimeMs !== before.mtimeMs || onDisk.size !== before.size) protocol.fail('读取期间文件发生变化,请重新打开。', 409);
|
|
79
|
-
return { name: path.basename(target), size: before.size, offset, revision, mimeType, data };
|
|
80
|
-
} finally { await handle.close(); }
|
|
81
|
-
}
|
|
82
|
-
if (operation === 'directory') {
|
|
83
|
-
const entries = await fs.readdir(target, { withFileTypes: true });
|
|
84
|
-
const result = { entries: entries.map(entry => ({ fileName: entry.name, isDirectory: entry.isDirectory(), isFile: entry.isFile() })) };
|
|
85
|
-
await checkedPath(root, relative);
|
|
86
|
-
return protocol.directory(result);
|
|
87
|
-
}
|
|
88
|
-
if (operation === 'file') {
|
|
89
|
-
const before = await fs.stat(target);
|
|
90
|
-
if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
|
|
91
|
-
if (before.size > protocol.maxBytes) protocol.fail('文件超过只读预览上限(1 MiB)。', 413);
|
|
92
|
-
const result = { dataBase64: (await fs.readFile(target)).toString('base64') };
|
|
93
|
-
const real = await checkedPath(root, relative), after = await fs.stat(real);
|
|
94
|
-
if (real !== target || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino) {
|
|
95
|
-
protocol.fail('读取期间文件发生变化,请刷新。', 409);
|
|
96
|
-
}
|
|
97
|
-
return { ...protocol.fromBase64(result.dataBase64), modifiedAtMs: after.mtimeMs };
|
|
98
|
-
}
|
|
99
|
-
protocol.fail('未知的代码浏览操作。', 404);
|
|
100
|
-
}
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import protocol from '../shared/code-browser.cjs';
|
|
5
|
+
import { fileRange, fileRevision } from './file-transfer-range.mjs';
|
|
6
|
+
|
|
7
|
+
async function checkedPath(root, relative) {
|
|
8
|
+
const target = protocol.child(root, relative);
|
|
9
|
+
return fs.realpath(target);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function git(cwd, args) {
|
|
13
|
+
return new Promise((resolve, reject) => execFile('git', ['--no-optional-locks', '--literal-pathspecs', '-c', 'core.quotePath=false', ...args],
|
|
14
|
+
{ cwd, encoding: 'utf8', maxBuffer: protocol.maxBytes, timeout: 15000, windowsHide: true },
|
|
15
|
+
(error, stdout, stderr) => {
|
|
16
|
+
if (error) reject(Object.assign(new Error(error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER'
|
|
17
|
+
? 'Git 输出超过预览上限(1 MiB)。' : String(stderr || error.message).trim()), { statusCode: 422 }));
|
|
18
|
+
else resolve(stdout);
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function browseCode(rpc, operation, query) {
|
|
23
|
+
if (operation === 'change') return protocol.change(rpc, query);
|
|
24
|
+
if (operation === 'changes') return protocol.changes(rpc, query);
|
|
25
|
+
if (operation === 'roots') return { ...await protocol.context(rpc, query), workspaceDiff: true, sizeCheckedBeforeRead: true };
|
|
26
|
+
const root = query.get('root');
|
|
27
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) protocol.fail('请输入电脑上的绝对目录路径。');
|
|
28
|
+
if (operation === 'workspace' || operation === 'workspace-file') {
|
|
29
|
+
const realRoot = await checkedPath(root, '');
|
|
30
|
+
const repository = (await git(realRoot, ['rev-parse', '--show-toplevel'])).trim();
|
|
31
|
+
const scope = path.relative(repository, realRoot).replaceAll('\\', '/') || '.';
|
|
32
|
+
if (scope.startsWith('..') || path.isAbsolute(scope)) protocol.fail('无法确认 Git 仓库范围。', 403);
|
|
33
|
+
const toRelative = name => protocol.relative(realRoot, path.resolve(repository, name));
|
|
34
|
+
if (operation === 'workspace-file') {
|
|
35
|
+
const group = query.get('group');
|
|
36
|
+
if (!['staged', 'unstaged'].includes(group)) protocol.fail('未知的变更来源。');
|
|
37
|
+
const names = [query.get('path'), query.get('oldPath')].filter(Boolean);
|
|
38
|
+
if (!names.length) protocol.fail('请选择变更文件。');
|
|
39
|
+
const paths = names.map(name => path.relative(repository, protocol.child(realRoot, name)).replaceAll('\\', '/'));
|
|
40
|
+
const diff = await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--no-ext-diff', '--no-textconv', '--no-color', names.length > 1 ? '--find-renames' : '--no-renames', '--', ...paths]);
|
|
41
|
+
return { diff, scope: group === 'staged' ? '当前工作区 · 已暂存' : '当前工作区 · 未暂存' };
|
|
42
|
+
}
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const group of ['unstaged', 'staged']) {
|
|
45
|
+
const fields = (await git(repository, ['diff', ...(group === 'staged' ? ['--cached'] : []), '--name-status', '-z', '--find-renames', '--no-ext-diff', '--no-textconv', '--', scope])).split('\0');
|
|
46
|
+
for (let i = 0; i < fields.length && fields[i];) {
|
|
47
|
+
const status = fields[i++], first = fields[i++];
|
|
48
|
+
const renamed = /^[RC]/.test(status), last = renamed ? fields[i++] : first;
|
|
49
|
+
const name = toRelative(last), oldPath = renamed ? toRelative(first) : null;
|
|
50
|
+
if (name == null) { if (oldPath != null) entries.push({ path: oldPath, oldPath: null, status: 'D', group }); continue; }
|
|
51
|
+
entries.push({ path: name, oldPath, status: renamed && oldPath == null ? 'A' : status, group });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const name of (await git(repository, ['ls-files', '--others', '--exclude-standard', '-z', '--', scope])).split('\0').filter(Boolean)) {
|
|
55
|
+
const relative = toRelative(name);
|
|
56
|
+
if (relative != null) entries.push({ path: relative, status: '?', group: 'untracked' });
|
|
57
|
+
}
|
|
58
|
+
if (entries.length > protocol.maxEntries) protocol.fail('变更文件超过 4000 项,暂不支持展示。', 413);
|
|
59
|
+
return { root, entries, scope: '当前工作区快照 · 非本次任务归属' };
|
|
60
|
+
}
|
|
61
|
+
const relative = query.get('path') || '';
|
|
62
|
+
const target = await checkedPath(root, relative);
|
|
63
|
+
if ((operation === 'image' || operation === 'binary')) {
|
|
64
|
+
const handle = await fs.open(target, 'r');
|
|
65
|
+
try {
|
|
66
|
+
const before = await handle.stat();
|
|
67
|
+
if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
|
|
68
|
+
if (before.size > protocol.maxFileBytes) protocol.fail('文件超过预览上限(20 MiB)。', 413);
|
|
69
|
+
if (!before.size) protocol.fail('文件为空。', 415);
|
|
70
|
+
const revision = fileRevision(before);
|
|
71
|
+
const { offset, length } = fileRange(query, before.size, revision);
|
|
72
|
+
const header = Buffer.alloc(12);
|
|
73
|
+
await handle.read(header, 0, header.length, 0);
|
|
74
|
+
const mimeType = operation === 'binary' ? 'application/octet-stream' : protocol.imageMime(header);
|
|
75
|
+
const data = Buffer.alloc(length);
|
|
76
|
+
const { bytesRead } = await handle.read(data, 0, data.length, offset);
|
|
77
|
+
const real = await checkedPath(root, relative), after = await handle.stat(), onDisk = await fs.stat(real);
|
|
78
|
+
if (real !== target || bytesRead !== data.length || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || after.size !== before.size || onDisk.ino !== before.ino || onDisk.dev !== before.dev || onDisk.mtimeMs !== before.mtimeMs || onDisk.size !== before.size) protocol.fail('读取期间文件发生变化,请重新打开。', 409);
|
|
79
|
+
return { name: path.basename(target), size: before.size, offset, revision, mimeType, data };
|
|
80
|
+
} finally { await handle.close(); }
|
|
81
|
+
}
|
|
82
|
+
if (operation === 'directory') {
|
|
83
|
+
const entries = await fs.readdir(target, { withFileTypes: true });
|
|
84
|
+
const result = { entries: entries.map(entry => ({ fileName: entry.name, isDirectory: entry.isDirectory(), isFile: entry.isFile() })) };
|
|
85
|
+
await checkedPath(root, relative);
|
|
86
|
+
return protocol.directory(result);
|
|
87
|
+
}
|
|
88
|
+
if (operation === 'file') {
|
|
89
|
+
const before = await fs.stat(target);
|
|
90
|
+
if (!before.isFile()) protocol.fail('请选择普通文件。', 415);
|
|
91
|
+
if (before.size > protocol.maxBytes) protocol.fail('文件超过只读预览上限(1 MiB)。', 413);
|
|
92
|
+
const result = { dataBase64: (await fs.readFile(target)).toString('base64') };
|
|
93
|
+
const real = await checkedPath(root, relative), after = await fs.stat(real);
|
|
94
|
+
if (real !== target || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino) {
|
|
95
|
+
protocol.fail('读取期间文件发生变化,请刷新。', 409);
|
|
96
|
+
}
|
|
97
|
+
return { ...protocol.fromBase64(result.dataBase64), modifiedAtMs: after.mtimeMs };
|
|
98
|
+
}
|
|
99
|
+
protocol.fail('未知的代码浏览操作。', 404);
|
|
100
|
+
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
-
import { readRecord, lifecycle } from './service-lifecycle.mjs';
|
|
5
|
-
import * as serviceHost from './platform/service-host.mjs';
|
|
6
|
-
import { managedPortAvailable as portAvailable } from './managed-app-server.mjs';
|
|
7
|
-
import { readManagedEndpoint } from './managed-endpoint.mjs';
|
|
8
|
-
export { portAvailable };
|
|
9
|
-
function desktopEnvironment(stateRoot, environment) {
|
|
10
|
-
const childEnv = { ...environment };
|
|
11
|
-
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
|
|
12
|
-
const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
|
|
13
|
-
if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
|
|
14
|
-
childEnv.CODEX_APP_SERVER_WS_URL = readManagedEndpoint(stateRoot);
|
|
15
|
-
return childEnv;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Opening the optional UI does not start, restart or supervise the kernel.
|
|
19
|
-
export async function startDesktop({ stateRoot, host, environment = process.env, restart = false }) {
|
|
20
|
-
const info = await host.inspect();
|
|
21
|
-
if (!info.desktop) throw new Error('未安装 Codex 桌面端。');
|
|
22
|
-
if (info.running && !restart) return { status: 'running', message: 'Codex 桌面端已启动。' };
|
|
23
|
-
if (info.running) await host.closeDesktop(info.desktop);
|
|
24
|
-
await host.startDesktop(info.desktop, desktopEnvironment(stateRoot, environment));
|
|
25
|
-
return { status: 'accepted', message: restart ? '已提交 Codex 桌面端重启请求。' : '已提交 Codex 桌面端启动请求。' };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function launchDesktop({ stateRoot, host, environment = process.env, wait = delay, timeout = 30000, recoveryId, progress = () => {}, service = lifecycle({ stateRoot, host: serviceHost }) }) {
|
|
29
|
-
fs.mkdirSync(stateRoot, { recursive: true });
|
|
30
|
-
const recovery = readRecord(path.join(stateRoot, 'shared-recovery.lock'));
|
|
31
|
-
if (recovery && recovery.operationId !== recoveryId) throw new Error('Codex 内核正在启动,请用 relax doctor 查看恢复状态。');
|
|
32
|
-
const lock = path.join(stateRoot, 'desktop-launch.lock');
|
|
33
|
-
let descriptor;
|
|
34
|
-
try { descriptor = fs.openSync(lock, 'wx'); }
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (error.code === 'EEXIST') throw new Error('另一个 Desktop 启动操作正在进行;若上次异常退出,请确认没有启动器运行后移除 desktop-launch.lock。');
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
const info = await host.inspect();
|
|
41
|
-
if (!info.desktop) throw new Error('未安装 Codex Desktop,请使用 relax start 启动工作站。');
|
|
42
|
-
const connected = await host.sharedServiceUrl();
|
|
43
|
-
if (info.running) {
|
|
44
|
-
if (connected) return { reused: true };
|
|
45
|
-
throw new Error('请先保存任务并完全退出 Codex Desktop,再接入 Codex 内核;不会自动结束正在运行的任务。');
|
|
46
|
-
}
|
|
47
|
-
progress('starting-app-server');
|
|
48
|
-
const health = await service.status();
|
|
49
|
-
if (!health.running || !health.managedAppServer?.ready) throw new Error(health.managedAppServer?.error || '工作站 Codex 内核未就绪。');
|
|
50
|
-
const childEnv = desktopEnvironment(stateRoot, environment);
|
|
51
|
-
progress('starting-desktop');
|
|
52
|
-
await host.startDesktop(info.desktop, childEnv);
|
|
53
|
-
const deadline = Date.now() + timeout;
|
|
54
|
-
while (true) {
|
|
55
|
-
const ready = await host.desktopReady();
|
|
56
|
-
if (ready) break;
|
|
57
|
-
if (Date.now() >= deadline) throw new Error('Desktop 尚未接入 Codex 内核。请查看 Desktop 窗口后重试。');
|
|
58
|
-
await wait(500);
|
|
59
|
-
}
|
|
60
|
-
return { reused: false };
|
|
61
|
-
} finally { fs.closeSync(descriptor); fs.rmSync(lock, { force: true }); }
|
|
62
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { readRecord, lifecycle } from './service-lifecycle.mjs';
|
|
5
|
+
import * as serviceHost from './platform/service-host.mjs';
|
|
6
|
+
import { managedPortAvailable as portAvailable } from './managed-app-server.mjs';
|
|
7
|
+
import { readManagedEndpoint } from './managed-endpoint.mjs';
|
|
8
|
+
export { portAvailable };
|
|
9
|
+
function desktopEnvironment(stateRoot, environment) {
|
|
10
|
+
const childEnv = { ...environment };
|
|
11
|
+
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnv[key];
|
|
12
|
+
const runtime = readRecord(path.join(stateRoot, 'codex-runtime.json'));
|
|
13
|
+
if (runtime?.codexHome) childEnv.CODEX_HOME = runtime.codexHome;
|
|
14
|
+
childEnv.CODEX_APP_SERVER_WS_URL = readManagedEndpoint(stateRoot);
|
|
15
|
+
return childEnv;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Opening the optional UI does not start, restart or supervise the kernel.
|
|
19
|
+
export async function startDesktop({ stateRoot, host, environment = process.env, restart = false }) {
|
|
20
|
+
const info = await host.inspect();
|
|
21
|
+
if (!info.desktop) throw new Error('未安装 Codex 桌面端。');
|
|
22
|
+
if (info.running && !restart) return { status: 'running', message: 'Codex 桌面端已启动。' };
|
|
23
|
+
if (info.running) await host.closeDesktop(info.desktop);
|
|
24
|
+
await host.startDesktop(info.desktop, desktopEnvironment(stateRoot, environment));
|
|
25
|
+
return { status: 'accepted', message: restart ? '已提交 Codex 桌面端重启请求。' : '已提交 Codex 桌面端启动请求。' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function launchDesktop({ stateRoot, host, environment = process.env, wait = delay, timeout = 30000, recoveryId, progress = () => {}, service = lifecycle({ stateRoot, host: serviceHost }) }) {
|
|
29
|
+
fs.mkdirSync(stateRoot, { recursive: true });
|
|
30
|
+
const recovery = readRecord(path.join(stateRoot, 'shared-recovery.lock'));
|
|
31
|
+
if (recovery && recovery.operationId !== recoveryId) throw new Error('Codex 内核正在启动,请用 relax doctor 查看恢复状态。');
|
|
32
|
+
const lock = path.join(stateRoot, 'desktop-launch.lock');
|
|
33
|
+
let descriptor;
|
|
34
|
+
try { descriptor = fs.openSync(lock, 'wx'); }
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === 'EEXIST') throw new Error('另一个 Desktop 启动操作正在进行;若上次异常退出,请确认没有启动器运行后移除 desktop-launch.lock。');
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const info = await host.inspect();
|
|
41
|
+
if (!info.desktop) throw new Error('未安装 Codex Desktop,请使用 relax start 启动工作站。');
|
|
42
|
+
const connected = await host.sharedServiceUrl();
|
|
43
|
+
if (info.running) {
|
|
44
|
+
if (connected) return { reused: true };
|
|
45
|
+
throw new Error('请先保存任务并完全退出 Codex Desktop,再接入 Codex 内核;不会自动结束正在运行的任务。');
|
|
46
|
+
}
|
|
47
|
+
progress('starting-app-server');
|
|
48
|
+
const health = await service.status();
|
|
49
|
+
if (!health.running || !health.managedAppServer?.ready) throw new Error(health.managedAppServer?.error || '工作站 Codex 内核未就绪。');
|
|
50
|
+
const childEnv = desktopEnvironment(stateRoot, environment);
|
|
51
|
+
progress('starting-desktop');
|
|
52
|
+
await host.startDesktop(info.desktop, childEnv);
|
|
53
|
+
const deadline = Date.now() + timeout;
|
|
54
|
+
while (true) {
|
|
55
|
+
const ready = await host.desktopReady();
|
|
56
|
+
if (ready) break;
|
|
57
|
+
if (Date.now() >= deadline) throw new Error('Desktop 尚未接入 Codex 内核。请查看 Desktop 窗口后重试。');
|
|
58
|
+
await wait(500);
|
|
59
|
+
}
|
|
60
|
+
return { reused: false };
|
|
61
|
+
} finally { fs.closeSync(descriptor); fs.rmSync(lock, { force: true }); }
|
|
62
|
+
}
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { readMessageUploads } from './message-upload-files.mjs';
|
|
3
|
-
|
|
4
|
-
// Desktop 26.1007.9446.0 Dll/Uhn/pA: reserve, upload bytes, confirm,
|
|
5
|
-
// then include an Uploaded file sediment pointer in the user message.
|
|
6
|
-
export async function uploadTaskFiles(files, { uploadsRoot, client, current, fetchImpl = fetch }) {
|
|
7
|
-
const uploads = await readMessageUploads({ files }, uploadsRoot);
|
|
8
|
-
const references = [];
|
|
9
|
-
for (const file of uploads) {
|
|
10
|
-
current();
|
|
11
|
-
const reserved = await client.request('/files', { method: 'POST', body: {
|
|
12
|
-
file_name: file.name, file_size: file.bytes.length, mime_type: file.mimeType,
|
|
13
|
-
use_case: 'codex', store_in_library: false, timezone_offset_min: new Date().getTimezoneOffset(), reset_rate_limits: false,
|
|
14
|
-
} });
|
|
15
|
-
current();
|
|
16
|
-
if (reserved?.status === 'error' || !/^[A-Za-z0-9_-]{1,256}$/.test(reserved?.file_id || '')
|
|
17
|
-
|| typeof reserved.upload_url !== 'string') throw new Error('云端文件上传地址未确认。');
|
|
18
|
-
const url = new URL(reserved.upload_url, 'https://chatgpt.com');
|
|
19
|
-
if (url.protocol !== 'https:' || url.username || url.password || url.port || url.hash) throw new Error('云端文件上传地址无效。');
|
|
20
|
-
if (url.origin === 'https://chatgpt.com' && url.pathname === '/backend-api/estuary/upload_content_bytes') {
|
|
21
|
-
const uploadUrl = url.searchParams.get('upload_url');
|
|
22
|
-
if (!uploadUrl) throw new Error('云端文件上传地址缺少参数。');
|
|
23
|
-
const boundary = `relax-${randomUUID()}`;
|
|
24
|
-
const name = file.name.replaceAll('"', '%22').replaceAll('\\', '%5C');
|
|
25
|
-
const body = Buffer.concat([Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${name}"\r\nContent-Type: ${file.mimeType}\r\n\r\n`),
|
|
26
|
-
file.bytes, Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="upload_url"\r\n\r\n${uploadUrl}\r\n--${boundary}--\r\n`)]);
|
|
27
|
-
await client.request(url.pathname.slice('/backend-api'.length) + url.search, {
|
|
28
|
-
method: 'POST', body, contentType: `multipart/form-data; boundary=${boundary}`, rawResponse: true,
|
|
29
|
-
});
|
|
30
|
-
} else {
|
|
31
|
-
if (!['.oaiusercontent.com', '.blob.core.windows.net', '.amazonaws.com'].some(suffix => url.hostname.endsWith(suffix))) {
|
|
32
|
-
throw new Error('云端文件上传地址不属于文件服务。');
|
|
33
|
-
}
|
|
34
|
-
const aws = [...url.searchParams.keys()].some(key => key.toLowerCase() === 'x-amz-algorithm');
|
|
35
|
-
const response = await fetchImpl(url, { method: 'PUT', body: file.bytes, redirect: 'error', signal: AbortSignal.timeout(60000),
|
|
36
|
-
headers: { 'Content-Type': file.mimeType, ...aws ? {} : { 'x-ms-blob-type': 'BlockBlob',
|
|
37
|
-
'x-ms-version': '2020-04-08', 'x-ms-blob-content-type': file.mimeType } } });
|
|
38
|
-
await response.body?.cancel();
|
|
39
|
-
if (!response.ok) throw new Error(`云端文件上传失败(${response.status})。`);
|
|
40
|
-
}
|
|
41
|
-
current();
|
|
42
|
-
await client.request(`/files/${encodeURIComponent(reserved.file_id)}/uploaded`, { method: 'POST', body: {} });
|
|
43
|
-
current();
|
|
44
|
-
references.push({ pointer: `sediment://${reserved.file_id}`, fileName: file.name });
|
|
45
|
-
}
|
|
46
|
-
return references;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function taskFilePrompt(text, references) {
|
|
50
|
-
if (!references.length) return text;
|
|
51
|
-
return `# Files mentioned by the user:\n${references.map(file => `\nUploaded file: ${JSON.stringify(file).replaceAll('#', '\\u0023')}\n`).join('')}\n## My request:\n${text || '请查看上述文件。'}`;
|
|
52
|
-
}
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readMessageUploads } from './message-upload-files.mjs';
|
|
3
|
+
|
|
4
|
+
// Desktop 26.1007.9446.0 Dll/Uhn/pA: reserve, upload bytes, confirm,
|
|
5
|
+
// then include an Uploaded file sediment pointer in the user message.
|
|
6
|
+
export async function uploadTaskFiles(files, { uploadsRoot, client, current, fetchImpl = fetch }) {
|
|
7
|
+
const uploads = await readMessageUploads({ files }, uploadsRoot);
|
|
8
|
+
const references = [];
|
|
9
|
+
for (const file of uploads) {
|
|
10
|
+
current();
|
|
11
|
+
const reserved = await client.request('/files', { method: 'POST', body: {
|
|
12
|
+
file_name: file.name, file_size: file.bytes.length, mime_type: file.mimeType,
|
|
13
|
+
use_case: 'codex', store_in_library: false, timezone_offset_min: new Date().getTimezoneOffset(), reset_rate_limits: false,
|
|
14
|
+
} });
|
|
15
|
+
current();
|
|
16
|
+
if (reserved?.status === 'error' || !/^[A-Za-z0-9_-]{1,256}$/.test(reserved?.file_id || '')
|
|
17
|
+
|| typeof reserved.upload_url !== 'string') throw new Error('云端文件上传地址未确认。');
|
|
18
|
+
const url = new URL(reserved.upload_url, 'https://chatgpt.com');
|
|
19
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.port || url.hash) throw new Error('云端文件上传地址无效。');
|
|
20
|
+
if (url.origin === 'https://chatgpt.com' && url.pathname === '/backend-api/estuary/upload_content_bytes') {
|
|
21
|
+
const uploadUrl = url.searchParams.get('upload_url');
|
|
22
|
+
if (!uploadUrl) throw new Error('云端文件上传地址缺少参数。');
|
|
23
|
+
const boundary = `relax-${randomUUID()}`;
|
|
24
|
+
const name = file.name.replaceAll('"', '%22').replaceAll('\\', '%5C');
|
|
25
|
+
const body = Buffer.concat([Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${name}"\r\nContent-Type: ${file.mimeType}\r\n\r\n`),
|
|
26
|
+
file.bytes, Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="upload_url"\r\n\r\n${uploadUrl}\r\n--${boundary}--\r\n`)]);
|
|
27
|
+
await client.request(url.pathname.slice('/backend-api'.length) + url.search, {
|
|
28
|
+
method: 'POST', body, contentType: `multipart/form-data; boundary=${boundary}`, rawResponse: true,
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
if (!['.oaiusercontent.com', '.blob.core.windows.net', '.amazonaws.com'].some(suffix => url.hostname.endsWith(suffix))) {
|
|
32
|
+
throw new Error('云端文件上传地址不属于文件服务。');
|
|
33
|
+
}
|
|
34
|
+
const aws = [...url.searchParams.keys()].some(key => key.toLowerCase() === 'x-amz-algorithm');
|
|
35
|
+
const response = await fetchImpl(url, { method: 'PUT', body: file.bytes, redirect: 'error', signal: AbortSignal.timeout(60000),
|
|
36
|
+
headers: { 'Content-Type': file.mimeType, ...aws ? {} : { 'x-ms-blob-type': 'BlockBlob',
|
|
37
|
+
'x-ms-version': '2020-04-08', 'x-ms-blob-content-type': file.mimeType } } });
|
|
38
|
+
await response.body?.cancel();
|
|
39
|
+
if (!response.ok) throw new Error(`云端文件上传失败(${response.status})。`);
|
|
40
|
+
}
|
|
41
|
+
current();
|
|
42
|
+
await client.request(`/files/${encodeURIComponent(reserved.file_id)}/uploaded`, { method: 'POST', body: {} });
|
|
43
|
+
current();
|
|
44
|
+
references.push({ pointer: `sediment://${reserved.file_id}`, fileName: file.name });
|
|
45
|
+
}
|
|
46
|
+
return references;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function taskFilePrompt(text, references) {
|
|
50
|
+
if (!references.length) return text;
|
|
51
|
+
return `# Files mentioned by the user:\n${references.map(file => `\nUploaded file: ${JSON.stringify(file).replaceAll('#', '\\u0023')}\n`).join('')}\n## My request:\n${text || '请查看上述文件。'}`;
|
|
52
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
// Execution availability is sampled in the background. Health requests may
|
|
2
|
-
// join the current sample so a reconnect cannot cache an initial false state.
|
|
3
|
-
// Only connections are retried, not work.
|
|
4
|
-
export class ExecutionHealth {
|
|
5
|
-
constructor(read, interval = 3000) { this.read = read; this.interval = interval; this.state = { state: 'checking', ready: false }; }
|
|
6
|
-
start() { this.stopped = false; void this.tick(); }
|
|
7
|
-
stop() { this.stopped = true; clearTimeout(this.timer); }
|
|
8
|
-
refresh() {
|
|
9
|
-
if (this.work) return this.work;
|
|
10
|
-
this.work = this.sample().finally(() => { this.work = null; });
|
|
11
|
-
return this.work;
|
|
12
|
-
}
|
|
13
|
-
async sample() {
|
|
14
|
-
try {
|
|
15
|
-
const result = await this.read();
|
|
16
|
-
this.state = result && typeof result === 'object'
|
|
17
|
-
? { ...result, ready: Boolean(result.ready), checkedAt: new Date().toISOString() }
|
|
18
|
-
: { state: result ? 'ready' : 'unavailable', ready: Boolean(result), checkedAt: new Date().toISOString() };
|
|
19
|
-
} catch (error) { this.state = { state: 'unavailable', ready: false, error: error.message, checkedAt: new Date().toISOString() }; }
|
|
20
|
-
return this.state;
|
|
21
|
-
}
|
|
22
|
-
async tick() {
|
|
23
|
-
await this.refresh();
|
|
24
|
-
if (!this.stopped) { this.timer = setTimeout(() => this.tick(), this.interval); this.timer.unref?.(); }
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
// Execution availability is sampled in the background. Health requests may
|
|
2
|
+
// join the current sample so a reconnect cannot cache an initial false state.
|
|
3
|
+
// Only connections are retried, not work.
|
|
4
|
+
export class ExecutionHealth {
|
|
5
|
+
constructor(read, interval = 3000) { this.read = read; this.interval = interval; this.state = { state: 'checking', ready: false }; }
|
|
6
|
+
start() { this.stopped = false; void this.tick(); }
|
|
7
|
+
stop() { this.stopped = true; clearTimeout(this.timer); }
|
|
8
|
+
refresh() {
|
|
9
|
+
if (this.work) return this.work;
|
|
10
|
+
this.work = this.sample().finally(() => { this.work = null; });
|
|
11
|
+
return this.work;
|
|
12
|
+
}
|
|
13
|
+
async sample() {
|
|
14
|
+
try {
|
|
15
|
+
const result = await this.read();
|
|
16
|
+
this.state = result && typeof result === 'object'
|
|
17
|
+
? { ...result, ready: Boolean(result.ready), checkedAt: new Date().toISOString() }
|
|
18
|
+
: { state: result ? 'ready' : 'unavailable', ready: Boolean(result), checkedAt: new Date().toISOString() };
|
|
19
|
+
} catch (error) { this.state = { state: 'unavailable', ready: false, error: error.message, checkedAt: new Date().toISOString() }; }
|
|
20
|
+
return this.state;
|
|
21
|
+
}
|
|
22
|
+
async tick() {
|
|
23
|
+
await this.refresh();
|
|
24
|
+
if (!this.stopped) { this.timer = setTimeout(() => this.tick(), this.interval); this.timer.unref?.(); }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export function validateInlineImage(value) {
|
|
2
|
-
const data = typeof value === 'string' ? value.slice(value.indexOf(',') + 1) : '';
|
|
3
|
-
if (typeof value !== 'string' || value.length > Math.ceil(8 * 1024 * 1024 / 3) * 4 + 64 ||
|
|
4
|
-
!/^data:image\/(?:jpeg|png|webp);base64,/.test(value) || !data || data.length % 4 !== 0 ||
|
|
5
|
-
!/^[A-Za-z0-9+/]*={0,2}$/.test(data) || Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) {
|
|
6
|
-
throw new Error('图片必须是 JPEG、PNG 或 WebP 数据,且不超过 8 MiB。');
|
|
7
|
-
}
|
|
8
|
-
return value;
|
|
9
|
-
}
|
|
1
|
+
export function validateInlineImage(value) {
|
|
2
|
+
const data = typeof value === 'string' ? value.slice(value.indexOf(',') + 1) : '';
|
|
3
|
+
if (typeof value !== 'string' || value.length > Math.ceil(8 * 1024 * 1024 / 3) * 4 + 64 ||
|
|
4
|
+
!/^data:image\/(?:jpeg|png|webp);base64,/.test(value) || !data || data.length % 4 !== 0 ||
|
|
5
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(data) || Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) {
|
|
6
|
+
throw new Error('图片必须是 JPEG、PNG 或 WebP 数据,且不超过 8 MiB。');
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import http2 from 'node:http2';
|
|
2
|
-
import pricing from '../shared/plan-pricing.cjs';
|
|
3
|
-
|
|
4
|
-
export async function readPlanPricing(client) {
|
|
5
|
-
await client.connect();
|
|
6
|
-
const token = async () => (await client.request('getAuthStatus', { includeToken: true, refreshToken: false })).authToken;
|
|
7
|
-
const authToken = await token();
|
|
8
|
-
if (!authToken) return null;
|
|
9
|
-
const identity = value => {
|
|
10
|
-
const auth = JSON.parse(Buffer.from(value.split('.')[1], 'base64url'))['https://api.openai.com/auth'];
|
|
11
|
-
return JSON.stringify([auth?.chatgpt_account_id, auth?.chatgpt_user_id]);
|
|
12
|
-
};
|
|
13
|
-
const initialIdentity = identity(authToken), accountId = JSON.parse(initialIdentity)[0];
|
|
14
|
-
if (!accountId) return null;
|
|
15
|
-
const session = http2.connect('https://chatgpt.com');
|
|
16
|
-
session.on('error', () => {}); // Each stream reports its own request error.
|
|
17
|
-
try {
|
|
18
|
-
const get = route => new Promise((resolve, reject) => {
|
|
19
|
-
const request = session.request({ ':method': 'GET', ':path': '/backend-api' + route,
|
|
20
|
-
authorization: `Bearer ${authToken}`, 'chatgpt-account-id': accountId,
|
|
21
|
-
'user-agent': 'Code-Relax/0.1.0', accept: 'application/json' });
|
|
22
|
-
let body = '', status, size = 0, ended = false;
|
|
23
|
-
const timer = setTimeout(() => request.destroy(new Error('套餐费用查询超时。')), 15000);
|
|
24
|
-
request.setEncoding('utf8');
|
|
25
|
-
request.on('response', headers => { status = headers[':status']; });
|
|
26
|
-
request.on('data', chunk => {
|
|
27
|
-
size += Buffer.byteLength(chunk);
|
|
28
|
-
if (size > 1024 * 1024) request.destroy(new Error('套餐费用响应过大。'));
|
|
29
|
-
else body += chunk;
|
|
30
|
-
});
|
|
31
|
-
request.on('error', reject);
|
|
32
|
-
request.on('close', () => { clearTimeout(timer); if (!ended) reject(new Error('套餐费用连接已断开。')); });
|
|
33
|
-
request.on('end', () => {
|
|
34
|
-
ended = true;
|
|
35
|
-
try {
|
|
36
|
-
if (status !== 200) throw new Error(`套餐费用查询失败(HTTP ${status})。`);
|
|
37
|
-
resolve(JSON.parse(body));
|
|
38
|
-
} catch (error) { reject(error); }
|
|
39
|
-
});
|
|
40
|
-
request.end();
|
|
41
|
-
});
|
|
42
|
-
const value = await pricing.read(accountId, get);
|
|
43
|
-
const current = await token();
|
|
44
|
-
if (!current || identity(current) !== initialIdentity) throw new Error('套餐查询期间账号已变化。');
|
|
45
|
-
return value;
|
|
46
|
-
} finally { session.destroy(); }
|
|
47
|
-
}
|
|
1
|
+
import http2 from 'node:http2';
|
|
2
|
+
import pricing from '../shared/plan-pricing.cjs';
|
|
3
|
+
|
|
4
|
+
export async function readPlanPricing(client) {
|
|
5
|
+
await client.connect();
|
|
6
|
+
const token = async () => (await client.request('getAuthStatus', { includeToken: true, refreshToken: false })).authToken;
|
|
7
|
+
const authToken = await token();
|
|
8
|
+
if (!authToken) return null;
|
|
9
|
+
const identity = value => {
|
|
10
|
+
const auth = JSON.parse(Buffer.from(value.split('.')[1], 'base64url'))['https://api.openai.com/auth'];
|
|
11
|
+
return JSON.stringify([auth?.chatgpt_account_id, auth?.chatgpt_user_id]);
|
|
12
|
+
};
|
|
13
|
+
const initialIdentity = identity(authToken), accountId = JSON.parse(initialIdentity)[0];
|
|
14
|
+
if (!accountId) return null;
|
|
15
|
+
const session = http2.connect('https://chatgpt.com');
|
|
16
|
+
session.on('error', () => {}); // Each stream reports its own request error.
|
|
17
|
+
try {
|
|
18
|
+
const get = route => new Promise((resolve, reject) => {
|
|
19
|
+
const request = session.request({ ':method': 'GET', ':path': '/backend-api' + route,
|
|
20
|
+
authorization: `Bearer ${authToken}`, 'chatgpt-account-id': accountId,
|
|
21
|
+
'user-agent': 'Code-Relax/0.1.0', accept: 'application/json' });
|
|
22
|
+
let body = '', status, size = 0, ended = false;
|
|
23
|
+
const timer = setTimeout(() => request.destroy(new Error('套餐费用查询超时。')), 15000);
|
|
24
|
+
request.setEncoding('utf8');
|
|
25
|
+
request.on('response', headers => { status = headers[':status']; });
|
|
26
|
+
request.on('data', chunk => {
|
|
27
|
+
size += Buffer.byteLength(chunk);
|
|
28
|
+
if (size > 1024 * 1024) request.destroy(new Error('套餐费用响应过大。'));
|
|
29
|
+
else body += chunk;
|
|
30
|
+
});
|
|
31
|
+
request.on('error', reject);
|
|
32
|
+
request.on('close', () => { clearTimeout(timer); if (!ended) reject(new Error('套餐费用连接已断开。')); });
|
|
33
|
+
request.on('end', () => {
|
|
34
|
+
ended = true;
|
|
35
|
+
try {
|
|
36
|
+
if (status !== 200) throw new Error(`套餐费用查询失败(HTTP ${status})。`);
|
|
37
|
+
resolve(JSON.parse(body));
|
|
38
|
+
} catch (error) { reject(error); }
|
|
39
|
+
});
|
|
40
|
+
request.end();
|
|
41
|
+
});
|
|
42
|
+
const value = await pricing.read(accountId, get);
|
|
43
|
+
const current = await token();
|
|
44
|
+
if (!current || identity(current) !== initialIdentity) throw new Error('套餐查询期间账号已变化。');
|
|
45
|
+
return value;
|
|
46
|
+
} finally { session.destroy(); }
|
|
47
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { platformModule } from './select.mjs';
|
|
2
|
-
const adapter = await import(platformModule('codex-runtime'));
|
|
3
|
-
export const { discoverCodex, resolveCodexRuntime, runtimeFilesAvailable, codeModeHostPath, runtimeFingerprint, pruneRetainedDesktopRuntimes } = adapter;
|
|
1
|
+
import { platformModule } from './select.mjs';
|
|
2
|
+
const adapter = await import(platformModule('codex-runtime'));
|
|
3
|
+
export const { discoverCodex, resolveCodexRuntime, runtimeFilesAvailable, codeModeHostPath, runtimeFingerprint, pruneRetainedDesktopRuntimes } = adapter;
|