fix(web-readability): resolve relative article links

This commit is contained in:
Peter Steinberger
2026-07-13 23:47:49 +01:00
parent 9f06eb23e8
commit 055f6649f9
3 changed files with 16 additions and 62 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- **Web Readability relative links:** seed parsed documents with the request URL so article links resolve correctly while removing the plugin's duplicate lazy-loader facade. (#106860)
- **Browser auto-routing:** fall back to the Gateway host when an implicitly selected browser node reports that its control host is unreachable, while preserving explicit node pins and ambiguous action failures.
- **Discord voice participant context:** maintain the live Gateway voice-state roster and include current channel participants in authorized voice agent turns so agents can answer who is present.
- **OC Path JSONC insertion:** patch object and array insertions through `jsonc-parser` so comments, trailing commas, and CRLF formatting survive. (#106847)

View File

@@ -20,6 +20,7 @@ const SAMPLE_HTML = `<!doctype html>
<h1>Example Article</h1>
<p>Main content starts here with enough words to satisfy readability.</p>
<p>Second paragraph for a bit more signal.</p>
<p><a href="../next">Continue reading</a></p>
</article>
</main>
<footer>Footer text</footer>
@@ -59,6 +60,7 @@ describe("web readability extractor", () => {
});
const extracted = requireReadabilityResult(result);
expect(extracted.text).toContain("Main content starts here");
expect(extracted.text).toContain("[Continue reading](https://example.com/next)");
expect(extracted.title).toBe("Example Article");
});

View File

@@ -1,4 +1,5 @@
// Web Readability plugin module implements web content extractor behavior.
import { createLazyRuntimeSurface } from "openclaw/plugin-sdk/lazy-runtime";
import type {
WebContentExtractionRequest,
WebContentExtractionResult,
@@ -30,65 +31,20 @@ const HTML_VOID_TAGS = new Set([
"wbr",
]);
type ParsedHtml = {
document: Document;
};
type ParseHtml = (html: string) => ParsedHtml;
type ReadabilityResult = {
content?: string;
textContent?: string | null;
title?: string | null;
};
type ReadabilityInstance = {
parse(): ReadabilityResult | null;
};
type ReadabilityConstructor = new (
document: Document,
options: { charThreshold: number },
) => ReadabilityInstance;
type ReadabilityModule = {
Readability: ReadabilityConstructor;
};
type LinkedomModule = {
parseHTML: ParseHtml;
};
const READABILITY_MODULE = "@mozilla/readability";
const LINKEDOM_MODULE = "linkedom";
let readabilityDepsPromise:
| Promise<{
Readability: ReadabilityConstructor;
parseHTML: ParseHtml;
}>
| undefined;
async function loadReadabilityDeps(): Promise<{
Readability: ReadabilityConstructor;
parseHTML: ParseHtml;
}> {
if (!readabilityDepsPromise) {
readabilityDepsPromise = Promise.all([
import(READABILITY_MODULE) as Promise<ReadabilityModule>,
import(LINKEDOM_MODULE) as Promise<LinkedomModule>,
]).then(([readability, linkedom]) => ({
Readability: readability.Readability,
parseHTML: linkedom.parseHTML,
}));
}
try {
return await readabilityDepsPromise;
} catch (error) {
readabilityDepsPromise = undefined;
throw error;
}
}
const loadReadabilityDeps = createLazyRuntimeSurface(
() =>
Promise.all([
import(READABILITY_MODULE) as Promise<typeof import("@mozilla/readability")>,
import(LINKEDOM_MODULE) as Promise<typeof import("linkedom")>,
]),
([readability, linkedom]) => ({
Readability: readability.Readability,
parseHTML: linkedom.parseHTML,
}),
);
function exceedsEstimatedHtmlNestingDepth(html: string, maxDepth: number): boolean {
let depth = 0;
@@ -175,12 +131,7 @@ async function extractWithReadability(
}
try {
const { Readability, parseHTML } = await loadReadabilityDeps();
const { document } = parseHTML(cleanHtml);
try {
(document as { baseURI?: string }).baseURI = request.url;
} catch {
// Best-effort base URI for relative links.
}
const { document } = parseHTML(cleanHtml, { location: { href: request.url } });
const reader = new Readability(document, { charThreshold: 0 });
const parsed = reader.parse();
if (!parsed?.content) {