@llblab/pi-telegram 0.11.2 → 0.12.0
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 +15 -11
- package/BACKLOG.md +0 -10
- package/CHANGELOG.md +18 -0
- package/README.md +18 -14
- package/api/inbound.ts +14 -0
- package/api/keyboard.ts +10 -0
- package/api/outbound.ts +11 -0
- package/api/sections.ts +17 -0
- package/api/updates.ts +11 -0
- package/api/voice.ts +24 -0
- package/docs/README.md +7 -5
- package/docs/architecture.md +160 -225
- package/docs/callback-namespaces.md +3 -3
- package/docs/{inbound-handlers.md → inbound.md} +13 -10
- package/docs/locks.md +3 -3
- package/docs/{outbound-handlers.md → outbound.md} +13 -10
- package/docs/public-api.md +266 -0
- package/docs/{extension-sections.md → sections.md} +31 -27
- package/docs/ui-style.md +165 -0
- package/docs/{external-handlers.md → updates.md} +33 -31
- package/docs/voice.md +17 -14
- package/index.ts +84 -238
- package/lib/bindings.ts +301 -0
- package/lib/commands.ts +114 -1
- package/lib/config.ts +43 -2
- package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
- package/lib/lifecycle.ts +41 -6
- package/lib/menu-model.ts +3 -3
- package/lib/menu-queue.ts +1 -1
- package/lib/menu-settings.ts +21 -10
- package/lib/menu-status.ts +1 -1
- package/lib/menu.ts +1 -1
- package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
- package/lib/polling.ts +4 -3
- package/lib/preview.ts +1 -1
- package/lib/routing.ts +44 -3
- package/lib/{extension-sections.ts → sections.ts} +37 -8
- package/lib/updates.ts +121 -1
- package/lib/voice.ts +33 -14
- package/package.json +11 -1
- package/lib/external-handlers.ts +0 -166
package/lib/external-handlers.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External Telegram handler registry
|
|
3
|
-
* Zones: telegram transport, layered extension interop
|
|
4
|
-
* Lets other pi extensions hook into the polling loop without owning their own getUpdates connection
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Verdict returned by an interceptor.
|
|
9
|
-
*
|
|
10
|
-
* - `"consume"` — the interceptor handled this update; pi-telegram skips default routing.
|
|
11
|
-
* - `"pass"` (or `void`/`undefined`) — pi-telegram routes the update normally.
|
|
12
|
-
*/
|
|
13
|
-
export type TelegramExternalHandlerVerdict = "consume" | "pass";
|
|
14
|
-
|
|
15
|
-
export type TelegramExternalHandler = (
|
|
16
|
-
update: unknown,
|
|
17
|
-
) =>
|
|
18
|
-
| TelegramExternalHandlerVerdict
|
|
19
|
-
| void
|
|
20
|
-
| Promise<TelegramExternalHandlerVerdict | void>;
|
|
21
|
-
|
|
22
|
-
export interface TelegramExternalHandlerRegistry {
|
|
23
|
-
/** Schema version of this registry shape. */
|
|
24
|
-
readonly version: 1;
|
|
25
|
-
/**
|
|
26
|
-
* Register an interceptor. Returns a disposer that removes it.
|
|
27
|
-
*
|
|
28
|
-
* Interceptors are invoked in registration order on every Telegram update,
|
|
29
|
-
* before pi-telegram's own routing. The first interceptor that returns
|
|
30
|
-
* `"consume"` wins and stops the chain for that update.
|
|
31
|
-
*/
|
|
32
|
-
add: (handler: TelegramExternalHandler) => () => void;
|
|
33
|
-
/**
|
|
34
|
-
* Run all registered interceptors against an update.
|
|
35
|
-
*
|
|
36
|
-
* Used by pi-telegram's polling runtime; layered extensions should call
|
|
37
|
-
* {@link onTelegramExternalUpdate} or `add` instead of dispatching directly.
|
|
38
|
-
*/
|
|
39
|
-
dispatch: (update: unknown) => Promise<TelegramExternalHandlerVerdict>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const REGISTRY_KEY = "__piTelegramExternalHandlerRegistry__";
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Validate that a value on `globalThis` matches the full v1 registry contract.
|
|
46
|
-
*
|
|
47
|
-
* pi-telegram's polling runtime invokes `dispatch`, so a partial object that
|
|
48
|
-
* only carries `version` and `add` (which an early draft of the zero-coupling
|
|
49
|
-
* docs showed) would silently break the first update. We treat any object
|
|
50
|
-
* tagged `version === 1` but missing required methods as malformed and
|
|
51
|
-
* replace it with a fresh, fully-formed registry. Layered extensions that
|
|
52
|
-
* follow the full documented shape are unaffected; ones that don't lose any
|
|
53
|
-
* handlers they registered against the malformed object, which is the
|
|
54
|
-
* desired fail-loud-during-development behavior.
|
|
55
|
-
*/
|
|
56
|
-
function isValidV1Registry(
|
|
57
|
-
candidate: unknown,
|
|
58
|
-
): candidate is TelegramExternalHandlerRegistry {
|
|
59
|
-
if (!candidate || typeof candidate !== "object") return false;
|
|
60
|
-
const r = candidate as Partial<TelegramExternalHandlerRegistry>;
|
|
61
|
-
return (
|
|
62
|
-
r.version === 1 &&
|
|
63
|
-
typeof r.add === "function" &&
|
|
64
|
-
typeof r.dispatch === "function"
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getOrCreateRegistry(): TelegramExternalHandlerRegistry {
|
|
69
|
-
const g = globalThis as Record<string, unknown>;
|
|
70
|
-
const existing = g[REGISTRY_KEY];
|
|
71
|
-
if (isValidV1Registry(existing)) return existing;
|
|
72
|
-
const handlers = new Set<TelegramExternalHandler>();
|
|
73
|
-
const registry: TelegramExternalHandlerRegistry = {
|
|
74
|
-
version: 1,
|
|
75
|
-
add(handler) {
|
|
76
|
-
handlers.add(handler);
|
|
77
|
-
return () => handlers.delete(handler);
|
|
78
|
-
},
|
|
79
|
-
async dispatch(update) {
|
|
80
|
-
for (const handler of handlers) {
|
|
81
|
-
try {
|
|
82
|
-
const result = await handler(update);
|
|
83
|
-
if (result === "consume") return "consume";
|
|
84
|
-
} catch {
|
|
85
|
-
// External handler errors must not break polling.
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return "pass";
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
g[REGISTRY_KEY] = registry;
|
|
92
|
-
return registry;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Called by pi-telegram's own runtime to obtain the registry it dispatches
|
|
97
|
-
* through. Layered extensions should not call this; use
|
|
98
|
-
* {@link onTelegramExternalUpdate} instead.
|
|
99
|
-
*/
|
|
100
|
-
export function getTelegramExternalHandlerRegistry(): TelegramExternalHandlerRegistry {
|
|
101
|
-
return getOrCreateRegistry();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface TelegramExternalHandlerWrapDeps<TUpdate, TContext> {
|
|
105
|
-
defaultHandle: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
106
|
-
registry?: TelegramExternalHandlerRegistry;
|
|
107
|
-
}
|
|
108
|
-
export type TelegramExternalInterceptorWrapDeps<TUpdate, TContext> =
|
|
109
|
-
TelegramExternalHandlerWrapDeps<TUpdate, TContext>;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Wrap a default polling `handleUpdate` with the external interceptor registry.
|
|
113
|
-
*
|
|
114
|
-
* Returned function dispatches `update` through registered interceptors first;
|
|
115
|
-
* if any returns `"consume"`, default routing is skipped for that update.
|
|
116
|
-
*
|
|
117
|
-
* Composition-root callers (pi-telegram's `index.ts`) should use this builder
|
|
118
|
-
* instead of writing the lifting logic inline.
|
|
119
|
-
*/
|
|
120
|
-
export function createTelegramExternalHandleUpdate<TUpdate, TContext>(
|
|
121
|
-
deps: TelegramExternalHandlerWrapDeps<TUpdate, TContext>,
|
|
122
|
-
): (update: TUpdate, ctx: TContext) => Promise<void> {
|
|
123
|
-
const registry = deps.registry ?? getOrCreateRegistry();
|
|
124
|
-
const { defaultHandle } = deps;
|
|
125
|
-
return async function handleInterceptedUpdate(update, ctx) {
|
|
126
|
-
const verdict = await registry.dispatch(update);
|
|
127
|
-
if (verdict === "consume") return;
|
|
128
|
-
await defaultHandle(update, ctx);
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Register an interceptor that runs before pi-telegram routes a Telegram
|
|
134
|
-
* update through its built-in handlers (commands, app menu, queue menu,
|
|
135
|
-
* model menu, default prompt routing).
|
|
136
|
-
*
|
|
137
|
-
* This is the recommended public surface for layered extensions that share
|
|
138
|
-
* the same bot and pi process with pi-telegram (single bot ↔ single
|
|
139
|
-
* `getUpdates` poller).
|
|
140
|
-
*
|
|
141
|
-
* Returns a disposer that removes the interceptor.
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* ```ts
|
|
145
|
-
* import { onTelegramExternalUpdate } from "@llblab/pi-telegram/lib/external-handlers.ts";
|
|
146
|
-
*
|
|
147
|
-
* const off = onTelegramExternalUpdate(async (update) => {
|
|
148
|
-
* const cb = (update as { callback_query?: { data?: string } }).callback_query;
|
|
149
|
-
* if (!cb?.data?.startsWith("myext:")) return "pass";
|
|
150
|
-
* await handleMyCallback(cb);
|
|
151
|
-
* return "consume"; // skip pi-telegram's default routing for this update
|
|
152
|
-
* });
|
|
153
|
-
*
|
|
154
|
-
* // later, e.g. on session shutdown:
|
|
155
|
-
* off();
|
|
156
|
-
* ```
|
|
157
|
-
*
|
|
158
|
-
* Extensions that prefer zero coupling can also reach the versioned registry
|
|
159
|
-
* directly on `globalThis`. This avoids importing `@llblab/pi-telegram` and
|
|
160
|
-
* tolerates either install order.
|
|
161
|
-
*/
|
|
162
|
-
export function onTelegramExternalUpdate(
|
|
163
|
-
handler: TelegramExternalHandler,
|
|
164
|
-
): () => void {
|
|
165
|
-
return getOrCreateRegistry().add(handler);
|
|
166
|
-
}
|