Files
1Panel/agent/app/service/agents_channels.go

1654 lines
52 KiB
Go

package service
import (
"encoding/json"
"fmt"
"os"
"path"
"sort"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
)
type openclawPluginPackage struct {
Version string `json:"version"`
}
func updateHermesChannelConfig(agent *model.Agent, install *model.AppInstall, write func(confDir string) error) error {
if err := write(path.Dir(agent.ConfigPath)); err != nil {
return err
}
_, err := compose.Restart(install.GetComposePath())
return err
}
func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesFeishuChannelConfig(path.Dir(agent.ConfigPath))
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return nil, err
}
result := extractFeishuConfig(conf)
installed, _ := checkPluginInstalled(install.GetPath(), "feishu")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppHermesAgent {
return updateHermesChannelConfig(agent, install, func(confDir string) error {
return writeHermesFeishuChannelConfig(confDir, dto.AgentFeishuConfig{
Enabled: req.Enabled,
ThreadSession: req.ThreadSession,
ReplyMode: req.ReplyMode,
Streaming: req.Streaming,
RequireMention: req.RequireMention,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
Domain: "feishu",
ConnectionMode: "websocket",
Bots: req.Bots,
})
})
}
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
config := dto.AgentFeishuConfig{
Enabled: req.Enabled,
ThreadSession: req.ThreadSession,
ReplyMode: req.ReplyMode,
Streaming: req.Streaming,
RequireMention: req.RequireMention,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
Domain: req.Domain,
ConnectionMode: req.ConnectionMode,
Bots: req.Bots,
}
setFeishuConfig(conf, config)
setFeishuPluginEnabled(conf, config.Enabled && hasEnabledBots(config.Bots))
return nil
})
}
func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) {
agent, _, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesTelegramChannelConfig(path.Dir(agent.ConfigPath))
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return nil, err
}
result := extractTelegramConfig(conf)
return &result, nil
}
func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppHermesAgent {
return updateHermesChannelConfig(agent, install, func(confDir string) error {
return writeHermesTelegramChannelConfig(confDir, dto.AgentTelegramConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
RequireMention: req.RequireMention,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
Proxy: req.Proxy,
Streaming: req.Streaming,
DefaultAccount: req.DefaultAccount,
Bots: req.Bots,
})
})
}
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setTelegramConfig(conf, dto.AgentTelegramConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
RequireMention: req.RequireMention,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
Proxy: req.Proxy,
Streaming: req.Streaming,
DefaultAccount: req.DefaultAccount,
Bots: req.Bots,
})
return nil
})
}
func (a AgentService) GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error) {
agent, _, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesDiscordChannelConfig(path.Dir(agent.ConfigPath))
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return nil, err
}
result := extractDiscordConfig(conf)
return &result, nil
}
func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppHermesAgent {
return updateHermesChannelConfig(agent, install, func(confDir string) error {
return writeHermesDiscordChannelConfig(confDir, dto.AgentDiscordConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
RequireMention: req.RequireMention,
GroupPolicy: req.GroupPolicy,
Proxy: req.Proxy,
DefaultAccount: req.DefaultAccount,
Bots: req.Bots,
})
})
}
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setDiscordConfig(conf, dto.AgentDiscordConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
RequireMention: req.RequireMention,
GroupPolicy: req.GroupPolicy,
Proxy: req.Proxy,
DefaultAccount: req.DefaultAccount,
Bots: req.Bots,
})
return nil
})
}
func (a AgentService) GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesQQBotChannelConfig(path.Dir(agent.ConfigPath))
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return nil, err
}
result := extractQQBotConfig(conf)
installed, _ := checkPluginInstalled(install.GetPath(), "qqbot")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppHermesAgent {
return updateHermesChannelConfig(agent, install, func(confDir string) error {
return writeHermesQQBotChannelConfig(confDir, dto.AgentQQBotConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
Bots: req.Bots,
})
})
}
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setQQBotConfig(conf, dto.AgentQQBotConfig{
Enabled: req.Enabled,
Bots: req.Bots,
})
return nil
})
}
func (a AgentService) GetWecomConfig(req dto.AgentIDReq) (*dto.AgentWecomConfig, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesWecomChannelConfig(path.Dir(agent.ConfigPath))
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return nil, err
}
result := extractWecomConfig(conf)
installed, _ := checkPluginInstalled(install.GetPath(), "wecom")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppHermesAgent {
return updateHermesChannelConfig(agent, install, func(confDir string) error {
return writeHermesWecomChannelConfig(confDir, dto.AgentWecomConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
BotID: req.BotID,
Secret: req.Secret,
})
})
}
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setWecomConfig(conf, dto.AgentWecomConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
BotID: req.BotID,
Secret: req.Secret,
})
return nil
})
}
func (a AgentService) GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesDingTalkChannelConfig(path.Dir(agent.ConfigPath))
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return nil, err
}
result := extractDingTalkConfig(conf)
installed, _ := checkPluginInstalled(install.GetPath(), "dingtalk")
result.Installed = installed
return &result, nil
}
func (a AgentService) GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error) {
agent, _, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppHermesAgent {
return readHermesWeixinChannelConfig(path.Dir(agent.ConfigPath))
}
return nil, fmt.Errorf("%s does not support", agent.AgentType)
}
func (a AgentService) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppHermesAgent {
return updateHermesChannelConfig(agent, install, func(confDir string) error {
return writeHermesDingTalkChannelConfig(confDir, dto.AgentDingTalkConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
Bots: req.Bots,
})
})
}
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
setDingTalkConfig(conf, dto.AgentDingTalkConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
AllowFrom: req.AllowFrom,
GroupPolicy: req.GroupPolicy,
GroupAllowFrom: req.GroupAllowFrom,
SeparateSessionByConversation: req.SeparateSessionByConversation,
GroupSessionScope: req.GroupSessionScope,
SharedMemoryAcrossConversations: req.SharedMemoryAcrossConversations,
AsyncMode: req.AsyncMode,
AckText: req.AckText,
Bots: req.Bots,
})
return nil
})
}
func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
spec, pluginID, err := resolvePluginMeta(req.Type)
if err != nil {
return err
}
installTask, err := task.NewTaskWithOps(req.Type, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask("Install OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Type == "qqbot" {
legacyPluginPath := path.Join(openclawPluginBaseDir, "qqbot")
if err := mgr.Run("docker", "exec", install.ContainerName, "test", "-d", legacyPluginPath); err == nil {
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, "qqbot"); err != nil {
return err
}
time.Sleep(2 * time.Second)
}
}
if err := installOpenclawPlugin(mgr, install.ContainerName, spec, pluginID); err != nil {
return err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
appendPluginAllow(conf, pluginID)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}, nil)
go func() {
if err := installTask.Execute(); err != nil {
global.LOG.Errorf("install openclaw plugin failed: %v", err)
}
}()
return nil
}
func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
spec, pluginID, err := resolvePluginMeta(req.Type)
if err != nil {
return err
}
upgradeTask, err := task.NewTaskWithOps(req.Type, task.TaskUpgrade, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
upgradeTask.AddSubTask("Upgrade OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, pluginID); err != nil {
return err
}
time.Sleep(2 * time.Second)
if err := installOpenclawPlugin(mgr, install.ContainerName, spec, pluginID); err != nil {
return err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
appendPluginAllow(conf, pluginID)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}, nil)
go func() {
if err := upgradeTask.Execute(); err != nil {
global.LOG.Errorf("upgrade openclaw plugin failed: %v", err)
}
}()
return nil
}
func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
_, pluginID, err := resolvePluginMeta(req.Type)
if err != nil {
return err
}
uninstallTask, err := task.NewTaskWithOps(req.Type, task.TaskUninstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
uninstallTask.AddSubTask("Uninstall OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, pluginID); err != nil {
return err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
cleanupOpenclawPluginConfig(conf, req.Type)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}, nil)
go func() {
if err := uninstallTask.Execute(); err != nil {
global.LOG.Errorf("uninstall openclaw plugin failed: %v", err)
}
}()
return nil
}
func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
loginTask, err := task.NewTaskWithOps("weixin", task.TaskExec, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
loginTask.AddSubTask("Login Weixin channel", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(30*time.Minute))
if agent.AgentType == constant.AppHermesAgent {
return mgr.Run("docker", buildHermesWeixinLoginArgs(install.ContainerName)...)
}
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "channels", "login", "--channel", "openclaw-weixin")
}, nil)
if agent.AgentType == constant.AppHermesAgent {
loginTask.AddSubTask("Restart Hermes-Agent container", func(t *task.Task) error {
output, err := compose.Restart(install.GetComposePath())
if output != "" {
t.Log(output)
}
return err
}, nil)
}
go func() {
if err := loginTask.Execute(); err != nil {
global.LOG.Errorf("login openclaw weixin channel failed: %v", err)
}
}()
return nil
}
func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType != constant.AppHermesAgent {
return fmt.Errorf("%s does not support", agent.AgentType)
}
return updateHermesChannelConfig(agent, install, func(confDir string) error {
switch req.Type {
case "telegram":
return deleteHermesTelegramChannelConfig(confDir)
case "discord":
return deleteHermesDiscordChannelConfig(confDir)
case "qqbot":
return deleteHermesQQBotChannelConfig(confDir)
case "wecom":
return deleteHermesWecomChannelConfig(confDir)
case "dingtalk":
return deleteHermesDingTalkChannelConfig(confDir)
case "feishu":
return deleteHermesFeishuChannelConfig(confDir)
case "weixin":
return deleteHermesWeixinChannelConfig(confDir)
default:
return fmt.Errorf("unsupported channel type: %s", req.Type)
}
})
}
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
installed, err := checkPluginInstalled(install.GetPath(), req.Type)
if err != nil {
return nil, err
}
status := &dto.AgentPluginStatus{Installed: installed}
if !installed {
return status, nil
}
currentVersion, err := loadOpenclawPluginCurrentVersion(install.GetPath(), req.Type)
if err != nil {
global.LOG.Errorf("load openclaw plugin current version failed: %v", err)
return status, nil
}
status.CurrentVersion = currentVersion
if !req.CheckLatest {
return status, nil
}
latestVersion, err := loadOpenclawPluginLatestVersion(install.ContainerName, req.Type)
if err != nil {
global.LOG.Errorf("load openclaw plugin latest version failed: %v", err)
return status, nil
}
status.LatestVersion = latestVersion
if currentVersion != "" && latestVersion != "" {
status.Upgradable = common.CompareVersion(latestVersion, currentVersion)
}
return status, nil
}
func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppHermesAgent {
mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
output, err := mgr.RunWithStdout(
"docker",
buildHermesDockerExecArgs(install.ContainerName, "pairing", "approve", req.Type, req.PairingCode)...,
)
return validateHermesPairingApproveResult(output, err)
}
if req.AccountID != "" {
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).Run(
"docker",
"exec",
install.ContainerName,
"openclaw",
"pairing",
"approve",
req.Type,
req.PairingCode,
"--account",
req.AccountID,
)
}
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).Run(
"docker",
"exec",
install.ContainerName,
"openclaw",
"pairing",
"approve",
req.Type,
req.PairingCode,
)
}
func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
result := dto.AgentFeishuConfig{
Enabled: true,
ThreadSession: true,
ReplyMode: "auto",
Streaming: false,
RequireMention: "true",
GroupPolicy: "open",
GroupAllowFrom: []string{},
Bots: []dto.AgentFeishuBot{},
}
feishu := getChannelConfig(conf, "feishu")
if len(feishu) == 0 {
return result
}
result.Enabled = extractFeishuPluginEnabled(conf, extractBoolValue(feishu["enabled"], result.Enabled))
if threadSession, ok := feishu["threadSession"].(bool); ok {
result.ThreadSession = threadSession
}
if replyMode := extractStringValue(feishu["replyMode"]); replyMode != "" {
result.ReplyMode = replyMode
}
if streaming, ok := feishu["streaming"].(bool); ok {
result.Streaming = streaming
}
result.RequireMention = extractRequireMentionValue(feishu["requireMention"], result.RequireMention)
if groupPolicy := extractStringValue(feishu["groupPolicy"]); groupPolicy != "" {
result.GroupPolicy = groupPolicy
}
result.GroupAllowFrom = extractStringList(feishu["groupAllowFrom"])
defaultBot := defaultFeishuBot()
defaultBot.Enabled = extractBoolValue(feishu["enabled"], defaultBot.Enabled)
defaultBot.Name = extractDisplayName(feishu, "", "default")
defaultBot.AppID = extractStringValue(feishu["appId"])
defaultBot.AppSecret = extractStringValue(feishu["appSecret"])
if dmPolicy := extractStringValue(feishu["dmPolicy"]); dmPolicy != "" {
defaultBot.DmPolicy = dmPolicy
}
if _, ok := feishu["allowFrom"]; ok {
defaultBot.AllowFrom = extractStringList(feishu["allowFrom"])
}
accounts := childMap(feishu, "accounts")
defaultAccount := childMap(accounts, "default")
if defaultBot.Name == "Default" {
defaultBot.Name = extractDisplayName(defaultAccount, extractStringValue(defaultAccount["botName"]), "Default")
}
if defaultBot.DmPolicy == "pairing" {
if dmPolicy := extractStringValue(defaultAccount["dmPolicy"]); dmPolicy != "" {
defaultBot.DmPolicy = dmPolicy
}
}
if len(defaultBot.AllowFrom) == 0 {
if _, ok := defaultAccount["allowFrom"]; ok {
defaultBot.AllowFrom = extractStringList(defaultAccount["allowFrom"])
}
}
baseEnabled := defaultBot.Enabled
baseDmPolicy := defaultBot.DmPolicy
baseAllowFrom := append([]string(nil), defaultBot.AllowFrom...)
bots := make([]dto.AgentFeishuBot, 0, len(accounts)+1)
if defaultBot.AppID != "" || defaultBot.AppSecret != "" {
bots = append(bots, defaultBot)
}
for _, accountID := range sortedChildKeys(accounts) {
if accountID == "default" {
continue
}
account := childMap(accounts, accountID)
bot := dto.AgentFeishuBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: accountID,
Name: extractDisplayName(account, extractStringValue(account["botName"]), accountID),
Enabled: extractBoolValue(account["enabled"], baseEnabled),
},
AppID: extractStringValue(account["appId"]),
AppSecret: extractStringValue(account["appSecret"]),
DmPolicy: baseDmPolicy,
AllowFrom: append([]string(nil), baseAllowFrom...),
}
if dmPolicy := extractStringValue(account["dmPolicy"]); dmPolicy != "" {
bot.DmPolicy = dmPolicy
}
if _, ok := account["allowFrom"]; ok {
bot.AllowFrom = extractStringList(account["allowFrom"])
}
bots = append(bots, bot)
}
result.Bots = bots
return result
}
func setFeishuConfig(conf map[string]interface{}, config dto.AgentFeishuConfig) {
channels := ensureChildMap(conf, "channels")
feishu := ensureChildMap(channels, "feishu")
defaultBot := getDefaultFeishuBot(config.Bots)
feishu["enabled"] = defaultBot.Enabled
feishu["threadSession"] = config.ThreadSession
feishu["replyMode"] = config.ReplyMode
feishu["streaming"] = config.Streaming
if config.RequireMention == "open" {
feishu["requireMention"] = "open"
} else {
feishu["requireMention"] = config.RequireMention == "true"
}
feishu["groupPolicy"] = config.GroupPolicy
if config.GroupPolicy == "allowlist" {
feishu["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...)
} else {
delete(feishu, "groupAllowFrom")
}
feishu["appId"] = defaultBot.AppID
feishu["appSecret"] = defaultBot.AppSecret
feishu["dmPolicy"] = defaultBot.DmPolicy
if defaultBot.Name != "" && defaultBot.Name != "Default" {
feishu["name"] = defaultBot.Name
} else {
delete(feishu, "name")
}
if defaultBot.DmPolicy == "open" {
feishu["allowFrom"] = []string{"*"}
} else if defaultBot.DmPolicy == "allowlist" {
feishu["allowFrom"] = append([]string(nil), defaultBot.AllowFrom...)
} else {
delete(feishu, "allowFrom")
}
delete(feishu, "botName")
delete(feishu, "connectionMode")
delete(feishu, "domain")
delete(feishu, "webhookPath")
delete(feishu, "reactionNotifications")
delete(feishu, "typingIndicator")
delete(feishu, "resolveSenderNames")
delete(feishu, "defaultAccount")
accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots {
if bot.AccountID == "default" || bot.IsDefault {
continue
}
account := map[string]interface{}{
"enabled": bot.Enabled,
"name": bot.Name,
"appId": bot.AppID,
"appSecret": bot.AppSecret,
}
if bot.DmPolicy != "" {
account["dmPolicy"] = bot.DmPolicy
}
if bot.DmPolicy == "open" {
account["allowFrom"] = []string{"*"}
} else if bot.DmPolicy == "allowlist" {
account["allowFrom"] = append([]string(nil), bot.AllowFrom...)
}
accounts[bot.AccountID] = account
}
if len(accounts) > 0 {
feishu["accounts"] = accounts
} else {
delete(feishu, "accounts")
}
}
func extractFeishuPluginEnabled(conf map[string]interface{}, defaultValue bool) bool {
plugins := childMap(conf, "plugins")
entries := childMap(plugins, "entries")
lark := childMap(entries, "openclaw-lark")
return extractBoolValue(lark["enabled"], defaultValue)
}
func setFeishuPluginEnabled(conf map[string]interface{}, enabled bool) {
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
lark := ensureChildMap(entries, "openclaw-lark")
lark["enabled"] = enabled
}
func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig {
result := dto.AgentTelegramConfig{
Enabled: true,
DmPolicy: "pairing",
AllowFrom: []string{},
RequireMention: false,
GroupPolicy: "open",
GroupAllowFrom: []string{},
Streaming: "partial",
}
telegram := getChannelConfig(conf, "telegram")
if len(telegram) == 0 {
return result
}
if enabled, ok := telegram["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy := extractStringValue(telegram["dmPolicy"]); dmPolicy != "" {
result.DmPolicy = dmPolicy
}
result.AllowFrom = extractStringList(telegram["allowFrom"])
if groupPolicy := extractStringValue(telegram["groupPolicy"]); groupPolicy != "" {
result.GroupPolicy = groupPolicy
}
result.RequireMention = result.GroupPolicy == "allowlist"
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
result.Proxy = extractStringValue(telegram["proxy"])
result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming)
accounts := childMap(telegram, "accounts")
if len(accounts) == 0 {
botToken := extractStringValue(telegram["botToken"])
if botToken != "" {
accounts["default"] = map[string]interface{}{
"enabled": extractBoolValue(telegram["enabled"], true),
"botToken": botToken,
"dmPolicy": result.DmPolicy,
"groupPolicy": result.GroupPolicy,
"streaming": result.Streaming,
}
}
}
bots := make([]dto.AgentTelegramBot, 0, len(accounts))
for _, accountID := range sortedChildKeys(accounts) {
account := childMap(accounts, accountID)
bots = append(bots, dto.AgentTelegramBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: accountID,
Name: extractDisplayName(account, accountID, accountID),
Enabled: extractBoolValue(account["enabled"], true),
},
BotToken: extractStringValue(account["botToken"]),
DmPolicy: extractStringValue(account["dmPolicy"]),
GroupPolicy: extractStringValue(account["groupPolicy"]),
Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming),
})
}
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
setTelegramDefaultFlags(bots, result.DefaultAccount)
result.Bots = bots
return result
}
func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConfig) {
channels := ensureChildMap(conf, "channels")
telegram := ensureChildMap(channels, "telegram")
defaultAccount := normalizeDefaultAccount(config.DefaultAccount, getTelegramBotAccountIDs(config.Bots))
effectiveEnabled := config.Enabled && hasEnabledBots(config.Bots)
telegram["enabled"] = effectiveEnabled
telegram["dmPolicy"] = config.DmPolicy
telegram["groupPolicy"] = config.GroupPolicy
telegram["defaultAccount"] = defaultAccount
if config.RequireMention {
telegram["groupPolicy"] = "allowlist"
} else if config.GroupPolicy == "allowlist" {
telegram["groupPolicy"] = "allowlist"
}
if config.DmPolicy == "open" {
telegram["allowFrom"] = []string{"*"}
} else if config.DmPolicy == "allowlist" {
telegram["allowFrom"] = append([]string(nil), config.AllowFrom...)
} else {
delete(telegram, "allowFrom")
}
if config.GroupPolicy == "allowlist" {
telegram["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...)
} else {
delete(telegram, "groupAllowFrom")
}
if config.Proxy != "" {
telegram["proxy"] = config.Proxy
} else {
delete(telegram, "proxy")
}
telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming)
accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots {
account := map[string]interface{}{
"enabled": bot.Enabled,
"name": bot.Name,
"botToken": bot.BotToken,
"dmPolicy": bot.DmPolicy,
"groupPolicy": bot.GroupPolicy,
"streaming": buildTelegramStreamingConfig(bot.Streaming),
}
if bot.DmPolicy == "open" {
account["allowFrom"] = []string{"*"}
}
accounts[bot.AccountID] = account
}
telegram["accounts"] = accounts
delete(telegram, "botToken")
}
func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string {
mode := defaultMode
switch typed := value.(type) {
case string:
mode = typed
case map[string]interface{}:
mode = extractStringValue(typed["mode"])
}
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "off", "partial", "block", "progress":
return mode
default:
return "partial"
}
}
func buildTelegramStreamingConfig(mode string) map[string]interface{} {
return map[string]interface{}{
"mode": normalizeTelegramStreamingMode(mode, "partial"),
}
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
discord := getChannelConfig(conf, "discord")
if len(discord) == 0 {
return result
}
if enabled, ok := discord["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy := extractStringValue(discord["dmPolicy"]); dmPolicy != "" {
result.DmPolicy = dmPolicy
} else if dm := childMap(discord, "dm"); dm != nil {
if policy := extractStringValue(dm["policy"]); policy != "" {
result.DmPolicy = policy
}
}
if groupPolicy := extractStringValue(discord["groupPolicy"]); groupPolicy != "" {
result.GroupPolicy = groupPolicy
}
result.RequireMention = result.GroupPolicy == "allowlist"
result.AllowFrom = extractStringList(discord["allowFrom"])
result.Proxy = extractStringValue(discord["proxy"])
accounts := childMap(discord, "accounts")
if len(accounts) == 0 {
token := extractStringValue(discord["token"])
if token != "" {
accounts["default"] = map[string]interface{}{
"enabled": extractBoolValue(discord["enabled"], true),
"token": token,
}
}
}
bots := make([]dto.AgentDiscordBot, 0, len(accounts))
for _, accountID := range sortedChildKeys(accounts) {
account := childMap(accounts, accountID)
bots = append(bots, dto.AgentDiscordBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: accountID,
Name: extractDisplayName(account, accountID, accountID),
Enabled: extractBoolValue(account["enabled"], true),
},
Token: extractStringValue(account["token"]),
})
}
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(discord["defaultAccount"]), getDiscordBotAccountIDs(bots))
setDiscordDefaultFlags(bots, result.DefaultAccount)
result.Bots = bots
return result
}
func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig) {
channels := ensureChildMap(conf, "channels")
discord := ensureChildMap(channels, "discord")
defaultAccount := normalizeDefaultAccount(config.DefaultAccount, getDiscordBotAccountIDs(config.Bots))
effectiveEnabled := config.Enabled && hasEnabledBots(config.Bots)
discord["enabled"] = effectiveEnabled
discord["dmPolicy"] = config.DmPolicy
discord["groupPolicy"] = config.GroupPolicy
discord["defaultAccount"] = defaultAccount
if config.RequireMention {
discord["groupPolicy"] = "allowlist"
} else if config.GroupPolicy == "allowlist" {
discord["groupPolicy"] = "allowlist"
}
if config.DmPolicy == "open" {
discord["allowFrom"] = []string{"*"}
} else if config.DmPolicy == "allowlist" {
discord["allowFrom"] = append([]string(nil), config.AllowFrom...)
} else {
delete(discord, "allowFrom")
}
if config.Proxy != "" {
discord["proxy"] = config.Proxy
} else {
delete(discord, "proxy")
}
accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots {
accounts[bot.AccountID] = map[string]interface{}{
"enabled": bot.Enabled,
"name": bot.Name,
"token": bot.Token,
"groupPolicy": config.GroupPolicy,
}
}
discord["accounts"] = accounts
delete(discord, "token")
delete(discord, "dm")
}
func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
result := dto.AgentQQBotConfig{Enabled: true}
qqbot := getChannelConfig(conf, "qqbot")
if len(qqbot) == 0 {
result.Bots = []dto.AgentQQBotBot{}
return result
}
if enabled, ok := qqbot["enabled"].(bool); ok {
result.Enabled = enabled
}
bots := make([]dto.AgentQQBotBot, 0, len(childMap(qqbot, "accounts"))+1)
defaultBot := dto.AgentQQBotBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: "default",
Name: extractStringValue(qqbot["name"]),
Enabled: extractBoolValue(qqbot["enabled"], true),
IsDefault: true,
},
AppID: extractStringValue(qqbot["appId"]),
ClientSecret: extractStringValue(qqbot["clientSecret"]),
AllowFrom: extractStringList(qqbot["allowFrom"]),
SystemPrompt: extractStringValue(qqbot["systemPrompt"]),
}
if defaultBot.Name == "" {
defaultBot.Name = "Default"
}
if defaultBot.AppID != "" || defaultBot.ClientSecret != "" {
bots = append(bots, defaultBot)
}
accounts := childMap(qqbot, "accounts")
for _, accountID := range sortedChildKeys(accounts) {
account := childMap(accounts, accountID)
bots = append(bots, dto.AgentQQBotBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: accountID,
Name: extractDisplayName(account, accountID, accountID),
Enabled: extractBoolValue(account["enabled"], true),
},
AppID: extractStringValue(account["appId"]),
ClientSecret: extractStringValue(account["clientSecret"]),
AllowFrom: extractStringList(account["allowFrom"]),
SystemPrompt: extractStringValue(account["systemPrompt"]),
})
}
result.Bots = bots
return result
}
func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig {
result := dto.AgentWecomConfig{
Enabled: true,
DmPolicy: "pairing",
AllowFrom: []string{},
GroupPolicy: "open",
GroupAllowFrom: []string{},
}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
wecom, ok := channels["wecom"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := wecom["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy := extractStringValue(wecom["dmPolicy"]); dmPolicy != "" {
result.DmPolicy = dmPolicy
}
result.AllowFrom = extractStringList(wecom["allowFrom"])
if groupPolicy := extractStringValue(wecom["groupPolicy"]); groupPolicy != "" {
result.GroupPolicy = groupPolicy
}
result.GroupAllowFrom = extractStringList(wecom["groupAllowFrom"])
result.BotID = extractStringValue(wecom["botId"])
result.Secret = extractStringValue(wecom["secret"])
return result
}
func extractDingTalkConfig(conf map[string]interface{}) dto.AgentDingTalkConfig {
result := dto.AgentDingTalkConfig{
Enabled: true,
DmPolicy: "open",
GroupPolicy: "disabled",
AllowFrom: []string{},
GroupAllowFrom: []string{},
SeparateSessionByConversation: true,
GroupSessionScope: "group",
SharedMemoryAcrossConversations: false,
AsyncMode: false,
AckText: "任务已接收,处理中...",
}
dingtalk := getChannelConfig(conf, "dingtalk-connector")
if len(dingtalk) == 0 {
return result
}
if enabled, ok := dingtalk["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy := extractStringValue(dingtalk["dmPolicy"]); dmPolicy != "" {
if dmPolicy == "pairing" {
result.DmPolicy = "open"
} else {
result.DmPolicy = dmPolicy
}
}
if groupPolicy := extractStringValue(dingtalk["groupPolicy"]); groupPolicy != "" {
result.GroupPolicy = groupPolicy
}
result.AllowFrom = extractStringList(dingtalk["allowFrom"])
result.GroupAllowFrom = extractStringList(dingtalk["groupAllowFrom"])
if separateSessionByConversation, ok := dingtalk["separateSessionByConversation"].(bool); ok {
result.SeparateSessionByConversation = separateSessionByConversation
}
if groupSessionScope := extractStringValue(dingtalk["groupSessionScope"]); groupSessionScope != "" {
result.GroupSessionScope = groupSessionScope
}
if sharedMemoryAcrossConversations, ok := dingtalk["sharedMemoryAcrossConversations"].(bool); ok {
result.SharedMemoryAcrossConversations = sharedMemoryAcrossConversations
}
if asyncMode, ok := dingtalk["asyncMode"].(bool); ok {
result.AsyncMode = asyncMode
}
if ackText := extractStringValue(dingtalk["ackText"]); ackText != "" {
result.AckText = ackText
}
accounts := childMap(dingtalk, "accounts")
if len(accounts) == 0 {
clientID := extractStringValue(dingtalk["clientId"])
clientSecret := extractStringValue(dingtalk["clientSecret"])
if clientID != "" || clientSecret != "" {
accounts["default"] = map[string]interface{}{
"enabled": extractBoolValue(dingtalk["enabled"], true),
"clientId": clientID,
"clientSecret": clientSecret,
}
}
}
bots := make([]dto.AgentDingTalkBot, 0, len(accounts))
for _, accountID := range sortedChildKeys(accounts) {
account := childMap(accounts, accountID)
bots = append(bots, dto.AgentDingTalkBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: accountID,
Name: accountID,
Enabled: extractBoolValue(account["enabled"], true),
},
ClientID: extractStringValue(account["clientId"]),
ClientSecret: extractStringValue(account["clientSecret"]),
})
}
result.Bots = bots
return result
}
func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) {
channels := ensureChildMap(conf, "channels")
wecom := ensureChildMap(channels, "wecom")
wecom["enabled"] = config.Enabled
wecom["botId"] = config.BotID
wecom["secret"] = config.Secret
wecom["dmPolicy"] = config.DmPolicy
wecom["groupPolicy"] = config.GroupPolicy
wecom["sendThinkingMessage"] = true
if config.DmPolicy == "allowlist" {
wecom["allowFrom"] = append([]string(nil), config.AllowFrom...)
} else {
delete(wecom, "allowFrom")
}
if config.GroupPolicy == "allowlist" {
wecom["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...)
} else {
delete(wecom, "groupAllowFrom")
}
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin")
wecomEntry["enabled"] = config.Enabled
}
func setDingTalkConfig(conf map[string]interface{}, config dto.AgentDingTalkConfig) {
channels := ensureChildMap(conf, "channels")
dingtalk := ensureChildMap(channels, "dingtalk-connector")
effectiveEnabled := config.Enabled && hasEnabledBots(config.Bots)
dingtalk["enabled"] = effectiveEnabled
dingtalk["dmPolicy"] = config.DmPolicy
dingtalk["groupPolicy"] = config.GroupPolicy
dingtalk["separateSessionByConversation"] = config.SeparateSessionByConversation
dingtalk["groupSessionScope"] = config.GroupSessionScope
dingtalk["sharedMemoryAcrossConversations"] = config.SharedMemoryAcrossConversations
dingtalk["asyncMode"] = config.AsyncMode
dingtalk["ackText"] = config.AckText
delete(dingtalk, "gatewayToken")
switch config.DmPolicy {
case "open":
dingtalk["allowFrom"] = []string{"*"}
case "allowlist":
dingtalk["allowFrom"] = append([]string(nil), config.AllowFrom...)
default:
delete(dingtalk, "allowFrom")
}
switch config.GroupPolicy {
case "open":
dingtalk["groupAllowFrom"] = []string{"*"}
case "allowlist":
dingtalk["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...)
default:
delete(dingtalk, "groupAllowFrom")
}
accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots {
accounts[bot.AccountID] = map[string]interface{}{
"enabled": bot.Enabled,
"clientId": bot.ClientID,
"clientSecret": bot.ClientSecret,
}
}
dingtalk["accounts"] = accounts
delete(dingtalk, "clientId")
delete(dingtalk, "clientSecret")
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
dingtalkEntry := ensureChildMap(entries, "dingtalk-connector")
dingtalkEntry["enabled"] = effectiveEnabled
gateway := ensureChildMap(conf, "gateway")
httpMap := ensureChildMap(gateway, "http")
endpoints := ensureChildMap(httpMap, "endpoints")
chatCompletions := ensureChildMap(endpoints, "chatCompletions")
chatCompletions["enabled"] = true
}
func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
channels := ensureChildMap(conf, "channels")
qqbot := ensureChildMap(channels, "qqbot")
defaultBot := getDefaultQQBot(config.Bots)
effectiveEnabled := config.Enabled && hasEnabledBots(config.Bots)
delete(qqbot, "dmPolicy")
qqbot["enabled"] = effectiveEnabled
qqbot["appId"] = defaultBot.AppID
qqbot["clientSecret"] = defaultBot.ClientSecret
qqbot["name"] = defaultBot.Name
if len(defaultBot.AllowFrom) > 0 {
qqbot["allowFrom"] = append([]string(nil), defaultBot.AllowFrom...)
} else {
delete(qqbot, "allowFrom")
}
if defaultBot.SystemPrompt != "" {
qqbot["systemPrompt"] = defaultBot.SystemPrompt
} else {
delete(qqbot, "systemPrompt")
}
accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots {
if bot.AccountID == "default" || bot.IsDefault {
continue
}
account := map[string]interface{}{
"enabled": bot.Enabled,
"name": bot.Name,
"appId": bot.AppID,
"clientSecret": bot.ClientSecret,
}
if len(bot.AllowFrom) > 0 {
account["allowFrom"] = append([]string(nil), bot.AllowFrom...)
}
if bot.SystemPrompt != "" {
account["systemPrompt"] = bot.SystemPrompt
}
accounts[bot.AccountID] = account
}
qqbot["accounts"] = accounts
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
delete(entries, "qqbot")
qqbotEntry := ensureChildMap(entries, "openclaw-qqbot")
qqbotEntry["enabled"] = effectiveEnabled
}
func appendPluginAllow(conf map[string]interface{}, pluginID string) {
plugins := ensureChildMap(conf, "plugins")
allow := make([]string, 0, 4)
seen := map[string]struct{}{}
switch values := plugins["allow"].(type) {
case []interface{}:
for _, value := range values {
text, ok := value.(string)
if !ok || text == "" {
continue
}
if _, ok := seen[text]; ok {
continue
}
seen[text] = struct{}{}
allow = append(allow, text)
}
}
if _, ok := seen[pluginID]; ok {
plugins["allow"] = allow
return
}
plugins["allow"] = append(allow, pluginID)
}
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
defer func() {
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
}()
if err := mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir); err != nil {
return err
}
if err := mgr.Run("docker", "exec", containerName, "mkdir", "-p", workdir); err != nil {
return err
}
if err := mgr.Run("docker", "exec", "-w", workdir, containerName, "npm", "pack", "--silent", spec); err != nil {
return err
}
pkgPath, err := cmd.RunDockerExecWithStdout(10*time.Minute, containerName, "find", workdir, "-maxdepth", "1", "-type", "f", "-name", "*.tgz", "-print", "-quit")
if err != nil {
return err
}
pkgPath = strings.TrimSpace(pkgPath)
if pkgPath == "" {
return fmt.Errorf("openclaw plugin package not found")
}
return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
}
func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
_, err := mgr.RunPipe(
cmd.PipeCommand{Name: "printf", Args: []string{"yes\n"}},
cmd.PipeCommand{Name: "docker", Args: []string{"exec", "-i", containerName, "openclaw", "plugins", "uninstall", pluginID}},
)
if err != nil && strings.Contains(err.Error(), "exit status 137") {
return nil
}
return err
}
func resolvePluginMeta(pluginType string) (string, string, error) {
switch pluginType {
case "qqbot":
return "@tencent-connect/openclaw-qqbot", "openclaw-qqbot", nil
case "feishu":
return "@larksuite/openclaw-lark", "openclaw-lark", nil
case "wecom":
return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil
case "dingtalk":
return "@dingtalk-real-ai/dingtalk-connector", "dingtalk-connector", nil
case "weixin":
return "@tencent-weixin/openclaw-weixin", "openclaw-weixin", nil
default:
return "", "", fmt.Errorf("unsupported plugin type")
}
}
func checkPluginInstalled(installPath, pluginType string) (bool, error) {
packagePath, err := resolveOpenclawPluginPackagePath(installPath, pluginType)
if err != nil {
return false, err
}
if _, err := os.Stat(packagePath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
func loadOpenclawPluginCurrentVersion(installPath, pluginType string) (string, error) {
packagePath, err := resolveOpenclawPluginPackagePath(installPath, pluginType)
if err != nil {
return "", err
}
content, err := os.ReadFile(packagePath)
if err != nil {
return "", err
}
var pkg openclawPluginPackage
if err := json.Unmarshal(content, &pkg); err != nil {
return "", err
}
return pkg.Version, nil
}
func loadOpenclawPluginLatestVersion(containerName, pluginType string) (string, error) {
spec, _, err := resolvePluginMeta(pluginType)
if err != nil {
return "", err
}
output, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "npm", "view", spec, "version", "--json")
if err != nil {
return "", err
}
var version string
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &version); err == nil {
return version, nil
}
return strings.Trim(strings.TrimSpace(output), `"`), nil
}
func resolveOpenclawPluginPackagePath(installPath, pluginType string) (string, error) {
_, pluginID, err := resolvePluginMeta(pluginType)
if err != nil {
return "", err
}
if installPath == "" {
return "", buserr.New("ErrRecordNotFound")
}
return path.Join(installPath, "data", "conf", "extensions", pluginID, "package.json"), nil
}
func cleanupOpenclawPluginConfig(conf map[string]interface{}, pluginType string) {
channels, _ := conf["channels"].(map[string]interface{})
plugins, _ := conf["plugins"].(map[string]interface{})
entries, _ := plugins["entries"].(map[string]interface{})
switch pluginType {
case "feishu":
delete(channels, "feishu")
delete(entries, "openclaw-lark")
delete(entries, "feishu")
case "qqbot":
delete(channels, "qqbot")
delete(entries, "openclaw-qqbot")
delete(entries, "qqbot")
case "wecom":
delete(channels, "wecom")
delete(entries, "wecom-openclaw-plugin")
case "dingtalk":
delete(channels, "dingtalk-connector")
delete(entries, "dingtalk-connector")
gateway := ensureChildMap(conf, "gateway")
httpMap := ensureChildMap(gateway, "http")
endpoints := ensureChildMap(httpMap, "endpoints")
chatCompletions := ensureChildMap(endpoints, "chatCompletions")
chatCompletions["enabled"] = false
case "weixin":
delete(channels, "weixin")
delete(entries, "openclaw-weixin")
}
}
func getChannelConfig(conf map[string]interface{}, channel string) map[string]interface{} {
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return nil
}
channelMap, _ := channels[channel].(map[string]interface{})
return channelMap
}
func childMap(parent map[string]interface{}, key string) map[string]interface{} {
if parent == nil {
return map[string]interface{}{}
}
if value, ok := parent[key].(map[string]interface{}); ok {
return value
}
return map[string]interface{}{}
}
func extractStringValue(value interface{}) string {
text, _ := value.(string)
return text
}
func extractRequireMentionValue(value interface{}, defaultValue string) string {
switch typed := value.(type) {
case bool:
if typed {
return "true"
}
return "false"
case string:
if typed != "" {
return typed
}
}
return defaultValue
}
func extractBoolValue(value interface{}, defaultValue bool) bool {
result, ok := value.(bool)
if !ok {
return defaultValue
}
return result
}
func sortedChildKeys(values map[string]interface{}) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func extractDisplayName(values map[string]interface{}, fallback string, accountID string) string {
name := extractStringValue(values["name"])
if name != "" {
return name
}
if fallback != "" {
return fallback
}
if accountID == "default" {
return "Default"
}
return accountID
}
func normalizeDefaultAccount(defaultAccount string, accountIDs []string) string {
if len(accountIDs) == 0 {
return ""
}
for _, accountID := range accountIDs {
if accountID == defaultAccount {
return defaultAccount
}
}
return accountIDs[0]
}
func getTelegramBotAccountIDs(bots []dto.AgentTelegramBot) []string {
accountIDs := make([]string, 0, len(bots))
for _, bot := range bots {
accountIDs = append(accountIDs, bot.AccountID)
}
return accountIDs
}
func getDiscordBotAccountIDs(bots []dto.AgentDiscordBot) []string {
accountIDs := make([]string, 0, len(bots))
for _, bot := range bots {
accountIDs = append(accountIDs, bot.AccountID)
}
return accountIDs
}
func setTelegramDefaultFlags(bots []dto.AgentTelegramBot, defaultAccount string) {
for i := range bots {
bots[i].IsDefault = bots[i].AccountID == defaultAccount
}
}
func setDiscordDefaultFlags(bots []dto.AgentDiscordBot, defaultAccount string) {
for i := range bots {
bots[i].IsDefault = bots[i].AccountID == defaultAccount
}
}
type enabledBot interface {
IsEnabled() bool
}
func hasEnabledBots[T enabledBot](bots []T) bool {
for _, bot := range bots {
if bot.IsEnabled() {
return true
}
}
return false
}
func defaultQQBot() dto.AgentQQBotBot {
return dto.AgentQQBotBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: "default",
Name: "Default",
Enabled: true,
IsDefault: true,
},
}
}
func defaultFeishuBot() dto.AgentFeishuBot {
return dto.AgentFeishuBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: "default",
Name: "Default",
Enabled: true,
IsDefault: true,
},
DmPolicy: "pairing",
AllowFrom: []string{},
}
}
func getDefaultFeishuBot(bots []dto.AgentFeishuBot) dto.AgentFeishuBot {
for _, bot := range bots {
if bot.IsDefault || bot.AccountID == "default" {
bot.IsDefault = true
if bot.Name == "" {
bot.Name = "Default"
}
return bot
}
}
if len(bots) > 0 {
bot := bots[0]
bot.IsDefault = true
if bot.AccountID == "" {
bot.AccountID = "default"
}
if bot.Name == "" {
bot.Name = "Default"
}
return bot
}
return defaultFeishuBot()
}
func getDefaultQQBot(bots []dto.AgentQQBotBot) dto.AgentQQBotBot {
for _, bot := range bots {
if bot.IsDefault || bot.AccountID == "default" {
bot.IsDefault = true
if bot.Name == "" {
bot.Name = "Default"
}
return bot
}
}
if len(bots) > 0 {
bot := bots[0]
bot.IsDefault = true
if bot.Name == "" {
bot.Name = "Default"
}
return bot
}
return defaultQQBot()
}