fix: 修复插件包生产路径发现

This commit is contained in:
sunlei 2026-06-18 09:03:55 +08:00
parent 1796360506
commit 1508c63e5c
2 changed files with 105 additions and 9 deletions

View File

@ -2,13 +2,10 @@ import { Inject, Injectable, Optional } from '@nestjs/common';
import { existsSync, statSync } from 'node:fs'; import { existsSync, statSync } from 'node:fs';
import { isAbsolute, relative, resolve, sep } from 'node:path'; import { isAbsolute, relative, resolve, sep } from 'node:path';
const DEFAULT_BUILTIN_PACKAGE_ROOT = resolve( const DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS = [
process.cwd(), ['src', 'modules', 'qqbot', 'plugins'],
'src', ['dist', 'modules', 'qqbot', 'plugins'],
'modules', ];
'qqbot',
'plugins',
);
export const QQBOT_PLUGIN_PACKAGE_CONTROLLED_ROOTS = Symbol( export const QQBOT_PLUGIN_PACKAGE_CONTROLLED_ROOTS = Symbol(
'QQBOT_PLUGIN_PACKAGE_CONTROLLED_ROOTS', 'QQBOT_PLUGIN_PACKAGE_CONTROLLED_ROOTS',
); );
@ -30,7 +27,9 @@ export class QqbotPluginPackagePathPolicyService {
controlledRoots?: string[], controlledRoots?: string[],
) { ) {
this.controlledRoots = ( this.controlledRoots = (
controlledRoots?.length ? controlledRoots : [DEFAULT_BUILTIN_PACKAGE_ROOT] controlledRoots?.length
? controlledRoots
: resolveDefaultBuiltinPackageRoots()
).map((root) => resolve(root)); ).map((root) => resolve(root));
} }
@ -58,7 +57,7 @@ export class QqbotPluginPackagePathPolicyService {
throw new Error('Plugin entry must stay inside the package root'); throw new Error('Plugin entry must stay inside the package root');
} }
return entryFile; return this.resolveCompiledEntryFile(entryFile);
} }
/** /**
@ -95,4 +94,33 @@ export class QqbotPluginPackagePathPolicyService {
isAbsolute(relation) isAbsolute(relation)
); );
} }
/**
* Resolves a manifest entry to the file Node can import in the current runtime.
* @param entryFile - Policy-checked entry path declared by `plugin.json`.
* @returns The declared file when it exists, otherwise the compiled `.js` sibling for TypeScript entries.
*/
private resolveCompiledEntryFile(entryFile: string): string {
if (existsSync(entryFile)) return entryFile;
if (entryFile.endsWith('.ts')) {
const compiledEntryFile = `${entryFile.slice(0, -3)}.js`;
if (existsSync(compiledEntryFile)) {
return compiledEntryFile;
}
}
return entryFile;
}
}
/**
* Resolves the default built-in package root for source development or production `dist` output.
* @returns A single preferred controlled root so package discovery does not duplicate source and dist manifests.
*/
function resolveDefaultBuiltinPackageRoots(): string[] {
const candidates = DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS.map((segments) =>
resolve(process.cwd(), ...segments),
);
return [candidates.find((candidate) => existsSync(candidate)) || candidates[0]];
} }

View File

@ -136,6 +136,74 @@ describe('QqbotPluginPackageSourceService', () => {
); );
}); });
it('resolves TypeScript manifest entries to compiled JavaScript files in dist packages', async () => {
const packageRoot = join(tempRoot, 'compiled-entry');
mkdirSync(join(packageRoot, 'src'), { recursive: true });
writeFileSync(
join(packageRoot, 'plugin.json'),
JSON.stringify({
key: 'compiled-entry',
name: 'Compiled Entry',
version: '1.0.0',
entry: 'src/index.ts',
runtime: {
workerType: 'thread',
timeoutMs: 5000,
memoryMb: 128,
maxConcurrency: 1,
},
operations: [],
}),
'utf8',
);
writeFileSync(
join(packageRoot, 'src', 'index.js'),
'module.exports = { createPlugin() {} };',
'utf8',
);
const source = new QqbotPluginPackageSourceService(
new QqbotPluginPackagePathPolicyService([tempRoot]),
);
await expect(source.discoverPackages()).resolves.toEqual([
expect.objectContaining({
entry: 'src/index.ts',
entryFile: join(packageRoot, 'src', 'index.js'),
pluginKey: 'compiled-entry',
}),
]);
});
it('discovers the default built-in plugin root from production dist output', async () => {
const previousCwd = process.cwd();
const productionRoot = join(tempRoot, 'production-app');
const distPluginRoot = join(
productionRoot,
'dist',
'modules',
'qqbot',
'plugins',
);
mkdirSync(distPluginRoot, { recursive: true });
try {
process.chdir(productionRoot);
jest.resetModules();
const {
QqbotPluginPackagePathPolicyService: RuntimePathPolicyService,
} = await import(
'../../../../src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service'
);
const pathPolicy = new RuntimePathPolicyService();
expect(pathPolicy.listExistingRoots()).toEqual([distPluginRoot]);
} finally {
process.chdir(previousCwd);
jest.resetModules();
}
});
it('does not keep platform-side manifest transfer shims', () => { it('does not keep platform-side manifest transfer shims', () => {
const source = readFileSync( const source = readFileSync(
join( join(