


























@@ -7,6 +7,7 @@ import {
77normalizeLowercaseStringOrEmpty,
88normalizeOptionalString,
99} from "../shared/string-coerce.js";
10+import { isBlockedObjectKey } from "./prototype-keys.js";
1011import { AgentModelSchema, AgentToolModelSchema } from "./zod-schema.agent-model.js";
1112import {
1213GroupChatSchema,
@@ -340,26 +341,95 @@ const CodexUserLocationSchema = z
340341})
341342.optional();
342343344+const LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS = new Set([
345+"brave",
346+"duckduckgo",
347+"exa",
348+"firecrawl",
349+"gemini",
350+"grok",
351+"kimi",
352+"minimax",
353+"ollama",
354+"perplexity",
355+"searxng",
356+"tavily",
357+]);
358+359+function isPlainRecord(value: unknown): value is Record<string, unknown> {
360+return value !== null && typeof value === "object" && !Array.isArray(value);
361+}
362+363+const BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD = "__openclawBlockedWebSearchKeys";
364+343365const ToolsWebSearchSchema = z
344-.object({
345-enabled: z.boolean().optional(),
346-provider: z.string().optional(),
347-maxResults: z.number().int().positive().optional(),
348-timeoutSeconds: z.number().int().positive().optional(),
349-cacheTtlMinutes: z.number().nonnegative().optional(),
350-apiKey: SecretInputSchema.optional().register(sensitive),
351-openaiCodex: z
366+.preprocess(
367+(value) => {
368+if (!isPlainRecord(value)) {
369+return value;
370+}
371+const blockedKeys = Object.getOwnPropertyNames(value).filter((key) =>
372+isBlockedObjectKey(key),
373+);
374+if (blockedKeys.length === 0) {
375+return value;
376+}
377+return {
378+ ...value,
379+[BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD]: blockedKeys,
380+};
381+},
382+z
352383.object({
353384enabled: z.boolean().optional(),
354-mode: z.union([z.literal("cached"), z.literal("live")]).optional(),
355-allowedDomains: CodexAllowedDomainsSchema,
356-contextSize: z.union([z.literal("low"), z.literal("medium"), z.literal("high")]).optional(),
357-userLocation: CodexUserLocationSchema,
385+provider: z.string().optional(),
386+maxResults: z.number().int().positive().optional(),
387+timeoutSeconds: z.number().int().positive().optional(),
388+cacheTtlMinutes: z.number().nonnegative().optional(),
389+apiKey: SecretInputSchema.optional().register(sensitive),
390+openaiCodex: z
391+.object({
392+enabled: z.boolean().optional(),
393+mode: z.union([z.literal("cached"), z.literal("live")]).optional(),
394+allowedDomains: CodexAllowedDomainsSchema,
395+contextSize: z
396+.union([z.literal("low"), z.literal("medium"), z.literal("high")])
397+.optional(),
398+userLocation: CodexUserLocationSchema,
399+})
400+.strict()
401+.optional(),
358402})
359-.strict()
360-.optional(),
361-})
362-.strict()
403+.catchall(z.unknown())
404+.superRefine((value, ctx) => {
405+const blockedKeys = value[BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD];
406+if (Array.isArray(blockedKeys)) {
407+for (const key of blockedKeys) {
408+if (typeof key !== "string") {
409+continue;
410+}
411+ctx.addIssue({
412+code: z.ZodIssueCode.custom,
413+path: [key],
414+message: "tools.web.search must not contain blocked object keys",
415+});
416+}
417+}
418+for (const [key, entry] of Object.entries(value)) {
419+if (key === BLOCKED_WEB_SEARCH_KEYS_ISSUE_FIELD || isBlockedObjectKey(key)) {
420+continue;
421+}
422+if (LEGACY_WEB_SEARCH_PROVIDER_CONFIG_KEYS.has(key) && isPlainRecord(entry)) {
423+ctx.addIssue({
424+code: z.ZodIssueCode.custom,
425+path: [key],
426+message:
427+"legacy web_search provider config must use plugins.entries.<plugin>.config.webSearch",
428+});
429+}
430+}
431+}),
432+)
363433.optional();
364434365435const ToolsWebFetchSchema = z
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。