mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
An image the manifest does not publish was rewritten to a raw.githubusercontent URL, which serves a private repository to nobody: raw answers 404 unauthenticated, and a github.com session does not authenticate it. Every image on the site was broken for every reader, and no gate could say so — link checking asks whether the file exists in the repository, not whether a site reader can fetch it. The projector now copies a referenced image into the generated tree beside the page and points the reference at it, so Vite bundles it like any other site asset and repository visibility cannot reach the published page. The copy lands beside the page so one relative URL is right from both locale trees and a page's assets leave with the page; two sources projecting onto one path throw rather than letting the last copy win. Canonical Markdown keeps writing ordinary repository-relative paths, so the same file still renders when read in the repository. The guide this was found through is renamed to its subject: 配置模型.
357 lines
14 KiB
TypeScript
357 lines
14 KiB
TypeScript
/**
|
|
* Build-time projection from canonical repository Markdown into VitePress.
|
|
*
|
|
* The generated tree is disposable: sources stay in their owning `docs/`
|
|
* tier, while this adapter rewrites cross-source links for the public site.
|
|
*/
|
|
|
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { basename, dirname, extname, posix, relative, resolve, sep } from 'node:path'
|
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
|
import { gfm } from 'micromark-extension-gfm'
|
|
import type { Nodes } from 'mdast'
|
|
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
|
|
|
|
const REPOSITORY_URL = 'https://github.com/deepseek-harness/deepseek-harness'
|
|
const root = resolve(import.meta.dirname, '..')
|
|
const generatedRoot = resolve(root, 'website/.generated')
|
|
|
|
interface Replacement {
|
|
start: number
|
|
end: number
|
|
value: string
|
|
}
|
|
|
|
interface DestinationRange {
|
|
start: number
|
|
end: number
|
|
}
|
|
|
|
type RewritableNode = Extract<Nodes, { type: 'link' | 'image' | 'definition' }>
|
|
|
|
/** Inputs for rewriting one canonical Markdown page. */
|
|
export interface RewriteMarkdownOptions {
|
|
locale: DocsLocale
|
|
sourcePath: string
|
|
route: string
|
|
pages: DocsPage[]
|
|
repoRoot: string
|
|
repositoryRef: string
|
|
/**
|
|
* Place one referenced image beside the projected page and return the URL to
|
|
* reach it from that page. A GitHub raw URL cannot serve this repository —
|
|
* `raw.githubusercontent.com` answers 404 for a private one, and no reader of
|
|
* the site is authenticated to it — so an image travels into the generated
|
|
* tree and Vite bundles it like any other site asset. Omitted by callers that
|
|
* only rewrite text, which then leave images pointing at the repository.
|
|
*/
|
|
placeImage?: (absPath: string) => string
|
|
}
|
|
|
|
function repoPath(absPath: string, repoRoot: string): string {
|
|
return relative(repoRoot, absPath).split(sep).join('/')
|
|
}
|
|
|
|
function isExternalOrSiteAbsolute(url: string): boolean {
|
|
return url.startsWith('#')
|
|
|| url.startsWith('//')
|
|
|| url.startsWith('/')
|
|
|| /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)
|
|
}
|
|
|
|
function skipWhitespace(source: string, start: number): number {
|
|
let index = start
|
|
while (/\s/.test(source[index] ?? '')) index += 1
|
|
return index
|
|
}
|
|
|
|
function labelEnd(source: string): number {
|
|
const first = source.indexOf('[')
|
|
if (first === -1) return -1
|
|
let depth = 0
|
|
for (let index = first; index < source.length; index += 1) {
|
|
const char = source[index]
|
|
if (char === '\\') {
|
|
index += 1
|
|
} else if (char === '[') {
|
|
depth += 1
|
|
} else if (char === ']') {
|
|
depth -= 1
|
|
if (depth === 0) return index
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
function destinationRange(rawNode: string, type: 'link' | 'image' | 'definition'): DestinationRange {
|
|
const endOfLabel = labelEnd(rawNode)
|
|
if (endOfLabel === -1) {
|
|
throw new Error(`project-doc-site: cannot locate label end in ${JSON.stringify(rawNode)}.`)
|
|
}
|
|
|
|
let start: number
|
|
if (type === 'definition') {
|
|
const colon = rawNode.indexOf(':', endOfLabel + 1)
|
|
if (colon === -1) {
|
|
throw new Error(`project-doc-site: cannot locate definition separator in ${JSON.stringify(rawNode)}.`)
|
|
}
|
|
start = skipWhitespace(rawNode, colon + 1)
|
|
} else {
|
|
if (rawNode[endOfLabel + 1] !== '(') {
|
|
throw new Error(`project-doc-site: cannot locate inline destination in ${JSON.stringify(rawNode)}.`)
|
|
}
|
|
start = skipWhitespace(rawNode, endOfLabel + 2)
|
|
}
|
|
|
|
if (rawNode[start] === '<') {
|
|
for (let index = start + 1; index < rawNode.length; index += 1) {
|
|
if (rawNode[index] === '\\') index += 1
|
|
else if (rawNode[index] === '>') return { start: start + 1, end: index }
|
|
}
|
|
throw new Error(`project-doc-site: cannot locate angle-bracket destination end in ${JSON.stringify(rawNode)}.`)
|
|
}
|
|
|
|
let depth = 0
|
|
for (let index = start; index < rawNode.length; index += 1) {
|
|
const char = rawNode[index]
|
|
if (char === '\\') {
|
|
index += 1
|
|
} else if (char === '(') {
|
|
depth += 1
|
|
} else if (char === ')') {
|
|
if (depth === 0) return { start, end: index }
|
|
depth -= 1
|
|
} else if (/\s/.test(char ?? '') && depth === 0) {
|
|
return { start, end: index }
|
|
}
|
|
}
|
|
return { start, end: rawNode.length }
|
|
}
|
|
|
|
function splitTarget(url: string): { path: string; suffix: string } {
|
|
const boundary = url.search(/[?#]/)
|
|
if (boundary === -1) return { path: url, suffix: '' }
|
|
return { path: url.slice(0, boundary), suffix: url.slice(boundary) }
|
|
}
|
|
|
|
function decodePath(path: string): string {
|
|
try {
|
|
return decodeURIComponent(path)
|
|
} catch {
|
|
throw new Error(`project-doc-site: malformed percent escape in ${JSON.stringify(path)}.`)
|
|
}
|
|
}
|
|
|
|
function routeTarget(fromRoute: string, toRoute: string, suffix: string): string {
|
|
const target = posix.relative(posix.dirname(fromRoute), toRoute)
|
|
return `${target.startsWith('.') ? target : `./${target}`}${suffix}`
|
|
}
|
|
|
|
function sourceMap(pages: DocsPage[]): Map<string, Map<DocsLocale, DocsPage>> {
|
|
const map = new Map<string, Map<DocsLocale, DocsPage>>()
|
|
for (const page of pages) {
|
|
for (const source of [page.source, ...(page.sourceAliases ?? [])]) {
|
|
const localized = map.get(source) ?? new Map<DocsLocale, DocsPage>()
|
|
if (localized.has(page.locale)) {
|
|
throw new Error(`project-doc-site: duplicate source or alias ${JSON.stringify(source)} for locale ${JSON.stringify(page.locale)}.`)
|
|
}
|
|
localized.set(page.locale, page)
|
|
map.set(source, localized)
|
|
}
|
|
}
|
|
return map
|
|
}
|
|
|
|
function counterpartSource(source: string): string {
|
|
return source.endsWith('.zh.md')
|
|
? source.replace(/\.zh\.md$/, '.md')
|
|
: source.replace(/\.md$/, '.zh.md')
|
|
}
|
|
|
|
function resolveRepositoryTarget(sourceAbs: string, rawPath: string, repoRoot: string): { absPath: string; line?: number } {
|
|
const decoded = decodePath(rawPath)
|
|
let absPath = resolve(dirname(sourceAbs), decoded)
|
|
if (existsSync(absPath)) return { absPath }
|
|
|
|
const lineMatch = decoded.match(/:(\d+)$/)
|
|
if (lineMatch !== null) {
|
|
const lineText = lineMatch[1]
|
|
if (lineText === undefined) throw new Error('project-doc-site: line suffix matched without a line number.')
|
|
absPath = resolve(dirname(sourceAbs), decoded.slice(0, -lineMatch[0].length))
|
|
if (existsSync(absPath)) return { absPath, line: Number.parseInt(lineText, 10) }
|
|
}
|
|
|
|
if (extname(decoded) === '') {
|
|
const markdown = resolve(dirname(sourceAbs), `${decoded}.md`)
|
|
if (existsSync(markdown)) return { absPath: markdown }
|
|
const index = resolve(dirname(sourceAbs), decoded, 'index.md')
|
|
if (existsSync(index)) return { absPath: index }
|
|
}
|
|
|
|
throw new Error(`project-doc-site: ${repoPath(sourceAbs, repoRoot)} links to missing path ${JSON.stringify(rawPath)}.`)
|
|
}
|
|
|
|
function githubTarget(
|
|
absPath: string,
|
|
line: number | undefined,
|
|
suffix: string,
|
|
repositoryRef: string,
|
|
repoRoot: string,
|
|
image: boolean,
|
|
): string {
|
|
const path = repoPath(absPath, repoRoot)
|
|
if (image) return `https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/${repositoryRef}/${path}${suffix}`
|
|
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
|
|
const lineSuffix = line === undefined ? suffix : `#L${line}`
|
|
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`
|
|
}
|
|
|
|
/**
|
|
* Rewrite repository-relative links without reserializing Markdown.
|
|
*
|
|
* @param source Markdown text from the canonical file.
|
|
* @param options Source, route, manifest, and repository context.
|
|
* @returns Markdown whose published links resolve inside the site or to GitHub.
|
|
*/
|
|
export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions): string {
|
|
const sourceAbs = resolve(options.repoRoot, options.sourcePath)
|
|
const published = sourceMap(options.pages)
|
|
const tree = fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
|
|
const replacements: Replacement[] = []
|
|
|
|
const rewrite = (node: RewritableNode): void => {
|
|
if (isExternalOrSiteAbsolute(node.url)) return
|
|
const { path, suffix } = splitTarget(node.url)
|
|
if (path === '') return
|
|
const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot)
|
|
const targetPath = repoPath(absPath, options.repoRoot)
|
|
const isLanguageSwitcher = targetPath === counterpartSource(options.sourcePath)
|
|
const targetLocale: DocsLocale = isLanguageSwitcher
|
|
? options.locale === 'root' ? 'en' : 'root'
|
|
: options.locale
|
|
const page = published.get(targetPath)?.get(targetLocale)
|
|
const nextUrl = page !== undefined
|
|
? routeTarget(options.route, page.route, suffix)
|
|
: node.type === 'image' && options.placeImage !== undefined
|
|
? options.placeImage(absPath)
|
|
: githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
|
|
|
|
const start = node.position?.start.offset
|
|
const end = node.position?.end.offset
|
|
if (start === undefined || end === undefined) {
|
|
throw new Error(`project-doc-site: link ${JSON.stringify(node.url)} has no source offsets.`)
|
|
}
|
|
const rawNode = source.slice(start, end)
|
|
const rawDestination = destinationRange(rawNode, node.type)
|
|
replacements.push({
|
|
start: start + rawDestination.start,
|
|
end: start + rawDestination.end,
|
|
value: nextUrl,
|
|
})
|
|
}
|
|
|
|
const visit = (node: Nodes): void => {
|
|
if ((node.type === 'link' || node.type === 'image' || node.type === 'definition') && 'url' in node) rewrite(node)
|
|
if ('children' in node) {
|
|
for (const child of node.children) visit(child)
|
|
}
|
|
}
|
|
visit(tree)
|
|
|
|
let projected = source
|
|
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
|
projected = projected.slice(0, replacement.start) + replacement.value + projected.slice(replacement.end)
|
|
}
|
|
return projected
|
|
}
|
|
|
|
/**
|
|
* Record the canonical edit target in VitePress frontmatter.
|
|
*
|
|
* @param markdown Projected Markdown content.
|
|
* @param page Publication manifest entry for the content.
|
|
* @returns Markdown with projection-owned frontmatter fields.
|
|
*/
|
|
export function addProjectionFrontmatter(markdown: string, page: Pick<DocsPage, 'source' | 'outline'>): string {
|
|
const fields = [
|
|
`editSource: ${JSON.stringify(page.source)}`,
|
|
...(page.outline === undefined ? [] : [`outline: ${JSON.stringify(page.outline)}`]),
|
|
].join('\n')
|
|
if (markdown.startsWith('---\n')) return markdown.replace('---\n', `---\n${fields}\n`)
|
|
return `---\n${fields}\n---\n\n${markdown}`
|
|
}
|
|
|
|
/**
|
|
* Select the Markdown rendered for one published page.
|
|
*
|
|
* @param markdown Rewritten canonical Markdown content.
|
|
* @param page Publication manifest entry for the content.
|
|
* @returns Full Markdown for ordinary pages or frontmatter-only Markdown for a locale home page.
|
|
*/
|
|
export function projectedPageContent(markdown: string, page: DocsPage): string {
|
|
if (page.sidebar !== null) return markdown
|
|
if (!markdown.startsWith('---\n')) {
|
|
throw new Error(`project-doc-site: locale home source ${JSON.stringify(page.source)} must start with YAML frontmatter.`)
|
|
}
|
|
const closingDelimiter = '\n---\n'
|
|
const closing = markdown.indexOf(closingDelimiter, 4)
|
|
if (closing === -1) {
|
|
throw new Error(`project-doc-site: locale home source ${JSON.stringify(page.source)} has unclosed YAML frontmatter.`)
|
|
}
|
|
return markdown.slice(0, closing + closingDelimiter.length)
|
|
}
|
|
|
|
/** Canonical Markdown files watched by the local VitePress dev server. */
|
|
export function docsSourceFiles(): string[] {
|
|
return [...new Set(docsPages.map(page => resolve(root, page.source)))]
|
|
}
|
|
|
|
/** Rebuild the disposable VitePress source tree from the publication manifest. */
|
|
export function projectDocs(): void {
|
|
const routes = new Set<string>()
|
|
/** Projected asset path to the source it came from, for collision detection. */
|
|
const assets = new Map<string, string>()
|
|
const repositoryRef = process.env.GITHUB_SHA ?? 'master'
|
|
rmSync(generatedRoot, { recursive: true, force: true })
|
|
|
|
for (const page of docsPages) {
|
|
if (routes.has(page.route)) throw new Error(`project-doc-site: duplicate route ${JSON.stringify(page.route)}.`)
|
|
routes.add(page.route)
|
|
const sourceAbs = resolve(root, page.source)
|
|
if (!existsSync(sourceAbs) || !lstatSync(sourceAbs).isFile()) {
|
|
throw new Error(`project-doc-site: source ${JSON.stringify(page.source)} does not exist or is not a file.`)
|
|
}
|
|
const output = resolve(generatedRoot, page.route)
|
|
mkdirSync(dirname(output), { recursive: true })
|
|
const markdown = readFileSync(sourceAbs, 'utf8')
|
|
const projected = rewriteMarkdown(markdown, {
|
|
sourcePath: page.source,
|
|
locale: page.locale,
|
|
route: page.route,
|
|
pages: docsPages,
|
|
repoRoot: root,
|
|
repositoryRef,
|
|
placeImage: (absPath) => {
|
|
// Beside the page that references it, under its own basename: each
|
|
// locale's route tree gets its own copy, so one relative URL is correct
|
|
// from both. Two sources that would land on one name are a collision
|
|
// rather than a silent overwrite of whichever copied last.
|
|
const name = basename(absPath)
|
|
const target = resolve(dirname(output), name)
|
|
const claimed = assets.get(target)
|
|
if (claimed !== undefined && claimed !== absPath) {
|
|
throw new Error(
|
|
`project-doc-site: ${repoPath(absPath, root)} and ${repoPath(claimed, root)}`
|
|
+ ` both project to ${relative(generatedRoot, target).split(sep).join('/')}.`,
|
|
)
|
|
}
|
|
assets.set(target, absPath)
|
|
copyFileSync(absPath, target)
|
|
return `./${name}`
|
|
},
|
|
})
|
|
writeFileSync(output, addProjectionFrontmatter(projectedPageContent(projected, page), page))
|
|
}
|
|
}
|