fix: 修复内置插件生产路径解析

This commit is contained in:
sunlei 2026-06-19 21:20:56 +08:00
parent 30f8c2bbc3
commit 9fc11975c1
2 changed files with 113 additions and 1 deletions

View File

@ -66,7 +66,9 @@ export class QqbotPluginPackagePathPolicyService {
* @returns Normalized absolute package directory when it is inside a controlled root. * @returns Normalized absolute package directory when it is inside a controlled root.
*/ */
assertControlledPackageRoot(packageRoot: string): string { assertControlledPackageRoot(packageRoot: string): string {
const normalizedPackageRoot = resolve(packageRoot); const normalizedPackageRoot =
this.resolvePersistedBuiltinPackageRoot(packageRoot) ||
resolve(packageRoot);
const isControlled = this.controlledRoots.some( const isControlled = this.controlledRoots.some(
(root) => (root) =>
normalizedPackageRoot === root || normalizedPackageRoot === root ||
@ -80,6 +82,35 @@ export class QqbotPluginPackagePathPolicyService {
return normalizedPackageRoot; return normalizedPackageRoot;
} }
/**
* Maps persisted built-in source package paths to the controlled root used by the current runtime.
* @param packageRoot - Package root persisted by seed SQL or install records; built-ins may store source-relative paths.
* @returns Runtime package directory under the active controlled root, or null for ordinary paths.
*/
private resolvePersistedBuiltinPackageRoot(
packageRoot: string,
): string | null {
if (isAbsolute(packageRoot)) return null;
const packageSegments = this.toPathSegments(packageRoot);
const builtinPrefix = DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS.find(
(segments) => this.startsWithSegments(packageSegments, segments),
);
if (!builtinPrefix || packageSegments.length !== builtinPrefix.length + 1) {
return null;
}
const packageName = packageSegments[packageSegments.length - 1];
if (!packageName || packageName === '..') return null;
const controlledRoot = this.controlledRoots.find((root) =>
DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS.some((segments) =>
this.endsWithSegments(this.toPathSegments(root), segments),
),
);
return controlledRoot ? resolve(controlledRoot, packageName) : null;
}
/** /**
* Checks whether a candidate package path escapes a controlled root. * Checks whether a candidate package path escapes a controlled root.
* @param root - Absolute controlled root that bounds package discovery. * @param root - Absolute controlled root that bounds package discovery.
@ -95,6 +126,42 @@ export class QqbotPluginPackagePathPolicyService {
); );
} }
/**
* Splits a path with either Windows or POSIX separators for suffix/prefix policy checks.
* @param pathValue - Raw absolute or relative filesystem path.
* @returns Non-empty path segments excluding current-directory markers.
*/
private toPathSegments(pathValue: string): string[] {
return pathValue
.replace(/\\/g, '/')
.split('/')
.filter((segment) => segment && segment !== '.');
}
/**
* Checks whether a path segment list starts with an expected built-in root prefix.
* @param candidate - Candidate package root split into path segments.
* @param expected - Built-in source or dist root segment sequence.
* @returns Whether candidate begins with the expected sequence.
*/
private startsWithSegments(candidate: string[], expected: string[]): boolean {
return expected.every((segment, index) => candidate[index] === segment);
}
/**
* Checks whether a controlled root ends with an expected built-in root segment sequence.
* @param candidate - Controlled root split into path segments.
* @param expected - Built-in source or dist root segment sequence.
* @returns Whether candidate ends with the expected sequence.
*/
private endsWithSegments(candidate: string[], expected: string[]): boolean {
const offset = candidate.length - expected.length;
if (offset < 0) return false;
return expected.every(
(segment, index) => candidate[offset + index] === segment,
);
}
/** /**
* Resolves a manifest entry to the file Node can import in the current runtime. * Resolves a manifest entry to the file Node can import in the current runtime.
* @param entryFile - Policy-checked entry path declared by `plugin.json`. * @param entryFile - Policy-checked entry path declared by `plugin.json`.

View File

@ -204,6 +204,51 @@ describe('QqbotPluginPackageSourceService', () => {
} }
}); });
it('resolves persisted source package paths to the current dist controlled root', () => {
const distPluginRoot = join(
tempRoot,
'production-app',
'dist',
'modules',
'qqbot',
'plugins',
);
const packageRoot = join(distPluginRoot, 'bilibili-card');
mkdirSync(join(packageRoot, 'src'), { recursive: true });
writeFileSync(
join(packageRoot, 'src', 'index.js'),
'module.exports = { createPlugin() {} };',
'utf8',
);
const source = new QqbotPluginPackageSourceService(
new QqbotPluginPackagePathPolicyService([distPluginRoot]),
);
expect(
source.resolveDescriptor('src/modules/qqbot/plugins/bilibili-card', {
key: 'bilibili-card',
name: 'Bilibili Card',
version: '1.0.0',
entry: 'src/index.ts',
runtime: {
workerType: 'thread',
timeoutMs: 10000,
memoryMb: 128,
maxConcurrency: 1,
},
operations: [],
events: [],
}),
).toEqual(
expect.objectContaining({
entryFile: join(packageRoot, 'src', 'index.js'),
packageRoot,
pluginKey: 'bilibili-card',
}),
);
});
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(