copilot-hub 0.1.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/README.md +215 -0
- package/apps/agent-engine/.env.example +41 -0
- package/apps/agent-engine/LICENSE +21 -0
- package/apps/agent-engine/README.md +57 -0
- package/apps/agent-engine/bot-registry.example.json +28 -0
- package/apps/agent-engine/capabilities/example/index.js +3 -0
- package/apps/agent-engine/capabilities/example/manifest.json +14 -0
- package/apps/agent-engine/dist/agent-worker.js +241 -0
- package/apps/agent-engine/dist/config.js +225 -0
- package/apps/agent-engine/dist/index.js +352 -0
- package/apps/agent-engine/dist/test/project-fingerprint.test.js +40 -0
- package/apps/agent-engine/dist/test/thread-id.test.js +12 -0
- package/apps/agent-engine/package.json +28 -0
- package/apps/control-plane/.env.example +25 -0
- package/apps/control-plane/README.md +35 -0
- package/apps/control-plane/bot-registry.example.json +40 -0
- package/apps/control-plane/capabilities/example/index.js +3 -0
- package/apps/control-plane/capabilities/example/manifest.json +14 -0
- package/apps/control-plane/dist/agent-worker.js +243 -0
- package/apps/control-plane/dist/channels/channel-factory.js +21 -0
- package/apps/control-plane/dist/channels/hub-ops-commands.js +752 -0
- package/apps/control-plane/dist/channels/telegram-channel.js +743 -0
- package/apps/control-plane/dist/channels/whatsapp-channel.js +35 -0
- package/apps/control-plane/dist/config.js +230 -0
- package/apps/control-plane/dist/copilot-hub.js +138 -0
- package/apps/control-plane/dist/index.js +349 -0
- package/apps/control-plane/dist/kernel/admin-contract.js +51 -0
- package/apps/control-plane/dist/test/project-fingerprint.test.js +40 -0
- package/apps/control-plane/dist/test/thread-id.test.js +12 -0
- package/apps/control-plane/package.json +27 -0
- package/package.json +89 -0
- package/packages/contracts/README.md +10 -0
- package/packages/contracts/dist/control-plane.d.ts +24 -0
- package/packages/contracts/dist/control-plane.js +37 -0
- package/packages/contracts/dist/control-plane.js.map +1 -0
- package/packages/contracts/dist/index.d.ts +1 -0
- package/packages/contracts/dist/index.js +2 -0
- package/packages/contracts/dist/index.js.map +1 -0
- package/packages/contracts/package.json +27 -0
- package/packages/core/README.md +33 -0
- package/packages/core/dist/agent-supervisor.d.ts +39 -0
- package/packages/core/dist/agent-supervisor.js +552 -0
- package/packages/core/dist/agent-supervisor.js.map +1 -0
- package/packages/core/dist/bot-manager.d.ts +66 -0
- package/packages/core/dist/bot-manager.js +333 -0
- package/packages/core/dist/bot-manager.js.map +1 -0
- package/packages/core/dist/bot-registry.d.ts +60 -0
- package/packages/core/dist/bot-registry.js +381 -0
- package/packages/core/dist/bot-registry.js.map +1 -0
- package/packages/core/dist/bot-runtime.d.ts +135 -0
- package/packages/core/dist/bot-runtime.js +349 -0
- package/packages/core/dist/bot-runtime.js.map +1 -0
- package/packages/core/dist/bridge-service.d.ts +39 -0
- package/packages/core/dist/bridge-service.js +272 -0
- package/packages/core/dist/bridge-service.js.map +1 -0
- package/packages/core/dist/capability-manager.d.ts +18 -0
- package/packages/core/dist/capability-manager.js +335 -0
- package/packages/core/dist/capability-manager.js.map +1 -0
- package/packages/core/dist/capability-scaffold.d.ts +26 -0
- package/packages/core/dist/capability-scaffold.js +118 -0
- package/packages/core/dist/capability-scaffold.js.map +1 -0
- package/packages/core/dist/channel-factory.d.ts +6 -0
- package/packages/core/dist/channel-factory.js +22 -0
- package/packages/core/dist/channel-factory.js.map +1 -0
- package/packages/core/dist/codex-app-client.d.ts +56 -0
- package/packages/core/dist/codex-app-client.js +762 -0
- package/packages/core/dist/codex-app-client.js.map +1 -0
- package/packages/core/dist/codex-provider.d.ts +31 -0
- package/packages/core/dist/codex-provider.js +64 -0
- package/packages/core/dist/codex-provider.js.map +1 -0
- package/packages/core/dist/control-permission.d.ts +19 -0
- package/packages/core/dist/control-permission.js +106 -0
- package/packages/core/dist/control-permission.js.map +1 -0
- package/packages/core/dist/control-plane-actions.d.ts +1 -0
- package/packages/core/dist/control-plane-actions.js +2 -0
- package/packages/core/dist/control-plane-actions.js.map +1 -0
- package/packages/core/dist/example-capability.d.ts +17 -0
- package/packages/core/dist/example-capability.js +22 -0
- package/packages/core/dist/example-capability.js.map +1 -0
- package/packages/core/dist/extension-contract.d.ts +22 -0
- package/packages/core/dist/extension-contract.js +28 -0
- package/packages/core/dist/extension-contract.js.map +1 -0
- package/packages/core/dist/index.d.ts +26 -0
- package/packages/core/dist/index.js +27 -0
- package/packages/core/dist/index.js.map +1 -0
- package/packages/core/dist/instance-lock.d.ts +9 -0
- package/packages/core/dist/instance-lock.js +74 -0
- package/packages/core/dist/instance-lock.js.map +1 -0
- package/packages/core/dist/kernel-control-plane.d.ts +16 -0
- package/packages/core/dist/kernel-control-plane.js +500 -0
- package/packages/core/dist/kernel-control-plane.js.map +1 -0
- package/packages/core/dist/kernel-version.d.ts +1 -0
- package/packages/core/dist/kernel-version.js +2 -0
- package/packages/core/dist/kernel-version.js.map +1 -0
- package/packages/core/dist/project-fingerprint.d.ts +11 -0
- package/packages/core/dist/project-fingerprint.js +33 -0
- package/packages/core/dist/project-fingerprint.js.map +1 -0
- package/packages/core/dist/provider-factory.d.ts +7 -0
- package/packages/core/dist/provider-factory.js +21 -0
- package/packages/core/dist/provider-factory.js.map +1 -0
- package/packages/core/dist/secret-store.d.ts +18 -0
- package/packages/core/dist/secret-store.js +110 -0
- package/packages/core/dist/secret-store.js.map +1 -0
- package/packages/core/dist/state-store.d.ts +50 -0
- package/packages/core/dist/state-store.js +324 -0
- package/packages/core/dist/state-store.js.map +1 -0
- package/packages/core/dist/telegram-channel.d.ts +27 -0
- package/packages/core/dist/telegram-channel.js +951 -0
- package/packages/core/dist/telegram-channel.js.map +1 -0
- package/packages/core/dist/thread-id.d.ts +1 -0
- package/packages/core/dist/thread-id.js +12 -0
- package/packages/core/dist/thread-id.js.map +1 -0
- package/packages/core/dist/whatsapp-channel.d.ts +26 -0
- package/packages/core/dist/whatsapp-channel.js +36 -0
- package/packages/core/dist/whatsapp-channel.js.map +1 -0
- package/packages/core/dist/workspace-paths.d.ts +5 -0
- package/packages/core/dist/workspace-paths.js +77 -0
- package/packages/core/dist/workspace-paths.js.map +1 -0
- package/packages/core/dist/workspace-policy.d.ts +30 -0
- package/packages/core/dist/workspace-policy.js +104 -0
- package/packages/core/dist/workspace-policy.js.map +1 -0
- package/packages/core/package.json +126 -0
- package/scripts/cli.mjs +537 -0
- package/scripts/configure.mjs +254 -0
- package/scripts/ensure-shared-build.mjs +96 -0
- package/scripts/run-node-tests.mjs +52 -0
- package/scripts/supervisor.mjs +332 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Bot } from "grammy";
|
|
5
|
+
export class TelegramChannel {
|
|
6
|
+
constructor({ channelConfig, runtime }) {
|
|
7
|
+
this.kind = "telegram";
|
|
8
|
+
this.id = String(channelConfig.id ?? "telegram");
|
|
9
|
+
this.config = channelConfig;
|
|
10
|
+
this.runtime = runtime;
|
|
11
|
+
this.allowedChatIds = normalizeAllowedChatIds(channelConfig.allowedChatIds);
|
|
12
|
+
this.bot = null;
|
|
13
|
+
this.running = false;
|
|
14
|
+
this.error = null;
|
|
15
|
+
this.activeTurnsByChat = new Map();
|
|
16
|
+
this.turnControlByChat = new Map();
|
|
17
|
+
this.nextTurnToken = 1;
|
|
18
|
+
}
|
|
19
|
+
async start() {
|
|
20
|
+
if (this.running) {
|
|
21
|
+
return this.getStatus();
|
|
22
|
+
}
|
|
23
|
+
const token = String(this.config.token ?? "").trim();
|
|
24
|
+
if (!token) {
|
|
25
|
+
throw new Error(`Telegram token is missing for channel '${this.id}'.`);
|
|
26
|
+
}
|
|
27
|
+
const bot = new Bot(token);
|
|
28
|
+
this.#attachHandlers(bot);
|
|
29
|
+
this.error = null;
|
|
30
|
+
this.running = true;
|
|
31
|
+
this.bot = bot;
|
|
32
|
+
void bot
|
|
33
|
+
.start({
|
|
34
|
+
onStart: () => {
|
|
35
|
+
console.log(`[${this.runtime.runtimeId}:${this.id}] Telegram polling started.`);
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
this.error = sanitizeError(error);
|
|
40
|
+
this.running = false;
|
|
41
|
+
this.bot = null;
|
|
42
|
+
console.error(`[${this.runtime.runtimeId}:${this.id}] Telegram polling error: ${this.error}`);
|
|
43
|
+
});
|
|
44
|
+
return this.getStatus();
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
if (this.bot) {
|
|
48
|
+
try {
|
|
49
|
+
this.bot.stop();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Ignore stop errors.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.bot = null;
|
|
56
|
+
this.running = false;
|
|
57
|
+
this.activeTurnsByChat.clear();
|
|
58
|
+
this.turnControlByChat.clear();
|
|
59
|
+
return this.getStatus();
|
|
60
|
+
}
|
|
61
|
+
async shutdown() {
|
|
62
|
+
await this.stop();
|
|
63
|
+
}
|
|
64
|
+
getStatus() {
|
|
65
|
+
return {
|
|
66
|
+
kind: this.kind,
|
|
67
|
+
id: this.id,
|
|
68
|
+
running: this.running,
|
|
69
|
+
error: this.error,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async notifyApproval(approval) {
|
|
73
|
+
const chatId = String(approval?.metadata?.chatId ?? "").trim();
|
|
74
|
+
if (!chatId || !this.bot) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const lines = [
|
|
78
|
+
`Approval required [${approval.id}]`,
|
|
79
|
+
`type: ${approval.kind}`,
|
|
80
|
+
`thread: ${approval.threadId}`,
|
|
81
|
+
];
|
|
82
|
+
if (approval.command) {
|
|
83
|
+
lines.push(`command: ${approval.command}`);
|
|
84
|
+
}
|
|
85
|
+
if (approval.cwd) {
|
|
86
|
+
lines.push(`cwd: ${approval.cwd}`);
|
|
87
|
+
}
|
|
88
|
+
if (approval.reason) {
|
|
89
|
+
lines.push(`reason: ${approval.reason}`);
|
|
90
|
+
}
|
|
91
|
+
lines.push(`Use: /approve ${approval.id} or /deny ${approval.id}`);
|
|
92
|
+
await sendChunkedMessage(this.bot.api, chatId, lines.join("\n"));
|
|
93
|
+
}
|
|
94
|
+
#attachHandlers(bot) {
|
|
95
|
+
bot.command("start", async (context) => {
|
|
96
|
+
const chatId = String(context.chat.id);
|
|
97
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
98
|
+
await context.reply("Chat not allowed for this bot.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
102
|
+
channelKind: this.kind,
|
|
103
|
+
channelId: this.id,
|
|
104
|
+
externalUserId: chatId,
|
|
105
|
+
});
|
|
106
|
+
await context.reply([
|
|
107
|
+
`Agent '${this.runtime.runtimeName}' ready.`,
|
|
108
|
+
"/thread - show active thread",
|
|
109
|
+
"/new - reset current thread",
|
|
110
|
+
"/status - show session status",
|
|
111
|
+
"/stop - stop current generation",
|
|
112
|
+
"/steer <instruction> - interrupt current generation and apply steer immediately",
|
|
113
|
+
"Send a message during generation to interrupt and replace immediately",
|
|
114
|
+
"During generation button: Interrompre",
|
|
115
|
+
"/approvals - list pending approvals",
|
|
116
|
+
"/approve <id> - approve a pending action",
|
|
117
|
+
"/approvealways <id> - approve and remember for session",
|
|
118
|
+
"/approveall - approve all pending actions",
|
|
119
|
+
"/approveallalways - approve all and remember for session",
|
|
120
|
+
"/deny <id> - deny a pending action",
|
|
121
|
+
"/denyall - deny all pending actions",
|
|
122
|
+
"/whoami - show Telegram chat id",
|
|
123
|
+
`Web: ${this.runtime.buildWebBotUrl()}`,
|
|
124
|
+
`threadId: ${threadId}`,
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join("\n"));
|
|
128
|
+
});
|
|
129
|
+
bot.command("thread", async (context) => {
|
|
130
|
+
const chatId = String(context.chat.id);
|
|
131
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
132
|
+
await context.reply("Chat not allowed for this bot.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
136
|
+
channelKind: this.kind,
|
|
137
|
+
channelId: this.id,
|
|
138
|
+
externalUserId: chatId,
|
|
139
|
+
});
|
|
140
|
+
await context.reply(`threadId: ${threadId}`);
|
|
141
|
+
});
|
|
142
|
+
bot.command("new", async (context) => {
|
|
143
|
+
const chatId = String(context.chat.id);
|
|
144
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
145
|
+
await context.reply("Chat not allowed for this bot.");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
149
|
+
channelKind: this.kind,
|
|
150
|
+
channelId: this.id,
|
|
151
|
+
externalUserId: chatId,
|
|
152
|
+
});
|
|
153
|
+
await this.runtime.resetThread(threadId);
|
|
154
|
+
await context.reply(`Thread '${threadId}' reset.`);
|
|
155
|
+
});
|
|
156
|
+
bot.command("status", async (context) => {
|
|
157
|
+
const chatId = String(context.chat.id);
|
|
158
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
159
|
+
await context.reply("Chat not allowed for this bot.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
163
|
+
channelKind: this.kind,
|
|
164
|
+
channelId: this.id,
|
|
165
|
+
externalUserId: chatId,
|
|
166
|
+
});
|
|
167
|
+
const { thread } = await this.runtime.getThread(threadId);
|
|
168
|
+
await context.reply([
|
|
169
|
+
`agent: ${this.runtime.runtimeName}`,
|
|
170
|
+
`threadId: ${threadId}`,
|
|
171
|
+
`turnCount: ${thread.turnCount ?? 0}`,
|
|
172
|
+
`sessionId: ${thread.sessionId ?? "<none>"}`,
|
|
173
|
+
`updatedAt: ${thread.updatedAt ?? "<unknown>"}`,
|
|
174
|
+
`web: ${this.runtime.buildWebBotUrl()}`,
|
|
175
|
+
].join("\n"));
|
|
176
|
+
});
|
|
177
|
+
bot.command("whoami", async (context) => {
|
|
178
|
+
const chatId = String(context.chat.id);
|
|
179
|
+
await context.reply(`chat_id: ${chatId}`);
|
|
180
|
+
});
|
|
181
|
+
bot.command("stop", async (context) => {
|
|
182
|
+
await this.#handleStopTurn(context);
|
|
183
|
+
});
|
|
184
|
+
bot.command("steer", async (context) => {
|
|
185
|
+
await this.#handleSteer(context);
|
|
186
|
+
});
|
|
187
|
+
bot.command("approvals", async (context) => {
|
|
188
|
+
const chatId = String(context.chat.id);
|
|
189
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
190
|
+
await context.reply("Chat not allowed for this bot.");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
194
|
+
channelKind: this.kind,
|
|
195
|
+
channelId: this.id,
|
|
196
|
+
externalUserId: chatId,
|
|
197
|
+
});
|
|
198
|
+
const approvals = await this.runtime.listPendingApprovals(threadId);
|
|
199
|
+
if (approvals.length === 0) {
|
|
200
|
+
await context.reply("No pending approvals.");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
await context.reply(formatApprovalList(approvals));
|
|
204
|
+
});
|
|
205
|
+
bot.command("approve", async (context) => {
|
|
206
|
+
await this.#handleSingleApproval(context, "accept");
|
|
207
|
+
});
|
|
208
|
+
bot.command("approvealways", async (context) => {
|
|
209
|
+
await this.#handleSingleApproval(context, "acceptForSession");
|
|
210
|
+
});
|
|
211
|
+
bot.command("approveall", async (context) => {
|
|
212
|
+
await this.#handleBulkApproval(context, "accept");
|
|
213
|
+
});
|
|
214
|
+
bot.command("approveallalways", async (context) => {
|
|
215
|
+
await this.#handleBulkApproval(context, "acceptForSession");
|
|
216
|
+
});
|
|
217
|
+
bot.command("deny", async (context) => {
|
|
218
|
+
await this.#handleSingleApproval(context, "decline");
|
|
219
|
+
});
|
|
220
|
+
bot.command("denyall", async (context) => {
|
|
221
|
+
await this.#handleBulkApproval(context, "decline");
|
|
222
|
+
});
|
|
223
|
+
bot.on("callback_query:data", async (context) => {
|
|
224
|
+
const payload = parseTurnControlCallbackData(context.callbackQuery.data);
|
|
225
|
+
if (!payload) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const chatId = String(context.callbackQuery.message?.chat?.id ?? "");
|
|
229
|
+
if (!chatId || !this.#isAllowedChat(chatId)) {
|
|
230
|
+
await context.answerCallbackQuery({
|
|
231
|
+
text: "Chat not allowed for this bot.",
|
|
232
|
+
show_alert: true,
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const active = this.#getActiveTurn(chatId);
|
|
237
|
+
if (!active || active.token !== payload.token) {
|
|
238
|
+
await context.answerCallbackQuery({
|
|
239
|
+
text: "No active generation for this action.",
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (payload.action === "stop") {
|
|
244
|
+
const result = await this.runtime.interruptThread(active.threadId);
|
|
245
|
+
if (result?.interrupted === true) {
|
|
246
|
+
await this.#closeTurnControls(chatId, payload.token, "Generation interruption requested.");
|
|
247
|
+
}
|
|
248
|
+
await context.answerCallbackQuery({
|
|
249
|
+
text: result?.interrupted === true
|
|
250
|
+
? "Interruption requested."
|
|
251
|
+
: "No active generation to stop.",
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
bot.on("message:text", async (context) => {
|
|
257
|
+
const text = context.message.text;
|
|
258
|
+
const chatId = String(context.chat.id);
|
|
259
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
260
|
+
await context.reply("Chat not allowed for this bot.");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (text.startsWith("/")) {
|
|
264
|
+
await context.reply("Unknown command. Use /start.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await this.#routeIncomingPrompt({
|
|
268
|
+
chatId,
|
|
269
|
+
prompt: text,
|
|
270
|
+
mode: "auto_message",
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
bot.on("message:photo", async (context) => {
|
|
274
|
+
const chatId = String(context.chat.id);
|
|
275
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
276
|
+
await context.reply("Chat not allowed for this bot.");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const photoInput = await this.#buildPhotoInputItem({ message: context.message });
|
|
281
|
+
const caption = String(context.message?.caption ?? "").trim();
|
|
282
|
+
const prompt = caption || "Analyze the image sent by the user and answer their request.";
|
|
283
|
+
await this.#routeIncomingPrompt({
|
|
284
|
+
chatId,
|
|
285
|
+
prompt,
|
|
286
|
+
mode: "auto_message",
|
|
287
|
+
inputItems: [photoInput],
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
await context.reply(`Unable to process photo:\n${sanitizeError(error)}`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
bot.on("message:voice", async (context) => {
|
|
295
|
+
const chatId = String(context.chat.id);
|
|
296
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
297
|
+
await context.reply("Chat not allowed for this bot.");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const prompt = await this.#buildVoicePrompt({
|
|
302
|
+
chatId,
|
|
303
|
+
message: context.message,
|
|
304
|
+
});
|
|
305
|
+
await this.#routeIncomingPrompt({
|
|
306
|
+
chatId,
|
|
307
|
+
prompt,
|
|
308
|
+
mode: "auto_message",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
await context.reply(`Unable to process voice note:\n${sanitizeError(error)}`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
bot.catch((error) => {
|
|
316
|
+
const details = error.error instanceof Error
|
|
317
|
+
? (error.error.stack ?? error.error.message)
|
|
318
|
+
: String(error.error);
|
|
319
|
+
this.error = details;
|
|
320
|
+
console.error(`[${this.runtime.runtimeId}:${this.id}] Telegram error in update ${error.ctx.update.update_id}: ${details}`);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async #routeIncomingPrompt({ chatId, prompt, mode = "auto_message", inputItems = null }) {
|
|
324
|
+
const normalizedPrompt = String(prompt ?? "").trim();
|
|
325
|
+
if (!normalizedPrompt) {
|
|
326
|
+
if (this.bot) {
|
|
327
|
+
await this.bot.api.sendMessage(chatId, "Message is empty.");
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
332
|
+
channelKind: this.kind,
|
|
333
|
+
channelId: this.id,
|
|
334
|
+
externalUserId: chatId,
|
|
335
|
+
});
|
|
336
|
+
const activeTurn = this.#getActiveTurn(chatId);
|
|
337
|
+
if (activeTurn && activeTurn.threadId === threadId) {
|
|
338
|
+
await this.#applySteerInstruction({
|
|
339
|
+
chatId,
|
|
340
|
+
threadId,
|
|
341
|
+
prompt: normalizedPrompt,
|
|
342
|
+
mode,
|
|
343
|
+
inputItems,
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
void this.#processTurn({
|
|
348
|
+
chatId,
|
|
349
|
+
threadId,
|
|
350
|
+
prompt: normalizedPrompt,
|
|
351
|
+
inputItems,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async #buildPhotoInputItem({ message }) {
|
|
355
|
+
const photos = Array.isArray(message?.photo) ? message.photo : [];
|
|
356
|
+
if (photos.length === 0) {
|
|
357
|
+
throw new Error("Telegram update does not contain photo payload.");
|
|
358
|
+
}
|
|
359
|
+
let chosen = photos[0];
|
|
360
|
+
for (const candidate of photos) {
|
|
361
|
+
if (Number(candidate?.file_size ?? 0) >= Number(chosen?.file_size ?? 0)) {
|
|
362
|
+
chosen = candidate;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const fileId = String(chosen?.file_id ?? "").trim();
|
|
366
|
+
if (!fileId) {
|
|
367
|
+
throw new Error("Telegram photo file id is missing.");
|
|
368
|
+
}
|
|
369
|
+
const file = await this.#resolveTelegramFile(fileId);
|
|
370
|
+
const telegramPath = String(file?.file_path ?? "").trim();
|
|
371
|
+
if (!telegramPath) {
|
|
372
|
+
throw new Error("Telegram photo file path is missing.");
|
|
373
|
+
}
|
|
374
|
+
const binary = await this.#downloadTelegramBinary(telegramPath);
|
|
375
|
+
const maxBytes = 8 * 1024 * 1024;
|
|
376
|
+
if (binary.length > maxBytes) {
|
|
377
|
+
throw new Error(`Photo is too large for direct model upload (${binary.length} bytes). Please send a smaller image.`);
|
|
378
|
+
}
|
|
379
|
+
const mime = mimeTypeFromExtension(path.extname(telegramPath), "image/jpeg");
|
|
380
|
+
const dataUrl = `data:${mime};base64,${binary.toString("base64")}`;
|
|
381
|
+
return {
|
|
382
|
+
type: "image",
|
|
383
|
+
url: dataUrl,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async #buildVoicePrompt({ chatId, message }) {
|
|
387
|
+
const voice = message?.voice;
|
|
388
|
+
if (!voice || !voice.file_id) {
|
|
389
|
+
throw new Error("Telegram update does not contain voice payload.");
|
|
390
|
+
}
|
|
391
|
+
const stored = await this.#saveTelegramMediaFile({
|
|
392
|
+
chatId,
|
|
393
|
+
fileId: String(voice.file_id ?? "").trim(),
|
|
394
|
+
mediaKind: "voice",
|
|
395
|
+
preferredExtension: extensionFromMimeType(voice.mime_type),
|
|
396
|
+
});
|
|
397
|
+
const caption = String(message?.caption ?? "").trim();
|
|
398
|
+
const lines = [
|
|
399
|
+
"Telegram user sent a voice note.",
|
|
400
|
+
`file_path: ${stored.relativePath}`,
|
|
401
|
+
`absolute_path: ${stored.absolutePath}`,
|
|
402
|
+
`duration_seconds: ${Number(voice.duration ?? 0)}`,
|
|
403
|
+
`mime_type: ${String(voice.mime_type ?? "audio/ogg")}`,
|
|
404
|
+
];
|
|
405
|
+
if (caption) {
|
|
406
|
+
lines.push(`caption: ${caption}`);
|
|
407
|
+
}
|
|
408
|
+
lines.push("Task: transcribe the audio first, then answer the user request. If transcription is not possible with available tools, explain what is missing.");
|
|
409
|
+
return lines.join("\n");
|
|
410
|
+
}
|
|
411
|
+
async #resolveTelegramFile(fileId) {
|
|
412
|
+
if (!this.bot) {
|
|
413
|
+
throw new Error("Telegram bot is not running.");
|
|
414
|
+
}
|
|
415
|
+
const normalizedFileId = String(fileId ?? "").trim();
|
|
416
|
+
if (!normalizedFileId) {
|
|
417
|
+
throw new Error("Telegram file id is missing.");
|
|
418
|
+
}
|
|
419
|
+
return this.bot.api.getFile(normalizedFileId);
|
|
420
|
+
}
|
|
421
|
+
async #downloadTelegramBinary(telegramPath) {
|
|
422
|
+
const token = String(this.config.token ?? "").trim();
|
|
423
|
+
if (!token) {
|
|
424
|
+
throw new Error("Telegram token is missing.");
|
|
425
|
+
}
|
|
426
|
+
const normalizedPath = String(telegramPath ?? "").trim();
|
|
427
|
+
if (!normalizedPath) {
|
|
428
|
+
throw new Error("Telegram file path is missing.");
|
|
429
|
+
}
|
|
430
|
+
const downloadUrl = `https://api.telegram.org/file/bot${token}/${normalizedPath}`;
|
|
431
|
+
const response = await fetch(downloadUrl);
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
throw new Error(`Telegram download failed (HTTP ${response.status}).`);
|
|
434
|
+
}
|
|
435
|
+
return Buffer.from(await response.arrayBuffer());
|
|
436
|
+
}
|
|
437
|
+
async #saveTelegramMediaFile({ chatId, fileId, mediaKind, preferredExtension }) {
|
|
438
|
+
if (!this.bot) {
|
|
439
|
+
throw new Error("Telegram bot is not running.");
|
|
440
|
+
}
|
|
441
|
+
const normalizedFileId = String(fileId ?? "").trim();
|
|
442
|
+
if (!normalizedFileId) {
|
|
443
|
+
throw new Error("Telegram file id is missing.");
|
|
444
|
+
}
|
|
445
|
+
const file = await this.#resolveTelegramFile(normalizedFileId);
|
|
446
|
+
const telegramPath = String(file?.file_path ?? "").trim();
|
|
447
|
+
if (!telegramPath) {
|
|
448
|
+
throw new Error("Telegram file path is missing.");
|
|
449
|
+
}
|
|
450
|
+
const workspaceRoot = this.#resolveWorkspaceRoot();
|
|
451
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
452
|
+
const targetDir = path.join(workspaceRoot, ".runtime_media", "telegram", sanitizePathSegment(this.id), sanitizePathSegment(chatId), day);
|
|
453
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
454
|
+
const extension = normalizeMediaExtension(path.extname(telegramPath), preferredExtension);
|
|
455
|
+
const fileName = `${sanitizePathSegment(mediaKind)}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${extension}`;
|
|
456
|
+
const absolutePath = path.join(targetDir, fileName);
|
|
457
|
+
const payload = await this.#downloadTelegramBinary(telegramPath);
|
|
458
|
+
await fs.writeFile(absolutePath, payload);
|
|
459
|
+
const relativePath = normalizePathForPrompt(path.relative(workspaceRoot, absolutePath) || fileName);
|
|
460
|
+
return {
|
|
461
|
+
absolutePath: normalizePathForPrompt(absolutePath),
|
|
462
|
+
relativePath,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
#resolveWorkspaceRoot() {
|
|
466
|
+
if (this.runtime && typeof this.runtime.getWorkspaceRoot === "function") {
|
|
467
|
+
return path.resolve(String(this.runtime.getWorkspaceRoot() ?? process.cwd()));
|
|
468
|
+
}
|
|
469
|
+
return path.resolve(process.cwd());
|
|
470
|
+
}
|
|
471
|
+
async #processTurn({ chatId, threadId, prompt, inputItems = null }) {
|
|
472
|
+
if (!this.bot) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const turnToken = this.#markActiveTurn(chatId, threadId);
|
|
476
|
+
await this.#openTurnControls(chatId, threadId, turnToken);
|
|
477
|
+
let controlStatus = "Generation completed.";
|
|
478
|
+
try {
|
|
479
|
+
await this.bot.api.sendChatAction(chatId, "typing");
|
|
480
|
+
const result = await this.runtime.sendTurn({
|
|
481
|
+
threadId,
|
|
482
|
+
prompt,
|
|
483
|
+
inputItems: Array.isArray(inputItems) ? inputItems : undefined,
|
|
484
|
+
source: "telegram",
|
|
485
|
+
metadata: {
|
|
486
|
+
chatId,
|
|
487
|
+
channelId: this.id,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
await sendChunkedMessage(this.bot.api, chatId, result.assistantText || "Assistant returned no text output.");
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
const safe = sanitizeError(error);
|
|
494
|
+
if (isTurnInterruptedError(safe)) {
|
|
495
|
+
controlStatus = "Generation interrupted.";
|
|
496
|
+
await this.bot.api.sendMessage(chatId, "Generation stopped.");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
controlStatus = "Generation failed.";
|
|
500
|
+
await this.bot.api.sendMessage(chatId, `Execution error:\n${safe}`);
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
this.#clearActiveTurn(chatId, turnToken);
|
|
504
|
+
await this.#closeTurnControls(chatId, turnToken, controlStatus);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async #handleStopTurn(context) {
|
|
508
|
+
const chatId = String(context.chat.id);
|
|
509
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
510
|
+
await context.reply("Chat not allowed for this bot.");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
514
|
+
channelKind: this.kind,
|
|
515
|
+
channelId: this.id,
|
|
516
|
+
externalUserId: chatId,
|
|
517
|
+
});
|
|
518
|
+
const result = await this.runtime.interruptThread(threadId);
|
|
519
|
+
await context.reply(formatInterruptResult(result));
|
|
520
|
+
}
|
|
521
|
+
async #handleSteer(context) {
|
|
522
|
+
const chatId = String(context.chat.id);
|
|
523
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
524
|
+
await context.reply("Chat not allowed for this bot.");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const instruction = extractCommandTail(context.message.text);
|
|
528
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
529
|
+
channelKind: this.kind,
|
|
530
|
+
channelId: this.id,
|
|
531
|
+
externalUserId: chatId,
|
|
532
|
+
});
|
|
533
|
+
if (!instruction) {
|
|
534
|
+
await context.reply("Usage: /steer <instruction>");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const prompt = buildSteerPrompt(instruction);
|
|
538
|
+
await this.#applySteerInstruction({
|
|
539
|
+
chatId,
|
|
540
|
+
threadId,
|
|
541
|
+
prompt,
|
|
542
|
+
mode: "command_steer",
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
#markActiveTurn(chatId, threadId) {
|
|
546
|
+
const token = `turn_${Date.now()}_${this.nextTurnToken++}`;
|
|
547
|
+
this.activeTurnsByChat.set(String(chatId), {
|
|
548
|
+
threadId: String(threadId),
|
|
549
|
+
token,
|
|
550
|
+
});
|
|
551
|
+
return token;
|
|
552
|
+
}
|
|
553
|
+
#clearActiveTurn(chatId, token) {
|
|
554
|
+
const key = String(chatId);
|
|
555
|
+
const current = this.activeTurnsByChat.get(key);
|
|
556
|
+
if (!current) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (String(current.token) !== String(token)) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this.activeTurnsByChat.delete(key);
|
|
563
|
+
}
|
|
564
|
+
#getActiveTurn(chatId) {
|
|
565
|
+
return this.activeTurnsByChat.get(String(chatId)) ?? null;
|
|
566
|
+
}
|
|
567
|
+
async #applySteerInstruction({ chatId, threadId, prompt, mode = "command_steer", inputItems = null, }) {
|
|
568
|
+
const nextPrompt = String(prompt ?? "").trim();
|
|
569
|
+
if (!nextPrompt) {
|
|
570
|
+
if (this.bot) {
|
|
571
|
+
await this.bot.api.sendMessage(chatId, "Message is empty.");
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const activeTurn = this.#getActiveTurn(chatId);
|
|
576
|
+
if (activeTurn && activeTurn.threadId === threadId) {
|
|
577
|
+
const interruption = await this.runtime.interruptThread(threadId);
|
|
578
|
+
const reason = String(interruption?.reason ?? "")
|
|
579
|
+
.trim()
|
|
580
|
+
.toLowerCase();
|
|
581
|
+
if (!(interruption?.interrupted === true ||
|
|
582
|
+
reason === "no_active_turn" ||
|
|
583
|
+
reason === "no_active_session")) {
|
|
584
|
+
if (this.bot) {
|
|
585
|
+
await this.bot.api.sendMessage(chatId, `Unable to apply steer now.\n${formatInterruptResult(interruption)}`);
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (this.bot) {
|
|
590
|
+
if (mode === "auto_message") {
|
|
591
|
+
await this.bot.api.sendMessage(chatId, "New message received. Generation interrupted, applying it now.");
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
await this.bot.api.sendMessage(chatId, "Steer accepted. Generation interrupted, applying instruction now.");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else if (this.bot) {
|
|
599
|
+
if (mode === "auto_message") {
|
|
600
|
+
await this.bot.api.sendMessage(chatId, "Applying your message now.");
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
await this.bot.api.sendMessage(chatId, "No active generation. Applying steer now.");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
void this.#processTurn({
|
|
607
|
+
chatId,
|
|
608
|
+
threadId,
|
|
609
|
+
prompt: nextPrompt,
|
|
610
|
+
inputItems,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async #openTurnControls(chatId, threadId, token) {
|
|
614
|
+
if (!this.bot) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const key = String(chatId);
|
|
618
|
+
await this.#closeTurnControls(chatId, null, null);
|
|
619
|
+
try {
|
|
620
|
+
const sent = await this.bot.api.sendMessage(chatId, "Generation in progress.", {
|
|
621
|
+
reply_markup: buildTurnControlKeyboard(token),
|
|
622
|
+
});
|
|
623
|
+
this.turnControlByChat.set(key, {
|
|
624
|
+
token: String(token),
|
|
625
|
+
threadId: String(threadId),
|
|
626
|
+
messageId: Number(sent?.message_id ?? 0),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// Non critical UI helper only.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async #closeTurnControls(chatId, token = null, finalText = null) {
|
|
634
|
+
const key = String(chatId);
|
|
635
|
+
const current = this.turnControlByChat.get(key);
|
|
636
|
+
if (!current) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (token && String(current.token) !== String(token)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
this.turnControlByChat.delete(key);
|
|
643
|
+
if (!this.bot) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
if (finalText) {
|
|
648
|
+
await this.bot.api.editMessageText(chatId, current.messageId, String(finalText), {
|
|
649
|
+
reply_markup: {
|
|
650
|
+
inline_keyboard: [],
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
await this.bot.api.editMessageReplyMarkup(chatId, current.messageId, {
|
|
656
|
+
reply_markup: {
|
|
657
|
+
inline_keyboard: [],
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// Message may be outdated/deleted; ignore.
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async #handleSingleApproval(context, decision) {
|
|
667
|
+
const chatId = String(context.chat.id);
|
|
668
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
669
|
+
await context.reply("Chat not allowed for this bot.");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
673
|
+
channelKind: this.kind,
|
|
674
|
+
channelId: this.id,
|
|
675
|
+
externalUserId: chatId,
|
|
676
|
+
});
|
|
677
|
+
const requestedId = extractFirstCommandArgument(context.message.text);
|
|
678
|
+
if (requestedId.toLowerCase() === "all") {
|
|
679
|
+
const summary = await this.#resolveAllApprovals({ threadId, decision });
|
|
680
|
+
await context.reply(summary);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const approvals = await this.runtime.listPendingApprovals(threadId);
|
|
684
|
+
const target = selectApproval(approvals, requestedId);
|
|
685
|
+
if (!target) {
|
|
686
|
+
await context.reply(buildApprovalSelectionMessage(approvals, requestedId));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
await this.runtime.resolvePendingApproval({
|
|
690
|
+
threadId,
|
|
691
|
+
approvalId: target.id,
|
|
692
|
+
decision,
|
|
693
|
+
});
|
|
694
|
+
if (decision === "decline") {
|
|
695
|
+
await context.reply(`Denied '${target.id}'.`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (decision === "acceptForSession") {
|
|
699
|
+
await context.reply(`Approved '${target.id}' with session remember.`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
await context.reply(`Approved '${target.id}'.`);
|
|
703
|
+
}
|
|
704
|
+
async #handleBulkApproval(context, decision) {
|
|
705
|
+
const chatId = String(context.chat.id);
|
|
706
|
+
if (!this.#isAllowedChat(chatId)) {
|
|
707
|
+
await context.reply("Chat not allowed for this bot.");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const threadId = await this.runtime.resolveThreadIdForChannel({
|
|
711
|
+
channelKind: this.kind,
|
|
712
|
+
channelId: this.id,
|
|
713
|
+
externalUserId: chatId,
|
|
714
|
+
});
|
|
715
|
+
const summary = await this.#resolveAllApprovals({
|
|
716
|
+
threadId,
|
|
717
|
+
decision,
|
|
718
|
+
});
|
|
719
|
+
await context.reply(summary);
|
|
720
|
+
}
|
|
721
|
+
async #resolveAllApprovals({ threadId, decision }) {
|
|
722
|
+
const approvals = await this.runtime.listPendingApprovals(threadId);
|
|
723
|
+
if (approvals.length === 0) {
|
|
724
|
+
return "No pending approvals.";
|
|
725
|
+
}
|
|
726
|
+
let successCount = 0;
|
|
727
|
+
const failedIds = [];
|
|
728
|
+
for (const approval of approvals) {
|
|
729
|
+
try {
|
|
730
|
+
await this.runtime.resolvePendingApproval({
|
|
731
|
+
threadId,
|
|
732
|
+
approvalId: approval.id,
|
|
733
|
+
decision,
|
|
734
|
+
});
|
|
735
|
+
successCount += 1;
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
failedIds.push(approval.id);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const decisionLabel = decision === "decline"
|
|
742
|
+
? "denied"
|
|
743
|
+
: decision === "acceptForSession"
|
|
744
|
+
? "approved (session)"
|
|
745
|
+
: "approved";
|
|
746
|
+
if (failedIds.length === 0) {
|
|
747
|
+
return `${successCount}/${approvals.length} approvals ${decisionLabel}.`;
|
|
748
|
+
}
|
|
749
|
+
return `${successCount}/${approvals.length} approvals ${decisionLabel}. Failed: ${failedIds.join(", ")}`;
|
|
750
|
+
}
|
|
751
|
+
#isAllowedChat(chatId) {
|
|
752
|
+
const set = this.allowedChatIds;
|
|
753
|
+
if (!set || set.size === 0) {
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
return set.has(String(chatId));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async function sendChunkedMessage(botApi, chatId, text) {
|
|
760
|
+
const max = 3900;
|
|
761
|
+
for (let start = 0; start < text.length; start += max) {
|
|
762
|
+
const chunk = text.slice(start, start + max);
|
|
763
|
+
await botApi.sendMessage(chatId, chunk || " ");
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function formatApprovalList(approvals) {
|
|
767
|
+
const lines = ["Pending approvals:"];
|
|
768
|
+
for (const approval of approvals) {
|
|
769
|
+
const commandPart = approval.command ? ` | ${approval.command.slice(0, 90)}` : "";
|
|
770
|
+
lines.push(`${approval.id} | ${approval.kind}${commandPart}`);
|
|
771
|
+
}
|
|
772
|
+
return lines.join("\n");
|
|
773
|
+
}
|
|
774
|
+
function extractFirstCommandArgument(text) {
|
|
775
|
+
const value = String(text ?? "").trim();
|
|
776
|
+
const parts = value.split(/\s+/).slice(1);
|
|
777
|
+
return String(parts[0] ?? "").trim();
|
|
778
|
+
}
|
|
779
|
+
function extractCommandTail(text) {
|
|
780
|
+
const value = String(text ?? "").trim();
|
|
781
|
+
const firstSpace = value.indexOf(" ");
|
|
782
|
+
if (firstSpace < 0) {
|
|
783
|
+
return "";
|
|
784
|
+
}
|
|
785
|
+
return value.slice(firstSpace + 1).trim();
|
|
786
|
+
}
|
|
787
|
+
function selectApproval(approvals, requestedId) {
|
|
788
|
+
const wantedId = String(requestedId ?? "").trim();
|
|
789
|
+
if (wantedId) {
|
|
790
|
+
return approvals.find((approval) => approval.id === wantedId) ?? null;
|
|
791
|
+
}
|
|
792
|
+
if (approvals.length === 1) {
|
|
793
|
+
return approvals[0];
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
function buildApprovalSelectionMessage(approvals, requestedId) {
|
|
798
|
+
const wantedId = String(requestedId ?? "").trim();
|
|
799
|
+
if (wantedId && approvals.length > 0) {
|
|
800
|
+
return `Approval '${wantedId}' not found.\n${formatApprovalList(approvals)}`;
|
|
801
|
+
}
|
|
802
|
+
if (approvals.length === 0) {
|
|
803
|
+
return "No pending approvals.";
|
|
804
|
+
}
|
|
805
|
+
return `Multiple approvals pending.\n${formatApprovalList(approvals)}\nUse /approve <id>, /approvealways <id>, /approveall, /approveallalways, /deny <id> or /denyall.`;
|
|
806
|
+
}
|
|
807
|
+
function formatInterruptResult(result) {
|
|
808
|
+
if (!result || typeof result !== "object") {
|
|
809
|
+
return "Stop request sent.";
|
|
810
|
+
}
|
|
811
|
+
if (result.interrupted === true) {
|
|
812
|
+
if (String(result.method ?? "") === "process_restart") {
|
|
813
|
+
return "Generation interruption requested (provider restarted).";
|
|
814
|
+
}
|
|
815
|
+
return "Generation interruption requested.";
|
|
816
|
+
}
|
|
817
|
+
const reason = String(result.reason ?? "")
|
|
818
|
+
.trim()
|
|
819
|
+
.toLowerCase();
|
|
820
|
+
if (reason === "no_active_turn" || reason === "no_active_session") {
|
|
821
|
+
return "No active generation to stop.";
|
|
822
|
+
}
|
|
823
|
+
if (reason === "not_supported") {
|
|
824
|
+
return "Stop is not supported by this provider.";
|
|
825
|
+
}
|
|
826
|
+
if (reason === "error") {
|
|
827
|
+
return `Stop failed:\n${String(result.error ?? "unknown error")}`;
|
|
828
|
+
}
|
|
829
|
+
return "No active generation to stop.";
|
|
830
|
+
}
|
|
831
|
+
function buildSteerPrompt(instruction) {
|
|
832
|
+
const text = String(instruction ?? "").trim();
|
|
833
|
+
return [
|
|
834
|
+
"Steer instruction from user:",
|
|
835
|
+
text,
|
|
836
|
+
"Continue by strictly applying this steering.",
|
|
837
|
+
].join("\n");
|
|
838
|
+
}
|
|
839
|
+
function isTurnInterruptedError(message) {
|
|
840
|
+
const normalized = String(message ?? "")
|
|
841
|
+
.trim()
|
|
842
|
+
.toLowerCase();
|
|
843
|
+
if (!normalized) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
return (normalized.includes("turn was interrupted") ||
|
|
847
|
+
normalized.includes("turn interrupted by user") ||
|
|
848
|
+
normalized.includes("process stopped while waiting for turn completion"));
|
|
849
|
+
}
|
|
850
|
+
function normalizePathForPrompt(value) {
|
|
851
|
+
return String(value ?? "").replace(/[\\/]+/g, "/");
|
|
852
|
+
}
|
|
853
|
+
function sanitizePathSegment(value) {
|
|
854
|
+
const normalized = String(value ?? "")
|
|
855
|
+
.trim()
|
|
856
|
+
.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
857
|
+
return normalized || "unknown";
|
|
858
|
+
}
|
|
859
|
+
function normalizeMediaExtension(primary, fallback = ".bin") {
|
|
860
|
+
const direct = String(primary ?? "").trim();
|
|
861
|
+
if (/^\.[A-Za-z0-9]{1,8}$/.test(direct)) {
|
|
862
|
+
return direct.toLowerCase();
|
|
863
|
+
}
|
|
864
|
+
const alt = String(fallback ?? "").trim();
|
|
865
|
+
if (/^\.[A-Za-z0-9]{1,8}$/.test(alt)) {
|
|
866
|
+
return alt.toLowerCase();
|
|
867
|
+
}
|
|
868
|
+
return ".bin";
|
|
869
|
+
}
|
|
870
|
+
function extensionFromMimeType(value) {
|
|
871
|
+
const mime = String(value ?? "")
|
|
872
|
+
.trim()
|
|
873
|
+
.toLowerCase();
|
|
874
|
+
if (mime.includes("ogg")) {
|
|
875
|
+
return ".ogg";
|
|
876
|
+
}
|
|
877
|
+
if (mime.includes("mpeg") || mime.includes("mp3")) {
|
|
878
|
+
return ".mp3";
|
|
879
|
+
}
|
|
880
|
+
if (mime.includes("wav")) {
|
|
881
|
+
return ".wav";
|
|
882
|
+
}
|
|
883
|
+
if (mime.includes("mp4")) {
|
|
884
|
+
return ".mp4";
|
|
885
|
+
}
|
|
886
|
+
return ".ogg";
|
|
887
|
+
}
|
|
888
|
+
function mimeTypeFromExtension(extension, fallback = "application/octet-stream") {
|
|
889
|
+
const ext = String(extension ?? "")
|
|
890
|
+
.trim()
|
|
891
|
+
.toLowerCase();
|
|
892
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
893
|
+
return "image/jpeg";
|
|
894
|
+
}
|
|
895
|
+
if (ext === ".png") {
|
|
896
|
+
return "image/png";
|
|
897
|
+
}
|
|
898
|
+
if (ext === ".webp") {
|
|
899
|
+
return "image/webp";
|
|
900
|
+
}
|
|
901
|
+
if (ext === ".gif") {
|
|
902
|
+
return "image/gif";
|
|
903
|
+
}
|
|
904
|
+
return fallback;
|
|
905
|
+
}
|
|
906
|
+
function sanitizeError(error) {
|
|
907
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
908
|
+
return raw.split(/\r?\n/).slice(0, 12).join("\n");
|
|
909
|
+
}
|
|
910
|
+
function normalizeAllowedChatIds(value) {
|
|
911
|
+
if (Array.isArray(value)) {
|
|
912
|
+
return new Set(value.map((entry) => String(entry ?? "").trim()).filter(Boolean));
|
|
913
|
+
}
|
|
914
|
+
if (typeof value === "string") {
|
|
915
|
+
return new Set(value
|
|
916
|
+
.split(",")
|
|
917
|
+
.map((entry) => entry.trim())
|
|
918
|
+
.filter(Boolean));
|
|
919
|
+
}
|
|
920
|
+
return new Set();
|
|
921
|
+
}
|
|
922
|
+
function buildTurnControlKeyboard(token) {
|
|
923
|
+
const safeToken = String(token ?? "").trim();
|
|
924
|
+
if (!safeToken) {
|
|
925
|
+
return {
|
|
926
|
+
inline_keyboard: [],
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
inline_keyboard: [
|
|
931
|
+
[
|
|
932
|
+
{
|
|
933
|
+
text: "Interrompre",
|
|
934
|
+
callback_data: `turnctl:stop:${safeToken}`,
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
],
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
function parseTurnControlCallbackData(raw) {
|
|
941
|
+
const value = String(raw ?? "").trim();
|
|
942
|
+
const match = /^turnctl:(stop):([A-Za-z0-9._:-]+)$/.exec(value);
|
|
943
|
+
if (!match) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
action: match[1],
|
|
948
|
+
token: match[2],
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
//# sourceMappingURL=telegram-channel.js.map
|