@walkhi/code-relax 0.1.0-beta.2 → 0.1.0-beta.21
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
package/dist/web/p2p-probe.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
const HIGH_WATER = 1024 * 1024;
|
|
3
|
-
const LOW_WATER = 512 * 1024;
|
|
4
|
-
const MAX_QUEUED = 16 * 1024 * 1024;
|
|
5
|
-
|
|
6
|
-
root.CodeRelaxP2pProbe = class {
|
|
7
|
-
constructor(send, report, receiveData = () => {}) {
|
|
8
|
-
this.send = send; this.report = report; this.receiveData = receiveData;
|
|
9
|
-
this.closed = false; this.queue = Promise.resolve(); this.outbound = []; this.queuedBytes = 0; this.dataSerial = 0;
|
|
10
|
-
this.diagnostics = new root.CodeRelaxP2pDiagnostics();
|
|
11
|
-
this.framer = new root.CodeRelaxP2pDataFramer();
|
|
12
|
-
}
|
|
13
|
-
start() {
|
|
14
|
-
if (typeof root.RTCPeerConnection !== 'function' || !root.crypto?.getRandomValues) {
|
|
15
|
-
this.close('unsupported'); return;
|
|
16
|
-
}
|
|
17
|
-
this.id = Array.from(crypto.getRandomValues(new Uint8Array(16)), n => n.toString(16).padStart(2, '0')).join('');
|
|
18
|
-
this.nonce = this.id;
|
|
19
|
-
this.timer = setTimeout(() => this.close('timeout'), 15000);
|
|
20
|
-
this.report('negotiating'); this.signal({ kind: 'start' });
|
|
21
|
-
}
|
|
22
|
-
createPeer(iceServers) {
|
|
23
|
-
const peer = this.peer = new RTCPeerConnection({ iceServers });
|
|
24
|
-
peer.onicecandidate = event => {
|
|
25
|
-
if (event.candidate && !this.closed) this.signal({ kind: 'candidate', candidate: event.candidate.candidate, mid: event.candidate.sdpMid || '0' });
|
|
26
|
-
if (event.candidate && !this.closed) this.diagnostics.candidate('local', event.candidate.candidate);
|
|
27
|
-
};
|
|
28
|
-
peer.oniceconnectionstatechange = () => { if (!this.closed) this.diagnostics.ice = peer.iceConnectionState; };
|
|
29
|
-
peer.onicegatheringstatechange = () => { if (!this.closed) this.diagnostics.gathering = peer.iceGatheringState; };
|
|
30
|
-
peer.onicecandidateerror = () => { if (!this.closed) this.diagnostics.error = 'stun'; };
|
|
31
|
-
peer.onconnectionstatechange = () => {
|
|
32
|
-
if (['failed', 'closed', 'disconnected'].includes(peer.connectionState)) this.close('failed');
|
|
33
|
-
};
|
|
34
|
-
peer.ondatachannel = event => {
|
|
35
|
-
if (this.channel || event.channel.label !== 'code-relax-probe-v1') { event.channel.close(); this.close('failed'); return; }
|
|
36
|
-
const channel = this.channel = event.channel;
|
|
37
|
-
channel.bufferedAmountLowThreshold = LOW_WATER;
|
|
38
|
-
channel.onbufferedamountlow = () => this.pump();
|
|
39
|
-
channel.onopen = () => {
|
|
40
|
-
if (!this.closed) { this.diagnostics.open = true; channel.send(JSON.stringify({ type: 'ping', nonce: this.nonce })); }
|
|
41
|
-
};
|
|
42
|
-
channel.onerror = () => { this.diagnostics.error = 'channel'; this.close('failed'); };
|
|
43
|
-
channel.onclose = () => this.close('closed');
|
|
44
|
-
channel.onmessage = event => this.receiveChannelMessage(event.data);
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
receiveChannelMessage(value) {
|
|
48
|
-
if (this.closed) return;
|
|
49
|
-
try {
|
|
50
|
-
if (typeof value !== 'string' || value.length > 65536) throw new Error();
|
|
51
|
-
const message = JSON.parse(value);
|
|
52
|
-
const data = this.framer.accept(message);
|
|
53
|
-
if (data.handled) {
|
|
54
|
-
if (!this.transportActive) throw new Error();
|
|
55
|
-
if (data.complete) this.receiveData(data.payload);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (message.type === 'ping' && /^[\w-]{16,128}$/.test(message.nonce)) {
|
|
59
|
-
this.channel.send(JSON.stringify({ type: 'pong', nonce: message.nonce }));
|
|
60
|
-
this.pingReceived = true; this.diagnostics.ping = true;
|
|
61
|
-
} else if (message.type === 'pong' && message.nonce === this.nonce) {
|
|
62
|
-
this.pongReceived = true; this.diagnostics.pong = true;
|
|
63
|
-
} else throw new Error();
|
|
64
|
-
this.verify();
|
|
65
|
-
} catch { this.diagnostics.error = 'frame'; this.close('failed'); }
|
|
66
|
-
}
|
|
67
|
-
verify() {
|
|
68
|
-
if (this.verified || !this.pingReceived || !this.pongReceived) return;
|
|
69
|
-
this.verified = true; this.transportActive = this.remoteDirect;
|
|
70
|
-
clearTimeout(this.timer);
|
|
71
|
-
this.report(this.transportActive ? 'direct' : 'verified', this.diagnostics.snapshot('verified'));
|
|
72
|
-
this.pump();
|
|
73
|
-
}
|
|
74
|
-
sendData(payload) {
|
|
75
|
-
if (!this.transportActive || this.closed || this.channel?.readyState !== 'open') return false;
|
|
76
|
-
let item;
|
|
77
|
-
try { item = this.framer.create(payload, `${this.id}-${++this.dataSerial}`); }
|
|
78
|
-
catch { return false; }
|
|
79
|
-
if (this.queuedBytes + item.bytes > MAX_QUEUED) return false;
|
|
80
|
-
this.outbound.push(item); this.queuedBytes += item.bytes; this.pump(); return true;
|
|
81
|
-
}
|
|
82
|
-
pump() {
|
|
83
|
-
if (!this.transportActive || this.closed || this.channel?.readyState !== 'open') return;
|
|
84
|
-
try {
|
|
85
|
-
while (this.outbound.length && this.channel.bufferedAmount < HIGH_WATER) {
|
|
86
|
-
const item = this.outbound[0], frame = this.framer.frame(item);
|
|
87
|
-
this.channel.send(frame);
|
|
88
|
-
if (this.framer.commit(item)) { this.outbound.shift(); this.queuedBytes -= item.bytes; }
|
|
89
|
-
}
|
|
90
|
-
} catch { this.diagnostics.error = 'channel'; this.close('failed'); }
|
|
91
|
-
}
|
|
92
|
-
signal(value) { this.send({ type: 'p2p-probe', version: 1, probeId: this.id, ...value }); }
|
|
93
|
-
receive(message) {
|
|
94
|
-
if (this.closed || message.probeId !== this.id || message.version !== 1) return;
|
|
95
|
-
this.queue = this.queue.then(async () => {
|
|
96
|
-
if (this.closed) return;
|
|
97
|
-
if (message.kind === 'offer' && !this.offerReceived) {
|
|
98
|
-
this.offerReceived = true; this.remoteDirect = message.direct === 1;
|
|
99
|
-
this.createPeer((message.iceServers || []).map(url => ({ urls: url })));
|
|
100
|
-
await this.peer.setRemoteDescription({ type: 'offer', sdp: message.sdp });
|
|
101
|
-
this.diagnostics.offer = true;
|
|
102
|
-
const answer = await this.peer.createAnswer();
|
|
103
|
-
if (this.closed) return;
|
|
104
|
-
await this.peer.setLocalDescription(answer);
|
|
105
|
-
this.diagnostics.answer = true;
|
|
106
|
-
if (!this.closed) this.signal({ kind: 'answer', sdp: answer.sdp, direct: 1 });
|
|
107
|
-
} else if (message.kind === 'candidate') {
|
|
108
|
-
this.diagnostics.candidate('remote', message.candidate);
|
|
109
|
-
await this.peer.addIceCandidate({ candidate: message.candidate, sdpMid: message.mid });
|
|
110
|
-
} else if (message.kind === 'close') this.close('closed');
|
|
111
|
-
else this.close('failed');
|
|
112
|
-
}).catch(() => { this.diagnostics.error = message.kind === 'candidate' ? 'candidate' : 'description'; this.close('failed'); });
|
|
113
|
-
}
|
|
114
|
-
close(state = 'closed', silent = false) {
|
|
115
|
-
if (this.closed) return;
|
|
116
|
-
this.closed = true; this.transportActive = false; clearTimeout(this.timer);
|
|
117
|
-
this.outbound = []; this.queuedBytes = 0; this.framer.reset();
|
|
118
|
-
this.channel?.close(); this.peer?.close();
|
|
119
|
-
if (this.id) this.signal({ kind: 'close' });
|
|
120
|
-
if (!silent) this.report(state, this.diagnostics.snapshot(state));
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
})(globalThis);
|
package/dist/web/page-resume.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
function preferredThreadId(options) {
|
|
3
|
-
const { pageResume, resumeConfig, saved, visited } = options;
|
|
4
|
-
return pageResume ? (resumeConfig?.resumeThreadId || saved?.selectedId || '') : (visited?.id || '');
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
async function resolveConversation(options) {
|
|
8
|
-
const { pageResume, resumeConfig, saved, threads, visited, readCached, readLastCached, onCacheError } = options;
|
|
9
|
-
const requestedThreadId = preferredThreadId({ pageResume, resumeConfig, saved, visited });
|
|
10
|
-
const requestedThread = requestedThreadId
|
|
11
|
-
? saved?.selectedThread || threads.find(thread => thread.id === requestedThreadId)
|
|
12
|
-
|| (visited?.id === requestedThreadId ? visited : { id: requestedThreadId })
|
|
13
|
-
: null;
|
|
14
|
-
let cached = saved?.timeline || null;
|
|
15
|
-
if (requestedThreadId && cached?.thread?.id !== requestedThreadId) cached = null;
|
|
16
|
-
if (!cached && requestedThread) {
|
|
17
|
-
cached = await readCached(requestedThreadId, requestedThread).catch(error => {
|
|
18
|
-
onCacheError(error);
|
|
19
|
-
return null;
|
|
20
|
-
}) || { thread: requestedThread, turns: [] };
|
|
21
|
-
}
|
|
22
|
-
if (!cached && !pageResume) cached = await readLastCached();
|
|
23
|
-
if (!cached?.thread || (requestedThreadId && cached.thread.id !== requestedThreadId)) return null;
|
|
24
|
-
return cached;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
root.CodexPageResume = Object.freeze({ preferredThreadId, resolveConversation });
|
|
28
|
-
})(globalThis);
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
function createPendingMessageStore() {
|
|
3
|
-
const messagesByThread = new Map();
|
|
4
|
-
const clientId = item => item?.clientId || item?.clientUserMessageId;
|
|
5
|
-
const normalizeText = text => (text || '').replace(/\r\n?/g, '\n').trim();
|
|
6
|
-
const isImage = attachment => attachment && (attachment.type === 'image' || attachment.type === 'localImage');
|
|
7
|
-
|
|
8
|
-
function entries(timeline) {
|
|
9
|
-
return (timeline?.turns || []).flatMap(turn => (turn.entries || [])
|
|
10
|
-
.map(entry => ({ ...entry, turnId: turn.id, key: `${turn.id}/${entry.id}` })));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function baseline(timeline) {
|
|
14
|
-
return new Set(entries(timeline).map(entry => entry.key));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function get(threadId) {
|
|
18
|
-
return messagesByThread.get(threadId) || [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function set(threadId, messages) {
|
|
22
|
-
if (messages?.length) messagesByThread.set(threadId, messages);
|
|
23
|
-
else messagesByThread.delete(threadId);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function has(threadId, message) {
|
|
27
|
-
return get(threadId).includes(message);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function retainImagePreviews(threadId, data) {
|
|
31
|
-
if (!data || data.cached) return;
|
|
32
|
-
const messages = get(threadId);
|
|
33
|
-
if (!messages.length) return;
|
|
34
|
-
const candidates = entries(data);
|
|
35
|
-
const consumed = new Set();
|
|
36
|
-
for (const message of messages) {
|
|
37
|
-
let match;
|
|
38
|
-
if (message.clientUserMessageId) {
|
|
39
|
-
match = candidates.find(candidate => candidate.role === 'user' && !consumed.has(candidate.key)
|
|
40
|
-
&& clientId(candidate) === message.clientUserMessageId);
|
|
41
|
-
}
|
|
42
|
-
match ||= candidates.find(candidate => candidate.role === 'user' && !consumed.has(candidate.key)
|
|
43
|
-
&& !message.baseline.has(candidate.key)
|
|
44
|
-
&& !(clientId(candidate) && message.baselineClientIds?.has(clientId(candidate)))
|
|
45
|
-
&& !(message.clientUserMessageId && clientId(candidate))
|
|
46
|
-
&& (!message.turnId || candidate.turnId === message.turnId || (message.settled && !message.status))
|
|
47
|
-
&& normalizeText(candidate.text) === normalizeText(message.text)
|
|
48
|
-
&& (candidate.attachments || []).length === message.attachments.length);
|
|
49
|
-
if (!match) continue;
|
|
50
|
-
consumed.add(match.key);
|
|
51
|
-
const previews = (message.attachments || []).filter(isImage);
|
|
52
|
-
const attachments = (match.attachments || []).filter(isImage);
|
|
53
|
-
attachments.forEach((attachment, index) => {
|
|
54
|
-
const preview = previews[index];
|
|
55
|
-
const url = preview?.url || preview?.thumbnailUrl;
|
|
56
|
-
if (!attachment.url && url) Object.assign(attachment, {
|
|
57
|
-
available: true,
|
|
58
|
-
url,
|
|
59
|
-
thumbnailUrl: preview.thumbnailUrl || url,
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function reconcile(threadId, timeline) {
|
|
66
|
-
const messages = get(threadId);
|
|
67
|
-
const candidates = entries(timeline);
|
|
68
|
-
const consumed = new Set();
|
|
69
|
-
const matches = new Map();
|
|
70
|
-
if (!timeline?.cached) for (const message of messages) {
|
|
71
|
-
if (!message.clientUserMessageId) continue;
|
|
72
|
-
const match = candidates.find(entry => entry.role === 'user' && !consumed.has(entry.key)
|
|
73
|
-
&& clientId(entry) === message.clientUserMessageId);
|
|
74
|
-
if (match) {
|
|
75
|
-
matches.set(message, match);
|
|
76
|
-
consumed.add(match.key);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
let interruptedUnconfirmed = 0;
|
|
80
|
-
const remaining = messages.filter(message => {
|
|
81
|
-
const match = matches.get(message) || (!timeline?.cached && candidates.find(entry => entry.role === 'user'
|
|
82
|
-
&& !message.baseline.has(entry.key) && !consumed.has(entry.key)
|
|
83
|
-
&& !(clientId(entry) && message.baselineClientIds?.has(clientId(entry)))
|
|
84
|
-
&& !(message.clientUserMessageId && clientId(entry))
|
|
85
|
-
&& (!message.turnId || entry.turnId === message.turnId || (message.settled && !message.status))
|
|
86
|
-
&& normalizeText(entry.text) === normalizeText(message.text)
|
|
87
|
-
&& (entry.attachments || []).length === message.attachments.length));
|
|
88
|
-
if (match) consumed.add(match.key);
|
|
89
|
-
if (!match && !timeline?.cached && message.settled && !message.status && message.turnId
|
|
90
|
-
&& timeline?.turns?.some(turn => turn.id === message.turnId && turn.status === 'interrupted')) {
|
|
91
|
-
interruptedUnconfirmed++;
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
return !match;
|
|
95
|
-
});
|
|
96
|
-
for (const message of remaining) for (const entry of candidates) if (consumed.has(entry.key)) {
|
|
97
|
-
message.baseline.add(entry.key);
|
|
98
|
-
if (clientId(entry)) (message.baselineClientIds ||= new Set()).add(clientId(entry));
|
|
99
|
-
}
|
|
100
|
-
set(threadId, remaining);
|
|
101
|
-
return { remaining, interruptedUnconfirmed };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return Object.freeze({ baseline, get, has, reconcile, retainImagePreviews, set });
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
root.createPendingMessageStore = createPendingMessageStore;
|
|
108
|
-
})(globalThis);
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
function createQueueController(options) {
|
|
3
|
-
const {
|
|
4
|
-
document, el, api, state, pendingMessages,
|
|
5
|
-
isComposerActive, isConnectionBlocked, isThreadReadOnly,
|
|
6
|
-
renderPendingMessages, scrollTimelineToBottom, showToast,
|
|
7
|
-
} = options;
|
|
8
|
-
const steering = new Set();
|
|
9
|
-
|
|
10
|
-
function render(items) {
|
|
11
|
-
const panel = el('queuePanel');
|
|
12
|
-
panel.replaceChildren();
|
|
13
|
-
for (const item of items) {
|
|
14
|
-
const row = document.createElement('div');
|
|
15
|
-
row.className = 'queued-message';
|
|
16
|
-
const text = document.createElement('span');
|
|
17
|
-
text.className = `queued-text${item.error ? ' queued-error' : ''}`;
|
|
18
|
-
text.textContent = item.error ? `${item.prompt} · ${item.error}` : item.prompt;
|
|
19
|
-
text.title = text.textContent;
|
|
20
|
-
const steerButton = document.createElement('button');
|
|
21
|
-
steerButton.className = 'queue-action';
|
|
22
|
-
steerButton.type = 'button';
|
|
23
|
-
steerButton.textContent = '↪ 引导';
|
|
24
|
-
steerButton.onclick = () => steer(item);
|
|
25
|
-
const removeButton = document.createElement('button');
|
|
26
|
-
removeButton.className = 'queue-action';
|
|
27
|
-
removeButton.type = 'button';
|
|
28
|
-
removeButton.textContent = '删除';
|
|
29
|
-
removeButton.onclick = () => remove(item.id);
|
|
30
|
-
const key = `${state.selectedId}/${item.id}`;
|
|
31
|
-
steerButton.disabled = removeButton.disabled = isConnectionBlocked() || isThreadReadOnly() || steering.has(key);
|
|
32
|
-
row.append(text, steerButton, removeButton);
|
|
33
|
-
panel.append(row);
|
|
34
|
-
}
|
|
35
|
-
panel.hidden = !items.length;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function steer(item) {
|
|
39
|
-
const threadId = state.selectedId;
|
|
40
|
-
const key = `${threadId}/${item.id}`;
|
|
41
|
-
if (!threadId || isConnectionBlocked() || steering.has(key)) return;
|
|
42
|
-
steering.add(key);
|
|
43
|
-
const message = {
|
|
44
|
-
queueItemId: item.id,
|
|
45
|
-
clientUserMessageId: item.clientUserMessageId,
|
|
46
|
-
text: item.text ?? item.prompt,
|
|
47
|
-
attachments: item.attachments || [],
|
|
48
|
-
status: '',
|
|
49
|
-
turnId: isComposerActive() ? state.timeline?.turns?.at(-1)?.id : null,
|
|
50
|
-
settled: false,
|
|
51
|
-
baseline: pendingMessages.baseline(state.timeline),
|
|
52
|
-
};
|
|
53
|
-
pendingMessages.set(threadId, [...pendingMessages.get(threadId), message]);
|
|
54
|
-
renderPendingMessages();
|
|
55
|
-
scrollTimelineToBottom(true);
|
|
56
|
-
try {
|
|
57
|
-
const result = await api(`/api/threads/${encodeURIComponent(threadId)}/queue/${item.id}/steer`, { method: 'POST' });
|
|
58
|
-
if (state.selectedId === threadId) render(result.items || []);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
message.status = '发送未确认,收到正式消息后会自动合并';
|
|
61
|
-
showToast(`插队失败:${error.message}`);
|
|
62
|
-
} finally {
|
|
63
|
-
message.settled = true;
|
|
64
|
-
steering.delete(key);
|
|
65
|
-
if (state.selectedId === threadId) renderPendingMessages();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function remove(itemId) {
|
|
70
|
-
try {
|
|
71
|
-
const result = await api(`/api/threads/${encodeURIComponent(state.selectedId)}/queue/${itemId}`, { method: 'DELETE' });
|
|
72
|
-
render(result.items || []);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
showToast(`删除失败:${error.message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return Object.freeze({ render });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
root.createQueueController = createQueueController;
|
|
82
|
-
})(globalThis);
|
package/dist/web/resources.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
["server-requests.js","approval-ui.js","chat.css","community.css","highlight.min.js","highlight-powershell.min.js","capabilities.js","timeline-sync.js","timeline-reducer.js","harmony-platform.js","client-platform.js","lan-session.js","history-cache.js","p2p-diagnostics.js","p2p-data-channel.js","p2p-probe.js","self-relay-session.js","chat-transport.js","page-resume.js","conversation-controller.js","history-controller.js","usage-controller.js","task-list-view.js","draft-controller.js","thread-attention-controller.js","thread-title-controller.js","thread-list-controller.js","thread-list-sync.js","station-connection-controller.js","disclosure-state-controller.js","pending-message-store.js","queue-controller.js","thread-context-controller.js","composer-controller.js","message-send-controller.js","link-action-controller.js","message-view.js","activity-view.js","community-view.js","timeline-formatters.js","timeline-scroll-controller.js","timeline-renderer.js","chat.js"]
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
let socketSerial = 0;
|
|
3
|
-
const nativeViews = new Map();
|
|
4
|
-
root.computerConnectionEvent = (key, event, value) => nativeViews.get(key)?.emit(event, JSON.parse(value));
|
|
5
|
-
class NativeView extends EventTarget {
|
|
6
|
-
constructor() {
|
|
7
|
-
super(); this.key = `${Date.now()}-${++socketSerial}`; this.readyState = 0; this.autoProbeStarted = false; nativeViews.set(this.key, this);
|
|
8
|
-
queueMicrotask(() => { if (this.readyState === 0) { this.readyState = 1; this.emit('open', {}); } });
|
|
9
|
-
}
|
|
10
|
-
emit(type, value) {
|
|
11
|
-
if (this.readyState === 3) return;
|
|
12
|
-
if (type === 'message' && value.type === 'p2p-probe') { this.probe?.receive(value); return; }
|
|
13
|
-
if (type === 'message' && value.type === 'p2p-restart') { this.startProbe(); return; }
|
|
14
|
-
if (type === 'message' && value.type === 'p2p-data-out') {
|
|
15
|
-
const sent = typeof value.payload === 'string' && this.probe?.sendData(value.payload) === true;
|
|
16
|
-
root.codexNative.computerTransport(JSON.stringify({ type: 'p2p-data-result', key: this.key,
|
|
17
|
-
payload: JSON.stringify({ token: value.token, sent }) }));
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
if (type === 'close') { this.probe?.close(); this.readyState = 3; nativeViews.delete(this.key); }
|
|
21
|
-
this.dispatch(type, value);
|
|
22
|
-
if (type === 'message' && value.type === 'ready' && value.p2pProbe === 1 && !this.autoProbeStarted) {
|
|
23
|
-
this.autoProbeStarted = true; queueMicrotask(() => { if (this.readyState !== 3) this.startProbe(); });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
dispatch(type, value) {
|
|
27
|
-
const event = type === 'message' ? new MessageEvent(type, { data: JSON.stringify(value) }) : new Event(type);
|
|
28
|
-
if (type === 'close') Object.defineProperty(event, 'code', { value: value.code });
|
|
29
|
-
this.dispatchEvent(event); this[`on${type}`]?.(event);
|
|
30
|
-
}
|
|
31
|
-
startProbe() {
|
|
32
|
-
this.probe?.close('replaced', true);
|
|
33
|
-
this.probe = new root.CodeRelaxP2pProbe(
|
|
34
|
-
signal => root.codexNative.computerTransport(JSON.stringify({ type: 'p2p-probe', key: this.key, payload: JSON.stringify(signal) })),
|
|
35
|
-
(state, diagnostic) => {
|
|
36
|
-
if (diagnostic) root.codexNative.computerTransport(JSON.stringify({ type: 'p2p-diagnostic', key: this.key, payload: diagnostic }));
|
|
37
|
-
root.codexNative.computerTransport(JSON.stringify({ type: 'p2p-status', key: this.key, payload: state }));
|
|
38
|
-
}, payload => root.codexNative.computerTransport(JSON.stringify({ type: 'p2p-data-in', key: this.key, payload })));
|
|
39
|
-
try { this.probe.start(); }
|
|
40
|
-
catch { this.probe.diagnostics.error = 'initialization'; this.probe.close('failed'); }
|
|
41
|
-
}
|
|
42
|
-
send(value) {
|
|
43
|
-
const frame = JSON.parse(value);
|
|
44
|
-
if (frame.type === 'data') root.codexNative.computerTransport(JSON.stringify({ type: 'send', key: this.key, payload: frame.payload }));
|
|
45
|
-
else root.codexNative.computerTransport(JSON.stringify({ type: 'attach', key: this.key }));
|
|
46
|
-
}
|
|
47
|
-
close() {
|
|
48
|
-
this.probe?.close();
|
|
49
|
-
root.codexNative.computerTransport(JSON.stringify({ type: 'detach', key: this.key }));
|
|
50
|
-
this.emit('close', { code: 1000 });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// Reuse LAN request, subscription and disconnect semantics; replace only the envelope.
|
|
54
|
-
root.CodexSelfRelaySession = class extends root.CodexLanSession {
|
|
55
|
-
constructor() { super(''); this.config = root.codexSelfRelayConfig; }
|
|
56
|
-
createSocket() { return root.codexNative?.computerTransport ? new NativeView() : super.createSocket(); }
|
|
57
|
-
connect() { return this.terminalError ? Promise.reject(this.terminalError) : super.connect(); }
|
|
58
|
-
socketUrl() {
|
|
59
|
-
const url = new URL(this.config.url);
|
|
60
|
-
if ((url.protocol !== 'wss:' && !(url.protocol === 'ws:' && url.hostname === '127.0.0.1')) ||
|
|
61
|
-
url.pathname !== '/relay' || url.search || url.hash || url.username || url.password) throw new Error('无效的Code Relax 中继地址');
|
|
62
|
-
return url.href;
|
|
63
|
-
}
|
|
64
|
-
authenticationMessage() {
|
|
65
|
-
const { room, code, credential, clientId } = this.config;
|
|
66
|
-
return code ? { type: 'pair', room, code, clientId, ...(credential ? { credential } : {}) } : { type: 'resume', room, credential, clientId };
|
|
67
|
-
}
|
|
68
|
-
closeError(event) {
|
|
69
|
-
const reasons = { 4401: '设备授权不存在或已撤销,请重新扫码授权', 4408: '配对码已过期或使用,请生成新的首次授权码',
|
|
70
|
-
4406: 'Code Relax 中继协议需要升级,请更新工作站和手机', 4409: '连接已由同一设备的新连接接替', 4410: '工作站暂时离线,正在等待恢复,设备授权仍保留',
|
|
71
|
-
4428: '当前服务器压力较大,请稍后再试',
|
|
72
|
-
1011: '工作站 Bridge 暂时不可用,正在恢复连接' };
|
|
73
|
-
const error = new TypeError(reasons[event.code] || `Code Relax 中继连接已断开(${event.code}),正在恢复`);
|
|
74
|
-
error.code = event.code;
|
|
75
|
-
if (event.code === 4401) root.codexHistoryCache?.clear().catch(() => {});
|
|
76
|
-
if ([1008, 4401, 4406, 4408, 4409, 4428, 4429].includes(event.code)) this.terminalError = error;
|
|
77
|
-
return error;
|
|
78
|
-
}
|
|
79
|
-
encodeMessage(message) {
|
|
80
|
-
const frame = { type: 'data', payload: JSON.stringify(message) };
|
|
81
|
-
if (new TextEncoder().encode(JSON.stringify(frame)).length > 8 * 1024 * 1024) throw new Error('Code Relax 中继私测消息超过 8 MiB,请减少图片或内容');
|
|
82
|
-
return frame;
|
|
83
|
-
}
|
|
84
|
-
async pause() {
|
|
85
|
-
const socket = this.socket;
|
|
86
|
-
if (!socket || socket.readyState === 3) return super.pause();
|
|
87
|
-
const closed = new Promise(resolve => socket.addEventListener('close', resolve, { once: true }));
|
|
88
|
-
await super.pause(); await closed;
|
|
89
|
-
}
|
|
90
|
-
decodeMessage(message) {
|
|
91
|
-
if (message.type === 'paired') {
|
|
92
|
-
if (message.room !== this.config.room) throw new Error('配对工作站不匹配');
|
|
93
|
-
if (this.config.clientId && message.protocolVersion !== 7) {
|
|
94
|
-
this.terminalError = Object.assign(new TypeError('Code Relax 中继协议需要升级'), { code: 4406 }); throw this.terminalError;
|
|
95
|
-
}
|
|
96
|
-
if (message.credential && this.config.credential && message.credential !== this.config.credential) throw new Error('设备凭据与已保存身份不一致');
|
|
97
|
-
if (message.credential) this.config.credential = message.credential;
|
|
98
|
-
if (Number.isSafeInteger(message.revision) && message.revision >= 0) {
|
|
99
|
-
if (this.config.revision !== undefined && this.config.revision !== message.revision) root.codexHistoryCache?.clear().catch(() => {});
|
|
100
|
-
this.config.revision = message.revision;
|
|
101
|
-
}
|
|
102
|
-
delete this.config.code;
|
|
103
|
-
root.codexNative?.selfRelayPaired?.(message.room, this.config.credential, this.config.revision || 0);
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
if (message.type === 'lan-candidates' && Array.isArray(message.candidates)) {
|
|
107
|
-
root.codexNative?.selfRelayLanCandidates?.(JSON.stringify(message.candidates));
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
if (message.type === 'computer-info' && typeof message.hostName === 'string') {
|
|
111
|
-
root.codexNative?.selfRelayComputerInfo?.(message.hostName);
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
if (message.type === 'ready') return message;
|
|
115
|
-
if (message.type === 'data' && typeof message.payload === 'string') {
|
|
116
|
-
const started = performance.now();
|
|
117
|
-
const decoded = JSON.parse(message.payload);
|
|
118
|
-
const path = this.pending.get(decoded.id)?.path || '';
|
|
119
|
-
const route = /\/timeline(?:\?|$)/.test(path) ? 'timeline' : /\/history\?/.test(path) ? 'history'
|
|
120
|
-
: /^\/api\/threads\?/.test(path) ? 'list' : '';
|
|
121
|
-
if (route && root.codexNative?.reportSyncMetric) {
|
|
122
|
-
root.codexNative.reportSyncMetric(`stage=parse;route=${route};ms=${Math.round(performance.now() - started)};chars=${message.payload.length}`);
|
|
123
|
-
}
|
|
124
|
-
return decoded;
|
|
125
|
-
}
|
|
126
|
-
throw new Error('无效的Code Relax 中继消息');
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
})(globalThis);
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
(function (root, factory) {
|
|
2
|
-
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
-
else root.CodexServerRequests = factory();
|
|
4
|
-
})(globalThis, function () {
|
|
5
|
-
const command = 'item/commandExecution/requestApproval';
|
|
6
|
-
const file = 'item/fileChange/requestApproval';
|
|
7
|
-
const input = 'item/tool/requestUserInput';
|
|
8
|
-
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
9
|
-
function decisions(request) {
|
|
10
|
-
if (request.method === command) return request.params.availableDecisions ?? ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
11
|
-
if (request.method === file) return ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
function validate(request, result) {
|
|
15
|
-
if ([command, file].includes(request.method)) {
|
|
16
|
-
if (!result || Object.keys(result).length !== 1 || !decisions(request).some(value => equal(value, result.decision))) throw new Error('审批选项不属于当前请求');
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (request.method === input) {
|
|
20
|
-
const questions = request.params.questions || [];
|
|
21
|
-
if (!result?.answers || Object.keys(result).length !== 1 || Object.keys(result.answers).length !== questions.length
|
|
22
|
-
|| questions.some(q => !Array.isArray(result.answers[q.id]?.answers) || result.answers[q.id].answers.some(a => typeof a !== 'string'))) throw new Error('回答格式无效');
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
throw new Error('此类请求需在工作站处理');
|
|
26
|
-
}
|
|
27
|
-
class Store {
|
|
28
|
-
constructor(epoch, changed = () => {}) { this.epoch = epoch; this.changed = changed; this.requests = new Map(); }
|
|
29
|
-
list(threadId) { return [...this.requests.values()].filter(r => r.params.threadId === threadId).map(r => ({ ...r })); }
|
|
30
|
-
receive(message) {
|
|
31
|
-
if (!['string', 'number'].includes(typeof message.id) || !message.params?.threadId || this.requests.has(message.id)) return;
|
|
32
|
-
const request = { id: message.id, method: message.method, params: message.params, epoch: this.epoch, status: 'pending' };
|
|
33
|
-
this.requests.set(message.id, request); this.changed(request.params.threadId);
|
|
34
|
-
}
|
|
35
|
-
notification(method, params) {
|
|
36
|
-
if (method === 'serverRequest/resolved') {
|
|
37
|
-
const request = this.requests.get(params.requestId);
|
|
38
|
-
if (request?.params.threadId === params.threadId) { this.requests.delete(params.requestId); this.changed(params.threadId); }
|
|
39
|
-
} else if (method === 'turn/completed' || method === 'thread/closed') {
|
|
40
|
-
for (const [id, r] of this.requests) if (r.params.threadId === params.threadId && (method === 'thread/closed' || r.params.turnId === params.turn?.id)) this.requests.delete(id);
|
|
41
|
-
this.changed(params.threadId);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
forget(threadId) {
|
|
45
|
-
for (const [id, request] of this.requests) if (request.params.threadId === threadId) this.requests.delete(id);
|
|
46
|
-
this.changed(threadId);
|
|
47
|
-
}
|
|
48
|
-
reset(epoch) {
|
|
49
|
-
const threads = new Set([...this.requests.values()].map(r => r.params.threadId));
|
|
50
|
-
this.requests.clear(); this.epoch = epoch; for (const id of threads) this.changed(id);
|
|
51
|
-
}
|
|
52
|
-
async respond(threadId, { id, epoch, result }, send) {
|
|
53
|
-
const request = this.requests.get(id);
|
|
54
|
-
if (!request || epoch !== this.epoch || request.params.threadId !== threadId) throw new Error('审批已处理或连接已变化,请等待同步');
|
|
55
|
-
if (request.status !== 'pending') throw new Error('决定已提交,正在等待工作站确认,请勿重复提交');
|
|
56
|
-
validate(request, result);
|
|
57
|
-
request.status = 'submitted'; this.changed(threadId);
|
|
58
|
-
try { await send({ id: request.id, result }); }
|
|
59
|
-
catch (error) {
|
|
60
|
-
if (this.requests.get(id) === request) { request.status = 'unknown'; this.changed(threadId); }
|
|
61
|
-
throw new Error('审批发送结果未确认,请检查工作站;不会自动重发。' + error.message);
|
|
62
|
-
}
|
|
63
|
-
return { submitted: true };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { Store, decisions, validate, command, file, input };
|
|
67
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
(function (root) {
|
|
2
|
-
function createStationConnectionController(options) {
|
|
3
|
-
let recoveryTask = null;
|
|
4
|
-
let epoch = 0;
|
|
5
|
-
let paused = false;
|
|
6
|
-
|
|
7
|
-
function isCurrent(value) {
|
|
8
|
-
return !paused && value === epoch;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function suspend() {
|
|
12
|
-
paused = true;
|
|
13
|
-
epoch++;
|
|
14
|
-
recoveryTask = null;
|
|
15
|
-
options.onSuspend?.();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function activate() {
|
|
19
|
-
paused = false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function recover() {
|
|
23
|
-
if (paused) return Promise.resolve();
|
|
24
|
-
if (recoveryTask) return recoveryTask;
|
|
25
|
-
const recoveryEpoch = ++epoch;
|
|
26
|
-
recoveryTask = Promise.resolve().then(async () => {
|
|
27
|
-
let attempts = 0;
|
|
28
|
-
options.onStarted?.();
|
|
29
|
-
while (isCurrent(recoveryEpoch)) {
|
|
30
|
-
options.onAttempt?.();
|
|
31
|
-
try {
|
|
32
|
-
await options.connect();
|
|
33
|
-
if (!isCurrent(recoveryEpoch)) return;
|
|
34
|
-
try { await options.restore(); }
|
|
35
|
-
catch (error) {
|
|
36
|
-
if (!isCurrent(recoveryEpoch)) return;
|
|
37
|
-
if (options.isConnected?.()) {
|
|
38
|
-
options.onRestoreFailure?.(error);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
if (!isCurrent(recoveryEpoch)) return;
|
|
44
|
-
options.onRecovered?.(attempts + 1);
|
|
45
|
-
return;
|
|
46
|
-
} catch (error) {
|
|
47
|
-
if (!isCurrent(recoveryEpoch)) return;
|
|
48
|
-
if (options.isTerminal(error)) {
|
|
49
|
-
options.onTerminal?.(error);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
options.onRetry?.(error, attempts + 1);
|
|
53
|
-
const delay = Math.min(1000 * 2 ** Math.min(attempts++, 5), 30000) * (0.8 + Math.random() * 0.2);
|
|
54
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}).finally(() => {
|
|
58
|
-
if (recoveryEpoch === epoch) recoveryTask = null;
|
|
59
|
-
});
|
|
60
|
-
return recoveryTask;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return Object.freeze({
|
|
64
|
-
recover,
|
|
65
|
-
suspend,
|
|
66
|
-
activate,
|
|
67
|
-
isPaused: () => paused,
|
|
68
|
-
isRecovering: () => Boolean(recoveryTask),
|
|
69
|
-
generation: () => epoch,
|
|
70
|
-
isCurrent,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
root.createStationConnectionController = createStationConnectionController;
|
|
75
|
-
})(globalThis);
|