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.
This commit is contained in:
Max 2026-03-02 08:08:10 +08:00
parent 5333302055
commit efb612734d
2 changed files with 182 additions and 0 deletions

View file

@ -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

View file

@ -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,
},
}
}