feat: add openclaw skill management menu (#12311)

This commit is contained in:
CityFun
2026-03-24 05:48:06 +00:00
committed by GitHub
parent 72764863bc
commit b91ca8bdb3
32 changed files with 643 additions and 186 deletions
+41
View File
@@ -680,6 +680,47 @@ func (b *BaseApi) UpdateAgentOtherConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary List Agent skills
// @Accept json
// @Param request body dto.AgentSkillsReq true "request"
// @Success 200 {array} dto.AgentSkillItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/skills/list [post]
func (b *BaseApi) ListAgentSkills(c *gin.Context) {
var req dto.AgentSkillsReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.ListSkills(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Update Agent skill status
// @Accept json
// @Param request body dto.AgentSkillUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/skills/update [post]
func (b *BaseApi) UpdateAgentSkill(c *gin.Context) {
var req dto.AgentSkillUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateSkill(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Login Agent Weixin channel
// @Accept json
+18
View File
@@ -353,3 +353,21 @@ type AgentOtherConfig struct {
BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"`
}
type AgentSkillsReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentSkillItem struct {
Name string `json:"name"`
Description string `json:"description"`
Source string `json:"source"`
Bundled bool `json:"bundled"`
Disabled bool `json:"disabled"`
}
type AgentSkillUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Name string `json:"name" validate:"required"`
Enabled bool `json:"enabled"`
}
+2
View File
@@ -33,6 +33,8 @@ type IAgentService interface {
UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error
GetOtherConfig(req dto.AgentOtherConfigReq) (*dto.AgentOtherConfig, error)
UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
ListSkills(req dto.AgentSkillsReq) ([]dto.AgentSkillItem, error)
UpdateSkill(req dto.AgentSkillUpdateReq) error
CreateAccount(req dto.AgentAccountCreateReq) error
UpdateAccount(req dto.AgentAccountUpdateReq) error
+10 -2
View File
@@ -166,6 +166,14 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
}
installTask.AddSubTask("Install OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Type == "qqbot" {
legacyPluginPath := path.Join(openclawPluginBaseDir, "qqbot")
if err := mgr.RunBashCf("docker exec %s test -d %s", install.ContainerName, legacyPluginPath); err == nil {
if err := mgr.RunBashCf("printf 'yes\\n' | docker exec -i %s openclaw plugins uninstall qqbot", install.ContainerName); err != nil {
return err
}
}
}
if err := mgr.RunBashCf("docker exec %s openclaw plugins install %s", install.ContainerName, spec); err != nil {
return err
}
@@ -539,7 +547,7 @@ func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
qqbotEntry := ensureChildMap(entries, "qqbot")
qqbotEntry := ensureChildMap(entries, "openclaw-qqbot")
qqbotEntry["enabled"] = config.Enabled
}
@@ -571,7 +579,7 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
func resolvePluginMeta(pluginType string) (string, string, error) {
switch pluginType {
case "qqbot":
return "@sliverp/qqbot@latest", "qqbot", nil
return "@tencent-connect/openclaw-qqbot@latest", "openclaw-qqbot", nil
case "wecom":
return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil
case "dingtalk":
+133
View File
@@ -0,0 +1,133 @@
package service
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
type openclawSkillsList struct {
Skills []openclawSkillListItem `json:"skills"`
}
type openclawSkillListItem struct {
Name string `json:"name"`
Description string `json:"description"`
Source string `json:"source"`
Bundled bool `json:"bundled"`
Disabled bool `json:"disabled"`
}
type openclawSkillInfo struct {
SkillKey string `json:"skillKey"`
}
func (a AgentService) ListSkills(req dto.AgentSkillsReq) ([]dto.AgentSkillItem, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType != constant.AppOpenclaw {
return nil, fmt.Errorf("copaw does not support skills")
}
status, err := checkContainerStatus(install.ContainerName)
if err != nil {
return nil, err
}
if status != "running" {
return nil, fmt.Errorf("container %s is not running, please check and retry", install.ContainerName)
}
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s openclaw skills list --json 2>&1",
30*time.Second,
install.ContainerName,
)
if err != nil {
return nil, err
}
if len(output) == 0 {
return nil, nil
}
return parseOpenclawSkillsList(output)
}
func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType != constant.AppOpenclaw {
return fmt.Errorf("copaw does not support skills")
}
status, err := checkContainerStatus(install.ContainerName)
if err != nil {
return err
}
if status != "running" {
return fmt.Errorf("container %s is not running, please check and retry", install.ContainerName)
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
skillKey, err := getOpenclawSkillKey(install.ContainerName, req.Name)
if err != nil {
return err
}
setOpenclawSkillEnabled(conf, skillKey, req.Enabled)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}
func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) {
var payload openclawSkillsList
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
return nil, err
}
items := make([]dto.AgentSkillItem, 0, len(payload.Skills))
for _, item := range payload.Skills {
items = append(items, dto.AgentSkillItem{
Name: item.Name,
Description: item.Description,
Source: item.Source,
Bundled: item.Bundled,
Disabled: item.Disabled,
})
}
return items, nil
}
func getOpenclawSkillKey(containerName, name string) (string, error) {
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s openclaw skills info %q --json 2>&1",
30*time.Second,
containerName,
name,
)
if err != nil {
return "", err
}
return parseOpenclawSkillKey(name, output)
}
func parseOpenclawSkillKey(name, output string) (string, error) {
var payload openclawSkillInfo
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
return "", err
}
if payload.SkillKey == "" {
return "", fmt.Errorf("skill %s does not have a skillKey", name)
}
return payload.SkillKey, nil
}
func setOpenclawSkillEnabled(conf map[string]interface{}, skillKey string, enabled bool) {
skills := ensureChildMap(conf, "skills")
entries := ensureChildMap(skills, "entries")
entry := ensureChildMap(entries, skillKey)
entry["enabled"] = enabled
}
+2
View File
@@ -74,6 +74,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/security/update", baseApi.UpdateAgentSecurityConfig)
aiToolsRouter.POST("/agents/other/get", baseApi.GetAgentOtherConfig)
aiToolsRouter.POST("/agents/other/update", baseApi.UpdateAgentOtherConfig)
aiToolsRouter.POST("/agents/skills/list", baseApi.ListAgentSkills)
aiToolsRouter.POST("/agents/skills/update", baseApi.UpdateAgentSkill)
aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing)
}
}
+18
View File
@@ -587,4 +587,22 @@ export namespace AI {
browserEnabled: boolean;
npmRegistry: string;
}
export interface AgentSkillsReq {
agentId: number;
}
export interface AgentSkillItem {
name: string;
description: string;
source: string;
bundled: boolean;
disabled: boolean;
}
export interface AgentSkillUpdateReq {
agentId: number;
name: string;
enabled: boolean;
}
}
+8
View File
@@ -229,6 +229,14 @@ export const updateAgentOtherConfig = (req: AI.AgentOtherConfigUpdateReq) => {
return http.post(`/ai/agents/other/update`, req);
};
export const listAgentSkills = (req: AI.AgentSkillsReq) => {
return http.post<AI.AgentSkillItem[]>(`/ai/agents/skills/list`, req);
};
export const updateAgentSkill = (req: AI.AgentSkillUpdateReq) => {
return http.post(`/ai/agents/skills/update`, req);
};
export const approveAgentChannelPairing = (req: AI.AgentChannelPairingApproveReq) => {
return http.post(`/ai/agents/channel/pairing/approve`, req);
};
+7 -14
View File
@@ -670,7 +670,6 @@ const message = {
},
aiTools: {
agents: {
agents: 'Agents',
agent: 'Agent',
account: 'Model Account',
noAccountHint: 'Choose an existing model account or add a new one.',
@@ -688,21 +687,16 @@ const message = {
allowedOriginsRequired: 'Enter at least one access address',
allowedOriginsInvalid: 'Use the format http(s)://host-or-ip[:port]',
provider: 'Provider',
apiKey: 'API Key',
baseUrl: 'Base URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Manual input',
verified: 'Verified',
verifySkipped: 'No verification',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Time Zone',
@@ -713,6 +707,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -722,16 +722,12 @@ const message = {
pluginNotInstalled: 'Plugin is not installed. Please install it first.',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -743,14 +739,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: 'Custom model providers do not validate whether the account is available.',
feishuSaveSuccess: 'Saved to Feishu',
},
model: {
model: 'Models',
+7 -14
View File
@@ -678,7 +678,6 @@ const message = {
},
aiTools: {
agents: {
agents: 'Agentes',
agent: 'Agente',
account: 'Cuenta de modelo',
noAccountHint: 'Selecciona una cuenta de modelo existente o agrega una nueva.',
@@ -696,21 +695,16 @@ const message = {
allowedOriginsRequired: 'Introduce al menos una dirección de acceso',
allowedOriginsInvalid: 'Usa el formato http(s)://host-o-ip[:puerto]',
provider: 'Proveedor de modelos',
apiKey: 'Clave API',
baseUrl: 'URL base',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Entrada manual de modelo',
verified: 'Verificado',
verifySkipped: 'Sin verificacion',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Zona horaria',
@@ -721,6 +715,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -730,16 +730,12 @@ const message = {
pluginNotInstalled: 'El plugin no está instalado. Instálalo primero.',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -751,14 +747,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Please fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: 'En el proveedor de modelo personalizado no se valida si la cuenta está disponible',
feishuSaveSuccess: 'Guardado en Feishu',
},
model: {
model: 'Modelo',
+7 -14
View File
@@ -671,7 +671,6 @@ const message = {
},
aiTools: {
agents: {
agents: 'エージェント',
agent: 'エージェント',
account: 'モデルアカウント',
noAccountHint: '既存のモデルアカウントを選択するか新規に追加してください',
@@ -689,21 +688,16 @@ const message = {
allowedOriginsRequired: '少なくとも 1 つのアクセスアドレスを入力してください',
allowedOriginsInvalid: 'http(s)://host-or-ip[:port] の形式で入力してください',
provider: 'モデルプロバイダー',
apiKey: 'API キー',
baseUrl: 'ベースURL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'トークン',
manualModel: '手動入力',
verified: '検証済み',
verifySkipped: '検証なし',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'タイムゾーン',
@@ -714,6 +708,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -723,16 +723,12 @@ const message = {
pluginNotInstalled: 'プラグインがインストールされていません先にインストールしてください',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -744,14 +740,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Please fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: 'カスタムモデルプロバイダーではアカウントの有効性を検証しません',
feishuSaveSuccess: 'Feishuに保存済み',
},
model: {
model: 'モデル',
+7 -14
View File
@@ -663,7 +663,6 @@ const message = {
},
aiTools: {
agents: {
agents: '에이전트',
agent: '에이전트',
account: '모델 계정',
noAccountHint: '기존 모델 계정을 선택하거나 새로 추가하세요.',
@@ -681,21 +680,16 @@ const message = {
allowedOriginsRequired: '접속 주소를 하나 이상 입력하세요',
allowedOriginsInvalid: 'http(s)://host-or-ip[:port] 형식으로 입력하세요',
provider: '모델 제공자',
apiKey: 'API ',
baseUrl: '기본 URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: '토큰',
manualModel: '수동 입력',
verified: '검증됨',
verifySkipped: '검증 ',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: '시간대',
@@ -706,6 +700,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -715,16 +715,12 @@ const message = {
pluginNotInstalled: '플러그인이 설치되지 않았습니다. 먼저 설치해 주세요.',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -736,14 +732,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Please fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: '사용자 정의 모델 공급자는 계정 사용 가능 여부를 검증하지 않습니다',
feishuSaveSuccess: 'Feishu에 저장됨',
},
model: {
model: '모델',
+7 -14
View File
@@ -678,7 +678,6 @@ const message = {
},
aiTools: {
agents: {
agents: 'Agen',
agent: 'Agen',
account: 'Akaun model',
noAccountHint: 'Pilih akaun model sedia ada atau tambah yang baharu.',
@@ -696,21 +695,16 @@ const message = {
allowedOriginsRequired: 'Masukkan sekurang-kurangnya satu alamat akses',
allowedOriginsInvalid: 'Gunakan format http(s)://hos-atau-ip[:port]',
provider: 'Penyedia model',
apiKey: 'Kunci API',
baseUrl: 'URL asas',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Input manual',
verified: 'Disahkan',
verifySkipped: 'Tanpa pengesahan',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Zon Waktu',
@@ -721,6 +715,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -730,16 +730,12 @@ const message = {
pluginNotInstalled: 'Plugin belum dipasang. Sila pasang dahulu.',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -751,14 +747,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Please fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: 'Penyedia model tersuai tidak mengesahkan sama ada akaun boleh digunakan',
feishuSaveSuccess: 'Disimpan ke Feishu',
},
model: {
model: 'Model',
+7 -14
View File
@@ -673,7 +673,6 @@ const message = {
},
aiTools: {
agents: {
agents: 'Agentes',
agent: 'Agente',
account: 'Conta de modelo',
noAccountHint: 'Selecione uma conta de modelo existente ou adicione uma nova.',
@@ -691,21 +690,16 @@ const message = {
allowedOriginsRequired: 'Informe pelo menos um endereço de acesso',
allowedOriginsInvalid: 'Use o formato http(s)://host-ou-ip[:porta]',
provider: 'Provedor de modelos',
apiKey: 'Chave API',
baseUrl: 'URL base',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Entrada manual',
verified: 'Verificado',
verifySkipped: 'Sem verificacao',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Fuso horário',
@@ -716,6 +710,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -725,16 +725,12 @@ const message = {
pluginNotInstalled: 'O plugin não está instalado. Instale-o primeiro.',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -746,14 +742,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Please fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: 'Provedores de modelo personalizados não validam se a conta está disponível',
feishuSaveSuccess: 'Salvo no Feishu',
},
model: {
model: 'Modelo',
+7 -14
View File
@@ -670,7 +670,6 @@ const message = {
},
aiTools: {
agents: {
agents: 'Агенты',
agent: 'Агент',
account: 'Аккаунт модели',
noAccountHint: 'Выберите существующий аккаунт модели или добавьте новый.',
@@ -688,21 +687,16 @@ const message = {
allowedOriginsRequired: 'Укажите хотя бы один адрес доступа',
allowedOriginsInvalid: 'Используйте формат http(s)://host-or-ip[:port]',
provider: 'Поставщик моделей',
apiKey: 'API ключ',
baseUrl: 'Базовый URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Токен',
manualModel: 'Ручной ввод',
verified: 'Проверено',
verifySkipped: 'Без проверки',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Часовой пояс',
@@ -713,6 +707,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -722,16 +722,12 @@ const message = {
pluginNotInstalled: 'Плагин не установлен. Сначала установите его.',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -743,14 +739,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Please fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: 'Для пользовательского провайдера модели доступность учетной записи не проверяется',
feishuSaveSuccess: 'Сохранено в Feishu',
},
model: {
model: 'Модель',
+7 -14
View File
@@ -674,7 +674,6 @@ const message = {
},
aiTools: {
agents: {
agents: 'Ajanlar',
agent: 'Ajan',
account: 'Model hesabı',
noAccountHint: 'Mevcut bir model hesabını seçin veya yeni bir tane ekleyin.',
@@ -692,21 +691,16 @@ const message = {
allowedOriginsRequired: 'En az bir erişim adresi girin',
allowedOriginsInvalid: 'http(s)://host-veya-ip[:port] biçimini kullanın',
provider: 'Model sağlayıcı',
apiKey: 'API anahtarı',
baseUrl: 'Temel URL',
accountModels: 'Model Catalog',
accountModelsHelper: 'Configure the models this account exposes to OpenClaw for switching and settings',
accountModelsRequired: 'Configure at least one model',
accountModelsDuplicate: 'Duplicate models exist in the catalog',
modelPool: 'Model Pool',
modelInputTypes: 'Input Types',
reasoning: 'Reasoning Model',
token: 'Token',
manualModel: 'Manuel giriş',
verified: 'Doğrulandı',
verifySkipped: 'Doğrulama yok',
configTitle: 'Configuration',
settingsTab: 'Settings',
skillsTab: 'Skills',
securityTab: 'Security',
otherTab: 'Other',
timeZone: 'Saat Dilimi',
@@ -717,6 +711,12 @@ const message = {
npmRegistryInvalid: 'Enter a valid NPM registry URL starting with http:// or https://',
pluginInstallNPMRegistryHelper:
'Go to Settings -> Other to configure the NPM registry and speed up plugin installation',
skillsSearchPlaceholder: 'Search skills...',
skillsEmpty: 'No skills',
skillsStatusDisabled: 'Disabled',
skillsGroupBuiltIn: 'Built-in',
skillsGroupExternal: 'External',
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
weixin: 'Weixin',
@@ -726,16 +726,12 @@ const message = {
pluginNotInstalled: 'Eklenti yüklü değil. Lütfen önce yükleyin.',
dmPolicy: 'DM Policy',
groupPolicy: 'Group Policy',
policyPairing: 'Pairing',
policyAllowlist: 'Allowlist',
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: 'DM Allowlist',
allowFromHelper: 'One sender ID per line. Used only when DM Policy is Allowlist.',
allowFromPlaceholder: 'One sender ID per line',
@@ -747,14 +743,11 @@ const message = {
pairingCode: 'Pairing Code',
pairingCodePlaceholder: 'Enter pairing code',
approvePairing: 'Approve Pairing',
feishuRequired: 'Please fill botName / appId / appSecret',
saveSuccess: 'Saved successfully',
pairingCodeRequired: 'Please enter pairing code',
pairingApproveSuccess: 'Pairing approved successfully',
scanConnect: 'Scan to Connect',
scanConnectHelper: 'Click to start the QR login task. The QR code will appear in the task log.',
customProviderHelper: 'Özel model sağlayıcısında hesabın kullanılabilirliği doğrulanmaz',
feishuSaveSuccess: "Feishu'ya kaydedildi",
},
model: {
model: 'Model',
+7 -14
View File
@@ -638,7 +638,6 @@ const message = {
},
aiTools: {
agents: {
agents: '智能體',
agent: '智能體',
account: '模型帳號',
noAccountHint: '選擇已有模型帳號或直接建立',
@@ -656,21 +655,16 @@ const message = {
allowedOriginsRequired: '請至少填寫一個訪問地址',
allowedOriginsInvalid: '訪問地址格式錯誤請輸入 http(s)://網域或IP[:埠]',
provider: '模型供應商',
apiKey: 'API Key',
baseUrl: 'Base URL',
accountModels: '模型池',
accountModelsHelper: '配置該帳號可提供給 OpenClaw 使用與切換的模型列表',
accountModelsRequired: '請至少配置一個模型',
accountModelsDuplicate: '模型池中存在重複模型請檢查後重試',
modelPool: '模型池',
modelInputTypes: '輸入類型',
reasoning: '推理模型',
token: 'Token',
manualModel: '手動輸入模型',
verified: '驗證狀態',
verifySkipped: '不驗證',
configTitle: '設定',
settingsTab: '設定',
skillsTab: '技能',
securityTab: '安全',
otherTab: '其他',
timeZone: '時區',
@@ -679,6 +673,12 @@ const message = {
npmRegistryHelper: '用於 OpenClaw 外掛安裝時的 npm registry可選擇預設來源或手動輸入自訂來源',
npmRegistryInvalid: '請輸入正確的 NPM 源地址需以 http:// 或 https:// 開頭',
pluginInstallNPMRegistryHelper: '可前往 設定 -> 其他 設定 NPM 以加速外掛安裝',
skillsSearchPlaceholder: '搜尋技能...',
skillsEmpty: '暫無技能',
skillsStatusDisabled: '已禁用',
skillsGroupBuiltIn: '內置',
skillsGroupExternal: '外部',
skillsGroupWorkspace: '工作區',
switchModelSuccess: '模型切換成功',
channelsTab: '頻道',
weixin: '微信',
@@ -688,16 +688,12 @@ const message = {
pluginNotInstalled: '插件未安裝請先安裝插件',
dmPolicy: '私聊策略',
groupPolicy: '群組策略',
policyPairing: '配對',
policyAllowlist: '白名單',
policyOpen: '開放',
policyDisabled: '禁用',
botName: '機器人名稱',
botId: 'Bot ID',
appId: '應用 App ID',
appSecret: '應用 App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: '私聊白名單',
allowFromHelper: '一行一個發送方識別碼僅在私聊策略為白名單時生效',
allowFromPlaceholder: '一行一個發送方識別碼',
@@ -709,14 +705,11 @@ const message = {
pairingCode: '配對碼',
pairingCodePlaceholder: '請輸入配對碼',
approvePairing: '批准配對',
feishuRequired: '請填寫 botName / appId / appSecret',
saveSuccess: '保存成功',
pairingCodeRequired: '請輸入配對碼',
pairingApproveSuccess: '配對成功',
scanConnect: '掃碼對接',
scanConnectHelper: '點擊後將在任務日誌中顯示 QR Code掃碼確認後即可完成登入',
customProviderHelper: '自訂模型供應商不驗證帳號是否可用',
feishuSaveSuccess: '儲存成功',
},
model: {
model: '模型',
+7 -14
View File
@@ -637,7 +637,6 @@ const message = {
},
aiTools: {
agents: {
agents: '智能体',
agent: '智能体',
account: '模型账号',
noAccountHint: '选择已有模型账号或直接创建',
@@ -655,21 +654,16 @@ const message = {
allowedOriginsRequired: '请至少填写一个访问地址',
allowedOriginsInvalid: '访问地址格式错误请输入 http(s)://域名或IP[:端口]',
provider: '模型供应商',
apiKey: 'API Key',
baseUrl: 'Base URL',
accountModels: '模型池',
accountModelsHelper: '配置账号可提供给 OpenClaw 使用和切换的模型列表',
accountModelsRequired: '请至少配置一个模型',
accountModelsDuplicate: '模型池中存在重复模型请检查后重试',
modelPool: '模型池',
modelInputTypes: '输入类型',
reasoning: '推理模型',
token: 'Token',
manualModel: '手动输入模型',
verified: '验证状态',
verifySkipped: '不验证',
configTitle: '配置',
settingsTab: '设置',
skillsTab: '技能',
securityTab: '安全',
otherTab: '其他',
timeZone: '时区',
@@ -678,6 +672,12 @@ const message = {
npmRegistryHelper: '用于 OpenClaw 插件安装时的 npm registry可选择预设源或手动输入自定义源',
npmRegistryInvalid: '请输入正确的 NPM 源地址需以 http:// 或 https:// 开头',
pluginInstallNPMRegistryHelper: '可前往 设置 -> 其他 配置 NPM 以加速插件安装',
skillsSearchPlaceholder: '搜索技能...',
skillsEmpty: '暂无技能',
skillsStatusDisabled: '已禁用',
skillsGroupBuiltIn: '内置',
skillsGroupExternal: '外部',
skillsGroupWorkspace: '工作区',
switchModelSuccess: '模型切换成功',
channelsTab: '频道',
weixin: '微信',
@@ -687,16 +687,12 @@ const message = {
pluginNotInstalled: '插件未安装请先安装插件',
dmPolicy: '私聊策略',
groupPolicy: '群组策略',
policyPairing: '配对码',
policyAllowlist: '白名单',
policyOpen: '开放',
policyDisabled: '禁用',
botName: '机器人名称',
botId: 'Bot ID',
appId: '应用 App ID',
appSecret: '应用 App Secret',
clientId: 'Client ID',
clientSecret: 'Client Secret',
allowFrom: '私聊白名单',
allowFromHelper: '一行一个发送方标识仅在私聊策略为白名单时生效',
allowFromPlaceholder: '一行一个发送方标识',
@@ -707,10 +703,7 @@ const message = {
pairingCode: '配对码',
pairingCodePlaceholder: '请输入配对码',
approvePairing: '批准配对',
feishuRequired: '请填写 botName / appId / appSecret',
saveSuccess: '保存成功',
feishuSaveSuccess: '保存成功',
pairingCodeRequired: '请输入配对码',
pairingApproveSuccess: '配对成功',
scanConnect: '扫码对接',
scanConnectHelper: '点击后将在任务日志中显示二维码扫码确认后即可完成登录',
+1 -1
View File
@@ -26,7 +26,7 @@ const databaseRouter = {
component: () => import('@/views/ai/agents/agent/index.vue'),
meta: {
icon: 'p-jiqiren2',
title: 'aiTools.agents.agents',
title: 'aiTools.agents.agent',
requiresAuth: true,
},
},
@@ -5,7 +5,7 @@
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item :label="`${$t('aiTools.agents.agents')}${$t('commons.table.type')}`" prop="agentType">
<el-form-item :label="`${$t('aiTools.agents.agent')}${$t('commons.table.type')}`" prop="agentType">
<el-select v-model="form.agentType" @change="handleAgentTypeChange">
<el-option :label="$t('aiTools.agents.openclawType')" value="openclaw" />
<el-option :label="$t('aiTools.agents.copawType')" value="copaw" />
@@ -76,10 +76,10 @@
</el-select>
<span class="input-help">{{ $t('aiTools.agents.accountModelsHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.baseUrl')" v-if="form.accountId" prop="baseURL">
<el-form-item label="Base URL" v-if="form.accountId" prop="baseURL">
<el-input v-model="form.baseURL" disabled />
</el-form-item>
<el-form-item :label="$t('aiTools.agents.token')">
<el-form-item label="Token">
<el-input v-model="form.token" disabled>
<template #append>
<CopyButton :content="form.token" />
@@ -8,7 +8,10 @@
<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.settingsTab')" name="settings">
<el-tab-pane :label="t('aiTools.agents.skillsTab')" name="skills">
<SkillsTab ref="skillsRef" />
</el-tab-pane>
<el-tab-pane :label="t('file.setting')" name="settings">
<SettingsTab ref="settingsRef" />
</el-tab-pane>
</el-tabs>
@@ -23,6 +26,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 SkillsTab from './tabs/skills.vue';
import SettingsTab from './tabs/settings.vue';
const { t } = useI18n();
@@ -31,25 +35,31 @@ const open = ref(false);
const activeTab = ref('channels');
const header = ref('');
const agentId = ref(0);
const currentAgent = ref<AI.AgentItem>();
const accountId = ref(0);
const model = ref('');
const channelsRef = ref();
const modelRef = ref();
const skillsRef = ref();
const settingsRef = ref();
const loadSettings = async () => {
if (!currentAgent.value) {
if (agentId.value <= 0) {
return;
}
await nextTick();
await settingsRef.value?.load(currentAgent.value);
await settingsRef.value?.load(agentId.value);
};
const loadModel = async () => {
if (!currentAgent.value) {
if (agentId.value <= 0) {
return;
}
await nextTick();
await modelRef.value?.load(currentAgent.value);
await modelRef.value?.load({
agentId: agentId.value,
accountId: accountId.value,
model: model.value,
});
};
const loadChannels = async () => {
@@ -60,6 +70,14 @@ const loadChannels = async () => {
await channelsRef.value?.load(agentId.value);
};
const loadSkills = async () => {
if (agentId.value <= 0) {
return;
}
await nextTick();
await skillsRef.value?.load(agentId.value);
};
const handleClose = () => {
activeTab.value = 'channels';
};
@@ -68,9 +86,12 @@ const handleTabClick = async (pane: TabsPaneContext) => {
if (pane.paneName === 'settings' && agentId.value > 0) {
await loadSettings();
}
if (pane.paneName === 'model' && currentAgent.value) {
if (pane.paneName === 'model') {
await loadModel();
}
if (pane.paneName === 'skills') {
await loadSkills();
}
if (pane.paneName === 'channels' && agentId.value > 0) {
await loadChannels();
}
@@ -82,7 +103,8 @@ const handleModelUpdated = () => {
const openDrawer = async (agent: AI.AgentItem) => {
agentId.value = agent.id;
currentAgent.value = agent;
accountId.value = agent.accountId;
model.value = agent.model;
header.value = `${agent.name} - ${t('menu.config')}`;
activeTab.value = 'channels';
open.value = true;
@@ -4,16 +4,16 @@
<el-form-item :label="t('commons.table.status')">
<el-switch v-model="form.enabled" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.clientId')" prop="clientId">
<el-form-item label="Client ID" prop="clientId">
<el-input v-model="form.clientId" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.clientSecret')" prop="clientSecret">
<el-form-item label="Client Secret" prop="clientSecret">
<el-input v-model="form.clientSecret" type="password" show-password />
</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.policyPairing')" value="pairing" />
<el-option :label="t('aiTools.agents.policyAllowlist')" value="allowlist" />
<el-option :label="t('aiTools.agents.pairingCode')" value="pairing" />
<el-option :label="t('waf.black.whiteList')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
@@ -30,7 +30,7 @@
<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('waf.black.whiteList')" value="allowlist" />
<el-option :label="t('aiTools.agents.policyDisabled')" value="disabled" />
</el-select>
</el-form-item>
@@ -191,7 +191,7 @@ const approvePairing = async () => {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodeRequired'));
MsgWarning(t('aiTools.agents.pairingCodePlaceholder'));
return;
}
approving.value = true;
@@ -5,7 +5,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.policyPairing')" value="pairing" />
<el-option :label="t('aiTools.agents.pairingCode')" value="pairing" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
</el-select>
</el-form-item>
@@ -109,7 +109,7 @@ const approvePairing = async () => {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodeRequired'));
MsgWarning(t('aiTools.agents.pairingCodePlaceholder'));
return;
}
approving.value = true;
@@ -10,7 +10,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.policyPairing')" value="pairing" />
<el-option :label="t('aiTools.agents.pairingCode')" value="pairing" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
</el-select>
</el-form-item>
@@ -113,7 +113,7 @@ const approvePairing = async () => {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodeRequired'));
MsgWarning(t('aiTools.agents.pairingCodePlaceholder'));
return;
}
approving.value = true;
@@ -5,7 +5,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.policyPairing')" value="pairing" />
<el-option :label="t('aiTools.agents.pairingCode')" value="pairing" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
</el-select>
</el-form-item>
@@ -97,7 +97,7 @@ const approvePairing = async () => {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodeRequired'));
MsgWarning(t('aiTools.agents.pairingCodePlaceholder'));
return;
}
approving.value = true;
@@ -6,7 +6,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.policyPairing')" value="pairing" />
<el-option :label="t('aiTools.agents.pairingCode')" value="pairing" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
</el-select>
</el-form-item>
@@ -109,7 +109,7 @@ const approvePairing = async () => {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodeRequired'));
MsgWarning(t('aiTools.agents.pairingCodePlaceholder'));
return;
}
approving.value = true;
@@ -42,6 +42,12 @@ const agentId = ref(0);
const accountOptions = ref<AI.AgentAccountItem[]>([]);
const modelOptions = ref<AI.AgentAccountModel[]>([]);
interface AgentModelLoadParams {
agentId: number;
accountId: number;
model: string;
}
const form = reactive({
accountId: undefined as unknown as number,
model: '',
@@ -75,10 +81,10 @@ const handleAccountChange = () => {
setModelOptionsByAccount(form.accountId);
};
const load = async (agent: AI.AgentItem) => {
const load = async (params: AgentModelLoadParams) => {
loading.value = true;
try {
agentId.value = agent.id;
agentId.value = params.agentId;
await loadAccounts();
if (accountOptions.value.length === 0) {
form.accountId = undefined as unknown as number;
@@ -87,9 +93,9 @@ const load = async (agent: AI.AgentItem) => {
return;
}
const currentAccount =
accountOptions.value.find((item) => item.id === agent.accountId) || accountOptions.value[0];
accountOptions.value.find((item) => item.id === params.accountId) || accountOptions.value[0];
form.accountId = currentAccount.id;
form.model = agent.model || currentAccount.models?.[0]?.id || '';
form.model = params.model || currentAccount.models?.[0]?.id || '';
setModelOptionsByAccount(currentAccount.id);
} finally {
loading.value = false;
@@ -1,6 +1,6 @@
<template>
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane v-if="agentType === 'openclaw'" :label="t('aiTools.agents.securityTab')" name="security">
<el-tab-pane :label="t('aiTools.agents.securityTab')" name="security">
<SecurityTab ref="securityRef" />
</el-tab-pane>
<el-tab-pane :label="t('aiTools.agents.otherTab')" name="other">
@@ -12,14 +12,12 @@
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import SecurityTab from './settings/security.vue';
import OtherTab from './settings/other.vue';
const { t } = useI18n();
const activeTab = ref('security');
const agentId = ref(0);
const agentType = ref<AI.AgentItem['agentType']>('openclaw');
const securityRef = ref();
const otherRef = ref();
@@ -28,7 +26,7 @@ const loadCurrentTab = async () => {
return;
}
await nextTick();
if (activeTab.value === 'security' && agentType.value === 'openclaw') {
if (activeTab.value === 'security') {
await securityRef.value?.load(agentId.value);
return;
}
@@ -41,10 +39,9 @@ const handleTabClick = async () => {
await loadCurrentTab();
};
const load = async (agent: AI.AgentItem) => {
agentId.value = agent.id;
agentType.value = agent.agentType;
activeTab.value = agent.agentType === 'openclaw' ? 'security' : 'other';
const load = async (id: number) => {
agentId.value = id;
activeTab.value = 'security';
await loadCurrentTab();
};
@@ -0,0 +1,272 @@
<template>
<div v-loading="loading">
<div class="toolbar">
<el-input
v-model="keyword"
:placeholder="t('aiTools.agents.skillsSearchPlaceholder')"
clearable
class="search-input"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button :loading="loading" @click="loadSkills">
<el-icon><Refresh /></el-icon>
</el-button>
</div>
<div v-if="groupedSkills.length" class="group-list">
<section v-for="group in groupedSkills" :key="group.key" class="group-section">
<div class="group-header">
<span class="group-title">{{ group.label }}</span>
<span class="group-count">{{ group.items.length }}</span>
</div>
<div class="skills-grid">
<el-card v-for="skill in group.items" :key="skill.name" class="skill-card">
<div class="skill-head">
<div class="skill-name">{{ skill.name }}</div>
<el-switch
:model-value="!skill.disabled"
:loading="updatingSkill === skill.name"
@change="(value) => toggleSkill(skill, Boolean(value))"
/>
</div>
<el-tooltip placement="top-start" :show-after="200" popper-class="skill-desc-tooltip">
<template #content>
<div class="skill-desc-tooltip-content">{{ skill.description }}</div>
</template>
<div class="skill-desc">
{{ skill.description }}
</div>
</el-tooltip>
<div class="skill-tags">
<el-tag class="skill-source-tag" size="small" effect="plain">
{{ group.tagLabel }}
</el-tag>
</div>
</el-card>
</div>
</section>
</div>
<el-empty v-else :description="t('aiTools.agents.skillsEmpty')" />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import { listAgentSkills, updateAgentSkill } from '@/api/modules/ai';
import { MsgSuccess } from '@/utils/message';
type SkillGroupKey = 'builtIn' | 'external' | 'workspace' | 'extra' | 'other';
const { t } = useI18n();
const loading = ref(false);
const keyword = ref('');
const agentId = ref(0);
const skills = ref<AI.AgentSkillItem[]>([]);
const updatingSkill = ref('');
const groupTagLabels = computed<Record<SkillGroupKey, string>>(() => ({
builtIn: t('aiTools.agents.skillsGroupBuiltIn'),
external: t('aiTools.agents.skillsGroupExternal'),
workspace: t('aiTools.agents.skillsGroupWorkspace'),
extra: t('runtime.extension'),
other: t('aiTools.agents.otherTab'),
}));
const formatSkillGroupLabel = (label: string) => {
const suffix = t('aiTools.agents.skillsTab');
return /[\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af]/.test(suffix) ? `${label}${suffix}` : `${label} ${suffix}`;
};
const groupLabels = computed<Record<SkillGroupKey, string>>(() => ({
builtIn: formatSkillGroupLabel(groupTagLabels.value.builtIn),
external: formatSkillGroupLabel(groupTagLabels.value.external),
workspace: formatSkillGroupLabel(groupTagLabels.value.workspace),
extra: formatSkillGroupLabel(groupTagLabels.value.extra),
other: formatSkillGroupLabel(groupTagLabels.value.other),
}));
const filteredSkills = computed(() => {
const value = keyword.value.trim().toLowerCase();
if (!value) {
return skills.value;
}
return skills.value.filter((item) =>
[item.name, item.description, item.source].join(' ').toLowerCase().includes(value),
);
});
const groupedSkills = computed(() => {
const groups: Record<SkillGroupKey, AI.AgentSkillItem[]> = {
builtIn: [],
external: [],
workspace: [],
extra: [],
other: [],
};
for (const skill of filteredSkills.value) {
groups[resolveGroupKey(skill)].push(skill);
}
const order: SkillGroupKey[] = ['external', 'extra', 'builtIn', 'workspace', 'other'];
return order
.filter((key) => groups[key].length > 0)
.map((key) => ({
key,
label: groupLabels.value[key],
tagLabel: groupTagLabels.value[key],
items: groups[key],
}));
});
const resolveGroupKey = (skill: AI.AgentSkillItem): SkillGroupKey => {
if (skill.bundled || skill.source === 'openclaw-bundled') {
return 'builtIn';
}
if (skill.source === 'openclaw-managed') {
return 'external';
}
if (skill.source === 'openclaw-workspace') {
return 'workspace';
}
if (skill.source === 'openclaw-extra') {
return 'extra';
}
return 'other';
};
const loadSkills = async () => {
if (!agentId.value) {
return;
}
loading.value = true;
try {
const res = await listAgentSkills({ agentId: agentId.value });
skills.value = res.data || [];
} finally {
loading.value = false;
}
};
const load = async (id: number) => {
agentId.value = id;
await loadSkills();
};
const toggleSkill = async (skill: AI.AgentSkillItem, enabled: boolean) => {
if (!agentId.value) {
return;
}
updatingSkill.value = skill.name;
try {
await updateAgentSkill({
agentId: agentId.value,
name: skill.name,
enabled,
});
MsgSuccess(t('aiTools.agents.saveSuccess'));
await loadSkills();
} finally {
updatingSkill.value = '';
}
};
defineExpose({
load,
});
</script>
<style scoped lang="scss">
.toolbar {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.search-input {
flex: 1;
}
.group-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.group-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
min-height: 42px;
border: 1px solid var(--el-border-color);
border-radius: 10px;
background: var(--el-fill-color-light);
}
.group-title,
.group-count {
color: var(--el-text-color-secondary);
}
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
padding: 2px;
}
.skill-card {
--el-card-border-color: var(--el-border-color-dark);
border-radius: 12px;
}
.skill-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.skill-name {
font-weight: 600;
}
.skill-desc {
margin-top: 12px;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: var(--el-text-color-secondary);
line-height: 1.6;
}
.skill-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
:global(.skill-desc-tooltip) {
max-width: 360px;
}
:global(.skill-desc-tooltip .skill-desc-tooltip-content) {
white-space: normal;
word-break: break-word;
}
</style>
+1 -1
View File
@@ -86,7 +86,7 @@
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.agents.token')" min-width="80">
<el-table-column label="Token" min-width="80">
<template #default="{ row }">
<el-space v-if="row.agentType !== 'copaw'">
<CopyButton :content="row.token" />
@@ -14,7 +14,7 @@
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.apiKey')" prop="apiKey">
<el-form-item label="API Key" prop="apiKey">
<el-input v-model="form.apiKey" type="password" show-password />
<span class="input-help" v-if="form.provider === 'custom' || form.provider === 'vllm'">
{{ $t('aiTools.agents.customProviderHelper') }}
@@ -23,7 +23,7 @@
<el-form-item>
<el-checkbox v-model="form.rememberApiKey">{{ $t('terminal.rememberPassword') }}</el-checkbox>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.baseUrl')" prop="baseURL">
<el-form-item label="Base URL" prop="baseURL">
<el-input v-model="form.baseURL" :disabled="!editableBaseURLProviders.includes(form.provider)" />
</el-form-item>
<el-form-item :label="'API ' + $t('commons.table.type')" prop="apiType">
@@ -32,7 +32,7 @@
</el-select>
</el-form-item>
<template v-if="showInitialModel">
<el-divider content-position="left">{{ $t('aiTools.agents.accountModels') }}</el-divider>
<el-divider content-position="left">{{ $t('aiTools.agents.modelPool') }}</el-divider>
<el-form-item :label="$t('aiTools.model.model')" prop="initialModel.id" :rules="[Rules.noSpace]">
<el-input v-model="form.initialModel.id" />
</el-form-item>
+2 -2
View File
@@ -17,8 +17,8 @@
{{ getAgentProviderDisplayName(row.provider, row.providerName) }}
</template>
</el-table-column>
<el-table-column :label="$t('aiTools.agents.baseUrl')" prop="baseUrl" min-width="200" />
<el-table-column :label="$t('aiTools.agents.apiKey')" prop="apiKey" min-width="160">
<el-table-column label="Base URL" prop="baseUrl" min-width="200" />
<el-table-column label="API Key" prop="apiKey" min-width="160">
<template #default="{ row }">
{{ maskKey(row.apiKey) }}
</template>