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 000000000..28557415c Binary files /dev/null and b/frontend/src/assets/images/ai-agent-deepseek-harness.png differ 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 000000000..04f286f77 Binary files /dev/null and b/frontend/src/assets/images/ai-agent-octop.png differ diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 023182701..445c12bf4 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -773,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', @@ -3692,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: diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 6cf8923e7..a829e521a 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -776,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', @@ -3759,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', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 993521945..19ee9fbfb 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -768,6 +768,14 @@ const message = { }, aiTools: { agents: { + deepSeekAccessHostHelper: + 'نشانی IPv4 یا نام میزبان مورد استفاده در مرورگر را بدون پروتکل، درگاه یا مسیر وارد کنید. HTTPS از گواهی محلی برنامه استفاده می‌کند.', + deepSeekAuthHelper: + 'نام کاربری: حروف لاتین، اعداد، نقطه، زیرخط یا خط تیره. گذرواژه: حداقل ۱۲ نویسه، بدون فاصله در ابتدا و انتها یا شکست خط.', + octopAuthHelper: + 'گذرواژه: حداقل ۸ نویسه شامل حروف و اعداد؛ از گذرواژه‌های رایج پرهیز کنید. اگر رد شد، راهنمای تنظیمات را در گزارش‌های برنامه ببینید.', + initialCredentials: 'اطلاعات ورود اولیه؛ اگر در برنامه تغییر کرده، از گذرواژه فعلی استفاده کنید.', + nativeAgentAccessUnavailable: 'نصب موجود نشانی یا درگاه HTTPS سازگار ندارد. تنظیمات برنامه را بررسی کنید.', agent: 'عامل', account: 'حساب مدل', batchOperation: 'عملیات دسته‌ای', @@ -3658,6 +3666,7 @@ const message = { link: 'لینک', showCurrentArch: 'معماری', syncLocalApp: 'همگام‌سازی برنامه محلی', + localAppSyncToNodes: 'همگام‌سازی گره‌های فرزند هنگام همگام‌سازی برنامه‌های محلی', memoryRequiredHelper: 'نیاز حافظه برنامه فعلی {0}', gpuConfig: 'فعال‌سازی پشتیبانی GPU', gpuConfigHelper: diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 5a2235ad8..f0b59c29b 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -772,6 +772,15 @@ const message = { }, aiTools: { agents: { + deepSeekAccessHostHelper: + 'ブラウザーで使用する IPv4 アドレスまたはホスト名を、スキーム・ポート・パスなしで入力してください。HTTPS はアプリのローカル証明書を使用します。', + deepSeekAuthHelper: + 'ユーザー名には英数字、ドット、アンダースコア、ハイフンを使用できます。パスワードは12文字以上で、前後の空白や改行を含めないでください。', + octopAuthHelper: + 'パスワードは英字と数字を含む8文字以上とし、よく使われるパスワードを避けてください。初期化時に拒否された場合は、アプリのログで設定手順を確認してください。', + initialCredentials: '初期ログイン情報です。アプリ内で変更した場合は現在のパスワードを使用してください。', + nativeAgentAccessUnavailable: + '既存のインストールに対応する HTTPS アクセス先またはポートがありません。アプリの設定を確認してください。', agent: 'エージェント', account: 'モデルアカウント', batchOperation: '一括操作', @@ -3678,6 +3687,7 @@ const message = { link: 'リンク', showCurrentArch: '現在のサーバーアーキテクチャのアプリケーション', syncLocalApp: 'ローカルアプリの同期', + localAppSyncToNodes: 'ローカルアプリ同期時に子ノードも同期', memoryRequiredHelper: '現在のアプリケーションは {0} メモリが必要です', gpuConfig: 'GPUサポートを有効化', gpuConfigHelper: diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 9103a3725..84294ff07 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -763,6 +763,15 @@ const message = { }, aiTools: { agents: { + deepSeekAccessHostHelper: + '브라우저에서 사용하는 IPv4 주소 또는 호스트 이름을 입력하세요. 프로토콜, 포트, 경로는 제외하세요. HTTPS는 앱의 로컬 인증서를 사용합니다.', + deepSeekAuthHelper: + '사용자 이름은 영문, 숫자, 점, 밑줄, 하이픈만 지원합니다. 비밀번호는 12자 이상이며 앞뒤 공백이나 줄바꿈을 포함할 수 없습니다.', + octopAuthHelper: + '비밀번호는 문자와 숫자를 포함한 8자 이상이어야 하며 흔한 비밀번호는 피하세요. 초기화 시 거부되면 앱 로그의 설정 안내를 확인하세요.', + initialCredentials: '초기 로그인 정보입니다. 앱에서 변경했다면 현재 비밀번호를 사용하세요.', + nativeAgentAccessUnavailable: + '기존 설치에 호환되는 HTTPS 접속 주소 또는 포트가 없습니다. 앱 설정을 확인하세요.', agent: '에이전트', account: '모델 계정', batchOperation: '일괄 작업', @@ -3613,6 +3622,7 @@ const message = { link: '링크', showCurrentArch: '현재 서버 아키텍처 애플리케이션', syncLocalApp: '로컬 애플리케이션 동기화', + localAppSyncToNodes: '로컬 앱 동기화 시 하위 노드도 동기화', memoryRequiredHelper: '현재 애플리케이션은 {0} 메모리가 필요합니다', gpuConfig: 'GPU 지원 활성화', gpuConfigHelper: diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 876254904..f87fb884c 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -761,6 +761,15 @@ const message = { }, aiTools: { agents: { + deepSeekAccessHostHelper: + 'ປ້ອນທີ່ຢູ່ IPv4 ຫຼືຊື່ໂຮສທີ່ໃຊ້ໃນບຣາວເຊີ ໂດຍບໍ່ມີໂປຣໂຕຄອນ, ພອດ ຫຼືເສັ້ນທາງ. HTTPS ໃຊ້ໃບຢັ້ງຢືນພາຍໃນຂອງແອັບ.', + deepSeekAuthHelper: + 'ຊື່ຜູ້ໃຊ້: ຕົວອັກສອນອັງກິດ, ຕົວເລກ, ຈຸດ, ຂີດລຸ່ມ ຫຼືຂີດກາງ. ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 12 ຕົວ ໂດຍບໍ່ມີຊ່ອງຫວ່າງຢູ່ຫົວ/ທ້າຍ ຫຼືຂຶ້ນແຖວໃໝ່.', + octopAuthHelper: + 'ລະຫັດຜ່ານ: ຢ່າງໜ້ອຍ 8 ຕົວ ປະກອບດ້ວຍຕົວອັກສອນແລະຕົວເລກ; ຫຼີກລ້ຽງລະຫັດທີ່ໃຊ້ທົ່ວໄປ. ຖ້າຖືກປະຕິເສດ ໃຫ້ເບິ່ງຄຳແນະນຳໃນບັນທຶກຂອງແອັບ.', + initialCredentials: 'ຂໍ້ມູນເຂົ້າລະບົບເບື້ອງຕົ້ນ; ຖ້າປ່ຽນໃນແອັບແລ້ວ ໃຫ້ໃຊ້ລະຫັດປັດຈຸບັນ.', + nativeAgentAccessUnavailable: + 'ການຕິດຕັ້ງທີ່ມີຢູ່ບໍ່ມີທີ່ຢູ່ ຫຼືພອດ HTTPS ທີ່ເຂົ້າກັນໄດ້. ກະລຸນາກວດສອບການຕັ້ງຄ່າແອັບ.', agent: 'ຕົວແທນ (Agent)', account: 'ບັນຊີໂມເດວ', batchOperation: 'ດຳເນີນການແບບກຸ່ມ', @@ -3590,6 +3599,7 @@ const message = { link: 'ລິ້ງ', showCurrentArch: 'ສະຖາປັດຕະຍະກຳ', syncLocalApp: 'ຊິງຄ໌ແອັບໃນເຄື່ອງ', + localAppSyncToNodes: 'ຮວມໂນດລູກເມື່ອຊິງຄ໌ແອັບໃນເຄື່ອງ', memoryRequiredHelper: 'ຄວາມຕ້ອງການໜ່ວຍຄວາມຈຳຂອງແອັບປະຈຸບັນ {0}', gpuConfig: 'ເປີດໃຊ້ການຮອງຮັບ GPU', gpuConfigHelper: diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index be5a7c1f9..77c90b415 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -781,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', @@ -3750,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: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index ba1bc68fa..0ad7768e1 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -774,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', @@ -3766,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: diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 197493503..f41bd2a91 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -772,6 +772,15 @@ const message = { }, aiTools: { agents: { + deepSeekAccessHostHelper: + 'Введите IPv4-адрес или имя хоста, используемое в браузере, без протокола, порта и пути. HTTPS использует локальный сертификат приложения.', + deepSeekAuthHelper: + 'Имя пользователя: латинские буквы, цифры, точка, подчёркивание или дефис. Пароль: минимум 12 символов, без пробелов по краям и переводов строк.', + octopAuthHelper: + 'Пароль: минимум 8 символов с буквами и цифрами; избегайте распространённых паролей. Если пароль отклонён, инструкции доступны в журнале приложения.', + initialCredentials: 'Начальные учётные данные; если пароль изменён в приложении, используйте текущий.', + nativeAgentAccessUnavailable: + 'У существующей установки нет совместимого HTTPS-адреса или порта. Проверьте настройки приложения.', agent: 'Агент', account: 'Аккаунт модели', batchOperation: 'Пакетная операция', @@ -3735,6 +3744,7 @@ const message = { link: 'Ссылка', showCurrentArch: 'Приложения для текущей архитектуры сервера', syncLocalApp: 'Синхронизировать локальное приложение', + localAppSyncToNodes: 'Включать дочерние узлы при синхронизации локальных приложений', memoryRequiredHelper: 'Текущее приложение требует {0} памяти', gpuConfig: 'Включить поддержку GPU', gpuConfigHelper: diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 674e015a9..64c6b2737 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -777,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', @@ -3751,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: diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index a7e060458..b506edcf4 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -739,6 +739,13 @@ const message = { }, aiTools: { agents: { + deepSeekAccessHostHelper: + '填寫瀏覽器實際存取的 IPv4 位址或主機名稱,不含協定、連接埠或路徑。HTTPS 使用應用程式的本機憑證。', + deepSeekAuthHelper: '使用者名稱僅支援字母、數字、點、底線及連字號;密碼至少 12 位,不含前後空格或換行。', + octopAuthHelper: + '密碼至少 8 位且包含字母及數字,避免常見弱密碼。若初始化拒絕該密碼,請查看應用程式日誌中的設定說明。', + initialCredentials: '初始登入資訊;若已在應用程式內修改,以目前密碼為準。', + nativeAgentAccessUnavailable: '現有安裝缺少相容的 HTTPS 存取位址或連接埠,請檢查應用程式設定。', agent: 'Agent', account: '模型帳號', batchOperation: '批次操作', @@ -3451,6 +3458,7 @@ const message = { link: '連結', showCurrentArch: '目前伺服器架構應用', syncLocalApp: '同步本機應用', + localAppSyncToNodes: '同步本機應用時聯動子節點', memoryRequiredHelper: '目前應用記憶體需求 {0}', gpuConfig: '開啟 GPU 支援', gpuConfigHelper: '請確保機器有 NVIDIA GPU 並且安裝 NVIDIA 驅動 和 NVIDIA docker Container Toolkit', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 4fd45d145..f5caddb27 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -756,6 +756,13 @@ const message = { }, aiTools: { agents: { + deepSeekAccessHostHelper: + '填写浏览器实际访问的 IPv4 地址或主机名,不含协议、端口或路径。HTTPS 使用应用的本地证书。', + deepSeekAuthHelper: '用户名仅支持字母、数字、点、下划线和连字符;密码至少 12 位,不含首尾空格或换行。', + octopAuthHelper: + '密码至少 8 位且包含字母和数字,避免常见弱密码。若初始化拒绝该密码,请查看应用日志中的设置说明。', + initialCredentials: '初始登录信息;若已在应用内修改,以当前密码为准。', + nativeAgentAccessUnavailable: '现有安装缺少兼容的 HTTPS 访问地址或端口,请检查应用设置。', agent: '智能体', account: '模型账号', batchOperation: '批量操作', @@ -3492,6 +3499,7 @@ const message = { link: '链接', showCurrentArch: '本服务器架构应用', syncLocalApp: '同步本地应用', + localAppSyncToNodes: '同步本地应用时联动子节点', memoryRequiredHelper: '当前应用内存需求 {0}', gpuConfig: '开启 GPU 支持', gpuConfigHelper: '请确保机器有 NVIDIA GPU 并且安装 NVIDIA 驱动 和 NVIDIA docker Container Toolkit', diff --git a/frontend/src/utils/agent.ts b/frontend/src/utils/agent.ts index 9170949bb..1b152ebe5 100644 --- a/frontend/src/utils/agent.ts +++ b/frontend/src/utils/agent.ts @@ -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 => { 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) => {