Explorar o código

feat(docs): add independent platform command tabs

gengruilin hai 1 semana
pai
achega
bcb49ade82

+ 6 - 0
.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.i18n.yaml

@@ -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/feature/2026-09-15-docs-platform-command-tabs.md
+2026-09-15-docs-platform-command-tabs.md: f21fef8b7091de296ee2aab329fdc7d6362e8c14
+2026-09-15-docs-platform-command-tabs.zh.md: 2b309a4587df3fc4b727baf6aa546f07e2a51311

+ 31 - 0
.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.md

@@ -0,0 +1,31 @@
+# Agent Note: Platform command tabs in the Python SDK guide
+
+Status: implemented
+
+English | [中文](2026-09-15-docs-platform-command-tabs.zh.md)
+
+## Problem
+
+The [Python SDK guide](../../../../docs/user/guide/python-sdk.md) gives different commands for POSIX shells and Windows PowerShell. Showing both versions in sequence makes readers skip an alternative at each step and can make alternative commands look cumulative.
+
+## Decision
+
+Each of the four platform-dependent steps uses VitePress's native `code-group`: Linux/macOS first, Windows PowerShell second. Each group selects independently and starts on Linux/macOS. Credential setup and running the example remain separate sequential steps. Shared Python and YAML examples stay directly visible.
+
+Both languages retain every command in canonical Markdown. The documentation projector preserves the groups and their code in the website's raw Markdown. Explicit anchors before each group preserve the former platform heading targets; following a platform anchor locates the group without selecting a tab.
+
+The theme adds a visible keyboard focus outline to native radio labels and displays both command blocks in search excerpts, whose tab controls are unavailable. A Markdown renderer adapter makes each native tab strip a form with submission prevented. Local search copies a section's rendered HTML, including radio names; separate form owners stop those copies from clearing the page's selection. VitePress owns selection, syntax highlighting, and copying the active code block. There is no shared selection state or stored preference.
+
+## Alternatives considered
+
+**Keep vertically repeated platform sections.** Both versions are immediately visible, but the repeated alternatives interrupt the guide's ordered steps.
+
+**Build a custom tab component with shared or persistent selection.** A Windows reader could select once, but shared state couples independent examples and introduces selection and lifecycle code beyond the required presentation. Native groups keep the interaction local.
+
+## Consequences
+
+Readers see one command version per step while repository readers and raw-Markdown consumers retain both. Windows readers select PowerShell in each group. Existing platform links still reach the corresponding step, although the platform names leave the page outline.
+
+Verification covers command preservation, bilingual pairing, projected and raw Markdown, and actual browser switching, copying, keyboard focus, search, themes, and narrow layouts. Framework-owned interaction is exercised on the running site; session replay is inapplicable to this documentation-only presentation.
+
+The focused renderer regression checks radio ownership when local search duplicates a section. The adapter depends on VitePress's native tab-strip markup and fails the build if that markup changes; a framework upgrade must retain the search isolation and browser checks.

+ 31 - 0
.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.zh.md

@@ -0,0 +1,31 @@
+# Agent Note: Python SDK 指南的平台命令标签
+
+Status: implemented
+
+[English](2026-09-15-docs-platform-command-tabs.md) | 中文
+
+## 问题
+
+[Python SDK 指南](../../../../docs/user/guide/python-sdk.zh.md)分别提供 POSIX shell 和 Windows PowerShell 命令。连续显示两个版本使读者在每一步都要跳过另一平台,也容易把二选一的命令理解为需要依次执行。
+
+## 决策
+
+四个平台相关步骤分别使用 VitePress 原生 `code-group`:Linux/macOS 在前,Windows PowerShell 在后。各组独立选择,初始均显示 Linux/macOS。配置凭据和运行示例仍是两个顺序步骤。共用的 Python 和 YAML 示例保持直接可见。
+
+两种语言均在规范 Markdown 中保留每条命令。文档投影器在网站原始 Markdown 中保留分组及代码。各组前的显式锚点保留原有平台标题的链接目标;平台锚点定位到分组,但不选择标签。
+
+主题为原生单选按钮标签补充可见的键盘焦点轮廓,并在无法操作标签的搜索摘要中显示两个命令块。Markdown 渲染适配器将各原生标签栏设为禁止提交的表单。本地搜索会复制章节渲染后的 HTML,其中包含单选按钮名称;独立的所属表单可避免副本清除页面的选择。VitePress 负责选择、语法高亮和复制当前代码块。不引入共享选择状态或持久化偏好。
+
+## 考虑过的替代方案
+
+**保留纵向重复的平台章节。** 两个版本都立即可见,但重复的备选命令会打断指南的顺序步骤。
+
+**自建标签组件并共享或持久化选择。** Windows 读者只需选择一次,但共享状态会耦合独立示例,并引入超出所需展示功能的选择与生命周期代码。原生分组将交互限定在各组内。
+
+## 影响
+
+读者每一步只看到一个命令版本,仓库读者和原始 Markdown 消费者仍能获取两个版本。Windows 读者需要在各组中选择 PowerShell。原有平台链接仍到达对应步骤,但平台名称不再出现在页面大纲中。
+
+验证覆盖命令保留、双语配对、投影和原始 Markdown,以及真实浏览器中的切换、复制、键盘焦点、搜索、主题和窄屏布局。框架负责的交互在运行中的站点验收;会话回放不适用于这项纯文档展示变更。
+
+聚焦的渲染回归验证本地搜索复制章节时的单选按钮归属。适配器依赖 VitePress 原生标签栏的 HTML,结构变化时会使构建失败;框架升级必须保留搜索隔离和浏览器检查。

+ 2 - 2
docs/user/guide/python-sdk.i18n.yaml

@@ -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/python-sdk.md
-python-sdk.md: 2cb01ffa19d173231902c5c816929fa6b4d499f0
-python-sdk.zh.md: 3db859116530e51a7c7478ab222cbc4d2435dbf2
+python-sdk.md: 445003e980c92c5c727e51b41deb1b516681b20b
+python-sdk.zh.md: b776781022dd3ef7af2331d169c006b9c9fa3381

+ 28 - 20
docs/user/guide/python-sdk.md

@@ -14,9 +14,11 @@ This tutorial installs the published Python SDK, runs the shipped standalone min
 
 ## Install the SDK
 
-### Linux and macOS
+<a id="linux-and-macos"></a><a id="windows-powershell"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 git clone https://github.com/deepseek-ai/deepseek-harness.git
 cd deepseek-harness
 python -m venv .venv
@@ -24,9 +26,7 @@ python -m venv .venv
 python -m pip install deepseek-harness-sdk
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 git clone https://github.com/deepseek-ai/deepseek-harness.git
 Set-Location deepseek-harness
 py -3.10 -m venv .venv
@@ -34,31 +34,37 @@ py -3.10 -m venv .venv
 python -m pip install deepseek-harness-sdk
 ```
 
+:::
+
 The installation includes a matching native runtime wheel and the `dsh` command. Normal SDK execution needs no system Node.js. Repository contributors who build the artifacts should use the [Python contributor workflow](../../../python/development.md).
 
 ## Run the checked-in example
 
 Export the credential and, when needed, a compatible proxy endpoint:
 
-### Linux and macOS
+<a id="linux-and-macos-1"></a><a id="windows-powershell-1"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 export DEEPSEEK_API_KEY=sk-your-key-here
 # export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 $env:DEEPSEEK_API_KEY = "sk-your-key-here"
 # $env:DEEPSEEK_BASE_URL = "http://127.0.0.1:8000/v1"
 ```
 
+:::
+
 Run one task with explicit workspace and home paths:
 
-### Linux and macOS
+<a id="linux-and-macos-2"></a><a id="windows-powershell-2"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 python python/sdk/examples/minimal.py \
   --workspace /absolute/path/to/disposable-workspace \
   --dsh-home /absolute/path/to/example-dsh-home \
@@ -66,9 +72,7 @@ python python/sdk/examples/minimal.py \
   "Inspect the repository and fix the failing tests."
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 python python/sdk/examples/minimal.py `
   --workspace C:\work\disposable-workspace `
   --dsh-home C:\work\example-dsh-home `
@@ -76,6 +80,8 @@ python python/sdk/examples/minimal.py `
   "Inspect the repository and fix the failing tests."
 ```
 
+:::
+
 The script prints the final assistant response. The selected home receives the generated `sdk-minimal` profile, installed plugins, and uncompressed JSONL session logs under `sessions/`. The example and SDK never silently read `~/.dsh`.
 
 ## Use the SDK in your program
@@ -109,22 +115,24 @@ The SDK starts the bundled `dsh --profile sdk-minimal` process lazily and reuses
 
 Use `dsh plugin` for dependencies and bundle layers that should persist in this home:
 
-### Linux and macOS
+<a id="linux-and-macos-3"></a><a id="windows-powershell-3"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 export DSH_HOME=/absolute/path/to/example-dsh-home
 dsh --profile sdk-minimal --dump-default-config >/dev/null
 dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 $env:DSH_HOME = "C:\work\example-dsh-home"
 dsh --profile sdk-minimal --dump-default-config | Out-Null
 dsh plugin --profile sdk-minimal add file:C:/work/my-plugin-bundle
 ```
 
+:::
+
 The first command initializes the shipped standalone profile. The second forwards package management to `pnpm`, then records any installed package that exports a `dsh.bundle` layer. Install `pnpm` only for this management command; launching the installed SDK does not need it. Edit `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` for persistent row changes, or pass patch files from Python for per-launch changes.
 
 Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Missing server rows, unresolved plugins, and invalid patches fail during startup instead of falling back to another composition.

+ 28 - 20
docs/user/guide/python-sdk.zh.md

@@ -14,9 +14,11 @@
 
 ## 安装 SDK
 
-### Linux 与 macOS
+<a id="linux-与-macos"></a><a id="windows-powershell"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 git clone https://github.com/deepseek-ai/deepseek-harness.git
 cd deepseek-harness
 python -m venv .venv
@@ -24,9 +26,7 @@ python -m venv .venv
 python -m pip install deepseek-harness-sdk
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 git clone https://github.com/deepseek-ai/deepseek-harness.git
 Set-Location deepseek-harness
 py -3.10 -m venv .venv
@@ -34,31 +34,37 @@ py -3.10 -m venv .venv
 python -m pip install deepseek-harness-sdk
 ```
 
+:::
+
 安装内容包含匹配的原生运行时 wheel 与 `dsh` 命令。普通 SDK 运行不需要系统 Node.js。需要构建产物的仓库贡献者应使用 [Python 贡献者工作流](../../../python/development.zh.md)。
 
 ## 运行检入示例
 
 导出凭据;使用兼容代理时再设置 endpoint:
 
-### Linux 与 macOS
+<a id="linux-与-macos-1"></a><a id="windows-powershell-1"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 export DEEPSEEK_API_KEY=sk-your-key-here
 # export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 $env:DEEPSEEK_API_KEY = "sk-your-key-here"
 # $env:DEEPSEEK_BASE_URL = "http://127.0.0.1:8000/v1"
 ```
 
+:::
+
 使用显式 workspace 与 home 路径运行一个任务:
 
-### Linux 与 macOS
+<a id="linux-与-macos-2"></a><a id="windows-powershell-2"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 python python/sdk/examples/minimal.py \
   --workspace /absolute/path/to/disposable-workspace \
   --dsh-home /absolute/path/to/example-dsh-home \
@@ -66,9 +72,7 @@ python python/sdk/examples/minimal.py \
   "Inspect the repository and fix the failing tests."
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 python python/sdk/examples/minimal.py `
   --workspace C:\work\disposable-workspace `
   --dsh-home C:\work\example-dsh-home `
@@ -76,6 +80,8 @@ python python/sdk/examples/minimal.py `
   "Inspect the repository and fix the failing tests."
 ```
 
+:::
+
 脚本会打印最终 assistant 响应。所选 home 会保存生成的 `sdk-minimal` profile、已安装插件,以及 `sessions/` 下的未压缩 JSONL 会话日志。示例与 SDK 绝不会静默读取 `~/.dsh`。
 
 ## 在程序中使用 SDK
@@ -109,22 +115,24 @@ SDK 会延迟启动内置的 `dsh --profile sdk-minimal` 进程,并复用到
 
 需要在该 home 中持久保存依赖与 bundle 层时,使用 `dsh plugin`:
 
-### Linux 与 macOS
+<a id="linux-与-macos-3"></a><a id="windows-powershell-3"></a>
 
-```sh
+::: code-group
+
+```sh [Linux/macOS]
 export DSH_HOME=/absolute/path/to/example-dsh-home
 dsh --profile sdk-minimal --dump-default-config >/dev/null
 dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle
 ```
 
-### Windows PowerShell
-
-```powershell
+```powershell [Windows PowerShell]
 $env:DSH_HOME = "C:\work\example-dsh-home"
 dsh --profile sdk-minimal --dump-default-config | Out-Null
 dsh plugin --profile sdk-minimal add file:C:/work/my-plugin-bundle
 ```
 
+:::
+
 第一个命令初始化随附的独立 profile。第二个命令把包管理转发给 `pnpm`,然后记录所有导出 `dsh.bundle` 层的已安装包。只有执行此管理命令时才需要安装 `pnpm`;启动已安装 SDK 不需要它。持久配置项变更应编辑 `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml`;单次启动变更则从 Python 传入 patch 文件。
 
 另一个 `profile` 只有包含 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项时才有效。缺失 server 配置项、无法解析的插件和非法 patch 会在启动时失败,不会回退到其他组合。

+ 1 - 1
package.json

@@ -123,7 +123,7 @@
     "docs:build": "tsx website/build.ts && pnpm run verify-doc-site-fragments",
     "docs:build:mpa": "tsx website/build.ts --mpa && pnpm run verify-doc-site-fragments",
     "docs:preview": "pnpm --filter @deepseek-ai/website run preview",
-    "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts scripts/verify-doc-site-fragments.spec.ts website/tests/mermaid-viewer.spec.ts && pnpm run docs:build",
+    "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts scripts/verify-doc-site-fragments.spec.ts website/tests/mermaid-viewer.spec.ts website/tests/code-groups.spec.ts && pnpm run docs:build",
     "website:dev": "pnpm run docs:dev",
     "website:build": "pnpm run docs:build",
     "verify-package-readme-limitations": "tsx scripts/verify-package-readme-limitations.ts",

+ 1 - 0
scripts/run-gates.ts

@@ -779,6 +779,7 @@ function docSyncLeafGates(options: {
     pnpmExec('docs-site-projection', [
       'vitest', 'run', 'scripts/project-doc-site.spec.ts', 'scripts/verify-doc-site-fragments.spec.ts',
       'website/tests/mermaid-viewer.spec.ts',
+      'website/tests/code-groups.spec.ts',
     ], {
       label: 'documentation site checks',
     }),

+ 22 - 0
website/.vitepress/code-groups.ts

@@ -0,0 +1,22 @@
+/** Isolates native code-group radios from copies rendered in local search. */
+
+import type { MarkdownRenderer } from 'vitepress'
+
+/**
+ * Gives each native tab strip its own form so search excerpts cannot clear the page's selection.
+ *
+ * @param md - VitePress renderer with the native code-group rules installed.
+ */
+export function isolateCodeGroupRadios(md: MarkdownRenderer): void {
+  const render = md.renderer.rules['container_code-group_open']
+  if (render === undefined) throw new Error('VitePress Markdown renderer is missing the code-group opening rule.')
+  md.renderer.rules['container_code-group_open'] = (...args) => {
+    const html = render(...args)
+    const opening = '<div class="tabs">'
+    const closing = '</div><div class="blocks">'
+    if (!html.includes(opening) || !html.includes(closing)) {
+      throw new Error('VitePress code-group markup does not contain the expected tab strip.')
+    }
+    return html.replace(opening, '<form class="tabs" @submit.prevent>').replace(closing, '</form><div class="blocks">')
+  }
+}

+ 2 - 0
website/.vitepress/config.ts

@@ -5,6 +5,7 @@ import { resolve } from 'node:path'
 import type { DefaultTheme, PageData, SiteConfig } from 'vitepress'
 import type { ViteDevServer } from 'vite'
 import { withMermaid } from 'vitepress-plugin-mermaid'
+import { isolateCodeGroupRadios } from './code-groups.ts'
 import { landingLink, localeCollections, orderedPages, routeLink, sectionSpec, type DocsLocale, type DocsPage, type DocsSidebar } from '../docs.ts'
 import { docsSourceFiles, emitRawMarkdownPages, llmsTxt, projectDocs, rawMarkdownRoute } from '../../scripts/project-doc-site.ts'
 
@@ -381,6 +382,7 @@ export default withMermaid({
   },
   markdown: {
     config(md) {
+      isolateCodeGroupRadios(md)
       const renderText = md.renderer.rules.text
       const renderCode = md.renderer.rules.code_inline
       const renderFence = md.renderer.rules.fence

+ 10 - 0
website/.vitepress/theme/code-group.css

@@ -0,0 +1,10 @@
+.vp-code-group input:focus-visible + label {
+  outline: 2px solid var(--vp-c-brand-1);
+  outline-offset: -4px;
+  border-radius: 4px;
+}
+
+/* Search excerpts have no tab controls; every matching command stays visible. */
+.VPLocalSearchBox .vp-code-group .blocks > div {
+  display: block;
+}

+ 1 - 0
website/.vitepress/theme/index.ts

@@ -4,6 +4,7 @@ import { useData, useRoute, type Theme } from 'vitepress'
 import { defineComponent, h, onBeforeUnmount, onMounted, watch } from 'vue'
 import type { MermaidViewer } from './mermaid-viewer.ts'
 import './mermaid-viewer.css'
+import './code-group.css'
 
 export default {
   extends: DefaultTheme,

+ 2 - 0
website/AGENTS.md

@@ -17,3 +17,5 @@ The build also emits each route's raw-Markdown twin (with a parent-level alias p
 Run `pnpm docs:check` after changing this subtree; the gate rejects additional non-ignored Markdown under `website/`.
 
 The default-theme extension owns the Mermaid fullscreen viewer. Keep its enhancements separate from Markdown projection and preserve the original SVG. Route, language, theme, and rendered-SVG changes close the active view; theme disposal releases every observer, listener, and scroll lock. The [viewer decision](../.agents/notes/implemented/feature/2026-09-14-docs-mermaid-viewer.md) explains SVG isolation and verification.
+
+Native code groups keep their radio controls in separate forms so copied search excerpts cannot clear the page's selected platform. Search excerpts display every command block because their tab controls are unavailable; the [platform-tab decision](../.agents/notes/implemented/feature/2026-09-15-docs-platform-command-tabs.md) owns the interaction and verification.

+ 35 - 0
website/tests/code-groups.spec.ts

@@ -0,0 +1,35 @@
+/** Native code-group markup remains isolated when local search copies a section. */
+
+import assert from 'node:assert/strict'
+import { JSDOM } from 'jsdom'
+import { createMarkdownRenderer } from 'vitepress'
+import { expect, it } from 'vitest'
+import { isolateCodeGroupRadios } from '../.vitepress/code-groups.ts'
+
+it('keeps the page selection when a search excerpt selects the same named radio', async () => {
+  const md = await createMarkdownRenderer(import.meta.dirname, { config: isolateCodeGroupRadios })
+  const html = md.render('::: code-group\n\n```sh [Linux/macOS]\n. .venv/bin/activate\n```\n\n```powershell [Windows PowerShell]\n.venv\\Scripts\\Activate.ps1\n```\n\n:::\n')
+  const dom = new JSDOM(`<main>${html}</main><aside>${html}</aside>`)
+  try {
+    const { document } = dom.window
+    const pageRadios = document.querySelectorAll<HTMLInputElement>('main input')
+    const excerptRadio = document.querySelector<HTMLInputElement>('aside input')
+    assert(pageRadios[0] && pageRadios[1] && excerptRadio)
+    pageRadios[1].checked = true
+    excerptRadio.checked = true
+
+    expect(pageRadios[0].checked).toBe(false)
+    expect(pageRadios[1].checked).toBe(true)
+    expect(excerptRadio.checked).toBe(true)
+    expect(pageRadios[0].name).toBe(excerptRadio.name)
+    expect(pageRadios[0].form).not.toBeNull()
+    expect(pageRadios[0].form).not.toBe(excerptRadio.form)
+    expect(document.querySelector('main .tabs')?.hasAttribute('@submit.prevent')).toBe(true)
+    expect([...document.querySelectorAll('main code')].map(code => code.textContent)).toEqual([
+      '. .venv/bin/activate', '.venv\\Scripts\\Activate.ps1',
+    ])
+    expect(document.querySelectorAll('main .blocks > .active')).toHaveLength(1)
+  } finally {
+    dom.window.close()
+  }
+})