@moxxy/plugin-telegram 0.27.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/LICENSE +21 -0
- package/dist/approval.d.ts +37 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +75 -0
- package/dist/approval.js.map +1 -0
- package/dist/channel/approval-prompt.d.ts +20 -0
- package/dist/channel/approval-prompt.d.ts.map +1 -0
- package/dist/channel/approval-prompt.js +34 -0
- package/dist/channel/approval-prompt.js.map +1 -0
- package/dist/channel/callback-handler.d.ts +32 -0
- package/dist/channel/callback-handler.d.ts.map +1 -0
- package/dist/channel/callback-handler.js +212 -0
- package/dist/channel/callback-handler.js.map +1 -0
- package/dist/channel/frame-pump.d.ts +58 -0
- package/dist/channel/frame-pump.d.ts.map +1 -0
- package/dist/channel/frame-pump.js +159 -0
- package/dist/channel/frame-pump.js.map +1 -0
- package/dist/channel/html.d.ts +24 -0
- package/dist/channel/html.d.ts.map +1 -0
- package/dist/channel/html.js +62 -0
- package/dist/channel/html.js.map +1 -0
- package/dist/channel/pairing-handler.d.ts +66 -0
- package/dist/channel/pairing-handler.d.ts.map +1 -0
- package/dist/channel/pairing-handler.js +147 -0
- package/dist/channel/pairing-handler.js.map +1 -0
- package/dist/channel/permission-prompt.d.ts +21 -0
- package/dist/channel/permission-prompt.d.ts.map +1 -0
- package/dist/channel/permission-prompt.js +36 -0
- package/dist/channel/permission-prompt.js.map +1 -0
- package/dist/channel/slash-handler.d.ts +47 -0
- package/dist/channel/slash-handler.d.ts.map +1 -0
- package/dist/channel/slash-handler.js +214 -0
- package/dist/channel/slash-handler.js.map +1 -0
- package/dist/channel/text-handler.d.ts +43 -0
- package/dist/channel/text-handler.d.ts.map +1 -0
- package/dist/channel/text-handler.js +116 -0
- package/dist/channel/text-handler.js.map +1 -0
- package/dist/channel/turn-runner.d.ts +40 -0
- package/dist/channel/turn-runner.d.ts.map +1 -0
- package/dist/channel/turn-runner.js +67 -0
- package/dist/channel/turn-runner.js.map +1 -0
- package/dist/channel/typing-indicator.d.ts +14 -0
- package/dist/channel/typing-indicator.d.ts.map +1 -0
- package/dist/channel/typing-indicator.js +29 -0
- package/dist/channel/typing-indicator.js.map +1 -0
- package/dist/channel/voice-handler.d.ts +42 -0
- package/dist/channel/voice-handler.d.ts.map +1 -0
- package/dist/channel/voice-handler.js +138 -0
- package/dist/channel/voice-handler.js.map +1 -0
- package/dist/channel.d.ts +118 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +478 -0
- package/dist/channel.js.map +1 -0
- package/dist/channel.test-d.d.ts +2 -0
- package/dist/channel.test-d.d.ts.map +1 -0
- package/dist/channel.test-d.js +5 -0
- package/dist/channel.test-d.js.map +1 -0
- package/dist/format.d.ts +38 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +237 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +37 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +42 -0
- package/dist/keys.js.map +1 -0
- package/dist/pair-flow.d.ts +18 -0
- package/dist/pair-flow.d.ts.map +1 -0
- package/dist/pair-flow.js +119 -0
- package/dist/pair-flow.js.map +1 -0
- package/dist/pairing.d.ts +90 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +97 -0
- package/dist/pairing.js.map +1 -0
- package/dist/permission.d.ts +25 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +49 -0
- package/dist/permission.js.map +1 -0
- package/dist/render.d.ts +68 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +439 -0
- package/dist/render.js.map +1 -0
- package/dist/setup-wizard.d.ts +21 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +140 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +91 -0
- package/src/approval.test.ts +103 -0
- package/src/approval.ts +86 -0
- package/src/channel/approval-prompt.ts +46 -0
- package/src/channel/callback-handler.test.ts +188 -0
- package/src/channel/callback-handler.ts +256 -0
- package/src/channel/frame-pump.ts +176 -0
- package/src/channel/html.test.ts +40 -0
- package/src/channel/html.ts +60 -0
- package/src/channel/pairing-handler.ts +181 -0
- package/src/channel/permission-prompt.ts +51 -0
- package/src/channel/slash-handler.ts +259 -0
- package/src/channel/text-handler.test.ts +222 -0
- package/src/channel/text-handler.ts +174 -0
- package/src/channel/turn-runner.ts +102 -0
- package/src/channel/typing-indicator.ts +31 -0
- package/src/channel/voice-handler.test.ts +328 -0
- package/src/channel/voice-handler.ts +198 -0
- package/src/channel/voice-reply.test.ts +223 -0
- package/src/channel.test-d.ts +7 -0
- package/src/channel.ts +564 -0
- package/src/format.test.ts +159 -0
- package/src/format.ts +257 -0
- package/src/index.ts +347 -0
- package/src/keys.ts +52 -0
- package/src/pair-flow.ts +131 -0
- package/src/pairing.test.ts +113 -0
- package/src/pairing.ts +184 -0
- package/src/permission.test.ts +56 -0
- package/src/permission.ts +61 -0
- package/src/render.test.ts +350 -0
- package/src/render.ts +522 -0
- package/src/setup-wizard.ts +178 -0
- package/src/subcommands.test.ts +153 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { InlineKeyboard } from 'grammy';
|
|
2
|
+
import { isSelectableMode } from '@moxxy/sdk';
|
|
3
|
+
import { resolveVoiceToggle } from '@moxxy/channel-kit';
|
|
4
|
+
/**
|
|
5
|
+
* Install guidance shown when enabling `/voice` with no active synthesizer —
|
|
6
|
+
* mirrors the voice-handler's transcriber guidance wording, pointing at the TTS
|
|
7
|
+
* plugin instead of the STT one.
|
|
8
|
+
*/
|
|
9
|
+
export const VOICE_NO_SYNTH_HINT = 'No text-to-speech backend is configured yet, so replies stay text-only. Install one with `moxxy plugins install tts-openai` and run `moxxy login openai` (or set OPENAI_API_KEY) to enable spoken replies.';
|
|
10
|
+
/**
|
|
11
|
+
* Slash-command dispatcher for the Telegram channel.
|
|
12
|
+
*
|
|
13
|
+
* First tries the shared `session.commands` registry — this is where
|
|
14
|
+
* the universal commands (/info, /clear, /new, /exit, /help) live, so
|
|
15
|
+
* Telegram gets them for free alongside any plugin-contributed
|
|
16
|
+
* commands without needing a switch case here.
|
|
17
|
+
*
|
|
18
|
+
* Falls through to channel-local cases for Telegram-specific UI
|
|
19
|
+
* (model/loop pickers as inline keyboards, /yolo toggle, /tools and
|
|
20
|
+
* /skills as text dumps).
|
|
21
|
+
*/
|
|
22
|
+
export async function runSlash(ctx, text, state, cb) {
|
|
23
|
+
const session = state.session;
|
|
24
|
+
if (!session)
|
|
25
|
+
return;
|
|
26
|
+
const [head, ...rest] = text.split(/\s+/);
|
|
27
|
+
const name = head.slice(1);
|
|
28
|
+
const args = rest.join(' ');
|
|
29
|
+
// 1) Shared registry dispatch.
|
|
30
|
+
const registered = session.commands.get(name);
|
|
31
|
+
if (registered) {
|
|
32
|
+
try {
|
|
33
|
+
const result = await registered.handler({
|
|
34
|
+
channel: 'telegram',
|
|
35
|
+
sessionId: session.id,
|
|
36
|
+
args,
|
|
37
|
+
session,
|
|
38
|
+
});
|
|
39
|
+
if (result.kind === 'text') {
|
|
40
|
+
await ctx.reply(result.text);
|
|
41
|
+
}
|
|
42
|
+
else if (result.kind === 'session-action') {
|
|
43
|
+
await cb.performSessionAction(ctx, result.action, result.notice);
|
|
44
|
+
}
|
|
45
|
+
else if (result.kind === 'error') {
|
|
46
|
+
await ctx.reply(`error: ${result.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
await ctx.reply(`command /${name} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// 2) Channel-local cases.
|
|
55
|
+
switch (head) {
|
|
56
|
+
case '/model':
|
|
57
|
+
await renderModelPicker(ctx, session, state, args);
|
|
58
|
+
return;
|
|
59
|
+
case '/mode':
|
|
60
|
+
await renderModePicker(ctx, session);
|
|
61
|
+
return;
|
|
62
|
+
case '/yolo': {
|
|
63
|
+
const enabled = cb.toggleYolo();
|
|
64
|
+
await ctx.reply(enabled
|
|
65
|
+
? '⚠ yolo mode ON — tool calls auto-approved for the rest of this session'
|
|
66
|
+
: 'yolo mode OFF — tool prompts will resume');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
case '/voice': {
|
|
70
|
+
const result = resolveVoiceToggle({
|
|
71
|
+
arg: args,
|
|
72
|
+
enabled: state.voiceReplies,
|
|
73
|
+
hasSynthesizer: session.synthesizers.tryGetActive() != null,
|
|
74
|
+
delivery: 'a voice note',
|
|
75
|
+
noSynthesizerHint: VOICE_NO_SYNTH_HINT,
|
|
76
|
+
});
|
|
77
|
+
if (result.persist)
|
|
78
|
+
await cb.setVoiceReplies(result.enabled);
|
|
79
|
+
await ctx.reply(result.reply);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
case '/tools': {
|
|
83
|
+
const list = session.tools
|
|
84
|
+
.list()
|
|
85
|
+
.map((t) => `${t.name} — ${t.description}`)
|
|
86
|
+
.join('\n');
|
|
87
|
+
await ctx.reply(list || '(no tools registered)');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case '/skills': {
|
|
91
|
+
const list = session.skills
|
|
92
|
+
.list()
|
|
93
|
+
.map((s) => {
|
|
94
|
+
const triggers = s.frontmatter.triggers ?? [];
|
|
95
|
+
const triggerLine = triggers.length
|
|
96
|
+
? `\n triggers: ${triggers.map((t) => `"${t}"`).join(', ')}`
|
|
97
|
+
: '';
|
|
98
|
+
return `${s.frontmatter.name} — ${s.frontmatter.description}${triggerLine}`;
|
|
99
|
+
})
|
|
100
|
+
.join('\n');
|
|
101
|
+
await ctx.reply(list || '(no skills discovered)');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
await ctx.reply(`unknown command: ${head} (try /help)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Telegram inline keyboards get unwieldy past this many rows; cap the picker. */
|
|
109
|
+
const MODEL_PICKER_CAP = 30;
|
|
110
|
+
async function renderModelPicker(ctx, session, state, filter = '') {
|
|
111
|
+
const providers = session.providers.list();
|
|
112
|
+
if (providers.length === 0) {
|
|
113
|
+
await ctx.reply('no providers registered');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const keyboard = new InlineKeyboard();
|
|
117
|
+
const providerName = session.providers.getActiveName() ?? '';
|
|
118
|
+
const activeModel = state.activeModelOverride ?? state.model ?? '';
|
|
119
|
+
// Same boot-time readiness set the TUI uses to flag unconfigured
|
|
120
|
+
// providers — set by `@moxxy/cli` at setup time. Providers that
|
|
121
|
+
// failed credential resolution get a "(not connected)" suffix
|
|
122
|
+
// and a tap on them surfaces the right setup command instead of
|
|
123
|
+
// a no-op switch.
|
|
124
|
+
const ready = session.readyProviders ?? new Set();
|
|
125
|
+
// `/model <substring>` narrows the list so a model that sorts past the
|
|
126
|
+
// keyboard cap is still reachable from chat (the cap below would otherwise
|
|
127
|
+
// silently hide it with no fallback).
|
|
128
|
+
const needle = filter.trim().toLowerCase();
|
|
129
|
+
// Flatten so we can both apply the substring filter and report how many the
|
|
130
|
+
// cap hides (vs. how many the filter excluded).
|
|
131
|
+
const all = providers.flatMap((p) => p.models.map((m) => ({ provider: p.name, model: m.id })));
|
|
132
|
+
const matched = needle
|
|
133
|
+
? all.filter((e) => `${e.provider}::${e.model}`.toLowerCase().includes(needle))
|
|
134
|
+
: all;
|
|
135
|
+
if (matched.length === 0) {
|
|
136
|
+
await ctx.reply(`no model matches "${filter.trim()}" — try /model with no argument.`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const shown = matched.slice(0, MODEL_PICKER_CAP);
|
|
140
|
+
for (const e of shown) {
|
|
141
|
+
const isCurrent = providerName === e.provider && activeModel === e.model;
|
|
142
|
+
const connected = ready.has(e.provider);
|
|
143
|
+
const label = `${isCurrent ? '• ' : ''}${e.provider}: ${e.model}` +
|
|
144
|
+
(connected ? '' : ' (not connected)');
|
|
145
|
+
keyboard.text(label, `model:${e.provider}::${e.model}`).row();
|
|
146
|
+
}
|
|
147
|
+
const hidden = matched.length - shown.length;
|
|
148
|
+
const prompt = hidden > 0
|
|
149
|
+
? `Pick a model (${hidden} more not shown — narrow with /model <text>):`
|
|
150
|
+
: 'Pick a model:';
|
|
151
|
+
await ctx.reply(prompt, { reply_markup: keyboard });
|
|
152
|
+
}
|
|
153
|
+
async function renderModePicker(ctx, session) {
|
|
154
|
+
// Special modes (e.g. collaborative, entered via /collab) are hidden from the
|
|
155
|
+
// picker the same way they are in the TUI. See ModeDef.special.
|
|
156
|
+
const modes = session.modes.list().filter(isSelectableMode);
|
|
157
|
+
if (modes.length === 0) {
|
|
158
|
+
await ctx.reply('no modes registered');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const keyboard = new InlineKeyboard();
|
|
162
|
+
const activeModeName = (() => {
|
|
163
|
+
try {
|
|
164
|
+
return session.modes.getActive().name;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return '';
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
for (const s of modes) {
|
|
171
|
+
const isCurrent = s.name === activeModeName;
|
|
172
|
+
keyboard.text(`${isCurrent ? '• ' : ''}${s.name}`, `mode:${s.name}`).row();
|
|
173
|
+
}
|
|
174
|
+
await ctx.reply('Pick a mode:', { reply_markup: keyboard });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Push the union of registry commands + Telegram-local commands to
|
|
178
|
+
* Telegram so they appear in the chat's command menu (the "/" picker
|
|
179
|
+
* the official client shows). Best-effort: a network failure here
|
|
180
|
+
* doesn't block channel startup, the commands still work via text.
|
|
181
|
+
*/
|
|
182
|
+
export async function publishBotCommands(bot, session, logger) {
|
|
183
|
+
if (!session || !bot)
|
|
184
|
+
return;
|
|
185
|
+
const LOCAL = [
|
|
186
|
+
{ command: 'model', description: 'Switch provider + model (inline keyboard)' },
|
|
187
|
+
{ command: 'mode', description: 'Switch mode' },
|
|
188
|
+
{ command: 'yolo', description: 'Toggle auto-approve mode' },
|
|
189
|
+
{ command: 'voice', description: 'Toggle spoken voice replies' },
|
|
190
|
+
{ command: 'tools', description: 'List the tools the active session can call' },
|
|
191
|
+
{ command: 'skills', description: 'List the discovered skills' },
|
|
192
|
+
{ command: 'cancel', description: 'Abort the current turn' },
|
|
193
|
+
];
|
|
194
|
+
const shared = session.commands
|
|
195
|
+
.listForChannel('telegram')
|
|
196
|
+
.map((c) => ({ command: c.name, description: c.description }));
|
|
197
|
+
const seen = new Set(shared.map((c) => c.command));
|
|
198
|
+
const merged = [...shared, ...LOCAL.filter((c) => !seen.has(c.command))]
|
|
199
|
+
.sort((a, b) => a.command.localeCompare(b.command))
|
|
200
|
+
// Telegram caps descriptions at 256 chars and rejects empties.
|
|
201
|
+
.map((c) => ({
|
|
202
|
+
command: c.command,
|
|
203
|
+
description: (c.description || c.command).slice(0, 256),
|
|
204
|
+
}));
|
|
205
|
+
try {
|
|
206
|
+
await bot.api.setMyCommands(merged);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
logger?.warn?.('telegram setMyCommands failed', {
|
|
210
|
+
err: err instanceof Error ? err.message : String(err),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=slash-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slash-handler.js","sourceRoot":"","sources":["../../src/channel/slash-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Context } from 'grammy';
|
|
2
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
3
|
+
import type { TelegramApprovalResolver } from '../approval.js';
|
|
4
|
+
import type { TelegramPermissionResolver } from '../permission.js';
|
|
5
|
+
import type { ChannelHandle } from '@moxxy/sdk';
|
|
6
|
+
import type { FramePump } from './frame-pump.js';
|
|
7
|
+
import type { AwaitingApprovalText } from './callback-handler.js';
|
|
8
|
+
import type { PairingHandler } from './pairing-handler.js';
|
|
9
|
+
export interface TextHandlerState {
|
|
10
|
+
readonly session: Session | null;
|
|
11
|
+
readonly model: string | undefined;
|
|
12
|
+
readonly activeModelOverride: string | null;
|
|
13
|
+
readonly yolo: boolean;
|
|
14
|
+
readonly voiceReplies: boolean;
|
|
15
|
+
readonly busy: boolean;
|
|
16
|
+
readonly turnController: AbortController | null;
|
|
17
|
+
readonly awaitingApprovalText: AwaitingApprovalText | null;
|
|
18
|
+
readonly handle: ChannelHandle | null;
|
|
19
|
+
}
|
|
20
|
+
export interface TextHandlerDeps {
|
|
21
|
+
readonly pairing: PairingHandler;
|
|
22
|
+
readonly approvalResolver: TelegramApprovalResolver;
|
|
23
|
+
readonly permissionResolver: TelegramPermissionResolver;
|
|
24
|
+
readonly framePump: FramePump;
|
|
25
|
+
}
|
|
26
|
+
export interface TextHandlerCallbacks {
|
|
27
|
+
readonly setAwaitingApprovalText: (state: AwaitingApprovalText | null) => void;
|
|
28
|
+
readonly toggleYolo: () => boolean;
|
|
29
|
+
readonly setYolo: (value: boolean) => void;
|
|
30
|
+
readonly setVoiceReplies: (on: boolean) => Promise<void>;
|
|
31
|
+
readonly runUserTurn: (ctx: Context, chatId: number, text: string) => Promise<void>;
|
|
32
|
+
/** Host-issued pairing fallback: try to pair an unauthorized chat whose message
|
|
33
|
+
* is the 6-digit code. Returns true when handled (so we skip the generic
|
|
34
|
+
* "not paired" reply). See {@link TelegramChannel.tryHostPair}. */
|
|
35
|
+
readonly tryHostPair: (chatId: number, text: string) => Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Top-level dispatch for inbound text messages: authorization gate,
|
|
39
|
+
* awaiting-approval-text capture, /cancel, slash routing, and finally
|
|
40
|
+
* the user-turn path.
|
|
41
|
+
*/
|
|
42
|
+
export declare function handleTextMessage(ctx: Context, state: TextHandlerState, deps: TextHandlerDeps, cb: TextHandlerCallbacks): Promise<void>;
|
|
43
|
+
//# sourceMappingURL=text-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-handler.d.ts","sourceRoot":"","sources":["../../src/channel/text-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,cAAc,EAAE,eAAe,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,oBAAoB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,wBAAwB,CAAC;IACpD,QAAQ,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;IACxD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,uBAAuB,EAAE,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,OAAO,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,eAAe,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF;;wEAEoE;IACpE,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,oBAAoB,GACvB,OAAO,CAAC,IAAI,CAAC,CAqEf"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { runSlash } from './slash-handler.js';
|
|
2
|
+
/**
|
|
3
|
+
* Top-level dispatch for inbound text messages: authorization gate,
|
|
4
|
+
* awaiting-approval-text capture, /cancel, slash routing, and finally
|
|
5
|
+
* the user-turn path.
|
|
6
|
+
*/
|
|
7
|
+
export async function handleTextMessage(ctx, state, deps, cb) {
|
|
8
|
+
const chatId = ctx.chat?.id;
|
|
9
|
+
const text = ctx.message?.text;
|
|
10
|
+
if (!chatId || !text)
|
|
11
|
+
return;
|
|
12
|
+
if (!deps.pairing.isAuthorized(chatId)) {
|
|
13
|
+
// Host-issued pairing: a bare 6-digit message from an unpaired chat may be
|
|
14
|
+
// the code presented back to us. If so, pair and stop here.
|
|
15
|
+
if (await cb.tryHostPair(chatId, text))
|
|
16
|
+
return;
|
|
17
|
+
await ctx.reply('This bot is paired with a different chat (or not paired yet). Run `moxxy telegram pair` to (re-)pair.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Capture awaiting-text BEFORE the busy guard so the user can answer
|
|
21
|
+
// an approval text prompt even while the strategy is technically
|
|
22
|
+
// still mid-turn (it's pending on us).
|
|
23
|
+
if (state.awaitingApprovalText) {
|
|
24
|
+
const { approvalId, optionId } = state.awaitingApprovalText;
|
|
25
|
+
cb.setAwaitingApprovalText(null);
|
|
26
|
+
const handled = deps.approvalResolver.resolvePendingWithText(approvalId, optionId, text);
|
|
27
|
+
if (handled) {
|
|
28
|
+
await ctx.reply(`✓ submitted (${optionId})`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
await ctx.reply('that approval is no longer pending');
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// /cancel works even while busy; everything else routes through
|
|
36
|
+
// runSlash or the user-turn path.
|
|
37
|
+
if (text === '/cancel') {
|
|
38
|
+
if (state.turnController && !state.turnController.signal.aborted) {
|
|
39
|
+
state.turnController.abort('user cancel');
|
|
40
|
+
await ctx.reply('cancelling current turn…');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await ctx.reply('nothing to cancel.');
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (text.startsWith('/')) {
|
|
48
|
+
await runSlash(ctx, text, {
|
|
49
|
+
session: state.session,
|
|
50
|
+
model: state.model,
|
|
51
|
+
activeModelOverride: state.activeModelOverride,
|
|
52
|
+
yolo: state.yolo,
|
|
53
|
+
voiceReplies: state.voiceReplies,
|
|
54
|
+
}, {
|
|
55
|
+
toggleYolo: cb.toggleYolo,
|
|
56
|
+
setVoiceReplies: cb.setVoiceReplies,
|
|
57
|
+
performSessionAction: (c, action, notice) => performSessionAction(c, action, notice, state, deps, cb),
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (state.busy) {
|
|
62
|
+
await ctx.reply('I am still working on the previous prompt. Send /cancel to abort it.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await cb.runUserTurn(ctx, chatId, text);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Channel-side handler for `session-action` outputs from registered
|
|
69
|
+
* commands. The TUI does the same thing; both channels translate the
|
|
70
|
+
* action into their own UI semantics (Telegram = reply text, Ink =
|
|
71
|
+
* setState + exit).
|
|
72
|
+
*/
|
|
73
|
+
async function performSessionAction(ctx, action, notice, state, deps, cb) {
|
|
74
|
+
if (!state.session)
|
|
75
|
+
return;
|
|
76
|
+
if (action === 'exit') {
|
|
77
|
+
await ctx.reply(notice ?? 'closing Telegram channel');
|
|
78
|
+
if (state.handle)
|
|
79
|
+
await state.handle.stop('user /exit');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (action === 'clear') {
|
|
83
|
+
deps.framePump.resetRenderer();
|
|
84
|
+
if (notice)
|
|
85
|
+
await ctx.reply(`✓ ${notice}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (action === 'new') {
|
|
89
|
+
if (state.turnController && !state.turnController.signal.aborted) {
|
|
90
|
+
state.turnController.abort('user reset');
|
|
91
|
+
}
|
|
92
|
+
deps.framePump.resetRenderer();
|
|
93
|
+
cb.setYolo(false);
|
|
94
|
+
cb.setAwaitingApprovalText(null);
|
|
95
|
+
deps.approvalResolver.abortAll('session reset');
|
|
96
|
+
deps.permissionResolver.abortAll('session reset');
|
|
97
|
+
// Wipe the history at its source. On a RemoteSession `reset()` asks the
|
|
98
|
+
// runner to clear its authoritative log (and persisted JSONL) and
|
|
99
|
+
// re-sync every attached mirror; on a local Session it clears the
|
|
100
|
+
// EventLog + truncates the sidecar. A mirror-only `log.clear()` would
|
|
101
|
+
// be cosmetic AND desync this client, so only claim success when the
|
|
102
|
+
// reset actually happened.
|
|
103
|
+
try {
|
|
104
|
+
if (typeof state.session.reset === 'function')
|
|
105
|
+
await state.session.reset();
|
|
106
|
+
else
|
|
107
|
+
state.session.log.clear();
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
await ctx.reply(`⚠ /new failed: ${err instanceof Error ? err.message : String(err)} — history NOT cleared`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
await ctx.reply(`✓ ${notice ?? 'new session — conversation history cleared'}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=text-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-handler.js","sourceRoot":"","sources":["../../src/channel/text-handler.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAmC9C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAY,EACZ,KAAuB,EACvB,IAAqB,EACrB,EAAwB;IAExB,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;IAC5B,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC;IAC/B,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI;QAAE,OAAO;IAE7B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;QACvC,2EAA2E;QAC3E,4DAA4D;QAC5D,IAAI,MAAM,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC;YAAE,OAAO;QAC/C,MAAM,GAAG,CAAC,KAAK,CACb,uGAAuG,CACxG,CAAC;QACF,OAAO;IACT,CAAC;IAED,qEAAqE;IACrE,iEAAiE;IACjE,uCAAuC;IACvC,IAAI,KAAK,CAAC,oBAAoB,EAAE,CAAC;QAC/B,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,oBAAoB,CAAC;QAC5D,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,sBAAsB,CAAC,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QACzF,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,GAAG,CAAC,KAAK,CAAC,gBAAgB,QAAQ,GAAG,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACxD,CAAC;QACD,OAAO;IACT,CAAC;IAED,gEAAgE;IAChE,kCAAkC;IAClC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACjE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAC1C,MAAM,GAAG,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,QAAQ,CACZ,GAAG,EACH,IAAI,EACJ;YACE,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,mBAAmB,EAAE,KAAK,CAAC,mBAAmB;YAC9C,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,YAAY,EAAE,KAAK,CAAC,YAAY;SACjC,EACD;YACE,UAAU,EAAE,EAAE,CAAC,UAAU;YACzB,eAAe,EAAE,EAAE,CAAC,eAAe;YACnC,oBAAoB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,CAC1C,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;SAC3D,CACF,CAAC;QACF,OAAO;IACT,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,GAAG,CAAC,KAAK,CAAC,sEAAsE,CAAC,CAAC;QACxF,OAAO;IACT,CAAC;IAED,MAAM,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,oBAAoB,CACjC,GAAY,EACZ,MAAgC,EAChC,MAA0B,EAC1B,KAAuB,EACvB,IAAqB,EACrB,EAAwB;IAExB,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO;IAC3B,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACtB,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,0BAA0B,CAAC,CAAC;QACtD,IAAI,KAAK,CAAC,MAAM;YAAE,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACxD,OAAO;IACT,CAAC;IACD,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,CAAC;QAC/B,IAAI,MAAM;YAAE,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC;QAC3C,OAAO;IACT,CAAC;IACD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QACrB,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACjE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,CAAC;QAC/B,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAClB,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;QAChD,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;QAClD,wEAAwE;QACxE,kEAAkE;QAClE,kEAAkE;QAClE,sEAAsE;QACtE,qEAAqE;QACrE,2BAA2B;QAC3B,IAAI,CAAC;YACH,IAAI,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,KAAK,UAAU;gBAAE,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;;gBACtE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QACjC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,CAAC,KAAK,CACb,kBAAkB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,wBAAwB,CAC3F,CAAC;YACF,OAAO;QACT,CAAC;QACD,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,MAAM,IAAI,4CAA4C,EAAE,CAAC,CAAC;IACjF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Bot, Context } from 'grammy';
|
|
2
|
+
import type { newTurnId } from '@moxxy/core';
|
|
3
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
4
|
+
import type { FramePump } from './frame-pump.js';
|
|
5
|
+
import type { TypingIndicator } from './typing-indicator.js';
|
|
6
|
+
export interface TurnRunnerLogger {
|
|
7
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
8
|
+
}
|
|
9
|
+
export interface TurnRunnerDeps {
|
|
10
|
+
readonly session: Session;
|
|
11
|
+
readonly bot: Bot | null;
|
|
12
|
+
readonly framePump: FramePump;
|
|
13
|
+
readonly typing: TypingIndicator;
|
|
14
|
+
readonly logger?: TurnRunnerLogger;
|
|
15
|
+
/**
|
|
16
|
+
* Called once with the FINAL assistant text after it has been flushed to the
|
|
17
|
+
* chat (so the text reply always lands first). Backs the optional voice
|
|
18
|
+
* reply. Best-effort — its failure is logged and never breaks the text turn.
|
|
19
|
+
*/
|
|
20
|
+
readonly onFinalReply?: (text: string) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface TurnRunnerOptions {
|
|
23
|
+
readonly chatId: number;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly model: string | undefined;
|
|
26
|
+
readonly controller: AbortController;
|
|
27
|
+
/** turnId for this turn. The channel mints it so it can also record it as an
|
|
28
|
+
* own-turn id (mirrorForeignTurn filters on those). */
|
|
29
|
+
readonly turnId: ReturnType<typeof newTurnId>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Drive a single user turn end-to-end: kick off typing, subscribe the
|
|
33
|
+
* frame pump to session events, run the turn through `runTurn`, flush
|
|
34
|
+
* the final frame, and unwind state in `finally`.
|
|
35
|
+
*
|
|
36
|
+
* The controller is owned by the caller so /cancel can abort just this
|
|
37
|
+
* turn without poisoning the session-level signal.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runUserTurn(ctx: Context, deps: TurnRunnerDeps, opts: TurnRunnerOptions): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=turn-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-runner.d.ts","sourceRoot":"","sources":["../../src/channel/turn-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAE3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzD;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC;4DACwD;IACxD,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,IAAI,CAAC,CAsDf"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { driveTurn, subscribeTurn } from '@moxxy/channel-kit';
|
|
2
|
+
/**
|
|
3
|
+
* Drive a single user turn end-to-end: kick off typing, subscribe the
|
|
4
|
+
* frame pump to session events, run the turn through `runTurn`, flush
|
|
5
|
+
* the final frame, and unwind state in `finally`.
|
|
6
|
+
*
|
|
7
|
+
* The controller is owned by the caller so /cancel can abort just this
|
|
8
|
+
* turn without poisoning the session-level signal.
|
|
9
|
+
*/
|
|
10
|
+
export async function runUserTurn(ctx, deps, opts) {
|
|
11
|
+
const { session, bot, framePump, typing, logger, onFinalReply } = deps;
|
|
12
|
+
const { chatId, text, model, controller, turnId } = opts;
|
|
13
|
+
framePump.beginTurn(chatId);
|
|
14
|
+
// Kick off "typing…" right away so the user gets immediate feedback.
|
|
15
|
+
// Don't send an ellipsis placeholder message — the typing indicator
|
|
16
|
+
// IS the placeholder. The frame pump lazily sends the first real
|
|
17
|
+
// frame when there's content to display, then edits that message for
|
|
18
|
+
// every subsequent frame.
|
|
19
|
+
typing.start(bot, chatId);
|
|
20
|
+
// turnId is minted by the caller (the channel records it as an own-turn id).
|
|
21
|
+
// The frame-pump subscriber filters by it: `session.log` fans out to every
|
|
22
|
+
// listener; without this a concurrent turn driven by another channel
|
|
23
|
+
// (HTTP/runner) on the same Session would render into THIS chat. (AGENTS.md:
|
|
24
|
+
// filter event-log subscribers by turnId.)
|
|
25
|
+
const unsubscribe = subscribeTurn(session, turnId, (event) => {
|
|
26
|
+
const frame = framePump.renderState.accept(event);
|
|
27
|
+
if (frame.hasUpdate)
|
|
28
|
+
framePump.scheduleEdit();
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
await driveTurn(session, { turnId, prompt: text, model, signal: controller.signal });
|
|
32
|
+
await framePump.flush(true);
|
|
33
|
+
// The text reply is now out. Speak the final assistant body if a voice
|
|
34
|
+
// reply is wired — isolated so a synth/transcode/transport failure can
|
|
35
|
+
// never break (or re-report) the already-delivered text turn.
|
|
36
|
+
if (onFinalReply) {
|
|
37
|
+
const finalText = framePump.renderState.snapshot().body;
|
|
38
|
+
if (finalText.trim()) {
|
|
39
|
+
try {
|
|
40
|
+
await onFinalReply(finalText);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
logger?.warn('telegram voice reply hook failed', {
|
|
44
|
+
err: err instanceof Error ? err.message : String(err),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
logger?.warn('telegram turn failed', {
|
|
52
|
+
err: err instanceof Error ? err.message : String(err),
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
await ctx.reply(`Turn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
typing.stop();
|
|
63
|
+
unsubscribe();
|
|
64
|
+
framePump.endTurn();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=turn-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-runner.js","sourceRoot":"","sources":["../../src/channel/turn-runner.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAgC9D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAY,EACZ,IAAoB,EACpB,IAAuB;IAEvB,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACvE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEzD,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC5B,qEAAqE;IACrE,oEAAoE;IACpE,iEAAiE;IACjE,qEAAqE;IACrE,0BAA0B;IAC1B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAE1B,6EAA6E;IAC7E,2EAA2E;IAC3E,qEAAqE;IACrE,6EAA6E;IAC7E,2CAA2C;IAC3C,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,KAAK,CAAC,SAAS;YAAE,SAAS,CAAC,YAAY,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QACrF,MAAM,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,uEAAuE;QACvE,uEAAuE;QACvE,8DAA8D;QAC9D,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,SAAS,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC;YACxD,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC;gBACrB,IAAI,CAAC;oBACH,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;gBAChC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,EAAE,IAAI,CAAC,kCAAkC,EAAE;wBAC/C,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;qBACtD,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,IAAI,CAAC,sBAAsB,EAAE;YACnC,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACtD,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,KAAK,CAAC,gBAAgB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,IAAI,EAAE,CAAC;QACd,WAAW,EAAE,CAAC;QACd,SAAS,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Bot } from 'grammy';
|
|
2
|
+
/**
|
|
3
|
+
* Show a "typing…" indicator in the chat for the lifetime of a turn.
|
|
4
|
+
* Telegram clears the indicator ~5s after the last sendChatAction, so
|
|
5
|
+
* we re-fire every 4s. Best-effort — a single failure shouldn't crash
|
|
6
|
+
* the turn (we keep the interval going so transient network blips
|
|
7
|
+
* recover on the next tick).
|
|
8
|
+
*/
|
|
9
|
+
export declare class TypingIndicator {
|
|
10
|
+
private timer;
|
|
11
|
+
start(bot: Bot | null, chatId: number): void;
|
|
12
|
+
stop(): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=typing-indicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typing-indicator.d.ts","sourceRoot":"","sources":["../../src/channel/typing-indicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC;;;;;;GAMG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAA+C;IAE5D,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAY5C,IAAI,IAAI,IAAI;CAMb"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show a "typing…" indicator in the chat for the lifetime of a turn.
|
|
3
|
+
* Telegram clears the indicator ~5s after the last sendChatAction, so
|
|
4
|
+
* we re-fire every 4s. Best-effort — a single failure shouldn't crash
|
|
5
|
+
* the turn (we keep the interval going so transient network blips
|
|
6
|
+
* recover on the next tick).
|
|
7
|
+
*/
|
|
8
|
+
export class TypingIndicator {
|
|
9
|
+
timer = null;
|
|
10
|
+
start(bot, chatId) {
|
|
11
|
+
if (!bot)
|
|
12
|
+
return;
|
|
13
|
+
this.stop();
|
|
14
|
+
const fire = () => {
|
|
15
|
+
bot.api.sendChatAction(chatId, 'typing').catch(() => {
|
|
16
|
+
/* best-effort */
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
fire();
|
|
20
|
+
this.timer = setInterval(fire, 4_000);
|
|
21
|
+
}
|
|
22
|
+
stop() {
|
|
23
|
+
if (this.timer) {
|
|
24
|
+
clearInterval(this.timer);
|
|
25
|
+
this.timer = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=typing-indicator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typing-indicator.js","sourceRoot":"","sources":["../../src/channel/typing-indicator.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,OAAO,eAAe;IAClB,KAAK,GAA0C,IAAI,CAAC;IAE5D,KAAK,CAAC,GAAe,EAAE,MAAc;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,GAAS,EAAE;YACtB,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;gBAClD,iBAAiB;YACnB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QACF,IAAI,EAAE,CAAC;QACP,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Context } from 'grammy';
|
|
2
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
3
|
+
import type { PairingHandler } from './pairing-handler.js';
|
|
4
|
+
export interface VoiceHandlerState {
|
|
5
|
+
readonly session: Session | null;
|
|
6
|
+
readonly busy: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface VoiceHandlerDeps {
|
|
9
|
+
readonly pairing: PairingHandler;
|
|
10
|
+
/** Bot token, used to build the Telegram file-download URL. */
|
|
11
|
+
readonly token: string;
|
|
12
|
+
readonly logger?: {
|
|
13
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface VoiceHandlerCallbacks {
|
|
17
|
+
readonly runUserTurn: (ctx: Context, chatId: number, text: string) => Promise<void>;
|
|
18
|
+
/** Override the network fetch for tests. Defaults to global `fetch`. */
|
|
19
|
+
readonly fetchAudio?: (url: string, init?: {
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}) => Promise<{
|
|
22
|
+
ok: boolean;
|
|
23
|
+
/** HTTP status — surfaced in the download-failure log for diagnostics. */
|
|
24
|
+
status?: number;
|
|
25
|
+
statusText?: string;
|
|
26
|
+
/** Content-Length header, when present — used to reject oversized bodies pre-buffer. */
|
|
27
|
+
headers?: {
|
|
28
|
+
get(name: string): string | null;
|
|
29
|
+
};
|
|
30
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Handle inbound voice notes and uploaded audio files. Authorization
|
|
35
|
+
* gate matches the text path. If no Transcriber is registered on the
|
|
36
|
+
* session, reply with a one-time guidance message instead of silently
|
|
37
|
+
* swallowing the audio. Otherwise: download via Bot-API file URL,
|
|
38
|
+
* transcribe through the active Transcriber, echo a short "heard:"
|
|
39
|
+
* confirmation, then run a normal user turn with the transcript.
|
|
40
|
+
*/
|
|
41
|
+
export declare function handleVoiceMessage(ctx: Context, state: VoiceHandlerState, deps: VoiceHandlerDeps, cb: VoiceHandlerCallbacks): Promise<void>;
|
|
42
|
+
//# sourceMappingURL=voice-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-handler.d.ts","sourceRoot":"","sources":["../../src/channel/voice-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAkB3D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC;CAC/E;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAC5B,OAAO,CAAC;QACX,EAAE,EAAE,OAAO,CAAC;QACZ,0EAA0E;QAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,wFAAwF;QACxF,OAAO,CAAC,EAAE;YAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC;QAC/C,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;KACrC,CAAC,CAAC;CACJ;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,EACtB,EAAE,EAAE,qBAAqB,GACxB,OAAO,CAAC,IAAI,CAAC,CAuIf"}
|