深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

Blog Comment Feishu Approval Bot: Real-Time Comment Notifications and One-Click Approval

English 阅读中文版

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

Feishu bot 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:

  1. Create a custom enterprise app
  2. Get the App ID and App Secret
  3. Subscribe to events: im.message.receive_v1 and card.action.trigger
  4. 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 // the admin who receives notifications
}

Starting the WebSocket connection

func (s *FeishuBotService) Start(ctx context.Context) error {
// Create the event handler
eventHandler := dispatcher.NewEventDispatcher("", "").
OnP2CardActionTrigger(func(ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error) {
return s.handleCardAction(event)
})

// WebSocket client
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{}{
// Comment info display
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),
},
},
// ... more fields
},
},
// Action buttons
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)
}

Handling button clicks

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)
// ... other actions
}
}

func (s *FeishuBotService) handleApproveComment(event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error) {
commentID := event.Event.Action.Value["comment_id"]

// Update the database status
s.db.Model(&models.Comment{}).Where("id = ?", commentID).Update("status", 1)

// Return the updated card
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() {
// ... initialize the database, routes, etc.

// Start the Feishu bot
if cfg.Feishu.AppID != "" {
botService := services.NewFeishuBotService(
db,
cfg.Feishu.AppID,
cfg.Feishu.AppSecret,
cfg.Feishu.AdminOpenID,
)

go func() {
botService.Start(context.Background())
}()
}

// Start the HTTP server
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

1. Card JSON format error

Symptom: Sending the card reports parse card json err

Fix: Feishu cards require array types to be []interface{} rather than []map[string]interface{}

// Wrong
"elements": []map[string]interface{}{...}

// Right
"elements": []interface{}{...}

2. Callback response format

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, // map[string]interface{}
},
}, 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.

文章作者:阿文
文章链接: https://www.awen.me/post/5476ca89.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!
本文于 2026-03-01 15:30 发布,已超过半年(229天),请注意甄别内容是否已过期。

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️