feat(telegram): add /models shortcut for model list and switch
This commit is contained in:
parent
e70928cc6f
commit
761a932263
5 changed files with 184 additions and 11 deletions
|
|
@ -31,16 +31,7 @@ func commandRegistrationDelay(attempt int) time.Duration {
|
|||
|
||||
// RegisterCommands registers bot commands on Telegram platform.
|
||||
func (c *TelegramChannel) RegisterCommands(ctx context.Context, defs []commands.Definition) error {
|
||||
botCommands := make([]telego.BotCommand, 0, len(defs))
|
||||
for _, def := range defs {
|
||||
if def.Name == "" || def.Description == "" {
|
||||
continue
|
||||
}
|
||||
botCommands = append(botCommands, telego.BotCommand{
|
||||
Command: def.Name,
|
||||
Description: def.Description,
|
||||
})
|
||||
}
|
||||
botCommands := buildTelegramBotCommands(defs)
|
||||
|
||||
current, err := c.bot.GetMyCommands(ctx, &telego.GetMyCommandsParams{})
|
||||
if err != nil {
|
||||
|
|
@ -57,6 +48,34 @@ func (c *TelegramChannel) RegisterCommands(ctx context.Context, defs []commands.
|
|||
})
|
||||
}
|
||||
|
||||
func buildTelegramBotCommands(defs []commands.Definition) []telego.BotCommand {
|
||||
const modelsCommand = "models"
|
||||
const modelsDescription = "List models or switch: /models <name>"
|
||||
|
||||
botCommands := make([]telego.BotCommand, 0, len(defs)+1)
|
||||
hasModelsShortcut := false
|
||||
for _, def := range defs {
|
||||
if def.Name == "" || def.Description == "" {
|
||||
continue
|
||||
}
|
||||
if def.Name == modelsCommand {
|
||||
hasModelsShortcut = true
|
||||
}
|
||||
botCommands = append(botCommands, telego.BotCommand{
|
||||
Command: def.Name,
|
||||
Description: def.Description,
|
||||
})
|
||||
}
|
||||
if !hasModelsShortcut {
|
||||
botCommands = append(botCommands, telego.BotCommand{
|
||||
Command: modelsCommand,
|
||||
Description: modelsDescription,
|
||||
})
|
||||
}
|
||||
|
||||
return botCommands
|
||||
}
|
||||
|
||||
func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []commands.Definition) {
|
||||
if len(defs) == 0 {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,10 +3,13 @@ package telegram
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mymmrac/telego"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/commands"
|
||||
)
|
||||
|
||||
|
|
@ -94,3 +97,37 @@ func TestStartCommandRegistration_StopsAfterCancel(t *testing.T) {
|
|||
t.Fatalf("expected retries to quiesce after cancel, got %d -> %d", stable, attempts.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTelegramBotCommands_AddsModelsShortcutWhenMissing(t *testing.T) {
|
||||
defs := []commands.Definition{
|
||||
{Name: "help", Description: "Help"},
|
||||
{Name: "list", Description: "List"},
|
||||
}
|
||||
|
||||
got := buildTelegramBotCommands(defs)
|
||||
|
||||
models := telego.BotCommand{
|
||||
Command: "models",
|
||||
Description: "List models or switch: /models <name>",
|
||||
}
|
||||
if !slices.Contains(got, models) {
|
||||
t.Fatalf("expected models shortcut in bot commands, got: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTelegramBotCommands_DoesNotDuplicateExistingModelsCommand(t *testing.T) {
|
||||
defs := []commands.Definition{
|
||||
{Name: "models", Description: "Configured models"},
|
||||
}
|
||||
|
||||
got := buildTelegramBotCommands(defs)
|
||||
count := 0
|
||||
for _, cmd := range got {
|
||||
if cmd.Command == "models" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("expected 1 models command, got %d (%#v)", count, got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
pkg/channels/telegram/model_shortcut.go
Normal file
31
pkg/channels/telegram/model_shortcut.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package telegram
|
||||
|
||||
import "strings"
|
||||
|
||||
// rewriteModelShortcut normalizes Telegram-specific /models shortcuts into
|
||||
// existing cross-channel commands handled by the command runtime.
|
||||
func rewriteModelShortcut(input, botUsername string) string {
|
||||
parts := strings.Fields(strings.TrimSpace(input))
|
||||
if len(parts) == 0 {
|
||||
return input
|
||||
}
|
||||
|
||||
token := parts[0]
|
||||
if !strings.HasPrefix(token, "/") {
|
||||
return input
|
||||
}
|
||||
|
||||
name, target, hasTarget := strings.Cut(strings.TrimPrefix(token, "/"), "@")
|
||||
if !strings.EqualFold(name, "models") {
|
||||
return input
|
||||
}
|
||||
if hasTarget && (botUsername == "" || !strings.EqualFold(target, botUsername)) {
|
||||
return input
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
return "/list models"
|
||||
}
|
||||
|
||||
return "/switch model to " + strings.Join(parts[1:], " ")
|
||||
}
|
||||
|
|
@ -583,7 +583,11 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
|||
}
|
||||
|
||||
if message.Text != "" {
|
||||
content += message.Text
|
||||
botUsername := ""
|
||||
if c.bot != nil {
|
||||
botUsername = c.bot.Username()
|
||||
}
|
||||
content += rewriteModelShortcut(message.Text, botUsername)
|
||||
}
|
||||
|
||||
if message.Caption != "" {
|
||||
|
|
|
|||
|
|
@ -46,3 +46,85 @@ func TestHandleMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
|||
t.Fatalf("content=%q", inbound.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMessage_RewritesModelsShortcutToListModels(t *testing.T) {
|
||||
messageBus := bus.NewMessageBus()
|
||||
ch := &TelegramChannel{
|
||||
BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil),
|
||||
chatIDs: make(map[string]int64),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
msg := &telego.Message{
|
||||
Text: "/models",
|
||||
MessageID: 10,
|
||||
Chat: telego.Chat{
|
||||
ID: 123,
|
||||
Type: "private",
|
||||
},
|
||||
From: &telego.User{
|
||||
ID: 42,
|
||||
FirstName: "Alice",
|
||||
},
|
||||
}
|
||||
|
||||
if err := ch.handleMessage(context.Background(), msg); err != nil {
|
||||
t.Fatalf("handleMessage error: %v", err)
|
||||
}
|
||||
|
||||
inbound, ok := <-messageBus.InboundChan()
|
||||
if !ok {
|
||||
t.Fatal("expected inbound message to be forwarded")
|
||||
}
|
||||
if inbound.Content != "/list models" {
|
||||
t.Fatalf("content=%q", inbound.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMessage_RewritesModelsShortcutWithArgumentToSwitchModel(t *testing.T) {
|
||||
messageBus := bus.NewMessageBus()
|
||||
ch := &TelegramChannel{
|
||||
BaseChannel: channels.NewBaseChannel("telegram", nil, messageBus, nil),
|
||||
chatIDs: make(map[string]int64),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
msg := &telego.Message{
|
||||
Text: "/models qwen-max",
|
||||
MessageID: 11,
|
||||
Chat: telego.Chat{
|
||||
ID: 123,
|
||||
Type: "private",
|
||||
},
|
||||
From: &telego.User{
|
||||
ID: 42,
|
||||
FirstName: "Alice",
|
||||
},
|
||||
}
|
||||
|
||||
if err := ch.handleMessage(context.Background(), msg); err != nil {
|
||||
t.Fatalf("handleMessage error: %v", err)
|
||||
}
|
||||
|
||||
inbound, ok := <-messageBus.InboundChan()
|
||||
if !ok {
|
||||
t.Fatal("expected inbound message to be forwarded")
|
||||
}
|
||||
if inbound.Content != "/switch model to qwen-max" {
|
||||
t.Fatalf("content=%q", inbound.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteModelShortcut_WithBotUsernameTarget(t *testing.T) {
|
||||
got := rewriteModelShortcut("/models@testbot qwen-max", "testbot")
|
||||
if got != "/switch model to qwen-max" {
|
||||
t.Fatalf("rewrite=%q, want %q", got, "/switch model to qwen-max")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteModelShortcut_DoesNotRewriteOtherBotTarget(t *testing.T) {
|
||||
got := rewriteModelShortcut("/models@otherbot qwen-max", "testbot")
|
||||
if got != "/models@otherbot qwen-max" {
|
||||
t.Fatalf("rewrite=%q, want unchanged", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue