opencode-telegram-mirror 0.4.0 → 0.4.3
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/package.json +1 -1
- package/src/main.ts +115 -9
- package/src/telegram.ts +3 -2
package/README.md
CHANGED
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -98,6 +98,10 @@ interface BotState {
|
|
|
98
98
|
assistantMessageIds: Set<string>;
|
|
99
99
|
pendingParts: Map<string, Part[]>;
|
|
100
100
|
sentPartIds: Set<string>;
|
|
101
|
+
typingIndicators: Map<
|
|
102
|
+
string,
|
|
103
|
+
{ stop: () => void; timeout: ReturnType<typeof setTimeout> | null; mode: "idle" | "tool" }
|
|
104
|
+
>;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
async function main() {
|
|
@@ -259,6 +263,7 @@ async function main() {
|
|
|
259
263
|
assistantMessageIds: new Set(),
|
|
260
264
|
pendingParts: new Map(),
|
|
261
265
|
sentPartIds: new Set(),
|
|
266
|
+
typingIndicators: new Map(),
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
if (initialThreadTitle && config.threadId) {
|
|
@@ -644,7 +649,7 @@ async function handleTelegramMessage(
|
|
|
644
649
|
directory: state.directory,
|
|
645
650
|
})
|
|
646
651
|
if (abortResult.data) {
|
|
647
|
-
|
|
652
|
+
log("info", "Abort request sent", { sessionId: state.sessionId })
|
|
648
653
|
} else {
|
|
649
654
|
log("error", "Failed to abort session", {
|
|
650
655
|
sessionId: state.sessionId,
|
|
@@ -690,7 +695,7 @@ async function handleTelegramMessage(
|
|
|
690
695
|
directory: state.directory,
|
|
691
696
|
})
|
|
692
697
|
if (abortResult.data) {
|
|
693
|
-
|
|
698
|
+
log("info", "Abort request sent", { sessionId: state.sessionId })
|
|
694
699
|
} else {
|
|
695
700
|
log("error", "Failed to abort session", {
|
|
696
701
|
sessionId: state.sessionId,
|
|
@@ -831,11 +836,13 @@ async function handleTelegramMessage(
|
|
|
831
836
|
> = []
|
|
832
837
|
|
|
833
838
|
if (msg.photo && msg.photo.length > 0) {
|
|
839
|
+
const stopTyping = state.telegram.startTyping()
|
|
834
840
|
const bestPhoto = msg.photo[msg.photo.length - 1]
|
|
835
841
|
const dataUrlResult = await state.telegram.downloadFileAsDataUrl(
|
|
836
842
|
bestPhoto.file_id,
|
|
837
843
|
"image/jpeg"
|
|
838
844
|
)
|
|
845
|
+
stopTyping()
|
|
839
846
|
if (dataUrlResult.status === "ok") {
|
|
840
847
|
parts.push({
|
|
841
848
|
type: "file",
|
|
@@ -857,6 +864,8 @@ async function handleTelegramMessage(
|
|
|
857
864
|
return
|
|
858
865
|
}
|
|
859
866
|
|
|
867
|
+
const stopTyping = state.telegram.startTyping()
|
|
868
|
+
|
|
860
869
|
log("info", "Processing voice message", {
|
|
861
870
|
duration: msg.voice.duration,
|
|
862
871
|
fileId: msg.voice.file_id,
|
|
@@ -864,6 +873,7 @@ async function handleTelegramMessage(
|
|
|
864
873
|
|
|
865
874
|
const fileUrlResult = await state.telegram.getFileUrl(msg.voice.file_id)
|
|
866
875
|
if (fileUrlResult.status === "error") {
|
|
876
|
+
stopTyping()
|
|
867
877
|
log("error", "Failed to get voice file URL", {
|
|
868
878
|
error: fileUrlResult.error.message,
|
|
869
879
|
})
|
|
@@ -873,6 +883,7 @@ async function handleTelegramMessage(
|
|
|
873
883
|
|
|
874
884
|
const audioResponse = await fetch(fileUrlResult.value)
|
|
875
885
|
if (!audioResponse.ok) {
|
|
886
|
+
stopTyping()
|
|
876
887
|
log("error", "Failed to download voice file", { status: audioResponse.status })
|
|
877
888
|
await state.telegram.sendMessage("Failed to download voice message.")
|
|
878
889
|
return
|
|
@@ -880,6 +891,7 @@ async function handleTelegramMessage(
|
|
|
880
891
|
|
|
881
892
|
const audioBuffer = await audioResponse.arrayBuffer()
|
|
882
893
|
const transcriptionResult = await transcribeVoice(audioBuffer, log)
|
|
894
|
+
stopTyping()
|
|
883
895
|
|
|
884
896
|
if (transcriptionResult.status === "error") {
|
|
885
897
|
log("error", "Voice transcription failed", {
|
|
@@ -1014,16 +1026,36 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
|
|
|
1014
1026
|
const sessionId =
|
|
1015
1027
|
ev.properties?.sessionID ??
|
|
1016
1028
|
ev.properties?.info?.sessionID ??
|
|
1017
|
-
ev.properties?.part?.sessionID
|
|
1029
|
+
ev.properties?.part?.sessionID ??
|
|
1030
|
+
ev.properties?.session?.id
|
|
1018
1031
|
const sessionTitle = ev.properties?.session?.title
|
|
1019
1032
|
|
|
1020
1033
|
// Log errors in full and send to Telegram
|
|
1021
1034
|
if (ev.type === "session.error") {
|
|
1022
1035
|
const errorMsg = JSON.stringify(ev.properties, null, 2)
|
|
1036
|
+
const error = ev.properties?.error as
|
|
1037
|
+
| { name?: string; data?: { message?: string } }
|
|
1038
|
+
| undefined
|
|
1039
|
+
const errorName = error?.name
|
|
1040
|
+
const errorText = error?.data?.message
|
|
1041
|
+
const isInterrupted =
|
|
1042
|
+
errorName === "MessageAbortedError" || errorText === "The operation was aborted."
|
|
1043
|
+
|
|
1023
1044
|
log("error", "OpenCode session error", {
|
|
1024
1045
|
sessionId,
|
|
1025
1046
|
error: ev.properties,
|
|
1026
1047
|
})
|
|
1048
|
+
|
|
1049
|
+
if (isInterrupted) {
|
|
1050
|
+
const sendResult = await state.telegram.sendMessage("Interrupted.")
|
|
1051
|
+
if (sendResult.status === "error") {
|
|
1052
|
+
log("error", "Failed to send interrupt message", {
|
|
1053
|
+
error: sendResult.error.message,
|
|
1054
|
+
})
|
|
1055
|
+
}
|
|
1056
|
+
return
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1027
1059
|
// Send error to Telegram for visibility
|
|
1028
1060
|
const sendResult = await state.telegram.sendMessage(
|
|
1029
1061
|
`OpenCode Error:\n${errorMsg.slice(0, 3500)}`
|
|
@@ -1046,6 +1078,23 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
|
|
|
1046
1078
|
|
|
1047
1079
|
if (!sessionId || sessionId !== state.sessionId) return
|
|
1048
1080
|
|
|
1081
|
+
// Stop typing when session becomes idle
|
|
1082
|
+
if (ev.type === "session.idle") {
|
|
1083
|
+
for (const [key, entry] of state.typingIndicators) {
|
|
1084
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
1085
|
+
if (entry.timeout) clearTimeout(entry.timeout)
|
|
1086
|
+
entry.stop()
|
|
1087
|
+
state.typingIndicators.delete(key)
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Send typing action on every session event to keep indicator active during long operations
|
|
1094
|
+
if (ev.type !== "session.error") {
|
|
1095
|
+
state.telegram.sendTypingAction()
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1049
1098
|
if (sessionTitle && state.threadId) {
|
|
1050
1099
|
const trimmedTitle = sessionTitle.trim()
|
|
1051
1100
|
const shouldUpdate = trimmedTitle && trimmedTitle !== state.threadTitle
|
|
@@ -1076,6 +1125,12 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
|
|
|
1076
1125
|
const key = `${info.sessionID}:${info.id}`
|
|
1077
1126
|
state.assistantMessageIds.add(key)
|
|
1078
1127
|
log("debug", "Registered assistant message", { key })
|
|
1128
|
+
const entry = state.typingIndicators.get(key)
|
|
1129
|
+
if (entry && entry.mode === "tool") {
|
|
1130
|
+
if (entry.timeout) clearTimeout(entry.timeout)
|
|
1131
|
+
entry.stop()
|
|
1132
|
+
state.typingIndicators.delete(key)
|
|
1133
|
+
}
|
|
1079
1134
|
}
|
|
1080
1135
|
}
|
|
1081
1136
|
|
|
@@ -1093,6 +1148,39 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
|
|
|
1093
1148
|
return
|
|
1094
1149
|
}
|
|
1095
1150
|
|
|
1151
|
+
const stopTypingIndicator = (targetKey: string) => {
|
|
1152
|
+
const entry = state.typingIndicators.get(targetKey)
|
|
1153
|
+
if (!entry) return
|
|
1154
|
+
if (entry.timeout) clearTimeout(entry.timeout)
|
|
1155
|
+
entry.stop()
|
|
1156
|
+
state.typingIndicators.delete(targetKey)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const startTypingIndicator = (targetKey: string, mode: "idle" | "tool") => {
|
|
1160
|
+
const existing = state.typingIndicators.get(targetKey)
|
|
1161
|
+
if (existing && existing.mode === mode) return
|
|
1162
|
+
if (existing) {
|
|
1163
|
+
if (existing.timeout) clearTimeout(existing.timeout)
|
|
1164
|
+
existing.stop()
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const stop = state.telegram.startTyping(mode === "tool" ? 1500 : 2500)
|
|
1168
|
+
state.typingIndicators.set(targetKey, { stop, timeout: null, mode })
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const bumpTypingIndicator = (targetKey: string, mode: "idle" | "tool") => {
|
|
1172
|
+
const existing = state.typingIndicators.get(targetKey)
|
|
1173
|
+
if (!existing || existing.mode !== mode) {
|
|
1174
|
+
startTypingIndicator(targetKey, mode)
|
|
1175
|
+
return
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (existing.timeout) clearTimeout(existing.timeout)
|
|
1179
|
+
existing.timeout = setTimeout(() => {
|
|
1180
|
+
stopTypingIndicator(targetKey)
|
|
1181
|
+
}, 12000)
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1096
1184
|
log("debug", "Processing message part", {
|
|
1097
1185
|
key,
|
|
1098
1186
|
partType: part.type,
|
|
@@ -1106,12 +1194,11 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
|
|
|
1106
1194
|
state.pendingParts.set(key, existing)
|
|
1107
1195
|
|
|
1108
1196
|
if (part.type !== "step-finish") {
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
}
|
|
1197
|
+
const typingMode =
|
|
1198
|
+
part.type === "tool" && (part.tool === "edit" || part.tool === "write")
|
|
1199
|
+
? "tool"
|
|
1200
|
+
: "idle"
|
|
1201
|
+
bumpTypingIndicator(key, typingMode)
|
|
1115
1202
|
}
|
|
1116
1203
|
|
|
1117
1204
|
// Send tools/reasoning immediately (except edit/write tools - wait for completion to get diff data)
|
|
@@ -1139,6 +1226,7 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
|
|
|
1139
1226
|
|
|
1140
1227
|
// On step-finish, send remaining parts
|
|
1141
1228
|
if (part.type === "step-finish") {
|
|
1229
|
+
stopTypingIndicator(key)
|
|
1142
1230
|
for (const p of existing) {
|
|
1143
1231
|
if (p.type === "step-start" || p.type === "step-finish") continue
|
|
1144
1232
|
if (state.sentPartIds.has(p.id)) continue
|
|
@@ -1241,6 +1329,24 @@ async function handleOpenCodeEvent(state: BotState, ev: OpenCodeEvent) {
|
|
|
1241
1329
|
}
|
|
1242
1330
|
}
|
|
1243
1331
|
|
|
1332
|
+
if (ev.type === "message.updated") {
|
|
1333
|
+
const info = ev.properties.info
|
|
1334
|
+
if (info?.role === "assistant") {
|
|
1335
|
+
const key = `${info.sessionID}:${info.id}`
|
|
1336
|
+
const entry = state.typingIndicators.get(key)
|
|
1337
|
+
if (entry && entry.mode === "tool") {
|
|
1338
|
+
const stopTypingIndicator = (targetKey: string) => {
|
|
1339
|
+
const existing = state.typingIndicators.get(targetKey)
|
|
1340
|
+
if (!existing) return
|
|
1341
|
+
if (existing.timeout) clearTimeout(existing.timeout)
|
|
1342
|
+
existing.stop()
|
|
1343
|
+
state.typingIndicators.delete(targetKey)
|
|
1344
|
+
}
|
|
1345
|
+
stopTypingIndicator(key)
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1244
1350
|
const threadId = state.threadId ?? 0
|
|
1245
1351
|
|
|
1246
1352
|
if (ev.type === "question.asked") {
|
package/src/telegram.ts
CHANGED
|
@@ -505,11 +505,12 @@ export class TelegramClient {
|
|
|
505
505
|
* Send typing indicator to show the bot is working
|
|
506
506
|
* Returns a stop function to cancel the typing indicator
|
|
507
507
|
*/
|
|
508
|
-
startTyping(intervalMs =
|
|
508
|
+
startTyping(intervalMs = 2500): () => void {
|
|
509
509
|
// Send immediately
|
|
510
510
|
this.sendTypingAction()
|
|
511
511
|
|
|
512
|
-
// Telegram typing indicator lasts ~5 seconds, so refresh every
|
|
512
|
+
// Telegram typing indicator lasts ~5 seconds, so refresh every 2.5 seconds by default
|
|
513
|
+
// to ensure continuous typing even with network delays
|
|
513
514
|
const interval = setInterval(() => {
|
|
514
515
|
this.sendTypingAction()
|
|
515
516
|
}, intervalMs)
|