mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:10:51 +00:00
fix(providers): keep setup flow on cold metadata
This commit is contained in:
@@ -72,8 +72,8 @@ or fallback behavior without changing runtime loading semantics.
|
||||
Setup discovery now prefers descriptor-owned ids such as `setup.providers` and
|
||||
`setup.cliBackends` to narrow candidate plugins before it falls back to
|
||||
`setup-api` for plugins that still need setup-time runtime hooks. Provider
|
||||
setup flow uses manifest `providerAuthChoices` first, then falls back to
|
||||
runtime wizard choices and install-catalog choices for compatibility. Explicit
|
||||
setup lists use manifest `providerAuthChoices`, descriptor-derived setup
|
||||
choices, and install-catalog metadata without loading provider runtime. Explicit
|
||||
`setup.requiresRuntime: false` is a descriptor-only cutoff; omitted
|
||||
`requiresRuntime` keeps the legacy setup-api fallback for compatibility. If more
|
||||
than one discovered plugin claims the same normalized setup provider or CLI
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
resolveProviderModelPickerFlowContributions,
|
||||
resolveProviderModelPickerFlowEntries,
|
||||
} from "../flows/provider-flow.js";
|
||||
} from "../flows/provider-flow.runtime.js";
|
||||
import { runProviderPluginAuthMethod } from "../plugins/provider-auth-choice.js";
|
||||
import {
|
||||
resolveProviderPluginChoice,
|
||||
|
||||
79
src/flows/provider-flow.runtime.ts
Normal file
79
src/flows/provider-flow.runtime.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
resolveProviderModelPickerEntries,
|
||||
type ProviderModelPickerEntry,
|
||||
} from "../plugins/provider-wizard.js";
|
||||
import { resolvePluginProviders } from "../plugins/providers.runtime.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import type { FlowContribution } from "./types.js";
|
||||
import { sortFlowContributionsByLabel } from "./types.js";
|
||||
|
||||
export type ProviderModelPickerFlowEntry = ProviderModelPickerEntry;
|
||||
|
||||
export type ProviderModelPickerFlowContribution = FlowContribution & {
|
||||
kind: "provider";
|
||||
surface: "model-picker";
|
||||
providerId: string;
|
||||
option: ProviderModelPickerFlowEntry;
|
||||
source: "runtime";
|
||||
};
|
||||
|
||||
function resolveProviderDocsById(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Map<string, string> {
|
||||
return new Map(
|
||||
resolvePluginProviders({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
mode: "setup",
|
||||
})
|
||||
.filter((provider): provider is ProviderPlugin & { docsPath: string } =>
|
||||
Boolean(normalizeOptionalString(provider.docsPath)),
|
||||
)
|
||||
.map((provider) => [provider.id, normalizeOptionalString(provider.docsPath)!]),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveProviderModelPickerFlowEntries(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderModelPickerFlowEntry[] {
|
||||
return resolveProviderModelPickerFlowContributions(params).map(
|
||||
(contribution) => contribution.option,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveProviderModelPickerFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderModelPickerFlowContribution[] {
|
||||
const docsByProvider = resolveProviderDocsById(params ?? {});
|
||||
return sortFlowContributionsByLabel(
|
||||
resolveProviderModelPickerEntries(params ?? {}).map((entry) => {
|
||||
const providerId = entry.value.startsWith("provider-plugin:")
|
||||
? entry.value.slice("provider-plugin:".length).split(":")[0]
|
||||
: entry.value;
|
||||
return {
|
||||
id: `provider:model-picker:${entry.value}`,
|
||||
kind: "provider" as const,
|
||||
surface: "model-picker" as const,
|
||||
providerId,
|
||||
option: {
|
||||
value: entry.value,
|
||||
label: entry.label,
|
||||
...(entry.hint ? { hint: entry.hint } : {}),
|
||||
...(docsByProvider.get(providerId)
|
||||
? { docs: { path: docsByProvider.get(providerId)! } }
|
||||
: {}),
|
||||
},
|
||||
source: "runtime" as const,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -41,10 +41,8 @@ vi.mock("../plugins/providers.runtime.js", () => ({
|
||||
resolvePluginProviders,
|
||||
}));
|
||||
|
||||
import {
|
||||
resolveProviderModelPickerFlowContributions,
|
||||
resolveProviderSetupFlowContributions,
|
||||
} from "./provider-flow.js";
|
||||
import { resolveProviderSetupFlowContributions } from "./provider-flow.js";
|
||||
import { resolveProviderModelPickerFlowContributions } from "./provider-flow.runtime.js";
|
||||
|
||||
describe("provider flow install catalog contributions", () => {
|
||||
beforeEach(() => {
|
||||
@@ -106,9 +104,11 @@ describe("provider flow install catalog contributions", () => {
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
}),
|
||||
);
|
||||
expect(resolveProviderWizardOptions).not.toHaveBeenCalled();
|
||||
expect(resolvePluginProviders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers manifest setup contributions over duplicate runtime and install-catalog entries", () => {
|
||||
it("prefers manifest setup contributions over duplicate install-catalog entries", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "openai",
|
||||
@@ -118,14 +118,6 @@ describe("provider flow install catalog contributions", () => {
|
||||
choiceLabel: "OpenAI API key",
|
||||
},
|
||||
]);
|
||||
resolveProviderWizardOptions.mockReturnValue([
|
||||
{
|
||||
value: "openai-api-key",
|
||||
label: "Runtime OpenAI API key",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
},
|
||||
]);
|
||||
resolveProviderInstallCatalogEntries.mockReturnValue([
|
||||
{
|
||||
pluginId: "openai",
|
||||
@@ -159,6 +151,7 @@ describe("provider flow install catalog contributions", () => {
|
||||
source: "manifest",
|
||||
},
|
||||
]);
|
||||
expect(resolveProviderWizardOptions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces install-catalog provider choices when runtime setup options are absent", () => {
|
||||
@@ -299,7 +292,7 @@ describe("provider flow install catalog contributions", () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("prefers runtime setup contributions over duplicate install-catalog entries", () => {
|
||||
it("keeps setup contributions on cold metadata instead of runtime wizard options", () => {
|
||||
resolveProviderWizardOptions.mockReturnValue([
|
||||
{
|
||||
value: "openai-api-key",
|
||||
@@ -331,6 +324,7 @@ describe("provider flow install catalog contributions", () => {
|
||||
kind: "provider",
|
||||
surface: "setup",
|
||||
providerId: "openai",
|
||||
pluginId: "openai",
|
||||
option: {
|
||||
value: "openai-api-key",
|
||||
label: "OpenAI API key",
|
||||
@@ -339,9 +333,11 @@ describe("provider flow install catalog contributions", () => {
|
||||
label: "OpenAI",
|
||||
},
|
||||
},
|
||||
source: "runtime",
|
||||
source: "install-catalog",
|
||||
},
|
||||
]);
|
||||
expect(resolveProviderWizardOptions).not.toHaveBeenCalled();
|
||||
expect(resolvePluginProviders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps docs attached to runtime model-picker contributions", () => {
|
||||
|
||||
@@ -2,13 +2,6 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../plugins/config-state.js";
|
||||
import { resolveManifestProviderAuthChoices } from "../plugins/provider-auth-choices.js";
|
||||
import { resolveProviderInstallCatalogEntries } from "../plugins/provider-install-catalog.js";
|
||||
import {
|
||||
resolveProviderModelPickerEntries,
|
||||
resolveProviderWizardOptions,
|
||||
} from "../plugins/provider-wizard.js";
|
||||
import { resolvePluginProviders } from "../plugins/providers.runtime.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import type { FlowContribution, FlowOption } from "./types.js";
|
||||
import { sortFlowContributionsByLabel } from "./types.js";
|
||||
|
||||
@@ -29,15 +22,7 @@ export type ProviderSetupFlowContribution = FlowContribution & {
|
||||
pluginId?: string;
|
||||
option: ProviderSetupFlowOption;
|
||||
onboardingScopes?: ProviderFlowScope[];
|
||||
source: "manifest" | "runtime" | "install-catalog";
|
||||
};
|
||||
|
||||
export type ProviderModelPickerFlowContribution = FlowContribution & {
|
||||
kind: "provider";
|
||||
surface: "model-picker";
|
||||
providerId: string;
|
||||
option: ProviderModelPickerFlowEntry;
|
||||
source: "runtime";
|
||||
source: "manifest" | "install-catalog";
|
||||
};
|
||||
|
||||
function includesProviderFlowScope(
|
||||
@@ -47,25 +32,6 @@ function includesProviderFlowScope(
|
||||
return scopes ? scopes.includes(scope) : scope === DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
}
|
||||
|
||||
function resolveProviderDocsById(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Map<string, string> {
|
||||
return new Map(
|
||||
resolvePluginProviders({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
mode: "setup",
|
||||
})
|
||||
.filter((provider): provider is ProviderPlugin & { docsPath: string } =>
|
||||
Boolean(normalizeOptionalString(provider.docsPath)),
|
||||
)
|
||||
.map((provider) => [provider.id, normalizeOptionalString(provider.docsPath)!]),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveInstallCatalogProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
@@ -174,7 +140,6 @@ export function resolveProviderSetupFlowContributions(params?: {
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
const docsByProvider = resolveProviderDocsById(params ?? {});
|
||||
const manifestContributions = resolveManifestProviderSetupFlowContributions({
|
||||
...params,
|
||||
scope,
|
||||
@@ -182,92 +147,11 @@ export function resolveProviderSetupFlowContributions(params?: {
|
||||
const seenOptionValues = new Set(
|
||||
manifestContributions.map((contribution) => contribution.option.value),
|
||||
);
|
||||
const runtimeContributions = resolveProviderWizardOptions(params ?? {})
|
||||
.filter((option) => includesProviderFlowScope(option.onboardingScopes, scope))
|
||||
.filter((option) => !seenOptionValues.has(option.value))
|
||||
.map((option) =>
|
||||
Object.assign(
|
||||
{
|
||||
id: `provider:setup:${option.value}`,
|
||||
kind: `provider` as const,
|
||||
surface: `setup` as const,
|
||||
providerId: option.groupId,
|
||||
option: {
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
...(option.hint ? { hint: option.hint } : {}),
|
||||
...(option.assistantPriority !== undefined
|
||||
? { assistantPriority: option.assistantPriority }
|
||||
: {}),
|
||||
...(option.assistantVisibility
|
||||
? { assistantVisibility: option.assistantVisibility }
|
||||
: {}),
|
||||
group: {
|
||||
id: option.groupId,
|
||||
label: option.groupLabel,
|
||||
...(option.groupHint ? { hint: option.groupHint } : {}),
|
||||
},
|
||||
...(docsByProvider.get(option.groupId)
|
||||
? { docs: { path: docsByProvider.get(option.groupId)! } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
option.onboardingScopes ? { onboardingScopes: [...option.onboardingScopes] } : {},
|
||||
{ source: `runtime` as const },
|
||||
),
|
||||
);
|
||||
for (const contribution of runtimeContributions) {
|
||||
seenOptionValues.add(contribution.option.value);
|
||||
}
|
||||
const installCatalogContributions = resolveInstallCatalogProviderSetupFlowContributions({
|
||||
...params,
|
||||
scope,
|
||||
}).filter((contribution) => !seenOptionValues.has(contribution.option.value));
|
||||
return sortFlowContributionsByLabel([
|
||||
...manifestContributions,
|
||||
...runtimeContributions,
|
||||
...installCatalogContributions,
|
||||
]);
|
||||
}
|
||||
|
||||
export function resolveProviderModelPickerFlowEntries(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderModelPickerFlowEntry[] {
|
||||
return resolveProviderModelPickerFlowContributions(params).map(
|
||||
(contribution) => contribution.option,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveProviderModelPickerFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderModelPickerFlowContribution[] {
|
||||
const docsByProvider = resolveProviderDocsById(params ?? {});
|
||||
return sortFlowContributionsByLabel(
|
||||
resolveProviderModelPickerEntries(params ?? {}).map((entry) => {
|
||||
const providerId = entry.value.startsWith("provider-plugin:")
|
||||
? entry.value.slice("provider-plugin:".length).split(":")[0]
|
||||
: entry.value;
|
||||
return {
|
||||
id: `provider:model-picker:${entry.value}`,
|
||||
kind: "provider" as const,
|
||||
surface: "model-picker" as const,
|
||||
providerId,
|
||||
option: {
|
||||
value: entry.value,
|
||||
label: entry.label,
|
||||
...(entry.hint ? { hint: entry.hint } : {}),
|
||||
...(docsByProvider.get(providerId)
|
||||
? { docs: { path: docsByProvider.get(providerId)! } }
|
||||
: {}),
|
||||
},
|
||||
source: "runtime" as const,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return sortFlowContributionsByLabel([...manifestContributions, ...installCatalogContributions]);
|
||||
}
|
||||
|
||||
export { includesProviderFlowScope };
|
||||
|
||||
Reference in New Issue
Block a user