Merge pull request #13 from voidxxl7/main

通知渠道增加企业微信应用
This commit is contained in:
dushixiang authored and GitHub committed 2025-12-22 16:21:04 +08:00
commit 13c5ab7ef5
3 files changed
+231 -2

No files matched your search

+120
View File
@@ -7,6 +7,7 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -17,6 +18,7 @@ import (
"github.com/dushixiang/pika/internal/models"
"github.com/dushixiang/pika/internal/utils"
"github.com/go-orz/cache"
"github.com/valyala/fasttemplate"
"go.uber.org/zap"
"gopkg.in/gomail.v2"
@@ -278,6 +280,85 @@ func (n *Notifier) sendWeCom(ctx context.Context, webhook, message string) error
return nil
}
var wecomAppAccessTokenCache = cache.New[string, string](time.Minute)
func (n *Notifier) getWecomAppToken(ctx context.Context, origin, corpId, corpSecret string) (string, error) {
key := fmt.Sprintf("%s#%s", corpId, corpSecret)
if token, found := wecomAppAccessTokenCache.Get(key); found {
return token, nil
}
accessTokenURL := fmt.Sprintf("%s/cgi-bin/gettoken?corpid=%s&corpsecret=%s", origin, corpId, corpSecret)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, accessTokenURL, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var tokenResp struct {
AccessToken string `json:"access_token"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
ExpiresIn int64 `json:"expires_in"` //Second
}
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", err
}
if tokenResp.ErrCode != 0 {
return "", errors.New(tokenResp.ErrMsg)
}
// 提前两分钟过期
token := tokenResp.AccessToken
expires := time.Duration(tokenResp.ExpiresIn)*time.Second - 2*time.Minute
wecomAppAccessTokenCache.Set(key, token, expires)
return token, nil
}
// sendWeComApp 发送企业应用微信通知
func (n *Notifier) sendWeComApp(ctx context.Context, origin, corpId, corpSecret string, agentId int, toUser string, message string) error {
token, err := n.getWecomAppToken(ctx, origin, corpId, corpSecret)
if err != nil {
return fmt.Errorf("获取企业微信应用ACCESS_TOKEN失败:%s", err)
}
webhook := fmt.Sprintf("%s/cgi-bin/message/send?access_token=%s", origin, token)
body := map[string]interface{}{
"touser": toUser,
"msgtype": "text",
"agentid": agentId,
"text": map[string]string{
"content": message,
},
"safe": 0,
}
result, err := n.sendJSONRequest(ctx, webhook, body)
if err != nil {
return err
}
var sendRespBody struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(result, &sendRespBody); err != nil {
return err
}
if sendRespBody.ErrCode != 0 {
return fmt.Errorf("%s", sendRespBody.ErrMsg)
}
return nil
}
// sendFeishu 发送飞书通知
func (n *Notifier) sendFeishu(ctx context.Context, webhook, message string) error {
body := map[string]interface{}{
@@ -651,6 +732,36 @@ func (n *Notifier) sendWeComByConfig(ctx context.Context, config map[string]inte
return n.sendWeCom(ctx, webhook, message)
}
// sendWeComAppByConfig 根据配置发送企业微信应用通知
func (n *Notifier) sendWeComAppByConfig(ctx context.Context, config map[string]interface{}, message string) error {
origin := "https://qyapi.weixin.qq.com"
if v, ok := config["origin"].(string); ok && v != "" {
origin = v
}
toUser := "@all"
if v, ok := config["toUser"].(string); ok && v != "" {
toUser = v
}
corpId, ok := config["corpId"].(string)
if !ok || corpId == "" {
return fmt.Errorf("企业微信应用配置缺少 corpid")
}
corpSecret, _ := config["corpSecret"].(string)
if !ok || corpSecret == "" {
return fmt.Errorf("企业微信应用配置缺少 corpsecret")
}
agentIdf, ok := config["agentId"].(float64)
if !ok || agentIdf <= 0 {
return fmt.Errorf("企业微信应用配置缺少 agentid")
}
return n.sendWeComApp(ctx, origin, corpId, corpSecret, int(agentIdf), toUser, message)
}
// sendFeishuByConfig 根据配置发送飞书通知
func (n *Notifier) sendFeishuByConfig(ctx context.Context, config map[string]interface{}, message string) error {
secretKey, ok := config["secretKey"].(string)
@@ -748,6 +859,8 @@ func (n *Notifier) SendNotificationByConfig(ctx context.Context, channelConfig *
return n.sendDingTalkByConfig(ctx, channelConfig.Config, message)
case "wecom":
return n.sendWeComByConfig(ctx, channelConfig.Config, message)
case "wecomApp":
return n.sendWeComAppByConfig(ctx, channelConfig.Config, message)
case "feishu":
return n.sendFeishuByConfig(ctx, channelConfig.Config, message)
case "telegram":
@@ -792,6 +905,11 @@ func (n *Notifier) SendWeComByConfig(ctx context.Context, config map[string]inte
return n.sendWeComByConfig(ctx, config, message)
}
// SendWeComAppByConfig 导出方法供外部调用
func (n *Notifier) SendWeComAppByConfig(ctx context.Context, config map[string]interface{}, message string) error {
return n.sendWeComAppByConfig(ctx, config, message)
}
// SendFeishuByConfig 导出方法供外部调用
func (n *Notifier) SendFeishuByConfig(ctx context.Context, config map[string]interface{}, message string) error {
return n.sendFeishuByConfig(ctx, config, message)
@@ -835,6 +953,8 @@ func (n *Notifier) SendTestNotification(ctx context.Context, channelType string,
return n.sendDingTalkByConfig(ctx, config, message)
case "wecom":
return n.sendWeComByConfig(ctx, config, message)
case "wecomApp":
return n.sendWeComAppByConfig(ctx, config, message)
case "feishu":
return n.sendFeishuByConfig(ctx, config, message)
case "telegram":
+1 -1
View File
@@ -54,7 +54,7 @@ const PROPERTY_ID_NOTIFICATION_CHANNELS = 'notification_channels';
// 通知渠道配置(通过 type 标识,不再使用独立ID)
export interface NotificationChannel {
type: 'dingtalk' | 'wecom' | 'feishu' | 'email' | 'webhook' | 'telegram'; // 渠道类型,作为唯一标识
type: 'dingtalk' | 'wecom' | 'wecomApp' | 'feishu' | 'email' | 'webhook' | 'telegram'; // 渠道类型,作为唯一标识
enabled: boolean; // 是否启用
config: Record<string, any>; // JSON配置,根据type不同而不同
}
+110 -1
View File
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { App, Button, Card, Collapse, Form, Input, Select, Space, Spin, Switch } from 'antd';
import { App, Button, Card, Collapse, Form, Input, InputNumber, Select, Space, Spin, Switch } from 'antd';
import { TestTube } from 'lucide-react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
@@ -18,6 +18,7 @@ const NotificationChannels = () => {
// 监听各个通知渠道的启用状态
const dingtalkEnabled = Form.useWatch('dingtalkEnabled', form);
const wecomEnabled = Form.useWatch('wecomEnabled', form);
const wecomAppEnabled = Form.useWatch('wecomAppEnabled', form);
const feishuEnabled = Form.useWatch('feishuEnabled', form);
const telegramEnabled = Form.useWatch('telegramEnabled', form);
const emailEnabled = Form.useWatch('emailEnabled', form);
@@ -65,6 +66,13 @@ const NotificationChannels = () => {
} else if (channel.type === 'wecom') {
formValues.wecomEnabled = channel.enabled;
formValues.wecomSecretKey = channel.config?.secretKey || '';
} else if (channel.type === 'wecomApp') {
formValues.wecomAppEnabled = channel.enabled;
formValues.wecomAppOrigin = channel.config?.origin || 'https://qyapi.weixin.qq.com';
formValues.wecomAppCorpId = channel.config?.corpId || '';
formValues.wecomAppCorpSecret = channel.config?.corpSecret || '';
formValues.wecomAppAgentId = channel.config?.agentId;
formValues.wecomAppToUser = channel.config?.toUser || '@all';
} else if (channel.type === 'feishu') {
formValues.feishuEnabled = channel.enabled;
formValues.feishuSecretKey = channel.config?.secretKey || '';
@@ -130,6 +138,21 @@ const NotificationChannels = () => {
});
}
// 企业微信应用
if (values.wecomAppEnabled || values.wecomAppOrigin) {
newChannels.push({
type: 'wecomApp',
enabled: values.wecomAppEnabled || false,
config: {
origin: values.wecomAppOrigin || '',
corpId: values.wecomAppCorpId || '',
corpSecret: values.wecomAppCorpSecret || '',
agentId: values.wecomAppAgentId,
toUser: values.wecomAppToUser || '',
},
});
}
// 飞书
if (values.feishuEnabled || values.feishuSecretKey) {
newChannels.push({
@@ -333,6 +356,92 @@ const NotificationChannels = () => {
</Form.Item>
</Card>
{/* 企业微信应用通知 */}
<Card
title={
<div className={'flex items-center gap-2'}>
<div>企业微信应用通知</div>
<div className={'text-xs font-normal'}>
了解更多:<a href="https://developer.work.weixin.qq.com/document/path/90236"
target="_blank"
rel="noopener noreferrer">https://developer.work.weixin.qq.com/document/path/90236</a>
</div>
</div>
}
type="inner"
className="mb-4"
extra={
<Button
type="link"
size="small"
icon={<TestTube size={14} />}
onClick={() => handleTest('wecomApp')}
loading={testMutation.isPending}
disabled={!wecomAppEnabled}
>
测试
</Button>
}
>
<Form.Item label="启用企业微信应用通知" name="wecomAppEnabled" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.wecomAppEnabled !== currentValues.wecomAppEnabled}
>
{({ getFieldValue }) =>
getFieldValue('wecomAppEnabled') ? (
<>
<Form.Item
label="origin"
name="wecomAppOrigin"
initialValue="https://qyapi.weixin.qq.com"
rules={[{ required: true, message: '请输入企业微信应用origin' }]}
tooltip="企业微信应用origin, Pika部署在可信IP的服务器下保持默认即可"
>
<Input placeholder="https://qyapi.weixin.qq.com" />
</Form.Item>
<Form.Item
label="corpid"
name="wecomAppCorpId"
rules={[{ required: true, message: '请输入企业微信的corpid' }]}
tooltip="企业微信的corpid"
>
<Input placeholder="输入您的企业的corpid" />
</Form.Item>
<Form.Item
label="corpsecret"
name="wecomAppCorpSecret"
rules={[{ required: true, message: '请输入企业微信应用的corpsecret' }]}
tooltip="企业微信应用的corpsecret"
>
<Input.Password placeholder="输入您的企业应用的corpsecret" />
</Form.Item>
<Form.Item
label="agentid"
name="wecomAppAgentId"
rules={[{ required: true, message: '请输入企业微信应用的agentid' }]}
tooltip="企业微信应用的agentid"
>
<InputNumber style={{ width: '100%' }} placeholder="输入您的企业应用的agentid" />
</Form.Item>
<Form.Item
label="touser"
name="wecomAppToUser"
initialValue="@all"
rules={[{ required: true, message: '请输入接收消息的用户' }]}
tooltip="接收告警消息的用户"
>
<Input placeholder="输入接收告警消息的用户,全部可填@all" />
</Form.Item>
</>
) : null
}
</Form.Item>
</Card>
{/* 飞书通知 */}
<Card
title={