pi-harness-runtime 1.1.376 → 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.
Files changed (123) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/mcp-adapter/package.json +1 -1
  51. package/packages/memory-engine/package.json +1 -1
  52. package/packages/milestone-manager/package.json +1 -1
  53. package/packages/model-registry/package.json +1 -1
  54. package/packages/nextjs-plugin/package.json +1 -1
  55. package/packages/notification/package.json +1 -1
  56. package/packages/observability/package.json +1 -1
  57. package/packages/okf-indexer/package.json +1 -1
  58. package/packages/paperclip-integration/package.json +1 -1
  59. package/packages/performance-optimizer/package.json +1 -1
  60. package/packages/permission-mode/package.json +1 -1
  61. package/packages/privilege-broker/package.json +1 -1
  62. package/packages/project-analyzer/package.json +1 -1
  63. package/packages/project-bootstrap/package.json +1 -1
  64. package/packages/projection-engine/package.json +1 -1
  65. package/packages/prompt-compiler/package.json +1 -1
  66. package/packages/prompt-versioning/package.json +1 -1
  67. package/packages/provider-adapter-sdk/package.json +1 -1
  68. package/packages/provider-router/package.json +1 -1
  69. package/packages/provider-selector/package.json +1 -1
  70. package/packages/providers/package.json +1 -1
  71. package/packages/qdrant-skills/package.json +1 -1
  72. package/packages/quota-manager/package.json +1 -1
  73. package/packages/rate-limiter/package.json +1 -1
  74. package/packages/react-vite-plugin/package.json +1 -1
  75. package/packages/relay/dist/client.d.ts +56 -35
  76. package/packages/relay/dist/client.js +278 -71
  77. package/packages/relay/dist/index.d.ts +12 -5
  78. package/packages/relay/dist/index.js +12 -4
  79. package/packages/relay/dist/server.d.ts +58 -20
  80. package/packages/relay/dist/server.js +344 -49
  81. package/packages/relay/dist/types.d.ts +137 -1
  82. package/packages/relay/dist/types.js +21 -2
  83. package/packages/relay/package.json +1 -1
  84. package/packages/relay/src/client.ts +375 -81
  85. package/packages/relay/src/index.ts +42 -5
  86. package/packages/relay/src/server.ts +452 -54
  87. package/packages/relay/src/types.ts +174 -1
  88. package/packages/release-manager/package.json +1 -1
  89. package/packages/requirement-compiler/package.json +1 -1
  90. package/packages/runtime/package.json +1 -1
  91. package/packages/scheduler/package.json +1 -1
  92. package/packages/scheduler-adapter/package.json +1 -1
  93. package/packages/session/package.json +1 -1
  94. package/packages/session-api/package.json +1 -1
  95. package/packages/session-export/package.json +1 -1
  96. package/packages/shared-context/package.json +1 -1
  97. package/packages/skill-mcp-client/package.json +1 -1
  98. package/packages/skill-registry/package.json +1 -1
  99. package/packages/skills/package.json +1 -1
  100. package/packages/sprint-planner/package.json +1 -1
  101. package/packages/subscription-engine/package.json +1 -1
  102. package/packages/task-compiler/package.json +1 -1
  103. package/packages/telegram/package.json +1 -1
  104. package/packages/tencentdb-memory/package.json +1 -1
  105. package/packages/tencentdb-sync/package.json +1 -1
  106. package/packages/test-data-generator/package.json +1 -1
  107. package/packages/test-generator/package.json +1 -1
  108. package/packages/todo-bd-sync/package.json +1 -1
  109. package/packages/token-estimation/package.json +1 -1
  110. package/packages/token-optimizer/package.json +1 -1
  111. package/packages/trajectory/package.json +1 -1
  112. package/packages/tui/package.json +1 -1
  113. package/packages/types/package.json +1 -1
  114. package/packages/voice-api/package.json +1 -1
  115. package/packages/webmcp-tester/package.json +1 -1
  116. package/packages/wiki/package.json +1 -1
  117. package/packages/workflow-events/package.json +1 -1
  118. package/packages/workspace-scanner/package.json +1 -1
  119. package/packages/worktree/package.json +1 -1
  120. package/packages/write-review/package.json +1 -1
  121. package/scripts/relay-client.ts +105 -96
  122. package/scripts/unified-webhook-server.ts +145 -143
  123. package/scripts/ws-server.ts +81 -0
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Real-time Callback Relay Client
3
+ * Real-time Callback Relay Client - Two-way streaming
4
4
  *
5
- * Connects to WebSocket server on VPS to receive callbacks instantly.
6
- * No file queue sync needed!
5
+ * Uses the relay package's typed streaming protocol:
6
+ * - Snapshot + Live event pattern (t3code style)
7
+ * - Connection state machine with automatic reconnection
8
+ * - Typed message handling
7
9
  *
8
10
  * Usage:
9
11
  * RELAY_WS_URL=wss://webhook.bunchee.online:8444 \
@@ -11,35 +13,20 @@
11
13
  * bun run scripts/relay-client.ts
12
14
  */
13
15
 
14
- import { WebSocket } from "ws";
15
16
  import { readFileSync, writeFileSync, existsSync } from "fs";
16
17
  import { join } from "path";
17
18
  import { homedir } from "os";
19
+ import { RelayClient } from "../packages/relay/src/client";
20
+ import type { CallbackPayload, SnapshotMessage } from "../packages/relay/src/types";
18
21
 
19
22
  // Configuration
20
23
  const RELAY_WS_URL = process.env.RELAY_WS_URL || "wss://webhook.bunchee.online:8444";
21
24
  const RELAY_AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
22
- const CALLBACK_QUEUE_FILE = join(homedir(), ".pi-harness-runtime", "callback-queue.jsonl");
23
25
  const JOB_PIPE_FILE = join(homedir(), ".pi-harness-runtime", "job-commands.jsonl");
24
26
 
25
- interface RelayMessage {
26
- type: string;
27
- payload: unknown;
28
- }
29
-
30
- interface CallbackPayload {
31
- type: "telegram_callback" | "line_callback";
32
- action: string;
33
- targetId: string;
34
- userId: number;
35
- username?: string;
36
- messageId?: number;
37
- timestamp: string;
38
- }
39
-
40
- let ws: WebSocket | null = null;
41
- let reconnectDelay = 1000;
42
- let authenticated = false;
27
+ // ============================================================================
28
+ // Telegram Integration
29
+ // ============================================================================
43
30
 
44
31
  function loadKeys() {
45
32
  const keysDir = join(homedir(), ".pi-harness-runtime", "keys");
@@ -52,7 +39,7 @@ function loadKeys() {
52
39
  function writeToJobPipe(command: { command: string; jobId: string; userId: number; timestamp: string }): void {
53
40
  const line = JSON.stringify(command) + "\n";
54
41
  writeFileSync(JOB_PIPE_FILE, line, { flag: "a" });
55
- console.log(`[RelayClient] Wrote to job pipe: ${command.command} ${command.jobId}`);
42
+ console.log(`[Client] Wrote to job pipe: ${command.command} ${command.jobId}`);
56
43
  }
57
44
 
58
45
  async function sendTelegramMessage(chatId: number, text: string): Promise<void> {
@@ -64,8 +51,12 @@ async function sendTelegramMessage(chatId: number, text: string): Promise<void>
64
51
  });
65
52
  }
66
53
 
54
+ // ============================================================================
55
+ // Callback Processing
56
+ // ============================================================================
57
+
67
58
  async function processCallback(callback: CallbackPayload): Promise<void> {
68
- console.log(`[RelayClient] Processing: ${callback.type} - ${callback.action} ${callback.targetId}`);
59
+ console.log(`[Client] Processing: ${callback.type} - ${callback.action} ${callback.targetId}`);
69
60
 
70
61
  switch (callback.action) {
71
62
  case "approve":
@@ -75,7 +66,7 @@ async function processCallback(callback: CallbackPayload): Promise<void> {
75
66
  userId: callback.userId,
76
67
  timestamp: new Date().toISOString(),
77
68
  });
78
- await sendTelegramMessage(callback.userId, `✅ Approved! Job ${callback.targetId} will resume.`);
69
+ await sendTelegramMessage(callback.userId, `Approved! Job ${callback.targetId} will resume.`);
79
70
  break;
80
71
 
81
72
  case "reject":
@@ -85,91 +76,109 @@ async function processCallback(callback: CallbackPayload): Promise<void> {
85
76
  userId: callback.userId,
86
77
  timestamp: new Date().toISOString(),
87
78
  });
88
- await sendTelegramMessage(callback.userId, `❌ Rejected. Job ${callback.targetId} cancelled.`);
79
+ await sendTelegramMessage(callback.userId, `Rejected. Job ${callback.targetId} cancelled.`);
89
80
  break;
90
81
 
91
82
  case "sayhi":
92
83
  await sendTelegramMessage(
93
84
  callback.userId,
94
- "👋 Hi! Two-way communication working! (Real-time via WebSocket)"
85
+ "Hi! Two-way streaming communication working!"
95
86
  );
96
87
  break;
97
88
 
98
89
  default:
99
- console.log(`[RelayClient] Unknown action: ${callback.action}`);
90
+ console.log(`[Client] Unknown action: ${callback.action}`);
100
91
  }
101
92
  }
102
93
 
103
- function connect(): void {
104
- console.log(`[RelayClient] Connecting to ${RELAY_WS_URL}...`);
105
-
106
- ws = new WebSocket(RELAY_WS_URL);
107
-
108
- ws.on("open", () => {
109
- console.log("[RelayClient] Connected!");
110
- reconnectDelay = 1000;
111
-
112
- // Authenticate
113
- ws!.send(JSON.stringify({ type: "auth", payload: RELAY_AUTH_TOKEN }));
114
- });
115
-
116
- ws.on("message", async (data) => {
117
- try {
118
- const msg = JSON.parse(data.toString()) as RelayMessage;
119
-
120
- switch (msg.type) {
121
- case "connected":
122
- console.log("[RelayClient] Server acknowledged connection");
123
- break;
124
-
125
- case "auth":
126
- const authResult = msg.payload as { success: boolean; error?: string };
127
- if (authResult.success) {
128
- authenticated = true;
129
- console.log("[RelayClient] Authenticated!");
130
- } else {
131
- console.error("[RelayClient] Auth failed:", authResult.error);
132
- ws?.close();
133
- }
134
- break;
135
-
136
- case "callback":
137
- if (!authenticated) break;
138
- await processCallback(msg.payload as CallbackPayload);
139
- break;
140
-
141
- case "pong":
142
- // Heartbeat response
143
- break;
144
- }
145
- } catch (err) {
146
- console.error("[RelayClient] Parse error:", err);
94
+ // ============================================================================
95
+ // Main
96
+ // ============================================================================
97
+
98
+ async function main() {
99
+ console.log("===========================================");
100
+ console.log("Real-time Callback Relay Client - Streaming v2");
101
+ console.log("===========================================");
102
+ console.log(`Server: ${RELAY_WS_URL}`);
103
+ console.log("===========================================");
104
+
105
+ // Create client with typed callbacks
106
+ const client = new RelayClient(
107
+ {
108
+ serverUrl: RELAY_WS_URL,
109
+ token: RELAY_AUTH_TOKEN,
110
+ keepAlive: 30000,
111
+ },
112
+ {
113
+ onConnected: (sessionId) => {
114
+ console.log(`[Client] Connected! Session: ${sessionId}`);
115
+ },
116
+
117
+ onDisconnected: () => {
118
+ console.log("[Client] Disconnected");
119
+ },
120
+
121
+ onSnapshot: (snapshot: SnapshotMessage["payload"]) => {
122
+ console.log(`[Client] Snapshot received:`);
123
+ console.log(` - Session: ${snapshot.sessionId}`);
124
+ console.log(` - Version: ${snapshot.serverVersion}`);
125
+ console.log(` - Connected clients: ${snapshot.connectedClients}`);
126
+ console.log(` - Recent callbacks: ${snapshot.recentCallbacks.length}`);
127
+
128
+ // Process any recent callbacks from snapshot
129
+ for (const callback of snapshot.recentCallbacks) {
130
+ processCallback(callback).catch(console.error);
131
+ }
132
+ },
133
+
134
+ onSynchronized: () => {
135
+ console.log("[Client] Synchronized - receiving live events");
136
+ },
137
+
138
+ onCallback: (callback: CallbackPayload) => {
139
+ console.log(`[Client] Live callback: ${callback.action} ${callback.targetId}`);
140
+ processCallback(callback).catch(console.error);
141
+ },
142
+
143
+ onStatus: (status) => {
144
+ console.log(`[Client] Status [${status.kind}]: ${status.code} - ${status.message}`);
145
+ },
146
+
147
+ onError: (error) => {
148
+ console.error("[Client] Error:", error.message);
149
+ },
150
+
151
+ onStateChange: (change) => {
152
+ const prev = change.previous.state;
153
+ const curr = change.current.state;
154
+ if (prev !== curr) {
155
+ console.log(`[Client] State: ${prev} → ${curr}`);
156
+ }
157
+ },
147
158
  }
148
- });
159
+ );
149
160
 
150
- ws.on("close", () => {
151
- authenticated = false;
152
- console.log(`[RelayClient] Disconnected. Reconnecting in ${reconnectDelay}ms...`);
153
- setTimeout(connect, reconnectDelay);
154
- reconnectDelay = Math.min(reconnectDelay * 2, 30000); // Max 30s
155
- });
161
+ // Handle graceful shutdown
162
+ const shutdown = () => {
163
+ console.log("\nShutting down...");
164
+ client.disconnect();
165
+ process.exit(0);
166
+ };
156
167
 
157
- ws.on("error", (err) => {
158
- console.error("[RelayClient] Error:", err.message);
159
- });
160
- }
168
+ process.on("SIGINT", shutdown);
169
+ process.on("SIGTERM", shutdown);
161
170
 
162
- // Send ping every 30s to keep alive
163
- setInterval(() => {
164
- if (ws?.readyState === WebSocket.OPEN) {
165
- ws.send(JSON.stringify({ type: "ping", payload: {} }));
171
+ // Connect
172
+ try {
173
+ await client.connect();
174
+ console.log("[Client] Connection established!");
175
+
176
+ // Keep running
177
+ await new Promise(() => {});
178
+ } catch (err) {
179
+ console.error("[Client] Failed to connect:", err);
180
+ process.exit(1);
166
181
  }
167
- }, 30000);
168
-
169
- console.log("===========================================");
170
- console.log("Real-time Callback Relay Client");
171
- console.log("===========================================");
172
- console.log(`Server: ${RELAY_WS_URL}`);
173
- console.log("===========================================");
182
+ }
174
183
 
175
- connect();
184
+ main();
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * Unified Webhook Server — Handles Telegram, LINE, and WebSocket relay
4
+ *
5
+ * Two-way streaming implementation using relay package:
6
+ * - Snapshot + Live event pattern (t3code style)
7
+ * - Typed message protocol
8
+ * - Connection state machine
9
+ * - Stream coalescing for high-frequency events
4
10
  *
5
11
  * Routes:
6
12
  * /telegram → Telegram webhook handler
@@ -11,12 +17,14 @@
11
17
  * bun run scripts/unified-webhook-server.ts
12
18
  */
13
19
 
14
- import { existsSync, readFileSync, appendFileSync } from "node:fs";
20
+ import { existsSync, readFileSync } from "node:fs";
15
21
  import { join } from "node:path";
16
22
  import { homedir } from "os";
17
23
  import { createServer, IncomingMessage, ServerResponse } from "node:http";
18
- import { WebSocketServer, WebSocket } from "ws";
19
- import { randomUUID } from "node:crypto";
24
+ import { createRelayServer } from "../packages/relay/src/server";
25
+ import { generateMessageId } from "../packages/relay/src/types";
26
+ import type { CallbackPayload } from "../packages/relay/src/types";
27
+ import type { RelayServerOptions } from "../packages/relay/src/server";
20
28
 
21
29
  // --- Configuration ---
22
30
  interface Config {
@@ -33,7 +41,8 @@ interface Config {
33
41
 
34
42
  const PORT = parseInt(process.env.PORT || "8443", 10);
35
43
  const WS_PORT = parseInt(process.env.WS_PORT || "8444", 10);
36
- const RELAY_AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "b61940936a2625036d6faf7fe32c52b3"; // Same as webhook secret
44
+ const AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
45
+ const SERVER_VERSION = process.env.SERVER_VERSION || "1.0.0";
37
46
 
38
47
  // --- Keys Loader ---
39
48
  function loadConfig(): Config {
@@ -76,17 +85,6 @@ function loadConfig(): Config {
76
85
  return config;
77
86
  }
78
87
 
79
- // --- Telegram HMAC Verification (for future use with secret hash) ---
80
- async function _verifyTelegramHmac(data: string, secret: string, hash: string): Promise<boolean> {
81
- const encoder = new TextEncoder();
82
- const key = await crypto.subtle.importKey(
83
- "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
84
- );
85
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
86
- const expected = Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, "0")).join("");
87
- return expected === hash;
88
- }
89
-
90
88
  // --- LINE Verification ---
91
89
  async function verifyLINE(body: string, channelSecret: string, xLineSignature: string): Promise<boolean> {
92
90
  const encoder = new TextEncoder();
@@ -98,58 +96,49 @@ async function verifyLINE(body: string, channelSecret: string, xLineSignature: s
98
96
  return expected === xLineSignature;
99
97
  }
100
98
 
101
- // --- Callback Queue (file-based fallback) ---
102
- const QUEUE_FILE = join(process.env.HOME || "/tmp", ".pi-harness-runtime", "callback-queue.jsonl");
103
- function queueCallback(callback: Record<string, unknown>) {
104
- appendFileSync(QUEUE_FILE, JSON.stringify({ ...callback, timestamp: new Date().toISOString() }) + "\n");
105
- }
106
-
107
- // --- WebSocket Relay ---
108
- const wsClients = new Map<string, WebSocket>();
109
-
110
- function broadcastCallback(callback: Record<string, unknown>) {
111
- const msg = JSON.stringify({ type: "callback", payload: callback });
112
- let count = 0;
113
- for (const ws of wsClients.values()) {
114
- if (ws.readyState === WebSocket.OPEN) {
115
- ws.send(msg);
116
- count++;
117
- }
99
+ // --- Parse Callback Data ---
100
+ function parseCallbackData(data: string): { action: string; targetId: string } {
101
+ const colonIdx = data.indexOf(":");
102
+ const underscoreIdx = data.indexOf("_");
103
+
104
+ if (colonIdx !== -1) {
105
+ return {
106
+ action: data.slice(0, colonIdx),
107
+ targetId: data.slice(colonIdx + 1).replace(/_+$/, ""),
108
+ };
109
+ } else if (underscoreIdx !== -1) {
110
+ return {
111
+ action: data.slice(0, underscoreIdx),
112
+ targetId: data.slice(underscoreIdx + 1).replace(/_+$/, ""),
113
+ };
118
114
  }
119
- return count;
120
- }
121
-
122
- function startWebSocketServer() {
123
- const wss = new WebSocketServer({ port: WS_PORT });
124
115
 
125
- wss.on("connection", (ws: WebSocket) => {
126
- const clientId = randomUUID();
127
- wsClients.set(clientId, ws);
128
- console.log(`[WS] Client connected: ${clientId}`);
129
-
130
- ws.on("message", (data) => {
131
- try {
132
- const msg = JSON.parse(data.toString());
133
- if (msg.type === "auth" && msg.payload === RELAY_AUTH_TOKEN) {
134
- ws.send(JSON.stringify({ type: "auth", payload: { success: true } }));
135
- console.log(`[WS] Client authenticated: ${clientId}`);
136
- }
137
- } catch (err) {
138
- console.error(`[WS] Invalid message from ${clientId}:`, err);
139
- }
140
- });
141
-
142
- ws.on("close", () => {
143
- wsClients.delete(clientId);
144
- console.log(`[WS] Client disconnected: ${clientId}`);
145
- });
146
- });
116
+ return { action: data, targetId: "" };
117
+ }
147
118
 
148
- console.log(`[WS] WebSocket server started on port ${WS_PORT}`);
119
+ // --- Create Callback Payload ---
120
+ function createCallbackPayload(
121
+ type: "telegram_callback" | "line_callback",
122
+ action: string,
123
+ targetId: string,
124
+ userId: number | string,
125
+ username?: string,
126
+ messageId?: number
127
+ ): CallbackPayload {
128
+ return {
129
+ id: generateMessageId(),
130
+ type,
131
+ action,
132
+ targetId,
133
+ userId: typeof userId === "string" ? parseInt(userId, 10) || 0 : userId,
134
+ username,
135
+ messageId,
136
+ timestamp: new Date().toISOString(),
137
+ };
149
138
  }
150
139
 
151
140
  // --- Telegram Handler ---
152
- async function handleTelegram(req: IncomingMessage, res: ServerResponse, config: Config) {
141
+ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config: Config, relayServer: ReturnType<typeof createRelayServer>) {
153
142
  if (!config.telegram) {
154
143
  res.writeHead(404);
155
144
  res.end("Telegram not configured");
@@ -185,37 +174,21 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
185
174
  }
186
175
 
187
176
  // Parse callback data
188
- const data = query.data || "";
189
- const colonIdx = data.indexOf(":");
190
- const underscoreIdx = data.indexOf("_");
191
- let action: string, targetId: string;
192
- if (colonIdx !== -1) {
193
- action = data.slice(0, colonIdx);
194
- targetId = data.slice(colonIdx + 1).replace(/_+$/, "");
195
- } else if (underscoreIdx !== -1) {
196
- action = data.slice(0, underscoreIdx);
197
- targetId = data.slice(underscoreIdx + 1).replace(/_+$/, "");
198
- } else {
199
- action = data;
200
- targetId = "";
201
- }
177
+ const { action, targetId } = parseCallbackData(query.data || "");
202
178
 
203
- const callback = {
204
- type: "telegram_callback",
179
+ // Create typed callback payload
180
+ const callback = createCallbackPayload(
181
+ "telegram_callback",
205
182
  action,
206
183
  targetId,
207
- userId: from.id,
208
- username: from.username,
209
- messageId: query.message?.message_id,
210
- };
211
-
212
- // Broadcast via WebSocket
213
- const wsCount = broadcastCallback(callback);
214
- if (wsCount === 0) {
215
- // Fallback to file queue
216
- queueCallback(callback);
217
- }
218
- console.log(`[Telegram] Callback: ${action} ${targetId} from @${from.username || from.id} (WS clients: ${wsCount})`);
184
+ from.id,
185
+ from.username,
186
+ query.message?.message_id
187
+ );
188
+
189
+ // Broadcast via typed streaming relay
190
+ relayServer.broadcastCallback(callback);
191
+ console.log(`[Telegram] Callback: ${action} ${targetId} from @${from.username || from.id}`);
219
192
 
220
193
  // Acknowledge immediately
221
194
  await fetch(`https://api.telegram.org/bot${config.telegram.botToken}/answerCallbackQuery`, {
@@ -235,7 +208,7 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
235
208
  }
236
209
 
237
210
  // --- LINE Handler ---
238
- async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Config) {
211
+ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Config, relayServer: ReturnType<typeof createRelayServer>) {
239
212
  if (!config.line) {
240
213
  res.writeHead(404);
241
214
  res.end("LINE not configured");
@@ -269,24 +242,19 @@ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Con
269
242
  for (const event of events) {
270
243
  if (event.type === "postback") {
271
244
  const data = event.postback?.data || "";
272
- const colonIdx = data.indexOf(":");
273
- let action = data.slice(0, colonIdx !== -1 ? colonIdx : undefined);
274
- let targetId = colonIdx !== -1 ? data.slice(colonIdx + 1) : "";
245
+ const { action, targetId } = parseCallbackData(data);
275
246
 
276
- const callback = {
277
- type: "line_callback",
247
+ // Create typed callback payload
248
+ const callback = createCallbackPayload(
249
+ "line_callback",
278
250
  action,
279
251
  targetId,
280
- userId: event.source?.userId,
281
- timestamp: new Date().toISOString(),
282
- };
283
-
284
- // Broadcast via WebSocket
285
- const wsCount = broadcastCallback(callback);
286
- if (wsCount === 0) {
287
- queueCallback(callback);
288
- }
289
- console.log(`[LINE] Postback: ${action} ${targetId} (WS clients: ${wsCount})`);
252
+ event.source?.userId || "unknown"
253
+ );
254
+
255
+ // Broadcast via typed streaming relay
256
+ relayServer.broadcastCallback(callback);
257
+ console.log(`[LINE] Postback: ${action} ${targetId}`);
290
258
  }
291
259
  }
292
260
 
@@ -300,45 +268,79 @@ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Con
300
268
  }
301
269
 
302
270
  // --- Main ---
303
- const config = loadConfig();
271
+ async function main() {
272
+ const config = loadConfig();
273
+
274
+ console.log("===========================================");
275
+ console.log("Unified Webhook Server - Streaming v2");
276
+ console.log("===========================================");
277
+ if (config.telegram) console.log(" /telegram - Telegram webhook");
278
+ if (config.line) console.log(" /line - LINE webhook");
279
+ console.log(` HTTP: http://0.0.0.0:${PORT}`);
280
+ console.log(` WebSocket: ws://0.0.0.0:${WS_PORT}`);
281
+ console.log("===========================================");
282
+
283
+ // Create relay server with typed streaming
284
+ const relayOptions: RelayServerOptions = {
285
+ port: WS_PORT,
286
+ host: "0.0.0.0",
287
+ authToken: AUTH_TOKEN,
288
+ serverVersion: SERVER_VERSION,
289
+ onStateChange: (change: { previous: { state: string }; current: { state: string }; timestamp: string }) => {
290
+ console.log(`[Relay] State: ${change.previous.state} → ${change.current.state}`);
291
+ },
292
+ };
304
293
 
305
- console.log("===========================================");
306
- console.log("Unified Webhook Server");
307
- console.log("===========================================");
308
- if (config.telegram) console.log(" /telegram - Telegram webhook");
309
- if (config.line) console.log(" /line - LINE webhook");
310
- console.log("===========================================");
294
+ const relayServer = createRelayServer(relayOptions);
295
+
296
+ // Start WebSocket relay
297
+ await relayServer.start();
298
+ console.log("[Relay] WebSocket server started on port", WS_PORT);
299
+
300
+ // HTTP webhook server
301
+ const server = createServer(async (req, res) => {
302
+ const url = new URL(req.url || "/", `http://localhost:${PORT}`);
303
+
304
+ try {
305
+ if (url.pathname === "/telegram" && req.method === "POST") {
306
+ await handleTelegram(req, res, config, relayServer);
307
+ } else if (url.pathname === "/line" && req.method === "POST") {
308
+ await handleLINE(req, res, config, relayServer);
309
+ } else if (url.pathname === "/health") {
310
+ res.writeHead(200);
311
+ res.end(JSON.stringify({
312
+ status: "ok",
313
+ clients: relayServer.getClientCount(),
314
+ running: relayServer.isRunning(),
315
+ }));
316
+ } else {
317
+ res.writeHead(404);
318
+ res.end("Not Found");
319
+ }
320
+ } catch (err) {
321
+ console.error("[Server] Error:", err);
322
+ res.writeHead(500);
323
+ res.end("Error");
324
+ }
325
+ });
311
326
 
312
- // Start WebSocket relay
313
- startWebSocketServer();
327
+ server.listen(PORT, () => {
328
+ console.log(`HTTP server running on http://0.0.0.0:${PORT}`);
329
+ console.log("Configure webhooks:");
330
+ if (config.telegram) console.log(` Telegram: https://webhook.bunchee.online/telegram`);
331
+ if (config.line) console.log(` LINE: https://webhook.bunchee.online/line`);
332
+ });
314
333
 
315
- // HTTP webhook server
316
- const server = createServer(async (req, res) => {
317
- const url = new URL(req.url || "/", `http://localhost:${PORT}`);
334
+ // Graceful shutdown
335
+ const shutdown = async () => {
336
+ console.log("\nShutting down...");
337
+ await relayServer.stop();
338
+ server.close();
339
+ process.exit(0);
340
+ };
318
341
 
319
- try {
320
- if (url.pathname === "/telegram" && req.method === "POST") {
321
- await handleTelegram(req, res, config);
322
- } else if (url.pathname === "/line" && req.method === "POST") {
323
- await handleLINE(req, res, config);
324
- } else if (url.pathname === "/health") {
325
- res.writeHead(200);
326
- res.end("OK");
327
- } else {
328
- res.writeHead(404);
329
- res.end("Not Found");
330
- }
331
- } catch (err) {
332
- console.error("[Server] Error:", err);
333
- res.writeHead(500);
334
- res.end("Error");
335
- }
336
- });
337
-
338
- server.listen(PORT, () => {
339
- console.log(`Server running on http://0.0.0.0:${PORT}`);
340
- console.log(`WebSocket relay on ws://0.0.0.0:${WS_PORT}`);
341
- console.log("Configure webhooks:");
342
- if (config.telegram) console.log(` Telegram: https://webhook.bunchee.online/telegram`);
343
- if (config.line) console.log(` LINE: https://webhook.bunchee.online/line`);
344
- });
342
+ process.on("SIGINT", shutdown);
343
+ process.on("SIGTERM", shutdown);
344
+ }
345
+
346
+ main().catch(console.error);