@walkhi/code-relax 0.1.0-beta.27 → 0.1.0-beta.29
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 +80 -67
- package/dist/bin/codex-remote.mjs +303 -239
- package/dist/bin/self-relay-server.mjs +2 -0
- package/dist/shared/app-server-events.cjs +102 -16
- package/dist/shared/code-browser.cjs +159 -0
- package/dist/shared/file-changes.cjs +31 -0
- package/dist/shared/file-citations.cjs +117 -82
- package/dist/shared/file-mentions.cjs +80 -35
- package/dist/shared/message-delivery.cjs +38 -0
- package/dist/shared/p2p-file-channel.cjs +74 -0
- package/dist/shared/server-requests.cjs +149 -41
- package/dist/shared/service-endpoints.cjs +11 -11
- package/dist/shared/timeline-sync.cjs +173 -156
- package/dist/skills/code-relax/SKILL.md +2 -1
- package/dist/src/bounded-process-log.mjs +63 -0
- package/dist/src/celsius-client.mjs +107 -0
- package/dist/src/code-browser.mjs +101 -0
- package/dist/src/desktop-launcher.mjs +30 -30
- package/dist/src/dots-approval-timeline.mjs +99 -0
- package/dist/src/dots-avatar.mjs +42 -0
- package/dist/src/dots-chat-projection.mjs +50 -0
- package/dist/src/dots-chat-server.mjs +412 -0
- package/dist/src/dots-message-images.mjs +319 -0
- package/dist/src/dots-profile.mjs +77 -0
- package/dist/src/dots-room-activity.mjs +94 -0
- package/dist/src/dots-room-client.mjs +135 -0
- package/dist/src/dots-room-projection.mjs +94 -0
- package/dist/src/dots-schedules.mjs +66 -0
- package/dist/src/dots-summary-tasks.mjs +32 -0
- package/dist/src/dots-task-catalog.mjs +411 -0
- package/dist/src/file-transfer-range.mjs +22 -0
- package/dist/src/file-upload-session.mjs +130 -124
- package/dist/src/hosted-history-app-server.mjs +103 -0
- package/dist/src/hosted-history-cache.mjs +113 -0
- package/dist/src/lan-socket-server.mjs +205 -160
- package/dist/src/managed-app-server.mjs +241 -238
- package/dist/src/message-upload-files.mjs +74 -0
- package/dist/src/platform/windows/activate-desktop.ps1 +48 -0
- package/dist/src/platform/windows/codex-runtime.mjs +253 -251
- package/dist/src/platform/windows/desktop-host.mjs +38 -32
- package/dist/src/platform/windows/desktop-info.ps1 +2 -2
- package/dist/src/platform/windows/desktop-project-registration.mjs +153 -0
- package/dist/src/platform/windows/dots-profile.mjs +49 -0
- package/dist/src/platform/windows/launch-worker.mjs +41 -42
- package/dist/src/platform/windows/project-locations.mjs +16 -0
- package/dist/src/project-create.mjs +119 -0
- package/dist/src/project-management.mjs +69 -0
- package/dist/src/self-relay/account-connections.mjs +52 -0
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-style.mjs +14 -9
- package/dist/src/self-relay/connector.mjs +23 -5
- package/dist/src/self-relay/email-accounts.mjs +35 -10
- package/dist/src/self-relay/file-wire.mjs +13 -0
- package/dist/src/self-relay/lifecycle.mjs +22 -4
- package/dist/src/self-relay/long-pairing.mjs +40 -0
- package/dist/src/self-relay/p2p-probe.mjs +152 -123
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -0
- package/dist/src/self-relay/server.mjs +922 -849
- package/dist/src/server.mjs +474 -199
- package/dist/src/service-control.mjs +13 -3
- package/dist/src/service-doctor.mjs +72 -61
- package/dist/src/service-lifecycle.mjs +51 -43
- package/dist/src/shared-app-server.mjs +166 -107
- package/dist/src/shared-catalog.mjs +132 -40
- package/dist/src/shared-thread-stream.mjs +103 -25
- package/dist/src/usage-reset.mjs +17 -0
- package/dist/src/workstation-downloads.mjs +120 -0
- package/package.json +1 -1
|
@@ -1,92 +1,124 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
|
-
import timelineSync from '../shared/timeline-sync.cjs';
|
|
5
|
-
import { createFileUploadSession, FILE_CHUNK_BYTES, MAX_FILE_BYTES, MAX_FILE_COUNT, MAX_TOTAL_BYTES } from './file-upload-session.mjs';
|
|
3
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
|
+
import timelineSync from '../shared/timeline-sync.cjs';
|
|
5
|
+
import { createFileUploadSession, FILE_CHUNK_BYTES, MAX_FILE_BYTES, MAX_FILE_COUNT, MAX_TOTAL_BYTES } from './file-upload-session.mjs';
|
|
6
|
+
import { FILE_READ_LIMIT } from './file-transfer-range.mjs';
|
|
6
7
|
|
|
7
8
|
// One authenticated connection carries requests and explicitly owned subscriptions.
|
|
8
9
|
// The dispatcher is shared business routing, not an HTTP loopback proxy.
|
|
9
|
-
export function attachLanSocket(server, { token, dispatch, authorizeDevice = () => false, onDeviceAuthorizationsChanged, uploadsRoot }) {
|
|
10
|
-
// Allow four individually valid images to reach the aggregate-size validator,
|
|
11
|
-
// so an oversized composition returns an error instead of dropping the connection.
|
|
12
|
-
const wss = new WebSocketServer({ noServer: true, maxPayload: 48 * 1024 * 1024, perMessageDeflate: false });
|
|
13
|
-
const uploadWss = new WebSocketServer({ noServer: true, maxPayload: 512 * 1024, perMessageDeflate: false });
|
|
14
|
-
const activeUploads = new
|
|
15
|
-
const deviceConnections = new Map();
|
|
16
|
-
const stopWatching = onDeviceAuthorizationsChanged?.(() => {
|
|
17
|
-
for (const connection of deviceConnections.values()) {
|
|
18
|
-
if (!authorizeDevice(connection.clientId, connection.credential)) connection.revoke();
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
server.on('upgrade', (request, socket, head) => {
|
|
10
|
+
export function attachLanSocket(server, { token, dispatch, authorizeDevice = () => false, onDeviceAuthorizationsChanged, uploadsRoot }) {
|
|
11
|
+
// Allow four individually valid images to reach the aggregate-size validator,
|
|
12
|
+
// so an oversized composition returns an error instead of dropping the connection.
|
|
13
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 48 * 1024 * 1024, perMessageDeflate: false });
|
|
14
|
+
const uploadWss = new WebSocketServer({ noServer: true, maxPayload: 512 * 1024, perMessageDeflate: false });
|
|
15
|
+
const activeUploads = new Map();
|
|
16
|
+
const deviceConnections = new Map();
|
|
17
|
+
const stopWatching = onDeviceAuthorizationsChanged?.(() => {
|
|
18
|
+
for (const connection of deviceConnections.values()) {
|
|
19
|
+
if (!authorizeDevice(connection.clientId, connection.credential)) connection.revoke();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
server.on('upgrade', (request, socket, head) => {
|
|
22
23
|
const origin = request.headers.origin;
|
|
23
|
-
const upload = request.url === '/api/file-transfer' && Boolean(uploadsRoot);
|
|
24
|
-
let allowed = request.url === '/api/socket' || upload;
|
|
25
|
-
// Packaged ArkWeb pages have an opaque ("null") origin; device authentication remains required.
|
|
26
|
-
if (origin && origin !== 'null') {
|
|
27
|
-
try {
|
|
28
|
-
const originUrl = new URL(origin);
|
|
29
|
-
const requestUrl = new URL(`http://${request.headers.host}`);
|
|
30
|
-
const nativeClient = request.headers['x-code-relax-native'] === '1';
|
|
31
|
-
allowed &&= ['http:', 'https:'].includes(originUrl.protocol) &&
|
|
32
|
-
(originUrl.host === request.headers.host || (nativeClient && originUrl.hostname === requestUrl.hostname));
|
|
33
|
-
}
|
|
34
|
-
catch { allowed = false; }
|
|
24
|
+
const upload = request.url === '/api/file-transfer' && Boolean(uploadsRoot);
|
|
25
|
+
let allowed = request.url === '/api/socket' || upload;
|
|
26
|
+
// Packaged ArkWeb pages have an opaque ("null") origin; device authentication remains required.
|
|
27
|
+
if (origin && origin !== 'null') {
|
|
28
|
+
try {
|
|
29
|
+
const originUrl = new URL(origin);
|
|
30
|
+
const requestUrl = new URL(`http://${request.headers.host}`);
|
|
31
|
+
const nativeClient = request.headers['x-code-relax-native'] === '1';
|
|
32
|
+
allowed &&= ['http:', 'https:'].includes(originUrl.protocol) &&
|
|
33
|
+
(originUrl.host === request.headers.host || (nativeClient && originUrl.hostname === requestUrl.hostname));
|
|
34
|
+
}
|
|
35
|
+
catch { allowed = false; }
|
|
35
36
|
}
|
|
36
37
|
if (!allowed) { socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n'); return; }
|
|
37
|
-
const target = upload ? uploadWss : wss;
|
|
38
|
-
target.handleUpgrade(request, socket, head, ws => target.emit('connection', ws));
|
|
39
|
-
});
|
|
40
|
-
uploadWss.on('connection', ws => {
|
|
41
|
-
let authenticated = false, stopped = false, identity = '', upload;
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
ws.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
stopped
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
ws.on('
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
38
|
+
const target = upload ? uploadWss : wss;
|
|
39
|
+
target.handleUpgrade(request, socket, head, ws => target.emit('connection', ws));
|
|
40
|
+
});
|
|
41
|
+
uploadWss.on('connection', ws => {
|
|
42
|
+
let authenticated = false, stopped = false, identity = '', upload, purpose, chunk;
|
|
43
|
+
let queued = 0;
|
|
44
|
+
let pending = Promise.resolve();
|
|
45
|
+
const authTimer = setTimeout(() => ws.close(1008, 'Authentication required'), 5000);
|
|
46
|
+
const reply = value => { if (!stopped && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(value)); };
|
|
47
|
+
ws.on('error', () => {});
|
|
48
|
+
const stop = () => {
|
|
49
|
+
if (stopped) return;
|
|
50
|
+
stopped = true;
|
|
51
|
+
deviceConnections.delete(ws);
|
|
52
|
+
clearTimeout(authTimer);
|
|
53
|
+
void pending.finally(async () => {
|
|
54
|
+
await upload?.close();
|
|
55
|
+
activeUploads.delete(ws);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
ws.on('close', stop);
|
|
59
|
+
ws.on('message', (bytes, binary) => {
|
|
60
|
+
if (stopped || ws.readyState !== WebSocket.OPEN) return;
|
|
61
|
+
if (++queued > 4) { stop(); ws.close(1013, 'File request queue exceeded'); return; }
|
|
62
|
+
pending = pending.then(async () => {
|
|
63
|
+
if (stopped || ws.readyState !== WebSocket.OPEN) return;
|
|
64
|
+
if (binary) {
|
|
65
|
+
if (!authenticated || purpose !== 'upload' || !chunk || bytes.length !== chunk.length) throw new Error('无效的文件分片');
|
|
66
|
+
const current = chunk; chunk = undefined;
|
|
67
|
+
reply(await upload.receive({ ...current, data: bytes })); return;
|
|
68
|
+
}
|
|
69
|
+
if (bytes.length > 16 * 1024 || chunk) throw new Error('无效的文件请求');
|
|
70
|
+
const message = JSON.parse(bytes.toString());
|
|
71
|
+
if (!authenticated) {
|
|
72
|
+
purpose = message.purpose;
|
|
73
|
+
if (!['upload', 'download'].includes(purpose)) throw new Error('文件通道用途无效');
|
|
74
|
+
if (message?.type === 'device-auth' && typeof message.clientId === 'string' &&
|
|
75
|
+
authorizeDevice(message.clientId, message.credential) === true) identity = `device:${message.clientId}`;
|
|
76
|
+
else if (token && message?.type === 'auth' && typeof message.token === 'string') {
|
|
77
|
+
const supplied = Buffer.from(message.token), expected = Buffer.from(token || '');
|
|
78
|
+
if (supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected)) identity = 'local-token';
|
|
79
|
+
}
|
|
80
|
+
const same = [...activeUploads.values()].filter(item => item.identity === identity && item.purpose === purpose);
|
|
81
|
+
const uploads = [...activeUploads.values()].filter(item => item.purpose === 'upload').length;
|
|
82
|
+
if (!identity || same.length >= (purpose === 'upload' ? 1 : 3) || activeUploads.size >= 8 ||
|
|
83
|
+
(purpose === 'upload' && uploads >= 2)) throw new Error('文件身份无效或文件任务过多');
|
|
84
|
+
if (message.type === 'device-auth') deviceConnections.set(ws, {
|
|
85
|
+
clientId: message.clientId, credential: message.credential,
|
|
86
|
+
revoke() { stop(); ws.close(4401, 'Device authorization revoked'); },
|
|
87
|
+
});
|
|
88
|
+
activeUploads.set(ws, { identity, purpose }); authenticated = true; clearTimeout(authTimer);
|
|
89
|
+
if (purpose === 'upload') upload = createFileUploadSession(uploadsRoot);
|
|
90
|
+
reply({ type: 'ready', version: 2, purpose, maxDownloadBytes: FILE_READ_LIMIT, maxFileBytes: MAX_FILE_BYTES, maxFileCount: MAX_FILE_COUNT,
|
|
91
|
+
maxTotalBytes: MAX_TOTAL_BYTES, chunkBytes: FILE_CHUNK_BYTES });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (purpose === 'download') {
|
|
95
|
+
if (message.type !== 'read' || typeof message.path !== 'string' || message.path.length > 12000 ||
|
|
96
|
+
!/^\/api\/(?:images\/[a-f0-9]{64}|files\/[a-f0-9]{64}|code\/(?:image|binary))(?:\?|$)/.test(message.path) || message.path.includes('#')) throw new Error('无效的文件读取请求');
|
|
97
|
+
await dispatch({ method: 'GET', url: message.path, headers: {}, socketRequest: true, fileTransfer: true }, {
|
|
98
|
+
json(status, body) { reply({ type: 'error', status, error: body?.error || '文件读取失败' }); },
|
|
99
|
+
filePart(metadata, data) {
|
|
100
|
+
if (stopped || ws.readyState !== WebSocket.OPEN) return;
|
|
101
|
+
if (!Buffer.isBuffer(data) || data.length > FILE_CHUNK_BYTES || ws.bufferedAmount > 1024 * 1024) throw new Error('文件接收过慢或分片无效');
|
|
102
|
+
reply({ type: 'part', ...metadata, length: data.length });
|
|
103
|
+
ws.send(data);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (message.type === 'chunk') {
|
|
109
|
+
if (!Number.isSafeInteger(message.length) || message.length < 1 || message.length > FILE_CHUNK_BYTES) throw new Error('文件分片大小无效');
|
|
110
|
+
chunk = message; return;
|
|
111
|
+
}
|
|
112
|
+
reply(await upload.receive(message));
|
|
113
|
+
}).catch(error => { reply({ type: 'error', error: error.message }); stop(); ws.close(1008, 'File request rejected'); })
|
|
114
|
+
.finally(() => { queued--; });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
85
117
|
wss.on('connection', ws => {
|
|
86
|
-
let authenticated = false, alive = true, stopped = false;
|
|
118
|
+
let authenticated = false, alive = true, stopped = false;
|
|
87
119
|
const active = new Map();
|
|
88
120
|
const send = value => {
|
|
89
|
-
if (stopped || ws.readyState !== WebSocket.OPEN) return;
|
|
121
|
+
if (stopped || ws.readyState !== WebSocket.OPEN) return;
|
|
90
122
|
if (ws.bufferedAmount > 64 * 1024 * 1024) { ws.close(1013, 'Client too slow'); return; }
|
|
91
123
|
ws.send(JSON.stringify(value));
|
|
92
124
|
};
|
|
@@ -97,49 +129,51 @@ export function attachLanSocket(server, { token, dispatch, authorizeDevice = ()
|
|
|
97
129
|
}, 30000);
|
|
98
130
|
ws.on('pong', () => { alive = true; });
|
|
99
131
|
ws.on('error', () => {}); // close rejects requests and releases every subscription.
|
|
100
|
-
const stop = () => {
|
|
101
|
-
if (stopped) return;
|
|
102
|
-
stopped = true;
|
|
103
|
-
deviceConnections.delete(ws);
|
|
104
|
-
clearTimeout(authTimer); clearInterval(heartbeat);
|
|
132
|
+
const stop = () => {
|
|
133
|
+
if (stopped) return;
|
|
134
|
+
stopped = true;
|
|
135
|
+
deviceConnections.delete(ws);
|
|
136
|
+
clearTimeout(authTimer); clearInterval(heartbeat);
|
|
105
137
|
for (const reply of active.values()) reply.end();
|
|
106
138
|
active.clear();
|
|
107
|
-
};
|
|
108
|
-
ws.on('close', stop);
|
|
109
|
-
ws.on('message', async (bytes, binary) => {
|
|
110
|
-
if (stopped || ws.readyState !== WebSocket.OPEN) return;
|
|
139
|
+
};
|
|
140
|
+
ws.on('close', stop);
|
|
141
|
+
ws.on('message', async (bytes, binary) => {
|
|
142
|
+
if (stopped || ws.readyState !== WebSocket.OPEN) return;
|
|
111
143
|
let message;
|
|
112
144
|
try { if (binary) throw new Error(); message = JSON.parse(bytes.toString()); }
|
|
113
145
|
catch { ws.close(1008, 'Invalid JSON'); return; }
|
|
114
146
|
if (!message || typeof message !== 'object') { ws.close(1008, 'Invalid message'); return; }
|
|
115
|
-
if (!authenticated) {
|
|
116
|
-
let accepted = false;
|
|
117
|
-
if (message.type === 'device-auth') {
|
|
118
|
-
accepted = authorizeDevice(message.clientId, message.credential) === true;
|
|
119
|
-
} else if (message.type === 'auth') {
|
|
120
|
-
const supplied = Buffer.from(typeof message.token === 'string' ? message.token : '');
|
|
121
|
-
const expected = Buffer.from(token || '');
|
|
122
|
-
accepted = supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected);
|
|
123
|
-
}
|
|
124
|
-
if (!accepted) {
|
|
125
|
-
ws.close(1008, 'Authentication failed'); return;
|
|
126
|
-
}
|
|
127
|
-
if (message.type === 'device-auth') deviceConnections.set(ws, {
|
|
128
|
-
clientId: message.clientId, credential: message.credential,
|
|
129
|
-
revoke() { stop(); ws.close(4401, 'Device authorization revoked'); },
|
|
130
|
-
});
|
|
147
|
+
if (!authenticated) {
|
|
148
|
+
let accepted = false;
|
|
149
|
+
if (message.type === 'device-auth') {
|
|
150
|
+
accepted = authorizeDevice(message.clientId, message.credential) === true;
|
|
151
|
+
} else if (message.type === 'auth') {
|
|
152
|
+
const supplied = Buffer.from(typeof message.token === 'string' ? message.token : '');
|
|
153
|
+
const expected = Buffer.from(token || '');
|
|
154
|
+
accepted = supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected);
|
|
155
|
+
}
|
|
156
|
+
if (!accepted) {
|
|
157
|
+
ws.close(1008, 'Authentication failed'); return;
|
|
158
|
+
}
|
|
159
|
+
if (message.type === 'device-auth') deviceConnections.set(ws, {
|
|
160
|
+
clientId: message.clientId, credential: message.credential,
|
|
161
|
+
revoke() { stop(); ws.close(4401, 'Device authorization revoked'); },
|
|
162
|
+
});
|
|
131
163
|
authenticated = true; clearTimeout(authTimer); send({ type: 'ready', version: 1 }); return;
|
|
132
164
|
}
|
|
133
|
-
const { id, type } = message;
|
|
134
|
-
if (!Number.isSafeInteger(id) || id < 1) { ws.close(1008, 'Invalid request id'); return; }
|
|
135
|
-
if (type === 'response-ack') { active.get(id)?.acknowledge?.(message.batch); return; }
|
|
136
|
-
if (type === 'cancel') { active.get(id)?.end(); return; }
|
|
165
|
+
const { id, type } = message;
|
|
166
|
+
if (!Number.isSafeInteger(id) || id < 1) { ws.close(1008, 'Invalid request id'); return; }
|
|
167
|
+
if (type === 'response-ack') { active.get(id)?.acknowledge?.(message.batch); return; }
|
|
168
|
+
if (type === 'cancel') { active.get(id)?.end(); return; }
|
|
137
169
|
if (type === 'unsubscribe') { const reply = active.get(id); if (reply?.subscription) reply.end(); return; }
|
|
138
170
|
if (!['request', 'subscribe'].includes(type) || active.has(id)) { ws.close(1008, 'Invalid request'); return; }
|
|
139
171
|
const subscription = type === 'subscribe';
|
|
140
172
|
const path = message.path;
|
|
141
|
-
const isStream = typeof path === 'string' && /^\/api\/(?:usage-events|turn-events|shared-recovery\/events|threads\/[^/?]+\/events)(?:\?|$)/.test(path);
|
|
142
|
-
|
|
173
|
+
const isStream = typeof path === 'string' && /^\/api\/(?:usage-events|thread-events|turn-events|shared-recovery\/events|threads\/[^/?]+\/(?:events|dots-activity-events))(?:\?|$)/.test(path);
|
|
174
|
+
// Native recovery, usage, catalog, current chat, and the main Dot's sidebar
|
|
175
|
+
// share one station connection (including when viewing a delegated task).
|
|
176
|
+
if (active.size >= 32 || (subscription && [...active.values()].filter(r => r.subscription).length >= 5)) {
|
|
143
177
|
send({ type: 'response', id, status: 429, body: { error: '同时进行的请求过多' } }); return;
|
|
144
178
|
}
|
|
145
179
|
if (typeof path !== 'string' || !path.startsWith('/api/') || path.includes('#') ||
|
|
@@ -149,58 +183,69 @@ export function attachLanSocket(server, { token, dispatch, authorizeDevice = ()
|
|
|
149
183
|
(message.body !== undefined && (!message.body || typeof message.body !== 'object' || Array.isArray(message.body)))) {
|
|
150
184
|
send({ type: 'response', id, status: 400, body: { error: '无效的连接请求' } }); return;
|
|
151
185
|
}
|
|
152
|
-
const reply = new EventEmitter();
|
|
153
|
-
let waiting, batch = 0;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
186
|
+
const reply = new EventEmitter();
|
|
187
|
+
let waiting, batch = 0;
|
|
188
|
+
const sendPart = (pieces, pageEnd = false) => new Promise((resolve, reject) => {
|
|
189
|
+
const timer = setTimeout(() => reject(new Error('历史接收确认超时')), 20000);
|
|
190
|
+
waiting = { resolve, reject, timer, batch: ++batch };
|
|
191
|
+
send({ type: subscription ? 'event-part' : 'response-part', id, batch, pieces, ...(pageEnd ? { pageEnd: true } : {}) });
|
|
192
|
+
});
|
|
193
|
+
const sendTimeline = async (status, body, event) => {
|
|
194
|
+
if (event) batch = 0;
|
|
195
|
+
try {
|
|
196
|
+
for (const pieces of timelineSync.timelineBatches(body)) {
|
|
197
|
+
if (reply.writableEnded) return;
|
|
198
|
+
await sendPart(pieces);
|
|
199
|
+
}
|
|
200
|
+
if (!reply.writableEnded) {
|
|
201
|
+
send(event ? { type: 'event', id, event, streamed: true } : { type: 'response', id, status, streamed: true });
|
|
202
|
+
if (!event) reply.end();
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!reply.writableEnded) {
|
|
206
|
+
send(event ? { type: 'event', id, event: 'bridge-error', data: { error: error.message } }
|
|
207
|
+
: { type: 'response', id, status: 500, body: { error: error.message } });
|
|
208
|
+
reply.end();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
Object.assign(reply, {
|
|
178
213
|
subscription, headersSent: false, writableEnded: false, destroyed: false,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
async historyPage(page) {
|
|
215
|
+
const parts = timelineSync.timelineBatches(page);
|
|
216
|
+
let part = parts.next();
|
|
217
|
+
while (!part.done) {
|
|
218
|
+
if (this.writableEnded) throw new Error('历史读取已取消');
|
|
219
|
+
const next = parts.next();
|
|
220
|
+
await sendPart(part.value, next.done);
|
|
221
|
+
part = next;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
json(status, body) {
|
|
225
|
+
if (this.writableEnded) return;
|
|
226
|
+
if (status < 400 && body?.schemaVersion === 1 && Array.isArray(body.turns)) {
|
|
227
|
+
void sendTimeline(status, body); return;
|
|
228
|
+
}
|
|
229
|
+
send({ type: 'response', id, status, body }); this.end();
|
|
230
|
+
},
|
|
231
|
+
acknowledge(sequence) {
|
|
232
|
+
if (!waiting || sequence !== waiting.batch) return;
|
|
233
|
+
const completed = waiting; waiting = null; clearTimeout(completed.timer);
|
|
234
|
+
this.emit('progress'); completed.resolve();
|
|
235
|
+
},
|
|
191
236
|
startEvents() {
|
|
192
237
|
if (this.writableEnded) return;
|
|
193
238
|
this.headersSent = true; send({ type: 'subscribed', id });
|
|
194
239
|
},
|
|
195
|
-
event(event, data) {
|
|
196
|
-
if (this.writableEnded) return;
|
|
197
|
-
if (event === 'update' && data?.schemaVersion === 1) return sendTimeline(200, data, event);
|
|
198
|
-
send({ type: 'event', id, event, data });
|
|
199
|
-
},
|
|
240
|
+
event(event, data) {
|
|
241
|
+
if (this.writableEnded) return;
|
|
242
|
+
if (event === 'update' && data?.schemaVersion === 1) return sendTimeline(200, data, event);
|
|
243
|
+
send({ type: 'event', id, event, data });
|
|
244
|
+
},
|
|
200
245
|
end() {
|
|
201
246
|
if (this.writableEnded) return;
|
|
202
|
-
this.writableEnded = this.destroyed = true; active.delete(id);
|
|
203
|
-
if (waiting) { clearTimeout(waiting.timer); waiting.reject(new Error('历史读取已取消')); waiting = null; }
|
|
247
|
+
this.writableEnded = this.destroyed = true; active.delete(id);
|
|
248
|
+
if (waiting) { clearTimeout(waiting.timer); waiting.reject(new Error('历史读取已取消')); waiting = null; }
|
|
204
249
|
if (subscription) send({ type: 'end', id });
|
|
205
250
|
this.emit('close');
|
|
206
251
|
},
|
|
@@ -210,9 +255,9 @@ export function attachLanSocket(server, { token, dispatch, authorizeDevice = ()
|
|
|
210
255
|
catch (error) { reply.json(500, { error: error.message }); }
|
|
211
256
|
});
|
|
212
257
|
});
|
|
213
|
-
return { close() {
|
|
214
|
-
stopWatching?.();
|
|
215
|
-
for (const ws of wss.clients) ws.terminate(); wss.close();
|
|
216
|
-
for (const ws of uploadWss.clients) ws.terminate(); uploadWss.close();
|
|
217
|
-
} };
|
|
218
|
-
}
|
|
258
|
+
return { close() {
|
|
259
|
+
stopWatching?.();
|
|
260
|
+
for (const ws of wss.clients) ws.terminate(); wss.close();
|
|
261
|
+
for (const ws of uploadWss.clients) ws.terminate(); uploadWss.close();
|
|
262
|
+
} };
|
|
263
|
+
}
|