Compare commits

..
54 changed files with 575 additions and 678 deletions
-84
View File
@@ -1,84 +0,0 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags Dashboard
// @Summary List quick directories
// @Success 200 {array} model.QuickDirectory
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [get]
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
items, err := dashboardService.ListQuickDirectories()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, items)
}
// @Tags Dashboard
// @Summary Create quick directory
// @Accept json
// @Param request body dto.QuickDirectoryCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory [post]
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.CreateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Update quick directory
// @Accept json
// @Param request body dto.QuickDirectoryUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/update [post]
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
item, err := dashboardService.UpdateQuickDirectory(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, item)
}
// @Tags Dashboard
// @Summary Delete quick directory
// @Accept json
// @Param request body dto.QuickDirectoryDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /dashboard/quick/directory/del [post]
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
var req dto.QuickDirectoryDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+3 -1
View File
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent deepseek-harness octop"`
AccessHost string `json:"accessHost"`
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
}
type AgentItem struct {
AccessHost string `json:"accessHost"`
ID uint `json:"id"`
Name string `json:"name"`
Remark string `json:"remark"`
+1 -21
View File
@@ -31,27 +31,7 @@ type DashboardBase struct {
}
type ChangeQuicks struct {
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
}
type QuickVisibility struct {
ID uint `json:"id" validate:"required"`
IsShow bool `json:"isShow"`
}
type QuickDirectoryCreate struct {
Path string `json:"path" validate:"required"`
Alias string `json:"alias"`
}
type QuickDirectoryDelete struct {
ID uint `json:"id" validate:"required"`
Legacy bool `json:"legacy"`
}
type QuickDirectoryUpdate struct {
QuickDirectoryDelete
QuickDirectoryCreate
Quicks []QuickJump `json:"quicks"`
}
type QuickJump struct {
-7
View File
@@ -5,13 +5,6 @@ type AppLauncher struct {
Key string `json:"key"`
}
type QuickDirectory struct {
BaseModel
Path string `json:"path"`
Alias string `json:"alias"`
Legacy bool `json:"legacy" gorm:"-"`
}
type QuickJump struct {
BaseModel
Name string `json:"name"`
+3 -69
View File
@@ -3,7 +3,6 @@ package repo
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type LauncherRepo struct{}
@@ -18,11 +17,6 @@ type ILauncherRepo interface {
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
ListQuickJump(withAll bool) []model.QuickJump
UpdateQuicks(quicks []model.QuickJump) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CountQuickDirectories() (int64, error)
CreateQuickDirectory(item *model.QuickDirectory) error
UpdateQuickDirectory(item *model.QuickDirectory) error
DeleteQuickDirectory(id uint, legacy bool) error
}
func NewILauncherRepo() ILauncherRepo {
@@ -101,73 +95,13 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
for _, item := range quicks {
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
"is_show": item.IsShow,
"detail": item.Detail,
"alias": item.Alias,
}).Error; err != nil {
tx.Rollback()
return err
}
}
return tx.Commit().Error
}
// Legacy entries remain in the overview configuration until explicitly deleted.
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
}
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
items := make([]model.QuickDirectory, 0)
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
return nil, err
}
var quicks []model.QuickJump
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
return nil, err
}
for _, quick := range quicks {
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
}
return items, nil
}
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
var count, legacyCount int64
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
return 0, err
}
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
return 0, err
}
return count + legacyCount, nil
}
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
return global.DB.Create(item).Error
}
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
var result *gorm.DB
if item.Legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
} else {
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
var result *gorm.DB
if legacy {
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
} else {
result = global.DB.Delete(&model.QuickDirectory{}, id)
}
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
tx.Commit()
return nil
}
+12 -1
View File
@@ -142,6 +142,9 @@ const (
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
agentType := req.AgentType
if err := validateNativeAgentCreate(req); err != nil {
return nil, err
}
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
@@ -237,6 +240,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
}
} else if agentType == constant.AppCopaw {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
}
params := map[string]interface{}{
@@ -245,6 +250,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
constant.HostIP: "",
}
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
if agentType == constant.AppDeepSeekHarness {
params["HTTPS_ACCESS_HOST"] = req.AccessHost
}
if agentType == constant.AppOctop {
params["OCTOP_LOG_LEVEL"] = "info"
}
if agentType == constant.AppOpenclaw {
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
params["ALLOWED_ORIGIN"] = allowedOrigin
@@ -738,7 +749,7 @@ func requiresAgentAccount(agentType string) bool {
}
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
params["PANEL_APP_PORT_HTTPS"] = webUIPort
return
}
+69
View File
@@ -0,0 +1,69 @@
package service
import (
"net"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
)
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
func isDeepSeekAccessHost(value string) bool {
if ip := net.ParseIP(value); ip != nil {
return ip.To4() != nil && !strings.Contains(value, ":")
}
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
return false
}
for _, label := range strings.Split(value, ".") {
if !deepSeekHostLabelPattern.MatchString(label) {
return false
}
}
return true
}
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
return nil
}
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
}
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
if req.AgentType == constant.AppDeepSeekHarness {
if !isDeepSeekAccessHost(req.AccessHost) {
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
}
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
}
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
return nil
}
hasLetter, hasDigit := false, false
for _, char := range req.DashboardPassword {
hasLetter = hasLetter || unicode.IsLetter(char)
hasDigit = hasDigit || unicode.IsDigit(char)
}
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
switch strings.ToLower(req.DashboardPassword) {
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
}
return nil
}
+15 -1
View File
@@ -415,6 +415,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
} else {
item.WebUIPort = appInstall.HttpPort
}
if agentType == constant.AppDeepSeekHarness {
item.WebUIPort = appInstall.HttpsPort
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
accessHost := values["HTTPS_ACCESS_HOST"]
if isDeepSeekAccessHost(accessHost) {
item.AccessHost = accessHost
}
}
}
item.Path = appInstall.GetPath()
item.Status = appInstall.Status
item.Message = appInstall.Message
@@ -442,7 +451,8 @@ func localizedAgentProviderName(provider string) string {
}
func isAgentAppKey(appKey string) bool {
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
}
func isOpenclawLegacyHTTPVersion(version string) bool {
@@ -1641,6 +1651,10 @@ func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
case constant.AppCopaw:
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
case constant.AppDeepSeekHarness:
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
case constant.AppOctop:
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", true
default:
return "", "", false
}
+6 -16
View File
@@ -43,10 +43,6 @@ type IDashboardService interface {
LoadQuickOptions() []dto.QuickJump
ChangeQuick(req dto.ChangeQuicks) error
ListQuickDirectories() ([]model.QuickDirectory, error)
CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error)
UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error)
DeleteQuickDirectory(req dto.QuickDirectoryDelete) error
LoadAppLauncher(ctx *gin.Context) ([]dto.AppLauncher, error)
ChangeShow(req dto.SettingUpdate) error
@@ -401,15 +397,16 @@ func (u *DashboardService) LoadQuickOptions() []dto.QuickJump {
}
func (u *DashboardService) ChangeQuick(req dto.ChangeQuicks) error {
showCount := 0
quicks := make([]model.QuickJump, 0, len(req.Quicks))
var quicks []model.QuickJump
for _, item := range req.Quicks {
var quick model.QuickJump
if item.IsShow {
showCount++
}
quicks = append(quicks, model.QuickJump{
BaseModel: model.BaseModel{ID: item.ID},
IsShow: item.IsShow,
})
if err := copier.Copy(&quick, &item); err != nil {
return err
}
quicks = append(quicks, quick)
}
if showCount == 0 {
return buserr.New("ErrMinQuickJump")
@@ -709,13 +706,6 @@ func loadQuickJump(base *dto.DashboardBase) {
quicks := launcherRepo.ListQuickJump(false)
for i := 0; i < len(quicks); i++ {
switch quicks[i].Name {
case "File":
count, err := launcherRepo.CountQuickDirectories()
if err != nil {
global.LOG.Errorf("load quick directory count: %v", err)
}
quicks[i].Detail = fmt.Sprintf("%d", count)
quicks[i].Alias = ""
case "Agent":
quicks[i].Detail = fmt.Sprintf("%d", base.AgentNumber)
case "Website":
-49
View File
@@ -1,49 +0,0 @@
package service
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
)
func (u *DashboardService) ListQuickDirectories() ([]model.QuickDirectory, error) {
return launcherRepo.ListQuickDirectories()
}
func quickDirectoryValues(req dto.QuickDirectoryCreate) (model.QuickDirectory, error) {
item := model.QuickDirectory{Path: req.Path, Alias: strings.TrimSpace(req.Alias)}
if strings.TrimSpace(item.Path) == "" {
return item, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
}
return item, nil
}
func (u *DashboardService) CreateQuickDirectory(req dto.QuickDirectoryCreate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req)
if err != nil {
return nil, err
}
if err := launcherRepo.CreateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) UpdateQuickDirectory(req dto.QuickDirectoryUpdate) (*model.QuickDirectory, error) {
item, err := quickDirectoryValues(req.QuickDirectoryCreate)
if err != nil {
return nil, err
}
item.ID = req.ID
item.Legacy = req.Legacy
if err := launcherRepo.UpdateQuickDirectory(&item); err != nil {
return nil, err
}
return &item, nil
}
func (u *DashboardService) DeleteQuickDirectory(req dto.QuickDirectoryDelete) error {
return launcherRepo.DeleteQuickDirectory(req.ID, req.Legacy)
}
+2
View File
@@ -10,6 +10,8 @@ const (
AppOpenclaw = "openclaw"
AppCopaw = "copaw"
AppHermesAgent = "hermes-agent"
AppDeepSeekHarness = "deepseek-harness"
AppOctop = "octop"
AppMysql = "mysql"
AppMariaDB = "mariadb"
AppPostgresql = "postgresql"
+1 -1
View File
@@ -115,7 +115,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddMonitorSettings,
migrations.AddVLLMRequestMetrics,
migrations.AddVLLMSampleInterval,
migrations.AddQuickDirectory,
migrations.AddNativeAgentInstalls,
}
}
@@ -0,0 +1,47 @@
package migrations
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var AddNativeAgentInstalls = &gormigrate.Migration{
ID: "20261020-add-deepseek-harness-octop-agent-installs",
Migrate: func(tx *gorm.DB) error {
var installs []struct {
model.AppInstall
AgentType string
}
if err := tx.Table("app_installs").
Select("app_installs.*, apps.key AS agent_type").
Joins("JOIN apps ON apps.id = app_installs.app_id").
Where("apps.key IN ?", []string{constant.AppDeepSeekHarness, constant.AppOctop}).
Where("NOT EXISTS (SELECT 1 FROM agents WHERE agents.app_install_id = app_installs.id)").
Order("app_installs.id").
Scan(&installs).Error; err != nil {
return err
}
for _, install := range installs {
var count int64
if err := tx.Model(&model.Agent{}).Where("LOWER(name) = LOWER(?)", install.Name).Count(&count).Error; err != nil {
return err
}
if count > 0 {
global.LOG.Warnf("skip agent registration for app install %d: name already exists", install.ID)
continue
}
agent := model.Agent{
BaseModel: model.BaseModel{CreatedAt: install.CreatedAt, UpdatedAt: install.UpdatedAt},
Name: install.Name, AgentType: install.AgentType, AppInstallID: install.ID,
Status: install.Status, Message: install.Message,
}
if err := tx.Create(&agent).Error; err != nil {
return err
}
}
return nil
},
}
-7
View File
@@ -1888,10 +1888,3 @@ var AddVLLMSampleInterval = &gormigrate.Migration{
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddQuickDirectory = &gormigrate.Migration{
ID: "20261016-add-quick-directory",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.QuickDirectory{})
},
}
-4
View File
@@ -14,10 +14,6 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
cmdRouter.GET("/quick/option", baseApi.LoadQuickOption)
cmdRouter.POST("/quick/change", baseApi.UpdateQuickJump)
cmdRouter.GET("/quick/directory", baseApi.ListQuickDirectories)
cmdRouter.POST("/quick/directory", baseApi.CreateQuickDirectory)
cmdRouter.POST("/quick/directory/update", baseApi.UpdateQuickDirectory)
cmdRouter.POST("/quick/directory/del", baseApi.DeleteQuickDirectory)
cmdRouter.GET("/app/launcher", baseApi.LoadAppLauncher)
cmdRouter.POST("/app/launcher/show", baseApi.UpdateAppLauncher)
cmdRouter.POST("/app/launcher/option", baseApi.LoadAppLauncherOption)
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "An unrevoked API key with this name already exists"
ErrAPIKeyNotFound: "The API key does not exist or does not belong to the current user"
ErrAPIKeyAppBindingDisabled: "APP QR binding is not allowed for this API key"
ErrAPIKeyTerminalUpgradeRequired: "Unable to verify API key terminal authorization support. Check the node connection and upgrade its Agent before retrying."
LocalAppSyncDispatch: "Dispatch local app sync"
LocalAppSyncDispatchNode: "Dispatch local app sync to {{.node}} (node task: {{.taskID}})"
ErrLocalAppSyncDisabled: "Local app sync across nodes is disabled"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Ya existe una clave API no revocada con este nombre"
ErrAPIKeyNotFound: "La clave API no existe o no pertenece al usuario actual"
ErrAPIKeyAppBindingDisabled: "Esta clave API no permite vincular la APP mediante código QR"
ErrAPIKeyTerminalUpgradeRequired: "No se pudo verificar la autorización de terminal con clave API. Compruebe la conexión del nodo y actualice su Agent antes de reintentar."
LocalAppSyncDispatch: "Enviar sincronización de apps locales"
LocalAppSyncDispatchNode: "Enviar sincronización de apps locales a {{.node}} (tarea del nodo: {{.taskID}})"
ErrLocalAppSyncDisabled: "La sincronización de apps locales entre nodos está desactivada"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "یک کلید API لغونشده با این نام از
ErrAPIKeyNotFound: "کلید API وجود ندارد یا متعلق به کاربر فعلی نیست"
ErrAPIKeyAppBindingDisabled: "اتصال برنامه با کد QR برای این کلید API مجاز نیست"
ErrAPIKeyTerminalUpgradeRequired: "امکان بررسی پشتیبانی از مجوزدهی ترمینال با کلید API وجود ندارد. اتصال گره را بررسی و Agent آن را ارتقا دهید، سپس دوباره تلاش کنید."
LocalAppSyncDispatch: "ارسال درخواست همگام‌سازی برنامه‌های محلی"
LocalAppSyncDispatchNode: "ارسال درخواست همگام‌سازی به {{.node}} (وظیفه گره: {{.taskID}})"
ErrLocalAppSyncDisabled: "همگام‌سازی برنامه‌های محلی بین گره‌ها غیرفعال است"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "同じ名前の未失効 API キーが既に存在しま
ErrAPIKeyNotFound: "API キーが存在しないか、現在のユーザーに属していません"
ErrAPIKeyAppBindingDisabled: "この API キーでは APP の QR コード連携が許可されていません"
ErrAPIKeyTerminalUpgradeRequired: "API キーによるターミナル認証への対応を確認できません。ノードの接続を確認し、Agent を更新して再試行してください。"
LocalAppSyncDispatch: "ローカルアプリ同期要求の送信"
LocalAppSyncDispatchNode: "{{.node}} にローカルアプリ同期要求を送信(ノードタスク: {{.taskID}})"
ErrLocalAppSyncDisabled: "ノード間のローカルアプリ同期は無効です"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "같은 이름의 폐기되지 않은 API Key가 이미 있
ErrAPIKeyNotFound: "API Key가 없거나 현재 사용자 소유가 아닙니다"
ErrAPIKeyAppBindingDisabled: "이 API Key는 APP QR 코드 연결을 허용하지 않습니다"
ErrAPIKeyTerminalUpgradeRequired: "API Key 터미널 인증 지원 여부를 확인할 수 없습니다. 노드 연결을 확인하고 Agent를 업그레이드한 후 다시 시도하세요."
LocalAppSyncDispatch: "로컬 앱 동기화 요청 전송"
LocalAppSyncDispatchNode: "{{.node}}에 로컬 앱 동기화 요청 전송 (노드 작업: {{.taskID}})"
ErrLocalAppSyncDisabled: "노드 간 로컬 앱 동기화가 비활성화되어 있습니다"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "ມີ API Key ທີ່ຍັງບໍ່ຖືກຖອ
ErrAPIKeyNotFound: "ບໍ່ພົບ API Key ຫຼືບໍ່ແມ່ນຂອງຜູ້ໃຊ້ປັດຈຸບັນ"
ErrAPIKeyAppBindingDisabled: "API Key ນີ້ບໍ່ອະນຸຍາດໃຫ້ຜູກ APP ຜ່ານລະຫັດ QR"
ErrAPIKeyTerminalUpgradeRequired: "ບໍ່ສາມາດຢືນຢັນການຮອງຮັບການອະນຸຍາດເທີມິນອນດ້ວຍ API Key. ກວດສອບການເຊື່ອມຕໍ່ໂນດ ແລະອັບເກຣດ Agent ກ່ອນລອງອີກ."
LocalAppSyncDispatch: "ສົ່ງຄຳຂໍຊິງຄ໌ແອັບໃນເຄື່ອງ"
LocalAppSyncDispatchNode: "ສົ່ງຄຳຂໍຊິງຄ໌ໄປຫາ {{.node}} (ວຽກຂອງໂນດ: {{.taskID}})"
ErrLocalAppSyncDisabled: "ການຊິງຄ໌ແອັບລະຫວ່າງໂນດຍັງບໍ່ໄດ້ເປີດ"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Kunci API yang belum dibatalkan dengan nama ini sudah wuju
ErrAPIKeyNotFound: "Kunci API tidak wujud atau bukan milik pengguna semasa"
ErrAPIKeyAppBindingDisabled: "Pemautan APP melalui kod QR tidak dibenarkan untuk kunci API ini"
ErrAPIKeyTerminalUpgradeRequired: "Sokongan pengesahan terminal melalui kunci API tidak dapat disahkan. Semak sambungan nod dan naik taraf Agent sebelum mencuba lagi."
LocalAppSyncDispatch: "Hantar penyegerakan aplikasi tempatan"
LocalAppSyncDispatchNode: "Hantar penyegerakan aplikasi tempatan ke {{.node}} (tugas nod: {{.taskID}})"
ErrLocalAppSyncDisabled: "Penyegerakan aplikasi tempatan antara nod dinyahdayakan"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Já existe uma chave API não revogada com esse nome"
ErrAPIKeyNotFound: "A chave API não existe ou não pertence ao usuário atual"
ErrAPIKeyAppBindingDisabled: "Esta chave API não permite vincular o APP por código QR"
ErrAPIKeyTerminalUpgradeRequired: "Não foi possível verificar o suporte à autorização de terminal por chave API. Verifique a conexão do nó e atualize o Agent antes de tentar novamente."
LocalAppSyncDispatch: "Enviar sincronização de apps locais"
LocalAppSyncDispatchNode: "Enviar sincronização de apps locais para {{.node}} (tarefa do nó: {{.taskID}})"
ErrLocalAppSyncDisabled: "A sincronização de apps locais entre nós está desativada"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Неотозванный ключ API с таким име
ErrAPIKeyNotFound: "Ключ API не существует или не принадлежит текущему пользователю"
ErrAPIKeyAppBindingDisabled: "Для этого ключа API привязка приложения через QR-код запрещена"
ErrAPIKeyTerminalUpgradeRequired: "Не удалось проверить поддержку авторизации терминала по ключу API. Проверьте подключение узла, обновите его Agent и повторите попытку."
LocalAppSyncDispatch: "Отправка запросов синхронизации локальных приложений"
LocalAppSyncDispatchNode: "Отправка запроса синхронизации узлу {{.node}} (задача узла: {{.taskID}})"
ErrLocalAppSyncDisabled: "Синхронизация локальных приложений между узлами отключена"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Bu adda iptal edilmemiş bir API anahtarı zaten var"
ErrAPIKeyNotFound: "API anahtarı mevcut değil veya geçerli kullanıcıya ait değil"
ErrAPIKeyAppBindingDisabled: "Bu API anahtarı için QR koduyla APP bağlantısına izin verilmiyor"
ErrAPIKeyTerminalUpgradeRequired: "API anahtarıyla terminal yetkilendirme desteği doğrulanamadı. Düğüm bağlantısını kontrol edip Agent sürümünü yükselttikten sonra tekrar deneyin."
LocalAppSyncDispatch: "Yerel uygulama eşitleme isteği gönder"
LocalAppSyncDispatchNode: "{{.node}} düğümüne yerel uygulama eşitleme isteği gönder (düğüm görevi: {{.taskID}})"
ErrLocalAppSyncDisabled: "Düğümler arası yerel uygulama eşitleme devre dışı"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名且未撤銷的 API Key"
ErrAPIKeyNotFound: "API Key 不存在或不屬於目前使用者"
ErrAPIKeyAppBindingDisabled: "此 API Key 未允許 APP 掃碼綁定"
ErrAPIKeyTerminalUpgradeRequired: "無法確認節點支援 API Key 終端授權,請檢查節點連線並升級 Agent 後重試。"
LocalAppSyncDispatch: "下發本機應用同步"
LocalAppSyncDispatchNode: "向節點 {{.node}} 下發本機應用同步(節點任務:{{.taskID}})"
ErrLocalAppSyncDisabled: "尚未啟用本機應用跨節點同步"
+3
View File
@@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名的未撤销 API Key"
ErrAPIKeyNotFound: "API Key 不存在或不属于当前用户"
ErrAPIKeyAppBindingDisabled: "此 API Key 未允许 APP 扫码绑定"
ErrAPIKeyTerminalUpgradeRequired: "无法确认节点支持 API Key 终端授权,请检查节点连接并升级 Agent 后重试。"
LocalAppSyncDispatch: "下发本地应用同步"
LocalAppSyncDispatchNode: "向节点 {{.node}} 下发本地应用同步(节点任务:{{.taskID}})"
ErrLocalAppSyncDisabled: "尚未开启本地应用跨节点同步"
+3 -1
View File
@@ -1,7 +1,7 @@
import { ReqPage } from '.';
export namespace AI {
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent';
export type AgentType = 'openclaw' | 'copaw' | 'hermes-agent' | 'deepseek-harness' | 'octop';
export interface OllamaModelInfo {
id: number;
@@ -423,6 +423,7 @@ export namespace AI {
}
export interface AgentCreateReq {
accessHost?: string;
name: string;
remark: string;
appVersion: string;
@@ -450,6 +451,7 @@ export namespace AI {
}
export interface AgentItem {
accessHost: string;
id: number;
name: string;
remark: string;
+3
View File
@@ -1,6 +1,9 @@
import { ReqPage, CommonModel } from '.';
export namespace App {
export interface LocalAppSyncConfig {
enabled: boolean;
}
export interface App extends CommonModel {
name: string;
icon?: string;
-6
View File
@@ -8,12 +8,6 @@ export namespace Dashboard {
diskSize: number;
}
export interface QuickDirectory {
id: number;
legacy?: boolean;
path: string;
alias: string;
}
export interface QuickJump {
id: number;
name: string;
+12
View File
@@ -144,3 +144,15 @@ export function getAppIconUrl(appKey: string, node?: string): string {
export const installAppToNodes = (param: App.InstallAppToNodes) => {
return http.post(`/core/xpack/sync/app/install`, param);
};
export const getLocalAppSyncConfig = () => {
return http.get<App.LocalAppSyncConfig>('/core/xpack/settings/app/local-sync');
};
export const updateLocalAppSyncConfig = (config: App.LocalAppSyncConfig) => {
return http.post('/core/xpack/settings/app/local-sync', config);
};
export const dispatchLocalAppSync = (req: App.AppStoreSync) => {
return http.post('/core/xpack/settings/app/local-sync/dispatch', req);
};
+1 -9
View File
@@ -8,7 +8,7 @@ export const loadOsInfo = () => {
export const loadQuickOption = () => {
return http.get<Array<Dashboard.QuickJump>>(`/dashboard/quick/option`);
};
export const changeQuick = (quicks: Array<Pick<Dashboard.QuickJump, 'id' | 'isShow'>>) => {
export const changeQuick = (quicks: Array<Dashboard.QuickJump>) => {
return http.post(`/dashboard/quick/change`, { quicks: quicks });
};
export const loadAppLauncher = () => {
@@ -48,11 +48,3 @@ export const loadTopMem = () => {
export const systemRestart = (operation: string) => {
return http.post(`/dashboard/system/restart/${operation}`);
};
export const loadQuickDirectories = () => http.get<Dashboard.QuickDirectory[]>('/dashboard/quick/directory');
export const createQuickDirectory = (data: Omit<Dashboard.QuickDirectory, 'id'>) =>
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory', data);
export const updateQuickDirectory = (data: Dashboard.QuickDirectory) =>
http.post<Dashboard.QuickDirectory>('/dashboard/quick/directory/update', data);
export const deleteQuickDirectory = (id: number, legacy = false) =>
http.post('/dashboard/quick/directory/del', { id, legacy });
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

+1 -2
View File
@@ -196,7 +196,7 @@ const favorites = ref<File.Favorite[]>([]);
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchableForSelect(paths);
const oldUrl = ref<string>('');
const em = defineEmits(['choose', 'close']);
const em = defineEmits(['choose']);
const form = reactive({
path: '/',
@@ -245,7 +245,6 @@ const handleClose = () => {
open.value = false;
selectRow.value = { path: '', name: '' };
selectRows.value = [];
em('close');
};
const openPage = () => {
+10 -1
View File
@@ -494,7 +494,6 @@ const message = {
recommend: 'Recommended',
dir: 'Directory',
alias: 'Alias',
quickDirDelete: 'Delete ({0})?',
quickDir: 'Quick Access',
minQuickJump: 'Add at least one quick access entry',
maxQuickJump: 'You can add up to four quick access entries',
@@ -774,6 +773,15 @@ const message = {
},
aiTools: {
agents: {
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:
'Username: letters, numbers, dot, underscore or hyphen. Password: at least 12 characters, without leading/trailing spaces or line breaks.',
octopAuthHelper:
'Password: at least 8 characters with letters and digits; avoid common passwords. If rejected during initialization, check the application logs for setup instructions.',
initialCredentials: "Initial credentials; use the application's current password if changed.",
nativeAgentAccessUnavailable:
'The existing installation has no compatible HTTPS access address or port. Check its application settings.',
agent: 'Agent',
account: 'Model Account',
batchOperation: 'Batch Operation',
@@ -3693,6 +3701,7 @@ const message = {
link: 'Link',
showCurrentArch: 'Architecture',
syncLocalApp: 'Sync Local App',
localAppSyncToNodes: 'Include child nodes when syncing local apps',
memoryRequiredHelper: 'Current application memory requirement {0}',
gpuConfig: 'Enable GPU Support',
gpuConfigHelper:
+10 -1
View File
@@ -493,7 +493,6 @@ const message = {
home: {
recommend: 'Recomendado',
dir: 'Directorio',
quickDirDelete: '¿Confirmar la eliminación de ({0})?',
quickDir: 'Acceso rápido',
minQuickJump: 'Agrega al menos una entrada de acceso rápido',
maxQuickJump: 'Puedes agregar hasta cuatro entradas de acceso rápido',
@@ -777,6 +776,15 @@ const message = {
},
aiTools: {
agents: {
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:
'Usuario: letras, números, punto, guion bajo o guion. Contraseña: al menos 12 caracteres, sin espacios al inicio o al final ni saltos de línea.',
octopAuthHelper:
'Contraseña: al menos 8 caracteres con letras y números; evite contraseñas comunes. Si se rechaza al iniciar, consulte las instrucciones en los registros de la aplicación.',
initialCredentials: 'Credenciales iniciales; si las cambió en la aplicación, use la contraseña actual.',
nativeAgentAccessUnavailable:
'La instalación existente no tiene una dirección o puerto HTTPS compatible. Revise la configuración de la aplicación.',
agent: 'Agente',
account: 'Cuenta de modelo',
batchOperation: 'Operación por lotes',
@@ -3760,6 +3768,7 @@ const message = {
link: 'Enlace',
showCurrentArch: 'Arquitectura',
syncLocalApp: 'Sincronizar app local',
localAppSyncToNodes: 'Incluir nodos secundarios al sincronizar apps locales',
memoryRequiredHelper: 'La app requiere {0} de memoria',
gpuConfig: 'Habilitar soporte GPU',
gpuConfigHelper: 'Asegúrese de tener GPU NVIDIA, drivers y NVIDIA Docker Container Toolkit instalados',
+9 -1
View File
@@ -489,7 +489,6 @@ const message = {
recommend: 'پیشنهادی',
dir: 'دایرکتوری',
alias: 'نام مستعار',
quickDirDelete: 'حذف ({0}) را تأیید می‌کنید؟',
quickDir: 'دسترسی سریع',
minQuickJump: 'حداقل یک ورودی دسترسی سریع اضافه کنید',
maxQuickJump: 'می‌توانید حداکثر چهار ورودی دسترسی سریع اضافه کنید',
@@ -769,6 +768,14 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.',
deepSeekAuthHelper:
'نام کاربری: حروف لاتین، اعداد، نقطه، زیرخط یا خط تیره. گذرواژه: حداقل ۱۲ نویسه، بدون فاصله در ابتدا و انتها یا شکست خط.',
octopAuthHelper:
'گذرواژه: حداقل ۸ نویسه شامل حروف و اعداد؛ از گذرواژه‌های رایج پرهیز کنید. اگر رد شد، راهنمای تنظیمات را در گزارش‌های برنامه ببینید.',
initialCredentials: 'اطلاعات ورود اولیه؛ اگر در برنامه تغییر کرده، از گذرواژه فعلی استفاده کنید.',
nativeAgentAccessUnavailable: 'نصب موجود نشانی یا درگاه HTTPS سازگار ندارد. تنظیمات برنامه را بررسی کنید.',
agent: 'عامل',
account: 'حساب مدل',
batchOperation: 'عملیات دسته‌ای',
@@ -3659,6 +3666,7 @@ const message = {
link: 'لینک',
showCurrentArch: 'معماری',
syncLocalApp: 'همگام‌سازی برنامه محلی',
localAppSyncToNodes: 'همگام‌سازی گره‌های فرزند هنگام همگام‌سازی برنامه‌های محلی',
memoryRequiredHelper: 'نیاز حافظه برنامه فعلی {0}',
gpuConfig: 'فعال‌سازی پشتیبانی GPU',
gpuConfigHelper:
+10 -1
View File
@@ -492,7 +492,6 @@ const message = {
recommend: 'おすすめ',
dir: 'ディレクトリ',
alias: 'エイリアス',
quickDirDelete: '({0})を削除しますか?',
quickDir: 'クイックアクセス',
minQuickJump: 'クイックアクセス項目を1つ以上追加してください',
maxQuickJump: '最大4件のクイックアクセス項目を追加できます',
@@ -773,6 +772,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。',
deepSeekAuthHelper:
'ユーザー名には英数字、ドット、アンダースコア、ハイフンを使用できます。パスワードは12文字以上で、前後の空白や改行を含めないでください。',
octopAuthHelper:
'パスワードは英字と数字を含む8文字以上とし、よく使われるパスワードを避けてください。初期化時に拒否された場合は、アプリのログで設定手順を確認してください。',
initialCredentials: '初期ログイン情報です。アプリ内で変更した場合は現在のパスワードを使用してください。',
nativeAgentAccessUnavailable:
'既存のインストールに対応する HTTPS アクセス先またはポートがありません。アプリの設定を確認してください。',
agent: 'エージェント',
account: 'モデルアカウント',
batchOperation: '一括操作',
@@ -3679,6 +3687,7 @@ const message = {
link: 'リンク',
showCurrentArch: '現在のサーバーアーキテクチャのアプリケーション',
syncLocalApp: 'ローカルアプリの同期',
localAppSyncToNodes: 'ローカルアプリ同期時に子ノードも同期',
memoryRequiredHelper: '現在のアプリケーションは {0} メモリが必要です',
gpuConfig: 'GPUサポートを有効化',
gpuConfigHelper:
+10 -1
View File
@@ -487,7 +487,6 @@ const message = {
recommend: '추천',
dir: '디렉토리',
alias: '별칭',
quickDirDelete: '({0})을(를) 삭제하시겠습니까?',
quickDir: '빠른 액세스',
minQuickJump: '최소 하나의 빠른 액세스 항목을 추가하세요',
maxQuickJump: '빠른 액세스를 최대 4개까지 추가할 수 있습니다',
@@ -764,6 +763,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.',
deepSeekAuthHelper:
'사용자 이름은 영문, 숫자, 점, 밑줄, 하이픈만 지원합니다. 비밀번호는 12자 이상이며 앞뒤 공백이나 줄바꿈을 포함할 수 없습니다.',
octopAuthHelper:
'비밀번호는 문자와 숫자를 포함한 8자 이상이어야 하며 흔한 비밀번호는 피하세요. 초기화 시 거부되면 앱 로그의 설정 안내를 확인하세요.',
initialCredentials: '초기 로그인 정보입니다. 앱에서 변경했다면 현재 비밀번호를 사용하세요.',
nativeAgentAccessUnavailable:
'기존 설치에 호환되는 HTTPS 접속 주소 또는 포트가 없습니다. 앱 설정을 확인하세요.',
agent: '에이전트',
account: '모델 계정',
batchOperation: '일괄 작업',
@@ -3614,6 +3622,7 @@ const message = {
link: '링크',
showCurrentArch: '현재 서버 아키텍처 애플리케이션',
syncLocalApp: '로컬 애플리케이션 동기화',
localAppSyncToNodes: '로컬 앱 동기화 시 하위 노드도 동기화',
memoryRequiredHelper: '현재 애플리케이션은 {0} 메모리가 필요합니다',
gpuConfig: 'GPU 지원 활성화',
gpuConfigHelper:
+10 -1
View File
@@ -487,7 +487,6 @@ const message = {
recommend: 'ແນະນຳ',
dir: 'ໄດເຣັກທໍຣີ',
alias: 'ຊື່ຫຼິ້ນ',
quickDirDelete: 'ຢືນຢັນການລຶບ ({0}) ຫຼືບໍ່?',
quickDir: 'ເຂົ້າເຖິງດ່ວນ',
minQuickJump: 'ເພີ່ມການເຂົ້າເຖິງດ່ວນຢ່າງໜ້ອຍໜຶ່ງລາຍການ',
maxQuickJump: 'ທ່ານສາມາດເພີ່ມການເຂົ້າເຖິງດ່ວນໄດ້ສູງສຸດສີ່ລາຍການ',
@@ -762,6 +761,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.',
deepSeekAuthHelper:
'ຊື່ຜູ້ໃຊ້: ຕົວອັກສອນອັງກິດ, ຕົວເລກ, ຈຸດ, ຂີດລຸ່ມ ຫຼືຂີດກາງ. ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 12 ຕົວ ໂດຍບໍ່ມີຊ່ອງຫວ່າງຢູ່ຫົວ/ທ້າຍ ຫຼືຂຶ້ນແຖວໃໝ່.',
octopAuthHelper:
'ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 8 ຕົວ ປະກອບດ້ວຍຕົວອັກສອນແລະຕົວເລກ; ຫຼີກລ້ຽງລະຫັດທີ່ໃຊ້ທົ່ວໄປ. ຖ້າຖືກປະຕິເສດ ໃຫ້ເບິ່ງຄຳແນະນຳໃນບັນທຶກຂອງແອັບ.',
initialCredentials: 'ຂໍ້ມູນເຂົ້າລະບົບເບື້ອງຕົ້ນ; ຖ້າປ່ຽນໃນແອັບແລ້ວ ໃຫ້ໃຊ້ລະຫັດປັດຈຸບັນ.',
nativeAgentAccessUnavailable:
'ການຕິດຕັ້ງທີ່ມີຢູ່ບໍ່ມີທີ່ຢູ່ ຫຼືພອດ HTTPS ທີ່ເຂົ້າກັນໄດ້. ກະລຸນາກວດສອບການຕັ້ງຄ່າແອັບ.',
agent: 'ຕົວແທນ (Agent)',
account: 'ບັນຊີໂມເດວ',
batchOperation: 'ດຳເນີນການແບບກຸ່ມ',
@@ -3591,6 +3599,7 @@ const message = {
link: 'ລິ້ງ',
showCurrentArch: 'ສະຖາປັດຕະຍະກຳ',
syncLocalApp: 'ຊິງຄ໌ແອັບໃນເຄື່ອງ',
localAppSyncToNodes: 'ຮວມໂນດລູກເມື່ອຊິງຄ໌ແອັບໃນເຄື່ອງ',
memoryRequiredHelper: 'ຄວາມຕ້ອງການໜ່ວຍຄວາມຈຳຂອງແອັບປະຈຸບັນ {0}',
gpuConfig: 'ເປີດໃຊ້ການຮອງຮັບ GPU',
gpuConfigHelper:
+10 -1
View File
@@ -499,7 +499,6 @@ const message = {
recommend: 'Disyorkan',
dir: 'Direktori',
alias: 'Alias',
quickDirDelete: 'Sahkan pemadaman ({0})?',
quickDir: 'Akses Pantas',
minQuickJump: 'Tambah sekurang-kurangnya satu entri akses pantas',
maxQuickJump: 'Anda boleh menambah sehingga empat entri akses pantas',
@@ -782,6 +781,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'Masukkan alamat IPv4 atau nama hos yang digunakan dalam pelayar, tanpa protokol, port atau laluan. HTTPS menggunakan sijil setempat aplikasi.',
deepSeekAuthHelper:
'Nama pengguna: huruf, nombor, titik, garis bawah atau sempang. Kata laluan: sekurang-kurangnya 12 aksara, tanpa ruang di awal/akhir atau baris baharu.',
octopAuthHelper:
'Kata laluan: sekurang-kurangnya 8 aksara dengan huruf dan nombor; elakkan kata laluan lazim. Jika ditolak, semak arahan persediaan dalam log aplikasi.',
initialCredentials: 'Maklumat log masuk awal; gunakan kata laluan semasa jika telah diubah dalam aplikasi.',
nativeAgentAccessUnavailable:
'Pemasangan sedia ada tiada alamat atau port HTTPS yang serasi. Semak tetapan aplikasi.',
agent: 'Agen',
account: 'Akaun model',
batchOperation: 'Operasi pukal',
@@ -3751,6 +3759,7 @@ const message = {
link: 'Pautan',
showCurrentArch: 'Aplikasi seni bina pelayan semasa',
syncLocalApp: 'Segerakkan Aplikasi Tempatan',
localAppSyncToNodes: 'Sertakan nod anak semasa menyegerakkan aplikasi tempatan',
memoryRequiredHelper: 'Aplikasi semasa memerlukan {0} memori',
gpuConfig: 'Aktifkan Sokongan GPU',
gpuConfigHelper:
+10 -1
View File
@@ -492,7 +492,6 @@ const message = {
recommend: 'Recomendado',
dir: 'Diretório',
alias: 'Apelido',
quickDirDelete: 'Confirmar a exclusão de ({0})?',
quickDir: 'Acesso rápido',
minQuickJump: 'Adicione pelo menos uma entrada de acesso rápido',
maxQuickJump: 'Você pode adicionar até quatro entradas de acesso rápido',
@@ -775,6 +774,15 @@ const message = {
},
aiTools: {
agents: {
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:
'Usuário: letras, números, ponto, sublinhado ou hífen. Senha: no mínimo 12 caracteres, sem espaços nas extremidades ou quebras de linha.',
octopAuthHelper:
'Senha: no mínimo 8 caracteres com letras e números; evite senhas comuns. Se rejeitada na inicialização, consulte as instruções nos logs do aplicativo.',
initialCredentials: 'Credenciais iniciais; se alteradas no aplicativo, use a senha atual.',
nativeAgentAccessUnavailable:
'A instalação existente não tem endereço ou porta HTTPS compatível. Verifique as configurações do aplicativo.',
agent: 'Agente',
account: 'Conta de modelo',
batchOperation: 'Operação em lote',
@@ -3767,6 +3775,7 @@ const message = {
link: 'Link',
showCurrentArch: 'Aplicações da arquitetura atual do servidor',
syncLocalApp: 'Sincronizar Aplicativo Local',
localAppSyncToNodes: 'Incluir nós filhos ao sincronizar apps locais',
memoryRequiredHelper: 'O aplicativo atual requer {0} de memória',
gpuConfig: 'Ativar Suporte a GPU',
gpuConfigHelper:
+10 -1
View File
@@ -491,7 +491,6 @@ const message = {
recommend: 'Рекомендуемое',
dir: 'Каталог',
alias: 'Псевдоним',
quickDirDelete: 'Подтвердить удаление ({0})?',
quickDir: 'Быстрый доступ',
minQuickJump: 'Добавьте хотя бы одну запись быстрого доступа',
maxQuickJump: 'Вы можете добавить до четырех записей быстрого доступа',
@@ -773,6 +772,15 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.',
deepSeekAuthHelper:
'Имя пользователя: латинские буквы, цифры, точка, подчёркивание или дефис. Пароль: минимум 12 символов, без пробелов по краям и переводов строк.',
octopAuthHelper:
'Пароль: минимум 8 символов с буквами и цифрами; избегайте распространённых паролей. Если пароль отклонён, инструкции доступны в журнале приложения.',
initialCredentials: 'Начальные учётные данные; если пароль изменён в приложении, используйте текущий.',
nativeAgentAccessUnavailable:
'У существующей установки нет совместимого HTTPS-адреса или порта. Проверьте настройки приложения.',
agent: 'Агент',
account: 'Аккаунт модели',
batchOperation: 'Пакетная операция',
@@ -3736,6 +3744,7 @@ const message = {
link: 'Ссылка',
showCurrentArch: 'Приложения для текущей архитектуры сервера',
syncLocalApp: 'Синхронизировать локальное приложение',
localAppSyncToNodes: 'Включать дочерние узлы при синхронизации локальных приложений',
memoryRequiredHelper: 'Текущее приложение требует {0} памяти',
gpuConfig: 'Включить поддержку GPU',
gpuConfigHelper:
+10 -1
View File
@@ -495,7 +495,6 @@ const message = {
recommend: 'Önerilen',
dir: 'Dizin',
alias: 'Takma Ad',
quickDirDelete: '({0}) silinsin mi?',
quickDir: 'Hızlı Erişim',
minQuickJump: 'En az bir hızlı erişim girdisi ekleyin',
maxQuickJump: 'En fazla dört hızlı erişim girdisi ekleyebilirsiniz',
@@ -778,6 +777,15 @@ const message = {
},
aiTools: {
agents: {
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:
'Kullanıcı adı: harf, rakam, nokta, alt çizgi veya tire. Parola: en az 12 karakter; başta/sonda boşluk veya satır sonu içeremez.',
octopAuthHelper:
'Parola: harf ve rakam içeren en az 8 karakter; yaygın parolalardan kaçının. Reddedilirse kurulum yönergeleri için uygulama günlüklerini kontrol edin.',
initialCredentials: 'İlk giriş bilgileri; uygulamada değiştirildiyse güncel parolayı kullanın.',
nativeAgentAccessUnavailable:
'Mevcut kurulumda uyumlu HTTPS erişim adresi veya bağlantı noktası yok. Uygulama ayarlarını kontrol edin.',
agent: 'Ajan',
account: 'Model hesabı',
batchOperation: 'Toplu işlem',
@@ -3752,6 +3760,7 @@ const message = {
link: 'Bağlantı',
showCurrentArch: 'Mimari',
syncLocalApp: 'Yerel Uygulamayı Senkronize Et',
localAppSyncToNodes: 'Yerel uygulama eşitlemesine alt düğümleri dahil et',
memoryRequiredHelper: 'Geçerli uygulama bellek gereksinimi {0}',
gpuConfig: 'GPU Desteğini Etkinleştir',
gpuConfigHelper:
+8 -1
View File
@@ -473,7 +473,6 @@ const message = {
recommend: '推薦',
dir: '目錄',
alias: '別名',
quickDirDelete: '確定刪除({0})',
quickDir: '快捷目錄',
minQuickJump: '請至少設定一個快速跳轉入口',
maxQuickJump: '最多可設定四個快速跳轉入口',
@@ -740,6 +739,13 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。',
deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。',
octopAuthHelper:
'密碼至少 8 位且包含字母及數字,避免常見弱密碼。若初始化拒絕該密碼,請查看應用程式日誌中的設定說明。',
initialCredentials: '初始登入資訊;若已在應用程式內修改,以目前密碼為準。',
nativeAgentAccessUnavailable: '現有安裝缺少相容的 HTTPS 存取位址或連接埠,請檢查應用程式設定。',
agent: 'Agent',
account: '模型帳號',
batchOperation: '批次操作',
@@ -3452,6 +3458,7 @@ const message = {
link: '連結',
showCurrentArch: '目前伺服器架構應用',
syncLocalApp: '同步本機應用',
localAppSyncToNodes: '同步本機應用時聯動子節點',
memoryRequiredHelper: '目前應用記憶體需求 {0}',
gpuConfig: '開啟 GPU 支援',
gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 和 NVIDIA docker Container Toolkit',
+8 -1
View File
@@ -473,7 +473,6 @@ const message = {
recommend: '推荐',
dir: '目录',
alias: '别名',
quickDirDelete: '确定删除({0})',
quickDir: '快捷目录',
minQuickJump: '请至少设置一个快速跳转入口!',
maxQuickJump: '最多可设置四个快速跳转入口!',
@@ -757,6 +756,13 @@ const message = {
},
aiTools: {
agents: {
deepSeekAccessHostHelper:
'填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。',
deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。',
octopAuthHelper:
'密码至少 8 位且包含字母和数字,避免常见弱密码。若初始化拒绝该密码,请查看应用日志中的设置说明。',
initialCredentials: '初始登录信息;若已在应用内修改,以当前密码为准。',
nativeAgentAccessUnavailable: '现有安装缺少兼容的 HTTPS 访问地址或端口,请检查应用设置。',
agent: '智能体',
account: '模型账号',
batchOperation: '批量操作',
@@ -3493,6 +3499,7 @@ const message = {
link: '链接',
showCurrentArch: '本服务器架构应用',
syncLocalApp: '同步本地应用',
localAppSyncToNodes: '同步本地应用时联动子节点',
memoryRequiredHelper: '当前应用内存需求 {0}',
gpuConfig: '开启 GPU 支持',
gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 和 NVIDIA docker Container Toolkit',
+43 -1
View File
@@ -1,10 +1,52 @@
import i18n from '@/lang';
import { compareVersion } from '@/utils/version';
import type { AI } from '@/api/interface/ai';
const openclawHTTPSVersion = '2026.3.13';
const openclawHTTPVersion = '2026.3.23';
const openclawDefaultAccessHost = '127.0.0.1';
export const isAgentType = (value: unknown): value is AI.AgentType =>
typeof value === 'string' && ['openclaw', 'copaw', 'hermes-agent', 'deepseek-harness', 'octop'].includes(value);
export const isDeepSeekAccessHost = (value: string): boolean => {
if (/^[0-9.]+$/.test(value)) {
return (
value.split('.').length === 4 &&
value.split('.').every((part) => /^(0|[1-9]\d{0,2})$/.test(part) && +part <= 255)
);
}
return (
value.length > 0 &&
value.length <= 253 &&
value === value.trim() &&
value.split('.').every((part) => /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(part))
);
};
export const isOctopPassword = (value: string): boolean =>
[...value].length >= 8 &&
value === value.trim() &&
!/[\r\n\0]/.test(value) &&
/\p{L}/u.test(value) &&
/\p{Nd}/u.test(value) &&
![
'password',
'password1',
'password12',
'password123',
'12345678',
'123456789',
'qwerty123',
'admin123',
'welcome1',
'letmein1',
'changeme1',
'octop123',
'abc12345',
'iloveyou1',
].includes(value.toLowerCase());
export const getAgentProviderDisplayName = (provider: string, displayName?: string): string => {
if (provider === 'custom' || displayName === 'Custom') {
return i18n.global.t('container.custom');
@@ -27,7 +69,7 @@ export const supportsAgentToken = (agentType: string): boolean => {
};
export const supportsAgentDashboardAuth = (agentType: string): boolean => {
return agentType === 'hermes-agent' || agentType === 'copaw';
return ['hermes-agent', 'copaw', 'deepseek-harness', 'octop'].includes(agentType);
};
export const isOpenclawHTTPSWindowVersion = (version: string): boolean => {
@@ -7,6 +7,8 @@
<el-option label="OpenClaw" value="openclaw" />
<el-option label="Hermes Agent" value="hermes-agent" />
<el-option label="QwenPaw" value="copaw" />
<el-option label="DeepSeek Harness" value="deepseek-harness" />
<el-option label="Octop" value="octop" />
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="name">
@@ -19,10 +21,19 @@
<el-select v-model="form.appVersion" filterable>
<el-option v-for="item in versions" :key="item" :label="item" :value="item" />
</el-select>
<span v-if="versions.length === 0" class="input-help">{{ $t('commons.msg.noneData') }}</span>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.webuiPort')" prop="webUIPort">
<el-input-number v-model="form.webUIPort" :min="1" :max="65535" />
</el-form-item>
<el-form-item
v-if="form.agentType === 'deepseek-harness'"
:label="$t('setting.systemIP')"
prop="accessHost"
>
<el-input v-model="form.accessHost" />
<span class="input-help">{{ $t('aiTools.agents.deepSeekAccessHostHelper') }}</span>
</el-form-item>
<template v-if="supportsAgentDashboardAuth(form.agentType)">
<el-form-item :label="$t('commons.login.username')" prop="dashboardUsername">
<el-input v-model="form.dashboardUsername" />
@@ -35,6 +46,12 @@
{{ $t('commons.button.random') }}
</el-button>
</div>
<span class="input-help" v-if="form.agentType === 'deepseek-harness'">
{{ $t('aiTools.agents.deepSeekAuthHelper') }}
</span>
<span class="input-help" v-if="form.agentType === 'octop'">
{{ $t('aiTools.agents.octopAuthHelper') }}
</span>
</el-form-item>
</template>
<el-form-item
@@ -130,6 +147,9 @@ import { getRandomStr, newUUID } from '@/utils/id';
import {
buildDefaultAllowedOrigin,
getAgentProviderDisplayName,
isAgentType,
isDeepSeekAccessHost,
isOctopPassword,
parseAllowedOriginsInput,
supportsAgentDashboardAuth,
validateAllowedOriginsInput,
@@ -138,6 +158,7 @@ import { App } from '@/api/interface/app';
import AdvancedSetting from '@/components/advanced-setting/index.vue';
import AccountAddDialog from '@/views/ai/agents/model/add/index.vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
import i18n from '@/lang';
const emit = defineEmits(['search', 'task']);
@@ -161,6 +182,7 @@ const form = reactive({
agentType: 'openclaw' as AI.AgentType,
appVersion: '',
webUIPort: 18789,
accessHost: '',
allowedOrigins: '',
provider: '',
accountId: undefined as unknown as number,
@@ -185,7 +207,10 @@ const form = reactive({
const showModelConfig = computed(() => form.agentType === 'openclaw' || form.agentType === 'hermes-agent');
const generateDashboardPassword = () => {
form.dashboardPassword = getRandomStr(8);
form.dashboardPassword =
form.agentType === 'deepseek-harness' || form.agentType === 'octop'
? `Aa2${Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('')}`
: getRandomStr(8);
};
const ensureDashboardAuth = () => {
@@ -201,7 +226,7 @@ const ensureDashboardAuth = () => {
};
const setDefaultWebUIPort = () => {
if (form.agentType === 'copaw') {
if (form.agentType === 'copaw' || form.agentType === 'octop') {
form.webUIPort = 8088;
return;
}
@@ -209,6 +234,10 @@ const setDefaultWebUIPort = () => {
form.webUIPort = 9119;
return;
}
if (form.agentType === 'deepseek-harness') {
form.webUIPort = 10443;
return;
}
form.webUIPort = 18789;
};
@@ -217,6 +246,19 @@ const rules = reactive({
agentType: [Rules.requiredSelect],
appVersion: [Rules.requiredSelect],
webUIPort: [Rules.requiredInput],
accessHost: [
Rules.requiredInput,
{
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
callback(
isDeepSeekAccessHost(value)
? undefined
: new Error(i18n.global.t('aiTools.agents.deepSeekAccessHostHelper')),
);
},
trigger: 'blur',
},
],
allowedOrigins: [
Rules.requiredInput,
{
@@ -234,8 +276,35 @@ const rules = reactive({
provider: [Rules.requiredSelect],
accountId: [Rules.requiredSelect],
model: [Rules.requiredInput],
dashboardUsername: [Rules.requiredInput],
dashboardPassword: [Rules.requiredInput],
dashboardUsername: [
Rules.requiredInput,
{
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
callback(
form.agentType === 'deepseek-harness' && !/^[A-Za-z0-9._-]+$/.test(value)
? new Error(i18n.global.t('aiTools.agents.deepSeekAuthHelper'))
: undefined,
);
},
trigger: 'blur',
},
],
dashboardPassword: [
Rules.requiredInput,
{
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
const invalidCharacters = value !== value.trim() || /[\r\n\0]/.test(value);
if (form.agentType === 'deepseek-harness' && (invalidCharacters || [...value].length < 12)) {
callback(new Error(i18n.global.t('aiTools.agents.deepSeekAuthHelper')));
} else if (form.agentType === 'octop' && !isOctopPassword(value)) {
callback(new Error(i18n.global.t('aiTools.agents.octopAuthHelper')));
} else {
callback();
}
},
trigger: 'blur',
},
],
containerName: [Rules.containerName],
restartPolicy: [Rules.requiredSelect],
cpuQuota: [checkNumberRange(0, 99999)],
@@ -273,7 +342,13 @@ const loadSystemIP = async () => {
};
const loadVersions = async (appKey: AI.AgentType) => {
versions.value = [];
appInfo.value = undefined;
form.dockerCompose = '';
const res = await getAppByKey(appKey);
if (form.agentType !== appKey) {
return;
}
appInfo.value = res.data;
versions.value = res.data.versions || [];
if (!form.appVersion && versions.value.length > 0) {
@@ -287,6 +362,10 @@ const getDefaultAgentName = (agentType: AI.AgentType) => {
return 'QwenPaw';
case 'hermes-agent':
return 'Hermes-Agent';
case 'deepseek-harness':
return 'DeepSeek-Harness';
case 'octop':
return 'Octop';
default:
return 'OpenClaw';
}
@@ -402,7 +481,9 @@ const handleAgentTypeChange = async () => {
form.name === 'OpenClaw' ||
form.name === 'CoPaw' ||
form.name === 'QwenPaw' ||
form.name === 'Hermes-Agent'
form.name === 'Hermes-Agent' ||
form.name === 'DeepSeek-Harness' ||
form.name === 'Octop'
) {
form.name = getDefaultAgentName(form.agentType);
}
@@ -413,6 +494,10 @@ const handleAgentTypeChange = async () => {
form.accountId = undefined as unknown as number;
form.baseURL = '';
ensureDashboardAuth();
if (form.agentType === 'deepseek-harness' && !form.accessHost) {
await loadSystemIP();
form.accessHost = isDeepSeekAccessHost(systemIP.value) ? systemIP.value : '';
}
if (form.agentType === 'openclaw') {
await loadSystemIP();
allowedOriginsAutoFilled.value = true;
@@ -485,6 +570,7 @@ const submit = async () => {
webUIPort: form.webUIPort,
allowedOrigins: form.agentType === 'openclaw' ? parseAllowedOriginsInput(form.allowedOrigins) : undefined,
agentType: form.agentType,
accessHost: form.agentType === 'deepseek-harness' ? form.accessHost : undefined,
model: showModelConfig.value ? form.model : undefined,
accountId: showModelConfig.value ? form.accountId : undefined,
token: form.agentType === 'openclaw' ? form.token : undefined,
@@ -524,45 +610,19 @@ const handleClose = () => {
form.dashboardPassword = '';
form.remark = '';
form.allowedOrigins = '';
form.accessHost = '';
form.dockerCompose = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
};
const openDrawer = async (agentType?: AI.AgentType) => {
const targetType =
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
const targetType = isAgentType(agentType) ? agentType : 'openclaw';
form.name = getDefaultAgentName(targetType);
open.value = true;
form.agentType = targetType;
setDefaultWebUIPort();
form.token = getRandomStr(32).toLowerCase();
ensureDashboardAuth();
if (form.agentType === 'copaw') {
form.allowedOrigins = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
await loadVersions('copaw');
providerOptions.value = [];
providerModels.value = {};
accountOptions.value = [];
return;
}
if (form.agentType === 'hermes-agent') {
form.allowedOrigins = '';
lastAutoAllowedOrigins.value = '';
allowedOriginsAutoFilled.value = true;
await loadVersions('hermes-agent');
await loadProviders();
await loadAccounts();
return;
}
await loadSystemIP();
allowedOriginsAutoFilled.value = true;
syncAllowedOriginsWithDefault(true);
await loadVersions('openclaw');
await loadProviders();
await loadAccounts();
await handleAgentTypeChange();
};
const openAccountCreate = () => {
+33 -12
View File
@@ -178,6 +178,9 @@
alignment="start"
:size="2"
>
<el-text v-if="row.agentType === 'octop'" type="info" size="small">
{{ $t('aiTools.agents.initialCredentials') }}
</el-text>
<span>{{ row.dashboardUsername || '-' }}</span>
<el-space alignment="center">
<span>{{ row.dashboardPassword ? '********' : '-' }}</span>
@@ -283,7 +286,7 @@ import { Website } from '@/api/interface/website';
import { SearchWithPage } from '@/api/interface';
import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id';
import { MsgSuccess } from '@/utils/message';
import { MsgError, MsgSuccess } from '@/utils/message';
import AddDialog from '@/views/ai/agents/agent/add/index.vue';
import DeleteDialog from '@/views/ai/agents/agent/delete/index.vue';
@@ -302,6 +305,8 @@ import DockerStatus from '@/views/container/docker-status/index.vue';
import {
getAgentProviderDisplayName,
getOpenclawAccessScheme,
isAgentType,
isDeepSeekAccessHost,
supportsAgentModelConfig,
supportsAgentToken,
supportsAgentDashboardAuth,
@@ -312,6 +317,8 @@ import NoApp from '@/views/app-store/apps/no-app/index.vue';
import openclawIcon from '@/assets/images/ai-agent-openclaw.svg';
import copawIcon from '@/assets/images/ai-agent-copaw.svg';
import hermesIcon from '@/assets/images/ai-agent-hermes-agent.svg';
import deepSeekIcon from '@/assets/images/ai-agent-deepseek-harness.png';
import octopIcon from '@/assets/images/ai-agent-octop.png';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { loadOptionalComponent } from '@/extensions/optional';
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
@@ -358,8 +365,7 @@ const buttons = [
{
label: i18n.global.t('menu.config'),
click: (row: AI.AgentItem) => openConfig(row),
show: (row: AI.AgentItem) =>
supportsAgentModelConfig(row.agentType) || supportsAgentDashboardAuth(row.agentType),
show: (row: AI.AgentItem) => supportsAgentModelConfig(row.agentType) || row.agentType === 'copaw',
disabled: (row: AI.AgentItem) => row.status !== 'Running',
},
{
@@ -441,6 +447,10 @@ const getAgentTypeLabel = (agentType: AI.AgentType) => {
return 'QwenPaw';
case 'hermes-agent':
return 'Hermes Agent';
case 'deepseek-harness':
return 'DeepSeek Harness';
case 'octop':
return 'Octop';
default:
return 'OpenClaw';
}
@@ -452,6 +462,10 @@ const getAgentTypeIcon = (agentType: AI.AgentType) => {
return copawIcon;
case 'hermes-agent':
return hermesIcon;
case 'deepseek-harness':
return deepSeekIcon;
case 'octop':
return octopIcon;
default:
return openclawIcon;
}
@@ -461,8 +475,7 @@ const openCreate = (agentType?: AI.AgentType) => {
if (noApp.value) {
return;
}
const targetType =
agentType === 'copaw' || agentType === 'hermes-agent' || agentType === 'openclaw' ? agentType : 'openclaw';
const targetType = isAgentType(agentType) ? agentType : 'openclaw';
if (addRef.value?.open) {
addRef.value.open(targetType);
}
@@ -473,12 +486,7 @@ const openCreateFromQuery = async () => {
if (!shouldOpen) {
return;
}
const agentType =
route.query.agentType === 'copaw'
? 'copaw'
: route.query.agentType === 'hermes-agent'
? 'hermes-agent'
: 'openclaw';
const agentType = isAgentType(route.query.agentType) ? route.query.agentType : 'openclaw';
openCreate(agentType);
const nextQuery = { ...route.query };
delete nextQuery.open;
@@ -547,7 +555,12 @@ const openTerminal = (row: AI.AgentItem) => {
openTerminalDialog({
containerID: row.containerName,
title,
users: row.agentType === 'hermes-agent' ? ['hermes', 'root'] : ['node', 'root'],
users:
row.agentType === 'hermes-agent'
? ['hermes', 'root']
: row.agentType === 'octop'
? ['root']
: ['node', 'root'],
shell: '/bin/bash',
initCmd: '',
});
@@ -579,6 +592,14 @@ const openWorkDir = (row: AI.AgentItem) => {
};
const jumpWebUI = (row: AI.AgentItem) => {
if (row.agentType === 'deepseek-harness') {
if (!isDeepSeekAccessHost(row.accessHost || '') || !row.webUIPort) {
MsgError(i18n.global.t('aiTools.agents.nativeAgentAccessUnavailable'));
return;
}
window.open(`https://${row.accessHost}:${row.webUIPort}`, '_blank', 'noopener,noreferrer');
return;
}
if (dialogPortJumpRef.value?.acceptParams) {
dialogPortJumpRef.value.acceptParams({
port: row.webUIPort,
+25 -11
View File
@@ -73,7 +73,15 @@
import { useSearchReset } from '@/composables/useSearchReset';
import { App } from '@/api/interface/app';
import { toRef, onMounted, reactive, ref } from 'vue';
import { searchApp, syncApp, syncCutomAppStore, syncLocalApp, getCurrentNodeCustomAppConfig } from '@/api/modules/app';
import {
searchApp,
syncApp,
syncCutomAppStore,
syncLocalApp,
getCurrentNodeCustomAppConfig,
getLocalAppSyncConfig,
dispatchLocalAppSync,
} from '@/api/modules/app';
import Install from '../detail/install/index.vue';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
@@ -88,7 +96,7 @@ import AppCard from '@/views/app-store/apps/app/index.vue';
import MainDiv from '@/components/main-div/index.vue';
import { jumpToInstall } from '@/utils/app';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isProductPro, isOffline, isMobile, isEnterprise } = useGlobalStore();
const { isProductPro, isOffline, isMobile, isEnterprise, isMaster, isXpackOrEE, isAdmin } = useGlobalStore();
const paginationConfig = reactive({
cacheSizeKey: 'app-page-size',
@@ -201,21 +209,27 @@ const sync = async () => {
}
};
const syncLocal = () => {
const syncLocal = async () => {
const taskID = newUUID();
const syncReq = {
taskID: taskID,
};
syncing.value = true;
syncLocalApp(syncReq)
.then(() => {
try {
const dispatch =
isMaster.value && isXpackOrEE.value && isAdmin.value && (await getLocalAppSyncConfig()).data.enabled;
if (dispatch) {
await dispatchLocalAppSync(syncReq);
taskLogRef.value.openWithTaskID(taskID, true, 'local');
} else {
await syncLocalApp(syncReq);
openTaskLog(taskID);
canUpdate.value = false;
search(req);
})
.finally(() => {
syncing.value = false;
});
}
canUpdate.value = false;
search(req);
} finally {
syncing.value = false;
}
};
const changeTag = (key: string) => {
+18 -13
View File
@@ -50,12 +50,20 @@
<el-col :span="6" v-for="item in baseInfo.quickJump" :key="item.name">
<span>{{ $t(item.title, 2) }}</span>
<div class="count">
<QuickDirectories
v-if="item.name === 'File'"
:count="item.detail"
:disabled="!checkPermission('File')"
@change="updateQuickDirectoryCount"
/>
<el-tooltip
v-if="item.alias || item.detail.length > 20"
:content="item.detail"
placement="bottom"
>
<el-button
link
:disabled="!checkPermission('File')"
type="primary"
@click="quickJump(item)"
>
{{ item.alias || item.detail.substring(0, 18) + '...' }}
</el-button>
</el-tooltip>
<el-button
link
:disabled="!checkPermission(item.name)"
@@ -442,7 +450,6 @@ import SystemStatus from '@/views/home/status/index.vue';
import AppLauncher from '@/views/home/app/index.vue';
import VCharts from '@/components/v-charts/index.vue';
import QuickJump from '@/views/home/quick/index.vue';
import QuickDirectories from '@/views/home/quick/directories.vue';
import CardWithHeader from '@/components/card-with-header/index.vue';
import MarkDownEditor from '@/components/mkdown-editor/index.vue';
import i18n from '@/lang';
@@ -463,7 +470,7 @@ import {
updateMemo,
updateSetting,
} from '@/api/modules/setting';
import { routerToNameWithQuery, routerToPath } from '@/utils/router';
import { routerToFileWithPath, routerToNameWithQuery, routerToPath } from '@/utils/router';
import { getWelcomePage } from '@/api/modules/auth';
import {
clearDashboardCache,
@@ -816,12 +823,10 @@ const onLoadBaseInfo = async (isInit: boolean, range: string) => {
}
};
const updateQuickDirectoryCount = (count: number) => {
const item = baseInfo.value.quickJump.find((item) => item.name === 'File');
if (item) item.detail = String(count);
};
const quickJump = (item: any) => {
if (item.name === 'File') {
return routerToFileWithPath(item.detail);
}
return routerToPath(item.router);
};
@@ -1,315 +0,0 @@
<template>
<el-popover
trigger="click"
placement="bottom"
:width="680"
:popper-style="{ maxWidth: 'calc(100vw - 32px)' }"
:visible="visible"
@update:visible="setVisible"
:disabled="disabled"
:title="$t('home.quickDir')"
@show="load"
>
<template #reference>
<el-button link type="primary" :disabled="disabled">{{ count }}</el-button>
</template>
<div v-loading="loading" class="quick-directories">
<div class="directory-search">
<el-input
v-model="searchInput"
clearable
prefix-icon="Search"
:placeholder="`${$t('file.path')} / ${$t('home.alias')}`"
:aria-label="$t('commons.button.search')"
@keyup.enter="search"
@clear="search"
/>
<el-button :disabled="loading" @click="search">{{ $t('commons.button.confirm') }}</el-button>
</div>
<div class="directory-row directory-header">
<span>{{ $t('file.path') }}</span>
<span>{{ $t('home.alias') }}</span>
<span />
</div>
<div class="directory-list">
<div v-if="!filteredRows.length" class="directory-empty">{{ $t('commons.msg.noneData') }}</div>
<div v-for="row in filteredRows" :key="row.key" class="directory-row">
<el-input
v-model="row.path"
:readonly="!row.editing"
:disabled="row.busy"
:aria-label="$t('file.path')"
>
<template #prepend>
<el-button
icon="Folder"
:disabled="!row.editing || row.busy"
:aria-label="$t('file.list')"
@click="selectPath(row)"
/>
</template>
</el-input>
<el-input
v-model="row.alias"
:readonly="!row.editing"
:disabled="row.busy"
:aria-label="$t('home.alias')"
/>
<div class="directory-actions">
<el-button
link
:type="row.editing ? 'success' : 'primary'"
:icon="row.editing ? 'Check' : 'Edit'"
:aria-label="$t(row.editing ? 'commons.button.save' : 'commons.button.edit')"
:disabled="loading || row.busy || (row.editing && !row.path.trim())"
@click="row.editing ? save(row) : (row.editing = true)"
/>
<el-button
v-if="row.id || rows.length > 1"
link
:type="row.editing ? 'info' : 'danger'"
:icon="row.editing ? 'Close' : 'Delete'"
:aria-label="$t('commons.button.delete')"
:disabled="loading || row.busy"
@click="remove(row)"
/>
<el-button
v-if="row.id"
link
type="primary"
icon="Position"
:title="$t('firewall.quickJump')"
:aria-label="$t('firewall.quickJump')"
:disabled="loading || row.busy"
@click="routerToFileWithPath(row.savedPath)"
/>
</div>
</div>
</div>
<el-button
class="directory-add"
link
type="primary"
icon="Plus"
:disabled="loading || !loaded"
@click="add"
>
{{ $t('commons.button.add') }}
</el-button>
</div>
<div class="directory-footer">
<el-button @click="close">{{ $t('commons.button.close') }}</el-button>
</div>
</el-popover>
<FileList ref="fileRef" @choose="loadPath" @close="selectingRow = undefined" />
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { routerToFileWithPath } from '@/utils/router';
import FileList from '@/components/file-list/index.vue';
import { Dashboard } from '@/api/interface/dashboard';
import {
loadQuickDirectories,
createQuickDirectory,
updateQuickDirectory,
deleteQuickDirectory,
} from '@/api/modules/dashboard';
defineProps<{ count: string; disabled: boolean }>();
const emit = defineEmits<{ (e: 'change', count: number): void }>();
type DirectoryRow = Dashboard.QuickDirectory & {
key: number;
savedPath: string;
editing: boolean;
busy: boolean;
};
const rows = ref<DirectoryRow[]>([]);
const searchInput = ref('');
const searchTerm = ref('');
const filteredRows = computed(() =>
rows.value.filter(
(row) =>
!row.id ||
row.editing ||
row.path.toLowerCase().includes(searchTerm.value) ||
(row.alias ?? '').toLowerCase().includes(searchTerm.value),
),
);
const search = () => {
searchTerm.value = searchInput.value.trim().toLowerCase();
};
const fileRef = ref<InstanceType<typeof FileList>>();
const selectingRow = ref<DirectoryRow>();
const visible = ref(false);
const loading = ref(false);
const loaded = ref(false);
let nextKey = 0;
const setVisible = (value: boolean) => {
if (value) visible.value = true;
};
const close = () => {
visible.value = false;
};
const selectPath = (row: DirectoryRow) => {
if (!row.editing || row.busy) return;
selectingRow.value = row;
fileRef.value?.acceptParams({ path: row.path, dir: false, isAll: true });
};
const loadPath = (path: string) => {
const row = selectingRow.value;
if (row?.editing && !row.busy) row.path = path;
};
const add = () => {
rows.value.push({
key: nextKey++,
id: 0,
path: '',
alias: '',
savedPath: '',
editing: true,
busy: false,
});
};
const updateCount = () => emit('change', rows.value.filter((row) => row.id).length);
const load = async () => {
if (loading.value || rows.value.some((row) => row.busy)) return;
loading.value = true;
loaded.value = false;
searchInput.value = '';
searchTerm.value = '';
rows.value = [];
try {
const { data } = await loadQuickDirectories();
rows.value = data.map((item) => ({
...item,
alias: item.alias ?? '',
key: nextKey++,
savedPath: item.path,
editing: false,
busy: false,
}));
if (!rows.value.length) add();
loaded.value = true;
updateCount();
} finally {
loading.value = false;
}
};
const save = async (row: DirectoryRow) => {
if (row.busy || !row.path.trim()) return;
row.busy = true;
try {
const payload = { path: row.path, alias: (row.alias ?? '').trim() };
const { data } = row.id
? await updateQuickDirectory({ id: row.id, legacy: row.legacy ?? false, ...payload })
: await createQuickDirectory(payload);
Object.assign(row, data, { savedPath: data.path, editing: false });
updateCount();
} finally {
row.busy = false;
}
};
const remove = async (row: DirectoryRow) => {
if (row.busy) return;
if (row.id) {
row.busy = true;
try {
await ElMessageBox.confirm('', i18n.global.t('home.quickDirDelete', [row.savedPath]), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
customClass: 'quick-directory-delete',
type: 'warning',
center: true,
});
} catch {
row.busy = false;
return;
}
try {
await deleteQuickDirectory(row.id, row.legacy ?? false);
} catch (error) {
row.busy = false;
throw error;
}
}
rows.value = rows.value.filter((item) => item.key !== row.key);
if (!rows.value.length) add();
updateCount();
};
</script>
<style lang="scss" scoped>
.quick-directories {
padding: 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
}
.directory-search {
display: flex;
gap: 12px;
padding: 0 10px 10px;
}
.directory-empty {
padding: 20px;
text-align: center;
color: var(--el-text-color-secondary);
}
.directory-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 96px;
gap: 12px;
align-items: center;
padding: 10px;
}
.directory-header {
font-weight: 600;
}
.directory-list {
max-height: 360px;
overflow-y: auto;
}
.directory-actions {
display: flex;
align-items: center;
:deep(.el-button + .el-button) {
margin-left: 8px;
}
}
.directory-footer {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.directory-add {
width: 100%;
margin-top: 8px;
padding: 12px;
}
</style>
<style lang="scss">
.quick-directory-delete {
.el-message-box__title {
justify-content: center;
gap: 10px;
padding: 14px 16px;
border-radius: 4px;
background-color: #59472a;
color: #f5cf87;
overflow-wrap: anywhere;
}
.el-message-box__status {
flex-shrink: 0;
font-size: 24px;
color: #f2b329;
}
.el-message-box__content {
display: none;
}
}
</style>
+32 -1
View File
@@ -10,6 +10,25 @@
<el-table-column prop="isShow" :label="$t('setting.ifShow')">
<template #default="{ row }">
<el-switch v-model="row.isShow" />
<div v-if="row.name === 'File' && row.isShow">
<el-input v-model="row.detail" class="w-full">
<template #prepend>
<el-button
class="w-16"
v-if="row.name === 'File' && row.isShow"
icon="Folder"
@click="fileRef.acceptParams({ path: row.detail, isAll: true })"
/>
</template>
</el-input>
<el-input v-model="row.alias" class="mt-1">
<template #prepend>
<el-button class="w-16">
{{ $t('home.alias') }}
</el-button>
</template>
</el-input>
</div>
</template>
</el-table-column>
</ComplexTable>
@@ -20,12 +39,14 @@
{{ $t('commons.button.confirm') }}
</el-button>
</template>
<FileList ref="fileRef" @choose="loadDir" />
</DrawerPro>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import i18n from '@/lang';
import { MsgInfo, MsgSuccess } from '@/utils/message';
import FileList from '@/components/file-list/index.vue';
import { changeQuick, loadQuickOption } from '@/api/modules/dashboard';
const emit = defineEmits<{ (e: 'search'): void }>();
@@ -36,6 +57,7 @@ const acceptParams = (): void => {
drawerVisible.value = true;
};
const quickOptions = ref([]);
const fileRef = ref();
const search = async () => {
loading.value = true;
@@ -64,17 +86,26 @@ const onChangeShow = async () => {
return;
}
loading.value = true;
await changeQuick(quickOptions.value.map(({ id, isShow }) => ({ id, isShow })))
await changeQuick(quickOptions.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
drawerVisible.value = false;
search();
})
.catch(() => {
loading.value = false;
});
};
const loadDir = async (path: string) => {
for (const item of quickOptions.value) {
if (item.name === 'File') {
item.detail = path;
}
}
};
const handleClose = () => {
drawerVisible.value = false;
emit('search');