@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.20
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 +55 -71
- package/dist/bin/codex-remote.mjs +150 -126
- package/dist/bin/manage-relay-admin.mjs +20 -0
- package/dist/bin/migrate-relay-db.mjs +107 -0
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +2 -2
- package/dist/bin/self-relay-server.mjs +18 -10
- package/dist/shared/app-server-events.cjs +42 -15
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/server-requests.cjs +86 -58
- package/dist/skills/code-relax/SKILL.md +7 -6
- package/dist/src/desktop-launcher.mjs +16 -21
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +52 -9
- package/dist/src/managed-app-server.mjs +44 -13
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +9 -11
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/background-process.mjs +1 -1
- package/dist/src/platform/windows/codex-runtime.mjs +251 -0
- package/dist/src/platform/windows/desktop-host.mjs +17 -3
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -0
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-start.js +20 -0
- package/dist/src/platform/windows/resident-startup.ps1 +76 -20
- package/dist/src/platform/windows/service-host.mjs +4 -16
- package/dist/src/platform/windows/start-hidden-console.ps1 +10 -5
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/platform/windows/user-environment.mjs +1 -1
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +79 -0
- package/dist/src/self-relay/admin-server.mjs +66 -0
- package/dist/src/self-relay/admin-state.mjs +82 -19
- package/dist/src/self-relay/admin-style.mjs +9 -0
- package/dist/src/self-relay/admin-usage-page.mjs +28 -0
- package/dist/src/self-relay/connector.mjs +243 -30
- package/dist/src/self-relay/demo.mjs +3 -4
- package/dist/src/self-relay/email-accounts.mjs +141 -0
- package/dist/src/self-relay/lifecycle.mjs +99 -142
- package/dist/src/self-relay/mysql-state.mjs +104 -0
- package/dist/src/self-relay/p2p-probe.mjs +1 -1
- package/dist/src/self-relay/server.mjs +335 -59
- package/dist/src/self-relay/usage-diagnostics.mjs +46 -0
- package/dist/src/self-relay/usage-meter.mjs +125 -0
- package/dist/src/server.mjs +731 -1439
- package/dist/src/service-control.mjs +4 -4
- package/dist/src/service-doctor.mjs +27 -29
- package/dist/src/service-health.mjs +2 -2
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/service-lifecycle.mjs +19 -19
- package/dist/src/shared-app-server.mjs +277 -270
- package/dist/src/shared-catalog.mjs +1 -11
- package/dist/src/shared-recovery.mjs +11 -9
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +17 -18
- package/tools/postinstall.mjs +69 -15
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -358
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -20
- package/dist/src/thread-catalog.mjs +0 -47
- package/dist/tools/find-desktop-pipe.mjs +0 -10
- package/dist/web/activity-view.js +0 -283
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -57
- package/dist/web/chat-transport.js +0 -202
- package/dist/web/chat.css +0 -782
- package/dist/web/chat.js +0 -3830
- package/dist/web/client-platform.js +0 -11
- package/dist/web/community-view.js +0 -20
- package/dist/web/community.css +0 -77
- package/dist/web/community.html +0 -37
- package/dist/web/composer-controller.js +0 -101
- package/dist/web/conversation-controller.js +0 -99
- package/dist/web/disclosure-state-controller.js +0 -95
- package/dist/web/draft-controller.js +0 -103
- package/dist/web/harmony-platform.js +0 -52
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -240
- package/dist/web/history-controller.js +0 -167
- package/dist/web/index.html +0 -325
- package/dist/web/lan-session.js +0 -122
- package/dist/web/link-action-controller.js +0 -212
- package/dist/web/message-send-controller.js +0 -177
- package/dist/web/message-view.js +0 -98
- package/dist/web/p2p-data-channel.js +0 -50
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -123
- package/dist/web/page-resume.js +0 -28
- package/dist/web/pending-message-store.js +0 -108
- package/dist/web/queue-controller.js +0 -82
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -129
- package/dist/web/server-requests.js +0 -67
- package/dist/web/station-connection-controller.js +0 -75
- package/dist/web/task-list-view.js +0 -296
- package/dist/web/thread-attention-controller.js +0 -124
- package/dist/web/thread-context-controller.js +0 -30
- package/dist/web/thread-list-controller.js +0 -61
- package/dist/web/thread-list-sync.js +0 -86
- package/dist/web/thread-title-controller.js +0 -57
- package/dist/web/timeline-formatters.js +0 -249
- package/dist/web/timeline-reducer.js +0 -81
- package/dist/web/timeline-renderer.js +0 -161
- package/dist/web/timeline-scroll-controller.js +0 -50
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/usage-controller.js +0 -258
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
- package/dist/web/vendor/lucide.LICENSE.txt +0 -17
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
export const MAX_FILE_COUNT = 4;
|
|
7
|
+
export const MAX_TOTAL_BYTES = 40 * 1024 * 1024;
|
|
8
|
+
export const FILE_CHUNK_BYTES = 256 * 1024;
|
|
9
|
+
|
|
10
|
+
function validName(value) {
|
|
11
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 180 &&
|
|
12
|
+
value !== '.' && value !== '..' && !/[\\/:*?"<>|\x00-\x1f]/.test(value) && !/[. ]$/.test(value) &&
|
|
13
|
+
!/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function uniqueDestination(directory, name, partial) {
|
|
17
|
+
const dot = name.lastIndexOf('.');
|
|
18
|
+
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
19
|
+
const extension = dot > 0 ? name.slice(dot) : '';
|
|
20
|
+
for (let index = 0; ; index++) {
|
|
21
|
+
const suffix = index ? ` (${index})` : '';
|
|
22
|
+
const candidate = index ? `${stem.slice(0, 180 - extension.length - suffix.length)}${suffix}${extension}` : name;
|
|
23
|
+
if (!candidate || candidate.length > 180) throw new Error('同名文件过多,文件名无法继续编号');
|
|
24
|
+
const destination = path.join(directory, candidate);
|
|
25
|
+
try {
|
|
26
|
+
await fs.link(partial, destination);
|
|
27
|
+
await fs.unlink(partial);
|
|
28
|
+
return { name: candidate, path: destination };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code !== 'EEXIST') throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createFileUploadSession(root) {
|
|
36
|
+
let upload;
|
|
37
|
+
const completed = new Map();
|
|
38
|
+
let completedBytes = 0;
|
|
39
|
+
const directory = path.join(root, 'incoming');
|
|
40
|
+
const clearActive = async () => {
|
|
41
|
+
const current = upload;
|
|
42
|
+
upload = undefined;
|
|
43
|
+
if (!current) return;
|
|
44
|
+
await current.handle.close().catch(() => {});
|
|
45
|
+
await fs.unlink(current.partial).catch(() => {});
|
|
46
|
+
};
|
|
47
|
+
const clear = async () => {
|
|
48
|
+
await clearActive();
|
|
49
|
+
for (const file of completed.values()) await fs.unlink(file.path).catch(() => {});
|
|
50
|
+
completed.clear(); completedBytes = 0;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
async receive(message) {
|
|
54
|
+
if (!message || typeof message !== 'object') throw new Error('无效的文件上传请求');
|
|
55
|
+
if (message.type === 'cancel') { await clear(); return { type: 'cancelled' }; }
|
|
56
|
+
if (message.type === 'commit') {
|
|
57
|
+
if (upload) throw new Error('文件尚未上传完成');
|
|
58
|
+
completed.clear(); completedBytes = 0;
|
|
59
|
+
return { type: 'committed' };
|
|
60
|
+
}
|
|
61
|
+
if (message.type === 'remove') {
|
|
62
|
+
if (upload) throw new Error('请等待当前文件上传完成');
|
|
63
|
+
const file = completed.get(message.id);
|
|
64
|
+
if (!file) throw new Error('文件上传会话已失效');
|
|
65
|
+
await fs.unlink(file.path);
|
|
66
|
+
completed.delete(message.id); completedBytes -= file.size;
|
|
67
|
+
return { type: 'removed', id: message.id };
|
|
68
|
+
}
|
|
69
|
+
if (message.type === 'start') {
|
|
70
|
+
if (upload) throw new Error('已有文件正在上传');
|
|
71
|
+
if (!validName(message.name)) throw new Error('文件名无效');
|
|
72
|
+
if (!Number.isSafeInteger(message.size) || message.size < 1 || message.size > MAX_FILE_BYTES) {
|
|
73
|
+
throw new Error('单个文件不得超过 20 MiB');
|
|
74
|
+
}
|
|
75
|
+
if (completed.size >= MAX_FILE_COUNT || completedBytes + message.size > MAX_TOTAL_BYTES) {
|
|
76
|
+
throw new Error('每条消息最多 4 个文件,合计不超过 40 MiB');
|
|
77
|
+
}
|
|
78
|
+
if (!/^[a-f0-9]{64}$/i.test(message.sha256 || '')) throw new Error('文件校验值无效');
|
|
79
|
+
const id = crypto.randomUUID();
|
|
80
|
+
await fs.mkdir(directory, { recursive: true });
|
|
81
|
+
const partial = path.join(directory, `.upload-${id}`);
|
|
82
|
+
const handle = await fs.open(partial, 'wx');
|
|
83
|
+
upload = { id, partial, handle, name: message.name, size: message.size,
|
|
84
|
+
expected: message.sha256.toLowerCase(), hash: crypto.createHash('sha256'), offset: 0 };
|
|
85
|
+
return { type: 'started', id, offset: 0, chunkBytes: FILE_CHUNK_BYTES };
|
|
86
|
+
}
|
|
87
|
+
const current = upload;
|
|
88
|
+
if (!current || message.id !== current.id) throw new Error('文件上传会话已失效');
|
|
89
|
+
if (message.type === 'chunk') {
|
|
90
|
+
if (!Number.isSafeInteger(message.offset) || message.offset !== current.offset) {
|
|
91
|
+
return { type: 'offset', id: current.id, offset: current.offset };
|
|
92
|
+
}
|
|
93
|
+
if (typeof message.data !== 'string' || message.data.length > Math.ceil(FILE_CHUNK_BYTES / 3) * 4 + 4 ||
|
|
94
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(message.data)) {
|
|
95
|
+
throw new Error('文件分片格式无效');
|
|
96
|
+
}
|
|
97
|
+
const bytes = Buffer.from(message.data, 'base64');
|
|
98
|
+
if (!bytes.length || bytes.length > FILE_CHUNK_BYTES || current.offset + bytes.length > current.size) {
|
|
99
|
+
throw new Error('文件分片大小无效');
|
|
100
|
+
}
|
|
101
|
+
await current.handle.writeFile(bytes);
|
|
102
|
+
current.hash.update(bytes);
|
|
103
|
+
current.offset += bytes.length;
|
|
104
|
+
return { type: 'offset', id: current.id, offset: current.offset };
|
|
105
|
+
}
|
|
106
|
+
if (message.type === 'finish') {
|
|
107
|
+
if (current.offset !== current.size || current.hash.digest('hex') !== current.expected) {
|
|
108
|
+
await clear(); throw new Error('文件大小或 SHA-256 校验失败');
|
|
109
|
+
}
|
|
110
|
+
await current.handle.sync();
|
|
111
|
+
await current.handle.close();
|
|
112
|
+
upload = undefined;
|
|
113
|
+
let saved;
|
|
114
|
+
try { saved = await uniqueDestination(directory, current.name, current.partial); }
|
|
115
|
+
catch (error) { await fs.unlink(current.partial).catch(() => {}); throw error; }
|
|
116
|
+
const result = { type: 'completed', id: current.id, ...saved, size: current.size };
|
|
117
|
+
completed.set(current.id, result); completedBytes += current.size;
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
throw new Error('不支持的文件上传操作');
|
|
121
|
+
},
|
|
122
|
+
close: clear,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
|
+
import { createFileUploadSession, FILE_CHUNK_BYTES, MAX_FILE_BYTES, MAX_FILE_COUNT, MAX_TOTAL_BYTES } from './file-upload-session.mjs';
|
|
4
5
|
|
|
5
6
|
// One authenticated connection carries requests and explicitly owned subscriptions.
|
|
6
7
|
// The dispatcher is shared business routing, not an HTTP loopback proxy.
|
|
7
|
-
export function attachLanSocket(server, { token, dispatch, authorizeDevice = () => false }) {
|
|
8
|
+
export function attachLanSocket(server, { token, dispatch, authorizeDevice = () => false, uploadsRoot }) {
|
|
8
9
|
// Allow four individually valid images to reach the aggregate-size validator,
|
|
9
10
|
// so an oversized composition returns an error instead of dropping the connection.
|
|
10
11
|
const wss = new WebSocketServer({ noServer: true, maxPayload: 48 * 1024 * 1024, perMessageDeflate: false });
|
|
11
|
-
|
|
12
|
+
const uploadWss = new WebSocketServer({ noServer: true, maxPayload: 512 * 1024, perMessageDeflate: false });
|
|
13
|
+
const activeUploads = new Set();
|
|
14
|
+
server.on('upgrade', (request, socket, head) => {
|
|
12
15
|
const origin = request.headers.origin;
|
|
13
|
-
|
|
16
|
+
const upload = request.url === '/api/file-transfer' && Boolean(uploadsRoot);
|
|
17
|
+
let allowed = request.url === '/api/socket' || upload;
|
|
14
18
|
// Packaged ArkWeb pages have an opaque ("null") origin; device authentication remains required.
|
|
15
19
|
if (origin && origin !== 'null') {
|
|
16
20
|
try {
|
|
@@ -23,8 +27,44 @@ export function attachLanSocket(server, { token, dispatch, authorizeDevice = ()
|
|
|
23
27
|
catch { allowed = false; }
|
|
24
28
|
}
|
|
25
29
|
if (!allowed) { socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'); return; }
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const target = upload ? uploadWss : wss;
|
|
31
|
+
target.handleUpgrade(request, socket, head, ws => target.emit('connection', ws));
|
|
32
|
+
});
|
|
33
|
+
uploadWss.on('connection', ws => {
|
|
34
|
+
let authenticated = false, identity = '', upload;
|
|
35
|
+
let pending = Promise.resolve();
|
|
36
|
+
const authTimer = setTimeout(() => ws.close(1008, 'Authentication required'), 5000);
|
|
37
|
+
const reply = value => { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(value)); };
|
|
38
|
+
ws.on('error', () => {});
|
|
39
|
+
ws.on('close', () => {
|
|
40
|
+
clearTimeout(authTimer);
|
|
41
|
+
void pending.finally(async () => {
|
|
42
|
+
await upload?.close();
|
|
43
|
+
if (identity) activeUploads.delete(identity);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
ws.on('message', (bytes, binary) => {
|
|
47
|
+
pending = pending.then(async () => {
|
|
48
|
+
if (binary || bytes.length > 512 * 1024) throw new Error('无效的上传分片');
|
|
49
|
+
const message = JSON.parse(bytes.toString());
|
|
50
|
+
if (!authenticated) {
|
|
51
|
+
if (message?.type === 'device-auth' && typeof message.clientId === 'string' &&
|
|
52
|
+
authorizeDevice(message.clientId, message.credential) === true) identity = `device:${message.clientId}`;
|
|
53
|
+
else if (token && message?.type === 'auth' && typeof message.token === 'string') {
|
|
54
|
+
const supplied = Buffer.from(message.token), expected = Buffer.from(token || '');
|
|
55
|
+
if (supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected)) identity = 'local-token';
|
|
56
|
+
}
|
|
57
|
+
if (!identity || activeUploads.has(identity) || activeUploads.size >= 2) throw new Error('上传身份无效或工作站已有上传任务');
|
|
58
|
+
activeUploads.add(identity); authenticated = true; clearTimeout(authTimer);
|
|
59
|
+
upload = createFileUploadSession(uploadsRoot);
|
|
60
|
+
reply({ type: 'ready', maxFileBytes: MAX_FILE_BYTES, maxFileCount: MAX_FILE_COUNT,
|
|
61
|
+
maxTotalBytes: MAX_TOTAL_BYTES, chunkBytes: FILE_CHUNK_BYTES });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
reply(await upload.receive(message));
|
|
65
|
+
}).catch(error => { reply({ type: 'error', error: error.message }); ws.close(1008, 'Upload rejected'); });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
28
68
|
wss.on('connection', ws => {
|
|
29
69
|
let authenticated = false, alive = true;
|
|
30
70
|
const active = new Map();
|
|
@@ -70,7 +110,7 @@ export function attachLanSocket(server, { token, dispatch, authorizeDevice = ()
|
|
|
70
110
|
if (!['request', 'subscribe'].includes(type) || active.has(id)) { ws.close(1008, 'Invalid request'); return; }
|
|
71
111
|
const subscription = type === 'subscribe';
|
|
72
112
|
const path = message.path;
|
|
73
|
-
const isStream = typeof path === 'string' && /^\/api\/(?:usage-events|shared-recovery\/events|threads\/[^/?]+\/events)(?:\?|$)/.test(path);
|
|
113
|
+
const isStream = typeof path === 'string' && /^\/api\/(?:usage-events|turn-events|shared-recovery\/events|threads\/[^/?]+\/events)(?:\?|$)/.test(path);
|
|
74
114
|
if (active.size >= 32 || (subscription && [...active.values()].filter(r => r.subscription).length >= 4)) {
|
|
75
115
|
send({ type: 'response', id, status: 429, body: { error: '同时进行的请求过多' } }); return;
|
|
76
116
|
}
|
|
@@ -105,5 +145,8 @@ export function attachLanSocket(server, { token, dispatch, authorizeDevice = ()
|
|
|
105
145
|
catch (error) { reply.json(500, { error: error.message }); }
|
|
106
146
|
});
|
|
107
147
|
});
|
|
108
|
-
return { close() {
|
|
109
|
-
|
|
148
|
+
return { close() {
|
|
149
|
+
for (const ws of wss.clients) ws.terminate(); wss.close();
|
|
150
|
+
for (const ws of uploadWss.clients) ws.terminate(); uploadWss.close();
|
|
151
|
+
} };
|
|
152
|
+
}
|
|
@@ -5,9 +5,11 @@ import { EventEmitter } from 'node:events';
|
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
6
|
import { readRecord, writeRecord } from './service-lifecycle.mjs';
|
|
7
7
|
import { SharedAppServer } from './shared-app-server.mjs';
|
|
8
|
+
import { pruneRetainedDesktopRuntimes, resolveCodexRuntime, runtimeFilesAvailable } from './platform/windows/codex-runtime.mjs';
|
|
8
9
|
|
|
9
10
|
export const managedAppServerUrl = 'ws://127.0.0.1:45839';
|
|
10
11
|
export const managedAppServerReadyUrl = 'http://127.0.0.1:45839/readyz';
|
|
12
|
+
export const managedAppServerLauncherRevision = 2;
|
|
11
13
|
const retryDelays = [1_000, 2_000, 5_000, 10_000, 20_000];
|
|
12
14
|
|
|
13
15
|
export function managedPortAvailable() {
|
|
@@ -30,7 +32,6 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
30
32
|
this.timeout = timeout;
|
|
31
33
|
this.now = now;
|
|
32
34
|
this.recordPath = path.join(stateRoot, 'managed-app-server.json');
|
|
33
|
-
this.legacyRecordPath = path.join(stateRoot, 'shared-desktop.json');
|
|
34
35
|
this.client = client;
|
|
35
36
|
this.state = { state: 'absent', ready: false, error: null };
|
|
36
37
|
this.restartStatus = null;
|
|
@@ -43,7 +44,11 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
snapshot() {
|
|
47
|
+
snapshot() {
|
|
48
|
+
return { ...this.state,
|
|
49
|
+
version: this.state.ready && this.client.ready ? this.client.serverVersion || null : null,
|
|
50
|
+
...(this.restartStatus ? { restart: { ...this.restartStatus } } : {}) };
|
|
51
|
+
}
|
|
47
52
|
|
|
48
53
|
async start() {
|
|
49
54
|
this.stopped = false;
|
|
@@ -65,36 +70,54 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
65
70
|
if (!this.state.ready) this.publish({ state: 'starting', ready: false, error: null });
|
|
66
71
|
try {
|
|
67
72
|
fs.mkdirSync(this.stateRoot, { recursive: true });
|
|
68
|
-
const info = await this.host.inspect();
|
|
69
|
-
if (!info.cli) throw new Error('未找到 Desktop 管理的 codex.exe,请先安装并登录 Codex Desktop。');
|
|
70
73
|
let record = null, actual = null;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
const candidate = readRecord(this.recordPath);
|
|
75
|
+
const process = candidate?.pid ? await this.host.process(candidate.pid) : null;
|
|
76
|
+
if (sameManagedProcess(candidate, process)) { record = candidate; actual = process; }
|
|
77
|
+
const startedHere = !actual;
|
|
78
|
+
const savedRuntime = actual ? readRecord(path.join(this.stateRoot, 'codex-runtime.json')) : null;
|
|
79
|
+
const runtime = actual && savedRuntime?.executable && runtimeFilesAvailable(savedRuntime.executable)
|
|
80
|
+
? savedRuntime
|
|
81
|
+
: await resolveCodexRuntime(this.stateRoot, { refresh: !actual, environment: this.environment });
|
|
82
|
+
if (actual && !runtimeFilesAvailable(actual.executable)) {
|
|
83
|
+
throw new Error(`运行中的 Codex 内核目录已被移除或缺少 codex-code-mode-host.exe:${actual.executable};已找到可用内核 ${runtime.executable},请确认重启内核。`);
|
|
74
84
|
}
|
|
75
85
|
if (!actual) {
|
|
76
86
|
if (!await this.available()) throw new Error('45839 被未识别进程占用,未启动或结束任何进程。');
|
|
77
87
|
const childEnvironment = { ...this.environment };
|
|
88
|
+
childEnvironment.CODEX_HOME = runtime.codexHome;
|
|
78
89
|
for (const key of ['CODEX_APP_SERVER_WS_URL', 'CODEX_APP_SERVER_FORCE_CLI', 'CODEX_APP_TOOLS_PIPE_PATH']) delete childEnvironment[key];
|
|
79
|
-
const
|
|
90
|
+
for (const key of Object.keys(childEnvironment)) {
|
|
91
|
+
if (/^npm_/iu.test(key) || key.toUpperCase() === 'INIT_CWD') delete childEnvironment[key];
|
|
92
|
+
}
|
|
93
|
+
const pid = await this.host.start(runtime.executable, ['app-server', '--listen', managedAppServerUrl], childEnvironment,
|
|
80
94
|
[path.join(this.stateRoot, 'managed-app-server.stdout.log'), path.join(this.stateRoot, 'managed-app-server.stderr.log')]);
|
|
81
95
|
const started = await this.host.process(pid);
|
|
82
96
|
if (!sameManagedProcess(started, started)) throw new Error('Codex 内核启动后进程身份无法确认。');
|
|
83
|
-
|
|
97
|
+
actual = started;
|
|
98
|
+
record = { ...started, launcherRevision: managedAppServerLauncherRevision };
|
|
84
99
|
writeRecord(this.recordPath, record);
|
|
85
100
|
} else if (!readRecord(this.recordPath)) {
|
|
86
|
-
|
|
101
|
+
record = { ...actual, launcherRevision: Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0 };
|
|
102
|
+
writeRecord(this.recordPath, record);
|
|
87
103
|
}
|
|
88
104
|
await this.waitUntilReady(actual);
|
|
89
105
|
await this.client.connect();
|
|
90
106
|
if (!this.client.ready) throw new Error('Codex 内核协议初始化未完成。');
|
|
91
|
-
|
|
107
|
+
if (startedHere) {
|
|
108
|
+
try { pruneRetainedDesktopRuntimes(this.stateRoot, actual.executable); }
|
|
109
|
+
catch (error) { console.error('旧版 Codex 内核文件清理失败:', error); }
|
|
110
|
+
}
|
|
111
|
+
const preferredExecutable = path.resolve(runtime.executable);
|
|
92
112
|
const updatePending = path.resolve(actual.executable).toLowerCase() !== preferredExecutable.toLowerCase();
|
|
113
|
+
const launcherRevision = Number.isSafeInteger(record?.launcherRevision) ? record.launcherRevision : 0;
|
|
93
114
|
this.retryAttempt = 0;
|
|
94
115
|
this.retryAfter = 0;
|
|
95
116
|
this.publish({ state: 'ready', ready: true, error: null, pid: actual.pid,
|
|
96
117
|
created: actual.created, executable: actual.executable, preferredExecutable,
|
|
97
|
-
updatePending,
|
|
118
|
+
updatePending, launcherRevision, selectedVersion: runtime.version, source: runtime.source,
|
|
119
|
+
launcherRestartRequired: launcherRevision !== managedAppServerLauncherRevision,
|
|
120
|
+
url: managedAppServerUrl });
|
|
98
121
|
return true;
|
|
99
122
|
} catch (error) {
|
|
100
123
|
this.failed(error instanceof Error ? error.message : String(error));
|
|
@@ -133,12 +156,16 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
133
156
|
const previous = this.state;
|
|
134
157
|
this.publish({ ...previous, state: 'restarting', ready: false, error: null });
|
|
135
158
|
try {
|
|
136
|
-
const record = readRecord(this.recordPath)
|
|
159
|
+
const record = readRecord(this.recordPath);
|
|
137
160
|
const actual = record?.pid ? await this.host.process(record.pid) : null;
|
|
138
161
|
if (actual && !sameManagedProcess(record, actual)) {
|
|
139
162
|
throw new Error('Codex 内核进程身份已经变化,未结束任何进程。');
|
|
140
163
|
}
|
|
164
|
+
if (!actual && !await this.available()) throw new Error('45839 被未识别进程占用,未结束任何进程。');
|
|
141
165
|
this.restartStage('stopping');
|
|
166
|
+
// Resolve the replacement before stopping the current working process.
|
|
167
|
+
const runtime = await resolveCodexRuntime(this.stateRoot, { refresh: true, environment: this.environment });
|
|
168
|
+
const desktop = await this.host.prepareKernelRestart?.();
|
|
142
169
|
this.client.close();
|
|
143
170
|
if (actual) {
|
|
144
171
|
await this.host.stop(actual.pid);
|
|
@@ -152,6 +179,10 @@ export class ManagedAppServer extends EventEmitter {
|
|
|
152
179
|
this.restartStage('starting');
|
|
153
180
|
const ready = await this.ensureNow();
|
|
154
181
|
if (!ready) throw new Error(this.state.error || 'Codex 内核未能重新启动。');
|
|
182
|
+
const desktopEnvironment = { ...this.environment, CODEX_HOME: runtime.codexHome, CODEX_APP_SERVER_WS_URL: managedAppServerUrl };
|
|
183
|
+
delete desktopEnvironment.CODEX_APP_SERVER_FORCE_CLI;
|
|
184
|
+
delete desktopEnvironment.CODEX_APP_TOOLS_PIPE_PATH;
|
|
185
|
+
await this.host.resumeAfterKernelRestart?.(desktop, desktopEnvironment);
|
|
155
186
|
this.restartStatus = { ...this.restartStatus, status: 'completed', stage: 'ready',
|
|
156
187
|
completedAt: new Date(this.now()).toISOString() };
|
|
157
188
|
this.emit('state', this.snapshot());
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
import sharp from 'sharp';
|
|
5
|
-
|
|
6
1
|
export function validateInlineImage(value) {
|
|
7
2
|
const data = typeof value === 'string' ? value.slice(value.indexOf(',') + 1) : '';
|
|
8
3
|
if (typeof value !== 'string' || value.length > Math.ceil(8 * 1024 * 1024 / 3) * 4 + 64 ||
|
|
@@ -12,16 +7,3 @@ export function validateInlineImage(value) {
|
|
|
12
7
|
}
|
|
13
8
|
return value;
|
|
14
9
|
}
|
|
15
|
-
|
|
16
|
-
// Only the Desktop tool compatibility path needs a file visible to the host.
|
|
17
|
-
export async function persistLegacyImage(value, directory) {
|
|
18
|
-
validateInlineImage(value);
|
|
19
|
-
const input = Buffer.from(value.slice(value.indexOf(',') + 1), 'base64');
|
|
20
|
-
const buffer = await sharp(input, { limitInputPixels: 40_000_000 }).rotate()
|
|
21
|
-
.resize({ width: 2560, height: 2560, fit: 'inside', withoutEnlargement: true }).webp({ quality: 88 }).toBuffer();
|
|
22
|
-
const id = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
23
|
-
await fs.mkdir(directory, { recursive: true });
|
|
24
|
-
const file = path.join(directory, `${id}.webp`);
|
|
25
|
-
await fs.writeFile(file, buffer);
|
|
26
|
-
return file;
|
|
27
|
-
}
|
package/dist/src/onboarding.mjs
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
export const connectionModesText = [
|
|
2
2
|
'可选连接模式:',
|
|
3
3
|
'• 局域网:启动时会把当前 Wi-Fi/有线网络候选临时发给已配对设备;App 验证后保存,也可手动输入 IP。',
|
|
4
|
-
'•
|
|
5
|
-
'• OpenAI
|
|
4
|
+
'• Code Relax 中继:同一设备授权可跨网络长期重连;Codex 内核就绪后才能进入聊天。',
|
|
5
|
+
'• OpenAI 中继:在已配对电脑内使用官方账号和设备授权;认证仍直连 OpenAI。',
|
|
6
6
|
].join('\n');
|
|
7
7
|
|
|
8
8
|
export function phonePairingSteps(transportMode, paired = false) {
|
|
9
|
-
const mode = transportMode === 'lan-shared' ? '局域网 ·
|
|
9
|
+
const mode = transportMode === 'lan-shared' ? '局域网 · Codex 内核已就绪' : '局域网(Codex 内核未就绪)';
|
|
10
10
|
return paired ? [
|
|
11
|
-
|
|
11
|
+
`已有移动设备授权。移动端可继续使用 Code Relax 中继;${mode} 会显示验证成功的当前网络和历史记录。`,
|
|
12
12
|
'添加另一台设备时执行 relax pair;每台设备只需扫码配对一次。',
|
|
13
13
|
] : [
|
|
14
|
-
'
|
|
14
|
+
'请先在移动端安装 Code Relax。当前为鸿蒙 HarmonyOS 7 私测版,调试 HAP 需通过 DevEco/调试设备安装,尚无商店下载入口。',
|
|
15
15
|
'使用 App 扫描配对二维码;每台移动设备只需配对一次。',
|
|
16
|
-
|
|
16
|
+
`配对后可直接使用 Code Relax 中继;${mode} 会接收并验证当前网络候选,也可手动输入 IP。`,
|
|
17
17
|
];
|
|
18
18
|
}
|
|
19
19
|
export const desktopSetupSteps = [
|
|
20
20
|
'Code Relax Skill 已随 npm 包自动安装。',
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'基础控制首次聊天需要在 Codex 对话启动一次 Code Relax 绑定上下文;完整控制不需要。',
|
|
21
|
+
'安装完成后桌面服务和 Codex 内核已启动,并默认在当前用户登录时启动。',
|
|
22
|
+
'CLI 用户无需安装 Desktop;已安装 Desktop 时,安装器负责将运行中的 Desktop 接入同一内核。',
|
|
24
23
|
'尚无设备授权时执行 relax pair,打开并扫描配对二维码。',
|
|
25
|
-
'若工作站提示 Desktop 尚未接入受管服务,可在手机确认重新打开 Desktop,无需电脑脚本或快捷方式。',
|
|
26
24
|
'统一配对先建立电脑记录;进入该电脑后可另行完成 OpenAI 官方 Remote 配对。',
|
|
27
25
|
];
|
|
28
26
|
|
|
29
27
|
export function pairingDisplayText(nextSteps, notice) {
|
|
30
|
-
return ['
|
|
28
|
+
return ['桌面服务已就绪,请在移动端完成连接。', connectionModesText, ...nextSteps.map((step, index) => `${index + 1}. ${step}`), notice].join('\n\n');
|
|
31
29
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
当前只实现 Windows Codex:
|
|
4
4
|
|
|
5
5
|
- `windows/runtime-paths.mjs`:固定服务、用户状态、Codex 数据、附件和长期凭据路径。
|
|
6
|
-
- `windows/service-host.mjs
|
|
6
|
+
- `windows/service-host.mjs`:网卡、Bridge 进程环境和旧状态迁移。
|
|
7
7
|
- `windows/desktop-host.mjs`、PowerShell 脚本:普通/共享 Desktop、快捷方式和隐藏控制台启动。
|
|
8
8
|
- `windows/credential-store.mjs/.ps1`:私有中继电脑凭据的 DPAPI 与 ACL。
|
|
9
9
|
|
|
@@ -25,6 +25,6 @@ export function startWindowsProcess(executable, args, environment, logs = []) {
|
|
|
25
25
|
if (!Number.isSafeInteger(pid) || pid <= 0) return reject(new Error('后台启动未返回有效 PID'));
|
|
26
26
|
resolve(pid);
|
|
27
27
|
});
|
|
28
|
-
helper.stdin.end(JSON.stringify({ executable, args, logs, environment }));
|
|
28
|
+
helper.stdin.end(JSON.stringify({ brokerExecutable: process.execPath, executable, args, logs, environment }));
|
|
29
29
|
});
|
|
30
30
|
}
|