Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
097d02eb
authored
Feb 19, 2025
by
1808837298@qq.com
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: Implement comprehensive webhook notification system
parent
1ae0a384
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
197 additions
and
13 deletions
+197
-13
relay/channel/cloudflare/adaptor.go
+2
-1
service/cf_worker.go
+37
-9
service/user_notify.go
+13
-2
service/webhook.go
+118
-0
web/src/components/PersonalSetting.js
+27
-1
No files found.
relay/channel/cloudflare/adaptor.go
View file @
097d02eb
...
...
@@ -4,13 +4,14 @@ import (
"bytes"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"io"
"net/http"
"one-api/dto"
"one-api/relay/channel"
relaycommon
"one-api/relay/common"
"one-api/relay/constant"
"github.com/gin-gonic/gin"
)
type
Adaptor
struct
{
...
...
service/cf_worker.go
View file @
097d02eb
...
...
@@ -2,6 +2,7 @@ package service
import
(
"bytes"
"encoding/json"
"fmt"
"net/http"
"one-api/common"
...
...
@@ -9,19 +10,46 @@ import (
"strings"
)
// WorkerRequest Worker请求的数据结构
type
WorkerRequest
struct
{
URL
string
`json:"url"`
Key
string
`json:"key"`
Method
string
`json:"method,omitempty"`
Headers
map
[
string
]
string
`json:"headers,omitempty"`
Body
json
.
RawMessage
`json:"body,omitempty"`
}
// DoWorkerRequest 通过Worker发送请求
func
DoWorkerRequest
(
req
*
WorkerRequest
)
(
*
http
.
Response
,
error
)
{
if
!
setting
.
EnableWorker
()
{
return
nil
,
fmt
.
Errorf
(
"worker not enabled"
)
}
if
!
strings
.
HasPrefix
(
req
.
URL
,
"https"
)
{
return
nil
,
fmt
.
Errorf
(
"only support https url"
)
}
workerUrl
:=
setting
.
WorkerUrl
if
!
strings
.
HasSuffix
(
workerUrl
,
"/"
)
{
workerUrl
+=
"/"
}
// 序列化worker请求数据
workerPayload
,
err
:=
json
.
Marshal
(
req
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"failed to marshal worker payload: %v"
,
err
)
}
return
http
.
Post
(
workerUrl
,
"application/json"
,
bytes
.
NewBuffer
(
workerPayload
))
}
func
DoDownloadRequest
(
originUrl
string
)
(
resp
*
http
.
Response
,
err
error
)
{
if
setting
.
EnableWorker
()
{
common
.
SysLog
(
fmt
.
Sprintf
(
"downloading file from worker: %s"
,
originUrl
))
if
!
strings
.
HasPrefix
(
originUrl
,
"https"
)
{
return
nil
,
fmt
.
Errorf
(
"only support https url"
)
}
workerUrl
:=
setting
.
WorkerUrl
if
!
strings
.
HasSuffix
(
workerUrl
,
"/"
)
{
workerUrl
+=
"/"
req
:=
&
WorkerRequest
{
URL
:
originUrl
,
Key
:
setting
.
WorkerValidKey
,
}
// post request to worker
data
:=
[]
byte
(
`{"url":"`
+
originUrl
+
`","key":"`
+
setting
.
WorkerValidKey
+
`"}`
)
return
http
.
Post
(
setting
.
WorkerUrl
,
"application/json"
,
bytes
.
NewBuffer
(
data
))
return
DoWorkerRequest
(
req
)
}
else
{
common
.
SysLog
(
fmt
.
Sprintf
(
"downloading from origin: %s"
,
originUrl
))
return
http
.
Get
(
originUrl
)
...
...
service/user_notify.go
View file @
097d02eb
...
...
@@ -49,8 +49,19 @@ func NotifyUser(user *model.UserBase, data dto.Notify) error {
common
.
SysError
(
fmt
.
Sprintf
(
"user %d has no webhook url, skip sending webhook"
,
user
.
Id
))
return
nil
}
// TODO: 实现webhook通知
_
=
webhookURL
// 临时处理未使用警告,等待webhook实现
webhookURLStr
,
ok
:=
webhookURL
.
(
string
)
if
!
ok
{
common
.
SysError
(
fmt
.
Sprintf
(
"user %d webhook url is not string type"
,
user
.
Id
))
return
nil
}
// 获取 webhook secret
var
webhookSecret
string
if
secret
,
ok
:=
userSetting
[
constant
.
UserSettingWebhookSecret
];
ok
{
webhookSecret
,
_
=
secret
.
(
string
)
}
return
SendWebhookNotify
(
webhookURLStr
,
webhookSecret
,
data
)
}
return
nil
}
...
...
service/webhook.go
0 → 100644
View file @
097d02eb
package
service
import
(
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"one-api/dto"
"one-api/setting"
"time"
)
// WebhookPayload webhook 通知的负载数据
type
WebhookPayload
struct
{
Type
string
`json:"type"`
Title
string
`json:"title"`
Content
string
`json:"content"`
Values
[]
interface
{}
`json:"values,omitempty"`
Timestamp
int64
`json:"timestamp"`
}
// generateSignature 生成 webhook 签名
func
generateSignature
(
secret
string
,
payload
[]
byte
)
string
{
h
:=
hmac
.
New
(
sha256
.
New
,
[]
byte
(
secret
))
h
.
Write
(
payload
)
return
hex
.
EncodeToString
(
h
.
Sum
(
nil
))
}
// SendWebhookNotify 发送 webhook 通知
func
SendWebhookNotify
(
webhookURL
string
,
secret
string
,
data
dto
.
Notify
)
error
{
// 处理占位符
content
:=
data
.
Content
for
_
,
value
:=
range
data
.
Values
{
content
=
fmt
.
Sprintf
(
content
,
value
)
}
// 构建 webhook 负载
payload
:=
WebhookPayload
{
Type
:
data
.
Type
,
Title
:
data
.
Title
,
Content
:
content
,
Values
:
data
.
Values
,
Timestamp
:
time
.
Now
()
.
Unix
(),
}
// 序列化负载
payloadBytes
,
err
:=
json
.
Marshal
(
payload
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to marshal webhook payload: %v"
,
err
)
}
// 创建 HTTP 请求
var
req
*
http
.
Request
var
resp
*
http
.
Response
if
setting
.
EnableWorker
()
{
// 构建worker请求数据
workerReq
:=
&
WorkerRequest
{
URL
:
webhookURL
,
Key
:
setting
.
WorkerValidKey
,
Method
:
http
.
MethodPost
,
Headers
:
map
[
string
]
string
{
"Content-Type"
:
"application/json"
,
},
Body
:
payloadBytes
,
}
// 如果有secret,添加签名到headers
if
secret
!=
""
{
signature
:=
generateSignature
(
secret
,
payloadBytes
)
workerReq
.
Headers
[
"X-Webhook-Signature"
]
=
signature
workerReq
.
Headers
[
"Authorization"
]
=
"Bearer "
+
secret
}
resp
,
err
=
DoWorkerRequest
(
workerReq
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to send webhook request through worker: %v"
,
err
)
}
defer
resp
.
Body
.
Close
()
// 检查响应状态
if
resp
.
StatusCode
<
200
||
resp
.
StatusCode
>=
300
{
return
fmt
.
Errorf
(
"webhook request failed with status code: %d"
,
resp
.
StatusCode
)
}
}
else
{
req
,
err
=
http
.
NewRequest
(
http
.
MethodPost
,
webhookURL
,
bytes
.
NewBuffer
(
payloadBytes
))
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to create webhook request: %v"
,
err
)
}
// 设置请求头
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
// 如果有 secret,生成签名
if
secret
!=
""
{
signature
:=
generateSignature
(
secret
,
payloadBytes
)
req
.
Header
.
Set
(
"X-Webhook-Signature"
,
signature
)
}
// 发送请求
client
:=
GetImpatientHttpClient
()
resp
,
err
=
client
.
Do
(
req
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"failed to send webhook request: %v"
,
err
)
}
defer
resp
.
Body
.
Close
()
// 检查响应状态
if
resp
.
StatusCode
<
200
||
resp
.
StatusCode
>=
300
{
return
fmt
.
Errorf
(
"webhook request failed with status code: %d"
,
resp
.
StatusCode
)
}
}
return
nil
}
web/src/components/PersonalSetting.js
View file @
097d02eb
...
...
@@ -78,6 +78,7 @@ const PersonalSetting = () => {
webhookSecret
:
''
,
notificationEmail
:
''
});
const
[
showWebhookDocs
,
setShowWebhookDocs
]
=
useState
(
false
);
useEffect
(()
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
...
...
@@ -771,7 +772,32 @@ const PersonalSetting = () => {
placeholder
=
{
t
(
'请输入Webhook地址,例如: https://example.com/webhook'
)}
/
>
<
Typography
.
Text
type
=
"secondary"
style
=
{{
marginTop
:
8
,
display
:
'block'
}}
>
{
t
(
'系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求'
)}
{
t
(
'只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求'
)}
<
/Typography.Text
>
<
Typography
.
Text
type
=
"secondary"
style
=
{{
marginTop
:
8
,
display
:
'block'
}}
>
<
div
style
=
{{
cursor
:
'pointer'
}}
onClick
=
{()
=>
setShowWebhookDocs
(
!
showWebhookDocs
)}
>
{
t
(
'Webhook请求结构'
)}
{
showWebhookDocs
?
'▼'
:
'▶'
}
<
/div
>
<
Collapsible
isOpen
=
{
showWebhookDocs
}
>
<
pre
style
=
{{
marginTop
:
4
,
background
:
'var(--semi-color-fill-0)'
,
padding
:
8
,
borderRadius
:
4
}}
>
{
`{
"type": "quota_exceed", // 通知类型
"title": "标题", // 通知标题
"content": "通知内容", // 通知内容,支持 {{value}} 变量占位符
"values": ["值1", "值2"], // 按顺序替换content中的 {{value}} 占位符
"timestamp": 1739950503 // 时间戳
}
示例:
{
"type": "quota_exceed",
"title": "额度预警通知",
"content": "您的额度即将用尽,当前剩余额度为 {{value}}",
"values": ["$0.99"],
"timestamp": 1739950503
}`
}
<
/pre
>
<
/Collapsible
>
<
/Typography.Text
>
<
/div
>
<
/div
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment