opencode-tbot 0.1.28 → 0.1.31
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/README.md +37 -2
- package/README.zh-CN.md +84 -50
- package/dist/assets/{plugin-config-DNeV2Ckw.js → plugin-config-jkAZYbFW.js} +91 -18
- package/dist/assets/plugin-config-jkAZYbFW.js.map +1 -0
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.js +1186 -338
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/plugin-config-DNeV2Ckw.js.map +0 -1
package/dist/plugin.js
CHANGED
|
@@ -1,73 +1,361 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { i as OPENCODE_TBOT_VERSION, n as preparePluginConfiguration, o as loadAppConfig } from "./assets/plugin-config-jkAZYbFW.js";
|
|
2
|
+
import { appendFile, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
7
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
8
8
|
import { run } from "@grammyjs/runner";
|
|
9
|
-
import { Bot, InlineKeyboard } from "grammy";
|
|
9
|
+
import { Bot, GrammyError, HttpError, InlineKeyboard } from "grammy";
|
|
10
10
|
//#region src/infra/utils/redact.ts
|
|
11
|
-
var REDACTED = "[REDACTED]";
|
|
12
|
-
var DEFAULT_PREVIEW_LENGTH = 160;
|
|
11
|
+
var REDACTED$1 = "[REDACTED]";
|
|
13
12
|
var TELEGRAM_TOKEN_PATTERN = /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g;
|
|
14
13
|
var BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/gi;
|
|
15
14
|
var NAMED_SECRET_PATTERN = /\b(api[_\s-]?key|token|secret|password)\b(\s*[:=]\s*)([^\s,;]+)/gi;
|
|
16
15
|
var API_KEY_LIKE_PATTERN = /\b(?:sk|pk)_[A-Za-z0-9_-]{10,}\b/g;
|
|
17
16
|
function redactSensitiveText(input) {
|
|
18
|
-
return input.replace(BEARER_TOKEN_PATTERN, `Bearer ${REDACTED}`).replace(TELEGRAM_TOKEN_PATTERN, REDACTED).replace(NAMED_SECRET_PATTERN, (_, name, separator) => `${name}${separator}${REDACTED}`).replace(API_KEY_LIKE_PATTERN, REDACTED);
|
|
19
|
-
}
|
|
20
|
-
function createRedactedPreview(input, maxLength = DEFAULT_PREVIEW_LENGTH) {
|
|
21
|
-
const redacted = redactSensitiveText(input).replace(/\s+/g, " ").trim();
|
|
22
|
-
if (redacted.length <= maxLength) return redacted;
|
|
23
|
-
return `${redacted.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
17
|
+
return input.replace(BEARER_TOKEN_PATTERN, `Bearer ${REDACTED$1}`).replace(TELEGRAM_TOKEN_PATTERN, REDACTED$1).replace(NAMED_SECRET_PATTERN, (_, name, separator) => `${name}${separator}${REDACTED$1}`).replace(API_KEY_LIKE_PATTERN, REDACTED$1);
|
|
24
18
|
}
|
|
25
19
|
//#endregion
|
|
26
20
|
//#region src/infra/logger/index.ts
|
|
21
|
+
var DEFAULT_COMPONENT = "app";
|
|
22
|
+
var DEFAULT_EVENT = "log";
|
|
27
23
|
var DEFAULT_SERVICE_NAME = "opencode-tbot";
|
|
24
|
+
var DEFAULT_MAX_LOG_FILES = 30;
|
|
25
|
+
var DEFAULT_MAX_TOTAL_LOG_BYTES = 314572800;
|
|
26
|
+
var CONTENT_OMITTED = "[OMITTED]";
|
|
27
|
+
var REDACTED = "[REDACTED]";
|
|
28
28
|
var LEVEL_PRIORITY = {
|
|
29
29
|
debug: 10,
|
|
30
30
|
info: 20,
|
|
31
31
|
warn: 30,
|
|
32
32
|
error: 40
|
|
33
33
|
};
|
|
34
|
+
var RESERVED_EVENT_FIELDS = new Set([
|
|
35
|
+
"attempt",
|
|
36
|
+
"callbackData",
|
|
37
|
+
"chatId",
|
|
38
|
+
"command",
|
|
39
|
+
"component",
|
|
40
|
+
"correlationId",
|
|
41
|
+
"durationMs",
|
|
42
|
+
"error",
|
|
43
|
+
"event",
|
|
44
|
+
"operationId",
|
|
45
|
+
"projectId",
|
|
46
|
+
"requestId",
|
|
47
|
+
"runtimeId",
|
|
48
|
+
"sessionId",
|
|
49
|
+
"sizeBytes",
|
|
50
|
+
"status",
|
|
51
|
+
"updateId",
|
|
52
|
+
"worktree"
|
|
53
|
+
]);
|
|
34
54
|
function createOpenCodeAppLogger(client, options = {}) {
|
|
35
55
|
const service = normalizeServiceName(options.service);
|
|
36
56
|
const minimumLevel = normalizeLogLevel(options.level);
|
|
57
|
+
const runtimeId = normalizeString(options.runtimeId) ?? randomUUID();
|
|
58
|
+
const boundRootContext = {
|
|
59
|
+
component: DEFAULT_COMPONENT,
|
|
60
|
+
runtimeId,
|
|
61
|
+
...options.worktree ? { worktree: options.worktree } : {}
|
|
62
|
+
};
|
|
63
|
+
const sinks = createSinks(client, {
|
|
64
|
+
file: {
|
|
65
|
+
dir: options.file?.dir,
|
|
66
|
+
maxFiles: options.file?.retention?.maxFiles,
|
|
67
|
+
maxTotalBytes: options.file?.retention?.maxTotalBytes
|
|
68
|
+
},
|
|
69
|
+
runtimeId,
|
|
70
|
+
service,
|
|
71
|
+
sinks: options.sinks
|
|
72
|
+
});
|
|
37
73
|
let queue = Promise.resolve();
|
|
38
|
-
const enqueue = (
|
|
39
|
-
if (LEVEL_PRIORITY[level] < LEVEL_PRIORITY[minimumLevel]) return;
|
|
40
|
-
const
|
|
41
|
-
const payload = {
|
|
42
|
-
service,
|
|
43
|
-
level,
|
|
44
|
-
message: text,
|
|
45
|
-
...extra ? { extra } : {}
|
|
46
|
-
};
|
|
74
|
+
const enqueue = (event) => {
|
|
75
|
+
if (LEVEL_PRIORITY[event.level] < LEVEL_PRIORITY[minimumLevel]) return;
|
|
76
|
+
const structuredEvent = buildStructuredLogEvent(event.level, event.input, event.message, service, event.context);
|
|
47
77
|
queue = queue.catch(() => void 0).then(async () => {
|
|
48
|
-
|
|
49
|
-
await client.app.log(payload);
|
|
50
|
-
} catch {}
|
|
78
|
+
await Promise.allSettled(sinks.map((sink) => sink.write(structuredEvent)));
|
|
51
79
|
});
|
|
52
80
|
};
|
|
53
|
-
|
|
81
|
+
const createLogger = (context) => ({
|
|
54
82
|
debug(input, message) {
|
|
55
|
-
enqueue(
|
|
83
|
+
enqueue({
|
|
84
|
+
context,
|
|
85
|
+
input,
|
|
86
|
+
level: "debug",
|
|
87
|
+
message
|
|
88
|
+
});
|
|
56
89
|
},
|
|
57
90
|
info(input, message) {
|
|
58
|
-
enqueue(
|
|
91
|
+
enqueue({
|
|
92
|
+
context,
|
|
93
|
+
input,
|
|
94
|
+
level: "info",
|
|
95
|
+
message
|
|
96
|
+
});
|
|
59
97
|
},
|
|
60
98
|
warn(input, message) {
|
|
61
|
-
enqueue(
|
|
99
|
+
enqueue({
|
|
100
|
+
context,
|
|
101
|
+
input,
|
|
102
|
+
level: "warn",
|
|
103
|
+
message
|
|
104
|
+
});
|
|
62
105
|
},
|
|
63
106
|
error(input, message) {
|
|
64
|
-
enqueue(
|
|
107
|
+
enqueue({
|
|
108
|
+
context,
|
|
109
|
+
input,
|
|
110
|
+
level: "error",
|
|
111
|
+
message
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
child(childContext) {
|
|
115
|
+
return createLogger({
|
|
116
|
+
...context,
|
|
117
|
+
...removeUndefinedFields(childContext)
|
|
118
|
+
});
|
|
65
119
|
},
|
|
66
120
|
async flush() {
|
|
67
121
|
await queue.catch(() => void 0);
|
|
122
|
+
await Promise.allSettled(sinks.map(async (sink) => {
|
|
123
|
+
await sink.flush?.();
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return createLogger(boundRootContext);
|
|
128
|
+
}
|
|
129
|
+
function logTelegramUpdate(logger, input, message) {
|
|
130
|
+
logger.info({
|
|
131
|
+
component: "telegram",
|
|
132
|
+
...input
|
|
133
|
+
}, message);
|
|
134
|
+
}
|
|
135
|
+
function logPromptLifecycle(logger, input, message) {
|
|
136
|
+
logger.info({
|
|
137
|
+
component: "prompt",
|
|
138
|
+
...input
|
|
139
|
+
}, message);
|
|
140
|
+
}
|
|
141
|
+
function logOpenCodeRequest(logger, input, message) {
|
|
142
|
+
logger.info({
|
|
143
|
+
component: "opencode",
|
|
144
|
+
...input
|
|
145
|
+
}, message);
|
|
146
|
+
}
|
|
147
|
+
function logPluginEvent(logger, input, message) {
|
|
148
|
+
logger.info({
|
|
149
|
+
component: "plugin-event",
|
|
150
|
+
...input
|
|
151
|
+
}, message);
|
|
152
|
+
}
|
|
153
|
+
function createSinks(client, options) {
|
|
154
|
+
const sinkOptions = options.sinks ?? {};
|
|
155
|
+
const sinks = [];
|
|
156
|
+
if (sinkOptions.host !== false) sinks.push(createHostSink(client, options.service));
|
|
157
|
+
if (sinkOptions.file !== false && options.file?.dir) sinks.push(createJsonlFileSink({
|
|
158
|
+
dir: options.file.dir,
|
|
159
|
+
maxFiles: options.file.maxFiles,
|
|
160
|
+
maxTotalBytes: options.file.maxTotalBytes,
|
|
161
|
+
runtimeId: options.runtimeId
|
|
162
|
+
}));
|
|
163
|
+
return sinks;
|
|
164
|
+
}
|
|
165
|
+
function createHostSink(client, service) {
|
|
166
|
+
return { async write(event) {
|
|
167
|
+
const payload = {
|
|
168
|
+
service,
|
|
169
|
+
level: event.level,
|
|
170
|
+
message: event.message,
|
|
171
|
+
extra: buildHostLogExtra(event)
|
|
172
|
+
};
|
|
173
|
+
if (client.app.log.length >= 2) {
|
|
174
|
+
await client.app.log(payload, {
|
|
175
|
+
responseStyle: "data",
|
|
176
|
+
throwOnError: true
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await client.app.log({
|
|
181
|
+
body: payload,
|
|
182
|
+
responseStyle: "data",
|
|
183
|
+
throwOnError: true
|
|
184
|
+
});
|
|
185
|
+
} };
|
|
186
|
+
}
|
|
187
|
+
function createJsonlFileSink(options) {
|
|
188
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_LOG_FILES;
|
|
189
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_LOG_BYTES;
|
|
190
|
+
const runtimeSegment = sanitizeFileSegment(options.runtimeId);
|
|
191
|
+
const timestampSegment = (/* @__PURE__ */ new Date()).toISOString().replace(/:/gu, "-");
|
|
192
|
+
const filePath = join(options.dir, `${timestampSegment}.${process.pid}.${runtimeSegment}.jsonl`);
|
|
193
|
+
let initialized = false;
|
|
194
|
+
let writeCount = 0;
|
|
195
|
+
const initialize = async () => {
|
|
196
|
+
if (initialized) return;
|
|
197
|
+
await mkdir(options.dir, { recursive: true });
|
|
198
|
+
await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
|
|
199
|
+
initialized = true;
|
|
200
|
+
};
|
|
201
|
+
return { async write(event) {
|
|
202
|
+
await initialize();
|
|
203
|
+
await appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
|
|
204
|
+
writeCount += 1;
|
|
205
|
+
if (writeCount === 1 || writeCount % 100 === 0) await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
|
|
206
|
+
} };
|
|
207
|
+
}
|
|
208
|
+
async function cleanupLogDirectory(directory, maxFiles, maxTotalBytes) {
|
|
209
|
+
let entries = [];
|
|
210
|
+
try {
|
|
211
|
+
const names = await readdir(directory);
|
|
212
|
+
entries = await Promise.all(names.filter((name) => name.endsWith(".jsonl")).map(async (name) => {
|
|
213
|
+
const entryPath = join(directory, name);
|
|
214
|
+
const entryStat = await stat(entryPath);
|
|
215
|
+
return {
|
|
216
|
+
path: entryPath,
|
|
217
|
+
size: entryStat.size,
|
|
218
|
+
time: entryStat.mtimeMs
|
|
219
|
+
};
|
|
220
|
+
}));
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
entries.sort((left, right) => right.time - left.time);
|
|
225
|
+
let totalBytes = 0;
|
|
226
|
+
const retained = [];
|
|
227
|
+
const deleted = [];
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const canKeepByCount = retained.length < maxFiles;
|
|
230
|
+
const canKeepBySize = retained.length === 0 || totalBytes + entry.size <= maxTotalBytes;
|
|
231
|
+
if (canKeepByCount && canKeepBySize) {
|
|
232
|
+
retained.push(entry);
|
|
233
|
+
totalBytes += entry.size;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
deleted.push(entry);
|
|
237
|
+
}
|
|
238
|
+
await Promise.allSettled(deleted.map(async (entry) => {
|
|
239
|
+
await unlink(entry.path);
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
function buildStructuredLogEvent(level, input, message, service, boundContext) {
|
|
243
|
+
const extracted = extractContextAndExtra(input);
|
|
244
|
+
const context = {
|
|
245
|
+
...removeUndefinedFields(boundContext),
|
|
246
|
+
...removeUndefinedFields(extracted.context)
|
|
247
|
+
};
|
|
248
|
+
const resolvedMessage = normalizeLogMessage(input, message);
|
|
249
|
+
return {
|
|
250
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
251
|
+
level,
|
|
252
|
+
service,
|
|
253
|
+
component: normalizeComponent(context.component),
|
|
254
|
+
event: normalizeEventName(context.event),
|
|
255
|
+
message: redactSensitiveText(resolvedMessage),
|
|
256
|
+
runtimeId: context.runtimeId ?? null,
|
|
257
|
+
operationId: context.operationId ?? null,
|
|
258
|
+
correlationId: resolveCorrelationId(context),
|
|
259
|
+
...context.worktree ? { worktree: redactSensitiveText(context.worktree) } : {},
|
|
260
|
+
...typeof context.chatId === "number" ? { chatId: context.chatId } : {},
|
|
261
|
+
...context.sessionId ? { sessionId: context.sessionId } : {},
|
|
262
|
+
...context.projectId ? { projectId: context.projectId } : {},
|
|
263
|
+
...context.requestId ? { requestId: context.requestId } : {},
|
|
264
|
+
...typeof context.updateId === "number" ? { updateId: context.updateId } : {},
|
|
265
|
+
...context.command ? { command: context.command } : {},
|
|
266
|
+
...context.callbackData ? { callbackData: context.callbackData } : {},
|
|
267
|
+
...typeof context.durationMs === "number" ? { durationMs: context.durationMs } : {},
|
|
268
|
+
...typeof context.attempt === "number" ? { attempt: context.attempt } : {},
|
|
269
|
+
...context.status ? { status: context.status } : {},
|
|
270
|
+
...typeof context.sizeBytes === "number" ? { sizeBytes: context.sizeBytes } : {},
|
|
271
|
+
...context.error ? { error: context.error } : {},
|
|
272
|
+
...removeUndefinedFields(extracted.extra)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function buildHostLogExtra(event) {
|
|
276
|
+
const { service: _service, level: _level, message: _message, ...rest } = event;
|
|
277
|
+
return rest;
|
|
278
|
+
}
|
|
279
|
+
function extractContextAndExtra(input) {
|
|
280
|
+
if (input instanceof Error) return {
|
|
281
|
+
context: { error: serializeError(input) },
|
|
282
|
+
extra: {}
|
|
283
|
+
};
|
|
284
|
+
if (Array.isArray(input)) return {
|
|
285
|
+
context: {},
|
|
286
|
+
extra: { items: input.map((item) => sanitizeValue(item)) }
|
|
287
|
+
};
|
|
288
|
+
if (input === null || input === void 0) return {
|
|
289
|
+
context: {},
|
|
290
|
+
extra: {}
|
|
291
|
+
};
|
|
292
|
+
if (typeof input === "string") return {
|
|
293
|
+
context: {},
|
|
294
|
+
extra: {}
|
|
295
|
+
};
|
|
296
|
+
if (typeof input !== "object") return {
|
|
297
|
+
context: {},
|
|
298
|
+
extra: { value: sanitizeValue(input) }
|
|
299
|
+
};
|
|
300
|
+
const record = input;
|
|
301
|
+
const context = {};
|
|
302
|
+
const extra = {};
|
|
303
|
+
for (const [key, value] of Object.entries(record)) {
|
|
304
|
+
if (RESERVED_EVENT_FIELDS.has(key)) {
|
|
305
|
+
assignReservedField(context, key, value);
|
|
306
|
+
continue;
|
|
68
307
|
}
|
|
308
|
+
extra[key] = sanitizeFieldValue(key, value);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
context,
|
|
312
|
+
extra
|
|
69
313
|
};
|
|
70
314
|
}
|
|
315
|
+
function assignReservedField(context, key, value) {
|
|
316
|
+
switch (key) {
|
|
317
|
+
case "attempt":
|
|
318
|
+
case "durationMs":
|
|
319
|
+
case "sizeBytes":
|
|
320
|
+
if (typeof value === "number") context[key] = value;
|
|
321
|
+
return;
|
|
322
|
+
case "chatId":
|
|
323
|
+
case "updateId":
|
|
324
|
+
if (typeof value === "number") context[key] = value;
|
|
325
|
+
return;
|
|
326
|
+
case "callbackData":
|
|
327
|
+
case "command":
|
|
328
|
+
case "component":
|
|
329
|
+
case "event":
|
|
330
|
+
case "projectId":
|
|
331
|
+
case "requestId":
|
|
332
|
+
case "sessionId":
|
|
333
|
+
case "status":
|
|
334
|
+
case "worktree":
|
|
335
|
+
if (typeof value === "string" && value.trim().length > 0) context[key] = redactSensitiveText(value.trim());
|
|
336
|
+
return;
|
|
337
|
+
case "correlationId":
|
|
338
|
+
case "operationId":
|
|
339
|
+
case "runtimeId":
|
|
340
|
+
if (typeof value === "string" && value.trim().length > 0) context[key] = value.trim();
|
|
341
|
+
else if (value === null) context[key] = null;
|
|
342
|
+
return;
|
|
343
|
+
case "error":
|
|
344
|
+
if (value instanceof Error) context.error = serializeError(value);
|
|
345
|
+
else if (isPlainObject(value)) context.error = normalizeStructuredLogErrorRecord(value);
|
|
346
|
+
return;
|
|
347
|
+
default: return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function normalizeLogMessage(input, message) {
|
|
351
|
+
if (typeof input === "string") {
|
|
352
|
+
const normalizedInput = input.trim();
|
|
353
|
+
return normalizedInput.length > 0 ? normalizedInput : "log";
|
|
354
|
+
}
|
|
355
|
+
if (message && message.trim().length > 0) return message.trim();
|
|
356
|
+
if (input instanceof Error) return input.message.trim() || input.name;
|
|
357
|
+
return "log";
|
|
358
|
+
}
|
|
71
359
|
function normalizeServiceName(value) {
|
|
72
360
|
const normalized = value?.trim();
|
|
73
361
|
return normalized && normalized.length > 0 ? normalized : DEFAULT_SERVICE_NAME;
|
|
@@ -80,59 +368,140 @@ function normalizeLogLevel(value) {
|
|
|
80
368
|
default: return "info";
|
|
81
369
|
}
|
|
82
370
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
371
|
+
function normalizeComponent(value) {
|
|
372
|
+
const normalized = value?.trim();
|
|
373
|
+
return normalized && normalized.length > 0 ? normalized : DEFAULT_COMPONENT;
|
|
374
|
+
}
|
|
375
|
+
function normalizeEventName(value) {
|
|
376
|
+
const normalized = value?.trim();
|
|
377
|
+
return normalized && normalized.length > 0 ? normalized : DEFAULT_EVENT;
|
|
378
|
+
}
|
|
379
|
+
function resolveCorrelationId(context) {
|
|
380
|
+
if (context.correlationId) return context.correlationId;
|
|
381
|
+
if (typeof context.updateId === "number") return String(context.updateId);
|
|
382
|
+
return context.operationId ?? null;
|
|
383
|
+
}
|
|
384
|
+
function serializeError(error) {
|
|
97
385
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
386
|
+
name: error.name,
|
|
387
|
+
message: redactSensitiveText(error.message),
|
|
388
|
+
...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
|
|
389
|
+
..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
|
|
100
390
|
};
|
|
101
391
|
}
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
if (input instanceof Error) return { error: serializeError(input) };
|
|
105
|
-
if (Array.isArray(input)) return { items: input.map((item) => sanitizeValue(item)) };
|
|
106
|
-
if (typeof input !== "object") return { value: sanitizeValue(input) };
|
|
107
|
-
return sanitizeRecord(input);
|
|
392
|
+
function sanitizePlainObject(value) {
|
|
393
|
+
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, sanitizeFieldValue(key, entryValue)]));
|
|
108
394
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
395
|
+
function sanitizeFieldValue(key, value) {
|
|
396
|
+
if (isSensitiveKey(key)) return redactSensitiveFieldValue(value);
|
|
397
|
+
if (isUrlLikeKey(key)) return summarizeUrlValue(value);
|
|
398
|
+
if (isTextContentKey(key)) return summarizeTextValue(value);
|
|
399
|
+
if (isAttachmentCollectionKey(key)) return summarizeAttachmentCollection(value);
|
|
400
|
+
return sanitizeValue(value);
|
|
111
401
|
}
|
|
112
402
|
function sanitizeValue(value) {
|
|
113
403
|
if (value instanceof Error) return serializeError(value);
|
|
114
|
-
if (Array.isArray(value)) return value.map((
|
|
404
|
+
if (Array.isArray(value)) return value.map((entry) => sanitizeValue(entry));
|
|
115
405
|
if (typeof value === "string") return redactSensitiveText(value);
|
|
116
406
|
if (!value || typeof value !== "object") return value;
|
|
117
|
-
return
|
|
407
|
+
return sanitizePlainObject(value);
|
|
118
408
|
}
|
|
119
|
-
function
|
|
409
|
+
function summarizeTextValue(value) {
|
|
410
|
+
if (typeof value === "string") return {
|
|
411
|
+
omitted: CONTENT_OMITTED,
|
|
412
|
+
length: value.length
|
|
413
|
+
};
|
|
414
|
+
if (Array.isArray(value)) return {
|
|
415
|
+
omitted: CONTENT_OMITTED,
|
|
416
|
+
count: value.length
|
|
417
|
+
};
|
|
418
|
+
if (value && typeof value === "object") return {
|
|
419
|
+
omitted: CONTENT_OMITTED,
|
|
420
|
+
kind: "object"
|
|
421
|
+
};
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
function summarizeUrlValue(value) {
|
|
425
|
+
if (typeof value === "string" && value.trim().length > 0) return {
|
|
426
|
+
omitted: CONTENT_OMITTED,
|
|
427
|
+
kind: "url"
|
|
428
|
+
};
|
|
429
|
+
return sanitizeValue(value);
|
|
430
|
+
}
|
|
431
|
+
function summarizeAttachmentCollection(value) {
|
|
432
|
+
if (!Array.isArray(value)) return summarizeTextValue(value);
|
|
120
433
|
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
|
|
124
|
-
..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
|
|
434
|
+
count: value.length,
|
|
435
|
+
items: value.map((entry) => summarizeAttachmentValue(entry))
|
|
125
436
|
};
|
|
126
437
|
}
|
|
438
|
+
function summarizeAttachmentValue(value) {
|
|
439
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return summarizeTextValue(value);
|
|
440
|
+
const record = value;
|
|
441
|
+
return removeUndefinedFields({
|
|
442
|
+
filename: normalizeString(record.filename) ?? normalizeString(record.fileName) ?? void 0,
|
|
443
|
+
mime: normalizeString(record.mime) ?? normalizeString(record.mimeType) ?? void 0,
|
|
444
|
+
sizeBytes: pickNumericValue(record, [
|
|
445
|
+
"sizeBytes",
|
|
446
|
+
"size",
|
|
447
|
+
"fileSize"
|
|
448
|
+
]),
|
|
449
|
+
hasCaption: pickStringValue(record, ["caption"]) !== null,
|
|
450
|
+
textLength: pickStringValue(record, ["text", "prompt"])?.length,
|
|
451
|
+
type: normalizeString(record.type) ?? void 0
|
|
452
|
+
});
|
|
453
|
+
}
|
|
127
454
|
function isSensitiveKey(key) {
|
|
128
|
-
return /token|secret|api[-_]?key|authorization|password/iu.test(key);
|
|
455
|
+
return /token|secret|api[-_]?key|authorization|password|cookie/iu.test(key);
|
|
456
|
+
}
|
|
457
|
+
function isTextContentKey(key) {
|
|
458
|
+
return /(^|[-_])(text|prompt|caption|body|markdown|content|messageText|fallbackText|input|raw)$/iu.test(key) || /bodyMd|bodyText|messageText|fallbackText/iu.test(key);
|
|
459
|
+
}
|
|
460
|
+
function isUrlLikeKey(key) {
|
|
461
|
+
return /(^|[-_])(url|uri|href|filePath|downloadPath|downloadUrl|fileUrl)$/iu.test(key);
|
|
462
|
+
}
|
|
463
|
+
function isAttachmentCollectionKey(key) {
|
|
464
|
+
return /(^|[-_])(files|parts|attachments)$/iu.test(key);
|
|
129
465
|
}
|
|
130
466
|
function redactSensitiveFieldValue(value) {
|
|
131
|
-
if (typeof value === "string" && value.trim().length > 0) return
|
|
132
|
-
if (Array.isArray(value)) return value.map(() =>
|
|
133
|
-
if (value && typeof value === "object") return
|
|
467
|
+
if (typeof value === "string" && value.trim().length > 0) return REDACTED;
|
|
468
|
+
if (Array.isArray(value)) return value.map(() => REDACTED);
|
|
469
|
+
if (value && typeof value === "object") return REDACTED;
|
|
134
470
|
return value;
|
|
135
471
|
}
|
|
472
|
+
function removeUndefinedFields(record) {
|
|
473
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
474
|
+
}
|
|
475
|
+
function isPlainObject(value) {
|
|
476
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
477
|
+
}
|
|
478
|
+
function normalizeString(value) {
|
|
479
|
+
return typeof value === "string" && value.trim().length > 0 ? redactSensitiveText(value.trim()) : null;
|
|
480
|
+
}
|
|
481
|
+
function pickNumericValue(record, keys) {
|
|
482
|
+
for (const key of keys) {
|
|
483
|
+
const value = record[key];
|
|
484
|
+
if (typeof value === "number") return value;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function pickStringValue(record, keys) {
|
|
488
|
+
for (const key of keys) {
|
|
489
|
+
const value = record[key];
|
|
490
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
function sanitizeFileSegment(value) {
|
|
495
|
+
return value.replace(/[^a-z0-9._-]+/giu, "_");
|
|
496
|
+
}
|
|
497
|
+
function normalizeStructuredLogErrorRecord(value) {
|
|
498
|
+
return {
|
|
499
|
+
name: normalizeString(value.name) ?? "Error",
|
|
500
|
+
message: normalizeString(value.message) ?? "Unknown error",
|
|
501
|
+
...typeof value.stack === "string" ? { stack: redactSensitiveText(value.stack) } : {},
|
|
502
|
+
...value.data !== void 0 ? { data: sanitizeValue(value.data) } : {}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
136
505
|
//#endregion
|
|
137
506
|
//#region src/repositories/pending-action.repo.ts
|
|
138
507
|
var FilePendingActionRepository = class {
|
|
@@ -292,7 +661,10 @@ var OpenCodeClient = class {
|
|
|
292
661
|
...SDK_OPTIONS,
|
|
293
662
|
...input.signal ? { signal: input.signal } : {}
|
|
294
663
|
};
|
|
295
|
-
return unwrapSdkData(
|
|
664
|
+
return unwrapSdkData(handler.length >= 2 ? await handler.call(target, input.legacyParameters, options) : await handler.call(target, input.parameters === void 0 ? options : {
|
|
665
|
+
...input.parameters,
|
|
666
|
+
...options
|
|
667
|
+
}));
|
|
296
668
|
}
|
|
297
669
|
async getHealth() {
|
|
298
670
|
try {
|
|
@@ -303,25 +675,43 @@ var OpenCodeClient = class {
|
|
|
303
675
|
}
|
|
304
676
|
}
|
|
305
677
|
async abortSession(sessionId) {
|
|
306
|
-
return this.callScopedSdkMethod("session", "abort", {
|
|
678
|
+
return this.callScopedSdkMethod("session", "abort", {
|
|
679
|
+
legacyParameters: { sessionID: sessionId },
|
|
680
|
+
parameters: { path: { id: sessionId } }
|
|
681
|
+
});
|
|
307
682
|
}
|
|
308
683
|
async deleteSession(sessionId) {
|
|
309
|
-
return this.callScopedSdkMethod("session", "delete", {
|
|
684
|
+
return this.callScopedSdkMethod("session", "delete", {
|
|
685
|
+
legacyParameters: { sessionID: sessionId },
|
|
686
|
+
parameters: { path: { id: sessionId } }
|
|
687
|
+
});
|
|
310
688
|
}
|
|
311
689
|
async forkSession(sessionId, messageId) {
|
|
312
|
-
return this.callScopedSdkMethod("session", "fork", {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
690
|
+
return this.callScopedSdkMethod("session", "fork", {
|
|
691
|
+
legacyParameters: {
|
|
692
|
+
sessionID: sessionId,
|
|
693
|
+
...messageId?.trim() ? { messageID: messageId.trim() } : {}
|
|
694
|
+
},
|
|
695
|
+
parameters: {
|
|
696
|
+
path: { id: sessionId },
|
|
697
|
+
...messageId?.trim() ? { body: { messageID: messageId.trim() } } : {}
|
|
698
|
+
}
|
|
699
|
+
});
|
|
316
700
|
}
|
|
317
701
|
async getPath() {
|
|
318
702
|
return this.callScopedSdkMethod("path", "get", {});
|
|
319
703
|
}
|
|
320
704
|
async listLspStatuses(directory) {
|
|
321
|
-
return this.callScopedSdkMethod("lsp", "status", {
|
|
705
|
+
return this.callScopedSdkMethod("lsp", "status", {
|
|
706
|
+
legacyParameters: directory ? { directory } : void 0,
|
|
707
|
+
parameters: directory ? { query: { directory } } : void 0
|
|
708
|
+
});
|
|
322
709
|
}
|
|
323
710
|
async listMcpStatuses(directory) {
|
|
324
|
-
return this.callScopedSdkMethod("mcp", "status", {
|
|
711
|
+
return this.callScopedSdkMethod("mcp", "status", {
|
|
712
|
+
legacyParameters: directory ? { directory } : void 0,
|
|
713
|
+
parameters: directory ? { query: { directory } } : void 0
|
|
714
|
+
});
|
|
325
715
|
}
|
|
326
716
|
async getSessionStatuses() {
|
|
327
717
|
return this.loadSessionStatuses();
|
|
@@ -336,29 +726,69 @@ var OpenCodeClient = class {
|
|
|
336
726
|
return this.callScopedSdkMethod("project", "current", {});
|
|
337
727
|
}
|
|
338
728
|
async createSessionForDirectory(directory, title) {
|
|
339
|
-
return this.callScopedSdkMethod("session", "create", {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
729
|
+
return this.callScopedSdkMethod("session", "create", {
|
|
730
|
+
legacyParameters: title ? {
|
|
731
|
+
directory,
|
|
732
|
+
title
|
|
733
|
+
} : { directory },
|
|
734
|
+
parameters: {
|
|
735
|
+
query: { directory },
|
|
736
|
+
...title ? { body: { title } } : {}
|
|
737
|
+
}
|
|
738
|
+
});
|
|
343
739
|
}
|
|
344
740
|
async renameSession(sessionId, title) {
|
|
345
|
-
return this.callScopedSdkMethod("session", "update", {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
741
|
+
return this.callScopedSdkMethod("session", "update", {
|
|
742
|
+
legacyParameters: {
|
|
743
|
+
sessionID: sessionId,
|
|
744
|
+
title
|
|
745
|
+
},
|
|
746
|
+
parameters: {
|
|
747
|
+
path: { id: sessionId },
|
|
748
|
+
body: { title }
|
|
749
|
+
}
|
|
750
|
+
});
|
|
349
751
|
}
|
|
350
752
|
async listAgents() {
|
|
351
753
|
return this.callScopedSdkMethod("app", "agents", {});
|
|
352
754
|
}
|
|
353
755
|
async listPendingPermissions(directory) {
|
|
354
|
-
return this.callScopedSdkMethod("permission", "list", {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
756
|
+
return (await this.callScopedSdkMethod("permission", "list", {
|
|
757
|
+
legacyParameters: directory ? { directory } : void 0,
|
|
758
|
+
parameters: directory ? { query: { directory } } : void 0
|
|
759
|
+
})).map(normalizePermissionRequest).filter((permission) => !!permission);
|
|
760
|
+
}
|
|
761
|
+
async replyToPermission(requestId, reply, message, sessionId) {
|
|
762
|
+
const normalizedMessage = message?.trim();
|
|
763
|
+
const rootPermissionHandler = this.client.postSessionIdPermissionsPermissionId;
|
|
764
|
+
if (sessionId && typeof rootPermissionHandler === "function") return unwrapSdkData(await rootPermissionHandler.call(this.client, {
|
|
765
|
+
...SDK_OPTIONS,
|
|
766
|
+
body: {
|
|
767
|
+
response: reply,
|
|
768
|
+
...normalizedMessage ? { message: normalizedMessage } : {}
|
|
769
|
+
},
|
|
770
|
+
path: {
|
|
771
|
+
id: sessionId,
|
|
772
|
+
permissionID: requestId
|
|
773
|
+
}
|
|
774
|
+
}));
|
|
775
|
+
return this.callScopedSdkMethod("permission", "reply", {
|
|
776
|
+
legacyParameters: {
|
|
777
|
+
requestID: requestId,
|
|
778
|
+
reply,
|
|
779
|
+
...normalizedMessage ? { message: normalizedMessage } : {}
|
|
780
|
+
},
|
|
781
|
+
parameters: sessionId ? {
|
|
782
|
+
body: {
|
|
783
|
+
response: reply,
|
|
784
|
+
...normalizedMessage ? { message: normalizedMessage } : {}
|
|
785
|
+
},
|
|
786
|
+
path: {
|
|
787
|
+
id: sessionId,
|
|
788
|
+
permissionID: requestId
|
|
789
|
+
}
|
|
790
|
+
} : void 0
|
|
791
|
+
});
|
|
362
792
|
}
|
|
363
793
|
async listModels() {
|
|
364
794
|
const now = Date.now();
|
|
@@ -494,10 +924,14 @@ var OpenCodeClient = class {
|
|
|
494
924
|
messageId
|
|
495
925
|
}, async (requestSignal) => {
|
|
496
926
|
return normalizePromptResponse(await this.callScopedSdkMethod("session", "message", {
|
|
497
|
-
|
|
927
|
+
legacyParameters: {
|
|
498
928
|
sessionID: sessionId,
|
|
499
929
|
messageID: messageId
|
|
500
930
|
},
|
|
931
|
+
parameters: { path: {
|
|
932
|
+
id: sessionId,
|
|
933
|
+
messageID: messageId
|
|
934
|
+
} },
|
|
501
935
|
signal: requestSignal
|
|
502
936
|
}));
|
|
503
937
|
}, signal);
|
|
@@ -525,10 +959,14 @@ var OpenCodeClient = class {
|
|
|
525
959
|
timeoutMs: this.promptTimeoutPolicy.pollRequestTimeoutMs
|
|
526
960
|
}, async (requestSignal) => {
|
|
527
961
|
return normalizePromptResponses(await this.callScopedSdkMethod("session", "messages", {
|
|
528
|
-
|
|
962
|
+
legacyParameters: {
|
|
529
963
|
sessionID: sessionId,
|
|
530
964
|
limit: PROMPT_MESSAGE_POLL_LIMIT
|
|
531
965
|
},
|
|
966
|
+
parameters: {
|
|
967
|
+
path: { id: sessionId },
|
|
968
|
+
query: { limit: PROMPT_MESSAGE_POLL_LIMIT }
|
|
969
|
+
},
|
|
532
970
|
signal: requestSignal
|
|
533
971
|
}));
|
|
534
972
|
}, signal);
|
|
@@ -589,10 +1027,14 @@ var OpenCodeClient = class {
|
|
|
589
1027
|
...input.variant ? { variant: input.variant } : {},
|
|
590
1028
|
parts
|
|
591
1029
|
};
|
|
592
|
-
const
|
|
1030
|
+
const legacyRequestParameters = {
|
|
593
1031
|
sessionID: input.sessionId,
|
|
594
1032
|
...requestBody
|
|
595
1033
|
};
|
|
1034
|
+
const requestParameters = {
|
|
1035
|
+
body: requestBody,
|
|
1036
|
+
path: { id: input.sessionId }
|
|
1037
|
+
};
|
|
596
1038
|
try {
|
|
597
1039
|
if (typeof this.client.session?.promptAsync === "function") {
|
|
598
1040
|
await this.runPromptRequestWithTimeout({
|
|
@@ -601,6 +1043,7 @@ var OpenCodeClient = class {
|
|
|
601
1043
|
timeoutMs: this.promptTimeoutPolicy.waitTimeoutMs
|
|
602
1044
|
}, async (signal) => {
|
|
603
1045
|
await this.callScopedSdkMethod("session", "promptAsync", {
|
|
1046
|
+
legacyParameters: legacyRequestParameters,
|
|
604
1047
|
parameters: requestParameters,
|
|
605
1048
|
signal
|
|
606
1049
|
});
|
|
@@ -618,10 +1061,7 @@ var OpenCodeClient = class {
|
|
|
618
1061
|
throw new Error("OpenCode SDK client does not expose session.promptAsync().");
|
|
619
1062
|
}
|
|
620
1063
|
async loadSessionStatuses(signal) {
|
|
621
|
-
return this.callScopedSdkMethod("session", "status", {
|
|
622
|
-
signal,
|
|
623
|
-
parameters: void 0
|
|
624
|
-
});
|
|
1064
|
+
return this.callScopedSdkMethod("session", "status", { signal });
|
|
625
1065
|
}
|
|
626
1066
|
async callRawSdkGet(url, signal) {
|
|
627
1067
|
const rawClient = getRawSdkRequestClient(this.client);
|
|
@@ -698,12 +1138,16 @@ var OpenCodeClient = class {
|
|
|
698
1138
|
logPromptRequest(level, extra, message) {
|
|
699
1139
|
const log = this.client.app?.log;
|
|
700
1140
|
if (typeof log !== "function") return;
|
|
701
|
-
|
|
1141
|
+
const payload = {
|
|
702
1142
|
service: PROMPT_LOG_SERVICE,
|
|
703
1143
|
level,
|
|
704
1144
|
message,
|
|
705
1145
|
extra
|
|
706
|
-
}
|
|
1146
|
+
};
|
|
1147
|
+
(log.length >= 2 ? log.call(this.client.app, payload, SDK_OPTIONS) : log.call(this.client.app, {
|
|
1148
|
+
body: payload,
|
|
1149
|
+
...SDK_OPTIONS
|
|
1150
|
+
})).catch(() => void 0);
|
|
707
1151
|
}
|
|
708
1152
|
};
|
|
709
1153
|
function createOpenCodeClientFromSdkClient(client, fetchFn = fetch, promptTimeoutPolicy = {}) {
|
|
@@ -825,7 +1269,7 @@ function isPromptResponseUsable(data, structured) {
|
|
|
825
1269
|
}
|
|
826
1270
|
function normalizePromptResponse(response) {
|
|
827
1271
|
return {
|
|
828
|
-
info: isPlainRecord(response?.info) ? response.info : null,
|
|
1272
|
+
info: isPlainRecord$1(response?.info) ? response.info : null,
|
|
829
1273
|
parts: normalizePromptParts(response?.parts)
|
|
830
1274
|
};
|
|
831
1275
|
}
|
|
@@ -849,7 +1293,7 @@ function toAssistantMessage(message) {
|
|
|
849
1293
|
if ("mode" in message && typeof message.mode === "string" && message.mode.trim().length > 0) normalized.mode = message.mode;
|
|
850
1294
|
if ("modelID" in message && typeof message.modelID === "string" && message.modelID.trim().length > 0) normalized.modelID = message.modelID;
|
|
851
1295
|
if ("parentID" in message && typeof message.parentID === "string" && message.parentID.trim().length > 0) normalized.parentID = message.parentID;
|
|
852
|
-
if ("path" in message && isPlainRecord(message.path)) normalized.path = {
|
|
1296
|
+
if ("path" in message && isPlainRecord$1(message.path)) normalized.path = {
|
|
853
1297
|
...typeof message.path.cwd === "string" && message.path.cwd.trim().length > 0 ? { cwd: message.path.cwd } : {},
|
|
854
1298
|
...typeof message.path.root === "string" && message.path.root.trim().length > 0 ? { root: message.path.root } : {}
|
|
855
1299
|
};
|
|
@@ -859,16 +1303,16 @@ function toAssistantMessage(message) {
|
|
|
859
1303
|
const structuredPayload = extractStructuredPayload(message);
|
|
860
1304
|
if (structuredPayload !== null) normalized.structured = structuredPayload;
|
|
861
1305
|
if ("summary" in message && typeof message.summary === "boolean") normalized.summary = message.summary;
|
|
862
|
-
if ("time" in message && isPlainRecord(message.time)) normalized.time = {
|
|
1306
|
+
if ("time" in message && isPlainRecord$1(message.time)) normalized.time = {
|
|
863
1307
|
...typeof message.time.created === "number" && Number.isFinite(message.time.created) ? { created: message.time.created } : {},
|
|
864
1308
|
...typeof message.time.completed === "number" && Number.isFinite(message.time.completed) ? { completed: message.time.completed } : {}
|
|
865
1309
|
};
|
|
866
|
-
if ("tokens" in message && isPlainRecord(message.tokens)) normalized.tokens = {
|
|
1310
|
+
if ("tokens" in message && isPlainRecord$1(message.tokens)) normalized.tokens = {
|
|
867
1311
|
...typeof message.tokens.input === "number" && Number.isFinite(message.tokens.input) ? { input: message.tokens.input } : {},
|
|
868
1312
|
...typeof message.tokens.output === "number" && Number.isFinite(message.tokens.output) ? { output: message.tokens.output } : {},
|
|
869
1313
|
...typeof message.tokens.reasoning === "number" && Number.isFinite(message.tokens.reasoning) ? { reasoning: message.tokens.reasoning } : {},
|
|
870
1314
|
...typeof message.tokens.total === "number" && Number.isFinite(message.tokens.total) ? { total: message.tokens.total } : {},
|
|
871
|
-
...isPlainRecord(message.tokens.cache) ? { cache: {
|
|
1315
|
+
...isPlainRecord$1(message.tokens.cache) ? { cache: {
|
|
872
1316
|
...typeof message.tokens.cache.read === "number" && Number.isFinite(message.tokens.cache.read) ? { read: message.tokens.cache.read } : {},
|
|
873
1317
|
...typeof message.tokens.cache.write === "number" && Number.isFinite(message.tokens.cache.write) ? { write: message.tokens.cache.write } : {}
|
|
874
1318
|
} } : {}
|
|
@@ -877,7 +1321,7 @@ function toAssistantMessage(message) {
|
|
|
877
1321
|
return normalized;
|
|
878
1322
|
}
|
|
879
1323
|
function extractMessageId(message) {
|
|
880
|
-
if (!isPlainRecord(message)) return null;
|
|
1324
|
+
if (!isPlainRecord$1(message)) return null;
|
|
881
1325
|
return typeof message.id === "string" && message.id.trim().length > 0 ? message.id : null;
|
|
882
1326
|
}
|
|
883
1327
|
function delay(ms, signal) {
|
|
@@ -973,6 +1417,27 @@ function unwrapSdkData(response) {
|
|
|
973
1417
|
if (response && typeof response === "object" && "data" in response) return response.data;
|
|
974
1418
|
return response;
|
|
975
1419
|
}
|
|
1420
|
+
function normalizePermissionRequest(permission) {
|
|
1421
|
+
if (!isPlainRecord$1(permission)) return null;
|
|
1422
|
+
const id = typeof permission.id === "string" && permission.id.trim().length > 0 ? permission.id : null;
|
|
1423
|
+
const sessionID = typeof permission.sessionID === "string" && permission.sessionID.trim().length > 0 ? permission.sessionID : null;
|
|
1424
|
+
const permissionName = typeof permission.permission === "string" && permission.permission.trim().length > 0 ? permission.permission : typeof permission.type === "string" && permission.type.trim().length > 0 ? permission.type : null;
|
|
1425
|
+
if (!id || !sessionID || !permissionName) return null;
|
|
1426
|
+
return {
|
|
1427
|
+
always: Array.isArray(permission.always) ? permission.always.filter((value) => typeof value === "string") : [],
|
|
1428
|
+
id,
|
|
1429
|
+
metadata: isPlainRecord$1(permission.metadata) ? permission.metadata : {},
|
|
1430
|
+
patterns: normalizePermissionPatterns$1(permission),
|
|
1431
|
+
permission: permissionName,
|
|
1432
|
+
sessionID
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function normalizePermissionPatterns$1(permission) {
|
|
1436
|
+
if (Array.isArray(permission.patterns)) return permission.patterns.filter((value) => typeof value === "string");
|
|
1437
|
+
if (typeof permission.pattern === "string" && permission.pattern.trim().length > 0) return [permission.pattern];
|
|
1438
|
+
if (Array.isArray(permission.pattern)) return permission.pattern.filter((value) => typeof value === "string");
|
|
1439
|
+
return [];
|
|
1440
|
+
}
|
|
976
1441
|
function getRawSdkRequestClient(client) {
|
|
977
1442
|
const compatibleClient = client;
|
|
978
1443
|
return compatibleClient.client ?? compatibleClient._client ?? null;
|
|
@@ -988,11 +1453,11 @@ function resolvePromptTimeoutPolicy(input) {
|
|
|
988
1453
|
};
|
|
989
1454
|
}
|
|
990
1455
|
function normalizeAssistantError(value) {
|
|
991
|
-
if (!isPlainRecord(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
|
|
1456
|
+
if (!isPlainRecord$1(value) || typeof value.name !== "string" || value.name.trim().length === 0) return;
|
|
992
1457
|
return {
|
|
993
1458
|
...value,
|
|
994
1459
|
name: value.name,
|
|
995
|
-
...isPlainRecord(value.data) ? { data: value.data } : {}
|
|
1460
|
+
...isPlainRecord$1(value.data) ? { data: value.data } : {}
|
|
996
1461
|
};
|
|
997
1462
|
}
|
|
998
1463
|
function isAssistantMessageCompleted(message) {
|
|
@@ -1005,7 +1470,7 @@ function isCompletedEmptyPromptResponse(data, structured) {
|
|
|
1005
1470
|
return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText && !bodyMd;
|
|
1006
1471
|
}
|
|
1007
1472
|
function extractStructuredPayload(message) {
|
|
1008
|
-
if (!isPlainRecord(message)) return null;
|
|
1473
|
+
if (!isPlainRecord$1(message)) return null;
|
|
1009
1474
|
if ("structured" in message && message.structured !== void 0) return message.structured;
|
|
1010
1475
|
if ("structured_output" in message && message.structured_output !== void 0) return message.structured_output;
|
|
1011
1476
|
return null;
|
|
@@ -1079,7 +1544,7 @@ function throwIfAborted(signal) {
|
|
|
1079
1544
|
if (!signal?.aborted) return;
|
|
1080
1545
|
throw normalizeAbortReason(signal.reason);
|
|
1081
1546
|
}
|
|
1082
|
-
function isPlainRecord(value) {
|
|
1547
|
+
function isPlainRecord$1(value) {
|
|
1083
1548
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1084
1549
|
}
|
|
1085
1550
|
async function resolveProviderAvailability(config, fetchFn) {
|
|
@@ -1872,10 +2337,11 @@ var SendPromptUseCase = class {
|
|
|
1872
2337
|
binding
|
|
1873
2338
|
});
|
|
1874
2339
|
binding = createdSession.binding;
|
|
1875
|
-
this.logger
|
|
2340
|
+
logPromptLifecycle(this.logger, {
|
|
1876
2341
|
chatId: input.chatId,
|
|
1877
|
-
|
|
2342
|
+
event: "prompt.session.created",
|
|
1878
2343
|
projectId: createdSession.session.projectID,
|
|
2344
|
+
sessionId: createdSession.session.id,
|
|
1879
2345
|
directory: createdSession.session.directory
|
|
1880
2346
|
}, "session created");
|
|
1881
2347
|
}
|
|
@@ -1883,11 +2349,15 @@ var SendPromptUseCase = class {
|
|
|
1883
2349
|
const selectedModel = (await this.opencodeClient.listModels()).find((model) => model.providerID === binding?.modelProviderId && model.id === binding?.modelId);
|
|
1884
2350
|
if (!selectedModel) {
|
|
1885
2351
|
binding = await clearStoredModelSelection(this.sessionRepo, binding);
|
|
1886
|
-
this.logger.warn?.({
|
|
2352
|
+
this.logger.warn?.({
|
|
2353
|
+
chatId: input.chatId,
|
|
2354
|
+
event: "prompt.model.unavailable"
|
|
2355
|
+
}, "selected model is no longer available, falling back to OpenCode default");
|
|
1887
2356
|
} else if (binding.modelVariant && !(binding.modelVariant in selectedModel.variants)) {
|
|
1888
2357
|
binding = await clearStoredModelVariant(this.sessionRepo, binding);
|
|
1889
2358
|
this.logger.warn?.({
|
|
1890
2359
|
chatId: input.chatId,
|
|
2360
|
+
event: "prompt.model.variant_unavailable",
|
|
1891
2361
|
providerId: selectedModel.providerID,
|
|
1892
2362
|
modelId: selectedModel.id
|
|
1893
2363
|
}, "selected model variant is no longer available, falling back to default variant");
|
|
@@ -1903,13 +2373,24 @@ var SendPromptUseCase = class {
|
|
|
1903
2373
|
const selectedAgent = resolveSelectedAgent(await this.opencodeClient.listAgents(), activeBinding.agentName);
|
|
1904
2374
|
if (activeBinding.agentName && selectedAgent?.name !== activeBinding.agentName) {
|
|
1905
2375
|
activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
|
|
1906
|
-
this.logger.warn?.({
|
|
2376
|
+
this.logger.warn?.({
|
|
2377
|
+
chatId: input.chatId,
|
|
2378
|
+
event: "prompt.agent.unavailable"
|
|
2379
|
+
}, "selected agent is no longer available, falling back to OpenCode default");
|
|
1907
2380
|
}
|
|
1908
2381
|
const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
|
|
1909
2382
|
const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
|
|
1910
2383
|
input.onExecutionSession?.(executionSessionId);
|
|
1911
2384
|
let result;
|
|
1912
2385
|
try {
|
|
2386
|
+
logOpenCodeRequest(this.logger, {
|
|
2387
|
+
chatId: input.chatId,
|
|
2388
|
+
event: "opencode.prompt.submit",
|
|
2389
|
+
projectId: activeBinding.projectId,
|
|
2390
|
+
sessionId: executionSessionId,
|
|
2391
|
+
fileCount: files.length,
|
|
2392
|
+
status: "started"
|
|
2393
|
+
}, "submitting OpenCode prompt");
|
|
1913
2394
|
result = await this.opencodeClient.promptSession({
|
|
1914
2395
|
sessionId: executionSessionId,
|
|
1915
2396
|
prompt: promptText,
|
|
@@ -1920,6 +2401,13 @@ var SendPromptUseCase = class {
|
|
|
1920
2401
|
...input.signal ? { signal: input.signal } : {},
|
|
1921
2402
|
...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
|
|
1922
2403
|
});
|
|
2404
|
+
logPromptLifecycle(this.logger, {
|
|
2405
|
+
chatId: input.chatId,
|
|
2406
|
+
event: "prompt.completed",
|
|
2407
|
+
projectId: activeBinding.projectId,
|
|
2408
|
+
sessionId: executionSessionId,
|
|
2409
|
+
status: "completed"
|
|
2410
|
+
}, "prompt completed");
|
|
1923
2411
|
} finally {
|
|
1924
2412
|
if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
|
|
1925
2413
|
}
|
|
@@ -1932,14 +2420,18 @@ var SendPromptUseCase = class {
|
|
|
1932
2420
|
}
|
|
1933
2421
|
async clearInvalidSessionContext(chatId, binding, reason) {
|
|
1934
2422
|
const nextBinding = await clearStoredSessionContext(this.sessionRepo, binding);
|
|
1935
|
-
this.logger.warn?.({
|
|
2423
|
+
this.logger.warn?.({
|
|
2424
|
+
chatId,
|
|
2425
|
+
event: "prompt.session.invalid_context"
|
|
2426
|
+
}, `${reason}, falling back to the current OpenCode project`);
|
|
1936
2427
|
return nextBinding;
|
|
1937
2428
|
}
|
|
1938
2429
|
async createTemporaryImageSession(chatId, sessionId) {
|
|
1939
2430
|
const temporarySession = await this.opencodeClient.forkSession(sessionId);
|
|
1940
2431
|
if (!temporarySession.id || temporarySession.id === sessionId) throw new Error("OpenCode did not return a distinct temporary session for the image turn.");
|
|
1941
|
-
this.logger
|
|
2432
|
+
logPromptLifecycle(this.logger, {
|
|
1942
2433
|
chatId,
|
|
2434
|
+
event: "prompt.temporary_session.created",
|
|
1943
2435
|
parentSessionId: sessionId,
|
|
1944
2436
|
sessionId: temporarySession.id
|
|
1945
2437
|
}, "created temporary image session");
|
|
@@ -1949,6 +2441,7 @@ var SendPromptUseCase = class {
|
|
|
1949
2441
|
try {
|
|
1950
2442
|
if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
|
|
1951
2443
|
chatId,
|
|
2444
|
+
event: "prompt.temporary_session.cleanup_failed",
|
|
1952
2445
|
parentSessionId,
|
|
1953
2446
|
sessionId
|
|
1954
2447
|
}, "failed to delete temporary image session");
|
|
@@ -1956,6 +2449,7 @@ var SendPromptUseCase = class {
|
|
|
1956
2449
|
this.logger.warn?.({
|
|
1957
2450
|
error,
|
|
1958
2451
|
chatId,
|
|
2452
|
+
event: "prompt.temporary_session.cleanup_failed",
|
|
1959
2453
|
parentSessionId,
|
|
1960
2454
|
sessionId
|
|
1961
2455
|
}, "failed to delete temporary image session");
|
|
@@ -2156,7 +2650,23 @@ function resolveExtension(mimeType) {
|
|
|
2156
2650
|
//#endregion
|
|
2157
2651
|
//#region src/app/container.ts
|
|
2158
2652
|
function createAppContainer(config, client) {
|
|
2159
|
-
const
|
|
2653
|
+
const runtimeId = randomUUID();
|
|
2654
|
+
const logger = createOpenCodeAppLogger(client, {
|
|
2655
|
+
file: {
|
|
2656
|
+
dir: config.loggingFileDir,
|
|
2657
|
+
retention: {
|
|
2658
|
+
maxFiles: config.loggingRetentionMaxFiles,
|
|
2659
|
+
maxTotalBytes: config.loggingRetentionMaxTotalBytes
|
|
2660
|
+
}
|
|
2661
|
+
},
|
|
2662
|
+
level: config.loggingLevel,
|
|
2663
|
+
runtimeId,
|
|
2664
|
+
sinks: {
|
|
2665
|
+
file: config.loggingFileSinkEnabled,
|
|
2666
|
+
host: config.loggingHostSinkEnabled
|
|
2667
|
+
},
|
|
2668
|
+
worktree: config.worktreePath
|
|
2669
|
+
});
|
|
2160
2670
|
return createContainer(config, createOpenCodeClientFromSdkClient(client, fetch, {
|
|
2161
2671
|
waitTimeoutMs: config.promptWaitTimeoutMs,
|
|
2162
2672
|
pollRequestTimeoutMs: config.promptPollRequestTimeoutMs,
|
|
@@ -2164,6 +2674,9 @@ function createAppContainer(config, client) {
|
|
|
2164
2674
|
}), logger);
|
|
2165
2675
|
}
|
|
2166
2676
|
function createContainer(config, opencodeClient, logger) {
|
|
2677
|
+
const storageLogger = logger.child({ component: "storage" });
|
|
2678
|
+
const opencodeLogger = logger.child({ component: "opencode" });
|
|
2679
|
+
const promptLogger = logger.child({ component: "prompt" });
|
|
2167
2680
|
const stateStore = new JsonStateStore({
|
|
2168
2681
|
filePath: config.stateFilePath,
|
|
2169
2682
|
createDefaultState: createDefaultOpencodeTbotState
|
|
@@ -2178,7 +2691,7 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2178
2691
|
});
|
|
2179
2692
|
const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
|
|
2180
2693
|
const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
|
|
2181
|
-
const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient,
|
|
2694
|
+
const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2182
2695
|
const getHealthUseCase = new GetHealthUseCase(opencodeClient);
|
|
2183
2696
|
const getPathUseCase = new GetPathUseCase(opencodeClient);
|
|
2184
2697
|
const listAgentsUseCase = new ListAgentsUseCase(sessionRepo, opencodeClient);
|
|
@@ -2187,11 +2700,11 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2187
2700
|
const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
|
|
2188
2701
|
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
|
|
2189
2702
|
const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
|
|
2190
|
-
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient,
|
|
2191
|
-
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient,
|
|
2192
|
-
const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient,
|
|
2193
|
-
const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient,
|
|
2194
|
-
const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient,
|
|
2703
|
+
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2704
|
+
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, promptLogger);
|
|
2705
|
+
const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2706
|
+
const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2707
|
+
const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2195
2708
|
let disposed = false;
|
|
2196
2709
|
return {
|
|
2197
2710
|
abortPromptUseCase,
|
|
@@ -2220,7 +2733,10 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2220
2733
|
async dispose() {
|
|
2221
2734
|
if (disposed) return;
|
|
2222
2735
|
disposed = true;
|
|
2223
|
-
|
|
2736
|
+
storageLogger.info({
|
|
2737
|
+
event: "storage.container.disposed",
|
|
2738
|
+
filePath: config.stateFilePath
|
|
2739
|
+
}, "disposing telegram bot container");
|
|
2224
2740
|
await logger.flush();
|
|
2225
2741
|
}
|
|
2226
2742
|
};
|
|
@@ -2295,25 +2811,40 @@ function escapeMarkdownV2(value) {
|
|
|
2295
2811
|
async function handleTelegramBotPluginEvent(runtime, event) {
|
|
2296
2812
|
switch (event.type) {
|
|
2297
2813
|
case "permission.asked":
|
|
2298
|
-
|
|
2814
|
+
case "permission.updated": {
|
|
2815
|
+
const request = normalizePermissionRequestEvent(event.properties);
|
|
2816
|
+
if (request) await handlePermissionAsked(runtime, request);
|
|
2299
2817
|
return;
|
|
2300
|
-
|
|
2301
|
-
|
|
2818
|
+
}
|
|
2819
|
+
case "permission.replied": {
|
|
2820
|
+
const replyEvent = normalizePermissionReplyEvent(event.properties);
|
|
2821
|
+
if (replyEvent) await handlePermissionReplied(runtime, replyEvent);
|
|
2302
2822
|
return;
|
|
2303
|
-
|
|
2304
|
-
|
|
2823
|
+
}
|
|
2824
|
+
case "session.error": {
|
|
2825
|
+
const sessionError = normalizeSessionErrorEvent(event.properties);
|
|
2826
|
+
if (sessionError) await handleSessionError(runtime, sessionError);
|
|
2305
2827
|
return;
|
|
2306
|
-
|
|
2307
|
-
|
|
2828
|
+
}
|
|
2829
|
+
case "session.idle": {
|
|
2830
|
+
const sessionIdle = normalizeSessionIdleEvent(event.properties);
|
|
2831
|
+
if (sessionIdle) await handleSessionIdle(runtime, sessionIdle);
|
|
2308
2832
|
return;
|
|
2309
|
-
|
|
2310
|
-
|
|
2833
|
+
}
|
|
2834
|
+
case "session.status": {
|
|
2835
|
+
const sessionStatus = normalizeSessionStatusEvent(event.properties);
|
|
2836
|
+
if (sessionStatus) await handleSessionStatus(runtime, sessionStatus);
|
|
2311
2837
|
return;
|
|
2838
|
+
}
|
|
2312
2839
|
default: return;
|
|
2313
2840
|
}
|
|
2314
2841
|
}
|
|
2315
|
-
async function handlePermissionAsked(runtime,
|
|
2316
|
-
const
|
|
2842
|
+
async function handlePermissionAsked(runtime, request) {
|
|
2843
|
+
const logger = runtime.container.logger.child({
|
|
2844
|
+
component: "plugin-event",
|
|
2845
|
+
requestId: request.id,
|
|
2846
|
+
sessionId: request.sessionID
|
|
2847
|
+
});
|
|
2317
2848
|
const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
|
|
2318
2849
|
const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
|
|
2319
2850
|
const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
|
|
@@ -2334,73 +2865,81 @@ async function handlePermissionAsked(runtime, event) {
|
|
|
2334
2865
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2335
2866
|
});
|
|
2336
2867
|
} catch (error) {
|
|
2337
|
-
|
|
2868
|
+
logger.error({
|
|
2338
2869
|
error,
|
|
2339
2870
|
chatId,
|
|
2871
|
+
event: "plugin-event.permission.ask.delivery_failed",
|
|
2340
2872
|
requestId: request.id
|
|
2341
2873
|
}, "failed to deliver permission request to Telegram");
|
|
2342
2874
|
}
|
|
2343
2875
|
}
|
|
2344
2876
|
}
|
|
2345
2877
|
async function handlePermissionReplied(runtime, event) {
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
|
|
2878
|
+
const logger = runtime.container.logger.child({
|
|
2879
|
+
component: "plugin-event",
|
|
2880
|
+
event: "plugin-event.permission.replied",
|
|
2881
|
+
requestId: event.requestId,
|
|
2882
|
+
sessionId: event.sessionId
|
|
2883
|
+
});
|
|
2884
|
+
const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(event.requestId);
|
|
2349
2885
|
await Promise.all(approvals.map(async (approval) => {
|
|
2350
2886
|
try {
|
|
2351
|
-
await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(requestId, reply));
|
|
2887
|
+
await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(event.requestId, event.reply));
|
|
2352
2888
|
} catch (error) {
|
|
2353
|
-
|
|
2889
|
+
logger.warn({
|
|
2354
2890
|
error,
|
|
2355
2891
|
chatId: approval.chatId,
|
|
2356
|
-
|
|
2892
|
+
event: "plugin-event.permission.reply_message_failed"
|
|
2357
2893
|
}, "failed to update Telegram permission message");
|
|
2358
2894
|
}
|
|
2359
|
-
await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, reply));
|
|
2895
|
+
await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, event.reply));
|
|
2360
2896
|
}));
|
|
2361
2897
|
}
|
|
2362
2898
|
async function handleSessionError(runtime, event) {
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
error,
|
|
2372
|
-
sessionId
|
|
2899
|
+
const logger = runtime.container.logger.child({
|
|
2900
|
+
component: "plugin-event",
|
|
2901
|
+
sessionId: event.sessionId
|
|
2902
|
+
});
|
|
2903
|
+
if (runtime.container.foregroundSessionTracker.fail(event.sessionId, event.error instanceof Error ? event.error : /* @__PURE__ */ new Error("Unknown session error."))) {
|
|
2904
|
+
logger.warn({
|
|
2905
|
+
error: event.error,
|
|
2906
|
+
event: "plugin-event.session.error.foreground_suppressed"
|
|
2373
2907
|
}, "session error suppressed for foreground Telegram session");
|
|
2374
2908
|
return;
|
|
2375
2909
|
}
|
|
2376
|
-
|
|
2910
|
+
const message = extractSessionErrorMessage(event.error) ?? "Unknown session error.";
|
|
2911
|
+
await notifyBoundChats(runtime, event.sessionId, `Session failed.\n\nSession: ${event.sessionId}\nError: ${message}`);
|
|
2377
2912
|
}
|
|
2378
2913
|
async function handleSessionIdle(runtime, event) {
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2914
|
+
const logger = runtime.container.logger.child({
|
|
2915
|
+
component: "plugin-event",
|
|
2916
|
+
sessionId: event.sessionId
|
|
2917
|
+
});
|
|
2918
|
+
if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
|
|
2919
|
+
logPluginEvent(logger, { event: "plugin-event.session.idle.foreground_suppressed" }, "session idle notification suppressed for foreground Telegram session");
|
|
2382
2920
|
return;
|
|
2383
2921
|
}
|
|
2384
|
-
await notifyBoundChats(runtime, sessionId, `Session finished.\n\nSession: ${sessionId}`);
|
|
2922
|
+
await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
|
|
2385
2923
|
}
|
|
2386
2924
|
async function handleSessionStatus(runtime, event) {
|
|
2387
|
-
if (event.
|
|
2388
|
-
await handleSessionIdle(runtime,
|
|
2389
|
-
type: "session.idle",
|
|
2390
|
-
properties: { sessionID: event.properties.sessionID }
|
|
2391
|
-
});
|
|
2925
|
+
if (event.statusType !== "idle") return;
|
|
2926
|
+
await handleSessionIdle(runtime, event);
|
|
2392
2927
|
}
|
|
2393
2928
|
async function notifyBoundChats(runtime, sessionId, text) {
|
|
2929
|
+
const logger = runtime.container.logger.child({
|
|
2930
|
+
component: "plugin-event",
|
|
2931
|
+
sessionId
|
|
2932
|
+
});
|
|
2394
2933
|
const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
|
|
2395
2934
|
const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
|
|
2396
2935
|
await Promise.all(chatIds.map(async (chatId) => {
|
|
2397
2936
|
try {
|
|
2398
2937
|
await runtime.bot.api.sendMessage(chatId, text);
|
|
2399
2938
|
} catch (error) {
|
|
2400
|
-
|
|
2939
|
+
logger.warn({
|
|
2401
2940
|
error,
|
|
2402
2941
|
chatId,
|
|
2403
|
-
|
|
2942
|
+
event: "plugin-event.session.notify_failed"
|
|
2404
2943
|
}, "failed to notify Telegram chat about session event");
|
|
2405
2944
|
}
|
|
2406
2945
|
}));
|
|
@@ -2412,6 +2951,79 @@ function toResolvedApproval(approval, reply) {
|
|
|
2412
2951
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2413
2952
|
};
|
|
2414
2953
|
}
|
|
2954
|
+
function normalizePermissionRequestEvent(properties) {
|
|
2955
|
+
if (!isPlainRecord(properties)) return null;
|
|
2956
|
+
const id = asNonEmptyString(properties.id);
|
|
2957
|
+
const sessionID = asNonEmptyString(properties.sessionID);
|
|
2958
|
+
const permission = asNonEmptyString(properties.permission) ?? asNonEmptyString(properties.type);
|
|
2959
|
+
if (!id || !sessionID || !permission) return null;
|
|
2960
|
+
return {
|
|
2961
|
+
always: Array.isArray(properties.always) ? properties.always.filter((value) => typeof value === "string") : [],
|
|
2962
|
+
id,
|
|
2963
|
+
metadata: isPlainRecord(properties.metadata) ? properties.metadata : {},
|
|
2964
|
+
patterns: normalizePermissionPatterns(properties),
|
|
2965
|
+
permission,
|
|
2966
|
+
sessionID
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
function normalizePermissionReplyEvent(properties) {
|
|
2970
|
+
if (!isPlainRecord(properties)) return null;
|
|
2971
|
+
const requestId = asNonEmptyString(properties.requestID) ?? asNonEmptyString(properties.permissionID);
|
|
2972
|
+
const reply = normalizePermissionReply(asNonEmptyString(properties.reply) ?? asNonEmptyString(properties.response));
|
|
2973
|
+
const sessionId = asNonEmptyString(properties.sessionID);
|
|
2974
|
+
if (!requestId || !reply || !sessionId) return null;
|
|
2975
|
+
return {
|
|
2976
|
+
reply,
|
|
2977
|
+
requestId,
|
|
2978
|
+
sessionId
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
function normalizeSessionErrorEvent(properties) {
|
|
2982
|
+
if (!isPlainRecord(properties)) return null;
|
|
2983
|
+
const sessionId = asNonEmptyString(properties.sessionID);
|
|
2984
|
+
if (!sessionId) return null;
|
|
2985
|
+
return {
|
|
2986
|
+
error: properties.error,
|
|
2987
|
+
sessionId
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
function normalizeSessionIdleEvent(properties) {
|
|
2991
|
+
if (!isPlainRecord(properties)) return null;
|
|
2992
|
+
const sessionId = asNonEmptyString(properties.sessionID);
|
|
2993
|
+
return sessionId ? { sessionId } : null;
|
|
2994
|
+
}
|
|
2995
|
+
function normalizeSessionStatusEvent(properties) {
|
|
2996
|
+
if (!isPlainRecord(properties) || !isPlainRecord(properties.status)) return null;
|
|
2997
|
+
const sessionId = asNonEmptyString(properties.sessionID);
|
|
2998
|
+
const statusType = asNonEmptyString(properties.status.type);
|
|
2999
|
+
if (!sessionId || !statusType) return null;
|
|
3000
|
+
return {
|
|
3001
|
+
sessionId,
|
|
3002
|
+
statusType
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
function normalizePermissionPatterns(properties) {
|
|
3006
|
+
if (Array.isArray(properties.patterns)) return properties.patterns.filter((value) => typeof value === "string");
|
|
3007
|
+
if (typeof properties.pattern === "string" && properties.pattern.trim().length > 0) return [properties.pattern];
|
|
3008
|
+
if (Array.isArray(properties.pattern)) return properties.pattern.filter((value) => typeof value === "string");
|
|
3009
|
+
return [];
|
|
3010
|
+
}
|
|
3011
|
+
function normalizePermissionReply(value) {
|
|
3012
|
+
if (value === "once" || value === "always" || value === "reject") return value;
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
function extractSessionErrorMessage(error) {
|
|
3016
|
+
if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
|
|
3017
|
+
if (!isPlainRecord(error)) return null;
|
|
3018
|
+
if (isPlainRecord(error.data) && typeof error.data.message === "string" && error.data.message.trim().length > 0) return error.data.message.trim();
|
|
3019
|
+
return asNonEmptyString(error.name);
|
|
3020
|
+
}
|
|
3021
|
+
function asNonEmptyString(value) {
|
|
3022
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
3023
|
+
}
|
|
3024
|
+
function isPlainRecord(value) {
|
|
3025
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3026
|
+
}
|
|
2415
3027
|
var SUPPORTED_BOT_LANGUAGES = [
|
|
2416
3028
|
"en",
|
|
2417
3029
|
"zh-CN",
|
|
@@ -3104,7 +3716,9 @@ var TELEGRAM_COMMAND_SYNC_SCOPES = [{ type: "default" }, { type: "all_private_ch
|
|
|
3104
3716
|
async function syncTelegramCommands(bot, logger) {
|
|
3105
3717
|
await Promise.all(TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => bot.api.setMyCommands(TELEGRAM_COMMANDS, { scope })));
|
|
3106
3718
|
logger.info({
|
|
3719
|
+
component: "runtime",
|
|
3107
3720
|
commands: TELEGRAM_COMMANDS.map((command) => command.command),
|
|
3721
|
+
event: "runtime.commands.synced",
|
|
3108
3722
|
scopes: TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => scope.type)
|
|
3109
3723
|
}, "telegram commands synced");
|
|
3110
3724
|
}
|
|
@@ -3115,109 +3729,42 @@ async function syncTelegramCommandsForChat(api, chatId, language) {
|
|
|
3115
3729
|
} });
|
|
3116
3730
|
}
|
|
3117
3731
|
//#endregion
|
|
3118
|
-
//#region src/bot/
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
return getBotCopy(await getChatLanguage(sessionRepo, chatId));
|
|
3125
|
-
}
|
|
3126
|
-
async function setChatLanguage(sessionRepo, chatId, language) {
|
|
3127
|
-
const binding = await sessionRepo.getByChatId(chatId);
|
|
3128
|
-
await sessionRepo.setCurrent({
|
|
3129
|
-
chatId,
|
|
3130
|
-
sessionId: binding?.sessionId ?? null,
|
|
3131
|
-
projectId: binding?.projectId ?? null,
|
|
3132
|
-
directory: binding?.directory ?? null,
|
|
3133
|
-
agentName: binding?.agentName ?? null,
|
|
3134
|
-
modelProviderId: binding?.modelProviderId ?? null,
|
|
3135
|
-
modelId: binding?.modelId ?? null,
|
|
3136
|
-
modelVariant: binding?.modelVariant ?? null,
|
|
3137
|
-
language,
|
|
3138
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
var NUMBERED_BUTTONS_PER_ROW = 5;
|
|
3142
|
-
function buildModelsKeyboard(models, requestedPage, copy = BOT_COPY) {
|
|
3143
|
-
const page = getModelsPage(models, requestedPage);
|
|
3144
|
-
const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `model:pick:${page.startIndex + index + 1}`);
|
|
3145
|
-
appendPaginationButtons(keyboard, page.page, page.totalPages, "model:page", copy);
|
|
3732
|
+
//#region src/bot/logger-context.ts
|
|
3733
|
+
function buildTelegramLoggerContext(ctx, component = "telegram") {
|
|
3734
|
+
const updateId = typeof ctx.update?.update_id === "number" ? ctx.update.update_id : void 0;
|
|
3735
|
+
const command = extractTelegramCommand(resolveMessageText(ctx));
|
|
3736
|
+
const callbackData = normalizeTelegramString(ctx.callbackQuery?.data);
|
|
3737
|
+
const operationId = typeof updateId === "number" ? `telegram-${updateId}` : null;
|
|
3146
3738
|
return {
|
|
3147
|
-
|
|
3148
|
-
|
|
3739
|
+
component,
|
|
3740
|
+
...typeof ctx.chat?.id === "number" ? { chatId: ctx.chat.id } : {},
|
|
3741
|
+
...typeof updateId === "number" ? { updateId } : {},
|
|
3742
|
+
...command ? { command } : {},
|
|
3743
|
+
...callbackData ? { callbackData } : {},
|
|
3744
|
+
correlationId: typeof updateId === "number" ? String(updateId) : operationId,
|
|
3745
|
+
operationId
|
|
3149
3746
|
};
|
|
3150
3747
|
}
|
|
3151
|
-
function
|
|
3152
|
-
|
|
3153
|
-
const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `agents:select:${page.startIndex + index + 1}`);
|
|
3154
|
-
appendPaginationButtons(keyboard, page.page, page.totalPages, "agents:page", copy);
|
|
3155
|
-
return {
|
|
3156
|
-
keyboard,
|
|
3157
|
-
page
|
|
3158
|
-
};
|
|
3748
|
+
function scopeLoggerToTelegramContext(logger, ctx, component = "telegram") {
|
|
3749
|
+
return logger.child(buildTelegramLoggerContext(ctx, component));
|
|
3159
3750
|
}
|
|
3160
|
-
function
|
|
3161
|
-
const page = getSessionsPage(sessions, requestedPage);
|
|
3162
|
-
const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (session) => `sessions:pick:${page.page}:${session.id}`);
|
|
3163
|
-
appendPaginationButtons(keyboard, page.page, page.totalPages, "sessions:page", copy);
|
|
3751
|
+
function scopeDependenciesToTelegramContext(dependencies, ctx, component = "telegram") {
|
|
3164
3752
|
return {
|
|
3165
|
-
|
|
3166
|
-
|
|
3753
|
+
...dependencies,
|
|
3754
|
+
logger: scopeLoggerToTelegramContext(dependencies.logger, ctx, component)
|
|
3167
3755
|
};
|
|
3168
3756
|
}
|
|
3169
|
-
function
|
|
3170
|
-
return
|
|
3171
|
-
}
|
|
3172
|
-
function buildModelVariantsKeyboard(variants, modelIndex) {
|
|
3173
|
-
return buildNumberedKeyboard(variants, 0, (_, index) => `model:variant:${modelIndex}:${index + 1}`);
|
|
3174
|
-
}
|
|
3175
|
-
function buildLanguageKeyboard(currentLanguage, copy = BOT_COPY) {
|
|
3176
|
-
const keyboard = new InlineKeyboard();
|
|
3177
|
-
SUPPORTED_BOT_LANGUAGES.forEach((language, index) => {
|
|
3178
|
-
const label = currentLanguage === language ? `[${getLanguageLabel(language, copy)}]` : getLanguageLabel(language, copy);
|
|
3179
|
-
keyboard.text(label, `language:select:${language}`);
|
|
3180
|
-
if (index !== SUPPORTED_BOT_LANGUAGES.length - 1) keyboard.row();
|
|
3181
|
-
});
|
|
3182
|
-
return keyboard;
|
|
3757
|
+
function resolveMessageText(ctx) {
|
|
3758
|
+
return normalizeTelegramString(ctx.message?.text) ?? normalizeTelegramString(ctx.msg?.text);
|
|
3183
3759
|
}
|
|
3184
|
-
function
|
|
3185
|
-
|
|
3760
|
+
function extractTelegramCommand(value) {
|
|
3761
|
+
if (!value || !value.startsWith("/")) return null;
|
|
3762
|
+
const token = value.split(/\s+/u, 1)[0]?.trim();
|
|
3763
|
+
if (!token) return null;
|
|
3764
|
+
return token.replace(/^\/+/u, "").split("@", 1)[0] ?? null;
|
|
3186
3765
|
}
|
|
3187
|
-
function
|
|
3188
|
-
return
|
|
3189
|
-
}
|
|
3190
|
-
function getSessionsPage(sessions, requestedPage) {
|
|
3191
|
-
return getPagedItems(sessions, requestedPage, 10);
|
|
3192
|
-
}
|
|
3193
|
-
function buildNumberedKeyboard(items, startIndex, buildCallbackData) {
|
|
3194
|
-
const keyboard = new InlineKeyboard();
|
|
3195
|
-
items.forEach((item, index) => {
|
|
3196
|
-
const displayIndex = startIndex + index + 1;
|
|
3197
|
-
keyboard.text(`${displayIndex}`, buildCallbackData(item, index));
|
|
3198
|
-
if (index !== items.length - 1 && (index + 1) % NUMBERED_BUTTONS_PER_ROW === 0) keyboard.row();
|
|
3199
|
-
});
|
|
3200
|
-
return keyboard;
|
|
3201
|
-
}
|
|
3202
|
-
function appendPaginationButtons(keyboard, page, totalPages, prefix, copy) {
|
|
3203
|
-
if (totalPages <= 1) return;
|
|
3204
|
-
if (page > 0) keyboard.text(copy.common.previousPage, `${prefix}:${page - 1}`);
|
|
3205
|
-
if (page < totalPages - 1) keyboard.text(copy.common.nextPage, `${prefix}:${page + 1}`);
|
|
3206
|
-
}
|
|
3207
|
-
function getPagedItems(items, requestedPage, pageSize) {
|
|
3208
|
-
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
3209
|
-
const page = clampPage(requestedPage, totalPages);
|
|
3210
|
-
const startIndex = page * pageSize;
|
|
3211
|
-
return {
|
|
3212
|
-
items: items.slice(startIndex, startIndex + pageSize),
|
|
3213
|
-
page,
|
|
3214
|
-
startIndex,
|
|
3215
|
-
totalPages
|
|
3216
|
-
};
|
|
3217
|
-
}
|
|
3218
|
-
function clampPage(page, totalPages) {
|
|
3219
|
-
if (!Number.isInteger(page) || page < 0) return 0;
|
|
3220
|
-
return Math.min(page, totalPages - 1);
|
|
3766
|
+
function normalizeTelegramString(value) {
|
|
3767
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
3221
3768
|
}
|
|
3222
3769
|
//#endregion
|
|
3223
3770
|
//#region src/bot/presenters/error.presenter.ts
|
|
@@ -3334,6 +3881,179 @@ function stringifyUnknown(value) {
|
|
|
3334
3881
|
}
|
|
3335
3882
|
}
|
|
3336
3883
|
//#endregion
|
|
3884
|
+
//#region src/bot/error-boundary.ts
|
|
3885
|
+
function extractTelegramUpdateContext(ctx) {
|
|
3886
|
+
return buildTelegramLoggerContext({
|
|
3887
|
+
callbackQuery: { data: getNestedString(ctx, ["callbackQuery", "data"]) },
|
|
3888
|
+
chat: { id: getNestedNumber(ctx, ["chat", "id"]) ?? void 0 },
|
|
3889
|
+
message: { text: getNestedString(ctx, ["message", "text"]) },
|
|
3890
|
+
update: { update_id: getNestedNumber(ctx, ["update", "update_id"]) ?? void 0 }
|
|
3891
|
+
});
|
|
3892
|
+
}
|
|
3893
|
+
async function replyWithDefaultTelegramError(ctx, logger, error) {
|
|
3894
|
+
const text = presentError(error, BOT_COPY);
|
|
3895
|
+
const editMessageText = getFunction(ctx, "editMessageText");
|
|
3896
|
+
const reply = getFunction(ctx, "reply");
|
|
3897
|
+
const callbackData = getNestedString(ctx, ["callbackQuery", "data"]);
|
|
3898
|
+
try {
|
|
3899
|
+
if (typeof callbackData === "string" && editMessageText) {
|
|
3900
|
+
await editMessageText.call(ctx, text);
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
if (reply) await reply.call(ctx, text);
|
|
3904
|
+
} catch (replyError) {
|
|
3905
|
+
logger.warn?.({
|
|
3906
|
+
...extractTelegramUpdateContext(ctx),
|
|
3907
|
+
error: replyError
|
|
3908
|
+
}, "failed to deliver fallback Telegram error message");
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
function getFunction(value, key) {
|
|
3912
|
+
if (!(key in value)) return null;
|
|
3913
|
+
const candidate = value[key];
|
|
3914
|
+
return typeof candidate === "function" ? candidate : null;
|
|
3915
|
+
}
|
|
3916
|
+
function getNestedNumber(value, path) {
|
|
3917
|
+
const candidate = getNestedValue(value, path);
|
|
3918
|
+
return typeof candidate === "number" ? candidate : null;
|
|
3919
|
+
}
|
|
3920
|
+
function getNestedString(value, path) {
|
|
3921
|
+
const candidate = getNestedValue(value, path);
|
|
3922
|
+
return typeof candidate === "string" ? candidate : null;
|
|
3923
|
+
}
|
|
3924
|
+
function getNestedValue(value, path) {
|
|
3925
|
+
let current = value;
|
|
3926
|
+
for (const segment of path) {
|
|
3927
|
+
if (!current || typeof current !== "object" || !(segment in current)) return null;
|
|
3928
|
+
current = current[segment];
|
|
3929
|
+
}
|
|
3930
|
+
return current;
|
|
3931
|
+
}
|
|
3932
|
+
//#endregion
|
|
3933
|
+
//#region src/bot/i18n.ts
|
|
3934
|
+
async function getChatLanguage(sessionRepo, chatId) {
|
|
3935
|
+
if (!chatId) return "en";
|
|
3936
|
+
return normalizeBotLanguage((await sessionRepo.getByChatId(chatId))?.language);
|
|
3937
|
+
}
|
|
3938
|
+
async function getSafeChatLanguage(sessionRepo, chatId, logger) {
|
|
3939
|
+
try {
|
|
3940
|
+
return await getChatLanguage(sessionRepo, chatId);
|
|
3941
|
+
} catch (error) {
|
|
3942
|
+
logger?.warn?.({
|
|
3943
|
+
error,
|
|
3944
|
+
chatId: chatId ?? void 0
|
|
3945
|
+
}, "failed to resolve Telegram chat language; falling back to the default locale");
|
|
3946
|
+
return "en";
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
async function getSafeChatCopy(sessionRepo, chatId, logger) {
|
|
3950
|
+
try {
|
|
3951
|
+
return getBotCopy(await getSafeChatLanguage(sessionRepo, chatId, logger));
|
|
3952
|
+
} catch (error) {
|
|
3953
|
+
logger?.warn?.({
|
|
3954
|
+
error,
|
|
3955
|
+
chatId: chatId ?? void 0
|
|
3956
|
+
}, "failed to resolve Telegram copy; falling back to the default locale");
|
|
3957
|
+
return BOT_COPY;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
async function setChatLanguage(sessionRepo, chatId, language) {
|
|
3961
|
+
const binding = await sessionRepo.getByChatId(chatId);
|
|
3962
|
+
await sessionRepo.setCurrent({
|
|
3963
|
+
chatId,
|
|
3964
|
+
sessionId: binding?.sessionId ?? null,
|
|
3965
|
+
projectId: binding?.projectId ?? null,
|
|
3966
|
+
directory: binding?.directory ?? null,
|
|
3967
|
+
agentName: binding?.agentName ?? null,
|
|
3968
|
+
modelProviderId: binding?.modelProviderId ?? null,
|
|
3969
|
+
modelId: binding?.modelId ?? null,
|
|
3970
|
+
modelVariant: binding?.modelVariant ?? null,
|
|
3971
|
+
language,
|
|
3972
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
var NUMBERED_BUTTONS_PER_ROW = 5;
|
|
3976
|
+
function buildModelsKeyboard(models, requestedPage, copy = BOT_COPY) {
|
|
3977
|
+
const page = getModelsPage(models, requestedPage);
|
|
3978
|
+
const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `model:pick:${page.startIndex + index + 1}`);
|
|
3979
|
+
appendPaginationButtons(keyboard, page.page, page.totalPages, "model:page", copy);
|
|
3980
|
+
return {
|
|
3981
|
+
keyboard,
|
|
3982
|
+
page
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
function buildAgentsKeyboard(agents, requestedPage, copy = BOT_COPY) {
|
|
3986
|
+
const page = getAgentsPage(agents, requestedPage);
|
|
3987
|
+
const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (_, index) => `agents:select:${page.startIndex + index + 1}`);
|
|
3988
|
+
appendPaginationButtons(keyboard, page.page, page.totalPages, "agents:page", copy);
|
|
3989
|
+
return {
|
|
3990
|
+
keyboard,
|
|
3991
|
+
page
|
|
3992
|
+
};
|
|
3993
|
+
}
|
|
3994
|
+
function buildSessionsKeyboard(sessions, requestedPage, copy = BOT_COPY) {
|
|
3995
|
+
const page = getSessionsPage(sessions, requestedPage);
|
|
3996
|
+
const keyboard = buildNumberedKeyboard(page.items, page.startIndex, (session) => `sessions:pick:${page.page}:${session.id}`);
|
|
3997
|
+
appendPaginationButtons(keyboard, page.page, page.totalPages, "sessions:page", copy);
|
|
3998
|
+
return {
|
|
3999
|
+
keyboard,
|
|
4000
|
+
page
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
function buildSessionActionKeyboard(sessionId, page, copy = BOT_COPY) {
|
|
4004
|
+
return new InlineKeyboard().text(copy.sessions.switchAction, `sessions:switch:${page}:${sessionId}`).text(copy.sessions.renameAction, `sessions:rename:${page}:${sessionId}`).row().text(copy.sessions.backToList, `sessions:back:${page}`);
|
|
4005
|
+
}
|
|
4006
|
+
function buildModelVariantsKeyboard(variants, modelIndex) {
|
|
4007
|
+
return buildNumberedKeyboard(variants, 0, (_, index) => `model:variant:${modelIndex}:${index + 1}`);
|
|
4008
|
+
}
|
|
4009
|
+
function buildLanguageKeyboard(currentLanguage, copy = BOT_COPY) {
|
|
4010
|
+
const keyboard = new InlineKeyboard();
|
|
4011
|
+
SUPPORTED_BOT_LANGUAGES.forEach((language, index) => {
|
|
4012
|
+
const label = currentLanguage === language ? `[${getLanguageLabel(language, copy)}]` : getLanguageLabel(language, copy);
|
|
4013
|
+
keyboard.text(label, `language:select:${language}`);
|
|
4014
|
+
if (index !== SUPPORTED_BOT_LANGUAGES.length - 1) keyboard.row();
|
|
4015
|
+
});
|
|
4016
|
+
return keyboard;
|
|
4017
|
+
}
|
|
4018
|
+
function getModelsPage(models, requestedPage) {
|
|
4019
|
+
return getPagedItems(models, requestedPage, 10);
|
|
4020
|
+
}
|
|
4021
|
+
function getAgentsPage(agents, requestedPage) {
|
|
4022
|
+
return getPagedItems(agents, requestedPage, 10);
|
|
4023
|
+
}
|
|
4024
|
+
function getSessionsPage(sessions, requestedPage) {
|
|
4025
|
+
return getPagedItems(sessions, requestedPage, 10);
|
|
4026
|
+
}
|
|
4027
|
+
function buildNumberedKeyboard(items, startIndex, buildCallbackData) {
|
|
4028
|
+
const keyboard = new InlineKeyboard();
|
|
4029
|
+
items.forEach((item, index) => {
|
|
4030
|
+
const displayIndex = startIndex + index + 1;
|
|
4031
|
+
keyboard.text(`${displayIndex}`, buildCallbackData(item, index));
|
|
4032
|
+
if (index !== items.length - 1 && (index + 1) % NUMBERED_BUTTONS_PER_ROW === 0) keyboard.row();
|
|
4033
|
+
});
|
|
4034
|
+
return keyboard;
|
|
4035
|
+
}
|
|
4036
|
+
function appendPaginationButtons(keyboard, page, totalPages, prefix, copy) {
|
|
4037
|
+
if (totalPages <= 1) return;
|
|
4038
|
+
if (page > 0) keyboard.text(copy.common.previousPage, `${prefix}:${page - 1}`);
|
|
4039
|
+
if (page < totalPages - 1) keyboard.text(copy.common.nextPage, `${prefix}:${page + 1}`);
|
|
4040
|
+
}
|
|
4041
|
+
function getPagedItems(items, requestedPage, pageSize) {
|
|
4042
|
+
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
4043
|
+
const page = clampPage(requestedPage, totalPages);
|
|
4044
|
+
const startIndex = page * pageSize;
|
|
4045
|
+
return {
|
|
4046
|
+
items: items.slice(startIndex, startIndex + pageSize),
|
|
4047
|
+
page,
|
|
4048
|
+
startIndex,
|
|
4049
|
+
totalPages
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
function clampPage(page, totalPages) {
|
|
4053
|
+
if (!Number.isInteger(page) || page < 0) return 0;
|
|
4054
|
+
return Math.min(page, totalPages - 1);
|
|
4055
|
+
}
|
|
4056
|
+
//#endregion
|
|
3337
4057
|
//#region src/bot/presenters/message.presenter.ts
|
|
3338
4058
|
var VARIANT_ORDER = [
|
|
3339
4059
|
"minimal",
|
|
@@ -3713,7 +4433,7 @@ function formatSessionLabel(session) {
|
|
|
3713
4433
|
//#endregion
|
|
3714
4434
|
//#region src/bot/commands/agents.ts
|
|
3715
4435
|
async function handleAgentsCommand(ctx, dependencies) {
|
|
3716
|
-
const copy = await
|
|
4436
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3717
4437
|
try {
|
|
3718
4438
|
const result = await dependencies.listAgentsUseCase.execute({ chatId: ctx.chat.id });
|
|
3719
4439
|
if (result.agents.length === 0) {
|
|
@@ -3733,13 +4453,13 @@ async function handleAgentsCommand(ctx, dependencies) {
|
|
|
3733
4453
|
}
|
|
3734
4454
|
function registerAgentsCommand(bot, dependencies) {
|
|
3735
4455
|
bot.command(["agents", "agent"], async (ctx) => {
|
|
3736
|
-
await handleAgentsCommand(ctx, dependencies);
|
|
4456
|
+
await handleAgentsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
3737
4457
|
});
|
|
3738
4458
|
}
|
|
3739
4459
|
//#endregion
|
|
3740
4460
|
//#region src/bot/sessions-menu.ts
|
|
3741
4461
|
async function buildSessionsListView(chatId, requestedPage, dependencies) {
|
|
3742
|
-
const copy = await
|
|
4462
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
|
|
3743
4463
|
const result = await dependencies.listSessionsUseCase.execute({ chatId });
|
|
3744
4464
|
if (result.sessions.length === 0) return {
|
|
3745
4465
|
copy,
|
|
@@ -3798,14 +4518,14 @@ async function getPendingSessionRenameAction(dependencies, chatId) {
|
|
|
3798
4518
|
}
|
|
3799
4519
|
async function replyIfSessionRenamePending(ctx, dependencies) {
|
|
3800
4520
|
if (!await getPendingSessionRenameAction(dependencies, ctx.chat.id)) return false;
|
|
3801
|
-
const copy = await
|
|
4521
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3802
4522
|
await ctx.reply(copy.sessions.renamePendingInput);
|
|
3803
4523
|
return true;
|
|
3804
4524
|
}
|
|
3805
4525
|
async function handlePendingSessionRenameText(ctx, dependencies) {
|
|
3806
4526
|
const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
|
|
3807
4527
|
if (!pendingAction) return false;
|
|
3808
|
-
const copy = await
|
|
4528
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3809
4529
|
const title = ctx.message.text?.trim() ?? "";
|
|
3810
4530
|
if (title.startsWith("/")) {
|
|
3811
4531
|
await ctx.reply(copy.sessions.renamePendingInput);
|
|
@@ -3837,7 +4557,7 @@ async function handlePendingSessionRenameText(ctx, dependencies) {
|
|
|
3837
4557
|
async function cancelPendingSessionRename(ctx, dependencies) {
|
|
3838
4558
|
const pendingAction = await getPendingSessionRenameAction(dependencies, ctx.chat.id);
|
|
3839
4559
|
if (!pendingAction) return false;
|
|
3840
|
-
const copy = await
|
|
4560
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3841
4561
|
await dependencies.pendingActionRepo.clear(ctx.chat.id);
|
|
3842
4562
|
await bestEffortRestoreSessionsList(ctx.api, pendingAction, dependencies);
|
|
3843
4563
|
await ctx.reply(copy.sessions.renameCancelled);
|
|
@@ -3856,7 +4576,7 @@ function isSessionRenamePendingAction(action) {
|
|
|
3856
4576
|
//#endregion
|
|
3857
4577
|
//#region src/bot/commands/cancel.ts
|
|
3858
4578
|
async function handleCancelCommand(ctx, dependencies) {
|
|
3859
|
-
const copy = await
|
|
4579
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3860
4580
|
try {
|
|
3861
4581
|
if (await cancelPendingSessionRename(ctx, dependencies)) return;
|
|
3862
4582
|
const result = await dependencies.abortPromptUseCase.execute({ chatId: ctx.chat.id });
|
|
@@ -3876,14 +4596,14 @@ async function handleCancelCommand(ctx, dependencies) {
|
|
|
3876
4596
|
}
|
|
3877
4597
|
function registerCancelCommand(bot, dependencies) {
|
|
3878
4598
|
bot.command("cancel", async (ctx) => {
|
|
3879
|
-
await handleCancelCommand(ctx, dependencies);
|
|
4599
|
+
await handleCancelCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
3880
4600
|
});
|
|
3881
4601
|
}
|
|
3882
4602
|
//#endregion
|
|
3883
4603
|
//#region src/bot/commands/language.ts
|
|
3884
4604
|
async function handleLanguageCommand(ctx, dependencies) {
|
|
3885
|
-
const language = await
|
|
3886
|
-
const copy = await
|
|
4605
|
+
const language = await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4606
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3887
4607
|
try {
|
|
3888
4608
|
await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
|
|
3889
4609
|
await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
|
|
@@ -3893,7 +4613,7 @@ async function handleLanguageCommand(ctx, dependencies) {
|
|
|
3893
4613
|
}
|
|
3894
4614
|
}
|
|
3895
4615
|
async function switchLanguageForChat(api, chatId, language, dependencies) {
|
|
3896
|
-
const currentCopy = await
|
|
4616
|
+
const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger);
|
|
3897
4617
|
if (!isBotLanguage(language)) return {
|
|
3898
4618
|
found: false,
|
|
3899
4619
|
copy: currentCopy
|
|
@@ -3902,7 +4622,7 @@ async function switchLanguageForChat(api, chatId, language, dependencies) {
|
|
|
3902
4622
|
await syncTelegramCommandsForChat(api, chatId, language);
|
|
3903
4623
|
return {
|
|
3904
4624
|
found: true,
|
|
3905
|
-
copy: await
|
|
4625
|
+
copy: await getSafeChatCopy(dependencies.sessionRepo, chatId, dependencies.logger),
|
|
3906
4626
|
language
|
|
3907
4627
|
};
|
|
3908
4628
|
}
|
|
@@ -3912,7 +4632,7 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
|
|
|
3912
4632
|
found: false,
|
|
3913
4633
|
copy: result.copy,
|
|
3914
4634
|
text: result.copy.language.expired,
|
|
3915
|
-
keyboard: buildLanguageKeyboard(await
|
|
4635
|
+
keyboard: buildLanguageKeyboard(await getSafeChatLanguage(dependencies.sessionRepo, chatId, dependencies.logger), result.copy)
|
|
3916
4636
|
};
|
|
3917
4637
|
return {
|
|
3918
4638
|
found: true,
|
|
@@ -3923,13 +4643,13 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
|
|
|
3923
4643
|
}
|
|
3924
4644
|
function registerLanguageCommand(bot, dependencies) {
|
|
3925
4645
|
bot.command("language", async (ctx) => {
|
|
3926
|
-
await handleLanguageCommand(ctx, dependencies);
|
|
4646
|
+
await handleLanguageCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
3927
4647
|
});
|
|
3928
4648
|
}
|
|
3929
4649
|
//#endregion
|
|
3930
4650
|
//#region src/bot/commands/models.ts
|
|
3931
4651
|
async function handleModelsCommand(ctx, dependencies) {
|
|
3932
|
-
const copy = await
|
|
4652
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3933
4653
|
try {
|
|
3934
4654
|
const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
|
|
3935
4655
|
if (result.models.length === 0) {
|
|
@@ -3951,13 +4671,13 @@ async function handleModelsCommand(ctx, dependencies) {
|
|
|
3951
4671
|
}
|
|
3952
4672
|
function registerModelsCommand(bot, dependencies) {
|
|
3953
4673
|
bot.command(["model", "models"], async (ctx) => {
|
|
3954
|
-
await handleModelsCommand(ctx, dependencies);
|
|
4674
|
+
await handleModelsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
3955
4675
|
});
|
|
3956
4676
|
}
|
|
3957
4677
|
//#endregion
|
|
3958
4678
|
//#region src/bot/commands/new.ts
|
|
3959
4679
|
async function handleNewCommand(ctx, dependencies) {
|
|
3960
|
-
const copy = await
|
|
4680
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
3961
4681
|
try {
|
|
3962
4682
|
const title = extractSessionTitle(ctx);
|
|
3963
4683
|
const result = await dependencies.createSessionUseCase.execute({
|
|
@@ -3972,7 +4692,7 @@ async function handleNewCommand(ctx, dependencies) {
|
|
|
3972
4692
|
}
|
|
3973
4693
|
function registerNewCommand(bot, dependencies) {
|
|
3974
4694
|
bot.command("new", async (ctx) => {
|
|
3975
|
-
await handleNewCommand(ctx, dependencies);
|
|
4695
|
+
await handleNewCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
3976
4696
|
});
|
|
3977
4697
|
}
|
|
3978
4698
|
function extractSessionTitle(ctx) {
|
|
@@ -4321,7 +5041,7 @@ function escapeLinkDestination(url) {
|
|
|
4321
5041
|
//#endregion
|
|
4322
5042
|
//#region src/bot/commands/status.ts
|
|
4323
5043
|
async function handleStatusCommand(ctx, dependencies) {
|
|
4324
|
-
const copy = await
|
|
5044
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
|
|
4325
5045
|
try {
|
|
4326
5046
|
const result = await dependencies.getStatusUseCase.execute({ chatId: ctx.chat?.id ?? 0 });
|
|
4327
5047
|
const renderedMarkdown = renderMarkdownToTelegramMarkdownV2(presentStatusMarkdownMessage(result, copy));
|
|
@@ -4337,13 +5057,13 @@ async function handleStatusCommand(ctx, dependencies) {
|
|
|
4337
5057
|
}
|
|
4338
5058
|
function registerStatusCommand(bot, dependencies) {
|
|
4339
5059
|
bot.command("status", async (ctx) => {
|
|
4340
|
-
await handleStatusCommand(ctx, dependencies);
|
|
5060
|
+
await handleStatusCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4341
5061
|
});
|
|
4342
5062
|
}
|
|
4343
5063
|
//#endregion
|
|
4344
5064
|
//#region src/bot/commands/sessions.ts
|
|
4345
5065
|
async function handleSessionsCommand(ctx, dependencies) {
|
|
4346
|
-
const copy = await
|
|
5066
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4347
5067
|
try {
|
|
4348
5068
|
await dependencies.pendingActionRepo.clear(ctx.chat.id);
|
|
4349
5069
|
const view = await buildSessionsListView(ctx.chat.id, 0, dependencies);
|
|
@@ -4355,7 +5075,7 @@ async function handleSessionsCommand(ctx, dependencies) {
|
|
|
4355
5075
|
}
|
|
4356
5076
|
function registerSessionsCommand(bot, dependencies) {
|
|
4357
5077
|
bot.command("sessions", async (ctx) => {
|
|
4358
|
-
await handleSessionsCommand(ctx, dependencies);
|
|
5078
|
+
await handleSessionsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4359
5079
|
});
|
|
4360
5080
|
}
|
|
4361
5081
|
//#endregion
|
|
@@ -4366,7 +5086,7 @@ function presentStartMarkdownMessage(copy = BOT_COPY) {
|
|
|
4366
5086
|
//#endregion
|
|
4367
5087
|
//#region src/bot/commands/start.ts
|
|
4368
5088
|
async function handleStartCommand(ctx, dependencies) {
|
|
4369
|
-
const copy = await
|
|
5089
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat?.id, dependencies.logger);
|
|
4370
5090
|
const reply = buildTelegramStaticReply(presentStartMarkdownMessage(copy));
|
|
4371
5091
|
try {
|
|
4372
5092
|
await ctx.reply(reply.preferred.text, reply.preferred.options);
|
|
@@ -4382,7 +5102,7 @@ async function handleStartCommand(ctx, dependencies) {
|
|
|
4382
5102
|
}
|
|
4383
5103
|
function registerStartCommand(bot, dependencies) {
|
|
4384
5104
|
bot.command("start", async (ctx) => {
|
|
4385
|
-
await handleStartCommand(ctx, dependencies);
|
|
5105
|
+
await handleStartCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4386
5106
|
});
|
|
4387
5107
|
}
|
|
4388
5108
|
//#endregion
|
|
@@ -4404,7 +5124,7 @@ async function handleAgentsCallback(ctx, dependencies) {
|
|
|
4404
5124
|
if (!data.startsWith("agents:")) return;
|
|
4405
5125
|
await ctx.answerCallbackQuery();
|
|
4406
5126
|
if (!ctx.chat) return;
|
|
4407
|
-
const copy = await
|
|
5127
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4408
5128
|
try {
|
|
4409
5129
|
if (data.startsWith(AGENTS_PAGE_PREFIX)) {
|
|
4410
5130
|
const requestedPage = Number(data.slice(12));
|
|
@@ -4448,7 +5168,7 @@ async function handleModelsCallback(ctx, dependencies) {
|
|
|
4448
5168
|
if (!data.startsWith("model:")) return;
|
|
4449
5169
|
await ctx.answerCallbackQuery();
|
|
4450
5170
|
if (!ctx.chat) return;
|
|
4451
|
-
const copy = await
|
|
5171
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4452
5172
|
try {
|
|
4453
5173
|
if (data.startsWith(MODEL_PAGE_PREFIX)) {
|
|
4454
5174
|
const requestedPage = Number(data.slice(11));
|
|
@@ -4527,7 +5247,7 @@ async function handleSessionsCallback(ctx, dependencies) {
|
|
|
4527
5247
|
if (!data.startsWith("sessions:")) return;
|
|
4528
5248
|
await ctx.answerCallbackQuery();
|
|
4529
5249
|
if (!ctx.chat) return;
|
|
4530
|
-
const copy = await
|
|
5250
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4531
5251
|
try {
|
|
4532
5252
|
if (data.startsWith(SESSIONS_PAGE_PREFIX)) {
|
|
4533
5253
|
const requestedPage = Number(data.slice(14));
|
|
@@ -4608,10 +5328,10 @@ async function handleLanguageCallback(ctx, dependencies) {
|
|
|
4608
5328
|
if (!data.startsWith("language:")) return;
|
|
4609
5329
|
await ctx.answerCallbackQuery();
|
|
4610
5330
|
if (!ctx.chat || !ctx.api) return;
|
|
4611
|
-
const currentCopy = await
|
|
5331
|
+
const currentCopy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4612
5332
|
try {
|
|
4613
5333
|
if (!data.startsWith(LANGUAGE_SELECT_PREFIX)) {
|
|
4614
|
-
await ctx.editMessageText(presentLanguageMessage(await
|
|
5334
|
+
await ctx.editMessageText(presentLanguageMessage(await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger), currentCopy), { reply_markup: buildLanguageKeyboard(await getSafeChatLanguage(dependencies.sessionRepo, ctx.chat.id, dependencies.logger), currentCopy) });
|
|
4615
5335
|
return;
|
|
4616
5336
|
}
|
|
4617
5337
|
const selectedLanguage = data.slice(16);
|
|
@@ -4648,19 +5368,19 @@ async function handlePermissionApprovalCallback(ctx, dependencies) {
|
|
|
4648
5368
|
}
|
|
4649
5369
|
function registerCallbackHandler(bot, dependencies) {
|
|
4650
5370
|
bot.callbackQuery(/^agents:/, async (ctx) => {
|
|
4651
|
-
await handleAgentsCallback(ctx, dependencies);
|
|
5371
|
+
await handleAgentsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4652
5372
|
});
|
|
4653
5373
|
bot.callbackQuery(/^sessions:/, async (ctx) => {
|
|
4654
|
-
await handleSessionsCallback(ctx, dependencies);
|
|
5374
|
+
await handleSessionsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4655
5375
|
});
|
|
4656
5376
|
bot.callbackQuery(/^model:/, async (ctx) => {
|
|
4657
|
-
await handleModelsCallback(ctx, dependencies);
|
|
5377
|
+
await handleModelsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4658
5378
|
});
|
|
4659
5379
|
bot.callbackQuery(/^language:/, async (ctx) => {
|
|
4660
|
-
await handleLanguageCallback(ctx, dependencies);
|
|
5380
|
+
await handleLanguageCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4661
5381
|
});
|
|
4662
5382
|
bot.callbackQuery(/^permission:/, async (ctx) => {
|
|
4663
|
-
await handlePermissionApprovalCallback(ctx, dependencies);
|
|
5383
|
+
await handlePermissionApprovalCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4664
5384
|
});
|
|
4665
5385
|
}
|
|
4666
5386
|
function parseSessionActionTarget(data, prefix) {
|
|
@@ -4678,7 +5398,7 @@ function parseSessionActionTarget(data, prefix) {
|
|
|
4678
5398
|
//#endregion
|
|
4679
5399
|
//#region src/bot/handlers/prompt.handler.ts
|
|
4680
5400
|
async function executePromptRequest(ctx, dependencies, resolvePrompt) {
|
|
4681
|
-
const copy = await
|
|
5401
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4682
5402
|
const foregroundRequest = dependencies.foregroundSessionTracker.acquire(ctx.chat.id);
|
|
4683
5403
|
if (!foregroundRequest) {
|
|
4684
5404
|
await ctx.reply(copy.status.alreadyProcessing);
|
|
@@ -4762,10 +5482,10 @@ async function handleImageMessage(ctx, dependencies) {
|
|
|
4762
5482
|
}
|
|
4763
5483
|
function registerFileHandler(bot, dependencies) {
|
|
4764
5484
|
bot.on("message:photo", async (ctx) => {
|
|
4765
|
-
await handleImageMessage(ctx, dependencies);
|
|
5485
|
+
await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4766
5486
|
});
|
|
4767
5487
|
bot.on("message:document", async (ctx) => {
|
|
4768
|
-
await handleImageMessage(ctx, dependencies);
|
|
5488
|
+
await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4769
5489
|
});
|
|
4770
5490
|
}
|
|
4771
5491
|
function resolveTelegramImage(message) {
|
|
@@ -4807,7 +5527,7 @@ async function handleTextMessage(ctx, dependencies) {
|
|
|
4807
5527
|
}
|
|
4808
5528
|
function registerMessageHandler(bot, dependencies) {
|
|
4809
5529
|
bot.on("message:text", async (ctx) => {
|
|
4810
|
-
await handleTextMessage(ctx, dependencies);
|
|
5530
|
+
await handleTextMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4811
5531
|
});
|
|
4812
5532
|
}
|
|
4813
5533
|
//#endregion
|
|
@@ -4815,12 +5535,12 @@ function registerMessageHandler(bot, dependencies) {
|
|
|
4815
5535
|
async function handleVoiceMessage(ctx, dependencies) {
|
|
4816
5536
|
if (!ctx.message.voice) return;
|
|
4817
5537
|
if (await replyIfSessionRenamePending(ctx, dependencies)) return;
|
|
4818
|
-
const copy = await
|
|
5538
|
+
const copy = await getSafeChatCopy(dependencies.sessionRepo, ctx.chat.id, dependencies.logger);
|
|
4819
5539
|
await ctx.reply(copy.errors.voiceUnsupported);
|
|
4820
5540
|
}
|
|
4821
5541
|
function registerVoiceHandler(bot, dependencies) {
|
|
4822
5542
|
bot.on("message:voice", async (ctx) => {
|
|
4823
|
-
await handleVoiceMessage(ctx, dependencies);
|
|
5543
|
+
await handleVoiceMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4824
5544
|
});
|
|
4825
5545
|
}
|
|
4826
5546
|
//#endregion
|
|
@@ -4841,17 +5561,18 @@ function createAuthMiddleware(allowedChatIds) {
|
|
|
4841
5561
|
function buildIncomingUpdateLogFields(ctx) {
|
|
4842
5562
|
const messageText = ctx.msg && "text" in ctx.msg ? ctx.msg.text : void 0;
|
|
4843
5563
|
return {
|
|
5564
|
+
...buildTelegramLoggerContext(ctx),
|
|
5565
|
+
event: "telegram.update.received",
|
|
4844
5566
|
updateId: ctx.update.update_id,
|
|
4845
5567
|
chatId: ctx.chat?.id,
|
|
4846
5568
|
fromId: ctx.from?.id,
|
|
4847
5569
|
hasText: typeof messageText === "string" && messageText.length > 0,
|
|
4848
|
-
textLength: typeof messageText === "string" ? messageText.length : 0
|
|
4849
|
-
textPreview: typeof messageText === "string" && messageText.length > 0 ? createRedactedPreview(messageText) : void 0
|
|
5570
|
+
textLength: typeof messageText === "string" ? messageText.length : 0
|
|
4850
5571
|
};
|
|
4851
5572
|
}
|
|
4852
5573
|
function createLoggingMiddleware(logger) {
|
|
4853
5574
|
return async (ctx, next) => {
|
|
4854
|
-
logger
|
|
5575
|
+
logTelegramUpdate(logger, { ...buildIncomingUpdateLogFields(ctx) }, "incoming update");
|
|
4855
5576
|
return next();
|
|
4856
5577
|
};
|
|
4857
5578
|
}
|
|
@@ -4860,45 +5581,133 @@ function createLoggingMiddleware(logger) {
|
|
|
4860
5581
|
function registerBot(bot, container, options) {
|
|
4861
5582
|
bot.use(createLoggingMiddleware(container.logger));
|
|
4862
5583
|
bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
5584
|
+
const safeBot = bot.errorBoundary(async (error) => {
|
|
5585
|
+
const scopedLogger = scopeLoggerToTelegramContext(container.logger, error.ctx, "telegram");
|
|
5586
|
+
scopedLogger.error({
|
|
5587
|
+
...extractTelegramUpdateContext(error.ctx),
|
|
5588
|
+
event: "telegram.middleware.failed",
|
|
5589
|
+
error: error.error
|
|
5590
|
+
}, "telegram middleware failed");
|
|
5591
|
+
await replyWithDefaultTelegramError(error.ctx, scopedLogger, error.error);
|
|
5592
|
+
});
|
|
5593
|
+
registerStartCommand(safeBot, container);
|
|
5594
|
+
registerStatusCommand(safeBot, container);
|
|
5595
|
+
registerNewCommand(safeBot, container);
|
|
5596
|
+
registerAgentsCommand(safeBot, container);
|
|
5597
|
+
registerSessionsCommand(safeBot, container);
|
|
5598
|
+
registerCancelCommand(safeBot, container);
|
|
5599
|
+
registerModelsCommand(safeBot, container);
|
|
5600
|
+
registerLanguageCommand(safeBot, container);
|
|
5601
|
+
registerCallbackHandler(safeBot, container);
|
|
5602
|
+
registerFileHandler(safeBot, container);
|
|
5603
|
+
registerMessageHandler(safeBot, container);
|
|
5604
|
+
registerVoiceHandler(safeBot, container);
|
|
4875
5605
|
}
|
|
4876
5606
|
//#endregion
|
|
4877
5607
|
//#region src/app/runtime.ts
|
|
5608
|
+
var TELEGRAM_RUNNER_OPTIONS = { runner: {
|
|
5609
|
+
fetch: { timeout: 30 },
|
|
5610
|
+
maxRetryTime: 900 * 1e3,
|
|
5611
|
+
retryInterval: "exponential",
|
|
5612
|
+
silent: true
|
|
5613
|
+
} };
|
|
4878
5614
|
async function startTelegramBotRuntime(input) {
|
|
4879
|
-
const
|
|
4880
|
-
|
|
5615
|
+
const runtimeKey = buildTelegramRuntimeKey(input.config);
|
|
5616
|
+
const registry = getTelegramBotRuntimeRegistry();
|
|
5617
|
+
const existingRuntime = registry.activeByKey.get(runtimeKey);
|
|
5618
|
+
const runtimeLogger = input.container.logger.child({ component: "runtime" });
|
|
5619
|
+
if (existingRuntime) {
|
|
5620
|
+
runtimeLogger.warn({
|
|
5621
|
+
event: "runtime.reused",
|
|
5622
|
+
runtimeKey,
|
|
5623
|
+
telegramApiRoot: input.config.telegramApiRoot
|
|
5624
|
+
}, "telegram runtime already active in this process; reusing the existing runner");
|
|
5625
|
+
await input.container.dispose();
|
|
5626
|
+
return existingRuntime;
|
|
5627
|
+
}
|
|
5628
|
+
const runtimePromise = startTelegramBotRuntimeInternal(input, runtimeKey, () => {
|
|
5629
|
+
if (registry.activeByKey.get(runtimeKey) === runtimePromise) registry.activeByKey.delete(runtimeKey);
|
|
5630
|
+
}).catch((error) => {
|
|
5631
|
+
if (registry.activeByKey.get(runtimeKey) === runtimePromise) registry.activeByKey.delete(runtimeKey);
|
|
5632
|
+
throw error;
|
|
5633
|
+
});
|
|
5634
|
+
registry.activeByKey.set(runtimeKey, runtimePromise);
|
|
5635
|
+
return runtimePromise;
|
|
5636
|
+
}
|
|
5637
|
+
async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime) {
|
|
5638
|
+
const bot = (input.botFactory ?? ((token, options) => new Bot(token, options)))(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
|
|
5639
|
+
const runtimeLogger = input.container.logger.child({ component: "runtime" });
|
|
5640
|
+
wrapTelegramGetUpdates(bot, input.container);
|
|
5641
|
+
(input.registerBotHandlers ?? registerBot)(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
|
|
4881
5642
|
bot.catch((error) => {
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
5643
|
+
const metadata = extractTelegramUpdateContext(error.ctx);
|
|
5644
|
+
const telegramLogger = input.container.logger.child({
|
|
5645
|
+
component: "telegram",
|
|
5646
|
+
...metadata
|
|
5647
|
+
});
|
|
5648
|
+
if (error.error instanceof GrammyError) {
|
|
5649
|
+
telegramLogger.error({
|
|
5650
|
+
event: "telegram.api.error",
|
|
5651
|
+
errorCode: error.error.error_code,
|
|
5652
|
+
description: error.error.description,
|
|
5653
|
+
method: error.error.method,
|
|
5654
|
+
parameters: error.error.parameters,
|
|
5655
|
+
payload: error.error.payload
|
|
5656
|
+
}, "telegram bot api request failed");
|
|
5657
|
+
return;
|
|
5658
|
+
}
|
|
5659
|
+
if (error.error instanceof HttpError) {
|
|
5660
|
+
telegramLogger.error({
|
|
5661
|
+
event: "telegram.http.error",
|
|
5662
|
+
error: error.error.error,
|
|
5663
|
+
message: error.error.message
|
|
5664
|
+
}, "telegram bot network request failed");
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5667
|
+
telegramLogger.error({
|
|
5668
|
+
event: "telegram.update.failed",
|
|
5669
|
+
error: error.error
|
|
4885
5670
|
}, "telegram bot update failed");
|
|
4886
5671
|
});
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
5672
|
+
runtimeLogger.info({
|
|
5673
|
+
event: "runtime.polling.starting",
|
|
5674
|
+
runtimeKey
|
|
5675
|
+
}, "telegram bot polling starting");
|
|
5676
|
+
const runner = (input.runBot ?? run)(bot, TELEGRAM_RUNNER_OPTIONS);
|
|
4890
5677
|
let stopped = false;
|
|
4891
5678
|
let disposed = false;
|
|
4892
|
-
|
|
5679
|
+
if (input.syncCommands ?? true) (input.syncCommandsHandler ?? syncTelegramCommands)(bot, input.container.logger).catch((error) => {
|
|
5680
|
+
runtimeLogger.warn({
|
|
5681
|
+
event: "runtime.commands.sync_failed",
|
|
5682
|
+
error,
|
|
5683
|
+
runtimeKey
|
|
5684
|
+
}, "failed to sync telegram commands; polling continues without command registration updates");
|
|
5685
|
+
});
|
|
5686
|
+
let stopPromise = null;
|
|
5687
|
+
const requestStop = async () => {
|
|
4893
5688
|
if (stopped) return;
|
|
4894
5689
|
stopped = true;
|
|
4895
|
-
runner.stop()
|
|
5690
|
+
stopPromise = runner.stop().catch((error) => {
|
|
5691
|
+
runtimeLogger.warn({
|
|
5692
|
+
event: "runtime.stop.failed",
|
|
5693
|
+
error,
|
|
5694
|
+
runtimeKey
|
|
5695
|
+
}, "failed to stop telegram runner cleanly");
|
|
5696
|
+
});
|
|
5697
|
+
await stopPromise;
|
|
5698
|
+
};
|
|
5699
|
+
const stop = () => {
|
|
5700
|
+
requestStop();
|
|
4896
5701
|
};
|
|
4897
5702
|
const dispose = async () => {
|
|
4898
5703
|
if (disposed) return;
|
|
4899
5704
|
disposed = true;
|
|
4900
|
-
|
|
4901
|
-
|
|
5705
|
+
try {
|
|
5706
|
+
await requestStop();
|
|
5707
|
+
await input.container.dispose();
|
|
5708
|
+
} finally {
|
|
5709
|
+
releaseRuntime();
|
|
5710
|
+
}
|
|
4902
5711
|
};
|
|
4903
5712
|
return {
|
|
4904
5713
|
bot,
|
|
@@ -4907,39 +5716,71 @@ async function startTelegramBotRuntime(input) {
|
|
|
4907
5716
|
dispose
|
|
4908
5717
|
};
|
|
4909
5718
|
}
|
|
5719
|
+
function wrapTelegramGetUpdates(bot, container) {
|
|
5720
|
+
const originalGetUpdates = bot.api.getUpdates.bind(bot.api);
|
|
5721
|
+
const runtimeLogger = container.logger.child({ component: "runtime" });
|
|
5722
|
+
bot.api.getUpdates = async (options, signal) => {
|
|
5723
|
+
const requestOptions = options ?? {
|
|
5724
|
+
limit: 100,
|
|
5725
|
+
offset: 0,
|
|
5726
|
+
timeout: 30
|
|
5727
|
+
};
|
|
5728
|
+
try {
|
|
5729
|
+
return await originalGetUpdates(requestOptions, signal);
|
|
5730
|
+
} catch (error) {
|
|
5731
|
+
runtimeLogger.warn({
|
|
5732
|
+
event: "runtime.telegram.get_updates_failed",
|
|
5733
|
+
error,
|
|
5734
|
+
limit: requestOptions.limit,
|
|
5735
|
+
offset: requestOptions.offset,
|
|
5736
|
+
timeout: requestOptions.timeout
|
|
5737
|
+
}, "telegram getUpdates failed");
|
|
5738
|
+
throw error;
|
|
5739
|
+
}
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
function buildTelegramRuntimeKey(config) {
|
|
5743
|
+
return `${config.telegramApiRoot}::${config.telegramBotToken}`;
|
|
5744
|
+
}
|
|
5745
|
+
function getTelegramBotRuntimeRegistry() {
|
|
5746
|
+
const globalScope = globalThis;
|
|
5747
|
+
globalScope.__opencodeTbotTelegramRuntimeRegistry__ ??= { activeByKey: /* @__PURE__ */ new Map() };
|
|
5748
|
+
return globalScope.__opencodeTbotTelegramRuntimeRegistry__;
|
|
5749
|
+
}
|
|
4910
5750
|
//#endregion
|
|
4911
5751
|
//#region src/plugin.ts
|
|
4912
|
-
var runtimeState = null;
|
|
4913
5752
|
async function ensureTelegramBotPluginRuntime(options) {
|
|
5753
|
+
const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
|
|
4914
5754
|
const cwd = resolvePluginRuntimeCwd(options.context);
|
|
4915
|
-
if (
|
|
4916
|
-
const activeState =
|
|
4917
|
-
|
|
5755
|
+
if (runtimeStateHolder.state && runtimeStateHolder.state.cwd !== cwd) {
|
|
5756
|
+
const activeState = runtimeStateHolder.state;
|
|
5757
|
+
runtimeStateHolder.state = null;
|
|
4918
5758
|
await disposeTelegramBotPluginRuntimeState(activeState);
|
|
4919
5759
|
}
|
|
4920
|
-
if (!
|
|
5760
|
+
if (!runtimeStateHolder.state) {
|
|
4921
5761
|
const runtimePromise = startPluginRuntime(options, cwd).then((runtime) => {
|
|
4922
|
-
if (
|
|
5762
|
+
if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state.runtime = runtime;
|
|
4923
5763
|
return runtime;
|
|
4924
5764
|
}).catch((error) => {
|
|
4925
|
-
if (
|
|
5765
|
+
if (runtimeStateHolder.state?.runtimePromise === runtimePromise) runtimeStateHolder.state = null;
|
|
4926
5766
|
throw error;
|
|
4927
5767
|
});
|
|
4928
|
-
|
|
5768
|
+
runtimeStateHolder.state = {
|
|
4929
5769
|
cwd,
|
|
4930
5770
|
runtime: null,
|
|
4931
5771
|
runtimePromise
|
|
4932
5772
|
};
|
|
4933
5773
|
}
|
|
4934
|
-
return
|
|
5774
|
+
return runtimeStateHolder.state.runtimePromise;
|
|
4935
5775
|
}
|
|
4936
5776
|
var TelegramBotPlugin = async (context) => {
|
|
4937
5777
|
return createHooks(await ensureTelegramBotPluginRuntime({ context }));
|
|
4938
5778
|
};
|
|
4939
5779
|
async function resetTelegramBotPluginRuntimeForTests() {
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
5780
|
+
const runtimeStateHolder = getTelegramBotPluginRuntimeStateHolder();
|
|
5781
|
+
if (!runtimeStateHolder.state) return;
|
|
5782
|
+
const activeState = runtimeStateHolder.state;
|
|
5783
|
+
runtimeStateHolder.state = null;
|
|
4943
5784
|
await disposeTelegramBotPluginRuntimeState(activeState);
|
|
4944
5785
|
}
|
|
4945
5786
|
async function startPluginRuntime(options, cwd) {
|
|
@@ -4952,8 +5793,9 @@ async function startPluginRuntime(options, cwd) {
|
|
|
4952
5793
|
});
|
|
4953
5794
|
const { config, container } = bootstrapApp(options.context.client, preparedConfiguration.config, { cwd: preparedConfiguration.cwd });
|
|
4954
5795
|
try {
|
|
4955
|
-
if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.warn({
|
|
5796
|
+
if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.child({ component: "runtime" }).warn({
|
|
4956
5797
|
cwd: preparedConfiguration.cwd,
|
|
5798
|
+
event: "runtime.config.legacy_worktree_ignored",
|
|
4957
5799
|
ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
|
|
4958
5800
|
globalConfigFilePath: preparedConfiguration.globalConfigFilePath
|
|
4959
5801
|
}, "legacy worktree plugin config is ignored; migrate settings to the global opencode-tbot config");
|
|
@@ -4961,8 +5803,9 @@ async function startPluginRuntime(options, cwd) {
|
|
|
4961
5803
|
config,
|
|
4962
5804
|
container
|
|
4963
5805
|
});
|
|
4964
|
-
container.logger.info({
|
|
5806
|
+
container.logger.child({ component: "runtime" }).info({
|
|
4965
5807
|
cwd: preparedConfiguration.cwd,
|
|
5808
|
+
event: "runtime.plugin.started",
|
|
4966
5809
|
globalConfigFilePath: preparedConfiguration.globalConfigFilePath,
|
|
4967
5810
|
ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
|
|
4968
5811
|
configFilePath: preparedConfiguration.configFilePath,
|
|
@@ -4992,6 +5835,11 @@ function createHooks(runtime) {
|
|
|
4992
5835
|
}
|
|
4993
5836
|
};
|
|
4994
5837
|
}
|
|
5838
|
+
function getTelegramBotPluginRuntimeStateHolder() {
|
|
5839
|
+
const globalScope = globalThis;
|
|
5840
|
+
globalScope.__opencodeTbotPluginRuntimeState__ ??= { state: null };
|
|
5841
|
+
return globalScope.__opencodeTbotPluginRuntimeState__;
|
|
5842
|
+
}
|
|
4995
5843
|
//#endregion
|
|
4996
5844
|
export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
|
|
4997
5845
|
|