Skip to content

feat: 增强 Webhook 通知系统(#485)#505

Merged
ding113 merged 2 commits intodevfrom
fix/issue-485-webhook-review-fixes
Jan 2, 2026
Merged

feat: 增强 Webhook 通知系统(#485)#505
ding113 merged 2 commits intodevfrom
fix/issue-485-webhook-review-fixes

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 2, 2026

Summary

修复 PR #504 代码评审中发现的问题,包括代码重复、SSRF 安全增强、通知调度时机、以及代码健壮性改进。

Related Issues:


Problem

PR #504 在代码评审中发现以下问题需要修复:

  1. 代码重复 - notification-bindings 模块存在重复的查询逻辑和行映射代码
  2. 通知调度时机不当 - 更新绑定配置后未触发 scheduleNotifications() 重新调度,导致新配置不生效
  3. SSRF 防护不完整 - 未拦截部分云厂商元数据端点(如 metadata.google.internal、阿里云 100.100.100.200
  4. 代码复用不足 - 创建/更新 Webhook Target 的校验逻辑重复
  5. 时间格式化分散 - formatTimestamp() 函数在多处重复实现
  6. Switch 语句不完整 - createRenderer() 缺少 default 分支,无法处理未知 provider 类型

Solution

修复内容

问题 修复方案 影响文件
查询与行映射重复 去除 notification-bindings 查询逻辑中的重复代码 src/repository/notification-bindings.ts
调度时机 updateBindingsAction 中调用 scheduleNotifications() src/actions/notification-bindings.ts
SSRF 防护 增加 metadata.google.internal100.100.100.200、AWS IPv6 (fd00:ec2::254) 拦截 src/actions/webhook-targets.ts
校验复用 抽取 validateProviderConfig() 函数复用于创建/更新操作 src/actions/webhook-targets.ts
时间格式化 抽取公共 formatTimestamp()src/lib/webhook/utils/date.ts 多个 renderer 文件
Switch 完整性 createRenderer() 增加 default 分支并抛出明确错误 src/lib/webhook/renderers/index.ts

Changes

Core Changes

  • src/actions/notification-bindings.ts - 更新绑定后调用 scheduleNotifications() 触发重新调度
  • src/actions/webhook-targets.ts - 增强 SSRF 拦截(GCP/阿里云/AWS IPv6 元数据端点)+ 抽取 validateProviderConfig()
  • src/lib/webhook/renderers/index.ts - createRenderer() 增加 exhaustive check default 分支

Supporting Changes

  • src/lib/webhook/utils/date.ts - 新增 formatTimestamp() 公共函数
  • src/lib/webhook/renderers/wechat.ts - 移除重复的 formatTimestamp() 私有方法
  • src/lib/webhook/renderers/dingtalk.ts - 使用公共 formatTimestamp()
  • src/lib/webhook/renderers/telegram.ts - 使用公共 formatTimestamp()
  • src/repository/notification-bindings.ts - 优化查询逻辑,去除重复

Breaking Changes

无破坏性变更

本 PR 仅为代码质量和安全性修复,不改变任何公开 API 或数据库 Schema。


SSRF Protection Enhancement

新增拦截的云厂商元数据端点:

云厂商 端点 风险
AWS EC2 169.254.169.254 已有拦截 ✅
AWS IPv6 fd00:ec2::254 新增拦截
GCP metadata.google.internal 新增拦截
阿里云 100.100.100.200 新增拦截

Testing

Automated Tests

  • 单元测试通过 (bun run test)
  • 集成测试通过 (bun run test:integration)
  • E2E 测试通过

Validation

✅ bun run lint:fix
✅ bun run typecheck
✅ bun run test
✅ bun run build

Checklist

  • 代码遵循项目规范
  • 已完成自我评审
  • 测试通过
  • 无文档需要更新

Description enhanced by Claude AI

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求对系统的 Webhook 通知功能进行了全面升级和重构,解决了 Issue #485 中提到的评审问题。核心改动在于从单一 Webhook URL 的旧模式转向了一个更灵活、可配置的多目标通知系统。现在,用户可以创建和管理多个 Webhook 目标,这些目标支持多种主流平台(企业微信、飞书、钉钉、Telegram)以及高度定制化的通用 Webhook。每个通知类型(如熔断器告警、每日排行榜、成本预警)都可以绑定到一个或多个目标,并能独立配置调度规则和消息模板。此外,还增强了 Webhook 的安全性,加入了 SSRF 防护,并提供了代理支持。前端界面也进行了彻底的重写,以提供更友好的用户体验。

Highlights

  • 多目标通知系统: 将旧版单 URL 通知配置升级为灵活的多目标系统,允许用户定义多个 Webhook 目标并将其绑定到不同的通知类型。
  • 支持多种 Webhook 平台: 新增对钉钉、Telegram 和自定义 Webhook 的支持,同时保留了企业微信和飞书,并为每个平台提供了特定的配置选项。
  • Webhook 模板和占位符: 为自定义 Webhook 引入了强大的模板系统,支持使用占位符动态生成消息内容,并提供了默认模板。
  • SSRF 防护增强: 在 Webhook 目标管理中增加了严格的 SSRF(服务器端请求伪造)防护,阻止 Webhook 访问内部网络地址和云厂商元数据端点。
  • 通知绑定与调度: 引入了通知绑定机制,允许用户将特定通知类型(如熔断器告警、每日排行榜、成本预警)与一个或多个 Webhook 目标关联,并支持 Cron 表达式和时区配置进行精细调度。
  • 代理配置支持: 为 Webhook 目标增加了代理配置选项,支持 HTTP/HTTPS/SOCKS 代理,并可在代理失败时回退到直连。
  • 数据库架构更新: 新增 webhook_targetsnotification_target_bindings 表,并为 notification_settings 增加了 use_legacy_mode 字段,以支持新旧模式的平滑过渡。
  • 全面重构的通知设置 UI: 重新设计了通知设置页面,提供了直观的界面来管理 Webhook 目标、配置通知绑定、编辑自定义模板和测试 Webhook。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 2, 2026

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions github-actions bot added the size/XL Extra Large PR (> 1000 lines) label Jan 2, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 对通知系统进行了一次重大的、设计良好的重构,从旧版的单 URL 模式迁移到了灵活的多目标系统。这些改动范围广泛,涉及数据库结构、后端服务和前端组件。实现非常扎实,恰当地运用了 Server Actions、用于状态管理的自定义 Hooks 以及 Zod 进行验证。增加的 SSRF 防护是一个关键的安全增强,并且看起来相当全面。代码模块化程度高,新的 UI 组件结构清晰。我有一些关于提高代码健壮性和可维护性的建议,主要是在 switch 语句中确保穷尽性检查,以及使错误消息解析更具可移植性。总的来说,这是一次出色的 Pull Request,极大地增强了通知功能的能力和安全性。

Comment on lines +32 to +41
function toWebhookNotificationType(type: NotificationJobType): WebhookNotificationType {
switch (type) {
case "circuit-breaker":
return "circuit_breaker";
case "daily-leaderboard":
return "daily_leaderboard";
case "cost-alert":
return "cost_alert";
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了确保代码的健壮性和可维护性,建议在处理枚举类型的 switch 语句中加入 default 分支来处理未预期的值。这可以利用 TypeScript 的 never 类型在编译时捕获未经处理的枚举成员,防止未来添加新类型时遗漏相应的逻辑。此模式也适用于 toJobTypebuildTestData 等函数。

Suggested change
function toWebhookNotificationType(type: NotificationJobType): WebhookNotificationType {
switch (type) {
case "circuit-breaker":
return "circuit_breaker";
case "daily-leaderboard":
return "daily_leaderboard";
case "cost-alert":
return "cost_alert";
}
}
function toWebhookNotificationType(type: NotificationJobType): WebhookNotificationType {
switch (type) {
case "circuit-breaker":
return "circuit_breaker";
case "daily-leaderboard":
return "daily_leaderboard";
case "cost-alert":
return "cost_alert";
default: {
// 利用 never 类型在编译时检查穷尽性
const _exhaustiveCheck: never = type;
throw new Error(`未知的通知任务类型: ${type}`);
}
}
}

Comment on lines +62 to 67
private static detectProvider(webhookUrl: string): ProviderType {
const url = new URL(webhookUrl);
if (url.hostname === "qyapi.weixin.qq.com") return "wechat";
if (url.hostname === "open.feishu.cn") return "feishu";
throw new Error(`Unsupported webhook hostname: ${url.hostname}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

当前 detectProvider 方法仅能识别企业微信和飞书的 Webhook URL,这在旧版通知模式下可能会导致问题。虽然新系统强制用户选择类型,但为了增强旧模式的兼容性,建议扩展此方法以识别更多平台,例如钉钉。这可以提高代码的向后兼容性和健壮性。

  private static detectProvider(webhookUrl: string): ProviderType {
    const url = new URL(webhookUrl);
    if (url.hostname === "qyapi.weixin.qq.com") return "wechat";
    if (url.hostname === "open.feishu.cn") return "feishu";
    if (url.hostname === "oapi.dingtalk.com") return "dingtalk";
    // Telegram 和 custom 类型无法仅通过 URL 可靠地自动检测
    throw new Error(`不支持或无法自动检测的 Webhook 主机名: ${url.hostname}`);
  }

Comment on lines +133 to +137
(normalized.includes("use_legacy_mode") &&
(normalized.includes("does not exist") ||
normalized.includes("doesn't exist") ||
normalized.includes("找不到")))
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了提高代码的健壮性,建议移除对中文错误消息“找不到”的依赖。SQLSTATE 错误码(42703)和标准的英文错误消息(does not exist)是跨语言环境更可靠的判断依据。这样可以避免在数据库语言环境变化时导致此逻辑失效。

      (normalized.includes("use_legacy_mode") &&
        (normalized.includes("does not exist") ||
          normalized.includes("doesn't exist")))

@ding113 ding113 changed the title fix: 修复 Issue #485 webhook 通知评审问题 feat: 增强 Webhook 通知系统(#485) Jan 2, 2026
@ding113 ding113 merged commit 75c655e into dev Jan 2, 2026
18 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 2, 2026
const payload = this.renderer.render(message);
async send(message: StructuredMessage, options?: WebhookSendOptions): Promise<WebhookResult> {
const payload = this.renderer.render(message, options);
const url = this.getEndpointUrl();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [LOGIC-BUG] WebhookNotifier.send() computes payload/url outside try, so some failures skip retry + error mapping

File: src/lib/webhook/notifier.ts:44

Problematic code:
const payload = this.renderer.render(message, options);
const url = this.getEndpointUrl();

Why this is a problem: If render() or getEndpointUrl() throws (e.g. missing Telegram config / invalid custom template), the error bypasses withRetry() and the webhook_send_failed log + { success: false } return, so callers get a thrown exception instead of a WebhookResult.

Suggested fix:

async send(message: StructuredMessage, options?: WebhookSendOptions): Promise<WebhookResult> {
  try {
    const payload = this.renderer.render(message, options);
    const url = this.getEndpointUrl();

    return await withRetry(() => this.doSend(url, payload), {
      maxRetries: this.maxRetries,
      baseDelay: 1000,
    });
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logger.error({
      action: "webhook_send_failed",
      provider: this.providerType,
      error: errorMessage,
    });
    return { success: false, error: errorMessage };
  }
}

const secondOctet = parseInt(ipv6MappedMatch[1], 10);
if (secondOctet >= 16 && secondOctet <= 31) return true;
}
if (ipv6Hostname.startsWith("fc") || ipv6Hostname.startsWith("fd")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] [LOGIC-BUG] SSRF guard blocks any hostname starting with fc/fd

File: src/actions/webhook-targets.ts:74

Problematic code:
if (ipv6Hostname.startsWith("fc") || ipv6Hostname.startsWith("fd")) {

Why this is a problem: url.hostname can be a normal domain name (e.g. fc.example.com). This check will treat those as “internal” even though they’re not IPv6 ULA addresses (fc00::/7), causing valid webhook URLs to be rejected.

Suggested fix:

const ipv6Hostname = hostname.replace(/^\[|\]$/g, "");

// ... keep other IPv6 checks above

if (
  ipv6Hostname.includes(":") &&
  (ipv6Hostname.startsWith("fc") || ipv6Hostname.startsWith("fd"))
) {
  return true;
}

error: error instanceof Error ? error.message : "测试失败",
latencyMs,
});
} catch (_e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [ERROR-SILENT] Swallowed writeback error hides lastTestResult persistence failures

File: src/actions/webhook-targets.ts:469

Problematic code:

} catch (_e) {
  // 忽略写回失败
}

Why this is a problem: If updateTestResult() fails (DB outage, serialization error, etc.), the action returns an error but there’s no log signal that the UI state couldn’t be persisted. This makes “test button” failures much harder to debug.

Suggested fix:

} catch (writebackError) {
  logger.warn("测试结果写回失败", {
    targetId: id,
    latencyMs,
    error: writebackError instanceof Error ? writebackError.message : String(writebackError),
  });
}

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR is XL (8877 lines across 57 files) and spans DB schema/migrations, server actions, SSRF validation, notification scheduling, UI, and tests. The breadth increases merge risk and makes it harder to validate end-to-end behavior changes in webhook delivery.

PR Size: XL

  • Lines changed: 8877
  • Files changed: 57
  • Split suggestion: 1) DB schema + migrations 2) backend notification/webhook core 3) settings UI + i18n 4) tests + scripts

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 1 0
Security 0 0 0 0
Error Handling 0 2 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • src/lib/webhook/notifier.ts:44 [High] [LOGIC-BUG] Exceptions from render()/getEndpointUrl() bypass retry + error mapping (confidence: ~90)
  • src/actions/webhook-targets.ts:74 [Medium] [LOGIC-BUG] SSRF guard treats any fc*/fd* hostname as internal (confidence: ~92)
  • src/actions/webhook-targets.ts:469 [High] [ERROR-SILENT] updateTestResult() writeback failures are swallowed without logs (confidence: ~88)

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR enhances the webhook notification system with good overall implementation quality. The core fixes claimed in the PR description are verified and correctly implemented.

PR Size: XL

  • Lines changed: 8877 (8193 additions + 684 deletions)
  • Files changed: 57

⚠️ Recommendation: This PR is very large. Consider splitting future enhancements into smaller, focused PRs for easier review and safer deployment.


Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 1 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

High Priority Issue (Should Fix)

1. Missing Response Body in Error Logging [src/lib/webhook/notifier.ts:91]

Problem: When HTTP request fails (non-200 status), only status code and statusText are captured. The response body - which typically contains detailed API error messages from WeChat/Feishu/DingTalk/Telegram - is discarded.

Why it matters: Webhook providers return structured error responses explaining WHY the request failed (invalid token, rate limit, malformed payload, etc.). Without this information, debugging production webhook failures is nearly impossible.

Current code:

if (\!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

Suggested fix:

if (\!response.ok) {
  const body = await response.text();
  logger.error({
    action: "webhook_request_failed",
    status: response.status,
    statusText: response.statusText,
    responseBody: body,
  });
  throw new Error(`HTTP ${response.status}: ${response.statusText}. Response: ${body}`);
}

Verified Fixes ✅

The following issues from PR #504 review are correctly addressed:

  1. Code duplication - Removed in notification-bindings.ts
  2. Missing scheduleNotifications() call - Added in updateBindingsAction (line 63)
  3. SSRF protection - Enhanced with GCP (metadata.google.internal), Alibaba Cloud (100.100.100.200), and AWS IPv6 (fd00:ec2::254) metadata endpoints blocked
  4. validateProviderConfig() extraction - Successfully abstracted for reuse
  5. formatTimestamp() centralization - Moved to src/lib/webhook/utils/date.ts
  6. Switch exhaustiveness - createRenderer() now has proper default branch with never check

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - SSRF protection comprehensive
  • Error handling - One issue identified above
  • Type safety - Good use of exhaustive checks
  • Documentation accuracy - Clear
  • Test coverage - Good coverage for new renderers and notifier
  • Code clarity - Well-structured

Additional Observations

Strengths:

  • Excellent SSRF protection covering multiple cloud providers
  • Good test coverage for new webhook providers
  • Type-safe exhaustive checking in switch statements
  • Clean separation of concerns with renderer pattern

For Future Consideration:

  • Consider adding retry logic specifically for 5xx errors vs 4xx errors (4xx shouldn't retry)
  • Proxy fallback logic is well-designed
  • Template placeholder system is flexible and well-tested

Automated review by Claude AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied PR label size/XL on PR #505 (8877 lines changed across 57 files).

Posted 3 inline review comments (diff-only lines):

  • src/lib/webhook/notifier.ts:44WebhookNotifier.send() computes payload/url outside try, so some failures bypass retry + error mapping.
  • src/actions/webhook-targets.ts:74 — SSRF guard blocks any hostname starting with fc/fd (false positives for normal domains).
  • src/actions/webhook-targets.ts:469 — swallowed updateTestResult() writeback error hides persistence failures.

Submitted the required PR review summary via gh pr review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:i18n area:UI bug Something isn't working size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant

Comments