speccore 6.40.0 → 6.40.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +20 -37
- package/dist/commands/analyze.js.map +1 -1
- package/dist/core/analyze-engine.d.ts.map +1 -1
- package/dist/core/analyze-engine.js +464 -27
- package/dist/core/analyze-engine.js.map +1 -1
- package/package.json +1 -1
|
@@ -1172,7 +1172,7 @@ async function writePerPlatform(iterDir, report, filename) {
|
|
|
1172
1172
|
.map(e => e.name);
|
|
1173
1173
|
if (features.length === 0)
|
|
1174
1174
|
return;
|
|
1175
|
-
// 平台列表从 CONSTITUTION
|
|
1175
|
+
// 平台列表从 CONSTITUTION 获取(不再硬编码默认值)
|
|
1176
1176
|
const platforms = await detectPlatformsFromConstitution();
|
|
1177
1177
|
for (const platform of platforms) {
|
|
1178
1178
|
const platformDir = (0, path_1.join)(specsBase, platform);
|
|
@@ -1193,10 +1193,13 @@ async function writePerPlatform(iterDir, report, filename) {
|
|
|
1193
1193
|
}
|
|
1194
1194
|
/** 端关键词映射表(用于从合并报告中提取特定端的内容) */
|
|
1195
1195
|
const PLATFORM_KEYWORDS = {
|
|
1196
|
-
admin: ['后台管理', '管理端', 'admin', 'Admin', 'Web管理', '后台'],
|
|
1197
|
-
h5: ['H5', 'h5', '移动端', 'mobile', 'Mobile', 'H5移动'],
|
|
1196
|
+
admin: ['后台管理', '管理端', 'admin', 'Admin', 'Web管理', '后台', '管理后台', '数据看板', '用户管理', '数据报表', '权限管理'],
|
|
1197
|
+
h5: ['H5', 'h5', '移动端', 'mobile', 'Mobile', 'H5移动', '快速预订', '扫码签到', '我的预订', '手机'],
|
|
1198
1198
|
miniapp: ['小程序', 'miniapp', 'MiniApp', '微信'],
|
|
1199
|
-
app: ['
|
|
1199
|
+
app: ['客户端', 'app', 'App', '原生'],
|
|
1200
|
+
'booking-service': ['预订', '订单', 'booking', '预订域', '预订生命周期'],
|
|
1201
|
+
'room-service': ['会议室', 'room', '会议室域', '会议室管理'],
|
|
1202
|
+
backend: ['后端服务', 'backend', '服务端', '接口', '数据模型', '业务域'],
|
|
1200
1203
|
web: ['Web', 'web', '桌面端', 'PC'],
|
|
1201
1204
|
android: ['Android', 'android', '安卓'],
|
|
1202
1205
|
ios: ['iOS', 'ios', '苹果'],
|
|
@@ -1621,21 +1624,193 @@ async function supplementAnalysis(input) {
|
|
|
1621
1624
|
remainingUncovered: remaining,
|
|
1622
1625
|
};
|
|
1623
1626
|
}
|
|
1624
|
-
/**
|
|
1627
|
+
/** 已检测到的后端平台列表(从 CONSTITUTION.md 解析) */
|
|
1628
|
+
let _detectedBackendPlatforms = [];
|
|
1629
|
+
/** 从 CONSTITUTION.md 动态提取的端名映射(技术栈标题解析) */
|
|
1630
|
+
let _dynamicPlatformAliases = {};
|
|
1631
|
+
/**
|
|
1632
|
+
* 从 CONSTITUTION.md 技术栈章节标题提取端名信息。
|
|
1633
|
+
* 匹配模式: ### 中文端名 (English Name)
|
|
1634
|
+
* 例如: ### 后台管理端 (Admin Dashboard) → { chinese: '后台管理端', english: 'Admin Dashboard' }
|
|
1635
|
+
*/
|
|
1636
|
+
function parseTechStackHeaders(content) {
|
|
1637
|
+
const results = [];
|
|
1638
|
+
const lines = content.split('\n');
|
|
1639
|
+
for (const line of lines) {
|
|
1640
|
+
// 匹配: ### 后台管理端 (Admin Dashboard) 或 ### H5 移动端 (Mobile H5)
|
|
1641
|
+
const match = line.match(/^###\s+(.+?)\s*\(([^)]+)\)/);
|
|
1642
|
+
if (match) {
|
|
1643
|
+
results.push({
|
|
1644
|
+
chinese: match[1].trim(),
|
|
1645
|
+
english: match[2].trim(),
|
|
1646
|
+
fullTitle: line.trim(),
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return results;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* 将技术栈标题解析结果合并到动态别名映射中。
|
|
1654
|
+
* 这样 inferPlatformFromPathOrContent 也能使用 CONSTITUTION.md 定义的端名。
|
|
1655
|
+
*/
|
|
1656
|
+
function buildDynamicAliasesFromTechStack(techStackEntries) {
|
|
1657
|
+
const aliases = {};
|
|
1658
|
+
for (const entry of techStackEntries) {
|
|
1659
|
+
const normalized = normalizeToStandardPlatform(entry.chinese) || normalizeToStandardPlatform(entry.english);
|
|
1660
|
+
if (!normalized)
|
|
1661
|
+
continue;
|
|
1662
|
+
if (!aliases[normalized]) {
|
|
1663
|
+
aliases[normalized] = [];
|
|
1664
|
+
}
|
|
1665
|
+
// 添加中文名、英文名、全标题作为别名
|
|
1666
|
+
const newAliases = [entry.chinese, entry.english, entry.fullTitle];
|
|
1667
|
+
for (const a of newAliases) {
|
|
1668
|
+
const lower = a.toLowerCase();
|
|
1669
|
+
if (!aliases[normalized].includes(lower)) {
|
|
1670
|
+
aliases[normalized].push(lower);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return aliases;
|
|
1675
|
+
}
|
|
1676
|
+
/** 从 CONSTITUTION.md 提取平台列表(两层确定性匹配)
|
|
1677
|
+
* Layer 1: 表格「对应需求端」列(用户显式声明)
|
|
1678
|
+
* Layer 2: 技术栈章节标题 ### 中文端名 (English Name)
|
|
1679
|
+
* ⚠️ 不再提供硬编码默认值 — 端列表应由 AI 根据项目实际情况判断
|
|
1680
|
+
*/
|
|
1625
1681
|
async function detectPlatformsFromConstitution() {
|
|
1626
1682
|
try {
|
|
1627
1683
|
const constitutionPath = (0, path_1.join)(process.cwd(), '.speccore', 'CONSTITUTION.md');
|
|
1628
1684
|
if (require('fs').existsSync(constitutionPath)) {
|
|
1629
1685
|
const content = require('fs').readFileSync(constitutionPath, 'utf-8');
|
|
1630
|
-
|
|
1686
|
+
const lines = content.split('\n');
|
|
1687
|
+
// ── Layer 1: 表格「对应需求端」列 ──
|
|
1688
|
+
let headerRowIndex = -1;
|
|
1689
|
+
let headerCells = [];
|
|
1690
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1691
|
+
const cells = lines[i].split('|').map((c) => c.trim()).filter(Boolean);
|
|
1692
|
+
const platformColIdx = cells.findIndex((c) => c.includes('对应需求端'));
|
|
1693
|
+
if (platformColIdx >= 0) {
|
|
1694
|
+
headerRowIndex = i;
|
|
1695
|
+
headerCells = cells;
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (headerRowIndex >= 0) {
|
|
1700
|
+
const platforms = [];
|
|
1701
|
+
const backendPlatforms = [];
|
|
1702
|
+
const seen = new Set();
|
|
1703
|
+
for (let i = headerRowIndex + 2; i < lines.length; i++) {
|
|
1704
|
+
const line = lines[i].trim();
|
|
1705
|
+
// 分隔行跳过(如 | :--- | :--- |)
|
|
1706
|
+
if (line.match(/^\|[\s:-]+/))
|
|
1707
|
+
continue;
|
|
1708
|
+
// 空行跳过(表格内可能有空行)
|
|
1709
|
+
if (line === '')
|
|
1710
|
+
continue;
|
|
1711
|
+
// 非表格行 → 当前表格结束,终止读取
|
|
1712
|
+
if (!line.startsWith('|'))
|
|
1713
|
+
break;
|
|
1714
|
+
const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
|
|
1715
|
+
const projectName = cells[0] || '';
|
|
1716
|
+
const platformChinese = cells[5] || cells[cells.length - 1] || '';
|
|
1717
|
+
// 跳过空值和占位符(如「待填写」)
|
|
1718
|
+
if (!platformChinese || !projectName)
|
|
1719
|
+
continue;
|
|
1720
|
+
if (/待填写|待补充|TODO|TBD|N\/A/i.test(platformChinese))
|
|
1721
|
+
continue;
|
|
1722
|
+
const normalized = normalizeToStandardPlatform(platformChinese);
|
|
1723
|
+
if (normalized && !seen.has(normalized)) {
|
|
1724
|
+
seen.add(normalized);
|
|
1725
|
+
platforms.push(normalized);
|
|
1726
|
+
const isBackend = /service|server|api|backend|后台|服务|后端/i.test(projectName) ||
|
|
1727
|
+
/后台|服务|后端/.test(platformChinese);
|
|
1728
|
+
if (isBackend) {
|
|
1729
|
+
backendPlatforms.push(normalized);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (platforms.length > 0) {
|
|
1734
|
+
_detectedBackendPlatforms = backendPlatforms;
|
|
1735
|
+
// Layer 1 成功:也解析技术栈标题来构建动态别名
|
|
1736
|
+
const techStackEntries = parseTechStackHeaders(content);
|
|
1737
|
+
if (techStackEntries.length > 0) {
|
|
1738
|
+
_dynamicPlatformAliases = buildDynamicAliasesFromTechStack(techStackEntries);
|
|
1739
|
+
}
|
|
1740
|
+
return platforms;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
// ── Layer 2: 技术栈章节标题 ### 中文端名 (English Name) ──
|
|
1744
|
+
const techStackEntries = parseTechStackHeaders(content);
|
|
1745
|
+
if (techStackEntries.length > 0) {
|
|
1746
|
+
const platforms = [];
|
|
1747
|
+
const backendPlatforms = [];
|
|
1748
|
+
const seen = new Set();
|
|
1749
|
+
for (const entry of techStackEntries) {
|
|
1750
|
+
const normalized = normalizeToStandardPlatform(entry.chinese) || normalizeToStandardPlatform(entry.english);
|
|
1751
|
+
if (!normalized || seen.has(normalized))
|
|
1752
|
+
continue;
|
|
1753
|
+
seen.add(normalized);
|
|
1754
|
+
platforms.push(normalized);
|
|
1755
|
+
// 判断前后端
|
|
1756
|
+
const isBackend = /service|server|api|backend|后台|服务|后端/i.test(entry.chinese + ' ' + entry.english);
|
|
1757
|
+
if (isBackend) {
|
|
1758
|
+
backendPlatforms.push(normalized);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (platforms.length > 0) {
|
|
1762
|
+
_detectedBackendPlatforms = backendPlatforms;
|
|
1763
|
+
_dynamicPlatformAliases = buildDynamicAliasesFromTechStack(techStackEntries);
|
|
1764
|
+
logger_1.logger.info(` 📋 从技术栈标题检测到 ${platforms.length} 个端: ${platforms.join(', ')}`);
|
|
1765
|
+
return platforms;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
// Layer 2.5: 简单正则匹配(兼容旧格式)
|
|
1631
1769
|
const match = content.match(/对应需求端[||]\s*([a-z,\s]+)/i);
|
|
1632
1770
|
if (match) {
|
|
1633
|
-
|
|
1771
|
+
const result = match[1].split(/[,,]/).map((s) => s.trim()).filter(Boolean);
|
|
1772
|
+
if (result.length > 0)
|
|
1773
|
+
return result;
|
|
1634
1774
|
}
|
|
1635
1775
|
}
|
|
1636
1776
|
}
|
|
1637
1777
|
catch { }
|
|
1638
|
-
|
|
1778
|
+
// ⚠️ 不再硬编码默认端列表 — 端列表应由 AI 根据 CONSTITUTION.md + 需求文档判断
|
|
1779
|
+
// 如果 Layer 1 和 Layer 2 都无法检测到端,返回空数组,由 AI 在 prompt 中自行发现
|
|
1780
|
+
return [];
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* 将中文端名/工程名归一化为标准端名
|
|
1784
|
+
* 使用 PLATFORM_ALIAS_MAP 进行语义匹配
|
|
1785
|
+
*/
|
|
1786
|
+
function normalizeToStandardPlatform(name) {
|
|
1787
|
+
const nameLower = name.toLowerCase().trim();
|
|
1788
|
+
// 【v6.40.2 修复】两阶段最长匹配策略:
|
|
1789
|
+
// Phase 1: 精确匹配(name === alias),最长优先
|
|
1790
|
+
// Phase 2: 包含匹配(name.includes(alias) 或 alias.includes(name)),最长优先
|
|
1791
|
+
// 避免「后台服务端」被短别名「后台」先匹配到 admin
|
|
1792
|
+
// 避免「移动端」被更长别名「移动端app」误匹配到 app
|
|
1793
|
+
const allPairs = [];
|
|
1794
|
+
for (const [standardName, aliases] of Object.entries(PLATFORM_ALIAS_MAP)) {
|
|
1795
|
+
for (const alias of aliases) {
|
|
1796
|
+
allPairs.push({ alias: alias.toLowerCase(), platform: standardName });
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
allPairs.sort((a, b) => b.alias.length - a.alias.length);
|
|
1800
|
+
// Phase 1: 精确匹配
|
|
1801
|
+
for (const pair of allPairs) {
|
|
1802
|
+
if (nameLower === pair.alias) {
|
|
1803
|
+
return pair.platform;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
// Phase 2: 包含匹配
|
|
1807
|
+
for (const pair of allPairs) {
|
|
1808
|
+
if (nameLower.includes(pair.alias) || pair.alias.includes(nameLower)) {
|
|
1809
|
+
return pair.platform;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
// 无法映射时,返回清理后的原始值
|
|
1813
|
+
return nameLower.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || null;
|
|
1639
1814
|
}
|
|
1640
1815
|
/**
|
|
1641
1816
|
* 从需求文档内容中提取结构化信息,生成有实质内容的 Spec 文件。
|
|
@@ -1683,7 +1858,12 @@ async function generateSpecsFromRequirements(reqPaths, iteration, specDir) {
|
|
|
1683
1858
|
if (unclassifiedFiles.length > 0) {
|
|
1684
1859
|
logger_1.logger.info('');
|
|
1685
1860
|
logger_1.logger.info(` ❓ 发现 ${unclassifiedFiles.length} 个文档无法自动识别所属端`);
|
|
1686
|
-
|
|
1861
|
+
if (platforms.length > 0) {
|
|
1862
|
+
logger_1.logger.info(` 💡 可用端列表: ${platforms.join(', ')}`);
|
|
1863
|
+
}
|
|
1864
|
+
else {
|
|
1865
|
+
logger_1.logger.info(` 💡 CONSTITUTION.md 未检测到端列表,将由 AI 根据需求文档内容判断`);
|
|
1866
|
+
}
|
|
1687
1867
|
logger_1.logger.info('');
|
|
1688
1868
|
for (const file of unclassifiedFiles) {
|
|
1689
1869
|
const fileName = file.path.split(/[/\\]/).pop() || file.path;
|
|
@@ -1776,11 +1956,11 @@ async function generateSpecsFromRequirements(reqPaths, iteration, specDir) {
|
|
|
1776
1956
|
const techContent = buildTechSpecForPlatform(iteration, now, apis, dataModels, archImpact, platform, features, uiPatterns, platformContents);
|
|
1777
1957
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(platformDir, 'TECH.md'), techContent);
|
|
1778
1958
|
// TEST.md — 该端测试计划
|
|
1779
|
-
const testContent = buildTestSpecForPlatform(iteration, now, features, apis, platform);
|
|
1959
|
+
const testContent = buildTestSpecForPlatform(iteration, now, features, apis, platform, platformContents);
|
|
1780
1960
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(platformDir, 'TEST.md'), testContent);
|
|
1781
1961
|
// UI_SPEC.md — 该端 UI 规格(仅前端)
|
|
1782
1962
|
if (!isBackendPlatform(platform)) {
|
|
1783
|
-
const uiContent = buildUISpecForPlatform(iteration, now, uiPatterns, platform);
|
|
1963
|
+
const uiContent = buildUISpecForPlatform(iteration, now, uiPatterns, platform, platformContents);
|
|
1784
1964
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(platformDir, 'UI_SPEC.md'), uiContent);
|
|
1785
1965
|
}
|
|
1786
1966
|
}
|
|
@@ -1910,6 +2090,8 @@ function extractBusinessRules(content) {
|
|
|
1910
2090
|
}
|
|
1911
2091
|
// ── 辅助函数:判断是否为后端平台 ──
|
|
1912
2092
|
function isBackendPlatform(platform) {
|
|
2093
|
+
if (_detectedBackendPlatforms.includes(platform))
|
|
2094
|
+
return true;
|
|
1913
2095
|
return platform === 'backend' || platform.startsWith('后台') || platform.includes('服务');
|
|
1914
2096
|
}
|
|
1915
2097
|
function stripTemplateNoise(content) {
|
|
@@ -2068,10 +2250,19 @@ function buildTechSpecForPlatform(iter, now, apis, models, archImpact, platform,
|
|
|
2068
2250
|
// 前端专属内容
|
|
2069
2251
|
md += `## 1. 页面结构\n\n`;
|
|
2070
2252
|
const platformPages = uiPatterns.pages.filter(p => p.route.includes(`/${platform}`) || p.name.includes(platform));
|
|
2253
|
+
// 【v6.40.1 修复】优先从 platformContents 中提取页面
|
|
2254
|
+
const platformContent = platformContents[platform] || '';
|
|
2255
|
+
const inferredPages = extractPagesFromPlatformContent(platformContent, platform);
|
|
2071
2256
|
if (platformPages.length > 0) {
|
|
2072
2257
|
md += `| 页面 | 路由 | 描述 |\n| :--- | :--- | :--- |\n`;
|
|
2073
2258
|
platformPages.forEach(p => { md += `| ${p.name} | \`${p.route}\` | ${p.desc} |\n`; });
|
|
2074
2259
|
}
|
|
2260
|
+
else if (inferredPages.length > 0) {
|
|
2261
|
+
// 【新增】从端专属内容中提取的页面
|
|
2262
|
+
md += `> 💡 **AI 智能提取**(基于需求文档中的「${platform} 端需求」章节)\n\n`;
|
|
2263
|
+
md += `| 页面 | 路由 | 描述 |\n| :--- | :--- | :--- |\n`;
|
|
2264
|
+
inferredPages.forEach(p => { md += `| ${p.name} | \`${p.route}\` | ${p.desc} |\n`; });
|
|
2265
|
+
}
|
|
2075
2266
|
else {
|
|
2076
2267
|
// 【增强】添加智能填充提示
|
|
2077
2268
|
md += `_待补充:从需求中提取 ${platform} 端的页面清单。_\n`;
|
|
@@ -2107,8 +2298,9 @@ function buildTechSpecForPlatform(iter, now, apis, models, archImpact, platform,
|
|
|
2107
2298
|
/**
|
|
2108
2299
|
* 生成指定端的测试计划(该端专属内容)
|
|
2109
2300
|
*/
|
|
2110
|
-
function buildTestSpecForPlatform(iter, now, features, apis, platform) {
|
|
2301
|
+
function buildTestSpecForPlatform(iter, now, features, apis, platform, platformContents = {}) {
|
|
2111
2302
|
let md = `# ${platform} 端测试计划\n\n> 迭代: ${iter} | 端: ${platform} | 生成: ${now}\n\n`;
|
|
2303
|
+
const platformContent = platformContents[platform] || '';
|
|
2112
2304
|
if (isBackendPlatform(platform)) {
|
|
2113
2305
|
md += `## 1. 接口测试\n\n`;
|
|
2114
2306
|
const backendApis = apis.filter(a => !a.path.startsWith('/h5') && !a.path.startsWith('/admin'));
|
|
@@ -2124,8 +2316,18 @@ function buildTestSpecForPlatform(iter, now, features, apis, platform) {
|
|
|
2124
2316
|
md += `- QPS 目标:待补充\n- 响应时间 P99:待补充\n- 并发用户数:待补充\n`;
|
|
2125
2317
|
}
|
|
2126
2318
|
else {
|
|
2319
|
+
// 【v6.40.1】从端专属内容中提取测试场景
|
|
2320
|
+
const platformFeatures = extractFeaturesFromPlatformContent(platformContent, platform);
|
|
2127
2321
|
md += `## 1. 页面流转测试\n\n`;
|
|
2128
|
-
|
|
2322
|
+
if (platformFeatures.length > 0) {
|
|
2323
|
+
md += `> 💡 **AI 智能提取**(基于需求文档中的端专属章节)\n\n`;
|
|
2324
|
+
for (const pf of platformFeatures) {
|
|
2325
|
+
md += `- [ ] **${pf.name}**:${pf.desc}\n`;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
md += `_待 AI 分析 ${platform} 端的页面跳转流程、入口校验、权限拦截。_\n`;
|
|
2330
|
+
}
|
|
2129
2331
|
md += `\n## 2. 交互测试\n\n`;
|
|
2130
2332
|
if (platform === 'h5' || platform === 'miniapp') {
|
|
2131
2333
|
md += `- 触摸手势识别\n- 下拉刷新/上拉加载\n- 键盘弹出适配\n`;
|
|
@@ -2141,14 +2343,22 @@ function buildTestSpecForPlatform(iter, now, features, apis, platform) {
|
|
|
2141
2343
|
/**
|
|
2142
2344
|
* 生成指定端的 UI 规格(仅前端)
|
|
2143
2345
|
*/
|
|
2144
|
-
function buildUISpecForPlatform(iter, now, uiPatterns, platform) {
|
|
2346
|
+
function buildUISpecForPlatform(iter, now, uiPatterns, platform, platformContents = {}) {
|
|
2145
2347
|
let md = `# ${platform} 端 UI 规格\n\n> 迭代: ${iter} | 端: ${platform} | 生成: ${now}\n\n`;
|
|
2348
|
+
const platformContent = platformContents[platform] || '';
|
|
2349
|
+
// 【v6.40.1】从端专属内容中提取页面和组件
|
|
2350
|
+
const inferredPages = extractPagesFromPlatformContent(platformContent, platform);
|
|
2146
2351
|
md += `## 1. 路由表\n\n`;
|
|
2147
2352
|
const platformPages = uiPatterns.pages.filter(p => p.route.includes(`/${platform}`) || p.name.includes(platform));
|
|
2148
2353
|
if (platformPages.length > 0) {
|
|
2149
2354
|
md += `| 页面 | 路由 | 入口 | 权限 |\n| :--- | :--- | :--- | :--- |\n`;
|
|
2150
2355
|
platformPages.forEach(p => { md += `| ${p.name} | \`${p.route}\` | 待补充 | 待补充 |\n`; });
|
|
2151
2356
|
}
|
|
2357
|
+
else if (inferredPages.length > 0) {
|
|
2358
|
+
md += `> 💡 **AI 智能提取**(基于需求文档中的端专属章节)\n\n`;
|
|
2359
|
+
md += `| 页面 | 路由 | 描述 |\n| :--- | :--- | :--- |\n`;
|
|
2360
|
+
inferredPages.forEach(p => { md += `| ${p.name} | \`${p.route}\` | ${p.desc} |\n`; });
|
|
2361
|
+
}
|
|
2152
2362
|
else {
|
|
2153
2363
|
md += `_待补充:从需求中提取 ${platform} 端的路由配置。_\n`;
|
|
2154
2364
|
}
|
|
@@ -2159,7 +2369,16 @@ function buildUISpecForPlatform(iter, now, uiPatterns, platform) {
|
|
|
2159
2369
|
platformComponents.forEach(c => { md += `| ${c.name} | ${c.type} | 高/中/低 |\n`; });
|
|
2160
2370
|
}
|
|
2161
2371
|
else {
|
|
2162
|
-
|
|
2372
|
+
// 【v6.40.1】从端内容中提取页面要素作为组件
|
|
2373
|
+
const componentList = extractComponentsFromPlatformContent(platformContent);
|
|
2374
|
+
if (componentList.length > 0) {
|
|
2375
|
+
md += `> 💡 **AI 智能提取**\n\n`;
|
|
2376
|
+
md += `| 组件 | 类型 | 所属页面 |\n| :--- | :--- | :--- |\n`;
|
|
2377
|
+
componentList.forEach((c) => { md += `| ${c.name} | ${c.type} | ${c.page} |\n`; });
|
|
2378
|
+
}
|
|
2379
|
+
else {
|
|
2380
|
+
md += `_待补充:从需求中提取 ${platform} 端的组件清单。_\n`;
|
|
2381
|
+
}
|
|
2163
2382
|
}
|
|
2164
2383
|
md += `\n## 3. 字段→UI 映射\n\n`;
|
|
2165
2384
|
const platformFields = uiPatterns.formFields.filter(f => f.page.includes(platform));
|
|
@@ -2178,7 +2397,16 @@ function buildUISpecForPlatform(iter, now, uiPatterns, platform) {
|
|
|
2178
2397
|
});
|
|
2179
2398
|
}
|
|
2180
2399
|
else {
|
|
2181
|
-
|
|
2400
|
+
// 【v6.40.1】从端内容中提取状态枚举
|
|
2401
|
+
const statusEnums = extractStatusEnumsFromContent(platformContent);
|
|
2402
|
+
if (statusEnums.length > 0) {
|
|
2403
|
+
md += `> 💡 **AI 智能提取**\n\n`;
|
|
2404
|
+
md += `| 字段 | 值 | 含义 |\n| :--- | :--- | :--- |\n`;
|
|
2405
|
+
statusEnums.forEach((s) => { md += `| ${s.field} | ${s.values.join(' / ')} | ${s.labels.join(' / ')} |\n`; });
|
|
2406
|
+
}
|
|
2407
|
+
else {
|
|
2408
|
+
md += `_待补充:前后端共享的状态值定义。_\n`;
|
|
2409
|
+
}
|
|
2182
2410
|
}
|
|
2183
2411
|
return md;
|
|
2184
2412
|
}
|
|
@@ -2471,12 +2699,19 @@ function guessModule(apiPath, features) {
|
|
|
2471
2699
|
function splitContentByPlatform(fullContent, platforms) {
|
|
2472
2700
|
const result = {};
|
|
2473
2701
|
const lines = fullContent.split('\n');
|
|
2474
|
-
// 1.
|
|
2475
|
-
const platformPatterns = platforms.map(p =>
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2702
|
+
// 1. 识别端标题的正则模式(支持多种写法 + 语义映射别名)
|
|
2703
|
+
const platformPatterns = platforms.map(p => {
|
|
2704
|
+
// 收集该端的所有别名(从 PLATFORM_ALIAS_MAP)
|
|
2705
|
+
const aliases = PLATFORM_ALIAS_MAP[p] || [p];
|
|
2706
|
+
const allNames = [p, p.toUpperCase(), p.charAt(0).toUpperCase() + p.slice(1), ...aliases];
|
|
2707
|
+
// 去重并转义正则特殊字符
|
|
2708
|
+
const uniqueNames = [...new Set(allNames)].map(n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
2709
|
+
const namesPattern = uniqueNames.join('|');
|
|
2710
|
+
return {
|
|
2711
|
+
platform: p,
|
|
2712
|
+
regex: new RegExp(`^#{1,4}\\s*.*?(?:${namesPattern}).*?(?:端|需求|$|管理|后台)`, 'i')
|
|
2713
|
+
};
|
|
2714
|
+
});
|
|
2480
2715
|
// 2. 扫描文档,找到每个端的起始位置
|
|
2481
2716
|
const platformStartLines = {};
|
|
2482
2717
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2532,6 +2767,12 @@ const PLATFORM_ALIAS_MAP = {
|
|
|
2532
2767
|
* @returns 推断出的端名,或 null(无法推断)
|
|
2533
2768
|
*/
|
|
2534
2769
|
function inferPlatformFromPathOrContent(filePath, content, platforms) {
|
|
2770
|
+
// 0. 【v6.40.1 修复】跨端通用文档不应归到单一端
|
|
2771
|
+
const baseName = filePath.split(/[/\\]/).pop()?.toUpperCase() || '';
|
|
2772
|
+
const globalDocNames = ['REQUIREMENT', 'REQUIREMENTS', 'INDEX', 'PRD', 'README', 'OVERVIEW'];
|
|
2773
|
+
if (globalDocNames.some(name => baseName.includes(name))) {
|
|
2774
|
+
return null; // 跨端通用文档,由 splitContentByPlatform 按端分割
|
|
2775
|
+
}
|
|
2535
2776
|
// 1. 从文件路径推断(优先级最高)
|
|
2536
2777
|
const pathLower = filePath.toLowerCase();
|
|
2537
2778
|
// 检查路径中是否包含端名目录,如: 010-requirements/app/REQUIREMENT.md
|
|
@@ -2557,17 +2798,28 @@ function inferPlatformFromPathOrContent(filePath, content, platforms) {
|
|
|
2557
2798
|
return platform;
|
|
2558
2799
|
}
|
|
2559
2800
|
}
|
|
2560
|
-
//
|
|
2801
|
+
// 2. 语义映射匹配:硬编码 PLATFORM_ALIAS_MAP + CONSTITUTION.md 动态别名
|
|
2561
2802
|
const firstLines = content.split('\n').slice(0, 50).join('\n');
|
|
2562
|
-
|
|
2563
|
-
|
|
2803
|
+
// 合并硬编码映射和 CONSTITUTION.md 动态提取的别名
|
|
2804
|
+
const mergedAliases = { ...PLATFORM_ALIAS_MAP };
|
|
2805
|
+
for (const [platform, aliases] of Object.entries(_dynamicPlatformAliases)) {
|
|
2806
|
+
if (!mergedAliases[platform]) {
|
|
2807
|
+
mergedAliases[platform] = [];
|
|
2808
|
+
}
|
|
2809
|
+
for (const a of aliases) {
|
|
2810
|
+
if (!mergedAliases[platform].includes(a)) {
|
|
2811
|
+
mergedAliases[platform].push(a);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
for (const [standardPlatform, aliases] of Object.entries(mergedAliases)) {
|
|
2564
2816
|
if (!platforms.includes(standardPlatform))
|
|
2565
2817
|
continue;
|
|
2566
|
-
// 检查是否有别名出现在内容中
|
|
2567
2818
|
for (const alias of aliases) {
|
|
2568
2819
|
const aliasPattern = new RegExp(alias, 'i');
|
|
2569
2820
|
if (aliasPattern.test(firstLines)) {
|
|
2570
|
-
|
|
2821
|
+
const source = PLATFORM_ALIAS_MAP[standardPlatform]?.includes(alias) ? '静态映射' : 'CONSTITUTION动态';
|
|
2822
|
+
logger_1.logger.info(` 🔄 语义映射(${source}): "${alias}" → "${standardPlatform}"`);
|
|
2571
2823
|
return standardPlatform;
|
|
2572
2824
|
}
|
|
2573
2825
|
}
|
|
@@ -2586,4 +2838,189 @@ function inferPlatformFromPathOrContent(filePath, content, platforms) {
|
|
|
2586
2838
|
}
|
|
2587
2839
|
return null; // 无法推断
|
|
2588
2840
|
}
|
|
2841
|
+
// ============================================================
|
|
2842
|
+
// 【新增】从端专属内容中提取页面信息
|
|
2843
|
+
// ============================================================
|
|
2844
|
+
/**
|
|
2845
|
+
* 从端专属需求内容中提取页面清单
|
|
2846
|
+
* @param platformContent 该端的专属需求内容
|
|
2847
|
+
* @param platform 端名
|
|
2848
|
+
* @returns 页面列表 { name, route, desc }
|
|
2849
|
+
*/
|
|
2850
|
+
function extractPagesFromPlatformContent(platformContent, platform) {
|
|
2851
|
+
const pages = [];
|
|
2852
|
+
if (!platformContent)
|
|
2853
|
+
return pages;
|
|
2854
|
+
// 1. 按 Markdown 标题分割内容(### 或 ####)
|
|
2855
|
+
const sections = platformContent.split(/^#{3,4}\s+/m);
|
|
2856
|
+
// 2. 识别功能模块标题(支持 F-01、P1、### 标题等多种格式)
|
|
2857
|
+
const featurePattern = /^(?:F-\d+|P\d+)\s*[||\s]\s*(.+)$/m;
|
|
2858
|
+
for (const section of sections) {
|
|
2859
|
+
const match = section.match(featurePattern);
|
|
2860
|
+
if (match) {
|
|
2861
|
+
const featureName = match[1].trim();
|
|
2862
|
+
// 提取该功能模块的第一段描述
|
|
2863
|
+
const descMatch = section.match(new RegExp("\\*\\*用户场景\\*\\*[::]?\\s*([\\s\\S]+?)(?:\\n\\*\\*|\\n\\n|\\n#)"));
|
|
2864
|
+
const desc = descMatch ? descMatch[1].trim().substring(0, 100) : featureName;
|
|
2865
|
+
// 生成页面信息
|
|
2866
|
+
pages.push({
|
|
2867
|
+
name: featureName,
|
|
2868
|
+
route: `/${platform}/${slugify(featureName)}`,
|
|
2869
|
+
desc: desc
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
// 3. 如果按标题分割没找到,尝试从表格中提取页面清单
|
|
2874
|
+
if (pages.length === 0) {
|
|
2875
|
+
// 匹配表格行: | P1 | 数据看板 | ... | ... | 功能概要 |
|
|
2876
|
+
const tableRowPattern = /^\|\s*(?:P\d+|F-\d+)\s*\|\s*([^|]+)\|/gm;
|
|
2877
|
+
let tableMatch;
|
|
2878
|
+
while ((tableMatch = tableRowPattern.exec(platformContent)) !== null) {
|
|
2879
|
+
const pageName = tableMatch[1].trim();
|
|
2880
|
+
if (pageName.length > 1 && pageName.length < 30 && !/^(序号|页面|编号|名称)/.test(pageName)) {
|
|
2881
|
+
// 尝试从同行中提取功能概要
|
|
2882
|
+
const fullRow = tableMatch[0];
|
|
2883
|
+
const cells = fullRow.split('|').map((c) => c.trim()).filter(Boolean);
|
|
2884
|
+
const desc = cells[cells.length - 1] || pageName;
|
|
2885
|
+
pages.push({
|
|
2886
|
+
name: pageName,
|
|
2887
|
+
route: `/${platform}/${slugify(pageName)}`,
|
|
2888
|
+
desc: desc.substring(0, 100)
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return pages;
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* 将文本转换为 URL 友好的 slug
|
|
2897
|
+
*/
|
|
2898
|
+
function slugify(text) {
|
|
2899
|
+
return text
|
|
2900
|
+
.toLowerCase()
|
|
2901
|
+
.replace(/[^\w\s-]/g, '')
|
|
2902
|
+
.replace(/[\s_]+/g, '-')
|
|
2903
|
+
.replace(/^-+|-+$/g, '')
|
|
2904
|
+
.substring(0, 50);
|
|
2905
|
+
}
|
|
2906
|
+
// ============================================================
|
|
2907
|
+
// 【v6.40.1】从端专属内容中提取功能测试场景
|
|
2908
|
+
// ============================================================
|
|
2909
|
+
function extractFeaturesFromPlatformContent(platformContent, _platform) {
|
|
2910
|
+
const features = [];
|
|
2911
|
+
if (!platformContent)
|
|
2912
|
+
return features;
|
|
2913
|
+
const sections = platformContent.split(/^#{3,4}\s+/m);
|
|
2914
|
+
const featurePattern = /^(?:F-\d+|P\d+)\s*[||\s]\s*(.+)$/m;
|
|
2915
|
+
for (const section of sections) {
|
|
2916
|
+
const match = section.match(featurePattern);
|
|
2917
|
+
if (match) {
|
|
2918
|
+
const name = match[1].trim();
|
|
2919
|
+
// 提取业务规则或用户场景作为描述
|
|
2920
|
+
const ruleMatch = section.match(new RegExp('\\*\\*业务规则\\*\\*[::]?\\s*([\\s\\S]+?)(?:\\n\\*\\*|\\n\\n|\\n#)'));
|
|
2921
|
+
const sceneMatch = section.match(new RegExp('\\*\\*用户场景\\*\\*[::]?\\s*([\\s\\S]+?)(?:\\n\\*\\*|\\n\\n|\\n#)'));
|
|
2922
|
+
const desc = ruleMatch
|
|
2923
|
+
? ruleMatch[1].trim().split('\n')[0].substring(0, 120)
|
|
2924
|
+
: sceneMatch
|
|
2925
|
+
? sceneMatch[1].trim().substring(0, 120)
|
|
2926
|
+
: name;
|
|
2927
|
+
features.push({ name, desc });
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
return features.slice(0, 15);
|
|
2931
|
+
}
|
|
2932
|
+
// ============================================================
|
|
2933
|
+
// 【v6.40.1】从端专属内容中提取组件清单
|
|
2934
|
+
// ============================================================
|
|
2935
|
+
function extractComponentsFromPlatformContent(platformContent) {
|
|
2936
|
+
const components = [];
|
|
2937
|
+
if (!platformContent)
|
|
2938
|
+
return components;
|
|
2939
|
+
const sections = platformContent.split(/^#{3,4}\s+/m);
|
|
2940
|
+
const featurePattern = /^(?:F-\d+|P\d+)\s*[||\s]\s*(.+)$/m;
|
|
2941
|
+
for (const section of sections) {
|
|
2942
|
+
const match = section.match(featurePattern);
|
|
2943
|
+
if (!match)
|
|
2944
|
+
continue;
|
|
2945
|
+
const pageName = match[1].trim();
|
|
2946
|
+
// 从「页面要素」中提取组件
|
|
2947
|
+
const elementsMatch = section.match(new RegExp('\\*\\*页面要素\\*\\*[::]?\\s*([\\s\\S]+?)(?:\\n\\*\\*|\\n\\n|\\n#)'));
|
|
2948
|
+
if (elementsMatch) {
|
|
2949
|
+
const lines = elementsMatch[1].trim().split('\n');
|
|
2950
|
+
for (const line of lines) {
|
|
2951
|
+
const item = line.replace(/^[-*]\s*/, '').trim();
|
|
2952
|
+
if (!item || item.length < 2)
|
|
2953
|
+
continue;
|
|
2954
|
+
// 推断组件类型
|
|
2955
|
+
let type = 'UI 组件';
|
|
2956
|
+
if (/\u5361\u7247|\u5361\u7247\u5217\u8868/.test(item))
|
|
2957
|
+
type = 'Card';
|
|
2958
|
+
else if (/\u8868\u683c|\u5217\u8868/.test(item))
|
|
2959
|
+
type = 'Table';
|
|
2960
|
+
else if (/\u8868\u5355|\u8f93\u5165|\u591a\u9009|\u4e0b\u62c9/.test(item))
|
|
2961
|
+
type = 'Form';
|
|
2962
|
+
else if (/\u56fe\u8868|\u6298\u7ebf|\u67f1\u72b6|\u70ed\u529b/.test(item))
|
|
2963
|
+
type = 'Chart';
|
|
2964
|
+
else if (/\u5f39\u7a97|\u786e\u8ba4|\u5f39\u51fa/.test(item))
|
|
2965
|
+
type = 'Modal';
|
|
2966
|
+
else if (/\u6807\u7b7e|\u72b6\u6001/.test(item))
|
|
2967
|
+
type = 'Tag';
|
|
2968
|
+
else if (/\u641c\u7d22|\u7b5b\u9009|\u5207\u6362/.test(item))
|
|
2969
|
+
type = 'Filter';
|
|
2970
|
+
else if (/\u6309\u94ae|\u63d0\u4ea4/.test(item))
|
|
2971
|
+
type = 'Button';
|
|
2972
|
+
components.push({ name: item.substring(0, 30), type, page: pageName });
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
return components.slice(0, 20);
|
|
2977
|
+
}
|
|
2978
|
+
// ============================================================
|
|
2979
|
+
// 【v6.40.1】从端专属内容中提取状态枚举
|
|
2980
|
+
// ============================================================
|
|
2981
|
+
function extractStatusEnumsFromContent(platformContent) {
|
|
2982
|
+
const enums = [];
|
|
2983
|
+
if (!platformContent)
|
|
2984
|
+
return enums;
|
|
2985
|
+
// 查找状态标签相关的描述
|
|
2986
|
+
const statusPatterns = [
|
|
2987
|
+
/\u72b6\u6001\u6807\u7b7e[:\uff1a]\s*([^\n]+)/,
|
|
2988
|
+
/\u72b6\u6001[:\uff1a]\s*([^\n]+)/,
|
|
2989
|
+
/(?:\u5f85\u5f00\u59cb|\u8fdb\u884c\u4e2d|\u5df2\u7ed3\u675f|\u5df2\u53d6\u6d88|\u672a\u7b7e\u5230)/,
|
|
2990
|
+
];
|
|
2991
|
+
for (const pattern of statusPatterns) {
|
|
2992
|
+
const match = platformContent.match(pattern);
|
|
2993
|
+
if (match) {
|
|
2994
|
+
const text = match[1] || match[0];
|
|
2995
|
+
// 从文本中提取状态值
|
|
2996
|
+
const statuses = text.split(/[,,/\u3001]/).map((s) => s.trim()).filter(Boolean);
|
|
2997
|
+
if (statuses.length >= 2) {
|
|
2998
|
+
enums.push({
|
|
2999
|
+
field: '\u4e1a\u52a1\u72b6\u6001',
|
|
3000
|
+
values: statuses,
|
|
3001
|
+
labels: statuses,
|
|
3002
|
+
});
|
|
3003
|
+
break;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
// 尝试从表格中提取状态枚举
|
|
3008
|
+
const tableMatch = platformContent.match(/\|\s*\u72b6\u6001[^|]*\|([^|]*)\|/g);
|
|
3009
|
+
if (tableMatch) {
|
|
3010
|
+
const allStatuses = new Set();
|
|
3011
|
+
for (const row of tableMatch) {
|
|
3012
|
+
const cells = row.split('|').map((c) => c.trim()).filter(Boolean);
|
|
3013
|
+
for (const cell of cells) {
|
|
3014
|
+
if (cell.length < 10 && !/\u72b6\u6001|\u64cd\u4f5c|\u6743\u9650/.test(cell)) {
|
|
3015
|
+
allStatuses.add(cell);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
if (allStatuses.size >= 2 && enums.length === 0) {
|
|
3020
|
+
const values = Array.from(allStatuses);
|
|
3021
|
+
enums.push({ field: '\u72b6\u6001', values, labels: values });
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return enums;
|
|
3025
|
+
}
|
|
2589
3026
|
//# sourceMappingURL=analyze-engine.js.map
|