opencode-tbot 0.1.4 → 0.1.6
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 -1
- package/README.zh-CN.md +1 -1
- package/dist/plugin.js +192 -23
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ A Telegram plugin for driving [OpenCode](https://opencode.ai) from chat.
|
|
|
22
22
|
Run:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
|
|
25
|
+
npm install opencode-tbot@latest
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
The installer registers the plugin globally and writes the default runtime config.
|
package/README.zh-CN.md
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -214,6 +214,15 @@ function buildOpenCodeSdkConfig(options) {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
var EMPTY_RESPONSE_TEXT = "OpenCode returned empty response.";
|
|
217
|
+
var PROMPT_MESSAGE_POLL_DELAYS_MS = [
|
|
218
|
+
0,
|
|
219
|
+
150,
|
|
220
|
+
300,
|
|
221
|
+
600,
|
|
222
|
+
1200,
|
|
223
|
+
2e3,
|
|
224
|
+
3200
|
|
225
|
+
];
|
|
217
226
|
var STRUCTURED_REPLY_SCHEMA = {
|
|
218
227
|
type: "json_schema",
|
|
219
228
|
retryCount: 2,
|
|
@@ -327,7 +336,7 @@ var OpenCodeClient = class {
|
|
|
327
336
|
url: file.url
|
|
328
337
|
}))];
|
|
329
338
|
if (parts.length === 0) throw new Error("Prompt requires text or file attachments.");
|
|
330
|
-
const
|
|
339
|
+
const initialData = unwrapSdkData(await this.client.session.prompt({
|
|
331
340
|
sessionID: input.sessionId,
|
|
332
341
|
...input.agent ? { agent: input.agent } : {},
|
|
333
342
|
...input.structured ? { format: STRUCTURED_REPLY_SCHEMA } : {},
|
|
@@ -335,18 +344,36 @@ var OpenCodeClient = class {
|
|
|
335
344
|
...input.variant ? { variant: input.variant } : {},
|
|
336
345
|
parts
|
|
337
346
|
}, SDK_OPTIONS));
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
return buildPromptSessionResult(await this.resolvePromptResponse(input, initialData), {
|
|
348
|
+
emptyResponseText: EMPTY_RESPONSE_TEXT,
|
|
349
|
+
finishedAt: Date.now(),
|
|
350
|
+
startedAt,
|
|
351
|
+
structured: input.structured ?? false
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
async resolvePromptResponse(input, data) {
|
|
355
|
+
if (!shouldPollPromptMessage(data, input.structured ?? false)) return data;
|
|
356
|
+
const messageId = data.info?.id;
|
|
357
|
+
if (!messageId) return data;
|
|
358
|
+
for (const delayMs of PROMPT_MESSAGE_POLL_DELAYS_MS) {
|
|
359
|
+
if (delayMs > 0) await delay(delayMs);
|
|
360
|
+
const next = await this.fetchPromptMessage(input.sessionId, messageId);
|
|
361
|
+
if (!next) continue;
|
|
362
|
+
data = next;
|
|
363
|
+
if (!shouldPollPromptMessage(data, input.structured ?? false)) return data;
|
|
364
|
+
}
|
|
365
|
+
return data;
|
|
366
|
+
}
|
|
367
|
+
async fetchPromptMessage(sessionId, messageId) {
|
|
368
|
+
if (typeof this.client.session.message !== "function") return null;
|
|
369
|
+
try {
|
|
370
|
+
return unwrapSdkData(await this.client.session.message({
|
|
371
|
+
sessionID: sessionId,
|
|
372
|
+
messageID: messageId
|
|
373
|
+
}, SDK_OPTIONS));
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
350
377
|
}
|
|
351
378
|
async loadModels() {
|
|
352
379
|
const [configResponse, providersResponse] = await Promise.all([this.client.config.get(void 0, SDK_OPTIONS), this.client.config.providers(void 0, SDK_OPTIONS)]);
|
|
@@ -446,8 +473,39 @@ function normalizeVariants(variants) {
|
|
|
446
473
|
}));
|
|
447
474
|
}
|
|
448
475
|
function extractTextFromParts(parts) {
|
|
476
|
+
if (!Array.isArray(parts)) return "";
|
|
449
477
|
return parts.filter((part) => part.type === "text").map((part) => part.text).join("").trim();
|
|
450
478
|
}
|
|
479
|
+
function buildPromptSessionResult(data, options) {
|
|
480
|
+
const assistantInfo = toAssistantMessage(data.info);
|
|
481
|
+
const bodyMd = options.structured ? extractStructuredMarkdown(assistantInfo?.structured) : null;
|
|
482
|
+
const responseParts = Array.isArray(data.parts) ? data.parts : [];
|
|
483
|
+
const fallbackText = extractTextFromParts(responseParts) || bodyMd || options.emptyResponseText;
|
|
484
|
+
return {
|
|
485
|
+
assistantError: assistantInfo?.error ?? null,
|
|
486
|
+
bodyMd,
|
|
487
|
+
fallbackText,
|
|
488
|
+
info: assistantInfo,
|
|
489
|
+
metrics: extractPromptMetrics(assistantInfo, options.startedAt, options.finishedAt),
|
|
490
|
+
parts: responseParts,
|
|
491
|
+
structured: assistantInfo?.structured ?? null
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function shouldPollPromptMessage(data, structured) {
|
|
495
|
+
const assistantInfo = toAssistantMessage(data.info);
|
|
496
|
+
const bodyMd = structured ? extractStructuredMarkdown(assistantInfo?.structured) : null;
|
|
497
|
+
const hasText = extractTextFromParts(Array.isArray(data.parts) ? data.parts : []).length > 0;
|
|
498
|
+
const hasAssistantError = !!assistantInfo?.error;
|
|
499
|
+
const isCompleted = typeof assistantInfo?.time?.completed === "number" && Number.isFinite(assistantInfo.time.completed);
|
|
500
|
+
return !hasText && !bodyMd && !hasAssistantError && !isCompleted;
|
|
501
|
+
}
|
|
502
|
+
function toAssistantMessage(message) {
|
|
503
|
+
if (!message || typeof message !== "object") return null;
|
|
504
|
+
return !("role" in message) || message.role === "assistant" ? message : null;
|
|
505
|
+
}
|
|
506
|
+
function delay(ms) {
|
|
507
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
508
|
+
}
|
|
451
509
|
function extractStructuredMarkdown(structured) {
|
|
452
510
|
const parsed = StructuredReplySchema.safeParse(structured);
|
|
453
511
|
if (!parsed.success) return null;
|
|
@@ -778,7 +836,10 @@ var OpenRouterVoiceTranscriptionClient = class {
|
|
|
778
836
|
temperature: 0
|
|
779
837
|
} }, { timeoutMs: this.timeoutMs });
|
|
780
838
|
} catch (error) {
|
|
781
|
-
throw new VoiceTranscriptionFailedError(
|
|
839
|
+
throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
|
|
840
|
+
format,
|
|
841
|
+
model: this.model
|
|
842
|
+
}));
|
|
782
843
|
}
|
|
783
844
|
return { text: extractTranscript(response) };
|
|
784
845
|
}
|
|
@@ -841,8 +902,108 @@ function extractTranscript(response) {
|
|
|
841
902
|
function isTextContentItem(value) {
|
|
842
903
|
return !!value && typeof value === "object" && "type" in value && value.type === "text" && "text" in value && typeof value.text === "string";
|
|
843
904
|
}
|
|
905
|
+
function buildTranscriptionErrorMessage(error, context) {
|
|
906
|
+
const parsedBody = parseJsonBody(extractStringField(error, "body"));
|
|
907
|
+
const rawMessages = dedupeNonEmptyStrings([
|
|
908
|
+
extractErrorMessage(error),
|
|
909
|
+
extractErrorMessage(readField(error, "error")),
|
|
910
|
+
extractErrorMessage(readField(error, "data")),
|
|
911
|
+
extractErrorMessage(readField(readField(error, "data"), "error")),
|
|
912
|
+
extractErrorMessage(parsedBody),
|
|
913
|
+
extractErrorMessage(readField(parsedBody, "error")),
|
|
914
|
+
extractMetadataRawMessage(error),
|
|
915
|
+
extractMetadataRawMessage(parsedBody)
|
|
916
|
+
]);
|
|
917
|
+
const messages = rawMessages.some((message) => !isGenericProviderMessage(message)) ? rawMessages.filter((message) => !isGenericProviderMessage(message)) : rawMessages;
|
|
918
|
+
const providerName = extractProviderName(error) ?? extractProviderName(parsedBody);
|
|
919
|
+
const statusCode = extractNumericField(error, "statusCode") ?? extractNumericField(parsedBody, "statusCode");
|
|
920
|
+
const errorCode = extractErrorCode(error, parsedBody);
|
|
921
|
+
return joinNonEmptyParts$1([
|
|
922
|
+
...messages,
|
|
923
|
+
`model: ${context.model}`,
|
|
924
|
+
`format: ${context.format}`,
|
|
925
|
+
providerName ? `provider: ${providerName}` : null,
|
|
926
|
+
statusCode !== null ? `status: ${statusCode}` : null,
|
|
927
|
+
errorCode !== null ? `code: ${errorCode}` : null
|
|
928
|
+
]) ?? "Failed to reach OpenRouter voice transcription.";
|
|
929
|
+
}
|
|
844
930
|
function extractErrorMessage(error) {
|
|
845
|
-
|
|
931
|
+
if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
|
|
932
|
+
return extractStringField(error, "message");
|
|
933
|
+
}
|
|
934
|
+
function extractMetadataRawMessage(value) {
|
|
935
|
+
const raw = extractStringField(readField(value, "metadata") ?? readField(readField(value, "error"), "metadata") ?? readField(readField(readField(value, "data"), "error"), "metadata"), "raw");
|
|
936
|
+
if (!raw) return null;
|
|
937
|
+
return raw.length <= 280 ? raw : `${raw.slice(0, 277)}...`;
|
|
938
|
+
}
|
|
939
|
+
function extractProviderName(value) {
|
|
940
|
+
const candidates = [
|
|
941
|
+
readField(value, "metadata"),
|
|
942
|
+
readField(readField(value, "error"), "metadata"),
|
|
943
|
+
readField(readField(readField(value, "data"), "error"), "metadata")
|
|
944
|
+
];
|
|
945
|
+
for (const candidate of candidates) {
|
|
946
|
+
const providerName = extractStringField(candidate, "provider_name") ?? extractStringField(candidate, "providerName") ?? extractStringField(candidate, "provider");
|
|
947
|
+
if (providerName) return providerName;
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
function extractErrorCode(...values) {
|
|
952
|
+
for (const value of values) {
|
|
953
|
+
const candidates = [
|
|
954
|
+
value,
|
|
955
|
+
readField(value, "error"),
|
|
956
|
+
readField(value, "data"),
|
|
957
|
+
readField(readField(value, "data"), "error")
|
|
958
|
+
];
|
|
959
|
+
for (const candidate of candidates) {
|
|
960
|
+
const code = extractNumericField(candidate, "code");
|
|
961
|
+
if (code !== null) return code;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
function extractNumericField(value, fieldName) {
|
|
967
|
+
if (!value || typeof value !== "object" || !(fieldName in value)) return null;
|
|
968
|
+
const fieldValue = value[fieldName];
|
|
969
|
+
return typeof fieldValue === "number" && Number.isFinite(fieldValue) ? fieldValue : null;
|
|
970
|
+
}
|
|
971
|
+
function extractStringField(value, fieldName) {
|
|
972
|
+
if (!value || typeof value !== "object" || !(fieldName in value)) return null;
|
|
973
|
+
const fieldValue = value[fieldName];
|
|
974
|
+
return typeof fieldValue === "string" && fieldValue.trim().length > 0 ? fieldValue.trim() : null;
|
|
975
|
+
}
|
|
976
|
+
function readField(value, fieldName) {
|
|
977
|
+
return value && typeof value === "object" && fieldName in value ? value[fieldName] : null;
|
|
978
|
+
}
|
|
979
|
+
function parseJsonBody(body) {
|
|
980
|
+
if (!body) return null;
|
|
981
|
+
try {
|
|
982
|
+
return JSON.parse(body);
|
|
983
|
+
} catch {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function dedupeNonEmptyStrings(values) {
|
|
988
|
+
const seen = /* @__PURE__ */ new Set();
|
|
989
|
+
const result = [];
|
|
990
|
+
for (const value of values) {
|
|
991
|
+
const normalized = value?.trim();
|
|
992
|
+
if (!normalized) continue;
|
|
993
|
+
const key = normalized.toLowerCase();
|
|
994
|
+
if (seen.has(key)) continue;
|
|
995
|
+
seen.add(key);
|
|
996
|
+
result.push(normalized);
|
|
997
|
+
}
|
|
998
|
+
return result;
|
|
999
|
+
}
|
|
1000
|
+
function joinNonEmptyParts$1(parts) {
|
|
1001
|
+
const filtered = parts.map((part) => part?.trim()).filter((part) => !!part);
|
|
1002
|
+
return filtered.length > 0 ? filtered.join(" | ") : null;
|
|
1003
|
+
}
|
|
1004
|
+
function isGenericProviderMessage(message) {
|
|
1005
|
+
const normalized = message.trim().toLowerCase();
|
|
1006
|
+
return normalized === "provider returned error" || normalized === "failed to reach openrouter voice transcription.";
|
|
846
1007
|
}
|
|
847
1008
|
//#endregion
|
|
848
1009
|
//#region src/services/voice-transcription/voice-transcription.service.ts
|
|
@@ -962,14 +1123,14 @@ var GetPathUseCase = class {
|
|
|
962
1123
|
//#endregion
|
|
963
1124
|
//#region src/use-cases/get-status.usecase.ts
|
|
964
1125
|
var GetStatusUseCase = class {
|
|
965
|
-
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo,
|
|
1126
|
+
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceRecognitionStatus) {
|
|
966
1127
|
this.getHealthUseCase = getHealthUseCase;
|
|
967
1128
|
this.getPathUseCase = getPathUseCase;
|
|
968
1129
|
this.listLspUseCase = listLspUseCase;
|
|
969
1130
|
this.listMcpUseCase = listMcpUseCase;
|
|
970
1131
|
this.listSessionsUseCase = listSessionsUseCase;
|
|
971
1132
|
this.sessionRepo = sessionRepo;
|
|
972
|
-
this.
|
|
1133
|
+
this.voiceRecognitionStatus = voiceRecognitionStatus;
|
|
973
1134
|
}
|
|
974
1135
|
async execute(input) {
|
|
975
1136
|
const [health, path, lsp, mcp] = await Promise.allSettled([
|
|
@@ -984,7 +1145,7 @@ var GetStatusUseCase = class {
|
|
|
984
1145
|
health: mapSettledResult(health),
|
|
985
1146
|
path: pathResult,
|
|
986
1147
|
plugins,
|
|
987
|
-
voiceRecognition:
|
|
1148
|
+
voiceRecognition: this.voiceRecognitionStatus,
|
|
988
1149
|
workspace,
|
|
989
1150
|
lsp: mapSettledResult(lsp),
|
|
990
1151
|
mcp: mapSettledResult(mcp)
|
|
@@ -1617,7 +1778,10 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
1617
1778
|
const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
|
|
1618
1779
|
const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
|
|
1619
1780
|
const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
|
|
1620
|
-
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo,
|
|
1781
|
+
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, {
|
|
1782
|
+
status: config.openrouter.configured ? "configured" : "not_configured",
|
|
1783
|
+
model: config.openrouter.configured ? config.openrouter.model : null
|
|
1784
|
+
});
|
|
1621
1785
|
const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
|
|
1622
1786
|
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
1623
1787
|
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
|
|
@@ -2678,7 +2842,7 @@ function presentStatusMarkdownSection(title, lines) {
|
|
|
2678
2842
|
return [`## ${title}`, ...lines].join("\n");
|
|
2679
2843
|
}
|
|
2680
2844
|
function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
2681
|
-
const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentPlainStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition
|
|
2845
|
+
const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentPlainStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, layout))];
|
|
2682
2846
|
if (input.health.status === "error") return [
|
|
2683
2847
|
...lines,
|
|
2684
2848
|
...presentStatusPlainErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -2691,7 +2855,7 @@ function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
|
2691
2855
|
];
|
|
2692
2856
|
}
|
|
2693
2857
|
function presentStatusMarkdownOverviewLines(input, copy, layout) {
|
|
2694
|
-
const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentMarkdownStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition
|
|
2858
|
+
const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout)), presentMarkdownStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, layout))];
|
|
2695
2859
|
if (input.health.status === "error") return [
|
|
2696
2860
|
...lines,
|
|
2697
2861
|
...presentStatusMarkdownErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -2791,8 +2955,9 @@ function splitStatusLines(text) {
|
|
|
2791
2955
|
function formatHealthBadge(healthy, layout) {
|
|
2792
2956
|
return healthy ? "🟢" : layout.errorStatus;
|
|
2793
2957
|
}
|
|
2794
|
-
function formatVoiceRecognitionBadge(
|
|
2795
|
-
|
|
2958
|
+
function formatVoiceRecognitionBadge(status, layout) {
|
|
2959
|
+
if (status.status === "configured") return status.model ? `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel} (${status.model})` : `\uD83D\uDFE1 ${layout.voiceRecognitionConfiguredLabel}`;
|
|
2960
|
+
return `\u26AA ${layout.voiceRecognitionNotConfiguredLabel}`;
|
|
2796
2961
|
}
|
|
2797
2962
|
function formatLspStatusBadge(status) {
|
|
2798
2963
|
switch (status.status) {
|
|
@@ -2862,7 +3027,9 @@ function getStatusLayoutCopy(copy) {
|
|
|
2862
3027
|
rootLabel: "Root",
|
|
2863
3028
|
statusLabel: "Status",
|
|
2864
3029
|
tbotVersionLabel: "opencode-tbot Version",
|
|
3030
|
+
voiceRecognitionConfiguredLabel: "configured",
|
|
2865
3031
|
voiceRecognitionLabel: "Voice Recognition",
|
|
3032
|
+
voiceRecognitionNotConfiguredLabel: "not configured",
|
|
2866
3033
|
workspaceTitle: "📁 Workspace"
|
|
2867
3034
|
};
|
|
2868
3035
|
return {
|
|
@@ -2885,7 +3052,9 @@ function getStatusLayoutCopy(copy) {
|
|
|
2885
3052
|
rootLabel: "根目录",
|
|
2886
3053
|
statusLabel: "状态",
|
|
2887
3054
|
tbotVersionLabel: "opencode-tbot版本",
|
|
3055
|
+
voiceRecognitionConfiguredLabel: "已配置",
|
|
2888
3056
|
voiceRecognitionLabel: "语音识别",
|
|
3057
|
+
voiceRecognitionNotConfiguredLabel: "未配置",
|
|
2889
3058
|
workspaceTitle: "📁 工作区"
|
|
2890
3059
|
};
|
|
2891
3060
|
}
|