@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.
- package/README.md +13 -17
- package/dist/app.js +12 -40
- package/dist/app.js.map +1 -1
- package/dist/core/config/schema.d.ts +0 -2
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +0 -1
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/dispatch/command.d.ts +24 -2
- package/dist/core/dispatch/command.d.ts.map +1 -1
- package/dist/core/dispatch/command.js +297 -60
- package/dist/core/dispatch/command.js.map +1 -1
- package/dist/core/dispatch/router.d.ts +17 -3
- package/dist/core/dispatch/router.d.ts.map +1 -1
- package/dist/core/dispatch/router.js +38 -2
- package/dist/core/dispatch/router.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/plugin/command-params.d.ts +15 -0
- package/dist/core/plugin/command-params.d.ts.map +1 -0
- package/dist/core/plugin/command-params.js +427 -0
- package/dist/core/plugin/command-params.js.map +1 -0
- package/dist/core/plugin/context.d.ts.map +1 -1
- package/dist/core/plugin/context.js +27 -0
- package/dist/core/plugin/context.js.map +1 -1
- package/dist/core/plugin/install-dir.d.ts +6 -9
- package/dist/core/plugin/install-dir.d.ts.map +1 -1
- package/dist/core/plugin/install-dir.js +99 -79
- package/dist/core/plugin/install-dir.js.map +1 -1
- package/dist/core/plugin/install-validate.d.ts +2 -1
- package/dist/core/plugin/install-validate.d.ts.map +1 -1
- package/dist/core/plugin/install-validate.js +25 -17
- package/dist/core/plugin/install-validate.js.map +1 -1
- package/dist/core/plugin/installer.d.ts +3 -3
- package/dist/core/plugin/installer.d.ts.map +1 -1
- package/dist/core/plugin/installer.js +143 -140
- package/dist/core/plugin/installer.js.map +1 -1
- package/dist/core/plugin/loader.d.ts +1 -0
- package/dist/core/plugin/loader.d.ts.map +1 -1
- package/dist/core/plugin/loader.js +24 -2
- package/dist/core/plugin/loader.js.map +1 -1
- package/dist/core/plugin/types.d.ts +11 -3
- package/dist/core/plugin/types.d.ts.map +1 -1
- package/dist/core/plugin/types.js.map +1 -1
- package/dist/plugins/admin/index.d.ts.map +1 -1
- package/dist/plugins/admin/index.js +119 -86
- package/dist/plugins/admin/index.js.map +1 -1
- package/dist/plugins/core/index.d.ts.map +1 -1
- package/dist/plugins/core/index.js +4 -1
- package/dist/plugins/core/index.js.map +1 -1
- package/docs/architecture.md +22 -24
- package/docs/configuration.md +19 -23
- package/docs/feature-parity.md +4 -4
- package/docs/migration.md +13 -13
- package/docs/plugin-api.md +156 -26
- package/docs/plugin-development.md +390 -0
- package/docs/release.md +2 -2
- package/examples/plugins/hello/README.md +10 -5
- package/examples/plugins/hello/index.js +3 -6
- package/package.json +2 -1
- package/src/app.ts +16 -55
- package/src/core/config/schema.ts +0 -1
- package/src/core/dispatch/command.ts +474 -70
- package/src/core/dispatch/router.ts +77 -3
- package/src/core/index.ts +2 -0
- package/src/core/plugin/command-params.ts +595 -0
- package/src/core/plugin/context.ts +47 -2
- package/src/core/plugin/install-dir.ts +117 -111
- package/src/core/plugin/install-validate.ts +36 -34
- package/src/core/plugin/installer.ts +228 -184
- package/src/core/plugin/loader.ts +36 -2
- package/src/core/plugin/types.ts +23 -3
- package/src/plugins/admin/index.ts +302 -233
- package/src/plugins/core/index.ts +5 -1
- package/dist/core/plugin/installed.d.ts +0 -9
- package/dist/core/plugin/installed.d.ts.map +0 -1
- package/dist/core/plugin/installed.js +0 -113
- package/dist/core/plugin/installed.js.map +0 -1
- package/src/core/plugin/installed.ts +0 -165
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { cp, mkdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
2
3
|
import { join } from 'node:path'
|
|
3
4
|
import type { InstallerConfig } from '../config/schema.js'
|
|
4
5
|
import type { Logger } from '../logging/logger.js'
|
|
5
6
|
import {
|
|
6
|
-
CORE_PACKAGE_NAME,
|
|
7
|
-
INSTALL_STATE_FILE,
|
|
8
7
|
ensureCoreLink,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
ensureInstallRoot,
|
|
9
|
+
packageDirectoryName,
|
|
10
|
+
pathExists,
|
|
11
11
|
readInstallState,
|
|
12
|
-
|
|
12
|
+
readJsonFile,
|
|
13
|
+
readPackageVersion,
|
|
14
|
+
VALIDATE_SCRIPT_FILE,
|
|
13
15
|
writeInstallState,
|
|
14
16
|
type InstalledPackageRecord,
|
|
15
17
|
type InstallState,
|
|
@@ -42,6 +44,7 @@ export class InstallerError extends Error {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface InstallerOptions {
|
|
47
|
+
/** Plugins root. Installed packages are placed in subdirectories here. */
|
|
45
48
|
dir: string
|
|
46
49
|
config: InstallerConfig
|
|
47
50
|
logger: Logger
|
|
@@ -52,13 +55,14 @@ export interface InstallerOptions {
|
|
|
52
55
|
now?: (() => Date) | undefined
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
interface BackupFile {
|
|
56
|
-
path: string
|
|
57
|
-
content: string | null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
58
|
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i
|
|
61
59
|
const NPM_FLAGS = ['--no-audit', '--no-fund', '--loglevel=error']
|
|
60
|
+
const STAGING_MANIFEST = {
|
|
61
|
+
name: '@jsbot-tellus/plugin-staging',
|
|
62
|
+
private: true,
|
|
63
|
+
version: '0.0.0',
|
|
64
|
+
type: 'module',
|
|
65
|
+
}
|
|
62
66
|
|
|
63
67
|
function messageOf(error: unknown): string {
|
|
64
68
|
return error instanceof Error ? error.message : String(error)
|
|
@@ -74,6 +78,15 @@ function asInstallerError(error: unknown): InstallerError {
|
|
|
74
78
|
return new InstallerError(messageOf(error), { cause: error })
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
function installDirectoryName(
|
|
82
|
+
packageName: string,
|
|
83
|
+
pluginIds: readonly string[]
|
|
84
|
+
): string {
|
|
85
|
+
const only = pluginIds.length === 1 ? pluginIds[0] : undefined
|
|
86
|
+
|
|
87
|
+
return only ?? packageDirectoryName(packageName)
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
export class NpmPluginInstaller implements PluginInstaller {
|
|
78
91
|
readonly enabled: boolean
|
|
79
92
|
|
|
@@ -101,30 +114,28 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
async list(): Promise<InstalledPackageInfo[]> {
|
|
104
|
-
const dependencies = await readDependencies(this.dir)
|
|
105
117
|
const state = await readInstallState(this.dir)
|
|
106
118
|
const loadedIds = this.reservedIds()
|
|
107
119
|
const result: InstalledPackageInfo[] = []
|
|
108
120
|
|
|
109
|
-
for (const name of Object.keys(
|
|
110
|
-
if (name === CORE_PACKAGE_NAME) continue
|
|
111
|
-
|
|
121
|
+
for (const name of Object.keys(state.packages).sort()) {
|
|
112
122
|
const record = state.packages[name]
|
|
123
|
+
|
|
124
|
+
if (!record) continue
|
|
125
|
+
|
|
113
126
|
const version =
|
|
114
|
-
(await
|
|
115
|
-
record
|
|
116
|
-
''
|
|
117
|
-
const pluginIds = record?.pluginIds ?? []
|
|
127
|
+
(await readPackageVersion(join(this.dir, record.dir))) ??
|
|
128
|
+
record.version
|
|
118
129
|
|
|
119
130
|
result.push({
|
|
120
131
|
packageName: name,
|
|
121
132
|
version,
|
|
122
|
-
spec: record
|
|
123
|
-
pluginIds,
|
|
124
|
-
installedAt: record
|
|
133
|
+
spec: record.spec,
|
|
134
|
+
pluginIds: [...record.pluginIds],
|
|
135
|
+
installedAt: record.installedAt,
|
|
125
136
|
loaded:
|
|
126
|
-
pluginIds.length > 0 &&
|
|
127
|
-
pluginIds.every((id) => loadedIds.has(id)),
|
|
137
|
+
record.pluginIds.length > 0 &&
|
|
138
|
+
record.pluginIds.every((id) => loadedIds.has(id)),
|
|
128
139
|
})
|
|
129
140
|
}
|
|
130
141
|
|
|
@@ -157,13 +168,14 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
return this.serialize(async () => {
|
|
160
|
-
await ensureInstallDir(this.dir)
|
|
161
|
-
|
|
162
|
-
const dependencies = await readDependencies(this.dir)
|
|
163
171
|
const state = await readInstallState(this.dir)
|
|
164
|
-
this.
|
|
165
|
-
const name = this.resolveTarget(target, dependencies, state)
|
|
172
|
+
const name = this.resolveTarget(target, state)
|
|
166
173
|
const record = state.packages[name]
|
|
174
|
+
|
|
175
|
+
if (!record) {
|
|
176
|
+
throw new InstallerError(`未找到已安装的插件包: ${name}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
167
179
|
const warnings = this.removalWarnings(name, record, state)
|
|
168
180
|
const dependents = this.dependentWarnings(name, record, state)
|
|
169
181
|
|
|
@@ -177,36 +189,52 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
177
189
|
)
|
|
178
190
|
}
|
|
179
191
|
|
|
180
|
-
const
|
|
192
|
+
const destination = join(this.dir, record.dir)
|
|
193
|
+
const backup = this.temporaryPath('backup')
|
|
194
|
+
const existed = await pathExists(destination)
|
|
181
195
|
|
|
182
196
|
try {
|
|
183
|
-
await
|
|
184
|
-
[
|
|
185
|
-
'uninstall',
|
|
186
|
-
name,
|
|
187
|
-
'--prefix',
|
|
188
|
-
this.dir,
|
|
189
|
-
...NPM_FLAGS,
|
|
190
|
-
'--ignore-scripts',
|
|
191
|
-
],
|
|
192
|
-
{ timeoutMs: this.config.timeoutMs, signal }
|
|
193
|
-
)
|
|
194
|
-
await ensureCoreLink(this.dir)
|
|
197
|
+
if (existed) await rename(destination, backup)
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
try {
|
|
200
|
+
delete state.packages[name]
|
|
201
|
+
await writeInstallState(this.dir, state)
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (existed) {
|
|
204
|
+
await rename(backup, destination).catch(
|
|
205
|
+
(restoreError) => {
|
|
206
|
+
this.logger.error(
|
|
207
|
+
'failed to restore plugin directory',
|
|
208
|
+
restoreError
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
}
|
|
200
213
|
|
|
201
|
-
|
|
202
|
-
packageName: name,
|
|
203
|
-
pluginIds: record?.pluginIds ?? [],
|
|
204
|
-
warnings,
|
|
214
|
+
throw error
|
|
205
215
|
}
|
|
206
216
|
} catch (error) {
|
|
207
|
-
await this.rollback(backup, error)
|
|
208
217
|
throw asInstallerError(error)
|
|
209
218
|
}
|
|
219
|
+
|
|
220
|
+
if (existed) {
|
|
221
|
+
await rm(backup, { recursive: true, force: true }).catch(
|
|
222
|
+
(error) => {
|
|
223
|
+
this.logger.warn(
|
|
224
|
+
`failed to remove backup directory ${backup}`,
|
|
225
|
+
error
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.logger.info(`removed plugin package ${name}`)
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
packageName: name,
|
|
235
|
+
pluginIds: [...record.pluginIds],
|
|
236
|
+
warnings,
|
|
237
|
+
}
|
|
210
238
|
})
|
|
211
239
|
}
|
|
212
240
|
|
|
@@ -221,25 +249,15 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
221
249
|
}
|
|
222
250
|
|
|
223
251
|
return this.serialize(async () => {
|
|
224
|
-
await ensureInstallDir(this.dir)
|
|
225
|
-
|
|
226
|
-
const dependencies = await readDependencies(this.dir)
|
|
227
252
|
const state = await readInstallState(this.dir)
|
|
228
|
-
this.assertStateCovers(dependencies, state)
|
|
229
253
|
const names =
|
|
230
254
|
target === undefined || target.trim().length === 0
|
|
231
|
-
? Object.keys(
|
|
232
|
-
|
|
233
|
-
.sort()
|
|
234
|
-
: [this.resolveTarget(target, dependencies, state)]
|
|
255
|
+
? Object.keys(state.packages).sort()
|
|
256
|
+
: [this.resolveTarget(target, state)]
|
|
235
257
|
|
|
236
258
|
const updated: PluginInstallResult[] = []
|
|
237
259
|
const failed: Array<{ packageName: string; message: string }> = []
|
|
238
260
|
|
|
239
|
-
if (names.length === 0) {
|
|
240
|
-
return { updated, failed }
|
|
241
|
-
}
|
|
242
|
-
|
|
243
261
|
for (const name of names) {
|
|
244
262
|
if (signal?.aborted) {
|
|
245
263
|
failed.push({ packageName: name, message: '操作已取消' })
|
|
@@ -275,20 +293,30 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
275
293
|
spec: PackageSpec,
|
|
276
294
|
signal: AbortSignal | undefined
|
|
277
295
|
): Promise<PluginInstallResult> {
|
|
278
|
-
await
|
|
296
|
+
await ensureInstallRoot(this.dir)
|
|
279
297
|
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
this.
|
|
283
|
-
const backup =
|
|
298
|
+
const state = await readInstallState(this.dir)
|
|
299
|
+
const previous = state.packages[spec.name]
|
|
300
|
+
const staging = this.temporaryPath('staging')
|
|
301
|
+
const backup = this.temporaryPath('backup')
|
|
302
|
+
let target: string | null = null
|
|
303
|
+
let movedTarget = false
|
|
304
|
+
let movedBackup = false
|
|
284
305
|
|
|
285
306
|
try {
|
|
307
|
+
await mkdir(staging, { recursive: true })
|
|
308
|
+
await writeFile(
|
|
309
|
+
join(staging, 'package.json'),
|
|
310
|
+
`${JSON.stringify(STAGING_MANIFEST, null, 4)}\n`,
|
|
311
|
+
'utf8'
|
|
312
|
+
)
|
|
313
|
+
|
|
286
314
|
await this.runner.run(
|
|
287
315
|
[
|
|
288
316
|
'install',
|
|
289
317
|
spec.raw,
|
|
290
318
|
'--prefix',
|
|
291
|
-
|
|
319
|
+
staging,
|
|
292
320
|
'--save-exact',
|
|
293
321
|
...NPM_FLAGS,
|
|
294
322
|
...(this.config.allowScripts ? [] : ['--ignore-scripts']),
|
|
@@ -297,29 +325,77 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
297
325
|
{ timeoutMs: this.config.timeoutMs, signal }
|
|
298
326
|
)
|
|
299
327
|
|
|
300
|
-
|
|
328
|
+
const packagePath = join(staging, 'node_modules', spec.name)
|
|
329
|
+
const manifest = await readJsonFile(
|
|
330
|
+
join(packagePath, 'package.json')
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if (
|
|
334
|
+
typeof manifest !== 'object' ||
|
|
335
|
+
manifest === null ||
|
|
336
|
+
Array.isArray(manifest)
|
|
337
|
+
) {
|
|
338
|
+
throw new InstallerError(`npm 没有安装插件包: ${spec.name}`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const flat = join(staging, 'flat')
|
|
342
|
+
|
|
343
|
+
await cp(packagePath, flat, {
|
|
344
|
+
recursive: true,
|
|
345
|
+
dereference: true,
|
|
346
|
+
})
|
|
347
|
+
await rename(
|
|
348
|
+
join(staging, 'node_modules'),
|
|
349
|
+
join(flat, 'node_modules')
|
|
350
|
+
)
|
|
351
|
+
await rm(join(flat, 'node_modules', spec.name), {
|
|
352
|
+
recursive: true,
|
|
353
|
+
force: true,
|
|
354
|
+
})
|
|
355
|
+
await ensureCoreLink(flat)
|
|
301
356
|
|
|
302
|
-
const validated = await this.validate(
|
|
357
|
+
const validated = await this.validate(flat, spec.name, {
|
|
303
358
|
timeoutMs: this.config.timeoutMs,
|
|
304
359
|
signal,
|
|
360
|
+
script: join(this.dir, '.jsbot', VALIDATE_SCRIPT_FILE),
|
|
305
361
|
})
|
|
306
362
|
|
|
307
|
-
this.checkPlugins(spec.name, validated,
|
|
363
|
+
this.checkPlugins(spec.name, validated, state)
|
|
308
364
|
|
|
309
|
-
const version = await
|
|
365
|
+
const version = await readPackageVersion(flat)
|
|
310
366
|
|
|
311
367
|
if (version === undefined) {
|
|
312
368
|
throw new InstallerError(`无法读取已安装包的版本: ${spec.name}`)
|
|
313
369
|
}
|
|
314
370
|
|
|
315
371
|
const pluginIds = validated.map((plugin) => plugin.id)
|
|
316
|
-
const
|
|
372
|
+
const directory =
|
|
373
|
+
previous?.dir ?? installDirectoryName(spec.name, pluginIds)
|
|
374
|
+
|
|
375
|
+
target = join(this.dir, directory)
|
|
376
|
+
|
|
377
|
+
await this.assertTargetAvailable(
|
|
378
|
+
state,
|
|
379
|
+
spec.name,
|
|
380
|
+
directory,
|
|
381
|
+
target,
|
|
382
|
+
previous
|
|
383
|
+
)
|
|
384
|
+
|
|
317
385
|
const warnings = this.configuredWarnings(
|
|
318
|
-
(
|
|
386
|
+
(previous?.pluginIds ?? []).filter(
|
|
319
387
|
(id) => !pluginIds.includes(id)
|
|
320
388
|
)
|
|
321
389
|
)
|
|
322
390
|
|
|
391
|
+
if (await pathExists(target)) {
|
|
392
|
+
await rename(target, backup)
|
|
393
|
+
movedBackup = true
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await rename(flat, target)
|
|
397
|
+
movedTarget = true
|
|
398
|
+
|
|
323
399
|
state.packages[spec.name] = {
|
|
324
400
|
spec: spec.raw,
|
|
325
401
|
version,
|
|
@@ -328,11 +404,28 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
328
404
|
validated.map((plugin) => [plugin.id, plugin.dependencies])
|
|
329
405
|
),
|
|
330
406
|
installedAt: this.now().toISOString(),
|
|
407
|
+
dir: directory,
|
|
331
408
|
}
|
|
332
409
|
|
|
333
410
|
await writeInstallState(this.dir, state)
|
|
334
411
|
|
|
335
|
-
|
|
412
|
+
movedTarget = false
|
|
413
|
+
|
|
414
|
+
if (movedBackup) {
|
|
415
|
+
await rm(backup, { recursive: true, force: true }).catch(
|
|
416
|
+
(error) => {
|
|
417
|
+
this.logger.warn(
|
|
418
|
+
`failed to remove backup directory ${backup}`,
|
|
419
|
+
error
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
)
|
|
423
|
+
movedBackup = false
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this.logger.info(
|
|
427
|
+
`installed plugin package ${spec.name}@${version} into ${target}`
|
|
428
|
+
)
|
|
336
429
|
|
|
337
430
|
return {
|
|
338
431
|
packageName: spec.name,
|
|
@@ -341,8 +434,62 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
341
434
|
warnings,
|
|
342
435
|
}
|
|
343
436
|
} catch (error) {
|
|
344
|
-
|
|
437
|
+
if (movedTarget && target !== null) {
|
|
438
|
+
await rm(target, { recursive: true, force: true }).catch(
|
|
439
|
+
(cleanupError) => {
|
|
440
|
+
this.logger.error(
|
|
441
|
+
'failed to remove partially installed plugin',
|
|
442
|
+
cleanupError
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (movedBackup && target !== null) {
|
|
449
|
+
await rename(backup, target).catch((restoreError) => {
|
|
450
|
+
this.logger.error(
|
|
451
|
+
'failed to restore previous plugin directory',
|
|
452
|
+
restoreError
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
movedBackup = false
|
|
456
|
+
}
|
|
457
|
+
|
|
345
458
|
throw asInstallerError(error)
|
|
459
|
+
} finally {
|
|
460
|
+
await rm(staging, { recursive: true, force: true }).catch(
|
|
461
|
+
(error) => {
|
|
462
|
+
this.logger.warn(
|
|
463
|
+
`failed to remove staging directory ${staging}`,
|
|
464
|
+
error
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private async assertTargetAvailable(
|
|
472
|
+
state: InstallState,
|
|
473
|
+
packageName: string,
|
|
474
|
+
directory: string,
|
|
475
|
+
target: string,
|
|
476
|
+
previous: InstalledPackageRecord | undefined
|
|
477
|
+
): Promise<void> {
|
|
478
|
+
for (const [name, record] of Object.entries(state.packages)) {
|
|
479
|
+
if (name === packageName) continue
|
|
480
|
+
if (record.dir !== directory) continue
|
|
481
|
+
|
|
482
|
+
throw new InstallerError(
|
|
483
|
+
`插件目录已被其它已安装包使用: ${record.dir}(${name})`
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (previous !== undefined) return
|
|
488
|
+
|
|
489
|
+
if (await pathExists(target)) {
|
|
490
|
+
throw new InstallerError(
|
|
491
|
+
`安装目录已存在且不是由安装器管理: ${target}。请先移除该本地插件或为其改名。`
|
|
492
|
+
)
|
|
346
493
|
}
|
|
347
494
|
}
|
|
348
495
|
|
|
@@ -437,18 +584,14 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
437
584
|
for (const id of graph.keys()) visit(id)
|
|
438
585
|
}
|
|
439
586
|
|
|
440
|
-
private resolveTarget(
|
|
441
|
-
target: string,
|
|
442
|
-
dependencies: Record<string, string>,
|
|
443
|
-
state: InstallState
|
|
444
|
-
): string {
|
|
587
|
+
private resolveTarget(target: string, state: InstallState): string {
|
|
445
588
|
const value = target.trim()
|
|
446
589
|
|
|
447
590
|
if (value.length === 0) {
|
|
448
591
|
throw new InstallerError('请提供包名或插件 id')
|
|
449
592
|
}
|
|
450
593
|
|
|
451
|
-
if (
|
|
594
|
+
if (state.packages[value] !== undefined) return value
|
|
452
595
|
|
|
453
596
|
for (const [name, record] of Object.entries(state.packages)) {
|
|
454
597
|
if (record.pluginIds.includes(value)) return name
|
|
@@ -516,24 +659,8 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
516
659
|
return parsed
|
|
517
660
|
}
|
|
518
661
|
|
|
519
|
-
private
|
|
520
|
-
|
|
521
|
-
state: InstallState
|
|
522
|
-
): void {
|
|
523
|
-
const missing = Object.keys(dependencies)
|
|
524
|
-
.filter((name) => name !== CORE_PACKAGE_NAME)
|
|
525
|
-
.filter((name) => state.packages[name] === undefined)
|
|
526
|
-
.sort()
|
|
527
|
-
|
|
528
|
-
if (missing.length === 0) return
|
|
529
|
-
|
|
530
|
-
throw new InstallerError(
|
|
531
|
-
[
|
|
532
|
-
'安装状态与 package.json 不一致,以下依赖缺少安装记录:',
|
|
533
|
-
...missing.map((name) => `- ${name}`),
|
|
534
|
-
`请检查安装目录中的 ${INSTALL_STATE_FILE},或手动清理后重新安装。`,
|
|
535
|
-
].join('\n')
|
|
536
|
-
)
|
|
662
|
+
private temporaryPath(kind: 'staging' | 'backup'): string {
|
|
663
|
+
return join(this.dir, `.${kind}-${randomUUID()}`)
|
|
537
664
|
}
|
|
538
665
|
|
|
539
666
|
private assertEnabled(): void {
|
|
@@ -544,89 +671,6 @@ export class NpmPluginInstaller implements PluginInstaller {
|
|
|
544
671
|
}
|
|
545
672
|
}
|
|
546
673
|
|
|
547
|
-
private async backup(): Promise<BackupFile[]> {
|
|
548
|
-
const files: BackupFile[] = []
|
|
549
|
-
|
|
550
|
-
for (const name of [
|
|
551
|
-
'package.json',
|
|
552
|
-
'package-lock.json',
|
|
553
|
-
INSTALL_STATE_FILE,
|
|
554
|
-
]) {
|
|
555
|
-
const path = join(this.dir, name)
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
files.push({ path, content: await readFile(path, 'utf8') })
|
|
559
|
-
} catch (error) {
|
|
560
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
561
|
-
throw error
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
files.push({ path, content: null })
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return files
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
private async rollback(files: BackupFile[], cause: unknown): Promise<void> {
|
|
572
|
-
try {
|
|
573
|
-
for (const file of files) {
|
|
574
|
-
if (file.content === null) {
|
|
575
|
-
await rm(file.path, { force: true })
|
|
576
|
-
} else {
|
|
577
|
-
await writeFile(file.path, file.content, 'utf8')
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const manifest = files.find((file) =>
|
|
582
|
-
file.path.endsWith('package.json')
|
|
583
|
-
)
|
|
584
|
-
const lock = files.find((file) =>
|
|
585
|
-
file.path.endsWith('package-lock.json')
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
if (manifest?.content === null) {
|
|
589
|
-
await rm(join(this.dir, 'node_modules'), {
|
|
590
|
-
recursive: true,
|
|
591
|
-
force: true,
|
|
592
|
-
})
|
|
593
|
-
return
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (lock?.content !== null && lock !== undefined) {
|
|
597
|
-
await this.runner.run(
|
|
598
|
-
[
|
|
599
|
-
'ci',
|
|
600
|
-
'--prefix',
|
|
601
|
-
this.dir,
|
|
602
|
-
...NPM_FLAGS,
|
|
603
|
-
'--ignore-scripts',
|
|
604
|
-
],
|
|
605
|
-
{ timeoutMs: this.config.timeoutMs }
|
|
606
|
-
)
|
|
607
|
-
} else {
|
|
608
|
-
await this.runner.run(
|
|
609
|
-
[
|
|
610
|
-
'install',
|
|
611
|
-
'--prefix',
|
|
612
|
-
this.dir,
|
|
613
|
-
...NPM_FLAGS,
|
|
614
|
-
'--ignore-scripts',
|
|
615
|
-
],
|
|
616
|
-
{ timeoutMs: this.config.timeoutMs }
|
|
617
|
-
)
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
await ensureCoreLink(this.dir)
|
|
621
|
-
} catch (error) {
|
|
622
|
-
this.logger.error('plugin install rollback failed', error)
|
|
623
|
-
throw new InstallerError(
|
|
624
|
-
`安装失败且回滚失败:${messageOf(cause)}`,
|
|
625
|
-
{ cause }
|
|
626
|
-
)
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
674
|
private serialize<T>(task: () => Promise<T>): Promise<T> {
|
|
631
675
|
const run = this.tail.then(task, task)
|
|
632
676
|
this.tail = run.then(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
import fg from 'fast-glob'
|
|
5
5
|
import type { Logger } from '../logging/logger.js'
|
|
@@ -60,3 +60,37 @@ export async function loadPluginsFromDirectory(
|
|
|
60
60
|
|
|
61
61
|
return plugins
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
export async function loadLocalPlugins(
|
|
65
|
+
root: string,
|
|
66
|
+
logger?: LoaderLogger | undefined
|
|
67
|
+
): Promise<AnyPluginDefinition[]> {
|
|
68
|
+
const absolute = resolve(root)
|
|
69
|
+
const info = await stat(absolute).catch(() => null)
|
|
70
|
+
|
|
71
|
+
if (!info?.isDirectory()) return []
|
|
72
|
+
|
|
73
|
+
const entries = (await readdir(absolute, { withFileTypes: true })).sort(
|
|
74
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
75
|
+
)
|
|
76
|
+
const plugins: AnyPluginDefinition[] = []
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
80
|
+
if (entry.name === 'node_modules') continue
|
|
81
|
+
|
|
82
|
+
const dist = join(absolute, entry.name, 'dist')
|
|
83
|
+
const distInfo = await stat(dist).catch(() => null)
|
|
84
|
+
|
|
85
|
+
if (!distInfo?.isDirectory()) {
|
|
86
|
+
logger?.warn(
|
|
87
|
+
`skipping local plugin ${entry.name}: dist directory not found`
|
|
88
|
+
)
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
plugins.push(...(await loadPluginsFromDirectory(dist, logger)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return plugins
|
|
96
|
+
}
|
package/src/core/plugin/types.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface CommandInfo {
|
|
|
28
28
|
aliases: string[]
|
|
29
29
|
permission: Permission
|
|
30
30
|
description?: string | undefined
|
|
31
|
+
usage?: string | undefined
|
|
31
32
|
pluginId: string
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -44,10 +45,12 @@ export interface EventContext<TEvent extends SnowLumaEvent = SnowLumaEvent> {
|
|
|
44
45
|
|
|
45
46
|
export type CommandHandler<
|
|
46
47
|
TEvent extends OneBotMessageEvent = OneBotMessageEvent,
|
|
48
|
+
TArgs extends unknown[] = [],
|
|
47
49
|
> = (
|
|
48
50
|
event: TEvent,
|
|
49
51
|
context: EventContext<TEvent>,
|
|
50
|
-
match: CommandMatch
|
|
52
|
+
match: CommandMatch,
|
|
53
|
+
...args: TArgs
|
|
51
54
|
) => void | Promise<void>
|
|
52
55
|
|
|
53
56
|
export type MessageHandler = (
|
|
@@ -77,6 +80,24 @@ export interface CommandOptions {
|
|
|
77
80
|
prefixes?: string[] | undefined
|
|
78
81
|
permission?: Permission | undefined
|
|
79
82
|
description?: string | undefined
|
|
83
|
+
params?: string | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CommandGroupOptions<
|
|
87
|
+
TArgs extends unknown[] = [],
|
|
88
|
+
> extends CommandOptions {
|
|
89
|
+
handler?: CommandHandler<OneBotMessageEvent, TArgs> | undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CommandGroupRegistrar {
|
|
93
|
+
register<TArgs extends unknown[] = []>(
|
|
94
|
+
options: CommandOptions,
|
|
95
|
+
handler: CommandHandler<OneBotMessageEvent, TArgs>
|
|
96
|
+
): () => void
|
|
97
|
+
group<TArgs extends unknown[] = []>(
|
|
98
|
+
options: CommandGroupOptions<TArgs>,
|
|
99
|
+
setup: (group: CommandGroupRegistrar) => void
|
|
100
|
+
): () => void
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
export interface NoticeOptions {
|
|
@@ -94,8 +115,7 @@ export interface MiddlewareOptions {
|
|
|
94
115
|
priority?: number | undefined
|
|
95
116
|
}
|
|
96
117
|
|
|
97
|
-
export interface CommandRegistrar {
|
|
98
|
-
register(options: CommandOptions, handler: CommandHandler): () => void
|
|
118
|
+
export interface CommandRegistrar extends CommandGroupRegistrar {
|
|
99
119
|
list(): ReadonlyArray<CommandInfo>
|
|
100
120
|
}
|
|
101
121
|
|