From ac63b981ad3d01ee220741f9e24b1b5e558cc2a8 Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <31820853+zhengkunwang223@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:48:21 +0800 Subject: [PATCH] feat(agent): add DeepSeek Harness and OctoP support --- agent/app/dto/agents.go | 4 +- agent/app/service/agents.go | 13 +- agent/app/service/agents_native.go | 69 ++++++++++ agent/app/service/agents_utils.go | 16 ++- agent/constant/app.go | 2 + agent/init/migration/migrate.go | 1 + .../migration/migrations/agents_native.go | 47 +++++++ core/cmd/server/conf/app.yaml | 2 +- core/i18n/lang/en.yaml | 3 + core/i18n/lang/es-ES.yaml | 3 + core/i18n/lang/fa.yaml | 3 + core/i18n/lang/ja.yaml | 3 + core/i18n/lang/ko.yaml | 3 + core/i18n/lang/lo.yaml | 3 + core/i18n/lang/ms.yaml | 3 + core/i18n/lang/pt-BR.yaml | 3 + core/i18n/lang/ru.yaml | 3 + core/i18n/lang/tr.yaml | 3 + core/i18n/lang/zh-Hant.yaml | 3 + core/i18n/lang/zh.yaml | 3 + frontend/src/api/interface/ai.ts | 4 +- frontend/src/api/interface/app.ts | 3 + frontend/src/api/modules/app.ts | 12 ++ .../images/ai-agent-deepseek-harness.png | Bin 0 -> 23412 bytes frontend/src/assets/images/ai-agent-octop.png | Bin 0 -> 7541 bytes frontend/src/lang/modules/en.ts | 10 ++ frontend/src/lang/modules/es-es.ts | 10 ++ frontend/src/lang/modules/fa.ts | 9 ++ frontend/src/lang/modules/ja.ts | 10 ++ frontend/src/lang/modules/ko.ts | 10 ++ frontend/src/lang/modules/lo.ts | 10 ++ frontend/src/lang/modules/ms.ts | 10 ++ frontend/src/lang/modules/pt-br.ts | 10 ++ frontend/src/lang/modules/ru.ts | 10 ++ frontend/src/lang/modules/tr.ts | 10 ++ frontend/src/lang/modules/zh-Hant.ts | 8 ++ frontend/src/lang/modules/zh.ts | 8 ++ frontend/src/utils/agent.ts | 44 +++++- .../src/views/ai/agents/agent/add/index.vue | 128 +++++++++++++----- frontend/src/views/ai/agents/agent/index.vue | 45 ++++-- frontend/src/views/app-store/apps/index.vue | 36 +++-- 41 files changed, 514 insertions(+), 63 deletions(-) create mode 100644 agent/app/service/agents_native.go create mode 100644 agent/init/migration/migrations/agents_native.go create mode 100644 frontend/src/assets/images/ai-agent-deepseek-harness.png create mode 100644 frontend/src/assets/images/ai-agent-octop.png diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 30305882e..45b63f924 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -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"` diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index f4d3d2c3e..d49f1ddb9 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -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 } diff --git a/agent/app/service/agents_native.go b/agent/app/service/agents_native.go new file mode 100644 index 000000000..4c6292c8c --- /dev/null +++ b/agent/app/service/agents_native.go @@ -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 +} diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index c1c07d389..8fea839fa 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -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 } diff --git a/agent/constant/app.go b/agent/constant/app.go index 11b2f5fad..b9348a64b 100644 --- a/agent/constant/app.go +++ b/agent/constant/app.go @@ -10,6 +10,8 @@ const ( AppOpenclaw = "openclaw" AppCopaw = "copaw" AppHermesAgent = "hermes-agent" + AppDeepSeekHarness = "deepseek-harness" + AppOctop = "octop" AppMysql = "mysql" AppMariaDB = "mariadb" AppPostgresql = "postgresql" diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 8570a1937..60db25feb 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -115,6 +115,7 @@ func agentDBMigrations() []*gormigrate.Migration { migrations.AddMonitorSettings, migrations.AddVLLMRequestMetrics, migrations.AddVLLMSampleInterval, + migrations.AddNativeAgentInstalls, } } diff --git a/agent/init/migration/migrations/agents_native.go b/agent/init/migration/migrations/agents_native.go new file mode 100644 index 000000000..809b19719 --- /dev/null +++ b/agent/init/migration/migrations/agents_native.go @@ -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 + }, +} diff --git a/core/cmd/server/conf/app.yaml b/core/cmd/server/conf/app.yaml index a1a166a29..c114bfea2 100644 --- a/core/cmd/server/conf/app.yaml +++ b/core/cmd/server/conf/app.yaml @@ -4,7 +4,7 @@ base: is_demo: false is_offline: false is_fxplay: false - is_enterprise: false + is_enterprise: true port: 9999 username: admin password: admin123 diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index da8c5f003..ec0b738e9 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -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" diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index 9f334a344..21bba0cc4 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -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" diff --git a/core/i18n/lang/fa.yaml b/core/i18n/lang/fa.yaml index a6367a00b..6764cc443 100644 --- a/core/i18n/lang/fa.yaml +++ b/core/i18n/lang/fa.yaml @@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "یک کلید API لغونشده با این نام از ErrAPIKeyNotFound: "کلید API وجود ندارد یا متعلق به کاربر فعلی نیست" ErrAPIKeyAppBindingDisabled: "اتصال برنامه با کد QR برای این کلید API مجاز نیست" ErrAPIKeyTerminalUpgradeRequired: "امکان بررسی پشتیبانی از مجوزدهی ترمینال با کلید API وجود ندارد. اتصال گره را بررسی و Agent آن را ارتقا دهید، سپس دوباره تلاش کنید." +LocalAppSyncDispatch: "ارسال درخواست همگام‌سازی برنامه‌های محلی" +LocalAppSyncDispatchNode: "ارسال درخواست همگام‌سازی به {{.node}} (وظیفه گره: {{.taskID}})" +ErrLocalAppSyncDisabled: "همگام‌سازی برنامه‌های محلی بین گره‌ها غیرفعال است" diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index 1b1f973b3..99d5e545d 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "同じ名前の未失効 API キーが既に存在しま ErrAPIKeyNotFound: "API キーが存在しないか、現在のユーザーに属していません" ErrAPIKeyAppBindingDisabled: "この API キーでは APP の QR コード連携が許可されていません" ErrAPIKeyTerminalUpgradeRequired: "API キーによるターミナル認証への対応を確認できません。ノードの接続を確認し、Agent を更新して再試行してください。" +LocalAppSyncDispatch: "ローカルアプリ同期要求の送信" +LocalAppSyncDispatchNode: "{{.node}} にローカルアプリ同期要求を送信(ノードタスク: {{.taskID}})" +ErrLocalAppSyncDisabled: "ノード間のローカルアプリ同期は無効です" diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index 210a94066..7c572f981 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "같은 이름의 폐기되지 않은 API Key가 이미 있 ErrAPIKeyNotFound: "API Key가 없거나 현재 사용자 소유가 아닙니다" ErrAPIKeyAppBindingDisabled: "이 API Key는 APP QR 코드 연결을 허용하지 않습니다" ErrAPIKeyTerminalUpgradeRequired: "API Key 터미널 인증 지원 여부를 확인할 수 없습니다. 노드 연결을 확인하고 Agent를 업그레이드한 후 다시 시도하세요." +LocalAppSyncDispatch: "로컬 앱 동기화 요청 전송" +LocalAppSyncDispatchNode: "{{.node}}에 로컬 앱 동기화 요청 전송 (노드 작업: {{.taskID}})" +ErrLocalAppSyncDisabled: "노드 간 로컬 앱 동기화가 비활성화되어 있습니다" diff --git a/core/i18n/lang/lo.yaml b/core/i18n/lang/lo.yaml index 446957ab4..4d311d043 100644 --- a/core/i18n/lang/lo.yaml +++ b/core/i18n/lang/lo.yaml @@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "ມີ API Key ທີ່ຍັງບໍ່ຖືກຖອ ErrAPIKeyNotFound: "ບໍ່ພົບ API Key ຫຼືບໍ່ແມ່ນຂອງຜູ້ໃຊ້ປັດຈຸບັນ" ErrAPIKeyAppBindingDisabled: "API Key ນີ້ບໍ່ອະນຸຍາດໃຫ້ຜູກ APP ຜ່ານລະຫັດ QR" ErrAPIKeyTerminalUpgradeRequired: "ບໍ່ສາມາດຢືນຢັນການຮອງຮັບການອະນຸຍາດເທີມິນອນດ້ວຍ API Key. ກວດສອບການເຊື່ອມຕໍ່ໂນດ ແລະອັບເກຣດ Agent ກ່ອນລອງອີກ." +LocalAppSyncDispatch: "ສົ່ງຄຳຂໍຊິງຄ໌ແອັບໃນເຄື່ອງ" +LocalAppSyncDispatchNode: "ສົ່ງຄຳຂໍຊິງຄ໌ໄປຫາ {{.node}} (ວຽກຂອງໂນດ: {{.taskID}})" +ErrLocalAppSyncDisabled: "ການຊິງຄ໌ແອັບລະຫວ່າງໂນດຍັງບໍ່ໄດ້ເປີດ" diff --git a/core/i18n/lang/ms.yaml b/core/i18n/lang/ms.yaml index c97ac693a..27dc9f3a8 100644 --- a/core/i18n/lang/ms.yaml +++ b/core/i18n/lang/ms.yaml @@ -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" diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index 0a5a24842..0973efc92 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -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" diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index 364588d2c..19fcf32f2 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "Неотозванный ключ API с таким име ErrAPIKeyNotFound: "Ключ API не существует или не принадлежит текущему пользователю" ErrAPIKeyAppBindingDisabled: "Для этого ключа API привязка приложения через QR-код запрещена" ErrAPIKeyTerminalUpgradeRequired: "Не удалось проверить поддержку авторизации терминала по ключу API. Проверьте подключение узла, обновите его Agent и повторите попытку." +LocalAppSyncDispatch: "Отправка запросов синхронизации локальных приложений" +LocalAppSyncDispatchNode: "Отправка запроса синхронизации узлу {{.node}} (задача узла: {{.taskID}})" +ErrLocalAppSyncDisabled: "Синхронизация локальных приложений между узлами отключена" diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index 9961891e2..80791583a 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -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ışı" diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index bd26347a7..16fd20fcd 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名且未撤銷的 API Key" ErrAPIKeyNotFound: "API Key 不存在或不屬於目前使用者" ErrAPIKeyAppBindingDisabled: "此 API Key 未允許 APP 掃碼綁定" ErrAPIKeyTerminalUpgradeRequired: "無法確認節點支援 API Key 終端授權,請檢查節點連線並升級 Agent 後重試。" +LocalAppSyncDispatch: "下發本機應用同步" +LocalAppSyncDispatchNode: "向節點 {{.node}} 下發本機應用同步(節點任務:{{.taskID}})" +ErrLocalAppSyncDisabled: "尚未啟用本機應用跨節點同步" diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index dff47e968..1f867c82d 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -499,3 +499,6 @@ ErrAPIKeyNameExists: "已存在同名的未撤销 API Key" ErrAPIKeyNotFound: "API Key 不存在或不属于当前用户" ErrAPIKeyAppBindingDisabled: "此 API Key 未允许 APP 扫码绑定" ErrAPIKeyTerminalUpgradeRequired: "无法确认节点支持 API Key 终端授权,请检查节点连接并升级 Agent 后重试。" +LocalAppSyncDispatch: "下发本地应用同步" +LocalAppSyncDispatchNode: "向节点 {{.node}} 下发本地应用同步(节点任务:{{.taskID}})" +ErrLocalAppSyncDisabled: "尚未开启本地应用跨节点同步" diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index e3705da86..bbaa9a1cb 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -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; diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index f4f890c51..314b880e8 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -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; diff --git a/frontend/src/api/modules/app.ts b/frontend/src/api/modules/app.ts index d4d0c65a7..21113a517 100644 --- a/frontend/src/api/modules/app.ts +++ b/frontend/src/api/modules/app.ts @@ -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('/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); +}; diff --git a/frontend/src/assets/images/ai-agent-deepseek-harness.png b/frontend/src/assets/images/ai-agent-deepseek-harness.png new file mode 100644 index 0000000000000000000000000000000000000000..28557415c9fcde54aba275dccdfa85580b55dd72 GIT binary patch literal 23412 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 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/images/ai-agent-octop.png b/frontend/src/assets/images/ai-agent-octop.png new file mode 100644 index 0000000000000000000000000000000000000000..04f286f77bfd73c358d4442ccb14d9d546946f87 GIT binary patch literal 7541 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 + 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 => { diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue index c8b9eaa99..2482663e1 100644 --- a/frontend/src/views/ai/agents/agent/add/index.vue +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -7,6 +7,8 @@ + + @@ -19,10 +21,19 @@ + {{ $t('commons.msg.noneData') }} + + + {{ $t('aiTools.agents.deepSeekAccessHostHelper') }} + 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 = () => { diff --git a/frontend/src/views/ai/agents/agent/index.vue b/frontend/src/views/ai/agents/agent/index.vue index 27a862ada..2071674e1 100644 --- a/frontend/src/views/ai/agents/agent/index.vue +++ b/frontend/src/views/ai/agents/agent/index.vue @@ -178,6 +178,9 @@ alignment="start" :size="2" > + + {{ $t('aiTools.agents.initialCredentials') }} + {{ row.dashboardUsername || '-' }} {{ row.dashboardPassword ? '********' : '-' }} @@ -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, diff --git a/frontend/src/views/app-store/apps/index.vue b/frontend/src/views/app-store/apps/index.vue index 6190565d2..a54f0bf01 100644 --- a/frontend/src/views/app-store/apps/index.vue +++ b/frontend/src/views/app-store/apps/index.vue @@ -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) => {