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,26 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Unified Webhook Server — Handles
|
|
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
|
|
7
13
|
* /line → LINE webhook handler
|
|
14
|
+
* /ws → WebSocket relay (port 8444)
|
|
8
15
|
*
|
|
9
16
|
* Usage:
|
|
10
17
|
* bun run scripts/unified-webhook-server.ts
|
|
11
|
-
*
|
|
12
|
-
* Environment variables (or keys/*.txt files):
|
|
13
|
-
* TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, TELEGRAM_ALLOWED_USERS
|
|
14
|
-
* LINE_CHANNEL_SECRET, LINE_ACCESS_TOKEN
|
|
15
|
-
*
|
|
16
|
-
* Webhook URLs to configure:
|
|
17
|
-
* Telegram: https://webhook.bunchee.online/telegram
|
|
18
|
-
* LINE: https://webhook.bunchee.online/line
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
|
-
import { existsSync, readFileSync
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
21
|
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "os";
|
|
23
23
|
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
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";
|
|
24
28
|
|
|
25
29
|
// --- Configuration ---
|
|
26
30
|
interface Config {
|
|
@@ -30,77 +34,58 @@ interface Config {
|
|
|
30
34
|
allowedUsers: Set<number>;
|
|
31
35
|
};
|
|
32
36
|
line?: {
|
|
33
|
-
channelSecret: string;
|
|
34
37
|
accessToken: string;
|
|
38
|
+
channelSecret: string;
|
|
35
39
|
};
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const PORT = parseInt(process.env.PORT || "8443", 10);
|
|
43
|
+
const WS_PORT = parseInt(process.env.WS_PORT || "8444", 10);
|
|
44
|
+
const AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
|
|
45
|
+
const SERVER_VERSION = process.env.SERVER_VERSION || "1.0.0";
|
|
46
|
+
|
|
47
|
+
// --- Keys Loader ---
|
|
48
|
+
function loadConfig(): Config {
|
|
49
|
+
const keysDir = join(homedir(), ".pi-harness-runtime", "keys");
|
|
41
50
|
const config: Config = {};
|
|
42
51
|
|
|
43
|
-
// Telegram
|
|
52
|
+
// Telegram
|
|
44
53
|
const telegramTokenPath = join(keysDir, "telegram-bot-token.txt");
|
|
45
54
|
const telegramSecretPath = join(keysDir, "telegram-webhook-secret.txt");
|
|
46
55
|
const telegramUsersPath = join(keysDir, "telegram-allowed-users.txt");
|
|
47
56
|
|
|
48
57
|
if (existsSync(telegramTokenPath)) {
|
|
49
|
-
const token =
|
|
50
|
-
const secret =
|
|
58
|
+
const token = readFileSync(telegramTokenPath, "utf8").trim();
|
|
59
|
+
const secret = existsSync(telegramSecretPath)
|
|
60
|
+
? readFileSync(telegramSecretPath, "utf8").trim()
|
|
61
|
+
: process.env.TELEGRAM_WEBHOOK_SECRET || "";
|
|
51
62
|
|
|
52
63
|
const allowedUsers = new Set<number>();
|
|
53
|
-
const envUsers = process.env.TELEGRAM_ALLOWED_USERS;
|
|
54
|
-
if (envUsers) {
|
|
55
|
-
for (const part of envUsers.split(",")) {
|
|
56
|
-
const num = parseInt(part.trim(), 10);
|
|
57
|
-
if (!isNaN(num) && num > 0) allowedUsers.add(num);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
64
|
if (existsSync(telegramUsersPath)) {
|
|
61
|
-
const
|
|
62
|
-
for (const
|
|
63
|
-
|
|
64
|
-
if (!isNaN(num) && num > 0) allowedUsers.add(num);
|
|
65
|
+
const users = readFileSync(telegramUsersPath, "utf8").trim();
|
|
66
|
+
for (const id of users.split(",").map((s) => parseInt(s.trim(), 10))) {
|
|
67
|
+
if (!isNaN(id) && id > 0) allowedUsers.add(id);
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
config.telegram = { botToken: token, secret, allowedUsers };
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
// LINE
|
|
72
|
-
const lineSecretPath = join(keysDir, "line-channel-secret.txt");
|
|
74
|
+
// LINE
|
|
73
75
|
const lineTokenPath = join(keysDir, "line-access-token.txt");
|
|
76
|
+
const lineSecretPath = join(keysDir, "line-channel-secret.txt");
|
|
74
77
|
|
|
75
|
-
if (existsSync(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
if (existsSync(lineTokenPath) && existsSync(lineSecretPath)) {
|
|
79
|
+
config.line = {
|
|
80
|
+
accessToken: readFileSync(lineTokenPath, "utf8").trim(),
|
|
81
|
+
channelSecret: readFileSync(lineSecretPath, "utf8").trim(),
|
|
82
|
+
};
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
return config;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
// ---
|
|
85
|
-
const LOG_FILE = "/tmp/unified-webhook.log";
|
|
86
|
-
function log(prefix: string, ...args: unknown[]) {
|
|
87
|
-
const msg = args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ");
|
|
88
|
-
const line = `[${new Date().toISOString()}] [${prefix}] ${msg}\n`;
|
|
89
|
-
appendFileSync(LOG_FILE, line);
|
|
90
|
-
console.log(line.trim());
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// --- Crypto utilities ---
|
|
94
|
-
async function verifyTelegram(data: string, secret: string, hash: string): Promise<boolean> {
|
|
95
|
-
const encoder = new TextEncoder();
|
|
96
|
-
const key = await crypto.subtle.importKey(
|
|
97
|
-
"raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
|
|
98
|
-
);
|
|
99
|
-
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
100
|
-
const expected = Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, "0")).join("");
|
|
101
|
-
return expected === hash;
|
|
102
|
-
}
|
|
103
|
-
|
|
88
|
+
// --- LINE Verification ---
|
|
104
89
|
async function verifyLINE(body: string, channelSecret: string, xLineSignature: string): Promise<boolean> {
|
|
105
90
|
const encoder = new TextEncoder();
|
|
106
91
|
const key = await crypto.subtle.importKey(
|
|
@@ -111,14 +96,49 @@ async function verifyLINE(body: string, channelSecret: string, xLineSignature: s
|
|
|
111
96
|
return expected === xLineSignature;
|
|
112
97
|
}
|
|
113
98
|
|
|
114
|
-
// --- Callback
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { action: data, targetId: "" };
|
|
117
|
+
}
|
|
118
|
+
|
|
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
|
+
};
|
|
118
138
|
}
|
|
119
139
|
|
|
120
|
-
// --- Telegram
|
|
121
|
-
async function handleTelegram(req: IncomingMessage, res: ServerResponse, config: Config) {
|
|
140
|
+
// --- Telegram Handler ---
|
|
141
|
+
async function handleTelegram(req: IncomingMessage, res: ServerResponse, config: Config, relayServer: ReturnType<typeof createRelayServer>) {
|
|
122
142
|
if (!config.telegram) {
|
|
123
143
|
res.writeHead(404);
|
|
124
144
|
res.end("Telegram not configured");
|
|
@@ -128,7 +148,6 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
|
|
|
128
148
|
// Verify secret token
|
|
129
149
|
const secretToken = req.headers["x-telegram-bot-api-secret-token"];
|
|
130
150
|
if (secretToken !== config.telegram.secret) {
|
|
131
|
-
log("Telegram", "Unauthorized - secret mismatch");
|
|
132
151
|
res.writeHead(403);
|
|
133
152
|
res.end("Forbidden");
|
|
134
153
|
return;
|
|
@@ -149,37 +168,27 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
|
|
|
149
168
|
|
|
150
169
|
// Check authorization
|
|
151
170
|
if (config.telegram.allowedUsers.size > 0 && !config.telegram.allowedUsers.has(from.id)) {
|
|
152
|
-
log("Telegram", `Unauthorized user: ${from.id}`);
|
|
153
171
|
res.writeHead(403);
|
|
154
172
|
res.end("Forbidden");
|
|
155
173
|
return;
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
// Parse callback data
|
|
159
|
-
const
|
|
160
|
-
const colonIdx = data.indexOf(":");
|
|
161
|
-
const underscoreIdx = data.indexOf("_");
|
|
162
|
-
let action: string, targetId: string;
|
|
163
|
-
if (colonIdx !== -1) {
|
|
164
|
-
action = data.slice(0, colonIdx);
|
|
165
|
-
targetId = data.slice(colonIdx + 1).replace(/_+$/, "");
|
|
166
|
-
} else if (underscoreIdx !== -1) {
|
|
167
|
-
action = data.slice(0, underscoreIdx);
|
|
168
|
-
targetId = data.slice(underscoreIdx + 1).replace(/_+$/, "");
|
|
169
|
-
} else {
|
|
170
|
-
action = data;
|
|
171
|
-
targetId = "";
|
|
172
|
-
}
|
|
177
|
+
const { action, targetId } = parseCallbackData(query.data || "");
|
|
173
178
|
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
// Create typed callback payload
|
|
180
|
+
const callback = createCallbackPayload(
|
|
181
|
+
"telegram_callback",
|
|
177
182
|
action,
|
|
178
183
|
targetId,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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}`);
|
|
183
192
|
|
|
184
193
|
// Acknowledge immediately
|
|
185
194
|
await fetch(`https://api.telegram.org/bot${config.telegram.botToken}/answerCallbackQuery`, {
|
|
@@ -187,21 +196,19 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
|
|
|
187
196
|
headers: { "Content-Type": "application/json" },
|
|
188
197
|
body: JSON.stringify({ callback_query_id: query.id }),
|
|
189
198
|
});
|
|
190
|
-
|
|
191
|
-
log("Telegram", `Callback: ${action} ${targetId} from @${from.username || from.id}`);
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
res.writeHead(200);
|
|
195
202
|
res.end("OK");
|
|
196
203
|
} catch (err) {
|
|
197
|
-
|
|
204
|
+
console.error("[Telegram] Error:", err);
|
|
198
205
|
res.writeHead(500);
|
|
199
206
|
res.end("Error");
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
// --- LINE
|
|
204
|
-
async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Config) {
|
|
210
|
+
// --- LINE Handler ---
|
|
211
|
+
async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Config, relayServer: ReturnType<typeof createRelayServer>) {
|
|
205
212
|
if (!config.line) {
|
|
206
213
|
res.writeHead(404);
|
|
207
214
|
res.end("LINE not configured");
|
|
@@ -211,7 +218,6 @@ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Con
|
|
|
211
218
|
// Verify signature
|
|
212
219
|
const signature = req.headers["x-line-signature"] as string;
|
|
213
220
|
if (!signature) {
|
|
214
|
-
log("LINE", "Missing signature");
|
|
215
221
|
res.writeHead(400);
|
|
216
222
|
res.end("Bad Request");
|
|
217
223
|
return;
|
|
@@ -224,108 +230,117 @@ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Con
|
|
|
224
230
|
|
|
225
231
|
// Verify
|
|
226
232
|
if (!(await verifyLINE(body, config.line.channelSecret, signature))) {
|
|
227
|
-
log("LINE", "Invalid signature");
|
|
228
233
|
res.writeHead(403);
|
|
229
234
|
res.end("Forbidden");
|
|
230
235
|
return;
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
try {
|
|
234
|
-
const
|
|
239
|
+
const bodyJson = JSON.parse(body);
|
|
240
|
+
const events = bodyJson.events || [];
|
|
235
241
|
|
|
236
242
|
for (const event of events) {
|
|
237
243
|
if (event.type === "postback") {
|
|
238
|
-
// Parse postback data
|
|
239
244
|
const data = event.postback?.data || "";
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
} else {
|
|
246
|
-
action = data;
|
|
247
|
-
targetId = "";
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
queueCallback({
|
|
251
|
-
type: "line_callback",
|
|
245
|
+
const { action, targetId } = parseCallbackData(data);
|
|
246
|
+
|
|
247
|
+
// Create typed callback payload
|
|
248
|
+
const callback = createCallbackPayload(
|
|
249
|
+
"line_callback",
|
|
252
250
|
action,
|
|
253
251
|
targetId,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Forward text messages to agent
|
|
261
|
-
queueCallback({
|
|
262
|
-
type: "line_message",
|
|
263
|
-
text: event.message.text,
|
|
264
|
-
userId: event.source?.userId,
|
|
265
|
-
replyToken: event.replyToken,
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
log("LINE", `Message: ${event.message.text}`);
|
|
252
|
+
event.source?.userId || "unknown"
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Broadcast via typed streaming relay
|
|
256
|
+
relayServer.broadcastCallback(callback);
|
|
257
|
+
console.log(`[LINE] Postback: ${action} ${targetId}`);
|
|
269
258
|
}
|
|
270
259
|
}
|
|
271
260
|
|
|
272
261
|
res.writeHead(200);
|
|
273
|
-
res.end("
|
|
262
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
274
263
|
} catch (err) {
|
|
275
|
-
|
|
264
|
+
console.error("[LINE] Error:", err);
|
|
276
265
|
res.writeHead(500);
|
|
277
266
|
res.end("Error");
|
|
278
267
|
}
|
|
279
268
|
}
|
|
280
269
|
|
|
281
|
-
// --- Main
|
|
282
|
-
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
283
|
-
|
|
270
|
+
// --- Main ---
|
|
284
271
|
async function main() {
|
|
285
|
-
const config =
|
|
272
|
+
const config = loadConfig();
|
|
286
273
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
console.log("Unified Webhook Server");
|
|
293
|
-
console.log(`Listening on port ${PORT}`);
|
|
274
|
+
console.log("===========================================");
|
|
275
|
+
console.log("Unified Webhook Server - Streaming v2");
|
|
276
|
+
console.log("===========================================");
|
|
294
277
|
if (config.telegram) console.log(" /telegram - Telegram webhook");
|
|
295
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
|
+
};
|
|
293
|
+
|
|
294
|
+
const relayServer = createRelayServer(relayOptions);
|
|
296
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
|
|
297
301
|
const server = createServer(async (req, res) => {
|
|
298
302
|
const url = new URL(req.url || "/", `http://localhost:${PORT}`);
|
|
299
303
|
|
|
300
304
|
try {
|
|
301
305
|
if (url.pathname === "/telegram" && req.method === "POST") {
|
|
302
|
-
await handleTelegram(req, res, config);
|
|
306
|
+
await handleTelegram(req, res, config, relayServer);
|
|
303
307
|
} else if (url.pathname === "/line" && req.method === "POST") {
|
|
304
|
-
await handleLINE(req, res, config);
|
|
308
|
+
await handleLINE(req, res, config, relayServer);
|
|
305
309
|
} else if (url.pathname === "/health") {
|
|
306
310
|
res.writeHead(200);
|
|
307
|
-
res.end(
|
|
311
|
+
res.end(JSON.stringify({
|
|
312
|
+
status: "ok",
|
|
313
|
+
clients: relayServer.getClientCount(),
|
|
314
|
+
running: relayServer.isRunning(),
|
|
315
|
+
}));
|
|
308
316
|
} else {
|
|
309
317
|
res.writeHead(404);
|
|
310
318
|
res.end("Not Found");
|
|
311
319
|
}
|
|
312
320
|
} catch (err) {
|
|
313
|
-
|
|
321
|
+
console.error("[Server] Error:", err);
|
|
314
322
|
res.writeHead(500);
|
|
315
323
|
res.end("Error");
|
|
316
324
|
}
|
|
317
325
|
});
|
|
318
326
|
|
|
319
327
|
server.listen(PORT, () => {
|
|
320
|
-
console.log(`
|
|
321
|
-
console.log(
|
|
322
|
-
if (config.telegram)
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
if (config.line) {
|
|
326
|
-
console.log(` LINE: https://webhook.bunchee.online/line`);
|
|
327
|
-
}
|
|
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`);
|
|
328
332
|
});
|
|
333
|
+
|
|
334
|
+
// Graceful shutdown
|
|
335
|
+
const shutdown = async () => {
|
|
336
|
+
console.log("\nShutting down...");
|
|
337
|
+
await relayServer.stop();
|
|
338
|
+
server.close();
|
|
339
|
+
process.exit(0);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
process.on("SIGINT", shutdown);
|
|
343
|
+
process.on("SIGTERM", shutdown);
|
|
329
344
|
}
|
|
330
345
|
|
|
331
|
-
main();
|
|
346
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket Relay Server - Two-way streaming callbacks
|
|
4
|
+
*
|
|
5
|
+
* Uses the relay package's typed streaming protocol:
|
|
6
|
+
* - Snapshot + Live event pattern (t3code style)
|
|
7
|
+
* - Typed message protocol
|
|
8
|
+
* - Connection state machine
|
|
9
|
+
* - Stream coalescing for high-frequency events
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* Telegram/LINE → Webhook → This Server → WebSocket → Local Client
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createRelayServer, type RelayServerOptions } from "../packages/relay/src/server";
|
|
16
|
+
|
|
17
|
+
// Configuration from environment
|
|
18
|
+
const PORT = parseInt(process.env.WS_PORT || "8444", 10);
|
|
19
|
+
const HOST = process.env.WS_HOST || "0.0.0.0";
|
|
20
|
+
const AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
|
|
21
|
+
const SERVER_VERSION = process.env.SERVER_VERSION || "1.0.0";
|
|
22
|
+
|
|
23
|
+
// Create and start server
|
|
24
|
+
const options: RelayServerOptions = {
|
|
25
|
+
port: PORT,
|
|
26
|
+
host: HOST,
|
|
27
|
+
authToken: AUTH_TOKEN,
|
|
28
|
+
serverVersion: SERVER_VERSION,
|
|
29
|
+
onTunnelConnect: (tunnel) => {
|
|
30
|
+
console.log(`[Main] Tunnel connected: ${tunnel.id}`);
|
|
31
|
+
},
|
|
32
|
+
onTunnelDisconnect: (tunnelId) => {
|
|
33
|
+
console.log(`[Main] Tunnel disconnected: ${tunnelId}`);
|
|
34
|
+
},
|
|
35
|
+
onStateChange: (change) => {
|
|
36
|
+
console.log(`[Main] State change: ${change.previous.state} → ${change.current.state}`);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const server = createRelayServer(options);
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
console.log("===========================================");
|
|
44
|
+
console.log("WebSocket Relay Server - Streaming v2");
|
|
45
|
+
console.log("===========================================");
|
|
46
|
+
console.log(` Host: ${HOST}`);
|
|
47
|
+
console.log(` Port: ${PORT}`);
|
|
48
|
+
console.log(` Auth: ${AUTH_TOKEN ? "enabled" : "disabled"}`);
|
|
49
|
+
console.log(` Version: ${SERVER_VERSION}`);
|
|
50
|
+
console.log("===========================================");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await server.start();
|
|
54
|
+
console.log("Server started successfully!");
|
|
55
|
+
|
|
56
|
+
// Log status periodically
|
|
57
|
+
setInterval(() => {
|
|
58
|
+
const clients = server.getClients();
|
|
59
|
+
console.log(`[Status] Clients: ${clients.length}, Running: ${server.isRunning()}`);
|
|
60
|
+
}, 30000);
|
|
61
|
+
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error("Failed to start server:", err);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Graceful shutdown
|
|
69
|
+
process.on("SIGINT", async () => {
|
|
70
|
+
console.log("\nShutting down...");
|
|
71
|
+
await server.stop();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
process.on("SIGTERM", async () => {
|
|
76
|
+
console.log("\nShutting down...");
|
|
77
|
+
await server.stop();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
main();
|