业务适配开发指南

August 29, 2026 · View on GitHub

本文面向需要把企业需求、缺陷或问题系统接入 DSH SDD 项目的业务开发人员。目标是让业务代码只负责“读取外部事实并归一化”,由 SDD 阶段 Agent 负责把事实整合成当前架构要求的交付件。

企业适配不是插件运行的前置条件。未开发适配器的项目可直接使用内置“手工录入”来源,填写标题、描述和可选子项;只有需要自动读取企业系统时才需要本指南中的 Connector 和 Adapter。

目录边界

Connector 和 Adapter 只有一套文件格式,可以按复用范围放在两个位置:

# 插件源码级:随插件安装一次,所有项目可用
business/
├── connectors/
│   └── company-alm.yaml
└── adapters/
    ├── fetch-company-alm.mjs
    └── lib/
        └── status-map.mjs
# 项目级:只供当前项目使用
.sdd/business/
├── README.md
├── connectors/
│   └── company-alm.yaml
└── adapters/
    ├── fetch-company-alm.mjs
    └── lib/
        └── status-map.mjs
  • connectors/ 只放 Connector YAML 配置。
  • adapters/ 放可执行适配器及其内部模块。
  • 项目存在同名 Connector 时覆盖插件 Connector,页面会显示“项目覆盖”。
  • Token、Cookie、私钥和本地凭证不得放入该目录或提交到 Git。
  • .sdd/sources/ 是插件生成的只读来源快照,不放业务代码。

最小接入流程

  1. 决定复用范围:企业通用适配放插件 business/,项目专用适配放 .sdd/business/
  2. 在对应 adapters/ 编写读取企业系统的脚本。
  3. 在同一范围的 connectors/ 声明启动命令、超时和允许继承的环境变量。
  4. .sdd/project.yamlrequirementdefect 等来源类型配置默认 Connector。
  5. 在命令行完成协议测试。
  6. 启动 DSH,在阶段页面按主业务编号导入,检查变更预览和生成的工作单元。

Connector 配置

business/connectors/company-alm.yaml.sdd/business/connectors/company-alm.yaml

schema: dsh-sdd/connector@1
id: company-alm
type: command
command:
  - node
  - .sdd/business/adapters/fetch-company-alm.mjs
timeoutMs: 30000
environment:
  - COMPANY_ALM_URL
  - COMPANY_ALM_TOKEN

约束如下:

  • id 使用 kebab-case,并与文件名一致。
  • command 是 argv 数组,不经过 shell。
  • .sdd/business/adapters/ 是统一逻辑路径:插件 Connector 映射到插件 business/adapters/,项目 Connector 映射到项目 .sdd/business/adapters/
  • Adapter 子进程仍以项目 Workspace 根目录为工作目录。
  • 子进程只继承基础启动变量和 environment 显式声明的变量。
  • 默认超时 30 秒,可配置范围为 100 毫秒到 5 分钟。
  • stdout 最大 2 MiB。

适配器输入协议

插件向 stdin 写入一个 JSON 对象:

{"operation":"get","kind":"requirement","key":"PAY-381"}

字段含义:

字段含义
operation当前固定为 get
kind来源类型,例如 requirementdefect
key用户输入的外部业务编号

适配器必须只把一个 dsh-sdd/source-bundle@1 JSON 对象写到 stdout。items 至少包含一项,单条来源就是长度为 1 的数组。调试日志和诊断信息写到 stderr;退出码非零表示获取失败。

适配器输出协议

输出统一符合 dsh-sdd/source-bundle@1。下面是只有一条来源的示例:

{
  "schema": "dsh-sdd/source-bundle@1",
  "uid": "company-alm:bundle:PAY-381",
  "provider": "company-alm",
  "kind": "requirement",
  "externalKey": "PAY-381",
  "title": "支持订单部分退款",
  "fetchedAt": "2026-08-26T09:00:00.000Z",
  "items": [{
    "schema": "dsh-sdd/source@1",
    "uid": "company-alm:requirement:PAY-381",
    "provider": "company-alm",
    "kind": "requirement",
    "externalKey": "PAY-381",
    "title": "支持订单部分退款",
    "revision": "42",
    "fetchedAt": "2026-08-26T09:00:00.000Z",
    "content": { "description": "允许针对订单中的部分商品退款。" }
  }],
  "relations": []
}

核心字段:

  • uid 是这份外部记录的稳定身份,建议包含系统、类型和编号,不要包含版本或随机值。
  • externalKey 保留业务系统原始单号。
  • revision 用于识别外部版本变化。
  • content 保留 AI 整合交付件所需的原始事实,不要在适配器里生成 SDD 结论。
  • tracking.status 保留企业原始状态;normalizedStatus 映射到 todoin-progressresolveddonecancelledblocked
  • fetchedAt 表示本次读取时间,不是业务单创建时间。

插件会校验输出并计算 contentHash,然后冻结为 .sdd/sources/*.yaml

一个主 ID 返回多个子需求

子需求可独立设计、开发和验收时,在同一个 items 数组返回多项:

{
  "schema": "dsh-sdd/source-bundle@1",
  "uid": "company-alm:bundle:EPIC-100",
  "provider": "company-alm",
  "kind": "requirement",
  "externalKey": "EPIC-100",
  "title": "支付能力升级",
  "fetchedAt": "2026-08-26T09:00:00.000Z",
  "root": { "schema": "dsh-sdd/source@1", "uid": "company-alm:requirement:EPIC-100", "provider": "company-alm", "kind": "requirement", "externalKey": "EPIC-100", "title": "支付能力升级", "fetchedAt": "2026-08-26T09:00:00.000Z", "content": { "goal": "统一支付能力" } },
  "items": [
    { "schema": "dsh-sdd/source@1", "uid": "company-alm:requirement:REQ-101", "provider": "company-alm", "kind": "requirement", "externalKey": "REQ-101", "title": "微信支付", "fetchedAt": "2026-08-26T09:00:00.000Z", "content": { "description": "..." } },
    { "schema": "dsh-sdd/source@1", "uid": "company-alm:requirement:REQ-102", "provider": "company-alm", "kind": "requirement", "externalKey": "REQ-102", "title": "支付宝", "fetchedAt": "2026-08-26T09:00:00.000Z", "content": { "description": "..." } }
  ],
  "relations": [
    { "from": "REQ-101", "to": "EPIC-100", "type": "child-of" },
    { "from": "REQ-102", "to": "EPIC-100", "type": "child-of" }
  ]
}

root 是可选的公共业务背景;items 中每一项的执行归属由页面入口决定:从项目看板导入时形成独立 SDD 工作单元,从当前需求内导入缺陷时保留独立来源记录、但挂到该需求的交付流程。适配器不需要返回 BUG -> REQ 关系,也不应自行决定归属。适配器每次同步必须返回主 ID 当前完整的子项集合,这样核心才能识别已从外部删除的子项。

再次输入同一个主 ID 会生成同步预览:

  • added:新建工作单元。
  • modified:保存新来源快照并标记受影响阶段重新评审。
  • removed:标记为外部已移除,负责人再选择保留本地继续推进或归档;禁止静默删除本地成果。
  • unchanged:不写文件。

稳定身份应由 provider + kind + externalKey 决定;uid 不应包含随机值。revision 和内容可变化,历史快照由核心保存。

Node.js 示例

business/adapters/fetch-company-alm.mjs.sdd/business/adapters/fetch-company-alm.mjs

let input = ''
for await (const chunk of process.stdin) input += chunk

const request = JSON.parse(input)
if (request.operation !== 'get') throw new Error('unsupported operation')
if (!['requirement', 'defect'].includes(request.kind)) throw new Error('unsupported kind')

const baseUrl = process.env.COMPANY_ALM_URL
const token = process.env.COMPANY_ALM_TOKEN
if (!baseUrl || !token) throw new Error('missing COMPANY_ALM_URL or COMPANY_ALM_TOKEN')

const response = await fetch(`${baseUrl}/api/items/${encodeURIComponent(request.key)}`, {
  headers: { authorization: `Bearer ${token}` },
})
if (!response.ok) throw new Error(`ALM returned ${response.status}`)
const item = await response.json()

const normalizedStatus = {
  新建: 'todo',
  研发中: 'in-progress',
  已解决: 'resolved',
  已关闭: 'done',
}[item.status]

const revision = String(item.version ?? item.updatedAt)
const source = {
  schema: 'dsh-sdd/source@1',
  uid: `company-alm:${request.kind}:${item.key}`,
  provider: 'company-alm',
  kind: request.kind,
  externalKey: item.key,
  title: item.title,
  status: item.status,
  revision,
  fetchedAt: new Date().toISOString(),
  tracking: {
    status: item.status,
    normalizedStatus,
    priority: item.priority,
    severity: item.severity,
    assignees: item.assignees ?? [],
    estimate: item.storyPoints == null ? undefined : { value: item.storyPoints, unit: 'point' },
    createdAt: item.createdAt,
    updatedAt: item.updatedAt,
    resolvedAt: item.resolvedAt,
  },
  content: {
    description: item.description,
    acceptanceCriteria: item.acceptanceCriteria,
    businessRules: item.businessRules,
    attachments: item.attachments,
  },
  links: item.url ? [{ url: item.url }] : [],
}

process.stdout.write(JSON.stringify({
  schema: 'dsh-sdd/source-bundle@1',
  uid: `company-alm:bundle:${request.key}`,
  provider: 'company-alm',
  kind: request.kind,
  externalKey: request.key,
  title: source.title,
  fetchedAt: new Date().toISOString(),
  items: [source],
  relations: [],
}))

项目绑定

.sdd/project.yaml 中配置默认来源:

sources:
  requirement:
    provider: command
    connector: company-alm
  defect:
    provider: command
    connector: company-alm

Provider 使用内置的 command。核心按 Connector ID 合并插件和项目目录,同名时选择项目版本,因此项目配置不需要记录物理路径或部署范围。

本地调试

先直接运行适配器:

macOS/Linux:

printf '%s' '{"operation":"get","kind":"requirement","key":"PAY-381"}' \
  | node .sdd/business/adapters/fetch-company-alm.mjs \
  | jq .

Windows PowerShell:

'{"operation":"get","kind":"requirement","key":"PAY-381"}' |
  node .sdd/business/adapters/fetch-company-alm.mjs |
  ConvertFrom-Json |
  ConvertTo-Json -Depth 20

检查以下内容:

  • stdout 能被解析为单个 JSON 对象。
  • schemauidproviderkindtitlefetchedAtcontent 存在。
  • provider 与 Connector ID 一致,或暂时填写 command
  • kind 与请求完全一致。
  • 错误场景使用非零退出码,且 stdout 不混入日志。
  • 同一外部版本能生成相同的稳定 uid

随后从 DSH 页面执行一次导入,逐条打开详情确认完整正文,检查预览数量正确、.sdd/sources/ 中生成快照、.sdd/work-items/ 中生成工作单元。还应分别验证项目看板导入缺陷会生成独立流程、需求内导入缺陷会记录 parentWorkItemUid 且不增加五阶段矩阵行。修改一个子项并再次同步,确认它显示为“有变更”而不是重复新增。

安全要求

  • 凭证只通过环境变量或企业凭证服务注入。
  • 不在 Connector 的 command 中拼接用户输入。
  • 不启用 shell,不执行外部系统写操作。
  • 对外部响应设置字段和大小限制,避免把无关敏感数据带入项目仓库。
  • content 中如包含个人信息或密钥,必须在适配器中脱敏。
  • 业务适配器默认只读;创建、修改、关闭外部单据应设计成另一套需要用户确认的写能力。

从项目适配提升为源码适配

项目验证完成后,可以把 .sdd/business/connectors/<id>.yaml 和对应 Adapter 原样移动到插件 business/。不需要改写 Class、输入协议或输出协议;重新构建并安装企业维护的插件版本后,所有项目都能选择该 Connector。项目需要特殊行为时,可在自己的 .sdd/business/ 放置同名文件覆盖。

无论放在哪个范围,外部数据都必须先转换为 Source Envelope,不能由适配器直接创建 accepted 交付件。