feat: Optimize the openclaw channel bot add flow (#12507)

This commit is contained in:
CityFun
2026-04-16 16:52:48 +08:00
committed by GitHub
parent 3c5b3c3467
commit df5d3d782c
25 changed files with 342 additions and 286 deletions

View File

@@ -371,7 +371,7 @@ type AgentTelegramConfig struct {
type AgentChannelPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot"`
PairingCode string `json:"pairingCode" validate:"required"`
AccountID string `json:"accountId"`
}

View File

@@ -5,11 +5,6 @@ import (
"strings"
)
type OpenClawPatch struct {
PrimaryModel string
Models map[string]interface{}
}
type OpenClawProviderPatch struct {
PrimaryModel string
ProviderKey string
@@ -20,24 +15,7 @@ type OpenClawProviderPatch struct {
AuthHeader bool
}
func BuildOpenClawPatch(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) {
providerPatch, err := BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey)
if err != nil {
return nil, err
}
_, maxTokens, contextWindow = ResolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
return newOpenClawPatch(
providerPatch,
resolveOpenClawModelName(provider, providerPatch.ModelID),
resolveOpenClawModelInput(provider, providerPatch.ModelID),
reasoning,
contextWindow,
maxTokens,
), nil
}
func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
modelName = strings.TrimSpace(modelName)
if modelName == "" {
return nil, fmt.Errorf("model is required")
}
@@ -45,29 +23,29 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey st
modelID := resolveOpenClawModelID(provider, modelName)
switch provider {
case "deepseek":
return newOpenClawProviderPatch(modelName, "deepseek", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
case "gemini":
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, strings.TrimSpace(apiKey), baseURL, resolvedAPIType, false), nil
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "moonshot", "kimi":
return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
case "bailian-coding-plan":
return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "ark-coding-plan":
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "minimax":
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, strings.TrimSpace(apiKey), baseURL, "anthropic-messages", true), nil
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
case "xiaomi":
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
case "custom", "vllm":
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, strings.TrimSpace(apiKey), strings.TrimSpace(baseURL), resolvedAPIType, false), nil
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "ollama":
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", strings.TrimSpace(baseURL), resolvedAPIType, false), nil
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
case "kimi-coding":
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, strings.TrimSpace(apiKey), baseURL, "anthropic-messages", false), nil
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
case "zai":
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false), nil
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
default:
return newOpenClawProviderPatch(modelName, provider, modelID, strings.TrimSpace(apiKey), baseURL, resolvedAPIType, false), nil
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
}
}
@@ -78,7 +56,7 @@ func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey st
providerKey = "moonshot"
primaryModel = "moonshot/" + modelID
}
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, strings.TrimSpace(apiKey), baseURL, "openai-completions", false)
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
}
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
@@ -93,84 +71,16 @@ func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseUR
}
}
func newOpenClawPatch(providerPatch *OpenClawProviderPatch, modelName string, input []string, reasoning bool, contextWindow, maxTokens int) *OpenClawPatch {
model := map[string]interface{}{
"id": providerPatch.ModelID,
"name": modelName,
"reasoning": reasoning,
"input": input,
"contextWindow": contextWindow,
"maxTokens": maxTokens,
"cost": map[string]interface{}{},
}
providerConfig := map[string]interface{}{
"apiKey": providerPatch.APIKey,
"baseUrl": providerPatch.BaseURL,
"api": providerPatch.APIType,
"models": []map[string]interface{}{model},
}
if providerPatch.AuthHeader {
providerConfig["authHeader"] = true
}
return &OpenClawPatch{
PrimaryModel: providerPatch.PrimaryModel,
Models: map[string]interface{}{
"mode": "merge",
"providers": map[string]interface{}{
providerPatch.ProviderKey: providerConfig,
},
},
}
}
func resolveOpenClawModelID(provider, modelName string) string {
if provider == "custom" || provider == "vllm" {
return normalizeCustomModel(modelName)
target := strings.TrimLeft(modelName, "/")
if parts := strings.SplitN(target, "/", 2); len(parts) == 2 && parts[0] == "custom" {
return strings.TrimLeft(parts[1], "/")
}
return target
}
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
return parts[1]
}
return modelName
}
func resolveOpenClawModelName(provider, modelID string) string {
if item, ok := resolveOpenClawCatalogModel(provider, modelID); ok {
return item.Name
}
if provider == "kimi-coding" {
return "Kimi for Coding"
}
return modelID
}
func resolveOpenClawModelInput(provider, modelID string) []string {
if item, ok := resolveOpenClawCatalogModel(provider, modelID); ok && len(item.Input) > 0 {
return item.Input
}
if provider == "kimi-coding" {
return []string{"text", "image"}
}
return []string{"text"}
}
func resolveOpenClawCatalogModel(provider, modelID string) (Model, bool) {
candidates := []string{provider + "/" + modelID}
if provider == "gemini" {
candidates = append(candidates, "google/"+modelID)
}
for _, candidate := range candidates {
if item, ok := FindModel(provider, candidate); ok {
return item, true
}
}
return Model{}, false
}
func normalizeCustomModel(modelName string) string {
trim := strings.TrimSpace(modelName)
trim = strings.TrimLeft(trim, "/")
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 && strings.EqualFold(parts[0], "custom") {
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
}
return trim
}

View File

@@ -95,7 +95,7 @@ type IAgentService interface {
}
const (
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome"
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/openclaw-browser"
defaultBrowserProfile = "openclaw"
defaultUserTimezone = "Asia/Shanghai"
defaultToolsProfile = "full"
@@ -114,9 +114,6 @@ const (
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
agentType := req.AgentType
if agentType != constant.AppOpenclaw && agentType != constant.AppCopaw && agentType != constant.AppHermesAgent {
return nil, fmt.Errorf("unsupported agent type: %s", agentType)
}
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
@@ -516,8 +513,8 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
return err
}
} else {
if agent, err = loadOpenclawAgentByID(req.AgentID); err != nil {
return err
if agent.AgentType != constant.AppOpenclaw {
return fmt.Errorf("%s does not support", agent.AgentType)
}
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
return err
@@ -1068,21 +1065,15 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return nil
}
for _, agent := range agents {
modelName := strings.TrimSpace(agent.Model)
var selectedAccountModel dto.AgentAccountModel
if modelName != "" {
selectedAccountModel, err = requireAgentAccountModelForProvider(account.Provider, accountModels, modelName)
if err != nil {
return buserr.WithName("ErrAgentModelInUse", agent.Name)
}
} else {
selectedAccountModel = accountModels[0]
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, agent.Model)
if err != nil {
return buserr.WithName("ErrAgentModelInUse", agent.Name)
}
resolvedRuntime, err := buildOpenclawAccountModelRuntime(account, selectedAccountModel)
if err != nil {
return err
}
modelName = resolvedRuntime.StoredModel
modelName := resolvedRuntime.StoredModel
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
confDir := path.Dir(agent.ConfigPath)
switch agent.AgentType {

View File

@@ -570,7 +570,7 @@ func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
RequireMention: "true",
GroupPolicy: "open",
GroupAllowFrom: []string{},
Bots: []dto.AgentFeishuBot{defaultFeishuBot()},
Bots: []dto.AgentFeishuBot{},
}
feishu := getChannelConfig(conf, "feishu")
if len(feishu) == 0 {
@@ -620,7 +620,10 @@ func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
baseEnabled := defaultBot.Enabled
baseDmPolicy := defaultBot.DmPolicy
baseAllowFrom := append([]string(nil), defaultBot.AllowFrom...)
bots := []dto.AgentFeishuBot{defaultBot}
bots := make([]dto.AgentFeishuBot, 0, len(accounts)+1)
if defaultBot.AppID != "" || defaultBot.AppSecret != "" {
bots = append(bots, defaultBot)
}
for _, accountID := range sortedChildKeys(accounts) {
if accountID == "default" {
continue
@@ -942,31 +945,34 @@ func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
result := dto.AgentQQBotConfig{Enabled: true}
qqbot := getChannelConfig(conf, "qqbot")
if len(qqbot) == 0 {
result.Bots = []dto.AgentQQBotBot{defaultQQBot()}
result.Bots = []dto.AgentQQBotBot{}
return result
}
if enabled, ok := qqbot["enabled"].(bool); ok {
result.Enabled = enabled
}
bots := []dto.AgentQQBotBot{
{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: "default",
Name: extractStringValue(qqbot["name"]),
Enabled: extractBoolValue(qqbot["enabled"], true),
IsDefault: true,
},
AppID: extractStringValue(qqbot["appId"]),
ClientSecret: extractStringValue(qqbot["clientSecret"]),
AllowFrom: extractStringList(qqbot["allowFrom"]),
SystemPrompt: extractStringValue(qqbot["systemPrompt"]),
bots := make([]dto.AgentQQBotBot, 0, len(childMap(qqbot, "accounts"))+1)
defaultBot := dto.AgentQQBotBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: "default",
Name: extractStringValue(qqbot["name"]),
Enabled: extractBoolValue(qqbot["enabled"], true),
IsDefault: true,
},
AppID: extractStringValue(qqbot["appId"]),
ClientSecret: extractStringValue(qqbot["clientSecret"]),
AllowFrom: extractStringList(qqbot["allowFrom"]),
SystemPrompt: extractStringValue(qqbot["systemPrompt"]),
}
if bots[0].Name == "" {
bots[0].Name = "Default"
if defaultBot.Name == "" {
defaultBot.Name = "Default"
}
for _, accountID := range sortedChildKeys(childMap(qqbot, "accounts")) {
account := childMap(childMap(qqbot, "accounts"), accountID)
if defaultBot.AppID != "" || defaultBot.ClientSecret != "" {
bots = append(bots, defaultBot)
}
accounts := childMap(qqbot, "accounts")
for _, accountID := range sortedChildKeys(accounts) {
account := childMap(accounts, accountID)
bots = append(bots, dto.AgentQQBotBot{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: accountID,

View File

@@ -78,7 +78,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
cfg["model"] = map[string]interface{}{
"default": resolveHermesModel(account.Provider, provider, modelName),
"provider": provider,
"base_url": strings.TrimSpace(account.BaseURL),
"base_url": account.BaseURL,
}
cfg["terminal"] = map[string]interface{}{
"backend": "local",
@@ -390,8 +390,8 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
if account == nil {
return nil
}
apiKey := strings.TrimSpace(account.APIKey)
baseURL := strings.TrimSpace(account.BaseURL)
apiKey := account.APIKey
baseURL := account.BaseURL
entries := make([]hermesEnvEntry, 0, 4)
appendEntry := func(key, value string) {

View File

@@ -1006,14 +1006,10 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
return "", nil, nil, err
}
if len(accountModels) == 0 {
return "", nil, nil, fmt.Errorf("model is required")
}
selectedModel = strings.TrimSpace(selectedModel)
if selectedModel == "" {
selectedModel = strings.TrimSpace(accountModels[0].ID)
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
}
if selectedModel == "" {
return "", nil, nil, fmt.Errorf("model is required")
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
}
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, selectedModel)
if err != nil {
@@ -1464,9 +1460,6 @@ func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.Agent
return err
}
for _, agent := range agents {
if strings.TrimSpace(agent.Model) == "" {
continue
}
if _, ok := findAgentAccountModelForProvider(account.Provider, models, agent.Model); !ok {
return buserr.WithName("ErrAgentModelInUse", agent.Name)
}

View File

@@ -704,7 +704,7 @@ export namespace AI {
export interface AgentQQBotConfig {
enabled: boolean;
dmPolicy?: 'open' | 'allowlist' | 'disabled';
dmPolicy?: 'pairing' | 'open' | 'allowlist' | 'disabled';
allowFrom?: string[];
groupPolicy?: 'open' | 'allowlist' | 'disabled';
groupAllowFrom?: string[];
@@ -715,7 +715,7 @@ export namespace AI {
export interface AgentQQBotConfigUpdateReq {
agentId: number;
enabled: boolean;
dmPolicy?: 'open' | 'allowlist' | 'disabled';
dmPolicy?: 'pairing' | 'open' | 'allowlist' | 'disabled';
allowFrom?: string[];
groupPolicy?: 'open' | 'allowlist' | 'disabled';
groupAllowFrom?: string[];

View File

@@ -801,8 +801,10 @@ const message = {
bots: 'Bots',
addBot: 'Add Bot',
accountId: 'Account ID',
accountIdPlaceholder: 'A unique ID used to distinguish bots and for later config and routing.',
setDefaultBot: 'Set as Default',
botDuplicateAccountId: 'Account ID already exists',
botDuplicateField: '{field} already exists',
botRequired: 'Add at least one bot',
botId: 'Bot ID',
allowFrom: 'DM Allowlist',

View File

@@ -811,8 +811,11 @@ const message = {
bots: 'Bots',
addBot: 'Agregar bot',
accountId: 'ID de cuenta',
accountIdPlaceholder:
'Un ID único para distinguir bots y usarlo después en la configuración y el enrutamiento.',
setDefaultBot: 'Establecer como predeterminado',
botDuplicateAccountId: 'El ID de cuenta ya existe',
botDuplicateField: '{field} ya existe',
botRequired: 'Agregue al menos un bot',
botId: 'ID del bot',
allowFrom: 'Lista permitida de MD',

View File

@@ -804,8 +804,10 @@ const message = {
bots: 'Bot',
addBot: 'Bot を追加',
accountId: 'アカウント ID',
accountIdPlaceholder: 'Bot を識別するための一意 ID です保存後は設定とルーティング識別に使用されます',
setDefaultBot: 'デフォルトに設定',
botDuplicateAccountId: 'アカウント ID は既に存在します',
botDuplicateField: '{field} は既に存在します',
botRequired: '少なくとも 1 つの Bot を追加してください',
botId: 'Bot ID',
allowFrom: 'DM 許可リスト',

View File

@@ -790,8 +790,10 @@ const message = {
bots: '봇',
addBot: ' 추가',
accountId: '계정 ID',
accountIdPlaceholder: '봇을 구분하는 고유 ID이며 저장 설정과 라우팅 식별에 사용됩니다.',
setDefaultBot: '기본값으로 설정',
botDuplicateAccountId: '계정 ID가 이미 존재합니다',
botDuplicateField: '{field}() 이미 존재합니다',
botRequired: '봇을 하나 이상 추가하세요',
botId: ' ID',
allowFrom: 'DM 허용 목록',

View File

@@ -809,8 +809,11 @@ const message = {
bots: 'Bot',
addBot: 'Tambah bot',
accountId: 'ID akaun',
accountIdPlaceholder:
'ID unik untuk membezakan bot dan digunakan untuk konfigurasi serta penghalaan selepas disimpan.',
setDefaultBot: 'Tetapkan sebagai lalai',
botDuplicateAccountId: 'ID akaun sudah wujud',
botDuplicateField: '{field} sudah wujud',
botRequired: 'Tambah sekurang-kurangnya satu bot',
botId: 'ID bot',
allowFrom: 'Senarai benarkan DM',

View File

@@ -806,8 +806,10 @@ const message = {
bots: 'Bots',
addBot: 'Adicionar bot',
accountId: 'ID da conta',
accountIdPlaceholder: 'Um ID único para diferenciar bots e usá-los depois na configuração e no roteamento.',
setDefaultBot: 'Definir como padrão',
botDuplicateAccountId: 'O ID da conta existe',
botDuplicateField: '{field} existe',
botRequired: 'Adicione pelo menos um bot',
botId: 'ID do bot',
allowFrom: 'Lista de permissões de DM',

View File

@@ -801,8 +801,11 @@ const message = {
bots: 'Боты',
addBot: 'Добавить бота',
accountId: 'ID аккаунта',
accountIdPlaceholder:
'Уникальный ID для различения ботов, который после сохранения используется в настройке и маршрутизации.',
setDefaultBot: 'Сделать по умолчанию',
botDuplicateAccountId: 'ID аккаунта уже существует',
botDuplicateField: '{field} уже существует',
botRequired: 'Добавьте хотя бы одного бота',
botId: 'ID бота',
allowFrom: 'Белый список DM',

View File

@@ -808,8 +808,11 @@ const message = {
bots: 'Botlar',
addBot: 'Bot ekle',
accountId: 'Hesap ID',
accountIdPlaceholder:
'Botlari ayirt etmek ve sonraki yapılandırma ile yönlendirme için kullanılan benzersiz kimlik.',
setDefaultBot: 'Varsayılan yap',
botDuplicateAccountId: 'Hesap ID zaten mevcut',
botDuplicateField: '{field} zaten mevcut',
botRequired: 'En az bir bot ekleyin',
botId: 'Bot ID',
allowFrom: 'DM izin listesi',

View File

@@ -744,7 +744,7 @@ const message = {
dmPolicy: '私聊策略',
threadSession: '執行緒會話',
replyMode: '回覆模式',
requireMention: '提及要求',
requireMention: '群聊需 @ 機器人',
requireMentionTrue: '需要 @',
requireMentionFalse: '無需 @',
requireMentionOpen: '依群組設定',
@@ -755,8 +755,10 @@ const message = {
bots: 'Bot 列表',
addBot: '新增 Bot',
accountId: '帳戶 ID',
accountIdPlaceholder: '用於區分不同 Bot 的唯一標識,儲存後用於配置和路由識別',
setDefaultBot: '設為預設 Bot',
botDuplicateAccountId: '帳戶 ID 不能重複',
botDuplicateField: '{field} 不能重複',
botRequired: '請至少新增一個 Bot',
botId: 'Bot ID',
allowFrom: '私聊白名單',

View File

@@ -739,7 +739,7 @@ const message = {
dmPolicy: '私聊策略',
threadSession: '线程会话',
replyMode: '回复模式',
requireMention: '提及要求',
requireMention: '群聊需 @ 机器人',
requireMentionTrue: '需要 @',
requireMentionFalse: '无需 @',
requireMentionOpen: '按群组配置',
@@ -750,8 +750,10 @@ const message = {
bots: 'Bot 列表',
addBot: '新增 Bot',
accountId: '账户 ID',
accountIdPlaceholder: '用于区分不同 Bot 的唯一标识,保存后用于配置和路由识别',
setDefaultBot: '设为默认 Bot',
botDuplicateAccountId: '账户 ID 不能重复',
botDuplicateField: '{field} 不能重复',
botRequired: '请至少添加一个 Bot',
botId: 'Bot ID',
allowFrom: '私聊白名单',

View File

@@ -23,7 +23,13 @@
<span>{{ summaryFormatter(row) }}</span>
</template>
</el-table-column>
<el-table-column :label="t('commons.table.status')" width="120">
<el-table-column
:label="t('commons.table.status')"
width="120"
fixed="right"
class-name="channel-bots__status-column"
label-class-name="channel-bots__status-column"
>
<template #default="{ row, $index }">
<el-switch
:model-value="row.enabled"
@@ -32,12 +38,26 @@
/>
</template>
</el-table-column>
<el-table-column :label="t('commons.table.operate')" width="180" fixed="right">
<el-table-column :label="t('commons.table.operate')" width="240" fixed="right">
<template #default="{ row, $index }">
<div class="channel-bots__actions">
<el-button link type="primary" :disabled="disabled" @click="openEdit(row, $index)">
{{ t('commons.button.edit') }}
</el-button>
<el-button
v-if="approvable"
link
type="primary"
:disabled="
disabled ||
!row.enabled ||
isBotActionDisabled(row) ||
(approveDisabled ? approveDisabled(row) : false)
"
@click="emit('approve', row)"
>
{{ t('aiTools.agents.approvePairing') }}
</el-button>
<el-button
:disabled="disabled || undeletableAccountIds.includes(row.accountId)"
link
@@ -63,18 +83,6 @@
>
{{ t('aiTools.agents.setDefaultBot') }}
</el-dropdown-item>
<el-dropdown-item
v-if="approvable"
command="approve"
:disabled="
disabled ||
!row.enabled ||
isBotActionDisabled(row) ||
(approveDisabled ? approveDisabled(row) : false)
"
>
{{ t('aiTools.agents.approvePairing') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -89,7 +97,11 @@
<el-input v-model="form.name" :disabled="disabled" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.accountId')" prop="accountId">
<el-input v-model="form.accountId" :disabled="disabled || accountIdLocked" />
<el-input
v-model="form.accountId"
:disabled="disabled || accountIdLocked"
:placeholder="t('aiTools.agents.accountIdPlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('commons.table.status')">
<el-switch v-model="form.enabled" :disabled="disabled" />
@@ -222,6 +234,18 @@ const props = defineProps({
type: Array as PropType<string[]>,
default: () => [],
},
uniqueFieldProp: {
type: String,
default: '',
},
uniqueFieldLabel: {
type: String,
default: '',
},
createPreset: {
type: Object as PropType<Partial<ChannelBotItem>>,
default: undefined,
},
});
const emit = defineEmits<{
@@ -238,7 +262,9 @@ const formRef = ref<FormInstance>();
const form = reactive<ChannelBotItem>(props.createBot());
const accountIdLocked = computed(() => props.fixedAccountIds.includes(editingAccountId.value));
const accountIdLocked = computed(() =>
props.fixedAccountIds.includes(editingAccountId.value || String(form.accountId || '')),
);
const dialogTitle = computed(() => (editIndex.value >= 0 ? t('commons.button.edit') : t('commons.button.create')));
const rules = computed<FormRules>(() => {
@@ -269,6 +295,30 @@ const rules = computed<FormRules>(() => {
}
config[field.prop] = [Rules.requiredInput];
}
if (props.uniqueFieldProp) {
config[props.uniqueFieldProp] = [
...(config[props.uniqueFieldProp] || []),
{
validator: (_rule, value, callback) => {
const exists = props.bots.some(
(bot, index) => index !== editIndex.value && bot[props.uniqueFieldProp] === value,
);
if (exists) {
callback(
new Error(
t('aiTools.agents.botDuplicateField', {
field: props.uniqueFieldLabel || props.uniqueFieldProp,
}),
),
);
return;
}
callback();
},
trigger: 'blur',
},
];
}
return config;
});
@@ -276,7 +326,7 @@ const resetForm = (bot?: ChannelBotItem) => {
Object.keys(form).forEach((key) => {
delete form[key];
});
Object.assign(form, props.createBot(), bot ? { ...bot } : {});
Object.assign(form, props.createBot(), bot ? { ...bot } : { ...(props.createPreset || {}) });
};
const emitBots = (bots: ChannelBotItem[]) => {
@@ -292,7 +342,7 @@ const isBotActionDisabled = (bot: ChannelBotItem) => {
};
const hasMoreActions = (bot: ChannelBotItem) => {
return (props.defaultable && !bot.isDefault) || props.approvable;
return props.defaultable && !bot.isDefault;
};
const openCreate = () => {
@@ -361,10 +411,6 @@ const updateEnabled = (index: number, enabled: boolean | string | number) => {
const handleMoreCommand = (bot: ChannelBotItem, _index: number, command: string | number | object) => {
if (command === 'default') {
setDefault(bot.accountId);
return;
}
if (command === 'approve') {
emit('approve', bot);
}
};
</script>
@@ -403,4 +449,8 @@ const handleMoreCommand = (bot: ChannelBotItem, _index: number, command: string
white-space: nowrap;
}
}
:deep(.channel-bots__status-column) {
box-shadow: inset 1px 0 0 var(--el-table-border-color);
}
</style>

View File

@@ -11,6 +11,7 @@
</el-form-item>
<el-form-item :label="t('aiTools.agents.dmPolicy')" prop="dmPolicy">
<el-select v-model="form.dmPolicy">
<el-option :label="t('aiTools.agents.pairingCode')" value="pairing" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
@@ -51,6 +52,16 @@
</el-button>
</el-form-item>
<el-alert type="info" :closable="false" :title="t('aiTools.agents.channelAutoRestartHelper')" />
<template v-if="form.dmPolicy === 'pairing'">
<el-form-item :label="t('aiTools.agents.pairingCode')">
<el-input v-model="pairingCode" :placeholder="t('aiTools.agents.pairingCodePlaceholder')" />
</el-form-item>
<el-form-item>
<el-button type="primary" plain :loading="approving" @click="approvePairing">
{{ t('aiTools.agents.approvePairing') }}
</el-button>
</el-form-item>
</template>
</el-form>
</template>
@@ -58,15 +69,15 @@
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { getAgentQQBotConfig, updateAgentQQBotConfig } from '@/api/modules/ai';
import { approveAgentChannelPairing, getAgentQQBotConfig, updateAgentQQBotConfig } from '@/api/modules/ai';
import { Rules } from '@/global/form-rules';
import { MsgSuccess } from '@/utils/message';
import { MsgSuccess, MsgWarning } from '@/utils/message';
interface QQBotForm {
enabled: boolean;
appId: string;
clientSecret: string;
dmPolicy: 'open' | 'allowlist' | 'disabled';
dmPolicy: 'pairing' | 'open' | 'allowlist' | 'disabled';
allowFromText: string;
groupPolicy: 'open' | 'allowlist' | 'disabled';
groupAllowFromText: string;
@@ -75,7 +86,9 @@ interface QQBotForm {
const { t } = useI18n();
const formRef = ref<FormInstance>();
const saving = ref(false);
const approving = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const form = reactive<QQBotForm>({
enabled: true,
appId: '',
@@ -130,6 +143,7 @@ const rules = reactive({
const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentQQBotConfig({ agentId: id });
form.enabled = res.data?.enabled ?? true;
form.appId = res.data?.bots?.[0]?.appId || '';
@@ -173,6 +187,28 @@ const save = async () => {
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodePlaceholder'));
return;
}
approving.value = true;
try {
await approveAgentChannelPairing({
agentId: agentId.value,
type: 'qqbot',
pairingCode: pairingCode.value,
});
pairingCode.value = '';
MsgSuccess(t('aiTools.agents.pairingApproveSuccess'));
} finally {
approving.value = false;
}
};
defineExpose({
load,
});

View File

@@ -18,70 +18,74 @@
<el-form-item :label="t('commons.table.status')" class="mt-4">
<el-switch v-model="form.enabled" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.dmPolicy')" prop="dmPolicy">
<el-select v-model="form.dmPolicy">
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
<el-form-item
v-if="form.dmPolicy === 'allowlist'"
:label="t('aiTools.agents.allowFrom')"
prop="allowFromText"
>
<el-input
v-model="form.allowFromText"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.allowFromPlaceholder')"
/>
<span class="input-help">{{ t('aiTools.agents.allowFromHelper') }}</span>
</el-form-item>
<el-form-item :label="t('aiTools.agents.groupPolicy')" prop="groupPolicy">
<el-select v-model="form.groupPolicy">
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
<el-form-item
v-if="form.groupPolicy === 'allowlist'"
:label="t('aiTools.agents.groupAllowFrom')"
prop="groupAllowFromText"
>
<el-input
v-model="form.groupAllowFromText"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.groupAllowFromPlaceholder')"
/>
<span class="input-help">{{ t('aiTools.agents.groupAllowFromHelper') }}</span>
</el-form-item>
<el-form-item :label="t('aiTools.agents.separateSessionByConversation')">
<el-switch v-model="form.separateSessionByConversation" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.groupSessionScope')" prop="groupSessionScope">
<el-select v-model="form.groupSessionScope">
<el-option :label="t('aiTools.agents.groupSessionScopeGroup')" value="group" />
<el-option :label="t('aiTools.agents.groupSessionScopeGroupSender')" value="group_sender" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.sharedMemoryAcrossConversations')">
<el-switch v-model="form.sharedMemoryAcrossConversations" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.asyncMode')">
<el-switch v-model="form.asyncMode" />
</el-form-item>
<el-form-item v-if="form.asyncMode" :label="t('aiTools.agents.ackText')">
<el-input v-model="form.ackText" />
</el-form-item>
<div>
<el-form-item :label="t('aiTools.agents.dmPolicy')" prop="dmPolicy">
<el-select v-model="form.dmPolicy">
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
<el-form-item
v-if="form.dmPolicy === 'allowlist'"
:label="t('aiTools.agents.allowFrom')"
prop="allowFromText"
>
<el-input
v-model="form.allowFromText"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.allowFromPlaceholder')"
/>
<span class="input-help">{{ t('aiTools.agents.allowFromHelper') }}</span>
</el-form-item>
<el-form-item :label="t('aiTools.agents.groupPolicy')" prop="groupPolicy">
<el-select v-model="form.groupPolicy">
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
<el-form-item
v-if="form.groupPolicy === 'allowlist'"
:label="t('aiTools.agents.groupAllowFrom')"
prop="groupAllowFromText"
>
<el-input
v-model="form.groupAllowFromText"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.groupAllowFromPlaceholder')"
/>
<span class="input-help">{{ t('aiTools.agents.groupAllowFromHelper') }}</span>
</el-form-item>
<el-form-item :label="t('aiTools.agents.separateSessionByConversation')">
<el-switch v-model="form.separateSessionByConversation" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.groupSessionScope')" prop="groupSessionScope">
<el-select v-model="form.groupSessionScope">
<el-option :label="t('aiTools.agents.groupSessionScopeGroup')" value="group" />
<el-option :label="t('aiTools.agents.groupSessionScopeGroupSender')" value="group_sender" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.sharedMemoryAcrossConversations')">
<el-switch v-model="form.sharedMemoryAcrossConversations" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.asyncMode')">
<el-switch v-model="form.asyncMode" />
</el-form-item>
<el-form-item v-if="form.asyncMode" :label="t('aiTools.agents.ackText')">
<el-input v-model="form.ackText" />
</el-form-item>
</div>
<ChannelBots
:bots="form.bots"
:fields="botFields"
:create-bot="createBot"
:show-name-field="false"
summary-label="Client ID"
unique-field-prop="clientId"
unique-field-label="Client ID"
:summary-formatter="getBotSummary"
:add-disabled="!installed"
:disabled="!installed"
@@ -280,6 +284,9 @@ const saveChannel = async (action: 'delete' | 'save' = 'save') => {
bots: form.bots,
});
MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess'));
} catch (error) {
await reload();
throw error;
} finally {
saving.value = false;
}

View File

@@ -23,6 +23,8 @@
:fields="botFields"
:create-bot="createBot"
summary-label="Token"
unique-field-prop="token"
unique-field-label="Token"
:summary-formatter="getBotSummary"
defaultable
approvable

View File

@@ -17,52 +17,57 @@
<el-form-item :label="t('commons.table.status')" class="mt-4">
<el-switch v-model="form.enabled" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.threadSession')">
<el-switch v-model="form.threadSession" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch v-model="form.streaming" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.replyMode')" prop="replyMode">
<el-input v-model="form.replyMode" placeholder="auto" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select v-model="form.requireMention">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.groupPolicy')" prop="groupPolicy">
<el-select v-model="form.groupPolicy">
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
<el-form-item
v-if="form.groupPolicy === 'allowlist'"
:label="t('aiTools.agents.groupAllowFrom')"
prop="groupAllowFromText"
>
<el-input
v-model="form.groupAllowFromText"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.groupAllowFromPlaceholder')"
/>
<span class="input-help">{{ t('aiTools.agents.groupAllowFromHelper') }}</span>
</el-form-item>
<el-form-item>
<el-link type="primary" icon="Position" @click="toFeishuDoc">
{{ t('container.mirrorsHelper2') }}
</el-link>
</el-form-item>
<div>
<el-form-item :label="t('aiTools.agents.threadSession')">
<el-switch v-model="form.threadSession" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.streaming')">
<el-switch v-model="form.streaming" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.replyMode')" prop="replyMode">
<el-input v-model="form.replyMode" placeholder="auto" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.requireMention')" prop="requireMention">
<el-select v-model="form.requireMention">
<el-option :label="t('aiTools.agents.requireMentionTrue')" value="true" />
<el-option :label="t('aiTools.agents.requireMentionFalse')" value="false" />
<el-option :label="t('aiTools.agents.requireMentionOpen')" value="open" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.groupPolicy')" prop="groupPolicy">
<el-select v-model="form.groupPolicy">
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
<el-form-item
v-if="form.groupPolicy === 'allowlist'"
:label="t('aiTools.agents.groupAllowFrom')"
prop="groupAllowFromText"
>
<el-input
v-model="form.groupAllowFromText"
type="textarea"
:rows="3"
:placeholder="t('aiTools.agents.groupAllowFromPlaceholder')"
/>
<span class="input-help">{{ t('aiTools.agents.groupAllowFromHelper') }}</span>
</el-form-item>
<el-form-item>
<el-link type="primary" icon="Position" @click="toFeishuDoc">
{{ t('container.mirrorsHelper2') }}
</el-link>
</el-form-item>
</div>
<ChannelBots
:bots="form.bots"
:fields="botFields"
:create-bot="createBot"
:create-preset="defaultBotCreatePreset"
summary-label="App ID"
unique-field-prop="appId"
unique-field-label="App ID"
:summary-formatter="getBotSummary"
:add-disabled="!installed"
:disabled="!installed"
@@ -84,7 +89,7 @@
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { computed, reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { ElMessageBox } from 'element-plus';
import { useI18n } from 'vue-i18n';
@@ -150,6 +155,16 @@ const form = reactive<FeishuForm>({
bots: [],
installed: false,
});
const defaultBotCreatePreset = computed(() => {
if (form.bots.length > 0) {
return undefined;
}
return {
accountId: 'default',
name: 'Default',
isDefault: true,
};
});
const parseTextList = (value: string): string[] => {
return Array.from(
@@ -309,6 +324,9 @@ const saveChannel = async (action: 'delete' | 'save' = 'save') => {
bots: buildPayloadBots(),
});
MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess'));
} catch (error) {
await reload();
throw error;
} finally {
saving.value = false;
}

View File

@@ -21,7 +21,10 @@
:bots="form.bots"
:fields="botFields"
:create-bot="createBot"
:create-preset="defaultBotCreatePreset"
summary-label="App ID"
unique-field-prop="appId"
unique-field-label="App ID"
:summary-formatter="getBotSummary"
:add-disabled="!installed"
:disabled="!installed"
@@ -40,7 +43,7 @@
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { computed, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import { getAgentQQBotConfig, updateAgentQQBotConfig } from '@/api/modules/ai';
@@ -88,6 +91,16 @@ const form = reactive<QQBotForm>({
enabled: true,
bots: [],
});
const defaultBotCreatePreset = computed(() => {
if (form.bots.length > 0) {
return undefined;
}
return {
accountId: 'default',
name: 'Default',
isDefault: true,
};
});
const parseTextList = (value: string): string[] => {
return Array.from(
@@ -187,6 +200,9 @@ const saveChannel = async (action: 'delete' | 'save' = 'save') => {
})),
});
MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess'));
} catch (error) {
await reload();
throw error;
} finally {
saving.value = false;
}

View File

@@ -56,6 +56,8 @@
:fields="botFields"
:create-bot="createBot"
summary-label="Token"
unique-field-prop="botToken"
unique-field-label="Bot Token"
:summary-formatter="getBotSummary"
defaultable
approvable

View File

@@ -21,7 +21,7 @@
prop="name"
min-width="120"
/>
<el-table-column :label="$t('commons.table.type')" prop="agentType" min-width="120">
<el-table-column :label="$t('commons.table.type')" prop="agentType" min-width="150">
<template #default="{ row }">
<div class="agent-type-cell">
<img
@@ -702,6 +702,7 @@ onMounted(async () => {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.agent-type-icon {