Preface As a blog author, I often run into these annoyances:
A reader submits a comment or message, and I can’t know about it right away
I have to log into the admin panel to review comments, which is cumbersome
Spam comments and legitimate comments are mixed together, making screening difficult
To solve these problems, I decided to build a Feishu approval bot for my blog, achieving:
📱 New comments/messages pushed to Feishu in real time
✅ One-click approval or rejection
🔄 Real-time card status updates
Results
Technical approach Architecture design ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Blog frontend │────▶│ Blog API │────▶│ Feishu server │ └─────────────┘ └─────────────┘ └─────────────┘ │ ┌──────┴──────┐ │ WebSocket │ │ long conn │ └──────┬──────┘ ▼ ┌─────────────┐ │ Feishu client │ └─────────────┘
Core tech choices
Feishu SDK : oapi-sdk-go/v3 - the official Go SDK
Connection : WebSocket long connection (no public domain needed)
Card interaction : Feishu message cards v2
Implementation process 1. Create the Feishu app First, create an app on the Feishu Open Platform :
Create a custom enterprise app
Get the App ID and App Secret
Subscribe to events: im.message.receive_v1 and card.action.trigger
Enable the bot capability
2. Core code implementation FeishuBotService struct type FeishuBotService struct { client *lark.Client db *gorm.DB appID string appSecret string adminOpenID string }
Starting the WebSocket connection func (s *FeishuBotService) Start(ctx context.Context) error { eventHandler := dispatcher.NewEventDispatcher("" , "" ). OnP2CardActionTrigger(func (ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error ) { return s.handleCardAction(event) }) cli := larkws.NewClient(s.appID, s.appSecret, larkws.WithEventHandler(eventHandler), ) return cli.Start(ctx) }
Sending the notification card func (s *FeishuBotService) SendCommentNotification(comment *models.Comment) error { card := map [string ]interface {}{ "config" : map [string ]interface {}{"wide_screen_mode" : true }, "header" : map [string ]interface {}{ "title" : map [string ]interface {}{ "tag" : "plain_text" , "content" : "📝 收到新评论" , }, "template" : "blue" , }, "elements" : []interface {}{ map [string ]interface {}{ "tag" : "div" , "fields" : []interface {}{ map [string ]interface {}{ "is_short" : true , "text" : map [string ]interface {}{ "tag" : "lark_md" , "content" : fmt.Sprintf("**评论者:**\n%s" , comment.Nickname), }, }, }, }, map [string ]interface {}{ "tag" : "action" , "actions" : []interface {}{ map [string ]interface {}{ "tag" : "button" , "text" : map [string ]interface {}{ "tag" : "plain_text" , "content" : "✅ 通过" , }, "type" : "primary" , "value" : map [string ]interface {}{ "action" : "approve_comment" , "comment_id" : comment.ID, }, }, map [string ]interface {}{ "tag" : "button" , "text" : map [string ]interface {}{ "tag" : "plain_text" , "content" : "❌ 拒绝" , }, "type" : "danger" , "value" : map [string ]interface {}{ "action" : "reject_comment" , "comment_id" : comment.ID, }, }, }, }, }, } return s.sendCard(s.adminOpenID, card) }
func (s *FeishuBotService) handleCardAction(event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error ) { action := event.Event.Action.Value["action" ] switch action { case "approve_comment" : return s.handleApproveComment(event) case "reject_comment" : return s.handleRejectComment(event) } }func (s *FeishuBotService) handleApproveComment(event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error ) { commentID := event.Event.Action.Value["comment_id" ] s.db.Model(&models.Comment{}).Where("id = ?" , commentID).Update("status" , 1 ) return &callback.CardActionTriggerResponse{ Toast: &callback.Toast{ Type: "success" , Content: "✅ 评论已通过" , }, Card: &callback.Card{ Type: "raw" , Data: s.buildApprovedCommentCard(comment), }, }, nil }
3. Integrating with the blog system Start the bot in the blog’s main program:
func main () { if cfg.Feishu.AppID != "" { botService := services.NewFeishuBotService( db, cfg.Feishu.AppID, cfg.Feishu.AppSecret, cfg.Feishu.AdminOpenID, ) go func () { botService.Start(context.Background()) }() } r.Run(addr) }
4. Deployment configuration Manage the service with systemd, configuring via environment variables:
[Service] Environment ="FEISHU_APP_ID=cli_xxxxxxxx" Environment ="FEISHU_APP_SECRET=xxxxxxxx" Environment ="FEISHU_ADMIN_OPEN_ID=ou_xxxxxxxx"
Problems encountered Symptom : Sending the card reports parse card json err
Fix : Feishu cards require array types to be []interface{} rather than []map[string]interface{}
"elements" : []map [string ]interface {}{...}"elements" : []interface {}{...}
Symptom : After clicking the button, the card doesn’t update
Fix : The callback response must use the callback.Card struct:
return &callback.CardActionTriggerResponse{ Toast: &callback.Toast{Type: "success" , Content: "已处理" }, Card: &callback.Card{ Type: "raw" , Data: cardData, }, }, nil
3. Getting the admin OpenID Send the bot any message, then grab the sender’s OpenID from the logs:
journalctl -u blog-api -f | grep "OpenId"
Final results ✅ Real-time notifications : Within 1 second of a comment/message being submitted, a Feishu notification arrives ✅ One-click approval : Click a button to complete the review, no admin panel login needed ✅ Status sync : After an action, the card updates immediately, showing the approval result and time ✅ Long connection : WebSocket mode, no public callback address needed
Summary With the Feishu bot, I simplified the blog comment review workflow from “log into admin → find the comment → review” to “receive notification → click a button,” greatly improving efficiency. At the same time, the WebSocket long-connection mode makes deployment simple—no public domain or callback configuration needed.
If you’d also like to add similar functionality to your blog, feel free to refer to the implementation in this article.
评论
0 条评论