@llblab/pi-telegram 0.18.4 → 0.18.6
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/AGENTS.md +8 -5
- package/BACKLOG.md +13 -34
- package/CHANGELOG.md +24 -1
- package/README.md +1 -1
- package/docs/multi-instance-bus.md +26 -5
- package/index.ts +56 -4
- package/lib/bindings.ts +3 -14
- package/lib/bus-follower.ts +66 -46
- package/lib/bus-leader.ts +171 -47
- package/lib/bus-transport.ts +227 -0
- package/lib/bus.ts +214 -46
- package/lib/polling.ts +58 -5
- package/lib/preview.ts +20 -39
- package/lib/routing.ts +5 -11
- package/lib/runtime-log.ts +19 -1
- package/lib/runtime.ts +23 -7
- package/lib/status.ts +52 -2
- package/lib/threads.ts +56 -2
- package/lib/updates.ts +43 -2
- package/package.json +1 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bus local transport boundary
|
|
3
|
+
* Zones: multi-instance bus, IPC transport, Windows named pipes, Unix sockets
|
|
4
|
+
* Owns endpoint derivation, transport-kind detection, retry/error classification, and small timing policy.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { createConnection } from "node:net";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
export type TelegramBusTransportKind = "pipe" | "socket";
|
|
12
|
+
|
|
13
|
+
export type TelegramBusTransportEventRecorder = (
|
|
14
|
+
phase: string,
|
|
15
|
+
details: Record<string, unknown>,
|
|
16
|
+
) => void;
|
|
17
|
+
|
|
18
|
+
export type TelegramBusTransportEndpointDiagnostics = Record<
|
|
19
|
+
string,
|
|
20
|
+
unknown
|
|
21
|
+
> & {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
transport: TelegramBusTransportKind;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface TelegramBusTransportRetryPolicy {
|
|
27
|
+
attempts: number;
|
|
28
|
+
delayMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TelegramBusTransportRetryPolicyOverrides {
|
|
32
|
+
attempts?: number;
|
|
33
|
+
delayMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TelegramBusTransportOperation = "registration" | "operation";
|
|
37
|
+
|
|
38
|
+
export const TELEGRAM_BUS_REGISTRATION_RETRY: TelegramBusTransportRetryPolicy = {
|
|
39
|
+
attempts: 10,
|
|
40
|
+
delayMs: 150,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const TELEGRAM_BUS_OPERATION_RETRY: TelegramBusTransportRetryPolicy = {
|
|
44
|
+
attempts: 3,
|
|
45
|
+
delayMs: 100,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const TELEGRAM_BUS_PIPE_REGISTRATION_RETRY =
|
|
49
|
+
TELEGRAM_BUS_REGISTRATION_RETRY;
|
|
50
|
+
|
|
51
|
+
export const TELEGRAM_BUS_PIPE_OPERATION_RETRY = TELEGRAM_BUS_OPERATION_RETRY;
|
|
52
|
+
|
|
53
|
+
export function getTelegramBusPipePath(input: {
|
|
54
|
+
agentDir: string;
|
|
55
|
+
scope: string;
|
|
56
|
+
}): string {
|
|
57
|
+
const digest = createHash("sha256")
|
|
58
|
+
.update(resolve(input.agentDir))
|
|
59
|
+
.digest("base64url")
|
|
60
|
+
.slice(0, 16);
|
|
61
|
+
const scope = input.scope.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
|
|
62
|
+
return `\\\\.\\pipe\\pi-telegram-${digest}-${scope}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isTelegramBusPipePath(endpoint: string): boolean {
|
|
66
|
+
return /^\\\\[.?]\\pipe\\/i.test(endpoint);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getTelegramBusTransportKind(
|
|
70
|
+
endpoint: string,
|
|
71
|
+
): TelegramBusTransportKind {
|
|
72
|
+
return isTelegramBusPipePath(endpoint) ? "pipe" : "socket";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getTelegramBusEndpointDiagnostics(
|
|
76
|
+
endpoint: string,
|
|
77
|
+
): TelegramBusTransportEndpointDiagnostics {
|
|
78
|
+
return {
|
|
79
|
+
endpoint,
|
|
80
|
+
transport: getTelegramBusTransportKind(endpoint),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getTelegramBusTransportRetryPolicy(input: {
|
|
85
|
+
endpoint: string;
|
|
86
|
+
operation: TelegramBusTransportOperation;
|
|
87
|
+
overrides?: TelegramBusTransportRetryPolicyOverrides;
|
|
88
|
+
}): TelegramBusTransportRetryPolicy | undefined {
|
|
89
|
+
const base =
|
|
90
|
+
input.operation === "registration"
|
|
91
|
+
? TELEGRAM_BUS_REGISTRATION_RETRY
|
|
92
|
+
: isTelegramBusPipePath(input.endpoint)
|
|
93
|
+
? TELEGRAM_BUS_OPERATION_RETRY
|
|
94
|
+
: undefined;
|
|
95
|
+
if (!base && !input.overrides) return undefined;
|
|
96
|
+
return {
|
|
97
|
+
attempts: Math.max(1, input.overrides?.attempts ?? base?.attempts ?? 1),
|
|
98
|
+
delayMs: Math.max(0, input.overrides?.delayMs ?? base?.delayMs ?? 0),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getTelegramBusLeaderEndpoint(input: {
|
|
103
|
+
agentDir: string;
|
|
104
|
+
platform: NodeJS.Platform | string;
|
|
105
|
+
}): string {
|
|
106
|
+
return input.platform === "win32"
|
|
107
|
+
? getTelegramBusPipePath({ agentDir: input.agentDir, scope: "bus" })
|
|
108
|
+
: join(input.agentDir, "tmp", "telegram", "bus.sock");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getTelegramBusFollowerEndpoint(input: {
|
|
112
|
+
agentDir: string;
|
|
113
|
+
platform: NodeJS.Platform | string;
|
|
114
|
+
instanceId: string;
|
|
115
|
+
}): string {
|
|
116
|
+
return input.platform === "win32"
|
|
117
|
+
? getTelegramBusPipePath({
|
|
118
|
+
agentDir: input.agentDir,
|
|
119
|
+
scope: `follower-${input.instanceId}`,
|
|
120
|
+
})
|
|
121
|
+
: join(
|
|
122
|
+
input.agentDir,
|
|
123
|
+
"tmp",
|
|
124
|
+
"telegram",
|
|
125
|
+
"followers",
|
|
126
|
+
`${input.instanceId.replace(/[^a-zA-Z0-9_.-]/g, "_")}.sock`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface TelegramBusTransportErrorInfo {
|
|
131
|
+
message: string;
|
|
132
|
+
code?: string;
|
|
133
|
+
syscall?: string;
|
|
134
|
+
kind: "connect" | "timeout" | "auth" | "protocol" | "unknown";
|
|
135
|
+
retryable: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function classifyTelegramBusTransportError(
|
|
139
|
+
error: unknown,
|
|
140
|
+
): TelegramBusTransportErrorInfo {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
const maybeNodeError = error as NodeJS.ErrnoException;
|
|
143
|
+
const code =
|
|
144
|
+
typeof maybeNodeError?.code === "string" ? maybeNodeError.code : undefined;
|
|
145
|
+
const syscall =
|
|
146
|
+
typeof maybeNodeError?.syscall === "string"
|
|
147
|
+
? maybeNodeError.syscall
|
|
148
|
+
: undefined;
|
|
149
|
+
const timeout = code === "ETIMEDOUT" || /timed out/i.test(message);
|
|
150
|
+
const retryable =
|
|
151
|
+
code === "ENOENT" ||
|
|
152
|
+
code === "ECONNREFUSED" ||
|
|
153
|
+
code === "EPIPE" ||
|
|
154
|
+
code === "ECONNRESET" ||
|
|
155
|
+
code === "EBUSY" ||
|
|
156
|
+
timeout;
|
|
157
|
+
const kind = timeout
|
|
158
|
+
? "timeout"
|
|
159
|
+
: code === "ENOENT" || code === "ECONNREFUSED"
|
|
160
|
+
? "connect"
|
|
161
|
+
: "unknown";
|
|
162
|
+
return { message, code, syscall, kind, retryable };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function isRetryableTelegramBusTransportError(error: unknown): boolean {
|
|
166
|
+
return classifyTelegramBusTransportError(error).retryable;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createTelegramBusTransportTimeoutError(
|
|
170
|
+
message: string,
|
|
171
|
+
): NodeJS.ErrnoException {
|
|
172
|
+
const error = new Error(message) as NodeJS.ErrnoException;
|
|
173
|
+
error.code = "ETIMEDOUT";
|
|
174
|
+
return error;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function delayTelegramBusTransportRetry(ms: number): Promise<void> {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const timer = setTimeout(resolve, ms);
|
|
180
|
+
timer.unref?.();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface TelegramBusTransportProbeResult
|
|
185
|
+
extends TelegramBusTransportEndpointDiagnostics {
|
|
186
|
+
reachable: boolean;
|
|
187
|
+
error?: TelegramBusTransportErrorInfo;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function probeTelegramBusEndpoint(input: {
|
|
191
|
+
endpoint: string;
|
|
192
|
+
timeoutMs?: number;
|
|
193
|
+
}): Promise<TelegramBusTransportProbeResult> {
|
|
194
|
+
const timeoutMs = input.timeoutMs ?? 250;
|
|
195
|
+
const diagnostics = getTelegramBusEndpointDiagnostics(input.endpoint);
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
const socket = createConnection(input.endpoint);
|
|
198
|
+
let settled = false;
|
|
199
|
+
const settle = (result: TelegramBusTransportProbeResult): void => {
|
|
200
|
+
if (settled) return;
|
|
201
|
+
settled = true;
|
|
202
|
+
clearTimeout(timeout);
|
|
203
|
+
socket.destroy();
|
|
204
|
+
resolve(result);
|
|
205
|
+
};
|
|
206
|
+
const timeout = setTimeout(() => {
|
|
207
|
+
settle({
|
|
208
|
+
...diagnostics,
|
|
209
|
+
reachable: false,
|
|
210
|
+
error: classifyTelegramBusTransportError(
|
|
211
|
+
createTelegramBusTransportTimeoutError(
|
|
212
|
+
"Timed out probing Telegram bus endpoint",
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
});
|
|
216
|
+
}, timeoutMs);
|
|
217
|
+
timeout.unref?.();
|
|
218
|
+
socket.once("connect", () => settle({ ...diagnostics, reachable: true }));
|
|
219
|
+
socket.once("error", (error) =>
|
|
220
|
+
settle({
|
|
221
|
+
...diagnostics,
|
|
222
|
+
reachable: false,
|
|
223
|
+
error: classifyTelegramBusTransportError(error),
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
}
|
package/lib/bus.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* cross-instance forwarding helpers, and the live follower registry model.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
9
|
import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
10
10
|
import {
|
|
11
11
|
createConnection,
|
|
@@ -16,6 +16,19 @@ import {
|
|
|
16
16
|
import { homedir, platform as getPlatform } from "node:os";
|
|
17
17
|
import { dirname, join, resolve } from "node:path";
|
|
18
18
|
|
|
19
|
+
import {
|
|
20
|
+
classifyTelegramBusTransportError,
|
|
21
|
+
createTelegramBusTransportTimeoutError,
|
|
22
|
+
delayTelegramBusTransportRetry,
|
|
23
|
+
getTelegramBusEndpointDiagnostics,
|
|
24
|
+
getTelegramBusFollowerEndpoint,
|
|
25
|
+
getTelegramBusLeaderEndpoint,
|
|
26
|
+
getTelegramBusTransportRetryPolicy,
|
|
27
|
+
isTelegramBusPipePath,
|
|
28
|
+
isRetryableTelegramBusTransportError,
|
|
29
|
+
type TelegramBusTransportEventRecorder,
|
|
30
|
+
type TelegramBusTransportRetryPolicy,
|
|
31
|
+
} from "./bus-transport.ts";
|
|
19
32
|
import type { TelegramTarget } from "./target.ts";
|
|
20
33
|
|
|
21
34
|
export type TelegramBusRole = "leader" | "follower";
|
|
@@ -30,30 +43,11 @@ export function createTelegramBusAuthSecret(): string {
|
|
|
30
43
|
return randomBytes(32).toString("base64url");
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
function getTelegramBusPipePath(input: {
|
|
34
|
-
agentDir: string;
|
|
35
|
-
scope: string;
|
|
36
|
-
}): string {
|
|
37
|
-
const digest = createHash("sha256")
|
|
38
|
-
.update(resolve(input.agentDir))
|
|
39
|
-
.digest("base64url")
|
|
40
|
-
.slice(0, 16);
|
|
41
|
-
const scope = input.scope.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
|
|
42
|
-
return `\\\\.\\pipe\\pi-telegram-${digest}-${scope}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isWindowsPipePath(socketPath: string): boolean {
|
|
46
|
-
return /^\\\\[.?]\\pipe\\/i.test(socketPath);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
46
|
export function getTelegramBusSocketPath(
|
|
50
47
|
agentDir = getAgentDir(),
|
|
51
48
|
platform = getPlatform(),
|
|
52
49
|
): string {
|
|
53
|
-
|
|
54
|
-
return getTelegramBusPipePath({ agentDir, scope: "bus" });
|
|
55
|
-
}
|
|
56
|
-
return join(agentDir, "tmp", "telegram", "bus.sock");
|
|
50
|
+
return getTelegramBusLeaderEndpoint({ agentDir, platform });
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
export function getTelegramBusFollowerSocketPath(
|
|
@@ -61,19 +55,7 @@ export function getTelegramBusFollowerSocketPath(
|
|
|
61
55
|
agentDir = getAgentDir(),
|
|
62
56
|
platform = getPlatform(),
|
|
63
57
|
): string {
|
|
64
|
-
|
|
65
|
-
return getTelegramBusPipePath({
|
|
66
|
-
agentDir,
|
|
67
|
-
scope: `follower-${instanceId}`,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
return join(
|
|
71
|
-
agentDir,
|
|
72
|
-
"tmp",
|
|
73
|
-
"telegram",
|
|
74
|
-
"followers",
|
|
75
|
-
`${instanceId.replace(/[^a-zA-Z0-9_.-]/g, "_")}.sock`,
|
|
76
|
-
);
|
|
58
|
+
return getTelegramBusFollowerEndpoint({ agentDir, platform, instanceId });
|
|
77
59
|
}
|
|
78
60
|
|
|
79
61
|
export interface TelegramBusInstanceRegistration {
|
|
@@ -91,6 +73,41 @@ export interface TelegramBusFollowerView extends TelegramBusInstanceRegistration
|
|
|
91
73
|
lastHeartbeatMs: number;
|
|
92
74
|
}
|
|
93
75
|
|
|
76
|
+
export function getTelegramFollowerTargetOwnership(input: {
|
|
77
|
+
target: TelegramTarget;
|
|
78
|
+
followers: TelegramBusFollowerView[];
|
|
79
|
+
activeThreadRecords?: Array<{
|
|
80
|
+
status?: string;
|
|
81
|
+
instanceId?: string;
|
|
82
|
+
profileKey?: string;
|
|
83
|
+
owner?: { kind?: string };
|
|
84
|
+
target: TelegramTarget;
|
|
85
|
+
}>;
|
|
86
|
+
currentInstanceId?: string;
|
|
87
|
+
}): { instanceId: string } | undefined {
|
|
88
|
+
const liveFollower = input.followers.find((follower) => {
|
|
89
|
+
return (
|
|
90
|
+
follower.target?.chatId === input.target.chatId &&
|
|
91
|
+
follower.target.threadId === input.target.threadId
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
if (liveFollower) return { instanceId: liveFollower.instanceId };
|
|
95
|
+
const record = input.activeThreadRecords?.find((candidate) => {
|
|
96
|
+
const isFollowerRecord = candidate.owner?.kind
|
|
97
|
+
? candidate.owner.kind === "manual-follower"
|
|
98
|
+
: candidate.profileKey?.startsWith("manual:") === true;
|
|
99
|
+
return (
|
|
100
|
+
isFollowerRecord &&
|
|
101
|
+
candidate.status === "active" &&
|
|
102
|
+
candidate.instanceId &&
|
|
103
|
+
candidate.instanceId !== input.currentInstanceId &&
|
|
104
|
+
candidate.target.chatId === input.target.chatId &&
|
|
105
|
+
candidate.target.threadId === input.target.threadId
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
return record?.instanceId ? { instanceId: record.instanceId } : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
export function isTelegramFollowerApiCallAllowed(input: {
|
|
95
112
|
follower: TelegramBusFollowerView;
|
|
96
113
|
method: string;
|
|
@@ -133,6 +150,28 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
133
150
|
const record = body as Record<string, unknown>;
|
|
134
151
|
return matchesId(record.chat_id, target.chatId);
|
|
135
152
|
};
|
|
153
|
+
const isTargetMessageScoped = (body: unknown): boolean => {
|
|
154
|
+
if (!isTargetChatScoped(body)) return false;
|
|
155
|
+
const messageId = (body as Record<string, unknown>).message_id;
|
|
156
|
+
const parsedMessageId =
|
|
157
|
+
typeof messageId === "number" ? messageId : Number(messageId);
|
|
158
|
+
return Number.isInteger(parsedMessageId) && matchesId(messageId, parsedMessageId);
|
|
159
|
+
};
|
|
160
|
+
const isBotCommandRegistration = (body: unknown): boolean => {
|
|
161
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
162
|
+
const commands = (body as Record<string, unknown>).commands;
|
|
163
|
+
return (
|
|
164
|
+
Array.isArray(commands) &&
|
|
165
|
+
commands.every(
|
|
166
|
+
(command) =>
|
|
167
|
+
command &&
|
|
168
|
+
typeof command === "object" &&
|
|
169
|
+
!Array.isArray(command) &&
|
|
170
|
+
typeof (command as Record<string, unknown>).command === "string" &&
|
|
171
|
+
typeof (command as Record<string, unknown>).description === "string",
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
};
|
|
136
175
|
if (input.method === "downloadFile") return true;
|
|
137
176
|
if (input.method === "call") {
|
|
138
177
|
const apiMethod = input.args[0];
|
|
@@ -144,7 +183,12 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
144
183
|
return true;
|
|
145
184
|
}
|
|
146
185
|
if (apiMethod === "getMe") return true;
|
|
186
|
+
if (apiMethod === "setMyCommands")
|
|
187
|
+
return isBotCommandRegistration(input.args[1]);
|
|
147
188
|
if (apiMethod === "sendChatAction") return isTargetChatScoped(input.args[1]);
|
|
189
|
+
if (apiMethod === "deleteMessage" || apiMethod === "editMessageText") {
|
|
190
|
+
return isTargetMessageScoped(input.args[1]);
|
|
191
|
+
}
|
|
148
192
|
return allowedCallMethods.has(apiMethod) && isTargetScoped(input.args[1]);
|
|
149
193
|
}
|
|
150
194
|
if (input.method === "callMultipart") {
|
|
@@ -310,12 +354,15 @@ export interface TelegramBusLocalServerDeps {
|
|
|
310
354
|
| Promise<TelegramBusEnvelope | undefined>
|
|
311
355
|
| TelegramBusEnvelope
|
|
312
356
|
| undefined;
|
|
357
|
+
recordTransportEvent?: TelegramBusTransportEventRecorder;
|
|
313
358
|
}
|
|
314
359
|
|
|
315
360
|
export interface TelegramBusLocalClientOptions {
|
|
316
361
|
socketPath: string;
|
|
317
362
|
envelope: TelegramBusEnvelope;
|
|
318
363
|
timeoutMs?: number;
|
|
364
|
+
retry?: TelegramBusTransportRetryPolicy;
|
|
365
|
+
recordTransportEvent?: TelegramBusTransportEventRecorder;
|
|
319
366
|
}
|
|
320
367
|
|
|
321
368
|
export interface TelegramBusForeignOwnedForwarderDeps {
|
|
@@ -362,6 +409,10 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
362
409
|
socketPath: deps.socketPath,
|
|
363
410
|
envelope,
|
|
364
411
|
timeoutMs: deps.timeoutMs,
|
|
412
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
413
|
+
endpoint: deps.socketPath,
|
|
414
|
+
operation: "operation",
|
|
415
|
+
}),
|
|
365
416
|
});
|
|
366
417
|
return response?.kind === "bus.ack" && response.ok;
|
|
367
418
|
};
|
|
@@ -449,6 +500,10 @@ export function createTelegramBusFollowerTargetController(
|
|
|
449
500
|
socketPath: follower.busSocketPath,
|
|
450
501
|
envelope,
|
|
451
502
|
timeoutMs: deps.timeoutMs,
|
|
503
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
504
|
+
endpoint: follower.busSocketPath,
|
|
505
|
+
operation: "operation",
|
|
506
|
+
}),
|
|
452
507
|
});
|
|
453
508
|
return response?.kind === "bus.ack" && response.ok;
|
|
454
509
|
},
|
|
@@ -517,7 +572,11 @@ export function createTelegramBusLocalServer(
|
|
|
517
572
|
return {
|
|
518
573
|
start: async () => {
|
|
519
574
|
if (server) return;
|
|
520
|
-
const usesWindowsPipe =
|
|
575
|
+
const usesWindowsPipe = isTelegramBusPipePath(deps.socketPath);
|
|
576
|
+
deps.recordTransportEvent?.(
|
|
577
|
+
"server-start",
|
|
578
|
+
getTelegramBusEndpointDiagnostics(deps.socketPath),
|
|
579
|
+
);
|
|
521
580
|
if (!usesWindowsPipe) {
|
|
522
581
|
const socketDir = dirname(deps.socketPath);
|
|
523
582
|
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
@@ -533,16 +592,40 @@ export function createTelegramBusLocalServer(
|
|
|
533
592
|
const lines = buffer.split("\n");
|
|
534
593
|
buffer = lines.pop() ?? "";
|
|
535
594
|
for (const line of lines) {
|
|
536
|
-
void handleTelegramBusSocketLine(
|
|
595
|
+
void handleTelegramBusSocketLine(
|
|
596
|
+
line,
|
|
597
|
+
socket,
|
|
598
|
+
deps.handleEnvelope,
|
|
599
|
+
deps.recordTransportEvent,
|
|
600
|
+
deps.socketPath,
|
|
601
|
+
);
|
|
537
602
|
}
|
|
538
603
|
});
|
|
539
604
|
socket.on("close", () => sockets.delete(socket));
|
|
540
|
-
socket.on("error", () =>
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
605
|
+
socket.on("error", (error) => {
|
|
606
|
+
deps.recordTransportEvent?.("server-socket-error", {
|
|
607
|
+
...getTelegramBusEndpointDiagnostics(deps.socketPath),
|
|
608
|
+
...classifyTelegramBusTransportError(error),
|
|
609
|
+
});
|
|
610
|
+
closeSocket(socket);
|
|
611
|
+
});
|
|
545
612
|
});
|
|
613
|
+
try {
|
|
614
|
+
await new Promise<void>((resolve, reject) => {
|
|
615
|
+
server?.once("error", reject);
|
|
616
|
+
server?.listen(deps.socketPath, resolve);
|
|
617
|
+
});
|
|
618
|
+
deps.recordTransportEvent?.(
|
|
619
|
+
"server-started",
|
|
620
|
+
getTelegramBusEndpointDiagnostics(deps.socketPath),
|
|
621
|
+
);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
deps.recordTransportEvent?.("server-start-failed", {
|
|
624
|
+
...getTelegramBusEndpointDiagnostics(deps.socketPath),
|
|
625
|
+
...classifyTelegramBusTransportError(error),
|
|
626
|
+
});
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
546
629
|
if (!usesWindowsPipe) chmodSync(deps.socketPath, 0o600);
|
|
547
630
|
},
|
|
548
631
|
stop: async () => {
|
|
@@ -554,14 +637,27 @@ export function createTelegramBusLocalServer(
|
|
|
554
637
|
activeServer.close(() => resolve()),
|
|
555
638
|
);
|
|
556
639
|
}
|
|
557
|
-
if (!
|
|
640
|
+
if (!isTelegramBusPipePath(deps.socketPath) && existsSync(deps.socketPath)) {
|
|
558
641
|
unlinkSync(deps.socketPath);
|
|
559
642
|
}
|
|
643
|
+
deps.recordTransportEvent?.(
|
|
644
|
+
"server-stopped",
|
|
645
|
+
getTelegramBusEndpointDiagnostics(deps.socketPath),
|
|
646
|
+
);
|
|
560
647
|
},
|
|
561
648
|
};
|
|
562
649
|
}
|
|
563
650
|
|
|
564
|
-
|
|
651
|
+
function getTelegramBusEnvelopeDiagnostics(
|
|
652
|
+
envelope: TelegramBusEnvelope,
|
|
653
|
+
): Record<string, unknown> {
|
|
654
|
+
return {
|
|
655
|
+
envelopeKind: envelope.kind,
|
|
656
|
+
requestId: envelope.requestId,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function sendTelegramBusLocalEnvelopeOnce(
|
|
565
661
|
options: TelegramBusLocalClientOptions,
|
|
566
662
|
): Promise<TelegramBusEnvelope | undefined> {
|
|
567
663
|
const timeoutMs = options.timeoutMs ?? 1000;
|
|
@@ -578,7 +674,11 @@ export function sendTelegramBusLocalEnvelope(
|
|
|
578
674
|
};
|
|
579
675
|
const timeout = setTimeout(() => {
|
|
580
676
|
settle(() =>
|
|
581
|
-
reject(
|
|
677
|
+
reject(
|
|
678
|
+
createTelegramBusTransportTimeoutError(
|
|
679
|
+
"Timed out waiting for Telegram bus response",
|
|
680
|
+
),
|
|
681
|
+
),
|
|
582
682
|
);
|
|
583
683
|
}, timeoutMs);
|
|
584
684
|
timeout.unref?.();
|
|
@@ -598,6 +698,39 @@ export function sendTelegramBusLocalEnvelope(
|
|
|
598
698
|
});
|
|
599
699
|
}
|
|
600
700
|
|
|
701
|
+
export async function sendTelegramBusLocalEnvelope(
|
|
702
|
+
options: TelegramBusLocalClientOptions,
|
|
703
|
+
): Promise<TelegramBusEnvelope | undefined> {
|
|
704
|
+
const attempts = Math.max(1, options.retry?.attempts ?? 1);
|
|
705
|
+
const delayMs = Math.max(0, options.retry?.delayMs ?? 0);
|
|
706
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
707
|
+
try {
|
|
708
|
+
return await sendTelegramBusLocalEnvelopeOnce(options);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
const info = classifyTelegramBusTransportError(error);
|
|
711
|
+
options.recordTransportEvent?.("client-failed", {
|
|
712
|
+
...getTelegramBusEndpointDiagnostics(options.socketPath),
|
|
713
|
+
...getTelegramBusEnvelopeDiagnostics(options.envelope),
|
|
714
|
+
attempt,
|
|
715
|
+
attempts,
|
|
716
|
+
...info,
|
|
717
|
+
});
|
|
718
|
+
if (attempt >= attempts || !isRetryableTelegramBusTransportError(error)) {
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
options.recordTransportEvent?.("client-retry", {
|
|
722
|
+
...getTelegramBusEndpointDiagnostics(options.socketPath),
|
|
723
|
+
...getTelegramBusEnvelopeDiagnostics(options.envelope),
|
|
724
|
+
attempt,
|
|
725
|
+
attempts,
|
|
726
|
+
delayMs,
|
|
727
|
+
...info,
|
|
728
|
+
});
|
|
729
|
+
await delayTelegramBusTransportRetry(delayMs);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
601
734
|
export interface TelegramBusFollowerRegistry {
|
|
602
735
|
register: (
|
|
603
736
|
registration: TelegramBusInstanceRegistration,
|
|
@@ -610,6 +743,7 @@ export interface TelegramBusFollowerRegistry {
|
|
|
610
743
|
getByTarget: (target: TelegramTarget) => TelegramBusFollowerView | undefined;
|
|
611
744
|
list: () => TelegramBusFollowerView[];
|
|
612
745
|
remove: (instanceId: string) => boolean;
|
|
746
|
+
clear: () => void;
|
|
613
747
|
pruneStale: (
|
|
614
748
|
nowMs: number,
|
|
615
749
|
staleAfterMs: number,
|
|
@@ -627,6 +761,17 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
627
761
|
return {
|
|
628
762
|
register: (registration) => {
|
|
629
763
|
const existing = followers.get(registration.instanceId);
|
|
764
|
+
for (const [instanceId, follower] of followers.entries()) {
|
|
765
|
+
if (instanceId === registration.instanceId) continue;
|
|
766
|
+
const sameProfile =
|
|
767
|
+
registration.profileKey !== undefined &&
|
|
768
|
+
registration.profileKey === follower.profileKey;
|
|
769
|
+
const sameTarget =
|
|
770
|
+
registration.target !== undefined &&
|
|
771
|
+
follower.target?.chatId === registration.target.chatId &&
|
|
772
|
+
follower.target.threadId === registration.target.threadId;
|
|
773
|
+
if (sameProfile || sameTarget) followers.delete(instanceId);
|
|
774
|
+
}
|
|
630
775
|
const next: TelegramBusFollowerView = {
|
|
631
776
|
...registration,
|
|
632
777
|
target: registration.target ? { ...registration.target } : undefined,
|
|
@@ -660,6 +805,7 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
660
805
|
},
|
|
661
806
|
list: () => [...followers.values()].map(clone),
|
|
662
807
|
remove: (instanceId) => followers.delete(instanceId),
|
|
808
|
+
clear: () => followers.clear(),
|
|
663
809
|
pruneStale: (nowMs, staleAfterMs) => {
|
|
664
810
|
const removed: TelegramBusFollowerView[] = [];
|
|
665
811
|
for (const [instanceId, follower] of followers.entries()) {
|
|
@@ -676,9 +822,15 @@ async function handleTelegramBusSocketLine(
|
|
|
676
822
|
line: string,
|
|
677
823
|
socket: Socket,
|
|
678
824
|
handleEnvelope: TelegramBusLocalServerDeps["handleEnvelope"],
|
|
825
|
+
recordTransportEvent: TelegramBusTransportEventRecorder | undefined,
|
|
826
|
+
socketPath: string,
|
|
679
827
|
): Promise<void> {
|
|
680
828
|
const envelope = parseTelegramBusEnvelope(line);
|
|
681
829
|
if (!envelope) {
|
|
830
|
+
recordTransportEvent?.("server-invalid-envelope", {
|
|
831
|
+
...getTelegramBusEndpointDiagnostics(socketPath),
|
|
832
|
+
byteLength: Buffer.byteLength(line),
|
|
833
|
+
});
|
|
682
834
|
socket.write(
|
|
683
835
|
encodeTelegramBusEnvelope({
|
|
684
836
|
kind: "bus.ack",
|
|
@@ -689,8 +841,24 @@ async function handleTelegramBusSocketLine(
|
|
|
689
841
|
);
|
|
690
842
|
return;
|
|
691
843
|
}
|
|
692
|
-
|
|
693
|
-
|
|
844
|
+
try {
|
|
845
|
+
const response = await handleEnvelope(envelope);
|
|
846
|
+
if (response) socket.write(encodeTelegramBusEnvelope(response));
|
|
847
|
+
} catch (error) {
|
|
848
|
+
recordTransportEvent?.("server-handler-failed", {
|
|
849
|
+
...getTelegramBusEndpointDiagnostics(socketPath),
|
|
850
|
+
...getTelegramBusEnvelopeDiagnostics(envelope),
|
|
851
|
+
...classifyTelegramBusTransportError(error),
|
|
852
|
+
});
|
|
853
|
+
socket.write(
|
|
854
|
+
encodeTelegramBusEnvelope({
|
|
855
|
+
kind: "bus.ack",
|
|
856
|
+
requestId: envelope.requestId,
|
|
857
|
+
ok: false,
|
|
858
|
+
message: "Telegram bus handler failed.",
|
|
859
|
+
}),
|
|
860
|
+
);
|
|
861
|
+
}
|
|
694
862
|
}
|
|
695
863
|
|
|
696
864
|
function parseRegisterEnvelope(
|