diff --git a/extensions/browser/browser-config.ts b/extensions/browser/browser-config.ts index 3640b8acde9..61d8e1a03a1 100644 --- a/extensions/browser/browser-config.ts +++ b/extensions/browser/browser-config.ts @@ -5,14 +5,13 @@ export { DEFAULT_OPENCLAW_BROWSER_COLOR, DEFAULT_OPENCLAW_BROWSER_ENABLED, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, -} from "./src/browser/constants.js"; -export type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./src/browser/config.js"; -export { - parseHttpUrl as parseBrowserHttpUrl, + parseBrowserHttpUrl, + redactCdpUrl, resolveBrowserConfig, + resolveBrowserControlAuth, resolveProfile, + type BrowserControlAuth, + type ResolvedBrowserConfig, + type ResolvedBrowserProfile, } from "./src/browser/config.js"; -export { redactCdpUrl } from "./src/browser/cdp.helpers.js"; export { DEFAULT_UPLOAD_DIR } from "./src/browser/paths.js"; -export type { BrowserControlAuth } from "./src/browser/control-auth.js"; -export { resolveBrowserControlAuth } from "./src/browser/control-auth.js"; diff --git a/extensions/browser/src/browser/config.ts b/extensions/browser/src/browser/config.ts index 9b0a374512d..7138d9ed6d4 100644 --- a/extensions/browser/src/browser/config.ts +++ b/extensions/browser/src/browser/config.ts @@ -1,383 +1,21 @@ -import type { BrowserConfig, BrowserProfileConfig, OpenClawConfig } from "../config/config.js"; -import { resolveGatewayPort } from "../config/paths.js"; -import { - deriveDefaultBrowserCdpPortRange, - deriveDefaultBrowserControlPort, - DEFAULT_BROWSER_CONTROL_PORT, -} from "../config/port-defaults.js"; -import { isLoopbackHost } from "../gateway/net.js"; -import type { SsrFPolicy } from "../infra/net/ssrf.js"; -import { resolveUserPath } from "../utils.js"; -import { +export { + DEFAULT_AI_SNAPSHOT_MAX_CHARS, + DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, + DEFAULT_BROWSER_EVALUATE_ENABLED, DEFAULT_OPENCLAW_BROWSER_COLOR, DEFAULT_OPENCLAW_BROWSER_ENABLED, - DEFAULT_BROWSER_EVALUATE_ENABLED, - DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, -} from "./constants.js"; -import { CDP_PORT_RANGE_START } from "./profiles.js"; + parseBrowserHttpUrl, + redactCdpUrl, + resolveBrowserConfig, + resolveBrowserControlAuth, + resolveProfile, + type BrowserControlAuth, + type ResolvedBrowserConfig, + type ResolvedBrowserProfile, +} from "openclaw/plugin-sdk/browser-config"; +export { parseBrowserHttpUrl as parseHttpUrl } from "openclaw/plugin-sdk/browser-config"; -export type ResolvedBrowserConfig = { - enabled: boolean; - evaluateEnabled: boolean; - controlPort: number; - cdpPortRangeStart: number; - cdpPortRangeEnd: number; - cdpProtocol: "http" | "https"; - cdpHost: string; - cdpIsLoopback: boolean; - remoteCdpTimeoutMs: number; - remoteCdpHandshakeTimeoutMs: number; - color: string; - executablePath?: string; - headless: boolean; - noSandbox: boolean; - attachOnly: boolean; - defaultProfile: string; - profiles: Record; - ssrfPolicy?: SsrFPolicy; - extraArgs: string[]; -}; - -export type ResolvedBrowserProfile = { - name: string; - cdpPort: number; - cdpUrl: string; - cdpHost: string; - cdpIsLoopback: boolean; - userDataDir?: string; - color: string; - driver: "openclaw" | "existing-session"; - attachOnly: boolean; -}; - -function normalizeHexColor(raw: string | undefined) { - const value = (raw ?? "").trim(); - if (!value) { - return DEFAULT_OPENCLAW_BROWSER_COLOR; - } - const normalized = value.startsWith("#") ? value : `#${value}`; - if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) { - return DEFAULT_OPENCLAW_BROWSER_COLOR; - } - return normalized.toUpperCase(); -} - -function normalizeTimeoutMs(raw: number | undefined, fallback: number) { - const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback; - return value < 0 ? fallback : value; -} - -function resolveCdpPortRangeStart( - rawStart: number | undefined, - fallbackStart: number, - rangeSpan: number, -) { - const start = - typeof rawStart === "number" && Number.isFinite(rawStart) - ? Math.floor(rawStart) - : fallbackStart; - if (start < 1 || start > 65535) { - throw new Error(`browser.cdpPortRangeStart must be between 1 and 65535, got: ${start}`); - } - const maxStart = 65535 - rangeSpan; - if (start > maxStart) { - throw new Error( - `browser.cdpPortRangeStart (${start}) is too high for a ${rangeSpan + 1}-port range; max is ${maxStart}.`, - ); - } - return start; -} - -function normalizeStringList(raw: string[] | undefined): string[] | undefined { - if (!Array.isArray(raw) || raw.length === 0) { - return undefined; - } - const values = raw - .map((value) => value.trim()) - .filter((value): value is string => value.length > 0); - return values.length > 0 ? values : undefined; -} - -function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined { - const rawPolicy = cfg?.ssrfPolicy as - | (BrowserConfig["ssrfPolicy"] & { allowPrivateNetwork?: boolean }) - | undefined; - const allowPrivateNetwork = rawPolicy?.allowPrivateNetwork; - const dangerouslyAllowPrivateNetwork = rawPolicy?.dangerouslyAllowPrivateNetwork; - const allowedHostnames = normalizeStringList(rawPolicy?.allowedHostnames); - const hostnameAllowlist = normalizeStringList(rawPolicy?.hostnameAllowlist); - const hasExplicitPrivateSetting = - allowPrivateNetwork !== undefined || dangerouslyAllowPrivateNetwork !== undefined; - // Browser defaults to trusted-network mode unless explicitly disabled by policy. - const resolvedAllowPrivateNetwork = - dangerouslyAllowPrivateNetwork === true || - allowPrivateNetwork === true || - !hasExplicitPrivateSetting; - - if ( - !resolvedAllowPrivateNetwork && - !hasExplicitPrivateSetting && - !allowedHostnames && - !hostnameAllowlist - ) { - return undefined; - } - - return { - ...(resolvedAllowPrivateNetwork ? { dangerouslyAllowPrivateNetwork: true } : {}), - ...(allowedHostnames ? { allowedHostnames } : {}), - ...(hostnameAllowlist ? { hostnameAllowlist } : {}), - }; -} - -export function parseHttpUrl(raw: string, label: string) { - const trimmed = raw.trim(); - const parsed = new URL(trimmed); - const allowed = ["http:", "https:", "ws:", "wss:"]; - if (!allowed.includes(parsed.protocol)) { - throw new Error(`${label} must be http(s) or ws(s), got: ${parsed.protocol.replace(":", "")}`); - } - - const isSecure = parsed.protocol === "https:" || parsed.protocol === "wss:"; - const port = - parsed.port && Number.parseInt(parsed.port, 10) > 0 - ? Number.parseInt(parsed.port, 10) - : isSecure - ? 443 - : 80; - - if (Number.isNaN(port) || port <= 0 || port > 65535) { - throw new Error(`${label} has invalid port: ${parsed.port}`); - } - - return { - parsed, - port, - normalized: parsed.toString().replace(/\/$/, ""), - }; -} - -/** - * Ensure the default "openclaw" profile exists in the profiles map. - * Auto-creates it with the legacy CDP port (from browser.cdpUrl) or first port if missing. - */ -function ensureDefaultProfile( - profiles: Record | undefined, - defaultColor: string, - legacyCdpPort?: number, - derivedDefaultCdpPort?: number, - legacyCdpUrl?: string, -): Record { - const result = { ...profiles }; - if (!result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]) { - result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] = { - cdpPort: legacyCdpPort ?? derivedDefaultCdpPort ?? CDP_PORT_RANGE_START, - color: defaultColor, - // Preserve the full cdpUrl for ws/wss endpoints so resolveProfile() - // doesn't reconstruct from cdpProtocol/cdpHost/cdpPort (which drops - // the WebSocket protocol and query params like API keys). - ...(legacyCdpUrl ? { cdpUrl: legacyCdpUrl } : {}), - }; - } - return result; -} - -/** - * Ensure a built-in "user" profile exists for Chrome's existing-session attach flow. - */ -function ensureDefaultUserBrowserProfile( - profiles: Record, -): Record { - const result = { ...profiles }; - if (result.user) { - return result; - } - result.user = { - driver: "existing-session", - attachOnly: true, - color: "#00AA00", - }; - return result; -} - -export function resolveBrowserConfig( - cfg: BrowserConfig | undefined, - rootConfig?: OpenClawConfig, -): ResolvedBrowserConfig { - const enabled = cfg?.enabled ?? DEFAULT_OPENCLAW_BROWSER_ENABLED; - const evaluateEnabled = cfg?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED; - const gatewayPort = resolveGatewayPort(rootConfig); - const controlPort = deriveDefaultBrowserControlPort(gatewayPort ?? DEFAULT_BROWSER_CONTROL_PORT); - const defaultColor = normalizeHexColor(cfg?.color); - const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500); - const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs( - cfg?.remoteCdpHandshakeTimeoutMs, - Math.max(2000, remoteCdpTimeoutMs * 2), - ); - - const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort); - const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start; - const cdpPortRangeStart = resolveCdpPortRangeStart( - cfg?.cdpPortRangeStart, - derivedCdpRange.start, - cdpRangeSpan, - ); - const cdpPortRangeEnd = cdpPortRangeStart + cdpRangeSpan; - - const rawCdpUrl = (cfg?.cdpUrl ?? "").trim(); - let cdpInfo: - | { - parsed: URL; - port: number; - normalized: string; - } - | undefined; - if (rawCdpUrl) { - cdpInfo = parseHttpUrl(rawCdpUrl, "browser.cdpUrl"); - } else { - const derivedPort = controlPort + 1; - if (derivedPort > 65535) { - throw new Error( - `Derived CDP port (${derivedPort}) is too high; check gateway port configuration.`, - ); - } - const derived = new URL(`http://127.0.0.1:${derivedPort}`); - cdpInfo = { - parsed: derived, - port: derivedPort, - normalized: derived.toString().replace(/\/$/, ""), - }; - } - - const headless = cfg?.headless === true; - const noSandbox = cfg?.noSandbox === true; - const attachOnly = cfg?.attachOnly === true; - const executablePath = cfg?.executablePath?.trim() || undefined; - - const defaultProfileFromConfig = cfg?.defaultProfile?.trim() || undefined; - // Use legacy cdpUrl port for backward compatibility when no profiles configured - const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined; - const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:"; - const legacyCdpUrl = rawCdpUrl && isWsUrl ? cdpInfo.normalized : undefined; - const profiles = ensureDefaultUserBrowserProfile( - ensureDefaultProfile( - cfg?.profiles, - defaultColor, - legacyCdpPort, - cdpPortRangeStart, - legacyCdpUrl, - ), - ); - const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http"; - - const defaultProfile = - defaultProfileFromConfig ?? - (profiles[DEFAULT_BROWSER_DEFAULT_PROFILE_NAME] - ? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME - : profiles[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] - ? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME - : "user"); - - const extraArgs = Array.isArray(cfg?.extraArgs) - ? cfg.extraArgs.filter((a): a is string => typeof a === "string" && a.trim().length > 0) - : []; - const ssrfPolicy = resolveBrowserSsrFPolicy(cfg); - return { - enabled, - evaluateEnabled, - controlPort, - cdpPortRangeStart, - cdpPortRangeEnd, - cdpProtocol, - cdpHost: cdpInfo.parsed.hostname, - cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname), - remoteCdpTimeoutMs, - remoteCdpHandshakeTimeoutMs, - color: defaultColor, - executablePath, - headless, - noSandbox, - attachOnly, - defaultProfile, - profiles, - ssrfPolicy, - extraArgs, - }; -} - -/** - * Resolve a profile by name from the config. - * Returns null if the profile doesn't exist. - */ -export function resolveProfile( - resolved: ResolvedBrowserConfig, - profileName: string, -): ResolvedBrowserProfile | null { - const profile = resolved.profiles[profileName]; - if (!profile) { - return null; - } - - const rawProfileUrl = profile.cdpUrl?.trim() ?? ""; - let cdpHost = resolved.cdpHost; - let cdpPort = profile.cdpPort ?? 0; - let cdpUrl = ""; - const driver = profile.driver === "existing-session" ? "existing-session" : "openclaw"; - - if (driver === "existing-session") { - // existing-session uses Chrome MCP auto-connect; no CDP port/URL needed - return { - name: profileName, - cdpPort: 0, - cdpUrl: "", - cdpHost: "", - cdpIsLoopback: true, - userDataDir: resolveUserPath(profile.userDataDir?.trim() || "") || undefined, - color: profile.color, - driver, - attachOnly: true, - }; - } - - // When both cdpPort and cdpUrl are set and the URL contains a - // /devtools/browser/ path (a session-specific WebSocket ID), prefer - // cdpPort — the HTTP endpoint is stable across Chrome restarts while the - // WS path goes stale. For cloud CDP services (e.g. Browserbase) that - // use WSS URLs without a devtools path, preserve the full URL. - const hasStaleWsPath = - rawProfileUrl !== "" && - cdpPort > 0 && - /^wss?:\/\//i.test(rawProfileUrl) && - /\/devtools\/browser\//i.test(rawProfileUrl); - - if (hasStaleWsPath) { - const parsed = new URL(rawProfileUrl); - cdpHost = parsed.hostname; - cdpUrl = `${resolved.cdpProtocol}://${cdpHost}:${cdpPort}`; - } else if (rawProfileUrl) { - const parsed = parseHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`); - cdpHost = parsed.parsed.hostname; - cdpPort = parsed.port; - cdpUrl = parsed.normalized; - } else if (cdpPort) { - cdpUrl = `${resolved.cdpProtocol}://${resolved.cdpHost}:${cdpPort}`; - } else { - throw new Error(`Profile "${profileName}" must define cdpPort or cdpUrl.`); - } - - return { - name: profileName, - cdpPort, - cdpUrl, - cdpHost, - cdpIsLoopback: isLoopbackHost(cdpHost), - color: profile.color, - driver, - attachOnly: profile.attachOnly ?? resolved.attachOnly, - }; -} - -export function shouldStartLocalBrowserServer(_resolved: ResolvedBrowserConfig) { +export function shouldStartLocalBrowserServer(_resolved: unknown) { return true; } diff --git a/package.json b/package.json index 425010eb2e3..a5f13af5745 100644 --- a/package.json +++ b/package.json @@ -347,9 +347,9 @@ "types": "./dist/plugin-sdk/bluebubbles-policy.d.ts", "default": "./dist/plugin-sdk/bluebubbles-policy.js" }, - "./plugin-sdk/elevenlabs": { - "types": "./dist/plugin-sdk/elevenlabs.d.ts", - "default": "./dist/plugin-sdk/elevenlabs.js" + "./plugin-sdk/browser-config": { + "types": "./dist/plugin-sdk/browser-config.d.ts", + "default": "./dist/plugin-sdk/browser-config.js" }, "./plugin-sdk/browser-config-support": { "types": "./dist/plugin-sdk/browser-config-support.d.ts", diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 46bd1614f4e..231a3fca204 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -76,7 +76,7 @@ "allowlist-config-edit", "bluebubbles", "bluebubbles-policy", - "elevenlabs", + "browser-config", "browser-config-support", "browser-support", "boolean-param", diff --git a/src/commands/doctor-legacy-config.ts b/src/commands/doctor-legacy-config.ts index 995c85500a3..dc7ce31716b 100644 --- a/src/commands/doctor-legacy-config.ts +++ b/src/commands/doctor-legacy-config.ts @@ -8,7 +8,6 @@ import { migrateLegacyWebSearchConfig } from "../config/legacy-web-search.js"; import { migrateLegacyXSearchConfig } from "../config/legacy-x-search.js"; import { normalizeTalkSection } from "../config/talk.js"; import { DEFAULT_GOOGLE_API_BASE_URL } from "../infra/google-api-base-url.js"; -import { normalizeCompatibilityConfig as normalizeElevenLabsCompatibilityConfig } from "../plugin-sdk/elevenlabs.js"; import { runPluginSetupConfigMigrations } from "../plugins/setup-registry.js"; import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; @@ -358,13 +357,6 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): { return; } - const legacyMigration = normalizeElevenLabsCompatibilityConfig({ cfg: next }); - if (legacyMigration.changes.length > 0) { - next = legacyMigration.config; - changes.push(...legacyMigration.changes); - return; - } - const normalizedTalk = normalizeTalkSection(rawTalk as OpenClawConfig["talk"]); if (!normalizedTalk) { return; diff --git a/src/config/legacy.migrations.runtime.ts b/src/config/legacy.migrations.runtime.ts index 0e6e9ce0de7..9c362de4adf 100644 --- a/src/config/legacy.migrations.runtime.ts +++ b/src/config/legacy.migrations.runtime.ts @@ -1,8 +1,3 @@ -import { - ELEVENLABS_TALK_LEGACY_CONFIG_RULES, - hasLegacyTalkFields, -} from "../plugin-sdk/elevenlabs.js"; -import { runPluginSetupLegacyConfigMigrations } from "../plugins/setup-registry.js"; import { buildDefaultControlUiAllowedOrigins, hasConfiguredControlUiAllowedOrigins, @@ -329,17 +324,6 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [ } }, }), - defineLegacyConfigMigration({ - id: "talk.legacy-fields->talk.providers", - describe: "Move legacy Talk flat fields into talk.providers.", - legacyRules: ELEVENLABS_TALK_LEGACY_CONFIG_RULES, - apply: (raw, changes) => { - if (!hasLegacyTalkFields(raw.talk)) { - return; - } - runPluginSetupLegacyConfigMigrations({ raw, changes }); - }, - }), defineLegacyConfigMigration({ id: "tools.web.x_search.apiKey->plugins.entries.xai.config.webSearch.apiKey", describe: "Move legacy x_search auth into the xAI plugin webSearch config", diff --git a/src/plugin-sdk/amazon-bedrock-mantle.ts b/src/plugin-sdk/amazon-bedrock-mantle.ts index 96af27f1956..2573314f24b 100644 --- a/src/plugin-sdk/amazon-bedrock-mantle.ts +++ b/src/plugin-sdk/amazon-bedrock-mantle.ts @@ -1 +1,8 @@ -export { resolveMantleBearerToken } from "../../extensions/amazon-bedrock-mantle/discovery.js"; +/** + * Mantle's OpenAI-compatible surface currently expects a bearer token. + * Plain IAM credentials are not sufficient until token generation is wired in. + */ +export function resolveMantleBearerToken(env: NodeJS.ProcessEnv = process.env): string | undefined { + const explicitToken = env.AWS_BEARER_TOKEN_BEDROCK?.trim(); + return explicitToken || undefined; +} diff --git a/src/plugin-sdk/browser-config.ts b/src/plugin-sdk/browser-config.ts index 64087001800..13d4dfa5af8 100644 --- a/src/plugin-sdk/browser-config.ts +++ b/src/plugin-sdk/browser-config.ts @@ -1,19 +1,489 @@ -export { - DEFAULT_AI_SNAPSHOT_MAX_CHARS, - DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, - DEFAULT_BROWSER_EVALUATE_ENABLED, - DEFAULT_OPENCLAW_BROWSER_COLOR, - DEFAULT_OPENCLAW_BROWSER_ENABLED, - DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, - DEFAULT_UPLOAD_DIR, - parseBrowserHttpUrl, - redactCdpUrl, - resolveBrowserConfig, - resolveBrowserControlAuth, - resolveProfile, -} from "../../extensions/browser/browser-config.js"; -export type { - BrowserControlAuth, - ResolvedBrowserConfig, - ResolvedBrowserProfile, -} from "../../extensions/browser/browser-config.js"; +import path from "node:path"; +import { resolveGatewayAuth } from "../gateway/auth.js"; +import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; +import { + DEFAULT_BROWSER_CONTROL_PORT, + deriveDefaultBrowserCdpPortRange, + deriveDefaultBrowserControlPort, + isLoopbackHost, + resolveGatewayPort, + resolveUserPath, +} from "./browser-config-support.js"; +import type { + BrowserConfig, + BrowserProfileConfig, + OpenClawConfig, + SsrFPolicy, +} from "./browser-support.js"; +import { ensureGatewayStartupAuth, loadConfig, redactSensitiveText } from "./browser-support.js"; + +export const DEFAULT_OPENCLAW_BROWSER_ENABLED = true; +export const DEFAULT_BROWSER_EVALUATE_ENABLED = true; +export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500"; +export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw"; +export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw"; +export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000; + +const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800; +const DEFAULT_FALLBACK_BROWSER_TMP_DIR = "/tmp/openclaw"; +const DEFAULT_UPLOADS_DIR_NAME = "uploads"; + +export type ResolvedBrowserConfig = { + enabled: boolean; + evaluateEnabled: boolean; + controlPort: number; + cdpPortRangeStart: number; + cdpPortRangeEnd: number; + cdpProtocol: "http" | "https"; + cdpHost: string; + cdpIsLoopback: boolean; + remoteCdpTimeoutMs: number; + remoteCdpHandshakeTimeoutMs: number; + color: string; + executablePath?: string; + headless: boolean; + noSandbox: boolean; + attachOnly: boolean; + defaultProfile: string; + profiles: Record; + ssrfPolicy?: SsrFPolicy; + extraArgs: string[]; +}; + +export type ResolvedBrowserProfile = { + name: string; + cdpPort: number; + cdpUrl: string; + cdpHost: string; + cdpIsLoopback: boolean; + userDataDir?: string; + color: string; + driver: "openclaw" | "existing-session"; + attachOnly: boolean; +}; + +export type BrowserControlAuth = { + token?: string; + password?: string; +}; + +function canUseNodeFs(): boolean { + const getBuiltinModule = ( + process as NodeJS.Process & { + getBuiltinModule?: (id: string) => unknown; + } + ).getBuiltinModule; + if (typeof getBuiltinModule !== "function") { + return false; + } + try { + return getBuiltinModule("fs") !== undefined; + } catch { + return false; + } +} + +const DEFAULT_BROWSER_TMP_DIR = canUseNodeFs() + ? resolvePreferredOpenClawTmpDir() + : DEFAULT_FALLBACK_BROWSER_TMP_DIR; + +export const DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, DEFAULT_UPLOADS_DIR_NAME); + +function normalizeHexColor(raw: string | undefined): string { + const value = (raw ?? "").trim(); + if (!value) { + return DEFAULT_OPENCLAW_BROWSER_COLOR; + } + const normalized = value.startsWith("#") ? value : `#${value}`; + if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) { + return DEFAULT_OPENCLAW_BROWSER_COLOR; + } + return normalized.toUpperCase(); +} + +function normalizeTimeoutMs(raw: number | undefined, fallback: number): number { + const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback; + return value < 0 ? fallback : value; +} + +function resolveCdpPortRangeStart( + rawStart: number | undefined, + fallbackStart: number, + rangeSpan: number, +): number { + const start = + typeof rawStart === "number" && Number.isFinite(rawStart) + ? Math.floor(rawStart) + : fallbackStart; + if (start < 1 || start > 65535) { + throw new Error(`browser.cdpPortRangeStart must be between 1 and 65535, got: ${start}`); + } + const maxStart = 65535 - rangeSpan; + if (start > maxStart) { + throw new Error( + `browser.cdpPortRangeStart (${start}) is too high for a ${rangeSpan + 1}-port range; max is ${maxStart}.`, + ); + } + return start; +} + +function normalizeStringList(raw: string[] | undefined): string[] | undefined { + if (!Array.isArray(raw) || raw.length === 0) { + return undefined; + } + const values = raw + .map((value) => value.trim()) + .filter((value): value is string => value.length > 0); + return values.length > 0 ? values : undefined; +} + +function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined { + const rawPolicy = cfg?.ssrfPolicy as + | (BrowserConfig["ssrfPolicy"] & { allowPrivateNetwork?: boolean }) + | undefined; + const allowPrivateNetwork = rawPolicy?.allowPrivateNetwork; + const dangerouslyAllowPrivateNetwork = rawPolicy?.dangerouslyAllowPrivateNetwork; + const allowedHostnames = normalizeStringList(rawPolicy?.allowedHostnames); + const hostnameAllowlist = normalizeStringList(rawPolicy?.hostnameAllowlist); + const hasExplicitPrivateSetting = + allowPrivateNetwork !== undefined || dangerouslyAllowPrivateNetwork !== undefined; + const resolvedAllowPrivateNetwork = + dangerouslyAllowPrivateNetwork === true || + allowPrivateNetwork === true || + !hasExplicitPrivateSetting; + + if ( + !resolvedAllowPrivateNetwork && + !hasExplicitPrivateSetting && + !allowedHostnames && + !hostnameAllowlist + ) { + return undefined; + } + + return { + ...(resolvedAllowPrivateNetwork ? { dangerouslyAllowPrivateNetwork: true } : {}), + ...(allowedHostnames ? { allowedHostnames } : {}), + ...(hostnameAllowlist ? { hostnameAllowlist } : {}), + }; +} + +export function parseBrowserHttpUrl(raw: string, label: string) { + const trimmed = raw.trim(); + const parsed = new URL(trimmed); + const allowed = ["http:", "https:", "ws:", "wss:"]; + if (!allowed.includes(parsed.protocol)) { + throw new Error(`${label} must be http(s) or ws(s), got: ${parsed.protocol.replace(":", "")}`); + } + + const isSecure = parsed.protocol === "https:" || parsed.protocol === "wss:"; + const port = + parsed.port && Number.parseInt(parsed.port, 10) > 0 + ? Number.parseInt(parsed.port, 10) + : isSecure + ? 443 + : 80; + + if (Number.isNaN(port) || port <= 0 || port > 65535) { + throw new Error(`${label} has invalid port: ${parsed.port}`); + } + + return { + parsed, + port, + normalized: parsed.toString().replace(/\/$/, ""), + }; +} + +function ensureDefaultProfile( + profiles: Record | undefined, + defaultColor: string, + legacyCdpPort?: number, + derivedDefaultCdpPort?: number, + legacyCdpUrl?: string, +): Record { + const result = { ...profiles }; + if (!result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]) { + result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] = { + cdpPort: legacyCdpPort ?? derivedDefaultCdpPort ?? DEFAULT_BROWSER_CDP_PORT_RANGE_START, + color: defaultColor, + ...(legacyCdpUrl ? { cdpUrl: legacyCdpUrl } : {}), + }; + } + return result; +} + +function ensureDefaultUserBrowserProfile( + profiles: Record, +): Record { + const result = { ...profiles }; + if (result.user) { + return result; + } + result.user = { + driver: "existing-session", + attachOnly: true, + color: "#00AA00", + }; + return result; +} + +export function resolveBrowserConfig( + cfg: BrowserConfig | undefined, + rootConfig?: OpenClawConfig, +): ResolvedBrowserConfig { + const enabled = cfg?.enabled ?? DEFAULT_OPENCLAW_BROWSER_ENABLED; + const evaluateEnabled = cfg?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED; + const gatewayPort = resolveGatewayPort(rootConfig); + const controlPort = deriveDefaultBrowserControlPort(gatewayPort ?? DEFAULT_BROWSER_CONTROL_PORT); + const defaultColor = normalizeHexColor(cfg?.color); + const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500); + const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs( + cfg?.remoteCdpHandshakeTimeoutMs, + Math.max(2000, remoteCdpTimeoutMs * 2), + ); + + const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort); + const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start; + const cdpPortRangeStart = resolveCdpPortRangeStart( + cfg?.cdpPortRangeStart, + derivedCdpRange.start, + cdpRangeSpan, + ); + const cdpPortRangeEnd = cdpPortRangeStart + cdpRangeSpan; + + const rawCdpUrl = (cfg?.cdpUrl ?? "").trim(); + let cdpInfo: + | { + parsed: URL; + port: number; + normalized: string; + } + | undefined; + if (rawCdpUrl) { + cdpInfo = parseBrowserHttpUrl(rawCdpUrl, "browser.cdpUrl"); + } else { + const derivedPort = controlPort + 1; + if (derivedPort > 65535) { + throw new Error( + `Derived CDP port (${derivedPort}) is too high; check gateway port configuration.`, + ); + } + const derived = new URL(`http://127.0.0.1:${derivedPort}`); + cdpInfo = { + parsed: derived, + port: derivedPort, + normalized: derived.toString().replace(/\/$/, ""), + }; + } + + const headless = cfg?.headless === true; + const noSandbox = cfg?.noSandbox === true; + const attachOnly = cfg?.attachOnly === true; + const executablePath = cfg?.executablePath?.trim() || undefined; + const defaultProfileFromConfig = cfg?.defaultProfile?.trim() || undefined; + + const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined; + const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:"; + const legacyCdpUrl = rawCdpUrl && isWsUrl ? cdpInfo.normalized : undefined; + const profiles = ensureDefaultUserBrowserProfile( + ensureDefaultProfile( + cfg?.profiles, + defaultColor, + legacyCdpPort, + cdpPortRangeStart, + legacyCdpUrl, + ), + ); + const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http"; + + const defaultProfile = + defaultProfileFromConfig ?? + (profiles[DEFAULT_BROWSER_DEFAULT_PROFILE_NAME] + ? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME + : profiles[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] + ? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME + : "user"); + + const extraArgs = Array.isArray(cfg?.extraArgs) + ? cfg.extraArgs.filter( + (value): value is string => typeof value === "string" && value.trim().length > 0, + ) + : []; + + return { + enabled, + evaluateEnabled, + controlPort, + cdpPortRangeStart, + cdpPortRangeEnd, + cdpProtocol, + cdpHost: cdpInfo.parsed.hostname, + cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname), + remoteCdpTimeoutMs, + remoteCdpHandshakeTimeoutMs, + color: defaultColor, + executablePath, + headless, + noSandbox, + attachOnly, + defaultProfile, + profiles, + ssrfPolicy: resolveBrowserSsrFPolicy(cfg), + extraArgs, + }; +} + +export function resolveProfile( + resolved: ResolvedBrowserConfig, + profileName: string, +): ResolvedBrowserProfile | null { + const profile = resolved.profiles[profileName]; + if (!profile) { + return null; + } + + const rawProfileUrl = profile.cdpUrl?.trim() ?? ""; + let cdpHost = resolved.cdpHost; + let cdpPort = profile.cdpPort ?? 0; + let cdpUrl = ""; + const driver = profile.driver === "existing-session" ? "existing-session" : "openclaw"; + + if (driver === "existing-session") { + return { + name: profileName, + cdpPort: 0, + cdpUrl: "", + cdpHost: "", + cdpIsLoopback: true, + userDataDir: resolveUserPath(profile.userDataDir?.trim() || "") || undefined, + color: profile.color, + driver, + attachOnly: true, + }; + } + + const hasStaleWsPath = + rawProfileUrl !== "" && + cdpPort > 0 && + /^wss?:\/\//i.test(rawProfileUrl) && + /\/devtools\/browser\//i.test(rawProfileUrl); + + if (hasStaleWsPath) { + const parsed = new URL(rawProfileUrl); + cdpHost = parsed.hostname; + cdpUrl = `${resolved.cdpProtocol}://${cdpHost}:${cdpPort}`; + } else if (rawProfileUrl) { + const parsed = parseBrowserHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`); + cdpHost = parsed.parsed.hostname; + cdpPort = parsed.port; + cdpUrl = parsed.normalized; + } else if (cdpPort) { + cdpUrl = `${resolved.cdpProtocol}://${resolved.cdpHost}:${cdpPort}`; + } else { + throw new Error(`Profile "${profileName}" must define cdpPort or cdpUrl.`); + } + + return { + name: profileName, + cdpPort, + cdpUrl, + cdpHost, + cdpIsLoopback: isLoopbackHost(cdpHost), + color: profile.color, + driver, + attachOnly: profile.attachOnly ?? resolved.attachOnly, + }; +} + +export function redactCdpUrl(cdpUrl: string | null | undefined): string | null | undefined { + if (typeof cdpUrl !== "string") { + return cdpUrl; + } + const trimmed = cdpUrl.trim(); + if (!trimmed) { + return trimmed; + } + try { + const parsed = new URL(trimmed); + parsed.username = ""; + parsed.password = ""; + return redactSensitiveText(parsed.toString().replace(/\/$/, "")); + } catch { + return redactSensitiveText(trimmed); + } +} + +export function resolveBrowserControlAuth( + cfg: OpenClawConfig | undefined, + env: NodeJS.ProcessEnv = process.env, +): BrowserControlAuth { + const auth = resolveGatewayAuth({ + authConfig: cfg?.gateway?.auth, + env, + tailscaleMode: cfg?.gateway?.tailscale?.mode, + }); + const token = typeof auth.token === "string" ? auth.token.trim() : ""; + const password = typeof auth.password === "string" ? auth.password.trim() : ""; + return { + token: token || undefined, + password: password || undefined, + }; +} + +function shouldAutoGenerateBrowserAuth(env: NodeJS.ProcessEnv): boolean { + const nodeEnv = (env.NODE_ENV ?? "").trim().toLowerCase(); + if (nodeEnv === "test") { + return false; + } + const vitest = (env.VITEST ?? "").trim().toLowerCase(); + if (vitest && vitest !== "0" && vitest !== "false" && vitest !== "off") { + return false; + } + return true; +} + +export async function ensureBrowserControlAuth(params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; +}): Promise<{ + auth: BrowserControlAuth; + generatedToken?: string; +}> { + const env = params.env ?? process.env; + const auth = resolveBrowserControlAuth(params.cfg, env); + if (auth.token || auth.password || !shouldAutoGenerateBrowserAuth(env)) { + return { auth }; + } + + const mode = params.cfg.gateway?.auth?.mode; + if (mode === "password" || mode === "none" || mode === "trusted-proxy") { + return { auth }; + } + + const latestCfg = loadConfig(); + const latestAuth = resolveBrowserControlAuth(latestCfg, env); + const latestMode = latestCfg.gateway?.auth?.mode; + if ( + latestAuth.token || + latestAuth.password || + latestMode === "password" || + latestMode === "none" || + latestMode === "trusted-proxy" + ) { + return { auth: latestAuth }; + } + + const ensured = await ensureGatewayStartupAuth({ + cfg: latestCfg, + env, + persist: true, + }); + return { + auth: { + token: ensured.auth.token, + password: ensured.auth.password, + }, + generatedToken: ensured.generatedToken, + }; +} diff --git a/src/plugin-sdk/browser-maintenance.ts b/src/plugin-sdk/browser-maintenance.ts index 446ba3cb1e9..688473dca99 100644 --- a/src/plugin-sdk/browser-maintenance.ts +++ b/src/plugin-sdk/browser-maintenance.ts @@ -5,32 +5,39 @@ import path from "node:path"; import { runCommandWithTimeout } from "../process/exec.js"; import { tryLoadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; -type BrowserRuntimeModule = typeof import("../../extensions/browser/browser-runtime-api.js"); +type CloseTrackedBrowserTabsForSessions = (params: { + sessionKeys: Array; + closeTab?: (tab: { targetId: string; baseUrl?: string; profile?: string }) => Promise; + onWarn?: (message: string) => void; +}) => Promise; + +type MovePathToTrash = (targetPath: string) => Promise; function createTrashCollisionSuffix(): string { return randomBytes(6).toString("hex"); } -export const closeTrackedBrowserTabsForSessions: BrowserRuntimeModule["closeTrackedBrowserTabsForSessions"] = - async (params) => { - if (!Array.isArray(params?.sessionKeys) || params.sessionKeys.length === 0) { - return 0; - } - // Session reset always attempts browser cleanup, even when browser is disabled. - // Keep that path a no-op unless the browser runtime is actually active. - const closeTrackedTabs = tryLoadActivatedBundledPluginPublicSurfaceModuleSync< - Pick - >({ - dirName: "browser", - artifactBasename: "runtime-api.js", - })?.closeTrackedBrowserTabsForSessions; - if (typeof closeTrackedTabs !== "function") { - return 0; - } - return await closeTrackedTabs(params); - }; +export const closeTrackedBrowserTabsForSessions: CloseTrackedBrowserTabsForSessions = async ( + params, +) => { + if (!Array.isArray(params?.sessionKeys) || params.sessionKeys.length === 0) { + return 0; + } + // Session reset always attempts browser cleanup, even when browser is disabled. + // Keep that path a no-op unless the browser runtime is actually active. + const closeTrackedTabs = tryLoadActivatedBundledPluginPublicSurfaceModuleSync<{ + closeTrackedBrowserTabsForSessions: CloseTrackedBrowserTabsForSessions; + }>({ + dirName: "browser", + artifactBasename: "runtime-api.js", + })?.closeTrackedBrowserTabsForSessions; + if (typeof closeTrackedTabs !== "function") { + return 0; + } + return await closeTrackedTabs(params); +}; -export const movePathToTrash: BrowserRuntimeModule["movePathToTrash"] = async (targetPath) => { +export const movePathToTrash: MovePathToTrash = async (targetPath) => { try { const result = await runCommandWithTimeout(["trash", targetPath], { timeoutMs: 10_000 }); if (result.code !== 0) { diff --git a/src/plugin-sdk/elevenlabs.ts b/src/plugin-sdk/elevenlabs.ts deleted file mode 100644 index 288a9710653..00000000000 --- a/src/plugin-sdk/elevenlabs.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Private helper surface for the bundled ElevenLabs speech plugin. -// Keep this surface narrow and limited to config/doctor compatibility. - -export { - ELEVENLABS_TALK_PROVIDER_ID, - ELEVENLABS_TALK_LEGACY_CONFIG_RULES, - hasLegacyTalkFields, - legacyConfigRules, - migrateElevenLabsLegacyTalkConfig, - normalizeCompatibilityConfig, -} from "../../extensions/elevenlabs/contract-api.js"; diff --git a/src/plugin-sdk/zai.ts b/src/plugin-sdk/zai.ts index f4b4aea4837..2d1ccb2d4bb 100644 --- a/src/plugin-sdk/zai.ts +++ b/src/plugin-sdk/zai.ts @@ -1,14 +1,228 @@ -// Internal Z.AI config seam. -// Keep load cheap for config/doctor/test code; do not route through the plugin facade. +import type { ModelDefinitionConfig } from "./provider-model-shared.js"; +import type { OpenClawConfig } from "./provider-onboard.js"; +import { applyProviderConfigWithModelCatalogPreset } from "./provider-onboard.js"; -export { - applyZaiConfig, - applyZaiProviderConfig, - ZAI_CN_BASE_URL, - ZAI_CODING_CN_BASE_URL, - ZAI_CODING_GLOBAL_BASE_URL, - ZAI_DEFAULT_COST, - ZAI_DEFAULT_MODEL_ID, - ZAI_DEFAULT_MODEL_REF, - ZAI_GLOBAL_BASE_URL, -} from "../../extensions/zai/config-api.js"; +export const ZAI_CODING_GLOBAL_BASE_URL = "https://api.z.ai/api/coding/paas/v4"; +export const ZAI_CODING_CN_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4"; +export const ZAI_GLOBAL_BASE_URL = "https://api.z.ai/api/paas/v4"; +export const ZAI_CN_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"; +export const ZAI_DEFAULT_MODEL_ID = "glm-5"; +export const ZAI_DEFAULT_MODEL_REF = `zai/${ZAI_DEFAULT_MODEL_ID}`; + +type ZaiCatalogEntry = { + name: string; + reasoning: boolean; + input: ModelDefinitionConfig["input"]; + contextWindow: number; + maxTokens: number; + cost: ModelDefinitionConfig["cost"]; +}; + +export const ZAI_DEFAULT_COST = { + input: 1, + output: 3.2, + cacheRead: 0.2, + cacheWrite: 0, +} satisfies ModelDefinitionConfig["cost"]; + +const ZAI_MODEL_CATALOG = { + "glm-5.1": { + name: "GLM-5.1", + reasoning: true, + input: ["text"], + contextWindow: 202800, + maxTokens: 131100, + cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 }, + }, + "glm-5": { + name: "GLM-5", + reasoning: true, + input: ["text"], + contextWindow: 202800, + maxTokens: 131100, + cost: ZAI_DEFAULT_COST, + }, + "glm-5-turbo": { + name: "GLM-5 Turbo", + reasoning: true, + input: ["text"], + contextWindow: 202800, + maxTokens: 131100, + cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 }, + }, + "glm-5v-turbo": { + name: "GLM-5V Turbo", + reasoning: true, + input: ["text", "image"], + contextWindow: 202800, + maxTokens: 131100, + cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 }, + }, + "glm-4.7": { + name: "GLM-4.7", + reasoning: true, + input: ["text"], + contextWindow: 204800, + maxTokens: 131072, + cost: { input: 0.6, output: 2.2, cacheRead: 0.11, cacheWrite: 0 }, + }, + "glm-4.7-flash": { + name: "GLM-4.7 Flash", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 131072, + cost: { input: 0.07, output: 0.4, cacheRead: 0, cacheWrite: 0 }, + }, + "glm-4.7-flashx": { + name: "GLM-4.7 FlashX", + reasoning: true, + input: ["text"], + contextWindow: 200000, + maxTokens: 128000, + cost: { input: 0.06, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, + }, + "glm-4.6": { + name: "GLM-4.6", + reasoning: true, + input: ["text"], + contextWindow: 204800, + maxTokens: 131072, + cost: { input: 0.6, output: 2.2, cacheRead: 0.11, cacheWrite: 0 }, + }, + "glm-4.6v": { + name: "GLM-4.6V", + reasoning: true, + input: ["text", "image"], + contextWindow: 128000, + maxTokens: 32768, + cost: { input: 0.3, output: 0.9, cacheRead: 0, cacheWrite: 0 }, + }, + "glm-4.5": { + name: "GLM-4.5", + reasoning: true, + input: ["text"], + contextWindow: 131072, + maxTokens: 98304, + cost: { input: 0.6, output: 2.2, cacheRead: 0.11, cacheWrite: 0 }, + }, + "glm-4.5-air": { + name: "GLM-4.5 Air", + reasoning: true, + input: ["text"], + contextWindow: 131072, + maxTokens: 98304, + cost: { input: 0.2, output: 1.1, cacheRead: 0.03, cacheWrite: 0 }, + }, + "glm-4.5-flash": { + name: "GLM-4.5 Flash", + reasoning: true, + input: ["text"], + contextWindow: 131072, + maxTokens: 98304, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + "glm-4.5v": { + name: "GLM-4.5V", + reasoning: true, + input: ["text", "image"], + contextWindow: 64000, + maxTokens: 16384, + cost: { input: 0.6, output: 1.8, cacheRead: 0, cacheWrite: 0 }, + }, +} as const satisfies Record; + +type ZaiCatalogId = keyof typeof ZAI_MODEL_CATALOG; + +export function resolveZaiBaseUrl(endpoint?: string): string { + switch (endpoint) { + case "coding-cn": + return ZAI_CODING_CN_BASE_URL; + case "global": + return ZAI_GLOBAL_BASE_URL; + case "cn": + return ZAI_CN_BASE_URL; + case "coding-global": + return ZAI_CODING_GLOBAL_BASE_URL; + default: + return ZAI_GLOBAL_BASE_URL; + } +} + +export function buildZaiModelDefinition(params: { + id: string; + name?: string; + reasoning?: boolean; + input?: ModelDefinitionConfig["input"]; + cost?: ModelDefinitionConfig["cost"]; + contextWindow?: number; + maxTokens?: number; +}): ModelDefinitionConfig { + const catalog = ZAI_MODEL_CATALOG[params.id as ZaiCatalogId]; + return { + id: params.id, + name: params.name ?? catalog?.name ?? `GLM ${params.id}`, + reasoning: params.reasoning ?? catalog?.reasoning ?? true, + input: + params.input ?? (catalog?.input ? ([...catalog.input] as ("text" | "image")[]) : ["text"]), + cost: params.cost ?? catalog?.cost ?? ZAI_DEFAULT_COST, + contextWindow: params.contextWindow ?? catalog?.contextWindow ?? 202800, + maxTokens: params.maxTokens ?? catalog?.maxTokens ?? 131100, + }; +} + +const ZAI_DEFAULT_MODELS = [ + buildZaiModelDefinition({ id: "glm-5.1" }), + buildZaiModelDefinition({ id: "glm-5" }), + buildZaiModelDefinition({ id: "glm-5-turbo" }), + buildZaiModelDefinition({ id: "glm-5v-turbo" }), + buildZaiModelDefinition({ id: "glm-4.7" }), + buildZaiModelDefinition({ id: "glm-4.7-flash" }), + buildZaiModelDefinition({ id: "glm-4.7-flashx" }), + buildZaiModelDefinition({ id: "glm-4.6" }), + buildZaiModelDefinition({ id: "glm-4.6v" }), + buildZaiModelDefinition({ id: "glm-4.5" }), + buildZaiModelDefinition({ id: "glm-4.5-air" }), + buildZaiModelDefinition({ id: "glm-4.5-flash" }), + buildZaiModelDefinition({ id: "glm-4.5v" }), +]; + +function resolveZaiPresetBaseUrl(cfg: OpenClawConfig, endpoint?: string): string { + const existingProvider = cfg.models?.providers?.zai; + const existingBaseUrl = + typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : ""; + return endpoint ? resolveZaiBaseUrl(endpoint) : existingBaseUrl || resolveZaiBaseUrl(); +} + +function applyZaiPreset( + cfg: OpenClawConfig, + params?: { endpoint?: string; modelId?: string }, + primaryModelRef?: string, +): OpenClawConfig { + const modelId = params?.modelId?.trim() || ZAI_DEFAULT_MODEL_ID; + const modelRef = `zai/${modelId}`; + return applyProviderConfigWithModelCatalogPreset(cfg, { + providerId: "zai", + api: "openai-completions", + baseUrl: resolveZaiPresetBaseUrl(cfg, params?.endpoint), + catalogModels: ZAI_DEFAULT_MODELS, + aliases: [{ modelRef, alias: "GLM" }], + primaryModelRef, + }); +} + +export function applyZaiProviderConfig( + cfg: OpenClawConfig, + params?: { endpoint?: string; modelId?: string }, +): OpenClawConfig { + return applyZaiPreset(cfg, params); +} + +export function applyZaiConfig( + cfg: OpenClawConfig, + params?: { endpoint?: string; modelId?: string }, +): OpenClawConfig { + const modelId = params?.modelId?.trim() || ZAI_DEFAULT_MODEL_ID; + const modelRef = modelId === ZAI_DEFAULT_MODEL_ID ? ZAI_DEFAULT_MODEL_REF : `zai/${modelId}`; + return applyZaiPreset(cfg, params, modelRef); +}