-
Notifications
You must be signed in to change notification settings - Fork 17
refactor: plugins #513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
refactor: plugins #513
Conversation
审阅者指南此 PR 通过将事件初始化、过滤、冷却、分派和权限逻辑集中到专用工具中,彻底改革了插件和事件架构。它用共享的 统一消息事件处理的时序图 (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
新插件注册 API 的类图 (karin 核心入口点)classDiagram
class karin {
+accept()
+command()
+button()
+handler()
+task()
+context()
+render()
+sendMaster()
+sendAdmin()
+sendMsg()
}
karin : Emitter
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
Emitter 和事件暴露的类图classDiagram
class Emitter {
+name: 'karin'
+emit()
+on()
...
}
class events {
+events(emitter: Emitter)
}
Emitter <.. events: used by
文件级别更改
可能关联的问题提示和命令与 Sourcery 交互
自定义您的体验访问您的 仪表板 以:
获取帮助Original review guide in EnglishReviewer's GuideThis 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 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
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
Class diagram for OptionsBase and plugin options inheritanceclassDiagram
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
Class diagram for Emitter and event exposureclassDiagram
class Emitter {
+name: 'karin'
+emit()
+on()
...
}
class events {
+events(emitter: Emitter)
}
Emitter <.. events: used by
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this 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>帮助我更有用!请点击每个评论上的 👍 或 👎,我将利用这些反馈来改进您的评论。
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
filterCallbackdefinitions 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>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>) |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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.
| 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') { |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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)
| 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)
| 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
Flattening if statements nested within else clauses generates code that iseasier to read and expand upon.
| if (ctx.isFriend) { | ||
| if (!config?.friend?.enable) { | ||
| logEvent('当前好友事件未启用') | ||
| return false | ||
| } | ||
| } else { | ||
| if (!config?.directs?.enable) { | ||
| logEvent('当前频道私信事件未启用') | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
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)
| 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)
| 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
Flattening if statements nested within else clauses generates code that iseasier to read and expand upon.
| } else if (ctx.isGuild) { | ||
| if (!config?.guilds?.enable) { | ||
| logEvent('当前频道事件未启用') | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
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)
| } 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)
| } else if (ctx.isGuild) { | |
| if (!config?.guilds?.enable) { | |
| logEvent('当前频道事件未启用') | |
| return false | |
| } | |
| } | |
| } else if (ctx.isGuild && !config?.guilds?.enable) { | |
| logEvent('当前频道事件未启用') | |
| return false | |
| } | |
Explanation
Reading deeply nested conditional code is confusing, since you have to keep track of whichconditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two
if conditions can be combined usingand is an easy win.
| if (config?.user?.enable_list?.length) { | ||
| if (!config?.user?.enable_list.includes(ctx.userId)) { | ||
| logEvent('用户未处于白名单') | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
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)
| 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)
| 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
Reading deeply nested conditional code is confusing, since you have to keep track of whichconditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two
if conditions can be combined usingand is an easy win.
| if (config?.user?.disable_list?.length) { | ||
| if (config?.user?.disable_list.includes(ctx.userId)) { | ||
| logEvent('用户处于黑名单') | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
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)
| 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)
| 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
Reading deeply nested conditional code is confusing, since you have to keep track of whichconditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two
if conditions can be combined usingand is an easy win.
This reverts commit 9052c2e.
|
你可以通过以下命令安装该版本: |
- 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.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Note Other AI code review bot(s) detectedCodeRabbit 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)
Comment |
Sourcery 总结
全面改进了核心插件框架,通过引入集中式插件管理器和注册模块,将插件 API(命令、接受、按钮、任务、处理程序和类样式)统一为带有控制接口的详细缓存对象,并将常见的事件处理、过滤、冷却、权限和分发逻辑提取到可重用工具中。添加了热重载支持和用于动态文件 globbing 的新
@karin/glob包。更新了服务器插件端点,整合了遗留代码,现代化了 Vite 构建配置,并提升了项目依赖。新特性:
@karin/glob包,包含importGlob和importGlobEager,用于动态文件加载和原始内容支持core/karin/class.ts引入了基于类的插件支持event/utils下添加了统一的事件工具(filter、cooldown、checker、permission、initializer、dispatch和logger)reloadApp和reloadPackage,用于实时插件更新改进:
handleMessageCommon和dispatch工具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:
Enhancements:
Build:
Documentation: