fix(plugins): load official hosted plugins from bundled resources
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Task: Use official bundled Plugins without Marketplace Release download
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260901-official-bundled-plugins-client-9d5f3b82
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260901-official-bundled-plugins-client-9d5f3b82-official-bundled-plugins
|
||||
- Worktree: D:\Datas\OthersProjects\makelore-official-bundled-plugins-9d5f3b82
|
||||
- Base commit: 52e2a97a12ffaf838c3218795ff33e7fb91940cf
|
||||
- Owner: codex-root
|
||||
- Status: In Progress
|
||||
|
||||
## Scope
|
||||
|
||||
- Ship the code-owned Game Resource and Web Search package resources inside MakeLore.
|
||||
- Materialize acquired bundled optional Plugins without a Package Store download while retaining policy/admission checks.
|
||||
- Show bundled delivery honestly in Marketplace/My Plugins/Project Plugins and hide only device-package actions.
|
||||
- Cover manifest, effective resolution, hosted admission, Renderer projection, and artifact reachability with focused tests.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Preserve free account acquisition/removal, project enablement, Agent assignment, lifecycle invalidation, and Token Point billing.
|
||||
- Preserve signed artifact download/update behavior for non-bundled Marketplace Plugins.
|
||||
- Do not add Provider authority or secrets to the client.
|
||||
- Keep unknown/removed Plugin configuration preservation unchanged.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Game Resource and Web Search are now fixed schema-2 code-owned bundled definitions with deterministic server Release IDs. Their exact package manifests, capability contracts, and Skills ship under `resources/coding-plugins/**` and match the server source bytes.
|
||||
- Effective resolution still requires the current Account Library entry, project enablement, Agent assignment, policy, and billing readiness, but no longer consults Package Store bytes for these official definitions. Registry definition lookup and hosted Admission resolve the exact bundled Release and reject any server drift.
|
||||
- My Plugins shows “随 MakeLore 提供” and removes download/update/Beta/device-delete actions for official bundles while retaining Account removal, project enablement, and Agent assignment. Project Plugins no longer reports them as missing a device download.
|
||||
- Third-party Marketplace package parsing, signature trust, Package Store installation, and lifecycle remain unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
- Initial RED: five official-bundle assertions failed while 33 adjacent tests passed, proving only Data Service was loaded and hosted Plugins still depended on Package Store.
|
||||
- Registry-boundary RED: bundled `getInstalledDefinition(releaseId)` returned null; GREEN now returns the fixed bundled definition without `getInstalledRelease`.
|
||||
- Focused manifest/effective resolver/Admission/resource-loader/UI suite: 57 passed across 6 files; broader adjacent suite: 133 passed.
|
||||
- Full unit diagnostic before the final registry correction: 214 files ran with 1,781 passed and 2 skipped; two Windows process-spawn tests failed with transient `spawn EBUSY`, then passed 3/3 alone with one worker. Pressure test passed 1/1.
|
||||
- TypeScript typecheck and scoped ESLint pass. `pnpm run build:vite` passes all Renderer/Main/Preload/utility builds (2,272 Renderer modules).
|
||||
- Server/client Game Resource and Web Search resource trees are byte-identical under `git diff --no-index`.
|
||||
- Windows package/artifact verification is pending the final source commit so embedded commit evidence names the exact deliverable.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Integrate the sole source commit into the client `main` root without touching its unrelated untracked task record, then build and verify the Windows package.
|
||||
- The matching Works Square deployment must reach migration `0078`; Provider/pricing readiness remains server-owned.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- None recorded.
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
type CodingProjectPluginService,
|
||||
type CodingPluginMarketplaceService,
|
||||
} from './coding-product-services';
|
||||
import { CODE_OWNED_OPTIONAL_BUNDLED_RELEASES } from '../../shared/coding-plugins';
|
||||
|
||||
export interface CodingCompositionPaths {
|
||||
executablePath: string;
|
||||
@@ -289,12 +290,14 @@ export function createCodingComposition(
|
||||
marketplace: marketplaceClient,
|
||||
packageStore,
|
||||
makeloreVersion: options.clientVersion ?? '2.0.0',
|
||||
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
|
||||
});
|
||||
const webSearchAdapter = createWebSearchPluginAdapter({
|
||||
client: new WebSearchClient(),
|
||||
marketplace: marketplaceClient,
|
||||
packageStore,
|
||||
makeloreVersion: options.clientVersion ?? '2.0.0',
|
||||
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
|
||||
});
|
||||
const policyClient = options.policyClient ?? new PluginPolicyClient();
|
||||
const knownPluginIds = new Set(pluginDefinitions.map(({ id }) => id));
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins'
|
||||
import { PiGameAssetTools } from '../../coding-runtime/pi/extensions/game-assets';
|
||||
import type { MarketplacePackageClientPort, PluginPackageStore } from '../package-store';
|
||||
import {
|
||||
type BundledHostedRelease,
|
||||
MarketplaceHostedAdmissionError,
|
||||
MarketplaceHostedAdmissionResolver,
|
||||
} from '../hosted-admission';
|
||||
@@ -32,6 +33,7 @@ export interface GameResourcePluginAdapterOptions {
|
||||
readonly marketplace: MarketplacePackageClientPort;
|
||||
readonly packageStore: Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>;
|
||||
readonly makeloreVersion: string;
|
||||
readonly bundledReleases?: Readonly<Record<string, BundledHostedRelease>>;
|
||||
readonly admissionResolver?: MarketplaceHostedAdmissionResolver;
|
||||
readonly gameAssets?: PiGameAssetTools;
|
||||
}
|
||||
@@ -175,10 +177,12 @@ export class GameResourcePluginAdapter implements CodingPluginAdapter {
|
||||
marketplace: options.marketplace,
|
||||
packageStore: options.packageStore,
|
||||
makeloreVersion: options.makeloreVersion,
|
||||
bundledReleases: options.bundledReleases,
|
||||
});
|
||||
}
|
||||
|
||||
async inspect(): Promise<PluginBackendProjection> {
|
||||
if (this.options.bundledReleases?.[PLUGIN_ID]) return { status: 'ready' };
|
||||
const installed = await this.options.packageStore.getInstalled(PLUGIN_ID).catch(() => null);
|
||||
return installed ? { status: 'ready' } : { status: 'unconfigured' };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
|
||||
import type { CapabilityBillingReceiptV1 } from '../../../shared/data-service';
|
||||
import {
|
||||
type BundledHostedRelease,
|
||||
MarketplaceHostedAdmissionError,
|
||||
MarketplaceHostedAdmissionResolver,
|
||||
} from '../hosted-admission';
|
||||
@@ -62,6 +63,7 @@ export interface WebSearchPluginAdapterOptions {
|
||||
readonly marketplace?: MarketplacePackageClientPort;
|
||||
readonly packageStore?: Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>;
|
||||
readonly makeloreVersion?: string;
|
||||
readonly bundledReleases?: Readonly<Record<string, BundledHostedRelease>>;
|
||||
readonly admissionResolver?: MarketplaceHostedAdmissionResolver;
|
||||
}
|
||||
|
||||
@@ -230,6 +232,7 @@ export class WebSearchPluginAdapter implements CodingPluginAdapter {
|
||||
marketplace: options.marketplace,
|
||||
packageStore: options.packageStore,
|
||||
makeloreVersion: options.makeloreVersion,
|
||||
bundledReleases: options.bundledReleases,
|
||||
});
|
||||
} else {
|
||||
throw new TypeError('Web Search adapter requires Marketplace admission dependencies');
|
||||
@@ -237,6 +240,7 @@ export class WebSearchPluginAdapter implements CodingPluginAdapter {
|
||||
}
|
||||
|
||||
async inspect(_projectPath: string): Promise<PluginBackendProjection> {
|
||||
if (this.options.bundledReleases?.[PLUGIN_ID]) return { status: 'ready' };
|
||||
const installed = await this.options.packageStore?.getInstalled(PLUGIN_ID).catch(() => null);
|
||||
return installed ? { status: 'ready' } : { status: 'unconfigured' };
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ export class EffectivePluginResolver {
|
||||
// this seam is exclusively for immutable user-installed package roots.
|
||||
return Object.freeze(definitions.flatMap(({ definition, installed, unavailableReason }) => (
|
||||
!installed || unavailableReason || blockedMarketplacePlugins.has(definition.id)
|
||||
|| definition.acquisitionMode !== 'user_acquired'
|
||||
|| definition.provenance.source !== 'marketplace'
|
||||
? []
|
||||
: definition.skills.map((skill) => ({
|
||||
id: skill.id,
|
||||
@@ -464,11 +464,20 @@ export class EffectivePluginResolver {
|
||||
pluginId: string,
|
||||
releaseId?: string | null,
|
||||
): Promise<CodingPluginDefinition | null> {
|
||||
const records = await this.definitionRecords();
|
||||
const bundled = records.find(({ definition }) => (
|
||||
definition.id === pluginId
|
||||
&& definition.provenance.source === 'bundled'
|
||||
&& definition.releaseId === releaseId
|
||||
));
|
||||
if (bundled) {
|
||||
return bundled.installed && !bundled.unavailableReason ? bundled.definition : null;
|
||||
}
|
||||
if (releaseId && this.options.packageStore) {
|
||||
const installed = await this.options.packageStore.getInstalledRelease(pluginId, releaseId);
|
||||
return installed && !installed.unavailableReason ? installed.definition : null;
|
||||
}
|
||||
const record = (await this.definitionRecords()).find(({ definition }) => definition.id === pluginId);
|
||||
const record = records.find(({ definition }) => definition.id === pluginId);
|
||||
return record?.installed && !record.unavailableReason ? record.definition : null;
|
||||
}
|
||||
|
||||
@@ -491,6 +500,7 @@ export class EffectivePluginResolver {
|
||||
}
|
||||
for (const definition of [...records.values()].map(({ definition }) => definition)) {
|
||||
if (definition.acquisitionMode !== 'user_acquired') continue;
|
||||
if (definition.provenance.source === 'bundled') continue;
|
||||
let installed = (this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id);
|
||||
let unavailableReason: PluginUnavailableReasonCode | undefined;
|
||||
if (installedIds.has(definition.id)) {
|
||||
|
||||
@@ -45,6 +45,13 @@ export interface MarketplaceHostedAdmissionResolverOptions {
|
||||
readonly packageStore: Pick<PluginPackageStore, 'getInstalledRelease'>;
|
||||
readonly marketplace: Pick<MarketplacePackageClientPort, 'resolve'>;
|
||||
readonly makeloreVersion: string;
|
||||
readonly bundledReleases?: Readonly<Record<string, BundledHostedRelease>>;
|
||||
}
|
||||
|
||||
export interface BundledHostedRelease {
|
||||
readonly releaseId: string;
|
||||
readonly version: string;
|
||||
readonly channel: 'stable';
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -112,6 +119,24 @@ function matchesFrozenRelease(
|
||||
return item.sha256 !== null && item.sha256 !== undefined && SHA256_PATTERN.test(item.sha256);
|
||||
}
|
||||
|
||||
function matchesBundledFrozenRelease(
|
||||
item: MarketplaceResolveItem,
|
||||
bundled: BundledHostedRelease,
|
||||
plugin: string,
|
||||
): item is MarketplaceResolveItem & {
|
||||
readonly releaseId: string;
|
||||
readonly releaseAdmissionId: string;
|
||||
} {
|
||||
return item.pluginId === plugin
|
||||
&& item.action === 'keep'
|
||||
&& releaseId(item.releaseId) === bundled.releaseId
|
||||
&& item.version === bundled.version
|
||||
&& item.channel === bundled.channel
|
||||
&& releaseId(item.releaseAdmissionId) !== null
|
||||
&& item.sha256 == null
|
||||
&& item.sizeBytes == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current account admission for the exact Release frozen into a
|
||||
* parent worker. Marketplace identity and Release freshness are the only
|
||||
@@ -129,15 +154,22 @@ export class MarketplaceHostedAdmissionResolver {
|
||||
if (!plugin || !request) throw unavailable('Hosted Plugin worker identity is unavailable');
|
||||
if (!frozenRelease) throw unavailable('Hosted Plugin worker Release is unavailable');
|
||||
|
||||
let installed: Awaited<ReturnType<PluginPackageStore['getInstalledRelease']>>;
|
||||
try {
|
||||
installed = await this.options.packageStore.getInstalledRelease(plugin, frozenRelease);
|
||||
} catch (error) {
|
||||
throw this.mapError(error);
|
||||
}
|
||||
if (!installed || installed.pluginId !== plugin || installed.releaseId !== frozenRelease
|
||||
|| installed.unavailableReason || !installed.channel) {
|
||||
throw unavailable('Installed hosted Plugin Release is unavailable');
|
||||
const bundled = this.options.bundledReleases?.[plugin];
|
||||
let installed: Awaited<ReturnType<PluginPackageStore['getInstalledRelease']>> = null;
|
||||
if (bundled) {
|
||||
if (releaseId(bundled.releaseId) !== frozenRelease || bundled.channel !== 'stable') {
|
||||
throw unavailable('Bundled hosted Plugin Release is unavailable');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
installed = await this.options.packageStore.getInstalledRelease(plugin, frozenRelease);
|
||||
} catch (error) {
|
||||
throw this.mapError(error);
|
||||
}
|
||||
if (!installed || installed.pluginId !== plugin || installed.releaseId !== frozenRelease
|
||||
|| installed.unavailableReason || !installed.channel) {
|
||||
throw unavailable('Installed hosted Plugin Release is unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
let resolved;
|
||||
@@ -145,11 +177,11 @@ export class MarketplaceHostedAdmissionResolver {
|
||||
resolved = await this.options.marketplace.resolve({
|
||||
resolveRequestId: request,
|
||||
makeloreVersion: this.options.makeloreVersion,
|
||||
channel: installed.channel,
|
||||
installed: [{
|
||||
pluginId: installed.pluginId,
|
||||
releaseId: installed.releaseId,
|
||||
sha256: installed.sha256,
|
||||
channel: bundled?.channel ?? installed?.channel ?? 'stable',
|
||||
installed: bundled ? [] : [{
|
||||
pluginId: installed!.pluginId,
|
||||
releaseId: installed!.releaseId,
|
||||
sha256: installed!.sha256,
|
||||
}],
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -160,7 +192,10 @@ export class MarketplaceHostedAdmissionResolver {
|
||||
throw stale('Hosted Plugin worker admission is stale');
|
||||
}
|
||||
const item = resolved.items.find(({ pluginId: candidate }) => candidate === plugin);
|
||||
if (!item || !matchesFrozenRelease(item, installed, plugin)) {
|
||||
const matches = item && (bundled
|
||||
? matchesBundledFrozenRelease(item, bundled, plugin)
|
||||
: matchesFrozenRelease(item, installed, plugin));
|
||||
if (!matches) {
|
||||
throw stale('Hosted Plugin worker Release admission is stale');
|
||||
}
|
||||
return Object.freeze({
|
||||
|
||||
@@ -10,10 +10,14 @@ import {
|
||||
BUNDLED_CODING_PLUGIN_PREVIEW_SURFACES,
|
||||
BUNDLED_CODING_PLUGIN_SETTINGS_SURFACES,
|
||||
CODE_OWNED_PLUGIN_PERMISSION_IDS,
|
||||
GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
||||
GAME_RESOURCE_PLUGIN_ID,
|
||||
DATA_SERVICE_CAPABILITY_IDS,
|
||||
DATA_SERVICE_OPERATION_DEFINITIONS,
|
||||
DATA_SERVICE_PLUGIN_ID,
|
||||
DATA_SERVICE_TOOL_NAMES,
|
||||
WEB_SEARCH_BUNDLED_RELEASE_ID,
|
||||
WEB_SEARCH_PLUGIN_ID,
|
||||
type AgentPluginsRootManifest,
|
||||
type CodingPluginDefinition,
|
||||
type CodingPluginAcquisitionMode,
|
||||
@@ -26,14 +30,40 @@ import {
|
||||
} from '../../shared/coding-plugins';
|
||||
|
||||
/**
|
||||
* P0 deliberately has one statically enumerated package root. Keeping this
|
||||
* list relative makes it impossible for an environment variable or a project
|
||||
* The trusted code-owned catalog is statically enumerated. Keeping these
|
||||
* roots relative makes it impossible for an environment variable or project
|
||||
* file to add a package to the trusted catalog.
|
||||
*/
|
||||
export const BUNDLED_CODING_PLUGIN_ROOTS = Object.freeze([
|
||||
'data-service',
|
||||
'game-resource',
|
||||
'web-search',
|
||||
] as const);
|
||||
|
||||
const BUNDLED_CODING_PLUGIN_METADATA = Object.freeze({
|
||||
'data-service': Object.freeze({
|
||||
pluginId: DATA_SERVICE_PLUGIN_ID,
|
||||
runtimeKind: 'bundled_typed' as const,
|
||||
acquisitionMode: 'system_included' as const,
|
||||
releaseId: null,
|
||||
bundledV2: false,
|
||||
}),
|
||||
'game-resource': Object.freeze({
|
||||
pluginId: GAME_RESOURCE_PLUGIN_ID,
|
||||
runtimeKind: 'platform_hosted' as const,
|
||||
acquisitionMode: 'user_acquired' as const,
|
||||
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
||||
bundledV2: true,
|
||||
}),
|
||||
'web-search': Object.freeze({
|
||||
pluginId: WEB_SEARCH_PLUGIN_ID,
|
||||
runtimeKind: 'platform_hosted' as const,
|
||||
acquisitionMode: 'user_acquired' as const,
|
||||
releaseId: WEB_SEARCH_BUNDLED_RELEASE_ID,
|
||||
bundledV2: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const CAPABILITY_MANIFEST_RELATIVE_PATH = 'com.makelore/capability.json';
|
||||
const PACKAGE_MANIFEST_FILE = 'plugin.json';
|
||||
const SKILL_ID_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u;
|
||||
@@ -160,11 +190,13 @@ export interface CodingPluginManifestParseOptions {
|
||||
acquisitionMode?: CodingPluginAcquisitionMode;
|
||||
releaseId?: string | null;
|
||||
provenance?: CodingPluginPackageProvenance;
|
||||
/** Only the fixed code-owned package catalog may opt schema 2 into bundled delivery. */
|
||||
bundledV2?: boolean;
|
||||
}
|
||||
|
||||
export type CodingPluginLoadOptions = Pick<
|
||||
CodingPluginManifestParseOptions,
|
||||
'runtimeKind' | 'acquisitionMode' | 'releaseId' | 'provenance'
|
||||
'runtimeKind' | 'acquisitionMode' | 'releaseId' | 'provenance' | 'bundledV2'
|
||||
>;
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
@@ -696,9 +728,16 @@ function trustedMetadata(
|
||||
if (schemaVersion === 1 && releaseId !== null) {
|
||||
fail(filePath, 'trusted metadata.releaseId', 'bundled schema-1 definitions cannot carry a Release ID');
|
||||
}
|
||||
const bundledV2 = schemaVersion === 2 && options.bundledV2 === true;
|
||||
if (options.bundledV2 && !bundledV2) {
|
||||
fail(filePath, 'trusted metadata.bundledV2', 'is valid only for schema 2 packages');
|
||||
}
|
||||
if (bundledV2 && (runtimeKind !== 'platform_hosted' || releaseId === null)) {
|
||||
fail(filePath, 'trusted metadata.bundledV2', 'requires a hosted runtime and fixed Release ID');
|
||||
}
|
||||
const defaultProvenance: CodingPluginPackageProvenance = {
|
||||
source: schemaVersion === 1 ? 'bundled' : 'marketplace',
|
||||
packageRoot: schemaVersion === 1 ? path.basename(packageRoot) : path.resolve(packageRoot),
|
||||
source: schemaVersion === 1 || bundledV2 ? 'bundled' : 'marketplace',
|
||||
packageRoot: schemaVersion === 1 || bundledV2 ? path.basename(packageRoot) : path.resolve(packageRoot),
|
||||
};
|
||||
const provenance = options.provenance ?? defaultProvenance;
|
||||
if (provenance.source !== defaultProvenance.source
|
||||
@@ -1154,13 +1193,15 @@ export async function loadBundledCodingPluginDefinitions(
|
||||
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
|
||||
const definitions = await Promise.all(roots.map(async (root, index) => {
|
||||
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
|
||||
const metadata = BUNDLED_CODING_PLUGIN_METADATA[expectedRoot];
|
||||
const definition = await loadCodingPluginDefinition(root, {
|
||||
runtimeKind: 'bundled_typed',
|
||||
acquisitionMode: 'system_included',
|
||||
releaseId: null,
|
||||
runtimeKind: metadata.runtimeKind,
|
||||
acquisitionMode: metadata.acquisitionMode,
|
||||
releaseId: metadata.releaseId,
|
||||
provenance: { source: 'bundled', packageRoot: expectedRoot },
|
||||
bundledV2: metadata.bundledV2,
|
||||
});
|
||||
if (definition.id !== `makelore.${expectedRoot}`) {
|
||||
if (definition.id !== metadata.pluginId) {
|
||||
throw new CodingPluginManifestError(
|
||||
path.join(root, PACKAGE_MANIFEST_FILE),
|
||||
'name',
|
||||
@@ -1179,13 +1220,15 @@ export function loadBundledCodingPluginDefinitionsSync(
|
||||
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
|
||||
const definitions = roots.map((root, index) => {
|
||||
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
|
||||
const metadata = BUNDLED_CODING_PLUGIN_METADATA[expectedRoot];
|
||||
const definition = loadCodingPluginDefinitionSync(root, {
|
||||
runtimeKind: 'bundled_typed',
|
||||
acquisitionMode: 'system_included',
|
||||
releaseId: null,
|
||||
runtimeKind: metadata.runtimeKind,
|
||||
acquisitionMode: metadata.acquisitionMode,
|
||||
releaseId: metadata.releaseId,
|
||||
provenance: { source: 'bundled', packageRoot: expectedRoot },
|
||||
bundledV2: metadata.bundledV2,
|
||||
});
|
||||
if (definition.id !== `makelore.${expectedRoot}`) {
|
||||
if (definition.id !== metadata.pluginId) {
|
||||
throw new CodingPluginManifestError(
|
||||
path.join(root, PACKAGE_MANIFEST_FILE),
|
||||
'name',
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"pluginId": "makelore.game-resource",
|
||||
"contractVersion": 1,
|
||||
"scope": "project",
|
||||
"runtime": {
|
||||
"kind": "platform_hosted",
|
||||
"protocol": "makelore-hosted.v1"
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"id": "game-resource",
|
||||
"entry": "../skills/game-resource/SKILL.md",
|
||||
"grants": [
|
||||
"game-resource.generate",
|
||||
"game-resource.library"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "game_resource_templates",
|
||||
"label": "List game resource templates",
|
||||
"description": "List the currently available hosted templates for pixel-art or HD generation.",
|
||||
"capabilityId": "game-resource.generate",
|
||||
"operation": "templates",
|
||||
"roles": ["parent"],
|
||||
"mutation": "read",
|
||||
"projectWriteLease": false,
|
||||
"permissions": ["hosted.game-resource.templates"],
|
||||
"executionMode": "synchronous",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind"],
|
||||
"properties": {
|
||||
"kind": {"type": "string", "minLength": 2, "maxLength": 5}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind", "templates"],
|
||||
"properties": {
|
||||
"kind": {"type": "string", "minLength": 2, "maxLength": 5},
|
||||
"templates": {
|
||||
"type": "array",
|
||||
"maxItems": 100,
|
||||
"items": {"type": "string", "minLength": 1, "maxLength": 200}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "game_resource_generate",
|
||||
"label": "Generate game resource",
|
||||
"description": "Submit one hosted pixel-art or HD game-resource generation job.",
|
||||
"capabilityId": "game-resource.generate",
|
||||
"operation": "generate",
|
||||
"roles": ["parent"],
|
||||
"mutation": "write",
|
||||
"projectWriteLease": false,
|
||||
"permissions": ["hosted.game-resource.generate"],
|
||||
"executionMode": "job",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind", "templateName", "requirement", "confirmed"],
|
||||
"properties": {
|
||||
"kind": {"type": "string", "minLength": 2, "maxLength": 5},
|
||||
"templateName": {"type": "string", "minLength": 1, "maxLength": 200},
|
||||
"templateConfigJson": {"type": "string", "maxLength": 16384},
|
||||
"requirement": {"type": "string", "minLength": 1, "maxLength": 12000},
|
||||
"aspectRatio": {"type": "string", "minLength": 1, "maxLength": 40},
|
||||
"temperature": {"type": "number", "minimum": 0, "maximum": 2},
|
||||
"jobName": {"type": "string", "minLength": 1, "maxLength": 200},
|
||||
"modelName": {"type": "string", "minLength": 1, "maxLength": 200},
|
||||
"resolution": {"type": "string", "minLength": 1, "maxLength": 40},
|
||||
"hdRemoveBgMode": {"type": "string", "minLength": 1, "maxLength": 80},
|
||||
"threadId": {"type": "string", "minLength": 1, "maxLength": 200},
|
||||
"confirmed": {"type": "boolean"},
|
||||
"referencePaths": {
|
||||
"type": "array",
|
||||
"maxItems": 8,
|
||||
"items": {"type": "string", "minLength": 1, "maxLength": 1024}
|
||||
}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["executionId", "status", "outputCount"],
|
||||
"properties": {
|
||||
"executionId": {"type": "string", "minLength": 1, "maxLength": 36},
|
||||
"status": {"type": "string", "maxLength": 32},
|
||||
"outputCount": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||
"pollIntervalSeconds": {"type": "integer", "minimum": 1, "maximum": 300},
|
||||
"errorCode": {"type": "string", "minLength": 1, "maxLength": 128}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "game_resource_status",
|
||||
"label": "Game resource status",
|
||||
"description": "Read and refresh one hosted game-resource generation job.",
|
||||
"capabilityId": "game-resource.generate",
|
||||
"operation": "status",
|
||||
"roles": ["parent"],
|
||||
"mutation": "read",
|
||||
"projectWriteLease": false,
|
||||
"permissions": ["hosted.game-resource.status"],
|
||||
"executionMode": "synchronous",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["executionId"],
|
||||
"properties": {
|
||||
"executionId": {"type": "string", "minLength": 1, "maxLength": 36}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["executionId", "status", "outputCount", "generationBilling"],
|
||||
"properties": {
|
||||
"executionId": {"type": "string", "minLength": 1, "maxLength": 36},
|
||||
"status": {"type": "string", "minLength": 1, "maxLength": 32},
|
||||
"outputCount": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||
"pollIntervalSeconds": {"type": "integer", "minimum": 1, "maximum": 300},
|
||||
"errorCode": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"generationBilling": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode", "status", "reserved_points", "usage_amount", "unit"],
|
||||
"properties": {
|
||||
"mode": {"type": "string", "minLength": 1, "maxLength": 32},
|
||||
"status": {"type": "string", "maxLength": 32},
|
||||
"reserved_points": {"type": "string", "maxLength": 32},
|
||||
"actual_points": {"type": "string", "maxLength": 32},
|
||||
"usage_amount": {"type": "integer", "minimum": 0, "maximum": 1},
|
||||
"unit": {"type": "string", "minLength": 1, "maxLength": 32}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "game_resource_save_output",
|
||||
"label": "Save generated game resource",
|
||||
"description": "Save one completed hosted output into a new file inside the current project.",
|
||||
"capabilityId": "game-resource.library",
|
||||
"operation": "save_output",
|
||||
"roles": ["parent"],
|
||||
"mutation": "write",
|
||||
"projectWriteLease": true,
|
||||
"permissions": ["hosted.game-resource.save-output"],
|
||||
"executionMode": "synchronous",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["executionId", "relativePath", "confirmed"],
|
||||
"properties": {
|
||||
"executionId": {"type": "string", "minLength": 1, "maxLength": 36},
|
||||
"outputIndex": {"type": "integer", "minimum": 0, "maximum": 99},
|
||||
"relativePath": {"type": "string", "minLength": 1, "maxLength": 1024},
|
||||
"confirmed": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["savedPath", "bytes"],
|
||||
"properties": {
|
||||
"savedPath": {"type": "string", "minLength": 1, "maxLength": 1024},
|
||||
"bytes": {"type": "integer", "minimum": 1, "maximum": 33554432}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "game_resource_cancel",
|
||||
"label": "Cancel game resource",
|
||||
"description": "Cancel one non-terminal hosted generation job.",
|
||||
"capabilityId": "game-resource.generate",
|
||||
"operation": "cancel",
|
||||
"roles": ["parent"],
|
||||
"mutation": "write",
|
||||
"projectWriteLease": false,
|
||||
"permissions": ["hosted.game-resource.cancel"],
|
||||
"executionMode": "synchronous",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["executionId"],
|
||||
"properties": {
|
||||
"executionId": {"type": "string", "minLength": 1, "maxLength": 36}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["executionId", "status", "outputCount", "generationBilling"],
|
||||
"properties": {
|
||||
"executionId": {"type": "string", "minLength": 1, "maxLength": 36},
|
||||
"status": {"type": "string", "minLength": 1, "maxLength": 32},
|
||||
"outputCount": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||
"pollIntervalSeconds": {"type": "integer", "minimum": 1, "maximum": 300},
|
||||
"errorCode": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"generationBilling": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode", "status", "reserved_points", "usage_amount", "unit"],
|
||||
"properties": {
|
||||
"mode": {"type": "string", "minLength": 1, "maxLength": 32},
|
||||
"status": {"type": "string", "maxLength": 32},
|
||||
"reserved_points": {"type": "string", "maxLength": 32},
|
||||
"actual_points": {"type": "string", "maxLength": 32},
|
||||
"usage_amount": {"type": "integer", "minimum": 0, "maximum": 1},
|
||||
"unit": {"type": "string", "minLength": 1, "maxLength": 32}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "game_asset_browser",
|
||||
"label": "Browse game assets",
|
||||
"description": "Open the project game-asset browser for user selection.",
|
||||
"capabilityId": "game-resource.library",
|
||||
"operation": "browse",
|
||||
"roles": ["parent"],
|
||||
"mutation": "read",
|
||||
"projectWriteLease": false,
|
||||
"permissions": ["hosted.game-resource.browse"],
|
||||
"executionMode": "synchronous",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [],
|
||||
"properties": {
|
||||
"invocationId": {"type": "string", "minLength": 1, "maxLength": 200}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["invocationId", "status"],
|
||||
"properties": {
|
||||
"invocationId": {"type": "string", "minLength": 1, "maxLength": 200},
|
||||
"status": {"type": "string", "minLength": 1, "maxLength": 32}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "game_asset_review",
|
||||
"label": "Review game asset",
|
||||
"description": "Open a project game asset for user review and decision.",
|
||||
"capabilityId": "game-resource.library",
|
||||
"operation": "review",
|
||||
"roles": ["parent"],
|
||||
"mutation": "read",
|
||||
"projectWriteLease": false,
|
||||
"permissions": ["hosted.game-resource.review"],
|
||||
"executionMode": "synchronous",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["candidateIds"],
|
||||
"properties": {
|
||||
"candidateIds": {
|
||||
"type": "array",
|
||||
"maxItems": 200,
|
||||
"items": {"type": "string", "minLength": 1, "maxLength": 200}
|
||||
},
|
||||
"invocationId": {"type": "string", "minLength": 1, "maxLength": 200}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["invocationId", "status"],
|
||||
"properties": {
|
||||
"invocationId": {"type": "string", "minLength": 1, "maxLength": 200},
|
||||
"status": {"type": "string", "minLength": 1, "maxLength": 32}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
resources/coding-plugins/game-resource/plugin.json
Normal file
14
resources/coding-plugins/game-resource/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "makelore.game-resource",
|
||||
"version": "1.0.0",
|
||||
"description": "Generate and review game resources through MakeLore's hosted provider",
|
||||
"author": {
|
||||
"name": "MakeLore"
|
||||
},
|
||||
"extensions": {
|
||||
"com.makelore": {
|
||||
"capabilityManifest": "./com.makelore/capability.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: game-resource
|
||||
description: 当用户明确要求为当前 MakeLore 项目生成、查找或审查游戏资源时使用;生成会消耗 Token Point,提交前必须说明并取得用户确认。
|
||||
---
|
||||
|
||||
# MakeLore 游戏资源
|
||||
|
||||
此 Skill 只使用 `makelore.game-resource` 提供的工具。不要请求、保存或展示
|
||||
Meowa 凭据、Provider URL、Provider job ID,也不要直接访问第三方接口。
|
||||
|
||||
## 生成流程
|
||||
|
||||
1. 先确认资源用途、像素或高清类型、画面要求、比例和实际需要的参考图;用
|
||||
`game_resource_templates` 获取当前可用模板,不要猜模板名。
|
||||
2. 明确告诉用户生成按 Token Point 用量计费,并等待用户确认本次具体生成请求。
|
||||
3. 确认后只调用一次 `game_resource_generate`。结果不确定、超时或
|
||||
`pending_review` 时不得换 logical operation 重试;报告状态并使用原
|
||||
`executionId` 查询。
|
||||
4. 用 `game_resource_status` 查询同一个 execution,直到 succeeded、failed、
|
||||
cancelled 或 pending_review。轮询间隔遵守返回的 `pollIntervalSeconds`。
|
||||
5. 只有用户明确要求取消时才调用 `game_resource_cancel`。
|
||||
6. succeeded 后,只有用户明确给出项目内目标路径并确认保存时,才调用
|
||||
`game_resource_save_output`。该工具只创建新文件,不覆盖现有文件。
|
||||
|
||||
## 浏览与审查
|
||||
|
||||
`game_asset_browser` 与 `game_asset_review` 只处理当前项目的本地候选资源和用户
|
||||
确认,不会生成资源,也不会产生新的 Token Point 交易。不得把本地路径当作
|
||||
Provider 地址或通过生成工具上传未获用户同意的文件。
|
||||
|
||||
## 完成标准
|
||||
|
||||
报告必须区分提交、Provider 状态和 Token Point receipt。只引用工具实际返回的
|
||||
execution、status、outputCount 与 billing;不要猜测余额、价格、Provider 成本或
|
||||
输出路径。没有 settled receipt 时不得声称计费已最终完成。
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"pluginId": "makelore.web-search",
|
||||
"contractVersion": 1,
|
||||
"scope": "project",
|
||||
"runtime": {
|
||||
"kind": "platform_hosted",
|
||||
"protocol": "makelore-hosted.v1"
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"id": "makelore-web-search",
|
||||
"entry": "../skills/makelore-web-search/SKILL.md",
|
||||
"grants": ["web-search.search"]
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "makelore_web_search",
|
||||
"label": "Search the web",
|
||||
"description": "Search the current web through MakeLore and return a grounded answer with sources.",
|
||||
"capabilityId": "web-search.search",
|
||||
"operation": "search",
|
||||
"roles": ["parent"],
|
||||
"mutation": "read",
|
||||
"projectWriteLease": false,
|
||||
"permissions": ["hosted.web-search.search"],
|
||||
"executionMode": "synchronous",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["query", "confirmed"],
|
||||
"properties": {
|
||||
"query": {"type": "string", "minLength": 1, "maxLength": 2000},
|
||||
"confirmed": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"outputSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["answer", "sources", "searchQueries"],
|
||||
"properties": {
|
||||
"answer": {"type": "string", "minLength": 1, "maxLength": 16000},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"maxItems": 20,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["title", "url"],
|
||||
"properties": {
|
||||
"title": {"type": "string", "minLength": 1, "maxLength": 500},
|
||||
"url": {"type": "string", "minLength": 1, "maxLength": 2048}
|
||||
}
|
||||
}
|
||||
},
|
||||
"searchQueries": {
|
||||
"type": "array",
|
||||
"maxItems": 8,
|
||||
"items": {"type": "string", "minLength": 1, "maxLength": 500}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
resources/coding-plugins/web-search/plugin.json
Normal file
14
resources/coding-plugins/web-search/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "makelore.web-search",
|
||||
"version": "1.0.0",
|
||||
"description": "Search the current web through MakeLore and return a grounded answer with sources",
|
||||
"author": {
|
||||
"name": "MakeLore"
|
||||
},
|
||||
"extensions": {
|
||||
"com.makelore": {
|
||||
"capabilityManifest": "./com.makelore/capability.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: makelore-web-search
|
||||
description: 当用户需要当前或外部网页信息且现有上下文不足时使用;搜索会消耗 Token Point,调用前必须说明并取得对本次准确查询的确认。
|
||||
---
|
||||
|
||||
# MakeLore 联网搜索
|
||||
|
||||
此 Skill 只使用 `makelore.web-search` 提供的 `makelore_web_search` 工具。不要请求、
|
||||
保存或展示 Provider 凭据、模型、地址或内部请求标识,也不要直接调用第三方接口。
|
||||
|
||||
## 搜索流程
|
||||
|
||||
1. 只有问题需要当前或外部网页信息、而现有上下文不足时才搜索;能从已有上下文回答时不要调用。
|
||||
2. 先明确将发送的准确查询,说明成功搜索会消耗 Token Point,并等待用户确认。
|
||||
3. 确认后只用同一个 logical operation 调用一次 `makelore_web_search`;不得为了自动重试改写查询或生成新的 logical operation。
|
||||
4. 只根据工具实际返回的 `answer`、`sources` 和 `searchQueries` 回答;引用应紧邻其支持的事实,不能虚构或补写来源。
|
||||
|
||||
## 不确定结果
|
||||
|
||||
- `submission_unknown`:结果不可用且提交状态未知。说明状态,不得自动重试。
|
||||
- `receipt_unavailable`:Main 已收到结果但收据不可用。可以使用实际结果,但必须说明收据暂不可用,且不得自动重试。
|
||||
- `pending_review`:可以使用工具返回的实际结果,但必须说明 Token Point 正在人工复核,且不得自动重试。
|
||||
|
||||
## 完成标准
|
||||
|
||||
区分搜索结果与 Token Point receipt;没有 settled receipt 时不得声称计费已最终完成。
|
||||
@@ -621,8 +621,12 @@ export async function verifyBundledCodingPluginResources({
|
||||
}
|
||||
|
||||
const sdkAssetPaths = packageFiles.filter((file) => SDK_ASSET_PATH_PATTERN.test(file));
|
||||
if (sdkAssetPaths.length === 0) throw new Error(`Packaged coding plugin has no SDK assets: ${packageName}`);
|
||||
const adapterId = packagedCapability.adapterId;
|
||||
if (packagedCapability.schemaVersion === 1 && sdkAssetPaths.length === 0) {
|
||||
throw new Error(`Packaged schema-1 coding plugin has no SDK assets: ${packageName}`);
|
||||
}
|
||||
const adapterId = packagedCapability.schemaVersion === 2
|
||||
? packagedCapability.pluginId
|
||||
: packagedCapability.adapterId;
|
||||
const tools = Array.isArray(packagedCapability.tools) ? packagedCapability.tools : [];
|
||||
const toolNames = tools.map((tool) => tool?.name);
|
||||
if (typeof adapterId !== 'string' || adapterId.length === 0 || tools.length === 0
|
||||
|
||||
@@ -101,6 +101,28 @@ export const DATA_SERVICE_ADAPTER_ID = 'data-service' as const;
|
||||
export const DATA_SERVICE_PROJECT_SETTINGS_SURFACE = 'data-service' as const;
|
||||
export const DATA_SERVICE_PREVIEW_RUNTIME_SURFACE = 'data-service-v1' as const;
|
||||
|
||||
export const GAME_RESOURCE_PLUGIN_ID = 'makelore.game-resource' as const;
|
||||
export const GAME_RESOURCE_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000105' as const;
|
||||
export const WEB_SEARCH_PLUGIN_ID = 'makelore.web-search' as const;
|
||||
export const WEB_SEARCH_BUNDLED_RELEASE_ID = '00000000-0000-4000-8000-000000000204' as const;
|
||||
|
||||
export const CODE_OWNED_OPTIONAL_BUNDLED_RELEASES = Object.freeze({
|
||||
[GAME_RESOURCE_PLUGIN_ID]: Object.freeze({
|
||||
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
||||
version: '1.0.0',
|
||||
channel: 'stable' as const,
|
||||
}),
|
||||
[WEB_SEARCH_PLUGIN_ID]: Object.freeze({
|
||||
releaseId: WEB_SEARCH_BUNDLED_RELEASE_ID,
|
||||
version: '1.0.0',
|
||||
channel: 'stable' as const,
|
||||
}),
|
||||
});
|
||||
|
||||
export function isCodeOwnedOptionalBundledPluginId(pluginId: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(CODE_OWNED_OPTIONAL_BUNDLED_RELEASES, pluginId);
|
||||
}
|
||||
|
||||
export const DATA_SERVICE_CAPABILITY_IDS = Object.freeze([
|
||||
'data-service.control',
|
||||
'data-service.documents',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
|
||||
import type { MarketplaceInstallation, MarketplaceLibrarySnapshot } from '@/lib/plugin-marketplace';
|
||||
import { pluginMarketplaceStore, usePluginMarketplaceStore } from '@/stores/plugin-marketplace';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { isCodeOwnedOptionalBundledPluginId } from '../../../shared/coding-plugins';
|
||||
|
||||
function failureText(reason?: string): string | null {
|
||||
if (!reason) return null;
|
||||
@@ -55,7 +56,7 @@ export function MyPluginsView(props: MyPluginsViewProps) {
|
||||
const visibleError = actionErrorText(props.error);
|
||||
return (
|
||||
<main data-testid="my-plugins-page" className="mx-auto w-full max-w-7xl text-foreground">
|
||||
<header className="flex flex-wrap items-start justify-between gap-4"><div><h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">我的插件</h1><p className="mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">账号插件库与本机下载分开管理。下载不会自动启用到项目,也不会分配给伙伴。</p></div><Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}><RefreshCw className="mr-2 h-4 w-4" />刷新</Button></header>
|
||||
<header className="flex flex-wrap items-start justify-between gap-4"><div><h1 className="text-balance text-3xl font-semibold tracking-[-0.03em]">我的插件</h1><p className="mt-2 max-w-2xl text-pretty text-sm text-muted-foreground">官方插件随 MakeLore 提供;其他插件的账号获取与本机下载分开管理。任何插件都不会自动启用到项目或分配给伙伴。</p></div><Button type="button" variant="outline" className="min-h-10" onClick={() => void props.onRefresh()}><RefreshCw className="mr-2 h-4 w-4" />刷新</Button></header>
|
||||
{props.library?.stale ? <p role="status" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">正在显示上次可信的插件库;服务恢复前,在线计量或托管能力保持不可用。</p> : null}
|
||||
{props.state === 'loading' && !props.library ? <p role="status" className="mt-8 text-sm">正在读取我的插件…</p> : null}
|
||||
{props.state === 'error' && !props.library ? <div role="alert" className="mt-8 rounded-2xl bg-warning/10 p-5"><h2 className="font-semibold">无法读取我的插件</h2><p className="mt-2 text-pretty text-sm">{props.error ?? '请稍后重试。'}</p></div> : null}
|
||||
@@ -65,7 +66,8 @@ export function MyPluginsView(props: MyPluginsViewProps) {
|
||||
{props.library?.items.map((plugin) => {
|
||||
const installed = props.installations[plugin.pluginId];
|
||||
const systemIncluded = plugin.acquisition === 'system_included';
|
||||
const hasDevicePackage = !systemIncluded && Boolean(installed?.version);
|
||||
const bundledDelivery = systemIncluded || isCodeOwnedOptionalBundledPluginId(plugin.pluginId);
|
||||
const hasDevicePackage = !bundledDelivery && Boolean(installed?.version);
|
||||
const removed = plugin.removedAt !== null;
|
||||
const suspended = plugin.runtimeStatus === 'suspended';
|
||||
const retired = plugin.catalogStatus === 'retired';
|
||||
@@ -74,17 +76,18 @@ export function MyPluginsView(props: MyPluginsViewProps) {
|
||||
const updateAvailable = Boolean(hasDevicePackage && channelVersion && installed?.version !== channelVersion);
|
||||
const betaChannelUnavailable = Boolean(hasDevicePackage && installedChannel === 'beta' && !plugin.betaVersion);
|
||||
const busy = Object.keys(props.pending).some((key) => key.endsWith(`:${plugin.pluginId}`));
|
||||
const failure = systemIncluded ? null : failureText(installed?.reason);
|
||||
const failure = bundledDelivery ? null : failureText(installed?.reason);
|
||||
return <article key={plugin.pluginId} className="rounded-2xl bg-background p-5 shadow-soft ring-1 ring-border/70">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4"><div><div className="flex flex-wrap items-center gap-2"><h2 className="text-balance text-xl font-semibold">{plugin.title}</h2>{systemIncluded ? <Badge variant="outline">系统内置</Badge> : removed ? <Badge variant="outline">已移除</Badge> : <Badge variant="secondary">已获取</Badge>}</div><p className="mt-2 text-pretty text-sm text-muted-foreground">{plugin.summary}</p></div><div className="text-right text-sm"><p className="tabular-nums">稳定版 {plugin.stableVersion ?? '不可用'}</p>{plugin.betaVersion ? <p className="mt-1 tabular-nums text-muted-foreground">Beta 版 {plugin.betaVersion}</p> : null}{systemIncluded ? <p className="mt-1 text-muted-foreground">随 MakeLore 提供</p> : hasDevicePackage ? <><p className="mt-1 tabular-nums text-muted-foreground">设备版本 {installed?.version}</p><p className="mt-1 text-muted-foreground">当前频道:{installedChannel === 'beta' ? 'Beta' : '稳定'}</p></> : <p className="mt-1 text-muted-foreground">尚未下载到设备</p>}</div></div>
|
||||
<div className="flex flex-wrap items-start justify-between gap-4"><div><div className="flex flex-wrap items-center gap-2"><h2 className="text-balance text-xl font-semibold">{plugin.title}</h2>{systemIncluded ? <Badge variant="outline">系统内置</Badge> : removed ? <Badge variant="outline">已移除</Badge> : <Badge variant="secondary">已获取</Badge>}</div><p className="mt-2 text-pretty text-sm text-muted-foreground">{plugin.summary}</p></div><div className="text-right text-sm"><p className="tabular-nums">稳定版 {plugin.stableVersion ?? '不可用'}</p>{plugin.betaVersion ? <p className="mt-1 tabular-nums text-muted-foreground">Beta 版 {plugin.betaVersion}</p> : null}{bundledDelivery ? <p className="mt-1 text-muted-foreground">随 MakeLore 提供</p> : hasDevicePackage ? <><p className="mt-1 tabular-nums text-muted-foreground">设备版本 {installed?.version}</p><p className="mt-1 text-muted-foreground">当前频道:{installedChannel === 'beta' ? 'Beta' : '稳定'}</p></> : <p className="mt-1 text-muted-foreground">尚未下载到设备</p>}</div></div>
|
||||
<div className="mt-4 space-y-1 text-pretty text-sm">{suspended ? <p className="text-destructive">运行已暂停</p> : null}{retired ? <p>{removed ? '已退役,移除后不可重新获取' : '已退役;现有账号插件库仍可继续使用受支持版本'}</p> : null}{betaChannelUnavailable ? <p>当前 Beta 频道暂无可用版本;不会静默切回稳定版。</p> : null}{failure ? <p className="text-destructive"><AlertCircle className="mr-1 inline h-4 w-4" />{failure}</p> : null}</div>
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{systemIncluded ? null
|
||||
: removed ? <Button type="button" className="min-h-10" disabled={busy || retired || suspended} aria-label={`重新免费获取${plugin.title}`} onClick={() => void props.onReacquire(plugin.pluginId)}>重新免费获取</Button>
|
||||
: bundledDelivery ? null
|
||||
: !hasDevicePackage ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`下载${plugin.title}`} onClick={() => void props.onInstall(plugin.pluginId)}><Download className="mr-2 h-4 w-4" />下载</Button>
|
||||
: updateAvailable ? <Button type="button" className="min-h-10" disabled={busy || suspended} aria-label={`${installedChannel === 'beta' ? '更新 Beta' : '更新'}${plugin.title}`} onClick={() => void (installedChannel === 'beta' ? props.onInstallBeta(plugin.pluginId) : props.onUpdate(plugin.pluginId))}><RefreshCw className="mr-2 h-4 w-4" />{installedChannel === 'beta' ? '更新 Beta' : '更新'}</Button>
|
||||
: <span className="inline-flex min-h-10 items-center px-2 text-sm font-medium"><PackageCheck className="mr-2 h-4 w-4 text-brand" />设备版本已是最新</span>}
|
||||
{!removed && plugin.acquisition !== 'system_included' && plugin.betaVersion && installedChannel !== 'beta' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy || suspended} aria-label={`安装 Beta${plugin.title}`} onClick={() => void props.onInstallBeta(plugin.pluginId)}>安装 Beta</Button> : null}
|
||||
{!removed && !bundledDelivery && plugin.betaVersion && installedChannel !== 'beta' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy || suspended} aria-label={`安装 Beta${plugin.title}`} onClick={() => void props.onInstallBeta(plugin.pluginId)}>安装 Beta</Button> : null}
|
||||
{!removed && plugin.acquisition !== 'system_included' ? <Button type="button" variant="outline" className="min-h-10" disabled={busy} aria-label={`移除${plugin.title}`} onClick={() => void props.onRemove(plugin.pluginId)}><Trash2 className="mr-2 h-4 w-4" />移除</Button> : null}
|
||||
{hasDevicePackage ? <Button type="button" variant="ghost" className="min-h-10" disabled={busy} aria-label={`删除设备上的${plugin.title}`} onClick={() => void props.onUninstall(plugin.pluginId)}>删除设备包</Button> : null}
|
||||
<Button asChild variant="outline" className="min-h-10"><Link to="/project-plugins">启用到项目</Link></Button>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { cn } from '@/lib/utils';
|
||||
import { codingPluginsStore, useCodingPluginsStore } from '@/stores/coding-plugins';
|
||||
import { pluginMarketplaceStore, usePluginMarketplaceStore } from '@/stores/plugin-marketplace';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { isCodeOwnedOptionalBundledPluginId } from '../../../shared/coding-plugins';
|
||||
import type { MarketplaceInstallation, MarketplaceLibrarySnapshot } from '@/lib/plugin-marketplace';
|
||||
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import type { DataServiceInstanceState } from '../../../shared/data-service';
|
||||
@@ -93,7 +94,7 @@ export function ProjectPluginsView(props: ViewProps) {
|
||||
{selected.enabled ? <Button type="button" variant="outline" className="min-h-10" disabled={pendingToggle} onClick={() => setDisableCandidate(selected)} aria-label={`禁用${selected.displayName}`}>禁用</Button> : <Button type="button" className="min-h-10" disabled={pendingToggle || selected.state === 'unavailable'} onClick={() => void props.onSetEnabled(selected.id, true)} aria-label={`启用到项目${selected.displayName}`}>启用到项目</Button>}
|
||||
</div>
|
||||
{libraryEntry?.removedAt ? <p role="status" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">账号插件库已移除;项目配置仍保留此插件 ID,重新免费获取后可恢复。</p> : null}
|
||||
{libraryEntry?.acquisitionMode === 'user_acquired' && !libraryEntry.removedAt && !installation ? <p role="status" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">插件已免费获取但尚未下载到本机,当前项目配置会继续保留。</p> : null}
|
||||
{libraryEntry?.acquisitionMode === 'user_acquired' && !libraryEntry.removedAt && !installation && !isCodeOwnedOptionalBundledPluginId(selected.id) ? <p role="status" className="mt-4 rounded-xl bg-warning/10 p-3 text-pretty text-sm">插件已免费获取但尚未下载到本机,当前项目配置会继续保留。</p> : null}
|
||||
<section className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4" aria-label="插件概览">
|
||||
<div className="rounded-xl bg-surface-subtle p-3"><p className="text-xs text-muted-foreground">发布者</p><p className="mt-1 font-semibold">MakeLore</p></div>
|
||||
<div className="rounded-xl bg-surface-subtle p-3"><p className="text-xs text-muted-foreground">包版本</p><p className="mt-1 font-semibold">{selected.version}</p></div>
|
||||
|
||||
@@ -395,4 +395,58 @@ describe('effective plugin resolver', () => {
|
||||
})).resolves.toMatchObject({ effectiveSkillIds: ['notes'], toolDefinitions: [], runtimePolicies: [] });
|
||||
expect(refresh).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('materializes an acquired code-owned bundled hosted Plugin without Package Store bytes', async () => {
|
||||
const bundled: CodingPluginDefinition = {
|
||||
...serverDefinition,
|
||||
id: 'makelore.web-search',
|
||||
releaseId: '00000000-0000-4000-8000-000000000204',
|
||||
provenance: { source: 'bundled', packageRoot: 'web-search' },
|
||||
skills: [{
|
||||
id: 'makelore-web-search',
|
||||
entryPath: 'skills/makelore-web-search/SKILL.md',
|
||||
grants: ['remote.read'],
|
||||
}],
|
||||
};
|
||||
const getInstalled = vi.fn(async () => null);
|
||||
const getInstalledRelease = vi.fn(async () => null);
|
||||
const policy = currentPolicy();
|
||||
policy.catalog.plugins[0] = { ...policy.catalog.plugins[0], plugin_id: bundled.id };
|
||||
const effective = createEffectivePluginResolver({
|
||||
definitions: [bundled],
|
||||
getAccountBinding: () => binding,
|
||||
getLibrary: vi.fn(async () => ({
|
||||
...library(),
|
||||
items: [{ ...library().items[0], pluginId: bundled.id }],
|
||||
})),
|
||||
packageStore: {
|
||||
readInstalledIndex: vi.fn(async () => []),
|
||||
getInstalled,
|
||||
getInstalledRelease,
|
||||
},
|
||||
getEnabledPluginIds: vi.fn(async () => [bundled.id]),
|
||||
policyClient: { getState: () => policy, refresh: vi.fn() },
|
||||
});
|
||||
|
||||
await expect(effective.resolve({
|
||||
projectId: 'project-a',
|
||||
projectPath: 'C:/project-a',
|
||||
assignedSkillIds: ['makelore-web-search'],
|
||||
role: 'parent',
|
||||
})).resolves.toMatchObject({
|
||||
pluginReleaseIds: [bundled.releaseId],
|
||||
effectiveSkillIds: ['makelore-web-search'],
|
||||
skillEntries: [{
|
||||
id: 'makelore-web-search',
|
||||
entryPath: 'skills/makelore-web-search/SKILL.md',
|
||||
}],
|
||||
toolDefinitions: [{ name: 'remote_read' }],
|
||||
unavailableReasons: [],
|
||||
});
|
||||
expect(getInstalled).not.toHaveBeenCalled();
|
||||
await expect(effective.getSkillSources()).resolves.toEqual([]);
|
||||
await expect(effective.getInstalledDefinition(bundled.id, bundled.releaseId))
|
||||
.resolves.toMatchObject({ id: bundled.id, releaseId: bundled.releaseId });
|
||||
expect(getInstalledRelease).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from '../../shared/coding-plugins';
|
||||
|
||||
const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service');
|
||||
const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource');
|
||||
const WEB_SEARCH_ROOT = path.resolve('resources/coding-plugins/web-search');
|
||||
|
||||
async function packageManifests(): Promise<{ root: Record<string, unknown>; capability: Record<string, unknown> }> {
|
||||
return {
|
||||
@@ -32,9 +34,13 @@ describe('bundled coding plugin manifests', () => {
|
||||
it('loads the fixed Data Service package and immutable declarations', async () => {
|
||||
const definitions = await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins'));
|
||||
const startupDefinitions = loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins'));
|
||||
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service']);
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([PACKAGE_ROOT]);
|
||||
expect(definitions).toHaveLength(1);
|
||||
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service', 'game-resource', 'web-search']);
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
||||
PACKAGE_ROOT,
|
||||
GAME_RESOURCE_ROOT,
|
||||
WEB_SEARCH_ROOT,
|
||||
]);
|
||||
expect(definitions).toHaveLength(3);
|
||||
expect(definitions[0]).toMatchObject({
|
||||
id: 'makelore.data-service',
|
||||
adapterId: 'data-service',
|
||||
@@ -42,7 +48,21 @@ describe('bundled coding plugin manifests', () => {
|
||||
skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
|
||||
});
|
||||
expect(definitions[0]?.tools.map(({ name }) => name)).toEqual(DATA_SERVICE_TOOL_NAMES);
|
||||
expect(definitions).toEqual([DATA_SERVICE_PLUGIN_DEFINITION]);
|
||||
expect(definitions[0]).toEqual(DATA_SERVICE_PLUGIN_DEFINITION);
|
||||
expect(definitions.slice(1)).toMatchObject([
|
||||
{
|
||||
id: 'makelore.game-resource', version: '1.0.0', runtimeKind: 'platform_hosted',
|
||||
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000105',
|
||||
provenance: { source: 'bundled', packageRoot: 'game-resource' },
|
||||
skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }],
|
||||
},
|
||||
{
|
||||
id: 'makelore.web-search', version: '1.0.0', runtimeKind: 'platform_hosted',
|
||||
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000204',
|
||||
provenance: { source: 'bundled', packageRoot: 'web-search' },
|
||||
skills: [{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' }],
|
||||
},
|
||||
]);
|
||||
expect(startupDefinitions).toEqual(definitions);
|
||||
expect(Object.isFrozen(startupDefinitions)).toBe(true);
|
||||
expect(Object.isFrozen(startupDefinitions[0]?.operations)).toBe(true);
|
||||
@@ -115,6 +135,8 @@ describe('bundled coding plugin manifests', () => {
|
||||
it('does not infer package roots from arbitrary directories', async () => {
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([
|
||||
path.resolve('tmp/data-service'),
|
||||
path.resolve('tmp/game-resource'),
|
||||
path.resolve('tmp/web-search'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -135,4 +135,30 @@ describe('MarketplaceHostedAdmissionResolver', () => {
|
||||
retryable: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves a code-owned bundled Release without consulting the device Package Store', async () => {
|
||||
const getInstalledRelease = vi.fn(async () => null);
|
||||
const resolve = vi.fn(async () => resolved({ sha256: null, sizeBytes: null }));
|
||||
const options = {
|
||||
packageStore: { getInstalledRelease },
|
||||
marketplace: { resolve },
|
||||
makeloreVersion: '2.0.0',
|
||||
bundledReleases: {
|
||||
[PLUGIN_ID]: { releaseId: RELEASE_ID, version: '1.0.0', channel: 'stable' as const },
|
||||
},
|
||||
} as unknown as ConstructorParameters<typeof MarketplaceHostedAdmissionResolver>[0];
|
||||
const bundledResolver = new MarketplaceHostedAdmissionResolver(options);
|
||||
|
||||
await expect(bundledResolver.resolve({
|
||||
pluginId: PLUGIN_ID,
|
||||
workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID },
|
||||
})).resolves.toEqual({ releaseId: RELEASE_ID, releaseAdmissionId: 'admission-game-1' });
|
||||
expect(getInstalledRelease).not.toHaveBeenCalled();
|
||||
expect(resolve).toHaveBeenCalledWith({
|
||||
resolveRequestId: REQUEST_ID,
|
||||
makeloreVersion: '2.0.0',
|
||||
channel: 'stable',
|
||||
installed: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,20 +74,24 @@ function matchingMetadata() {
|
||||
};
|
||||
}
|
||||
|
||||
async function bundledResourceFixture() {
|
||||
async function bundledResourceFixture({ schemaVersion = 1, includeSdkAssets = true } = {}) {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-bundled-plugin-'));
|
||||
roots.push(root);
|
||||
const projectRoot = path.join(root, 'project');
|
||||
const sourceResources = path.join(projectRoot, 'resources');
|
||||
const pluginRoot = path.join(sourceResources, 'coding-plugins', 'example');
|
||||
const capability = {
|
||||
schemaVersion,
|
||||
pluginId: 'example.plugin',
|
||||
adapterId: 'example-adapter',
|
||||
...(schemaVersion === 2
|
||||
? { runtime: { kind: 'platform_hosted', protocol: 'makelore-hosted.v1' } }
|
||||
: {}),
|
||||
skills: [{ id: 'example', entry: '../skills/example/SKILL.md', grants: [] }],
|
||||
tools: [{ name: 'example_read' }],
|
||||
};
|
||||
await mkdir(path.join(pluginRoot, 'com.makelore'), { recursive: true });
|
||||
await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true });
|
||||
await mkdir(path.join(pluginRoot, 'skills', 'example'), { recursive: true });
|
||||
await mkdir(path.join(sourceResources, 'coding-skills', 'agent-browser'), { recursive: true });
|
||||
await writeFile(path.join(pluginRoot, 'plugin.json'), JSON.stringify({
|
||||
name: 'example.plugin',
|
||||
@@ -96,8 +100,11 @@ async function bundledResourceFixture() {
|
||||
}));
|
||||
await writeFile(path.join(pluginRoot, 'com.makelore', 'capability.json'), JSON.stringify(capability));
|
||||
await writeFile(path.join(pluginRoot, 'skills', 'example', 'SKILL.md'), '# Example Skill\n');
|
||||
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n');
|
||||
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n');
|
||||
if (includeSdkAssets) {
|
||||
await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true });
|
||||
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n');
|
||||
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n');
|
||||
}
|
||||
await writeFile(path.join(sourceResources, 'coding-skills', 'agent-browser', 'SKILL.md'), '# Browser\n');
|
||||
const resourcesDirectory = path.join(root, 'packaged');
|
||||
await cp(sourceResources, path.join(resourcesDirectory, 'resources'), { recursive: true });
|
||||
@@ -206,6 +213,20 @@ describe('final Pi product artifact verification', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts schema-2 hosted plugins whose implementation has no client SDK assets', async () => {
|
||||
const fixture = await bundledResourceFixture({ schemaVersion: 2, includeSdkAssets: false });
|
||||
await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({
|
||||
packages: [{
|
||||
id: 'example.plugin',
|
||||
skills: [{ id: 'example', path: 'skills/example/SKILL.md' }],
|
||||
sdkAssets: [],
|
||||
adapterId: 'example.plugin',
|
||||
tools: ['example_read'],
|
||||
}],
|
||||
result: 'pass',
|
||||
});
|
||||
});
|
||||
|
||||
it('proves the packaged Marketplace trust, routes, effective snapshot, and Renderer assets', () => {
|
||||
const artifact = Buffer.from(MARKETPLACE_ARTIFACT_TEXT);
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ async function fixtureRoot(): Promise<{
|
||||
mkdir(path.join(skillsDir, 'grilling'), { recursive: true }),
|
||||
mkdir(path.join(skillsDir, 'agent-browser'), { recursive: true }),
|
||||
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service'), { recursive: true }),
|
||||
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource'), { recursive: true }),
|
||||
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search'), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(path.join(projectDir, '.pi', 'skills', 'untrusted-project-skill', 'SKILL.md'), 'untrusted', 'utf8'),
|
||||
@@ -51,6 +53,16 @@ async function fixtureRoot(): Promise<{
|
||||
'---\nname: data-service\n---\n',
|
||||
'utf8',
|
||||
),
|
||||
writeFile(
|
||||
path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'),
|
||||
'---\nname: game-resource\n---\n',
|
||||
'utf8',
|
||||
),
|
||||
writeFile(
|
||||
path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'),
|
||||
'---\nname: makelore-web-search\n---\n',
|
||||
'utf8',
|
||||
),
|
||||
]);
|
||||
return { root, userDataDir, projectDir, skillsDir };
|
||||
}
|
||||
@@ -160,15 +172,21 @@ describe('Pi managed resource loader', () => {
|
||||
projectId: 'project-1',
|
||||
agentId: 'agent-1',
|
||||
prompt: 'plugin prompt',
|
||||
skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
|
||||
skillEntries: [
|
||||
{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' },
|
||||
{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' },
|
||||
{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' },
|
||||
],
|
||||
catalogRevision: 13,
|
||||
bundledSkillsDir: fixture.skillsDir,
|
||||
revision: { provider: 1, resources: 1 },
|
||||
});
|
||||
|
||||
expect(resources.skillIds).toEqual(['data-service']);
|
||||
expect(resources.skillIds).toEqual(['data-service', 'game-resource', 'makelore-web-search']);
|
||||
expect(resources.skillPaths).toEqual([
|
||||
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'),
|
||||
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'),
|
||||
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'),
|
||||
]);
|
||||
expect(resources.catalogRevision).toBe(13);
|
||||
});
|
||||
|
||||
@@ -129,6 +129,29 @@ describe('My Plugins', () => {
|
||||
expect(screen.getByRole('link', { name: '分配给伙伴' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows acquired code-owned bundled Plugins as supplied without device download actions', () => {
|
||||
const bundled = {
|
||||
...library.items[0],
|
||||
pluginId: 'makelore.web-search',
|
||||
title: '联网搜索',
|
||||
stableVersion: '1.0.0',
|
||||
betaVersion: null,
|
||||
};
|
||||
render(<MemoryRouter><MyPluginsView
|
||||
library={{ ...library, items: [bundled] }} installations={{}} state="ready"
|
||||
pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
|
||||
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
|
||||
/></MemoryRouter>);
|
||||
|
||||
expect(screen.getByText('随 MakeLore 提供')).toBeVisible();
|
||||
expect(screen.queryByText('尚未下载到设备')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '下载联网搜索' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '更新联网搜索' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '删除设备上的联网搜索' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '移除联网搜索' })).toBeVisible();
|
||||
expect(screen.getByRole('link', { name: '启用到项目' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('updates an installed Beta package only through the explicit Beta action', () => {
|
||||
const onUpdate = vi.fn();
|
||||
const onInstallBeta = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user