kld-sdd 2.4.10 → 2.4.11
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/README.md +9 -6
- package/lib/init.js +67 -12
- package/lib/skills-bundle.js +32 -0
- package/package.json +2 -2
- package/templates/skills/kld-sdd/SKILLS.md +28 -0
- package/templates/skills/{opsx-apply → kld-sdd/opsx-apply}/SKILL.md +4 -0
- package/templates/skills/{opsx-design → kld-sdd/opsx-design}/SKILL.md +4 -0
- package/templates/skills/kld-sdd/opsx-knowledge/SKILL.md +132 -0
- package/templates/skills/kld-sdd/opsx-knowledge/references/modules.md +26 -0
- package/templates/skills/kld-sdd/opsx-knowledge/scripts/config.json +39 -0
- package/templates/skills/kld-sdd/opsx-knowledge/scripts/retrieve.cjs +199 -0
- package/templates/skills/{opsx-propose → kld-sdd/opsx-propose}/SKILL.md +4 -0
- package/templates/skills/{opsx-spec → kld-sdd/opsx-spec}/SKILL.md +4 -0
- /package/templates/skills/{opsx-archive → kld-sdd/opsx-archive}/SKILL.md +0 -0
- /package/templates/skills/{opsx-check → kld-sdd/opsx-check}/SKILL.md +0 -0
- /package/templates/skills/{opsx-explore → kld-sdd/opsx-explore}/SKILL.md +0 -0
- /package/templates/skills/{opsx-task → kld-sdd/opsx-task}/SKILL.md +0 -0
- /package/templates/skills/{opsx-test → kld-sdd/opsx-test}/SKILL.md +0 -0
package/README.md
CHANGED
|
@@ -136,9 +136,9 @@ propose → spec → design → task → check
|
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
-
## 预置技能:`
|
|
139
|
+
## 预置技能:`kld-sdd`
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
初始化后,SDD 全套技能会统一打包部署到 `.*/skills/kld-sdd/`(与 `gitnexus/` 目录模式一致),包含 `opsx-propose`、`opsx-spec`、`opsx-design`、`opsx-task`、`opsx-check`、`opsx-apply`、`opsx-test`、`opsx-archive`、`opsx-explore`、`opsx-knowledge` 等子技能。
|
|
142
142
|
|
|
143
143
|
激活后,AI 会化身 **SDD 工作流专家**,检测当前项目的文档状态,引导你从当前进度继续:
|
|
144
144
|
|
|
@@ -190,16 +190,19 @@ your-project/
|
|
|
190
190
|
│ └── task.md
|
|
191
191
|
├── .cursor/
|
|
192
192
|
│ ├── commands/opsx/ # opsx 命令(9个)
|
|
193
|
-
│ └── skills/
|
|
193
|
+
│ └── skills/kld-sdd/ # SDD 技能统一目录
|
|
194
|
+
│ ├── opsx-propose/
|
|
195
|
+
│ ├── opsx-spec/
|
|
196
|
+
│ └── ... # 其他 opsx-* 子技能
|
|
194
197
|
├── .claude/
|
|
195
198
|
│ ├── commands/opsx/ # opsx 命令(9个)
|
|
196
|
-
│ └── skills/
|
|
199
|
+
│ └── skills/kld-sdd/ # SDD 技能统一目录
|
|
197
200
|
├── .codebuddy/
|
|
198
201
|
│ ├── commands/opsx/ # opsx 命令(9个)
|
|
199
|
-
│ └── skills/
|
|
202
|
+
│ └── skills/kld-sdd/ # SDD 技能统一目录
|
|
200
203
|
└── .agents/
|
|
201
204
|
├── commands/opsx/ # Codex opsx 命令(9个)
|
|
202
|
-
└── skills/
|
|
205
|
+
└── skills/kld-sdd/ # Codex 项目级技能
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
---
|
package/lib/init.js
CHANGED
|
@@ -327,9 +327,7 @@ function copyDir(source, target) {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
function renderTemplate(content, config, toolKey) {
|
|
330
|
-
return content
|
|
331
|
-
.replace(/^\uFEFF/, '')
|
|
332
|
-
.replace(/<Agent(?:\u7c7b\u578b|\u7eeb\u8bf2\u7037)>/g, config.agentType || toolKey);
|
|
330
|
+
return require('./skills-bundle').renderTemplate(content, config, toolKey);
|
|
333
331
|
}
|
|
334
332
|
|
|
335
333
|
function copyDirRendered(source, target, config, toolKey) {
|
|
@@ -504,9 +502,50 @@ function overrideOpsxCommands(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
|
504
502
|
return true;
|
|
505
503
|
}
|
|
506
504
|
|
|
505
|
+
const {
|
|
506
|
+
SKILLS_BUNDLE_NAME,
|
|
507
|
+
OPSX_SKILL_DIRS,
|
|
508
|
+
renderTemplate
|
|
509
|
+
} = require('./skills-bundle');
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 清理旧版扁平 opsx-* skills(迁移到 skills/kld-sdd/ 统一目录)
|
|
513
|
+
* @param {string[]} selectedTools - 用户选择的编辑器列表
|
|
514
|
+
*/
|
|
515
|
+
function cleanupLegacyFlatOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
516
|
+
console.log('🧹 正在清理旧版扁平 opsx-* skills...');
|
|
517
|
+
|
|
518
|
+
const cwd = process.cwd();
|
|
519
|
+
const legacySkillDirs = [...OPSX_SKILL_DIRS, 'opsx-sdd'];
|
|
520
|
+
|
|
521
|
+
for (const toolKey of selectedTools) {
|
|
522
|
+
const config = TOOL_CONFIGS[toolKey];
|
|
523
|
+
if (!config || !config.skillsDir) continue;
|
|
524
|
+
|
|
525
|
+
const skillsDir = path.join(cwd, config.skillsDir);
|
|
526
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
527
|
+
|
|
528
|
+
let removed = 0;
|
|
529
|
+
for (const legacySkill of legacySkillDirs) {
|
|
530
|
+
const legacySkillDir = path.join(skillsDir, legacySkill);
|
|
531
|
+
if (fs.existsSync(legacySkillDir)) {
|
|
532
|
+
fs.rmSync(legacySkillDir, { recursive: true, force: true });
|
|
533
|
+
removed++;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (removed > 0) {
|
|
538
|
+
console.log(` 🗑 ${config.name}: 删除 ${removed} 个旧版扁平 opsx skills`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log('✅ 旧版扁平 opsx skills 清理完成');
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
|
|
507
546
|
/**
|
|
508
|
-
* 部署 SDD opsx skills
|
|
509
|
-
* 从 templates/skills/
|
|
547
|
+
* 部署 SDD opsx skills
|
|
548
|
+
* 从 templates/skills/kld-sdd/ 复制到各编辑器的 skills/kld-sdd/ 目录
|
|
510
549
|
* @param {string[]} selectedTools - 用户选择的编辑器列表
|
|
511
550
|
*/
|
|
512
551
|
function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
@@ -515,14 +554,14 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
|
515
554
|
const pkgPath = getPackagePath();
|
|
516
555
|
const cwd = process.cwd();
|
|
517
556
|
|
|
518
|
-
const skillsTemplatePath = path.join(pkgPath, 'templates', 'skills');
|
|
557
|
+
const skillsTemplatePath = path.join(pkgPath, 'templates', 'skills', SKILLS_BUNDLE_NAME);
|
|
519
558
|
|
|
520
559
|
if (!fs.existsSync(skillsTemplatePath)) {
|
|
521
560
|
console.log(`⚠️ SDD skills 模板目录不存在: ${skillsTemplatePath}`);
|
|
522
561
|
return false;
|
|
523
562
|
}
|
|
524
563
|
|
|
525
|
-
//
|
|
564
|
+
// 获取 bundle 内所有 skill 目录
|
|
526
565
|
const skillDirs = fs.readdirSync(skillsTemplatePath).filter(d => {
|
|
527
566
|
return fs.statSync(path.join(skillsTemplatePath, d)).isDirectory();
|
|
528
567
|
});
|
|
@@ -537,17 +576,21 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
|
537
576
|
if (!config || !config.skillsDir) continue;
|
|
538
577
|
|
|
539
578
|
const targetSkillsDir = path.join(cwd, config.skillsDir);
|
|
579
|
+
const targetBundleDir = path.join(targetSkillsDir, SKILLS_BUNDLE_NAME);
|
|
540
580
|
|
|
541
581
|
// 确保 skills 目录存在
|
|
542
582
|
if (!fs.existsSync(targetSkillsDir)) {
|
|
543
583
|
fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
544
584
|
}
|
|
585
|
+
if (!fs.existsSync(targetBundleDir)) {
|
|
586
|
+
fs.mkdirSync(targetBundleDir, { recursive: true });
|
|
587
|
+
}
|
|
545
588
|
|
|
546
|
-
console.log(` 部署 ${config.name} 的
|
|
589
|
+
console.log(` 部署 ${config.name} 的 ${SKILLS_BUNDLE_NAME} skills(${skillDirs.length} 个)...`);
|
|
547
590
|
|
|
548
591
|
for (const skillDir of skillDirs) {
|
|
549
592
|
const sourceDir = path.join(skillsTemplatePath, skillDir);
|
|
550
|
-
const targetDir = path.join(
|
|
593
|
+
const targetDir = path.join(targetBundleDir, skillDir);
|
|
551
594
|
|
|
552
595
|
if (!fs.existsSync(targetDir)) {
|
|
553
596
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -555,9 +598,18 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
|
|
|
555
598
|
|
|
556
599
|
if (fs.existsSync(sourceDir)) {
|
|
557
600
|
copyDirRendered(sourceDir, targetDir, config, toolKey);
|
|
558
|
-
console.log(` ✓ ${skillDir}`);
|
|
601
|
+
console.log(` ✓ ${SKILLS_BUNDLE_NAME}/${skillDir}`);
|
|
559
602
|
}
|
|
560
603
|
}
|
|
604
|
+
|
|
605
|
+
// 复制 bundle 根目录说明文件(如 SKILLS.md)
|
|
606
|
+
for (const file of fs.readdirSync(skillsTemplatePath)) {
|
|
607
|
+
const sourceFile = path.join(skillsTemplatePath, file);
|
|
608
|
+
if (!fs.statSync(sourceFile).isFile()) continue;
|
|
609
|
+
const rendered = renderTemplate(fs.readFileSync(sourceFile, 'utf8'), config, toolKey);
|
|
610
|
+
fs.writeFileSync(path.join(targetBundleDir, file), rendered, 'utf8');
|
|
611
|
+
console.log(` ✓ ${SKILLS_BUNDLE_NAME}/${file}`);
|
|
612
|
+
}
|
|
561
613
|
}
|
|
562
614
|
|
|
563
615
|
console.log('✅ opsx skills 部署完成');
|
|
@@ -1019,10 +1071,13 @@ async function main() {
|
|
|
1019
1071
|
// 3. 部署 SDD opsx 命令(根据编辑器类型使用不同模板)
|
|
1020
1072
|
overrideOpsxCommands(selectedTools);
|
|
1021
1073
|
|
|
1022
|
-
// 4.
|
|
1074
|
+
// 4. 清理旧版扁平 opsx-* skills(迁移到 skills/kld-sdd/)
|
|
1075
|
+
cleanupLegacyFlatOpsxSkills(selectedTools);
|
|
1076
|
+
|
|
1077
|
+
// 4.1 部署 SDD opsx skills(统一打包到 skills/kld-sdd/)
|
|
1023
1078
|
deployOpsxSkills(selectedTools);
|
|
1024
1079
|
|
|
1025
|
-
// 4.
|
|
1080
|
+
// 4.2 清理原生 openspec-* skills(防止残留)
|
|
1026
1081
|
cleanupNativeOpenspecSkills(selectedTools);
|
|
1027
1082
|
|
|
1028
1083
|
// 4.2 部署 Claude Code Hook Pack(可选增强)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDD skills bundle 共享配置与模板渲染
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const SKILLS_BUNDLE_NAME = 'kld-sdd';
|
|
6
|
+
|
|
7
|
+
const OPSX_SKILL_DIRS = [
|
|
8
|
+
'opsx-propose',
|
|
9
|
+
'opsx-spec',
|
|
10
|
+
'opsx-design',
|
|
11
|
+
'opsx-task',
|
|
12
|
+
'opsx-check',
|
|
13
|
+
'opsx-apply',
|
|
14
|
+
'opsx-test',
|
|
15
|
+
'opsx-archive',
|
|
16
|
+
'opsx-explore',
|
|
17
|
+
'opsx-knowledge'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function renderTemplate(content, config, toolKey) {
|
|
21
|
+
const skillsBundlePath = `${config.skillsDir}/${SKILLS_BUNDLE_NAME}`;
|
|
22
|
+
return content
|
|
23
|
+
.replace(/^\uFEFF/, '')
|
|
24
|
+
.replace(/<Agent(?:\u7c7b\u578b|\u7eeb\u8bf2\u7037)>/g, config.agentType || toolKey)
|
|
25
|
+
.replace(/<SkillsBundlePath>/g, skillsBundlePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
SKILLS_BUNDLE_NAME,
|
|
30
|
+
OPSX_SKILL_DIRS,
|
|
31
|
+
renderTemplate
|
|
32
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kld-sdd",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.11",
|
|
4
4
|
"description": "KLD SDD OpenSpec 项目初始化工具 - 内置模版一键初始化",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"kld-sdd-init": "bin/kld-sdd-init.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "node test/validate-skills-bundle.cjs"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"kld",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# KLD SDD Skills Bundle
|
|
2
|
+
|
|
3
|
+
所有 SDD 技能统一打包在本目录,部署到 `<SkillsBundlePath>/`(如 `.claude/skills/kld-sdd/`)。
|
|
4
|
+
|
|
5
|
+
## 子技能索引
|
|
6
|
+
|
|
7
|
+
| 技能 name | 路径 | 用途 |
|
|
8
|
+
|-----------|------|------|
|
|
9
|
+
| `opsx-propose` | `opsx-propose/SKILL.md` | 业务意图文档 |
|
|
10
|
+
| `opsx-spec` | `opsx-spec/SKILL.md` | 技术契约 |
|
|
11
|
+
| `opsx-design` | `opsx-design/SKILL.md` | 技术方案 |
|
|
12
|
+
| `opsx-task` | `opsx-task/SKILL.md` | 任务拆解 |
|
|
13
|
+
| `opsx-check` | `opsx-check/SKILL.md` | 质量门禁 |
|
|
14
|
+
| `opsx-apply` | `opsx-apply/SKILL.md` | 变更实施 |
|
|
15
|
+
| `opsx-test` | `opsx-test/SKILL.md` | 单元测试 |
|
|
16
|
+
| `opsx-archive` | `opsx-archive/SKILL.md` | 变更归档 |
|
|
17
|
+
| `opsx-explore` | `opsx-explore/SKILL.md` | 变更浏览 |
|
|
18
|
+
| `opsx-knowledge` | `opsx-knowledge/SKILL.md` | RAGFlow 业务知识库(可选) |
|
|
19
|
+
|
|
20
|
+
## 路径约定
|
|
21
|
+
|
|
22
|
+
- 子 skill 内相对引用:如 `references/modules.md`、`scripts/retrieve.cjs`(相对于该 skill 目录)
|
|
23
|
+
- 跨 skill 引用:使用 frontmatter 中的 `name`(如 `opsx-knowledge`),或 bundle 路径 `<SkillsBundlePath>/opsx-knowledge/`
|
|
24
|
+
- 知识库脚本:`<SkillsBundlePath>/opsx-knowledge/scripts/retrieve.cjs`
|
|
25
|
+
|
|
26
|
+
## 与 /opsx:* 命令的关系
|
|
27
|
+
|
|
28
|
+
命令定义在 `.*/commands/opsx/*.md`;本 bundle 提供对应阶段的 skill 详细工作流。两者通过相同的 `name`(如 `opsx-propose`)关联,**不依赖扁平的 `skills/opsx-*` 顶层目录**。
|
|
@@ -122,6 +122,10 @@ openspec list --json
|
|
|
122
122
|
⛔ 隔离红线:禁止加载其他 Capability 的文档!
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
**【可选】业务知识库检索**:
|
|
126
|
+
编码中遇到不明业务名词时,可调用 **opsx-knowledge** skill(`<SkillsBundlePath>/opsx-knowledge/`)辅助理解。
|
|
127
|
+
不得因知识库建议偏离 tasks/spec;查询失败时继续实施。
|
|
128
|
+
|
|
125
129
|
### 3. 解析 DAG 任务拓扑
|
|
126
130
|
|
|
127
131
|
从 tasks.md 中:
|
|
@@ -94,6 +94,10 @@ openspec list
|
|
|
94
94
|
| 架构文档 | 了解系统边界、模块关系 | 对齐总体设计方向 |
|
|
95
95
|
| 数据库 Schema | 了解数据模型约束 | 纳入数据设计章节 |
|
|
96
96
|
|
|
97
|
+
**【可选】业务知识库检索**:
|
|
98
|
+
设计涉及 MM/CO 领域概念且 spec 未充分定义时,可调用 **opsx-knowledge** skill(`<SkillsBundlePath>/opsx-knowledge/`)。
|
|
99
|
+
仅作背景参考,不得覆盖 spec;内网不可达时跳过并继续 design。
|
|
100
|
+
|
|
97
101
|
### 3. 渐进式上下文加载
|
|
98
102
|
|
|
99
103
|
**⛔ 必须严格按以下顺序加载:**
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: opsx-knowledge
|
|
3
|
+
description: "Queries RAGFlow business knowledge base for MM/CO domain terms during SDD workflows. Use when encountering unclear business nouns, internal ERP terminology, MM/CO module concepts, or domain rules in propose/spec/design/apply. Results are advisory only and must not override SDD documents or user input."
|
|
4
|
+
argument-hint: "[question] [--module=mm|co|auto]"
|
|
5
|
+
license: MIT
|
|
6
|
+
metadata:
|
|
7
|
+
author: sdd-team
|
|
8
|
+
version: "1.0"
|
|
9
|
+
allowed-tools:
|
|
10
|
+
- Bash
|
|
11
|
+
- Read
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# SDD 业务知识库检索(RAGFlow)
|
|
15
|
+
|
|
16
|
+
为 SDD 工作流提供 **MM / CO 模块** 的业务知识检索。检索结果 **只做建议参考**,不得作为强制约束写入 spec 红线。
|
|
17
|
+
|
|
18
|
+
## RAGFlow 知识库配置
|
|
19
|
+
|
|
20
|
+
```env
|
|
21
|
+
RAGFLOW_BASE_URL=http://10.29.219.26:9380
|
|
22
|
+
RAGFLOW_API_KEY=ragflow-6xxy9RUUtRiE9N6gzpTLfQbY00tnGGKWsEuBR53zp0U
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| 模块 | dataset_id |
|
|
26
|
+
|------|------------|
|
|
27
|
+
| MM模块 | `12eda67a4ffb11f19cb713ede57b176f` |
|
|
28
|
+
| CO模块 | `02841d1e4ffb11f19cb713ede57b176f` |
|
|
29
|
+
|
|
30
|
+
配置已写死在 `scripts/config.json`,脚本会自动读取。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 使用原则(强制)
|
|
35
|
+
|
|
36
|
+
1. **可选**:仅当业务名词/领域含义不清时才查询
|
|
37
|
+
2. **非强制**:不得因知识库结果覆盖 proposal / spec / design / 用户确认内容
|
|
38
|
+
3. **冲突处理**:与用户输入、现有代码、openspec 文档冲突时,以用户 / 文档 / 代码为准
|
|
39
|
+
4. **引用格式**:输出中标注 `📚 知识库建议:...`,禁止写成“必须 / 规定 / 红线”
|
|
40
|
+
5. **失败降级**:内网不可达、超时、无结果时 **继续 SDD 主流程**,仅提示“未查询到知识库”
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 何时查询
|
|
45
|
+
|
|
46
|
+
**适合查询:**
|
|
47
|
+
- 不明业务名词(如“物料凭证”“评估类”“成本中心”)
|
|
48
|
+
- MM / CO 模块内部术语
|
|
49
|
+
- propose / spec / design 阶段需要补充领域背景
|
|
50
|
+
|
|
51
|
+
**不必查询:**
|
|
52
|
+
- 通用技术概念
|
|
53
|
+
- 文档已明确定义
|
|
54
|
+
- 用户已给出权威解释
|
|
55
|
+
|
|
56
|
+
模块判断细则见 [references/modules.md](references/modules.md)。
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 执行步骤
|
|
61
|
+
|
|
62
|
+
### 1. 判断模块
|
|
63
|
+
|
|
64
|
+
按上下文自行判断查询 **MM**、**CO**,或两者都查:
|
|
65
|
+
|
|
66
|
+
| 信号 | 倾向模块 |
|
|
67
|
+
|------|----------|
|
|
68
|
+
| 物料、库存、采购、MRP、BOM、仓库 | MM |
|
|
69
|
+
| 成本、费用、预算、核算、CO-PA、成本中心 | CO |
|
|
70
|
+
| proposal/spec 的 capability 名称或路径 | 按模块归属 |
|
|
71
|
+
| 无法判断 | 跳过查询,不阻塞主流程 |
|
|
72
|
+
|
|
73
|
+
### 2. 调用检索脚本
|
|
74
|
+
|
|
75
|
+
在项目根目录执行(路径由初始化时注入,统一在 `<SkillsBundlePath>/` 下):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node <SkillsBundlePath>/opsx-knowledge/scripts/retrieve.cjs --question="物料凭证是什么" --module=mm
|
|
79
|
+
node <SkillsBundlePath>/opsx-knowledge/scripts/retrieve.cjs --question="成本中心与利润中心的区别" --module=co
|
|
80
|
+
node <SkillsBundlePath>/opsx-knowledge/scripts/retrieve.cjs --question="..." --module=auto
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`--module` 取值:
|
|
84
|
+
- `mm` / `co`:指定模块
|
|
85
|
+
- `auto`:按 question 关键词自动匹配(默认)
|
|
86
|
+
|
|
87
|
+
部署后示例:
|
|
88
|
+
- Claude Code:`.claude/skills/kld-sdd/opsx-knowledge/scripts/retrieve.cjs`
|
|
89
|
+
- Cursor:`.cursor/skills/kld-sdd/opsx-knowledge/scripts/retrieve.cjs`
|
|
90
|
+
|
|
91
|
+
### 3. 处理结果
|
|
92
|
+
|
|
93
|
+
**成功(`ok: true`)**:读取 `chunks`,提炼 1-3 条建议,标注来源模块。
|
|
94
|
+
|
|
95
|
+
**失败(`ok: false`)**:不 retry 阻塞主流程,继续原有 SDD 步骤。
|
|
96
|
+
|
|
97
|
+
输出模板:
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
📚 知识库建议(MM模块,仅供参考):
|
|
101
|
+
- ...
|
|
102
|
+
- ...
|
|
103
|
+
|
|
104
|
+
说明:以上来自 RAGFlow 知识库,不作为 SDD 契约;如与 spec / 用户输入冲突,以 spec / 用户输入为准。
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## API 参考
|
|
110
|
+
|
|
111
|
+
内网 RAGFlow retrieval 接口:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
curl --request POST \
|
|
115
|
+
--url http://10.29.219.26:9380/api/v1/retrieval \
|
|
116
|
+
--header 'Content-Type: application/json' \
|
|
117
|
+
--header 'Authorization: Bearer ragflow-6xxy9RUUtRiE9N6gzpTLfQbY00tnGGKWsEuBR53zp0U' \
|
|
118
|
+
--data '{
|
|
119
|
+
"question": "What is advantage of ragflow?",
|
|
120
|
+
"dataset_ids": ["12eda67a4ffb11f19cb713ede57b176f"]
|
|
121
|
+
}'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
日常优先使用 `scripts/retrieve.cjs`,不要手写 curl。
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 与其他 SDD Skill 的关系
|
|
129
|
+
|
|
130
|
+
- **opsx-propose / opsx-spec / opsx-design / opsx-apply**:遇到业务名词时可 **可选** 调用本 skill
|
|
131
|
+
- **opsx-check**:不得把知识库内容当作质量红线依据
|
|
132
|
+
- 本 skill **不修改** 任何 openspec 文档,只提供参考信息
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 模块知识库映射
|
|
2
|
+
|
|
3
|
+
| 模块 | dataset_id | 典型场景 |
|
|
4
|
+
|------|------------|----------|
|
|
5
|
+
| MM | `12eda67a4ffb11f19cb713ede57b176f` | 物料、库存、采购、MRP、BOM、仓库、批次 |
|
|
6
|
+
| CO | `02841d1e4ffb11f19cb713ede57b176f` | 成本、费用、预算、核算、CO-PA、成本中心 |
|
|
7
|
+
|
|
8
|
+
## 模块选择规则
|
|
9
|
+
|
|
10
|
+
1. 优先看 **proposal/spec 中的 Capability 名称与描述**
|
|
11
|
+
2. 再看 **用户输入中的业务名词**
|
|
12
|
+
3. 再看 **代码路径/包名**(如含 `mm`、`co`、`material`、`cost`)
|
|
13
|
+
4. 若 MM 与 CO 都相关,可分别查询两个模块后合并为建议
|
|
14
|
+
5. 若无法判断模块,**跳过查询**,不要阻塞 SDD 主流程
|
|
15
|
+
|
|
16
|
+
## 查询时机
|
|
17
|
+
|
|
18
|
+
**适合查询:**
|
|
19
|
+
- 业务名词含义不明(如“物料凭证”“成本中心”“评估类”)
|
|
20
|
+
- 内部术语可能有多种解释
|
|
21
|
+
- 需要补充领域背景以写 propose/spec/design
|
|
22
|
+
|
|
23
|
+
**不必查询:**
|
|
24
|
+
- 通用技术概念(HTTP、JWT、Redis)
|
|
25
|
+
- proposal/spec/design 已明确定义
|
|
26
|
+
- 用户已给出权威定义
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"baseUrl": "http://10.29.219.26:9380",
|
|
3
|
+
"apiKey": "ragflow-6xxy9RUUtRiE9N6gzpTLfQbY00tnGGKWsEuBR53zp0U",
|
|
4
|
+
"timeoutMs": 8000,
|
|
5
|
+
"modules": {
|
|
6
|
+
"mm": {
|
|
7
|
+
"datasetId": "12eda67a4ffb11f19cb713ede57b176f",
|
|
8
|
+
"label": "MM模块",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"MM",
|
|
11
|
+
"物料",
|
|
12
|
+
"库存",
|
|
13
|
+
"采购",
|
|
14
|
+
"MRP",
|
|
15
|
+
"BOM",
|
|
16
|
+
"仓库",
|
|
17
|
+
"入库",
|
|
18
|
+
"出库",
|
|
19
|
+
"批次",
|
|
20
|
+
"Material"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"co": {
|
|
24
|
+
"datasetId": "02841d1e4ffb11f19cb713ede57b176f",
|
|
25
|
+
"label": "CO模块",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"CO",
|
|
28
|
+
"成本",
|
|
29
|
+
"费用",
|
|
30
|
+
"预算",
|
|
31
|
+
"核算",
|
|
32
|
+
"CO-PA",
|
|
33
|
+
"利润中心",
|
|
34
|
+
"成本中心",
|
|
35
|
+
"Controlling"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* RAGFlow 知识库检索 CLI(SDD opsx-knowledge 专用)
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node scripts/retrieve.cjs --question="什么是物料凭证" --module=mm
|
|
7
|
+
* node scripts/retrieve.cjs --question="成本中心是什么" --module=co
|
|
8
|
+
* node scripts/retrieve.cjs --question="..." --module=auto
|
|
9
|
+
*
|
|
10
|
+
* 失败时 exit 0 并返回 ok:false,不阻断 SDD 主流程。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { URL } = require('url');
|
|
17
|
+
|
|
18
|
+
const CONFIG_PATH = path.join(__dirname, 'config.json');
|
|
19
|
+
|
|
20
|
+
function readConfig() {
|
|
21
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const args = {};
|
|
26
|
+
for (const item of argv.slice(2)) {
|
|
27
|
+
if (!item.startsWith('--')) continue;
|
|
28
|
+
const eq = item.indexOf('=');
|
|
29
|
+
if (eq === -1) {
|
|
30
|
+
args[item.slice(2)] = true;
|
|
31
|
+
} else {
|
|
32
|
+
args[item.slice(2, eq)] = item.slice(eq + 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scoreModule(question, moduleConfig) {
|
|
39
|
+
const text = String(question || '').toLowerCase();
|
|
40
|
+
let score = 0;
|
|
41
|
+
for (const keyword of moduleConfig.keywords || []) {
|
|
42
|
+
if (text.includes(String(keyword).toLowerCase())) {
|
|
43
|
+
score += 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return score;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveModules(question, moduleArg, config) {
|
|
50
|
+
const modules = config.modules || {};
|
|
51
|
+
if (moduleArg && moduleArg !== 'auto' && modules[moduleArg]) {
|
|
52
|
+
return [moduleArg];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ranked = Object.entries(modules)
|
|
56
|
+
.map(([key, value]) => ({ key, score: scoreModule(question, value) }))
|
|
57
|
+
.filter(item => item.score > 0)
|
|
58
|
+
.sort((a, b) => b.score - a.score);
|
|
59
|
+
|
|
60
|
+
if (ranked.length === 0) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const topScore = ranked[0].score;
|
|
65
|
+
return ranked.filter(item => item.score === topScore).map(item => item.key);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function postRetrieval(config, question, datasetIds) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const endpoint = new URL('/api/v1/retrieval', config.baseUrl);
|
|
71
|
+
const payload = JSON.stringify({
|
|
72
|
+
question,
|
|
73
|
+
dataset_ids: datasetIds
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const req = http.request(
|
|
77
|
+
{
|
|
78
|
+
hostname: endpoint.hostname,
|
|
79
|
+
port: endpoint.port || 80,
|
|
80
|
+
path: endpoint.pathname,
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
85
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
86
|
+
},
|
|
87
|
+
timeout: config.timeoutMs || 8000
|
|
88
|
+
},
|
|
89
|
+
res => {
|
|
90
|
+
let body = '';
|
|
91
|
+
res.on('data', chunk => {
|
|
92
|
+
body += chunk;
|
|
93
|
+
});
|
|
94
|
+
res.on('end', () => {
|
|
95
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
96
|
+
reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 500)}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
resolve(JSON.parse(body));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
reject(new Error(`Invalid JSON response: ${error.message}`));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
req.on('timeout', () => {
|
|
109
|
+
req.destroy(new Error(`Request timeout after ${config.timeoutMs || 8000}ms`));
|
|
110
|
+
});
|
|
111
|
+
req.on('error', reject);
|
|
112
|
+
req.write(payload);
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeChunks(response) {
|
|
118
|
+
const chunks = response?.data?.chunks || response?.chunks || [];
|
|
119
|
+
return chunks.map((chunk, index) => ({
|
|
120
|
+
index: index + 1,
|
|
121
|
+
content: chunk.content || chunk.text || '',
|
|
122
|
+
document_name: chunk.document_name || chunk.doc_name || chunk.document_id || '',
|
|
123
|
+
similarity: chunk.similarity ?? chunk.score ?? null
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function failOutput(error, extra = {}) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
advisory: true,
|
|
131
|
+
error: String(error?.message || error),
|
|
132
|
+
chunks: [],
|
|
133
|
+
...extra
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
const args = parseArgs(process.argv);
|
|
139
|
+
const question = String(args.question || '').trim();
|
|
140
|
+
const moduleArg = String(args.module || 'auto').trim().toLowerCase();
|
|
141
|
+
|
|
142
|
+
if (!question) {
|
|
143
|
+
console.log(JSON.stringify(failOutput(new Error('Missing required --question=')), null, 2));
|
|
144
|
+
process.exit(0);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let config;
|
|
149
|
+
try {
|
|
150
|
+
config = readConfig();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.log(JSON.stringify(failOutput(error)), null, 2);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const selectedModules = resolveModules(question, moduleArg, config);
|
|
158
|
+
if (selectedModules.length === 0) {
|
|
159
|
+
console.log(JSON.stringify({
|
|
160
|
+
ok: false,
|
|
161
|
+
advisory: true,
|
|
162
|
+
skipped: true,
|
|
163
|
+
reason: 'No matching module for question; specify --module=mm or --module=co',
|
|
164
|
+
question,
|
|
165
|
+
chunks: []
|
|
166
|
+
}, null, 2));
|
|
167
|
+
process.exit(0);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const datasetIds = selectedModules.map(key => config.modules[key].datasetId);
|
|
172
|
+
const moduleLabels = selectedModules.map(key => config.modules[key].label);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const response = await postRetrieval(config, question, datasetIds);
|
|
176
|
+
const chunks = normalizeChunks(response);
|
|
177
|
+
console.log(JSON.stringify({
|
|
178
|
+
ok: true,
|
|
179
|
+
advisory: true,
|
|
180
|
+
question,
|
|
181
|
+
modules: selectedModules,
|
|
182
|
+
module_labels: moduleLabels,
|
|
183
|
+
dataset_ids: datasetIds,
|
|
184
|
+
chunk_count: chunks.length,
|
|
185
|
+
chunks
|
|
186
|
+
}, null, 2));
|
|
187
|
+
process.exit(0);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.log(JSON.stringify(failOutput(error, {
|
|
190
|
+
question,
|
|
191
|
+
modules: selectedModules,
|
|
192
|
+
module_labels: moduleLabels,
|
|
193
|
+
dataset_ids: datasetIds
|
|
194
|
+
}), null, 2));
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main();
|
|
@@ -88,6 +88,10 @@ allowed-tools:
|
|
|
88
88
|
- 代码文件 → 分析现有实现,识别修改点
|
|
89
89
|
- API 文档 → 了解接口约束、数据模型
|
|
90
90
|
|
|
91
|
+
**【可选】业务知识库检索**:
|
|
92
|
+
遇到不明 MM/CO 业务名词时,可调用 **opsx-knowledge** skill(`<SkillsBundlePath>/opsx-knowledge/`)查询 RAGFlow 知识库。
|
|
93
|
+
结果仅作 `📚 知识库建议`,不得写入 proposal 强制约束;查询失败时继续主流程。
|
|
94
|
+
|
|
91
95
|
### 3. 创建变更目录
|
|
92
96
|
|
|
93
97
|
```bash
|
|
@@ -98,6 +98,10 @@ openspec list
|
|
|
98
98
|
| 代码文件 | 了解现有实现约束 | 纳入技术契约和约束条件 |
|
|
99
99
|
| API 文档 | 接口规范参考 | 对齐数据契约和接口格式 |
|
|
100
100
|
|
|
101
|
+
**【可选】业务知识库检索**:
|
|
102
|
+
术语含义不清且可能影响 spec 准确性时,可调用 **opsx-knowledge** skill(`<SkillsBundlePath>/opsx-knowledge/`)。
|
|
103
|
+
知识库结果仅供参考,spec 契约以用户确认和 proposal 为准;失败时不阻塞。
|
|
104
|
+
|
|
101
105
|
### 4. 【关键步骤】读取本地模板文件
|
|
102
106
|
|
|
103
107
|
**必须先读取** `openspec-templates/spec.md` 作为文档结构模板:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|