From efb612734deacee41550b1fabfb6a2f95f15b041 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 2 Mar 2026 08:08:10 +0800 Subject: [PATCH] Add integration credential verification endpoint to robot API - Introduce a new POST endpoint for verifying integration credentials at /robots/integrations/verify. - Ensure the endpoint is positioned correctly in the routing to avoid conflicts with existing routes. - Enhance the API's capabilities for integration management by allowing credential verification. --- openapi/agent/robot/robot.go | 3 + openapi/agent/robot/verify.go | 179 ++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 openapi/agent/robot/verify.go diff --git a/openapi/agent/robot/robot.go b/openapi/agent/robot/robot.go index e9b54b4e..7bb0dcd4 100644 --- a/openapi/agent/robot/robot.go +++ b/openapi/agent/robot/robot.go @@ -22,6 +22,9 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) { // Activities - Cross-robot activity feed for team (must be before /:id to avoid conflict) group.GET("/activities", ListActivities) // GET /robots/activities - List team activities + // Integration credential verification (must be before /:id to avoid conflict) + group.POST("/integrations/verify", VerifyIntegration) // POST /robots/integrations/verify - Verify integration credentials + group.GET("/:id", GetRobot) // GET /robots/:id - Get robot details group.PUT("/:id", UpdateRobot) // PUT /robots/:id - Update robot group.DELETE("/:id", DeleteRobot) // DELETE /robots/:id - Delete robot diff --git a/openapi/agent/robot/verify.go b/openapi/agent/robot/verify.go new file mode 100644 index 00000000..04f2b419 --- /dev/null +++ b/openapi/agent/robot/verify.go @@ -0,0 +1,179 @@ +package robot + +import ( + "context" + "fmt" + "time" + + "github.com/gin-gonic/gin" + larkcore "github.com/larksuite/oapi-sdk-go/v3/core" + "github.com/yaoapp/yao/integrations/dingtalk" + "github.com/yaoapp/yao/integrations/discord" + "github.com/yaoapp/yao/integrations/feishu" + "github.com/yaoapp/yao/integrations/telegram" + "github.com/yaoapp/yao/openapi/response" +) + +// VerifyIntegrationRequest — POST body for credential verification. +type VerifyIntegrationRequest struct { + Provider string `json:"provider" binding:"required"` // telegram | feishu | dingtalk | discord + Config map[string]any `json:"config" binding:"required"` +} + +// VerifyIntegrationResponse — returned to the frontend. +type VerifyIntegrationResponse struct { + Valid bool `json:"valid"` + Info map[string]any `json:"info,omitempty"` + Error string `json:"error,omitempty"` +} + +// VerifyIntegration tests whether the supplied credentials are valid +// by making a lightweight API call to the target platform. +// +// POST /v1/agent/robots/integrations/verify +func VerifyIntegration(c *gin.Context) { + var req VerifyIntegrationRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.RespondWithError(c, response.StatusBadRequest, &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Invalid request body: " + err.Error(), + }) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + var resp VerifyIntegrationResponse + + switch req.Provider { + case "telegram": + resp = verifyTelegram(ctx, req.Config) + case "feishu": + resp = verifyFeishu(ctx, req.Config) + case "dingtalk": + resp = verifyDingtalk(ctx, req.Config) + case "discord": + resp = verifyDiscord(ctx, req.Config) + default: + response.RespondWithError(c, response.StatusBadRequest, &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: fmt.Sprintf("unsupported provider: %s", req.Provider), + }) + return + } + + response.RespondWithSuccess(c, response.StatusOK, resp) +} + +func str(m map[string]any, key string) string { + v, _ := m[key].(string) + return v +} + +func verifyTelegram(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse { + token := str(cfg, "bot_token") + if token == "" { + return VerifyIntegrationResponse{Valid: false, Error: "bot_token is required"} + } + + var opts []telegram.BotOption + if host := str(cfg, "host"); host != "" { + opts = append(opts, telegram.WithAPIBase(host)) + } + + bot := telegram.NewBot(token, "", opts...) + user, err := bot.GetMe(ctx) + if err != nil { + return VerifyIntegrationResponse{Valid: false, Error: err.Error()} + } + + info := map[string]any{ + "id": user.ID, + "username": user.Username, + "name": user.FirstName, + } + if user.LastName != "" { + info["name"] = user.FirstName + " " + user.LastName + } + return VerifyIntegrationResponse{Valid: true, Info: info} +} + +func verifyFeishu(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse { + appID := str(cfg, "app_id") + appSecret := str(cfg, "app_secret") + if appID == "" || appSecret == "" { + return VerifyIntegrationResponse{Valid: false, Error: "app_id and app_secret are required"} + } + + bot := feishu.NewBot(appID, appSecret) + + resp, err := bot.Client().GetTenantAccessTokenBySelfBuiltApp(ctx, + &larkcore.SelfBuiltTenantAccessTokenReq{AppID: appID, AppSecret: appSecret}) + if err != nil { + return VerifyIntegrationResponse{Valid: false, Error: err.Error()} + } + if resp == nil || !resp.Success() { + msg := "failed to obtain tenant access token" + if resp != nil { + msg = fmt.Sprintf("code=%d msg=%s", resp.Code, resp.Msg) + } + return VerifyIntegrationResponse{Valid: false, Error: msg} + } + + return VerifyIntegrationResponse{ + Valid: true, + Info: map[string]any{ + "app_id": appID, + "status": "credentials_valid", + }, + } +} + +func verifyDingtalk(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse { + clientID := str(cfg, "client_id") + clientSecret := str(cfg, "client_secret") + if clientID == "" || clientSecret == "" { + return VerifyIntegrationResponse{Valid: false, Error: "client_id and client_secret are required"} + } + + bot := dingtalk.NewBot(clientID, clientSecret) + if err := bot.GetBotInfo(ctx); err != nil { + return VerifyIntegrationResponse{Valid: false, Error: err.Error()} + } + + return VerifyIntegrationResponse{ + Valid: true, + Info: map[string]any{ + "client_id": clientID, + "status": "credentials_valid", + }, + } +} + +func verifyDiscord(ctx context.Context, cfg map[string]any) VerifyIntegrationResponse { + botToken := str(cfg, "bot_token") + if botToken == "" { + return VerifyIntegrationResponse{Valid: false, Error: "bot_token is required"} + } + + appID := str(cfg, "app_id") + bot, err := discord.NewBot(botToken, appID) + if err != nil { + return VerifyIntegrationResponse{Valid: false, Error: err.Error()} + } + + user, err := bot.BotUser() + if err != nil { + return VerifyIntegrationResponse{Valid: false, Error: err.Error()} + } + + return VerifyIntegrationResponse{ + Valid: true, + Info: map[string]any{ + "id": user.ID, + "username": user.Username, + "name": user.GlobalName, + }, + } +}