针对测试用例生成添加了常用测试方法;更新了需求提取工具

This commit is contained in:
2026-04-18 21:13:33 +08:00
parent c7c0659a85
commit 0c2ed67e2a
21 changed files with 2029 additions and 481 deletions

60
.github/AGENTS.md vendored
View File

@@ -3,22 +3,66 @@
## 适用范围
- 本工作区包含基于 Tool Calling 与 Skill Calling 的测试内容生成链路。
- 当用户提出测试项分解、测试用例生成或预期成果生成需求时,必须触发 testing-orchestrator。
- 本约定用于约束技能间数据传递、输出结构与容错策略。
## 已注册技能
- identify-requirement-type用户需求文本识别为明确的测试需求类型,为后续测试项分解与测试用例生成提供分类依据
- decompose-test-items按需求类型规则生成正常测试与异常测试测试项
- generate-test-cases按测试项生成可执行测试用例,至少 1 条/测试项
- testing-orchestrator按标准顺序编排工具调用并输出结构化结果。
- identify-requirement-type将需求文本识别为测试需求类型并给出推荐测试方法
- decompose-test-items按需求类型分解为可执行的正常/异常测试项,并输出覆盖分析
- generate-test-cases按测试项生成结构化测试用例,覆盖测试用例要素
- build-expected-results将测试用例中的预期占位符展开为可度量预期成果。
- format-output将测试项、测试用例、预期成果整理为统一三段式输出。
- testing-orchestrator按标准顺序编排技能并管理上下文传递。
## 强制调用链
1. identify-requirement-type
2. decompose-test-items
3. generate-test-cases
4. build_expected_results
5. format_output
4. build-expected-results
5. format-output
## 约束规则
## 步骤输入输出契
### Step 1: identify-requirement-type
- 输入user_requirement_text
- 输出requirement_type, reason, candidates, recommended_test_methods, suggested_decompose_template
### Step 2: decompose-test-items
- 输入user_requirement_text, requirement_type, recommended_test_methods, suggested_decompose_template
- 输出normal_test_items, abnormal_test_items, coverage_analysis
### Step 3: generate-test-cases
- 输入normal_test_items, abnormal_test_items, requirement_type, recommended_test_methods
- 输出normal_test_cases, abnormal_test_cases, method_alignment_report
### Step 4: build-expected-results
- 输入normal_test_cases, abnormal_test_cases, requirement_type, recommended_test_methods
- 输出normal_expected_results, abnormal_expected_results
### Step 5: format-output
- 输入normal_test_items, abnormal_test_items, normal_test_cases, abnormal_test_cases, normal_expected_results, abnormal_expected_results, coverage_analysis
- 输出:三段式结构化 Markdown
## 全局约束规则
- 除非用户明确要求只看中间步骤,否则禁止跳步。
- 每一步都必须显式接收上一步输出作为上下文输入。
-无法识别类型,必须输出未知类型及候选类型,继续执行通用分解。
- requirement_type 为未知类型,必须继续执行通用分解,不得中断调用链
- 每个测试项必须同时覆盖正常与异常视角;复杂功能必须细分到可直接对应测试用例的粒度。
- 每个测试项、测试用例、预期成果必须具备唯一标识,并保持可追踪映射关系。
- 最终输出必须严格包含测试项、测试用例、预期成果三段,并按正常测试/异常测试分组。
- 测试项和测试用例设计必须对齐以下规范:
- 测试项分解要求
- 测试项与测试用例技术要求
- 常用测试方法
## 失败降级策略
| 失败场景 | 处理策略 | 是否继续 |
| --- | --- | --- |
| Step 1 无法稳定识别类型 | requirement_type=未知类型,输出 1-3 个候选类型 | 是 |
| Step 2 覆盖率不足 | 输出 coverage_analysis 与补充建议 | 是 |
| Step 3 方法不完全对齐 | 输出 method_alignment_report 并标记低对齐项 | 是 |
| Step 4 缺少可量化指标 | 使用通用默认口径并标记待确认字段 | 是 |
| Step 5 格式化失败 | 回退为结构化 JSON 输出,不得丢失三段内容 | 是 |
## 调试模式
- debug=true 时,每一步追加 step_name, input_summary, output_summary, success, fallback_used, duration_ms。
- debug=true 时,最终输出追加覆盖矩阵与方法追踪矩阵。

View File

@@ -0,0 +1,67 @@
---
name: build-expected-results
description: "当需要将测试用例中的 expected_result_placeholder 展开为可度量预期成果时使用。"
---
# build-expected-results
## 目标
将测试用例中的预期占位符展开为可验证、可测量、可判定通过/失败的预期成果。
## 输入
- normal_test_cases
- abnormal_test_cases
- requirement_type
- recommended_test_methods可选
- quality_criteria可选精度、时间、误差、资源阈值
## 输出
- normal_expected_results
- abnormal_expected_results
每条预期成果至少包含:
- result_id
- case_id
- expected_results_detail
- evaluation_criteria
- pass_criteria
- termination_condition
## 强制规则
1. 预期成果必须来源于对应测试用例,不得脱离 case_id 独立生成。
2. 每条预期成果必须可验证,禁止使用模糊描述。
3. 预期成果必须覆盖:结果值、状态变化、时间要求、异常处理、通过准则。
4. 若包含定量结果,必须给出精度或允许偏差范围。
5. 若实际结果存在不确定性,必须定义重测条件。
## placeholder 展开映射
- {{return_value}} -> 返回码、返回体字段、字段值/类型约束
- {{state_change}} -> 状态机迁移、数据库字段变化、持久化副作用
- {{error_message}} -> 错误码、提示文案、触发条件
- {{data_persistence}} -> 数据落库、版本号、审计轨迹
- {{ui_display}} -> 页面元素、提示位置、可见性与文案
- {{precision_tolerance}} -> 精度阈值、允许误差上限与下限
- {{time_constraint}} -> 响应时间上限/下限、事件间隔
- {{retry_condition}} -> 触发重测的条件与次数限制
- {{error_handling}} -> 异常处理动作、回滚策略、保护措施
- {{sequence_event}} -> 事件顺序、状态切换顺序、时序关系
- {{resource_usage}} -> CPU/内存/磁盘/连接占用阈值
- {{pass_criteria}} -> 最终通过判定表达式
## 结果构造模板
每条 expected_results_detail 推荐按以下结构输出:
- observable_result可直接观测的结果
- measurable_constraints可量化约束精度、时间、阈值等
- side_effects副作用与状态变更
- error_handling_expectation异常处理期望
- retry_policy重测策略
- final_pass_rule通过准则
## 默认口径
当 quality_criteria 缺失时,使用以下默认规则:
- 时间约束:若无明确要求,标记为待确认,不擅自给固定毫秒值。
- 精度约束:若需求涉及计算,至少给出精度位数或误差范围占位。
- 资源阈值:若需求未给出,标记为待确认并保留观测项。
## 调试
- debug=true 时输出 placeholder_expansion_trace包含 case_id、placeholder、expansion_source、fallback_used。

View File

@@ -6,15 +6,29 @@ description: "当需要基于需求类型把需求文本分解为可执行的正
# decompose-test-items
## 目标
基于用户需求文本和已识别需求类型,生成测试项列表。
基于用户需求文本和已识别需求类型,生成完整、可执行、可追踪的测试项列表。
## 输入
- user_requirement_text
- requirement_type
- recommended_test_methods可选
- suggested_decompose_template可选
## 输出
- normal_test_items完整、可执行的正常测试项列表。
- abnormal_test_items完整、可执行的异常测试项列表。
- coverage_analysis覆盖分析包含覆盖率、缺口与补充建议。
每个测试项至少包含以下字段(对齐 C.1 要素):
- item_id测试项唯一标识。
- item_name测试项名称。
- item_description测试项说明测试目标和测试内容
- test_method测试方法可包含 1-N 个)。
- test_data_strategy测试数据生成/注入/捕获/分析方式。
- adequacy_requirement测试充分性要求。
- termination_condition正常终止与异常终止条件。
- priority优先级高/中/低)。
- traceability追踪关系需求点、子功能、接口或性能项
## 强制规则
1. 每个软件功能至少应被正常测试与被认可的异常场景覆盖;复杂功能需继续细分。
@@ -23,24 +37,62 @@ description: "当需要基于需求类型把需求文本分解为可执行的正
4. 粒度需适中,避免过粗或过细。
5. 对未知类型必须执行通用分解,并保持正常/异常分组。
6. 对需求说明未显式给出但在用户手册或操作手册体现的功能,也应补充测试项覆盖。
7. 每个测试项必须至少绑定一种测试方法,并标明追踪来源。
8. 每组测试项都需覆盖合法边界值与非法边界值(适用时)。
## 14类最小分解检查点
- 功能测试:正常覆盖功能主路径、基本数据类型、合法边界值与状态转换;异常覆盖非法输入、不规则输入、非法边界值与最坏情况。
- 性能测试:正常覆盖处理精度、响应时间、处理数据量与模块协调性;异常覆盖超负荷、软硬件限制、负载潜力上限与资源占用异常
- 外部接口测试:正常覆盖全部外部接口格式与内容正确性;异常覆盖每个输入输出接口的错误格式、错误内容与异常交互
- 人机交互界面测试:正常覆盖界面风格一致性与标准操作流程;异常覆盖误操作、快速操作、非法输入、错误命令与错误流程提示。
- 强度测试:正常覆盖设计极限下系统功能和性能表现;异常覆盖超出极限时的降级行为、健壮性与饱和表现
- 余量测试:正常覆盖存储、通道、处理时间余量是否满足要求;异常覆盖余量不足或耗尽时系统告警与受控行为
- 可靠性测试:正常覆盖典型环境、运行剖面与输入变量组合;异常覆盖失效等级场景、边界环境变化、不合法输入域及失效记录。
- 安全性测试:正常覆盖安全关键部件、安全结构与合法操作路径;异常覆盖危险状态、故障模式、边界接合部、非法进入与数据完整性保护
- 恢复性测试:正常覆盖故障探测、备用切换、恢复后继续执行;异常覆盖故障中作业保护、状态保护与恢复失败路径
- 边界测试:正常覆盖输入输出域边界、状态转换端点与功能界限;异常覆盖性能界限、容量界限和越界端点。
- 安装性测试:正常覆盖标准及不同配置下安装卸载流程;异常覆盖安装规程错误、依赖异常与中断后的处理
- 互操作性测试:正常覆盖两个或多个软件同时运行与互操作过程;异常覆盖互操作失败、并行冲突与协同异常
- 敏感性测试:正常覆盖有效输入类中典型数据组合;异常覆盖引发不稳定或不正常处理的特殊数据组合。
- 测试充分性要求:正常覆盖需求覆盖率、配置项覆盖与代码覆盖达标;异常覆盖未覆盖部分逐项分析、确认与报告输出
## 14类详细分解执行清单
- 功能测试
- 正常分解:主路径功能、基本数据类型、合法边界值、状态转换、运行模式与时间约束
- 异常分解:不规则输入、非法边界值、最坏情况(超负荷/饱和)、手册中隐含功能异常路径
- 性能测试
- 正常分解:处理精度、响应时间、可处理数据量、高低速协调
- 异常分解:软硬件瓶颈、过载极限、空间占用异常、性能退化阈值
- 外部接口测试
- 正常分解:接口格式合法性、字段内容正确性、输入输出链路可达
- 异常分解:格式错误、字段缺失/越界、协议不一致、异常 I/O 交互
- 人机交互界面测试
- 正常分解:界面风格一致性、标准流程操作、手册逐条操作一致
- 异常分解:误操作、快速操作、非法输入、错误命令与错误流程提示
- 强度测试
- 正常分解:设计极限内性能、降级行为前置阈值
- 异常分解:超极限、系统饱和、降级能力失效、健壮性边界。
- 余量测试
- 正常分解存储余量、I/O 通道余量、处理时间余量。
- 异常分解:余量不足(默认小于 20%)、余量耗尽时保护与告警。
- 可靠性测试
- 正常分解:运行剖面、概率分布输入、重要输入组合与典型环境一致性。
- 异常分解:失效等级场景、不合法输入域、环境边界变化、失效现象与时间记录。
- 安全性测试
- 正常分解:安全关键部件、容错/冗余/中断处理、合法输入下安全行为。
- 异常分解:危险状态、硬件/软件故障模式、界外与边界接合部、非法入侵与数据完整性攻击。
- 恢复性测试
- 正常分解:错误探测、备用切换、恢复后从无错状态继续执行。
- 异常分解:故障中作业保护失败、状态损坏、重置失败与恢复中断。
- 边界测试
- 正常分解:输入/输出域边界点、状态转换端点、功能与性能临界点。
- 异常分解:越界点、容量上限/下限之外、边界接合断裂行为。
- 安装性测试
- 正常分解:不同配置下安装和卸载流程、安装规程一致性。
- 异常分解:依赖缺失、安装中断、重复安装、回滚失败。
- 互操作性测试
- 正常分解:多软件并行运行、标准互操作流程。
- 异常分解:互操作失败、版本不兼容、并发冲突与消息错序。
- 敏感性测试
- 正常分解:有效输入类典型组合。
- 异常分解:引起不稳定或不正常处理的有效输入组合。
- 测试充分性要求
- 正常分解:需求覆盖率 100%、配置项要求覆盖、编译器一致性。
- 异常分解:语句/分支未覆盖点逐项分析、确认并形成分析结论。
## coverage_analysis 输出要求
- requirement_points需求点列表。
- covered_points已覆盖需求点。
- uncovered_points未覆盖需求点。
- requirement_coverage_rate覆盖率计算规则为 covered_points/requirement_points。
- recommended_supplementary_items建议补充测试项。
## 未知类型容错
- 当 requirement_type 无法确定时,仍需输出正常/异常两组测试项。
- 通用正常项至少包含:主流程正确性、合法边界值、标准输入输出。
- 通用异常项至少包含:非法输入、越界输入、资源异常或状态冲突。
- 未知类型场景下,默认使用功能分解、等价类划分、边界值分析三种方法。

77
.github/skills/format-output/SKILL.md vendored Normal file
View File

@@ -0,0 +1,77 @@
---
name: format-output
description: "当需要将测试项、测试用例、预期成果按统一格式输出时使用。"
---
# format-output
## 目标
将测试链路中生成的结构化数据整理为标准化三段式输出,确保可读性与追踪关系。
## 输入
- normal_test_items
- abnormal_test_items
- normal_test_cases
- abnormal_test_cases
- normal_expected_results
- abnormal_expected_results
- coverage_analysis可选
- method_alignment_report可选
- debug可选
## 输出
- markdown_output
## 强制规则
1. 输出必须包含三段:测试项、测试用例、预期成果。
2. 每段必须按正常测试与异常测试分组。
3. 编号必须保持追踪关系:
- 测试项编号TI-Nxx / TI-Exx
- 测试用例编号TC-Nxx / TC-Exx
- 预期成果编号ER-Nxx / ER-Exx
4. 测试用例必须显示对应测试项;预期成果必须显示对应测试用例。
5. 任一分段数据为空时,必须显式输出待补充,不允许静默省略。
6. 每条输出使用一段完整描述,不强制拆分为多个字段子项。
## 输出模板
**测试项**
**正常测试**
1. [TI-N01]...
**异常测试**
1. [TI-E01]...
**测试用例**
**正常测试**
1. [TC-N01](对应 TI-N01...
**异常测试**
1. [TC-E01](对应 TI-E01...
**预期成果**
**正常测试**
1. [ER-N01](对应 TC-N01...
**异常测试**
1. [ER-E01](对应 TC-E01...
## 调试模式
当 debug=true 时追加:
### 覆盖率分析
- requirement_coverage_rate
- uncovered_points
- recommended_supplementary_items
### 方法对齐分析
- method_alignment_report
### 步骤日志
- step_name
- success
- fallback_used
- duration_ms

View File

@@ -6,20 +6,32 @@ description: "当需要根据已分解测试项生成包含操作步骤与测试
# generate-test-cases
## 目标
按测试项生成测试用例,每个测试项至少对应 1 条用例。
按测试项生成测试用例,每个测试项至少对应 1 条可重复执行、可评估通过的测试用例。
## 输入
- normal_test_items
- abnormal_test_items
- requirement_type可选
- recommended_test_methods可选
## 输出
- normal_test_cases
- abnormal_test_cases
- method_alignment_report
每条测试用例必须包含:
- case_id
- case_name
- test_traceability
- case_summary
- initialization_requirements
- test_inputs
- operation_steps
- test_content
- expected_result_placeholder
- evaluation_criteria
- preconditions_constraints
- termination_condition
- pass_criteria
## 规则
1. 测试项与测试用例应保持一一对应关系。
@@ -28,13 +40,48 @@ description: "当需要根据已分解测试项生成包含操作步骤与测试
4. 操作步骤应可顺序执行,避免歧义。
5. 操作步骤必须包含明确动作、对象和输入条件,禁止笼统动作词。
6. test_content 必须包含可验证条件,便于后续生成可度量预期成果。
7. 测试用例应符合可重复执行原则,初始条件和参数必须可复现。
8. 测试输入必须标识有效值/无效值/边界值性质、输入来源、真实或模拟属性及事件顺序。
9. 每个操作步骤应包含测试输入、动作、中间期望、评估准则与异常终止信号。
10. pass_criteria 必须明确是否通过的判定条件,禁止模糊描述。
## expected_result_placeholder 映射
## expected_result_placeholder 映射(用于 Step 4 展开)
- {{return_value}}:接口或函数返回值验证。
- {{state_change}}:系统状态变化验证。
- {{error_message}}:异常场景错误信息验证。
- {{data_persistence}}:数据库或存储落库结果验证。
- {{ui_display}}:界面显示反馈验证。
- {{precision_tolerance}}:精度与允许误差范围验证。
- {{time_constraint}}:时间上限/下限或事件间隔验证。
- {{retry_condition}}:不确定结果时重测触发条件验证。
- {{error_handling}}:出错处理流程与保护动作验证。
- {{sequence_event}}:时序、状态切换或事件顺序验证。
- {{resource_usage}}:资源占用与空间约束验证。
- {{pass_criteria}}:用例通过准则验证。
## 常用测试方法应用清单B.1.1-B.2.6
每个用例必须在 summary 中标注所用方法,并在 steps 中体现方法特征。
| 方法 | 适用场景 | 输入构造规则 | 步骤设计特征 |
| --- | --- | --- | --- |
| 功能分解 | 功能/接口主流程 | 按子功能拆输入集 | 用例按子功能逐级覆盖 |
| 等价类划分 | 功能/接口/边界 | 有效类与无效类分别取代表值 | 每类至少一条步骤 |
| 边界值分析 | 输入输出边界 | 取边界、邻近、越界值 | 连续执行边界点序列 |
| 判定表 | 多条件组合逻辑 | 依据条件桩生成规则列 | 一列规则对应一条步骤流 |
| 因果图 | 条件与结果耦合 | 构建原因-结果及约束 | 覆盖关键因果链路 |
| 场景法 | 事件驱动流程 | 基本流与备选流输入 | 按场景路径组织步骤 |
| 功能图法 | 状态与逻辑联合 | 状态转移输入序列 | 状态路径+局部逻辑组合 |
| 随机测试 | 可靠性/强度 | 输入区间+概率分布 | 指定随机规则和样本量 |
| 猜错法 | 高风险经验缺陷 | 构造易错输入集合 | 直接验证高风险点 |
| 正交试验法 | 多因子组合优化 | 因子-水平表+正交表 | 覆盖代表组合点 |
| 组合测试法 | 参数组合覆盖 | 选定组合强度pairwise/K | 按组合强度生成步骤 |
| 蜕变测试法 | 预期结果难判定 | 构造蜕变关系输入组 | 校验用例间关系一致性 |
| 控制流测试 | 白盒流程覆盖 | 按路径目标构造输入 | 步骤对应语句/分支路径 |
| 数据流测试 | 白盒变量使用 | 定义-使用链路输入 | 覆盖关键定义引用对 |
| 程序变异 | 错误驱动验证 | 针对变异体生成输入 | 验证是否杀死变异体 |
| 程序插桩 | 运行行为观测 | 插桩点对应输入 | 步骤包含采样与比对 |
| 域测试 | 输入空间划分检验 | 构造域边界/域内样本 | 验证域划分正确性 |
| 符号求值 | 公式与路径推导 | 依据符号约束反推输入 | 校验符号关系与结果 |
## 禁止模糊描述
- 错误示例:"检查功能正常";正确示例:"验证返回状态码为200且响应体包含status=success"。
@@ -43,3 +90,7 @@ description: "当需要根据已分解测试项生成包含操作步骤与测试
## 预期结果耦合
- 每条用例必须可在下一步绑定一条明确、可验证的预期成果。
- 每条 expected_result_placeholder 必须可映射到定量或可观察的检查项。
## 对齐校验
- 输出 method_alignment_report至少包含case_id、selected_methods、alignment_score、gaps、fix_suggestions。

View File

@@ -6,10 +6,11 @@ description: "当需要在测试项分解与测试用例生成之前识别需求
# identify-requirement-type
## 目标
将用户需求文本识别为明确的测试需求类型,为后续测试项分解与测试用例生成提供分类依据。
将用户需求文本识别为明确的测试需求类型,为后续测试项分解与测试用例生成提供分类依据、推荐测试方法与分解模板
## 输入
- user_requirement_text用户原始需求文本。
- debug可选是否返回分类分数和证据切片。
## 输出
- requirement_type以下之一
@@ -30,6 +31,9 @@ description: "当需要在测试项分解与测试用例生成之前识别需求
- 未知类型
- reason简要判断依据。
- candidates当 requirement_type 为未知类型时,给出 1-3 个最接近候选类型。
- recommended_test_methods基于类型推荐的测试方法列表按优先级排序
- suggested_decompose_template推荐的测试项分解模板名称。
- type_signals触发该类型判断的关键语义信号。
## 类型识别信号
- 功能测试:关注功能需求逐项验证、业务流程正确性、输入输出行为、状态转换与边界值处理。
@@ -47,16 +51,40 @@ description: "当需要在测试项分解与测试用例生成之前识别需求
- 敏感性测试:关注有效输入类中可能引发不稳定或不正常处理的数据组合。
- 测试充分性要求:关注需求覆盖率、配置项覆盖、语句覆盖、分支覆盖及未覆盖分析确认。
## 附录 A类型到方法与模板映射
| 需求类型 | 推荐测试方法(优先顺序) | 推荐分解模板 |
| --- | --- | --- |
| 功能测试 | 功能分解, 等价类划分, 边界值分析, 场景法 | functional-standard-template |
| 性能测试 | 边界值分析, 组合测试法, 随机测试, 正交试验法 | performance-metric-template |
| 外部接口测试 | 等价类划分, 边界值分析, 判定表, 因果图 | external-interface-template |
| 人机交互界面测试 | 场景法, 猜错法, 等价类划分, 边界值分析 | hmi-flow-template |
| 强度测试 | 随机测试, 组合测试法, 边界值分析 | stress-limit-template |
| 余量测试 | 边界值分析, 组合测试法 | margin-capacity-template |
| 可靠性测试 | 随机测试, 场景法, 组合测试法, 蜕变测试法 | reliability-profile-template |
| 安全性测试 | 边界值分析, 场景法, 猜错法, 因果图 | safety-hazard-template |
| 恢复性测试 | 场景法, 功能图法, 猜错法 | recovery-fault-template |
| 边界测试 | 边界值分析, 等价类划分, 组合测试法 | boundary-focused-template |
| 安装性测试 | 场景法, 猜错法 | install-config-template |
| 互操作性测试 | 场景法, 组合测试法, 因果图 | interoperability-template |
| 敏感性测试 | 组合测试法, 正交试验法, 随机测试, 猜错法 | sensitivity-combination-template |
| 测试充分性要求 | 控制流测试, 数据流测试, 程序变异, 程序插桩 | adequacy-coverage-template |
| 未知类型 | 功能分解, 等价类划分, 边界值分析 | generic-fallback-template |
## 规则
1. 优先依据需求文本中的显式表述进行分类。
2. 分类应以语义意图为主,不能只做关键词机械匹配。
3. 置信度不足时输出未知类型,并提供候选类型。
4. 判断依据需简洁、可追溯到文本证据。
5. 若需求同时覆盖多个类型,输出主类型 requirement_type并将次类型放入 candidates。
6. recommended_test_methods 至少返回 2 个方法,优先返回文档中可直接执行的方法。
7. suggested_decompose_template 必须与 requirement_type 一致。
## 容错
- 当需求描述过于笼统或跨多类型混合时,输出未知类型,并在 candidates 给出最接近类型。
- 当识别不稳定时,优先保守分类,不强行归入单一类型。
- 未知类型不阻断后续流程,应继续执行通用测试项分解。
- 当文本信息不足以支持白盒方法推荐时,保留黑盒优先推荐并标记 reason。
## 调试
- debug 模式下返回每个类型的分类分数 classification_scores。
- debug 模式下追加 evidence_spans触发分类的文本片段与 method_selection_reason。

View File

@@ -6,14 +6,14 @@ description: "当用户要求测试项分解或测试用例生成且需要完整
# testing-orchestrator
## 目标
严格执行测试生成调用链,并显式传递每一步上下文。
严格执行测试生成调用链,确保每一步上下文显式传递、结构一致、可追踪
## 标准调用链
1. identify-requirement-type
2. decompose-test-items
3. generate-test-cases
4. build_expected_results
5. format_output
4. build-expected-results
5. format-output
## 编排规则
1. 优先使用 Skill 与 Tool不使用临时硬编码逻辑替代。
@@ -21,32 +21,76 @@ description: "当用户要求测试项分解或测试用例生成且需要完整
3. 每一步必须显式接收上一步输出。
4. 分类失败时输出未知类型并继续执行通用分解。
## 步骤契约
### Step 1: identify-requirement-type
- 输入:
- user_requirement_text
- debug可选
- 输出:
- requirement_type
- reason
- candidates
- recommended_test_methods
- suggested_decompose_template
### Step 2: decompose-test-items
- 输入:
- user_requirement_text
- requirement_type来自 Step 1
- recommended_test_methods来自 Step 1
- suggested_decompose_template来自 Step 1
- 输出:
- normal_test_items
- abnormal_test_items
- coverage_analysis
### Step 3: generate-test-cases
- 输入:
- normal_test_items来自 Step 2
- abnormal_test_items来自 Step 2
- requirement_type来自 Step 1
- recommended_test_methods来自 Step 1
- 输出:
- normal_test_cases
- abnormal_test_cases
- method_alignment_report
### Step 4: build-expected-results
- 输入:
- normal_test_cases来自 Step 3
- abnormal_test_cases来自 Step 3
- requirement_type来自 Step 1
- recommended_test_methods来自 Step 1
- 输出:
- normal_expected_results
- abnormal_expected_results
### Step 5: format-output
- 输入:
- normal_test_items, abnormal_test_items来自 Step 2
- normal_test_cases, abnormal_test_cases来自 Step 3
- normal_expected_results, abnormal_expected_results来自 Step 4
- coverage_analysis来自 Step 2
- method_alignment_report来自 Step 3
- debug可选
- 输出:
- markdown_output
## 执行顺序与回退
1. Step 1 必须先执行,若无法确定类型则输出未知类型并继续。
2. Step 2 若覆盖率不足,不中断流程,记录 coverage_analysis.gaps 并继续。
3. Step 3 若方法对齐不足,不中断流程,输出 method_alignment_report。
4. Step 4 若缺少定量口径,使用通用默认口径并标记待确认字段。
5. Step 5 若 Markdown 格式化失败,回退为结构化 JSON不丢失三段内容。
## 成果完整性检查
- 最终输出必须同时包含:测试项、测试用例、预期成果。
- 每个测试用例必须能追踪到测试项,每个预期成果必须能追踪到测试用例。
- 输出必须按正常测试/异常测试分组。
## 输出模板
最终输出必须严格遵循以下分组结构:
**测试项**
**正常测试**
1. [测试项 N1]...
**异常测试**
1. [测试项 E1]...
**测试用例**
**正常测试**
1. [用例 N1](对应测试项 N1...
**异常测试**
1. [用例 E1](对应测试项 E1...
**预期成果**
**正常测试**
1. [预期 N1](对应用例 N1...
**异常测试**
1. [预期 E1](对应用例 E1...
最终输出由 format-output 统一生成,必须遵循三段式结构并保持编号追踪。
## 调试模式
当 debug=true 时,输出步骤日志并包含:
@@ -55,3 +99,4 @@ description: "当用户要求测试项分解或测试用例生成且需要完整
- output_summary
- success
- fallback_used
- duration_ms

116
.github/常用测试方法.md vendored Normal file
View File

@@ -0,0 +1,116 @@
B.1 常用的黑盒测试方法
B.1.1 功能分解
功能分解是将规格说明中每一个功能加以分解,确保各个功能被全面地测试。功能分解是一种较常用的方法。
功能分解方法的步骤为:
a. 使用程序设计中的功能抽象方法把程序分解为功能单元;
b. 使用数据抽象方法产生测试每个功能单元的数据。
功能抽象中程序被看成一种抽象的功能层次,每个层次可标识被测试的功能,层次结构中的某一功能由其下一层功能定义。按照功能层次进行分解,可以得到众多的最低层次的子功能,以这些子功能为对象,进行测试用例设计。
数据抽象中,数据结构可以由抽象数据类型的层次图来描述,每个抽象数据类型有其取值集合。
程序的每一个输入和输出量的取值集合用数据抽象来描述。
B.1.2 等价类划分
等价类划分是在分析规格说明的基础上,把程序的输入域划分成若干部分,然后在每部分中选取代表性数据形成测试用例。
等价类划分方法的步骤为:
a. 划分有效等价类:对规格说明是有意义、合理的输入数据所构成的集合;
b. 划分无效等价类:对规格说明是无意义、不合理的输入数据所构成的集合;
c. 为每一个等价类定义一个唯一的编号;
d. 为每一个等价类设计一组测试用例,确保覆盖相应的等价类。
B.1.3 边界值分析
边界值分析是使用等于、小于或大于边界值的数据对程序进行测试的方法。边界值分析方法的步骤为:
a. 通过分析规格说明,找出所有可能的边界条件;
b. 对每一个边界条件,给出满足和不满足边界值的输入数据;
c. 设计相应的测试用例。
对满足边界值的输入可以发现计算错误,对不满足的输入可以发现域错误。该方法会为其他测试方法补充一些测试用例,绝大多数测试都会用到本方法。
B.1.4 判定表
判定表由四部分组成:条件桩,条件条目,动作桩,动作条目。任何一个条件组合的取值及其相应要执行的操作构成规则,条目中的每一列是一条规则。
条件引用输入的等价类,动作引用被测软件的主要功能处理部分,规则就是测试用例。
建立并优化判定表,把判定表中每一列表示的情况写成测试用例。该方法的使用有以下要求:
a. 规格说明以判定表形式给出,或是很容易转换成判定表;
b. 条件的排列顺序不会影响执行哪些操作;
c. 规则的排列顺序不会影响执行哪些操作;
d. 每当某一规则的条件已经满足,并确定要执行的操作后,不必检验别的规则;
e. 如果某一规则的条件得到满足,将执行多个操作,这些操作的执行与顺序无关。
B.1.5 因果图
因果图方法是通过画因果图,把用自然语言描述的功能说明转换为判定表,然后为判定表的每一列设计一个测试用例。
因果图方法的步骤为:
a. 分析规格说明,引出原因(输入条件)和结果(输出结果),并给每个原因和结果赋予一个标识符;
b. 分析规格说明中语义的内容,并将其表示成连接各个原因和各个结果的“因果图”;
c. 在因果图上标明约束条件;
d. 通过跟踪因果图中的状态条件,把因果图转换成有限项的判定表;
e. 把判定表中每一列表示的情况生成测试用例。
如果规格说明中含有输入条件的组合,宜采用本方法。有些软件的因果图可能非常庞大,以致于根据因果图得到的测试用例数目非常大,此时不宜使用本方法。
B.1.6 场景法
通常,对由事件触发控制流程的软件,事件触发时的情景形成了场景。同一事件可能有不同的触发顺序和处理结果。因此,用况场景不仅描述了软件预期的或所希望的使用方式,也描述了流经用况的路径,包括从用况开始到结束遍历这条路径上所有的基本流和备选流。基本流是经过用况的最简单的路径。一个备选流可能从基本流开始,也可能起源于另一个备选流,备选流在某个特定条件下执行,最后可能重新加入基本流中,或者终止用况而不再重新加入到某个流中。按照经过用况的每个可能路径,可以确定不同的用况场景。每个用况场景的执行都是在特定条件触发下发生的,因此,可根据某个特定条件为每个场景生成测试用例。
场景法的步骤为:
a. 确定用况的基本流和备选流;
b. 根据基本流和备选流确定不同的场景;
c. 针对每一个场景设计测试用例,可采用矩阵或判定表设计测试用例;
d. 对 c.中设计的全部测试用例进行审验,取消多余或等效的测试用例,确保测试用例准确和适度;
e. 针对每一个测试用例设计测试数据。
B.1.7 功能图法
功能图法用功能图形式化地表示程序的功能说明,并生成测试用例。测试用例是由测试中经过的一系列状态和在每个状态中必须依靠输入/输出数据满足的一对条件组成。
功能图模型由状态转移图和逻辑功能模型构成。状态转移图用于表示输入数据序列以及相应的输出数据,在状态转移图中,由输入数据和当前状态决定输出数据和后续状态。逻辑功能模型用于表示在状态中输入条件和输出条件之间的对应关系,且输出数据仅由输入数据决定。逻辑功能模型可用因果图和判定表来描述。
功能图法生成测试用例的步骤为:
a. 由逻辑功能模型导出测试用例。对每个状态用因果图法生成局部测试用例;
b. 由状态转移图导出测试用例。用节点代替状态,用弧代替迁移,将状态转移图转化为控制流图形式。用白盒测试方法中介绍的控制流测试生成路径测试用例;
c. 合成测试用例。把局部测试用例分配给测试路径上的某一个状态,将路径测试用例与局部测试用例结合在一起,生成实用的功能图的测试用例。
B.1.8 随机测试
随机测试指测试输入数据是在所有可能输入值中随机选取的。测试人员只需规定输入变量的取值区间,在需要时提供必要的变换机制,使产生的随机数服从预期的概率分布。该方法获得预期输出比较困难,多用于可靠性测试和系统强度测试。
B.1.9 猜错法
猜错法是有经验的测试人员,通过列出可能有的错误和易错情况表,写出测试用例的方法。
B.1.10 正交试验法
正交试验法是从大量的实验点中挑出适量的、有代表性的点,应用正交表,合理地安排实验的一种科学的实验设计方法。
利用正交试验法来设计测试用例时,首先要根据被测软件的规格说明书找出影响功能实现的操作对象和外部因素,把它们当作因子,而把各个因子的取值当作状态,生成二元的因素分析表。然后,利用正交表进行各因子的状态的组合,构造有效的测试输入数据集,并由此建立因果图。这样得出的测试用例的数目将大大减少。
B.1.11 组合测试法
组合测试是一种测试用例的生成方法。被测对象的不同输入参数分别有若干个取值。可以为不同输入参数轮流取不同值,以组合生成一组测试用例集。在黑盒测试中,组合的参数可以是软件的输入数据;在白盒测试中,组合的参数通常对应于单元的输入参数。
对组合生成的测试用例集的检验,是看其能否满足不同的组合强度覆盖要求。常见的组合强度覆盖有单一选择、基本选择、成对组合、全组合和 K 强度组合等。
B.1.12 蜕变测试法
蜕变测试是一种测试结果验证方法。当被测软件结构复杂,单个测试用例的测试结果的正确性难以判定时,可以通过构造蜕变关系,验证该测试用例与其他相关测试用例的测试结果是否满足蜕变关系,如果不满足蜕变关系,则这些测试用例不通过,并由此发现失效。蜕变测试可以与其他测试方法共同使用,通过蜕变测试对测试结果进行验证,或生成补充测试用例。
B.2 常用的白盒测试方法
B.2.1 控制流测试
依据控制流程图产生测试用例通过对不同控制结构成份的测试验证程序的控制结构。所谓验证某种控制结构即指使这种控制结构在程序运行中得到执行也称这一过程为覆盖。常用的覆盖有语句覆盖、分支覆盖、条件覆盖、条件组合覆盖、修正的条件判定覆盖MC/DC、路径覆盖等。
控制流测试的步骤为:
a. 将程序流程图转换成控制流图;
b. 经过语法分析求得路径表达式;
c. 生成路径树;
d. 进行路径编码;
e. 经过译码得到执行的路径;
f. 通过路径枚举产生特定路径的测试用例。
B.2.2 数据流测试
数据流测试是用控制流程图对变量的定义和引用进行分析,查找出未定义的变量或定义了而未使用的变量,这些变量可能是拼错的变量、变量混淆或丢失了语句。数据流测试一般使用工具进行。
数据流测试通过一定的覆盖准则,检查程序中每个数据对象的每次定义、使用和消除的情况。数据流测试方法的步骤为:
a. 将程序流程图转换成控制流图;
b. 在每个链路上标注对有关变量的数据操作的操作符号或符号序列;
c. 选定数据流测试策略;
d. 根据测试策略得到测试路径;
e. 根据路径可以获得测试输入数据和测试用例。
动态数据流异常检查在程序运行时执行,获得的是对数据对象的真实操作序列,克服了静态分析检查的局限,但动态方式检查是沿与测试输入有关的一部分路径进行的,检查的全面性和程序结构覆 盖有关。
B.2.3 程序变异
程序变异是一种错误驱动测试方法。该方法针对某类特定程序错误,定义一系列的变异算子,将变异算子作用于程序代码,产生若干符合语法的变异体,实际是将故障植入程序,用测试数据逐个测试变异体。根据变异程度的不同,可以分为强变异和弱变异。程序变异方法具有针对性强、系统性强的特点,在对软件进行测试的同时,也可评价测试用例集的错误检测能力。
B.2.4 程序插桩
程序插桩是向被测程序中插入操作以实现测试目的方法。程序插桩不应该影响被测程序的运行过程和功能。
有很多的工具有程序插桩功能。由于数据记录量大,手工进行将是一件很烦琐的事。
B.2.5 域测试
域测试是要判别程序对输入空间的划分是否正确。该方法限制太多,使用不方便,供有特殊要求
的测试使用。
B.2.6 符号求值
符号求值是允许数值变量取“符号值”以及数值。符号求值可以检查公式的执行结果是否达到程序预期的目的,也可以通过程序的符号执行,产生出程序的路径,用于产生测试数据。
符号求值最好使用工具,在公式分支较少时手工推导也是可行的。

View File

@@ -0,0 +1,54 @@
C.1 测试项
测试项是指通过测试需求分析所得到的需要测试的特定科目。测试项有明确的测试目标和测试方法。通常一个测试对象对应多个测试项,一个测试项可划分成多个测试子项。测试项可对应由一种或 多种测试类型覆盖。
测试项(或测试子项)的要素至少应包括:
a. 名称和标识。每个测试项应有唯一的名称和标识;
b. 测试项说明。简要描述测试目标和测试内容;
c. 测试方法。说明对测试项进行测试所采用的策略,包括采用的测试方法以及测试数据生成方法、测试数据注入方法、测试结果捕获方法及分析方法、使用的测试工具等;
d. 测试充分性要求。说明为实现测试目标,测试项应覆盖的范围及覆盖程度;
e. 测试项终止条件。说明正常终止的条件(例如,测试充分性是否达到要求)和导致测试异常终止的可能情况;
f. 优先级。说明测试项的优先顺序;
g. 追踪关系。说明测试项对测试依据的追踪关系,应追踪到测试依据的某个具体功能(或子功能)、接口、性能等。测试依据一般包括软件测试任务书/合同、软件开发文档、软件更改报告等。
C.2 测试用例
C.2.1 设计原则
设计测试用例应遵循以下原则:
a. 基于测试需求的原则。测试需求来自于软件测试任务书、合同或其他等效文件、软件开发文档、软件更改报告、软件代码等。应在测试项的基础上设计测试用例;
b. 基于测试方法的原则。应明确所采用的测试用例设计方法。为达到测试充分性要求,应采用相应的测试方法;
c. 兼顾测试充分性和效率的原则。测试用例集应兼顾测试的充分性和测试效率,每个测试用例应完整、具有可操作性;
d. 测试执行的可重复性原则。应保证测试用例执行的可重复性。
C.2.2 要素内容
测试用例是针对测试项所设计的,描述测试输入、测试方法、操作步骤、预期结果的集合。通常一个测试项由多个测试用例来覆盖。
测试用例至少应包括以下要素:
a. 名称和标识。每个测试用例应有唯一的名称和标识。
b. 测试追踪。说明测试所依据的内容来源,通常是对测试大纲/计划的追踪关系,应追踪到测试大纲/计划中的具体测试项或子测试项。
c. 用例综述。简要描述测试目的和所采用的测试方法。
d. 测试的初始化要求。应考虑下述初始化要求:
1. 硬件配置。被测系统的硬件配置情况,包括硬件条件或电气状态;
2. 软件配置。被测系统的软件配置情况,包括测试的初始条件;
3. 测试配置。测试系统的配置情况,例如,用于测试的模拟系统和测试工具等的配置情况;
4. 参数设置。测试开始前的设置,例如,标志、第一断点、指针、控制参数和初始化数据等的设置。
5. 其他对于测试用例的特殊说明。
e. 测试的输入。在测试用例执行中发送给被测对象的所有测试命令、数据和信号等。对于每个测试用例应提供:
1. 每个测试输入的具体内容(例如,确定的数值、状态或信号等)及其性质(例如,有效值、无效值、边界值等);
2. 测试输入的来源(例如,测试程序产生、磁盘文件、通过网络接收、人工键盘输入等),以及选择输入所使用的方法(例如,等价类划分、边界值分析、错误推测、因果图、功能 图方法等);
3. 测试输入是真实的还是模拟的;
4. 测试输入的时间顺序或事件顺序。
f. 期望结果。说明测试用例执行中由被测软件所产生的期望测试结果,即经过验证,认为正确的结果。必要时,应提供中间的期望结果。期望测试结果应该有具体内容,例如,确定的数值、 状态或信号等,不应是不确切的概念或笼统的描述。
g. 测试结果评估准则。判断测试用例执行中产生的中间和最后结果是否正确的标准。对于每个测试结果,应根据不同情况提供:
1. 实际测试结果所需的精度;
2. 实际测试结果与期望结果之间的差异允许的上限、下限;
3. 时间的最大和/或最小间隔,或事件数目的最大和/或最小值;
4. 实际测试结果不确定时,再测试的条件;
5. 与产生测试结果有关的出错处理;
6. 上面没有提及的其他标准。
h. 操作步骤。实施测试用例的执行步骤。对于每个操作应提供:
1. 每一步的测试输入;
2. 每一步所需的操作动作、测试程序的输入/输出操作、设备操作等;
3. 每一步的期望结果;
4. 每一步的评估准则;
5. 程序终止伴随的动作或错误指示;
6. 获取和分析实际测试结果的过程。
i. 前提和约束。在测试用例说明中施加的所有前提条件和约束条件,如果有特别限制、参数偏差或异常处理,应该标识出来,并说明它们对测试用例的影响。
j. 测试用例终止条件。说明测试正常终止和异常终止的条件。
k. 测试用例通过准则。判断测试用例是否通过的标准。

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"python-envs.defaultEnvManager": "ms-python.python:conda",
"python-envs.defaultPackageManager": "ms-python.python:conda"
}

View File

@@ -1,17 +1,8 @@
<div align="center">
<img src="./docs/images/github-cover-new.png" alt="RAG Web UI" />
<br />
<p>
<strong>基于 RAG 的知识库问答与文档处理平台</strong>
</p>
<p>
<a href="https://github.com/rag-web-ui/rag-web-ui/blob/main/LICENSE"><img src="https://img.shields.io/github/license/rag-web-ui/rag-web-ui" alt="License" /></a>
<a href="#"><img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="Python" /></a>
<a href="#"><img src="https://img.shields.io/badge/node-%3E%3D18-green.svg" alt="Node" /></a>
<a href="#"><img src="https://github.com/rag-web-ui/rag-web-ui/actions/workflows/test.yml/badge.svg" alt="CI" /></a>
</p>
<p>
<a href="#简介">简介</a> •
<a href="#核心能力">核心能力</a> •

View File

@@ -3,14 +3,14 @@
# LLM配置 - 阿里云千问
llm:
# 是否启用LLM设为false则使用纯规则提取
# 是否启用LLM当前版本必须为true
enabled: true
# LLM提供商qwen阿里云千问
provider: "qwen"
# 模型名称
model: "qwen3-max"
# API密钥统一由 rag-web-ui 的环境变量提供
api_key: ""
model: "glm-5"
# API密钥(建议使用环境变量 DASHSCOPE_API_KEY
api_key: "sk-7097f7842f724f0c9e70c4bf3b16dacb"
# 可选参数
temperature: 0.3
max_tokens: 1024
@@ -48,7 +48,7 @@ extraction:
priority: 1
接口需求:
prefix: "IR"
keywords: ["接口", "interface", "api", "外部接口", "内部接口", "CAN", "以太网", "通信"]
keywords: ["接口", "interface", "api", "外部接口", "内部接口", "输入输出"]
priority: 2
性能需求:
prefix: "PR"
@@ -68,23 +68,105 @@ extraction:
priority: 6
splitter:
enabled: true
max_sentence_len: 120
min_clause_len: 12
max_sentence_len: 160
min_clause_len: 20
semantic_type_policy:
interface_section_hints:
- "接口描述"
- "接口需求"
- "接口要求"
- "外部接口"
- "内部接口"
- "I/O"
interface_title_excludes:
- "计算机通信需求"
- "通信需求"
- "通信要求"
functional_section_hints:
- "功能需求"
- "功能要求"
other_section_hints:
- "安全性需求"
- "保密性需求"
- "适应性需求"
- "环境需求"
- "资源需求"
- "质量"
- "设计约束"
- "培训需求"
- "软件保障"
- "验收"
- "交付"
- "包装"
- "通信需求"
- "计算机通信需求"
- "硬件环境"
- "软件环境"
- "运行环境"
semantic_guard:
enabled: true
preserve_condition_action_chain: true
preserve_alarm_chain: true
system_description_hints:
- "系统描述"
- "功能描述"
- "概述"
- "示意图"
- "组成"
- "架构"
- "原理"
table_strategy:
llm_semantic_enabled: true
sequence_table_merge: "single_requirement"
merge_time_series_rows_min: 3
skip_keywords:
- "系统功能要求"
- "性能要求"
- "系统性能要求"
- "系统接口要求"
- "功能矩阵"
- "能力对照"
- "性能指标对照"
interface_keywords:
- "接口"
- "interface"
- "输入输出"
- "I/O"
- "数据来源"
- "数据目的地"
- "来源"
- "目的地"
single_requirement_keywords:
- "硬件要求"
- "软件要求"
- "运行环境"
- "硬件环境"
- "软件环境"
- "运行硬件环境"
- "运行软件环境"
- "环境需求"
- "资源需求"
- "计算机资源"
rewrite_policy:
llm_light_rewrite_enabled: true
preserve_ratio_min: 0.65
max_length_growth_ratio: 1.25
non_interface_max_edit_distance: 20
renumber_policy:
enabled: true
mode: "section_continuous"
dedup_policy:
similarity_threshold: 0.88
enable_cross_section_dedup: true
prefer_text_over_table: true
interface_policy:
unknown_fallback: "未知"
normalization_policy:
ocr_spacing_normalize: true
fidelity_policy:
preserve_source_text_for_text_blocks: true
punctuation_policy:
ensure_terminal_period: true
# 输出配置
output:

View File

@@ -4,7 +4,6 @@
支持PDF和Docx格式针对GJB438B标准SRS文档优化
"""
import os
import re
import logging
import importlib
@@ -119,43 +118,19 @@ class DocumentParser(ABC):
sections: 章节列表
parent_number: 父章节编号
"""
# 仅在顶级章节重编号
if not parent_number:
# 前置章节关键词(需要跳过的)
skip_keywords = ['目录', '封面', '扉页', '未命名', '', '']
# 正文章节关键词(遇到这些说明正文开始)
content_keywords = ['外部接口', '接口', '软件需求', '需求', '功能', '性能', '设计', '概述', '标识', '引言']
start_index = 0
for idx, section in enumerate(sections):
# 优先检查是否是正文章节
is_content = any(kw in section.title for kw in content_keywords)
if is_content and section.level == 1:
start_index = idx
break
# 重新编号所有章节
counter = 1
for i, section in enumerate(sections):
if i < start_index:
# 前置章节不编号
section.number = ""
else:
# 正文章节顶级章节从1开始编号
if section.level == 1:
section.number = str(counter)
counter += 1
# 递归处理子章节
if section.children:
self._auto_number_sections(section.children, section.number)
else:
# 子章节编号
for i, section in enumerate(sections, 1):
if not section.number or self._is_chinese_number(section.number):
section.generate_auto_number(parent_number, i)
if section.children:
self._auto_number_sections(section.children, section.number)
if not sections:
return
# 仅为缺失编号的章节补号;已存在的文档原始编号必须保留。
sibling_index = 0
for section in sections:
has_number = bool((section.number or "").strip()) and not self._is_chinese_number(section.number)
if not has_number:
sibling_index += 1
section.generate_auto_number(parent_number, sibling_index)
if section.children:
self._auto_number_sections(section.children, section.number)
def _is_chinese_number(self, text: str) -> bool:
"""检查是否是中文数字编号"""
@@ -327,8 +302,13 @@ class PDFParser(DocumentParser):
'优先', '关键', '合格', '追踪', '注释',
'CSCI', '计算机', '软件', '硬件', '通信', '通讯',
'数据', '适应', '可靠', '内部', '外部',
'描述', '要求', '规定', '说明', '定义',
'电场', '防护', '装置', '控制', '监控', '显控'
'描述', '要求', '规定', '说明', '定义'
]
TOP_LEVEL_TITLE_KEYWORDS = [
'范围', '标识', '概述', '引用', '文档', '需求', '接口', '性能',
'安全', '保密', '环境', '资源', '质量', '设计', '约束', '验收',
'交付', '包装', '注释'
]
# 明显无效的章节标题模式(噪声)
@@ -411,21 +391,41 @@ class PDFParser(DocumentParser):
if page_idx < len(self._page_texts):
page_text = self._page_texts[page_idx]
extracted_tables = page.extract_tables() or []
for table_idx, table in enumerate(extracted_tables):
table_objs = page.find_tables() or []
if table_objs:
extracted_tables = [(idx, t.extract(), t.bbox) for idx, t in enumerate(table_objs)]
else:
raw_tables = page.extract_tables() or []
extracted_tables = [(idx, t, None) for idx, t in enumerate(raw_tables)]
for table_idx, table, bbox in extracted_tables:
cleaned_table: List[List[str]] = []
for row in table or []:
cells = [re.sub(r'\s+', ' ', str(cell or '')).strip() for cell in row]
# 只要存在非空单元格就保留,避免有效行被误丢弃。
if any(cells):
cleaned_table.append(cells)
if cleaned_table:
section_hint = ""
if bbox:
try:
top = float(bbox[1])
text_above = page.crop((0, 0, page.width, top)).extract_text() or ""
section_hint = self._find_last_section_number(text_above)
except Exception:
section_hint = ""
table_ref = self._extract_table_reference(cleaned_table)
tables.append(
{
"page_idx": page_idx,
"table_idx": table_idx,
"page_text": page_text,
"data": cleaned_table,
"section_hint": section_hint,
"table_ref": table_ref,
}
)
except Exception as e:
@@ -435,16 +435,86 @@ class PDFParser(DocumentParser):
logger.info(f"PDF表格提取完成{len(tables)}个表格")
return tables
def _extract_table_reference(self, table: List[List[str]]) -> str:
"""从表格前几行中提取表号引用如“表3-5”。"""
if not table:
return ""
head_rows = table[:2]
merged = " ".join(" ".join(str(c or "") for c in row) for row in head_rows)
merged = re.sub(r"\s+", "", merged)
m = re.search(r"\s*(\d+(?:[-]\d+){1,3})", merged)
if not m:
return ""
return m.group(1).replace("", "-")
def _build_table_reference_index(self, sections: List[Section]) -> Dict[str, List[Section]]:
"""构建“表号 -> 章节”索引,用于优先精确挂接表格。"""
index: Dict[str, List[Section]] = {}
for section in sections:
content = re.sub(r"\s+", "", section.content or "")
for m in re.finditer(r"\s*(\d+(?:[-]\d+){1,3})", content):
ref = m.group(1).replace("", "-")
index.setdefault(ref, []).append(section)
return index
def _find_last_section_number(self, text: str) -> str:
"""从文本中提取最后出现的章节号。"""
if not text:
return ""
found = ""
for line in text.split("\n"):
line = line.strip()
if not line:
continue
section_info = self._match_section_header(line, set())
if section_info:
found = section_info[0]
return found
def _attach_pdf_tables_to_sections(self, tables: List[Dict[str, Any]]) -> None:
"""将提取出的PDF表格挂接到最匹配的章节。"""
flat_sections = self._flatten_sections(self.sections)
if not flat_sections:
return
section_by_number = {
(s.number or "").strip(): s
for s in flat_sections
if (s.number or "").strip()
}
table_ref_index = self._build_table_reference_index(flat_sections)
last_section: Optional[Section] = None
for table in tables:
matched = self._match_table_section(table.get("page_text", ""), flat_sections)
target = matched or last_section or flat_sections[0]
target = None
table_ref = (table.get("table_ref") or "").strip()
if table_ref and table_ref in table_ref_index:
candidates = table_ref_index[table_ref]
# 同表号命中多个章节时,优先更深层章节,避免父级“汇总章节”抢占。
target = max(candidates, key=lambda s: (s.level, len(s.content or "")))
section_hint = (table.get("section_hint") or "").strip()
if not target and section_hint and section_hint in section_by_number:
target = section_by_number[section_hint]
if not target:
target = self._match_table_section(table.get("page_text", ""), flat_sections)
# 兜底优先使用上一个命中章节,避免错误挂到首章节造成跨章污染。
if not target:
target = last_section
if not target:
logger.warning(
"未定位到表格归属章节,跳过: page=%s table=%s",
table.get("page_idx", -1),
table.get("table_idx", -1),
)
continue
target.add_table(table["data"])
last_section = target
@@ -464,7 +534,7 @@ class PDFParser(DocumentParser):
return None
matched: Optional[Section] = None
matched_score = -1
matched_score = (-1, -1)
for section in sections:
title = (section.title or "").strip()
if not title:
@@ -479,7 +549,7 @@ class PDFParser(DocumentParser):
for candidate in candidates:
normalized_candidate = re.sub(r"\s+", "", candidate).lower()
if normalized_candidate and normalized_candidate in normalized_page:
score = len(normalized_candidate)
score = (len(normalized_candidate), section.level)
if score > matched_score:
matched = section
matched_score = score
@@ -514,6 +584,7 @@ class PDFParser(DocumentParser):
current_section = None
content_buffer = []
found_sections = set()
last_top_level_number = 0
for line in lines:
line = line.strip()
@@ -526,6 +597,22 @@ class PDFParser(DocumentParser):
if section_info:
number, title = section_info
level = len(number.split('.'))
top_level_number = int(number.split('.')[0])
# 顶级章节序号大幅跳跃通常是误识别如正文中的“8 表...”)。
if level == 1 and last_top_level_number and top_level_number > last_top_level_number + 1:
if line and not self._is_noise(line):
content_buffer.append(line)
continue
# 顶级章节编号倒退通常是正文枚举项被误识别如“1 综合监控...”)。
if level == 1 and last_top_level_number and top_level_number < last_top_level_number:
if line and not self._is_noise(line):
content_buffer.append(line)
continue
if level > 6:
continue
# 保存之前章节的内容
if current_section and content_buffer:
@@ -540,6 +627,7 @@ class PDFParser(DocumentParser):
if level == 1:
sections.append(section)
section_stack = {1: section}
last_top_level_number = top_level_number
else:
parent_level = level - 1
while parent_level >= 1 and parent_level not in section_stack:
@@ -557,6 +645,10 @@ class PDFParser(DocumentParser):
for l in list(section_stack.keys()):
if l > level:
del section_stack[l]
# 若出现层级跳跃如1->3自动回退到父级+1。
if level > 1 and (level - 1) not in section_stack:
section.level = max(section_stack.keys()) if section_stack else 1
current_section = section
else:
@@ -577,13 +669,14 @@ class PDFParser(DocumentParser):
Returns:
(章节编号, 章节标题) 或 None
"""
# 模式: "3.1功能需求" "3.1 功能需求"
match = re.match(r'^(\d+(?:\.\d+)*)\s*(.+)$', line)
# 模式: "3.1 功能需求" / "3.1.2 电场..."
match = re.match(r'^(\d+(?:\.\d+)*)[\s、.)]*(.+)$', line)
if not match:
return None
number = match.group(1)
title = match.group(2).strip()
level = len(number.split('.'))
# 排除目录行
if '...' in title or title.count('.') > 5:
@@ -609,6 +702,18 @@ class PDFParser(DocumentParser):
# 标题长度检查
if len(title) > 60 or len(title) < 2:
return None
# 过滤更像正文描述的句式。
if self._looks_like_statement(title):
return None
# 过滤疑似正文句子(含句号/分号且过长)。
if len(title) > 24 and re.search(r'[。;;]', title):
return None
# 过滤指令拼接噪声标题(逗号过多通常是正文残片)。
if title.count('') >= 2 and len(title) > 20:
return None
# 放宽标题字符要求兼容部分PDF字体导致中文抽取异常的情况
if not re.search(r'[\u4e00-\u9fa5A-Za-z]', title):
@@ -631,8 +736,30 @@ class PDFParser(DocumentParser):
# 检查标题是否包含反斜杠(通常是表格噪声)
if '\\' in title and '需求' not in title:
return None
# 常见有效标题关键词兜底,降低正文被识别为标题的概率。
if not any(k in title for k in self.VALID_TITLE_KEYWORDS):
return None
# 顶级章节标题需符合SRS结构性关键词避免“综合监控”“电场”等正文短语被识别。
if level == 1 and not any(k in title for k in self.TOP_LEVEL_TITLE_KEYWORDS):
return None
return (number, title)
def _looks_like_statement(self, title: str) -> bool:
"""判断标题是否更像正文语句而非章节名。"""
if not title:
return False
statement_hints = ["", "能够", "可以", "进行", "通过", "", "同时", "", "如果", ""]
if any(h in title for h in statement_hints):
return True
if len(title) > 24 and re.search(r'[,。;;:]', title):
return True
return False
def _is_noise(self, line: str) -> bool:
"""检查是否是噪声行"""

View File

@@ -146,8 +146,8 @@ class JSONGenerator:
if req.type == 'interface':
req_dict["接口名称"] = req.interface_name
req_dict["接口类型"] = req.interface_type
req_dict["来源"] = req.source
req_dict["目的地"] = req.destination
req_dict["数据来源"] = req.source
req_dict["数据目的地"] = req.destination
result["需求列表"].append(req_dict)
# 如果有子章节,添加子章节

View File

@@ -7,6 +7,7 @@ import logging
import json
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Any
import requests
from .utils import get_env_or_config
@@ -86,6 +87,34 @@ class QwenLLM(LLMInterface):
except ImportError:
logger.error("dashscope库未安装请运行: pip install dashscope")
raise
def _call_compatible_mode(self, prompt: str) -> str:
"""使用OpenAI兼容模式HTTP接口调用千问。"""
endpoint = self.api_endpoint.rstrip("/") + "/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
payload = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
"temperature": self.extra_params.get("temperature", 0.3),
"max_tokens": self.extra_params.get("max_tokens", 1024),
}
resp = requests.post(endpoint, headers=headers, json=payload, timeout=120)
if resp.status_code != 200:
raise Exception(f"兼容模式API调用失败: {resp.status_code} {resp.text[:300]}")
data = resp.json()
choices = data.get("choices", [])
if not choices:
raise Exception(f"兼容模式API返回无choices: {str(data)[:300]}")
message = choices[0].get("message", {})
content = message.get("content", "")
if not content:
raise Exception(f"兼容模式API返回空内容: {str(data)[:300]}")
return content
def call(self, prompt: str) -> str:
"""
@@ -153,6 +182,11 @@ class QwenLLM(LLMInterface):
return str(response)
except Exception as e:
err_msg = str(e)
# DashScope旧路径报url error时回退到兼容模式接口。
if "url error" in err_msg.lower() or "status: 400" in err_msg.lower():
logger.warning("DashScope调用失败回退兼容模式接口: %s", err_msg)
return self._call_compatible_mode(prompt)
logger.error(f"调用千问LLM失败: {e}")
raise

View File

@@ -33,8 +33,10 @@ class RequirementSplitter:
CONNECTOR_HINTS = ["", "并且", "同时", "然后", "", "以及", ""]
CONDITIONAL_HINTS = ["如果", "", "", "", "其中", "此时", "满足"]
CONTEXT_PRONOUN_HINTS = ["", "", "上述", "", "这些", "那些"]
CHAIN_HINTS = ["从而", "以便", "用于", "以实现", "并据此", "进而", "从而实现"]
ENUMERATION_HINTS = ["具体包括", "包括但不限于", "主要包括", "其中包括", "如下"]
def __init__(self, max_sentence_len: int = 120, min_clause_len: int = 12):
def __init__(self, max_sentence_len: int = 160, min_clause_len: int = 20):
self.max_sentence_len = max_sentence_len
self.min_clause_len = min_clause_len
@@ -107,6 +109,14 @@ class RequirementSplitter:
if len(current) < self.min_clause_len:
return False
# “具体包括/其中包括”后的列举项通常是上一句延伸,不应拆分为独立需求。
if any(h in current for h in self.ENUMERATION_HINTS):
return False
# 承接链条短语一般不是独立需求动作,避免切断语义链。
if any(fragment.startswith(h) for h in self.CHAIN_HINTS):
return False
# 指代承接片段通常是语义延续,不应切断。
if any(fragment.startswith(h) for h in self.CONTEXT_PRONOUN_HINTS):
return False
@@ -123,6 +133,12 @@ class RequirementSplitter:
has_action = any(h in fragment for h in self.ACTION_HINTS)
current_has_action = any(h in current for h in self.ACTION_HINTS)
# 并列连接词后接“控制/处理/显示”等限定短语时,优先视为同一需求。
if has_connector and len(fragment) < self.max_sentence_len // 3 and not any(
kw in fragment for kw in ["并输出", "并上传", "并记录", "并触发"]
):
return False
# 连接词 + 动作词,且当前片段已经包含动作,优先拆分。
if has_connector and has_action and current_has_action:
return True
@@ -147,6 +163,9 @@ class RequirementSplitter:
return merged
def _should_merge(self, prev: str, current: str) -> bool:
if any(h in prev for h in self.ENUMERATION_HINTS):
return True
# 指代开头:如“该报警信号...”。
if any(current.startswith(h) for h in self.CONTEXT_PRONOUN_HINTS):
return True

View File

@@ -60,6 +60,46 @@ class AppSettings:
"other": "OR",
}
DEFAULT_INTERFACE_SECTION_HINTS = [
"接口描述",
"接口需求",
"接口要求",
"外部接口",
"内部接口",
"i/o",
]
DEFAULT_INTERFACE_TITLE_EXCLUDES = [
"计算机通信需求",
"通信需求",
"通信要求",
]
DEFAULT_FUNCTIONAL_SECTION_HINTS = [
"功能需求",
"功能要求",
]
DEFAULT_OTHER_SECTION_HINTS = [
"安全性需求",
"保密性需求",
"适应性需求",
"环境需求",
"资源需求",
"质量",
"设计约束",
"培训需求",
"软件保障",
"验收",
"交付",
"包装",
"通信需求",
"计算机通信需求",
"硬件环境",
"软件环境",
"运行环境",
]
def __init__(self, config: Dict[str, Any] = None):
self.config = config or {}
@@ -75,6 +115,20 @@ class AppSettings:
self.type_prefix = self._build_type_prefix(req_types_cfg)
self.type_chinese = self._build_type_chinese(req_types_cfg)
semantic_type_cfg = extraction_cfg.get("semantic_type_policy", {})
self.interface_section_hints = [
str(x).lower() for x in semantic_type_cfg.get("interface_section_hints", self.DEFAULT_INTERFACE_SECTION_HINTS)
]
self.interface_title_excludes = [
str(x).lower() for x in semantic_type_cfg.get("interface_title_excludes", self.DEFAULT_INTERFACE_TITLE_EXCLUDES)
]
self.functional_section_hints = [
str(x).lower() for x in semantic_type_cfg.get("functional_section_hints", self.DEFAULT_FUNCTIONAL_SECTION_HINTS)
]
self.other_section_hints = [
str(x).lower() for x in semantic_type_cfg.get("other_section_hints", self.DEFAULT_OTHER_SECTION_HINTS)
]
splitter_cfg = extraction_cfg.get("splitter", {})
self.splitter_max_sentence_len = int(splitter_cfg.get("max_sentence_len", 120))
self.splitter_min_clause_len = int(splitter_cfg.get("min_clause_len", 12))
@@ -91,16 +145,61 @@ class AppSettings:
self.table_llm_semantic_enabled = bool(table_cfg.get("llm_semantic_enabled", True))
self.sequence_table_merge = table_cfg.get("sequence_table_merge", "single_requirement")
self.merge_time_series_rows_min = int(table_cfg.get("merge_time_series_rows_min", 3))
self.table_skip_keywords = list(
table_cfg.get(
"skip_keywords",
["系统功能要求", "性能要求", "功能矩阵", "能力对照", "性能指标对照"],
)
)
self.table_interface_keywords = list(
table_cfg.get(
"interface_keywords",
["接口", "interface", "输入输出", "I/O", "数据来源", "数据目的地", "来源", "目的地"],
)
)
self.table_single_requirement_keywords = list(
table_cfg.get(
"single_requirement_keywords",
["硬件要求", "软件要求", "运行环境", "环境需求", "资源需求", "计算机资源"],
)
)
rewrite_cfg = extraction_cfg.get("rewrite_policy", {})
self.llm_light_rewrite_enabled = bool(rewrite_cfg.get("llm_light_rewrite_enabled", True))
self.preserve_ratio_min = float(rewrite_cfg.get("preserve_ratio_min", 0.65))
self.max_length_growth_ratio = float(rewrite_cfg.get("max_length_growth_ratio", 1.25))
self.non_interface_max_edit_distance = int(rewrite_cfg.get("non_interface_max_edit_distance", 20))
self.system_description_hints = list(
extraction_cfg.get(
"system_description_hints",
["系统描述", "功能描述", "概述", "示意图", "组成", "架构", "原理"],
)
)
renumber_cfg = extraction_cfg.get("renumber_policy", {})
self.renumber_enabled = bool(renumber_cfg.get("enabled", True))
self.renumber_mode = renumber_cfg.get("mode", "section_continuous")
dedup_cfg = extraction_cfg.get("dedup_policy", {})
self.dedup_similarity_threshold = float(dedup_cfg.get("similarity_threshold", 0.88))
self.enable_cross_section_dedup = bool(dedup_cfg.get("enable_cross_section_dedup", True))
self.prefer_text_over_table = bool(dedup_cfg.get("prefer_text_over_table", True))
interface_cfg = extraction_cfg.get("interface_policy", {})
self.interface_unknown_fallback = str(interface_cfg.get("unknown_fallback", "未知"))
normalization_cfg = extraction_cfg.get("normalization_policy", {})
self.ocr_spacing_normalize = bool(normalization_cfg.get("ocr_spacing_normalize", True))
fidelity_cfg = extraction_cfg.get("fidelity_policy", {})
self.preserve_source_text_for_text_blocks = bool(
fidelity_cfg.get("preserve_source_text_for_text_blocks", True)
)
punctuation_cfg = extraction_cfg.get("punctuation_policy", {})
self.ensure_terminal_period = bool(punctuation_cfg.get("ensure_terminal_period", True))
def _build_rules(self, req_types_cfg: Dict[str, Dict[str, Any]]) -> List[RequirementTypeRule]:
rules: List[RequirementTypeRule] = []
if not req_types_cfg:
@@ -153,10 +252,45 @@ class AppSettings:
def is_non_requirement_section(self, title: str) -> bool:
return any(keyword in title for keyword in self.non_requirement_sections)
def is_interface_semantic_title(self, title: str) -> bool:
t = (title or "").strip().lower()
if not t:
return False
excluded = any(x in t for x in self.interface_title_excludes)
if excluded and "接口" not in t:
return False
return any(h in t for h in self.interface_section_hints)
def is_functional_semantic_title(self, title: str) -> bool:
t = (title or "").strip().lower()
if not t:
return False
return any(h in t for h in self.functional_section_hints)
def is_other_semantic_title(self, title: str) -> bool:
t = (title or "").strip().lower()
if not t:
return False
return any(h in t for h in self.other_section_hints)
def detect_requirement_type(self, title: str, content: str) -> str:
# 章节语义优先:接口仅由接口类章节触发;安全/保密/适应性等统一归其他需求。
if self.is_interface_semantic_title(title):
return "interface"
if self.is_functional_semantic_title(title):
return "functional"
if self.is_other_semantic_title(title):
return "other"
combined_text = f"{title} {(content or '')[:500]}".lower()
for rule in self.requirement_rules:
if rule.key == "interface" and not self.is_interface_semantic_title(title):
continue
for keyword in rule.keywords:
if keyword.lower() in combined_text:
if rule.key in {"performance", "security", "reliability", "other"}:
return "other"
return rule.key
return "functional"

View File

@@ -55,6 +55,9 @@ class SRSTool:
ToolRegistry.register(self.DEFINITION)
def run(self, file_path: str, enable_llm: bool = True) -> Dict[str, Any]:
if not enable_llm:
raise ValueError("当前版本仅支持LLM模式请将 enable_llm 设为 true")
config = self._load_config()
llm = self._build_llm(config, enable_llm=enable_llm)
@@ -122,12 +125,12 @@ class SRSTool:
def _build_llm(self, config: Dict[str, Any], enable_llm: bool) -> QwenLLM | None:
if not enable_llm:
return None
raise ValueError("当前版本仅支持LLM模式")
llm_cfg = config.get("llm", {})
api_key = llm_cfg.get("api_key")
if not api_key:
return None
raise ValueError("未配置API密钥请设置 DASH_SCOPE_API_KEY 或 DASHSCOPE_API_KEY")
return QwenLLM(
api_key=api_key,

View File

@@ -22,6 +22,7 @@ unstructured[md]>=0.10.0
openai>=1.30.0
email-validator
dashscope>=1.13.6
requests>=2.31.0
langchain-deepseek==0.1.1
langchain-ollama==0.2.3
docx2txt==0.8

View File

@@ -3,7 +3,14 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { FileIcon, defaultStyles } from "react-file-icon";
import { ArrowRight, Plus, Settings, Trash2, Search } from "lucide-react";
import {
ArrowRight,
ChevronRight,
Plus,
Search,
Settings,
Trash2,
} from "lucide-react";
import DashboardLayout from "@/components/layout/dashboard-layout";
import { api, ApiError } from "@/lib/api";
import { useToast } from "@/components/ui/use-toast";
@@ -29,6 +36,9 @@ interface Document {
export default function KnowledgeBasePage() {
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
const [collapsedDocumentSections, setCollapsedDocumentSections] = useState<
Record<number, boolean>
>({});
const [loading, setLoading] = useState(true);
const { toast } = useToast();
@@ -76,6 +86,13 @@ export default function KnowledgeBasePage() {
}
};
const toggleDocumentsSection = (knowledgeBaseId: number) => {
setCollapsedDocumentSections((prev) => ({
...prev,
[knowledgeBaseId]: !prev[knowledgeBaseId],
}));
};
return (
<DashboardLayout>
<div className="space-y-8">
@@ -98,11 +115,15 @@ export default function KnowledgeBasePage() {
</div>
<div className="grid gap-6">
{knowledgeBases.map((kb) => (
<div
key={kb.id}
className="rounded-lg border bg-card p-6 space-y-4"
>
{knowledgeBases.map((kb) => {
const isDocumentsCollapsed =
collapsedDocumentSections[kb.id] ?? true;
return (
<div
key={kb.id}
className="rounded-lg border bg-card p-6 space-y-4"
>
<div className="flex justify-between items-start">
<div>
<h3 className="text-lg font-semibold">{kb.name}</h3>
@@ -139,61 +160,79 @@ export default function KnowledgeBasePage() {
{kb.documents.length > 0 && (
<div className="border-t pt-4">
<h4 className="text-sm font-medium mb-2"></h4>
<div className="flex flex-wrap gap-2 max-h-[400px] overflow-y-auto">
{kb.documents.slice(0, 9).map((doc) => (
<div
key={doc.id}
className="flex flex-col items-center gap-2 p-2 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors w-[150px] h-[150px] justify-center"
>
<div className="w-8 h-8 mb-2">
{doc.content_type.toLowerCase().includes("pdf") ? (
<FileIcon extension="pdf" {...defaultStyles.pdf} />
) : doc.content_type.toLowerCase().includes("doc") ? (
<FileIcon extension="doc" {...defaultStyles.docx} />
) : doc.content_type.toLowerCase().includes("txt") ? (
<FileIcon extension="txt" {...defaultStyles.txt} />
) : doc.content_type.toLowerCase().includes("md") ? (
<FileIcon extension="md" {...defaultStyles.md} />
) : (
<FileIcon
extension={doc.file_name.split(".").pop() || ""}
color="#E2E8F0"
labelColor="#94A3B8"
/>
)}
</div>
<div className="text-sm font-medium text-center max-w-[100px]">
<div className="line-clamp-2 overflow-hidden text-ellipsis">
{doc.file_name}
<button
type="button"
onClick={() => toggleDocumentsSection(kb.id)}
className="mb-2 flex w-full items-center justify-between rounded-md px-1 py-1 text-left hover:bg-accent/50"
>
<h4 className="text-sm font-medium"></h4>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>{isDocumentsCollapsed ? "展开" : "收起"}</span>
<ChevronRight
className={`h-4 w-4 transition-transform ${
isDocumentsCollapsed ? "" : "rotate-90"
}`}
/>
</div>
</button>
{!isDocumentsCollapsed && (
<div className="flex flex-wrap gap-2 max-h-[400px] overflow-y-auto">
{kb.documents.slice(0, 9).map((doc) => (
<div
key={doc.id}
className="flex flex-col items-center gap-2 p-2 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors w-[150px] h-[150px] justify-center"
>
<div className="w-8 h-8 mb-2">
{doc.content_type.toLowerCase().includes("pdf") ? (
<FileIcon extension="pdf" {...defaultStyles.pdf} />
) : doc.content_type.toLowerCase().includes("doc") ? (
<FileIcon extension="doc" {...defaultStyles.docx} />
) : doc.content_type.toLowerCase().includes("txt") ? (
<FileIcon extension="txt" {...defaultStyles.txt} />
) : doc.content_type.toLowerCase().includes("md") ? (
<FileIcon extension="md" {...defaultStyles.md} />
) : (
<FileIcon
extension={doc.file_name.split(".").pop() || ""}
color="#E2E8F0"
labelColor="#94A3B8"
/>
)}
</div>
<div className="text-sm font-medium text-center max-w-[100px]">
<div className="line-clamp-2 overflow-hidden text-ellipsis">
{doc.file_name}
</div>
</div>
<span className="text-xs text-muted-foreground mt-1">
{new Date(doc.created_at).toLocaleDateString("zh-CN")}
</span>
</div>
<span className="text-xs text-muted-foreground mt-1">
{new Date(doc.created_at).toLocaleDateString("zh-CN")}
</span>
</div>
))}
{kb.documents.length > 9 && (
<Link
href={`/dashboard/knowledge/${kb.id}`}
className="flex flex-col items-center p-2 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors w-[150px] h-[150px] justify-center"
>
<div className="w-8 h-8 mb-2 flex items-center justify-center">
<ArrowRight className="w-6 h-6" />
</div>
<span className="text-sm font-medium text-center">
</span>
<span className="text-xs text-muted-foreground mt-1">
{kb.documents.length}
</span>
</Link>
)}
</div>
))}
{kb.documents.length > 9 && (
<Link
href={`/dashboard/knowledge/${kb.id}`}
className="flex flex-col items-center p-2 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors w-[150px] h-[150px] justify-center"
>
<div className="w-8 h-8 mb-2 flex items-center justify-center">
<ArrowRight className="w-6 h-6" />
</div>
<span className="text-sm font-medium text-center">
</span>
<span className="text-xs text-muted-foreground mt-1">
{kb.documents.length}
</span>
</Link>
)}
</div>
)}
</div>
)}
</div>
))}
</div>
);
})}
{!loading && knowledgeBases.length === 0 && (
<div className="text-center py-12">