mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix: start configured generation providers
This commit is contained in:
@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- WhatsApp/onboarding: canonicalize setup and pairing allowlist entries to WhatsApp's digit-only phone ids while still accepting E.164, JID, and `whatsapp:` inputs, so personal-phone allowlists match WhatsApp Web sender ids after setup. Thanks @vincentkoc.
|
||||
- Gateway/startup: load provider plugins that own explicitly configured image, video, or music generation defaults so generation tools become live after gateway restart instead of remaining catalog-only. Fixes #77244. Thanks @buyuangtampan, @Nikoxx99, and @vincentkoc.
|
||||
- Slack/subagents: keep resumed parent `message.send` calls in the originating Slack thread when ambient session thread context is present, and suppress successful silent child completion rows from follow-up findings. Thanks @bek91.
|
||||
- Infra/Windows: skip the POSIX `/tmp/openclaw` preferred path on Windows in `resolvePreferredOpenClawTmpDir` so log files, TTS temp files, and other writes land in `%TEMP%\openclaw-<uid>` instead of `C:\tmp\openclaw`. Fixes #60713. Thanks @juan-flores077.
|
||||
- Gateway/diagnostics: make stuck-session recovery outcome-driven and generation-guarded, add `diagnostics.stuckSessionAbortMs`, and emit structured recovery requested/completed events so stale or skipped recovery no longer looks like a successful abort.
|
||||
|
||||
@@ -164,6 +164,10 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
|
||||
enabledByDefault: true,
|
||||
providers: ["openai", "openai-codex"],
|
||||
cliBackends: ["codex-cli"],
|
||||
contracts: {
|
||||
imageGenerationProviders: ["openai"],
|
||||
videoGenerationProviders: ["openai"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "google",
|
||||
@@ -172,6 +176,11 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
|
||||
enabledByDefault: true,
|
||||
providers: ["google", "google-gemini-cli"],
|
||||
cliBackends: ["google-gemini-cli"],
|
||||
contracts: {
|
||||
imageGenerationProviders: ["google"],
|
||||
videoGenerationProviders: ["google"],
|
||||
musicGenerationProviders: ["google"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "codex",
|
||||
@@ -754,6 +763,53 @@ describe("resolveGatewayStartupPluginIds", () => {
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"includes bundled generation providers configured by media defaults at startup",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "openai/gpt-image-2",
|
||||
fallbacks: ["google/gemini-3-pro-image-preview"],
|
||||
},
|
||||
videoGenerationModel: {
|
||||
primary: "google/veo-3.1-fast-generate-preview",
|
||||
},
|
||||
musicGenerationModel: {
|
||||
primary: "google/lyria-3-clip-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
["browser", "openai", "google", "memory-core"],
|
||||
],
|
||||
[
|
||||
"honors explicit plugin disablement for configured generation providers",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: { primary: "google/gemini-3-pro-image-preview" },
|
||||
},
|
||||
},
|
||||
plugins: { entries: { google: { enabled: false } } },
|
||||
} as OpenClawConfig,
|
||||
["browser", "memory-core"],
|
||||
],
|
||||
[
|
||||
"keeps configured generation providers behind restrictive allowlists",
|
||||
{
|
||||
channels: {},
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: { primary: "google/gemini-3-pro-image-preview" },
|
||||
},
|
||||
},
|
||||
plugins: { allow: ["browser"] },
|
||||
} as OpenClawConfig,
|
||||
["browser"],
|
||||
],
|
||||
[
|
||||
"includes explicitly enabled non-channel sidecars in startup scope",
|
||||
createStartupConfig({
|
||||
|
||||
@@ -39,6 +39,11 @@ export type GatewayStartupPluginPlan = {
|
||||
};
|
||||
|
||||
type NormalizedPluginsConfig = ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
type GenerationProviderContractKey =
|
||||
| "imageGenerationProviders"
|
||||
| "videoGenerationProviders"
|
||||
| "musicGenerationProviders";
|
||||
type ConfiguredGenerationProviderIds = Record<GenerationProviderContractKey, ReadonlySet<string>>;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
@@ -209,6 +214,123 @@ function manifestOwnsConfiguredSpeechProvider(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function listModelProviderRefs(value: unknown): string[] {
|
||||
if (typeof value === "string") {
|
||||
return [value];
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
return [];
|
||||
}
|
||||
const refs: string[] = [];
|
||||
if (typeof value.primary === "string") {
|
||||
refs.push(value.primary);
|
||||
}
|
||||
if (Array.isArray(value.fallbacks)) {
|
||||
for (const fallback of value.fallbacks) {
|
||||
if (typeof fallback === "string") {
|
||||
refs.push(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function collectModelProviderIds(value: unknown): ReadonlySet<string> {
|
||||
return new Set(
|
||||
listModelProviderRefs(value)
|
||||
.map((ref) => {
|
||||
const slashIndex = ref.indexOf("/");
|
||||
return slashIndex > 0 ? normalizeOptionalLowercaseString(ref.slice(0, slashIndex)) : "";
|
||||
})
|
||||
.filter((providerId): providerId is string => Boolean(providerId)),
|
||||
);
|
||||
}
|
||||
|
||||
function collectConfiguredGenerationProviderIds(
|
||||
config: OpenClawConfig,
|
||||
): ConfiguredGenerationProviderIds {
|
||||
const defaults = config.agents?.defaults;
|
||||
return {
|
||||
imageGenerationProviders: collectModelProviderIds(defaults?.imageGenerationModel),
|
||||
videoGenerationProviders: collectModelProviderIds(defaults?.videoGenerationModel),
|
||||
musicGenerationProviders: collectModelProviderIds(defaults?.musicGenerationModel),
|
||||
};
|
||||
}
|
||||
|
||||
function manifestOwnsConfiguredGenerationProvider(params: {
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
configuredGenerationProviderIds: ConfiguredGenerationProviderIds;
|
||||
}): boolean {
|
||||
for (const contractKey of [
|
||||
"imageGenerationProviders",
|
||||
"videoGenerationProviders",
|
||||
"musicGenerationProviders",
|
||||
] as const) {
|
||||
const configuredProviderIds = params.configuredGenerationProviderIds[contractKey];
|
||||
if (configuredProviderIds.size === 0) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(params.manifest?.contracts?.[contractKey] ?? []).some((providerId) => {
|
||||
const normalized = normalizeOptionalLowercaseString(providerId);
|
||||
return normalized ? configuredProviderIds.has(normalized) : false;
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function canStartConfiguredGenerationProviderPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
config: OpenClawConfig;
|
||||
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
activationSource: {
|
||||
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
rootConfig?: OpenClawConfig;
|
||||
};
|
||||
configuredGenerationProviderIds: ConfiguredGenerationProviderIds;
|
||||
platform?: NodeJS.Platform;
|
||||
}): boolean {
|
||||
if (
|
||||
!manifestOwnsConfiguredGenerationProvider({
|
||||
manifest: params.manifest,
|
||||
configuredGenerationProviderIds: params.configuredGenerationProviderIds,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
||||
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
||||
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: params.plugin.pluginId,
|
||||
origin: params.plugin.origin,
|
||||
config: params.pluginsConfig,
|
||||
rootConfig: params.config,
|
||||
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
|
||||
activationSource: params.activationSource,
|
||||
});
|
||||
return (
|
||||
activationState.enabled &&
|
||||
(params.plugin.origin === "bundled" || activationState.explicitlyEnabled)
|
||||
);
|
||||
}
|
||||
|
||||
function canStartConfiguredSpeechProviderPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
@@ -512,6 +634,8 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
|
||||
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
|
||||
const manifestLookup = createManifestRegistryLookup(params.manifestRegistry);
|
||||
const configuredSpeechProviderIds = collectConfiguredSpeechProviderIds(activationSourceConfig);
|
||||
const configuredGenerationProviderIds =
|
||||
collectConfiguredGenerationProviderIds(activationSourceConfig);
|
||||
const normalizePluginId = createPluginRegistryIdNormalizer(params.index, {
|
||||
manifestRegistry: params.manifestRegistry,
|
||||
});
|
||||
@@ -581,6 +705,19 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
canStartConfiguredGenerationProviderPlugin({
|
||||
plugin,
|
||||
manifest,
|
||||
config: params.config,
|
||||
pluginsConfig,
|
||||
activationSource,
|
||||
configuredGenerationProviderIds,
|
||||
platform: params.platform,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
canStartExplicitHookPlugin({
|
||||
plugin,
|
||||
|
||||
Reference in New Issue
Block a user