Add Telegram file upload directive
This commit is contained in:
@@ -26,11 +26,12 @@ const (
|
||||
resumeThreadPageSize = 8
|
||||
assistantStreamEditInterval = 1200 * time.Millisecond
|
||||
assistantStreamInitialRunes = 24
|
||||
telegramFileDirectiveStart = "<!-- telegram-file "
|
||||
telegramPhotoDirectiveStart = "<!-- telegram-photo "
|
||||
telegramThreadRenameDirectiveStart = "<!-- codex-thread-rename "
|
||||
telegramThreadCWDDirectiveStart = "<!-- codex-thread-cwd "
|
||||
telegramDirectiveEnd = " -->"
|
||||
telegramPhotoCaptionLimit = 1024
|
||||
telegramCaptionLimit = 1024
|
||||
pictureMediaGroupLimit = 10
|
||||
)
|
||||
|
||||
@@ -54,11 +55,17 @@ type Bot struct {
|
||||
|
||||
type assistantMessageSegment struct {
|
||||
Text string
|
||||
File *assistantFileDirective
|
||||
Photo *assistantPhotoDirective
|
||||
ThreadRename *assistantThreadRenameDirective
|
||||
ThreadCWD *assistantThreadCWDDirective
|
||||
}
|
||||
|
||||
type assistantFileDirective struct {
|
||||
Path string `json:"path"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
}
|
||||
|
||||
type assistantPhotoDirective struct {
|
||||
Path string `json:"path"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
@@ -2580,6 +2587,11 @@ func splitAssistantMessageSegments(text string) []assistantMessageSegment {
|
||||
for _, line := range strings.SplitAfter(text, "\n") {
|
||||
body := strings.TrimSuffix(line, "\n")
|
||||
body = strings.TrimSuffix(body, "\r")
|
||||
if directive, ok := parseAssistantFileDirectiveLine(body); ok {
|
||||
flushVisible()
|
||||
segments = append(segments, assistantMessageSegment{File: &directive})
|
||||
continue
|
||||
}
|
||||
if directive, ok := parseAssistantPhotoDirectiveLine(body); ok {
|
||||
flushVisible()
|
||||
segments = append(segments, assistantMessageSegment{Photo: &directive})
|
||||
@@ -2631,6 +2643,22 @@ func parseAssistantThreadCWDDirectiveLine(line string) (assistantThreadCWDDirect
|
||||
return directive, true
|
||||
}
|
||||
|
||||
func parseAssistantFileDirectiveLine(line string) (assistantFileDirective, bool) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, telegramFileDirectiveStart) || !strings.HasSuffix(trimmed, telegramDirectiveEnd) {
|
||||
return assistantFileDirective{}, false
|
||||
}
|
||||
raw := strings.TrimSuffix(strings.TrimPrefix(trimmed, telegramFileDirectiveStart), telegramDirectiveEnd)
|
||||
raw = strings.TrimSpace(raw)
|
||||
var directive assistantFileDirective
|
||||
if err := json.Unmarshal([]byte(raw), &directive); err != nil {
|
||||
return assistantFileDirective{}, false
|
||||
}
|
||||
directive.Path = strings.TrimSpace(directive.Path)
|
||||
directive.Caption = strings.TrimSpace(directive.Caption)
|
||||
return directive, true
|
||||
}
|
||||
|
||||
func parseAssistantPhotoDirectiveLine(line string) (assistantPhotoDirective, bool) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, telegramPhotoDirectiveStart) || !strings.HasSuffix(trimmed, telegramDirectiveEnd) {
|
||||
@@ -2654,6 +2682,14 @@ func (b *Bot) sendAssistantText(ctx context.Context, threadID string, chatID int
|
||||
return err
|
||||
}
|
||||
}
|
||||
if segment.File != nil {
|
||||
if err := b.sendAssistantFile(ctx, chatID, *segment.File); err != nil {
|
||||
b.logger.Printf("send assistant file: %v", err)
|
||||
if sendErr := b.sendLong(ctx, chatID, "Could not send file: "+err.Error()); sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
}
|
||||
if segment.Photo != nil {
|
||||
if err := b.sendAssistantPhoto(ctx, chatID, *segment.Photo); err != nil {
|
||||
b.logger.Printf("send assistant photo: %v", err)
|
||||
@@ -2721,6 +2757,25 @@ func (b *Bot) applyAssistantThreadCWD(ctx context.Context, threadID string, dire
|
||||
return b.store.SetSessionWorkspace(ctx, thread.TelegramUserID, workspace.ID)
|
||||
}
|
||||
|
||||
func (b *Bot) sendAssistantFile(ctx context.Context, chatID int64, directive assistantFileDirective) error {
|
||||
path := strings.TrimSpace(directive.Path)
|
||||
if path == "" {
|
||||
return errors.New("file directive is missing a path")
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
return fmt.Errorf("file path must be absolute: %s", path)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %v", filepath.Base(path), err)
|
||||
}
|
||||
caption := truncateTelegramCaption(directive.Caption)
|
||||
if _, err := b.tg.SendDocumentBytes(ctx, chatID, path, data, caption); err != nil {
|
||||
return fmt.Errorf("send %s: %v", filepath.Base(path), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) sendAssistantPhoto(ctx context.Context, chatID int64, directive assistantPhotoDirective) error {
|
||||
path := strings.TrimSpace(directive.Path)
|
||||
if path == "" {
|
||||
@@ -2736,22 +2791,22 @@ func (b *Bot) sendAssistantPhoto(ctx context.Context, chatID int64, directive as
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %v", filepath.Base(path), err)
|
||||
}
|
||||
caption := truncateTelegramPhotoCaption(directive.Caption)
|
||||
caption := truncateTelegramCaption(directive.Caption)
|
||||
if _, err := b.tg.SendPhotoBytes(ctx, chatID, path, data, caption); err != nil {
|
||||
return fmt.Errorf("send %s: %v", filepath.Base(path), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncateTelegramPhotoCaption(caption string) string {
|
||||
func truncateTelegramCaption(caption string) string {
|
||||
runes := []rune(caption)
|
||||
if len(runes) <= telegramPhotoCaptionLimit {
|
||||
if len(runes) <= telegramCaptionLimit {
|
||||
return caption
|
||||
}
|
||||
if telegramPhotoCaptionLimit <= 3 {
|
||||
return string(runes[:telegramPhotoCaptionLimit])
|
||||
if telegramCaptionLimit <= 3 {
|
||||
return string(runes[:telegramCaptionLimit])
|
||||
}
|
||||
return string(runes[:telegramPhotoCaptionLimit-3]) + "..."
|
||||
return string(runes[:telegramCaptionLimit-3]) + "..."
|
||||
}
|
||||
|
||||
func assistantStreamPreview(text string) string {
|
||||
@@ -2768,7 +2823,9 @@ func assistantStreamPreview(text string) string {
|
||||
}
|
||||
|
||||
func isAssistantDirectiveStart(line string) bool {
|
||||
return strings.HasPrefix(line, telegramPhotoDirectiveStart) ||
|
||||
return strings.HasPrefix(line, telegramFileDirectiveStart) ||
|
||||
strings.HasPrefix(line, strings.TrimSpace(telegramFileDirectiveStart)) ||
|
||||
strings.HasPrefix(line, telegramPhotoDirectiveStart) ||
|
||||
strings.HasPrefix(line, strings.TrimSpace(telegramPhotoDirectiveStart)) ||
|
||||
strings.HasPrefix(line, telegramThreadRenameDirectiveStart) ||
|
||||
strings.HasPrefix(line, strings.TrimSpace(telegramThreadRenameDirectiveStart)) ||
|
||||
|
||||
@@ -252,6 +252,24 @@ func TestSplitAssistantMessageSegmentsWithPhotoDirective(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitAssistantMessageSegmentsWithFileDirective(t *testing.T) {
|
||||
filePath := filepath.Join(string(filepath.Separator), "workspace", "report.pdf")
|
||||
text := fmt.Sprintf("before\n<!-- telegram-file {\"path\":%q,\"caption\":\"report\"} -->\nafter", filePath)
|
||||
segments := splitAssistantMessageSegments(text)
|
||||
if len(segments) != 3 {
|
||||
t.Fatalf("segments = %d, want 3: %#v", len(segments), segments)
|
||||
}
|
||||
if segments[0].Text != "before\n" || segments[0].File != nil {
|
||||
t.Fatalf("unexpected first segment: %#v", segments[0])
|
||||
}
|
||||
if segments[1].File == nil || segments[1].File.Path != filePath || segments[1].File.Caption != "report" {
|
||||
t.Fatalf("unexpected file segment: %#v", segments[1])
|
||||
}
|
||||
if segments[2].Text != "after" || segments[2].File != nil {
|
||||
t.Fatalf("unexpected final segment: %#v", segments[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPhotoDirectiveStaysVisible(t *testing.T) {
|
||||
text := "<!-- telegram-photo not-json -->"
|
||||
segments := splitAssistantMessageSegments(text)
|
||||
@@ -261,7 +279,7 @@ func TestInvalidPhotoDirectiveStaysVisible(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAssistantStreamPreviewHidesDirectives(t *testing.T) {
|
||||
text := "before\n<!-- telegram-photo {\"path\":\"/workspace/photo.jpg\"} -->\nafter\n<!-- codex-thread-rename "
|
||||
text := "before\n<!-- telegram-file {\"path\":\"/workspace/report.pdf\"} -->\n<!-- telegram-photo {\"path\":\"/workspace/photo.jpg\"} -->\nafter\n<!-- codex-thread-rename "
|
||||
got := assistantStreamPreview(text)
|
||||
want := "before\nafter\n"
|
||||
if got != want {
|
||||
|
||||
Reference in New Issue
Block a user