


























@@ -7,6 +7,12 @@ import {
77normalizeNullableString,
88} from "@openclaw/normalization-core/string-coerce";
99import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
10+import { MediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js";
11+import { extractGeminiResponse } from "../../packages/media-understanding-common/src/output-extract.js";
12+import {
13+estimateBase64Size,
14+resolveVideoMaxBase64Bytes,
15+} from "../../packages/media-understanding-common/src/video.js";
1016import {
1117collectProviderApiKeysForExecution,
1218executeWithApiKeyRotation,
@@ -19,6 +25,7 @@ import {
1925} from "../agents/provider-request-config.js";
2026import type { MsgContext } from "../auto-reply/templating.js";
2127import { applyTemplate } from "../auto-reply/templating.js";
28+import { formatCliCommand } from "../cli/command-format.js";
2229import type { ModelProviderConfig, OpenClawConfig } from "../config/types.js";
2330import type {
2431MediaUnderstandingConfig,
@@ -29,14 +36,13 @@ import { writeExternalFileWithinRoot } from "../infra/fs-safe.js";
2936import { resolveProxyFetchFromEnv } from "../infra/net/proxy-fetch.js";
3037import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
3138import { runFfmpeg } from "../media/media-services.js";
39+import {
40+getOfficialExternalPluginCatalogManifest,
41+listOfficialExternalProviderCatalogEntries,
42+} from "../plugins/official-external-plugin-catalog.js";
43+import { resolveOfficialExternalPluginRepairHint } from "../plugins/official-external-plugin-repair-hints.js";
3244import { runExec } from "../process/exec.js";
3345import { providerOperationRetryConfig } from "../provider-runtime/operation-retry.js";
34-import { MediaUnderstandingSkipError } from "../../packages/media-understanding-common/src/errors.js";
35-import { extractGeminiResponse } from "../../packages/media-understanding-common/src/output-extract.js";
36-import {
37-estimateBase64Size,
38-resolveVideoMaxBase64Bytes,
39-} from "../../packages/media-understanding-common/src/video.js";
4046import { MediaAttachmentCache } from "./attachments.js";
4147import {
4248CLI_OUTPUT_MAX_BUFFER,
@@ -666,6 +672,53 @@ function assertMinAudioSize(params: { size: number; attachmentIndex: number }):
666672);
667673}
668674675+/**
676+ * Build an actionable hint suffix for "provider not available" errors.
677+ *
678+ * Restricts the hint to ids that are owned by the official external
679+ * provider catalog — NOT the combined channel/plugin catalog — so a media
680+ * provider id like `feishu` (an official channel, not a media provider)
681+ * never emits a misleading install hint from a media-provider error.
682+ *
683+ * Tier 1: provider id is owned by an official external provider entry that
684+ * declares a `contracts.mediaUnderstandingProviders` block listing the
685+ * id — emit the catalog-backed install + registry refresh + doctor fix
686+ * commands.
687+ * Tier 2: empty string — keeps the legacy message verbatim for ids that
688+ * are not in the provider catalog (channel ids, plugin ids, unknown
689+ * ids, internal ids, etc.). Newly externalized media providers must
690+ * register with the official external provider catalog to receive the
691+ * actionable hint.
692+ */
693+export function formatMissingProviderHint(providerId: string): string {
694+const trimmed = providerId.trim();
695+if (!trimmed) {
696+return "";
697+}
698+// Look up the id only in catalog entries that declare
699+// `contracts.mediaUnderstandingProviders`. This ensures the install hint
700+// only fires for provider packages that actually own the missing
701+// media-understanding capability. Providers that have a generic `providers[]`
702+// catalog entry but no media-understanding contract (e.g. Amazon Bedrock)
703+// will not emit misleading hints.
704+const providerEntry = listOfficialExternalProviderCatalogEntries().find((entry) => {
705+const manifest = getOfficialExternalPluginCatalogManifest(entry);
706+const mediaProviders = manifest?.contracts?.mediaUnderstandingProviders ?? [];
707+return mediaProviders.some((mediaId) => mediaId === trimmed);
708+});
709+if (!providerEntry) {
710+return "";
711+}
712+// `resolveOfficialExternalPluginRepairHint` is contract-agnostic but we
713+// already validated ownership via the provider-only catalog, so the
714+// returned hint is for the correct provider entry.
715+const catalogHint = resolveOfficialExternalPluginRepairHint(trimmed);
716+if (!catalogHint) {
717+return "";
718+}
719+return ` Install the official external plugin with: ${formatCliCommand(catalogHint.installCommand)}, then run ${formatCliCommand("openclaw plugins registry --refresh")} and stop and start the gateway service, or run ${formatCliCommand(catalogHint.doctorFixCommand)} to repair automatically.`;
720+}
721+669722/** Executes one provider-backed media-understanding entry for one attachment. */
670723export async function runProviderEntry(params: {
671724capability: MediaUnderstandingCapability;
@@ -741,7 +794,9 @@ export async function runProviderEntry(params: {
741794742795const provider = getMediaUnderstandingProvider(providerId, params.providerRegistry);
743796if (!provider) {
744-throw new Error(`Media provider not available: ${providerId}`);
797+throw new Error(
798+`Media provider not available: ${providerId}${formatMissingProviderHint(providerId)}`,
799+);
745800}
746801747802// Resolve proxy-aware fetch from env vars (HTTPS_PROXY, HTTP_PROXY, etc.)
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。