sdd-flow-kit 1.3.13 → 1.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/prdArtifacts.js +29 -0
- package/dist/core/writeNext.js +1 -1
- package/package.json +1 -1
- package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +13 -3
- package/src/templates/skills/__pycache__/confluence-doc.py.cpython-314.pyc +0 -0
- package/src/templates/skills/confluence-doc.py.template +84 -18
- package/src/templates/skills/doc-skill.SKILL.md.template +4 -2
|
@@ -16,6 +16,31 @@ const PRD_PLACEHOLDER_MARKERS = [
|
|
|
16
16
|
"占位",
|
|
17
17
|
"placeholder",
|
|
18
18
|
];
|
|
19
|
+
const PRD_TECHNICAL_TITLE_MARKERS = [
|
|
20
|
+
"技术文档",
|
|
21
|
+
"技术方案",
|
|
22
|
+
"详细设计",
|
|
23
|
+
"接口文档",
|
|
24
|
+
"后端",
|
|
25
|
+
"数据库",
|
|
26
|
+
"实现文档",
|
|
27
|
+
"设计文档",
|
|
28
|
+
"开发文档",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* 判断 PRD 标题是否属于技术/后端/设计类文档,避免错误进入需求分析链路。
|
|
32
|
+
*/
|
|
33
|
+
function hasForbiddenPrdTitle(title) {
|
|
34
|
+
const upper = (title || "").toUpperCase();
|
|
35
|
+
return PRD_TECHNICAL_TITLE_MARKERS.some((marker) => upper.includes(marker.toUpperCase()));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 从 Markdown 主标题读取 Confluence 页面标题,作为 prd-fetched 的类型校验入口。
|
|
39
|
+
*/
|
|
40
|
+
function extractPrdTitle(content) {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
return (_b = (_a = content.split("\n").find((line) => line.trim().startsWith("# "))) === null || _a === void 0 ? void 0 : _a.replace(/^#\s*/, "").trim()) !== null && _b !== void 0 ? _b : "";
|
|
43
|
+
}
|
|
19
44
|
async function isPrdFetched(outputRoot) {
|
|
20
45
|
const prdPath = path_1.default.join(outputRoot, "source", "PRD.md");
|
|
21
46
|
const content = await (0, fs_1.readFileIfExists)(prdPath);
|
|
@@ -25,6 +50,10 @@ async function isPrdFetched(outputRoot) {
|
|
|
25
50
|
if (PRD_PLACEHOLDER_MARKERS.some((m) => content.includes(m))) {
|
|
26
51
|
return { ok: false, detail: "source/PRD.md 仍为占位内容,请先执行 Step1 拉取 PRD" };
|
|
27
52
|
}
|
|
53
|
+
const title = extractPrdTitle(content);
|
|
54
|
+
if (title && hasForbiddenPrdTitle(title)) {
|
|
55
|
+
return { ok: false, detail: `source/PRD.md 命中技术文档标题: ${title}` };
|
|
56
|
+
}
|
|
28
57
|
return { ok: true, detail: "PRD 已回填" };
|
|
29
58
|
}
|
|
30
59
|
async function isQuestionsClosed(outputRoot) {
|
package/dist/core/writeNext.js
CHANGED
|
@@ -35,7 +35,7 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
|
|
|
35
35
|
"| `tasks.md` | AC 的可执行映射(`[AC-XX]`) |",
|
|
36
36
|
"",
|
|
37
37
|
"## 1. Step1 — 拉取 PRD",
|
|
38
|
-
"按 `01-获取需求文档指引.md` 执行 confluence
|
|
38
|
+
"按 `01-获取需求文档指引.md` 执行 confluence 脚本;脚本和 gate 会拒绝 `技术文档`/`后端`/`详细设计` 等非 PRD 页面,然后:",
|
|
39
39
|
"```bash",
|
|
40
40
|
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect prd-fetched`,
|
|
41
41
|
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to after-prd`,
|
package/package.json
CHANGED
|
@@ -14,11 +14,17 @@ python3 confluence-doc.py {{VERSION_EXAMPLE}}
|
|
|
14
14
|
|
|
15
15
|
2. 脚本会依次用关键词打开 **Confluence 专用搜索页** `dosearchsite.action?queryString=<关键词>`(非首页顶栏搜索),并依次尝试:`ADI_V*` → `ADI-V*` → `ADI_*` → `ADI-*` → `ADI_v*` → `ADI-v*` → `ADI V*` → `ADI_V*结算` → `V*` 等。
|
|
16
16
|
|
|
17
|
-
3.
|
|
17
|
+
3. 匹配规则:先收集 **标题包含关键词或以关键词为前缀**、或 **URL 路径包含关键词** 的候选页,再执行 PRD 类型校验。
|
|
18
18
|
|
|
19
|
-
4.
|
|
19
|
+
4. PRD 类型校验(强制黑名单):候选页必须通过脚本内 `is_prd_page()` 与 `has_forbidden_prd_title()` 双重校验。以下标题特征会被直接拒绝:
|
|
20
|
+
- `技术文档`、`技术方案`、`后端`、`详细设计`、`数据库`、`接口文档`
|
|
21
|
+
- `实现文档`、`设计文档`、`开发文档`
|
|
22
|
+
|
|
23
|
+
若所有候选均未通过校验,脚本必须失败退出(exit code 1),**禁止回退使用第一个非 PRD 候选**。AI 不得手动复制技术文档到 `source/PRD.md`。
|
|
20
24
|
|
|
21
|
-
5.
|
|
25
|
+
5. **禁止**在 Confluence 脚本未跑完或未确认失败前,用本地 Word/原型替代 `source/PRD.md`。
|
|
26
|
+
|
|
27
|
+
6. 成功后把生成的 `docs/*.md` **主文档**复制/回填到本 run 目录:
|
|
22
28
|
- `source/PRD.md`
|
|
23
29
|
|
|
24
30
|
## 直链兜底(可选,非默认)
|
|
@@ -49,6 +55,10 @@ python3 confluence-doc.py 2.3.4
|
|
|
49
55
|
## 失败排查
|
|
50
56
|
|
|
51
57
|
- 输出 `❌ 未找到`:到 Confluence 确认页面标题(ADI 常为 `ADI_V2.3.4结算、开票…`,**下划线**而非 `ADI-V`);可尝试输入更长关键词如 `ADI_V2.3.4结算`
|
|
58
|
+
- 输出 `✗ 命中技术文档标题,拒绝作为 PRD` 或 `跳过技术文档候选` 或 `⚠ 搜索结果均未通过 PRD 校验` 时:
|
|
59
|
+
- **禁止**手动复制技术/后端/设计页到 `source/PRD.md`
|
|
60
|
+
- 应在 Confluence 中找到真正的业务 PRD 页面(通常标题含「结算」「开票」「回款」「数据对接」等业务词汇)
|
|
61
|
+
- 使用该页面的 pageId 或 URL 直链重跑:`CONFLUENCE_PAGE_ID=<正确pageId> python3 confluence-doc.py 2.3.4`
|
|
52
62
|
- 页面存在但搜不到:加 `CONFLUENCE_DEBUG=1` 查看选择器解析日志;或(可选)设置 `CONFLUENCE_PAGE_ID` / `CONFLUENCE_PAGE_URL` 直链下载
|
|
53
63
|
- Playwright 失败且设置 `CONFLUENCE_USE_REST=1` 时才回退 Token;登录最多重试 3 次,并用 `expect_navigation` 避免竞态
|
|
54
64
|
- 登录偶发失败:用 `CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500` 有头观察;失败截图 `confluence-login-fail-*.png`
|
|
@@ -292,10 +292,36 @@ def read_breadcrumb_text(page) -> str:
|
|
|
292
292
|
return " > ".join(parts)
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
PRD_TECHNICAL_TITLE_MARKERS = (
|
|
296
|
+
"技术文档",
|
|
297
|
+
"技术方案",
|
|
298
|
+
"详细设计",
|
|
299
|
+
"接口文档",
|
|
300
|
+
"后端",
|
|
301
|
+
"数据库",
|
|
302
|
+
"实现文档",
|
|
303
|
+
"设计文档",
|
|
304
|
+
"开发文档",
|
|
305
|
+
"API文档",
|
|
306
|
+
"数据结构",
|
|
307
|
+
"表设计",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def has_forbidden_prd_title(title: str) -> bool:
|
|
312
|
+
"""技术文档/后端设计页必须在 PRD 拉取阶段直接拦截。"""
|
|
313
|
+
title_upper = (title or "").upper()
|
|
314
|
+
return any(marker.upper() in title_upper for marker in PRD_TECHNICAL_TITLE_MARKERS)
|
|
315
|
+
|
|
316
|
+
|
|
295
317
|
def is_adi_requirement_page(title: str, crumbs: str, page_url: str) -> bool:
|
|
296
318
|
"""ADI 需求页常无 PRD 字样,标题形如 ADI_V2.3.4结算、开票、回款数据对接欢聚。"""
|
|
297
319
|
title_upper = (title or "").upper()
|
|
298
320
|
crumbs_upper = (crumbs or "").upper()
|
|
321
|
+
|
|
322
|
+
if has_forbidden_prd_title(title):
|
|
323
|
+
print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {title[:80]}")
|
|
324
|
+
return False
|
|
299
325
|
if re.search(r"ADI[_-]?V\d+\.\d+\.\d+", title_upper):
|
|
300
326
|
print(f" ✓ ADI 版本标题命中: {title[:80]}")
|
|
301
327
|
return True
|
|
@@ -326,6 +352,10 @@ def is_prd_page(page, page_url: str) -> bool:
|
|
|
326
352
|
except Exception:
|
|
327
353
|
title = ""
|
|
328
354
|
|
|
355
|
+
if has_forbidden_prd_title(title):
|
|
356
|
+
print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {title[:80]}")
|
|
357
|
+
return False
|
|
358
|
+
|
|
329
359
|
# ADI:标题常无「PRD/需求」字样,单独识别
|
|
330
360
|
if prefix == "ADI" and is_adi_requirement_page(title, crumbs, page_url):
|
|
331
361
|
return True
|
|
@@ -445,7 +475,9 @@ def rest_search_candidates(
|
|
|
445
475
|
for item in data.get("results", []):
|
|
446
476
|
title = (item.get("title") or "").strip()
|
|
447
477
|
page_id = str(item.get("id", ""))
|
|
448
|
-
if not page_id:
|
|
478
|
+
if not page_id or has_forbidden_prd_title(title):
|
|
479
|
+
if title and has_forbidden_prd_title(title):
|
|
480
|
+
print(f" 跳过技术文档候选: {title[:80]}")
|
|
449
481
|
continue
|
|
450
482
|
href = f"/spaces/{(item.get('space') or {}).get('key', '')}/pages/{page_id}/"
|
|
451
483
|
score = score_link_match(title, href, prefix, version, keyword)
|
|
@@ -462,10 +494,13 @@ def rest_resolve_target(
|
|
|
462
494
|
direct_id = resolve_direct_page_id()
|
|
463
495
|
if direct_id:
|
|
464
496
|
page = rest_fetch_page(base_url, token, direct_id)
|
|
497
|
+
if has_forbidden_prd_title(page["title"]):
|
|
498
|
+
print(f"❌ 直链页面不是业务 PRD,命中技术文档黑名单: {page['title']}")
|
|
499
|
+
print(" 请确认提供的 pageId 是业务需求文档,而非技术文档/后端设计文档")
|
|
500
|
+
raise RuntimeError(f"直链页面不是 PRD,命中技术文档标题: {page['title']}")
|
|
465
501
|
kw = keywords[0] if keywords else f"{prefix}_V{version}"
|
|
466
502
|
return page["id"], page["title"], kw, page["url"]
|
|
467
503
|
|
|
468
|
-
fallback: tuple[str, str, str, str] | None = None
|
|
469
504
|
for kw in keywords:
|
|
470
505
|
print(f"\n🔍 [REST] 搜索: {kw}")
|
|
471
506
|
candidates = rest_search_candidates(base_url, token, prefix, version, kw)
|
|
@@ -475,17 +510,25 @@ def rest_resolve_target(
|
|
|
475
510
|
for score, page_id, title in candidates:
|
|
476
511
|
print(f" 候选 (score={score}): {title[:80]}")
|
|
477
512
|
page = rest_fetch_page(base_url, token, page_id)
|
|
478
|
-
|
|
513
|
+
# 双重校验:标题黑名单 + ADI 特殊规则
|
|
514
|
+
if has_forbidden_prd_title(page["title"]):
|
|
515
|
+
print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {page['title'][:80]}")
|
|
516
|
+
continue
|
|
517
|
+
if prefix.upper() == "ADI" and is_adi_title_match(page["title"], version):
|
|
518
|
+
print(f" ✓ ADI 业务 PRD 命中")
|
|
479
519
|
return page_id, page["title"], kw, page["url"]
|
|
480
520
|
if score >= 4:
|
|
481
521
|
return page_id, page["title"], kw, page["url"]
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
522
|
+
print(" ⚠ REST 搜索结果均未通过 PRD 校验")
|
|
523
|
+
print(" → 所有候选页面要么是技术/后端/设计文档,要么评分不足")
|
|
524
|
+
print(" → 不回退到非 PRD 候选;请确认业务需求文档的 pageId")
|
|
525
|
+
return None
|
|
485
526
|
|
|
486
527
|
|
|
487
528
|
def is_adi_title_match(title: str, version: str) -> bool:
|
|
488
529
|
"""ADI 长标题页识别(面包屑可无 PRD 字样)。"""
|
|
530
|
+
if has_forbidden_prd_title(title):
|
|
531
|
+
return False
|
|
489
532
|
title_upper = (title or "").upper()
|
|
490
533
|
v_compact = version.replace(".", "")
|
|
491
534
|
if re.search(rf"ADI[_-]?V{re.escape(version)}", title_upper):
|
|
@@ -702,7 +745,6 @@ def resolve_prd_target(
|
|
|
702
745
|
) -> tuple[str | None, str | None, str]:
|
|
703
746
|
"""依次用每个关键词搜索,直到找到父级为 PRD 的页面。"""
|
|
704
747
|
seen_urls: set[str] = set()
|
|
705
|
-
fallback: tuple[str, str, str] | None = None # url, title, keyword
|
|
706
748
|
|
|
707
749
|
for kw in keywords:
|
|
708
750
|
print(f"\n🔍 搜索关键词: {kw}")
|
|
@@ -720,15 +762,16 @@ def resolve_prd_target(
|
|
|
720
762
|
continue
|
|
721
763
|
seen_urls.add(url)
|
|
722
764
|
print(f" · 候选: {title[:70]}")
|
|
765
|
+
# 先执行标题级黑名单快速拒绝
|
|
766
|
+
if has_forbidden_prd_title(title):
|
|
767
|
+
print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {title[:80]}")
|
|
768
|
+
continue
|
|
723
769
|
if is_prd_page(page, url):
|
|
724
770
|
return url, kw, title
|
|
725
|
-
if fallback is None:
|
|
726
|
-
fallback = (url, kw, title)
|
|
727
771
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return url, kw, title
|
|
772
|
+
print(" ⚠ 搜索结果均未通过 PRD 校验")
|
|
773
|
+
print(" → 所有候选页面要么是技术/后端/设计文档,要么面包屑不含 PRD 父级")
|
|
774
|
+
print(" → 不回退到非 PRD 候选;请确认业务需求文档的 Confluence 标题与 pageId")
|
|
732
775
|
return None, None, ""
|
|
733
776
|
|
|
734
777
|
|
|
@@ -919,8 +962,12 @@ def main():
|
|
|
919
962
|
return
|
|
920
963
|
|
|
921
964
|
print("\n❌ PRD 拉取失败:Playwright 与 REST 均未成功。")
|
|
922
|
-
print(" 排查:
|
|
923
|
-
print("
|
|
965
|
+
print(" 排查:")
|
|
966
|
+
print(" 1. CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500 有头模式观察登录与搜索结果")
|
|
967
|
+
print(" 2. 到 Confluence 确认业务 PRD 页面的准确标题与 pageId")
|
|
968
|
+
print(" 3. 若搜索命中了技术/后端/设计文档,这些已被黑名单拒绝")
|
|
969
|
+
print(" 4. 使用业务 PRD 的直链重跑:CONFLUENCE_PAGE_ID=<pageId> python3 confluence-doc.py <版本>")
|
|
970
|
+
print(" 5. 禁止手动复制非 PRD 页面到 source/PRD.md")
|
|
924
971
|
sys.exit(1)
|
|
925
972
|
|
|
926
973
|
|
|
@@ -976,23 +1023,42 @@ def _download_via_playwright(
|
|
|
976
1023
|
print(f" 已保存调试截图: {debug_path}")
|
|
977
1024
|
except Exception:
|
|
978
1025
|
pass
|
|
979
|
-
print(f"❌ 未找到 {prefix} {version}
|
|
1026
|
+
print(f"❌ 未找到 {prefix} {version} 的业务需求文档(PRD)。")
|
|
980
1027
|
print(
|
|
981
1028
|
" 排查:1) 页面标题是否含 ADI_V2.3.4(下划线,非 ADI-V)及中文后缀"
|
|
982
1029
|
)
|
|
983
1030
|
print(
|
|
984
1031
|
" 2) 依次尝试 ADI_V2.3.4 / ADI-V2.3.4 / ADI_V2.3.4结算 / V2.3.4"
|
|
985
1032
|
)
|
|
986
|
-
print(" 3)
|
|
987
|
-
print(" 4)
|
|
1033
|
+
print(" 3) 若搜索命中了「技术文档」「后端」「详细设计」等页面,这些已被黑名单自动拒绝")
|
|
1034
|
+
print(" 4) 直链解决:CONFLUENCE_PAGE_ID=<正确pageId> 或 CONFLUENCE_PAGE_URL=<完整URL>")
|
|
1035
|
+
print(" 5) ADI 页面包屑可能无 PRD 字样,脚本已按 ADI_V* 标题规则识别")
|
|
1036
|
+
print(" 6) 禁止手动复制技术文档到 source/PRD.md,必须找到真正的业务 PRD")
|
|
988
1037
|
return False
|
|
989
1038
|
|
|
990
1039
|
print(f"✅ 命中关键词: {matched_keyword}")
|
|
991
1040
|
if matched_title:
|
|
992
1041
|
print(f" 页面: {matched_title[:100]}")
|
|
993
1042
|
print(f" URL: {target_url[:120]}...")
|
|
1043
|
+
|
|
1044
|
+
# 直链模式下也需检查标题黑名单
|
|
1045
|
+
if direct_url:
|
|
1046
|
+
page.goto(target_url, wait_until="domcontentloaded", timeout=60000)
|
|
1047
|
+
try:
|
|
1048
|
+
direct_title = page.locator("#title-text").text_content().strip()
|
|
1049
|
+
if has_forbidden_prd_title(direct_title):
|
|
1050
|
+
print(f"❌ 直链页面不是业务 PRD,命中技术文档黑名单: {direct_title}")
|
|
1051
|
+
print(" 请确认提供的 pageId/URL 是业务需求文档,而非技术文档/后端设计文档")
|
|
1052
|
+
raise RuntimeError("直链页面命中技术文档标题,拒绝作为 PRD")
|
|
1053
|
+
except Exception as e:
|
|
1054
|
+
if "技术文档" in str(e) or "黑名单" in str(e):
|
|
1055
|
+
raise
|
|
1056
|
+
|
|
994
1057
|
page.goto(target_url, wait_until="networkidle", timeout=60000)
|
|
995
1058
|
|
|
1059
|
+
if not is_prd_page(page, target_url):
|
|
1060
|
+
raise RuntimeError("命中的页面未通过 PRD 校验,疑似技术文档或设计文档")
|
|
1061
|
+
|
|
996
1062
|
try:
|
|
997
1063
|
title = page.locator("#title-text").text_content().strip()
|
|
998
1064
|
except Exception:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: {{SKILL_NAME}}
|
|
3
3
|
description: |
|
|
4
4
|
当用户说「获取 {{PRODUCT}} Vx.y.z 需求文档」「下载 {{PRODUCT}}-Vx.y.z PRD」「查询 {{PRODUCT}} x.y.z 版本需求」时触发。
|
|
5
|
-
自动从 Confluence 搜索对应版本 PRD
|
|
5
|
+
自动从 Confluence 搜索对应版本 PRD 页面;`技术文档`、`后端`、`详细设计` 类页面会被拒绝,避免误下载为 PRD;下载图片并生成 Markdown 到本仓库 `docs/`。
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# {{PRODUCT}} 需求文档获取(docs/{{SKILL_DIR}})
|
|
@@ -60,7 +60,9 @@ python3 confluence-doc.py {{PRODUCT_PREFIX}}_V1.2.3
|
|
|
60
60
|
4. `{{PRODUCT_PREFIX}}-<版本>`(如 ADI-2.3.2)
|
|
61
61
|
5. `ADI_V<版本>结算`(ADI 长标题页,如 V2.3.4 结算对接欢聚)
|
|
62
62
|
|
|
63
|
-
每个关键词优先走 **`dosearchsite.action?queryString=...` 专用搜索页**(比首页 `#all-updates`
|
|
63
|
+
每个关键词优先走 **`dosearchsite.action?queryString=...` 专用搜索页**(比首页 `#all-updates` 顶栏搜索稳定);无结果时再兜底顶栏搜索。匹配时先收集 **标题或 URL 路径** 命中的候选,再执行 PRD 类型校验;`技术文档`、`技术方案`、`后端`、`详细设计`、`数据库`、`接口文档` 等页面会被直接拒绝。ADI 页按 `ADI_V*` 标题规则识别(面包屑可无 PRD 字样)。
|
|
64
|
+
|
|
65
|
+
若所有候选均未通过 PRD 校验,脚本必须失败退出,**禁止回退使用第一个非 PRD 候选**;此时应改用正确 PRD 的 `CONFLUENCE_PAGE_ID` / `CONFLUENCE_PAGE_URL` 直链。
|
|
64
66
|
|
|
65
67
|
调试搜索解析:
|
|
66
68
|
|