mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: add agent config role management (#12364)
This commit is contained in:
@@ -352,6 +352,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
|
||||
|
||||
@@ -88,6 +88,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"`
|
||||
|
||||
@@ -44,6 +44,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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -57,6 +57,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)
|
||||
|
||||
@@ -314,6 +314,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;
|
||||
|
||||
@@ -121,6 +121,30 @@ export const getAgentOverview = (req: AI.AgentOverviewReq) => {
|
||||
return http.post<AI.AgentOverview>(`/ai/agents/overview`, req);
|
||||
};
|
||||
|
||||
export const createAgentRole = (req: AI.AgentRoleCreateReq) => {
|
||||
return http.post<AI.AgentRoleCreateResp>(`/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.AgentConfiguredAgentItem[]>(`/ai/agents/agent/list`, req);
|
||||
};
|
||||
|
||||
export const getAgentRoleChannels = (req: AI.AgentRoleChannelsReq) => {
|
||||
return http.post<AI.AgentRoleChannelItem[]>(`/ai/agents/agent/channels`, req);
|
||||
};
|
||||
|
||||
export const getAgentRoleMarkdownFiles = (req: AI.AgentRoleMarkdownFilesReq) => {
|
||||
return http.post<AI.AgentRoleMarkdownFileItem[]>(`/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.ProviderInfo[]>(`/ai/agents/providers`);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '預設模型',
|
||||
|
||||
@@ -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: '默认模型',
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<el-tab-pane :label="t('aiTools.model.model')" name="model">
|
||||
<ModelTab ref="modelRef" @updated="handleModelUpdated" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('aiTools.agents.agentRoleTab')" name="agent">
|
||||
<AgentTab ref="agentRef" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('aiTools.agents.skillsTab')" name="skills">
|
||||
<SkillsTab ref="skillsRef" :app-version="appVersion" />
|
||||
</el-tab-pane>
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<DialogPro v-model="open" :title="`${$t('commons.button.add')}`" size="large" @close="handleClose">
|
||||
<div v-loading="loading" class="create-role-dialog">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||||
<div class="create-role-section">
|
||||
<div class="create-role-section__title">{{ $t('commons.table.name') }}</div>
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model.trim="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.model.model')">
|
||||
<el-select v-model="form.model" clearable filterable class="w-full">
|
||||
<el-option
|
||||
v-for="item in modelOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="bindings-divider"></div>
|
||||
<el-table v-if="form.bindings.length" :data="form.bindings" class="bindings-table" size="small">
|
||||
<el-table-column :label="$t('aiTools.agents.channelsTab')" min-width="180">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
v-model="row.channel"
|
||||
clearable
|
||||
filterable
|
||||
class="w-full"
|
||||
@change="handleBindingChannelChange($index)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:disabled="isChannelDisabled(item, $index)"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.agents.accountIdOptional')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.accountId"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getAccountIdOptions(row.channel)"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.operate')" width="90" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link @click="removeBinding($index)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else :description="$t('commons.msg.noneData')" :image-size="60" />
|
||||
<div class="bindings-footer">
|
||||
<el-button type="primary" link @click="addBinding">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :disabled="loading" @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="submit">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import { createAgentRole, getAgentRoleChannels, pageAgentAccounts } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
bound?: boolean;
|
||||
accountIds?: string[];
|
||||
}
|
||||
|
||||
interface DialogParams {
|
||||
agentId: number;
|
||||
accountId: number;
|
||||
model: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const { isIntl } = useGlobalStore();
|
||||
const blockedProviders = new Set(['ark-coding-plan', 'bailian-coding-plan']);
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const agentId = ref(0);
|
||||
const accountId = ref(0);
|
||||
const modelOptions = ref<SelectOption[]>([]);
|
||||
const channelOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
bindings: [] as AI.AgentRoleBinding[],
|
||||
model: '',
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
name: [Rules.simpleName],
|
||||
});
|
||||
|
||||
const resetForm = (currentModel?: string) => {
|
||||
form.name = '';
|
||||
form.bindings = [];
|
||||
form.model = currentModel || '';
|
||||
};
|
||||
|
||||
const addBinding = () => {
|
||||
form.bindings.push({
|
||||
channel: '',
|
||||
accountId: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeBinding = (index: number) => {
|
||||
form.bindings.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleBindingChannelChange = (index: number) => {
|
||||
const binding = form.bindings[index];
|
||||
if (!binding) {
|
||||
return;
|
||||
}
|
||||
binding.accountId = '';
|
||||
};
|
||||
|
||||
const getAccountIdOptions = (channel: string) => {
|
||||
return channelOptions.value.find((item) => item.value === channel)?.accountIds || [];
|
||||
};
|
||||
|
||||
const isChannelDisabled = (option: SelectOption, index: number) => {
|
||||
if (option.bound) {
|
||||
return true;
|
||||
}
|
||||
return form.bindings.some((item, bindingIndex) => bindingIndex !== index && item.channel === option.value);
|
||||
};
|
||||
|
||||
const loadAccounts = async () => {
|
||||
const res = await pageAgentAccounts({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
provider: '',
|
||||
name: '',
|
||||
});
|
||||
const items = res.data.items || [];
|
||||
const accountOptions = isIntl.value ? items.filter((item) => !blockedProviders.has(item.provider)) : items;
|
||||
const selected = accountOptions.find((item) => item.id === accountId.value);
|
||||
modelOptions.value = (selected?.models || []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
};
|
||||
|
||||
const loadChannels = async () => {
|
||||
const res = await getAgentRoleChannels({ agentId: agentId.value });
|
||||
channelOptions.value = (res.data || []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
bound: item.bound,
|
||||
accountIds: item.accountIds || [],
|
||||
}));
|
||||
};
|
||||
|
||||
const acceptParams = async (params: DialogParams) => {
|
||||
agentId.value = params.agentId;
|
||||
accountId.value = params.accountId;
|
||||
resetForm(params.model);
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([loadAccounts(), loadChannels()]);
|
||||
addBinding();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!formRef.value || !agentId.value) {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
await createAgentRole({
|
||||
agentId: agentId.value,
|
||||
name: form.name.trim(),
|
||||
model: form.model.trim(),
|
||||
bindings: form.bindings
|
||||
.filter((item) => item.channel)
|
||||
.map((item) => ({
|
||||
channel: item.channel,
|
||||
accountId: item.accountId.trim(),
|
||||
})),
|
||||
} as AI.AgentRoleCreateReq);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('success');
|
||||
handleClose();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.create-role-dialog {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.create-role-section {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.bindings-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bindings-table :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bindings-divider {
|
||||
margin: 4px 0 12px;
|
||||
border-top: 1px dashed var(--el-border-color);
|
||||
}
|
||||
|
||||
.bindings-footer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<DialogPro v-model="open" :title="header" size="w-60" @close="handleClose">
|
||||
<div v-loading="loading" class="agent-file-drawer">
|
||||
<el-alert v-if="!agentId || !workspace" :title="$t('commons.msg.noneData')" type="info" :closable="false" />
|
||||
<template v-else>
|
||||
<el-tabs v-model="activeFile">
|
||||
<el-tab-pane v-for="item in fileItems" :key="item.name" :name="item.name">
|
||||
<template #label>{{ item.name }}</template>
|
||||
<div class="agent-file-status" v-if="item.error">{{ item.error }}</div>
|
||||
<CodemirrorPro
|
||||
v-model="item.content"
|
||||
:height-diff="470"
|
||||
:line-wrapping="true"
|
||||
:placeholder="item.name"
|
||||
/>
|
||||
<ul v-if="getFileDescriptionItems(item.name).length" class="agent-file-description">
|
||||
<li v-for="description in getFileDescriptionItems(item.name)" :key="description">
|
||||
{{ description }}
|
||||
</li>
|
||||
</ul>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="saving" @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button :loading="loading" :disabled="saving || !agentId || !workspace" @click="reloadFiles">
|
||||
{{ $t('commons.button.refresh') }}
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="saving" :disabled="!currentFile" @click="saveAllFiles">
|
||||
{{ $t('aiTools.agents.saveAllMd') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { getAgentRoleMarkdownFiles, updateAgentRoleMarkdownFile } from '@/api/modules/ai';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
|
||||
interface DialogParams {
|
||||
name: string;
|
||||
agentId: number;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
interface AgentFileItem {
|
||||
name: string;
|
||||
content: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const FILE_NAMES = [
|
||||
'AGENTS.md',
|
||||
'SOUL.md',
|
||||
'USER.md',
|
||||
'IDENTITY.md',
|
||||
'TOOLS.md',
|
||||
'HEARTBEAT.md',
|
||||
'BOOT.md',
|
||||
'BOOTSTRAP.md',
|
||||
];
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const roleName = ref('');
|
||||
const agentId = ref(0);
|
||||
const workspace = ref('');
|
||||
const activeFile = ref(FILE_NAMES[0]);
|
||||
const fileItems = ref<AgentFileItem[]>([]);
|
||||
|
||||
const header = computed(() => `${roleName.value || '-'}`);
|
||||
const currentFile = computed(() => fileItems.value.find((item) => item.name === activeFile.value));
|
||||
const getFileDescriptionItems = (name: string): string[] => {
|
||||
const descriptions = i18n.global.tm('aiTools.agents.roleMarkdownDescriptions') as Record<string, unknown>;
|
||||
const items = descriptions?.[name];
|
||||
return Array.isArray(items) ? items.map((item) => String(item)) : [];
|
||||
};
|
||||
|
||||
const buildFiles = (items: AI.AgentRoleMarkdownFileItem[]) =>
|
||||
FILE_NAMES.map((name) => ({
|
||||
name,
|
||||
content: items.find((item) => item.name === name)?.content || '',
|
||||
error: '',
|
||||
}));
|
||||
|
||||
const reloadFiles = async () => {
|
||||
if (!agentId.value || !workspace.value) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getAgentRoleMarkdownFiles({
|
||||
agentId: agentId.value,
|
||||
workspace: workspace.value,
|
||||
});
|
||||
fileItems.value = buildFiles(res.data || []);
|
||||
activeFile.value = fileItems.value[0]?.name || FILE_NAMES[0];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const acceptParams = async (params: DialogParams) => {
|
||||
roleName.value = params.name;
|
||||
agentId.value = params.agentId;
|
||||
workspace.value = params.workspace;
|
||||
open.value = true;
|
||||
await reloadFiles();
|
||||
};
|
||||
|
||||
const saveAllFiles = async () => {
|
||||
if (!fileItems.value.length) {
|
||||
return;
|
||||
}
|
||||
let restart = false;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
i18n.global.t('aiTools.agents.roleMarkdownRestartHelper'),
|
||||
i18n.global.t('aiTools.agents.saveAllMd'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('setting.restartNow'),
|
||||
cancelButtonText: i18n.global.t('setting.restartLater'),
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true,
|
||||
},
|
||||
);
|
||||
restart = true;
|
||||
} catch (error) {
|
||||
if (error === 'cancel') {
|
||||
restart = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await updateAgentRoleMarkdownFile({
|
||||
agentId: agentId.value,
|
||||
workspace: workspace.value,
|
||||
restart,
|
||||
files: fileItems.value.map((item) => ({
|
||||
name: item.name,
|
||||
content: item.content,
|
||||
})),
|
||||
});
|
||||
fileItems.value.forEach((item) => {
|
||||
item.error = '';
|
||||
});
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-file-drawer {
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.agent-file-status {
|
||||
margin-bottom: 12px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.agent-file-description {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.agent-file-description li + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<el-alert
|
||||
v-if="agentType !== 'openclaw'"
|
||||
:title="$t('aiTools.agents.agentRoleUnsupported')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="role-toolbar">
|
||||
<el-button type="primary" @click="openCreateDialog">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<ComplexTable :data="configuredAgents">
|
||||
<el-table-column :label="$t('commons.table.name')" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.workspace" type="primary" link @click="openDetailDrawer(row)">
|
||||
{{ row.name || row.id || '-' }}
|
||||
</el-button>
|
||||
<span v-else>{{ row.name || row.id || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.model.model')" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.model || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.agents.workspace')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.workspace" :content="row.workspace" placement="top">
|
||||
<el-button type="primary" link @click="routerToFileWithPath(row.workspace)">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.agents.agentDir')" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.agentDir" :content="row.agentDir" placement="top">
|
||||
<el-button type="primary" link @click="routerToFileWithPath(row.agentDir)">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('aiTools.agents.bindings')" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ formatBindings(row.bindings) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" width="90" fixed="right" />
|
||||
</ComplexTable>
|
||||
|
||||
<CreateDialog ref="createRef" @success="handleCreated" />
|
||||
<DetailDrawer ref="detailRef" />
|
||||
<OpDialog ref="opRef" @search="handleCreated" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { deleteAgentRole, getConfiguredAgentRoles } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import i18n from '@/lang';
|
||||
import { routerToFileWithPath } from '@/utils/router';
|
||||
import CreateDialog from './create/index.vue';
|
||||
import DetailDrawer from './detail/index.vue';
|
||||
|
||||
interface AgentRoleLoadParams {
|
||||
agentId: number;
|
||||
agentType: 'openclaw' | 'copaw';
|
||||
accountId: number;
|
||||
model: string;
|
||||
configPath: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const createRef = ref<InstanceType<typeof CreateDialog> | null>(null);
|
||||
const detailRef = ref<InstanceType<typeof DetailDrawer> | null>(null);
|
||||
const opRef = ref();
|
||||
const agentId = ref(0);
|
||||
const agentType = ref<'openclaw' | 'copaw'>('openclaw');
|
||||
const accountId = ref(0);
|
||||
const currentModel = ref('');
|
||||
const configuredAgents = ref<AI.AgentConfiguredAgentItem[]>([]);
|
||||
|
||||
const loadConfiguredAgents = async (id: number) => {
|
||||
const res = await getConfiguredAgentRoles({ agentId: id });
|
||||
configuredAgents.value = res.data || [];
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
createRef.value?.acceptParams({
|
||||
agentId: agentId.value,
|
||||
accountId: accountId.value,
|
||||
model: currentModel.value,
|
||||
});
|
||||
};
|
||||
|
||||
const openDetailDrawer = (row: AI.AgentConfiguredAgentItem) => {
|
||||
if (!row.workspace) {
|
||||
return;
|
||||
}
|
||||
detailRef.value?.acceptParams({
|
||||
name: row.name || row.id || '-',
|
||||
agentId: agentId.value,
|
||||
workspace: row.workspace,
|
||||
});
|
||||
};
|
||||
|
||||
const formatBindings = (bindings: AI.AgentRoleBinding[] = []) => {
|
||||
if (!bindings.length) {
|
||||
return '-';
|
||||
}
|
||||
return bindings
|
||||
.map((item) => (item.accountId ? `${item.channel}:${item.accountId}` : item.channel))
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
const handleDelete = async (row: AI.AgentConfiguredAgentItem) => {
|
||||
opRef.value?.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('aiTools.agents.agentRoleTab'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
names: [row.name],
|
||||
api: deleteAgentRole,
|
||||
params: {
|
||||
agentId: agentId.value,
|
||||
id: row.id,
|
||||
},
|
||||
successMsg: i18n.global.t('commons.msg.operationSuccess'),
|
||||
noMsg: false,
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: AI.AgentConfiguredAgentItem) => handleDelete(row),
|
||||
},
|
||||
];
|
||||
|
||||
const handleCreated = async () => {
|
||||
if (!agentId.value) {
|
||||
return;
|
||||
}
|
||||
await loadConfiguredAgents(agentId.value);
|
||||
};
|
||||
|
||||
const load = async (params: AgentRoleLoadParams) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
agentId.value = params.agentId;
|
||||
agentType.value = params.agentType;
|
||||
accountId.value = params.accountId;
|
||||
currentModel.value = params.model;
|
||||
configuredAgents.value = [];
|
||||
if (agentType.value === 'openclaw') {
|
||||
await loadConfiguredAgents(params.agentId);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
load,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-toolbar {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user