// api-docs-app.jsx —— PR-S.2 API Docs:第 6 个 console page。 // // Layout:topbar + 左 TOC 栏(260px,sticky)+ 右滚动 article。 // 数据:GET /v1/docs/agent-api.md → marked(CDN)→ HTML 注入。 // TOC 我们自己从 markdown 源里 grep `## ` / `### ` 出标题 + 同步 marked // 生成的锚 id(marked 默认 slugify 跟 GitHub 一致:小写 + 非字母连字符 // 归为 `-`)。点 TOC item 跳锚,scroll-spy 高亮当前可视的 h2。 // // 不引 highlight.js —— 设计稿 / 仓库整体偏 quiet,代码块 mono + 深 bg 即可, // 不上着色器减体积。 const { useState: adUseState, useEffect: adUseEffect, useMemo: adUseMemo, useRef: adUseRef } = React; function createRequestId() { if (window.crypto?.randomUUID) return window.crypto.randomUUID(); return `rid-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; } // ---------------------------------------------------------------------------- // TOC extraction // ---------------------------------------------------------------------------- // Slugify a heading the same way marked default renderer does, so our TOC // anchors line up with the ids it injects. marked's default slugger: // lowercase → strip non-[a-z0-9 -] → spaces → "-" // We add a counter for collisions (marked does too). function slugify(text, seen) { const base = text .toLowerCase() .replace(/[^\w一-龥\s-]/g, "") .trim() .replace(/\s+/g, "-"); let slug = base || "section"; let n = 1; while (seen.has(slug)) { n += 1; slug = `${base}-${n}`; } seen.add(slug); return slug; } // Scan markdown source for `## ` / `### ` headings + slug; skip h1. function extractToc(md) { if (!md) return []; const seen = new Set(); const out = []; // Walk line-by-line but ignore lines inside fenced code blocks. let inFence = false; for (const raw of md.split("\n")) { const line = raw.replace(/\r$/, ""); if (/^```/.test(line)) { inFence = !inFence; continue; } if (inFence) continue; const m = /^(#{1,3})\s+(.+?)\s*$/.exec(line); if (!m) continue; const level = m[1].length; if (level === 1) continue; // skip h1, it's the page title const text = m[2].trim(); out.push({ level, text, id: slugify(text, seen) }); } return out; } // ---------------------------------------------------------------------------- // Markdown renderer // ---------------------------------------------------------------------------- // Configure marked once. // // BUGFIX 三连(api-docs TOC 点击无效)—— 真正的 root cause 是 marked@12 的 // renderer 签名变了:`renderer.heading` 现在收单个 token 对象,不再是老的 // `(text, level)`。旧自定义 renderer 把 token 当字符串 `String(token)` → // "[object Object]",`` → ``,id 全错 → TOC slug // 永远对不上 DOM(连 querySelector("h2[id]") fuzzy 兜底都失效,因为 DOM // 里压根没有 h2/h3,只有坏掉的 hundefined)。 // // 修法:**彻底不靠 marked 给 heading 加 id**。renderer 不 override,让 // marked 出干净的 `

`;渲染进 DOM 后,用 `assignHeadingIds` 走一遍 // 真实 heading 元素,用跟 TOC 完全相同的 slugify + 相同序列打 id。两边由 // 同一函数同一输入产出 → 字节级对齐,且跟 marked 版本解耦。 let _markedReady = false; function ensureMarked() { if (_markedReady || !window.marked) return _markedReady; window.marked.use({ gfm: true, breaks: false }); _markedReady = true; return true; } // 渲染后给真实 DOM heading 打 id + § 锚。处理 h2/h3(跟 extractToc 同口径, // h1 是页标题不进 TOC),fresh seen Set 保证序列跟 TOC 一致。 function assignHeadingIds(root) { if (!root) return; const seen = new Set(); for (const el of root.querySelectorAll("h2, h3")) { const plain = (el.textContent || "").replace(/§\s*$/, "").trim(); const id = slugify(plain, seen); el.id = id; if (!el.querySelector("a.anchor")) { const a = document.createElement("a"); a.className = "anchor"; a.href = `#${id}`; a.setAttribute("aria-label", "anchor"); a.textContent = "§"; el.appendChild(document.createTextNode(" ")); el.appendChild(a); } } } function renderMarkdown(md) { if (!md) return ""; if (!window.marked) return "

marked.js failed to load.

"; ensureMarked(); // id 不在 parse 阶段打 —— 渲染进 DOM 后 assignHeadingIds 统一处理。 return window.marked.parse(md); } // ---------------------------------------------------------------------------- // App // ---------------------------------------------------------------------------- function ApiDocsApp() { const [md, setMd] = adUseState(null); const [error, setError] = adUseState(null); const [activeId, setActiveId] = adUseState(null); const articleRef = adUseRef(null); const selectedDoc = new URLSearchParams(location.search).get("doc") === "apps" ? "apps" : "agent"; const docPath = selectedDoc === "apps" ? "/v1/docs/apps-api.md" : "/v1/docs/agent-api.md"; // PR-S.17d · topbar 上 Users 入口 admin-gate const [isAdmin, setIsAdmin] = adUseState(false); adUseEffect(() => { window.api.authMe() .then((m) => setIsAdmin(!!m.is_admin)) .catch(() => {}); }, []); // Load the markdown source. We don't go through `window.api.call` because // the endpoint returns text/markdown (not JSON) and is public-read inside // an authenticated session — call() would still work but its JSON parse // would fight us. adUseEffect(() => { let cancelled = false; (async () => { try { const token = localStorage.getItem("baizhi.auth.token"); if (!token) { location.replace("/login"); return; } const resp = await fetch(docPath, { headers: { "Authorization": `Bearer ${token}`, "X-Request-ID": createRequestId() }, }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const text = await resp.text(); if (!cancelled) setMd(text); } catch (e) { if (!cancelled) setError(e?.message || String(e)); } })(); return () => { cancelled = true; }; }, [docPath]); const toc = adUseMemo(() => extractToc(md), [md]); const html = adUseMemo(() => renderMarkdown(md), [md]); // 渲染进 DOM 后第一件事:给真实 heading 打 id + § 锚。必须在 deep-link / // scroll-spy 之前(它们靠 getElementById / querySelectorAll("h2[id]"))。 // 同 deps 的 effect 按声明顺序执行,所以放最前面。 adUseEffect(() => { if (!html) return; assignHeadingIds(articleRef.current); }, [html]); // After render: deep-link to whatever hash the URL came in with. adUseEffect(() => { if (!html) return; if (location.hash) { const id = decodeURIComponent(location.hash.slice(1)); const el = document.getElementById(id); if (el) requestAnimationFrame(() => el.scrollIntoView({ behavior: "auto", block: "start" })); } }, [html]); // BUGFIX(TOC 点击无效)第二层:不依赖浏览器原生 fragment 导航 —— // body overflow:hidden + 内部滚动容器布局下原生 #hash 跳转不可靠, // 且 TOC slug(markdown 源)跟 marked slug(渲染后 HTML)对 escape 的 // 处理可能有出入。点击拦截:先 getElementById,找不到按 heading 文本 // 模糊匹配兜底,然后手动 scrollIntoView(article 是真正的滚动容器)。 const scrollToHeading = (it) => { const root = articleRef.current; if (!root) return; let el = document.getElementById(it.id); if (!el) { const want = it.text.replace(/[`*_]/g, "").trim(); el = [...root.querySelectorAll("h2[id], h3[id]")].find((h) => { const plain = (h.textContent || "").replace(/§\s*$/, "").trim(); return plain === want || plain.startsWith(want) || want.startsWith(plain); }) || null; } if (!el) return; el.scrollIntoView({ behavior: "smooth", block: "start" }); // 同步 URL hash(可分享 deep-link),不触发原生跳转 history.replaceState(null, "", `#${el.id}`); setActiveId(el.id); }; // Scroll-spy: which h2/h3 is currently topmost in the visible area? Use a // single IntersectionObserver instead of polling scrollTop on every frame. adUseEffect(() => { if (!html || !articleRef.current) return; const root = articleRef.current; const headings = root.querySelectorAll("h2[id], h3[id]"); if (!headings.length) return; // Pick the heading whose top is closest to ~80px below the viewport top. const visible = new Map(); const obs = new IntersectionObserver( (entries) => { for (const e of entries) { if (e.isIntersecting) visible.set(e.target.id, e.intersectionRatio); else visible.delete(e.target.id); } // Of the currently-intersecting headings, pick the one earliest in DOM. let pick = null; for (const h of headings) { if (visible.has(h.id)) { pick = h.id; break; } } if (pick) setActiveId(pick); }, { root, rootMargin: "-12% 0px -75% 0px", threshold: 0 }, ); headings.forEach((h) => obs.observe(h)); return () => obs.disconnect(); }, [html]); // ---- topbar nav ---- const navStyle = { padding: "6px 10px", borderRadius: 6, color: "var(--text-muted)", textDecoration: "none", fontSize: 13, fontWeight: 500, }; return (
baizhi / API Docs
Playground Manage Evals Traces Marketplace Apps {isAdmin && Users}
{error ? (
Failed to load {docPath}:
{error}
) : !md ? (
Loading API docs…
) : (
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();