2026-01-05 04:33:30 +08:00
|
|
|
|
package welcome
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"cellbot/internal/engine"
|
|
|
|
|
|
"cellbot/internal/protocol"
|
|
|
|
|
|
"cellbot/pkg/utils"
|
|
|
|
|
|
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
|
// 监听群成员增加通知事件
|
|
|
|
|
|
engine.OnNotice("group_increase").
|
|
|
|
|
|
Priority(50). // 设置较高优先级,确保及时响应
|
|
|
|
|
|
Handle(handleWelcomeEvent)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleWelcomeEvent 处理群成员加入欢迎事件
|
|
|
|
|
|
func handleWelcomeEvent(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
|
|
|
|
|
|
logger.Info("Welcome event received",
|
|
|
|
|
|
zap.String("event_type", string(event.GetType())),
|
|
|
|
|
|
zap.String("detail_type", event.GetDetailType()),
|
|
|
|
|
|
zap.String("sub_type", event.GetSubType()),
|
|
|
|
|
|
zap.String("self_id", event.GetSelfID()))
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:中间件已经过滤了 detail_type,这里可以简化检查
|
|
|
|
|
|
data := event.GetData()
|
|
|
|
|
|
logger.Debug("Event data", zap.Any("data", data))
|
|
|
|
|
|
|
|
|
|
|
|
// 获取群ID和用户ID
|
|
|
|
|
|
groupID, ok := data["group_id"]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
logger.Warn("Missing group_id in event data")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
userID, ok := data["user_id"]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
logger.Warn("Missing user_id in event data")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.Info("Processing welcome event",
|
|
|
|
|
|
zap.Any("group_id", groupID),
|
|
|
|
|
|
zap.Any("user_id", userID))
|
|
|
|
|
|
|
|
|
|
|
|
// 获取操作者ID(邀请者或审批者)
|
|
|
|
|
|
var operatorID interface{}
|
|
|
|
|
|
if opID, exists := data["operator_id"]; exists {
|
|
|
|
|
|
operatorID = opID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取子类型(approve: 管理员同意, invite: 邀请)
|
|
|
|
|
|
subType := event.GetSubType()
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 bot 实例
|
|
|
|
|
|
selfID := event.GetSelfID()
|
|
|
|
|
|
bot, ok := botManager.Get(selfID)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
bots := botManager.GetAll()
|
|
|
|
|
|
if len(bots) == 0 {
|
|
|
|
|
|
logger.Error("No bot instance available")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
bot = bots[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建欢迎消息链(使用HTML模板渲染图片)
|
|
|
|
|
|
logger.Info("Building welcome message",
|
|
|
|
|
|
zap.Any("user_id", userID),
|
|
|
|
|
|
zap.Any("operator_id", operatorID),
|
|
|
|
|
|
zap.String("sub_type", subType))
|
|
|
|
|
|
|
|
|
|
|
|
welcomeChain, err := buildWelcomeMessage(ctx, userID, operatorID, subType, logger)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Error("Failed to build welcome message",
|
|
|
|
|
|
zap.Any("user_id", userID),
|
|
|
|
|
|
zap.Any("operator_id", operatorID),
|
|
|
|
|
|
zap.String("sub_type", subType),
|
|
|
|
|
|
zap.Error(err))
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.Info("Welcome message built successfully",
|
|
|
|
|
|
zap.Int("chain_length", len(welcomeChain)))
|
|
|
|
|
|
|
|
|
|
|
|
// 发送群消息
|
|
|
|
|
|
logger.Info("Sending welcome message",
|
|
|
|
|
|
zap.Any("group_id", groupID),
|
|
|
|
|
|
zap.Any("user_id", userID))
|
|
|
|
|
|
|
|
|
|
|
|
action := &protocol.BaseAction{
|
|
|
|
|
|
Type: protocol.ActionTypeSendGroupMessage,
|
|
|
|
|
|
Params: map[string]interface{}{
|
|
|
|
|
|
"group_id": groupID,
|
|
|
|
|
|
"message": welcomeChain,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result, err := bot.SendAction(ctx, action)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Error("Failed to send welcome message",
|
|
|
|
|
|
zap.Any("group_id", groupID),
|
|
|
|
|
|
zap.Any("user_id", userID),
|
|
|
|
|
|
zap.Any("action_type", action.Type),
|
|
|
|
|
|
zap.Error(err))
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.Info("Welcome message sent successfully",
|
|
|
|
|
|
zap.Any("group_id", groupID),
|
|
|
|
|
|
zap.Any("user_id", userID),
|
|
|
|
|
|
zap.Any("result", result))
|
|
|
|
|
|
|
|
|
|
|
|
logger.Info("Welcome message sent",
|
|
|
|
|
|
zap.Any("group_id", groupID),
|
|
|
|
|
|
zap.Any("user_id", userID),
|
|
|
|
|
|
zap.String("sub_type", subType))
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// welcomeTemplate HTML欢迎消息模板
|
|
|
|
|
|
const welcomeTemplate = `
|
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
padding: 40px 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.welcome-icon {
|
|
|
|
|
|
font-size: 64px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.title {
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
.content {
|
|
|
|
|
|
margin: 30px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.user-info {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
|
border-bottom: 1px solid #e9ecef;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-item:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-label {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #495057;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-value {
|
|
|
|
|
|
color: #212529;
|
|
|
|
|
|
}
|
|
|
|
|
|
.join-type {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tips {
|
|
|
|
|
|
background: #e7f3ff;
|
|
|
|
|
|
border-left: 4px solid #2196F3;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tips-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #1976D2;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tips-list {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
padding-left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tips-list li {
|
|
|
|
|
|
padding: 5px 0;
|
|
|
|
|
|
color: #424242;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tips-list li:before {
|
|
|
|
|
|
content: "• ";
|
|
|
|
|
|
color: #2196F3;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
.footer {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-top: 30px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="welcome-icon">🎉</div>
|
|
|
|
|
|
<div class="title">欢迎加入!</div>
|
|
|
|
|
|
<div class="subtitle">Welcome to the Group</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="content">
|
|
|
|
|
|
<div class="user-info">
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="info-label">用户ID:</span>
|
|
|
|
|
|
<span class="info-value">{{.UserID}}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{{if .OperatorID}}
|
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
|
<span class="info-label">{{.OperatorLabel}}:</span>
|
|
|
|
|
|
<span class="info-value">{{.OperatorID}}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{{end}}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{if .JoinType}}
|
|
|
|
|
|
<div class="join-type">
|
|
|
|
|
|
{{.JoinType}}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{{end}}
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tips">
|
|
|
|
|
|
<div class="tips-title">💡 温馨提示</div>
|
|
|
|
|
|
<ul class="tips-list">
|
|
|
|
|
|
<li>请遵守群规,文明交流</li>
|
|
|
|
|
|
<li>如有问题可以@管理员</li>
|
|
|
|
|
|
<li>发送 /help 查看帮助</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
|
|
希望你在群里玩得开心!
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
|
|
// buildWelcomeMessage 构建欢迎消息链(使用HTML模板渲染图片)
|
|
|
|
|
|
func buildWelcomeMessage(ctx context.Context, userID, operatorID interface{}, subType string, logger *zap.Logger) (protocol.MessageChain, error) {
|
|
|
|
|
|
logger.Debug("Starting to build welcome message",
|
|
|
|
|
|
zap.Any("user_id", userID),
|
|
|
|
|
|
zap.Any("operator_id", operatorID),
|
|
|
|
|
|
zap.String("sub_type", subType))
|
|
|
|
|
|
|
|
|
|
|
|
// 准备模板数据
|
|
|
|
|
|
data := map[string]interface{}{
|
|
|
|
|
|
"UserID": fmt.Sprintf("%v", userID),
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.Debug("Template data prepared", zap.Any("data", data))
|
|
|
|
|
|
|
|
|
|
|
|
// 根据加入方式设置不同的信息
|
|
|
|
|
|
switch subType {
|
|
|
|
|
|
case "approve":
|
|
|
|
|
|
data["JoinType"] = "✅ 管理员审批通过"
|
|
|
|
|
|
if operatorID != nil {
|
|
|
|
|
|
data["OperatorID"] = fmt.Sprintf("%v", operatorID)
|
|
|
|
|
|
data["OperatorLabel"] = "审批管理员"
|
|
|
|
|
|
}
|
|
|
|
|
|
case "invite":
|
|
|
|
|
|
data["JoinType"] = "👥 被邀请加入"
|
|
|
|
|
|
if operatorID != nil {
|
|
|
|
|
|
data["OperatorID"] = fmt.Sprintf("%v", operatorID)
|
|
|
|
|
|
data["OperatorLabel"] = "邀请人"
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
data["JoinType"] = "🎊 加入群聊"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 配置截图选项
|
|
|
|
|
|
opts := &utils.ScreenshotOptions{
|
2026-01-05 05:14:31 +08:00
|
|
|
|
Width: 1200, // 增加宽度,确保内容有足够空间
|
|
|
|
|
|
Height: 800, // 增加高度,确保内容完整显示
|
2026-01-05 04:33:30 +08:00
|
|
|
|
Timeout: 60 * time.Second, // 增加超时时间到60秒
|
|
|
|
|
|
WaitTime: 3 * time.Second, // 增加等待时间,确保页面完全加载
|
|
|
|
|
|
FullPage: false,
|
|
|
|
|
|
Quality: 90,
|
|
|
|
|
|
Logger: logger,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染模板并截图
|
|
|
|
|
|
logger.Info("Rendering template and taking screenshot",
|
|
|
|
|
|
zap.Int("width", opts.Width),
|
|
|
|
|
|
zap.Int("height", opts.Height),
|
|
|
|
|
|
zap.Duration("timeout", opts.Timeout))
|
|
|
|
|
|
|
|
|
|
|
|
chain, err := utils.ScreenshotTemplateToMessageChain(ctx, welcomeTemplate, data, opts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Error("Failed to render welcome template",
|
|
|
|
|
|
zap.Any("data", data),
|
|
|
|
|
|
zap.Error(err))
|
|
|
|
|
|
return nil, fmt.Errorf("failed to render welcome template: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.Info("Template rendered and screenshot taken successfully",
|
|
|
|
|
|
zap.Int("chain_length", len(chain)))
|
|
|
|
|
|
|
|
|
|
|
|
return chain, nil
|
|
|
|
|
|
}
|