diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index a06e849223af..c7ad7ac00de8 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -332,6 +332,130 @@ func (b *BaseApi) DeleteAgentAccount(c *gin.Context) { helper.Success(c) } +// @Tags AI +// @Summary Create Agent role +// @Accept json +// @Param request body dto.AgentRoleCreateReq true "request" +// @Success 200 {object} dto.AgentRoleCreateResp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/agent/create [post] +func (b *BaseApi) CreateAgentRole(c *gin.Context) { + var req dto.AgentRoleCreateReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := agentService.CreateRole(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags AI +// @Summary Delete Agent role +// @Accept json +// @Param request body dto.AgentRoleDeleteReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/agent/delete [post] +func (b *BaseApi) DeleteAgentRole(c *gin.Context) { + var req dto.AgentRoleDeleteReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.DeleteRole(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + +// @Tags AI +// @Summary Get configured Agent roles from config file +// @Accept json +// @Param request body dto.AgentConfiguredAgentsReq true "request" +// @Success 200 {array} dto.AgentConfiguredAgentItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/agent/list [post] +func (b *BaseApi) GetConfiguredAgentRoles(c *gin.Context) { + var req dto.AgentConfiguredAgentsReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := agentService.GetConfiguredAgents(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags AI +// @Summary Get Agent role channels from config file +// @Accept json +// @Param request body dto.AgentRoleChannelsReq true "request" +// @Success 200 {array} dto.AgentRoleChannelItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/agent/channels [post] +func (b *BaseApi) GetAgentRoleChannels(c *gin.Context) { + var req dto.AgentRoleChannelsReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := agentService.GetRoleChannels(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags AI +// @Summary Get Agent role markdown files +// @Accept json +// @Param request body dto.AgentRoleMarkdownFilesReq true "request" +// @Success 200 {array} dto.AgentRoleMarkdownFileItem +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/agent/md/list [post] +func (b *BaseApi) GetAgentRoleMarkdownFiles(c *gin.Context) { + var req dto.AgentRoleMarkdownFilesReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := agentService.GetRoleMarkdownFiles(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags AI +// @Summary Update Agent role markdown file +// @Accept json +// @Param request body dto.AgentRoleMarkdownFilesUpdateReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/agent/md/update [post] +func (b *BaseApi) UpdateAgentRoleMarkdownFile(c *gin.Context) { + var req dto.AgentRoleMarkdownFilesUpdateReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.UpdateRoleMarkdownFiles(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + // @Tags AI // @Summary Get Agent Feishu channel config // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 4a6b3bb4835f..5657d14ece03 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -81,6 +81,72 @@ type AgentOverview struct { Snapshot AgentOverviewSnapshot `json:"snapshot"` } +type AgentRoleBinding struct { + Channel string `json:"channel" validate:"required"` + AccountID string `json:"accountId"` +} + +type AgentRoleCreateReq struct { + AgentID uint `json:"agentId" validate:"required"` + Name string `json:"name" validate:"required"` + Model string `json:"model"` + Bindings []AgentRoleBinding `json:"bindings"` +} + +type AgentRoleCreateResp struct { + Output string `json:"output"` +} + +type AgentRoleDeleteReq struct { + AgentID uint `json:"agentId" validate:"required"` + ID string `json:"id" validate:"required"` +} + +type AgentConfiguredAgentsReq struct { + AgentID uint `json:"agentId" validate:"required"` +} + +type AgentRoleChannelsReq struct { + AgentID uint `json:"agentId" validate:"required"` +} + +type AgentRoleChannelItem struct { + Name string `json:"name"` + Bound bool `json:"bound"` + AccountIDs []string `json:"accountIds"` +} + +type AgentRoleMarkdownFilesReq struct { + AgentID uint `json:"agentId" validate:"required"` + Workspace string `json:"workspace" validate:"required"` +} + +type AgentConfiguredAgentItem struct { + ID string `json:"id"` + Name string `json:"name"` + Workspace string `json:"workspace"` + Model string `json:"model"` + AgentDir string `json:"agentDir"` + Bindings []AgentRoleBinding `json:"bindings"` +} + +type AgentRoleMarkdownFileItem struct { + Name string `json:"name"` + Content string `json:"content"` +} + +type AgentRoleMarkdownFileUpdateItem struct { + Name string `json:"name" validate:"required,oneof=AGENTS.md SOUL.md USER.md IDENTITY.md TOOLS.md HEARTBEAT.md BOOT.md BOOTSTRAP.md"` + Content string `json:"content"` +} + +type AgentRoleMarkdownFilesUpdateReq struct { + AgentID uint `json:"agentId" validate:"required"` + Workspace string `json:"workspace" validate:"required"` + Restart bool `json:"restart"` + Files []AgentRoleMarkdownFileUpdateItem `json:"files" validate:"required"` +} + type AgentOverviewSnapshot struct { ContainerStatus string `json:"containerStatus"` AppVersion string `json:"appVersion"` diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index 2aa2273258f1..a30e3701c3fb 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -43,6 +43,13 @@ type IAgentService interface { UpdateSkill(req dto.AgentSkillUpdateReq) error InstallSkill(req dto.AgentSkillInstallReq) error + CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error) + DeleteRole(req dto.AgentRoleDeleteReq) 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 diff --git a/agent/app/service/agents_agents.go b/agent/app/service/agents_agents.go new file mode 100644 index 000000000000..c52c14f62505 --- /dev/null +++ b/agent/app/service/agents_agents.go @@ -0,0 +1,390 @@ +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 + } + + name := strings.TrimSpace(req.Name) + args := []string{"exec", install.ContainerName, "openclaw", "agents", "add", name} + workspace := "/home/node/.openclaw/workspace-agent_" + name + agentDir := "/home/node/.openclaw/agents/" + name + args = append(args, "--workspace", workspace) + if model := strings.TrimSpace(req.Model); model != "" { + args = append(args, "--model", model) + } + for _, binding := range req.Bindings { + channel := strings.TrimSpace(binding.Channel) + if channel == "" { + continue + } + if accountID := strings.TrimSpace(binding.AccountID); accountID != "" { + channel = channel + ":" + accountID + } + args = append(args, "--bind", channel) + } + args = append(args, "--agent-dir", agentDir) + args = append(args, "--non-interactive", "--json") + + mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute)) + output, err := mgr.RunWithStdout("docker", 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 + } + boundChannels := loadBoundChannelSet(conf["bindings"]) + result := make([]dto.AgentRoleChannelItem, 0, len(channels)) + for key := range channels { + key = strings.TrimSpace(key) + if key == "" { + continue + } + channelConf, _ := channels[key].(map[string]interface{}) + result = append(result, dto.AgentRoleChannelItem{ + Name: key, + Bound: boundChannels[key], + AccountIDs: extractChannelAccountIDs(channelConf), + }) + } + 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") + roleID := strings.TrimSpace(req.ID) + if roleID == "" { + return buserr.New("ErrRecordNotFound") + } + target, ok := findConfiguredAgentByID(baseDir, conf, roleID) + if !ok { + return buserr.New("ErrRecordNotFound") + } + + args := []string{"exec", install.ContainerName, "openclaw", "agents", "delete", roleID, "--force"} + + mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute)) + if _, err = mgr.RunWithStdout("docker", 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) 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 = strings.TrimSpace(id) + } else if id, ok := record["agentId"].(string); ok { + item.ID = strings.TrimSpace(id) + } + if name, ok := record["name"].(string); ok { + item.Name = strings.TrimSpace(name) + } + if workspace, ok := record["workspace"].(string); ok { + item.Workspace = resolveRoleDir(installDir, strings.TrimSpace(workspace)) + } + if model, ok := record["model"].(string); ok { + item.Model = strings.TrimSpace(model) + } + if agentDir, ok := record["agentDir"].(string); ok { + item.AgentDir = strings.TrimSpace(agentDir) + } else if agentDir, ok := record["agent_dir"].(string); ok { + item.AgentDir = strings.TrimSpace(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(strings.TrimSpace(configured.ID), strings.TrimSpace(id)) { + return configured, true + } + } + return dto.AgentConfiguredAgentItem{}, false +} + +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(strings.TrimSpace(bindingType), "route") { + continue + } + targetID, _ := record["agentId"].(string) + targetID = strings.TrimSpace(targetID) + if targetID == "" { + continue + } + match, ok := record["match"].(map[string]interface{}) + if !ok { + continue + } + channel, _ := match["channel"].(string) + channel = strings.TrimSpace(channel) + if channel == "" { + continue + } + index, ok := indexByID[targetID] + if !ok { + index, ok = indexByName[targetID] + } + if !ok { + continue + } + accountID, _ := match["accountId"].(string) + if strings.TrimSpace(accountID) == "" { + accountID, _ = record["accountId"].(string) + } + agents[index].Bindings = append(agents[index].Bindings, dto.AgentRoleBinding{ + Channel: channel, + AccountID: strings.TrimSpace(accountID), + }) + } +} + +func loadBoundChannelSet(value interface{}) map[string]bool { + result := make(map[string]bool) + 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(strings.TrimSpace(bindingType), "route") { + continue + } + match, ok := record["match"].(map[string]interface{}) + if !ok { + continue + } + channel, _ := match["channel"].(string) + channel = strings.TrimSpace(channel) + if channel == "" { + continue + } + result[channel] = true + } + return result +} + +func extractChannelAccountIDs(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 { + key = strings.TrimSpace(key) + if key == "" { + continue + } + result = append(result, key) + } + sort.Strings(result) + return result +} + +func resolveRoleDir(installDir, workspace string) string { + workspace = strings.TrimSpace(workspace) + 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) { + workspace = strings.TrimSpace(workspace) + 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") +} diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 5aade3056e3a..ebd9456bd4b9 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -56,6 +56,12 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/agents/accounts/models/delete", baseApi.DeleteAgentAccountModel) aiToolsRouter.POST("/agents/accounts/verify", baseApi.VerifyAgentAccount) aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount) + aiToolsRouter.POST("/agents/agent/create", baseApi.CreateAgentRole) + aiToolsRouter.POST("/agents/agent/delete", baseApi.DeleteAgentRole) + aiToolsRouter.POST("/agents/agent/list", baseApi.GetConfiguredAgentRoles) + aiToolsRouter.POST("/agents/agent/channels", baseApi.GetAgentRoleChannels) + aiToolsRouter.POST("/agents/agent/md/list", baseApi.GetAgentRoleMarkdownFiles) + aiToolsRouter.POST("/agents/agent/md/update", baseApi.UpdateAgentRoleMarkdownFile) aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig) aiToolsRouter.POST("/agents/channel/feishu/update", baseApi.UpdateAgentFeishuConfig) aiToolsRouter.POST("/agents/channel/telegram/get", baseApi.GetAgentTelegramConfig) diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index aafaf4bc8028..93571cdfc2af 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -307,6 +307,72 @@ export namespace AI { agentId: number; } + export interface AgentRoleCreateReq { + agentId: number; + name: string; + model: string; + bindings: AgentRoleBinding[]; + } + + export interface AgentRoleBinding { + channel: string; + accountId: string; + } + + export interface AgentRoleCreateResp { + output: string; + } + + export interface AgentRoleDeleteReq { + agentId: number; + id: string; + } + + export interface AgentConfiguredAgentsReq { + agentId: number; + } + + export interface AgentRoleChannelsReq { + agentId: number; + } + + export interface AgentRoleChannelItem { + name: string; + bound: boolean; + accountIds: string[]; + } + + export interface AgentRoleMarkdownFilesReq { + agentId: number; + workspace: string; + } + + export interface AgentConfiguredAgentItem { + id: string; + name: string; + workspace: string; + model: string; + agentDir: string; + bindings: AgentRoleBinding[]; + } + + export interface AgentRoleMarkdownFileItem { + name: string; + content: string; + } + + export interface AgentRoleMarkdownFileUpdateItem { + name: string; + content: string; + } + + export interface AgentRoleMarkdownFilesUpdateReq { + agentId: number; + workspace: string; + restart: boolean; + files: AgentRoleMarkdownFileUpdateItem[]; + } + export interface AgentOverviewSnapshot { containerStatus: string; appVersion: string; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index 5c8c37125eb8..4faa8c06cd97 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -117,6 +117,30 @@ export const getAgentOverview = (req: AI.AgentOverviewReq) => { return http.post(`/ai/agents/overview`, req); }; +export const createAgentRole = (req: AI.AgentRoleCreateReq) => { + return http.post(`/ai/agents/agent/create`, req); +}; + +export const deleteAgentRole = (req: AI.AgentRoleDeleteReq) => { + return http.post(`/ai/agents/agent/delete`, req); +}; + +export const getConfiguredAgentRoles = (req: AI.AgentConfiguredAgentsReq) => { + return http.post(`/ai/agents/agent/list`, req); +}; + +export const getAgentRoleChannels = (req: AI.AgentRoleChannelsReq) => { + return http.post(`/ai/agents/agent/channels`, req); +}; + +export const getAgentRoleMarkdownFiles = (req: AI.AgentRoleMarkdownFilesReq) => { + return http.post(`/ai/agents/agent/md/list`, req); +}; + +export const updateAgentRoleMarkdownFile = (req: AI.AgentRoleMarkdownFilesUpdateReq) => { + return http.post(`/ai/agents/agent/md/update`, req); +}; + export const getAgentProviders = () => { return http.get(`/ai/agents/providers`); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 2f5ed5c4b9c6..79e52232ba61 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -720,6 +720,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 781afe2ff4d5..6bca2e19a2b1 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -728,6 +728,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 245db5ed7610..0478402ad56c 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -721,6 +721,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 3031a4ffec37..90356f42e344 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -713,6 +713,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 46d034efc961..5bd59b1984f1 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -728,6 +728,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 83baf60b89e1..e1270eb2e4e3 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -723,6 +723,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 840e22db52cd..e589c84027d5 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -720,6 +720,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 42827e34ba7c..6e74f248f618 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -724,6 +724,42 @@ const message = { skillsGroupWorkspace: 'Workspace', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', + agentRoleTab: 'Agents', + agentRoleUnsupported: 'Role management is currently supported only for OpenClaw.', + workspace: 'Workspace Directory', + agentDir: 'Agent Directory', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Operating instructions for the agent and how it should use memory.', + 'Loaded at the start of every session.', + 'Good place for rules, priorities, and "how to behave" details.', + ], + 'SOUL.md': ['Persona, tone, and boundaries.', 'Loaded every session.'], + 'USER.md': ['Who the user is and how to address them.', 'Loaded every session.'], + 'IDENTITY.md': [ + "The agent's name, vibe, and emoji.", + 'Created or updated during the bootstrap ritual.', + ], + 'TOOLS.md': [ + 'Notes about your local tools and conventions.', + 'Does not control tool availability; it is only guidance.', + ], + 'HEARTBEAT.md': ['Optional tiny checklist for heartbeat runs.', 'Keep it short to avoid token burn.'], + 'BOOT.md': [ + 'Optional startup checklist executed on gateway restart when internal hooks are enabled.', + 'Keep it short; use the message tool for outbound sends.', + ], + 'BOOTSTRAP.md': [ + 'One-time first-run ritual.', + 'Only created for a brand-new workspace.', + 'Delete it after the ritual is complete.', + ], + }, + bindings: 'Bindings', + accountIdOptional: 'Account ID (Optional)', + saveAllMd: 'Save All', + roleMarkdownRestartHelper: + 'Saving all current markdown files requires a container restart to take effect. Choose whether to restart now or later.', configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 0fbd185733c5..546bf967bee1 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -686,6 +686,31 @@ const message = { skillsGroupWorkspace: '工作區', switchModelSuccess: '模型切換成功', channelsTab: '頻道', + agentRoleTab: '角色', + agentRoleUnsupported: '目前僅 OpenClaw 支援角色管理。', + workspace: '工作區目錄', + agentDir: 'Agent 目錄', + roleMarkdownDescriptions: { + 'AGENTS.md': [ + 'Agent 的操作說明,以及如何使用記憶。', + '每次會話開始時都會載入。', + '適合放規則、優先級和行為方式等內容。', + ], + 'SOUL.md': ['人格、語氣和邊界。', '每次會話都會載入。'], + 'USER.md': ['使用者是誰,以及應該如何稱呼和回應使用者。', '每次會話都會載入。'], + 'IDENTITY.md': ['Agent 的名字、氣質和 emoji。', '會在 bootstrap 儀式中建立或更新。'], + 'TOOLS.md': ['關於本地工具和約定的說明。', '不會控制工具可用性,僅作為使用指引。'], + 'HEARTBEAT.md': ['可選的心跳執行檢查清單。', '盡量保持簡短,避免消耗過多 token。'], + 'BOOT.md': [ + '可選的啟動檢查清單,會在 gateway 重啟且啟用內部 hooks 時執行。', + '盡量保持簡短;如需對外發送訊息,請使用 message 工具。', + ], + 'BOOTSTRAP.md': ['一次性的首次執行儀式。', '只會在全新的 workspace 中建立。', '儀式完成後請刪除它。'], + }, + bindings: '綁定', + accountIdOptional: '帳號 ID(可選)', + saveAllMd: '保存全部', + roleMarkdownRestartHelper: '保存當前全部 MD 檔案後,需要重新啟動容器才能生效。請選擇立即重啟或稍後重啟。', configFileRestartHelper: '保存配置檔後需要立即重新啟動容器才能生效。', overviewSnapshot: '狀態概覽', defaultModel: '預設模型', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8d2782532a26..1da9344b1687 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -685,6 +685,27 @@ const message = { skillsGroupWorkspace: '工作区', switchModelSuccess: '模型切换成功', channelsTab: '频道', + agentRoleTab: '角色', + agentRoleUnsupported: '当前仅 OpenClaw 支持角色管理。', + workspace: '工作区目录', + agentDir: 'Agent 目录', + roleMarkdownDescriptions: { + 'AGENTS.md': ['Agent 的操作说明,以及如何使用记忆。', '适合放规则、优先级和行为方式等内容。'], + 'SOUL.md': ['人格、语气和边界。'], + 'USER.md': ['用户是谁,以及应该如何称呼和回应用户。'], + 'IDENTITY.md': ['Agent 的名字、气质和 emoji。'], + 'TOOLS.md': ['关于本地工具和约定的说明。'], + 'HEARTBEAT.md': ['可选的心跳运行检查清单。', '尽量保持简短,避免消耗过多 token。'], + 'BOOT.md': [ + '可选的启动检查清单,会在 gateway 重启且启用内部 hooks 时执行。', + '尽量保持简短;如需对外发送消息,请使用 message 工具。', + ], + 'BOOTSTRAP.md': ['首次运行引导流程', '只会在全新的工作区中创建。'], + }, + bindings: '绑定', + accountIdOptional: '账号 ID(可选)', + saveAllMd: '保存全部', + roleMarkdownRestartHelper: '保存当前全部 MD 文件后,需要重启容器才能生效。请选择立即重启或稍后重启。', configFileRestartHelper: '保存配置文件后需要重启容器才能生效。', overviewSnapshot: '状态概览', defaultModel: '默认模型', diff --git a/frontend/src/views/ai/agents/agent/config/index.vue b/frontend/src/views/ai/agents/agent/config/index.vue index cbae12bf5a6f..f2d3e31b3e35 100644 --- a/frontend/src/views/ai/agents/agent/config/index.vue +++ b/frontend/src/views/ai/agents/agent/config/index.vue @@ -8,6 +8,9 @@ + + + @@ -26,6 +29,7 @@ import { useI18n } from 'vue-i18n'; import { AI } from '@/api/interface/ai'; import ChannelsTab from './tabs/channels.vue'; import ModelTab from './tabs/model.vue'; +import AgentTab from './tabs/agents/index.vue'; import SkillsTab from './tabs/skills.vue'; import SettingsTab from './tabs/settings.vue'; @@ -38,8 +42,11 @@ const agentId = ref(0); const accountId = ref(0); const model = ref(''); const appVersion = ref(''); +const configPath = ref(''); +const agentType = ref<'openclaw' | 'copaw'>('openclaw'); const channelsRef = ref(); const modelRef = ref(); +const agentRef = ref(); const skillsRef = ref(); const settingsRef = ref(); @@ -74,6 +81,20 @@ const loadChannels = async () => { await channelsRef.value?.load(agentId.value); }; +const loadAgent = async () => { + if (agentId.value <= 0) { + return; + } + await nextTick(); + await agentRef.value?.load({ + agentId: agentId.value, + agentType: agentType.value, + accountId: accountId.value, + model: model.value, + configPath: configPath.value, + }); +}; + const loadSkills = async () => { if (agentId.value <= 0) { return; @@ -96,6 +117,9 @@ const handleTabClick = async (pane: TabsPaneContext) => { if (pane.paneName === 'skills') { await loadSkills(); } + if (pane.paneName === 'agent') { + await loadAgent(); + } if (pane.paneName === 'channels' && agentId.value > 0) { await loadChannels(); } @@ -110,6 +134,8 @@ const openDrawer = async (agent: AI.AgentItem) => { accountId.value = agent.accountId; model.value = agent.model; appVersion.value = agent.appVersion; + configPath.value = agent.configPath; + agentType.value = agent.agentType; header.value = `${agent.name} - ${t('menu.config')}`; activeTab.value = 'channels'; open.value = true; diff --git a/frontend/src/views/ai/agents/agent/config/tabs/agents/create/index.vue b/frontend/src/views/ai/agents/agent/config/tabs/agents/create/index.vue new file mode 100644 index 000000000000..3d6bdc57709c --- /dev/null +++ b/frontend/src/views/ai/agents/agent/config/tabs/agents/create/index.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/frontend/src/views/ai/agents/agent/config/tabs/agents/detail/index.vue b/frontend/src/views/ai/agents/agent/config/tabs/agents/detail/index.vue new file mode 100644 index 000000000000..d49ab189600c --- /dev/null +++ b/frontend/src/views/ai/agents/agent/config/tabs/agents/detail/index.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/frontend/src/views/ai/agents/agent/config/tabs/agents/index.vue b/frontend/src/views/ai/agents/agent/config/tabs/agents/index.vue new file mode 100644 index 000000000000..c04dc6aedbeb --- /dev/null +++ b/frontend/src/views/ai/agents/agent/config/tabs/agents/index.vue @@ -0,0 +1,183 @@ + + + + +