mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-25 08:00:56 +00:00
feat: openclaw channel support wecom (#12146)
This commit is contained in:
+3
-1
@@ -73,4 +73,6 @@ agent/.golangci.yml
|
||||
openspec
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
opencode.json
|
||||
opencode.json
|
||||
superpowers
|
||||
.worktrees/
|
||||
|
||||
+41
-20
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '保存並重新啟動網關',
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user