@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,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:命令注册表与匹配(实现 `CommandSink`)
|
|
3
|
+
* 依赖方向:依赖类型包、plugin/hooks 的登记结构、util/text;不依赖事件实现与适配器
|
|
4
|
+
* 生命周期:随内核创建,随内核销毁;每条注册返回 Disposer,插件卸载即摘除
|
|
5
|
+
* 注意事项:**按触发词首字符分桶,而不是每条消息线性匹配全部正则。** 线性匹配下,
|
|
6
|
+
* 装 20 个插件就意味着每条消息数百次正则测试,且插件越多越慢。
|
|
7
|
+
*
|
|
8
|
+
* 分桶后:`"#体力"` 进入 `#` 桶,一条 `#体力` 仅
|
|
9
|
+
* 尝试 `#` 桶内的候选。桶内按 `priority` 预排序(注册时排序一次,
|
|
10
|
+
* 匹配时不排序),因此匹配路径上不存在 sort 与正则编译。
|
|
11
|
+
*
|
|
12
|
+
* 正则模式尽力提取字面前缀(`/^#体力$/` → `#`)亦进入桶;无法提取的
|
|
13
|
+
* (`/^\d+/`、带 `m` 或 `i` 标志的字母开头)落入 `#unbucketed`,
|
|
14
|
+
* 每条消息均需尝试 —— 这是必须付出的代价,因此插件作者在可使用字符串时
|
|
15
|
+
* 不应改用正则。
|
|
16
|
+
*/
|
|
17
|
+
import type {
|
|
18
|
+
CommandInfo,
|
|
19
|
+
CommandMatch,
|
|
20
|
+
CommandOptions,
|
|
21
|
+
CommandPattern,
|
|
22
|
+
Disposer,
|
|
23
|
+
Logger,
|
|
24
|
+
MessageEvent,
|
|
25
|
+
MessageScene
|
|
26
|
+
} from "@yunzai-ng/types"
|
|
27
|
+
import type { CommandRegistration, CommandSink } from "../plugin/hooks.js"
|
|
28
|
+
|
|
29
|
+
/** 缺省优先级 */
|
|
30
|
+
const DEFAULT_PRIORITY = 100
|
|
31
|
+
|
|
32
|
+
/** 正则里不能当字面前缀的字符 */
|
|
33
|
+
const REGEX_META = new Set([".", "*", "+", "?", "(", ")", "[", "]", "{", "}", "|", "$", "^"])
|
|
34
|
+
|
|
35
|
+
/** 会让前一个字符变成"可选"的量词,出现即无法据此分桶 */
|
|
36
|
+
const OPTIONAL_QUANTIFIER = new Set(["?", "*"])
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 一次模式命中的原始结果
|
|
40
|
+
*
|
|
41
|
+
* 与 `CommandMatch` 的区别是不含 `name`/`plugin` —— 那两个来自登记而非模式。
|
|
42
|
+
*/
|
|
43
|
+
interface PatternHit {
|
|
44
|
+
/** 命中的模式可读形式 */
|
|
45
|
+
pattern: string
|
|
46
|
+
/** 命中的触发词 */
|
|
47
|
+
trigger: string
|
|
48
|
+
/** 触发词之后剩余的文本,已 trim */
|
|
49
|
+
rest: string
|
|
50
|
+
/** 命名捕获组 */
|
|
51
|
+
groups: Readonly<Record<string, string>>
|
|
52
|
+
/** 数字捕获,下标 0 为整体匹配 */
|
|
53
|
+
captures: readonly string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 编译后的单个模式 */
|
|
57
|
+
interface CompiledPattern {
|
|
58
|
+
/** 可读形式;字符串模式即其本身,正则取 `source` */
|
|
59
|
+
readonly label: string
|
|
60
|
+
/** 分桶键(触发词首字符);`undefined` 表示无法分桶 */
|
|
61
|
+
readonly key: string | undefined
|
|
62
|
+
/**
|
|
63
|
+
* 尝试匹配
|
|
64
|
+
* @param text 消息纯文本
|
|
65
|
+
* @returns 命中结果;未命中返回 undefined
|
|
66
|
+
*/
|
|
67
|
+
match(text: string): PatternHit | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 注册表内部条目 */
|
|
71
|
+
interface Entry {
|
|
72
|
+
/** 原始登记(可变,插件可能随后追加别名) */
|
|
73
|
+
readonly reg: CommandRegistration
|
|
74
|
+
/** 注册序号,用于同优先级时的稳定排序 */
|
|
75
|
+
readonly seq: number
|
|
76
|
+
/** 编译后的模式 */
|
|
77
|
+
compiled: CompiledPattern[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 一条命中的候选 */
|
|
81
|
+
export interface RouterCandidate {
|
|
82
|
+
/** 命中的登记 */
|
|
83
|
+
readonly reg: CommandRegistration
|
|
84
|
+
/** 填给 `e.command` 的匹配结果 */
|
|
85
|
+
readonly match: CommandMatch
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 命令路由器构造参数 */
|
|
89
|
+
export interface CommandRouterOptions {
|
|
90
|
+
/** 日志器 */
|
|
91
|
+
readonly logger: Logger
|
|
92
|
+
/**
|
|
93
|
+
* 判断某命令是否被用户禁用
|
|
94
|
+
*
|
|
95
|
+
* 由配置层注入而非由路由器自行读取配置:路由器不应知晓配置文件的结构,
|
|
96
|
+
* 而 WebUI 的"停用某命令"开关(阶段四)只需替换该函数即可接入。
|
|
97
|
+
* @param reg 登记内容
|
|
98
|
+
* @returns 是否禁用
|
|
99
|
+
*/
|
|
100
|
+
readonly isDisabled?: (reg: CommandRegistration) => boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 取字符串模式的分桶键
|
|
105
|
+
* @param pattern 字符串模式
|
|
106
|
+
* @returns 首字符
|
|
107
|
+
*/
|
|
108
|
+
function keyOfString(pattern: string): string {
|
|
109
|
+
return [...pattern][0] ?? ""
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 尝试从正则里提取可分桶的字面首字符
|
|
114
|
+
*
|
|
115
|
+
* 只在**能证明**"命中必然以该字符开头"时才返回,宁可放弃优化也不能漏匹配。
|
|
116
|
+
* 放弃的情形:
|
|
117
|
+
* - 没有 `^` 锚定 —— 可以从任意位置命中
|
|
118
|
+
* - `m` 标志 —— `^` 也匹配行首,消息第二行开头也算命中,首字符判断不成立
|
|
119
|
+
* - `i` 标志且首字符是 ASCII 字母 —— 大小写两个桶,不值当
|
|
120
|
+
* - 首字符是元字符、字符类(`\d`)、或后面跟着 `?`/`*` 等可选量词
|
|
121
|
+
* @param re 正则
|
|
122
|
+
* @returns 分桶键;无法提取时 undefined
|
|
123
|
+
*/
|
|
124
|
+
function keyOfRegExp(re: RegExp): string | undefined {
|
|
125
|
+
if (re.flags.includes("m")) return undefined
|
|
126
|
+
|
|
127
|
+
const src = re.source
|
|
128
|
+
if (!src.startsWith("^")) return undefined
|
|
129
|
+
|
|
130
|
+
let literal: string
|
|
131
|
+
let after: number
|
|
132
|
+
|
|
133
|
+
const first = src[1]
|
|
134
|
+
if (first === undefined) return undefined
|
|
135
|
+
|
|
136
|
+
if (first === "\\") {
|
|
137
|
+
const escaped = src[2]
|
|
138
|
+
if (escaped === undefined) return undefined
|
|
139
|
+
// `\d` / `\w` / `\s` / `\b` 这类是字符类或断言,不是字面量
|
|
140
|
+
if (/[a-zA-Z0-9]/.test(escaped)) return undefined
|
|
141
|
+
literal = escaped
|
|
142
|
+
after = 3
|
|
143
|
+
} else if (REGEX_META.has(first)) {
|
|
144
|
+
return undefined
|
|
145
|
+
} else {
|
|
146
|
+
literal = first
|
|
147
|
+
after = 2
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const next = src[after]
|
|
151
|
+
if (next !== undefined && OPTIONAL_QUANTIFIER.has(next)) return undefined
|
|
152
|
+
// `{0,3}` 也让首字符可选;`{2}` 不会,但区分成本高于收益,一律放弃
|
|
153
|
+
if (next === "{") return undefined
|
|
154
|
+
|
|
155
|
+
if (re.flags.includes("i") && /[a-zA-Z]/.test(literal)) return undefined
|
|
156
|
+
|
|
157
|
+
return literal
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 去掉正则的有状态标志
|
|
162
|
+
*
|
|
163
|
+
* `g` 与 `y` 会让 `exec` 推进 `lastIndex`:同一个正则对象在第二条消息上
|
|
164
|
+
* 会从上次结束的位置开始找,表现为"命令只灵一次"。这类 bug 极难排查,
|
|
165
|
+
* 因此在注册时就消灭掉,并告知插件作者。
|
|
166
|
+
* @param re 原始正则
|
|
167
|
+
* @param label 日志用的命令标识
|
|
168
|
+
* @param logger 日志器
|
|
169
|
+
* @returns 无状态标志的正则
|
|
170
|
+
*/
|
|
171
|
+
function stripStatefulFlags(re: RegExp, label: string, logger: Logger): RegExp {
|
|
172
|
+
if (!re.flags.includes("g") && !re.flags.includes("y")) return re
|
|
173
|
+
const flags = re.flags.replace(/[gy]/g, "")
|
|
174
|
+
logger.warn(`命令 ${label} 的正则带 g/y 标志,已自动去除:这两个标志会让匹配位置在消息之间残留`)
|
|
175
|
+
return new RegExp(re.source, flags)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 编译一个模式
|
|
180
|
+
* @param pattern 原始模式
|
|
181
|
+
* @param opts 命令选项(`anywhere` 影响字符串匹配方式)
|
|
182
|
+
* @param label 日志用的命令标识
|
|
183
|
+
* @param logger 日志器
|
|
184
|
+
* @returns 编译结果
|
|
185
|
+
* @throws 字符串模式为空时
|
|
186
|
+
*/
|
|
187
|
+
function compile(pattern: CommandPattern, opts: CommandOptions, label: string, logger: Logger): CompiledPattern {
|
|
188
|
+
if (typeof pattern === "string") {
|
|
189
|
+
if (pattern === "") throw new Error(`命令 ${label} 的字符串模式不能为空:空串会匹配所有消息`)
|
|
190
|
+
|
|
191
|
+
if (opts.anywhere === true) {
|
|
192
|
+
return {
|
|
193
|
+
label: pattern,
|
|
194
|
+
// anywhere 的触发词可以出现在任何位置,首字符判断不成立
|
|
195
|
+
key: undefined,
|
|
196
|
+
match: text => {
|
|
197
|
+
const at = text.indexOf(pattern)
|
|
198
|
+
if (at < 0) return undefined
|
|
199
|
+
return {
|
|
200
|
+
pattern,
|
|
201
|
+
trigger: pattern,
|
|
202
|
+
rest: text.slice(at + pattern.length).trim(),
|
|
203
|
+
groups: {},
|
|
204
|
+
captures: [pattern]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
label: pattern,
|
|
212
|
+
key: keyOfString(pattern),
|
|
213
|
+
match: text => {
|
|
214
|
+
if (!text.startsWith(pattern)) return undefined
|
|
215
|
+
return {
|
|
216
|
+
pattern,
|
|
217
|
+
trigger: pattern,
|
|
218
|
+
rest: text.slice(pattern.length).trim(),
|
|
219
|
+
groups: {},
|
|
220
|
+
captures: [pattern]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const re = stripStatefulFlags(pattern, label, logger)
|
|
227
|
+
return {
|
|
228
|
+
label: re.source,
|
|
229
|
+
key: keyOfRegExp(re),
|
|
230
|
+
match: text => {
|
|
231
|
+
const m = re.exec(text)
|
|
232
|
+
if (m === null) return undefined
|
|
233
|
+
const whole = m[0]
|
|
234
|
+
return {
|
|
235
|
+
pattern: re.source,
|
|
236
|
+
trigger: whole,
|
|
237
|
+
rest: text.slice(m.index + whole.length).trim(),
|
|
238
|
+
groups: m.groups ?? {},
|
|
239
|
+
// 捕获组可能不参与匹配(`(a)|(b)`),undefined 统一成空串,
|
|
240
|
+
// 免得插件里到处写 `captures[1] ?? ""`
|
|
241
|
+
captures: Array.from(m, v => v ?? "")
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 比较两个条目的匹配顺序
|
|
249
|
+
* @param a 条目 a
|
|
250
|
+
* @param b 条目 b
|
|
251
|
+
* @returns 排序结果
|
|
252
|
+
*/
|
|
253
|
+
function byPriority(a: Entry, b: Entry): number {
|
|
254
|
+
const pa = a.reg.options.priority ?? DEFAULT_PRIORITY
|
|
255
|
+
const pb = b.reg.options.priority ?? DEFAULT_PRIORITY
|
|
256
|
+
return pa !== pb ? pa - pb : a.seq - b.seq
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 把一项按序插入已排序数组
|
|
261
|
+
*
|
|
262
|
+
* 注册是低频操作、匹配是高频操作,因此把排序成本挪到注册时,
|
|
263
|
+
* 让匹配路径上一次 sort 都不做。
|
|
264
|
+
* @param list 已排序数组(就地修改)
|
|
265
|
+
* @param entry 待插入条目
|
|
266
|
+
*/
|
|
267
|
+
function insertSorted(list: Entry[], entry: Entry): void {
|
|
268
|
+
let lo = 0
|
|
269
|
+
let hi = list.length
|
|
270
|
+
while (lo < hi) {
|
|
271
|
+
const mid = (lo + hi) >>> 1
|
|
272
|
+
// 断言安全:mid < hi <= length
|
|
273
|
+
if (byPriority(list[mid]!, entry) <= 0) lo = mid + 1
|
|
274
|
+
else hi = mid
|
|
275
|
+
}
|
|
276
|
+
list.splice(lo, 0, entry)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 判断事件场景是否落在命令声明的范围内
|
|
281
|
+
* @param opts 命令选项
|
|
282
|
+
* @param scene 事件场景
|
|
283
|
+
* @returns 是否允许
|
|
284
|
+
*/
|
|
285
|
+
function sceneAllowed(opts: CommandOptions, scene: MessageScene): boolean {
|
|
286
|
+
const want = opts.scene
|
|
287
|
+
if (want === undefined) return true
|
|
288
|
+
return Array.isArray(want) ? want.includes(scene) : want === scene
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 命令路由器
|
|
293
|
+
*
|
|
294
|
+
* 既是 `ctx.command()` 的落点(`CommandSink`),也是消息管线的查询入口
|
|
295
|
+
* (`match()`),还是 WebUI 的数据来源(`list()`)。
|
|
296
|
+
*/
|
|
297
|
+
export class CommandRouter implements CommandSink {
|
|
298
|
+
/** 日志器 */
|
|
299
|
+
readonly #logger: Logger
|
|
300
|
+
/** 命令禁用判定 */
|
|
301
|
+
readonly #isDisabled: (reg: CommandRegistration) => boolean
|
|
302
|
+
/** 登记 → 条目 */
|
|
303
|
+
readonly #entries = new Map<CommandRegistration, Entry>()
|
|
304
|
+
/** 首字符 → 已排序的条目数组 */
|
|
305
|
+
readonly #buckets = new Map<string, Entry[]>()
|
|
306
|
+
/** 无法分桶、每条消息都要试的条目(已排序) */
|
|
307
|
+
#unbucketed: Entry[] = []
|
|
308
|
+
/** 注册序号发号器 */
|
|
309
|
+
#seq = 0
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param opts 构造参数
|
|
313
|
+
*/
|
|
314
|
+
constructor(opts: CommandRouterOptions) {
|
|
315
|
+
this.#logger = opts.logger
|
|
316
|
+
this.#isDisabled = opts.isDisabled ?? (() => false)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 已注册的命令条数 */
|
|
320
|
+
get size(): number {
|
|
321
|
+
return this.#entries.size
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 登记一条命令
|
|
326
|
+
*
|
|
327
|
+
* 不会抛错:单个模式编译失败(如空串)只落一条 error 日志并跳过该模式,
|
|
328
|
+
* 其余别名照旧可用 —— 一条命令的一个别名写错不该让插件装不上。
|
|
329
|
+
* @param reg 登记内容
|
|
330
|
+
* @returns 注销句柄
|
|
331
|
+
*/
|
|
332
|
+
register(reg: CommandRegistration): Disposer {
|
|
333
|
+
if (this.#entries.has(reg)) return () => this.#remove(reg)
|
|
334
|
+
|
|
335
|
+
const entry: Entry = { reg, seq: this.#seq++, compiled: [] }
|
|
336
|
+
entry.compiled = this.#compileAll(reg)
|
|
337
|
+
this.#entries.set(reg, entry)
|
|
338
|
+
this.#index(entry)
|
|
339
|
+
|
|
340
|
+
return () => this.#remove(reg)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 模式变化后重建索引
|
|
345
|
+
*
|
|
346
|
+
* 链式构造器的 `.alias()` 会往 `reg.patterns` 里塞新模式。整条重建而不是
|
|
347
|
+
* 增量追加:别名极少,而增量逻辑要处理"旧键还在不在别的模式里用"这种
|
|
348
|
+
* 引用计数问题,不值得。
|
|
349
|
+
* @param reg 登记内容
|
|
350
|
+
*/
|
|
351
|
+
reindex(reg: CommandRegistration): void {
|
|
352
|
+
const entry = this.#entries.get(reg)
|
|
353
|
+
if (entry === undefined) return
|
|
354
|
+
this.#unindex(entry)
|
|
355
|
+
entry.compiled = this.#compileAll(reg)
|
|
356
|
+
this.#index(entry)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 找出该消息命中的全部命令,按匹配顺序排列
|
|
361
|
+
*
|
|
362
|
+
* 只做**模式匹配**与**零成本的静态过滤**(禁用、场景、@我、权限)。
|
|
363
|
+
* 冷却是异步的、要写 KV,且命中后才该计费,因此留给 dispatch。
|
|
364
|
+
* @param e 消息事件
|
|
365
|
+
* @returns 候选数组;无命中时为空数组
|
|
366
|
+
*/
|
|
367
|
+
match(e: MessageEvent): RouterCandidate[] {
|
|
368
|
+
const candidates = this.#candidates(e.text)
|
|
369
|
+
if (candidates.length === 0) return []
|
|
370
|
+
|
|
371
|
+
const out: RouterCandidate[] = []
|
|
372
|
+
for (const entry of candidates) {
|
|
373
|
+
const { reg } = entry
|
|
374
|
+
if (reg.handler === undefined) continue
|
|
375
|
+
if (this.#isDisabled(reg)) continue
|
|
376
|
+
if (!sceneAllowed(reg.options, e.scene)) continue
|
|
377
|
+
// 机器人自己发的消息:全局 bot.ignoreSelf 为 false 时事件才会走到这里,
|
|
378
|
+
// 但单条命令仍然默认不响应自己 —— 否则一个"复读"类命令会自我触发成死循环
|
|
379
|
+
if (reg.options.ignoreSelf !== false && e.sender.uid === e.selfId) continue
|
|
380
|
+
// 群聊要求 @ 机器人;私聊天然算"对我说话"
|
|
381
|
+
if (reg.options.atMe === true && e.isGroup && !e.atMe) continue
|
|
382
|
+
if (reg.options.master === true && !e.isMaster) continue
|
|
383
|
+
// 主人不受群管限制:否则主人在自己不是管理员的群里连管理命令都用不了
|
|
384
|
+
if (reg.options.admin === true && !e.isGroupAdmin && !e.isMaster) continue
|
|
385
|
+
|
|
386
|
+
for (const pattern of entry.compiled) {
|
|
387
|
+
const hit = pattern.match(e.text)
|
|
388
|
+
if (hit === undefined) continue
|
|
389
|
+
out.push({
|
|
390
|
+
reg,
|
|
391
|
+
match: {
|
|
392
|
+
name: this.#nameOf(entry),
|
|
393
|
+
pattern: hit.pattern,
|
|
394
|
+
trigger: hit.trigger,
|
|
395
|
+
rest: hit.rest,
|
|
396
|
+
groups: hit.groups,
|
|
397
|
+
captures: hit.captures,
|
|
398
|
+
plugin: reg.plugin
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
// 同一条命令的多个别名只算一次命中
|
|
402
|
+
break
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return out
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 导出全部命令描述
|
|
410
|
+
* @returns 命令描述数组,按匹配顺序
|
|
411
|
+
*/
|
|
412
|
+
list(): CommandInfo[] {
|
|
413
|
+
const all = [...this.#entries.values()].sort(byPriority)
|
|
414
|
+
return all.map(entry => {
|
|
415
|
+
const { options: o, plugin } = entry.reg
|
|
416
|
+
const info: CommandInfo = {
|
|
417
|
+
name: this.#nameOf(entry),
|
|
418
|
+
patterns: entry.compiled.map(p => p.label),
|
|
419
|
+
plugin,
|
|
420
|
+
master: o.master === true,
|
|
421
|
+
admin: o.admin === true,
|
|
422
|
+
hidden: o.hidden === true,
|
|
423
|
+
disabled: this.#isDisabled(entry.reg)
|
|
424
|
+
}
|
|
425
|
+
if (o.desc !== undefined) info.desc = o.desc
|
|
426
|
+
if (o.usage !== undefined) info.usage = o.usage
|
|
427
|
+
if (o.group !== undefined) info.group = o.group
|
|
428
|
+
return info
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 摘除某插件的全部命令
|
|
434
|
+
*
|
|
435
|
+
* 插件卸载时上下文会逐条调用 Disposer,此处为兜底:若存在未经 ctx 的登记
|
|
436
|
+
*(例如插件 setup 中途抛错),亦不会留下悬空命令。
|
|
437
|
+
* @param plugin 插件名
|
|
438
|
+
* @returns 摘除的条数
|
|
439
|
+
*/
|
|
440
|
+
removePlugin(plugin: string): number {
|
|
441
|
+
let n = 0
|
|
442
|
+
for (const reg of [...this.#entries.keys()]) {
|
|
443
|
+
if (reg.plugin === plugin) {
|
|
444
|
+
this.#remove(reg)
|
|
445
|
+
n++
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return n
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** 清空全部登记 */
|
|
452
|
+
clear(): void {
|
|
453
|
+
this.#entries.clear()
|
|
454
|
+
this.#buckets.clear()
|
|
455
|
+
this.#unbucketed = []
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* 编译一条登记的全部模式
|
|
460
|
+
* @param reg 登记内容
|
|
461
|
+
* @returns 编译结果数组
|
|
462
|
+
*/
|
|
463
|
+
#compileAll(reg: CommandRegistration): CompiledPattern[] {
|
|
464
|
+
const label = `${reg.plugin}:${String(reg.patterns[0] ?? "?")}`
|
|
465
|
+
const out: CompiledPattern[] = []
|
|
466
|
+
for (const p of reg.patterns) {
|
|
467
|
+
try {
|
|
468
|
+
out.push(compile(p, reg.options, label, this.#logger))
|
|
469
|
+
} catch (err) {
|
|
470
|
+
// 一个别名编译失败不该让整条命令消失,其余模式仍然可用
|
|
471
|
+
this.#logger.error(`命令 ${label} 的模式 ${String(p)} 无效:${err instanceof Error ? err.message : String(err)}`)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (out.length === 0) this.#logger.error(`命令 ${label} 没有任何有效模式,将不会被触发`)
|
|
475
|
+
return out
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 取命令展示名
|
|
480
|
+
* @param entry 条目 t
|
|
481
|
+
* @returns 首个模式的可读形式
|
|
482
|
+
*/
|
|
483
|
+
#nameOf(entry: Entry): string {
|
|
484
|
+
return entry.compiled[0]?.label ?? String(entry.reg.patterns[0] ?? "?")
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 把条目挂进索引
|
|
489
|
+
* @param entry 条目
|
|
490
|
+
*/
|
|
491
|
+
#index(entry: Entry): void {
|
|
492
|
+
// 同一条命令的多个模式可能落在同一个桶(`#体力`/`#树脂` 都在 `#`),
|
|
493
|
+
// 用 Set 去重,避免一条命令在桶里出现两次导致重复命中
|
|
494
|
+
const keys = new Set<string>()
|
|
495
|
+
let needsUnbucketed = false
|
|
496
|
+
for (const p of entry.compiled) {
|
|
497
|
+
if (p.key === undefined || p.key === "") needsUnbucketed = true
|
|
498
|
+
else keys.add(p.key)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
for (const key of keys) {
|
|
502
|
+
let bucket = this.#buckets.get(key)
|
|
503
|
+
if (bucket === undefined) {
|
|
504
|
+
bucket = []
|
|
505
|
+
this.#buckets.set(key, bucket)
|
|
506
|
+
}
|
|
507
|
+
insertSorted(bucket, entry)
|
|
508
|
+
}
|
|
509
|
+
if (needsUnbucketed) insertSorted(this.#unbucketed, entry)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 将条目从索引中摘除
|
|
514
|
+
* @param entry 条目
|
|
515
|
+
*/
|
|
516
|
+
#unindex(entry: Entry): void {
|
|
517
|
+
for (const [key, bucket] of this.#buckets) {
|
|
518
|
+
const at = bucket.indexOf(entry)
|
|
519
|
+
if (at >= 0) bucket.splice(at, 1)
|
|
520
|
+
if (bucket.length === 0) this.#buckets.delete(key)
|
|
521
|
+
}
|
|
522
|
+
const at = this.#unbucketed.indexOf(entry)
|
|
523
|
+
if (at >= 0) this.#unbucketed.splice(at, 1)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 注销一条登记
|
|
528
|
+
* @param reg 登记内容
|
|
529
|
+
*/
|
|
530
|
+
#remove(reg: CommandRegistration): void {
|
|
531
|
+
const entry = this.#entries.get(reg)
|
|
532
|
+
if (entry === undefined) return
|
|
533
|
+
this.#unindex(entry)
|
|
534
|
+
this.#entries.delete(reg)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 取该文本需要试的候选条目(已按匹配顺序排好)
|
|
539
|
+
* @param text 消息纯文本
|
|
540
|
+
* @returns 候选条目数组
|
|
541
|
+
*/
|
|
542
|
+
#candidates(text: string): readonly Entry[] {
|
|
543
|
+
const first = [...text][0]
|
|
544
|
+
const bucket = first === undefined ? undefined : this.#buckets.get(first)
|
|
545
|
+
const loose = this.#unbucketed
|
|
546
|
+
|
|
547
|
+
// 绝大多数消息走这两条零分配的捷径
|
|
548
|
+
if (bucket === undefined) return loose
|
|
549
|
+
if (loose.length === 0) return bucket
|
|
550
|
+
|
|
551
|
+
// 两边都已排序,线性归并即可,不必 concat + sort
|
|
552
|
+
const out: Entry[] = []
|
|
553
|
+
let i = 0
|
|
554
|
+
let j = 0
|
|
555
|
+
while (i < bucket.length && j < loose.length) {
|
|
556
|
+
const a = bucket[i]!
|
|
557
|
+
const b = loose[j]!
|
|
558
|
+
if (a === b) {
|
|
559
|
+
// 一条命令可能同时有可分桶与不可分桶的模式,只保留一次
|
|
560
|
+
out.push(a)
|
|
561
|
+
i++
|
|
562
|
+
j++
|
|
563
|
+
} else if (byPriority(a, b) <= 0) {
|
|
564
|
+
out.push(a)
|
|
565
|
+
i++
|
|
566
|
+
} else {
|
|
567
|
+
out.push(b)
|
|
568
|
+
j++
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
while (i < bucket.length) out.push(bucket[i++]!)
|
|
572
|
+
while (j < loose.length) out.push(loose[j++]!)
|
|
573
|
+
return out
|
|
574
|
+
}
|
|
575
|
+
}
|