pi-harness-runtime 1.1.374 → 1.1.376

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 (125) hide show
  1. package/README.md +68 -39
  2. package/package.json +2 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/dist/adapters/telegram-adapter.d.ts +5 -0
  57. package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
  58. package/packages/notification/dist/adapters/telegram-adapter.js +23 -2
  59. package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
  60. package/packages/notification/dist/notification-center.d.ts.map +1 -1
  61. package/packages/notification/dist/notification-center.js +5 -1
  62. package/packages/notification/dist/notification-center.js.map +1 -1
  63. package/packages/notification/package.json +1 -1
  64. package/packages/notification/src/adapters/telegram-adapter.ts +26 -5
  65. package/packages/notification/src/notification-center.ts +5 -1
  66. package/packages/observability/package.json +1 -1
  67. package/packages/okf-indexer/package.json +1 -1
  68. package/packages/paperclip-integration/package.json +1 -1
  69. package/packages/performance-optimizer/package.json +1 -1
  70. package/packages/permission-mode/package.json +1 -1
  71. package/packages/privilege-broker/package.json +1 -1
  72. package/packages/project-analyzer/package.json +1 -1
  73. package/packages/project-bootstrap/package.json +1 -1
  74. package/packages/projection-engine/package.json +1 -1
  75. package/packages/prompt-compiler/package.json +1 -1
  76. package/packages/prompt-versioning/package.json +1 -1
  77. package/packages/provider-adapter-sdk/package.json +1 -1
  78. package/packages/provider-router/package.json +1 -1
  79. package/packages/provider-selector/package.json +1 -1
  80. package/packages/providers/package.json +1 -1
  81. package/packages/qdrant-skills/package.json +1 -1
  82. package/packages/quota-manager/package.json +1 -1
  83. package/packages/rate-limiter/package.json +1 -1
  84. package/packages/react-vite-plugin/package.json +1 -1
  85. package/packages/relay/dist/ws-server.d.ts +48 -0
  86. package/packages/relay/dist/ws-server.js +130 -0
  87. package/packages/relay/package.json +1 -1
  88. package/packages/relay/src/ws-server.ts +165 -0
  89. package/packages/release-manager/package.json +1 -1
  90. package/packages/requirement-compiler/package.json +1 -1
  91. package/packages/runtime/package.json +1 -1
  92. package/packages/scheduler/package.json +1 -1
  93. package/packages/scheduler-adapter/package.json +1 -1
  94. package/packages/session/package.json +1 -1
  95. package/packages/session-api/package.json +1 -1
  96. package/packages/session-export/package.json +1 -1
  97. package/packages/shared-context/package.json +1 -1
  98. package/packages/skill-mcp-client/package.json +1 -1
  99. package/packages/skill-registry/package.json +1 -1
  100. package/packages/skills/package.json +1 -1
  101. package/packages/sprint-planner/package.json +1 -1
  102. package/packages/subscription-engine/package.json +1 -1
  103. package/packages/task-compiler/package.json +1 -1
  104. package/packages/telegram/package.json +1 -1
  105. package/packages/tencentdb-memory/package.json +1 -1
  106. package/packages/tencentdb-sync/package.json +1 -1
  107. package/packages/test-data-generator/package.json +1 -1
  108. package/packages/test-generator/package.json +1 -1
  109. package/packages/todo-bd-sync/package.json +1 -1
  110. package/packages/token-estimation/package.json +1 -1
  111. package/packages/token-optimizer/package.json +1 -1
  112. package/packages/trajectory/package.json +1 -1
  113. package/packages/tui/package.json +1 -1
  114. package/packages/types/package.json +1 -1
  115. package/packages/voice-api/package.json +1 -1
  116. package/packages/webmcp-tester/package.json +1 -1
  117. package/packages/wiki/package.json +1 -1
  118. package/packages/workflow-events/package.json +1 -1
  119. package/packages/workspace-scanner/package.json +1 -1
  120. package/packages/worktree/package.json +1 -1
  121. package/packages/write-review/package.json +1 -1
  122. package/scripts/relay-client.ts +175 -0
  123. package/scripts/sync-callback-queue.ts +57 -0
  124. package/scripts/telegram-callback-processor.ts +16 -0
  125. package/scripts/unified-webhook-server.ts +344 -0
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Real-time Callback Relay Client
4
+ *
5
+ * Connects to WebSocket server on VPS to receive callbacks instantly.
6
+ * No file queue sync needed!
7
+ *
8
+ * Usage:
9
+ * RELAY_WS_URL=wss://webhook.bunchee.online:8444 \
10
+ * RELAY_AUTH_TOKEN=change-me-in-production \
11
+ * bun run scripts/relay-client.ts
12
+ */
13
+
14
+ import { WebSocket } from "ws";
15
+ import { readFileSync, writeFileSync, existsSync } from "fs";
16
+ import { join } from "path";
17
+ import { homedir } from "os";
18
+
19
+ // Configuration
20
+ const RELAY_WS_URL = process.env.RELAY_WS_URL || "wss://webhook.bunchee.online:8444";
21
+ 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
+ const JOB_PIPE_FILE = join(homedir(), ".pi-harness-runtime", "job-commands.jsonl");
24
+
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;
43
+
44
+ function loadKeys() {
45
+ const keysDir = join(homedir(), ".pi-harness-runtime", "keys");
46
+ return {
47
+ telegramBotToken: readFileSync(join(keysDir, "telegram-bot-token.txt"), "utf8").trim(),
48
+ telegramAllowedUsers: readFileSync(join(keysDir, "telegram-allowed-users.txt"), "utf8").trim(),
49
+ };
50
+ }
51
+
52
+ function writeToJobPipe(command: { command: string; jobId: string; userId: number; timestamp: string }): void {
53
+ const line = JSON.stringify(command) + "\n";
54
+ writeFileSync(JOB_PIPE_FILE, line, { flag: "a" });
55
+ console.log(`[RelayClient] Wrote to job pipe: ${command.command} ${command.jobId}`);
56
+ }
57
+
58
+ async function sendTelegramMessage(chatId: number, text: string): Promise<void> {
59
+ const keys = loadKeys();
60
+ await fetch(`https://api.telegram.org/bot${keys.telegramBotToken}/sendMessage`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({ chat_id: chatId, text }),
64
+ });
65
+ }
66
+
67
+ async function processCallback(callback: CallbackPayload): Promise<void> {
68
+ console.log(`[RelayClient] Processing: ${callback.type} - ${callback.action} ${callback.targetId}`);
69
+
70
+ switch (callback.action) {
71
+ case "approve":
72
+ writeToJobPipe({
73
+ command: "resume",
74
+ jobId: callback.targetId,
75
+ userId: callback.userId,
76
+ timestamp: new Date().toISOString(),
77
+ });
78
+ await sendTelegramMessage(callback.userId, `✅ Approved! Job ${callback.targetId} will resume.`);
79
+ break;
80
+
81
+ case "reject":
82
+ writeToJobPipe({
83
+ command: "cancel",
84
+ jobId: callback.targetId,
85
+ userId: callback.userId,
86
+ timestamp: new Date().toISOString(),
87
+ });
88
+ await sendTelegramMessage(callback.userId, `❌ Rejected. Job ${callback.targetId} cancelled.`);
89
+ break;
90
+
91
+ case "sayhi":
92
+ await sendTelegramMessage(
93
+ callback.userId,
94
+ "👋 Hi! Two-way communication working! (Real-time via WebSocket)"
95
+ );
96
+ break;
97
+
98
+ default:
99
+ console.log(`[RelayClient] Unknown action: ${callback.action}`);
100
+ }
101
+ }
102
+
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);
147
+ }
148
+ });
149
+
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
+ });
156
+
157
+ ws.on("error", (err) => {
158
+ console.error("[RelayClient] Error:", err.message);
159
+ });
160
+ }
161
+
162
+ // Send ping every 30s to keep alive
163
+ setInterval(() => {
164
+ if (ws?.readyState === WebSocket.OPEN) {
165
+ ws.send(JSON.stringify({ type: "ping", payload: {} }));
166
+ }
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("===========================================");
174
+
175
+ connect();
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Syncs callback queue from VPS to local for processing.
4
+ * Run this alongside telegram-callback-processor.ts
5
+ *
6
+ * Usage:
7
+ * bun run scripts/sync-callback-queue.ts
8
+ *
9
+ * Or set up a cron job:
10
+ * * * * * * bun /home/frappe/pi-harness-runtime/scripts/sync-callback-queue.ts
11
+ */
12
+
13
+ import { execSync } from "child_process";
14
+ import { readFileSync, writeFileSync, existsSync } from "fs";
15
+ import { join } from "path";
16
+
17
+ const VPS_HOST = process.env.VPS_HOST || "frappe@217.216.37.251";
18
+ const VPS_QUEUE = "/home/frappe/.pi-harness-runtime/callback-queue.jsonl";
19
+ const LOCAL_QUEUE = join(process.env.HOME || "/home/frappe", ".pi-harness-runtime", "callback-queue.jsonl");
20
+
21
+ function syncQueue(): number {
22
+ try {
23
+ // SSH to get VPS queue content
24
+ const vpsQueueContent = execSync(`ssh -o BatchMode=yes ${VPS_HOST} "cat ${VPS_QUEUE}"`, { encoding: "utf8" });
25
+
26
+ if (!vpsQueueContent.trim()) {
27
+ return 0; // Nothing to sync
28
+ }
29
+
30
+ // Append to local queue
31
+ const existing = existsSync(LOCAL_QUEUE) ? readFileSync(LOCAL_QUEUE, "utf8") : "";
32
+ const newQueue = existing + vpsQueueContent;
33
+ writeFileSync(LOCAL_QUEUE, newQueue);
34
+
35
+ // Clear VPS queue
36
+ execSync(`ssh -o BatchMode=yes ${VPS_HOST} "truncate -s 0 ${VPS_QUEUE}"`);
37
+
38
+ const count = vpsQueueContent.split("\n").filter(l => l.trim()).length;
39
+ if (count > 0) {
40
+ console.log(`[Sync] Synced ${count} callback(s) from VPS`);
41
+ }
42
+ return count;
43
+ } catch (err) {
44
+ console.error("[Sync] Error:", err);
45
+ return 0;
46
+ }
47
+ }
48
+
49
+ // Run once and exit (for cron) unless --watch is passed
50
+ const isWatch = process.argv.includes("--watch");
51
+
52
+ if (isWatch) {
53
+ console.log("[Sync] Watching VPS queue every 500ms...");
54
+ setInterval(syncQueue, 500);
55
+ } else {
56
+ syncQueue();
57
+ }
@@ -89,6 +89,22 @@ export async function processCallbacks(): Promise<number> {
89
89
  break;
90
90
  }
91
91
 
92
+ case "sayhi": {
93
+ console.log(`👋 SAYHI: ${item.targetId} from @${item.username || item.userId}`);
94
+ // Send greeting response via Telegram
95
+ const botToken = process.env.TELEGRAM_BOT_TOKEN || readFileSync(join(process.env.HOME || "", ".pi-harness-runtime", "keys", "telegram-bot-token.txt"), "utf-8").trim();
96
+ const chatId = item.userId;
97
+ await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({
101
+ chat_id: chatId,
102
+ text: "👋 Hi! Two-way communication is working! Click buttons to interact."
103
+ })
104
+ });
105
+ break;
106
+ }
107
+
92
108
  default: {
93
109
  console.log(`❓ UNKNOWN: ${item.action}_${item.targetId}`);
94
110
  if (onUnknown) {
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Unified Webhook Server — Handles Telegram, LINE, and WebSocket relay
4
+ *
5
+ * Routes:
6
+ * /telegram → Telegram webhook handler
7
+ * /line → LINE webhook handler
8
+ * /ws → WebSocket relay (port 8444)
9
+ *
10
+ * Usage:
11
+ * bun run scripts/unified-webhook-server.ts
12
+ */
13
+
14
+ import { existsSync, readFileSync, appendFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { homedir } from "os";
17
+ import { createServer, IncomingMessage, ServerResponse } from "node:http";
18
+ import { WebSocketServer, WebSocket } from "ws";
19
+ import { randomUUID } from "node:crypto";
20
+
21
+ // --- Configuration ---
22
+ interface Config {
23
+ telegram?: {
24
+ botToken: string;
25
+ secret: string;
26
+ allowedUsers: Set<number>;
27
+ };
28
+ line?: {
29
+ accessToken: string;
30
+ channelSecret: string;
31
+ };
32
+ }
33
+
34
+ const PORT = parseInt(process.env.PORT || "8443", 10);
35
+ 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
37
+
38
+ // --- Keys Loader ---
39
+ function loadConfig(): Config {
40
+ const keysDir = join(homedir(), ".pi-harness-runtime", "keys");
41
+ const config: Config = {};
42
+
43
+ // Telegram
44
+ const telegramTokenPath = join(keysDir, "telegram-bot-token.txt");
45
+ const telegramSecretPath = join(keysDir, "telegram-webhook-secret.txt");
46
+ const telegramUsersPath = join(keysDir, "telegram-allowed-users.txt");
47
+
48
+ if (existsSync(telegramTokenPath)) {
49
+ const token = readFileSync(telegramTokenPath, "utf8").trim();
50
+ const secret = existsSync(telegramSecretPath)
51
+ ? readFileSync(telegramSecretPath, "utf8").trim()
52
+ : process.env.TELEGRAM_WEBHOOK_SECRET || "";
53
+
54
+ const allowedUsers = new Set<number>();
55
+ if (existsSync(telegramUsersPath)) {
56
+ const users = readFileSync(telegramUsersPath, "utf8").trim();
57
+ for (const id of users.split(",").map((s) => parseInt(s.trim(), 10))) {
58
+ if (!isNaN(id) && id > 0) allowedUsers.add(id);
59
+ }
60
+ }
61
+
62
+ config.telegram = { botToken: token, secret, allowedUsers };
63
+ }
64
+
65
+ // LINE
66
+ const lineTokenPath = join(keysDir, "line-access-token.txt");
67
+ const lineSecretPath = join(keysDir, "line-channel-secret.txt");
68
+
69
+ if (existsSync(lineTokenPath) && existsSync(lineSecretPath)) {
70
+ config.line = {
71
+ accessToken: readFileSync(lineTokenPath, "utf8").trim(),
72
+ channelSecret: readFileSync(lineSecretPath, "utf8").trim(),
73
+ };
74
+ }
75
+
76
+ return config;
77
+ }
78
+
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
+ // --- LINE Verification ---
91
+ async function verifyLINE(body: string, channelSecret: string, xLineSignature: string): Promise<boolean> {
92
+ const encoder = new TextEncoder();
93
+ const key = await crypto.subtle.importKey(
94
+ "raw", encoder.encode(channelSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
95
+ );
96
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
97
+ const expected = Buffer.from(signature).toString("base64");
98
+ return expected === xLineSignature;
99
+ }
100
+
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
+ }
118
+ }
119
+ return count;
120
+ }
121
+
122
+ function startWebSocketServer() {
123
+ const wss = new WebSocketServer({ port: WS_PORT });
124
+
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
+ });
147
+
148
+ console.log(`[WS] WebSocket server started on port ${WS_PORT}`);
149
+ }
150
+
151
+ // --- Telegram Handler ---
152
+ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config: Config) {
153
+ if (!config.telegram) {
154
+ res.writeHead(404);
155
+ res.end("Telegram not configured");
156
+ return;
157
+ }
158
+
159
+ // Verify secret token
160
+ const secretToken = req.headers["x-telegram-bot-api-secret-token"];
161
+ if (secretToken !== config.telegram.secret) {
162
+ res.writeHead(403);
163
+ res.end("Forbidden");
164
+ return;
165
+ }
166
+
167
+ // Collect body
168
+ const chunks: Buffer[] = [];
169
+ for await (const chunk of req) chunks.push(chunk);
170
+ const body = Buffer.concat(chunks).toString();
171
+
172
+ try {
173
+ const update = JSON.parse(body);
174
+
175
+ // Handle callback query (button click)
176
+ if (update.callback_query) {
177
+ const query = update.callback_query;
178
+ const from = query.from;
179
+
180
+ // Check authorization
181
+ if (config.telegram.allowedUsers.size > 0 && !config.telegram.allowedUsers.has(from.id)) {
182
+ res.writeHead(403);
183
+ res.end("Forbidden");
184
+ return;
185
+ }
186
+
187
+ // 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
+ }
202
+
203
+ const callback = {
204
+ type: "telegram_callback",
205
+ action,
206
+ 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})`);
219
+
220
+ // Acknowledge immediately
221
+ await fetch(`https://api.telegram.org/bot${config.telegram.botToken}/answerCallbackQuery`, {
222
+ method: "POST",
223
+ headers: { "Content-Type": "application/json" },
224
+ body: JSON.stringify({ callback_query_id: query.id }),
225
+ });
226
+ }
227
+
228
+ res.writeHead(200);
229
+ res.end("OK");
230
+ } catch (err) {
231
+ console.error("[Telegram] Error:", err);
232
+ res.writeHead(500);
233
+ res.end("Error");
234
+ }
235
+ }
236
+
237
+ // --- LINE Handler ---
238
+ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Config) {
239
+ if (!config.line) {
240
+ res.writeHead(404);
241
+ res.end("LINE not configured");
242
+ return;
243
+ }
244
+
245
+ // Verify signature
246
+ const signature = req.headers["x-line-signature"] as string;
247
+ if (!signature) {
248
+ res.writeHead(400);
249
+ res.end("Bad Request");
250
+ return;
251
+ }
252
+
253
+ // Collect body
254
+ const chunks: Buffer[] = [];
255
+ for await (const chunk of req) chunks.push(chunk);
256
+ const body = Buffer.concat(chunks).toString();
257
+
258
+ // Verify
259
+ if (!(await verifyLINE(body, config.line.channelSecret, signature))) {
260
+ res.writeHead(403);
261
+ res.end("Forbidden");
262
+ return;
263
+ }
264
+
265
+ try {
266
+ const bodyJson = JSON.parse(body);
267
+ const events = bodyJson.events || [];
268
+
269
+ for (const event of events) {
270
+ if (event.type === "postback") {
271
+ 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) : "";
275
+
276
+ const callback = {
277
+ type: "line_callback",
278
+ action,
279
+ 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})`);
290
+ }
291
+ }
292
+
293
+ res.writeHead(200);
294
+ res.end(JSON.stringify({ status: "ok" }));
295
+ } catch (err) {
296
+ console.error("[LINE] Error:", err);
297
+ res.writeHead(500);
298
+ res.end("Error");
299
+ }
300
+ }
301
+
302
+ // --- Main ---
303
+ const config = loadConfig();
304
+
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("===========================================");
311
+
312
+ // Start WebSocket relay
313
+ startWebSocketServer();
314
+
315
+ // HTTP webhook server
316
+ const server = createServer(async (req, res) => {
317
+ const url = new URL(req.url || "/", `http://localhost:${PORT}`);
318
+
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
+ });