Forráskód Böngészése

fix(docs): address documentation site review

Yichen Jiang 3 hónapja
szülő
commit
6be219a0bc

+ 9 - 8
docs/user/zh-CN/develop/basic/config.md

@@ -4,10 +4,11 @@
 
 ## 定义 Config 类型
 
-在插件中导出一个 `Config` 类型和可选的默认值:
+在插件中导出一个 `Config` 类型和同名的 Schemastery schema;默认值直接写在 schema 中:
 
 ```typescript
 import type { Context } from 'cordis'
+import Schema from 'schemastery'
 
 export const name = 'my-plugin'
 
@@ -17,11 +18,11 @@ export interface Config {
   verbose?: boolean
 }
 
-export const Config = {
-  greeting: 'Hello',
-  maxRetries: 3,
-  verbose: false,
-}
+export const Config: Schema<Config> = Schema.object({
+  greeting: Schema.string().default('Hello'),
+  maxRetries: Schema.number().default(3),
+  verbose: Schema.boolean().default(false),
+})
 
 export function apply(ctx: Context, config: Config) {
   console.log(config.greeting)  // 用户配置或默认值
@@ -37,7 +38,7 @@ export function apply(ctx: Context, config: Config) {
     maxRetries: 5
 ```
 
-未提供的字段使用导出的 `Config` 对象中的默认值。
+插件加载时,Cordis 会通过导出的 schema 校验配置,并填充未提供字段的默认值。不要导出普通对象作为 `Config`,因为它不满足 Cordis 要求的 Standard Schema 接口。
 
 ## Schema 校验
 
@@ -92,7 +93,7 @@ export interface Config {
 
 ```typescript
 export function apply(ctx: Context, config: Config) {
-  if (!ctx.llm.hasAdapter(config.model)) {
+  if (!ctx.llm.models().includes(config.model)) {
     throw new Error(`Model "${config.model}" is not registered by any LLM adapter`)
   }
 }

+ 4 - 9
docs/user/zh-CN/develop/basic/index.md

@@ -28,10 +28,8 @@ import type { Context } from 'cordis'
 export const name = 'hello-plugin'
 
 export function apply(ctx: Context) {
-  // 监听 agent-loop 的 ready 事件
-  ctx.on('ready', () => {
-    console.log('[hello-plugin] 插件已加载!')
-  })
+  // apply 被调用时,插件的必选依赖已就绪
+  console.log('[hello-plugin] 插件已加载!')
 }
 ```
 
@@ -100,17 +98,14 @@ export default {
 ### 类形式
 
 ```typescript
-import { Service } from 'cordis'
+import { Service, type Context } from 'cordis'
 
 export default class MyService extends Service {
   static inject = ['tools']
 
   constructor(ctx: Context) {
     super(ctx, 'myService')
-  }
-
-  start() {
-    // 服务启动逻辑
+    // 构造函数内完成同步初始化
   }
 }
 ```

+ 5 - 7
docs/user/zh-CN/develop/basic/tool.md

@@ -133,14 +133,14 @@ defineTool({
   // ...
   presentCall(args) {
     return {
-      intent: 'terminal',
-      title: `bash(${JSON.stringify(args.command).slice(0, 60)})`,
+      card: 'terminal',
+      title: args.command,
     }
   },
   presentResult(args, result) {
     return {
-      intent: 'terminal',
-      body: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
+      card: 'terminal',
+      output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
     }
   },
 })
@@ -156,9 +156,7 @@ defineTool({
 // 这样就够了:
 ctx.tools.register(defineTool({ /* ... */ }))
 
-// 不需要:
-// const dispose = ctx.tools.register(...)
-// ctx.on('dispose', dispose)
+// 不需要额外保存 disposer 或注册清理逻辑
 ```
 
 ## 完整实战示例

+ 18 - 32
docs/user/zh-CN/develop/framework/events.md

@@ -24,15 +24,15 @@ Cordis 提供多种事件触发模式,适用于不同场景:
 
 ### emit — 广播
 
-所有监听器并行执行,不关心返回值:
+所有监听器同步执行,不关心返回值:
 
 ```typescript
 // 触发
-ctx.emit('agent/turn-end', { agentId, turnIndex })
+ctx.emit('my-plugin/ready', { id: 'worker-1' })
 
 // 监听
-ctx.on('agent/turn-end', ({ agentId, turnIndex }) => {
-  console.log(`Turn ${turnIndex} ended`)
+ctx.on('my-plugin/ready', ({ id }) => {
+  console.log(`${id} is ready`)
 })
 ```
 
@@ -53,7 +53,7 @@ ctx.on('some-check', (input) => {
 
 ### serial — 顺序执行
 
-所有监听器按注册顺序依次执行(异步安全):
+监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行:
 
 ```typescript
 await ctx.serial('setup-phase', context)
@@ -61,18 +61,16 @@ await ctx.serial('setup-phase', context)
 
 ### waterfall — 管道
 
-每个监听器接收前一个的输出,形成数据管道。**必须调用 `next()` 传递给下游**,不调用即为否决:
+每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决:
 
 ```typescript
 // 触发
-const finalMessages = await ctx.waterfall('llm/pre-request', messages)
+const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
 
 // 监听(必须调用 next)
-ctx.on('llm/pre-request', async (messages, next) => {
-  // 可以修改 messages
-  messages.push(extraMessage)
-  // 必须调用 next() 传递给下一个监听器
-  return next(messages)
+ctx.on('my-plugin/transform', async (_input, next) => {
+  const downstream = await next()
+  return downstream.trim()
 })
 ```
 
@@ -89,6 +87,7 @@ declare module 'cordis' {
   interface Events {
     'my-plugin/ready': (payload: { id: string }) => void
     'my-plugin/check': (input: string) => boolean | undefined
+    'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
   }
 }
 
@@ -96,20 +95,11 @@ declare module 'cordis' {
 // 都有正确的类型推导
 ```
 
-## 命名约定
+## Cordis 事件与会话记录
 
-Harness 事件遵循 `namespace/action` 命名:
+Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/step-result`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../../cordis-catalog/events.md)。
 
-```
-agent/pre-step      — agent 执行一步之前
-agent/post-step     — agent 执行一步之后
-tool/call           — tool 被调用
-tool/result         — tool 返回结果
-llm/pre-request     — LLM 请求发送前
-session/event       — 会话事件被记录
-compact/start       — 压缩开始
-compact/end         — 压缩结束
-```
+`turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。
 
 ## 事件也是效果
 
@@ -118,7 +108,7 @@ compact/end         — 压缩结束
 ```typescript
 export function apply(ctx: Context) {
   // 这个监听器在插件 dispose 时自动清理
-  ctx.on('agent/turn-end', handler)
+  ctx.on('tools/result', handler)
 }
 ```
 
@@ -132,14 +122,10 @@ import type { Context } from 'cordis'
 export const name = 'tool-logger'
 
 export function apply(ctx: Context) {
-  ctx.on('tool/call', ({ name, args }) => {
-    console.log(`[tool] ${name}(${JSON.stringify(args)})`)
-  })
-
-  ctx.on('tool/result', ({ name, result }) => {
+  ctx.on('tools/result', (exec, result) => {
+    console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
     const text = result.content
-      .filter(b => b.type === 'text')
-      .map(b => b.text)
+      .map(block => block.type === 'text' ? block.text : '')
       .join('')
     console.log(`[tool result] ${text.slice(0, 100)}`)
   })

+ 1 - 11
docs/user/zh-CN/develop/framework/index.md

@@ -105,14 +105,6 @@ fiber.dispose()
 export function apply(ctx: Context) {
   console.log('plugin loading')
 
-  ctx.on('ready', () => {
-    console.log('context ready')
-  })
-
-  ctx.on('dispose', () => {
-    console.log('plugin disposing')
-  })
-
   ctx.effect(() => {
     console.log('effect registered')
     return () => console.log('effect cleaned up')
@@ -124,12 +116,10 @@ export function apply(ctx: Context) {
 ```
 plugin loading
 effect registered
-context ready
 ```
 
-卸载时输出(逆序):
+卸载时输出:
 ```
-plugin disposing
 effect cleaned up
 ```
 

+ 13 - 4
docs/user/zh-CN/develop/framework/service.md

@@ -90,8 +90,11 @@ export default class MetricsService extends Service {
 // 必选:服务不存在时,插件不会加载
 export const inject = ['tools']
 
-// 可选:服务不存在时,插件仍然加载,但 ctx.xxx 可能是 undefined
-export const inject = { optional: ['metrics'] }
+// 可选:不写入 inject,使用时通过 ctx.get() 查询
+export function apply(ctx: Context) {
+  const metrics = ctx.get('metrics')
+  metrics?.record('plugin_loaded', 1)
+}
 ```
 
 ### 服务消失时的行为
@@ -109,7 +112,10 @@ export const inject = { optional: ['metrics'] }
 
 ```yaml
 - id: group-a
-  name: 'group:'
+  name: '@cordisjs/plugin-group'
+  group: true
+  isolate:
+    bash: true
   config:
     - name: '@deepseek-ai/dsh-bash-local'
       config:
@@ -117,7 +123,10 @@ export const inject = { optional: ['metrics'] }
     - name: './src/plugin-a.ts'
 
 - id: group-b
-  name: 'group:'
+  name: '@cordisjs/plugin-group'
+  group: true
+  isolate:
+    bash: true
   config:
     - name: '@deepseek-ai/dsh-bash-local'
       config:

+ 6 - 1
docs/user/zh-CN/develop/practice/llm-adapter.md

@@ -114,6 +114,8 @@ interface GenerateOptions {
   maxTokens?: number
   /** 温度 */
   temperature?: number
+  /** 取消或卸载时中止进行中的请求 */
+  signal?: AbortSignal
 }
 ```
 
@@ -160,7 +162,10 @@ mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地
 
 ```typescript
 async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
-  const response = await fetch(this.endpoint, { /* ... */ })
+  const response = await fetch(this.endpoint, {
+    // ...method、headers 和 body
+    signal: options.signal,
+  })
   if (!response.ok) {
     throw new Error(`API error: ${response.status}`)
   }

+ 1 - 1
docs/user/zh-CN/index.md

@@ -13,7 +13,7 @@ hero:
       link: /develop/basic/
 features:
   - title: 插件化架构
-    details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。
+    details: 基于 Cordis 插件系统,所有能力通过插件注册,加载即生效、卸载即还原。
   - title: 配置即组合
     details: 一个 cordis.yml 决定整个 Agent 的能力组合——换模型、加工具,只需改一行配置。
   - title: 开箱即用

+ 34 - 3
scripts/project-doc-site.spec.ts

@@ -4,7 +4,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { afterEach, describe, expect, it } from 'vitest'
-import type { DocsPage } from '../website/docs.ts'
+import { docsPages, type DocsPage } from '../website/docs.ts'
 import { addProjectionFrontmatter, rewriteMarkdown } from './project-doc-site.ts'
 
 const roots: string[] = []
@@ -25,8 +25,10 @@ function fixture(): { root: string; pages: DocsPage[] } {
   return {
     root,
     pages: [
-      { source: 'docs/a.md', route: 'en/a.md', label: 'A', sidebar: 'en-docs', section: 'Test', order: 1 },
-      { source: 'docs/b.md', route: 'en/reference/b.md', label: 'B', sidebar: 'en-docs', section: 'Test', order: 2 },
+      { locale: 'root', contentLocale: 'en-US', source: 'docs/a.md', route: 'a.md', label: 'A', sidebar: 'zh-reference', section: 'Test', order: 1 },
+      { locale: 'root', contentLocale: 'en-US', source: 'docs/b.md', route: 'reference-root/b.md', label: 'B', sidebar: 'zh-reference', section: 'Test', order: 2 },
+      { locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', route: 'en/a.md', label: 'A', sidebar: 'en-reference', section: 'Test', order: 1 },
+      { locale: 'en', contentLocale: 'en-US', source: 'docs/b.md', route: 'en/reference/b.md', label: 'B', sidebar: 'en-reference', section: 'Test', order: 2 },
     ],
   }
 }
@@ -36,6 +38,7 @@ describe('rewriteMarkdown', () => {
     const { root, pages } = fixture()
     const source = '[B](b.md#part) [source](../packages/tool.ts:2) [web](https://example.com)\n'
     expect(rewriteMarkdown(source, {
+      locale: 'en',
       sourcePath: 'docs/a.md',
       route: 'en/a.md',
       pages,
@@ -48,9 +51,22 @@ describe('rewriteMarkdown', () => {
     )
   })
 
+  it('selects the published target in the current site locale', () => {
+    const { root, pages } = fixture()
+    expect(rewriteMarkdown('[B](b.md)\n', {
+      locale: 'root',
+      sourcePath: 'docs/a.md',
+      route: 'a.md',
+      pages,
+      repoRoot: root,
+      repositoryRef: 'abc123',
+    })).toBe('[B](./reference-root/b.md)\n')
+  })
+
   it('uses raw GitHub content for unpublished images', () => {
     const { root, pages } = fixture()
     expect(rewriteMarkdown('![logo](../packages/logo.svg)\n', {
+      locale: 'en',
       sourcePath: 'docs/a.md',
       route: 'en/a.md',
       pages,
@@ -63,6 +79,7 @@ describe('rewriteMarkdown', () => {
     const { root, pages } = fixture()
     const source = '```md\n[B](b.md)\n```\n'
     expect(rewriteMarkdown(source, {
+      locale: 'en',
       sourcePath: 'docs/a.md',
       route: 'en/a.md',
       pages,
@@ -74,6 +91,7 @@ describe('rewriteMarkdown', () => {
   it('fails loud when a relative target is missing', () => {
     const { root, pages } = fixture()
     expect(() => rewriteMarkdown('[missing](missing.md)\n', {
+      locale: 'en',
       sourcePath: 'docs/a.md',
       route: 'en/a.md',
       pages,
@@ -83,6 +101,19 @@ describe('rewriteMarkdown', () => {
   })
 })
 
+describe('docsPages locale routes', () => {
+  it('publishes the same canonical source at every corresponding locale route', () => {
+    const byRoute = new Map(docsPages.map(page => [page.route, page]))
+    for (const page of docsPages.filter(page => page.locale === 'root')) {
+      const counterpart = byRoute.get(`en/${page.route}`)
+      expect(counterpart, page.route).toBeDefined()
+      expect(counterpart?.locale).toBe('en')
+      expect(counterpart?.source).toBe(page.source)
+      expect(counterpart?.contentLocale).toBe(page.contentLocale)
+    }
+  })
+})
+
 describe('addProjectionFrontmatter', () => {
   it('adds frontmatter to an ordinary Markdown page', () => {
     expect(addProjectionFrontmatter('# Guide\n', 'docs/guide.md')).toBe(

+ 11 - 7
scripts/project-doc-site.ts

@@ -11,7 +11,7 @@ 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 DocsPage } from '../website/docs.ts'
+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, '..')
@@ -25,6 +25,7 @@ interface Replacement {
 
 /** Inputs for rewriting one canonical Markdown page. */
 export interface RewriteMarkdownOptions {
+  locale: DocsLocale
   sourcePath: string
   route: string
   pages: DocsPage[]
@@ -62,14 +63,16 @@ function routeTarget(fromRoute: string, toRoute: string, suffix: string): string
   return `${target.startsWith('.') ? target : `./${target}`}${suffix}`
 }
 
-function sourceMap(pages: DocsPage[]): Map<string, DocsPage> {
-  const map = new Map<string, DocsPage>()
+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 ?? [])]) {
-      if (map.has(source)) {
-        throw new Error(`project-doc-site: duplicate source or alias ${JSON.stringify(source)}.`)
+      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)}.`)
       }
-      map.set(source, page)
+      localized.set(page.locale, page)
+      map.set(source, localized)
     }
   }
   return map
@@ -132,7 +135,7 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
     if (path === '') return
     const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot)
     const targetPath = repoPath(absPath, options.repoRoot)
-    const page = published.get(targetPath)
+    const page = published.get(targetPath)?.get(options.locale)
     const nextUrl = page === undefined
       ? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
       : routeTarget(options.route, page.route, suffix)
@@ -205,6 +208,7 @@ export function projectDocs(): void {
     const markdown = readFileSync(sourceAbs, 'utf8')
     const projected = rewriteMarkdown(markdown, {
       sourcePath: page.source,
+      locale: page.locale,
       route: page.route,
       pages: docsPages,
       repoRoot: root,

+ 54 - 7
website/.vitepress/config.ts

@@ -13,6 +13,14 @@ const sectionOrder = [
   '基础',
   '框架能力',
   '实战',
+  '概念',
+  '生成参考',
+  '数据结构',
+  '开发手册',
+  'Guide',
+  'Basics',
+  'Framework',
+  'Practice',
   'Concepts',
   'Generated reference',
   'Data structures',
@@ -20,7 +28,7 @@ const sectionOrder = [
 ]
 
 function sidebar(collection: DocsPage['sidebar']): DefaultTheme.SidebarItem[] {
-  const pages = docsPages.filter(page => page.sidebar === collection && page.route !== 'index.md')
+  const pages = docsPages.filter(page => page.sidebar === collection)
   const sections = new Map<string, DocsPage[]>()
   for (const page of pages) {
     const entries = sections.get(page.section) ?? []
@@ -51,7 +59,36 @@ function escapeVueInterpolation(html: string): string {
 }
 
 const sharedTheme: Pick<DefaultTheme.Config, 'search' | 'socialLinks' | 'editLink'> = {
-  search: { provider: 'local' },
+  search: {
+    provider: 'local',
+    options: {
+      locales: {
+        root: {
+          translations: {
+            button: {
+              buttonText: '搜索文档',
+              buttonAriaLabel: '搜索文档',
+            },
+            modal: {
+              displayDetails: '显示详细列表',
+              resetButtonTitle: '清除搜索',
+              backButtonTitle: '关闭搜索',
+              noResultsText: '未找到相关结果',
+              footer: {
+                selectText: '选择',
+                selectKeyAriaLabel: '回车键',
+                navigateText: '切换',
+                navigateUpKeyAriaLabel: '上方向键',
+                navigateDownKeyAriaLabel: '下方向键',
+                closeText: '关闭',
+                closeKeyAriaLabel: 'Esc 键',
+              },
+            },
+          },
+        },
+      },
+    },
+  },
   socialLinks: [
     { icon: 'github', link: 'https://github.com/deepseek-harness/deepseek-harness' },
   ],
@@ -81,14 +118,22 @@ export default withMermaid({
         nav: [
           { text: '入门', link: '/guide/', activeMatch: '^/guide/' },
           { text: '开发', link: '/develop/basic/', activeMatch: '^/develop/' },
-          { text: 'Reference', link: '/en/', activeMatch: '^/en/' },
+          { text: '参考', link: '/reference/', activeMatch: '^/reference/' },
         ],
         sidebar: {
           '/guide/': sidebar('zh-guide'),
           '/develop/': sidebar('zh-develop'),
+          '/reference/': sidebar('zh-reference'),
         },
         outline: { label: '本页目录' },
         docFooter: { prev: '上一篇', next: '下一篇' },
+        darkModeSwitchLabel: '外观',
+        lightModeSwitchTitle: '切换到浅色主题',
+        darkModeSwitchTitle: '切换到深色主题',
+        sidebarMenuLabel: '菜单',
+        returnToTopLabel: '返回顶部',
+        langMenuLabel: '切换语言',
+        skipToContentLabel: '跳至内容',
       },
     },
     en: {
@@ -97,12 +142,14 @@ export default withMermaid({
       link: '/en/',
       themeConfig: {
         nav: [
-          { text: 'Concepts', link: '/en/' },
-          { text: 'Reference', link: '/en/config-catalog' },
-          { text: '中文指南', link: '/guide/' },
+          { text: 'Guide', link: '/en/guide/', activeMatch: '^/en/guide/' },
+          { text: 'Develop', link: '/en/develop/basic/', activeMatch: '^/en/develop/' },
+          { text: 'Reference', link: '/en/reference/', activeMatch: '^/en/reference/' },
         ],
         sidebar: {
-          '/en/': sidebar('en-docs'),
+          '/en/guide/': sidebar('en-guide'),
+          '/en/develop/': sidebar('en-develop'),
+          '/en/reference/': sidebar('en-reference'),
         },
         editLink: {
           pattern: ({ frontmatter }: PageData) => {

+ 177 - 122
website/docs.ts

@@ -1,20 +1,38 @@
 /**
  * Canonical publication manifest for the documentation website.
  *
- * Markdown stays in its owning repository tier. This manifest only maps a
- * source file to its public route and navigation placement.
+ * Markdown stays in its owning repository tier. This manifest maps each
+ * canonical source into matching route trees for both site locales; when a
+ * translation is absent, both routes intentionally project the available
+ * source instead of copying Markdown.
  */
 
+/** Locale key used by the VitePress site. */
+export type DocsLocale = 'root' | 'en'
+
+/** Sidebar collection rendered for one locale and top-level module. */
+type DocsSidebar =
+  | 'zh-guide'
+  | 'zh-develop'
+  | 'zh-reference'
+  | 'en-guide'
+  | 'en-develop'
+  | 'en-reference'
+
 /** A page projected into the VitePress source tree. */
 export interface DocsPage {
+  /** VitePress locale whose route tree owns this projection. */
+  locale: DocsLocale
+  /** Language of the canonical source currently projected at this route. */
+  contentLocale: 'zh-CN' | 'en-US'
   /** Repository-relative canonical Markdown source. */
   source: string
   /** VitePress route, including the `.md` suffix. */
   route: string
   /** Navigation label shown in the sidebar. */
   label: string
-  /** Sidebar collection that owns the page. */
-  sidebar: 'zh-guide' | 'zh-develop' | 'en-docs'
+  /** Sidebar collection that owns the page, or null for a locale home page. */
+  sidebar: DocsSidebar | null
   /** Section label within the sidebar. */
   section: string
   /** Stable order within the section. */
@@ -23,191 +41,228 @@ export interface DocsPage {
   sourceAliases?: string[]
 }
 
-const zhGuide: DocsPage[] = [
+interface MirroredPage {
+  source: string
+  route: string
+  contentLocale: DocsPage['contentLocale']
+  label: Record<DocsLocale, string>
+  sidebar: Record<DocsLocale, DocsSidebar | null>
+  section: Record<DocsLocale, string>
+  order: number
+  sourceAliases?: string[]
+}
+
+function mirroredPages(pages: MirroredPage[]): DocsPage[] {
+  return pages.flatMap(page => (['root', 'en'] as const).map(locale => ({
+    locale,
+    contentLocale: page.contentLocale,
+    source: page.source,
+    route: locale === 'root' ? page.route : `en/${page.route}`,
+    label: page.label[locale],
+    sidebar: page.sidebar[locale],
+    section: page.section[locale],
+    order: page.order,
+    ...(page.sourceAliases === undefined ? {} : { sourceAliases: page.sourceAliases }),
+  })))
+}
+
+const homeAndGuide = mirroredPages([
   {
     source: 'docs/user/zh-CN/index.md',
     route: 'index.md',
-    label: 'DeepSeek Harness',
-    sidebar: 'zh-guide',
-    section: '入门',
+    contentLocale: 'zh-CN',
+    label: { root: 'DeepSeek Harness', en: 'DeepSeek Harness' },
+    sidebar: { root: null, en: null },
+    section: { root: '首页', en: 'Home' },
     order: 0,
   },
   {
     source: 'docs/user/zh-CN/guide/index.md',
     route: 'guide/index.md',
-    label: '介绍',
-    sidebar: 'zh-guide',
-    section: '入门',
+    contentLocale: 'zh-CN',
+    label: { root: '介绍', en: 'Introduction' },
+    sidebar: { root: 'zh-guide', en: 'en-guide' },
+    section: { root: '入门', en: 'Guide' },
     order: 1,
     sourceAliases: ['docs/user/zh-CN/guide'],
   },
   {
     source: 'docs/user/zh-CN/guide/quickstart.md',
     route: 'guide/quickstart.md',
-    label: '快速开始',
-    sidebar: 'zh-guide',
-    section: '入门',
+    contentLocale: 'zh-CN',
+    label: { root: '快速开始', en: 'Quick start' },
+    sidebar: { root: 'zh-guide', en: 'en-guide' },
+    section: { root: '入门', en: 'Guide' },
     order: 2,
   },
   {
     source: 'docs/user/zh-CN/guide/config.md',
     route: 'guide/config.md',
-    label: '配置文件',
-    sidebar: 'zh-guide',
-    section: '入门',
+    contentLocale: 'zh-CN',
+    label: { root: '配置文件', en: 'Configuration' },
+    sidebar: { root: 'zh-guide', en: 'en-guide' },
+    section: { root: '入门', en: 'Guide' },
     order: 3,
   },
-]
+])
 
-const zhDevelop: DocsPage[] = [
+const develop = mirroredPages([
   {
     source: 'docs/user/zh-CN/develop/basic/index.md',
     route: 'develop/basic/index.md',
-    label: '第一个插件',
-    sidebar: 'zh-develop',
-    section: '基础',
+    contentLocale: 'zh-CN',
+    label: { root: '第一个插件', en: 'First plugin' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '基础', en: 'Basics' },
     order: 1,
     sourceAliases: ['docs/user/zh-CN/develop/basic'],
   },
   {
     source: 'docs/user/zh-CN/develop/basic/tool.md',
     route: 'develop/basic/tool.md',
-    label: '开发一个 Tool',
-    sidebar: 'zh-develop',
-    section: '基础',
+    contentLocale: 'zh-CN',
+    label: { root: '开发一个 Tool', en: 'Build a tool' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '基础', en: 'Basics' },
     order: 2,
   },
   {
     source: 'docs/user/zh-CN/develop/basic/config.md',
     route: 'develop/basic/config.md',
-    label: '插件配置',
-    sidebar: 'zh-develop',
-    section: '基础',
+    contentLocale: 'zh-CN',
+    label: { root: '插件配置', en: 'Plugin configuration' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '基础', en: 'Basics' },
     order: 3,
   },
   {
     source: 'docs/user/zh-CN/develop/framework/index.md',
     route: 'develop/framework/index.md',
-    label: '插件与生命周期',
-    sidebar: 'zh-develop',
-    section: '框架能力',
+    contentLocale: 'zh-CN',
+    label: { root: '插件与生命周期', en: 'Plugin lifecycle' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '框架能力', en: 'Framework' },
     order: 1,
     sourceAliases: ['docs/user/zh-CN/develop/framework'],
   },
   {
     source: 'docs/user/zh-CN/develop/framework/service.md',
     route: 'develop/framework/service.md',
-    label: '服务与依赖',
-    sidebar: 'zh-develop',
-    section: '框架能力',
+    contentLocale: 'zh-CN',
+    label: { root: '服务与依赖', en: 'Services and dependencies' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '框架能力', en: 'Framework' },
     order: 2,
   },
   {
     source: 'docs/user/zh-CN/develop/framework/events.md',
     route: 'develop/framework/events.md',
-    label: '事件系统',
-    sidebar: 'zh-develop',
-    section: '框架能力',
+    contentLocale: 'zh-CN',
+    label: { root: '事件系统', en: 'Event system' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '框架能力', en: 'Framework' },
     order: 3,
   },
   {
     source: 'docs/user/zh-CN/develop/practice/index.md',
     route: 'develop/practice/index.md',
-    label: '能力的三层拆分',
-    sidebar: 'zh-develop',
-    section: '实战',
+    contentLocale: 'zh-CN',
+    label: { root: '能力的三层拆分', en: 'Capability layering' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '实战', en: 'Practice' },
     order: 1,
     sourceAliases: ['docs/user/zh-CN/develop/practice'],
   },
   {
     source: 'docs/user/zh-CN/develop/practice/llm-adapter.md',
     route: 'develop/practice/llm-adapter.md',
-    label: 'LLM 适配器',
-    sidebar: 'zh-develop',
-    section: '实战',
+    contentLocale: 'zh-CN',
+    label: { root: 'LLM 适配器', en: 'LLM adapter' },
+    sidebar: { root: 'zh-develop', en: 'en-develop' },
+    section: { root: '实战', en: 'Practice' },
     order: 2,
   },
-]
-
-const enOverview: DocsPage[] = ([
-  ['docs/architecture.md', 'en/index.md', 'Architecture'],
-  ['docs/cordis-primer.md', 'en/cordis-primer.md', 'Cordis primer'],
-  ['docs/capability-seams.md', 'en/capability-seams.md', 'Capability services'],
-  ['docs/agent-lifecycle.md', 'en/agent-lifecycle.md', 'Agent lifecycle'],
-  ['docs/tool-execution-pipeline.md', 'en/tool-execution-pipeline.md', 'Tool execution'],
-] as const).map(([source, route, label], order) => ({
-  source,
-  route,
-  label,
-  sidebar: 'en-docs',
-  section: 'Concepts',
-  order,
-}))
-
-const enCatalogs: DocsPage[] = ([
-  ['docs/config-catalog.md', 'en/config-catalog.md', 'Plugin configuration'],
-  ['docs/tool-catalog.md', 'en/tool-catalog.md', 'Tool schemas'],
-  ['docs/cordis-catalog/services.md', 'en/cordis-catalog/services.md', 'Services'],
-  ['docs/cordis-catalog/events.md', 'en/cordis-catalog/events.md', 'Events'],
-  ['docs/persistence-catalog.md', 'en/persistence-catalog.md', 'Persistence events'],
-] as const).map(([source, route, label], order) => ({
-  source,
-  route,
-  label,
-  sidebar: 'en-docs',
-  section: 'Generated reference',
-  order,
-}))
-
-const corePages = [
-  ['core.md', 'Core data structures'],
-  ['session.md', 'Sessions'],
-  ['tools.md', 'Tools'],
-  ['llm-streaming.md', 'LLM streaming'],
-  ['bash.md', 'Bash execution'],
-  ['filesystem.md', 'Filesystem'],
-  ['code-runtime.md', 'Code runtime'],
-  ['compaction.md', 'Compaction'],
-  ['subagent.md', 'Subagents'],
-  ['workflow.md', 'Workflows'],
-  ['skills.md', 'Skills'],
-  ['approval.md', 'Approvals'],
-  ['user-interaction.md', 'User interaction'],
-  ['sandbox.md', 'Sandboxing'],
-  ['web.md', 'Web access'],
-  ['persistence.md', 'Session persistence'],
-] as const
-
-const enCore: DocsPage[] = corePages.map(([file, label], order) => ({
-  source: `docs/core-data-structures/${file}`,
-  route: `en/core-data-structures/${file}`,
-  label,
-  sidebar: 'en-docs',
-  section: 'Data structures',
-  order,
-  ...(file === 'core.md' ? { sourceAliases: ['docs/core-data-structures'] } : {}),
-}))
+])
 
-const enCookbook: DocsPage[] = ([
-  ['adding-a-package.md', 'Adding a package'],
-  ['adding-a-tool.md', 'Adding a tool'],
-  ['adding-an-llm-adapter.md', 'Adding an LLM adapter'],
-  ['extension-cookbook.md', 'Extension patterns'],
-] as const).map(([file, label], order) => ({
-  source: `docs/cookbook/${file}`,
-  route: `en/cookbook/${file}`,
-  label,
-  sidebar: 'en-docs',
-  section: 'Cookbook',
-  order,
-}))
+const reference = mirroredPages([
+  ...([
+    ['docs/architecture.md', 'reference/index.md', '架构', 'Architecture'],
+    ['docs/cordis-primer.md', 'reference/cordis-primer.md', 'Cordis 入门', 'Cordis primer'],
+    ['docs/capability-seams.md', 'reference/capability-seams.md', '能力服务', 'Capability services'],
+    ['docs/agent-lifecycle.md', 'reference/agent-lifecycle.md', 'Agent 生命周期', 'Agent lifecycle'],
+    ['docs/tool-execution-pipeline.md', 'reference/tool-execution-pipeline.md', 'Tool 执行', 'Tool execution'],
+  ] as const).map(([source, route, rootLabel, enLabel], order): MirroredPage => ({
+    source,
+    route,
+    contentLocale: 'en-US',
+    label: { root: rootLabel, en: enLabel },
+    sidebar: { root: 'zh-reference', en: 'en-reference' },
+    section: { root: '概念', en: 'Concepts' },
+    order,
+  })),
+  ...([
+    ['docs/config-catalog.md', 'reference/config-catalog.md', '插件配置', 'Plugin configuration'],
+    ['docs/tool-catalog.md', 'reference/tool-catalog.md', 'Tool Schema', 'Tool schemas'],
+    ['docs/cordis-catalog/services.md', 'reference/cordis-catalog/services.md', '服务', 'Services'],
+    ['docs/cordis-catalog/events.md', 'reference/cordis-catalog/events.md', '事件', 'Events'],
+    ['docs/persistence-catalog.md', 'reference/persistence-catalog.md', '持久化事件', 'Persistence events'],
+  ] as const).map(([source, route, rootLabel, enLabel], order): MirroredPage => ({
+    source,
+    route,
+    contentLocale: 'en-US',
+    label: { root: rootLabel, en: enLabel },
+    sidebar: { root: 'zh-reference', en: 'en-reference' },
+    section: { root: '生成参考', en: 'Generated reference' },
+    order,
+  })),
+  ...([
+    ['core.md', '核心数据结构', 'Core data structures'],
+    ['scope.md', '作用域', 'Scopes'],
+    ['session.md', '会话', 'Sessions'],
+    ['system-prompt.md', '系统提示词', 'System prompts'],
+    ['tools.md', '工具', 'Tools'],
+    ['llm-streaming.md', 'LLM 流式响应', 'LLM streaming'],
+    ['bash.md', 'Bash 执行', 'Bash execution'],
+    ['filesystem.md', '文件系统', 'Filesystem'],
+    ['code-runtime.md', '代码运行时', 'Code runtime'],
+    ['compaction.md', '上下文压缩', 'Compaction'],
+    ['subagent.md', '子代理', 'Subagents'],
+    ['workflow.md', '工作流', 'Workflows'],
+    ['skills.md', '技能', 'Skills'],
+    ['approval.md', '审批', 'Approvals'],
+    ['user-interaction.md', '用户交互', 'User interaction'],
+    ['sandbox.md', '沙箱', 'Sandboxing'],
+    ['web.md', 'Web 访问', 'Web access'],
+    ['persistence.md', '会话持久化', 'Session persistence'],
+  ] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({
+    source: `docs/core-data-structures/${file}`,
+    route: `reference/core-data-structures/${file}`,
+    contentLocale: 'en-US',
+    label: { root: rootLabel, en: enLabel },
+    sidebar: { root: 'zh-reference', en: 'en-reference' },
+    section: { root: '数据结构', en: 'Data structures' },
+    order,
+    ...(file === 'core.md' ? { sourceAliases: ['docs/core-data-structures'] } : {}),
+  })),
+  ...([
+    ['adding-a-package.md', '新增 Package', 'Adding a package'],
+    ['adding-a-tool.md', '新增 Tool', 'Adding a tool'],
+    ['adding-an-llm-adapter.md', '新增 LLM Adapter', 'Adding an LLM adapter'],
+    ['extension-cookbook.md', '扩展模式', 'Extension patterns'],
+  ] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({
+    source: `docs/cookbook/${file}`,
+    route: `reference/cookbook/${file}`,
+    contentLocale: 'en-US',
+    label: { root: rootLabel, en: enLabel },
+    sidebar: { root: 'zh-reference', en: 'en-reference' },
+    section: { root: '开发手册', en: 'Cookbook' },
+    order,
+  })),
+])
 
 /** Every canonical page published by the documentation website. */
 export const docsPages: DocsPage[] = [
-  ...zhGuide,
-  ...zhDevelop,
-  ...enOverview,
-  ...enCatalogs,
-  ...enCore,
-  ...enCookbook,
+  ...homeAndGuide,
+  ...develop,
+  ...reference,
 ]