@jsbot-tellus/core 2.0.3 → 2.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.
Files changed (78) hide show
  1. package/README.md +13 -17
  2. package/dist/app.js +12 -40
  3. package/dist/app.js.map +1 -1
  4. package/dist/core/config/schema.d.ts +0 -2
  5. package/dist/core/config/schema.d.ts.map +1 -1
  6. package/dist/core/config/schema.js +0 -1
  7. package/dist/core/config/schema.js.map +1 -1
  8. package/dist/core/dispatch/command.d.ts +24 -2
  9. package/dist/core/dispatch/command.d.ts.map +1 -1
  10. package/dist/core/dispatch/command.js +297 -60
  11. package/dist/core/dispatch/command.js.map +1 -1
  12. package/dist/core/dispatch/router.d.ts +17 -3
  13. package/dist/core/dispatch/router.d.ts.map +1 -1
  14. package/dist/core/dispatch/router.js +38 -2
  15. package/dist/core/dispatch/router.js.map +1 -1
  16. package/dist/core/index.d.ts +1 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/plugin/command-params.d.ts +15 -0
  19. package/dist/core/plugin/command-params.d.ts.map +1 -0
  20. package/dist/core/plugin/command-params.js +427 -0
  21. package/dist/core/plugin/command-params.js.map +1 -0
  22. package/dist/core/plugin/context.d.ts.map +1 -1
  23. package/dist/core/plugin/context.js +27 -0
  24. package/dist/core/plugin/context.js.map +1 -1
  25. package/dist/core/plugin/install-dir.d.ts +6 -9
  26. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  27. package/dist/core/plugin/install-dir.js +99 -79
  28. package/dist/core/plugin/install-dir.js.map +1 -1
  29. package/dist/core/plugin/install-validate.d.ts +2 -1
  30. package/dist/core/plugin/install-validate.d.ts.map +1 -1
  31. package/dist/core/plugin/install-validate.js +25 -17
  32. package/dist/core/plugin/install-validate.js.map +1 -1
  33. package/dist/core/plugin/installer.d.ts +3 -3
  34. package/dist/core/plugin/installer.d.ts.map +1 -1
  35. package/dist/core/plugin/installer.js +143 -140
  36. package/dist/core/plugin/installer.js.map +1 -1
  37. package/dist/core/plugin/loader.d.ts +1 -0
  38. package/dist/core/plugin/loader.d.ts.map +1 -1
  39. package/dist/core/plugin/loader.js +24 -2
  40. package/dist/core/plugin/loader.js.map +1 -1
  41. package/dist/core/plugin/types.d.ts +11 -3
  42. package/dist/core/plugin/types.d.ts.map +1 -1
  43. package/dist/core/plugin/types.js.map +1 -1
  44. package/dist/plugins/admin/index.d.ts.map +1 -1
  45. package/dist/plugins/admin/index.js +119 -86
  46. package/dist/plugins/admin/index.js.map +1 -1
  47. package/dist/plugins/core/index.d.ts.map +1 -1
  48. package/dist/plugins/core/index.js +4 -1
  49. package/dist/plugins/core/index.js.map +1 -1
  50. package/docs/architecture.md +22 -24
  51. package/docs/configuration.md +19 -23
  52. package/docs/feature-parity.md +4 -4
  53. package/docs/migration.md +13 -13
  54. package/docs/plugin-api.md +156 -26
  55. package/docs/plugin-development.md +390 -0
  56. package/docs/release.md +2 -2
  57. package/examples/plugins/hello/README.md +10 -5
  58. package/examples/plugins/hello/index.js +3 -6
  59. package/package.json +2 -1
  60. package/src/app.ts +16 -55
  61. package/src/core/config/schema.ts +0 -1
  62. package/src/core/dispatch/command.ts +474 -70
  63. package/src/core/dispatch/router.ts +77 -3
  64. package/src/core/index.ts +2 -0
  65. package/src/core/plugin/command-params.ts +595 -0
  66. package/src/core/plugin/context.ts +47 -2
  67. package/src/core/plugin/install-dir.ts +117 -111
  68. package/src/core/plugin/install-validate.ts +36 -34
  69. package/src/core/plugin/installer.ts +228 -184
  70. package/src/core/plugin/loader.ts +36 -2
  71. package/src/core/plugin/types.ts +23 -3
  72. package/src/plugins/admin/index.ts +302 -233
  73. package/src/plugins/core/index.ts +5 -1
  74. package/dist/core/plugin/installed.d.ts +0 -9
  75. package/dist/core/plugin/installed.d.ts.map +0 -1
  76. package/dist/core/plugin/installed.js +0 -113
  77. package/dist/core/plugin/installed.js.map +0 -1
  78. package/src/core/plugin/installed.ts +0 -165
@@ -10,11 +10,10 @@ import {
10
10
  symlink,
11
11
  writeFile,
12
12
  } from 'node:fs/promises'
13
- import { dirname, join, resolve } from 'node:path'
13
+ import { dirname, join } from 'node:path'
14
14
  import { fileURLToPath } from 'node:url'
15
15
 
16
16
  export const CORE_PACKAGE_NAME = '@jsbot-tellus/core'
17
- export const INSTALL_PACKAGE_NAME = '@jsbot-tellus/installed-plugins'
18
17
  export const INSTALL_STATE_FILE = '.jsbot-installed.json'
19
18
  export const VALIDATE_SCRIPT_FILE = 'validate.mjs'
20
19
 
@@ -24,10 +23,12 @@ export interface InstalledPackageRecord {
24
23
  pluginIds: string[]
25
24
  dependencies: Record<string, string[]>
26
25
  installedAt: string
26
+ /** Directory name under the plugins root. */
27
+ dir: string
27
28
  }
28
29
 
29
30
  export interface InstallState {
30
- version: 1
31
+ version: 2
31
32
  packages: Record<string, InstalledPackageRecord>
32
33
  }
33
34
 
@@ -38,25 +39,8 @@ export class InstallStateError extends Error {
38
39
  }
39
40
  }
40
41
 
41
- const INSTALL_MANIFEST = {
42
- name: INSTALL_PACKAGE_NAME,
43
- private: true,
44
- version: '0.0.0',
45
- type: 'module',
46
- dependencies: {},
47
- }
48
-
49
- export function resolveInstallDir(
50
- config: { dir?: string | undefined },
51
- dataDir: string
52
- ): string {
53
- return config.dir
54
- ? resolve(process.cwd(), config.dir)
55
- : join(dataDir, 'plugins')
56
- }
57
-
58
- export function packageDir(dir: string, name: string): string {
59
- return join(dir, 'node_modules', name)
42
+ export function packageDirectoryName(name: string): string {
43
+ return name.startsWith('@') ? name.replace('/', '__') : name
60
44
  }
61
45
 
62
46
  export async function readJsonFile(path: string): Promise<unknown> {
@@ -69,68 +53,12 @@ export async function readJsonFile(path: string): Promise<unknown> {
69
53
  }
70
54
  }
71
55
 
72
- export async function ensureInstallDir(dir: string): Promise<void> {
73
- await mkdir(dir, { recursive: true })
74
-
75
- const manifestPath = join(dir, 'package.json')
76
- const manifest = await readJsonFile(manifestPath)
77
-
78
- if (manifest === undefined) {
79
- await writeFile(
80
- manifestPath,
81
- `${JSON.stringify(INSTALL_MANIFEST, null, 4)}\n`
82
- )
83
- } else if (
84
- typeof manifest !== 'object' ||
85
- manifest === null ||
86
- Array.isArray(manifest)
87
- ) {
88
- throw new Error(`安装目录的 package.json 无效: ${manifestPath}`)
89
- }
90
-
91
- await mkdir(join(dir, '.jsbot'), { recursive: true })
92
- await writeFile(
93
- join(dir, '.jsbot', VALIDATE_SCRIPT_FILE),
94
- VALIDATE_SCRIPT,
95
- 'utf8'
96
- )
97
- }
98
-
99
- export async function readDependencies(
100
- dir: string
101
- ): Promise<Record<string, string>> {
102
- const manifest = await readJsonFile(join(dir, 'package.json'))
103
-
104
- if (typeof manifest !== 'object' || manifest === null) return {}
105
-
106
- const dependencies = (manifest as Record<string, unknown>)['dependencies']
107
-
108
- if (
109
- typeof dependencies !== 'object' ||
110
- dependencies === null ||
111
- Array.isArray(dependencies)
112
- ) {
113
- return {}
114
- }
115
-
116
- const result: Record<string, string> = {}
117
-
118
- for (const [name, value] of Object.entries(dependencies)) {
119
- if (typeof value === 'string') {
120
- result[name] = value
121
- }
122
- }
123
-
124
- return result
125
- }
126
-
127
- export async function readInstalledVersion(
128
- dir: string,
129
- name: string
56
+ export async function readPackageVersion(
57
+ directory: string
130
58
  ): Promise<string | undefined> {
131
- const manifest = await readJsonFile(
132
- join(packageDir(dir, name), 'package.json')
133
- ).catch(() => undefined)
59
+ const manifest = await readJsonFile(join(directory, 'package.json')).catch(
60
+ () => undefined
61
+ )
134
62
 
135
63
  if (typeof manifest !== 'object' || manifest === null) return undefined
136
64
 
@@ -139,6 +67,16 @@ export async function readInstalledVersion(
139
67
  return typeof version === 'string' ? version : undefined
140
68
  }
141
69
 
70
+ function isSafeDirectoryName(value: string): boolean {
71
+ return (
72
+ value.length > 0 &&
73
+ value !== '.' &&
74
+ value !== '..' &&
75
+ !value.includes('/') &&
76
+ !value.includes('\\')
77
+ )
78
+ }
79
+
142
80
  export async function readInstallState(dir: string): Promise<InstallState> {
143
81
  const path = join(dir, INSTALL_STATE_FILE)
144
82
  let value: unknown
@@ -152,14 +90,14 @@ export async function readInstallState(dir: string): Promise<InstallState> {
152
90
  }
153
91
 
154
92
  if (value === undefined) {
155
- return { version: 1, packages: {} }
93
+ return { version: 2, packages: {} }
156
94
  }
157
95
 
158
96
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
159
97
  throw new InstallStateError(`安装状态文件格式无效: ${path}`)
160
98
  }
161
99
 
162
- if ((value as Record<string, unknown>)['version'] !== 1) {
100
+ if ((value as Record<string, unknown>)['version'] !== 2) {
163
101
  throw new InstallStateError(`不支持的安装状态版本,请检查 ${path}`)
164
102
  }
165
103
 
@@ -174,6 +112,7 @@ export async function readInstallState(dir: string): Promise<InstallState> {
174
112
  }
175
113
 
176
114
  const result: Record<string, InstalledPackageRecord> = {}
115
+ const directories = new Set<string>()
177
116
 
178
117
  for (const [name, rawRecord] of Object.entries(packages)) {
179
118
  if (
@@ -187,17 +126,28 @@ export async function readInstallState(dir: string): Promise<InstallState> {
187
126
  const record = rawRecord as Record<string, unknown>
188
127
  const pluginIds = record['pluginIds']
189
128
  const dependencies = record['dependencies']
129
+ const directory = record['dir']
190
130
 
191
131
  if (
192
132
  typeof record['spec'] !== 'string' ||
193
133
  typeof record['version'] !== 'string' ||
194
134
  typeof record['installedAt'] !== 'string' ||
135
+ typeof directory !== 'string' ||
136
+ !isSafeDirectoryName(directory) ||
195
137
  !Array.isArray(pluginIds) ||
196
138
  pluginIds.some((item) => typeof item !== 'string')
197
139
  ) {
198
140
  throw new InstallStateError(`安装状态记录不完整: ${name}`)
199
141
  }
200
142
 
143
+ if (directories.has(directory)) {
144
+ throw new InstallStateError(
145
+ `安装状态存在重复的插件目录: ${directory}`
146
+ )
147
+ }
148
+
149
+ directories.add(directory)
150
+
201
151
  if (
202
152
  typeof dependencies !== 'object' ||
203
153
  dependencies === null ||
@@ -227,17 +177,18 @@ export async function readInstallState(dir: string): Promise<InstallState> {
227
177
  pluginIds: [...pluginIds],
228
178
  dependencies: parsedDependencies,
229
179
  installedAt: record['installedAt'],
180
+ dir: directory,
230
181
  }
231
182
  }
232
183
 
233
- return { version: 1, packages: result }
184
+ return { version: 2, packages: result }
234
185
  }
235
186
 
236
187
  export async function writeInstallState(
237
188
  dir: string,
238
189
  state: InstallState
239
190
  ): Promise<void> {
240
- const sorted: InstallState = { version: 1, packages: {} }
191
+ const sorted: InstallState = { version: 2, packages: {} }
241
192
 
242
193
  for (const name of Object.keys(state.packages).sort()) {
243
194
  const record = state.packages[name]
@@ -312,10 +263,57 @@ export async function pathExists(path: string): Promise<boolean> {
312
263
  return (await stat(path).catch(() => null)) !== null
313
264
  }
314
265
 
315
- const VALIDATE_SCRIPT = `import process from 'node:process'
266
+ export async function ensureInstallRoot(root: string): Promise<void> {
267
+ await mkdir(root, { recursive: true })
268
+ await mkdir(join(root, '.jsbot'), { recursive: true })
269
+ await writeFile(
270
+ join(root, '.jsbot', VALIDATE_SCRIPT_FILE),
271
+ VALIDATE_SCRIPT,
272
+ 'utf8'
273
+ )
274
+ }
275
+
276
+ const VALIDATE_SCRIPT = `import { readdir } from 'node:fs/promises'
277
+ import { extname, join } from 'node:path'
278
+ import { pathToFileURL } from 'node:url'
279
+
280
+ const directory = process.argv[2]
281
+
282
+ if (!directory) {
283
+ console.error('missing plugin directory')
284
+ process.exit(2)
285
+ }
286
+
287
+ const extensions = new Set(['.js', '.mjs', '.cjs'])
288
+
289
+ async function filesIn(directory) {
290
+ const entries = await readdir(directory, { withFileTypes: true })
291
+ entries.sort((a, b) => a.name.localeCompare(b.name))
292
+
293
+ const files = []
294
+
295
+ for (const entry of entries) {
296
+ if (entry.name.startsWith('.')) continue
297
+
298
+ const path = join(directory, entry.name)
299
+
300
+ if (entry.isDirectory()) {
301
+ if (entry.name === 'node_modules') continue
302
+ files.push(...(await filesIn(path)))
303
+ continue
304
+ }
305
+
306
+ if (
307
+ entry.isFile() &&
308
+ extensions.has(extname(entry.name)) &&
309
+ !/\\.d\\.(ts|mts|cts)$/.test(entry.name)
310
+ ) {
311
+ files.push(path)
312
+ }
313
+ }
316
314
 
317
- const name = process.argv[2]
318
- const entry = process.argv[3]
315
+ return files
316
+ }
319
317
 
320
318
  function isPlugin(value) {
321
319
  if (typeof value !== 'object' || value === null) return false
@@ -367,40 +365,48 @@ function collect(value, result) {
367
365
  }
368
366
  }
369
367
 
370
- if (!name) {
371
- console.error('missing package name')
372
- process.exit(2)
373
- }
368
+ const files = await filesIn(directory).catch((error) => {
369
+ console.error(error instanceof Error ? error.message : String(error))
370
+ process.exit(3)
371
+ })
374
372
 
375
- if (!entry) {
376
- console.error('missing package entry')
377
- process.exit(2)
373
+ if (files.length === 0) {
374
+ console.error('插件 dist 目录中没有可加载的文件')
375
+ process.exit(4)
378
376
  }
379
377
 
380
- let imported
378
+ const found = []
379
+ const invalid = []
381
380
 
382
- try {
383
- imported = await import(entry)
384
- } catch (error) {
385
- console.error(error instanceof Error ? error.stack ?? error.message : String(error))
386
- process.exit(3)
387
- }
381
+ for (const file of files) {
382
+ let imported
388
383
 
389
- const found = []
384
+ try {
385
+ imported = await import(pathToFileURL(file).href)
386
+ } catch (error) {
387
+ invalid.push(
388
+ \`failed to import \${file}: \${error instanceof Error ? error.message : String(error)}\`
389
+ )
390
+ continue
391
+ }
392
+
393
+ if (typeof imported !== 'object' || imported === null) continue
390
394
 
391
- if (typeof imported === 'object' && imported !== null) {
392
395
  for (const candidate of [imported.default, imported.plugin, imported.plugins]) {
393
396
  collect(candidate, found)
394
397
  }
395
398
  }
396
399
 
397
- if (found.length === 0) {
398
- console.error('package does not export a JsBot plugin (default / plugin / plugins)')
399
- process.exit(4)
400
- }
401
-
402
400
  const unique = found.filter((plugin, index) => found.indexOf(plugin) === index)
403
401
 
402
+ if (unique.length === 0) {
403
+ console.error(
404
+ invalid[0] ??
405
+ 'package does not export a JsBot plugin (default / plugin / plugins)'
406
+ )
407
+ process.exit(5)
408
+ }
409
+
404
410
  console.log(
405
411
  '__JSBOT_PLUGIN__' +
406
412
  JSON.stringify(
@@ -1,11 +1,6 @@
1
- import { join } from 'node:path'
2
- import { pathToFileURL } from 'node:url'
3
- import {
4
- VALIDATE_SCRIPT_FILE,
5
- packageDir,
6
- readJsonFile,
7
- } from './install-dir.js'
8
- import { packageEntry } from './installed.js'
1
+ import { stat } from 'node:fs/promises'
2
+ import { dirname, join, resolve } from 'node:path'
3
+ import { pathExists, VALIDATE_SCRIPT_FILE } from './install-dir.js'
9
4
  import { runProcess } from './npm-runner.js'
10
5
 
11
6
  export interface ValidatedPlugin {
@@ -17,10 +12,29 @@ export interface ValidatedPlugin {
17
12
  export interface ValidateOptions {
18
13
  timeoutMs: number
19
14
  signal?: AbortSignal | undefined
15
+ script?: string | undefined
16
+ }
17
+
18
+ async function findValidationScript(directory: string): Promise<string> {
19
+ let current = dirname(directory)
20
+
21
+ for (;;) {
22
+ const script = join(current, '.jsbot', VALIDATE_SCRIPT_FILE)
23
+
24
+ if (await pathExists(script)) return script
25
+
26
+ const parent = dirname(current)
27
+
28
+ if (parent === current) break
29
+
30
+ current = parent
31
+ }
32
+
33
+ throw new Error('找不到插件校验脚本,请重新执行安装')
20
34
  }
21
35
 
22
36
  export type PackageValidator = (
23
- dir: string,
37
+ directory: string,
24
38
  name: string,
25
39
  options: ValidateOptions
26
40
  ) => Promise<ValidatedPlugin[]>
@@ -28,36 +42,24 @@ export type PackageValidator = (
28
42
  export const PLUGIN_MARKER = '__JSBOT_PLUGIN__'
29
43
 
30
44
  export const validateInstalledPackage: PackageValidator = async (
31
- dir,
45
+ directory,
32
46
  name,
33
47
  options
34
48
  ) => {
35
- const script = join(dir, '.jsbot', VALIDATE_SCRIPT_FILE)
36
- const location = packageDir(dir, name)
37
- const manifest = await readJsonFile(join(location, 'package.json'))
38
-
39
- if (
40
- typeof manifest !== 'object' ||
41
- manifest === null ||
42
- Array.isArray(manifest)
43
- ) {
44
- throw new Error(`插件包缺少有效的 package.json: ${name}`)
49
+ const absolute = resolve(directory)
50
+ const dist = join(absolute, 'dist')
51
+ const info = await stat(dist).catch(() => null)
52
+
53
+ if (!info?.isDirectory()) {
54
+ throw new Error(`插件包缺少 dist 目录: ${name}`)
45
55
  }
46
56
 
47
- // Resolve the same entry the runtime loader will import. Importing the
48
- // package name here would fail for packages that only expose `require`.
49
- const entry = pathToFileURL(
50
- packageEntry(location, manifest as Record<string, unknown>)
51
- ).href
52
- const { stdout } = await runProcess(
53
- process.execPath,
54
- [script, name, entry],
55
- {
56
- cwd: dir,
57
- timeoutMs: options.timeoutMs,
58
- ...(options.signal !== undefined ? { signal: options.signal } : {}),
59
- }
60
- )
57
+ const script = options.script ?? (await findValidationScript(absolute))
58
+ const { stdout } = await runProcess(process.execPath, [script, absolute], {
59
+ cwd: absolute,
60
+ timeoutMs: options.timeoutMs,
61
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
62
+ })
61
63
 
62
64
  const line = stdout
63
65
  .split('\n')