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.
|
// RegisterCommands registers bot commands on Telegram platform.
|
||||||
func (c *TelegramChannel) RegisterCommands(ctx context.Context, defs []commands.Definition) error {
|
func (c *TelegramChannel) RegisterCommands(ctx context.Context, defs []commands.Definition) error {
|
||||||
botCommands := make([]telego.BotCommand, 0, len(defs))
|
botCommands := buildTelegramBotCommands(defs)
|
||||||
for _, def := range defs {
|
|
||||||
if def.Name == "" || def.Description == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
botCommands = append(botCommands, telego.BotCommand{
|
|
||||||
Command: def.Name,
|
|
||||||
Description: def.Description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
current, err := c.bot.GetMyCommands(ctx, &telego.GetMyCommandsParams{})
|
current, err := c.bot.GetMyCommands(ctx, &telego.GetMyCommandsParams{})
|
||||||
if err != nil {
|
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) {
|
func (c *TelegramChannel) startCommandRegistration(ctx context.Context, defs []commands.Definition) {
|
||||||
if len(defs) == 0 {
|
if len(defs) == 0 {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,13 @@ package telegram
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mymmrac/telego"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/commands"
|
"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())
|
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 != "" {
|
if message.Text != "" {
|
||||||
content += message.Text
|
botUsername := ""
|
||||||
|
if c.bot != nil {
|
||||||
|
botUsername = c.bot.Username()
|
||||||
|
}
|
||||||
|
content += rewriteModelShortcut(message.Text, botUsername)
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.Caption != "" {
|
if message.Caption != "" {
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,85 @@ func TestHandleMessage_DoesNotConsumeGenericCommandsLocally(t *testing.T) {
|
||||||
t.Fatalf("content=%q", inbound.Content)
|
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