@walkhi/code-relax 0.1.0-beta.10 → 0.1.0-beta.11

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.
Files changed (78) hide show
  1. package/README.md +44 -54
  2. package/dist/bin/codex-remote.mjs +22 -58
  3. package/dist/shared/app-server-events.cjs +15 -3
  4. package/dist/shared/file-mentions.cjs +35 -0
  5. package/dist/skills/code-relax/SKILL.md +1 -1
  6. package/dist/src/desktop-launcher.mjs +6 -11
  7. package/dist/src/file-upload-session.mjs +124 -0
  8. package/dist/src/lan-socket-server.mjs +51 -8
  9. package/dist/src/managed-app-server.mjs +9 -7
  10. package/dist/src/onboarding.mjs +3 -3
  11. package/dist/src/platform/windows/desktop-host.mjs +1 -2
  12. package/dist/src/platform/windows/desktop-shortcuts.ps1 +6 -13
  13. package/dist/src/platform/windows/service-host.mjs +0 -9
  14. package/dist/src/self-relay/admin-page.mjs +1 -1
  15. package/dist/src/self-relay/connector.mjs +84 -12
  16. package/dist/src/self-relay/lifecycle.mjs +8 -24
  17. package/dist/src/self-relay/p2p-probe.mjs +1 -1
  18. package/dist/src/self-relay/server.mjs +67 -12
  19. package/dist/src/server.mjs +141 -34
  20. package/dist/src/service-doctor.mjs +1 -1
  21. package/dist/src/service-install.mjs +1 -4
  22. package/dist/src/shared-app-server.mjs +30 -28
  23. package/dist/src/shared-catalog.mjs +1 -11
  24. package/dist/src/shared-recovery.mjs +5 -5
  25. package/dist/src/skill-install.mjs +0 -13
  26. package/package.json +1 -1
  27. package/dist/src/debug-ui.mjs +0 -131
  28. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  29. package/dist/web/activity-view.js +0 -283
  30. package/dist/web/approval-ui.js +0 -188
  31. package/dist/web/capabilities.js +0 -50
  32. package/dist/web/chat-transport.js +0 -196
  33. package/dist/web/chat.css +0 -778
  34. package/dist/web/chat.js +0 -3815
  35. package/dist/web/client-platform.js +0 -11
  36. package/dist/web/community.css +0 -82
  37. package/dist/web/community.html +0 -36
  38. package/dist/web/composer-controller.js +0 -97
  39. package/dist/web/conversation-controller.js +0 -99
  40. package/dist/web/disclosure-state-controller.js +0 -95
  41. package/dist/web/draft-controller.js +0 -103
  42. package/dist/web/harmony-platform.js +0 -52
  43. package/dist/web/highlight-powershell.min.js +0 -39
  44. package/dist/web/highlight.min.js +0 -1262
  45. package/dist/web/history-cache.js +0 -240
  46. package/dist/web/history-controller.js +0 -167
  47. package/dist/web/index.html +0 -288
  48. package/dist/web/lan-session.js +0 -122
  49. package/dist/web/link-action-controller.js +0 -212
  50. package/dist/web/message-send-controller.js +0 -177
  51. package/dist/web/message-view.js +0 -98
  52. package/dist/web/p2p-data-channel.js +0 -50
  53. package/dist/web/p2p-diagnostics.js +0 -28
  54. package/dist/web/p2p-probe.js +0 -123
  55. package/dist/web/page-resume.js +0 -28
  56. package/dist/web/pending-message-store.js +0 -108
  57. package/dist/web/queue-controller.js +0 -82
  58. package/dist/web/resources.json +0 -1
  59. package/dist/web/self-relay-session.js +0 -129
  60. package/dist/web/server-requests.js +0 -67
  61. package/dist/web/station-connection-controller.js +0 -75
  62. package/dist/web/task-list-view.js +0 -289
  63. package/dist/web/thread-attention-controller.js +0 -124
  64. package/dist/web/thread-context-controller.js +0 -30
  65. package/dist/web/thread-list-controller.js +0 -61
  66. package/dist/web/thread-list-sync.js +0 -86
  67. package/dist/web/thread-title-controller.js +0 -57
  68. package/dist/web/timeline-formatters.js +0 -249
  69. package/dist/web/timeline-reducer.js +0 -77
  70. package/dist/web/timeline-renderer.js +0 -161
  71. package/dist/web/timeline-scroll-controller.js +0 -50
  72. package/dist/web/timeline-sync.js +0 -67
  73. package/dist/web/usage-controller.js +0 -258
  74. package/dist/web/vendor/README.md +0 -14
  75. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  76. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  77. package/dist/web/vendor/highlight.min.js +0 -1262
  78. package/dist/web/vendor/lucide.LICENSE.txt +0 -17
@@ -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);
@@ -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);
@@ -1 +0,0 @@
1
- ["server-requests.js","approval-ui.js","chat.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","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);