Files
planet/frontend/src/pages/Docs/docs-content.ts
linkong 8c204717cd
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.70.0
2026-06-04 17:16:23 +08:00

290 lines
11 KiB
TypeScript

export type DocsGroup = 'Overview' | 'Architecture' | 'Manual' | 'Earth' | 'Frontend' | 'Backend' | 'Agents' | 'Ops' | 'Other'
export type DocsLang = 'zh' | 'en'
export interface DocsEntry {
slug: string
filename: string
title: string
group: DocsGroup
order: number
access: DocsAccess
}
export type DocsAccess = 'public' | 'docs_user' | 'docs_developer' | 'docs_admin'
export interface DocsCatalogItem {
slug: string
filename: string
lang: DocsLang
title: string
group: DocsGroup
order: number
access: DocsAccess
}
export interface DocsHeading {
id: string
level: number
text: string
}
export interface DocsMetadataEntry {
zh: { title: string; group: DocsGroup; order: number }
en: { title: string; group: DocsGroup; order: number }
}
const DOCS_GROUP_LABELS: Record<DocsLang, Record<DocsGroup, string>> = {
zh: {
Overview: '概览',
Architecture: '业务架构',
Manual: '使用手册',
Earth: '智能星球',
Frontend: '前端',
Backend: '后端',
Agents: '智能体',
Ops: '运维',
Other: '其他',
},
en: {
Overview: 'Overview',
Architecture: 'Architecture',
Manual: 'Manual',
Earth: 'Intelligent Planet',
Frontend: 'Frontend',
Backend: 'Backend',
Agents: 'Agents',
Ops: 'Ops',
Other: 'Other',
},
}
const DOCS_README_FILENAME = 'README.md'
const MAX_HEADING_ID_LENGTH = 80
export const defaultDocsSlug = 'overview'
export const DOCS_METADATA: Record<string, DocsMetadataEntry> = {
[DOCS_README_FILENAME]: {
zh: { title: '技术文档', group: 'Overview', order: 0 },
en: { title: 'Technical Docs', group: 'Overview', order: 0 },
},
'quickstart.md': {
zh: { title: '快速开始', group: 'Manual', order: 2 },
en: { title: 'Quickstart', group: 'Manual', order: 2 },
},
'manual.md': {
zh: { title: '智能星球使用手册', group: 'Manual', order: 1 },
en: { title: 'Intelligent Planet Manual', group: 'Manual', order: 1 },
},
'faq.md': {
zh: { title: '常见问题', group: 'Manual', order: 3 },
en: { title: 'FAQ', group: 'Manual', order: 3 },
},
'platform-data-flows.md': {
zh: { title: '业务架构与数据流转', group: 'Architecture', order: 5 },
en: { title: 'Business Architecture and Data Flows', group: 'Architecture', order: 5 },
},
'earth-frontend-context.md': {
zh: { title: '智能星球前端结构', group: 'Earth', order: 10 },
en: { title: 'Intelligent Planet Frontend Context', group: 'Earth', order: 10 },
},
'earth-layer-style-reference.md': {
zh: { title: '智能星球图层样式属性索引', group: 'Earth', order: 11 },
en: { title: 'Intelligent Planet Layer Style Reference', group: 'Earth', order: 11 },
},
'earth-render-layer-order.md': {
zh: { title: '智能星球渲染图层顺序', group: 'Earth', order: 12 },
en: { title: 'Intelligent Planet Render Layer Order', group: 'Earth', order: 12 },
},
'earth-satellite-footprint-policy.md': {
zh: { title: '智能星球卫星覆盖策略', group: 'Earth', order: 13 },
en: { title: 'Intelligent Planet Satellite Footprint Policy', group: 'Earth', order: 13 },
},
'earth-bgp-context.md': {
zh: { title: 'BGP 态势上下文', group: 'Earth', order: 14 },
en: { title: 'BGP Context', group: 'Earth', order: 14 },
},
'earth-news-live-streams-collector-format.md': {
zh: { title: '新闻直播采集格式', group: 'Backend', order: 36 },
en: { title: 'News Live Streams Collector Format', group: 'Backend', order: 36 },
},
'earth-interactable-usage.md': {
zh: { title: '智能星球可交互图标接入', group: 'Earth', order: 16 },
en: { title: 'Intelligent Planet Interactable Usage', group: 'Earth', order: 16 },
},
'earth-interactable-clustering.md': {
zh: { title: '智能星球可交互图标聚类策略', group: 'Earth', order: 17 },
en: { title: 'Intelligent Planet Interactable Clustering', group: 'Earth', order: 17 },
},
'earth-toolbar-overlay-coordination.md': {
zh: { title: '智能星球工具栏与浮层协同', group: 'Earth', order: 18 },
en: { title: 'Intelligent Planet Toolbar and Overlay Coordination', group: 'Earth', order: 18 },
},
'earth-news-sources.md': {
zh: { title: '智能星球新闻源配置', group: 'Earth', order: 19 },
en: { title: 'Intelligent Planet News Source Configuration', group: 'Earth', order: 19 },
},
'frontend-admin-frontend-context.md': {
zh: { title: '控制台前端结构', group: 'Frontend', order: 20 },
en: { title: 'Admin Frontend Context', group: 'Frontend', order: 20 },
},
'frontend-layout-guidelines.md': {
zh: { title: '前端布局指南', group: 'Frontend', order: 21 },
en: { title: 'Frontend Layout Guidelines', group: 'Frontend', order: 21 },
},
'docs-gatekeeper-development.md': {
zh: { title: 'Docs Gatekeeper 开发说明', group: 'Backend', order: 37 },
en: { title: 'Docs Gatekeeper Development Guide', group: 'Backend', order: 37 },
},
'naming-glossary.md': {
zh: { title: '命名与术语对照', group: 'Architecture', order: 6 },
en: { title: 'Naming Glossary', group: 'Architecture', order: 6 },
},
'tactile-ui-components.md': {
zh: { title: 'Tactile UI 组件库', group: 'Frontend', order: 24 },
en: { title: 'Tactile UI Components', group: 'Frontend', order: 24 },
},
'backend-collectors.md': {
zh: { title: '数据采集系统', group: 'Backend', order: 30 },
en: { title: 'Data Collectors', group: 'Backend', order: 30 },
},
'backend-enum-contracts.md': {
zh: { title: '后端枚举与字符串兼容契约', group: 'Backend', order: 35 },
en: { title: 'Backend Enum and String Compatibility Contract', group: 'Backend', order: 35 },
},
'backend-system-service-control.md': {
zh: { title: '系统服务控制', group: 'Backend', order: 31 },
en: { title: 'System Service Control', group: 'Backend', order: 31 },
},
'datasource-collector-settings-connectivity.md': {
zh: { title: '数据源、采集器设置与连接验证', group: 'Backend', order: 32 },
en: { title: 'Datasource Collector Settings and Connectivity', group: 'Backend', order: 32 },
},
'backend-datasources-api-performance.md': {
zh: { title: '数据源 API 性能', group: 'Backend', order: 33 },
en: { title: 'Datasource API Performance', group: 'Backend', order: 33 },
},
'data-job-earth-sync-architecture.md': {
zh: { title: '数据作业与 Outbox 技术架构', group: 'Backend', order: 34 },
en: { title: 'Data Jobs and Outbox Architecture', group: 'Backend', order: 34 },
},
'location-pipeline-development.md': {
zh: { title: '通用位置估算管线开发说明', group: 'Backend', order: 35 },
en: { title: 'Shared Location Resolution Pipeline Development Guide', group: 'Backend', order: 35 },
},
'agents-aiprovider.md': {
zh: { title: 'AI Provider 指南', group: 'Agents', order: 40 },
en: { title: 'AI Provider Guide', group: 'Agents', order: 40 },
},
'ops-runbook.md': {
zh: { title: '智能星球运维手册', group: 'Ops', order: 49 },
en: { title: 'Intelligent Planet Ops Runbook', group: 'Ops', order: 49 },
},
'ops-docker-compose-buildx-upgrade.md': {
zh: { title: 'Docker + Compose + Buildx 升级', group: 'Ops', order: 50 },
en: { title: 'Docker + Compose + Buildx Upgrade', group: 'Ops', order: 50 },
},
'ops-planet-sh-startup.md': {
zh: { title: 'planet.sh 启动机制', group: 'Ops', order: 51 },
en: { title: 'planet.sh Startup', group: 'Ops', order: 51 },
},
}
const GROUP_ORDER: DocsGroup[] = ['Overview', 'Manual', 'Architecture', 'Earth', 'Frontend', 'Backend', 'Agents', 'Ops', 'Other']
const PUBLIC_DOCS_FILENAMES = new Set(Object.keys(DOCS_METADATA))
export function slugFromFilename(filename: string): string {
return filename === DOCS_README_FILENAME ? defaultDocsSlug : filename.replace(/\.md$/, '')
}
export function getDocsEntries(lang: DocsLang, catalogItems: DocsCatalogItem[]): DocsEntry[] {
return catalogItems
.filter((item) => item.lang === lang)
.map((item) => ({
slug: item.slug,
filename: item.filename,
title: item.title,
group: item.group,
order: item.order,
access: item.access,
}))
.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title))
}
export function getDocsEntry(slug: string | undefined, entries: DocsEntry[]): DocsEntry | undefined {
const normalizedSlug = slug || defaultDocsSlug
return entries.find((entry) => entry.slug === normalizedSlug)
}
export function groupDocsEntries(entries: DocsEntry[]): Array<{ group: DocsGroup; entries: DocsEntry[] }> {
return GROUP_ORDER.map((group) => ({
group,
entries: entries.filter((entry) => entry.group === group),
})).filter((group) => group.entries.length > 0)
}
export function getDocsGroupLabel(group: DocsGroup, lang: DocsLang): string {
return DOCS_GROUP_LABELS[lang][group]
}
export function createHeadingId(text: string, usedIds: Map<string, number>): string {
const base = text
.toLowerCase()
.replace(/`([^`]+)`/g, '$1')
.replace(/[^\p{L}\p{N}\s-]/gu, '')
.trim()
.replace(/\s+/g, '-')
.slice(0, MAX_HEADING_ID_LENGTH) || 'section'
const count = usedIds.get(base) || 0
usedIds.set(base, count + 1)
return count === 0 ? base : `${base}-${count + 1}`
}
export function extractHeadings(markdown: string): DocsHeading[] {
const usedIds = new Map<string, number>()
return markdown
.split(/\r?\n/)
.map((line) => line.trim().match(/^(#{1,3})\s+(.+)$/))
.filter((match): match is RegExpMatchArray => Boolean(match))
.map((match) => {
const text = match[2].trim()
return {
id: createHeadingId(text, usedIds),
level: match[1].length,
text,
}
})
}
// Returns a factory — call factory() inside MarkdownRenderer to get a fresh resolver
// per render. This is necessary because StrictMode double-invokes renders, which
// would exhaust a shared stateful closure and cause heading IDs to become undefined.
export function createHeadingIdResolver(markdown: string): () => (text: string, level: number) => string | undefined {
const headings = extractHeadings(markdown)
return () => {
const indexByKey = new Map<string, number>()
return (text: string, level: number) => {
const key = `${level}:${text}`
const currentIndex = indexByKey.get(key) || 0
indexByKey.set(key, currentIndex + 1)
const matchingHeadings = headings.filter((heading) => heading.level === level && heading.text === text)
return matchingHeadings[currentIndex]?.id
}
}
}
export function slugFromDocsHref(href: string): string | null {
const normalized = decodeURIComponent(href).split('#')[0].replace(/\\/g, '/')
if (!/\/docs\/technical\/(zh|en)\//.test(normalized)) {
return null
}
const filename = normalized.split('/').pop()
if (!filename?.endsWith('.md') || !PUBLIC_DOCS_FILENAMES.has(filename)) {
return null
}
return slugFromFilename(filename)
}