Compare commits

...
106 changed files with 4162 additions and 216 deletions
+20
View File
@@ -826,6 +826,26 @@ func (b *BaseApi) UpdateAgentFeishuConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Switch to the bundled OpenClaw Feishu plugin
// @Accept json
// @Param request body dto.AgentFeishuSwitchReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/feishu/switch [post]
func (b *BaseApi) SwitchAgentFeishuPlugin(c *gin.Context) {
var req dto.AgentFeishuSwitchReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.SwitchFeishuPlugin(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent Telegram channel config
// @Accept json
@@ -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"`
}
type AgentFeishuTransition struct {
Confirmed bool `json:"confirmed"`
Streaming bool `json:"streaming"`
GroupSessionScope string `json:"groupSessionScope"`
RequireMention string `json:"requireMention"`
}
type AgentFeishuSwitchReq struct {
AgentID uint `json:"agentId" validate:"required"`
TaskID string `json:"taskID" validate:"required"`
Streaming bool `json:"streaming"`
GroupSessionScope string `json:"groupSessionScope" validate:"required,oneof=group group_sender group_topic group_topic_sender"`
RequireMention string `json:"requireMention" validate:"required,oneof=true false"`
}
type AgentFeishuConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"`
ReplyMode string `json:"replyMode" validate:"required"`
Streaming bool `json:"streaming"`
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
GroupAllowFrom []string `json:"groupAllowFrom"`
Domain string `json:"domain"`
ConnectionMode string `json:"connectionMode"`
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
GroupSessionScope string `json:"groupSessionScope" validate:"omitempty,oneof=group group_sender group_topic group_topic_sender"`
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"`
ReplyMode string `json:"replyMode" validate:"required"`
Streaming bool `json:"streaming"`
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
GroupAllowFrom []string `json:"groupAllowFrom"`
Domain string `json:"domain"`
ConnectionMode string `json:"connectionMode"`
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
}
type AgentFeishuConfig struct {
Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"`
ReplyMode string `json:"replyMode"`
Streaming bool `json:"streaming"`
RequireMention string `json:"requireMention"`
GroupPolicy string `json:"groupPolicy"`
GroupAllowFrom []string `json:"groupAllowFrom"`
Domain string `json:"domain"`
ConnectionMode string `json:"connectionMode"`
Bots []AgentFeishuBot `json:"bots"`
Installed bool `json:"installed"`
BuiltinManaged bool `json:"builtinManaged"`
LegacyConfigured bool `json:"legacyConfigured"`
GroupSessionScope string `json:"groupSessionScope"`
BuiltinAvailable bool `json:"builtinAvailable"`
Legacy bool `json:"legacy"`
Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"`
ReplyMode string `json:"replyMode"`
Streaming bool `json:"streaming"`
RequireMention string `json:"requireMention"`
GroupPolicy string `json:"groupPolicy"`
GroupAllowFrom []string `json:"groupAllowFrom"`
Domain string `json:"domain"`
ConnectionMode string `json:"connectionMode"`
Bots []AgentFeishuBot `json:"bots"`
Installed bool `json:"installed"`
}
type AgentTelegramConfigReq struct {
+5 -3
View File
@@ -43,9 +43,11 @@ type AppOssConfig struct {
}
type AppVersion struct {
Version string `json:"version"`
DetailId uint `json:"detailId"`
DockerCompose string `json:"dockerCompose"`
FeishuLegacy bool `json:"feishuLegacy,omitempty"`
FeishuConfigured bool `json:"feishuConfigured,omitempty"`
Version string `json:"version"`
DetailId uint `json:"detailId"`
DockerCompose string `json:"dockerCompose"`
}
type AppList struct {
+10 -7
View File
@@ -21,13 +21,16 @@ type InspectReq struct {
}
type ContainerInfo struct {
ContainerID string `json:"containerID"`
Name string `json:"name"`
ImageId string `json:"imageID"`
ImageName string `json:"imageName"`
CreateTime string `json:"createTime"`
State string `json:"state"`
RunTime string `json:"runTime"`
LatestVersion string `json:"latestVersion"`
TargetImage string `json:"targetImage"`
HasUpdate bool `json:"hasUpdate"`
ContainerID string `json:"containerID"`
Name string `json:"name"`
ImageId string `json:"imageID"`
ImageName string `json:"imageName"`
CreateTime string `json:"createTime"`
State string `json:"state"`
RunTime string `json:"runTime"`
Network []string `json:"network"`
Ports []string `json:"ports"`
+22 -20
View File
@@ -89,31 +89,33 @@ type AppBackupDelete struct {
}
type AppInstalledOperate struct {
InstallId uint `json:"installId" validate:"required"`
BackupId uint `json:"backupId"`
DetailId uint `json:"detailId"`
Operate constant.AppOperate `json:"operate" validate:"required"`
ForceDelete bool `json:"forceDelete"`
DeleteBackup bool `json:"deleteBackup"`
DeleteDB bool `json:"deleteDB"`
Backup bool `json:"backup"`
PullImage bool `json:"pullImage"`
DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"`
DeleteImage bool `json:"deleteImage"`
Favorite bool `json:"favorite"`
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
InstallId uint `json:"installId" validate:"required"`
BackupId uint `json:"backupId"`
DetailId uint `json:"detailId"`
Operate constant.AppOperate `json:"operate" validate:"required"`
ForceDelete bool `json:"forceDelete"`
DeleteBackup bool `json:"deleteBackup"`
DeleteDB bool `json:"deleteDB"`
Backup bool `json:"backup"`
PullImage bool `json:"pullImage"`
DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"`
DeleteImage bool `json:"deleteImage"`
Favorite bool `json:"favorite"`
UseLifecycleScripts bool `json:"-"`
}
type AppInstallUpgrade struct {
InstallID uint `json:"installId"`
DetailID uint `json:"detailId"`
Backup bool `json:"backup"`
PullImage bool `json:"pullImage"`
DeleteImage bool `json:"deleteImage"`
DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"`
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
InstallID uint `json:"installId"`
DetailID uint `json:"detailId"`
Backup bool `json:"backup"`
PullImage bool `json:"pullImage"`
DeleteImage bool `json:"deleteImage"`
DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"`
}
type AppInstallDelete struct {
@@ -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)
UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error
SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error
GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error)
UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error)
@@ -1591,6 +1592,10 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
if err != nil {
return err
}
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
return buserr.New("TaskIsExecuting")
}
defer feishuSwitches.Delete(agent.ConfigPath)
if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
return err
}
+38 -3
View File
@@ -45,9 +45,29 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF
if err != nil {
return nil, err
}
if agent.AgentType != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(install.Version) {
result := extractFeishuConfig(conf)
result.Installed, _ = checkPluginInstalled(install.GetPath(), "feishu")
return &result, nil
}
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
if err != nil {
return nil, err
}
result := extractFeishuConfig(conf)
installed, _ := checkPluginInstalled(install.GetPath(), "feishu")
result.Installed = installed
if !legacy {
result = extractBundledFeishuConfig(conf)
}
result.BuiltinManaged = true
result.BuiltinAvailable = hasBundledFeishu(install.ContainerName)
result.Installed = result.BuiltinAvailable
result.Legacy, result.LegacyConfigured = legacy, configured
if legacy {
result.GroupSessionScope = "group"
if result.ThreadSession {
result.GroupSessionScope = "group_topic"
}
}
return &result, nil
}
@@ -72,6 +92,9 @@ func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) err
})
})
}
if agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
return saveBundledFeishuConfig(agent, install, req)
}
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
config := dto.AgentFeishuConfig{
Enabled: req.Enabled,
@@ -358,6 +381,9 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
if err != nil {
return err
}
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
return buserr.New("ErrFeishuManaged")
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
@@ -403,6 +429,9 @@ func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error {
if err != nil {
return err
}
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
return buserr.New("ErrFeishuManaged")
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
@@ -443,6 +472,9 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
if err != nil {
return err
}
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
return buserr.New("ErrFeishuManaged")
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
@@ -538,10 +570,13 @@ func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
}
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
_, install, err := a.loadAgentAndInstall(req.AgentID)
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
return &dto.AgentPluginStatus{Installed: hasBundledFeishu(install.ContainerName)}, nil
}
installed, err := checkPluginInstalled(install.GetPath(), req.Type)
if err != nil {
return nil, err
+677
View File
@@ -0,0 +1,677 @@
package service
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
)
// Keep panel configuration writes out of the backup/activation/recovery window.
var feishuSwitches sync.Map
func isOpenclawBundledFeishuVersion(version string) bool {
return common.CompareAppVersion(version, "2026.10.7")
}
func hasBundledFeishu(container string) bool {
image, err := feishuContainerImage(container)
if err != nil {
return false
}
_, err = bundledFeishuImage(image)
return err == nil
}
func usesBundledFeishu(conf map[string]interface{}) bool {
f := getChannelConfig(conf, "feishu")
for _, key := range []string{"threadSession", "replyMode"} {
if _, old := f[key]; old {
return false
}
}
if _, old := f["streaming"].(bool); old {
return false
}
entries := childMap(childMap(conf, "plugins"), "entries")
if extractBoolValue(childMap(entries, "openclaw-lark")["enabled"], false) {
return false
}
_, streaming := f["streaming"].(map[string]interface{})
_, selected := entries["feishu"]
return selected && (extractBoolValue(childMap(entries, "feishu")["enabled"], false) || f["groupSessionScope"] != nil || streaming)
}
func hasLegacyFeishu(conf map[string]interface{}, installPath string) bool {
legacy, _, err := classifyLegacyFeishu(conf, installPath)
return err != nil || legacy
}
func selectBundledFeishu(conf map[string]interface{}, enabled bool) {
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
if legacy, ok := entries["openclaw-lark"]; ok {
if entry, ok := legacy.(map[string]interface{}); ok {
entry["enabled"] = false
}
}
ensureChildMap(entries, "feishu")["enabled"] = enabled
// Preserve an absent allowlist; creating one would disable unrelated plugins.
if allow, ok := plugins["allow"]; ok && len(extractStringList(allow)) > 0 {
ids := slices.DeleteFunc(extractStringList(allow), func(id string) bool { return id == "openclaw-lark" })
if !slices.Contains(ids, "feishu") {
ids = append(ids, "feishu")
}
plugins["allow"] = ids
}
if deny, ok := plugins["deny"]; ok {
plugins["deny"] = slices.DeleteFunc(extractStringList(deny), func(id string) bool { return id == "feishu" })
}
}
func feishuConfigError(field string) error { return buserr.WithDetail("ErrFeishuConfig", field, nil) }
func convertLegacyFeishu(conf map[string]interface{}, req dto.AgentFeishuSwitchReq) error {
f := getChannelConfig(conf, "feishu")
if len(f) == 0 {
if raw, exists := childMap(conf, "channels")["feishu"]; exists && raw != nil {
if _, ok := raw.(map[string]interface{}); !ok {
return feishuConfigError("channels.feishu")
}
}
f = ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
f["enabled"], f["dmPolicy"], f["groupPolicy"] = false, "pairing", "allowlist"
}
if err := checkLegacyFeishuFields(f, "channels.feishu", false); err != nil {
return err
}
if err := convertFeishuAccess(f, "channels.feishu"); err != nil {
return err
}
delete(f, "threadSession")
delete(f, "replyMode")
f["streaming"] = map[string]interface{}{"mode": "off"}
if req.Streaming {
f["streaming"] = map[string]interface{}{"mode": "partial"}
}
f["groupSessionScope"] = req.GroupSessionScope
f["requireMention"] = req.RequireMention == "true"
delete(f, "topicSessionMode")
// Preserve legacy defaults explicitly; bundled defaults are more restrictive.
if _, ok := f["groupPolicy"]; !ok {
f["groupPolicy"] = "open"
}
entries := ensureChildMap(ensureChildMap(conf, "plugins"), "entries")
ensureChildMap(entries, "openclaw-lark")["enabled"] = false
selectBundledFeishu(conf, extractBoolValue(f["enabled"], true))
return nil
}
// Limit automatic conversion to the shared channel fields; custom plugin options require review.
func checkLegacyFeishuFields(f map[string]interface{}, field string, group bool) error {
allowed := strings.Fields("enabled name appId appSecret encryptKey verificationToken domain connectionMode webhookPath defaultAccount dmPolicy allowFrom groupPolicy groupAllowFrom groupSenderAllowFrom requireMention accounts groups streaming threadSession replyMode groupSessionScope topicSessionMode replyInThread")
if group {
allowed = strings.Fields("enabled requireMention allowFrom groupSessionScope topicSessionMode replyInThread")
}
for _, key := range sortedChildKeys(f) {
if !slices.Contains(allowed, key) {
return feishuConfigError(field + "." + key)
}
}
if enabled, exists := f["enabled"]; exists {
if _, ok := enabled.(bool); !ok {
return feishuConfigError(field + ".enabled")
}
}
for _, key := range []string{"dmPolicy", "groupPolicy"} {
if raw, exists := f[key]; exists {
value, ok := raw.(string)
options := []string{"open", "allowlist", "disabled"}
if key == "dmPolicy" {
options = append(options, "pairing")
}
if !ok || !slices.Contains(options, value) {
return feishuConfigError(field + "." + key)
}
}
}
for _, key := range []string{"allowFrom", "groupSenderAllowFrom"} {
if raw, exists := f[key]; exists {
values, ok := raw.([]interface{})
if !ok {
return feishuConfigError(field + "." + key)
}
for _, value := range values {
if _, ok := value.(string); !ok {
return feishuConfigError(field + "." + key)
}
}
}
}
for _, section := range []string{"accounts", "groups"} {
raw, exists := f[section]
if !exists {
continue
}
entries, ok := raw.(map[string]interface{})
if !ok {
return feishuConfigError(field + "." + section)
}
for _, id := range sortedChildKeys(entries) {
entry, ok := entries[id].(map[string]interface{})
name := field + "." + section + "." + id
if !ok {
return feishuConfigError(name)
}
if err := checkLegacyFeishuFields(entry, name, section == "groups"); err != nil {
return err
}
}
}
return nil
}
func convertFeishuAccess(f map[string]interface{}, field string) error {
if raw, ok := f["groupAllowFrom"]; ok {
values, ok := raw.([]interface{})
if !ok {
return feishuConfigError(field + ".groupAllowFrom")
}
groups, senders := []string{}, []string{}
for _, value := range values {
id, ok := value.(string)
if !ok {
return feishuConfigError(field + ".groupAllowFrom")
}
switch {
case strings.HasPrefix(id, "oc_"):
groups = append(groups, id)
case strings.HasPrefix(id, "ou_"):
senders = append(senders, id)
default:
return feishuConfigError(field + ".groupAllowFrom")
}
}
// A sender-only legacy allowlist has different group admission semantics.
if len(senders) > 0 && (len(groups) == 0 || f["groupSenderAllowFrom"] != nil) {
return feishuConfigError(field + ".groupAllowFrom")
}
f["groupAllowFrom"] = groups
if len(senders) > 0 {
f["groupSenderAllowFrom"] = senders
}
}
for _, section := range []string{"accounts", "groups"} {
for id, value := range childMap(f, section) {
entry, ok := value.(map[string]interface{})
name := field + "." + section + "." + id
if !ok {
return feishuConfigError(name)
}
// Account/group overrides must be resolved explicitly, not flattened.
for _, key := range []string{"threadSession", "replyMode"} {
if _, ok := entry[key]; ok {
return feishuConfigError(name + "." + key)
}
}
if mention, ok := entry["requireMention"]; ok {
if _, ok := mention.(bool); !ok {
return feishuConfigError(name + ".requireMention")
}
}
if raw, exists := entry["streaming"]; exists {
streaming, ok := raw.(bool)
if !ok || streaming {
return feishuConfigError(name + ".streaming")
}
entry["streaming"] = map[string]interface{}{"mode": "off"}
}
if err := convertFeishuAccess(entry, name); err != nil {
return err
}
}
}
return nil
}
func extractBundledFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
result := extractFeishuConfig(conf)
f := getChannelConfig(conf, "feishu")
result.Enabled = extractBoolValue(f["enabled"], true) && extractBoolValue(childMap(childMap(childMap(conf, "plugins"), "entries"), "feishu")["enabled"], true)
result.GroupSessionScope = extractStringValue(f["groupSessionScope"])
if result.GroupSessionScope == "" {
result.GroupSessionScope = "group"
}
result.Streaming = extractStringValue(childMap(f, "streaming")["mode"]) != "off"
result.GroupPolicy = extractStringValue(f["groupPolicy"])
if result.GroupPolicy == "" {
result.GroupPolicy = "allowlist"
}
if len(f) == 0 {
result.Streaming = false
result.RequireMention = "true"
} else if _, ok := f["requireMention"]; !ok {
result.RequireMention = "open"
}
accounts := childMap(f, "accounts")
ids := sortedChildKeys(accounts)
if (f["appId"] != nil || f["appSecret"] != nil) && !slices.Contains(ids, "default") {
ids = append([]string{"default"}, ids...)
}
result.Bots = []dto.AgentFeishuBot{}
for _, id := range ids {
effective := make(map[string]interface{}, len(f))
for k, v := range f {
effective[k] = v
}
for k, v := range childMap(accounts, id) {
effective[k] = v
}
bot := defaultFeishuBot()
bot.AccountID, bot.IsDefault = id, id == "default"
bot.Name = extractDisplayName(childMap(accounts, id), "", id)
bot.Enabled = extractBoolValue(childMap(accounts, id)["enabled"], true)
bot.AppID, bot.AppSecret = extractStringValue(effective["appId"]), extractStringValue(effective["appSecret"])
bot.DmPolicy = extractStringValue(effective["dmPolicy"])
if bot.DmPolicy == "" {
bot.DmPolicy = "pairing"
}
bot.AllowFrom = extractStringList(effective["allowFrom"])
result.Bots = append(result.Bots, bot)
}
return result
}
func setBundledFeishuConfig(conf map[string]interface{}, req dto.AgentFeishuConfigUpdateReq) error {
if !slices.Contains([]string{"group", "group_sender", "group_topic", "group_topic_sender"}, req.GroupSessionScope) {
return feishuConfigError("groupSessionScope")
}
f := ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
accounts := childMap(f, "accounts")
seen := map[string]bool{}
appIDs := map[string]bool{}
for _, bot := range req.Bots {
if bot.AccountID == "" || bot.AccountID != strings.ToLower(strings.TrimSpace(bot.AccountID)) || seen[bot.AccountID] || appIDs[bot.AppID] || bot.AppID == "" || !slices.Contains([]string{"pairing", "open", "allowlist", "disabled"}, bot.DmPolicy) || (bot.DmPolicy == "allowlist" && len(bot.AllowFrom) == 0) {
return feishuConfigError("accounts")
}
seen[bot.AccountID] = true
appIDs[bot.AppID] = true
entry := childMap(accounts, bot.AccountID)
if _, exists := entry["appSecret"]; !exists && bot.AppSecret == "" {
entry["appSecret"] = f["appSecret"]
}
// SecretRef credentials cannot be represented by the form: keep them intact.
if _, secretRef := entry["appSecret"].(map[string]interface{}); !secretRef && bot.AppSecret == "" {
return feishuConfigError("accounts." + bot.AccountID + ".appSecret")
}
entry["appId"], entry["name"], entry["enabled"], entry["dmPolicy"] = bot.AppID, bot.Name, bot.Enabled, bot.DmPolicy
if bot.AppSecret != "" {
entry["appSecret"] = bot.AppSecret
}
switch bot.DmPolicy {
case "open":
entry["allowFrom"] = []string{"*"}
case "allowlist":
entry["allowFrom"] = bot.AllowFrom
default:
if len(bot.AllowFrom) > 0 {
entry["allowFrom"] = bot.AllowFrom
} else {
entry["allowFrom"] = []string{}
}
}
accounts[bot.AccountID] = entry
}
for id := range accounts {
if !seen[id] {
delete(accounts, id)
}
}
if !seen["default"] {
delete(f, "appId")
delete(f, "appSecret")
}
if id := extractStringValue(f["defaultAccount"]); id != "" && !seen[id] {
return feishuConfigError("defaultAccount")
}
if len(accounts) > 0 {
f["accounts"] = accounts
} else {
delete(f, "accounts")
}
f["enabled"], f["groupSessionScope"], f["groupPolicy"] = req.Enabled, req.GroupSessionScope, req.GroupPolicy
if req.RequireMention == "open" {
delete(f, "requireMention")
} else {
f["requireMention"] = req.RequireMention == "true"
}
if req.GroupPolicy == "allowlist" {
f["groupAllowFrom"] = req.GroupAllowFrom
} else {
delete(f, "groupAllowFrom")
}
streaming := ensureChildMap(f, "streaming")
streaming["mode"] = "off"
if req.Streaming {
streaming["mode"] = "partial"
}
selectBundledFeishu(conf, req.Enabled && hasEnabledBots(req.Bots))
return nil
}
// Write beside the original so validation and rename preserve the mounted filesystem and owner.
func stageFeishuConfig(configPath string, payload []byte) (string, error) {
info, err := os.Lstat(configPath)
if err != nil {
return "", err
}
if !info.Mode().IsRegular() {
return "", feishuConfigError("openclaw.json")
}
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-*.json")
if err != nil {
return "", err
}
name := f.Name()
defer func() {
f.Close()
if err != nil {
os.Remove(name)
}
}()
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
err = f.Chown(int(stat.Uid), int(stat.Gid))
}
if err == nil {
err = f.Chmod(info.Mode().Perm())
}
if err == nil {
_, err = f.Write(payload)
}
if err == nil {
err = f.Sync()
}
if err == nil {
err = f.Close()
}
return name, err
}
func validateFeishuConfig(container, configPath string) error {
out, err := cmd.RunDockerExecWithStdout(time.Minute, container, "env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath), "openclaw", "config", "validate", "--json")
return feishuValidationResult(out, err)
}
func replaceFeishuConfig(configPath string, expected, payload []byte) error {
tmp, err := stageFeishuConfig(configPath, payload)
if err != nil {
return err
}
defer os.Remove(tmp)
current, err := os.ReadFile(configPath)
if err != nil {
return err
}
if !bytes.Equal(current, expected) {
return buserr.New("ErrFeishuConfigChanged")
}
return os.Rename(tmp, configPath)
}
func (a AgentService) SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error {
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if !isOpenclawBundledFeishuVersion(install.Version) {
return buserr.New("ErrFeishuImageRequired")
}
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
return buserr.New("TaskIsExecuting")
}
started := false
defer func() {
if !started {
feishuSwitches.Delete(agent.ConfigPath)
}
}()
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
if !hasBundledFeishu(install.ContainerName) {
return buserr.New("ErrFeishuImageRequired")
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
if err != nil {
return err
}
if !legacy {
return buserr.New("ErrFeishuAlreadySwitched")
}
if err := prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
return err
}
switchTask, err := task.NewTaskWithOps("Feishu", task.TaskUpdate, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
switchTask.AddSubTaskWithAlias("AgentFeishuSwitch", func(t *task.Task) error { return a.runFeishuSwitch(agent, install, req, t) }, nil)
started = true
go func() { defer feishuSwitches.Delete(agent.ConfigPath); executeAgentPluginTask(switchTask) }()
return nil
}
func (a AgentService) runFeishuSwitch(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuSwitchReq, t *task.Task) (result error) {
original, conf, err := readFeishuConfigBytes(agent.ConfigPath)
if err != nil {
return err
}
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
if err != nil {
return err
}
if !legacy {
return buserr.New("ErrFeishuAlreadySwitched")
}
image, err := feishuContainerImage(install.ContainerName)
if err != nil {
return err
}
image, err = bundledFeishuImage(image)
if err != nil {
return err
}
if err = prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
return err
}
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
recovery, err := feishuRecoveryPayload(payload, configured)
if err != nil {
return err
}
// Validate both candidates without invoking Doctor or mutating retained state.
for _, candidate := range [][]byte{payload, recovery} {
staged, stageErr := stageFeishuConfig(agent.ConfigPath, candidate)
if stageErr != nil {
return stageErr
}
validateErr := validateFeishuImageConfig(image, staged)
os.Remove(staged)
if validateErr != nil {
return validateErr
}
}
if _, err = compose.Stop(install.GetComposePath()); err != nil {
return err
}
replaced := false
defer func() {
if !replaced {
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
}
return
}
if result == nil {
return
}
if _, stopErr := compose.Stop(install.GetComposePath()); stopErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", stopErr))
return
}
if restoreErr := replaceFeishuConfig(agent.ConfigPath, payload, recovery); restoreErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", restoreErr))
return
}
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
return
}
if healthErr := verifyFeishuActivation(install.ContainerName, false); healthErr != nil {
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", healthErr))
return
}
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
}()
current, err := os.ReadFile(agent.ConfigPath)
if err != nil {
return err
}
if !bytes.Equal(current, original) {
return buserr.New("ErrFeishuConfigChanged")
}
backup, err := backupFeishuConfig(agent.ConfigPath, original)
if err != nil {
return err
}
t.Log(i18n.GetWithName("AgentFeishuBackup", filepath.Base(backup)))
if err = replaceFeishuConfig(agent.ConfigPath, original, payload); err != nil {
return err
}
replaced = true
if _, err = compose.Operate(install.GetComposePath(), "start"); err != nil {
return err
}
enabled := configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
if err = verifyFeishuActivation(install.ContainerName, enabled); err != nil {
return err
}
if enabled {
probeFeishuConnection(install.ContainerName, t)
}
if configured {
t.Log(i18n.GetMsgByKey("AgentFeishuVerifyMessages"))
} else {
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
}
return nil
}
func saveBundledFeishuConfig(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuConfigUpdateReq) error {
if !isOpenclawBundledFeishuVersion(install.Version) {
return buserr.New("ErrFeishuImageRequired")
}
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
return buserr.New("TaskIsExecuting")
}
defer feishuSwitches.Delete(agent.ConfigPath)
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
if !hasBundledFeishu(install.ContainerName) {
return buserr.New("ErrFeishuImageRequired")
}
original, err := os.ReadFile(agent.ConfigPath)
if err != nil {
return err
}
var conf map[string]interface{}
if err = json.Unmarshal(original, &conf); err != nil {
return err
}
if hasLegacyFeishu(conf, install.GetPath()) {
return buserr.New("ErrFeishuSwitchRequired")
}
if err = setBundledFeishuConfig(conf, req); err != nil {
return err
}
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
staged, err := stageFeishuConfig(agent.ConfigPath, payload)
if err != nil {
return err
}
defer os.Remove(staged)
if err = validateFeishuConfig(install.ContainerName, staged); err != nil {
return err
}
return replaceFeishuConfig(agent.ConfigPath, original, payload)
}
func feishuProbeOK(output string) bool {
raw, err := extractEmbeddedJSON(output)
if err != nil {
return false
}
var status struct {
ChannelAccounts map[string][]struct {
Enabled bool `json:"enabled"`
Configured bool `json:"configured"`
Running bool `json:"running"`
Probe struct {
OK bool `json:"ok"`
} `json:"probe"`
} `json:"channelAccounts"`
}
if json.Unmarshal(raw, &status) != nil {
return false
}
accounts, ok := status.ChannelAccounts["feishu"]
if !ok {
return false
}
for _, account := range accounts {
if account.Enabled && (!account.Configured || !account.Running || !account.Probe.OK) {
return false
}
}
return true
}
func feishuGatewayReady(output string, enabled bool) bool {
raw, err := extractEmbeddedJSON(output)
if err != nil {
return false
}
var health struct {
OK bool `json:"ok"`
Plugins struct {
Loaded []string `json:"loaded"`
} `json:"plugins"`
}
return json.Unmarshal(raw, &health) == nil && health.OK && !slices.Contains(health.Plugins.Loaded, "openclaw-lark") && (!enabled || slices.Contains(health.Plugins.Loaded, "feishu"))
}
+201
View File
@@ -0,0 +1,201 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/docker/docker/api/types/container"
)
// Inspect a stopped disposable container; its entrypoint never touches retained state.
func bundledFeishuImage(image string) (string, error) {
cli, err := docker.NewDockerClient()
if err != nil {
return "", err
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
info, _, err := cli.ImageInspectWithRaw(ctx, image)
if err != nil {
return "", buserr.New("ErrFeishuImageRequired")
}
created, err := cli.ContainerCreate(ctx, &container.Config{Image: info.ID, Entrypoint: []string{"/bin/true"}}, &container.HostConfig{NetworkMode: "none"}, nil, nil, "")
if err != nil {
return "", err
}
defer func() {
cleanup, done := context.WithTimeout(context.Background(), 10*time.Second)
defer done()
_ = cli.ContainerRemove(cleanup, created.ID, container.RemoveOptions{Force: true, RemoveVolumes: true})
}()
if _, err = cli.ContainerStatPath(ctx, created.ID, "/app/.1panel-feishu-bundled-v1"); err != nil {
return "", buserr.New("ErrFeishuImageRequired")
}
return info.ID, nil
}
func feishuContainerImage(name string) (string, error) {
cli, err := docker.NewDockerClient()
if err != nil {
return "", err
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
info, err := cli.ContainerInspect(ctx, name)
if err != nil {
return "", err
}
return info.Image, nil
}
// The original state is read-only and the normal Doctor entrypoint is bypassed.
func validateFeishuImageConfig(image, configPath string) error {
info, err := os.Stat(configPath)
if err != nil {
return err
}
uid, gid := uint32(1000), uint32(1000)
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
uid, gid = stat.Uid, stat.Gid
}
user := fmt.Sprintf("%d:%d", uid, gid)
tmpfs := fmt.Sprintf("/tmp:rw,mode=0700,uid=%d,gid=%d", uid, gid)
name := "1panel-feishu-" + strings.TrimSuffix(filepath.Base(configPath), ".json")
mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
defer func() { _ = cmd.NewCommandMgr(cmd.WithTimeout(15*time.Second)).Run("docker", "rm", "-f", "-v", name) }()
out, runErr := mgr.RunWithStdout("docker", "run", "--name", name, "--rm", "--network", "none", "--read-only", "--user", user,
"--tmpfs", tmpfs,
"--mount", "type=bind,src="+filepath.Dir(configPath)+",dst=/home/node/.openclaw,readonly",
"--env", "HOME=/home/node", "--env", "TMPDIR=/tmp", "--env", "OPENCLAW_STATE_DIR=/tmp/state",
"--env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath),
"--entrypoint", "sh", image, "-c", "mkdir -m 700 /tmp/state && ln -s /home/node/.openclaw/extensions /tmp/state/extensions && exec node /app/openclaw.mjs config validate --json")
return feishuValidationResult(out, runErr)
}
func feishuValidationResult(out string, runErr error) error {
raw, _ := extractEmbeddedJSON(out)
var result struct {
Valid bool `json:"valid"`
Issues []struct {
Path string `json:"path"`
} `json:"issues"`
}
if json.Unmarshal(raw, &result) == nil && result.Valid && runErr == nil {
return nil
}
fields := []string{}
for _, issue := range result.Issues {
if issue.Path != "" {
fields = append(fields, issue.Path)
}
}
if len(fields) == 0 {
fields = append(fields, "openclaw.json")
}
return feishuConfigError(strings.Join(fields, ", "))
}
func backupFeishuConfig(configPath string, original []byte) (string, error) {
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-backup-*.json")
if err != nil {
return "", err
}
if _, err = f.Write(original); err == nil {
err = f.Sync()
}
closeErr := f.Close()
if err != nil {
return "", err
}
if closeErr != nil {
return "", closeErr
}
return f.Name(), nil
}
// Doctor can spend 94 seconds waiting for the previous container's owner lease.
func waitOpenclawReady(name string) error {
script := `const fs=require('fs');const c=JSON.parse(fs.readFileSync('/home/node/.openclaw/openclaw.json','utf8'));const tls=c.gateway?.tls?.enabled===true;const h=require(tls?'https':'http');function check(p){const r=h.get({hostname:'127.0.0.1',port:c.gateway?.port||18789,path:p,rejectUnauthorized:false,timeout:3000},s=>{s.resume();if(s.statusCode===404&&p==='/readyz')return check('/healthz');process.exitCode=s.statusCode===200?0:1});r.on('timeout',()=>r.destroy());r.on('error',()=>process.exitCode=1)}check('/readyz')`
deadline := time.Now().Add(4 * time.Minute)
for time.Now().Before(deadline) {
if cmd.RunDockerExec(8*time.Second, name, "node", "-e", script) == nil {
return nil
}
time.Sleep(2 * time.Second)
}
return buserr.New("ErrFeishuActivation")
}
func verifyFeishuActivation(name string, enabled bool) error {
if err := waitOpenclawReady(name); err != nil {
return err
}
output, err := cmd.RunDockerExecWithStdout(time.Minute, name, "openclaw", "plugins", "list", "--json")
if err != nil {
return buserr.New("ErrFeishuActivation")
}
raw, _ := extractEmbeddedJSON(output)
var result struct {
Plugins []struct {
ID string `json:"id"`
Status string `json:"status"`
Origin string `json:"origin"`
} `json:"plugins"`
}
if json.Unmarshal(raw, &result) != nil {
return buserr.New("ErrFeishuActivation")
}
found := false
for _, p := range result.Plugins {
if p.ID == "openclaw-lark" && p.Status != "disabled" {
return buserr.New("ErrFeishuActivation")
}
if p.ID == "feishu" && p.Origin == "bundled" && (p.Status == "loaded" || (!enabled && p.Status == "disabled")) {
found = true
}
}
if !found {
return buserr.New("ErrFeishuActivation")
}
health, err := cmd.RunDockerExecWithStdout(20*time.Second, name, "openclaw", "gateway", "health", "--json")
if err != nil || !feishuGatewayReady(health, enabled) {
return buserr.New("ErrFeishuActivation")
}
return nil
}
func readFeishuConfigBytes(file string) ([]byte, map[string]interface{}, error) {
info, err := os.Lstat(file)
if err != nil {
return nil, nil, err
}
if !info.Mode().IsRegular() {
return nil, nil, feishuConfigError("openclaw.json")
}
f, err := os.Open(file)
if err != nil {
return nil, nil, err
}
defer f.Close()
raw, err := io.ReadAll(f)
if err != nil {
return nil, nil, err
}
var conf map[string]interface{}
if err = json.Unmarshal(raw, &conf); err != nil || conf == nil {
return nil, nil, feishuConfigError("openclaw.json")
}
return raw, conf, nil
}
+6
View File
@@ -107,6 +107,9 @@ func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) e
if err != nil {
return err
}
if isOpenclawBundledFeishuVersion(install.Version) && (strings.TrimSpace(req.Package) == "@larksuite/openclaw-lark" || strings.TrimSpace(req.Package) == "@openclaw/feishu") {
return buserr.New("ErrFeishuManaged")
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
@@ -132,6 +135,9 @@ func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
if err != nil {
return err
}
if isOpenclawBundledFeishuVersion(install.Version) && (req.PluginID == "openclaw-lark" || req.PluginID == "feishu") {
return buserr.New("ErrFeishuManaged")
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
+4
View File
@@ -148,6 +148,10 @@ func readOpenclawConfig(configPath string) (map[string]interface{}, error) {
}
func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error {
if _, busy := feishuSwitches.LoadOrStore(configPath, true); busy {
return buserr.New("TaskIsExecuting")
}
defer feishuSwitches.Delete(configPath)
ensureGatewaySecurityDefaults(conf)
ensureOpenclawUpdateDefaults(conf)
payload, err := json.MarshalIndent(conf, "", " ")
+26 -10
View File
@@ -305,13 +305,14 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return syncAppInstallStatus(&install, true)
case constant.Upgrade:
upgradeReq := request.AppInstallUpgrade{
InstallID: install.ID,
DetailID: req.DetailId,
Backup: req.Backup,
PullImage: req.PullImage,
DeleteImage: req.DeleteImage,
DockerCompose: req.DockerCompose,
TaskID: req.TaskID,
FeishuTransition: req.FeishuTransition,
InstallID: install.ID,
DetailID: req.DetailId,
Backup: req.Backup,
PullImage: req.PullImage,
DeleteImage: req.DeleteImage,
DockerCompose: req.DockerCompose,
TaskID: req.TaskID,
}
return upgradeInstall(upgradeReq)
case constant.Reload:
@@ -716,6 +717,7 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
if err != nil {
return versions, err
}
var feishuLegacy, feishuConfigured, feishuChecked bool
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
if err != nil {
return versions, err
@@ -740,6 +742,18 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
}
if canUpgrade {
feishuTarget := app.Key == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(detail.Version)
if feishuTarget && !feishuChecked {
conf, readErr := readOpenclawConfig(path.Join(install.GetPath(), "data/conf/openclaw.json"))
if readErr != nil {
return versions, readErr
}
feishuLegacy, feishuConfigured, err = classifyLegacyFeishu(conf, install.GetPath())
if err != nil {
return versions, err
}
feishuChecked = true
}
var newCompose string
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
filename := filepath.Base(detail.DownloadUrl)
@@ -774,9 +788,11 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
}
}
versions = append(versions, dto.AppVersion{
Version: detail.Version,
DetailId: detail.ID,
DockerCompose: newCompose,
FeishuLegacy: feishuTarget && feishuLegacy,
FeishuConfigured: feishuTarget && feishuConfigured,
Version: detail.Version,
DetailId: detail.ID,
DockerCompose: newCompose,
})
}
}
+74 -4
View File
@@ -60,10 +60,16 @@ type upgradeFileSnapshot struct {
}
type appUpgradeContext struct {
req request.AppInstallUpgrade
original model.AppInstall
candidate model.AppInstall
detail model.AppDetail
openclawOriginal []byte
feishuOriginal []byte
feishuImage string
feishuOldImage string
feishuEnabled bool
feishuLockPath string
req request.AppInstallUpgrade
original model.AppInstall
candidate model.AppInstall
detail model.AppDetail
phase appUpgradePhase
stopAttempted bool
@@ -102,6 +108,19 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
}()
feishuLockPath := ""
if install.App.Key == constant.AppOpenclaw {
feishuLockPath = path.Join(install.GetPath(), "data/conf/openclaw.json")
if _, busy := feishuSwitches.LoadOrStore(feishuLockPath, true); busy {
return buserr.New("TaskIsExecuting")
}
defer func() {
if releaseLock {
feishuSwitches.Delete(feishuLockPath)
}
}()
}
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
return err
@@ -129,6 +148,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
return err
}
ctx := &appUpgradeContext{
feishuLockPath: feishuLockPath,
req: req,
original: install,
candidate: install,
@@ -222,6 +242,10 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
return err
}
u.openclawOriginal, err = os.ReadFile(path.Join(u.stageDir, "data/conf/openclaw.json"))
if err != nil {
return err
}
}
if u.original.App.Key == constant.AppOpenresty {
for _, relativePath := range []string{
@@ -330,6 +354,9 @@ func (u *appUpgradeContext) prepare(t *task.Task) error {
if u.original.App.Resource == constant.AppResourceRemote {
go RequestDownloadCallBack(u.detail.DownloadCallBackUrl)
}
if err = u.prepareFeishuUpgrade(); err != nil {
return err
}
u.phase = appUpgradePrepared
return nil
}
@@ -387,6 +414,9 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
}
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
u.phase = appUpgradeStopped
if err := u.checkFeishuUpgradeSource(); err != nil {
return err
}
var err error
if u.original.App.Key == constant.AppOpenresty {
@@ -454,6 +484,9 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
} else {
t.Log("WARNING: no containers found for the upgraded application")
}
if err = u.verifyFeishuUpgrade(t); err != nil {
return err
}
u.candidate.Status = constant.StatusRunning
u.candidate.Message = ""
@@ -579,8 +612,34 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
if u.feishuOriginal != nil {
return err
}
rollbackErr = err
}
if u.feishuOriginal != nil {
if u.backupFile == "" {
return buserr.New("ErrFeishuUpgradeBackup")
}
if err := u.restoreManagedFiles(); err != nil {
return err
}
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", "", false); err != nil {
return err
}
if err := pinFeishuRollbackImage(u.original.GetComposePath(), u.feishuOldImage); err != nil {
return err
}
restoredCompose, err := os.ReadFile(u.original.GetComposePath())
if err != nil {
return err
}
u.original.DockerCompose = string(restoredCompose)
if _, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
return err
}
return u.finishRollback()
}
if u.backupFile != "" {
_ = u.restoreManagedFiles()
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
@@ -602,6 +661,11 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
}
func (u *appUpgradeContext) finishRollback() error {
if u.feishuOriginal != nil {
if err := waitOpenclawReady(u.original.ContainerName); err != nil {
return err
}
}
restored := u.original
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
return err
@@ -643,6 +707,12 @@ func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
}
func (u *appUpgradeContext) cleanup() {
if u.feishuLockPath != "" {
feishuSwitches.Delete(u.feishuLockPath)
}
if u.feishuOriginal != nil && u.rollbackErr != nil {
return
}
if u.snapshot != nil {
u.snapshot.Cleanup()
}
+336
View File
@@ -0,0 +1,336 @@
package service
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"gopkg.in/yaml.v3"
)
func classifyLegacyFeishu(conf map[string]interface{}, installPath string) (legacy, configured bool, err error) {
for _, field := range []string{"channels", "channels.feishu", "plugins", "plugins.entries", "plugins.entries.openclaw-lark", "plugins.entries.feishu"} {
parts := strings.Split(field, ".")
parent := conf
for _, part := range parts[:len(parts)-1] {
parent = childMap(parent, part)
}
if raw, exists := parent[parts[len(parts)-1]]; exists {
if _, ok := raw.(map[string]interface{}); !ok {
return false, false, feishuConfigError(field)
}
}
}
plugins := childMap(conf, "plugins")
for _, key := range []string{"allow", "deny"} {
if raw, exists := plugins[key]; exists {
if _, ok := raw.([]string); ok {
continue
}
ids, ok := raw.([]interface{})
if !ok {
return false, false, feishuConfigError("plugins." + key)
}
for _, id := range ids {
if _, ok := id.(string); !ok {
return false, false, feishuConfigError("plugins." + key)
}
}
}
}
for _, id := range []string{"openclaw-lark", "feishu"} {
if raw, exists := childMap(childMap(plugins, "entries"), id)["enabled"]; exists {
if _, ok := raw.(bool); !ok {
return false, false, feishuConfigError("plugins.entries." + id + ".enabled")
}
}
}
if usesBundledFeishu(conf) {
return false, false, nil
}
f := getChannelConfig(conf, "feishu")
configured = len(f) > 0
if raw, exists := conf["bindings"]; exists {
bindings, ok := raw.([]interface{})
if !ok {
return false, false, feishuConfigError("bindings")
}
for _, raw := range bindings {
binding, ok := raw.(map[string]interface{})
if !ok {
return false, false, feishuConfigError("bindings")
}
if match, exists := binding["match"]; exists {
if _, ok := match.(map[string]interface{}); !ok {
return false, false, feishuConfigError("bindings.match")
}
}
if extractStringValue(childMap(binding, "match")["channel"]) == "feishu" {
configured = true
}
}
}
entries := childMap(childMap(conf, "plugins"), "entries")
entry, selected := entries["openclaw-lark"]
if selected {
old, ok := entry.(map[string]interface{})
if !ok {
return false, configured, feishuConfigError("plugins.entries.openclaw-lark")
}
// A completed disable-only repair must not repeat on every upgrade.
if enabled, ok := old["enabled"].(bool); ok && !enabled && !configured {
return false, false, nil
}
}
installed, err := checkPluginInstalled(installPath, "feishu")
if err != nil {
return false, configured, err
}
_, oldThread := f["threadSession"]
_, oldReply := f["replyMode"]
_, oldStreaming := f["streaming"].(bool)
return selected || installed || oldThread || oldReply || oldStreaming, configured, nil
}
func prepareLegacyFeishu(conf map[string]interface{}, configured bool, transition *dto.AgentFeishuTransition) error {
if !configured {
ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false
return nil
}
if transition == nil || !transition.Confirmed {
return buserr.New("ErrFeishuUpgradeConfirm")
}
if !validFeishuTransition(transition.GroupSessionScope, transition.RequireMention) {
return feishuConfigError("feishuTransition")
}
return convertLegacyFeishu(conf, dto.AgentFeishuSwitchReq{Streaming: transition.Streaming, GroupSessionScope: transition.GroupSessionScope, RequireMention: transition.RequireMention})
}
func validFeishuTransition(scope, mention string) bool {
return (scope == "group" || scope == "group_sender" || scope == "group_topic" || scope == "group_topic_sender") && (mention == "true" || mention == "false")
}
func feishuRecoveryPayload(candidate []byte, configured bool) ([]byte, error) {
var conf map[string]interface{}
if err := json.Unmarshal(candidate, &conf); err != nil {
return nil, err
}
if configured {
ensureChildMap(ensureChildMap(conf, "channels"), "feishu")["enabled"] = false
selectBundledFeishu(conf, false)
}
ensureChildMap(ensureChildMap(ensureChildMap(conf, "plugins"), "entries"), "openclaw-lark")["enabled"] = false
return json.MarshalIndent(conf, "", " ")
}
func (u *appUpgradeContext) prepareFeishuUpgrade() error {
if u.original.App.Key != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(u.candidate.Version) {
return nil
}
source := filepath.Join(u.original.GetPath(), "data/conf/openclaw.json")
staged := filepath.Join(u.stageDir, "data/conf/openclaw.json")
// The staged copy predates protocol migration and is the exact cutover comparison source.
original := u.openclawOriginal
current, err := os.ReadFile(source)
if err != nil {
return err
}
if !bytes.Equal(original, current) {
return buserr.New("ErrFeishuConfigChanged")
}
_, conf, err := readFeishuConfigBytes(staged)
if err != nil {
return err
}
legacy, configured, err := classifyLegacyFeishu(conf, u.original.GetPath())
if err != nil || !legacy {
return err
}
if !u.req.Backup {
return buserr.New("ErrFeishuUpgradeBackup")
}
if err = prepareLegacyFeishu(conf, configured, u.req.FeishuTransition); err != nil {
return err
}
project, err := docker.GetComposeProject(u.original.Name, u.original.GetPath(), []byte(u.candidate.DockerCompose), u.envContent, false)
if err != nil {
return err
}
if len(project.Services) != 1 {
return buserr.New("ErrFeishuUpgradeMounts")
}
for _, service := range project.Services {
if service.Build != nil {
return buserr.New("ErrFeishuImageRequired")
}
u.feishuImage, err = bundledFeishuImage(service.Image)
if err != nil {
return err
}
configMounted := false
for _, v := range service.Volumes {
if v.Target == "/etc/localtime" {
continue
}
if v.Type != "bind" {
return buserr.New("ErrFeishuUpgradeMounts")
}
if err = feishuBackupPath(u.original.GetPath(), v.Source); err != nil {
return err
}
if v.Target == "/home/node/.openclaw" {
configMounted = filepath.Clean(v.Source) == filepath.Dir(source)
}
}
if !configMounted {
return buserr.New("ErrFeishuUpgradeMounts")
}
}
cli, err := docker.NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
info, err := cli.ContainerInspect(ctx, u.original.ContainerName)
if err != nil {
return err
}
u.feishuOldImage = info.Image
for _, m := range info.Mounts {
if m.Destination == "/etc/localtime" {
continue
}
if string(m.Type) != "bind" {
return buserr.New("ErrFeishuUpgradeMounts")
}
if err = feishuBackupPath(u.original.GetPath(), m.Source); err != nil {
return err
}
}
// Tar preserves symlinks: an external target would not be in the application backup.
if err = filepath.WalkDir(filepath.Join(u.original.GetPath(), "data"), func(file string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.Type()&os.ModeSymlink != 0 {
return feishuBackupPath(u.original.GetPath(), file)
}
return nil
}); err != nil {
return err
}
u.feishuOriginal = original
u.feishuEnabled = configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
before, err := os.ReadFile(staged)
if err != nil {
return err
}
if err = replaceFeishuConfig(staged, before, payload); err != nil {
return err
}
targetCompose := filepath.Join(u.stageDir, "docker-compose.yml")
if err = pinFeishuRollbackImage(targetCompose, u.feishuImage); err != nil {
return err
}
pinned, err := os.ReadFile(targetCompose)
if err != nil {
return err
}
u.candidate.DockerCompose = string(pinned)
return nil
}
func feishuBackupPath(root, source string) error {
root, err := filepath.EvalSymlinks(root)
if err != nil {
return err
}
resolved, err := filepath.EvalSymlinks(source)
if err != nil {
return buserr.New("ErrFeishuUpgradeMounts")
}
rel, err := filepath.Rel(root, resolved)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return buserr.New("ErrFeishuUpgradeMounts")
}
return nil
}
func (u *appUpgradeContext) checkFeishuUpgradeSource() error {
if u.feishuOriginal == nil {
return nil
}
current, err := os.ReadFile(filepath.Join(u.original.GetPath(), "data/conf/openclaw.json"))
if err != nil {
return err
}
if !bytes.Equal(current, u.feishuOriginal) {
return buserr.New("ErrFeishuConfigChanged")
}
return nil
}
func (u *appUpgradeContext) verifyFeishuUpgrade(t *task.Task) error {
if u.feishuOriginal == nil {
return nil
}
if err := verifyFeishuActivation(u.candidate.ContainerName, u.feishuEnabled); err != nil {
return err
}
if u.feishuEnabled {
probeFeishuConnection(u.candidate.ContainerName, t)
}
return nil
}
func probeFeishuConnection(name string, t *task.Task) {
output, err := cmd.RunDockerExecWithStdout(30*time.Second, name, "openclaw", "channels", "status", "--channel", "feishu", "--probe", "--json")
if err != nil || !feishuProbeOK(output) {
t.Log(i18n.GetMsgByKey("AgentFeishuProbeWarning"))
}
}
// Pin the restored service to the actual old image, even if its registry tag moved.
func pinFeishuRollbackImage(composeFile string, image string) error {
raw, err := os.ReadFile(composeFile)
if err != nil {
return err
}
var conf map[string]interface{}
if err = yaml.Unmarshal(raw, &conf); err != nil {
return err
}
services := childMap(conf, "services")
if len(services) != 1 {
return buserr.New("ErrFeishuUpgradeMounts")
}
for _, raw := range services {
service, ok := raw.(map[string]interface{})
if !ok {
return buserr.New("ErrFeishuUpgradeMounts")
}
service["image"] = image
}
payload, err := yaml.Marshal(conf)
if err != nil {
return err
}
return writeUpgradeFile(composeFile, payload, constant.FilePerm)
}
+16 -4
View File
@@ -197,7 +197,8 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
return nil
}
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string) error {
func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverFile string, isRollback bool, secret, taskID string, startAfterRecover ...bool) error {
startAfter := len(startAfterRecover) == 0 || startAfterRecover[0]
// Validate AI Gateway backups before stopping the app, creating rollback
// backups, or touching its files/database. Keep the validated extraction for
// the restore so uploaded/encrypted archives are not unpacked twice.
@@ -246,12 +247,15 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
return err
}
defer func() {
defer os.RemoveAll(tmpPath)
if !startAfter {
return
}
if isRollback {
_, _ = compose.UpWithoutBuild(install.GetComposePath())
} else {
_, _ = compose.Up(install.GetComposePath())
}
_ = os.RemoveAll(tmpPath)
}()
if !fileOp.Stat(tmpPath+"/app.json") || !fileOp.Stat(tmpPath+"/app.tar.gz") {
@@ -352,8 +356,12 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
appDir := install.GetPath()
backPath := fmt.Sprintf("%s_bak", appDir)
_ = fileOp.Rename(appDir, backPath)
_ = fileOp.CreateDir(appDir, constant.DirPerm)
if err := fileOp.Rename(appDir, backPath); err != nil && !startAfter {
return err
}
if err := fileOp.CreateDir(appDir, constant.DirPerm); err != nil && !startAfter {
return err
}
deCompressName := i18n.GetWithName("DeCompressFile", "app.tar.gz")
t.LogStart(deCompressName)
@@ -376,6 +384,10 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
_, _ = file.WriteString(mergedEnvContent)
}
// The upgrade transaction verifies readiness before saving its restored install.
if !startAfter {
return nil
}
backupInstall.ID = install.ID
backupInstall.Status = constant.StatusRunning
backupInstall.AppId = install.AppId
+23
View File
@@ -140,6 +140,29 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
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++ {
install, _ := appInstallRepo.GetFirst(appInstallRepo.WithContainerName(backData[i].Name))
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)
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 {
t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr))
}
+11
View File
@@ -5,12 +5,14 @@ import (
"fmt"
"os"
"path"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/mysql"
"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 {
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 {
req.Timeout = 30
}
@@ -180,6 +185,9 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
}
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 {
req.Timeout = 30
}
@@ -303,6 +311,9 @@ func (u *DatabaseService) Delete(req dto.DatabaseDelete) 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 {
case constant.AppPostgresql:
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) {
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")
}
if !isSupportedMongodbPrivilege(req.Permission) {
@@ -146,7 +146,7 @@ func (u *MongodbService) LoadFromRemote(req dto.MongodbLoadDB) 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")
}
@@ -168,7 +168,7 @@ func (u *MongodbService) BindUser(req dto.MongodbBind) 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")
}
@@ -190,7 +190,7 @@ func (u *MongodbService) ChangePassword(req dto.MongodbPassword) 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")
}
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) {
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")
}
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 {
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")
}
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 {
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")
}
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 {
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")
}
if err := checkMysqlNormalUser(req.Username); err != nil {
@@ -1302,7 +1302,7 @@ func isMysqlDatabaseResourceInUseError(err error) bool {
}
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")
}
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 {
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")
}
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) {
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")
}
@@ -366,7 +366,7 @@ func (u *PostgresqlService) ChangePrivileges(req dto.PostgresqlPrivileges) 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")
}
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
}
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))
if err != nil {
return err
@@ -656,6 +656,7 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
runtime.Version = req.Version
return runtimeRepo.Save(runtime)
}
oldRuntime := *runtime
appDetail, err := getRuntimeUpdateAppDetail(runtime, req)
if err != nil {
return err
@@ -700,6 +701,20 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err = downloadApp(app, appDetail, nil, nil); err != nil {
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 err = updateRuntimeVersionFiles(runtime, appVersionDir); err != nil {
return err
@@ -773,7 +788,13 @@ func (r *RuntimeService) Update(req request.RuntimeUpdate) error {
if err = runtimeRepo.Save(runtime); err != nil {
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
}
+86
View File
@@ -1,6 +1,9 @@
package service
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
@@ -12,6 +15,89 @@ import (
"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) {
current, err := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if err != nil {
+5 -1
View File
@@ -77,7 +77,11 @@ func Run() {
global.LOG.Errorf("generate random hour failed: %v", err)
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())
}
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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。"
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。"
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.AddLocalModelAccountLink,
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/channel/feishu/get", baseApi.GetAgentFeishuConfig)
aiToolsRouter.POST("/agents/channel/feishu/update", baseApi.UpdateAgentFeishuConfig)
aiToolsRouter.POST("/agents/channel/feishu/switch", baseApi.SwitchAgentFeishuPlugin)
aiToolsRouter.POST("/agents/channel/telegram/get", baseApi.GetAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig)
+3
View File
@@ -16,6 +16,9 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
baRouter.POST("", baseApi.ContainerCreate)
baRouter.POST("/update", baseApi.ContainerUpdate)
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("/search", baseApi.SearchContainer)
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 {
return err
}
defer dockerCLi.Close()
for _, image := range images {
if !forcePull {
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 {
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 nil
defer out.Close()
return handlePullImageProcess(out, nil)
}
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 (
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._-]*$`
VMNameValidationPattern = `^[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() {
patterns := []string{
TokenizerDownloadNamePattern,
ModelDownloaderRepoIDPattern,
VMNameValidationPattern,
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": {
"post": {
"consumes": [
@@ -30117,6 +30152,33 @@ const docTemplate = `{
}
},
"definitions": {
"dto.AgentFeishuTransition": {
"type": "object",
"properties": {
"confirmed": {
"type": "boolean"
},
"streaming": {
"type": "boolean"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"requireMention": {
"type": "string",
"enum": [
"true",
"false"
]
}
}
},
"dto.AgentAccountCreateReq": {
"properties": {
"apiKey": {
@@ -31215,12 +31277,18 @@ const docTemplate = `{
},
"dto.AgentFeishuConfig": {
"properties": {
"builtinManaged": {
"type": "boolean"
},
"bots": {
"items": {
"$ref": "#/definitions/dto.AgentFeishuBot"
},
"type": "array"
},
"builtinAvailable": {
"type": "boolean"
},
"connectionMode": {
"type": "string"
},
@@ -31239,9 +31307,21 @@ const docTemplate = `{
"groupPolicy": {
"type": "string"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"installed": {
"type": "boolean"
},
"legacy": {
"type": "boolean"
},
"replyMode": {
"type": "string"
},
@@ -31253,10 +31333,49 @@ const docTemplate = `{
},
"threadSession": {
"type": "boolean"
},
"legacyConfigured": {
"type": "boolean"
}
},
"type": "object"
},
"dto.AgentFeishuSwitchReq": {
"properties": {
"agentId": {
"type": "integer"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"requireMention": {
"type": "string",
"enum": [
"true",
"false"
]
},
"streaming": {
"type": "boolean"
},
"taskID": {
"type": "string"
}
},
"required": [
"agentId",
"taskID",
"groupSessionScope",
"requireMention"
],
"type": "object"
},
"dto.AgentFeishuConfigReq": {
"properties": {
"agentId": {
@@ -31303,6 +31422,15 @@ const docTemplate = `{
],
"type": "string"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"replyMode": {
"type": "string"
},
@@ -33167,6 +33295,12 @@ const docTemplate = `{
},
"version": {
"type": "string"
},
"feishuLegacy": {
"type": "boolean"
},
"feishuConfigured": {
"type": "boolean"
}
},
"type": "object"
@@ -42953,6 +43087,9 @@ const docTemplate = `{
},
"taskID": {
"type": "string"
},
"feishuTransition": {
"$ref": "#/definitions/dto.AgentFeishuTransition"
}
},
"required": [
+137
View File
@@ -1167,6 +1167,41 @@
]
}
},
"/ai/agents/channel/feishu/switch": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.AgentFeishuSwitchReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Switch to the bundled OpenClaw Feishu plugin",
"tags": [
"AI"
]
}
},
"/ai/agents/channel/feishu/update": {
"post": {
"consumes": [
@@ -30113,6 +30148,33 @@
}
},
"definitions": {
"dto.AgentFeishuTransition": {
"type": "object",
"properties": {
"confirmed": {
"type": "boolean"
},
"streaming": {
"type": "boolean"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"requireMention": {
"type": "string",
"enum": [
"true",
"false"
]
}
}
},
"dto.AgentAccountCreateReq": {
"properties": {
"apiKey": {
@@ -31211,12 +31273,18 @@
},
"dto.AgentFeishuConfig": {
"properties": {
"builtinManaged": {
"type": "boolean"
},
"bots": {
"items": {
"$ref": "#/definitions/dto.AgentFeishuBot"
},
"type": "array"
},
"builtinAvailable": {
"type": "boolean"
},
"connectionMode": {
"type": "string"
},
@@ -31235,9 +31303,21 @@
"groupPolicy": {
"type": "string"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"installed": {
"type": "boolean"
},
"legacy": {
"type": "boolean"
},
"replyMode": {
"type": "string"
},
@@ -31249,10 +31329,49 @@
},
"threadSession": {
"type": "boolean"
},
"legacyConfigured": {
"type": "boolean"
}
},
"type": "object"
},
"dto.AgentFeishuSwitchReq": {
"properties": {
"agentId": {
"type": "integer"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"requireMention": {
"type": "string",
"enum": [
"true",
"false"
]
},
"streaming": {
"type": "boolean"
},
"taskID": {
"type": "string"
}
},
"required": [
"agentId",
"taskID",
"groupSessionScope",
"requireMention"
],
"type": "object"
},
"dto.AgentFeishuConfigReq": {
"properties": {
"agentId": {
@@ -31299,6 +31418,15 @@
],
"type": "string"
},
"groupSessionScope": {
"type": "string",
"enum": [
"group",
"group_sender",
"group_topic",
"group_topic_sender"
]
},
"replyMode": {
"type": "string"
},
@@ -33163,6 +33291,12 @@
},
"version": {
"type": "string"
},
"feishuLegacy": {
"type": "boolean"
},
"feishuConfigured": {
"type": "boolean"
}
},
"type": "object"
@@ -42949,6 +43083,9 @@
},
"taskID": {
"type": "string"
},
"feishuTransition": {
"$ref": "#/definitions/dto.AgentFeishuTransition"
}
},
"required": [
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Sync basic node info"
GenerateSSLInfo: "Generate node SSL info"
ConnInfoNotMatch: "Connection info mismatch"
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"
StartService: "Start service"
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"
ConnInfoNotMatch: "La información de conexión no coincide"
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"
StartService: "Iniciar servicio"
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 گره"
ConnInfoNotMatch: "اطلاعات اتصال مطابقت ندارد"
MakeAgentPackage: "تولید بسته نصب گره"
PrepareNodePackage: "آماده‌سازی بسته موجود در گره فرزند"
PrepareNodePackageDetail: "استفاده از بسته موجود در گره فرزند: {{ .name }}"
SyncNodeConfig: "همگام‌سازی پیکربندی گره"
ErrNodePackagePath: "مسیر مطلق فایل بسته در گره فرزند را وارد کنید"
ErrNodePackageFile: "پردازش فایل بسته {{ .name }} ناموفق بود: {{ .err }}"
ErrNodePackageMember: "بسته دارای ورودی نامعتبر یا تکراری است: {{ .name }}"
ErrNodePackageMismatch: "بسته باید با نسخه گره اصلی {{ .version }} و معماری گره فرزند {{ .arch }} مطابقت داشته باشد"
ErrNodePackageMissing: "فایل ضروری در بسته موجود نیست: {{ .name }}"
ErrNodePackageVersion: "نسخه بسته {{ .version }} با نسخه گره اصلی {{ .expected }} مطابقت ندارد"
ErrNodePackageArch: "معماری فایل اجرایی Agent با معماری گره فرزند {{ .arch }} مطابقت ندارد"
SendAgent: "توزیع بسته نصب گره"
StartService: "شروع سرویس"
NoBackupNode: "گره پشتیبان خالی است. ابتدا یکی را انتخاب کرده و ذخیره کنید"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "ノードの基本情報を同期"
GenerateSSLInfo: "ノードのSSL情報を生成"
ConnInfoNotMatch: "接続情報が一致しません"
MakeAgentPackage: "ノードインストールパッケージを生成"
PrepareNodePackage: "子ノード上の既存パッケージを準備"
PrepareNodePackageDetail: "子ノード上の既存パッケージを使用: {{ .name }}"
SyncNodeConfig: "ノード設定を同期"
ErrNodePackagePath: "子ノード上のパッケージの絶対ファイルパスを入力してください"
ErrNodePackageFile: "パッケージファイル {{ .name }} の処理に失敗しました: {{ .err }}"
ErrNodePackageMember: "パッケージに無効または重複する項目があります: {{ .name }}"
ErrNodePackageMismatch: "パッケージはメインノードのバージョン {{ .version }} と子ノードのアーキテクチャ {{ .arch }} に一致する必要があります"
ErrNodePackageMissing: "パッケージに必要なファイルがありません: {{ .name }}"
ErrNodePackageVersion: "パッケージのバージョン {{ .version }} がメインノードのバージョン {{ .expected }} と一致しません"
ErrNodePackageArch: "Agent 実行ファイルのアーキテクチャが子ノードのアーキテクチャ {{ .arch }} と一致しません"
SendAgent: "ノードインストールパッケージを送信"
StartService: "サービスを開始"
NoBackupNode: "現在バックアップノードが空です。保存するバックアップノードを選択して再試行してください!"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "노드 기본 데이터 동기화"
GenerateSSLInfo: "노드 SSL 정보 생성"
ConnInfoNotMatch: "연결 정보가 일치하지 않음"
MakeAgentPackage: "노드 설치 패키지 생성"
PrepareNodePackage: "하위 노드의 기존 패키지 준비"
PrepareNodePackageDetail: "하위 노드의 기존 패키지 사용: {{ .name }}"
SyncNodeConfig: "노드 설정 동기화"
ErrNodePackagePath: "하위 노드에 있는 패키지의 절대 파일 경로를 입력하세요"
ErrNodePackageFile: "패키지 파일 {{ .name }} 처리 실패: {{ .err }}"
ErrNodePackageMember: "패키지에 잘못되었거나 중복된 항목이 있습니다: {{ .name }}"
ErrNodePackageMismatch: "패키지는 주 노드 버전 {{ .version }} 및 하위 노드 아키텍처 {{ .arch }}와 일치해야 합니다"
ErrNodePackageMissing: "패키지에 필수 파일이 없습니다: {{ .name }}"
ErrNodePackageVersion: "패키지 버전 {{ .version }}이 주 노드 버전 {{ .expected }}와 일치하지 않습니다"
ErrNodePackageArch: "Agent 실행 파일의 아키텍처가 하위 노드 아키텍처 {{ .arch }}와 일치하지 않습니다"
SendAgent: "노드 설치 패키지 배포"
StartService: "서비스 시작"
NoBackupNode: "현재 백업 노드가 비어 있습니다. 저장할 백업 노드를 선택한 후 다시 시도하십시오"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "ຊິງຂໍ້ມູນພື້ນຖານຂອງໂ
GenerateSSLInfo: "ສ້າງຂໍ້ມູນ SSL ຂອງໂນດ"
ConnInfoNotMatch: "ຂໍ້ມູນການເຊື່ອມຕໍ່ບໍ່ກົງກັນ"
MakeAgentPackage: "ສ້າງແພັກເກດຕິດຕັ້ງໂນດ"
PrepareNodePackage: "ກະກຽມແພັກເກດທີ່ມີຢູ່ໃນໂນດລູກ"
PrepareNodePackageDetail: "ໃຊ້ແພັກເກດທີ່ມີຢູ່ໃນໂນດລູກ: {{ .name }}"
SyncNodeConfig: "ຊິງຂໍ້ມູນການຕັ້ງຄ່າໂນດ"
ErrNodePackagePath: "ປ້ອນເສັ້ນທາງໄຟລ໌ແບບເຕັມຂອງແພັກເກດໃນໂນດລູກ"
ErrNodePackageFile: "ປະມວນຜົນໄຟລ໌ແພັກເກດ {{ .name }} ລົ້ມເຫຼວ: {{ .err }}"
ErrNodePackageMember: "ແພັກເກດມີລາຍການທີ່ບໍ່ຖືກຕ້ອງ ຫຼື ຊ້ຳກັນ: {{ .name }}"
ErrNodePackageMismatch: "ແພັກເກດຕ້ອງກົງກັບເວີຊັນໂນດຫຼັກ {{ .version }} ແລະ ສະຖາປັດຕະຍະກຳໂນດລູກ {{ .arch }}"
ErrNodePackageMissing: "ແພັກເກດຂາດໄຟລ໌ທີ່ຈຳເປັນ: {{ .name }}"
ErrNodePackageVersion: "ເວີຊັນແພັກເກດ {{ .version }} ບໍ່ກົງກັບເວີຊັນໂນດຫຼັກ {{ .expected }}"
ErrNodePackageArch: "ສະຖາປັດຕະຍະກຳຂອງໂປຣແກຣມ Agent ບໍ່ກົງກັບສະຖາປັດຕະຍະກຳໂນດລູກ {{ .arch }}"
SendAgent: "ແຈກຢາຍແພັກເກດຕິດຕັ້ງໂນດ"
StartService: "ເລີ່ມບໍລິການ"
NoBackupNode: "ໂນດສຳຮອງຫວ່າງເປົ່າ. ກະລຸນາເລືອກແລ້ວບັນທຶກກ່ອນ"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Sinkronkan maklumat asas nod"
GenerateSSLInfo: "Cipta maklumat SSL nod"
ConnInfoNotMatch: "Maklumat sambungan tidak sepadan"
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"
StartService: "Mulakan perkhidmatan"
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ó"
ConnInfoNotMatch: "As informações de conexão não coincidem"
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ó"
StartService: "Iniciando serviço"
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 информацию узла"
ConnInfoNotMatch: "Информация о соединении не совпадает"
MakeAgentPackage: "Сгенерировать установочный пакет узла"
PrepareNodePackage: "Подготовка готового пакета на дочернем узле"
PrepareNodePackageDetail: "Использование готового пакета на дочернем узле: {{ .name }}"
SyncNodeConfig: "Синхронизация конфигурации узла"
ErrNodePackagePath: "Укажите абсолютный путь к файлу пакета на дочернем узле"
ErrNodePackageFile: "Не удалось обработать файл пакета {{ .name }}: {{ .err }}"
ErrNodePackageMember: "Пакет содержит недопустимую или повторяющуюся запись: {{ .name }}"
ErrNodePackageMismatch: "Пакет должен соответствовать версии главного узла {{ .version }} и архитектуре дочернего узла {{ .arch }}"
ErrNodePackageMissing: "В пакете отсутствует обязательный файл: {{ .name }}"
ErrNodePackageVersion: "Версия пакета {{ .version }} не совпадает с версией главного узла {{ .expected }}"
ErrNodePackageArch: "Архитектура исполняемого файла Agent не совпадает с архитектурой дочернего узла {{ .arch }}"
SendAgent: "Отправить установочный пакет узла"
StartService: "Запустить сервис"
NoBackupNode: "Резервный узел в настоящее время пуст. Выберите резервный узел для сохранения и повторите попытку"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "Temel düğüm bilgilerini senkronize et"
GenerateSSLInfo: "Düğüm SSL bilgilerini oluştur"
ConnInfoNotMatch: "Bağlantı bilgisi uyuşmuyor"
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"
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"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "同步節點基礎資料"
GenerateSSLInfo: "產生節點 SSL 資訊"
ConnInfoNotMatch: "連接資訊不符"
MakeAgentPackage: "產生節點安裝包"
PrepareNodePackage: "準備子節點已有安裝包"
PrepareNodePackageDetail: "使用子節點已有安裝包:{{ .name }}"
SyncNodeConfig: "同步節點設定"
ErrNodePackagePath: "請填寫子節點安裝包的絕對檔案路徑"
ErrNodePackageFile: "處理安裝包檔案 {{ .name }} 失敗:{{ .err }}"
ErrNodePackageMember: "安裝包包含無效或重複項目:{{ .name }}"
ErrNodePackageMismatch: "安裝包必須符合主節點版本 {{ .version }} 和子節點架構 {{ .arch }}"
ErrNodePackageMissing: "安裝包缺少必要檔案:{{ .name }}"
ErrNodePackageVersion: "安裝包版本 {{ .version }} 與主節點版本 {{ .expected }} 不一致"
ErrNodePackageArch: "Agent 程式架構與子節點架構 {{ .arch }} 不一致"
SendAgent: "下發節點安裝包"
StartService: "啟動服務"
NoBackupNode: "目前備份節點為空,請選擇備份節點儲存後重試!"
+10
View File
@@ -296,6 +296,16 @@ SyncAgentBaseInfo: "同步节点基础数据"
GenerateSSLInfo: "生成节点 SSL 信息"
ConnInfoNotMatch: "连接信息不匹配"
MakeAgentPackage: "生成节点安装包"
PrepareNodePackage: "准备子节点已有安装包"
PrepareNodePackageDetail: "使用子节点已有安装包:{{ .name }}"
SyncNodeConfig: "同步节点配置"
ErrNodePackagePath: "请填写子节点安装包的绝对文件路径"
ErrNodePackageFile: "处理安装包文件 {{ .name }} 失败:{{ .err }}"
ErrNodePackageMember: "安装包包含无效或重复条目:{{ .name }}"
ErrNodePackageMismatch: "安装包必须匹配主节点版本 {{ .version }} 和子节点架构 {{ .arch }}"
ErrNodePackageMissing: "安装包缺少必要文件:{{ .name }}"
ErrNodePackageVersion: "安装包版本 {{ .version }} 与主节点版本 {{ .expected }} 不一致"
ErrNodePackageArch: "Agent 程序架构与子节点架构 {{ .arch }} 不一致"
SendAgent: "下发节点安装包"
StartService: "启动服务"
NoBackupNode: "当前备份节点为空,请选择备份节点保存后重试!"
+1 -3
View File
@@ -6,15 +6,13 @@ import (
)
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)
func Init() {
patterns := []string{
TokenizerDisallowedCharsPattern,
OrderByValidationPattern,
}
+21
View File
@@ -779,6 +779,11 @@ export namespace AI {
}
export interface AgentFeishuConfig {
builtinManaged?: boolean;
legacyConfigured?: boolean;
groupSessionScope?: string;
builtinAvailable?: boolean;
legacy?: boolean;
enabled: boolean;
threadSession: boolean;
replyMode: string;
@@ -793,6 +798,7 @@ export namespace AI {
}
export interface AgentFeishuConfigUpdateReq {
groupSessionScope?: string;
agentId: number;
enabled: boolean;
threadSession: boolean;
@@ -806,6 +812,21 @@ export namespace AI {
bots: AgentFeishuBot[];
}
export interface AgentFeishuTransition {
confirmed: boolean;
streaming: boolean;
groupSessionScope: string;
requireMention: string;
}
export interface AgentFeishuSwitchReq {
agentId: number;
taskID: string;
streaming: boolean;
groupSessionScope: string;
requireMention: 'true' | 'false';
}
export interface AgentTelegramConfigReq {
agentId: number;
}
+4
View File
@@ -1,3 +1,4 @@
import { AI } from './ai';
import { ReqPage, CommonModel } from '.';
export namespace App {
@@ -260,6 +261,7 @@ export namespace App {
}
export interface AppInstalledOp {
feishuTransition?: AI.AgentFeishuTransition;
installId: number;
operate: string;
backupId?: number;
@@ -285,6 +287,8 @@ export namespace App {
}
export interface VersionDetail {
feishuLegacy?: boolean;
feishuConfigured?: boolean;
version: string;
detailId: number;
}
+3
View File
@@ -136,6 +136,9 @@ export namespace Container {
mode: string;
}
export interface ContainerInfo {
latestVersion?: string;
targetImage?: string;
hasUpdate?: boolean;
containerID: string;
name: 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);
};
export const switchAgentFeishuPlugin = (req: AI.AgentFeishuSwitchReq) => {
return http.post(`/ai/agents/channel/feishu/switch`, req);
};
export const updateAgentFeishuConfig = (req: AI.AgentFeishuConfigUpdateReq) => {
return http.post(`/ai/agents/channel/feishu/update`, req);
};
+30
View File
@@ -285,3 +285,33 @@ export const updateIpv6Option = (fixedCidrV6: string, ip6Tables: boolean, experi
export const updateDaemonJsonByfile = (params: Container.DaemonJsonUpdateByFile) => {
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 -4
View File
@@ -478,10 +478,7 @@ onMounted(async () => {
onUnmounted(() => {
onCloseLog();
if (resizeObserver && logContainer.value) {
resizeObserver.unobserve(logContainer.value);
resizeObserver.disconnect();
}
resizeObserver?.disconnect();
});
defineExpose({ changeTail, onDownload, clearLog });
+17 -6
View File
@@ -1,12 +1,20 @@
<template>
<el-dialog v-model="open" :show-close="showClose" @closed="handleClose" :width="width">
<div v-if="open">
<LogFile :config="config" :showTail="showTail"></LogFile>
</div>
<DialogPro
v-model="open"
:show-close="showClose"
: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>
<slot name="task-footer"></slot>
</template>
</el-dialog>
</DialogPro>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
@@ -72,8 +80,11 @@ const openWithResourceID = (taskType: string, taskOperate: string, resourceID: n
const em = defineEmits(['close']);
const handleClose = () => {
em('close', true);
open.value = false;
};
const handleClosed = () => {
em('close', true);
bus.emit('refreshTask', true);
bus.emit('refreshApp', true);
};
@@ -13,14 +13,16 @@
@click.stop="handleButtonClick(button)"
>
<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>
</span>
<span v-if="actions.more.length" class="fu-table-operations__action">
<el-dropdown class="fu-table-more-button" :trigger="trigger" @command="handleButtonClick">
<span class="fu-table-operations__dropdown-trigger">
<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>
</span>
<template #dropdown>
@@ -33,7 +35,9 @@
:divided="button.divided"
>
<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-menu>
</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) => {
if (!isOperationDisabled(button, props.row)) {
button.click?.(props.row);
+1
View File
@@ -23,6 +23,7 @@ export interface FuTableOperationButton<Row = any> {
type?: string;
icon?: any;
divided?: boolean;
dot?: boolean | ((row: Row) => boolean);
}
export const FU_TABLE_STORAGE_PREFIX = 'FU-T-';
+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) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.complexityPassword')));
@@ -682,6 +706,8 @@ interface CommonRule {
userName: FormItemRule;
simpleName: FormItemRule;
simplePassword: FormItemRule;
databaseUsername: FormItemRule;
databasePassword: FormItemRule;
dbName: FormItemRule;
imageName: FormItemRule;
composeName: FormItemRule;
@@ -860,6 +886,14 @@ export const Rules: CommonRule = {
required: true,
trigger: 'blur',
},
databaseUsername: {
validator: checkDatabaseUsername,
trigger: 'blur',
},
databasePassword: {
validator: checkDatabasePassword,
trigger: 'blur',
},
port: {
required: true,
trigger: 'blur',
+35 -1
View File
@@ -292,7 +292,7 @@ const message = {
rePassword: 'Confirm password is inconsistent with the password.',
requiredInput: 'This field is required.',
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.",
duplicate: 'This value must be unique.',
commonName:
@@ -774,6 +774,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'Legacy Feishu must be handled before upgrading. A full application backup is required; failure restores the old image and matching data. An unconfigured legacy plugin is only disabled.',
feishuRecoveryNotice:
'If switching fails, recovery will attempt to restore the Gateway with Feishu disabled. The legacy plugin will not be re-enabled. The original config backup is retained.',
feishuRecover: 'Disable legacy plugin and recover startup',
feishuRecoverConfirm:
'Back up the config, disable the unconfigured legacy Feishu plugin and restart this instance? All channels will be briefly interrupted. Plugin files are retained.',
feishuBuiltin: 'Built-in Feishu',
feishuImageRequired: 'Use an image containing bundled Feishu.',
feishuLegacy:
'The legacy plugin is no longer maintained. Review the session, streaming and mention settings below before switching.',
feishuBuiltinHint: 'This plugin is updated with the OpenClaw image.',
feishuSessionTopic: 'Separate by group topic',
feishuSessionTopicSender: 'Separate by group topic and sender',
feishuSwitch: 'Switch to built-in plugin',
feishuMentionChoice: 'Choose explicitly whether a mention is required before switching.',
feishuSwitchConfirm:
'Switch to bundled Feishu and restart this instance? All channels will be briefly interrupted. Legacy-only tools become unavailable; streaming and topic behavior follow your choices, and some chats may start new sessions. Credentials, bindings and pairing storage are retained.',
deepSeekAccessHostHelper:
"Enter the IPv4 address or hostname used in your browser, without a scheme, port or path. HTTPS uses the application's local certificate.",
deepSeekAuthHelper:
@@ -2076,6 +2095,14 @@ const message = {
missing: 'Missing',
},
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',
importContainerBackupTip: 'Import container 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.',
nodeUpgradePackageManage: 'Installation Package Management',
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:
'Only `1panel-agent_v*_*` or `1panel-v*-linux-*` installation packages are supported',
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.',
requiredInput: 'Este campo es obligatorio.',
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.',
duplicate: 'Este valor debe ser único.',
commonName:
@@ -777,6 +777,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'Antes de actualizar se debe tratar el complemento Feishu antiguo. Se requiere una copia completa; si falla, se restauran la imagen anterior y sus datos. Un complemento sin configurar solo se desactiva.',
feishuRecoveryNotice:
'Si el cambio falla, se intentará recuperar la puerta de enlace con Feishu desactivado. No se reactivará el complemento antiguo. Se conserva la copia de configuración.',
feishuRecover: 'Desactivar complemento antiguo y recuperar inicio',
feishuRecoverConfirm:
'¿Guardar la configuración, desactivar el complemento Feishu antiguo sin configurar y reiniciar? Todos los canales se interrumpirán brevemente. Se conservan los archivos del complemento.',
feishuBuiltin: 'Feishu integrado',
feishuImageRequired: 'Use una imagen que incluya Feishu.',
feishuLegacy:
'El complemento anterior ya no se mantiene. Revise las sesiones, la transmisión y las menciones antes de cambiar.',
feishuBuiltinHint: 'Este complemento se actualiza con la imagen de OpenClaw.',
feishuSessionTopic: 'Separar por tema del grupo',
feishuSessionTopicSender: 'Separar por tema del grupo y remitente',
feishuSwitch: 'Cambiar al complemento integrado',
feishuMentionChoice: 'Elija explícitamente si se requiere una mención antes de cambiar.',
feishuSwitchConfirm:
'¿Cambiar a Feishu integrado y reiniciar? Todos los canales se interrumpirán brevemente. Las herramientas exclusivas del complemento antiguo dejarán de funcionar; la transmisión y los temas seguirán sus opciones y algunas conversaciones podrán iniciar sesiones nuevas. Se conservan credenciales, vínculos y emparejamientos.',
deepSeekAccessHostHelper:
'Introduzca la IPv4 o el nombre de host usado en el navegador, sin protocolo, puerto ni ruta. HTTPS usa el certificado local de la aplicación.',
deepSeekAuthHelper:
@@ -2094,6 +2113,14 @@ const message = {
missing: 'No existe',
},
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',
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',
@@ -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.',
nodeUpgradePackageManage: 'Gestión de Paquetes de Instalación',
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:
'Solo se admiten paquetes de instalación `1panel-agent_v*_*` o `1panel-v*-linux-*`',
nodeUpgradePackageHostConfirm: 'Se seleccionará el archivo del servidor {0}. ¿Continuar?',
+35 -1
View File
@@ -291,7 +291,7 @@ const message = {
rePassword: 'تکرار رمز عبور با رمز عبور مطابقت ندارد.',
requiredInput: 'این فیلد الزامی است.',
requiredSelect: 'یک مورد از لیست انتخاب کنید',
illegalChar: 'کاراکترهای & ; $ \' ` ( ) " > < | در حال حاضر پشتیبانی نمی‌شوند',
illegalChar: "کاراکترهای & ; $ ' ` ( ) \" > < {'|'} در حال حاضر پشتیبانی نمی‌شوند",
illegalInput: 'این فیلد نباید شامل کاراکترهای غیرمجاز باشد.',
duplicate: 'این مقدار باید یکتا باشد.',
commonName:
@@ -769,6 +769,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'پیش از ارتقا باید افزونه قدیمی Feishu بررسی شود. پشتیبان کامل برنامه لازم است؛ در صورت شکست، تصویر قبلی و داده‌های مربوط بازیابی می‌شوند. افزونه پیکربندی‌نشده فقط غیرفعال می‌شود.',
feishuRecoveryNotice:
'در صورت شکست تغییر، بازیابی دروازه با Feishu غیرفعال انجام می‌شود. افزونه قدیمی دوباره فعال نخواهد شد. پشتیبان تنظیمات اصلی نگه داشته می‌شود.',
feishuRecover: 'غیرفعال کردن افزونه قدیمی و بازیابی راه‌اندازی',
feishuRecoverConfirm:
'از تنظیمات پشتیبان گرفته شود، افزونه قدیمی و پیکربندی‌نشده Feishu غیرفعال و نمونه دوباره راه‌اندازی شود؟ همه کانال‌ها موقتاً قطع می‌شوند. فایل‌های افزونه حفظ می‌شوند.',
feishuBuiltin: 'Feishu داخلی',
feishuImageRequired: 'از تصویری با Feishu داخلی استفاده کنید.',
feishuLegacy:
'افزونه قدیمی دیگر نگهداری نمی‌شود. پیش از تغییر، تنظیمات نشست، جریان پاسخ و اشاره را بررسی کنید.',
feishuBuiltinHint: 'این افزونه همراه تصویر OpenClaw به‌روز می‌شود.',
feishuSessionTopic: 'تفکیک بر اساس موضوع گروه',
feishuSessionTopicSender: 'تفکیک بر اساس موضوع گروه و فرستنده',
feishuSwitch: 'تغییر به افزونه داخلی',
feishuMentionChoice: 'پیش از تغییر مشخص کنید آیا اشاره لازم است.',
feishuSwitchConfirm:
'به Feishu داخلی تغییر داده و دوباره راه‌اندازی شود؟ همه کانال‌ها موقتاً قطع می‌شوند. ابزارهای ویژه افزونه قدیمی در دسترس نخواهند بود؛ جریان پاسخ و موضوع‌ها طبق انتخاب شما عمل می‌کنند و بعضی گفتگوها ممکن است نشست جدید بسازند. اعتبارنامه‌ها، اتصال‌ها و اطلاعات جفت‌سازی حفظ می‌شوند.',
deepSeekAccessHostHelper:
'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.',
deepSeekAuthHelper:
@@ -2055,6 +2074,14 @@ const message = {
missing: 'ناموجود',
},
container: {
imageCheckUpdates: 'بررسی به‌روزرسانی ایمیج‌ها',
imageCheckUpdateNow: 'بررسی اکنون',
imageCheckUpdateSchedule: 'بررسی زمان‌بندی‌شده',
imageCheckUpdateDisable: 'غیرفعال‌کردن بررسی زمان‌بندی‌شده',
imageCheckUpdateStarted: 'در حال بررسی به‌روزرسانی ایمیج‌های همه کانتینرهای این گره',
imageCheckUpdateResult:
'بررسی پایان یافت: بررسی‌شده {checked}، قابل ارتقا {updates}، ناموفق {failed}، ردشده {skipped}',
createByCommand: 'ایجاد با دستور',
importContainerBackupTip: 'واردات فایل پشتیبان کانتینر، فقط .tar.gz پشتیبانی می‌شود',
importComposeBackupTip: 'واردات فایل پشتیبان compose، فقط .tar.gz پشتیبانی می‌شود',
@@ -6932,6 +6959,13 @@ const message = {
'گره‌های انتخاب شده پس از اتمام ارتقاء گره اصلی، به طور خودکار شروع به ارتقاء می‌کنند، بدون نیاز به عملیات دستی.',
nodeUpgradePackageManage: 'مدیریت بسته نصب',
nodeUpgradePackageClean: 'پاک‌سازی',
packageSource: 'منبع بسته',
packageFromMaster: 'گره اصلی',
packageFromNode: 'بسته موجود',
packagePath: 'مسیر بسته در گره فرزند',
packagePathHelper:
'مسیر مطلق بسته .tar.gz در گره فرزند را وارد کنید. نسخه باید با گره اصلی و معماری با گره فرزند مطابقت داشته باشد. بسته اصلی حفظ می‌شود؛ فایل‌های کمکی ناموجود از گره اصلی منتقل می‌شوند.',
packagePathInvalid: 'مسیر مطلق فایل بسته در گره فرزند را وارد کنید',
nodeUpgradePackageFormatHelper:
'فقط بسته‌های نصب `1panel-agent_v*_*` یا `1panel-v*-linux-*` پشتیبانی می‌شوند',
nodeUpgradePackageHostConfirm: 'فایل سرور {0} انتخاب خواهد شد. ادامه می‌دهید؟',
+34 -1
View File
@@ -292,7 +292,7 @@ const message = {
rePassword: 'パスワードがパスワードと矛盾することを確認してください。',
requiredInput: 'この項目は必須です。',
requiredSelect: 'リスト内のアイテムを選択します',
illegalChar: '現在、文字 & ; $ \' ` ( ) " > < | の注入はサポートされていません',
illegalChar: "現在、文字 & ; $ ' ` ( ) \" > < {'|'} の注入はサポートされていません",
illegalInput: 'このフィールドには違法なキャラクターが含まれてはなりません。',
duplicate: 'この値は一意である必要があります。',
commonName:
@@ -773,6 +773,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'更新前に旧 Feishu プラグインを処理します。アプリ全体のバックアップが必要です。失敗時は旧イメージと対応するデータを復元します。未設定の旧プラグインは無効化のみ行います。',
feishuRecoveryNotice:
'切り替え失敗時は Feishu を無効にした状態でゲートウェイの復旧を試みます。旧プラグインは再有効化しません。元の設定のバックアップは保持します。',
feishuRecover: '旧プラグインを無効化して起動を復旧',
feishuRecoverConfirm:
'設定をバックアップし、未設定の旧 Feishu プラグインを無効化して再起動しますか?全チャネルが一時中断されます。プラグインのファイルは保持します。',
feishuBuiltin: '組み込み Feishu',
feishuImageRequired: 'Feishu を内蔵したイメージを使用してください。',
feishuLegacy:
'旧プラグインの保守は終了しました。切り替え前にセッション分離、ストリーミング、メンション設定を確認してください。',
feishuBuiltinHint: 'このプラグインは OpenClaw イメージとともに更新されます。',
feishuSessionTopic: 'グループのトピック別',
feishuSessionTopicSender: 'グループのトピックと送信者別',
feishuSwitch: '組み込みプラグインに切り替え',
feishuMentionChoice: '切り替え前にメンションが必要か選択してください。',
feishuSwitchConfirm:
'内蔵 Feishu に切り替えて再起動しますか?全チャネルが一時中断され、旧プラグイン専用ツールは利用できなくなります。ストリーミングとトピックは選択した設定に従い、一部の会話は新しいセッションになります。認証情報、紐付け、ペアリング情報は保持します。',
deepSeekAccessHostHelper:
'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。',
deepSeekAuthHelper:
@@ -2060,6 +2079,13 @@ const message = {
missing: '存在しない',
},
container: {
imageCheckUpdates: 'イメージの更新を確認',
imageCheckUpdateNow: '今すぐ確認',
imageCheckUpdateSchedule: '定期チェック',
imageCheckUpdateDisable: '定期チェックを無効にする',
imageCheckUpdateStarted: 'このノードのすべてのコンテナで使用されているイメージの更新を確認しています',
imageCheckUpdateResult: '確認完了:成功 {checked}、更新可能 {updates}、失敗 {failed}、スキップ {skipped}',
createByCommand: 'コマンドで作成',
importContainerBackupTip: 'コンテナバックアップファイルをインポートします。.tar.gz のみ対応です',
importComposeBackupTip: 'Compose バックアップファイルをインポートします。.tar.gz のみ対応です',
@@ -6963,6 +6989,13 @@ const message = {
'選択したノードは、マスターノードのアップグレード完了後に自動的にアップグレードを開始し、手動操作は不要です。',
nodeUpgradePackageManage: 'インストールパッケージ管理',
nodeUpgradePackageClean: 'クリーンアップ',
packageSource: 'パッケージの取得元',
packageFromMaster: 'メインノード',
packageFromNode: '既存パッケージ',
packagePath: '子ノード上のパッケージパス',
packagePathHelper:
'子ノード上の .tar.gz パッケージの絶対パスを入力してください。バージョンはメインノード、アーキテクチャは子ノードと一致する必要があります。元のパッケージは保持され、不足する補助ファイルはメインノードから転送されます。',
packagePathInvalid: '子ノード上のパッケージの絶対ファイルパスを入力してください',
nodeUpgradePackageFormatHelper:
'`1panel-agent_v*_*` または `1panel-v*-linux-*` のインストールパッケージのみ対応しています',
nodeUpgradePackageHostConfirm: 'サーバーファイル {0} を選択します。続行しますか?',
+34 -1
View File
@@ -290,7 +290,7 @@ const message = {
rePassword: '확인 비밀번호가 비밀번호와 일치하지 않습니다.',
requiredInput: '이 필드는 필수 항목입니다.',
requiredSelect: '목록에서 항목을 선택하세요',
illegalChar: '현재 & ; $ \' ` ( ) " > < | 문자 주입은 지원되지 않습니다',
illegalChar: "현재 & ; $ ' ` ( ) \" > < {'|'} 문자 주입은 지원되지 않습니다",
illegalInput: '이 필드에는 유효하지 않은 문자가 포함될 수 없습니다.',
duplicate: '이 값은 고유해야 합니다.',
commonName:
@@ -764,6 +764,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'업그레이드 전에 기존 Feishu 플러그인을 처리합니다. 전체 앱 백업이 필요하며 실패하면 이전 이미지와 해당 데이터를 복원합니다. 미설정 플러그인은 비활성화만 합니다.',
feishuRecoveryNotice:
'전환에 실패하면 Feishu를 비활성화한 상태로 게이트웨이 복구를 시도합니다. 기존 플러그인은 다시 활성화하지 않으며 원본 설정 백업을 보관합니다.',
feishuRecover: '기존 플러그인 비활성화 및 시작 복구',
feishuRecoverConfirm:
'설정을 백업하고 미설정 Feishu 플러그인을 비활성화한 뒤 다시 시작할까요? 모든 채널이 잠시 중단됩니다. 플러그인 파일은 유지합니다.',
feishuBuiltin: '내장 Feishu',
feishuImageRequired: 'Feishu가 내장된 이미지를 사용하세요.',
feishuLegacy:
'기존 플러그인의 유지 관리가 종료되었습니다. 전환 전에 세션 분리, 스트리밍 및 멘션 설정을 확인하세요.',
feishuBuiltinHint: '이 플러그인은 OpenClaw 이미지와 함께 업데이트됩니다.',
feishuSessionTopic: '그룹 주제별 분리',
feishuSessionTopicSender: '그룹 주제 및 발신자별 분리',
feishuSwitch: '내장 플러그인으로 전환',
feishuMentionChoice: '전환 전에 멘션 필요 여부를 명시적으로 선택하세요.',
feishuSwitchConfirm:
'내장 Feishu로 전환하고 다시 시작할까요? 모든 채널이 잠시 중단되고 기존 전용 도구는 사용할 수 없습니다. 스트리밍과 주제 동작은 선택한 설정을 따르며 일부 대화는 새 세션으로 시작됩니다. 자격 증명, 연결 및 페어링 정보는 유지됩니다.',
deepSeekAccessHostHelper:
'브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.',
deepSeekAuthHelper:
@@ -2036,6 +2055,13 @@ const message = {
missing: '없음',
},
container: {
imageCheckUpdates: '이미지 업데이트 확인',
imageCheckUpdateNow: '지금 확인',
imageCheckUpdateSchedule: '정기 확인',
imageCheckUpdateDisable: '정기 확인 끄기',
imageCheckUpdateStarted: '현재 노드의 모든 컨테이너에서 사용하는 이미지의 업데이트를 확인하고 있습니다',
imageCheckUpdateResult: '확인 완료: 성공 {checked}, 업데이트 가능 {updates}, 실패 {failed}, 건너뜀 {skipped}',
createByCommand: '명령으로 생성',
importContainerBackupTip: '컨테이너 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
importComposeBackupTip: 'Compose 백업 파일 가져오기, .tar.gz 형식만 지원합니다',
@@ -6821,6 +6847,13 @@ const message = {
'선택된 노드는 마스터 노드 업그레이드 완료 후 자동으로 업그레이드를 시작하며 수동 작업이 필요하지 않습니다.',
nodeUpgradePackageManage: '설치 패키지 관리',
nodeUpgradePackageClean: '정리',
packageSource: '패키지 소스',
packageFromMaster: '주 노드',
packageFromNode: '기존 패키지',
packagePath: '하위 노드의 패키지 경로',
packagePathHelper:
'하위 노드에 있는 .tar.gz 패키지의 절대 경로를 입력하세요. 버전은 주 노드와, 아키텍처는 하위 노드와 일치해야 합니다. 원본 패키지는 유지되며 누락된 보조 파일은 주 노드에서 전송됩니다.',
packagePathInvalid: '하위 노드에 있는 패키지의 절대 파일 경로를 입력하세요',
nodeUpgradePackageFormatHelper: '`1panel-agent_v*_*` 또는 `1panel-v*-linux-*` 설치 패키지만 지원됩니다',
nodeUpgradePackageHostConfirm: '서버 파일 {0}을(를) 선택합니다. 계속하시겠습니까?',
nodeUpgradePackageCleanHelper: '모든 낮은 버전의 업데이트 패키지를 정리합니다. 계속하시겠습니까?',
+33 -1
View File
@@ -288,7 +288,7 @@ const message = {
rePassword: 'ຢືນຢັນລະຫັດຜ່ານບໍ່ກົງກັນ.',
requiredInput: 'ຈຳເປັນຕ້ອງປ້ອນຂໍ້ມູນນີ້.',
requiredSelect: 'ເລືອກລາຍການໃນລາຍຊື່',
illegalChar: 'ບໍ່ຮອງຮັບການໃຊ້ຕົວອັກສອນ & ; $ \' ` ( ) " > < | ໃນເວລານີ້',
illegalChar: "ບໍ່ຮອງຮັບການໃຊ້ຕົວອັກສອນ & ; $ ' ` ( ) \" > < {'|'} ໃນເວລານີ້",
illegalInput: 'ຂໍ້ມູນນີ້ຕ້ອງບໍ່ມີຕົວອັກສອນທີ່ບໍ່ຖືກຕ້ອງ.',
duplicate: 'ຄ່ານີ້ຕ້ອງບໍ່ຊ້ຳກັນ.',
commonName:
@@ -762,6 +762,24 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'ຕ້ອງຈັດການປລັກອິນ Feishu ເກົ່າກ່ອນອັບເກຣດ. ຕ້ອງສຳຮອງແອັບທັງໝົດ; ຖ້າລົ້ມເຫຼວຈະກູ້ຄືນອິມເມຈເກົ່າແລະຂໍ້ມູນ. ປລັກອິນທີ່ຍັງບໍ່ຕັ້ງຄ່າຈະຖືກປິດເທົ່ານັ້ນ.',
feishuRecoveryNotice:
'ຖ້າການປ່ຽນລົ້ມເຫຼວ ຈະພະຍາຍາມກູ້ຄືນເກດເວໂດຍປິດ Feishu. ຈະບໍ່ເປີດປລັກອິນເກົ່າຄືນ. ເກັບສຳຮອງການຕັ້ງຄ່າເດີມໄວ້.',
feishuRecover: 'ປິດປລັກອິນເກົ່າແລະກູ້ຄືນການເລີ່ມ',
feishuRecoverConfirm:
'ສຳຮອງການຕັ້ງຄ່າ, ປິດປລັກອິນ Feishu ເກົ່າທີ່ຍັງບໍ່ຕັ້ງຄ່າ ແລະເລີ່ມໃໝ່ບໍ? ທຸກຊ່ອງທາງຈະຢຸດຊົ່ວຄາວ. ໄຟລ໌ປລັກອິນຍັງຖືກເກັບໄວ້.',
feishuBuiltin: 'Feishu ໃນຕົວ',
feishuImageRequired: 'ໃຊ້ອິມເມຈທີ່ມີ Feishu ໃນຕົວ.',
feishuLegacy: 'ປລັກອິນເກົ່າຢຸດບຳລຸງຮັກສາແລ້ວ. ກວດການແຍກເຊດຊັນ, ສະຕຣີມ ແລະ ການເອີ້ນ @ ກ່ອນສະຫຼັບ.',
feishuBuiltinHint: 'ປລັກອິນນີ້ອັບເດດພ້ອມອິມເມຈ OpenClaw.',
feishuSessionTopic: 'ແຍກຕາມຫົວຂໍ້ກຸ່ມ',
feishuSessionTopicSender: 'ແຍກຕາມຫົວຂໍ້ກຸ່ມ ແລະ ຜູ້ສົ່ງ',
feishuSwitch: 'ສະຫຼັບເປັນປລັກອິນໃນຕົວ',
feishuMentionChoice: 'ກ່ອນສະຫຼັບ ກະລຸນາເລືອກວ່າຕ້ອງການ @ ຫຼືບໍ່.',
feishuSwitchConfirm:
'ປ່ຽນເປັນ Feishu ໃນຕົວແລະເລີ່ມໃໝ່ບໍ? ທຸກຊ່ອງທາງຈະຢຸດຊົ່ວຄາວ. ເຄື່ອງມືສະເພາະຂອງປລັກອິນເກົ່າຈະໃຊ້ບໍ່ໄດ້; ການສົ່ງແບບສະຕຣີມແລະຫົວຂໍ້ຈະຕາມຄ່າທີ່ເລືອກ ແລະບາງການສົນທະນາອາດເລີ່ມເຊສຊັນໃໝ່. ຂໍ້ມູນຢືນຢັນ, ການຜູກ ແລະການຈັບຄູ່ຈະຖືກເກັບໄວ້.',
deepSeekAccessHostHelper:
'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.',
deepSeekAuthHelper:
@@ -2023,6 +2041,13 @@ const message = {
missing: 'ບໍ່ມີ',
},
container: {
imageCheckUpdates: 'ກວດສອບການອັບເດດອິມເມຈ',
imageCheckUpdateNow: 'ກວດສອບຕອນນີ້',
imageCheckUpdateSchedule: 'ກວດສອບຕາມຕາຕະລາງ',
imageCheckUpdateDisable: 'ປິດການກວດສອບຕາມຕາຕະລາງ',
imageCheckUpdateStarted: 'ກຳລັງກວດສອບການອັບເດດອິມເມຈຂອງທຸກຄອນເທນເນີໃນໂນດນີ້',
imageCheckUpdateResult: 'ກວດສອບສຳເລັດ: ກວດແລ້ວ {checked}, ອັບເກຣດໄດ້ {updates}, ລົ້ມເຫຼວ {failed}, ຂ້າມ {skipped}',
createByCommand: 'ສ້າງດ້ວຍຄຳສັ່ງ',
importContainerBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງຄອນເທນເນີ, ຮອງຮັບສະເພາະ .tar.gz',
importComposeBackupTip: 'ນຳເຂົ້າໄຟລ໌ສຳຮອງ compose, ຮອງຮັບສະເພາະ .tar.gz',
@@ -6750,6 +6775,13 @@ const message = {
nodeUpgradeHelper: 'ໂນດທີ່ເລືອກຈະເລີ່ມອັບເກຣດອັດຕະໂນມັດຫຼັງຈາກໂນດຫຼັກອັບເກຣດສຳເລັດ.',
nodeUpgradePackageManage: 'ຈັດການແພັກເກັດຕິດຕັ້ງ',
nodeUpgradePackageClean: 'ລ້າງຂໍ້ມູນ',
packageSource: 'ແຫຼ່ງທີ່ມາຂອງແພັກເກດ',
packageFromMaster: 'ໂນດຫຼັກ',
packageFromNode: 'ແພັກເກດທີ່ມີຢູ່',
packagePath: 'ເສັ້ນທາງແພັກເກດໃນໂນດລູກ',
packagePathHelper:
'ປ້ອນເສັ້ນທາງແບບເຕັມຂອງແພັກເກດ .tar.gz ໃນໂນດລູກ. ເວີຊັນຕ້ອງກົງກັບໂນດຫຼັກ ແລະ ສະຖາປັດຕະຍະກຳຕ້ອງກົງກັບໂນດລູກ. ແພັກເກດຕົ້ນສະບັບຈະຖືກເກັບໄວ້; ໄຟລ໌ປະກອບທີ່ຂາດຈະຖືກໂອນຈາກໂນດຫຼັກ.',
packagePathInvalid: 'ກະລຸນາປ້ອນເສັ້ນທາງໄຟລ໌ແບບເຕັມຂອງແພັກເກດໃນໂນດລູກ',
nodeUpgradePackageFormatHelper:
'ຮອງຮັບສະເພາະແພັກເກັດ `1panel-agent_v*_*` ຫຼື `1panel-v*-linux-*` ເທົ່ານັ້ນ',
nodeUpgradePackageHostConfirm: 'ໄຟລ໌ເຊີເວີ {0} ຈະຖືກເລືອກ. ຕ້ອງການຕໍ່ບໍ?',
+35 -1
View File
@@ -295,7 +295,7 @@ const message = {
rePassword: 'Pengesahan kata laluan tidak sepadan dengan kata laluan.',
requiredInput: 'Ruangan ini wajib diisi.',
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.',
duplicate: 'Nilai ini mesti unik.',
commonName:
@@ -782,6 +782,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'Pemalam Feishu lama perlu ditangani sebelum naik taraf. Sandaran aplikasi penuh diperlukan; kegagalan memulihkan imej lama dan datanya. Pemalam tanpa konfigurasi hanya dinyahaktifkan.',
feishuRecoveryNotice:
'Jika pertukaran gagal, pemulihan gerbang akan dicuba dengan Feishu dinyahaktifkan. Pemalam lama tidak diaktifkan semula. Sandaran konfigurasi asal dikekalkan.',
feishuRecover: 'Nyahaktif pemalam lama dan pulihkan permulaan',
feishuRecoverConfirm:
'Sandarkan konfigurasi, nyahaktifkan pemalam Feishu lama yang belum dikonfigurasi dan mulakan semula? Semua saluran terganggu seketika. Fail pemalam dikekalkan.',
feishuBuiltin: 'Feishu terbina dalam',
feishuImageRequired: 'Gunakan imej yang mengandungi Feishu terbina dalam.',
feishuLegacy:
'Pemalam lama tidak lagi diselenggara. Semak tetapan sesi, penstriman dan sebutan sebelum bertukar.',
feishuBuiltinHint: 'Pemalam ini dikemas kini bersama imej OpenClaw.',
feishuSessionTopic: 'Asingkan mengikut topik kumpulan',
feishuSessionTopicSender: 'Asingkan mengikut topik kumpulan dan penghantar',
feishuSwitch: 'Tukar kepada pemalam terbina dalam',
feishuMentionChoice: 'Pilih dengan jelas sama ada sebutan diperlukan sebelum bertukar.',
feishuSwitchConfirm:
'Tukar kepada Feishu terbina dalam dan mulakan semula? Semua saluran terganggu seketika. Alat khusus pemalam lama tidak lagi tersedia; penstriman dan topik mengikut pilihan anda, dan sesetengah sembang mungkin memulakan sesi baharu. Bukti kelayakan, ikatan dan pasangan dikekalkan.',
deepSeekAccessHostHelper:
'Masukkan alamat IPv4 atau nama hos yang digunakan dalam pelayar, tanpa protokol, port atau laluan. HTTPS menggunakan sijil setempat aplikasi.',
deepSeekAuthHelper:
@@ -2091,6 +2110,14 @@ const message = {
missing: 'Tiada',
},
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',
importContainerBackupTip: 'Import fail sandaran kontena, 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.',
nodeUpgradePackageManage: 'Pengurusan Pakej Pemasangan',
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:
'Hanya pakej pemasangan `1panel-agent_v*_*` atau `1panel-v*-linux-*` disokong',
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.',
requiredInput: 'Este campo é obrigatório.',
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.',
duplicate: 'Este valor deve ser único.',
commonName:
@@ -775,6 +775,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'O plugin Feishu antigo deve ser tratado antes da atualização. É necessário um backup completo; em caso de falha, a imagem anterior e seus dados serão restaurados. Um plugin não configurado será apenas desativado.',
feishuRecoveryNotice:
'Se a troca falhar, será tentada a recuperação do gateway com Feishu desativado. O plugin antigo não será reativado. O backup original será mantido.',
feishuRecover: 'Desativar plugin antigo e recuperar inicialização',
feishuRecoverConfirm:
'Fazer backup, desativar o plugin Feishu antigo não configurado e reiniciar? Todos os canais serão interrompidos brevemente. Os arquivos do plugin serão mantidos.',
feishuBuiltin: 'Feishu integrado',
feishuImageRequired: 'Use uma imagem com Feishu integrado.',
feishuLegacy:
'O plugin antigo não é mais mantido. Revise as sessões, o streaming e as menções antes de trocar.',
feishuBuiltinHint: 'Este plugin é atualizado com a imagem do OpenClaw.',
feishuSessionTopic: 'Separar por tópico do grupo',
feishuSessionTopicSender: 'Separar por tópico do grupo e remetente',
feishuSwitch: 'Trocar para o plugin integrado',
feishuMentionChoice: 'Escolha explicitamente se uma menção é necessária antes de trocar.',
feishuSwitchConfirm:
'Mudar para Feishu integrado e reiniciar? Todos os canais serão interrompidos brevemente. As ferramentas exclusivas do plugin antigo deixarão de funcionar; streaming e tópicos seguirão suas escolhas e algumas conversas poderão iniciar novas sessões. Credenciais, vínculos e pareamentos serão mantidos.',
deepSeekAccessHostHelper:
'Informe o IPv4 ou nome do host usado no navegador, sem protocolo, porta ou caminho. O HTTPS usa o certificado local do aplicativo.',
deepSeekAuthHelper:
@@ -2093,6 +2112,14 @@ const message = {
missing: 'Não existe',
},
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',
importContainerBackupTip: 'Importar arquivo de backup de contêiner, 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.',
nodeUpgradePackageManage: 'Gerenciamento de Pacotes de Instalação',
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:
'Somente pacotes de instalação `1panel-agent_v*_*` ou `1panel-v*-linux-*` são suportados',
nodeUpgradePackageHostConfirm: 'O arquivo do servidor {0} será selecionado. Continuar?',
+35 -1
View File
@@ -293,7 +293,7 @@ const message = {
rePassword: 'Подтверждение пароля не совпадает с паролем.',
requiredInput: 'Это поле обязательно для заполнения.',
requiredSelect: 'Выберите элемент из списка',
illegalChar: 'В настоящее время не поддерживается вставка символов & ; $ \' ` ( ) " > < |',
illegalChar: "В настоящее время не поддерживается вставка символов & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: 'Это поле не должно содержать недопустимых символов.',
duplicate: 'Значение должно быть уникальным.',
commonName:
@@ -773,6 +773,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'Перед обновлением необходимо обработать старый плагин Feishu. Требуется полная резервная копия; при сбое восстанавливаются прежний образ и его данные. Ненастроенный плагин только отключается.',
feishuRecoveryNotice:
'При сбое переключения будет предпринята попытка восстановить шлюз с отключённым Feishu. Старый плагин не включается повторно. Исходная резервная копия сохраняется.',
feishuRecover: 'Отключить старый плагин и восстановить запуск',
feishuRecoverConfirm:
'Создать резервную копию, отключить ненастроенный старый плагин Feishu и перезапустить? Все каналы будут кратковременно прерваны. Файлы плагина сохраняются.',
feishuBuiltin: 'Встроенный Feishu',
feishuImageRequired: 'Используйте образ со встроенным Feishu.',
feishuLegacy:
'Старый плагин больше не поддерживается. Перед переключением проверьте разделение сессий, потоковые ответы и упоминания.',
feishuBuiltinHint: 'Плагин обновляется вместе с образом OpenClaw.',
feishuSessionTopic: 'Разделять по теме группы',
feishuSessionTopicSender: 'Разделять по теме группы и отправителю',
feishuSwitch: 'Перейти на встроенный плагин',
feishuMentionChoice: 'Перед переключением явно выберите, требуется ли упоминание.',
feishuSwitchConfirm:
'Переключиться на встроенный Feishu и перезапустить? Все каналы будут кратковременно прерваны. Инструменты старого плагина станут недоступны; потоковые ответы и темы будут работать по выбранным настройкам, некоторые чаты начнут новые сеансы. Учётные данные, привязки и сопряжения сохраняются.',
deepSeekAccessHostHelper:
'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.',
deepSeekAuthHelper:
@@ -2086,6 +2105,14 @@ const message = {
missing: 'Отсутствует',
},
container: {
imageCheckUpdates: 'Проверить обновления образов',
imageCheckUpdateNow: 'Проверить сейчас',
imageCheckUpdateSchedule: 'Проверка по расписанию',
imageCheckUpdateDisable: 'Отключить проверку по расписанию',
imageCheckUpdateStarted: 'Проверка обновлений образов всех контейнеров на текущем узле',
imageCheckUpdateResult:
'Проверка завершена: проверено {checked}, доступны обновления {updates}, ошибок {failed}, пропущено {skipped}',
createByCommand: 'Создать с помощью команды',
importContainerBackupTip: 'Импорт файла резервной копии контейнера, поддерживается только .tar.gz',
importComposeBackupTip: 'Импорт файла резервной копии compose, поддерживается только .tar.gz',
@@ -7082,6 +7109,13 @@ const message = {
'Выбранные узлы автоматически начнут обновляться после завершения обновления основного узла, ручное вмешательство не требуется.',
nodeUpgradePackageManage: 'Управление пакетами установки',
nodeUpgradePackageClean: 'Очистить',
packageSource: 'Источник пакета',
packageFromMaster: 'Главный узел',
packageFromNode: 'Готовый пакет',
packagePath: 'Путь к пакету на дочернем узле',
packagePathHelper:
'Укажите абсолютный путь к пакету .tar.gz на дочернем узле. Версия должна совпадать с главным узлом, а архитектура — с дочерним. Исходный пакет сохраняется; недостающие вспомогательные файлы передаются с главного узла.',
packagePathInvalid: 'Укажите абсолютный путь к файлу пакета на дочернем узле',
nodeUpgradePackageFormatHelper:
'Поддерживаются только установочные пакеты `1panel-agent_v*_*` или `1panel-v*-linux-*`',
nodeUpgradePackageHostConfirm: 'Будет выбран файл сервера {0}. Продолжить?',
+35 -1
View File
@@ -295,7 +295,7 @@ const message = {
rePassword: 'Şifre onayı, şifre ile tutarsız.',
requiredInput: 'Bu alan gereklidir.',
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.',
duplicate: 'Bu değer benzersiz olmalıdır.',
commonName:
@@ -778,6 +778,25 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'Yükseltmeden önce eski Feishu eklentisi ele alınmalıdır. Tam yedek gerekir; hata durumunda eski imaj ve verileri geri yüklenir. Yapılandırılmamış eklenti yalnızca devre dışı bırakılır.',
feishuRecoveryNotice:
'Geçiş başarısız olursa Feishu kapalı tutularak ağ geçidi kurtarılmaya çalışılır. Eski eklenti yeniden etkinleştirilmez. Özgün yapılandırma yedeği korunur.',
feishuRecover: 'Eski eklentiyi kapat ve başlatmayı kurtar',
feishuRecoverConfirm:
'Yapılandırma yedeklensin, yapılandırılmamış eski Feishu eklentisi kapatılsın ve örnek yeniden başlatılsın mı? Tüm kanallar kısa süre kesilir. Eklenti dosyaları korunur.',
feishuBuiltin: 'Yerleşik Feishu',
feishuImageRequired: 'Yerleşik Feishu içeren bir imaj kullanın.',
feishuLegacy:
'Eski eklenti artık bakımlı değil. Geçişten önce oturum, akış ve bahsetme ayarlarını gözden geçirin.',
feishuBuiltinHint: 'Bu eklenti OpenClaw imajıyla birlikte güncellenir.',
feishuSessionTopic: 'Grup konusuna göre ayır',
feishuSessionTopicSender: 'Grup konusu ve gönderene göre ayır',
feishuSwitch: 'Yerleşik eklentiye geç',
feishuMentionChoice: 'Geçişten önce bahsetmenin gerekli olup olmadığını açıkça seçin.',
feishuSwitchConfirm:
'Yerleşik Feishu eklentisine geçilip yeniden başlatılsın mı? Tüm kanallar kısa süre kesilir. Eski eklentiye özel araçlar kullanılamaz; akış ve konular seçimlerinize göre çalışır, bazı sohbetler yeni oturum açabilir. Kimlik bilgileri, bağlamalar ve eşleştirmeler korunur.',
deepSeekAccessHostHelper:
'Tarayıcıda kullanılan IPv4 adresini veya ana bilgisayar adını protokol, bağlantı noktası veya yol olmadan girin. HTTPS, uygulamanın yerel sertifikasını kullanır.',
deepSeekAuthHelper:
@@ -2091,6 +2110,14 @@ const message = {
missing: 'Mevcut değil',
},
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',
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',
@@ -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.',
nodeUpgradePackageManage: 'Kurulum Paketi Yönetimi',
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:
'Yalnızca `1panel-agent_v*_*` veya `1panel-v*-linux-*` kurulum paketleri desteklenir',
nodeUpgradePackageHostConfirm: 'Sunucu dosyası {0} seçilecek. Devam edilsin mi?',
+32 -1
View File
@@ -284,7 +284,7 @@ const message = {
rePassword: '密碼不一致,請檢查後重新輸入',
requiredInput: '請填寫必填項',
requiredSelect: '請選擇必選項',
illegalChar: '暫不支援注入字元 & ; $ \' ` ( ) " > < |',
illegalChar: "暫不支援注入字元 & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: '輸入框中存在不合法字元',
commonName: '支援非特殊字元開頭,英文、中文、數字、.-和_,長度1-128',
userName: '支援非特殊字元開頭、英文、中文、數字和_,長度3-30',
@@ -740,6 +740,23 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'升級前需要處理舊飛書外掛。此次升級必須完整備份;失敗時還原原映像及配套資料。未設定飛書時僅停用舊外掛。',
feishuRecoveryNotice: '切換失敗時將嘗試恢復閘道,但飛書保持停用,不會重新啟用舊外掛。原設定備份會保留。',
feishuRecover: '停用舊外掛並恢復啟動',
feishuRecoverConfirm:
'將備份設定、停用未設定的舊飛書外掛並重新啟動執行個體,短暫中斷所有頻道。外掛檔案將保留。是否繼續?',
feishuBuiltin: '內建飛書外掛',
feishuImageRequired: '請使用包含內建飛書外掛的映像。',
feishuLegacy: '舊外掛已停止維護。切換前請確認下方的工作階段隔離、串流回覆和 @ 設定。',
feishuBuiltinHint: '外掛隨 OpenClaw 映像一起更新。',
feishuSessionTopic: '依群組話題隔離',
feishuSessionTopicSender: '依群組話題和傳送者隔離',
feishuSwitch: '切換至內建外掛',
feishuMentionChoice: '切換前請明確選擇是否需要 @。',
feishuSwitchConfirm:
'將切換至內建飛書並重新啟動執行個體,短暫中斷所有頻道。舊外掛專有工具不再可用;串流與話題行為依所選設定執行,部分聊天可能產生新工作階段。憑證、繫結和配對儲存會保留。是否繼續?',
deepSeekAccessHostHelper:
'填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。',
deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。',
@@ -1962,6 +1979,13 @@ const message = {
missing: '不存在',
},
container: {
imageCheckUpdates: '檢查更新映像',
imageCheckUpdateNow: '立即檢查',
imageCheckUpdateSchedule: '定時檢查',
imageCheckUpdateDisable: '關閉定時檢查',
imageCheckUpdateStarted: '已開始檢查目前節點全部容器的映像更新',
imageCheckUpdateResult: '檢查完成:成功 {checked},可升級 {updates},失敗 {failed},略過 {skipped}',
createByCommand: '指令建立',
importContainerBackupTip: '匯入容器備份檔案,僅支援 .tar.gz 格式',
importComposeBackupTip: '匯入編排備份檔案,僅支援 .tar.gz 格式',
@@ -6470,6 +6494,13 @@ const message = {
nodeUpgradeHelper: '選中的節點將在主節點升級完成後自動開始升級,無需手動操作。',
nodeUpgradePackageManage: '安裝包管理',
nodeUpgradePackageClean: '清理',
packageSource: '安裝包來源',
packageFromMaster: '主節點分發',
packageFromNode: '使用現有包',
packagePath: '子節點安裝包路徑',
packagePathHelper:
'填寫子節點上 .tar.gz 安裝包的絕對路徑。版本須與主節點一致,架構須符合子節點。保留原包,缺少的輔助檔案由主節點補傳。',
packagePathInvalid: '請輸入子節點上安裝包的絕對檔案路徑',
nodeUpgradePackageFormatHelper: '僅支援 `1panel-agent_v*_*` 或 `1panel-v*-linux-*` 安裝包',
nodeUpgradePackageHostConfirm: '即將選擇伺服器檔案:{0},是否繼續?',
nodeUpgradePackageCleanHelper: '即將清理所有低版本升級包,是否繼續?',
+32 -1
View File
@@ -284,7 +284,7 @@ const message = {
rePassword: '密码不一致,请检查后重新输入',
requiredInput: '请填写必填项',
requiredSelect: '请选择必选项',
illegalChar: '暂不支持注入字符 & ; $ \' ` ( ) " > < |',
illegalChar: "暂不支持注入字符 & ; $ ' ` ( ) \" > < {'|'}",
illegalInput: '输入框中存在不合法字符',
duplicate: '内容不能重复',
commonName: '支持非特殊字符开头,英文、中文、数字、.-和_,长度1-128',
@@ -757,6 +757,23 @@ const message = {
},
aiTools: {
agents: {
feishuUpgradeNotice:
'升级前需要处理旧飞书插件。此次升级必须完整备份;失败时恢复原镜像及配套数据。未配置飞书时仅禁用旧插件。',
feishuRecoveryNotice: '切换失败时将尝试恢复网关,但飞书保持停用,不会重新启用旧插件。原配置备份会保留。',
feishuRecover: '禁用旧插件并恢复启动',
feishuRecoverConfirm:
'将备份配置、禁用未配置的旧飞书插件并重启当前实例,短暂中断所有渠道。插件文件将保留。是否继续?',
feishuBuiltin: '内置飞书插件',
feishuImageRequired: '请使用包含内置飞书插件的镜像。',
feishuLegacy: '旧插件已停止维护。切换前请确认下方的会话隔离、流式回复和 @ 设置。',
feishuBuiltinHint: '插件随 OpenClaw 镜像一起更新。',
feishuSessionTopic: '按群话题隔离',
feishuSessionTopicSender: '按群话题和发送者隔离',
feishuSwitch: '切换到内置插件',
feishuMentionChoice: '切换前请明确选择是否需要 @。',
feishuSwitchConfirm:
'将切换到内置飞书并重启实例,短暂中断所有渠道。旧插件独有工具不再可用;流式回复和话题行为按所选设置执行,部分聊天可能产生新会话。凭据、绑定和配对存储会保留。是否继续?',
deepSeekAccessHostHelper:
'填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。',
deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。',
@@ -1971,6 +1988,13 @@ const message = {
missing: '不存在',
},
container: {
imageCheckUpdates: '检查更新镜像',
imageCheckUpdateNow: '立即检查',
imageCheckUpdateSchedule: '定时检查',
imageCheckUpdateDisable: '关闭定时检查',
imageCheckUpdateStarted: '已开始检查当前节点全部容器的镜像更新',
imageCheckUpdateResult: '检查完成:成功 {checked},可升级 {updates},失败 {failed},跳过 {skipped}',
createByCommand: '命令创建',
importContainerBackupTip: '导入容器备份文件,仅支持 .tar.gz 格式',
importComposeBackupTip: '导入编排备份文件,仅支持 .tar.gz 格式',
@@ -5428,6 +5452,13 @@ const message = {
nodeUpgradeHelper: '选中的节点将在主节点升级完成后自动开始升级,无需手动操作。',
nodeUpgradePackageManage: '安装包管理',
nodeUpgradePackageClean: '清理',
packageSource: '安装包来源',
packageFromMaster: '主节点分发',
packageFromNode: '使用已有包',
packagePath: '子节点安装包路径',
packagePathHelper:
'填写子节点上 .tar.gz 安装包的绝对路径。版本须与主节点一致,架构须匹配子节点。原包保留,缺少的辅助文件由主节点补传。',
packagePathInvalid: '请输入子节点上安装包的绝对文件路径',
nodeUpgradePackageFormatHelper: '仅支持 `1panel-agent_v*_*` 或 `1panel-v*-linux-*` 安装包',
nodeUpgradePackageHostConfirm: '即将选择服务器文件:{0},是否继续?',
nodeUpgradePackageCleanHelper: '即将清理所有低版本升级包,是否继续?',
+6
View File
@@ -130,6 +130,12 @@ html.dark {
--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-checked-text-color: var(--panel-main-bg-color-10);
}
@@ -0,0 +1,31 @@
<template>
<el-form-item :label="t('aiTools.agents.groupSessionScope')" prop="groupSessionScope">
<el-select :model-value="groupSessionScope" @update:model-value="emit('update:groupSessionScope', $event)">
<el-option :label="t('aiTools.agents.groupSessionScopeGroup')" value="group" />
<el-option :label="t('aiTools.agents.groupSessionScopeGroupSender')" value="group_sender" />
<el-option :label="t('aiTools.agents.feishuSessionTopic')" value="group_topic" />
<el-option :label="t('aiTools.agents.feishuSessionTopicSender')" value="group_topic_sender" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch :model-value="streaming" @update:model-value="emit('update:streaming', Boolean($event))" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select :model-value="requireMention" @update:model-value="emit('update:requireMention', $event)">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option v-if="allowInherit" :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
defineProps<{ streaming: boolean; groupSessionScope?: string; requireMention: string; allowInherit?: boolean }>();
const emit = defineEmits<{
'update:streaming': [value: boolean];
'update:groupSessionScope': [value: string];
'update:requireMention': [value: string];
}>();
</script>
@@ -1,6 +1,13 @@
<template>
<el-form ref="formRef" v-loading="loading || approving" :model="form" :rules="rules" label-position="top">
<el-form
ref="formRef"
v-loading="loading || pluginLoading || approving"
:model="form"
:rules="rules"
label-position="top"
>
<PluginInstall
v-if="!builtinManaged"
:installed="installed"
:installing="installing"
:upgrading="upgrading"
@@ -13,29 +20,55 @@
:uninstall-action="uninstallPlugin"
:on-task-close="reload"
/>
<el-alert
v-else
:closable="false"
:type="!installed || legacy ? 'warning' : 'info'"
:title="t('aiTools.agents.feishuBuiltin')"
:description="
t(
!installed
? 'aiTools.agents.feishuImageRequired'
: legacy
? legacyConfigured
? 'aiTools.agents.feishuLegacy'
: 'aiTools.agents.feishuRecoverConfirm'
: 'aiTools.agents.feishuBuiltinHint',
)
"
/>
<template v-if="installed">
<el-form-item :label="t('commons.table.status')" class="mt-4">
<el-switch v-model="form.enabled" />
<el-switch v-model="form.enabled" :disabled="legacy" />
</el-form-item>
<div>
<el-form-item :label="t('aiTools.agents.threadSession')">
<el-switch v-model="form.threadSession" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch v-model="form.streaming" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.replyMode')" prop="replyMode">
<el-input v-model="form.replyMode" placeholder="auto" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select v-model="form.requireMention">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
<div v-if="!legacy || legacyConfigured">
<FeishuTransition
v-if="builtinManaged"
v-model:streaming="form.streaming"
v-model:group-session-scope="form.groupSessionScope"
v-model:require-mention="form.requireMention"
:allow-inherit="!legacy"
/>
<template v-else>
<el-form-item :label="t('aiTools.agents.threadSession')">
<el-switch v-model="form.threadSession" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch v-model="form.streaming" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.replyMode')" prop="replyMode">
<el-input v-model="form.replyMode" placeholder="auto" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select v-model="form.requireMention">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
</template>
<el-form-item :label="t('aiTools.agents.groupPolicy')" prop="groupPolicy">
<el-select v-model="form.groupPolicy">
<el-select v-model="form.groupPolicy" :disabled="legacy">
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
@@ -48,6 +81,7 @@
>
<el-input
v-model="form.groupAllowFromText"
:disabled="legacy"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.groupAllowFromPlaceholder')"
@@ -63,6 +97,7 @@
</el-form-item>
</div>
<ChannelBots
v-if="!legacy || legacyConfigured"
:bots="form.bots"
:fields="botFields"
:create-bot="createBot"
@@ -71,8 +106,8 @@
unique-field-prop="appId"
unique-field-label="App ID"
:summary-formatter="getBotSummary"
:add-disabled="!installed"
:disabled="!installed"
:add-disabled="legacy"
:disabled="legacy"
approvable
:approve-disabled="isApproveDisabled"
:fixed-account-ids="['default']"
@@ -81,12 +116,18 @@
@save="saveChannel"
@approve="approvePairing"
/>
<el-form-item class="mt-4">
<el-button v-permission type="primary" :loading="saving" :disabled="!installed" @click="saveChannel">
<el-form-item v-if="!legacy" class="mt-4">
<el-button v-permission type="primary" :loading="saving" :disabled="legacy" @click="saveChannel">
{{ t('commons.button.save') }}
</el-button>
</el-form-item>
<el-form-item v-else class="mt-4">
<el-button v-permission type="primary" :loading="switching" @click="switchPlugin">
{{ t(legacyConfigured ? 'aiTools.agents.feishuSwitch' : 'aiTools.agents.feishuRecover') }}
</el-button>
</el-form-item>
</template>
<TaskLog ref="taskLogRef" @close="reload" />
</el-form>
</template>
@@ -96,12 +137,20 @@ import type { FormInstance } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import { approveAgentChannelPairing, getAgentFeishuConfig, updateAgentFeishuConfig } from '@/api/modules/ai';
import {
approveAgentChannelPairing,
getAgentFeishuConfig,
updateAgentFeishuConfig,
switchAgentFeishuPlugin,
} from '@/api/modules/ai';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import TaskLog from '@/components/log/task/index.vue';
import { newUUID } from '@/utils/id';
import PluginInstall from '../components/plugin-install.vue';
import { useAgentPluginChannel } from './useAgentPluginChannel';
import ChannelBots from '../components/channel-bots.vue';
import FeishuTransition from '../components/feishu-transition.vue';
const { t } = useI18n();
@@ -129,10 +178,11 @@ interface FeishuForm extends Omit<AI.AgentFeishuConfig, 'bots'> {
const saving = ref(false);
const approving = ref(false);
const formRef = ref<FormInstance>();
const loading = ref(false);
const {
agentId,
loading,
installed,
loading: pluginLoading,
installed: pluginInstalled,
installing,
upgrading,
uninstalling,
@@ -144,14 +194,22 @@ const {
upgradePlugin,
uninstallPlugin,
} = useAgentPluginChannel('feishu');
const builtinManaged = ref(true);
const builtinAvailable = ref(false);
const installed = computed(() => (builtinManaged.value ? builtinAvailable.value : pluginInstalled.value));
const legacy = ref(false);
const legacyConfigured = ref(false);
const switching = ref(false);
const taskLogRef = ref();
const form = reactive<FeishuForm>({
enabled: true,
threadSession: true,
streaming: false,
replyMode: 'auto',
groupSessionScope: 'group',
requireMention: 'true',
groupPolicy: 'open',
groupPolicy: 'allowlist',
groupAllowFrom: [],
groupAllowFromText: '',
bots: [],
@@ -181,12 +239,17 @@ const parseTextList = (value: string): string[] => {
const rules = reactive({
replyMode: [Rules.requiredInput],
groupSessionScope: [Rules.requiredSelect],
requireMention: [Rules.requiredSelect],
groupPolicy: [Rules.requiredSelect],
groupAllowFromText: [
{
validator: (_rule, value, callback) => {
if (form.groupPolicy === 'allowlist' && parseTextList(String(value || '')).length === 0) {
if (
!legacy.value &&
form.groupPolicy === 'allowlist' &&
parseTextList(String(value || '')).length === 0
) {
callback(new Error(t('aiTools.agents.allowFromRequired')));
return;
}
@@ -250,20 +313,33 @@ const updateBots = (bots: FeishuBotForm[]) => {
form.bots = bots;
};
const isApproveDisabled = (bot: FeishuBotForm) => bot.dmPolicy !== 'pairing';
const isApproveDisabled = (bot: FeishuBotForm) => legacy.value || bot.dmPolicy !== 'pairing';
const load = async (id: number) => {
await loadPlugin(id);
const res = await getAgentFeishuConfig({ agentId: id });
form.enabled = res.data?.enabled ?? true;
form.threadSession = res.data?.threadSession ?? true;
form.streaming = res.data?.streaming ?? false;
form.replyMode = res.data?.replyMode || 'auto';
form.requireMention = res.data?.requireMention || 'true';
form.groupPolicy = res.data?.groupPolicy || 'open';
form.groupAllowFrom = res.data?.groupAllowFrom || [];
form.groupAllowFromText = (res.data?.groupAllowFrom || []).join('\n');
form.bots = (res.data?.bots || []).map((bot) => normalizeBot(bot));
agentId.value = id;
loading.value = true;
try {
const res = await getAgentFeishuConfig({ agentId: id });
builtinManaged.value = Boolean(res.data?.builtinManaged);
builtinAvailable.value = Boolean(res.data?.builtinAvailable);
legacy.value = builtinManaged.value && Boolean(res.data?.legacy);
legacyConfigured.value = Boolean(res.data?.legacyConfigured);
if (!builtinManaged.value) {
await loadPlugin(id);
}
form.groupSessionScope = res.data?.groupSessionScope || 'group';
form.enabled = res.data?.enabled ?? true;
form.threadSession = res.data?.threadSession ?? true;
form.streaming = res.data?.streaming ?? false;
form.replyMode = res.data?.replyMode || 'auto';
form.requireMention = res.data?.requireMention || 'true';
form.groupPolicy = res.data?.groupPolicy || (builtinManaged.value ? 'allowlist' : 'open');
form.groupAllowFrom = res.data?.groupAllowFrom || [];
form.groupAllowFromText = (res.data?.groupAllowFrom || []).join('\n');
form.bots = (res.data?.bots || []).map((bot) => normalizeBot(bot));
} finally {
loading.value = false;
}
};
const reload = async () => {
@@ -314,6 +390,7 @@ const saveChannel = async (action: 'delete' | 'save' = 'save') => {
agentId: agentId.value,
enabled: form.enabled,
threadSession: form.threadSession,
...(builtinManaged.value ? { groupSessionScope: form.groupSessionScope } : {}),
streaming: form.streaming,
replyMode: form.replyMode,
requireMention: form.requireMention,
@@ -330,6 +407,40 @@ const saveChannel = async (action: 'delete' | 'save' = 'save') => {
}
};
const switchPlugin = async () => {
if (!agentId.value || switching.value || !formRef.value) return;
if (legacyConfigured.value) await formRef.value.validateField(['groupSessionScope', 'requireMention']);
if (legacyConfigured.value && form.requireMention === 'open') {
MsgWarning(t('aiTools.agents.feishuMentionChoice'));
return;
}
await ElMessageBox.confirm(
legacyConfigured.value
? t('aiTools.agents.feishuSwitchConfirm') + ' ' + t('aiTools.agents.feishuRecoveryNotice')
: t('aiTools.agents.feishuRecoverConfirm'),
t('aiTools.agents.feishuSwitch'),
{
type: 'warning',
confirmButtonText: t('commons.button.confirm'),
cancelButtonText: t('commons.button.cancel'),
},
);
switching.value = true;
try {
const taskID = newUUID();
await switchAgentFeishuPlugin({
agentId: agentId.value,
taskID,
streaming: form.streaming,
groupSessionScope: form.groupSessionScope || 'group',
requireMention: legacyConfigured.value && form.requireMention === 'false' ? 'false' : 'true',
});
taskLogRef.value?.openWithTaskID(taskID);
} finally {
switching.value = false;
}
};
const approvePairing = async (bot: FeishuBotForm) => {
if (!agentId.value) {
return;
@@ -55,8 +55,22 @@
<div>2. {{ $t('app.openclawHttpsUpgradeNoticeItem2') }}</div>
</div>
</el-alert>
<template v-if="feishuLegacy && operateReq.operate === 'upgrade'">
<el-alert
class="mb-4"
type="warning"
:closable="false"
:title="$t('aiTools.agents.feishuUpgradeNotice')"
/>
<FeishuTransition
v-if="feishuConfigured"
v-model:streaming="feishuTransition.streaming"
v-model:group-session-scope="feishuTransition.groupSessionScope"
v-model:require-mention="feishuTransition.requireMention"
/>
</template>
<el-form-item prop="backup" v-if="operateReq.operate === 'upgrade'">
<el-checkbox v-model="operateReq.backup" :label="$t('app.backupApp')" />
<el-checkbox v-model="operateReq.backup" :disabled="feishuLegacy" :label="$t('app.backupApp')" />
<span class="input-help">
<el-text type="warning">{{ $t('app.backupAppHelper') }}</el-text>
</span>
@@ -100,6 +114,8 @@ import TaskLog from '@/components/log/task/index.vue';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import { App } from '@/api/interface/app';
import { AI } from '@/api/interface/ai';
import FeishuTransition from '@/views/ai/agents/agent/config/tabs/channels/components/feishu-transition.vue';
import { getAppUpdateVersions, ignoreUpgrade, installedOp } from '@/api/modules/app';
import { getAppStoreConfig } from '@/api/modules/setting';
import { isOpenclawCurrentHTTPVersion, isOpenclawHTTPSWindowVersion } from '@/utils/agent';
@@ -128,6 +144,15 @@ const operateReq = reactive({
version: '',
dockerCompose: '',
taskID: '',
feishuTransition: undefined as AI.AgentFeishuTransition | undefined,
});
const feishuLegacy = ref(false);
const feishuConfigured = ref(false);
const feishuTransition = reactive<AI.AgentFeishuTransition>({
confirmed: false,
streaming: false,
groupSessionScope: 'group',
requireMention: 'true',
});
const resourceName = ref('');
const rules = ref<any>({
@@ -196,13 +221,22 @@ const initData = async () => {
operateReq.dockerCompose = '';
};
const acceptParams = (appInstall: App.AppInstallDto, op: string, opNode?: string) => {
const acceptParams = async (appInstall: App.AppInstallDto, op: string, opNode?: string) => {
if (opNode) {
node.value = opNode;
} else {
node.value = currentNode.value;
}
initData();
feishuLegacy.value = false;
feishuConfigured.value = false;
operateReq.feishuTransition = undefined;
Object.assign(feishuTransition, {
confirmed: false,
streaming: false,
groupSessionScope: 'group',
requireMention: 'true',
});
await initData();
isEdit.value = appInstall.isEdit;
currentVersion.value = appInstall.version;
currentAppKey.value = appInstall.appKey;
@@ -235,6 +269,9 @@ const getVersions = async (version: string) => {
if (version != '') {
item = res.data.find((v) => v.version === version);
}
feishuLegacy.value = Boolean(item.feishuLegacy);
feishuConfigured.value = Boolean(item.feishuConfigured);
if (feishuLegacy.value) operateReq.backup = true;
operateReq.detailId = item.detailId;
operateReq.version = item.version;
newContent.value = item.dockerCompose;
@@ -279,15 +316,20 @@ const operate = async () => {
};
const onOperate = async () => {
ElMessageBox.confirm(
i18n.global.t('app.operatorHelper', [i18n.global.t('commons.button.' + operateReq.operate)]),
i18n.global.t('commons.button.upgrade'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(() => {
await updateRef.value?.validate();
const helper = i18n.global.t('app.operatorHelper', [i18n.global.t('commons.button.' + operateReq.operate)]);
const notice =
feishuLegacy.value && operateReq.operate === 'upgrade'
? i18n.global.t('aiTools.agents.feishuUpgradeNotice') +
(feishuConfigured.value ? ' ' + i18n.global.t('aiTools.agents.feishuSwitchConfirm') : '')
: helper;
ElMessageBox.confirm(notice, i18n.global.t('commons.button.upgrade'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(() => {
operateReq.feishuTransition =
feishuLegacy.value && feishuConfigured.value ? { ...feishuTransition, confirmed: true } : undefined;
operate();
});
};
@@ -31,6 +31,34 @@
<el-button v-permission type="primary" plain @click="onClean()">
{{ $t('container.containerPrune') }}
</el-button>
<el-dropdown v-permission trigger="click" @command="onImageCheckUpdateCommand">
<el-button type="primary" plain :loading="imageCheckUpdateBusy">
{{ $t('container.imageCheckUpdates') }}
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="check"
:disabled="imageCheckUpdateBusy || imageCheckUpdateStatus?.running"
>
{{ $t('container.imageCheckUpdateNow') }}
</el-dropdown-item>
<el-dropdown-item
command="schedule"
:disabled="imageCheckUpdateBusy || !imageCheckUpdateStatus"
>
{{
$t(
imageCheckUpdateStatus?.enabled
? 'container.imageCheckUpdateDisable'
: 'container.imageCheckUpdateSchedule',
)
}}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #rightToolBar>
<TableViewSwitch v-model="viewMode" storage-key="container" />
@@ -471,8 +499,13 @@ import Uploads from '@/components/upload/index.vue';
import DockerStatus from '@/views/container/docker-status/index.vue';
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
import Status from '@/components/status/index.vue';
import { toRef, computed, onBeforeUnmount, onDeactivated, reactive, ref } from 'vue';
import { toRef, computed, onMounted, onActivated, onBeforeUnmount, onDeactivated, reactive, ref, watch } from 'vue';
import { ArrowDown } from '@element-plus/icons-vue';
import {
getContainerImageCheckUpdateStatus,
runContainerImageCheckUpdate,
setContainerImageCheckUpdateSchedule,
type ContainerImageCheckUpdateStatus,
containerItemStats,
containerListStats,
containerOperator,
@@ -776,6 +809,105 @@ const loadContainers = async (background = false, column?: any) => {
const search = (column?: any) => loadContainers(false, column);
const imageCheckUpdateStatus = ref<ContainerImageCheckUpdateStatus>();
const imageCheckUpdateBusy = ref(false);
let imageCheckUpdateTimer: ReturnType<typeof setTimeout> | undefined;
let imageCheckUpdateGeneration = 0;
let imageCheckUpdateActive = false;
let imageCheckUpdateController: AbortController | undefined;
const pauseImageCheckUpdatePolling = () => {
imageCheckUpdateGeneration++;
clearTimeout(imageCheckUpdateTimer);
imageCheckUpdateController?.abort();
imageCheckUpdateController = undefined;
};
const stopImageCheckUpdatePolling = () => {
imageCheckUpdateActive = false;
pauseImageCheckUpdatePolling();
};
const showImageCheckUpdateResult = (status: ContainerImageCheckUpdateStatus) => {
if (status.error) MsgWarning(status.error);
else {
const summary = i18n.global.t('container.imageCheckUpdateResult', { ...status });
if (status.failed) MsgWarning(summary);
else MsgSuccess(summary);
}
};
const refreshImageCheckUpdateStatus = async (generation: number) => {
if (!imageCheckUpdateActive || document.hidden || generation !== imageCheckUpdateGeneration) return;
const node = currentNode.value;
const controller = new AbortController();
imageCheckUpdateController = controller;
try {
const res = await getContainerImageCheckUpdateStatus(node, controller.signal);
if (generation !== imageCheckUpdateGeneration || node !== currentNode.value) return;
const previous = imageCheckUpdateStatus.value;
imageCheckUpdateStatus.value = res.data;
if (previous && res.data.finishedAt && previous.finishedAt !== res.data.finishedAt) {
await loadContainers();
if (generation !== imageCheckUpdateGeneration || node !== currentNode.value) return;
showImageCheckUpdateResult(res.data);
}
} catch {
// The HTTP layer displays errors. Retry on the next status poll.
} finally {
if (imageCheckUpdateController === controller) imageCheckUpdateController = undefined;
if (generation === imageCheckUpdateGeneration && imageCheckUpdateActive && !document.hidden) {
const interval = imageCheckUpdateStatus.value?.running ? 10000 : 60000;
imageCheckUpdateTimer = setTimeout(() => refreshImageCheckUpdateStatus(generation), interval);
}
}
};
const restartImageCheckUpdatePolling = () => {
pauseImageCheckUpdatePolling();
if (imageCheckUpdateActive && !document.hidden) void refreshImageCheckUpdateStatus(imageCheckUpdateGeneration);
};
const startImageCheckUpdatePolling = () => {
if (imageCheckUpdateActive) return;
imageCheckUpdateActive = true;
restartImageCheckUpdatePolling();
};
const onImageCheckUpdateCommand = async (command: string) => {
const node = currentNode.value;
const generation = imageCheckUpdateGeneration;
imageCheckUpdateBusy.value = true;
try {
const res =
command === 'check'
? await runContainerImageCheckUpdate(node)
: await setContainerImageCheckUpdateSchedule(!imageCheckUpdateStatus.value?.enabled, node);
if (generation !== imageCheckUpdateGeneration || node !== currentNode.value) return;
imageCheckUpdateStatus.value = res.data;
if (command === 'check' && res.data.taskID) openTaskLog(res.data.taskID);
if (command === 'check' && !res.data.running) {
showImageCheckUpdateResult(res.data);
await loadContainers();
} else {
MsgSuccess(
i18n.global.t(
command === 'check' ? 'container.imageCheckUpdateStarted' : 'commons.msg.operationSuccess',
),
);
}
if (generation === imageCheckUpdateGeneration && node === currentNode.value) restartImageCheckUpdatePolling();
} catch {
// The HTTP layer reports failures; keep the last confirmed schedule state.
} finally {
if (node === currentNode.value) imageCheckUpdateBusy.value = false;
}
};
onMounted(startImageCheckUpdatePolling);
onMounted(() => document.addEventListener('visibilitychange', restartImageCheckUpdatePolling));
onBeforeUnmount(() => document.removeEventListener('visibilitychange', restartImageCheckUpdatePolling));
onActivated(startImageCheckUpdatePolling);
onBeforeUnmount(stopImageCheckUpdatePolling);
onDeactivated(stopImageCheckUpdatePolling);
watch(currentNode, () => {
imageCheckUpdateStatus.value = undefined;
imageCheckUpdateBusy.value = false;
restartImageCheckUpdatePolling();
});
const searchWithStatus = (item: string) => {
activeTag.value = item;
paginationConfig.state = activeTag.value;
@@ -989,8 +1121,15 @@ const buttons = [
{
label: i18n.global.t('commons.button.upgrade'),
permission: true,
dot: (row: Container.ContainerInfo) => Boolean(row.hasUpdate),
click: (row: Container.ContainerInfo) => {
dialogUpgradeRef.value!.acceptParams({ container: row.name, image: row.imageName, fromApp: row.isFromApp });
dialogUpgradeRef.value!.acceptParams({
container: row.name,
image: row.imageName,
fromApp: row.isFromApp,
targetImage: row.targetImage,
hasUpdate: row.hasUpdate,
});
},
},
{
@@ -99,15 +99,18 @@ interface DialogProps {
container: string;
image: string;
fromApp: boolean;
targetImage?: string;
hasUpdate?: boolean;
}
const acceptParams = (props: DialogProps): void => {
form.names = [props.container];
isIndeterminate.value = true;
form.oldImageName = props.image;
form.fromApp = props.fromApp;
form.forcePull = Boolean(props.hasUpdate);
form.hasName = props.image.indexOf('sha256:') === -1;
if (form.hasName) {
form.newImageName = props.image;
form.newImageName = props.hasUpdate && props.targetImage ? props.targetImage : props.image;
} else {
form.newImageName = '';
}
@@ -50,7 +50,7 @@ const form = reactive({
const rules = reactive({
username: [Rules.requiredInput, Rules.name],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
password: [Rules.requiredInput, Rules.databasePassword],
});
interface DialogProps {
@@ -162,7 +162,7 @@ const form = reactive({
});
const rules = reactive({
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
password: [Rules.requiredInput, Rules.databasePassword],
});
type FormInstance = InstanceType<typeof ElForm>;
@@ -263,6 +263,7 @@
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item :label="$t('database.permission')" prop="permission">
<el-select v-model="createForm.permission" class="w-full">
@@ -418,7 +419,7 @@ const createForm = reactive({
const rules = reactive({
name: [Rules.requiredInput, Rules.dbName],
username: [Rules.requiredInput, Rules.name],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
password: [Rules.requiredInput, Rules.databasePassword],
permission: [Rules.requiredSelect],
});
@@ -49,7 +49,7 @@ const form = reactive({
});
const rules = reactive({
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
password: [Rules.requiredInput, Rules.databasePassword],
});
interface DialogProps {
@@ -38,6 +38,7 @@
show-password
v-model.trim="dialogData.rowData!.password"
/>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item :label="$t('database.timeout')" prop="timeout">
<el-input-number
@@ -122,8 +123,8 @@ const rules = reactive({
version: [Rules.requiredSelect],
address: [Rules.ipV4V6OrDomain],
port: [Rules.port],
username: [Rules.requiredInput],
password: [Rules.requiredInput],
username: [Rules.requiredInput, Rules.databaseUsername],
password: [Rules.requiredInput, Rules.databasePassword],
timeout: [Rules.number, checkNumberRange(1, 600)],
});
@@ -131,6 +131,7 @@
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password" required>
<el-input type="password" clearable show-password v-model.trim="form.password" />
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item :label="$t('database.permission')" prop="permission">
<el-select v-model="form.permission" @change="changePermission">
@@ -214,7 +215,7 @@ const rules = reactive({
username: [Rules.requiredInput, Rules.name],
permission: [Rules.requiredSelect],
host: [Rules.requiredInput, Rules.noSpace, Rules.illegal, { validator: checkMysqlHosts, trigger: 'blur' }],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
password: [Rules.requiredInput, Rules.databasePassword],
});
interface DialogProps {
@@ -161,7 +161,7 @@ const form = reactive({
remoteIP: '',
});
const rules = reactive({
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
password: [Rules.requiredInput, Rules.databasePassword],
});
type FormInstance = InstanceType<typeof ElForm>;
@@ -183,7 +183,7 @@ const rules = reactive({
userMode: [Rules.requiredSelect],
userKey: [Rules.requiredSelect],
username: [Rules.requiredInput, Rules.name],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
password: [Rules.requiredInput, Rules.databasePassword],
permission: [Rules.requiredSelect],
permissionIPs: [Rules.requiredInput, Rules.noSpace, Rules.illegal, { validator: checkMysqlHosts, trigger: 'blur' }],
});
@@ -48,6 +48,7 @@
show-password
v-model.trim="dialogData.rowData!.password"
/>
<span class="input-help">{{ $t('commons.rule.illegalChar') }}</span>
</el-form-item>
<el-form-item>
<el-checkbox @change="isOK = false" v-model="dialogData.rowData!.ssl" :label="$t('database.ssl')" />
@@ -167,8 +168,8 @@ const rules = reactive({
version: [Rules.requiredSelect],
address: [Rules.ipV4V6OrDomain],
port: [Rules.port],
username: [Rules.requiredInput],
password: [Rules.requiredInput],
username: [Rules.requiredInput, Rules.databaseUsername],
password: [Rules.requiredInput, Rules.databasePassword],
timeout: [Rules.number, checkNumberRange(1, 600)],
});

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