@jsbot-tellus/core 2.5.1 → 2.7.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.
Files changed (66) hide show
  1. package/README.md +16 -4
  2. package/dist/core/dispatch/command.d.ts +5 -2
  3. package/dist/core/dispatch/command.d.ts.map +1 -1
  4. package/dist/core/dispatch/command.js +33 -7
  5. package/dist/core/dispatch/command.js.map +1 -1
  6. package/dist/core/dispatch/handler.d.ts +5 -3
  7. package/dist/core/dispatch/handler.d.ts.map +1 -1
  8. package/dist/core/dispatch/handler.js +6 -1
  9. package/dist/core/dispatch/handler.js.map +1 -1
  10. package/dist/core/dispatch/router.d.ts +4 -2
  11. package/dist/core/dispatch/router.d.ts.map +1 -1
  12. package/dist/core/dispatch/router.js +9 -5
  13. package/dist/core/dispatch/router.js.map +1 -1
  14. package/dist/core/index.d.ts +1 -1
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/core/kernel.d.ts +1 -1
  17. package/dist/core/kernel.d.ts.map +1 -1
  18. package/dist/core/kernel.js +4 -1
  19. package/dist/core/kernel.js.map +1 -1
  20. package/dist/core/plugin/context.d.ts.map +1 -1
  21. package/dist/core/plugin/context.js +12 -0
  22. package/dist/core/plugin/context.js.map +1 -1
  23. package/dist/core/plugin/loader.d.ts +1 -1
  24. package/dist/core/plugin/loader.d.ts.map +1 -1
  25. package/dist/core/plugin/loader.js +31 -46
  26. package/dist/core/plugin/loader.js.map +1 -1
  27. package/dist/core/plugin/params/declaration.d.ts.map +1 -1
  28. package/dist/core/plugin/params/declaration.js +67 -4
  29. package/dist/core/plugin/params/declaration.js.map +1 -1
  30. package/dist/core/plugin/params/input.d.ts.map +1 -1
  31. package/dist/core/plugin/params/input.js +17 -2
  32. package/dist/core/plugin/params/input.js.map +1 -1
  33. package/dist/core/plugin/params/type-level.d.ts +2 -1
  34. package/dist/core/plugin/params/type-level.d.ts.map +1 -1
  35. package/dist/core/plugin/params/types.d.ts +5 -0
  36. package/dist/core/plugin/params/types.d.ts.map +1 -1
  37. package/dist/core/plugin/params/types.js.map +1 -1
  38. package/dist/core/plugin/params/values.d.ts +1 -0
  39. package/dist/core/plugin/params/values.d.ts.map +1 -1
  40. package/dist/core/plugin/params/values.js +3 -0
  41. package/dist/core/plugin/params/values.js.map +1 -1
  42. package/dist/core/plugin/types.d.ts +9 -1
  43. package/dist/core/plugin/types.d.ts.map +1 -1
  44. package/dist/core/plugin/types.js.map +1 -1
  45. package/docs/architecture.md +3 -3
  46. package/docs/configuration.md +3 -3
  47. package/docs/docker.md +166 -0
  48. package/docs/feature-parity.md +2 -2
  49. package/docs/migration.md +1 -1
  50. package/docs/plugin-api.md +43 -15
  51. package/docs/plugin-development.md +30 -27
  52. package/docs/release.md +50 -8
  53. package/package.json +2 -2
  54. package/src/core/dispatch/command.ts +53 -1
  55. package/src/core/dispatch/handler.ts +19 -1
  56. package/src/core/dispatch/router.ts +20 -3
  57. package/src/core/index.ts +4 -0
  58. package/src/core/kernel.ts +8 -1
  59. package/src/core/plugin/context.ts +21 -0
  60. package/src/core/plugin/loader.ts +41 -66
  61. package/src/core/plugin/params/declaration.ts +112 -4
  62. package/src/core/plugin/params/input.ts +29 -2
  63. package/src/core/plugin/params/type-level.ts +7 -3
  64. package/src/core/plugin/params/types.ts +6 -0
  65. package/src/core/plugin/params/values.ts +4 -0
  66. package/src/core/plugin/types.ts +20 -0
@@ -4,7 +4,7 @@ import { pathToFileURL } from 'node:url'
4
4
  import fg from 'fast-glob'
5
5
  import type { Logger } from '../logging/logger.js'
6
6
  import { collectPluginExports } from './exports.js'
7
- import { readInstallState, readJsonFile } from './install-dir.js'
7
+ import { readJsonFile } from './install-dir.js'
8
8
  import { NpmCliRunner, type NpmRunner } from './npm-runner.js'
9
9
  import type { AnyPluginDefinition } from './types.js'
10
10
 
@@ -13,7 +13,7 @@ type LoaderLogger = Pick<Logger, 'warn' | 'error'> & {
13
13
  }
14
14
 
15
15
  export interface LocalPluginLoaderOptions {
16
- /** Build local plugins that have a build script but no dist. Defaults to true. */
16
+ /** Rebuild local plugins that have a build script on every load. Defaults to true. */
17
17
  autoBuild?: boolean | undefined
18
18
  /** Timeout for a single local plugin build. Defaults to 120 seconds. */
19
19
  buildTimeoutMs?: number | undefined
@@ -49,19 +49,7 @@ async function readBuildScript(directory: string): Promise<string | undefined> {
49
49
  : undefined
50
50
  }
51
51
 
52
- async function readManagedDirectories(
53
- root: string
54
- ): Promise<Set<string> | null> {
55
- try {
56
- const state = await readInstallState(root)
57
-
58
- return new Set(
59
- Object.values(state.packages).map((record) => record.dir)
60
- )
61
- } catch {
62
- return null
63
- }
64
- }
52
+ type BuildOutcome = 'built' | 'missing-node-modules' | 'failed'
65
53
 
66
54
  async function buildLocalPlugin(
67
55
  directory: string,
@@ -69,22 +57,15 @@ async function buildLocalPlugin(
69
57
  logger: LoaderLogger | undefined,
70
58
  runner: NpmRunner,
71
59
  timeoutMs: number
72
- ): Promise<boolean> {
73
- if ((await readBuildScript(directory)) === undefined) {
74
- logger?.warn(
75
- `skipping local plugin ${name}: dist directory not found and package.json has no build script`
76
- )
77
- return false
78
- }
79
-
60
+ ): Promise<BuildOutcome> {
80
61
  if (!(await directoryExists(join(directory, 'node_modules')))) {
81
62
  logger?.warn(
82
- `skipping local plugin ${name}: dist directory not found and node_modules is missing; run npm install --prefix ${directory}`
63
+ `cannot build local plugin ${name}: node_modules is missing; run npm install --prefix ${directory}`
83
64
  )
84
- return false
65
+ return 'missing-node-modules'
85
66
  }
86
67
 
87
- logger?.info?.(`building local plugin ${name}: dist directory not found`)
68
+ logger?.info?.(`building local plugin ${name}`)
88
69
 
89
70
  try {
90
71
  await runner.run(['run', 'build'], {
@@ -93,13 +74,12 @@ async function buildLocalPlugin(
93
74
  })
94
75
  } catch (error) {
95
76
  logger?.error(`failed to build local plugin ${name}`, error)
96
- logger?.warn(`skipping local plugin ${name}: build failed`)
97
- return false
77
+ return 'failed'
98
78
  }
99
79
 
100
80
  logger?.info?.(`built local plugin ${name}`)
101
81
 
102
- return true
82
+ return 'built'
103
83
  }
104
84
 
105
85
  export async function loadPluginsFromDirectory(
@@ -127,7 +107,14 @@ export async function loadPluginsFromDirectory(
127
107
  let imported: unknown
128
108
 
129
109
  try {
130
- imported = await import(pathToFileURL(file).href)
110
+ const info = await stat(file).catch(() => null)
111
+ const url = pathToFileURL(file)
112
+
113
+ if (info !== null) {
114
+ url.searchParams.set('v', String(info.mtimeMs))
115
+ }
116
+
117
+ imported = await import(url.href)
131
118
  } catch (error) {
132
119
  logger?.error(`failed to import plugin file: ${file}`, error)
133
120
  invalid.push(
@@ -170,22 +157,7 @@ export async function loadLocalPlugins(
170
157
  )
171
158
  const plugins: AnyPluginDefinition[] = []
172
159
 
173
- let autoBuild = options.autoBuild !== false
174
- let managed = new Set<string>()
175
-
176
- if (autoBuild) {
177
- const directories = await readManagedDirectories(absolute)
178
-
179
- if (directories === null) {
180
- logger?.warn(
181
- 'cannot read plugin install state; automatic local plugin build is disabled'
182
- )
183
- autoBuild = false
184
- } else {
185
- managed = directories
186
- }
187
- }
188
-
160
+ const autoBuild = options.autoBuild !== false
189
161
  const runner = options.runner ?? new NpmCliRunner()
190
162
  const timeoutMs = options.buildTimeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS
191
163
 
@@ -195,24 +167,11 @@ export async function loadLocalPlugins(
195
167
 
196
168
  const directory = join(absolute, entry.name)
197
169
  const dist = join(directory, 'dist')
170
+ const hasBuildScript = (await readBuildScript(directory)) !== undefined
198
171
  let distInfo = await stat(dist).catch(() => null)
199
172
 
200
- if (!distInfo?.isDirectory()) {
201
- if (!autoBuild) {
202
- logger?.warn(
203
- `skipping local plugin ${entry.name}: dist directory not found`
204
- )
205
- continue
206
- }
207
-
208
- if (managed.has(entry.name)) {
209
- logger?.warn(
210
- `skipping local plugin ${entry.name}: dist directory not found (installed package is expected to include dist)`
211
- )
212
- continue
213
- }
214
-
215
- const built = await buildLocalPlugin(
173
+ if (autoBuild && hasBuildScript) {
174
+ const outcome = await buildLocalPlugin(
216
175
  directory,
217
176
  entry.name,
218
177
  logger,
@@ -220,16 +179,32 @@ export async function loadLocalPlugins(
220
179
  timeoutMs
221
180
  )
222
181
 
223
- if (!built) continue
224
-
225
182
  distInfo = await stat(dist).catch(() => null)
226
183
 
227
184
  if (!distInfo?.isDirectory()) {
185
+ const reason =
186
+ outcome === 'built'
187
+ ? 'build finished but dist directory not found'
188
+ : outcome === 'missing-node-modules'
189
+ ? 'node_modules is missing and dist directory not found'
190
+ : 'build failed and dist directory not found'
191
+
192
+ logger?.warn(`skipping local plugin ${entry.name}: ${reason}`)
193
+ continue
194
+ }
195
+
196
+ if (outcome !== 'built') {
228
197
  logger?.warn(
229
- `skipping local plugin ${entry.name}: build finished but dist directory not found`
198
+ `using existing dist for local plugin ${entry.name}: rebuild failed`
230
199
  )
231
- continue
232
200
  }
201
+ } else if (!distInfo?.isDirectory()) {
202
+ logger?.warn(
203
+ hasBuildScript
204
+ ? `skipping local plugin ${entry.name}: dist directory not found and automatic build is disabled`
205
+ : `skipping local plugin ${entry.name}: dist directory not found and package.json has no build script`
206
+ )
207
+ continue
233
208
  }
234
209
 
235
210
  plugins.push(...(await loadPluginsFromDirectory(dist, logger)))
@@ -1,11 +1,18 @@
1
1
  import {
2
2
  declarationError,
3
3
  type CommandParamType,
4
+ type ParamLength,
4
5
  type ParamSpec,
5
6
  } from './types.js'
6
- import { parseBoolean, parseTimeValue, unquote } from './values.js'
7
+ import {
8
+ measureLength,
9
+ parseBoolean,
10
+ parseTimeValue,
11
+ unquote,
12
+ } from './values.js'
7
13
 
8
14
  const NAME_PATTERN = /^[\p{L}_][\p{L}\p{N}_-]*$/u
15
+ const LENGTH_PATTERN = /^(\d*)\.\.(\d*)$/
9
16
 
10
17
  export function parseType(value: string): CommandParamType {
11
18
  switch (value) {
@@ -23,6 +30,90 @@ export function parseType(value: string): CommandParamType {
23
30
  }
24
31
  }
25
32
 
33
+ interface ParsedType {
34
+ type: CommandParamType
35
+ length?: ParamLength | undefined
36
+ }
37
+
38
+ function parseLengthNumber(raw: string, token: string): number {
39
+ const value = Number(raw)
40
+
41
+ if (!Number.isSafeInteger(value)) {
42
+ throw declarationError(`长度限制不是安全的整数:${token}`)
43
+ }
44
+
45
+ return value
46
+ }
47
+
48
+ function parseLength(raw: string, token: string): ParamLength {
49
+ const value = raw.trim()
50
+ const match = LENGTH_PATTERN.exec(value)
51
+
52
+ if (!match) {
53
+ throw declarationError(`非法的长度限制 "${raw}":${token}`)
54
+ }
55
+
56
+ const minRaw = match[1] ?? ''
57
+ const maxRaw = match[2] ?? ''
58
+
59
+ if (minRaw === '' && maxRaw === '') {
60
+ throw declarationError(`长度限制不能为空:${token}`)
61
+ }
62
+
63
+ const min = minRaw === '' ? undefined : parseLengthNumber(minRaw, token)
64
+ const max = maxRaw === '' ? undefined : parseLengthNumber(maxRaw, token)
65
+
66
+ if (min !== undefined && max !== undefined && min > max) {
67
+ throw declarationError(`长度下限不能大于上限:${token}`)
68
+ }
69
+
70
+ return { min, max }
71
+ }
72
+
73
+ function parseTypeDeclaration(raw: string, token: string): ParsedType {
74
+ const value = raw.trim()
75
+ const open = value.indexOf('(')
76
+
77
+ if (open < 0) {
78
+ return { type: parseType(value) }
79
+ }
80
+
81
+ if (!value.endsWith(')')) {
82
+ throw declarationError(`非法的参数声明 "${token}"`)
83
+ }
84
+
85
+ const type = parseType(value.slice(0, open).trim())
86
+ const length = parseLength(value.slice(open + 1, -1), token)
87
+
88
+ if (type !== 'string' && type !== 'text') {
89
+ throw declarationError(`只有 string/text 参数支持长度限制:${token}`)
90
+ }
91
+
92
+ return { type, length }
93
+ }
94
+
95
+ function validateDefaultLength(
96
+ name: string,
97
+ value: string,
98
+ length: ParamLength | undefined
99
+ ): void {
100
+ if (length === undefined) return
101
+
102
+ const measured = measureLength(value)
103
+
104
+ if (length.min !== undefined && measured < length.min) {
105
+ throw declarationError(
106
+ `参数 ${name} 的默认值至少需要 ${length.min} 个字符`
107
+ )
108
+ }
109
+
110
+ if (length.max !== undefined && measured > length.max) {
111
+ throw declarationError(
112
+ `参数 ${name} 的默认值不能超过 ${length.max} 个字符`
113
+ )
114
+ }
115
+ }
116
+
26
117
  export function validateName(value: string): string {
27
118
  const name = value.trim()
28
119
 
@@ -120,7 +211,13 @@ export function parseToken(token: string): ParamSpec {
120
211
  throw declarationError('变长参数必须使用 [ ]')
121
212
  }
122
213
 
123
- const name = validateName(inner.slice(3))
214
+ const rest = inner.slice(3)
215
+
216
+ if (rest.includes('(')) {
217
+ throw declarationError(`变长参数不支持长度限制:${token}`)
218
+ }
219
+
220
+ const name = validateName(rest)
124
221
 
125
222
  return {
126
223
  name,
@@ -140,20 +237,30 @@ export function parseToken(token: string): ParamSpec {
140
237
  const defaultRaw = equals >= 0 ? inner.slice(equals + 1).trim() : undefined
141
238
  const colon = head.indexOf(':')
142
239
  const name = validateName(colon >= 0 ? head.slice(0, colon) : head)
143
- const type = parseType(colon >= 0 ? head.slice(colon + 1).trim() : 'string')
240
+ const { type, length } = parseTypeDeclaration(
241
+ colon >= 0 ? head.slice(colon + 1) : 'string',
242
+ token
243
+ )
144
244
 
145
245
  if (required && defaultRaw !== undefined) {
146
246
  throw declarationError(`必填参数不能有默认值:${token}`)
147
247
  }
148
248
 
149
249
  if (defaultRaw !== undefined) {
250
+ const defaultValue = parseDefaultValue(type, defaultRaw, name)
251
+
252
+ if (typeof defaultValue === 'string') {
253
+ validateDefaultLength(name, defaultValue, length)
254
+ }
255
+
150
256
  return {
151
257
  name,
152
258
  type,
153
259
  required,
154
260
  rest: false,
155
261
  hasDefault: true,
156
- defaultValue: parseDefaultValue(type, defaultRaw, name),
262
+ defaultValue,
263
+ length,
157
264
  }
158
265
  }
159
266
 
@@ -163,6 +270,7 @@ export function parseToken(token: string): ParamSpec {
163
270
  required,
164
271
  rest: false,
165
272
  hasDefault: false,
273
+ length,
166
274
  }
167
275
  }
168
276
 
@@ -4,7 +4,7 @@ import {
4
4
  type ParamSpec,
5
5
  type ParsedCommandParams,
6
6
  } from './types.js'
7
- import { parseAt, parseBoolean, parseTime } from './values.js'
7
+ import { measureLength, parseAt, parseBoolean, parseTime } from './values.js'
8
8
 
9
9
  export function tokenizeInput(input: string, usage: string): InputToken[] {
10
10
  const tokens: InputToken[] = []
@@ -101,6 +101,32 @@ export function tokenizeInput(input: string, usage: string): InputToken[] {
101
101
  return tokens
102
102
  }
103
103
 
104
+ function enforceLength(spec: ParamSpec, value: string, usage: string): string {
105
+ const length = spec.length
106
+
107
+ if (length === undefined) {
108
+ return value
109
+ }
110
+
111
+ const measured = measureLength(value)
112
+
113
+ if (length.min !== undefined && measured < length.min) {
114
+ throw new CommandParamsError(
115
+ `参数 ${spec.name} 至少需要 ${length.min} 个字符`,
116
+ usage
117
+ )
118
+ }
119
+
120
+ if (length.max !== undefined && measured > length.max) {
121
+ throw new CommandParamsError(
122
+ `参数 ${spec.name} 不能超过 ${length.max} 个字符`,
123
+ usage
124
+ )
125
+ }
126
+
127
+ return value
128
+ }
129
+
104
130
  export function parseValue(
105
131
  spec: ParamSpec,
106
132
  raw: string,
@@ -108,6 +134,7 @@ export function parseValue(
108
134
  ): unknown {
109
135
  switch (spec.type) {
110
136
  case 'string':
137
+ return enforceLength(spec, raw, usage)
111
138
  case 'text':
112
139
  return raw
113
140
  case 'number': {
@@ -218,7 +245,7 @@ export function parseInput(
218
245
  const value =
219
246
  remaining === 1 ? token.value : input.slice(token.start).trim()
220
247
 
221
- args.push(value)
248
+ args.push(enforceLength(spec, value, usage))
222
249
  index = tokens.length
223
250
  continue
224
251
  }
@@ -8,12 +8,16 @@ type TrimRight<Source extends string> =
8
8
 
9
9
  type Trim<Source extends string> = TrimLeft<TrimRight<Source>>
10
10
 
11
+ type StripLength<Type extends string> =
12
+ Trim<Type> extends `${infer Base}(${string})` ? Trim<Base> : Trim<Type>
13
+
11
14
  type ParamValue<Type extends string> =
12
- Trim<Type> extends 'string' | 'text'
15
+ StripLength<Type> extends 'string' | 'text'
13
16
  ? string
14
- : Trim<Type> extends 'number' | 'integer' | 'at' | 'reply' | 'time'
17
+ : StripLength<Type> extends
18
+ 'number' | 'integer' | 'at' | 'reply' | 'time'
15
19
  ? number
16
- : Trim<Type> extends 'boolean'
20
+ : StripLength<Type> extends 'boolean'
17
21
  ? boolean
18
22
  : unknown
19
23
 
@@ -30,6 +30,11 @@ export class CommandParamsError extends Error {
30
30
  }
31
31
  }
32
32
 
33
+ export interface ParamLength {
34
+ min?: number | undefined
35
+ max?: number | undefined
36
+ }
37
+
33
38
  export interface ParamSpec {
34
39
  name: string
35
40
  type: CommandParamType
@@ -37,6 +42,7 @@ export interface ParamSpec {
37
42
  rest: boolean
38
43
  hasDefault: boolean
39
44
  defaultValue?: unknown
45
+ length?: ParamLength | undefined
40
46
  }
41
47
 
42
48
  export interface InputToken {
@@ -37,6 +37,10 @@ export function unquote(value: string): string {
37
37
  return result
38
38
  }
39
39
 
40
+ export function measureLength(value: string): number {
41
+ return Array.from(value).length
42
+ }
43
+
40
44
  export function parseBoolean(value: string): boolean | null {
41
45
  const normalized = value.trim().toLowerCase()
42
46
 
@@ -1,6 +1,8 @@
1
1
  import type {
2
+ OneBotGroupMessageEvent,
2
3
  OneBotMessageEvent,
3
4
  OneBotNoticeEvent,
5
+ OneBotPrivateMessageEvent,
4
6
  OneBotRequestEvent,
5
7
  OutgoingMessage,
6
8
  SnowLumaEvent,
@@ -17,6 +19,10 @@ import type { CommandParamArgs } from './command-params.js'
17
19
 
18
20
  export type EventType = 'message' | 'notice' | 'request' | 'meta'
19
21
 
22
+ export type MessageType = 'group' | 'private'
23
+
24
+ export type MessageTypeFilter = MessageType | readonly MessageType[]
25
+
20
26
  export interface CommandMatch {
21
27
  command: string
22
28
  prefix: string
@@ -28,6 +34,7 @@ export interface CommandInfo {
28
34
  name: string
29
35
  aliases: string[]
30
36
  permission: Permission
37
+ messageTypes: MessageType[]
31
38
  description?: string | undefined
32
39
  usage?: string | undefined
33
40
  pluginId: string
@@ -59,6 +66,16 @@ export type MessageHandler = (
59
66
  context: EventContext<OneBotMessageEvent>
60
67
  ) => void | Promise<void>
61
68
 
69
+ export type GroupMessageHandler = (
70
+ event: OneBotGroupMessageEvent,
71
+ context: EventContext<OneBotGroupMessageEvent>
72
+ ) => void | Promise<void>
73
+
74
+ export type PrivateMessageHandler = (
75
+ event: OneBotPrivateMessageEvent,
76
+ context: EventContext<OneBotPrivateMessageEvent>
77
+ ) => void | Promise<void>
78
+
62
79
  export type NoticeHandler = (
63
80
  event: OneBotNoticeEvent,
64
81
  context: EventContext<OneBotNoticeEvent>
@@ -80,6 +97,7 @@ export interface CommandOptions<Declaration extends string = string> {
80
97
  aliases?: string[] | undefined
81
98
  prefixes?: string[] | undefined
82
99
  permission?: Permission | undefined
100
+ messageType?: MessageTypeFilter | undefined
83
101
  description?: string | undefined
84
102
  params?: Declaration | undefined
85
103
  }
@@ -127,6 +145,8 @@ export interface CommandRegistrar extends CommandGroupRegistrar {
127
145
 
128
146
  export interface EventRegistrar {
129
147
  onMessage(handler: MessageHandler): () => void
148
+ onGroupMessage(handler: GroupMessageHandler): () => void
149
+ onPrivateMessage(handler: PrivateMessageHandler): () => void
130
150
  onNotice(options: NoticeOptions, handler: NoticeHandler): () => void
131
151
  onRequest(options: RequestOptions, handler: RequestHandler): () => void
132
152
  }