@botcord/daemon 0.2.38 → 0.2.41
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/dist/gateway/channels/telegram.js +46 -7
- package/dist/gateway/dispatcher.js +2 -1
- package/dist/gateway-control.d.ts +37 -1
- package/dist/provision.js +6 -6
- package/package.json +2 -2
- package/src/__tests__/provision.test.ts +5 -5
- package/src/gateway/__tests__/dispatcher.test.ts +16 -1
- package/src/gateway/__tests__/telegram-channel.test.ts +50 -0
- package/src/gateway/channels/telegram.ts +48 -5
- package/src/gateway/dispatcher.ts +3 -1
- package/src/gateway-control.ts +103 -16
- package/src/provision.ts +6 -6
|
@@ -72,17 +72,18 @@ export function createTelegramChannel(opts) {
|
|
|
72
72
|
}
|
|
73
73
|
return botToken;
|
|
74
74
|
}
|
|
75
|
-
async function callApi(method, params, timeoutMs) {
|
|
75
|
+
async function callApi(method, params, timeoutMs, abortSignal) {
|
|
76
76
|
if (!botToken)
|
|
77
77
|
throw new Error("telegram bot token not loaded");
|
|
78
78
|
const url = `${baseUrl}/bot${botToken}/${method}`;
|
|
79
|
+
const signalLease = createTimeoutSignal(timeoutMs, abortSignal);
|
|
79
80
|
let resp;
|
|
80
81
|
try {
|
|
81
82
|
resp = await fetchImpl(url, {
|
|
82
83
|
method: "POST",
|
|
83
84
|
headers: { "Content-Type": "application/json" },
|
|
84
85
|
body: JSON.stringify(params),
|
|
85
|
-
signal:
|
|
86
|
+
signal: signalLease.signal,
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
catch (err) {
|
|
@@ -94,6 +95,9 @@ export function createTelegramChannel(opts) {
|
|
|
94
95
|
next.name = e.name ?? "Error";
|
|
95
96
|
throw next;
|
|
96
97
|
}
|
|
98
|
+
finally {
|
|
99
|
+
signalLease.cleanup();
|
|
100
|
+
}
|
|
97
101
|
const json = (await resp.json());
|
|
98
102
|
return json;
|
|
99
103
|
}
|
|
@@ -156,7 +160,7 @@ export function createTelegramChannel(opts) {
|
|
|
156
160
|
replyTo: null,
|
|
157
161
|
mentioned: false,
|
|
158
162
|
receivedAt: Date.now(),
|
|
159
|
-
trace: { id: messageId, streamable:
|
|
163
|
+
trace: { id: messageId, streamable: true },
|
|
160
164
|
};
|
|
161
165
|
}
|
|
162
166
|
async function pollLoop(ctx) {
|
|
@@ -194,20 +198,20 @@ export function createTelegramChannel(opts) {
|
|
|
194
198
|
});
|
|
195
199
|
log.info("telegram poll loop starting", { gatewayId: opts.id, offset });
|
|
196
200
|
let stopped = false;
|
|
201
|
+
const stopController = new AbortController();
|
|
197
202
|
const onAbort = () => {
|
|
198
203
|
stopped = true;
|
|
204
|
+
stopController.abort();
|
|
199
205
|
};
|
|
200
206
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
201
|
-
stopCallback =
|
|
202
|
-
stopped = true;
|
|
203
|
-
};
|
|
207
|
+
stopCallback = onAbort;
|
|
204
208
|
while (!stopped && !abortSignal.aborted) {
|
|
205
209
|
try {
|
|
206
210
|
const resp = await callApi("getUpdates", {
|
|
207
211
|
offset,
|
|
208
212
|
timeout: POLL_TIMEOUT_S,
|
|
209
213
|
allowed_updates: ["message"],
|
|
210
|
-
}, (POLL_TIMEOUT_S + 15) * 1000);
|
|
214
|
+
}, (POLL_TIMEOUT_S + 15) * 1000, stopController.signal);
|
|
211
215
|
markStatus({ lastPollAt: Date.now() });
|
|
212
216
|
if (!resp.ok) {
|
|
213
217
|
log.warn("telegram getUpdates non-ok", {
|
|
@@ -369,3 +373,38 @@ function sleep(ms, signal) {
|
|
|
369
373
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
370
374
|
});
|
|
371
375
|
}
|
|
376
|
+
function createTimeoutSignal(timeoutMs, parent) {
|
|
377
|
+
const controller = new AbortController();
|
|
378
|
+
let settled = false;
|
|
379
|
+
const abort = (reason) => {
|
|
380
|
+
if (settled)
|
|
381
|
+
return;
|
|
382
|
+
settled = true;
|
|
383
|
+
try {
|
|
384
|
+
controller.abort(reason);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
controller.abort();
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const timer = setTimeout(() => {
|
|
391
|
+
abort(new DOMException("Timeout", "TimeoutError"));
|
|
392
|
+
}, timeoutMs);
|
|
393
|
+
const onParentAbort = () => abort(parent?.reason);
|
|
394
|
+
if (parent) {
|
|
395
|
+
if (parent.aborted) {
|
|
396
|
+
onParentAbort();
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
parent.addEventListener("abort", onParentAbort, { once: true });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
signal: controller.signal,
|
|
404
|
+
cleanup: () => {
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
if (parent)
|
|
407
|
+
parent.removeEventListener("abort", onParentAbort);
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
@@ -591,6 +591,7 @@ export class Dispatcher {
|
|
|
591
591
|
const trustLevel = route.trustLevel ?? "trusted";
|
|
592
592
|
const streamable = msg.trace?.streamable === true;
|
|
593
593
|
const traceId = msg.trace?.id;
|
|
594
|
+
const canType = streamable && typeof traceId === "string" && typeof channel.typing === "function";
|
|
594
595
|
const canStream = streamable && typeof traceId === "string" && typeof channel.streamBlock === "function";
|
|
595
596
|
const recordBlock = (block) => {
|
|
596
597
|
const summary = { type: block.kind };
|
|
@@ -670,7 +671,7 @@ export class Dispatcher {
|
|
|
670
671
|
forwardBlockToChannel(synth);
|
|
671
672
|
};
|
|
672
673
|
const fireTypingIfNeeded = () => {
|
|
673
|
-
if (!
|
|
674
|
+
if (!canType || typingFired)
|
|
674
675
|
return;
|
|
675
676
|
typingFired = true;
|
|
676
677
|
const key = `${msg.accountId}:${msg.conversation.id}`;
|
|
@@ -8,13 +8,49 @@
|
|
|
8
8
|
* gateway, login-session store, fetch impl, and config I/O — `provision.ts`
|
|
9
9
|
* wires the production defaults.
|
|
10
10
|
*/
|
|
11
|
-
import type { ControlAck
|
|
11
|
+
import type { ControlAck } from "@botcord/protocol-core";
|
|
12
12
|
import type { Gateway } from "./gateway/index.js";
|
|
13
13
|
import { type DaemonConfig } from "./config.js";
|
|
14
14
|
import { LoginSessionStore } from "./gateway/channels/login-session.js";
|
|
15
15
|
import { getBotQrcode, getQrcodeStatus } from "./gateway/channels/wechat-login.js";
|
|
16
16
|
import type { FetchLike } from "./gateway/channels/http-types.js";
|
|
17
17
|
type AckBody = Omit<ControlAck, "id">;
|
|
18
|
+
type GatewayProvider = "telegram" | "wechat";
|
|
19
|
+
interface UpsertGatewayParams {
|
|
20
|
+
id: string;
|
|
21
|
+
type: GatewayProvider;
|
|
22
|
+
accountId: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
loginId?: string;
|
|
26
|
+
secret?: {
|
|
27
|
+
botToken?: string;
|
|
28
|
+
};
|
|
29
|
+
settings?: {
|
|
30
|
+
baseUrl?: string;
|
|
31
|
+
allowedSenderIds?: string[];
|
|
32
|
+
allowedChatIds?: string[];
|
|
33
|
+
splitAt?: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
interface RemoveGatewayParams {
|
|
37
|
+
id: string;
|
|
38
|
+
deleteSecret?: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface TestGatewayParams {
|
|
41
|
+
id: string;
|
|
42
|
+
}
|
|
43
|
+
interface GatewayLoginStartParams {
|
|
44
|
+
provider: GatewayProvider;
|
|
45
|
+
accountId: string;
|
|
46
|
+
gatewayId?: string;
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
}
|
|
49
|
+
interface GatewayLoginStatusParams {
|
|
50
|
+
provider: GatewayProvider;
|
|
51
|
+
loginId: string;
|
|
52
|
+
accountId: string;
|
|
53
|
+
}
|
|
18
54
|
export type { FetchLike };
|
|
19
55
|
export interface GatewayControlContext {
|
|
20
56
|
gateway: Gateway;
|
package/dist/provision.js
CHANGED
|
@@ -201,9 +201,9 @@ export function createProvisioner(opts) {
|
|
|
201
201
|
daemonLog.debug("list_runtimes", { count: snapshot.runtimes.length });
|
|
202
202
|
return { ok: true, result: snapshot };
|
|
203
203
|
}
|
|
204
|
-
case
|
|
204
|
+
case "list_gateways":
|
|
205
205
|
return gatewayControl.handleList();
|
|
206
|
-
case
|
|
206
|
+
case "upsert_gateway": {
|
|
207
207
|
const v = validateGatewayParams(frame.params, {
|
|
208
208
|
required: ["id", "type", "accountId"],
|
|
209
209
|
});
|
|
@@ -211,19 +211,19 @@ export function createProvisioner(opts) {
|
|
|
211
211
|
return v.ack;
|
|
212
212
|
return gatewayControl.handleUpsert(v.params);
|
|
213
213
|
}
|
|
214
|
-
case
|
|
214
|
+
case "remove_gateway": {
|
|
215
215
|
const v = validateGatewayParams(frame.params, { required: ["id"] });
|
|
216
216
|
if (!v.ok)
|
|
217
217
|
return v.ack;
|
|
218
218
|
return gatewayControl.handleRemove(v.params);
|
|
219
219
|
}
|
|
220
|
-
case
|
|
220
|
+
case "test_gateway": {
|
|
221
221
|
const v = validateGatewayParams(frame.params, { required: ["id"] });
|
|
222
222
|
if (!v.ok)
|
|
223
223
|
return v.ack;
|
|
224
224
|
return gatewayControl.handleTest(v.params);
|
|
225
225
|
}
|
|
226
|
-
case
|
|
226
|
+
case "gateway_login_start": {
|
|
227
227
|
const v = validateGatewayParams(frame.params, {
|
|
228
228
|
required: ["provider", "accountId"],
|
|
229
229
|
});
|
|
@@ -231,7 +231,7 @@ export function createProvisioner(opts) {
|
|
|
231
231
|
return v.ack;
|
|
232
232
|
return gatewayControl.handleLoginStart(v.params);
|
|
233
233
|
}
|
|
234
|
-
case
|
|
234
|
+
case "gateway_login_status": {
|
|
235
235
|
const v = validateGatewayParams(frame.params, {
|
|
236
236
|
required: ["provider", "loginId"],
|
|
237
237
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botcord/daemon",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.41",
|
|
4
4
|
"description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@botcord/cli": "^0.1.7",
|
|
31
|
-
"@botcord/protocol-core": "^0.2.
|
|
31
|
+
"@botcord/protocol-core": "^0.2.4",
|
|
32
32
|
"ws": "^8.18.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
@@ -1698,7 +1698,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
|
|
|
1698
1698
|
});
|
|
1699
1699
|
const ack = await provisioner({
|
|
1700
1700
|
id: "req_w8a",
|
|
1701
|
-
type:
|
|
1701
|
+
type: "upsert_gateway",
|
|
1702
1702
|
params: { id: "gw_x", type: "telegram" } as unknown as Record<string, unknown>,
|
|
1703
1703
|
});
|
|
1704
1704
|
expect(ack.ok).toBe(false);
|
|
@@ -1714,7 +1714,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
|
|
|
1714
1714
|
});
|
|
1715
1715
|
const ack = await provisioner({
|
|
1716
1716
|
id: "req_w8b",
|
|
1717
|
-
type:
|
|
1717
|
+
type: "remove_gateway",
|
|
1718
1718
|
// @ts-expect-error — exercising the runtime guard
|
|
1719
1719
|
params: "not-an-object",
|
|
1720
1720
|
});
|
|
@@ -1729,7 +1729,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
|
|
|
1729
1729
|
});
|
|
1730
1730
|
const ack = await provisioner({
|
|
1731
1731
|
id: "req_w8c",
|
|
1732
|
-
type:
|
|
1732
|
+
type: "test_gateway",
|
|
1733
1733
|
params: {},
|
|
1734
1734
|
});
|
|
1735
1735
|
expect(ack.ok).toBe(false);
|
|
@@ -1743,7 +1743,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
|
|
|
1743
1743
|
});
|
|
1744
1744
|
const ack = await provisioner({
|
|
1745
1745
|
id: "req_w8d",
|
|
1746
|
-
type:
|
|
1746
|
+
type: "gateway_login_start",
|
|
1747
1747
|
params: { accountId: "ag_a" },
|
|
1748
1748
|
});
|
|
1749
1749
|
expect(ack.ok).toBe(false);
|
|
@@ -1757,7 +1757,7 @@ describe("W8: gateway frame param validation in provision dispatch", () => {
|
|
|
1757
1757
|
});
|
|
1758
1758
|
const ack = await provisioner({
|
|
1759
1759
|
id: "req_w8e",
|
|
1760
|
-
type:
|
|
1760
|
+
type: "gateway_login_status",
|
|
1761
1761
|
params: { provider: "wechat" },
|
|
1762
1762
|
});
|
|
1763
1763
|
expect(ack.ok).toBe(false);
|
|
@@ -619,7 +619,7 @@ describe("Dispatcher", () => {
|
|
|
619
619
|
// typing / thinking lifecycle (design: runtime-typing-thinking-status-design.md)
|
|
620
620
|
// ---------------------------------------------------------------------------
|
|
621
621
|
|
|
622
|
-
it("typing: fires channel.typing once before runtime.run when
|
|
622
|
+
it("typing: fires channel.typing once before runtime.run when trace is streamable", async () => {
|
|
623
623
|
const observed: Array<{ typings: number; calls: number }> = [];
|
|
624
624
|
const runtime = new FakeRuntime({
|
|
625
625
|
observeRun: () => {
|
|
@@ -644,6 +644,21 @@ describe("Dispatcher", () => {
|
|
|
644
644
|
expect(observed[0]?.typings).toBe(1);
|
|
645
645
|
});
|
|
646
646
|
|
|
647
|
+
it("typing: does not require channel streamBlock support", async () => {
|
|
648
|
+
const runtime = new FakeRuntime({ reply: "ok", newSessionId: "sid" });
|
|
649
|
+
const channel = new FakeChannel({ withStream: false });
|
|
650
|
+
const { dispatcher } = await scaffold({ channel, runtimeFactory: () => runtime });
|
|
651
|
+
|
|
652
|
+
await dispatcher.handle(
|
|
653
|
+
makeEnvelope({ trace: { id: "trace_t", streamable: true } }),
|
|
654
|
+
);
|
|
655
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
656
|
+
|
|
657
|
+
expect(channel.typings.length).toBe(1);
|
|
658
|
+
expect(channel.streams.length).toBe(0);
|
|
659
|
+
expect(channel.sends.length).toBe(1);
|
|
660
|
+
});
|
|
661
|
+
|
|
647
662
|
it("typing: not fired when streamable is false", async () => {
|
|
648
663
|
const channel = new FakeChannel();
|
|
649
664
|
const { dispatcher } = await scaffold({
|
|
@@ -144,6 +144,7 @@ describe("createTelegramChannel — start()", () => {
|
|
|
144
144
|
expect(msg.text).toBe("hello world");
|
|
145
145
|
expect(msg.accountId).toBe("ag_self");
|
|
146
146
|
expect(msg.channel).toBe("gw_tg_a");
|
|
147
|
+
expect(msg.trace).toEqual({ id: "telegram:42:7", streamable: true });
|
|
147
148
|
});
|
|
148
149
|
|
|
149
150
|
it("uses telegram:group:<id> for non-private chats", async () => {
|
|
@@ -319,6 +320,55 @@ describe("createTelegramChannel — start()", () => {
|
|
|
319
320
|
expect(calls).toHaveLength(1);
|
|
320
321
|
expect((calls[0]!.body as Record<string, unknown>).offset).toBe(999);
|
|
321
322
|
});
|
|
323
|
+
|
|
324
|
+
it("aborts an in-flight getUpdates request when stopped", async () => {
|
|
325
|
+
const calls: FetchCall[] = [];
|
|
326
|
+
let requestSignal: AbortSignal | undefined;
|
|
327
|
+
let fetchStarted!: () => void;
|
|
328
|
+
const fetchStartedPromise = new Promise<void>((resolve) => {
|
|
329
|
+
fetchStarted = resolve;
|
|
330
|
+
});
|
|
331
|
+
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
332
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
333
|
+
const body = init?.body ? JSON.parse(init.body as string) : undefined;
|
|
334
|
+
calls.push({ url, body });
|
|
335
|
+
requestSignal = init?.signal ?? undefined;
|
|
336
|
+
fetchStarted();
|
|
337
|
+
return await new Promise<Response>((_resolve, reject) => {
|
|
338
|
+
requestSignal?.addEventListener(
|
|
339
|
+
"abort",
|
|
340
|
+
() => {
|
|
341
|
+
const err = new Error("aborted");
|
|
342
|
+
err.name = "AbortError";
|
|
343
|
+
reject(err);
|
|
344
|
+
},
|
|
345
|
+
{ once: true },
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
}) as typeof fetch;
|
|
349
|
+
|
|
350
|
+
const channel = createTelegramChannel({
|
|
351
|
+
id: "gw_tg_stop",
|
|
352
|
+
accountId: "ag_self",
|
|
353
|
+
botToken: "tok",
|
|
354
|
+
allowedChatIds: ["42"],
|
|
355
|
+
allowedSenderIds: ["42"],
|
|
356
|
+
stateFile: path.join(tmp, "state.json"),
|
|
357
|
+
stateDebounceMs: 0,
|
|
358
|
+
fetchImpl,
|
|
359
|
+
});
|
|
360
|
+
const abort = new AbortController();
|
|
361
|
+
const { ctx } = makeStartCtx({ abort });
|
|
362
|
+
const startPromise = channel.start(ctx);
|
|
363
|
+
|
|
364
|
+
await fetchStartedPromise;
|
|
365
|
+
expect(requestSignal?.aborted).toBe(false);
|
|
366
|
+
await channel.stop!({ reason: "remove_gateway" });
|
|
367
|
+
await startPromise;
|
|
368
|
+
|
|
369
|
+
expect(requestSignal?.aborted).toBe(true);
|
|
370
|
+
expect(calls[0]!.url).toContain("/getUpdates");
|
|
371
|
+
});
|
|
322
372
|
});
|
|
323
373
|
|
|
324
374
|
describe("createTelegramChannel — send()", () => {
|
|
@@ -152,16 +152,18 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
|
|
|
152
152
|
method: string,
|
|
153
153
|
params: Record<string, unknown>,
|
|
154
154
|
timeoutMs: number,
|
|
155
|
+
abortSignal?: AbortSignal,
|
|
155
156
|
): Promise<TelegramApiResult<T>> {
|
|
156
157
|
if (!botToken) throw new Error("telegram bot token not loaded");
|
|
157
158
|
const url = `${baseUrl}/bot${botToken}/${method}`;
|
|
159
|
+
const signalLease = createTimeoutSignal(timeoutMs, abortSignal);
|
|
158
160
|
let resp: Response;
|
|
159
161
|
try {
|
|
160
162
|
resp = await fetchImpl(url, {
|
|
161
163
|
method: "POST",
|
|
162
164
|
headers: { "Content-Type": "application/json" },
|
|
163
165
|
body: JSON.stringify(params),
|
|
164
|
-
signal:
|
|
166
|
+
signal: signalLease.signal,
|
|
165
167
|
});
|
|
166
168
|
} catch (err) {
|
|
167
169
|
// C3: fetch errors often stringify the URL (which embeds the token).
|
|
@@ -171,6 +173,8 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
|
|
|
171
173
|
const next = new Error(redacted);
|
|
172
174
|
next.name = e.name ?? "Error";
|
|
173
175
|
throw next;
|
|
176
|
+
} finally {
|
|
177
|
+
signalLease.cleanup();
|
|
174
178
|
}
|
|
175
179
|
const json = (await resp.json()) as TelegramApiResult<T>;
|
|
176
180
|
return json;
|
|
@@ -237,7 +241,7 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
|
|
|
237
241
|
replyTo: null,
|
|
238
242
|
mentioned: false,
|
|
239
243
|
receivedAt: Date.now(),
|
|
240
|
-
trace: { id: messageId, streamable:
|
|
244
|
+
trace: { id: messageId, streamable: true },
|
|
241
245
|
};
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -280,13 +284,13 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
|
|
|
280
284
|
log.info("telegram poll loop starting", { gatewayId: opts.id, offset });
|
|
281
285
|
|
|
282
286
|
let stopped = false;
|
|
287
|
+
const stopController = new AbortController();
|
|
283
288
|
const onAbort = () => {
|
|
284
289
|
stopped = true;
|
|
290
|
+
stopController.abort();
|
|
285
291
|
};
|
|
286
292
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
287
|
-
stopCallback =
|
|
288
|
-
stopped = true;
|
|
289
|
-
};
|
|
293
|
+
stopCallback = onAbort;
|
|
290
294
|
|
|
291
295
|
while (!stopped && !abortSignal.aborted) {
|
|
292
296
|
try {
|
|
@@ -298,6 +302,7 @@ export function createTelegramChannel(opts: TelegramChannelOptions): ChannelAdap
|
|
|
298
302
|
allowed_updates: ["message"],
|
|
299
303
|
},
|
|
300
304
|
(POLL_TIMEOUT_S + 15) * 1000,
|
|
305
|
+
stopController.signal,
|
|
301
306
|
);
|
|
302
307
|
markStatus({ lastPollAt: Date.now() });
|
|
303
308
|
if (!resp.ok) {
|
|
@@ -467,3 +472,41 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
467
472
|
});
|
|
468
473
|
}
|
|
469
474
|
|
|
475
|
+
function createTimeoutSignal(
|
|
476
|
+
timeoutMs: number,
|
|
477
|
+
parent?: AbortSignal,
|
|
478
|
+
): { signal: AbortSignal; cleanup: () => void } {
|
|
479
|
+
const controller = new AbortController();
|
|
480
|
+
let settled = false;
|
|
481
|
+
|
|
482
|
+
const abort = (reason?: unknown) => {
|
|
483
|
+
if (settled) return;
|
|
484
|
+
settled = true;
|
|
485
|
+
try {
|
|
486
|
+
controller.abort(reason);
|
|
487
|
+
} catch {
|
|
488
|
+
controller.abort();
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const timer = setTimeout(() => {
|
|
493
|
+
abort(new DOMException("Timeout", "TimeoutError"));
|
|
494
|
+
}, timeoutMs);
|
|
495
|
+
|
|
496
|
+
const onParentAbort = () => abort(parent?.reason);
|
|
497
|
+
if (parent) {
|
|
498
|
+
if (parent.aborted) {
|
|
499
|
+
onParentAbort();
|
|
500
|
+
} else {
|
|
501
|
+
parent.addEventListener("abort", onParentAbort, { once: true });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
signal: controller.signal,
|
|
507
|
+
cleanup: () => {
|
|
508
|
+
clearTimeout(timer);
|
|
509
|
+
if (parent) parent.removeEventListener("abort", onParentAbort);
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
@@ -803,6 +803,8 @@ export class Dispatcher {
|
|
|
803
803
|
|
|
804
804
|
const streamable = msg.trace?.streamable === true;
|
|
805
805
|
const traceId = msg.trace?.id;
|
|
806
|
+
const canType =
|
|
807
|
+
streamable && typeof traceId === "string" && typeof channel.typing === "function";
|
|
806
808
|
const canStream =
|
|
807
809
|
streamable && typeof traceId === "string" && typeof channel.streamBlock === "function";
|
|
808
810
|
const recordBlock = (block: StreamBlock): void => {
|
|
@@ -886,7 +888,7 @@ export class Dispatcher {
|
|
|
886
888
|
};
|
|
887
889
|
|
|
888
890
|
const fireTypingIfNeeded = (): void => {
|
|
889
|
-
if (!
|
|
891
|
+
if (!canType || typingFired) return;
|
|
890
892
|
typingFired = true;
|
|
891
893
|
const key = `${msg.accountId}:${msg.conversation.id}`;
|
|
892
894
|
const now = Date.now();
|
package/src/gateway-control.ts
CHANGED
|
@@ -9,22 +9,7 @@
|
|
|
9
9
|
* wires the production defaults.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type {
|
|
13
|
-
ControlAck,
|
|
14
|
-
GatewayLoginStartParams,
|
|
15
|
-
GatewayLoginStartResult,
|
|
16
|
-
GatewayLoginStatusParams,
|
|
17
|
-
GatewayLoginStatusResult,
|
|
18
|
-
GatewayProfileSummary,
|
|
19
|
-
GatewayProvider,
|
|
20
|
-
ListGatewaysResult,
|
|
21
|
-
RemoveGatewayParams,
|
|
22
|
-
RemoveGatewayResult,
|
|
23
|
-
TestGatewayParams,
|
|
24
|
-
TestGatewayResult,
|
|
25
|
-
UpsertGatewayParams,
|
|
26
|
-
UpsertGatewayResult,
|
|
27
|
-
} from "@botcord/protocol-core";
|
|
12
|
+
import type { ControlAck } from "@botcord/protocol-core";
|
|
28
13
|
import type { Gateway, GatewayChannelConfig } from "./gateway/index.js";
|
|
29
14
|
import {
|
|
30
15
|
loadConfig,
|
|
@@ -56,6 +41,108 @@ import type { FetchLike } from "./gateway/channels/http-types.js";
|
|
|
56
41
|
|
|
57
42
|
type AckBody = Omit<ControlAck, "id">;
|
|
58
43
|
|
|
44
|
+
type GatewayProvider = "telegram" | "wechat";
|
|
45
|
+
|
|
46
|
+
interface GatewayProfileSummary {
|
|
47
|
+
id: string;
|
|
48
|
+
type: GatewayProvider;
|
|
49
|
+
accountId: string;
|
|
50
|
+
label?: string;
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
baseUrl?: string;
|
|
53
|
+
allowedSenderIds?: string[];
|
|
54
|
+
allowedChatIds?: string[];
|
|
55
|
+
splitAt?: number;
|
|
56
|
+
status?: {
|
|
57
|
+
running: boolean;
|
|
58
|
+
connected?: boolean;
|
|
59
|
+
authorized?: boolean;
|
|
60
|
+
lastPollAt?: number;
|
|
61
|
+
lastInboundAt?: number;
|
|
62
|
+
lastSendAt?: number;
|
|
63
|
+
lastError?: string | null;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ListGatewaysResult {
|
|
68
|
+
gateways: GatewayProfileSummary[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface UpsertGatewayParams {
|
|
72
|
+
id: string;
|
|
73
|
+
type: GatewayProvider;
|
|
74
|
+
accountId: string;
|
|
75
|
+
label?: string;
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
loginId?: string;
|
|
78
|
+
secret?: {
|
|
79
|
+
botToken?: string;
|
|
80
|
+
};
|
|
81
|
+
settings?: {
|
|
82
|
+
baseUrl?: string;
|
|
83
|
+
allowedSenderIds?: string[];
|
|
84
|
+
allowedChatIds?: string[];
|
|
85
|
+
splitAt?: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface UpsertGatewayResult {
|
|
90
|
+
id: string;
|
|
91
|
+
type: GatewayProvider;
|
|
92
|
+
accountId: string;
|
|
93
|
+
enabled: boolean;
|
|
94
|
+
tokenPreview?: string;
|
|
95
|
+
status?: GatewayProfileSummary["status"];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface RemoveGatewayParams {
|
|
99
|
+
id: string;
|
|
100
|
+
deleteSecret?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface RemoveGatewayResult {
|
|
104
|
+
id: string;
|
|
105
|
+
removed: boolean;
|
|
106
|
+
secretDeleted: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface TestGatewayParams {
|
|
110
|
+
id: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface TestGatewayResult {
|
|
114
|
+
id: string;
|
|
115
|
+
ok: boolean;
|
|
116
|
+
info?: Record<string, unknown>;
|
|
117
|
+
error?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface GatewayLoginStartParams {
|
|
121
|
+
provider: GatewayProvider;
|
|
122
|
+
accountId: string;
|
|
123
|
+
gatewayId?: string;
|
|
124
|
+
baseUrl?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface GatewayLoginStartResult {
|
|
128
|
+
loginId: string;
|
|
129
|
+
qrcode?: string;
|
|
130
|
+
qrcodeUrl?: string;
|
|
131
|
+
expiresAt: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface GatewayLoginStatusParams {
|
|
135
|
+
provider: GatewayProvider;
|
|
136
|
+
loginId: string;
|
|
137
|
+
accountId: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface GatewayLoginStatusResult {
|
|
141
|
+
status: "pending" | "scanned" | "confirmed" | "expired" | "failed";
|
|
142
|
+
baseUrl?: string;
|
|
143
|
+
tokenPreview?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
59
146
|
export type { FetchLike };
|
|
60
147
|
|
|
61
148
|
export interface GatewayControlContext {
|
package/src/provision.ts
CHANGED
|
@@ -322,10 +322,10 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
322
322
|
return { ok: true, result: snapshot };
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
case
|
|
325
|
+
case "list_gateways":
|
|
326
326
|
return gatewayControl.handleList();
|
|
327
327
|
|
|
328
|
-
case
|
|
328
|
+
case "upsert_gateway": {
|
|
329
329
|
const v = validateGatewayParams(frame.params, {
|
|
330
330
|
required: ["id", "type", "accountId"],
|
|
331
331
|
});
|
|
@@ -335,7 +335,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
335
335
|
);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
case
|
|
338
|
+
case "remove_gateway": {
|
|
339
339
|
const v = validateGatewayParams(frame.params, { required: ["id"] });
|
|
340
340
|
if (!v.ok) return v.ack;
|
|
341
341
|
return gatewayControl.handleRemove(
|
|
@@ -343,7 +343,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
343
343
|
);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
case
|
|
346
|
+
case "test_gateway": {
|
|
347
347
|
const v = validateGatewayParams(frame.params, { required: ["id"] });
|
|
348
348
|
if (!v.ok) return v.ack;
|
|
349
349
|
return gatewayControl.handleTest(
|
|
@@ -351,7 +351,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
351
351
|
);
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
case
|
|
354
|
+
case "gateway_login_start": {
|
|
355
355
|
const v = validateGatewayParams(frame.params, {
|
|
356
356
|
required: ["provider", "accountId"],
|
|
357
357
|
});
|
|
@@ -361,7 +361,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
361
361
|
);
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
case
|
|
364
|
+
case "gateway_login_status": {
|
|
365
365
|
const v = validateGatewayParams(frame.params, {
|
|
366
366
|
required: ["provider", "loginId"],
|
|
367
367
|
});
|