Skip to content

Commit 105d77d

Browse files
committed
fix(ui): open docs markdown links on docs host
Rewrite recognized docs-root markdown links in Control UI renderers to https://docs.openclaw.ai while preserving Control UI routes, base-mounted resources, and plugin viewer URLs. Fixes #89465.
1 parent 4eb4b87 commit 105d77d

2 files changed

Lines changed: 360 additions & 1 deletion

File tree

ui/src/ui/markdown.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ function htmlFragment(html: string): HTMLElement {
1616
return container;
1717
}
1818

19+
function withControlUiBasePath<T>(basePath: string, fn: () => T): T {
20+
Object.defineProperty(window, "__OPENCLAW_CONTROL_UI_BASE_PATH__", {
21+
value: basePath,
22+
writable: true,
23+
configurable: true,
24+
});
25+
try {
26+
return fn();
27+
} finally {
28+
delete window["__OPENCLAW_CONTROL_UI_BASE_PATH__"];
29+
}
30+
}
31+
1932
describe("toSanitizedMarkdownHtml", () => {
2033
// ── Original tests from before markdown-it migration ──
2134
it("strips scripts and unsafe links", () => {
@@ -589,6 +602,26 @@ PY
589602
'<p><a href="/usage" rel="noreferrer noopener" target="_blank">usage</a></p>\n',
590603
);
591604
});
605+
606+
it("rewrites docs-root links to the public docs host", () => {
607+
const html = toSanitizedMarkdownHtml(
608+
"[workspace](/concepts/agent-workspace) [hooks](/automation/hooks#session-memory) [telegram](/channels/telegram?tab=setup) [shortlink](/telegram) [openai](/openai) [images](/images) [groups](/groups) [camera](/nodes/camera) [macOS](/platforms/macos) [cliSessions](/cli/sessions) [toolSkills](/tools/skills) [pluginDocs](/plugins/reference/diffs) [prose](/prose) [refactor](/refactor/ingress-core)",
609+
);
610+
expect(html).toBe(
611+
'<p><a href="https://docs.openclaw.ai/concepts/agent-workspace" rel="noreferrer noopener" target="_blank">workspace</a> <a href="https://docs.openclaw.ai/automation/hooks#session-memory" rel="noreferrer noopener" target="_blank">hooks</a> <a href="https://docs.openclaw.ai/channels/telegram?tab=setup" rel="noreferrer noopener" target="_blank">telegram</a> <a href="https://docs.openclaw.ai/telegram" rel="noreferrer noopener" target="_blank">shortlink</a> <a href="https://docs.openclaw.ai/openai" rel="noreferrer noopener" target="_blank">openai</a> <a href="https://docs.openclaw.ai/images" rel="noreferrer noopener" target="_blank">images</a> <a href="https://docs.openclaw.ai/groups" rel="noreferrer noopener" target="_blank">groups</a> <a href="https://docs.openclaw.ai/nodes/camera" rel="noreferrer noopener" target="_blank">camera</a> <a href="https://docs.openclaw.ai/platforms/macos" rel="noreferrer noopener" target="_blank">macOS</a> <a href="https://docs.openclaw.ai/cli/sessions" rel="noreferrer noopener" target="_blank">cliSessions</a> <a href="https://docs.openclaw.ai/tools/skills" rel="noreferrer noopener" target="_blank">toolSkills</a> <a href="https://docs.openclaw.ai/plugins/reference/diffs" rel="noreferrer noopener" target="_blank">pluginDocs</a> <a href="https://docs.openclaw.ai/prose" rel="noreferrer noopener" target="_blank">prose</a> <a href="https://docs.openclaw.ai/refactor/ingress-core" rel="noreferrer noopener" target="_blank">refactor</a></p>\n',
612+
);
613+
});
614+
615+
it("keeps app and resource routes instead of treating them as docs roots", () => {
616+
const html = withControlUiBasePath("/control", () =>
617+
toSanitizedMarkdownHtml(
618+
"[channels](/channels) [automation](/automation) [workshop](/skills/workshop) [chat](/chat) [baseChat](/control/chat?session=abc) [baseSessions](/control/sessions) [health](/healthz) [pluginDynamic](/googlechat) [asset](/api/files/1) [baseApi](/control/api/files/1) [baseAvatar](/control/avatar/main) [plugin](/plugins/diffs/view/id/token) [basePlugin](/control/plugins/diffs/view/id/token) [artifact](/__openclaw__/canvas/documents/x/index.html) [baseArtifact](/control/__openclaw__/canvas/x)",
619+
),
620+
);
621+
expect(html).toBe(
622+
'<p><a href="/channels" rel="noreferrer noopener" target="_blank">channels</a> <a href="/automation" rel="noreferrer noopener" target="_blank">automation</a> <a href="/skills/workshop" rel="noreferrer noopener" target="_blank">workshop</a> <a href="/chat" rel="noreferrer noopener" target="_blank">chat</a> <a href="/control/chat?session=abc" rel="noreferrer noopener" target="_blank">baseChat</a> <a href="/control/sessions" rel="noreferrer noopener" target="_blank">baseSessions</a> <a href="/healthz" rel="noreferrer noopener" target="_blank">health</a> <a href="/googlechat" rel="noreferrer noopener" target="_blank">pluginDynamic</a> <a href="/api/files/1" rel="noreferrer noopener" target="_blank">asset</a> <a href="/control/api/files/1" rel="noreferrer noopener" target="_blank">baseApi</a> <a href="/control/avatar/main" rel="noreferrer noopener" target="_blank">baseAvatar</a> <a href="/plugins/diffs/view/id/token" rel="noreferrer noopener" target="_blank">plugin</a> <a href="/control/plugins/diffs/view/id/token" rel="noreferrer noopener" target="_blank">basePlugin</a> <a href="/__openclaw__/canvas/documents/x/index.html" rel="noreferrer noopener" target="_blank">artifact</a> <a href="/control/__openclaw__/canvas/x" rel="noreferrer noopener" target="_blank">baseArtifact</a></p>\n',
623+
);
624+
});
592625
});
593626

594627
describe("ReDoS protection", () => {

0 commit comments

Comments
 (0)