refactor(commands): use shared /switch model path

This commit is contained in:
niuguy 2026-03-10 21:06:58 +00:00
parent 47accae0d8
commit ffb7841572
7 changed files with 49 additions and 287 deletions

View file

@ -25,7 +25,6 @@ import (
_ "github.com/sipeed/picoclaw/pkg/channels/qq" _ "github.com/sipeed/picoclaw/pkg/channels/qq"
_ "github.com/sipeed/picoclaw/pkg/channels/slack" _ "github.com/sipeed/picoclaw/pkg/channels/slack"
_ "github.com/sipeed/picoclaw/pkg/channels/telegram" _ "github.com/sipeed/picoclaw/pkg/channels/telegram"
tgchannel "github.com/sipeed/picoclaw/pkg/channels/telegram"
_ "github.com/sipeed/picoclaw/pkg/channels/wecom" _ "github.com/sipeed/picoclaw/pkg/channels/wecom"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp" _ "github.com/sipeed/picoclaw/pkg/channels/whatsapp"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp_native" _ "github.com/sipeed/picoclaw/pkg/channels/whatsapp_native"
@ -144,12 +143,6 @@ func gatewayCmd(debug bool) error {
logger.InfoCF("voice", "Transcription enabled (agent-level)", map[string]any{"provider": transcriber.Name()}) logger.InfoCF("voice", "Transcription enabled (agent-level)", map[string]any{"provider": transcriber.Name()})
} }
if telegramCh, ok := channelManager.GetChannel("telegram"); ok {
if tc, ok := telegramCh.(*tgchannel.TelegramChannel); ok {
tc.SetRegistry(agentLoop.GetRegistry())
}
}
enabledChannels := channelManager.GetEnabledChannels() enabledChannels := channelManager.GetEnabledChannels()
if len(enabledChannels) > 0 { if len(enabledChannels) > 0 {
fmt.Printf("✓ Channels enabled: %s\n", enabledChannels) fmt.Printf("✓ Channels enabled: %s\n", enabledChannels)

View file

@ -1812,12 +1812,18 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt
} }
if agent != nil { if agent != nil {
rt.GetModelInfo = func() (string, string) { rt.GetModelInfo = func() (string, string) {
return agent.Model, al.cfg.Agents.Defaults.Provider provider := al.cfg.Agents.Defaults.Provider
if len(agent.Candidates) > 0 && agent.Candidates[0].Provider != "" {
provider = agent.Candidates[0].Provider
}
return agent.Model, provider
} }
rt.SwitchModel = func(value string) (string, error) { rt.SwitchModel = func(value string) (string, error) {
oldModel := agent.Model if al.registry == nil {
agent.Model = value return "", fmt.Errorf("agent registry not initialized")
return oldModel, nil }
oldModel, _, err := al.registry.SwitchDefaultAgentModel(value)
return oldModel, err
} }
rt.ClearHistory = func() error { rt.ClearHistory = func() error {

View file

@ -531,10 +531,23 @@ func TestProcessMessage_SwitchModelShowModelConsistency(t *testing.T) {
Workspace: tmpDir, Workspace: tmpDir,
Provider: "openai", Provider: "openai",
Model: "before-switch", Model: "before-switch",
ModelName: "test-openai-mini",
MaxTokens: 4096, MaxTokens: 4096,
MaxToolIterations: 10, MaxToolIterations: 10,
}, },
}, },
ModelList: []config.ModelConfig{
{
ModelName: "test-openai-mini",
Model: "openai/fake-openai-mini",
APIKey: "test-openai-key",
},
{
ModelName: "test-qwen-plus",
Model: "qwen/fake-qwen-plus",
APIKey: "test-qwen-key",
},
},
} }
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
@ -546,13 +559,13 @@ func TestProcessMessage_SwitchModelShowModelConsistency(t *testing.T) {
Channel: "telegram", Channel: "telegram",
SenderID: "user1", SenderID: "user1",
ChatID: "chat1", ChatID: "chat1",
Content: "/switch model to after-switch", Content: "/switch model to test-qwen-plus",
Peer: bus.Peer{ Peer: bus.Peer{
Kind: "direct", Kind: "direct",
ID: "user1", ID: "user1",
}, },
}) })
if !strings.Contains(switchResp, "Switched model from before-switch to after-switch") { if !strings.Contains(switchResp, "Switched model from test-openai-mini to fake-qwen-plus") {
t.Fatalf("unexpected /switch reply: %q", switchResp) t.Fatalf("unexpected /switch reply: %q", switchResp)
} }
@ -566,7 +579,7 @@ func TestProcessMessage_SwitchModelShowModelConsistency(t *testing.T) {
ID: "user1", ID: "user1",
}, },
}) })
if !strings.Contains(showResp, "Current Model: after-switch (Provider: openai)") { if !strings.Contains(showResp, "Current Model: fake-qwen-plus (Provider: qwen)") {
t.Fatalf("unexpected /show model reply after switch: %q", showResp) t.Fatalf("unexpected /show model reply after switch: %q", showResp)
} }

View file

@ -40,13 +40,12 @@ var (
type TelegramChannel struct { type TelegramChannel struct {
*channels.BaseChannel *channels.BaseChannel
bot *telego.Bot bot *telego.Bot
bh *th.BotHandler bh *th.BotHandler
commands TelegramCommander config *config.Config
config *config.Config chatIDs map[string]int64
chatIDs map[string]int64 ctx context.Context
ctx context.Context cancel context.CancelFunc
cancel context.CancelFunc
registerFunc func(context.Context, []commands.Definition) error registerFunc func(context.Context, []commands.Definition) error
commandRegCancel context.CancelFunc commandRegCancel context.CancelFunc
@ -96,17 +95,12 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
return &TelegramChannel{ return &TelegramChannel{
BaseChannel: base, BaseChannel: base,
commands: NewTelegramCommands(bot, cfg, nil),
bot: bot, bot: bot,
config: cfg, config: cfg,
chatIDs: make(map[string]int64), chatIDs: make(map[string]int64),
}, nil }, nil
} }
func (c *TelegramChannel) SetRegistry(switcher AgentModelSwitcher) {
c.commands = NewTelegramCommands(c.bot, c.config, switcher)
}
func (c *TelegramChannel) Start(ctx context.Context) error { func (c *TelegramChannel) Start(ctx context.Context) error {
logger.InfoC("telegram", "Starting Telegram bot (polling mode)...") logger.InfoC("telegram", "Starting Telegram bot (polling mode)...")
@ -127,25 +121,6 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
} }
c.bh = bh c.bh = bh
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.commands.Start(ctx, message)
}, th.CommandEqual("start"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.commands.Help(ctx, message)
}, th.CommandEqual("help"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.commands.Show(ctx, message)
}, th.CommandEqual("show"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.commands.List(ctx, message)
}, th.CommandEqual("list"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.commands.Model(ctx, message)
}, th.CommandEqual("model"))
bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { bh.HandleMessage(func(ctx *th.Context, message telego.Message) error {
return c.handleMessage(ctx, &message) return c.handleMessage(ctx, &message)
}, th.AnyMessage()) }, th.AnyMessage())
@ -155,11 +130,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
"username": c.bot.Username(), "username": c.bot.Username(),
}) })
commandDefs := append(commands.BuiltinDefinitions(), commands.Definition{ c.startCommandRegistration(c.ctx, commands.BuiltinDefinitions())
Name: "model",
Description: "Show or switch the active model",
})
c.startCommandRegistration(c.ctx, commandDefs)
go func() { go func() {
if err = bh.Start(); err != nil { if err = bh.Start(); err != nil {

View file

@ -1,234 +0,0 @@
package telegram
import (
"context"
"fmt"
"strings"
"github.com/mymmrac/telego"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers"
)
// AgentModelSwitcher is the minimal interface needed to get/set the active model.
type AgentModelSwitcher interface {
GetDefaultAgentModel() string
SwitchDefaultAgentModel(modelName string) (string, string, error)
}
type TelegramCommander interface {
Help(ctx context.Context, message telego.Message) error
Start(ctx context.Context, message telego.Message) error
Show(ctx context.Context, message telego.Message) error
List(ctx context.Context, message telego.Message) error
Model(ctx context.Context, message telego.Message) error
}
type cmd struct {
bot *telego.Bot
config *config.Config
switcher AgentModelSwitcher
}
func NewTelegramCommands(bot *telego.Bot, cfg *config.Config, switcher AgentModelSwitcher) TelegramCommander {
return &cmd{
bot: bot,
config: cfg,
switcher: switcher,
}
}
func commandArgs(text string) string {
parts := strings.SplitN(text, " ", 2)
if len(parts) < 2 {
return ""
}
return strings.TrimSpace(parts[1])
}
func (c *cmd) Help(ctx context.Context, message telego.Message) error {
msg := `/start - Start the bot
/help - Show this help message
/show [model|channel] - Show current configuration
/list [models|channels] - List available options
/model - Show or switch the active model
/model - show current model
/model list - list available models
/model <name> - switch to named model
`
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: msg,
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
func (c *cmd) Start(ctx context.Context, message telego.Message) error {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: "Hello! I am PicoClaw 🦞",
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
func (c *cmd) Show(ctx context.Context, message telego.Message) error {
args := commandArgs(message.Text)
if args == "" {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: "Usage: /show [model|channel]",
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
var response string
switch args {
case "model":
if c.switcher == nil {
response = fmt.Sprintf("Current model: %s", c.config.Agents.Defaults.GetModelName())
} else {
response = fmt.Sprintf("Current model: %s", c.switcher.GetDefaultAgentModel())
}
case "channel":
response = "Current Channel: telegram"
default:
response = fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args)
}
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: response,
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
func (c *cmd) List(ctx context.Context, message telego.Message) error {
args := commandArgs(message.Text)
if args == "" {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: "Usage: /list [models|channels]",
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
var response string
switch args {
case "models":
provider := c.config.Agents.Defaults.Provider
if provider == "" {
provider = "configured default"
}
response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.json",
c.config.Agents.Defaults.GetModelName(), provider)
case "channels":
var enabled []string
if c.config.Channels.Telegram.Enabled {
enabled = append(enabled, "telegram")
}
if c.config.Channels.WhatsApp.Enabled {
enabled = append(enabled, "whatsapp")
}
if c.config.Channels.Feishu.Enabled {
enabled = append(enabled, "feishu")
}
if c.config.Channels.Discord.Enabled {
enabled = append(enabled, "discord")
}
if c.config.Channels.Slack.Enabled {
enabled = append(enabled, "slack")
}
response = fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- "))
default:
response = fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args)
}
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: response,
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}
func (c *cmd) Model(ctx context.Context, message telego.Message) error {
args := commandArgs(message.Text)
var response string
switch {
case args == "":
// Show current model
if c.switcher == nil {
response = "No agent configured."
} else {
response = fmt.Sprintf("Current model: %s", c.switcher.GetDefaultAgentModel())
}
case args == "list":
// List models from config
if len(c.config.ModelList) == 0 {
response = "No models configured in model_list."
} else {
currentModel := ""
if c.switcher != nil {
currentModel = c.switcher.GetDefaultAgentModel()
}
lines := make([]string, 0, len(c.config.ModelList))
for _, m := range c.config.ModelList {
_, modelID := providers.ExtractProtocol(m.Model)
line := "• " + m.ModelName
if modelID == currentModel {
line += " (active)"
}
line += " -> " + m.Model
lines = append(lines, line)
}
response = "Available models:\n" + strings.Join(lines, "\n")
}
default:
// Switch to the named model
modelName := args
if c.switcher == nil {
response = "No agent configured."
} else {
oldModel, newModel, err := c.switcher.SwitchDefaultAgentModel(modelName)
if err != nil {
response = fmt.Sprintf("Failed to switch model: %v", err)
} else if oldModel == newModel {
response = fmt.Sprintf("Model already active: %s", newModel)
} else {
response = fmt.Sprintf("Switched model: %s -> %s", oldModel, newModel)
}
}
}
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID},
Text: response,
ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID,
},
})
return err
}

View file

@ -27,7 +27,14 @@ func switchCommand() Definition {
if err != nil { if err != nil {
return req.Reply(err.Error()) return req.Reply(err.Error())
} }
return req.Reply(fmt.Sprintf("Switched model from %s to %s", oldModel, value)) newModel := value
if rt.GetModelInfo != nil {
currentModel, _ := rt.GetModelInfo()
if currentModel != "" {
newModel = currentModel
}
}
return req.Reply(fmt.Sprintf("Switched model from %s to %s", oldModel, newModel))
}, },
}, },
{ {

View file

@ -11,6 +11,9 @@ func TestSwitchModel_Success(t *testing.T) {
SwitchModel: func(value string) (string, error) { SwitchModel: func(value string) (string, error) {
return "old-model", nil return "old-model", nil
}, },
GetModelInfo: func() (string, string) {
return "openai/test-model", "openai"
},
} }
ex := NewExecutor(NewRegistry(BuiltinDefinitions()), rt) ex := NewExecutor(NewRegistry(BuiltinDefinitions()), rt)
@ -25,7 +28,7 @@ func TestSwitchModel_Success(t *testing.T) {
if res.Outcome != OutcomeHandled { if res.Outcome != OutcomeHandled {
t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled)
} }
want := "Switched model from old-model to gpt-4" want := "Switched model from old-model to openai/test-model"
if reply != want { if reply != want {
t.Fatalf("reply=%q, want=%q", reply, want) t.Fatalf("reply=%q, want=%q", reply, want)
} }
@ -239,6 +242,9 @@ func TestSwitch_BangPrefix(t *testing.T) {
SwitchModel: func(value string) (string, error) { SwitchModel: func(value string) (string, error) {
return "old", nil return "old", nil
}, },
GetModelInfo: func() (string, string) {
return "openai/test-model", "openai"
},
} }
ex := NewExecutor(NewRegistry(BuiltinDefinitions()), rt) ex := NewExecutor(NewRegistry(BuiltinDefinitions()), rt)
@ -253,7 +259,7 @@ func TestSwitch_BangPrefix(t *testing.T) {
if res.Outcome != OutcomeHandled { if res.Outcome != OutcomeHandled {
t.Fatalf("! prefix: outcome=%v, want=%v", res.Outcome, OutcomeHandled) t.Fatalf("! prefix: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
} }
if reply != "Switched model from old to gpt-4" { if reply != "Switched model from old to openai/test-model" {
t.Fatalf("! prefix: reply=%q, want success message", reply) t.Fatalf("! prefix: reply=%q, want success message", reply)
} }
} }