业务适配开发指南
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/是插件生成的只读来源快照,不放业务代码。
最小接入流程
- 决定复用范围:企业通用适配放插件
business/,项目专用适配放.sdd/business/。 - 在对应
adapters/编写读取企业系统的脚本。 - 在同一范围的
connectors/声明启动命令、超时和允许继承的环境变量。 - 在
.sdd/project.yaml为requirement、defect等来源类型配置默认 Connector。 - 在命令行完成协议测试。
- 启动 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 | 来源类型,例如 requirement、defect |
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映射到todo、in-progress、resolved、done、cancelled或blocked。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 对象。
schema、uid、provider、kind、title、fetchedAt和content存在。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 交付件。