Skip to content

Commit 4caa892

Browse files
authored
release: v0.24.4 Refactor attachment delivery flow and decouple media handling
Merge pull request #24 from pardnchiu/develop
2 parents 64e9e1a + ec699a9 commit 4caa892

11 files changed

Lines changed: 143 additions & 117 deletions

File tree

configs/prompts/discord_format.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,10 @@ go, js, ts, py, rs, java, c, cpp, cs, php, rb, swift, kt, sh, bash, sql, json, y
8181

8282
## Sending Files
8383

84-
- To send a local file (image, text file, etc.), include `[SEND_FILE:/absolute/path]` in the reply — the system will automatically attach the file
84+
- To send a local file (image, text file, etc.), include `[SEND_FILE:/absolute/path]` in the reply — after the reply is sent, the system uploads the file in the background
8585
- Multiple files can be sent; use one marker per file: `[SEND_FILE:/path/a.png][SEND_FILE:/path/b.txt]`
8686
- Markers are not displayed in the message text
87+
- **Phrasing**: write the message in **in-progress** tense, not completed tense. Use 「現在傳送中」「正在上傳」「稍後送達」etc.; do NOT use 「已傳送」「已附上」「傳完了」 because the upload has not actually finished when the message is sent
8788

8889
---
8990

configs/prompts/discord_system_prompt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ When a user message contains any of the following time-delay intents, **must** g
105105
When the final output of a task is a **local file** (md, json, txt, etc.):
106106
- **The 1600-character limit applies only to the Discord message reply itself**, not to the file content
107107
- File content prioritizes completeness and is not subject to the character limit
108-
- The Discord message only needs to say "完成,檔案位於 `{path}`" and attach `[SEND_FILE:{path}]` if needed
108+
- The Discord message only needs to say "現在傳送中,檔案位於 `{path}`" (in-progress tense) and attach `[SEND_FILE:{path}]` if needed
109109

110110
### When Reply Is Incomplete
111111
- If the content cannot be fully presented within the character limit, prioritize the most essential conclusion or answer

configs/prompts/telegram_format.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,10 @@ Do not use `<ul>` / `<li>`.
101101

102102
## Sending Files
103103

104-
- To send a local file (image, text file, etc.), include `[SEND_FILE:/absolute/path]` in the reply — the system will automatically attach the file
104+
- To send a local file (image, text file, etc.), include `[SEND_FILE:/absolute/path]` in the reply — after the reply is sent, the system uploads the file in the background
105105
- Multiple files can be sent; use one marker per file: `[SEND_FILE:/path/a.png][SEND_FILE:/path/b.txt]`
106106
- Markers are not displayed in the message text
107+
- **Phrasing**: write the message in **in-progress** tense, not completed tense. Use 「現在傳送中」「正在上傳」「稍後送達」etc.; do NOT use 「已傳送」「已附上」「傳完了」 because the upload has not actually finished when the message is sent
107108
- Images conforming to Telegram photo constraints (PNG/JPG/WebP, width+height ≤ 10000 px, ratio ≤ 20:1, ≤ 10 MB) will be sent as inline photos (multiple images in one reply are grouped as a single Telegram media group); non-conforming files (including SVG, oversized images, archives, source files) are sent as documents
108109

109110
---

configs/prompts/telegram_system_prompt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ When a user message contains any of the following time-delay intents, **must** g
106106
When the final output of a task is a **local file** (md, json, txt, etc.):
107107
- **The 3500-character limit applies only to the Telegram message reply itself**, not to the file content
108108
- File content prioritizes completeness and is not subject to the character limit
109-
- The Telegram message only needs to say "完成,檔案位於 <code>{path}</code>" and attach `[SEND_FILE:{path}]` if needed
109+
- The Telegram message only needs to say "現在傳送中,檔案位於 <code>{path}</code>" (in-progress tense) and attach `[SEND_FILE:{path}]` if needed
110110

111111
### When Reply Is Incomplete
112112
- If the content cannot be fully presented within the character limit, prioritize the most essential conclusion or answer

internal/runtime/discord/attachments.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ func sendAttachments(ctx context.Context, client *go_bot_discord.Bot, channelID,
1717
return
1818
}
1919

20-
notifyFailure := func(label, detail, errMsg string) {
21-
text := fmt.Sprintf("-# ⎿ ⚠️ %s failed", label)
20+
sendFailure := func(label, detail, errMsg string) {
21+
text := fmt.Sprintf("-# ⎿ ⚠️ %s failed (background upload)", label)
2222
if detail != "" {
2323
text = fmt.Sprintf("%s: `%s`", text, detail)
2424
}
2525
text = fmt.Sprintf("%s\n-# ⎿ `%s`", text, errMsg)
2626
if _, err := client.Send(ctx, channelID, replyTo, text); err != nil {
27-
slog.Warn("github.com/pardnchiu/go-bot/discord Bot.Send (notify)",
27+
slog.Error("github.com/pardnchiu/go-bot/discord Bot.Send (notify)",
2828
slog.String("label", label),
2929
slog.String("error", err.Error()))
3030
}
@@ -34,11 +34,12 @@ func sendAttachments(ctx context.Context, client *go_bot_discord.Bot, channelID,
3434
end := min(start+10, len(paths))
3535
batch := paths[start:end]
3636
if _, err := client.SendFiles(ctx, channelID, replyTo, batch); err != nil {
37-
slog.Warn("github.com/pardnchiu/go-bot/discord Bot.SendFiles",
37+
slog.Error("github.com/pardnchiu/go-bot/discord Bot.SendFiles",
3838
slog.String("channel", channelName),
3939
slog.Int("count", len(batch)),
40+
slog.String("paths", strings.Join(batch, ", ")),
4041
slog.String("error", err.Error()))
41-
notifyFailure("SendFiles", strings.Join(batch, ", "), err.Error())
42+
sendFailure("SendFiles", strings.Join(batch, ", "), err.Error())
4243
}
4344
}
4445
}

internal/runtime/discord/run.go

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ func run(ctx context.Context, b *Bot, in go_bot_discord.Input) error {
249249
if doneEvent.Usage != nil {
250250
footer = fmt.Sprintf("%s | in:%s out:%s", footer, utils.FormatUsage(doneEvent.Usage.Input), utils.FormatUsage(doneEvent.Usage.Output))
251251
}
252+
if len(attachmentPaths) > 0 || len(voiceTexts) > 0 {
253+
footer = "🔗 " + footer
254+
}
252255
replyText = fmt.Sprintf("%s\n-# ⎿ %s", replyText, footer)
253256
if len(execErrors) > 0 {
254257
replyText = fmt.Sprintf("%s\n-# ⎿ ⚠️ %s", replyText, strings.Join(execErrors, ", "))
@@ -269,51 +272,47 @@ func run(ctx context.Context, b *Bot, in go_bot_discord.Input) error {
269272
if replyMsg != nil {
270273
replyToID = replyMsg.ID
271274
}
272-
sendFailure := func(label, detail, errMsg string) {
273-
text := fmt.Sprintf("-# ⎿ ⚠️ %s failed", label)
274-
if detail != "" {
275-
text = fmt.Sprintf("%s: `%s`", text, detail)
276-
}
277-
text = fmt.Sprintf("%s\n-# ⎿ `%s`", text, errMsg)
278-
if _, err := b.client.Send(ctx, in.ChannelID, replyToID, text); err != nil {
279-
slog.Warn("github.com/pardnchiu/go-bot/discord Bot.client.Send (notify)",
280-
slog.String("label", label),
281-
slog.String("error", err.Error()))
282-
}
283-
}
284-
285-
if err := b.client.SendStatus(ctx, in.ChannelID, replyToID, "sending…",
286-
go_bot_discord.WithStatusEmoji("⚡")); err != nil {
287-
slog.Warn("github.com/pardnchiu/go-bot/discord Bot.client.SendStatus",
288-
slog.String("channel", channelName(in)),
289-
slog.String("error", err.Error()))
290-
}
291275

292276
if len(attachmentPaths) > 0 {
293-
sendAttachments(ctx, b.client, in.ChannelID, channelName(in), replyToID, attachmentPaths)
277+
bgCtx := context.WithoutCancel(ctx)
278+
channel := channelName(in)
279+
client := b.client
280+
paths := attachmentPaths
281+
go sendAttachments(bgCtx, client, in.ChannelID, channel, replyToID, paths)
294282
}
295283

296284
if len(voiceTexts) > 0 {
297-
apiKey := strings.TrimSpace(keychain.Get("GEMINI_API_KEY"))
298-
if apiKey == "" {
299-
slog.Warn("keychain.Get GEMINI_API_KEY missing",
300-
slog.String("channel", channelName(in)))
301-
sendFailure("SendVoice", "", "GEMINI_API_KEY missing")
302-
} else {
303-
for _, text := range voiceTexts {
304-
if _, err := b.client.SendVoice(ctx, in.ChannelID, replyToID, text, apiKey); err != nil {
305-
slog.Warn("github.com/pardnchiu/go-bot/discord Bot.client.SendVoice",
285+
bgCtx := context.WithoutCancel(ctx)
286+
channel := channelName(in)
287+
channelID := in.ChannelID
288+
reply := replyToID
289+
client := b.client
290+
texts := voiceTexts
291+
go func() {
292+
sendFailure := func(errMsg string) {
293+
text := fmt.Sprintf("-# ⎿ ⚠️ SendVoice failed (background)\n-# ⎿ `%s`", errMsg)
294+
if _, err := client.Send(bgCtx, channelID, reply, text); err != nil {
295+
slog.Error("github.com/pardnchiu/go-bot/discord Bot.client.Send (notify)",
296+
slog.String("channel", channel),
306297
slog.String("error", err.Error()))
307-
sendFailure("SendVoice", "", err.Error())
308298
}
309299
}
310-
}
311-
}
312-
313-
if err := b.client.FinishStatus(ctx, in.ChannelID); err != nil {
314-
slog.Warn("github.com/pardnchiu/go-bot/discord Bot.client.FinishStatus",
315-
slog.String("channel", channelName(in)),
316-
slog.String("error", err.Error()))
300+
apiKey := strings.TrimSpace(keychain.Get("GEMINI_API_KEY"))
301+
if apiKey == "" {
302+
slog.Error("keychain.Get GEMINI_API_KEY missing",
303+
slog.String("channel", channel))
304+
sendFailure("GEMINI_API_KEY missing")
305+
return
306+
}
307+
for _, text := range texts {
308+
if _, err := client.SendVoice(bgCtx, channelID, reply, text, apiKey); err != nil {
309+
slog.Error("github.com/pardnchiu/go-bot/discord Bot.client.SendVoice",
310+
slog.String("channel", channel),
311+
slog.String("error", err.Error()))
312+
sendFailure(err.Error())
313+
}
314+
}
315+
}()
317316
}
318317

319318
return nil

internal/runtime/telegram/attachments.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import (
1818

1919
const attachmentSendTimeout = 10 * time.Minute
2020

21-
func sendAttachments(ctx context.Context, chatID int64, chatName string, replyToID int, photoPaths, docPaths []string) {
21+
func sendAttachments(ctx context.Context, chatID int64, chatName string, photoPaths, docPaths []string) {
2222
if len(photoPaths) == 0 && len(docPaths) == 0 {
2323
return
2424
}
2525

2626
token := strings.TrimSpace(keychain.Get(Key))
2727
if token == "" {
28-
slog.Warn("github.com/pardnchiu/go-pkg/filesystem/keychain Get",
28+
slog.Error("github.com/pardnchiu/go-pkg/filesystem/keychain Get",
2929
slog.String("chat", chatName),
3030
slog.String("key", Key))
3131
return
@@ -34,7 +34,7 @@ func sendAttachments(ctx context.Context, chatID int64, chatName string, replyTo
3434
client, err := go_bot_telegram.New(token,
3535
go_bot_telegram.WithHTTPClient(&http.Client{Timeout: attachmentSendTimeout}))
3636
if err != nil {
37-
slog.Warn("github.com/pardnchiu/go-bot/telegram New",
37+
slog.Error("github.com/pardnchiu/go-bot/telegram New",
3838
slog.String("chat", chatName),
3939
slog.String("error", err.Error()))
4040
return
@@ -45,9 +45,9 @@ func sendAttachments(ctx context.Context, chatID int64, chatName string, replyTo
4545
if detail != "" {
4646
body = fmt.Sprintf("<code>%s</code>: %s", html.EscapeString(detail), body)
4747
}
48-
text := fmt.Sprintf("⚠️ %s failed\n%s", label, body)
49-
if _, err := client.Send(ctx, chatID, replyToID, text, go_bot_telegram.WithSendType(go_bot_telegram.TypeHTML)); err != nil {
50-
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.Send (notify)",
48+
text := fmt.Sprintf("⚠️ %s failed (background upload)\n%s", label, body)
49+
if _, err := client.Send(ctx, chatID, 0, text, go_bot_telegram.WithSendType(go_bot_telegram.TypeHTML)); err != nil {
50+
slog.Error("github.com/pardnchiu/go-bot/telegram Bot.Send (notify)",
5151
slog.String("label", label),
5252
slog.String("error", err.Error()))
5353
}
@@ -57,16 +57,17 @@ func sendAttachments(ctx context.Context, chatID int64, chatName string, replyTo
5757
end := start + 10
5858
end = min(end, len(photoPaths))
5959
if _, err := client.SendPhoto(ctx, chatID, photoPaths[start:end]); err != nil {
60-
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.SendPhoto",
60+
slog.Error("github.com/pardnchiu/go-bot/telegram Bot.SendPhoto",
6161
slog.String("chat", chatName),
6262
slog.Int("count", end-start),
63+
slog.String("paths", strings.Join(photoPaths[start:end], ", ")),
6364
slog.String("error", err.Error()))
6465
notifyFailure("SendPhoto", strings.Join(photoPaths[start:end], ", "), err.Error())
6566
}
6667
}
6768
for _, path := range docPaths {
6869
if _, err := client.SendFile(ctx, chatID, go_bot_telegram.TypeDocument, path); err != nil {
69-
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.SendFile",
70+
slog.Error("github.com/pardnchiu/go-bot/telegram Bot.SendFile",
7071
slog.String("chat", chatName),
7172
slog.String("path", path),
7273
slog.String("error", err.Error()))

internal/runtime/telegram/push.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func PushTelegramResult(ctx context.Context, payload exec.PushPayload) {
7272
}
7373
}
7474

75-
sendAttachments(ctx, chatID, chatName, 0, photoPaths, docPaths)
75+
sendAttachments(ctx, chatID, chatName, photoPaths, docPaths)
7676
}
7777

7878
func buildPushFooter(model string, usage *agentTypes.Usage) string {

internal/runtime/telegram/run.go

Lines changed: 34 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ func run(ctx context.Context, b *Bot, in go_bot_telegram.Input) error {
270270
if doneEvent.Usage != nil {
271271
footer = fmt.Sprintf("%s | in:%s out:%s", footer, utils.FormatUsage(doneEvent.Usage.Input), utils.FormatUsage(doneEvent.Usage.Output))
272272
}
273+
if len(photoPaths) > 0 || len(docPaths) > 0 || len(voiceTexts) > 0 {
274+
footer = "🔗 " + footer
275+
}
273276
replyText = fmt.Sprintf("%s\n\n<blockquote expandable>%s</blockquote>", replyText, footer)
274277
if len(execErrors) > 0 {
275278
replyText = fmt.Sprintf("%s\n\n<blockquote expandable>⚠️ %s</blockquote>", replyText, strings.Join(execErrors, ", "))
@@ -278,7 +281,7 @@ func run(ctx context.Context, b *Bot, in go_bot_telegram.Input) error {
278281
if in.MessageID != 0 {
279282
replyText = "​\n" + replyText
280283
}
281-
replyMsg, sendErr := b.client.Send(ctx, in.ChatID, in.MessageID, replyText, go_bot_telegram.WithSendType(go_bot_telegram.TypeHTML))
284+
_, sendErr := b.client.Send(ctx, in.ChatID, in.MessageID, replyText, go_bot_telegram.WithSendType(go_bot_telegram.TypeHTML))
282285
if sendErr != nil {
283286
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.client.Send",
284287
slog.String("error", sendErr.Error()))
@@ -288,62 +291,43 @@ func run(ctx context.Context, b *Bot, in go_bot_telegram.Input) error {
288291
return nil
289292
}
290293

291-
replyToID := 0
292-
if replyMsg != nil {
293-
replyToID = replyMsg.ID
294-
}
295-
sendStatus := func(text string) {
296-
wrapped := fmt.Sprintf("<blockquote expandable>%s</blockquote>", html.EscapeString(text))
297-
if err := b.client.SendStatus(ctx, in.ChatID, replyToID, wrapped,
298-
go_bot_telegram.WithStatusEmoji("⚡"),
299-
go_bot_telegram.WithStatusSendType(go_bot_telegram.TypeHTML),
300-
); err != nil {
301-
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.client.SendStatus",
302-
slog.String("text", text),
303-
slog.String("chat", chatName(in)),
304-
slog.Int("replyTo", replyToID),
305-
slog.String("error", err.Error()))
306-
}
307-
}
308-
sendFailure := func(label, detail, errMsg string) {
309-
body := fmt.Sprintf("<code>%s</code>", html.EscapeString(errMsg))
310-
if detail != "" {
311-
body = fmt.Sprintf("<code>%s</code>: %s", html.EscapeString(detail), body)
312-
}
313-
text := fmt.Sprintf("⚠️ %s failed\n%s", label, body)
314-
if _, err := b.client.Send(ctx, in.ChatID, replyToID, text, go_bot_telegram.WithSendType(go_bot_telegram.TypeHTML)); err != nil {
315-
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.client.Send (notify)",
316-
slog.String("label", label),
317-
slog.String("error", err.Error()))
318-
}
319-
}
320-
sendStatus("sending…")
321-
322294
if len(photoPaths) > 0 || len(docPaths) > 0 {
323-
sendAttachments(ctx, in.ChatID, chatName(in), replyToID, photoPaths, docPaths)
295+
bgCtx := context.WithoutCancel(ctx)
296+
chat := chatName(in)
297+
go sendAttachments(bgCtx, in.ChatID, chat, photoPaths, docPaths)
324298
}
325299

326300
if len(voiceTexts) > 0 {
327-
apiKey := strings.TrimSpace(keychain.Get("GEMINI_API_KEY"))
328-
if apiKey == "" {
329-
slog.Warn("keychain.Get GEMINI_API_KEY missing",
330-
slog.String("chat", chatName(in)))
331-
sendFailure("SendVoice", "", "GEMINI_API_KEY missing")
332-
} else {
333-
for _, text := range voiceTexts {
334-
if _, err := b.client.SendVoice(ctx, in.ChatID, text, apiKey); err != nil {
335-
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.client.SendVoice",
301+
bgCtx := context.WithoutCancel(ctx)
302+
chat := chatName(in)
303+
chatID := in.ChatID
304+
client := b.client
305+
texts := voiceTexts
306+
go func() {
307+
notifyFailure := func(errMsg string) {
308+
text := fmt.Sprintf("⚠️ SendVoice failed (background)\n<code>%s</code>", html.EscapeString(errMsg))
309+
if _, err := client.Send(bgCtx, chatID, 0, text, go_bot_telegram.WithSendType(go_bot_telegram.TypeHTML)); err != nil {
310+
slog.Error("github.com/pardnchiu/go-bot/telegram Bot.client.Send (notify)",
311+
slog.String("chat", chat),
336312
slog.String("error", err.Error()))
337-
sendFailure("SendVoice", "", err.Error())
338313
}
339314
}
340-
}
341-
}
342-
343-
if err := b.client.FinishStatus(ctx, in.ChatID); err != nil {
344-
slog.Warn("github.com/pardnchiu/go-bot/telegram Bot.client.FinishStatus",
345-
slog.String("chat", chatName(in)),
346-
slog.String("error", err.Error()))
315+
apiKey := strings.TrimSpace(keychain.Get("GEMINI_API_KEY"))
316+
if apiKey == "" {
317+
slog.Error("keychain.Get GEMINI_API_KEY missing",
318+
slog.String("chat", chat))
319+
notifyFailure("GEMINI_API_KEY missing")
320+
return
321+
}
322+
for _, text := range texts {
323+
if _, err := client.SendVoice(bgCtx, chatID, text, apiKey); err != nil {
324+
slog.Error("github.com/pardnchiu/go-bot/telegram Bot.client.SendVoice",
325+
slog.String("chat", chat),
326+
slog.String("error", err.Error()))
327+
notifyFailure(err.Error())
328+
}
329+
}
330+
}()
347331
}
348332

349333
return nil

internal/utils/fileMarker.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package utils
22

33
import (
4+
"os"
45
"regexp"
56
"strings"
67
)
@@ -12,13 +13,14 @@ var (
1213

1314
func ExtractFileMarkers(text string) (cleanText string, paths []string) {
1415
seen := map[string]bool{}
16+
var raw []string
1517
collect := func(path string) {
1618
path = strings.TrimSpace(path)
1719
if path == "" || seen[path] {
1820
return
1921
}
2022
seen[path] = true
21-
paths = append(paths, path)
23+
raw = append(raw, path)
2224
}
2325

2426
for _, m := range fileMarkerRegex.FindAllStringSubmatch(text, -1) {
@@ -31,6 +33,14 @@ func ExtractFileMarkers(text string) (cleanText string, paths []string) {
3133
}
3234
text = fileLineRegex.ReplaceAllString(text, "")
3335

36+
for _, p := range raw {
37+
info, err := os.Stat(p)
38+
if err != nil || info.IsDir() {
39+
continue
40+
}
41+
paths = append(paths, p)
42+
}
43+
3444
cleanText = strings.TrimSpace(text)
3545
return
3646
}

0 commit comments

Comments
 (0)