通知渠道增加企业微信应用
This commit is contained in:
1 parent
dbdafd8774
commit
d519cfd16e
3 files changed
+231
-2
No files matched your search
@@ -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":
|
||||
|
||||
@@ -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不同而不同
|
||||
}
|
||||
|
||||
@@ -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={
|
||||
|
||||
Reference in new issue
Block a user