feat(telegram): add /models shortcut for model list and switch

This commit is contained in:
Alix-007 2026-03-29 17:11:03 +08:00
parent e70928cc6f
commit 761a932263
5 changed files with 184 additions and 11 deletions

View file

@ -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

View file

@ -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)
}
}

View 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:], " ")
}

View file

@ -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 != "" {

View file

@ -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)
}
}