377 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package notification
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"aimodels-prices/models"
)
// FeishuWebhook 飞书webhook配置
type FeishuWebhook struct {
URL string
}
// TextMessage 文本消息结构
type TextMessage struct {
MsgType string `json:"msg_type"`
Content struct {
Text string `json:"text"`
} `json:"content"`
}
// CardMessage 卡片消息结构
type CardMessage struct {
MsgType string `json:"msg_type"`
Card Card `json:"card"`
}
// Card 卡片结构
type Card struct {
Schema string `json:"schema"`
Config CardConfig `json:"config"`
Header CardHeader `json:"header"`
Body CardBody `json:"body"`
}
// CardConfig 卡片配置
type CardConfig struct {
UpdateMulti bool `json:"update_multi"`
}
// CardHeader 卡片头部
type CardHeader struct {
Title Title `json:"title"`
Template string `json:"template"`
Padding string `json:"padding"`
}
// Title 标题
type Title struct {
Tag string `json:"tag"`
Content string `json:"content"`
}
// CardBody 卡片主体
type CardBody struct {
Direction string `json:"direction"`
Padding string `json:"padding"`
Elements []CardElement `json:"elements"`
}
// CardElement 卡片元素
type CardElement struct {
Tag string `json:"tag"`
Content string `json:"content,omitempty"`
TextAlign string `json:"text_align,omitempty"`
TextSize string `json:"text_size,omitempty"`
Margin string `json:"margin,omitempty"`
}
// NewFeishuWebhook 创建飞书webhook实例
func NewFeishuWebhook() *FeishuWebhook {
url := os.Getenv("FEISHU_WEBHOOK_URL")
if url == "" {
return nil
}
return &FeishuWebhook{URL: url}
}
// SendTextMessage 发送文本消息
func (f *FeishuWebhook) SendTextMessage(text string) error {
if f == nil || f.URL == "" {
return nil // 如果没有配置webhook则跳过
}
message := TextMessage{
MsgType: "text",
Content: struct {
Text string `json:"text"`
}{
Text: text,
},
}
return f.sendMessage(message)
}
// SendPendingPriceNotification 发送待审核价格通知卡片
func (f *FeishuWebhook) SendPendingPriceNotification(price models.Price, providerName string, isNew bool) error {
if f == nil || f.URL == "" {
return nil // 如果没有配置webhook则跳过
}
var actionText string
if isNew {
actionText = "新增"
} else {
actionText = "更新"
}
// 构建卡片内容
content := fmt.Sprintf("**%s模型价格**\n\n", actionText)
content += fmt.Sprintf("**模型名称:** %s\n", getDisplayModel(price))
content += fmt.Sprintf("**厂商:** %s\n", providerName)
content += fmt.Sprintf("**计费类型:** %s\n", getBillingTypeText(getDisplayBillingType(price)))
content += fmt.Sprintf("**输入价格:** %.6f %s/1K tokens\n", getDisplayInputPrice(price), getDisplayCurrency(price))
content += fmt.Sprintf("**输出价格:** %.6f %s/1K tokens\n", getDisplayOutputPrice(price), getDisplayCurrency(price))
content += fmt.Sprintf("**创建者:** %s\n", price.CreatedBy)
content += fmt.Sprintf("**创建时间:** %s", time.Now().Format("2006-01-02 15:04:05"))
// 如果有扩展价格字段,也显示出来
if hasExtendedPrices(price) {
content += "\n\n**扩展价格:**\n"
if getDisplayInputAudioTokens(price) != nil {
content += fmt.Sprintf("- 音频输入:%.6f %s/1K tokens\n", *getDisplayInputAudioTokens(price), getDisplayCurrency(price))
}
if getDisplayOutputAudioTokens(price) != nil {
content += fmt.Sprintf("- 音频输出:%.6f %s/1K tokens\n", *getDisplayOutputAudioTokens(price), getDisplayCurrency(price))
}
if getDisplayCachedTokens(price) != nil {
content += fmt.Sprintf("- 缓存:%.6f %s/1K tokens\n", *getDisplayCachedTokens(price), getDisplayCurrency(price))
}
if getDisplayReasoningTokens(price) != nil {
content += fmt.Sprintf("- 推理:%.6f %s/1K tokens\n", *getDisplayReasoningTokens(price), getDisplayCurrency(price))
}
}
card := CardMessage{
MsgType: "interactive",
Card: Card{
Schema: "2.0",
Config: CardConfig{
UpdateMulti: true,
},
Header: CardHeader{
Title: Title{
Tag: "plain_text",
Content: fmt.Sprintf("🔔 有新的价格待审核 - %s", actionText),
},
Template: "orange",
Padding: "12px 12px 12px 12px",
},
Body: CardBody{
Direction: "vertical",
Padding: "12px 12px 12px 12px",
Elements: []CardElement{
{
Tag: "markdown",
Content: content,
TextAlign: "left",
TextSize: "normal",
Margin: "0px 0px 0px 0px",
},
},
},
},
}
return f.sendMessage(card)
}
// SendBatchNotification 发送批量通知
func (f *FeishuWebhook) SendBatchNotification(count int) error {
if f == nil || f.URL == "" {
return nil
}
content := fmt.Sprintf("📢 **批量价格更新通知**\n\n本次共有 **%d** 个模型价格等待审核,请及时处理。", count)
card := CardMessage{
MsgType: "interactive",
Card: Card{
Schema: "2.0",
Config: CardConfig{
UpdateMulti: true,
},
Header: CardHeader{
Title: Title{
Tag: "plain_text",
Content: "📊 批量价格更新通知",
},
Template: "blue",
Padding: "12px 12px 12px 12px",
},
Body: CardBody{
Direction: "vertical",
Padding: "12px 12px 12px 12px",
Elements: []CardElement{
{
Tag: "markdown",
Content: content,
TextAlign: "left",
TextSize: "normal",
Margin: "0px 0px 0px 0px",
},
},
},
},
}
return f.sendMessage(card)
}
// SendPendingPricesDetailedNotification 发送详细的待审核价格统计通知
func (f *FeishuWebhook) SendPendingPricesDetailedNotification(content string, count int) error {
if f == nil || f.URL == "" {
return nil
}
card := CardMessage{
MsgType: "interactive",
Card: Card{
Schema: "2.0",
Config: CardConfig{
UpdateMulti: true,
},
Header: CardHeader{
Title: Title{
Tag: "plain_text",
Content: fmt.Sprintf("🔍 待审核价格检查报告 - %d个待审核", count),
},
Template: "red",
Padding: "12px 12px 12px 12px",
},
Body: CardBody{
Direction: "vertical",
Padding: "12px 12px 12px 12px",
Elements: []CardElement{
{
Tag: "markdown",
Content: content,
TextAlign: "left",
TextSize: "normal",
Margin: "0px 0px 0px 0px",
},
},
},
},
}
return f.sendMessage(card)
}
// sendMessage 发送消息到飞书
func (f *FeishuWebhook) sendMessage(message interface{}) error {
jsonData, err := json.Marshal(message)
if err != nil {
return fmt.Errorf("failed to marshal message: %v", err)
}
resp, err := http.Post(f.URL, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to send webhook: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("webhook returned status code: %d", resp.StatusCode)
}
return nil
}
// 辅助函数:获取显示用的模型名称
func getDisplayModel(price models.Price) string {
if price.TempModel != nil {
return *price.TempModel
}
return price.Model
}
// 辅助函数:获取显示用的计费类型
func getDisplayBillingType(price models.Price) string {
if price.TempBillingType != nil {
return *price.TempBillingType
}
return price.BillingType
}
// 辅助函数:获取显示用的货币
func getDisplayCurrency(price models.Price) string {
if price.TempCurrency != nil {
return *price.TempCurrency
}
return price.Currency
}
// 辅助函数:获取显示用的输入价格
func getDisplayInputPrice(price models.Price) float64 {
if price.TempInputPrice != nil {
return *price.TempInputPrice
}
return price.InputPrice
}
// 辅助函数:获取显示用的输出价格
func getDisplayOutputPrice(price models.Price) float64 {
if price.TempOutputPrice != nil {
return *price.TempOutputPrice
}
return price.OutputPrice
}
// 辅助函数:获取显示用的音频输入价格
func getDisplayInputAudioTokens(price models.Price) *float64 {
if price.TempInputAudioTokens != nil {
return price.TempInputAudioTokens
}
return price.InputAudioTokens
}
// 辅助函数:获取显示用的音频输出价格
func getDisplayOutputAudioTokens(price models.Price) *float64 {
if price.TempOutputAudioTokens != nil {
return price.TempOutputAudioTokens
}
return price.OutputAudioTokens
}
// 辅助函数:获取显示用的缓存价格
func getDisplayCachedTokens(price models.Price) *float64 {
if price.TempCachedTokens != nil {
return price.TempCachedTokens
}
return price.CachedTokens
}
// 辅助函数:获取显示用的推理价格
func getDisplayReasoningTokens(price models.Price) *float64 {
if price.TempReasoningTokens != nil {
return price.TempReasoningTokens
}
return price.ReasoningTokens
}
// 辅助函数:检查是否有扩展价格字段
func hasExtendedPrices(price models.Price) bool {
return getDisplayInputAudioTokens(price) != nil ||
getDisplayOutputAudioTokens(price) != nil ||
getDisplayCachedTokens(price) != nil ||
getDisplayReasoningTokens(price) != nil
}
// 辅助函数:获取计费类型中文显示
func getBillingTypeText(billingType string) string {
switch billingType {
case "token":
return "按Token计费"
case "request":
return "按请求计费"
case "minute":
return "按分钟计费"
case "hour":
return "按小时计费"
case "day":
return "按天计费"
case "month":
return "按月计费"
default:
return billingType
}
}