mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-08 07:12:57 +00:00
Extract web-content shared runtime helpers into packages/web-content-core, move the focused tests with the new package, and split quiet CI shards so the node matrix no longer stalls past the no-output watchdog.\n\nVerification: node scripts/run-vitest.mjs test/scripts/ci-node-test-plan.test.ts test/scripts/run-vitest.test.ts src/infra/restart.test.ts src/infra/os-summary.test.ts src/infra/gateway-processes.test.ts src/infra/inline-option-token.test.ts src/infra/map-size.test.ts src/infra/machine-name.test.ts src/commands/doctor-whatsapp-responsiveness.test.ts; autoreview clean; manual CI https://github.com/openclaw/openclaw/actions/runs/26693962844; dependency guard https://github.com/openclaw/openclaw/actions/runs/26693959937. Admin merge used because optional Mantis Telegram Desktop proof was cancelled after blocking merge outside this PR's required proof.
303 lines
8.5 KiB
TypeScript
303 lines
8.5 KiB
TypeScript
export type WebProviderConfigSource = {
|
|
tools?: {
|
|
web?: {
|
|
search?: unknown;
|
|
fetch?: unknown;
|
|
};
|
|
};
|
|
};
|
|
|
|
type SecretRefSource = "env" | "file" | "exec";
|
|
|
|
type SecretRef = {
|
|
source: SecretRefSource;
|
|
provider: string;
|
|
id: string;
|
|
};
|
|
|
|
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
|
|
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
|
|
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
|
|
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
|
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
|
|
|
|
type RuntimeWebProviderMetadata = {
|
|
providerConfigured?: string;
|
|
selectedProvider?: string;
|
|
};
|
|
|
|
type ProviderWithCredential = {
|
|
envVars: string[];
|
|
authProviderId?: string;
|
|
requiresCredential?: boolean;
|
|
};
|
|
|
|
type WebContentProcessEnv = Record<string, string | undefined>;
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function normalizeSecretInputString(value: unknown): string | undefined {
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
function normalizeSecretInput(value: unknown): string {
|
|
if (typeof value !== "string") {
|
|
return "";
|
|
}
|
|
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
|
|
let latin1Only = "";
|
|
for (const char of collapsed) {
|
|
const codePoint = char.codePointAt(0);
|
|
if (typeof codePoint === "number" && codePoint <= 0xff) {
|
|
latin1Only += char;
|
|
}
|
|
}
|
|
return latin1Only.trim();
|
|
}
|
|
|
|
function isSecretRef(value: unknown): value is SecretRef {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
if (Object.keys(value).length !== 3) {
|
|
return false;
|
|
}
|
|
return (
|
|
(value.source === "env" || value.source === "file" || value.source === "exec") &&
|
|
typeof value.provider === "string" &&
|
|
value.provider.trim().length > 0 &&
|
|
typeof value.id === "string" &&
|
|
value.id.trim().length > 0
|
|
);
|
|
}
|
|
|
|
function coerceSecretRef(value: unknown): SecretRef | null {
|
|
if (isSecretRef(value)) {
|
|
return value;
|
|
}
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
const legacyPrefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)
|
|
? LEGACY_SECRETREF_ENV_MARKER_PREFIX
|
|
: trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
|
|
? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX
|
|
: undefined;
|
|
if (legacyPrefix) {
|
|
const id = trimmed.slice(legacyPrefix.length);
|
|
return ENV_SECRET_REF_ID_RE.test(id)
|
|
? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id }
|
|
: null;
|
|
}
|
|
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
|
|
return match ? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: match[1] } : null;
|
|
}
|
|
if (
|
|
isRecord(value) &&
|
|
(value.source === "env" || value.source === "file" || value.source === "exec") &&
|
|
typeof value.id === "string" &&
|
|
value.id.trim().length > 0 &&
|
|
value.provider === undefined
|
|
) {
|
|
return {
|
|
source: value.source,
|
|
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
|
|
id: value.id,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveWebProviderConfig(
|
|
cfg: WebProviderConfigSource | undefined,
|
|
kind: "search" | "fetch",
|
|
): Record<string, unknown> | undefined {
|
|
const webConfig = cfg?.tools?.web;
|
|
if (!webConfig || typeof webConfig !== "object") {
|
|
return undefined;
|
|
}
|
|
const toolConfig = webConfig[kind];
|
|
if (!toolConfig || typeof toolConfig !== "object") {
|
|
return undefined;
|
|
}
|
|
return toolConfig as Record<string, unknown>;
|
|
}
|
|
|
|
export function readWebProviderEnvValue(
|
|
envVars: string[],
|
|
processEnv: WebContentProcessEnv = process.env,
|
|
): string | undefined {
|
|
for (const envVar of envVars) {
|
|
const value = normalizeSecretInput(processEnv[envVar]);
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function providerRequiresCredential(
|
|
provider: Pick<ProviderWithCredential, "requiresCredential">,
|
|
): boolean {
|
|
return provider.requiresCredential !== false;
|
|
}
|
|
|
|
export function hasWebProviderEntryCredential<
|
|
TProvider extends ProviderWithCredential,
|
|
TConfigSource extends WebProviderConfigSource,
|
|
TConfig extends Record<string, unknown> | undefined,
|
|
>(params: {
|
|
provider: TProvider;
|
|
config: TConfigSource | undefined;
|
|
toolConfig: TConfig;
|
|
resolveRawValue: (params: {
|
|
provider: TProvider;
|
|
config: TConfigSource | undefined;
|
|
toolConfig: TConfig;
|
|
}) => unknown;
|
|
resolveFallbackRawValue?: (params: {
|
|
provider: TProvider;
|
|
config: TConfigSource | undefined;
|
|
toolConfig: TConfig;
|
|
}) => unknown;
|
|
resolveEnvValue: (params: {
|
|
provider: TProvider;
|
|
configuredEnvVarId?: string;
|
|
}) => string | undefined;
|
|
resolveProviderAuthValue?: (providerId: string) => boolean;
|
|
}): boolean {
|
|
if (!providerRequiresCredential(params.provider)) {
|
|
return true;
|
|
}
|
|
const rawValue = params.resolveRawValue({
|
|
provider: params.provider,
|
|
config: params.config,
|
|
toolConfig: params.toolConfig,
|
|
});
|
|
const configuredRef = coerceSecretRef(rawValue);
|
|
if (configuredRef && configuredRef.source !== "env") {
|
|
return true;
|
|
}
|
|
const fromConfig = normalizeSecretInput(normalizeSecretInputString(rawValue));
|
|
if (fromConfig) {
|
|
return true;
|
|
}
|
|
if (
|
|
params.provider.authProviderId &&
|
|
params.resolveProviderAuthValue?.(params.provider.authProviderId)
|
|
) {
|
|
return true;
|
|
}
|
|
if (
|
|
params.resolveEnvValue({
|
|
provider: params.provider,
|
|
configuredEnvVarId: configuredRef?.source === "env" ? configuredRef.id : undefined,
|
|
})
|
|
) {
|
|
return true;
|
|
}
|
|
const fallbackRawValue = params.resolveFallbackRawValue?.({
|
|
provider: params.provider,
|
|
config: params.config,
|
|
toolConfig: params.toolConfig,
|
|
});
|
|
const fallbackRef = coerceSecretRef(fallbackRawValue);
|
|
if (fallbackRef && fallbackRef.source !== "env") {
|
|
return true;
|
|
}
|
|
const fallbackConfig = normalizeSecretInput(normalizeSecretInputString(fallbackRawValue));
|
|
if (fallbackConfig) {
|
|
return true;
|
|
}
|
|
return Boolean(
|
|
fallbackRef?.source === "env"
|
|
? params.resolveEnvValue({
|
|
provider: params.provider,
|
|
configuredEnvVarId: fallbackRef.id,
|
|
})
|
|
: undefined,
|
|
);
|
|
}
|
|
|
|
export function resolveWebProviderDefinition<
|
|
TProvider extends { id: string },
|
|
TConfigSource extends WebProviderConfigSource,
|
|
TConfig extends Record<string, unknown> | undefined,
|
|
TRuntimeMetadata extends RuntimeWebProviderMetadata,
|
|
TDefinition,
|
|
>(params: {
|
|
config: TConfigSource | undefined;
|
|
toolConfig: TConfig;
|
|
runtimeMetadata: TRuntimeMetadata | undefined;
|
|
sandboxed?: boolean;
|
|
providerId?: string;
|
|
providers: TProvider[];
|
|
resolveEnabled: (params: { toolConfig: TConfig; sandboxed?: boolean }) => boolean;
|
|
resolveAutoProviderId: (params: {
|
|
config: TConfigSource | undefined;
|
|
toolConfig: TConfig;
|
|
providers: TProvider[];
|
|
}) => string;
|
|
resolveFallbackProviderId?: (params: {
|
|
config: TConfigSource | undefined;
|
|
toolConfig: TConfig;
|
|
providers: TProvider[];
|
|
providerId: string;
|
|
}) => string | undefined;
|
|
createTool: (params: {
|
|
provider: TProvider;
|
|
config: TConfigSource | undefined;
|
|
toolConfig: TConfig;
|
|
runtimeMetadata: TRuntimeMetadata | undefined;
|
|
}) => TDefinition | null;
|
|
}): { provider: TProvider; definition: TDefinition } | null {
|
|
if (!params.resolveEnabled({ toolConfig: params.toolConfig, sandboxed: params.sandboxed })) {
|
|
return null;
|
|
}
|
|
const providers = params.providers.filter(Boolean);
|
|
if (providers.length === 0) {
|
|
return null;
|
|
}
|
|
const autoProviderId = params.resolveAutoProviderId({
|
|
config: params.config,
|
|
toolConfig: params.toolConfig,
|
|
providers,
|
|
});
|
|
const providerId =
|
|
params.providerId ?? params.runtimeMetadata?.selectedProvider ?? autoProviderId;
|
|
if (!providerId) {
|
|
return null;
|
|
}
|
|
const provider =
|
|
providers.find((entry) => entry.id === providerId) ??
|
|
providers.find(
|
|
(entry) =>
|
|
entry.id ===
|
|
params.resolveFallbackProviderId?.({
|
|
config: params.config,
|
|
toolConfig: params.toolConfig,
|
|
providers,
|
|
providerId,
|
|
}),
|
|
);
|
|
if (!provider) {
|
|
return null;
|
|
}
|
|
const definition = params.createTool({
|
|
provider,
|
|
config: params.config,
|
|
toolConfig: params.toolConfig,
|
|
runtimeMetadata: params.runtimeMetadata,
|
|
});
|
|
if (!definition) {
|
|
return null;
|
|
}
|
|
return { provider, definition };
|
|
}
|