开源文档体系

本文讲开源项目的文档体系怎么搭,回答如何用 Diátaxis 四象限组织内容、快速上手为什么留不住人、API 参考如何自动生成、多语言文档怎么维护。覆盖文档即代码的仓库与评审流程、版本化与搜索、文档度量与持续改进,并给出可落地的目录结构与常见返工点。

引言

文档是开源项目里「投入产出比最高、却最常被拖欠」的工作。代码写得再好,用户在三分钟内跑不起来就会离开;贡献者再热心,找不到「怎么开始」也会流失。一个残酷的事实是:大多数用户对你的项目的第一印象来自 README 的前二十行,而不是源码质量。

工程上的难点有三层。第一层是类型混淆:一份文档里同时混着「教新手跑起来」「告诉老手怎么配某个选项」「列全部参数」「解释为什么这样设计」四种内容,读者不知道自己该看哪一段,作者也不知道该写多细。第二层是同步失效:代码改了,文档没改,久而久之文档变成「不可信的参考」,读者转而去看源码或 Stack Overflow。第三层是维护成本:文档是纯人力投入,没有测试兜底,改错的代价不会立刻暴露,于是长期被排在所有工作之后。

还有一条被严重低估:文档是社区运营的入口。贡献者漏斗的第一级是「首次接触」,而绝大多数首次接触发生在文档页,不是 issue 列表。文档质量直接决定新人会不会提交第一个 PR。相关漏斗模型在开源社区运营与贡献者漏斗 里有系统讨论,本文只负责把文档这一环做扎实。

本文按「四象限 → 快速上手 → 教程设计 → 操作与参考 → API 自动生成 → 文档即代码 → 国际化 → 搜索与版本化 → 度量与改进」展开。需要划清一条边界:贡献者成长路径(新手议题、导师制、权限晋升)见贡献者成长路径与留存机制 ,本文只讲文档本身怎么组织与维护。读者对象是项目维护者、技术写作者与负责开发者体验的工程师。

目录

  1. 文档的四个象限
  2. 快速上手:五分钟能跑起来
  3. 教程与学习路径设计
  4. 操作指南与参考文档
  5. API 参考的自动生成
  6. 文档即代码:仓库、评审与测试
  7. 国际化与本地化
  8. 搜索、导航与版本化
  9. 文档度量与持续改进

1. 文档的四个象限

Diátaxis 把文档按「读者处于什么状态」分成四类,每一类的写法、语气、深度都不同。混乱的根源几乎总是「把四类揉在一起」。

Diátaxis 四象限:
  教程 Tutorial        学习导向  读者在「学」     我带你走一遍,保证成功
  操作指南 How-to      任务导向  读者在「做」     你要完成 X,步骤是这些
  参考 Reference       信息导向  读者在「查」     参数、返回值、全部选项
  解释 Explanation     理解导向  读者在「想」     为什么这样设计、权衡是什么

两个轴:
  实践 ↔ 理论   (教程/操作指南 偏实践,参考/解释 偏理论)
  学习 ↔ 应用   (教程/参考 偏学习,操作指南/解释 偏应用)

四类的判定标准是「读者带着什么问题来」:

类型读者的问题写作要点常见错误
教程「我该怎么上手」有唯一正确路径,保证能跑通掺入可选配置、分支说明
操作指南「我要做 X,怎么做」面向具体目标,步骤可跳跃从零讲起、重复教程内容
参考「这个参数什么意思」完备、精确、结构化写成散文、缺边界条件
解释「为什么这样设计」讲背景与权衡,可发散混进步骤、假装是教程

教程不写「可选」。教程的目标是让读者成功一次,任何「如果你用 Y 就改成 Z」都会打断节奏、制造分支、增加失败点。可选配置属于操作指南,边界情况属于参考,设计动机属于解释。把这条守住,教程的完成率会显著上升。

参考要能扫读。参考文档的读者是来「查」的,不是来「读」的。表格、统一的结构(每个参数都是「名称 / 类型 / 默认值 / 说明 / 示例」)、可锚定的标题,比流畅的段落有用得多。一份把参数说明写成三段散文的参考文档,实际使用率极低。

四象限的目录映射示例:
  docs/
    tutorials/     教程:getting-started.md、first-plugin.md
    guides/        操作指南:configure-auth.md、deploy-to-k8s.md
    reference/     参考:cli.md、api/、config-schema.md
    explanation/   解释:architecture.md、why-this-design.md

物理目录按象限划分,而不是按功能模块划分,是让作者和读者都清楚「这段该写什么」的最直接手段。Docusaurus、VitePress、MkDocs Material 都支持这种分区并各自生成导航。

2. 快速上手:五分钟能跑起来

「快速上手」(quickstart)不属于严格的四象限,它是项目最重要的一页,介于教程与操作指南之间。它的唯一目标是:让读者在五分钟内看到某个明确的结果。

快速上手的黄金结构:
  1. 一句话说明这是什么、解决什么问题(不超过两行)
  2. 前置要求(明确版本号,如 Node 20+、Docker 24+)
  3. 安装(一条命令,复制即可执行)
  4. 最小可运行示例(一段能直接跑的代码或一条命令)
  5. 期望输出(贴出真实输出,让读者能对照)
  6. 下一步链接(指向教程或操作指南)

第 5 步「期望输出」是被漏掉最多、价值却最高的一步。没有它,读者跑完不知道「这算成功了吗」,会去开 issue 或直接放弃。贴出真实输出(含版本号、耗时、关键行),读者一眼就能判断自己是否走对了路。

# 安装:一条命令,复制即用
curl -fsSL https://example.com/install.sh | sh -s -- --version 1.4.0

# 验证:跑一个最小示例
app init demo && cd demo && app run

# 期望输出(v1.4.0,Linux amd64,约 2 秒):
#   ✓ 已创建项目 demo
#   ✓ 服务已启动,监听 http://localhost:8080
#   ✓ 健康检查通过

版本要写死。快速上手里用 latest 或浮动 tag,会让读者在半年后照着文档跑却失败——因为默认行为已经变了。安装命令里写明确的版本号(--version 1.4.0),并在文档头部标注「本文档对应 v1.4」,是让文档长期可用的关键。

前置要求要具体。「需要较新版本的 Python」是无用的,「Python ≥ 3.10(3.9 缺少 match 语句支持)」才有用。明确版本能省掉大量「为什么报错」的 issue。同时列出常见环境的差异(macOS 的 Homebrew 路径、Windows 的 WSL 要求),但不要展开成安装教程——那是操作系统的文档该干的事。

快速上手的反模式后果修正
开头讲架构与设计读者还没跑起来就失去耐心把解释移到 explanation
安装命令用 latest半年后照着跑会失败写死版本号
没有期望输出读者不知道是否成功贴真实输出
前置要求含糊大量环境问题开成 issue写明版本号与已知差异
一上来就给三种安装方式选择困难,任一路径都可能踩坑给一条主路径,其余移到操作指南

3. 教程与学习路径设计

教程面向「完全没有上下文的读者」,目标是让他们在不理解原理的前提下也能成功一次。这决定了教程必须线性、必须可验证、必须容忍读者犯错。

好教程的四条纪律:
  1. 单一路径      不提供选项,所有分支移到别处
  2. 每步可验证    每步之后都有「你应该看到 X」,读者能自检
  3. 不解释原理    原理放 explanation,教程里最多一句「细节见 X」
  4. 结尾有成果    读者带走一个能跑的东西,而不是一堆知识

「每步可验证」是教程与博客的分水岭。博客可以写「然后配置一下」,教程必须写「打开 config.yaml,把 port 改成 8080,保存;运行 app run,应看到 listening on :8080」。可验证的步骤把调试成本从读者身上移走。

学习路径(learning path) 是多篇教程的编排。当项目有多个使用场景(部署到云、本地开发、集成到 CI)时,用一张「路径图」告诉读者不同起点该走哪条线:

docs/tutorials/
  README.md              # 路径图:我是谁 → 该读哪几篇
  01-getting-started.md  # 所有人
  02-first-project.md    # 所有人
  03-deploy-local.md     # 本地部署线
  04-deploy-cloud.md     # 云部署线
  05-integrate-ci.md     # CI 集成线

路径图的价值在于降低选择成本。新手面对一个二十篇的文档目录会不知所措,一张「你是 X,请读 1-2-4」的表格能立刻消除这种焦虑。路径图里可以引用持续集成这类外部主题的文档,但不要复制它们的内容。

4. 操作指南与参考文档

操作指南面向「已经会基本用法、要完成某个具体任务」的读者。它假定读者有上下文,可以跳跃、可以只讲一条路径、可以省略基础概念。

操作指南的写法要点:
  标题用「如何 X」或动宾短语,不用名词(configure-auth 而非 authentication)
  开头一句话说明「完成本文后你会得到什么」与前置条件
  步骤围绕目标组织,可含多方案(如「用 CLI」/「用 API」两种)
  结尾说明如何验证结果、常见错误及排查

操作指南要写「为什么不这样做」。一份好的操作指南不只给步骤,还会点出读者最容易走错的地方:「不要用 root 运行,否则后续权限检查会失败」。这类提示来自真实 issue,是把社区经验沉淀进文档的方式。

参考文档是四象限里唯一要求「完备」的一类,也是最容易自动化的。它的结构必须高度统一,因为读者是来扫读的:

### `--timeout`

| 属性 | 值 |
|------|-----|
| 类型 | `duration` |
| 默认 | `30s` |
| 环境变量 | `APP_TIMEOUT` |
| 自 v1.2 起 | 支持 `ms`/`s`/`m` 单位 |

请求超时时间。超过该值时请求被取消并返回 `ETIMEDOUT`。
设为 `0` 表示不超时(不推荐,可能导致连接挂起)。

每个条目字段顺序一致、用表格而非段落、标注「自哪个版本起引入」、给出边界情况(0 的含义),是参考文档质量的核心指标。「自 vX 起」这一项尤其重要:读者用的是旧版本时,能立刻知道这个参数是不是还没出现。

文档类型是否自动生成更新频率负责人
参考(API/CLI 参数)能自动则自动随代码生成器 + 代码注释
参考(配置 schema)可从 schema 生成随代码代码
操作指南人工随功能维护者 + 社区
教程人工随大版本维护者
解释人工不频繁核心维护者

判断「能不能自动生成」的简单标准:如果内容在代码里已经存在一份,就不要手写。参数名、类型、默认值、枚举值都在代码里,手写必然漂移。相关工程实践(把 schema、类型、注释作为唯一事实源)在 DevOps 与平台工程 的文档化实践里有相通之处。

5. API 参考的自动生成

API 参考是文档里最枯燥、最容易过期、也最该自动化的一部分。按语言生态选工具,原则是从代码本身提取,而不是另写一份。

主流 API 文档生成工具:
  OpenAPI/Swagger   HTTP API,从 openapi.yaml 生成交互式文档
  TypeDoc / JSDoc   TypeScript / JavaScript
  Sphinx + autodoc  Python(docstring)
  godoc / pkgsite   Go(直接从源码与注释生成)
  rustdoc           Rust(文档即注释,含可执行测试)
  Javadoc / Dokka   Java / Kotlin
  Doxygen           C / C++ / 多语言

HTTP API 优先用 OpenAPI。一份 openapi.yaml 既是文档来源,也是客户端代码生成、契约测试、mock 服务器的来源,一处维护多处受益:

  # openapi.yaml 片段
paths:
  /users/{id}:
    get:
      summary: 获取用户
      parameters:
        - name: id
          in: path
          required: true
          schema: {type: string, format: uuid}
      responses:
        '200':
          description: 用户详情
          content:
            application/json:
              schema: {$ref: '#/components/schemas/User'}
        '404':
          description: 用户不存在
# 从 openapi.yaml 生成静态文档站点
redocly build-docs openapi.yaml -o dist/api.html
# 或生成客户端 SDK,让文档与代码共享同一契约
openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o sdk/

注释即文档是最省力的模式。Rust 的 ///、Go 的 //、Python 的 docstring 都在源码里,工具直接提取。做法上有两条纪律:一是把注释当作面向使用者的说明(写「返回排序后的用户列表」而不是「调用 sort 方法」),二是给注释加可执行示例——rustdoc 的 doctest 会在 CI 里真正运行文档中的代码,文档即测试。

def retry(fn, times: int = 3, backoff: float = 0.5):
    """带指数退避的重试。

    Args:
        fn: 无参可调用对象。
        times: 最大尝试次数,含首次。必须 >= 1。
        backoff: 首次退避秒数,每次翻倍。

    Returns:
        fn 的返回值。

    Raises:
        RuntimeError: 全部尝试失败后抛出最后一次异常。

    Example:
        >>> retry(lambda: fetch("http://x"), times=5)
    """
生成方式优点缺点
从注释提取与代码同步、零额外维护受语言注释能力限制
从 schema 生成(OpenAPI/JSON Schema)契约驱动、可生成 SDK需要先有 schema
从类型推导类型准、无需额外注解描述性文字仍需手写
手写灵活必然过期

无论哪种方式,生成的文档里要能嵌入手写内容。纯生成的参考缺少「这个参数为什么存在」「什么场景该用」这类解释,做法是在注释或 schema 里用 description 字段承载,生成器原样带出。

6. 文档即代码:仓库、评审与测试

「文档即代码」不是把文档放进 Git 就完事,而是让文档享受代码同等的工程待遇:版本控制、评审、CI 检查、可测试。

文档即代码的四项实践:
  1. 同仓库或联动仓库  文档与代码在同一 PR 里改,避免版本错位
  2. 走 PR 评审        文档变更同样需要 review,至少一个批准
  3. CI 检查           链接有效性、构建成功、格式、拼写、代码块可执行
  4. 可预览            每个 PR 部署一个预览站点,评审时看渲染结果

文档与代码同仓库是首选。放在独立仓库会导致「代码改了、文档忘了」的经典问题,且版本对齐困难。如果因为权限或发布流程必须分离,至少要建立联动机制:代码仓库的 PR 模板里提示「是否需要更新文档」,并在 CI 里检测公开 API 变更但文档未改时给出警告。

CI 要检查什么,按投入产出排序:

# 1) 链接有效性(最常见的文档腐化)
lychee --offline --no-progress docs/          # 本地链接
lychee --no-progress docs/                    # 含外链(需网络,注意限流)
# 2) 构建成功(配置错、frontmatter 错、短代码错)
mkdocs build --strict
# 3) 代码块可执行(把文档里的示例当测试跑)
pytest --doctest-modules src/                 # Python doctest
mdbook test                                   # Rust 风格
# 4) 格式与拼写
markdownlint-cli2 "docs/**/*.md"
typos docs/

链接检查必须做。文档腐化最普遍的形式就是死链:重构后锚点变了、外部资源下线、相对路径写错。lychee、markdown-link-check 这类工具能自动抓出,接进 CI 后基本消灭死链。注意外链检查会受网络与限流影响,建议内部链接每次检查、外部链接每日定时跑。

  # .github/workflows/docs.yml 片段
- name: Build docs
  run: mkdocs build --strict
- name: Check links
  uses: lycheeverse/lychee-action@v2
  with: {args: "--no-progress --offline docs/"}

预览站点大幅提升评审质量。纯 Markdown diff 很难看出渲染效果,而一个临时部署的预览站点让评审者直接看到最终页面。Vercel、Netlify、GitHub Pages 都能对 PR 自动部署预览。相关流水线搭建方式可参考 GitHub Actions 的部署实践。

检查项工具示例阻断强度频率
内部链接lychee(offline)阻断每次 PR
外部链接lychee警告每日定时
构建mkdocs build –strict阻断每次 PR
代码块doctest / mdbook test阻断每次 PR
拼写typos警告每次 PR
格式markdownlint警告每次 PR

7. 国际化与本地化

当项目的用户跨越语言,文档国际化(i18n)就从「加分项」变成「必选项」。但国际化的成本是维护成本的乘法,需要谨慎设计。

三种国际化策略:
  A. 只维护英文         成本最低,覆盖最广(英文是技术通用语)
  B. 英文 + 少量核心语言 把快速上手与安装翻译,其余保持英文
  C. 全量多语言          覆盖最全,但每次改动要同步 N 份,成本极高

推荐策略 B:优先翻译「快速上手、安装、常见问题」这三类高频页面,其余保持英文并在页面顶部标注「本页仅有英文版本」。这样既降低了非英语用户的门槛,又不会让维护者陷入「改一句要改五份」的泥潭。

docs/
  en/            # 主语言(事实源)
  zh-CN/         # 翻译,核心页面
  ja/            # 翻译,核心页面

主语言必须是事实源。所有内容先写英文(或先写中文),再翻译,绝不允许某个语言单独更新。翻译的同步机制有两种:

  • 基于工具链:Crowdin、Weblate、Transifex 等平台把翻译流程外包给社区,支持「原文变更时标记译文为待更新」。
  • 基于目录约定:翻译文件与原文件路径一一对应,CI 检查「原文件变更但对应译文未变更」时给出提醒。
  # 翻译同步的 CI 检查思路
- name: Detect stale translations
  run: |
    ./scripts/check-translations.py --source docs/en --target docs/zh-CN \
      --fail-on-stale-core   # 核心页面译文过期即失败

技术术语不翻译。pull request、commit、token、endpoint 这类术语保留英文,强行翻译反而增加理解成本(「拉取请求」「提交」「令牌」在中文技术语境里反而不如原词直观)。术语表(glossary)应当作为翻译资产的一部分维护,确保同一术语在全文一致。

国际化策略维护成本覆盖面适合项目
只英文1x技术用户早期项目、工具库
核心页面翻译1.3x主要用户群多数成熟项目
全量多语言N x全部面向终端用户的产品

还有一条实务经验:翻译要滞后于原文,但必须可追溯。在页面顶部标注「本文档对应英文版 v1.4,最新英文版见 X」,让读者知道译文可能落后,而不是以为这就是最新内容。

8. 搜索、导航与版本化

文档规模超过几十页后,能不能被找到比「写得好不好」更重要。三个机制决定可发现性:导航结构、站内搜索、版本化。

导航结构的三层:
  顶部导航    按象限或产品模块(Tutorials / Guides / Reference / Explanation)
  侧边栏      当前分区内的目录树,随滚动高亮
  页内目录    右侧 TOC,长页面的锚点导航

搜索的关键要求:
  中文分词支持(默认的英文分词器对中文几乎无效)
  代码与参数名可搜(--timeout 应能搜到)
  结果带上下文片段,而非只有标题

中文搜索是常被忽略的坑。MkDocs 的默认 lunr 搜索对中文按整词匹配,实际几乎搜不到东西,必须换用支持中文分词的分词器(如 jieba)或改用 Algolia DocSearch 这类外部搜索。如果文档以中文为主,这一项在选型阶段就要确认,事后补救成本很高。

版本化文档是库与框架项目的必备能力。用户用的是 v1.4,就不该在文档里看到 v2.0 才有的参数。主流方案(Docusaurus 的 versioned docs、MkDocs 的 mike、VitePress 的多版本配置)都支持「发布时归档当前版本」:

# mike:为 v1.4 归档一份文档快照,并设为默认
mike deploy --push --update-aliases 1.4 latest
mike set-default --push latest

版本化的策略:维护「最新稳定版」+「上一两个 major」即可,不必保留全部历史版本。文档站顶部的版本切换器让用户能切到自己的版本,同时默认展示最新稳定版(而不是开发版)。

机制缺失后果工具
侧边栏目录读者不知道文档有哪些内容所有主流生成器内置
页内 TOC长页面难以跳转生成器内置
中文搜索中文文档搜不到内容jieba / Algolia
版本化用户看到不适用自己版本的文档mike / Docusaurus

9. 文档度量与持续改进

文档的效果可以度量,且度量结果能直接指导改进方向。三个层次:

文档度量的三个层次:
  可达性   有多少人找到了文档?搜索命中率、404 率、跳出率
  有效性   找到后有没有用起来?快速上手完成率、示例复制率
  健康度   文档本身的状态?死链数、过期页面数、覆盖率、更新滞后

快速上手完成率是最高价值的指标,但需要埋点或用户反馈才能拿到。低成本替代方案是在快速上手页末尾放一个「你跑通了吗?」的二选一反馈按钮,收集失败率与失败原因。这个数据比任何主观评估都可靠。

文档健康度的可自动采集项:
  死链数量          链接检查工具,目标 0
  过期页面比例      最后更新时间 > 12 个月且对应代码已变更的页面
  API 覆盖率        被文档化的公开接口比例,目标 100%(自动生成时天然达成)
  示例可执行率      文档中代码块通过测试的比例
  反馈率            页面「有帮助/没帮助」的比例与差评集中页

把「文档变更」纳入 PR 流程是持续改进的制度保障。做法有三:PR 模板里加「是否影响文档」的勾选项;CI 检测到公开 API 变更但 docs/ 未改时给警告;把「文档是否更新」作为评审清单的一项。制度比自觉可靠。

指标采集方式目标恶化时先查
死链数链接检查工具0最近的重构 PR
过期页面比例对比代码与文档更新时间< 10%哪些模块文档无人维护
快速上手失败率页面反馈按钮< 15%前置要求、版本、期望输出
示例可执行率CI 跑文档代码块100%哪些示例已腐化
差评集中页页面反馈无集中该页的读者画像是否错配

**定期「文档走查」**是低成本高收益的实践:每季度让一位没参与过该功能的贡献者照着快速上手走一遍,记录所有卡住的地方。走查发现的「文档作者觉得显然、读者却卡住」的环节,往往是改进优先级最高的地方。

权衡取舍

取舍点偏 A偏 B建议
文档位置独立仓库,权限清晰与代码同仓库,易同步优先同仓库,权限问题再分离
参考文档手写,灵活可控自动生成,同步但生硬能生成则生成,描述手写
国际化只英文,成本最低全量多语言,覆盖最全核心页面翻译 + 其余标注英文
搜索内置搜索,零配置外部搜索,效果好但依赖中文项目必须换分词器
版本化只维护最新版多版本并存最新稳定 + 上一两个 major
检查强度全阻断,质量高但拖慢只警告,快但易腐化构建与死链阻断,拼写格式警告
文档深度只写 API 参考,够用含教程与解释,完整但费力先保证快速上手与参考,再补教程

核心取舍是覆盖深度与维护可持续性的平衡。文档写得越全,同步成本越高,越容易腐化。工程上最优解通常是「把高频路径写透、把低频路径交给自动生成、把原理性内容写一次并接受它缓慢过时」,而不是追求「所有内容都完整且最新」。

常见坑清单

  • 四象限混写:教程里掺可选配置、参考里写散文——按读者问题分类,物理目录也分开。
  • 快速上手没有期望输出:读者不知道是否成功,转而开 issue——贴真实输出。
  • 安装命令用浮动版本:半年后照着跑会失败——写死版本号并标注文档对应的版本。
  • API 参考手写:必然与代码漂移——从注释或 schema 自动生成。
  • 文档放独立仓库且无联动:代码改了文档忘了——同仓库或加 CI 联动检查。
  • CI 只构建不查链接:死链长期积累——内部链接每次 PR 阻断检查。
  • 中文文档用默认英文分词搜索:搜不到内容,等于没有搜索——换中文分词器或外部搜索。
  • 翻译全量维护:改一句要同步 N 份,很快放弃——只翻译核心页面,其余标注英文。
  • 只维护最新版文档:旧版本用户看到不适用的内容——按 major 版本化并保留上一两个。
  • 示例代码从不执行:示例悄悄腐化,读者照抄报错——把代码块纳入 CI 测试。
  • 术语随意翻译:同一概念在不同页面译法不同——维护术语表并保持英文术语。
  • 文档变更不评审:错误无人发现——文档 PR 同样走评审,部署预览站点。

小结

开源文档体系的本质是按读者的状态组织内容,并用工程手段保证它不腐化。Diátaxis 四象限解决「写什么、写给谁」,快速上手与参考文档解决「高频路径要好用」,文档即代码与 CI 检查解决「长期不失真」,版本化与搜索解决「能不能被找到」。四件事里任何一件缺失,文档都会退化成「过期的摆设」。

落地顺序建议从快速上手与参考文档开始:先把「跑起来」和「查参数」这两条最高频的路径做扎实,再加 CI 检查(死链与构建),最后补教程、解释与国际化。跳过前两步直接做多语言,通常以「翻译了一堆没人看的内容」告终。

如果想继续深入,建议沿两条线读:一条是社区侧,从开源社区运营与贡献者漏斗看文档如何作为贡献者漏斗的第一级入口,理解「好文档带来好贡献者」的因果链;另一条是体验侧,从贡献者成长路径与留存机制看新手友好议题与文档如何配合,把「让新人跑起来」与「让新人留下来」两条路径接上。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「开源生态」更多文章

  1. 开源商标与品牌治理
  2. 开源度量与分析
  3. 企业参与开源与 OSPO