perf(browser): speed up navigation + fix desktop knowledge viewer links

This commit is contained in:
zhayujie
2026-07-16 23:32:06 +08:00
parent 6fad628551
commit 1d226979d1
3 changed files with 127 additions and 22 deletions

View File

@@ -463,7 +463,18 @@ class BrowserService:
headless_cfg = self._config.get("headless") headless_cfg = self._config.get("headless")
self._headless = headless_cfg if headless_cfg is not None else _should_use_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: if self._headless:
launch_args.append("--no-sandbox") launch_args.append("--no-sandbox")
@@ -733,11 +744,15 @@ class BrowserService:
except Exception as e: except Exception as e:
return {"error": f"Navigation failed: {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: try:
page.wait_for_load_state("networkidle", timeout=8000) page.wait_for_load_state("networkidle", timeout=1500)
except Exception: except Exception:
pass pass
page.wait_for_timeout(500) page.wait_for_timeout(300)
try: try:
title = page.title() title = page.title()

View File

@@ -100,29 +100,53 @@ md.renderer.rules.fence = function (tokens, idx, options, env, self) {
interface MarkdownProps { interface MarkdownProps {
content: string 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<MarkdownProps> = ({ content }) => { const Markdown: React.FC<MarkdownProps> = ({ content, onInternalLink }) => {
const rootRef = useRef<HTMLDivElement>(null) const rootRef = useRef<HTMLDivElement>(null)
const html = useMemo(() => md.render(content || ''), [content]) const html = useMemo(() => md.render(content || ''), [content])
// Delegate copy clicks on code blocks (buttons are injected as raw HTML). // Delegate clicks: copy buttons on code blocks, and internal doc links.
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => { const handleClick = useCallback(
const target = e.target as HTMLElement (e: React.MouseEvent<HTMLDivElement>) => {
const btn = target.closest('.code-copy-btn') as HTMLElement | null const target = e.target as HTMLElement
if (!btn) return
const pre = btn.closest('.code-block-wrapper')?.querySelector('pre') // Internal knowledge links (relative *.md), when a handler is provided.
if (!pre) return if (onInternalLink) {
navigator.clipboard.writeText(pre.textContent || '') const a = target.closest('a') as HTMLAnchorElement | null
const original = btn.textContent if (a) {
btn.textContent = t('msg_copied') const href = a.getAttribute('href') || ''
btn.classList.add('copied') if (href.endsWith('.md') && !/^https?:\/\//i.test(href)) {
setTimeout(() => { e.preventDefault()
btn.textContent = original onInternalLink(href)
btn.classList.remove('copied') return
}, 1600) }
}, []) }
}
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 (
<div <div

View File

@@ -49,6 +49,19 @@ const formatSize = (bytes: number): string => {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB' 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). // Flatten the tree into category paths (for destination selectors).
function categoryPaths(dirs: KnowledgeDir[], parent = ''): string[] { function categoryPaths(dirs: KnowledgeDir[], parent = ''): string[] {
const paths: string[] = [] const paths: string[] = []
@@ -105,6 +118,48 @@ function firstFile(list: KnowledgeList): { path: string; title: string } | null
return 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. // Resolve a document's display title from its path, falling back to the stem.
function findTitle(list: KnowledgeList, path: string): string { function findTitle(list: KnowledgeList, path: string): string {
const fallback = path.split('/').pop()?.replace(/\.md$/i, '') || path const fallback = path.split('/').pop()?.replace(/\.md$/i, '') || path
@@ -167,7 +222,7 @@ const KnowledgePage: React.FC<KnowledgePageProps> = ({ baseUrl }) => {
setContent('') setContent('')
try { try {
const res = await apiClient.readKnowledge(path) const res = await apiClient.readKnowledge(path)
setContent(res.content || '') setContent(stripDuplicateH1(res.content || '', title))
} catch { } catch {
setContent(`> ${t('knowledge_doc_load_error')}`) setContent(`> ${t('knowledge_doc_load_error')}`)
} finally { } finally {
@@ -175,6 +230,17 @@ const KnowledgePage: React.FC<KnowledgePageProps> = ({ 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 // Reload the tree. When targetPath is given, open it; otherwise keep the
// currently open doc (or open the first one on the initial load). // currently open doc (or open the first one on the initial load).
const refresh = useCallback( const refresh = useCallback(
@@ -519,7 +585,7 @@ const KnowledgePage: React.FC<KnowledgePageProps> = ({ baseUrl }) => {
<Loader2 size={16} className="animate-spin mr-2" /> <Loader2 size={16} className="animate-spin mr-2" />
</div> </div>
) : ( ) : (
<Markdown content={content} /> <Markdown content={content} onInternalLink={openInternalLink} />
)} )}
</div> </div>
)} )}