pi-harness-runtime 1.1.375 → 1.1.377
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 +68 -39
- package/package.json +2 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/dist/client.d.ts +56 -35
- package/packages/relay/dist/client.js +278 -71
- package/packages/relay/dist/index.d.ts +12 -5
- package/packages/relay/dist/index.js +12 -4
- package/packages/relay/dist/server.d.ts +58 -20
- package/packages/relay/dist/server.js +344 -49
- package/packages/relay/dist/types.d.ts +137 -1
- package/packages/relay/dist/types.js +21 -2
- package/packages/relay/dist/ws-server.d.ts +48 -0
- package/packages/relay/dist/ws-server.js +130 -0
- package/packages/relay/package.json +1 -1
- package/packages/relay/src/client.ts +375 -81
- package/packages/relay/src/index.ts +42 -5
- package/packages/relay/src/server.ts +452 -54
- package/packages/relay/src/types.ts +174 -1
- package/packages/relay/src/ws-server.ts +165 -0
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/relay-client.ts +184 -0
- package/scripts/sync-callback-queue.ts +57 -0
- package/scripts/telegram-callback-processor.ts +16 -0
- package/scripts/unified-webhook-server.ts +160 -145
- package/scripts/ws-server.ts +81 -0
|
@@ -1,93 +1,388 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Server -
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Generate a UUID v4
|
|
6
|
-
*/
|
|
7
|
-
function generateUUID() {
|
|
8
|
-
// Use crypto.randomUUID if available
|
|
9
|
-
const crypto = globalThis;
|
|
10
|
-
if (crypto.crypto?.randomUUID) {
|
|
11
|
-
return crypto.crypto.randomUUID();
|
|
12
|
-
}
|
|
13
|
-
// Fallback
|
|
14
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
15
|
-
const r = (Math.random() * 16) | 0;
|
|
16
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
17
|
-
return v.toString(16);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Relay Server
|
|
2
|
+
* Relay Server - Two-way streaming WebSocket relay
|
|
22
3
|
*
|
|
23
|
-
*
|
|
4
|
+
* Implements t3code-style streaming with:
|
|
5
|
+
* - Snapshot + Live event pattern
|
|
6
|
+
* - Typed message protocol
|
|
7
|
+
* - Connection state machine
|
|
8
|
+
* - Stream coalescing
|
|
24
9
|
*/
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { PROTOCOL_VERSION, generateMessageId, createTimestamp, } from './types.js';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const DEFAULT_PORT = 8444;
|
|
18
|
+
const DEFAULT_HOST = "0.0.0.0";
|
|
19
|
+
const MAX_RECENT_CALLBACKS = 100;
|
|
20
|
+
const STREAM_BATCH_SIZE = 50; // Batch events for coalescing
|
|
21
|
+
const STREAM_BATCH_TIMEOUT_MS = 100; // Flush batch after this delay
|
|
22
|
+
function createEventBuffer() {
|
|
23
|
+
return {
|
|
24
|
+
callbacks: [],
|
|
25
|
+
timer: null,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Relay Server - Two-way Streaming Implementation
|
|
30
|
+
// ============================================================================
|
|
25
31
|
export class RelayServer {
|
|
32
|
+
server = null;
|
|
33
|
+
wss = null;
|
|
34
|
+
clients = new Map();
|
|
26
35
|
tunnels = new Map();
|
|
27
36
|
options;
|
|
28
37
|
running = false;
|
|
38
|
+
serverSessionId;
|
|
39
|
+
// Event stream buffering for coalescing
|
|
40
|
+
eventBuffer = createEventBuffer();
|
|
41
|
+
// Recent callbacks for snapshot
|
|
42
|
+
recentCallbacks = [];
|
|
29
43
|
constructor(options = {}) {
|
|
44
|
+
this.serverSessionId = randomUUID();
|
|
30
45
|
this.options = {
|
|
31
|
-
port: options.port ??
|
|
32
|
-
host: options.host ??
|
|
46
|
+
port: options.port ?? DEFAULT_PORT,
|
|
47
|
+
host: options.host ?? DEFAULT_HOST,
|
|
48
|
+
authToken: options.authToken ?? "change-me-in-production",
|
|
49
|
+
serverVersion: options.serverVersion ?? "1.0.0",
|
|
33
50
|
onTunnelConnect: options.onTunnelConnect ?? (() => { }),
|
|
34
51
|
onTunnelDisconnect: options.onTunnelDisconnect ?? (() => { }),
|
|
35
52
|
onMessage: options.onMessage ?? (() => { }),
|
|
53
|
+
onStateChange: options.onStateChange ?? (() => { }),
|
|
36
54
|
};
|
|
37
55
|
}
|
|
56
|
+
// ========================================================================
|
|
57
|
+
// Lifecycle
|
|
58
|
+
// ========================================================================
|
|
38
59
|
/**
|
|
39
|
-
* Start the
|
|
60
|
+
* Start the WebSocket server
|
|
40
61
|
*/
|
|
41
62
|
async start() {
|
|
42
63
|
if (this.running)
|
|
43
64
|
return;
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
this.server = createServer();
|
|
66
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
67
|
+
this.wss.on("connection", (ws, req) => {
|
|
68
|
+
const clientId = randomUUID();
|
|
69
|
+
const clientIp = req.socket.remoteAddress;
|
|
70
|
+
console.log(`[RelayServer] Client connecting: ${clientId} from ${clientIp}`);
|
|
71
|
+
// Create client session
|
|
72
|
+
const session = {
|
|
73
|
+
id: clientId,
|
|
74
|
+
ws,
|
|
75
|
+
state: { state: 'connecting' },
|
|
76
|
+
subscriptions: new Set(['callback', 'status', 'sync']),
|
|
77
|
+
connectedAt: createTimestamp(),
|
|
78
|
+
lastPing: createTimestamp(),
|
|
79
|
+
pendingSnapshot: true,
|
|
80
|
+
};
|
|
81
|
+
this.clients.set(clientId, session);
|
|
82
|
+
this.updateState(session, { state: 'connecting' });
|
|
83
|
+
// Handle incoming messages
|
|
84
|
+
ws.on("message", (data) => {
|
|
85
|
+
this.handleMessage(clientId, data);
|
|
86
|
+
});
|
|
87
|
+
ws.on("close", () => {
|
|
88
|
+
this.clients.delete(clientId);
|
|
89
|
+
console.log(`[RelayServer] Client disconnected: ${clientId}`);
|
|
90
|
+
});
|
|
91
|
+
ws.on("error", (err) => {
|
|
92
|
+
console.error(`[RelayServer] Client error: ${clientId}`, err.message);
|
|
93
|
+
this.clients.delete(clientId);
|
|
94
|
+
});
|
|
95
|
+
// Send initial connected message
|
|
96
|
+
this.sendMessage(session, {
|
|
97
|
+
version: PROTOCOL_VERSION,
|
|
98
|
+
type: 'connected',
|
|
99
|
+
payload: {
|
|
100
|
+
clientId,
|
|
101
|
+
serverTime: createTimestamp(),
|
|
102
|
+
},
|
|
103
|
+
timestamp: createTimestamp(),
|
|
104
|
+
});
|
|
105
|
+
// Auto-auth for now (can be changed to require explicit auth)
|
|
106
|
+
this.updateState(session, { state: 'authenticating' });
|
|
107
|
+
this.handleAuth(clientId, this.options.authToken);
|
|
108
|
+
});
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
111
|
+
this.running = true;
|
|
112
|
+
console.log(`[RelayServer] Server started on ${this.options.host}:${this.options.port}`);
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
this.server.on("error", (err) => {
|
|
116
|
+
console.error("[RelayServer] Server error:", err);
|
|
117
|
+
reject(err);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
47
120
|
}
|
|
48
121
|
/**
|
|
49
122
|
* Stop the relay server
|
|
50
123
|
*/
|
|
51
124
|
async stop() {
|
|
52
|
-
this.running
|
|
53
|
-
|
|
125
|
+
if (!this.running)
|
|
126
|
+
return;
|
|
127
|
+
// Close all client connections
|
|
128
|
+
for (const session of this.clients.values()) {
|
|
129
|
+
session.ws.close();
|
|
130
|
+
}
|
|
131
|
+
this.clients.clear();
|
|
132
|
+
// Close event buffer timer
|
|
133
|
+
if (this.eventBuffer.timer) {
|
|
134
|
+
clearTimeout(this.eventBuffer.timer);
|
|
135
|
+
}
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
this.server.close(() => {
|
|
138
|
+
this.running = false;
|
|
139
|
+
console.log("[RelayServer] Server stopped");
|
|
140
|
+
resolve();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// ========================================================================
|
|
145
|
+
// State Management
|
|
146
|
+
// ========================================================================
|
|
147
|
+
updateState(session, newState) {
|
|
148
|
+
const previous = session.state;
|
|
149
|
+
session.state = newState;
|
|
150
|
+
this.options.onStateChange({
|
|
151
|
+
previous,
|
|
152
|
+
current: newState,
|
|
153
|
+
timestamp: createTimestamp(),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// ========================================================================
|
|
157
|
+
// Message Handling
|
|
158
|
+
// ========================================================================
|
|
159
|
+
handleMessage(clientId, data) {
|
|
160
|
+
const session = this.clients.get(clientId);
|
|
161
|
+
if (!session)
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
const msg = JSON.parse(data.toString());
|
|
165
|
+
switch (msg.type) {
|
|
166
|
+
case 'auth':
|
|
167
|
+
this.handleAuth(clientId, msg.payload.token);
|
|
168
|
+
break;
|
|
169
|
+
case 'ping':
|
|
170
|
+
this.handlePing(session);
|
|
171
|
+
break;
|
|
172
|
+
case 'subscribe':
|
|
173
|
+
this.handleSubscribe(session, msg);
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
const _msg = msg;
|
|
177
|
+
console.warn(`[RelayServer] Unknown message type: ${_msg?.type}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error("[RelayServer] Invalid message:", err);
|
|
182
|
+
this.sendStatus(session, 'error', 'INVALID_MESSAGE', 'Failed to parse message');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handleAuth(clientId, token) {
|
|
186
|
+
const session = this.clients.get(clientId);
|
|
187
|
+
if (!session)
|
|
188
|
+
return;
|
|
189
|
+
const success = token === this.options.authToken;
|
|
190
|
+
const response = {
|
|
191
|
+
version: PROTOCOL_VERSION,
|
|
192
|
+
type: 'auth',
|
|
193
|
+
payload: {
|
|
194
|
+
success,
|
|
195
|
+
sessionId: success ? this.serverSessionId : undefined,
|
|
196
|
+
error: success ? undefined : 'Invalid token',
|
|
197
|
+
},
|
|
198
|
+
timestamp: createTimestamp(),
|
|
199
|
+
messageId: generateMessageId(),
|
|
200
|
+
};
|
|
201
|
+
this.sendMessage(session, response);
|
|
202
|
+
if (success) {
|
|
203
|
+
this.updateState(session, { state: 'connected', sessionId: this.serverSessionId });
|
|
204
|
+
console.log(`[RelayServer] Client authenticated: ${clientId}`);
|
|
205
|
+
// Send snapshot + synchronized (t3code pattern)
|
|
206
|
+
this.sendSnapshot(session);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.updateState(session, { state: 'error', error: 'Authentication failed' });
|
|
210
|
+
session.ws.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
handlePing(session) {
|
|
214
|
+
session.lastPing = createTimestamp();
|
|
215
|
+
this.sendMessage(session, {
|
|
216
|
+
version: PROTOCOL_VERSION,
|
|
217
|
+
type: 'pong',
|
|
218
|
+
payload: {},
|
|
219
|
+
timestamp: createTimestamp(),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
handleSubscribe(session, msg) {
|
|
223
|
+
session.subscriptions = new Set(msg.payload.events);
|
|
224
|
+
console.log(`[RelayServer] Client ${session.id} subscribed to: ${Array.from(session.subscriptions).join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
// ========================================================================
|
|
227
|
+
// Snapshot + Live Streaming (t3code pattern)
|
|
228
|
+
// ========================================================================
|
|
229
|
+
/**
|
|
230
|
+
* Send initial snapshot followed by synchronized marker
|
|
231
|
+
* This is the t3code pattern: snapshot -> synchronized -> live events
|
|
232
|
+
*/
|
|
233
|
+
async sendSnapshot(session) {
|
|
234
|
+
// Build snapshot message
|
|
235
|
+
const snapshot = {
|
|
236
|
+
version: PROTOCOL_VERSION,
|
|
237
|
+
type: 'snapshot',
|
|
238
|
+
payload: {
|
|
239
|
+
sessionId: this.serverSessionId,
|
|
240
|
+
serverVersion: this.options.serverVersion,
|
|
241
|
+
connectedClients: this.clients.size,
|
|
242
|
+
recentCallbacks: this.recentCallbacks.slice(-20), // Last 20 callbacks
|
|
243
|
+
},
|
|
244
|
+
timestamp: createTimestamp(),
|
|
245
|
+
messageId: generateMessageId(),
|
|
246
|
+
};
|
|
247
|
+
// Send snapshot
|
|
248
|
+
this.sendMessage(session, snapshot);
|
|
249
|
+
// Send synchronized marker (t3code pattern: indicates snapshot complete)
|
|
250
|
+
const synchronized = {
|
|
251
|
+
version: PROTOCOL_VERSION,
|
|
252
|
+
type: 'synchronized',
|
|
253
|
+
payload: {},
|
|
254
|
+
timestamp: createTimestamp(),
|
|
255
|
+
messageId: generateMessageId(),
|
|
256
|
+
};
|
|
257
|
+
this.sendMessage(session, synchronized);
|
|
258
|
+
session.pendingSnapshot = false;
|
|
259
|
+
console.log(`[RelayServer] Snapshot sent to ${session.id}`);
|
|
54
260
|
}
|
|
55
261
|
/**
|
|
56
|
-
*
|
|
262
|
+
* Flush event buffer and send batched callbacks
|
|
57
263
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
264
|
+
flushEventBuffer() {
|
|
265
|
+
if (this.eventBuffer.callbacks.length === 0)
|
|
266
|
+
return;
|
|
267
|
+
const callbacks = this.eventBuffer.callbacks;
|
|
268
|
+
this.eventBuffer.callbacks = [];
|
|
269
|
+
this.eventBuffer.timer = null;
|
|
270
|
+
// Send each callback as a separate live event
|
|
271
|
+
for (const callback of callbacks) {
|
|
272
|
+
this.broadcastCallback(callback, false); // false = don't add to recent
|
|
273
|
+
}
|
|
60
274
|
}
|
|
275
|
+
// ========================================================================
|
|
276
|
+
// Broadcasting
|
|
277
|
+
// ========================================================================
|
|
61
278
|
/**
|
|
62
|
-
*
|
|
279
|
+
* Broadcast callback to all connected clients
|
|
280
|
+
* Uses stream batching for high-frequency events
|
|
63
281
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
282
|
+
broadcastCallback(callback, batch = true) {
|
|
283
|
+
// Add to recent callbacks (circular buffer)
|
|
284
|
+
this.recentCallbacks.push(callback);
|
|
285
|
+
if (this.recentCallbacks.length > MAX_RECENT_CALLBACKS) {
|
|
286
|
+
this.recentCallbacks.shift();
|
|
287
|
+
}
|
|
288
|
+
const message = {
|
|
289
|
+
version: PROTOCOL_VERSION,
|
|
290
|
+
type: 'callback',
|
|
291
|
+
payload: callback,
|
|
292
|
+
timestamp: createTimestamp(),
|
|
293
|
+
messageId: generateMessageId(),
|
|
294
|
+
};
|
|
295
|
+
if (batch) {
|
|
296
|
+
// Add to buffer for coalescing
|
|
297
|
+
this.eventBuffer.callbacks.push(callback);
|
|
298
|
+
if (!this.eventBuffer.timer) {
|
|
299
|
+
this.eventBuffer.timer = setTimeout(() => {
|
|
300
|
+
this.flushEventBuffer();
|
|
301
|
+
}, STREAM_BATCH_TIMEOUT_MS);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// Send immediately
|
|
306
|
+
this.broadcast(message);
|
|
307
|
+
}
|
|
66
308
|
}
|
|
67
309
|
/**
|
|
68
|
-
*
|
|
310
|
+
* Send status to all clients
|
|
69
311
|
*/
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
312
|
+
broadcastStatus(kind, code, message) {
|
|
313
|
+
const msg = {
|
|
314
|
+
version: PROTOCOL_VERSION,
|
|
315
|
+
type: 'status',
|
|
316
|
+
payload: { kind, code, message },
|
|
317
|
+
timestamp: createTimestamp(),
|
|
318
|
+
messageId: generateMessageId(),
|
|
77
319
|
};
|
|
78
|
-
this.
|
|
79
|
-
return tunnel;
|
|
320
|
+
this.broadcast(msg);
|
|
80
321
|
}
|
|
81
322
|
/**
|
|
82
|
-
*
|
|
323
|
+
* Broadcast to all subscribed clients
|
|
83
324
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
325
|
+
broadcast(message) {
|
|
326
|
+
let count = 0;
|
|
327
|
+
for (const session of this.clients.values()) {
|
|
328
|
+
if (this.isConnected(session)) {
|
|
329
|
+
this.sendMessage(session, message);
|
|
330
|
+
count++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
console.log(`[RelayServer] Broadcast ${message.type} to ${count} client(s)`);
|
|
86
334
|
}
|
|
87
335
|
/**
|
|
88
|
-
* Check if
|
|
336
|
+
* Check if client is connected and authenticated
|
|
89
337
|
*/
|
|
338
|
+
isConnected(session) {
|
|
339
|
+
return session.ws.readyState === WebSocket.OPEN &&
|
|
340
|
+
session.state.state === 'connected';
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Send a typed message to a client
|
|
344
|
+
*/
|
|
345
|
+
sendMessage(session, message) {
|
|
346
|
+
if (session.ws.readyState === WebSocket.OPEN) {
|
|
347
|
+
session.ws.send(JSON.stringify(message));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Send status to specific client
|
|
352
|
+
*/
|
|
353
|
+
sendStatus(session, kind, code, message) {
|
|
354
|
+
const msg = {
|
|
355
|
+
version: PROTOCOL_VERSION,
|
|
356
|
+
type: 'status',
|
|
357
|
+
payload: { kind, code, message },
|
|
358
|
+
timestamp: createTimestamp(),
|
|
359
|
+
messageId: generateMessageId(),
|
|
360
|
+
};
|
|
361
|
+
this.sendMessage(session, msg);
|
|
362
|
+
}
|
|
363
|
+
// ========================================================================
|
|
364
|
+
// Public API
|
|
365
|
+
// ========================================================================
|
|
90
366
|
isRunning() {
|
|
91
367
|
return this.running;
|
|
92
368
|
}
|
|
369
|
+
getClientCount() {
|
|
370
|
+
return this.clients.size;
|
|
371
|
+
}
|
|
372
|
+
getClients() {
|
|
373
|
+
return Array.from(this.clients.values()).map(s => ({
|
|
374
|
+
id: s.id,
|
|
375
|
+
state: s.state,
|
|
376
|
+
connectedAt: s.connectedAt,
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
getRecentCallbacks(limit = 20) {
|
|
380
|
+
return this.recentCallbacks.slice(-limit);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Export for use in webhook server
|
|
385
|
+
// ============================================================================
|
|
386
|
+
export function createRelayServer(options) {
|
|
387
|
+
return new RelayServer(options);
|
|
93
388
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Types
|
|
2
|
+
* Relay Types - Two-way Streaming Communication
|
|
3
3
|
*/
|
|
4
4
|
export interface RelayConfig {
|
|
5
5
|
/** Relay server URL */
|
|
@@ -27,6 +27,142 @@ export interface RelayTunnel {
|
|
|
27
27
|
/** Error message if any */
|
|
28
28
|
error?: string;
|
|
29
29
|
}
|
|
30
|
+
export type ConnectionState = {
|
|
31
|
+
state: 'disconnected';
|
|
32
|
+
} | {
|
|
33
|
+
state: 'connecting';
|
|
34
|
+
} | {
|
|
35
|
+
state: 'authenticating';
|
|
36
|
+
} | {
|
|
37
|
+
state: 'connected';
|
|
38
|
+
sessionId: string;
|
|
39
|
+
} | {
|
|
40
|
+
state: 'reconnecting';
|
|
41
|
+
attempt: number;
|
|
42
|
+
} | {
|
|
43
|
+
state: 'error';
|
|
44
|
+
error: string;
|
|
45
|
+
};
|
|
46
|
+
export interface ConnectionStateChange {
|
|
47
|
+
previous: ConnectionState;
|
|
48
|
+
current: ConnectionState;
|
|
49
|
+
timestamp: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Protocol version for typed streaming
|
|
53
|
+
*/
|
|
54
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
55
|
+
/**
|
|
56
|
+
* Generate a unique message ID
|
|
57
|
+
*/
|
|
58
|
+
export declare function generateMessageId(): string;
|
|
59
|
+
/**
|
|
60
|
+
* Create a timestamp
|
|
61
|
+
*/
|
|
62
|
+
export declare function createTimestamp(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Base message interface
|
|
65
|
+
*/
|
|
66
|
+
export interface BaseMessage<T extends string, P = unknown> {
|
|
67
|
+
version: typeof PROTOCOL_VERSION;
|
|
68
|
+
type: T;
|
|
69
|
+
payload: P;
|
|
70
|
+
timestamp: string;
|
|
71
|
+
messageId?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Server -> Client: Initial state snapshot sent on connect
|
|
75
|
+
*/
|
|
76
|
+
export interface SnapshotMessage extends BaseMessage<'snapshot'> {
|
|
77
|
+
payload: {
|
|
78
|
+
sessionId: string;
|
|
79
|
+
serverVersion: string;
|
|
80
|
+
connectedClients: number;
|
|
81
|
+
recentCallbacks: CallbackPayload[];
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Server -> Client: Synchronization marker (indicates all initial state sent)
|
|
86
|
+
*/
|
|
87
|
+
export interface SynchronizedMessage extends BaseMessage<'synchronized'> {
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Callback types from Telegram/LINE
|
|
91
|
+
*/
|
|
92
|
+
export interface CallbackPayload {
|
|
93
|
+
id: string;
|
|
94
|
+
type: 'telegram_callback' | 'line_callback';
|
|
95
|
+
action: string;
|
|
96
|
+
targetId: string;
|
|
97
|
+
userId: number;
|
|
98
|
+
username?: string;
|
|
99
|
+
messageId?: number;
|
|
100
|
+
timestamp: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Server -> Client: Live callback event (after synchronized)
|
|
104
|
+
*/
|
|
105
|
+
export interface CallbackMessage extends BaseMessage<'callback'> {
|
|
106
|
+
payload: CallbackPayload;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Server -> Client: Status update
|
|
110
|
+
*/
|
|
111
|
+
export type StatusKind = 'info' | 'warning' | 'error';
|
|
112
|
+
export interface StatusMessage extends BaseMessage<'status'> {
|
|
113
|
+
payload: {
|
|
114
|
+
kind: StatusKind;
|
|
115
|
+
code: string;
|
|
116
|
+
message: string;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Heartbeat messages
|
|
121
|
+
*/
|
|
122
|
+
export interface PingMessage extends BaseMessage<'ping'> {
|
|
123
|
+
}
|
|
124
|
+
export interface PongMessage extends BaseMessage<'pong'> {
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Authentication messages
|
|
128
|
+
*/
|
|
129
|
+
export interface AuthRequestMessage extends BaseMessage<'auth'> {
|
|
130
|
+
payload: {
|
|
131
|
+
token: string;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export interface AuthResponseMessage extends BaseMessage<'auth'> {
|
|
135
|
+
payload: {
|
|
136
|
+
success: boolean;
|
|
137
|
+
sessionId?: string;
|
|
138
|
+
error?: string;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Client -> Server: Subscribe to specific event types
|
|
143
|
+
*/
|
|
144
|
+
export interface SubscribeMessage extends BaseMessage<'subscribe'> {
|
|
145
|
+
payload: {
|
|
146
|
+
events: ('callback' | 'status' | 'sync')[];
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Connection acknowledged by server
|
|
151
|
+
*/
|
|
152
|
+
export interface ConnectedMessage extends BaseMessage<'connected'> {
|
|
153
|
+
payload: {
|
|
154
|
+
clientId: string;
|
|
155
|
+
serverTime: string;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Union type of all server -> client messages
|
|
160
|
+
*/
|
|
161
|
+
export type ServerMessage = SnapshotMessage | SynchronizedMessage | CallbackMessage | StatusMessage | PongMessage | ConnectedMessage | AuthResponseMessage;
|
|
162
|
+
/**
|
|
163
|
+
* Union type of all client -> server messages
|
|
164
|
+
*/
|
|
165
|
+
export type ClientMessage = AuthRequestMessage | PingMessage | SubscribeMessage;
|
|
30
166
|
export interface RelayMessage {
|
|
31
167
|
type: 'connect' | 'disconnect' | 'data' | 'ping' | 'pong' | 'error';
|
|
32
168
|
tunnelId?: string;
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Types
|
|
2
|
+
* Relay Types - Two-way Streaming Communication
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Typed Streaming Protocol
|
|
7
|
+
// ============================================================================
|
|
8
|
+
/**
|
|
9
|
+
* Protocol version for typed streaming
|
|
10
|
+
*/
|
|
11
|
+
export const PROTOCOL_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Generate a unique message ID
|
|
14
|
+
*/
|
|
15
|
+
export function generateMessageId() {
|
|
16
|
+
return randomUUID();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a timestamp
|
|
20
|
+
*/
|
|
21
|
+
export function createTimestamp() {
|
|
22
|
+
return new Date().toISOString();
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Relay Server - Pushes callbacks to connected clients in real-time
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* Telegram/LINE → Webhook → This Server → WebSocket → Local Client
|
|
6
|
+
*
|
|
7
|
+
* No file queue needed - real-time streaming!
|
|
8
|
+
*/
|
|
9
|
+
export interface RelayMessage {
|
|
10
|
+
type: "callback" | "ping" | "pong" | "auth";
|
|
11
|
+
payload: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface CallbackPayload {
|
|
14
|
+
type: "telegram_callback" | "line_callback";
|
|
15
|
+
action: string;
|
|
16
|
+
targetId: string;
|
|
17
|
+
userId: number;
|
|
18
|
+
username?: string;
|
|
19
|
+
messageId?: number;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* WebSocket Relay Server
|
|
24
|
+
*/
|
|
25
|
+
export declare class WsRelayServer {
|
|
26
|
+
private server;
|
|
27
|
+
private wss;
|
|
28
|
+
private clients;
|
|
29
|
+
private port;
|
|
30
|
+
private running;
|
|
31
|
+
constructor(port?: number);
|
|
32
|
+
/**
|
|
33
|
+
* Start the WebSocket server
|
|
34
|
+
*/
|
|
35
|
+
start(): Promise<void>;
|
|
36
|
+
private handleMessage;
|
|
37
|
+
/**
|
|
38
|
+
* Broadcast callback to all connected clients
|
|
39
|
+
*/
|
|
40
|
+
broadcastCallback(callback: CallbackPayload): void;
|
|
41
|
+
/**
|
|
42
|
+
* Stop the server
|
|
43
|
+
*/
|
|
44
|
+
stop(): Promise<void>;
|
|
45
|
+
isRunning(): boolean;
|
|
46
|
+
getClientCount(): number;
|
|
47
|
+
}
|
|
48
|
+
export declare function getRelayServer(): WsRelayServer;
|