feat: openclaw channel support wecom (#12146)

This commit is contained in:
CityFun
2026-03-11 10:53:27 +00:00
committed by GitHub
parent d114933fe9
commit 175162753e
21 changed files with 886 additions and 510 deletions
+3 -1
View File
@@ -73,4 +73,6 @@ agent/.golangci.yml
openspec
CLAUDE.md
AGENTS.md
opencode.json
opencode.json
superpowers
.worktrees/
+41 -20
View File
@@ -353,6 +353,47 @@ func (b *BaseApi) UpdateAgentDiscordConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent QQ Bot channel config
// @Accept json
// @Param request body dto.AgentWecomConfigReq true "request"
// @Success 200 {object} dto.AgentWecomConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/wecom/get [post]
func (b *BaseApi) GetAgentWecomConfig(c *gin.Context) {
var req dto.AgentWecomConfigReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.GetWecomConfig(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Update Agent WeCom channel config
// @Accept json
// @Param request body dto.AgentWecomConfigUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/wecom/update [post]
func (b *BaseApi) UpdateAgentWecomConfig(c *gin.Context) {
var req dto.AgentWecomConfigUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateWecomConfig(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent QQ Bot channel config
// @Accept json
@@ -517,26 +558,6 @@ func (b *BaseApi) UpdateAgentOtherConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Approve Agent Feishu pairing code
// @Accept json
// @Param request body dto.AgentFeishuPairingApproveReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/channel/feishu/approve [post]
func (b *BaseApi) ApproveAgentFeishuPairing(c *gin.Context) {
var req dto.AgentFeishuPairingApproveReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.ApproveFeishuPairing(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Approve Agent channel pairing code
// @Accept json
+23 -3
View File
@@ -194,10 +194,30 @@ type AgentTelegramConfig struct {
type AgentChannelPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord"`
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentWecomConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentWecomConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"`
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing open"`
BotID string `json:"botId" validate:"required"`
Secret string `json:"secret" validate:"required"`
}
type AgentWecomConfig struct {
Enabled bool `json:"enabled"`
DmPolicy string `json:"dmPolicy"`
BotID string `json:"botId"`
Secret string `json:"secret"`
Installed bool `json:"installed"`
}
type AgentQQBotConfigReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
@@ -218,13 +238,13 @@ type AgentQQBotConfig struct {
type AgentPluginInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=qqbot"`
Type string `json:"type" validate:"required,oneof=qqbot wecom"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginCheckReq struct {
AgentID uint `json:"agentId" validate:"required"`
Type string `json:"type" validate:"required,oneof=qqbot"`
Type string `json:"type" validate:"required,oneof=qqbot wecom"`
}
type AgentPluginStatus struct {
+333
View File
@@ -0,0 +1,333 @@
package provider
import (
"fmt"
"strings"
)
type OpenClawPatch struct {
PrimaryModel string
Models map[string]interface{}
}
func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) {
provider = strings.ToLower(strings.TrimSpace(provider))
modelName = strings.TrimSpace(modelName)
if modelName == "" {
return nil, fmt.Errorf("model is required")
}
modelID := modelName
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
modelID = parts[1]
}
switch provider {
case "deepseek":
return buildDeepseekPatch(modelName, baseURL, apiKey), nil
case "moonshot", "kimi":
return buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey), nil
case "bailian-coding-plan":
return buildBailianPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
case "ark-coding-plan":
return buildArkPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
case "minimax":
return buildMiniMaxPatch(modelID, baseURL, apiKey), nil
case "custom", "vllm":
return buildCustomPatch(provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
case "ollama":
return buildOllamaPatch(modelName, modelID, apiType, baseURL), nil
case "kimi-coding":
return buildKimiCodingPatch(modelName, modelID, baseURL, apiKey), nil
case "zai":
return buildZaiPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil
default:
return buildGenericPatch(provider, modelName, modelID, apiType, maxTokens, contextWindow, baseURL, apiKey), nil
}
}
func buildDeepseekPatch(modelName, baseURL, apiKey string) *OpenClawPatch {
return &OpenClawPatch{
PrimaryModel: modelName,
Models: providerModels("deepseek", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.deepseek.com/v1"), "openai-completions", map[string]interface{}{
"id": "deepseek-chat",
"name": "DeepSeek Chat",
"reasoning": false,
"input": []string{"text"},
"contextWindow": 128000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
}
}
func buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawPatch {
configProvider := provider
primaryModel := modelName
if provider == "kimi" {
configProvider = "moonshot"
primaryModel = "moonshot/" + modelID
}
return &OpenClawPatch{
PrimaryModel: primaryModel,
Models: providerModels(configProvider, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), "openai-completions", map[string]interface{}{
"id": modelID,
"name": modelID,
"reasoning": strings.Contains(strings.ToLower(modelID), "thinking"),
"input": []string{"text"},
"contextWindow": 256000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
}
}
func buildBailianPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
normalizedID := normalizeBailianCodingPlanModelID(modelID)
return &OpenClawPatch{
PrimaryModel: "bailian-coding-plan/" + bailianPrimaryModelID(normalizedID),
Models: providerModels("bailian-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("bailian-coding-plan", baseURL), "openai-completions", map[string]interface{}{
"id": normalizedID,
"name": normalizedID,
"reasoning": isReasoningModel(normalizedID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 256000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
}
}
func buildArkPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
normalizedID := normalizeArkCodingPlanModelID(modelID)
return &OpenClawPatch{
PrimaryModel: "ark-coding-plan/" + normalizedID,
Models: providerModels("ark-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("ark-coding-plan", baseURL), "openai-completions", map[string]interface{}{
"id": normalizedID,
"name": normalizedID,
"reasoning": isReasoningModel(normalizedID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 256000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
}
}
func buildMiniMaxPatch(modelID, baseURL, apiKey string) *OpenClawPatch {
normalizedID := normalizeMiniMaxModelID(modelID)
return &OpenClawPatch{
PrimaryModel: "minimax-portal/" + normalizedID,
Models: providerModels("minimax-portal", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.minimaxi.com/anthropic"), "anthropic-messages", map[string]interface{}{
"id": normalizedID,
"name": strings.ReplaceAll(normalizedID, "-", " "),
"reasoning": false,
"input": []string{"text"},
"contextWindow": 200000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
}
}
func buildCustomPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
customModelID := normalizeCustomModel(modelName)
return &OpenClawPatch{
PrimaryModel: provider + "/" + customModelID,
Models: providerModels(provider, strings.TrimSpace(apiKey), strings.TrimSpace(baseURL), apiType, map[string]interface{}{
"id": customModelID,
"name": customModelID,
"reasoning": isReasoningModel(customModelID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 128000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
}
}
func buildOllamaPatch(modelName, modelID, apiType, baseURL string) *OpenClawPatch {
api := normalizeAPIType(apiType)
if api != "openai-completions" && api != "openai-responses" {
api = "openai-responses"
}
return &OpenClawPatch{
PrimaryModel: modelName,
Models: providerModels("ollama", "ollama", strings.TrimSpace(baseURL), api, map[string]interface{}{
"id": modelID,
"name": modelID,
"reasoning": api != "openai-completions",
"input": []string{"text"},
"contextWindow": 160000,
"maxTokens": 8192,
"cost": map[string]interface{}{},
}),
}
}
func buildKimiCodingPatch(modelName, modelID, baseURL, apiKey string) *OpenClawPatch {
return &OpenClawPatch{
PrimaryModel: modelName,
Models: providerModels("kimi-coding", strings.TrimSpace(apiKey), withCatalogDefault("kimi-coding", baseURL), "anthropic-messages", map[string]interface{}{
"id": modelID,
"name": "Kimi for Coding",
"reasoning": true,
"input": []string{"text", "image"},
"contextWindow": 262144,
"maxTokens": 32768,
"cost": map[string]interface{}{},
}),
}
}
func buildZaiPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
return &OpenClawPatch{
PrimaryModel: "zai/" + modelID,
Models: providerModels("zai", strings.TrimSpace(apiKey), withCatalogDefault("zai", baseURL), "openai-completions", map[string]interface{}{
"id": modelID,
"name": zaiModelDisplayName(modelID),
"reasoning": modelID == "glm-5",
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 204800),
"maxTokens": fallbackInt(maxTokens, 131072),
"cost": map[string]interface{}{},
}),
}
}
func buildGenericPatch(provider, modelName, modelID, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch {
providerName := provider
primaryModel := modelName
if provider == "gemini" {
providerName = "google"
primaryModel = "google/" + modelID
}
return &OpenClawPatch{
PrimaryModel: primaryModel,
Models: providerModels(providerName, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), normalizeAPIType(apiType), map[string]interface{}{
"id": modelID,
"name": modelID,
"reasoning": isReasoningModel(modelID),
"input": []string{"text"},
"contextWindow": fallbackInt(contextWindow, 256000),
"maxTokens": fallbackInt(maxTokens, 8192),
"cost": map[string]interface{}{},
}),
}
}
func providerModels(provider, apiKey, baseURL, api string, model map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"mode": "merge",
"providers": map[string]interface{}{
provider: map[string]interface{}{
"apiKey": apiKey,
"baseUrl": baseURL,
"api": api,
"models": []map[string]interface{}{model},
},
},
}
}
func withCatalogDefault(provider, baseURL string) string {
if strings.TrimSpace(baseURL) != "" {
return strings.TrimSpace(baseURL)
}
if defaultURL, ok := DefaultBaseURL(provider); ok {
return defaultURL
}
return ""
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func fallbackInt(value, fallback int) int {
if value > 0 {
return value
}
return fallback
}
func normalizeAPIType(apiType string) string {
trim := strings.ToLower(strings.TrimSpace(apiType))
if trim == "" {
return "openai-completions"
}
return trim
}
func normalizeCustomModel(modelName string) string {
trim := strings.TrimSpace(modelName)
trim = strings.TrimLeft(trim, "/")
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 && strings.EqualFold(parts[0], "custom") {
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
}
return trim
}
func normalizeBailianCodingPlanModelID(modelID string) string {
trim := strings.TrimSpace(modelID)
switch strings.ToLower(trim) {
case "minimax-m2.5", "minimax m2.5", "minimax/minimax-m2.5", "minimax/minimax m2.5":
return "MiniMax/MiniMax-M2.5"
default:
return trim
}
}
func normalizeArkCodingPlanModelID(modelID string) string {
return strings.ToLower(strings.TrimSpace(modelID))
}
func normalizeMiniMaxModelID(modelID string) string {
switch strings.ToLower(strings.TrimSpace(modelID)) {
case "minimax-m2.1", "minimax m2.1", "minimax-m2.1-preview", "minimax-m2.1-latest":
return "MiniMax-M2.1"
case "minimax-m2.1-lightning", "minimax m2.1 lightning":
return "MiniMax-M2.1-lightning"
default:
return modelID
}
}
func zaiModelDisplayName(modelID string) string {
switch strings.ToLower(strings.TrimSpace(modelID)) {
case "glm-5":
return "GLM-5"
case "glm-4.7":
return "GLM-4.7"
case "glm-4.7-flash":
return "GLM-4.7-Flash"
case "glm-4.7-flashx":
return "GLM-4.7-FlashX"
default:
return strings.TrimSpace(modelID)
}
}
func bailianPrimaryModelID(modelID string) string {
trim := strings.TrimSpace(modelID)
if trim == "" {
return ""
}
parts := strings.Split(trim, "/")
for i := len(parts) - 1; i >= 0; i-- {
part := strings.TrimSpace(parts[i])
if part != "" {
return part
}
}
return trim
}
func isReasoningModel(modelID string) bool {
trim := strings.ToLower(strings.TrimSpace(modelID))
return strings.Contains(trim, "reason") || strings.Contains(trim, "thinking")
}
+136
View File
@@ -0,0 +1,136 @@
package provider
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/buserr"
)
type VerifyRequest struct {
Method string
URL string
Headers map[string]string
Body []byte
}
func SkipVerification(key string) bool {
switch strings.ToLower(strings.TrimSpace(key)) {
case "custom", "vllm", "ollama", "kimi-coding":
return true
default:
return false
}
}
func VerifyAccount(provider, baseURL, apiKey string) error {
req := BuildVerifyRequest(provider, baseURL, apiKey)
var body *bytes.Buffer
if len(req.Body) > 0 {
body = bytes.NewBuffer(req.Body)
} else {
body = bytes.NewBuffer(nil)
}
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil {
return err
}
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(httpReq)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
}
return nil
}
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
provider = strings.ToLower(strings.TrimSpace(provider))
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
headers := map[string]string{}
request := VerifyRequest{Method: http.MethodGet, Headers: headers}
switch provider {
case "anthropic", "kimi-coding":
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
case "gemini":
if strings.Contains(base, "/v1beta") {
request.URL = fmt.Sprintf("%s/models?key=%s", base, apiKey)
} else {
request.URL = fmt.Sprintf("%s/v1beta/models?key=%s", base, apiKey)
}
case "zai":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
request.URL = base + "/models"
case "bailian-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "qwen3.5-plus",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "ark-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/api/coding/v3") {
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "doubao-seed-2.0-code",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "minimax":
request.Method = http.MethodPost
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "MiniMax-M2.1",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
default:
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
}
return request
}
func mustJSON(value interface{}) []byte {
payload, err := json.Marshal(value)
if err != nil {
return []byte("{}")
}
return payload
}
+113 -478
View File
@@ -1,7 +1,6 @@
package service
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
@@ -52,6 +51,8 @@ type IAgentService interface {
UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
GetDiscordConfig(req dto.AgentDiscordConfigReq) (*dto.AgentDiscordConfig, error)
UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error
GetWecomConfig(req dto.AgentWecomConfigReq) (*dto.AgentWecomConfig, error)
UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error
GetQQBotConfig(req dto.AgentQQBotConfigReq) (*dto.AgentQQBotConfig, error)
UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
InstallPlugin(req dto.AgentPluginInstallReq) error
@@ -61,7 +62,6 @@ type IAgentService interface {
GetOtherConfig(req dto.AgentOtherConfigReq) (*dto.AgentOtherConfig, error)
UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
ApproveFeishuPairing(req dto.AgentFeishuPairingApproveReq) error
}
func NewIAgentService() IAgentService {
@@ -146,7 +146,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if err != nil {
return nil, err
}
if !account.Verified && !isVerificationSkippedProvider(account.Provider) {
if !account.Verified && !providercatalog.SkipVerification(account.Provider) {
return nil, buserr.New("ErrAgentAccountNotVerified")
}
if account.Provider != "" && provider != "" && account.Provider != provider {
@@ -373,7 +373,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
if err != nil {
return err
}
if !account.Verified && !isVerificationSkippedProvider(account.Provider) {
if !account.Verified && !providercatalog.SkipVerification(account.Provider) {
return buserr.New("ErrAgentAccountNotVerified")
}
provider := strings.ToLower(strings.TrimSpace(account.Provider))
@@ -489,7 +489,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: apiKey}); err != nil {
return err
}
verified := !isVerificationSkippedProvider(provider)
verified := !providercatalog.SkipVerification(provider)
_, maxTokens, contextWindow := resolveRuntimeParams(provider, apiType, req.MaxTokens, req.ContextWindow)
account := &model.AgentAccount{
Provider: provider,
@@ -562,7 +562,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
if err := a.VerifyAccount(dto.AgentAccountVerifyReq{Provider: provider, BaseURL: baseURL, APIKey: req.APIKey}); err != nil {
return err
}
verified := !isVerificationSkippedProvider(provider)
verified := !providercatalog.SkipVerification(provider)
account.Name = req.Name
account.APIKey = req.APIKey
account.RememberAPIKey = req.RememberAPIKey
@@ -658,13 +658,10 @@ func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
if provider == "ollama" && baseURL == "" {
return buserr.New("ErrAgentBaseURLRequired")
}
if provider == "ollama" {
if providercatalog.SkipVerification(provider) {
return nil
}
if provider == "custom" || provider == "vllm" || provider == "kimi-coding" {
return nil
}
return verifyProvider(provider, baseURL, apiKey)
return providercatalog.VerifyAccount(provider, baseURL, apiKey)
}
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
@@ -830,6 +827,42 @@ func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
return nil
}
func (a AgentService) GetWecomConfig(req dto.AgentWecomConfigReq) (*dto.AgentWecomConfig, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return nil, err
}
result := extractWecomConfig(conf)
installed, _ := checkPluginInstalled(install.ContainerName, "wecom")
result.Installed = installed
return &result, nil
}
func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error {
agent, _, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
setWecomConfig(conf, dto.AgentWecomConfig{
Enabled: req.Enabled,
DmPolicy: req.DmPolicy,
BotID: req.BotID,
Secret: req.Secret,
})
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
return err
}
return nil
}
func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
_, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
@@ -940,7 +973,7 @@ func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveRe
if channelType == "" {
channelType = "feishu"
}
if channelType != "feishu" && channelType != "telegram" && channelType != "discord" {
if channelType != "feishu" && channelType != "telegram" && channelType != "discord" && channelType != "wecom" {
return fmt.Errorf("unsupported channel type: %s", channelType)
}
if err := cmd.RunDefaultBashCf(
@@ -954,14 +987,6 @@ func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveRe
return nil
}
func (a AgentService) ApproveFeishuPairing(req dto.AgentFeishuPairingApproveReq) error {
return a.ApproveChannelPairing(dto.AgentChannelPairingApproveReq{
AgentID: req.AgentID,
Type: "feishu",
PairingCode: req.PairingCode,
})
}
func (a AgentService) loadAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) {
agent, err := agentRepo.GetFirst(repo.WithByID(agentID))
if err != nil {
@@ -1224,6 +1249,50 @@ func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig {
return result
}
func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig {
result := dto.AgentWecomConfig{Enabled: true, DmPolicy: "pairing"}
channels, ok := conf["channels"].(map[string]interface{})
if !ok {
return result
}
wecom, ok := channels["wecom"].(map[string]interface{})
if !ok {
return result
}
if enabled, ok := wecom["enabled"].(bool); ok {
result.Enabled = enabled
}
if dmPolicy, ok := wecom["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" {
result.DmPolicy = strings.TrimSpace(dmPolicy)
}
if botID, ok := wecom["botId"].(string); ok {
result.BotID = botID
}
if secret, ok := wecom["secret"].(string); ok {
result.Secret = secret
}
return result
}
func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) {
channels := ensureChildMap(conf, "channels")
wecom := ensureChildMap(channels, "wecom")
wecom["enabled"] = config.Enabled
wecom["botId"] = strings.TrimSpace(config.BotID)
wecom["secret"] = strings.TrimSpace(config.Secret)
wecom["dmPolicy"] = strings.TrimSpace(config.DmPolicy)
if strings.EqualFold(config.DmPolicy, "open") {
wecom["allowFrom"] = []string{"*"}
} else {
wecom["allowFrom"] = []string{}
}
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin")
wecomEntry["enabled"] = config.Enabled
}
func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
channels := ensureChildMap(conf, "channels")
qqbot := ensureChildMap(channels, "qqbot")
@@ -1242,6 +1311,8 @@ func resolvePluginMeta(pluginType string) (string, string, error) {
switch strings.ToLower(strings.TrimSpace(pluginType)) {
case "qqbot":
return "@sliverp/qqbot@latest", "qqbot", nil
case "wecom":
return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil
default:
return "", "", fmt.Errorf("unsupported plugin type")
}
@@ -1333,140 +1404,6 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return nil
}
func verifyProvider(provider, baseURL, apiKey string) error {
if provider == "minimax" {
return verifyMinimax("https://api.minimax.chat/v1", apiKey)
}
if provider == "bailian-coding-plan" {
return verifyBailianCodingPlan(baseURL, apiKey)
}
if provider == "ark-coding-plan" {
return verifyArkCodingPlan(baseURL, apiKey)
}
client := &http.Client{Timeout: 10 * time.Second}
reqURL, headers := buildVerifyRequest(provider, baseURL, apiKey)
request, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return err
}
for key, value := range headers {
request.Header.Set(key, value)
}
resp, err := client.Do(request)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
}
return nil
}
func verifyBailianCodingPlan(baseURL, apiKey string) error {
client := &http.Client{Timeout: 10 * time.Second}
base := strings.TrimRight(baseURL, "/")
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
reqURL := base + "/chat/completions"
body := map[string]interface{}{
"model": "qwen3.5-plus",
"messages": []map[string]string{
{"role": "user", "content": "test"},
},
"max_tokens": 1,
}
payload, err := json.Marshal(body)
if err != nil {
return err
}
request, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewBuffer(payload))
if err != nil {
return err
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
request.Header.Set("Content-Type", "application/json")
resp, err := client.Do(request)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
}
return nil
}
func verifyArkCodingPlan(baseURL, apiKey string) error {
client := &http.Client{Timeout: 10 * time.Second}
base := strings.TrimRight(baseURL, "/")
if !strings.Contains(base, "/api/coding/v3") {
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
}
reqURL := base + "/chat/completions"
body := map[string]interface{}{
"model": "doubao-seed-2.0-code",
"messages": []map[string]string{
{"role": "user", "content": "test"},
},
"max_tokens": 1,
}
payload, err := json.Marshal(body)
if err != nil {
return err
}
request, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewBuffer(payload))
if err != nil {
return err
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
request.Header.Set("Content-Type", "application/json")
resp, err := client.Do(request)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
}
return nil
}
func verifyMinimax(baseURL, apiKey string) error {
client := &http.Client{Timeout: 10 * time.Second}
base := strings.TrimRight(baseURL, "/")
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
reqURL := base + "/chat/completions"
body := map[string]interface{}{
"model": "MiniMax-M2.1",
"messages": []map[string]string{
{"role": "user", "content": "test"},
},
}
payload, err := json.Marshal(body)
if err != nil {
return err
}
request, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewBuffer(payload))
if err != nil {
return err
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
request.Header.Set("Content-Type", "application/json")
resp, err := client.Do(request)
if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
}
return nil
}
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
agentType := normalizeAgentType(agent.AgentType)
if appInstall != nil && appInstall.ID > 0 && appInstall.App.Key == constant.AppCopaw {
@@ -1734,288 +1671,18 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens
},
}
provider = strings.ToLower(strings.TrimSpace(provider))
modelID := modelName
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
modelID = parts[1]
resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
patch, err := providercatalog.BuildOpenClawPatch(provider, modelName, resolvedAPIType, resolvedMaxTokens, resolvedContextWindow, baseURL, apiKey)
if err != nil {
return err
}
configProvider := provider
primaryModel := modelName
if provider == "kimi" {
configProvider = "moonshot"
primaryModel = "moonshot/" + modelID
}
if provider == "deepseek" {
cfg.Agents.Defaults.Model.Primary = modelName
base := baseURL
if base == "" {
base = "https://api.deepseek.com/v1"
}
plainKey := strings.TrimSpace(apiKey)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"deepseek": {
ApiKey: plainKey,
BaseUrl: base,
Api: "openai-completions",
Models: []modelEntry{
{
ID: "deepseek-chat",
Name: "DeepSeek Chat",
Reasoning: false,
Input: []string{"text"},
ContextWindow: 128000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "moonshot" || provider == "kimi" {
cfg.Agents.Defaults.Model.Primary = primaryModel
base := baseURL
if base == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
base = defaultURL
}
}
plainKey := strings.TrimSpace(apiKey)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
configProvider: {
ApiKey: plainKey,
BaseUrl: base,
Api: "openai-completions",
Models: []modelEntry{
{
ID: modelID,
Name: modelID,
Reasoning: strings.Contains(modelID, "thinking"),
Input: []string{"text"},
ContextWindow: 256000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "bailian-coding-plan" {
normalizedID := normalizeBailianCodingPlanModelID(modelID)
cfg.Agents.Defaults.Model.Primary = "bailian-coding-plan/" + bailianPrimaryModelID(normalizedID)
base := baseURL
if base == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
base = defaultURL
}
}
plainKey := strings.TrimSpace(apiKey)
_, useMaxTokens, useContextWindow := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"bailian-coding-plan": {
ApiKey: plainKey,
BaseUrl: base,
Api: "openai-completions",
Models: []modelEntry{
{
ID: normalizedID,
Name: normalizedID,
Reasoning: strings.Contains(strings.ToLower(normalizedID), "reason") || strings.Contains(strings.ToLower(normalizedID), "thinking"),
Input: []string{"text"},
ContextWindow: useContextWindow,
MaxTokens: useMaxTokens,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "ark-coding-plan" {
normalizedID := normalizeArkCodingPlanModelID(modelID)
cfg.Agents.Defaults.Model.Primary = "ark-coding-plan/" + normalizedID
base := baseURL
if base == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
base = defaultURL
}
}
plainKey := strings.TrimSpace(apiKey)
_, useMaxTokens, useContextWindow := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"ark-coding-plan": {
ApiKey: plainKey,
BaseUrl: base,
Api: "openai-completions",
Models: []modelEntry{
{
ID: normalizedID,
Name: normalizedID,
Reasoning: strings.Contains(strings.ToLower(normalizedID), "reason") || strings.Contains(strings.ToLower(normalizedID), "thinking"),
Input: []string{"text"},
ContextWindow: useContextWindow,
MaxTokens: useMaxTokens,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "minimax" {
normalizedID := modelID
switch strings.ToLower(modelID) {
case "minimax-m2.1", "minimax m2.1", "minimax-m2.1-preview", "minimax-m2.1-latest":
normalizedID = "MiniMax-M2.1"
case "minimax-m2.1-lightning", "minimax m2.1 lightning":
normalizedID = "MiniMax-M2.1-lightning"
}
cfg.Agents.Defaults.Model.Primary = "minimax-portal/" + normalizedID
base := baseURL
if base == "" {
base = "https://api.minimaxi.com/anthropic"
}
plainKey := strings.TrimSpace(apiKey)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"minimax-portal": {
ApiKey: plainKey,
BaseUrl: base,
Api: "anthropic-messages",
Models: []modelEntry{
{
ID: normalizedID,
Name: strings.ReplaceAll(normalizedID, "-", " "),
Reasoning: false,
Input: []string{"text"},
ContextWindow: 200000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "custom" || provider == "vllm" {
customModelID := normalizeCustomModel(modelName)
primary := provider + "/" + customModelID
cfg.Agents.Defaults.Model.Primary = primary
base := strings.TrimSpace(baseURL)
plainKey := strings.TrimSpace(apiKey)
useAPIType, useMaxTokens, useContextWindow := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
provider: {
ApiKey: plainKey,
BaseUrl: base,
Api: useAPIType,
Models: []modelEntry{
{
ID: customModelID,
Name: customModelID,
Reasoning: strings.Contains(strings.ToLower(customModelID), "reason") || strings.Contains(strings.ToLower(customModelID), "thinking"),
Input: []string{"text"},
ContextWindow: useContextWindow,
MaxTokens: useMaxTokens,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "ollama" {
cfg.Agents.Defaults.Model.Primary = modelName
useAPIType, _, _ := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
reasoning := useAPIType != "openai-completions"
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"ollama": {
ApiKey: "ollama",
BaseUrl: baseURL,
Api: useAPIType,
Models: []modelEntry{
{
ID: modelID,
Name: modelID,
Reasoning: reasoning,
Input: []string{"text"},
ContextWindow: 160000,
MaxTokens: 8192,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "kimi-coding" {
cfg.Agents.Defaults.Model.Primary = modelName
base := baseURL
if base == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
base = defaultURL
}
}
plainKey := strings.TrimSpace(apiKey)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"kimi-coding": {
ApiKey: plainKey,
BaseUrl: base,
Api: "anthropic-messages",
Models: []modelEntry{
{
ID: modelID,
Name: "Kimi for Coding",
Reasoning: true,
Input: []string{"text", "image"},
ContextWindow: 262144,
MaxTokens: 32768,
Cost: modelCost{},
},
},
},
},
}
} else if provider == "zai" {
cfg.Agents.Defaults.Model.Primary = "zai/" + modelID
base := baseURL
if base == "" {
if defaultURL, ok := providerDefaultBaseURL(provider); ok {
base = defaultURL
}
}
plainKey := strings.TrimSpace(apiKey)
_, useMaxTokens, useContextWindow := resolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
cfg.Models = &modelsConfig{
Mode: "merge",
Providers: map[string]modelProvider{
"zai": {
ApiKey: plainKey,
BaseUrl: base,
Api: "openai-completions",
Models: []modelEntry{
{
ID: modelID,
Name: zaiModelDisplayName(modelID),
Reasoning: modelID == "glm-5",
Input: []string{"text"},
ContextWindow: useContextWindow,
MaxTokens: useMaxTokens,
Cost: modelCost{},
},
},
},
},
cfg.Agents.Defaults.Model.Primary = patch.PrimaryModel
if patch.Models != nil {
modelsMap, err := mapToModelsConfig(patch.Models)
if err != nil {
return err
}
cfg.Models = modelsMap
}
configPath := path.Join(confDir, "openclaw.json")
@@ -2116,6 +1783,18 @@ func structToMap(value interface{}) (map[string]interface{}, error) {
return result, nil
}
func mapToModelsConfig(value map[string]interface{}) (*modelsConfig, error) {
payload, err := json.Marshal(value)
if err != nil {
return nil, err
}
result := &modelsConfig{}
if err := json.Unmarshal(payload, result); err != nil {
return nil, err
}
return result, nil
}
func providerEnvKey(provider string) string {
return providercatalog.EnvKey(provider)
}
@@ -2162,15 +1841,6 @@ func fixedProviderBaseURL(provider string) (string, bool) {
}
}
func isVerificationSkippedProvider(provider string) bool {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "custom", "vllm", "ollama", "kimi-coding":
return true
default:
return false
}
}
func isSupportedAgentProvider(provider string) bool {
return providercatalog.IsEnabled(provider)
}
@@ -2179,41 +1849,6 @@ func providerDisplayName(provider string) string {
return providercatalog.DisplayName(provider)
}
func buildVerifyRequest(provider, baseURL, apiKey string) (string, map[string]string) {
headers := map[string]string{}
base := strings.TrimRight(baseURL, "/")
switch provider {
case "anthropic":
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
return base + "/models", headers
}
return base + "/v1/models", headers
case "kimi-coding":
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
return base + "/models", headers
}
return base + "/v1/models", headers
case "gemini":
if strings.Contains(base, "/v1beta") {
return fmt.Sprintf("%s/models?key=%s", base, apiKey), headers
}
return fmt.Sprintf("%s/v1beta/models?key=%s", base, apiKey), headers
case "zai":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
return base + "/models", headers
default:
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
return base + "/models", headers
}
return base + "/v1/models", headers
}
}
func readInstallEnv(envStr string) map[string]interface{} {
if strings.TrimSpace(envStr) == "" {
return nil
+2 -1
View File
@@ -53,11 +53,12 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/accounts/delete", baseApi.DeleteAgentAccount)
aiToolsRouter.POST("/agents/channel/feishu/get", baseApi.GetAgentFeishuConfig)
aiToolsRouter.POST("/agents/channel/feishu/update", baseApi.UpdateAgentFeishuConfig)
aiToolsRouter.POST("/agents/channel/feishu/approve", baseApi.ApproveAgentFeishuPairing)
aiToolsRouter.POST("/agents/channel/telegram/get", baseApi.GetAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig)
aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig)
aiToolsRouter.POST("/agents/channel/discord/update", baseApi.UpdateAgentDiscordConfig)
aiToolsRouter.POST("/agents/channel/wecom/get", baseApi.GetAgentWecomConfig)
aiToolsRouter.POST("/agents/channel/wecom/update", baseApi.UpdateAgentWecomConfig)
aiToolsRouter.POST("/agents/channel/qqbot/get", baseApi.GetAgentQQBotConfig)
aiToolsRouter.POST("/agents/channel/qqbot/update", baseApi.UpdateAgentQQBotConfig)
aiToolsRouter.POST("/agents/plugin/install", baseApi.InstallAgentPlugin)
+23 -3
View File
@@ -428,10 +428,30 @@ export namespace AI {
export interface AgentChannelPairingApproveReq {
agentId: number;
type: 'feishu' | 'telegram' | 'discord';
type: 'feishu' | 'telegram' | 'discord' | 'wecom';
pairingCode: string;
}
export interface AgentWecomConfigReq {
agentId: number;
}
export interface AgentWecomConfig {
enabled: boolean;
dmPolicy: 'pairing' | 'open';
botId: string;
secret: string;
installed: boolean;
}
export interface AgentWecomConfigUpdateReq {
agentId: number;
enabled: boolean;
dmPolicy: 'pairing' | 'open';
botId: string;
secret: string;
}
export interface AgentQQBotConfigReq {
agentId: number;
}
@@ -452,13 +472,13 @@ export namespace AI {
export interface AgentPluginInstallReq {
agentId: number;
type: 'qqbot';
type: 'qqbot' | 'wecom';
taskID: string;
}
export interface AgentPluginCheckReq {
agentId: number;
type: 'qqbot';
type: 'qqbot' | 'wecom';
}
export interface AgentPluginStatus {
+8 -4
View File
@@ -145,10 +145,6 @@ export const updateAgentFeishuConfig = (req: AI.AgentFeishuConfigUpdateReq) => {
return http.post(`/ai/agents/channel/feishu/update`, req);
};
export const approveAgentFeishuPairing = (req: AI.AgentFeishuPairingApproveReq) => {
return http.post(`/ai/agents/channel/feishu/approve`, req);
};
export const getAgentTelegramConfig = (req: AI.AgentTelegramConfigReq) => {
return http.post<AI.AgentTelegramConfig>(`/ai/agents/channel/telegram/get`, req);
};
@@ -165,6 +161,14 @@ export const updateAgentDiscordConfig = (req: AI.AgentDiscordConfigUpdateReq) =>
return http.post(`/ai/agents/channel/discord/update`, req);
};
export const getAgentWecomConfig = (req: AI.AgentWecomConfigReq) => {
return http.post<AI.AgentWecomConfig>(`/ai/agents/channel/wecom/get`, req);
};
export const updateAgentWecomConfig = (req: AI.AgentWecomConfigUpdateReq) => {
return http.post(`/ai/agents/channel/wecom/update`, req);
};
export const getAgentQQBotConfig = (req: AI.AgentQQBotConfigReq) => {
return http.post<AI.AgentQQBotConfig>(`/ai/agents/channel/qqbot/get`, req);
};
+2
View File
@@ -699,6 +699,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: 'Plugin is not installed. Please install it first.',
dmPolicy: 'DM Policy',
@@ -707,6 +708,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -707,6 +707,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: 'El plugin no está instalado. Instálalo primero.',
dmPolicy: 'DM Policy',
@@ -715,6 +716,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -700,6 +700,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: 'プラグインがインストールされていません。先にインストールしてください。',
dmPolicy: 'DM Policy',
@@ -708,6 +709,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -692,6 +692,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: '플러그인이 설치되지 않았습니다. 먼저 설치해 주세요.',
dmPolicy: 'DM Policy',
@@ -700,6 +701,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -707,6 +707,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: 'Plugin belum dipasang. Sila pasang dahulu.',
dmPolicy: 'DM Policy',
@@ -715,6 +716,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -702,6 +702,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: 'O plugin não está instalado. Instale-o primeiro.',
dmPolicy: 'DM Policy',
@@ -710,6 +711,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -699,6 +699,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: 'Плагин не установлен. Сначала установите его.',
dmPolicy: 'DM Policy',
@@ -707,6 +708,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -703,6 +703,7 @@ const message = {
executablePath: 'Executable Path',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
wecom: 'WeCom',
feishu: 'Feishu',
pluginNotInstalled: 'Eklenti yüklü değil. Lütfen önce yükleyin.',
dmPolicy: 'DM Policy',
@@ -711,6 +712,7 @@ const message = {
policyOpen: 'Open',
policyDisabled: 'Disabled',
botName: 'Bot Name',
botId: 'Bot ID',
appId: 'App ID',
appSecret: 'App Secret',
saveAndRestartGateway: 'Save and restart gateway',
+2
View File
@@ -667,6 +667,7 @@ const message = {
executablePath: '瀏覽器可執行路徑',
switchModelSuccess: '模型切換成功',
channelsTab: '頻道',
wecom: '企業微信',
feishu: '飛書',
pluginNotInstalled: '插件未安裝,請先安裝插件',
dmPolicy: '私聊策略',
@@ -675,6 +676,7 @@ const message = {
policyOpen: '開放',
policyDisabled: '禁用',
botName: '機器人名稱',
botId: 'Bot ID',
appId: '應用 App ID',
appSecret: '應用 App Secret',
saveAndRestartGateway: '保存並重新啟動網關',
+2
View File
@@ -666,6 +666,7 @@ const message = {
executablePath: '浏览器可执行路径',
switchModelSuccess: '模型切换成功',
channelsTab: '频道',
wecom: '企业微信',
feishu: '飞书',
pluginNotInstalled: '插件未安装,请先安装插件',
dmPolicy: '私聊策略',
@@ -674,6 +675,7 @@ const message = {
policyOpen: '开放',
policyDisabled: '禁用',
botName: '机器人名称',
botId: 'Bot ID',
appId: '应用 App ID',
appSecret: '应用 App Secret',
pairingCode: '配对码',
@@ -3,6 +3,9 @@
<el-tab-pane label="QQ" name="qqbot">
<QQBotTab ref="qqbotRef" />
</el-tab-pane>
<el-tab-pane :label="t('aiTools.agents.wecom')" name="wecom">
<WecomTab ref="wecomRef" />
</el-tab-pane>
<el-tab-pane :label="t('aiTools.agents.feishu')" name="feishu">
<FeishuTab ref="feishuRef" />
</el-tab-pane>
@@ -22,6 +25,7 @@ import FeishuTab from './channels/feishu.vue';
import TelegramTab from './channels/telegram.vue';
import DiscordTab from './channels/discord.vue';
import QQBotTab from './channels/qq.vue';
import WecomTab from './channels/wecom.vue';
const { t } = useI18n();
const activeTab = ref('qqbot');
@@ -30,6 +34,7 @@ const feishuRef = ref();
const telegramRef = ref();
const discordRef = ref();
const qqbotRef = ref();
const wecomRef = ref();
const loadCurrentTab = async () => {
if (agentId.value <= 0) {
@@ -44,6 +49,10 @@ const loadCurrentTab = async () => {
await telegramRef.value?.load(agentId.value);
return;
}
if (activeTab.value === 'wecom') {
await wecomRef.value?.load(agentId.value);
return;
}
if (activeTab.value === 'qqbot') {
await qqbotRef.value?.load(agentId.value);
return;
@@ -0,0 +1,175 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-alert
v-if="!form.installed"
type="warning"
:closable="false"
:title="t('aiTools.agents.pluginNotInstalled')"
class="mb-4"
/>
<el-form-item>
<el-button v-if="!form.installed" type="primary" :loading="installing" @click="installPlugin">
{{ t('commons.button.install') }}
</el-button>
</el-form-item>
<el-form-item :label="t('commons.table.status')">
<el-switch v-model="form.enabled" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.dmPolicy')" prop="dmPolicy">
<el-select v-model="form.dmPolicy">
<el-option :label="t('aiTools.agents.policyPairing')" value="pairing" />
<el-option :label="t('aiTools.agents.policyOpen')" value="open" />
</el-select>
</el-form-item>
<el-form-item :label="t('aiTools.agents.botId')" prop="botId">
<el-input v-model="form.botId" />
</el-form-item>
<el-form-item :label="t('setting.secret')" prop="secret">
<el-input v-model="form.secret" type="password" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveChannel">
{{ t('commons.button.save') }}
</el-button>
</el-form-item>
<el-divider />
<el-form-item :label="t('aiTools.agents.pairingCode')">
<el-input v-model="pairingCode" :placeholder="t('aiTools.agents.pairingCodePlaceholder')" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="approving" @click="approvePairing">
{{ t('aiTools.agents.approvePairing') }}
</el-button>
</el-form-item>
</el-form>
<TaskLog ref="taskLogRef" @close="checkPluginStatus" />
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import {
approveAgentChannelPairing,
checkAgentPlugin,
getAgentWecomConfig,
installAgentPlugin,
updateAgentWecomConfig,
} from '@/api/modules/ai';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import { newUUID } from '@/utils/util';
import TaskLog from '@/components/log/task/index.vue';
const { t } = useI18n();
const saving = ref(false);
const approving = ref(false);
const installing = ref(false);
const agentId = ref(0);
const pairingCode = ref('');
const formRef = ref<FormInstance>();
const taskLogRef = ref();
const form = reactive<AI.AgentWecomConfig>({
enabled: true,
dmPolicy: 'pairing',
botId: '',
secret: '',
installed: false,
});
const rules = reactive({
dmPolicy: [Rules.requiredSelect],
botId: [Rules.requiredInput],
secret: [Rules.requiredInput],
});
const checkPluginStatus = async () => {
if (!agentId.value) {
return;
}
const res = await checkAgentPlugin({
agentId: agentId.value,
type: 'wecom',
});
form.installed = Boolean(res.data?.installed);
};
const load = async (id: number) => {
agentId.value = id;
pairingCode.value = '';
const res = await getAgentWecomConfig({ agentId: id });
Object.assign(form, res.data || {});
if (!form.dmPolicy) {
form.dmPolicy = 'pairing';
}
await checkPluginStatus();
};
const saveChannel = async () => {
if (!agentId.value || !formRef.value) {
return;
}
await formRef.value.validate();
saving.value = true;
try {
await updateAgentWecomConfig({
agentId: agentId.value,
enabled: form.enabled,
dmPolicy: form.dmPolicy,
botId: form.botId,
secret: form.secret,
});
MsgSuccess(t('aiTools.agents.saveSuccess'));
} finally {
saving.value = false;
}
};
const approvePairing = async () => {
if (!agentId.value) {
return;
}
if (!pairingCode.value) {
MsgWarning(t('aiTools.agents.pairingCodeRequired'));
return;
}
approving.value = true;
try {
await approveAgentChannelPairing({
agentId: agentId.value,
type: 'wecom',
pairingCode: pairingCode.value,
});
MsgSuccess(t('aiTools.agents.pairingApproveSuccess'));
pairingCode.value = '';
} finally {
approving.value = false;
}
};
const installPlugin = async () => {
if (!agentId.value) {
return;
}
const taskID = newUUID();
installing.value = true;
try {
await installAgentPlugin({
agentId: agentId.value,
type: 'wecom',
taskID,
});
taskLogRef.value?.openWithTaskID(taskID);
} finally {
installing.value = false;
}
};
defineExpose({
load,
});
</script>