mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(scripts): let the documentation site carry its own images
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: 配置模型.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-06-doc-site-carries-its-images.md
|
||||
2026-08-06-doc-site-carries-its-images.md: 21593c2cadb6b2aaf52350ab61156ad892bc4163
|
||||
2026-08-06-doc-site-carries-its-images.zh.md: 54aee878a9d0f16d1fe3b219da7b248fb5148fa3
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: The documentation site carries its own images
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-06-doc-site-carries-its-images.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`scripts/project-doc-site.ts` rewrote every repository-relative target that the publication manifest does not publish into a GitHub URL, and for an image that meant `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`. Nothing in the site build copies files: `srcDir` is the disposable `.generated` tree, VitePress sets no `publicDir` (its default, `<srcDir>/public`, is inside the tree the projector deletes on every run), and only Markdown is written there.
|
||||
|
||||
That works only for a public repository. This one is private, and `raw.githubusercontent.com` answers 404 to an unauthenticated request — a browser session on github.com does not authenticate it either, since GitHub's own UI serves private blobs through separately signed URLs. Every image on the site was therefore broken for every reader, and no gate said so: `verify-md-links` and the projector check that the target file *exists in the repository*, which is a different question from whether a site reader can fetch it.
|
||||
|
||||
## Decision
|
||||
|
||||
`rewriteMarkdown` takes an optional `placeImage(absPath): string`. When a page references an image the manifest does not publish as a page, the projector copies that file into the generated tree beside the page and rewrites the reference to `./<basename>`; Vite then bundles it like any other site asset. Nothing about repository visibility can reach the published page.
|
||||
|
||||
The copy lands beside the page rather than in a shared asset directory. Each locale's route tree gets its own copy, so one relative URL is correct from both `guide/` and `en/guide/` without computing per-locale prefixes, and a page's assets are removed with the page when the manifest drops it. Two sources that would project onto one path throw, in the same spirit as the existing duplicate-route check, rather than letting whichever copied last win.
|
||||
|
||||
`placeImage` is optional because `rewriteMarkdown` is also called directly by its spec, where no generated tree exists. Without it the old GitHub-raw behavior stands, which keeps that seam honest: the fallback is still the correct answer for a consumer that only rewrites text.
|
||||
|
||||
Canonical Markdown keeps writing ordinary repository-relative image paths, so the same file renders on GitHub and on the site. No document carries a site-absolute URL to satisfy VitePress.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Set `publicDir` outside `.generated` and reference site-absolute URLs.** Fewer moving parts in the projector, but every image reference would then be broken when the same Markdown is read in the repository, and canonical docs are read both ways.
|
||||
|
||||
**Host images on the assets branch, as demo GIFs already are.** That branch exists to keep large binaries out of the main history, and its raw URLs have exactly the same visibility problem. It remains the right home for recordings; it does not solve this.
|
||||
|
||||
**Wait for the repository to become public.** It would fix the symptom without making the site self-contained, and the site would silently depend on GitHub's availability and rate limits for every image.
|
||||
|
||||
## Consequences
|
||||
|
||||
Images in published documentation now work regardless of who is reading or whether the repository is public, and the site build has no runtime dependency on GitHub for them. The generated tree grows by one copy of each referenced image per locale — the four screenshots in the model-provider guide add roughly 270 KB per locale.
|
||||
|
||||
Images referenced from *unpublished* documents are untouched: they still resolve to GitHub raw, and still fail for a private repository. Nothing consumes them today, and a document that is not on the site has no site build to carry its assets.
|
||||
|
||||
## Testing
|
||||
|
||||
`scripts/project-doc-site.spec.ts` covers the placer receiving the resolved absolute path and the returned URL landing in the Markdown, a published page link still resolving to its route when a placer is present, and the unchanged GitHub-raw fallback when no placer is supplied. `pnpm docs:check` builds the site with the model-provider guide's screenshots and fails on a missing source; the copied files and their `./<basename>` references were verified in `website/.generated` and in a running `docs:dev` (`naturalWidth > 0` in both locales).
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: 文档站点自带图片
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-06-doc-site-carries-its-images.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`scripts/project-doc-site.ts` 会把发布清单未收录的仓库相对目标一律改写成 GitHub 地址,对图片而言就是 `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`。站点构建不拷贝任何文件:`srcDir` 是用完即弃的 `.generated` 树,VitePress 没有设置 `publicDir`(其默认值 `<srcDir>/public` 恰好位于投影每次运行时删除的那棵树里),而写进去的只有 Markdown。
|
||||
|
||||
这只对公开仓库成立。本仓库是私有的,而 `raw.githubusercontent.com` 对未认证请求一律回 404——github.com 上的登录会话也不能认证它,因为 GitHub 自家界面是用另一套单独签名的地址提供私有 blob 的。于是站点上的每一张图片对每一位读者都是坏的,却没有任何门禁能说出来:`verify-md-links` 与投影校验的是目标文件**在仓库里是否存在**,那与站点读者能否取到它是两个问题。
|
||||
|
||||
## Decision
|
||||
|
||||
`rewriteMarkdown` 新增可选的 `placeImage(absPath): string`。当页面引用了一张清单未作为页面发布的图片时,投影把该文件复制进生成树中该页面的旁边,并把引用改写为 `./<basename>`;随后 Vite 会像处理其他站点资源一样打包它。仓库可见性再也影响不到已发布页面。
|
||||
|
||||
副本落在页面旁边,而不是某个共享资源目录。每个 locale 的路由树各持一份副本,因此同一个相对 URL 在 `guide/` 与 `en/guide/` 下都正确,无需按 locale 计算前缀;清单撤下某页时,它的资源也随之消失。两个来源若会投影到同一路径则抛错——与既有的重复路由检查同一个立场——而不是让最后拷贝的那个静默胜出。
|
||||
|
||||
`placeImage` 之所以可选,是因为 `rewriteMarkdown` 也被它自己的 spec 直接调用,而那里并不存在生成树。不传它时保持原有的 GitHub raw 行为,这也让该接缝保持诚实:对只改写文本的消费方而言,这个回退仍是正确答案。
|
||||
|
||||
正本 Markdown 照旧写普通的仓库相对图片路径,因此同一份文件在 GitHub 上和站点上都能正常显示。没有任何文档为了迁就 VitePress 而写站内绝对 URL。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把 `publicDir` 设到 `.generated` 之外,并使用站内绝对 URL。** 投影这边的活动部件更少,但同一份 Markdown 在仓库中阅读时,每一处图片引用都会是坏的,而正本文档是两种方式都要读的。
|
||||
|
||||
**把图片放到 assets 分支,就像演示 GIF 那样。** 那个分支的存在是为了让大体积二进制不进主线历史,而它的 raw 地址有着完全相同的可见性问题。它仍然是录屏的正确归宿;但它解决不了这件事。
|
||||
|
||||
**等仓库转为公开。** 那只是消除症状,不会让站点自给自足,而且每一张图片都会让站点隐式依赖 GitHub 的可用性与限流。
|
||||
|
||||
## Consequences
|
||||
|
||||
已发布文档中的图片,现在无论谁在阅读、无论仓库是否公开都能显示,站点构建也不再为图片依赖 GitHub 的运行时可达性。生成树会为每个 locale 各增加一份被引用图片的副本——配置模型指南里的四张截图,每个 locale 约 270 KB。
|
||||
|
||||
**未发布**文档引用的图片不受影响:它们仍解析到 GitHub raw,对私有仓库仍然失败。今天没有任何消费方用到它们,而不在站点上的文档也没有站点构建可以承载其资源。
|
||||
|
||||
## Testing
|
||||
|
||||
`scripts/project-doc-site.spec.ts` 覆盖:placer 收到解析后的绝对路径且其返回的 URL 落进 Markdown、存在 placer 时已发布页面的链接仍解析到自己的路由、以及不传 placer 时不变的 GitHub raw 回退。`pnpm docs:check` 会带着配置模型指南的截图构建站点,并在来源缺失时失败;被拷贝的文件及其 `./<basename>` 引用已在 `website/.generated` 与运行中的 `docs:dev` 里核实(两个 locale 均 `naturalWidth > 0`)。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/guide/providers.md
|
||||
providers.md: c555e62d5343ccc758c0c6e699d30ffec229f2df
|
||||
providers.zh.md: 6c4154c86db3d95c6b083519533954fc4cc90e45
|
||||
providers.md: d96cab0fa09583d81d98863169819fdd78d636e7
|
||||
providers.zh.md: d413fec2f9d703e31e82e50fcbe83b24bd58ee39
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Configure model providers
|
||||
# Configure models
|
||||
|
||||
English | [中文](providers.zh.md)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 配置模型提供方
|
||||
# 配置模型
|
||||
|
||||
[English](providers.md) | 中文
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/user/guide/quickstart.md
|
||||
quickstart.md: 8a9ed716d9395448aadfb97d0935bd42ee06e6c1
|
||||
quickstart.zh.md: 3652b0453f870640b278ce6f1355e67e85983ffe
|
||||
quickstart.md: e81e0ff57384156ee2963d4788519d2384c362ea
|
||||
quickstart.zh.md: 9755da8bf078c817f9c2a00134360b5169c536bf
|
||||
|
||||
@@ -57,6 +57,6 @@ headless-agent uses the `@deepseek-ai/dsh-cli-demo` app. `dsh web` instead compo
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Model providers](./providers.md) — reach providers beyond DeepSeek, and custom gateways
|
||||
- [Configure models](./providers.md) — reach providers beyond DeepSeek, and custom gateways
|
||||
- [Configuration](./config.md) — understand the `cordis.yml` format
|
||||
- [Develop a plugin](../develop/basic/) — build your own tool or backend
|
||||
|
||||
@@ -57,6 +57,6 @@ headless-agent 使用 `@deepseek-ai/dsh-cli-demo` app。`dsh web` 则组合 [`ap
|
||||
|
||||
## 下一步
|
||||
|
||||
- [配置模型提供方](./providers.md) — 接入 DeepSeek 之外的提供方与自定义网关
|
||||
- [配置模型](./providers.md) — 接入 DeepSeek 之外的提供方与自定义网关
|
||||
- [配置文件](./config.md) — 了解 `cordis.yml` 的格式
|
||||
- [开发插件](../develop/basic/) — 编写自己的 tool 或后端
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('rewriteMarkdown', () => {
|
||||
})).toBe('[B](./reference-root/b.md)\n')
|
||||
})
|
||||
|
||||
it('uses raw GitHub content for unpublished images', () => {
|
||||
it('uses raw GitHub content for unpublished images when nothing places them', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('\n', {
|
||||
locale: 'en',
|
||||
@@ -105,6 +105,39 @@ describe('rewriteMarkdown', () => {
|
||||
})).toBe('\n')
|
||||
})
|
||||
|
||||
it('hands an image to the placer and uses the URL it returns', () => {
|
||||
// A raw GitHub URL cannot serve a private repository, so the site build
|
||||
// carries images itself; the placer is what puts them there.
|
||||
const { root, pages } = fixture()
|
||||
const placed: string[] = []
|
||||
expect(rewriteMarkdown('\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: (absPath) => {
|
||||
placed.push(absPath.split('/').pop() ?? '')
|
||||
return './logo.svg'
|
||||
},
|
||||
})).toBe('\n')
|
||||
expect(placed).toEqual(['logo.svg'])
|
||||
})
|
||||
|
||||
it('leaves a published page link to the route even when a placer exists', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('[B](b.md)\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: () => { throw new Error('a page link must not be placed as an asset') },
|
||||
})).toBe('[B](./reference/b.md)\n')
|
||||
})
|
||||
|
||||
it('does not rewrite Markdown-looking text inside code fences', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '```md\n[B](b.md)\n```\n'
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* tier, while this adapter rewrites cross-source links for the public site.
|
||||
*/
|
||||
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, extname, posix, relative, resolve, sep } from 'node:path'
|
||||
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'
|
||||
@@ -38,6 +38,15 @@ export interface RewriteMarkdownOptions {
|
||||
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 {
|
||||
@@ -222,9 +231,11 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
|
||||
? options.locale === 'root' ? 'en' : 'root'
|
||||
: options.locale
|
||||
const page = published.get(targetPath)?.get(targetLocale)
|
||||
const nextUrl = page === undefined
|
||||
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
|
||||
: routeTarget(options.route, page.route, suffix)
|
||||
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
|
||||
@@ -299,6 +310,8 @@ export function docsSourceFiles(): string[] {
|
||||
/** 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 })
|
||||
|
||||
@@ -319,6 +332,24 @@ export function projectDocs(): void {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ const homeAndGuide = pairedPages([
|
||||
{
|
||||
source: 'docs/user/guide/providers.md',
|
||||
route: 'guide/providers.md',
|
||||
label: { root: '配置模型提供方', en: 'Model providers' },
|
||||
label: { root: '配置模型', en: 'Configure models' },
|
||||
sidebar: { root: 'zh-guide', en: 'en-guide' },
|
||||
section: { root: '入门', en: 'Guide' },
|
||||
order: 3,
|
||||
|
||||
Reference in New Issue
Block a user