opencode-tbot 0.1.11 → 0.1.13
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 +1 -2
- package/README.zh-CN.md +1 -2
- package/dist/plugin.js +331 -250
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -211,6 +211,14 @@ function buildOpenCodeSdkConfig(options) {
|
|
|
211
211
|
...apiKey ? { auth: apiKey } : {}
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
|
+
var OpenCodePromptTimeoutError = class extends Error {
|
|
215
|
+
data;
|
|
216
|
+
constructor(data) {
|
|
217
|
+
super(data.message ?? "OpenCode prompt timed out.");
|
|
218
|
+
this.name = "OpenCodePromptTimeoutError";
|
|
219
|
+
this.data = data;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
214
222
|
var EMPTY_RESPONSE_TEXT = "OpenCode returned empty response.";
|
|
215
223
|
var PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS = [
|
|
216
224
|
0,
|
|
@@ -220,8 +228,11 @@ var PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS = [
|
|
|
220
228
|
1e3
|
|
221
229
|
];
|
|
222
230
|
var PROMPT_MESSAGE_POLL_INTERVAL_MS = 2e3;
|
|
231
|
+
var PROMPT_POLL_REQUEST_TIMEOUT_MS = 15e3;
|
|
232
|
+
var PROMPT_SEND_TIMEOUT_MS = 3e4;
|
|
223
233
|
var PROMPT_MESSAGE_POLL_TIMEOUT_MS = 6e4;
|
|
224
234
|
var PROMPT_MESSAGE_POLL_LIMIT = 20;
|
|
235
|
+
var PROMPT_LOG_SERVICE = "opencode-tbot";
|
|
225
236
|
var STRUCTURED_REPLY_SCHEMA = {
|
|
226
237
|
type: "json_schema",
|
|
227
238
|
retryCount: 2,
|
|
@@ -243,6 +254,11 @@ var StructuredReplySchema = z.object({ body_md: z.string() });
|
|
|
243
254
|
var OpenCodeClient = class {
|
|
244
255
|
client;
|
|
245
256
|
fetchFn;
|
|
257
|
+
promptRequestTimeouts = {
|
|
258
|
+
pollRequestMs: PROMPT_POLL_REQUEST_TIMEOUT_MS,
|
|
259
|
+
sendMs: PROMPT_SEND_TIMEOUT_MS,
|
|
260
|
+
totalPollMs: PROMPT_MESSAGE_POLL_TIMEOUT_MS
|
|
261
|
+
};
|
|
246
262
|
modelCache = {
|
|
247
263
|
expiresAt: 0,
|
|
248
264
|
promise: null,
|
|
@@ -287,8 +303,7 @@ var OpenCodeClient = class {
|
|
|
287
303
|
return unwrapSdkData(await this.client.mcp.status(directory ? { directory } : void 0, SDK_OPTIONS));
|
|
288
304
|
}
|
|
289
305
|
async getSessionStatuses() {
|
|
290
|
-
|
|
291
|
-
return unwrapSdkData(await this.client.session.status(void 0, SDK_OPTIONS));
|
|
306
|
+
return this.loadSessionStatuses();
|
|
292
307
|
}
|
|
293
308
|
async listProjects() {
|
|
294
309
|
if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", { url: "/project" });
|
|
@@ -394,7 +409,7 @@ var OpenCodeClient = class {
|
|
|
394
409
|
structured
|
|
395
410
|
};
|
|
396
411
|
let bestCandidate = selectPromptResponseCandidate([data], candidateOptions) ?? data;
|
|
397
|
-
const deadlineAt = Date.now() +
|
|
412
|
+
const deadlineAt = Date.now() + this.promptRequestTimeouts.totalPollMs;
|
|
398
413
|
let idleStatusSeen = false;
|
|
399
414
|
let attempt = 0;
|
|
400
415
|
while (true) {
|
|
@@ -412,7 +427,7 @@ var OpenCodeClient = class {
|
|
|
412
427
|
if (!shouldPollPromptMessage(next, structured)) return bestCandidate;
|
|
413
428
|
}
|
|
414
429
|
}
|
|
415
|
-
const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions);
|
|
430
|
+
const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "poll-messages");
|
|
416
431
|
if (latest) {
|
|
417
432
|
bestCandidate = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions) ?? bestCandidate;
|
|
418
433
|
if (!shouldPollPromptMessage(bestCandidate, structured)) return bestCandidate;
|
|
@@ -423,57 +438,114 @@ var OpenCodeClient = class {
|
|
|
423
438
|
}
|
|
424
439
|
if (Date.now() >= deadlineAt) break;
|
|
425
440
|
}
|
|
426
|
-
const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions);
|
|
427
|
-
|
|
441
|
+
const latest = await this.findLatestPromptResponse(input.sessionId, candidateOptions, "final-scan");
|
|
442
|
+
const resolved = selectPromptResponseCandidate([bestCandidate, latest], candidateOptions) ?? bestCandidate;
|
|
443
|
+
if (shouldPollPromptMessage(resolved, structured)) {
|
|
444
|
+
const error = createOpenCodePromptTimeoutError({
|
|
445
|
+
sessionId: input.sessionId,
|
|
446
|
+
stage: "final-scan",
|
|
447
|
+
timeoutMs: this.promptRequestTimeouts.totalPollMs,
|
|
448
|
+
messageId: messageId ?? void 0
|
|
449
|
+
});
|
|
450
|
+
this.logPromptRequestFailure(error, {
|
|
451
|
+
sessionId: input.sessionId,
|
|
452
|
+
stage: "final-scan",
|
|
453
|
+
timeoutMs: this.promptRequestTimeouts.totalPollMs,
|
|
454
|
+
messageId
|
|
455
|
+
});
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
return resolved;
|
|
428
459
|
}
|
|
429
460
|
async fetchPromptMessage(sessionId, messageId) {
|
|
430
461
|
try {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
462
|
+
return await this.runPromptRequestWithTimeout({
|
|
463
|
+
sessionId,
|
|
464
|
+
stage: "poll-message",
|
|
465
|
+
timeoutMs: this.promptRequestTimeouts.pollRequestMs,
|
|
466
|
+
messageId
|
|
467
|
+
}, async (signal) => {
|
|
468
|
+
if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponse(await this.requestRaw("get", {
|
|
469
|
+
url: "/session/{sessionID}/message/{messageID}",
|
|
470
|
+
path: {
|
|
471
|
+
sessionID: sessionId,
|
|
472
|
+
messageID: messageId
|
|
473
|
+
},
|
|
474
|
+
signal
|
|
475
|
+
}));
|
|
476
|
+
if (typeof this.client.session.message !== "function") return null;
|
|
477
|
+
return normalizePromptResponse(unwrapSdkData(await this.client.session.message({
|
|
434
478
|
sessionID: sessionId,
|
|
435
479
|
messageID: messageId
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
480
|
+
}, {
|
|
481
|
+
...SDK_OPTIONS,
|
|
482
|
+
signal
|
|
483
|
+
})));
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
this.logPromptRequestFailure(error, {
|
|
487
|
+
sessionId,
|
|
488
|
+
stage: "poll-message",
|
|
489
|
+
timeoutMs: this.promptRequestTimeouts.pollRequestMs,
|
|
490
|
+
messageId
|
|
491
|
+
});
|
|
444
492
|
return null;
|
|
445
493
|
}
|
|
446
494
|
}
|
|
447
495
|
async captureKnownMessageIds(sessionId) {
|
|
448
|
-
const messages = await this.fetchRecentPromptMessages(sessionId);
|
|
496
|
+
const messages = await this.fetchRecentPromptMessages(sessionId, "capture-known-messages");
|
|
449
497
|
if (!messages) return /* @__PURE__ */ new Set();
|
|
450
498
|
return new Set(messages.map((message) => extractMessageId(message.info)).filter((id) => typeof id === "string" && id.length > 0));
|
|
451
499
|
}
|
|
452
|
-
async fetchRecentPromptMessages(sessionId) {
|
|
500
|
+
async fetchRecentPromptMessages(sessionId, stage) {
|
|
453
501
|
try {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
502
|
+
return await this.runPromptRequestWithTimeout({
|
|
503
|
+
sessionId,
|
|
504
|
+
stage,
|
|
505
|
+
timeoutMs: this.promptRequestTimeouts.pollRequestMs
|
|
506
|
+
}, async (signal) => {
|
|
507
|
+
if (hasRawSdkMethod(this.client, "get")) return normalizePromptResponses(await this.requestRaw("get", {
|
|
508
|
+
url: "/session/{sessionID}/message",
|
|
509
|
+
path: { sessionID: sessionId },
|
|
510
|
+
query: { limit: PROMPT_MESSAGE_POLL_LIMIT },
|
|
511
|
+
signal
|
|
512
|
+
}));
|
|
513
|
+
if (typeof this.client.session.messages !== "function") return null;
|
|
514
|
+
return normalizePromptResponses(unwrapSdkData(await this.client.session.messages({
|
|
515
|
+
sessionID: sessionId,
|
|
516
|
+
limit: PROMPT_MESSAGE_POLL_LIMIT
|
|
517
|
+
}, {
|
|
518
|
+
...SDK_OPTIONS,
|
|
519
|
+
signal
|
|
520
|
+
})));
|
|
521
|
+
});
|
|
522
|
+
} catch (error) {
|
|
523
|
+
this.logPromptRequestFailure(error, {
|
|
524
|
+
sessionId,
|
|
525
|
+
stage,
|
|
526
|
+
timeoutMs: this.promptRequestTimeouts.pollRequestMs
|
|
527
|
+
});
|
|
465
528
|
return null;
|
|
466
529
|
}
|
|
467
530
|
}
|
|
468
531
|
async fetchPromptSessionStatus(sessionId) {
|
|
469
532
|
try {
|
|
470
|
-
return (await this.
|
|
471
|
-
|
|
533
|
+
return (await this.runPromptRequestWithTimeout({
|
|
534
|
+
sessionId,
|
|
535
|
+
stage: "poll-status",
|
|
536
|
+
timeoutMs: this.promptRequestTimeouts.pollRequestMs
|
|
537
|
+
}, async (signal) => this.loadSessionStatuses(signal)))[sessionId] ?? null;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.logPromptRequestFailure(error, {
|
|
540
|
+
sessionId,
|
|
541
|
+
stage: "poll-status",
|
|
542
|
+
timeoutMs: this.promptRequestTimeouts.pollRequestMs
|
|
543
|
+
});
|
|
472
544
|
return null;
|
|
473
545
|
}
|
|
474
546
|
}
|
|
475
|
-
async findLatestPromptResponse(sessionId, options) {
|
|
476
|
-
const messages = await this.fetchRecentPromptMessages(sessionId);
|
|
547
|
+
async findLatestPromptResponse(sessionId, options, stage) {
|
|
548
|
+
const messages = await this.fetchRecentPromptMessages(sessionId, stage);
|
|
477
549
|
if (!messages || messages.length === 0) return null;
|
|
478
550
|
return selectPromptResponseCandidate(messages, options);
|
|
479
551
|
}
|
|
@@ -497,25 +569,44 @@ var OpenCodeClient = class {
|
|
|
497
569
|
return unwrapSdkData(await this.client.config.providers(void 0, SDK_OPTIONS));
|
|
498
570
|
}
|
|
499
571
|
async sendPromptRequest(input, parts) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
572
|
+
try {
|
|
573
|
+
return await this.runPromptRequestWithTimeout({
|
|
574
|
+
sessionId: input.sessionId,
|
|
575
|
+
stage: "send-prompt",
|
|
576
|
+
timeoutMs: this.promptRequestTimeouts.sendMs
|
|
577
|
+
}, async (signal) => {
|
|
578
|
+
if (hasRawSdkMethod(this.client, "post")) return normalizePromptResponse(await this.requestRaw("post", {
|
|
579
|
+
url: "/session/{sessionID}/message",
|
|
580
|
+
path: { sessionID: input.sessionId },
|
|
581
|
+
body: {
|
|
582
|
+
...input.agent ? { agent: input.agent } : {},
|
|
583
|
+
...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
|
|
584
|
+
...input.model ? { model: input.model } : {},
|
|
585
|
+
...input.variant ? { variant: input.variant } : {},
|
|
586
|
+
parts
|
|
587
|
+
},
|
|
588
|
+
signal
|
|
589
|
+
}));
|
|
590
|
+
return normalizePromptResponse(unwrapSdkData(await this.client.session.prompt({
|
|
591
|
+
sessionID: input.sessionId,
|
|
592
|
+
...input.agent ? { agent: input.agent } : {},
|
|
593
|
+
...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
|
|
594
|
+
...input.model ? { model: input.model } : {},
|
|
595
|
+
...input.variant ? { variant: input.variant } : {},
|
|
596
|
+
parts
|
|
597
|
+
}, {
|
|
598
|
+
...SDK_OPTIONS,
|
|
599
|
+
signal
|
|
600
|
+
})));
|
|
601
|
+
});
|
|
602
|
+
} catch (error) {
|
|
603
|
+
this.logPromptRequestFailure(error, {
|
|
604
|
+
sessionId: input.sessionId,
|
|
605
|
+
stage: "send-prompt",
|
|
606
|
+
timeoutMs: this.promptRequestTimeouts.sendMs
|
|
607
|
+
});
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
519
610
|
}
|
|
520
611
|
async requestRaw(method, options) {
|
|
521
612
|
const handler = getRawSdkClient(this.client)?.[method];
|
|
@@ -525,6 +616,67 @@ var OpenCodeClient = class {
|
|
|
525
616
|
...options
|
|
526
617
|
}));
|
|
527
618
|
}
|
|
619
|
+
async loadSessionStatuses(signal) {
|
|
620
|
+
if (hasRawSdkMethod(this.client, "get")) return this.requestRaw("get", {
|
|
621
|
+
url: "/session/status",
|
|
622
|
+
...signal ? { signal } : {}
|
|
623
|
+
});
|
|
624
|
+
return unwrapSdkData(await this.client.session.status(void 0, {
|
|
625
|
+
...SDK_OPTIONS,
|
|
626
|
+
...signal ? { signal } : {}
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
async runPromptRequestWithTimeout(input, operation) {
|
|
630
|
+
const startedAt = Date.now();
|
|
631
|
+
const controller = new AbortController();
|
|
632
|
+
let timeoutHandle = null;
|
|
633
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
634
|
+
timeoutHandle = setTimeout(() => {
|
|
635
|
+
reject(createOpenCodePromptTimeoutError({
|
|
636
|
+
sessionId: input.sessionId,
|
|
637
|
+
stage: input.stage,
|
|
638
|
+
timeoutMs: input.timeoutMs,
|
|
639
|
+
messageId: input.messageId ?? void 0,
|
|
640
|
+
elapsedMs: Date.now() - startedAt
|
|
641
|
+
}));
|
|
642
|
+
controller.abort();
|
|
643
|
+
}, input.timeoutMs);
|
|
644
|
+
});
|
|
645
|
+
try {
|
|
646
|
+
return await Promise.race([operation(controller.signal), timeoutPromise]);
|
|
647
|
+
} finally {
|
|
648
|
+
if (timeoutHandle !== null) clearTimeout(timeoutHandle);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
logPromptRequestFailure(error, input) {
|
|
652
|
+
if (error instanceof OpenCodePromptTimeoutError) {
|
|
653
|
+
this.logPromptRequest("warn", {
|
|
654
|
+
elapsedMs: error.data.elapsedMs,
|
|
655
|
+
messageId: error.data.messageId,
|
|
656
|
+
sessionId: error.data.sessionId,
|
|
657
|
+
stage: error.data.stage,
|
|
658
|
+
timeoutMs: error.data.timeoutMs
|
|
659
|
+
}, "OpenCode prompt request timed out");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
this.logPromptRequest("warn", {
|
|
663
|
+
error,
|
|
664
|
+
messageId: input.messageId ?? void 0,
|
|
665
|
+
sessionId: input.sessionId,
|
|
666
|
+
stage: input.stage,
|
|
667
|
+
timeoutMs: input.timeoutMs
|
|
668
|
+
}, "OpenCode prompt request failed");
|
|
669
|
+
}
|
|
670
|
+
logPromptRequest(level, extra, message) {
|
|
671
|
+
const log = this.client.app?.log;
|
|
672
|
+
if (typeof log !== "function") return;
|
|
673
|
+
log.call(this.client.app, {
|
|
674
|
+
service: PROMPT_LOG_SERVICE,
|
|
675
|
+
level,
|
|
676
|
+
message,
|
|
677
|
+
extra
|
|
678
|
+
}).catch(() => void 0);
|
|
679
|
+
}
|
|
528
680
|
};
|
|
529
681
|
function createOpenCodeClientFromSdkClient(client, fetchFn = fetch) {
|
|
530
682
|
return new OpenCodeClient(void 0, client, fetchFn);
|
|
@@ -696,6 +848,13 @@ function extractMessageId(message) {
|
|
|
696
848
|
function delay(ms) {
|
|
697
849
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
698
850
|
}
|
|
851
|
+
function createOpenCodePromptTimeoutError(input) {
|
|
852
|
+
return new OpenCodePromptTimeoutError({
|
|
853
|
+
...input,
|
|
854
|
+
elapsedMs: input.elapsedMs ?? input.timeoutMs,
|
|
855
|
+
message: input.message ?? "The OpenCode host did not finish this request in time."
|
|
856
|
+
});
|
|
857
|
+
}
|
|
699
858
|
function getPromptMessagePollDelayMs(attempt) {
|
|
700
859
|
return PROMPT_MESSAGE_POLL_INITIAL_DELAYS_MS[attempt] ?? PROMPT_MESSAGE_POLL_INTERVAL_MS;
|
|
701
860
|
}
|
|
@@ -2255,7 +2414,6 @@ var SUPPORTED_BOT_LANGUAGES = ["en", "zh-CN"];
|
|
|
2255
2414
|
var EN_BOT_COPY = {
|
|
2256
2415
|
commands: {
|
|
2257
2416
|
start: "Welcome and quick start",
|
|
2258
|
-
help: "Show commands and examples",
|
|
2259
2417
|
status: "Show system status",
|
|
2260
2418
|
new: "Create a new session",
|
|
2261
2419
|
agents: "View and switch agents",
|
|
@@ -2269,37 +2427,11 @@ var EN_BOT_COPY = {
|
|
|
2269
2427
|
"",
|
|
2270
2428
|
"Talk to your OpenCode server from Telegram.",
|
|
2271
2429
|
"",
|
|
2272
|
-
"## What you can send",
|
|
2273
|
-
"- Text prompts",
|
|
2274
|
-
"- Images with an optional caption",
|
|
2275
|
-
"- Voice messages (requires OpenRouter voice transcription)",
|
|
2276
|
-
"",
|
|
2277
2430
|
"## Quick start",
|
|
2278
2431
|
"1. Run `/status` to confirm the server is ready.",
|
|
2279
2432
|
"2. Run `/new [title]` to create a fresh session.",
|
|
2280
|
-
"3. Send a text, image, or voice message.",
|
|
2281
2433
|
"",
|
|
2282
|
-
"
|
|
2283
|
-
] },
|
|
2284
|
-
help: { lines: [
|
|
2285
|
-
"# Help",
|
|
2286
|
-
"",
|
|
2287
|
-
"Use this bot to chat with OpenCode from Telegram.",
|
|
2288
|
-
"",
|
|
2289
|
-
"## Commands",
|
|
2290
|
-
"- `/status` Check server, workspace, MCP, and LSP status",
|
|
2291
|
-
"- `/new [title]` Create a new session",
|
|
2292
|
-
"- `/sessions` View, switch, or rename sessions",
|
|
2293
|
-
"- `/agents` View and switch agents",
|
|
2294
|
-
"- `/model` View and switch models and reasoning levels",
|
|
2295
|
-
"- `/language` Switch the bot display language",
|
|
2296
|
-
"- `/cancel` Cancel session rename or abort the running request",
|
|
2297
|
-
"",
|
|
2298
|
-
"## Examples",
|
|
2299
|
-
"- `/new bug triage`",
|
|
2300
|
-
"- Send a plain text message directly",
|
|
2301
|
-
"- Send an image with a caption",
|
|
2302
|
-
"- Send a voice message if OpenRouter voice transcription is configured"
|
|
2434
|
+
"Send a text, image, or voice message directly."
|
|
2303
2435
|
] },
|
|
2304
2436
|
systemStatus: { title: "System Status" },
|
|
2305
2437
|
common: {
|
|
@@ -2336,6 +2468,7 @@ var EN_BOT_COPY = {
|
|
|
2336
2468
|
unexpected: "Unexpected error.",
|
|
2337
2469
|
providerAuth: "Provider authentication failed.",
|
|
2338
2470
|
requestAborted: "Request was aborted.",
|
|
2471
|
+
promptTimeout: "OpenCode request timed out.",
|
|
2339
2472
|
structuredOutput: "Structured output validation failed.",
|
|
2340
2473
|
voiceNotConfigured: "Voice transcription is not configured.",
|
|
2341
2474
|
voiceDownload: "Failed to download the Telegram voice file.",
|
|
@@ -2490,7 +2623,6 @@ var EN_BOT_COPY = {
|
|
|
2490
2623
|
var ZH_CN_BOT_COPY = {
|
|
2491
2624
|
commands: {
|
|
2492
2625
|
start: "查看欢迎与快速开始",
|
|
2493
|
-
help: "查看命令说明与示例",
|
|
2494
2626
|
status: "查看系统状态",
|
|
2495
2627
|
new: "新建会话",
|
|
2496
2628
|
agents: "查看并切换代理",
|
|
@@ -2504,37 +2636,11 @@ var ZH_CN_BOT_COPY = {
|
|
|
2504
2636
|
"",
|
|
2505
2637
|
"通过 Telegram 直接和 OpenCode 服务对话。",
|
|
2506
2638
|
"",
|
|
2507
|
-
"## 支持的输入",
|
|
2508
|
-
"- 文本消息",
|
|
2509
|
-
"- 图片 (可附带 caption)",
|
|
2510
|
-
"- 语音消息 (需先配置 OpenRouter 语音转写)",
|
|
2511
|
-
"",
|
|
2512
2639
|
"## 快速开始",
|
|
2513
2640
|
"1. 先运行 `/status` 确认服务状态正常。",
|
|
2514
2641
|
"2. 运行 `/new [title]` 创建一个新会话。",
|
|
2515
|
-
"3. 直接发送文本、图片或语音消息。",
|
|
2516
|
-
"",
|
|
2517
|
-
"更多命令和示例请查看 `/help`。"
|
|
2518
|
-
] },
|
|
2519
|
-
help: { lines: [
|
|
2520
|
-
"# 帮助",
|
|
2521
|
-
"",
|
|
2522
|
-
"使用这个机器人可以通过 Telegram 与 OpenCode 对话。",
|
|
2523
|
-
"",
|
|
2524
|
-
"## 命令",
|
|
2525
|
-
"- `/status` 查看服务、工作区、MCP 和 LSP 状态",
|
|
2526
|
-
"- `/new [title]` 创建一个新会话",
|
|
2527
|
-
"- `/sessions` 查看、切换或重命名会话",
|
|
2528
|
-
"- `/agents` 查看并切换代理",
|
|
2529
|
-
"- `/model` 查看并切换模型与推理级别",
|
|
2530
|
-
"- `/language` 切换机器人的显示语言",
|
|
2531
|
-
"- `/cancel` 取消会话重命名或中止当前请求",
|
|
2532
2642
|
"",
|
|
2533
|
-
"
|
|
2534
|
-
"- `/new bug triage`",
|
|
2535
|
-
"- 直接发送一条文本消息",
|
|
2536
|
-
"- 发送一张带 caption 的图片",
|
|
2537
|
-
"- 如果已配置 OpenRouter 语音转写,直接发送语音消息"
|
|
2643
|
+
"直接发送文本、图片或语音消息即可。"
|
|
2538
2644
|
] },
|
|
2539
2645
|
systemStatus: { title: "系统状态" },
|
|
2540
2646
|
common: {
|
|
@@ -2571,6 +2677,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2571
2677
|
unexpected: "发生未知错误。",
|
|
2572
2678
|
providerAuth: "Provider 认证失败。",
|
|
2573
2679
|
requestAborted: "请求已中止。",
|
|
2680
|
+
promptTimeout: "OpenCode 响应超时。",
|
|
2574
2681
|
structuredOutput: "结构化输出校验失败。",
|
|
2575
2682
|
voiceNotConfigured: "未配置语音转写服务。",
|
|
2576
2683
|
voiceDownload: "下载 Telegram 语音文件失败。",
|
|
@@ -2747,10 +2854,6 @@ function getTelegramCommands(language = "en") {
|
|
|
2747
2854
|
command: "start",
|
|
2748
2855
|
description: copy.commands.start
|
|
2749
2856
|
},
|
|
2750
|
-
{
|
|
2751
|
-
command: "help",
|
|
2752
|
-
description: copy.commands.help
|
|
2753
|
-
},
|
|
2754
2857
|
{
|
|
2755
2858
|
command: "status",
|
|
2756
2859
|
description: copy.commands.status
|
|
@@ -2918,6 +3021,10 @@ function normalizeError(error, copy) {
|
|
|
2918
3021
|
message: copy.errors.requestAborted,
|
|
2919
3022
|
cause: extractMessage(error.data) ?? null
|
|
2920
3023
|
};
|
|
3024
|
+
if (isNamedError(error, "OpenCodePromptTimeoutError")) return {
|
|
3025
|
+
message: copy.errors.promptTimeout,
|
|
3026
|
+
cause: null
|
|
3027
|
+
};
|
|
2921
3028
|
if (isNamedError(error, "StructuredOutputError")) return {
|
|
2922
3029
|
message: copy.errors.structuredOutput,
|
|
2923
3030
|
cause: joinNonEmptyParts([extractMessage(error.data), extractRetries(error.data)])
|
|
@@ -3579,6 +3686,112 @@ function registerCancelCommand(bot, dependencies) {
|
|
|
3579
3686
|
});
|
|
3580
3687
|
}
|
|
3581
3688
|
//#endregion
|
|
3689
|
+
//#region src/bot/commands/language.ts
|
|
3690
|
+
async function handleLanguageCommand(ctx, dependencies) {
|
|
3691
|
+
const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
|
|
3692
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3693
|
+
try {
|
|
3694
|
+
await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
|
|
3695
|
+
await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
|
|
3696
|
+
} catch (error) {
|
|
3697
|
+
dependencies.logger.error({ error }, "failed to show language options");
|
|
3698
|
+
await ctx.reply(presentError(error, copy));
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
async function switchLanguageForChat(api, chatId, language, dependencies) {
|
|
3702
|
+
const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
|
|
3703
|
+
if (!isBotLanguage(language)) return {
|
|
3704
|
+
found: false,
|
|
3705
|
+
copy: currentCopy
|
|
3706
|
+
};
|
|
3707
|
+
await setChatLanguage(dependencies.sessionRepo, chatId, language);
|
|
3708
|
+
await syncTelegramCommandsForChat(api, chatId, language);
|
|
3709
|
+
return {
|
|
3710
|
+
found: true,
|
|
3711
|
+
copy: await getChatCopy(dependencies.sessionRepo, chatId),
|
|
3712
|
+
language
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3715
|
+
async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
|
|
3716
|
+
const result = await switchLanguageForChat(api, chatId, language, dependencies);
|
|
3717
|
+
if (!result.found) return {
|
|
3718
|
+
found: false,
|
|
3719
|
+
copy: result.copy,
|
|
3720
|
+
text: result.copy.language.expired,
|
|
3721
|
+
keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
|
|
3722
|
+
};
|
|
3723
|
+
return {
|
|
3724
|
+
found: true,
|
|
3725
|
+
copy: result.copy,
|
|
3726
|
+
text: presentLanguageSwitchMessage(result.language, result.copy),
|
|
3727
|
+
keyboard: buildLanguageKeyboard(result.language, result.copy)
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
function registerLanguageCommand(bot, dependencies) {
|
|
3731
|
+
bot.command("language", async (ctx) => {
|
|
3732
|
+
await handleLanguageCommand(ctx, dependencies);
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
//#endregion
|
|
3736
|
+
//#region src/bot/commands/models.ts
|
|
3737
|
+
async function handleModelsCommand(ctx, dependencies) {
|
|
3738
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3739
|
+
try {
|
|
3740
|
+
const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
|
|
3741
|
+
if (result.models.length === 0) {
|
|
3742
|
+
await ctx.reply(copy.models.none);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
|
|
3746
|
+
await ctx.reply(presentModelsMessage({
|
|
3747
|
+
currentModelId: result.currentModelId,
|
|
3748
|
+
currentModelProviderId: result.currentModelProviderId,
|
|
3749
|
+
currentModelVariant: result.currentModelVariant,
|
|
3750
|
+
models: result.models,
|
|
3751
|
+
page: page.page
|
|
3752
|
+
}, copy), { reply_markup: keyboard });
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
dependencies.logger.error({ error }, "failed to list models");
|
|
3755
|
+
await ctx.reply(presentError(error, copy));
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
function registerModelsCommand(bot, dependencies) {
|
|
3759
|
+
bot.command(["model", "models"], async (ctx) => {
|
|
3760
|
+
await handleModelsCommand(ctx, dependencies);
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
//#endregion
|
|
3764
|
+
//#region src/bot/commands/new.ts
|
|
3765
|
+
async function handleNewCommand(ctx, dependencies) {
|
|
3766
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3767
|
+
try {
|
|
3768
|
+
const title = extractSessionTitle(ctx);
|
|
3769
|
+
const result = await dependencies.createSessionUseCase.execute({
|
|
3770
|
+
chatId: ctx.chat.id,
|
|
3771
|
+
title
|
|
3772
|
+
});
|
|
3773
|
+
await ctx.reply(presentSessionCreatedMessage(result.session, copy));
|
|
3774
|
+
} catch (error) {
|
|
3775
|
+
dependencies.logger.error({ error }, "failed to create new session");
|
|
3776
|
+
await ctx.reply(presentError(error, copy));
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
function registerNewCommand(bot, dependencies) {
|
|
3780
|
+
bot.command("new", async (ctx) => {
|
|
3781
|
+
await handleNewCommand(ctx, dependencies);
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
function extractSessionTitle(ctx) {
|
|
3785
|
+
if (typeof ctx.match === "string") {
|
|
3786
|
+
const title = ctx.match.trim();
|
|
3787
|
+
return title ? title : null;
|
|
3788
|
+
}
|
|
3789
|
+
const messageText = ctx.message?.text?.trim();
|
|
3790
|
+
if (!messageText) return null;
|
|
3791
|
+
const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
|
|
3792
|
+
return title ? title : null;
|
|
3793
|
+
}
|
|
3794
|
+
//#endregion
|
|
3582
3795
|
//#region src/services/telegram/telegram-format.ts
|
|
3583
3796
|
var MAX_TELEGRAM_MESSAGE_LENGTH = 4096;
|
|
3584
3797
|
var TRUNCATED_SUFFIX = "...";
|
|
@@ -3912,142 +4125,6 @@ function escapeLinkDestination(url) {
|
|
|
3912
4125
|
return url.replace(/\\/g, "\\\\").replace(/\)/g, "\\)").replace(/\(/g, "\\(");
|
|
3913
4126
|
}
|
|
3914
4127
|
//#endregion
|
|
3915
|
-
//#region src/bot/presenters/static.presenter.ts
|
|
3916
|
-
function presentStartMarkdownMessage(copy = BOT_COPY) {
|
|
3917
|
-
return copy.start.lines.join("\n");
|
|
3918
|
-
}
|
|
3919
|
-
function presentHelpMarkdownMessage(copy = BOT_COPY) {
|
|
3920
|
-
return copy.help.lines.join("\n");
|
|
3921
|
-
}
|
|
3922
|
-
//#endregion
|
|
3923
|
-
//#region src/bot/commands/help.ts
|
|
3924
|
-
async function handleHelpCommand(ctx, dependencies) {
|
|
3925
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
|
|
3926
|
-
const reply = buildTelegramStaticReply(presentHelpMarkdownMessage(copy));
|
|
3927
|
-
try {
|
|
3928
|
-
await ctx.reply(reply.preferred.text, reply.preferred.options);
|
|
3929
|
-
} catch (error) {
|
|
3930
|
-
if (reply.preferred.options) {
|
|
3931
|
-
dependencies.logger.error({ error }, "failed to send help markdown reply, falling back to plain text");
|
|
3932
|
-
await ctx.reply(reply.fallback.text);
|
|
3933
|
-
return;
|
|
3934
|
-
}
|
|
3935
|
-
dependencies.logger.error({ error }, "failed to show help message");
|
|
3936
|
-
await ctx.reply(presentError(error, copy));
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
function registerHelpCommand(bot, dependencies) {
|
|
3940
|
-
bot.command("help", async (ctx) => {
|
|
3941
|
-
await handleHelpCommand(ctx, dependencies);
|
|
3942
|
-
});
|
|
3943
|
-
}
|
|
3944
|
-
//#endregion
|
|
3945
|
-
//#region src/bot/commands/language.ts
|
|
3946
|
-
async function handleLanguageCommand(ctx, dependencies) {
|
|
3947
|
-
const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
|
|
3948
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3949
|
-
try {
|
|
3950
|
-
await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
|
|
3951
|
-
await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
|
|
3952
|
-
} catch (error) {
|
|
3953
|
-
dependencies.logger.error({ error }, "failed to show language options");
|
|
3954
|
-
await ctx.reply(presentError(error, copy));
|
|
3955
|
-
}
|
|
3956
|
-
}
|
|
3957
|
-
async function switchLanguageForChat(api, chatId, language, dependencies) {
|
|
3958
|
-
const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
|
|
3959
|
-
if (!isBotLanguage(language)) return {
|
|
3960
|
-
found: false,
|
|
3961
|
-
copy: currentCopy
|
|
3962
|
-
};
|
|
3963
|
-
await setChatLanguage(dependencies.sessionRepo, chatId, language);
|
|
3964
|
-
await syncTelegramCommandsForChat(api, chatId, language);
|
|
3965
|
-
return {
|
|
3966
|
-
found: true,
|
|
3967
|
-
copy: await getChatCopy(dependencies.sessionRepo, chatId),
|
|
3968
|
-
language
|
|
3969
|
-
};
|
|
3970
|
-
}
|
|
3971
|
-
async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
|
|
3972
|
-
const result = await switchLanguageForChat(api, chatId, language, dependencies);
|
|
3973
|
-
if (!result.found) return {
|
|
3974
|
-
found: false,
|
|
3975
|
-
copy: result.copy,
|
|
3976
|
-
text: result.copy.language.expired,
|
|
3977
|
-
keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
|
|
3978
|
-
};
|
|
3979
|
-
return {
|
|
3980
|
-
found: true,
|
|
3981
|
-
copy: result.copy,
|
|
3982
|
-
text: presentLanguageSwitchMessage(result.language, result.copy),
|
|
3983
|
-
keyboard: buildLanguageKeyboard(result.language, result.copy)
|
|
3984
|
-
};
|
|
3985
|
-
}
|
|
3986
|
-
function registerLanguageCommand(bot, dependencies) {
|
|
3987
|
-
bot.command("language", async (ctx) => {
|
|
3988
|
-
await handleLanguageCommand(ctx, dependencies);
|
|
3989
|
-
});
|
|
3990
|
-
}
|
|
3991
|
-
//#endregion
|
|
3992
|
-
//#region src/bot/commands/models.ts
|
|
3993
|
-
async function handleModelsCommand(ctx, dependencies) {
|
|
3994
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3995
|
-
try {
|
|
3996
|
-
const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
|
|
3997
|
-
if (result.models.length === 0) {
|
|
3998
|
-
await ctx.reply(copy.models.none);
|
|
3999
|
-
return;
|
|
4000
|
-
}
|
|
4001
|
-
const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
|
|
4002
|
-
await ctx.reply(presentModelsMessage({
|
|
4003
|
-
currentModelId: result.currentModelId,
|
|
4004
|
-
currentModelProviderId: result.currentModelProviderId,
|
|
4005
|
-
currentModelVariant: result.currentModelVariant,
|
|
4006
|
-
models: result.models,
|
|
4007
|
-
page: page.page
|
|
4008
|
-
}, copy), { reply_markup: keyboard });
|
|
4009
|
-
} catch (error) {
|
|
4010
|
-
dependencies.logger.error({ error }, "failed to list models");
|
|
4011
|
-
await ctx.reply(presentError(error, copy));
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
function registerModelsCommand(bot, dependencies) {
|
|
4015
|
-
bot.command(["model", "models"], async (ctx) => {
|
|
4016
|
-
await handleModelsCommand(ctx, dependencies);
|
|
4017
|
-
});
|
|
4018
|
-
}
|
|
4019
|
-
//#endregion
|
|
4020
|
-
//#region src/bot/commands/new.ts
|
|
4021
|
-
async function handleNewCommand(ctx, dependencies) {
|
|
4022
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
4023
|
-
try {
|
|
4024
|
-
const title = extractSessionTitle(ctx);
|
|
4025
|
-
const result = await dependencies.createSessionUseCase.execute({
|
|
4026
|
-
chatId: ctx.chat.id,
|
|
4027
|
-
title
|
|
4028
|
-
});
|
|
4029
|
-
await ctx.reply(presentSessionCreatedMessage(result.session, copy));
|
|
4030
|
-
} catch (error) {
|
|
4031
|
-
dependencies.logger.error({ error }, "failed to create new session");
|
|
4032
|
-
await ctx.reply(presentError(error, copy));
|
|
4033
|
-
}
|
|
4034
|
-
}
|
|
4035
|
-
function registerNewCommand(bot, dependencies) {
|
|
4036
|
-
bot.command("new", async (ctx) => {
|
|
4037
|
-
await handleNewCommand(ctx, dependencies);
|
|
4038
|
-
});
|
|
4039
|
-
}
|
|
4040
|
-
function extractSessionTitle(ctx) {
|
|
4041
|
-
if (typeof ctx.match === "string") {
|
|
4042
|
-
const title = ctx.match.trim();
|
|
4043
|
-
return title ? title : null;
|
|
4044
|
-
}
|
|
4045
|
-
const messageText = ctx.message?.text?.trim();
|
|
4046
|
-
if (!messageText) return null;
|
|
4047
|
-
const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
|
|
4048
|
-
return title ? title : null;
|
|
4049
|
-
}
|
|
4050
|
-
//#endregion
|
|
4051
4128
|
//#region src/bot/commands/status.ts
|
|
4052
4129
|
async function handleStatusCommand(ctx, dependencies) {
|
|
4053
4130
|
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
|
|
@@ -4088,6 +4165,11 @@ function registerSessionsCommand(bot, dependencies) {
|
|
|
4088
4165
|
});
|
|
4089
4166
|
}
|
|
4090
4167
|
//#endregion
|
|
4168
|
+
//#region src/bot/presenters/static.presenter.ts
|
|
4169
|
+
function presentStartMarkdownMessage(copy = BOT_COPY) {
|
|
4170
|
+
return copy.start.lines.join("\n");
|
|
4171
|
+
}
|
|
4172
|
+
//#endregion
|
|
4091
4173
|
//#region src/bot/commands/start.ts
|
|
4092
4174
|
async function handleStartCommand(ctx, dependencies) {
|
|
4093
4175
|
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
|
|
@@ -4604,7 +4686,6 @@ function registerBot(bot, container, options) {
|
|
|
4604
4686
|
bot.use(createLoggingMiddleware(container.logger));
|
|
4605
4687
|
bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
|
|
4606
4688
|
registerStartCommand(bot, container);
|
|
4607
|
-
registerHelpCommand(bot, container);
|
|
4608
4689
|
registerStatusCommand(bot, container);
|
|
4609
4690
|
registerNewCommand(bot, container);
|
|
4610
4691
|
registerAgentsCommand(bot, container);
|