@sympoies/dsh-telegram 0.0.0-bootstrap.0 → 0.6.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 +1 -2
- package/NOTICE +19 -0
- package/README.md +589 -2
- package/cordis.patch.yml +14 -0
- package/lib/access.d.ts +76 -0
- package/lib/access.d.ts.map +1 -0
- package/lib/access.js +137 -0
- package/lib/access.js.map +1 -0
- package/lib/client.js +835 -0
- package/lib/commands.d.ts +51 -0
- package/lib/commands.d.ts.map +1 -0
- package/lib/commands.js +83 -0
- package/lib/commands.js.map +1 -0
- package/lib/config.d.ts +125 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +97 -0
- package/lib/config.js.map +1 -0
- package/lib/diagnostics.d.ts +58 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +59 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/failures.d.ts +71 -0
- package/lib/failures.d.ts.map +1 -0
- package/lib/failures.js +139 -0
- package/lib/failures.js.map +1 -0
- package/lib/harness/host.d.ts +106 -0
- package/lib/harness/host.d.ts.map +1 -0
- package/lib/harness/host.js +142 -0
- package/lib/harness/host.js.map +1 -0
- package/lib/harness/message.d.ts +45 -0
- package/lib/harness/message.d.ts.map +1 -0
- package/lib/harness/message.js +28 -0
- package/lib/harness/message.js.map +1 -0
- package/lib/harness/model-selection.d.ts +82 -0
- package/lib/harness/model-selection.d.ts.map +1 -0
- package/lib/harness/model-selection.js +103 -0
- package/lib/harness/model-selection.js.map +1 -0
- package/lib/harness/questions-seam.d.ts +34 -0
- package/lib/harness/questions-seam.d.ts.map +1 -0
- package/lib/harness/questions-seam.js +45 -0
- package/lib/harness/questions-seam.js.map +1 -0
- package/lib/harness/types.d.ts +108 -0
- package/lib/harness/types.d.ts.map +1 -0
- package/lib/harness/types.js +23 -0
- package/lib/harness/types.js.map +1 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +977 -0
- package/lib/index.js.map +1 -0
- package/lib/interact/approvals.d.ts +59 -0
- package/lib/interact/approvals.d.ts.map +1 -0
- package/lib/interact/approvals.js +132 -0
- package/lib/interact/approvals.js.map +1 -0
- package/lib/interact/pending.d.ts +56 -0
- package/lib/interact/pending.d.ts.map +1 -0
- package/lib/interact/pending.js +89 -0
- package/lib/interact/pending.js.map +1 -0
- package/lib/interact/questions.d.ts +100 -0
- package/lib/interact/questions.d.ts.map +1 -0
- package/lib/interact/questions.js +255 -0
- package/lib/interact/questions.js.map +1 -0
- package/lib/interact/surface.d.ts +33 -0
- package/lib/interact/surface.d.ts.map +1 -0
- package/lib/interact/surface.js +39 -0
- package/lib/interact/surface.js.map +1 -0
- package/lib/interact/text-capture.d.ts +38 -0
- package/lib/interact/text-capture.d.ts.map +1 -0
- package/lib/interact/text-capture.js +75 -0
- package/lib/interact/text-capture.js.map +1 -0
- package/lib/media/collect.d.ts +157 -0
- package/lib/media/collect.d.ts.map +1 -0
- package/lib/media/collect.js +235 -0
- package/lib/media/collect.js.map +1 -0
- package/lib/media/extractor.d.ts +117 -0
- package/lib/media/extractor.d.ts.map +1 -0
- package/lib/media/extractor.js +207 -0
- package/lib/media/extractor.js.map +1 -0
- package/lib/media/intake.d.ts +57 -0
- package/lib/media/intake.d.ts.map +1 -0
- package/lib/media/intake.js +115 -0
- package/lib/media/intake.js.map +1 -0
- package/lib/media/limits.d.ts +69 -0
- package/lib/media/limits.d.ts.map +1 -0
- package/lib/media/limits.js +97 -0
- package/lib/media/limits.js.map +1 -0
- package/lib/media/ocr.d.ts +66 -0
- package/lib/media/ocr.d.ts.map +1 -0
- package/lib/media/ocr.js +131 -0
- package/lib/media/ocr.js.map +1 -0
- package/lib/media/screenshot.d.ts +57 -0
- package/lib/media/screenshot.d.ts.map +1 -0
- package/lib/media/screenshot.js +101 -0
- package/lib/media/screenshot.js.map +1 -0
- package/lib/media/vision.d.ts +60 -0
- package/lib/media/vision.d.ts.map +1 -0
- package/lib/media/vision.js +74 -0
- package/lib/media/vision.js.map +1 -0
- package/lib/render/clamp.d.ts +34 -0
- package/lib/render/clamp.d.ts.map +1 -0
- package/lib/render/clamp.js +65 -0
- package/lib/render/clamp.js.map +1 -0
- package/lib/render/escape.d.ts +15 -0
- package/lib/render/escape.d.ts.map +1 -0
- package/lib/render/escape.js +17 -0
- package/lib/render/escape.js.map +1 -0
- package/lib/reply/activity.d.ts +31 -0
- package/lib/reply/activity.d.ts.map +1 -0
- package/lib/reply/activity.js +88 -0
- package/lib/reply/activity.js.map +1 -0
- package/lib/reply/rich-stream.d.ts +173 -0
- package/lib/reply/rich-stream.d.ts.map +1 -0
- package/lib/reply/rich-stream.js +286 -0
- package/lib/reply/rich-stream.js.map +1 -0
- package/lib/reply/split-markdown.d.ts +21 -0
- package/lib/reply/split-markdown.d.ts.map +1 -0
- package/lib/reply/split-markdown.js +87 -0
- package/lib/reply/split-markdown.js.map +1 -0
- package/lib/reply/turn-bridge.d.ts +143 -0
- package/lib/reply/turn-bridge.d.ts.map +1 -0
- package/lib/reply/turn-bridge.js +182 -0
- package/lib/reply/turn-bridge.js.map +1 -0
- package/lib/router.d.ts +415 -0
- package/lib/router.d.ts.map +1 -0
- package/lib/router.js +663 -0
- package/lib/router.js.map +1 -0
- package/lib/secrets.d.ts +30 -0
- package/lib/secrets.d.ts.map +1 -0
- package/lib/secrets.js +51 -0
- package/lib/secrets.js.map +1 -0
- package/lib/session/bindings.d.ts +81 -0
- package/lib/session/bindings.d.ts.map +1 -0
- package/lib/session/bindings.js +170 -0
- package/lib/session/bindings.js.map +1 -0
- package/lib/session/history.d.ts +50 -0
- package/lib/session/history.d.ts.map +1 -0
- package/lib/session/history.js +127 -0
- package/lib/session/history.js.map +1 -0
- package/lib/session/models.d.ts +99 -0
- package/lib/session/models.d.ts.map +1 -0
- package/lib/session/models.js +123 -0
- package/lib/session/models.js.map +1 -0
- package/lib/session/permission.d.ts +83 -0
- package/lib/session/permission.d.ts.map +1 -0
- package/lib/session/permission.js +107 -0
- package/lib/session/permission.js.map +1 -0
- package/lib/session/picker.d.ts +49 -0
- package/lib/session/picker.d.ts.map +1 -0
- package/lib/session/picker.js +107 -0
- package/lib/session/picker.js.map +1 -0
- package/lib/session/preferences.d.ts +55 -0
- package/lib/session/preferences.d.ts.map +1 -0
- package/lib/session/preferences.js +102 -0
- package/lib/session/preferences.js.map +1 -0
- package/lib/session/recovery.d.ts +66 -0
- package/lib/session/recovery.d.ts.map +1 -0
- package/lib/session/recovery.js +112 -0
- package/lib/session/recovery.js.map +1 -0
- package/lib/session/runner.d.ts +223 -0
- package/lib/session/runner.d.ts.map +1 -0
- package/lib/session/runner.js +260 -0
- package/lib/session/runner.js.map +1 -0
- package/lib/session/workspaces.d.ts +28 -0
- package/lib/session/workspaces.d.ts.map +1 -0
- package/lib/session/workspaces.js +53 -0
- package/lib/session/workspaces.js.map +1 -0
- package/lib/telegram/addressing.d.ts +39 -0
- package/lib/telegram/addressing.d.ts.map +1 -0
- package/lib/telegram/addressing.js +81 -0
- package/lib/telegram/addressing.js.map +1 -0
- package/lib/telegram/albums.d.ts +46 -0
- package/lib/telegram/albums.d.ts.map +1 -0
- package/lib/telegram/albums.js +104 -0
- package/lib/telegram/albums.js.map +1 -0
- package/lib/telegram/api.d.ts +238 -0
- package/lib/telegram/api.d.ts.map +1 -0
- package/lib/telegram/api.js +518 -0
- package/lib/telegram/api.js.map +1 -0
- package/lib/telegram/poller.d.ts +59 -0
- package/lib/telegram/poller.d.ts.map +1 -0
- package/lib/telegram/poller.js +94 -0
- package/lib/telegram/poller.js.map +1 -0
- package/lib/telegram/types.d.ts +130 -0
- package/lib/telegram/types.d.ts.map +1 -0
- package/lib/telegram/types.js +9 -0
- package/lib/telegram/types.js.map +1 -0
- package/lib/telegram/typing.d.ts +47 -0
- package/lib/telegram/typing.d.ts.map +1 -0
- package/lib/telegram/typing.js +91 -0
- package/lib/telegram/typing.js.map +1 -0
- package/lib/versions.d.ts +63 -0
- package/lib/versions.d.ts.map +1 -0
- package/lib/versions.js +135 -0
- package/lib/versions.js.map +1 -0
- package/package.json +94 -10
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One harness session per Telegram conversation.
|
|
3
|
+
*
|
|
4
|
+
* The lifecycle has three states and the transitions between them are the
|
|
5
|
+
* whole job: a conversation with no session yet, one whose session is loaded
|
|
6
|
+
* in this process, and one whose session exists only in the durable log
|
|
7
|
+
* because the harness has restarted since.
|
|
8
|
+
*
|
|
9
|
+
* The third case is why `resume` exists and why a failed resume is not fatal.
|
|
10
|
+
* A session log can be pruned, moved, or written by an incompatible version;
|
|
11
|
+
* when it can no longer be loaded, the user gets a fresh conversation and a
|
|
12
|
+
* note, which is far better than a bot that answers every message with the
|
|
13
|
+
* same load error.
|
|
14
|
+
*
|
|
15
|
+
* The agent host is injected rather than reached for directly, so this logic
|
|
16
|
+
* is exercised without a running harness.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import { SILENT_LOGGER } from '../harness/types.js';
|
|
20
|
+
import { escapeHtml } from '../render/escape.js';
|
|
21
|
+
/** The first words of a prompt, for labelling the conversation in a list. */
|
|
22
|
+
function firstText(content) {
|
|
23
|
+
const said = content.find((part) => part.type === 'text');
|
|
24
|
+
return said?.type === 'text' && said.text.trim() !== '' ? said.text : undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Whether a prompt carries an image, which changes a conversation for good. */
|
|
27
|
+
function carriesImage(content) {
|
|
28
|
+
return content.some((part) => part.type === 'image');
|
|
29
|
+
}
|
|
30
|
+
export class SessionRunner {
|
|
31
|
+
options;
|
|
32
|
+
logger;
|
|
33
|
+
newSessionId;
|
|
34
|
+
/** Serialises work per conversation, so two fast messages cannot both create. */
|
|
35
|
+
chains = new Map();
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.options = options;
|
|
38
|
+
this.logger = options.logger ?? SILENT_LOGGER;
|
|
39
|
+
this.newSessionId = options.newSessionId ?? (() => `tg-${randomUUID()}`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Deliver a prompt, opening or resuming the conversation's session first.
|
|
43
|
+
*
|
|
44
|
+
* @param target - the conversation the prompt came from.
|
|
45
|
+
* @param text - what the user typed.
|
|
46
|
+
*/
|
|
47
|
+
async prompt(target, content) {
|
|
48
|
+
if (content.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
// Asked once and used three times: whether to read the picture elsewhere,
|
|
51
|
+
// whether to refuse it, and whether to move the conversation off its own
|
|
52
|
+
// model. A model that can look wants none of that done for it.
|
|
53
|
+
const sees = (await this.options.modelSees?.(target).catch(() => false)) === true;
|
|
54
|
+
// Resolved before the conversation's agent is touched at all, and outside
|
|
55
|
+
// the serialisation: reading an image takes a model call, and holding the
|
|
56
|
+
// conversation's queue for it would stall every later message behind it.
|
|
57
|
+
const resolved = sees ? [...content] : await this.resolve(target, content);
|
|
58
|
+
if (resolved.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
await this.serialize(target, async () => {
|
|
61
|
+
const agent = await this.agentFor(target);
|
|
62
|
+
// Only reached when an image survived resolution. Set before the prompt:
|
|
63
|
+
// the harness reads the selection while assembling each step, so the
|
|
64
|
+
// override must be in place by the time the turn runs.
|
|
65
|
+
agent.useModel(this.routeFor(target, resolved, sees));
|
|
66
|
+
// Recorded BEFORE the turn runs, because from this point the session's
|
|
67
|
+
// log carries an image and every later turn must be routed for it.
|
|
68
|
+
if (carriesImage(resolved))
|
|
69
|
+
await this.options.bindings.markImages(target);
|
|
70
|
+
// Recorded with the prompt rather than at creation, because the opening
|
|
71
|
+
// words are what make the conversation recognisable in a list.
|
|
72
|
+
await this.options.history
|
|
73
|
+
?.remember(target, {
|
|
74
|
+
sessionId: agent.sessionId,
|
|
75
|
+
startedAt: Date.now(),
|
|
76
|
+
cwd: this.options.cwdFor(target),
|
|
77
|
+
...(firstText(resolved) === undefined ? {} : { label: firstText(resolved) }),
|
|
78
|
+
})
|
|
79
|
+
.catch(() => undefined);
|
|
80
|
+
agent.followup(resolved);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Turn images into text, where there is anything able to do it.
|
|
85
|
+
*
|
|
86
|
+
* A failure here is not the turn's failure: the picture simply goes through
|
|
87
|
+
* as it is, and the conversation moves to a model that can see it.
|
|
88
|
+
*/
|
|
89
|
+
async resolve(target, content) {
|
|
90
|
+
const extractor = this.options.extractor;
|
|
91
|
+
if (!extractor?.available || !carriesImage(content))
|
|
92
|
+
return [...content];
|
|
93
|
+
try {
|
|
94
|
+
return await extractor.resolve(content, this.options.visionRoute?.(target));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
this.logger.warn('[dsh-telegram] could not read an image; sending it as it is', error);
|
|
98
|
+
return [...content];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Which model this conversation's next step must run on.
|
|
103
|
+
*
|
|
104
|
+
* Not a per-turn choice, though it looks like one. A provider checks the
|
|
105
|
+
* WHOLE request history for images, so once a session's log carries one,
|
|
106
|
+
* every later turn — however plain its own text — fails on a model that
|
|
107
|
+
* cannot see. The override therefore sticks to the conversation from the
|
|
108
|
+
* first image until it is reset with `/new`.
|
|
109
|
+
*/
|
|
110
|
+
routeFor(target, content, sees) {
|
|
111
|
+
// The conversation's own choice always applies. Dropping it here is what
|
|
112
|
+
// made a model chosen with `/model` fall back to the deployment default —
|
|
113
|
+
// and since that choice is the whole reason the model could see, the check
|
|
114
|
+
// for "it can see" was undoing the thing it had just observed.
|
|
115
|
+
const chosen = this.options.chosenRoute?.(target);
|
|
116
|
+
if (sees)
|
|
117
|
+
return chosen;
|
|
118
|
+
const carried = this.options.bindings.forChat(target)?.hasImages === true;
|
|
119
|
+
// An image outranks the conversation's own choice: a model that cannot see
|
|
120
|
+
// fails the whole request, so this is not a preference to honour.
|
|
121
|
+
if (carriesImage(content) || carried)
|
|
122
|
+
return this.options.visionRoute?.(target) ?? chosen;
|
|
123
|
+
return chosen;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Point a conversation at a session it had before.
|
|
127
|
+
*
|
|
128
|
+
* The session is not loaded here: the next message resumes it through the
|
|
129
|
+
* ordinary path, which is also the path that handles a log that can no
|
|
130
|
+
* longer be read.
|
|
131
|
+
*
|
|
132
|
+
* @param target - the conversation.
|
|
133
|
+
* @param sessionId - a session this chat had earlier.
|
|
134
|
+
*/
|
|
135
|
+
async adopt(target, sessionId) {
|
|
136
|
+
await this.serialize(target, async () => {
|
|
137
|
+
const binding = this.options.bindings.forChat(target);
|
|
138
|
+
if (binding?.sessionId === sessionId)
|
|
139
|
+
return;
|
|
140
|
+
// Released rather than disposed: the conversation being left is one the
|
|
141
|
+
// user may well come back to, and disposing would end it.
|
|
142
|
+
await this.options.bindings.bind(target, sessionId);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Forget the conversation's session so the next prompt opens a new one.
|
|
147
|
+
*
|
|
148
|
+
* @param target - the conversation to reset.
|
|
149
|
+
*/
|
|
150
|
+
async reset(target) {
|
|
151
|
+
await this.serialize(target, async () => {
|
|
152
|
+
const binding = this.options.bindings.forChat(target);
|
|
153
|
+
if (!binding)
|
|
154
|
+
return;
|
|
155
|
+
const live = this.options.host.live(binding.sessionId);
|
|
156
|
+
if (live)
|
|
157
|
+
await live.dispose().catch((error) => {
|
|
158
|
+
this.logger.warn('[dsh-telegram] could not dispose the previous agent', error);
|
|
159
|
+
});
|
|
160
|
+
await this.options.bindings.unbind(target);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Cancel whatever the conversation's agent is doing.
|
|
165
|
+
*
|
|
166
|
+
* @returns whether there was a loaded agent to cancel.
|
|
167
|
+
*/
|
|
168
|
+
async stop(target) {
|
|
169
|
+
const binding = this.options.bindings.forChat(target);
|
|
170
|
+
if (!binding)
|
|
171
|
+
return false;
|
|
172
|
+
const live = this.options.host.live(binding.sessionId);
|
|
173
|
+
if (!live)
|
|
174
|
+
return false;
|
|
175
|
+
live.cancel('stopped from Telegram');
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Describe the conversation as rows, for the caller to render.
|
|
180
|
+
*
|
|
181
|
+
* Rows rather than finished markup, because the caller adds its own — the
|
|
182
|
+
* model, the effort, what the agent may do — and stitching those onto a
|
|
183
|
+
* rendered string would mean parsing it back apart.
|
|
184
|
+
*/
|
|
185
|
+
async status(target) {
|
|
186
|
+
const directory = this.options.cwdFor(target);
|
|
187
|
+
const binding = this.options.bindings.forChat(target);
|
|
188
|
+
// Worth naming the directory even with no conversation: it is what the
|
|
189
|
+
// next message opens in, and `/cd` before the first message is reasonable.
|
|
190
|
+
if (!binding) {
|
|
191
|
+
return [
|
|
192
|
+
{ label: 'Session', value: 'none yet — send a message to start one' },
|
|
193
|
+
{ label: 'Directory', value: directory },
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
const live = this.options.host.live(binding.sessionId) !== undefined;
|
|
197
|
+
return [
|
|
198
|
+
{ label: 'Session', value: binding.sessionId },
|
|
199
|
+
{ label: 'Directory', value: directory },
|
|
200
|
+
{ label: 'State', value: live ? 'loaded' : 'idle, resumes on your next message' },
|
|
201
|
+
{
|
|
202
|
+
label: 'Since',
|
|
203
|
+
value: new Date(binding.createdAt).toISOString().replace('T', ' ').slice(0, 16),
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The agent for a conversation: loaded, resumed, or freshly created.
|
|
209
|
+
*
|
|
210
|
+
* A binding whose session cannot be resumed is replaced rather than
|
|
211
|
+
* reported: the user's next message should work.
|
|
212
|
+
*/
|
|
213
|
+
async agentFor(target) {
|
|
214
|
+
const binding = this.options.bindings.forChat(target);
|
|
215
|
+
if (binding) {
|
|
216
|
+
const live = this.options.host.live(binding.sessionId);
|
|
217
|
+
if (live)
|
|
218
|
+
return live;
|
|
219
|
+
const resumed = await this.options.host.resume(binding.sessionId).catch((error) => {
|
|
220
|
+
this.logger.warn(`[dsh-telegram] could not resume '${binding.sessionId}'`, error);
|
|
221
|
+
return undefined;
|
|
222
|
+
});
|
|
223
|
+
if (resumed) {
|
|
224
|
+
this.options.permission?.apply(target, resumed.sessionId);
|
|
225
|
+
return resumed;
|
|
226
|
+
}
|
|
227
|
+
this.logger.warn(`[dsh-telegram] session '${binding.sessionId}' is gone; starting a fresh conversation`);
|
|
228
|
+
await this.options.bindings.unbind(target);
|
|
229
|
+
}
|
|
230
|
+
const sessionId = this.newSessionId();
|
|
231
|
+
const cwd = this.options.cwdFor(target);
|
|
232
|
+
const agent = await this.options.host.create(sessionId, cwd);
|
|
233
|
+
// After creation, because the harness pins an initial permission before
|
|
234
|
+
// publishing the session; switching afterwards is the supported path.
|
|
235
|
+
this.options.permission?.apply(target, sessionId);
|
|
236
|
+
await this.options.bindings.bind(target, sessionId);
|
|
237
|
+
await this.options.history
|
|
238
|
+
?.remember(target, { sessionId, startedAt: Date.now(), cwd })
|
|
239
|
+
.catch((error) => {
|
|
240
|
+
// Losing the entry costs a way back to this conversation, not the
|
|
241
|
+
// conversation itself.
|
|
242
|
+
this.logger.warn('[dsh-telegram] could not record the conversation', error);
|
|
243
|
+
});
|
|
244
|
+
return agent;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Run one conversation's work after its previous work.
|
|
248
|
+
*
|
|
249
|
+
* Two messages arriving in the same poll batch would otherwise both see no
|
|
250
|
+
* binding and both create a session, orphaning the first.
|
|
251
|
+
*/
|
|
252
|
+
serialize(target, task) {
|
|
253
|
+
const key = target.threadId === undefined ? target.chatId : `${target.chatId}#${target.threadId}`;
|
|
254
|
+
const previous = this.chains.get(key) ?? Promise.resolve();
|
|
255
|
+
const next = previous.then(task, task);
|
|
256
|
+
this.chains.set(key, next.then(() => undefined, () => undefined));
|
|
257
|
+
return next;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/session/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working out which directory a `/cd` argument names.
|
|
3
|
+
*
|
|
4
|
+
* The store itself is {@link ChatPreferences}: a chat's directory and a chat's
|
|
5
|
+
* model are the same shape — one durable string per conversation that has to
|
|
6
|
+
* outlive both `/new` and a restart — so they share one implementation.
|
|
7
|
+
*
|
|
8
|
+
* A session's cwd is fixed when the session opens; the sandbox derives its
|
|
9
|
+
* writable root from it and the harness calls that root immutable. Changing
|
|
10
|
+
* directory therefore starts a new conversation, which is why the choice
|
|
11
|
+
* cannot live on the session binding that `/new` discards.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Work out which directory a `/cd` argument names.
|
|
15
|
+
*
|
|
16
|
+
* Pure, so every spelling a person might type is pinned by a test rather than
|
|
17
|
+
* discovered in a chat. Nothing here touches the filesystem — whether the
|
|
18
|
+
* result exists is the caller's question, and a separate one.
|
|
19
|
+
*
|
|
20
|
+
* @param input - what the user typed after `/cd`.
|
|
21
|
+
* @param current - the conversation's directory now, for relative paths.
|
|
22
|
+
* @param home - the user's home directory, for `~`.
|
|
23
|
+
* @returns an absolute, normalized path, or undefined for empty input.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveDirectory(input: string, current: string, home: string): string | undefined;
|
|
26
|
+
/** Whether a stored directory is still usable as one. */
|
|
27
|
+
export declare function isUsableDirectory(value: string): boolean;
|
|
28
|
+
//# sourceMappingURL=workspaces.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/session/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAcpB;AASD,yDAAyD;AACzD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIxD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working out which directory a `/cd` argument names.
|
|
3
|
+
*
|
|
4
|
+
* The store itself is {@link ChatPreferences}: a chat's directory and a chat's
|
|
5
|
+
* model are the same shape — one durable string per conversation that has to
|
|
6
|
+
* outlive both `/new` and a restart — so they share one implementation.
|
|
7
|
+
*
|
|
8
|
+
* A session's cwd is fixed when the session opens; the sandbox derives its
|
|
9
|
+
* writable root from it and the harness calls that root immutable. Changing
|
|
10
|
+
* directory therefore starts a new conversation, which is why the choice
|
|
11
|
+
* cannot live on the session binding that `/new` discards.
|
|
12
|
+
*/
|
|
13
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
14
|
+
/**
|
|
15
|
+
* Work out which directory a `/cd` argument names.
|
|
16
|
+
*
|
|
17
|
+
* Pure, so every spelling a person might type is pinned by a test rather than
|
|
18
|
+
* discovered in a chat. Nothing here touches the filesystem — whether the
|
|
19
|
+
* result exists is the caller's question, and a separate one.
|
|
20
|
+
*
|
|
21
|
+
* @param input - what the user typed after `/cd`.
|
|
22
|
+
* @param current - the conversation's directory now, for relative paths.
|
|
23
|
+
* @param home - the user's home directory, for `~`.
|
|
24
|
+
* @returns an absolute, normalized path, or undefined for empty input.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDirectory(input, current, home) {
|
|
27
|
+
// Paths get pasted from a terminal, and a shell would have eaten the quotes.
|
|
28
|
+
const trimmed = unquote(input.trim());
|
|
29
|
+
if (trimmed === '')
|
|
30
|
+
return undefined;
|
|
31
|
+
if (trimmed === '~')
|
|
32
|
+
return resolve(home);
|
|
33
|
+
if (trimmed.startsWith('~/'))
|
|
34
|
+
return resolve(home, trimmed.slice(2));
|
|
35
|
+
// A bare `~user` is deliberately NOT expanded: resolving another account's
|
|
36
|
+
// home needs the password database, and guessing it wrong would silently
|
|
37
|
+
// open a directory the user did not name.
|
|
38
|
+
if (isAbsolute(trimmed))
|
|
39
|
+
return resolve(trimmed);
|
|
40
|
+
return resolve(current, trimmed);
|
|
41
|
+
}
|
|
42
|
+
/** Strip one matching pair of surrounding quotes. */
|
|
43
|
+
function unquote(text) {
|
|
44
|
+
const quoted = (text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"));
|
|
45
|
+
return quoted && text.length >= 2 ? text.slice(1, -1) : text;
|
|
46
|
+
}
|
|
47
|
+
/** Whether a stored directory is still usable as one. */
|
|
48
|
+
export function isUsableDirectory(value) {
|
|
49
|
+
// A relative path would resolve against whatever the process happens to be
|
|
50
|
+
// running in, which is not the directory the user chose.
|
|
51
|
+
return isAbsolute(value);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=workspaces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaces.js","sourceRoot":"","sources":["../../src/session/workspaces.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAE/C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAa,EACb,OAAe,EACf,IAAY;IAEZ,6EAA6E;IAC7E,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;IACrC,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,SAAS,CAAA;IAEpC,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAEpE,2EAA2E;IAC3E,yEAAyE;IACzE,0CAA0C;IAC1C,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,CAAA;IAEhD,OAAO,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;AAClC,CAAC;AAED,qDAAqD;AACrD,SAAS,OAAO,CAAC,IAAY;IAC3B,MAAM,MAAM,GACV,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9F,OAAO,MAAM,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC9D,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,2EAA2E;IAC3E,yDAAyD;IACzD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAA;AAC1B,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a message in a group was meant for this bot.
|
|
3
|
+
*
|
|
4
|
+
* In a private chat the question does not arise: every message is for the bot,
|
|
5
|
+
* because there is nobody else there. A group is the opposite — people talk to
|
|
6
|
+
* each other all day, and a bot that answers every line is one nobody keeps in
|
|
7
|
+
* the room. Telegram's own convention is the one users already expect: address
|
|
8
|
+
* it by @handle, or reply to something it said.
|
|
9
|
+
*
|
|
10
|
+
* This is separate from access. Access asks whether a person MAY drive the
|
|
11
|
+
* agent and is checked first; this asks whether they were talking to it at all.
|
|
12
|
+
* Conflating them would either let a stranger's @mention through or make the
|
|
13
|
+
* allowlist decide conversational etiquette.
|
|
14
|
+
*/
|
|
15
|
+
import type { TelegramMessage } from './types.js';
|
|
16
|
+
/** Whether a chat is one where other people are talking too. */
|
|
17
|
+
export declare function isGroupChat(message: TelegramMessage): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether this message addresses the bot.
|
|
20
|
+
*
|
|
21
|
+
* @param message - the incoming message.
|
|
22
|
+
* @param botUsername - this bot's @handle, without the `@`.
|
|
23
|
+
* @param botId - this bot's user id, for recognising a reply to itself.
|
|
24
|
+
* @returns true in a private chat, and in a group only when addressed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function addressesBot(message: TelegramMessage, botUsername: string | undefined, botId: number | undefined): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Remove the bot's own @mention from the text it was addressed with.
|
|
29
|
+
*
|
|
30
|
+
* The mention is addressing, not content. Left in, every prompt in a group
|
|
31
|
+
* would open with the bot's own name, which reads to the model as part of the
|
|
32
|
+
* question.
|
|
33
|
+
*
|
|
34
|
+
* @param text - the message text.
|
|
35
|
+
* @param botUsername - this bot's @handle, without the `@`.
|
|
36
|
+
* @returns the text without a leading or trailing self-mention.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stripMention(text: string, botUsername: string | undefined): string;
|
|
39
|
+
//# sourceMappingURL=addressing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addressing.d.ts","sourceRoot":"","sources":["../../src/telegram/addressing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAG7D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,eAAe,EACxB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAgCT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAKlF"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a message in a group was meant for this bot.
|
|
3
|
+
*
|
|
4
|
+
* In a private chat the question does not arise: every message is for the bot,
|
|
5
|
+
* because there is nobody else there. A group is the opposite — people talk to
|
|
6
|
+
* each other all day, and a bot that answers every line is one nobody keeps in
|
|
7
|
+
* the room. Telegram's own convention is the one users already expect: address
|
|
8
|
+
* it by @handle, or reply to something it said.
|
|
9
|
+
*
|
|
10
|
+
* This is separate from access. Access asks whether a person MAY drive the
|
|
11
|
+
* agent and is checked first; this asks whether they were talking to it at all.
|
|
12
|
+
* Conflating them would either let a stranger's @mention through or make the
|
|
13
|
+
* allowlist decide conversational etiquette.
|
|
14
|
+
*/
|
|
15
|
+
/** Whether a chat is one where other people are talking too. */
|
|
16
|
+
export function isGroupChat(message) {
|
|
17
|
+
const type = message.chat.type;
|
|
18
|
+
return type === 'group' || type === 'supergroup';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether this message addresses the bot.
|
|
22
|
+
*
|
|
23
|
+
* @param message - the incoming message.
|
|
24
|
+
* @param botUsername - this bot's @handle, without the `@`.
|
|
25
|
+
* @param botId - this bot's user id, for recognising a reply to itself.
|
|
26
|
+
* @returns true in a private chat, and in a group only when addressed.
|
|
27
|
+
*/
|
|
28
|
+
export function addressesBot(message, botUsername, botId) {
|
|
29
|
+
if (!isGroupChat(message))
|
|
30
|
+
return true;
|
|
31
|
+
// A reply to something the bot said continues that exchange. This is the
|
|
32
|
+
// path that makes a group usable: nobody wants to re-@mention a bot to say
|
|
33
|
+
// "yes, do it".
|
|
34
|
+
const repliedTo = message.reply_to_message?.from;
|
|
35
|
+
if (repliedTo !== undefined && botId !== undefined && repliedTo.id === botId)
|
|
36
|
+
return true;
|
|
37
|
+
const text = message.text ?? message.caption;
|
|
38
|
+
if (text === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
const entities = message.entities ?? message.caption_entities ?? [];
|
|
41
|
+
for (const entity of entities) {
|
|
42
|
+
// `text_mention` names a user without a @handle — the reliable signal when
|
|
43
|
+
// a bot has none, and the only one for an account that hides its username.
|
|
44
|
+
if (entity.type === 'text_mention') {
|
|
45
|
+
if (botId !== undefined && entity.user?.id === botId)
|
|
46
|
+
return true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (entity.type !== 'mention' || botUsername === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
// Compared against the entity's own span rather than searched for in the
|
|
52
|
+
// text: `@mybot_staging` contains `@mybot`, and a substring match would
|
|
53
|
+
// hand another bot's mentions to this one.
|
|
54
|
+
const mentioned = text.slice(entity.offset, entity.offset + entity.length);
|
|
55
|
+
if (mentioned.toLowerCase() === `@${botUsername.toLowerCase()}`)
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Remove the bot's own @mention from the text it was addressed with.
|
|
62
|
+
*
|
|
63
|
+
* The mention is addressing, not content. Left in, every prompt in a group
|
|
64
|
+
* would open with the bot's own name, which reads to the model as part of the
|
|
65
|
+
* question.
|
|
66
|
+
*
|
|
67
|
+
* @param text - the message text.
|
|
68
|
+
* @param botUsername - this bot's @handle, without the `@`.
|
|
69
|
+
* @returns the text without a leading or trailing self-mention.
|
|
70
|
+
*/
|
|
71
|
+
export function stripMention(text, botUsername) {
|
|
72
|
+
if (botUsername === undefined)
|
|
73
|
+
return text;
|
|
74
|
+
const mention = new RegExp(`@${escapeRegExp(botUsername)}\\b`, 'gi');
|
|
75
|
+
return text.replace(mention, ' ').replace(/\s+/g, ' ').trim();
|
|
76
|
+
}
|
|
77
|
+
/** Quote a username for use inside a pattern. */
|
|
78
|
+
function escapeRegExp(text) {
|
|
79
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=addressing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addressing.js","sourceRoot":"","sources":["../../src/telegram/addressing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,gEAAgE;AAChE,MAAM,UAAU,WAAW,CAAC,OAAwB;IAClD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAA;IAC9B,OAAO,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,YAAY,CAAA;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAwB,EACxB,WAA+B,EAC/B,KAAyB;IAEzB,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IAEtC,yEAAyE;IACzE,2EAA2E;IAC3E,gBAAgB;IAChB,MAAM,SAAS,GAAG,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAA;IAChD,IAAI,SAAS,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,SAAS,CAAC,EAAE,KAAK,KAAK;QAAE,OAAO,IAAI,CAAA;IAEzF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAA;IAC5C,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAEpC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,gBAAgB,IAAI,EAAE,CAAA;IAEnE,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,2EAA2E;QAC3E,2EAA2E;QAC3E,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACnC,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,KAAK;gBAAE,OAAO,IAAI,CAAA;YACjE,SAAQ;QACV,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,WAAW,KAAK,SAAS;YAAE,SAAQ;QAEpE,yEAAyE;QACzE,wEAAwE;QACxE,2CAA2C;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;QAC1E,IAAI,SAAS,CAAC,WAAW,EAAE,KAAK,IAAI,WAAW,CAAC,WAAW,EAAE,EAAE;YAAE,OAAO,IAAI,CAAA;IAC9E,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,WAA+B;IACxE,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAE1C,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,IAAI,YAAY,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACpE,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;AAC/D,CAAC;AAED,iDAAiD;AACjD,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;AACpD,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gathering an album back into one message.
|
|
3
|
+
*
|
|
4
|
+
* Telegram has no "several photos in one message". Sending three screenshots
|
|
5
|
+
* produces three separate updates, tied together only by a shared
|
|
6
|
+
* `media_group_id`, with the caption on exactly one of them. Handled one at a
|
|
7
|
+
* time that becomes three turns: the first carries the question, and the other
|
|
8
|
+
* two arrive as bare images the agent has no reason for.
|
|
9
|
+
*
|
|
10
|
+
* So a message belonging to an album is held briefly rather than answered, and
|
|
11
|
+
* the group is delivered once it stops growing. The wait is the cost: it is
|
|
12
|
+
* paid only by albums, and only once per album, which is a better trade than
|
|
13
|
+
* answering the same question three times.
|
|
14
|
+
*/
|
|
15
|
+
import type { TelegramMessage } from './types.js';
|
|
16
|
+
/** Construction options. */
|
|
17
|
+
export interface AlbumBufferOptions {
|
|
18
|
+
/** Called once per album, with its parts in the order Telegram numbered them. */
|
|
19
|
+
readonly deliver: (messages: TelegramMessage[]) => void;
|
|
20
|
+
readonly windowMs?: number;
|
|
21
|
+
readonly maxParts?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class AlbumBuffer {
|
|
24
|
+
private readonly options;
|
|
25
|
+
private readonly gathering;
|
|
26
|
+
constructor(options: AlbumBufferOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Take a message if it belongs to an album.
|
|
29
|
+
*
|
|
30
|
+
* @param message - the incoming message.
|
|
31
|
+
* @returns whether it was taken; the caller should stop handling it if so,
|
|
32
|
+
* because the whole album is delivered later instead.
|
|
33
|
+
*/
|
|
34
|
+
offer(message: TelegramMessage): boolean;
|
|
35
|
+
/** Deliver every album still gathering — the plugin is unloading. */
|
|
36
|
+
flush(): void;
|
|
37
|
+
/** Drop every album still gathering, delivering none. */
|
|
38
|
+
dispose(): void;
|
|
39
|
+
/** Start gathering one album. */
|
|
40
|
+
private hold;
|
|
41
|
+
/** Arm the "it stopped growing" timer for one album. */
|
|
42
|
+
private arm;
|
|
43
|
+
}
|
|
44
|
+
/** The caption an album carries, which exactly one of its parts holds. */
|
|
45
|
+
export declare function captionOf(messages: readonly TelegramMessage[]): string | undefined;
|
|
46
|
+
//# sourceMappingURL=albums.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"albums.d.ts","sourceRoot":"","sources":["../../src/telegram/albums.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAQjD,4BAA4B;AAC5B,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAQD,qBAAa,WAAW;IAGV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+B;gBAE5B,OAAO,EAAE,kBAAkB;IAExD;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO;IA0BxC,qEAAqE;IACrE,KAAK,IAAI,IAAI;IAQb,yDAAyD;IACzD,OAAO,IAAI,IAAI;IAKf,iCAAiC;IACjC,OAAO,CAAC,IAAI;IAIZ,wDAAwD;IACxD,OAAO,CAAC,GAAG;CAaZ;AAYD,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,GAAG,MAAM,GAAG,SAAS,CAMlF"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gathering an album back into one message.
|
|
3
|
+
*
|
|
4
|
+
* Telegram has no "several photos in one message". Sending three screenshots
|
|
5
|
+
* produces three separate updates, tied together only by a shared
|
|
6
|
+
* `media_group_id`, with the caption on exactly one of them. Handled one at a
|
|
7
|
+
* time that becomes three turns: the first carries the question, and the other
|
|
8
|
+
* two arrive as bare images the agent has no reason for.
|
|
9
|
+
*
|
|
10
|
+
* So a message belonging to an album is held briefly rather than answered, and
|
|
11
|
+
* the group is delivered once it stops growing. The wait is the cost: it is
|
|
12
|
+
* paid only by albums, and only once per album, which is a better trade than
|
|
13
|
+
* answering the same question three times.
|
|
14
|
+
*/
|
|
15
|
+
/** How long to wait after the last part before deciding an album is complete. */
|
|
16
|
+
const WINDOW_MS = 1200;
|
|
17
|
+
/** Telegram's own album limit; anything beyond it is a different album. */
|
|
18
|
+
const MAX_PARTS = 10;
|
|
19
|
+
export class AlbumBuffer {
|
|
20
|
+
options;
|
|
21
|
+
gathering = new Map();
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Take a message if it belongs to an album.
|
|
27
|
+
*
|
|
28
|
+
* @param message - the incoming message.
|
|
29
|
+
* @returns whether it was taken; the caller should stop handling it if so,
|
|
30
|
+
* because the whole album is delivered later instead.
|
|
31
|
+
*/
|
|
32
|
+
offer(message) {
|
|
33
|
+
const id = message.media_group_id;
|
|
34
|
+
if (id === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
const existing = this.gathering.get(id);
|
|
37
|
+
if (existing) {
|
|
38
|
+
clearTimeout(existing.timer);
|
|
39
|
+
// Beyond Telegram's own limit this is no longer one album, so the parts
|
|
40
|
+
// held so far go out rather than growing without bound.
|
|
41
|
+
if (existing.messages.length >= (this.options.maxParts ?? MAX_PARTS)) {
|
|
42
|
+
this.gathering.delete(id);
|
|
43
|
+
this.options.deliver(sorted(existing.messages));
|
|
44
|
+
this.hold(id, [message]);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
existing.messages.push(message);
|
|
48
|
+
existing.timer = this.arm(id);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
this.hold(id, [message]);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/** Deliver every album still gathering — the plugin is unloading. */
|
|
55
|
+
flush() {
|
|
56
|
+
for (const [id, entry] of [...this.gathering]) {
|
|
57
|
+
clearTimeout(entry.timer);
|
|
58
|
+
this.gathering.delete(id);
|
|
59
|
+
this.options.deliver(sorted(entry.messages));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Drop every album still gathering, delivering none. */
|
|
63
|
+
dispose() {
|
|
64
|
+
for (const entry of this.gathering.values())
|
|
65
|
+
clearTimeout(entry.timer);
|
|
66
|
+
this.gathering.clear();
|
|
67
|
+
}
|
|
68
|
+
/** Start gathering one album. */
|
|
69
|
+
hold(id, messages) {
|
|
70
|
+
this.gathering.set(id, { messages, timer: this.arm(id) });
|
|
71
|
+
}
|
|
72
|
+
/** Arm the "it stopped growing" timer for one album. */
|
|
73
|
+
arm(id) {
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
const entry = this.gathering.get(id);
|
|
76
|
+
if (!entry)
|
|
77
|
+
return;
|
|
78
|
+
this.gathering.delete(id);
|
|
79
|
+
this.options.deliver(sorted(entry.messages));
|
|
80
|
+
}, this.options.windowMs ?? WINDOW_MS);
|
|
81
|
+
// A pending album is never a reason to keep the process alive.
|
|
82
|
+
timer.unref?.();
|
|
83
|
+
return timer;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The album in the order it was sent.
|
|
88
|
+
*
|
|
89
|
+
* Telegram numbers the parts even when it delivers them out of order, and the
|
|
90
|
+
* order is what decides which image the caption belongs to.
|
|
91
|
+
*/
|
|
92
|
+
function sorted(messages) {
|
|
93
|
+
return [...messages].sort((a, b) => a.message_id - b.message_id);
|
|
94
|
+
}
|
|
95
|
+
/** The caption an album carries, which exactly one of its parts holds. */
|
|
96
|
+
export function captionOf(messages) {
|
|
97
|
+
for (const message of messages) {
|
|
98
|
+
const said = (message.caption ?? message.text)?.trim();
|
|
99
|
+
if (said !== undefined && said !== '')
|
|
100
|
+
return said;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=albums.js.map
|