mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-28 16:00:18 +00:00
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:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, "", " ")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
|
||||
|
||||
@@ -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 は無効のままで、元の設定のバックアップは保持しています。"
|
||||
|
||||
@@ -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는 비활성 상태이며 원본 설정 백업은 유지됩니다."
|
||||
|
||||
@@ -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 ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 отключён; исходная резервная копия конфигурации сохранена."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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: "閘道已恢復,飛書保持停用;原設定備份已保留。"
|
||||
|
||||
@@ -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: "网关已恢复,飞书保持停用;原配置备份已保留。"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 位,不含前後空格或換行。',
|
||||
|
||||
@@ -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 位,不含首尾空格或换行。',
|
||||
|
||||
+31
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user