From 1798f9768edc65cdaa896af6cc6aa9b1a35774e8 Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:24:03 +0800 Subject: [PATCH] fix(ai): preserve legacy Feishu and safely migrate OpenClaw 2026.10.7 upgrades (#14214) * fix(ai): preserve legacy Feishu and migrate bundled plugin upgrades * fix(ai): override inherited Feishu allowlists when cleared --- agent/app/api/v2/agents.go | 20 + agent/app/dto/agents.go | 65 +- agent/app/dto/app.go | 8 +- agent/app/dto/request/app.go | 42 +- agent/app/service/agents.go | 5 + agent/app/service/agents_channels.go | 41 +- agent/app/service/agents_feishu.go | 677 ++++++++++++++++++ agent/app/service/agents_feishu_runtime.go | 201 ++++++ agent/app/service/agents_plugins.go | 6 + agent/app/service/agents_utils.go | 4 + agent/app/service/app_install.go | 36 +- agent/app/service/app_upgrade.go | 78 +- agent/app/service/app_upgrade_feishu.go | 336 +++++++++ agent/app/service/backup_app.go | 20 +- agent/i18n/lang/en.yaml | 23 + agent/i18n/lang/es-ES.yaml | 23 + agent/i18n/lang/fa.yaml | 23 + agent/i18n/lang/ja.yaml | 23 + agent/i18n/lang/ko.yaml | 23 + agent/i18n/lang/lo.yaml | 23 + agent/i18n/lang/ms.yaml | 23 + agent/i18n/lang/pt-BR.yaml | 23 + agent/i18n/lang/ru.yaml | 23 + agent/i18n/lang/tr.yaml | 23 + agent/i18n/lang/zh-Hant.yaml | 23 + agent/i18n/lang/zh.yaml | 23 + agent/router/ro_ai.go | 1 + core/cmd/server/docs/docs.go | 137 ++++ core/cmd/server/docs/swagger.json | 137 ++++ frontend/src/api/interface/ai.ts | 21 + frontend/src/api/interface/app.ts | 4 + frontend/src/api/modules/ai.ts | 4 + frontend/src/lang/modules/en.ts | 19 + frontend/src/lang/modules/es-es.ts | 19 + frontend/src/lang/modules/fa.ts | 19 + frontend/src/lang/modules/ja.ts | 19 + frontend/src/lang/modules/ko.ts | 19 + frontend/src/lang/modules/lo.ts | 18 + frontend/src/lang/modules/ms.ts | 19 + frontend/src/lang/modules/pt-br.ts | 19 + frontend/src/lang/modules/ru.ts | 19 + frontend/src/lang/modules/tr.ts | 19 + frontend/src/lang/modules/zh-Hant.ts | 17 + frontend/src/lang/modules/zh.ts | 17 + .../channels/components/feishu-transition.vue | 31 + .../config/tabs/channels/openclaw/feishu.vue | 193 +++-- .../app-store/installed/upgrade/index.vue | 66 +- 47 files changed, 2513 insertions(+), 119 deletions(-) create mode 100644 agent/app/service/agents_feishu.go create mode 100644 agent/app/service/agents_feishu_runtime.go create mode 100644 agent/app/service/app_upgrade_feishu.go create mode 100644 frontend/src/views/ai/agents/agent/config/tabs/channels/components/feishu-transition.vue diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index 44d69b200..004b20287 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -826,6 +826,26 @@ func (b *BaseApi) UpdateAgentFeishuConfig(c *gin.Context) { helper.Success(c) } +// @Tags AI +// @Summary Switch to the bundled OpenClaw Feishu plugin +// @Accept json +// @Param request body dto.AgentFeishuSwitchReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/channel/feishu/switch [post] +func (b *BaseApi) SwitchAgentFeishuPlugin(c *gin.Context) { + var req dto.AgentFeishuSwitchReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.SwitchFeishuPlugin(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + // @Tags AI // @Summary Get Agent Telegram channel config // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 8cda5d933..28fda50b1 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -433,32 +433,53 @@ type AgentFeishuConfigReq struct { AgentID uint `json:"agentId" validate:"required"` } +type AgentFeishuTransition struct { + Confirmed bool `json:"confirmed"` + Streaming bool `json:"streaming"` + GroupSessionScope string `json:"groupSessionScope"` + RequireMention string `json:"requireMention"` +} + +type AgentFeishuSwitchReq struct { + AgentID uint `json:"agentId" validate:"required"` + TaskID string `json:"taskID" validate:"required"` + Streaming bool `json:"streaming"` + GroupSessionScope string `json:"groupSessionScope" validate:"required,oneof=group group_sender group_topic group_topic_sender"` + RequireMention string `json:"requireMention" validate:"required,oneof=true false"` +} + type AgentFeishuConfigUpdateReq struct { - AgentID uint `json:"agentId" validate:"required"` - Enabled bool `json:"enabled"` - ThreadSession bool `json:"threadSession"` - ReplyMode string `json:"replyMode" validate:"required"` - Streaming bool `json:"streaming"` - RequireMention string `json:"requireMention" validate:"required,oneof=true false open"` - GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"` - GroupAllowFrom []string `json:"groupAllowFrom"` - Domain string `json:"domain"` - ConnectionMode string `json:"connectionMode"` - Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"` + GroupSessionScope string `json:"groupSessionScope" validate:"omitempty,oneof=group group_sender group_topic group_topic_sender"` + AgentID uint `json:"agentId" validate:"required"` + Enabled bool `json:"enabled"` + ThreadSession bool `json:"threadSession"` + ReplyMode string `json:"replyMode" validate:"required"` + Streaming bool `json:"streaming"` + RequireMention string `json:"requireMention" validate:"required,oneof=true false open"` + GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"` + GroupAllowFrom []string `json:"groupAllowFrom"` + Domain string `json:"domain"` + ConnectionMode string `json:"connectionMode"` + Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"` } type AgentFeishuConfig struct { - Enabled bool `json:"enabled"` - ThreadSession bool `json:"threadSession"` - ReplyMode string `json:"replyMode"` - Streaming bool `json:"streaming"` - RequireMention string `json:"requireMention"` - GroupPolicy string `json:"groupPolicy"` - GroupAllowFrom []string `json:"groupAllowFrom"` - Domain string `json:"domain"` - ConnectionMode string `json:"connectionMode"` - Bots []AgentFeishuBot `json:"bots"` - Installed bool `json:"installed"` + BuiltinManaged bool `json:"builtinManaged"` + LegacyConfigured bool `json:"legacyConfigured"` + GroupSessionScope string `json:"groupSessionScope"` + BuiltinAvailable bool `json:"builtinAvailable"` + Legacy bool `json:"legacy"` + Enabled bool `json:"enabled"` + ThreadSession bool `json:"threadSession"` + ReplyMode string `json:"replyMode"` + Streaming bool `json:"streaming"` + RequireMention string `json:"requireMention"` + GroupPolicy string `json:"groupPolicy"` + GroupAllowFrom []string `json:"groupAllowFrom"` + Domain string `json:"domain"` + ConnectionMode string `json:"connectionMode"` + Bots []AgentFeishuBot `json:"bots"` + Installed bool `json:"installed"` } type AgentTelegramConfigReq struct { diff --git a/agent/app/dto/app.go b/agent/app/dto/app.go index 72e342427..364019c23 100644 --- a/agent/app/dto/app.go +++ b/agent/app/dto/app.go @@ -43,9 +43,11 @@ type AppOssConfig struct { } type AppVersion struct { - Version string `json:"version"` - DetailId uint `json:"detailId"` - DockerCompose string `json:"dockerCompose"` + FeishuLegacy bool `json:"feishuLegacy,omitempty"` + FeishuConfigured bool `json:"feishuConfigured,omitempty"` + Version string `json:"version"` + DetailId uint `json:"detailId"` + DockerCompose string `json:"dockerCompose"` } type AppList struct { diff --git a/agent/app/dto/request/app.go b/agent/app/dto/request/app.go index 4435a3999..9f4204546 100644 --- a/agent/app/dto/request/app.go +++ b/agent/app/dto/request/app.go @@ -89,31 +89,33 @@ type AppBackupDelete struct { } type AppInstalledOperate struct { - InstallId uint `json:"installId" validate:"required"` - BackupId uint `json:"backupId"` - DetailId uint `json:"detailId"` - Operate constant.AppOperate `json:"operate" validate:"required"` - ForceDelete bool `json:"forceDelete"` - DeleteBackup bool `json:"deleteBackup"` - DeleteDB bool `json:"deleteDB"` - Backup bool `json:"backup"` - PullImage bool `json:"pullImage"` - DockerCompose string `json:"dockerCompose"` - TaskID string `json:"taskID"` - DeleteImage bool `json:"deleteImage"` - Favorite bool `json:"favorite"` + FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"` + InstallId uint `json:"installId" validate:"required"` + BackupId uint `json:"backupId"` + DetailId uint `json:"detailId"` + Operate constant.AppOperate `json:"operate" validate:"required"` + ForceDelete bool `json:"forceDelete"` + DeleteBackup bool `json:"deleteBackup"` + DeleteDB bool `json:"deleteDB"` + Backup bool `json:"backup"` + PullImage bool `json:"pullImage"` + DockerCompose string `json:"dockerCompose"` + TaskID string `json:"taskID"` + DeleteImage bool `json:"deleteImage"` + Favorite bool `json:"favorite"` UseLifecycleScripts bool `json:"-"` } type AppInstallUpgrade struct { - InstallID uint `json:"installId"` - DetailID uint `json:"detailId"` - Backup bool `json:"backup"` - PullImage bool `json:"pullImage"` - DeleteImage bool `json:"deleteImage"` - DockerCompose string `json:"dockerCompose"` - TaskID string `json:"taskID"` + FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"` + InstallID uint `json:"installId"` + DetailID uint `json:"detailId"` + Backup bool `json:"backup"` + PullImage bool `json:"pullImage"` + DeleteImage bool `json:"deleteImage"` + DockerCompose string `json:"dockerCompose"` + TaskID string `json:"taskID"` } type AppInstallDelete struct { diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index bf90f4d24..727b44b49 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -88,6 +88,7 @@ type IAgentService interface { GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error + SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error) @@ -1591,6 +1592,10 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error { if err != nil { return err } + if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy { + return buserr.New("TaskIsExecuting") + } + defer feishuSwitches.Delete(agent.ConfigPath) if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil { return err } diff --git a/agent/app/service/agents_channels.go b/agent/app/service/agents_channels.go index f898a328a..d4c120c80 100644 --- a/agent/app/service/agents_channels.go +++ b/agent/app/service/agents_channels.go @@ -45,9 +45,29 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF if err != nil { return nil, err } + if agent.AgentType != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(install.Version) { + result := extractFeishuConfig(conf) + result.Installed, _ = checkPluginInstalled(install.GetPath(), "feishu") + return &result, nil + } + legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath()) + if err != nil { + return nil, err + } result := extractFeishuConfig(conf) - installed, _ := checkPluginInstalled(install.GetPath(), "feishu") - result.Installed = installed + if !legacy { + result = extractBundledFeishuConfig(conf) + } + result.BuiltinManaged = true + result.BuiltinAvailable = hasBundledFeishu(install.ContainerName) + result.Installed = result.BuiltinAvailable + result.Legacy, result.LegacyConfigured = legacy, configured + if legacy { + result.GroupSessionScope = "group" + if result.ThreadSession { + result.GroupSessionScope = "group_topic" + } + } return &result, nil } @@ -72,6 +92,9 @@ func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) err }) }) } + if agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) { + return saveBundledFeishuConfig(agent, install, req) + } return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { config := dto.AgentFeishuConfig{ Enabled: req.Enabled, @@ -358,6 +381,9 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error { if err != nil { return err } + if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) { + return buserr.New("ErrFeishuManaged") + } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil { return err } @@ -403,6 +429,9 @@ func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error { if err != nil { return err } + if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) { + return buserr.New("ErrFeishuManaged") + } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil { return err } @@ -443,6 +472,9 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error { if err != nil { return err } + if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) { + return buserr.New("ErrFeishuManaged") + } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil { return err } @@ -538,10 +570,13 @@ func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error { } func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) { - _, install, err := a.loadAgentAndInstall(req.AgentID) + agent, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { return nil, err } + if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) { + return &dto.AgentPluginStatus{Installed: hasBundledFeishu(install.ContainerName)}, nil + } installed, err := checkPluginInstalled(install.GetPath(), req.Type) if err != nil { return nil, err diff --git a/agent/app/service/agents_feishu.go b/agent/app/service/agents_feishu.go new file mode 100644 index 000000000..9e7af6426 --- /dev/null +++ b/agent/app/service/agents_feishu.go @@ -0,0 +1,677 @@ +package service + +import ( + "bytes" + "encoding/json" + "errors" + "os" + "path/filepath" + "slices" + "strings" + "sync" + "syscall" + "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/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/compose" +) + +// Keep panel configuration writes out of the backup/activation/recovery window. +var feishuSwitches sync.Map + +func isOpenclawBundledFeishuVersion(version string) bool { + return common.CompareAppVersion(version, "2026.10.7") +} + +func hasBundledFeishu(container string) bool { + image, err := feishuContainerImage(container) + if err != nil { + return false + } + _, err = bundledFeishuImage(image) + return err == nil +} + +func usesBundledFeishu(conf map[string]interface{}) bool { + f := getChannelConfig(conf, "feishu") + for _, key := range []string{"threadSession", "replyMode"} { + if _, old := f[key]; old { + return false + } + } + if _, old := f["streaming"].(bool); old { + return false + } + entries := childMap(childMap(conf, "plugins"), "entries") + if extractBoolValue(childMap(entries, "openclaw-lark")["enabled"], false) { + return false + } + _, streaming := f["streaming"].(map[string]interface{}) + _, selected := entries["feishu"] + return selected && (extractBoolValue(childMap(entries, "feishu")["enabled"], false) || f["groupSessionScope"] != nil || streaming) +} + +func hasLegacyFeishu(conf map[string]interface{}, installPath string) bool { + legacy, _, err := classifyLegacyFeishu(conf, installPath) + return err != nil || legacy +} + +func selectBundledFeishu(conf map[string]interface{}, enabled bool) { + plugins := ensureChildMap(conf, "plugins") + entries := ensureChildMap(plugins, "entries") + if legacy, ok := entries["openclaw-lark"]; ok { + if entry, ok := legacy.(map[string]interface{}); ok { + entry["enabled"] = false + } + } + ensureChildMap(entries, "feishu")["enabled"] = enabled + // Preserve an absent allowlist; creating one would disable unrelated plugins. + if allow, ok := plugins["allow"]; ok && len(extractStringList(allow)) > 0 { + ids := slices.DeleteFunc(extractStringList(allow), func(id string) bool { return id == "openclaw-lark" }) + if !slices.Contains(ids, "feishu") { + ids = append(ids, "feishu") + } + plugins["allow"] = ids + } + if deny, ok := plugins["deny"]; ok { + plugins["deny"] = slices.DeleteFunc(extractStringList(deny), func(id string) bool { return id == "feishu" }) + } +} + +func feishuConfigError(field string) error { return buserr.WithDetail("ErrFeishuConfig", field, nil) } + +func convertLegacyFeishu(conf map[string]interface{}, req dto.AgentFeishuSwitchReq) error { + f := getChannelConfig(conf, "feishu") + if len(f) == 0 { + if raw, exists := childMap(conf, "channels")["feishu"]; exists && raw != nil { + if _, ok := raw.(map[string]interface{}); !ok { + return feishuConfigError("channels.feishu") + } + } + f = ensureChildMap(ensureChildMap(conf, "channels"), "feishu") + f["enabled"], f["dmPolicy"], f["groupPolicy"] = false, "pairing", "allowlist" + } + if err := checkLegacyFeishuFields(f, "channels.feishu", false); err != nil { + return err + } + if err := convertFeishuAccess(f, "channels.feishu"); err != nil { + return err + } + delete(f, "threadSession") + delete(f, "replyMode") + f["streaming"] = map[string]interface{}{"mode": "off"} + if req.Streaming { + f["streaming"] = map[string]interface{}{"mode": "partial"} + } + f["groupSessionScope"] = req.GroupSessionScope + f["requireMention"] = req.RequireMention == "true" + delete(f, "topicSessionMode") + // Preserve legacy defaults explicitly; bundled defaults are more restrictive. + if _, ok := f["groupPolicy"]; !ok { + f["groupPolicy"] = "open" + } + entries := ensureChildMap(ensureChildMap(conf, "plugins"), "entries") + ensureChildMap(entries, "openclaw-lark")["enabled"] = false + selectBundledFeishu(conf, extractBoolValue(f["enabled"], true)) + return nil +} + +// Limit automatic conversion to the shared channel fields; custom plugin options require review. +func checkLegacyFeishuFields(f map[string]interface{}, field string, group bool) error { + allowed := strings.Fields("enabled name appId appSecret encryptKey verificationToken domain connectionMode webhookPath defaultAccount dmPolicy allowFrom groupPolicy groupAllowFrom groupSenderAllowFrom requireMention accounts groups streaming threadSession replyMode groupSessionScope topicSessionMode replyInThread") + if group { + allowed = strings.Fields("enabled requireMention allowFrom groupSessionScope topicSessionMode replyInThread") + } + for _, key := range sortedChildKeys(f) { + if !slices.Contains(allowed, key) { + return feishuConfigError(field + "." + key) + } + } + if enabled, exists := f["enabled"]; exists { + if _, ok := enabled.(bool); !ok { + return feishuConfigError(field + ".enabled") + } + } + for _, key := range []string{"dmPolicy", "groupPolicy"} { + if raw, exists := f[key]; exists { + value, ok := raw.(string) + options := []string{"open", "allowlist", "disabled"} + if key == "dmPolicy" { + options = append(options, "pairing") + } + if !ok || !slices.Contains(options, value) { + return feishuConfigError(field + "." + key) + } + } + } + for _, key := range []string{"allowFrom", "groupSenderAllowFrom"} { + if raw, exists := f[key]; exists { + values, ok := raw.([]interface{}) + if !ok { + return feishuConfigError(field + "." + key) + } + for _, value := range values { + if _, ok := value.(string); !ok { + return feishuConfigError(field + "." + key) + } + } + } + } + for _, section := range []string{"accounts", "groups"} { + raw, exists := f[section] + if !exists { + continue + } + entries, ok := raw.(map[string]interface{}) + if !ok { + return feishuConfigError(field + "." + section) + } + for _, id := range sortedChildKeys(entries) { + entry, ok := entries[id].(map[string]interface{}) + name := field + "." + section + "." + id + if !ok { + return feishuConfigError(name) + } + if err := checkLegacyFeishuFields(entry, name, section == "groups"); err != nil { + return err + } + } + } + return nil +} + +func convertFeishuAccess(f map[string]interface{}, field string) error { + if raw, ok := f["groupAllowFrom"]; ok { + values, ok := raw.([]interface{}) + if !ok { + return feishuConfigError(field + ".groupAllowFrom") + } + groups, senders := []string{}, []string{} + for _, value := range values { + id, ok := value.(string) + if !ok { + return feishuConfigError(field + ".groupAllowFrom") + } + switch { + case strings.HasPrefix(id, "oc_"): + groups = append(groups, id) + case strings.HasPrefix(id, "ou_"): + senders = append(senders, id) + default: + return feishuConfigError(field + ".groupAllowFrom") + } + } + // A sender-only legacy allowlist has different group admission semantics. + if len(senders) > 0 && (len(groups) == 0 || f["groupSenderAllowFrom"] != nil) { + return feishuConfigError(field + ".groupAllowFrom") + } + f["groupAllowFrom"] = groups + if len(senders) > 0 { + f["groupSenderAllowFrom"] = senders + } + } + for _, section := range []string{"accounts", "groups"} { + for id, value := range childMap(f, section) { + entry, ok := value.(map[string]interface{}) + name := field + "." + section + "." + id + if !ok { + return feishuConfigError(name) + } + // Account/group overrides must be resolved explicitly, not flattened. + for _, key := range []string{"threadSession", "replyMode"} { + if _, ok := entry[key]; ok { + return feishuConfigError(name + "." + key) + } + } + if mention, ok := entry["requireMention"]; ok { + if _, ok := mention.(bool); !ok { + return feishuConfigError(name + ".requireMention") + } + } + if raw, exists := entry["streaming"]; exists { + streaming, ok := raw.(bool) + if !ok || streaming { + return feishuConfigError(name + ".streaming") + } + entry["streaming"] = map[string]interface{}{"mode": "off"} + } + if err := convertFeishuAccess(entry, name); err != nil { + return err + } + } + } + return nil +} + +func extractBundledFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig { + result := extractFeishuConfig(conf) + f := getChannelConfig(conf, "feishu") + result.Enabled = extractBoolValue(f["enabled"], true) && extractBoolValue(childMap(childMap(childMap(conf, "plugins"), "entries"), "feishu")["enabled"], true) + result.GroupSessionScope = extractStringValue(f["groupSessionScope"]) + if result.GroupSessionScope == "" { + result.GroupSessionScope = "group" + } + result.Streaming = extractStringValue(childMap(f, "streaming")["mode"]) != "off" + result.GroupPolicy = extractStringValue(f["groupPolicy"]) + if result.GroupPolicy == "" { + result.GroupPolicy = "allowlist" + } + if len(f) == 0 { + result.Streaming = false + result.RequireMention = "true" + } else if _, ok := f["requireMention"]; !ok { + result.RequireMention = "open" + } + accounts := childMap(f, "accounts") + ids := sortedChildKeys(accounts) + if (f["appId"] != nil || f["appSecret"] != nil) && !slices.Contains(ids, "default") { + ids = append([]string{"default"}, ids...) + } + result.Bots = []dto.AgentFeishuBot{} + for _, id := range ids { + effective := make(map[string]interface{}, len(f)) + for k, v := range f { + effective[k] = v + } + for k, v := range childMap(accounts, id) { + effective[k] = v + } + bot := defaultFeishuBot() + bot.AccountID, bot.IsDefault = id, id == "default" + bot.Name = extractDisplayName(childMap(accounts, id), "", id) + bot.Enabled = extractBoolValue(childMap(accounts, id)["enabled"], true) + bot.AppID, bot.AppSecret = extractStringValue(effective["appId"]), extractStringValue(effective["appSecret"]) + bot.DmPolicy = extractStringValue(effective["dmPolicy"]) + if bot.DmPolicy == "" { + bot.DmPolicy = "pairing" + } + bot.AllowFrom = extractStringList(effective["allowFrom"]) + result.Bots = append(result.Bots, bot) + } + return result +} + +func setBundledFeishuConfig(conf map[string]interface{}, req dto.AgentFeishuConfigUpdateReq) error { + if !slices.Contains([]string{"group", "group_sender", "group_topic", "group_topic_sender"}, req.GroupSessionScope) { + return feishuConfigError("groupSessionScope") + } + f := ensureChildMap(ensureChildMap(conf, "channels"), "feishu") + accounts := childMap(f, "accounts") + seen := map[string]bool{} + appIDs := map[string]bool{} + for _, bot := range req.Bots { + if bot.AccountID == "" || bot.AccountID != strings.ToLower(strings.TrimSpace(bot.AccountID)) || seen[bot.AccountID] || appIDs[bot.AppID] || bot.AppID == "" || !slices.Contains([]string{"pairing", "open", "allowlist", "disabled"}, bot.DmPolicy) || (bot.DmPolicy == "allowlist" && len(bot.AllowFrom) == 0) { + return feishuConfigError("accounts") + } + seen[bot.AccountID] = true + appIDs[bot.AppID] = true + entry := childMap(accounts, bot.AccountID) + if _, exists := entry["appSecret"]; !exists && bot.AppSecret == "" { + entry["appSecret"] = f["appSecret"] + } + // SecretRef credentials cannot be represented by the form: keep them intact. + if _, secretRef := entry["appSecret"].(map[string]interface{}); !secretRef && bot.AppSecret == "" { + return feishuConfigError("accounts." + bot.AccountID + ".appSecret") + } + entry["appId"], entry["name"], entry["enabled"], entry["dmPolicy"] = bot.AppID, bot.Name, bot.Enabled, bot.DmPolicy + if bot.AppSecret != "" { + entry["appSecret"] = bot.AppSecret + } + switch bot.DmPolicy { + case "open": + entry["allowFrom"] = []string{"*"} + case "allowlist": + entry["allowFrom"] = bot.AllowFrom + default: + if len(bot.AllowFrom) > 0 { + entry["allowFrom"] = bot.AllowFrom + } else { + entry["allowFrom"] = []string{} + } + } + accounts[bot.AccountID] = entry + } + for id := range accounts { + if !seen[id] { + delete(accounts, id) + } + } + if !seen["default"] { + delete(f, "appId") + delete(f, "appSecret") + } + if id := extractStringValue(f["defaultAccount"]); id != "" && !seen[id] { + return feishuConfigError("defaultAccount") + } + if len(accounts) > 0 { + f["accounts"] = accounts + } else { + delete(f, "accounts") + } + f["enabled"], f["groupSessionScope"], f["groupPolicy"] = req.Enabled, req.GroupSessionScope, req.GroupPolicy + if req.RequireMention == "open" { + delete(f, "requireMention") + } else { + f["requireMention"] = req.RequireMention == "true" + } + if req.GroupPolicy == "allowlist" { + f["groupAllowFrom"] = req.GroupAllowFrom + } else { + delete(f, "groupAllowFrom") + } + streaming := ensureChildMap(f, "streaming") + streaming["mode"] = "off" + if req.Streaming { + streaming["mode"] = "partial" + } + selectBundledFeishu(conf, req.Enabled && hasEnabledBots(req.Bots)) + return nil +} + +// Write beside the original so validation and rename preserve the mounted filesystem and owner. +func stageFeishuConfig(configPath string, payload []byte) (string, error) { + info, err := os.Lstat(configPath) + if err != nil { + return "", err + } + if !info.Mode().IsRegular() { + return "", feishuConfigError("openclaw.json") + } + f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-*.json") + if err != nil { + return "", err + } + name := f.Name() + defer func() { + f.Close() + if err != nil { + os.Remove(name) + } + }() + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + err = f.Chown(int(stat.Uid), int(stat.Gid)) + } + if err == nil { + err = f.Chmod(info.Mode().Perm()) + } + if err == nil { + _, err = f.Write(payload) + } + if err == nil { + err = f.Sync() + } + if err == nil { + err = f.Close() + } + return name, err +} + +func validateFeishuConfig(container, configPath string) error { + out, err := cmd.RunDockerExecWithStdout(time.Minute, container, "env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath), "openclaw", "config", "validate", "--json") + return feishuValidationResult(out, err) +} + +func replaceFeishuConfig(configPath string, expected, payload []byte) error { + tmp, err := stageFeishuConfig(configPath, payload) + if err != nil { + return err + } + defer os.Remove(tmp) + current, err := os.ReadFile(configPath) + if err != nil { + return err + } + if !bytes.Equal(current, expected) { + return buserr.New("ErrFeishuConfigChanged") + } + return os.Rename(tmp, configPath) +} + +func (a AgentService) SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error { + agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID) + if err != nil { + return err + } + if !isOpenclawBundledFeishuVersion(install.Version) { + return buserr.New("ErrFeishuImageRequired") + } + if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy { + return buserr.New("TaskIsExecuting") + } + started := false + defer func() { + if !started { + feishuSwitches.Delete(agent.ConfigPath) + } + }() + if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil { + return err + } + if !hasBundledFeishu(install.ContainerName) { + return buserr.New("ErrFeishuImageRequired") + } + conf, err := readOpenclawConfig(agent.ConfigPath) + if err != nil { + return err + } + legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath()) + if err != nil { + return err + } + if !legacy { + return buserr.New("ErrFeishuAlreadySwitched") + } + if err := prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil { + return err + } + switchTask, err := task.NewTaskWithOps("Feishu", task.TaskUpdate, task.TaskScopeAI, req.TaskID, req.AgentID) + if err != nil { + return err + } + switchTask.AddSubTaskWithAlias("AgentFeishuSwitch", func(t *task.Task) error { return a.runFeishuSwitch(agent, install, req, t) }, nil) + started = true + go func() { defer feishuSwitches.Delete(agent.ConfigPath); executeAgentPluginTask(switchTask) }() + return nil +} + +func (a AgentService) runFeishuSwitch(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuSwitchReq, t *task.Task) (result error) { + original, conf, err := readFeishuConfigBytes(agent.ConfigPath) + if err != nil { + return err + } + legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath()) + if err != nil { + return err + } + if !legacy { + return buserr.New("ErrFeishuAlreadySwitched") + } + image, err := feishuContainerImage(install.ContainerName) + if err != nil { + return err + } + image, err = bundledFeishuImage(image) + if err != nil { + return err + } + if err = prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil { + return err + } + payload, err := json.MarshalIndent(conf, "", " ") + if err != nil { + return err + } + recovery, err := feishuRecoveryPayload(payload, configured) + if err != nil { + return err + } + // Validate both candidates without invoking Doctor or mutating retained state. + for _, candidate := range [][]byte{payload, recovery} { + staged, stageErr := stageFeishuConfig(agent.ConfigPath, candidate) + if stageErr != nil { + return stageErr + } + validateErr := validateFeishuImageConfig(image, staged) + os.Remove(staged) + if validateErr != nil { + return validateErr + } + } + if _, err = compose.Stop(install.GetComposePath()); err != nil { + return err + } + replaced := false + defer func() { + if !replaced { + if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil { + result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr)) + } + return + } + if result == nil { + return + } + if _, stopErr := compose.Stop(install.GetComposePath()); stopErr != nil { + result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", stopErr)) + return + } + if restoreErr := replaceFeishuConfig(agent.ConfigPath, payload, recovery); restoreErr != nil { + result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", restoreErr)) + return + } + if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil { + result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr)) + return + } + if healthErr := verifyFeishuActivation(install.ContainerName, false); healthErr != nil { + result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", healthErr)) + return + } + t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled")) + }() + current, err := os.ReadFile(agent.ConfigPath) + if err != nil { + return err + } + if !bytes.Equal(current, original) { + return buserr.New("ErrFeishuConfigChanged") + } + backup, err := backupFeishuConfig(agent.ConfigPath, original) + if err != nil { + return err + } + t.Log(i18n.GetWithName("AgentFeishuBackup", filepath.Base(backup))) + if err = replaceFeishuConfig(agent.ConfigPath, original, payload); err != nil { + return err + } + replaced = true + if _, err = compose.Operate(install.GetComposePath(), "start"); err != nil { + return err + } + enabled := configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true) + if err = verifyFeishuActivation(install.ContainerName, enabled); err != nil { + return err + } + if enabled { + probeFeishuConnection(install.ContainerName, t) + } + if configured { + t.Log(i18n.GetMsgByKey("AgentFeishuVerifyMessages")) + } else { + t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled")) + } + return nil +} + +func saveBundledFeishuConfig(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuConfigUpdateReq) error { + if !isOpenclawBundledFeishuVersion(install.Version) { + return buserr.New("ErrFeishuImageRequired") + } + if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy { + return buserr.New("TaskIsExecuting") + } + defer feishuSwitches.Delete(agent.ConfigPath) + if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil { + return err + } + if !hasBundledFeishu(install.ContainerName) { + return buserr.New("ErrFeishuImageRequired") + } + original, err := os.ReadFile(agent.ConfigPath) + if err != nil { + return err + } + var conf map[string]interface{} + if err = json.Unmarshal(original, &conf); err != nil { + return err + } + if hasLegacyFeishu(conf, install.GetPath()) { + return buserr.New("ErrFeishuSwitchRequired") + } + if err = setBundledFeishuConfig(conf, req); err != nil { + return err + } + payload, err := json.MarshalIndent(conf, "", " ") + if err != nil { + return err + } + staged, err := stageFeishuConfig(agent.ConfigPath, payload) + if err != nil { + return err + } + defer os.Remove(staged) + if err = validateFeishuConfig(install.ContainerName, staged); err != nil { + return err + } + return replaceFeishuConfig(agent.ConfigPath, original, payload) +} + +func feishuProbeOK(output string) bool { + raw, err := extractEmbeddedJSON(output) + if err != nil { + return false + } + var status struct { + ChannelAccounts map[string][]struct { + Enabled bool `json:"enabled"` + Configured bool `json:"configured"` + Running bool `json:"running"` + Probe struct { + OK bool `json:"ok"` + } `json:"probe"` + } `json:"channelAccounts"` + } + if json.Unmarshal(raw, &status) != nil { + return false + } + accounts, ok := status.ChannelAccounts["feishu"] + if !ok { + return false + } + for _, account := range accounts { + if account.Enabled && (!account.Configured || !account.Running || !account.Probe.OK) { + return false + } + } + return true +} + +func feishuGatewayReady(output string, enabled bool) bool { + raw, err := extractEmbeddedJSON(output) + if err != nil { + return false + } + var health struct { + OK bool `json:"ok"` + Plugins struct { + Loaded []string `json:"loaded"` + } `json:"plugins"` + } + return json.Unmarshal(raw, &health) == nil && health.OK && !slices.Contains(health.Plugins.Loaded, "openclaw-lark") && (!enabled || slices.Contains(health.Plugins.Loaded, "feishu")) +} diff --git a/agent/app/service/agents_feishu_runtime.go b/agent/app/service/agents_feishu_runtime.go new file mode 100644 index 000000000..08213a6ce --- /dev/null +++ b/agent/app/service/agents_feishu_runtime.go @@ -0,0 +1,201 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types/container" +) + +// Inspect a stopped disposable container; its entrypoint never touches retained state. +func bundledFeishuImage(image string) (string, error) { + cli, err := docker.NewDockerClient() + if err != nil { + return "", err + } + defer cli.Close() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + info, _, err := cli.ImageInspectWithRaw(ctx, image) + if err != nil { + return "", buserr.New("ErrFeishuImageRequired") + } + created, err := cli.ContainerCreate(ctx, &container.Config{Image: info.ID, Entrypoint: []string{"/bin/true"}}, &container.HostConfig{NetworkMode: "none"}, nil, nil, "") + if err != nil { + return "", err + } + defer func() { + cleanup, done := context.WithTimeout(context.Background(), 10*time.Second) + defer done() + _ = cli.ContainerRemove(cleanup, created.ID, container.RemoveOptions{Force: true, RemoveVolumes: true}) + }() + if _, err = cli.ContainerStatPath(ctx, created.ID, "/app/.1panel-feishu-bundled-v1"); err != nil { + return "", buserr.New("ErrFeishuImageRequired") + } + return info.ID, nil +} + +func feishuContainerImage(name string) (string, error) { + cli, err := docker.NewDockerClient() + if err != nil { + return "", err + } + defer cli.Close() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + info, err := cli.ContainerInspect(ctx, name) + if err != nil { + return "", err + } + return info.Image, nil +} + +// The original state is read-only and the normal Doctor entrypoint is bypassed. +func validateFeishuImageConfig(image, configPath string) error { + info, err := os.Stat(configPath) + if err != nil { + return err + } + uid, gid := uint32(1000), uint32(1000) + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + uid, gid = stat.Uid, stat.Gid + } + user := fmt.Sprintf("%d:%d", uid, gid) + tmpfs := fmt.Sprintf("/tmp:rw,mode=0700,uid=%d,gid=%d", uid, gid) + name := "1panel-feishu-" + strings.TrimSuffix(filepath.Base(configPath), ".json") + mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute)) + defer func() { _ = cmd.NewCommandMgr(cmd.WithTimeout(15*time.Second)).Run("docker", "rm", "-f", "-v", name) }() + out, runErr := mgr.RunWithStdout("docker", "run", "--name", name, "--rm", "--network", "none", "--read-only", "--user", user, + "--tmpfs", tmpfs, + "--mount", "type=bind,src="+filepath.Dir(configPath)+",dst=/home/node/.openclaw,readonly", + "--env", "HOME=/home/node", "--env", "TMPDIR=/tmp", "--env", "OPENCLAW_STATE_DIR=/tmp/state", + "--env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath), + "--entrypoint", "sh", image, "-c", "mkdir -m 700 /tmp/state && ln -s /home/node/.openclaw/extensions /tmp/state/extensions && exec node /app/openclaw.mjs config validate --json") + return feishuValidationResult(out, runErr) +} + +func feishuValidationResult(out string, runErr error) error { + raw, _ := extractEmbeddedJSON(out) + var result struct { + Valid bool `json:"valid"` + Issues []struct { + Path string `json:"path"` + } `json:"issues"` + } + if json.Unmarshal(raw, &result) == nil && result.Valid && runErr == nil { + return nil + } + fields := []string{} + for _, issue := range result.Issues { + if issue.Path != "" { + fields = append(fields, issue.Path) + } + } + if len(fields) == 0 { + fields = append(fields, "openclaw.json") + } + return feishuConfigError(strings.Join(fields, ", ")) +} + +func backupFeishuConfig(configPath string, original []byte) (string, error) { + f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-backup-*.json") + if err != nil { + return "", err + } + if _, err = f.Write(original); err == nil { + err = f.Sync() + } + closeErr := f.Close() + if err != nil { + return "", err + } + if closeErr != nil { + return "", closeErr + } + return f.Name(), nil +} + +// Doctor can spend 94 seconds waiting for the previous container's owner lease. +func waitOpenclawReady(name string) error { + script := `const fs=require('fs');const c=JSON.parse(fs.readFileSync('/home/node/.openclaw/openclaw.json','utf8'));const tls=c.gateway?.tls?.enabled===true;const h=require(tls?'https':'http');function check(p){const r=h.get({hostname:'127.0.0.1',port:c.gateway?.port||18789,path:p,rejectUnauthorized:false,timeout:3000},s=>{s.resume();if(s.statusCode===404&&p==='/readyz')return check('/healthz');process.exitCode=s.statusCode===200?0:1});r.on('timeout',()=>r.destroy());r.on('error',()=>process.exitCode=1)}check('/readyz')` + deadline := time.Now().Add(4 * time.Minute) + for time.Now().Before(deadline) { + if cmd.RunDockerExec(8*time.Second, name, "node", "-e", script) == nil { + return nil + } + time.Sleep(2 * time.Second) + } + return buserr.New("ErrFeishuActivation") +} + +func verifyFeishuActivation(name string, enabled bool) error { + if err := waitOpenclawReady(name); err != nil { + return err + } + output, err := cmd.RunDockerExecWithStdout(time.Minute, name, "openclaw", "plugins", "list", "--json") + if err != nil { + return buserr.New("ErrFeishuActivation") + } + raw, _ := extractEmbeddedJSON(output) + var result struct { + Plugins []struct { + ID string `json:"id"` + Status string `json:"status"` + Origin string `json:"origin"` + } `json:"plugins"` + } + if json.Unmarshal(raw, &result) != nil { + return buserr.New("ErrFeishuActivation") + } + found := false + for _, p := range result.Plugins { + if p.ID == "openclaw-lark" && p.Status != "disabled" { + return buserr.New("ErrFeishuActivation") + } + if p.ID == "feishu" && p.Origin == "bundled" && (p.Status == "loaded" || (!enabled && p.Status == "disabled")) { + found = true + } + } + if !found { + return buserr.New("ErrFeishuActivation") + } + health, err := cmd.RunDockerExecWithStdout(20*time.Second, name, "openclaw", "gateway", "health", "--json") + if err != nil || !feishuGatewayReady(health, enabled) { + return buserr.New("ErrFeishuActivation") + } + return nil +} + +func readFeishuConfigBytes(file string) ([]byte, map[string]interface{}, error) { + info, err := os.Lstat(file) + if err != nil { + return nil, nil, err + } + if !info.Mode().IsRegular() { + return nil, nil, feishuConfigError("openclaw.json") + } + f, err := os.Open(file) + if err != nil { + return nil, nil, err + } + defer f.Close() + raw, err := io.ReadAll(f) + if err != nil { + return nil, nil, err + } + var conf map[string]interface{} + if err = json.Unmarshal(raw, &conf); err != nil || conf == nil { + return nil, nil, feishuConfigError("openclaw.json") + } + return raw, conf, nil +} diff --git a/agent/app/service/agents_plugins.go b/agent/app/service/agents_plugins.go index a80bae42d..b38ae9892 100644 --- a/agent/app/service/agents_plugins.go +++ b/agent/app/service/agents_plugins.go @@ -107,6 +107,9 @@ func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) e if err != nil { return err } + if isOpenclawBundledFeishuVersion(install.Version) && (strings.TrimSpace(req.Package) == "@larksuite/openclaw-lark" || strings.TrimSpace(req.Package) == "@openclaw/feishu") { + return buserr.New("ErrFeishuManaged") + } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil { return err } @@ -132,6 +135,9 @@ func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error { if err != nil { return err } + if isOpenclawBundledFeishuVersion(install.Version) && (req.PluginID == "openclaw-lark" || req.PluginID == "feishu") { + return buserr.New("ErrFeishuManaged") + } if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil { return err } diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index 0d549cf26..c2f1119d5 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -148,6 +148,10 @@ func readOpenclawConfig(configPath string) (map[string]interface{}, error) { } func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error { + if _, busy := feishuSwitches.LoadOrStore(configPath, true); busy { + return buserr.New("TaskIsExecuting") + } + defer feishuSwitches.Delete(configPath) ensureGatewaySecurityDefaults(conf) ensureOpenclawUpdateDefaults(conf) payload, err := json.MarshalIndent(conf, "", " ") diff --git a/agent/app/service/app_install.go b/agent/app/service/app_install.go index 4dbc5fdcd..de4679cd2 100644 --- a/agent/app/service/app_install.go +++ b/agent/app/service/app_install.go @@ -305,13 +305,14 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error { return syncAppInstallStatus(&install, true) case constant.Upgrade: upgradeReq := request.AppInstallUpgrade{ - InstallID: install.ID, - DetailID: req.DetailId, - Backup: req.Backup, - PullImage: req.PullImage, - DeleteImage: req.DeleteImage, - DockerCompose: req.DockerCompose, - TaskID: req.TaskID, + FeishuTransition: req.FeishuTransition, + InstallID: install.ID, + DetailID: req.DetailId, + Backup: req.Backup, + PullImage: req.PullImage, + DeleteImage: req.DeleteImage, + DockerCompose: req.DockerCompose, + TaskID: req.TaskID, } return upgradeInstall(upgradeReq) case constant.Reload: @@ -716,6 +717,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d if err != nil { return versions, err } + var feishuLegacy, feishuConfigured, feishuChecked bool details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) if err != nil { return versions, err @@ -740,6 +742,18 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage) } if canUpgrade { + feishuTarget := app.Key == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(detail.Version) + if feishuTarget && !feishuChecked { + conf, readErr := readOpenclawConfig(path.Join(install.GetPath(), "data/conf/openclaw.json")) + if readErr != nil { + return versions, readErr + } + feishuLegacy, feishuConfigured, err = classifyLegacyFeishu(conf, install.GetPath()) + if err != nil { + return versions, err + } + feishuChecked = true + } var newCompose string if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() { filename := filepath.Base(detail.DownloadUrl) @@ -774,9 +788,11 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d } } versions = append(versions, dto.AppVersion{ - Version: detail.Version, - DetailId: detail.ID, - DockerCompose: newCompose, + FeishuLegacy: feishuTarget && feishuLegacy, + FeishuConfigured: feishuTarget && feishuConfigured, + Version: detail.Version, + DetailId: detail.ID, + DockerCompose: newCompose, }) } } diff --git a/agent/app/service/app_upgrade.go b/agent/app/service/app_upgrade.go index 7040174ee..26ad34b92 100644 --- a/agent/app/service/app_upgrade.go +++ b/agent/app/service/app_upgrade.go @@ -60,10 +60,16 @@ type upgradeFileSnapshot struct { } type appUpgradeContext struct { - req request.AppInstallUpgrade - original model.AppInstall - candidate model.AppInstall - detail model.AppDetail + openclawOriginal []byte + feishuOriginal []byte + feishuImage string + feishuOldImage string + feishuEnabled bool + feishuLockPath string + req request.AppInstallUpgrade + original model.AppInstall + candidate model.AppInstall + detail model.AppDetail phase appUpgradePhase stopAttempted bool @@ -102,6 +108,19 @@ func upgradeInstall(req request.AppInstallUpgrade) error { } }() + feishuLockPath := "" + if install.App.Key == constant.AppOpenclaw { + feishuLockPath = path.Join(install.GetPath(), "data/conf/openclaw.json") + if _, busy := feishuSwitches.LoadOrStore(feishuLockPath, true); busy { + return buserr.New("TaskIsExecuting") + } + defer func() { + if releaseLock { + feishuSwitches.Delete(feishuLockPath) + } + }() + } + detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID)) if err != nil { return err @@ -129,6 +148,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error { return err } ctx := &appUpgradeContext{ + feishuLockPath: feishuLockPath, req: req, original: install, candidate: install, @@ -222,6 +242,10 @@ func (u *appUpgradeContext) prepare(t *task.Task) error { if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil { return err } + u.openclawOriginal, err = os.ReadFile(path.Join(u.stageDir, "data/conf/openclaw.json")) + if err != nil { + return err + } } if u.original.App.Key == constant.AppOpenresty { for _, relativePath := range []string{ @@ -330,6 +354,9 @@ func (u *appUpgradeContext) prepare(t *task.Task) error { if u.original.App.Resource == constant.AppResourceRemote { go RequestDownloadCallBack(u.detail.DownloadCallBackUrl) } + if err = u.prepareFeishuUpgrade(); err != nil { + return err + } u.phase = appUpgradePrepared return nil } @@ -387,6 +414,9 @@ func (u *appUpgradeContext) cutover(t *task.Task) error { } t.LogSuccess(i18n.GetMsgByKey("UpgradeStop")) u.phase = appUpgradeStopped + if err := u.checkFeishuUpgradeSource(); err != nil { + return err + } var err error if u.original.App.Key == constant.AppOpenresty { @@ -454,6 +484,9 @@ func (u *appUpgradeContext) cutover(t *task.Task) error { } else { t.Log("WARNING: no containers found for the upgraded application") } + if err = u.verifyFeishuUpgrade(t); err != nil { + return err + } u.candidate.Status = constant.StatusRunning u.candidate.Message = "" @@ -579,8 +612,34 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) { if out != "" { err = fmt.Errorf("%s: %w", out, err) } + if u.feishuOriginal != nil { + return err + } rollbackErr = err } + if u.feishuOriginal != nil { + if u.backupFile == "" { + return buserr.New("ErrFeishuUpgradeBackup") + } + if err := u.restoreManagedFiles(); err != nil { + return err + } + if err := handleAppRecover(&u.original, t, u.backupFile, true, "", "", false); err != nil { + return err + } + if err := pinFeishuRollbackImage(u.original.GetComposePath(), u.feishuOldImage); err != nil { + return err + } + restoredCompose, err := os.ReadFile(u.original.GetComposePath()) + if err != nil { + return err + } + u.original.DockerCompose = string(restoredCompose) + if _, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil { + return err + } + return u.finishRollback() + } if u.backupFile != "" { _ = u.restoreManagedFiles() if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil { @@ -602,6 +661,11 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) { } func (u *appUpgradeContext) finishRollback() error { + if u.feishuOriginal != nil { + if err := waitOpenclawReady(u.original.ContainerName); err != nil { + return err + } + } restored := u.original if err := appInstallRepo.Save(context.Background(), &restored); err != nil { return err @@ -643,6 +707,12 @@ func (u *appUpgradeContext) deleteOldImages(t *task.Task) { } func (u *appUpgradeContext) cleanup() { + if u.feishuLockPath != "" { + feishuSwitches.Delete(u.feishuLockPath) + } + if u.feishuOriginal != nil && u.rollbackErr != nil { + return + } if u.snapshot != nil { u.snapshot.Cleanup() } diff --git a/agent/app/service/app_upgrade_feishu.go b/agent/app/service/app_upgrade_feishu.go new file mode 100644 index 000000000..6445ca3c4 --- /dev/null +++ b/agent/app/service/app_upgrade_feishu.go @@ -0,0 +1,336 @@ +package service + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "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/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "gopkg.in/yaml.v3" +) + +func classifyLegacyFeishu(conf map[string]interface{}, installPath string) (legacy, configured bool, err error) { + for _, field := range []string{"channels", "channels.feishu", "plugins", "plugins.entries", "plugins.entries.openclaw-lark", "plugins.entries.feishu"} { + parts := strings.Split(field, ".") + parent := conf + for _, part := range parts[:len(parts)-1] { + parent = childMap(parent, part) + } + if raw, exists := parent[parts[len(parts)-1]]; exists { + if _, ok := raw.(map[string]interface{}); !ok { + return false, false, feishuConfigError(field) + } + } + } + plugins := childMap(conf, "plugins") + for _, key := range []string{"allow", "deny"} { + if raw, exists := plugins[key]; exists { + if _, ok := raw.([]string); ok { + continue + } + ids, ok := raw.([]interface{}) + if !ok { + return false, false, feishuConfigError("plugins." + key) + } + for _, id := range ids { + if _, ok := id.(string); !ok { + return false, false, feishuConfigError("plugins." + key) + } + } + } + } + for _, id := range []string{"openclaw-lark", "feishu"} { + if raw, exists := childMap(childMap(plugins, "entries"), id)["enabled"]; exists { + if _, ok := raw.(bool); !ok { + return false, false, feishuConfigError("plugins.entries." + id + ".enabled") + } + } + } + if usesBundledFeishu(conf) { + return false, false, nil + } + f := getChannelConfig(conf, "feishu") + configured = len(f) > 0 + if raw, exists := conf["bindings"]; exists { + bindings, ok := raw.([]interface{}) + if !ok { + return false, false, feishuConfigError("bindings") + } + for _, raw := range bindings { + binding, ok := raw.(map[string]interface{}) + if !ok { + return false, false, feishuConfigError("bindings") + } + if match, exists := binding["match"]; exists { + if _, ok := match.(map[string]interface{}); !ok { + return false, false, feishuConfigError("bindings.match") + } + } + if extractStringValue(childMap(binding, "match")["channel"]) == "feishu" { + configured = true + } + } + } + entries := childMap(childMap(conf, "plugins"), "entries") + entry, selected := entries["openclaw-lark"] + if selected { + old, ok := entry.(map[string]interface{}) + if !ok { + return false, configured, feishuConfigError("plugins.entries.openclaw-lark") + } + // A completed disable-only repair must not repeat on every upgrade. + if enabled, ok := old["enabled"].(bool); ok && !enabled && !configured { + return false, false, nil + } + } + installed, err := checkPluginInstalled(installPath, "feishu") + if err != nil { + return false, configured, err + } + _, oldThread := f["threadSession"] + _, oldReply := f["replyMode"] + _, oldStreaming := f["streaming"].(bool) + return selected || installed || oldThread || oldReply || oldStreaming, configured, nil +} + +func prepareLegacyFeishu(conf map[string]interface{}, configured bool, transition *dto.AgentFeishuTransition) error { + if !configured { + ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false + return nil + } + if transition == nil || !transition.Confirmed { + return buserr.New("ErrFeishuUpgradeConfirm") + } + if !validFeishuTransition(transition.GroupSessionScope, transition.RequireMention) { + return feishuConfigError("feishuTransition") + } + return convertLegacyFeishu(conf, dto.AgentFeishuSwitchReq{Streaming: transition.Streaming, GroupSessionScope: transition.GroupSessionScope, RequireMention: transition.RequireMention}) +} + +func validFeishuTransition(scope, mention string) bool { + return (scope == "group" || scope == "group_sender" || scope == "group_topic" || scope == "group_topic_sender") && (mention == "true" || mention == "false") +} + +func feishuRecoveryPayload(candidate []byte, configured bool) ([]byte, error) { + var conf map[string]interface{} + if err := json.Unmarshal(candidate, &conf); err != nil { + return nil, err + } + if configured { + ensureChildMap(ensureChildMap(conf, "channels"), "feishu")["enabled"] = false + selectBundledFeishu(conf, false) + } + ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false + return json.MarshalIndent(conf, "", " ") +} + +func (u *appUpgradeContext) prepareFeishuUpgrade() error { + if u.original.App.Key != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(u.candidate.Version) { + return nil + } + source := filepath.Join(u.original.GetPath(), "data/conf/openclaw.json") + staged := filepath.Join(u.stageDir, "data/conf/openclaw.json") + // The staged copy predates protocol migration and is the exact cutover comparison source. + original := u.openclawOriginal + current, err := os.ReadFile(source) + if err != nil { + return err + } + if !bytes.Equal(original, current) { + return buserr.New("ErrFeishuConfigChanged") + } + _, conf, err := readFeishuConfigBytes(staged) + if err != nil { + return err + } + legacy, configured, err := classifyLegacyFeishu(conf, u.original.GetPath()) + if err != nil || !legacy { + return err + } + if !u.req.Backup { + return buserr.New("ErrFeishuUpgradeBackup") + } + if err = prepareLegacyFeishu(conf, configured, u.req.FeishuTransition); err != nil { + return err + } + project, err := docker.GetComposeProject(u.original.Name, u.original.GetPath(), []byte(u.candidate.DockerCompose), u.envContent, false) + if err != nil { + return err + } + if len(project.Services) != 1 { + return buserr.New("ErrFeishuUpgradeMounts") + } + for _, service := range project.Services { + if service.Build != nil { + return buserr.New("ErrFeishuImageRequired") + } + u.feishuImage, err = bundledFeishuImage(service.Image) + if err != nil { + return err + } + configMounted := false + for _, v := range service.Volumes { + if v.Target == "/etc/localtime" { + continue + } + if v.Type != "bind" { + return buserr.New("ErrFeishuUpgradeMounts") + } + if err = feishuBackupPath(u.original.GetPath(), v.Source); err != nil { + return err + } + if v.Target == "/home/node/.openclaw" { + configMounted = filepath.Clean(v.Source) == filepath.Dir(source) + } + } + if !configMounted { + return buserr.New("ErrFeishuUpgradeMounts") + } + } + cli, err := docker.NewDockerClient() + if err != nil { + return err + } + defer cli.Close() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + info, err := cli.ContainerInspect(ctx, u.original.ContainerName) + if err != nil { + return err + } + u.feishuOldImage = info.Image + for _, m := range info.Mounts { + if m.Destination == "/etc/localtime" { + continue + } + if string(m.Type) != "bind" { + return buserr.New("ErrFeishuUpgradeMounts") + } + if err = feishuBackupPath(u.original.GetPath(), m.Source); err != nil { + return err + } + } + // Tar preserves symlinks: an external target would not be in the application backup. + if err = filepath.WalkDir(filepath.Join(u.original.GetPath(), "data"), func(file string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.Type()&os.ModeSymlink != 0 { + return feishuBackupPath(u.original.GetPath(), file) + } + return nil + }); err != nil { + return err + } + u.feishuOriginal = original + u.feishuEnabled = configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true) + payload, err := json.MarshalIndent(conf, "", " ") + if err != nil { + return err + } + before, err := os.ReadFile(staged) + if err != nil { + return err + } + if err = replaceFeishuConfig(staged, before, payload); err != nil { + return err + } + targetCompose := filepath.Join(u.stageDir, "docker-compose.yml") + if err = pinFeishuRollbackImage(targetCompose, u.feishuImage); err != nil { + return err + } + pinned, err := os.ReadFile(targetCompose) + if err != nil { + return err + } + u.candidate.DockerCompose = string(pinned) + return nil +} + +func feishuBackupPath(root, source string) error { + root, err := filepath.EvalSymlinks(root) + if err != nil { + return err + } + resolved, err := filepath.EvalSymlinks(source) + if err != nil { + return buserr.New("ErrFeishuUpgradeMounts") + } + rel, err := filepath.Rel(root, resolved) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return buserr.New("ErrFeishuUpgradeMounts") + } + return nil +} + +func (u *appUpgradeContext) checkFeishuUpgradeSource() error { + if u.feishuOriginal == nil { + return nil + } + current, err := os.ReadFile(filepath.Join(u.original.GetPath(), "data/conf/openclaw.json")) + if err != nil { + return err + } + if !bytes.Equal(current, u.feishuOriginal) { + return buserr.New("ErrFeishuConfigChanged") + } + return nil +} + +func (u *appUpgradeContext) verifyFeishuUpgrade(t *task.Task) error { + if u.feishuOriginal == nil { + return nil + } + if err := verifyFeishuActivation(u.candidate.ContainerName, u.feishuEnabled); err != nil { + return err + } + if u.feishuEnabled { + probeFeishuConnection(u.candidate.ContainerName, t) + } + return nil +} + +func probeFeishuConnection(name string, t *task.Task) { + output, err := cmd.RunDockerExecWithStdout(30*time.Second, name, "openclaw", "channels", "status", "--channel", "feishu", "--probe", "--json") + if err != nil || !feishuProbeOK(output) { + t.Log(i18n.GetMsgByKey("AgentFeishuProbeWarning")) + } +} + +// Pin the restored service to the actual old image, even if its registry tag moved. +func pinFeishuRollbackImage(composeFile string, image string) error { + raw, err := os.ReadFile(composeFile) + if err != nil { + return err + } + var conf map[string]interface{} + if err = yaml.Unmarshal(raw, &conf); err != nil { + return err + } + services := childMap(conf, "services") + if len(services) != 1 { + return buserr.New("ErrFeishuUpgradeMounts") + } + for _, raw := range services { + service, ok := raw.(map[string]interface{}) + if !ok { + return buserr.New("ErrFeishuUpgradeMounts") + } + service["image"] = image + } + payload, err := yaml.Marshal(conf) + if err != nil { + return err + } + return writeUpgradeFile(composeFile, payload, constant.FilePerm) +} diff --git a/agent/app/service/backup_app.go b/agent/app/service/backup_app.go index 714bd9ae9..64e9395cf 100644 --- a/agent/app/service/backup_app.go +++ b/agent/app/service/backup_app.go @@ -197,7 +197,8 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID return nil } -func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error { +func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string, startAfterRecover ...bool) error { + startAfter := len(startAfterRecover) == 0 || startAfterRecover[0] // Validate AI Gateway backups before stopping the app, creating rollback // backups, or touching its files/database. Keep the validated extraction for // the restore so uploaded/encrypted archives are not unpacked twice. @@ -246,12 +247,15 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF return err } defer func() { + defer os.RemoveAll(tmpPath) + if !startAfter { + return + } if isRollback { _, _ = compose.UpWithoutBuild(install.GetComposePath()) } else { _, _ = compose.Up(install.GetComposePath()) } - _ = os.RemoveAll(tmpPath) }() if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") { @@ -352,8 +356,12 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF appDir := install.GetPath() backPath := fmt.Sprintf("%s_bak", appDir) - _ = fileOp.Rename(appDir, backPath) - _ = fileOp.CreateDir(appDir, constant.DirPerm) + if err := fileOp.Rename(appDir, backPath); err != nil && !startAfter { + return err + } + if err := fileOp.CreateDir(appDir, constant.DirPerm); err != nil && !startAfter { + return err + } deCompressName := i18n.GetWithName("DeCompressFile", "app.tar.gz") t.LogStart(deCompressName) @@ -376,6 +384,10 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF _, _ = file.WriteString(mergedEnvContent) } + // The upgrade transaction verifies readiness before saving its restored install. + if !startAfter { + return nil + } backupInstall.ID = install.ID backupInstall.Status = constant.StatusRunning backupInstall.AppId = install.AppId diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index a3a47cfad..4e29cdfc5 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "Delete virtual machine snapshot [{{ .snapshot }}]" ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "Resolve incompatible configuration before continuing: {{ .detail }}" +ErrFeishuConfigChanged: "Configuration changed during switching. Retry after reloading." +ErrFeishuImageRequired: "Use an image containing bundled Feishu." +ErrFeishuAlreadySwitched: "Feishu has already switched to the built-in plugin." +ErrFeishuSwitchRequired: "Switch to the built-in plugin from the Feishu channel page first." +ErrFeishuManaged: "Manage Feishu from its channel page; the plugin is updated with the image." +ErrFeishuRestore: "Failed to restore Feishu configuration: {{ .err }}" +ErrFeishuActivation: "The built-in Feishu plugin or gateway failed to start." +AgentFeishuSwitch: "Switch Feishu plugin" +AgentFeishuBackup: "Configuration backup: {{ .name }}" +AgentFeishuRestored: "Previous configuration restored." +AgentFeishuProbeWarning: "Feishu connection check failed. Check the network and credentials; the switch has been retained." +AgentFeishuVerifyMessages: "Local activation completed. Verify pairing, DMs, groups, bot routing and streaming in Feishu." + +ErrFeishuUpgradeConfirm: "Confirm the Feishu transition options on the upgrade page before retrying." + +ErrFeishuUpgradeBackup: "A full application backup is required for the Feishu compatibility upgrade." + +ErrFeishuUpgradeMounts: "These mounts cannot be fully backed up or restored. Use standard data mounts inside the application directory." + +AgentFeishuRecoveryDisabled: "Gateway recovered with Feishu disabled; the original configuration backup is retained." diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index e9ddb5505..05e44451d 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "Eliminar instantánea de máquina virtual [{{ .snapshot } ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "Corrija la configuración incompatible antes de continuar: {{ .detail }}" +ErrFeishuConfigChanged: "La configuración cambió durante la transición. Recargue y reintente." +ErrFeishuImageRequired: "Use una imagen que incluya Feishu." +ErrFeishuAlreadySwitched: "Feishu ya utiliza el complemento integrado." +ErrFeishuSwitchRequired: "Cambie primero al complemento integrado desde la página del canal Feishu." +ErrFeishuManaged: "Gestione Feishu desde su canal; el complemento se actualiza con la imagen." +ErrFeishuRestore: "Error al restaurar la configuración de Feishu: {{ .err }}" +ErrFeishuActivation: "No se pudo iniciar el complemento Feishu integrado o la puerta de enlace." +AgentFeishuSwitch: "Cambiar el complemento Feishu" +AgentFeishuBackup: "Copia de configuración: {{ .name }}" +AgentFeishuRestored: "Se restauró la configuración anterior." +AgentFeishuProbeWarning: "Falló la comprobación de conexión de Feishu. Revise la red y las credenciales; se conservó el cambio." +AgentFeishuVerifyMessages: "Activación local completada. Verifique el emparejamiento, chats, grupos, rutas de bots y transmisión en Feishu." + +ErrFeishuUpgradeConfirm: "Confirme las opciones de transición de Feishu en la página de actualización." + +ErrFeishuUpgradeBackup: "La actualización de compatibilidad de Feishu requiere una copia completa de la aplicación." + +ErrFeishuUpgradeMounts: "Estos montajes no permiten una copia y restauración completas. Use los montajes de datos estándar dentro del directorio de la aplicación." + +AgentFeishuRecoveryDisabled: "Puerta de enlace recuperada con Feishu desactivado; se conserva la copia de configuración original." diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 2fa82e5c8..8011e3b16 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -778,3 +778,26 @@ VMSnapshotDeleteStep: "حذف اسنپ‌شات ماشین مجازی [{{ .snaps ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "پیش از ادامه تنظیمات ناسازگار را اصلاح کنید: {{ .detail }}" +ErrFeishuConfigChanged: "تنظیمات هنگام تغییر عوض شد. صفحه را تازه و دوباره تلاش کنید." +ErrFeishuImageRequired: "از تصویری با Feishu داخلی استفاده کنید." +ErrFeishuAlreadySwitched: "Feishu قبلاً به افزونه داخلی تغییر کرده است." +ErrFeishuSwitchRequired: "ابتدا در صفحه کانال Feishu به افزونه داخلی تغییر دهید." +ErrFeishuManaged: "Feishu را از صفحه کانال مدیریت کنید؛ افزونه همراه تصویر به‌روز می‌شود." +ErrFeishuRestore: "بازیابی تنظیمات Feishu ناموفق بود: {{ .err }}" +ErrFeishuActivation: "راه‌اندازی افزونه داخلی Feishu یا دروازه ناموفق بود." +AgentFeishuSwitch: "تغییر افزونه Feishu" +AgentFeishuBackup: "پشتیبان تنظیمات: {{ .name }}" +AgentFeishuRestored: "تنظیمات قبلی بازیابی شد." +AgentFeishuProbeWarning: "بررسی اتصال Feishu ناموفق بود. شبکه و اطلاعات ورود را بررسی کنید؛ تغییر حفظ شد." +AgentFeishuVerifyMessages: "فعال‌سازی محلی انجام شد. جفت‌سازی، پیام خصوصی و گروهی، مسیریابی ربات و پاسخ جریانی را در Feishu بررسی کنید." + +ErrFeishuUpgradeConfirm: "گزینه‌های انتقال Feishu را در صفحه ارتقا تأیید کنید." + +ErrFeishuUpgradeBackup: "ارتقای سازگاری Feishu به پشتیبان کامل برنامه نیاز دارد." + +ErrFeishuUpgradeMounts: "این اتصال‌های ذخیره‌سازی کاملاً قابل پشتیبان‌گیری و بازیابی نیستند. از مسیرهای استاندارد داده در پوشه برنامه استفاده کنید." + +AgentFeishuRecoveryDisabled: "دروازه بازیابی شد و Feishu غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد." diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index fd0b6dfea..127078b53 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "仮想マシンのスナップショット [{{ .snapshot ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "続行前に互換性のない設定を修正してください:{{ .detail }}" +ErrFeishuConfigChanged: "切り替え中に設定が変更されました。再読み込みして再試行してください。" +ErrFeishuImageRequired: "Feishu を内蔵したイメージを使用してください。" +ErrFeishuAlreadySwitched: "Feishu は組み込みプラグインに切り替え済みです。" +ErrFeishuSwitchRequired: "先に Feishu チャネル画面で組み込みプラグインに切り替えてください。" +ErrFeishuManaged: "Feishu はチャネル画面で管理してください。プラグインはイメージとともに更新されます。" +ErrFeishuRestore: "Feishu 設定の復元に失敗しました:{{ .err }}" +ErrFeishuActivation: "組み込み Feishu プラグインまたはゲートウェイを起動できませんでした。" +AgentFeishuSwitch: "Feishu プラグインの切り替え" +AgentFeishuBackup: "設定のバックアップ:{{ .name }}" +AgentFeishuRestored: "以前の設定を復元しました。" +AgentFeishuProbeWarning: "Feishu の接続確認に失敗しました。ネットワークと認証情報を確認してください。切り替え結果は保持しています。" +AgentFeishuVerifyMessages: "ローカルの有効化が完了しました。Feishu でペアリング、個別・グループチャット、ボットルーティング、ストリーミングを確認してください。" + +ErrFeishuUpgradeConfirm: "更新画面で Feishu の切り替え設定を確認してください。" + +ErrFeishuUpgradeBackup: "Feishu 互換更新にはアプリ全体のバックアップが必要です。" + +ErrFeishuUpgradeMounts: "現在のマウントは完全なバックアップ・復元に対応しません。アプリディレクトリ内の標準データマウントを使用してください。" + +AgentFeishuRecoveryDisabled: "ゲートウェイを復旧しました。Feishu は無効のままで、元の設定のバックアップは保持しています。" diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 5c171955f..68706ecaa 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "가상 머신 스냅샷 [{{ .snapshot }}] 삭제" ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "계속하기 전에 호환되지 않는 설정을 수정하세요: {{ .detail }}" +ErrFeishuConfigChanged: "전환 중 설정이 변경되었습니다. 새로고침 후 다시 시도하세요." +ErrFeishuImageRequired: "Feishu가 내장된 이미지를 사용하세요." +ErrFeishuAlreadySwitched: "이미 내장 Feishu 플러그인으로 전환했습니다." +ErrFeishuSwitchRequired: "먼저 Feishu 채널 페이지에서 내장 플러그인으로 전환하세요." +ErrFeishuManaged: "Feishu 채널 페이지에서 관리하세요. 플러그인은 이미지와 함께 업데이트됩니다." +ErrFeishuRestore: "Feishu 설정 복구 실패: {{ .err }}" +ErrFeishuActivation: "내장 Feishu 플러그인 또는 게이트웨이 시작에 실패했습니다." +AgentFeishuSwitch: "Feishu 플러그인 전환" +AgentFeishuBackup: "설정 백업: {{ .name }}" +AgentFeishuRestored: "이전 설정을 복구했습니다." +AgentFeishuProbeWarning: "Feishu 연결 확인에 실패했습니다. 네트워크와 자격 증명을 확인하세요. 전환 결과는 유지됩니다." +AgentFeishuVerifyMessages: "로컬 활성화가 완료되었습니다. Feishu에서 페어링, 개인 및 그룹 채팅, 봇 라우팅과 스트리밍을 확인하세요." + +ErrFeishuUpgradeConfirm: "업그레이드 화면에서 Feishu 전환 옵션을 확인하세요." + +ErrFeishuUpgradeBackup: "Feishu 호환 업그레이드에는 전체 앱 백업이 필요합니다." + +ErrFeishuUpgradeMounts: "현재 마운트는 전체 백업 및 복원을 지원하지 않습니다. 앱 디렉터리 내부의 표준 데이터 마운트를 사용하세요." + +AgentFeishuRecoveryDisabled: "게이트웨이가 복구되었습니다. Feishu는 비활성 상태이며 원본 설정 백업은 유지됩니다." diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index b2c23b6dd..8c8cdf978 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -768,3 +768,26 @@ VMSnapshotDeleteStep: "ລຶບສະແນັບຊັອດເຄື່ອງ ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "ກະລຸນາແກ້ໄຂການຕັ້ງຄ່າທີ່ບໍ່ເຂົ້າກັນກ່ອນ: {{ .detail }}" +ErrFeishuConfigChanged: "ການຕັ້ງຄ່າປ່ຽນໃນລະຫວ່າງການສະຫຼັບ. ໂຫຼດໃໝ່ແລ້ວລອງອີກ." +ErrFeishuImageRequired: "ໃຊ້ອິມເມຈທີ່ມີ Feishu ໃນຕົວ." +ErrFeishuAlreadySwitched: "Feishu ໄດ້ສະຫຼັບເປັນປລັກອິນໃນຕົວແລ້ວ." +ErrFeishuSwitchRequired: "ກະລຸນາສະຫຼັບໃນໜ້າຊ່ອງ Feishu ກ່ອນ." +ErrFeishuManaged: "ຈັດການ Feishu ໃນໜ້າຊ່ອງ; ປລັກອິນອັບເດດພ້ອມອິມເມຈ." +ErrFeishuRestore: "ກູ້ຄືນການຕັ້ງຄ່າ Feishu ບໍ່ສຳເລັດ: {{ .err }}" +ErrFeishuActivation: "ປລັກອິນ Feishu ໃນຕົວ ຫຼື ເກດເວເລີ່ມບໍ່ສຳເລັດ." +AgentFeishuSwitch: "ສະຫຼັບປລັກອິນ Feishu" +AgentFeishuBackup: "ສຳຮອງການຕັ້ງຄ່າ: {{ .name }}" +AgentFeishuRestored: "ກູ້ຄືນການຕັ້ງຄ່າເດີມແລ້ວ." +AgentFeishuProbeWarning: "ກວດການເຊື່ອມຕໍ່ Feishu ບໍ່ຜ່ານ. ກວດເຄືອຂ່າຍ ແລະ ຂໍ້ມູນເຂົ້າໃຊ້; ຮັກສາຜົນການສະຫຼັບໄວ້." +AgentFeishuVerifyMessages: "ເປີດໃຊ້ພາຍໃນສຳເລັດ. ກວດການຈັບຄູ່, ແຊັດສ່ວນຕົວ, ກຸ່ມ, ເສັ້ນທາງບອດ ແລະ ສະຕຣີມໃນ Feishu." + +ErrFeishuUpgradeConfirm: "ຢືນຢັນຕົວເລືອກການປ່ຽນ Feishu ໃນໜ້າອັບເກຣດ." + +ErrFeishuUpgradeBackup: "ການອັບເກຣດ Feishu ຕ້ອງສຳຮອງແອັບທັງໝົດ." + +ErrFeishuUpgradeMounts: "ຈຸດເຊື່ອມຂໍ້ມູນນີ້ບໍ່ສາມາດສຳຮອງແລະກູ້ຄືນທັງໝົດໄດ້. ໃຊ້ຈຸດເຊື່ອມມາດຕະຖານພາຍໃນໂຟນເດີແອັບ." + +AgentFeishuRecoveryDisabled: "ເກດເວກູ້ຄືນແລ້ວ ແຕ່ Feishu ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້." diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 54410f746..ebb057ab1 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "Padam syot kilat mesin maya [{{ .snapshot }}]" ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "Betulkan konfigurasi tidak serasi sebelum meneruskan: {{ .detail }}" +ErrFeishuConfigChanged: "Konfigurasi berubah semasa pertukaran. Muat semula dan cuba lagi." +ErrFeishuImageRequired: "Gunakan imej yang mengandungi Feishu terbina dalam." +ErrFeishuAlreadySwitched: "Feishu sudah menggunakan pemalam terbina dalam." +ErrFeishuSwitchRequired: "Tukar dahulu kepada pemalam terbina dalam pada halaman saluran Feishu." +ErrFeishuManaged: "Urus Feishu pada halaman salurannya; pemalam dikemas kini bersama imej." +ErrFeishuRestore: "Gagal memulihkan konfigurasi Feishu: {{ .err }}" +ErrFeishuActivation: "Pemalam Feishu terbina dalam atau gerbang gagal dimulakan." +AgentFeishuSwitch: "Tukar pemalam Feishu" +AgentFeishuBackup: "Sandaran konfigurasi: {{ .name }}" +AgentFeishuRestored: "Konfigurasi sebelumnya dipulihkan." +AgentFeishuProbeWarning: "Semakan sambungan Feishu gagal. Semak rangkaian dan kelayakan; pertukaran dikekalkan." +AgentFeishuVerifyMessages: "Pengaktifan setempat selesai. Sahkan pasangan, mesej peribadi, kumpulan, penghalaan bot dan penstriman dalam Feishu." + +ErrFeishuUpgradeConfirm: "Sahkan pilihan peralihan Feishu pada halaman naik taraf." + +ErrFeishuUpgradeBackup: "Naik taraf keserasian Feishu memerlukan sandaran aplikasi penuh." + +ErrFeishuUpgradeMounts: "Lekapan ini tidak boleh disandar dan dipulihkan sepenuhnya. Gunakan lekapan data standard dalam direktori aplikasi." + +AgentFeishuRecoveryDisabled: "Gerbang dipulihkan dengan Feishu dinyahaktifkan; sandaran konfigurasi asal dikekalkan." diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 52127cf9d..680c79373 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "Excluir snapshot da máquina virtual [{{ .snapshot }}]" ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "Corrija a configuração incompatível antes de continuar: {{ .detail }}" +ErrFeishuConfigChanged: "A configuração mudou durante a troca. Recarregue e tente novamente." +ErrFeishuImageRequired: "Use uma imagem com Feishu integrado." +ErrFeishuAlreadySwitched: "O Feishu já usa o plugin integrado." +ErrFeishuSwitchRequired: "Primeiro troque para o plugin integrado na página do canal Feishu." +ErrFeishuManaged: "Gerencie o Feishu pela página do canal; o plugin é atualizado com a imagem." +ErrFeishuRestore: "Falha ao restaurar a configuração do Feishu: {{ .err }}" +ErrFeishuActivation: "Falha ao iniciar o plugin Feishu integrado ou o gateway." +AgentFeishuSwitch: "Trocar plugin Feishu" +AgentFeishuBackup: "Backup da configuração: {{ .name }}" +AgentFeishuRestored: "Configuração anterior restaurada." +AgentFeishuProbeWarning: "Falha na verificação de conexão do Feishu. Verifique a rede e as credenciais; a troca foi mantida." +AgentFeishuVerifyMessages: "Ativação local concluída. Verifique pareamento, conversas, grupos, rotas de bots e streaming no Feishu." + +ErrFeishuUpgradeConfirm: "Confirme as opções de transição do Feishu na página de atualização." + +ErrFeishuUpgradeBackup: "A atualização de compatibilidade do Feishu exige backup completo do aplicativo." + +ErrFeishuUpgradeMounts: "Estas montagens não permitem backup e restauração completos. Use montagens de dados padrão dentro do diretório do aplicativo." + +AgentFeishuRecoveryDisabled: "Gateway recuperado com Feishu desativado; o backup da configuração original foi mantido." diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 424b100c6..9dd2c58ed 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "Удалить снимок виртуальной маш ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "Исправьте несовместимые настройки перед продолжением: {{ .detail }}" +ErrFeishuConfigChanged: "Настройки изменились во время переключения. Обновите страницу и повторите." +ErrFeishuImageRequired: "Используйте образ со встроенным Feishu." +ErrFeishuAlreadySwitched: "Feishu уже переключён на встроенный плагин." +ErrFeishuSwitchRequired: "Сначала перейдите на встроенный плагин на странице канала Feishu." +ErrFeishuManaged: "Управляйте Feishu на странице канала; плагин обновляется с образом." +ErrFeishuRestore: "Не удалось восстановить настройки Feishu: {{ .err }}" +ErrFeishuActivation: "Не удалось запустить встроенный плагин Feishu или шлюз." +AgentFeishuSwitch: "Переключить плагин Feishu" +AgentFeishuBackup: "Резервная копия настроек: {{ .name }}" +AgentFeishuRestored: "Прежние настройки восстановлены." +AgentFeishuProbeWarning: "Проверка подключения Feishu не пройдена. Проверьте сеть и учётные данные; переключение сохранено." +AgentFeishuVerifyMessages: "Локальная активация завершена. Проверьте сопряжение, личные и групповые сообщения, маршрутизацию ботов и потоковые ответы в Feishu." + +ErrFeishuUpgradeConfirm: "Подтвердите параметры перехода Feishu на странице обновления." + +ErrFeishuUpgradeBackup: "Для обновления совместимости Feishu нужна полная резервная копия приложения." + +ErrFeishuUpgradeMounts: "Эти монтирования не поддерживают полное резервное копирование и восстановление. Используйте стандартные данные внутри каталога приложения." + +AgentFeishuRecoveryDisabled: "Шлюз восстановлен, Feishu отключён; исходная резервная копия конфигурации сохранена." diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 73dbcc12e..fd16d7cda 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "Sanal makine anlık görüntüsü [{{ .snapshot }}] sil" ErrCAUpload: "Upload a ZIP (up to 10 MB) containing a valid self-signed CA certificate ca.crt and its matching private key ca.key, with a supported RSA or EC algorithm." ErrCAImport: "Provide a valid self-signed root CA certificate and matching private key (up to 1 MB each), using a supported RSA or EC algorithm." + +# Built-in Feishu +ErrFeishuConfig: "Devam etmeden önce uyumsuz ayarları düzeltin: {{ .detail }}" +ErrFeishuConfigChanged: "Geçiş sırasında ayarlar değişti. Yenileyip tekrar deneyin." +ErrFeishuImageRequired: "Yerleşik Feishu içeren bir imaj kullanın." +ErrFeishuAlreadySwitched: "Feishu zaten yerleşik eklentiye geçti." +ErrFeishuSwitchRequired: "Önce Feishu kanal sayfasından yerleşik eklentiye geçin." +ErrFeishuManaged: "Feishu kanal sayfasından yönetin; eklenti imajla güncellenir." +ErrFeishuRestore: "Feishu ayarları geri yüklenemedi: {{ .err }}" +ErrFeishuActivation: "Yerleşik Feishu eklentisi veya ağ geçidi başlatılamadı." +AgentFeishuSwitch: "Feishu eklentisini değiştir" +AgentFeishuBackup: "Ayar yedeği: {{ .name }}" +AgentFeishuRestored: "Önceki ayarlar geri yüklendi." +AgentFeishuProbeWarning: "Feishu bağlantı kontrolü başarısız. Ağı ve kimlik bilgilerini kontrol edin; geçiş korundu." +AgentFeishuVerifyMessages: "Yerel etkinleştirme tamamlandı. Feishu üzerinden eşleştirmeyi, özel ve grup mesajlarını, bot yönlendirmesini ve akışı doğrulayın." + +ErrFeishuUpgradeConfirm: "Yükseltme sayfasında Feishu geçiş seçeneklerini onaylayın." + +ErrFeishuUpgradeBackup: "Feishu uyumluluk yükseltmesi tam uygulama yedeği gerektirir." + +ErrFeishuUpgradeMounts: "Bu bağlamalar tam yedeklenip geri yüklenemiyor. Uygulama dizinindeki standart veri bağlamalarını kullanın." + +AgentFeishuRecoveryDisabled: "Ağ geçidi kurtarıldı, Feishu kapalı; özgün yapılandırma yedeği korundu." diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 9746d9909..8fdb70743 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "刪除虛擬機快照 [{{ .snapshot }}]" ErrCAUpload: "請上傳包含有效自簽根憑證 ca.crt 及匹配私鑰 ca.key 的 ZIP 檔案(最大 10 MB),金鑰演算法須為支援的 RSA 或 EC。" ErrCAImport: "請提供有效的自簽根憑證及匹配私鑰(每個最大 1 MB),金鑰演算法須為支援的 RSA 或 EC。" + +# Built-in Feishu +ErrFeishuConfig: "請先處理不相容的設定:{{ .detail }}" +ErrFeishuConfigChanged: "切換期間設定已變更,請重新整理後重試。" +ErrFeishuImageRequired: "請使用包含內建飛書外掛的映像。" +ErrFeishuAlreadySwitched: "飛書已切換至內建外掛。" +ErrFeishuSwitchRequired: "請先在飛書管道頁面切換至內建外掛。" +ErrFeishuManaged: "請在飛書管道頁面管理,外掛隨映像更新。" +ErrFeishuRestore: "還原飛書設定失敗:{{ .err }}" +ErrFeishuActivation: "內建飛書外掛或閘道啟動失敗。" +AgentFeishuSwitch: "切換飛書外掛" +AgentFeishuBackup: "設定備份:{{ .name }}" +AgentFeishuRestored: "已還原切換前的設定。" +AgentFeishuProbeWarning: "飛書連線檢查未通過,請檢查網路和憑證;已保留切換結果。" +AgentFeishuVerifyMessages: "本機啟用完成,請在飛書驗證配對、私聊、群聊、機器人路由和串流回覆。" + +ErrFeishuUpgradeConfirm: "請在升級頁面確認飛書切換選項後重試。" + +ErrFeishuUpgradeBackup: "飛書相容升級必須開啟完整應用程式備份。" + +ErrFeishuUpgradeMounts: "目前掛載無法完整備份或還原,請使用應用程式目錄內的標準資料掛載。" + +AgentFeishuRecoveryDisabled: "閘道已恢復,飛書保持停用;原設定備份已保留。" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 3f1a457a8..2e1504ea1 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -777,3 +777,26 @@ VMSnapshotDeleteStep: "删除虚拟机快照 [{{ .snapshot }}]" ErrCAUpload: "请上传包含有效自签根证书 ca.crt 及匹配私钥 ca.key 的 ZIP 文件(最大 10 MB),密钥算法须为支持的 RSA 或 EC。" ErrCAImport: "请提供有效的自签根证书及匹配私钥(每个最大 1 MB),密钥算法须为支持的 RSA 或 EC。" + +# Built-in Feishu +ErrFeishuConfig: "请先处理不兼容的配置:{{ .detail }}" +ErrFeishuConfigChanged: "切换期间配置已改变,请刷新后重试。" +ErrFeishuImageRequired: "请使用包含内置飞书插件的镜像。" +ErrFeishuAlreadySwitched: "飞书已切换到内置插件。" +ErrFeishuSwitchRequired: "请先在飞书渠道页面切换到内置插件。" +ErrFeishuManaged: "请在飞书渠道页面管理,插件随镜像更新。" +ErrFeishuRestore: "恢复飞书配置失败:{{ .err }}" +ErrFeishuActivation: "内置飞书插件或网关启动失败。" +AgentFeishuSwitch: "切换飞书插件" +AgentFeishuBackup: "配置备份:{{ .name }}" +AgentFeishuRestored: "已恢复切换前的配置。" +AgentFeishuProbeWarning: "飞书连接检查未通过,请检查网络和凭据;已保留切换结果。" +AgentFeishuVerifyMessages: "本地启用完成,请在飞书中验证配对、私聊、群聊、机器人路由和流式回复。" + +ErrFeishuUpgradeConfirm: "请在升级页面确认飞书切换选项后重试。" + +ErrFeishuUpgradeBackup: "飞书兼容升级必须开启完整应用备份。" + +ErrFeishuUpgradeMounts: "当前挂载无法完整备份或恢复,请使用应用目录内的标准数据挂载后重试。" + +AgentFeishuRecoveryDisabled: "网关已恢复,飞书保持停用;原配置备份已保留。" diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 600bbb6ff..f66485d48 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -91,6 +91,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/agents/agent/md/update", baseApi.UpdateAgentRoleMarkdownFile) aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig) aiToolsRouter.POST("/agents/channel/feishu/update", baseApi.UpdateAgentFeishuConfig) + aiToolsRouter.POST("/agents/channel/feishu/switch", baseApi.SwitchAgentFeishuPlugin) aiToolsRouter.POST("/agents/channel/telegram/get", baseApi.GetAgentTelegramConfig) aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig) aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig) diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index 2f30213f2..1df02a9c7 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -1171,6 +1171,41 @@ const docTemplate = `{ ] } }, + "/ai/agents/channel/feishu/switch": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentFeishuSwitchReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Switch to the bundled OpenClaw Feishu plugin", + "tags": [ + "AI" + ] + } + }, "/ai/agents/channel/feishu/update": { "post": { "consumes": [ @@ -30117,6 +30152,33 @@ const docTemplate = `{ } }, "definitions": { + "dto.AgentFeishuTransition": { + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "streaming": { + "type": "boolean" + }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, + "requireMention": { + "type": "string", + "enum": [ + "true", + "false" + ] + } + } + }, "dto.AgentAccountCreateReq": { "properties": { "apiKey": { @@ -31215,12 +31277,18 @@ const docTemplate = `{ }, "dto.AgentFeishuConfig": { "properties": { + "builtinManaged": { + "type": "boolean" + }, "bots": { "items": { "$ref": "#/definitions/dto.AgentFeishuBot" }, "type": "array" }, + "builtinAvailable": { + "type": "boolean" + }, "connectionMode": { "type": "string" }, @@ -31239,9 +31307,21 @@ const docTemplate = `{ "groupPolicy": { "type": "string" }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, "installed": { "type": "boolean" }, + "legacy": { + "type": "boolean" + }, "replyMode": { "type": "string" }, @@ -31253,10 +31333,49 @@ const docTemplate = `{ }, "threadSession": { "type": "boolean" + }, + "legacyConfigured": { + "type": "boolean" } }, "type": "object" }, + "dto.AgentFeishuSwitchReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, + "requireMention": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "streaming": { + "type": "boolean" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "agentId", + "taskID", + "groupSessionScope", + "requireMention" + ], + "type": "object" + }, "dto.AgentFeishuConfigReq": { "properties": { "agentId": { @@ -31303,6 +31422,15 @@ const docTemplate = `{ ], "type": "string" }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, "replyMode": { "type": "string" }, @@ -33167,6 +33295,12 @@ const docTemplate = `{ }, "version": { "type": "string" + }, + "feishuLegacy": { + "type": "boolean" + }, + "feishuConfigured": { + "type": "boolean" } }, "type": "object" @@ -42953,6 +43087,9 @@ const docTemplate = `{ }, "taskID": { "type": "string" + }, + "feishuTransition": { + "$ref": "#/definitions/dto.AgentFeishuTransition" } }, "required": [ diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index b36d6efd0..8201d51a5 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -1167,6 +1167,41 @@ ] } }, + "/ai/agents/channel/feishu/switch": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentFeishuSwitchReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Switch to the bundled OpenClaw Feishu plugin", + "tags": [ + "AI" + ] + } + }, "/ai/agents/channel/feishu/update": { "post": { "consumes": [ @@ -30113,6 +30148,33 @@ } }, "definitions": { + "dto.AgentFeishuTransition": { + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "streaming": { + "type": "boolean" + }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, + "requireMention": { + "type": "string", + "enum": [ + "true", + "false" + ] + } + } + }, "dto.AgentAccountCreateReq": { "properties": { "apiKey": { @@ -31211,12 +31273,18 @@ }, "dto.AgentFeishuConfig": { "properties": { + "builtinManaged": { + "type": "boolean" + }, "bots": { "items": { "$ref": "#/definitions/dto.AgentFeishuBot" }, "type": "array" }, + "builtinAvailable": { + "type": "boolean" + }, "connectionMode": { "type": "string" }, @@ -31235,9 +31303,21 @@ "groupPolicy": { "type": "string" }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, "installed": { "type": "boolean" }, + "legacy": { + "type": "boolean" + }, "replyMode": { "type": "string" }, @@ -31249,10 +31329,49 @@ }, "threadSession": { "type": "boolean" + }, + "legacyConfigured": { + "type": "boolean" } }, "type": "object" }, + "dto.AgentFeishuSwitchReq": { + "properties": { + "agentId": { + "type": "integer" + }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, + "requireMention": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "streaming": { + "type": "boolean" + }, + "taskID": { + "type": "string" + } + }, + "required": [ + "agentId", + "taskID", + "groupSessionScope", + "requireMention" + ], + "type": "object" + }, "dto.AgentFeishuConfigReq": { "properties": { "agentId": { @@ -31299,6 +31418,15 @@ ], "type": "string" }, + "groupSessionScope": { + "type": "string", + "enum": [ + "group", + "group_sender", + "group_topic", + "group_topic_sender" + ] + }, "replyMode": { "type": "string" }, @@ -33163,6 +33291,12 @@ }, "version": { "type": "string" + }, + "feishuLegacy": { + "type": "boolean" + }, + "feishuConfigured": { + "type": "boolean" } }, "type": "object" @@ -42949,6 +43083,9 @@ }, "taskID": { "type": "string" + }, + "feishuTransition": { + "$ref": "#/definitions/dto.AgentFeishuTransition" } }, "required": [ diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index e5d30df35..49f6a4e11 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -779,6 +779,11 @@ export namespace AI { } export interface AgentFeishuConfig { + builtinManaged?: boolean; + legacyConfigured?: boolean; + groupSessionScope?: string; + builtinAvailable?: boolean; + legacy?: boolean; enabled: boolean; threadSession: boolean; replyMode: string; @@ -793,6 +798,7 @@ export namespace AI { } export interface AgentFeishuConfigUpdateReq { + groupSessionScope?: string; agentId: number; enabled: boolean; threadSession: boolean; @@ -806,6 +812,21 @@ export namespace AI { bots: AgentFeishuBot[]; } + export interface AgentFeishuTransition { + confirmed: boolean; + streaming: boolean; + groupSessionScope: string; + requireMention: string; + } + + export interface AgentFeishuSwitchReq { + agentId: number; + taskID: string; + streaming: boolean; + groupSessionScope: string; + requireMention: 'true' | 'false'; + } + export interface AgentTelegramConfigReq { agentId: number; } diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index a0ca8c3d5..03de17f32 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -1,3 +1,4 @@ +import { AI } from './ai'; import { ReqPage, CommonModel } from '.'; export namespace App { @@ -260,6 +261,7 @@ export namespace App { } export interface AppInstalledOp { + feishuTransition?: AI.AgentFeishuTransition; installId: number; operate: string; backupId?: number; @@ -285,6 +287,8 @@ export namespace App { } export interface VersionDetail { + feishuLegacy?: boolean; + feishuConfigured?: boolean; version: string; detailId: number; } diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index 40a16d8d8..3b550661f 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -272,6 +272,10 @@ export const getAgentFeishuConfig = (req: AI.AgentFeishuConfigReq) => { return http.post(`/ai/agents/channel/feishu/get`, req); }; +export const switchAgentFeishuPlugin = (req: AI.AgentFeishuSwitchReq) => { + return http.post(`/ai/agents/channel/feishu/switch`, req); +}; + export const updateAgentFeishuConfig = (req: AI.AgentFeishuConfigUpdateReq) => { return http.post(`/ai/agents/channel/feishu/update`, req); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 98335b5db..d1f1f780c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -774,6 +774,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'Legacy Feishu must be handled before upgrading. A full application backup is required; failure restores the old image and matching data. An unconfigured legacy plugin is only disabled.', + feishuRecoveryNotice: + 'If switching fails, recovery will attempt to restore the Gateway with Feishu disabled. The legacy plugin will not be re-enabled. The original config backup is retained.', + feishuRecover: 'Disable legacy plugin and recover startup', + feishuRecoverConfirm: + 'Back up the config, disable the unconfigured legacy Feishu plugin and restart this instance? All channels will be briefly interrupted. Plugin files are retained.', + feishuBuiltin: 'Built-in Feishu', + feishuImageRequired: 'Use an image containing bundled Feishu.', + feishuLegacy: + 'The legacy plugin is no longer maintained. Review the session, streaming and mention settings below before switching.', + feishuBuiltinHint: 'This plugin is updated with the OpenClaw image.', + feishuSessionTopic: 'Separate by group topic', + feishuSessionTopicSender: 'Separate by group topic and sender', + feishuSwitch: 'Switch to built-in plugin', + feishuMentionChoice: 'Choose explicitly whether a mention is required before switching.', + feishuSwitchConfirm: + 'Switch to bundled Feishu and restart this instance? All channels will be briefly interrupted. Legacy-only tools become unavailable; streaming and topic behavior follow your choices, and some chats may start new sessions. Credentials, bindings and pairing storage are retained.', + deepSeekAccessHostHelper: "Enter the IPv4 address or hostname used in your browser, without a scheme, port or path. HTTPS uses the application's local certificate.", deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 51c2169ea..0685b1640 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -777,6 +777,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'Antes de actualizar se debe tratar el complemento Feishu antiguo. Se requiere una copia completa; si falla, se restauran la imagen anterior y sus datos. Un complemento sin configurar solo se desactiva.', + feishuRecoveryNotice: + 'Si el cambio falla, se intentará recuperar la puerta de enlace con Feishu desactivado. No se reactivará el complemento antiguo. Se conserva la copia de configuración.', + feishuRecover: 'Desactivar complemento antiguo y recuperar inicio', + feishuRecoverConfirm: + '¿Guardar la configuración, desactivar el complemento Feishu antiguo sin configurar y reiniciar? Todos los canales se interrumpirán brevemente. Se conservan los archivos del complemento.', + feishuBuiltin: 'Feishu integrado', + feishuImageRequired: 'Use una imagen que incluya Feishu.', + feishuLegacy: + 'El complemento anterior ya no se mantiene. Revise las sesiones, la transmisión y las menciones antes de cambiar.', + feishuBuiltinHint: 'Este complemento se actualiza con la imagen de OpenClaw.', + feishuSessionTopic: 'Separar por tema del grupo', + feishuSessionTopicSender: 'Separar por tema del grupo y remitente', + feishuSwitch: 'Cambiar al complemento integrado', + feishuMentionChoice: 'Elija explícitamente si se requiere una mención antes de cambiar.', + feishuSwitchConfirm: + '¿Cambiar a Feishu integrado y reiniciar? Todos los canales se interrumpirán brevemente. Las herramientas exclusivas del complemento antiguo dejarán de funcionar; la transmisión y los temas seguirán sus opciones y algunas conversaciones podrán iniciar sesiones nuevas. Se conservan credenciales, vínculos y emparejamientos.', + deepSeekAccessHostHelper: 'Introduzca la IPv4 o el nombre de host usado en el navegador, sin protocolo, puerto ni ruta. HTTPS usa el certificado local de la aplicación.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index da67a9e25..59d6c03b4 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -769,6 +769,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'پیش از ارتقا باید افزونه قدیمی Feishu بررسی شود. پشتیبان کامل برنامه لازم است؛ در صورت شکست، تصویر قبلی و داده‌های مربوط بازیابی می‌شوند. افزونه پیکربندی‌نشده فقط غیرفعال می‌شود.', + feishuRecoveryNotice: + 'در صورت شکست تغییر، بازیابی دروازه با Feishu غیرفعال انجام می‌شود. افزونه قدیمی دوباره فعال نخواهد شد. پشتیبان تنظیمات اصلی نگه داشته می‌شود.', + feishuRecover: 'غیرفعال کردن افزونه قدیمی و بازیابی راه‌اندازی', + feishuRecoverConfirm: + 'از تنظیمات پشتیبان گرفته شود، افزونه قدیمی و پیکربندی‌نشده Feishu غیرفعال و نمونه دوباره راه‌اندازی شود؟ همه کانال‌ها موقتاً قطع می‌شوند. فایل‌های افزونه حفظ می‌شوند.', + feishuBuiltin: 'Feishu داخلی', + feishuImageRequired: 'از تصویری با Feishu داخلی استفاده کنید.', + feishuLegacy: + 'افزونه قدیمی دیگر نگهداری نمی‌شود. پیش از تغییر، تنظیمات نشست، جریان پاسخ و اشاره را بررسی کنید.', + feishuBuiltinHint: 'این افزونه همراه تصویر OpenClaw به‌روز می‌شود.', + feishuSessionTopic: 'تفکیک بر اساس موضوع گروه', + feishuSessionTopicSender: 'تفکیک بر اساس موضوع گروه و فرستنده', + feishuSwitch: 'تغییر به افزونه داخلی', + feishuMentionChoice: 'پیش از تغییر مشخص کنید آیا اشاره لازم است.', + feishuSwitchConfirm: + 'به Feishu داخلی تغییر داده و دوباره راه‌اندازی شود؟ همه کانال‌ها موقتاً قطع می‌شوند. ابزارهای ویژه افزونه قدیمی در دسترس نخواهند بود؛ جریان پاسخ و موضوع‌ها طبق انتخاب شما عمل می‌کنند و بعضی گفتگوها ممکن است نشست جدید بسازند. اعتبارنامه‌ها، اتصال‌ها و اطلاعات جفت‌سازی حفظ می‌شوند.', + deepSeekAccessHostHelper: 'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 694729045..410e93c98 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -773,6 +773,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + '更新前に旧 Feishu プラグインを処理します。アプリ全体のバックアップが必要です。失敗時は旧イメージと対応するデータを復元します。未設定の旧プラグインは無効化のみ行います。', + feishuRecoveryNotice: + '切り替え失敗時は Feishu を無効にした状態でゲートウェイの復旧を試みます。旧プラグインは再有効化しません。元の設定のバックアップは保持します。', + feishuRecover: '旧プラグインを無効化して起動を復旧', + feishuRecoverConfirm: + '設定をバックアップし、未設定の旧 Feishu プラグインを無効化して再起動しますか?全チャネルが一時中断されます。プラグインのファイルは保持します。', + feishuBuiltin: '組み込み Feishu', + feishuImageRequired: 'Feishu を内蔵したイメージを使用してください。', + feishuLegacy: + '旧プラグインの保守は終了しました。切り替え前にセッション分離、ストリーミング、メンション設定を確認してください。', + feishuBuiltinHint: 'このプラグインは OpenClaw イメージとともに更新されます。', + feishuSessionTopic: 'グループのトピック別', + feishuSessionTopicSender: 'グループのトピックと送信者別', + feishuSwitch: '組み込みプラグインに切り替え', + feishuMentionChoice: '切り替え前にメンションが必要か選択してください。', + feishuSwitchConfirm: + '内蔵 Feishu に切り替えて再起動しますか?全チャネルが一時中断され、旧プラグイン専用ツールは利用できなくなります。ストリーミングとトピックは選択した設定に従い、一部の会話は新しいセッションになります。認証情報、紐付け、ペアリング情報は保持します。', + deepSeekAccessHostHelper: 'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index c58c88b41..58be9b425 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -764,6 +764,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + '업그레이드 전에 기존 Feishu 플러그인을 처리합니다. 전체 앱 백업이 필요하며 실패하면 이전 이미지와 해당 데이터를 복원합니다. 미설정 플러그인은 비활성화만 합니다.', + feishuRecoveryNotice: + '전환에 실패하면 Feishu를 비활성화한 상태로 게이트웨이 복구를 시도합니다. 기존 플러그인은 다시 활성화하지 않으며 원본 설정 백업을 보관합니다.', + feishuRecover: '기존 플러그인 비활성화 및 시작 복구', + feishuRecoverConfirm: + '설정을 백업하고 미설정 Feishu 플러그인을 비활성화한 뒤 다시 시작할까요? 모든 채널이 잠시 중단됩니다. 플러그인 파일은 유지합니다.', + feishuBuiltin: '내장 Feishu', + feishuImageRequired: 'Feishu가 내장된 이미지를 사용하세요.', + feishuLegacy: + '기존 플러그인의 유지 관리가 종료되었습니다. 전환 전에 세션 분리, 스트리밍 및 멘션 설정을 확인하세요.', + feishuBuiltinHint: '이 플러그인은 OpenClaw 이미지와 함께 업데이트됩니다.', + feishuSessionTopic: '그룹 주제별 분리', + feishuSessionTopicSender: '그룹 주제 및 발신자별 분리', + feishuSwitch: '내장 플러그인으로 전환', + feishuMentionChoice: '전환 전에 멘션 필요 여부를 명시적으로 선택하세요.', + feishuSwitchConfirm: + '내장 Feishu로 전환하고 다시 시작할까요? 모든 채널이 잠시 중단되고 기존 전용 도구는 사용할 수 없습니다. 스트리밍과 주제 동작은 선택한 설정을 따르며 일부 대화는 새 세션으로 시작됩니다. 자격 증명, 연결 및 페어링 정보는 유지됩니다.', + deepSeekAccessHostHelper: '브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 2f045f6e3..bb71e8bb6 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -762,6 +762,24 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'ຕ້ອງຈັດການປລັກອິນ Feishu ເກົ່າກ່ອນອັບເກຣດ. ຕ້ອງສຳຮອງແອັບທັງໝົດ; ຖ້າລົ້ມເຫຼວຈະກູ້ຄືນອິມເມຈເກົ່າແລະຂໍ້ມູນ. ປລັກອິນທີ່ຍັງບໍ່ຕັ້ງຄ່າຈະຖືກປິດເທົ່ານັ້ນ.', + feishuRecoveryNotice: + 'ຖ້າການປ່ຽນລົ້ມເຫຼວ ຈະພະຍາຍາມກູ້ຄືນເກດເວໂດຍປິດ Feishu. ຈະບໍ່ເປີດປລັກອິນເກົ່າຄືນ. ເກັບສຳຮອງການຕັ້ງຄ່າເດີມໄວ້.', + feishuRecover: 'ປິດປລັກອິນເກົ່າແລະກູ້ຄືນການເລີ່ມ', + feishuRecoverConfirm: + 'ສຳຮອງການຕັ້ງຄ່າ, ປິດປລັກອິນ Feishu ເກົ່າທີ່ຍັງບໍ່ຕັ້ງຄ່າ ແລະເລີ່ມໃໝ່ບໍ? ທຸກຊ່ອງທາງຈະຢຸດຊົ່ວຄາວ. ໄຟລ໌ປລັກອິນຍັງຖືກເກັບໄວ້.', + feishuBuiltin: 'Feishu ໃນຕົວ', + feishuImageRequired: 'ໃຊ້ອິມເມຈທີ່ມີ Feishu ໃນຕົວ.', + feishuLegacy: 'ປລັກອິນເກົ່າຢຸດບຳລຸງຮັກສາແລ້ວ. ກວດການແຍກເຊດຊັນ, ສະຕຣີມ ແລະ ການເອີ້ນ @ ກ່ອນສະຫຼັບ.', + feishuBuiltinHint: 'ປລັກອິນນີ້ອັບເດດພ້ອມອິມເມຈ OpenClaw.', + feishuSessionTopic: 'ແຍກຕາມຫົວຂໍ້ກຸ່ມ', + feishuSessionTopicSender: 'ແຍກຕາມຫົວຂໍ້ກຸ່ມ ແລະ ຜູ້ສົ່ງ', + feishuSwitch: 'ສະຫຼັບເປັນປລັກອິນໃນຕົວ', + feishuMentionChoice: 'ກ່ອນສະຫຼັບ ກະລຸນາເລືອກວ່າຕ້ອງການ @ ຫຼືບໍ່.', + feishuSwitchConfirm: + 'ປ່ຽນເປັນ Feishu ໃນຕົວແລະເລີ່ມໃໝ່ບໍ? ທຸກຊ່ອງທາງຈະຢຸດຊົ່ວຄາວ. ເຄື່ອງມືສະເພາະຂອງປລັກອິນເກົ່າຈະໃຊ້ບໍ່ໄດ້; ການສົ່ງແບບສະຕຣີມແລະຫົວຂໍ້ຈະຕາມຄ່າທີ່ເລືອກ ແລະບາງການສົນທະນາອາດເລີ່ມເຊສຊັນໃໝ່. ຂໍ້ມູນຢືນຢັນ, ການຜູກ ແລະການຈັບຄູ່ຈະຖືກເກັບໄວ້.', + deepSeekAccessHostHelper: 'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 2411ca28c..137757338 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -782,6 +782,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'Pemalam Feishu lama perlu ditangani sebelum naik taraf. Sandaran aplikasi penuh diperlukan; kegagalan memulihkan imej lama dan datanya. Pemalam tanpa konfigurasi hanya dinyahaktifkan.', + feishuRecoveryNotice: + 'Jika pertukaran gagal, pemulihan gerbang akan dicuba dengan Feishu dinyahaktifkan. Pemalam lama tidak diaktifkan semula. Sandaran konfigurasi asal dikekalkan.', + feishuRecover: 'Nyahaktif pemalam lama dan pulihkan permulaan', + feishuRecoverConfirm: + 'Sandarkan konfigurasi, nyahaktifkan pemalam Feishu lama yang belum dikonfigurasi dan mulakan semula? Semua saluran terganggu seketika. Fail pemalam dikekalkan.', + feishuBuiltin: 'Feishu terbina dalam', + feishuImageRequired: 'Gunakan imej yang mengandungi Feishu terbina dalam.', + feishuLegacy: + 'Pemalam lama tidak lagi diselenggara. Semak tetapan sesi, penstriman dan sebutan sebelum bertukar.', + feishuBuiltinHint: 'Pemalam ini dikemas kini bersama imej OpenClaw.', + feishuSessionTopic: 'Asingkan mengikut topik kumpulan', + feishuSessionTopicSender: 'Asingkan mengikut topik kumpulan dan penghantar', + feishuSwitch: 'Tukar kepada pemalam terbina dalam', + feishuMentionChoice: 'Pilih dengan jelas sama ada sebutan diperlukan sebelum bertukar.', + feishuSwitchConfirm: + 'Tukar kepada Feishu terbina dalam dan mulakan semula? Semua saluran terganggu seketika. Alat khusus pemalam lama tidak lagi tersedia; penstriman dan topik mengikut pilihan anda, dan sesetengah sembang mungkin memulakan sesi baharu. Bukti kelayakan, ikatan dan pasangan dikekalkan.', + deepSeekAccessHostHelper: 'Masukkan alamat IPv4 atau nama hos yang digunakan dalam pelayar, tanpa protokol, port atau laluan. HTTPS menggunakan sijil setempat aplikasi.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 0610556c5..e058b31f0 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -775,6 +775,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'O plugin Feishu antigo deve ser tratado antes da atualização. É necessário um backup completo; em caso de falha, a imagem anterior e seus dados serão restaurados. Um plugin não configurado será apenas desativado.', + feishuRecoveryNotice: + 'Se a troca falhar, será tentada a recuperação do gateway com Feishu desativado. O plugin antigo não será reativado. O backup original será mantido.', + feishuRecover: 'Desativar plugin antigo e recuperar inicialização', + feishuRecoverConfirm: + 'Fazer backup, desativar o plugin Feishu antigo não configurado e reiniciar? Todos os canais serão interrompidos brevemente. Os arquivos do plugin serão mantidos.', + feishuBuiltin: 'Feishu integrado', + feishuImageRequired: 'Use uma imagem com Feishu integrado.', + feishuLegacy: + 'O plugin antigo não é mais mantido. Revise as sessões, o streaming e as menções antes de trocar.', + feishuBuiltinHint: 'Este plugin é atualizado com a imagem do OpenClaw.', + feishuSessionTopic: 'Separar por tópico do grupo', + feishuSessionTopicSender: 'Separar por tópico do grupo e remetente', + feishuSwitch: 'Trocar para o plugin integrado', + feishuMentionChoice: 'Escolha explicitamente se uma menção é necessária antes de trocar.', + feishuSwitchConfirm: + 'Mudar para Feishu integrado e reiniciar? Todos os canais serão interrompidos brevemente. As ferramentas exclusivas do plugin antigo deixarão de funcionar; streaming e tópicos seguirão suas escolhas e algumas conversas poderão iniciar novas sessões. Credenciais, vínculos e pareamentos serão mantidos.', + deepSeekAccessHostHelper: 'Informe o IPv4 ou nome do host usado no navegador, sem protocolo, porta ou caminho. O HTTPS usa o certificado local do aplicativo.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index f43f5b672..97cd621f3 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -773,6 +773,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'Перед обновлением необходимо обработать старый плагин Feishu. Требуется полная резервная копия; при сбое восстанавливаются прежний образ и его данные. Ненастроенный плагин только отключается.', + feishuRecoveryNotice: + 'При сбое переключения будет предпринята попытка восстановить шлюз с отключённым Feishu. Старый плагин не включается повторно. Исходная резервная копия сохраняется.', + feishuRecover: 'Отключить старый плагин и восстановить запуск', + feishuRecoverConfirm: + 'Создать резервную копию, отключить ненастроенный старый плагин Feishu и перезапустить? Все каналы будут кратковременно прерваны. Файлы плагина сохраняются.', + feishuBuiltin: 'Встроенный Feishu', + feishuImageRequired: 'Используйте образ со встроенным Feishu.', + feishuLegacy: + 'Старый плагин больше не поддерживается. Перед переключением проверьте разделение сессий, потоковые ответы и упоминания.', + feishuBuiltinHint: 'Плагин обновляется вместе с образом OpenClaw.', + feishuSessionTopic: 'Разделять по теме группы', + feishuSessionTopicSender: 'Разделять по теме группы и отправителю', + feishuSwitch: 'Перейти на встроенный плагин', + feishuMentionChoice: 'Перед переключением явно выберите, требуется ли упоминание.', + feishuSwitchConfirm: + 'Переключиться на встроенный Feishu и перезапустить? Все каналы будут кратковременно прерваны. Инструменты старого плагина станут недоступны; потоковые ответы и темы будут работать по выбранным настройкам, некоторые чаты начнут новые сеансы. Учётные данные, привязки и сопряжения сохраняются.', + deepSeekAccessHostHelper: 'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 521328ea2..0f2d9981d 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -778,6 +778,25 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + 'Yükseltmeden önce eski Feishu eklentisi ele alınmalıdır. Tam yedek gerekir; hata durumunda eski imaj ve verileri geri yüklenir. Yapılandırılmamış eklenti yalnızca devre dışı bırakılır.', + feishuRecoveryNotice: + 'Geçiş başarısız olursa Feishu kapalı tutularak ağ geçidi kurtarılmaya çalışılır. Eski eklenti yeniden etkinleştirilmez. Özgün yapılandırma yedeği korunur.', + feishuRecover: 'Eski eklentiyi kapat ve başlatmayı kurtar', + feishuRecoverConfirm: + 'Yapılandırma yedeklensin, yapılandırılmamış eski Feishu eklentisi kapatılsın ve örnek yeniden başlatılsın mı? Tüm kanallar kısa süre kesilir. Eklenti dosyaları korunur.', + feishuBuiltin: 'Yerleşik Feishu', + feishuImageRequired: 'Yerleşik Feishu içeren bir imaj kullanın.', + feishuLegacy: + 'Eski eklenti artık bakımlı değil. Geçişten önce oturum, akış ve bahsetme ayarlarını gözden geçirin.', + feishuBuiltinHint: 'Bu eklenti OpenClaw imajıyla birlikte güncellenir.', + feishuSessionTopic: 'Grup konusuna göre ayır', + feishuSessionTopicSender: 'Grup konusu ve gönderene göre ayır', + feishuSwitch: 'Yerleşik eklentiye geç', + feishuMentionChoice: 'Geçişten önce bahsetmenin gerekli olup olmadığını açıkça seçin.', + feishuSwitchConfirm: + 'Yerleşik Feishu eklentisine geçilip yeniden başlatılsın mı? Tüm kanallar kısa süre kesilir. Eski eklentiye özel araçlar kullanılamaz; akış ve konular seçimlerinize göre çalışır, bazı sohbetler yeni oturum açabilir. Kimlik bilgileri, bağlamalar ve eşleştirmeler korunur.', + deepSeekAccessHostHelper: 'Tarayıcıda kullanılan IPv4 adresini veya ana bilgisayar adını protokol, bağlantı noktası veya yol olmadan girin. HTTPS, uygulamanın yerel sertifikasını kullanır.', deepSeekAuthHelper: diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index ac9c3c350..a4c89852a 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -740,6 +740,23 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + '升級前需要處理舊飛書外掛。此次升級必須完整備份;失敗時還原原映像及配套資料。未設定飛書時僅停用舊外掛。', + feishuRecoveryNotice: '切換失敗時將嘗試恢復閘道,但飛書保持停用,不會重新啟用舊外掛。原設定備份會保留。', + feishuRecover: '停用舊外掛並恢復啟動', + feishuRecoverConfirm: + '將備份設定、停用未設定的舊飛書外掛並重新啟動執行個體,短暫中斷所有頻道。外掛檔案將保留。是否繼續?', + feishuBuiltin: '內建飛書外掛', + feishuImageRequired: '請使用包含內建飛書外掛的映像。', + feishuLegacy: '舊外掛已停止維護。切換前請確認下方的工作階段隔離、串流回覆和 @ 設定。', + feishuBuiltinHint: '外掛隨 OpenClaw 映像一起更新。', + feishuSessionTopic: '依群組話題隔離', + feishuSessionTopicSender: '依群組話題和傳送者隔離', + feishuSwitch: '切換至內建外掛', + feishuMentionChoice: '切換前請明確選擇是否需要 @。', + feishuSwitchConfirm: + '將切換至內建飛書並重新啟動執行個體,短暫中斷所有頻道。舊外掛專有工具不再可用;串流與話題行為依所選設定執行,部分聊天可能產生新工作階段。憑證、繫結和配對儲存會保留。是否繼續?', + deepSeekAccessHostHelper: '填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。', deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 9b957e6a1..15b731471 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -757,6 +757,23 @@ const message = { }, aiTools: { agents: { + feishuUpgradeNotice: + '升级前需要处理旧飞书插件。此次升级必须完整备份;失败时恢复原镜像及配套数据。未配置飞书时仅禁用旧插件。', + feishuRecoveryNotice: '切换失败时将尝试恢复网关,但飞书保持停用,不会重新启用旧插件。原配置备份会保留。', + feishuRecover: '禁用旧插件并恢复启动', + feishuRecoverConfirm: + '将备份配置、禁用未配置的旧飞书插件并重启当前实例,短暂中断所有渠道。插件文件将保留。是否继续?', + feishuBuiltin: '内置飞书插件', + feishuImageRequired: '请使用包含内置飞书插件的镜像。', + feishuLegacy: '旧插件已停止维护。切换前请确认下方的会话隔离、流式回复和 @ 设置。', + feishuBuiltinHint: '插件随 OpenClaw 镜像一起更新。', + feishuSessionTopic: '按群话题隔离', + feishuSessionTopicSender: '按群话题和发送者隔离', + feishuSwitch: '切换到内置插件', + feishuMentionChoice: '切换前请明确选择是否需要 @。', + feishuSwitchConfirm: + '将切换到内置飞书并重启实例,短暂中断所有渠道。旧插件独有工具不再可用;流式回复和话题行为按所选设置执行,部分聊天可能产生新会话。凭据、绑定和配对存储会保留。是否继续?', + deepSeekAccessHostHelper: '填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。', deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。', diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/components/feishu-transition.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/components/feishu-transition.vue new file mode 100644 index 000000000..34b8a7e6f --- /dev/null +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/components/feishu-transition.vue @@ -0,0 +1,31 @@ + + + diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/openclaw/feishu.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/openclaw/feishu.vue index 0dcb63447..86bd8e87a 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels/openclaw/feishu.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/openclaw/feishu.vue @@ -1,6 +1,13 @@ @@ -96,12 +137,20 @@ import type { FormInstance } from 'element-plus'; import { ElMessageBox } from 'element-plus'; import { useI18n } from 'vue-i18n'; import { AI } from '@/api/interface/ai'; -import { approveAgentChannelPairing, getAgentFeishuConfig, updateAgentFeishuConfig } from '@/api/modules/ai'; +import { + approveAgentChannelPairing, + getAgentFeishuConfig, + updateAgentFeishuConfig, + switchAgentFeishuPlugin, +} from '@/api/modules/ai'; import { MsgSuccess, MsgWarning } from '@/utils/message'; import { Rules } from '@/global/form-rules'; +import TaskLog from '@/components/log/task/index.vue'; +import { newUUID } from '@/utils/id'; import PluginInstall from '../components/plugin-install.vue'; import { useAgentPluginChannel } from './useAgentPluginChannel'; import ChannelBots from '../components/channel-bots.vue'; +import FeishuTransition from '../components/feishu-transition.vue'; const { t } = useI18n(); @@ -129,10 +178,11 @@ interface FeishuForm extends Omit { const saving = ref(false); const approving = ref(false); const formRef = ref(); +const loading = ref(false); const { agentId, - loading, - installed, + loading: pluginLoading, + installed: pluginInstalled, installing, upgrading, uninstalling, @@ -144,14 +194,22 @@ const { upgradePlugin, uninstallPlugin, } = useAgentPluginChannel('feishu'); +const builtinManaged = ref(true); +const builtinAvailable = ref(false); +const installed = computed(() => (builtinManaged.value ? builtinAvailable.value : pluginInstalled.value)); +const legacy = ref(false); +const legacyConfigured = ref(false); +const switching = ref(false); +const taskLogRef = ref(); const form = reactive({ enabled: true, threadSession: true, streaming: false, replyMode: 'auto', + groupSessionScope: 'group', requireMention: 'true', - groupPolicy: 'open', + groupPolicy: 'allowlist', groupAllowFrom: [], groupAllowFromText: '', bots: [], @@ -181,12 +239,17 @@ const parseTextList = (value: string): string[] => { const rules = reactive({ replyMode: [Rules.requiredInput], + groupSessionScope: [Rules.requiredSelect], requireMention: [Rules.requiredSelect], groupPolicy: [Rules.requiredSelect], groupAllowFromText: [ { validator: (_rule, value, callback) => { - if (form.groupPolicy === 'allowlist' && parseTextList(String(value || '')).length === 0) { + if ( + !legacy.value && + form.groupPolicy === 'allowlist' && + parseTextList(String(value || '')).length === 0 + ) { callback(new Error(t('aiTools.agents.allowFromRequired'))); return; } @@ -250,20 +313,33 @@ const updateBots = (bots: FeishuBotForm[]) => { form.bots = bots; }; -const isApproveDisabled = (bot: FeishuBotForm) => bot.dmPolicy !== 'pairing'; +const isApproveDisabled = (bot: FeishuBotForm) => legacy.value || bot.dmPolicy !== 'pairing'; const load = async (id: number) => { - await loadPlugin(id); - const res = await getAgentFeishuConfig({ agentId: id }); - form.enabled = res.data?.enabled ?? true; - form.threadSession = res.data?.threadSession ?? true; - form.streaming = res.data?.streaming ?? false; - form.replyMode = res.data?.replyMode || 'auto'; - form.requireMention = res.data?.requireMention || 'true'; - form.groupPolicy = res.data?.groupPolicy || 'open'; - form.groupAllowFrom = res.data?.groupAllowFrom || []; - form.groupAllowFromText = (res.data?.groupAllowFrom || []).join('\n'); - form.bots = (res.data?.bots || []).map((bot) => normalizeBot(bot)); + agentId.value = id; + loading.value = true; + try { + const res = await getAgentFeishuConfig({ agentId: id }); + builtinManaged.value = Boolean(res.data?.builtinManaged); + builtinAvailable.value = Boolean(res.data?.builtinAvailable); + legacy.value = builtinManaged.value && Boolean(res.data?.legacy); + legacyConfigured.value = Boolean(res.data?.legacyConfigured); + if (!builtinManaged.value) { + await loadPlugin(id); + } + form.groupSessionScope = res.data?.groupSessionScope || 'group'; + form.enabled = res.data?.enabled ?? true; + form.threadSession = res.data?.threadSession ?? true; + form.streaming = res.data?.streaming ?? false; + form.replyMode = res.data?.replyMode || 'auto'; + form.requireMention = res.data?.requireMention || 'true'; + form.groupPolicy = res.data?.groupPolicy || (builtinManaged.value ? 'allowlist' : 'open'); + form.groupAllowFrom = res.data?.groupAllowFrom || []; + form.groupAllowFromText = (res.data?.groupAllowFrom || []).join('\n'); + form.bots = (res.data?.bots || []).map((bot) => normalizeBot(bot)); + } finally { + loading.value = false; + } }; const reload = async () => { @@ -314,6 +390,7 @@ const saveChannel = async (action: 'delete' | 'save' = 'save') => { agentId: agentId.value, enabled: form.enabled, threadSession: form.threadSession, + ...(builtinManaged.value ? { groupSessionScope: form.groupSessionScope } : {}), streaming: form.streaming, replyMode: form.replyMode, requireMention: form.requireMention, @@ -330,6 +407,40 @@ const saveChannel = async (action: 'delete' | 'save' = 'save') => { } }; +const switchPlugin = async () => { + if (!agentId.value || switching.value || !formRef.value) return; + if (legacyConfigured.value) await formRef.value.validateField(['groupSessionScope', 'requireMention']); + if (legacyConfigured.value && form.requireMention === 'open') { + MsgWarning(t('aiTools.agents.feishuMentionChoice')); + return; + } + await ElMessageBox.confirm( + legacyConfigured.value + ? t('aiTools.agents.feishuSwitchConfirm') + ' ' + t('aiTools.agents.feishuRecoveryNotice') + : t('aiTools.agents.feishuRecoverConfirm'), + t('aiTools.agents.feishuSwitch'), + { + type: 'warning', + confirmButtonText: t('commons.button.confirm'), + cancelButtonText: t('commons.button.cancel'), + }, + ); + switching.value = true; + try { + const taskID = newUUID(); + await switchAgentFeishuPlugin({ + agentId: agentId.value, + taskID, + streaming: form.streaming, + groupSessionScope: form.groupSessionScope || 'group', + requireMention: legacyConfigured.value && form.requireMention === 'false' ? 'false' : 'true', + }); + taskLogRef.value?.openWithTaskID(taskID); + } finally { + switching.value = false; + } +}; + const approvePairing = async (bot: FeishuBotForm) => { if (!agentId.value) { return; diff --git a/frontend/src/views/app-store/installed/upgrade/index.vue b/frontend/src/views/app-store/installed/upgrade/index.vue index 1d59c6023..a45ab900d 100644 --- a/frontend/src/views/app-store/installed/upgrade/index.vue +++ b/frontend/src/views/app-store/installed/upgrade/index.vue @@ -55,8 +55,22 @@
2. {{ $t('app.openclawHttpsUpgradeNoticeItem2') }}
+ - + {{ $t('app.backupAppHelper') }} @@ -100,6 +114,8 @@ import TaskLog from '@/components/log/task/index.vue'; import CodemirrorPro from '@/components/codemirror-pro/index.vue'; import { App } from '@/api/interface/app'; +import { AI } from '@/api/interface/ai'; +import FeishuTransition from '@/views/ai/agents/agent/config/tabs/channels/components/feishu-transition.vue'; import { getAppUpdateVersions, ignoreUpgrade, installedOp } from '@/api/modules/app'; import { getAppStoreConfig } from '@/api/modules/setting'; import { isOpenclawCurrentHTTPVersion, isOpenclawHTTPSWindowVersion } from '@/utils/agent'; @@ -128,6 +144,15 @@ const operateReq = reactive({ version: '', dockerCompose: '', taskID: '', + feishuTransition: undefined as AI.AgentFeishuTransition | undefined, +}); +const feishuLegacy = ref(false); +const feishuConfigured = ref(false); +const feishuTransition = reactive({ + confirmed: false, + streaming: false, + groupSessionScope: 'group', + requireMention: 'true', }); const resourceName = ref(''); const rules = ref({ @@ -196,13 +221,22 @@ const initData = async () => { operateReq.dockerCompose = ''; }; -const acceptParams = (appInstall: App.AppInstallDto, op: string, opNode?: string) => { +const acceptParams = async (appInstall: App.AppInstallDto, op: string, opNode?: string) => { if (opNode) { node.value = opNode; } else { node.value = currentNode.value; } - initData(); + feishuLegacy.value = false; + feishuConfigured.value = false; + operateReq.feishuTransition = undefined; + Object.assign(feishuTransition, { + confirmed: false, + streaming: false, + groupSessionScope: 'group', + requireMention: 'true', + }); + await initData(); isEdit.value = appInstall.isEdit; currentVersion.value = appInstall.version; currentAppKey.value = appInstall.appKey; @@ -235,6 +269,9 @@ const getVersions = async (version: string) => { if (version != '') { item = res.data.find((v) => v.version === version); } + feishuLegacy.value = Boolean(item.feishuLegacy); + feishuConfigured.value = Boolean(item.feishuConfigured); + if (feishuLegacy.value) operateReq.backup = true; operateReq.detailId = item.detailId; operateReq.version = item.version; newContent.value = item.dockerCompose; @@ -279,15 +316,20 @@ const operate = async () => { }; const onOperate = async () => { - ElMessageBox.confirm( - i18n.global.t('app.operatorHelper', [i18n.global.t('commons.button.' + operateReq.operate)]), - i18n.global.t('commons.button.upgrade'), - { - confirmButtonText: i18n.global.t('commons.button.confirm'), - cancelButtonText: i18n.global.t('commons.button.cancel'), - type: 'info', - }, - ).then(() => { + await updateRef.value?.validate(); + const helper = i18n.global.t('app.operatorHelper', [i18n.global.t('commons.button.' + operateReq.operate)]); + const notice = + feishuLegacy.value && operateReq.operate === 'upgrade' + ? i18n.global.t('aiTools.agents.feishuUpgradeNotice') + + (feishuConfigured.value ? ' ' + i18n.global.t('aiTools.agents.feishuSwitchConfirm') : '') + : helper; + ElMessageBox.confirm(notice, i18n.global.t('commons.button.upgrade'), { + confirmButtonText: i18n.global.t('commons.button.confirm'), + cancelButtonText: i18n.global.t('commons.button.cancel'), + type: 'info', + }).then(() => { + operateReq.feishuTransition = + feishuLegacy.value && feishuConfigured.value ? { ...feishuTransition, confirmed: true } : undefined; operate(); }); };