diff --git a/package.json b/package.json index f572971b588..110968977c0 100644 --- a/package.json +++ b/package.json @@ -1757,6 +1757,7 @@ "@mariozechner/pi-coding-agent", "@modelcontextprotocol/sdk", "ajv", + "chalk", "chokidar", "commander", "croner", diff --git a/src/gateway/server-startup-plugins.test.ts b/src/gateway/server-startup-plugins.test.ts index 687253e59b2..ffce1a250f7 100644 --- a/src/gateway/server-startup-plugins.test.ts +++ b/src/gateway/server-startup-plugins.test.ts @@ -289,6 +289,13 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => { exactPluginIds: ["telegram"], }), ); + expect(resolveOpenClawPackageRootSync).toHaveBeenCalledWith( + expect.objectContaining({ + moduleUrl: expect.stringContaining("server-startup-plugins"), + argv1: process.argv[1], + cwd: process.cwd(), + }), + ); expect(prepareBundledPluginRuntimeLoadRoot).toHaveBeenCalledWith( expect.objectContaining({ pluginId: "telegram", diff --git a/src/gateway/server-startup-plugins.ts b/src/gateway/server-startup-plugins.ts index 1250298d501..f3d1f0b17b6 100644 --- a/src/gateway/server-startup-plugins.ts +++ b/src/gateway/server-startup-plugins.ts @@ -75,7 +75,11 @@ async function prestageGatewayBundledRuntimeDepsImpl(params: { return {}; } let repairError: unknown; - const packageRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url }); + const packageRoot = resolveOpenClawPackageRootSync({ + moduleUrl: import.meta.url, + argv1: process.argv[1], + cwd: process.cwd(), + }); if (packageRoot) { try { pruneUnknownBundledRuntimeDepsRoots({ diff --git a/src/plugins/loader-records.test.ts b/src/plugins/loader-records.test.ts index bb9f6a65e10..de0a8839477 100644 --- a/src/plugins/loader-records.test.ts +++ b/src/plugins/loader-records.test.ts @@ -29,4 +29,42 @@ describe("plugin loader records", () => { expect(record.providerIds).toEqual(["kitchen-sink-provider"]); }); + + it("preserves manifest-declared capability provider ids before runtime registration", () => { + const record = createPluginRecord({ + id: "kitchen-sink", + name: "Kitchen Sink", + source: "/tmp/kitchen-sink/index.js", + origin: "global", + enabled: true, + contracts: { + speechProviders: ["kitchen-sink-speech-provider"], + realtimeTranscriptionProviders: ["kitchen-sink-transcription-provider"], + realtimeVoiceProviders: ["kitchen-sink-voice-provider"], + mediaUnderstandingProviders: ["kitchen-sink-media-provider"], + imageGenerationProviders: ["kitchen-sink-image-provider"], + videoGenerationProviders: ["kitchen-sink-video-provider"], + musicGenerationProviders: ["kitchen-sink-music-provider"], + webFetchProviders: ["kitchen-sink-web-fetch-provider"], + webSearchProviders: ["kitchen-sink-web-search-provider"], + migrationProviders: ["kitchen-sink-migration-provider"], + memoryEmbeddingProviders: ["kitchen-sink-memory-provider"], + }, + configSchema: false, + }); + + expect(record.speechProviderIds).toEqual(["kitchen-sink-speech-provider"]); + expect(record.realtimeTranscriptionProviderIds).toEqual([ + "kitchen-sink-transcription-provider", + ]); + expect(record.realtimeVoiceProviderIds).toEqual(["kitchen-sink-voice-provider"]); + expect(record.mediaUnderstandingProviderIds).toEqual(["kitchen-sink-media-provider"]); + expect(record.imageGenerationProviderIds).toEqual(["kitchen-sink-image-provider"]); + expect(record.videoGenerationProviderIds).toEqual(["kitchen-sink-video-provider"]); + expect(record.musicGenerationProviderIds).toEqual(["kitchen-sink-music-provider"]); + expect(record.webFetchProviderIds).toEqual(["kitchen-sink-web-fetch-provider"]); + expect(record.webSearchProviderIds).toEqual(["kitchen-sink-web-search-provider"]); + expect(record.migrationProviderIds).toEqual(["kitchen-sink-migration-provider"]); + expect(record.memoryEmbeddingProviderIds).toEqual(["kitchen-sink-memory-provider"]); + }); }); diff --git a/src/plugins/loader-records.ts b/src/plugins/loader-records.ts index 0073d9b7d63..8b98665276a 100644 --- a/src/plugins/loader-records.ts +++ b/src/plugins/loader-records.ts @@ -52,18 +52,18 @@ export function createPluginRecord(params: { channelIds: [...(params.channelIds ?? [])], cliBackendIds: [], providerIds: [...(params.providerIds ?? [])], - speechProviderIds: [], - realtimeTranscriptionProviderIds: [], - realtimeVoiceProviderIds: [], - mediaUnderstandingProviderIds: [], - imageGenerationProviderIds: [], - videoGenerationProviderIds: [], - musicGenerationProviderIds: [], - webFetchProviderIds: [], - webSearchProviderIds: [], - migrationProviderIds: [], + speechProviderIds: [...(params.contracts?.speechProviders ?? [])], + realtimeTranscriptionProviderIds: [...(params.contracts?.realtimeTranscriptionProviders ?? [])], + realtimeVoiceProviderIds: [...(params.contracts?.realtimeVoiceProviders ?? [])], + mediaUnderstandingProviderIds: [...(params.contracts?.mediaUnderstandingProviders ?? [])], + imageGenerationProviderIds: [...(params.contracts?.imageGenerationProviders ?? [])], + videoGenerationProviderIds: [...(params.contracts?.videoGenerationProviders ?? [])], + musicGenerationProviderIds: [...(params.contracts?.musicGenerationProviders ?? [])], + webFetchProviderIds: [...(params.contracts?.webFetchProviders ?? [])], + webSearchProviderIds: [...(params.contracts?.webSearchProviders ?? [])], + migrationProviderIds: [...(params.contracts?.migrationProviders ?? [])], contextEngineIds: [], - memoryEmbeddingProviderIds: [], + memoryEmbeddingProviderIds: [...(params.contracts?.memoryEmbeddingProviders ?? [])], agentHarnessIds: [], gatewayMethods: [], cliCommands: [], diff --git a/src/plugins/registry.provider-like.test.ts b/src/plugins/registry.provider-like.test.ts new file mode 100644 index 00000000000..61397218487 --- /dev/null +++ b/src/plugins/registry.provider-like.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { createPluginRecord } from "./loader-records.js"; +import { createPluginRegistry } from "./registry.js"; +import type { PluginRuntime } from "./runtime/types.js"; + +function createTestRegistry() { + return createPluginRegistry({ + logger: { + info() {}, + warn() {}, + error() {}, + debug() {}, + }, + runtime: {} as PluginRuntime, + activateGlobalSideEffects: false, + }); +} + +describe("plugin registry provider-like registrations", () => { + it("does not duplicate manifest-declared capability provider ids during runtime registration", () => { + const pluginRegistry = createTestRegistry(); + const record = createPluginRecord({ + id: "kitchen-sink", + name: "Kitchen Sink", + source: "/tmp/kitchen-sink/index.js", + origin: "global", + enabled: true, + contracts: { + speechProviders: ["kitchen-sink-speech-provider"], + }, + configSchema: false, + }); + + pluginRegistry.registerSpeechProvider(record, { + id: "kitchen-sink-speech-provider", + label: "Kitchen Sink Speech", + isConfigured: () => true, + synthesize: async () => ({ + audioBuffer: Buffer.alloc(0), + fileExtension: "mp3", + outputFormat: "audio/mpeg", + voiceCompatible: true, + }), + }); + + expect(record.speechProviderIds).toEqual(["kitchen-sink-speech-provider"]); + expect(pluginRegistry.registry.speechProviders).toHaveLength(1); + }); +}); diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts index b38f32d83db..aef8bf069c0 100644 --- a/src/plugins/registry.ts +++ b/src/plugins/registry.ts @@ -970,7 +970,9 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) { }); return; } - params.ownedIds.push(id); + if (!params.ownedIds.includes(id)) { + params.ownedIds.push(id); + } params.registrations.push({ pluginId: record.id, pluginName: record.name, diff --git a/src/plugins/status.registry-snapshot.test.ts b/src/plugins/status.registry-snapshot.test.ts index eaddf388bb7..91661a1acc7 100644 --- a/src/plugins/status.registry-snapshot.test.ts +++ b/src/plugins/status.registry-snapshot.test.ts @@ -33,6 +33,11 @@ describe("buildPluginRegistrySnapshotReport", () => { description: "Manifest-backed list metadata", version: "1.2.3", providers: ["indexed-provider"], + contracts: { + speechProviders: ["indexed-speech-provider"], + realtimeTranscriptionProviders: ["indexed-transcription-provider"], + realtimeVoiceProviders: ["indexed-voice-provider"], + }, commandAliases: [{ name: "indexed-demo" }], configSchema: { type: "object", @@ -58,6 +63,9 @@ describe("buildPluginRegistrySnapshotReport", () => { version: "9.8.7", format: "openclaw", providerIds: ["indexed-provider"], + speechProviderIds: ["indexed-speech-provider"], + realtimeTranscriptionProviderIds: ["indexed-transcription-provider"], + realtimeVoiceProviderIds: ["indexed-voice-provider"], commands: ["indexed-demo"], source: fs.realpathSync(fixture.runtimeSource), status: "loaded", diff --git a/src/plugins/status.ts b/src/plugins/status.ts index 1d16ca3eb20..4e7e285370e 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -200,17 +200,19 @@ function buildPluginRecordFromInstalledIndex( channelIds: [...(manifest?.channels ?? [])], cliBackendIds: [...(manifest?.cliBackends ?? []), ...(manifest?.setup?.cliBackends ?? [])], providerIds: [...(manifest?.providers ?? [])], - speechProviderIds: [], - realtimeTranscriptionProviderIds: [], - realtimeVoiceProviderIds: [], - mediaUnderstandingProviderIds: [], - imageGenerationProviderIds: [], - videoGenerationProviderIds: [], - musicGenerationProviderIds: [], - webFetchProviderIds: [], - webSearchProviderIds: [], - migrationProviderIds: [], - memoryEmbeddingProviderIds: [], + speechProviderIds: [...(manifest?.contracts?.speechProviders ?? [])], + realtimeTranscriptionProviderIds: [ + ...(manifest?.contracts?.realtimeTranscriptionProviders ?? []), + ], + realtimeVoiceProviderIds: [...(manifest?.contracts?.realtimeVoiceProviders ?? [])], + mediaUnderstandingProviderIds: [...(manifest?.contracts?.mediaUnderstandingProviders ?? [])], + imageGenerationProviderIds: [...(manifest?.contracts?.imageGenerationProviders ?? [])], + videoGenerationProviderIds: [...(manifest?.contracts?.videoGenerationProviders ?? [])], + musicGenerationProviderIds: [...(manifest?.contracts?.musicGenerationProviders ?? [])], + webFetchProviderIds: [...(manifest?.contracts?.webFetchProviders ?? [])], + webSearchProviderIds: [...(manifest?.contracts?.webSearchProviders ?? [])], + migrationProviderIds: [...(manifest?.contracts?.migrationProviders ?? [])], + memoryEmbeddingProviderIds: [...(manifest?.contracts?.memoryEmbeddingProviders ?? [])], agentHarnessIds: [], gatewayMethods: [], cliCommands: [],