Skip to content

Conversation

@sj817
Copy link
Member

@sj817 sj817 commented Jul 20, 2025

Sourcery 总结

全面改进了核心插件框架,通过引入集中式插件管理器和注册模块,将插件 API(命令、接受、按钮、任务、处理程序和类样式)统一为带有控制接口的详细缓存对象,并将常见的事件处理、过滤、冷却、权限和分发逻辑提取到可重用工具中。添加了热重载支持和用于动态文件 globbing 的新 @karin/glob 包。更新了服务器插件端点,整合了遗留代码,现代化了 Vite 构建配置,并提升了项目依赖。

新特性:

  • 添加了 @karin/glob 包,包含 importGlobimportGlobEager,用于动态文件加载和原始内容支持
  • 为基于命令、接受、按钮、任务、处理程序和类的插件实现了集中式插件管理器和注册模块
  • 通过 core/karin/class.ts 引入了基于类的插件支持
  • event/utils 下添加了统一的事件工具(filtercooldowncheckerpermissioninitializerdispatchlogger
  • 添加了热重载函数 reloadAppreloadPackage,用于实时插件更新

改进:

  • 重构了所有消息、通知和请求处理程序,以使用共享的 handleMessageCommondispatch 工具
  • 重新设计了核心 Karin API,以返回带有流畅控制接口的丰富插件缓存对象
  • 更新了服务器插件端点和类型,以与新的插件缓存和管理器集成
  • 通过将 manualChunks 迁移到 advancedChunks 并调整压缩和命名设置来简化 Vite 构建配置
  • 删除了遗留的内联处理程序逻辑,并整合到新的工具驱动结构中

构建:

  • 更新了 devDependencies 版本并添加了缺少的依赖项,如 glob

文档:

  • @karin/glob 包添加了 README.md 文档
Original summary in English

Summary by Sourcery

Overhaul the core plugin framework by introducing a centralized plugin manager and register modules, unifying plugin APIs (command, accept, button, task, handler, and class styles) into detailed cache objects with control interfaces, and extracting common event handling, filtering, cooldown, permission, and dispatch logic into reusable utilities. Add hot-reload support and a new @karin/glob package for dynamic file globbing. Update server plugin endpoints, consolidate legacy code, modernize Vite build configuration, and bump project dependencies.

New Features:

  • Add @karin/glob package with importGlob and importGlobEager for dynamic file loading and raw content support
  • Implement centralized plugin manager and register modules for command, accept, button, task, handler, and class-based plugins
  • Introduce class-based plugin support via core/karin/class.ts
  • Add unified event utilities (filter, cooldown, checker, permission, initializer, dispatch, and logger) under event/utils
  • Add hot-reload functions reloadApp and reloadPackage for live plugin updates

Enhancements:

  • Refactor all message, notice, and request handlers to use shared handleMessageCommon and dispatch utilities
  • Redesign core Karin APIs to return rich plugin cache objects with fluent control interfaces
  • Update server plugin endpoints and types to integrate with the new plugin cache and manager
  • Simplify Vite build configuration by migrating manualChunks to advancedChunks and adjusting compression and naming settings
  • Remove legacy inline handler logic and consolidate into the new utility-driven structure

Build:

  • Update devDependencies versions and add missing dependencies like glob

Documentation:

  • Add README.md documentation for @karin/glob package

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jul 20, 2025

审阅者指南

此 PR 通过将事件初始化、过滤、冷却、分派和权限逻辑集中到专用工具中,彻底改革了插件和事件架构。它用共享的 handleMessageCommon 流程取代了临时处理程序实现,并围绕新的管理器/注册 API 和统一的 core/karin 入口点(用于命令、接受、按钮、任务和处理程序)重塑了插件系统。

统一消息事件处理的时序图 (handleMessageCommon)

sequenceDiagram
  participant EventSource as Event Source
  participant Handler as groupHandler/groupTempHandler/guildHandler/friendHandler/directHandler
  participant Utils as utils
  participant handleMessageCommon as handleMessageCommon
  participant Dispatch as utils.dispatch

  EventSource->>Handler: Receives message event
  Handler->>Utils: Get config, eventConfig, isPrint
  Handler->>handleMessageCommon: Call with ctx, configs, hook, dispatch, filterCallback
  handleMessageCommon->>Utils: Run hook (e.g. hooksMessageEmit.group)
  handleMessageCommon->>Utils: Compute cooldown (cd)
  handleMessageCommon->>Utils: Filter event (groupEvent/privateEvent)
  handleMessageCommon->>Dispatch: If filter passes, dispatch message
  Dispatch->>Plugins: Call matching plugins
Loading

新插件注册 API 的类图 (karin 核心入口点)

classDiagram
  class karin {
    +accept()
    +command()
    +button()
    +handler()
    +task()
    +context()
    +render()
    +sendMaster()
    +sendAdmin()
    +sendMsg()
  }
  karin : Emitter
Loading

OptionsBase 和插件选项继承的类图

classDiagram
  class OptionsBase {
    +name?: string
    +log?: boolean
    +perm?: Permission
    +rank?: number
    +adapter?: AdapterProtocol[]
    +dsbAdapter?: AdapterProtocol[]
    +permission?: Permission
    +priority?: number
    +notAdapter?: AdapterProtocol[]
  }
  class CommandOptions {
    +event?: string
    +authFailMsg?: boolean | string
    ...
  }
  class ButtonOptions {
    ...
  }
  class TaskOptions {
    ...
  }
  class AcceptOptions {
    +event?: string
    ...
  }
  class HandlerOptions {
    ...
  }
  OptionsBase <|-- CommandOptions
  OptionsBase <|-- ButtonOptions
  OptionsBase <|-- TaskOptions
  OptionsBase <|-- AcceptOptions
  OptionsBase <|-- HandlerOptions
Loading

Emitter 和事件暴露的类图

classDiagram
  class Emitter {
    +name: 'karin'
    +emit()
    +on()
    ...
  }
  class events {
    +events(emitter: Emitter)
  }
  Emitter <.. events: used by
Loading

文件级别更改

更改 详情 文件
集中消息事件处理
  • 替换了 group, groupTemp, guild, friend, direct 处理程序中内联的 initMsg/initRole/initAlias/emit 逻辑
  • 引入了 handleMessageCommon 以统一 hook 执行和分派
  • 移除了旧的 context, filter, cd, 以及 groupsDeal/privateDeal 代码块
packages/core/src/event/handler/message/groups.ts
packages/core/src/event/handler/message/private.ts
packages/core/src/event/handler/message/common.ts
模块化事件工具
  • 将 initializer, filter, cooldown, checker, permission, logger 和 dispatch 提取到 event/utils 中
  • 将各种内联辅助函数调用替换为 eventUtils API
packages/core/src/event/utils/initializer.ts
packages/core/src/event/utils/filter.ts
packages/core/src/event/utils/cd.ts
packages/core/src/event/utils/checker.ts
packages/core/src/event/utils/logger.ts
packages/core/src/event/utils/dispatch/message.ts
packages/core/src/event/utils/dispatch/notice_request.ts
重构 core/karin 下的插件定义 API
  • 重新设计了 command, accept, button, task, handler 导出,以使用 formatOptions, createID, createLogger, manager 和 register 模块
  • 引入了用于动态更新/注销的控制接口
  • 废弃了旧的 plugin/tools 方法
packages/core/src/core/karin/command.ts
packages/core/src/core/karin/accept.ts
packages/core/src/core/karin/button.ts
packages/core/src/core/karin/task.ts
packages/core/src/core/karin/handler.ts
packages/core/src/core/karin/export.ts
packages/core/src/core/karin/context.ts
packages/core/src/core/karin/util.ts
packages/core/src/core/karin/options.ts
实现统一的插件管理器和注册模块
  • 创建了 manager/cache, getPlugins, getPluginDetails, isPluginPackage, getAppsDir, getMain 等
  • 构建了 register.ts,包含每种插件类型的注册/注销功能
  • 在 plugins 下添加了加载和初始化流程
packages/core/src/plugins/manager.ts
packages/core/src/plugins/register.ts
packages/core/src/plugins/load.ts
packages/core/src/plugins/init.ts
调整构建配置
  • 在 Vite 配置中用 advancedChunks 替换了 manualChunks 策略
  • 增加了压缩阈值
  • 调整了 chunkSizeWarningLimit
packages/web/vite.config.ts
添加 @karin/glob 包
  • 引入了带有 importGlob 和 importGlobEager API 的增强型 glob
  • 与 tsup 和 tsdown 配置捆绑
packages/glob/src/index.ts
packages/glob/package.json
packages/glob/tsup.config.ts
packages/glob/tsdown.config.ts
packages/glob/tsconfig.json

可能关联的问题

  • 加油 #1: 此 PR 重构了插件系统,与插件开发、管理和市场重构的问题任务保持一致。
  • 加油 #1: 此 PR 直接实现了问题中详述的广泛重构任务,包括 API、事件、模块和类方法的重组。

提示和命令

与 Sourcery 交互

  • 触发新审阅: 在拉取请求上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审阅评论。
  • 从审阅评论生成 GitHub issue: 通过回复审阅评论,要求 Sourcery 从中创建一个 issue。您也可以回复审阅评论并带上 @sourcery-ai issue 来从中创建一个 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 overhauls the plugin and event architecture by centralizing event initialization, filtering, cooldown, dispatch, and permission logic into dedicated utilities, replacing ad-hoc handler implementations with a shared handleMessageCommon flow, and reshaping the plugin system around a new manager/register API and unified core/karin entrypoints for commands, accepts, buttons, tasks, and handlers.

Sequence diagram for unified message event handling (handleMessageCommon)

sequenceDiagram
  participant EventSource as Event Source
  participant Handler as groupHandler/groupTempHandler/guildHandler/friendHandler/directHandler
  participant Utils as utils
  participant handleMessageCommon as handleMessageCommon
  participant Dispatch as utils.dispatch

  EventSource->>Handler: Receives message event
  Handler->>Utils: Get config, eventConfig, isPrint
  Handler->>handleMessageCommon: Call with ctx, configs, hook, dispatch, filterCallback
  handleMessageCommon->>Utils: Run hook (e.g. hooksMessageEmit.group)
  handleMessageCommon->>Utils: Compute cooldown (cd)
  handleMessageCommon->>Utils: Filter event (groupEvent/privateEvent)
  handleMessageCommon->>Dispatch: If filter passes, dispatch message
  Dispatch->>Plugins: Call matching plugins
Loading

Class diagram for new plugin registration API (karin core entrypoints)

classDiagram
  class karin {
    +accept()
    +command()
    +button()
    +handler()
    +task()
    +context()
    +render()
    +sendMaster()
    +sendAdmin()
    +sendMsg()
  }
  karin : Emitter
Loading

Class diagram for OptionsBase and plugin options inheritance

classDiagram
  class OptionsBase {
    +name?: string
    +log?: boolean
    +perm?: Permission
    +rank?: number
    +adapter?: AdapterProtocol[]
    +dsbAdapter?: AdapterProtocol[]
    +permission?: Permission
    +priority?: number
    +notAdapter?: AdapterProtocol[]
  }
  class CommandOptions {
    +event?: string
    +authFailMsg?: boolean | string
    ...
  }
  class ButtonOptions {
    ...
  }
  class TaskOptions {
    ...
  }
  class AcceptOptions {
    +event?: string
    ...
  }
  class HandlerOptions {
    ...
  }
  OptionsBase <|-- CommandOptions
  OptionsBase <|-- ButtonOptions
  OptionsBase <|-- TaskOptions
  OptionsBase <|-- AcceptOptions
  OptionsBase <|-- HandlerOptions
Loading

Class diagram for Emitter and event exposure

classDiagram
  class Emitter {
    +name: 'karin'
    +emit()
    +on()
    ...
  }
  class events {
    +events(emitter: Emitter)
  }
  Emitter <.. events: used by
Loading

File-Level Changes

Change Details Files
Centralize message event handling
  • Replaced inline initMsg/initRole/initAlias/emit logic in group, groupTemp, guild, friend, direct handlers
  • Introduced handleMessageCommon to unify hook execution and dispatch
  • Removed old context, filter, cd, and groupsDeal/privateDeal code blocks
packages/core/src/event/handler/message/groups.ts
packages/core/src/event/handler/message/private.ts
packages/core/src/event/handler/message/common.ts
Modularize event utilities
  • Extracted initializer, filter, cooldown, checker, permission, logger, and dispatch into event/utils
  • Swapped assorted inline helper calls for eventUtils APIs
packages/core/src/event/utils/initializer.ts
packages/core/src/event/utils/filter.ts
packages/core/src/event/utils/cd.ts
packages/core/src/event/utils/checker.ts
packages/core/src/event/utils/logger.ts
packages/core/src/event/utils/dispatch/message.ts
packages/core/src/event/utils/dispatch/notice_request.ts
Rework plugin definition APIs under core/karin
  • Redesigned command, accept, button, task, handler exports to use formatOptions, createID, createLogger, manager and register modules
  • Introduced control interfaces for dynamic updates/unregistration
  • Deprecated old plugin/tools approach
packages/core/src/core/karin/command.ts
packages/core/src/core/karin/accept.ts
packages/core/src/core/karin/button.ts
packages/core/src/core/karin/task.ts
packages/core/src/core/karin/handler.ts
packages/core/src/core/karin/export.ts
packages/core/src/core/karin/context.ts
packages/core/src/core/karin/util.ts
packages/core/src/core/karin/options.ts
Implement unified plugin manager and register modules
  • Created manager/cache, getPlugins, getPluginDetails, isPluginPackage, getAppsDir, getMain etc.
  • Built register.ts with register/unregister for each plugin type
  • Added load and init flow under plugins
packages/core/src/plugins/manager.ts
packages/core/src/plugins/register.ts
packages/core/src/plugins/load.ts
packages/core/src/plugins/init.ts
Adjust build configuration
  • Replaced manualChunks strategy with advancedChunks in Vite config
  • Increased compression threshold
  • Tweaked chunkSizeWarningLimit
packages/web/vite.config.ts
Add @karin/glob package
  • Introduced enhanced glob with importGlob and importGlobEager APIs
  • Bundled with tsup and tsdown configs
packages/glob/src/index.ts
packages/glob/package.json
packages/glob/tsup.config.ts
packages/glob/tsdown.config.ts
packages/glob/tsconfig.json

Possibly linked issues

  • 加油 #1: The PR refactors the plugin system, aligning with issue tasks for plugin development, management, and market refactor.
  • 加油 #1: The PR directly implements the extensive refactoring tasks detailed in the issue, including API, event, module, and class method reorganizations.

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
Contributor

@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 @sj817 - 我已审阅了你的更改 - 以下是一些反馈:

  • 考虑将这个庞大的重构拆分成更小、更集中的 PR(例如处理程序整合、插件管理器设置、工具提取),以简化审阅并降低回归风险。
  • 在 vite.config.ts 中,注释仍然是“只有大于1kb的文件才会被压缩”,但现在的阈值是 10240 (10 KB) — 请相应地更新注释。
  • 每个消息处理程序中的内联 filterCallback 定义共享几乎相同的权限逻辑——将它们提取到共享辅助函数中可以减少样板代码并提高可维护性。
给 AI 代理的提示
请处理此代码审查中的评论:
## 总体评论
- 考虑将这个庞大的重构拆分成更小、更集中的 PR(例如处理程序整合、插件管理器设置、工具提取),以简化审阅并降低回归风险。
- 在 vite.config.ts 中,注释仍然是“只有大于1kb的文件才会被压缩”,但现在的阈值是 10240 (10 KB) — 请相应地更新注释。
- 每个消息处理程序中的内联 `filterCallback` 定义共享几乎相同的权限逻辑——将它们提取到共享辅助函数中可以减少样板代码并提高可维护性。

## 个人评论

### 评论 1
<location> `packages/core/src/core/karin/command.ts:209` </location>
<code_context>
+  let regCache = formatReg(reg)
+  let optCache = Object.freeze(formatOptions(options))
+  let logCache = Object.freeze(createLogger(options.log, true))
+  let fncCache = Object.freeze(formatFnc<T>(second, options) as MessageCallback<keyof MessageEventMap>)
+
+  const result: CommandCache = {
</code_context>

<issue_to_address>
冻结函数没有必要,并且可能引起误解。

`Object.freeze` 不会给函数增加有意义的不变性,并可能导致混淆。建议从 `fncCache` 等函数对象中移除它。
</issue_to_address>

### 评论 2
<location> `packages/core/src/core/karin/base.ts:78` </location>
<code_context>
+ * @param absPath 调用者
+ * @returns 返回插件文件属性
+ */
+export const createPluginFileProperties = (absPath: string): PluginCacheKeyFile => {
+  return {
+    absPath: formatPath(absPath),
</code_context>

<issue_to_address>
`formatPath` 应用于 `path.basename`,这可能没有必要。

由于 `basename` 只返回文件名,因此使用 `formatPath` 规范化斜杠可能是多余的。请确认这是否是必需的。
</issue_to_address>

### 评论 3
<location> `packages/core/src/types/server/plugins.ts:91` </location>
<code_context>
   /** 插件名称 `文件夹根目录名称` */
   name: string
   /** 插件类型 */
-  type: KarinPluginAppsType
+  type: Omit<PluginPackageType, 'apps'> | 'app'
   /** 插件版本 App类型为空 */
   version: string
</code_context>

<issue_to_address>
类型字段现在使用 'app' 而不是 'apps',这可能会导致混淆。

与代码库其余部分的不一致可能会导致混淆或匹配错误。请标准化 `type` 字段使用的值。
</issue_to_address>

### 评论 4
<location> `packages/core/src/event/utils/permission.ts:56` </location>
<code_context>
+    perm: CommandCache['register']['options']['permission'],
+    authFailMsg: CommandCache['register']['options']['authFailMsg']
+  ): boolean => {
+    if (!perm || perm === 'all') {
+      return true
+    }
</code_context>

<issue_to_address>
默认权限回退可能允许意外访问。

如果缺少权限或设置为 'all',则所有用户都将获得访问权限。如果插件作者省略了权限,这可能会导致意外暴露。考虑使用更严格的默认值。
</issue_to_address>

### 评论 5
<location> `packages/core/src/event/utils/permission.ts:99` </location>
<code_context>
-
-  /** 计算插件处理时间 */
-  const start = Date.now()
-  if (ctx.isGroup) {
-    const result = await eventCallEmit.group(ctx, plugin)
-    if (!result) return false
</code_context>

<issue_to_address>
群组和公会权限检查重复。

将共享权限逻辑重构为公共函数,以简化维护并减少代码重复。
</issue_to_address>

### 评论 6
<location> `packages/core/src/core/karin/command.ts:196` </location>
<code_context>
  * @returns 返回插件对象
  */
-export const command: CommandType = <T extends keyof MessageEventMap = keyof MessageEventMap> (
+export const command: Callbacks = <T extends keyof MessageEventMap = keyof MessageEventMap> (
   reg: string | RegExp,
-  second: string | Elements | Elements[] | CmdFnc<T>,
</code_context>

<issue_to_address>
考虑将重复的插件缓存连接逻辑提取到一个可重用的辅助函数中,以简化和缩小 `command` 的实现。

以下是一种大幅减少 `command` 中样板代码的方法:将其共同的“插件缓存”连接逻辑提取到一个小型的辅助函数中。所有嵌套的 getter、冻结以及注册/注销调用都移到该辅助函数中,`command` 只需传递其特定部分(`reg``fnc``options`,加上您的注册/注销函数):

```ts
// util/buildCache.ts
type GetterMap<C,R> = {
  type: string
  getPkg: (ctx: C) => any
  getFile: (ctx: C) => any
  getApp:  (ctx: C) => Record<'id'|'log'|'name', any>
  getReg:  (ctx: C) => R['register']
  getCtl:  (ctx: C, res: R) => R['control']
  onInit?: (ctx: C, res: R) => void
}
export function buildCache<C, R extends {register:any, control:any}>(ctx: C, map: GetterMap<C,R>): R {
  const res = {} as R
  Object.defineProperties(res, {
    type: { get: () => map.type },
    pkg:  { get: () => map.getPkg(ctx) },
    file: { get: () => map.getFile(ctx) },
    app:  { get: () => map.getApp(ctx) },
    register: { get: () => map.getReg(ctx) },
    control:  { get: () => map.getCtl(ctx, res) },
  })
  map.onInit?.(ctx, res)
  return res
}
```

然后你的 `command` 会缩短为:

```ts
import { buildCache } from './util/buildCache'
export const command: Callbacks = <T extends keyof MessageEventMap = keyof MessageEventMap> (
  reg: string|RegExp,
  second: MessageCallback<T>|MessageSegment,
  options: Options<T>|StringOptions<T> = {}
): CommandCache => {
  // --- your existing setup ---
  const caller = system.getCaller(import.meta.url)!
  const pkgName = manager.getPackageName(caller)!
  const id = createID()
  let regCache = formatReg(reg)
  let optCache = Object.freeze(formatOptions(options))
  let fncCache = Object.freeze(formatFnc(second, options) as MessageCallback<any>)
  let logCache = Object.freeze(createLogger(options.log, true))

  return buildCache(
    { caller, pkgName, id, regCache, optCache, fncCache, logCache },
    {
      type: 'command',
      getPkg:    ctx => manager.getPluginPackageDetail(ctx.pkgName),
      getFile:   ctx => manager.getFileCache(ctx.caller),
      getApp:    ctx => ({ id: ctx.id, log: ctx.logCache, name: ctx.optCache.name }),
      getReg:    ctx => ({ reg: ctx.regCache, fnc: ctx.fncCache, options: ctx.optCache }),
      getCtl:    (ctx, res) => ({
        setReg:    r => ctx.regCache = formatReg(r),
        setFnc:    f => ctx.fncCache = Object.freeze(f),
        setOptions(o) {
          ctx.optCache = Object.freeze(formatOptions(o))
          ctx.logCache = Object.freeze(createLogger(o.log, true))
          ctx.fncCache = Object.freeze(formatFnc(second, o) as any)
        },
        remove:    () => register.unregisterCommand(res.app.id),
      }),
      onInit:    (ctx, res) => register.registerCommand(res),
    }
  )
}
```

好处:

- 所有嵌套的 `get pkg/file/app/register/control` 逻辑都只在 `buildCache` 中存在一次。
- `command` 只需指定如何派生这五个部分以及您的初始化钩子。
- 将来的插件类型(例如事件、钩子、类)可以重用相同的辅助函数及其自己的映射。
</issue_to_address>

### 评论 7
<location> `packages/core/src/core/karin/button.ts:80` </location>
<code_context>
  * @param fnc - 函数
+ * @param options - 选项
  */
 export const button = (
   reg: RegExp | string,
-  fnc: Button['fnc'],
</code_context>

<issue_to_address>
考虑将通用的插件构建逻辑提取到一个可重用的工厂函数中,以简化和统一插件创建。

```suggestion
// plugins/base.ts
import { system, types } from '@/utils'
import * as manager from './manager'
import * as registerFns from './register'
import { createID, createLogger } from './util'

export interface PluginBuilderConfig<TOpts, TCache> {
  type: string
  formatOptions: (opts: TOpts) => any
  formatReg?: (reg: string | RegExp) => RegExp
  register: (cache: TCache) => void
  unregister: (id: string) => void
}

export function makePlugin<TOpts, TCache extends { control: { remove(): void }, register: any }>(
  cfg: PluginBuilderConfig<TOpts, TCache>
) {
  return (reg: string | RegExp, fnc: any, options: TOpts): TCache => {
    if (!reg) throw new Error(`[${cfg.type}]: missing reg`)
    if (!fnc) throw new Error(`[${cfg.type}]: missing fnc`)

    const caller = system.getCaller(import.meta.url)!
    const pkgName = manager.getPackageName(caller)!
    const id = createID()

    let regCache = cfg.formatReg ? cfg.formatReg(reg) : (reg instanceof RegExp ? reg : new RegExp(reg))
    let fncCache = fnc
    let optCache = Object.freeze(cfg.formatOptions(options))
    const logCache = Object.freeze(createLogger((options as any).log, true))

    const base: any = {
      get type() { return cfg.type },
      get pkg() {
        if (!pkgName) throw new Error(`use only in standard modules: ${caller}`)
        return manager.getPluginPackageDetail(pkgName)!
      },
      get file() { return manager.getFileCache(caller)! },
      get app() {
        return {
          get id() { return id },
          get type() { return cfg.type },
          get log() { return logCache },
          get name() { return optCache.name }
        }
      },
      get register() {
        return {
          get reg() { return regCache },
          get fnc() { return fncCache },
          get options() { return optCache },
        }
      },
      get control() {
        return {
          setReg(r: string | RegExp) { regCache = cfg.formatReg ? cfg.formatReg(r) : (r instanceof RegExp ? r : new RegExp(r)) },
          setFnc(f: any) { fncCache = f },
          setOptions(o: TOpts) { optCache = cfg.formatOptions(o) },
          remove() { cfg.unregister(id) },
        }
      }
    }

    const cache = base as TCache
    cfg.register(cache)
    return cache
  }
}

// plugins/button.ts
import { makePlugin } from './base'
import type { ButtonOptions, ButtonCache } from './types'
import { formatOptions, formatReg } from './formatters'
import { registerButton, unregisterButton } from './register'

export const button = makePlugin<ButtonOptions, ButtonCache>({
  type: 'button',
  formatOptions,
  formatReg,
  register: registerButton,
  unregister: unregisterButton
})
```

应用步骤:
1. 将通用的构建器逻辑提取到 `makePlugin` 中(如 `plugins/base.ts` 所示)。
2. 为每个插件定义 `formatOptions` 和可选的 `formatReg`/`formatters`3. 通过调用 `makePlugin` 并传入 `registerButton`/`unregisterButton` 来连接 `button`4. 删除冗长的 `button` 实现并替换为上述代码。

这将把重复的 getter 和控制方法折叠到一个工厂中,在不损失功能的情况下减少了维护面。
</issue_to_address>

### 评论 8
<location> `packages/core/src/core/karin/accept.ts:85` </location>
<code_context>
+  let optCache = Object.freeze(formatOptions(options))
+  const logCache = Object.freeze(createLogger(options.log, true))
+
+  const cache: AcceptCache = {
+    get type (): typeof type {
+      return type
</code_context>

<issue_to_address>
考虑将重复的插件缓存、getter 和控制逻辑提取到一个共享工厂函数中,以减少跨插件的样板代码。

以下是一种将几乎所有重复的“缓存 + getter + 控制 + 注册”样板代码简化为一个共享工厂的方法。外部功能保持 100% 不变,但每个插件只需调用此辅助函数:

```ts
// plugins/registerUtil.ts
import { createID, createLogger } from './util'
import * as manager from './manager'

export type PluginConfig<
  Opts,
  RegInfo,
  Cache extends { register: RegInfo; control: any }
> = {
  type: string
  formatOptions: (opts: Opts) => any
  buildRegisterInfo: (
    opts: ReturnType<PluginConfig<Opts, RegInfo, Cache>['formatOptions']>,
    fnc: (...args: any[]) => any
  ) => RegInfo
  registerFn: (cache: Cache) => void
}

export function createPlugin<Opts, RegInfo, Cache extends { register: RegInfo }>(
  cfg: PluginConfig<Opts, RegInfo, Cache>
) {
  return <T extends any>(
    eventOrFnc: T,
    maybeFnc?: (...args: any[]) => any,
    opts: Opts = {} as Opts
  ): Cache => {
    const caller = manager.getCaller(import.meta.url)
    const pkgName = manager.getPackageName(caller)
    const id = createID()
    const type = cfg.type
    // 1) format, 2) build register, 3) build the shared cache shape
    const optCache = Object.freeze(cfg.formatOptions(opts))
    const regInfo = Object.freeze(
      cfg.buildRegisterInfo(optCache, maybeFnc as any)
    )
    const cache = {
      get type() { return type },
      get pkg() {
        if (!pkgName) throw new Error(`请在符合标准规范的文件中使用此方法: ${caller}`)
        return manager.getPluginPackageDetail(pkgName)!
      },
      get file() { return manager.getFileCache(caller) },
      get app() {
        return {
          id,
          type,
          log: createLogger(optCache.log, true),
          name: optCache.name
        }
      },
      get register() { return regInfo },
      get control() {
        return {
          setEvent(e: any) { regInfo.event = e },
          setFnc(f: any) { regInfo.fnc = f },
          setOptions(o: any) { /* re-format and reassign */ },
          remove() { cfg.registerFn(cache as Cache) }
        }
      }
    } as unknown as Cache

    cfg.registerFn(cache)
    return cache
  }
}
```

然后你的 `accept.ts` 变为:

```ts
import { types } from '@/utils'
import { createPlugin } from '../plugins/registerUtil'
import * as register from '../plugins/register'
import type { NoticeAndRequest } from '@/plugins/types'
import type { FNC } from './util'
import type { AcceptOptions } from './options'

const formatOptions = (o: AcceptOptions) => ({
  event: types.string(o.event, 'notice'),
  adapter: types.array(o.adapter, []),
  dsbAdapter: types.array(o.dsbAdapter, types.array(o.notAdapter, [])),
  priority: types.number(o.rank, types.number(o.priority, 10000)),
  name: types.string(o.name, 'accept'),
  log: types.bool(o.log, true),
})

export interface AcceptRegInfo {
  event: keyof NoticeAndRequest
  fnc: FNC<NoticeAndRequest[keyof NoticeAndRequest]>
  options: ReturnType<typeof formatOptions>
}

export const accept = createPlugin<
  AcceptOptions,
  AcceptRegInfo,
  { register: AcceptRegInfo; control: any }
>({
  type: 'accept',
  formatOptions,
  buildRegisterInfo: (opts, fnc) => ({
    event: opts.event,
    fnc,
    options: opts,
  }),
  registerFn: register.registerAccept,
})
```

这样:

- 每个插件(accept/notice/request/…)都调用*相同的* `createPlugin` 辅助函数
- 所有 getter、ID/Logger 连接、`control.*``registerFn` 调用都集中在一个地方
- 每个插件文件大约 20 行代码,而不是 150 行
- 功能不变且完全类型安全
</issue_to_address>

### 评论 9
<location> `packages/core/src/core/karin/handler.ts:81` </location>
<code_context>
+  let optCache = Object.freeze(formatOptions(options))
+  const logCache = Object.freeze(createLogger(options.log, true))
+
+  const cache: HandlerCache = {
+    get type (): typeof type {
+      return type
</code_context>

<issue_to_address>
考虑将重复的插件缓存逻辑提取到一个可重用的工厂函数中,以简化处理程序实现。

考虑将共享的“插件缓存”样板代码提取到一个小型工厂中。你将得到一个大约 20 行代码的实用工具,而你的 `handler()` 可以缩减到几行:

```ts
// pluginFactory.ts
import { system, types } from '@/utils'
import * as manager from '../../plugins/manager'

export interface PluginFactoryParams<R, O, C> {
  type: string
  callerUrl: string
  options: O
  formatOptions(o: O): C['register']['options']
  registerPayload: R
  registerFn(cache: C): void
  unregisterFn(id: string): void
  createID(): string
  createLogger?(logOpt: boolean): ReturnType<typeof console.log>
}

export function createPluginCache<R, O, C extends { register: { options: any } }>(
  params: PluginFactoryParams<R, O, C>
): C {
  const {
    type,
    callerUrl,
    options,
    formatOptions,
    registerPayload,
    createID,
    registerFn,
    unregisterFn,
    unregisterFn: remove,
    createLogger,
  } = params

  const caller = system.getCaller(callerUrl)!
  const pkgName = manager.getPackageName(caller)!
  const id = createID()
  let reg = { ...registerPayload }
  let opt = Object.freeze(formatOptions(options))
  const logger = createLogger ? Object.freeze(createLogger(opt.log)) : null

  const cache = {
    get type () { return type },
    get pkg () { return manager.getPluginPackageDetail(pkgName) },
    get file() { return manager.getFileCache(caller) },
    get app() {
      return { id, type, log: logger, name: opt.name }
    },
    get register() {
      return { ...reg, options: opt }
    },
    get control() {
      return {
        setKey(k: any) { reg = { ...reg, key: k } },
        setFnc(f: any) { reg = { ...reg, fnc: f } },
        setOptions(o: O) { opt = Object.freeze(formatOptions(o)) },
        remove() { unregisterFn(id) },
      }
    },
  } as unknown as C

  registerFn(cache)
  return cache
}
```

```ts
// handler.ts
import { createPluginCache } from './pluginFactory'
import { createID, createLogger } from './util'
import * as register from '../../plugins/register'
import type { HandlerCache, HandlerOptions } from './types'

const formatOptions = (opts: HandlerOptions) => ({ /*...*/ })

export function handler(
  key: string,
  fnc: HandlerCache['register']['fnc'],
  options: HandlerOptions = {}
): HandlerCache {
  if (!key || !fnc) throw new Error('[handler]: missing key or fnc')
  return createPluginCache<
    HandlerCache['register'],
    HandlerOptions,
    HandlerCache
  >({
    type: 'handler',
    callerUrl: import.meta.url,
    options,
    formatOptions,
    registerPayload: { key, fnc },
    registerFn: register.registerHandler,
    unregisterFn: register.unregisterHandler,
    createID,
    createLogger: opts => createLogger(opts.log, true),
  })
}
```

这将把大约 100 行的 getter + 控制代码折叠到一个可重用的工厂中,同时保留所有行为。
</issue_to_address>

### 评论 10
<location> `packages/core/src/core/karin/base.ts:30` </location>
<code_context>
+}
+
+/** 插件缓存文件属性类型 */
+export interface PluginCacheKeyFile {
+  /** 绝对路径 */
+  absPath: string
</code_context>

<issue_to_address>
考虑通过在创建时计算值并使用 `ReturnType` 进行类型安全,从而移除手动 `PluginCacheKeyFile` 接口和属性 getter。

这里有两个小调整,它们保留了所有新功能,但简化了 `PluginCacheKeyFile` 周围的一些样板代码:

1.  在创建时一次性计算 `basename`/`dirname`(无需 getter)。
2.  使用 `ReturnType` 避免重复接口。

```ts
// 之前你需要保持 PluginCacheKeyFile 同步…
export const createPluginFileProperties = (absPath: string) => {
  const formatted = formatPath(absPath)
  return {
    absPath: formatted,
    basename: formatPath(path.basename(absPath)),
    dirname:  formatPath(path.dirname(absPath)),
  }
}
// …以及一个接口 PluginCacheKeyFile

// 相反,删除手动接口:
export type PluginCacheKeyFile = ReturnType<typeof createPluginFileProperties>
```

然后在 `PluginCache` 中,你仍然可以获得完整的类型安全:

```ts
export interface PluginCache {
  type: pluginTypes
  app: PluginCacheKeyApp
  file: PluginCacheKeyFile
  pkg: PluginCacheKeyPkg
  register: unknown
  control: unknown
}
```

这移除了 getter 样板代码和重复的接口,同时保留了所有现有行为。
</issue_to_address>

### 评论 11
<location> `packages/core/src/core/karin/class.ts:356` </location>
<code_context>
+ * @param file 文件路径
+ * @param Cls 类
+ */
+export const loadClass = (
+  pkgName: string,
+  file: string,
</code_context>

<issue_to_address>
考虑通过将 `CommandCache` 和控制 API 构造提取到单独的工厂函数中来重构 `loadClass` 函数,以减少文件大小并提高可维护性。

```markdown
你可以通过将“构建 CommandCache”和“构建其控制 API”的代码移到两个小型工厂中,从而大幅缩减 `loadClass` 并保留所有功能。例如:

```ts
// src/plugins/class/command-factory.ts
import { createID } from '@/core/karin/util';
import { formatReg, createLogger, formatFnc } from './util';
import type { Plugin, FormatOptions, CommandCache } from './types';
import * as manager from '../../plugins/manager';
import * as register from '../../plugins/register';

export function createClassCommand(
  pkgName: string,
  file: string,
  plugin: Plugin,
  rule: FormatOptions['rule'][number],
  options: FormatOptions,
  ids: string[],
): CommandCache | void {
  if (typeof rule.fnc === 'string' && !(plugin as any)[rule.fnc]) {
    console.warn(`[loadClass][${pkgName}] missing method ${rule.fnc} on ${file}`);
    return;
  }

  const id = createID();
  ids.push(id);
  let regCache = formatReg(rule.reg);
  let fncCache = formatFnc(rule.fnc, plugin);
  let ruleOpts = rule;

  const result: CommandCache = {
    type: 'class',
    get pkg()   { return manager.getPluginPackageDetail(pkgName)!; },
    get file()  { return manager.getFileCache(file); },
    get app()   {
      return { id, type: 'class', log: rule.log, name: rule.name };
    },
    get register() {
      return {
        get reg()     { return regCache; },
        get fnc()     { return fncCache; },
        get options() { return ruleOpts; },
        get instance(){ return plugin; },
        get ids()     { return ids; },
      };
    },
    get control() {
      return {
        setReg: (r) => { regCache = formatReg(r); },
        setFnc: (f) => { fncCache = formatFnc(f, plugin); },
        setRule: (r) => {
          const formatted = formatOptions({ ...options, rule: [r] });
          ruleOpts    = formatted.rule[0];
          regCache    = formatReg(ruleOpts.reg);
        },
        addRule: (newRule) => {
          const { rule: [added] } = formatOptions({ ...options, rule: [newRule] });
          options.rule.push(added);
          const cmd = createClassCommand(pkgName, file, plugin, added, options, ids);
          if (cmd) register.registerCommand(cmd);
        },
        remove: () => register.unregisterCommand(id),
      };
    },
  };

  Object.freeze(result.app.id);
  return result;
}
```

然后你的 `loadClass` 缩减为:

```ts
// src/plugins/class/load-class.ts
import { formatOptions } from './util';
import { createClassCommand } from './command-factory';
import type { Plugin } from './types';
import * as register from '../../plugins/register';

export const loadClass = (pkgName: string, file: string, Cls: unknown) => {
  if (!isClass<Plugin>(Cls)) return;
  const plugin = new (Cls as any)();
  if (!plugin._options) return;

  const opts = formatOptions(plugin._options);
  opts.exportName = plugin.constructor.name;

  const ids: string[] = [];
  for (const rule of opts.rule) {
    const cmd = createClassCommand(pkgName, file, plugin, rule, opts, ids);
    if (cmd) register.registerCommand(cmd);
  }
};
```

这会将关注点拆分为两个大约 80 行的文件,而不是一个 500 行的庞大文件,同时保留了每个 getter/setter 和运行时检查。
</issue_to_address>

### 评论 12
<location> `packages/core/src/plugins/register.ts:23` </location>
<code_context>
+ * 注册命令插件
+ * @param command 命令
+ */
+export const registerCommand = (command: CommandCache) => {
+  manager.cache.command.push(command)
+  manager.cache.count.command++
</code_context>

<issue_to_address>
考虑将重复的注册和注销逻辑重构为通用工厂函数,以减少重复并集中自定义行为。

以下是一种可能的方法,可以将所有几乎相同的注册/注销块合并为两个小型工厂,加上少量仅在需要时(处理程序和任务)的自定义逻辑。所有现有功能保持不变:

```ts
// 文件顶部
type CacheListKey = 'command' | 'accept' | 'button' | 'task';
type CountKey    = CacheListKey;

interface HasAppAndFile {
  app: { id: string };
  file: { absPath: string };
}

// 简单列表的通用工厂
function makeListHandlers<K extends CacheListKey, T extends HasAppAndFile>(
  listKey: K,
  countKey: CountKey,
  cleanup?: (item: T) => void
) {
  return {
    register(item: T) {
      (manager.cache[listKey] as T[]).push(item);
      manager.cache.count[countKey]++;
    },
    unregister(target: string) {
      target = _formatPath(target);
      const arr = manager.cache[listKey] as T[];
      const idx = arr.findIndex(v => v.app.id === target || v.file.absPath === target);
      if (idx === -1) return;

      if (cleanup) cleanup(arr[idx]);
      arr.splice(idx, 1);
      manager.cache.count[countKey]--;
    }
  };
}

// 用于 command/accept/button
const { register: registerCommand, unregister: unregisterCommand } =
  makeListHandlers<'command', CommandCache>('command', 'command');

const { register: registerAccept, unregister: unregisterAccept } =
  makeListHandlers<'accept', AcceptCache>('accept', 'accept');

const { register: registerButton, unregister: unregisterButton } =
  makeListHandlers<'button', ButtonCache>('button', 'button');

// 任务需要取消
const { register: registerTask, unregister: unregisterTask } =
  makeListHandlers<'task', TaskCache>(
    'task',
    'task',
    task => {
      if (task.register.schedule?.cancel) {
        task.register.schedule.cancel();
      }
    }
  );
```

对于处理程序(它们存在于数组映射中,而不是平面列表中),我们使用相同的模式,但迭代键:

```ts
function makeMapHandlers<T extends HasAppAndFile & { register: { key: string } }>(
  mapKey: 'handler',
  countKey: 'handler'
) {
  return {
    register(item: T) {
      const map = manager.cache[mapKey];
      if (!map[item.register.key]) {
        map[item.register.key] = [];
        manager.cache.count[countKey].key++;
      }
      map[item.register.key].push(item);
      manager.cache.count[countKey].fnc++;
    },
    unregister(target: string) {
      target = _formatPath(target);
      const map = manager.cache[mapKey];
      Object.keys(map).forEach(k => {
        const before = map[k].length;
        map[k] = map[k].filter(v => v.app.id !== target && v.file.absPath !== target);
        const removed = before - map[k].length;
        manager.cache.count[countKey].fnc -= removed;
      });
    }
  };
}

export const { register: registerHandler, unregister: unregisterHandler } =
  makeMapHandlers<HandlerCache>('handler', 'handler');
```

你现在可以删除所有重复的 `registerX` / `unregisterX` 块,只保留这 3 个工厂 + 你现有的 `unregisterApp``unregisterPackage``sort` 等。
</issue_to_address>

### 评论 13
<location> `packages/core/src/event/utils/dispatch/message.ts:86` </location>
<code_context>
+ * @param config 好友配置
+ * @param print 打印日志回调函数
+ */
+const callback = async (
+  ctx: FriendMessage | DirectMessage | GroupMessage | GuildMessage | GroupTempMessage,
+  plugin: CommandCache,
</code_context>

<issue_to_address>
考虑将回调函数中重复的上下文检查和权限逻辑重构为数据驱动的辅助函数,以保持函数简洁和专注。

```suggestion
你可以将重复的上下文检查和权限逻辑折叠成小型、数据驱动的辅助函数。这使得 `callback` 保持简短和专注:

1. 在一处定义你的上下文钩子:
```ts
// at top
const contextHooks: Array<{
  matches: (ctx: any) => boolean;
  emit: typeof eventCallEmit.friend | typeof eventCallEmit.direct /*…etc*/;
}> = [
  { matches: (ctx) => ctx.isFriend,   emit: eventCallEmit.friend },
  { matches: (ctx) => ctx.isDirect,   emit: eventCallEmit.direct },
  { matches: (ctx) => ctx.isGroup,    emit: eventCallEmit.group },
  { matches: (ctx) => ctx.isGuild,    emit: eventCallEmit.guild },
  { matches: (ctx) => ctx.isGroupTemp,emit: eventCallEmit.groupTemp },
]
```

2. 将大的 `if/else` 块替换为循环:
```ts
// inside callback():
for (const { matches, emit } of contextHooks) {
  if (matches(ctx)) {
    const ok = await emit(ctx, plugin)
    if (!ok) return false
    break
  }
}
// then the generic message hook
if (!(await eventCallEmit.message(ctx, plugin))) return false
```

3. 合并两个权限检查:
```ts
// inside try { … } before calling plugin.register.fnc
const checkPerm = ctx.isPrivate
  ? utils.permission.checkPrivate
  : utils.permission.checkGroup

if (!checkPerm(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
  return false
}
```

通过这些更改,`callback` 不再需要深度嵌套——每个关注点都是 3-4 行的步骤,使其保持简洁和可读,而不会改变任何行为。
</issue_to_address>

### 评论 14
<location> `packages/core/src/event/utils/initializer.ts:29` </location>
<code_context>
-export const initTips = (
-  ctx: Notice | Request
-) => {
-  switch (ctx.subEvent) {
-    case 'friendPoke':
-      ctx.tips = `戳一戳: ${ctx.content.operatorId} ${ctx.content.action || '戳了戳'} ${ctx.content.targetId}`
</code_context>

<issue_to_address>
考虑用将子事件映射到生成器函数的查找表替换事件提示的大型 switch 语句。

```suggestion
// 在模块顶部提取两个映射:
const noticeTipGenerators: Record<string, (ctx: Notice) => string> = {
  friendPoke: ({ content }) =>
    `戳一戳: ${content.operatorId} ${content.action || '戳了戳'} ${content.targetId}`,
  receiveLike: ({ content }) => `收到点赞: ${content.count}`,
  friendRecall: ({ content }) => `撤回消息: ${content.messageId}`,
  // …在此处添加其余的子事件
}

const requestTipGenerators: Record<string, (ctx: Request) => string> = {
  groupInvite: ({ content }) =>
    `群邀请: ${content.inviterId} 邀请Bot加入群 ${content.groupId},flag: ${content.flag}`,
  friendApply: ({ content }) =>
    `好友申请: ${content.applierId} 申请添加Bot为好友,验证信息: ${content.comment || '无'}`,
  // …在此处添加其余的子事件
}

const defaultTip = (rawEvent: any) => `未知子事件: ${JSON.stringify(rawEvent)}`

// 然后简化方法:
export const initializer = {
  notice: (ctx: Notice): void => {
    const gen = noticeTipGenerators[ctx.subEvent] ?? ((c: Notice) => defaultTip(c.rawEvent))
    ctx.tips = gen(ctx)
  },

  request: (ctx: Request): void => {
    const gen = requestTipGenerators[ctx.subEvent] ?? ((c: Request) => defaultTip(c.rawEvent))
    ctx.tips = gen(ctx)
  },

  // …其他方法不变
}
```

- 用两个小型查找表替换大型 `switch` 块。
- 在没有匹配键时回退到 `defaultTip`,以保持相同的功能。
- 这使得添加/删除子事件变得更容易,并降低了圈复杂度。
</issue_to_address>

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

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

  • Consider splitting this massive refactor into smaller, focused PRs (e.g. handler consolidation, plugin manager setup, utility extraction) to simplify review and reduce the risk of regressions.
  • In vite.config.ts the comment still says “只有大于1kb的文件才会被压缩” but the threshold is now 10240 (10 KB) — please update the comment accordingly.
  • The inline filterCallback definitions in each message handler share nearly identical permission logic—extracting them into a shared helper would cut down on boilerplate and improve maintainability.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider splitting this massive refactor into smaller, focused PRs (e.g. handler consolidation, plugin manager setup, utility extraction) to simplify review and reduce the risk of regressions.
- In vite.config.ts the comment still says “只有大于1kb的文件才会被压缩” but the threshold is now 10240 (10 KB) — please update the comment accordingly.
- The inline `filterCallback` definitions in each message handler share nearly identical permission logic—extracting them into a shared helper would cut down on boilerplate and improve maintainability.

## Individual Comments

### Comment 1
<location> `packages/core/src/core/karin/command.ts:209` </location>
<code_context>
+  let regCache = formatReg(reg)
+  let optCache = Object.freeze(formatOptions(options))
+  let logCache = Object.freeze(createLogger(options.log, true))
+  let fncCache = Object.freeze(formatFnc<T>(second, options) as MessageCallback<keyof MessageEventMap>)
+
+  const result: CommandCache = {
</code_context>

<issue_to_address>
Freezing functions is not necessary and may be misleading.

`Object.freeze` does not add meaningful immutability to functions and may cause confusion. Recommend removing it from function objects like `fncCache`.
</issue_to_address>

### Comment 2
<location> `packages/core/src/core/karin/base.ts:78` </location>
<code_context>
+ * @param absPath 调用者
+ * @returns 返回插件文件属性
+ */
+export const createPluginFileProperties = (absPath: string): PluginCacheKeyFile => {
+  return {
+    absPath: formatPath(absPath),
</code_context>

<issue_to_address>
formatPath is applied to path.basename, which may be unnecessary.

Since `basename` returns just the filename, normalizing slashes with `formatPath` may be redundant. Please confirm if this is required.
</issue_to_address>

### Comment 3
<location> `packages/core/src/types/server/plugins.ts:91` </location>
<code_context>
   /** 插件名称 `文件夹根目录名称` */
   name: string
   /** 插件类型 */
-  type: KarinPluginAppsType
+  type: Omit<PluginPackageType, 'apps'> | 'app'
   /** 插件版本 App类型为空 */
   version: string
</code_context>

<issue_to_address>
Type field now uses 'app' instead of 'apps', which may cause confusion.

This inconsistency with the rest of the codebase could cause confusion or matching errors. Please standardize the value used for the `type` field.
</issue_to_address>

### Comment 4
<location> `packages/core/src/event/utils/permission.ts:56` </location>
<code_context>
+    perm: CommandCache['register']['options']['permission'],
+    authFailMsg: CommandCache['register']['options']['authFailMsg']
+  ): boolean => {
+    if (!perm || perm === 'all') {
+      return true
+    }
</code_context>

<issue_to_address>
Default permission fallback may allow unintended access.

If a permission is missing or set to 'all', access is granted to all users. This could lead to unintended exposure if a plugin author omits the permission. Consider using a more restrictive default.
</issue_to_address>

### Comment 5
<location> `packages/core/src/event/utils/permission.ts:99` </location>
<code_context>
-
-  /** 计算插件处理时间 */
-  const start = Date.now()
-  if (ctx.isGroup) {
-    const result = await eventCallEmit.group(ctx, plugin)
-    if (!result) return false
</code_context>

<issue_to_address>
Group and guild permission checks are duplicated.

Refactor the shared permission logic to a common function to simplify maintenance and reduce code duplication.
</issue_to_address>

### Comment 6
<location> `packages/core/src/core/karin/command.ts:196` </location>
<code_context>
  * @returns 返回插件对象
  */
-export const command: CommandType = <T extends keyof MessageEventMap = keyof MessageEventMap> (
+export const command: Callbacks = <T extends keyof MessageEventMap = keyof MessageEventMap> (
   reg: string | RegExp,
-  second: string | Elements | Elements[] | CmdFnc<T>,
</code_context>

<issue_to_address>
Consider extracting the repeated plugin cache wiring logic into a reusable helper function to simplify and shrink the `command` implementation.

Here’s one way to drastically reduce the boilerplate in `command` by pulling out the common “plugin‐cache” wiring into a tiny helper.  All of your nested getters, freezing, and register/unregister calls move into that helper, and `command` simply passes its specific bits (reg ↔ fnc ↔ options, plus your register/unregister functions):

```ts
// util/buildCache.ts
type GetterMap<C,R> = {
  type: string
  getPkg: (ctx: C) => any
  getFile: (ctx: C) => any
  getApp:  (ctx: C) => Record<'id'|'log'|'name', any>
  getReg:  (ctx: C) => R['register']
  getCtl:  (ctx: C, res: R) => R['control']
  onInit?: (ctx: C, res: R) => void
}
export function buildCache<C, R extends {register:any, control:any}>(ctx: C, map: GetterMap<C,R>): R {
  const res = {} as R
  Object.defineProperties(res, {
    type: { get: () => map.type },
    pkg:  { get: () => map.getPkg(ctx) },
    file: { get: () => map.getFile(ctx) },
    app:  { get: () => map.getApp(ctx) },
    register: { get: () => map.getReg(ctx) },
    control:  { get: () => map.getCtl(ctx, res) },
  })
  map.onInit?.(ctx, res)
  return res
}
```

Then your `command` shrinks to:

```ts
import { buildCache } from './util/buildCache'
export const command: Callbacks = <T extends keyof MessageEventMap>(
  reg: string|RegExp,
  second: MessageCallback<T>|MessageSegment,
  options: Options<T>|StringOptions<T> = {}
): CommandCache => {
  // --- your existing setup ---
  const caller = system.getCaller(import.meta.url)!
  const pkgName = manager.getPackageName(caller)!
  const id = createID()
  let regCache = formatReg(reg)
  let optCache = Object.freeze(formatOptions(options))
  let fncCache = Object.freeze(formatFnc(second, options) as MessageCallback<any>)
  let logCache = Object.freeze(createLogger(options.log, true))

  return buildCache(
    { caller, pkgName, id, regCache, optCache, fncCache, logCache },
    {
      type: 'command',
      getPkg:    ctx => manager.getPluginPackageDetail(ctx.pkgName),
      getFile:   ctx => manager.getFileCache(ctx.caller),
      getApp:    ctx => ({ id: ctx.id, log: ctx.logCache, name: ctx.optCache.name }),
      getReg:    ctx => ({ reg: ctx.regCache, fnc: ctx.fncCache, options: ctx.optCache }),
      getCtl:    (ctx, res) => ({
        setReg:    r => ctx.regCache = formatReg(r),
        setFnc:    f => ctx.fncCache = Object.freeze(f),
        setOptions(o) {
          ctx.optCache = Object.freeze(formatOptions(o))
          ctx.logCache = Object.freeze(createLogger(o.log, true))
          ctx.fncCache = Object.freeze(formatFnc(second, o) as any)
        },
        remove:    () => register.unregisterCommand(res.app.id),
      }),
      onInit:    (ctx, res) => register.registerCommand(res),
    }
  )
}
```

Benefits:

- All the nested `get pkg/file/app/register/control` logic lives once in `buildCache`.
- `command` only specifies how to derive those five pieces plus your init hook.
- Future plugin types (e.g. events, hooks, classes) can reuse the same helper with their own mappings.
</issue_to_address>

### Comment 7
<location> `packages/core/src/core/karin/button.ts:80` </location>
<code_context>
  * @param fnc - 函数
+ * @param options - 选项
  */
 export const button = (
   reg: RegExp | string,
-  fnc: Button['fnc'],
</code_context>

<issue_to_address>
Consider extracting the common plugin construction logic into a reusable factory function to simplify and unify plugin creation.

```suggestion
// plugins/base.ts
import { system, types } from '@/utils'
import * as manager from './manager'
import * as registerFns from './register'
import { createID, createLogger } from './util'

export interface PluginBuilderConfig<TOpts, TCache> {
  type: string
  formatOptions: (opts: TOpts) => any
  formatReg?: (reg: string | RegExp) => RegExp
  register: (cache: TCache) => void
  unregister: (id: string) => void
}

export function makePlugin<TOpts, TCache extends { control: { remove(): void }, register: any }>(
  cfg: PluginBuilderConfig<TOpts, TCache>
) {
  return (reg: string | RegExp, fnc: any, options: TOpts): TCache => {
    if (!reg) throw new Error(`[${cfg.type}]: missing reg`)
    if (!fnc) throw new Error(`[${cfg.type}]: missing fnc`)

    const caller = system.getCaller(import.meta.url)!
    const pkgName = manager.getPackageName(caller)!
    const id = createID()

    let regCache = cfg.formatReg ? cfg.formatReg(reg) : (reg instanceof RegExp ? reg : new RegExp(reg))
    let fncCache = fnc
    let optCache = Object.freeze(cfg.formatOptions(options))
    const logCache = Object.freeze(createLogger((options as any).log, true))

    const base: any = {
      get type() { return cfg.type },
      get pkg() {
        if (!pkgName) throw new Error(`use only in standard modules: ${caller}`)
        return manager.getPluginPackageDetail(pkgName)!
      },
      get file() { return manager.getFileCache(caller)! },
      get app() {
        return {
          get id() { return id },
          get type() { return cfg.type },
          get log() { return logCache },
          get name() { return optCache.name }
        }
      },
      get register() {
        return {
          get reg() { return regCache },
          get fnc() { return fncCache },
          get options() { return optCache },
        }
      },
      get control() {
        return {
          setReg(r: string | RegExp) { regCache = cfg.formatReg ? cfg.formatReg(r) : (r instanceof RegExp ? r : new RegExp(r)) },
          setFnc(f: any) { fncCache = f },
          setOptions(o: TOpts) { optCache = cfg.formatOptions(o) },
          remove() { cfg.unregister(id) },
        }
      }
    }

    const cache = base as TCache
    cfg.register(cache)
    return cache
  }
}

// plugins/button.ts
import { makePlugin } from './base'
import type { ButtonOptions, ButtonCache } from './types'
import { formatOptions, formatReg } from './formatters'
import { registerButton, unregisterButton } from './register'

export const button = makePlugin<ButtonOptions, ButtonCache>({
  type: 'button',
  formatOptions,
  formatReg,
  register: registerButton,
  unregister: unregisterButton
})
```

Steps to apply:
1. Extract common builder logic into `makePlugin` (shown in `plugins/base.ts`).
2. Define per-plugin `formatOptions` and optionally `formatReg`/`formatters`.
3. Wire up `button` by calling `makePlugin`, passing `registerButton`/`unregisterButton`.
4. Remove the verbose `button` implementation and replace with the above.

This collapses duplicated getters and control methods into a single factory, reducing maintenance surface without losing functionality.
</issue_to_address>

### Comment 8
<location> `packages/core/src/core/karin/accept.ts:85` </location>
<code_context>
+  let optCache = Object.freeze(formatOptions(options))
+  const logCache = Object.freeze(createLogger(options.log, true))
+
+  const cache: AcceptCache = {
+    get type (): typeof type {
+      return type
</code_context>

<issue_to_address>
Consider extracting the repeated plugin cache, getters, and control logic into a shared factory function to reduce boilerplate across plugins.

Below is one way to trim out almost all of the duplicated “cache + getters + control + register” boilerplate into a single, shared factory. Everything stays 100% the same on the outside, but every plugin just calls this helper:

```ts
// plugins/registerUtil.ts
import { createID, createLogger } from './util'
import * as manager from './manager'

export type PluginConfig<
  Opts,
  RegInfo,
  Cache extends { register: RegInfo; control: any }
> = {
  type: string
  formatOptions: (opts: Opts) => any
  buildRegisterInfo: (
    opts: ReturnType<PluginConfig<Opts, RegInfo, Cache>['formatOptions']>,
    fnc: (...args: any[]) => any
  ) => RegInfo
  registerFn: (cache: Cache) => void
}

export function createPlugin<Opts, RegInfo, Cache extends { register: RegInfo }>(
  cfg: PluginConfig<Opts, RegInfo, Cache>
) {
  return <T extends any>(
    eventOrFnc: T,
    maybeFnc?: (...args: any[]) => any,
    opts: Opts = {} as Opts
  ): Cache => {
    const caller = manager.getCaller(import.meta.url)
    const pkgName = manager.getPackageName(caller)
    const id = createID()
    const type = cfg.type
    // 1) format, 2) build register, 3) build the shared cache shape
    const optCache = Object.freeze(cfg.formatOptions(opts))
    const regInfo = Object.freeze(
      cfg.buildRegisterInfo(optCache, maybeFnc as any)
    )
    const cache = {
      get type() { return type },
      get pkg() {
        if (!pkgName) throw new Error(`请在符合标准规范的文件中使用此方法: ${caller}`)
        return manager.getPluginPackageDetail(pkgName)!
      },
      get file() { return manager.getFileCache(caller) },
      get app() {
        return {
          id,
          type,
          log: createLogger(optCache.log, true),
          name: optCache.name
        }
      },
      get register() { return regInfo },
      get control() {
        return {
          setEvent(e: any) { regInfo.event = e },
          setFnc(f: any) { regInfo.fnc = f },
          setOptions(o: any) { /* re-format and reassign */ },
          remove() { cfg.registerFn(cache as Cache) }
        }
      }
    } as unknown as Cache

    cfg.registerFn(cache)
    return cache
  }
}
```

Then your `accept.ts` becomes:

```ts
import { types } from '@/utils'
import { createPlugin } from '../plugins/registerUtil'
import * as register from '../plugins/register'
import type { NoticeAndRequest } from '@/plugins/types'
import type { FNC } from './util'
import type { AcceptOptions } from './options'

const formatOptions = (o: AcceptOptions) => ({
  event: types.string(o.event, 'notice'),
  adapter: types.array(o.adapter, []),
  dsbAdapter: types.array(o.dsbAdapter, types.array(o.notAdapter, [])),
  priority: types.number(o.rank, types.number(o.priority, 10000)),
  name: types.string(o.name, 'accept'),
  log: types.bool(o.log, true),
})

export interface AcceptRegInfo {
  event: keyof NoticeAndRequest
  fnc: FNC<NoticeAndRequest[keyof NoticeAndRequest]>
  options: ReturnType<typeof formatOptions>
}

export const accept = createPlugin<
  AcceptOptions,
  AcceptRegInfo,
  { register: AcceptRegInfo; control: any }
>({
  type: 'accept',
  formatOptions,
  buildRegisterInfo: (opts, fnc) => ({
    event: opts.event,
    fnc,
    options: opts,
  }),
  registerFn: register.registerAccept,
})
```

This way:

- every plugin (accept/notice/request/…​) calls the *same* `createPlugin` helper  
- all the getters, ID/Logger wiring, `control.*` and `registerFn` call live in one place  
- each plugin file is ~20 LOC instead of ~150  
- functionality is unchanged and fully type-safe
</issue_to_address>

### Comment 9
<location> `packages/core/src/core/karin/handler.ts:81` </location>
<code_context>
+  let optCache = Object.freeze(formatOptions(options))
+  const logCache = Object.freeze(createLogger(options.log, true))
+
+  const cache: HandlerCache = {
+    get type (): typeof type {
+      return type
</code_context>

<issue_to_address>
Consider extracting the repeated plugin cache logic into a reusable factory function to simplify the handler implementation.

Consider extracting the shared “plugin‐cache” boilerplate into a small factory. You’ll end up with a ~20 LOC utility and your `handler()` can collapse to a handful of lines:

```ts
// pluginFactory.ts
import { system, types } from '@/utils'
import * as manager from '../../plugins/manager'

export interface PluginFactoryParams<R, O, C> {
  type: string
  callerUrl: string
  options: O
  formatOptions(o: O): C['register']['options']
  registerPayload: R
  registerFn(cache: C): void
  unregisterFn(id: string): void
  createID(): string
  createLogger?(logOpt: boolean): ReturnType<typeof console.log>
}

export function createPluginCache<R, O, C extends { register: { options: any } }>(
  params: PluginFactoryParams<R, O, C>
): C {
  const {
    type,
    callerUrl,
    options,
    formatOptions,
    registerPayload,
    createID,
    registerFn,
    unregisterFn,
    unregisterFn: remove,
    createLogger,
  } = params

  const caller = system.getCaller(callerUrl)!
  const pkgName = manager.getPackageName(caller)!
  const id = createID()
  let reg = { ...registerPayload }
  let opt = Object.freeze(formatOptions(options))
  const logger = createLogger ? Object.freeze(createLogger(opt.log)) : null

  const cache = {
    get type () { return type },
    get pkg () { return manager.getPluginPackageDetail(pkgName) },
    get file() { return manager.getFileCache(caller) },
    get app() {
      return { id, type, log: logger, name: opt.name }
    },
    get register() {
      return { ...reg, options: opt }
    },
    get control() {
      return {
        setKey(k: any) { reg = { ...reg, key: k } },
        setFnc(f: any) { reg = { ...reg, fnc: f } },
        setOptions(o: O) { opt = Object.freeze(formatOptions(o)) },
        remove() { unregisterFn(id) },
      }
    },
  } as unknown as C

  registerFn(cache)
  return cache
}
```

```ts
// handler.ts
import { createPluginCache } from './pluginFactory'
import { createID, createLogger } from './util'
import * as register from '../../plugins/register'
import type { HandlerCache, HandlerOptions } from './types'

const formatOptions = (opts: HandlerOptions) => ({ /*...*/ })

export function handler(
  key: string,
  fnc: HandlerCache['register']['fnc'],
  options: HandlerOptions = {}
): HandlerCache {
  if (!key || !fnc) throw new Error('[handler]: missing key or fnc')
  return createPluginCache<
    HandlerCache['register'],
    HandlerOptions,
    HandlerCache
  >({
    type: 'handler',
    callerUrl: import.meta.url,
    options,
    formatOptions,
    registerPayload: { key, fnc },
    registerFn: register.registerHandler,
    unregisterFn: register.unregisterHandler,
    createID,
    createLogger: opts => createLogger(opts.log, true),
  })
}
```

This collapses the ~100 LOC of getters + controls into a single reusable factory, while preserving all behavior.
</issue_to_address>

### Comment 10
<location> `packages/core/src/core/karin/base.ts:30` </location>
<code_context>
+}
+
+/** 插件缓存文件属性类型 */
+export interface PluginCacheKeyFile {
+  /** 绝对路径 */
+  absPath: string
</code_context>

<issue_to_address>
Consider removing the manual PluginCacheKeyFile interface and property getters by computing values at creation and using ReturnType for type safety.

Here are two small tweaks that keep all the new functionality but collapse some of the boilerplate around `PluginCacheKeyFile`:

1.  Compute `basename`/`dirname` once at creation (no need for getters).
2.  Use `ReturnType` to avoid repeating the interface.

```ts
// before you had to keep PluginCacheKeyFile in sync…
export const createPluginFileProperties = (absPath: string) => {
  const formatted = formatPath(absPath)
  return {
    absPath: formatted,
    basename: formatPath(path.basename(absPath)),
    dirname:  formatPath(path.dirname(absPath)),
  }
}
// …and an interface PluginCacheKeyFile

// instead, drop the manual interface:
export type PluginCacheKeyFile = ReturnType<typeof createPluginFileProperties>
```

Then in `PluginCache` you still get full type safety:

```ts
export interface PluginCache {
  type: pluginTypes
  app: PluginCacheKeyApp
  file: PluginCacheKeyFile
  pkg: PluginCacheKeyPkg
  register: unknown
  control: unknown
}
```

This removes the getter boilerplate and the duplicated interface while preserving all existing behavior.
</issue_to_address>

### Comment 11
<location> `packages/core/src/core/karin/class.ts:356` </location>
<code_context>
+ * @param file 文件路径
+ * @param Cls 类
+ */
+export const loadClass = (
+  pkgName: string,
+  file: string,
</code_context>

<issue_to_address>
Consider refactoring the 'loadClass' function by extracting CommandCache and control API construction into separate factory functions to reduce file size and improve maintainability.

```markdown
You can dramatically shrink `loadClass` and keep all functionality by moving the “build a CommandCache” and “build its control API” bits into two small factories. For example:

```ts
// src/plugins/class/command-factory.ts
import { createID } from '@/core/karin/util';
import { formatReg, createLogger, formatFnc } from './util';
import type { Plugin, FormatOptions, CommandCache } from './types';
import * as manager from '../../plugins/manager';
import * as register from '../../plugins/register';

export function createClassCommand(
  pkgName: string,
  file: string,
  plugin: Plugin,
  rule: FormatOptions['rule'][number],
  options: FormatOptions,
  ids: string[],
): CommandCache | void {
  if (typeof rule.fnc === 'string' && !(plugin as any)[rule.fnc]) {
    console.warn(`[loadClass][${pkgName}] missing method ${rule.fnc} on ${file}`);
    return;
  }

  const id = createID();
  ids.push(id);
  let regCache = formatReg(rule.reg);
  let fncCache = formatFnc(rule.fnc, plugin);
  let ruleOpts = rule;

  const result: CommandCache = {
    type: 'class',
    get pkg()   { return manager.getPluginPackageDetail(pkgName)!; },
    get file()  { return manager.getFileCache(file); },
    get app()   {
      return { id, type: 'class', log: rule.log, name: rule.name };
    },
    get register() {
      return {
        get reg()     { return regCache; },
        get fnc()     { return fncCache; },
        get options() { return ruleOpts; },
        get instance(){ return plugin; },
        get ids()     { return ids; },
      };
    },
    get control() {
      return {
        setReg: (r) => { regCache = formatReg(r); },
        setFnc: (f) => { fncCache = formatFnc(f, plugin); },
        setRule: (r) => {
          const formatted = formatOptions({ ...options, rule: [r] });
          ruleOpts    = formatted.rule[0];
          regCache    = formatReg(ruleOpts.reg);
        },
        addRule: (newRule) => {
          const { rule: [added] } = formatOptions({ ...options, rule: [newRule] });
          options.rule.push(added);
          const cmd = createClassCommand(pkgName, file, plugin, added, options, ids);
          if (cmd) register.registerCommand(cmd);
        },
        remove: () => register.unregisterCommand(id),
      };
    },
  };

  Object.freeze(result.app.id);
  return result;
}
```

Then your `loadClass` collapses to:

```ts
// src/plugins/class/load-class.ts
import { formatOptions } from './util';
import { createClassCommand } from './command-factory';
import type { Plugin } from './types';
import * as register from '../../plugins/register';

export const loadClass = (pkgName: string, file: string, Cls: unknown) => {
  if (!isClass<Plugin>(Cls)) return;
  const plugin = new (Cls as any)();
  if (!plugin._options) return;

  const opts = formatOptions(plugin._options);
  opts.exportName = plugin.constructor.name;

  const ids: string[] = [];
  for (const rule of opts.rule) {
    const cmd = createClassCommand(pkgName, file, plugin, rule, opts, ids);
    if (cmd) register.registerCommand(cmd);
  }
};
```

This splits concerns into two ~80-line files instead of one 500-line blob, while preserving every getter/setter and runtime check.
</issue_to_address>

### Comment 12
<location> `packages/core/src/plugins/register.ts:23` </location>
<code_context>
+ * 注册命令插件
+ * @param command 命令
+ */
+export const registerCommand = (command: CommandCache) => {
+  manager.cache.command.push(command)
+  manager.cache.count.command++
</code_context>

<issue_to_address>
Consider refactoring the repeated register and unregister logic into generic factory functions to reduce duplication and centralize custom behavior.

Here’s one possible way to collapse all of the nearly‐identical register/unregister blocks into two small factories plus a tiny bit of custom logic only where needed (handlers & tasks).  All existing functionality stays the same:

```ts
// at top of file
type CacheListKey = 'command' | 'accept' | 'button' | 'task';
type CountKey    = CacheListKey;

interface HasAppAndFile {
  app: { id: string };
  file: { absPath: string };
}

// generic factory for simple lists
function makeListHandlers<K extends CacheListKey, T extends HasAppAndFile>(
  listKey: K,
  countKey: CountKey,
  cleanup?: (item: T) => void
) {
  return {
    register(item: T) {
      (manager.cache[listKey] as T[]).push(item);
      manager.cache.count[countKey]++;
    },
    unregister(target: string) {
      target = _formatPath(target);
      const arr = manager.cache[listKey] as T[];
      const idx = arr.findIndex(v => v.app.id === target || v.file.absPath === target);
      if (idx === -1) return;

      if (cleanup) cleanup(arr[idx]);
      arr.splice(idx, 1);
      manager.cache.count[countKey]--;
    }
  };
}

// use it for command/accept/button
const { register: registerCommand, unregister: unregisterCommand } =
  makeListHandlers<'command', CommandCache>('command', 'command');

const { register: registerAccept, unregister: unregisterAccept } =
  makeListHandlers<'accept', AcceptCache>('accept', 'accept');

const { register: registerButton, unregister: unregisterButton } =
  makeListHandlers<'button', ButtonCache>('button', 'button');

// tasks need to cancel
const { register: registerTask, unregister: unregisterTask } =
  makeListHandlers<'task', TaskCache>(
    'task',
    'task',
    task => {
      if (task.register.schedule?.cancel) {
        task.register.schedule.cancel();
      }
    }
  );
```

For handlers (which live in a map of arrays instead of a flat list), we do the same pattern but iterate keys:

```ts
function makeMapHandlers<T extends HasAppAndFile & { register: { key: string } }>(
  mapKey: 'handler',
  countKey: 'handler'
) {
  return {
    register(item: T) {
      const map = manager.cache[mapKey];
      if (!map[item.register.key]) {
        map[item.register.key] = [];
        manager.cache.count[countKey].key++;
      }
      map[item.register.key].push(item);
      manager.cache.count[countKey].fnc++;
    },
    unregister(target: string) {
      target = _formatPath(target);
      const map = manager.cache[mapKey];
      Object.keys(map).forEach(k => {
        const before = map[k].length;
        map[k] = map[k].filter(v => v.app.id !== target && v.file.absPath !== target);
        const removed = before - map[k].length;
        manager.cache.count[countKey].fnc -= removed;
      });
    }
  };
}

export const { register: registerHandler, unregister: unregisterHandler } =
  makeMapHandlers<HandlerCache>('handler', 'handler');
```

You can now delete all of the duplicated `registerX` / `unregisterX` blocks, and keep only these 3 factories + your existing `unregisterApp`, `unregisterPackage`, `sort`, etc.
</issue_to_address>

### Comment 13
<location> `packages/core/src/event/utils/dispatch/message.ts:86` </location>
<code_context>
+ * @param config 好友配置
+ * @param print 打印日志回调函数
+ */
+const callback = async (
+  ctx: FriendMessage | DirectMessage | GroupMessage | GuildMessage | GroupTempMessage,
+  plugin: CommandCache,
</code_context>

<issue_to_address>
Consider refactoring the repetitive context checks and permission logic in the callback function into data-driven helpers to keep the function concise and focused.

```suggestion
You can collapse the repetitive context‐checks and permission logic into small, data-driven helpers. This keeps `callback` short and focused:

1. Define your context hooks in one place:
```ts
// at top
const contextHooks: Array<{
  matches: (ctx: any) => boolean;
  emit: typeof eventCallEmit.friend | typeof eventCallEmit.direct /*…etc*/;
}> = [
  { matches: (ctx) => ctx.isFriend,   emit: eventCallEmit.friend },
  { matches: (ctx) => ctx.isDirect,   emit: eventCallEmit.direct },
  { matches: (ctx) => ctx.isGroup,    emit: eventCallEmit.group },
  { matches: (ctx) => ctx.isGuild,    emit: eventCallEmit.guild },
  { matches: (ctx) => ctx.isGroupTemp,emit: eventCallEmit.groupTemp },
]
```

2. Replace the big `if/else` block with a loop:
```ts
// inside callback():
for (const { matches, emit } of contextHooks) {
  if (matches(ctx)) {
    const ok = await emit(ctx, plugin)
    if (!ok) return false
    break
  }
}
// then the generic message hook
if (!(await eventCallEmit.message(ctx, plugin))) return false
```

3. Merge both permission checks:
```ts
// inside try { … } before calling plugin.register.fnc
const checkPerm = ctx.isPrivate
  ? utils.permission.checkPrivate
  : utils.permission.checkGroup

if (!checkPerm(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
  return false
}
```

With these changes, `callback` no longer needs deep nesting—each concern is a 3–4 line step, keeping it concise and readable without altering any behavior.
</issue_to_address>

### Comment 14
<location> `packages/core/src/event/utils/initializer.ts:29` </location>
<code_context>
-export const initTips = (
-  ctx: Notice | Request
-) => {
-  switch (ctx.subEvent) {
-    case 'friendPoke':
-      ctx.tips = `戳一戳: ${ctx.content.operatorId} ${ctx.content.action || '戳了戳'} ${ctx.content.targetId}`
</code_context>

<issue_to_address>
Consider replacing the large switch statements for event tips with lookup tables mapping subEvents to generator functions.

```suggestion
// Extract both maps at the top of your module:
const noticeTipGenerators: Record<string, (ctx: Notice) => string> = {
  friendPoke: ({ content }) =>
    `戳一戳: ${content.operatorId} ${content.action || '戳了戳'} ${content.targetId}`,
  receiveLike: ({ content }) => `收到点赞: ${content.count}`,
  friendRecall: ({ content }) => `撤回消息: ${content.messageId}`,
  // …add the rest of your subEvents here
}

const requestTipGenerators: Record<string, (ctx: Request) => string> = {
  groupInvite: ({ content }) =>
    `群邀请: ${content.inviterId} 邀请Bot加入群 ${content.groupId},flag: ${content.flag}`,
  friendApply: ({ content }) =>
    `好友申请: ${content.applierId} 申请添加Bot为好友,验证信息: ${content.comment || '无'}`,
  // …add the rest of your subEvents here
}

const defaultTip = (rawEvent: any) => `未知子事件: ${JSON.stringify(rawEvent)}`

// Then simplify the methods:
export const initializer = {
  notice: (ctx: Notice): void => {
    const gen = noticeTipGenerators[ctx.subEvent] ?? ((c: Notice) => defaultTip(c.rawEvent))
    ctx.tips = gen(ctx)
  },

  request: (ctx: Request): void => {
    const gen = requestTipGenerators[ctx.subEvent] ?? ((c: Request) => defaultTip(c.rawEvent))
    ctx.tips = gen(ctx)
  },

  // …other methods unchanged
}
```

- Replace the big `switch` blocks with two small lookup tables.
- Keep the same functionality by falling back to `defaultTip` when no key matches.
- This makes adding/removing subEvents much easier and reduces cyclomatic complexity.
</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.

let regCache = formatReg(reg)
let optCache = Object.freeze(formatOptions(options))
let logCache = Object.freeze(createLogger(options.log, true))
let fncCache = Object.freeze(formatFnc<T>(second, options) as MessageCallback<keyof MessageEventMap>)
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: 冻结函数没有必要,并且可能引起误解。

Object.freeze 不会给函数增加有意义的不变性,并可能导致混淆。建议从 fncCache 等函数对象中移除它。

Original comment in English

nitpick: Freezing functions is not necessary and may be misleading.

Object.freeze does not add meaningful immutability to functions and may cause confusion. Recommend removing it from function objects like fncCache.

* @param absPath 调用者
* @returns 返回插件文件属性
*/
export const createPluginFileProperties = (absPath: string): PluginCacheKeyFile => {
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: formatPath 应用于 path.basename,这可能没有必要。

由于 basename 只返回文件名,因此使用 formatPath 规范化斜杠可能是多余的。请确认这是否是必需的。

Original comment in English

nitpick: formatPath is applied to path.basename, which may be unnecessary.

Since basename returns just the filename, normalizing slashes with formatPath may be redundant. Please confirm if this is required.

/** 插件名称 `文件夹根目录名称` */
name: string
/** 插件类型 */
type: KarinPluginAppsType
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: 类型字段现在使用 'app' 而不是 'apps',这可能会导致混淆。

与代码库其余部分的不一致可能会导致混淆或匹配错误。请标准化 type 字段使用的值。

Original comment in English

issue: Type field now uses 'app' instead of 'apps', which may cause confusion.

This inconsistency with the rest of the codebase could cause confusion or matching errors. Please standardize the value used for the type field.

perm: CommandCache['register']['options']['permission'],
authFailMsg: CommandCache['register']['options']['authFailMsg']
): boolean => {
if (!perm || perm === 'all') {
Copy link
Contributor

Choose a reason for hiding this comment

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

🚨 question (security): 默认权限回退可能允许意外访问。

如果缺少权限或设置为 'all',则所有用户都将获得访问权限。如果插件作者省略了权限,这可能会导致意外暴露。考虑使用更严格的默认值。

Original comment in English

🚨 question (security): Default permission fallback may allow unintended access.

If a permission is missing or set to 'all', access is granted to all users. This could lead to unintended exposure if a plugin author omits the permission. Consider using a more restrictive default.

Comment on lines 99 to 105
if (ctx.isGroup) {
if (perm === 'group.owner') {
if (ctx.isMaster || ctx.isAdmin || ctx.sender?.role === 'owner') return true
return permission.handleNoPermission(ctx, authFailMsg, permission.messages.owner)
}

if (perm === 'group.admin') {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: 群组和公会权限检查重复。

将共享权限逻辑重构为公共函数,以简化维护并减少代码重复。

Original comment in English

suggestion: Group and guild permission checks are duplicated.

Refactor the shared permission logic to a common function to simplify maintenance and reduce code duplication.

Comment on lines 142 to 150
if (ctx.isPrivate) {
if (!utils.permission.checkPrivate(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
} else {
if (!utils.permission.checkGroup(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): 将 else 子句的嵌套 if 语句合并到 else if 中 (merge-else-if)

Suggested change
if (ctx.isPrivate) {
if (!utils.permission.checkPrivate(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
} else {
if (!utils.permission.checkGroup(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
}
if (ctx.isPrivate) {
if (!utils.permission.checkPrivate(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
}
else if (!utils.permission.checkGroup(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}


Explanation将嵌套在 else 子句中的 if 语句扁平化会生成更易于阅读和扩展的代码。

Original comment in English

suggestion (code-quality): Merge else clause's nested if statement into else if (merge-else-if)

Suggested change
if (ctx.isPrivate) {
if (!utils.permission.checkPrivate(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
} else {
if (!utils.permission.checkGroup(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
}
if (ctx.isPrivate) {
if (!utils.permission.checkPrivate(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}
}
else if (!utils.permission.checkGroup(ctx, plugin.register.options.permission, plugin.register.options.authFailMsg)) {
return false
}


ExplanationFlattening if statements nested within else clauses generates code that is
easier to read and expand upon.

Comment on lines 27 to 37
if (ctx.isFriend) {
if (!config?.friend?.enable) {
logEvent('当前好友事件未启用')
return false
}
} else {
if (!config?.directs?.enable) {
logEvent('当前频道私信事件未启用')
return false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): 将 else 子句的嵌套 if 语句合并到 else if 中 (merge-else-if)

Suggested change
if (ctx.isFriend) {
if (!config?.friend?.enable) {
logEvent('当前好友事件未启用')
return false
}
} else {
if (!config?.directs?.enable) {
logEvent('当前频道私信事件未启用')
return false
}
}
if (ctx.isFriend) {
if (!config?.friend?.enable) {
logEvent('当前好友事件未启用')
return false
}
}
else if (!config?.directs?.enable) {
logEvent('当前频道私信事件未启用')
return false
}


Explanation将嵌套在 else 子句中的 if 语句扁平化会生成更易于阅读和扩展的代码。

Original comment in English

suggestion (code-quality): Merge else clause's nested if statement into else if (merge-else-if)

Suggested change
if (ctx.isFriend) {
if (!config?.friend?.enable) {
logEvent('当前好友事件未启用')
return false
}
} else {
if (!config?.directs?.enable) {
logEvent('当前频道私信事件未启用')
return false
}
}
if (ctx.isFriend) {
if (!config?.friend?.enable) {
logEvent('当前好友事件未启用')
return false
}
}
else if (!config?.directs?.enable) {
logEvent('当前频道私信事件未启用')
return false
}


ExplanationFlattening if statements nested within else clauses generates code that is
easier to read and expand upon.

Comment on lines 119 to 124
} else if (ctx.isGuild) {
if (!config?.guilds?.enable) {
logEvent('当前频道事件未启用')
return false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): 合并嵌套的 if 条件 (merge-nested-ifs)

Suggested change
} else if (ctx.isGuild) {
if (!config?.guilds?.enable) {
logEvent('当前频道事件未启用')
return false
}
}
} else if (ctx.isGuild && !config?.guilds?.enable) {
logEvent('当前频道事件未启用')
return false
}


Explanation阅读深度嵌套的条件代码令人困惑,因为你必须跟踪哪些条件与哪些级别相关。因此,我们力求在可能的情况下减少嵌套,并且可以将两个 if 条件使用 and 组合的情况是一个轻松的胜利。

Original comment in English

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
} else if (ctx.isGuild) {
if (!config?.guilds?.enable) {
logEvent('当前频道事件未启用')
return false
}
}
} else if (ctx.isGuild && !config?.guilds?.enable) {
logEvent('当前频道事件未启用')
return false
}


ExplanationReading deeply nested conditional code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

Comment on lines 132 to 137
if (config?.user?.enable_list?.length) {
if (!config?.user?.enable_list.includes(ctx.userId)) {
logEvent('用户未处于白名单')
return false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): 合并嵌套的 if 条件 (merge-nested-ifs)

Suggested change
if (config?.user?.enable_list?.length) {
if (!config?.user?.enable_list.includes(ctx.userId)) {
logEvent('用户未处于白名单')
return false
}
}
if (config?.user?.enable_list?.length && !config?.user?.enable_list.includes(ctx.userId)) {
logEvent('用户未处于白名单')
return false
}


Explanation阅读深度嵌套的条件代码令人困惑,因为你必须跟踪哪些条件与哪些级别相关。因此,我们力求在可能的情况下减少嵌套,并且可以将两个 if 条件使用 and 组合的情况是一个轻松的胜利。

Original comment in English

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if (config?.user?.enable_list?.length) {
if (!config?.user?.enable_list.includes(ctx.userId)) {
logEvent('用户未处于白名单')
return false
}
}
if (config?.user?.enable_list?.length && !config?.user?.enable_list.includes(ctx.userId)) {
logEvent('用户未处于白名单')
return false
}


ExplanationReading deeply nested conditional code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

Comment on lines 140 to 145
if (config?.user?.disable_list?.length) {
if (config?.user?.disable_list.includes(ctx.userId)) {
logEvent('用户处于黑名单')
return false
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (code-quality): 合并嵌套的 if 条件 (merge-nested-ifs)

Suggested change
if (config?.user?.disable_list?.length) {
if (config?.user?.disable_list.includes(ctx.userId)) {
logEvent('用户处于黑名单')
return false
}
}
if (config?.user?.disable_list?.length && config?.user?.disable_list.includes(ctx.userId)) {
logEvent('用户处于黑名单')
return false
}


Explanation阅读深度嵌套的条件代码令人困惑,因为你必须跟踪哪些条件与哪些级别相关。因此,我们力求在可能的情况下减少嵌套,并且可以将两个 if 条件使用 and 组合的情况是一个轻松的胜利。

Original comment in English

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if (config?.user?.disable_list?.length) {
if (config?.user?.disable_list.includes(ctx.userId)) {
logEvent('用户处于黑名单')
return false
}
}
if (config?.user?.disable_list?.length && config?.user?.disable_list.includes(ctx.userId)) {
logEvent('用户处于黑名单')
return false
}


ExplanationReading deeply nested conditional code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

@github-actions
Copy link
Contributor

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

pnpm add https://pkg.pr.new/node-karin@d1fef09 -w

sj817 added 15 commits August 19, 2025 15:31
- Implemented OneBot HTTP client with heartbeat and error handling.
- Created OneBot WebSocket client and server with event handling.
- Added retry utility for asynchronous functions.
- Defined event types and error types for WebSocket communication.
- Updated README with usage examples for HTTP and WebSocket clients.
- Introduced configuration options for both HTTP and WebSocket clients.
… error handling

- Added internal utility functions for ESM module cache management.
- Created a PluginLifecycleManager for managing plugin lifecycle events including loading, unloading, and reloading plugins.
- Developed a PluginLoader class to handle the loading of plugin packages and their applications.
- Introduced a market module for fetching and caching plugin lists from various sources.
- Established a RegisterManager for registering and unregistering commands, handlers, buttons, and tasks.
- Implemented a hot module replacement (HMR) mechanism for production environments.
- Added web.config management for plugins.
- Enhanced error handling for plugin loading and task execution.
- Created metadata parsing utilities for plugin information extraction.
- Developed a sample timer plugin demonstrating command registration and timer management.
@sj817 sj817 marked this pull request as draft October 2, 2025 01:03
@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-plugins

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

2 participants