From 1d226979d1c12058e0d9c181100d71de6c8ab00f Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 16 Jul 2026 23:32:06 +0800 Subject: [PATCH] perf(browser): speed up navigation + fix desktop knowledge viewer links --- agent/tools/browser/browser_service.py | 21 +++++- .../src/renderer/src/components/Markdown.tsx | 58 ++++++++++----- .../src/renderer/src/pages/KnowledgePage.tsx | 70 ++++++++++++++++++- 3 files changed, 127 insertions(+), 22 deletions(-) diff --git a/agent/tools/browser/browser_service.py b/agent/tools/browser/browser_service.py index 0077e172..e080fa6a 100644 --- a/agent/tools/browser/browser_service.py +++ b/agent/tools/browser/browser_service.py @@ -463,7 +463,18 @@ class BrowserService: headless_cfg = self._config.get("headless") self._headless = headless_cfg if headless_cfg is not None else _should_use_headless() - launch_args = ["--disable-dev-shm-usage"] + launch_args = [ + "--disable-dev-shm-usage", + # Trim first-launch overhead: skip the first-run wizard, the default + # browser prompt, and Chrome's background/component network chatter. + # These have no effect on page interaction but noticeably speed up + # cold starts and each navigation. + "--no-first-run", + "--no-default-browser-check", + "--disable-background-networking", + "--disable-component-update", + "--disable-features=Translate,OptimizationHints", + ] if self._headless: launch_args.append("--no-sandbox") @@ -733,11 +744,15 @@ class BrowserService: except Exception as e: return {"error": f"Navigation failed: {e}"} + # SPAs keep long-lived connections (websockets, polling, analytics) and + # rarely reach true "networkidle", so waiting the full timeout is wasted + # time. domcontentloaded already gives a usable DOM; give the page a + # short grace period for initial render/XHR, then proceed. try: - page.wait_for_load_state("networkidle", timeout=8000) + page.wait_for_load_state("networkidle", timeout=1500) except Exception: pass - page.wait_for_timeout(500) + page.wait_for_timeout(300) try: title = page.title() diff --git a/desktop/src/renderer/src/components/Markdown.tsx b/desktop/src/renderer/src/components/Markdown.tsx index eb44008b..364d1250 100644 --- a/desktop/src/renderer/src/components/Markdown.tsx +++ b/desktop/src/renderer/src/components/Markdown.tsx @@ -100,29 +100,53 @@ md.renderer.rules.fence = function (tokens, idx, options, env, self) { interface MarkdownProps { content: string + /** + * Intercept clicks on internal document links (relative `.md` hrefs). When + * provided, such links open in-app instead of being handed to the OS. Used by + * the knowledge viewer so index links open the target doc rather than firing + * an "application cannot be opened (-120)" error in Electron. + */ + onInternalLink?: (href: string) => void } -const Markdown: React.FC = ({ content }) => { +const Markdown: React.FC = ({ content, onInternalLink }) => { const rootRef = useRef(null) const html = useMemo(() => md.render(content || ''), [content]) - // Delegate copy clicks on code blocks (buttons are injected as raw HTML). - const handleClick = useCallback((e: React.MouseEvent) => { - const target = e.target as HTMLElement - const btn = target.closest('.code-copy-btn') as HTMLElement | null - if (!btn) return - const pre = btn.closest('.code-block-wrapper')?.querySelector('pre') - if (!pre) return - navigator.clipboard.writeText(pre.textContent || '') - const original = btn.textContent - btn.textContent = t('msg_copied') - btn.classList.add('copied') - setTimeout(() => { - btn.textContent = original - btn.classList.remove('copied') - }, 1600) - }, []) + // Delegate clicks: copy buttons on code blocks, and internal doc links. + const handleClick = useCallback( + (e: React.MouseEvent) => { + const target = e.target as HTMLElement + + // Internal knowledge links (relative *.md), when a handler is provided. + if (onInternalLink) { + const a = target.closest('a') as HTMLAnchorElement | null + if (a) { + const href = a.getAttribute('href') || '' + if (href.endsWith('.md') && !/^https?:\/\//i.test(href)) { + e.preventDefault() + onInternalLink(href) + return + } + } + } + + const btn = target.closest('.code-copy-btn') as HTMLElement | null + if (!btn) return + const pre = btn.closest('.code-block-wrapper')?.querySelector('pre') + if (!pre) return + navigator.clipboard.writeText(pre.textContent || '') + const original = btn.textContent + btn.textContent = t('msg_copied') + btn.classList.add('copied') + setTimeout(() => { + btn.textContent = original + btn.classList.remove('copied') + }, 1600) + }, + [onInternalLink] + ) return (
{ return (bytes / (1024 * 1024)).toFixed(1) + ' MB' } +// The viewer already shows the doc title above the body, so a leading `# H1` +// that repeats it looks duplicated. Drop that first H1 (and any blank lines +// right after it) when it matches the title; leave the body untouched otherwise. +function stripDuplicateH1(content: string, title: string): string { + if (!content) return content + const norm = (s: string) => s.trim().toLowerCase() + // Skip a leading blank/whitespace region, then match the first `# heading`. + const m = content.match(/^\s*#\s+(.+?)\s*(?:\r?\n|$)/) + if (!m) return content + if (norm(m[1]) !== norm(title)) return content + return content.slice(m[0].length).replace(/^\s*\r?\n/, '') +} + // Flatten the tree into category paths (for destination selectors). function categoryPaths(dirs: KnowledgeDir[], parent = ''): string[] { const paths: string[] = [] @@ -105,6 +118,48 @@ function firstFile(list: KnowledgeList): { path: string; title: string } | null return null } +// Find a document by its bare filename anywhere in the tree (root files first, +// then a DFS). Used to resolve relative `../foo.md` links from index docs. +function findFileByName(list: KnowledgeList, filename: string): { path: string; title: string } | null { + for (const f of list.root_files || []) { + if (f.name === filename) return { path: f.name, title: f.title || f.name } + } + const walk = (dir: KnowledgeDir, prefix: string): { path: string; title: string } | null => { + const dirPath = prefix ? `${prefix}/${dir.dir}` : dir.dir + for (const f of dir.files) { + if (f.name === filename) return { path: `${dirPath}/${f.name}`, title: f.title || f.name } + } + for (const c of dir.children) { + const hit = walk(c, dirPath) + if (hit) return hit + } + return null + } + for (const d of list.tree || []) { + const hit = walk(d, '') + if (hit) return hit + } + return null +} + +// Resolve a relative `.md` link (from a document body) into a knowledge path. +// Mirrors the web console's bindChatKnowledgeLinks logic: supports +// `knowledge/…/x.md`, `category/x.md`, and bare/relative `../x.md` (by name). +function resolveKnowledgeLink(list: KnowledgeList, href: string): { path: string; title: string } | null { + const clean = href.split('#')[0].split('?')[0] + if (!clean.endsWith('.md')) return null + if (clean.startsWith('knowledge/')) { + const path = clean.replace(/^knowledge\//, '') + return { path, title: findTitle(list, path) } + } + if (/^[a-z0-9_-]+\/[a-z0-9_.-]+\.md$/i.test(clean) && !clean.startsWith('/') && !clean.startsWith('.')) { + return { path: clean, title: findTitle(list, clean) } + } + // Relative/other path: fall back to matching by filename. + const filename = clean.split('/').pop() || clean + return findFileByName(list, filename) +} + // Resolve a document's display title from its path, falling back to the stem. function findTitle(list: KnowledgeList, path: string): string { const fallback = path.split('/').pop()?.replace(/\.md$/i, '') || path @@ -167,7 +222,7 @@ const KnowledgePage: React.FC = ({ baseUrl }) => { setContent('') try { const res = await apiClient.readKnowledge(path) - setContent(res.content || '') + setContent(stripDuplicateH1(res.content || '', title)) } catch { setContent(`> ${t('knowledge_doc_load_error')}`) } finally { @@ -175,6 +230,17 @@ const KnowledgePage: React.FC = ({ baseUrl }) => { } }, []) + // Open an internal knowledge link (relative `.md`) from within a doc body. + // Falls back silently when the target can't be resolved in the current tree. + const openInternalLink = useCallback( + (href: string) => { + if (!data) return + const hit = resolveKnowledgeLink(data, href) + if (hit) void openDoc(hit.path, hit.title) + }, + [data, openDoc] + ) + // Reload the tree. When targetPath is given, open it; otherwise keep the // currently open doc (or open the first one on the initial load). const refresh = useCallback( @@ -519,7 +585,7 @@ const KnowledgePage: React.FC = ({ baseUrl }) => {
) : ( - + )} )}