From 9fc11975c10710a424d5e324335eee6884471bec Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 19 Jun 2026 21:20:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=94=9F=E4=BA=A7=E8=B7=AF=E5=BE=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin-package-path-policy.service.ts | 69 ++++++++++++++++++- .../plugin-package-source.spec.ts | 45 ++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts b/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts index d0dc11f..7192e65 100644 --- a/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts +++ b/src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts @@ -66,7 +66,9 @@ export class QqbotPluginPackagePathPolicyService { * @returns Normalized absolute package directory when it is inside a controlled root. */ assertControlledPackageRoot(packageRoot: string): string { - const normalizedPackageRoot = resolve(packageRoot); + const normalizedPackageRoot = + this.resolvePersistedBuiltinPackageRoot(packageRoot) || + resolve(packageRoot); const isControlled = this.controlledRoots.some( (root) => normalizedPackageRoot === root || @@ -80,6 +82,35 @@ export class QqbotPluginPackagePathPolicyService { 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. * @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. * @param entryFile - Policy-checked entry path declared by `plugin.json`. diff --git a/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts b/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts index fa78f88..671722b 100644 --- a/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts +++ b/test/modules/qqbot/plugin-platform/plugin-package-source.spec.ts @@ -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', () => { const source = readFileSync( join(