feat: add send many account mode

This commit is contained in:
engigu
2025-12-14 12:43:09 +08:00
parent 2125ddc853
commit 5e20533aa9
26 changed files with 1547 additions and 793 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"kiroAgent.configureMCP": "Disabled"
}
+1 -2
View File
@@ -6,8 +6,6 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化
如果你有很多消息推送方式,每次都需要调用各种接口去发送消息到各个渠道,或者不同的项目你都需要复制同样的发消息代码,这个项目可以帮你管理各种消息方式,并提供统一的发送api接入。你可以自由组合各种消息渠道,一个api推送到各种渠道,帮你省去接入的繁琐步骤。
这个项目适合推送固定接受人(或者固定多个接收渠道)的场景,不适合批量群发场景。虽然也能支持群发,但是群发本身也有一定风险,不是维护的主要场景,群发的反馈不会处理。
演示站点(演示站点的服务器比较烂,见谅) [demo](https://message-nest-demo-site.qwapi.eu.org/)
[![Hits](https://hits.sh/github.com/engigu/Message-Push-Nest.svg?view=today-total)](https://hits.sh/github.com/engigu/Message-Push-Nest/)
@@ -17,6 +15,7 @@ Message Nest 是一个灵活而强大的消息推送整合平台,旨在简化
- 🔄 **整合性:** 提供了多种消息推送方式,包括邮件、钉钉、企业微信等,方便你集中管理和定制通知。
- 🎨 **自定义性:** 可以根据需求定制消息推送策略,满足不同场景的个性化需求。
- 📝 **模板化(⭐推荐):** 支持消息模板功能,通过占位符实现动态内容替换,一次定义多处复用,大幅提高开发效率和维护便利性。
- 📧 **群发模式(🆕):** 支持动态接收者功能,可在 API 调用时指定多个接收账号,实现邮
- 🛠 **开放性:** 易于扩展和集成新的消息通知服务,以适应未来的变化。
## 进度 🔨
+51 -8
View File
@@ -51,6 +51,33 @@ POST /api/v1/message/send
@提醒功能仅在支持的渠道(钉钉、企业微信)中生效。
:::
### 动态接收者参数(可选)🆕
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| recipients | array | 否 | 动态接收者列表,如 `["user1@example.com", "user2@example.com"]` |
::: tip 群发模式
动态接收者功能允许在 API 调用时指定接收者列表,实现群发功能。
**支持的渠道:**
-**邮件** - 支持多个收件人,实现邮件群发
-**微信公众号** - 支持多个 OpenID,实现公众号群发
**使用条件:**
1. 任务实例必须配置为"动态接收者模式"
2. 一个任务只能配置一个动态接收实例
3. 动态接收实例不能与固定接收实例混合使用
**配置方式:**
在任务编辑页面,添加实例时勾选"动态接收者模式",此时无需配置固定接收者。
**注意事项:**
- 如果任务配置了动态接收实例,`recipients` 参数为必填
- 不支持动态接收的渠道会忽略此参数
- 建议控制接收者数量,避免触发渠道限流
:::
## 请求示例
### 基本示例(纯文本)
@@ -88,6 +115,22 @@ POST /api/v1/message/send
}
```
### 动态接收者示例(群发)🆕
```json
{
"token": "a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e",
"title": "系统维护通知",
"text": "系统将于今晚22:00进行维护,预计持续2小时。",
"html": "<h2>系统维护通知</h2><p>系统将于今晚<strong>22:00</strong>进行维护,预计持续<strong>2小时</strong>。</p>",
"recipients": [
"user1@example.com",
"user2@example.com",
"user3@example.com"
]
}
```
### cURL 示例
```bash
@@ -258,14 +301,14 @@ V1 API 支持三种消息格式,系统会根据任务实例配置的格式类
## 支持的推送渠道
| 渠道 | 支持格式 | @提醒 | 说明 |
|------|---------|------|------|
| **邮件** | Text, HTML | ❌ | SMTP邮件发送,推荐使用 HTML 格式 |
| **钉钉** | Text, Markdown | ✅ | 钉钉机器人,支持 Markdown 富文本 |
| **企业微信** | Text, Markdown | ✅ | 企业微信机器人,支持 Markdown |
| **微信公众号** | Text | ❌ | 微信测试公众号模板消息 |
| **自定义Webhook** | Text, HTML, Markdown | ❌ | 自定义HTTP请求,格式取决于配置 |
| **自托管消息** | Text, HTML, Markdown | ❌ | 站内消息,支持多种格式 |
| 渠道 | 支持格式 | @提醒 | 动态接收者 | 说明 |
|------|---------|------|-----------|------|
| **邮件** | Text, HTML | ❌ | ✅ | SMTP邮件发送,支持群发多个收件人 |
| **钉钉** | Text, Markdown | ✅ | ❌ | 钉钉机器人,支持 Markdown 富文本 |
| **企业微信** | Text, Markdown | ✅ | ❌ | 企业微信机器人,支持 Markdown |
| **微信公众号** | Text | ❌ | ✅ | 微信测试公众号,支持多个 OpenID 群发 |
| **自定义Webhook** | Text, HTML, Markdown | ❌ | ❌ | 自定义HTTP请求,格式取决于配置 |
| **自托管消息** | Text, HTML, Markdown | ❌ | ❌ | 站内消息,支持多种格式 |
## 使用流程
+46
View File
@@ -38,6 +38,7 @@ POST /api/v2/message/send
| token | string | 是 | 加密的模板 Token |
| title | string | 是 | 消息标题 |
| placeholders | object | 否 | 占位符键值对 |
| recipients | array | 条件必填 | 动态接收者列表(群发模式)🆕 |
### 参数说明
@@ -59,6 +60,19 @@ POST /api/v2/message/send
- 如果模板中定义了占位符但未传递,将使用默认值
- 如果既未传递也无默认值,占位符将保持原样
#### recipients 🆕
- 动态接收者列表,用于群发场景
- 格式为字符串数组:`["user1@example.com", "user2@example.com"]`
- **条件必填**:如果模板配置了动态接收实例,此参数为必填
- **支持的渠道**
- ✅ 邮件 - 多个收件人邮箱地址
- ✅ 微信公众号 - 多个用户 OpenID
- **使用限制**
- 一个模板只能配置一个动态接收实例
- 动态接收实例不能与固定接收实例混合使用
- 建议控制接收者数量,避免触发渠道限流
## 请求示例
### 基本示例
@@ -165,6 +179,38 @@ axios.post(url, data)
});
```
### 动态接收者示例(群发)🆕
```javascript
const axios = require('axios');
const url = 'http://your-domain/api/v2/message/send';
const data = {
token: 'a3541c2f0d3e1b4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b',
title: '活动通知',
placeholders: {
activity_name: '双十二大促',
start_time: '2024-12-12 00:00:00',
discount: '全场8折'
},
recipients: [
'user1@example.com',
'user2@example.com',
'user3@example.com'
]
};
axios.post(url, data)
.then(response => {
console.log(response.data);
console.log(`成功发送给 ${response.data.data.count} 个接收者`);
})
.catch(error => {
console.error('Error:', error);
});
```
### Java 示例
```java
+54 -2
View File
@@ -53,14 +53,30 @@
#### 邮件渠道
- **收件人邮箱** - 接收邮件的邮箱地址
- **消息格式** - Text 或 HTML
- **固定模式**
- 收件人邮箱 - 接收邮件的邮箱地址
- 消息格式 - Text 或 HTML
- **动态接收者模式(群发)** 🆕:
- 勾选"动态接收者模式"
- 无需配置固定收件人
- 发送时通过 API 的 `recipients` 参数指定多个收件人
- 适用于邮件群发场景
#### 钉钉/企业微信
- **消息格式** - Text 或 Markdown
- **@提醒** - 可选配置@手机号或@所有人
#### 微信公众号
- **固定模式**
- 用户 OpenID - 接收消息的用户标识
- **动态接收者模式(群发)** 🆕:
- 勾选"动态接收者模式"
- 无需配置固定 OpenID
- 发送时通过 API 的 `recipients` 参数指定多个 OpenID
- 适用于公众号群发场景
#### 自定义 Webhook
- 根据 Webhook 要求配置相应参数
@@ -70,6 +86,13 @@
- 无需额外配置
- **消息格式** - Text、HTML 或 Markdown
::: warning 动态接收者模式限制
1. 一个任务只能配置一个动态接收实例
2. 动态接收实例不能与固定接收实例混合使用
3. 如果配置了动态接收实例,API 调用时 `recipients` 参数为必填
4. 建议控制接收者数量,避免触发渠道限流
:::
## 管理任务
### 查看任务列表
@@ -190,6 +213,35 @@ curl -X POST http://your-domain/api/v1/message/send \
}
```
### 动态接收者功能(群发)🆕
对于支持的渠道(邮件、微信公众号),可以使用动态接收者实现群发:
```json
{
"token": "your_task_token",
"title": "系统维护通知",
"text": "系统将于今晚22:00进行维护",
"html": "<h2>系统维护通知</h2><p>系统将于今晚<strong>22:00</strong>进行维护</p>",
"recipients": [
"user1@example.com",
"user2@example.com",
"user3@example.com"
]
}
```
**使用场景:**
- 邮件群发通知
- 微信公众号批量推送
- 营销活动通知
- 系统公告发布
**注意事项:**
- 任务必须配置为动态接收者模式
- 一个任务只能有一个动态接收实例
- 建议控制接收者数量,避免触发限流
## 工作流程
```mermaid
+41
View File
@@ -169,15 +169,36 @@ Markdown 格式,适用于钉钉、企业微信等支持 Markdown 的渠道:
不同渠道需要配置不同的参数:
**邮件渠道:**
- **固定模式**
- 收件人邮箱地址
- 消息格式:Text 或 HTML
- **动态接收者模式(群发)** 🆕:
- 勾选"动态接收者模式"
- 无需配置固定收件人
- 发送时通过 API 的 `recipients` 参数指定多个收件人
- 适用于邮件群发场景
**钉钉/企业微信:**
- 消息格式:Text 或 Markdown
**微信公众号:**
- **固定模式**
- 用户 OpenID
- **动态接收者模式(群发)** 🆕:
- 勾选"动态接收者模式"
- 无需配置固定 OpenID
- 发送时通过 API 的 `recipients` 参数指定多个 OpenID
**自定义 Webhook**
- 根据 Webhook 要求配置
::: warning 动态接收者模式限制
1. 一个模板只能配置一个动态接收实例
2. 动态接收实例不能与固定接收实例混合使用
3. 如果配置了动态接收实例,API 调用时 `recipients` 参数为必填
4. 建议控制接收者数量,避免触发渠道限流
:::
### 3. 管理实例
- **启用/禁用** - 控制实例是否参与发送
@@ -220,6 +241,26 @@ curl -X POST http://your-domain/api/v2/message/send \
}'
```
**动态接收者示例(群发)** 🆕:
```bash
curl -X POST http://your-domain/api/v2/message/send \
-H "Content-Type: application/json" \
-d '{
"token": "encrypted_template_token",
"title": "活动通知",
"placeholders": {
"activity_name": "双十二大促",
"discount": "全场8折"
},
"recipients": [
"user1@example.com",
"user2@example.com",
"user3@example.com"
]
}'
```
## 最佳实践
### 1. 占位符命名
+14 -4
View File
@@ -3,6 +3,10 @@ module message-nest
go 1.20
require (
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10
github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.0
github.com/alibabacloud-go/tea v1.2.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.6
github.com/engigu/logrus-prefixed-formatter v0.5.3-0.20240113025353-498bcddfa6b5
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.11.0
@@ -22,11 +26,19 @@ require (
)
require (
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.3.1 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
@@ -42,7 +54,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -55,11 +66,11 @@ require (
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/tidwall/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
@@ -74,8 +85,7 @@ require (
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/ini.v1 v1.47.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
+160 -10
View File
@@ -1,13 +1,63 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.0 h1:DRd/m0TAVzmVoacUv4+C32HrpoehAPxe7Yi8jBwW5HY=
github.com/alibabacloud-go/dysmsapi-20170525/v4 v4.1.0/go.mod h1:XA0GIZV/FIFTuasWP6WlGolkSfH/t8J5eqxT2dHAFbE=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLiSke5RsN5LcyQ0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M=
github.com/alicebob/miniredis/v2 v2.30.0/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.3.10 h1:45Xxrae/evfzQL9V10zL3xX31eqgLWEaIdCoPipOEQA=
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -20,6 +70,10 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -29,6 +83,9 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/engigu/logrus-prefixed-formatter v0.5.3-0.20240113025353-498bcddfa6b5 h1:XRkGO/dTf/3p1SDYr/BNtDmqwLtI+jssymsCgzvqf5c=
github.com/engigu/logrus-prefixed-formatter v0.5.3-0.20240113025353-498bcddfa6b5/go.mod h1:wpl/H0ZIheoONzhaJIPU5Qe+lmLxPXEdY1PNWjla63E=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -64,7 +121,11 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -73,6 +134,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -85,8 +147,9 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -95,6 +158,7 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@@ -104,8 +168,6 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -123,9 +185,13 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyex
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@@ -147,6 +213,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -163,14 +230,16 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -189,6 +258,9 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -201,7 +273,10 @@ github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -211,24 +286,60 @@ golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -239,6 +350,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -246,30 +358,64 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -283,19 +429,21 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/ini.v1 v1.47.0 h1:XAVsOWcIxjm6JVEbCbSZgSBZIF0BrCzXs4orAQr6uc8=
gopkg.in/ini.v1 v1.47.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -306,6 +454,8 @@ gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkD
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+6
View File
@@ -41,6 +41,12 @@ type InsCustomConfig struct {
type InsMessageNestConfig struct {
}
// InsAliyunSMSConfig 实例里面的阿里云短信config
type InsAliyunSMSConfig struct {
PhoneNumber string `json:"phone_number" validate:"required" label:"手机号码"`
TemplateCode string `json:"template_code" validate:"required" label:"短信模板CODE"`
}
// ManyAddTaskIns 批量添加实例
func ManyAddTaskIns(taskIns []SendTasksIns) error {
tx := db.Begin()
+4
View File
@@ -28,6 +28,9 @@ type SendMessageReq struct {
AtMobiles []string `json:"at_mobiles" label:"@的手机号列表"`
AtUserIds []string `json:"at_user_ids" label:"@的用户ID列表"`
AtAll bool `json:"at_all" label:"是否@所有人"`
// 动态接收者(用于邮箱、微信公众号等支持群发的渠道)
Recipients []string `json:"recipients" label:"接收者列表"`
}
// DoSendMassage 外部调用发信接口
@@ -72,6 +75,7 @@ func DoSendMassage(c *gin.Context) {
AtMobiles: req.AtMobiles,
AtUserIds: req.AtUserIds,
AtAll: req.AtAll,
Recipients: req.Recipients,
DefaultLogger: logrus.WithFields(logrus.Fields{
"prefix": "[Send Instance]",
}),
+37
View File
@@ -28,6 +28,20 @@ func (sw *SendTaskInsService) ValidateDiffIns(ins models.SendTasksIns) (string,
if err != nil {
return "邮箱auth反序列化失败!", empty
}
// 检查是否为动态接收者模式
var configMap map[string]interface{}
json.Unmarshal([]byte(ins.Config), &configMap)
allowMultiRecip, exists := configMap["allowMultiRecip"].(bool)
// allowMultiRecip=true为动态模式,to_account可以为空
// 历史数据(不存在allowMultiRecip字段)默认为固定模式
if exists && allowMultiRecip && emailConfig.ToAccount == "" {
// 动态模式,不验证to_account
return "", emailConfig
}
// 固定模式或有to_account时,进行常规验证
_, Msg := app.CommonPlaygroundValid(emailConfig)
return Msg, emailConfig
}
@@ -58,6 +72,29 @@ func (sw *SendTaskInsService) ValidateDiffIns(ins models.SendTasksIns) (string,
if err != nil {
return "微信公众号发送配置反序列化失败!", empty
}
// 检查是否为动态接收者模式
var configMap map[string]interface{}
json.Unmarshal([]byte(ins.Config), &configMap)
allowMultiRecip, exists := configMap["allowMultiRecip"].(bool)
// allowMultiRecip=true为动态模式,to_account可以为空
// 历史数据(不存在allowMultiRecip字段)默认为固定模式
if exists && allowMultiRecip && Config.ToAccount == "" {
// 动态模式,不验证to_account
return "", Config
}
// 固定模式或有to_account时,进行常规验证
_, Msg := app.CommonPlaygroundValid(Config)
return Msg, Config
}
if ins.WayType == "AliyunSMS" {
var Config models.InsAliyunSMSConfig
err := json.Unmarshal([]byte(ins.Config), &Config)
if err != nil {
return "阿里云短信配置反序列化失败!", empty
}
_, Msg := app.CommonPlaygroundValid(Config)
return Msg, Config
}
+86 -1
View File
@@ -1,6 +1,7 @@
package send_message_service
import (
"encoding/json"
"errors"
"fmt"
"message-nest/models"
@@ -48,6 +49,9 @@ type SendMessageService struct {
AtUserIds []string
AtAll bool
// 动态接收者(用于邮箱、微信公众号等支持群发的渠道)
Recipients []string
Status int
LogOutput []string
@@ -253,13 +257,36 @@ func (sm *SendMessageService) Send(task models.TaskIns) (string, error) {
}
}
// 使用 SendUnified 方法(自动格式转换和@功能支持
// 处理动态接收者(邮箱、微信公众号等支持群发的渠道
isDynamicMode := sm.isDynamicRecipientMode(ins.SendTasksIns)
if isDynamicMode && sm.supportsDynamicRecipient(way.Type) && len(sm.Recipients) > 0 {
// 动态接收模式:使用API传入的Recipients列表(群发)
sm.LogsAndStatusMark(fmt.Sprintf("动态接收模式,共 %d 个接收者", len(sm.Recipients)), sm.Status)
for recipientIdx, recipient := range sm.Recipients {
sm.LogsAndStatusMark(fmt.Sprintf(">>> 接收者 %d/%d: %s", recipientIdx+1, len(sm.Recipients), recipient), sm.Status)
// 临时修改实例配置中的接收者
modifiedIns := sm.modifyInsRecipient(ins.SendTasksIns, recipient, way.Type)
// 使用 SendUnified 方法发送
res, errMsg := channel.SendUnified(msgObj, modifiedIns, unifiedContent)
if res != "" {
sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s", res), sm.Status)
} else {
sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
}
}
} else {
// 固定接收模式:使用实例配置的to_account
sm.LogsAndStatusMark("固定接收模式,使用实例配置的接收者", sm.Status)
res, errMsg := channel.SendUnified(msgObj, ins.SendTasksIns, unifiedContent)
if res != "" {
sm.LogsAndStatusMark(fmt.Sprintf("返回内容:%s\n", res), sm.Status)
} else {
sm.LogsAndStatusMark(sm.TransError(errMsg), errStrIsSuccess(errMsg))
}
}
}
@@ -366,6 +393,64 @@ func (sm *SendMessageService) BuildTemplateContent(ins models.SendTasksIns) *uni
return content
}
// supportsDynamicRecipient 判断渠道是否支持动态接收者
func (sm *SendMessageService) supportsDynamicRecipient(wayType string) bool {
// 支持动态接收者的渠道类型
supportedTypes := map[string]bool{
unified.MessageTypeEmail: true,
unified.MessageTypeWeChatOFAccount: true,
// 可以继续添加其他支持动态接收者的渠道
}
return supportedTypes[wayType]
}
// isDynamicRecipientMode 判断实例是否配置为动态接收模式
func (sm *SendMessageService) isDynamicRecipientMode(ins models.SendTasksIns) bool {
// 解析实例配置
var config map[string]interface{}
if err := json.Unmarshal([]byte(ins.Config), &config); err != nil {
return false
}
// 检查allowMultiRecip字段
// allowMultiRecip=true: 动态模式
// allowMultiRecip=false或不存在: 固定模式
if allowMultiRecip, ok := config["allowMultiRecip"]; ok {
if allow, ok := allowMultiRecip.(bool); ok {
return allow
}
}
// 默认为固定模式(兼容历史数据)
return false
}
// modifyInsRecipient 临时修改实例配置中的接收者
func (sm *SendMessageService) modifyInsRecipient(ins models.SendTasksIns, recipient string, wayType string) models.SendTasksIns {
// 创建副本
modifiedIns := ins
// 根据渠道类型修改配置
var config map[string]interface{}
if err := json.Unmarshal([]byte(ins.Config), &config); err != nil {
sm.LogsAndStatusMark(fmt.Sprintf("解析实例配置失败: %s", err.Error()), SendFail)
return ins
}
// 修改接收者字段
config["to_account"] = recipient
// 序列化回JSON
modifiedConfigBytes, err := json.Marshal(config)
if err != nil {
sm.LogsAndStatusMark(fmt.Sprintf("序列化实例配置失败: %s", err.Error()), SendFail)
return ins
}
modifiedIns.Config = string(modifiedConfigBytes)
return modifiedIns
}
// GetSendMsg 获取对应消息内容(任务模式使用)
// 先根据实例设置的类型取,取不到或者取到的是空,则使用text发送
func (sm *SendMessageService) GetSendMsg(ins models.SendTasksIns) (string, string) {
@@ -24,6 +24,7 @@ const (
MessageTypeCustom = channels.MessageTypeCustom
MessageTypeWeChatOFAccount = channels.MessageTypeWeChatOFAccount
MessageTypeMessageNest = channels.MessageTypeMessageNest
MessageTypeAliyunSMS = channels.MessageTypeAliyunSMS
)
// ChannelRegistry 渠道注册表
@@ -95,6 +96,7 @@ func GetGlobalChannelRegistry() *ChannelRegistry {
globalChannelRegistry.Register(channels.NewCustomChannel())
globalChannelRegistry.Register(channels.NewWeChatOFAccountChannel())
globalChannelRegistry.Register(channels.NewMessageNestChannel())
globalChannelRegistry.Register(channels.NewAliyunSMSChannel())
})
return globalChannelRegistry
}
@@ -0,0 +1,106 @@
package channels
import (
"encoding/json"
"fmt"
"message-nest/models"
"message-nest/service/send_ins_service"
"message-nest/service/send_way_service"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dysmsapi "github.com/alibabacloud-go/dysmsapi-20170525/v4/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type AliyunSMSChannel struct{ *BaseChannel }
func NewAliyunSMSChannel() *AliyunSMSChannel {
return &AliyunSMSChannel{BaseChannel: NewBaseChannel(MessageTypeAliyunSMS, []string{FormatTypeText})}
}
func (c *AliyunSMSChannel) SendUnified(msgObj interface{}, ins models.SendTasksIns, content *UnifiedMessageContent) (string, string) {
auth, ok := msgObj.(send_way_service.WayDetailAliyunSMS)
if !ok {
return "", "类型转换失败"
}
insService := send_ins_service.SendTaskInsService{}
errStr, configInterface := insService.ValidateDiffIns(ins)
if errStr != "" {
return "", errStr
}
config, ok := configInterface.(models.InsAliyunSMSConfig)
if !ok {
return "", "阿里云短信config校验失败"
}
// 格式化内容
_, formattedContent, err := c.FormatContent(content)
if err != nil {
return "", err.Error()
}
// 创建阿里云短信客户端
client, err := c.createClient(auth.AccessKeyId, auth.AccessKeySecret)
if err != nil {
return "", fmt.Sprintf("创建阿里云短信客户端失败: %s", err.Error())
}
// 准备模板参数
templateParam := map[string]interface{}{
"content": formattedContent,
}
if content.Extra != nil {
for k, v := range content.Extra {
templateParam[k] = v
}
}
templateParamJSON, _ := json.Marshal(templateParam)
// 发送短信
sendSmsRequest := &dysmsapi.SendSmsRequest{
PhoneNumbers: tea.String(config.PhoneNumber),
SignName: tea.String(auth.SignName),
TemplateCode: tea.String(config.TemplateCode),
TemplateParam: tea.String(string(templateParamJSON)),
}
runtime := &util.RuntimeOptions{}
tryErr := func() error {
defer func() {
if r := tea.Recover(recover()); r != nil {
// 处理panic
}
}()
response, _err := client.SendSmsWithOptions(sendSmsRequest, runtime)
if _err != nil {
return _err
}
if response.Body.Code != nil && *response.Body.Code != "OK" {
return fmt.Errorf("发送失败: %s - %s", tea.StringValue(response.Body.Code), tea.StringValue(response.Body.Message))
}
return nil
}()
if tryErr != nil {
return "", tryErr.Error()
}
return "", ""
}
// createClient 创建阿里云短信客户端
func (c *AliyunSMSChannel) createClient(accessKeyId, accessKeySecret string) (*dysmsapi.Client, error) {
config := &openapi.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("dysmsapi.aliyuncs.com"),
}
return dysmsapi.NewClient(config)
}
@@ -15,6 +15,7 @@ const (
MessageTypeCustom = "Custom"
MessageTypeWeChatOFAccount = "WeChatOFAccount"
MessageTypeMessageNest = "MessageNest"
MessageTypeAliyunSMS = "AliyunSMS"
)
// UnifiedMessageContent 统一的消息内容结构
+19
View File
@@ -56,6 +56,13 @@ type WeChatOFAccount struct {
TempID string `json:"tempid" validate:"max=2000" label:"模板消息id"`
}
// WayDetailAliyunSMS 阿里云短信渠道明细字段
type WayDetailAliyunSMS struct {
AccessKeyId string `json:"access_key_id" validate:"required,max=100" label:"AccessKeyId"`
AccessKeySecret string `json:"access_key_secret" validate:"required,max=100" label:"AccessKeySecret"`
SignName string `json:"sign_name" validate:"required,max=50" label:"短信签名"`
}
// MessageNest 自托管消息
type MessageNest struct {
}
@@ -182,6 +189,14 @@ func (sw *SendWay) ValidateDiffWay() (string, interface{}) {
}
_, Msg := app.CommonPlaygroundValid(wca)
return Msg, wca
} else if sw.Type == "AliyunSMS" {
var aliyunSMS WayDetailAliyunSMS
err := json.Unmarshal([]byte(sw.Auth), &aliyunSMS)
if err != nil {
return "阿里云短信auth反序列化失败!", empty
}
_, Msg := app.CommonPlaygroundValid(aliyunSMS)
return Msg, aliyunSMS
}
return fmt.Sprintf("未知的发信渠道校验: %s", sw.Type), empty
}
@@ -227,5 +242,9 @@ func (sw *SendWay) TestSendWay(msgObj interface{}) (string, string) {
if ok {
return "自托管消息不用测试运行,请直接添加", ""
}
_, ok = msgObj.(WayDetailAliyunSMS)
if ok {
return "阿里云短信不用测试运行,请直接添加", ""
}
return fmt.Sprintf("未知的发信渠道校验: %s", sw.Type), ""
}
@@ -1,12 +1,13 @@
<script lang="ts">
import { ref, defineComponent } from 'vue'
import { toast } from 'vue-sonner'
import { ref, defineComponent, watch, toRef } from 'vue'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
// @ts-ignore
import { TemplateApiStrGenerate } from '@/util/viewApi'
import { useInstanceData } from '@/composables/useInstanceData'
import { useApiCodeViewer } from '@/composables/useApiCodeViewer'
export default defineComponent({
name: 'TemplateApiViewer',
@@ -39,58 +40,67 @@ export default defineComponent({
emit('update:open', value)
}
// 当前选中的标签
const activeTab = ref('curl')
// 使用实例数据管理 composable
const { hasDynamicRecipientInstance, enabledChannelNames } = useInstanceData(
'template',
toRef(props, 'templateData'),
toRef(props, 'open')
)
// 代码语言选项
const codeLanguages = [
{ value: 'curl', label: 'cURL', icon: '🌐' },
{ value: 'javascript', label: 'JS', icon: '🟨' },
{ value: 'python', label: 'Python', icon: '🐍' },
{ value: 'php', label: 'PHP', icon: '🐘' },
{ value: 'golang', label: 'Go', icon: '🐹' },
{ value: 'java', label: 'Java', icon: '☕' },
{ value: 'rust', label: 'Rust', icon: '🦀' }
]
// 使用 API 代码查看器 composable
const { activeTab, codeLanguages, copyToClipboard } = useApiCodeViewer()
// 可选参数选项
const showRecipients = ref(false)
// 监听动态接收实例变化,自动勾选
watch(hasDynamicRecipientInstance, (newVal) => {
if (newVal) {
showRecipients.value = true
}
})
// 监听弹窗关闭,重置状态
watch(() => props.open, (newVal) => {
if (!newVal) {
showRecipients.value = false
}
})
// 生成API代码示例
const generateApiCode = (language: string) => {
const templateId = props.templateData?.id || 'TEMPLATE_ID'
const placeholders = props.templateData?.placeholders || '[]'
const options = {
recipients: showRecipients.value
}
switch (language) {
case 'curl':
return TemplateApiStrGenerate.getCurlString(templateId, placeholders)
return TemplateApiStrGenerate.getCurlString(templateId, placeholders, options)
case 'javascript':
return TemplateApiStrGenerate.getNodeString(templateId, placeholders)
return TemplateApiStrGenerate.getNodeString(templateId, placeholders, options)
case 'python':
return TemplateApiStrGenerate.getPythonString(templateId, placeholders)
return TemplateApiStrGenerate.getPythonString(templateId, placeholders, options)
case 'php':
return TemplateApiStrGenerate.getPHPString(templateId, placeholders)
return TemplateApiStrGenerate.getPHPString(templateId, placeholders, options)
case 'golang':
return TemplateApiStrGenerate.getGolangString(templateId, placeholders)
return TemplateApiStrGenerate.getGolangString(templateId, placeholders, options)
case 'java':
return TemplateApiStrGenerate.getJavaString(templateId, placeholders)
return TemplateApiStrGenerate.getJavaString(templateId, placeholders, options)
case 'rust':
return TemplateApiStrGenerate.getRustString(templateId, placeholders)
return TemplateApiStrGenerate.getRustString(templateId, placeholders, options)
default:
return '// 请选择一种编程语言查看示例代码'
}
}
// 复制代码到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
toast.success('复制成功')
} catch (err) {
toast.error('复制失败')
}
}
return {
handleUpdateOpen,
activeTab,
hasDynamicRecipientInstance,
enabledChannelNames,
showRecipients,
codeLanguages,
generateApiCode,
copyToClipboard
@@ -123,6 +133,45 @@ export default defineComponent({
<p><strong>可选参数:</strong> 根据模板配置的@提醒字段自动应用</p>
<p class="text-amber-600 dark:text-amber-400"><strong> 注意:</strong> V2接口使用加密token不支持明文ID</p>
</div>
<!-- 已启用的渠道列表 -->
<div v-if="enabledChannelNames.length > 0" class="mt-3 pt-3 border-t">
<p class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">已启用的发送渠道</p>
<div class="flex flex-wrap gap-2">
<Badge
v-for="(name, index) in enabledChannelNames"
:key="index"
variant="secondary"
class="text-xs"
>
{{ name }}
</Badge>
</div>
</div>
<div v-else class="mt-3 pt-3 border-t">
<p class="text-xs text-amber-600 dark:text-amber-400"> 该模板暂无启用的发送渠道</p>
</div>
</div>
<!-- 可选参数 -->
<div v-if="hasDynamicRecipientInstance" class="border rounded-lg p-4 bg-gray-50 dark:bg-slate-800/50">
<h3 class="font-semibold mb-3">可选参数</h3>
<div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 cursor-not-allowed opacity-75">
<input
type="checkbox"
v-model="showRecipients"
disabled
class="rounded cursor-not-allowed"
>
<span class="text-sm">动态接收者</span>
<Badge variant="secondary" class="text-xs">必填</Badge>
</label>
</div>
<div class="space-y-1 text-xs text-gray-500 dark:text-gray-400 mt-3">
<p>📧 动态接收者该模板配置了动态接收实例发送时必须通过API指定接收者列表群发模式</p>
<p class="text-amber-600 dark:text-amber-400"> 此参数已自动勾选且不可取消因为模板已配置动态接收实例</p>
</div>
</div>
<!-- 代码示例 -->
@@ -1,27 +1,6 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Button } from '@/components/ui/button'
import { Badge } from "@/components/ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import EmptyTableState from '@/components/ui/EmptyTableState.vue'
import {
Combobox,
ComboboxAnchor,
ComboboxInput,
ComboboxList,
ComboboxItem,
ComboboxViewport
} from '@/components/ui/combobox'
import { CheckIcon } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Switch } from '@/components/ui/switch'
import { CONSTANT } from '@/constant'
import { toast } from 'vue-sonner'
import { request } from '@/api/api'
import { generateBizUniqueID } from '@/util/uuid'
import InstanceConfig from '@/components/ui/InstanceConfig.vue'
// 组件props
interface Props {
@@ -37,225 +16,6 @@ const props = withDefaults(defineProps<Props>(), {
defineEmits<{
'update:open': [value: boolean]
}>()
// 前端的页面添加配置
const waysConfigMap = CONSTANT.WAYS_DATA
// 搜索相关状态
const searchQuery = ref('')
const isSearching = ref(false)
const channelName = ref('')
// 当前显示的选项(搜索结果或所有选项)
const displayOptions = ref<Array<{ id: string, name: string, type: string }>>([])
// 输入框显示值(只在用户搜索时显示搜索内容)
const inputDisplayValue = computed({
get: () => searchQuery.value,
set: (value: string) => {
searchQuery.value = value
}
})
// 当前选中渠道的配置
const currentChannelConfig = computed(() => {
// 先根据label找到type
let type = displayOptions.value.find(item => item.name === channelName.value)?.type
// 再根据type找到配置
let rs = waysConfigMap.find(item => item.type === type) || null;
return rs;
})
// 表单数据
const formData = ref<Record<string, any>>({})
// 监听渠道变化
const handlechannelNameChange = () => {
// 数据加载后,text/html单选设置默认选中(这里选第一个)
if (currentChannelConfig.value?.taskInsRadios.length > 0) {
formData.value.templ_type = currentChannelConfig.value?.taskInsRadios[0].subLabel
}
}
// 添加单条实例配置
const handleAddSubmit = async () => {
// 验证是否选择了渠道
if (!channelName.value) {
toast.error('请选择发送渠道')
return
}
// 验证内容类型
const contentType = formData.value.templ_type
if (!contentType) {
toast.error('请选择消息格式')
return
}
// 验证模板对应格式的内容是否为空
const templateFieldMap: Record<string, string> = {
'text': 'text_template',
'html': 'html_template',
'markdown': 'markdown_template'
}
const fieldName = templateFieldMap[contentType.toLowerCase()]
if (fieldName) {
const templateContent = props.templateData?.[fieldName] || ''
// 检查是否为空(去除所有空白字符后检查)
if (!templateContent.trim()) {
toast.error(`模板的 ${contentType} 格式内容为空,无法添加此类型的实例`)
return
}
}
// 组建表单数据
let postData = {
"id": generateBizUniqueID('IN'),
"enable": 1,
"template_id": props.templateData.id,
"way_id": displayOptions.value[0]?.id,
"way_type": displayOptions.value[0]?.type,
"way_name": displayOptions.value[0]?.name,
"content_type": formData.value.templ_type,
"config": JSON.stringify(formData.value),
}
try {
const response = await request.post('/templates/ins/addone', postData)
if (response.status === 200 && response.data.code === 200) {
toast.success(response.data.msg)
// 重新加载实例列表
await queryInsListData()
// 清空表单
channelName.value = ''
formData.value = {}
} else {
toast.error(response.data.msg || '添加实例失败')
}
} catch (error: any) {
toast.error(error.response?.data?.msg || '添加实例失败')
}
}
// 实例表格数据
const insTableData = ref<any[]>([])
// 查询实例列表数据
const queryInsListData = async () => {
if (!props.templateData?.id) return
try {
const response = await request.get('/templates/ins/get', {
params: { id: props.templateData.id }
})
if (response.status === 200 && response.data.code === 200) {
const insList = response.data.data.ins_list || []
insTableData.value = insList
}
} catch (error) {
console.error('获取实例列表失败', error)
}
}
// 删除实例
const handleDeleteIns = async (insId: string) => {
try {
const response = await request.post('/sendtasks/ins/delete', { id: insId })
if (response.status === 200 && response.data.code === 200) {
toast.success(response.data.msg)
await queryInsListData()
} else {
toast.error(response.data.msg || '删除失败')
}
} catch (error: any) {
toast.error(error.response?.data?.msg || '删除失败')
}
}
// 切换实例启用状态
const handleToggleEnable = async (insId: string, currentStatus: number | string) => {
const isEnabled = Number(currentStatus) === 1
const newStatus = isEnabled ? 0 : 1
// 立即更新本地状态,提供即时反馈
const insIndex = insTableData.value.findIndex(ins => ins.id === insId)
if (insIndex !== -1) {
insTableData.value[insIndex].enable = newStatus
}
try {
const response = await request.post('/sendtasks/ins/update_enable', {
ins_id: insId,
status: newStatus
})
if (response.status === 200 && response.data.code === 200) {
toast.success(response.data.msg)
// 重新加载确保数据同步
await queryInsListData()
} else {
toast.error(response.data.msg || '更新失败')
// 失败时恢复原状态
if (insIndex !== -1) {
insTableData.value[insIndex].enable = currentStatus
}
}
} catch (error: any) {
console.error('状态切换失败:', error)
toast.error(error.response?.data?.msg || '更新失败')
// 失败时恢复原状态
if (insIndex !== -1) {
insTableData.value[insIndex].enable = currentStatus
}
}
}
// 防抖定时器
let searchTimer: number | null = null
// 搜索渠道(带防抖)
const handleSearch = (query: string) => {
// 清除之前的定时器
if (searchTimer) {
clearTimeout(searchTimer)
}
if (!query.trim()) {
displayOptions.value = []
return
}
// 设置防抖延迟(500ms
searchTimer = window.setTimeout(async () => {
isSearching.value = true
try {
const response = await request.get('/sendways/list', {
params: { name: query }
})
if (response.status === 200 && response.data.code === 200) {
displayOptions.value = response.data.data.lists.map((item: any) => ({
id: item.id,
name: item.name,
type: item.type
}))
}
} catch (error) {
console.error('搜索渠道失败', error)
displayOptions.value = []
} finally {
isSearching.value = false
}
}, 500)
}
// 监听对话框打开状态,打开时加载实例列表
watch(() => props.open, (newVal) => {
if (newVal && props.templateData?.id) {
queryInsListData()
}
}, { immediate: true })
</script>
<template>
@@ -265,120 +25,12 @@ watch(() => props.open, (newVal) => {
<DialogTitle>配置发送实例</DialogTitle>
</DialogHeader>
<div class="px-4 pb-4 flex-1 overflow-y-auto">
<div class="space-y-4">
<!-- 模板信息 -->
<div class="mb-6 p-3 bg-muted rounded-lg space-y-1">
<div class="flex items-baseline gap-2">
<span class="text-base font-semibold">{{ templateData?.name }}</span>
<Badge variant="outline" class="text-xs">{{ templateData?.id }}</Badge>
</div>
<div class="text-xs text-muted-foreground">为此模板配置发送实例</div>
</div>
<!-- 添加实例表单 -->
<div class="space-y-4">
<div class="flex items-end gap-2">
<div class="flex-1 space-y-2">
<Label class="text-sm font-medium">选择发送渠道</Label>
<Combobox v-model="channelName" @update:model-value="handlechannelNameChange">
<ComboboxAnchor class="w-full">
<ComboboxInput v-model="inputDisplayValue" @input="handleSearch(inputDisplayValue)"
class="flex h-10 w-full" placeholder="搜索或选择渠道类型进行实例的添加..." />
</ComboboxAnchor>
<ComboboxList class="w-[var(--reka-combobox-trigger-width)]">
<ComboboxViewport>
<ComboboxItem v-for="option in displayOptions" :key="option.id" :value="option.name">
<div class="flex items-center justify-between w-full">
<span>{{ option.name }}</span>
<CheckIcon v-if="channelName === option.name" class="h-4 w-4" />
</div>
</ComboboxItem>
<div v-if="isSearching" class="p-2 text-sm text-muted-foreground">搜索中...</div>
<div v-if="!isSearching && displayOptions.length === 0 && searchQuery" class="p-2 text-sm text-muted-foreground">
未找到匹配的渠道
</div>
</ComboboxViewport>
</ComboboxList>
</Combobox>
</div>
<Button size="sm" variant="outline" @click="handleAddSubmit">添加实例</Button>
</div>
</div>
<!-- 渠道配置表单 -->
<div v-if="currentChannelConfig" class="mt-4">
<!-- 实例配置输入字段 -->
<div v-if="currentChannelConfig.taskInsInputs && currentChannelConfig.taskInsInputs.length > 0" class="mb-2">
<Label class="text-sm font-medium mb-1">实例配置</Label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="input in currentChannelConfig.taskInsInputs" :key="input.col" class="space-y-2">
<label class="text-xs font-medium text-muted-foreground">{{ input.label || input.desc }}</label>
<Input v-model="formData[input.col]" :placeholder="input.desc || `请输入${input.label}`"
:type="input.type || 'text'" class="w-full" />
</div>
</div>
</div>
<!-- 单选框 -->
<div v-if="currentChannelConfig.taskInsRadios && currentChannelConfig.taskInsRadios.length > 0" class="mt-4">
<Label class="text-sm font-medium mb-2">消息格式</Label>
<RadioGroup v-model="formData.templ_type" class="flex gap-4">
<div v-for="radio in currentChannelConfig.taskInsRadios" :key="radio.subLabel" class="flex items-center space-x-2">
<RadioGroupItem :value="radio.subLabel" :id="radio.subLabel" />
<Label :for="radio.subLabel" class="text-sm cursor-pointer">{{ radio.subLabel }}</Label>
</div>
</RadioGroup>
</div>
</div>
<!-- 关联的实例表 -->
<div class="mt-4">
<h3 class="text-sm font-medium mb-3">已经关联的实例</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>渠道名称</TableHead>
<TableHead>内容类型</TableHead>
<TableHead class="text-center">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="ins in insTableData" :key="ins.id">
<TableCell>
<div class="font-medium">{{ ins.way_name || '未命名' }}</div>
<div class="text-xs text-muted-foreground">{{ ins.way_type }}</div>
</TableCell>
<TableCell>
<Badge variant="secondary">{{ ins.content_type }}</Badge>
</TableCell>
<TableCell class="text-center">
<div class="flex items-center justify-center gap-2">
<Switch
:model-value="ins.enable === 1"
@update:model-value="() => handleToggleEnable(ins.id, ins.enable)"
<div class="flex-1 overflow-y-auto">
<InstanceConfig
type="template"
:data="templateData"
:in-dialog="true"
/>
<Button
size="sm"
variant="outline"
class="text-red-500 border-red-300 hover:bg-red-50 hover:border-red-400 hover:text-red-600 hover:shadow-md transition-all duration-200"
@click="handleDeleteIns(ins.id)"
>
删除
</Button>
</div>
</TableCell>
</TableRow>
<TableRow v-if="!insTableData || insTableData.length === 0">
<TableCell :colspan="3" class="h-24">
<EmptyTableState title="暂无实例" description="还没有配置任何实例请先添加" />
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</div>
</DialogContent>
</Dialog>
@@ -1,12 +1,13 @@
<script lang="ts">
import { ref, defineComponent } from 'vue'
import { toast } from 'vue-sonner'
import { ref, defineComponent, watch, toRef } from 'vue'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
// @ts-ignore
import { ApiStrGenerate } from '@/util/viewApi'
import { useInstanceData } from '@/composables/useInstanceData'
import { useApiCodeViewer } from '@/composables/useApiCodeViewer'
export default defineComponent({
name: 'ApiCodeViewer',
@@ -39,8 +40,15 @@ export default defineComponent({
emit('update:open', value)
}
// 当前选中的标签
const activeTab = ref('curl')
// 使用实例数据管理 composable
const { hasDynamicRecipientInstance, enabledChannelNames } = useInstanceData(
'task',
toRef(props, 'taskData'),
toRef(props, 'open')
)
// 使用 API 代码查看器 composable
const { activeTab, codeLanguages, copyToClipboard } = useApiCodeViewer()
// 可选参数选项
const showHtml = ref(false)
@@ -49,17 +57,21 @@ export default defineComponent({
const showAtMobiles = ref(false)
const showAtUserIds = ref(false)
const showAtAll = ref(false)
const showRecipients = ref(false)
// 代码语言选项
const codeLanguages = [
{ value: 'curl', label: 'cURL', icon: '🌐' },
{ value: 'javascript', label: 'JS', icon: '🟨' },
{ value: 'python', label: 'Python', icon: '🐍' },
{ value: 'php', label: 'PHP', icon: '🐘' },
{ value: 'golang', label: 'Go', icon: '🐹' },
{ value: 'java', label: 'Java', icon: '☕' },
{ value: 'rust', label: 'Rust', icon: '🦀' }
]
// 监听动态接收实例变化,自动勾选
watch(hasDynamicRecipientInstance, (newVal) => {
if (newVal) {
showRecipients.value = true
}
})
// 监听弹窗关闭,重置状态
watch(() => props.open, (newVal) => {
if (!newVal) {
showRecipients.value = false
}
})
// 生成API代码示例
const generateApiCode = (language: string) => {
@@ -70,7 +82,8 @@ export default defineComponent({
url: showUrl.value,
at_mobiles: showAtMobiles.value,
at_user_ids: showAtUserIds.value,
at_all: showAtAll.value
at_all: showAtAll.value,
recipients: showRecipients.value
}
switch (language) {
@@ -93,25 +106,18 @@ export default defineComponent({
}
}
// 复制代码到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
toast.success('复制成功')
} catch (err) {
toast.error('复制失败')
}
}
return {
handleUpdateOpen,
activeTab,
hasDynamicRecipientInstance,
enabledChannelNames,
showHtml,
showMarkdown,
showUrl,
showAtMobiles,
showAtUserIds,
showAtAll,
showRecipients,
codeLanguages,
generateApiCode,
copyToClipboard
@@ -132,16 +138,31 @@ export default defineComponent({
<div class=" space-y-2">
<!-- API 信息概览 -->
<!-- <div class="space-y-3">
<div class="border rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<div class="border rounded-lg p-4 space-y-2 bg-white dark:bg-slate-900">
<div class="flex items-center gap-2">
<Badge variant="default">POST</Badge>
<code class="text-sm">/sendtasks/send</code>
<code class="text-sm bg-gray-100 dark:bg-slate-800 px-2 py-1 rounded">/api/v1/message/send</code>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">发送消息到任务配置的渠道</p>
<!-- 已启用的渠道列表 -->
<div v-if="enabledChannelNames.length > 0" class="mt-3 pt-3 border-t">
<p class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">已启用的发送渠道</p>
<div class="flex flex-wrap gap-2">
<Badge
v-for="(name, index) in enabledChannelNames"
:key="index"
variant="secondary"
class="text-xs"
>
{{ name }}
</Badge>
</div>
</div>
<div v-else class="mt-3 pt-3 border-t">
<p class="text-xs text-amber-600 dark:text-amber-400"> 该任务暂无启用的发送渠道</p>
</div>
<p class="text-sm text-gray-600">发送消息创建新的消息</p>
</div>
</div> -->
<!-- 可选参数 -->
<div class="border rounded-lg p-4 space-y-3">
@@ -174,9 +195,23 @@ export default defineComponent({
<span class="text-sm">@所有人</span>
<Badge variant="secondary" class="text-xs"></Badge>
</label>
<label
v-if="hasDynamicRecipientInstance"
class="flex items-center gap-2 cursor-not-allowed opacity-75"
>
<input
type="checkbox"
v-model="showRecipients"
disabled
class="rounded cursor-not-allowed"
>
<span class="text-sm">动态接收者</span>
<Badge variant="secondary" class="text-xs">必填</Badge>
</label>
</div>
<div class="space-y-1 text-xs text-gray-500 dark:text-gray-400">
<p>💡 提示@功能仅钉钉和企业微信支持</p>
<p v-if="hasDynamicRecipientInstance" class="text-amber-600 dark:text-amber-400">📧 动态接收者该任务配置了动态接收实例发送时必须通过API指定接收者列表此参数已自动勾选且不可取消</p>
<p>📋 发送顺序实例配置的内容类型优先若为空则按 <code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">HTML Markdown Text</code> 顺序回退</p>
</div>
</div>
+13 -308
View File
@@ -1,27 +1,11 @@
<script setup lang="ts">
import { ref, computed, defineProps, withDefaults, onMounted } from 'vue'
import { defineProps, withDefaults } from 'vue'
import { Button } from '@/components/ui/button'
import { Badge } from "@/components/ui/badge"
import EmptyTableState from '@/components/ui/EmptyTableState.vue'
import {
Combobox,
ComboboxAnchor,
ComboboxInput,
ComboboxList,
ComboboxItem,
ComboboxViewport
} from '@/components/ui/combobox'
import { CheckIcon } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Switch } from '@/components/ui/switch'
import { CONSTANT } from '@/constant'
import InstanceConfig from '@/components/ui/InstanceConfig.vue'
import { toast } from 'vue-sonner'
import { request } from '@/api/api'
import { generateBizUniqueID } from '@/util/uuid'
// 组件props
interface Props {
@@ -33,198 +17,16 @@ const props = withDefaults(defineProps<Props>(), {
editData: null
})
// 前端的页面添加配置
const waysConfigMap = CONSTANT.WAYS_DATA
// 搜索相关状态
const searchQuery = ref('')
const isSearching = ref(false)
const channelName = ref('')
// 当前显示的选项(搜索结果或所有选项)
const displayOptions = ref<Array<{ id: string, name: string, type: string }>>([])
// 输入框显示值(只在用户搜索时显示搜索内容)
const inputDisplayValue = computed({
get: () => searchQuery.value,
set: (value: string) => {
searchQuery.value = value
}
})
// 当前选中任务的配置
const currentChannelConfig = computed(() => {
// 先根据label找到type
let type = displayOptions.value.find(item => item.name === channelName.value)?.type
// 再根据type找到配置
let rs = waysConfigMap.find(item => item.type === type) || null;
return rs;
})
// 表单数据
const formData = ref<Record<string, any>>({})
// 监听任务模式变化
const handlechannelNameChange = () => {
// 数据加载后,text/html单选设置默认选中(这里选第一个)
if (currentChannelConfig.value?.taskInsRadios.length > 0) {
formData.value.taskInsRadio = currentChannelConfig.value?.taskInsRadios[0].subLabel
}
}
// 添加单条实例配置
const handleAddSubmit = async () => {
// 组建表单数据
let postData = {
"id": generateBizUniqueID('IN'),
"enable": 1,
"task_id": props.editData.id,
"way_id": displayOptions.value[0]?.id,
"way_type": displayOptions.value[0]?.type,
"way_name": displayOptions.value[0]?.name,
"content_type": formData.value.taskInsRadio,
"config": "{}"
};
const { taskInsRadio, ...configValue } = formData.value
if (configValue) {
postData.config = JSON.stringify(configValue)
}
const rsp = await request.post('/sendtasks/ins/addone', postData);
if (await rsp.data.code == 200) {
// 动态添加表格第一行
const newItem = {
...postData,
created_by: '',
modified_by: '',
created_on: new Date().toISOString(),
modified_on: new Date().toISOString(),
extra: ''
};
insTableData.value.unshift(newItem);
toast.success(rsp.data.msg);
}
}
const insTableData = ref<Array<{
id: string;
created_by: string;
modified_by: string;
created_on: string;
modified_on: string;
task_id: string;
way_id: string;
way_type: string;
content_type: string;
config: string;
extra: string;
enable: number;
way_name: string;
}>>([]);
const queryInsListData = async () => {
let params = { id: props.editData.id };
const rsp = await request.get('/sendtasks/ins/gettask', { params: params });
insTableData.value = await rsp.data.data.ins_data;
}
// 防抖定时器
let searchTimer: number | null = null
// 搜索处理函数(带防抖)
const handleSearch = (query: string) => {
// 清除之前的定时器
if (searchTimer) {
clearTimeout(searchTimer)
}
if (!query.trim()) {
displayOptions.value = []
return
}
// 设置防抖延迟(500ms
searchTimer = window.setTimeout(async () => {
isSearching.value = true
try {
const results = await querySearchWayAsync(query)
displayOptions.value = results.map((item: any) => ({
id: item.id,
name: item.name,
type: item.type
}))
} catch (error) {
console.error('搜索失败:', error)
displayOptions.value = []
} finally {
isSearching.value = false
}
}, 500)
}
const querySearchWayAsync = async (query: string) => {
const params = { name: query }
const rsp = await request.get('/sendways/list', { params })
return rsp.data.data.lists
}
const getWayTypeText = (type: string) => {
const wayData = CONSTANT.WAYS_DATA.find(item => item.type === type)
return wayData ? wayData.label : type
}
// 切换启用状态
const toggleEnable = async (item: any) => {
const newStatus = item.enable === 1 ? 0 : 1
let postData = { ins_id: item.id, status: newStatus };
const rsp = await request.post('/sendtasks/ins/update_enable', postData);
if (await rsp.data.code == 200) {
toast.success(rsp.data.msg);
// 更新本地状态
item.enable = newStatus
}
}
const handleDelete = async (item: any) => {
const rsp = await request.post('/sendtasks/ins/delete', { id: item.id });
if (rsp.status == 200) {
toast.success(rsp.data.msg);
insTableData.value = insTableData.value.filter((ins) => ins.id !== item.id)
}
}
// 定义emits
const emit = defineEmits(['update:open', 'save'])
const handleEditTask = async () => {
let postData = { id: props.editData.id, name: props.editData.name };
const rsp = await request.post('/sendtasks/edit', postData);
let postData = { id: props.editData.id, name: props.editData.name }
const rsp = await request.post('/sendtasks/edit', postData)
if (await rsp.data.code == 200) {
toast.success(rsp.data.msg);
toast.success(rsp.data.msg)
}
}
// 格式化额外信息列的值
const formatInsConfigDisplay = (row: any) => {
if (!row.config) {
return ""
}
if (["Email", "WeChatOFAccount"].includes(row.way_type)) {
let config = JSON.parse(row.config)
let info = `${config.to_account}`
return info
} else {
return ""
}
}
// 组件挂载时加载实例配置列表
onMounted(() => {
queryInsListData();
})
</script>
<template>
@@ -236,110 +38,13 @@ onMounted(() => {
<Button size="sm" variant="outline" class="w-full sm:w-auto sm:ml-auto" @click="handleEditTask">修改</Button>
</div>
<div class="mt-4">
<div class="flex items-end gap-2">
<div class="flex-1 space-y-2">
<Label class="text-sm font-medium">选择发送渠道</Label>
<Combobox v-model="channelName" @update:model-value="handlechannelNameChange">
<ComboboxAnchor class="w-full">
<ComboboxInput v-model="inputDisplayValue" @input="handleSearch(inputDisplayValue)"
class="flex h-10 w-full" placeholder="搜索或选择渠道类型进行实例的添加..." />
</ComboboxAnchor>
<ComboboxList class="w-[var(--reka-combobox-trigger-width)]">
<ComboboxViewport>
<div v-if="isSearching" class="px-2 py-1.5 text-sm text-muted-foreground">
搜索中...
<!-- 实例配置组件 -->
<div class="p-4">
<InstanceConfig
type="task"
:data="editData"
/>
</div>
<div v-else-if="searchQuery && displayOptions.length === 0"
class="px-2 py-1.5 text-sm text-muted-foreground">
未找到匹配项
</div>
<ComboboxItem v-for="option in displayOptions" :key="option.id" :value="option.name"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50">
<CheckIcon class="mr-2 h-4 w-4" :class="channelName === option.name ? 'opacity-100' : 'opacity-0'" />
{{ option.name }}
</ComboboxItem>
</ComboboxViewport>
</ComboboxList>
</Combobox>
</div>
<Button size="sm" variant="outline" @click="handleAddSubmit">添加实例</Button>
</div>
<!-- 动态任务配置区域 -->
<div v-if="currentChannelConfig" class="mt-4">
<!-- 任务指令输入字段 -->
<div v-if="currentChannelConfig.taskInsInputs && currentChannelConfig.taskInsInputs.length > 0" class="mb-2">
<Label class="text-sm font-medium text-gray-700 mb-1">实例配置</Label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="input in currentChannelConfig.taskInsInputs" :key="input.col" class="space-y-2">
<label class="text-xs font-medium text-gray-600">{{ input.label }}</label>
<Input v-model="formData[input.col]" :placeholder="input.desc || `请输入${input.desc}`"
:type="input.type || 'text'" class="text-sm" />
</div>
</div>
</div>
<!-- 任务指令单选项 -->
<div v-if="currentChannelConfig.taskInsRadios && currentChannelConfig.taskInsRadios.length > 0">
<Label class="text-sm font-medium text-gray-700 mb-1">消息格式</Label>
<RadioGroup v-model="formData.taskInsRadio" class="flex flex-wrap gap-4">
<div v-for="radio in currentChannelConfig.taskInsRadios" :key="radio.value"
class="flex items-center space-x-2">
<RadioGroupItem :value="radio.subLabel" :id="radio.subLabel" />
<Label :for="radio.value"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{{ radio.subLabel }}
</Label>
</div>
</RadioGroup>
</div>
</div>
</div>
</div>
<!-- 关联的实例表 -->
<div class="mt-4">
<h3 class="text-sm font-medium text-gray-900 mb-3">已经关联的实例</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>渠道名</TableHead>
<TableHead>渠道/内容</TableHead>
<TableHead class="text-center">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="item in insTableData" :key="item.id">
<TableCell>{{ item.way_name }}</TableCell>
<TableCell>
<Badge variant="outline">
{{ getWayTypeText(item.way_type) }}
</Badge>
<Badge variant="outline">
{{ item.content_type }}
</Badge>
<Badge variant="outline" v-if="item.config !== '{}'">
{{ formatInsConfigDisplay(item) }}
</Badge>
</TableCell>
<TableCell class="text-center">
<div class="flex items-center justify-center gap-2">
<Switch :model-value="item.enable === 1" @update:model-value="() => toggleEnable(item)" />
<Button size="sm" variant="outline" class="text-red-500 border-red-300 hover:bg-red-50
hover:border-red-400 hover:text-red-600 hover:shadow-md
transition-all duration-200" @click="() => handleDelete(item)">删除</Button>
</div>
</TableCell>
</TableRow>
<TableRow v-if="!insTableData || insTableData.length === 0">
<TableCell :colspan="3" class="h-24">
<EmptyTableState title="暂无实例" description="还没有配置任何实例请先添加" />
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</template>
+30 -2
View File
@@ -250,20 +250,48 @@ const saveButtonText = computed(() => {
<!-- Radio Group -->
<div class="mb-6">
<label class="text-lg font-medium mb-3 block">渠道类型</label>
<RadioGroup v-model="channelMode" @update:model-value="handleChannelModeChange" class="flex flex-wrap gap-4">
<RadioGroup v-model="channelMode" @update:model-value="handleChannelModeChange" class="flex flex-wrap gap-3">
<div v-for="option in channelModeOptions" :key="option.value" class="flex items-center space-x-2">
<RadioGroupItem :value="option.value" :id="option.value" />
<label :for="option.value"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 flex items-center gap-1.5">
{{ option.label }}
<span
v-if="waysConfigMap.find(item => item.type === option.value)?.dynamicRecipient?.support"
class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
:title="`支持${waysConfigMap.find(item => item.type === option.value)?.dynamicRecipient?.label || '动态接收者'}群发`"
>
群发
</span>
</label>
</div>
</RadioGroup>
<p class="text-[11px] text-gray-500 dark:text-gray-400 mt-2.5 leading-relaxed">
💡 "群发"标识的渠道支持动态接收者可在 API 调用时指定多个接收账号
</p>
</div>
<div class="w-full">
<!-- 动态表单 -->
<div v-if="currentChannelConfig" class="mt-6">
<!-- 动态接收者支持提示 -->
<div
v-if="currentChannelConfig.dynamicRecipient?.support"
class="mb-4 p-2.5 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md"
>
<div class="flex items-start gap-2">
<span class="text-blue-600 dark:text-blue-400 text-sm mt-0.5">📧</span>
<div class="flex-1 space-y-1">
<p class="text-xs text-blue-800 dark:text-blue-200 font-medium">
支持群发模式 - 可在配置实例时启用"动态接收者"通过 API <code class="px-1 py-0.5 bg-blue-100 dark:bg-blue-800 rounded text-[11px]">recipients</code> 参数指定多个{{ currentChannelConfig.dynamicRecipient.label }}
</p>
<p class="text-[11px] text-blue-600 dark:text-blue-400">
适用邮件群发公众号批量推送营销通知等
</p>
</div>
</div>
</div>
<!-- 基本配置输入字段 -->
<div v-if="currentChannelConfig.inputs && currentChannelConfig.inputs.length > 0" class="mb-8">
<h4 class="text-base font-medium mb-4 text-gray-800">基本配置</h4>
+519
View File
@@ -0,0 +1,519 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Button } from '@/components/ui/button'
import { Badge } from "@/components/ui/badge"
import EmptyTableState from '@/components/ui/EmptyTableState.vue'
import {
Combobox,
ComboboxAnchor,
ComboboxInput,
ComboboxList,
ComboboxItem,
ComboboxViewport
} from '@/components/ui/combobox'
import { CheckIcon } from 'lucide-vue-next'
import { Input } from '@/components/ui/input'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Label } from '@/components/ui/label'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Switch } from '@/components/ui/switch'
import { CONSTANT } from '@/constant'
import { toast } from 'vue-sonner'
import { request } from '@/api/api'
import { generateBizUniqueID } from '@/util/uuid'
// 组件props
interface Props {
// 实例类型:'template' 或 'task'
type: 'template' | 'task'
// 关联的数据(模板数据或任务数据)
data: any
// 是否在对话框中显示(用于模板)
inDialog?: boolean
}
const props = withDefaults(defineProps<Props>(), {
inDialog: false
})
// API 配置映射
const apiConfig = computed(() => {
if (props.type === 'template') {
return {
addIns: '/templates/ins/addone',
getIns: '/templates/ins/get',
deleteIns: '/sendtasks/ins/delete',
updateEnable: '/sendtasks/ins/update_enable',
idField: 'template_id',
nameField: 'name'
}
} else {
return {
addIns: '/sendtasks/ins/addone',
getIns: '/sendtasks/ins/gettask',
deleteIns: '/sendtasks/ins/delete',
updateEnable: '/sendtasks/ins/update_enable',
idField: 'task_id',
nameField: 'name'
}
}
})
// 前端的页面添加配置
const waysConfigMap = CONSTANT.WAYS_DATA
// 搜索相关状态
const searchQuery = ref('')
const isSearching = ref(false)
const channelName = ref('')
// 当前显示的选项(搜索结果或所有选项)
const displayOptions = ref<Array<{ id: string, name: string, type: string }>>([])
// 输入框显示值(只在用户搜索时显示搜索内容)
const inputDisplayValue = computed({
get: () => searchQuery.value,
set: (value: string) => {
searchQuery.value = value
}
})
// 当前选中渠道的配置
const currentChannelConfig = computed(() => {
// 先根据label找到type
let type = displayOptions.value.find(item => item.name === channelName.value)?.type
// 再根据type找到配置
let rs = waysConfigMap.find(item => item.type === type) || null
return rs
})
// 表单数据
const formData = ref<Record<string, any>>({
allowMultiRecip: false // 默认false为固定模式,true为动态模式
})
// 是否显示接收者输入框
const shouldShowRecipientInput = computed(() => {
// 支持动态接收者 且 未勾选(固定模式)时显示输入框
return currentChannelConfig.value?.dynamicRecipient?.support && !formData.value.allowMultiRecip
})
// 监听渠道变化
const handlechannelNameChange = () => {
// 数据加载后,text/html单选设置默认选中(这里选第一个)
if (currentChannelConfig.value?.taskInsRadios.length > 0) {
formData.value.templ_type = currentChannelConfig.value?.taskInsRadios[0].subLabel
}
// 重置动态接收者设置
formData.value.allowMultiRecip = false
}
// 添加单条实例配置
const handleAddSubmit = async () => {
// 验证是否选择了渠道
if (!channelName.value) {
toast.error('请选择发送渠道')
return
}
// 检查动态接收和固定接收不能混合使用
if (insTableData.value.length > 0) {
const hasDynamicInstance = insTableData.value.some(ins => {
try {
const config = JSON.parse(ins.config)
return config.allowMultiRecip === true
} catch {
return false
}
})
const entityName = props.type === 'template' ? '模板' : '任务'
// 如果要添加动态接收实例,但已有其他实例
if (formData.value.allowMultiRecip === true) {
if (hasDynamicInstance) {
toast.error(`${entityName}已存在动态接收实例,一个${entityName}只能配置一个动态接收实例`)
return
}
if (insTableData.value.length > 0) {
toast.error(`动态接收实例不能与固定接收实例混合使用,请先删除所有固定实例`)
return
}
}
// 如果要添加固定接收实例,但已有动态接收实例
if (formData.value.allowMultiRecip !== true && hasDynamicInstance) {
toast.error(`${entityName}已配置动态接收实例,不能再添加固定接收实例`)
return
}
}
// 验证内容类型
const contentType = formData.value.templ_type
if (!contentType) {
toast.error('请选择消息格式')
return
}
// 仅模板需要验证对应格式的内容是否为空
if (props.type === 'template') {
const templateFieldMap: Record<string, string> = {
'text': 'text_template',
'html': 'html_template',
'markdown': 'markdown_template'
}
const fieldName = templateFieldMap[contentType.toLowerCase()]
if (fieldName) {
const templateContent = props.data?.[fieldName] || ''
// 检查是否为空(去除所有空白字符后检查)
if (!templateContent.trim()) {
toast.error(`模板的 ${contentType} 格式内容为空,无法添加此类型的实例`)
return
}
}
}
// 组建表单数据
let postData: Record<string, any> = {
"id": generateBizUniqueID('IN'),
"enable": 1,
[apiConfig.value.idField]: props.data.id,
"way_id": displayOptions.value[0]?.id,
"way_type": displayOptions.value[0]?.type,
"way_name": displayOptions.value[0]?.name,
"content_type": formData.value.templ_type,
"config": JSON.stringify(formData.value),
}
try {
const response = await request.post(apiConfig.value.addIns, postData)
if (response.status === 200 && response.data.code === 200) {
toast.success(response.data.msg)
// 重新加载实例列表
await queryInsListData()
// 清空表单
channelName.value = ''
formData.value = { allowMultiRecip: false }
} else {
toast.error(response.data.msg || '添加实例失败')
}
} catch (error: any) {
toast.error(error.response?.data?.msg || '添加实例失败')
}
}
// 实例表格数据
const insTableData = ref<any[]>([])
// 格式化额外信息列的值
const formatInsConfigDisplay = (row: any) => {
if (!row.config) {
return ""
}
if (["Email", "WeChatOFAccount"].includes(row.way_type)) {
let config = JSON.parse(row.config)
// 检查是否为动态接收者模式
if (config.allowMultiRecip === true) {
return "动态接收"
}
// 固定模式,显示接收者
return config.to_account || ""
} else {
return ""
}
}
// 查询实例列表数据
const queryInsListData = async () => {
if (!props.data?.id) return
try {
const response = await request.get(apiConfig.value.getIns, {
params: { id: props.data.id }
})
if (response.status === 200 && response.data.code === 200) {
// 模板返回 ins_list,任务返回 ins_data
const insList = response.data.data.ins_list || response.data.data.ins_data || []
insTableData.value = insList
}
} catch (error) {
console.error('获取实例列表失败', error)
}
}
// 删除实例
const handleDeleteIns = async (insId: string) => {
try {
const response = await request.post(apiConfig.value.deleteIns, { id: insId })
if (response.status === 200 && response.data.code === 200) {
toast.success(response.data.msg)
await queryInsListData()
} else {
toast.error(response.data.msg || '删除失败')
}
} catch (error: any) {
toast.error(error.response?.data?.msg || '删除失败')
}
}
// 切换实例启用状态
const handleToggleEnable = async (insId: string, currentStatus: number | string) => {
const isEnabled = Number(currentStatus) === 1
const newStatus = isEnabled ? 0 : 1
// 立即更新本地状态,提供即时反馈
const insIndex = insTableData.value.findIndex(ins => ins.id === insId)
if (insIndex !== -1) {
insTableData.value[insIndex].enable = newStatus
}
try {
const response = await request.post(apiConfig.value.updateEnable, {
ins_id: insId,
status: newStatus
})
if (response.status === 200 && response.data.code === 200) {
toast.success(response.data.msg)
// 重新加载确保数据同步
await queryInsListData()
} else {
toast.error(response.data.msg || '更新失败')
// 失败时恢复原状态
if (insIndex !== -1) {
insTableData.value[insIndex].enable = currentStatus
}
}
} catch (error: any) {
console.error('状态切换失败:', error)
toast.error(error.response?.data?.msg || '更新失败')
// 失败时恢复原状态
if (insIndex !== -1) {
insTableData.value[insIndex].enable = currentStatus
}
}
}
// 防抖定时器
let searchTimer: number | null = null
// 搜索渠道(带防抖)
const handleSearch = (query: string) => {
// 清除之前的定时器
if (searchTimer) {
clearTimeout(searchTimer)
}
if (!query.trim()) {
displayOptions.value = []
return
}
// 设置防抖延迟(500ms
searchTimer = window.setTimeout(async () => {
isSearching.value = true
try {
const response = await request.get('/sendways/list', {
params: { name: query }
})
if (response.status === 200 && response.data.code === 200) {
displayOptions.value = response.data.data.lists.map((item: any) => ({
id: item.id,
name: item.name,
type: item.type
}))
}
} catch (error) {
console.error('搜索渠道失败', error)
displayOptions.value = []
} finally {
isSearching.value = false
}
}, 500)
}
// 监听数据变化,自动加载实例列表
watch(() => props.data?.id, (newVal) => {
if (newVal) {
queryInsListData()
}
}, { immediate: true })
// 暴露方法供父组件调用
defineExpose({
queryInsListData
})
</script>
<template>
<div class="space-y-4" :class="{ 'px-4 pb-4': inDialog }">
<!-- 信息展示区域 -->
<div v-if="data" class="p-3 bg-muted rounded-lg space-y-1">
<div class="flex items-baseline gap-2">
<span class="text-base font-semibold">{{ data[apiConfig.nameField] }}</span>
<Badge variant="outline" class="text-xs">{{ data.id }}</Badge>
</div>
<div class="text-xs text-muted-foreground">
为此{{ type === 'template' ? '模板' : '任务' }}配置发送实例
</div>
</div>
<!-- 添加实例表单 -->
<div class="space-y-4">
<div class="flex items-end gap-2">
<div class="flex-1 space-y-2">
<Label class="text-sm font-medium">选择发送渠道</Label>
<Combobox v-model="channelName" @update:model-value="handlechannelNameChange">
<ComboboxAnchor class="w-full">
<ComboboxInput
v-model="inputDisplayValue"
@input="handleSearch(inputDisplayValue)"
class="flex h-10 w-full"
placeholder="搜索或选择渠道类型进行实例的添加..."
/>
</ComboboxAnchor>
<ComboboxList class="w-[var(--reka-combobox-trigger-width)]">
<ComboboxViewport>
<ComboboxItem v-for="option in displayOptions" :key="option.id" :value="option.name">
<div class="flex items-center justify-between w-full">
<span>{{ option.name }}</span>
<CheckIcon v-if="channelName === option.name" class="h-4 w-4" />
</div>
</ComboboxItem>
<div v-if="isSearching" class="p-2 text-sm text-muted-foreground">搜索中...</div>
<div v-if="!isSearching && displayOptions.length === 0 && searchQuery" class="p-2 text-sm text-muted-foreground">
未找到匹配的渠道
</div>
</ComboboxViewport>
</ComboboxList>
</Combobox>
</div>
<Button size="sm" variant="outline" @click="handleAddSubmit">添加实例</Button>
</div>
</div>
<!-- 渠道配置表单 -->
<div v-if="currentChannelConfig" class="mt-4">
<!-- 动态接收者勾选框 -->
<div v-if="currentChannelConfig.dynamicRecipient?.support" class="mb-4 p-3 border rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="flex items-center space-x-2">
<Switch
:model-value="formData.allowMultiRecip"
@update:model-value="(val: boolean) => formData.allowMultiRecip = val"
:id="`allow-multi-${channelName}`"
/>
<Label :for="`allow-multi-${channelName}`" class="text-sm font-medium cursor-pointer">
动态接收者模式
</Label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 ml-8">
{{ formData.allowMultiRecip ? '支持动态接收者,发送时通过API指定接收者列表(群发模式)' : '固定接收者模式,需要在下方配置固定接收者' }}
</p>
<p v-if="formData.allowMultiRecip" class="text-xs text-orange-500 dark:text-orange-400 mt-1 ml-8 font-medium">
⚠️ 注意:一个{{ type === 'template' ? '模板' : '任务' }}只能配置一个动态接收实例,且不能与固定接收实例混合使用
</p>
</div>
<!-- 接收者输入字段 -->
<div v-if="shouldShowRecipientInput" class="mb-2">
<Label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">实例配置</Label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">
{{ currentChannelConfig.dynamicRecipient.label }}
</label>
<Input
v-model="formData[currentChannelConfig.dynamicRecipient.field]"
:placeholder="`请输入${currentChannelConfig.dynamicRecipient.desc}`"
type="text"
class="text-sm"
/>
</div>
</div>
</div>
<!-- 实例配置输入字段(排除动态接收者字段) -->
<div v-if="currentChannelConfig.taskInsInputs && currentChannelConfig.taskInsInputs.length > 0" class="mb-2">
<Label class="text-sm font-medium mb-1">实例配置</Label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="input in currentChannelConfig.taskInsInputs.filter((inp: any) => inp.col !== currentChannelConfig?.dynamicRecipient?.field)"
:key="input.col"
class="space-y-2"
>
<label class="text-xs font-medium text-muted-foreground">{{ input.label || input.desc }}</label>
<Input
v-model="formData[input.col]"
:placeholder="input.desc || `请输入${input.label}`"
:type="input.type || 'text'"
class="w-full"
/>
</div>
</div>
</div>
<!-- 单选框 -->
<div v-if="currentChannelConfig.taskInsRadios && currentChannelConfig.taskInsRadios.length > 0" class="mt-4">
<Label class="text-sm font-medium mb-2">消息格式</Label>
<RadioGroup v-model="formData.templ_type" class="flex gap-4">
<div v-for="radio in currentChannelConfig.taskInsRadios" :key="radio.subLabel" class="flex items-center space-x-2">
<RadioGroupItem :value="radio.subLabel" :id="radio.subLabel" />
<Label :for="radio.subLabel" class="text-sm cursor-pointer">{{ radio.subLabel }}</Label>
</div>
</RadioGroup>
</div>
</div>
<!-- 关联的实例表 -->
<div class="mt-4">
<h3 class="text-sm font-medium mb-3">已经关联的实例</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>渠道名称</TableHead>
<TableHead>内容类型</TableHead>
<TableHead>接收者</TableHead>
<TableHead class="text-center">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="ins in insTableData" :key="ins.id">
<TableCell>
<div class="font-medium">{{ ins.way_name || '未命名' }}</div>
<div class="text-xs text-muted-foreground">{{ ins.way_type }}</div>
</TableCell>
<TableCell>
<Badge variant="secondary">{{ ins.content_type }}</Badge>
</TableCell>
<TableCell>
<span class="text-sm">{{ formatInsConfigDisplay(ins) }}</span>
</TableCell>
<TableCell class="text-center">
<div class="flex items-center justify-center gap-2">
<Switch
:model-value="ins.enable === 1"
@update:model-value="() => handleToggleEnable(ins.id, ins.enable)"
/>
<Button
size="sm"
variant="outline"
class="text-red-500 border-red-300 hover:bg-red-50 hover:border-red-400 hover:text-red-600 hover:shadow-md transition-all duration-200"
@click="handleDeleteIns(ins.id)"
>
删除
</Button>
</div>
</TableCell>
</TableRow>
<TableRow v-if="!insTableData || insTableData.length === 0">
<TableCell :colspan="4" class="h-24">
<EmptyTableState title="暂无实例" description="还没有配置任何实例请先添加" />
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</template>
+37
View File
@@ -0,0 +1,37 @@
import { ref } from 'vue'
import { toast } from 'vue-sonner'
/**
* API 代码查看器公共逻辑 Composable
*/
export function useApiCodeViewer() {
// 当前选中的标签
const activeTab = ref('curl')
// 代码语言选项
const codeLanguages = [
{ value: 'curl', label: 'cURL', icon: '🌐' },
{ value: 'javascript', label: 'JS', icon: '🟨' },
{ value: 'python', label: 'Python', icon: '🐍' },
{ value: 'php', label: 'PHP', icon: '🐘' },
{ value: 'golang', label: 'Go', icon: '🐹' },
{ value: 'java', label: 'Java', icon: '☕' },
{ value: 'rust', label: 'Rust', icon: '🦀' }
]
// 复制代码到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
toast.success('复制成功')
} catch (err) {
toast.error('复制失败')
}
}
return {
activeTab,
codeLanguages,
copyToClipboard
}
}
+83
View File
@@ -0,0 +1,83 @@
import { ref, computed, watch } from 'vue'
import { request } from '@/api/api'
/**
* 实例数据管理 Composable
* 用于任务和模板的实例数据加载和处理
*/
export function useInstanceData(
type: 'task' | 'template',
dataRef: any,
openRef: any
) {
// 实例数据
const instances = ref<any[]>([])
// 检查是否有支持动态接收者的实例
const hasDynamicRecipientInstance = computed(() => {
if (instances.value && Array.isArray(instances.value)) {
return instances.value.some((ins: any) => {
try {
const config = typeof ins.config === 'string' ? JSON.parse(ins.config) : ins.config
return config?.allowMultiRecip === true
} catch {
return false
}
})
}
return false
})
// 获取已启用的实例渠道名称列表
const enabledChannelNames = computed(() => {
if (instances.value && Array.isArray(instances.value)) {
return instances.value
.filter((ins: any) => ins.enable === 1)
.map((ins: any) => ins.way_name)
}
return []
})
// 加载实例数据
const loadInstances = async () => {
const id = dataRef.value?.id
if (!id) return
try {
let response
if (type === 'template') {
response = await request.get('/templates/ins/get', {
params: { id }
})
instances.value = response.data.data.ins_list || []
} else {
response = await request.get('/sendtasks/ins/gettask', {
params: { id }
})
instances.value = response.data.data.ins_data || []
}
} catch (err) {
console.error(`加载${type === 'template' ? '模板' : '任务'}实例失败:`, err)
instances.value = []
}
}
// 监听弹窗打开状态
watch(
() => openRef.value,
async (newVal: boolean) => {
if (newVal) {
await loadInstances()
} else {
instances.value = []
}
}
)
return {
instances,
hasDynamicRecipientInstance,
enabledChannelNames,
loadInstances
}
}
+37 -3
View File
@@ -20,6 +20,13 @@ const CONSTANT = {
{
type: 'Email',
label: '邮箱',
// 动态接收者配置
dynamicRecipient: {
support: true, // 是否支持动态接收者
field: 'to_account', // 接收者字段名
label: '收件邮箱', // 接收者字段标签
desc: '邮箱地址', // 接收者字段描述
},
inputs: [
{ subLabel: 'smtp服务地址', value: '', col: 'server', desc: "smtp@xyz.com" },
{ subLabel: 'smtp服务端口', value: '', col: 'port', desc: "port" },
@@ -32,8 +39,7 @@ const CONSTANT = {
{ subLabel: 'html', content: 'html' },
],
taskInsInputs: [
{ value: '', col: 'to_account', desc: "目的邮箱账号(发给谁)" },
// { value: '', col: 'title', desc: "邮箱标题" },
{ value: '', col: 'to_account', desc: '收件邮箱', label: '收件邮箱' },
],
},
{
@@ -90,6 +96,13 @@ const CONSTANT = {
{
type: 'WeChatOFAccount',
label: '微信测试公众号模板',
// 动态接收者配置
dynamicRecipient: {
support: true, // 是否支持动态接收者
field: 'to_account', // 接收者字段名
label: '接收者OpenId', // 接收者字段标签
desc: 'OpenId', // 接收者字段描述
},
inputs: [
{ subLabel: 'appID', value: '', col: 'appID', desc: "公众号appid" },
{ subLabel: 'appsecret', value: '', col: 'appsecret', desc: "公众号appsecret" },
@@ -103,7 +116,7 @@ const CONSTANT = {
{ subLabel: 'text', content: 'text' },
],
taskInsInputs: [
{ value: '', col: 'to_account', desc: "要发送的OpenId(登录微信公众号后台查看)" },
{ value: '', col: 'to_account', desc: '接收者OpenId', label: '接收者OpenId' },
],
},
{
@@ -121,6 +134,27 @@ const CONSTANT = {
taskInsInputs: [
],
},
// 暂时屏蔽阿里云短信入口
// {
// type: 'AliyunSMS',
// label: '阿里云短信',
// inputs: [
// { subLabel: 'AccessKeyId', value: '', col: 'access_key_id', desc: "阿里云AccessKeyId" },
// { subLabel: 'AccessKeySecret', value: '', col: 'access_key_secret', desc: "阿里云AccessKeySecret" },
// { subLabel: '短信签名', value: '', col: 'sign_name', desc: "短信签名名称" },
// { subLabel: '渠道名', value: '', col: 'name', desc: "想要设置的渠道名字" },
// ],
// tips: {
// text: "阿里云短信说明", desc: "使用阿里云短信服务发送短信,需要在阿里云控制台申请短信签名和模板。<br />AccessKey请在阿里云控制台获取。"
// },
// taskInsRadios: [
// { subLabel: 'text', content: 'text' },
// ],
// taskInsInputs: [
// { value: '', col: 'phone_number', desc: "手机号码(接收短信的手机号)" },
// { value: '', col: 'template_code', desc: "短信模板CODE(在阿里云短信控制台获取)" },
// ],
// },
],
API_VIEW_DATA: [
{ label: "curl", class: "language-shell line-numbers", code: "", func: ApiStrGenerate.getCurlString },
+23 -15
View File
@@ -179,6 +179,8 @@ class ApiStrGenerate {
if (options.at_mobiles) data.at_mobiles = ['13800138000', '13900139000'];
if (options.at_user_ids) data.at_user_ids = ['zhangsan', 'lisi'];
if (options.at_all) data.at_all = true;
// 动态接收者字段(用于邮箱、微信公众号等支持动态接收者的渠道)
if (options.recipients) data.recipients = ['user1@example.com', 'user2@example.com'];
return JSON.stringify(data, null, 4);
}
@@ -218,7 +220,7 @@ class ApiStrGenerate {
// ==================== 模板 API (V2) ====================
class TemplateApiStrGenerate {
static getTemplateDataString(template_id, placeholders_json) {
static getTemplateDataString(template_id, placeholders_json, options = {}) {
// 解析占位符配置
let placeholders = {};
try {
@@ -241,6 +243,12 @@ class TemplateApiStrGenerate {
title: 'message title',
placeholders: placeholders
};
// 添加动态接收者字段(如果需要)
if (options.recipients) {
data.recipients = ['user1@example.com', 'user2@example.com'];
}
return JSON.stringify(data, null, 4);
}
@@ -248,32 +256,32 @@ class TemplateApiStrGenerate {
return `${gethttpOrigin()}/api/v2/message/send`;
}
static getCurlString(template_id, placeholders_json) {
return CodeTemplates.getCurl(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
static getCurlString(template_id, placeholders_json, options = {}) {
return CodeTemplates.getCurl(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json, options));
}
static getGolangString(template_id, placeholders_json) {
return CodeTemplates.getGolang(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
static getGolangString(template_id, placeholders_json, options = {}) {
return CodeTemplates.getGolang(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json, options));
}
static getPythonString(template_id, placeholders_json) {
return CodeTemplates.getPython(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
static getPythonString(template_id, placeholders_json, options = {}) {
return CodeTemplates.getPython(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json, options));
}
static getJavaString(template_id, placeholders_json) {
return CodeTemplates.getJava(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
static getJavaString(template_id, placeholders_json, options = {}) {
return CodeTemplates.getJava(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json, options));
}
static getRustString(template_id, placeholders_json) {
return CodeTemplates.getRust(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
static getRustString(template_id, placeholders_json, options = {}) {
return CodeTemplates.getRust(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json, options));
}
static getPHPString(template_id, placeholders_json) {
return CodeTemplates.getPHP(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
static getPHPString(template_id, placeholders_json, options = {}) {
return CodeTemplates.getPHP(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json, options));
}
static getNodeString(template_id, placeholders_json) {
return CodeTemplates.getNode(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json));
static getNodeString(template_id, placeholders_json, options = {}) {
return CodeTemplates.getNode(this.getApiUrl(), this.getTemplateDataString(template_id, placeholders_json, options));
}
}