Skip to content

Conversation

@wuliya336
Copy link
Contributor

@wuliya336 wuliya336 commented Nov 11, 2025

  • 新增议题订阅功能,支持 push 和 issue 事件
  • 添加 issue.svg 图标资源
  • 创建议题推送页面模板和样式文件
  • 实现定时任务检测议题变更并推送
  • 数据库新增 event 和 issue 表用于存储订阅信息
  • 更新添加仓库命令,支持指定订阅事件类型
  • 优化日期格式化工具函数
  • 修复客户端代理配置逻辑错误
  • 完善议题信息渲染模板
  • 添加议题推送合集转发消息功能

Sourcery 总结

通过扩展命令、数据库 schema、模型、任务和模板,支持订阅和推送仓库问题变更以及推送事件。

新功能:

  • 实现问题订阅功能,支持定时轮询和推送通知
  • 扩展 AddRepo/RemoveRepo 命令,以接受和管理推送事件和问题订阅类型
  • 引入新的事件和问题表及模型,用于存储订阅元数据
  • 添加问题通知模板、样式和 issue.svg 图标资源

错误修复:

  • 修复客户端代理配置逻辑

改进:

  • 重构推送任务,使其根据事件订阅进行过滤
  • 添加 formatDatemake_hash 工具函数,用于日期格式化和内容哈希
  • 改进客户端代理/反向代理配置逻辑

文档:

  • 更新帮助页面,以说明订阅命令的 event 参数

杂项:

  • nipaw 依赖项版本提升至 ^1.8.0
Original summary in English

Sourcery 总结

通过扩展命令、数据库 schema 和计划任务,支持订阅仓库 issue 事件以及 push 事件,并增强用于渲染通知的工具和模板。

新功能:

  • 添加 issue 订阅功能,通过计划轮询任务检测并推送仓库 issue 变更
  • 扩展 AddRepo 和 RemoveRepo 命令,以接受和管理订阅事件类型 (push, issue)
  • 引入 event 和 issue 数据库表和模型,用于存储订阅元数据
  • 添加 issue 推送页面模板、CSS 样式和 issue.svg 图标资源,用于通知

错误修复:

  • 修复客户端代理和反向代理配置逻辑

改进:

  • 重构 push 任务逻辑,根据订阅的事件类型过滤仓库
  • 添加 formatDate 和 make_hash 工具函数,用于日期格式化和内容哈希
  • 重命名 admin 命令命名空间并改进 tsconfig 路径格式

构建:

  • 将 nipaw 依赖升级到 ^1.8.0

文档:

  • 更新帮助页面,在订阅命令中包含 event 参数
Original summary in English

Sourcery 总结

实现一个统一的仓库推送和议题事件订阅系统:引入新的数据库 schema 和模型,扩展管理命令以管理多种订阅类型,并添加带模板的计划任务来渲染和分发通知。

新功能:

  • 支持订阅仓库议题变更事件并发送渲染后的通知
  • 添加一个通用事件订阅模型,并新增数据库表 (event, issue)
  • 扩展管理命令 (AddRepo/RemoveRepo) 以指定订阅类型 (push, issue)
  • 引入计划任务,用于按平台轮询和推送议题和推送事件
  • 添加议题通知页面模板、CSS 样式和 issue.svg 图标资源

错误修复:

  • 修复客户端代理配置逻辑错误

改进:

  • 重构推送逻辑以使用统一的基于事件的过滤
  • 添加 formatDate 和 make_hash 工具函数
  • 改进客户端代理/反向代理配置处理

构建:

  • 将 nipaw 依赖升级到 ^1.8.0

文档:

  • 更新帮助页面以记录订阅命令的新事件参数
Original summary in English

Sourcery 总结

通过添加新命令、数据库表、计划任务、模板和实用工具,实现了问题订阅功能,同时支持推送通知;重构了基于事件订阅的模式和任务逻辑;修复了代理配置并升级了依赖。

新功能:

  • 支持订阅仓库问题事件以及推送事件,并发送渲染后的通知
  • 扩展 AddRepo 和 RemoveRepo 管理命令,以接受和管理推送和问题事件类型的订阅
  • 引入计划任务,用于轮询多个平台(GitHub、Gitee、GitCode、Cnb)并推送问题变更通知
  • 添加问题通知模板(HTML、CSS)和一个用于渲染问题更新的 issue.svg 图标
  • 引入 formatDate 和 make_hash 实用函数,用于日期格式化和内容哈希

错误修复:

  • 修复通用客户端中的客户端代理和反向代理配置逻辑

改进:

  • 通过添加事件表和问题表,并更新推送表以引用事件订阅来重构数据库模式
  • 重构推送任务逻辑,根据事件类型过滤和处理订阅
  • 将 nipaw 依赖升级到 ^1.8.0,并调整 TypeScript 配置路径和格式
  • 添加 EventType 枚举,并扩展数据库模型和类型以支持事件和问题订阅元数据

构建:

  • 将 nipaw 依赖版本升级到 ^1.8.0

文档:

  • 更新帮助页面,以说明订阅命令中的新事件参数
Original summary in English

Summary by Sourcery

Implement issue subscription alongside push notifications by adding new commands, database tables, scheduled tasks, templates, and utility tools; refactor schema and task logic for event-based subscriptions; fix proxy configuration and bump dependency.

New Features:

  • Support subscribing to repository issue events along with push events and sending rendered notifications
  • Extend AddRepo and RemoveRepo admin commands to accept and manage subscriptions for push and issue event types
  • Introduce scheduled tasks to poll multiple platforms (GitHub, Gitee, GitCode, Cnb) and push issue change notifications
  • Add issue notification templates (HTML, CSS) and an issue.svg icon for rendering issue updates
  • Introduce formatDate and make_hash utility functions for date formatting and content hashing

Bug Fixes:

  • Fix client proxy and reverse proxy configuration logic in the common client

Enhancements:

  • Restructure database schema by adding event and issue tables and updating the push table to reference event subscriptions
  • Refactor push task logic to filter and process subscriptions based on event types
  • Upgrade nipaw dependency to ^1.8.0 and adjust TypeScript configuration paths and formatting
  • Add EventType enum and expand DB models and types for event and issue subscription metadata

Build:

  • Bump nipaw dependency version to ^1.8.0

Documentation:

  • Update help page to document the new event parameter in subscription commands

- 新增议题订阅功能,支持 push 和 issue 事件
- 添加 issue.svg 图标资源
- 创建议题推送页面模板和样式文件
- 实现定时任务检测议题变更并推送
- 数据库新增 event 和 issue 表用于存储订阅信息
- 更新添加仓库命令,支持指定订阅事件类型
- 优化日期格式化工具函数
- 修复客户端代理配置逻辑错误
- 完善议题信息渲染模板
- 添加议题推送合集转发消息功能
@wuliya336 wuliya336 linked an issue Nov 11, 2025 that may be closed by this pull request
4 tasks
@sourcery-ai
Copy link

sourcery-ai bot commented Nov 11, 2025

审阅者指南

此 PR 通过引入新的事件和议题数据库表和模型,扩展管理员命令以管理订阅类型,重构推送和议题任务以利用统一的事件模型,并添加用于渲染和分发通知的模板、实用程序和配置增强功能,实现了一个事件驱动的仓库推送和议题事件订阅系统。

AddRepo 命令与事件订阅的序列图

sequenceDiagram
  actor Admin
  participant Bot
  participant DB
  participant Client
  Admin->>Bot: #git添加[platform]订阅仓库owner/repo[:branch] [event]
  Bot->>DB: GetRepo(botId, groupId, owner, repo)
  alt 仓库不存在
    Bot->>DB: AddRepo(...)
    Bot->>DB: GetRepo(...)
  end
  Bot->>DB: GetRepo(platform, repoId, eventType)
  alt 事件不存在
    Bot->>DB: AddRepo(platform, repoId, eventType)
    Bot->>DB: GetRepo(platform, repoId, eventType)
  else 事件存在
    Bot->>DB: UpdateEventType(platform, repoId, eventType)
  end
  alt eventType 包含 push
    Bot->>Client: getRepoInfo(owner, repo)
    Bot->>DB: GetRepo(eventId, branch)
    alt pushRepo 不存在
      Bot->>DB: AddRepo(eventId, branch)
    end
  end
  alt eventType 包含 issue
    Bot->>Client: getIssueList(owner, repo)
    loop 针对每个议题
      Bot->>DB: AddRepo(eventId, issue.number, hash(title), hash(body), state)
    end
  end
  Bot->>Admin: 回复订阅结果
Loading

计划的议题轮询和通知推送的序列图

sequenceDiagram
  participant TaskScheduler
  participant Bot
  participant DB
  participant Client
  participant Group
  TaskScheduler->>Bot: 定时任务触发 (议题轮询)
  Bot->>DB: GetAll(platform, EventType.Issue)
  loop 针对每个事件
    Bot->>DB: GetRepo(event.repoId)
    Bot->>Client: getIssueList(owner, repo)
    loop 针对每个议题
      Bot->>DB: GetRepo(event.id, issue.number)
      alt 议题不存在
        Bot->>DB: AddRepo(event.id, issue.number, hash(title), hash(body), state)
        Bot->>Group: 推送新议题通知
      else 议题已更改
        Bot->>DB: UpdateState(event.id, issue.number, state)
        Bot->>Group: 推送议题变更通知
      end
    end
  end
Loading

新增和更新表的 ER 图:event, push, issue, repo

erDiagram
  REPO ||--o{ EVENT : "has"
  EVENT ||--o{ PUSH : "has"
  EVENT ||--o{ ISSUE : "has"
  REPO {
    id int PK
    botId string
    groupId string
    owner string
    repo string
    createAt date
    updateAt date
  }
  EVENT {
    id int PK
    repoId int FK
    platform string
    eventType string
    createdAt date
    updatedAt date
  }
  PUSH {
    id int PK
    eventId int FK
    branch string
    commitSha string
    createdAt date
    updatedAt date
  }
  ISSUE {
    id int PK
    eventId int FK
    issueId string
    title string
    body string
    state string
    createdAt date
    updatedAt date
  }
Loading

新增和更新数据模型的类图:RepoInfo, EventRepo, PushRepo, IssueRepo

classDiagram
  class RepoInfo {
    +int id
    +string botId
    +string groupId
    +string owner
    +string repo
    +Date createAt
    +Date updateAt
  }
  class EventRepo {
    +int id
    +int repoId
    +Platform platform
    +EventType[] eventType
    +Date createAt
    +Date updateAt
  }
  class PushRepo {
    +int eventId
    +string branch
    +string commitSha
    +Date createAt
    +Date updateAt
  }
  class IssueRepo {
    +int eventId
    +string issueId
    +string title
    +string body
    +StateType state
    +Date createAt
    +Date updateAt
  }
  RepoInfo "1" --o "*" EventRepo : 拥有
  EventRepo "1" --o "*" PushRepo : 拥有
  EventRepo "1" --o "*" IssueRepo : 拥有
Loading

文件级更改

更改 详情 文件
引入事件驱动订阅架构
  • 添加了带有 eventType 字段和 repo 外键的事件表
  • 重新定义了 push 和 issue 表以引用 eventId
  • 创建了 db/event.ts 和 db/issue.ts 模块,包含 CRUD 操作
  • 更新了 db/push.ts 以使用 eventId 而非 platform 和 repoId 进行操作
src/models/db/index.ts
src/models/db/event.ts
src/models/db/issue.ts
src/models/db/push.ts
扩展管理员命令以处理议题订阅
  • 更新 AddRepo/RemoveRepo 以解析和验证 push 和 issue 事件类型
  • 在命令工作流中集成了事件、推送和议题数据库操作
  • 在 admin 命名空间下重命名了命令并完善了正则表达式模式
src/apps/admin.ts
resources/help/index.html
实现计划的议题通知任务
  • 添加了 src/apps/issue.ts,其中包含特定于平台的任务和 handleRepoIssue 逻辑
  • 对新增或更改的议题进行分组并渲染通知
  • 支持多条议题更新的聚合转发消息
src/apps/issue.ts
重构推送任务以使用统一事件模型
  • 将 db.push.GetAll 切换为 db.event.GetAll 进行订阅迭代
  • 遍历事件以处理推送和议题仓库
  • 更新了 handleRepoPush 中的分组逻辑和 commitSha 更新
src/apps/push.ts
增强通用实用程序和客户端配置
  • 在 common/tools.ts 中引入了 formatDate 和 make_hash
  • 优化了 common/client.ts 中的代理和反向代理处理
  • 重新导出了 tools 模块并清理了 common/index.ts
src/common/tools.ts
src/common/client.ts
src/common/index.ts
添加议题通知模板和资产
  • 添加了议题通知的 HTML 模板和 CSS
  • 包含了 issue.svg 图标资源
resources/issue/index.html
resources/issue/index.css
resources/icons/issue.svg
更新依赖项并清理配置
  • 将 nipaw 依赖项提升到 ^1.8.0
  • 重构了 tsconfig.json 的格式和路径
  • 清理了 CHANGELOG.md 条目
package.json
tsconfig.json
CHANGELOG.md

对关联议题的评估

议题 目标 已解决 解释
#8 实现议题打开关闭相关的推送(即监听议题的打开/关闭事件并推送通知)
#8 实现获取议题列表
#8 数据库维护一个议题列表和状态,对照,并检测状态变化

可能关联的议题

  • #feat: 议题打开/关闭事件监听: 此 PR 通过添加议题事件订阅、计划的变更检测以及用于跟踪议题状态以进行通知的数据库,直接实现了该议题。

提示和命令

与 Sourcery 互动

  • 触发新审查: 在拉取请求上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub 议题: 回复 Sourcery 的审查评论,要求它从评论创建议题。你也可以回复审查评论并加上 @sourcery-ai issue 来创建一个议题。
  • 生成拉取请求标题: 在拉取请求标题的任何位置写入 @sourcery-ai,随时生成标题。你也可以在拉取请求上评论 @sourcery-ai title,随时(重新)生成标题。
  • 生成拉取请求摘要: 在拉取请求正文的任何位置写入 @sourcery-ai summary,随时在你想要的位置生成 PR 摘要。你也可以在拉取请求上评论 @sourcery-ai summary,随时(重新)生成摘要。
  • 生成审阅者指南: 在拉取请求上评论 @sourcery-ai guide,随时(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在拉取请求上评论 @sourcery-ai resolve,解决所有 Sourcery 评论。如果你已经处理了所有评论并且不想再看到它们,这会很有用。
  • 驳回所有 Sourcery 审查: 在拉取请求上评论 @sourcery-ai dismiss,驳回所有现有 Sourcery 审查。如果你想重新开始一次新的审查,这会特别有用——别忘了评论 @sourcery-ai review 来触发新的审查!

自定义你的体验

访问你的 仪表盘 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审阅者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

This PR implements an event-driven subscription system for repository push and issue events by introducing new event and issue database tables and models, extending admin commands to manage subscription types, refactoring push and issue tasks to leverage the unified event model, and adding templates, utilities, and configuration enhancements for rendering and dispatching notifications.

Sequence diagram for AddRepo command with event subscription

sequenceDiagram
  actor Admin
  participant Bot
  participant DB
  participant Client
  Admin->>Bot: #git添加[platform]订阅仓库owner/repo[:branch] [event]
  Bot->>DB: GetRepo(botId, groupId, owner, repo)
  alt repo not exists
    Bot->>DB: AddRepo(...)
    Bot->>DB: GetRepo(...)
  end
  Bot->>DB: GetRepo(platform, repoId, eventType)
  alt event not exists
    Bot->>DB: AddRepo(platform, repoId, eventType)
    Bot->>DB: GetRepo(platform, repoId, eventType)
  else event exists
    Bot->>DB: UpdateEventType(platform, repoId, eventType)
  end
  alt eventType includes push
    Bot->>Client: getRepoInfo(owner, repo)
    Bot->>DB: GetRepo(eventId, branch)
    alt pushRepo not exists
      Bot->>DB: AddRepo(eventId, branch)
    end
  end
  alt eventType includes issue
    Bot->>Client: getIssueList(owner, repo)
    loop for each issue
      Bot->>DB: AddRepo(eventId, issue.number, hash(title), hash(body), state)
    end
  end
  Bot->>Admin: 回复订阅结果
Loading

Sequence diagram for scheduled issue polling and notification push

sequenceDiagram
  participant TaskScheduler
  participant Bot
  participant DB
  participant Client
  participant Group
  TaskScheduler->>Bot: 定时任务触发 (issue polling)
  Bot->>DB: GetAll(platform, EventType.Issue)
  loop for each event
    Bot->>DB: GetRepo(event.repoId)
    Bot->>Client: getIssueList(owner, repo)
    loop for each issue
      Bot->>DB: GetRepo(event.id, issue.number)
      alt issue not exists
        Bot->>DB: AddRepo(event.id, issue.number, hash(title), hash(body), state)
        Bot->>Group: 推送新议题通知
      else issue changed
        Bot->>DB: UpdateState(event.id, issue.number, state)
        Bot->>Group: 推送议题变更通知
      end
    end
  end
Loading

ER diagram for new and updated tables: event, push, issue, repo

erDiagram
  REPO ||--o{ EVENT : "has"
  EVENT ||--o{ PUSH : "has"
  EVENT ||--o{ ISSUE : "has"
  REPO {
    id int PK
    botId string
    groupId string
    owner string
    repo string
    createAt date
    updateAt date
  }
  EVENT {
    id int PK
    repoId int FK
    platform string
    eventType string
    createdAt date
    updatedAt date
  }
  PUSH {
    id int PK
    eventId int FK
    branch string
    commitSha string
    createdAt date
    updatedAt date
  }
  ISSUE {
    id int PK
    eventId int FK
    issueId string
    title string
    body string
    state string
    createdAt date
    updatedAt date
  }
Loading

Class diagram for new and updated data models: RepoInfo, EventRepo, PushRepo, IssueRepo

classDiagram
  class RepoInfo {
    +int id
    +string botId
    +string groupId
    +string owner
    +string repo
    +Date createAt
    +Date updateAt
  }
  class EventRepo {
    +int id
    +int repoId
    +Platform platform
    +EventType[] eventType
    +Date createAt
    +Date updateAt
  }
  class PushRepo {
    +int eventId
    +string branch
    +string commitSha
    +Date createAt
    +Date updateAt
  }
  class IssueRepo {
    +int eventId
    +string issueId
    +string title
    +string body
    +StateType state
    +Date createAt
    +Date updateAt
  }
  RepoInfo "1" --o "*" EventRepo : has
  EventRepo "1" --o "*" PushRepo : has
  EventRepo "1" --o "*" IssueRepo : has
Loading

File-Level Changes

Change Details Files
Introduce event-driven subscription architecture
  • Added event table with eventType field and foreign key to repo
  • Redefined push and issue tables to reference eventId
  • Created db/event.ts and db/issue.ts modules with CRUD operations
  • Updated db/push.ts to operate using eventId rather than platform and repoId
src/models/db/index.ts
src/models/db/event.ts
src/models/db/issue.ts
src/models/db/push.ts
Extend admin commands to handle issue subscriptions
  • Updated AddRepo/RemoveRepo to parse and validate push and issue event types
  • Integrated event, push, and issue database operations in command workflows
  • Renamed commands under admin namespace and refined RegExp patterns
src/apps/admin.ts
resources/help/index.html
Implement scheduled issue notification tasks
  • Added src/apps/issue.ts with platform-specific tasks and handleRepoIssue logic
  • Grouped new or changed issues and rendered notifications
  • Supported aggregated forward messages for multiple issue updates
src/apps/issue.ts
Refactor push task to use unified event model
  • Switched from db.push.GetAll to db.event.GetAll for subscription iteration
  • Loop through events to handle both push and issue repositories
  • Updated grouping logic and commitSha updates in handleRepoPush
src/apps/push.ts
Enhance common utilities and client configuration
  • Introduced formatDate and make_hash in common/tools.ts
  • Optimized proxy and reverseProxy handling in common/client.ts
  • Re-exported tools module and cleaned up common/index.ts
src/common/tools.ts
src/common/client.ts
src/common/index.ts
Add issue notification templates and assets
  • Added HTML template and CSS for issue notifications
  • Included issue.svg icon resource
resources/issue/index.html
resources/issue/index.css
resources/icons/issue.svg
Update dependencies and clean up configurations
  • Bumped nipaw dependency to ^1.8.0
  • Refactored tsconfig.json formatting and paths
  • Cleaned up CHANGELOG.md entries
package.json
tsconfig.json
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#8 实现议题打开关闭相关的推送(即监听议题的打开/关闭事件并推送通知)
#8 实现获取议题列表
#8 数据库维护一个议题列表和状态,对照,并检测状态变化

Possibly linked issues

  • #feat: 议题打开/关闭事件监听: The PR directly implements the issue by adding issue event subscription, scheduled change detection, and a database to track issue states for notifications.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

大家好 - 我已经审阅了您的更改 - 以下是一些反馈:

  • 在 AddRepo/RemoveRepo 命令中为事件参数添加输入验证和默认处理,以避免用户省略或错误输入事件类型时出现运行时错误。
  • 将您的 TypeScript 模型字段名称 (createAt/updateAt) 与数据库列名称 (createdAt/updatedAt) 对齐,以避免映射不匹配。
  • 将重复的平台到客户端映射逻辑提取到共享助手函数中,以减少命令和任务之间的重复。
AI 代理提示
请解决此代码审查中的评论:

## 总体评论
- 在 AddRepo/RemoveRepo 命令中为事件参数添加输入验证和默认处理,以避免用户省略或错误输入事件类型时出现运行时错误。
- 将您的 TypeScript 模型字段名称 (createAt/updateAt) 与数据库列名称 (createdAt/updatedAt) 对齐,以避免映射不匹配。
- 将重复的平台到客户端映射逻辑提取到共享助手函数中,以减少命令和任务之间的重复。

## 单独评论

### 评论 1
<location> `src/apps/admin.ts:44` </location>
<code_context>
+    let eventInfo = await db.event.GetRepo(platformName, repoInfo.id)
+    if (!eventInfo?.length) {
</code_context>

<issue_to_address>
**issue:** `eventInfo` 是数组及其用法之间可能存在的混淆。

由于 `eventInfo` 始终是一个数组,如果数组为空,检查 `!eventInfo` 可能无法按预期工作。请使用 `!eventInfo.length` 来确保检查准确无误。
</issue_to_address>

### 评论 2
<location> `src/apps/admin.ts:54` </location>
<code_context>
+
+    let msg = `添加订阅仓库成功, 平台: ${platformName}, 仓库: ${owner}/${repo}, 订阅类型: ${event}`
+
+    if (eventType.includes(EventType.Push)) {
+      const PushBranch = branch || 'main'
+      const pushRepo = await db.push.GetRepo(
</code_context>

<issue_to_address>
**issue (bug_risk):** 推送订阅逻辑硬编码了 Platform.GitHub。

在 `pushRepo` 查找中使用 `Platform.GitHub` 可能会导致非 GitHub 平台上的行为不正确。请使用 `platformName` 以保持一致性。
</issue_to_address>

### 评论 3
<location> `src/apps/push.ts:193` </location>
<code_context>
+        pushRepoInfo.repo,
+        pushRepo.branch,
+      )
+      if (!commitInfo || commitInfo.sha === pushRepo.commitSha) continue
+      const groupKey = `${pushRepoInfo.botId}-${pushRepoInfo.groupId}`
+      if (!groupMap.has(groupKey)) {
</code_context>

<issue_to_address>
**issue (bug_risk):** 处理新提交后未更新 `commitSha`。

未更新 `commitSha` 可能会导致重复通知。请在处理后更新数据库中的 `commitSha`。
</issue_to_address>

### 评论 4
<location> `src/apps/issue.ts:22` </location>
<code_context>
+    if (isEmpty(token)) return logger.warn('未配置GitHub Token, 跳过任务')
+    try {
+      const client = Client.github()
+      handleRepoIssue(client, Platform.GitHub)
+    } catch (e) {
+      logger.error(e)
</code_context>

<issue_to_address>
**issue (bug_risk):** `handleRepoIssue` 缺少 `await` 可能会导致未处理的 Promise 拒绝。

由于 `handleRepoIssue` 是异步的,不等待它可能会导致执行不完整和遗漏错误。请添加 `await` 以确保正确处理。
</issue_to_address>

### 评论 5
<location> `src/apps/push.ts:159` </location>
<code_context>
 )

 const handleRepoPush = async (client: ClientType, platform: Platform) => {
-  const all = await db.push.GetAll()
+  const all = await db.event.GetAll()
</code_context>

<issue_to_address>
**issue (complexity):** 考虑通过提取辅助函数来重构 `handleRepoPush`,用于获取、分组和处理推送项。

考虑将 `handleRepoPush` 分解为更小的步骤并提取分组逻辑。例如:

1) 提取所有“符合推送条件”的项  
2) 按机器人+组进行分组  
3) 按组渲染/发送  

```ts
// helpers/push.ts
export async function fetchPushItems(
  client: ClientType,
  platform: Platform
): Promise<Array<{
  pushRepo: PushRepo
  pushRepoInfo: RepoInfo
  commitInfo: CommitInfo
}>> {
  const events = await db.event.GetAll()
  const pushEvents = events.filter(
    e => e.platform === platform && e.eventType.includes(EventType.Push)
  )
  const items: Array<{
    pushRepo: PushRepo
    pushRepoInfo: RepoInfo
    commitInfo: CommitInfo
  }> = []
  for (const ev of pushEvents) {
    const pushList = (await db.push.GetAll()).filter(p => p.repoId === ev.repoId)
    for (const pushRepo of pushList) {
      const info = await db.repo.GetRepo(pushRepo.repoId)
      if (!info) continue
      const commitInfo = await client.getCommitInfo(
        info.owner, info.repo, pushRepo.branch
      )
      if (!commitInfo || commitInfo.sha === pushRepo.commitSha) continue
      items.push({ pushRepo, pushRepoInfo: info, commitInfo })
    }
  }
  return items
}

export function groupByBotGroup<T extends { pushRepoInfo: RepoInfo }>(
  items: T[]
): Record<string, T[]> {
  return items.reduce((map, item) => {
    const key = `${item.pushRepoInfo.botId}-${item.pushRepoInfo.groupId}`
    ;(map[key] = map[key] || []).push(item)
    return map
  }, {} as Record<string, T[]>)
}
```

然后简化 `handleRepoPush````ts
import { fetchPushItems, groupByBotGroup } from './helpers/push'

const handleRepoPush = async (client: ClientType, platform: Platform) => {
  const items = await fetchPushItems(client, platform)
  if (items.length === 0) return

  const groups = groupByBotGroup(items)
  for (const [key, groupItems] of Object.entries(groups)) {
    const [botId, groupId] = key.split('-')
    let image: ImageElement[] = []

    for (const { pushRepo, pushRepoInfo, commitInfo } of groupItems) {
      const parts = commitInfo.commit.message.split('\n')
      // …build PushCommitInfo, Render, etc.
    }

    // …send to contactGroup(botId, groupId, image)
  }
}
```  

优点:
- 单一职责的助手函数  
- 更扁平的循环(核心内部没有嵌套的“.filter”+“.map”)  
- 更容易测试/维护每个部分而无需改变整体行为
</issue_to_address>

### 评论 6
<location> `src/models/db/issue.ts:7` </location>
<code_context>
+import { EventRepo, EventType, Platform } from '@/types'
+import { createClient } from './index'
+
+export async function AddRepo(
+  platform: Platform,
+  repoId: number,
</code_context>

<issue_to_address>
**issue (complexity):** 考虑将重载函数拆分为单一职责函数,并将数据库调用提取到辅助方法中,以阐明意图并减少分支。

```markdown
您可以通过以下方式显著降低认知负担:

1. 将每个重载拆分为两个命名清晰、职责单一的函数。
2. 将原始的 `client.run`/`client.all`/`client.get` 提取为返回 Promise 的小型辅助函数。

例如:

```ts
// db.ts
async function run(sql: string, params: any[]): Promise<void> {
  const client = await createClient();
  return new Promise((res, rej) =>
    client.run(sql, params, err => (err ? rej(err) : res()))
  );
}

async function all<T>(sql: string, params: any[]): Promise<T[]> {
  const client = await createClient();
  return new Promise((res, rej) =>
    client.all(sql, params, (err, rows) => (err ? rej(err) : res(rows as T[])))
  );
}

async function getOne<T>(sql: string, params: any[]): Promise<T | null> {
  const client = await createClient();
  return new Promise((res, rej) =>
    client.get(sql, params, (err, row) => (err ? rej(err) : res(row as T | null)))
  );
}
```

```ts
// issue.ts
export async function createRepo(platform: Platform, repoId: number) {
  await run(
    'INSERT INTO issue(platform, repoId) VALUES (?, ?)',
    [platform, repoId]
  );
}

export async function createIssue(
  platform: Platform,
  repoId: number,
  issueId: string,
  title: string,
  body: string | null,
  state: StateType
) {
  await run(
    'INSERT INTO issue(platform, repoId, issueId, title, body, state) VALUES (?, ?, ?, ?, ?, ?)',
    [platform, repoId, issueId, title, body, state]
  );
}

export async function getAllIssues(): Promise<IssueRepo[]> {
  return all<IssueRepo>('SELECT * FROM issue', []);
}

export async function getRepoIssues(
  platform: Platform,
  repoId: number
): Promise<IssueRepo[]> {
  return all<IssueRepo>(
    'SELECT * FROM issue WHERE platform = ? AND repoId = ?',
    [platform, repoId]
  );
}

export async function getIssue(
  platform: Platform,
  repoId: number,
  issueId: string
): Promise<IssueRepo | null> {
  return getOne<IssueRepo>(
    'SELECT * FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
    [platform, repoId, issueId]
  );
}

export async function deleteRepo(platform: Platform, repoId: number) {
  await run(
    'DELETE FROM issue WHERE platform = ? AND repoId = ?',
    [platform, repoId]
  );
}

export async function deleteIssue(
  platform: Platform,
  repoId: number,
  issueId: string
) {
  await run(
    'DELETE FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
    [platform, repoId, issueId]
  );
}

export async function updateState(
  platform: Platform,
  repoId: number,
  issueId: string,
  state: StateType
) {
  await run(
    'UPDATE issue SET state = ? WHERE platform = ? AND repoId = ? AND issueId = ?',
    [state, platform, repoId, issueId]
  );
}
```

这将:

- 使每个函数目的和参数明确。
- 消除可选参数分支。
- 将 Promise 封装整合到**三个**小型辅助函数中。
```
</issue_to_address>

### 评论 7
<location> `src/apps/issue.ts:18` </location>
<code_context>
    const token = Config.github.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** 在访问和使用属性时,首选对象解构。([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.github
```

<br/><details><summary>解释</summary>对象解构通常可以消除不必要的临时引用,并使您的代码更简洁。

来自 [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### 评论 8
<location> `src/apps/issue.ts:33` </location>
<code_context>
    const token = Config.gitee.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** 在访问和使用属性时,首选对象解构。([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.gitee
```

<br/><details><summary>解释</summary>对象解构通常可以消除不必要的临时引用,并使您的代码更简洁。

来自 [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### 评论 9
<location> `src/apps/issue.ts:48` </location>
<code_context>
    const token = Config.gitcode.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** 在访问和使用属性时,首选对象解构。([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.gitcode
```

<br/><details><summary>解释</summary>对象解构通常可以消除不必要的临时引用,并使您的代码更简洁。

来自 [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### 10
<location> `src/apps/issue.ts:63` </location>
<code_context>
    const token = Config.cnb.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** 在访问和使用属性时,首选对象解构。([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.cnb
```

<br/><details><summary>解释</summary>对象解构通常可以消除不必要的临时引用,并使您的代码更简洁。

来自 [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### 评论 11
<location> `src/models/db/issue.ts:47-53` </location>
<code_context>
  const result = await new Promise<IssueRepo[]>((resolve, reject) => {
    client.all('SELECT * FROM issue', [], (err, rows) => {
      if (err) reject(err)
      else resolve(rows as IssueRepo[])
    })
  })
  return result

</code_context>

<issue_to_address>
**suggestion (code-quality):** 内联立即返回的变量 ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
  return await new Promise<IssueRepo[]>((resolve, reject) => {
      client.all('SELECT * FROM issue', [], (err, rows) => {
        if (err) reject(err)
        else resolve(rows as IssueRepo[])
      })
    });

```

<br/><details><summary>解释</summary>我们经常在人们的代码中看到将结果变量赋值然后立即返回的情况。

直接返回结果可以缩短代码并消除不必要的变量,从而减少阅读函数的认知负担。

中间变量的有用之处在于,如果它们被用作参数或条件,并且名称可以作为变量表示内容的注释。在您从函数返回它的情况下,函数名称会告诉您结果是什么,因此变量名称是不必要的。
</details>
</issue_to_address>

### 评论 12
<location> `src/models/db/issue.ts:74-84` </location>
<code_context>
    const result = await new Promise<IssueRepo | null>((resolve, reject) => {
      client.get(
        'SELECT * FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
        [platform, repoId, issueId],
        (err, rows) => {
          if (err) reject(err)
          else resolve(rows as unknown as IssueRepo | null)
        },
      )
    })
    return result

</code_context>

<issue_to_address>
**suggestion (code-quality):** 内联立即返回的变量 ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
    return await new Promise<IssueRepo | null>((resolve, reject) => {
          client.get(
            'SELECT * FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
            [platform, repoId, issueId],
            (err, rows) => {
              if (err) reject(err)
              else resolve(rows as unknown as IssueRepo | null)
            },
          )
        });

```

<br/><details><summary>解释</summary>我们经常在人们的代码中看到将结果变量赋值然后立即返回的情况。

直接返回结果可以缩短代码并消除不必要的变量,从而减少阅读函数的认知负担。

中间变量的有用之处在于,如果它们被用作参数或条件,并且名称可以作为变量表示内容的注释。在您从函数返回它的情况下,函数名称会告诉您结果是什么,因此变量名称是不必要的。
</details>
</issue_to_address>

### 评论 13
<location> `src/models/db/issue.ts:86-96` </location>
<code_context>
    const result = await new Promise<IssueRepo[]>((resolve, reject) => {
      client.all(
        'SELECT * FROM issue WHERE platform = ? AND repoId = ?',
        [platform, repoId],
        (err, rows) => {
          if (err) reject(err)
          else resolve(rows as IssueRepo[])
        },
      )
    })
    return result

</code_context>

<issue_to_address>
**suggestion (code-quality):** 内联立即返回的变量 ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
    return await new Promise<IssueRepo[]>((resolve, reject) => {
          client.all(
            'SELECT * FROM issue WHERE platform = ? AND repoId = ?',
            [platform, repoId],
            (err, rows) => {
              if (err) reject(err)
              else resolve(rows as IssueRepo[])
            },
          )
        });

```

<br/><details><summary>解释</summary>我们经常在人们的代码中看到将结果变量赋值然后立即返回的情况。

直接返回结果可以缩短代码并消除不必要的变量,从而减少阅读函数的认知负担。

中间变量的有用之处在于,如果它们被用作参数或条件,并且名称可以作为变量表示内容的注释。在您从函数返回它的情况下,函数名称会告诉您结果是什么,因此变量名称是不必要的。
</details>
</issue_to_address>

Sourcery 对开源免费 - 如果您喜欢我们的评论,请考虑分享 ✨
帮助我更有用!请点击每个评论上的 👍 或 👎,我将使用反馈来改进您的评论。
Original comment in English

Hey there - I've reviewed your changes - here's some feedback:

  • Add input validation and default handling for the event parameter in AddRepo/RemoveRepo commands to avoid runtime errors when the user omits or mistypes the event type.
  • Align your TypeScript model field names (createAt/updateAt) with the database column names (createdAt/updatedAt) to avoid mapping mismatches.
  • Extract the repeated platform–to–client mapping logic into a shared helper to reduce duplication across commands and tasks.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Add input validation and default handling for the event parameter in AddRepo/RemoveRepo commands to avoid runtime errors when the user omits or mistypes the event type.
- Align your TypeScript model field names (createAt/updateAt) with the database column names (createdAt/updatedAt) to avoid mapping mismatches.
- Extract the repeated platform–to–client mapping logic into a shared helper to reduce duplication across commands and tasks.

## Individual Comments

### Comment 1
<location> `src/apps/admin.ts:44` </location>
<code_context>
+    let eventInfo = await db.event.GetRepo(platformName, repoInfo.id)
+    if (!eventInfo?.length) {
</code_context>

<issue_to_address>
**issue:** Possible confusion between eventInfo being an array and its usage.

Since eventInfo is always an array, checking '!eventInfo' may not work as intended if the array is empty. Use '!eventInfo.length' to ensure the check is accurate.
</issue_to_address>

### Comment 2
<location> `src/apps/admin.ts:54` </location>
<code_context>
+
+    let msg = `添加订阅仓库成功, 平台: ${platformName}, 仓库: ${owner}/${repo}, 订阅类型: ${event}`
+
+    if (eventType.includes(EventType.Push)) {
+      const PushBranch = branch || 'main'
+      const pushRepo = await db.push.GetRepo(
</code_context>

<issue_to_address>
**issue (bug_risk):** Push subscription logic hardcodes Platform.GitHub.

Using Platform.GitHub in pushRepo lookup may lead to incorrect behavior on non-GitHub platforms. Use platformName instead for consistency.
</issue_to_address>

### Comment 3
<location> `src/apps/push.ts:193` </location>
<code_context>
+        pushRepoInfo.repo,
+        pushRepo.branch,
+      )
+      if (!commitInfo || commitInfo.sha === pushRepo.commitSha) continue
+      const groupKey = `${pushRepoInfo.botId}-${pushRepoInfo.groupId}`
+      if (!groupMap.has(groupKey)) {
</code_context>

<issue_to_address>
**issue (bug_risk):** No update to commitSha after processing new commits.

Not updating commitSha may cause duplicate notifications. Please update commitSha in the database after processing.
</issue_to_address>

### Comment 4
<location> `src/apps/issue.ts:22` </location>
<code_context>
+    if (isEmpty(token)) return logger.warn('未配置GitHub Token, 跳过任务')
+    try {
+      const client = Client.github()
+      handleRepoIssue(client, Platform.GitHub)
+    } catch (e) {
+      logger.error(e)
</code_context>

<issue_to_address>
**issue (bug_risk):** Missing await for handleRepoIssue may cause unhandled promise rejections.

Since handleRepoIssue is async, not awaiting it may result in incomplete execution and missed errors. Add 'await' to ensure proper handling.
</issue_to_address>

### Comment 5
<location> `src/apps/push.ts:159` </location>
<code_context>
 )

 const handleRepoPush = async (client: ClientType, platform: Platform) => {
-  const all = await db.push.GetAll()
+  const all = await db.event.GetAll()
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring `handleRepoPush` by extracting helper functions for fetching, grouping, and processing push items.

Consider breaking `handleRepoPush` into smaller steps and extracting the grouping logic. For example:

1) Extract fetching all “push‐eligible” items  
2) Group them by bot+group  
3) Render/send per group  

```ts
// helpers/push.ts
export async function fetchPushItems(
  client: ClientType,
  platform: Platform
): Promise<Array<{
  pushRepo: PushRepo
  pushRepoInfo: RepoInfo
  commitInfo: CommitInfo
}>> {
  const events = await db.event.GetAll()
  const pushEvents = events.filter(
    e => e.platform === platform && e.eventType.includes(EventType.Push)
  )
  const items: Array<{
    pushRepo: PushRepo
    pushRepoInfo: RepoInfo
    commitInfo: CommitInfo
  }> = []
  for (const ev of pushEvents) {
    const pushList = (await db.push.GetAll()).filter(p => p.repoId === ev.repoId)
    for (const pushRepo of pushList) {
      const info = await db.repo.GetRepo(pushRepo.repoId)
      if (!info) continue
      const commitInfo = await client.getCommitInfo(
        info.owner, info.repo, pushRepo.branch
      )
      if (!commitInfo || commitInfo.sha === pushRepo.commitSha) continue
      items.push({ pushRepo, pushRepoInfo: info, commitInfo })
    }
  }
  return items
}

export function groupByBotGroup<T extends { pushRepoInfo: RepoInfo }>(
  items: T[]
): Record<string, T[]> {
  return items.reduce((map, item) => {
    const key = `${item.pushRepoInfo.botId}-${item.pushRepoInfo.groupId}`
    ;(map[key] = map[key] || []).push(item)
    return map
  }, {} as Record<string, T[]>)
}
```

Then simplify `handleRepoPush`:

```ts
import { fetchPushItems, groupByBotGroup } from './helpers/push'

const handleRepoPush = async (client: ClientType, platform: Platform) => {
  const items = await fetchPushItems(client, platform)
  if (items.length === 0) return

  const groups = groupByBotGroup(items)
  for (const [key, groupItems] of Object.entries(groups)) {
    const [botId, groupId] = key.split('-')
    let image: ImageElement[] = []

    for (const { pushRepo, pushRepoInfo, commitInfo } of groupItems) {
      const parts = commitInfo.commit.message.split('\n')
      // …build PushCommitInfo, Render, etc.
    }

    // …send to contactGroup(botId, groupId, image)
  }
}
```  

Benefits:
- Single‐responsibility helpers  
- Flatter loops (no nested “.filter” + “.map” inside core)  
- Easier to test/maintain each piece without changing overall behavior
</issue_to_address>

### Comment 6
<location> `src/models/db/issue.ts:7` </location>
<code_context>
+import { EventRepo, EventType, Platform } from '@/types'
+import { createClient } from './index'
+
+export async function AddRepo(
+  platform: Platform,
+  repoId: number,
</code_context>

<issue_to_address>
**issue (complexity):** Consider splitting overloaded functions into single-responsibility functions and extracting database calls into helper methods to clarify intent and reduce branching.

```markdown
You can dramatically reduce cognitive overhead by

1. Splitting each overload into two clearly-named, single-responsibility functions.
2. Factoring out your raw `client.run`/`client.all`/`client.get` into small promise-returning helpers.

For example:

```ts
// db.ts
async function run(sql: string, params: any[]): Promise<void> {
  const client = await createClient();
  return new Promise((res, rej) =>
    client.run(sql, params, err => (err ? rej(err) : res()))
  );
}

async function all<T>(sql: string, params: any[]): Promise<T[]> {
  const client = await createClient();
  return new Promise((res, rej) =>
    client.all(sql, params, (err, rows) => (err ? rej(err) : res(rows as T[])))
  );
}

async function getOne<T>(sql: string, params: any[]): Promise<T | null> {
  const client = await createClient();
  return new Promise((res, rej) =>
    client.get(sql, params, (err, row) => (err ? rej(err) : res(row as T | null)))
  );
}
```

```ts
// issue.ts
export async function createRepo(platform: Platform, repoId: number) {
  await run(
    'INSERT INTO issue(platform, repoId) VALUES (?, ?)',
    [platform, repoId]
  );
}

export async function createIssue(
  platform: Platform,
  repoId: number,
  issueId: string,
  title: string,
  body: string | null,
  state: StateType
) {
  await run(
    'INSERT INTO issue(platform, repoId, issueId, title, body, state) VALUES (?, ?, ?, ?, ?, ?)',
    [platform, repoId, issueId, title, body, state]
  );
}

export async function getAllIssues(): Promise<IssueRepo[]> {
  return all<IssueRepo>('SELECT * FROM issue', []);
}

export async function getRepoIssues(
  platform: Platform,
  repoId: number
): Promise<IssueRepo[]> {
  return all<IssueRepo>(
    'SELECT * FROM issue WHERE platform = ? AND repoId = ?',
    [platform, repoId]
  );
}

export async function getIssue(
  platform: Platform,
  repoId: number,
  issueId: string
): Promise<IssueRepo | null> {
  return getOne<IssueRepo>(
    'SELECT * FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
    [platform, repoId, issueId]
  );
}

export async function deleteRepo(platform: Platform, repoId: number) {
  await run(
    'DELETE FROM issue WHERE platform = ? AND repoId = ?',
    [platform, repoId]
  );
}

export async function deleteIssue(
  platform: Platform,
  repoId: number,
  issueId: string
) {
  await run(
    'DELETE FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
    [platform, repoId, issueId]
  );
}

export async function updateState(
  platform: Platform,
  repoId: number,
  issueId: string,
  state: StateType
) {
  await run(
    'UPDATE issue SET state = ? WHERE platform = ? AND repoId = ? AND issueId = ?',
    [state, platform, repoId, issueId]
  );
}
```

This:

- Makes each function’s purpose and parameters explicit.
- Removes optional-parameter branching.
- Consolidates promise-wrapping in **three** tiny helpers.
```
</issue_to_address>

### Comment 7
<location> `src/apps/issue.ts:18` </location>
<code_context>
    const token = Config.github.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.github
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### Comment 8
<location> `src/apps/issue.ts:33` </location>
<code_context>
    const token = Config.gitee.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.gitee
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### Comment 9
<location> `src/apps/issue.ts:48` </location>
<code_context>
    const token = Config.gitcode.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.gitcode
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### Comment 10
<location> `src/apps/issue.ts:63` </location>
<code_context>
    const token = Config.cnb.token

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
    const {token} = Config.cnb
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### Comment 11
<location> `src/models/db/issue.ts:47-53` </location>
<code_context>
  const result = await new Promise<IssueRepo[]>((resolve, reject) => {
    client.all('SELECT * FROM issue', [], (err, rows) => {
      if (err) reject(err)
      else resolve(rows as IssueRepo[])
    })
  })
  return result

</code_context>

<issue_to_address>
**suggestion (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
  return await new Promise<IssueRepo[]>((resolve, reject) => {
      client.all('SELECT * FROM issue', [], (err, rows) => {
        if (err) reject(err)
        else resolve(rows as IssueRepo[])
      })
    });

```

<br/><details><summary>Explanation</summary>Something that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.
</details>
</issue_to_address>

### Comment 12
<location> `src/models/db/issue.ts:74-84` </location>
<code_context>
    const result = await new Promise<IssueRepo | null>((resolve, reject) => {
      client.get(
        'SELECT * FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
        [platform, repoId, issueId],
        (err, rows) => {
          if (err) reject(err)
          else resolve(rows as unknown as IssueRepo | null)
        },
      )
    })
    return result

</code_context>

<issue_to_address>
**suggestion (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
    return await new Promise<IssueRepo | null>((resolve, reject) => {
          client.get(
            'SELECT * FROM issue WHERE platform = ? AND repoId = ? AND issueId = ?',
            [platform, repoId, issueId],
            (err, rows) => {
              if (err) reject(err)
              else resolve(rows as unknown as IssueRepo | null)
            },
          )
        });

```

<br/><details><summary>Explanation</summary>Something that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.
</details>
</issue_to_address>

### Comment 13
<location> `src/models/db/issue.ts:86-96` </location>
<code_context>
    const result = await new Promise<IssueRepo[]>((resolve, reject) => {
      client.all(
        'SELECT * FROM issue WHERE platform = ? AND repoId = ?',
        [platform, repoId],
        (err, rows) => {
          if (err) reject(err)
          else resolve(rows as IssueRepo[])
        },
      )
    })
    return result

</code_context>

<issue_to_address>
**suggestion (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
    return await new Promise<IssueRepo[]>((resolve, reject) => {
          client.all(
            'SELECT * FROM issue WHERE platform = ? AND repoId = ?',
            [platform, repoId],
            (err, rows) => {
              if (err) reject(err)
              else resolve(rows as IssueRepo[])
            },
          )
        });

```

<br/><details><summary>Explanation</summary>Something that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

wuliya336 and others added 3 commits November 12, 2025 10:29
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@wuliya336
Copy link
Contributor Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

你好 - 我已经审查了你的更改,它们看起来很棒!

AI 代理提示
请解决此代码审查中的评论:

## 单独评论

### 评论 1
<location> `src/apps/admin.ts:69` </location>
<code_context>
+      }
     }
+
+    if (eventType.includes(EventType.Issue)) {
+      const IssueRepo = await db.issue.GetRepo(Platform.GitHub, repoInfo.id)
+      if (!IssueRepo?.length) {
</code_context>

<issue_to_address>
**issue (bug_risk):** 问题订阅逻辑可能导致数据库条目重复。

对同一存储库重复执行 AddRepo 可能会插入重复的问题。在插入之前实施唯一性约束或检查现有问题。
</issue_to_address>

### 评论 2
<location> `src/apps/admin.ts:54` </location>
<code_context>
+
+    let msg = `添加订阅仓库成功, 平台: ${platformName}, 仓库: ${owner}/${repo}, 订阅类型: ${event}`
+
+    if (eventType.includes(EventType.Push)) {
+      const PushBranch = branch || 'main'
+      const pushRepo = await db.push.GetRepo(
</code_context>

<issue_to_address>
**issue (bug_risk):** db.push.GetRepo 中的硬编码平台可能导致不一致。

db.push.GetRepo 始终使用 Platform.GitHub,这可能导致非 GitHub 平台的行为不正确。请改用 platformName 以确保正确处理。
</issue_to_address>

### 评论 3
<location> `src/apps/admin.ts:6` </location>
<code_context>
+import { ClientType, EventType, Platform } from '@/types'
 import karin from 'node-karin'

 export const AddRepo = karin.command(
-  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?$/i,
+  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?\s+([^/\s]+)?$/i,
</code_context>

<issue_to_address>
**issue (complexity):** 考虑重构命令逻辑,将平台解析、事件解析和事件订阅提取到辅助函数中,并将 Push/Issue 处理拆分为专用函数。

```markdown
我们可以通过提取三个辅助函数来大幅简化这两个命令:  
1) `resolvePlatform` (平台名称 + 客户端)  
2) `parseEventTypes`  
3) `ensureEventSubscription`  

然后将 Push/Issue 处理拆分为 `handlePush`/`handleIssue`。  

--- helpers.ts ---
```ts
import { ClientType, EventType, Platform } from '@/types'
import { Client } from '@/common'

// 1) platform + client resolution
export function resolvePlatform(p?: string): {
  platform: Platform,
  client: ClientType
} {
  switch (p?.toLowerCase()) {
    case 'gitcode': return { platform: Platform.GitCode, client: Client.gitcode() }
    case 'gitee':   return { platform: Platform.Gitee,  client: Client.gitee() }
    case 'cnb':     return { platform: Platform.Cnb,    client: Client.cnb() }
    default:        return { platform: Platform.GitHub, client: Client.github() }
  }
}

// 2) event parser
export function parseEventTypes(ev?: string): EventType[] {
  if (!ev) return [EventType.Push]
  return ev.toLowerCase()
           .split(',')
           .map(v => v.trim() === 'issue' ? EventType.Issue : EventType.Push)
}

// 3) event‐table upsert
export async function ensureEventSubscription(
  platform: Platform, repoId: number, types: EventType[]
) {
  const existing = await db.event.GetRepo(platform, repoId)
  if (!existing?.length) {
    await db.event.AddRepo(platform, repoId, types)
    return db.event.GetRepo(platform, repoId)
  } else {
    await db.event.UpdateEventType(platform, repoId, types)
    return existing
  }
}
```

--- usage in AddRepo.ts ---
```ts
import { resolvePlatform, parseEventTypes, ensureEventSubscription } from './helpers'

export const AddRepo = karin.command(
  /^...$/,
  async e => {
    const [, platStr, owner, name, branch, evStr] = e.msg.match(AddRepo.reg)!
    const { platform, client } = resolvePlatform(platStr)
    const eventTypes = parseEventTypes(evStr)

    // repo ensure
    let repoInfo = await db.repo.GetRepo(e.selfId, e.groupId, owner, name)
    if (!repoInfo) {
      await db.repo.AddRepo(e.selfId, e.groupId, owner, name)
      repoInfo = await db.repo.GetRepo(e.selfId, e.groupId, owner, name)
    }
    if (!repoInfo) return e.reply('添加订阅仓库失败,请重试')

    await ensureEventSubscription(platform, repoInfo.id, eventTypes)

    let msg = `添加订阅仓库成功, 平台: ${platform}, 仓库: ${owner}/${name}, 订阅类型: ${evStr}`

    if (eventTypes.includes(EventType.Push)) {
      // ... call a small handlePush helper if you like
    }
    if (eventTypes.includes(EventType.Issue)) {
      // ... call a small handleIssue helper
    }

    return e.reply(msg)
  },
  { /* ... */ }
)
```

这会提取所有重复的分支,使每个步骤都集中,并保持您现有的行为不变。
</issue_to_address>

### 评论 4
<location> `src/apps/issue.ts:14` </location>
<code_context>
+  logger,
+} from 'node-karin'
+
+export const github = karin.task(
+  'karin-plugin-git:issue:github',
+  Config.github.cron || '0 */5 * * * *',
</code_context>

<issue_to_address>
**issue (complexity):** 考虑通过将重复的任务定义提取到辅助函数中,并将主处理程序拆分为专注于功能的函数,以提高清晰度和可维护性来重构代码。

```suggestion
您可以通过以下方式显着减少重复和嵌套:

1. 将四个几乎相同的任务定义提取到 `createIssueTask` 辅助函数中。
2. 将 `handleRepoIssue` 拆分为三个专注的函数:
   - `getRepoInfos(platform)`
   - `groupNewOrUpdatedIssues(client, repos, platform)`
   - `dispatchGroups(groupMap)`

这是一个最小示例:

```ts
// issue.ts

// 1. TASK FACTORY
function createIssueTask(
  name: string,
  platform: Platform,
  cfg: { cron?: string; token?: string },
  clientFactory: () => ClientType
) {
  return karin.task(
    `karin-plugin-git:issue:${name}`,
    cfg.cron || '0 */5 * * * *',
    async () => {
      if (isEmpty(cfg.token)) return logger.warn(`未配置${name} Token, 跳过任务`);
      try {
        const client = clientFactory();
        await handleRepoIssue(client, platform);
      } catch (e) {
        logger.error(e);
      }
    }
  );
}

export const github = createIssueTask('github', Platform.GitHub, Config.github, Client.github);
export const gitee  = createIssueTask('gitee',  Platform.Gitee,  Config.gitee,  Client.gitee);
export const gitcode= createIssueTask('gitcode',Platform.GitCode, Config.gitcode,Client.gitcode);
export const cnb    = createIssueTask('cnb',    Platform.Cnb,     Config.cnb,    Client.cnb);

// 2. MAIN HANDLER
async function handleRepoIssue(client: ClientType, platform: Platform) {
  const repos = await getRepoInfos(platform);
  const groupMap = await groupNewOrUpdatedIssues(client, repos, platform);
  await dispatchGroups(groupMap);
}

// 3a. FETCH REPO SUBSCRIPTIONS
async function getRepoInfos(platform: Platform) {
  const subs = await db.event.GetAll();
  return subs.filter(
    s => s.platform === platform && s.eventType.includes(EventType.Issue)
  );
}

// 3b. BUILD GROUP MAP
async function groupNewOrUpdatedIssues(
  client: ClientType,
  subs: Array<{ repoId: string }>,
  platform: Platform
) {
  const map = new Map<string, IssuePayload[]>();
  for (const sub of subs) {
    const repoInfo = await db.repo.GetRepo(sub.repoId);
    if (!repoInfo) continue;
    const key = `${repoInfo.botId}-${repoInfo.groupId}`;
    const list = await client.getIssueList(repoInfo.owner, repoInfo.repo, { perPage: 100 });
    for (const issue of list) {
      const changed = await isIssueChanged(sub.repoId, platform, issue);
      if (!changed) continue;
      await saveIssueState(sub.repoId, platform, issue);
      const entry = formatIssue(issue, repoInfo);
      (map.get(key) ?? map.set(key, []).get(key)!).push(entry);
    }
  }
  return map;
}

// 3c. DISPATCH MESSAGES
async function dispatchGroups(map: Map<string, IssuePayload[]>) {
  for (const [key, items] of map) {
    const [botId, groupId] = key.split('-');
    const images = await Promise.all(items.map(i => Render.render('issue/index', { issue: i })));
    if (images.length) {
      await sendImage(botId, groupId, images);
    }
  }
}

// Helper examples:
async function isIssueChanged(repoId, platform, issue) { /* compare db.issue.GetRepo */ }
async function saveIssueState(repoId, platform, issue) { /* db.issue.AddRepo/UpdateState */ }
function formatIssue(issue, repoInfo) { /* returns IssuePayload */ }
```

这保留了所有现有行为,但:

- 移除了四个复制粘贴的任务。
-`handleRepoIssue` 扁平化为清晰、可测试的步骤。
- 将数据库逻辑移动到小型辅助函数中 (`isIssueChanged`, `saveIssueState`, `formatIssue`)。
```
</issue_to_address>

### 评论 5
<location> `src/models/db/issue.ts:5` </location>
<code_context>
+import { createClient } from './index'
+import { Platform, IssueRepo } from '@/types'
+
+export async function AddRepo(platform: Platform, repoId: number): Promise<void>
+
+export async function AddRepo(
</code_context>

<issue_to_address>
**issue (complexity):** 考虑重构代码以使用通用的异步 SQL 辅助函数,并将重载折叠为每个 CRUD 操作的单个函数。

这里有一种方法可以消除所有重复的 Promise/SQL 样板代码,并将您的重载折叠成几个通用辅助函数。您保留了 100% 的相同功能,但:

  • 将每个原始的 `new Promise` + 回调替换为简单的 `await run/get/all`  
  • 删除了每个 CRUD 方法的 3 个重载  

```ts
// db.ts
import { createClient } from './index'
import { Database } from 'sqlite3'
import { promisify } from 'util'

type Client = Database & {
  runAsync: (sql: string, params?: any[]) => Promise<void>
  getAsync: <T>(sql: string, params?: any[]) => Promise<T | undefined>
  allAsync: <T>(sql: string, params?: any[]) => Promise<T[]>
}

export async function getClient(): Promise<Client> {
  const client: Database = await createClient()
  return Object.assign(client, {
    runAsync: promisify(client.run!.bind(client)),
    getAsync: promisify(client.get!.bind(client)),
    allAsync: promisify(client.all!.bind(client)),
  })
}
```

```ts
// issueModel.ts
import { getClient } from './db'
import { Platform, IssueRepo } from '@/types'
import { StateType } from 'nipaw'

const TABLE = 'issue'

async function queryOne<T>(sql: string, params: any[]): Promise<T | null> {
  const client = await getClient()
  const row = await client.getAsync<T>(sql, params)
  return row ?? null
}

async function queryAll<T>(sql: string, params: any[]): Promise<T[]> {
  const client = await getClient()
  return client.allAsync<T>(sql, params)
}

async function execute(sql: string, params: any[]): Promise<void> {
  const client = await getClient()
  await client.runAsync(sql, params)
}

export async function AddRepo(
  platform: Platform,
  repoId: number,
  issueId?: string,
  title?: string,
  body?: string | null,
  state?: StateType
): Promise<void> {
  if (issueId && title != null && body != null && state != null) {
    await execute(
      `INSERT INTO ${TABLE} 
        (platform, repoId, issueId, title, body, state) 
       VALUES (?, ?, ?, ?, ?, ?)`,
      [platform, repoId, issueId, title, body, state]
    )
  } else {
    await execute(
      `INSERT INTO ${TABLE} (platform, repoId) VALUES (?, ?)`,
      [platform, repoId]
    )
  }
}

export async function GetAll(): Promise<IssueRepo[]> {
  return queryAll<IssueRepo>(`SELECT * FROM ${TABLE}`, [])
}

export async function GetRepo(
  platform: Platform,
  repoId: number,
  issueId?: string
): Promise<IssueRepo[] | IssueRepo | null> {
  if (issueId) {
    return queryOne<IssueRepo>(
      `SELECT * FROM ${TABLE}
          WHERE platform = ? AND repoId = ? AND issueId = ?`,
      [platform, repoId, issueId]
    )
  }
  return queryAll<IssueRepo>(
    `SELECT * FROM ${TABLE} WHERE platform = ? AND repoId = ?`,
    [platform, repoId]
  )
}

export async function RemoveRepo(
  platform: Platform,
  repoId: number,
  issueId?: string
): Promise<void> {
  const sql = issueId
    ? `DELETE FROM ${TABLE} WHERE platform = ? AND repoId = ? AND issueId = ?`
    : `DELETE FROM ${TABLE} WHERE platform = ? AND repoId = ?`
  const params = issueId ? [platform, repoId, issueId] : [platform, repoId]
  return execute(sql, params)
}

export async function UpdateState(
  platform: Platform,
  repoId: number,
  issueId: string,
  state: StateType
): Promise<void> {
  return execute(
    `UPDATE ${TABLE} 
       SET state = ? 
     WHERE platform = ? AND repoId = ? AND issueId = ?`,
    [state, platform, repoId, issueId]
  )
}
```

优点:
- 不再重复 `new Promise` + 回调  
- 每次查询只调用一次 `getClient()`  
- 所有重载都折叠成一个可选参数签名  
- SQL 字符串与辅助函数相邻,因此仍然清楚正在运行的内容。
</issue_to_address>

Sourcery 对开源免费 - 如果您喜欢我们的评论,请考虑分享它们 ✨
请帮助我更有用!请点击每个评论上的 👍 或 👎,我将使用反馈来改进您的评论。
Original comment in English

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/apps/admin.ts:69` </location>
<code_context>
+      }
     }
+
+    if (eventType.includes(EventType.Issue)) {
+      const IssueRepo = await db.issue.GetRepo(Platform.GitHub, repoInfo.id)
+      if (!IssueRepo?.length) {
</code_context>

<issue_to_address>
**issue (bug_risk):** Issue subscription logic may result in duplicate database entries.

Repeated AddRepo executions for the same repository may insert duplicate issues. Implement a uniqueness constraint or check for existing issues before inserting.
</issue_to_address>

### Comment 2
<location> `src/apps/admin.ts:54` </location>
<code_context>
+
+    let msg = `添加订阅仓库成功, 平台: ${platformName}, 仓库: ${owner}/${repo}, 订阅类型: ${event}`
+
+    if (eventType.includes(EventType.Push)) {
+      const PushBranch = branch || 'main'
+      const pushRepo = await db.push.GetRepo(
</code_context>

<issue_to_address>
**issue (bug_risk):** Hardcoded platform in db.push.GetRepo may cause inconsistency.

db.push.GetRepo always uses Platform.GitHub, which may lead to incorrect behavior for non-GitHub platforms. Use platformName instead to ensure proper handling.
</issue_to_address>

### Comment 3
<location> `src/apps/admin.ts:6` </location>
<code_context>
+import { ClientType, EventType, Platform } from '@/types'
 import karin from 'node-karin'

 export const AddRepo = karin.command(
-  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?$/i,
+  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?\s+([^/\s]+)?$/i,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the command logic by extracting platform resolution, event parsing, and event subscription into helpers, and splitting Push/Issue handling into dedicated functions.

```markdown
We can drastically simplify both commands by pulling out three helpers:  
1) `resolvePlatform` (platformName + client)  
2) `parseEventTypes`  
3) `ensureEventSubscription`  

Then split Push/Issue handling into `handlePush`/`handleIssue`.  

--- helpers.ts ---
```ts
import { ClientType, EventType, Platform } from '@/types'
import { Client } from '@/common'

// 1) platform + client resolution
export function resolvePlatform(p?: string): {
  platform: Platform,
  client: ClientType
} {
  switch (p?.toLowerCase()) {
    case 'gitcode': return { platform: Platform.GitCode, client: Client.gitcode() }
    case 'gitee':   return { platform: Platform.Gitee,  client: Client.gitee() }
    case 'cnb':     return { platform: Platform.Cnb,    client: Client.cnb() }
    default:        return { platform: Platform.GitHub, client: Client.github() }
  }
}

// 2) event parser
export function parseEventTypes(ev?: string): EventType[] {
  if (!ev) return [EventType.Push]
  return ev.toLowerCase()
           .split(',')
           .map(v => v.trim() === 'issue' ? EventType.Issue : EventType.Push)
}

// 3) event‐table upsert
export async function ensureEventSubscription(
  platform: Platform, repoId: number, types: EventType[]
) {
  const existing = await db.event.GetRepo(platform, repoId)
  if (!existing?.length) {
    await db.event.AddRepo(platform, repoId, types)
    return db.event.GetRepo(platform, repoId)
  } else {
    await db.event.UpdateEventType(platform, repoId, types)
    return existing
  }
}
```

--- usage in AddRepo.ts ---
```ts
import { resolvePlatform, parseEventTypes, ensureEventSubscription } from './helpers'

export const AddRepo = karin.command(
  /^...$/,
  async e => {
    const [, platStr, owner, name, branch, evStr] = e.msg.match(AddRepo.reg)!
    const { platform, client } = resolvePlatform(platStr)
    const eventTypes = parseEventTypes(evStr)

    // repo ensure
    let repoInfo = await db.repo.GetRepo(e.selfId, e.groupId, owner, name)
    if (!repoInfo) {
      await db.repo.AddRepo(e.selfId, e.groupId, owner, name)
      repoInfo = await db.repo.GetRepo(e.selfId, e.groupId, owner, name)
    }
    if (!repoInfo) return e.reply('添加订阅仓库失败,请重试')

    await ensureEventSubscription(platform, repoInfo.id, eventTypes)

    let msg = `添加订阅仓库成功, 平台: ${platform}, 仓库: ${owner}/${name}, 订阅类型: ${evStr}`

    if (eventTypes.includes(EventType.Push)) {
      // ... call a small handlePush helper if you like
    }
    if (eventTypes.includes(EventType.Issue)) {
      // ... call a small handleIssue helper
    }

    return e.reply(msg)
  },
  { /* ... */ }
)
```

This pulls out all repeated branching, makes each step focused, and keeps your existing behavior intact.
</issue_to_address>

### Comment 4
<location> `src/apps/issue.ts:14` </location>
<code_context>
+  logger,
+} from 'node-karin'
+
+export const github = karin.task(
+  'karin-plugin-git:issue:github',
+  Config.github.cron || '0 */5 * * * *',
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the code by extracting repeated task definitions into a helper and splitting the main handler into focused functions for clarity and maintainability.

```suggestion
You can significantly reduce duplication and nesting by:

1. Extracting the four nearly‐identical task definitions into a `createIssueTask` helper.
2. Breaking `handleRepoIssue` into three focused functions:
   - `getRepoInfos(platform)`
   - `groupNewOrUpdatedIssues(client, repos, platform)`
   - `dispatchGroups(groupMap)`

Here’s a minimal example:

```ts
// issue.ts

// 1. TASK FACTORY
function createIssueTask(
  name: string,
  platform: Platform,
  cfg: { cron?: string; token?: string },
  clientFactory: () => ClientType
) {
  return karin.task(
    `karin-plugin-git:issue:${name}`,
    cfg.cron || '0 */5 * * * *',
    async () => {
      if (isEmpty(cfg.token)) return logger.warn(`未配置${name} Token, 跳过任务`);
      try {
        const client = clientFactory();
        await handleRepoIssue(client, platform);
      } catch (e) {
        logger.error(e);
      }
    }
  );
}

export const github = createIssueTask('github', Platform.GitHub, Config.github, Client.github);
export const gitee  = createIssueTask('gitee',  Platform.Gitee,  Config.gitee,  Client.gitee);
export const gitcode= createIssueTask('gitcode',Platform.GitCode, Config.gitcode,Client.gitcode);
export const cnb    = createIssueTask('cnb',    Platform.Cnb,     Config.cnb,    Client.cnb);

// 2. MAIN HANDLER
async function handleRepoIssue(client: ClientType, platform: Platform) {
  const repos = await getRepoInfos(platform);
  const groupMap = await groupNewOrUpdatedIssues(client, repos, platform);
  await dispatchGroups(groupMap);
}

// 3a. FETCH REPO SUBSCRIPTIONS
async function getRepoInfos(platform: Platform) {
  const subs = await db.event.GetAll();
  return subs.filter(
    s => s.platform === platform && s.eventType.includes(EventType.Issue)
  );
}

// 3b. BUILD GROUP MAP
async function groupNewOrUpdatedIssues(
  client: ClientType,
  subs: Array<{ repoId: string }>,
  platform: Platform
) {
  const map = new Map<string, IssuePayload[]>();
  for (const sub of subs) {
    const repoInfo = await db.repo.GetRepo(sub.repoId);
    if (!repoInfo) continue;
    const key = `${repoInfo.botId}-${repoInfo.groupId}`;
    const list = await client.getIssueList(repoInfo.owner, repoInfo.repo, { perPage: 100 });
    for (const issue of list) {
      const changed = await isIssueChanged(sub.repoId, platform, issue);
      if (!changed) continue;
      await saveIssueState(sub.repoId, platform, issue);
      const entry = formatIssue(issue, repoInfo);
      (map.get(key) ?? map.set(key, []).get(key)!).push(entry);
    }
  }
  return map;
}

// 3c. DISPATCH MESSAGES
async function dispatchGroups(map: Map<string, IssuePayload[]>) {
  for (const [key, items] of map) {
    const [botId, groupId] = key.split('-');
    const images = await Promise.all(items.map(i => Render.render('issue/index', { issue: i })));
    if (images.length) {
      await sendImage(botId, groupId, images);
    }
  }
}

// Helper examples:
async function isIssueChanged(repoId, platform, issue) { /* compare db.issue.GetRepo */ }
async function saveIssueState(repoId, platform, issue) { /* db.issue.AddRepo/UpdateState */ }
function formatIssue(issue, repoInfo) { /* returns IssuePayload */ }
```

This keeps all existing behavior but:

- Removes four copy‐pasted tasks.
- Flattens `handleRepoIssue` into clear, testable steps.
- Moves DB logic into small helpers (`isIssueChanged`, `saveIssueState`, `formatIssue`).
```
</issue_to_address>

### Comment 5
<location> `src/models/db/issue.ts:5` </location>
<code_context>
+import { createClient } from './index'
+import { Platform, IssueRepo } from '@/types'
+
+export async function AddRepo(platform: Platform, repoId: number): Promise<void>
+
+export async function AddRepo(
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the code to use generic async SQL helpers and collapse overloads into single functions for each CRUD operation.

Here’s one way to DRY‐up all the repeated Promise/SQL boilerplate and collapse your overloads into a couple of generic helpers.  You keep 100% of the same functionality but:

  • replace every raw `new Promise` + callback with a simple `await run/get/all`  
  • remove the 3 overloads per CRUD method  

```ts
// db.ts
import { createClient } from './index'
import { Database } from 'sqlite3'
import { promisify } from 'util'

type Client = Database & {
  runAsync: (sql: string, params?: any[]) => Promise<void>
  getAsync: <T>(sql: string, params?: any[]) => Promise<T | undefined>
  allAsync: <T>(sql: string, params?: any[]) => Promise<T[]>
}

export async function getClient(): Promise<Client> {
  const client: Database = await createClient()
  return Object.assign(client, {
    runAsync: promisify(client.run!.bind(client)),
    getAsync: promisify(client.get!.bind(client)),
    allAsync: promisify(client.all!.bind(client)),
  })
}
```

```ts
// issueModel.ts
import { getClient } from './db'
import { Platform, IssueRepo } from '@/types'
import { StateType } from 'nipaw'

const TABLE = 'issue'

async function queryOne<T>(sql: string, params: any[]): Promise<T | null> {
  const client = await getClient()
  const row = await client.getAsync<T>(sql, params)
  return row ?? null
}

async function queryAll<T>(sql: string, params: any[]): Promise<T[]> {
  const client = await getClient()
  return client.allAsync<T>(sql, params)
}

async function execute(sql: string, params: any[]): Promise<void> {
  const client = await getClient()
  await client.runAsync(sql, params)
}

export async function AddRepo(
  platform: Platform,
  repoId: number,
  issueId?: string,
  title?: string,
  body?: string | null,
  state?: StateType
): Promise<void> {
  if (issueId && title != null && body != null && state != null) {
    await execute(
      `INSERT INTO ${TABLE} 
        (platform, repoId, issueId, title, body, state) 
       VALUES (?, ?, ?, ?, ?, ?)`,
      [platform, repoId, issueId, title, body, state]
    )
  } else {
    await execute(
      `INSERT INTO ${TABLE} (platform, repoId) VALUES (?, ?)`,
      [platform, repoId]
    )
  }
}

export async function GetAll(): Promise<IssueRepo[]> {
  return queryAll<IssueRepo>(`SELECT * FROM ${TABLE}`, [])
}

export async function GetRepo(
  platform: Platform,
  repoId: number,
  issueId?: string
): Promise<IssueRepo[] | IssueRepo | null> {
  if (issueId) {
    return queryOne<IssueRepo>(
      `SELECT * FROM ${TABLE}
          WHERE platform = ? AND repoId = ? AND issueId = ?`,
      [platform, repoId, issueId]
    )
  }
  return queryAll<IssueRepo>(
    `SELECT * FROM ${TABLE} WHERE platform = ? AND repoId = ?`,
    [platform, repoId]
  )
}

export async function RemoveRepo(
  platform: Platform,
  repoId: number,
  issueId?: string
): Promise<void> {
  const sql = issueId
    ? `DELETE FROM ${TABLE} WHERE platform = ? AND repoId = ? AND issueId = ?`
    : `DELETE FROM ${TABLE} WHERE platform = ? AND repoId = ?`
  const params = issueId ? [platform, repoId, issueId] : [platform, repoId]
  return execute(sql, params)
}

export async function UpdateState(
  platform: Platform,
  repoId: number,
  issueId: string,
  state: StateType
): Promise<void> {
  return execute(
    `UPDATE ${TABLE} 
       SET state = ? 
     WHERE platform = ? AND repoId = ? AND issueId = ?`,
    [state, platform, repoId, issueId]
  )
}
```

Benefits:
- no more repeated `new Promise` + callbacks  
- only one `getClient()` call per query  
- all overloads collapse into a single optional‐param signature  
- SQL strings live next to the helpers, so it’s still obvious what’s running.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@wuliya336
Copy link
Contributor Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

你好 - 我已审阅你的更改,它们看起来很棒!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/apps/issue.ts:75` </location>
<code_context>
+)
+
+const handleRepoIssue = async (client: ClientType, platform: Platform) => {
+  const all = await db.event.GetAll(platform, EventType.Push)
+  const groupMap = new Map<
+    string,
</code_context>

<issue_to_address>
**issue (bug_risk):** 用于检索议题事件的 EventType.Push 可能不正确。

请考虑更新 handleRepoIssue 中的事件检索,以使用 EventType.Issue,确保处理相关的议题事件。
</issue_to_address>

### Comment 2
<location> `src/models/db/event.ts:32-33` </location>
<code_context>
+): Promise<Array<EventRepo>> {
+  let client = await createClient()
+  let result: Array<EventRepo>
+  if (platform) {
+    result = await new Promise<Array<EventRepo>>((resolve, reject) => {
+      client.all(
+        'SELECT * FROM event WHERE platform = ?',
</code_context>

<issue_to_address>
**suggestion (bug_risk):** EventType 过滤逻辑可能无法按预期工作。

目前,当同时指定 platform 和 eventType 时,仅使用 platform 进行过滤。请更新逻辑以在提供这两个参数时支持按两者进行过滤。
</issue_to_address>

### Comment 3
<location> `src/models/db/event.ts:165` </location>
<code_context>
+  await new Promise<void>((resolve, reject) => {
+    client.run(
+      'UPDATE event SET eventType = ? WHERE platform = ? AND repoId = ?',
+      [event, platform, repoId],
+      (err) => {
+        if (err) reject(err)
</code_context>

<issue_to_address>
**issue (bug_risk):** EventType 数组直接传递给 SQL,这可能会导致序列化错误。

在 UpdateEventType 中,请在将事件数组作为参数传递之前,将其连接成一个逗号分隔的字符串,以防止与 SQL 存储格式发生类型不匹配。
</issue_to_address>

### Comment 4
<location> `src/apps/admin.ts:12-14` </location>
<code_context>
-    const [, platform, owner, repo, branch] = e.msg.match(AddRepo!.reg)!
+    const [, platform, owner, repo, branch, event] = e.msg.match(AddRepo!.reg)!
+
+    const eventType = event
+      ? (event.toLocaleLowerCase().split(',') as Array<EventType>)
+      : [EventType.Push]
</code_context>

<issue_to_address>
**suggestion:** 如果事件为空字符串,eventType 解析可能会产生空数组。

如果事件为空字符串,eventType 将是 [''],这与任何有效的 EventType 都不匹配。请过滤掉空字符串,或者在事件为空时默认设置为 [EventType.Push]```suggestion
    const eventType = event
      ? (
          event
            .toLocaleLowerCase()
            .split(',')
            .map(e => e.trim())
            .filter(e => e) as Array<EventType>
        )
      : [EventType.Push]

    // If eventType is empty after filtering, default to [EventType.Push]
    if (!eventType.length) {
      eventType.push(EventType.Push)
    }
```
</issue_to_address>

### Comment 5
<location> `src/apps/admin.ts:139` </location>
<code_context>
     }
-    await db.push.RemoveRepo(platformName, repoInfo.id, PushBranch)
+    if (eventType.includes(EventType.Issue)) {
+      const issueRepo = await db.issue.GetRepo(repoInfo.id)
+      if (!issueRepo) {
+        return await e.reply('议题订阅不存在,删除失败')
</code_context>

<issue_to_address>
**question:** issueRepo 检索中 eventId 的使用不一致。

请验证此处使用 repoInfo.id 是否正确,并与其他 eventInfo.id 的使用保持一致。
</issue_to_address>

### Comment 6
<location> `src/apps/admin.ts:7` </location>
<code_context>
+import { isEmpty } from 'es-toolkit/compat'
 import karin from 'node-karin'

 export const AddRepo = karin.command(
-  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?$/i,
+  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?\s+([^/\s]+)?$/i,
</code_context>

<issue_to_address>
**issue (complexity):** 考虑通过将平台/客户端解析、仓库/事件记录管理和事件订阅逻辑提取到辅助函数中来重构命令处理程序,以提高模块化。

```suggestion
`AddRepo` 和 `RemoveRepo` 都在一个函数中完成了相同的三件事:  
1) 解析平台→客户端,  
2) 确保仓库和事件记录,  
3) 订阅/取消订阅每种事件类型。  

你可以将这些任务分解成小的辅助函数——这样你的命令处理程序就只负责协调:

1. 提取平台 + 客户端解析  
2. 提取“确保仓库”和“确保事件”逻辑  
3. 提取推送/议题订阅 + 取消订阅  

示例:

```ts
// 1) resolve platform + client
function getPlatformClient(p?: string): { platform: Platform, client: ClientType } {
  switch (p?.toLowerCase()) {
    case 'gitcode': return { platform: Platform.GitCode, client: Client.gitcode() }
    case 'gitee':   return { platform: Platform.Gitee,   client: Client.gitee()   }
    case 'cnb':     return { platform: Platform.Cnb,     client: Client.cnb()     }
    default:        return { platform: Platform.GitHub, client: Client.github() }
  }
}

// 2) ensure repo + event records
async function ensureRepoEvent(
  botId: number, groupId: number,
  owner: string, repo: string,
  platform: Platform, events: EventType[]
) {
  let info = await db.repo.GetRepo(botId, groupId, owner, repo)
  if (!info) {
    await db.repo.AddRepo(botId, groupId, owner, repo)
    info = await db.repo.GetRepo(botId, groupId, owner, repo)!
  }

  let ev = await db.event.GetRepo(platform, info.id, events)
  if (!ev) {
    await db.event.AddRepo(platform, info.id, events)
    ev = await db.event.GetRepo(platform, info.id, events)!
  } else {
    await db.event.UpdateEventType(platform, info.id, events)
  }

  return { repoId: info.id, eventId: ev!.id }
}

// 3a) push subscription
async function subscribePush(eventId: number, branch: string, client: ClientType) {
  const existing = await db.push.GetRepo(eventId, branch)
  if (!existing) {
    await db.push.AddRepo(eventId, branch)
    return `分支: ${branch}`
  } else {
    return `推送订阅已存在,分支: ${branch}`
  }
}

// 3b) issue subscription
async function subscribeIssue(eventId: number, owner: string, repo: string, client: ClientType) {
  const existing = await db.issue.GetRepo(eventId)
  if (existing.length === 0) {
    const list = await client.getIssueList(owner, repo, { perPage: 100 })
    for (const i of list) {
      await db.issue.AddRepo(eventId, i.number, make_hash(i.title), i.body && make_hash(i.body), i.state)
    }
    return `议题 ${list.length} 条已订阅`
  } else {
    return `议题订阅已存在`
  }
}

// In your command:
export const AddRepo = karin.command(/^…$/, async e => {
  const [, platformArg, owner, repo, branchArg, evtArg] = e.msg.match(AddRepo!.reg)!
  const events = evtArg?.toLowerCase().split(',') as EventType[] ?? [EventType.Push]
  const { platform, client } = getPlatformClient(platformArg)
  const { repoId, eventId } = await ensureRepoEvent(e.selfId, e.groupId, owner, repo, platform, events)

  const parts: string[] = [`平台: ${platform}`, `仓库: ${owner}/${repo}`, `订阅类型: ${events.join(',')}`]

  if (events.includes(EventType.Push)) {
    const branch = branchArg || (await client.getRepoInfo(owner, repo)).defaultBranch
    parts.push(await subscribePush(eventId, branch, client))
  }
  if (events.includes(EventType.Issue)) {
    parts.push(await subscribeIssue(eventId, owner, repo, client))
  }
  await e.reply(`添加订阅仓库成功,` + parts.join(','))
})
````RemoveRepo` 中应用相同的模式。这使得命令处理程序专注于协调,而辅助函数则负责繁重的工作。
</issue_to_address>

### Comment 7
<location> `src/apps/issue.ts:14` </location>
<code_context>
+  logger,
+} from 'node-karin'
+
+export const github = karin.task(
+  'karin-plugin-git:issue:github',
+  Config.github.cron || '0 */5 * * * *',
</code_context>

<issue_to_address>
**issue (complexity):** 考虑重构代码,将任务注册和议题分组逻辑整合到可重用函数中,以提高可维护性。

```typescript
// 1) 消除任务注册中的重复代码 (DRY up task registration)
const ISSUE_TASKS = [
  { name: 'github', clientFn: Client.github, cfg: Config.github, platform: Platform.GitHub },
  { name: 'gitee',  clientFn: Client.gitee,  cfg: Config.gitee,  platform: Platform.Gitee  },
  { name: 'gitcode',clientFn: Client.gitcode,cfg: Config.gitcode,platform: Platform.GitCode},
  { name: 'cnb',     clientFn: Client.cnb,    cfg: Config.cnb,    platform: Platform.Cnb     },
] as const;

for (const { name, clientFn, cfg, platform } of ISSUE_TASKS) {
  karin.task(
    `karin-plugin-git:issue:${name}`,
    cfg.cron || '0 */5 * * * *',
    async () => {
      if (isEmpty(cfg.token)) {
        return logger.warn(`${name} token 未配置,跳过`);
      }
      try {
        await handleRepoIssue(clientFn(), platform);
      } catch (e) {
        logger.error(e);
      }
    },
  );
}

// 2) 将获取 + 分组提取到单独的函数中 (Extract fetching + grouping into its own function)
type IssuePayload = {
  owner: string;
  repo: string;
  title: string;
  body: string | null;
  user: IssueUserInfo;
  state: StateType;
  issueDate: string;
};

async function collectIssueGroups(
  events: Array<{ id: string; repoId: string }>,
  client: ClientType,
): Promise<Map<string, IssuePayload[]>> {
  const map = new Map<string, IssuePayload[]>();

  for (const ev of events) {
    const repo = await db.repo.GetRepo(ev.repoId);
    if (!repo) continue;

    const key = `${repo.groupId}-${repo.botId}`;
    map.set(key, map.get(key) ?? []);

    const list = await client.getIssueList(repo.owner, repo.repo, { perPage: 100 });
    for (const issue of list) {
      const current = await db.issue.GetRepo(ev.id, issue.number);
      const hashTitle = make_hash(issue.title);
      const hashBody = issue.body ? make_hash(issue.body) : null;
      const changed =
        !current ||
        current.state !== issue.state ||
        current.title !== hashTitle ||
        current.body !== hashBody;

      if (!current) {
        await db.issue.AddRepo(ev.id, issue.number, hashTitle, hashBody, issue.state);
      }
      if (changed) {
        map.get(key)!.push({
          owner: repo.owner,
          repo: repo.repo,
          title: await Render.markdown(issue.title),
          body: issue.body ? await Render.markdown(issue.body) : null,
          user: issue.user,
          state: issue.state,
          issueDate: formatDate(issue.createdAt),
        });
        if (current) {
          await db.issue.UpdateState(ev.id, issue.number, issue.state);
        }
      }
    }
  }

  return map;
}

// 3) 简化 handleRepoIssue (Simplify handleRepoIssue)
async function handleRepoIssue(client: ClientType, platform: Platform) {
  const events = await db.event.GetAll(platform, EventType.Push);
  const groups = await collectIssueGroups(events, client);

  for (const [key, issues] of groups) {
    const [groupId, botId] = key.split('-');
    if (issues.length === 0) continue;
    const images = await Promise.all(
      issues.map((issue) => Render.render('issue/index', { issue })),
    );
    await sendImage(botId, groupId, images);
  }
}
```  
这分解了:

- 任务注册循环  
- `collectIssueGroups` 用于获取、哈希、数据库更新  
- 一个精简的 `handleRepoIssue`,只负责连接各项功能。
</issue_to_address>

### Comment 8
<location> `src/models/db/event.ts:17` </location>
<code_context>
+  )
+}
+
+export async function GetAll(platform: Platform): Promise<Array<EventRepo>>
+
+export async function GetAll(
</code_context>

<issue_to_address>
**issue (complexity):** 考虑提取一个辅助函数用于查询和 eventType 解析,以简化 GetAll 和 GetRepo 中重复的逻辑并使其集中化。

```markdown
我建议提取一个单独的辅助函数来封装 `client.all` + `eventType` 分割,然后将 `GetAll`/`GetRepo` 简化为各几行代码:

```ts
// event.ts

async function queryEvents(
  client: sqlite3.Database,
  sql: string,
  params: unknown[],
): Promise<EventRepo[]> {
  const rows: any[] = await new Promise((res, rej) =>
    client.all(sql, params, (err, rows) => (err ? rej(err) : res(rows))),
  );
  return rows.map(r => ({
    ...r,
    eventType: (r.eventType as string).split(',') as EventType[],
  }));
}

export async function GetAll(
  platform?: Platform,
  eventType?: EventType,
): Promise<EventRepo[]> {
  const client = await createClient();
  const clauses: string[] = [];
  const params: unknown[] = [];

  if (platform) {
    clauses.push('platform = ?');
    params.push(platform);
  }
  if (eventType) {
    clauses.push('eventType = ?');
    params.push(eventType);
  }

  const where = clauses.length ? ' WHERE ' + clauses.join(' AND ') : '';
  return queryEvents(client, `SELECT * FROM event${where}`, params);
}

export async function GetRepo(
  platform: Platform,
  repoId: number,
  eventType?: EventType | EventType[],
): Promise<EventRepo | EventRepo[] | null> {
  const client = await createClient();
  // add repoId clause first
  const clauses = ['platform = ?', 'repoId = ?'];
  const params: unknown[] = [platform, repoId];

  if (eventType) {
    const evts = Array.isArray(eventType) ? eventType : [eventType];
    // for simplicity, do server‐side LIKE or IN if your DB supports it,
    // otherwise fetch all and filter in‐memory:
    const rows = await queryEvents(
      client,
      `SELECT * FROM event WHERE ${clauses.join(' AND ')}`,
      params,
    );
    const filtered = rows.filter(r => evts.some(e => r.eventType.includes(e)));
    return Array.isArray(eventType) ? filtered : filtered[0] || null;
  }

  // no eventType => return all matching repo
  return queryEvents(
    client,
    `SELECT * FROM event WHERE ${clauses.join(' AND ')}`,
    params,
  );
}
```

- 移除了所有重复的 `new Promise` 样板代码  
-`eventType.split(',')` 集中在一处  
- 将重载合并为可选参数 + 简单的保护子句  
- 保持了相同的功能和 SQL 覆盖范围。
</issue_to_address>

### Comment 9
<location> `src/models/db/issue.ts:7` </location>
<code_context>
+import { EventRepo, EventType, Platform } from '@/types'
+import { createClient } from './index'
+
+export async function AddRepo(
+  platform: Platform,
+  repoId: number,
</code_context>

<issue_to_address>
**issue (complexity):** 考虑将重载函数重构为针对每个用例的单独、命名清晰的函数,以避免基于可选参数进行分支。

考虑将每个重载拆分为其自己的命名清晰的函数,这样就不会有基于可选参数的分支:

```ts
// issue.ts

export async function addEvent(eventId: number): Promise<void> {
  const client = await createClient()
  await client.run(
    'INSERT INTO issue (eventId) VALUES (?)',
    [eventId],
  )
}

export async function addIssueDetail(
  eventId: number,
  issueId: string,
  title: string,
  body: string | null,
  state: StateType,
): Promise<void> {
  const client = await createClient()
  await client.run(
    'INSERT INTO issue (eventId, issueId, title, body, state) VALUES (?, ?, ?, ?, ?)',
    [eventId, issueId, title, body, state],
  )
}
```

```ts
// fetching

export async function getIssuesByEvent(eventId: number): Promise<IssueRepo[]> {
  const client = await createClient()
  return new Promise((res, rej) =>
    client.all('SELECT * FROM issue WHERE eventId = ?', [eventId], (e, rows) =>
      e ? rej(e) : res(rows as IssueRepo[]),
    ),
  )
}

export async function getIssue(
  eventId: number,
  issueId: string,
): Promise<IssueRepo | null> {
  const client = await createClient()
  return new Promise((res, rej) =>
    client.get(
      'SELECT * FROM issue WHERE eventId = ? AND issueId = ?',
      [eventId, issueId],
      (e, row) => (e ? rej(e) : res(row as IssueRepo | null)),
    ),
  )
}
```

```ts
// deleting

export async function removeIssuesByEvent(eventId: number): Promise<void> {
  const client = await createClient()
  await client.run('DELETE FROM issue WHERE eventId = ?', [eventId])
}

export async function removeIssue(
  eventId: number,
  issueId: string,
): Promise<void> {
  const client = await createClient()
  await client.run(
    'DELETE FROM issue WHERE eventId = ? AND issueId = ?',
    [eventId, issueId],
  )
}
```

这消除了重载,并使每个函数都具有单一用途和自文档化。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
请帮助我更有用!请在每条评论上点击 👍 或 👎,我将利用这些反馈来改进你的评论。
Original comment in English

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/apps/issue.ts:75` </location>
<code_context>
+)
+
+const handleRepoIssue = async (client: ClientType, platform: Platform) => {
+  const all = await db.event.GetAll(platform, EventType.Push)
+  const groupMap = new Map<
+    string,
</code_context>

<issue_to_address>
**issue (bug_risk):** EventType.Push used for issue event retrieval may be incorrect.

Consider updating the event retrieval in handleRepoIssue to use EventType.Issue to ensure relevant issue events are processed.
</issue_to_address>

### Comment 2
<location> `src/models/db/event.ts:32-33` </location>
<code_context>
+): Promise<Array<EventRepo>> {
+  let client = await createClient()
+  let result: Array<EventRepo>
+  if (platform) {
+    result = await new Promise<Array<EventRepo>>((resolve, reject) => {
+      client.all(
+        'SELECT * FROM event WHERE platform = ?',
</code_context>

<issue_to_address>
**suggestion (bug_risk):** EventType filtering logic may not work as intended.

Currently, when both platform and eventType are specified, only platform is used for filtering. Please update the logic to support filtering by both parameters when provided.
</issue_to_address>

### Comment 3
<location> `src/models/db/event.ts:165` </location>
<code_context>
+  await new Promise<void>((resolve, reject) => {
+    client.run(
+      'UPDATE event SET eventType = ? WHERE platform = ? AND repoId = ?',
+      [event, platform, repoId],
+      (err) => {
+        if (err) reject(err)
</code_context>

<issue_to_address>
**issue (bug_risk):** EventType array is passed directly to SQL, which may serialize incorrectly.

In UpdateEventType, join the event array into a comma-separated string before passing it as a parameter to prevent type mismatches with the SQL storage format.
</issue_to_address>

### Comment 4
<location> `src/apps/admin.ts:12-14` </location>
<code_context>
-    const [, platform, owner, repo, branch] = e.msg.match(AddRepo!.reg)!
+    const [, platform, owner, repo, branch, event] = e.msg.match(AddRepo!.reg)!
+
+    const eventType = event
+      ? (event.toLocaleLowerCase().split(',') as Array<EventType>)
+      : [EventType.Push]
</code_context>

<issue_to_address>
**suggestion:** eventType parsing may produce empty array if event is empty string.

If event is an empty string, eventType will be [''], which won't match any valid EventType. Filter out empty strings or default to [EventType.Push] when event is empty.

```suggestion
    const eventType = event
      ? (
          event
            .toLocaleLowerCase()
            .split(',')
            .map(e => e.trim())
            .filter(e => e) as Array<EventType>
        )
      : [EventType.Push]

    // If eventType is empty after filtering, default to [EventType.Push]
    if (!eventType.length) {
      eventType.push(EventType.Push)
    }
```
</issue_to_address>

### Comment 5
<location> `src/apps/admin.ts:139` </location>
<code_context>
     }
-    await db.push.RemoveRepo(platformName, repoInfo.id, PushBranch)
+    if (eventType.includes(EventType.Issue)) {
+      const issueRepo = await db.issue.GetRepo(repoInfo.id)
+      if (!issueRepo) {
+        return await e.reply('议题订阅不存在,删除失败')
</code_context>

<issue_to_address>
**question:** Inconsistent eventId usage for issueRepo retrieval.

Please verify that using repoInfo.id here is correct and consistent with other usages of eventInfo.id.
</issue_to_address>

### Comment 6
<location> `src/apps/admin.ts:7` </location>
<code_context>
+import { isEmpty } from 'es-toolkit/compat'
 import karin from 'node-karin'

 export const AddRepo = karin.command(
-  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?$/i,
+  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?\s+([^/\s]+)?$/i,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the command handlers by extracting platform/client resolution, repo/event record management, and event subscription logic into helper functions for better modularity.

```suggestion
Both `AddRepo` and `RemoveRepo` are doing the same three things in a single function:  
1) resolve platform→client,  
2) ensure repo & event records,  
3) subscribe/unsubscribe each event type.  

You can split each of those into small helpers—then your command handler is just orchestration:

1. Extract platform + client resolution  
2. Extract “ensure repo” and “ensure event” logic  
3. Extract push/issue subscribe + unsubscribe  

Example:

```ts
// 1) resolve platform + client
function getPlatformClient(p?: string): { platform: Platform, client: ClientType } {
  switch (p?.toLowerCase()) {
    case 'gitcode': return { platform: Platform.GitCode, client: Client.gitcode() }
    case 'gitee':   return { platform: Platform.Gitee,   client: Client.gitee()   }
    case 'cnb':     return { platform: Platform.Cnb,     client: Client.cnb()     }
    default:        return { platform: Platform.GitHub, client: Client.github() }
  }
}

// 2) ensure repo + event records
async function ensureRepoEvent(
  botId: number, groupId: number,
  owner: string, repo: string,
  platform: Platform, events: EventType[]
) {
  let info = await db.repo.GetRepo(botId, groupId, owner, repo)
  if (!info) {
    await db.repo.AddRepo(botId, groupId, owner, repo)
    info = await db.repo.GetRepo(botId, groupId, owner, repo)!
  }

  let ev = await db.event.GetRepo(platform, info.id, events)
  if (!ev) {
    await db.event.AddRepo(platform, info.id, events)
    ev = await db.event.GetRepo(platform, info.id, events)!
  } else {
    await db.event.UpdateEventType(platform, info.id, events)
  }

  return { repoId: info.id, eventId: ev!.id }
}

// 3a) push subscription
async function subscribePush(eventId: number, branch: string, client: ClientType) {
  const existing = await db.push.GetRepo(eventId, branch)
  if (!existing) {
    await db.push.AddRepo(eventId, branch)
    return `分支: ${branch}`
  } else {
    return `推送订阅已存在,分支: ${branch}`
  }
}

// 3b) issue subscription
async function subscribeIssue(eventId: number, owner: string, repo: string, client: ClientType) {
  const existing = await db.issue.GetRepo(eventId)
  if (existing.length === 0) {
    const list = await client.getIssueList(owner, repo, { perPage: 100 })
    for (const i of list) {
      await db.issue.AddRepo(eventId, i.number, make_hash(i.title), i.body && make_hash(i.body), i.state)
    }
    return `议题 ${list.length} 条已订阅`
  } else {
    return `议题订阅已存在`
  }
}

// In your command:
export const AddRepo = karin.command(/^…$/, async e => {
  const [, platformArg, owner, repo, branchArg, evtArg] = e.msg.match(AddRepo!.reg)!
  const events = evtArg?.toLowerCase().split(',') as EventType[] ?? [EventType.Push]
  const { platform, client } = getPlatformClient(platformArg)
  const { repoId, eventId } = await ensureRepoEvent(e.selfId, e.groupId, owner, repo, platform, events)

  const parts: string[] = [`平台: ${platform}`, `仓库: ${owner}/${repo}`, `订阅类型: ${events.join(',')}`]

  if (events.includes(EventType.Push)) {
    const branch = branchArg || (await client.getRepoInfo(owner, repo)).defaultBranch
    parts.push(await subscribePush(eventId, branch, client))
  }
  if (events.includes(EventType.Issue)) {
    parts.push(await subscribeIssue(eventId, owner, repo, client))
  }
  await e.reply(`添加订阅仓库成功,` + parts.join(','))
})
```

Apply the same pattern in `RemoveRepo`. This keeps the command handler focused on orchestration while the helpers do the heavy lifting.
</issue_to_address>

### Comment 7
<location> `src/apps/issue.ts:14` </location>
<code_context>
+  logger,
+} from 'node-karin'
+
+export const github = karin.task(
+  'karin-plugin-git:issue:github',
+  Config.github.cron || '0 */5 * * * *',
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the code to consolidate task registration and issue grouping logic into reusable functions for improved maintainability.

```typescript
// 1) DRY up task registration
const ISSUE_TASKS = [
  { name: 'github', clientFn: Client.github, cfg: Config.github, platform: Platform.GitHub },
  { name: 'gitee',  clientFn: Client.gitee,  cfg: Config.gitee,  platform: Platform.Gitee  },
  { name: 'gitcode',clientFn: Client.gitcode,cfg: Config.gitcode,platform: Platform.GitCode},
  { name: 'cnb',     clientFn: Client.cnb,    cfg: Config.cnb,    platform: Platform.Cnb     },
] as const;

for (const { name, clientFn, cfg, platform } of ISSUE_TASKS) {
  karin.task(
    `karin-plugin-git:issue:${name}`,
    cfg.cron || '0 */5 * * * *',
    async () => {
      if (isEmpty(cfg.token)) {
        return logger.warn(`${name} token not configured, skipping`);
      }
      try {
        await handleRepoIssue(clientFn(), platform);
      } catch (e) {
        logger.error(e);
      }
    },
  );
}

// 2) Extract fetching + grouping into its own function
type IssuePayload = {
  owner: string;
  repo: string;
  title: string;
  body: string | null;
  user: IssueUserInfo;
  state: StateType;
  issueDate: string;
};

async function collectIssueGroups(
  events: Array<{ id: string; repoId: string }>,
  client: ClientType,
): Promise<Map<string, IssuePayload[]>> {
  const map = new Map<string, IssuePayload[]>();

  for (const ev of events) {
    const repo = await db.repo.GetRepo(ev.repoId);
    if (!repo) continue;

    const key = `${repo.groupId}-${repo.botId}`;
    map.set(key, map.get(key) ?? []);

    const list = await client.getIssueList(repo.owner, repo.repo, { perPage: 100 });
    for (const issue of list) {
      const current = await db.issue.GetRepo(ev.id, issue.number);
      const hashTitle = make_hash(issue.title);
      const hashBody = issue.body ? make_hash(issue.body) : null;
      const changed =
        !current ||
        current.state !== issue.state ||
        current.title !== hashTitle ||
        current.body !== hashBody;

      if (!current) {
        await db.issue.AddRepo(ev.id, issue.number, hashTitle, hashBody, issue.state);
      }
      if (changed) {
        map.get(key)!.push({
          owner: repo.owner,
          repo: repo.repo,
          title: await Render.markdown(issue.title),
          body: issue.body ? await Render.markdown(issue.body) : null,
          user: issue.user,
          state: issue.state,
          issueDate: formatDate(issue.createdAt),
        });
        if (current) {
          await db.issue.UpdateState(ev.id, issue.number, issue.state);
        }
      }
    }
  }

  return map;
}

// 3) Simplify handleRepoIssue
async function handleRepoIssue(client: ClientType, platform: Platform) {
  const events = await db.event.GetAll(platform, EventType.Push);
  const groups = await collectIssueGroups(events, client);

  for (const [key, issues] of groups) {
    const [groupId, botId] = key.split('-');
    if (issues.length === 0) continue;
    const images = await Promise.all(
      issues.map((issue) => Render.render('issue/index', { issue })),
    );
    await sendImage(botId, groupId, images);
  }
}
```  
This factors out:

- task registration loop  
- `collectIssueGroups` for fetching, hashing, DB updates  
- a lean `handleRepoIssue` that just wires things up.
</issue_to_address>

### Comment 8
<location> `src/models/db/event.ts:17` </location>
<code_context>
+  )
+}
+
+export async function GetAll(platform: Platform): Promise<Array<EventRepo>>
+
+export async function GetAll(
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting a helper for querying and eventType parsing to simplify and centralize repeated logic in GetAll and GetRepo.

```markdown
I’d extract a single helper to wrap `client.all` + the `eventType` splitting, then reduce `GetAll`/`GetRepo` to a couple of lines each:

```ts
// event.ts

async function queryEvents(
  client: sqlite3.Database,
  sql: string,
  params: unknown[],
): Promise<EventRepo[]> {
  const rows: any[] = await new Promise((res, rej) =>
    client.all(sql, params, (err, rows) => (err ? rej(err) : res(rows))),
  );
  return rows.map(r => ({
    ...r,
    eventType: (r.eventType as string).split(',') as EventType[],
  }));
}

export async function GetAll(
  platform?: Platform,
  eventType?: EventType,
): Promise<EventRepo[]> {
  const client = await createClient();
  const clauses: string[] = [];
  const params: unknown[] = [];

  if (platform) {
    clauses.push('platform = ?');
    params.push(platform);
  }
  if (eventType) {
    clauses.push('eventType = ?');
    params.push(eventType);
  }

  const where = clauses.length ? ' WHERE ' + clauses.join(' AND ') : '';
  return queryEvents(client, `SELECT * FROM event${where}`, params);
}

export async function GetRepo(
  platform: Platform,
  repoId: number,
  eventType?: EventType | EventType[],
): Promise<EventRepo | EventRepo[] | null> {
  const client = await createClient();
  // add repoId clause first
  const clauses = ['platform = ?', 'repoId = ?'];
  const params: unknown[] = [platform, repoId];

  if (eventType) {
    const evts = Array.isArray(eventType) ? eventType : [eventType];
    // for simplicity, do server‐side LIKE or IN if your DB supports it,
    // otherwise fetch all and filter in‐memory:
    const rows = await queryEvents(
      client,
      `SELECT * FROM event WHERE ${clauses.join(' AND ')}`,
      params,
    );
    const filtered = rows.filter(r => evts.some(e => r.eventType.includes(e)));
    return Array.isArray(eventType) ? filtered : filtered[0] || null;
  }

  // no eventType => return all matching repo
  return queryEvents(
    client,
    `SELECT * FROM event WHERE ${clauses.join(' AND ')}`,
    params,
  );
}
```

- Removes all the repeated `new Promise` boilerplate  
- Centralizes `eventType.split(',')` in one place  
- Collapses overloads into optional args + simple guard clauses  
- Keeps identical functionality and SQL coverage.
</issue_to_address>

### Comment 9
<location> `src/models/db/issue.ts:7` </location>
<code_context>
+import { EventRepo, EventType, Platform } from '@/types'
+import { createClient } from './index'
+
+export async function AddRepo(
+  platform: Platform,
+  repoId: number,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring overloaded functions into separate, clearly-named functions for each use case to avoid branching on optional arguments.

Consider splitting each overload into its own clearly‐named function so there’s no branching on optional args:

```ts
// issue.ts

export async function addEvent(eventId: number): Promise<void> {
  const client = await createClient()
  await client.run(
    'INSERT INTO issue (eventId) VALUES (?)',
    [eventId],
  )
}

export async function addIssueDetail(
  eventId: number,
  issueId: string,
  title: string,
  body: string | null,
  state: StateType,
): Promise<void> {
  const client = await createClient()
  await client.run(
    'INSERT INTO issue (eventId, issueId, title, body, state) VALUES (?, ?, ?, ?, ?)',
    [eventId, issueId, title, body, state],
  )
}
```

```ts
// fetching

export async function getIssuesByEvent(eventId: number): Promise<IssueRepo[]> {
  const client = await createClient()
  return new Promise((res, rej) =>
    client.all('SELECT * FROM issue WHERE eventId = ?', [eventId], (e, rows) =>
      e ? rej(e) : res(rows as IssueRepo[]),
    ),
  )
}

export async function getIssue(
  eventId: number,
  issueId: string,
): Promise<IssueRepo | null> {
  const client = await createClient()
  return new Promise((res, rej) =>
    client.get(
      'SELECT * FROM issue WHERE eventId = ? AND issueId = ?',
      [eventId, issueId],
      (e, row) => (e ? rej(e) : res(row as IssueRepo | null)),
    ),
  )
}
```

```ts
// deleting

export async function removeIssuesByEvent(eventId: number): Promise<void> {
  const client = await createClient()
  await client.run('DELETE FROM issue WHERE eventId = ?', [eventId])
}

export async function removeIssue(
  eventId: number,
  issueId: string,
): Promise<void> {
  const client = await createClient()
  await client.run(
    'DELETE FROM issue WHERE eventId = ? AND issueId = ?',
    [eventId, issueId],
  )
}
```

This eliminates overloads and keeps each function single‐purpose and self‐documenting.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 32 to 33
if (platform) {
result = await new Promise<Array<EventRepo>>((resolve, reject) => {
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): EventType 过滤逻辑可能无法按预期工作。

目前,当同时指定 platform 和 eventType 时,仅使用 platform 进行过滤。请更新逻辑以在提供这两个参数时支持按两者进行过滤。

Original comment in English

suggestion (bug_risk): EventType filtering logic may not work as intended.

Currently, when both platform and eventType are specified, only platform is used for filtering. Please update the logic to support filtering by both parameters when provided.

- 修改数据库表结构,将 push 和 issue 表关联到 event 表
- 更新事件类型处理逻辑,支持多事件订阅
- 重构添加/删除仓库订阅功能,提高代码可读性
- 增加对空仓库议题列表的处理
- 修复推送和议题订阅的查询逻辑
- 调整数据库索引以提升查询性能
- 修正命令参数默认值设置问题
- 完善错误提示信息,增强用户体验
@wuliya336
Copy link
Contributor Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The SQL filters in event.GetAll and event.GetRepo use exact matches on the comma‐separated eventType string, which will miss repos subscribed to multiple events—consider switching to LIKE or a normalized junction table to correctly include multi‐event subscriptions.
  • The AddRepo/RemoveRepo regex and positional parsing of branch vs. event can get ambiguous when one or the other is omitted; consider switching to explicit flags (e.g. --branch and --event) or reordering arguments to avoid misparses.
  • Push and issue task handlers share a lot of logic—extract common steps (e.g. grouping by bot/group, rendering, sending) into reusable helpers to reduce duplication and ease future changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The SQL filters in event.GetAll and event.GetRepo use exact matches on the comma‐separated eventType string, which will miss repos subscribed to multiple events—consider switching to LIKE or a normalized junction table to correctly include multi‐event subscriptions.
- The AddRepo/RemoveRepo regex and positional parsing of branch vs. event can get ambiguous when one or the other is omitted; consider switching to explicit flags (e.g. `--branch` and `--event`) or reordering arguments to avoid misparses.
- Push and issue task handlers share a lot of logic—extract common steps (e.g. grouping by bot/group, rendering, sending) into reusable helpers to reduce duplication and ease future changes.

## Individual Comments

### Comment 1
<location> `src/apps/admin.ts:60` </location>
<code_context>
+      return await e.reply('添加仓库订阅事件失败,请重试')
+    }
+
+    let msg = `添加订阅仓库成功, 平台: ${platformName}, 仓库: ${owner}/${repo}, 订阅类型: ${event}`
+
+    const PushEvent = eventType.includes(EventType.Push)
</code_context>

<issue_to_address>
**suggestion:** Message may not reflect actual eventType array.

Use eventType.join(',') instead of the raw 'event' string to ensure the message accurately reflects the normalized event types.

```suggestion
    let msg = `添加订阅仓库成功, 平台: ${platformName}, 仓库: ${owner}/${repo}, 订阅类型: ${eventType.join(',')}`
```
</issue_to_address>

### Comment 2
<location> `src/apps/push.ts:84` </location>
<code_context>
       const botId = e.selfId
       const groupId = e.groupId
-      const PushInfos = await db.push.GetAll()
+      const all = await db.event.GetAll()

-      let client: ClientType 
</code_context>

<issue_to_address>
**suggestion (performance):** No filtering by platform or event type in push command.

Querying all events without filters can lead to processing irrelevant data. Please add platform and event type filters to optimize efficiency.

Suggested implementation:

```typescript
      // Filter events by platform and event type for efficiency
      const platform = Platform.Cnb // or use the relevant platform variable
      const eventType = 'push' // or use the relevant event type variable
      const all = await db.event.GetAll({ platform, eventType })

```

Make sure that `db.event.GetAll` supports filtering by platform and event type via its parameters. If it does not, you will need to update its implementation to accept and use these filters.
</issue_to_address>

### Comment 3
<location> `src/apps/push.ts:111-120` </location>
<code_context>
+        for (const pushInfo of pushRepoList) {
</code_context>

<issue_to_address>
**suggestion:** No error handling for Render.render failures.

Add error handling to catch exceptions or validate the output of Render.render before adding images to the array.
</issue_to_address>

### Comment 4
<location> `src/apps/push.ts:145-142` </location>
<code_context>
+        for (const issue of issueRepoList) {
</code_context>

<issue_to_address>
**suggestion (performance):** Potential for unnecessary API calls for empty issues.

Filter out empty issues before making API requests to avoid unnecessary calls and reduce system load.
</issue_to_address>

### Comment 5
<location> `src/apps/push.ts:88-97` </location>
<code_context>
+    }>
+  >()
+
+  for (const event of all) {
+    const eventRepoInfo = await db.repo.GetRepo(event.repoId)
+    if (!eventRepoInfo) continue
</code_context>

<issue_to_address>
**issue (bug_risk):** Possible bug in pushRepoList fallback logic.

Ensure that the repo added when pushRepoList is empty uses the same identifier as the one used for fetching (event.repoId) to avoid inconsistencies.
</issue_to_address>

### Comment 6
<location> `src/models/db/push.ts:24-33` </location>
<code_context>
+  return result
+}
+
+export async function GetRepo(
+  platform: Platform,
+  repoId: number,
+): Promise<Array<EventRepo>>
+
+export async function GetRepo(
+  platform: Platform,
+  repoId: number,
+  eventType: Array<EventType> | EventType,
+): Promise<EventRepo | null>
+export async function GetRepo(
+  platform: Platform,
+  repoId: number,
+  eventType?: Array<EventType> | EventType,
+): Promise<Array<EventRepo> | EventRepo | null> {
+  let client = await createClient()
+
+  if (eventType) {
+    const result = await new Promise<Array<EventRepo>>((resolve, reject) => {
+      client.all(
+        'SELECT * FROM event WHERE platform = ? AND repoId = ?',
+        [platform, repoId],
+        (err, rows) => {
+          if (err) reject(err)
+          else resolve(rows as Array<EventRepo>)
+        },
+      )
+    })
+
+    if (result && result.length > 0) {
</code_context>

<issue_to_address>
**suggestion:** Overloaded GetRepo signature may cause confusion.

Overloading GetRepo with different return types based on arguments can introduce ambiguity and runtime errors. Recommend using distinct function names or ensuring a consistent return type to improve clarity and safety.
</issue_to_address>

### Comment 7
<location> `src/apps/issue.ts:75` </location>
<code_context>
+)
+
+const handleRepoIssue = async (client: ClientType, platform: Platform) => {
+  const all = await db.event.GetAll(platform, EventType.Push)
+  const groupMap = new Map<
+    string,
</code_context>

<issue_to_address>
**issue:** EventType.Push used for issue handling may be incorrect.

Consider fetching events with EventType.Issue, or both Push and Issue, to ensure all relevant issue events are handled.
</issue_to_address>

### Comment 8
<location> `src/apps/admin.ts:7` </location>
<code_context>
+import { isEmpty } from 'es-toolkit/compat'
 import karin from 'node-karin'

 export const AddRepo = karin.command(
-  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?$/i,
+  /^#?git(?:添加|add)([^\s]+)?订阅仓库([^/\s]+)\/([^:\s]+)(?::([^/\s]+))?\s+([^/\s]+)?$/i,
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting repeated logic in the handlers into small helper functions to flatten and simplify the code structure.

```markdown
You can dramatically flatten both handlers by extracting the repeated bits into small helpers. For example:

1) helpers/platform.ts  
```ts
import { Client, Config } from '@/common'
import { Platform, ClientType } from '@/types'

const PLATFORM_MAP: Record<string, { name: Platform; client: ClientType }> = {
  gitcode: { name: Platform.GitCode, client: Client.gitcode() },
  gitee:  { name: Platform.Gitee,  client: Client.gitee()  },
  cnb:    { name: Platform.Cnb,    client: Client.cnb()    },
  github: { name: Platform.GitHub, client: Client.github() },
}

export function parsePlatform(raw?: string) {
  const key = raw?.toLowerCase() || 'github'
  return PLATFORM_MAP[key] || PLATFORM_MAP['github']
}
```

2) helpers/events.ts  
```ts
import { EventType } from '@/types'

export function parseEventTypes(raw?: string): EventType[] {
  if (!raw) return [EventType.Push]
  const events = raw
    .toLowerCase()
    .split(',')
    .map(e => e.trim())
    .filter(Boolean) as EventType[]
  return events.length ? events : [EventType.Push]
}
```

3) helpers/repo.ts  
```ts
import { db } from '@/models'

export async function getOrCreateRepo(
  botId: number, groupId: number,
  owner: string, repo: string
) {
  let info = await db.repo.GetRepo(botId, groupId, owner, repo)
  if (!info) {
    await db.repo.AddRepo(botId, groupId, owner, repo)
    info = await db.repo.GetRepo(botId, groupId, owner, repo)
  }
  if (!info) throw new Error('repo')
  return info
}
```

4) In your `AddRepo` you then reduce nesting to:  
```ts
import { parsePlatform } from './helpers/platform'
import { parseEventTypes } from './helpers/events'
import { getOrCreateRepo } from './helpers/repo'

export const AddRepo = karin.command(
  /^$/,
  async e => {
    const [, rawPlat, owner, repo, branch, rawEvt] = e.msg.match(AddRepo.reg)!
    const { name: platformName, client } = parsePlatform(rawPlat)
    const eventTypes = parseEventTypes(rawEvt)

    const repoInfo = await getOrCreateRepo(e.selfId, e.groupId, owner, repo)
    // … similarly extract event‐table upsert, pushSubscribe, issueSubscribe
    // build `msg` and just `await e.reply(msg)`
  },
  { … }
)
```

Repeat the same pattern in `RemoveRepo`. This keeps all logic, removes deep nesting and makes each step easy to read and test.
</issue_to_address>

### Comment 9
<location> `src/apps/push.ts:87` </location>
<code_context>
+
+  for (const [groupKey, issues] of groupMap) {
+    const [groupId, botId] = groupKey.split('-')
+    let image: ImageElement[] = []
+
+    for (const issue of issues) {
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring client selection and rendering logic into helper functions and batch asynchronous operations to simplify control flow.

- Extract client‐selection into a helper  
- Pull “split & render” logic for commits/issues into small reusable functions  
- Batch your renders with `Promise.all` and avoid inline loops/awaits  

Example helpers you can introduce:

```ts
function getClientByPlatform(platform: Platform): ClientType {
  switch (platform) {
    case Platform.Gitee:
      if (isEmpty(Config.gitee.token)) throw new Error('请先配置Gitee Token')
      return Client.gitee()
    case Platform.GitCode:
      if (isEmpty(Config.gitcode.token)) throw new Error('请先配置GitCode Token')
      return Client.gitcode()
    case Platform.Cnb:
      if (isEmpty(Config.cnb.token)) throw new Error('请先配置Cnb Token')
      return Client.cnb()
    default:
      if (isEmpty(Config.github.token)) throw new Error('请先配置GitHub Token')
      return Client.github()
  }
}
```

```ts
async function renderCommitImage(
  item: { commitInfo: CommitInfo; pushRepoInfo: RepoInfo; pushRepo: PushRepo }
): Promise<ImageElement> {
  const parts = item.commitInfo.commit.message.split('\n')
  const data: PushCommitInfo = {
    ...item.commitInfo,
    owner: item.pushRepoInfo.owner,
    repo: item.pushRepoInfo.repo,
    branch: item.pushRepo.branch,
    botId: item.pushRepoInfo.botId,
    groupId: item.pushRepoInfo.groupId,
    title: await Render.markdown(parts[0]),
    body: await Render.markdown(parts.slice(1).join('\n')),
    commitDate: formatDate(item.commitInfo.commit.committer.date),
  }
  return Render.render('commit/index', { commit: data })
}

async function renderIssueImage(
  issue: IssueType,  // replace with your Issue interface
  repoInfo: RepoInfo
): Promise<ImageElement> {
  const data = {
    owner: repoInfo.owner,
    repo: repoInfo.repo,
    title: await Render.markdown(issue.title),
    body: issue.body ? await Render.markdown(issue.body) : null,
    user: issue.user,
    state: issue.state,
    issueDate: formatDate(issue.createdAt),
  }
  return Render.render('issue/index', { issue: data })
}
```

Then in your command you can write:

```ts
const client = getClientByPlatform(event.platform)
const [repos, issues] = await Promise.all([
  db.push.GetRepo(event.id),
  db.issue.GetRepo(event.id)
])

const [commitImages, issueImages] = await Promise.all([
  Promise.all(repos.map(r => renderCommitImage({ commitInfo: await client.getCommitInfo(...), pushRepoInfo: repoInfo, pushRepo: r }))),
  Promise.all(issues.map(i => renderIssueImage(await client.getIssueInfo(...), repoInfo)))
])

const images = [...commitImages, ...issueImages]
if (images.length) await sendImage(botId, groupId, images)
```

This removes nested loops, consolidates repeated DB and render calls, and keeps all original behavior.
</issue_to_address>

### Comment 10
<location> `src/models/db/event.ts:17` </location>
<code_context>
+  )
+}
+
+export async function GetAll(platform: Platform): Promise<Array<EventRepo>>
+
+export async function GetAll(
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the API to use a single event listing function with helper utilities for SQL and CSV handling to simplify the code.

Here’s one way to collapse all of those overloads and manual CSV handling into a single, very‐readable API—while keeping every codepath intact:

1) Introduce a single `listEvents(filter?)` instead of four overloads.  
2) Push SQL‐building into a tiny helper.  
3) Centralize CSV joins/splits into `encodeTypes()` / `decodeTypes()`.  

```ts
type EventFilter = {
  platform?: Platform
  eventType?: EventType
}

function encodeTypes(ev: EventType[]) {
  return ev.join(',')
}
function decodeTypes(csv: string): EventType[] {
  return csv.split(',') as EventType[]
}

async function listEvents(filter: EventFilter = {}): Promise<EventRepo[]> {
  const client = await createClient()
  const parts: string[] = ['SELECT * FROM event']
  const params: (Platform | EventType)[] = []

  if (filter.platform) {
    parts.push('platform = ?')
    params.push(filter.platform)
  }
  if (filter.eventType) {
    parts.push(filter.platform ? 'AND eventType = ?' : 'eventType = ?')
    params.push(filter.eventType)
  }

  const sql = parts.join(' ')
  const rows: any[] = await clientAll(client, sql, params)
  return rows.map(row => ({
    ...row,
    eventType: decodeTypes(row.eventType)
  }))
}

// helper to promisify client.all
function clientAll(client, sql: string, params: any[]) {
  return new Promise<any[]>((res, rej) =>
    client.all(sql, params, (e, r) => (e ? rej(e) : res(r)))
  )
}
```

You can then implement every other method in terms of that:

```ts
export const GetAll = listEvents

export async function GetAllByPlatform(p: Platform) {
  return listEvents({ platform: p })
}

export async function GetAllByType(e: EventType) {
  return listEvents({ eventType: e })
}

// get a single repo
export async function GetRepo(
  platform: Platform,
  repoId: number,
): Promise<EventRepo[]> {
  const rows = await listEvents({ platform })
  return rows.filter(r => r.repoId === repoId)
}

// Insert / Update / Delete can share encodeTypes() likewise
export async function AddRepo(
  platform: Platform,
  repoId: number,
  events: EventType[],
) {
  const client = await createClient()
  await clientRun(
    client,
    'INSERT INTO event (platform,repoId,eventType) VALUES (?,?,?)',
    [platform, repoId, encodeTypes(events)],
  )
}
```

— this removes *all* overloads, keeps CSV logic in two tiny helpers and centralizes SQL building.
</issue_to_address>

### Comment 11
<location> `src/models/db/issue.ts:7` </location>
<code_context>
+import { EventRepo, EventType, Platform } from '@/types'
+import { createClient } from './index'
+
+export async function AddRepo(
+  platform: Platform,
+  repoId: number,
</code_context>

<issue_to_address>
**issue (complexity):** Consider splitting overloaded and multi-purpose functions into single-purpose functions with clear names, and extract repeated promise-wrapping logic into helpers.

```suggestion
Rather than packing all behaviors into overloads of `AddRepo`, `GetRepo`, and `RemoveRepo`, split them into single‐purpose functions with clear names. Also extract the common promise‐wrapping of sqlite methods into helpers.

1. Create helpers once:
```ts
// db.ts
import { createClient } from './index';
import { promisify } from 'util';

export async function getDb() {
  const client = await createClient();
  return {
    run: promisify(client.run.bind(client)),
    get: promisify(client.get.bind(client)),
    all: promisify(client.all.bind(client)),
  };
}
```

2. Split AddRepo overloads to two functions:
```ts
// issue.ts
import { getDb } from './db';
import { StateType, IssueRepo } from '@/types';

export async function createIssueRepo(eventId: number): Promise<void> {
  const { run } = await getDb();
  await run('INSERT INTO issue(eventId) VALUES(?)', [eventId]);
}

export async function addIssue(
  eventId: number,
  issueId: string,
  title: string,
  body: string | null,
  state: StateType
): Promise<void> {
  const { run } = await getDb();
  await run(
    'INSERT INTO issue(eventId, issueId, title, body, state) VALUES(?,?,?,?,?)',
    [eventId, issueId, title, body, state]
  );
}
```

3. Split GetRepo into list/get:
```ts
export async function listIssues(eventId: number): Promise<IssueRepo[]> {
  const { all } = await getDb();
  return all('SELECT * FROM issue WHERE eventId = ?', [eventId]);
}

export async function getIssue(
  eventId: number,
  issueId: string
): Promise<IssueRepo | null> {
  const { get } = await getDb();
  return get(
    'SELECT * FROM issue WHERE eventId = ? AND issueId = ?',
    [eventId, issueId]
  );
}
```

4. Split RemoveRepo into deleteAll/deleteOne:
```ts
export async function deleteAllIssues(eventId: number): Promise<void> {
  const { run } = await getDb();
  await run('DELETE FROM issue WHERE eventId = ?', [eventId]);
}

export async function deleteIssue(
  eventId: number,
  issueId: string
): Promise<void> {
  const { run } = await getDb();
  await run(
    'DELETE FROM issue WHERE eventId = ? AND issueId = ?',
    [eventId, issueId]
  );
}
```

5. Keep UpdateState as is, or use `run` helper:
```ts
export async function updateIssueState(
  eventId: number,
  issueId: string,
  state: StateType
): Promise<void> {
  const { run } = await getDb();
  await run(
    'UPDATE issue SET state = ? WHERE eventId = ? AND issueId = ?',
    [state, eventId, issueId]
  );
}
```

These changes eliminate overloads/optional‐parameter branching and make each function single‐purpose while preserving all existing functionality.
</issue_to_address>

### Comment 12
<location> `src/apps/issue.ts:74` </location>
<code_context>
+  },
+)
+
+const handleRepoIssue = async (client: ClientType, platform: Platform) => {
+  const all = await db.event.GetAll(platform, EventType.Push)
+  const groupMap = new Map<
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring handleRepoIssue by splitting its logic into smaller helper functions for fetching, detecting, rendering, and sending issues.

```markdown
It looks like `handleRepoIssue` is doing 4 distinct jobs inline (fetch events & repos, detect new/changed issues, render images, send messages). Splitting those responsibilities into small helpers will make it far easier to test and maintain:

1) Extract “fetch & group issues”  
2) Extract “detect new/changed issues per event”  
3) Extract “render images”  
4) Extract “send images”

Here’s a sketch:

```ts
// services/issueLoader.ts
import { db } from '@/models'
import { ClientType, Platform } from '@/types'

export interface IssueData { /* title, body, user, state, date… */ }

export async function loadAllPushEvents(platform: Platform) {
  return db.event.GetAll(platform, EventType.Push)
}

export async function detectIssuesForEvent(
  event: Event,
  client: ClientType
): Promise<IssueData[]> {
  const repo = await db.repo.GetRepo(event.repoId)
  if (!repo) return []

  const rawList = await client.getIssueList(repo.owner, repo.repo, { perPage: 100 })
  const results: IssueData[] = []

  for (const raw of rawList) {
    const prev = await db.issue.GetRepo(event.id, raw.number)
    const hashBody = raw.body ? make_hash(raw.body) : null

    // first‐time or changed?
    if (!prev || prev.state !== raw.state || prev.title !== make_hash(raw.title) || prev.body !== hashBody) {
      if (!prev) {
        await db.issue.AddRepo(event.id, raw.number, make_hash(raw.title), hashBody, raw.state)
      } else {
        await db.issue.UpdateState(event.id, raw.number, raw.state)
      }

      results.push({
        owner: repo.owner,
        repo: repo.repo,
        title: await Render.markdown(raw.title),
        body: raw.body ? await Render.markdown(raw.body) : null,
        user: raw.user,
        state: raw.state,
        issueDate: formatDate(raw.createdAt),
      })
    }
  }
  return results
}
```

```ts
// services/issueNotifier.ts
import { ImageElement, getBot, common, contactGroup } from 'node-karin'

export async function renderImages(issues: IssueData[]): Promise<ImageElement[]> {
  return Promise.all(
    issues.map(issue => Render.render('issue/index', { issue }))
  )
}

export async function sendImages(
  botId: string,
  groupId: string,
  images: ImageElement[]
) {
  const bot = getBot(botId)
  const contact = await contactGroup(groupId)
  if (images.length > 1) {
    const forward = await common.makeForward(images, botId, bot?.account.name)
    await bot?.sendForwardMsg(contact, forward, { /* ... */ })
  } else {
    await bot?.sendMsg(contact, images[0])
  }
}
```

```ts
// tasks/issueTask.ts
import { loadAllPushEvents, detectIssuesForEvent } from './services/issueLoader'
import { renderImages, sendImages }       from './services/issueNotifier'

async function handleRepoIssue(client: ClientType, platform: Platform) {
  const events = await loadAllPushEvents(platform)
  const groupMap = new Map<string, IssueData[]>()

  for (const ev of events) {
    const changes = await detectIssuesForEvent(ev, client)
    const key = `${ev.groupId}-${ev.botId}`
    if (changes.length) {
      groupMap.set(key, (groupMap.get(key) || []).concat(changes))
    }
  }

  for (const [key, issues] of groupMap) {
    const [groupId, botId] = key.split('-')
    const imgs = await renderImages(issues)
    await sendImages(botId, groupId, imgs)
  }
}
```

This preserves all functionality but:

- each function is < 40 lines  
- you can write unit‐tests for each piece  
- `handleRepoIssue` reads top‐to‐bottom like a clear flow.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

const botId = e.selfId
const groupId = e.groupId
const PushInfos = await db.push.GetAll()
const all = await db.event.GetAll()
Copy link

Choose a reason for hiding this comment

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

suggestion (performance): No filtering by platform or event type in push command.

Querying all events without filters can lead to processing irrelevant data. Please add platform and event type filters to optimize efficiency.

Suggested implementation:

      // Filter events by platform and event type for efficiency
      const platform = Platform.Cnb // or use the relevant platform variable
      const eventType = 'push' // or use the relevant event type variable
      const all = await db.event.GetAll({ platform, eventType })

Make sure that db.event.GetAll supports filtering by platform and event type via its parameters. If it does not, you will need to update its implementation to accept and use these filters.

Comment on lines +111 to +120
for (const pushInfo of pushRepoList) {
let commitInfo: CommitInfo
try {
commitInfo = await client.getCommitInfo(
RepoInfo.owner,
RepoInfo.repo,
pushInfo.branch,
)
} catch (error) {
logger.warn(
Copy link

Choose a reason for hiding this comment

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

suggestion: No error handling for Render.render failures.

Add error handling to catch exceptions or validate the output of Render.render before adding images to the array.

const img = await Render.render('commit/index', {
commit: pushCommitInfo,
})
image.push(img)
Copy link

Choose a reason for hiding this comment

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

suggestion (performance): Potential for unnecessary API calls for empty issues.

Filter out empty issues before making API requests to avoid unnecessary calls and reduce system load.

Comment on lines 29 to +33
export async function GetRepo(
platform: Platform,
repoId: number,
eventId: number,
branch: string,
): Promise<PushRepo | null> {
): Promise<PushRepo | null>

export async function GetRepo(eventId: number): Promise<Array<PushRepo>>

export async function GetRepo(
eventId: number,
branch?: string,
Copy link

Choose a reason for hiding this comment

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

suggestion: Overloaded GetRepo signature may cause confusion.

Overloading GetRepo with different return types based on arguments can introduce ambiguity and runtime errors. Recommend using distinct function names or ensuring a consistent return type to improve clarity and safety.

- 合并重复的数据库查询代码
- 使用动态查询构建减少条件分支
- 支持同时按平台和事件类型过滤
- 统一结果处理逻辑
@candriajs
Copy link
Contributor

candriajs bot commented Nov 12, 2025

🎉 🎉 🎉 构建完成!
该版本是基于 issue 分支的快照 bb5272e 所构建的。

你可以通过以下命令安装该版本:

pnpm add https://pkg.pr.new/CandriaJS/karin-plugin-git/karin-plugin-git@bb5272e -w

@wuliya336 wuliya336 merged commit 83718de into main Nov 12, 2025
5 checks passed
@wuliya336 wuliya336 deleted the issue branch November 12, 2025 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 议题打开/关闭事件监听

2 participants