新增 Webhook 通知渠道 - #1799
Conversation
…ification/webhook
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough后端新增 Webhook 推送提供者,并将 ChangesWebhook 推送契约与调用链
前端 Webhook 配置
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant AlertManagerImpl
participant PushManagerImpl
participant WebhookPushProvider
participant HTTPUtil
AlertManagerImpl->>PushManagerImpl: pushMessage(title, description, level)
PushManagerImpl->>WebhookPushProvider: push(title, description, level)
WebhookPushProvider->>HTTPUtil: 执行渲染后的 GET/POST 请求
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new Webhook push provider, enabling notifications to be sent to custom endpoints via GET or POST requests with configurable body templates and headers. The changes include the backend implementation for request execution and template rendering, alongside a frontend configuration interface with multi-language support. Review feedback suggests extending template rendering to the URL for dynamic GET parameters, removing redundant null checks for OkHttp response bodies, and implementing JSON escaping within templates to prevent malformed payloads when special characters are present.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (1)
189-202:DateTimeFormatter可缓存为静态常量。每次推送都会通过
DateTimeFormatter.ofPattern(...)重新构造三个实例,对热路径而言完全可避免。DateTimeFormatter是线程安全的,建议提取为类级常量。♻️ 建议
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @@ - OffsetDateTime now = OffsetDateTime.now(); - String date = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); - String time = now.format(DateTimeFormatter.ofPattern("HH:mm:ss")); - String datetime = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + OffsetDateTime now = OffsetDateTime.now(); + String date = now.format(DATE_FORMATTER); + String time = now.format(TIME_FORMATTER); + String datetime = now.format(DATETIME_FORMATTER);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java` around lines 189 - 202, renderTemplate currently constructs three DateTimeFormatter instances on every call; extract them as reusable class-level constants (e.g. private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"), TIME_FMT = DateTimeFormatter.ofPattern("HH:mm:ss"), DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) and replace the inline ofPattern(...) calls in renderTemplate with these constants (class WebhookPushProvider, method renderTemplate) to avoid repeated allocation and leverage DateTimeFormatter's thread-safety.webui/src/views/settings/components/config/components/push/forms/webhookForm.vue (2)
142-153:JSON.stringify比较对键序敏感,可能导致不必要的行重建。
JSON.stringify(fromRows)与JSON.stringify(next)在键内容相同但顺序不同时仍会判定为不相等,从而触发headerRows重建。这会丢失用户当前的行顺序与可能正在编辑中的焦点状态。常见触发场景:外部回填时后端返回的 headers 键序与本地rowsToHeaders按行顺序产出的键序不一致。可考虑改为按键集合 + 值的语义比较:
♻️ 建议改写
-watch( - () => model.value.headers, - (headers) => { - const fromRows = rowsToHeaders(headerRows.value) - const next = headers ?? {} - if (JSON.stringify(fromRows) === JSON.stringify(next)) { - return - } - headerRows.value = headersToRows(next) - } -) +const headersEqual = (a: Record<string, string>, b: Record<string, string>) => { + const ak = Object.keys(a) + const bk = Object.keys(b) + if (ak.length !== bk.length) return false + return ak.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]) +} + +watch( + () => model.value.headers, + (headers) => { + const fromRows = rowsToHeaders(headerRows.value) + const next = headers ?? {} + if (headersEqual(fromRows, next)) return + headerRows.value = headersToRows(next) + } +)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webui/src/views/settings/components/config/components/push/forms/webhookForm.vue` around lines 142 - 153, The current watch uses JSON.stringify to compare fromRows and next which is order-sensitive and can trigger unnecessary headerRows resets; instead implement an order-insensitive deep equality check (e.g., a headersEqual(from, next) that compares key sets and value equality for each key) and use that in the watch before deciding to reassign headerRows.value; locate the watch block that references model.value.headers, rowsToHeaders(headerRows.value) and headersToRows(next) and replace the JSON.stringify comparison with this headersEqual check to avoid rebuilding rows when only key order differs.
19-19: 在选项中使用枚举值作为标签和值。
Object.values(WebhookMethod)和Object.values(WebhookContentType)都返回字符串数组。ArcoDesign 的a-select组件接受字符串数组,并将字符串同时用作标签和值展示。由于GET/POST和application/json/text/plain都是通用术语,当前的用户体验是可以接受的。如果后续需要本地化或显示更友好的描述(例如"JSON (application/json)"),建议改为使用显式的
{ label, value }数组格式,但这可以在未来的优化中处理,不是当前 PR 必需的改动。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webui/src/views/settings/components/config/components/push/forms/webhookForm.vue` at line 19, 当前在 a-select 中直接使用 Object.values(WebhookMethod) 和 Object.values(WebhookContentType),它们返回字符串数组并被用作标签和值,这在现有场景可接受;若将来需要本地化或更友好的展示,请把这两个枚举映射为显式的 { label, value } 数组(例如在组件或一个 helper 中将 WebhookMethod/WebhookContentType 转为 { label, value } 列表),并将结果传给 a-select 的 :options(绑定到 model.method 等字段),以便后续替换为更友好的文本而不改动绑定逻辑。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 130-138: The current try/catch in WebhookPushProvider wraps an
IllegalStateException thrown for non-success HTTP responses with a generic
"Failed to send..." message, losing the original HTTP failure message; change
the error handling so HTTP-check exceptions are not double-wrapped — e.g., in
the method where you call
httpUtil.newBuilder().build().newCall(request).execute() keep the
response.isSuccessful() check as-is but replace the broad catch (Exception e)
with a narrower catch (IOException e) (or explicitly rethrow
IllegalStateException: if (e instanceof IllegalStateException) throw e;), so
network IO errors are still handled while the original IllegalStateException
containing the HTTP body/message bubbles up unmodified from WebhookPushProvider.
- Around line 189-202: The renderTemplate method currently inserts title/content
directly and can break JSON payloads; update renderTemplate (or its caller) to
accept or read the contentType and, when contentType equals "application/json",
JSON-escape values used in replacements (title, content, channelName returned by
name, and any other inserted fields), then perform the .replace calls with the
escaped strings (keep extractLevel(title) but pass an escaped title if it's also
placed into JSON); implement escaping that handles backslashes, double quotes,
control chars (newline, \r, tabs) and Unicode as needed so the produced JSON
remains valid.
- Around line 78-93: loadFromJson currently assumes
JsonUtil.getGson().fromJson(json, Config.class) returns a non-null Config; add a
null-check right after deserialization in loadFromJson and throw a clear
IllegalArgumentException (or custom config exception) stating the provider name
and that the config JSON is invalid so callers like
PushManagerImpl#createPushProvider receive a precise error instead of an NPE;
reference the Config class and return path to WebhookPushProvider only after the
config is validated and defaults (method, contentType, bodyTemplate, headers)
are applied.
- Around line 159-170: The Content-Type format isn't validated so
MediaType.parse(contentType) can return null and an invalid header gets sent;
update normalizeContentType to validate the incoming contentType by calling
MediaType.parse(contentType) and return a normalized valid string (or
null/empty) when parse fails, or alternatively modify createRequestBody to
handle a null MediaType: call MediaType.parse(contentType) there, and if it
returns null use a safe default MediaType (e.g., application/json or
application/octet-stream) and avoid setting an invalid header in
applyContentType; reference normalizeContentType, createRequestBody,
applyContentType, RequestBody.create and MediaType.parse when making the change.
In `@webui/src/views/settings/components/config/locale/en-US.ts`:
- Around line 171-172: The placeholder value for the localization key
'page.settings.tab.config.push.form.webhook.body_template.placeholder' uses a
CJK delimiter "、"; replace it with ASCII commas and spaces between variables
(e.g. "{l}title{r}, {l}content{r}, {l}level{r}, {l}date{r}, {l}time{r},
{l}datetime{r}, {l}channelName{r}") so the English UI uses proper comma+space
separation.
---
Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 189-202: renderTemplate currently constructs three
DateTimeFormatter instances on every call; extract them as reusable class-level
constants (e.g. private static final DateTimeFormatter DATE_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd"), TIME_FMT =
DateTimeFormatter.ofPattern("HH:mm:ss"), DATETIME_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) and replace the inline
ofPattern(...) calls in renderTemplate with these constants (class
WebhookPushProvider, method renderTemplate) to avoid repeated allocation and
leverage DateTimeFormatter's thread-safety.
In
`@webui/src/views/settings/components/config/components/push/forms/webhookForm.vue`:
- Around line 142-153: The current watch uses JSON.stringify to compare fromRows
and next which is order-sensitive and can trigger unnecessary headerRows resets;
instead implement an order-insensitive deep equality check (e.g., a
headersEqual(from, next) that compares key sets and value equality for each key)
and use that in the watch before deciding to reassign headerRows.value; locate
the watch block that references model.value.headers,
rowsToHeaders(headerRows.value) and headersToRows(next) and replace the
JSON.stringify comparison with this headersEqual check to avoid rebuilding rows
when only key order differs.
- Line 19: 当前在 a-select 中直接使用 Object.values(WebhookMethod) 和
Object.values(WebhookContentType),它们返回字符串数组并被用作标签和值,这在现有场景可接受;若将来需要本地化或更友好的展示,请把这两个枚举映射为显式的
{ label, value } 数组(例如在组件或一个 helper 中将 WebhookMethod/WebhookContentType 转为 {
label, value } 列表),并将结果传给 a-select 的 :options(绑定到 model.method
等字段),以便后续替换为更友好的文本而不改动绑定逻辑。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 41d2e1eb-5fd8-42c7-abef-e092336f8281
📒 Files selected for processing (9)
src/main/java/com/ghostchu/peerbanhelper/util/push/PushManagerImpl.javasrc/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.javawebui/src/api/model/push.tswebui/src/views/settings/components/config/components/push/editPush.vuewebui/src/views/settings/components/config/components/push/forms/webhookForm.vuewebui/src/views/settings/components/config/components/push/pushCard.vuewebui/src/views/settings/components/config/locale/en-US.tswebui/src/views/settings/components/config/locale/zh-CN.tswebui/src/views/settings/components/config/locale/zh-TW.ts
Ghost-chu
left a comment
There was a problem hiding this comment.
您好👋,感谢提交拉取请求。总的来说后端部分的代码质量相当不错,我已完成后端部分的代码审阅,仍有几个小问题需要更改,需要更改的部分已在 Review 中标出,还麻烦您查看。
要合并到主线中,前端部分仍需前端团队审阅。相关部分的更改将由 @Gaojianli 老师进行审阅。
此外,如果您愿意也可以将自己的 GitHub 用户名添加到 credit.txt 的 Contributors 部分中。再次感谢您的无私贡献 ;)
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (1)
185-194:Content-Type顺序:自定义 header 会覆盖默认Content-Type,请确认是预期行为。第 134 行先调用
applyContentType,再调用applyCustomHeaders;OkHttp 的Request.Builder.header(name, value)是覆盖语义,因此用户在headers中显式设置Content-Type会覆盖根据RequestBody推断的值(包括大小写归一化后的值,OkHttp 头匹配大小写不敏感)。一般这是合理的“用户优先”行为,但如果意图是“用户的自定义头不允许覆盖 body 实际的 Content-Type”,则需要在applyCustomHeaders中过滤掉Content-Type。建议在文档/UI 上说明,或在代码中按需调整。OkHttp Request.Builder.header vs addHeader case-insensitive override behavior🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java` around lines 185 - 194, The current flow calls applyContentType(...) before applyCustomHeaders(...), so custom headers can override the Content-Type inferred from the RequestBody via Request.Builder.header(...) — if you want to prevent user headers from replacing the body-derived Content-Type, update applyCustomHeaders to ignore any header whose name equals "Content-Type" (case-insensitive, use equalsIgnoreCase) and leave DEFAULT_CONTENT_TYPE and mediaType logic in applyContentType unchanged; alternatively, if you prefer “user wins”, document this behavior or move applyCustomHeaders to run after applyContentType — pick one approach and implement the corresponding change inside applyCustomHeaders or by swapping the call order that references applyContentType and applyCustomHeaders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 208-211: 删除未使用的 OffsetDateTime now 声明并将三次调用的
System.currentTimeMillis() 缓存为一个局部 long 变量(例如 millis),然后用该 millis 依次调用
TimeUtil.formatDateOnly, TimeUtil.formatTimeOnly, TimeUtil.formatDateTime 来初始化
date、time 和 datetime,确保在方法/块中不再引用 OffsetDateTime 并避免跨秒导致的不一致。
- Around line 130-131: The URL template replacements in WebhookPushProvider are
not URL-encoding variables (renderTemplate(config.getUrl(), ... , null)), which
breaks GET/query-string URLs; fix by adding a URL-encoding template renderer
(e.g., renderUrlTemplate or extend renderTemplate with a urlEncode mode) and use
URLEncoder.encode(value, StandardCharsets.UTF_8) for
{title},{content},{level},{date},{time},{datetime},{channelName} when building
renderedUrl (or whenever contentType==null/when method is GET), then replace the
current call in WebhookPushProvider to call that URL-encoding renderer so the
final Request.Builder.url(renderedUrl) receives a valid encoded URL.
---
Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 185-194: The current flow calls applyContentType(...) before
applyCustomHeaders(...), so custom headers can override the Content-Type
inferred from the RequestBody via Request.Builder.header(...) — if you want to
prevent user headers from replacing the body-derived Content-Type, update
applyCustomHeaders to ignore any header whose name equals "Content-Type"
(case-insensitive, use equalsIgnoreCase) and leave DEFAULT_CONTENT_TYPE and
mediaType logic in applyContentType unchanged; alternatively, if you prefer
“user wins”, document this behavior or move applyCustomHeaders to run after
applyContentType — pick one approach and implement the corresponding change
inside applyCustomHeaders or by swapping the call order that references
applyContentType and applyCustomHeaders.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d7990e4b-b823-4da0-b7e9-6bdbc2b1aaaf
📒 Files selected for processing (3)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.javasrc/main/resources/assets/credit.txtwebui/src/views/settings/components/config/locale/en-US.ts
✅ Files skipped from review due to trivial changes (2)
- src/main/resources/assets/credit.txt
- webui/src/views/settings/components/config/locale/en-US.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (1)
165-180:createRequestBody中存在不可达分支。
normalizeMethod(Lines 146–155)已将method限制为"GET"或"POST",因此 Line 166 已处理 GET,Line 174 之后method必为"POST",Line 177 的return null永远不会执行。可以简化逻辑,避免后续维护误读。♻️ 建议简化
private RequestBody createRequestBody(String method, String contentType, String bodyContent) { if ("GET".equals(method)) { return null; } MediaType mediaType = MediaType.parse(contentType); if (mediaType == null) { mediaType = MediaType.parse(DEFAULT_CONTENT_TYPE); } - if (bodyContent.isEmpty()) { - if ("POST".equals(method)) { - return RequestBody.create("", mediaType); - } - return null; - } return RequestBody.create(bodyContent, mediaType); }
RequestBody.create("", mediaType)和RequestBody.create(bodyContent, mediaType)在bodyContent为空字符串时等价,无需额外分支。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java` around lines 165 - 180, createRequestBody contains an unreachable branch because normalizeMethod already restricts method to "GET" or "POST"; remove the redundant empty-body conditional and the unreachable return null (the branch after checking POST) and simplify to: if method is "GET" return null, resolve mediaType with MediaType.parse(contentType) fallback to DEFAULT_CONTENT_TYPE, then always return RequestBody.create(bodyContent, mediaType) for POST (RequestBody.create("", mediaType) is equivalent when bodyContent is empty). Update the method createRequestBody accordingly and keep references to DEFAULT_CONTENT_TYPE and normalizeMethod intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 226-237: The method renderUrlTemplate currently references a
non-existent variable `template` and uses URLEncoder/StandardCharsets without
imports; change the returned expression to operate on the `urlTemplate`
parameter (i.e., use urlTemplate.replace(...)), add imports for
java.net.URLEncoder and java.nio.charset.StandardCharsets, and update the
encoder UnaryOperator in renderUrlTemplate to convert null->"" then
URLEncoder.encode(..., StandardCharsets.UTF_8) and replace "+" with "%20" to
avoid space->'+' issues; keep the existing uses of extractLevel(title),
TimeUtil.formatDateOnly/formatTimeOnly/formatDateTime(now) and name.
---
Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 165-180: createRequestBody contains an unreachable branch because
normalizeMethod already restricts method to "GET" or "POST"; remove the
redundant empty-body conditional and the unreachable return null (the branch
after checking POST) and simplify to: if method is "GET" return null, resolve
mediaType with MediaType.parse(contentType) fallback to DEFAULT_CONTENT_TYPE,
then always return RequestBody.create(bodyContent, mediaType) for POST
(RequestBody.create("", mediaType) is equivalent when bodyContent is empty).
Update the method createRequestBody accordingly and keep references to
DEFAULT_CONTENT_TYPE and normalizeMethod intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9652fefc-4405-4ce8-9d33-bc31b79d798b
📒 Files selected for processing (1)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java
|
忘了说了,已经好了ww 我更新一下PR说明的图 |
|
@Gaojianli 看一眼 |
not ok yet
2. Header表单缺乏验证
|
收到,以前没怎么写过这种,我再去修改一下 |
|
@Gaojianli header部分是只要key的校验还是key和content校验都需要?content要根据RFC 5987/8187转码吗 |
转码是后端的,你得考虑,key不校验的话会报错吧。 |
done |
|
header value不遵守RFC规范会直接拋异常 |
还真是哦,我再改改 |
done |
| Object.entries(headers).map(([key, value]) => ({ key, value })) | ||
|
|
||
| const headerRows = ref<HeaderRow[]>(createHeaderRows(model.value.headers)) | ||
| const headerTouched = ref<{ key: boolean; value: boolean }[]>( |
There was a problem hiding this comment.
这个是记录每个 header 行的 key 和 value 有没有被用户碰过的。不然用户加一行、value 填了 key 还没填的时候,报错立刻就跳出来了


增加 Webhook 用于没有需要的通知渠道情况下用户自定义
支持 Post/Get,支持json/plaintext
支持自定义消息模板,可用变量 {title} {content} {level} {date} {time} {datetime} {channelName}
截图


json post
plaintext post


get


第二页header

Summary by CodeRabbit