diff --git a/agent/app/api/v2/backup.go b/agent/app/api/v2/backup.go index 855f2e62fdcf..1b10ba8b11de 100644 --- a/agent/app/api/v2/backup.go +++ b/agent/app/api/v2/backup.go @@ -558,3 +558,46 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) { } helper.Success(c) } + +// @Tags Backup Account +// @Summary List files in cloud storage account +// @Accept json +// @Param request body dto.CloudFileListReq true "request" +// @Success 200 {array} dto.CloudFileInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /backups/cloud/files [post] +func (b *BaseApi) ListCloudFiles(c *gin.Context) { + var req dto.CloudFileListReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + files, err := backupService.ListCloudFiles(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, files) +} + +// @Tags Backup Account +// @Summary Sync a file from cloud storage to local path +// @Accept json +// @Param request body dto.CloudFileSyncReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /backups/cloud/sync [post] +func (b *BaseApi) SyncCloudFileToLocal(c *gin.Context) { + var req dto.CloudFileSyncReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := backupService.SyncCloudFileToLocal(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} diff --git a/agent/app/dto/backup.go b/agent/app/dto/backup.go index 11158f53c103..319d4f416100 100644 --- a/agent/app/dto/backup.go +++ b/agent/app/dto/backup.go @@ -135,3 +135,20 @@ type RecordFileSize struct { Name string `json:"name"` Size int64 `json:"size"` } + +type CloudFileInfo struct { + Name string `json:"name"` + Size int64 `json:"size"` + IsDir bool `json:"isDir"` +} + +type CloudFileListReq struct { + AccountID uint `json:"accountID" validate:"required"` + Path string `json:"path"` +} + +type CloudFileSyncReq struct { + AccountID uint `json:"accountID" validate:"required"` + SrcPath string `json:"srcPath" validate:"required"` + DstPath string `json:"dstPath" validate:"required"` +} diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index a95eccc13203..d69ed041a7eb 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -111,6 +111,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { appKey := constant.AppOpenclaw if agentType == constant.AppCopaw { appKey = constant.AppCopaw + } else if agentType == constant.AppNemoclaw { + appKey = constant.AppNemoclaw } app, err := appRepo.GetFirst(appRepo.WithKey(appKey)) if err != nil || app.ID == 0 { @@ -347,7 +349,7 @@ func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error { if err != nil { return err } - if normalizeAgentType(agent.AgentType) == constant.AppCopaw { + if normalizeAgentType(agent.AgentType) == constant.AppCopaw || normalizeAgentType(agent.AgentType) == constant.AppNemoclaw { return fmt.Errorf("copaw does not support token") } configPath := strings.TrimSpace(agent.ConfigPath) @@ -390,7 +392,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error if err != nil { return err } - if normalizeAgentType(agent.AgentType) == constant.AppCopaw { + if normalizeAgentType(agent.AgentType) == constant.AppCopaw || normalizeAgentType(agent.AgentType) == constant.AppNemoclaw { return fmt.Errorf("copaw does not support model config") } account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) @@ -867,7 +869,7 @@ func (a AgentService) GetSecurityConfig(req dto.AgentSecurityConfigReq) (*dto.Ag if err != nil { return nil, err } - if normalizeAgentType(agent.AgentType) == constant.AppCopaw { + if normalizeAgentType(agent.AgentType) == constant.AppCopaw || normalizeAgentType(agent.AgentType) == constant.AppNemoclaw { return nil, fmt.Errorf("copaw does not support security config") } conf, err := readOpenclawConfig(agent.ConfigPath) @@ -883,7 +885,7 @@ func (a AgentService) UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) if err != nil { return err } - if normalizeAgentType(agent.AgentType) == constant.AppCopaw { + if normalizeAgentType(agent.AgentType) == constant.AppCopaw || normalizeAgentType(agent.AgentType) == constant.AppNemoclaw { return fmt.Errorf("copaw does not support security config") } allowedOrigins, err := normalizeAllowedOrigins(req.AllowedOrigins) @@ -1567,6 +1569,8 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map agentType := normalizeAgentType(agent.AgentType) if appInstall != nil && appInstall.ID > 0 && appInstall.App.Key == constant.AppCopaw { agentType = constant.AppCopaw + } else if appInstall != nil && appInstall.ID > 0 && appInstall.App.Key == constant.AppNemoclaw { + agentType = constant.AppNemoclaw } item := dto.AgentItem{ ID: agent.ID, @@ -2375,7 +2379,7 @@ func providerModelPrefix(provider string) string { func isSupportedAgentType(agentType string) bool { switch normalizeAgentType(agentType) { - case constant.AppOpenclaw, constant.AppCopaw: + case constant.AppOpenclaw, constant.AppCopaw, constant.AppNemoclaw: return true default: return false diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index d7c82541fb6d..7179fa5a9977 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -392,7 +392,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error { return err } appKey := install.App.Key - if appKey == constant.AppOpenclaw || appKey == constant.AppCopaw { + if appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppNemoclaw { _ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID) } diff --git a/agent/app/service/backup.go b/agent/app/service/backup.go index b75fbaca5e78..ac1b5dd7d25b 100644 --- a/agent/app/service/backup.go +++ b/agent/app/service/backup.go @@ -62,6 +62,9 @@ type IBackupService interface { ContainerRecover(req dto.CommonRecover) error ComposeBackup(req dto.CommonBackup) error ComposeRecover(req dto.CommonRecover) error + + ListCloudFiles(req dto.CloudFileListReq) ([]dto.CloudFileInfo, error) + SyncCloudFileToLocal(req dto.CloudFileSyncReq) error } func NewIBackupService() IBackupService { @@ -635,3 +638,31 @@ func changeLocalBackup(oldPath, newPath string) error { _ = fileOp.RmRf(path.Join(oldPath, "master")) return nil } + +func (u *BackupService) ListCloudFiles(req dto.CloudFileListReq) ([]dto.CloudFileInfo, error) { + _, backClient, err := NewBackupClientWithID(req.AccountID) + if err != nil { + return nil, err + } + names, err := backClient.ListObjects(req.Path) + if err != nil { + return nil, err + } + var result []dto.CloudFileInfo + for _, name := range names { + info := dto.CloudFileInfo{Name: name} + result = append(result, info) + } + return result, nil +} + +func (u *BackupService) SyncCloudFileToLocal(req dto.CloudFileSyncReq) error { + _, backClient, err := NewBackupClientWithID(req.AccountID) + if err != nil { + return err + } + if _, err := backClient.Download(req.SrcPath, req.DstPath); err != nil { + return err + } + return nil +} diff --git a/agent/constant/app.go b/agent/constant/app.go index 051d4a087fd3..64ff42355422 100644 --- a/agent/constant/app.go +++ b/agent/constant/app.go @@ -9,6 +9,7 @@ const ( AppOpenresty = "openresty" AppOpenclaw = "openclaw" AppCopaw = "copaw" + AppNemoclaw = "nemoclaw" AppMysql = "mysql" AppMariaDB = "mariadb" AppPostgresql = "postgresql" diff --git a/agent/go.mod b/agent/go.mod index e85541c8f9d7..7d968f798e6f 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go v1.55.0 github.com/compose-spec/compose-go/v2 v2.9.0 github.com/creack/pty v1.1.24 + github.com/docker/cli v28.5.1+incompatible github.com/docker/compose/v2 v2.40.2 github.com/docker/docker v28.5.1+incompatible github.com/docker/go-connections v0.6.0 @@ -113,7 +114,6 @@ require ( github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/buildx v0.29.1 // indirect - github.com/docker/cli v28.5.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.1 // indirect diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 16b763bcc864..030bc37dbd39 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -73,6 +73,7 @@ func InitAgentDB() { migrations.AddAgentTypeForAgents, migrations.NormalizeAgentAccountVerifiedStatus, migrations.NormalizeOllamaAccountAPIType, + migrations.AddNemoclawAgentType, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index ea83ad1e5710..32f4a7c1e249 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -1095,3 +1095,14 @@ var NormalizeOllamaAccountAPIType = &gormigrate.Migration{ Update("api_type", "openai-responses").Error }, } + +var AddNemoclawAgentType = &gormigrate.Migration{ + ID: "20260320-add-nemoclaw-agent-type", + Migrate: func(tx *gorm.DB) error { + return tx.Exec( + "UPDATE agents SET agent_type = ? WHERE app_install_id IN (SELECT ai.id FROM app_installs ai JOIN apps a ON ai.app_id = a.id WHERE a.key = ?)", + constant.AppNemoclaw, + constant.AppNemoclaw, + ).Error + }, +} diff --git a/agent/router/backup.go b/agent/router/backup.go index ca8c16bc4c05..aeb54ff090ac 100644 --- a/agent/router/backup.go +++ b/agent/router/backup.go @@ -34,5 +34,8 @@ func (s *BackupRouter) InitRouter(Router *gin.RouterGroup) { backupRouter.POST("/record/download", baseApi.DownloadRecord) backupRouter.POST("/record/del", baseApi.DeleteBackupRecord) backupRouter.POST("/record/description/update", baseApi.UpdateRecordDescription) + + backupRouter.POST("/cloud/files", baseApi.ListCloudFiles) + backupRouter.POST("/cloud/sync", baseApi.SyncCloudFileToLocal) } } diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index bab030ce8f48..b9db17a35335 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -242,7 +242,7 @@ export namespace AI { webUIPort: number; bridgePort?: number; allowedOrigins?: string[]; - agentType: 'openclaw' | 'copaw'; + agentType: 'openclaw' | 'copaw' | 'nemoclaw'; provider?: string; model?: string; apiType?: string; @@ -269,7 +269,7 @@ export namespace AI { export interface AgentItem { id: number; name: string; - agentType: 'openclaw' | 'copaw'; + agentType: 'openclaw' | 'copaw' | 'nemoclaw'; provider: string; providerName: string; model: string; diff --git a/frontend/src/api/interface/backup.ts b/frontend/src/api/interface/backup.ts index e9b74dd85777..2ecece94d71e 100644 --- a/frontend/src/api/interface/backup.ts +++ b/frontend/src/api/interface/backup.ts @@ -104,4 +104,18 @@ export namespace Backup { secret: string; taskID: string; } + export interface CloudFileInfo { + name: string; + size: number; + isDir: boolean; + } + export interface CloudFileListReq { + accountID: number; + path: string; + } + export interface CloudFileSyncReq { + accountID: number; + srcPath: string; + dstPath: string; + } } diff --git a/frontend/src/api/modules/backup.ts b/frontend/src/api/modules/backup.ts index 6a306be49829..f218e4576c9b 100644 --- a/frontend/src/api/modules/backup.ts +++ b/frontend/src/api/modules/backup.ts @@ -131,3 +131,13 @@ export const deleteBackup = (params: { id: number; name: string; isPublic: boole } return http.post('/core/backups/del', { name: params.name }); }; + +export const listCloudFiles = (params: Backup.CloudFileListReq, node?: string) => { + const query = node ? `?operateNode=${node}` : ''; + return http.post>(`/backups/cloud/files${query}`, params); +}; + +export const syncCloudFileToLocal = (params: Backup.CloudFileSyncReq, node?: string) => { + const query = node ? `?operateNode=${node}` : ''; + return http.post(`/backups/cloud/sync${query}`, params); +}; diff --git a/frontend/src/assets/images/ai-agent-nemoclaw.svg b/frontend/src/assets/images/ai-agent-nemoclaw.svg new file mode 100644 index 000000000000..925003fbc227 --- /dev/null +++ b/frontend/src/assets/images/ai-agent-nemoclaw.svg @@ -0,0 +1,10 @@ + + + + + + + + + N + diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 54f9e4ec60f2..35e9ba75ef70 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -677,6 +677,7 @@ const message = { syncAgentsHelper: 'Update openclaw.json for agents using this model account', openclawType: 'OpenClaw', copawType: 'CoPaw', + nemoclawType: 'NemoClaw', appVersion: 'App Version', webuiPort: 'WebUI Port', allowedOrigins: 'Access Addresses', @@ -1993,6 +1994,13 @@ const message = { loadBucket: 'Get bucket', accountName: 'Account name', accountKey: 'Account key', + cloudFiles: 'Cloud Files', + cloudFileBrowser: 'Cloud File Browser', + cloudFileBrowserHelper: 'Browse and sync files from your cloud storage account to local storage', + syncToLocal: 'Sync to Local', + syncToLocalPath: 'Local Destination Path', + syncToLocalSuccess: 'File synced to local storage successfully', + cloudFilePath: 'Cloud File Path', address: 'Address', path: 'Path', safe: 'Security', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index c2483571a8ef..4136380de15b 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -644,6 +644,7 @@ const message = { syncAgentsHelper: '更新使用该模型账号的智能体 openclaw.json', openclawType: 'OpenClaw', copawType: 'CoPaw', + nemoclawType: 'NemoClaw', appVersion: '应用版本', webuiPort: 'WebUI 端口', allowedOrigins: '访问地址', @@ -1862,6 +1863,13 @@ const message = { loadBucket: '获取桶', accountName: '账户名称', accountKey: '账户密钥', + cloudFiles: '云文件', + cloudFileBrowser: '云存储文件浏览', + cloudFileBrowserHelper: '浏览云存储账号中的文件,并同步到本地存储', + syncToLocal: '同步到本地', + syncToLocalPath: '本地目标路径', + syncToLocalSuccess: '文件已成功同步到本地存储', + cloudFilePath: '云文件路径', address: '地址', path: '路径', backupJump: '未在当前备份列表中的备份文件,请尝试从文件目录中下载后导入备份。', diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue index ccc4a8697533..11284c50489d 100644 --- a/frontend/src/views/ai/agents/agent/add/index.vue +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -9,6 +9,7 @@ + @@ -144,7 +145,7 @@ const { isIntl } = useGlobalStore(); const form = reactive({ name: '', - agentType: 'openclaw' as 'openclaw' | 'copaw', + agentType: 'openclaw' as 'openclaw' | 'copaw' | 'nemoclaw', appVersion: '', webUIPort: 18789, allowedOrigins: '', @@ -223,7 +224,7 @@ const loadSystemIP = async () => { } }; -const loadVersions = async (appKey: 'openclaw' | 'copaw') => { +const loadVersions = async (appKey: 'openclaw' | 'copaw' | 'nemoclaw') => { const res = await getAppByKey(appKey); appInfo.value = res.data; versions.value = res.data.versions || []; @@ -320,8 +321,14 @@ const handleProviderChange = () => { }; const handleAgentTypeChange = async () => { - if (form.name === '' || form.name === 'OpenClaw' || form.name === 'CoPaw') { - form.name = form.agentType === 'copaw' ? 'CoPaw' : 'OpenClaw'; + if (form.name === '' || form.name === 'OpenClaw' || form.name === 'CoPaw' || form.name === 'NemoClaw') { + if (form.agentType === 'copaw') { + form.name = 'CoPaw'; + } else if (form.agentType === 'nemoclaw') { + form.name = 'NemoClaw'; + } else { + form.name = 'OpenClaw'; + } } form.appVersion = ''; form.model = ''; @@ -342,7 +349,11 @@ const handleAgentTypeChange = async () => { form.allowedOrigins = ''; lastAutoAllowedOrigins.value = ''; allowedOriginsAutoFilled.value = true; - await loadVersions('copaw'); + if (form.agentType === 'nemoclaw') { + await loadVersions('nemoclaw'); + } else { + await loadVersions('copaw'); + } }; const handleModelChange = () => { @@ -454,18 +465,24 @@ const handleClose = () => { allowedOriginsAutoFilled.value = true; }; -const openDrawer = async (agentType?: 'openclaw' | 'copaw') => { - const targetType = agentType === 'copaw' ? 'copaw' : 'openclaw'; - form.name = targetType === 'copaw' ? 'CoPaw' : 'OpenClaw'; +const openDrawer = async (agentType?: 'openclaw' | 'copaw' | 'nemoclaw') => { + const targetType = agentType === 'copaw' ? 'copaw' : agentType === 'nemoclaw' ? 'nemoclaw' : 'openclaw'; + if (targetType === 'copaw') { + form.name = 'CoPaw'; + } else if (targetType === 'nemoclaw') { + form.name = 'NemoClaw'; + } else { + form.name = 'OpenClaw'; + } open.value = true; manualModel.value = false; form.agentType = targetType; form.token = getRandomStr(32).toLowerCase(); - if (form.agentType === 'copaw') { + if (form.agentType === 'copaw' || form.agentType === 'nemoclaw') { form.allowedOrigins = ''; lastAutoAllowedOrigins.value = ''; allowedOriginsAutoFilled.value = true; - await loadVersions('copaw'); + await loadVersions(form.agentType); providerOptions.value = []; providerModels.value = {}; accountOptions.value = []; diff --git a/frontend/src/views/ai/agents/agent/index.vue b/frontend/src/views/ai/agents/agent/index.vue index faecf693fb1b..b446745b7bf3 100644 --- a/frontend/src/views/ai/agents/agent/index.vue +++ b/frontend/src/views/ai/agents/agent/index.vue @@ -26,13 +26,15 @@
{{ row.agentType === 'copaw' ? $t('aiTools.agents.copawType') + : row.agentType === 'nemoclaw' + ? $t('aiTools.agents.nemoclawType') : $t('aiTools.agents.openclawType') }} @@ -61,7 +63,7 @@ min-width="120" >