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
This commit is contained in:
CityFun
2026-10-28 11:24:03 +08:00
committed by GitHub
parent eb4beb50c4
commit 1798f9768e
47 changed files with 2513 additions and 119 deletions
+20
View File
@@ -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
+43 -22
View File
@@ -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 {
+5 -3
View File
@@ -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 {
+22 -20
View File
@@ -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 {
+5
View File
@@ -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
}
+38 -3
View File
@@ -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
+677
View File
@@ -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"))
}
+201
View File
@@ -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
}
+6
View File
@@ -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
}
+4
View File
@@ -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, "", " ")
+26 -10
View File
@@ -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,
})
}
}
+74 -4
View File
@@ -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()
}
+336
View File
@@ -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)
}
+16 -4
View File
@@ -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
+23
View File
@@ -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."
+23
View File
@@ -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."
+23
View File
@@ -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 غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
+23
View File
@@ -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 は無効のままで、元の設定のバックアップは保持しています。"
+23
View File
@@ -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는 비활성 상태이며 원본 설정 백업은 유지됩니다."
+23
View File
@@ -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 ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
+23
View File
@@ -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."
+23
View File
@@ -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."
+23
View File
@@ -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 отключён; исходная резервная копия конфигурации сохранена."
+23
View File
@@ -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."
+23
View File
@@ -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: "閘道已恢復,飛書保持停用;原設定備份已保留。"
+23
View File
@@ -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: "网关已恢复,飞书保持停用;原配置备份已保留。"
+1
View File
@@ -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)
+137
View File
@@ -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": [
+137
View File
@@ -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": [
+21
View File
@@ -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;
}
+4
View File
@@ -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;
}
+4
View File
@@ -272,6 +272,10 @@ export const getAgentFeishuConfig = (req: AI.AgentFeishuConfigReq) => {
return http.post<AI.AgentFeishuConfig>(`/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);
};
+19
View File
@@ -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:
+19
View File
@@ -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:
+19
View File
@@ -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:
+19
View File
@@ -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:
+19
View File
@@ -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:
+18
View File
@@ -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:
+19
View File
@@ -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:
+19
View File
@@ -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:
+19
View File
@@ -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:
+19
View File
@@ -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:
+17
View File
@@ -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 位,不含前後空格或換行。',
+17
View File
@@ -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 位,不含首尾空格或换行。',
@@ -0,0 +1,31 @@
<template>
<el-form-item :label="t('aiTools.agents.groupSessionScope')" prop="groupSessionScope">
<el-select :model-value="groupSessionScope" @update:model-value="emit('update:groupSessionScope', $event)">
<el-option :label="t('aiTools.agents.groupSessionScopeGroup')" value="group" />
<el-option :label="t('aiTools.agents.groupSessionScopeGroupSender')" value="group_sender" />
<el-option :label="t('aiTools.agents.feishuSessionTopic')" value="group_topic" />
<el-option :label="t('aiTools.agents.feishuSessionTopicSender')" value="group_topic_sender" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch :model-value="streaming" @update:model-value="emit('update:streaming', Boolean($event))" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select :model-value="requireMention" @update:model-value="emit('update:requireMention', $event)">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option v-if="allowInherit" :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
defineProps<{ streaming: boolean; groupSessionScope?: string; requireMention: string; allowInherit?: boolean }>();
const emit = defineEmits<{
'update:streaming': [value: boolean];
'update:groupSessionScope': [value: string];
'update:requireMention': [value: string];
}>();
</script>
@@ -1,6 +1,13 @@
<template>
<el-form ref="formRef" v-loading="loading || approving" :model="form" :rules="rules" label-position="top">
<el-form
ref="formRef"
v-loading="loading || pluginLoading || approving"
:model="form"
:rules="rules"
label-position="top"
>
<PluginInstall
v-if="!builtinManaged"
:installed="installed"
:installing="installing"
:upgrading="upgrading"
@@ -13,29 +20,55 @@
:uninstall-action="uninstallPlugin"
:on-task-close="reload"
/>
<el-alert
v-else
:closable="false"
:type="!installed || legacy ? 'warning' : 'info'"
:title="t('aiTools.agents.feishuBuiltin')"
:description="
t(
!installed
? 'aiTools.agents.feishuImageRequired'
: legacy
? legacyConfigured
? 'aiTools.agents.feishuLegacy'
: 'aiTools.agents.feishuRecoverConfirm'
: 'aiTools.agents.feishuBuiltinHint',
)
"
/>
<template v-if="installed">
<el-form-item :label="t('commons.table.status')" class="mt-4">
<el-switch v-model="form.enabled" />
<el-switch v-model="form.enabled" :disabled="legacy" />
</el-form-item>
<div>
<el-form-item :label="t('aiTools.agents.threadSession')">
<el-switch v-model="form.threadSession" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch v-model="form.streaming" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.replyMode')" prop="replyMode">
<el-input v-model="form.replyMode" placeholder="auto" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select v-model="form.requireMention">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
<div v-if="!legacy || legacyConfigured">
<FeishuTransition
v-if="builtinManaged"
v-model:streaming="form.streaming"
v-model:group-session-scope="form.groupSessionScope"
v-model:require-mention="form.requireMention"
:allow-inherit="!legacy"
/>
<template v-else>
<el-form-item :label="t('aiTools.agents.threadSession')">
<el-switch v-model="form.threadSession" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch v-model="form.streaming" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.replyMode')" prop="replyMode">
<el-input v-model="form.replyMode" placeholder="auto" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select v-model="form.requireMention">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
</template>
<el-form-item :label="t('aiTools.agents.groupPolicy')" prop="groupPolicy">
<el-select v-model="form.groupPolicy">
<el-select v-model="form.groupPolicy" :disabled="legacy">
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
@@ -48,6 +81,7 @@
>
<el-input
v-model="form.groupAllowFromText"
:disabled="legacy"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.groupAllowFromPlaceholder')"
@@ -63,6 +97,7 @@
</el-form-item>
</div>
<ChannelBots
v-if="!legacy || legacyConfigured"
:bots="form.bots"
:fields="botFields"
:create-bot="createBot"
@@ -71,8 +106,8 @@
unique-field-prop="appId"
unique-field-label="App ID"
:summary-formatter="getBotSummary"
:add-disabled="!installed"
:disabled="!installed"
:add-disabled="legacy"
:disabled="legacy"
approvable
:approve-disabled="isApproveDisabled"
:fixed-account-ids="['default']"
@@ -81,12 +116,18 @@
@save="saveChannel"
@approve="approvePairing"
/>
<el-form-item class="mt-4">
<el-button v-permission type="primary" :loading="saving" :disabled="!installed" @click="saveChannel">
<el-form-item v-if="!legacy" class="mt-4">
<el-button v-permission type="primary" :loading="saving" :disabled="legacy" @click="saveChannel">
{{ t('commons.button.save') }}
</el-button>
</el-form-item>
<el-form-item v-else class="mt-4">
<el-button v-permission type="primary" :loading="switching" @click="switchPlugin">
{{ t(legacyConfigured ? 'aiTools.agents.feishuSwitch' : 'aiTools.agents.feishuRecover') }}
</el-button>
</el-form-item>
</template>
<TaskLog ref="taskLogRef" @close="reload" />
</el-form>
</template>
@@ -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<AI.AgentFeishuConfig, 'bots'> {
const saving = ref(false);
const approving = ref(false);
const formRef = ref<FormInstance>();
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<FeishuForm>({
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;
@@ -55,8 +55,22 @@
<div>2. {{ $t('app.openclawHttpsUpgradeNoticeItem2') }}</div>
</div>
</el-alert>
<template v-if="feishuLegacy && operateReq.operate === 'upgrade'">
<el-alert
class="mb-4"
type="warning"
:closable="false"
:title="$t('aiTools.agents.feishuUpgradeNotice')"
/>
<FeishuTransition
v-if="feishuConfigured"
v-model:streaming="feishuTransition.streaming"
v-model:group-session-scope="feishuTransition.groupSessionScope"
v-model:require-mention="feishuTransition.requireMention"
/>
</template>
<el-form-item prop="backup" v-if="operateReq.operate === 'upgrade'">
<el-checkbox v-model="operateReq.backup" :label="$t('app.backupApp')" />
<el-checkbox v-model="operateReq.backup" :disabled="feishuLegacy" :label="$t('app.backupApp')" />
<span class="input-help">
<el-text type="warning">{{ $t('app.backupAppHelper') }}</el-text>
</span>
@@ -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<AI.AgentFeishuTransition>({
confirmed: false,
streaming: false,
groupSessionScope: 'group',
requireMention: 'true',
});
const resourceName = ref('');
const rules = ref<any>({
@@ -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();
});
};