mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: add agent role binding management (#12382)
This commit is contained in:
@@ -393,6 +393,46 @@ func (b *BaseApi) DeleteAgentRole(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Bind Agent role channel
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleBindReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/bind [post]
|
||||
func (b *BaseApi) BindAgentRole(c *gin.Context) {
|
||||
var req dto.AgentRoleBindReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.BindRole(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Unbind Agent role channel
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleBindReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/unbind [post]
|
||||
func (b *BaseApi) UnbindAgentRole(c *gin.Context) {
|
||||
var req dto.AgentRoleBindReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UnbindRole(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get configured Agent roles from config file
|
||||
// @Accept json
|
||||
|
||||
@@ -109,6 +109,13 @@ type AgentRoleDeleteReq struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentRoleBindReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
Channel string `json:"channel" validate:"required"`
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
type AgentConfiguredAgentsReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ type IAgentService interface {
|
||||
|
||||
CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error)
|
||||
DeleteRole(req dto.AgentRoleDeleteReq) error
|
||||
BindRole(req dto.AgentRoleBindReq) error
|
||||
UnbindRole(req dto.AgentRoleBindReq) error
|
||||
GetConfiguredAgents(req dto.AgentConfiguredAgentsReq) ([]dto.AgentConfiguredAgentItem, error)
|
||||
GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.AgentRoleChannelItem, error)
|
||||
GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error)
|
||||
|
||||
@@ -91,18 +91,19 @@ func (a AgentService) GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.Agent
|
||||
if !ok || len(channels) == 0 {
|
||||
return []dto.AgentRoleChannelItem{}, nil
|
||||
}
|
||||
boundChannels := loadBoundChannelSet(conf["bindings"])
|
||||
boundBindings := loadBoundChannelBindings(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{})
|
||||
accountIDs := extractRoleChannelAccountIDs(conf, key)
|
||||
availableAccountIDs := filterAvailableChannelAccountIDs(boundBindings, key, accountIDs)
|
||||
result = append(result, dto.AgentRoleChannelItem{
|
||||
Name: key,
|
||||
Bound: boundChannels[key],
|
||||
AccountIDs: extractChannelAccountIDs(channelConf),
|
||||
Bound: isRoleChannelFullyBound(boundBindings, key, accountIDs, availableAccountIDs),
|
||||
AccountIDs: availableAccountIDs,
|
||||
})
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
@@ -146,6 +147,50 @@ func (a AgentService) DeleteRole(req dto.AgentRoleDeleteReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) BindRole(req dto.AgentRoleBindReq) error {
|
||||
return a.operateRoleBinding(req, "bind")
|
||||
}
|
||||
|
||||
func (a AgentService) UnbindRole(req dto.AgentRoleBindReq) error {
|
||||
return a.operateRoleBinding(req, "unbind")
|
||||
}
|
||||
|
||||
func (a AgentService) operateRoleBinding(req dto.AgentRoleBindReq, action string) error {
|
||||
agent, install, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data")
|
||||
roleID := req.ID
|
||||
if roleID == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if _, ok := findConfiguredAgentByID(baseDir, conf, roleID); !ok {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
binding := formatRoleBinding(req.Channel, req.AccountID)
|
||||
if binding == "" {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
args := []string{
|
||||
"exec",
|
||||
install.ContainerName,
|
||||
"openclaw",
|
||||
"agents",
|
||||
action,
|
||||
"--agent",
|
||||
roleID,
|
||||
"--bind",
|
||||
binding,
|
||||
}
|
||||
args = append(args, "--json")
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
|
||||
_, err = mgr.RunWithStdout("docker", args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error) {
|
||||
agent, err := loadOpenclawAgentByID(req.AgentID)
|
||||
if err != nil {
|
||||
@@ -250,6 +295,16 @@ func findConfiguredAgentByID(baseDir string, conf map[string]interface{}, id str
|
||||
return dto.AgentConfiguredAgentItem{}, false
|
||||
}
|
||||
|
||||
func formatRoleBinding(channel, accountID string) string {
|
||||
if channel == "" {
|
||||
return ""
|
||||
}
|
||||
if accountID == "" {
|
||||
return channel
|
||||
}
|
||||
return channel + ":" + accountID
|
||||
}
|
||||
|
||||
func applyConfiguredAgentBindings(agents []dto.AgentConfiguredAgentItem, value interface{}) {
|
||||
bindings, ok := value.([]interface{})
|
||||
if !ok || len(agents) == 0 {
|
||||
@@ -305,8 +360,8 @@ func applyConfiguredAgentBindings(agents []dto.AgentConfiguredAgentItem, value i
|
||||
}
|
||||
}
|
||||
|
||||
func loadBoundChannelSet(value interface{}) map[string]bool {
|
||||
result := make(map[string]bool)
|
||||
func loadBoundChannelBindings(value interface{}) map[string]map[string]struct{} {
|
||||
result := make(map[string]map[string]struct{})
|
||||
bindings, ok := value.([]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
@@ -328,12 +383,112 @@ func loadBoundChannelSet(value interface{}) map[string]bool {
|
||||
if channel == "" {
|
||||
continue
|
||||
}
|
||||
result[channel] = true
|
||||
accountID, _ := match["accountId"].(string)
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
accountID, _ = record["accountId"].(string)
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
}
|
||||
if _, ok := result[channel]; !ok {
|
||||
result[channel] = make(map[string]struct{})
|
||||
}
|
||||
result[channel][accountID] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func extractChannelAccountIDs(channel map[string]interface{}) []string {
|
||||
func extractRoleChannelAccountIDs(conf map[string]interface{}, channel string) []string {
|
||||
switch channel {
|
||||
case "feishu":
|
||||
config := extractFeishuConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "telegram":
|
||||
config := extractTelegramConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "discord":
|
||||
config := extractDiscordConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "qqbot":
|
||||
config := extractQQBotConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "dingtalk-connector":
|
||||
config := extractDingTalkConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "wecom":
|
||||
return []string{}
|
||||
default:
|
||||
return extractRawChannelAccountIDs(getChannelConfig(conf, channel))
|
||||
}
|
||||
}
|
||||
|
||||
func filterAvailableChannelAccountIDs(bindings map[string]map[string]struct{}, channel string, accountIDs []string) []string {
|
||||
channelBindings, ok := bindings[strings.TrimSpace(channel)]
|
||||
if !ok || len(channelBindings) == 0 {
|
||||
return append([]string(nil), accountIDs...)
|
||||
}
|
||||
if _, ok := channelBindings[""]; ok {
|
||||
return []string{}
|
||||
}
|
||||
result := make([]string, 0, len(accountIDs))
|
||||
for _, accountID := range accountIDs {
|
||||
if _, ok := channelBindings[accountID]; ok {
|
||||
continue
|
||||
}
|
||||
result = append(result, accountID)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isRoleChannelFullyBound(bindings map[string]map[string]struct{}, channel string, allAccountIDs, availableAccountIDs []string) bool {
|
||||
channelBindings, ok := bindings[strings.TrimSpace(channel)]
|
||||
if !ok || len(channelBindings) == 0 {
|
||||
return false
|
||||
}
|
||||
if _, ok := channelBindings[""]; ok {
|
||||
return true
|
||||
}
|
||||
if len(allAccountIDs) == 0 {
|
||||
return true
|
||||
}
|
||||
return len(availableAccountIDs) == 0
|
||||
}
|
||||
|
||||
func extractRawChannelAccountIDs(channel map[string]interface{}) []string {
|
||||
if len(channel) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
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/bind", baseApi.BindAgentRole)
|
||||
aiToolsRouter.POST("/agents/agent/unbind", baseApi.UnbindAgentRole)
|
||||
aiToolsRouter.POST("/agents/agent/list", baseApi.GetConfiguredAgentRoles)
|
||||
aiToolsRouter.POST("/agents/agent/channels", baseApi.GetAgentRoleChannels)
|
||||
aiToolsRouter.POST("/agents/agent/md/list", baseApi.GetAgentRoleMarkdownFiles)
|
||||
|
||||
@@ -335,6 +335,13 @@ export namespace AI {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface AgentRoleBindReq {
|
||||
agentId: number;
|
||||
id: string;
|
||||
channel: string;
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
export interface AgentConfiguredAgentsReq {
|
||||
agentId: number;
|
||||
}
|
||||
|
||||
@@ -129,6 +129,14 @@ export const deleteAgentRole = (req: AI.AgentRoleDeleteReq) => {
|
||||
return http.post(`/ai/agents/agent/delete`, req);
|
||||
};
|
||||
|
||||
export const bindAgentRole = (req: AI.AgentRoleBindReq) => {
|
||||
return http.post(`/ai/agents/agent/bind`, req);
|
||||
};
|
||||
|
||||
export const unbindAgentRole = (req: AI.AgentRoleBindReq) => {
|
||||
return http.post(`/ai/agents/agent/unbind`, req);
|
||||
};
|
||||
|
||||
export const getConfiguredAgentRoles = (req: AI.AgentConfiguredAgentsReq) => {
|
||||
return http.post<AI.AgentConfiguredAgentItem[]>(`/ai/agents/agent/list`, req);
|
||||
};
|
||||
|
||||
@@ -752,6 +752,7 @@ const message = {
|
||||
],
|
||||
},
|
||||
bindings: 'Bindings',
|
||||
duplicateBinding: 'The channel and account ID combination must be unique',
|
||||
accountIdOptional: 'Account ID (Optional)',
|
||||
saveAllMd: 'Save All',
|
||||
roleMarkdownRestartHelper:
|
||||
|
||||
@@ -703,6 +703,7 @@ const message = {
|
||||
'BOOTSTRAP.md': ['首次运行引导流程', '只会在全新的工作区中创建。'],
|
||||
},
|
||||
bindings: '绑定',
|
||||
duplicateBinding: '频道和账号 ID 不能重复',
|
||||
accountIdOptional: '账号 ID(可选)',
|
||||
saveAllMd: '保存全部',
|
||||
roleMarkdownRestartHelper: '保存当前全部 MD 文件后,需要重启容器才能生效。请选择立即重启或稍后重启。',
|
||||
|
||||
@@ -234,10 +234,6 @@ html {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.el-input-group__append {
|
||||
button.el-button {
|
||||
span {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<DialogPro v-model="open" :title="$t('commons.button.bind')" size="large" @close="handleClose">
|
||||
<div v-loading="loading" class="binding-dialog">
|
||||
<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="220">
|
||||
<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="100" align="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="removeBinding($index)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="binding-dialog__empty">
|
||||
<el-empty :description="$t('commons.msg.noneData')" :image-size="60" />
|
||||
</div>
|
||||
<div class="binding-dialog__actions">
|
||||
<el-button type="primary" link :disabled="loading || submitting" @click="addBinding">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="loading || !form.bindings.length"
|
||||
@click="submitBind"
|
||||
>
|
||||
{{ $t('commons.button.bind') }}
|
||||
</el-button>
|
||||
<el-button :disabled="loading || submitting" @click="handleClose">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</DialogPro>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { bindAgentRole, getAgentRoleChannels } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
accountIds: string[];
|
||||
}
|
||||
|
||||
interface DialogParams {
|
||||
agentId: number;
|
||||
role: AI.AgentConfiguredAgentItem;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const open = ref(false);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const agentId = ref(0);
|
||||
const roleId = ref('');
|
||||
const channelOptions = ref<SelectOption[]>([]);
|
||||
|
||||
const form = ref<{ bindings: AI.AgentRoleBinding[] }>({
|
||||
bindings: [],
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
bindings: [],
|
||||
};
|
||||
};
|
||||
|
||||
const addBinding = () => {
|
||||
form.value.bindings.push({
|
||||
channel: '',
|
||||
accountId: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeBinding = (index: number) => {
|
||||
form.value.bindings.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleBindingChannelChange = (index: number) => {
|
||||
const binding = form.value.bindings[index];
|
||||
if (!binding) {
|
||||
return;
|
||||
}
|
||||
binding.accountId = '';
|
||||
const options = getAccountIdOptions(binding.channel);
|
||||
if (options.length === 1) {
|
||||
binding.accountId = options[0];
|
||||
}
|
||||
};
|
||||
|
||||
const getAccountIdOptions = (channel: string) => {
|
||||
return channelOptions.value.find((item) => item.value === channel)?.accountIds || [];
|
||||
};
|
||||
|
||||
const isChannelDisabled = (option: SelectOption, index: number) => {
|
||||
if (option.accountIds.length > 0) {
|
||||
return false;
|
||||
}
|
||||
return form.value.bindings.some((item, bindingIndex) => bindingIndex !== index && item.channel === option.value);
|
||||
};
|
||||
|
||||
const loadBindOptions = async () => {
|
||||
const res = await getAgentRoleChannels({ agentId: agentId.value });
|
||||
channelOptions.value = (res.data || [])
|
||||
.filter((item) => !item.bound)
|
||||
.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
accountIds: item.accountIds || [],
|
||||
}));
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
await loadBindOptions();
|
||||
};
|
||||
|
||||
const acceptParams = async (params: DialogParams) => {
|
||||
agentId.value = params.agentId;
|
||||
roleId.value = params.role.id;
|
||||
resetForm();
|
||||
loading.value = true;
|
||||
try {
|
||||
await refreshData();
|
||||
addBinding();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const submitBind = async () => {
|
||||
const bindings = form.value.bindings.filter((item) => item.channel);
|
||||
if (!bindings.length) {
|
||||
MsgError(i18n.global.t('commons.msg.selectOne', [i18n.global.t('aiTools.agents.channelsTab')]));
|
||||
return;
|
||||
}
|
||||
const hasDuplicate = bindings.some((item, index) =>
|
||||
bindings.some(
|
||||
(current, currentIndex) =>
|
||||
currentIndex !== index && current.channel === item.channel && current.accountId === item.accountId,
|
||||
),
|
||||
);
|
||||
if (hasDuplicate) {
|
||||
MsgError(i18n.global.t('aiTools.agents.duplicateBinding'));
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
loading.value = true;
|
||||
try {
|
||||
for (const item of bindings) {
|
||||
await bindAgentRole({
|
||||
agentId: agentId.value,
|
||||
id: roleId.value,
|
||||
channel: item.channel,
|
||||
accountId: item.accountId,
|
||||
});
|
||||
}
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
resetForm();
|
||||
emit('success');
|
||||
handleClose();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
channelOptions.value = [];
|
||||
resetForm();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.binding-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.bindings-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.binding-dialog__actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bindings-table :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.binding-dialog__empty {
|
||||
padding: 12px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,6 @@
|
||||
<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>
|
||||
@@ -90,7 +89,7 @@ import { createAgentRole, getAgentRoleChannels, pageAgentAccounts } from '@/api/
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
|
||||
interface SelectOption {
|
||||
@@ -151,6 +150,10 @@ const handleBindingChannelChange = (index: number) => {
|
||||
return;
|
||||
}
|
||||
binding.accountId = '';
|
||||
const options = getAccountIdOptions(binding.channel);
|
||||
if (options.length === 1) {
|
||||
binding.accountId = options[0];
|
||||
}
|
||||
};
|
||||
|
||||
const getAccountIdOptions = (channel: string) => {
|
||||
@@ -161,6 +164,9 @@ const isChannelDisabled = (option: SelectOption, index: number) => {
|
||||
if (option.bound) {
|
||||
return true;
|
||||
}
|
||||
if ((option.accountIds || []).length > 0) {
|
||||
return false;
|
||||
}
|
||||
return form.bindings.some((item, bindingIndex) => bindingIndex !== index && item.channel === option.value);
|
||||
};
|
||||
|
||||
@@ -209,18 +215,27 @@ const submit = async () => {
|
||||
return;
|
||||
}
|
||||
await formRef.value.validate();
|
||||
const bindings = form.bindings.filter((item) => item.channel);
|
||||
const hasDuplicate = bindings.some((item, index) =>
|
||||
bindings.some(
|
||||
(current, currentIndex) =>
|
||||
currentIndex !== index && current.channel === item.channel && current.accountId === item.accountId,
|
||||
),
|
||||
);
|
||||
if (hasDuplicate) {
|
||||
MsgError(i18n.global.t('aiTools.agents.duplicateBinding'));
|
||||
return;
|
||||
}
|
||||
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(),
|
||||
})),
|
||||
bindings: bindings.map((item) => ({
|
||||
channel: item.channel,
|
||||
accountId: item.accountId.trim(),
|
||||
})),
|
||||
} as AI.AgentRoleCreateReq);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
emit('success');
|
||||
|
||||
@@ -12,49 +12,79 @@
|
||||
{{ $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-empty
|
||||
v-if="!configuredAgents.length"
|
||||
:description="$t('commons.msg.noneData')"
|
||||
:image-size="80"
|
||||
class="role-empty"
|
||||
/>
|
||||
<div v-else>
|
||||
<el-card v-for="row in configuredAgents" :key="row.id || row.name" class="role-card">
|
||||
<div class="role-card__header">
|
||||
<div class="role-card__name">
|
||||
<div class="role-card__field">
|
||||
<el-button
|
||||
v-if="row.workspace"
|
||||
type="primary"
|
||||
link
|
||||
class="role-card__title"
|
||||
@click="openDetailDrawer(row)"
|
||||
>
|
||||
{{ row.name || row.id || '-' }}
|
||||
</el-button>
|
||||
<span v-else class="role-card__title role-card__title--static">
|
||||
{{ row.name || row.id || '-' }}
|
||||
</span>
|
||||
<div class="role-card__model">{{ row.model || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="role-card__path-actions">
|
||||
<el-tooltip v-if="row.workspace" :content="row.workspace" placement="top">
|
||||
<el-button plain size="small" round @click="routerToFileWithPath(row.workspace)">
|
||||
{{ $t('aiTools.agents.workspace') }}
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="row.agentDir" :content="row.agentDir" placement="top">
|
||||
<el-button plain size="small" round @click="routerToFileWithPath(row.agentDir)">
|
||||
{{ $t('aiTools.agents.agentDir') }}
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-button plain size="small" round @click="handleDelete(row)">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="role-card__content">
|
||||
<div class="role-card__field role-card__field--bindings">
|
||||
<el-divider class="role-card__divider" border-style="dashed" />
|
||||
<div class="role-card__section-title">
|
||||
{{ $t('aiTools.agents.channelsTab') }}
|
||||
</div>
|
||||
<div v-if="row.bindings?.length" class="role-card__tags">
|
||||
<el-tag
|
||||
v-for="item in row.bindings"
|
||||
:key="`${item.channel}-${item.accountId || 'default'}`"
|
||||
closable
|
||||
@close="handleUnbind(row, item)"
|
||||
>
|
||||
{{ item.accountId ? `${item.channel}:${item.accountId}` : item.channel }}
|
||||
</el-tag>
|
||||
<el-button size="small" @click="openBindingDialog(row)">
|
||||
+ {{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="role-card__tags">
|
||||
<el-button size="small" @click="openBindingDialog(row)">
|
||||
+ {{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<CreateDialog ref="createRef" @success="handleCreated" />
|
||||
<BindingDialog ref="bindingRef" @success="handleCreated" />
|
||||
<DetailDrawer ref="detailRef" />
|
||||
<OpDialog ref="opRef" @search="handleCreated" />
|
||||
</template>
|
||||
@@ -63,11 +93,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { deleteAgentRole, getConfiguredAgentRoles } from '@/api/modules/ai';
|
||||
import { deleteAgentRole, getConfiguredAgentRoles, unbindAgentRole } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import i18n from '@/lang';
|
||||
import { routerToFileWithPath } from '@/utils/router';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import CreateDialog from './create/index.vue';
|
||||
import BindingDialog from './binding/index.vue';
|
||||
import DetailDrawer from './detail/index.vue';
|
||||
|
||||
interface AgentRoleLoadParams {
|
||||
@@ -80,6 +112,7 @@ interface AgentRoleLoadParams {
|
||||
|
||||
const loading = ref(false);
|
||||
const createRef = ref<InstanceType<typeof CreateDialog> | null>(null);
|
||||
const bindingRef = ref<InstanceType<typeof BindingDialog> | null>(null);
|
||||
const detailRef = ref<InstanceType<typeof DetailDrawer> | null>(null);
|
||||
const opRef = ref();
|
||||
const agentId = ref(0);
|
||||
@@ -101,6 +134,13 @@ const openCreateDialog = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const openBindingDialog = (row: AI.AgentConfiguredAgentItem) => {
|
||||
bindingRef.value?.acceptParams({
|
||||
agentId: agentId.value,
|
||||
role: row,
|
||||
});
|
||||
};
|
||||
|
||||
const openDetailDrawer = (row: AI.AgentConfiguredAgentItem) => {
|
||||
if (!row.workspace) {
|
||||
return;
|
||||
@@ -112,16 +152,6 @@ const openDetailDrawer = (row: AI.AgentConfiguredAgentItem) => {
|
||||
});
|
||||
};
|
||||
|
||||
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'),
|
||||
@@ -140,12 +170,21 @@ const handleDelete = async (row: AI.AgentConfiguredAgentItem) => {
|
||||
});
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: AI.AgentConfiguredAgentItem) => handleDelete(row),
|
||||
},
|
||||
];
|
||||
const handleUnbind = async (row: AI.AgentConfiguredAgentItem, item: AI.AgentRoleBinding) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await unbindAgentRole({
|
||||
agentId: agentId.value,
|
||||
id: row.id,
|
||||
channel: item.channel,
|
||||
accountId: item.accountId || '',
|
||||
});
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
await handleCreated();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreated = async () => {
|
||||
if (!agentId.value) {
|
||||
@@ -180,4 +219,95 @@ defineExpose({
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.role-empty {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
--el-card-border-color: var(--el-border-color);
|
||||
border-radius: 18px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.role-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.role-card__name {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.role-card__title {
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-card__title--static {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.role-card__model {
|
||||
margin-top: 6px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.role-card__field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.role-card__field--bindings {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.role-card__path-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.role-card__content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.role-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.role-card__divider {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.role-card__section-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.role-card__header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.role-card__path-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user