mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(plugins): supplement external catalog contracts
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/catalog: supplement lagging official external WeCom and Yuanbao npm manifests with channel config descriptors and declared tool contracts from the OpenClaw catalog, so trusted package sweeps no longer fail because external package metadata trails the host contract. Thanks @vincentkoc.
|
||||
- Plugins/install: let trusted official `@openclaw/*` catalog installs recover when npm `latest` points at a prerelease by falling back to the newest stable version, or by allowing prerelease-only launch packages with a warning instead of making beta/development plugin sweeps fail at install time. Thanks @vincentkoc.
|
||||
- Google Meet: use the local call-control microphone button instead of disabled remote participant mute buttons, and block realtime speech when the OpenClaw Meet microphone remains muted.
|
||||
- Google Meet: refresh realtime browser state during status and retry delayed speech after Meet finishes joining, so a just-opened in-call tab no longer leaves speech stuck behind stale `not-in-call` health.
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"id": "wecom-openclaw-plugin",
|
||||
"label": "WeCom"
|
||||
},
|
||||
"contracts": {
|
||||
"tools": ["wecom_mcp"]
|
||||
},
|
||||
"channel": {
|
||||
"id": "wecom",
|
||||
"label": "WeCom",
|
||||
@@ -21,6 +24,16 @@
|
||||
"aliases": ["qywx", "wework", "enterprise-wechat"],
|
||||
"order": 45
|
||||
},
|
||||
"channelConfigs": {
|
||||
"wecom": {
|
||||
"label": "WeCom",
|
||||
"description": "Enterprise WeChat conversation channel.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@wecom/wecom-openclaw-plugin@2026.4.23",
|
||||
"defaultChoice": "npm",
|
||||
@@ -38,6 +51,9 @@
|
||||
"id": "openclaw-plugin-yuanbao",
|
||||
"label": "Yuanbao"
|
||||
},
|
||||
"contracts": {
|
||||
"tools": ["query_group_info", "query_session_members", "yuanbao_remind"]
|
||||
},
|
||||
"channel": {
|
||||
"id": "yuanbao",
|
||||
"label": "Yuanbao",
|
||||
@@ -49,6 +65,16 @@
|
||||
"aliases": ["yuanbao", "yb", "tencent-yuanbao", "元宝"],
|
||||
"order": 85
|
||||
},
|
||||
"channelConfigs": {
|
||||
"yuanbao": {
|
||||
"label": "Yuanbao",
|
||||
"description": "Tencent Yuanbao AI assistant channel.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "openclaw-plugin-yuanbao@2.11.0",
|
||||
"defaultChoice": "npm",
|
||||
|
||||
@@ -62,6 +62,7 @@ function createPluginCandidate(params: {
|
||||
origin: "bundled" | "global" | "workspace" | "config";
|
||||
format?: "openclaw" | "bundle";
|
||||
bundleFormat?: "codex" | "claude" | "cursor";
|
||||
packageName?: string;
|
||||
packageManifest?: OpenClawPackageManifest;
|
||||
packageDir?: string;
|
||||
bundledManifest?: PluginCandidate["bundledManifest"];
|
||||
@@ -74,6 +75,7 @@ function createPluginCandidate(params: {
|
||||
origin: params.origin,
|
||||
format: params.format,
|
||||
bundleFormat: params.bundleFormat,
|
||||
packageName: params.packageName,
|
||||
packageManifest: params.packageManifest,
|
||||
packageDir: params.packageDir,
|
||||
bundledManifest: params.bundledManifest,
|
||||
@@ -1098,6 +1100,39 @@ describe("loadPluginManifestRegistry", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("hydrates supplemental official external catalog contracts for lagging npm manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "wecom-openclaw-plugin",
|
||||
channels: ["wecom"],
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const registry = loadRegistry([
|
||||
createPluginCandidate({
|
||||
idHint: "wecom-openclaw-plugin",
|
||||
rootDir: dir,
|
||||
origin: "global",
|
||||
packageName: "@wecom/wecom-openclaw-plugin",
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(registry.plugins[0]?.contracts?.tools).toEqual(["wecom_mcp"]);
|
||||
expect(registry.plugins[0]?.channelConfigs?.wecom).toEqual(
|
||||
expect.objectContaining({
|
||||
label: "WeCom",
|
||||
schema: expect.objectContaining({
|
||||
type: "object",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
registry.diagnostics.some((diagnostic) =>
|
||||
diagnostic.message.includes("without channelConfigs metadata"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("drops prototype-polluting channel config keys from plugin manifests", () => {
|
||||
const dir = makeTempDir();
|
||||
writeTextFile(
|
||||
|
||||
@@ -43,6 +43,10 @@ import {
|
||||
type PluginPackageInstall,
|
||||
} from "./manifest.js";
|
||||
import { checkMinHostVersion } from "./min-host-version.js";
|
||||
import {
|
||||
getOfficialExternalPluginCatalogEntryForPackage,
|
||||
getOfficialExternalPluginCatalogManifest,
|
||||
} from "./official-external-plugin-catalog.js";
|
||||
import { isPathInside, safeRealpathSync } from "./path-safety.js";
|
||||
import type { PluginKind } from "./plugin-kind.types.js";
|
||||
import type { PluginOrigin } from "./plugin-origin.types.js";
|
||||
@@ -258,6 +262,72 @@ function mergePackageChannelMetaIntoChannelConfigs(params: {
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mergeContractLists(
|
||||
left: readonly string[] | undefined,
|
||||
right: readonly string[] | undefined,
|
||||
): string[] | undefined {
|
||||
const merged = [...(left ?? []), ...(right ?? [])]
|
||||
.map((value) => value.trim())
|
||||
.filter((value, index, all) => value.length > 0 && all.indexOf(value) === index);
|
||||
return merged.length > 0 ? merged : undefined;
|
||||
}
|
||||
|
||||
function mergeManifestContracts(
|
||||
manifestContracts: PluginManifestContracts | undefined,
|
||||
catalogContracts: PluginManifestContracts | undefined,
|
||||
): PluginManifestContracts | undefined {
|
||||
if (!catalogContracts) {
|
||||
return manifestContracts;
|
||||
}
|
||||
const contracts: PluginManifestContracts = {};
|
||||
for (const key of [
|
||||
"embeddedExtensionFactories",
|
||||
"agentToolResultMiddleware",
|
||||
"externalAuthProviders",
|
||||
"memoryEmbeddingProviders",
|
||||
"speechProviders",
|
||||
"realtimeTranscriptionProviders",
|
||||
"realtimeVoiceProviders",
|
||||
"mediaUnderstandingProviders",
|
||||
"documentExtractors",
|
||||
"imageGenerationProviders",
|
||||
"videoGenerationProviders",
|
||||
"musicGenerationProviders",
|
||||
"webContentExtractors",
|
||||
"webFetchProviders",
|
||||
"webSearchProviders",
|
||||
"migrationProviders",
|
||||
"tools",
|
||||
] as const) {
|
||||
const merged = mergeContractLists(manifestContracts?.[key], catalogContracts[key]);
|
||||
if (merged) {
|
||||
contracts[key] = merged;
|
||||
}
|
||||
}
|
||||
return Object.keys(contracts).length > 0 ? contracts : undefined;
|
||||
}
|
||||
|
||||
function mergeCatalogChannelConfigs(params: {
|
||||
manifestChannelConfigs?: Record<string, PluginManifestChannelConfig>;
|
||||
catalogChannelConfigs?: Record<string, PluginManifestChannelConfig>;
|
||||
}): Record<string, PluginManifestChannelConfig> | undefined {
|
||||
if (!params.catalogChannelConfigs) {
|
||||
return params.manifestChannelConfigs;
|
||||
}
|
||||
const merged: Record<string, PluginManifestChannelConfig> = Object.create(null);
|
||||
for (const [key, value] of Object.entries(params.catalogChannelConfigs)) {
|
||||
if (!isBlockedObjectKey(key)) {
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(params.manifestChannelConfigs ?? {})) {
|
||||
if (!isBlockedObjectKey(key)) {
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||
}
|
||||
|
||||
function buildRecord(params: {
|
||||
manifest: PluginManifest;
|
||||
candidate: PluginCandidate;
|
||||
@@ -274,8 +344,17 @@ function buildRecord(params: {
|
||||
packageManifest: params.candidate.packageManifest,
|
||||
})
|
||||
: params.manifest.channelConfigs;
|
||||
const officialCatalogManifest =
|
||||
params.candidate.origin !== "bundled"
|
||||
? getOfficialExternalPluginCatalogManifest(
|
||||
getOfficialExternalPluginCatalogEntryForPackage(params.candidate.packageName) ?? {},
|
||||
)
|
||||
: undefined;
|
||||
const channelConfigs = mergePackageChannelMetaIntoChannelConfigs({
|
||||
channelConfigs: manifestChannelConfigs,
|
||||
channelConfigs: mergeCatalogChannelConfigs({
|
||||
manifestChannelConfigs,
|
||||
catalogChannelConfigs: officialCatalogManifest?.channelConfigs,
|
||||
}),
|
||||
packageChannel: params.candidate.packageManifest?.channel,
|
||||
});
|
||||
const packageChannelCommands = normalizePackageChannelCommands(
|
||||
@@ -341,7 +420,10 @@ function buildRecord(params: {
|
||||
schemaCacheKey: params.schemaCacheKey,
|
||||
configSchema: params.configSchema,
|
||||
configUiHints: params.manifest.uiHints,
|
||||
contracts: params.manifest.contracts,
|
||||
contracts: mergeManifestContracts(
|
||||
params.manifest.contracts,
|
||||
officialCatalogManifest?.contracts,
|
||||
),
|
||||
mediaUnderstandingProviderMetadata: params.manifest.mediaUnderstandingProviderMetadata,
|
||||
imageGenerationProviderMetadata: params.manifest.imageGenerationProviderMetadata,
|
||||
videoGenerationProviderMetadata: params.manifest.videoGenerationProviderMetadata,
|
||||
|
||||
@@ -4,7 +4,11 @@ import officialExternalProviderCatalog from "../../scripts/lib/official-external
|
||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { PluginPackageInstall } from "./manifest.js";
|
||||
import type {
|
||||
PluginManifestChannelConfig,
|
||||
PluginManifestContracts,
|
||||
PluginPackageInstall,
|
||||
} from "./manifest.js";
|
||||
|
||||
type ManifestKey = typeof MANIFEST_KEY;
|
||||
|
||||
@@ -60,6 +64,8 @@ export type OfficialExternalPluginCatalogManifest = {
|
||||
providers?: readonly OfficialExternalProviderCatalogProvider[];
|
||||
webSearchProviders?: readonly OfficialExternalWebSearchProvider[];
|
||||
install?: PluginPackageInstall;
|
||||
contracts?: PluginManifestContracts;
|
||||
channelConfigs?: Record<string, PluginManifestChannelConfig>;
|
||||
};
|
||||
|
||||
export type OfficialExternalPluginCatalogEntry = {
|
||||
@@ -198,3 +204,15 @@ export function getOfficialExternalPluginCatalogEntry(
|
||||
resolveOfficialExternalPluginLookupIds(entry).includes(normalized),
|
||||
);
|
||||
}
|
||||
|
||||
export function getOfficialExternalPluginCatalogEntryForPackage(
|
||||
packageName: string | undefined,
|
||||
): OfficialExternalPluginCatalogEntry | undefined {
|
||||
const normalized = packageName?.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return listOfficialExternalPluginCatalogEntries().find(
|
||||
(entry) => normalizeOptionalString(entry.name) === normalized,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user