@hunterzheng/kld-sdd 2.4.19
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 +275 -0
- package/bin/kld-sdd-init.js +24 -0
- package/index.js +13 -0
- package/lib/init.js +1124 -0
- package/lib/skills-bundle.js +30 -0
- package/package.json +48 -0
- package/skywalk-sdd/apply-worktree-finish.cjs +551 -0
- package/skywalk-sdd/index.cjs +2991 -0
- package/templates/ci/github-actions-sdd.yml +67 -0
- package/templates/ci/gitlab-ci-sdd.yml +44 -0
- package/templates/git-hooks/pre-commit-sdd-check.cjs +155 -0
- package/templates/git-hooks/pre-push-sdd-check.cjs +56 -0
- package/templates/hooks/claude/hooks/sdd-apply-gate.cjs +173 -0
- package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +315 -0
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +146 -0
- package/templates/hooks/claude/hooks/sdd-pre-tool.cjs +41 -0
- package/templates/hooks/claude/hooks/sdd-prompt.cjs +88 -0
- package/templates/hooks/claude/hooks/sdd-skill-apply-gate.cjs +268 -0
- package/templates/hooks/claude/hooks/sdd-stop.cjs +108 -0
- package/templates/hooks/claude/settings.json +72 -0
- package/templates/openspec/design.md +290 -0
- package/templates/openspec/overview.md +143 -0
- package/templates/openspec/proposal.md +108 -0
- package/templates/openspec/spec.md +185 -0
- package/templates/openspec/tasks.md +287 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +251 -0
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +94 -0
- package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +129 -0
- package/templates/skills/kld-sdd/opsx-apply/reference.md +335 -0
- package/templates/skills/kld-sdd/opsx-apply/worktree-setup.md +104 -0
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +162 -0
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +33 -0
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +197 -0
- package/templates/skills/kld-sdd/opsx-check/checklist.md +35 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +166 -0
- package/templates/skills/kld-sdd/opsx-design/checklist.md +46 -0
- package/templates/skills/kld-sdd/opsx-design/reference.md +44 -0
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +104 -0
- package/templates/skills/kld-sdd/opsx-knowledge/SKILL.md +130 -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/kld-sdd/opsx-propose/SKILL.md +201 -0
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +44 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +94 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +168 -0
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +46 -0
- package/templates/skills/kld-sdd/opsx-spec/reference.md +49 -0
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +199 -0
- package/templates/skills/kld-sdd/opsx-task/checklist.md +46 -0
- package/templates/skills/kld-sdd/opsx-task/reference.md +40 -0
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +143 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: opsx-design 的详细模板:telemetry 命令、上下文类型表、渐进式加载层级图。仅在需要参考详细模板时读取。
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# opsx-design — 详细参考(reference)
|
|
6
|
+
|
|
7
|
+
> 本文件承载 opsx-design 的重细节模板:telemetry 命令、§2 上下文类型表、§3 渐进式上下文加载层级图。
|
|
8
|
+
> SKILL.md 保留入口骨架与指针;本文件为详细模板来源。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 📊 Telemetry 命令模板(必做,不得跳过)
|
|
13
|
+
|
|
14
|
+
> 阶段开始:`node skywalk-sdd/log.cjs start --command=design --project=. --change=<变更名称> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID>`(保存 event_id)
|
|
15
|
+
> 阶段结束:`node skywalk-sdd/log.cjs end --event-id=<event_id> --command=design --project=. --change=<变更名称> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID> --result=success|failure --summary="摘要"`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## §2 上下文类型与用途
|
|
20
|
+
|
|
21
|
+
| 上下文类型 | 用途 | 如何融入 design |
|
|
22
|
+
|------------|------|----------------|
|
|
23
|
+
| 代码文件 | 分析现有实现、依赖关系 | 确定代码锚点、修改策略 |
|
|
24
|
+
| 架构文档 | 了解系统边界、模块关系 | 对齐总体设计方向 |
|
|
25
|
+
| 数据库 Schema | 了解数据模型约束 | 纳入数据设计章节 |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## §3 渐进式上下文加载层级图
|
|
30
|
+
|
|
31
|
+
**⛔ 必须严格按以下顺序加载:**
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
第 1 层:全局基线
|
|
35
|
+
→ openspec/specs/overview.md
|
|
36
|
+
|
|
37
|
+
第 2 层:宏观背景
|
|
38
|
+
→ changes/<name>/proposal.md
|
|
39
|
+
|
|
40
|
+
第 3 层:精准打击(仅当前 Capability)
|
|
41
|
+
→ changes/<name>/specs/<capability>/spec.md
|
|
42
|
+
|
|
43
|
+
⛔ 隔离红线:禁止读取其他 Capability 的文档!
|
|
44
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: opsx-explore
|
|
3
|
+
description: "浏览变更技能 - 查看所有变更状态和 SDD 文档完整性概览"
|
|
4
|
+
argument-hint: "[change-name]"
|
|
5
|
+
license: MIT
|
|
6
|
+
compatibility: Requires openspec CLI.
|
|
7
|
+
metadata:
|
|
8
|
+
author: sdd-team
|
|
9
|
+
version: "3.0"
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
- Write
|
|
14
|
+
- Edit
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
你是一个 SDD(Specification-Driven Development)变更浏览专家。激活本技能后,你将展示项目中所有变更的状态概览,并引导用户执行下一步操作。
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
> **🖥️ 跨平台执行规则**
|
|
21
|
+
> - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
|
|
22
|
+
> - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
|
|
23
|
+
> - 在 Windows Bash / Git Bash / Claude Bash 中,禁止裸写 Windows 反斜杠绝对路径(如 `D:\project\demo`);如必须使用绝对路径,请写成正斜杠路径或加引号。
|
|
24
|
+
> - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
|
|
25
|
+
> **📊 Telemetry(必做,不得跳过)**
|
|
26
|
+
> - 阶段开始:`node skywalk-sdd/log.cjs start --command=explore --project=. --change=<变更名称> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID>`(保存 event_id)
|
|
27
|
+
> - 阶段结束:`node skywalk-sdd/log.cjs end --event-id=<event_id> --command=explore --project=. --change=<变更名称> --agent=<Agent类型> --source=opsx-command --session-id=<会话ID> --result=success|failure --summary="摘要"`
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 技能定位
|
|
32
|
+
|
|
33
|
+
| 维度 | 内容 |
|
|
34
|
+
|------|------|
|
|
35
|
+
| 核心问题 | 项目中所有变更的进度如何 |
|
|
36
|
+
| 关键输出 | 变更概览表(文档完整性 + 状态 + 建议操作) |
|
|
37
|
+
| 操作类型 | 只读浏览,不修改任何文件 |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 启动流程
|
|
42
|
+
|
|
43
|
+
### 1. 获取所有变更列表
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openspec list
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
若没有任何变更,提示:
|
|
50
|
+
> "当前项目还没有任何变更。运行 `/opsx-propose <变更描述>` 开始第一个变更。"
|
|
51
|
+
|
|
52
|
+
### 2. 检查每个变更的 SDD 文档完整性
|
|
53
|
+
|
|
54
|
+
对每个变更,检查以下文件是否存在:
|
|
55
|
+
- `openspec/changes/<name>/proposal.md`(P)
|
|
56
|
+
- `openspec/changes/<name>/specs/<capability>/spec.md`(S)
|
|
57
|
+
- `openspec/changes/<name>/specs/<capability>/design.md`(D)
|
|
58
|
+
- `openspec/changes/<name>/specs/<capability>/tasks.md`(T)
|
|
59
|
+
|
|
60
|
+
同时获取每个变更状态:
|
|
61
|
+
```bash
|
|
62
|
+
openspec status --change "<name>" --json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. 展示变更概览表
|
|
66
|
+
|
|
67
|
+
> "📋 **项目变更概览**(P=提案 S=规格 D=设计 T=任务):
|
|
68
|
+
>
|
|
69
|
+
> | 变更名称 | P | S | D | T | openspec状态 | 建议下一步 |
|
|
70
|
+
> |---------|---|---|---|---|------------|---------|
|
|
71
|
+
> | add-user-auth | ✅ | ✅ | ✅ | ❌ | PENDING | `/opsx-task add-user-auth` |
|
|
72
|
+
> | payment-refund | ✅ | ❌ | ❌ | ❌ | PENDING | `/opsx-spec payment-refund` |
|
|
73
|
+
> | points-exchange | ✅ | ✅ | ✅ | ✅ | IMPLEMENTING | `/opsx-check points-exchange` |"
|
|
74
|
+
|
|
75
|
+
### 4. 【交互引导】选择操作
|
|
76
|
+
|
|
77
|
+
使用 **AskUserQuestion** 让用户选择:
|
|
78
|
+
> "请选择操作:
|
|
79
|
+
> - A. 查看变更详情:输入变更名称
|
|
80
|
+
> - B. 创建新变更:运行 `/opsx-propose`
|
|
81
|
+
> - C. 执行质量检查:运行 `/opsx-check <name>`
|
|
82
|
+
> - D. 申请实施:运行 `/opsx-apply <name>`
|
|
83
|
+
> - E. 退出浏览"
|
|
84
|
+
|
|
85
|
+
### 5. 若用户选择查看详情
|
|
86
|
+
|
|
87
|
+
展示选定变更的详细信息:
|
|
88
|
+
```bash
|
|
89
|
+
openspec status --change "<name>" --json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
展示:
|
|
93
|
+
- 变更描述和创建时间
|
|
94
|
+
- 各文档状态(存在/缺失/文件大小)
|
|
95
|
+
- openspec 内部状态(`applyRequires` 列表)
|
|
96
|
+
- 建议下一步操作和对应命令
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Guardrails
|
|
101
|
+
|
|
102
|
+
- explore 是**只读操作**,不修改任何文件
|
|
103
|
+
- 发现文档缺失时,推荐对应命令但不自动执行
|
|
104
|
+
- 若某变更 applyRequires 中有未完成项,在概览表中以 ⚠️ 标注
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
在项目根目录执行(使用 skill 目录变量,与 gitnexus 子 skill 脚本引用方式一致):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node ${CLAUDE_SKILL_DIR}/scripts/retrieve.cjs --question="物料凭证是什么" --module=mm
|
|
79
|
+
node ${CLAUDE_SKILL_DIR}/scripts/retrieve.cjs --question="成本中心与利润中心的区别" --module=co
|
|
80
|
+
node ${CLAUDE_SKILL_DIR}/scripts/retrieve.cjs --question="..." --module=auto
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`${CLAUDE_SKILL_DIR}` 由 Claude Code 注入,指向本 skill 目录(如 `.claude/skills/opsx-knowledge/`)。
|
|
84
|
+
|
|
85
|
+
`--module` 取值:
|
|
86
|
+
- `mm` / `co`:指定模块
|
|
87
|
+
- `auto`:按 question 关键词自动匹配(默认)
|
|
88
|
+
|
|
89
|
+
### 3. 处理结果
|
|
90
|
+
|
|
91
|
+
**成功(`ok: true`)**:读取 `chunks`,提炼 1-3 条建议,标注来源模块。
|
|
92
|
+
|
|
93
|
+
**失败(`ok: false`)**:不 retry 阻塞主流程,继续原有 SDD 步骤。
|
|
94
|
+
|
|
95
|
+
输出模板:
|
|
96
|
+
|
|
97
|
+
```markdown
|
|
98
|
+
📚 知识库建议(MM模块,仅供参考):
|
|
99
|
+
- ...
|
|
100
|
+
- ...
|
|
101
|
+
|
|
102
|
+
说明:以上来自 RAGFlow 知识库,不作为 SDD 契约;如与 spec / 用户输入冲突,以 spec / 用户输入为准。
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## API 参考
|
|
108
|
+
|
|
109
|
+
内网 RAGFlow retrieval 接口:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
curl --request POST \
|
|
113
|
+
--url http://10.29.219.26:9380/api/v1/retrieval \
|
|
114
|
+
--header 'Content-Type: application/json' \
|
|
115
|
+
--header 'Authorization: Bearer ragflow-6xxy9RUUtRiE9N6gzpTLfQbY00tnGGKWsEuBR53zp0U' \
|
|
116
|
+
--data '{
|
|
117
|
+
"question": "What is advantage of ragflow?",
|
|
118
|
+
"dataset_ids": ["12eda67a4ffb11f19cb713ede57b176f"]
|
|
119
|
+
}'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
日常优先使用 `scripts/retrieve.cjs`,不要手写 curl。
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 与其他 SDD Skill 的关系
|
|
127
|
+
|
|
128
|
+
- **opsx-propose / opsx-spec / opsx-design / opsx-apply**:遇到业务名词时可 **可选** 调用本 skill
|
|
129
|
+
- **opsx-check**:不得把知识库内容当作质量红线依据
|
|
130
|
+
- 本 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();
|