package service import ( "os" "path" "sort" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "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" ) var agentMarkdownFileNames = []string{"AGENTS.md", "SOUL.md", "USER.md", "IDENTITY.md", "TOOLS.md", "HEARTBEAT.md", "BOOT.md", "BOOTSTRAP.md"} func (a AgentService) CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error) { _, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { return nil, err } args := []string{"openclaw", "agents", "add", req.Name} workspace := "/home/node/.openclaw/workspace-agent_" + req.Name agentDir := "/home/node/.openclaw/agents/" + req.Name args = append(args, "--workspace", workspace) if model := strings.TrimSpace(req.Model); model != "" { args = append(args, "--model", model) } for _, binding := range req.Bindings { channel := binding.Channel if channel == "" { continue } if accountID := binding.AccountID; accountID != "" { channel = channel + ":" + accountID } args = append(args, "--bind", channel) } args = append(args, "--agent-dir", agentDir) args = append(args, "--non-interactive", "--json") output, err := cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, args...) if err != nil { return nil, err } return &dto.AgentRoleCreateResp{Output: strings.TrimSpace(output)}, nil } func (a AgentService) GetConfiguredAgents(req dto.AgentConfiguredAgentsReq) ([]dto.AgentConfiguredAgentItem, error) { agent, _, conf, err := a.loadAgentConfig(req.AgentID) if err != nil { return nil, err } agents, ok := conf["agents"].(map[string]interface{}) if !ok { return []dto.AgentConfiguredAgentItem{}, nil } rawList, ok := agents["list"].([]interface{}) if !ok { return []dto.AgentConfiguredAgentItem{}, nil } result := make([]dto.AgentConfiguredAgentItem, 0, len(rawList)) baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data") for _, item := range rawList { record, ok := item.(map[string]interface{}) if !ok { continue } configured := extractConfiguredAgentItem(baseDir, record) if strings.EqualFold(configured.ID, "main") { continue } result = append(result, configured) } applyConfiguredAgentBindings(result, conf["bindings"]) return result, nil } func (a AgentService) GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.AgentRoleChannelItem, error) { _, _, conf, err := a.loadAgentConfig(req.AgentID) if err != nil { return nil, err } channels, ok := conf["channels"].(map[string]interface{}) if !ok || len(channels) == 0 { return []dto.AgentRoleChannelItem{}, nil } boundBindings := loadBoundChannelBindings(conf["bindings"]) result := make([]dto.AgentRoleChannelItem, 0, len(channels)) for key := range channels { if key == "" { continue } accountIDs := extractRoleChannelAccountIDs(conf, key) availableAccountIDs := filterAvailableChannelAccountIDs(boundBindings, key, accountIDs) result = append(result, dto.AgentRoleChannelItem{ Name: key, Bound: isRoleChannelFullyBound(boundBindings, key, accountIDs, availableAccountIDs), AccountIDs: availableAccountIDs, }) } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) return result, nil } func (a AgentService) DeleteRole(req dto.AgentRoleDeleteReq) error { agent, install, conf, err := a.loadAgentConfig(req.AgentID) if err != nil { return err } baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data") if req.ID == "" { return buserr.New("ErrRecordNotFound") } target, ok := findConfiguredAgentByID(baseDir, conf, req.ID) if !ok { return buserr.New("ErrRecordNotFound") } args := []string{"openclaw", "agents", "delete", req.ID, "--force"} if _, err = cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, args...); err != nil { return err } if target.Workspace != "" { if err := os.RemoveAll(target.Workspace); err != nil { return err } } if target.AgentDir != "" { if err := os.RemoveAll(target.AgentDir); err != nil { return err } } return nil } func (a AgentService) BindRole(req dto.AgentRoleBindReq) error { return a.operateRoleBinding(req, "bind") } func (a AgentService) UnbindRole(req dto.AgentRoleBindReq) error { return a.operateRoleBinding(req, "unbind") } func (a AgentService) operateRoleBinding(req dto.AgentRoleBindReq, action string) error { agent, install, conf, err := a.loadAgentConfig(req.AgentID) if err != nil { return err } baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data") if req.ID == "" { return buserr.New("ErrRecordNotFound") } if _, ok := findConfiguredAgentByID(baseDir, conf, req.ID); !ok { return buserr.New("ErrRecordNotFound") } binding := formatRoleBinding(req.Channel, req.AccountID) if binding == "" { return buserr.New("ErrInvalidParams") } args := []string{ "openclaw", "agents", action, "--agent", req.ID, "--bind", binding, } args = append(args, "--json") _, err = cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, args...) return err } func (a AgentService) GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error) { agent, err := loadOpenclawAgentByID(req.AgentID) if err != nil { return nil, err } baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data") workspaceDir, err := resolveMarkdownWorkspaceDir(baseDir, req.Workspace) if err != nil { return nil, err } items := make([]dto.AgentRoleMarkdownFileItem, 0, len(agentMarkdownFileNames)) for _, name := range agentMarkdownFileNames { item := dto.AgentRoleMarkdownFileItem{ Name: name, } content, readErr := os.ReadFile(path.Join(workspaceDir, name)) if readErr == nil { item.Content = string(content) } else if !os.IsNotExist(readErr) { return nil, readErr } items = append(items, item) } return items, nil } func (a AgentService) UpdateRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesUpdateReq) error { agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID) if err != nil { return err } baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data") dirPath, err := resolveMarkdownWorkspaceDir(baseDir, req.Workspace) if err != nil { return err } if err = os.MkdirAll(dirPath, 0755); err != nil { return err } for _, item := range req.Files { file := path.Join(dirPath, item.Name) if err = os.WriteFile(file, []byte(item.Content), 0644); err != nil { return err } } if req.Restart { return NewIAppInstalledService().Operate(request.AppInstalledOperate{ InstallId: install.ID, Operate: constant.Restart, }) } return nil } func extractConfiguredAgentItem(installDir string, record map[string]interface{}) dto.AgentConfiguredAgentItem { item := dto.AgentConfiguredAgentItem{Bindings: []dto.AgentRoleBinding{}} if id, ok := record["id"].(string); ok { item.ID = id } else if id, ok := record["agentId"].(string); ok { item.ID = id } if name, ok := record["name"].(string); ok { item.Name = name } if workspace, ok := record["workspace"].(string); ok { item.Workspace = resolveRoleDir(installDir, workspace) } if model, ok := record["model"].(string); ok { item.Model = model } if agentDir, ok := record["agentDir"].(string); ok { item.AgentDir = agentDir } else if agentDir, ok := record["agent_dir"].(string); ok { item.AgentDir = agentDir } item.AgentDir = resolveRoleDir(installDir, item.AgentDir) return item } func findConfiguredAgentByID(baseDir string, conf map[string]interface{}, id string) (dto.AgentConfiguredAgentItem, bool) { agents, ok := conf["agents"].(map[string]interface{}) if !ok { return dto.AgentConfiguredAgentItem{}, false } rawList, ok := agents["list"].([]interface{}) if !ok { return dto.AgentConfiguredAgentItem{}, false } for _, item := range rawList { record, ok := item.(map[string]interface{}) if !ok { continue } configured := extractConfiguredAgentItem(baseDir, record) if strings.EqualFold(configured.ID, id) { return configured, true } } return dto.AgentConfiguredAgentItem{}, false } func formatRoleBinding(channel, accountID string) string { if channel == "" { return "" } if accountID == "" { return channel } return channel + ":" + accountID } func applyConfiguredAgentBindings(agents []dto.AgentConfiguredAgentItem, value interface{}) { bindings, ok := value.([]interface{}) if !ok || len(agents) == 0 { return } indexByID := make(map[string]int, len(agents)) indexByName := make(map[string]int, len(agents)) for i, agent := range agents { if agent.ID != "" { indexByID[agent.ID] = i } if agent.Name != "" { indexByName[agent.Name] = i } } for _, binding := range bindings { record, ok := binding.(map[string]interface{}) if !ok { continue } if bindingType, _ := record["type"].(string); !strings.EqualFold(bindingType, "route") { continue } targetID, _ := record["agentId"].(string) if targetID == "" { continue } match, ok := record["match"].(map[string]interface{}) if !ok { continue } channel, _ := match["channel"].(string) if channel == "" { continue } index, ok := indexByID[targetID] if !ok { index, ok = indexByName[targetID] } if !ok { continue } accountID, _ := match["accountId"].(string) if accountID == "" { accountID, _ = record["accountId"].(string) } agents[index].Bindings = append(agents[index].Bindings, dto.AgentRoleBinding{ Channel: channel, AccountID: accountID, }) } } func loadBoundChannelBindings(value interface{}) map[string]map[string]struct{} { result := make(map[string]map[string]struct{}) bindings, ok := value.([]interface{}) if !ok { return result } for _, binding := range bindings { record, ok := binding.(map[string]interface{}) if !ok { continue } if bindingType, _ := record["type"].(string); !strings.EqualFold(bindingType, "route") { continue } match, ok := record["match"].(map[string]interface{}) if !ok { continue } channel, _ := match["channel"].(string) if channel == "" { continue } accountID, _ := match["accountId"].(string) if accountID == "" { accountID, _ = record["accountId"].(string) } if _, ok := result[channel]; !ok { result[channel] = make(map[string]struct{}) } result[channel][accountID] = struct{}{} } return result } func extractRoleChannelAccountIDs(conf map[string]interface{}, channel string) []string { switch channel { case "feishu": config := extractFeishuConfig(conf) accountIDs := make([]string, 0, len(config.Bots)) for _, item := range config.Bots { if accountID := item.AccountID; accountID != "" { accountIDs = append(accountIDs, accountID) } } sort.Strings(accountIDs) return accountIDs case "telegram": config := extractTelegramConfig(conf) accountIDs := make([]string, 0, len(config.Bots)) for _, item := range config.Bots { if accountID := item.AccountID; accountID != "" { accountIDs = append(accountIDs, accountID) } } sort.Strings(accountIDs) return accountIDs case "discord": config := extractDiscordConfig(conf) accountIDs := make([]string, 0, len(config.Bots)) for _, item := range config.Bots { if accountID := item.AccountID; accountID != "" { accountIDs = append(accountIDs, accountID) } } sort.Strings(accountIDs) return accountIDs case "qqbot": config := extractQQBotConfig(conf) accountIDs := make([]string, 0, len(config.Bots)) for _, item := range config.Bots { if accountID := item.AccountID; accountID != "" { accountIDs = append(accountIDs, accountID) } } sort.Strings(accountIDs) return accountIDs case "dingtalk-connector": config := extractDingTalkConfig(conf) accountIDs := make([]string, 0, len(config.Bots)) for _, item := range config.Bots { if accountID := item.AccountID; accountID != "" { accountIDs = append(accountIDs, accountID) } } sort.Strings(accountIDs) return accountIDs case "wecom": return []string{} default: return extractRawChannelAccountIDs(getChannelConfig(conf, channel)) } } func filterAvailableChannelAccountIDs(bindings map[string]map[string]struct{}, channel string, accountIDs []string) []string { channelBindings, ok := bindings[channel] if !ok || len(channelBindings) == 0 { return append([]string(nil), accountIDs...) } if _, ok := channelBindings[""]; ok { return []string{} } result := make([]string, 0, len(accountIDs)) for _, accountID := range accountIDs { if _, ok := channelBindings[accountID]; ok { continue } result = append(result, accountID) } return result } func isRoleChannelFullyBound(bindings map[string]map[string]struct{}, channel string, allAccountIDs, availableAccountIDs []string) bool { channelBindings, ok := bindings[channel] if !ok || len(channelBindings) == 0 { return false } if _, ok := channelBindings[""]; ok { return true } if len(allAccountIDs) == 0 { return true } return len(availableAccountIDs) == 0 } func extractRawChannelAccountIDs(channel map[string]interface{}) []string { if len(channel) == 0 { return []string{} } accounts, ok := channel["accounts"].(map[string]interface{}) if !ok || len(accounts) == 0 { return []string{} } result := make([]string, 0, len(accounts)) for key := range accounts { if key == "" { continue } result = append(result, key) } sort.Strings(result) return result } func resolveRoleDir(installDir, workspace string) string { if workspace == "" { return "" } if strings.HasPrefix(workspace, "/home/node/.openclaw/workspace/") { return strings.ReplaceAll(workspace, "/home/node/.openclaw", installDir) } return strings.ReplaceAll(workspace, "/home/node/.openclaw", path.Join(installDir, "conf")) } func resolveMarkdownWorkspaceDir(installDir, workspace string) (string, error) { if workspace == "" { return "", buserr.New("ErrRecordNotFound") } confDir := path.Join(installDir, "conf") allowedOpenclawPrefixes := []string{ "/home/node/.openclaw/workspace/", "/home/node/.openclaw/workspace-agent_", } for _, prefix := range allowedOpenclawPrefixes { if strings.HasPrefix(workspace, prefix) { return resolveRoleDir(installDir, workspace), nil } } cleanWorkspace := path.Clean(workspace) cleanConfDir := path.Clean(confDir) if cleanWorkspace == cleanConfDir || strings.HasPrefix(cleanWorkspace, cleanConfDir+"/") { return cleanWorkspace, nil } return "", buserr.New("ErrRecordNotFound") }