mirror of https://github.com/openclaw/openclaw.git
refactor: collapse plugin sdk extension shims
This commit is contained in:
parent
eb8f0e1bf2
commit
6f2f840e97
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<string, BrowserProfileConfig>;
|
||||
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<string, BrowserProfileConfig> | undefined,
|
||||
defaultColor: string,
|
||||
legacyCdpPort?: number,
|
||||
derivedDefaultCdpPort?: number,
|
||||
legacyCdpUrl?: string,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
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<string, BrowserProfileConfig>,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
"allowlist-config-edit",
|
||||
"bluebubbles",
|
||||
"bluebubbles-policy",
|
||||
"elevenlabs",
|
||||
"browser-config",
|
||||
"browser-config-support",
|
||||
"browser-support",
|
||||
"boolean-param",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.<provider>",
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, BrowserProfileConfig>;
|
||||
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<string, BrowserProfileConfig> | undefined,
|
||||
defaultColor: string,
|
||||
legacyCdpPort?: number,
|
||||
derivedDefaultCdpPort?: number,
|
||||
legacyCdpUrl?: string,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
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<string, BrowserProfileConfig>,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string | undefined>;
|
||||
closeTab?: (tab: { targetId: string; baseUrl?: string; profile?: string }) => Promise<void>;
|
||||
onWarn?: (message: string) => void;
|
||||
}) => Promise<number>;
|
||||
|
||||
type MovePathToTrash = (targetPath: string) => Promise<string>;
|
||||
|
||||
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<BrowserRuntimeModule, "closeTrackedBrowserTabsForSessions">
|
||||
>({
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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<string, ZaiCatalogEntry>;
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue