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)) {
pushIssue(
issues,
'INVALID_PLUGIN_KEY',
'pluginKey',
pluginKeyPath,
'Plugin key must be lower-case kebab-case.',
);
}
const version = getString(manifestLike, 'version') || '';
const minApiSdkVersion = getString(manifestLike, 'minApiSdkVersion') || '';
const minApiSdkVersion =
getString(manifestLike, 'minApiSdkVersion') || '1.0.0';
requireSemver(version, 'version', issues);
requireSemver(minApiSdkVersion, 'minApiSdkVersion', issues);
@ -633,6 +636,7 @@ export const parseQqbotPluginManifest = (
'permissions',
issues,
),
key: pluginKey,
pluginKey,
runtime: parseRuntime(manifestLike, issues),
tasks: parseTasks(manifestLike, issues),

View File

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

View File

@ -2,10 +2,7 @@ import { Injectable } from '@nestjs/common';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import {
parseQqbotPluginManifest,
type QqbotPluginManifest,
} from '@/modules/qqbot/plugin-platform/domain/manifest';
import { parseQqbotPluginManifest } from '@/modules/qqbot/plugin-platform/domain/manifest';
import { QqbotPluginPackagePathPolicyService } from './plugin-package-path-policy.service';
import type { QqbotPluginPackageDescriptor } from './plugin-package.types';
@ -62,16 +59,15 @@ export class QqbotPluginPackageSourceService {
controlledPackageRoot,
entry,
);
const manifest = parseQqbotPluginManifest(
this.normalizeManifestForCurrentParser(manifestLike),
{ pluginRoot: controlledPackageRoot },
);
const pluginKey = manifest.pluginKey;
const manifest = parseQqbotPluginManifest(manifestLike, {
pluginRoot: controlledPackageRoot,
});
const pluginKey = manifest.key;
return {
entry: manifest.entry,
entryFile,
manifest: this.attachPackageKeyAlias(manifest, pluginKey),
manifest,
packageRoot: controlledPackageRoot,
pluginKey,
};
@ -98,65 +94,6 @@ export class QqbotPluginPackageSourceService {
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.
* @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> {
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', () => {
const manifest = createValidManifest();
manifest.permissions = ['qqbot.send', 'host.env.read'];

View File

@ -51,7 +51,11 @@ describe('QqbotPluginPackageSourceService', () => {
expect.objectContaining({
entry: '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,
pluginKey: 'sample',
}),