package service import ( "context" "encoding/json" "errors" "fmt" "os" "path" "sort" "strings" "sync" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/model" providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/docker" terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai" "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/docker/docker/api/types/container" "gopkg.in/yaml.v3" "gorm.io/gorm" ) var agentAccountMutationMu sync.Mutex type IAgentService interface { Create(req dto.AgentCreateReq) (*dto.AgentItem, error) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error) Delete(req dto.AgentDeleteReq) error ResetToken(req dto.AgentTokenResetReq) error UpdateRemark(req dto.AgentRemarkUpdateReq) error BindWebsite(req dto.AgentWebsiteBindReq) error UnbindWebsite(req dto.AgentIDReq) error GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error) RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error) GetProviders() ([]dto.ProviderInfo, error) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig, error) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, error) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) UpdateSkill(req dto.AgentSkillUpdateReq) error InstallSkill(req dto.AgentSkillInstallReq) error UninstallSkill(req dto.AgentSkillUninstallReq) error CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error) DeleteRole(req dto.AgentRoleDeleteReq) error BindRole(req dto.AgentRoleBindReq) error UnbindRole(req dto.AgentRoleBindReq) error GetConfiguredAgents(req dto.AgentConfiguredAgentsReq) ([]dto.AgentConfiguredAgentItem, error) GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.AgentRoleChannelItem, error) GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error) UpdateRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesUpdateReq) error CreateAccount(req dto.AgentAccountCreateReq) error UpdateAccount(req dto.AgentAccountUpdateReq) error SyncAgentsByAccount(account *model.AgentAccount) error PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) CreateAccountModel(req dto.AgentAccountModelCreateReq) error UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error VerifyAccount(req dto.AgentAccountVerifyReq) error DeleteAccount(req dto.AgentAccountDeleteReq) error GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error GetWecomConfig(req dto.AgentIDReq) (*dto.AgentWecomConfig, error) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error DeleteChannelConfig(req dto.AgentChannelDeleteReq) error InstallPlugin(req dto.AgentPluginInstallReq) error UpgradePlugin(req dto.AgentPluginUpgradeReq) error UninstallPlugin(req dto.AgentPluginUninstallReq) error CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error) SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error OperatePlugin(req dto.AgentPluginOperateReq) error ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error } type batchUpgradePlan struct { ctx batchAgentInstallContext req request.AppInstallUpgrade } type batchAgentInstallContext struct { agent model.Agent install model.AppInstall } const ( defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/openclaw-browser" defaultBrowserProfile = "openclaw" defaultUserTimezone = "Asia/Shanghai" defaultToolsProfile = "full" defaultToolsSessionVisibility = "all" maxCommunityAIAgents = int64(5) openclawPluginBaseDir = "/home/node/.openclaw/extensions" openclawPluginPackageTmpDir = "/tmp/openclaw-plugin" openclawManagedSkillsDir = "/home/node/.openclaw/skills" openclawGatewayPort = 18789 openclawAllowedOriginHost = "127.0.0.1" openclawHTTPSVersion = "2026.3.13" openclawHTTPVersion = "2026.3.23" openclawTrustedProxyLoopback = "127.0.0.1/32" defaultOpenclawNPMRegistry = "https://registry.npmjs.org/" ) func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { agentType := req.AgentType if err := checkPortExist(req.WebUIPort); err != nil { return nil, err } if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 { return nil, buserr.New("ErrNameIsExist") } if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 { return nil, buserr.New("ErrNameIsExist") } if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() { count, _, err := agentRepo.Page(1, 1) if err != nil { return nil, err } if count >= maxCommunityAIAgents { return nil, buserr.WithMap("ErrAgentLimitReached", map[string]interface{}{"max": maxCommunityAIAgents}, nil) } } app, err := appRepo.GetFirst(appRepo.WithKey(agentType)) if err != nil || app.ID == 0 { return nil, buserr.New("ErrRecordNotFound") } detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(req.AppVersion)) if err != nil || detail.ID == 0 { return nil, buserr.New("ErrRecordNotFound") } provider := "" baseURL := "" apiType := "" apiKey := "" runtimeModel := "" accountID := uint(0) token := "" configPath := "" storedModel := "" var allowedOrigins []string var account *model.AgentAccount var installHooks *appInstallHooks var dashboardAuth agentDashboardAuth if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent { if req.AccountID == 0 { return nil, buserr.New("ErrAgentAccountRequired") } account, err = agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) if err != nil { return nil, err } if !account.Verified { return nil, buserr.New("ErrAgentAccountNotVerified") } provider = account.Provider baseURL = account.BaseURL resolvedRuntime, err := resolveOpenclawAccountModelRuntimeByID(account, req.Model) if err != nil { return nil, err } storedModel = resolvedRuntime.StoredModel apiType = resolvedRuntime.APIType runtimeModel = resolvedRuntime.PrimaryModel apiKey = account.APIKey accountID = account.ID } if agentType == constant.AppOpenclaw { var err error allowedOrigins, err = normalizeAllowedOrigins(req.AllowedOrigins) if err != nil { return nil, err } if len(allowedOrigins) == 0 { return nil, fmt.Errorf("allowed origins is required") } token = strings.TrimSpace(req.Token) if token == "" { token = generateToken() } installHooks = &appInstallHooks{ AfterCopyData: func(appInstall *model.AppInstall) error { return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins) }, } } else if agentType == constant.AppHermesAgent { dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword) installHooks = &appInstallHooks{ AfterCopyData: func(appInstall *model.AppInstall) error { if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil { return err } return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false) }, } } else if agentType == constant.AppCopaw { dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword) } params := map[string]interface{}{ constant.CPUS: "0", constant.MemoryLimit: "0", constant.HostIP: "", } setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort) if agentType == constant.AppOpenclaw { if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" { params["ALLOWED_ORIGIN"] = allowedOrigin } params["PROVIDER"] = provider params["MODEL"] = runtimeModel params["API_TYPE"] = apiType params["BASE_URL"] = baseURL params["API_KEY"] = apiKey params["OPENCLAW_GATEWAY_TOKEN"] = token } if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok { params[usernameKey] = dashboardAuth.Username params[passwordKey] = dashboardAuth.Password if agentType == constant.AppCopaw { params[qwenPawAuthEnabledEnvKey] = "true" } } if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" { return nil, buserr.New("ErrAgentComposeRequired") } installReq := request.AppInstallCreate{ AppDetailId: detail.ID, Name: req.Name, Params: params, TaskID: req.TaskID, AppContainerConfig: request.AppContainerConfig{ Advanced: req.Advanced, ContainerName: req.ContainerName, AllowPort: req.AllowPort, SpecifyIP: req.SpecifyIP, RestartPolicy: req.RestartPolicy, CpuQuota: req.CpuQuota, MemoryLimit: req.MemoryLimit, MemoryUnit: req.MemoryUnit, PullImage: req.PullImage, EditCompose: req.EditCompose, DockerCompose: req.DockerCompose, }, } appInstall, err := AppService{}.installWithHooks(installReq, false, installHooks) if err != nil { return nil, err } if agentType == constant.AppOpenclaw { configPath = path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json") } if agentType == constant.AppHermesAgent { configPath = path.Join(appInstall.GetPath(), "data", "config.yaml") } agent := &model.Agent{ Name: req.Name, Remark: req.Remark, AgentType: agentType, Provider: provider, Model: storedModel, APIType: apiType, BaseURL: baseURL, APIKey: apiKey, Token: token, Status: appInstall.Status, Message: appInstall.Message, AppInstallID: appInstall.ID, AccountID: accountID, ConfigPath: configPath, } if err := agentRepo.Create(agent); err != nil { return nil, err } item := buildAgentItem(agent, appInstall, nil) return &item, nil } func (a AgentService) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) { accountID, err := a.ensureBatchInstallAccount(req) if err != nil { return nil, err } createReq := buildCreateReqFromBatchInstallReq(req) createReq.AccountID = accountID if strings.TrimSpace(createReq.Name) == "" { name, err := buildBatchInstallAgentName(createReq.AgentType) if err != nil { return nil, err } createReq.Name = name } if createReq.AgentType == constant.AppOpenclaw && len(createReq.AllowedOrigins) == 0 { allowedOrigin, err := buildBatchInstallOpenclawAllowedOrigin(req) if err != nil { return nil, err } createReq.AllowedOrigins = []string{allowedOrigin} } return a.Create(createReq) } func (a AgentService) BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error) { plans, results, err := buildBatchUpgradePlans(req) if err != nil { return nil, err } for _, plan := range plans { result := dto.AgentBatchUpgradeResult{ AgentID: plan.ctx.agent.ID, AgentName: plan.ctx.agent.Name, AppInstallID: plan.ctx.agent.AppInstallID, } if err := upgradeInstall(plan.req); err != nil { result.Message = err.Error() } else { result.Success = true } results = append(results, result) } return results, nil } func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error) { if req.AgentType != constant.AppOpenclaw && req.AgentType != constant.AppHermesAgent { return nil, fmt.Errorf("%s does not support skill batch install", req.AgentType) } packagePath, err := validateLocalSkillPackagePath(req.PackagePath) if err != nil { return nil, err } if format, err := localSkillPackageFormat(packagePath); err != nil { return nil, err } else if format != "zip" { return nil, fmt.Errorf("only .zip skill packages can be installed") } skillName := sanitizeLocalSkillDirName(req.SkillName, packagePath, req.SkillName) agents, err := listBatchAgents(req.AgentType) if err != nil { return nil, err } results := make([]dto.AgentBatchSkillInstallResult, 0, len(agents)) for _, agent := range agents { result := dto.AgentBatchSkillInstallResult{ AgentID: agent.ID, AgentName: agent.Name, AppInstallID: agent.AppInstallID, } ctx, message := loadBatchAgentInstall(agent, req.AgentType) if message != "" { result.Message = message results = append(results, result) continue } install := ctx.install result.AppInstallID = install.ID if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading { result.Message = fmt.Sprintf("agent status is %s", install.Status) results = append(results, result) continue } if err := ensureContainerRunning(install.ContainerName); err != nil { result.Message = err.Error() results = append(results, result) continue } installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID) if err != nil { result.Message = err.Error() results = append(results, result) continue } currentAgent := ctx.agent currentInstall := ctx.install installTask.AddSubTask("Install local skill", func(t *task.Task) error { mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute)) return installLocalSkillPackage(mgr, currentInstall.ContainerName, currentAgent.AgentType, currentAgent.ConfigPath, packagePath, skillName) }, nil) go func() { if err := installTask.Execute(); err != nil { global.LOG.Errorf("batch install local skill failed: %v", err) } }() result.Success = true results = append(results, result) } return results, nil } func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error) { operate := constant.AppOperate(strings.TrimSpace(req.Operate)) if operate != constant.Start && operate != constant.Stop && operate != constant.Restart && operate != constant.Delete { return nil, fmt.Errorf("operate %s is not supported", req.Operate) } agents, err := listBatchAgents(req.AgentType) if err != nil { return nil, err } results := make([]dto.AgentBatchOperateResult, 0, len(agents)) for _, agent := range agents { result := dto.AgentBatchOperateResult{ AgentID: agent.ID, AgentName: agent.Name, AppInstallID: agent.AppInstallID, } if operate == constant.Delete { if err := a.Delete(dto.AgentDeleteReq{ ID: agent.ID, TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), ForceDelete: req.ForceDelete, }); err != nil { result.Message = err.Error() } else { result.Success = true } results = append(results, result) continue } ctx, message := loadBatchAgentInstall(agent, req.AgentType) if message != "" { result.Message = message results = append(results, result) continue } install := ctx.install result.AppInstallID = install.ID if message := batchOperateSkipMessage(operate, install.Status); message != "" { result.Success = true result.Skipped = true result.Message = message results = append(results, result) continue } if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{ InstallId: install.ID, Operate: operate, TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), }); err != nil { result.Message = err.Error() } else { result.Success = true } results = append(results, result) } return results, nil } func listBatchAgents(agentType string) ([]model.Agent, error) { return agentRepo.List(func(db *gorm.DB) *gorm.DB { return db.Where("agent_type = ?", agentType).Order("id ASC") }) } func loadBatchAgentInstall(agent model.Agent, agentType string) (batchAgentInstallContext, string) { ctx := batchAgentInstallContext{agent: agent} if agent.AppInstallID == 0 { return ctx, "agent app install id is empty" } install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID)) if err != nil { return ctx, err.Error() } ctx.install = install if install.App.Key != agentType { return ctx, fmt.Sprintf("app key %s does not match agent type %s", install.App.Key, agentType) } return ctx, "" } func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, []dto.AgentBatchUpgradeResult, error) { agents, err := listBatchAgents(req.AgentType) if err != nil { return nil, nil, err } plans := make([]batchUpgradePlan, 0, len(agents)) results := make([]dto.AgentBatchUpgradeResult, 0) for _, agent := range agents { result := dto.AgentBatchUpgradeResult{ AgentID: agent.ID, AgentName: agent.Name, AppInstallID: agent.AppInstallID, } ctx, message := loadBatchAgentInstall(agent, req.AgentType) if message != "" { result.Message = message results = append(results, result) continue } install := ctx.install result.AppInstallID = install.ID if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading { result.Message = fmt.Sprintf("agent status is %s", install.Status) results = append(results, result) continue } if install.Version == req.TargetVersion { result.Success = true result.Skipped = true result.Message = "already target version" results = append(results, result) continue } detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(install.AppId), appDetailRepo.WithVersion(req.TargetVersion)) if err != nil { result.Message = err.Error() results = append(results, result) continue } if detail.ID == 0 { result.Message = fmt.Sprintf("target version %s not found", req.TargetVersion) results = append(results, result) continue } plans = append(plans, batchUpgradePlan{ ctx: ctx, req: request.AppInstallUpgrade{ InstallID: install.ID, DetailID: detail.ID, Backup: req.Backup, PullImage: req.PullImage, TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID), }, }) } return plans, results, nil } func buildBatchTaskID(taskID, prefix string, id uint) string { taskID = strings.TrimSpace(taskID) if taskID == "" { taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano()) } return fmt.Sprintf("%s-%d", taskID, id) } func batchOperateSkipMessage(operate constant.AppOperate, status string) string { switch status { case constant.StatusInstalling, constant.StatusUpgrading, constant.StatusUninstalling, constant.StatusRebuilding: return fmt.Sprintf("agent status is %s", status) } switch operate { case constant.Start: if status == constant.StatusRunning || status == constant.StatusStarting || status == constant.StatusRestarting { return fmt.Sprintf("agent status is %s", status) } case constant.Stop: if status != constant.StatusRunning { return fmt.Sprintf("agent status is %s", status) } case constant.Restart: if status == constant.StatusStarting { return fmt.Sprintf("agent status is %s", status) } } return "" } func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq { return dto.AgentCreateReq{ Name: req.Name, Remark: req.Remark, AppVersion: req.AppVersion, WebUIPort: req.WebUIPort, BridgePort: req.BridgePort, AllowedOrigins: req.AllowedOrigins, AgentType: req.AgentType, Model: req.Model, AccountID: req.AccountID, Token: req.Token, DashboardUsername: req.DashboardUsername, DashboardPassword: req.DashboardPassword, TaskID: req.TaskID, Advanced: req.Advanced, ContainerName: req.ContainerName, AllowPort: req.AllowPort, SpecifyIP: req.SpecifyIP, RestartPolicy: req.RestartPolicy, CpuQuota: req.CpuQuota, MemoryLimit: req.MemoryLimit, MemoryUnit: req.MemoryUnit, PullImage: req.PullImage, EditCompose: req.EditCompose, DockerCompose: req.DockerCompose, } } func buildBatchInstallAgentName(agentType string) (string, error) { prefix := strings.ToLower(strings.TrimSpace(agentType)) if prefix == "" { prefix = "agent" } for i := 1; i <= 10000; i++ { name := fmt.Sprintf("%s-%d", prefix, i) exists, err := agentNameExists(name) if err != nil { return "", err } if !exists { return name, nil } } return "", fmt.Errorf("no available agent name") } func agentNameExists(name string) (bool, error) { if exist, err := agentRepo.GetFirst(repo.WithByLowerName(name)); err == nil && exist != nil && exist.ID > 0 { return true, nil } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return false, err } installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(name)) if err != nil { return false, err } return len(installs) > 0, nil } func buildBatchInstallOpenclawAllowedOrigin(req dto.AgentBatchInstallReq) (string, error) { host := "" if value, err := settingRepo.GetValueByKey("SystemIP"); err == nil { host = strings.TrimSpace(value) } if host == "" { host = strings.TrimSpace(req.FallbackAccessHost) } if host == "" { host = "127.0.0.1" } return buildOpenclawAllowedOrigin(openclawAllowedOriginScheme(req.AppVersion), host, req.WebUIPort) } func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (uint, error) { if !requiresAgentAccount(req.AgentType) { return 0, nil } if req.MasterAccountID == 0 { return 0, buserr.New("ErrInvalidParams") } snapshot := req.AccountSnapshot account, err := agentAccountRepo.GetFirst(agentAccountRepo.WithByMasterAccountID(req.MasterAccountID)) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return 0, err } if account == nil && global.IsMaster { account, err = agentAccountRepo.GetFirst(repo.WithByID(req.MasterAccountID)) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return 0, err } } if account == nil { account = &model.AgentAccount{ MasterAccountID: req.MasterAccountID, } } account.MasterAccountID = req.MasterAccountID account.Provider = snapshot.Provider account.Name = snapshot.Name account.APIKey = snapshot.APIKey account.RememberAPIKey = snapshot.RememberAPIKey account.BaseURL = snapshot.BaseURL account.APIType = snapshot.APIType account.AuthMode = snapshot.AuthMode account.Remark = snapshot.Remark account.Verified = true initialModels, err := buildInitialAgentAccountModels(account, req.AccountModels) if err != nil { return 0, err } verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels) if err != nil { return 0, err } account.VerifyModel = verifyModel if err := global.DB.Transaction(func(tx *gorm.DB) error { if account.ID == 0 { if err := tx.Create(account).Error; err != nil { return err } } else { if err := tx.Save(account).Error; err != nil { return err } } return replacePersistedAgentAccountModelsWithTx(tx, account.ID, initialModels) }); err != nil { return 0, err } return account.ID, nil } func requiresAgentAccount(agentType string) bool { return agentType != constant.AppCopaw } func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) { if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) { params["PANEL_APP_PORT_HTTPS"] = webUIPort return } params["PANEL_APP_PORT_HTTP"] = webUIPort } func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) { var opts []repo.DBOption if strings.TrimSpace(req.Info) != "" { opts = append(opts, repo.WithByLikeName(req.Info)) } count, list, err := agentRepo.Page(req.Page, req.PageSize, opts...) if err != nil { return 0, nil, err } items := make([]dto.AgentItem, 0, len(list)) appInstalls := make([]model.AppInstall, 0, len(list)) for _, item := range list { appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID)) appInstalls = append(appInstalls, appInstall) } syncAgentAppInstalls(appInstalls) for index, item := range list { appInstall := appInstalls[index] envMap := readInstallEnv(appInstall.Env) agentItem := buildAgentItem(&item, &appInstall, envMap) agentItem.Upgradable = checkAgentUpgradable(appInstall) items = append(items, agentItem) } if err := hydrateAgentWebsiteItems(items); err != nil { return 0, nil, err } return count, items, nil } func (a AgentService) Delete(req dto.AgentDeleteReq) error { agent, err := agentRepo.GetFirst(repo.WithByID(req.ID)) if err != nil { return err } resources, err := a.deleteCheckByAgent(agent) if err != nil { return err } if len(resources) > 0 { return buserr.New("ErrAgentWebsiteBound") } if agent.AppInstallID == 0 { return agentRepo.DeleteByID(agent.ID) } operate := request.AppInstalledOperate{ InstallId: agent.AppInstallID, Operate: constant.Delete, TaskID: req.TaskID, ForceDelete: req.ForceDelete, } if err := NewIAppInstalledService().Operate(operate); err != nil { return err } return nil } func (a AgentService) DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error) { agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID)) if err != nil { return nil, err } return a.deleteCheckByAgent(agent) } func (a AgentService) deleteCheckByAgent(agent *model.Agent) ([]dto.AppResource, error) { if agent == nil || agent.WebsiteID == 0 { return nil, nil } website, err := websiteRepo.GetFirst(repo.WithByID(agent.WebsiteID)) if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } websiteName, err := loadAgentWebsiteResourceName(website) if err != nil { return nil, err } return []dto.AppResource{{Type: "website", Name: websiteName}}, nil } func syncAgentAppInstalls(appInstalls []model.AppInstall) { if len(appInstalls) == 0 { return } var containersMap map[string]container.Summary cli, err := docker.NewClient() if err == nil { defer cli.Close() containers, err := cli.ListAllContainers() if err == nil { containersMap = make(map[string]container.Summary, len(containers)) for _, contain := range containers { containersMap[contain.Names[0]] = contain } } } for index := range appInstalls { if appInstalls[index].ID == 0 || doNotNeedSync(appInstalls[index]) { continue } synAppInstall(containersMap, &appInstalls[index], false) } } func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error { agent, err := loadOpenclawAgentByID(req.ID) if err != nil { return err } conf, err := readOpenclawConfig(agent.ConfigPath) if err != nil { return err } newToken := generateToken() if newToken == "" { return fmt.Errorf("generate token failed") } gatewayMap := ensureChildMap(conf, "gateway") authMap := ensureChildMap(gatewayMap, "auth") if _, ok := authMap["mode"]; !ok { authMap["mode"] = "token" } authMap["token"] = newToken if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil { return err } agent.Token = newToken return agentRepo.Save(agent) } func (a AgentService) UpdateRemark(req dto.AgentRemarkUpdateReq) error { agent, err := agentRepo.GetFirst(repo.WithByID(req.ID)) if err != nil { return err } agent.Remark = req.Remark return agentRepo.Save(agent) } func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) { agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID)) if err != nil { return nil, err } if agent.AgentType == constant.AppHermesAgent { cfg, err := readHermesConfig(agent.ConfigPath) if err != nil { return nil, err } account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID)) if err != nil { return nil, err } accountModels, err := loadAgentAccountModels(account) if err != nil { return nil, err } model, err := resolveHermesConfiguredModelID(account, accountModels, cfg.Model.Default) if err != nil { return nil, err } return &dto.AgentModelConfig{ AccountID: agent.AccountID, Model: model, Fallbacks: []string{}, }, nil } agent, _, conf, err := a.loadOpenclawAgentConfig(req.AgentID) if err != nil { return nil, err } account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID)) if err != nil { return nil, err } models, err := loadAgentAccountModels(account) if err != nil { return nil, err } model := extractOpenclawPrimaryModelID(conf, account, models) if model == "" { model = agent.Model } return &dto.AgentModelConfig{ AccountID: agent.AccountID, Model: model, Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model), }, nil } func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error { agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID)) if err != nil { return err } account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) if err != nil { return err } resolvedRuntime, err := resolveOpenclawAccountModelRuntimeByID(account, req.Model) if err != nil { return err } modelName := resolvedRuntime.StoredModel apiType := resolvedRuntime.APIType confDir := path.Dir(agent.ConfigPath) if agent.AgentType == constant.AppHermesAgent { cfg, err := readHermesConfig(agent.ConfigPath) if err != nil { return err } if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil { return err } } else { if agent.AgentType != constant.AppOpenclaw { return fmt.Errorf("%s does not support", agent.AgentType) } if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil { return err } } agent.Provider = account.Provider agent.Model = modelName agent.APIType = apiType agent.BaseURL = account.BaseURL agent.APIKey = account.APIKey agent.AccountID = account.ID return agentRepo.Save(agent) } func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) { definitions := providercatalog.All() providers := make([]dto.ProviderInfo, 0, len(definitions)) for key, def := range definitions { models := make([]dto.ProviderModelInfo, 0, len(def.Models)) for _, item := range def.Models { models = append(models, dto.ProviderModelInfo{ ID: item.ID, Name: item.Name, }) } apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs)) for _, item := range def.APIConfigs { apiModels := make([]dto.ProviderModelInfo, 0, len(item.Models)) for _, model := range item.Models { apiModels = append(apiModels, dto.ProviderModelInfo{ ID: model.ID, Name: model.Name, }) } apiTypes = append(apiTypes, dto.ProviderAPIInfo{ APIType: item.APIType, BaseURL: item.BaseURL, EditableBaseURL: item.EditableBaseURL, SupportsModelDiscovery: item.DiscoverModels, DefaultAuthMode: item.DefaultAuthMode, AuthModes: item.AuthModes, Models: apiModels, }) } baseURL, _ := providercatalog.DefaultBaseURL(key) providers = append(providers, dto.ProviderInfo{ Sort: def.Sort, Provider: key, DisplayName: localizedAgentProviderName(key), BaseURL: baseURL, DefaultAPIType: def.DefaultAPIType, APITypes: apiTypes, Models: models, }) } sort.Slice(providers, func(i, j int) bool { return providers[i].Sort < providers[j].Sort }) return providers, nil } func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { agentAccountMutationMu.Lock() defer agentAccountMutationMu.Unlock() provider := req.Provider if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil { return err } initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models) if err != nil { return err } verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels) if err != nil { return err } validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability) if err != nil { return err } account := &model.AgentAccount{ Provider: resolvedInput.Provider, Name: req.Name, APIKey: resolvedInput.APIKey, RememberAPIKey: req.RememberAPIKey, BaseURL: resolvedInput.BaseURL, APIType: resolvedInput.APIType, AuthMode: resolvedInput.AuthMode, VerifyModel: verifyModel, Verified: true, Remark: req.Remark, } if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Create(account).Error; err != nil { return err } if len(initialModels) == 0 { return nil } return replacePersistedAgentAccountModelsWithTx(tx, account.ID, initialModels) }); err != nil { return err } asyncReportAIProviderInstall(provider) return nil } func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { agentAccountMutationMu.Lock() defer agentAccountMutationMu.Unlock() account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID)) if err != nil { return err } if req.APIType != account.APIType { return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil) } provider := account.Provider if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil { return err } models, err := loadAgentAccountModels(account) if err != nil { return err } requestedVerifyModel := req.VerifyModel if strings.TrimSpace(requestedVerifyModel) == "" { requestedVerifyModel = account.VerifyModel } verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models) if err != nil { return err } validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability) if err != nil { return err } account.Name = req.Name account.APIKey = resolvedInput.APIKey account.RememberAPIKey = req.RememberAPIKey account.BaseURL = resolvedInput.BaseURL account.APIType = resolvedInput.APIType account.AuthMode = resolvedInput.AuthMode account.VerifyModel = verifyModel account.Remark = req.Remark account.Verified = true if err := global.DB.Save(account).Error; err != nil { return err } terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateFileAIRuntimeCache() if req.SyncAgents { if err := a.syncAgentsByAccount(account); err != nil { return err } } return nil } func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) { var opts []repo.DBOption if strings.TrimSpace(req.Provider) != "" { opts = append(opts, repo.WithByProvider(req.Provider)) } if apiType := strings.TrimSpace(req.APIType); apiType != "" { opts = append(opts, repo.WithByAPIType(apiType)) } if req.TextOnly { opts = append(opts, repo.WithTextAPIType()) } if strings.TrimSpace(req.Name) != "" { opts = append(opts, repo.WithByLikeName(req.Name)) } count, list, err := agentAccountRepo.Page(req.Page, req.PageSize, opts...) if err != nil { return 0, nil, err } items := make([]dto.AgentAccountInfo, 0, len(list)) for _, item := range list { apiKey := "" if item.RememberAPIKey { apiKey = item.APIKey } items = append(items, dto.AgentAccountInfo{ ID: item.ID, MasterAccountID: item.MasterAccountID, Provider: item.Provider, ProviderName: localizedAgentProviderName(item.Provider), Name: item.Name, APIKey: apiKey, RememberAPIKey: item.RememberAPIKey, BaseURL: item.BaseURL, Models: nil, APIType: item.APIType, AuthMode: item.AuthMode, VerifyModel: item.VerifyModel, Verified: item.Verified, Remark: item.Remark, CreatedAt: item.CreatedAt, }) } if len(list) > 0 { accountIDs := make([]uint, 0, len(list)) for _, account := range list { accountIDs = append(accountIDs, account.ID) } var rows []model.AgentAccountModel if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil { return 0, nil, err } modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list)) for _, row := range rows { modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{ RecordID: row.ID, ID: strings.TrimSpace(row.Model), Name: strings.TrimSpace(row.Name), }) } for index, account := range list { items[index].Models = modelsByAccount[account.ID] } } return count, items, nil } func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) { return agentAccountRepo.CountTextByProviders(req.Providers) } func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) { account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) if err != nil { return nil, err } return loadAgentAccountModels(account) } func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) { config, ok := providercatalog.FindAPIConfig(req.Provider, req.APIType) if !ok || !config.DiscoverModels { return nil, buserr.New("ErrAgentAccountModelsRequired") } baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL) if err != nil { return nil, buserr.WithErr("ErrAgentAccountUnavailable", err) } models, err := providercatalog.DiscoverModels(baseURL, req.APIKey) if err != nil { return nil, buserr.WithErr("ErrAgentAccountUnavailable", err) } return buildDiscoveredAgentAccountModels(models), nil } func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error { account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) if err != nil { return err } models, err := loadAgentAccountModels(account) if err != nil { return err } nextModel, err := normalizeAgentAccountModel(account, req.Model) if err != nil { return err } if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok { return buserr.New("ErrRecordExist") } sortOrder := len(models) + 1 record := &model.AgentAccountModel{ AccountID: account.ID, Model: nextModel.ID, Name: nextModel.Name, SortOrder: sortOrder, } if err := agentAccountModelRepo.Create(record); err != nil { return err } return a.syncAgentsByAccount(account) } func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error { account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) if err != nil { return err } record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.Model.RecordID), repo.WithByAccountID(req.AccountID)) if err != nil { return err } models, err := loadAgentAccountModels(account) if err != nil { return err } nextModel, err := normalizeAgentAccountModel(account, req.Model) if err != nil { return err } nextModel.RecordID = req.Model.RecordID for _, item := range models { if item.RecordID == req.Model.RecordID { continue } if sameProviderModelID(account.Provider, item.ID, nextModel.ID) { return buserr.New("ErrRecordExist") } } nextModels := make([]dto.AgentAccountModel, 0, len(models)) for _, item := range models { if item.RecordID == req.Model.RecordID { nextModels = append(nextModels, nextModel) continue } nextModels = append(nextModels, item) } if err := ensureAccountModelsNotBound(account, nextModels); err != nil { return err } previousModelID := record.Model record.Model = nextModel.ID record.Name = nextModel.Name if err := agentAccountModelRepo.Save(record); err != nil { return err } if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) { account.VerifyModel = nextModel.ID if err := agentAccountRepo.Save(account); err != nil { return err } } terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateFileAIRuntimeCache() return a.syncAgentsByAccount(account) } func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error { account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) if err != nil { return err } record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)) if err != nil { return err } if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) { return buserr.New("ErrAgentVerifyModelInUse") } models, err := loadAgentAccountModels(account) if err != nil { return err } nextModels := make([]dto.AgentAccountModel, 0, len(models)) for _, item := range models { if item.RecordID == req.RecordID { continue } nextModels = append(nextModels, item) } if err := ensureAccountModelsNotBound(account, nextModels); err != nil { return err } if err := agentAccountModelRepo.DeleteByID(req.RecordID); err != nil { return err } if err := compactPersistedAgentAccountModelSortOrder(req.AccountID); err != nil { return err } terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateFileAIRuntimeCache() return a.syncAgentsByAccount(account) } func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error { if account == nil || account.ID == 0 { return nil } return a.syncAgentsByAccount(account) } func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error { _, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true) return err } func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { agentAccountMutationMu.Lock() defer agentAccountMutationMu.Unlock() if err := global.DB.Transaction(func(tx *gorm.DB) error { var agentCount int64 if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil { return err } if agentCount > 0 { return buserr.New("ErrAgentAccountBound") } used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID") if err != nil { return err } if used { return buserr.New("ErrTerminalAIAccountInUse") } used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID") if err != nil { return err } if used { return buserr.New("ErrFileAIAccountInUse") } if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil { return err } return tx.Delete(&model.AgentAccount{}, req.ID).Error }); err != nil { return err } terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateFileAIRuntimeCache() return nil } func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) { agent, _, err := a.loadOpenclawAgentAndInstall(req.AgentID) if err != nil { return nil, err } conf, err := readOpenclawConfig(agent.ConfigPath) if err != nil { return nil, err } result := extractSecurityConfig(conf) return &result, nil } func (a AgentService) UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error { agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID) if err != nil { return err } allowedOrigins, err := normalizeAllowedOrigins(req.AllowedOrigins) if err != nil { return err } if len(allowedOrigins) == 0 { return fmt.Errorf("allowed origins is required") } conf, err := readOpenclawConfig(agent.ConfigPath) if err != nil { return err } setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins}) if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil { return err } if err := syncOpenclawAllowedOriginEnv(install, allowedOrigins); err != nil { return err } return appInstallRepo.Save(context.Background(), install) } func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig, error) { agent, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { return nil, err } if agent.AgentType == constant.AppHermesAgent { cfg, err := readHermesConfig(agent.ConfigPath) if err != nil { return nil, err } auth := readAgentDashboardAuthFromInstall(install, agent.AgentType) return &dto.AgentOtherConfig{ UserTimezone: cfg.Timezone, BrowserEnabled: true, NPMRegistry: "https://registry.npmjs.org/", DashboardUsername: auth.Username, DashboardPassword: auth.Password, }, nil } if agent.AgentType == constant.AppCopaw { auth := readAgentDashboardAuthFromInstall(install, agent.AgentType) return &dto.AgentOtherConfig{ DashboardUsername: auth.Username, DashboardPassword: auth.Password, }, nil } conf, err := readOpenclawConfig(agent.ConfigPath) if err != nil { return nil, err } result := extractOtherConfig(conf) npmRegistry, err := getOpenclawNPMRegistry(install.ContainerName) if err == nil { result.NPMRegistry = npmRegistry } return &result, nil } func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error { agent, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { return err } if agent.AgentType == constant.AppHermesAgent { if strings.TrimSpace(req.UserTimezone) == "" { return buserr.New("ErrInvalidParams") } account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID)) if err != nil { return err } previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType) nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword) if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil { return err } if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil { return err } operate := constant.Restart if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password { operate = constant.Rebuild } return NewIAppInstalledService().Operate(request.AppInstalledOperate{ InstallId: install.ID, Operate: operate, }) } if agent.AgentType == constant.AppCopaw { return updateQwenPawDashboardAuth(install, normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)) } if strings.TrimSpace(req.UserTimezone) == "" || strings.TrimSpace(req.NPMRegistry) == "" { return buserr.New("ErrInvalidParams") } if err := ensureContainerRunning(install.ContainerName); err != nil { return err } conf, err := readOpenclawConfig(agent.ConfigPath) if err != nil { return err } setOtherConfig(conf, dto.AgentOtherConfig{ UserTimezone: strings.TrimSpace(req.UserTimezone), BrowserEnabled: req.BrowserEnabled, }) if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil { return err } if err := setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry); err != nil { return err } return nil } func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) { agent, _, err := a.loadAgentAndInstall(req.AgentID) if err != nil { return nil, err } content, err := os.ReadFile(agent.ConfigPath) if err != nil { return nil, err } return &dto.AgentConfigFile{Content: string(content)}, nil } func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error { agent, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { return err } if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil { return err } info, err := os.Stat(agent.ConfigPath) if err != nil { return err } if err := os.WriteFile(agent.ConfigPath, []byte(req.Content), info.Mode()); err != nil { return err } if agent.AgentType == constant.AppHermesAgent { cfg, err := readHermesConfig(agent.ConfigPath) if err != nil { return err } if cfg.Model.Default != "" { agent.Model = cfg.Model.Default if err := agentRepo.Save(agent); err != nil { return err } } } return NewIAppInstalledService().Operate(request.AppInstalledOperate{ InstallId: install.ID, Operate: constant.Restart, }) } func validateAgentConfigFileContent(agentType, content string) error { var payload interface{} if agentType == constant.AppHermesAgent { return yaml.Unmarshal([]byte(content), &payload) } return json.Unmarshal([]byte(content), &payload) } func getOpenclawNPMRegistry(containerName string) (string, error) { registry, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "npm", "get", "registry") if err != nil { return "", err } registry = strings.TrimSpace(registry) if registry == "" { return defaultOpenclawNPMRegistry, nil } return registry, nil } func setOpenclawNPMRegistry(containerName, registry string) error { return cmd.NewCommandMgr().Run("docker", "exec", containerName, "npm", "set", "registry", registry) } func (a AgentService) loadAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) { agent, err := agentRepo.GetFirst(repo.WithByID(agentID)) if err != nil { return nil, nil, err } if agent.AppInstallID == 0 { return nil, nil, buserr.New("ErrRecordNotFound") } install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID)) if err != nil { return nil, nil, err } return agent, &install, nil } func (a AgentService) loadOpenclawAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) { agent, install, err := a.loadAgentAndInstall(agentID) if err != nil { return nil, nil, err } if agent.AgentType != constant.AppOpenclaw { return nil, nil, fmt.Errorf("%s does not support", agent.AgentType) } return agent, install, nil } func (a AgentService) loadAgentConfig(agentID uint) (*model.Agent, *model.AppInstall, map[string]interface{}, error) { agent, install, err := a.loadAgentAndInstall(agentID) if err != nil { return nil, nil, nil, err } conf, err := readOpenclawConfig(agent.ConfigPath) if err != nil { return nil, nil, nil, err } return agent, install, conf, nil } func (a AgentService) loadOpenclawAgentConfig(agentID uint) (*model.Agent, *model.AppInstall, map[string]interface{}, error) { agent, install, conf, err := a.loadAgentConfig(agentID) if err != nil { return nil, nil, nil, err } if agent.AgentType != constant.AppOpenclaw { return nil, nil, nil, fmt.Errorf("%s does not support", agent.AgentType) } return agent, install, conf, nil } func (a AgentService) mutateAgentConfig(agentID uint, fn func(agent *model.Agent, install *model.AppInstall, conf map[string]interface{}) error) error { agent, install, conf, err := a.loadAgentConfig(agentID) if err != nil { return err } if err := fn(agent, install, conf); err != nil { return err } return writeOpenclawConfigRaw(agent.ConfigPath, conf) } func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { agents, err := agentRepo.List(repo.WithByAccountID(account.ID)) if err != nil { return err } accountModels, err := loadAgentAccountModels(account) if err != nil { return err } if len(accountModels) == 0 { return nil } for _, agent := range agents { selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, agent.Model) if err != nil { return buserr.WithName("ErrAgentModelInUse", agent.Name) } resolvedRuntime, err := buildOpenclawAccountModelRuntime(account, selectedAccountModel) if err != nil { return err } modelName := resolvedRuntime.StoredModel apiType := resolvedRuntime.APIType confDir := path.Dir(agent.ConfigPath) switch agent.AgentType { case constant.AppOpenclaw: conf, err := readOpenclawConfig(agent.ConfigPath) if err != nil { return err } fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID) if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil { return err } case constant.AppHermesAgent: cfg, err := readHermesConfig(agent.ConfigPath) if err != nil { return err } if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil { return err } default: continue } agent.BaseURL = account.BaseURL agent.APIKey = account.APIKey agent.Provider = account.Provider agent.Model = modelName agent.APIType = apiType _ = agentRepo.Save(&agent) } return nil }