diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a9275b..1c595c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,29 +33,6 @@ jobs: - name: Run go vet run: go vet ./... - build: - name: Build - runs-on: ubuntu-latest - needs: test - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Go with cache - uses: ./.github/actions/setup-go - with: - go-version: "1.21" - - - name: Build - run: go build -v -o bin/feishu-github-tracker ./cmd/feishu-github-tracker - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: feishu-github-tracker-binary - path: bin/feishu-github-tracker - retention-days: 7 - docker: name: Build and Push Docker Image runs-on: ubuntu-latest diff --git a/.vscode/settings.json b/.vscode/settings.json index 4297ac6..e96b09d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -110,6 +110,7 @@ "zoneinfo" ], "markdownlint.config": { + "no-inline-html": false, "first-line-h1": false, "no-duplicate-heading": false }, diff --git a/QUICKSTART.md b/QUICKSTART.md index 2bcf2a0..00b120a 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -50,16 +50,41 @@ 4. 修改配置 - 编辑 `./configs/` 目录下的配置文件,参考 [README.md](README.md) or [configs](configs/) 目录下的示例配置文件的注释说明。你最可能需要修改的有下面几个内容: - - [./configs/server.yaml](configs/server.yaml):修改服务器监听地址和端口 - - [./configs/feishu-bots.yaml](configs/feishu-bots.yaml):配置飞书机器人的 Webhook URL 和别名 + - [./configs/server.yaml](configs/server.yaml):修改服务器监听地址,端口和自定义一个 `secret`(如果需要的话,如果测试用可以不设置) + - [./configs/feishu-bots.yaml](configs/feishu-bots.yaml):配置飞书机器人的 Webhook URL 和别名(每个可选添加配置模板选择)。 + - 如果不确定 `飞书机器人` 和 `Webhook URL` 是什么,可以参考 [这个文档](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot),在一个群组中创建一个机器人并复制其 Webhook URL 到这里。 - [./configs/repos.yaml](configs/repos.yaml):配置需要监听的 GitHub 仓库和事件,以及对应的通知对象 + - [./configs/templates.jsonc](configs/templates.jsonc):默认消息模板(可选:创建/使用 `templates.<自定义名称,如「cn」>.jsonc` 自定义模板) - 修改后保存,程序会在下一次收到 GitHub Webhook 请求时自动热重载最新配置。 -5. 简要调试 +5. 多模板配置(可选) + + 如果需要为不同的飞书 bot 配置不同的消息模板(如中英文双语),可以在 `./configs/feishu-bots.yaml` 中指定模板: + + ```yaml + feishu_bots: + - alias: 'team-cn' + url: 'https://open.feishu.cn/open-apis/bot/v2/hook/cn-webhook' + template: 'cn' # 使用中文模板,如不设置默认使用 templates.jsonc 英文模板 + ``` + + 也可以根据现有的修改并创建新的模版文件 `templates.<自定义名称>.jsonc`,然后在 `feishu-bots.yaml` 中引用。 + +6. 添加 GitHub Webhook + + - 进入你想监听的 GitHub 仓库,点击 `Settings` -> `Webhooks` -> `Add webhook` + - 在 `Payload URL` 中填入你的服务器地址,例如 `http://your-domain-or-ip:4594/webhook` + - 在 `Content type` 中选什么都可以,都支持 + - 在 `Secret` 中填入你在 `server.yaml` 中配置的 `secret`(如果配置了的话) + - 选择你想监听的事件类型,可以选 `仅push`,也可以选 `Everything` 然后在这个项目的 [configs/events.yaml](configs/events.yaml) 中更详细地选择你想要监听每个事件什么类型甚至什么分支上的事件;如果想要简单一些,也选择 `Let me select individual events` 然后勾选需要的事件,而在这边的 [./configs/repos.yaml](configs/repos.yaml) 你想监听的项目中选择 `all:` + - 点击 `Add webhook` 保存 + - **✅ 成功提示**:如果前面的步骤没有错误,几秒钟后你会在飞书群组中收到一条 "GitHub Webhook 添加成功" 的通知(这是 GitHub 发送的 ping 事件)。这表示 Webhook 已正确配置并能正常工作! + +7. 简要调试 - 若没有收到通知,请检查: - GitHub Webhook 配置(Payload URL、Secret、事件类型) - - `docker-compose logs -f` 中的错误日志 - - 本地修改配置后,尝试触发一次 GitHub 的 Webhook 事件,然后查看日志确认是否重载成功 + - `docker-compose logs -f` 中的错误日志,如果没有任何日志,请确认 GitHub Webhook 是否有成功发送请求 + - 修改配置后,尝试触发一次 GitHub 的 Webhook 事件,然后查看日志确认是否重载成功 -更多配置与高级用法请在启动容器后参见 [README.md](README.md) or [configs](configs/) 目录下的示例配置文件的注释说明 or [internal/handler/](internal/handler/) 目录下的文档。 +更多配置与高级用法请在启动容器后参见 [README.md](README.md) or [configs](configs/) 目录下的示例配置文件的注释说明 or [internal/handler/](internal/handler/) 目录下的相关文档 or [internal/template/](internal/template/) 目录下的消息模板说明。 diff --git a/README.md b/README.md index 5537b07..62b32b3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ 一个用于接收 GitHub Webhook 并转发到飞书机器人的中间件服务。支持灵活的配置、事件过滤和自定义消息模板。 +![logo](./assets/images/logo.png) + ## 写在前面 ### 为什么有这个项目 @@ -27,13 +29,66 @@ - 详见 [configs/events.yaml](configs/events.yaml) - 对应的处理方法以及文档详见 [internal/handler/](internal/handler/) - 默认提供的消息模板详见 [configs/templates.jsonc](configs/templates.jsonc) -- 也可以自定义模板,使用我们 `handler` 提供的的 `占位符变量` ([详见文档](internal/handler/README.md)) 对发出消息的格式做相应的修改 +- 也可以自定义模板,使用我们 `handler` 提供的的 `占位符变量` ([详见文档](internal/handler/README.md)) 以及 `template` 提供的 `模板引擎的语法` `过滤器` `条件块` 等功能 ([详见文档](internal/template/README.md)) 对发出消息的格式做相应的修改 + +### 🔔 Webhook 设置提醒 + +当您在 GitHub 上添加 Webhook 时(无论是仓库级别还是组织级别),GitHub 会发送一个 **ping 事件**来测试 Webhook 配置。本服务会: + +1. **自动识别 ping 事件**:无需在 `repos.yaml` 中特别配置 +2. **智能匹配通知目标**: + - 对于组织级 webhook:自动发送到配置了该组织所有仓库的飞书 bot, 即仅 `org-name/*` 模式匹配的仓库 + - 对于仓库级 webhook:自动发送到配置了该仓库的飞书 bot +3. **发送成功通知**:向飞书发送一条友好的 Webhook 设置成功消息,包含: + - GitHub 禅语(zen message) + - Hook ID 和类型 + - 仓库或组织信息 + +这样您就能立即确认 Webhook 已正确配置并能正常工作。 + +### 消息演示 + +#### Misc + +image + +image + +#### 支持双语,可以快速切换(所有卡片都有对应) + +image + +#### Workflow 通知 + +image + +#### Release + +image + +#### Issue 相关 + +image - 注意:模板引擎的语法、过滤器和条件块示例详见 `internal/template/README.md`,里面有占位符示例和进阶用法。 +#### PR 相关 + +image + +#### 其他事件(Star,Watch,等等,只要 GitHub 支持的我们都支持,详见上方说明) + +image ## 🚀 快速开始 -参考 [QUICKSTART.md](./QUICKSTART.md) 了解如何快速部署和测试。 +参考 [QUICKSTART.md](./QUICKSTART.md) 了解如何快速自建服务器部署和测试。 + +### 体验/使用现成服务(适合自己部署成本/难度较高的用户) + +当然我们也有部署好的服务(,直接 call 是没效果的,需要在我的服务器上更新配置才能转发到你那里 😈)可供大家使用或者尝试(下方邮件联系我获取试用和少量技术支持)。由于当前的服务是部署在个人服务器上需要运维成本,且配置当前依然需要人力维护,如果想要长期使用,需联系我付费 25¥ (含**永久使用权** + **1 年的不限次数配置更新&技术支持**,1 年后如需要更新配置或者技术支持,也请续费 10¥/年~~辛苦费~~)。 + +当然为了鼓励大家参与开源,**如果你能提一个有效的 PR(修复 bug 或者添加功能(请先提 issue))且最终被合并,或者提出一个详细的包含修复方法的 bug report issue**,试用款可以全额退回,后续也不需要支出任何额外费用。 + +如有想法可以邮件联系我:[hnrobert@qq.com](mailto:hnrobert@qq.com) ## 📁 项目结构 @@ -99,8 +154,34 @@ feishu_bots: - alias: 'org-notify' url: 'https://open.feishu.cn/open-apis/bot/v2/hook/zzzzzzz' + + - alias: 'org-cn-notify' + url: 'https://open.feishu.cn/open-apis/bot/v2/hook/aaaaaaa' + template: 'cn' # 可选:指定使用的消息模板,默认为 'default' ``` +**多模板支持**: + +从 v1.1.0 开始,支持为不同的飞书 bot 配置不同的消息模板。这在以下场景特别有用: + +- 中英文双语团队,需要发送不同语言的通知 +- 不同团队需要不同格式的消息 +- 测试环境和生产环境使用不同的消息格式 + +配置方法: + +1. 在 `feishu-bots.yaml` 中为 bot 指定 `template` 字段(可选) +2. 在 `configs/` 目录下创建对应的模板文件,命名格式为 `templates..jsonc` + +例如: + +- `templates.jsonc` - 默认模板(必需) +- `templates.cn.jsonc` - 中文模板 +- `templates.en.jsonc` - 英文模板 +- `templates.simple.jsonc` - 简化模板 + +如果某个 bot 没有指定 `template` 字段,或指定的模板文件不存在,将自动使用 `templates.jsonc` 作为默认模板。 + ### events.yaml 定义事件模板和具体事件配置: diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..14d6cc1 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/configs/events.yaml b/configs/events.yaml index 10a7633..e638a4c 100644 --- a/configs/events.yaml +++ b/configs/events.yaml @@ -9,7 +9,7 @@ # 注意:如果在模板里包含了一个完整事件(例如使用 `push:`), # 并且在该事件下没有进一步指定分支/类型等条件,我们会把它视为“全选”(匹配该事件的所有子类型)。 event_sets: - # "all" 模板:包含GitHub支持的全部事件(每个事件都列出完整定义,便于模板内直接细化) + # "all" 模板:包含GitHub支持的全部事件(包含所有事件) all: branch_protection_configuration: branch_protection_rule: @@ -88,8 +88,8 @@ event_sets: workflow_dispatch: workflow_job: workflow_run: - - # "basic" 模板:适合中小型项目(将事件的完整定义原样列出,可在模板里覆盖或细化) + + # "basic" 模板:适合中小型项目,一般常用事件 basic: discussion: discussion_comment: @@ -101,7 +101,7 @@ event_sets: pull_request_review_comment: push: release: - + # 自定义模板:可按需添加事件并细化自定义新的 event_sets(示例) custom: discussion: @@ -132,28 +132,55 @@ event_sets: - published - edited -# 下面是完整事件列表,供模板引用或自定义精细化配置,也可以直接在 repos.yaml 中每个仓库的 events 中使用 + org: + organization: + org_block: + team: + team_add: + member: + membership: + repository: + repository_vulnerability_alert: + secret_scanning_alert: + secret_scanning_alert_location: + security_advisory: + security_and_analysis: + github_app_authorization: + installation: + installation_repositories: + installation_target: + marketplace_purchase: + sponsorship: + personal_access_token_request: + meta: + +# 下面是完整事件列表,供模板引用或自定义精细化配置,也可以直接在 repos.yaml 中每个仓库的 events 中使用。如果要 CV 请注意缩进。 events: + # 在仓库分支保护配置启用/禁用时触发(仓库或组织级别的分支保护设置变更) branch_protection_configuration: types: - disabled - enabled + # 当单个分支保护规则被创建/删除/编辑时触发 branch_protection_rule: types: - created - deleted - edited + # 单个 check run 的生命周期事件(CI 检查) check_run: types: - created - completed - rerequested - requested_action + # check suite(多个 check run 的集合)相关事件 check_suite: types: - completed - requested - rerequested + # 代码扫描发现的告警/漏洞相关事件 code_scanning_alert: types: - appeared_in_branch @@ -162,20 +189,26 @@ events: - fixed - reopened - reopened_by_user + # 对 commit 的评论 commit_comment: types: - created + # 创建分支或标签时触发 create: + # 自定义属性(如项目自定义字段)被操作时触发 custom_property: types: - created - deleted - promote_to_enterprise - updated + # 自定义属性的值发生变更时触发 custom_property_values: types: - updated + # 删除分支或标签时触发 delete: + # Dependabot 报警相关事件 dependabot_alert: types: - auto_dismissed @@ -185,20 +218,26 @@ events: - fixed - reintroduced - reopened + # 部署密钥创建/删除 deploy_key: types: - created - deleted + # 部署(deployment)创建或相关操作 deployment: + # 部署保护规则请求(如需要人工批准) deployment_protection_rule: types: - requested + # 部署审查(批准/拒绝/请求) deployment_review: types: - approved - rejected - requested + # 部署状态更新 deployment_status: + # 仓库讨论(discussion)及其状态变更 discussion: types: - created @@ -213,16 +252,21 @@ events: - unlabeled - answered - unanswered + # 对讨论的评论 discussion_comment: types: - created - edited - deleted + # 仓库被 fork fork: + # GitHub App 授权/撤销等事件 github_app_authorization: types: - revoked + # wiki (gollum) 页面变更 gollum: + # GitHub App 安装/卸载/权限变更 installation: types: - created @@ -230,24 +274,29 @@ events: - new_permissions_accepted - suspend - unsuspend + # 安装的仓库被添加或移除 installation_repositories: types: - added - removed + # 安装目标(target)被重命名等 installation_target: types: - renamed + # issue 上的评论 issue_comment: types: - created - edited - deleted + # issue 依赖关系变更 issue_dependencies: types: - blocked_by_added - blocked_by_removed - blocking_added - blocking_removed + # issue 本身的生命周期(打开/关闭/重新打开等) issues: types: - opened @@ -268,11 +317,13 @@ events: - unlocked - typed - untyped + # 标签操作(创建/删除/编辑) label: types: - created - deleted - edited + # Marketplace 购买/变更事件 marketplace_purchase: types: - cancelled @@ -280,22 +331,27 @@ events: - pending_change - pending_change_cancelled - purchased + # 组织成员(member)被添加/移除/编辑 member: types: - added - removed - edited + # 团队成员关系变更(membership) membership: types: - added - removed + # merge group 相关事件(例如 checks 请求被创建/销毁) merge_group: types: - checks_requested - destroyed + # 元数据事件(如 meta 信息被删除) meta: types: - deleted + # milestone 生命周期事件 milestone: types: - created @@ -303,27 +359,34 @@ events: - opened - edited - deleted + # 组织阻止(block)/解锁用户 org_block: types: - blocked - unblocked + # 组织级别操作(成员/邀请等) organization: types: - member_added - member_removed - member_invited + # 包(package)发布/更新事件 package: types: - published - updated + # GitHub Pages 构建事件 page_build: + # 个人访问令牌请求(PAT)相关的审批/创建/拒绝等 personal_access_token_request: types: - approved - cancelled - created - denied + # Webhook 的 ping(当创建 webhook 时 GitHub 发送以测试连通性) ping: + # classic project(项目)的事件(创建/更新/关闭等) project: types: - created @@ -331,6 +394,7 @@ events: - closed - reopened - deleted + # project 卡片的变更 project_card: types: - created @@ -338,12 +402,14 @@ events: - converted - edited - deleted + # project 列的变更 project_column: types: - created - updated - moved - deleted + # projects v2(新版项目)整体事件 projects_v2: types: - closed @@ -351,6 +417,7 @@ events: - deleted - edited - reopened + # projects v2 中单个 item 的事件 projects_v2_item: types: - archived @@ -360,12 +427,15 @@ events: - edited - reordered - restored + # projects v2 状态更新 projects_v2_status_update: types: - created - deleted - edited + # 仓库从私有变为公开等 public 事件 public: + # Pull Request 事件(创建/同步/合并等) pull_request: branches: - "*" @@ -391,27 +461,33 @@ events: - unlocked - milestoned - demilestoned + # pull request 的代码评审(review)相关事件 pull_request_review: types: - submitted - edited - dismissed + # pull request 中的 review comment(审查评论) pull_request_review_comment: types: - created - edited - deleted + # review comment 线程(thread)被解决/未解决 pull_request_review_thread: types: - resolved - unresolved + # push 事件(提交被推送到仓库)——通常关心分支过滤 push: branches: - "*" + # 注册到 package registry 的包的发布/更新事件 registry_package: types: - published - updated + # release(发布)相关事件,例如创建/发布/撤销预发布 release: types: - published @@ -421,6 +497,7 @@ events: - deleted - prereleased - released + # 仓库本身的生命周期事件(创建/删除/归档/重命名等) repository: types: - created @@ -432,23 +509,29 @@ events: - transferred - publicized - privatized + # 与安全或合规相关的仓库建议或公告 repository_advisory: types: - published - reported + # repository_dispatch:手动/外部触发的自定义事件 repository_dispatch: + # repository_import:仓库导入相关事件(例如从另一个平台导入) repository_import: + # 仓库规则集(ruleset)创建/删除/编辑 repository_ruleset: types: - created - deleted - edited + # 仓库漏洞告警(vulnerability alert) repository_vulnerability_alert: types: - create - dismiss - reopen - resolve + # Secret scanning 报警(如检测到泄露的密钥) secret_scanning_alert: types: - created @@ -456,18 +539,23 @@ events: - reopened - resolved - validated + # 密钥泄露位置相关通知 secret_scanning_alert_location: types: - created + # 密钥扫描任务完成事件 secret_scanning_scan: types: - completed + # 安全通告(advisory)发布/更新/撤回等 security_advisory: types: - published - updated - withdrawn + # 与安全及分析相关的设置变更 security_and_analysis: + # 赞助(sponsorship)相关事件,例如创建/取消等 sponsorship: types: - cancelled @@ -476,17 +564,21 @@ events: - pending_cancellation - pending_tier_change - tier_changed + # star(给仓库加星)事件 star: types: - created - deleted + # 状态(status)通常与 CI 状态/Checks 相关 status: + # 子任务/子 issue 相关事件(如果使用子 issue 功能) sub_issues: types: - parent_issue_added - parent_issue_removed - sub_issue_added - sub_issue_removed + # 团队(team)相关的创建/删除/编辑及权限变化 team: types: - created @@ -494,17 +586,22 @@ events: - edited - added_to_repository - removed_from_repository + # team_add:将团队添加到仓库的简短事件 team_add: + # watch/star 事件(用户开始关注/开始看仓库) watch: types: - started + # workflow_dispatch:手动触发的 GitHub Actions workflow workflow_dispatch: + # workflow_job:GitHub Actions 中单个 job 的状态更新 workflow_job: types: - completed - in_progress - queued - waiting + # workflow_run:整个 workflow 的运行生命周期事件 workflow_run: types: - completed diff --git a/configs/feishu-bots.yaml b/configs/feishu-bots.yaml index 2e37516..af67cc1 100644 --- a/configs/feishu-bots.yaml +++ b/configs/feishu-bots.yaml @@ -14,3 +14,7 @@ feishu_bots: - alias: "org-notify" url: "https://open.feishu.cn/open-apis/bot/v2/hook/zzzzzzz" + + - alias: "org-cn-notify" + url: "https://open.feishu.cn/open-apis/bot/v2/hook/zzzzzzz" + template: "cn" diff --git a/configs/templates.cn.jsonc b/configs/templates.cn.jsonc index 509f8d9..39bcf19 100644 --- a/configs/templates.cn.jsonc +++ b/configs/templates.cn.jsonc @@ -122,7 +122,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**规则:** {{rule_name}}\n**仓库:** {{repository_link_md}}\n**操作:** {{action}}\n" + "content": "**仓库:** {{repository_link_md}}\n**规则:** {{rule_name}}\n**操作:** {{action}}\n" } } ] @@ -294,7 +294,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**检查:** {{check_run.name}}\n**仓库:** {{repository_link_md | default('')}}\n**状态:** {{check_run.status}}\n**结论:** {{check_run.conclusion}}\n" + "content": "**仓库:** {{repository_link_md | default('')}}**检查:** {{check_run.name}}\n\n**状态:** {{check_run.status}}\n**结果:** {{check_run.conclusion}}\n" } } ] @@ -324,7 +324,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**检查:** {{check_run.name}}\n**仓库:** {{repository_link_md | default('')}}\n**状态:** {{check_run.status}}\n**结论:** {{check_run.conclusion}}\n" + "content": "**仓库:** {{repository_link_md | default('')}}\n**检查:** {{check_run.name}}\n**状态:** {{check_run.status}}\n**结果:** {{check_run.conclusion}}\n" } } ] @@ -353,7 +353,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**检查:** {{check_run.name}}\n**仓库:** {{repository_link_md | default('')}}\n**状态:** {{check_run.status}}\n**结论:** {{check_run.conclusion}}\n" + "content": "**仓库:** {{repository_link_md | default('')}}\n**检查:** {{check_run.name}}\n**状态:** {{check_run.status}}\n**结果:** {{check_run.conclusion}}\n" } } ] @@ -524,7 +524,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**结论:** {{check_suite.conclusion | default('N/A')}}\n**提交:** {{check_suite.head_sha}}\n**仓库:** {{repository_link_md | default('')}}\n" + "content": "**仓库:** {{repository_link_md | default('')}}\n**结果:** {{check_suite.conclusion | default('N/A')}}\n**提交:** {{check_suite.head_sha}}\n" } } ] @@ -695,7 +695,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**警告:** {{alert_rule_description}}\n**严重度:** {{alert_rule_severity}}\n**状态:** {{alert_state}}\n**仓库:** {{repository_link_md}}\n" + "content": "**仓库:** {{repository_link_md}}\n**警告:** {{alert_rule_description}}\n**严重度:** {{alert_rule_severity}}\n**状态:** {{alert_state}}\n" } } ] @@ -1401,7 +1401,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**包:** {{alert_package_name}}\n**严重度:** {{alert_severity}}\n**状态:** {{alert_state}}\n**仓库:** {{repository_link_md}}\n" + "content": "**仓库:** {{repository_link_md}}\n**包:** {{alert_package_name}}\n**严重度:** {{alert_severity}}\n**状态:** {{alert_state}}\n" } } ] @@ -3902,7 +3902,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🐛 **发现 Bug**,报告者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "🐛 **发现 Bug**,报告者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -3946,7 +3946,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✨ **新功能请求**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "✨ **新功能请求**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -3990,7 +3990,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📋 **任务创建**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "📋 **任务创建**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4034,7 +4034,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **Bug 已修复**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **Bug 已修复**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4078,7 +4078,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **功能已完成**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **功能已完成**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4122,7 +4122,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **任务已完成**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **任务已完成**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4166,7 +4166,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **Bug 已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **Bug 已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4210,7 +4210,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **功能已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **功能已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4254,7 +4254,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **任务已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **任务已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4297,7 +4297,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📝 **Issue 已开启**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "📝 **Issue 已开启**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**描述:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4340,7 +4340,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **Issue 已关闭**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **Issue 已关闭**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**状态:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4383,7 +4383,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **Issue 已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **Issue 已重新开启**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**标签:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4426,7 +4426,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "👤 **Issue 已分配**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**分配给:** {{assignee_link_md}}{{/if}}" + "content": "👤 **Issue 已分配**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**分配给:** {{assignee_link_md}}{{/if}}" } }, { @@ -4469,7 +4469,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "👤 **Issue 已取消分配**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**移除分配:** {{assignee_link_md}}{{/if}}" + "content": "👤 **Issue 已取消分配**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**移除分配:** {{assignee_link_md}}{{/if}}" } }, { @@ -4512,7 +4512,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✏️ **Issue 已编辑**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_title_from}}**标题改自:** {{changes_title_from}}{{/if}}" + "content": "✏️ **Issue 已编辑**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_title_from}}**标题改自:** {{changes_title_from}}{{/if}}" } }, { @@ -4555,7 +4555,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🎯 **里程碑已添加**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**里程碑:** {{milestone_title}}{{/if}}" + "content": "🎯 **里程碑已添加**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**里程碑:** {{milestone_title}}{{/if}}" } }, { @@ -4598,7 +4598,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🎯 **里程碑已移除**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**移除里程碑:** {{milestone_title}}{{/if}}" + "content": "🎯 **里程碑已移除**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**移除里程碑:** {{milestone_title}}{{/if}}" } }, { @@ -4641,7 +4641,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📦 **Issue 已转移**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_new_repository_full_name}}**转移到仓库:** {{changes_new_repository_full_name}}{{/if}}" + "content": "📦 **Issue 已转移**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_new_repository_full_name}}**转移到仓库:** {{changes_new_repository_full_name}}{{/if}}" } }, { @@ -4684,7 +4684,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🗑️ **Issue 已删除**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "🗑️ **Issue 已删除**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } } ] @@ -4713,7 +4713,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📌 **Issue 已置顶**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "📌 **Issue 已置顶**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -4756,7 +4756,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📌 **Issue 已取消置顶**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "📌 **Issue 已取消置顶**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -4799,7 +4799,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔒 **Issue 已锁定**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.active_lock_reason}}**原因:** {{issue.active_lock_reason}}{{/if}}" + "content": "🔒 **Issue 已锁定**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.active_lock_reason}}**原因:** {{issue.active_lock_reason}}{{/if}}" } }, { @@ -4842,7 +4842,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔓 **Issue 已解锁**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "🔓 **Issue 已解锁**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -4885,7 +4885,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**标签:** {{labeled_label_link_md | default(labeled_label_name)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**操作者:** {{sender_link_md | default(sender.login)}}\n" + "content": "**标签:** {{labeled_label_link_md | default(labeled_label_name)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**操作者:** {{sender_link_md | default(sender.login)}}\n" } }, { @@ -4928,7 +4928,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**标签:** {{labeled_label_link_md | default(labeled_label_name)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**操作者:** {{sender_link_md | default(sender.login)}}\n" + "content": "**标签:** {{labeled_label_link_md | default(labeled_label_name)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**操作者:** {{sender_link_md | default(sender.login)}}\n" } }, { @@ -4971,7 +4971,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✏️ **Issue 已设置类型**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_type}}**类型:** {{issue_type}}{{/if}}" + "content": "✏️ **Issue 已设置类型**,操作者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_type}}**类型:** {{issue_type}}{{/if}}" } }, { @@ -5014,7 +5014,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✏️ **Issue 类型已移除**,操作者:{{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "✏️ **Issue 类型已移除**,操作者:{{sender_link_md | default(sender.login)}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -5057,7 +5057,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🐛 **发现 Bug**,报告者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "🐛 **发现 Bug**,报告者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5100,7 +5100,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✨ **功能需求**,提出者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "✨ **功能需求**,提出者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5143,7 +5143,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📝 **任务**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "📝 **任务**,创建者:{{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5186,7 +5186,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "⚠️ **Issue**,创建者:{{sender_link_md | default(sender.login)}} {{issue_type_display | default(issue_user_link_md | default(''))}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "⚠️ **Issue**,创建者:{{sender_link_md | default(sender.login)}} {{issue_type_display | default(issue_user_link_md | default(''))}}\n仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5229,7 +5229,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**作者:** {{sender_link_md | default(sender.login)}}\n**状态:** {{issue.state | default('unknown')}}\n" + "content": "仓库:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**作者:** {{sender_link_md | default(sender.login)}}\n**状态:** {{issue.state | default('unknown')}}\n" } }, { @@ -5276,7 +5276,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**标签:** {{label_name}}\n**操作:** {{action}}\n**仓库:** {{repository_link_md}}\n" + "content": "**仓库:** {{repository_link_md}}\n**标签:** {{label_name}}\n**操作:** {{action}}\n" } } ] @@ -5576,7 +5576,7 @@ "header": { "title": { "tag": "plain_text", - "content": "🔔 市场购买 待变更已取消" + "content": "🔔 市场购买 待变更项目已取消" }, "template": "yellow" }, @@ -5681,7 +5681,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**成员:** {{member.login}}\n**动作:** {{action}}\n" + "content": "**成员:** {{member.login}}\n**操作:** {{action}}\n" } } ] @@ -5852,7 +5852,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**成员:** {{member.user.login}}\n**动作:** {{action}}\n" + "content": "**成员:** {{member.user.login}}\n**操作:** {{action}}\n" } } ] @@ -7019,7 +7019,7 @@ "header": { "title": { "tag": "plain_text", - "content": "🏓 Webhook测试" + "content": "✅ GitHub Webhook 添加成功" }, "template": "green" }, @@ -7028,7 +7028,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**禅语:** {{zen}}\n**Hook ID:** {{hook_id}}\n" + "content": "**GitHub 寄语:** {{zen}}\n\n{{#if repository}}**仓库:** [{{repository.full_name}}]({{repository.html_url}})\n{{/if}}{{#if organization}}**组织:** [{{organization.login}}](https:\/\/github.com/{{organization.login}})\n{{/if}}**Hook ID:** {{hook_id}}\n**Hook 类型:** {{hook_type}}" // fuck jsonc } } ] @@ -7061,7 +7061,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**项目:** {{project.name}}\n**动作:** {{action}}\n" + "content": "**项目:** {{project.name}}\n**操作:** {{action}}\n" } } ] @@ -7324,7 +7324,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**卡片:** {{project_card.note | default('无注释')}}\n**动作:** {{action}}\n" + "content": "**卡片:** {{project_card.note | default('无注释')}}\n**操作:** {{action}}\n" } } ] @@ -7587,7 +7587,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**列名:** {{project_column.name}}\n**动作:** {{action}}\n" + "content": "**列名:** {{project_column.name}}\n**操作:** {{action}}\n" } } ] @@ -8994,7 +8994,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**仓库:** {{repository_link_md}}\n**PR:** [#{{pr_number}} {{pr_title}}]({{pr_url}})\n**动作:** {{action}}\n**用户:** {{sender_link_md}}" + "content": "**仓库:** {{repository_link_md}}\n**PR:** [#{{pr_number}} {{pr_title}}]({{pr_url}})\n**操作:** {{action}}\n**用户:** {{sender_link_md}}" } }, { @@ -9688,7 +9688,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**审查者:** {{review_user_link_md | default(review.user.login)}}\n**PR:** #{{pull_request.number}} {{pull_request.title}}\n**结论:** {{review.state}}\n{{review.body}}\n" + "content": "**审查者:** {{review_user_link_md | default(review.user.login)}}\n**PR:** #{{pull_request.number}} {{pull_request.title}}\n**结果:** {{review.state}}\n{{review.body}}\n" } }, { @@ -10774,7 +10774,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**动作:** {{action}}\n**仓库:** {{repository.full_name}}\n" + "content": "**操作:** {{action}}\n**仓库:** {{repository.full_name}}\n" } }, { @@ -11761,7 +11761,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**密钥类型:** {{alert_secret_type}}\n**状态:** {{alert_state}}\n**仓库:** {{repository_link_md}}\n" + "content": "**仓库:** {{repository_link_md}}\n**密钥类型:** {{alert_secret_type}}\n**状态:** {{alert_state}}\n" } } ] @@ -12628,7 +12628,7 @@ "header": { "title": { "tag": "plain_text", - "content": "⭐ 仰慕:{{action}} by {{sender_link_md | default(sender.login)}}" + "content": "⭐ {{action}} by {{sender_name | default(sender.login)}}" }, "template": "yellow" }, @@ -12637,7 +12637,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**仓库:** {{repository.full_name}}\n**动作:** {{action}}\n" + "content": "**仓库:** {{repository.full_name}}\n**操作:** {{action}}\n" } } ] @@ -12762,7 +12762,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**仓库:** {{repository.full_name}}\n**动作:** {{action}}\n" + "content": "**仓库:** {{repository.full_name}}\n**操作:** {{action}}\n" } } ] @@ -12983,7 +12983,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**团队:** {{team.name}}\n**动作:** {{action}}\n" + "content": "**团队:** {{team.name}}\n**操作:** {{action}}\n" } } ] @@ -13287,7 +13287,7 @@ "header": { "title": { "tag": "plain_text", - "content": "🔔 Watch:{{action}} by {{sender_link_md | default(sender.login)}}" + "content": "🔔 来自 {{sender_name | default(sender.login)}} 的关注:{{action}}" }, "template": "yellow" }, @@ -13296,7 +13296,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**仓库:** {{repository.full_name}}\n**动作:** {{action}}\n" + "content": "**仓库:** {{repository.full_name}}\n**操作:** {{action}}\n" } } ] @@ -13426,7 +13426,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**任务:** {{job_name}}\n**工作流:** {{workflow_name}}\n**状态:** {{job_status}}\n**结论:** {{job_conclusion}}\n" + "content": "**任务:** {{job_name}}\n**工作流:** {{workflow_name}}\n**状态:** {{job_status}}\n**结果:** {{job_conclusion}}\n" } }, { @@ -13470,7 +13470,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**任务:** {{job_name}}\n**工作流:** {{workflow_name}}\n**状态:** {{job_status}}\n**结论:** {{job_conclusion}}\n" + "content": "**任务:** {{job_name}}\n**工作流:** {{workflow_name}}\n**状态:** {{job_status}}\n**结果:** {{job_conclusion}}\n" } }, { diff --git a/configs/templates.jsonc b/configs/templates.jsonc index 5aaf339..a92a943 100644 --- a/configs/templates.jsonc +++ b/configs/templates.jsonc @@ -122,7 +122,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Rule:** {{rule_name}}\n**Repository:** {{repository_link_md}}\n**Action:** {{action}}\n" + "content": "**Repository:** {{repository_link_md}}\n**Rule:** {{rule_name}}\n**Action:** {{action}}\n" } } ] @@ -294,7 +294,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Check:** {{check_run.name}}\n**Repo:** {{repository_link_md | default('')}}\n**Status:** {{check_run.status}}\n**Conclusion:** {{check_run.conclusion}}\n" + "content": "**Repository:** {{repository_link_md | default('')}}\n**Check:** {{check_run.name}}\n**Status:** {{check_run.status}}\n**Conclusion:** {{check_run.conclusion}}\n" } } ] @@ -324,7 +324,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Check:** {{check_run.name}}\n**Repo:** {{repository_link_md | default('')}}\n**Status:** {{check_run.status}}\n**Conclusion:** {{check_run.conclusion}}\n" + "content": "Repository:** {{repository_link_md | default('')}}\n**Check:** {{check_run.name}}\n****Status:** {{check_run.status}}\n**Conclusion:** {{check_run.conclusion}}\n" } } ] @@ -353,7 +353,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Check:** {{check_run.name}}\n**Repo:** {{repository_link_md | default('')}}\n**Status:** {{check_run.status}}\n**Conclusion:** {{check_run.conclusion}}\n" + "content": "Repository:** {{repository_link_md | default('')}}\n**Check:** {{check_run.name}}\n****Status:** {{check_run.status}}\n**Conclusion:** {{check_run.conclusion}}\n" } } ] @@ -524,7 +524,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Conclusion:** {{check_suite.conclusion | default('N/A')}}\n**Head SHA:** {{check_suite.head_sha}}\n**Repo:** {{repository_link_md | default('')}}\n" + "content": "**Repository:** {{repository_link_md | default('')}}\n**Conclusion:** {{check_suite.conclusion | default('N/A')}}\n**Head SHA:** {{check_suite.head_sha}}\n" } } ] @@ -695,7 +695,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Alert:** {{alert_rule_description}}\n**Severity:** {{alert_rule_severity}}\n**State:** {{alert_state}}\n**Repository:** {{repository_link_md}}\n" + "content": "**Repository:** {{repository_link_md}}\n**Alert:** {{alert_rule_description}}\n**Severity:** {{alert_rule_severity}}\n**State:** {{alert_state}}\n" } } ] @@ -1401,7 +1401,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Package:** {{alert_package_name}}\n**Severity:** {{alert_severity}}\n**State:** {{alert_state}}\n**Repository:** {{repository_link_md}}\n" + "content": "**Repository:** {{repository_link_md}}\n**Package:** {{alert_package_name}}\n**Severity:** {{alert_severity}}\n**State:** {{alert_state}}\n" } } ] @@ -2975,7 +2975,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Forker:** {{sender_link_md | default(sender.login)}}\n**Forked repo:** {{forkee.full_name}}\n" + "content": "**Forked repo to:** {{forkee.full_name}}\n**Forker:** {{sender_link_md | default(sender.login)}}\n" } }, { @@ -3902,7 +3902,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🐛 **Bug reported** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "🐛 **Bug reported** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -3946,7 +3946,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✨ **Feature request** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "✨ **Feature request** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -3990,7 +3990,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📋 **Task created** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "📋 **Task created** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4034,7 +4034,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **Bug fixed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **Bug fixed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4078,7 +4078,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **Feature completed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **Feature completed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4122,7 +4122,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **Task completed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **Task completed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4166,7 +4166,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **Bug reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **Bug reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4210,7 +4210,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **Feature reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **Feature reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4254,7 +4254,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **Task reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **Task reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4297,7 +4297,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📝 **Issue opened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "📝 **Issue opened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.body}}**Description:** {{issue.body | truncate(200)}}{{/if}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4340,7 +4340,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✅ **Issue closed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "✅ **Issue closed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**State:** {{issue.state | default('closed')}} ({{issue_state_reason | default('completed')}})\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4383,7 +4383,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔄 **Issue reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" + "content": "🔄 **Issue reopened** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_labels_joined}}**Labels:** {{issue_labels_joined}}{{/if}}" } }, { @@ -4426,7 +4426,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "👤 **Issue assigned** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**Assignee:** {{assignee_link_md}}{{/if}}" + "content": "👤 **Issue assigned** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**Assignee:** {{assignee_link_md}}{{/if}}" } }, { @@ -4469,7 +4469,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "👤 **Issue unassigned** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**Removed assignee:** {{assignee_link_md}}{{/if}}" + "content": "👤 **Issue unassigned** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if assignee_link_md}}**Removed assignee:** {{assignee_link_md}}{{/if}}" } }, { @@ -4512,7 +4512,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✏️ **Issue edited** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_title_from}}**Title changed from:** {{changes_title_from}}{{/if}}" + "content": "✏️ **Issue edited** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_title_from}}**Title changed from:** {{changes_title_from}}{{/if}}" } }, { @@ -4555,7 +4555,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🎯 **Milestone added** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**Milestone:** {{milestone_title}}{{/if}}" + "content": "🎯 **Milestone added** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**Milestone:** {{milestone_title}}{{/if}}" } }, { @@ -4598,7 +4598,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🎯 **Milestone removed** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**Removed milestone:** {{milestone_title}}{{/if}}" + "content": "🎯 **Milestone removed** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if milestone_title}}**Removed milestone:** {{milestone_title}}{{/if}}" } }, { @@ -4641,7 +4641,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📦 **Issue transferred** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_new_repository_full_name}}**To repository:** {{changes_new_repository_full_name}}{{/if}}" + "content": "📦 **Issue transferred** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if changes_new_repository_full_name}}**To repository:** {{changes_new_repository_full_name}}{{/if}}" } }, { @@ -4684,7 +4684,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🗑️ **Issue deleted** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "🗑️ **Issue deleted** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } } ] @@ -4713,7 +4713,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📌 **Issue pinned** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "📌 **Issue pinned** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -4756,7 +4756,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📌 **Issue unpinned** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "📌 **Issue unpinned** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -4799,7 +4799,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔒 **Issue locked** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.active_lock_reason}}**Reason:** {{issue.active_lock_reason}}{{/if}}" + "content": "🔒 **Issue locked** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue.active_lock_reason}}**Reason:** {{issue.active_lock_reason}}{{/if}}" } }, { @@ -4842,7 +4842,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🔓 **Issue unlocked** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "🔓 **Issue unlocked** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -4885,7 +4885,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Label:** {{labeled_label_link_md | default(labeled_label_name)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**By:** {{sender_link_md | default(sender.login)}}\n" + "content": "**Label:** {{labeled_label_link_md | default(labeled_label_name)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**By:** {{sender_link_md | default(sender.login)}}\n" } }, { @@ -4928,7 +4928,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Label:** {{labeled_label_link_md | default(labeled_label_name)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**By:** {{sender_link_md | default(sender.login)}}\n" + "content": "**Label:** {{labeled_label_link_md | default(labeled_label_name)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**By:** {{sender_link_md | default(sender.login)}}\n" } }, { @@ -4971,7 +4971,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✏️ **Issue typed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_type}}**Type:** {{issue_type}}{{/if}}" + "content": "✏️ **Issue typed** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n{{#if issue_type}}**Type:** {{issue_type}}{{/if}}" } }, { @@ -5014,7 +5014,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✏️ **Issue type removed** by {{sender_link_md | default(sender.login)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" + "content": "✏️ **Issue type removed** by {{sender_link_md | default(sender.login)}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}" } }, { @@ -5057,7 +5057,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "🐛 **Bug reported** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "🐛 **Bug reported** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5100,7 +5100,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "✨ **Feature request** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "✨ **Feature request** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5143,7 +5143,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "📝 **Task created** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "📝 **Task created** by {{sender_link_md | default(sender.login)}} {{issue_user_link_md | default('')}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5186,7 +5186,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "⚠️ **Issue** by {{sender_link_md | default(sender.login)}} {{issue_type_display | default(issue_user_link_md | default(''))}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" + "content": "⚠️ **Issue** by {{sender_link_md | default(sender.login)}} {{issue_type_display | default(issue_user_link_md | default(''))}}\n**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n" } }, { @@ -5229,7 +5229,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Label:** {{labeled_label_link_md | default(labeled_label_name)}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**By:** {{sender_link_md | default(sender.login)}}\n" + "content": "**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**Label:** {{labeled_label_link_md | default(labeled_label_name)}}\n**By:** {{sender_link_md | default(sender.login)}}\n" } }, { @@ -5272,7 +5272,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**Author:** {{sender_link_md | default(sender.login)}}\n**State:** {{issue.state | default('unknown')}}\n" + "content": "**Repository:** {{repository_link_md}}\n**Issue:** {{issue_link_md | default(issue.html_url | default(''))}}\n**Author:** {{sender_link_md | default(sender.login)}}\n**State:** {{issue.state | default('unknown')}}\n" } }, { @@ -5319,7 +5319,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Label:** {{label_name}}\n**Action:** {{action}}\n**Repository:** {{repository_link_md}}\n" + "content": "**Repository:** {{repository_link_md}}\n**Label:** {{label_name}}\n**Action:** {{action}}\n" } } ] @@ -6850,7 +6850,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Repo:** {{repository.full_name}}\n**Action:** {{action}}\n" + "content": "**Repository:** {{repository.full_name}}\n**Action:** {{action}}\n" } } ] @@ -7062,7 +7062,7 @@ "header": { "title": { "tag": "plain_text", - "content": "🏓 Webhook Ping" + "content": "✅ GitHub Webhook Successfully Added" }, "template": "green" }, @@ -7071,7 +7071,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Zen:** {{zen}}\n**Hook ID:** {{hook_id}}\n" + "content": "**GitHub says:** {{zen}}\n\n{{#if repository}}**Repository:** [{{repository.full_name}}]({{repository.html_url}})\n{{/if}}{{#if organization}}**Organization:** [{{organization.login}}](https:\/\/github.com/{{organization.login}})\n{{/if}}**Hook ID:** {{hook_id}}\n**Hook Type:** {{hook_type}}" // fuck jsonc } } ] @@ -10817,7 +10817,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Action:** {{action}}\n**Repo:** {{repository.full_name}}\n" + "content": "**Repository:** {{repository.full_name}}\n**Action:** {{action}}\n" } }, { @@ -11804,7 +11804,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Secret Type:** {{alert_secret_type}}\n**State:** {{alert_state}}\n**Repository:** {{repository_link_md}}\n" + "content": "**Repository:** {{repository_link_md}}\n**Secret Type:** {{alert_secret_type}}\n**State:** {{alert_state}}\n" } } ] @@ -12671,7 +12671,7 @@ "header": { "title": { "tag": "plain_text", - "content": "⭐ {{action}} by {{sender_link_md | default(sender.login)}}" + "content": "⭐ {{action}} by {{sender_name | default(sender.login)}}" }, "template": "yellow" }, @@ -12680,7 +12680,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Repo:** {{repository.full_name}}\n**Action:** {{action}}\n" + "content": "**Repository:** {{repository.full_name}}\n**Action:** {{action}}\n" } } ] @@ -13330,7 +13330,7 @@ "header": { "title": { "tag": "plain_text", - "content": "🔔 Watch: {{action}} by {{sender_link_md | default(sender.login)}}" + "content": "🔔 Watch: {{action}} by {{sender_name | default(sender.login)}}" }, "template": "yellow" }, @@ -13339,7 +13339,7 @@ "tag": "div", "text": { "tag": "lark_md", - "content": "**Repo:** {{repository.full_name}}\n**Action:** {{action}}\n" + "content": "**Repository:** {{repository.full_name}}\n**Action:** {{action}}\n" } } ] diff --git a/internal/config/config.go b/internal/config/config.go index ac74563..da88d9b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,7 +17,7 @@ type Config struct { Repos ReposConfig Events EventsConfig FeishuBots FeishuBotsConfig - Templates TemplatesConfig + Templates map[string]TemplatesConfig // Key: template name (e.g., "default", "cn") } // ServerConfig represents server.yaml @@ -56,8 +56,9 @@ type FeishuBotsConfig struct { } type FeishuBot struct { - Alias string `yaml:"alias"` - URL string `yaml:"url"` + Alias string `yaml:"alias"` + URL string `yaml:"url"` + Template string `yaml:"template"` // Optional: template name (e.g., "cn"), defaults to "default" } // TemplatesConfig represents templates.jsonc (JSONC) @@ -76,7 +77,9 @@ type PayloadTemplate struct { // Load loads all configuration files from the given directory func Load(configDir string) (*Config, error) { - cfg := &Config{} + cfg := &Config{ + Templates: make(map[string]TemplatesConfig), + } // Load server.yaml if err := loadConfigFile(filepath.Join(configDir, "server.yaml"), &cfg.Server); err != nil { @@ -98,15 +101,70 @@ func Load(configDir string) (*Config, error) { return nil, fmt.Errorf("failed to load feishu-bots.yaml: %w", err) } - // Load templates.jsonc (JSONC is required) - templatesJSONC := filepath.Join(configDir, "templates.jsonc") - if err := loadConfigFile(templatesJSONC, &cfg.Templates); err != nil { + // Load templates.jsonc as default template (required) + defaultTemplatesPath := filepath.Join(configDir, "templates.jsonc") + var defaultTemplates TemplatesConfig + if err := loadConfigFile(defaultTemplatesPath, &defaultTemplates); err != nil { return nil, fmt.Errorf("failed to load templates.jsonc: %w", err) } + cfg.Templates["default"] = defaultTemplates + + // Load additional template files (templates.*.jsonc) + // Scan for templates.cn.jsonc, templates.en.jsonc, etc. + entries, err := os.ReadDir(configDir) + if err != nil { + return nil, fmt.Errorf("failed to read config directory: %w", err) + } + + templatePattern := regexp.MustCompile(`^templates\.([a-zA-Z0-9_-]+)\.jsonc$`) + for _, entry := range entries { + if entry.IsDir() { + continue + } + + matches := templatePattern.FindStringSubmatch(entry.Name()) + if len(matches) > 1 { + templateName := matches[1] + var tmpl TemplatesConfig + templatePath := filepath.Join(configDir, entry.Name()) + if err := loadConfigFile(templatePath, &tmpl); err != nil { + return nil, fmt.Errorf("failed to load %s: %w", entry.Name(), err) + } + cfg.Templates[templateName] = tmpl + } + } return cfg, nil } +// GetBotTemplate returns the template name for a given bot alias +// Returns "default" if the bot doesn't specify a template or if the bot is not found +func (c *Config) GetBotTemplate(botAlias string) string { + for _, bot := range c.FeishuBots.FeishuBots { + if bot.Alias == botAlias { + if bot.Template != "" { + return bot.Template + } + return "default" + } + } + return "default" +} + +// GetTemplateConfig returns the template configuration for a given template name +// Returns the default template if the specified template is not found +func (c *Config) GetTemplateConfig(templateName string) TemplatesConfig { + if tmpl, exists := c.Templates[templateName]; exists { + return tmpl + } + // Fallback to default + if tmpl, exists := c.Templates["default"]; exists { + return tmpl + } + // Return empty config if even default is missing (shouldn't happen) + return TemplatesConfig{} +} + // loadConfigFile loads either YAML or JSONC (JSON with comments) based on file extension func loadConfigFile(path string, out any) error { data, err := os.ReadFile(path) @@ -117,13 +175,40 @@ func loadConfigFile(path string, out any) error { if strings.HasSuffix(path, ".jsonc") || strings.HasSuffix(path, ".json") { // strip comments and unmarshal as JSON cleaned := stripJSONCComments(string(data)) - return json.Unmarshal([]byte(cleaned), out) + if err := json.Unmarshal([]byte(cleaned), out); err != nil { + // If it's a syntax error, try to compute line/column + if serr, ok := err.(*json.SyntaxError); ok { + line, col := offsetToLineCol([]byte(cleaned), serr.Offset) + return fmt.Errorf("%w at line %d column %d (offset %d)", err, line, col, serr.Offset) + } + return err + } + return nil } // default to YAML return yaml.Unmarshal(data, out) } +// offsetToLineCol converts a 1-based byte offset into line and column numbers (1-based) +func offsetToLineCol(b []byte, offset int64) (int, int) { + if offset <= 0 { + return 1, 1 + } + var line = 1 + var col = 1 + var i int64 + for i = 0; i < offset-1 && i < int64(len(b)); i++ { + if b[i] == '\n' { + line++ + col = 1 + } else { + col++ + } + } + return line, col +} + // stripJSONCComments removes // and /* */ style comments from JSONC input. func stripJSONCComments(s string) string { // Remove /* ... */ block comments diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0289c5e..2b5e310 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -46,9 +46,12 @@ events: feishu_bots: - alias: "test-bot" url: "https://example.com/webhook" + - alias: "test-bot-cn" + url: "https://example.com/webhook-cn" + template: "cn" ` - templatesYAML := ` + templatesConfig := ` { // templates.jsonc "templates": { @@ -62,15 +65,32 @@ feishu_bots: } } } +` + + templatesCnConfig := ` +{ + // templates.cn.jsonc + "templates": { + "push": { + "payloads": [ + { + "tags": ["default"], + "payload": { "msg_type": "text", "content": { "text": "测试" } } + } + ] + } + } +} ` // Write test config files files := map[string]string{ - "server.yaml": serverYAML, - "repos.yaml": reposYAML, - "events.yaml": eventsYAML, - "feishu-bots.yaml": botsYAML, - "templates.jsonc": templatesYAML, + "server.yaml": serverYAML, + "repos.yaml": reposYAML, + "events.yaml": eventsYAML, + "feishu-bots.yaml": botsYAML, + "templates.jsonc": templatesConfig, + "templates.cn.jsonc": templatesCnConfig, } for name, content := range files { @@ -99,7 +119,238 @@ feishu_bots: t.Errorf("Expected 1 repo, got %d", len(cfg.Repos.Repos)) } - if len(cfg.FeishuBots.FeishuBots) != 1 { - t.Errorf("Expected 1 bot, got %d", len(cfg.FeishuBots.FeishuBots)) + if len(cfg.FeishuBots.FeishuBots) != 2 { + t.Errorf("Expected 2 bots, got %d", len(cfg.FeishuBots.FeishuBots)) + } + + // Test template loading + if len(cfg.Templates) != 2 { + t.Errorf("Expected 2 templates (default + cn), got %d", len(cfg.Templates)) + } + + if _, ok := cfg.Templates["default"]; !ok { + t.Error("Expected default template to be loaded") + } + + if _, ok := cfg.Templates["cn"]; !ok { + t.Error("Expected cn template to be loaded") + } + + // Test GetBotTemplate + if tmpl := cfg.GetBotTemplate("test-bot"); tmpl != "default" { + t.Errorf("Expected default template for test-bot, got %s", tmpl) + } + + if tmpl := cfg.GetBotTemplate("test-bot-cn"); tmpl != "cn" { + t.Errorf("Expected cn template for test-bot-cn, got %s", tmpl) + } + + if tmpl := cfg.GetBotTemplate("non-existent"); tmpl != "default" { + t.Errorf("Expected default template for non-existent bot, got %s", tmpl) + } + + // Test GetTemplateConfig + defaultTmpl := cfg.GetTemplateConfig("default") + if _, ok := defaultTmpl.Templates["push"]; !ok { + t.Error("Expected push template in default config") } + + cnTmpl := cfg.GetTemplateConfig("cn") + if _, ok := cnTmpl.Templates["push"]; !ok { + t.Error("Expected push template in cn config") + } + + // Test fallback for non-existent template + fallbackTmpl := cfg.GetTemplateConfig("non-existent") + if _, ok := fallbackTmpl.Templates["push"]; !ok { + t.Error("Expected fallback to default template") + } +} + +// TestLoadRealTemplates tests loading the actual templates.jsonc and templates.cn.jsonc files +// This test is skipped by default as the real template files are very large and may have formatting issues + +func TestLoadRealTemplates(t *testing.T) { + // Use real templates from the project's configs directory. + // This test requires the files to exist in the repository root. + projectRoot := filepath.Join("..", "..", "configs") + + // Ensure templates.jsonc exists + templatesPath := filepath.Join(projectRoot, "templates.jsonc") + if _, err := os.Stat(templatesPath); os.IsNotExist(err) { + t.Fatalf("Required file missing: %s", templatesPath) + } + + // Ensure templates.cn.jsonc exists + templatesCnPath := filepath.Join(projectRoot, "templates.cn.jsonc") + if _, err := os.Stat(templatesCnPath); os.IsNotExist(err) { + t.Fatalf("Required file missing: %s", templatesCnPath) + } + + // Test loading templates.jsonc + t.Run("LoadDefaultTemplates", func(t *testing.T) { + var templates TemplatesConfig + err := loadConfigFile(templatesPath, &templates) + if err != nil { + t.Fatalf("Failed to load templates.jsonc: %v", err) + } + + // Check if ping template exists + if _, ok := templates.Templates["ping"]; !ok { + t.Error("Expected ping template in templates.jsonc") + } + + // Check if other common templates exist + commonTemplates := []string{"push", "pull_request", "issues", "issue_comment"} + for _, tmpl := range commonTemplates { + if _, ok := templates.Templates[tmpl]; !ok { + t.Errorf("Expected %s template in templates.jsonc", tmpl) + } + } + + t.Logf("Successfully loaded %d templates from templates.jsonc", len(templates.Templates)) + }) + + // Test loading templates.cn.jsonc if it exists + t.Run("LoadChineseTemplates", func(t *testing.T) { + templatesCnPath := filepath.Join(projectRoot, "templates.cn.jsonc") + if _, err := os.Stat(templatesCnPath); os.IsNotExist(err) { + t.Skip("Skipping test: templates.cn.jsonc not found") + } + + var templates TemplatesConfig + err := loadConfigFile(templatesCnPath, &templates) + if err != nil { + t.Fatalf("Failed to load templates.cn.jsonc: %v", err) + } + + // Check if ping template exists + if _, ok := templates.Templates["ping"]; !ok { + t.Error("Expected ping template in templates.cn.jsonc") + } + + // Check if other common templates exist + commonTemplates := []string{"push", "pull_request", "issues", "issue_comment"} + for _, tmpl := range commonTemplates { + if _, ok := templates.Templates[tmpl]; !ok { + t.Errorf("Expected %s template in templates.cn.jsonc", tmpl) + } + } + + t.Logf("Successfully loaded %d templates from templates.cn.jsonc", len(templates.Templates)) + }) + + // Test ping template structure + t.Run("ValidatePingTemplate", func(t *testing.T) { + var templates TemplatesConfig + err := loadConfigFile(templatesPath, &templates) + if err != nil { + t.Fatalf("Failed to load templates.jsonc: %v", err) + } + + pingTemplate, ok := templates.Templates["ping"] + if !ok { + t.Fatal("ping template not found") + } + + if len(pingTemplate.Payloads) == 0 { + t.Fatal("ping template has no payloads") + } + + // Validate first payload + firstPayload := pingTemplate.Payloads[0] + + if len(firstPayload.Tags) == 0 { + t.Error("ping template payload has no tags") + } + + if firstPayload.Payload == nil { + t.Fatal("ping template payload is nil") + } + + // Check for required fields in Feishu card + if msgType, ok := firstPayload.Payload["msg_type"]; !ok || msgType != "interactive" { + t.Error("ping template should have msg_type: interactive") + } + + t.Logf("Ping template has %d payload(s)", len(pingTemplate.Payloads)) + }) + + // Test loading complete config with real templates + t.Run("LoadCompleteConfigWithRealTemplates", func(t *testing.T) { + // Create temp dir with minimal config files + tmpDir := t.TempDir() + + serverYAML := ` +server: + host: "127.0.0.1" + port: 4594 + secret: "test_secret" +` + + reposYAML := ` +repos: + - pattern: "test/repo" + events: + push: + notify_to: + - test-bot +` + + eventsYAML := ` +events: + push: +` + + botsYAML := ` +feishu_bots: + - alias: "test-bot" + url: "https://example.com/webhook" +` + + // Write minimal configs + files := map[string]string{ + "server.yaml": serverYAML, + "repos.yaml": reposYAML, + "events.yaml": eventsYAML, + "feishu-bots.yaml": botsYAML, + } + + for name, content := range files { + path := filepath.Join(tmpDir, name) + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("Failed to write %s: %v", name, err) + } + } + + // Copy real templates + realTemplatesPath := filepath.Join(projectRoot, "templates.jsonc") + templatesData, err := os.ReadFile(realTemplatesPath) + if err != nil { + t.Fatalf("Failed to read real templates.jsonc: %v", err) + } + + tmpTemplatesPath := filepath.Join(tmpDir, "templates.jsonc") + if err := os.WriteFile(tmpTemplatesPath, templatesData, 0644); err != nil { + t.Fatalf("Failed to write templates.jsonc to temp dir: %v", err) + } + + // Load config + cfg, err := Load(tmpDir) + if err != nil { + t.Fatalf("Failed to load config with real templates: %v", err) + } + + // Validate + if _, ok := cfg.Templates["default"]; !ok { + t.Error("Expected default template to be loaded") + } + + defaultTmpl := cfg.GetTemplateConfig("default") + if _, ok := defaultTmpl.Templates["ping"]; !ok { + t.Error("Expected ping template in default config") + } + + t.Log("Successfully loaded complete config with real templates") + }) } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index bfe2868..739487c 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -53,10 +53,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { logger.Error("Failed to reload configuration: %v", err) // Continue with old config instead of failing } else { + // compare with previous config and log info if different + changed := false + if h.config != nil { + oldB, _ := json.Marshal(h.config) + newB, _ := json.Marshal(cfg) + if string(oldB) != string(newB) { + logger.Info("Configuration changes detected, applying new configuration") + changed = true + } + } else { + logger.Info("Configuration loaded") + changed = true + } + h.config = cfg // Update notifier with new config h.notifier = notifier.New(cfg.FeishuBots) - logger.Debug("Configuration reloaded successfully") + + if !changed { + logger.Debug("Configuration reloaded successfully (no changes detected)") + } } } @@ -126,7 +143,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - logger.Info("Received %s event", eventType) + logger.Debug("Received %s event", eventType) logger.Debug("Payload: %v", payload) // Process the webhook @@ -154,71 +171,149 @@ func (h *Handler) verifySignature(signature string, body []byte) bool { } func (h *Handler) processWebhook(eventType string, payload map[string]any) error { - // Extract repository full name + // Extract repository full name (may be empty for org-level webhooks or certain events) repoFullName := h.extractRepoFullName(payload) - if repoFullName == "" { - return fmt.Errorf("failed to extract repository name from payload") - } - logger.Info("Processing event for repository: %s", repoFullName) + // Extract organization name (for org-level webhooks) + orgName := h.extractOrgName(payload) - // Match repository pattern - repoPattern, err := matcher.MatchRepo(repoFullName, h.config.Repos.Repos) - if err != nil { - return fmt.Errorf("failed to match repository: %w", err) - } - if repoPattern == nil { - logger.Info("No matching repository pattern found for %s, skipping", repoFullName) - return nil - } + // Determine target bots based on repository or organization + var repoPattern *config.RepoPattern + var err error + var targetBots []string + + if repoFullName != "" { + // Repository-level webhook + logger.Debug("Processing %s event for repository: %s", eventType, repoFullName) + + repoPattern, err = matcher.MatchRepo(repoFullName, h.config.Repos.Repos) + if err != nil { + return fmt.Errorf("failed to match repository: %w", err) + } + if repoPattern == nil { + logger.Debug("No matching repository pattern found for %s, skipping", repoFullName) + return nil + } - logger.Info("Matched repository pattern: %s", repoPattern.Pattern) + logger.Debug("Matched repository pattern: %s", repoPattern.Pattern) + targetBots = repoPattern.NotifyTo - // Expand events (resolve templates) - expandedEvents := matcher.ExpandEvents( - repoPattern.Events, - h.config.Events.EventSets, - h.config.Events.Events, - ) + } else if orgName != "" { + // Organization-level webhook + logger.Debug("Processing %s event for organization: %s", eventType, orgName) + + // Find all repo patterns matching this organization (exact match for org/*) + for _, repo := range h.config.Repos.Repos { + if repo.Pattern == orgName+"/*" { + targetBots = append(targetBots, repo.NotifyTo...) + } + } + + if len(targetBots) == 0 { + logger.Debug("No matching repository patterns found for organization %s (expected pattern: %s/*), skipping", orgName, orgName) + return nil + } - // Extract event details - action := h.extractAction(payload) - ref := h.extractRef(payload) + // Remove duplicates + targetBots = uniqueStrings(targetBots) - // Match event - if !matcher.MatchEvent(eventType, action, ref, payload, expandedEvents) { - logger.Info("Event %s (action: %s, ref: %s) does not match configured events, skipping", eventType, action, ref) + } else { + // No repository or organization info + logger.Warn("Event %s does not contain repository or organization information, skipping", eventType) return nil } - logger.Info("Event matched, preparing notification") + // For ping events, skip filter and send to all matched bots + isPingEvent := (eventType == "ping") - // Determine tags for template selection - tags := template.DetermineTags(eventType, payload) + if !isPingEvent { + // For non-ping events, check event filter + if repoPattern == nil { + logger.Warn("Event %s requires event filtering but no repo pattern matched", eventType) + return nil + } - // Select template - tmpl, err := template.SelectTemplate(eventType, tags, h.config.Templates) - if err != nil { - return fmt.Errorf("failed to select template: %w", err) + // Expand events (resolve templates) + expandedEvents := matcher.ExpandEvents( + repoPattern.Events, + h.config.Events.EventSets, + h.config.Events.Events, + ) + + // Extract event details + action := h.extractAction(payload) + ref := h.extractRef(payload) + + // Match event + if !matcher.MatchEvent(eventType, action, ref, payload, expandedEvents) { + logger.Debug("Event %s (action: %s, ref: %s) does not match configured events, skipping", eventType, action, ref) + return nil + } + } else { + logger.Debug("Ping event - bypassing filter, will notify all matched bots") } - // Prepare data for template filling + logger.Info("Event matched: %s, sending notification", eventType) + + // Determine tags for template selection + tags := template.DetermineTags(eventType, payload) + + // Prepare data for template filling (common for all templates) data := h.prepareTemplateData(eventType, payload) - // Fill template - filledPayload, err := template.FillTemplate(tmpl, data) - if err != nil { - return fmt.Errorf("failed to fill template: %w", err) + // Group targets by template + targetsByTemplate := h.groupTargetsByTemplate(targetBots) + + // Process each template group + var errs []string + for templateName, targets := range targetsByTemplate { + logger.Debug("Processing %d target(s) with template: %s", len(targets), templateName) + + // Get the appropriate template configuration + templatesConfig := h.config.GetTemplateConfig(templateName) + + // Select template + tmpl, err := template.SelectTemplate(eventType, tags, templatesConfig) + if err != nil { + logger.Error("Failed to select template for %s: %v", templateName, err) + errs = append(errs, fmt.Sprintf("template %s: %v", templateName, err)) + continue + } + + // Fill template + filledPayload, err := template.FillTemplate(tmpl, data) + if err != nil { + logger.Error("Failed to fill template for %s: %v", templateName, err) + errs = append(errs, fmt.Sprintf("template %s: %v", templateName, err)) + continue + } + + // Send notifications to this group + if err := h.notifier.Send(targets, filledPayload); err != nil { + logger.Error("Failed to send notifications for template %s: %v", templateName, err) + errs = append(errs, fmt.Sprintf("template %s: %v", templateName, err)) + } } - // Send notifications - if err := h.notifier.Send(repoPattern.NotifyTo, filledPayload); err != nil { - return fmt.Errorf("failed to send notifications: %w", err) + if len(errs) > 0 { + return fmt.Errorf("failed to process some templates: %s", strings.Join(errs, "; ")) } return nil } +// groupTargetsByTemplate groups notification targets by their template preference +func (h *Handler) groupTargetsByTemplate(targets []string) map[string][]string { + result := make(map[string][]string) + + for _, target := range targets { + templateName := h.config.GetBotTemplate(target) + result[templateName] = append(result[templateName], target) + } + + return result +} + func (h *Handler) extractRepoFullName(payload map[string]any) string { if repo, ok := payload["repository"].(map[string]any); ok { if fullName, ok := repo["full_name"].(string); ok { @@ -228,6 +323,15 @@ func (h *Handler) extractRepoFullName(payload map[string]any) string { return "" } +func (h *Handler) extractOrgName(payload map[string]any) string { + if org, ok := payload["organization"].(map[string]any); ok { + if login, ok := org["login"].(string); ok { + return login + } + } + return "" +} + func (h *Handler) extractAction(payload map[string]any) string { if action, ok := payload["action"].(string); ok { return action diff --git a/internal/handler/handler_ping.go b/internal/handler/handler_ping.go index 6128ef5..5696f2d 100644 --- a/internal/handler/handler_ping.go +++ b/internal/handler/handler_ping.go @@ -26,3 +26,16 @@ func preparePingData(data map[string]any, payload map[string]any) { data["ping"] = payload } + +// uniqueStrings returns a slice with duplicate strings removed +func uniqueStrings(input []string) []string { + seen := make(map[string]bool) + result := []string{} + for _, str := range input { + if !seen[str] { + seen[str] = true + result = append(result, str) + } + } + return result +} diff --git a/internal/handler/handler_repository.go b/internal/handler/handler_repository.go index 2439293..298b5ac 100644 --- a/internal/handler/handler_repository.go +++ b/internal/handler/handler_repository.go @@ -11,4 +11,11 @@ func prepareRepositoryData(data map[string]any, payload map[string]any) { data["repo_url"] = url } } + + // Ensure templates can access the event action for repository events + // Many other prepare* functions set data["action"] = payload["action"]. + // repository events did not — that caused {{action}} to be empty in templates. + if a, ok := payload["action"]; ok { + data["action"] = a + } } diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index 0053c74..794e62a 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -60,16 +60,18 @@ func TestServeHTTP_FormEncodedPayload(t *testing.T) { "push": map[string]any{"ref": "*"}, }, }, - Templates: config.TemplatesConfig{ - Templates: map[string]config.EventTemplate{ - "push": { - Payloads: []config.PayloadTemplate{ - { - Tags: []string{"push", "default"}, - Payload: map[string]any{ - "msg_type": "text", - "content": map[string]any{ - "text": "Test push: {{repository.full_name}}", + Templates: map[string]config.TemplatesConfig{ + "default": { + Templates: map[string]config.EventTemplate{ + "push": { + Payloads: []config.PayloadTemplate{ + { + Tags: []string{"push", "default"}, + Payload: map[string]any{ + "msg_type": "text", + "content": map[string]any{ + "text": "Test push: {{repository.full_name}}", + }, }, }, }, diff --git a/scripts/analyze_coverage.py b/scripts/analyze_coverage.py deleted file mode 100755 index 2ee83a3..0000000 --- a/scripts/analyze_coverage.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env python3 -""" -分析模板覆盖率 - 检查哪些事件类型在模板中缺失 -""" - -import json -import re -from collections import defaultdict - -import yaml - - -def remove_comments(content): - """移除 JSONC 注释""" - # Remove single-line comments - content = re.sub(r'//.*', '', content) - # Remove multi-line comments - content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) - return content - - -def load_jsonc(filepath): - """加载 JSONC 文件""" - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - content = remove_comments(content) - return json.loads(content) - - -def extract_template_tags(data): - """从模板数据中提取所有 event:tag 组合""" - if 'templates' in data: - data = data['templates'] - tags = set() - for event, event_data in data.items(): - if 'payloads' in event_data: - for payload in event_data['payloads']: - if 'tags' in payload: - for tag in payload['tags']: - # 将 'default' 标签转换为空字符串,以匹配 events.yaml - normalized_tag = '' if tag == 'default' else tag - tags.add((event, normalized_tag)) - return tags - - -def extract_event_types(yaml_path): - """从 events.yaml 提取所有事件和类型""" - with open(yaml_path, 'r', encoding='utf-8') as f: - data = yaml.safe_load(f) - - event_types = defaultdict(list) - for event, config in data.get('events', {}).items(): - if config is None: - # 无配置的事件,使用空字符串表示默认 - event_types[event] = [''] - else: - types = config.get('types', []) - if types: - event_types[event] = types - else: - # 无 types 的事件,使用空字符串表示默认 - event_types[event] = [''] - - return event_types - - -def main(): - print("=" * 80) - print("模板覆盖率分析") - print("=" * 80) - - # Load data - print("\n📂 加载配置文件...") - event_types = extract_event_types('configs/events.yaml') - en_templates = load_jsonc('configs/templates.jsonc') - cn_templates = load_jsonc('configs/templates.cn.jsonc') - - # Extract tags - en_tags = extract_template_tags(en_templates) - cn_tags = extract_template_tags(cn_templates) - - print(f"✅ Events.yaml: {len(event_types)} events") - print(f"✅ English templates: {len(en_tags)} event:tag combinations") - print(f"✅ Chinese templates: {len(cn_tags)} event:tag combinations") - - # Calculate expected combinations - expected_combinations = set() - for event, types in event_types.items(): - for type_ in types: - expected_combinations.add((event, type_)) - - print(f"✅ Expected combinations: {len(expected_combinations)}") - - # Find missing - print("\n" + "=" * 80) - print("🔍 检查缺失的模板") - print("=" * 80) - - missing_en = expected_combinations - en_tags - missing_cn = expected_combinations - cn_tags - - print(f"\n❌ English templates missing: {len(missing_en)}") - if missing_en: - # Group by event - missing_by_event = defaultdict(list) - for event, tag in sorted(missing_en): - missing_by_event[event].append(tag) - - for event in sorted(missing_by_event.keys()): - tags = missing_by_event[event] - print( - f" - {event}: {', '.join(sorted(tags)) if tags[0] else '(default)'}") - - print(f"\n❌ Chinese templates missing: {len(missing_cn)}") - if missing_cn: - # Group by event - missing_by_event = defaultdict(list) - for event, tag in sorted(missing_cn): - missing_by_event[event].append(tag) - - for event in sorted(missing_by_event.keys()): - tags = missing_by_event[event] - print( - f" - {event}: {', '.join(sorted(tags)) if tags[0] else '(default)'}") - - # Check for templates not in events.yaml - print("\n" + "=" * 80) - print("⚠️ 检查多余的模板(不在 events.yaml 中)") - print("=" * 80) - - extra_en = en_tags - expected_combinations - extra_cn = cn_tags - expected_combinations - - if extra_en: - print(f"\n⚠️ English extra templates: {len(extra_en)}") - for event, tag in sorted(extra_en): - print(f" - {event}:{tag}") - else: - print("\n✅ No extra English templates") - - if extra_cn: - print(f"\n⚠️ Chinese extra templates: {len(extra_cn)}") - for event, tag in sorted(extra_cn): - print(f" - {event}:{tag}") - else: - print("\n✅ No extra Chinese templates") - - # Summary - print("\n" + "=" * 80) - print("📊 总结") - print("=" * 80) - - en_coverage = (len(en_tags) / len(expected_combinations) - * 100) if expected_combinations else 0 - cn_coverage = (len(cn_tags) / len(expected_combinations) - * 100) if expected_combinations else 0 - - print( - f"\n英文模板覆盖率: {en_coverage:.1f}% ({len(en_tags)}/{len(expected_combinations)})") - print( - f"中文模板覆盖率: {cn_coverage:.1f}% ({len(cn_tags)}/{len(expected_combinations)})") - - if missing_en or missing_cn: - print("\n❌ 仍有缺失的模板需要补全!") - return 1 - else: - print("\n✅ 所有模板已完整覆盖!") - return 0 - - -if __name__ == '__main__': - import sys - sys.exit(main()) diff --git a/scripts/compare_templates.py b/scripts/compare_templates.py deleted file mode 100755 index 41b9398..0000000 --- a/scripts/compare_templates.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -"""对比中英文模板差异""" - -import json -import re - - -def remove_comments(content): - content = re.sub(r'//.*', '', content) - content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) - return content - - -def extract_tags(data): - if 'templates' in data: - data = data['templates'] - tags = {} - for event, event_data in data.items(): - tags[event] = set() - if 'payloads' in event_data: - for payload in event_data['payloads']: - if 'tags' in payload: - for tag in payload['tags']: - tags[event].add(tag) - return tags - - -with open('configs/templates.jsonc', 'r') as f: - en_data = json.loads(remove_comments(f.read())) - -with open('configs/templates.cn.jsonc', 'r') as f: - cn_data = json.loads(remove_comments(f.read())) - -en_tags = extract_tags(en_data) -cn_tags = extract_tags(cn_data) - -print('=' * 80) -print('中文模板缺少的内容(相对于英文模板)') -print('=' * 80) -print() - -missing_events = [] -partial_events = [] - -for event in sorted(en_tags.keys()): - if event not in cn_tags: - missing_events.append((event, sorted(en_tags[event]))) - else: - missing = en_tags[event] - cn_tags[event] - if missing: - partial_events.append((event, sorted(missing))) - -if missing_events: - print(f'❌ 完全缺失的事件 ({len(missing_events)} 个):\n') - for event, tags in missing_events: - print(f' - {event}') - print(f' Tags: {", ".join(tags)}') - print() - -if partial_events: - print(f'⚠️ 部分缺失的事件 ({len(partial_events)} 个):\n') - for event, tags in partial_events: - print(f' - {event}') - print(f' 缺少的 tags: {", ".join(tags)}') - print() - -print('=' * 80) -print(f'总计: 完全缺失 {len(missing_events)} 个事件, 部分缺失 {len(partial_events)} 个事件') -print('=' * 80) diff --git a/scripts/generate_missing_templates.py b/scripts/generate_missing_templates.py deleted file mode 100755 index 3337fce..0000000 --- a/scripts/generate_missing_templates.py +++ /dev/null @@ -1,422 +0,0 @@ -#!/usr/bin/env python3 -""" -批量生成缺失的模板 -""" - -import json -import re -import yaml -from collections import defaultdict - - -def remove_comments(content): - """移除 JSONC 注释""" - content = re.sub(r'//.*', '', content) - content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) - return content - - -def load_jsonc(filepath): - """加载 JSONC 文件""" - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - content = remove_comments(content) - return json.loads(content) - - -def extract_template_tags(data): - """从模板数据中提取所有 event:tag 组合""" - if 'templates' in data: - data = data['templates'] - tags = set() - for event, event_data in data.items(): - if 'payloads' in event_data: - for payload in event_data['payloads']: - if 'tags' in payload: - for tag in payload['tags']: - tags.add((event, tag)) - return tags - - -def extract_event_types(yaml_path): - """从 events.yaml 提取所有事件和类型""" - with open(yaml_path, 'r', encoding='utf-8') as f: - data = yaml.safe_load(f) - - event_types = defaultdict(list) - for event, config in data.get('events', {}).items(): - if config is None: - event_types[event] = [''] - else: - types = config.get('types', []) - if types: - event_types[event] = types - else: - event_types[event] = [''] - - return event_types - - -# 颜色映射 -COLOR_MAP = { - # PR/Issue 相关 - 'opened': 'green', - 'created': 'green', - 'published': 'green', - 'fixed': 'green', - 'resolved': 'green', - 'approved': 'green', - 'completed': 'green', - 'success': 'green', - - 'closed': 'red', - 'deleted': 'red', - 'removed': 'red', - 'failure': 'red', - - 'edited': 'blue', - 'updated': 'blue', - 'synchronize': 'blue', - - 'locked': 'orange', - 'dismissed': 'orange', - 'requested': 'orange', - - 'unlocked': 'green', - 'reopened': 'orange', - - # 默认 - '': 'turquoise', - 'default': 'turquoise' -} - - -def get_color(action): - """根据动作获取颜色""" - if action in COLOR_MAP: - return COLOR_MAP[action] - # 启发式匹配 - if 'success' in action or 'approved' in action or 'fixed' in action: - return 'green' - if 'fail' in action or 'error' in action or 'delete' in action: - return 'red' - if 'warning' in action or 'pending' in action: - return 'yellow' - if 'cancel' in action: - return 'orange' - if 'progress' in action or 'running' in action: - return 'blue' - return 'turquoise' - - -def generate_template(event, tag, is_chinese=False): - """生成单个模板""" - color = get_color(tag if tag else '') - - # 事件名映射 - event_names_cn = { - 'branch_protection_configuration': '分支保护配置', - 'branch_protection_rule': '分支保护规则', - 'check_run': '检查运行', - 'check_suite': '检查套件', - 'code_scanning_alert': '代码扫描警报', - 'commit_comment': '提交评论', - 'create': '创建', - 'custom_property': '自定义属性', - 'custom_property_values': '自定义属性值', - 'delete': '删除', - 'dependabot_alert': 'Dependabot 警报', - 'deploy_key': '部署密钥', - 'deployment': '部署', - 'deployment_protection_rule': '部署保护规则', - 'deployment_review': '部署审查', - 'deployment_status': '部署状态', - 'issue_comment': 'Issue 评论', - 'issue_dependencies': 'Issue 依赖', - 'label': '标签', - 'marketplace_purchase': '市场购买', - 'member': '成员', - 'membership': '成员资格', - 'merge_group': '合并组', - 'meta': '元数据', - 'milestone': '里程碑', - 'org_block': '组织封禁', - 'organization': '组织', - 'personal_access_token_request': '个人访问令牌请求', - 'project': '项目', - 'project_card': '项目卡片', - 'project_column': '项目列', - 'projects_v2': '项目 V2', - 'projects_v2_item': '项目 V2 条目', - 'projects_v2_status_update': '项目 V2 状态更新', - 'public': '公开', - 'registry_package': '注册表包', - 'repository': '仓库', - 'repository_advisory': '仓库公告', - 'repository_dispatch': '仓库调度', - 'repository_import': '仓库导入', - 'repository_ruleset': '仓库规则集', - 'repository_vulnerability_alert': '仓库漏洞警报', - 'secret_scanning_alert': '密钥扫描警报', - 'secret_scanning_alert_location': '密钥扫描警报位置', - 'secret_scanning_scan': '密钥扫描', - 'security_advisory': '安全公告', - 'security_and_analysis': '安全与分析', - 'sponsorship': '赞助', - 'star': '星标', - 'status': '状态', - 'sub_issues': '子 Issue', - 'team': '团队', - 'team_add': '团队添加', - 'watch': '关注', - 'workflow_dispatch': '工作流调度', - 'workflow_job': '工作流作业', - } - - # Action 中文映射 - action_names_cn = { - 'created': '已创建', - 'deleted': '已删除', - 'edited': '已编辑', - 'opened': '已打开', - 'closed': '已关闭', - 'reopened': '已重新打开', - 'locked': '已锁定', - 'unlocked': '已解锁', - 'completed': '已完成', - 'requested': '已请求', - 'approved': '已批准', - 'rejected': '已拒绝', - 'dismissed': '已驳回', - 'fixed': '已修复', - 'resolved': '已解决', - 'published': '已发布', - 'updated': '已更新', - 'enabled': '已启用', - 'disabled': '已禁用', - 'added': '已添加', - 'removed': '已移除', - 'transferred': '已转移', - 'renamed': '已重命名', - 'archived': '已归档', - 'unarchived': '已取消归档', - 'publicized': '已公开', - 'privatized': '已私有化', - 'pinned': '已固定', - 'unpinned': '已取消固定', - 'labeled': '已添加标签', - 'unlabeled': '已移除标签', - 'milestoned': '已添加里程碑', - 'demilestoned': '已移除里程碑', - 'assigned': '已分配', - 'unassigned': '已取消分配', - 'in_progress': '进行中', - 'queued': '已排队', - 'waiting': '等待中', - 'success': '成功', - 'failure': '失败', - 'cancelled': '已取消', - 'appeared_in_branch': '出现在分支', - 'closed_by_user': '被用户关闭', - 'reopened_by_user': '被用户重新打开', - 'auto_dismissed': '自动驳回', - 'auto_reopened': '自动重新打开', - 'reintroduced': '重新引入', - 'publicly_leaked': '公开泄露', - 'validated': '已验证', - 'checks_requested': '已请求检查', - 'destroyed': '已销毁', - 'converted': '已转换', - 'moved': '已移动', - 'reordered': '已重新排序', - 'restored': '已恢复', - 'answered': '已回答', - 'unanswered': '未回答', - 'blocked': '已封禁', - 'unblocked': '已解除封禁', - 'member_added': '成员已添加', - 'member_invited': '成员已邀请', - 'member_removed': '成员已移除', - 'added_to_repository': '已添加到仓库', - 'removed_from_repository': '已从仓库移除', - 'suspend': '已暂停', - 'unsuspend': '已恢复', - 'revoked': '已撤销', - 'new_permissions_accepted': '新权限已接受', - 'submitted': '已提交', - 'typed': '已分类', - 'untyped': '已取消分类', - 'blocked_by_added': '被阻止者已添加', - 'blocked_by_removed': '被阻止者已移除', - 'blocking_added': '阻止者已添加', - 'blocking_removed': '阻止者已移除', - 'parent_issue_added': '父 Issue 已添加', - 'parent_issue_removed': '父 Issue 已移除', - 'sub_issue_added': '子 Issue 已添加', - 'sub_issue_removed': '子 Issue 已移除', - 'requested_action': '已请求操作', - 'rerequested': '已重新请求', - 'started': '已开始', - 'withdrawn': '已撤回', - 'reported': '已报告', - 'pending_cancellation': '待取消', - 'pending_tier_change': '待变更等级', - 'tier_changed': '等级已变更', - 'changed': '已变更', - 'pending_change': '待变更', - 'pending_change_cancelled': '待变更已取消', - 'purchased': '已购买', - 'create': '创建', - 'dismiss': '驳回', - 'reopen': '重新打开', - 'resolve': '解决', - 'promote_to_enterprise': '提升至企业', - } - - event_name = event_names_cn.get( - event, event) if is_chinese else event.replace('_', ' ').title() - action_name = action_names_cn.get( - tag, tag) if is_chinese else tag.replace('_', ' ').title() - - if is_chinese: - title = f"{event_name} {action_name}" if tag else f"{event_name}" - repo_label = "仓库:" - action_label = "操作:" - user_label = "用户:" - button_text = "查看详情" - else: - title = f"{event_name} {action_name}" if tag else f"{event_name} Event" - repo_label = "Repository:" - action_label = "Action:" - user_label = "User:" - button_text = "View Details" - - template = { - "tags": [tag] if tag else ["default"], - "payload": { - "msg_type": "interactive", - "card": { - "config": { - "wide_screen_mode": True - }, - "header": { - "title": { - "tag": "plain_text", - "content": f"🔔 {title}" - }, - "template": color - }, - "elements": [ - { - "tag": "div", - "text": { - "tag": "lark_md", - "content": f"**{repo_label}** {{{{repository_link_md}}}}\n**{action_label}** {{{{action}}}}\n**{user_label}** {{{{sender_link_md}}}}" - } - }, - { - "tag": "hr" - }, - { - "tag": "action", - "actions": [ - { - "tag": "button", - "text": { - "tag": "plain_text", - "content": button_text - }, - "url": "{{repository.html_url}}", - "type": "default" - } - ] - } - ] - } - } - } - - return template - - -def main(): - print("=" * 80) - print("批量生成缺失模板") - print("=" * 80) - - # Load data - event_types = extract_event_types('configs/events.yaml') - en_templates = load_jsonc('configs/templates.jsonc') - cn_templates = load_jsonc('configs/templates.cn.jsonc') - - # Extract existing tags - en_tags = extract_template_tags(en_templates) - cn_tags = extract_template_tags(cn_templates) - - # Calculate expected combinations - expected_combinations = set() - for event, types in event_types.items(): - for type_ in types: - expected_combinations.add((event, type_)) - - # Find missing - missing_en = expected_combinations - en_tags - missing_cn = expected_combinations - cn_tags - - print(f"\n需要生成的英文模板: {len(missing_en)}") - print(f"需要生成的中文模板: {len(missing_cn)}") - - # Generate templates - if missing_en: - print("\n生成英文模板...") - # Group by event - missing_by_event = defaultdict(list) - for event, tag in sorted(missing_en): - missing_by_event[event].append(tag) - - generated_en = {} - for event in sorted(missing_by_event.keys()): - tags = missing_by_event[event] - templates = [] - for tag in sorted(tags): - templates.append(generate_template( - event, tag, is_chinese=False)) - generated_en[event] = {"payloads": templates} - - # Save to file - with open('configs/generated_missing_templates_en.json', 'w', encoding='utf-8') as f: - json.dump({"templates": generated_en}, f, - indent=2, ensure_ascii=False) - print( - f"✅ 已生成 {len(missing_en)} 个英文模板到 configs/generated_missing_templates_en.json") - - if missing_cn: - print("\n生成中文模板...") - # Group by event - missing_by_event = defaultdict(list) - for event, tag in sorted(missing_cn): - missing_by_event[event].append(tag) - - generated_cn = {} - for event in sorted(missing_by_event.keys()): - tags = missing_by_event[event] - templates = [] - for tag in sorted(tags): - templates.append(generate_template( - event, tag, is_chinese=True)) - generated_cn[event] = {"payloads": templates} - - # Save to file - with open('configs/generated_missing_templates_cn.json', 'w', encoding='utf-8') as f: - json.dump({"templates": generated_cn}, f, - indent=2, ensure_ascii=False) - print( - f"✅ 已生成 {len(missing_cn)} 个中文模板到 configs/generated_missing_templates_cn.json") - - print("\n" + "=" * 80) - print("✅ 完成!请review生成的模板,然后手动合并到主模板文件中") - print("=" * 80) - - -if __name__ == '__main__': - main() diff --git a/scripts/merge_generated_templates.py b/scripts/merge_generated_templates.py deleted file mode 100755 index 58d29f5..0000000 --- a/scripts/merge_generated_templates.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -""" -自动合并生成的模板到主模板文件 -""" - -import json -import re -from collections import OrderedDict - - -def remove_comments(content): - """移除 JSONC 注释""" - content = re.sub(r'//.*', '', content) - content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) - return content - - -def load_jsonc(filepath): - """加载 JSONC 文件""" - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - content = remove_comments(content) - return json.loads(content) - - -def save_jsonc(filepath, data, is_chinese=False): - """保存为 JSONC 格式""" - json_str = json.dumps(data, indent=2, ensure_ascii=False) - - # 添加注释头 - if is_chinese: - header = """// 中文模板配置文件 -// 此文件定义了所有 GitHub 事件的飞书消息卡片模板(中文版) - -""" - else: - header = """// Template configuration file -// This file defines all Feishu message card templates for GitHub events (English version) - -""" - - with open(filepath, 'w', encoding='utf-8') as f: - f.write(header) - f.write(json_str) - - -def merge_templates(main_file, generated_file, output_file, is_chinese=False): - """合并模板""" - print(f"\n合并 {generated_file} 到 {output_file}...") - - # Load files - main_data = load_jsonc(main_file) - generated_data = load_jsonc(generated_file) - - # Extract templates - if 'templates' in main_data: - main_templates = main_data['templates'] - else: - main_templates = main_data - - if 'templates' in generated_data: - generated_templates = generated_data['templates'] - else: - generated_templates = generated_data - - # Merge - merged = OrderedDict() - all_events = sorted(set(list(main_templates.keys()) + - list(generated_templates.keys()))) - - stats = {'main': 0, 'generated': 0, 'total': 0} - - for event in all_events: - if event in main_templates and event in generated_templates: - # Both exist, merge payloads - main_payloads = main_templates[event].get('payloads', []) - generated_payloads = generated_templates[event].get('payloads', []) - - # Get existing tags - existing_tags = set() - for payload in main_payloads: - for tag in payload.get('tags', []): - existing_tags.add(tag) - - # Add only new payloads - merged_payloads = list(main_payloads) - for payload in generated_payloads: - tags = payload.get('tags', []) - if not any(tag in existing_tags for tag in tags): - merged_payloads.append(payload) - stats['generated'] += 1 - else: - stats['main'] += 1 - - merged[event] = {'payloads': merged_payloads} - elif event in main_templates: - # Only in main - merged[event] = main_templates[event] - stats['main'] += len(main_templates[event].get('payloads', [])) - else: - # Only in generated - merged[event] = generated_templates[event] - stats['generated'] += len( - generated_templates[event].get('payloads', [])) - - stats['total'] = stats['main'] + stats['generated'] - - # Save - output_data = {'templates': merged} - save_jsonc(output_file, output_data, is_chinese) - - print(f"✅ 合并完成:") - print(f" - 保留主模板: {stats['main']} 个") - print(f" - 新增模板: {stats['generated']} 个") - print(f" - 总计: {stats['total']} 个") - - return stats - - -def main(): - print("=" * 80) - print("自动合并生成的模板") - print("=" * 80) - - # Merge English templates - en_stats = merge_templates( - 'configs/templates.jsonc', - 'configs/generated_missing_templates_en.json', - 'configs/templates.jsonc', - is_chinese=False - ) - - # Merge Chinese templates - cn_stats = merge_templates( - 'configs/templates.cn.jsonc', - 'configs/generated_missing_templates_cn.json', - 'configs/templates.cn.jsonc', - is_chinese=True - ) - - print("\n" + "=" * 80) - print("📊 合并统计") - print("=" * 80) - print(f"\n英文模板:") - print(f" - 原有: {en_stats['main']} 个") - print(f" - 新增: {en_stats['generated']} 个") - print(f" - 总计: {en_stats['total']} 个") - - print(f"\n中文模板:") - print(f" - 原有: {cn_stats['main']} 个") - print(f" - 新增: {cn_stats['generated']} 个") - print(f" - 总计: {cn_stats['total']} 个") - - print("\n" + "=" * 80) - print("✅ 所有模板已合并!正在验证编译...") - print("=" * 80) - - -if __name__ == '__main__': - main()