fix: 原生支持插件包manifest契约

This commit is contained in:
sunlei 2026-06-18 02:29:32 +08:00
parent 9314b303fc
commit 3a941d8a6d
5 changed files with 37 additions and 84 deletions

View File

@ -597,18 +597,21 @@ export const parseQqbotPluginManifest = (
]); ]);
} }
const pluginKey = getString(manifestLike, 'pluginKey') || ''; const pluginKey =
getString(manifestLike, 'key') || getString(manifestLike, 'pluginKey') || '';
const pluginKeyPath = getString(manifestLike, 'key') ? 'key' : 'pluginKey';
if (!pluginKeyPattern.test(pluginKey)) { if (!pluginKeyPattern.test(pluginKey)) {
pushIssue( pushIssue(
issues, issues,
'INVALID_PLUGIN_KEY', 'INVALID_PLUGIN_KEY',
'pluginKey', pluginKeyPath,
'Plugin key must be lower-case kebab-case.', 'Plugin key must be lower-case kebab-case.',
); );
} }
const version = getString(manifestLike, 'version') || ''; const version = getString(manifestLike, 'version') || '';
const minApiSdkVersion = getString(manifestLike, 'minApiSdkVersion') || ''; const minApiSdkVersion =
getString(manifestLike, 'minApiSdkVersion') || '1.0.0';
requireSemver(version, 'version', issues); requireSemver(version, 'version', issues);
requireSemver(minApiSdkVersion, 'minApiSdkVersion', issues); requireSemver(minApiSdkVersion, 'minApiSdkVersion', issues);
@ -633,6 +636,7 @@ export const parseQqbotPluginManifest = (
'permissions', 'permissions',
issues, issues,
), ),
key: pluginKey,
pluginKey, pluginKey,
runtime: parseRuntime(manifestLike, issues), runtime: parseRuntime(manifestLike, issues),
tasks: parseTasks(manifestLike, issues), tasks: parseTasks(manifestLike, issues),

View File

@ -13,6 +13,7 @@ export const QQBOT_PLUGIN_ALLOWED_PERMISSIONS = [
export const QQBOT_PLUGIN_WORKER_TYPES = [ export const QQBOT_PLUGIN_WORKER_TYPES = [
'child-process', 'child-process',
'node-worker', 'node-worker',
'thread',
] as const; ] as const;
export type QqbotPluginPermission = export type QqbotPluginPermission =
@ -85,6 +86,7 @@ export type QqbotPluginManifest = {
name: string; name: string;
operations: QqbotPluginOperationManifest[]; operations: QqbotPluginOperationManifest[];
permissions: QqbotPluginPermission[]; permissions: QqbotPluginPermission[];
key: string;
pluginKey: string; pluginKey: string;
runtime: QqbotPluginRuntimeManifest; runtime: QqbotPluginRuntimeManifest;
tasks: QqbotPluginTaskManifest[]; tasks: QqbotPluginTaskManifest[];

View File

@ -2,10 +2,7 @@ import { Injectable } from '@nestjs/common';
import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { import { parseQqbotPluginManifest } from '@/modules/qqbot/plugin-platform/domain/manifest';
parseQqbotPluginManifest,
type QqbotPluginManifest,
} from '@/modules/qqbot/plugin-platform/domain/manifest';
import { QqbotPluginPackagePathPolicyService } from './plugin-package-path-policy.service'; import { QqbotPluginPackagePathPolicyService } from './plugin-package-path-policy.service';
import type { QqbotPluginPackageDescriptor } from './plugin-package.types'; import type { QqbotPluginPackageDescriptor } from './plugin-package.types';
@ -62,16 +59,15 @@ export class QqbotPluginPackageSourceService {
controlledPackageRoot, controlledPackageRoot,
entry, entry,
); );
const manifest = parseQqbotPluginManifest( const manifest = parseQqbotPluginManifest(manifestLike, {
this.normalizeManifestForCurrentParser(manifestLike), pluginRoot: controlledPackageRoot,
{ pluginRoot: controlledPackageRoot }, });
); const pluginKey = manifest.key;
const pluginKey = manifest.pluginKey;
return { return {
entry: manifest.entry, entry: manifest.entry,
entryFile, entryFile,
manifest: this.attachPackageKeyAlias(manifest, pluginKey), manifest,
packageRoot: controlledPackageRoot, packageRoot: controlledPackageRoot,
pluginKey, pluginKey,
}; };
@ -98,65 +94,6 @@ export class QqbotPluginPackageSourceService {
return typeof manifestLike.entry === 'string' ? manifestLike.entry : ''; return typeof manifestLike.entry === 'string' ? manifestLike.entry : '';
} }
/**
* Adapts planned package manifest aliases to the parser contract currently used by the repo.
* @param manifestLike - Parsed `plugin.json` content from a QQBot plugin package.
* @returns Parser input with the package key and worker type normalized for this codebase.
*/
private normalizeManifestForCurrentParser(manifestLike: unknown): unknown {
if (!this.isRecord(manifestLike)) return manifestLike;
const normalized: Record<string, unknown> = {
minApiSdkVersion: '1.0.0',
...manifestLike,
};
const packageKey = this.readPackageKey(manifestLike);
if (!this.readString(normalized.pluginKey) && packageKey) {
normalized.pluginKey = packageKey;
}
if (this.isRecord(manifestLike.runtime)) {
normalized.runtime = {
...manifestLike.runtime,
workerType:
manifestLike.runtime.workerType === 'thread'
? 'node-worker'
: manifestLike.runtime.workerType,
};
}
return normalized;
}
/**
* Adds the planned `manifest.key` alias while preserving the current manifest parser type.
* @param manifest - Parsed manifest whose `pluginKey` identifies the package.
* @param pluginKey - Package key exposed to descriptor consumers and future generic workers.
* @returns Manifest object with a runtime `key` alias for descriptor compatibility.
*/
private attachPackageKeyAlias(
manifest: QqbotPluginManifest,
pluginKey: string,
): QqbotPluginManifest {
return {
...manifest,
key: pluginKey,
} as QqbotPluginManifest;
}
/**
* Reads the package key from either the planned `key` field or the current `pluginKey` field.
* @param manifestLike - Parsed `plugin.json` object that declares the package identity.
* @returns Non-empty package key when declared.
*/
private readPackageKey(manifestLike: Record<string, unknown>): string {
return (
this.readString(manifestLike.key) ||
this.readString(manifestLike.pluginKey) ||
''
);
}
/** /**
* Narrows JSON values to object records before reading manifest fields. * Narrows JSON values to object records before reading manifest fields.
* @param value - Parsed JSON value from a QQBot plugin package manifest. * @param value - Parsed JSON value from a QQBot plugin package manifest.
@ -165,15 +102,4 @@ export class QqbotPluginPackageSourceService {
private isRecord(value: unknown): value is Record<string, unknown> { private isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value); return !!value && typeof value === 'object' && !Array.isArray(value);
} }
/**
* Normalizes string manifest fields used by package discovery.
* @param value - Raw JSON field value read from `plugin.json`.
* @returns Trimmed string when the field is non-empty.
*/
private readString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim()
? value.trim()
: undefined;
}
} }

View File

@ -176,6 +176,23 @@ describe('QQBot plugin manifest contract', () => {
]); ]);
}); });
it('accepts package key and thread worker type without platform transfer shims', () => {
const validManifest = createValidManifest();
const manifest = parseQqbotPluginManifest({
...validManifest,
key: 'sample-plugin',
pluginKey: undefined,
runtime: {
...validManifest.runtime,
workerType: 'thread',
},
});
expect(manifest.key).toBe('sample-plugin');
expect(manifest.pluginKey).toBe('sample-plugin');
expect(manifest.runtime.workerType).toBe('thread');
});
it('rejects unknown permissions from both plugin and operation scopes', () => { it('rejects unknown permissions from both plugin and operation scopes', () => {
const manifest = createValidManifest(); const manifest = createValidManifest();
manifest.permissions = ['qqbot.send', 'host.env.read']; manifest.permissions = ['qqbot.send', 'host.env.read'];

View File

@ -51,7 +51,11 @@ describe('QqbotPluginPackageSourceService', () => {
expect.objectContaining({ expect.objectContaining({
entry: 'src/index.ts', entry: 'src/index.ts',
entryFile: join(packageRoot, 'src', 'index.ts'), entryFile: join(packageRoot, 'src', 'index.ts'),
manifest: expect.objectContaining({ key: 'sample' }), manifest: expect.objectContaining({
key: 'sample',
pluginKey: 'sample',
runtime: expect.objectContaining({ workerType: 'thread' }),
}),
packageRoot, packageRoot,
pluginKey: 'sample', pluginKey: 'sample',
}), }),