@yunzai-ng/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:`e.prompt()` 的等待者登记表(会话内"等下一条消息")
|
|
3
|
+
* 依赖方向:仅依赖类型包
|
|
4
|
+
* 生命周期:随内核创建;每个等待者自带超时与插件卸载信号,不会长期驻留
|
|
5
|
+
* 注意事项:**等待者集中登记,不挂在插件实例上。** 挂在实例上的上下文会随热重载
|
|
6
|
+
* 一并悬空:使用者仍在等输入,而插件已被替换,此后他回复的消息永远得不到
|
|
7
|
+
* 响应,那个闭包也永远不被释放。
|
|
8
|
+
*
|
|
9
|
+
* 集中登记后**同时**绑定三个终止条件:
|
|
10
|
+
* 超时、插件卸载信号、被消息满足。任一条件触发均会摘除登记,
|
|
11
|
+
* 因此不存在永久驻留的等待者。
|
|
12
|
+
*
|
|
13
|
+
* 同一会话可存在多个等待者,按 FIFO 交付:先登记者先取得。
|
|
14
|
+
* 随机交付或全部唤醒均将使"两个插件同时向使用者提问"的行为不可预测。
|
|
15
|
+
*/
|
|
16
|
+
import type { Disposer, MessageEvent } from "@yunzai-ng/types"
|
|
17
|
+
import { targetKey } from "../message/target.js"
|
|
18
|
+
|
|
19
|
+
/** 缺省等待超时(毫秒) */
|
|
20
|
+
export const DEFAULT_PROMPT_TIMEOUT = 60_000
|
|
21
|
+
|
|
22
|
+
/** 一个等待者 */
|
|
23
|
+
interface Waiter {
|
|
24
|
+
/** 会话键 */
|
|
25
|
+
readonly key: string
|
|
26
|
+
/** 发起等待的用户 */
|
|
27
|
+
readonly uid: string
|
|
28
|
+
/** 是否只接受同一用户 */
|
|
29
|
+
readonly sameUser: boolean
|
|
30
|
+
/** 额外过滤 */
|
|
31
|
+
readonly filter: ((e: MessageEvent) => boolean) | undefined
|
|
32
|
+
/** 发起等待的插件名(日志与按插件取消用) */
|
|
33
|
+
readonly plugin: string
|
|
34
|
+
/** 结束等待 */
|
|
35
|
+
resolve(e: MessageEvent | undefined): void
|
|
36
|
+
/** 摘除全部终止条件 */
|
|
37
|
+
cleanup(): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 登记一个等待者所需的参数 */
|
|
41
|
+
export interface PromptWaitParams {
|
|
42
|
+
/** 会话键,用 `sessionKeyOf()` 计算 */
|
|
43
|
+
readonly key: string
|
|
44
|
+
/** 发起等待的用户 id */
|
|
45
|
+
readonly uid: string
|
|
46
|
+
/** 是否只接受同一用户的消息 */
|
|
47
|
+
readonly sameUser: boolean
|
|
48
|
+
/** 超时毫秒 */
|
|
49
|
+
readonly timeout: number
|
|
50
|
+
/** 额外过滤;返回 false 表示这条不算,继续等 */
|
|
51
|
+
readonly filter?: ((e: MessageEvent) => boolean) | undefined
|
|
52
|
+
/** 发起等待的插件名 */
|
|
53
|
+
readonly plugin: string
|
|
54
|
+
/** 插件卸载信号;abort 时等待以 undefined 结束 */
|
|
55
|
+
readonly signal?: AbortSignal | undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 计算会话键
|
|
60
|
+
*
|
|
61
|
+
* `selfId` 参与计算:同一个群中接入两个账号时,各自的等待互不干扰。
|
|
62
|
+
* 缺少它,A 号提出的问题会被回复给 B 号的等待者。
|
|
63
|
+
*
|
|
64
|
+
* 会话部分复用 `targetKey()`:发送队列用的是同一个函数,两处对"哪个算同一个
|
|
65
|
+
* 会话"的理解因此不可能走样。
|
|
66
|
+
* @param e 消息事件
|
|
67
|
+
* @returns 会话键
|
|
68
|
+
*/
|
|
69
|
+
export function sessionKeyOf(e: MessageEvent): string {
|
|
70
|
+
return `${e.platform}:${e.selfId}:${targetKey(e.target)}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `e.prompt()` 的等待者登记表 */
|
|
74
|
+
export class PromptRegistry {
|
|
75
|
+
/** 会话键 → 该会话上的等待者队列(FIFO) */
|
|
76
|
+
readonly #waiters = new Map<string, Waiter[]>()
|
|
77
|
+
|
|
78
|
+
/** 当前等待中的数量 */
|
|
79
|
+
get pending(): number {
|
|
80
|
+
let n = 0
|
|
81
|
+
for (const list of this.#waiters.values()) n += list.length
|
|
82
|
+
return n
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 登记一个等待者
|
|
87
|
+
* @param params 等待参数
|
|
88
|
+
* @returns 下一条满足条件的消息;超时或插件卸载时 undefined
|
|
89
|
+
*/
|
|
90
|
+
wait(params: PromptWaitParams): Promise<MessageEvent | undefined> {
|
|
91
|
+
return new Promise<MessageEvent | undefined>(resolve => {
|
|
92
|
+
let done = false
|
|
93
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
94
|
+
let offSignal: Disposer | undefined
|
|
95
|
+
|
|
96
|
+
/** 统一收尾:摘登记、清定时器、解绑信号 */
|
|
97
|
+
const cleanup = (): void => {
|
|
98
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
99
|
+
offSignal?.()
|
|
100
|
+
this.#detach(params.key, waiter)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 只允许结束一次 */
|
|
104
|
+
const finish = (e: MessageEvent | undefined): void => {
|
|
105
|
+
if (done) return
|
|
106
|
+
done = true
|
|
107
|
+
cleanup()
|
|
108
|
+
resolve(e)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const waiter: Waiter = {
|
|
112
|
+
key: params.key,
|
|
113
|
+
uid: params.uid,
|
|
114
|
+
sameUser: params.sameUser,
|
|
115
|
+
filter: params.filter,
|
|
116
|
+
plugin: params.plugin,
|
|
117
|
+
resolve: finish,
|
|
118
|
+
cleanup
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (params.timeout > 0) {
|
|
122
|
+
timer = setTimeout(() => finish(undefined), params.timeout)
|
|
123
|
+
// 不使一个等待中的 prompt 阻止进程退出:使用者可能始终不再回复,
|
|
124
|
+
// 而停机不应为此额外等待 60 秒
|
|
125
|
+
timer.unref?.()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const signal = params.signal
|
|
129
|
+
if (signal !== undefined) {
|
|
130
|
+
if (signal.aborted) {
|
|
131
|
+
finish(undefined)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
/** 插件卸载即结束等待 */
|
|
135
|
+
const onAbort = (): void => finish(undefined)
|
|
136
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
137
|
+
offSignal = () => signal.removeEventListener("abort", onAbort)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let list = this.#waiters.get(params.key)
|
|
141
|
+
if (list === undefined) {
|
|
142
|
+
list = []
|
|
143
|
+
this.#waiters.set(params.key, list)
|
|
144
|
+
}
|
|
145
|
+
list.push(waiter)
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 把消息投给该会话上的等待者
|
|
151
|
+
*
|
|
152
|
+
* 命中即消费:这条消息不再进入命令路由。否则"用户回答的内容恰好是个命令"
|
|
153
|
+
* 就会既满足等待又触发命令,两条回复一起发出来。
|
|
154
|
+
* @param e 消息事件
|
|
155
|
+
* @returns 是否被某个等待者消费
|
|
156
|
+
*/
|
|
157
|
+
offer(e: MessageEvent): boolean {
|
|
158
|
+
const key = sessionKeyOf(e)
|
|
159
|
+
const list = this.#waiters.get(key)
|
|
160
|
+
if (list === undefined || list.length === 0) return false
|
|
161
|
+
|
|
162
|
+
for (const waiter of [...list]) {
|
|
163
|
+
if (waiter.sameUser && waiter.uid !== e.sender.uid) continue
|
|
164
|
+
if (waiter.filter !== undefined && !waiter.filter(e)) continue
|
|
165
|
+
waiter.resolve(e)
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 取消某插件的全部等待
|
|
173
|
+
*
|
|
174
|
+
* 插件的 `ctx.signal` 已经能覆盖正常卸载路径,这里是兜底:`prompt` 若被
|
|
175
|
+
* 转手到别的插件(服务注入场景)调用,signal 可能对不上。
|
|
176
|
+
* @param plugin 插件名
|
|
177
|
+
* @returns 取消的数量
|
|
178
|
+
*/
|
|
179
|
+
cancelPlugin(plugin: string): number {
|
|
180
|
+
let n = 0
|
|
181
|
+
for (const list of [...this.#waiters.values()]) {
|
|
182
|
+
for (const waiter of [...list]) {
|
|
183
|
+
if (waiter.plugin === plugin) {
|
|
184
|
+
waiter.resolve(undefined)
|
|
185
|
+
n++
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return n
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 结束全部等待(停机时调用) */
|
|
193
|
+
clear(): void {
|
|
194
|
+
for (const list of [...this.#waiters.values()]) {
|
|
195
|
+
for (const waiter of [...list]) waiter.resolve(undefined)
|
|
196
|
+
}
|
|
197
|
+
this.#waiters.clear()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 将等待者从队列中摘除
|
|
202
|
+
* @param key 会话键
|
|
203
|
+
* @param waiter 等待者
|
|
204
|
+
*/
|
|
205
|
+
#detach(key: string, waiter: Waiter): void {
|
|
206
|
+
const list = this.#waiters.get(key)
|
|
207
|
+
if (list === undefined) return
|
|
208
|
+
const at = list.indexOf(waiter)
|
|
209
|
+
if (at >= 0) list.splice(at, 1)
|
|
210
|
+
if (list.length === 0) this.#waiters.delete(key)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:命令路由器的单元测试 —— 分桶索引、优先级、权限静态门、正则处理
|
|
3
|
+
* 依赖方向:测试文件,依赖 pipeline/router 与 testing/fake
|
|
4
|
+
* 生命周期:每个用例新建一个路由器,无共享状态
|
|
5
|
+
* 注意事项:这里**故意不建真事件**。`match()` 只读 8 个字段
|
|
6
|
+
* (text / scene / sender.uid / selfId / isGroup / atMe / isMaster /
|
|
7
|
+
* isGroupAdmin),为此启动一整套 EventFactory + BotApi 只会把
|
|
8
|
+
* "路由到底按什么匹配"埋进无关噪音里 —— 端到端那一层由
|
|
9
|
+
* kernel/runtime.test.ts 覆盖,这里专测索引与门禁本身。
|
|
10
|
+
*
|
|
11
|
+
* 分桶是这个文件最容易悄悄失效的优化:写错了功能照样正确,只是退化成
|
|
12
|
+
* 全表扫描。所以有几处断言直接盯着"另一个桶里的正则一次都没被 exec",
|
|
13
|
+
* 而不只是断言"没匹配上"。
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it, vi } from "vitest"
|
|
16
|
+
import type { CommandOptions, CommandPattern, MessageEvent, MessageScene } from "@yunzai-ng/types"
|
|
17
|
+
import type { CommandRegistration } from "../plugin/hooks.js"
|
|
18
|
+
import { fakeLogger, type FakeLogger } from "../testing/fake.js"
|
|
19
|
+
import { CommandRouter } from "./router.js"
|
|
20
|
+
|
|
21
|
+
/** 构造假消息事件的入参 */
|
|
22
|
+
interface MsgSpec {
|
|
23
|
+
/** 纯文本 */
|
|
24
|
+
text: string
|
|
25
|
+
/** 场景,缺省私聊 */
|
|
26
|
+
scene?: MessageScene
|
|
27
|
+
/** 发送者,缺省 20000 */
|
|
28
|
+
uid?: string
|
|
29
|
+
/** 接收账号,缺省 10000 */
|
|
30
|
+
selfId?: string
|
|
31
|
+
/** 是否 @ 了机器人 */
|
|
32
|
+
atMe?: boolean
|
|
33
|
+
/** 是否主人 */
|
|
34
|
+
isMaster?: boolean
|
|
35
|
+
/** 是否群管 */
|
|
36
|
+
isGroupAdmin?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 造一个够路由用的假消息事件
|
|
41
|
+
* @param spec 文本或完整描述
|
|
42
|
+
* @returns 消息事件
|
|
43
|
+
*/
|
|
44
|
+
function msg(spec: MsgSpec | string): MessageEvent {
|
|
45
|
+
const s: MsgSpec = typeof spec === "string" ? { text: spec } : spec
|
|
46
|
+
const scene = s.scene ?? "private"
|
|
47
|
+
return {
|
|
48
|
+
kind: "message",
|
|
49
|
+
scene,
|
|
50
|
+
text: s.text,
|
|
51
|
+
selfId: s.selfId ?? "10000",
|
|
52
|
+
sender: { uid: s.uid ?? "20000", name: "阿测" },
|
|
53
|
+
isPrivate: scene === "private",
|
|
54
|
+
isGroup: scene === "group",
|
|
55
|
+
atMe: s.atMe ?? false,
|
|
56
|
+
isMaster: s.isMaster ?? false,
|
|
57
|
+
isGroupAdmin: s.isGroupAdmin ?? false,
|
|
58
|
+
isGroupOwner: false
|
|
59
|
+
} as unknown as MessageEvent
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 造一条命令登记
|
|
64
|
+
* @param patterns 模式或模式数组
|
|
65
|
+
* @param options 命令选项
|
|
66
|
+
* @param plugin 插件名,断言顺序时用它当标记
|
|
67
|
+
* @returns 登记内容
|
|
68
|
+
*/
|
|
69
|
+
function reg(
|
|
70
|
+
patterns: CommandPattern | CommandPattern[],
|
|
71
|
+
options: CommandOptions = {},
|
|
72
|
+
plugin = "p"
|
|
73
|
+
): CommandRegistration {
|
|
74
|
+
return {
|
|
75
|
+
plugin,
|
|
76
|
+
patterns: Array.isArray(patterns) ? patterns : [patterns],
|
|
77
|
+
options,
|
|
78
|
+
handler: () => undefined
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 建一个路由器与它的日志器 */
|
|
83
|
+
function make(isDisabled?: (r: CommandRegistration) => boolean): { router: CommandRouter; logger: FakeLogger } {
|
|
84
|
+
const logger = fakeLogger()
|
|
85
|
+
const router = new CommandRouter(isDisabled === undefined ? { logger } : { logger, isDisabled })
|
|
86
|
+
return { router, logger }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 取命中的插件名序列,断言顺序时比逐个挖字段清楚
|
|
91
|
+
* @param router 路由器
|
|
92
|
+
* @param spec 消息
|
|
93
|
+
* @returns 插件名数组
|
|
94
|
+
*/
|
|
95
|
+
function hits(router: CommandRouter, spec: MsgSpec | string): string[] {
|
|
96
|
+
return router.match(msg(spec)).map(c => c.reg.plugin)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("字符串模式", () => {
|
|
100
|
+
it("按前缀命中,填好 trigger/rest/captures", () => {
|
|
101
|
+
const { router } = make()
|
|
102
|
+
router.register(reg("#查角色"))
|
|
103
|
+
|
|
104
|
+
const [hit] = router.match(msg("#查角色 刻晴 命座"))
|
|
105
|
+
expect(hit?.match.name).toBe("#查角色")
|
|
106
|
+
expect(hit?.match.pattern).toBe("#查角色")
|
|
107
|
+
expect(hit?.match.trigger).toBe("#查角色")
|
|
108
|
+
// rest 已 trim:插件里最常见的一行就是 `const name = e.command.rest`
|
|
109
|
+
expect(hit?.match.rest).toBe("刻晴 命座")
|
|
110
|
+
expect(hit?.match.groups).toEqual({})
|
|
111
|
+
expect(hit?.match.captures).toEqual(["#查角色"])
|
|
112
|
+
expect(hit?.match.plugin).toBe("p")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("前缀不在开头时不命中", () => {
|
|
116
|
+
const { router } = make()
|
|
117
|
+
router.register(reg("#ping"))
|
|
118
|
+
|
|
119
|
+
expect(router.match(msg("你好 #ping"))).toEqual([])
|
|
120
|
+
expect(router.match(msg("ping"))).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("anywhere 允许触发词出现在任意位置", () => {
|
|
124
|
+
const { router } = make()
|
|
125
|
+
router.register(reg("体力", { anywhere: true }))
|
|
126
|
+
|
|
127
|
+
const [hit] = router.match(msg("帮我看看体力还剩多少"))
|
|
128
|
+
expect(hit?.match.trigger).toBe("体力")
|
|
129
|
+
expect(hit?.match.rest).toBe("还剩多少")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("空串模式被拒绝,同一条命令的其余别名照旧可用", () => {
|
|
133
|
+
const { router, logger } = make()
|
|
134
|
+
router.register(reg(["", "#ok"]))
|
|
135
|
+
|
|
136
|
+
expect(hits(router, "#ok")).toEqual(["p"])
|
|
137
|
+
expect(logger.lines.some(l => l.includes("无效"))).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("一条命令的模式全都无效时不会被触发,但会留下告警", () => {
|
|
141
|
+
const { router, logger } = make()
|
|
142
|
+
router.register(reg(""))
|
|
143
|
+
|
|
144
|
+
expect(router.match(msg("随便说点什么"))).toEqual([])
|
|
145
|
+
expect(logger.lines.some(l => l.includes("没有任何有效模式"))).toBe(true)
|
|
146
|
+
// 条目本身还在注册表里:WebUI 得能显示"这条命令坏了"
|
|
147
|
+
expect(router.size).toBe(1)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe("正则模式", () => {
|
|
152
|
+
it("命名捕获与数字捕获都填进 match,未参与的组归一成空串", () => {
|
|
153
|
+
const { router } = make()
|
|
154
|
+
router.register(reg(/^#查(?<name>\S+?)(\d+)?命座/))
|
|
155
|
+
|
|
156
|
+
const [hit] = router.match(msg("#查刻晴命座 顺便看天赋"))
|
|
157
|
+
expect(hit?.match.groups).toEqual({ name: "刻晴" })
|
|
158
|
+
expect(hit?.match.captures).toEqual(["#查刻晴命座", "刻晴", ""])
|
|
159
|
+
expect(hit?.match.rest).toBe("顺便看天赋")
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("g/y 标志被去掉:同一条命令连发两次都要命中", () => {
|
|
163
|
+
const { router, logger } = make()
|
|
164
|
+
router.register(reg(/^#dup/g))
|
|
165
|
+
|
|
166
|
+
// 不去标志的话第二次 exec 会从上次的 lastIndex 继续找,表现为"命令只灵一次"
|
|
167
|
+
expect(router.match(msg("#dup"))).toHaveLength(1)
|
|
168
|
+
expect(router.match(msg("#dup"))).toHaveLength(1)
|
|
169
|
+
expect(logger.lines.some(l => l.includes("g/y"))).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("正则的 name 取 source,不含斜杠与标志", () => {
|
|
173
|
+
const { router } = make()
|
|
174
|
+
router.register(reg(/^#状态$/i))
|
|
175
|
+
|
|
176
|
+
expect(router.list()[0]?.name).toBe("^#状态$")
|
|
177
|
+
expect(router.list()[0]?.patterns).toEqual(["^#状态$"])
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe("首字符分桶", () => {
|
|
182
|
+
it("首字符不同的命令连 exec 都不会被调到", () => {
|
|
183
|
+
const { router } = make()
|
|
184
|
+
const re = /^#ping/
|
|
185
|
+
const spy = vi.spyOn(re, "exec")
|
|
186
|
+
router.register(reg(re))
|
|
187
|
+
|
|
188
|
+
expect(router.match(msg("!别的命令"))).toEqual([])
|
|
189
|
+
// 这一条是分桶优化本身的断言:功能正确但退化成全表扫描时它会红
|
|
190
|
+
expect(spy).not.toHaveBeenCalled()
|
|
191
|
+
|
|
192
|
+
expect(router.match(msg("#ping"))).toHaveLength(1)
|
|
193
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("提不出字面前缀的正则每条消息都要试", () => {
|
|
197
|
+
const { router } = make()
|
|
198
|
+
router.register(reg(/^\d+/))
|
|
199
|
+
|
|
200
|
+
const [hit] = router.match(msg("42 天后"))
|
|
201
|
+
expect(hit?.match.trigger).toBe("42")
|
|
202
|
+
expect(hit?.match.rest).toBe("天后")
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("带 m 标志的正则不分桶:第二行开头也算命中", () => {
|
|
206
|
+
const { router } = make()
|
|
207
|
+
router.register(reg(/^#x/m))
|
|
208
|
+
|
|
209
|
+
// 首字符是"啊",按 `#` 桶找不到;正因为 m 标志落进了每条都试的那份名单才命中
|
|
210
|
+
expect(hits(router, "啊\n#x")).toEqual(["p"])
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it("带 i 标志且字母开头的正则不分桶:大小写都能命中", () => {
|
|
214
|
+
const { router } = make()
|
|
215
|
+
router.register(reg(/^abc/i))
|
|
216
|
+
|
|
217
|
+
expect(hits(router, "ABC 大写")).toEqual(["p"])
|
|
218
|
+
expect(hits(router, "abc 小写")).toEqual(["p"])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("同一条命令的多个别名落在同一个桶里也只命中一次", () => {
|
|
222
|
+
const { router } = make()
|
|
223
|
+
router.register(reg(["#a", "#ab"]))
|
|
224
|
+
|
|
225
|
+
const found = router.match(msg("#ab"))
|
|
226
|
+
expect(found).toHaveLength(1)
|
|
227
|
+
// 按声明顺序取第一个能匹配上的模式
|
|
228
|
+
expect(found[0]?.match.pattern).toBe("#a")
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it("一条命令同时有可分桶与不可分桶的模式时也只命中一次", () => {
|
|
232
|
+
const { router } = make()
|
|
233
|
+
router.register(reg(["#x", /#x/]))
|
|
234
|
+
|
|
235
|
+
expect(router.match(msg("#x"))).toHaveLength(1)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe("优先级与顺序", () => {
|
|
240
|
+
it("priority 小者在前,同优先级按注册先后", () => {
|
|
241
|
+
const { router } = make()
|
|
242
|
+
router.register(reg("#a", {}, "先注册"))
|
|
243
|
+
router.register(reg("#a", { priority: 50 }, "更优先"))
|
|
244
|
+
router.register(reg("#a", {}, "后注册"))
|
|
245
|
+
|
|
246
|
+
expect(hits(router, "#a")).toEqual(["更优先", "先注册", "后注册"])
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it("桶内与不分桶的候选按同一套优先级归并", () => {
|
|
250
|
+
const a = make()
|
|
251
|
+
a.router.register(reg("#x", {}, "桶内"))
|
|
252
|
+
a.router.register(reg(/#x/, { priority: 50 }, "不分桶"))
|
|
253
|
+
expect(hits(a.router, "#x")).toEqual(["不分桶", "桶内"])
|
|
254
|
+
|
|
255
|
+
const b = make()
|
|
256
|
+
b.router.register(reg("#x", {}, "桶内"))
|
|
257
|
+
b.router.register(reg(/#x/, { priority: 150 }, "不分桶"))
|
|
258
|
+
expect(hits(b.router, "#x")).toEqual(["桶内", "不分桶"])
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe("静态门禁", () => {
|
|
263
|
+
it("还没 action() 的命令被跳过", () => {
|
|
264
|
+
const { router } = make()
|
|
265
|
+
const r = reg("#半成品")
|
|
266
|
+
r.handler = undefined
|
|
267
|
+
router.register(r)
|
|
268
|
+
|
|
269
|
+
expect(router.match(msg("#半成品"))).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("被禁用的命令被跳过,但仍出现在 list() 里且标了 disabled", () => {
|
|
273
|
+
const { router } = make(r => r.options.group === "关掉的")
|
|
274
|
+
router.register(reg("#关", { group: "关掉的" }))
|
|
275
|
+
router.register(reg("#开"))
|
|
276
|
+
|
|
277
|
+
expect(router.match(msg("#关"))).toEqual([])
|
|
278
|
+
expect(hits(router, "#开")).toEqual(["p"])
|
|
279
|
+
expect(router.list().map(i => i.disabled)).toEqual([true, false])
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("scene 限定生效,单值与数组两种写法都算", () => {
|
|
283
|
+
const { router } = make()
|
|
284
|
+
router.register(reg("#群限", { scene: "group" }, "仅群"))
|
|
285
|
+
router.register(reg("#多限", { scene: ["private", "guild"] }, "私聊与频道"))
|
|
286
|
+
|
|
287
|
+
expect(hits(router, "#群限")).toEqual([])
|
|
288
|
+
expect(hits(router, { text: "#群限", scene: "group" })).toEqual(["仅群"])
|
|
289
|
+
expect(hits(router, "#多限")).toEqual(["私聊与频道"])
|
|
290
|
+
expect(hits(router, { text: "#多限", scene: "group" })).toEqual([])
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it("默认不响应机器人自己发的消息,ignoreSelf:false 才放行", () => {
|
|
294
|
+
const { router } = make()
|
|
295
|
+
router.register(reg("#复读", {}, "默认"))
|
|
296
|
+
router.register(reg("#复读", { ignoreSelf: false }, "放行"))
|
|
297
|
+
|
|
298
|
+
// 自己发的:只有显式声明放行的那条能命中,否则复读类命令会自我触发成死循环
|
|
299
|
+
expect(hits(router, { text: "#复读", uid: "10000", selfId: "10000" })).toEqual(["放行"])
|
|
300
|
+
expect(hits(router, { text: "#复读", uid: "20000" })).toEqual(["默认", "放行"])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it("atMe 只约束群聊,私聊天然算对我说话", () => {
|
|
304
|
+
const { router } = make()
|
|
305
|
+
router.register(reg("#要艾特", { atMe: true }))
|
|
306
|
+
|
|
307
|
+
expect(hits(router, { text: "#要艾特", scene: "group" })).toEqual([])
|
|
308
|
+
expect(hits(router, { text: "#要艾特", scene: "group", atMe: true })).toEqual(["p"])
|
|
309
|
+
expect(hits(router, "#要艾特")).toEqual(["p"])
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it("master 门只对主人开", () => {
|
|
313
|
+
const { router } = make()
|
|
314
|
+
router.register(reg("#重启", { master: true }))
|
|
315
|
+
|
|
316
|
+
expect(hits(router, "#重启")).toEqual([])
|
|
317
|
+
expect(hits(router, { text: "#重启", isMaster: true })).toEqual(["p"])
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it("admin 门对群管开,主人不受群管限制", () => {
|
|
321
|
+
const { router } = make()
|
|
322
|
+
router.register(reg("#踢人", { admin: true }))
|
|
323
|
+
|
|
324
|
+
expect(hits(router, { text: "#踢人", scene: "group" })).toEqual([])
|
|
325
|
+
expect(hits(router, { text: "#踢人", scene: "group", isGroupAdmin: true })).toEqual(["p"])
|
|
326
|
+
// 主人在自己不是管理员的群里也该能用管理命令
|
|
327
|
+
expect(hits(router, { text: "#踢人", scene: "group", isMaster: true })).toEqual(["p"])
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe("注册表维护", () => {
|
|
332
|
+
it("注销后自所有桶中完整摘除", () => {
|
|
333
|
+
const { router } = make()
|
|
334
|
+
const off = router.register(reg(["#a", "!b"]))
|
|
335
|
+
expect(hits(router, "#a")).toEqual(["p"])
|
|
336
|
+
expect(hits(router, "!b")).toEqual(["p"])
|
|
337
|
+
|
|
338
|
+
off()
|
|
339
|
+
|
|
340
|
+
expect(router.match(msg("#a"))).toEqual([])
|
|
341
|
+
expect(router.match(msg("!b"))).toEqual([])
|
|
342
|
+
expect(router.size).toBe(0)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it("重复注册同一条登记是幂等的", () => {
|
|
346
|
+
const { router } = make()
|
|
347
|
+
const r = reg("#once")
|
|
348
|
+
router.register(r)
|
|
349
|
+
router.register(r)
|
|
350
|
+
|
|
351
|
+
expect(router.size).toBe(1)
|
|
352
|
+
expect(router.match(msg("#once"))).toHaveLength(1)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it("reindex 让后加的别名生效,展示名仍是主模式", () => {
|
|
356
|
+
const { router } = make()
|
|
357
|
+
const r = reg("#体力")
|
|
358
|
+
router.register(r)
|
|
359
|
+
expect(router.match(msg("#树脂"))).toEqual([])
|
|
360
|
+
|
|
361
|
+
r.patterns.push("#树脂")
|
|
362
|
+
router.reindex(r)
|
|
363
|
+
|
|
364
|
+
expect(hits(router, "#树脂")).toEqual(["p"])
|
|
365
|
+
expect(hits(router, "#体力")).toEqual(["p"])
|
|
366
|
+
expect(router.list()[0]?.name).toBe("#体力")
|
|
367
|
+
// 重建而不是追加:别名不该在桶里留下重复条目
|
|
368
|
+
expect(router.match(msg("#体力"))).toHaveLength(1)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it("reindex 未注册过的登记是空操作", () => {
|
|
372
|
+
const { router } = make()
|
|
373
|
+
router.reindex(reg("#没注册过"))
|
|
374
|
+
expect(router.size).toBe(0)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it("removePlugin 只摘该插件的命令", () => {
|
|
378
|
+
const { router } = make()
|
|
379
|
+
router.register(reg("#a", {}, "甲"))
|
|
380
|
+
router.register(reg("#b", {}, "甲"))
|
|
381
|
+
router.register(reg("#c", {}, "乙"))
|
|
382
|
+
|
|
383
|
+
expect(router.removePlugin("甲")).toBe(2)
|
|
384
|
+
expect(router.size).toBe(1)
|
|
385
|
+
expect(hits(router, "#c")).toEqual(["乙"])
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it("clear 清空一切", () => {
|
|
389
|
+
const { router } = make()
|
|
390
|
+
router.register(reg("#a"))
|
|
391
|
+
router.register(reg(/^\d+/))
|
|
392
|
+
|
|
393
|
+
router.clear()
|
|
394
|
+
|
|
395
|
+
expect(router.size).toBe(0)
|
|
396
|
+
expect(router.list()).toEqual([])
|
|
397
|
+
expect(router.match(msg("#a"))).toEqual([])
|
|
398
|
+
expect(router.match(msg("42"))).toEqual([])
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it("list() 导出 WebUI 需要的全部字段,按匹配顺序", () => {
|
|
402
|
+
const { router } = make()
|
|
403
|
+
router.register(reg("#后但更优先", { priority: 10 }, "乙"))
|
|
404
|
+
router.register(
|
|
405
|
+
reg("#帮助", { desc: "看帮助", usage: "#帮助 [页码]", group: "系统", hidden: true, master: true }, "甲")
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
expect(router.list()).toEqual([
|
|
409
|
+
{
|
|
410
|
+
name: "#后但更优先",
|
|
411
|
+
patterns: ["#后但更优先"],
|
|
412
|
+
plugin: "乙",
|
|
413
|
+
master: false,
|
|
414
|
+
admin: false,
|
|
415
|
+
hidden: false,
|
|
416
|
+
disabled: false
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "#帮助",
|
|
420
|
+
patterns: ["#帮助"],
|
|
421
|
+
plugin: "甲",
|
|
422
|
+
master: true,
|
|
423
|
+
admin: false,
|
|
424
|
+
hidden: true,
|
|
425
|
+
disabled: false,
|
|
426
|
+
desc: "看帮助",
|
|
427
|
+
usage: "#帮助 [页码]",
|
|
428
|
+
group: "系统"
|
|
429
|
+
}
|
|
430
|
+
])
|
|
431
|
+
})
|
|
432
|
+
})
|