feat: add message push function call
This commit is contained in:
@@ -3,15 +3,22 @@ module github.com/engigu/baihu-panel
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
github.com/gofrs/flock v0.13.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/rs/xid v1.6.0
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5
|
github.com/shirou/gopsutil/v3 v3.24.5
|
||||||
|
github.com/silenceper/wechat/v2 v2.1.12
|
||||||
|
github.com/sirupsen/logrus v1.9.4
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
|
golang.org/x/net v0.47.0
|
||||||
golang.org/x/text v0.32.0
|
golang.org/x/text v0.32.0
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
@@ -26,10 +33,14 @@ require (
|
|||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/bep/godartsass/v2 v2.5.0 // indirect
|
github.com/bep/godartsass/v2 v2.5.0 // indirect
|
||||||
github.com/bep/golibsass v1.2.0 // indirect
|
github.com/bep/golibsass v1.2.0 // indirect
|
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
@@ -38,19 +49,19 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gofrs/flock v0.13.0 // indirect
|
|
||||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
@@ -60,17 +71,18 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/spf13/afero v1.14.0 // indirect
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
github.com/spf13/cast v1.9.2 // indirect
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
github.com/spf13/cobra v1.10.2 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
|
||||||
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
github.com/tdewolff/parse/v2 v2.8.3 // 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/tklauser/go-sysconf v0.3.12 // indirect
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
@@ -79,10 +91,10 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.46.0 // indirect
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
||||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
|
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||||
github.com/air-verse/air v1.64.5 h1:+gs/NgTzYYe+gGPyfHy3XxpJReQWC1pIsiKIg0LgNt4=
|
github.com/air-verse/air v1.64.5 h1:+gs/NgTzYYe+gGPyfHy3XxpJReQWC1pIsiKIg0LgNt4=
|
||||||
github.com/air-verse/air v1.64.5/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
|
github.com/air-verse/air v1.64.5/go.mod h1:OaJZSfZqf7wyjS2oP/CcEVyIt0JmZuPh5x1gdtklmmY=
|
||||||
|
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||||
|
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/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
|
||||||
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
|
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
|
||||||
@@ -42,22 +52,30 @@ github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE=
|
|||||||
github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
|
github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
|
||||||
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
|
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
|
||||||
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
|
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
|
||||||
|
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.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
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.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||||
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
@@ -68,9 +86,14 @@ github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs=
|
|||||||
github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
@@ -87,6 +110,7 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
|
|||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
@@ -101,8 +125,11 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
|
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
|
||||||
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
@@ -127,13 +154,29 @@ github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XG
|
|||||||
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
|
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
|
||||||
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
|
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
|
||||||
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
|
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
|
||||||
|
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
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=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
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.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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -142,12 +185,14 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
|||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
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/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
||||||
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -162,12 +207,17 @@ 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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
@@ -209,8 +259,13 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
|
|||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||||
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
|
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
|
||||||
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
|
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
|
||||||
|
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
||||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||||
|
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=
|
||||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||||
@@ -221,6 +276,19 @@ github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
|||||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
|
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||||
|
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||||
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
@@ -246,25 +314,28 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
|||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||||
|
github.com/silenceper/wechat/v2 v2.1.12 h1:hoBeuL7Mgafz/ox6rn6r02rffFxHZhu7E0SlDRa+m28=
|
||||||
|
github.com/silenceper/wechat/v2 v2.1.12/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
|
||||||
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
|
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -280,71 +351,168 @@ github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
|||||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||||
|
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
|
||||||
|
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
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/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
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/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||||
|
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||||
|
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
|
||||||
|
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||||
|
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=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
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=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
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-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=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
|
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||||
|
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
|
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||||
|
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=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
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.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
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/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const (
|
|||||||
SectionSystem = "system"
|
SectionSystem = "system"
|
||||||
SectionScheduler = "scheduler"
|
SectionScheduler = "scheduler"
|
||||||
SectionSecurity = "security"
|
SectionSecurity = "security"
|
||||||
|
SectionNotify = "notify"
|
||||||
|
|
||||||
// Site Settings Key 常量
|
// Site Settings Key 常量
|
||||||
KeyTitle = "title"
|
KeyTitle = "title"
|
||||||
@@ -54,6 +55,25 @@ const (
|
|||||||
KeyQueueSize = "queue_size"
|
KeyQueueSize = "queue_size"
|
||||||
KeyRateInterval = "rate_interval"
|
KeyRateInterval = "rate_interval"
|
||||||
|
|
||||||
|
// Notify Settings Key 常量
|
||||||
|
KeyNotifyChannels = "channels"
|
||||||
|
KeyNotifyEvents = "events"
|
||||||
|
KeyNotifyToken = "notify_token"
|
||||||
|
|
||||||
|
// 事件绑定类型
|
||||||
|
BindingTypeSystem = "system"
|
||||||
|
BindingTypeTask = "task"
|
||||||
|
|
||||||
|
// 系统事件类型
|
||||||
|
EventUserLogin = "user_login"
|
||||||
|
EventBruteForceLogin = "brute_force_login"
|
||||||
|
EventPasswordChanged = "password_changed"
|
||||||
|
|
||||||
|
// 任务事件类型
|
||||||
|
EventTaskSuccess = "task_success"
|
||||||
|
EventTaskFailed = "task_failed"
|
||||||
|
EventTaskTimeout = "task_timeout"
|
||||||
|
|
||||||
// WebSocket 消息类型
|
// WebSocket 消息类型
|
||||||
WSTypeHeartbeat = "heartbeat"
|
WSTypeHeartbeat = "heartbeat"
|
||||||
WSTypeHeartbeatAck = "heartbeat_ack"
|
WSTypeHeartbeatAck = "heartbeat_ack"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -54,6 +55,10 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
attempt := val.(*loginAttempt)
|
attempt := val.(*loginAttempt)
|
||||||
if attempt.Count >= 5 && time.Since(attempt.LastAttempt) < time.Minute {
|
if attempt.Count >= 5 && time.Since(attempt.LastAttempt) < time.Minute {
|
||||||
ac.loginLogService.Create(req.Username, ip, userAgent, "failed", "尝试次数过多,请一分钟后再试")
|
ac.loginLogService.Create(req.Username, ip, userAgent, "failed", "尝试次数过多,请一分钟后再试")
|
||||||
|
go services.NewNotificationService().TriggerEvent(constant.BindingTypeSystem, constant.EventBruteForceLogin, "", map[string]interface{}{
|
||||||
|
"ip": ip,
|
||||||
|
"username": req.Username,
|
||||||
|
})
|
||||||
utils.TooManyRequests(c, "尝试次数过多,请一分钟后再试")
|
utils.TooManyRequests(c, "尝试次数过多,请一分钟后再试")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -102,6 +107,11 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
// 记录登录成功日志
|
// 记录登录成功日志
|
||||||
ac.loginLogService.Create(req.Username, ip, userAgent, "success", "登录成功")
|
ac.loginLogService.Create(req.Username, ip, userAgent, "success", "登录成功")
|
||||||
|
|
||||||
|
go services.NewNotificationService().TriggerEvent(constant.BindingTypeSystem, constant.EventUserLogin, "", map[string]interface{}{
|
||||||
|
"ip": ip,
|
||||||
|
"username": req.Username,
|
||||||
|
})
|
||||||
|
|
||||||
utils.Success(c, gin.H{
|
utils.Success(c, gin.H{
|
||||||
"user": user.Username,
|
"user": user.Username,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/services"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationController struct {
|
||||||
|
notifyService *services.NotificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationController() *NotificationController {
|
||||||
|
return &NotificationController{
|
||||||
|
notifyService: services.NewNotificationService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelTypes 获取支持的渠道类型
|
||||||
|
func (nc *NotificationController) GetChannelTypes(c *gin.Context) {
|
||||||
|
utils.Success(c, gin.H{
|
||||||
|
"channel_types": services.SupportedChannelTypes,
|
||||||
|
"event_types": services.SupportedEvents,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannels 获取所有渠道
|
||||||
|
func (nc *NotificationController) GetChannels(c *gin.Context) {
|
||||||
|
channels := nc.notifyService.GetChannels()
|
||||||
|
utils.Success(c, channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveChannel 保存/更新渠道
|
||||||
|
func (nc *NotificationController) SaveChannel(c *gin.Context) {
|
||||||
|
var req services.NotifyChannel
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
utils.BadRequest(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" || req.Type == "" {
|
||||||
|
utils.BadRequest(c, "渠道名称和类型不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nc.notifyService.SaveChannel(req); err != nil {
|
||||||
|
utils.ServerError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SuccessMsg(c, "保存成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteChannel 删除渠道
|
||||||
|
func (nc *NotificationController) DeleteChannel(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
utils.BadRequest(c, "缺少渠道ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nc.notifyService.DeleteChannel(id); err != nil {
|
||||||
|
utils.ServerError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SuccessMsg(c, "删除成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestChannel 测试渠道
|
||||||
|
func (nc *NotificationController) TestChannel(c *gin.Context) {
|
||||||
|
var req services.NotifyChannel
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
utils.BadRequest(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := nc.notifyService.SendToChannel(req, &services.NotifyMessage{
|
||||||
|
Title: "🔔 白虎面板测试通知",
|
||||||
|
Text: "如果你看到这条消息,说明通知渠道配置正确!",
|
||||||
|
})
|
||||||
|
|
||||||
|
utils.Success(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GetBindings 获取事件绑定列表
|
||||||
|
func (nc *NotificationController) GetBindings(c *gin.Context) {
|
||||||
|
bindings := nc.notifyService.GetBindings()
|
||||||
|
utils.Success(c, bindings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveBinding 保存事件绑定
|
||||||
|
func (nc *NotificationController) SaveBinding(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
WayID string `json:"way_id"`
|
||||||
|
DataID string `json:"data_id"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
utils.BadRequest(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type == "" || req.Event == "" || req.WayID == "" {
|
||||||
|
utils.BadRequest(c, "类型、事件和渠道ID不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding := &models.NotifyBinding{
|
||||||
|
ID: req.ID,
|
||||||
|
Type: req.Type,
|
||||||
|
Event: req.Event,
|
||||||
|
WayID: req.WayID,
|
||||||
|
DataID: req.DataID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nc.notifyService.SaveBinding(binding); err != nil {
|
||||||
|
utils.ServerError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Success(c, binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBinding 删除事件绑定
|
||||||
|
func (nc *NotificationController) DeleteBinding(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
utils.BadRequest(c, "缺少绑定ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nc.notifyService.DeleteBinding(id); err != nil {
|
||||||
|
utils.ServerError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SuccessMsg(c, "删除成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNotification API 发送通知(供脚本调用)
|
||||||
|
func (nc *NotificationController) SendNotification(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
ChannelID string `json:"channel_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
utils.BadRequest(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ChannelID == "" || req.Title == "" {
|
||||||
|
utils.BadRequest(c, "channel_id 和 title 不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := nc.notifyService.SendByChannelID(req.ChannelID, &services.NotifyMessage{
|
||||||
|
Title: req.Title,
|
||||||
|
Text: req.Text,
|
||||||
|
})
|
||||||
|
|
||||||
|
utils.Success(c, result)
|
||||||
|
}
|
||||||
@@ -76,6 +76,10 @@ func (sc *SettingsController) ChangePassword(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go services.NewNotificationService().TriggerEvent(constant.BindingTypeSystem, constant.EventPasswordChanged, "", map[string]interface{}{
|
||||||
|
"username": user.Username,
|
||||||
|
})
|
||||||
|
|
||||||
utils.SuccessMsg(c, "密码修改成功")
|
utils.SuccessMsg(c, "密码修改成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,3 +377,39 @@ func (sc *SettingsController) RestoreBackup(c *gin.Context) {
|
|||||||
|
|
||||||
utils.SuccessMsg(c, "恢复成功")
|
utils.SuccessMsg(c, "恢复成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSetting 获取单个设置值
|
||||||
|
func (sc *SettingsController) GetSetting(c *gin.Context) {
|
||||||
|
section := c.Param("section")
|
||||||
|
key := c.Param("key")
|
||||||
|
|
||||||
|
if section == "" || key == "" {
|
||||||
|
utils.BadRequest(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value := sc.settingsService.Get(section, key)
|
||||||
|
utils.Success(c, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSettingToken 为指定设置生成随机token
|
||||||
|
func (sc *SettingsController) GenerateSettingToken(c *gin.Context) {
|
||||||
|
section := c.Param("section")
|
||||||
|
key := c.Param("key")
|
||||||
|
|
||||||
|
if section == "" || key == "" {
|
||||||
|
utils.BadRequest(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成32位随机token
|
||||||
|
token := strings.ToLower(utils.RandomString(32))
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
if err := sc.settingsService.Set(section, key, token); err != nil {
|
||||||
|
utils.ServerError(c, "保存失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Success(c, token)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ func Migrate() error {
|
|||||||
&models.Agent{},
|
&models.Agent{},
|
||||||
&models.AgentToken{},
|
&models.AgentToken{},
|
||||||
&models.Language{},
|
&models.Language{},
|
||||||
|
&models.NotifyWay{},
|
||||||
|
&models.NotifyBinding{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
|
"github.com/engigu/baihu-panel/internal/services"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifyTokenAuth 通知 Token 认证中间件
|
||||||
|
func NotifyTokenAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
token := c.GetHeader("notify-token")
|
||||||
|
if token == "" {
|
||||||
|
utils.Unauthorized(c, "缺少通知 Token")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 settings 表读取配置的通知 Token
|
||||||
|
settingsService := services.NewSettingsService()
|
||||||
|
savedToken := settingsService.Get(constant.SectionNotify, constant.KeyNotifyToken)
|
||||||
|
|
||||||
|
if savedToken == "" {
|
||||||
|
utils.Unauthorized(c, "通知 Token 未配置")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != savedToken {
|
||||||
|
utils.Unauthorized(c, "通知 Token 无效")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifyBinding 事件绑定表
|
||||||
|
type NotifyBinding struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
|
Type string `json:"type" gorm:"size:20;not null;index"` // system 或 task
|
||||||
|
Event string `json:"event" gorm:"size:50;not null;index"` // 事件类型
|
||||||
|
WayID string `json:"way_id" gorm:"size:20;not null;index"` // 通知渠道ID
|
||||||
|
DataID string `json:"data_id" gorm:"size:20;index"` // 关联ID,系统事件为空,任务事件为任务ID
|
||||||
|
CreatedAt LocalTime `json:"created_at"`
|
||||||
|
UpdatedAt LocalTime `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NotifyBinding) TableName() string {
|
||||||
|
return constant.TablePrefix + "notify_bindings"
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifyWay 消息推送渠道
|
||||||
|
type NotifyWay struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
|
Name string `json:"name" gorm:"size:100;not null"`
|
||||||
|
Type string `json:"type" gorm:"size:50;not null;index"`
|
||||||
|
Config string `json:"config" gorm:"type:text"`
|
||||||
|
Enabled bool `json:"enabled" gorm:"default:true;index"`
|
||||||
|
CreatedAt LocalTime `json:"created_at"`
|
||||||
|
UpdatedAt LocalTime `json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (NotifyWay) TableName() string {
|
||||||
|
return constant.TablePrefix + "notify_ways"
|
||||||
|
}
|
||||||
+16
-15
@@ -30,7 +30,7 @@ func RegisterControllers() *Controllers {
|
|||||||
// 清理 task 运行状态的任务可以直接由 executorService 承担或在此处通过 Database 直接清理
|
// 清理 task 运行状态的任务可以直接由 executorService 承担或在此处通过 Database 直接清理
|
||||||
// 简单期间,我们使用一个新方法 tasks.CleanupRunningTasks() 或者让 executorService 启动时清理
|
// 简单期间,我们使用一个新方法 tasks.CleanupRunningTasks() 或者让 executorService 启动时清理
|
||||||
|
|
||||||
executorService = tasks.NewExecutorService(taskService, taskLogService, agentWSManager, settingsService, envService)
|
executorService = tasks.NewExecutorService(taskService, taskLogService, agentWSManager, settingsService, envService, services.NewNotificationService())
|
||||||
// 启动时清理残留的运行状态
|
// 启动时清理残留的运行状态
|
||||||
_ = executorService.CleanupRunningTasks()
|
_ = executorService.CleanupRunningTasks()
|
||||||
|
|
||||||
@@ -39,20 +39,21 @@ func RegisterControllers() *Controllers {
|
|||||||
|
|
||||||
// 初始化并返回控制器
|
// 初始化并返回控制器
|
||||||
return &Controllers{
|
return &Controllers{
|
||||||
Task: controllers.NewTaskController(taskService, executorService),
|
Task: controllers.NewTaskController(taskService, executorService),
|
||||||
Auth: controllers.NewAuthController(userService, settingsService, loginLogService),
|
Auth: controllers.NewAuthController(userService, settingsService, loginLogService),
|
||||||
Env: controllers.NewEnvController(envService),
|
Env: controllers.NewEnvController(envService),
|
||||||
Script: controllers.NewScriptController(scriptService),
|
Script: controllers.NewScriptController(scriptService),
|
||||||
Executor: controllers.NewExecutorController(executorService),
|
Executor: controllers.NewExecutorController(executorService),
|
||||||
File: controllers.NewFileController(constant.ScriptsWorkDir),
|
File: controllers.NewFileController(constant.ScriptsWorkDir),
|
||||||
Dashboard: controllers.NewDashboardController(executorService),
|
Dashboard: controllers.NewDashboardController(executorService),
|
||||||
Log: controllers.NewLogController(),
|
Log: controllers.NewLogController(),
|
||||||
LogWS: controllers.NewLogWSController(),
|
LogWS: controllers.NewLogWSController(),
|
||||||
Terminal: controllers.NewTerminalController(envService),
|
Terminal: controllers.NewTerminalController(envService),
|
||||||
Settings: controllers.NewSettingsController(userService, loginLogService, executorService),
|
Settings: controllers.NewSettingsController(userService, loginLogService, executorService),
|
||||||
Dependency: controllers.NewDependencyController(),
|
Dependency: controllers.NewDependencyController(),
|
||||||
Agent: controllers.NewAgentController(settingsService),
|
Agent: controllers.NewAgentController(settingsService),
|
||||||
Mise: controllers.NewMiseController(services.NewMiseService()),
|
Mise: controllers.NewMiseController(services.NewMiseService()),
|
||||||
|
Notification: controllers.NewNotificationController(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ type Controllers struct {
|
|||||||
Terminal *controllers.TerminalController
|
Terminal *controllers.TerminalController
|
||||||
Settings *controllers.SettingsController
|
Settings *controllers.SettingsController
|
||||||
Dependency *controllers.DependencyController
|
Dependency *controllers.DependencyController
|
||||||
Agent *controllers.AgentController
|
Agent *controllers.AgentController
|
||||||
Mise *controllers.MiseController
|
Mise *controllers.MiseController
|
||||||
|
Notification *controllers.NotificationController
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustSubFS(fsys fs.FS, dir string) fs.FS {
|
func mustSubFS(fsys fs.FS, dir string) fs.FS {
|
||||||
@@ -199,6 +200,9 @@ func Setup(c *Controllers) *gin.Engine {
|
|||||||
settings.GET("/backup/status", c.Settings.GetBackupStatus)
|
settings.GET("/backup/status", c.Settings.GetBackupStatus)
|
||||||
settings.GET("/backup/download", c.Settings.DownloadBackup)
|
settings.GET("/backup/download", c.Settings.DownloadBackup)
|
||||||
settings.POST("/restore", c.Settings.RestoreBackup)
|
settings.POST("/restore", c.Settings.RestoreBackup)
|
||||||
|
// 通用设置接口
|
||||||
|
settings.GET("/:section/:key", c.Settings.GetSetting)
|
||||||
|
settings.POST("/:section/:key/generate", c.Settings.GenerateSettingToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dependency routes (依赖管理)
|
// Dependency routes (依赖管理)
|
||||||
@@ -246,6 +250,26 @@ func Setup(c *Controllers) *gin.Engine {
|
|||||||
{
|
{
|
||||||
agentAPIv1.GET("/download", c.Agent.Download)
|
agentAPIv1.GET("/download", c.Agent.Download)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知推送模块
|
||||||
|
notify := authorized.Group("/notify")
|
||||||
|
{
|
||||||
|
notify.GET("/types", c.Notification.GetChannelTypes)
|
||||||
|
notify.GET("/channels", c.Notification.GetChannels)
|
||||||
|
notify.POST("/channels", c.Notification.SaveChannel)
|
||||||
|
notify.DELETE("/channels/:id", c.Notification.DeleteChannel)
|
||||||
|
notify.POST("/channels/test", c.Notification.TestChannel)
|
||||||
|
notify.GET("/bindings", c.Notification.GetBindings)
|
||||||
|
notify.POST("/bindings", c.Notification.SaveBinding)
|
||||||
|
notify.DELETE("/bindings/:id", c.Notification.DeleteBinding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知发送 API(使用通知 Token 认证,供脚本调用)
|
||||||
|
notifyAPI := api.Group("/notify")
|
||||||
|
notifyAPI.Use(middleware.NotifyTokenAuth())
|
||||||
|
{
|
||||||
|
notifyAPI.POST("/send", c.Notification.SendNotification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type barkResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bark struct {
|
||||||
|
PushKey string
|
||||||
|
Archive string
|
||||||
|
Group string
|
||||||
|
Sound string
|
||||||
|
Icon string
|
||||||
|
Level string
|
||||||
|
URL string
|
||||||
|
Key string
|
||||||
|
IV string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bark) Request(title, content string) ([]byte, error) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"title": title,
|
||||||
|
"body": content,
|
||||||
|
}
|
||||||
|
if b.Archive != "" {
|
||||||
|
data["isArchive"] = b.Archive
|
||||||
|
}
|
||||||
|
if b.Group != "" {
|
||||||
|
data["group"] = b.Group
|
||||||
|
}
|
||||||
|
if b.Sound != "" {
|
||||||
|
data["sound"] = b.Sound
|
||||||
|
}
|
||||||
|
if b.Icon != "" {
|
||||||
|
data["icon"] = b.Icon
|
||||||
|
}
|
||||||
|
if b.Level != "" {
|
||||||
|
data["level"] = b.Level
|
||||||
|
}
|
||||||
|
if b.URL != "" {
|
||||||
|
data["url"] = b.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
var postData interface{}
|
||||||
|
url := b.getURL()
|
||||||
|
|
||||||
|
if b.Key != "" && b.IV != "" {
|
||||||
|
// Use encryption
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ciphertext, err := b.encryptPayload(string(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encryption failed: %v", err)
|
||||||
|
}
|
||||||
|
postData = map[string]interface{}{
|
||||||
|
"ciphertext": ciphertext,
|
||||||
|
"device_key": b.PushKey,
|
||||||
|
"sound": b.Sound,
|
||||||
|
}
|
||||||
|
// When using encryption, use the push endpoint if PushKey is just a key
|
||||||
|
if !strings.HasPrefix(b.PushKey, "http") {
|
||||||
|
url = "https://api.day.app/push"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal request
|
||||||
|
postData = data
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(postData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(url, "application/json;charset=utf-8", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var r barkResponse
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Code != 200 {
|
||||||
|
return body, fmt.Errorf("bark response error: %s", string(body))
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bark) getURL() string {
|
||||||
|
pushKey := b.PushKey
|
||||||
|
if strings.HasPrefix(pushKey, "http") {
|
||||||
|
return pushKey
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://api.day.app/%s", pushKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bark) encryptPayload(payload string) (string, error) {
|
||||||
|
key := []byte(b.Key)
|
||||||
|
iv := []byte(b.IV)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
paddedPayload := b.pkcs7Pad([]byte(payload), aes.BlockSize)
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
ciphertext := make([]byte, len(paddedPayload))
|
||||||
|
mode.CryptBlocks(ciphertext, paddedPayload)
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bark) pkcs7Pad(data []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(data)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(data, padtext...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomWebhook struct {
|
||||||
|
Webhook string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Client = &http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cw *CustomWebhook) Request(url string, msg string) ([]byte, error) {
|
||||||
|
resp, err := Client.Post(url, "application/json", bytes.NewBuffer([]byte(msg)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
err := Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
Code int `json:"errcode"`
|
||||||
|
Msg string `json:"errmsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dtalk struct {
|
||||||
|
AccessToken string
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Dtalk) Request(msg interface{}) ([]byte, error) {
|
||||||
|
b, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := http.Post(t.getURL(), "application/json", bytes.NewBuffer(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
err := Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var r response
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
if r.Code != 0 {
|
||||||
|
return body, fmt.Errorf("response error: %s", string(body))
|
||||||
|
}
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageText Function to send message
|
||||||
|
func (t *Dtalk) SendMessageText(text string, at ...string) ([]byte, error) {
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"msgtype": "text",
|
||||||
|
"text": map[string]string{
|
||||||
|
"content": text,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加@功能
|
||||||
|
if len(at) > 0 {
|
||||||
|
atMobiles := []string{}
|
||||||
|
isAtAll := false
|
||||||
|
|
||||||
|
for _, mobile := range at {
|
||||||
|
if mobile == "all" || mobile == "@all" {
|
||||||
|
isAtAll = true
|
||||||
|
} else {
|
||||||
|
atMobiles = append(atMobiles, mobile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg["at"] = map[string]interface{}{
|
||||||
|
"atMobiles": atMobiles,
|
||||||
|
"isAtAll": isAtAll,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := t.Request(msg)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Dtalk) SendMessageMarkdown(title, text string, at ...string) ([]byte, error) {
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"msgtype": "markdown",
|
||||||
|
"markdown": map[string]string{
|
||||||
|
"title": title,
|
||||||
|
"text": text,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加@功能
|
||||||
|
if len(at) > 0 {
|
||||||
|
atMobiles := []string{}
|
||||||
|
isAtAll := false
|
||||||
|
|
||||||
|
for _, mobile := range at {
|
||||||
|
if mobile == "all" || mobile == "@all" {
|
||||||
|
isAtAll = true
|
||||||
|
} else {
|
||||||
|
atMobiles = append(atMobiles, mobile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg["at"] = map[string]interface{}{
|
||||||
|
"atMobiles": atMobiles,
|
||||||
|
"isAtAll": isAtAll,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := t.Request(msg)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Dtalk) hmacSha256(stringToSign string, secret string) string {
|
||||||
|
h := hmac.New(sha256.New, []byte(secret))
|
||||||
|
h.Write([]byte(stringToSign))
|
||||||
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Dtalk) getURL() string {
|
||||||
|
wh := "https://oapi.dingtalk.com/robot/send?access_token=" + t.AccessToken
|
||||||
|
timestamp := time.Now().UnixNano() / 1e6
|
||||||
|
stringToSign := fmt.Sprintf("%d\n%s", timestamp, t.Secret)
|
||||||
|
sign := t.hmacSha256(stringToSign, t.Secret)
|
||||||
|
url := fmt.Sprintf("%s×tamp=%d&sign=%s", wh, timestamp, sign)
|
||||||
|
return url
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gopkg.in/gomail.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailMessage struct {
|
||||||
|
Server string
|
||||||
|
Port int
|
||||||
|
Account string
|
||||||
|
Passwd string
|
||||||
|
FromName string
|
||||||
|
GM *gomail.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EmailMessage) Init(host string, port int, account string, passwd string, fromName string) {
|
||||||
|
e.Server = host
|
||||||
|
e.Port = port
|
||||||
|
e.Account = account
|
||||||
|
e.Passwd = passwd
|
||||||
|
e.FromName = fromName
|
||||||
|
e.GM = gomail.NewDialer(host, port, account, passwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EmailMessage) SendTextMessage(toEmail string, title string, content string) string {
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
if e.FromName != "" {
|
||||||
|
m.SetAddressHeader("From", e.Account, e.FromName)
|
||||||
|
} else {
|
||||||
|
m.SetHeader("From", e.Account)
|
||||||
|
}
|
||||||
|
m.SetHeader("To", toEmail)
|
||||||
|
m.SetHeader("Subject", title)
|
||||||
|
m.SetBody("text/html", content)
|
||||||
|
|
||||||
|
if err := e.GM.DialAndSend(m); err != nil {
|
||||||
|
return fmt.Sprintf("邮件发送失败: %s", err)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EmailMessage) SendHtmlMessage(toEmail string, title string, content string) string {
|
||||||
|
return e.SendTextMessage(toEmail, title, content)
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type feishuResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feishu struct {
|
||||||
|
AccessToken string
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// genSign 生成飞书签名
|
||||||
|
func (f *Feishu) genSign(timestamp int64) string {
|
||||||
|
if f.Secret == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
stringToSign := fmt.Sprintf("%v\n%s", timestamp, f.Secret)
|
||||||
|
h := hmac.New(sha256.New, []byte(stringToSign))
|
||||||
|
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
return signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageText 发送文本消息
|
||||||
|
func (f *Feishu) SendMessageText(content string, atMobiles ...string) ([]byte, error) {
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
sign := f.genSign(timestamp)
|
||||||
|
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"timestamp": strconv.FormatInt(timestamp, 10),
|
||||||
|
"sign": sign,
|
||||||
|
"msg_type": "text",
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"text": content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.send(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageMarkdown 发送 Markdown 消息
|
||||||
|
func (f *Feishu) SendMessageMarkdown(title, content string, atMobiles ...string) ([]byte, error) {
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
sign := f.genSign(timestamp)
|
||||||
|
|
||||||
|
// 处理 @ 人员
|
||||||
|
atContent := ""
|
||||||
|
if len(atMobiles) > 0 {
|
||||||
|
for _, mobile := range atMobiles {
|
||||||
|
if mobile == "all" {
|
||||||
|
atContent += "<at user_id=\"all\">所有人</at>"
|
||||||
|
} else {
|
||||||
|
atContent += fmt.Sprintf("<at user_id=\"%s\"></at>", mobile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content = atContent + "\n" + content
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"timestamp": strconv.FormatInt(timestamp, 10),
|
||||||
|
"sign": sign,
|
||||||
|
"msg_type": "interactive",
|
||||||
|
"card": map[string]interface{}{
|
||||||
|
"header": map[string]interface{}{
|
||||||
|
"title": map[string]interface{}{
|
||||||
|
"tag": "plain_text",
|
||||||
|
"content": title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"elements": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"tag": "markdown",
|
||||||
|
"content": content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.send(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send 发送请求
|
||||||
|
func (f *Feishu) send(msg map[string]interface{}) ([]byte, error) {
|
||||||
|
url := fmt.Sprintf("https://open.feishu.cn/open-apis/bot/v2/hook/%s", f.AccessToken)
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("JSON序列化失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("发送请求失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result feishuResponse
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return body, fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return body, fmt.Errorf("飞书返回错误: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gotifyResponse struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
ErrorCode int `json:"errorCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Gotify struct {
|
||||||
|
Url string
|
||||||
|
Token string
|
||||||
|
Priority int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Gotify) Request(title, content string) ([]byte, error) {
|
||||||
|
// Construct the URL with token
|
||||||
|
u, err := url.Parse(fmt.Sprintf("%s/message", g.Url))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("token", g.Token)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"title": title,
|
||||||
|
"message": content,
|
||||||
|
"priority": g.Priority,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(u.String(), "application/json", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var r gotifyResponse
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Id == 0 {
|
||||||
|
return body, fmt.Errorf("gotify response error: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ntfy struct {
|
||||||
|
Url string
|
||||||
|
Topic string
|
||||||
|
Priority string
|
||||||
|
Icon string
|
||||||
|
Token string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Actions string
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeRFC2047(text string) string {
|
||||||
|
encoded := base64.StdEncoding.EncodeToString([]byte(text))
|
||||||
|
return fmt.Sprintf("=?utf-8?B?%s?=", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Ntfy) Request(title, content string) ([]byte, error) {
|
||||||
|
if n.Url == "" {
|
||||||
|
n.Url = "https://ntfy.sh"
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s/%s", n.Url, n.Topic)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBufferString(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Title", encodeRFC2047(title))
|
||||||
|
priority := n.Priority
|
||||||
|
if priority == "" {
|
||||||
|
priority = "3"
|
||||||
|
}
|
||||||
|
req.Header.Set("Priority", priority)
|
||||||
|
if n.Icon != "" {
|
||||||
|
req.Header.Set("Icon", n.Icon)
|
||||||
|
}
|
||||||
|
if n.Actions != "" {
|
||||||
|
req.Header.Set("Actions", encodeRFC2047(n.Actions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+n.Token)
|
||||||
|
} else if n.Username != "" && n.Password != "" {
|
||||||
|
authStr := n.Username + ":" + n.Password
|
||||||
|
encodedAuth := base64.StdEncoding.EncodeToString([]byte(authStr))
|
||||||
|
req.Header.Set("Authorization", "Basic "+encodedAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return body, fmt.Errorf("ntfy response error: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PushMe struct {
|
||||||
|
PushKey string
|
||||||
|
URL string
|
||||||
|
Date string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PushMe) Request(title, content string) (string, error) {
|
||||||
|
apiURL := p.URL
|
||||||
|
if apiURL == "" {
|
||||||
|
apiURL = "https://push.i-i.me/"
|
||||||
|
}
|
||||||
|
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("push_key", p.PushKey)
|
||||||
|
data.Set("title", title)
|
||||||
|
data.Set("content", content)
|
||||||
|
if p.Date != "" {
|
||||||
|
data.Set("date", p.Date)
|
||||||
|
}
|
||||||
|
if p.Type != "" {
|
||||||
|
data.Set("type", p.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Post(apiURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == 200 && string(body) == "success" {
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(body), fmt.Errorf("PushMe response error: %s", string(body))
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type qywxResponse struct {
|
||||||
|
Code int `json:"errcode"`
|
||||||
|
Msg string `json:"errmsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QyWeiXin struct {
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *QyWeiXin) Request(msg interface{}) ([]byte, error) {
|
||||||
|
b, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := http.Post(t.getURL(), "application/json", bytes.NewBuffer(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
err := Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var r qywxResponse
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
if r.Code != 0 {
|
||||||
|
return body, fmt.Errorf("response error: %s", string(body))
|
||||||
|
}
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageText Function to send message
|
||||||
|
func (t *QyWeiXin) SendMessageText(text string, at ...string) ([]byte, error) {
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"msgtype": "text",
|
||||||
|
"text": map[string]interface{}{
|
||||||
|
"content": text,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加@功能
|
||||||
|
// 企业微信支持两种@方式:
|
||||||
|
// 1. mentioned_list: userid列表或"@all"
|
||||||
|
// 2. mentioned_mobile_list: 手机号列表
|
||||||
|
if len(at) > 0 {
|
||||||
|
mentionedList := []string{}
|
||||||
|
mentionedMobileList := []string{}
|
||||||
|
|
||||||
|
for _, item := range at {
|
||||||
|
if item == "@all" || item == "all" {
|
||||||
|
mentionedList = append(mentionedList, "@all")
|
||||||
|
} else if len(item) == 11 && item[0] == '1' {
|
||||||
|
// 判断是否为手机号(简单判断:11位且以1开头)
|
||||||
|
mentionedMobileList = append(mentionedMobileList, item)
|
||||||
|
} else {
|
||||||
|
// 否则当作userid处理
|
||||||
|
mentionedList = append(mentionedList, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textContent := msg["text"].(map[string]interface{})
|
||||||
|
if len(mentionedList) > 0 {
|
||||||
|
textContent["mentioned_list"] = mentionedList
|
||||||
|
}
|
||||||
|
if len(mentionedMobileList) > 0 {
|
||||||
|
textContent["mentioned_mobile_list"] = mentionedMobileList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := t.Request(msg)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *QyWeiXin) SendMessageMarkdown(title, text string, at ...string) ([]byte, error) {
|
||||||
|
msg := map[string]interface{}{
|
||||||
|
"msgtype": "markdown",
|
||||||
|
"markdown": map[string]interface{}{
|
||||||
|
"content": text,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业微信Markdown消息不支持@功能,但可以在内容中手动添加
|
||||||
|
// 如果需要@功能,建议使用text类型
|
||||||
|
|
||||||
|
resp, err := t.Request(msg)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *QyWeiXin) getURL() string {
|
||||||
|
url := "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + t.AccessToken
|
||||||
|
return url
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type telegramResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Telegram struct {
|
||||||
|
BotToken string
|
||||||
|
ChatID string
|
||||||
|
ApiHost string // 可选的自定义API地址(优先级最高)
|
||||||
|
ProxyURL string // 可选的代理地址,支持 http://、https://、socks5:// 格式
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Telegram) Request(params map[string]interface{}) ([]byte, error) {
|
||||||
|
apiURL := t.getAPIURL()
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
data := url.Values{}
|
||||||
|
for key, value := range params {
|
||||||
|
data.Set(key, fmt.Sprintf("%v", value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HTTP 客户端
|
||||||
|
client := t.getHTTPClient()
|
||||||
|
|
||||||
|
resp, err := client.Post(apiURL, "application/x-www-form-urlencoded", bytes.NewBufferString(data.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
err := Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
// 忽略关闭错误
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var r telegramResponse
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Ok {
|
||||||
|
return body, fmt.Errorf("telegram api error: %s", r.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageText 发送文本消息
|
||||||
|
func (t *Telegram) SendMessageText(text string) ([]byte, error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"chat_id": t.ChatID,
|
||||||
|
"text": text,
|
||||||
|
"disable_web_page_preview": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Request(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageMarkdown 发送Markdown格式消息
|
||||||
|
func (t *Telegram) SendMessageMarkdown(text string) ([]byte, error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"chat_id": t.ChatID,
|
||||||
|
"text": text,
|
||||||
|
"parse_mode": "Markdown",
|
||||||
|
"disable_web_page_preview": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Request(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessageHTML 发送HTML格式消息
|
||||||
|
func (t *Telegram) SendMessageHTML(text string) ([]byte, error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"chat_id": t.ChatID,
|
||||||
|
"text": text,
|
||||||
|
"parse_mode": "HTML",
|
||||||
|
"disable_web_page_preview": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Request(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Telegram) getAPIURL() string {
|
||||||
|
// 自定义 API 地址优先级最高
|
||||||
|
if t.ApiHost != "" {
|
||||||
|
return fmt.Sprintf("%s/bot%s/sendMessage", t.ApiHost, t.BotToken)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", t.BotToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHTTPClient 获取配置了代理的 HTTP 客户端
|
||||||
|
func (t *Telegram) getHTTPClient() *http.Client {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果配置了代理且没有自定义 API 地址,则使用代理
|
||||||
|
// 自定义 API 地址优先级更高,通常用于自建代理服务器
|
||||||
|
if t.ProxyURL != "" && t.ApiHost == "" {
|
||||||
|
proxyURL, err := url.Parse(t.ProxyURL)
|
||||||
|
if err == nil {
|
||||||
|
// 判断是否为 SOCKS5 代理
|
||||||
|
if strings.HasPrefix(strings.ToLower(t.ProxyURL), "socks5://") {
|
||||||
|
// 使用 SOCKS5 代理
|
||||||
|
dialer, err := t.createSOCKS5Dialer(proxyURL)
|
||||||
|
if err == nil {
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
DialContext: dialer.DialContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用 HTTP/HTTPS 代理
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxyURL),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSOCKS5Dialer 创建 SOCKS5 代理拨号器
|
||||||
|
func (t *Telegram) createSOCKS5Dialer(proxyURL *url.URL) (proxy.ContextDialer, error) {
|
||||||
|
// 解析代理地址
|
||||||
|
host := proxyURL.Host
|
||||||
|
|
||||||
|
// 检查是否有认证信息
|
||||||
|
var auth *proxy.Auth
|
||||||
|
if proxyURL.User != nil {
|
||||||
|
password, _ := proxyURL.User.Password()
|
||||||
|
auth = &proxy.Auth{
|
||||||
|
User: proxyURL.User.Username(),
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建基础拨号器
|
||||||
|
baseDialer := &net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 SOCKS5 拨号器
|
||||||
|
dialer, err := proxy.SOCKS5("tcp", host, auth, baseDialer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 ContextDialer
|
||||||
|
contextDialer, ok := dialer.(proxy.ContextDialer)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("failed to convert to ContextDialer")
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextDialer, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/silenceper/wechat/v2"
|
||||||
|
"github.com/silenceper/wechat/v2/cache"
|
||||||
|
offConfig "github.com/silenceper/wechat/v2/officialaccount/config"
|
||||||
|
"github.com/silenceper/wechat/v2/officialaccount/message"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WeChatOFAccount struct {
|
||||||
|
AppID string
|
||||||
|
AppSecret string
|
||||||
|
ToUser string
|
||||||
|
TemplateID string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用内存缓存进行token的存储
|
||||||
|
var memory = cache.NewMemory()
|
||||||
|
|
||||||
|
func (cw *WeChatOFAccount) Send(title string, content string) (string, error) {
|
||||||
|
wc := wechat.NewWechat()
|
||||||
|
cfg := &offConfig.Config{
|
||||||
|
AppID: cw.AppID,
|
||||||
|
AppSecret: cw.AppSecret,
|
||||||
|
Cache: memory,
|
||||||
|
}
|
||||||
|
officialAccount := wc.GetOfficialAccount(cfg)
|
||||||
|
|
||||||
|
// 获取 Access Token
|
||||||
|
_, err := officialAccount.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("获取access token失败:%s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
msgData := make(map[string]*message.TemplateDataItem)
|
||||||
|
msgData["content"] = &message.TemplateDataItem{
|
||||||
|
Value: content,
|
||||||
|
}
|
||||||
|
msgData["title"] = &message.TemplateDataItem{
|
||||||
|
Value: title,
|
||||||
|
//Color: "#173177",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建模板消息
|
||||||
|
templateMessage := &message.TemplateMessage{
|
||||||
|
ToUser: cw.ToUser,
|
||||||
|
TemplateID: cw.TemplateID,
|
||||||
|
URL: cw.URL,
|
||||||
|
Data: msgData,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送模板消息
|
||||||
|
_, err = officialAccount.GetTemplate().Send(templateMessage)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("发送模板消息失败: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
//logrus.Infof("模板消息发送成功。 消息ID: %d", msgID)
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# Messenger SDK
|
||||||
|
|
||||||
|
统一消息发送 SDK,可独立于 Message-Push-Nest 业务层使用。
|
||||||
|
|
||||||
|
## 特点
|
||||||
|
|
||||||
|
- **零业务依赖**:不依赖数据库、路由、认证等业务代码
|
||||||
|
- **开箱即用**:直接传配置 + 消息即可发送
|
||||||
|
- **支持 12 种渠道**:Email, 钉钉, 企业微信, 飞书, Telegram, Bark, Ntfy, Gotify, PushMe, 自定义Webhook, 微信公众号, 阿里云短信
|
||||||
|
- **可扩展**:通过 `RegisterChannel` 注册自定义渠道
|
||||||
|
|
||||||
|
## 快速使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "message-nest/pkg/sdk/messenger"
|
||||||
|
|
||||||
|
// 方式1: 直接发送
|
||||||
|
result, err := messenger.Send("Telegram", messenger.ChannelConfig{
|
||||||
|
"bot_token": "your-bot-token",
|
||||||
|
"chat_id": "123456",
|
||||||
|
}, &messenger.Message{
|
||||||
|
Title: "告警通知",
|
||||||
|
Text: "服务器 CPU 超过 90%",
|
||||||
|
Markdown: "**服务器 CPU** 超过 `90%`",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
log.Printf("发送失败: %s", result.Error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 Client(预设默认配置)
|
||||||
|
|
||||||
|
```go
|
||||||
|
client := messenger.NewClient()
|
||||||
|
|
||||||
|
// 预设 Telegram 配置
|
||||||
|
client.SetDefaultConfig("Telegram", messenger.ChannelConfig{
|
||||||
|
"bot_token": "your-bot-token",
|
||||||
|
"chat_id": "default-chat",
|
||||||
|
})
|
||||||
|
|
||||||
|
// 后续发送可以覆盖部分配置,也可以传 nil 使用全部默认值
|
||||||
|
result, err := client.Send("Telegram", nil, &messenger.Message{
|
||||||
|
Title: "Hello",
|
||||||
|
Text: "World",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 各渠道配置参数
|
||||||
|
|
||||||
|
### Email
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `server` | ✅ | SMTP 服务地址 |
|
||||||
|
| `port` | ✅ | SMTP 端口 |
|
||||||
|
| `account` | ✅ | 邮箱账号 |
|
||||||
|
| `passwd` | ✅ | 邮箱密码 |
|
||||||
|
| `from_name` | ❌ | 发信人名称 |
|
||||||
|
| `to_account` | ✅ | 收件邮箱 |
|
||||||
|
|
||||||
|
### Dtalk(钉钉)
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `access_token` | ✅ | 钉钉 access_token |
|
||||||
|
| `secret` | ❌ | 加签秘钥 |
|
||||||
|
|
||||||
|
### QyWeiXin(企业微信)
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `access_token` | ✅ | 企业微信 access_token |
|
||||||
|
|
||||||
|
### Feishu(飞书)
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `access_token` | ✅ | 飞书 access_token |
|
||||||
|
| `secret` | ❌ | 加签秘钥 |
|
||||||
|
|
||||||
|
### Telegram
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `bot_token` | ✅ | Bot Token |
|
||||||
|
| `chat_id` | ✅ | Chat ID |
|
||||||
|
| `api_host` | ❌ | 自定义 API 地址 |
|
||||||
|
| `proxy_url` | ❌ | 代理地址 (http/https/socks5) |
|
||||||
|
|
||||||
|
### Bark
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `push_key` | ✅ | Bark Push Key |
|
||||||
|
| `archive` | ❌ | 是否存档 |
|
||||||
|
| `group` | ❌ | 推送分组 |
|
||||||
|
| `sound` | ❌ | 推送声音 |
|
||||||
|
| `icon` | ❌ | 推送图标 |
|
||||||
|
| `level` | ❌ | 时效性 |
|
||||||
|
| `url` | ❌ | 跳转URL |
|
||||||
|
| `key` | ❌ | 加密Key |
|
||||||
|
| `iv` | ❌ | 加密IV |
|
||||||
|
|
||||||
|
### Ntfy
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `topic` | ✅ | Topic |
|
||||||
|
| `url` | ❌ | 自定义 API 地址 |
|
||||||
|
| `priority` | ❌ | 优先级 |
|
||||||
|
| `icon` | ❌ | 图标 URL |
|
||||||
|
| `token` | ❌ | Token |
|
||||||
|
| `username` | ❌ | 用户名 |
|
||||||
|
| `password` | ❌ | 密码 |
|
||||||
|
| `actions` | ❌ | Actions |
|
||||||
|
|
||||||
|
### Gotify
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `url` | ✅ | Gotify 服务地址 |
|
||||||
|
| `token` | ✅ | Token |
|
||||||
|
| `priority` | ❌ | 优先级 |
|
||||||
|
|
||||||
|
### PushMe
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `push_key` | ✅ | PushMe Push Key |
|
||||||
|
| `url` | ❌ | 自定义 API 地址 |
|
||||||
|
| `date` | ❌ | 日期 |
|
||||||
|
| `type` | ❌ | 类型 |
|
||||||
|
|
||||||
|
### Custom(自定义Webhook)
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `webhook` | ✅ | Webhook URL |
|
||||||
|
| `body` | ❌ | 请求体模板(`TEXT` 占位符会被替换) |
|
||||||
|
|
||||||
|
### WeChatOFAccount(微信公众号)
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `appID` | ✅ | 公众号 AppID |
|
||||||
|
| `appsecret` | ✅ | 公众号 AppSecret |
|
||||||
|
| `tempid` | ❌ | 模板消息 ID |
|
||||||
|
| `to_account` | ✅ | 接收者 OpenID |
|
||||||
|
|
||||||
|
### AliyunSMS(阿里云短信)
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `access_key_id` | ✅ | AccessKeyId |
|
||||||
|
| `access_key_secret` | ✅ | AccessKeySecret |
|
||||||
|
| `sign_name` | ✅ | 短信签名 |
|
||||||
|
| `region_id` | ❌ | 区域ID(默认 cn-hangzhou) |
|
||||||
|
| `phone_number` | ✅ | 手机号码 |
|
||||||
|
| `template_code` | ✅ | 短信模板 CODE |
|
||||||
|
|
||||||
|
## 自定义扩展
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 注册自定义渠道
|
||||||
|
messenger.RegisterChannel("MyChannel", func() messenger.Channel {
|
||||||
|
return &myCustomChannel{}
|
||||||
|
})
|
||||||
|
```
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
type AliyunSMSChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewAliyunSMSChannel() Channel {
|
||||||
|
return &AliyunSMSChannel{NewBaseChannel(ChannelAliyunSMS, []string{FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AliyunSMSChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
accessKeyId := config.GetString("access_key_id")
|
||||||
|
accessKeySecret := config.GetString("access_key_secret")
|
||||||
|
signName := config.GetString("sign_name")
|
||||||
|
regionId := config.GetString("region_id")
|
||||||
|
phoneNumber := config.GetString("phone_number")
|
||||||
|
templateCode := config.GetString("template_code")
|
||||||
|
|
||||||
|
if accessKeyId == "" || accessKeySecret == "" || signName == "" {
|
||||||
|
return SendError("aliyun sms config missing: access_key_id, access_key_secret, sign_name are required"), nil
|
||||||
|
}
|
||||||
|
if phoneNumber == "" || templateCode == "" {
|
||||||
|
return SendError("aliyun sms config missing: phone_number, template_code are required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, formattedContent := c.FormatContent(msg)
|
||||||
|
|
||||||
|
if regionId == "" {
|
||||||
|
regionId = "cn-hangzhou"
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := createAliyunSMSClient(accessKeyId, accessKeySecret, regionId)
|
||||||
|
if err != nil {
|
||||||
|
return SendError("创建阿里云短信客户端失败: %s", err.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := sendAliyunSMS(client, phoneNumber, signName, templateCode, formattedContent, msg.Extra)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult("", err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(result), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type BarkChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewBarkChannel() Channel {
|
||||||
|
return &BarkChannel{NewBaseChannel(ChannelBark, []string{FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BarkChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
pushKey := config.GetString("push_key")
|
||||||
|
if pushKey == "" {
|
||||||
|
return SendError("bark config missing: push_key is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := message.Bark{
|
||||||
|
PushKey: pushKey,
|
||||||
|
Archive: config.GetString("archive"),
|
||||||
|
Group: config.GetString("group"),
|
||||||
|
Sound: config.GetString("sound"),
|
||||||
|
Icon: config.GetString("icon"),
|
||||||
|
Level: config.GetString("level"),
|
||||||
|
URL: config.GetString("url"),
|
||||||
|
Key: config.GetString("key"),
|
||||||
|
IV: config.GetString("iv"),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := cli.Request(msg.Title, msg.Text)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Channel 渠道接口 - SDK 版本,零业务依赖
|
||||||
|
type Channel interface {
|
||||||
|
// GetType 返回渠道类型标识
|
||||||
|
GetType() string
|
||||||
|
// GetSupportedFormats 返回支持的消息格式
|
||||||
|
GetSupportedFormats() []string
|
||||||
|
// Send 发送消息
|
||||||
|
Send(config ChannelConfig, msg *Message) (*Result, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseChannel 渠道基础实现
|
||||||
|
type BaseChannel struct {
|
||||||
|
channelType string
|
||||||
|
supportedFormats []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBaseChannel(channelType string, supportedFormats []string) *BaseChannel {
|
||||||
|
return &BaseChannel{channelType: channelType, supportedFormats: supportedFormats}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BaseChannel) GetType() string { return c.channelType }
|
||||||
|
func (c *BaseChannel) GetSupportedFormats() []string { return c.supportedFormats }
|
||||||
|
|
||||||
|
// FormatContent 根据渠道支持的格式选择最佳内容
|
||||||
|
func (c *BaseChannel) FormatContent(msg *Message) (formatType string, content string) {
|
||||||
|
for _, ft := range c.supportedFormats {
|
||||||
|
switch ft {
|
||||||
|
case FormatTypeMarkdown:
|
||||||
|
if msg.HasMarkdown() {
|
||||||
|
return FormatTypeMarkdown, msg.Markdown
|
||||||
|
}
|
||||||
|
case FormatTypeHTML:
|
||||||
|
if msg.HasHTML() {
|
||||||
|
return FormatTypeHTML, msg.HTML
|
||||||
|
}
|
||||||
|
case FormatTypeText:
|
||||||
|
if msg.HasText() {
|
||||||
|
return FormatTypeText, msg.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msg.HasText() {
|
||||||
|
return FormatTypeText, msg.Text
|
||||||
|
}
|
||||||
|
return FormatTypeText, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuccessResult 创建成功结果
|
||||||
|
func SuccessResult(response string) *Result {
|
||||||
|
return &Result{Success: true, Response: response}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorResult 创建失败结果
|
||||||
|
func ErrorResult(response string, err error) *Result {
|
||||||
|
errMsg := ""
|
||||||
|
if err != nil {
|
||||||
|
errMsg = err.Error()
|
||||||
|
}
|
||||||
|
return &Result{Success: false, Response: response, Error: errMsg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorResultStr 创建失败结果(字符串错误)
|
||||||
|
func ErrorResultStr(response string, errMsg string) *Result {
|
||||||
|
return &Result{Success: false, Response: response, Error: errMsg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendError 发送失败时的格式化错误
|
||||||
|
func SendError(format string, args ...any) *Result {
|
||||||
|
return &Result{Success: false, Error: fmt.Sprintf(format, args...)}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type CustomChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewCustomChannel() Channel {
|
||||||
|
return &CustomChannel{NewBaseChannel(ChannelCustom, []string{FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CustomChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
webhook := config.GetString("webhook")
|
||||||
|
body := config.GetString("body")
|
||||||
|
|
||||||
|
if webhook == "" {
|
||||||
|
return SendError("custom config missing: webhook is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, formattedContent := c.FormatContent(msg)
|
||||||
|
cli := message.CustomWebhook{}
|
||||||
|
|
||||||
|
// 替换 body 模板中的 TEXT 占位符
|
||||||
|
bodyStr := body
|
||||||
|
if bodyStr != "" {
|
||||||
|
bodyStr = replaceBodyPlaceholder(bodyStr, formattedContent)
|
||||||
|
} else {
|
||||||
|
bodyStr = formattedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := cli.Request(webhook, bodyStr)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type DtalkChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewDtalkChannel() Channel {
|
||||||
|
return &DtalkChannel{NewBaseChannel(ChannelDtalk, []string{FormatTypeMarkdown, FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DtalkChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
accessToken := config.GetString("access_token")
|
||||||
|
secret := config.GetString("secret")
|
||||||
|
|
||||||
|
if accessToken == "" {
|
||||||
|
return SendError("dtalk config missing: access_token is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, formattedContent := c.FormatContent(msg)
|
||||||
|
atMobiles := msg.GetAtMobiles()
|
||||||
|
if msg.AtAll {
|
||||||
|
atMobiles = append(atMobiles, "all")
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := message.Dtalk{AccessToken: accessToken, Secret: secret}
|
||||||
|
var res []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if contentType == FormatTypeText {
|
||||||
|
res, err = cli.SendMessageText(formattedContent, atMobiles...)
|
||||||
|
} else if contentType == FormatTypeMarkdown {
|
||||||
|
res, err = cli.SendMessageMarkdown(msg.Title, formattedContent, atMobiles...)
|
||||||
|
} else {
|
||||||
|
return SendError("未知的钉钉发送内容类型:%s", contentType), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewEmailChannel() Channel {
|
||||||
|
return &EmailChannel{NewBaseChannel(ChannelEmail, []string{FormatTypeHTML, FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EmailChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
server := config.GetString("server")
|
||||||
|
portStr := config.GetString("port")
|
||||||
|
account := config.GetString("account")
|
||||||
|
passwd := config.GetString("passwd")
|
||||||
|
fromName := config.GetString("from_name")
|
||||||
|
toAccount := config.GetString("to_account")
|
||||||
|
|
||||||
|
if server == "" || account == "" || passwd == "" {
|
||||||
|
return SendError("email config missing: server, account, passwd are required"), nil
|
||||||
|
}
|
||||||
|
if toAccount == "" {
|
||||||
|
return SendError("email config missing: to_account is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
contentType, formattedContent := c.FormatContent(msg)
|
||||||
|
|
||||||
|
var emailer message.EmailMessage
|
||||||
|
emailer.Init(server, port, account, passwd, fromName)
|
||||||
|
|
||||||
|
var errMsg string
|
||||||
|
if contentType == FormatTypeText {
|
||||||
|
errMsg = emailer.SendTextMessage(toAccount, msg.Title, formattedContent)
|
||||||
|
} else if contentType == FormatTypeHTML {
|
||||||
|
errMsg = emailer.SendHtmlMessage(toAccount, msg.Title, formattedContent)
|
||||||
|
} else {
|
||||||
|
errMsg = fmt.Sprintf("未知的邮件发送内容类型:%s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errMsg != "" {
|
||||||
|
return ErrorResultStr("", errMsg), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(""), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type FeishuChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewFeishuChannel() Channel {
|
||||||
|
return &FeishuChannel{NewBaseChannel(ChannelFeishu, []string{FormatTypeMarkdown, FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *FeishuChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
accessToken := config.GetString("access_token")
|
||||||
|
secret := config.GetString("secret")
|
||||||
|
|
||||||
|
if accessToken == "" {
|
||||||
|
return SendError("feishu config missing: access_token is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, formattedContent := c.FormatContent(msg)
|
||||||
|
atMobiles := msg.GetAtMobiles()
|
||||||
|
atUserIds := msg.GetAtUserIds()
|
||||||
|
atList := append(atMobiles, atUserIds...)
|
||||||
|
if msg.AtAll {
|
||||||
|
atList = append(atList, "all")
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := message.Feishu{AccessToken: accessToken, Secret: secret}
|
||||||
|
var res []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if contentType == FormatTypeText {
|
||||||
|
res, err = cli.SendMessageText(formattedContent, atList...)
|
||||||
|
} else if contentType == FormatTypeMarkdown {
|
||||||
|
res, err = cli.SendMessageMarkdown(msg.Title, formattedContent, atList...)
|
||||||
|
} else {
|
||||||
|
return SendError("未知的飞书发送内容类型:%s", contentType), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GotifyChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewGotifyChannel() Channel {
|
||||||
|
return &GotifyChannel{NewBaseChannel(ChannelGotify, []string{FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GotifyChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
url := config.GetString("url")
|
||||||
|
token := config.GetString("token")
|
||||||
|
|
||||||
|
if url == "" || token == "" {
|
||||||
|
return SendError("gotify config missing: url and token are required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
priority, _ := strconv.Atoi(config.GetString("priority"))
|
||||||
|
cli := message.Gotify{
|
||||||
|
Url: url,
|
||||||
|
Token: token,
|
||||||
|
Priority: priority,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := cli.Request(msg.Title, msg.Text)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// replaceBodyPlaceholder 替换自定义 webhook body 中的 TEXT 占位符
|
||||||
|
func replaceBodyPlaceholder(body string, content string) string {
|
||||||
|
data, _ := json.Marshal(content)
|
||||||
|
dataStr := strings.Trim(string(data), "\"")
|
||||||
|
return strings.Replace(body, "TEXT", dataStr, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createAliyunSMSClient 创建阿里云短信客户端
|
||||||
|
func createAliyunSMSClient(accessKeyId, accessKeySecret, regionId string) (*dysmsapi.Client, error) {
|
||||||
|
return dysmsapi.NewClientWithAccessKey(regionId, accessKeyId, accessKeySecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendAliyunSMS 发送短信
|
||||||
|
func sendAliyunSMS(client *dysmsapi.Client, phoneNumber, signName, templateCode, content string, extra map[string]any) (string, error) {
|
||||||
|
templateParam := map[string]interface{}{
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
if extra != nil {
|
||||||
|
for k, v := range extra {
|
||||||
|
templateParam[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templateParamJSON, _ := json.Marshal(templateParam)
|
||||||
|
|
||||||
|
request := dysmsapi.CreateSendSmsRequest()
|
||||||
|
request.Scheme = "https"
|
||||||
|
request.PhoneNumbers = phoneNumber
|
||||||
|
request.SignName = signName
|
||||||
|
request.TemplateCode = templateCode
|
||||||
|
request.TemplateParam = string(templateParamJSON)
|
||||||
|
|
||||||
|
response, err := client.SendSms(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("发送短信失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Code != "OK" {
|
||||||
|
return "", fmt.Errorf("发送失败: %s - %s", response.Code, response.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("RequestId: %s, BizId: %s", response.RequestId, response.BizId), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type NtfyChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewNtfyChannel() Channel {
|
||||||
|
return &NtfyChannel{NewBaseChannel(ChannelNtfy, []string{FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NtfyChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
topic := config.GetString("topic")
|
||||||
|
if topic == "" {
|
||||||
|
return SendError("ntfy config missing: topic is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := message.Ntfy{
|
||||||
|
Url: config.GetString("url"),
|
||||||
|
Topic: topic,
|
||||||
|
Priority: config.GetString("priority"),
|
||||||
|
Icon: config.GetString("icon"),
|
||||||
|
Token: config.GetString("token"),
|
||||||
|
Username: config.GetString("username"),
|
||||||
|
Password: config.GetString("password"),
|
||||||
|
Actions: config.GetString("actions"),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := cli.Request(msg.Title, msg.Text)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type PushMeChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewPushMeChannel() Channel {
|
||||||
|
return &PushMeChannel{NewBaseChannel(ChannelPushMe, []string{FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PushMeChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
pushKey := config.GetString("push_key")
|
||||||
|
if pushKey == "" {
|
||||||
|
return SendError("pushme config missing: push_key is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := message.PushMe{
|
||||||
|
PushKey: pushKey,
|
||||||
|
URL: config.GetString("url"),
|
||||||
|
Date: config.GetString("date"),
|
||||||
|
Type: config.GetString("type"),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := cli.Request(msg.Title, msg.Text)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(res, err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(res), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type QyWeiXinChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewQyWeiXinChannel() Channel {
|
||||||
|
return &QyWeiXinChannel{NewBaseChannel(ChannelQyWeiXin, []string{FormatTypeMarkdown, FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *QyWeiXinChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
accessToken := config.GetString("access_token")
|
||||||
|
|
||||||
|
if accessToken == "" {
|
||||||
|
return SendError("qyweixin config missing: access_token is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, formattedContent := c.FormatContent(msg)
|
||||||
|
atList := []string{}
|
||||||
|
atList = append(atList, msg.GetAtUserIds()...)
|
||||||
|
atList = append(atList, msg.GetAtMobiles()...)
|
||||||
|
if msg.AtAll {
|
||||||
|
atList = append(atList, "@all")
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := message.QyWeiXin{AccessToken: accessToken}
|
||||||
|
var res []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if contentType == FormatTypeText {
|
||||||
|
res, err = cli.SendMessageText(formattedContent, atList...)
|
||||||
|
} else if contentType == FormatTypeMarkdown {
|
||||||
|
res, err = cli.SendMessageMarkdown(msg.Title, formattedContent, atList...)
|
||||||
|
} else {
|
||||||
|
return SendError("未知的企业微信发送内容类型:%s", contentType), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type TelegramChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewTelegramChannel() Channel {
|
||||||
|
return &TelegramChannel{NewBaseChannel(ChannelTelegram, []string{FormatTypeMarkdown, FormatTypeHTML, FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
botToken := config.GetString("bot_token")
|
||||||
|
chatID := config.GetString("chat_id")
|
||||||
|
apiHost := config.GetString("api_host")
|
||||||
|
proxyURL := config.GetString("proxy_url")
|
||||||
|
|
||||||
|
if botToken == "" || chatID == "" {
|
||||||
|
return SendError("telegram config missing: bot_token, chat_id are required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, formattedContent := c.FormatContent(msg)
|
||||||
|
cli := message.Telegram{
|
||||||
|
BotToken: botToken,
|
||||||
|
ChatID: chatID,
|
||||||
|
ApiHost: apiHost,
|
||||||
|
ProxyURL: proxyURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
var res []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case FormatTypeText:
|
||||||
|
res, err = cli.SendMessageText(formattedContent)
|
||||||
|
case FormatTypeMarkdown:
|
||||||
|
res, err = cli.SendMessageMarkdown(formattedContent)
|
||||||
|
case FormatTypeHTML:
|
||||||
|
res, err = cli.SendMessageHTML(formattedContent)
|
||||||
|
default:
|
||||||
|
return SendError("未知的Telegram发送内容类型:%s", contentType), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(string(res), err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(string(res)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
// Message 统一消息内容
|
||||||
|
type Message struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
HTML string `json:"html"`
|
||||||
|
Markdown string `json:"markdown"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
AtMobiles []string `json:"at_mobiles"`
|
||||||
|
AtUserIds []string `json:"at_user_ids"`
|
||||||
|
AtAll bool `json:"at_all"`
|
||||||
|
Extra map[string]any `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) HasText() bool { return m.Text != "" }
|
||||||
|
func (m *Message) HasHTML() bool { return m.HTML != "" }
|
||||||
|
func (m *Message) HasMarkdown() bool { return m.Markdown != "" }
|
||||||
|
|
||||||
|
func (m *Message) GetAtMobiles() []string {
|
||||||
|
if m.AtMobiles == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return m.AtMobiles
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) GetAtUserIds() []string {
|
||||||
|
if m.AtUserIds == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
return m.AtUserIds
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelConfig 渠道认证配置(Key-Value 形式,各渠道自行定义字段)
|
||||||
|
type ChannelConfig map[string]string
|
||||||
|
|
||||||
|
// GetString 安全获取配置值
|
||||||
|
func (c ChannelConfig) GetString(key string) string {
|
||||||
|
if v, ok := c[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result 发送结果
|
||||||
|
type Result struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Response string `json:"response"` // 原始响应
|
||||||
|
Error string `json:"error"` // 错误信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息格式类型常量
|
||||||
|
const (
|
||||||
|
FormatTypeText = "text"
|
||||||
|
FormatTypeHTML = "html"
|
||||||
|
FormatTypeMarkdown = "markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 渠道类型常量
|
||||||
|
const (
|
||||||
|
ChannelEmail = "Email"
|
||||||
|
ChannelDtalk = "Dtalk"
|
||||||
|
ChannelQyWeiXin = "QyWeiXin"
|
||||||
|
ChannelFeishu = "Feishu"
|
||||||
|
ChannelCustom = "Custom"
|
||||||
|
ChannelWeChatOFAccount = "WeChatOFAccount"
|
||||||
|
ChannelAliyunSMS = "AliyunSMS"
|
||||||
|
ChannelTelegram = "Telegram"
|
||||||
|
ChannelBark = "Bark"
|
||||||
|
ChannelPushMe = "PushMe"
|
||||||
|
ChannelNtfy = "Ntfy"
|
||||||
|
ChannelGotify = "Gotify"
|
||||||
|
)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import "github.com/engigu/baihu-panel/internal/sdk/message"
|
||||||
|
|
||||||
|
type WeChatOFAccountChannel struct{ *BaseChannel }
|
||||||
|
|
||||||
|
func NewWeChatOFAccountChannel() Channel {
|
||||||
|
return &WeChatOFAccountChannel{NewBaseChannel(ChannelWeChatOFAccount, []string{FormatTypeText})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WeChatOFAccountChannel) Send(config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
appID := config.GetString("appID")
|
||||||
|
appSecret := config.GetString("appsecret")
|
||||||
|
tempID := config.GetString("tempid")
|
||||||
|
toAccount := config.GetString("to_account")
|
||||||
|
|
||||||
|
if appID == "" || appSecret == "" {
|
||||||
|
return SendError("wechat config missing: appID, appsecret are required"), nil
|
||||||
|
}
|
||||||
|
if toAccount == "" {
|
||||||
|
return SendError("wechat config missing: to_account is required"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, formattedContent := c.FormatContent(msg)
|
||||||
|
cli := message.WeChatOFAccount{
|
||||||
|
AppID: appID,
|
||||||
|
AppSecret: appSecret,
|
||||||
|
TemplateID: tempID,
|
||||||
|
ToUser: toAccount,
|
||||||
|
URL: msg.URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := cli.Send(msg.Title, formattedContent)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResult(res, err), nil
|
||||||
|
}
|
||||||
|
return SuccessResult(res), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
// Package messenger 提供统一的消息发送SDK
|
||||||
|
//
|
||||||
|
// 此包可独立于 Message-Push-Nest 的业务层(数据库、HTTP路由等)使用
|
||||||
|
// 适合在其他服务中直接引入来发送消息
|
||||||
|
//
|
||||||
|
// 快速使用:
|
||||||
|
//
|
||||||
|
// result, err := messenger.Send("Telegram", messenger.ChannelConfig{
|
||||||
|
// "bot_token": "your-bot-token",
|
||||||
|
// "chat_id": "your-chat-id",
|
||||||
|
// }, &messenger.Message{
|
||||||
|
// Title: "Hello",
|
||||||
|
// Text: "World",
|
||||||
|
// })
|
||||||
|
package messenger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/engigu/baihu-panel/internal/sdk/messenger/channels"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 重导出 channels 包的类型,方便外部使用
|
||||||
|
type (
|
||||||
|
Channel = channels.Channel
|
||||||
|
Message = channels.Message
|
||||||
|
ChannelConfig = channels.ChannelConfig
|
||||||
|
Result = channels.Result
|
||||||
|
BaseChannel = channels.BaseChannel
|
||||||
|
)
|
||||||
|
|
||||||
|
// 重导出常量
|
||||||
|
const (
|
||||||
|
FormatTypeText = channels.FormatTypeText
|
||||||
|
FormatTypeHTML = channels.FormatTypeHTML
|
||||||
|
FormatTypeMarkdown = channels.FormatTypeMarkdown
|
||||||
|
|
||||||
|
ChannelEmail = channels.ChannelEmail
|
||||||
|
ChannelDtalk = channels.ChannelDtalk
|
||||||
|
ChannelQyWeiXin = channels.ChannelQyWeiXin
|
||||||
|
ChannelFeishu = channels.ChannelFeishu
|
||||||
|
ChannelCustom = channels.ChannelCustom
|
||||||
|
ChannelWeChatOFAccount = channels.ChannelWeChatOFAccount
|
||||||
|
ChannelAliyunSMS = channels.ChannelAliyunSMS
|
||||||
|
ChannelTelegram = channels.ChannelTelegram
|
||||||
|
ChannelBark = channels.ChannelBark
|
||||||
|
ChannelPushMe = channels.ChannelPushMe
|
||||||
|
ChannelNtfy = channels.ChannelNtfy
|
||||||
|
ChannelGotify = channels.ChannelGotify
|
||||||
|
)
|
||||||
|
|
||||||
|
// 重导出辅助函数
|
||||||
|
var (
|
||||||
|
SuccessResult = channels.SuccessResult
|
||||||
|
ErrorResult = channels.ErrorResult
|
||||||
|
ErrorResultStr = channels.ErrorResultStr
|
||||||
|
SendError = channels.SendError
|
||||||
|
NewBaseChannel = channels.NewBaseChannel
|
||||||
|
)
|
||||||
|
|
||||||
|
// channelFactory 渠道工厂注册表
|
||||||
|
var (
|
||||||
|
channelFactories = map[string]func() Channel{}
|
||||||
|
factoryMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册所有内置渠道
|
||||||
|
RegisterChannel(ChannelEmail, func() Channel { return channels.NewEmailChannel() })
|
||||||
|
RegisterChannel(ChannelDtalk, func() Channel { return channels.NewDtalkChannel() })
|
||||||
|
RegisterChannel(ChannelQyWeiXin, func() Channel { return channels.NewQyWeiXinChannel() })
|
||||||
|
RegisterChannel(ChannelFeishu, func() Channel { return channels.NewFeishuChannel() })
|
||||||
|
RegisterChannel(ChannelTelegram, func() Channel { return channels.NewTelegramChannel() })
|
||||||
|
RegisterChannel(ChannelBark, func() Channel { return channels.NewBarkChannel() })
|
||||||
|
RegisterChannel(ChannelNtfy, func() Channel { return channels.NewNtfyChannel() })
|
||||||
|
RegisterChannel(ChannelGotify, func() Channel { return channels.NewGotifyChannel() })
|
||||||
|
RegisterChannel(ChannelPushMe, func() Channel { return channels.NewPushMeChannel() })
|
||||||
|
RegisterChannel(ChannelCustom, func() Channel { return channels.NewCustomChannel() })
|
||||||
|
RegisterChannel(ChannelWeChatOFAccount, func() Channel { return channels.NewWeChatOFAccountChannel() })
|
||||||
|
RegisterChannel(ChannelAliyunSMS, func() Channel { return channels.NewAliyunSMSChannel() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterChannel 注册自定义渠道(可用于扩展)
|
||||||
|
func RegisterChannel(channelType string, factory func() Channel) {
|
||||||
|
factoryMu.Lock()
|
||||||
|
defer factoryMu.Unlock()
|
||||||
|
channelFactories[channelType] = factory
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannel 获取渠道实例
|
||||||
|
func GetChannel(channelType string) (Channel, error) {
|
||||||
|
factoryMu.RLock()
|
||||||
|
defer factoryMu.RUnlock()
|
||||||
|
factory, ok := channelFactories[channelType]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("未知的渠道类型: %s", channelType)
|
||||||
|
}
|
||||||
|
return factory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListChannels 列出所有已注册的渠道类型
|
||||||
|
func ListChannels() []string {
|
||||||
|
factoryMu.RLock()
|
||||||
|
defer factoryMu.RUnlock()
|
||||||
|
types := make([]string, 0, len(channelFactories))
|
||||||
|
for t := range channelFactories {
|
||||||
|
types = append(types, t)
|
||||||
|
}
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 发送消息的便捷函数
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - channelType: 渠道类型(如 "Telegram", "Dtalk" 等)
|
||||||
|
// - config: 渠道必要的认证配置
|
||||||
|
// - msg: 消息内容
|
||||||
|
//
|
||||||
|
// 使用示例:
|
||||||
|
//
|
||||||
|
// result, err := messenger.Send("Ntfy", messenger.ChannelConfig{
|
||||||
|
// "topic": "my-topic",
|
||||||
|
// }, &messenger.Message{
|
||||||
|
// Title: "Alert",
|
||||||
|
// Text: "Something happened!",
|
||||||
|
// })
|
||||||
|
func Send(channelType string, config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
ch, err := GetChannel(channelType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ch.Send(config, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client 消息发送客户端(支持预设默认配置)
|
||||||
|
type Client struct {
|
||||||
|
defaultConfigs map[string]ChannelConfig
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建消息发送客户端
|
||||||
|
func NewClient() *Client {
|
||||||
|
return &Client{
|
||||||
|
defaultConfigs: make(map[string]ChannelConfig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultConfig 为指定渠道设置默认配置
|
||||||
|
//
|
||||||
|
// 使用示例:
|
||||||
|
//
|
||||||
|
// client := messenger.NewClient()
|
||||||
|
// client.SetDefaultConfig("Telegram", messenger.ChannelConfig{
|
||||||
|
// "bot_token": "default-token",
|
||||||
|
// "chat_id": "default-chat",
|
||||||
|
// })
|
||||||
|
// // 后续发送时不需要再传 config 的相关字段
|
||||||
|
// result, err := client.Send("Telegram", nil, &messenger.Message{...})
|
||||||
|
func (c *Client) SetDefaultConfig(channelType string, config ChannelConfig) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.defaultConfigs[channelType] = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send 使用客户端发送消息(会合并默认配置)
|
||||||
|
func (c *Client) Send(channelType string, config ChannelConfig, msg *Message) (*Result, error) {
|
||||||
|
mergedConfig := c.mergeConfig(channelType, config)
|
||||||
|
return Send(channelType, mergedConfig, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) mergeConfig(channelType string, config ChannelConfig) ChannelConfig {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
defaultConfig, hasDefault := c.defaultConfigs[channelType]
|
||||||
|
if !hasDefault && config == nil {
|
||||||
|
return ChannelConfig{}
|
||||||
|
}
|
||||||
|
if !hasDefault {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
return defaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并:config 覆盖 defaultConfig
|
||||||
|
merged := make(ChannelConfig, len(defaultConfig)+len(config))
|
||||||
|
for k, v := range defaultConfig {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range config {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/engigu/baihu-panel/internal/constant"
|
||||||
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
|
"github.com/engigu/baihu-panel/internal/logger"
|
||||||
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
|
|
||||||
|
"github.com/engigu/baihu-panel/internal/sdk/messenger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifyChannel 通知渠道配置
|
||||||
|
type NotifyChannel struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Config map[string]string `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyMessage 通知消息
|
||||||
|
type NotifyMessage struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyResult 发送结果
|
||||||
|
type NotifyResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedChannelTypes 支持的渠道类型
|
||||||
|
var SupportedChannelTypes = []map[string]string{
|
||||||
|
{"type": messenger.ChannelTelegram, "label": "Telegram"},
|
||||||
|
{"type": messenger.ChannelBark, "label": "Bark"},
|
||||||
|
{"type": messenger.ChannelDtalk, "label": "钉钉"},
|
||||||
|
{"type": messenger.ChannelQyWeiXin, "label": "企业微信"},
|
||||||
|
{"type": messenger.ChannelFeishu, "label": "飞书"},
|
||||||
|
{"type": messenger.ChannelEmail, "label": "邮件"},
|
||||||
|
{"type": messenger.ChannelCustom, "label": "自定义Webhook"},
|
||||||
|
{"type": messenger.ChannelNtfy, "label": "Ntfy"},
|
||||||
|
{"type": messenger.ChannelGotify, "label": "Gotify"},
|
||||||
|
{"type": messenger.ChannelPushMe, "label": "PushMe"},
|
||||||
|
// {"type": messenger.ChannelWeChatOFAccount, "label": "微信公众号"},
|
||||||
|
{"type": messenger.ChannelAliyunSMS, "label": "阿里云短信"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedEvents 支持的事件类型
|
||||||
|
var SupportedEvents = []map[string]string{
|
||||||
|
{"type": constant.EventUserLogin, "label": "用户登录", "binding_type": constant.BindingTypeSystem},
|
||||||
|
{"type": constant.EventBruteForceLogin, "label": "密码多次错误", "binding_type": constant.BindingTypeSystem},
|
||||||
|
{"type": constant.EventPasswordChanged, "label": "密码修改", "binding_type": constant.BindingTypeSystem},
|
||||||
|
{"type": constant.EventTaskSuccess, "label": "任务成功", "binding_type": constant.BindingTypeTask},
|
||||||
|
{"type": constant.EventTaskFailed, "label": "任务失败", "binding_type": constant.BindingTypeTask},
|
||||||
|
{"type": constant.EventTaskTimeout, "label": "任务超时", "binding_type": constant.BindingTypeTask},
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationService struct {
|
||||||
|
settingsService *SettingsService
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationService() *NotificationService {
|
||||||
|
return &NotificationService{
|
||||||
|
settingsService: NewSettingsService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannels 获取所有渠道
|
||||||
|
func (s *NotificationService) GetChannels() []NotifyChannel {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.getChannelsInternal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveChannel 保存/更新渠道
|
||||||
|
func (s *NotificationService) SaveChannel(channel NotifyChannel) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
configJSON, err := json.Marshal(channel.Config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.ID == "" {
|
||||||
|
// 新建
|
||||||
|
channel.ID = utils.GenerateID()
|
||||||
|
notifyWay := &models.NotifyWay{
|
||||||
|
ID: channel.ID,
|
||||||
|
Name: channel.Name,
|
||||||
|
Type: channel.Type,
|
||||||
|
Config: string(configJSON),
|
||||||
|
Enabled: channel.Enabled,
|
||||||
|
}
|
||||||
|
return database.DB.Create(notifyWay).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"name": channel.Name,
|
||||||
|
"type": channel.Type,
|
||||||
|
"config": string(configJSON),
|
||||||
|
"enabled": channel.Enabled,
|
||||||
|
}
|
||||||
|
return database.DB.Model(&models.NotifyWay{}).Where("id = ?", channel.ID).Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteChannel 删除渠道
|
||||||
|
func (s *NotificationService) DeleteChannel(id string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
// 检查渠道是否存在
|
||||||
|
var count int64
|
||||||
|
database.DB.Model(&models.NotifyWay{}).Where("id = ?", id).Count(&count)
|
||||||
|
if count == 0 {
|
||||||
|
return fmt.Errorf("渠道 %s 不存在", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除渠道
|
||||||
|
if err := database.DB.Unscoped().Where("id = ?", id).Delete(&models.NotifyWay{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时清理事件绑定中引用此渠道的配置
|
||||||
|
if err := database.DB.Unscoped().Where("way_id = ?", id).Delete(&models.NotifyBinding{}).Error; err != nil {
|
||||||
|
logger.Errorf("[Notify] 清理事件绑定失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBindings 获取事件绑定列表(新接口,用于前端展示)
|
||||||
|
func (s *NotificationService) GetBindings() []models.NotifyBinding {
|
||||||
|
var bindings []models.NotifyBinding
|
||||||
|
database.DB.Find(&bindings)
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveBinding 保存事件绑定
|
||||||
|
func (s *NotificationService) SaveBinding(binding *models.NotifyBinding) error {
|
||||||
|
if binding.ID == "" {
|
||||||
|
// 检查是否已经存在相同的绑定(避免重复点击导致多个记录)
|
||||||
|
var existing models.NotifyBinding
|
||||||
|
err := database.DB.Where("type = ? AND event = ? AND way_id = ? AND data_id = ?",
|
||||||
|
binding.Type, binding.Event, binding.WayID, binding.DataID).First(&existing).Error
|
||||||
|
if err == nil {
|
||||||
|
// 如果已存在且未删除,直接返回(或者更新它)
|
||||||
|
*binding = existing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.ID = utils.GenerateID()
|
||||||
|
return database.DB.Create(binding).Error
|
||||||
|
}
|
||||||
|
return database.DB.Save(binding).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBinding 删除事件绑定
|
||||||
|
func (s *NotificationService) DeleteBinding(id string) error {
|
||||||
|
return database.DB.Unscoped().Where("id = ?", id).Delete(&models.NotifyBinding{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBindingsByEvent 根据事件类型和数据ID获取绑定
|
||||||
|
func (s *NotificationService) GetBindingsByEvent(bindingType, event, dataID string) []models.NotifyBinding {
|
||||||
|
var bindings []models.NotifyBinding
|
||||||
|
|
||||||
|
// 如果是任务事件且带有 dataID,只获取特定任务的绑定(禁用全局任务配置)
|
||||||
|
if bindingType == constant.BindingTypeTask && dataID != "" {
|
||||||
|
database.DB.Where("type = ? AND event = ? AND data_id = ?", constant.BindingTypeTask, event, dataID).Find(&bindings)
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于系统事件或其他情况
|
||||||
|
query := database.DB.Where("event = ?", event)
|
||||||
|
if bindingType != "" {
|
||||||
|
query = query.Where("type = ?", bindingType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataID != "" {
|
||||||
|
query = query.Where("data_id = ?", dataID)
|
||||||
|
} else {
|
||||||
|
query = query.Where("data_id = ? OR data_id IS NULL", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Find(&bindings)
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendToChannel 使用 messenger SDK 发送通知到指定渠道
|
||||||
|
func (s *NotificationService) SendToChannel(channel NotifyChannel, msg *NotifyMessage) *NotifyResult {
|
||||||
|
result, err := messenger.Send(channel.Type, messenger.ChannelConfig(channel.Config), &messenger.Message{
|
||||||
|
Title: msg.Title,
|
||||||
|
Text: msg.Text,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &NotifyResult{Success: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
if !result.Success {
|
||||||
|
return &NotifyResult{Success: false, Error: result.Error}
|
||||||
|
}
|
||||||
|
return &NotifyResult{Success: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendByChannelID 根据渠道ID发送通知
|
||||||
|
func (s *NotificationService) SendByChannelID(channelID string, msg *NotifyMessage) *NotifyResult {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
var notifyWay models.NotifyWay
|
||||||
|
if err := database.DB.Where("id = ?", channelID).First(¬ifyWay).Error; err != nil {
|
||||||
|
return &NotifyResult{Success: false, Error: "渠道不存在"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !notifyWay.Enabled {
|
||||||
|
return &NotifyResult{Success: false, Error: "渠道已禁用"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var config map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(notifyWay.Config), &config); err != nil {
|
||||||
|
return &NotifyResult{Success: false, Error: "渠道配置解析失败"}
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := NotifyChannel{
|
||||||
|
ID: notifyWay.ID,
|
||||||
|
Name: notifyWay.Name,
|
||||||
|
Type: notifyWay.Type,
|
||||||
|
Enabled: notifyWay.Enabled,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
return s.SendToChannel(ch, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerEvent 触发事件通知(实现 tasks.Notifier 接口)
|
||||||
|
func (s *NotificationService) TriggerEvent(bindingType string, eventType string, dataID string, templateData map[string]interface{}) {
|
||||||
|
var title, text string
|
||||||
|
|
||||||
|
switch eventType {
|
||||||
|
case constant.EventUserLogin:
|
||||||
|
title = "用户登录通知"
|
||||||
|
text = fmt.Sprintf("用户 %v 在 IP %v 登录成功", templateData["username"], templateData["ip"])
|
||||||
|
case constant.EventBruteForceLogin:
|
||||||
|
title = "系统安全警告"
|
||||||
|
text = fmt.Sprintf("检测到 IP %v 正在尝试暴力破解用户 %v", templateData["ip"], templateData["username"])
|
||||||
|
case constant.EventPasswordChanged:
|
||||||
|
title = "账户安全通知"
|
||||||
|
text = fmt.Sprintf("用户 %v 刚刚修改了密码", templateData["username"])
|
||||||
|
case constant.EventTaskSuccess:
|
||||||
|
title = fmt.Sprintf("任务[%v] 成功", templateData["task_name"])
|
||||||
|
text = fmt.Sprintf("任务 #%v %v\n状态: 成功\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["duration"])
|
||||||
|
case constant.EventTaskFailed:
|
||||||
|
title = fmt.Sprintf("任务[%v] 失败", templateData["task_name"])
|
||||||
|
if errStr, ok := templateData["error"]; ok {
|
||||||
|
text = fmt.Sprintf("任务 #%v %v\n执行失败\n错误: %v", templateData["task_id"], templateData["task_name"], errStr)
|
||||||
|
} else {
|
||||||
|
text = fmt.Sprintf("任务 #%v %v\n执行失败\n状态: %v\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["status"], templateData["duration"])
|
||||||
|
}
|
||||||
|
case constant.EventTaskTimeout:
|
||||||
|
title = fmt.Sprintf("任务[%v] 超时", templateData["task_name"])
|
||||||
|
text = fmt.Sprintf("任务 #%v %v\n执行超时\n耗时: %vms", templateData["task_id"], templateData["task_name"], templateData["duration"])
|
||||||
|
default:
|
||||||
|
title = "系统通知"
|
||||||
|
text = "收到未知事件"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &NotifyMessage{Title: title, Text: text}
|
||||||
|
|
||||||
|
bindings := s.GetBindingsByEvent(bindingType, eventType, dataID)
|
||||||
|
if len(bindings) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channels := s.GetChannels()
|
||||||
|
channelMap := make(map[string]NotifyChannel)
|
||||||
|
for _, ch := range channels {
|
||||||
|
channelMap[ch.ID] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, binding := range bindings {
|
||||||
|
ch, ok := channelMap[binding.WayID]
|
||||||
|
if !ok || !ch.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func(channel NotifyChannel) {
|
||||||
|
result := s.SendToChannel(channel, msg)
|
||||||
|
if !result.Success {
|
||||||
|
logger.Warnf("[Notify] 发送事件 %s 到渠道 %s(%s) 失败: %s", eventType, channel.Name, channel.Type, result.Error)
|
||||||
|
}
|
||||||
|
}(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 内部方法 ---
|
||||||
|
|
||||||
|
// getChannelsInternal 从 notify_ways 表中读取所有渠道配置
|
||||||
|
func (s *NotificationService) getChannelsInternal() []NotifyChannel {
|
||||||
|
var notifyWays []models.NotifyWay
|
||||||
|
database.DB.Find(¬ifyWays)
|
||||||
|
|
||||||
|
channels := make([]NotifyChannel, 0, len(notifyWays))
|
||||||
|
for _, nw := range notifyWays {
|
||||||
|
var config map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(nw.Config), &config); err != nil {
|
||||||
|
logger.Warnf("[Notify] 解析渠道 %s 配置失败: %v", nw.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
channels = append(channels, NotifyChannel{
|
||||||
|
ID: nw.ID,
|
||||||
|
Name: nw.Name,
|
||||||
|
Type: nw.Type,
|
||||||
|
Enabled: nw.Enabled,
|
||||||
|
Config: config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return channels
|
||||||
|
}
|
||||||
@@ -89,6 +89,11 @@ func (s *SettingsService) Set(section, key, value string) error {
|
|||||||
return database.DB.Model(&setting).Update("value", value).Error
|
return database.DB.Model(&setting).Update("value", value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete 删除单个设置
|
||||||
|
func (s *SettingsService) Delete(section, key string) error {
|
||||||
|
return database.DB.Where("section = ? AND `key` = ?", section, key).Delete(&models.Setting{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
// GetSection 获取整个 section 的设置
|
// GetSection 获取整个 section 的设置
|
||||||
func (s *SettingsService) GetSection(section string) map[string]string {
|
func (s *SettingsService) GetSection(section string) map[string]string {
|
||||||
if section == constant.SectionSite {
|
if section == constant.SectionSite {
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ type EnvService interface {
|
|||||||
GetEnvVarsByIDs(ids string) []string
|
GetEnvVarsByIDs(ids string) []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notifier 通知服务接口定义(避免循环依赖)
|
||||||
|
type Notifier interface {
|
||||||
|
TriggerEvent(bindingType string, eventType string, dataID string, templateData map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
// ExecutorService handles task execution and scheduling
|
// ExecutorService handles task execution and scheduling
|
||||||
type ExecutorService struct {
|
type ExecutorService struct {
|
||||||
taskService *TaskService
|
taskService *TaskService
|
||||||
@@ -46,6 +51,7 @@ type ExecutorService struct {
|
|||||||
agentWSManager AgentWSManager
|
agentWSManager AgentWSManager
|
||||||
settingsService SettingsService
|
settingsService SettingsService
|
||||||
envService EnvService
|
envService EnvService
|
||||||
|
notifier Notifier
|
||||||
scheduler *executor.Scheduler
|
scheduler *executor.Scheduler
|
||||||
cronManager *executor.CronManager
|
cronManager *executor.CronManager
|
||||||
results []executor.ExecutionResult
|
results []executor.ExecutionResult
|
||||||
@@ -65,6 +71,7 @@ func NewExecutorService(
|
|||||||
agentWSManager AgentWSManager,
|
agentWSManager AgentWSManager,
|
||||||
settingsService SettingsService,
|
settingsService SettingsService,
|
||||||
envService EnvService,
|
envService EnvService,
|
||||||
|
notifier Notifier,
|
||||||
) *ExecutorService {
|
) *ExecutorService {
|
||||||
es := &ExecutorService{
|
es := &ExecutorService{
|
||||||
taskService: taskService,
|
taskService: taskService,
|
||||||
@@ -72,6 +79,7 @@ func NewExecutorService(
|
|||||||
agentWSManager: agentWSManager,
|
agentWSManager: agentWSManager,
|
||||||
settingsService: settingsService,
|
settingsService: settingsService,
|
||||||
envService: envService,
|
envService: envService,
|
||||||
|
notifier: notifier,
|
||||||
results: make([]executor.ExecutionResult, 0, 100),
|
results: make([]executor.ExecutionResult, 0, 100),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
@@ -246,6 +254,29 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
|||||||
|
|
||||||
// ======= 重试逻辑 =======
|
// ======= 重试逻辑 =======
|
||||||
h.es.HandleTaskRetry(task, req, result.Success, result.Status, result.ExitCode)
|
h.es.HandleTaskRetry(task, req, result.Success, result.Status, result.ExitCode)
|
||||||
|
|
||||||
|
// ======= 通知触发 =======
|
||||||
|
if h.es.notifier != nil {
|
||||||
|
go func() {
|
||||||
|
var eventType string
|
||||||
|
switch result.Status {
|
||||||
|
case constant.TaskStatusSuccess:
|
||||||
|
eventType = constant.EventTaskSuccess
|
||||||
|
case constant.TaskStatusFailed:
|
||||||
|
eventType = constant.EventTaskFailed
|
||||||
|
case constant.TaskStatusTimeout:
|
||||||
|
eventType = constant.EventTaskTimeout
|
||||||
|
}
|
||||||
|
if eventType != "" {
|
||||||
|
h.es.notifier.TriggerEvent(constant.BindingTypeTask, eventType, task.ID, map[string]interface{}{
|
||||||
|
"task_id": task.ID,
|
||||||
|
"task_name": task.Name,
|
||||||
|
"status": result.Status,
|
||||||
|
"duration": result.Duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
|
func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
|
||||||
@@ -305,6 +336,21 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
|
|||||||
|
|
||||||
// ======= 重试逻辑 =======
|
// ======= 重试逻辑 =======
|
||||||
h.es.HandleTaskRetry(task, req, false, constant.TaskStatusFailed, 1)
|
h.es.HandleTaskRetry(task, req, false, constant.TaskStatusFailed, 1)
|
||||||
|
|
||||||
|
// ======= 通知触发 =======
|
||||||
|
if h.es.notifier != nil {
|
||||||
|
go func() {
|
||||||
|
taskName := "未知任务"
|
||||||
|
if task != nil {
|
||||||
|
taskName = task.Name
|
||||||
|
}
|
||||||
|
h.es.notifier.TriggerEvent(constant.BindingTypeTask, constant.EventTaskFailed, taskID, map[string]interface{}{
|
||||||
|
"task_id": taskID,
|
||||||
|
"task_name": taskName,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleTaskRetry 处理任务失败重试逻辑
|
// HandleTaskRetry 处理任务失败重试逻辑
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TaskService) DeleteTask(id string) bool {
|
func (ts *TaskService) DeleteTask(id string) bool {
|
||||||
|
// 同时删除关联的通知推送设置
|
||||||
|
database.DB.Where("type = ? AND data_id = ?", constant.BindingTypeTask, id).Delete(&models.NotifyBinding{})
|
||||||
|
|
||||||
result := database.DB.Where("id = ?", id).Delete(&models.Task{})
|
result := database.DB.Where("id = ?", id).Delete(&models.Task{})
|
||||||
return result.RowsAffected > 0
|
return result.RowsAffected > 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ export const api = {
|
|||||||
request('/settings/scheduler', { method: 'PUT', body: JSON.stringify(data) }),
|
request('/settings/scheduler', { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
getPaths: () => request<{ scripts_dir: string }>('/settings/paths'),
|
getPaths: () => request<{ scripts_dir: string }>('/settings/paths'),
|
||||||
getAbout: () => request<AboutInfo>('/settings/about'),
|
getAbout: () => request<AboutInfo>('/settings/about'),
|
||||||
|
get: (section: string, key: string) => request<string>(`/settings/${section}/${key}`),
|
||||||
|
generateToken: (section: string, key: string) =>
|
||||||
|
request<string>(`/settings/${section}/${key}/generate`, { method: 'POST' }),
|
||||||
getLoginLogs: (params?: { page?: number; page_size?: number; username?: string }) => {
|
getLoginLogs: (params?: { page?: number; page_size?: number; username?: string }) => {
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
if (params?.page) query.set('page', String(params.page))
|
if (params?.page) query.set('page', String(params.page))
|
||||||
@@ -261,6 +264,22 @@ export const api = {
|
|||||||
},
|
},
|
||||||
terminal: {
|
terminal: {
|
||||||
cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds')
|
cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds')
|
||||||
|
},
|
||||||
|
notify: {
|
||||||
|
getTypes: () => request<{ channel_types: ChannelType[]; event_types: EventType[] }>('/notify/types'),
|
||||||
|
getChannels: () => request<NotifyChannel[]>('/notify/channels'),
|
||||||
|
saveChannel: (data: Partial<NotifyChannel>) =>
|
||||||
|
request('/notify/channels', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
|
deleteChannel: (id: string) => request('/notify/channels/' + id, { method: 'DELETE' }),
|
||||||
|
testChannel: (data: Partial<NotifyChannel>) =>
|
||||||
|
request<NotifyResult>('/notify/channels/test', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
|
|
||||||
|
getBindings: () => request<NotifyBinding[]>('/notify/bindings'),
|
||||||
|
saveBinding: (data: Partial<NotifyBinding>) =>
|
||||||
|
request<NotifyBinding>('/notify/bindings', { method: 'POST', body: JSON.stringify(data) }),
|
||||||
|
deleteBinding: (id: string) => request('/notify/bindings/' + id, { method: 'DELETE' }),
|
||||||
|
send: (data: { channel_id: string; title: string; text: string }) =>
|
||||||
|
request<NotifyResult>('/notify/send', { method: 'POST', body: JSON.stringify(data) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,3 +515,39 @@ export interface MiseLanguage {
|
|||||||
install_path?: string
|
install_path?: string
|
||||||
installed_at?: string // 安装日期
|
installed_at?: string // 安装日期
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChannelType {
|
||||||
|
type: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventType {
|
||||||
|
type: string
|
||||||
|
label: string
|
||||||
|
binding_type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotifyChannel {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
enabled: boolean
|
||||||
|
config: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotifyBinding {
|
||||||
|
id: string
|
||||||
|
type: string
|
||||||
|
event: string
|
||||||
|
way_id: string
|
||||||
|
data_id: string
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotifyResult {
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,10 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 高级感增强 - 微妙的渐变背景 */
|
/* 高级感增强 - 微妙的渐变背景 */
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||||
import { resetAuthCache } from '@/router'
|
import { resetAuthCache } from '@/router'
|
||||||
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Menu, X, Server, Globe } from 'lucide-vue-next'
|
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound, Menu, X, Server, Globe, Bell } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||||
import { api } from '@/api'
|
import { api } from '@/api'
|
||||||
@@ -60,6 +60,7 @@ const navItems = [
|
|||||||
{ to: '/environments', icon: Variable, label: '环境变量', exact: true },
|
{ to: '/environments', icon: Variable, label: '环境变量', exact: true },
|
||||||
{ to: '/languages', icon: Globe, label: '语言依赖', exact: true },
|
{ to: '/languages', icon: Globe, label: '语言依赖', exact: true },
|
||||||
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
|
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
|
||||||
|
{ to: '/notify', icon: Bell, label: '消息推送', exact: true },
|
||||||
{ to: '/loginlogs', icon: KeyRound, label: '登录日志', exact: true },
|
{ to: '/loginlogs', icon: KeyRound, label: '登录日志', exact: true },
|
||||||
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
|
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const router = createRouter({
|
|||||||
{ path: 'history', name: 'history', component: () => import('@/views/history/History.vue') },
|
{ path: 'history', name: 'history', component: () => import('@/views/history/History.vue') },
|
||||||
{ path: 'loginlogs', name: 'loginlogs', component: () => import('@/views/loginlogs/LoginLogs.vue') },
|
{ path: 'loginlogs', name: 'loginlogs', component: () => import('@/views/loginlogs/LoginLogs.vue') },
|
||||||
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
|
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
|
||||||
|
{ path: 'notify', name: 'notify', component: () => import('@/views/notify/Notify.vue') },
|
||||||
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
|
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog'
|
||||||
|
import { api, type NotifyChannel, type ChannelType, type EventType, type NotifyBinding } from '@/api'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import ChannelList from './components/ChannelList.vue'
|
||||||
|
import EventBinding from './components/EventBinding.vue'
|
||||||
|
import ApiUsage from './components/ApiUsage.vue'
|
||||||
|
import ChannelDialog from './components/ChannelDialog.vue'
|
||||||
|
|
||||||
|
const activeTab = ref('channels')
|
||||||
|
|
||||||
|
// 渠道数据
|
||||||
|
const channels = ref<NotifyChannel[]>([])
|
||||||
|
const channelTypes = ref<ChannelType[]>([])
|
||||||
|
const eventTypes = ref<EventType[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// API Token
|
||||||
|
const apiToken = ref('')
|
||||||
|
|
||||||
|
// 编辑弹窗
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const editingChannel = ref<Partial<NotifyChannel>>({
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
enabled: true,
|
||||||
|
config: {}
|
||||||
|
})
|
||||||
|
const isEditing = ref(false)
|
||||||
|
|
||||||
|
// 删除确认
|
||||||
|
const showDeleteConfirm = ref(false)
|
||||||
|
const deletingChannelId = ref('')
|
||||||
|
|
||||||
|
// 事件绑定
|
||||||
|
const bindings = ref<NotifyBinding[]>([])
|
||||||
|
const allTasks = ref<Task[]>([])
|
||||||
|
|
||||||
|
// 渠道配置模板
|
||||||
|
const channelConfigFields: Record<string, { key: string; label: string; required: boolean; placeholder?: string; type?: string }[]> = {
|
||||||
|
Telegram: [
|
||||||
|
{ key: 'bot_token', label: 'Bot Token', required: true, placeholder: '从 @BotFather 获取' },
|
||||||
|
{ key: 'chat_id', label: 'Chat ID', required: true, placeholder: '聊天/群组 ID' },
|
||||||
|
{ key: 'api_host', label: 'API 地址', required: false, placeholder: '自定义 API 地址,留空使用官方' },
|
||||||
|
{ key: 'proxy_url', label: '代理地址', required: false, placeholder: 'http/https/socks5 代理' },
|
||||||
|
],
|
||||||
|
Bark: [
|
||||||
|
{ key: 'push_key', label: 'Push Key', required: true, placeholder: 'Bark Push Key' },
|
||||||
|
{ key: 'sound', label: '推送声音', required: false, placeholder: '留空使用默认' },
|
||||||
|
{ key: 'group', label: '推送分组', required: false },
|
||||||
|
{ key: 'icon', label: '推送图标', required: false, placeholder: '图标 URL' },
|
||||||
|
{ key: 'level', label: '时效性', required: false, placeholder: 'active / timeSensitive / passive' },
|
||||||
|
{ key: 'url', label: '跳转URL', required: false },
|
||||||
|
],
|
||||||
|
Dtalk: [
|
||||||
|
{ key: 'access_token', label: 'Access Token', required: true, placeholder: '钉钉机器人 access_token' },
|
||||||
|
{ key: 'secret', label: '加签秘钥', required: false, placeholder: '可选' },
|
||||||
|
],
|
||||||
|
QyWeiXin: [
|
||||||
|
{ key: 'access_token', label: 'Access Token', required: true, placeholder: '企业微信机器人 Key' },
|
||||||
|
],
|
||||||
|
Feishu: [
|
||||||
|
{ key: 'access_token', label: 'Access Token', required: true, placeholder: '飞书机器人 access_token' },
|
||||||
|
{ key: 'secret', label: '加签秘钥', required: false, placeholder: '可选' },
|
||||||
|
],
|
||||||
|
Custom: [
|
||||||
|
{ key: 'webhook', label: 'Webhook URL', required: true, placeholder: 'https://...' },
|
||||||
|
{ key: 'body', label: '请求体模板', required: false, placeholder: '使用 TEXT 作为消息内容占位符', type: 'textarea' },
|
||||||
|
],
|
||||||
|
Ntfy: [
|
||||||
|
{ key: 'topic', label: 'Topic', required: true },
|
||||||
|
{ key: 'url', label: 'API 地址', required: false, placeholder: '默认 https://ntfy.sh' },
|
||||||
|
{ key: 'priority', label: '优先级', required: false, placeholder: '1-5' },
|
||||||
|
{ key: 'icon', label: '图标 URL', required: false },
|
||||||
|
{ key: 'token', label: 'Token', required: false },
|
||||||
|
{ key: 'username', label: '用户名', required: false },
|
||||||
|
{ key: 'password', label: '密码', required: false },
|
||||||
|
],
|
||||||
|
Gotify: [
|
||||||
|
{ key: 'url', label: '服务地址', required: true, placeholder: 'https://gotify.example.com' },
|
||||||
|
{ key: 'token', label: 'Token', required: true },
|
||||||
|
{ key: 'priority', label: '优先级', required: false, placeholder: '0-10' },
|
||||||
|
],
|
||||||
|
PushMe: [
|
||||||
|
{ key: 'push_key', label: 'Push Key', required: true },
|
||||||
|
{ key: 'url', label: 'API 地址', required: false, placeholder: '默认 https://push.i-i.me' },
|
||||||
|
{ key: 'type', label: '类型', required: false },
|
||||||
|
],
|
||||||
|
Email: [
|
||||||
|
{ key: 'server', label: 'SMTP 服务器', required: true, placeholder: 'smtp.example.com' },
|
||||||
|
{ key: 'port', label: '端口', required: true, placeholder: '465' },
|
||||||
|
{ key: 'account', label: '邮箱账号', required: true },
|
||||||
|
{ key: 'passwd', label: '邮箱密码', required: true },
|
||||||
|
{ key: 'from_name', label: '发信人名称', required: false },
|
||||||
|
{ key: 'to_account', label: '收件邮箱', required: true },
|
||||||
|
],
|
||||||
|
AliyunSMS: [
|
||||||
|
{ key: 'access_key_id', label: 'AccessKeyId', required: true },
|
||||||
|
{ key: 'access_key_secret', label: 'AccessKeySecret', required: true },
|
||||||
|
{ key: 'sign_name', label: '短信签名', required: true },
|
||||||
|
{ key: 'region_id', label: '区域ID', required: false, placeholder: '默认 cn-hangzhou' },
|
||||||
|
{ key: 'phone_number', label: '手机号码', required: true },
|
||||||
|
{ key: 'template_code', label: '短信模板 CODE', required: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [typesRes, channelsRes, tasksRes] = await Promise.all([
|
||||||
|
api.notify.getTypes(),
|
||||||
|
api.notify.getChannels(),
|
||||||
|
api.tasks.list({ page: 1, page_size: 1000 })
|
||||||
|
])
|
||||||
|
channelTypes.value = typesRes.channel_types
|
||||||
|
eventTypes.value = typesRes.event_types
|
||||||
|
channels.value = channelsRes
|
||||||
|
allTasks.value = tasksRes.data
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('加载失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEvents() {
|
||||||
|
try {
|
||||||
|
bindings.value = await api.notify.getBindings()
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('加载事件绑定失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渠道操作
|
||||||
|
function openNewChannel() {
|
||||||
|
editingChannel.value = { name: '', type: '', enabled: true, config: {} }
|
||||||
|
isEditing.value = false
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditChannel(ch: NotifyChannel) {
|
||||||
|
editingChannel.value = { ...ch, config: { ...ch.config } }
|
||||||
|
isEditing.value = true
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTypeChange(val: string) {
|
||||||
|
editingChannel.value.type = val
|
||||||
|
const existing = editingChannel.value.config || {}
|
||||||
|
const fields = channelConfigFields[val] || []
|
||||||
|
const newConfig: Record<string, string> = {}
|
||||||
|
for (const f of fields) {
|
||||||
|
newConfig[f.key] = existing[f.key] || ''
|
||||||
|
}
|
||||||
|
editingChannel.value.config = newConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChannel() {
|
||||||
|
if (!editingChannel.value.name || !editingChannel.value.type) {
|
||||||
|
toast.error('请填写渠道名称和类型')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 enabled 字段有值
|
||||||
|
const channelData = {
|
||||||
|
...editingChannel.value,
|
||||||
|
enabled: editingChannel.value.enabled ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.notify.saveChannel(channelData)
|
||||||
|
toast.success('保存成功')
|
||||||
|
showDialog.value = false
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('保存失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(id: string) {
|
||||||
|
deletingChannelId.value = id
|
||||||
|
showDeleteConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteChannel() {
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
try {
|
||||||
|
await api.notify.deleteChannel(deletingChannelId.value)
|
||||||
|
toast.success('删除成功')
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('删除失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testChannel(ch: NotifyChannel) {
|
||||||
|
try {
|
||||||
|
const result = await api.notify.testChannel(ch)
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('测试发送成功!')
|
||||||
|
} else {
|
||||||
|
toast.error('测试发送失败: ' + (result.error || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('测试失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件绑定
|
||||||
|
async function saveBindings(newBindings: Partial<NotifyBinding>[]) {
|
||||||
|
try {
|
||||||
|
for (const binding of newBindings) {
|
||||||
|
await api.notify.saveBinding(binding)
|
||||||
|
}
|
||||||
|
toast.success('绑定保存成功')
|
||||||
|
await loadEvents()
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('保存失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBinding(id: string) {
|
||||||
|
try {
|
||||||
|
await api.notify.deleteBinding(id)
|
||||||
|
toast.success('绑定已删除')
|
||||||
|
await loadEvents()
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('删除失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Token
|
||||||
|
async function loadApiToken() {
|
||||||
|
try {
|
||||||
|
const token = await api.settings.get('notify', 'notify_token')
|
||||||
|
console.log('Loaded API Token:', token, 'Type:', typeof token, 'Length:', token?.length)
|
||||||
|
apiToken.value = token || ''
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('加载 API Token 失败:', e)
|
||||||
|
// 如果是404或者其他错误,token保持为空字符串
|
||||||
|
apiToken.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateApiToken() {
|
||||||
|
try {
|
||||||
|
const newToken = await api.settings.generateToken('notify', 'notify_token')
|
||||||
|
apiToken.value = newToken
|
||||||
|
toast.success('API Token 已生成')
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error('生成失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyApiToken() {
|
||||||
|
if (!apiToken.value) {
|
||||||
|
toast.error('请先生成 Token')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(apiToken.value)
|
||||||
|
toast.success('Token 已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
toast.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyApiExample() {
|
||||||
|
if (channels.value.length === 0) {
|
||||||
|
toast.error('请先添加一个通知渠道')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ch = channels.value[0]
|
||||||
|
if (!ch) return
|
||||||
|
const token = apiToken.value || 'YOUR_API_TOKEN'
|
||||||
|
const example = `curl -X POST "{{API_URL}}/api/v1/notify/send" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-H "notify-token: ${token}" \\
|
||||||
|
-d '{"channel_id": "${ch.id}", "title": "测试通知", "text": "来自脚本的通知"}'`
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(example)
|
||||||
|
toast.success('API 调用示例已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
toast.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
loadEvents()
|
||||||
|
loadApiToken()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<Tabs v-model="activeTab" class="w-full">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl sm:text-2xl font-bold tracking-tight">消息推送</h2>
|
||||||
|
<p class="text-muted-foreground text-sm">配置通知渠道,绑定系统事件实现自动推送</p>
|
||||||
|
</div>
|
||||||
|
<TabsList class="w-full sm:w-auto grid grid-cols-3 sm:inline-flex h-9 gap-1 p-1">
|
||||||
|
<TabsTrigger value="channels" class="text-xs px-3 py-1">渠道管理</TabsTrigger>
|
||||||
|
<TabsTrigger value="events" class="text-xs px-3 py-1">事件绑定</TabsTrigger>
|
||||||
|
<TabsTrigger value="api" class="text-xs px-3 py-1">脚本调用</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 渠道管理 -->
|
||||||
|
<TabsContent value="channels">
|
||||||
|
<ChannelList
|
||||||
|
:channels="channels"
|
||||||
|
:channel-types="channelTypes"
|
||||||
|
@add="openNewChannel"
|
||||||
|
@edit="openEditChannel"
|
||||||
|
@delete="confirmDelete"
|
||||||
|
@test="testChannel"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<!-- 事件绑定 -->
|
||||||
|
<TabsContent value="events">
|
||||||
|
<EventBinding
|
||||||
|
:channels="channels"
|
||||||
|
:channel-types="channelTypes"
|
||||||
|
:event-types="eventTypes"
|
||||||
|
:bindings="bindings"
|
||||||
|
:tasks="allTasks"
|
||||||
|
@save="saveBindings"
|
||||||
|
@delete="deleteBinding"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<!-- 脚本调用 -->
|
||||||
|
<TabsContent value="api">
|
||||||
|
<ApiUsage
|
||||||
|
:channels="channels"
|
||||||
|
:channel-types="channelTypes"
|
||||||
|
:api-token="apiToken"
|
||||||
|
@generate-token="generateApiToken"
|
||||||
|
@copy-token="copyApiToken"
|
||||||
|
@copy-example="copyApiExample"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<!-- 添加/编辑渠道弹窗 -->
|
||||||
|
<ChannelDialog
|
||||||
|
v-model:open="showDialog"
|
||||||
|
:is-editing="isEditing"
|
||||||
|
v-model:channel="editingChannel"
|
||||||
|
:channel-types="channelTypes"
|
||||||
|
:config-fields="channelConfigFields"
|
||||||
|
@type-change="onTypeChange"
|
||||||
|
@save="saveChannel"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 删除确认 -->
|
||||||
|
<AlertDialog :open="showDeleteConfirm" @update:open="showDeleteConfirm = $event">
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
删除后将无法恢复,同时会取消该渠道的所有事件绑定。确定要删除吗?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction @click="deleteChannel">确认删除</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Copy, Terminal, Key, FileJson, RefreshCw, Check, Hash, Info } from 'lucide-vue-next'
|
||||||
|
import type { NotifyChannel, ChannelType } from '@/api'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
channels: NotifyChannel[]
|
||||||
|
channelTypes: ChannelType[]
|
||||||
|
apiToken: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
generateToken: []
|
||||||
|
copyToken: []
|
||||||
|
copyExample: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const copiedBlock = ref<string | null>(null)
|
||||||
|
const host = ref(window.location.host)
|
||||||
|
|
||||||
|
function copyToClipboard(text: string, blockId: string) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
copiedBlock.value = blockId
|
||||||
|
toast.success('已复制到剪贴板')
|
||||||
|
setTimeout(() => {
|
||||||
|
copiedBlock.value = null
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiExample = `POST /api/v1/notify/send
|
||||||
|
Content-Type: application/json
|
||||||
|
notify-token: <你的API Token>
|
||||||
|
|
||||||
|
{
|
||||||
|
"channel_id": "渠道ID",
|
||||||
|
"title": "标题",
|
||||||
|
"text": "内容"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const shellExample = computed(() => `curl -s -X POST "http://${host.value}/api/v1/notify/send" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-H "notify-token: ${props.apiToken || 'YOUR_TOKEN'}" \\
|
||||||
|
-d '{"channel_id":"YOUR_CHANNEL_ID","title":"标题","text":"通知内容"}'`)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- API Token 管理卡片 -->
|
||||||
|
<Card class="border bg-card shadow-sm overflow-hidden">
|
||||||
|
<CardHeader class="pb-4">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<div class="p-1.5 rounded-md bg-primary/10 text-primary">
|
||||||
|
<Key class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<CardTitle class="text-base font-semibold">身份验证 (API Token)</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>用于外部脚本或工具调用 API 时的安全凭证</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="relative flex-1 group">
|
||||||
|
<Input
|
||||||
|
:model-value="apiToken"
|
||||||
|
readonly
|
||||||
|
placeholder="尚未生成 Token"
|
||||||
|
class="h-10 pr-10 bg-muted/30 border-muted-foreground/20 focus-visible:ring-primary/30 font-code text-sm tracking-tight"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="apiToken"
|
||||||
|
@click="copyToClipboard(apiToken, 'token')"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-primary cursor-pointer transition-colors p-1 rounded-md hover:bg-muted"
|
||||||
|
title="复制 Token"
|
||||||
|
>
|
||||||
|
<Check v-if="copiedBlock === 'token'" class="w-4 h-4 text-emerald-500 animate-in zoom-in" />
|
||||||
|
<Copy v-else class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="default" @click="emit('generateToken')" class="h-10 px-4 shrink-0 transition-all active:scale-95">
|
||||||
|
<RefreshCw class="w-3.5 h-3.5 mr-2" />
|
||||||
|
{{ apiToken ? '重新生成' : '生成 Token' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-2 p-3 rounded-lg bg-amber-500/5 border border-amber-500/10 text-[13px] text-amber-700 dark:text-amber-400">
|
||||||
|
<Info class="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
|
<p>请妥善保管您的 Token,一旦丢失需通过上方按钮重新生成。令牌将作为请求头中的 <code>notify-token</code> 字段发送。</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- API 接口规格 -->
|
||||||
|
<Card class="border bg-card shadow-sm flex flex-col overflow-hidden">
|
||||||
|
<CardHeader class="pb-3 shrink-0">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="p-1.5 rounded-md bg-emerald-500/10 text-emerald-600">
|
||||||
|
<FileJson class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<CardTitle class="text-sm font-bold uppercase tracking-wider">API 接口说明</CardTitle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="p-0 flex-1">
|
||||||
|
<div class="bg-zinc-950 dark:bg-black/40 p-5 font-code text-xs sm:text-sm leading-relaxed text-zinc-300 relative group h-full">
|
||||||
|
<div class="flex items-center justify-between mb-4 border-b border-zinc-700/50 pb-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge class="bg-emerald-600 text-white border-none py-0 px-2 text-[10px]">POST</Badge>
|
||||||
|
<code class="text-zinc-400">/api/v1/notify/send</code>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7 text-zinc-500 hover:text-white hover:bg-zinc-800 transition-all"
|
||||||
|
@click="copyToClipboard(apiExample, 'api')"
|
||||||
|
>
|
||||||
|
<Check v-if="copiedBlock === 'api'" class="w-3.5 h-3.5 text-emerald-500" />
|
||||||
|
<Copy v-else class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<span class="text-zinc-500 block mb-1 uppercase text-[10px] font-bold tracking-widest">Headers</span>
|
||||||
|
<div class="pl-2 space-y-1">
|
||||||
|
<p><span class="text-zinc-500">Content-Type:</span> application/json</p>
|
||||||
|
<p><span class="text-zinc-500">notify-token:</span> <span class="text-primary"><TOKEN></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-zinc-500 block mb-1 uppercase text-[10px] font-bold tracking-widest">Body (JSON)</span>
|
||||||
|
<div class="pl-2">
|
||||||
|
<p class="text-zinc-400">{</p>
|
||||||
|
<p class="pl-4">"channel_id": <span class="text-orange-400">"ID"</span>, <span class="text-zinc-500 font-sans italic text-[11px]">// 渠道唯一标识</span></p>
|
||||||
|
<p class="pl-4">"title": <span class="text-orange-400">"标题"</span>, <span class="text-zinc-500 font-sans italic text-[11px]">// 可选</span></p>
|
||||||
|
<p class="pl-4">"text": <span class="text-orange-400">"内容"</span> <span class="text-zinc-500 font-sans italic text-[11px]">// 必填</span></p>
|
||||||
|
<p class="text-zinc-400">}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Shell 示例 -->
|
||||||
|
<Card class="border bg-card shadow-sm flex flex-col overflow-hidden">
|
||||||
|
<CardHeader class="pb-3 shrink-0">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="p-1.5 rounded-md bg-sky-500/10 text-sky-600">
|
||||||
|
<Terminal class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<CardTitle class="text-sm font-bold uppercase tracking-wider">Shell 脚本示例</CardTitle>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 px-2 text-[10px] border-muted-foreground/30 hover:bg-muted transition-all"
|
||||||
|
@click="copyToClipboard(shellExample, 'shell')"
|
||||||
|
>
|
||||||
|
<Check v-if="copiedBlock === 'shell'" class="w-3 h-3 text-emerald-500 mr-1.5" />
|
||||||
|
<Copy v-else class="w-3 h-3 mr-1.5" />
|
||||||
|
一键复制
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="p-0 flex-1">
|
||||||
|
<div class="bg-zinc-950 dark:bg-black/40 p-5 font-code text-[12px] sm:text-[13px] leading-relaxed text-zinc-300 h-full">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p><span class="text-zinc-600"># 使用 CURL 调用推送接口</span></p>
|
||||||
|
<p>curl -s -X POST <span class="text-emerald-400">"http://{{ host }}/api/v1/notify/send"</span> \</p>
|
||||||
|
<p class="pl-4"> -H <span class="text-orange-400">"Content-Type: application/json"</span> \</p>
|
||||||
|
<p class="pl-4"> -H <span class="text-orange-400">"notify-token: {{ apiToken || 'YOUR_TOKEN' }}"</span> \</p>
|
||||||
|
<p class="pl-4"> -d <span class="text-orange-400">'{"channel_id":"ID","title":"任务完成","text":"脚本执行完毕"}'</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 pt-4 border-t border-zinc-800">
|
||||||
|
<span class="text-zinc-500 block mb-2 uppercase text-[10px] font-bold tracking-widest flex items-center gap-1.5">
|
||||||
|
<Hash class="w-3 h-3" /> 渠道 ID 快速查找
|
||||||
|
</span>
|
||||||
|
<div v-if="channels.length === 0" class="text-xs text-zinc-600 italic">暂无活跃渠道</div>
|
||||||
|
<div v-else class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<div v-for="ch in channels" :key="ch.id" class="flex items-center gap-2 text-xs bg-zinc-900 px-2 py-1.5 rounded border border-zinc-800 hover:border-zinc-700 transition-colors group">
|
||||||
|
<code class="text-primary font-bold tracking-tighter font-code">{{ ch.id.slice(0, 8) }}</code>
|
||||||
|
<span class="text-zinc-500 truncate max-w-[100px]">{{ ch.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import type { NotifyChannel, ChannelType } from '@/api'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open: boolean
|
||||||
|
isEditing: boolean
|
||||||
|
channel: Partial<NotifyChannel>
|
||||||
|
channelTypes: ChannelType[]
|
||||||
|
configFields: Record<string, { key: string; label: string; required: boolean; placeholder?: string; type?: string }[]>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
'update:channel': [channel: Partial<NotifyChannel>]
|
||||||
|
'type-change': [type: string]
|
||||||
|
save: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const currentConfigFields = computed(() => {
|
||||||
|
return props.configFields[props.channel.type || ''] || []
|
||||||
|
})
|
||||||
|
|
||||||
|
const enabledModel = computed({
|
||||||
|
get: () => props.channel.enabled ?? true,
|
||||||
|
set: (value) => updateChannelField('enabled', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateChannelField(field: string, value: any) {
|
||||||
|
emit('update:channel', { ...props.channel, [field]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConfigField(key: string, value: string) {
|
||||||
|
const newConfig = { ...props.channel.config, [key]: value }
|
||||||
|
emit('update:channel', { ...props.channel, config: newConfig })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
||||||
|
<DialogContent class="sm:max-w-lg max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ isEditing ? '编辑渠道' : '添加渠道' }}</DialogTitle>
|
||||||
|
<DialogDescription>配置消息推送渠道</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div class="space-y-4 py-2">
|
||||||
|
<div class="grid grid-cols-4 items-center gap-3">
|
||||||
|
<Label class="text-right text-sm">名称</Label>
|
||||||
|
<Input
|
||||||
|
:model-value="channel.name"
|
||||||
|
@update:model-value="updateChannelField('name', $event)"
|
||||||
|
placeholder="给渠道起个名字"
|
||||||
|
class="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 items-center gap-3">
|
||||||
|
<Label class="text-right text-sm">类型</Label>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<Select
|
||||||
|
:model-value="channel.type"
|
||||||
|
@update:model-value="(val: any) => emit('type-change', String(val))"
|
||||||
|
:disabled="isEditing"
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="选择渠道类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem v-for="ct in channelTypes" :key="ct.type" :value="ct.type">
|
||||||
|
{{ ct.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 items-center gap-3">
|
||||||
|
<Label class="text-right text-sm">启用</Label>
|
||||||
|
<Switch v-model="enabledModel" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentConfigFields.length > 0" class="border-t pt-4 mt-4">
|
||||||
|
<h4 class="text-sm font-medium mb-3 text-muted-foreground">渠道配置</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="field in currentConfigFields" :key="field.key" class="grid grid-cols-4 items-start gap-3">
|
||||||
|
<Label class="text-right text-sm pt-2">
|
||||||
|
{{ field.label }}
|
||||||
|
<span v-if="field.required" class="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<Input
|
||||||
|
v-if="!field.type || field.type !== 'textarea'"
|
||||||
|
:model-value="channel.config?.[field.key] || ''"
|
||||||
|
@update:model-value="updateConfigField(field.key, String($event))"
|
||||||
|
:placeholder="field.placeholder || ''"
|
||||||
|
class="text-sm"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-else
|
||||||
|
:value="channel.config?.[field.key] || ''"
|
||||||
|
@input="(e: Event) => updateConfigField(field.key, (e.target as HTMLTextAreaElement).value)"
|
||||||
|
:placeholder="field.placeholder || ''"
|
||||||
|
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring min-h-[80px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" @click="emit('update:open', false)">取消</Button>
|
||||||
|
<Button @click="emit('save')">保存</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Plus, Trash2, TestTube, Pencil, Bell } from 'lucide-vue-next'
|
||||||
|
import type { NotifyChannel, ChannelType } from '@/api'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
channels: NotifyChannel[]
|
||||||
|
channelTypes: ChannelType[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
add: []
|
||||||
|
edit: [channel: NotifyChannel]
|
||||||
|
delete: [id: string]
|
||||||
|
test: [channel: NotifyChannel]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function getChannelTypeName(type: string, channelTypes: ChannelType[]): string {
|
||||||
|
const found = channelTypes.find(t => t.type === type)
|
||||||
|
return found ? found.label : type
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>通知渠道</CardTitle>
|
||||||
|
<CardDescription>管理消息推送渠道配置</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" @click="emit('add')">
|
||||||
|
<Plus class="w-4 h-4 mr-1" />
|
||||||
|
添加渠道
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div v-if="channels.length === 0" class="text-center py-12 text-muted-foreground">
|
||||||
|
<Bell class="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||||
|
<p class="text-sm">暂无通知渠道</p>
|
||||||
|
<p class="text-xs mt-1">点击"添加渠道"开始配置</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="ch in channels"
|
||||||
|
:key="ch.id"
|
||||||
|
class="flex items-center justify-between p-3 rounded-lg border bg-card hover:bg-accent/30 transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
<div class="flex flex-col min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-sm truncate">{{ ch.name }}</span>
|
||||||
|
<Badge variant="secondary" class="text-[10px] shrink-0">{{ getChannelTypeName(ch.type, channelTypes) }}</Badge>
|
||||||
|
<Badge
|
||||||
|
:class="ch.enabled ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20' : 'bg-zinc-500/10 text-zinc-500 border-zinc-500/20'"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] shrink-0"
|
||||||
|
>
|
||||||
|
{{ ch.enabled ? '启用' : '禁用' }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click="emit('test', ch)" title="测试发送">
|
||||||
|
<TestTube class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" class="h-7 w-7" @click="emit('edit', ch)" title="编辑">
|
||||||
|
<Pencil class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive hover:text-destructive" @click="emit('delete', ch.id)" title="删除">
|
||||||
|
<Trash2 class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import { X, Plus, Shield, Terminal, Search } from 'lucide-vue-next'
|
||||||
|
import type { NotifyChannel, ChannelType, EventType, NotifyBinding, Task } from '@/api'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
channels: NotifyChannel[]
|
||||||
|
channelTypes: ChannelType[]
|
||||||
|
eventTypes: EventType[]
|
||||||
|
bindings: NotifyBinding[]
|
||||||
|
tasks: Task[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
save: [bindings: Partial<NotifyBinding>[]]
|
||||||
|
delete: [id: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// --- 状态管理 ---
|
||||||
|
const selectedTaskId = ref<string>('') // 默认不选中任何任务,强制用户选择
|
||||||
|
const selectedChannels = ref<Record<string, string>>({})
|
||||||
|
const taskSearchQuery = ref('')
|
||||||
|
const isTaskDropdownOpen = ref(false)
|
||||||
|
|
||||||
|
// 任务过滤逻辑
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
if (!taskSearchQuery.value) return props.tasks
|
||||||
|
const q = taskSearchQuery.value.toLowerCase()
|
||||||
|
return props.tasks.filter(t => t.name.toLowerCase().includes(q))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取当前选择的任务显示名称
|
||||||
|
const selectedTaskDisplay = computed(() => {
|
||||||
|
const task = props.tasks.find(t => t.id === selectedTaskId.value)
|
||||||
|
return task ? task.name : '请选择任务'
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectTask(id: string) {
|
||||||
|
selectedTaskId.value = id
|
||||||
|
isTaskDropdownOpen.value = false
|
||||||
|
taskSearchQuery.value = '' // 清空搜索词
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组事件
|
||||||
|
const systemEvents = computed(() => props.eventTypes.filter(e => e.binding_type === 'system'))
|
||||||
|
const taskEvents = computed(() => props.eventTypes.filter(e => e.binding_type === 'task'))
|
||||||
|
|
||||||
|
// 获取事件的绑定渠道
|
||||||
|
function getBindings(eventType: string, isSystem: boolean): NotifyBinding[] {
|
||||||
|
if (isSystem) {
|
||||||
|
return props.bindings.filter(b => b.event === eventType && b.type === 'system')
|
||||||
|
}
|
||||||
|
return props.bindings.filter(b => b.event === eventType && b.type === 'task' && b.data_id === selectedTaskId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取渠道名称与类型标签
|
||||||
|
function getChannelName(wayId: string): string {
|
||||||
|
const channel = props.channels.find(c => c.id === wayId)
|
||||||
|
return channel ? channel.name : '未知渠道'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChannelTypeLabel(wayId: string): string {
|
||||||
|
const channel = props.channels.find(c => c.id === wayId)
|
||||||
|
if (!channel) return ''
|
||||||
|
const type = props.channelTypes.find(t => t.type === channel.type)
|
||||||
|
return type ? type.label : channel.type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加渠道到事件
|
||||||
|
function addChannelToEvent(eventType: string, bindingType: 'system' | 'task') {
|
||||||
|
const dataId = bindingType === 'task' ? selectedTaskId.value : ''
|
||||||
|
const key = `${eventType}-${bindingType}-${dataId}`
|
||||||
|
const channelId = selectedChannels.value[key]
|
||||||
|
if (!channelId) return
|
||||||
|
|
||||||
|
const newBinding: Partial<NotifyBinding> = {
|
||||||
|
type: bindingType,
|
||||||
|
event: eventType,
|
||||||
|
way_id: channelId,
|
||||||
|
data_id: dataId
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('save', [newBinding])
|
||||||
|
selectedChannels.value[key] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可选的渠道
|
||||||
|
function getAvailableChannels(eventType: string, bindingType: 'system' | 'task') {
|
||||||
|
const isSystem = bindingType === 'system'
|
||||||
|
const boundChannelIds = getBindings(eventType, isSystem).map(b => b.way_id)
|
||||||
|
return props.channels.filter(c => !boundChannelIds.includes(c.id) && c.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除绑定
|
||||||
|
function removeBinding(binding: NotifyBinding) {
|
||||||
|
emit('delete', binding.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 系统事件卡片 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Shield class="w-5 h-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<CardTitle>系统事件</CardTitle>
|
||||||
|
<CardDescription>配置帐号登录、安全告警等系统级事件的通知推送</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div v-if="channels.length === 0"
|
||||||
|
class="text-center py-8 text-muted-foreground border-2 border-dashed rounded-lg">
|
||||||
|
<p class="text-sm">请先在“渠道管理”中添加通知渠道</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div v-for="event in systemEvents" :key="event.type"
|
||||||
|
class="flex flex-col p-4 rounded-lg border bg-card hover:bg-accent/10 transition-colors">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-bold text-sm">{{ event.label }}</span>
|
||||||
|
<Badge variant="outline" class="text-[10px] font-mono opacity-50">{{ event.type }}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已绑定渠道 -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] items-center">
|
||||||
|
<template v-if="getBindings(event.type, true).length > 0">
|
||||||
|
<div v-for="binding in getBindings(event.type, true)" :key="binding.id"
|
||||||
|
class="inline-flex items-center gap-1.5 px-2 py-1 rounded bg-secondary/50 border text-[11px] font-medium">
|
||||||
|
<span class="truncate max-w-[100px]">{{ getChannelName(binding.way_id) }}</span>
|
||||||
|
<button @click="removeBinding(binding)"
|
||||||
|
class="hover:text-destructive p-0.5 rounded-sm transition-colors">
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-[11px] text-muted-foreground italic">未绑定渠道</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加绑定 -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Select v-model="selectedChannels[`${event.type}-system-`]">
|
||||||
|
<SelectTrigger class="h-8 text-xs flex-1">
|
||||||
|
<SelectValue placeholder="添加通知渠道" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem v-for="ch in getAvailableChannels(event.type, 'system')" :key="ch.id" :value="ch.id">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs">{{ ch.name }}</span>
|
||||||
|
<Badge variant="outline" class="text-[9px]">{{ getChannelTypeLabel(ch.id) }}</Badge>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button size="sm" variant="outline" class="h-8 px-2" @click="addChannelToEvent(event.type, 'system')"
|
||||||
|
:disabled="!selectedChannels[`${event.type}-system-`]">
|
||||||
|
<Plus class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 任务事件卡片 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Terminal class="w-5 h-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<CardTitle>任务事件</CardTitle>
|
||||||
|
<CardDescription>配置定时任务执行状态的通知行为(需选择具体任务)</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full sm:w-auto min-w-[240px]">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 bg-transparent border border-input rounded-md px-3 h-9 focus-within:ring-1 focus-within:ring-ring/30 transition-all cursor-pointer"
|
||||||
|
@click="isTaskDropdownOpen = !isTaskDropdownOpen">
|
||||||
|
<Search class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
|
<div class="flex-1 text-xs truncate">
|
||||||
|
<span v-if="!isTaskDropdownOpen" class="font-medium"
|
||||||
|
:class="{ 'text-muted-foreground': !selectedTaskId }">{{ selectedTaskDisplay }}</span>
|
||||||
|
<input v-else v-model="taskSearchQuery"
|
||||||
|
class="w-full bg-transparent border-none outline-none p-0 text-xs" placeholder="搜索任务名..." @click.stop
|
||||||
|
autofocus />
|
||||||
|
</div>
|
||||||
|
<X v-if="taskSearchQuery || isTaskDropdownOpen"
|
||||||
|
class="w-3 h-3 text-muted-foreground hover:text-destructive"
|
||||||
|
@click.stop="taskSearchQuery = ''; isTaskDropdownOpen = false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义下拉列表 -->
|
||||||
|
<div v-if="isTaskDropdownOpen"
|
||||||
|
class="absolute top-full left-0 w-full mt-1 bg-card border border-border rounded-md shadow-xl z-50 max-h-[250px] overflow-auto py-1 animate-in fade-in zoom-in-95 duration-150">
|
||||||
|
<div v-if="filteredTasks.length === 0"
|
||||||
|
class="px-3 py-4 text-center text-[10px] text-muted-foreground italic">
|
||||||
|
未找到相关任务
|
||||||
|
</div>
|
||||||
|
<div v-for="task in filteredTasks" :key="task.id"
|
||||||
|
class="px-3 py-1.5 text-xs hover:bg-accent cursor-pointer flex items-center justify-between"
|
||||||
|
:class="{ 'bg-accent/50 font-medium': selectedTaskId === task.id }" @click="selectTask(task.id)">
|
||||||
|
<span class="truncate">{{ task.name }}</span>
|
||||||
|
<Badge v-if="selectedTaskId === task.id" variant="outline" class="h-4 p-0 px-1 text-[8px]">当前</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<!-- 未选择任务时的提示 -->
|
||||||
|
<div v-if="!selectedTaskId"
|
||||||
|
class="flex flex-col items-center justify-center py-12 text-muted-foreground border-2 border-dashed rounded-lg bg-accent/5">
|
||||||
|
<Terminal class="w-10 h-10 mb-3 opacity-20" />
|
||||||
|
<p class="text-sm font-medium">请在右上角选择需要配置的任务</p>
|
||||||
|
<p class="text-[11px] opacity-70 mt-1">每个任务可以独立配置通知渠道</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<!-- 任务特定模式提示 -->
|
||||||
|
<div class="flex items-center justify-between p-3 rounded-lg bg-primary/5 border border-primary/20">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge class="bg-primary/20 text-primary border-none text-[10px]">当前任务</Badge>
|
||||||
|
<span class="text-xs font-bold truncate max-w-[200px]">{{tasks.find(t => t.id === selectedTaskId)?.name
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="channels.length === 0"
|
||||||
|
class="text-center py-8 text-muted-foreground border-2 border-dashed rounded-lg">
|
||||||
|
<p class="text-sm">请先添加通知渠道</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="event in taskEvents" :key="event.type"
|
||||||
|
class="flex flex-col p-4 rounded-lg border bg-card hover:bg-accent/10 transition-colors">
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="font-bold text-sm">{{ event.label }}</span>
|
||||||
|
<Badge variant="outline" class="text-[10px] font-mono opacity-50">{{ event.type }}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已绑定渠道 -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-3 min-h-[32px] items-center">
|
||||||
|
<template v-if="getBindings(event.type, false).length > 0">
|
||||||
|
<div v-for="binding in getBindings(event.type, false)" :key="binding.id"
|
||||||
|
class="inline-flex items-center gap-1.5 px-2 py-1 rounded bg-secondary/50 border text-[11px] font-medium">
|
||||||
|
<span class="truncate max-w-[100px]">{{ getChannelName(binding.way_id) }}</span>
|
||||||
|
<button @click="removeBinding(binding)"
|
||||||
|
class="hover:text-destructive p-0.5 rounded-sm transition-colors">
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-[11px] text-muted-foreground italic">未绑定渠道</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加绑定 -->
|
||||||
|
<div class="flex gap-2 font-sans">
|
||||||
|
<Select v-model="selectedChannels[`${event.type}-task-${selectedTaskId}`]">
|
||||||
|
<SelectTrigger class="h-8 text-xs flex-1">
|
||||||
|
<SelectValue placeholder="添加通知渠道" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem v-for="ch in getAvailableChannels(event.type, 'task')" :key="ch.id" :value="ch.id">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs">{{ ch.name }}</span>
|
||||||
|
<Badge variant="outline" class="text-[9px]">{{ getChannelTypeLabel(ch.id) }}</Badge>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button size="sm" variant="outline" class="h-8 px-2" @click="addChannelToEvent(event.type, 'task')"
|
||||||
|
:disabled="!selectedChannels[`${event.type}-task-${selectedTaskId}`]">
|
||||||
|
<Plus class="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user