opencode-tbot 0.1.30 → 0.1.32
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 +35 -3
- package/README.zh-CN.md +83 -51
- 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 +808 -240
- 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,83 +1,359 @@
|
|
|
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
|
-
import { z } from "zod";
|
|
6
|
-
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
7
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
8
7
|
import { run } from "@grammyjs/runner";
|
|
9
8
|
import { Bot, GrammyError, HttpError, InlineKeyboard } from "grammy";
|
|
10
9
|
//#region src/infra/utils/redact.ts
|
|
11
|
-
var REDACTED = "[REDACTED]";
|
|
12
|
-
var DEFAULT_PREVIEW_LENGTH = 160;
|
|
10
|
+
var REDACTED$1 = "[REDACTED]";
|
|
13
11
|
var TELEGRAM_TOKEN_PATTERN = /\b\d{6,}:[A-Za-z0-9_-]{20,}\b/g;
|
|
14
12
|
var BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/gi;
|
|
15
13
|
var NAMED_SECRET_PATTERN = /\b(api[_\s-]?key|token|secret|password)\b(\s*[:=]\s*)([^\s,;]+)/gi;
|
|
16
14
|
var API_KEY_LIKE_PATTERN = /\b(?:sk|pk)_[A-Za-z0-9_-]{10,}\b/g;
|
|
17
15
|
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))}...`;
|
|
16
|
+
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
17
|
}
|
|
25
18
|
//#endregion
|
|
26
19
|
//#region src/infra/logger/index.ts
|
|
20
|
+
var DEFAULT_COMPONENT = "app";
|
|
21
|
+
var DEFAULT_EVENT = "log";
|
|
27
22
|
var DEFAULT_SERVICE_NAME = "opencode-tbot";
|
|
23
|
+
var DEFAULT_MAX_LOG_FILES = 30;
|
|
24
|
+
var DEFAULT_MAX_TOTAL_LOG_BYTES = 314572800;
|
|
25
|
+
var CONTENT_OMITTED = "[OMITTED]";
|
|
26
|
+
var REDACTED = "[REDACTED]";
|
|
28
27
|
var LEVEL_PRIORITY = {
|
|
29
28
|
debug: 10,
|
|
30
29
|
info: 20,
|
|
31
30
|
warn: 30,
|
|
32
31
|
error: 40
|
|
33
32
|
};
|
|
33
|
+
var RESERVED_EVENT_FIELDS = new Set([
|
|
34
|
+
"attempt",
|
|
35
|
+
"callbackData",
|
|
36
|
+
"chatId",
|
|
37
|
+
"command",
|
|
38
|
+
"component",
|
|
39
|
+
"correlationId",
|
|
40
|
+
"durationMs",
|
|
41
|
+
"error",
|
|
42
|
+
"event",
|
|
43
|
+
"operationId",
|
|
44
|
+
"projectId",
|
|
45
|
+
"requestId",
|
|
46
|
+
"runtimeId",
|
|
47
|
+
"sessionId",
|
|
48
|
+
"sizeBytes",
|
|
49
|
+
"status",
|
|
50
|
+
"updateId",
|
|
51
|
+
"worktree"
|
|
52
|
+
]);
|
|
34
53
|
function createOpenCodeAppLogger(client, options = {}) {
|
|
35
54
|
const service = normalizeServiceName(options.service);
|
|
36
55
|
const minimumLevel = normalizeLogLevel(options.level);
|
|
56
|
+
const runtimeId = normalizeString(options.runtimeId) ?? randomUUID();
|
|
57
|
+
const boundRootContext = {
|
|
58
|
+
component: DEFAULT_COMPONENT,
|
|
59
|
+
runtimeId,
|
|
60
|
+
...options.worktree ? { worktree: options.worktree } : {}
|
|
61
|
+
};
|
|
62
|
+
const sinks = createSinks(client, {
|
|
63
|
+
file: {
|
|
64
|
+
dir: options.file?.dir,
|
|
65
|
+
maxFiles: options.file?.retention?.maxFiles,
|
|
66
|
+
maxTotalBytes: options.file?.retention?.maxTotalBytes
|
|
67
|
+
},
|
|
68
|
+
runtimeId,
|
|
69
|
+
service,
|
|
70
|
+
sinks: options.sinks
|
|
71
|
+
});
|
|
37
72
|
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
|
-
};
|
|
73
|
+
const enqueue = (event) => {
|
|
74
|
+
if (LEVEL_PRIORITY[event.level] < LEVEL_PRIORITY[minimumLevel]) return;
|
|
75
|
+
const structuredEvent = buildStructuredLogEvent(event.level, event.input, event.message, service, event.context);
|
|
47
76
|
queue = queue.catch(() => void 0).then(async () => {
|
|
48
|
-
|
|
49
|
-
if (client.app.log.length >= 2) {
|
|
50
|
-
await client.app.log(payload, {
|
|
51
|
-
responseStyle: "data",
|
|
52
|
-
throwOnError: true
|
|
53
|
-
});
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
await client.app.log({
|
|
57
|
-
body: payload,
|
|
58
|
-
responseStyle: "data",
|
|
59
|
-
throwOnError: true
|
|
60
|
-
});
|
|
61
|
-
} catch {}
|
|
77
|
+
await Promise.allSettled(sinks.map((sink) => sink.write(structuredEvent)));
|
|
62
78
|
});
|
|
63
79
|
};
|
|
64
|
-
|
|
80
|
+
const createLogger = (context) => ({
|
|
65
81
|
debug(input, message) {
|
|
66
|
-
enqueue(
|
|
82
|
+
enqueue({
|
|
83
|
+
context,
|
|
84
|
+
input,
|
|
85
|
+
level: "debug",
|
|
86
|
+
message
|
|
87
|
+
});
|
|
67
88
|
},
|
|
68
89
|
info(input, message) {
|
|
69
|
-
enqueue(
|
|
90
|
+
enqueue({
|
|
91
|
+
context,
|
|
92
|
+
input,
|
|
93
|
+
level: "info",
|
|
94
|
+
message
|
|
95
|
+
});
|
|
70
96
|
},
|
|
71
97
|
warn(input, message) {
|
|
72
|
-
enqueue(
|
|
98
|
+
enqueue({
|
|
99
|
+
context,
|
|
100
|
+
input,
|
|
101
|
+
level: "warn",
|
|
102
|
+
message
|
|
103
|
+
});
|
|
73
104
|
},
|
|
74
105
|
error(input, message) {
|
|
75
|
-
enqueue(
|
|
106
|
+
enqueue({
|
|
107
|
+
context,
|
|
108
|
+
input,
|
|
109
|
+
level: "error",
|
|
110
|
+
message
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
child(childContext) {
|
|
114
|
+
return createLogger({
|
|
115
|
+
...context,
|
|
116
|
+
...removeUndefinedFields(childContext)
|
|
117
|
+
});
|
|
76
118
|
},
|
|
77
119
|
async flush() {
|
|
78
120
|
await queue.catch(() => void 0);
|
|
121
|
+
await Promise.allSettled(sinks.map(async (sink) => {
|
|
122
|
+
await sink.flush?.();
|
|
123
|
+
}));
|
|
79
124
|
}
|
|
125
|
+
});
|
|
126
|
+
return createLogger(boundRootContext);
|
|
127
|
+
}
|
|
128
|
+
function logTelegramUpdate(logger, input, message) {
|
|
129
|
+
logger.info({
|
|
130
|
+
component: "telegram",
|
|
131
|
+
...input
|
|
132
|
+
}, message);
|
|
133
|
+
}
|
|
134
|
+
function logPromptLifecycle(logger, input, message) {
|
|
135
|
+
logger.info({
|
|
136
|
+
component: "prompt",
|
|
137
|
+
...input
|
|
138
|
+
}, message);
|
|
139
|
+
}
|
|
140
|
+
function logOpenCodeRequest(logger, input, message) {
|
|
141
|
+
logger.info({
|
|
142
|
+
component: "opencode",
|
|
143
|
+
...input
|
|
144
|
+
}, message);
|
|
145
|
+
}
|
|
146
|
+
function logPluginEvent(logger, input, message) {
|
|
147
|
+
logger.info({
|
|
148
|
+
component: "plugin-event",
|
|
149
|
+
...input
|
|
150
|
+
}, message);
|
|
151
|
+
}
|
|
152
|
+
function createSinks(client, options) {
|
|
153
|
+
const sinkOptions = options.sinks ?? {};
|
|
154
|
+
const sinks = [];
|
|
155
|
+
if (sinkOptions.host !== false) sinks.push(createHostSink(client, options.service));
|
|
156
|
+
if (sinkOptions.file !== false && options.file?.dir) sinks.push(createJsonlFileSink({
|
|
157
|
+
dir: options.file.dir,
|
|
158
|
+
maxFiles: options.file.maxFiles,
|
|
159
|
+
maxTotalBytes: options.file.maxTotalBytes,
|
|
160
|
+
runtimeId: options.runtimeId
|
|
161
|
+
}));
|
|
162
|
+
return sinks;
|
|
163
|
+
}
|
|
164
|
+
function createHostSink(client, service) {
|
|
165
|
+
return { async write(event) {
|
|
166
|
+
const payload = {
|
|
167
|
+
service,
|
|
168
|
+
level: event.level,
|
|
169
|
+
message: event.message,
|
|
170
|
+
extra: buildHostLogExtra(event)
|
|
171
|
+
};
|
|
172
|
+
if (client.app.log.length >= 2) {
|
|
173
|
+
await client.app.log(payload, {
|
|
174
|
+
responseStyle: "data",
|
|
175
|
+
throwOnError: true
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await client.app.log({
|
|
180
|
+
body: payload,
|
|
181
|
+
responseStyle: "data",
|
|
182
|
+
throwOnError: true
|
|
183
|
+
});
|
|
184
|
+
} };
|
|
185
|
+
}
|
|
186
|
+
function createJsonlFileSink(options) {
|
|
187
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_LOG_FILES;
|
|
188
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_LOG_BYTES;
|
|
189
|
+
const runtimeSegment = sanitizeFileSegment(options.runtimeId);
|
|
190
|
+
const timestampSegment = (/* @__PURE__ */ new Date()).toISOString().replace(/:/gu, "-");
|
|
191
|
+
const filePath = join(options.dir, `${timestampSegment}.${process.pid}.${runtimeSegment}.jsonl`);
|
|
192
|
+
let initialized = false;
|
|
193
|
+
let writeCount = 0;
|
|
194
|
+
const initialize = async () => {
|
|
195
|
+
if (initialized) return;
|
|
196
|
+
await mkdir(options.dir, { recursive: true });
|
|
197
|
+
await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
|
|
198
|
+
initialized = true;
|
|
199
|
+
};
|
|
200
|
+
return { async write(event) {
|
|
201
|
+
await initialize();
|
|
202
|
+
await appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
|
|
203
|
+
writeCount += 1;
|
|
204
|
+
if (writeCount === 1 || writeCount % 100 === 0) await cleanupLogDirectory(options.dir, maxFiles, maxTotalBytes);
|
|
205
|
+
} };
|
|
206
|
+
}
|
|
207
|
+
async function cleanupLogDirectory(directory, maxFiles, maxTotalBytes) {
|
|
208
|
+
let entries = [];
|
|
209
|
+
try {
|
|
210
|
+
const names = await readdir(directory);
|
|
211
|
+
entries = await Promise.all(names.filter((name) => name.endsWith(".jsonl")).map(async (name) => {
|
|
212
|
+
const entryPath = join(directory, name);
|
|
213
|
+
const entryStat = await stat(entryPath);
|
|
214
|
+
return {
|
|
215
|
+
path: entryPath,
|
|
216
|
+
size: entryStat.size,
|
|
217
|
+
time: entryStat.mtimeMs
|
|
218
|
+
};
|
|
219
|
+
}));
|
|
220
|
+
} catch {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
entries.sort((left, right) => right.time - left.time);
|
|
224
|
+
let totalBytes = 0;
|
|
225
|
+
const retained = [];
|
|
226
|
+
const deleted = [];
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const canKeepByCount = retained.length < maxFiles;
|
|
229
|
+
const canKeepBySize = retained.length === 0 || totalBytes + entry.size <= maxTotalBytes;
|
|
230
|
+
if (canKeepByCount && canKeepBySize) {
|
|
231
|
+
retained.push(entry);
|
|
232
|
+
totalBytes += entry.size;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
deleted.push(entry);
|
|
236
|
+
}
|
|
237
|
+
await Promise.allSettled(deleted.map(async (entry) => {
|
|
238
|
+
await unlink(entry.path);
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
function buildStructuredLogEvent(level, input, message, service, boundContext) {
|
|
242
|
+
const extracted = extractContextAndExtra(input);
|
|
243
|
+
const context = {
|
|
244
|
+
...removeUndefinedFields(boundContext),
|
|
245
|
+
...removeUndefinedFields(extracted.context)
|
|
80
246
|
};
|
|
247
|
+
const resolvedMessage = normalizeLogMessage(input, message);
|
|
248
|
+
return {
|
|
249
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
250
|
+
level,
|
|
251
|
+
service,
|
|
252
|
+
component: normalizeComponent(context.component),
|
|
253
|
+
event: normalizeEventName(context.event),
|
|
254
|
+
message: redactSensitiveText(resolvedMessage),
|
|
255
|
+
runtimeId: context.runtimeId ?? null,
|
|
256
|
+
operationId: context.operationId ?? null,
|
|
257
|
+
correlationId: resolveCorrelationId(context),
|
|
258
|
+
...context.worktree ? { worktree: redactSensitiveText(context.worktree) } : {},
|
|
259
|
+
...typeof context.chatId === "number" ? { chatId: context.chatId } : {},
|
|
260
|
+
...context.sessionId ? { sessionId: context.sessionId } : {},
|
|
261
|
+
...context.projectId ? { projectId: context.projectId } : {},
|
|
262
|
+
...context.requestId ? { requestId: context.requestId } : {},
|
|
263
|
+
...typeof context.updateId === "number" ? { updateId: context.updateId } : {},
|
|
264
|
+
...context.command ? { command: context.command } : {},
|
|
265
|
+
...context.callbackData ? { callbackData: context.callbackData } : {},
|
|
266
|
+
...typeof context.durationMs === "number" ? { durationMs: context.durationMs } : {},
|
|
267
|
+
...typeof context.attempt === "number" ? { attempt: context.attempt } : {},
|
|
268
|
+
...context.status ? { status: context.status } : {},
|
|
269
|
+
...typeof context.sizeBytes === "number" ? { sizeBytes: context.sizeBytes } : {},
|
|
270
|
+
...context.error ? { error: context.error } : {},
|
|
271
|
+
...removeUndefinedFields(extracted.extra)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function buildHostLogExtra(event) {
|
|
275
|
+
const { service: _service, level: _level, message: _message, ...rest } = event;
|
|
276
|
+
return rest;
|
|
277
|
+
}
|
|
278
|
+
function extractContextAndExtra(input) {
|
|
279
|
+
if (input instanceof Error) return {
|
|
280
|
+
context: { error: serializeError(input) },
|
|
281
|
+
extra: {}
|
|
282
|
+
};
|
|
283
|
+
if (Array.isArray(input)) return {
|
|
284
|
+
context: {},
|
|
285
|
+
extra: { items: input.map((item) => sanitizeValue(item)) }
|
|
286
|
+
};
|
|
287
|
+
if (input === null || input === void 0) return {
|
|
288
|
+
context: {},
|
|
289
|
+
extra: {}
|
|
290
|
+
};
|
|
291
|
+
if (typeof input === "string") return {
|
|
292
|
+
context: {},
|
|
293
|
+
extra: {}
|
|
294
|
+
};
|
|
295
|
+
if (typeof input !== "object") return {
|
|
296
|
+
context: {},
|
|
297
|
+
extra: { value: sanitizeValue(input) }
|
|
298
|
+
};
|
|
299
|
+
const record = input;
|
|
300
|
+
const context = {};
|
|
301
|
+
const extra = {};
|
|
302
|
+
for (const [key, value] of Object.entries(record)) {
|
|
303
|
+
if (RESERVED_EVENT_FIELDS.has(key)) {
|
|
304
|
+
assignReservedField(context, key, value);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
extra[key] = sanitizeFieldValue(key, value);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
context,
|
|
311
|
+
extra
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function assignReservedField(context, key, value) {
|
|
315
|
+
switch (key) {
|
|
316
|
+
case "attempt":
|
|
317
|
+
case "durationMs":
|
|
318
|
+
case "sizeBytes":
|
|
319
|
+
if (typeof value === "number") context[key] = value;
|
|
320
|
+
return;
|
|
321
|
+
case "chatId":
|
|
322
|
+
case "updateId":
|
|
323
|
+
if (typeof value === "number") context[key] = value;
|
|
324
|
+
return;
|
|
325
|
+
case "callbackData":
|
|
326
|
+
case "command":
|
|
327
|
+
case "component":
|
|
328
|
+
case "event":
|
|
329
|
+
case "projectId":
|
|
330
|
+
case "requestId":
|
|
331
|
+
case "sessionId":
|
|
332
|
+
case "status":
|
|
333
|
+
case "worktree":
|
|
334
|
+
if (typeof value === "string" && value.trim().length > 0) context[key] = redactSensitiveText(value.trim());
|
|
335
|
+
return;
|
|
336
|
+
case "correlationId":
|
|
337
|
+
case "operationId":
|
|
338
|
+
case "runtimeId":
|
|
339
|
+
if (typeof value === "string" && value.trim().length > 0) context[key] = value.trim();
|
|
340
|
+
else if (value === null) context[key] = null;
|
|
341
|
+
return;
|
|
342
|
+
case "error":
|
|
343
|
+
if (value instanceof Error) context.error = serializeError(value);
|
|
344
|
+
else if (isPlainObject(value)) context.error = normalizeStructuredLogErrorRecord(value);
|
|
345
|
+
return;
|
|
346
|
+
default: return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function normalizeLogMessage(input, message) {
|
|
350
|
+
if (typeof input === "string") {
|
|
351
|
+
const normalizedInput = input.trim();
|
|
352
|
+
return normalizedInput.length > 0 ? normalizedInput : "log";
|
|
353
|
+
}
|
|
354
|
+
if (message && message.trim().length > 0) return message.trim();
|
|
355
|
+
if (input instanceof Error) return input.message.trim() || input.name;
|
|
356
|
+
return "log";
|
|
81
357
|
}
|
|
82
358
|
function normalizeServiceName(value) {
|
|
83
359
|
const normalized = value?.trim();
|
|
@@ -91,59 +367,140 @@ function normalizeLogLevel(value) {
|
|
|
91
367
|
default: return "info";
|
|
92
368
|
}
|
|
93
369
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
370
|
+
function normalizeComponent(value) {
|
|
371
|
+
const normalized = value?.trim();
|
|
372
|
+
return normalized && normalized.length > 0 ? normalized : DEFAULT_COMPONENT;
|
|
373
|
+
}
|
|
374
|
+
function normalizeEventName(value) {
|
|
375
|
+
const normalized = value?.trim();
|
|
376
|
+
return normalized && normalized.length > 0 ? normalized : DEFAULT_EVENT;
|
|
377
|
+
}
|
|
378
|
+
function resolveCorrelationId(context) {
|
|
379
|
+
if (context.correlationId) return context.correlationId;
|
|
380
|
+
if (typeof context.updateId === "number") return String(context.updateId);
|
|
381
|
+
return context.operationId ?? null;
|
|
382
|
+
}
|
|
383
|
+
function serializeError(error) {
|
|
108
384
|
return {
|
|
109
|
-
|
|
110
|
-
|
|
385
|
+
name: error.name,
|
|
386
|
+
message: redactSensitiveText(error.message),
|
|
387
|
+
...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
|
|
388
|
+
..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
|
|
111
389
|
};
|
|
112
390
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
if (input instanceof Error) return { error: serializeError(input) };
|
|
116
|
-
if (Array.isArray(input)) return { items: input.map((item) => sanitizeValue(item)) };
|
|
117
|
-
if (typeof input !== "object") return { value: sanitizeValue(input) };
|
|
118
|
-
return sanitizeRecord(input);
|
|
391
|
+
function sanitizePlainObject(value) {
|
|
392
|
+
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, sanitizeFieldValue(key, entryValue)]));
|
|
119
393
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
394
|
+
function sanitizeFieldValue(key, value) {
|
|
395
|
+
if (isSensitiveKey(key)) return redactSensitiveFieldValue(value);
|
|
396
|
+
if (isUrlLikeKey(key)) return summarizeUrlValue(value);
|
|
397
|
+
if (isTextContentKey(key)) return summarizeTextValue(value);
|
|
398
|
+
if (isAttachmentCollectionKey(key)) return summarizeAttachmentCollection(value);
|
|
399
|
+
return sanitizeValue(value);
|
|
122
400
|
}
|
|
123
401
|
function sanitizeValue(value) {
|
|
124
402
|
if (value instanceof Error) return serializeError(value);
|
|
125
|
-
if (Array.isArray(value)) return value.map((
|
|
403
|
+
if (Array.isArray(value)) return value.map((entry) => sanitizeValue(entry));
|
|
126
404
|
if (typeof value === "string") return redactSensitiveText(value);
|
|
127
405
|
if (!value || typeof value !== "object") return value;
|
|
128
|
-
return
|
|
406
|
+
return sanitizePlainObject(value);
|
|
129
407
|
}
|
|
130
|
-
function
|
|
408
|
+
function summarizeTextValue(value) {
|
|
409
|
+
if (typeof value === "string") return {
|
|
410
|
+
omitted: CONTENT_OMITTED,
|
|
411
|
+
length: value.length
|
|
412
|
+
};
|
|
413
|
+
if (Array.isArray(value)) return {
|
|
414
|
+
omitted: CONTENT_OMITTED,
|
|
415
|
+
count: value.length
|
|
416
|
+
};
|
|
417
|
+
if (value && typeof value === "object") return {
|
|
418
|
+
omitted: CONTENT_OMITTED,
|
|
419
|
+
kind: "object"
|
|
420
|
+
};
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
function summarizeUrlValue(value) {
|
|
424
|
+
if (typeof value === "string" && value.trim().length > 0) return {
|
|
425
|
+
omitted: CONTENT_OMITTED,
|
|
426
|
+
kind: "url"
|
|
427
|
+
};
|
|
428
|
+
return sanitizeValue(value);
|
|
429
|
+
}
|
|
430
|
+
function summarizeAttachmentCollection(value) {
|
|
431
|
+
if (!Array.isArray(value)) return summarizeTextValue(value);
|
|
131
432
|
return {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...error.stack ? { stack: redactSensitiveText(error.stack) } : {},
|
|
135
|
-
..."data" in error && error.data && typeof error.data === "object" ? { data: sanitizeValue(error.data) } : {}
|
|
433
|
+
count: value.length,
|
|
434
|
+
items: value.map((entry) => summarizeAttachmentValue(entry))
|
|
136
435
|
};
|
|
137
436
|
}
|
|
437
|
+
function summarizeAttachmentValue(value) {
|
|
438
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return summarizeTextValue(value);
|
|
439
|
+
const record = value;
|
|
440
|
+
return removeUndefinedFields({
|
|
441
|
+
filename: normalizeString(record.filename) ?? normalizeString(record.fileName) ?? void 0,
|
|
442
|
+
mime: normalizeString(record.mime) ?? normalizeString(record.mimeType) ?? void 0,
|
|
443
|
+
sizeBytes: pickNumericValue(record, [
|
|
444
|
+
"sizeBytes",
|
|
445
|
+
"size",
|
|
446
|
+
"fileSize"
|
|
447
|
+
]),
|
|
448
|
+
hasCaption: pickStringValue(record, ["caption"]) !== null,
|
|
449
|
+
textLength: pickStringValue(record, ["text", "prompt"])?.length,
|
|
450
|
+
type: normalizeString(record.type) ?? void 0
|
|
451
|
+
});
|
|
452
|
+
}
|
|
138
453
|
function isSensitiveKey(key) {
|
|
139
|
-
return /token|secret|api[-_]?key|authorization|password/iu.test(key);
|
|
454
|
+
return /token|secret|api[-_]?key|authorization|password|cookie/iu.test(key);
|
|
455
|
+
}
|
|
456
|
+
function isTextContentKey(key) {
|
|
457
|
+
return /(^|[-_])(text|prompt|caption|body|markdown|content|messageText|fallbackText|input|raw)$/iu.test(key) || /bodyMd|bodyText|messageText|fallbackText/iu.test(key);
|
|
458
|
+
}
|
|
459
|
+
function isUrlLikeKey(key) {
|
|
460
|
+
return /(^|[-_])(url|uri|href|filePath|downloadPath|downloadUrl|fileUrl)$/iu.test(key);
|
|
461
|
+
}
|
|
462
|
+
function isAttachmentCollectionKey(key) {
|
|
463
|
+
return /(^|[-_])(files|parts|attachments)$/iu.test(key);
|
|
140
464
|
}
|
|
141
465
|
function redactSensitiveFieldValue(value) {
|
|
142
|
-
if (typeof value === "string" && value.trim().length > 0) return
|
|
143
|
-
if (Array.isArray(value)) return value.map(() =>
|
|
144
|
-
if (value && typeof value === "object") return
|
|
466
|
+
if (typeof value === "string" && value.trim().length > 0) return REDACTED;
|
|
467
|
+
if (Array.isArray(value)) return value.map(() => REDACTED);
|
|
468
|
+
if (value && typeof value === "object") return REDACTED;
|
|
145
469
|
return value;
|
|
146
470
|
}
|
|
471
|
+
function removeUndefinedFields(record) {
|
|
472
|
+
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
473
|
+
}
|
|
474
|
+
function isPlainObject(value) {
|
|
475
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
476
|
+
}
|
|
477
|
+
function normalizeString(value) {
|
|
478
|
+
return typeof value === "string" && value.trim().length > 0 ? redactSensitiveText(value.trim()) : null;
|
|
479
|
+
}
|
|
480
|
+
function pickNumericValue(record, keys) {
|
|
481
|
+
for (const key of keys) {
|
|
482
|
+
const value = record[key];
|
|
483
|
+
if (typeof value === "number") return value;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function pickStringValue(record, keys) {
|
|
487
|
+
for (const key of keys) {
|
|
488
|
+
const value = record[key];
|
|
489
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
function sanitizeFileSegment(value) {
|
|
494
|
+
return value.replace(/[^a-z0-9._-]+/giu, "_");
|
|
495
|
+
}
|
|
496
|
+
function normalizeStructuredLogErrorRecord(value) {
|
|
497
|
+
return {
|
|
498
|
+
name: normalizeString(value.name) ?? "Error",
|
|
499
|
+
message: normalizeString(value.message) ?? "Unknown error",
|
|
500
|
+
...typeof value.stack === "string" ? { stack: redactSensitiveText(value.stack) } : {},
|
|
501
|
+
...value.data !== void 0 ? { data: sanitizeValue(value.data) } : {}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
147
504
|
//#endregion
|
|
148
505
|
//#region src/repositories/pending-action.repo.ts
|
|
149
506
|
var FilePendingActionRepository = class {
|
|
@@ -213,6 +570,139 @@ var FileSessionRepository = class {
|
|
|
213
570
|
}
|
|
214
571
|
};
|
|
215
572
|
//#endregion
|
|
573
|
+
//#region src/infra/utils/markdown-text.ts
|
|
574
|
+
var HTML_TAG_PATTERN = /<\/?[A-Za-z][^>]*>/g;
|
|
575
|
+
function stripMarkdownToPlainText(markdown) {
|
|
576
|
+
const lines = preprocessMarkdownForPlainText(markdown).split("\n");
|
|
577
|
+
const rendered = [];
|
|
578
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
579
|
+
const tableBlock = consumeMarkdownTable$1(lines, index);
|
|
580
|
+
if (tableBlock) {
|
|
581
|
+
rendered.push(renderTableAsPlainText(tableBlock.rows));
|
|
582
|
+
index = tableBlock.nextIndex - 1;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
rendered.push(lines[index] ?? "");
|
|
586
|
+
}
|
|
587
|
+
return rendered.join("\n").replace(/```[A-Za-z0-9_-]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^(\s*)[-+*]\s+/gm, "$1- ").replace(/^(\s*)(\d+)[.)]\s+/gm, "$1$2. ").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(HTML_TAG_PATTERN, "").trim();
|
|
588
|
+
}
|
|
589
|
+
function preprocessMarkdownForPlainText(markdown) {
|
|
590
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
591
|
+
const processed = [];
|
|
592
|
+
let activeFence = null;
|
|
593
|
+
for (const line of lines) {
|
|
594
|
+
const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
|
|
595
|
+
if (fenceMatch) {
|
|
596
|
+
const language = (fenceMatch[1] ?? "").toLowerCase();
|
|
597
|
+
if (activeFence === "markdown") {
|
|
598
|
+
activeFence = null;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (activeFence === "plain") {
|
|
602
|
+
processed.push(line);
|
|
603
|
+
activeFence = null;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (language === "md" || language === "markdown") {
|
|
607
|
+
activeFence = "markdown";
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
activeFence = "plain";
|
|
611
|
+
processed.push(line);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
processed.push(line);
|
|
615
|
+
}
|
|
616
|
+
return processed.join("\n");
|
|
617
|
+
}
|
|
618
|
+
function consumeMarkdownTable$1(lines, startIndex) {
|
|
619
|
+
if (startIndex + 1 >= lines.length) return null;
|
|
620
|
+
const headerCells = parseMarkdownTableRow$1(lines[startIndex] ?? "");
|
|
621
|
+
const separatorCells = parseMarkdownTableSeparator$1(lines[startIndex + 1] ?? "");
|
|
622
|
+
if (!headerCells || !separatorCells || headerCells.length !== separatorCells.length) return null;
|
|
623
|
+
const rows = [headerCells];
|
|
624
|
+
let index = startIndex + 2;
|
|
625
|
+
while (index < lines.length) {
|
|
626
|
+
const rowCells = parseMarkdownTableRow$1(lines[index] ?? "");
|
|
627
|
+
if (!rowCells || rowCells.length !== headerCells.length) break;
|
|
628
|
+
rows.push(rowCells);
|
|
629
|
+
index += 1;
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
rows,
|
|
633
|
+
nextIndex: index
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function parseMarkdownTableRow$1(line) {
|
|
637
|
+
const trimmed = line.trim();
|
|
638
|
+
if (!trimmed.includes("|")) return null;
|
|
639
|
+
const cells = splitMarkdownTableCells$1(trimmed).map((cell) => normalizeTableCell$1(cell));
|
|
640
|
+
return cells.length >= 2 ? cells : null;
|
|
641
|
+
}
|
|
642
|
+
function parseMarkdownTableSeparator$1(line) {
|
|
643
|
+
const cells = splitMarkdownTableCells$1(line.trim());
|
|
644
|
+
if (cells.length < 2) return null;
|
|
645
|
+
return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim())) ? cells : null;
|
|
646
|
+
}
|
|
647
|
+
function splitMarkdownTableCells$1(line) {
|
|
648
|
+
const content = line.replace(/^\|/, "").replace(/\|$/, "");
|
|
649
|
+
const cells = [];
|
|
650
|
+
let current = "";
|
|
651
|
+
let escaped = false;
|
|
652
|
+
for (const char of content) {
|
|
653
|
+
if (escaped) {
|
|
654
|
+
current += char;
|
|
655
|
+
escaped = false;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (char === "\\") {
|
|
659
|
+
escaped = true;
|
|
660
|
+
current += char;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (char === "|") {
|
|
664
|
+
cells.push(current);
|
|
665
|
+
current = "";
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
current += char;
|
|
669
|
+
}
|
|
670
|
+
cells.push(current);
|
|
671
|
+
return cells;
|
|
672
|
+
}
|
|
673
|
+
function normalizeTableCell$1(cell) {
|
|
674
|
+
return cell.trim().replace(/\\\|/g, "|").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(HTML_TAG_PATTERN, "");
|
|
675
|
+
}
|
|
676
|
+
function renderTableAsPlainText(rows) {
|
|
677
|
+
return buildAlignedTableLines$1(rows).join("\n");
|
|
678
|
+
}
|
|
679
|
+
function buildAlignedTableLines$1(rows) {
|
|
680
|
+
const columnWidths = calculateTableColumnWidths$1(rows);
|
|
681
|
+
return [
|
|
682
|
+
formatTableRow$1(rows[0] ?? [], columnWidths),
|
|
683
|
+
columnWidths.map((width) => "-".repeat(Math.max(3, width))).join("-+-"),
|
|
684
|
+
...rows.slice(1).map((row) => formatTableRow$1(row, columnWidths))
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
function calculateTableColumnWidths$1(rows) {
|
|
688
|
+
return (rows[0] ?? []).map((_, columnIndex) => rows.reduce((maxWidth, row) => Math.max(maxWidth, getDisplayWidth$1(row[columnIndex] ?? "")), 0));
|
|
689
|
+
}
|
|
690
|
+
function formatTableRow$1(row, columnWidths) {
|
|
691
|
+
return row.map((cell, index) => padDisplayWidth$1(cell, columnWidths[index] ?? 0)).join(" | ");
|
|
692
|
+
}
|
|
693
|
+
function padDisplayWidth$1(value, targetWidth) {
|
|
694
|
+
const padding = Math.max(0, targetWidth - getDisplayWidth$1(value));
|
|
695
|
+
return `${value}${" ".repeat(padding)}`;
|
|
696
|
+
}
|
|
697
|
+
function getDisplayWidth$1(value) {
|
|
698
|
+
let width = 0;
|
|
699
|
+
for (const char of value) width += isWideCharacter$1(char.codePointAt(0) ?? 0) ? 2 : 1;
|
|
700
|
+
return width;
|
|
701
|
+
}
|
|
702
|
+
function isWideCharacter$1(codePoint) {
|
|
703
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510);
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
216
706
|
//#region src/services/opencode/opencode.client.ts
|
|
217
707
|
function buildOpenCodeSdkConfig(options) {
|
|
218
708
|
const apiKey = options.apiKey?.trim();
|
|
@@ -256,24 +746,11 @@ var DEFAULT_OPENCODE_PROMPT_TIMEOUT_POLICY = {
|
|
|
256
746
|
recoveryInactivityTimeoutMs: 12e4,
|
|
257
747
|
waitTimeoutMs: 18e5
|
|
258
748
|
};
|
|
259
|
-
var STRUCTURED_REPLY_SCHEMA = {
|
|
260
|
-
type: "json_schema",
|
|
261
|
-
retryCount: 2,
|
|
262
|
-
schema: {
|
|
263
|
-
type: "object",
|
|
264
|
-
additionalProperties: false,
|
|
265
|
-
required: ["body_md"],
|
|
266
|
-
properties: { body_md: {
|
|
267
|
-
type: "string",
|
|
268
|
-
description: "Markdown body only. Do not include duration, token usage, or any footer. Do not wrap the whole answer in ```md or ```markdown fences. Use Markdown formatting directly unless the user explicitly asks for raw Markdown source."
|
|
269
|
-
} }
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
749
|
var SDK_OPTIONS = {
|
|
273
750
|
responseStyle: "data",
|
|
274
751
|
throwOnError: true
|
|
275
752
|
};
|
|
276
|
-
var
|
|
753
|
+
var TEXT_OUTPUT_FORMAT = { type: "text" };
|
|
277
754
|
var OpenCodeClient = class {
|
|
278
755
|
client;
|
|
279
756
|
fetchFn;
|
|
@@ -461,20 +938,17 @@ var OpenCodeClient = class {
|
|
|
461
938
|
return buildPromptSessionResult(await this.resolvePromptResponse(input, null, knownMessageIds, startedAt), {
|
|
462
939
|
emptyResponseText: EMPTY_RESPONSE_TEXT,
|
|
463
940
|
finishedAt: Date.now(),
|
|
464
|
-
startedAt
|
|
465
|
-
structured: input.structured ?? false
|
|
941
|
+
startedAt
|
|
466
942
|
});
|
|
467
943
|
}
|
|
468
944
|
async resolvePromptResponse(input, data, knownMessageIds, startedAt) {
|
|
469
|
-
|
|
470
|
-
if (data && shouldReturnPromptResponseImmediately(data, structured)) return data;
|
|
945
|
+
if (data && shouldReturnPromptResponseImmediately(data)) return data;
|
|
471
946
|
const messageId = data ? extractMessageId(data.info) : null;
|
|
472
947
|
const candidateOptions = {
|
|
473
948
|
initialMessageId: messageId,
|
|
474
949
|
initialParentId: data ? toAssistantMessage(data.info)?.parentID ?? null : null,
|
|
475
950
|
knownMessageIds,
|
|
476
|
-
requestStartedAt: resolvePromptCandidateStartTime(startedAt, data)
|
|
477
|
-
structured
|
|
951
|
+
requestStartedAt: resolvePromptCandidateStartTime(startedAt, data)
|
|
478
952
|
};
|
|
479
953
|
let bestCandidate = selectPromptResponseCandidate(data ? [data] : [], candidateOptions);
|
|
480
954
|
let lastProgressAt = Date.now();
|
|
@@ -499,26 +973,26 @@ var OpenCodeClient = class {
|
|
|
499
973
|
if (next) {
|
|
500
974
|
const nextCandidate = selectPromptResponseCandidate([bestCandidate, next], candidateOptions);
|
|
501
975
|
if (nextCandidate) {
|
|
502
|
-
if (didPromptResponseAdvance(bestCandidate, nextCandidate
|
|
976
|
+
if (didPromptResponseAdvance(bestCandidate, nextCandidate)) {
|
|
503
977
|
lastProgressAt = Date.now();
|
|
504
978
|
idleStatusSeen = false;
|
|
505
979
|
}
|
|
506
980
|
bestCandidate = nextCandidate;
|
|
507
981
|
}
|
|
508
|
-
if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate
|
|
982
|
+
if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate)) return bestCandidate;
|
|
509
983
|
}
|
|
510
984
|
}
|
|
511
985
|
const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages", input.signal);
|
|
512
986
|
if (latest) {
|
|
513
987
|
const nextCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
|
|
514
988
|
if (nextCandidate) {
|
|
515
|
-
if (didPromptResponseAdvance(bestCandidate, nextCandidate
|
|
989
|
+
if (didPromptResponseAdvance(bestCandidate, nextCandidate)) {
|
|
516
990
|
lastProgressAt = Date.now();
|
|
517
991
|
idleStatusSeen = false;
|
|
518
992
|
}
|
|
519
993
|
bestCandidate = nextCandidate;
|
|
520
994
|
}
|
|
521
|
-
if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate
|
|
995
|
+
if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && shouldReturnPromptResponseImmediately(bestCandidate)) return bestCandidate;
|
|
522
996
|
}
|
|
523
997
|
const status = await this.fetchPromptSessionStatus(input.sessionId, input.signal);
|
|
524
998
|
lastStatus = status;
|
|
@@ -529,14 +1003,14 @@ var OpenCodeClient = class {
|
|
|
529
1003
|
if (idleStatusSeen) break;
|
|
530
1004
|
idleStatusSeen = true;
|
|
531
1005
|
}
|
|
532
|
-
if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate
|
|
1006
|
+
if (bestCandidate && isPromptResponseForCurrentRequest(bestCandidate, candidateOptions) && isCompletedEmptyPromptResponse(bestCandidate) && status?.type !== "busy" && status?.type !== "retry") break;
|
|
533
1007
|
if (Date.now() >= deadlineAt) break;
|
|
534
1008
|
}
|
|
535
1009
|
const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan", input.signal);
|
|
536
1010
|
const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions);
|
|
537
1011
|
const requestScopedResolved = resolved && isPromptResponseForCurrentRequest(resolved, candidateOptions) ? resolved : null;
|
|
538
|
-
if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved
|
|
539
|
-
if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved
|
|
1012
|
+
if (lastStatus?.type === "idle" && (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved))) throw createMessageAbortedError();
|
|
1013
|
+
if (!requestScopedResolved || shouldPollPromptMessage(requestScopedResolved)) {
|
|
540
1014
|
const timeoutReason = Date.now() >= deadlineAt ? "max-wait" : "recovery-inactivity";
|
|
541
1015
|
const timeoutMs = timeoutReason === "max-wait" ? this.promptTimeoutPolicy.waitTimeoutMs : this.promptTimeoutPolicy.recoveryInactivityTimeoutMs;
|
|
542
1016
|
const error = createOpenCodePromptTimeoutError({
|
|
@@ -662,9 +1136,10 @@ var OpenCodeClient = class {
|
|
|
662
1136
|
return this.callScopedSdkMethod("config", "providers", {});
|
|
663
1137
|
}
|
|
664
1138
|
async sendPromptRequest(input, parts) {
|
|
1139
|
+
const format = input.format ?? TEXT_OUTPUT_FORMAT;
|
|
665
1140
|
const requestBody = {
|
|
666
1141
|
...input.agent ? { agent: input.agent } : {},
|
|
667
|
-
|
|
1142
|
+
format,
|
|
668
1143
|
...input.model ? { model: input.model } : {},
|
|
669
1144
|
...input.variant ? { variant: input.variant } : {},
|
|
670
1145
|
parts
|
|
@@ -881,10 +1356,9 @@ function extractTextFromParts(parts) {
|
|
|
881
1356
|
}
|
|
882
1357
|
function buildPromptSessionResult(data, options) {
|
|
883
1358
|
const assistantInfo = toAssistantMessage(data.info);
|
|
884
|
-
const structuredPayload = extractStructuredPayload(assistantInfo);
|
|
885
|
-
const bodyMd = options.structured ? extractStructuredMarkdown(structuredPayload) : null;
|
|
886
1359
|
const responseParts = Array.isArray(data.parts) ? data.parts : [];
|
|
887
|
-
const
|
|
1360
|
+
const bodyMd = extractTextFromParts(responseParts) || null;
|
|
1361
|
+
const fallbackText = bodyMd ? stripMarkdownToPlainText(bodyMd) || bodyMd : options.emptyResponseText;
|
|
888
1362
|
return {
|
|
889
1363
|
assistantError: assistantInfo?.error ?? null,
|
|
890
1364
|
bodyMd,
|
|
@@ -892,22 +1366,21 @@ function buildPromptSessionResult(data, options) {
|
|
|
892
1366
|
info: assistantInfo,
|
|
893
1367
|
metrics: extractPromptMetrics(assistantInfo, options.startedAt, options.finishedAt),
|
|
894
1368
|
parts: responseParts,
|
|
895
|
-
structured:
|
|
1369
|
+
structured: null
|
|
896
1370
|
};
|
|
897
1371
|
}
|
|
898
|
-
function shouldPollPromptMessage(data
|
|
1372
|
+
function shouldPollPromptMessage(data) {
|
|
899
1373
|
const assistantInfo = toAssistantMessage(data.info);
|
|
900
|
-
const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
|
|
901
1374
|
const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
|
|
902
1375
|
const hasAssistantError = !!assistantInfo?.error;
|
|
903
1376
|
const isCompleted = isAssistantMessageCompleted(assistantInfo);
|
|
904
|
-
return !hasText && !
|
|
1377
|
+
return !hasText && !hasAssistantError && !isCompleted;
|
|
905
1378
|
}
|
|
906
|
-
function shouldReturnPromptResponseImmediately(data
|
|
907
|
-
return !shouldPollPromptMessage(data
|
|
1379
|
+
function shouldReturnPromptResponseImmediately(data) {
|
|
1380
|
+
return !shouldPollPromptMessage(data) && !isCompletedEmptyPromptResponse(data);
|
|
908
1381
|
}
|
|
909
|
-
function isPromptResponseUsable(data
|
|
910
|
-
return !shouldPollPromptMessage(data
|
|
1382
|
+
function isPromptResponseUsable(data) {
|
|
1383
|
+
return !shouldPollPromptMessage(data) && !isCompletedEmptyPromptResponse(data);
|
|
911
1384
|
}
|
|
912
1385
|
function normalizePromptResponse(response) {
|
|
913
1386
|
return {
|
|
@@ -942,8 +1415,6 @@ function toAssistantMessage(message) {
|
|
|
942
1415
|
if ("providerID" in message && typeof message.providerID === "string" && message.providerID.trim().length > 0) normalized.providerID = message.providerID;
|
|
943
1416
|
if ("role" in message && message.role === "assistant") normalized.role = "assistant";
|
|
944
1417
|
if ("sessionID" in message && typeof message.sessionID === "string" && message.sessionID.trim().length > 0) normalized.sessionID = message.sessionID;
|
|
945
|
-
const structuredPayload = extractStructuredPayload(message);
|
|
946
|
-
if (structuredPayload !== null) normalized.structured = structuredPayload;
|
|
947
1418
|
if ("summary" in message && typeof message.summary === "boolean") normalized.summary = message.summary;
|
|
948
1419
|
if ("time" in message && isPlainRecord$1(message.time)) normalized.time = {
|
|
949
1420
|
...typeof message.time.created === "number" && Number.isFinite(message.time.created) ? { created: message.time.created } : {},
|
|
@@ -984,8 +1455,8 @@ function delay(ms, signal) {
|
|
|
984
1455
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
985
1456
|
});
|
|
986
1457
|
}
|
|
987
|
-
function didPromptResponseAdvance(previous, next
|
|
988
|
-
return getPromptResponseProgressSignature(previous
|
|
1458
|
+
function didPromptResponseAdvance(previous, next) {
|
|
1459
|
+
return getPromptResponseProgressSignature(previous) !== getPromptResponseProgressSignature(next);
|
|
989
1460
|
}
|
|
990
1461
|
function createOpenCodePromptTimeoutError(input) {
|
|
991
1462
|
return new OpenCodePromptTimeoutError({
|
|
@@ -1007,12 +1478,6 @@ function resolvePromptEndpointKind(stage) {
|
|
|
1007
1478
|
function getPromptMessagePollDelayMs(attempt) {
|
|
1008
1479
|
return PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS[attempt] ?? PROMPT_MESSAGE_POLL_INTERVAL_MS;
|
|
1009
1480
|
}
|
|
1010
|
-
function extractStructuredMarkdown(structured) {
|
|
1011
|
-
const parsed = StructuredReplySchema.safeParse(structured);
|
|
1012
|
-
if (!parsed.success) return null;
|
|
1013
|
-
const bodyMd = parsed.data.body_md.replace(/\r\n?/g, "\n").trim();
|
|
1014
|
-
return bodyMd.length > 0 ? bodyMd : null;
|
|
1015
|
-
}
|
|
1016
1481
|
function extractPromptMetrics(info, startedAt, finishedAt) {
|
|
1017
1482
|
const createdAt = typeof info?.time?.created === "number" && Number.isFinite(info.time.created) ? info.time.created : null;
|
|
1018
1483
|
const completedAt = typeof info?.time?.completed === "number" && Number.isFinite(info.time.completed) ? info.time.completed : null;
|
|
@@ -1105,17 +1570,10 @@ function normalizeAssistantError(value) {
|
|
|
1105
1570
|
function isAssistantMessageCompleted(message) {
|
|
1106
1571
|
return !!message?.error || typeof message?.time?.completed === "number" || typeof message?.finish === "string" && message.finish.trim().length > 0;
|
|
1107
1572
|
}
|
|
1108
|
-
function isCompletedEmptyPromptResponse(data
|
|
1573
|
+
function isCompletedEmptyPromptResponse(data) {
|
|
1109
1574
|
const assistantInfo = toAssistantMessage(data.info);
|
|
1110
|
-
const bodyMd = structured ? extractStructuredMarkdown(extractStructuredPayload(assistantInfo)) : null;
|
|
1111
1575
|
const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
|
|
1112
|
-
return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText
|
|
1113
|
-
}
|
|
1114
|
-
function extractStructuredPayload(message) {
|
|
1115
|
-
if (!isPlainRecord$1(message)) return null;
|
|
1116
|
-
if ("structured" in message && message.structured !== void 0) return message.structured;
|
|
1117
|
-
if ("structured_output" in message && message.structured_output !== void 0) return message.structured_output;
|
|
1118
|
-
return null;
|
|
1576
|
+
return isAssistantMessageCompleted(assistantInfo) && !assistantInfo?.error && !hasText;
|
|
1119
1577
|
}
|
|
1120
1578
|
function selectPromptResponseCandidate(candidates, options) {
|
|
1121
1579
|
const availableCandidates = candidates.filter((candidate) => !!candidate).filter((candidate) => toAssistantMessage(candidate.info) !== null);
|
|
@@ -1135,7 +1593,7 @@ function getPromptResponseCandidateRank(message, options) {
|
|
|
1135
1593
|
createdAt,
|
|
1136
1594
|
isInitial: !!id && id === options.initialMessageId,
|
|
1137
1595
|
isNewSinceRequestStart: isPromptResponseNewSinceRequestStart(id, createdAt, options.knownMessageIds, options.requestStartedAt),
|
|
1138
|
-
isUsable: isPromptResponseUsable(message
|
|
1596
|
+
isUsable: isPromptResponseUsable(message),
|
|
1139
1597
|
sharesParent: !!assistant?.parentID && assistant.parentID === options.initialParentId
|
|
1140
1598
|
};
|
|
1141
1599
|
}
|
|
@@ -1145,13 +1603,13 @@ function resolvePromptCandidateStartTime(startedAt, initialMessage) {
|
|
|
1145
1603
|
if (initialCreatedAt === null) return startedAt;
|
|
1146
1604
|
return areComparablePromptTimestamps(startedAt, initialCreatedAt) ? startedAt : initialCreatedAt;
|
|
1147
1605
|
}
|
|
1148
|
-
function getPromptResponseProgressSignature(response
|
|
1606
|
+
function getPromptResponseProgressSignature(response) {
|
|
1149
1607
|
if (!response) return "null";
|
|
1150
1608
|
const assistant = toAssistantMessage(response.info);
|
|
1151
1609
|
const responseParts = Array.isArray(response.parts) ? response.parts : [];
|
|
1152
1610
|
return JSON.stringify({
|
|
1153
1611
|
assistantError: assistant?.error?.name ?? null,
|
|
1154
|
-
bodyMd:
|
|
1612
|
+
bodyMd: extractTextFromParts(responseParts) || null,
|
|
1155
1613
|
completedAt: assistant?.time?.completed ?? null,
|
|
1156
1614
|
finish: assistant?.finish ?? null,
|
|
1157
1615
|
id: assistant?.id ?? null,
|
|
@@ -1979,10 +2437,11 @@ var SendPromptUseCase = class {
|
|
|
1979
2437
|
binding
|
|
1980
2438
|
});
|
|
1981
2439
|
binding = createdSession.binding;
|
|
1982
|
-
this.logger
|
|
2440
|
+
logPromptLifecycle(this.logger, {
|
|
1983
2441
|
chatId: input.chatId,
|
|
1984
|
-
|
|
2442
|
+
event: "prompt.session.created",
|
|
1985
2443
|
projectId: createdSession.session.projectID,
|
|
2444
|
+
sessionId: createdSession.session.id,
|
|
1986
2445
|
directory: createdSession.session.directory
|
|
1987
2446
|
}, "session created");
|
|
1988
2447
|
}
|
|
@@ -1990,11 +2449,15 @@ var SendPromptUseCase = class {
|
|
|
1990
2449
|
const selectedModel = (await this.opencodeClient.listModels()).find((model) => model.providerID === binding?.modelProviderId && model.id === binding?.modelId);
|
|
1991
2450
|
if (!selectedModel) {
|
|
1992
2451
|
binding = await clearStoredModelSelection(this.sessionRepo, binding);
|
|
1993
|
-
this.logger.warn?.({
|
|
2452
|
+
this.logger.warn?.({
|
|
2453
|
+
chatId: input.chatId,
|
|
2454
|
+
event: "prompt.model.unavailable"
|
|
2455
|
+
}, "selected model is no longer available, falling back to OpenCode default");
|
|
1994
2456
|
} else if (binding.modelVariant && !(binding.modelVariant in selectedModel.variants)) {
|
|
1995
2457
|
binding = await clearStoredModelVariant(this.sessionRepo, binding);
|
|
1996
2458
|
this.logger.warn?.({
|
|
1997
2459
|
chatId: input.chatId,
|
|
2460
|
+
event: "prompt.model.variant_unavailable",
|
|
1998
2461
|
providerId: selectedModel.providerID,
|
|
1999
2462
|
modelId: selectedModel.id
|
|
2000
2463
|
}, "selected model variant is no longer available, falling back to default variant");
|
|
@@ -2010,23 +2473,41 @@ var SendPromptUseCase = class {
|
|
|
2010
2473
|
const selectedAgent = resolveSelectedAgent(await this.opencodeClient.listAgents(), activeBinding.agentName);
|
|
2011
2474
|
if (activeBinding.agentName && selectedAgent?.name !== activeBinding.agentName) {
|
|
2012
2475
|
activeBinding = await clearStoredAgentSelection(this.sessionRepo, activeBinding);
|
|
2013
|
-
this.logger.warn?.({
|
|
2476
|
+
this.logger.warn?.({
|
|
2477
|
+
chatId: input.chatId,
|
|
2478
|
+
event: "prompt.agent.unavailable"
|
|
2479
|
+
}, "selected agent is no longer available, falling back to OpenCode default");
|
|
2014
2480
|
}
|
|
2015
2481
|
const temporarySessionId = shouldIsolateImageTurn ? await this.createTemporaryImageSession(input.chatId, activeBinding.sessionId) : null;
|
|
2016
2482
|
const executionSessionId = temporarySessionId ?? activeBinding.sessionId;
|
|
2017
2483
|
input.onExecutionSession?.(executionSessionId);
|
|
2018
2484
|
let result;
|
|
2019
2485
|
try {
|
|
2486
|
+
logOpenCodeRequest(this.logger, {
|
|
2487
|
+
chatId: input.chatId,
|
|
2488
|
+
event: "opencode.prompt.submit",
|
|
2489
|
+
projectId: activeBinding.projectId,
|
|
2490
|
+
sessionId: executionSessionId,
|
|
2491
|
+
fileCount: files.length,
|
|
2492
|
+
status: "started"
|
|
2493
|
+
}, "submitting OpenCode prompt");
|
|
2020
2494
|
result = await this.opencodeClient.promptSession({
|
|
2021
2495
|
sessionId: executionSessionId,
|
|
2022
2496
|
prompt: promptText,
|
|
2023
2497
|
...files.length > 0 ? { files } : {},
|
|
2024
2498
|
...selectedAgent ? { agent: selectedAgent.name } : {},
|
|
2025
|
-
|
|
2499
|
+
format: { type: "text" },
|
|
2026
2500
|
...model ? { model } : {},
|
|
2027
2501
|
...input.signal ? { signal: input.signal } : {},
|
|
2028
2502
|
...activeBinding.modelVariant ? { variant: activeBinding.modelVariant } : {}
|
|
2029
2503
|
});
|
|
2504
|
+
logPromptLifecycle(this.logger, {
|
|
2505
|
+
chatId: input.chatId,
|
|
2506
|
+
event: "prompt.completed",
|
|
2507
|
+
projectId: activeBinding.projectId,
|
|
2508
|
+
sessionId: executionSessionId,
|
|
2509
|
+
status: "completed"
|
|
2510
|
+
}, "prompt completed");
|
|
2030
2511
|
} finally {
|
|
2031
2512
|
if (temporarySessionId) await this.cleanupTemporaryImageSession(input.chatId, activeBinding.sessionId, temporarySessionId);
|
|
2032
2513
|
}
|
|
@@ -2039,14 +2520,18 @@ var SendPromptUseCase = class {
|
|
|
2039
2520
|
}
|
|
2040
2521
|
async clearInvalidSessionContext(chatId, binding, reason) {
|
|
2041
2522
|
const nextBinding = await clearStoredSessionContext(this.sessionRepo, binding);
|
|
2042
|
-
this.logger.warn?.({
|
|
2523
|
+
this.logger.warn?.({
|
|
2524
|
+
chatId,
|
|
2525
|
+
event: "prompt.session.invalid_context"
|
|
2526
|
+
}, `${reason}, falling back to the current OpenCode project`);
|
|
2043
2527
|
return nextBinding;
|
|
2044
2528
|
}
|
|
2045
2529
|
async createTemporaryImageSession(chatId, sessionId) {
|
|
2046
2530
|
const temporarySession = await this.opencodeClient.forkSession(sessionId);
|
|
2047
2531
|
if (!temporarySession.id || temporarySession.id === sessionId) throw new Error("OpenCode did not return a distinct temporary session for the image turn.");
|
|
2048
|
-
this.logger
|
|
2532
|
+
logPromptLifecycle(this.logger, {
|
|
2049
2533
|
chatId,
|
|
2534
|
+
event: "prompt.temporary_session.created",
|
|
2050
2535
|
parentSessionId: sessionId,
|
|
2051
2536
|
sessionId: temporarySession.id
|
|
2052
2537
|
}, "created temporary image session");
|
|
@@ -2056,6 +2541,7 @@ var SendPromptUseCase = class {
|
|
|
2056
2541
|
try {
|
|
2057
2542
|
if (!await this.opencodeClient.deleteSession(sessionId)) this.logger.warn?.({
|
|
2058
2543
|
chatId,
|
|
2544
|
+
event: "prompt.temporary_session.cleanup_failed",
|
|
2059
2545
|
parentSessionId,
|
|
2060
2546
|
sessionId
|
|
2061
2547
|
}, "failed to delete temporary image session");
|
|
@@ -2063,6 +2549,7 @@ var SendPromptUseCase = class {
|
|
|
2063
2549
|
this.logger.warn?.({
|
|
2064
2550
|
error,
|
|
2065
2551
|
chatId,
|
|
2552
|
+
event: "prompt.temporary_session.cleanup_failed",
|
|
2066
2553
|
parentSessionId,
|
|
2067
2554
|
sessionId
|
|
2068
2555
|
}, "failed to delete temporary image session");
|
|
@@ -2263,7 +2750,23 @@ function resolveExtension(mimeType) {
|
|
|
2263
2750
|
//#endregion
|
|
2264
2751
|
//#region src/app/container.ts
|
|
2265
2752
|
function createAppContainer(config, client) {
|
|
2266
|
-
const
|
|
2753
|
+
const runtimeId = randomUUID();
|
|
2754
|
+
const logger = createOpenCodeAppLogger(client, {
|
|
2755
|
+
file: {
|
|
2756
|
+
dir: config.loggingFileDir,
|
|
2757
|
+
retention: {
|
|
2758
|
+
maxFiles: config.loggingRetentionMaxFiles,
|
|
2759
|
+
maxTotalBytes: config.loggingRetentionMaxTotalBytes
|
|
2760
|
+
}
|
|
2761
|
+
},
|
|
2762
|
+
level: config.loggingLevel,
|
|
2763
|
+
runtimeId,
|
|
2764
|
+
sinks: {
|
|
2765
|
+
file: config.loggingFileSinkEnabled,
|
|
2766
|
+
host: config.loggingHostSinkEnabled
|
|
2767
|
+
},
|
|
2768
|
+
worktree: config.worktreePath
|
|
2769
|
+
});
|
|
2267
2770
|
return createContainer(config, createOpenCodeClientFromSdkClient(client, fetch, {
|
|
2268
2771
|
waitTimeoutMs: config.promptWaitTimeoutMs,
|
|
2269
2772
|
pollRequestTimeoutMs: config.promptPollRequestTimeoutMs,
|
|
@@ -2271,6 +2774,9 @@ function createAppContainer(config, client) {
|
|
|
2271
2774
|
}), logger);
|
|
2272
2775
|
}
|
|
2273
2776
|
function createContainer(config, opencodeClient, logger) {
|
|
2777
|
+
const storageLogger = logger.child({ component: "storage" });
|
|
2778
|
+
const opencodeLogger = logger.child({ component: "opencode" });
|
|
2779
|
+
const promptLogger = logger.child({ component: "prompt" });
|
|
2274
2780
|
const stateStore = new JsonStateStore({
|
|
2275
2781
|
filePath: config.stateFilePath,
|
|
2276
2782
|
createDefaultState: createDefaultOpencodeTbotState
|
|
@@ -2285,7 +2791,7 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2285
2791
|
});
|
|
2286
2792
|
const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
|
|
2287
2793
|
const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient, foregroundSessionTracker);
|
|
2288
|
-
const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient,
|
|
2794
|
+
const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2289
2795
|
const getHealthUseCase = new GetHealthUseCase(opencodeClient);
|
|
2290
2796
|
const getPathUseCase = new GetPathUseCase(opencodeClient);
|
|
2291
2797
|
const listAgentsUseCase = new ListAgentsUseCase(sessionRepo, opencodeClient);
|
|
@@ -2294,11 +2800,11 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2294
2800
|
const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
|
|
2295
2801
|
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
|
|
2296
2802
|
const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
|
|
2297
|
-
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient,
|
|
2298
|
-
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient,
|
|
2299
|
-
const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient,
|
|
2300
|
-
const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient,
|
|
2301
|
-
const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient,
|
|
2803
|
+
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2804
|
+
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, promptLogger);
|
|
2805
|
+
const switchAgentUseCase = new SwitchAgentUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2806
|
+
const switchModelUseCase = new SwitchModelUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2807
|
+
const switchSessionUseCase = new SwitchSessionUseCase(sessionRepo, opencodeClient, opencodeLogger);
|
|
2302
2808
|
let disposed = false;
|
|
2303
2809
|
return {
|
|
2304
2810
|
abortPromptUseCase,
|
|
@@ -2327,7 +2833,10 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2327
2833
|
async dispose() {
|
|
2328
2834
|
if (disposed) return;
|
|
2329
2835
|
disposed = true;
|
|
2330
|
-
|
|
2836
|
+
storageLogger.info({
|
|
2837
|
+
event: "storage.container.disposed",
|
|
2838
|
+
filePath: config.stateFilePath
|
|
2839
|
+
}, "disposing telegram bot container");
|
|
2331
2840
|
await logger.flush();
|
|
2332
2841
|
}
|
|
2333
2842
|
};
|
|
@@ -2431,6 +2940,11 @@ async function handleTelegramBotPluginEvent(runtime, event) {
|
|
|
2431
2940
|
}
|
|
2432
2941
|
}
|
|
2433
2942
|
async function handlePermissionAsked(runtime, request) {
|
|
2943
|
+
const logger = runtime.container.logger.child({
|
|
2944
|
+
component: "plugin-event",
|
|
2945
|
+
requestId: request.id,
|
|
2946
|
+
sessionId: request.sessionID
|
|
2947
|
+
});
|
|
2434
2948
|
const bindings = await runtime.container.sessionRepo.listBySessionId(request.sessionID);
|
|
2435
2949
|
const chatIds = new Set([...bindings.map((binding) => binding.chatId), ...runtime.container.foregroundSessionTracker.listChatIds(request.sessionID)]);
|
|
2436
2950
|
const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(request.id);
|
|
@@ -2451,35 +2965,45 @@ async function handlePermissionAsked(runtime, request) {
|
|
|
2451
2965
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2452
2966
|
});
|
|
2453
2967
|
} catch (error) {
|
|
2454
|
-
|
|
2968
|
+
logger.error({
|
|
2455
2969
|
error,
|
|
2456
2970
|
chatId,
|
|
2971
|
+
event: "plugin-event.permission.ask.delivery_failed",
|
|
2457
2972
|
requestId: request.id
|
|
2458
2973
|
}, "failed to deliver permission request to Telegram");
|
|
2459
2974
|
}
|
|
2460
2975
|
}
|
|
2461
2976
|
}
|
|
2462
2977
|
async function handlePermissionReplied(runtime, event) {
|
|
2978
|
+
const logger = runtime.container.logger.child({
|
|
2979
|
+
component: "plugin-event",
|
|
2980
|
+
event: "plugin-event.permission.replied",
|
|
2981
|
+
requestId: event.requestId,
|
|
2982
|
+
sessionId: event.sessionId
|
|
2983
|
+
});
|
|
2463
2984
|
const approvals = await runtime.container.permissionApprovalRepo.listByRequestId(event.requestId);
|
|
2464
2985
|
await Promise.all(approvals.map(async (approval) => {
|
|
2465
2986
|
try {
|
|
2466
2987
|
await runtime.bot.api.editMessageText(approval.chatId, approval.messageId, buildPermissionApprovalResolvedMessage(event.requestId, event.reply));
|
|
2467
2988
|
} catch (error) {
|
|
2468
|
-
|
|
2989
|
+
logger.warn({
|
|
2469
2990
|
error,
|
|
2470
2991
|
chatId: approval.chatId,
|
|
2471
|
-
|
|
2472
|
-
sessionId: event.sessionId
|
|
2992
|
+
event: "plugin-event.permission.reply_message_failed"
|
|
2473
2993
|
}, "failed to update Telegram permission message");
|
|
2474
2994
|
}
|
|
2475
2995
|
await runtime.container.permissionApprovalRepo.set(toResolvedApproval(approval, event.reply));
|
|
2476
2996
|
}));
|
|
2477
2997
|
}
|
|
2478
2998
|
async function handleSessionError(runtime, event) {
|
|
2479
|
-
|
|
2480
|
-
|
|
2999
|
+
const logger = runtime.container.logger.child({
|
|
3000
|
+
component: "plugin-event",
|
|
3001
|
+
sessionId: event.sessionId
|
|
3002
|
+
});
|
|
3003
|
+
if (runtime.container.foregroundSessionTracker.fail(event.sessionId, normalizeForegroundSessionError(event.error))) {
|
|
3004
|
+
logger.warn({
|
|
2481
3005
|
error: event.error,
|
|
2482
|
-
|
|
3006
|
+
event: "plugin-event.session.error.foreground_suppressed"
|
|
2483
3007
|
}, "session error suppressed for foreground Telegram session");
|
|
2484
3008
|
return;
|
|
2485
3009
|
}
|
|
@@ -2487,8 +3011,12 @@ async function handleSessionError(runtime, event) {
|
|
|
2487
3011
|
await notifyBoundChats(runtime, event.sessionId, `Session failed.\n\nSession: ${event.sessionId}\nError: ${message}`);
|
|
2488
3012
|
}
|
|
2489
3013
|
async function handleSessionIdle(runtime, event) {
|
|
3014
|
+
const logger = runtime.container.logger.child({
|
|
3015
|
+
component: "plugin-event",
|
|
3016
|
+
sessionId: event.sessionId
|
|
3017
|
+
});
|
|
2490
3018
|
if (runtime.container.foregroundSessionTracker.clear(event.sessionId)) {
|
|
2491
|
-
|
|
3019
|
+
logPluginEvent(logger, { event: "plugin-event.session.idle.foreground_suppressed" }, "session idle notification suppressed for foreground Telegram session");
|
|
2492
3020
|
return;
|
|
2493
3021
|
}
|
|
2494
3022
|
await notifyBoundChats(runtime, event.sessionId, `Session finished.\n\nSession: ${event.sessionId}`);
|
|
@@ -2498,16 +3026,20 @@ async function handleSessionStatus(runtime, event) {
|
|
|
2498
3026
|
await handleSessionIdle(runtime, event);
|
|
2499
3027
|
}
|
|
2500
3028
|
async function notifyBoundChats(runtime, sessionId, text) {
|
|
3029
|
+
const logger = runtime.container.logger.child({
|
|
3030
|
+
component: "plugin-event",
|
|
3031
|
+
sessionId
|
|
3032
|
+
});
|
|
2501
3033
|
const bindings = await runtime.container.sessionRepo.listBySessionId(sessionId);
|
|
2502
3034
|
const chatIds = [...new Set(bindings.map((binding) => binding.chatId))];
|
|
2503
3035
|
await Promise.all(chatIds.map(async (chatId) => {
|
|
2504
3036
|
try {
|
|
2505
3037
|
await runtime.bot.api.sendMessage(chatId, text);
|
|
2506
3038
|
} catch (error) {
|
|
2507
|
-
|
|
3039
|
+
logger.warn({
|
|
2508
3040
|
error,
|
|
2509
3041
|
chatId,
|
|
2510
|
-
|
|
3042
|
+
event: "plugin-event.session.notify_failed"
|
|
2511
3043
|
}, "failed to notify Telegram chat about session event");
|
|
2512
3044
|
}
|
|
2513
3045
|
}));
|
|
@@ -2586,6 +3118,16 @@ function extractSessionErrorMessage(error) {
|
|
|
2586
3118
|
if (isPlainRecord(error.data) && typeof error.data.message === "string" && error.data.message.trim().length > 0) return error.data.message.trim();
|
|
2587
3119
|
return asNonEmptyString(error.name);
|
|
2588
3120
|
}
|
|
3121
|
+
function normalizeForegroundSessionError(error) {
|
|
3122
|
+
if (error instanceof Error) return error;
|
|
3123
|
+
if (isPlainRecord(error) && typeof error.name === "string" && error.name.trim().length > 0) {
|
|
3124
|
+
const normalized = new Error(extractSessionErrorMessage(error) ?? "Unknown session error.");
|
|
3125
|
+
normalized.name = error.name.trim();
|
|
3126
|
+
if (isPlainRecord(error.data)) normalized.data = error.data;
|
|
3127
|
+
return normalized;
|
|
3128
|
+
}
|
|
3129
|
+
return /* @__PURE__ */ new Error("Unknown session error.");
|
|
3130
|
+
}
|
|
2589
3131
|
function asNonEmptyString(value) {
|
|
2590
3132
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
2591
3133
|
}
|
|
@@ -3284,7 +3826,9 @@ var TELEGRAM_COMMAND_SYNC_SCOPES = [{ type: "default" }, { type: "all_private_ch
|
|
|
3284
3826
|
async function syncTelegramCommands(bot, logger) {
|
|
3285
3827
|
await Promise.all(TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => bot.api.setMyCommands(TELEGRAM_COMMANDS, { scope })));
|
|
3286
3828
|
logger.info({
|
|
3829
|
+
component: "runtime",
|
|
3287
3830
|
commands: TELEGRAM_COMMANDS.map((command) => command.command),
|
|
3831
|
+
event: "runtime.commands.synced",
|
|
3288
3832
|
scopes: TELEGRAM_COMMAND_SYNC_SCOPES.map((scope) => scope.type)
|
|
3289
3833
|
}, "telegram commands synced");
|
|
3290
3834
|
}
|
|
@@ -3295,6 +3839,44 @@ async function syncTelegramCommandsForChat(api, chatId, language) {
|
|
|
3295
3839
|
} });
|
|
3296
3840
|
}
|
|
3297
3841
|
//#endregion
|
|
3842
|
+
//#region src/bot/logger-context.ts
|
|
3843
|
+
function buildTelegramLoggerContext(ctx, component = "telegram") {
|
|
3844
|
+
const updateId = typeof ctx.update?.update_id === "number" ? ctx.update.update_id : void 0;
|
|
3845
|
+
const command = extractTelegramCommand(resolveMessageText(ctx));
|
|
3846
|
+
const callbackData = normalizeTelegramString(ctx.callbackQuery?.data);
|
|
3847
|
+
const operationId = typeof updateId === "number" ? `telegram-${updateId}` : null;
|
|
3848
|
+
return {
|
|
3849
|
+
component,
|
|
3850
|
+
...typeof ctx.chat?.id === "number" ? { chatId: ctx.chat.id } : {},
|
|
3851
|
+
...typeof updateId === "number" ? { updateId } : {},
|
|
3852
|
+
...command ? { command } : {},
|
|
3853
|
+
...callbackData ? { callbackData } : {},
|
|
3854
|
+
correlationId: typeof updateId === "number" ? String(updateId) : operationId,
|
|
3855
|
+
operationId
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
function scopeLoggerToTelegramContext(logger, ctx, component = "telegram") {
|
|
3859
|
+
return logger.child(buildTelegramLoggerContext(ctx, component));
|
|
3860
|
+
}
|
|
3861
|
+
function scopeDependenciesToTelegramContext(dependencies, ctx, component = "telegram") {
|
|
3862
|
+
return {
|
|
3863
|
+
...dependencies,
|
|
3864
|
+
logger: scopeLoggerToTelegramContext(dependencies.logger, ctx, component)
|
|
3865
|
+
};
|
|
3866
|
+
}
|
|
3867
|
+
function resolveMessageText(ctx) {
|
|
3868
|
+
return normalizeTelegramString(ctx.message?.text) ?? normalizeTelegramString(ctx.msg?.text);
|
|
3869
|
+
}
|
|
3870
|
+
function extractTelegramCommand(value) {
|
|
3871
|
+
if (!value || !value.startsWith("/")) return null;
|
|
3872
|
+
const token = value.split(/\s+/u, 1)[0]?.trim();
|
|
3873
|
+
if (!token) return null;
|
|
3874
|
+
return token.replace(/^\/+/u, "").split("@", 1)[0] ?? null;
|
|
3875
|
+
}
|
|
3876
|
+
function normalizeTelegramString(value) {
|
|
3877
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
3878
|
+
}
|
|
3879
|
+
//#endregion
|
|
3298
3880
|
//#region src/bot/presenters/error.presenter.ts
|
|
3299
3881
|
function presentError(error, copy = BOT_COPY) {
|
|
3300
3882
|
const presented = normalizeError(error, copy);
|
|
@@ -3411,16 +3993,12 @@ function stringifyUnknown(value) {
|
|
|
3411
3993
|
//#endregion
|
|
3412
3994
|
//#region src/bot/error-boundary.ts
|
|
3413
3995
|
function extractTelegramUpdateContext(ctx) {
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
...typeof chatId === "number" ? { chatId } : {},
|
|
3421
|
-
...typeof messageText === "string" && messageText.trim().length > 0 ? { messageText } : {},
|
|
3422
|
-
...typeof callbackData === "string" && callbackData.trim().length > 0 ? { callbackData } : {}
|
|
3423
|
-
};
|
|
3996
|
+
return buildTelegramLoggerContext({
|
|
3997
|
+
callbackQuery: { data: getNestedString(ctx, ["callbackQuery", "data"]) },
|
|
3998
|
+
chat: { id: getNestedNumber(ctx, ["chat", "id"]) ?? void 0 },
|
|
3999
|
+
message: { text: getNestedString(ctx, ["message", "text"]) },
|
|
4000
|
+
update: { update_id: getNestedNumber(ctx, ["update", "update_id"]) ?? void 0 }
|
|
4001
|
+
});
|
|
3424
4002
|
}
|
|
3425
4003
|
async function replyWithDefaultTelegramError(ctx, logger, error) {
|
|
3426
4004
|
const text = presentError(error, BOT_COPY);
|
|
@@ -3985,7 +4563,7 @@ async function handleAgentsCommand(ctx, dependencies) {
|
|
|
3985
4563
|
}
|
|
3986
4564
|
function registerAgentsCommand(bot, dependencies) {
|
|
3987
4565
|
bot.command(["agents", "agent"], async (ctx) => {
|
|
3988
|
-
await handleAgentsCommand(ctx, dependencies);
|
|
4566
|
+
await handleAgentsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
3989
4567
|
});
|
|
3990
4568
|
}
|
|
3991
4569
|
//#endregion
|
|
@@ -4128,7 +4706,7 @@ async function handleCancelCommand(ctx, dependencies) {
|
|
|
4128
4706
|
}
|
|
4129
4707
|
function registerCancelCommand(bot, dependencies) {
|
|
4130
4708
|
bot.command("cancel", async (ctx) => {
|
|
4131
|
-
await handleCancelCommand(ctx, dependencies);
|
|
4709
|
+
await handleCancelCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4132
4710
|
});
|
|
4133
4711
|
}
|
|
4134
4712
|
//#endregion
|
|
@@ -4175,7 +4753,7 @@ async function presentLanguageSwitchForChat(chatId, api, language, dependencies)
|
|
|
4175
4753
|
}
|
|
4176
4754
|
function registerLanguageCommand(bot, dependencies) {
|
|
4177
4755
|
bot.command("language", async (ctx) => {
|
|
4178
|
-
await handleLanguageCommand(ctx, dependencies);
|
|
4756
|
+
await handleLanguageCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4179
4757
|
});
|
|
4180
4758
|
}
|
|
4181
4759
|
//#endregion
|
|
@@ -4203,7 +4781,7 @@ async function handleModelsCommand(ctx, dependencies) {
|
|
|
4203
4781
|
}
|
|
4204
4782
|
function registerModelsCommand(bot, dependencies) {
|
|
4205
4783
|
bot.command(["model", "models"], async (ctx) => {
|
|
4206
|
-
await handleModelsCommand(ctx, dependencies);
|
|
4784
|
+
await handleModelsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4207
4785
|
});
|
|
4208
4786
|
}
|
|
4209
4787
|
//#endregion
|
|
@@ -4224,7 +4802,7 @@ async function handleNewCommand(ctx, dependencies) {
|
|
|
4224
4802
|
}
|
|
4225
4803
|
function registerNewCommand(bot, dependencies) {
|
|
4226
4804
|
bot.command("new", async (ctx) => {
|
|
4227
|
-
await handleNewCommand(ctx, dependencies);
|
|
4805
|
+
await handleNewCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4228
4806
|
});
|
|
4229
4807
|
}
|
|
4230
4808
|
function extractSessionTitle(ctx) {
|
|
@@ -4245,7 +4823,7 @@ var MARKDOWN_SPECIAL_CHARACTERS = /([_*\[\]()~`>#+\-=|{}.!\\])/g;
|
|
|
4245
4823
|
function buildTelegramPromptReply(result, copy = BOT_COPY) {
|
|
4246
4824
|
const renderedMarkdown = result.bodyMd ? renderMarkdownToTelegramMarkdownV2(result.bodyMd) : null;
|
|
4247
4825
|
const footerPlain = formatPlainMetricsFooter(result.metrics, copy);
|
|
4248
|
-
const fallback = { text: joinBodyAndFooter(truncatePlainBody(normalizePlainBody(result,
|
|
4826
|
+
const fallback = { text: joinBodyAndFooter(truncatePlainBody(normalizePlainBody(result, copy), footerPlain), footerPlain) };
|
|
4249
4827
|
if (!renderedMarkdown) return {
|
|
4250
4828
|
preferred: fallback,
|
|
4251
4829
|
fallback
|
|
@@ -4349,24 +4927,10 @@ function renderMarkdownToTelegramMarkdownV2(markdown) {
|
|
|
4349
4927
|
if (inCodeBlock) return null;
|
|
4350
4928
|
return rendered.join("\n");
|
|
4351
4929
|
}
|
|
4352
|
-
function
|
|
4353
|
-
const lines = preprocessMarkdownForTelegram(markdown).split("\n");
|
|
4354
|
-
const rendered = [];
|
|
4355
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
4356
|
-
const tableBlock = consumeMarkdownTable(lines, index);
|
|
4357
|
-
if (tableBlock) {
|
|
4358
|
-
rendered.push(renderTableAsPlainText(tableBlock.rows));
|
|
4359
|
-
index = tableBlock.nextIndex - 1;
|
|
4360
|
-
continue;
|
|
4361
|
-
}
|
|
4362
|
-
rendered.push(lines[index]);
|
|
4363
|
-
}
|
|
4364
|
-
return rendered.join("\n").replace(/```[A-Za-z0-9_-]*\n?/g, "").replace(/```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/^\s*>\s?/gm, "").replace(/^(\s*)[-+*]\s+/gm, "$1- ").replace(/^(\s*)(\d+)[.)]\s+/gm, "$1$2. ").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").trim();
|
|
4365
|
-
}
|
|
4366
|
-
function normalizePlainBody(result, preferStructured, copy) {
|
|
4930
|
+
function normalizePlainBody(result, copy) {
|
|
4367
4931
|
const fromStructured = result.bodyMd ? stripMarkdownToPlainText(result.bodyMd) : "";
|
|
4368
4932
|
const fromFallback = result.fallbackText.trim();
|
|
4369
|
-
return (
|
|
4933
|
+
return (fromStructured || fromFallback).trim() || result.fallbackText || copy.prompt.emptyResponse;
|
|
4370
4934
|
}
|
|
4371
4935
|
function truncatePlainBody(body, footer) {
|
|
4372
4936
|
const reservedLength = footer.length + 2;
|
|
@@ -4490,9 +5054,6 @@ function renderTableAsTelegramCodeBlock(rows) {
|
|
|
4490
5054
|
"```"
|
|
4491
5055
|
].join("\n");
|
|
4492
5056
|
}
|
|
4493
|
-
function renderTableAsPlainText(rows) {
|
|
4494
|
-
return buildAlignedTableLines(rows).join("\n");
|
|
4495
|
-
}
|
|
4496
5057
|
function buildAlignedTableLines(rows) {
|
|
4497
5058
|
const columnWidths = calculateTableColumnWidths(rows);
|
|
4498
5059
|
return [
|
|
@@ -4589,7 +5150,7 @@ async function handleStatusCommand(ctx, dependencies) {
|
|
|
4589
5150
|
}
|
|
4590
5151
|
function registerStatusCommand(bot, dependencies) {
|
|
4591
5152
|
bot.command("status", async (ctx) => {
|
|
4592
|
-
await handleStatusCommand(ctx, dependencies);
|
|
5153
|
+
await handleStatusCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4593
5154
|
});
|
|
4594
5155
|
}
|
|
4595
5156
|
//#endregion
|
|
@@ -4607,7 +5168,7 @@ async function handleSessionsCommand(ctx, dependencies) {
|
|
|
4607
5168
|
}
|
|
4608
5169
|
function registerSessionsCommand(bot, dependencies) {
|
|
4609
5170
|
bot.command("sessions", async (ctx) => {
|
|
4610
|
-
await handleSessionsCommand(ctx, dependencies);
|
|
5171
|
+
await handleSessionsCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4611
5172
|
});
|
|
4612
5173
|
}
|
|
4613
5174
|
//#endregion
|
|
@@ -4634,7 +5195,7 @@ async function handleStartCommand(ctx, dependencies) {
|
|
|
4634
5195
|
}
|
|
4635
5196
|
function registerStartCommand(bot, dependencies) {
|
|
4636
5197
|
bot.command("start", async (ctx) => {
|
|
4637
|
-
await handleStartCommand(ctx, dependencies);
|
|
5198
|
+
await handleStartCommand(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4638
5199
|
});
|
|
4639
5200
|
}
|
|
4640
5201
|
//#endregion
|
|
@@ -4900,19 +5461,19 @@ async function handlePermissionApprovalCallback(ctx, dependencies) {
|
|
|
4900
5461
|
}
|
|
4901
5462
|
function registerCallbackHandler(bot, dependencies) {
|
|
4902
5463
|
bot.callbackQuery(/^agents:/, async (ctx) => {
|
|
4903
|
-
await handleAgentsCallback(ctx, dependencies);
|
|
5464
|
+
await handleAgentsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4904
5465
|
});
|
|
4905
5466
|
bot.callbackQuery(/^sessions:/, async (ctx) => {
|
|
4906
|
-
await handleSessionsCallback(ctx, dependencies);
|
|
5467
|
+
await handleSessionsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4907
5468
|
});
|
|
4908
5469
|
bot.callbackQuery(/^model:/, async (ctx) => {
|
|
4909
|
-
await handleModelsCallback(ctx, dependencies);
|
|
5470
|
+
await handleModelsCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4910
5471
|
});
|
|
4911
5472
|
bot.callbackQuery(/^language:/, async (ctx) => {
|
|
4912
|
-
await handleLanguageCallback(ctx, dependencies);
|
|
5473
|
+
await handleLanguageCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4913
5474
|
});
|
|
4914
5475
|
bot.callbackQuery(/^permission:/, async (ctx) => {
|
|
4915
|
-
await handlePermissionApprovalCallback(ctx, dependencies);
|
|
5476
|
+
await handlePermissionApprovalCallback(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
4916
5477
|
});
|
|
4917
5478
|
}
|
|
4918
5479
|
function parseSessionActionTarget(data, prefix) {
|
|
@@ -4949,7 +5510,7 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
|
|
|
4949
5510
|
},
|
|
4950
5511
|
signal: foregroundRequest.signal,
|
|
4951
5512
|
text: promptInput.text
|
|
4952
|
-
})).assistantReply, copy
|
|
5513
|
+
})).assistantReply, copy), copy);
|
|
4953
5514
|
try {
|
|
4954
5515
|
await ctx.reply(telegramReply.preferred.text, telegramReply.preferred.options);
|
|
4955
5516
|
} catch (replyError) {
|
|
@@ -4970,26 +5531,14 @@ async function executePromptRequest(ctx, dependencies, resolvePrompt) {
|
|
|
4970
5531
|
}
|
|
4971
5532
|
}
|
|
4972
5533
|
}
|
|
4973
|
-
function normalizePromptReplyForDisplay(promptReply, copy
|
|
5534
|
+
function normalizePromptReplyForDisplay(promptReply, copy) {
|
|
4974
5535
|
if (!promptReply.assistantError) return promptReply;
|
|
4975
|
-
if (isRecoverableStructuredOutputError(promptReply)) {
|
|
4976
|
-
dependencies.logger.warn?.({ error: promptReply.assistantError }, "structured output validation failed, falling back to assistant text reply");
|
|
4977
|
-
return {
|
|
4978
|
-
...promptReply,
|
|
4979
|
-
assistantError: null
|
|
4980
|
-
};
|
|
4981
|
-
}
|
|
4982
5536
|
return {
|
|
4983
5537
|
...promptReply,
|
|
4984
5538
|
bodyMd: null,
|
|
4985
5539
|
fallbackText: presentError(promptReply.assistantError, copy)
|
|
4986
5540
|
};
|
|
4987
5541
|
}
|
|
4988
|
-
function isRecoverableStructuredOutputError(promptReply) {
|
|
4989
|
-
if (promptReply.assistantError?.name !== "StructuredOutputError") return false;
|
|
4990
|
-
if (promptReply.bodyMd?.trim()) return true;
|
|
4991
|
-
return promptReply.parts.some((part) => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
|
|
4992
|
-
}
|
|
4993
5542
|
//#endregion
|
|
4994
5543
|
//#region src/bot/handlers/file.handler.ts
|
|
4995
5544
|
var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
@@ -5014,10 +5563,10 @@ async function handleImageMessage(ctx, dependencies) {
|
|
|
5014
5563
|
}
|
|
5015
5564
|
function registerFileHandler(bot, dependencies) {
|
|
5016
5565
|
bot.on("message:photo", async (ctx) => {
|
|
5017
|
-
await handleImageMessage(ctx, dependencies);
|
|
5566
|
+
await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
5018
5567
|
});
|
|
5019
5568
|
bot.on("message:document", async (ctx) => {
|
|
5020
|
-
await handleImageMessage(ctx, dependencies);
|
|
5569
|
+
await handleImageMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
5021
5570
|
});
|
|
5022
5571
|
}
|
|
5023
5572
|
function resolveTelegramImage(message) {
|
|
@@ -5059,7 +5608,7 @@ async function handleTextMessage(ctx, dependencies) {
|
|
|
5059
5608
|
}
|
|
5060
5609
|
function registerMessageHandler(bot, dependencies) {
|
|
5061
5610
|
bot.on("message:text", async (ctx) => {
|
|
5062
|
-
await handleTextMessage(ctx, dependencies);
|
|
5611
|
+
await handleTextMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
5063
5612
|
});
|
|
5064
5613
|
}
|
|
5065
5614
|
//#endregion
|
|
@@ -5072,7 +5621,7 @@ async function handleVoiceMessage(ctx, dependencies) {
|
|
|
5072
5621
|
}
|
|
5073
5622
|
function registerVoiceHandler(bot, dependencies) {
|
|
5074
5623
|
bot.on("message:voice", async (ctx) => {
|
|
5075
|
-
await handleVoiceMessage(ctx, dependencies);
|
|
5624
|
+
await handleVoiceMessage(ctx, scopeDependenciesToTelegramContext(dependencies, ctx, "telegram"));
|
|
5076
5625
|
});
|
|
5077
5626
|
}
|
|
5078
5627
|
//#endregion
|
|
@@ -5093,17 +5642,18 @@ function createAuthMiddleware(allowedChatIds) {
|
|
|
5093
5642
|
function buildIncomingUpdateLogFields(ctx) {
|
|
5094
5643
|
const messageText = ctx.msg && "text" in ctx.msg ? ctx.msg.text : void 0;
|
|
5095
5644
|
return {
|
|
5645
|
+
...buildTelegramLoggerContext(ctx),
|
|
5646
|
+
event: "telegram.update.received",
|
|
5096
5647
|
updateId: ctx.update.update_id,
|
|
5097
5648
|
chatId: ctx.chat?.id,
|
|
5098
5649
|
fromId: ctx.from?.id,
|
|
5099
5650
|
hasText: typeof messageText === "string" && messageText.length > 0,
|
|
5100
|
-
textLength: typeof messageText === "string" ? messageText.length : 0
|
|
5101
|
-
textPreview: typeof messageText === "string" && messageText.length > 0 ? createRedactedPreview(messageText) : void 0
|
|
5651
|
+
textLength: typeof messageText === "string" ? messageText.length : 0
|
|
5102
5652
|
};
|
|
5103
5653
|
}
|
|
5104
5654
|
function createLoggingMiddleware(logger) {
|
|
5105
5655
|
return async (ctx, next) => {
|
|
5106
|
-
logger
|
|
5656
|
+
logTelegramUpdate(logger, { ...buildIncomingUpdateLogFields(ctx) }, "incoming update");
|
|
5107
5657
|
return next();
|
|
5108
5658
|
};
|
|
5109
5659
|
}
|
|
@@ -5113,11 +5663,13 @@ function registerBot(bot, container, options) {
|
|
|
5113
5663
|
bot.use(createLoggingMiddleware(container.logger));
|
|
5114
5664
|
bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
|
|
5115
5665
|
const safeBot = bot.errorBoundary(async (error) => {
|
|
5116
|
-
container.logger.
|
|
5666
|
+
const scopedLogger = scopeLoggerToTelegramContext(container.logger, error.ctx, "telegram");
|
|
5667
|
+
scopedLogger.error({
|
|
5117
5668
|
...extractTelegramUpdateContext(error.ctx),
|
|
5669
|
+
event: "telegram.middleware.failed",
|
|
5118
5670
|
error: error.error
|
|
5119
5671
|
}, "telegram middleware failed");
|
|
5120
|
-
await replyWithDefaultTelegramError(error.ctx,
|
|
5672
|
+
await replyWithDefaultTelegramError(error.ctx, scopedLogger, error.error);
|
|
5121
5673
|
});
|
|
5122
5674
|
registerStartCommand(safeBot, container);
|
|
5123
5675
|
registerStatusCommand(safeBot, container);
|
|
@@ -5144,8 +5696,10 @@ async function startTelegramBotRuntime(input) {
|
|
|
5144
5696
|
const runtimeKey = buildTelegramRuntimeKey(input.config);
|
|
5145
5697
|
const registry = getTelegramBotRuntimeRegistry();
|
|
5146
5698
|
const existingRuntime = registry.activeByKey.get(runtimeKey);
|
|
5699
|
+
const runtimeLogger = input.container.logger.child({ component: "runtime" });
|
|
5147
5700
|
if (existingRuntime) {
|
|
5148
|
-
|
|
5701
|
+
runtimeLogger.warn({
|
|
5702
|
+
event: "runtime.reused",
|
|
5149
5703
|
runtimeKey,
|
|
5150
5704
|
telegramApiRoot: input.config.telegramApiRoot
|
|
5151
5705
|
}, "telegram runtime already active in this process; reusing the existing runner");
|
|
@@ -5163,13 +5717,18 @@ async function startTelegramBotRuntime(input) {
|
|
|
5163
5717
|
}
|
|
5164
5718
|
async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime) {
|
|
5165
5719
|
const bot = (input.botFactory ?? ((token, options) => new Bot(token, options)))(input.config.telegramBotToken, { client: { apiRoot: input.config.telegramApiRoot } });
|
|
5720
|
+
const runtimeLogger = input.container.logger.child({ component: "runtime" });
|
|
5166
5721
|
wrapTelegramGetUpdates(bot, input.container);
|
|
5167
5722
|
(input.registerBotHandlers ?? registerBot)(bot, input.container, { telegramAllowedChatIds: input.config.telegramAllowedChatIds });
|
|
5168
5723
|
bot.catch((error) => {
|
|
5169
5724
|
const metadata = extractTelegramUpdateContext(error.ctx);
|
|
5725
|
+
const telegramLogger = input.container.logger.child({
|
|
5726
|
+
component: "telegram",
|
|
5727
|
+
...metadata
|
|
5728
|
+
});
|
|
5170
5729
|
if (error.error instanceof GrammyError) {
|
|
5171
|
-
|
|
5172
|
-
|
|
5730
|
+
telegramLogger.error({
|
|
5731
|
+
event: "telegram.api.error",
|
|
5173
5732
|
errorCode: error.error.error_code,
|
|
5174
5733
|
description: error.error.description,
|
|
5175
5734
|
method: error.error.method,
|
|
@@ -5179,24 +5738,28 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
|
|
|
5179
5738
|
return;
|
|
5180
5739
|
}
|
|
5181
5740
|
if (error.error instanceof HttpError) {
|
|
5182
|
-
|
|
5183
|
-
|
|
5741
|
+
telegramLogger.error({
|
|
5742
|
+
event: "telegram.http.error",
|
|
5184
5743
|
error: error.error.error,
|
|
5185
5744
|
message: error.error.message
|
|
5186
5745
|
}, "telegram bot network request failed");
|
|
5187
5746
|
return;
|
|
5188
5747
|
}
|
|
5189
|
-
|
|
5190
|
-
|
|
5748
|
+
telegramLogger.error({
|
|
5749
|
+
event: "telegram.update.failed",
|
|
5191
5750
|
error: error.error
|
|
5192
5751
|
}, "telegram bot update failed");
|
|
5193
5752
|
});
|
|
5194
|
-
|
|
5753
|
+
runtimeLogger.info({
|
|
5754
|
+
event: "runtime.polling.starting",
|
|
5755
|
+
runtimeKey
|
|
5756
|
+
}, "telegram bot polling starting");
|
|
5195
5757
|
const runner = (input.runBot ?? run)(bot, TELEGRAM_RUNNER_OPTIONS);
|
|
5196
5758
|
let stopped = false;
|
|
5197
5759
|
let disposed = false;
|
|
5198
5760
|
if (input.syncCommands ?? true) (input.syncCommandsHandler ?? syncTelegramCommands)(bot, input.container.logger).catch((error) => {
|
|
5199
|
-
|
|
5761
|
+
runtimeLogger.warn({
|
|
5762
|
+
event: "runtime.commands.sync_failed",
|
|
5200
5763
|
error,
|
|
5201
5764
|
runtimeKey
|
|
5202
5765
|
}, "failed to sync telegram commands; polling continues without command registration updates");
|
|
@@ -5206,7 +5769,8 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
|
|
|
5206
5769
|
if (stopped) return;
|
|
5207
5770
|
stopped = true;
|
|
5208
5771
|
stopPromise = runner.stop().catch((error) => {
|
|
5209
|
-
|
|
5772
|
+
runtimeLogger.warn({
|
|
5773
|
+
event: "runtime.stop.failed",
|
|
5210
5774
|
error,
|
|
5211
5775
|
runtimeKey
|
|
5212
5776
|
}, "failed to stop telegram runner cleanly");
|
|
@@ -5235,6 +5799,7 @@ async function startTelegramBotRuntimeInternal(input, runtimeKey, releaseRuntime
|
|
|
5235
5799
|
}
|
|
5236
5800
|
function wrapTelegramGetUpdates(bot, container) {
|
|
5237
5801
|
const originalGetUpdates = bot.api.getUpdates.bind(bot.api);
|
|
5802
|
+
const runtimeLogger = container.logger.child({ component: "runtime" });
|
|
5238
5803
|
bot.api.getUpdates = async (options, signal) => {
|
|
5239
5804
|
const requestOptions = options ?? {
|
|
5240
5805
|
limit: 100,
|
|
@@ -5244,7 +5809,8 @@ function wrapTelegramGetUpdates(bot, container) {
|
|
|
5244
5809
|
try {
|
|
5245
5810
|
return await originalGetUpdates(requestOptions, signal);
|
|
5246
5811
|
} catch (error) {
|
|
5247
|
-
|
|
5812
|
+
runtimeLogger.warn({
|
|
5813
|
+
event: "runtime.telegram.get_updates_failed",
|
|
5248
5814
|
error,
|
|
5249
5815
|
limit: requestOptions.limit,
|
|
5250
5816
|
offset: requestOptions.offset,
|
|
@@ -5308,8 +5874,9 @@ async function startPluginRuntime(options, cwd) {
|
|
|
5308
5874
|
});
|
|
5309
5875
|
const { config, container } = bootstrapApp(options.context.client, preparedConfiguration.config, { cwd: preparedConfiguration.cwd });
|
|
5310
5876
|
try {
|
|
5311
|
-
if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.warn({
|
|
5877
|
+
if (preparedConfiguration.ignoredProjectConfigFilePath) container.logger.child({ component: "runtime" }).warn({
|
|
5312
5878
|
cwd: preparedConfiguration.cwd,
|
|
5879
|
+
event: "runtime.config.legacy_worktree_ignored",
|
|
5313
5880
|
ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
|
|
5314
5881
|
globalConfigFilePath: preparedConfiguration.globalConfigFilePath
|
|
5315
5882
|
}, "legacy worktree plugin config is ignored; migrate settings to the global opencode-tbot config");
|
|
@@ -5317,8 +5884,9 @@ async function startPluginRuntime(options, cwd) {
|
|
|
5317
5884
|
config,
|
|
5318
5885
|
container
|
|
5319
5886
|
});
|
|
5320
|
-
container.logger.info({
|
|
5887
|
+
container.logger.child({ component: "runtime" }).info({
|
|
5321
5888
|
cwd: preparedConfiguration.cwd,
|
|
5889
|
+
event: "runtime.plugin.started",
|
|
5322
5890
|
globalConfigFilePath: preparedConfiguration.globalConfigFilePath,
|
|
5323
5891
|
ignoredProjectConfigFilePath: preparedConfiguration.ignoredProjectConfigFilePath,
|
|
5324
5892
|
configFilePath: preparedConfiguration.configFilePath,
|