Compare commits

..
163 changed files with 4671 additions and 452 deletions
+20
View File
@@ -826,6 +826,26 @@ func (b *BaseApi) UpdateAgentFeishuConfig(c *gin.Context) {
helper.Success(c) 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 // @Tags AI
// @Summary Get Agent Telegram channel config // @Summary Get Agent Telegram channel config
// @Accept json // @Accept json
@@ -0,0 +1,43 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) GetContainerImageCheckUpdateStatus(c *gin.Context) {
status, err := service.GetContainerImageCheckUpdateStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, status)
}
func (b *BaseApi) ContainerImageCheckUpdate(c *gin.Context) {
taskID, err := service.StartContainerImageCheckUpdate()
if err != nil {
helper.InternalServer(c, err)
return
}
status, err := service.GetContainerImageCheckUpdateStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
status.TaskID = taskID
helper.SuccessWithData(c, status)
}
func (b *BaseApi) SetContainerImageCheckUpdateSchedule(c *gin.Context) {
var req struct {
Enabled *bool `json:"enabled" validate:"required"`
}
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := service.SetContainerImageCheckUpdateSchedule(*req.Enabled); err != nil {
helper.InternalServer(c, err)
return
}
b.GetContainerImageCheckUpdateStatus(c)
}
+43 -22
View File
@@ -433,32 +433,53 @@ type AgentFeishuConfigReq struct {
AgentID uint `json:"agentId" validate:"required"` 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 { type AgentFeishuConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` GroupSessionScope string `json:"groupSessionScope" validate:"omitempty,oneof=group group_sender group_topic group_topic_sender"`
Enabled bool `json:"enabled"` AgentID uint `json:"agentId" validate:"required"`
ThreadSession bool `json:"threadSession"` Enabled bool `json:"enabled"`
ReplyMode string `json:"replyMode" validate:"required"` ThreadSession bool `json:"threadSession"`
Streaming bool `json:"streaming"` ReplyMode string `json:"replyMode" validate:"required"`
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"` Streaming bool `json:"streaming"`
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"` RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
GroupAllowFrom []string `json:"groupAllowFrom"` GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
Domain string `json:"domain"` GroupAllowFrom []string `json:"groupAllowFrom"`
ConnectionMode string `json:"connectionMode"` Domain string `json:"domain"`
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"` ConnectionMode string `json:"connectionMode"`
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
} }
type AgentFeishuConfig struct { type AgentFeishuConfig struct {
Enabled bool `json:"enabled"` BuiltinManaged bool `json:"builtinManaged"`
ThreadSession bool `json:"threadSession"` LegacyConfigured bool `json:"legacyConfigured"`
ReplyMode string `json:"replyMode"` GroupSessionScope string `json:"groupSessionScope"`
Streaming bool `json:"streaming"` BuiltinAvailable bool `json:"builtinAvailable"`
RequireMention string `json:"requireMention"` Legacy bool `json:"legacy"`
GroupPolicy string `json:"groupPolicy"` Enabled bool `json:"enabled"`
GroupAllowFrom []string `json:"groupAllowFrom"` ThreadSession bool `json:"threadSession"`
Domain string `json:"domain"` ReplyMode string `json:"replyMode"`
ConnectionMode string `json:"connectionMode"` Streaming bool `json:"streaming"`
Bots []AgentFeishuBot `json:"bots"` RequireMention string `json:"requireMention"`
Installed bool `json:"installed"` 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 { type AgentTelegramConfigReq struct {
+5 -3
View File
@@ -43,9 +43,11 @@ type AppOssConfig struct {
} }
type AppVersion struct { type AppVersion struct {
Version string `json:"version"` FeishuLegacy bool `json:"feishuLegacy,omitempty"`
DetailId uint `json:"detailId"` FeishuConfigured bool `json:"feishuConfigured,omitempty"`
DockerCompose string `json:"dockerCompose"` Version string `json:"version"`
DetailId uint `json:"detailId"`
DockerCompose string `json:"dockerCompose"`
} }
type AppList struct { type AppList struct {
+10 -7
View File
@@ -21,13 +21,16 @@ type InspectReq struct {
} }
type ContainerInfo struct { type ContainerInfo struct {
ContainerID string `json:"containerID"` LatestVersion string `json:"latestVersion"`
Name string `json:"name"` TargetImage string `json:"targetImage"`
ImageId string `json:"imageID"` HasUpdate bool `json:"hasUpdate"`
ImageName string `json:"imageName"` ContainerID string `json:"containerID"`
CreateTime string `json:"createTime"` Name string `json:"name"`
State string `json:"state"` ImageId string `json:"imageID"`
RunTime string `json:"runTime"` ImageName string `json:"imageName"`
CreateTime string `json:"createTime"`
State string `json:"state"`
RunTime string `json:"runTime"`
Network []string `json:"network"` Network []string `json:"network"`
Ports []string `json:"ports"` Ports []string `json:"ports"`
+22 -20
View File
@@ -89,31 +89,33 @@ type AppBackupDelete struct {
} }
type AppInstalledOperate struct { type AppInstalledOperate struct {
InstallId uint `json:"installId" validate:"required"` FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
BackupId uint `json:"backupId"` InstallId uint `json:"installId" validate:"required"`
DetailId uint `json:"detailId"` BackupId uint `json:"backupId"`
Operate constant.AppOperate `json:"operate" validate:"required"` DetailId uint `json:"detailId"`
ForceDelete bool `json:"forceDelete"` Operate constant.AppOperate `json:"operate" validate:"required"`
DeleteBackup bool `json:"deleteBackup"` ForceDelete bool `json:"forceDelete"`
DeleteDB bool `json:"deleteDB"` DeleteBackup bool `json:"deleteBackup"`
Backup bool `json:"backup"` DeleteDB bool `json:"deleteDB"`
PullImage bool `json:"pullImage"` Backup bool `json:"backup"`
DockerCompose string `json:"dockerCompose"` PullImage bool `json:"pullImage"`
TaskID string `json:"taskID"` DockerCompose string `json:"dockerCompose"`
DeleteImage bool `json:"deleteImage"` TaskID string `json:"taskID"`
Favorite bool `json:"favorite"` DeleteImage bool `json:"deleteImage"`
Favorite bool `json:"favorite"`
UseLifecycleScripts bool `json:"-"` UseLifecycleScripts bool `json:"-"`
} }
type AppInstallUpgrade struct { type AppInstallUpgrade struct {
InstallID uint `json:"installId"` FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
DetailID uint `json:"detailId"` InstallID uint `json:"installId"`
Backup bool `json:"backup"` DetailID uint `json:"detailId"`
PullImage bool `json:"pullImage"` Backup bool `json:"backup"`
DeleteImage bool `json:"deleteImage"` PullImage bool `json:"pullImage"`
DockerCompose string `json:"dockerCompose"` DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"` DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"`
} }
type AppInstallDelete struct { type AppInstallDelete struct {
@@ -0,0 +1,20 @@
package model
import "time"
// ContainerImageCheckUpdate is a cache tied to the exact image used by a container.
type ContainerImageCheckUpdate struct {
ContainerID string `gorm:"primaryKey" json:"containerID"`
ImageID string `json:"imageID"`
ImageName string `json:"imageName"`
LatestVersion string `json:"latestVersion"`
TargetImage string `json:"targetImage"`
TargetImageID string `json:"-"`
HasUpdate bool `gorm:"not null;default:false" json:"hasUpdate"`
CheckedAt time.Time `json:"checkedAt"`
CheckStatus string `json:"checkStatus"`
CheckError string `json:"checkError"`
}
// TableName preserves the table created by the original migration.
func (ContainerImageCheckUpdate) TableName() string { return "container_image_updates" }
+5
View File
@@ -88,6 +88,7 @@ type IAgentService interface {
GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error)
UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error
SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error
GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error)
UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error) GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error)
@@ -1591,6 +1592,10 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
if err != nil { if err != nil {
return err 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 { if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
return err return err
} }
+38 -3
View File
@@ -45,9 +45,29 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF
if err != nil { if err != nil {
return nil, err 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) result := extractFeishuConfig(conf)
installed, _ := checkPluginInstalled(install.GetPath(), "feishu") if !legacy {
result.Installed = installed 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 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 { return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
config := dto.AgentFeishuConfig{ config := dto.AgentFeishuConfig{
Enabled: req.Enabled, Enabled: req.Enabled,
@@ -358,6 +381,9 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
if err != nil { if err != nil {
return err 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 { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err return err
} }
@@ -403,6 +429,9 @@ func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error {
if err != nil { if err != nil {
return err 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 { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err return err
} }
@@ -443,6 +472,9 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
if err != nil { if err != nil {
return err 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 { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err return err
} }
@@ -538,10 +570,13 @@ func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
} }
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, 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 { if err != nil {
return nil, err 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) installed, err := checkPluginInstalled(install.GetPath(), req.Type)
if err != nil { if err != nil {
return nil, err return nil, err
+677
View File
@@ -0,0 +1,677 @@
package service
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
)
// Keep panel configuration writes out of the backup/activation/recovery window.
var feishuSwitches sync.Map
func isOpenclawBundledFeishuVersion(version string) bool {
return common.CompareAppVersion(version, "2026.10.7")
}
func hasBundledFeishu(container string) bool {
image, err := feishuContainerImage(container)
if err != nil {
return false
}
_, err = bundledFeishuImage(image)
return err == nil
}
func usesBundledFeishu(conf map[string]interface{}) bool {
f := getChannelConfig(conf, "feishu")
for _, key := range []string{"threadSession", "replyMode"} {
if _, old := f[key]; old {
return false
}
}
if _, old := f["streaming"].(bool); old {
return false
}
entries := childMap(childMap(conf, "plugins"), "entries")
if extractBoolValue(childMap(entries, "openclaw-lark")["enabled"], false) {
return false
}
_, streaming := f["streaming"].(map[string]interface{})
_, selected := entries["feishu"]
return selected && (extractBoolValue(childMap(entries, "feishu")["enabled"], false) || f["groupSessionScope"] != nil || streaming)
}
func hasLegacyFeishu(conf map[string]interface{}, installPath string) bool {
legacy, _, err := classifyLegacyFeishu(conf, installPath)
return err != nil || legacy
}
func selectBundledFeishu(conf map[string]interface{}, enabled bool) {
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
if legacy, ok := entries["openclaw-lark"]; ok {
if entry, ok := legacy.(map[string]interface{}); ok {
entry["enabled"] = false
}
}
ensureChildMap(entries, "feishu")["enabled"] = enabled
// Preserve an absent allowlist; creating one would disable unrelated plugins.
if allow, ok := plugins["allow"]; ok && len(extractStringList(allow)) > 0 {
ids := slices.DeleteFunc(extractStringList(allow), func(id string) bool { return id == "openclaw-lark" })
if !slices.Contains(ids, "feishu") {
ids = append(ids, "feishu")
}
plugins["allow"] = ids
}
if deny, ok := plugins["deny"]; ok {
plugins["deny"] = slices.DeleteFunc(extractStringList(deny), func(id string) bool { return id == "feishu" })
}
}
func feishuConfigError(field string) error { return buserr.WithDetail("ErrFeishuConfig", field, nil) }
func convertLegacyFeishu(conf map[string]interface{}, req dto.AgentFeishuSwitchReq) error {
f := getChannelConfig(conf, "feishu")
if len(f) == 0 {
if raw, exists := childMap(conf, "channels")["feishu"]; exists && raw != nil {
if _, ok := raw.(map[string]interface{}); !ok {
return feishuConfigError("channels.feishu")
}
}
f = ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
f["enabled"], f["dmPolicy"], f["groupPolicy"] = false, "pairing", "allowlist"
}
if err := checkLegacyFeishuFields(f, "channels.feishu", false); err != nil {
return err
}
if err := convertFeishuAccess(f, "channels.feishu"); err != nil {
return err
}
delete(f, "threadSession")
delete(f, "replyMode")
f["streaming"] = map[string]interface{}{"mode": "off"}
if req.Streaming {
f["streaming"] = map[string]interface{}{"mode": "partial"}
}
f["groupSessionScope"] = req.GroupSessionScope
f["requireMention"] = req.RequireMention == "true"
delete(f, "topicSessionMode")
// Preserve legacy defaults explicitly; bundled defaults are more restrictive.
if _, ok := f["groupPolicy"]; !ok {
f["groupPolicy"] = "open"
}
entries := ensureChildMap(ensureChildMap(conf, "plugins"), "entries")
ensureChildMap(entries, "openclaw-lark")["enabled"] = false
selectBundledFeishu(conf, extractBoolValue(f["enabled"], true))
return nil
}
// Limit automatic conversion to the shared channel fields; custom plugin options require review.
func checkLegacyFeishuFields(f map[string]interface{}, field string, group bool) error {
allowed := strings.Fields("enabled name appId appSecret encryptKey verificationToken domain connectionMode webhookPath defaultAccount dmPolicy allowFrom groupPolicy groupAllowFrom groupSenderAllowFrom requireMention accounts groups streaming threadSession replyMode groupSessionScope topicSessionMode replyInThread")
if group {
allowed = strings.Fields("enabled requireMention allowFrom groupSessionScope topicSessionMode replyInThread")
}
for _, key := range sortedChildKeys(f) {
if !slices.Contains(allowed, key) {
return feishuConfigError(field + "." + key)
}
}
if enabled, exists := f["enabled"]; exists {
if _, ok := enabled.(bool); !ok {
return feishuConfigError(field + ".enabled")
}
}
for _, key := range []string{"dmPolicy", "groupPolicy"} {
if raw, exists := f[key]; exists {
value, ok := raw.(string)
options := []string{"open", "allowlist", "disabled"}
if key == "dmPolicy" {
options = append(options, "pairing")
}
if !ok || !slices.Contains(options, value) {
return feishuConfigError(field + "." + key)
}
}
}
for _, key := range []string{"allowFrom", "groupSenderAllowFrom"} {
if raw, exists := f[key]; exists {
values, ok := raw.([]interface{})
if !ok {
return feishuConfigError(field + "." + key)
}
for _, value := range values {
if _, ok := value.(string); !ok {
return feishuConfigError(field + "." + key)
}
}
}
}
for _, section := range []string{"accounts", "groups"} {
raw, exists := f[section]
if !exists {
continue
}
entries, ok := raw.(map[string]interface{})
if !ok {
return feishuConfigError(field + "." + section)
}
for _, id := range sortedChildKeys(entries) {
entry, ok := entries[id].(map[string]interface{})
name := field + "." + section + "." + id
if !ok {
return feishuConfigError(name)
}
if err := checkLegacyFeishuFields(entry, name, section == "groups"); err != nil {
return err
}
}
}
return nil
}
func convertFeishuAccess(f map[string]interface{}, field string) error {
if raw, ok := f["groupAllowFrom"]; ok {
values, ok := raw.([]interface{})
if !ok {
return feishuConfigError(field + ".groupAllowFrom")
}
groups, senders := []string{}, []string{}
for _, value := range values {
id, ok := value.(string)
if !ok {
return feishuConfigError(field + ".groupAllowFrom")
}
switch {
case strings.HasPrefix(id, "oc_"):
groups = append(groups, id)
case strings.HasPrefix(id, "ou_"):
senders = append(senders, id)
default:
return feishuConfigError(field + ".groupAllowFrom")
}
}
// A sender-only legacy allowlist has different group admission semantics.
if len(senders) > 0 && (len(groups) == 0 || f["groupSenderAllowFrom"] != nil) {
return feishuConfigError(field + ".groupAllowFrom")
}
f["groupAllowFrom"] = groups
if len(senders) > 0 {
f["groupSenderAllowFrom"] = senders
}
}
for _, section := range []string{"accounts", "groups"} {
for id, value := range childMap(f, section) {
entry, ok := value.(map[string]interface{})
name := field + "." + section + "." + id
if !ok {
return feishuConfigError(name)
}
// Account/group overrides must be resolved explicitly, not flattened.
for _, key := range []string{"threadSession", "replyMode"} {
if _, ok := entry[key]; ok {
return feishuConfigError(name + "." + key)
}
}
if mention, ok := entry["requireMention"]; ok {
if _, ok := mention.(bool); !ok {
return feishuConfigError(name + ".requireMention")
}
}
if raw, exists := entry["streaming"]; exists {
streaming, ok := raw.(bool)
if !ok || streaming {
return feishuConfigError(name + ".streaming")
}
entry["streaming"] = map[string]interface{}{"mode": "off"}
}
if err := convertFeishuAccess(entry, name); err != nil {
return err
}
}
}
return nil
}
func extractBundledFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
result := extractFeishuConfig(conf)
f := getChannelConfig(conf, "feishu")
result.Enabled = extractBoolValue(f["enabled"], true) && extractBoolValue(childMap(childMap(childMap(conf, "plugins"), "entries"), "feishu")["enabled"], true)
result.GroupSessionScope = extractStringValue(f["groupSessionScope"])
if result.GroupSessionScope == "" {
result.GroupSessionScope = "group"
}
result.Streaming = extractStringValue(childMap(f, "streaming")["mode"]) != "off"
result.GroupPolicy = extractStringValue(f["groupPolicy"])
if result.GroupPolicy == "" {
result.GroupPolicy = "allowlist"
}
if len(f) == 0 {
result.Streaming = false
result.RequireMention = "true"
} else if _, ok := f["requireMention"]; !ok {
result.RequireMention = "open"
}
accounts := childMap(f, "accounts")
ids := sortedChildKeys(accounts)
if (f["appId"] != nil || f["appSecret"] != nil) && !slices.Contains(ids, "default") {
ids = append([]string{"default"}, ids...)
}
result.Bots = []dto.AgentFeishuBot{}
for _, id := range ids {
effective := make(map[string]interface{}, len(f))
for k, v := range f {
effective[k] = v
}
for k, v := range childMap(accounts, id) {
effective[k] = v
}
bot := defaultFeishuBot()
bot.AccountID, bot.IsDefault = id, id == "default"
bot.Name = extractDisplayName(childMap(accounts, id), "", id)
bot.Enabled = extractBoolValue(childMap(accounts, id)["enabled"], true)
bot.AppID, bot.AppSecret = extractStringValue(effective["appId"]), extractStringValue(effective["appSecret"])
bot.DmPolicy = extractStringValue(effective["dmPolicy"])
if bot.DmPolicy == "" {
bot.DmPolicy = "pairing"
}
bot.AllowFrom = extractStringList(effective["allowFrom"])
result.Bots = append(result.Bots, bot)
}
return result
}
func setBundledFeishuConfig(conf map[string]interface{}, req dto.AgentFeishuConfigUpdateReq) error {
if !slices.Contains([]string{"group", "group_sender", "group_topic", "group_topic_sender"}, req.GroupSessionScope) {
return feishuConfigError("groupSessionScope")
}
f := ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
accounts := childMap(f, "accounts")
seen := map[string]bool{}
appIDs := map[string]bool{}
for _, bot := range req.Bots {
if bot.AccountID == "" || bot.AccountID != strings.ToLower(strings.TrimSpace(bot.AccountID)) || seen[bot.AccountID] || appIDs[bot.AppID] || bot.AppID == "" || !slices.Contains([]string{"pairing", "open", "allowlist", "disabled"}, bot.DmPolicy) || (bot.DmPolicy == "allowlist" && len(bot.AllowFrom) == 0) {
return feishuConfigError("accounts")
}
seen[bot.AccountID] = true
appIDs[bot.AppID] = true
entry := childMap(accounts, bot.AccountID)
if _, exists := entry["appSecret"]; !exists && bot.AppSecret == "" {
entry["appSecret"] = f["appSecret"]
}
// SecretRef credentials cannot be represented by the form: keep them intact.
if _, secretRef := entry["appSecret"].(map[string]interface{}); !secretRef && bot.AppSecret == "" {
return feishuConfigError("accounts." + bot.AccountID + ".appSecret")
}
entry["appId"], entry["name"], entry["enabled"], entry["dmPolicy"] = bot.AppID, bot.Name, bot.Enabled, bot.DmPolicy
if bot.AppSecret != "" {
entry["appSecret"] = bot.AppSecret
}
switch bot.DmPolicy {
case "open":
entry["allowFrom"] = []string{"*"}
case "allowlist":
entry["allowFrom"] = bot.AllowFrom
default:
if len(bot.AllowFrom) > 0 {
entry["allowFrom"] = bot.AllowFrom
} else {
entry["allowFrom"] = []string{}
}
}
accounts[bot.AccountID] = entry
}
for id := range accounts {
if !seen[id] {
delete(accounts, id)
}
}
if !seen["default"] {
delete(f, "appId")
delete(f, "appSecret")
}
if id := extractStringValue(f["defaultAccount"]); id != "" && !seen[id] {
return feishuConfigError("defaultAccount")
}
if len(accounts) > 0 {
f["accounts"] = accounts
} else {
delete(f, "accounts")
}
f["enabled"], f["groupSessionScope"], f["groupPolicy"] = req.Enabled, req.GroupSessionScope, req.GroupPolicy
if req.RequireMention == "open" {
delete(f, "requireMention")
} else {
f["requireMention"] = req.RequireMention == "true"
}
if req.GroupPolicy == "allowlist" {
f["groupAllowFrom"] = req.GroupAllowFrom
} else {
delete(f, "groupAllowFrom")
}
streaming := ensureChildMap(f, "streaming")
streaming["mode"] = "off"
if req.Streaming {
streaming["mode"] = "partial"
}
selectBundledFeishu(conf, req.Enabled && hasEnabledBots(req.Bots))
return nil
}
// Write beside the original so validation and rename preserve the mounted filesystem and owner.
func stageFeishuConfig(configPath string, payload []byte) (string, error) {
info, err := os.Lstat(configPath)
if err != nil {
return "", err
}
if !info.Mode().IsRegular() {
return "", feishuConfigError("openclaw.json")
}
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-*.json")
if err != nil {
return "", err
}
name := f.Name()
defer func() {
f.Close()
if err != nil {
os.Remove(name)
}
}()
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
err = f.Chown(int(stat.Uid), int(stat.Gid))
}
if err == nil {
err = f.Chmod(info.Mode().Perm())
}
if err == nil {
_, err = f.Write(payload)
}
if err == nil {
err = f.Sync()
}
if err == nil {
err = f.Close()
}
return name, err
}
func validateFeishuConfig(container, configPath string) error {
out, err := cmd.RunDockerExecWithStdout(time.Minute, container, "env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath), "openclaw", "config", "validate", "--json")
return feishuValidationResult(out, err)
}
func replaceFeishuConfig(configPath string, expected, payload []byte) error {
tmp, err := stageFeishuConfig(configPath, payload)
if err != nil {
return err
}
defer os.Remove(tmp)
current, err := os.ReadFile(configPath)
if err != nil {
return err
}
if !bytes.Equal(current, expected) {
return buserr.New("ErrFeishuConfigChanged")
}
return os.Rename(tmp, configPath)
}
func (a AgentService) SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error {
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if !isOpenclawBundledFeishuVersion(install.Version) {
return buserr.New("ErrFeishuImageRequired")
}
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
return buserr.New("TaskIsExecuting")
}
started := false
defer func() {
if !started {
feishuSwitches.Delete(agent.ConfigPath)
}
}()
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
if !hasBundledFeishu(install.ContainerName) {
return buserr.New("ErrFeishuImageRequired")
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
if err != nil {
return err
}
if !legacy {
return buserr.New("ErrFeishuAlreadySwitched")
}
if err := prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
return err
}
switchTask, err := task.NewTaskWithOps("Feishu", task.TaskUpdate, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
switchTask.AddSubTaskWithAlias("AgentFeishuSwitch", func(t *task.Task) error { return a.runFeishuSwitch(agent, install, req, t) }, nil)
started = true
go func() { defer feishuSwitches.Delete(agent.ConfigPath); executeAgentPluginTask(switchTask) }()
return nil
}
func (a AgentService) runFeishuSwitch(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuSwitchReq, t *task.Task) (result error) {
original, conf, err := readFeishuConfigBytes(agent.ConfigPath)
if err != nil {
return err
}
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
if err != nil {
return err
}
if !legacy {
return buserr.New("ErrFeishuAlreadySwitched")
}
image, err := feishuContainerImage(install.ContainerName)
if err != nil {
return err
}
image, err = bundledFeishuImage(image)
if err != nil {
return err
}
if err = prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
return err
}
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
recovery, err := feishuRecoveryPayload(payload, configured)
if err != nil {
return err
}
// Validate both candidates without invoking Doctor or mutating retained state.
for _, candidate := range [][]byte{payload, recovery} {
staged, stageErr := stageFeishuConfig(agent.ConfigPath, candidate)
if stageErr != nil {
return stageErr
}
validateErr := validateFeishuImageConfig(image, staged)
os.Remove(staged)
if validateErr != nil {
return validateErr
}
}
if _, err = compose.Stop(install.GetComposePath()); err != nil {
return err
}
replaced := false
defer func() {
if !replaced {
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
}
return
}
if result == nil {
return
}
if _, stopErr := compose.Stop(install.GetComposePath()); stopErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", stopErr))
return
}
if restoreErr := replaceFeishuConfig(agent.ConfigPath, payload, recovery); restoreErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", restoreErr))
return
}
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
return
}
if healthErr := verifyFeishuActivation(install.ContainerName, false); healthErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", healthErr))
return
}
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
}()
current, err := os.ReadFile(agent.ConfigPath)
if err != nil {
return err
}
if !bytes.Equal(current, original) {
return buserr.New("ErrFeishuConfigChanged")
}
backup, err := backupFeishuConfig(agent.ConfigPath, original)
if err != nil {
return err
}
t.Log(i18n.GetWithName("AgentFeishuBackup", filepath.Base(backup)))
if err = replaceFeishuConfig(agent.ConfigPath, original, payload); err != nil {
return err
}
replaced = true
if _, err = compose.Operate(install.GetComposePath(), "start"); err != nil {
return err
}
enabled := configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
if err = verifyFeishuActivation(install.ContainerName, enabled); err != nil {
return err
}
if enabled {
probeFeishuConnection(install.ContainerName, t)
}
if configured {
t.Log(i18n.GetMsgByKey("AgentFeishuVerifyMessages"))
} else {
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
}
return nil
}
func saveBundledFeishuConfig(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuConfigUpdateReq) error {
if !isOpenclawBundledFeishuVersion(install.Version) {
return buserr.New("ErrFeishuImageRequired")
}
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
return buserr.New("TaskIsExecuting")
}
defer feishuSwitches.Delete(agent.ConfigPath)
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
if !hasBundledFeishu(install.ContainerName) {
return buserr.New("ErrFeishuImageRequired")
}
original, err := os.ReadFile(agent.ConfigPath)
if err != nil {
return err
}
var conf map[string]interface{}
if err = json.Unmarshal(original, &conf); err != nil {
return err
}
if hasLegacyFeishu(conf, install.GetPath()) {
return buserr.New("ErrFeishuSwitchRequired")
}
if err = setBundledFeishuConfig(conf, req); err != nil {
return err
}
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
staged, err := stageFeishuConfig(agent.ConfigPath, payload)
if err != nil {
return err
}
defer os.Remove(staged)
if err = validateFeishuConfig(install.ContainerName, staged); err != nil {
return err
}
return replaceFeishuConfig(agent.ConfigPath, original, payload)
}
func feishuProbeOK(output string) bool {
raw, err := extractEmbeddedJSON(output)
if err != nil {
return false
}
var status struct {
ChannelAccounts map[string][]struct {
Enabled bool `json:"enabled"`
Configured bool `json:"configured"`
Running bool `json:"running"`
Probe struct {
OK bool `json:"ok"`
} `json:"probe"`
} `json:"channelAccounts"`
}
if json.Unmarshal(raw, &status) != nil {
return false
}
accounts, ok := status.ChannelAccounts["feishu"]
if !ok {
return false
}
for _, account := range accounts {
if account.Enabled && (!account.Configured || !account.Running || !account.Probe.OK) {
return false
}
}
return true
}
func feishuGatewayReady(output string, enabled bool) bool {
raw, err := extractEmbeddedJSON(output)
if err != nil {
return false
}
var health struct {
OK bool `json:"ok"`
Plugins struct {
Loaded []string `json:"loaded"`
} `json:"plugins"`
}
return json.Unmarshal(raw, &health) == nil && health.OK && !slices.Contains(health.Plugins.Loaded, "openclaw-lark") && (!enabled || slices.Contains(health.Plugins.Loaded, "feishu"))
}
+201
View File
@@ -0,0 +1,201 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/container"
)
// Inspect a stopped disposable container; its entrypoint never touches retained state.
func bundledFeishuImage(image string) (string, error) {
cli, err := docker.NewDockerClient()
if err != nil {
return "", err
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
info, _, err := cli.ImageInspectWithRaw(ctx, image)
if err != nil {
return "", buserr.New("ErrFeishuImageRequired")
}
created, err := cli.ContainerCreate(ctx, &container.Config{Image: info.ID, Entrypoint: []string{"/bin/true"}}, &container.HostConfig{NetworkMode: "none"}, nil, nil, "")
if err != nil {
return "", err
}
defer func() {
cleanup, done := context.WithTimeout(context.Background(), 10*time.Second)
defer done()
_ = cli.ContainerRemove(cleanup, created.ID, container.RemoveOptions{Force: true, RemoveVolumes: true})
}()
if _, err = cli.ContainerStatPath(ctx, created.ID, "/app/.1panel-feishu-bundled-v1"); err != nil {
return "", buserr.New("ErrFeishuImageRequired")
}
return info.ID, nil
}
func feishuContainerImage(name string) (string, error) {
cli, err := docker.NewDockerClient()
if err != nil {
return "", err
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
info, err := cli.ContainerInspect(ctx, name)
if err != nil {
return "", err
}
return info.Image, nil
}
// The original state is read-only and the normal Doctor entrypoint is bypassed.
func validateFeishuImageConfig(image, configPath string) error {
info, err := os.Stat(configPath)
if err != nil {
return err
}
uid, gid := uint32(1000), uint32(1000)
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid, gid = stat.Uid, stat.Gid
}
user := fmt.Sprintf("%d:%d", uid, gid)
tmpfs := fmt.Sprintf("/tmp:rw,mode=0700,uid=%d,gid=%d", uid, gid)
name := "1panel-feishu-" + strings.TrimSuffix(filepath.Base(configPath), ".json")
mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
defer func() { _ = cmd.NewCommandMgr(cmd.WithTimeout(15*time.Second)).Run("docker", "rm", "-f", "-v", name) }()
out, runErr := mgr.RunWithStdout("docker", "run", "--name", name, "--rm", "--network", "none", "--read-only", "--user", user,
"--tmpfs", tmpfs,
"--mount", "type=bind,src="+filepath.Dir(configPath)+",dst=/home/node/.openclaw,readonly",
"--env", "HOME=/home/node", "--env", "TMPDIR=/tmp", "--env", "OPENCLAW_STATE_DIR=/tmp/state",
"--env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath),
"--entrypoint", "sh", image, "-c", "mkdir -m 700 /tmp/state && ln -s /home/node/.openclaw/extensions /tmp/state/extensions && exec node /app/openclaw.mjs config validate --json")
return feishuValidationResult(out, runErr)
}
func feishuValidationResult(out string, runErr error) error {
raw, _ := extractEmbeddedJSON(out)
var result struct {
Valid bool `json:"valid"`
Issues []struct {
Path string `json:"path"`
} `json:"issues"`
}
if json.Unmarshal(raw, &result) == nil && result.Valid && runErr == nil {
return nil
}
fields := []string{}
for _, issue := range result.Issues {
if issue.Path != "" {
fields = append(fields, issue.Path)
}
}
if len(fields) == 0 {
fields = append(fields, "openclaw.json")
}
return feishuConfigError(strings.Join(fields, ", "))
}
func backupFeishuConfig(configPath string, original []byte) (string, error) {
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-backup-*.json")
if err != nil {
return "", err
}
if _, err = f.Write(original); err == nil {
err = f.Sync()
}
closeErr := f.Close()
if err != nil {
return "", err
}
if closeErr != nil {
return "", closeErr
}
return f.Name(), nil
}
// Doctor can spend 94 seconds waiting for the previous container's owner lease.
func waitOpenclawReady(name string) error {
script := `const fs=require('fs');const c=JSON.parse(fs.readFileSync('/home/node/.openclaw/openclaw.json','utf8'));const tls=c.gateway?.tls?.enabled===true;const h=require(tls?'https':'http');function check(p){const r=h.get({hostname:'127.0.0.1',port:c.gateway?.port||18789,path:p,rejectUnauthorized:false,timeout:3000},s=>{s.resume();if(s.statusCode===404&&p==='/readyz')return check('/healthz');process.exitCode=s.statusCode===200?0:1});r.on('timeout',()=>r.destroy());r.on('error',()=>process.exitCode=1)}check('/readyz')`
deadline := time.Now().Add(4 * time.Minute)
for time.Now().Before(deadline) {
if cmd.RunDockerExec(8*time.Second, name, "node", "-e", script) == nil {
return nil
}
time.Sleep(2 * time.Second)
}
return buserr.New("ErrFeishuActivation")
}
func verifyFeishuActivation(name string, enabled bool) error {
if err := waitOpenclawReady(name); err != nil {
return err
}
output, err := cmd.RunDockerExecWithStdout(time.Minute, name, "openclaw", "plugins", "list", "--json")
if err != nil {
return buserr.New("ErrFeishuActivation")
}
raw, _ := extractEmbeddedJSON(output)
var result struct {
Plugins []struct {
ID string `json:"id"`
Status string `json:"status"`
Origin string `json:"origin"`
} `json:"plugins"`
}
if json.Unmarshal(raw, &result) != nil {
return buserr.New("ErrFeishuActivation")
}
found := false
for _, p := range result.Plugins {
if p.ID == "openclaw-lark" && p.Status != "disabled" {
return buserr.New("ErrFeishuActivation")
}
if p.ID == "feishu" && p.Origin == "bundled" && (p.Status == "loaded" || (!enabled && p.Status == "disabled")) {
found = true
}
}
if !found {
return buserr.New("ErrFeishuActivation")
}
health, err := cmd.RunDockerExecWithStdout(20*time.Second, name, "openclaw", "gateway", "health", "--json")
if err != nil || !feishuGatewayReady(health, enabled) {
return buserr.New("ErrFeishuActivation")
}
return nil
}
func readFeishuConfigBytes(file string) ([]byte, map[string]interface{}, error) {
info, err := os.Lstat(file)
if err != nil {
return nil, nil, err
}
if !info.Mode().IsRegular() {
return nil, nil, feishuConfigError("openclaw.json")
}
f, err := os.Open(file)
if err != nil {
return nil, nil, err
}
defer f.Close()
raw, err := io.ReadAll(f)
if err != nil {
return nil, nil, err
}
var conf map[string]interface{}
if err = json.Unmarshal(raw, &conf); err != nil || conf == nil {
return nil, nil, feishuConfigError("openclaw.json")
}
return raw, conf, nil
}
+6
View File
@@ -107,6 +107,9 @@ func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) e
if err != nil { if err != nil {
return err 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 { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err return err
} }
@@ -132,6 +135,9 @@ func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
if err != nil { if err != nil {
return err 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 { if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err return err
} }
+4
View File
@@ -148,6 +148,10 @@ func readOpenclawConfig(configPath string) (map[string]interface{}, error) {
} }
func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error { 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) ensureGatewaySecurityDefaults(conf)
ensureOpenclawUpdateDefaults(conf) ensureOpenclawUpdateDefaults(conf)
payload, err := json.MarshalIndent(conf, "", " ") payload, err := json.MarshalIndent(conf, "", " ")
+26 -10
View File
@@ -305,13 +305,14 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return syncAppInstallStatus(&install, true) return syncAppInstallStatus(&install, true)
case constant.Upgrade: case constant.Upgrade:
upgradeReq := request.AppInstallUpgrade{ upgradeReq := request.AppInstallUpgrade{
InstallID: install.ID, FeishuTransition: req.FeishuTransition,
DetailID: req.DetailId, InstallID: install.ID,
Backup: req.Backup, DetailID: req.DetailId,
PullImage: req.PullImage, Backup: req.Backup,
DeleteImage: req.DeleteImage, PullImage: req.PullImage,
DockerCompose: req.DockerCompose, DeleteImage: req.DeleteImage,
TaskID: req.TaskID, DockerCompose: req.DockerCompose,
TaskID: req.TaskID,
} }
return upgradeInstall(upgradeReq) return upgradeInstall(upgradeReq)
case constant.Reload: case constant.Reload:
@@ -716,6 +717,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
if err != nil { if err != nil {
return versions, err return versions, err
} }
var feishuLegacy, feishuConfigured, feishuChecked bool
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID)) details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
if err != nil { if err != nil {
return versions, err return versions, err
@@ -740,6 +742,18 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage) canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
} }
if canUpgrade { 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 var newCompose string
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() { if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
filename := filepath.Base(detail.DownloadUrl) filename := filepath.Base(detail.DownloadUrl)
@@ -774,9 +788,11 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
} }
} }
versions = append(versions, dto.AppVersion{ versions = append(versions, dto.AppVersion{
Version: detail.Version, FeishuLegacy: feishuTarget && feishuLegacy,
DetailId: detail.ID, FeishuConfigured: feishuTarget && feishuConfigured,
DockerCompose: newCompose, Version: detail.Version,
DetailId: detail.ID,
DockerCompose: newCompose,
}) })
} }
} }
+74 -4
View File
@@ -60,10 +60,16 @@ type upgradeFileSnapshot struct {
} }
type appUpgradeContext struct { type appUpgradeContext struct {
req request.AppInstallUpgrade openclawOriginal []byte
original model.AppInstall feishuOriginal []byte
candidate model.AppInstall feishuImage string
detail model.AppDetail feishuOldImage string
feishuEnabled bool
feishuLockPath string
req request.AppInstallUpgrade
original model.AppInstall
candidate model.AppInstall
detail model.AppDetail
phase appUpgradePhase phase appUpgradePhase
stopAttempted bool 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)) detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil { if err != nil {
return err return err
@@ -129,6 +148,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
return err return err
} }
ctx := &appUpgradeContext{ ctx := &appUpgradeContext{
feishuLockPath: feishuLockPath,
req: req, req: req,
original: install, original: install,
candidate: 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 { if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
return err 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 { if u.original.App.Key == constant.AppOpenresty {
for _, relativePath := range []string{ for _, relativePath := range []string{
@@ -330,6 +354,9 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
if u.original.App.Resource == constant.AppResourceRemote { if u.original.App.Resource == constant.AppResourceRemote {
go RequestDownloadCallBack(u.detail.DownloadCallBackUrl) go RequestDownloadCallBack(u.detail.DownloadCallBackUrl)
} }
if err = u.prepareFeishuUpgrade(); err != nil {
return err
}
u.phase = appUpgradePrepared u.phase = appUpgradePrepared
return nil return nil
} }
@@ -387,6 +414,9 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
} }
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop")) t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
u.phase = appUpgradeStopped u.phase = appUpgradeStopped
if err := u.checkFeishuUpgradeSource(); err != nil {
return err
}
var err error var err error
if u.original.App.Key == constant.AppOpenresty { if u.original.App.Key == constant.AppOpenresty {
@@ -454,6 +484,9 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
} else { } else {
t.Log("WARNING: no containers found for the upgraded application") 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.Status = constant.StatusRunning
u.candidate.Message = "" u.candidate.Message = ""
@@ -579,8 +612,34 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
if out != "" { if out != "" {
err = fmt.Errorf("%s: %w", out, err) err = fmt.Errorf("%s: %w", out, err)
} }
if u.feishuOriginal != nil {
return err
}
rollbackErr = 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 != "" { if u.backupFile != "" {
_ = u.restoreManagedFiles() _ = u.restoreManagedFiles()
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil { 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 { func (u *appUpgradeContext) finishRollback() error {
if u.feishuOriginal != nil {
if err := waitOpenclawReady(u.original.ContainerName); err != nil {
return err
}
}
restored := u.original restored := u.original
if err := appInstallRepo.Save(context.Background(), &restored); err != nil { if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
return err return err
@@ -643,6 +707,12 @@ func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
} }
func (u *appUpgradeContext) cleanup() { func (u *appUpgradeContext) cleanup() {
if u.feishuLockPath != "" {
feishuSwitches.Delete(u.feishuLockPath)
}
if u.feishuOriginal != nil && u.rollbackErr != nil {
return
}
if u.snapshot != nil { if u.snapshot != nil {
u.snapshot.Cleanup() u.snapshot.Cleanup()
} }
+336
View File
@@ -0,0 +1,336 @@
package service
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"gopkg.in/yaml.v3"
)
func classifyLegacyFeishu(conf map[string]interface{}, installPath string) (legacy, configured bool, err error) {
for _, field := range []string{"channels", "channels.feishu", "plugins", "plugins.entries", "plugins.entries.openclaw-lark", "plugins.entries.feishu"} {
parts := strings.Split(field, ".")
parent := conf
for _, part := range parts[:len(parts)-1] {
parent = childMap(parent, part)
}
if raw, exists := parent[parts[len(parts)-1]]; exists {
if _, ok := raw.(map[string]interface{}); !ok {
return false, false, feishuConfigError(field)
}
}
}
plugins := childMap(conf, "plugins")
for _, key := range []string{"allow", "deny"} {
if raw, exists := plugins[key]; exists {
if _, ok := raw.([]string); ok {
continue
}
ids, ok := raw.([]interface{})
if !ok {
return false, false, feishuConfigError("plugins." + key)
}
for _, id := range ids {
if _, ok := id.(string); !ok {
return false, false, feishuConfigError("plugins." + key)
}
}
}
}
for _, id := range []string{"openclaw-lark", "feishu"} {
if raw, exists := childMap(childMap(plugins, "entries"), id)["enabled"]; exists {
if _, ok := raw.(bool); !ok {
return false, false, feishuConfigError("plugins.entries." + id + ".enabled")
}
}
}
if usesBundledFeishu(conf) {
return false, false, nil
}
f := getChannelConfig(conf, "feishu")
configured = len(f) > 0
if raw, exists := conf["bindings"]; exists {
bindings, ok := raw.([]interface{})
if !ok {
return false, false, feishuConfigError("bindings")
}
for _, raw := range bindings {
binding, ok := raw.(map[string]interface{})
if !ok {
return false, false, feishuConfigError("bindings")
}
if match, exists := binding["match"]; exists {
if _, ok := match.(map[string]interface{}); !ok {
return false, false, feishuConfigError("bindings.match")
}
}
if extractStringValue(childMap(binding, "match")["channel"]) == "feishu" {
configured = true
}
}
}
entries := childMap(childMap(conf, "plugins"), "entries")
entry, selected := entries["openclaw-lark"]
if selected {
old, ok := entry.(map[string]interface{})
if !ok {
return false, configured, feishuConfigError("plugins.entries.openclaw-lark")
}
// A completed disable-only repair must not repeat on every upgrade.
if enabled, ok := old["enabled"].(bool); ok && !enabled && !configured {
return false, false, nil
}
}
installed, err := checkPluginInstalled(installPath, "feishu")
if err != nil {
return false, configured, err
}
_, oldThread := f["threadSession"]
_, oldReply := f["replyMode"]
_, oldStreaming := f["streaming"].(bool)
return selected || installed || oldThread || oldReply || oldStreaming, configured, nil
}
func prepareLegacyFeishu(conf map[string]interface{}, configured bool, transition *dto.AgentFeishuTransition) error {
if !configured {
ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false
return nil
}
if transition == nil || !transition.Confirmed {
return buserr.New("ErrFeishuUpgradeConfirm")
}
if !validFeishuTransition(transition.GroupSessionScope, transition.RequireMention) {
return feishuConfigError("feishuTransition")
}
return convertLegacyFeishu(conf, dto.AgentFeishuSwitchReq{Streaming: transition.Streaming, GroupSessionScope: transition.GroupSessionScope, RequireMention: transition.RequireMention})
}
func validFeishuTransition(scope, mention string) bool {
return (scope == "group" || scope == "group_sender" || scope == "group_topic" || scope == "group_topic_sender") && (mention == "true" || mention == "false")
}
func feishuRecoveryPayload(candidate []byte, configured bool) ([]byte, error) {
var conf map[string]interface{}
if err := json.Unmarshal(candidate, &conf); err != nil {
return nil, err
}
if configured {
ensureChildMap(ensureChildMap(conf, "channels"), "feishu")["enabled"] = false
selectBundledFeishu(conf, false)
}
ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false
return json.MarshalIndent(conf, "", " ")
}
func (u *appUpgradeContext) prepareFeishuUpgrade() error {
if u.original.App.Key != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(u.candidate.Version) {
return nil
}
source := filepath.Join(u.original.GetPath(), "data/conf/openclaw.json")
staged := filepath.Join(u.stageDir, "data/conf/openclaw.json")
// The staged copy predates protocol migration and is the exact cutover comparison source.
original := u.openclawOriginal
current, err := os.ReadFile(source)
if err != nil {
return err
}
if !bytes.Equal(original, current) {
return buserr.New("ErrFeishuConfigChanged")
}
_, conf, err := readFeishuConfigBytes(staged)
if err != nil {
return err
}
legacy, configured, err := classifyLegacyFeishu(conf, u.original.GetPath())
if err != nil || !legacy {
return err
}
if !u.req.Backup {
return buserr.New("ErrFeishuUpgradeBackup")
}
if err = prepareLegacyFeishu(conf, configured, u.req.FeishuTransition); err != nil {
return err
}
project, err := docker.GetComposeProject(u.original.Name, u.original.GetPath(), []byte(u.candidate.DockerCompose), u.envContent, false)
if err != nil {
return err
}
if len(project.Services) != 1 {
return buserr.New("ErrFeishuUpgradeMounts")
}
for _, service := range project.Services {
if service.Build != nil {
return buserr.New("ErrFeishuImageRequired")
}
u.feishuImage, err = bundledFeishuImage(service.Image)
if err != nil {
return err
}
configMounted := false
for _, v := range service.Volumes {
if v.Target == "/etc/localtime" {
continue
}
if v.Type != "bind" {
return buserr.New("ErrFeishuUpgradeMounts")
}
if err = feishuBackupPath(u.original.GetPath(), v.Source); err != nil {
return err
}
if v.Target == "/home/node/.openclaw" {
configMounted = filepath.Clean(v.Source) == filepath.Dir(source)
}
}
if !configMounted {
return buserr.New("ErrFeishuUpgradeMounts")
}
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
info, err := cli.ContainerInspect(ctx, u.original.ContainerName)
if err != nil {
return err
}
u.feishuOldImage = info.Image
for _, m := range info.Mounts {
if m.Destination == "/etc/localtime" {
continue
}
if string(m.Type) != "bind" {
return buserr.New("ErrFeishuUpgradeMounts")
}
if err = feishuBackupPath(u.original.GetPath(), m.Source); err != nil {
return err
}
}
// Tar preserves symlinks: an external target would not be in the application backup.
if err = filepath.WalkDir(filepath.Join(u.original.GetPath(), "data"), func(file string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.Type()&os.ModeSymlink != 0 {
return feishuBackupPath(u.original.GetPath(), file)
}
return nil
}); err != nil {
return err
}
u.feishuOriginal = original
u.feishuEnabled = configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
before, err := os.ReadFile(staged)
if err != nil {
return err
}
if err = replaceFeishuConfig(staged, before, payload); err != nil {
return err
}
targetCompose := filepath.Join(u.stageDir, "docker-compose.yml")
if err = pinFeishuRollbackImage(targetCompose, u.feishuImage); err != nil {
return err
}
pinned, err := os.ReadFile(targetCompose)
if err != nil {
return err
}
u.candidate.DockerCompose = string(pinned)
return nil
}
func feishuBackupPath(root, source string) error {
root, err := filepath.EvalSymlinks(root)
if err != nil {
return err
}
resolved, err := filepath.EvalSymlinks(source)
if err != nil {
return buserr.New("ErrFeishuUpgradeMounts")
}
rel, err := filepath.Rel(root, resolved)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return buserr.New("ErrFeishuUpgradeMounts")
}
return nil
}
func (u *appUpgradeContext) checkFeishuUpgradeSource() error {
if u.feishuOriginal == nil {
return nil
}
current, err := os.ReadFile(filepath.Join(u.original.GetPath(), "data/conf/openclaw.json"))
if err != nil {
return err
}
if !bytes.Equal(current, u.feishuOriginal) {
return buserr.New("ErrFeishuConfigChanged")
}
return nil
}
func (u *appUpgradeContext) verifyFeishuUpgrade(t *task.Task) error {
if u.feishuOriginal == nil {
return nil
}
if err := verifyFeishuActivation(u.candidate.ContainerName, u.feishuEnabled); err != nil {
return err
}
if u.feishuEnabled {
probeFeishuConnection(u.candidate.ContainerName, t)
}
return nil
}
func probeFeishuConnection(name string, t *task.Task) {
output, err := cmd.RunDockerExecWithStdout(30*time.Second, name, "openclaw", "channels", "status", "--channel", "feishu", "--probe", "--json")
if err != nil || !feishuProbeOK(output) {
t.Log(i18n.GetMsgByKey("AgentFeishuProbeWarning"))
}
}
// Pin the restored service to the actual old image, even if its registry tag moved.
func pinFeishuRollbackImage(composeFile string, image string) error {
raw, err := os.ReadFile(composeFile)
if err != nil {
return err
}
var conf map[string]interface{}
if err = yaml.Unmarshal(raw, &conf); err != nil {
return err
}
services := childMap(conf, "services")
if len(services) != 1 {
return buserr.New("ErrFeishuUpgradeMounts")
}
for _, raw := range services {
service, ok := raw.(map[string]interface{})
if !ok {
return buserr.New("ErrFeishuUpgradeMounts")
}
service["image"] = image
}
payload, err := yaml.Marshal(conf)
if err != nil {
return err
}
return writeUpgradeFile(composeFile, payload, constant.FilePerm)
}
+16 -4
View File
@@ -197,7 +197,8 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
return nil 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 // Validate AI Gateway backups before stopping the app, creating rollback
// backups, or touching its files/database. Keep the validated extraction for // backups, or touching its files/database. Keep the validated extraction for
// the restore so uploaded/encrypted archives are not unpacked twice. // 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 return err
} }
defer func() { defer func() {
defer os.RemoveAll(tmpPath)
if !startAfter {
return
}
if isRollback { if isRollback {
_, _ = compose.UpWithoutBuild(install.GetComposePath()) _, _ = compose.UpWithoutBuild(install.GetComposePath())
} else { } else {
_, _ = compose.Up(install.GetComposePath()) _, _ = compose.Up(install.GetComposePath())
} }
_ = os.RemoveAll(tmpPath)
}() }()
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") { 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() appDir := install.GetPath()
backPath := fmt.Sprintf("%s_bak", appDir) backPath := fmt.Sprintf("%s_bak", appDir)
_ = fileOp.Rename(appDir, backPath) if err := fileOp.Rename(appDir, backPath); err != nil && !startAfter {
_ = fileOp.CreateDir(appDir, constant.DirPerm) return err
}
if err := fileOp.CreateDir(appDir, constant.DirPerm); err != nil && !startAfter {
return err
}
deCompressName := i18n.GetWithName("DeCompressFile", "app.tar.gz") deCompressName := i18n.GetWithName("DeCompressFile", "app.tar.gz")
t.LogStart(deCompressName) t.LogStart(deCompressName)
@@ -376,6 +384,10 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
_, _ = file.WriteString(mergedEnvContent) _, _ = file.WriteString(mergedEnvContent)
} }
// The upgrade transaction verifies readiness before saving its restored install.
if !startAfter {
return nil
}
backupInstall.ID = install.ID backupInstall.ID = install.ID
backupInstall.Status = constant.StatusRunning backupInstall.Status = constant.StatusRunning
backupInstall.AppId = install.AppId backupInstall.AppId = install.AppId
+23
View File
@@ -140,6 +140,29 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
backData = records[start:end] backData = records[start:end]
} }
// Batch-load persisted checks; never contact Docker Hub while rendering the list.
ids := make([]string, 0, len(backData))
for _, row := range backData {
ids = append(ids, row.ContainerID)
}
if len(ids) > 0 {
var checks []model.ContainerImageCheckUpdate
if err := global.DB.Where("container_id IN ?", ids).Find(&checks).Error; err != nil {
return 0, nil, err
}
byID := make(map[string]model.ContainerImageCheckUpdate, len(checks))
for _, check := range checks {
byID[check.ContainerID] = check
}
for i := range backData {
check, ok := byID[backData[i].ContainerID]
if ok && sameContainerImage(check.ImageID, backData[i].ImageId) && check.ImageName == backData[i].ImageName {
backData[i].LatestVersion = check.LatestVersion
backData[i].TargetImage = check.TargetImage
backData[i].HasUpdate = check.HasUpdate
}
}
}
for i := 0; i < len(backData); i++ { for i := 0; i < len(backData); i++ {
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name)) install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
if install.ID > 0 { if install.ID > 0 {
@@ -0,0 +1,379 @@
package service
import (
"context"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/dockerhub"
"github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const imageCheckUpdateSetting = "ContainerImageUpdateEnabled"
type ImageCheckUpdateStatus struct {
TaskID string `json:"taskID"`
Enabled bool `json:"enabled"`
Running bool `json:"running"`
Checked int `json:"checked"`
Updates int `json:"updates"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
Error string `json:"error"`
FinishedAt int64 `json:"finishedAt"`
}
var imageCheckUpdateState struct {
sync.Mutex
status ImageCheckUpdateStatus
}
func GetContainerImageCheckUpdateStatus() (ImageCheckUpdateStatus, error) {
var setting model.Setting
if err := global.DB.Where("key = ?", imageCheckUpdateSetting).First(&setting).Error; err != nil {
return ImageCheckUpdateStatus{}, err
}
imageCheckUpdateState.Lock()
defer imageCheckUpdateState.Unlock()
status := imageCheckUpdateState.status
status.Enabled = setting.Value == "Enable"
return status, nil
}
func SetContainerImageCheckUpdateSchedule(enabled bool) error {
value := "Disable"
if enabled {
value = "Enable"
}
return global.DB.Model(&model.Setting{}).Where("key = ?", imageCheckUpdateSetting).Update("value", value).Error
}
// StartContainerImageCheckUpdate coalesces manual and scheduled runs on this node.
func StartContainerImageCheckUpdate() (string, error) {
imageCheckUpdateState.Lock()
defer imageCheckUpdateState.Unlock()
if imageCheckUpdateState.status.Running {
return imageCheckUpdateState.status.TaskID, nil
}
taskItem, err := task.NewTask(i18n.GetMsgByKey("ContainerImageCheckUpdate"), task.TaskScan, task.TaskScopeImage, "", 0)
if err != nil {
return "", err
}
// Persist before returning the ID so the log drawer can immediately find it.
if err := global.TaskDB.Save(taskItem.Task).Error; err != nil {
_ = taskItem.Logger.Out.(io.Closer).Close()
taskItem.DeleteLogFile()
if cancel, ok := global.LoadTaskCancel(taskItem.TaskID); ok {
cancel()
}
global.RemoveTaskCancel(taskItem.TaskID)
return "", err
}
imageCheckUpdateState.status = ImageCheckUpdateStatus{Running: true, TaskID: taskItem.TaskID}
go func() {
result := ImageCheckUpdateStatus{TaskID: taskItem.TaskID}
defer func() {
result.FinishedAt = time.Now().UnixMilli()
imageCheckUpdateState.Lock()
imageCheckUpdateState.status = result
imageCheckUpdateState.Unlock()
}()
// The action owns its deadline so the task runner waits for cleanup and logs.
taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerImageCheckUpdate"), func(t *task.Task) (err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("image update check failed: %v", recovered)
}
t.Log(i18n.GetMsgWithMap("ContainerImageCheckSummary", map[string]interface{}{
"checked": result.Checked, "updates": result.Updates, "failed": result.Failed, "skipped": result.Skipped,
}))
}()
ctx, cancel := context.WithTimeout(t.TaskCtx, 30*time.Minute)
defer cancel()
if err = runContainerImageCheckUpdate(ctx, &result, t.Log); err != nil {
return err
}
if result.Failed > 0 {
return fmt.Errorf("%s", i18n.GetMsgWithMap("ContainerImageCheckFailures", map[string]interface{}{"count": result.Failed}))
}
return nil
}, nil, 0, 0)
if err := taskItem.Execute(); err != nil {
result.Error = err.Error()
global.LOG.Errorf("container image check: %s", err)
}
}()
return taskItem.TaskID, nil
}
// These caches live only for one check. Remote results do not depend on a
// container's local image ID, which is compared separately for every baseline.
type imageCheckUpdateRepository struct {
client imageCheckUpdateHubClient
err error
tags []string
tagsLoaded bool
tagsErr error
}
type imageCheckUpdateTargetKey struct {
repository, tag string
platform dockerhub.Platform
}
type imageCheckUpdateTargetResult struct {
target dockerhub.Target
compatible bool
err error
}
type imageCheckUpdateResult struct {
record model.ContainerImageCheckUpdate
err error
}
type imageCheckUpdateCache struct {
username, password string
repositories map[string]*imageCheckUpdateRepository
targets map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult
}
func runContainerImageCheckUpdate(ctx context.Context, result *ImageCheckUpdateStatus, logger func(string)) error {
log := func(key string, values map[string]interface{}) {
if logger != nil {
logger(i18n.GetMsgWithMap(key, values))
}
}
cli, err := newImageCheckUpdateDockerClient()
if err != nil {
return err
}
defer cli.Close()
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return err
}
log("ContainerImageCheckScope", map[string]interface{}{"count": len(containers)})
// Load old results once, rather than issuing one SELECT for every failed check.
var previousRows []model.ContainerImageCheckUpdate
if err := global.DB.WithContext(ctx).Find(&previousRows).Error; err != nil {
return err
}
previous := make(map[string]model.ContainerImageCheckUpdate, len(previousRows))
for _, row := range previousRows {
previous[row.ContainerID] = row
}
cache := imageCheckUpdateCache{repositories: map[string]*imageCheckUpdateRepository{}, targets: map[imageCheckUpdateTargetKey]imageCheckUpdateTargetResult{}}
cfg := config.LoadDefaultConfigFile(io.Discard)
if cfg != nil {
for _, host := range []string{"https://index.docker.io/v1/", "docker.io", "registry-1.docker.io"} {
auth, e := cfg.GetAuthConfig(host)
if e == nil && auth.Username != "" {
cache.username, cache.password = auth.Username, auth.Password
break
}
}
}
checks := map[string]imageCheckUpdateResult{}
ids := make([]string, 0, len(containers))
names := make(map[string]string, len(containers))
pending := make([]model.ContainerImageCheckUpdate, 0, 100)
for _, item := range containers {
if err := ctx.Err(); err != nil {
return err
}
ids = append(ids, item.ID)
name := item.ID
if len(item.Names) > 0 {
name = strings.TrimPrefix(item.Names[0], "/")
}
names[item.ID] = name
log("ContainerImageCheckItem", map[string]interface{}{"name": name, "image": item.Image})
repository, tag, supported := dockerhub.Reference(item.Image)
key := repository + ":" + tag + "@" + item.ImageID
entry, found := checks[key]
if !found {
entry.record, entry.err = checkContainerImageUpdate(ctx, cli, item, repository, tag, supported, &cache)
checks[key] = entry
}
record := entry.record
record.ContainerID, record.ImageName = item.ID, item.Image
if entry.err != nil {
record.CheckStatus, record.CheckError = "failed", entry.err.Error()
old := previous[item.ID]
if old.ImageID == item.ImageID && old.ImageName == item.Image {
record.LatestVersion, record.TargetImage, record.TargetImageID, record.HasUpdate = old.LatestVersion, old.TargetImage, old.TargetImageID, old.HasUpdate
}
}
pending = append(pending, record)
if len(pending) == 100 {
if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil {
return err
}
pending = pending[:0]
}
}
if err := saveContainerImageCheckUpdates(ctx, cli, pending, names, result, log); err != nil {
return err
}
query := global.DB.WithContext(ctx).Where("1 = 1")
if len(ids) > 0 {
query = query.Where("container_id NOT IN ?", ids)
}
return query.Delete(&model.ContainerImageCheckUpdate{}).Error
}
func checkContainerImageUpdate(ctx context.Context, cli imageCheckUpdateDockerClient, item container.Summary, repository, tag string, supported bool, cache *imageCheckUpdateCache) (model.ContainerImageCheckUpdate, error) {
record := model.ContainerImageCheckUpdate{ImageID: item.ImageID, ImageName: item.Image, CheckStatus: "success", CheckedAt: time.Now()}
if !supported {
record.CheckStatus = "skipped"
return record, nil
}
inspected, err := cli.ImageInspect(ctx, item.ImageID)
if err != nil {
return record, err
}
repo, loaded := cache.repositories[repository]
if !loaded {
repo = &imageCheckUpdateRepository{}
repo.client, repo.err = newImageCheckUpdateHubClient(ctx, repository, cache.username, cache.password)
cache.repositories[repository] = repo
}
if repo.err != nil {
return record, repo.err
}
if dockerhub.NeedsTags(tag) {
if !repo.tagsLoaded {
repo.tags, repo.tagsErr = repo.client.Tags(ctx)
repo.tagsLoaded = true
}
if repo.tagsErr != nil {
return record, repo.tagsErr
}
}
platform := dockerhub.Platform{OS: inspected.Os, Architecture: inspected.Architecture, Variant: inspected.Variant}
for _, candidate := range dockerhub.Candidates(tag, repo.tags) {
key := imageCheckUpdateTargetKey{repository: repository, tag: candidate, platform: platform}
remote, loaded := cache.targets[key]
if !loaded {
remote.target, remote.compatible, remote.err = repo.client.Resolve(ctx, candidate, platform)
cache.targets[key] = remote
}
if remote.err != nil {
return record, remote.err
}
if !remote.compatible {
continue
}
record.LatestVersion = remote.target.Tag
record.TargetImage = "docker.io/" + repository + ":" + remote.target.Tag
record.TargetImageID = remote.target.ConfigDigest
record.HasUpdate = remote.target.Tag != tag || remote.target.ConfigDigest != inspected.ID
return record, nil
}
return record, fmt.Errorf("no compatible image for %s/%s", platform.OS, platform.Architecture)
}
// Revalidate immediately before each small write batch. Never hold a database
// transaction open while waiting for Docker or Docker Hub.
func saveContainerImageCheckUpdates(ctx context.Context, cli imageCheckUpdateDockerClient, pending []model.ContainerImageCheckUpdate, names map[string]string, result *ImageCheckUpdateStatus, log func(string, map[string]interface{})) error {
records := make([]model.ContainerImageCheckUpdate, 0, len(pending))
for _, record := range pending {
current, err := cli.ContainerInspect(ctx, record.ContainerID)
if err != nil && !errdefs.IsNotFound(err) {
return err
}
if err != nil || current.Image != record.ImageID {
result.Skipped++
log("ContainerImageCheckChanged", map[string]interface{}{"name": names[record.ContainerID]})
continue
}
records = append(records, record)
}
if len(records) == 0 {
return nil
}
if err := global.DB.WithContext(ctx).Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(&records, 100).Error; err != nil {
return err
}
// Count and announce only results that were successfully persisted.
for _, record := range records {
values := map[string]interface{}{"name": names[record.ContainerID], "image": record.ImageName}
switch record.CheckStatus {
case "failed":
result.Failed++
values["error"] = record.CheckError
log("ContainerImageCheckFailed", values)
case "skipped":
result.Skipped++
log("ContainerImageCheckSkipped", values)
default:
result.Checked++
if record.HasUpdate {
result.Updates++
values["target"] = record.TargetImage
log("ContainerImageCheckAvailable", values)
} else {
log("ContainerImageCheckCurrent", values)
}
}
}
return nil
}
// sameContainerImage guards cached results after external image changes.
func sameContainerImage(stored, displayed string) bool {
return strings.TrimPrefix(stored, "sha256:") == strings.TrimPrefix(displayed, "sha256:")
}
// Reconcile only after Docker confirms the replacement container uses the target.
func reconcileContainerImageCheckUpdate(oldID, newID, imageID, imageName string) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
var previous model.ContainerImageCheckUpdate
if err := tx.Where("container_id = ?", oldID).Find(&previous).Error; err != nil {
return err
}
if previous.ContainerID == "" {
return nil
}
if err := tx.Where("container_id = ?", oldID).Delete(&model.ContainerImageCheckUpdate{}).Error; err != nil {
return err
}
if previous.TargetImageID != imageID {
return nil
}
previous.ContainerID = newID
previous.ImageID = imageID
previous.ImageName = imageName
previous.HasUpdate = false
previous.CheckStatus = "success"
previous.CheckError = ""
previous.CheckedAt = time.Now()
return tx.Clauses(clause.OnConflict{UpdateAll: true}).Create(&previous).Error
})
}
type imageCheckUpdateDockerClient interface {
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
ContainerInspect(context.Context, string) (container.InspectResponse, error)
ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error)
Close() error
}
type imageCheckUpdateHubClient interface {
Tags(context.Context) ([]string, error)
Resolve(context.Context, string, dockerhub.Platform) (dockerhub.Target, bool, error)
}
var newImageCheckUpdateDockerClient = func() (imageCheckUpdateDockerClient, error) { return docker.NewDockerClient() }
var newImageCheckUpdateHubClient = func(ctx context.Context, repository, username, password string) (imageCheckUpdateHubClient, error) {
return dockerhub.New(ctx, repository, username, password)
}
+5
View File
@@ -152,6 +152,11 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
upgradeErrors = append(upgradeErrors, upgradeErr) upgradeErrors = append(upgradeErrors, upgradeErr)
return upgradeErr return upgradeErr
} }
if updated, inspectErr := client.ContainerInspect(ctx, item); inspectErr == nil {
if cacheErr := reconcileContainerImageCheckUpdate(oldContainer.ID, updated.ID, updated.Image, updated.Config.Image); cacheErr != nil {
global.LOG.Warnf("refresh container image update state: %v", cacheErr)
}
}
if cleanupErr != nil { if cleanupErr != nil {
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr)) t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
} }
+11
View File
@@ -5,12 +5,14 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/mysql" "github.com/1Panel-dev/1Panel/agent/utils/mysql"
"github.com/1Panel-dev/1Panel/agent/utils/mysql/client" "github.com/1Panel-dev/1Panel/agent/utils/mysql/client"
@@ -126,6 +128,9 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
} }
func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool { func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
if (req.Type == constant.AppMysql || req.Type == constant.AppMariaDB || req.Type == constant.AppPostgresql || req.Type == constant.AppMongodb) && (cmd.CheckIllegal(req.Username, req.Password) || strings.Contains(req.Password, " ")) {
return false
}
if req.Timeout == 0 { if req.Timeout == 0 {
req.Timeout = 30 req.Timeout = 30
} }
@@ -180,6 +185,9 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
} }
func (u *DatabaseService) Create(req dto.DatabaseCreate) error { func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
if (req.Type == constant.AppMysql || req.Type == constant.AppMariaDB || req.Type == constant.AppPostgresql || req.Type == constant.AppMongodb) && (cmd.CheckIllegal(req.Username, req.Password) || strings.Contains(req.Password, " ")) {
return buserr.New("ErrCmdIllegal")
}
if req.Timeout == 0 { if req.Timeout == 0 {
req.Timeout = 30 req.Timeout = 30
} }
@@ -303,6 +311,9 @@ func (u *DatabaseService) Delete(req dto.DatabaseDelete) error {
} }
func (u *DatabaseService) Update(req dto.DatabaseUpdate) error { func (u *DatabaseService) Update(req dto.DatabaseUpdate) error {
if (req.Type == constant.AppMysql || req.Type == constant.AppMariaDB || req.Type == constant.AppPostgresql || req.Type == constant.AppMongodb) && (cmd.CheckIllegal(req.Username, req.Password) || strings.Contains(req.Password, " ")) {
return buserr.New("ErrCmdIllegal")
}
switch req.Type { switch req.Type {
case constant.AppPostgresql: case constant.AppPostgresql:
if _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{ if _, err := postgresql.NewPostgresqlClient(pgclient.DBInfo{
+4 -4
View File
@@ -64,7 +64,7 @@ func (u *MongodbService) UpdateDescription(req dto.UpdateDescription) error {
} }
func (u *MongodbService) Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error) { func (u *MongodbService) Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error) {
if cmd.CheckIllegal(req.Name, req.Username, req.Password) { if cmd.CheckIllegal(req.Name, req.Username, req.Password) || strings.Contains(req.Password, " ") {
return nil, buserr.New("ErrCmdIllegal") return nil, buserr.New("ErrCmdIllegal")
} }
if !isSupportedMongodbPrivilege(req.Permission) { if !isSupportedMongodbPrivilege(req.Permission) {
@@ -146,7 +146,7 @@ func (u *MongodbService) LoadFromRemote(req dto.MongodbLoadDB) error {
} }
func (u *MongodbService) BindUser(req dto.MongodbBind) error { func (u *MongodbService) BindUser(req dto.MongodbBind) error {
if cmd.CheckIllegal(req.Database, req.Name, req.Username, req.Password) { if cmd.CheckIllegal(req.Database, req.Name, req.Username, req.Password) || strings.Contains(req.Password, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
@@ -168,7 +168,7 @@ func (u *MongodbService) BindUser(req dto.MongodbBind) error {
} }
func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error { func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error {
if cmd.CheckIllegal(req.Database, req.Name, req.Password) { if cmd.CheckIllegal(req.Database, req.Name, req.Password) || strings.Contains(req.Password, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
@@ -190,7 +190,7 @@ func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error {
} }
func (u *MongodbService) ChangeRootPassword(req dto.ChangeDBInfo) error { func (u *MongodbService) ChangeRootPassword(req dto.ChangeDBInfo) error {
if cmd.CheckIllegal(req.Value) { if cmd.CheckIllegal(req.Value) || strings.Contains(req.Value, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
if req.From != constant.AppResourceLocal { if req.From != constant.AppResourceLocal {
+5 -5
View File
@@ -437,7 +437,7 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
} }
func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBounded bool) (*model.DatabaseMysql, error) { func (u *MysqlService) create(ctx context.Context, req dto.MysqlDBCreate, mcpBounded bool) (*model.DatabaseMysql, error) {
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) { if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) || strings.Contains(req.Password, " ") {
return nil, buserr.New("ErrCmdIllegal") return nil, buserr.New("ErrCmdIllegal")
} }
if len(req.Username) != 0 && len(req.Password) == 0 { if len(req.Username) != 0 && len(req.Password) == 0 {
@@ -680,7 +680,7 @@ func (u *MysqlService) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[st
} }
func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error { func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
if cmd.CheckIllegal(req.Username, req.Password, req.Host) { if cmd.CheckIllegal(req.Username, req.Password, req.Host) || strings.Contains(req.Password, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
dbs := make([]string, 0, len(req.DBs)) dbs := make([]string, 0, len(req.DBs))
@@ -883,7 +883,7 @@ func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
} }
func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error { func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
if cmd.CheckIllegal(req.Username, req.Host, req.Password) { if cmd.CheckIllegal(req.Username, req.Host, req.Password) || strings.Contains(req.Password, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
if err := checkMysqlNormalUser(req.Username); err != nil { if err := checkMysqlNormalUser(req.Username); err != nil {
@@ -936,7 +936,7 @@ func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
} }
func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error { func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error {
if cmd.CheckIllegal(req.Username, req.Host, req.Password) { if cmd.CheckIllegal(req.Username, req.Host, req.Password) || strings.Contains(req.Password, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
if err := checkMysqlNormalUser(req.Username); err != nil { if err := checkMysqlNormalUser(req.Username); err != nil {
@@ -1302,7 +1302,7 @@ func isMysqlDatabaseResourceInUseError(err error) bool {
} }
func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error { func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error {
if cmd.CheckIllegal(req.Value) { if cmd.CheckIllegal(req.Value) || strings.Contains(req.Value, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
cli, version, err := LoadMysqlClientByFrom(req.Database) cli, version, err := LoadMysqlClientByFrom(req.Database)
+3 -3
View File
@@ -87,7 +87,7 @@ func (u *PostgresqlService) ListDBOption() ([]dto.PostgresqlOption, error) {
} }
func (u *PostgresqlService) BindUser(req dto.PostgresqlBindUser) error { func (u *PostgresqlService) BindUser(req dto.PostgresqlBindUser) error {
if cmd.CheckIllegal(req.Name, req.Username, req.Password) { if cmd.CheckIllegal(req.Name, req.Username, req.Password) || strings.Contains(req.Password, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(req.Database), repo.WithByName(req.Name)) dbItem, err := postgresqlRepo.Get(postgresqlRepo.WithByPostgresqlName(req.Database), repo.WithByName(req.Name))
@@ -123,7 +123,7 @@ func (u *PostgresqlService) BindUser(req dto.PostgresqlBindUser) error {
} }
func (u *PostgresqlService) Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) { func (u *PostgresqlService) Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error) {
if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format) { if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format) || strings.Contains(req.Password, " ") {
return nil, buserr.New("ErrCmdIllegal") return nil, buserr.New("ErrCmdIllegal")
} }
@@ -366,7 +366,7 @@ func (u *PostgresqlService) ChangePrivileges(req dto.PostgresqlPrivileges) error
} }
func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error { func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error {
if cmd.CheckIllegal(req.Value) { if cmd.CheckIllegal(req.Value) || strings.Contains(req.Value, " ") {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
cli, err := LoadPostgresqlClientByFrom(req.Database) cli, err := LoadPostgresqlClientByFrom(req.Database)
+23 -2
View File
@@ -647,7 +647,7 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
return &res, nil return &res, nil
} }
func (r *RuntimeService) Update(req request.RuntimeUpdate) error { func (r *RuntimeService) Update(req request.RuntimeUpdate) (err error) {
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(req.ID)) runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
@@ -656,6 +656,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
runtime.Version = req.Version runtime.Version = req.Version
return runtimeRepo.Save(runtime) return runtimeRepo.Save(runtime)
} }
oldRuntime := *runtime
appDetail, err := getRuntimeUpdateAppDetail(runtime, req) appDetail, err := getRuntimeUpdateAppDetail(runtime, req)
if err != nil { if err != nil {
return err return err
@@ -700,6 +701,20 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err = downloadApp(app, appDetail, nil, nil); err != nil { if err = downloadApp(app, appDetail, nil, nil); err != nil {
return err return err
} }
var snapshot *runtimeUpdateSnapshot
if runtime.Type != constant.RuntimePHP {
snapshot, err = snapshotRuntimeUpdate(&oldRuntime)
if err != nil {
return err
}
defer func() {
if err != nil {
if restoreErr := snapshot.restoreFiles(); restoreErr != nil {
err = fmt.Errorf("%w; restore runtime configuration: %v", err, restoreErr)
}
}
}()
}
if versionChanged { if versionChanged {
if err = updateRuntimeVersionFiles(runtime, appVersionDir); err != nil { if err = updateRuntimeVersionFiles(runtime, appVersionDir); err != nil {
return err return err
@@ -773,7 +788,13 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err = runtimeRepo.Save(runtime); err != nil { if err = runtimeRepo.Save(runtime); err != nil {
return err return err
} }
go reCreateRuntime(runtime) go func() {
if err := prepareRuntimeUpdate(runtime, snapshot); err != nil {
global.LOG.Errorf("prepare runtime [%s] update failed: %v", runtime.Name, err)
return
}
reCreateRuntime(runtime)
}()
} }
return nil return nil
} }
+86
View File
@@ -1,6 +1,9 @@
package service package service
import ( import (
"errors"
"fmt"
"os"
"path/filepath" "path/filepath"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
@@ -12,6 +15,89 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// Keep the on-disk configuration as well as the model: files may contain edits
// that have not been reflected in the database.
type runtimeUpdateSnapshot struct {
runtime model.Runtime
files []runtimeUpdateFile
}
type runtimeUpdateFile struct {
path string
content []byte
mode os.FileMode
exists bool
}
func snapshotRuntimeUpdate(runtime *model.Runtime) (*runtimeUpdateSnapshot, error) {
snapshot := &runtimeUpdateSnapshot{runtime: *runtime}
for _, name := range []string{"docker-compose.yml", ".env", "run.sh"} {
file := runtimeUpdateFile{path: filepath.Join(runtime.GetPath(), name)}
info, err := os.Stat(file.path)
if errors.Is(err, os.ErrNotExist) {
snapshot.files = append(snapshot.files, file)
continue
}
if err != nil {
return nil, err
}
file.content, err = os.ReadFile(file.path)
if err != nil {
return nil, err
}
file.exists, file.mode = true, info.Mode().Perm()
snapshot.files = append(snapshot.files, file)
}
return snapshot, nil
}
func (s *runtimeUpdateSnapshot) restoreFiles() error {
var result error
for _, file := range s.files {
var err error
if file.exists {
err = os.WriteFile(file.path, file.content, file.mode)
if err == nil {
err = os.Chmod(file.path, file.mode)
}
} else {
err = os.Remove(file.path)
if errors.Is(err, os.ErrNotExist) {
err = nil
}
}
if err != nil {
result = errors.Join(result, fmt.Errorf("restore %s: %w", file.path, err))
}
}
return result
}
// This must finish before reCreateRuntime stops the existing container. A
// failed preparation restores the previous version without touching Docker.
func prepareRuntimeUpdate(runtime *model.Runtime, snapshot *runtimeUpdateSnapshot) error {
if err := pullRuntimeComposeImages(runtime.GetComposePath(), false, nil); err != nil {
result := fmt.Errorf("prepare runtime images: %w", err)
restoreErr := snapshot.restoreFiles()
*runtime = snapshot.runtime
if restoreErr != nil {
result = errors.Join(result, restoreErr)
runtime.Status = constant.StatusError
}
runtime.Message = result.Error()
if saveErr := runtimeRepo.Save(runtime); saveErr != nil {
result = errors.Join(result, fmt.Errorf("restore runtime record: %w", saveErr))
}
logFile, logErr := os.OpenFile(runtime.GetLogPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
if logErr == nil {
_, logErr = fmt.Fprintln(logFile, result.Error())
logErr = errors.Join(logErr, logFile.Close())
}
return errors.Join(result, logErr)
}
return nil
}
func getRuntimeUpdateAppDetail(runtime *model.Runtime, req request.RuntimeUpdate) (model.AppDetail, error) { func getRuntimeUpdateAppDetail(runtime *model.Runtime, req request.RuntimeUpdate) (model.AppDetail, error) {
current, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID)) current, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err != nil { if err != nil {
+5 -1
View File
@@ -77,7 +77,11 @@ func Run() {
global.LOG.Errorf("generate random hour failed: %v", err) global.LOG.Errorf("generate random hour failed: %v", err)
hourRand = big.NewInt(0) hourRand = big.NewInt(0)
} }
if _, err := global.Cron.AddJob(fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64()), job.NewAppStoreJob()); err != nil { appSyncSchedule := fmt.Sprintf("%v %v * * *", minuteRand.Int64(), hourRand.Int64())
if _, err := global.Cron.AddJob(appSyncSchedule, job.NewContainerImageCheckUpdateJob()); err != nil {
global.LOG.Errorf("cannot add container image update cron job: %v", err)
}
if _, err := global.Cron.AddJob(appSyncSchedule, job.NewAppStoreJob()); err != nil {
global.LOG.Errorf("can not add appstore corn job: %s", err.Error()) global.LOG.Errorf("can not add appstore corn job: %s", err.Error())
} }
if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil { if _, err := global.Cron.AddJob("0 3 */31 * *", job.NewBackupJob()); err != nil {
@@ -0,0 +1,24 @@
package job
import (
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
)
type containerImageCheckUpdate struct{}
func NewContainerImageCheckUpdateJob() *containerImageCheckUpdate {
return &containerImageCheckUpdate{}
}
func (j *containerImageCheckUpdate) Run() {
status, err := service.GetContainerImageCheckUpdateStatus()
if err != nil {
global.LOG.Errorf("load container image update schedule: %v", err)
return
}
if status.Enabled {
if _, err := service.StartContainerImageCheckUpdate(); err != nil {
global.LOG.Errorf("start container image update check: %v", err)
}
}
}
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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."
ContainerImageCheckUpdate: "Check for image updates"
ContainerImageCheckScope: "Checking all {{ .count }} containers on this node, regardless of pagination or filters"
ContainerImageCheckItem: "Checking container {{ .name }}, image {{ .image }}"
ContainerImageCheckFailed: "Container {{ .name }} check failed: {{ .error }}; retaining previous results matching the current image"
ContainerImageCheckSkipped: "Skipping {{ .name }}: not a tagged Docker Hub image, a local image, or pinned digest"
ContainerImageCheckAvailable: "Container {{ .name }} has an update: {{ .image }} -> {{ .target }}"
ContainerImageCheckCurrent: "No update found for container {{ .name }}"
ContainerImageCheckChanged: "Container {{ .name }} was removed or its image changed; result was not saved"
ContainerImageCheckSummary: "Check finished: {{ .checked }} checked, {{ .updates }} upgradable, {{ .failed }} failed, {{ .skipped }} skipped"
ContainerImageCheckFailures: "{{ .count }} container checks failed; see the task log"
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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."
ContainerImageCheckUpdate: "Buscar actualizaciones de imágenes"
ContainerImageCheckScope: "Comprobando los {{ .count }} contenedores de este nodo, sin aplicar paginación ni filtros"
ContainerImageCheckItem: "Comprobando el contenedor {{ .name }}, imagen {{ .image }}"
ContainerImageCheckFailed: "Error al comprobar el contenedor {{ .name }}: {{ .error }}; se conservan los resultados anteriores correspondientes a la imagen actual"
ContainerImageCheckSkipped: "Se omite {{ .name }}: no es una imagen de Docker Hub con etiqueta, es una imagen local o tiene un digest fijo"
ContainerImageCheckAvailable: "El contenedor {{ .name }} tiene una actualización: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "No se encontraron actualizaciones para el contenedor {{ .name }}"
ContainerImageCheckChanged: "El contenedor {{ .name }} se eliminó o su imagen cambió; no se guardó el resultado"
ContainerImageCheckSummary: "Comprobación finalizada: comprobados {{ .checked }}, actualizables {{ .updates }}, fallidos {{ .failed }}, omitidos {{ .skipped }}"
ContainerImageCheckFailures: "Falló la comprobación de {{ .count }} contenedores; consulte el registro de la tarea"
+34
View File
@@ -778,3 +778,37 @@ 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." 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." 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 غیرفعال است؛ پشتیبان تنظیمات اصلی حفظ شد."
ContainerImageCheckUpdate: "بررسی به‌روزرسانی ایمیج‌ها"
ContainerImageCheckScope: "در حال بررسی همه {{ .count }} کانتینر این گره، بدون توجه به صفحه‌بندی و فیلترها"
ContainerImageCheckItem: "در حال بررسی کانتینر {{ .name }}، ایمیج {{ .image }}"
ContainerImageCheckFailed: "بررسی کانتینر {{ .name }} ناموفق بود: {{ .error }}؛ نتایج قبلیِ منطبق با ایمیج فعلی حفظ می‌شوند"
ContainerImageCheckSkipped: "کانتینر {{ .name }} رد شد: ایمیج تگ‌دار Docker Hub نیست، ایمیج محلی است یا digest ثابت دارد"
ContainerImageCheckAvailable: "برای کانتینر {{ .name }} به‌روزرسانی موجود است: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "به‌روزرسانی برای کانتینر {{ .name }} یافت نشد"
ContainerImageCheckChanged: "کانتینر {{ .name }} حذف شده یا ایمیج آن تغییر کرده است؛ نتیجه ذخیره نشد"
ContainerImageCheckSummary: "بررسی پایان یافت: بررسی‌شده {{ .checked }}، قابل ارتقا {{ .updates }}، ناموفق {{ .failed }}، ردشده {{ .skipped }}"
ContainerImageCheckFailures: "بررسی {{ .count }} کانتینر ناموفق بود؛ گزارش وظیفه را مشاهده کنید"
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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 は無効のままで、元の設定のバックアップは保持しています。"
ContainerImageCheckUpdate: "イメージの更新を確認"
ContainerImageCheckScope: "ページ分割やフィルターに関係なく、このノードの全 {{ .count }} コンテナを確認します"
ContainerImageCheckItem: "コンテナ {{ .name }}、イメージ {{ .image }} を確認しています"
ContainerImageCheckFailed: "コンテナ {{ .name }} の確認に失敗しました:{{ .error }}。現在のイメージに対応する前回の結果を保持します"
ContainerImageCheckSkipped: "{{ .name }} をスキップ:タグ付きの Docker Hub イメージではないか、ローカルイメージまたは digest 固定のイメージです"
ContainerImageCheckAvailable: "コンテナ {{ .name }} は更新可能です:{{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "コンテナ {{ .name }} の更新は見つかりませんでした"
ContainerImageCheckChanged: "コンテナ {{ .name }} が削除されたか、イメージが変更されたため、結果は保存されませんでした"
ContainerImageCheckSummary: "確認終了:成功 {{ .checked }}、更新可能 {{ .updates }}、失敗 {{ .failed }}、スキップ {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} コンテナの確認に失敗しました。タスクログを確認してください"
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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는 비활성 상태이며 원본 설정 백업은 유지됩니다."
ContainerImageCheckUpdate: "이미지 업데이트 확인"
ContainerImageCheckScope: "페이지 및 필터와 관계없이 현재 노드의 모든 컨테이너 {{ .count }}개를 확인합니다"
ContainerImageCheckItem: "컨테이너 {{ .name }}, 이미지 {{ .image }} 확인 중"
ContainerImageCheckFailed: "컨테이너 {{ .name }} 확인 실패: {{ .error }}. 현재 이미지와 일치하는 이전 결과를 유지합니다"
ContainerImageCheckSkipped: "{{ .name }} 건너뜀: 태그가 있는 Docker Hub 이미지가 아니거나 로컬 이미지 또는 digest가 고정된 이미지입니다"
ContainerImageCheckAvailable: "컨테이너 {{ .name }} 업데이트 가능: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "컨테이너 {{ .name }}의 업데이트를 찾지 못했습니다"
ContainerImageCheckChanged: "컨테이너 {{ .name }}이 삭제되었거나 이미지가 변경되어 결과를 저장하지 않았습니다"
ContainerImageCheckSummary: "확인 종료: 성공 {{ .checked }}, 업데이트 가능 {{ .updates }}, 실패 {{ .failed }}, 건너뜀 {{ .skipped }}"
ContainerImageCheckFailures: "컨테이너 {{ .count }}개 확인에 실패했습니다. 작업 로그를 확인하세요"
+34
View File
@@ -768,3 +768,37 @@ 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." 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." 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 ຍັງປິດ; ສຳຮອງການຕັ້ງຄ່າເດີມຍັງຖືກເກັບໄວ້."
ContainerImageCheckUpdate: "ກວດສອບການອັບເດດອິມເມຈ"
ContainerImageCheckScope: "ກຳລັງກວດສອບທັງໝົດ {{ .count }} ຄອນເທນເນີໃນໂນດນີ້ ໂດຍບໍ່ຂຶ້ນກັບການແບ່ງໜ້າ ຫຼື ຕົວກອງ"
ContainerImageCheckItem: "ກຳລັງກວດສອບຄອນເທນເນີ {{ .name }}, ອິມເມຈ {{ .image }}"
ContainerImageCheckFailed: "ກວດສອບຄອນເທນເນີ {{ .name }} ລົ້ມເຫຼວ: {{ .error }}; ເກັບຜົນກ່ອນໜ້າທີ່ກົງກັບອິມເມຈປັດຈຸບັນໄວ້"
ContainerImageCheckSkipped: "ຂ້າມ {{ .name }}: ບໍ່ແມ່ນອິມເມຈ Docker Hub ທີ່ມີແທັກ, ເປັນອິມເມຈພາຍໃນ ຫຼື ກຳນົດ digest ຄົງທີ່"
ContainerImageCheckAvailable: "ຄອນເທນເນີ {{ .name }} ມີອັບເດດ: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "ບໍ່ພົບອັບເດດສຳລັບຄອນເທນເນີ {{ .name }}"
ContainerImageCheckChanged: "ຄອນເທນເນີ {{ .name }} ຖືກລຶບ ຫຼື ອິມເມຈປ່ຽນແປງ; ບໍ່ໄດ້ບັນທຶກຜົນ"
ContainerImageCheckSummary: "ກວດສອບສິ້ນສຸດ: ກວດແລ້ວ {{ .checked }}, ອັບເກຣດໄດ້ {{ .updates }}, ລົ້ມເຫຼວ {{ .failed }}, ຂ້າມ {{ .skipped }}"
ContainerImageCheckFailures: "ກວດສອບ {{ .count }} ຄອນເທນເນີລົ້ມເຫຼວ; ກະລຸນາເບິ່ງບັນທຶກວຽກ"
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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."
ContainerImageCheckUpdate: "Semak kemas kini imej"
ContainerImageCheckScope: "Menyemak kesemua {{ .count }} kontena pada nod ini tanpa mengira pembahagian halaman atau penapis"
ContainerImageCheckItem: "Menyemak kontena {{ .name }}, imej {{ .image }}"
ContainerImageCheckFailed: "Semakan kontena {{ .name }} gagal: {{ .error }}; keputusan terdahulu yang sepadan dengan imej semasa dikekalkan"
ContainerImageCheckSkipped: "Melangkau {{ .name }}: bukan imej Docker Hub bertag, imej setempat atau digest ditetapkan"
ContainerImageCheckAvailable: "Kontena {{ .name }} mempunyai kemas kini: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "Tiada kemas kini ditemui untuk kontena {{ .name }}"
ContainerImageCheckChanged: "Kontena {{ .name }} telah dipadam atau imejnya berubah; keputusan tidak disimpan"
ContainerImageCheckSummary: "Semakan selesai: disemak {{ .checked }}, boleh dikemas kini {{ .updates }}, gagal {{ .failed }}, dilangkau {{ .skipped }}"
ContainerImageCheckFailures: "Semakan {{ .count }} kontena gagal; lihat log tugas"
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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."
ContainerImageCheckUpdate: "Verificar atualizações de imagens"
ContainerImageCheckScope: "Verificando todos os {{ .count }} contêineres deste nó, independentemente da paginação e dos filtros"
ContainerImageCheckItem: "Verificando o contêiner {{ .name }}, imagem {{ .image }}"
ContainerImageCheckFailed: "Falha ao verificar o contêiner {{ .name }}: {{ .error }}; os resultados anteriores correspondentes à imagem atual serão mantidos"
ContainerImageCheckSkipped: "Ignorando {{ .name }}: não é uma imagem do Docker Hub com tag, é uma imagem local ou tem digest fixo"
ContainerImageCheckAvailable: "O contêiner {{ .name }} tem uma atualização: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "Nenhuma atualização encontrada para o contêiner {{ .name }}"
ContainerImageCheckChanged: "O contêiner {{ .name }} foi removido ou sua imagem mudou; o resultado não foi salvo"
ContainerImageCheckSummary: "Verificação concluída: verificados {{ .checked }}, atualizáveis {{ .updates }}, falhas {{ .failed }}, ignorados {{ .skipped }}"
ContainerImageCheckFailures: "Falha na verificação de {{ .count }} contêineres; consulte o log da tarefa"
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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 отключён; исходная резервная копия конфигурации сохранена."
ContainerImageCheckUpdate: "Проверить обновления образов"
ContainerImageCheckScope: "Проверка всех контейнеров на текущем узле ({{ .count }}) независимо от страниц и фильтров"
ContainerImageCheckItem: "Проверка контейнера {{ .name }}, образ {{ .image }}"
ContainerImageCheckFailed: "Ошибка проверки контейнера {{ .name }}: {{ .error }}; сохранены предыдущие результаты, соответствующие текущему образу"
ContainerImageCheckSkipped: "Пропуск {{ .name }}: это не образ Docker Hub с тегом, локальный образ или образ с фиксированным digest"
ContainerImageCheckAvailable: "Доступно обновление контейнера {{ .name }}: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "Обновлений для контейнера {{ .name }} не найдено"
ContainerImageCheckChanged: "Контейнер {{ .name }} удалён или его образ изменился; результат не сохранён"
ContainerImageCheckSummary: "Проверка завершена: проверено {{ .checked }}, доступны обновления {{ .updates }}, ошибок {{ .failed }}, пропущено {{ .skipped }}"
ContainerImageCheckFailures: "Не удалось проверить контейнеры: {{ .count }}. Подробности в журнале задачи"
+34
View File
@@ -777,3 +777,37 @@ 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." 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." 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."
ContainerImageCheckUpdate: "İmaj güncellemelerini denetle"
ContainerImageCheckScope: "Sayfalama ve filtrelerden bağımsız olarak bu düğümdeki {{ .count }} konteynerin tümü denetleniyor"
ContainerImageCheckItem: "Konteyner {{ .name }}, imaj {{ .image }} denetleniyor"
ContainerImageCheckFailed: "{{ .name }} konteyneri denetlenemedi: {{ .error }}; mevcut imajla eşleşen önceki sonuçlar korunuyor"
ContainerImageCheckSkipped: "{{ .name }} atlanıyor: etiketli bir Docker Hub imajı değil, yerel bir imaj veya digest değeri sabitlenmiş"
ContainerImageCheckAvailable: "{{ .name }} konteyneri için güncelleme mevcut: {{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "{{ .name }} konteyneri için güncelleme bulunamadı"
ContainerImageCheckChanged: "{{ .name }} konteyneri silindi veya imajı değişti; sonuç kaydedilmedi"
ContainerImageCheckSummary: "Denetim tamamlandı: denetlenen {{ .checked }}, güncellenebilir {{ .updates }}, başarısız {{ .failed }}, atlanan {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} konteyner denetlenemedi; görev günlüğünü inceleyin"
+34
View File
@@ -777,3 +777,37 @@ VMSnapshotDeleteStep: "刪除虛擬機快照 [{{ .snapshot }}]"
ErrCAUpload: "請上傳包含有效自簽根憑證 ca.crt 及匹配私鑰 ca.key 的 ZIP 檔案(最大 10 MB),金鑰演算法須為支援的 RSA 或 EC。" ErrCAUpload: "請上傳包含有效自簽根憑證 ca.crt 及匹配私鑰 ca.key 的 ZIP 檔案(最大 10 MB),金鑰演算法須為支援的 RSA 或 EC。"
ErrCAImport: "請提供有效的自簽根憑證及匹配私鑰(每個最大 1 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: "閘道已恢復,飛書保持停用;原設定備份已保留。"
ContainerImageCheckUpdate: "檢查更新映像"
ContainerImageCheckScope: "檢查目前節點全部 {{ .count }} 個容器,不受分頁和篩選影響"
ContainerImageCheckItem: "檢查容器 {{ .name }},映像 {{ .image }}"
ContainerImageCheckFailed: "容器 {{ .name }} 檢查失敗:{{ .error }};保留符合目前映像的上次結果"
ContainerImageCheckSkipped: "略過 {{ .name }}:非 Docker Hub 標籤映像、本機映像或固定 digest"
ContainerImageCheckAvailable: "容器 {{ .name }} 可升級:{{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "容器 {{ .name }} 未發現更新"
ContainerImageCheckChanged: "容器 {{ .name }} 已刪除或映像已變更,未儲存本次結果"
ContainerImageCheckSummary: "檢查結束:成功 {{ .checked }},可升級 {{ .updates }},失敗 {{ .failed }},略過 {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} 個容器檢查失敗,請查看任務日誌"
+34
View File
@@ -777,3 +777,37 @@ VMSnapshotDeleteStep: "删除虚拟机快照 [{{ .snapshot }}]"
ErrCAUpload: "请上传包含有效自签根证书 ca.crt 及匹配私钥 ca.key 的 ZIP 文件(最大 10 MB),密钥算法须为支持的 RSA 或 EC。" ErrCAUpload: "请上传包含有效自签根证书 ca.crt 及匹配私钥 ca.key 的 ZIP 文件(最大 10 MB),密钥算法须为支持的 RSA 或 EC。"
ErrCAImport: "请提供有效的自签根证书及匹配私钥(每个最大 1 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: "网关已恢复,飞书保持停用;原配置备份已保留。"
ContainerImageCheckUpdate: "检查更新镜像"
ContainerImageCheckScope: "检查当前节点全部 {{ .count }} 个容器,不受分页和筛选影响"
ContainerImageCheckItem: "检查容器 {{ .name }},镜像 {{ .image }}"
ContainerImageCheckFailed: "容器 {{ .name }} 检查失败:{{ .error }};保留匹配当前镜像的上次结果"
ContainerImageCheckSkipped: "跳过 {{ .name }}:非 Docker Hub 标签镜像、本地镜像或固定 digest"
ContainerImageCheckAvailable: "容器 {{ .name }} 可升级:{{ .image }} → {{ .target }}"
ContainerImageCheckCurrent: "容器 {{ .name }} 未发现更新"
ContainerImageCheckChanged: "容器 {{ .name }} 已删除或镜像发生变化,未保存本次结果"
ContainerImageCheckSummary: "检查结束:成功 {{ .checked }},可升级 {{ .updates }},失败 {{ .failed }},跳过 {{ .skipped }}"
ContainerImageCheckFailures: "{{ .count }} 个容器检查失败,请查看任务日志"
+1
View File
@@ -119,6 +119,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddQuickDirectory, migrations.AddQuickDirectory,
migrations.AddLocalModelAccountLink, migrations.AddLocalModelAccountLink,
migrations.AddOllamaModelAccountSync, migrations.AddOllamaModelAccountSync,
migrations.AddContainerImageCheckUpdates,
} }
} }
@@ -0,0 +1,17 @@
package migrations
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var AddContainerImageCheckUpdates = &gormigrate.Migration{
ID: "20261028-container-image-updates",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.ContainerImageCheckUpdate{}); err != nil {
return err
}
return tx.Where("key = ?", "ContainerImageUpdateEnabled").FirstOrCreate(&model.Setting{Key: "ContainerImageUpdateEnabled", Value: "Disable"}).Error
},
}
+1
View File
@@ -91,6 +91,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/agent/md/update", baseApi.UpdateAgentRoleMarkdownFile) aiToolsRouter.POST("/agents/agent/md/update", baseApi.UpdateAgentRoleMarkdownFile)
aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig) aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig)
aiToolsRouter.POST("/agents/channel/feishu/update", baseApi.UpdateAgentFeishuConfig) 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/get", baseApi.GetAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig) aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig) aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig)
+3
View File
@@ -16,6 +16,9 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("", baseApi.ContainerCreate) baRouter.POST("", baseApi.ContainerCreate)
baRouter.POST("/update", baseApi.ContainerUpdate) baRouter.POST("/update", baseApi.ContainerUpdate)
baRouter.POST("/upgrade", baseApi.ContainerUpgrade) baRouter.POST("/upgrade", baseApi.ContainerUpgrade)
baRouter.GET("/image-updates", baseApi.GetContainerImageCheckUpdateStatus)
baRouter.POST("/image-updates/check", baseApi.ContainerImageCheckUpdate)
baRouter.POST("/image-updates/schedule", baseApi.SetContainerImageCheckUpdateSchedule)
baRouter.POST("/info", baseApi.ContainerInfo) baRouter.POST("/info", baseApi.ContainerInfo)
baRouter.POST("/search", baseApi.SearchContainer) baRouter.POST("/search", baseApi.SearchContainer)
baRouter.POST("/list", baseApi.ListContainer) baRouter.POST("/list", baseApi.ListContainer)
+1
View File
@@ -104,6 +104,7 @@ func pullComposeImages(filePath string, forcePull bool, task *task.Task, project
if err != nil { if err != nil {
return err return err
} }
defer dockerCLi.Close()
for _, image := range images { for _, image := range images {
if !forcePull { if !forcePull {
if exist, _ := dockerCLi.ImageExists(image); exist { if exist, _ := dockerCLi.ImageExists(image); exist {
+4 -2
View File
@@ -345,10 +345,12 @@ func PullImage(imageName string) error {
if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok { if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok {
options.RegistryAuth = authStr options.RegistryAuth = authStr
} }
if _, err := cli.ImagePull(context.Background(), imageName, options); err != nil { out, err := cli.ImagePull(context.Background(), imageName, options)
if err != nil {
return err return err
} }
return nil defer out.Close()
return handlePullImageProcess(out, nil)
} }
func loadRegistryAuthFromDockerConfig(imageName string) (string, bool) { func loadRegistryAuthFromDockerConfig(imageName string) (string, bool) {
+266
View File
@@ -0,0 +1,266 @@
// Package dockerhub checks Docker Hub metadata without downloading image layers.
package dockerhub
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/distribution/reference"
)
const registryURL = "https://registry-1.docker.io"
// Reference only accepts tagged Docker Hub references. Digest pins are intentional.
func Reference(raw string) (string, string, bool) {
if strings.HasPrefix(raw, "sha256:") {
return "", "", false
}
ref, err := reference.ParseNormalizedNamed(raw)
if err != nil || reference.Domain(ref) != "docker.io" {
return "", "", false
}
if _, pinned := ref.(reference.Digested); pinned {
return "", "", false
}
tagged := reference.TagNameOnly(ref).(reference.NamedTagged)
return reference.Path(ref), tagged.Tag(), true
}
type Client struct {
HTTP *http.Client
repository string
token string
}
type Platform struct{ OS, Architecture, Variant string }
type Target struct{ Tag, Digest, ConfigDigest string }
func New(ctx context.Context, repository, username, password string) (*Client, error) {
c := &Client{HTTP: &http.Client{Timeout: 30 * time.Second}, repository: repository}
endpoint := "https://auth.docker.io/token?service=registry.docker.io&scope=" + url.QueryEscape("repository:"+repository+":pull")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
if username != "" {
req.SetBasicAuth(username, password)
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Docker Hub authentication: HTTP %d", resp.StatusCode)
}
var body struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil {
return nil, err
}
c.token = body.Token
if c.token == "" {
c.token = body.AccessToken
}
if c.token == "" {
return nil, fmt.Errorf("Docker Hub returned an empty token")
}
return c, nil
}
func (c *Client) get(ctx context.Context, endpoint string, result interface{}) (http.Header, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json")
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Docker Hub metadata: HTTP %d", resp.StatusCode)
}
err = json.NewDecoder(io.LimitReader(resp.Body, 16<<20)).Decode(result)
return resp.Header, err
}
func (c *Client) Tags(ctx context.Context) ([]string, error) {
endpoint := registryURL + "/v2/" + c.repository + "/tags/list?n=1000"
var tags []string
for page := 0; page < 100; page++ {
var body struct {
Tags []string `json:"tags"`
}
headers, err := c.get(ctx, endpoint, &body)
if err != nil {
return nil, err
}
tags = append(tags, body.Tags...)
link := headers.Get("Link")
if link == "" {
return tags, nil
}
start, end := strings.Index(link, "<"), strings.Index(link, ">")
if start < 0 || end <= start {
return nil, fmt.Errorf("invalid Docker Hub pagination")
}
base, _ := url.Parse(registryURL)
next, err := base.Parse(link[start+1 : end])
if err != nil || next.Scheme != "https" || next.Host != base.Host || next.Path != "/v2/"+c.repository+"/tags/list" {
return nil, fmt.Errorf("invalid Docker Hub pagination URL")
}
endpoint = next.String()
}
return nil, fmt.Errorf("Docker Hub tag pagination limit exceeded")
}
type manifest struct {
Config struct {
Digest string `json:"digest"`
} `json:"config"`
Manifests []struct {
Digest string `json:"digest"`
Platform struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
Variant string `json:"variant"`
} `json:"platform"`
} `json:"manifests"`
}
func (c *Client) Resolve(ctx context.Context, tag string, platform Platform) (Target, bool, error) {
var m manifest
headers, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(tag), &m)
if err != nil {
return Target{}, false, err
}
digest := headers.Get("Docker-Content-Digest")
if len(m.Manifests) > 0 {
digest = ""
for _, item := range m.Manifests {
if item.Platform.OS == platform.OS && item.Platform.Architecture == platform.Architecture && normalizedVariant(platform.Architecture, item.Platform.Variant) == normalizedVariant(platform.Architecture, platform.Variant) {
digest = item.Digest
break
}
}
if digest == "" {
return Target{}, false, nil
}
m = manifest{}
if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/manifests/"+url.PathEscape(digest), &m); err != nil {
return Target{}, false, err
}
}
if m.Config.Digest == "" {
return Target{}, false, fmt.Errorf("unsupported Docker Hub manifest")
}
// Validate single-platform manifests too; the config digest is the Docker Image ID.
var config struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
Variant string `json:"variant"`
}
if _, err := c.get(ctx, registryURL+"/v2/"+c.repository+"/blobs/"+url.PathEscape(m.Config.Digest), &config); err != nil {
return Target{}, false, err
}
if config.OS != platform.OS || config.Architecture != platform.Architecture || (platform.Variant != "" && config.Variant != "" && config.Variant != platform.Variant) {
return Target{}, false, nil
}
return Target{Tag: tag, Digest: digest, ConfigDigest: m.Config.Digest}, true, nil
}
var versionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)(?:\.(\d+))?(-[a-zA-Z][a-zA-Z0-9_.-]*)?$`)
var prereleasePattern = regexp.MustCompile(`(?i)(^|[-_.])(alpha|beta|rc|dev|nightly|edge|test|unstable|preview)([0-9_.-]|$)`)
type version struct {
numbers [3]uint64
parts int
variant string
}
func parseVersion(tag string) (version, bool) {
m := versionPattern.FindStringSubmatch(tag)
if m == nil || prereleasePattern.MatchString(m[4]) {
return version{}, false
}
v := version{parts: 2, variant: m[4]}
for i := 0; i < 3; i++ {
if m[i+1] != "" {
n, err := strconv.ParseUint(m[i+1], 10, 64)
if err != nil {
return version{}, false
}
v.numbers[i] = n
}
}
if m[3] != "" {
v.parts = 3
}
return v, true
}
func greater(a, b version) bool {
for i := 0; i < 3; i++ {
if a.numbers[i] != b.numbers[i] {
return a.numbers[i] > b.numbers[i]
}
}
return false
}
// Candidates preserves the current tag's variant and precision. Unknown/floating
// tags are checked only for content changes, never guessed from upload dates.
func Candidates(current string, tags []string) []string {
baseline, ok := parseVersion(current)
if !ok {
return []string{current}
}
type candidate struct {
tag string
version version
}
candidates := []candidate{}
seen := map[string]bool{}
for _, tag := range tags {
v, valid := parseVersion(tag)
if valid && v.parts == baseline.parts && v.variant == baseline.variant && greater(v, baseline) && !seen[tag] {
candidates = append(candidates, candidate{tag: tag, version: v})
seen[tag] = true
}
}
sort.Slice(candidates, func(i, j int) bool {
a, b := candidates[i], candidates[j]
if a.version.numbers == b.version.numbers {
return a.tag < b.tag
}
return greater(a.version, b.version)
})
result := make([]string, 0, len(candidates)+1)
for _, candidate := range candidates {
result = append(result, candidate.tag)
}
return append(result, current)
}
func NeedsTags(tag string) bool { _, ok := parseVersion(tag); return ok }
func normalizedVariant(architecture, variant string) string {
if architecture == "arm64" && variant == "" {
return "v8"
}
return variant
}
-2
View File
@@ -6,7 +6,6 @@ import (
) )
const ( const (
TokenizerDownloadNamePattern = `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}-[0-9a-f]{12}$`
ModelDownloaderRepoIDPattern = `^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$` ModelDownloaderRepoIDPattern = `^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$`
VMNameValidationPattern = `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$` VMNameValidationPattern = `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`
VMCommonPattern = `^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,63}$` VMCommonPattern = `^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,63}$`
@@ -68,7 +67,6 @@ var regexMap = make(map[string]*regexp.Regexp)
func Init() { func Init() {
patterns := []string{ patterns := []string{
TokenizerDownloadNamePattern,
ModelDownloaderRepoIDPattern, ModelDownloaderRepoIDPattern,
VMNameValidationPattern, VMNameValidationPattern,
VMCommonPattern, VMCommonPattern,
+137
View File
@@ -1171,6 +1171,41 @@ const docTemplate = `{
] ]
} }
}, },
"/ai/agents/channel/feishu/switch": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.AgentFeishuSwitchReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Switch to the bundled OpenClaw Feishu plugin",
"tags": [
"AI"
]
}
},
"/ai/agents/channel/feishu/update": { "/ai/agents/channel/feishu/update": {
"post": { "post": {
"consumes": [ "consumes": [
@@ -30117,6 +30152,33 @@ const docTemplate = `{
} }
}, },
"definitions": { "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": { "dto.AgentAccountCreateReq": {
"properties": { "properties": {
"apiKey": { "apiKey": {
@@ -31215,12 +31277,18 @@ const docTemplate = `{
}, },
"dto.AgentFeishuConfig": { "dto.AgentFeishuConfig": {
"properties": { "properties": {
"builtinManaged": {
"type": "boolean"
},
"bots": { "bots": {
"items": { "items": {
"$ref": "#/definitions/dto.AgentFeishuBot" "$ref": "#/definitions/dto.AgentFeishuBot"
}, },
"type": "array" "type": "array"
}, },
"builtinAvailable": {
"type": "boolean"
},
"connectionMode": { "connectionMode": {
"type": "string" "type": "string"
}, },
@@ -31239,9 +31307,21 @@ const docTemplate = `{
"groupPolicy": { "groupPolicy": {
"type": "string" "type": "string"
}, },
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"installed": { "installed": {
"type": "boolean" "type": "boolean"
}, },
"legacy": {
"type": "boolean"
},
"replyMode": { "replyMode": {
"type": "string" "type": "string"
}, },
@@ -31253,10 +31333,49 @@ const docTemplate = `{
}, },
"threadSession": { "threadSession": {
"type": "boolean" "type": "boolean"
},
"legacyConfigured": {
"type": "boolean"
} }
}, },
"type": "object" "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": { "dto.AgentFeishuConfigReq": {
"properties": { "properties": {
"agentId": { "agentId": {
@@ -31303,6 +31422,15 @@ const docTemplate = `{
], ],
"type": "string" "type": "string"
}, },
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"replyMode": { "replyMode": {
"type": "string" "type": "string"
}, },
@@ -33167,6 +33295,12 @@ const docTemplate = `{
}, },
"version": { "version": {
"type": "string" "type": "string"
},
"feishuLegacy": {
"type": "boolean"
},
"feishuConfigured": {
"type": "boolean"
} }
}, },
"type": "object" "type": "object"
@@ -42953,6 +43087,9 @@ const docTemplate = `{
}, },
"taskID": { "taskID": {
"type": "string" "type": "string"
},
"feishuTransition": {
"$ref": "#/definitions/dto.AgentFeishuTransition"
} }
}, },
"required": [ "required": [
+137
View File
@@ -1167,6 +1167,41 @@
] ]
} }
}, },
"/ai/agents/channel/feishu/switch": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.AgentFeishuSwitchReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Switch to the bundled OpenClaw Feishu plugin",
"tags": [
"AI"
]
}
},
"/ai/agents/channel/feishu/update": { "/ai/agents/channel/feishu/update": {
"post": { "post": {
"consumes": [ "consumes": [
@@ -30113,6 +30148,33 @@
} }
}, },
"definitions": { "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": { "dto.AgentAccountCreateReq": {
"properties": { "properties": {
"apiKey": { "apiKey": {
@@ -31211,12 +31273,18 @@
}, },
"dto.AgentFeishuConfig": { "dto.AgentFeishuConfig": {
"properties": { "properties": {
"builtinManaged": {
"type": "boolean"
},
"bots": { "bots": {
"items": { "items": {
"$ref": "#/definitions/dto.AgentFeishuBot" "$ref": "#/definitions/dto.AgentFeishuBot"
}, },
"type": "array" "type": "array"
}, },
"builtinAvailable": {
"type": "boolean"
},
"connectionMode": { "connectionMode": {
"type": "string" "type": "string"
}, },
@@ -31235,9 +31303,21 @@
"groupPolicy": { "groupPolicy": {
"type": "string" "type": "string"
}, },
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"installed": { "installed": {
"type": "boolean" "type": "boolean"
}, },
"legacy": {
"type": "boolean"
},
"replyMode": { "replyMode": {
"type": "string" "type": "string"
}, },
@@ -31249,10 +31329,49 @@
}, },
"threadSession": { "threadSession": {
"type": "boolean" "type": "boolean"
},
"legacyConfigured": {
"type": "boolean"
} }
}, },
"type": "object" "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": { "dto.AgentFeishuConfigReq": {
"properties": { "properties": {
"agentId": { "agentId": {
@@ -31299,6 +31418,15 @@
], ],
"type": "string" "type": "string"
}, },
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"replyMode": { "replyMode": {
"type": "string" "type": "string"
}, },
@@ -33163,6 +33291,12 @@
}, },
"version": { "version": {
"type": "string" "type": "string"
},
"feishuLegacy": {
"type": "boolean"
},
"feishuConfigured": {
"type": "boolean"
} }
}, },
"type": "object" "type": "object"
@@ -42949,6 +43083,9 @@
}, },
"taskID": { "taskID": {
"type": "string" "type": "string"
},
"feishuTransition": {
"$ref": "#/definitions/dto.AgentFeishuTransition"
} }
}, },
"required": [ "required": [
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Sync basic node info"
GenerateSSLInfo: "Generate node SSL info" GenerateSSLInfo: "Generate node SSL info"
ConnInfoNotMatch: "Connection info mismatch" ConnInfoNotMatch: "Connection info mismatch"
MakeAgentPackage: "Generate node installation package" MakeAgentPackage: "Generate node installation package"
PrepareNodePackage: "Prepare existing package on child node"
PrepareNodePackageDetail: "Use existing package on child node: {{ .name }}"
SyncNodeConfig: "Sync node configuration"
ErrNodePackagePath: "Enter the absolute file path to the package on the child node"
ErrNodePackageFile: "Failed to process package file {{ .name }}: {{ .err }}"
ErrNodePackageMember: "Package contains an invalid or duplicate entry: {{ .name }}"
ErrNodePackageMismatch: "Package must match main node version {{ .version }} and child node architecture {{ .arch }}"
ErrNodePackageMissing: "Required file missing from package: {{ .name }}"
ErrNodePackageVersion: "Package version {{ .version }} does not match main node version {{ .expected }}"
ErrNodePackageArch: "Agent executable architecture does not match child node architecture {{ .arch }}"
SendAgent: "Distribute node installation package" SendAgent: "Distribute node installation package"
StartService: "Start service" StartService: "Start service"
NoBackupNode: "Backup node is empty. Select one and save first" NoBackupNode: "Backup node is empty. Select one and save first"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Sincronizar información básica del nodo"
GenerateSSLInfo: "Generar información SSL del nodo" GenerateSSLInfo: "Generar información SSL del nodo"
ConnInfoNotMatch: "La información de conexión no coincide" ConnInfoNotMatch: "La información de conexión no coincide"
MakeAgentPackage: "Generar paquete de instalación del nodo" MakeAgentPackage: "Generar paquete de instalación del nodo"
PrepareNodePackage: "Preparar el paquete existente en el nodo secundario"
PrepareNodePackageDetail: "Usar el paquete existente en el nodo secundario: {{ .name }}"
SyncNodeConfig: "Sincronizar la configuración del nodo"
ErrNodePackagePath: "Introduzca la ruta absoluta del archivo del paquete en el nodo secundario"
ErrNodePackageFile: "Error al procesar el archivo del paquete {{ .name }}: {{ .err }}"
ErrNodePackageMember: "El paquete contiene una entrada no válida o duplicada: {{ .name }}"
ErrNodePackageMismatch: "El paquete debe coincidir con la versión {{ .version }} del nodo principal y la arquitectura {{ .arch }} del nodo secundario"
ErrNodePackageMissing: "Falta un archivo necesario en el paquete: {{ .name }}"
ErrNodePackageVersion: "La versión {{ .version }} del paquete no coincide con la versión {{ .expected }} del nodo principal"
ErrNodePackageArch: "La arquitectura del ejecutable Agent no coincide con la arquitectura {{ .arch }} del nodo secundario"
SendAgent: "Distribuir paquete de instalación del nodo" SendAgent: "Distribuir paquete de instalación del nodo"
StartService: "Iniciar servicio" StartService: "Iniciar servicio"
NoBackupNode: "Actualmente no hay nodo de respaldo. Seleccione uno para guardar e intente de nuevo." NoBackupNode: "Actualmente no hay nodo de respaldo. Seleccione uno para guardar e intente de nuevo."
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "همگام‌سازی اطلاعات پایه گره"
GenerateSSLInfo: "تولید اطلاعات SSL گره" GenerateSSLInfo: "تولید اطلاعات SSL گره"
ConnInfoNotMatch: "اطلاعات اتصال مطابقت ندارد" ConnInfoNotMatch: "اطلاعات اتصال مطابقت ندارد"
MakeAgentPackage: "تولید بسته نصب گره" MakeAgentPackage: "تولید بسته نصب گره"
PrepareNodePackage: "آماده‌سازی بسته موجود در گره فرزند"
PrepareNodePackageDetail: "استفاده از بسته موجود در گره فرزند: {{ .name }}"
SyncNodeConfig: "همگام‌سازی پیکربندی گره"
ErrNodePackagePath: "مسیر مطلق فایل بسته در گره فرزند را وارد کنید"
ErrNodePackageFile: "پردازش فایل بسته {{ .name }} ناموفق بود: {{ .err }}"
ErrNodePackageMember: "بسته دارای ورودی نامعتبر یا تکراری است: {{ .name }}"
ErrNodePackageMismatch: "بسته باید با نسخه گره اصلی {{ .version }} و معماری گره فرزند {{ .arch }} مطابقت داشته باشد"
ErrNodePackageMissing: "فایل ضروری در بسته موجود نیست: {{ .name }}"
ErrNodePackageVersion: "نسخه بسته {{ .version }} با نسخه گره اصلی {{ .expected }} مطابقت ندارد"
ErrNodePackageArch: "معماری فایل اجرایی Agent با معماری گره فرزند {{ .arch }} مطابقت ندارد"
SendAgent: "توزیع بسته نصب گره" SendAgent: "توزیع بسته نصب گره"
StartService: "شروع سرویس" StartService: "شروع سرویس"
NoBackupNode: "گره پشتیبان خالی است. ابتدا یکی را انتخاب کرده و ذخیره کنید" NoBackupNode: "گره پشتیبان خالی است. ابتدا یکی را انتخاب کرده و ذخیره کنید"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "ノードの基本情報を同期"
GenerateSSLInfo: "ノードのSSL情報を生成" GenerateSSLInfo: "ノードのSSL情報を生成"
ConnInfoNotMatch: "接続情報が一致しません" ConnInfoNotMatch: "接続情報が一致しません"
MakeAgentPackage: "ノードインストールパッケージを生成" MakeAgentPackage: "ノードインストールパッケージを生成"
PrepareNodePackage: "子ノード上の既存パッケージを準備"
PrepareNodePackageDetail: "子ノード上の既存パッケージを使用: {{ .name }}"
SyncNodeConfig: "ノード設定を同期"
ErrNodePackagePath: "子ノード上のパッケージの絶対ファイルパスを入力してください"
ErrNodePackageFile: "パッケージファイル {{ .name }} の処理に失敗しました: {{ .err }}"
ErrNodePackageMember: "パッケージに無効または重複する項目があります: {{ .name }}"
ErrNodePackageMismatch: "パッケージはメインノードのバージョン {{ .version }} と子ノードのアーキテクチャ {{ .arch }} に一致する必要があります"
ErrNodePackageMissing: "パッケージに必要なファイルがありません: {{ .name }}"
ErrNodePackageVersion: "パッケージのバージョン {{ .version }} がメインノードのバージョン {{ .expected }} と一致しません"
ErrNodePackageArch: "Agent 実行ファイルのアーキテクチャが子ノードのアーキテクチャ {{ .arch }} と一致しません"
SendAgent: "ノードインストールパッケージを送信" SendAgent: "ノードインストールパッケージを送信"
StartService: "サービスを開始" StartService: "サービスを開始"
NoBackupNode: "現在バックアップノードが空です。保存するバックアップノードを選択して再試行してください!" NoBackupNode: "現在バックアップノードが空です。保存するバックアップノードを選択して再試行してください!"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "노드 기본 데이터 동기화"
GenerateSSLInfo: "노드 SSL 정보 생성" GenerateSSLInfo: "노드 SSL 정보 생성"
ConnInfoNotMatch: "연결 정보가 일치하지 않음" ConnInfoNotMatch: "연결 정보가 일치하지 않음"
MakeAgentPackage: "노드 설치 패키지 생성" MakeAgentPackage: "노드 설치 패키지 생성"
PrepareNodePackage: "하위 노드의 기존 패키지 준비"
PrepareNodePackageDetail: "하위 노드의 기존 패키지 사용: {{ .name }}"
SyncNodeConfig: "노드 설정 동기화"
ErrNodePackagePath: "하위 노드에 있는 패키지의 절대 파일 경로를 입력하세요"
ErrNodePackageFile: "패키지 파일 {{ .name }} 처리 실패: {{ .err }}"
ErrNodePackageMember: "패키지에 잘못되었거나 중복된 항목이 있습니다: {{ .name }}"
ErrNodePackageMismatch: "패키지는 주 노드 버전 {{ .version }} 및 하위 노드 아키텍처 {{ .arch }}와 일치해야 합니다"
ErrNodePackageMissing: "패키지에 필수 파일이 없습니다: {{ .name }}"
ErrNodePackageVersion: "패키지 버전 {{ .version }}이 주 노드 버전 {{ .expected }}와 일치하지 않습니다"
ErrNodePackageArch: "Agent 실행 파일의 아키텍처가 하위 노드 아키텍처 {{ .arch }}와 일치하지 않습니다"
SendAgent: "노드 설치 패키지 배포" SendAgent: "노드 설치 패키지 배포"
StartService: "서비스 시작" StartService: "서비스 시작"
NoBackupNode: "현재 백업 노드가 비어 있습니다. 저장할 백업 노드를 선택한 후 다시 시도하십시오" NoBackupNode: "현재 백업 노드가 비어 있습니다. 저장할 백업 노드를 선택한 후 다시 시도하십시오"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "ຊິງຂໍ້ມູນພື້ນຖານຂອງໂ
GenerateSSLInfo: "ສ້າງຂໍ້ມູນ SSL ຂອງໂນດ" GenerateSSLInfo: "ສ້າງຂໍ້ມູນ SSL ຂອງໂນດ"
ConnInfoNotMatch: "ຂໍ້ມູນການເຊື່ອມຕໍ່ບໍ່ກົງກັນ" ConnInfoNotMatch: "ຂໍ້ມູນການເຊື່ອມຕໍ່ບໍ່ກົງກັນ"
MakeAgentPackage: "ສ້າງແພັກເກດຕິດຕັ້ງໂນດ" MakeAgentPackage: "ສ້າງແພັກເກດຕິດຕັ້ງໂນດ"
PrepareNodePackage: "ກະກຽມແພັກເກດທີ່ມີຢູ່ໃນໂນດລູກ"
PrepareNodePackageDetail: "ໃຊ້ແພັກເກດທີ່ມີຢູ່ໃນໂນດລູກ: {{ .name }}"
SyncNodeConfig: "ຊິງຂໍ້ມູນການຕັ້ງຄ່າໂນດ"
ErrNodePackagePath: "ປ້ອນເສັ້ນທາງໄຟລ໌ແບບເຕັມຂອງແພັກເກດໃນໂນດລູກ"
ErrNodePackageFile: "ປະມວນຜົນໄຟລ໌ແພັກເກດ {{ .name }} ລົ້ມເຫຼວ: {{ .err }}"
ErrNodePackageMember: "ແພັກເກດມີລາຍການທີ່ບໍ່ຖືກຕ້ອງ ຫຼື ຊ້ຳກັນ: {{ .name }}"
ErrNodePackageMismatch: "ແພັກເກດຕ້ອງກົງກັບເວີຊັນໂນດຫຼັກ {{ .version }} ແລະ ສະຖາປັດຕະຍະກຳໂນດລູກ {{ .arch }}"
ErrNodePackageMissing: "ແພັກເກດຂາດໄຟລ໌ທີ່ຈຳເປັນ: {{ .name }}"
ErrNodePackageVersion: "ເວີຊັນແພັກເກດ {{ .version }} ບໍ່ກົງກັບເວີຊັນໂນດຫຼັກ {{ .expected }}"
ErrNodePackageArch: "ສະຖາປັດຕະຍະກຳຂອງໂປຣແກຣມ Agent ບໍ່ກົງກັບສະຖາປັດຕະຍະກຳໂນດລູກ {{ .arch }}"
SendAgent: "ແຈກຢາຍແພັກເກດຕິດຕັ້ງໂນດ" SendAgent: "ແຈກຢາຍແພັກເກດຕິດຕັ້ງໂນດ"
StartService: "ເລີ່ມບໍລິການ" StartService: "ເລີ່ມບໍລິການ"
NoBackupNode: "ໂນດສຳຮອງຫວ່າງເປົ່າ. ກະລຸນາເລືອກແລ້ວບັນທຶກກ່ອນ" NoBackupNode: "ໂນດສຳຮອງຫວ່າງເປົ່າ. ກະລຸນາເລືອກແລ້ວບັນທຶກກ່ອນ"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Sinkronkan maklumat asas nod"
GenerateSSLInfo: "Cipta maklumat SSL nod" GenerateSSLInfo: "Cipta maklumat SSL nod"
ConnInfoNotMatch: "Maklumat sambungan tidak sepadan" ConnInfoNotMatch: "Maklumat sambungan tidak sepadan"
MakeAgentPackage: "Cipta pakej pemasangan nod" MakeAgentPackage: "Cipta pakej pemasangan nod"
PrepareNodePackage: "Sediakan pakej sedia ada pada nod anak"
PrepareNodePackageDetail: "Gunakan pakej sedia ada pada nod anak: {{ .name }}"
SyncNodeConfig: "Segerakkan konfigurasi nod"
ErrNodePackagePath: "Masukkan laluan fail mutlak pakej pada nod anak"
ErrNodePackageFile: "Gagal memproses fail pakej {{ .name }}: {{ .err }}"
ErrNodePackageMember: "Pakej mengandungi entri tidak sah atau pendua: {{ .name }}"
ErrNodePackageMismatch: "Pakej mesti sepadan dengan versi nod utama {{ .version }} dan seni bina nod anak {{ .arch }}"
ErrNodePackageMissing: "Fail yang diperlukan tiada dalam pakej: {{ .name }}"
ErrNodePackageVersion: "Versi pakej {{ .version }} tidak sepadan dengan versi nod utama {{ .expected }}"
ErrNodePackageArch: "Seni bina fail boleh laksana Agent tidak sepadan dengan seni bina nod anak {{ .arch }}"
SendAgent: "Muat naik pakej pemasangan nod" SendAgent: "Muat naik pakej pemasangan nod"
StartService: "Mulakan perkhidmatan" StartService: "Mulakan perkhidmatan"
NoBackupNode: "Nod sandaran kosong. Sila pilih nod sandaran untuk disimpan dan cuba lagi" NoBackupNode: "Nod sandaran kosong. Sila pilih nod sandaran untuk disimpan dan cuba lagi"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Sincronizando dados básicos do nó"
GenerateSSLInfo: "Gerando informações SSL do nó" GenerateSSLInfo: "Gerando informações SSL do nó"
ConnInfoNotMatch: "As informações de conexão não coincidem" ConnInfoNotMatch: "As informações de conexão não coincidem"
MakeAgentPackage: "Gerando pacote de instalação do nó" MakeAgentPackage: "Gerando pacote de instalação do nó"
PrepareNodePackage: "Preparar o pacote existente no nó secundário"
PrepareNodePackageDetail: "Usar o pacote existente no nó secundário: {{ .name }}"
SyncNodeConfig: "Sincronizar a configuração do nó"
ErrNodePackagePath: "Informe o caminho absoluto do arquivo do pacote no nó secundário"
ErrNodePackageFile: "Falha ao processar o arquivo do pacote {{ .name }}: {{ .err }}"
ErrNodePackageMember: "O pacote contém uma entrada inválida ou duplicada: {{ .name }}"
ErrNodePackageMismatch: "O pacote deve corresponder à versão {{ .version }} do nó principal e à arquitetura {{ .arch }} do nó secundário"
ErrNodePackageMissing: "Arquivo necessário ausente no pacote: {{ .name }}"
ErrNodePackageVersion: "A versão {{ .version }} do pacote não corresponde à versão {{ .expected }} do nó principal"
ErrNodePackageArch: "A arquitetura do executável Agent não corresponde à arquitetura {{ .arch }} do nó secundário"
SendAgent: "Enviando pacote de instalação do nó" SendAgent: "Enviando pacote de instalação do nó"
StartService: "Iniciando serviço" StartService: "Iniciando serviço"
NoBackupNode: "O nó de backup está vazio atualmente. Selecione um nó de backup para salvar e tente novamente" NoBackupNode: "O nó de backup está vazio atualmente. Selecione um nó de backup para salvar e tente novamente"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Синхронизировать базовую информ
GenerateSSLInfo: "Сгенерировать SSL информацию узла" GenerateSSLInfo: "Сгенерировать SSL информацию узла"
ConnInfoNotMatch: "Информация о соединении не совпадает" ConnInfoNotMatch: "Информация о соединении не совпадает"
MakeAgentPackage: "Сгенерировать установочный пакет узла" MakeAgentPackage: "Сгенерировать установочный пакет узла"
PrepareNodePackage: "Подготовка готового пакета на дочернем узле"
PrepareNodePackageDetail: "Использование готового пакета на дочернем узле: {{ .name }}"
SyncNodeConfig: "Синхронизация конфигурации узла"
ErrNodePackagePath: "Укажите абсолютный путь к файлу пакета на дочернем узле"
ErrNodePackageFile: "Не удалось обработать файл пакета {{ .name }}: {{ .err }}"
ErrNodePackageMember: "Пакет содержит недопустимую или повторяющуюся запись: {{ .name }}"
ErrNodePackageMismatch: "Пакет должен соответствовать версии главного узла {{ .version }} и архитектуре дочернего узла {{ .arch }}"
ErrNodePackageMissing: "В пакете отсутствует обязательный файл: {{ .name }}"
ErrNodePackageVersion: "Версия пакета {{ .version }} не совпадает с версией главного узла {{ .expected }}"
ErrNodePackageArch: "Архитектура исполняемого файла Agent не совпадает с архитектурой дочернего узла {{ .arch }}"
SendAgent: "Отправить установочный пакет узла" SendAgent: "Отправить установочный пакет узла"
StartService: "Запустить сервис" StartService: "Запустить сервис"
NoBackupNode: "Резервный узел в настоящее время пуст. Выберите резервный узел для сохранения и повторите попытку" NoBackupNode: "Резервный узел в настоящее время пуст. Выберите резервный узел для сохранения и повторите попытку"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Temel düğüm bilgilerini senkronize et"
GenerateSSLInfo: "Düğüm SSL bilgilerini oluştur" GenerateSSLInfo: "Düğüm SSL bilgilerini oluştur"
ConnInfoNotMatch: "Bağlantı bilgisi uyuşmuyor" ConnInfoNotMatch: "Bağlantı bilgisi uyuşmuyor"
MakeAgentPackage: "Düğüm kurulum paketini oluştur" MakeAgentPackage: "Düğüm kurulum paketini oluştur"
PrepareNodePackage: "Alt düğümdeki mevcut paketi hazırla"
PrepareNodePackageDetail: "Alt düğümdeki mevcut paketi kullan: {{ .name }}"
SyncNodeConfig: "Düğüm yapılandırmasını eşitle"
ErrNodePackagePath: "Alt düğümdeki paket dosyasının mutlak yolunu girin"
ErrNodePackageFile: "{{ .name }} paket dosyası işlenemedi: {{ .err }}"
ErrNodePackageMember: "Paket geçersiz veya yinelenen bir girdi içeriyor: {{ .name }}"
ErrNodePackageMismatch: "Paket, ana düğüm sürümü {{ .version }} ve alt düğüm mimarisi {{ .arch }} ile eşleşmelidir"
ErrNodePackageMissing: "Pakette gerekli dosya eksik: {{ .name }}"
ErrNodePackageVersion: "Paket sürümü {{ .version }}, ana düğüm sürümü {{ .expected }} ile eşleşmiyor"
ErrNodePackageArch: "Agent yürütülebilir dosyasının mimarisi, alt düğüm mimarisi {{ .arch }} ile eşleşmiyor"
SendAgent: "Düğüm kurulum paketini dağıt" SendAgent: "Düğüm kurulum paketini dağıt"
StartService: "Servisi başlat" StartService: "Servisi başlat"
NoBackupNode: "Yedek düğüm şu anda boş. Lütfen kaydetmek için bir yedek düğüm seçin ve tekrar deneyin" NoBackupNode: "Yedek düğüm şu anda boş. Lütfen kaydetmek için bir yedek düğüm seçin ve tekrar deneyin"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "同步節點基礎資料"
GenerateSSLInfo: "產生節點 SSL 資訊" GenerateSSLInfo: "產生節點 SSL 資訊"
ConnInfoNotMatch: "連接資訊不符" ConnInfoNotMatch: "連接資訊不符"
MakeAgentPackage: "產生節點安裝包" MakeAgentPackage: "產生節點安裝包"
PrepareNodePackage: "準備子節點已有安裝包"
PrepareNodePackageDetail: "使用子節點已有安裝包:{{ .name }}"
SyncNodeConfig: "同步節點設定"
ErrNodePackagePath: "請填寫子節點安裝包的絕對檔案路徑"
ErrNodePackageFile: "處理安裝包檔案 {{ .name }} 失敗:{{ .err }}"
ErrNodePackageMember: "安裝包包含無效或重複項目:{{ .name }}"
ErrNodePackageMismatch: "安裝包必須符合主節點版本 {{ .version }} 和子節點架構 {{ .arch }}"
ErrNodePackageMissing: "安裝包缺少必要檔案:{{ .name }}"
ErrNodePackageVersion: "安裝包版本 {{ .version }} 與主節點版本 {{ .expected }} 不一致"
ErrNodePackageArch: "Agent 程式架構與子節點架構 {{ .arch }} 不一致"
SendAgent: "下發節點安裝包" SendAgent: "下發節點安裝包"
StartService: "啟動服務" StartService: "啟動服務"
NoBackupNode: "目前備份節點為空,請選擇備份節點儲存後重試!" NoBackupNode: "目前備份節點為空,請選擇備份節點儲存後重試!"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "同步节点基础数据"
GenerateSSLInfo: "生成节点 SSL 信息" GenerateSSLInfo: "生成节点 SSL 信息"
ConnInfoNotMatch: "连接信息不匹配" ConnInfoNotMatch: "连接信息不匹配"
MakeAgentPackage: "生成节点安装包" MakeAgentPackage: "生成节点安装包"
PrepareNodePackage: "准备子节点已有安装包"
PrepareNodePackageDetail: "使用子节点已有安装包:{{ .name }}"
SyncNodeConfig: "同步节点配置"
ErrNodePackagePath: "请填写子节点安装包的绝对文件路径"
ErrNodePackageFile: "处理安装包文件 {{ .name }} 失败:{{ .err }}"
ErrNodePackageMember: "安装包包含无效或重复条目:{{ .name }}"
ErrNodePackageMismatch: "安装包必须匹配主节点版本 {{ .version }} 和子节点架构 {{ .arch }}"
ErrNodePackageMissing: "安装包缺少必要文件:{{ .name }}"
ErrNodePackageVersion: "安装包版本 {{ .version }} 与主节点版本 {{ .expected }} 不一致"
ErrNodePackageArch: "Agent 程序架构与子节点架构 {{ .arch }} 不一致"
SendAgent: "下发节点安装包" SendAgent: "下发节点安装包"
StartService: "启动服务" StartService: "启动服务"
NoBackupNode: "当前备份节点为空,请选择备份节点保存后重试!" NoBackupNode: "当前备份节点为空,请选择备份节点保存后重试!"
+1 -3
View File
@@ -6,15 +6,13 @@ import (
) )
const ( const (
TokenizerDisallowedCharsPattern = `[^A-Za-z0-9._-]+` OrderByValidationPattern = `^[a-zA-Z_][a-zA-Z0-9_]*$`
OrderByValidationPattern = `^[a-zA-Z_][a-zA-Z0-9_]*$`
) )
var regexMap = make(map[string]*regexp.Regexp) var regexMap = make(map[string]*regexp.Regexp)
func Init() { func Init() {
patterns := []string{ patterns := []string{
TokenizerDisallowedCharsPattern,
OrderByValidationPattern, OrderByValidationPattern,
} }
+21
View File
@@ -779,6 +779,11 @@ export namespace AI {
} }
export interface AgentFeishuConfig { export interface AgentFeishuConfig {
builtinManaged?: boolean;
legacyConfigured?: boolean;
groupSessionScope?: string;
builtinAvailable?: boolean;
legacy?: boolean;
enabled: boolean; enabled: boolean;
threadSession: boolean; threadSession: boolean;
replyMode: string; replyMode: string;
@@ -793,6 +798,7 @@ export namespace AI {
} }
export interface AgentFeishuConfigUpdateReq { export interface AgentFeishuConfigUpdateReq {
groupSessionScope?: string;
agentId: number; agentId: number;
enabled: boolean; enabled: boolean;
threadSession: boolean; threadSession: boolean;
@@ -806,6 +812,21 @@ export namespace AI {
bots: AgentFeishuBot[]; 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 { export interface AgentTelegramConfigReq {
agentId: number; agentId: number;
} }
+4
View File
@@ -1,3 +1,4 @@
import { AI } from './ai';
import { ReqPage, CommonModel } from '.'; import { ReqPage, CommonModel } from '.';
export namespace App { export namespace App {
@@ -260,6 +261,7 @@ export namespace App {
} }
export interface AppInstalledOp { export interface AppInstalledOp {
feishuTransition?: AI.AgentFeishuTransition;
installId: number; installId: number;
operate: string; operate: string;
backupId?: number; backupId?: number;
@@ -285,6 +287,8 @@ export namespace App {
} }
export interface VersionDetail { export interface VersionDetail {
feishuLegacy?: boolean;
feishuConfigured?: boolean;
version: string; version: string;
detailId: number; detailId: number;
} }
+3
View File
@@ -136,6 +136,9 @@ export namespace Container {
mode: string; mode: string;
} }
export interface ContainerInfo { export interface ContainerInfo {
latestVersion?: string;
targetImage?: string;
hasUpdate?: boolean;
containerID: string; containerID: string;
name: string; name: string;
imageName: string; imageName: string;
+4
View File
@@ -272,6 +272,10 @@ export const getAgentFeishuConfig = (req: AI.AgentFeishuConfigReq) => {
return http.post<AI.AgentFeishuConfig>(`/ai/agents/channel/feishu/get`, req); 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) => { export const updateAgentFeishuConfig = (req: AI.AgentFeishuConfigUpdateReq) => {
return http.post(`/ai/agents/channel/feishu/update`, req); return http.post(`/ai/agents/channel/feishu/update`, req);
}; };
+30
View File
@@ -285,3 +285,33 @@ export const updateIpv6Option = (fixedCidrV6: string, ip6Tables: boolean, experi
export const updateDaemonJsonByfile = (params: Container.DaemonJsonUpdateByFile) => { export const updateDaemonJsonByfile = (params: Container.DaemonJsonUpdateByFile) => {
return http.post(`/containers/daemonjson/update/byfile`, params, TimeoutEnum.T_3M); return http.post(`/containers/daemonjson/update/byfile`, params, TimeoutEnum.T_3M);
}; };
export interface ContainerImageCheckUpdateStatus {
taskID: string;
enabled: boolean;
running: boolean;
checked: number;
updates: number;
failed: number;
skipped: number;
error: string;
finishedAt: number;
}
export const getContainerImageCheckUpdateStatus = (node: string, signal?: AbortSignal) =>
http.get<ContainerImageCheckUpdateStatus>(
'/containers/image-updates',
{},
{ headers: { CurrentNode: node }, signal },
);
export const runContainerImageCheckUpdate = (node: string) =>
http.postWithConfig<ContainerImageCheckUpdateStatus>(
'/containers/image-updates/check',
{},
{ headers: { CurrentNode: node } },
);
export const setContainerImageCheckUpdateSchedule = (enabled: boolean, node: string) =>
http.postWithConfig<ContainerImageCheckUpdateStatus>(
'/containers/image-updates/schedule',
{ enabled },
{ headers: { CurrentNode: node } },
);
@@ -1,5 +1,5 @@
<template> <template>
<DialogPro v-model="submitVisible" :title="header"> <DialogPro v-model="submitVisible" :title="header" size="mini">
<div> <div>
<span v-if="operationInfo" class="operation-info">{{ operationInfo }}</span> <span v-if="operationInfo" class="operation-info">{{ operationInfo }}</span>
<slot /> <slot />
+21 -19
View File
@@ -3,7 +3,7 @@
<component <component
:is="drawer ? DrawerPro : DialogPro" :is="drawer ? DrawerPro : DialogPro"
v-model="open" v-model="open"
v-bind="drawer ? { header: form.title, size: 'large' } : { title: form.title }" v-bind="drawer ? { header: form.title, size: 'large' } : { title: form.title, size: 'small' }"
@close="handleClose" @close="handleClose"
> >
<div v-loading="loading" class="del-dialog-body"> <div v-loading="loading" class="del-dialog-body">
@@ -56,8 +56,8 @@ const form = reactive({
api: null as Function, api: null as Function,
params: {}, params: {},
}); });
const loading = ref(false); const loading = ref();
const open = ref(false); const open = ref();
const successMsg = ref(''); const successMsg = ref('');
const noMsg = ref(false); const noMsg = ref(false);
@@ -68,8 +68,8 @@ interface DialogProps {
api: Function; api: Function;
params: object; params: object;
successMsg?: string; successMsg: string;
noMsg?: boolean; noMsg: boolean;
} }
const acceptParams = (props: DialogProps): void => { const acceptParams = (props: DialogProps): void => {
form.title = props.title; form.title = props.title;
@@ -78,32 +78,34 @@ const acceptParams = (props: DialogProps): void => {
form.api = props.api; form.api = props.api;
form.params = props.params; form.params = props.params;
successMsg.value = props.successMsg; successMsg.value = props.successMsg;
noMsg.value = props.noMsg ?? false; if (props.noMsg) {
noMsg.value = props.noMsg;
}
open.value = true; open.value = true;
}; };
const emit = defineEmits(['search', 'cancel', 'submit']); const emit = defineEmits(['search', 'cancel', 'submit']);
const onConfirm = async () => { const onConfirm = async () => {
if (loading.value) return;
if (form.api === null) { if (form.api === null) {
emit('submit'); emit('submit');
open.value = false; open.value = false;
return; return;
} }
loading.value = true; loading.value = true;
try { await form
await form.api(form.params); .api(form.params)
emit('search'); .then(() => {
if (!noMsg.value) { emit('search');
MsgSuccess(successMsg.value ?? i18n.global.t('commons.msg.deleteSuccess')); if (!noMsg.value) {
} MsgSuccess(successMsg.value ?? i18n.global.t('commons.msg.deleteSuccess'));
open.value = false; }
} catch { open.value = false;
// API errors are reported by the request interceptor; keep the dialog open for retry. loading.value = false;
} finally { })
loading.value = false; .catch(() => {
} loading.value = false;
});
}; };
const handleClose = () => { const handleClose = () => {
+26 -17
View File
@@ -5,11 +5,10 @@
:destroy-on-close="true" :destroy-on-close="true"
:close-on-click-modal="closeOnClickModal" :close-on-click-modal="closeOnClickModal"
:show-close="showClose" :show-close="showClose"
:width="width" :width="size"
:class="{ 'dialog-full-width': props.size === 'full', 'dialog-sized': !!props.size && props.size !== 'full' }"
append-to-body append-to-body
draggable draggable
@open="open" :open="open"
@opened="opened" @opened="opened"
:before-close="handleBeforeClose" :before-close="handleBeforeClose"
> >
@@ -47,7 +46,7 @@ const props = defineProps({
}, },
size: { size: {
type: String, type: String,
default: '', default: 'normal',
}, },
modelValue: { modelValue: {
type: Boolean, type: Boolean,
@@ -55,23 +54,33 @@ const props = defineProps({
}, },
}); });
// Explicit sizes preserve layouts such as imports; unspecified dialogs use the shared width.
const widths: Record<string, string> = {
mini: '20%',
small: '30%',
normal: '40%',
large: '50%',
full: '100%',
'w-60': '60%',
'w-70': '70%',
'w-90': '90%',
};
const width = computed(() => (props.size ? (widths[props.size] ?? '50%') : undefined));
const slots = useSlots(); const slots = useSlots();
const emit = defineEmits(['update:modelValue', 'close', 'open', 'opened']); const emit = defineEmits(['update:modelValue', 'close', 'open', 'opened']);
const size = computed(() => {
switch (props.size) {
case 'mini':
return '20%';
case 'small':
return '30%';
case 'normal':
return '40%';
case 'large':
return '50%';
case 'full':
return '100%';
case 'w-60':
return '60%';
case 'w-70':
return '70%';
case 'w-90':
return '90%';
default:
return '50%';
}
});
const dialogVisible = computed({ const dialogVisible = computed({
get() { get() {
return props.modelValue; return props.modelValue;
@@ -1,5 +1,5 @@
<template> <template>
<DialogPro v-model="open" :title="$t('commons.msg.infoTitle')"> <DialogPro v-model="open" :title="$t('commons.msg.infoTitle')" size="small">
<el-form ref="formRef" label-position="top" @submit.prevent> <el-form ref="formRef" label-position="top" @submit.prevent>
<el-form-item :label="$t('xpack.node.syncProxyHelper')"> <el-form-item :label="$t('xpack.node.syncProxyHelper')">
<el-radio-group v-model="restart"> <el-radio-group v-model="restart">
@@ -1,5 +1,5 @@
<template> <template>
<DialogPro v-model="open"> <DialogPro v-model="open" size="small">
<el-form ref="formRef" label-position="top" @submit.prevent> <el-form ref="formRef" label-position="top" @submit.prevent>
<el-form-item :label="title"> <el-form-item :label="title">
<el-radio-group v-model="restart"> <el-radio-group v-model="restart">
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<DialogPro v-model="dialogVisible" :title="$t('file.existFileTitle')"> <DialogPro v-model="dialogVisible" :title="$t('file.existFileTitle')" size="normal">
<el-alert :show-icon="true" type="warning" :closable="false"> <el-alert :show-icon="true" type="warning" :closable="false">
<div class="whitespace-break-spaces"> <div class="whitespace-break-spaces">
<span>{{ $t('file.existFileHelper') }}</span> <span>{{ $t('file.existFileHelper') }}</span>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<DialogPro :title="$t('file.list')" v-model="open" @close="handleClose"> <DialogPro :title="$t('file.list')" size="w-60" v-model="open" @close="handleClose">
<div> <div>
<div <div
v-show="!searchableStatus" v-show="!searchableStatus"
@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<DialogPro size="normal" v-model="open" class="level-up-pro" @close="handleClose"> <DialogPro v-model="open" class="level-up-pro" @close="handleClose">
<div style="text-align: center" v-loading="loading"> <div style="text-align: center" v-loading="loading">
<span class="text-3xl font-medium title"> <span class="text-3xl font-medium title">
{{ $t('license.importLicense') }} {{ $t('license.importLicense') }}
@@ -32,7 +32,12 @@
<div class="log-container" :style="styleVars"> <div class="log-container" :style="styleVars">
<div class="xterm-log-viewer" ref="terminalElement"></div> <div class="xterm-log-viewer" ref="terminalElement"></div>
</div> </div>
<DialogPro v-model="downloadDialogVisible" :title="$t('commons.button.download')" :close-on-click-modal="true"> <DialogPro
v-model="downloadDialogVisible"
:title="$t('commons.button.download')"
size="small"
:close-on-click-modal="true"
>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item :label="$t('container.fetch')"> <el-form-item :label="$t('container.fetch')">
<el-select v-model="downloadForm.mode" class="w-full"> <el-select v-model="downloadForm.mode" class="w-full">
+1 -4
View File
@@ -478,10 +478,7 @@ onMounted(async () => {
onUnmounted(() => { onUnmounted(() => {
onCloseLog(); onCloseLog();
if (resizeObserver && logContainer.value) { resizeObserver?.disconnect();
resizeObserver.unobserve(logContainer.value);
resizeObserver.disconnect();
}
}); });
defineExpose({ changeTail, onDownload, clearLog }); defineExpose({ changeTail, onDownload, clearLog });
+17 -6
View File
@@ -1,12 +1,20 @@
<template> <template>
<el-dialog v-model="open" :show-close="showClose" @closed="handleClose" :width="width"> <DialogPro
<div v-if="open"> v-model="open"
<LogFile :config="config" :showTail="showTail"></LogFile> :show-close="showClose"
</div> :close-on-click-modal="true"
@closed="handleClosed"
:width="width"
>
<template #content>
<div v-if="open">
<LogFile :config="config" :showTail="showTail"></LogFile>
</div>
</template>
<template #footer> <template #footer>
<slot name="task-footer"></slot> <slot name="task-footer"></slot>
</template> </template>
</el-dialog> </DialogPro>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
@@ -72,8 +80,11 @@ const openWithResourceID = (taskType: string, taskOperate: string, resourceID: n
const em = defineEmits(['close']); const em = defineEmits(['close']);
const handleClose = () => { const handleClose = () => {
em('close', true);
open.value = false; open.value = false;
};
const handleClosed = () => {
em('close', true);
bus.emit('refreshTask', true); bus.emit('refreshTask', true);
bus.emit('refreshApp', true); bus.emit('refreshApp', true);
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<DialogPro v-model="open" :title="$t('app.checkTitle')"> <DialogPro v-model="open" :title="$t('app.checkTitle')" size="small">
<el-alert :closable="false" :title="$t('setting.systemIPWarning')" type="info"> <el-alert :closable="false" :title="$t('setting.systemIPWarning')" type="info">
<el-link icon="Position" @click="jumpToPath(router, '/settings/panel')" type="primary"> <el-link icon="Position" @click="jumpToPath(router, '/settings/panel')" type="primary">
{{ $t('firewall.quickJump') }} {{ $t('firewall.quickJump') }}
@@ -13,14 +13,16 @@
@click.stop="handleButtonClick(button)" @click.stop="handleButtonClick(button)"
> >
<el-icon v-if="button.icon" class="mr-1"><component :is="button.icon" /></el-icon> <el-icon v-if="button.icon" class="mr-1"><component :is="button.icon" /></el-icon>
<span>{{ button.label }}</span> <el-badge is-dot :hidden="!hasDot(button)">
<span>{{ button.label }}</span>
</el-badge>
</el-button> </el-button>
</span> </span>
<span v-if="actions.more.length" class="fu-table-operations__action"> <span v-if="actions.more.length" class="fu-table-operations__action">
<el-dropdown class="fu-table-more-button" :trigger="trigger" @command="handleButtonClick"> <el-dropdown class="fu-table-more-button" :trigger="trigger" @command="handleButtonClick">
<span class="fu-table-operations__dropdown-trigger"> <span class="fu-table-operations__dropdown-trigger">
<el-button class="fu-table-operations__button" link type="primary"> <el-button class="fu-table-operations__button" link type="primary">
{{ t('fu.table.more') }} <el-badge is-dot :hidden="!actions.more.some(hasDot)">{{ t('fu.table.more') }}</el-badge>
</el-button> </el-button>
</span> </span>
<template #dropdown> <template #dropdown>
@@ -33,7 +35,9 @@
:divided="button.divided" :divided="button.divided"
> >
<el-icon v-if="button.icon" class="mr-1"><component :is="button.icon" /></el-icon> <el-icon v-if="button.icon" class="mr-1"><component :is="button.icon" /></el-icon>
<span>{{ button.label }}</span> <el-badge is-dot :hidden="!hasDot(button)">
<span>{{ button.label }}</span>
</el-badge>
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -75,6 +79,9 @@ const actions = computed(() => {
}; };
}); });
const hasDot = (button: FuTableOperationButton) =>
typeof button.dot === 'function' ? button.dot(props.row) : Boolean(button.dot);
const handleButtonClick = (button: FuTableOperationButton) => { const handleButtonClick = (button: FuTableOperationButton) => {
if (!isOperationDisabled(button, props.row)) { if (!isOperationDisabled(button, props.row)) {
button.click?.(props.row); button.click?.(props.row);
+1
View File
@@ -23,6 +23,7 @@ export interface FuTableOperationButton<Row = any> {
type?: string; type?: string;
icon?: any; icon?: any;
divided?: boolean; divided?: boolean;
dot?: boolean | ((row: Row) => boolean);
} }
export const FU_TABLE_STORAGE_PREFIX = 'FU-T-'; export const FU_TABLE_STORAGE_PREFIX = 'FU-T-';
@@ -15,6 +15,7 @@
v-if="isAdmin" v-if="isAdmin"
v-model="open" v-model="open"
:title="$t('menu.terminal')" :title="$t('menu.terminal')"
size="w-70"
:show-close="false" :show-close="false"
:modal="false" :modal="false"
modal-penetrable modal-penetrable
+34
View File
@@ -151,6 +151,30 @@ const checkIllegal: RuleValidator = (_rule, value, callback) => {
} }
}; };
const checkDatabaseUsername: RuleValidator = (_rule, value, callback) => {
if (value && /[&|;'`$()"><\n\r]/.test(value)) {
callback(new Error(i18n.global.t('commons.rule.illegalInput')));
return;
}
callback();
};
const checkDatabasePassword: RuleValidator = (_rule, value, callback) => {
if (!value) {
callback();
return;
}
if (value.includes(' ')) {
callback(new Error(i18n.global.t('setting.noSpace')));
return;
}
if (/[&|;'`$()"><\n\r]/.test(value)) {
callback(new Error(i18n.global.t('commons.rule.illegalInput')));
return;
}
callback();
};
const complexityPassword: RuleValidator = (_rule, value, callback) => { const complexityPassword: RuleValidator = (_rule, value, callback) => {
if (value === '' || typeof value === 'undefined' || value == null) { if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.complexityPassword'))); callback(new Error(i18n.global.t('commons.rule.complexityPassword')));
@@ -682,6 +706,8 @@ interface CommonRule {
userName: FormItemRule; userName: FormItemRule;
simpleName: FormItemRule; simpleName: FormItemRule;
simplePassword: FormItemRule; simplePassword: FormItemRule;
databaseUsername: FormItemRule;
databasePassword: FormItemRule;
dbName: FormItemRule; dbName: FormItemRule;
imageName: FormItemRule; imageName: FormItemRule;
composeName: FormItemRule; composeName: FormItemRule;
@@ -860,6 +886,14 @@ export const Rules: CommonRule = {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
}, },
databaseUsername: {
validator: checkDatabaseUsername,
trigger: 'blur',
},
databasePassword: {
validator: checkDatabasePassword,
trigger: 'blur',
},
port: { port: {
required: true, required: true,
trigger: 'blur', trigger: 'blur',
+35 -1
View File
@@ -292,7 +292,7 @@ const message = {
rePassword: 'Confirm password is inconsistent with the password.', rePassword: 'Confirm password is inconsistent with the password.',
requiredInput: 'This field is required.', requiredInput: 'This field is required.',
requiredSelect: 'Select an item in the list', requiredSelect: 'Select an item in the list',
illegalChar: 'Injection of characters & ; $ \' ` ( ) " > < | is currently not supported', illegalChar: "Injection of characters & ; $ ' ` ( ) \" > < {'|'} is currently not supported",
illegalInput: "This field mustn't contains illegal characters.", illegalInput: "This field mustn't contains illegal characters.",
duplicate: 'This value must be unique.', duplicate: 'This value must be unique.',
commonName: commonName:
@@ -774,6 +774,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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: deepSeekAccessHostHelper:
"Enter the IPv4 address or hostname used in your browser, without a scheme, port or path. HTTPS uses the application's local certificate.", "Enter the IPv4 address or hostname used in your browser, without a scheme, port or path. HTTPS uses the application's local certificate.",
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2076,6 +2095,14 @@ const message = {
missing: 'Missing', missing: 'Missing',
}, },
container: { container: {
imageCheckUpdates: 'Check for image updates',
imageCheckUpdateNow: 'Check now',
imageCheckUpdateSchedule: 'Scheduled checks',
imageCheckUpdateDisable: 'Disable scheduled checks',
imageCheckUpdateStarted: 'Checking images used by all containers on this node',
imageCheckUpdateResult:
'Check complete: {checked} checked, {updates} upgradable, {failed} failed, {skipped} skipped',
createByCommand: 'Create by command', createByCommand: 'Create by command',
importContainerBackupTip: 'Import container backup file, only .tar.gz is supported', importContainerBackupTip: 'Import container backup file, only .tar.gz is supported',
importComposeBackupTip: 'Import compose backup file, only .tar.gz is supported', importComposeBackupTip: 'Import compose backup file, only .tar.gz is supported',
@@ -6976,6 +7003,13 @@ const message = {
'Selected nodes will automatically start upgrading after the master node upgrade is completed, no manual operation required.', 'Selected nodes will automatically start upgrading after the master node upgrade is completed, no manual operation required.',
nodeUpgradePackageManage: 'Installation Package Management', nodeUpgradePackageManage: 'Installation Package Management',
nodeUpgradePackageClean: 'Clean', nodeUpgradePackageClean: 'Clean',
packageSource: 'Package source',
packageFromMaster: 'Main node',
packageFromNode: 'Existing package',
packagePath: 'Package path on child node',
packagePathHelper:
'Enter the absolute path to a .tar.gz package on the child node. Its version must match the main node and its architecture must match the child node. The original package is kept; missing supporting files are transferred from the main node.',
packagePathInvalid: 'Enter an absolute file path to the package on the child node',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'Only `1panel-agent_v*_*` or `1panel-v*-linux-*` installation packages are supported', 'Only `1panel-agent_v*_*` or `1panel-v*-linux-*` installation packages are supported',
nodeUpgradePackageHostConfirm: 'The server file {0} will be selected. Continue?', nodeUpgradePackageHostConfirm: 'The server file {0} will be selected. Continue?',
+35 -1
View File
@@ -291,7 +291,7 @@ const message = {
rePassword: 'La confirmación de la contraseña no coincide con la contraseña.', rePassword: 'La confirmación de la contraseña no coincide con la contraseña.',
requiredInput: 'Este campo es obligatorio.', requiredInput: 'Este campo es obligatorio.',
requiredSelect: 'Seleccione un elemento de la lista', requiredSelect: 'Seleccione un elemento de la lista',
illegalChar: 'Actualmente no se admite la inyección de caracteres & ; $ \' ` ( ) " > < |', illegalChar: "Actualmente no se admite la inyección de caracteres & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: 'Este campo no debe contener caracteres no permitidos.', illegalInput: 'Este campo no debe contener caracteres no permitidos.',
duplicate: 'Este valor debe ser único.', duplicate: 'Este valor debe ser único.',
commonName: commonName:
@@ -777,6 +777,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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: 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.', '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: deepSeekAuthHelper:
@@ -2094,6 +2113,14 @@ const message = {
missing: 'No existe', missing: 'No existe',
}, },
container: { container: {
imageCheckUpdates: 'Buscar actualizaciones de imágenes',
imageCheckUpdateNow: 'Comprobar ahora',
imageCheckUpdateSchedule: 'Comprobación programada',
imageCheckUpdateDisable: 'Desactivar comprobación programada',
imageCheckUpdateStarted: 'Buscando actualizaciones de las imágenes de todos los contenedores de este nodo',
imageCheckUpdateResult:
'Comprobación finalizada: comprobados {checked}, actualizables {updates}, fallidos {failed}, omitidos {skipped}',
createByCommand: 'Crear por comando', createByCommand: 'Crear por comando',
importContainerBackupTip: 'Importar archivo de copia de seguridad de contenedor, solo se admite .tar.gz', importContainerBackupTip: 'Importar archivo de copia de seguridad de contenedor, solo se admite .tar.gz',
importComposeBackupTip: 'Importar archivo de copia de seguridad de compose, solo se admite .tar.gz', importComposeBackupTip: 'Importar archivo de copia de seguridad de compose, solo se admite .tar.gz',
@@ -7088,6 +7115,13 @@ const message = {
'Los nodos seleccionados comenzarán a actualizarse automáticamente después de que se complete la actualización del nodo maestro, sin necesidad de operación manual.', 'Los nodos seleccionados comenzarán a actualizarse automáticamente después de que se complete la actualización del nodo maestro, sin necesidad de operación manual.',
nodeUpgradePackageManage: 'Gestión de Paquetes de Instalación', nodeUpgradePackageManage: 'Gestión de Paquetes de Instalación',
nodeUpgradePackageClean: 'Limpiar', nodeUpgradePackageClean: 'Limpiar',
packageSource: 'Origen del paquete',
packageFromMaster: 'Nodo principal',
packageFromNode: 'Paquete existente',
packagePath: 'Ruta del paquete en el nodo secundario',
packagePathHelper:
'Introduzca la ruta absoluta de un paquete .tar.gz en el nodo secundario. La versión debe coincidir con el nodo principal y la arquitectura con el nodo secundario. Se conserva el paquete original; los archivos auxiliares que falten se transfieren desde el nodo principal.',
packagePathInvalid: 'Introduzca una ruta absoluta al archivo del paquete en el nodo secundario',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'Solo se admiten paquetes de instalación `1panel-agent_v*_*` o `1panel-v*-linux-*`', 'Solo se admiten paquetes de instalación `1panel-agent_v*_*` o `1panel-v*-linux-*`',
nodeUpgradePackageHostConfirm: 'Se seleccionará el archivo del servidor {0}. ¿Continuar?', nodeUpgradePackageHostConfirm: 'Se seleccionará el archivo del servidor {0}. ¿Continuar?',
+35 -1
View File
@@ -291,7 +291,7 @@ const message = {
rePassword: 'تکرار رمز عبور با رمز عبور مطابقت ندارد.', rePassword: 'تکرار رمز عبور با رمز عبور مطابقت ندارد.',
requiredInput: 'این فیلد الزامی است.', requiredInput: 'این فیلد الزامی است.',
requiredSelect: 'یک مورد از لیست انتخاب کنید', requiredSelect: 'یک مورد از لیست انتخاب کنید',
illegalChar: 'کاراکترهای & ; $ \' ` ( ) " > < | در حال حاضر پشتیبانی نمی‌شوند', illegalChar: "کاراکترهای & ; $ ' ` ( ) \" > < {'|'} در حال حاضر پشتیبانی نمی‌شوند",
illegalInput: 'این فیلد نباید شامل کاراکترهای غیرمجاز باشد.', illegalInput: 'این فیلد نباید شامل کاراکترهای غیرمجاز باشد.',
duplicate: 'این مقدار باید یکتا باشد.', duplicate: 'این مقدار باید یکتا باشد.',
commonName: commonName:
@@ -769,6 +769,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
feishuUpgradeNotice:
'پیش از ارتقا باید افزونه قدیمی Feishu بررسی شود. پشتیبان کامل برنامه لازم است؛ در صورت شکست، تصویر قبلی و داده‌های مربوط بازیابی می‌شوند. افزونه پیکربندی‌نشده فقط غیرفعال می‌شود.',
feishuRecoveryNotice:
'در صورت شکست تغییر، بازیابی دروازه با Feishu غیرفعال انجام می‌شود. افزونه قدیمی دوباره فعال نخواهد شد. پشتیبان تنظیمات اصلی نگه داشته می‌شود.',
feishuRecover: 'غیرفعال کردن افزونه قدیمی و بازیابی راه‌اندازی',
feishuRecoverConfirm:
'از تنظیمات پشتیبان گرفته شود، افزونه قدیمی و پیکربندی‌نشده Feishu غیرفعال و نمونه دوباره راه‌اندازی شود؟ همه کانال‌ها موقتاً قطع می‌شوند. فایل‌های افزونه حفظ می‌شوند.',
feishuBuiltin: 'Feishu داخلی',
feishuImageRequired: 'از تصویری با Feishu داخلی استفاده کنید.',
feishuLegacy:
'افزونه قدیمی دیگر نگهداری نمی‌شود. پیش از تغییر، تنظیمات نشست، جریان پاسخ و اشاره را بررسی کنید.',
feishuBuiltinHint: 'این افزونه همراه تصویر OpenClaw به‌روز می‌شود.',
feishuSessionTopic: 'تفکیک بر اساس موضوع گروه',
feishuSessionTopicSender: 'تفکیک بر اساس موضوع گروه و فرستنده',
feishuSwitch: 'تغییر به افزونه داخلی',
feishuMentionChoice: 'پیش از تغییر مشخص کنید آیا اشاره لازم است.',
feishuSwitchConfirm:
'به Feishu داخلی تغییر داده و دوباره راه‌اندازی شود؟ همه کانال‌ها موقتاً قطع می‌شوند. ابزارهای ویژه افزونه قدیمی در دسترس نخواهند بود؛ جریان پاسخ و موضوع‌ها طبق انتخاب شما عمل می‌کنند و بعضی گفتگوها ممکن است نشست جدید بسازند. اعتبارنامه‌ها، اتصال‌ها و اطلاعات جفت‌سازی حفظ می‌شوند.',
deepSeekAccessHostHelper: deepSeekAccessHostHelper:
'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.', 'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.',
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2055,6 +2074,14 @@ const message = {
missing: 'ناموجود', missing: 'ناموجود',
}, },
container: { container: {
imageCheckUpdates: 'بررسی به‌روزرسانی ایمیج‌ها',
imageCheckUpdateNow: 'بررسی اکنون',
imageCheckUpdateSchedule: 'بررسی زمان‌بندی‌شده',
imageCheckUpdateDisable: 'غیرفعال‌کردن بررسی زمان‌بندی‌شده',
imageCheckUpdateStarted: 'در حال بررسی به‌روزرسانی ایمیج‌های همه کانتینرهای این گره',
imageCheckUpdateResult:
'بررسی پایان یافت: بررسی‌شده {checked}، قابل ارتقا {updates}، ناموفق {failed}، ردشده {skipped}',
createByCommand: 'ایجاد با دستور', createByCommand: 'ایجاد با دستور',
importContainerBackupTip: 'واردات فایل پشتیبان کانتینر، فقط .tar.gz پشتیبانی می‌شود', importContainerBackupTip: 'واردات فایل پشتیبان کانتینر، فقط .tar.gz پشتیبانی می‌شود',
importComposeBackupTip: 'واردات فایل پشتیبان compose، فقط .tar.gz پشتیبانی می‌شود', importComposeBackupTip: 'واردات فایل پشتیبان compose، فقط .tar.gz پشتیبانی می‌شود',
@@ -6932,6 +6959,13 @@ const message = {
'گره‌های انتخاب شده پس از اتمام ارتقاء گره اصلی، به طور خودکار شروع به ارتقاء می‌کنند، بدون نیاز به عملیات دستی.', 'گره‌های انتخاب شده پس از اتمام ارتقاء گره اصلی، به طور خودکار شروع به ارتقاء می‌کنند، بدون نیاز به عملیات دستی.',
nodeUpgradePackageManage: 'مدیریت بسته نصب', nodeUpgradePackageManage: 'مدیریت بسته نصب',
nodeUpgradePackageClean: 'پاک‌سازی', nodeUpgradePackageClean: 'پاک‌سازی',
packageSource: 'منبع بسته',
packageFromMaster: 'گره اصلی',
packageFromNode: 'بسته موجود',
packagePath: 'مسیر بسته در گره فرزند',
packagePathHelper:
'مسیر مطلق بسته .tar.gz در گره فرزند را وارد کنید. نسخه باید با گره اصلی و معماری با گره فرزند مطابقت داشته باشد. بسته اصلی حفظ می‌شود؛ فایل‌های کمکی ناموجود از گره اصلی منتقل می‌شوند.',
packagePathInvalid: 'مسیر مطلق فایل بسته در گره فرزند را وارد کنید',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'فقط بسته‌های نصب `1panel-agent_v*_*` یا `1panel-v*-linux-*` پشتیبانی می‌شوند', 'فقط بسته‌های نصب `1panel-agent_v*_*` یا `1panel-v*-linux-*` پشتیبانی می‌شوند',
nodeUpgradePackageHostConfirm: 'فایل سرور {0} انتخاب خواهد شد. ادامه می‌دهید؟', nodeUpgradePackageHostConfirm: 'فایل سرور {0} انتخاب خواهد شد. ادامه می‌دهید؟',
+34 -1
View File
@@ -292,7 +292,7 @@ const message = {
rePassword: 'パスワードがパスワードと矛盾することを確認してください。', rePassword: 'パスワードがパスワードと矛盾することを確認してください。',
requiredInput: 'この項目は必須です。', requiredInput: 'この項目は必須です。',
requiredSelect: 'リスト内のアイテムを選択します', requiredSelect: 'リスト内のアイテムを選択します',
illegalChar: '現在、文字 & ; $ \' ` ( ) " > < | の注入はサポートされていません', illegalChar: "現在、文字 & ; $ ' ` ( ) \" > < {'|'} の注入はサポートされていません",
illegalInput: 'このフィールドには違法なキャラクターが含まれてはなりません。', illegalInput: 'このフィールドには違法なキャラクターが含まれてはなりません。',
duplicate: 'この値は一意である必要があります。', duplicate: 'この値は一意である必要があります。',
commonName: commonName:
@@ -773,6 +773,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
feishuUpgradeNotice:
'更新前に旧 Feishu プラグインを処理します。アプリ全体のバックアップが必要です。失敗時は旧イメージと対応するデータを復元します。未設定の旧プラグインは無効化のみ行います。',
feishuRecoveryNotice:
'切り替え失敗時は Feishu を無効にした状態でゲートウェイの復旧を試みます。旧プラグインは再有効化しません。元の設定のバックアップは保持します。',
feishuRecover: '旧プラグインを無効化して起動を復旧',
feishuRecoverConfirm:
'設定をバックアップし、未設定の旧 Feishu プラグインを無効化して再起動しますか?全チャネルが一時中断されます。プラグインのファイルは保持します。',
feishuBuiltin: '組み込み Feishu',
feishuImageRequired: 'Feishu を内蔵したイメージを使用してください。',
feishuLegacy:
'旧プラグインの保守は終了しました。切り替え前にセッション分離、ストリーミング、メンション設定を確認してください。',
feishuBuiltinHint: 'このプラグインは OpenClaw イメージとともに更新されます。',
feishuSessionTopic: 'グループのトピック別',
feishuSessionTopicSender: 'グループのトピックと送信者別',
feishuSwitch: '組み込みプラグインに切り替え',
feishuMentionChoice: '切り替え前にメンションが必要か選択してください。',
feishuSwitchConfirm:
'内蔵 Feishu に切り替えて再起動しますか?全チャネルが一時中断され、旧プラグイン専用ツールは利用できなくなります。ストリーミングとトピックは選択した設定に従い、一部の会話は新しいセッションになります。認証情報、紐付け、ペアリング情報は保持します。',
deepSeekAccessHostHelper: deepSeekAccessHostHelper:
'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。', 'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。',
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2060,6 +2079,13 @@ const message = {
missing: '存在しない', missing: '存在しない',
}, },
container: { container: {
imageCheckUpdates: 'イメージの更新を確認',
imageCheckUpdateNow: '今すぐ確認',
imageCheckUpdateSchedule: '定期チェック',
imageCheckUpdateDisable: '定期チェックを無効にする',
imageCheckUpdateStarted: 'このノードのすべてのコンテナで使用されているイメージの更新を確認しています',
imageCheckUpdateResult: '確認完了:成功 {checked}、更新可能 {updates}、失敗 {failed}、スキップ {skipped}',
createByCommand: 'コマンドで作成', createByCommand: 'コマンドで作成',
importContainerBackupTip: 'コンテナバックアップファイルをインポートします。.tar.gz のみ対応です', importContainerBackupTip: 'コンテナバックアップファイルをインポートします。.tar.gz のみ対応です',
importComposeBackupTip: 'Compose バックアップファイルをインポートします。.tar.gz のみ対応です', importComposeBackupTip: 'Compose バックアップファイルをインポートします。.tar.gz のみ対応です',
@@ -6963,6 +6989,13 @@ const message = {
'選択したノードは、マスターノードのアップグレード完了後に自動的にアップグレードを開始し、手動操作は不要です。', '選択したノードは、マスターノードのアップグレード完了後に自動的にアップグレードを開始し、手動操作は不要です。',
nodeUpgradePackageManage: 'インストールパッケージ管理', nodeUpgradePackageManage: 'インストールパッケージ管理',
nodeUpgradePackageClean: 'クリーンアップ', nodeUpgradePackageClean: 'クリーンアップ',
packageSource: 'パッケージの取得元',
packageFromMaster: 'メインノード',
packageFromNode: '既存パッケージ',
packagePath: '子ノード上のパッケージパス',
packagePathHelper:
'子ノード上の .tar.gz パッケージの絶対パスを入力してください。バージョンはメインノード、アーキテクチャは子ノードと一致する必要があります。元のパッケージは保持され、不足する補助ファイルはメインノードから転送されます。',
packagePathInvalid: '子ノード上のパッケージの絶対ファイルパスを入力してください',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'`1panel-agent_v*_*` または `1panel-v*-linux-*` のインストールパッケージのみ対応しています', '`1panel-agent_v*_*` または `1panel-v*-linux-*` のインストールパッケージのみ対応しています',
nodeUpgradePackageHostConfirm: 'サーバーファイル {0} を選択します。続行しますか?', nodeUpgradePackageHostConfirm: 'サーバーファイル {0} を選択します。続行しますか?',
+34 -1
View File
@@ -290,7 +290,7 @@ const message = {
rePassword: '확인 비밀번호가 비밀번호와 일치하지 않습니다.', rePassword: '확인 비밀번호가 비밀번호와 일치하지 않습니다.',
requiredInput: '이 필드는 필수 항목입니다.', requiredInput: '이 필드는 필수 항목입니다.',
requiredSelect: '목록에서 항목을 선택하세요', requiredSelect: '목록에서 항목을 선택하세요',
illegalChar: '현재 & ; $ \' ` ( ) " > < | 문자 주입은 지원되지 않습니다', illegalChar: "현재 & ; $ ' ` ( ) \" > < {'|'} 문자 주입은 지원되지 않습니다",
illegalInput: '이 필드에는 유효하지 않은 문자가 포함될 수 없습니다.', illegalInput: '이 필드에는 유효하지 않은 문자가 포함될 수 없습니다.',
duplicate: '이 값은 고유해야 합니다.', duplicate: '이 값은 고유해야 합니다.',
commonName: commonName:
@@ -764,6 +764,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
feishuUpgradeNotice:
'업그레이드 전에 기존 Feishu 플러그인을 처리합니다. 전체 앱 백업이 필요하며 실패하면 이전 이미지와 해당 데이터를 복원합니다. 미설정 플러그인은 비활성화만 합니다.',
feishuRecoveryNotice:
'전환에 실패하면 Feishu를 비활성화한 상태로 게이트웨이 복구를 시도합니다. 기존 플러그인은 다시 활성화하지 않으며 원본 설정 백업을 보관합니다.',
feishuRecover: '기존 플러그인 비활성화 및 시작 복구',
feishuRecoverConfirm:
'설정을 백업하고 미설정 Feishu 플러그인을 비활성화한 뒤 다시 시작할까요? 모든 채널이 잠시 중단됩니다. 플러그인 파일은 유지합니다.',
feishuBuiltin: '내장 Feishu',
feishuImageRequired: 'Feishu가 내장된 이미지를 사용하세요.',
feishuLegacy:
'기존 플러그인의 유지 관리가 종료되었습니다. 전환 전에 세션 분리, 스트리밍 및 멘션 설정을 확인하세요.',
feishuBuiltinHint: '이 플러그인은 OpenClaw 이미지와 함께 업데이트됩니다.',
feishuSessionTopic: '그룹 주제별 분리',
feishuSessionTopicSender: '그룹 주제 및 발신자별 분리',
feishuSwitch: '내장 플러그인으로 전환',
feishuMentionChoice: '전환 전에 멘션 필요 여부를 명시적으로 선택하세요.',
feishuSwitchConfirm:
'내장 Feishu로 전환하고 다시 시작할까요? 모든 채널이 잠시 중단되고 기존 전용 도구는 사용할 수 없습니다. 스트리밍과 주제 동작은 선택한 설정을 따르며 일부 대화는 새 세션으로 시작됩니다. 자격 증명, 연결 및 페어링 정보는 유지됩니다.',
deepSeekAccessHostHelper: deepSeekAccessHostHelper:
'브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.', '브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.',
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2036,6 +2055,13 @@ const message = {
missing: '없음', missing: '없음',
}, },
container: { container: {
imageCheckUpdates: '이미지 업데이트 확인',
imageCheckUpdateNow: '지금 확인',
imageCheckUpdateSchedule: '정기 확인',
imageCheckUpdateDisable: '정기 확인 끄기',
imageCheckUpdateStarted: '현재 노드의 모든 컨테이너에서 사용하는 이미지의 업데이트를 확인하고 있습니다',
imageCheckUpdateResult: '확인 완료: 성공 {checked}, 업데이트 가능 {updates}, 실패 {failed}, 건너뜀 {skipped}',
createByCommand: '명령으로 생성', createByCommand: '명령으로 생성',
importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다', importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
importComposeBackupTip: 'Compose 백업 파일 가져오기, .tar.gz 형식만 지원합니다', importComposeBackupTip: 'Compose 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
@@ -6821,6 +6847,13 @@ const message = {
'선택된 노드는 마스터 노드 업그레이드 완료 후 자동으로 업그레이드를 시작하며 수동 작업이 필요하지 않습니다.', '선택된 노드는 마스터 노드 업그레이드 완료 후 자동으로 업그레이드를 시작하며 수동 작업이 필요하지 않습니다.',
nodeUpgradePackageManage: '설치 패키지 관리', nodeUpgradePackageManage: '설치 패키지 관리',
nodeUpgradePackageClean: '정리', nodeUpgradePackageClean: '정리',
packageSource: '패키지 소스',
packageFromMaster: '주 노드',
packageFromNode: '기존 패키지',
packagePath: '하위 노드의 패키지 경로',
packagePathHelper:
'하위 노드에 있는 .tar.gz 패키지의 절대 경로를 입력하세요. 버전은 주 노드와, 아키텍처는 하위 노드와 일치해야 합니다. 원본 패키지는 유지되며 누락된 보조 파일은 주 노드에서 전송됩니다.',
packagePathInvalid: '하위 노드에 있는 패키지의 절대 파일 경로를 입력하세요',
nodeUpgradePackageFormatHelper: '`1panel-agent_v*_*` 또는 `1panel-v*-linux-*` 설치 패키지만 지원됩니다', nodeUpgradePackageFormatHelper: '`1panel-agent_v*_*` 또는 `1panel-v*-linux-*` 설치 패키지만 지원됩니다',
nodeUpgradePackageHostConfirm: '서버 파일 {0}을(를) 선택합니다. 계속하시겠습니까?', nodeUpgradePackageHostConfirm: '서버 파일 {0}을(를) 선택합니다. 계속하시겠습니까?',
nodeUpgradePackageCleanHelper: '모든 낮은 버전의 업데이트 패키지를 정리합니다. 계속하시겠습니까?', nodeUpgradePackageCleanHelper: '모든 낮은 버전의 업데이트 패키지를 정리합니다. 계속하시겠습니까?',
+33 -1
View File
@@ -288,7 +288,7 @@ const message = {
rePassword: 'ຢືນຢັນລະຫັດຜ່ານບໍ່ກົງກັນ.', rePassword: 'ຢືນຢັນລະຫັດຜ່ານບໍ່ກົງກັນ.',
requiredInput: 'ຈຳເປັນຕ້ອງປ້ອນຂໍ້ມູນນີ້.', requiredInput: 'ຈຳເປັນຕ້ອງປ້ອນຂໍ້ມູນນີ້.',
requiredSelect: 'ເລືອກລາຍການໃນລາຍຊື່', requiredSelect: 'ເລືອກລາຍການໃນລາຍຊື່',
illegalChar: 'ບໍ່ຮອງຮັບການໃຊ້ຕົວອັກສອນ & ; $ \' ` ( ) " > < | ໃນເວລານີ້', illegalChar: "ບໍ່ຮອງຮັບການໃຊ້ຕົວອັກສອນ & ; $ ' ` ( ) \" > < {'|'} ໃນເວລານີ້",
illegalInput: 'ຂໍ້ມູນນີ້ຕ້ອງບໍ່ມີຕົວອັກສອນທີ່ບໍ່ຖືກຕ້ອງ.', illegalInput: 'ຂໍ້ມູນນີ້ຕ້ອງບໍ່ມີຕົວອັກສອນທີ່ບໍ່ຖືກຕ້ອງ.',
duplicate: 'ຄ່ານີ້ຕ້ອງບໍ່ຊ້ຳກັນ.', duplicate: 'ຄ່ານີ້ຕ້ອງບໍ່ຊ້ຳກັນ.',
commonName: commonName:
@@ -762,6 +762,24 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
feishuUpgradeNotice:
'ຕ້ອງຈັດການປລັກອິນ Feishu ເກົ່າກ່ອນອັບເກຣດ. ຕ້ອງສຳຮອງແອັບທັງໝົດ; ຖ້າລົ້ມເຫຼວຈະກູ້ຄືນອິມເມຈເກົ່າແລະຂໍ້ມູນ. ປລັກອິນທີ່ຍັງບໍ່ຕັ້ງຄ່າຈະຖືກປິດເທົ່ານັ້ນ.',
feishuRecoveryNotice:
'ຖ້າການປ່ຽນລົ້ມເຫຼວ ຈະພະຍາຍາມກູ້ຄືນເກດເວໂດຍປິດ Feishu. ຈະບໍ່ເປີດປລັກອິນເກົ່າຄືນ. ເກັບສຳຮອງການຕັ້ງຄ່າເດີມໄວ້.',
feishuRecover: 'ປິດປລັກອິນເກົ່າແລະກູ້ຄືນການເລີ່ມ',
feishuRecoverConfirm:
'ສຳຮອງການຕັ້ງຄ່າ, ປິດປລັກອິນ Feishu ເກົ່າທີ່ຍັງບໍ່ຕັ້ງຄ່າ ແລະເລີ່ມໃໝ່ບໍ? ທຸກຊ່ອງທາງຈະຢຸດຊົ່ວຄາວ. ໄຟລ໌ປລັກອິນຍັງຖືກເກັບໄວ້.',
feishuBuiltin: 'Feishu ໃນຕົວ',
feishuImageRequired: 'ໃຊ້ອິມເມຈທີ່ມີ Feishu ໃນຕົວ.',
feishuLegacy: 'ປລັກອິນເກົ່າຢຸດບຳລຸງຮັກສາແລ້ວ. ກວດການແຍກເຊດຊັນ, ສະຕຣີມ ແລະ ການເອີ້ນ @ ກ່ອນສະຫຼັບ.',
feishuBuiltinHint: 'ປລັກອິນນີ້ອັບເດດພ້ອມອິມເມຈ OpenClaw.',
feishuSessionTopic: 'ແຍກຕາມຫົວຂໍ້ກຸ່ມ',
feishuSessionTopicSender: 'ແຍກຕາມຫົວຂໍ້ກຸ່ມ ແລະ ຜູ້ສົ່ງ',
feishuSwitch: 'ສະຫຼັບເປັນປລັກອິນໃນຕົວ',
feishuMentionChoice: 'ກ່ອນສະຫຼັບ ກະລຸນາເລືອກວ່າຕ້ອງການ @ ຫຼືບໍ່.',
feishuSwitchConfirm:
'ປ່ຽນເປັນ Feishu ໃນຕົວແລະເລີ່ມໃໝ່ບໍ? ທຸກຊ່ອງທາງຈະຢຸດຊົ່ວຄາວ. ເຄື່ອງມືສະເພາະຂອງປລັກອິນເກົ່າຈະໃຊ້ບໍ່ໄດ້; ການສົ່ງແບບສະຕຣີມແລະຫົວຂໍ້ຈະຕາມຄ່າທີ່ເລືອກ ແລະບາງການສົນທະນາອາດເລີ່ມເຊສຊັນໃໝ່. ຂໍ້ມູນຢືນຢັນ, ການຜູກ ແລະການຈັບຄູ່ຈະຖືກເກັບໄວ້.',
deepSeekAccessHostHelper: deepSeekAccessHostHelper:
'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.', 'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.',
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2023,6 +2041,13 @@ const message = {
missing: 'ບໍ່ມີ', missing: 'ບໍ່ມີ',
}, },
container: { container: {
imageCheckUpdates: 'ກວດສອບການອັບເດດອິມເມຈ',
imageCheckUpdateNow: 'ກວດສອບຕອນນີ້',
imageCheckUpdateSchedule: 'ກວດສອບຕາມຕາຕະລາງ',
imageCheckUpdateDisable: 'ປິດການກວດສອບຕາມຕາຕະລາງ',
imageCheckUpdateStarted: 'ກຳລັງກວດສອບການອັບເດດອິມເມຈຂອງທຸກຄອນເທນເນີໃນໂນດນີ້',
imageCheckUpdateResult: 'ກວດສອບສຳເລັດ: ກວດແລ້ວ {checked}, ອັບເກຣດໄດ້ {updates}, ລົ້ມເຫຼວ {failed}, ຂ້າມ {skipped}',
createByCommand: 'ສ້າງດ້ວຍຄຳສັ່ງ', createByCommand: 'ສ້າງດ້ວຍຄຳສັ່ງ',
importContainerBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງຄອນເທນເນີ, ຮອງຮັບສະເພາະ .tar.gz', importContainerBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງຄອນເທນເນີ, ຮອງຮັບສະເພາະ .tar.gz',
importComposeBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງ compose, ຮອງຮັບສະເພາະ .tar.gz', importComposeBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງ compose, ຮອງຮັບສະເພາະ .tar.gz',
@@ -6750,6 +6775,13 @@ const message = {
nodeUpgradeHelper: 'ໂນດທີ່ເລືອກຈະເລີ່ມອັບເກຣດອັດຕະໂນມັດຫຼັງຈາກໂນດຫຼັກອັບເກຣດສຳເລັດ.', nodeUpgradeHelper: 'ໂນດທີ່ເລືອກຈະເລີ່ມອັບເກຣດອັດຕະໂນມັດຫຼັງຈາກໂນດຫຼັກອັບເກຣດສຳເລັດ.',
nodeUpgradePackageManage: 'ຈັດການແພັກເກັດຕິດຕັ້ງ', nodeUpgradePackageManage: 'ຈັດການແພັກເກັດຕິດຕັ້ງ',
nodeUpgradePackageClean: 'ລ້າງຂໍ້ມູນ', nodeUpgradePackageClean: 'ລ້າງຂໍ້ມູນ',
packageSource: 'ແຫຼ່ງທີ່ມາຂອງແພັກເກດ',
packageFromMaster: 'ໂນດຫຼັກ',
packageFromNode: 'ແພັກເກດທີ່ມີຢູ່',
packagePath: 'ເສັ້ນທາງແພັກເກດໃນໂນດລູກ',
packagePathHelper:
'ປ້ອນເສັ້ນທາງແບບເຕັມຂອງແພັກເກດ .tar.gz ໃນໂນດລູກ. ເວີຊັນຕ້ອງກົງກັບໂນດຫຼັກ ແລະ ສະຖາປັດຕະຍະກຳຕ້ອງກົງກັບໂນດລູກ. ແພັກເກດຕົ້ນສະບັບຈະຖືກເກັບໄວ້; ໄຟລ໌ປະກອບທີ່ຂາດຈະຖືກໂອນຈາກໂນດຫຼັກ.',
packagePathInvalid: 'ກະລຸນາປ້ອນເສັ້ນທາງໄຟລ໌ແບບເຕັມຂອງແພັກເກດໃນໂນດລູກ',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'ຮອງຮັບສະເພາະແພັກເກັດ `1panel-agent_v*_*` ຫຼື `1panel-v*-linux-*` ເທົ່ານັ້ນ', 'ຮອງຮັບສະເພາະແພັກເກັດ `1panel-agent_v*_*` ຫຼື `1panel-v*-linux-*` ເທົ່ານັ້ນ',
nodeUpgradePackageHostConfirm: 'ໄຟລ໌ເຊີເວີ {0} ຈະຖືກເລືອກ. ຕ້ອງການຕໍ່ບໍ?', nodeUpgradePackageHostConfirm: 'ໄຟລ໌ເຊີເວີ {0} ຈະຖືກເລືອກ. ຕ້ອງການຕໍ່ບໍ?',
+35 -1
View File
@@ -295,7 +295,7 @@ const message = {
rePassword: 'Pengesahan kata laluan tidak sepadan dengan kata laluan.', rePassword: 'Pengesahan kata laluan tidak sepadan dengan kata laluan.',
requiredInput: 'Ruangan ini wajib diisi.', requiredInput: 'Ruangan ini wajib diisi.',
requiredSelect: 'Pilih satu item dalam senarai', requiredSelect: 'Pilih satu item dalam senarai',
illegalChar: 'Suntikan aksara & ; $ \' ` ( ) " > < | tidak disokong buat masa ini', illegalChar: "Suntikan aksara & ; $ ' ` ( ) \" > < {'|'} tidak disokong buat masa ini",
illegalInput: 'Ruangan ini tidak boleh mengandungi aksara tidak sah.', illegalInput: 'Ruangan ini tidak boleh mengandungi aksara tidak sah.',
duplicate: 'Nilai ini mesti unik.', duplicate: 'Nilai ini mesti unik.',
commonName: commonName:
@@ -782,6 +782,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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: deepSeekAccessHostHelper:
'Masukkan alamat IPv4 atau nama hos yang digunakan dalam pelayar, tanpa protokol, port atau laluan. HTTPS menggunakan sijil setempat aplikasi.', 'Masukkan alamat IPv4 atau nama hos yang digunakan dalam pelayar, tanpa protokol, port atau laluan. HTTPS menggunakan sijil setempat aplikasi.',
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2091,6 +2110,14 @@ const message = {
missing: 'Tiada', missing: 'Tiada',
}, },
container: { container: {
imageCheckUpdates: 'Semak kemas kini imej',
imageCheckUpdateNow: 'Semak sekarang',
imageCheckUpdateSchedule: 'Semakan berjadual',
imageCheckUpdateDisable: 'Matikan semakan berjadual',
imageCheckUpdateStarted: 'Menyemak kemas kini imej yang digunakan oleh semua kontena pada nod ini',
imageCheckUpdateResult:
'Semakan selesai: disemak {checked}, boleh dikemas kini {updates}, gagal {failed}, dilangkau {skipped}',
createByCommand: 'Cipta melalui perintah', createByCommand: 'Cipta melalui perintah',
importContainerBackupTip: 'Import fail sandaran kontena, hanya format .tar.gz disokong', importContainerBackupTip: 'Import fail sandaran kontena, hanya format .tar.gz disokong',
importComposeBackupTip: 'Import fail sandaran compose, hanya format .tar.gz disokong', importComposeBackupTip: 'Import fail sandaran compose, hanya format .tar.gz disokong',
@@ -7070,6 +7097,13 @@ const message = {
'Nod terpilih akan mula dinaik taraf secara automatik selepas naik taraf nod induk selesai, tiada operasi manual diperlukan.', 'Nod terpilih akan mula dinaik taraf secara automatik selepas naik taraf nod induk selesai, tiada operasi manual diperlukan.',
nodeUpgradePackageManage: 'Pengurusan Pakej Pemasangan', nodeUpgradePackageManage: 'Pengurusan Pakej Pemasangan',
nodeUpgradePackageClean: 'Bersihkan', nodeUpgradePackageClean: 'Bersihkan',
packageSource: 'Sumber pakej',
packageFromMaster: 'Nod utama',
packageFromNode: 'Pakej sedia ada',
packagePath: 'Laluan pakej pada nod anak',
packagePathHelper:
'Masukkan laluan mutlak pakej .tar.gz pada nod anak. Versi mesti sepadan dengan nod utama dan seni bina mesti sepadan dengan nod anak. Pakej asal dikekalkan; fail sokongan yang tiada dipindahkan daripada nod utama.',
packagePathInvalid: 'Masukkan laluan fail mutlak pakej pada nod anak',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'Hanya pakej pemasangan `1panel-agent_v*_*` atau `1panel-v*-linux-*` disokong', 'Hanya pakej pemasangan `1panel-agent_v*_*` atau `1panel-v*-linux-*` disokong',
nodeUpgradePackageHostConfirm: 'Fail pelayan {0} akan dipilih. Teruskan?', nodeUpgradePackageHostConfirm: 'Fail pelayan {0} akan dipilih. Teruskan?',
+35 -1
View File
@@ -292,7 +292,7 @@ const message = {
rePassword: 'A confirmação da senha não corresponde à senha.', rePassword: 'A confirmação da senha não corresponde à senha.',
requiredInput: 'Este campo é obrigatório.', requiredInput: 'Este campo é obrigatório.',
requiredSelect: 'Selecione um item na lista', requiredSelect: 'Selecione um item na lista',
illegalChar: 'Atualmente não há suporte para injeção dos caracteres & ; $ \' ` ( ) " > < |', illegalChar: "Atualmente não há suporte para injeção dos caracteres & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: 'Este campo não deve conter caracteres ilegais.', illegalInput: 'Este campo não deve conter caracteres ilegais.',
duplicate: 'Este valor deve ser único.', duplicate: 'Este valor deve ser único.',
commonName: commonName:
@@ -775,6 +775,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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: deepSeekAccessHostHelper:
'Informe o IPv4 ou nome do host usado no navegador, sem protocolo, porta ou caminho. O HTTPS usa o certificado local do aplicativo.', 'Informe o IPv4 ou nome do host usado no navegador, sem protocolo, porta ou caminho. O HTTPS usa o certificado local do aplicativo.',
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2093,6 +2112,14 @@ const message = {
missing: 'Não existe', missing: 'Não existe',
}, },
container: { container: {
imageCheckUpdates: 'Verificar atualizações de imagens',
imageCheckUpdateNow: 'Verificar agora',
imageCheckUpdateSchedule: 'Verificação agendada',
imageCheckUpdateDisable: 'Desativar verificação agendada',
imageCheckUpdateStarted: 'Verificando atualizações das imagens de todos os contêineres deste nó',
imageCheckUpdateResult:
'Verificação concluída: verificados {checked}, atualizáveis {updates}, falhas {failed}, ignorados {skipped}',
createByCommand: 'Criar por comando', createByCommand: 'Criar por comando',
importContainerBackupTip: 'Importar arquivo de backup de contêiner, apenas .tar.gz é suportado', importContainerBackupTip: 'Importar arquivo de backup de contêiner, apenas .tar.gz é suportado',
importComposeBackupTip: 'Importar arquivo de backup de compose, apenas .tar.gz é suportado', importComposeBackupTip: 'Importar arquivo de backup de compose, apenas .tar.gz é suportado',
@@ -7113,6 +7140,13 @@ const message = {
'Os nós selecionados começarão a atualizar automaticamente após a conclusão da atualização do nó mestre, nenhuma operação manual necessária.', 'Os nós selecionados começarão a atualizar automaticamente após a conclusão da atualização do nó mestre, nenhuma operação manual necessária.',
nodeUpgradePackageManage: 'Gerenciamento de Pacotes de Instalação', nodeUpgradePackageManage: 'Gerenciamento de Pacotes de Instalação',
nodeUpgradePackageClean: 'Limpar', nodeUpgradePackageClean: 'Limpar',
packageSource: 'Origem do pacote',
packageFromMaster: 'Nó principal',
packageFromNode: 'Pacote existente',
packagePath: 'Caminho do pacote no nó secundário',
packagePathHelper:
'Informe o caminho absoluto de um pacote .tar.gz no nó secundário. A versão deve corresponder à do nó principal e a arquitetura à do nó secundário. O pacote original é mantido; arquivos auxiliares ausentes são transferidos do nó principal.',
packagePathInvalid: 'Informe um caminho absoluto para o arquivo do pacote no nó secundário',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'Somente pacotes de instalação `1panel-agent_v*_*` ou `1panel-v*-linux-*` são suportados', 'Somente pacotes de instalação `1panel-agent_v*_*` ou `1panel-v*-linux-*` são suportados',
nodeUpgradePackageHostConfirm: 'O arquivo do servidor {0} será selecionado. Continuar?', nodeUpgradePackageHostConfirm: 'O arquivo do servidor {0} será selecionado. Continuar?',
+35 -1
View File
@@ -293,7 +293,7 @@ const message = {
rePassword: 'Подтверждение пароля не совпадает с паролем.', rePassword: 'Подтверждение пароля не совпадает с паролем.',
requiredInput: 'Это поле обязательно для заполнения.', requiredInput: 'Это поле обязательно для заполнения.',
requiredSelect: 'Выберите элемент из списка', requiredSelect: 'Выберите элемент из списка',
illegalChar: 'В настоящее время не поддерживается вставка символов & ; $ \' ` ( ) " > < |', illegalChar: "В настоящее время не поддерживается вставка символов & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: 'Это поле не должно содержать недопустимых символов.', illegalInput: 'Это поле не должно содержать недопустимых символов.',
duplicate: 'Значение должно быть уникальным.', duplicate: 'Значение должно быть уникальным.',
commonName: commonName:
@@ -773,6 +773,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
feishuUpgradeNotice:
'Перед обновлением необходимо обработать старый плагин Feishu. Требуется полная резервная копия; при сбое восстанавливаются прежний образ и его данные. Ненастроенный плагин только отключается.',
feishuRecoveryNotice:
'При сбое переключения будет предпринята попытка восстановить шлюз с отключённым Feishu. Старый плагин не включается повторно. Исходная резервная копия сохраняется.',
feishuRecover: 'Отключить старый плагин и восстановить запуск',
feishuRecoverConfirm:
'Создать резервную копию, отключить ненастроенный старый плагин Feishu и перезапустить? Все каналы будут кратковременно прерваны. Файлы плагина сохраняются.',
feishuBuiltin: 'Встроенный Feishu',
feishuImageRequired: 'Используйте образ со встроенным Feishu.',
feishuLegacy:
'Старый плагин больше не поддерживается. Перед переключением проверьте разделение сессий, потоковые ответы и упоминания.',
feishuBuiltinHint: 'Плагин обновляется вместе с образом OpenClaw.',
feishuSessionTopic: 'Разделять по теме группы',
feishuSessionTopicSender: 'Разделять по теме группы и отправителю',
feishuSwitch: 'Перейти на встроенный плагин',
feishuMentionChoice: 'Перед переключением явно выберите, требуется ли упоминание.',
feishuSwitchConfirm:
'Переключиться на встроенный Feishu и перезапустить? Все каналы будут кратковременно прерваны. Инструменты старого плагина станут недоступны; потоковые ответы и темы будут работать по выбранным настройкам, некоторые чаты начнут новые сеансы. Учётные данные, привязки и сопряжения сохраняются.',
deepSeekAccessHostHelper: deepSeekAccessHostHelper:
'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.', 'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.',
deepSeekAuthHelper: deepSeekAuthHelper:
@@ -2086,6 +2105,14 @@ const message = {
missing: 'Отсутствует', missing: 'Отсутствует',
}, },
container: { container: {
imageCheckUpdates: 'Проверить обновления образов',
imageCheckUpdateNow: 'Проверить сейчас',
imageCheckUpdateSchedule: 'Проверка по расписанию',
imageCheckUpdateDisable: 'Отключить проверку по расписанию',
imageCheckUpdateStarted: 'Проверка обновлений образов всех контейнеров на текущем узле',
imageCheckUpdateResult:
'Проверка завершена: проверено {checked}, доступны обновления {updates}, ошибок {failed}, пропущено {skipped}',
createByCommand: 'Создать с помощью команды', createByCommand: 'Создать с помощью команды',
importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz', importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz',
importComposeBackupTip: 'Импорт файла резервной копии compose, поддерживается только .tar.gz', importComposeBackupTip: 'Импорт файла резервной копии compose, поддерживается только .tar.gz',
@@ -7082,6 +7109,13 @@ const message = {
'Выбранные узлы автоматически начнут обновляться после завершения обновления основного узла, ручное вмешательство не требуется.', 'Выбранные узлы автоматически начнут обновляться после завершения обновления основного узла, ручное вмешательство не требуется.',
nodeUpgradePackageManage: 'Управление пакетами установки', nodeUpgradePackageManage: 'Управление пакетами установки',
nodeUpgradePackageClean: 'Очистить', nodeUpgradePackageClean: 'Очистить',
packageSource: 'Источник пакета',
packageFromMaster: 'Главный узел',
packageFromNode: 'Готовый пакет',
packagePath: 'Путь к пакету на дочернем узле',
packagePathHelper:
'Укажите абсолютный путь к пакету .tar.gz на дочернем узле. Версия должна совпадать с главным узлом, а архитектура — с дочерним. Исходный пакет сохраняется; недостающие вспомогательные файлы передаются с главного узла.',
packagePathInvalid: 'Укажите абсолютный путь к файлу пакета на дочернем узле',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'Поддерживаются только установочные пакеты `1panel-agent_v*_*` или `1panel-v*-linux-*`', 'Поддерживаются только установочные пакеты `1panel-agent_v*_*` или `1panel-v*-linux-*`',
nodeUpgradePackageHostConfirm: 'Будет выбран файл сервера {0}. Продолжить?', nodeUpgradePackageHostConfirm: 'Будет выбран файл сервера {0}. Продолжить?',
+35 -1
View File
@@ -295,7 +295,7 @@ const message = {
rePassword: 'Şifre onayı, şifre ile tutarsız.', rePassword: 'Şifre onayı, şifre ile tutarsız.',
requiredInput: 'Bu alan gereklidir.', requiredInput: 'Bu alan gereklidir.',
requiredSelect: 'Listeden bir öğe seçin', requiredSelect: 'Listeden bir öğe seçin',
illegalChar: '& ; $ ` ( ) " > < | karakterlerinin enjekte edilmesi şu anda desteklenmiyor', illegalChar: "& ; $ ' ` ( ) \" > < {'|'} karakterlerinin enjekte edilmesi şu anda desteklenmiyor",
illegalInput: 'Bu alan yasadışı karakterler içermemelidir.', illegalInput: 'Bu alan yasadışı karakterler içermemelidir.',
duplicate: 'Bu değer benzersiz olmalıdır.', duplicate: 'Bu değer benzersiz olmalıdır.',
commonName: commonName:
@@ -778,6 +778,25 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { 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: 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.', '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: deepSeekAuthHelper:
@@ -2091,6 +2110,14 @@ const message = {
missing: 'Mevcut değil', missing: 'Mevcut değil',
}, },
container: { container: {
imageCheckUpdates: 'İmaj güncellemelerini denetle',
imageCheckUpdateNow: 'Şimdi denetle',
imageCheckUpdateSchedule: 'Zamanlanmış denetim',
imageCheckUpdateDisable: 'Zamanlanmış denetimi kapat',
imageCheckUpdateStarted: 'Bu düğümdeki tüm konteynerlerin imaj güncellemeleri denetleniyor',
imageCheckUpdateResult:
'Denetim tamamlandı: denetlenen {checked}, güncellenebilir {updates}, başarısız {failed}, atlanan {skipped}',
createByCommand: 'Komutla oluştur', createByCommand: 'Komutla oluştur',
importContainerBackupTip: 'Konteyner yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir', importContainerBackupTip: 'Konteyner yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir',
importComposeBackupTip: 'Compose yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir', importComposeBackupTip: 'Compose yedek dosyasını içe aktar, yalnızca .tar.gz desteklenir',
@@ -7089,6 +7116,13 @@ const message = {
'Seçilen düğümler, ana düğüm yükseltmesi tamamlandıktan sonra otomatik olarak yükseltmeye başlayacaktır, manuel işlem gerekmez.', 'Seçilen düğümler, ana düğüm yükseltmesi tamamlandıktan sonra otomatik olarak yükseltmeye başlayacaktır, manuel işlem gerekmez.',
nodeUpgradePackageManage: 'Kurulum Paketi Yönetimi', nodeUpgradePackageManage: 'Kurulum Paketi Yönetimi',
nodeUpgradePackageClean: 'Temizle', nodeUpgradePackageClean: 'Temizle',
packageSource: 'Paket kaynağı',
packageFromMaster: 'Ana düğüm',
packageFromNode: 'Mevcut paket',
packagePath: 'Alt düğümdeki paket yolu',
packagePathHelper:
'Alt düğümdeki .tar.gz paketinin mutlak yolunu girin. Sürüm ana düğümle, mimari ise alt düğümle eşleşmelidir. Orijinal paket korunur; eksik yardımcı dosyalar ana düğümden aktarılır.',
packagePathInvalid: 'Alt düğümdeki paket dosyasının mutlak yolunu girin',
nodeUpgradePackageFormatHelper: nodeUpgradePackageFormatHelper:
'Yalnızca `1panel-agent_v*_*` veya `1panel-v*-linux-*` kurulum paketleri desteklenir', 'Yalnızca `1panel-agent_v*_*` veya `1panel-v*-linux-*` kurulum paketleri desteklenir',
nodeUpgradePackageHostConfirm: 'Sunucu dosyası {0} seçilecek. Devam edilsin mi?', nodeUpgradePackageHostConfirm: 'Sunucu dosyası {0} seçilecek. Devam edilsin mi?',
+32 -1
View File
@@ -284,7 +284,7 @@ const message = {
rePassword: '密碼不一致,請檢查後重新輸入', rePassword: '密碼不一致,請檢查後重新輸入',
requiredInput: '請填寫必填項', requiredInput: '請填寫必填項',
requiredSelect: '請選擇必選項', requiredSelect: '請選擇必選項',
illegalChar: '暫不支援注入字元 & ; $ \' ` ( ) " > < |', illegalChar: "暫不支援注入字元 & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: '輸入框中存在不合法字元', illegalInput: '輸入框中存在不合法字元',
commonName: '支援非特殊字元開頭,英文、中文、數字、.-和_,長度1-128', commonName: '支援非特殊字元開頭,英文、中文、數字、.-和_,長度1-128',
userName: '支援非特殊字元開頭、英文、中文、數字和_,長度3-30', userName: '支援非特殊字元開頭、英文、中文、數字和_,長度3-30',
@@ -740,6 +740,23 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
feishuUpgradeNotice:
'升級前需要處理舊飛書外掛。此次升級必須完整備份;失敗時還原原映像及配套資料。未設定飛書時僅停用舊外掛。',
feishuRecoveryNotice: '切換失敗時將嘗試恢復閘道,但飛書保持停用,不會重新啟用舊外掛。原設定備份會保留。',
feishuRecover: '停用舊外掛並恢復啟動',
feishuRecoverConfirm:
'將備份設定、停用未設定的舊飛書外掛並重新啟動執行個體,短暫中斷所有頻道。外掛檔案將保留。是否繼續?',
feishuBuiltin: '內建飛書外掛',
feishuImageRequired: '請使用包含內建飛書外掛的映像。',
feishuLegacy: '舊外掛已停止維護。切換前請確認下方的工作階段隔離、串流回覆和 @ 設定。',
feishuBuiltinHint: '外掛隨 OpenClaw 映像一起更新。',
feishuSessionTopic: '依群組話題隔離',
feishuSessionTopicSender: '依群組話題和傳送者隔離',
feishuSwitch: '切換至內建外掛',
feishuMentionChoice: '切換前請明確選擇是否需要 @。',
feishuSwitchConfirm:
'將切換至內建飛書並重新啟動執行個體,短暫中斷所有頻道。舊外掛專有工具不再可用;串流與話題行為依所選設定執行,部分聊天可能產生新工作階段。憑證、繫結和配對儲存會保留。是否繼續?',
deepSeekAccessHostHelper: deepSeekAccessHostHelper:
'填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。', '填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。',
deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。', deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。',
@@ -1962,6 +1979,13 @@ const message = {
missing: '不存在', missing: '不存在',
}, },
container: { container: {
imageCheckUpdates: '檢查更新映像',
imageCheckUpdateNow: '立即檢查',
imageCheckUpdateSchedule: '定時檢查',
imageCheckUpdateDisable: '關閉定時檢查',
imageCheckUpdateStarted: '已開始檢查目前節點全部容器的映像更新',
imageCheckUpdateResult: '檢查完成:成功 {checked},可升級 {updates},失敗 {failed},略過 {skipped}',
createByCommand: '指令建立', createByCommand: '指令建立',
importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式', importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式',
importComposeBackupTip: '匯入編排備份檔案,僅支援 .tar.gz 格式', importComposeBackupTip: '匯入編排備份檔案,僅支援 .tar.gz 格式',
@@ -6470,6 +6494,13 @@ const message = {
nodeUpgradeHelper: '選中的節點將在主節點升級完成後自動開始升級,無需手動操作。', nodeUpgradeHelper: '選中的節點將在主節點升級完成後自動開始升級,無需手動操作。',
nodeUpgradePackageManage: '安裝包管理', nodeUpgradePackageManage: '安裝包管理',
nodeUpgradePackageClean: '清理', nodeUpgradePackageClean: '清理',
packageSource: '安裝包來源',
packageFromMaster: '主節點分發',
packageFromNode: '使用現有包',
packagePath: '子節點安裝包路徑',
packagePathHelper:
'填寫子節點上 .tar.gz 安裝包的絕對路徑。版本須與主節點一致,架構須符合子節點。保留原包,缺少的輔助檔案由主節點補傳。',
packagePathInvalid: '請輸入子節點上安裝包的絕對檔案路徑',
nodeUpgradePackageFormatHelper: '僅支援 `1panel-agent_v*_*` 或 `1panel-v*-linux-*` 安裝包', nodeUpgradePackageFormatHelper: '僅支援 `1panel-agent_v*_*` 或 `1panel-v*-linux-*` 安裝包',
nodeUpgradePackageHostConfirm: '即將選擇伺服器檔案:{0},是否繼續?', nodeUpgradePackageHostConfirm: '即將選擇伺服器檔案:{0},是否繼續?',
nodeUpgradePackageCleanHelper: '即將清理所有低版本升級包,是否繼續?', nodeUpgradePackageCleanHelper: '即將清理所有低版本升級包,是否繼續?',
+32 -1
View File
@@ -284,7 +284,7 @@ const message = {
rePassword: '密码不一致,请检查后重新输入', rePassword: '密码不一致,请检查后重新输入',
requiredInput: '请填写必填项', requiredInput: '请填写必填项',
requiredSelect: '请选择必选项', requiredSelect: '请选择必选项',
illegalChar: '暂不支持注入字符 & ; $ \' ` ( ) " > < |', illegalChar: "暂不支持注入字符 & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: '输入框中存在不合法字符', illegalInput: '输入框中存在不合法字符',
duplicate: '内容不能重复', duplicate: '内容不能重复',
commonName: '支持非特殊字符开头,英文、中文、数字、.-和_,长度1-128', commonName: '支持非特殊字符开头,英文、中文、数字、.-和_,长度1-128',
@@ -757,6 +757,23 @@ const message = {
}, },
aiTools: { aiTools: {
agents: { agents: {
feishuUpgradeNotice:
'升级前需要处理旧飞书插件。此次升级必须完整备份;失败时恢复原镜像及配套数据。未配置飞书时仅禁用旧插件。',
feishuRecoveryNotice: '切换失败时将尝试恢复网关,但飞书保持停用,不会重新启用旧插件。原配置备份会保留。',
feishuRecover: '禁用旧插件并恢复启动',
feishuRecoverConfirm:
'将备份配置、禁用未配置的旧飞书插件并重启当前实例,短暂中断所有渠道。插件文件将保留。是否继续?',
feishuBuiltin: '内置飞书插件',
feishuImageRequired: '请使用包含内置飞书插件的镜像。',
feishuLegacy: '旧插件已停止维护。切换前请确认下方的会话隔离、流式回复和 @ 设置。',
feishuBuiltinHint: '插件随 OpenClaw 镜像一起更新。',
feishuSessionTopic: '按群话题隔离',
feishuSessionTopicSender: '按群话题和发送者隔离',
feishuSwitch: '切换到内置插件',
feishuMentionChoice: '切换前请明确选择是否需要 @。',
feishuSwitchConfirm:
'将切换到内置飞书并重启实例,短暂中断所有渠道。旧插件独有工具不再可用;流式回复和话题行为按所选设置执行,部分聊天可能产生新会话。凭据、绑定和配对存储会保留。是否继续?',
deepSeekAccessHostHelper: deepSeekAccessHostHelper:
'填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。', '填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。',
deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。', deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。',
@@ -1971,6 +1988,13 @@ const message = {
missing: '不存在', missing: '不存在',
}, },
container: { container: {
imageCheckUpdates: '检查更新镜像',
imageCheckUpdateNow: '立即检查',
imageCheckUpdateSchedule: '定时检查',
imageCheckUpdateDisable: '关闭定时检查',
imageCheckUpdateStarted: '已开始检查当前节点全部容器的镜像更新',
imageCheckUpdateResult: '检查完成:成功 {checked},可升级 {updates},失败 {failed},跳过 {skipped}',
createByCommand: '命令创建', createByCommand: '命令创建',
importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式', importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式',
importComposeBackupTip: '导入编排备份文件,仅支持 .tar.gz 格式', importComposeBackupTip: '导入编排备份文件,仅支持 .tar.gz 格式',
@@ -5428,6 +5452,13 @@ const message = {
nodeUpgradeHelper: '选中的节点将在主节点升级完成后自动开始升级,无需手动操作。', nodeUpgradeHelper: '选中的节点将在主节点升级完成后自动开始升级,无需手动操作。',
nodeUpgradePackageManage: '安装包管理', nodeUpgradePackageManage: '安装包管理',
nodeUpgradePackageClean: '清理', nodeUpgradePackageClean: '清理',
packageSource: '安装包来源',
packageFromMaster: '主节点分发',
packageFromNode: '使用已有包',
packagePath: '子节点安装包路径',
packagePathHelper:
'填写子节点上 .tar.gz 安装包的绝对路径。版本须与主节点一致,架构须匹配子节点。原包保留,缺少的辅助文件由主节点补传。',
packagePathInvalid: '请输入子节点上安装包的绝对文件路径',
nodeUpgradePackageFormatHelper: '仅支持 `1panel-agent_v*_*` 或 `1panel-v*-linux-*` 安装包', nodeUpgradePackageFormatHelper: '仅支持 `1panel-agent_v*_*` 或 `1panel-v*-linux-*` 安装包',
nodeUpgradePackageHostConfirm: '即将选择服务器文件:{0},是否继续?', nodeUpgradePackageHostConfirm: '即将选择服务器文件:{0},是否继续?',
nodeUpgradePackageCleanHelper: '即将清理所有低版本升级包,是否继续?', nodeUpgradePackageCleanHelper: '即将清理所有低版本升级包,是否继续?',
@@ -205,7 +205,12 @@
</template> </template>
</DialogPro> </DialogPro>
<DialogPro v-model="passkeyPrereqDialogOpen" :title="$t('setting.passkey')" @close="handlePasskeyPrereqDialogClose"> <DialogPro
v-model="passkeyPrereqDialogOpen"
:title="$t('setting.passkey')"
size="small"
@close="handlePasskeyPrereqDialogClose"
>
<div class="passkey-prereq-dialog"> <div class="passkey-prereq-dialog">
<div class="passkey-prereq-title">{{ $t('setting.passkeyPrereqTitle') }}</div> <div class="passkey-prereq-title">{{ $t('setting.passkeyPrereqTitle') }}</div>
<div class="passkey-prereq-list"> <div class="passkey-prereq-list">
-8
View File
@@ -413,11 +413,3 @@ html {
} }
} }
} }
// Standard dialogs share one width; explicitly sized dialogs keep their layout.
.el-dialog:not(.is-fullscreen):not(.dialog-full-width):not(.dialog-sized),
.el-message-box {
width: 60vw !important;
min-width: 0;
max-width: 60vw;
}
+6
View File
@@ -130,6 +130,12 @@ html.dark {
--el-text-color-placeholder: var(--panel-main-bg-color-4); --el-text-color-placeholder: var(--panel-main-bg-color-4);
.terminal-dock-handle {
box-shadow:
inset 0 0 0 1px var(--el-border-color-darker),
var(--el-box-shadow-light);
}
.el-radio-button { .el-radio-button {
--el-radio-button-checked-text-color: var(--panel-main-bg-color-10); --el-radio-button-checked-text-color: var(--panel-main-bg-color-10);
} }
+2 -8
View File
@@ -84,12 +84,9 @@
} }
@media only screen and (max-width: 1024px) { @media only screen and (max-width: 1024px) {
.el-dialog.dialog-sized:not(.is-fullscreen) { .el-dialog:not(.is-fullscreen) {
min-width: min(420px, calc(100vw - 24px)); min-width: min(420px, calc(100vw - 24px));
max-width: calc(100vw - 24px); max-width: calc(100vw - 24px);
}
.el-dialog:not(.is-fullscreen) {
max-height: calc(100vh - 24px); max-height: calc(100vh - 24px);
max-height: calc(100dvh - 24px); max-height: calc(100dvh - 24px);
display: flex; display: flex;
@@ -136,13 +133,10 @@
} }
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
.el-dialog.dialog-sized:not(.is-fullscreen) { .el-dialog:not(.is-fullscreen) {
width: calc(100vw - 24px) !important; width: calc(100vw - 24px) !important;
min-width: 0; min-width: 0;
max-width: calc(100vw - 24px); max-width: calc(100vw - 24px);
}
.el-dialog:not(.is-fullscreen) {
margin: 12px auto; margin: 12px auto;
} }

Some files were not shown because too many files have changed in this diff Show More