diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index e260224db186..1bf139e1630a 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -352,7 +352,6 @@ packages/gateway-protocol/src/schema/agents-models-skills.ts packages/gateway-protocol/src/schema/protocol-schemas.ts packages/markdown-core/src/ir.ts packages/memory-host-sdk/src/host/session-files.ts -packages/model-catalog-core/src/model-catalog-normalize.ts packages/sdk/src/client.ts packages/sdk/src/index.test.ts packages/speech-core/src/tts.test.ts diff --git a/extensions/slack/src/post-message-identity.ts b/extensions/slack/src/post-message-identity.ts index 9b9973049be4..cd84b57748a5 100644 --- a/extensions/slack/src/post-message-identity.ts +++ b/extensions/slack/src/post-message-identity.ts @@ -3,6 +3,7 @@ import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, + normalizeTrimmedStringList, } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { SlackBasePostMessagePayload, @@ -24,16 +25,6 @@ type SlackWebApiErrorData = { }; }; -function normalizeSlackScopeList(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - return value.flatMap((scope) => { - const normalized = normalizeOptionalString(scope); - return normalized ? [normalized] : []; - }); -} - function getSlackWebApiErrorData(err: unknown): SlackWebApiErrorData | undefined { if (!(err instanceof Error)) { return undefined; @@ -53,8 +44,8 @@ function isSlackCustomizeScopeError(err: unknown): boolean { return true; } const scopes = [ - ...normalizeSlackScopeList(data?.response_metadata?.scopes), - ...normalizeSlackScopeList(data?.response_metadata?.acceptedScopes), + ...normalizeTrimmedStringList(data?.response_metadata?.scopes), + ...normalizeTrimmedStringList(data?.response_metadata?.acceptedScopes), ].map((scope) => normalizeLowercaseStringOrEmpty(scope)); return scopes.includes("chat:write.customize"); } diff --git a/extensions/slack/src/send.identity-fallback.test.ts b/extensions/slack/src/send.identity-fallback.test.ts index ef4a4ab0e0eb..a0f529d4982a 100644 --- a/extensions/slack/src/send.identity-fallback.test.ts +++ b/extensions/slack/src/send.identity-fallback.test.ts @@ -227,7 +227,9 @@ describe("sendMessageSlack customize-scope fallback", () => { const client = createSlackSendTestClient(); vi.mocked(client.chat.postMessage) .mockRejectedValueOnce( - buildMissingScopeError({ acceptedScopes: ["chat:write", "chat:write.customize"] }), + buildMissingScopeError({ + acceptedScopes: [" chat:write ", "", " chat:write.customize "], + }), ) .mockResolvedValueOnce({ ts: "171234.567" }); @@ -383,8 +385,8 @@ describe("sendMessageSlack customize-scope fallback", () => { vi.mocked(client.chat.postMessage).mockRejectedValueOnce( buildMissingScopeError({ needed: "im:write", - scopes: ["chat:write", "users:read"], - acceptedScopes: ["im:write", "mpim:write"], + scopes: [" chat:write ", "", " users:read "], + acceptedScopes: [" im:write ", " mpim:write "], }), ); diff --git a/extensions/slack/src/send.ts b/extensions/slack/src/send.ts index 2167b3077ea5..09689ca2cac1 100644 --- a/extensions/slack/src/send.ts +++ b/extensions/slack/src/send.ts @@ -26,6 +26,7 @@ import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime"; import { normalizeOptionalString, normalizeOptionalString as normalizeSlackApiString, + normalizeTrimmedStringList, } from "openclaw/plugin-sdk/string-coerce-runtime"; import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { SlackTokenSource } from "./accounts.js"; @@ -195,16 +196,6 @@ function resolveSlackSendIdentity(params: { ); } -function normalizeSlackScopeList(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - return value.flatMap((scope) => { - const normalized = normalizeSlackApiString(scope); - return normalized ? [normalized] : []; - }); -} - function getSlackWebApiErrorData(err: unknown): SlackWebApiErrorData | undefined { if (!(err instanceof Error)) { return undefined; @@ -230,11 +221,11 @@ function formatSlackWebApiErrorMessage(err: unknown): string | undefined { if (needed) { details.push(`needed: ${needed}`); } - const scopes = normalizeSlackScopeList(data?.response_metadata?.scopes); + const scopes = normalizeTrimmedStringList(data?.response_metadata?.scopes); if (scopes.length) { details.push(`granted: ${scopes.join(", ")}`); } - const acceptedScopes = normalizeSlackScopeList(data?.response_metadata?.acceptedScopes); + const acceptedScopes = normalizeTrimmedStringList(data?.response_metadata?.acceptedScopes); if (acceptedScopes.length) { details.push(`accepted: ${acceptedScopes.join(", ")}`); } diff --git a/packages/model-catalog-core/src/model-catalog-normalize.test.ts b/packages/model-catalog-core/src/model-catalog-normalize.test.ts index 5255057edb72..588fc242aa18 100644 --- a/packages/model-catalog-core/src/model-catalog-normalize.test.ts +++ b/packages/model-catalog-core/src/model-catalog-normalize.test.ts @@ -57,11 +57,14 @@ describe("model catalog normalization", () => { compat: { supportsTools: true, openRouterRouting: { - only: ["anthropic", 1], + only: [" anthropic ", "", 1], allow_fallbacks: false, require_parameters: "no", }, - vercelGatewayRouting: { order: ["anthropic", 1], only: "openai" }, + vercelGatewayRouting: { + order: [" anthropic ", "", 1], + only: "openai", + }, zaiToolStream: true, cacheControlFormat: "anthropic", sendSessionAffinityHeaders: true, @@ -75,9 +78,9 @@ describe("model catalog normalization", () => { }, status: "preview", statusReason: "rolling out", - replaces: ["gpt-5.3"], + replaces: [" gpt-5.3 ", ""], replacedBy: "gpt-5.5", - tags: ["default"], + tags: [" default ", ""], }, { id: "", diff --git a/packages/model-catalog-core/src/model-catalog-normalize.ts b/packages/model-catalog-core/src/model-catalog-normalize.ts index c444fe34a4b1..fa97dbbf1db1 100644 --- a/packages/model-catalog-core/src/model-catalog-normalize.ts +++ b/packages/model-catalog-core/src/model-catalog-normalize.ts @@ -1,4 +1,9 @@ // Model Catalog Core helper module supports model catalog normalize behavior. +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { + normalizeOptionalTrimmedStringList, + normalizeTrimmedStringList, +} from "@openclaw/normalization-core/string-normalization"; import { buildModelCatalogMergeKey, buildModelCatalogRef, @@ -48,31 +53,6 @@ function isBlockedObjectKey(key: string): boolean { return key === "__proto__" || key === "prototype" || key === "constructor"; } -/** Normalize optional catalog strings. */ -function normalizeOptionalString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - -/** Normalize arrays of trimmed strings, dropping invalid entries. */ -function normalizeTrimmedStringList(value: unknown): string[] { - if (!Array.isArray(value)) { - return []; - } - return value.flatMap((entry) => { - const normalized = normalizeOptionalString(entry); - return normalized ? [normalized] : []; - }); -} - -function normalizeOptionalTrimmedStringList(value: unknown): string[] | undefined { - const normalized = normalizeTrimmedStringList(value); - return normalized.length > 0 ? normalized : undefined; -} - function normalizeModelCatalogThinkingLevelMap( value: unknown, ): ModelCatalogThinkingLevelMap | undefined { @@ -761,4 +741,3 @@ export function normalizeModelCatalogProviderRows(params: { return rows.toSorted((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id)); } -/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/packages/normalization-core/src/string-normalization.test.ts b/packages/normalization-core/src/string-normalization.test.ts index 0228ff635d87..ef42e0c63cb2 100644 --- a/packages/normalization-core/src/string-normalization.test.ts +++ b/packages/normalization-core/src/string-normalization.test.ts @@ -3,10 +3,12 @@ import { describe, expect, it } from "vitest"; import { normalizeAtHashSlug, normalizeHyphenSlug, + normalizeOptionalTrimmedStringList, normalizeSortedUniqueStringEntries, normalizeSortedUniqueTrimmedStringList, normalizeStringEntries, normalizeStringEntriesLower, + normalizeTrimmedStringList, normalizeUniqueSingleOrTrimmedStringList, normalizeUniqueStringEntries, normalizeUniqueStringEntriesLower, @@ -59,6 +61,14 @@ describe("normalization-core/string-normalization", () => { expect(normalizeUniqueTrimmedStringList("b")).toEqual([]); }); + it("normalizes array-backed trimmed string lists", () => { + const values = [" first ", "", 42, " second ", null]; + expect(normalizeTrimmedStringList(values)).toEqual(["first", "second"]); + expect(normalizeTrimmedStringList("first")).toEqual([]); + expect(normalizeOptionalTrimmedStringList(values)).toEqual(["first", "second"]); + expect(normalizeOptionalTrimmedStringList(["", 42])).toBeUndefined(); + }); + it("normalizes sorted unique trimmed string lists", () => { expect(normalizeSortedUniqueTrimmedStringList([" b ", "a", "b", "", "a"])).toEqual(["a", "b"]); expect(normalizeSortedUniqueTrimmedStringList(["z", 1, " a "] as unknown[])).toEqual([