mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: add openclaw skill management menu (#12311)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'モデル',
|
||||
|
||||
@@ -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: '모델',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Модель',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '模型',
|
||||
|
||||
@@ -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: '点击后将在任务日志中显示二维码,扫码确认后即可完成登录',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user