refactor: simplify provider inference and zoned parsing helpers

This commit is contained in:
Peter Steinberger
2026-03-23 21:21:06 -07:00
parent 1bfef17825
commit e28e520379
3 changed files with 70 additions and 115 deletions

View File

@@ -20,6 +20,16 @@ export type CommandAuthorization = {
to?: string;
};
type InferredProviderCandidate = {
providerId: ChannelId;
hadResolutionError: boolean;
};
type InferredProviderProbe = {
candidates: InferredProviderCandidate[];
droppedResolutionError: boolean;
};
function resolveProviderFromContext(
ctx: MsgContext,
cfg: OpenClawConfig,
@@ -56,8 +66,25 @@ function resolveProviderFromContext(
return { providerId: normalized, hadResolutionError: false };
}
}
let droppedInferenceResolutionError = false;
const configured = listChannelPlugins()
const inferredProviders = probeInferredProviders(ctx, cfg);
const inferred = inferredProviders.candidates;
if (inferred.length === 1) {
return {
providerId: inferred[0].providerId,
hadResolutionError: inferred[0].hadResolutionError,
};
}
return {
providerId: undefined,
hadResolutionError:
inferredProviders.droppedResolutionError ||
inferred.some((entry) => entry.hadResolutionError),
};
}
function probeInferredProviders(ctx: MsgContext, cfg: OpenClawConfig): InferredProviderProbe {
let droppedResolutionError = false;
const candidates = listChannelPlugins()
.map((plugin) => {
const resolvedAllowFrom = resolveProviderAllowFrom({
plugin,
@@ -72,36 +99,19 @@ function resolveProviderFromContext(
});
if (allowFrom.length === 0) {
if (resolvedAllowFrom.hadResolutionError) {
droppedInferenceResolutionError = true;
droppedResolutionError = true;
}
return null;
}
return {
providerId: plugin.id,
allowFrom,
hadResolutionError: resolvedAllowFrom.hadResolutionError,
};
})
.filter(
(
value,
): value is {
providerId: ChannelId;
allowFrom: string[];
hadResolutionError: boolean;
} => Boolean(value),
);
const inferred = configured.filter((entry) => entry.allowFrom.length > 0);
if (inferred.length === 1) {
return {
providerId: inferred[0].providerId,
hadResolutionError: inferred[0].hadResolutionError,
};
}
.filter((value): value is InferredProviderCandidate => Boolean(value));
return {
providerId: undefined,
hadResolutionError:
droppedInferenceResolutionError || configured.some((entry) => entry.hadResolutionError),
candidates,
droppedResolutionError,
};
}

View File

@@ -1,5 +1,7 @@
import { createHash } from "node:crypto";
const SCRIPT_ATTRIBUTE_NAME_RE = /\s([^\s=/>]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/g;
/**
* Compute SHA-256 CSP hashes for inline `<script>` blocks in an HTML string.
* Only scripts without a `src` attribute are considered inline.
@@ -24,57 +26,9 @@ export function computeInlineScriptHashes(html: string): string[] {
}
function hasScriptSrcAttribute(openTag: string): boolean {
let i = openTag.search(/\bscript\b/i);
if (i < 0) {
return false;
}
i += "script".length;
while (i < openTag.length) {
while (i < openTag.length && /\s/.test(openTag[i] ?? "")) {
i += 1;
}
const current = openTag[i];
if (!current || current === ">") {
return false;
}
if (current === "/") {
i += 1;
continue;
}
const nameStart = i;
while (i < openTag.length && /[^\s=/>]/.test(openTag[i] ?? "")) {
i += 1;
}
const attributeName = openTag.slice(nameStart, i).toLowerCase();
if (attributeName === "src") {
return true;
}
while (i < openTag.length && /\s/.test(openTag[i] ?? "")) {
i += 1;
}
if ((openTag[i] ?? "") !== "=") {
continue;
}
i += 1;
while (i < openTag.length && /\s/.test(openTag[i] ?? "")) {
i += 1;
}
const quote = openTag[i];
if (quote === '"' || quote === "'") {
i += 1;
while (i < openTag.length && openTag[i] !== quote) {
i += 1;
}
if (openTag[i] === quote) {
i += 1;
}
continue;
}
while (i < openTag.length && /[^\s>]/.test(openTag[i] ?? "")) {
i += 1;
}
}
return false;
return Array.from(openTag.matchAll(SCRIPT_ATTRIBUTE_NAME_RE)).some(
(match) => (match[1] ?? "").toLowerCase() === "src",
);
}
export function buildControlUiCspHeader(opts?: { inlineScriptHashes?: string[] }): string {

View File

@@ -21,11 +21,8 @@ export function parseOffsetlessIsoDateTimeInTimeZone(raw: string, timeZone: stri
if (!expectedParts) {
return null;
}
if (!isOffsetlessIsoDateTime(raw)) {
return null;
}
try {
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date());
getZonedDateTimeParts(Date.now(), timeZone);
const naiveMs = new Date(`${raw}Z`).getTime();
if (Number.isNaN(naiveMs)) {
@@ -69,36 +66,33 @@ function matchesOffsetlessIsoDateTimeParts(
timeZone: string,
expected: OffsetlessIsoDateTimeParts,
): boolean {
const utcDate = new Date(utcMs);
if (utcDate.getUTCMilliseconds() !== expected.millisecond) {
return false;
}
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
hourCycle: "h23",
}).formatToParts(utcDate);
const getNumericPart = (type: string) => {
const part = parts.find((candidate) => candidate.type === type);
return Number.parseInt(part?.value ?? "0", 10);
};
const actual = getZonedDateTimeParts(utcMs, timeZone);
return (
getNumericPart("year") === expected.year &&
getNumericPart("month") === expected.month &&
getNumericPart("day") === expected.day &&
getNumericPart("hour") === expected.hour &&
getNumericPart("minute") === expected.minute &&
getNumericPart("second") === expected.second
actual.year === expected.year &&
actual.month === expected.month &&
actual.day === expected.day &&
actual.hour === expected.hour &&
actual.minute === expected.minute &&
actual.second === expected.second &&
actual.millisecond === expected.millisecond
);
}
function getTimeZoneOffsetMs(utcMs: number, timeZone: string): number {
const parts = getZonedDateTimeParts(utcMs, timeZone);
const localAsUtc = Date.UTC(
parts.year,
parts.month - 1,
parts.day,
parts.hour,
parts.minute,
parts.second,
);
return localAsUtc - utcMs;
}
function getZonedDateTimeParts(utcMs: number, timeZone: string): OffsetlessIsoDateTimeParts {
const utcDate = new Date(utcMs);
const parts = new Intl.DateTimeFormat("en-US", {
timeZone,
@@ -111,20 +105,17 @@ function getTimeZoneOffsetMs(utcMs: number, timeZone: string): number {
hour12: false,
hourCycle: "h23",
}).formatToParts(utcDate);
const getNumericPart = (type: string) => {
const part = parts.find((candidate) => candidate.type === type);
return Number.parseInt(part?.value ?? "0", 10);
};
const localAsUtc = Date.UTC(
getNumericPart("year"),
getNumericPart("month") - 1,
getNumericPart("day"),
getNumericPart("hour"),
getNumericPart("minute"),
getNumericPart("second"),
);
return localAsUtc - utcMs;
return {
year: getNumericPart("year"),
month: getNumericPart("month"),
day: getNumericPart("day"),
hour: getNumericPart("hour"),
minute: getNumericPart("minute"),
second: getNumericPart("second"),
millisecond: utcDate.getUTCMilliseconds(),
};
}