speccore 8.3.267 → 8.3.269
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/commands/doc2spec.d.ts.map +1 -1
- package/dist/commands/doc2spec.js +12 -2
- package/dist/commands/doc2spec.js.map +1 -1
- package/dist/commands/iteration/taskify.js +192 -24
- package/dist/commands/iteration/taskify.js.map +1 -1
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +10 -3
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/spec-skeleton.d.ts.map +1 -1
- package/dist/core/spec-skeleton.js +15 -0
- package/dist/core/spec-skeleton.js.map +1 -1
- package/dist/core/task-paths.d.ts.map +1 -1
- package/dist/core/task-paths.js +6 -3
- package/dist/core/task-paths.js.map +1 -1
- package/dist/utils/synonyms.d.ts.map +1 -1
- package/dist/utils/synonyms.js +8 -1
- package/dist/utils/synonyms.js.map +1 -1
- package/package.json +1 -1
|
@@ -1701,27 +1701,49 @@ async function createTaskFromSection(iterationDir, taskId, section, allPlatforms
|
|
|
1701
1701
|
taskPlatforms = [section.platform];
|
|
1702
1702
|
}
|
|
1703
1703
|
else {
|
|
1704
|
-
// v8.3.
|
|
1704
|
+
// v8.3.268: 端覆盖裁剪——_matrix 显式声明优先;为空时从 020-specs 端级文档反推
|
|
1705
|
+
// (仅当端级存在非骨架、≥200 字符的 TECH/TEST/UI_SPEC 才算涉及端);
|
|
1706
|
+
// 仍无结果才回退 Spec 匹配 + 六层推断。修复 _matrix platforms 全空时
|
|
1707
|
+
// 每 Task 平铺全部端、幽灵端子任务无中生有(17 Task × 4 端 ≈ 半数无真实规格)。
|
|
1705
1708
|
taskPlatforms = [];
|
|
1706
1709
|
const sectionFeatureName = section.name.replace(/^\d+(\.\d+)*\s*/, '').trim();
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1710
|
+
const matrixPlatforms = await readMatrixPlatforms(iterationDir, sectionFeatureName);
|
|
1711
|
+
if (matrixPlatforms.length > 0) {
|
|
1712
|
+
taskPlatforms = matrixPlatforms;
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
for (const platform of allPlatforms) {
|
|
1716
|
+
const hasRealSpec = Object.entries(specContents).some(([key, content]) => {
|
|
1717
|
+
const parts = key.split('/');
|
|
1718
|
+
return parts.length >= 3 && parts[0].includes(sectionFeatureName) && parts[1] === platform &&
|
|
1719
|
+
/(TECH|TEST|UI_SPEC)\.md$/.test(parts[2]) &&
|
|
1720
|
+
!content.includes('SPEC-SKELETON') && content.trim().length >= 200;
|
|
1721
|
+
});
|
|
1722
|
+
if (hasRealSpec)
|
|
1723
|
+
taskPlatforms.push(platform);
|
|
1711
1724
|
}
|
|
1712
1725
|
}
|
|
1713
|
-
// v8.3.185+: Spec 匹配失败时,用六层端推断机制(显式声明→_matrix→目录→关键词→AI)
|
|
1714
1726
|
if (taskPlatforms.length === 0) {
|
|
1715
|
-
|
|
1716
|
-
const
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if (
|
|
1724
|
-
|
|
1727
|
+
// v8.3.125+: 使用三层匹配推断平台(替代脆弱的字符串包含匹配)
|
|
1728
|
+
for (const platform of allPlatforms) {
|
|
1729
|
+
const bestMatch = findBestSpecMatch(specContents, sectionFeatureName, platform, 'TECH.md');
|
|
1730
|
+
if (bestMatch && bestMatch.score >= 20) {
|
|
1731
|
+
taskPlatforms.push(platform);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
// v8.3.185+: Spec 匹配失败时,用六层端推断机制(显式声明→_matrix→目录→关键词→AI)
|
|
1735
|
+
if (taskPlatforms.length === 0) {
|
|
1736
|
+
const { inferFeaturePlatforms } = await Promise.resolve().then(() => __importStar(require('../../core/platform-inference')));
|
|
1737
|
+
const featureDir = (0, path_1.join)(iterationDir, '010-requirements', 'features', sectionFeatureName);
|
|
1738
|
+
const inference = await inferFeaturePlatforms({
|
|
1739
|
+
featureName: sectionFeatureName,
|
|
1740
|
+
featureDir,
|
|
1741
|
+
allPlatforms,
|
|
1742
|
+
});
|
|
1743
|
+
taskPlatforms = inference.platforms;
|
|
1744
|
+
if (inference.needsConfirm || inference.source === 'ai-required') {
|
|
1745
|
+
logger_1.logger.warn(` ⚠️ 功能「${sectionFeatureName}」端推断置信度 ${inference.confidence}(${inference.source}),建议人工确认 _matrix.md`);
|
|
1746
|
+
}
|
|
1725
1747
|
}
|
|
1726
1748
|
}
|
|
1727
1749
|
}
|
|
@@ -1838,6 +1860,46 @@ ${taskPlatforms.map((p) => `| ${subtaskIdMap.get(p)} | ${p} | ${owner} | 🔲
|
|
|
1838
1860
|
await (0, fs_extra_1.writeFile)((0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml'), contractYaml);
|
|
1839
1861
|
}
|
|
1840
1862
|
}
|
|
1863
|
+
// v8.3.268: 合并端级契约——全局契约可能是空壳(apis: [] 或无接口),
|
|
1864
|
+
// 真契约在各功能单元的 020-specs/{feature}/{端}/API_CONTRACT.yaml。
|
|
1865
|
+
// 修复 execute 注入 _shared/API_CONTRACT.yaml 时拿到空契约、AI 无中生有发明接口的断链。
|
|
1866
|
+
const featName = section.name.replace(/^\d+(\.\d+)*\s*/, '').trim();
|
|
1867
|
+
const specsRoot = (0, path_1.join)(iterationDir, '020-specs');
|
|
1868
|
+
const platContractParts = [];
|
|
1869
|
+
if (await (0, fs_extra_1.pathExists)(specsRoot)) {
|
|
1870
|
+
const featEntries = await (0, fs_extra_1.readdir)(specsRoot, { withFileTypes: true });
|
|
1871
|
+
for (const fe of featEntries) {
|
|
1872
|
+
if (!fe.isDirectory() || !fe.name.includes(featName))
|
|
1873
|
+
continue;
|
|
1874
|
+
const platEntries = await (0, fs_extra_1.readdir)((0, path_1.join)(specsRoot, fe.name), { withFileTypes: true });
|
|
1875
|
+
for (const pe of platEntries) {
|
|
1876
|
+
if (!pe.isDirectory())
|
|
1877
|
+
continue;
|
|
1878
|
+
const contractPath = (0, path_1.join)(specsRoot, fe.name, pe.name, 'API_CONTRACT.yaml');
|
|
1879
|
+
if (!(await (0, fs_extra_1.pathExists)(contractPath)))
|
|
1880
|
+
continue;
|
|
1881
|
+
const contractContent = await (0, fs_extra_1.readFile)(contractPath, 'utf-8');
|
|
1882
|
+
// 空壳判定:无接口条目(apis: [] / paths 缺 / 仅说明文字)
|
|
1883
|
+
const hasApis = /apis:\s*[\n\r]\s*-\s/m.test(contractContent) || /^paths:\s*$/m.test(contractContent);
|
|
1884
|
+
if (!hasApis)
|
|
1885
|
+
continue;
|
|
1886
|
+
platContractParts.push(`## ${pe.name} 端契约(${fe.name})\n\n${contractContent.trim()}`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (platContractParts.length > 0) {
|
|
1891
|
+
const sharedContractPath = (0, path_1.join)(taskDir, '_shared', 'API_CONTRACT.yaml');
|
|
1892
|
+
let baseContract = await (0, fs_extra_1.pathExists)(sharedContractPath)
|
|
1893
|
+
? await (0, fs_extra_1.readFile)(sharedContractPath, 'utf-8')
|
|
1894
|
+
: '';
|
|
1895
|
+
// 全局索引壳保留,但去掉空 apis 标记,避免误导(真契约已合并于下方)
|
|
1896
|
+
baseContract = baseContract.replace(/\n?apis:\s*\[\]\s*$/, '').trimEnd();
|
|
1897
|
+
const merged = (baseContract
|
|
1898
|
+
? `${baseContract}\n\n---\n\n`
|
|
1899
|
+
: '# 任务级接口契约(合并自端级契约)\n\n') + platContractParts.join('\n\n---\n\n') + '\n';
|
|
1900
|
+
await (0, fs_extra_1.writeFile)(sharedContractPath, merged);
|
|
1901
|
+
logger_1.logger.debug(` 📄 ${taskDir.split('/').pop()}: _shared 契约合并 ${platContractParts.length} 份端级契约`);
|
|
1902
|
+
}
|
|
1841
1903
|
// ── 4. 核心规格写入 00-specs/(REQ/TECH/SCHEMA/CHANGELOG) ──
|
|
1842
1904
|
const acItems = generateAcceptanceCriteria(section);
|
|
1843
1905
|
const aiReqContent = section._reqContent;
|
|
@@ -2982,6 +3044,38 @@ function generateMonitorTemplate(section, material) {
|
|
|
2982
3044
|
// 从 analyze 产出中提取任务相关内容
|
|
2983
3045
|
// ═══════════════════════════════════════════════
|
|
2984
3046
|
/** 加载迭代级 020-specs/ 文档内容(包括 global/ 全局文档 + 根目录回退 + 各端子目录文档) */
|
|
3047
|
+
/**
|
|
3048
|
+
* v8.3.268: 读取功能单元的端覆盖声明(010-requirements/features/{feature}/_matrix.md 的 platforms 字段)。
|
|
3049
|
+
* 修复 _matrix platforms 全空/缺失时 taskify 每 Task 平铺全部端、幽灵端子任务无中生有的问题。
|
|
3050
|
+
* 解析格式:platforms: 后跟随 ` - 端名` 行;` []`(空标记)或不存在时返回空数组,交由反推逻辑处理。
|
|
3051
|
+
*/
|
|
3052
|
+
async function readMatrixPlatforms(iterationDir, featureName) {
|
|
3053
|
+
try {
|
|
3054
|
+
const featuresDir = (0, path_1.join)(iterationDir, '010-requirements', 'features');
|
|
3055
|
+
if (!(await (0, fs_extra_1.pathExists)(featuresDir)))
|
|
3056
|
+
return [];
|
|
3057
|
+
const entries = await (0, fs_extra_1.readdir)(featuresDir, { withFileTypes: true });
|
|
3058
|
+
for (const e of entries) {
|
|
3059
|
+
if (!e.isDirectory() || !e.name.includes(featureName))
|
|
3060
|
+
continue;
|
|
3061
|
+
const matrixPath = (0, path_1.join)(featuresDir, e.name, '_matrix.md');
|
|
3062
|
+
if (!(await (0, fs_extra_1.pathExists)(matrixPath)))
|
|
3063
|
+
continue;
|
|
3064
|
+
const raw = await (0, fs_extra_1.readFile)(matrixPath, 'utf-8');
|
|
3065
|
+
const m = raw.match(/platforms:\s*\n((?:\s*[-\[]?[\s\S]*?))(?=\n(?:dependencies|apis|interfaces|$))/);
|
|
3066
|
+
const block = m ? m[1] : '';
|
|
3067
|
+
if (!block || /^\s*\[\s*\]\s*$/.test(block.trim()))
|
|
3068
|
+
continue;
|
|
3069
|
+
const plats = block.split('\n')
|
|
3070
|
+
.map(l => l.replace(/^\s*-\s*/, '').trim())
|
|
3071
|
+
.filter(p => p && !p.startsWith('#') && !p.includes(':'));
|
|
3072
|
+
if (plats.length > 0)
|
|
3073
|
+
return plats;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
catch { /* 读取失败不阻断,回退端级规格反推逻辑 */ }
|
|
3077
|
+
return [];
|
|
3078
|
+
}
|
|
2985
3079
|
async function loadSpecContents(iterationDir) {
|
|
2986
3080
|
const specs = {};
|
|
2987
3081
|
const specDir = (0, path_1.join)(iterationDir, '020-specs');
|
|
@@ -3078,7 +3172,10 @@ function findBestSpecMatch(specContents, taskName, platform, docType) {
|
|
|
3078
3172
|
continue;
|
|
3079
3173
|
let score = 0;
|
|
3080
3174
|
// 第一层:精确匹配(字符串包含,最高权重)
|
|
3081
|
-
|
|
3175
|
+
// v8.3.269: 归一化对称——taskName 与 key 同样去除分隔符(-/_.)。
|
|
3176
|
+
// 此前 taskName 只去空格、key 去分隔符,含连字符的任务名(如 admin-web-02-会议室管理)
|
|
3177
|
+
// 第一层 includes 永远不命中,端前缀匹配失效,只能靠二三层同义词/标题刷分兜底。
|
|
3178
|
+
const normalizedTask = taskName.toLowerCase().replace(/[\s\/\-_\.]+/g, '');
|
|
3082
3179
|
const normalizedKey = key.toLowerCase().replace(/[\/\-_\.]/g, '');
|
|
3083
3180
|
if (normalizedKey.includes(normalizedTask) || normalizedTask.includes(normalizedKey)) {
|
|
3084
3181
|
score += 100;
|
|
@@ -3110,6 +3207,17 @@ function findBestSpecMatch(specContents, taskName, platform, docType) {
|
|
|
3110
3207
|
return candidates[0];
|
|
3111
3208
|
}
|
|
3112
3209
|
/** 从完整文档中提取与任务名相关的段落 */
|
|
3210
|
+
/** v8.3.269: 展开 analyze 落盘的 Python 数组字面量(['| a | b |', '| c | d |'])为多行表格行。
|
|
3211
|
+
* analyze 阶段 AI 用 Python 生成验收标准表格时偶发未展开数组即写入 Markdown,
|
|
3212
|
+
* taskify 提取时原样搬运会把不可读的单行数组塞进 REQ.md,此处做数据容错还原。 */
|
|
3213
|
+
function expandPythonListLiteral(content) {
|
|
3214
|
+
if (!content.includes("['"))
|
|
3215
|
+
return content;
|
|
3216
|
+
return content.replace(/\['((?=[^']*\|)[^']*(?:'\s*,\s*'[^']*)*)'\]/g, (...args) => {
|
|
3217
|
+
const inner = args[1];
|
|
3218
|
+
return inner.split(/'\s*,\s*'/).join('\n');
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3113
3221
|
function extractRelevantSection(fullContent, taskName, sectionHint) {
|
|
3114
3222
|
if (!fullContent || !taskName)
|
|
3115
3223
|
return '';
|
|
@@ -3163,7 +3271,7 @@ function extractRelevantSection(fullContent, taskName, sectionHint) {
|
|
|
3163
3271
|
}
|
|
3164
3272
|
}
|
|
3165
3273
|
if (matched.length > 0)
|
|
3166
|
-
return matched.join('\n\n');
|
|
3274
|
+
return expandPythonListLiteral(matched.join('\n\n'));
|
|
3167
3275
|
// 回退:提取包含关键词的连续行
|
|
3168
3276
|
const fallback = [];
|
|
3169
3277
|
for (const line of lines) {
|
|
@@ -3171,7 +3279,7 @@ function extractRelevantSection(fullContent, taskName, sectionHint) {
|
|
|
3171
3279
|
if (kwArray.some(kw => lower.includes(kw)))
|
|
3172
3280
|
fallback.push(line);
|
|
3173
3281
|
}
|
|
3174
|
-
return fallback.length > 0 ? fallback.join('\n') : '';
|
|
3282
|
+
return fallback.length > 0 ? expandPythonListLiteral(fallback.join('\n')) : '';
|
|
3175
3283
|
}
|
|
3176
3284
|
/** 从 TECH.md 提取前端平台相关内容 */
|
|
3177
3285
|
function extractFrontendContent(techContent, taskName, platform) {
|
|
@@ -3241,10 +3349,42 @@ function extractTaskDevGuideContent(specContents, section, taskPlatforms) {
|
|
|
3241
3349
|
}
|
|
3242
3350
|
return results.join('\n\n');
|
|
3243
3351
|
}
|
|
3352
|
+
// v8.3.268: 按文档类型从 TECH 提取中分化内容——按 ### 标题切段,
|
|
3353
|
+
// 只保留与文档类型相关的段落。修复「一段多用」:ROUTES/STATE/STYLE_GUIDE/
|
|
3354
|
+
// COMPONENT_TREE 四份文档正文完全相同(仅标题不同)的问题。
|
|
3355
|
+
function extractSectionByKeywords(material, keywords) {
|
|
3356
|
+
const sections = material.split(/^#{2,4}\s+/m);
|
|
3357
|
+
const matched = [];
|
|
3358
|
+
for (const sec of sections) {
|
|
3359
|
+
const headLine = sec.split('\n')[0] || '';
|
|
3360
|
+
if (keywords.some(kw => headLine.includes(kw))) {
|
|
3361
|
+
matched.push(sec.trim());
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
// 无 ### 标题结构的兜底:整段中关键词密度最高的前 N 行
|
|
3365
|
+
if (matched.length === 0 && material.includes('\n|')) {
|
|
3366
|
+
const lines = material.split('\n');
|
|
3367
|
+
const tableStart = lines.findIndex(l => keywords.some(kw => l.includes(kw)));
|
|
3368
|
+
if (tableStart >= 0) {
|
|
3369
|
+
const block = [];
|
|
3370
|
+
for (let i = tableStart; i < lines.length; i++) {
|
|
3371
|
+
if (i > tableStart && /^#{2,4}\s/.test(lines[i]))
|
|
3372
|
+
break;
|
|
3373
|
+
block.push(lines[i]);
|
|
3374
|
+
}
|
|
3375
|
+
return block.join('\n').trim();
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
return matched.join('\n\n');
|
|
3379
|
+
}
|
|
3244
3380
|
// 前端专属:组件树
|
|
3245
3381
|
function generateComponentTree(section, platform, material) {
|
|
3246
3382
|
if (material && material.trim().length > 200) {
|
|
3247
|
-
|
|
3383
|
+
// v8.3.268: 仅提取组件拆分/组件清单相关段落
|
|
3384
|
+
const extracted = extractSectionByKeywords(material, ['组件拆分', '组件清单', '组件树', '共享组件']);
|
|
3385
|
+
if (extracted) {
|
|
3386
|
+
return `# ${section.name} — 组件树 (${platform})\n\n> 来源: analyze → TECH.md(组件拆分段提取)\n\n${extracted}\n`;
|
|
3387
|
+
}
|
|
3248
3388
|
}
|
|
3249
3389
|
return `# ${section.name} — 组件树 (${platform})
|
|
3250
3390
|
|
|
@@ -3267,7 +3407,11 @@ function generateComponentTree(section, platform, material) {
|
|
|
3267
3407
|
// 前端专属:路由
|
|
3268
3408
|
function generateRoutesDoc(section, platform, material) {
|
|
3269
3409
|
if (material && material.trim().length > 200) {
|
|
3270
|
-
|
|
3410
|
+
// v8.3.268: 仅提取页面清单与路由相关段落
|
|
3411
|
+
const extracted = extractSectionByKeywords(material, ['页面清单', '路由', '导航']);
|
|
3412
|
+
if (extracted) {
|
|
3413
|
+
return `# ${section.name} — 路由设计 (${platform})\n\n> 来源: analyze → TECH.md(页面/路由段提取)\n\n${extracted}\n`;
|
|
3414
|
+
}
|
|
3271
3415
|
}
|
|
3272
3416
|
return `# ${section.name} — 路由设计 (${platform})
|
|
3273
3417
|
|
|
@@ -3285,7 +3429,11 @@ function generateRoutesDoc(section, platform, material) {
|
|
|
3285
3429
|
// 前端专属:状态管理
|
|
3286
3430
|
function generateStateDoc(section, platform, material) {
|
|
3287
3431
|
if (material && material.trim().length > 200) {
|
|
3288
|
-
|
|
3432
|
+
// v8.3.268: 仅提取状态管理/Store/数据流相关段落
|
|
3433
|
+
const extracted = extractSectionByKeywords(material, ['状态管理', 'Store', '数据流', '状态枚举']);
|
|
3434
|
+
if (extracted) {
|
|
3435
|
+
return `# ${section.name} — 状态管理 (${platform})\n\n> 来源: analyze → TECH.md(状态管理段提取)\n\n${extracted}\n`;
|
|
3436
|
+
}
|
|
3289
3437
|
}
|
|
3290
3438
|
return `# ${section.name} — 状态管理 (${platform})
|
|
3291
3439
|
|
|
@@ -3308,7 +3456,11 @@ function generateStateDoc(section, platform, material) {
|
|
|
3308
3456
|
// 前端专属:样式规范
|
|
3309
3457
|
function generateStyleGuide(section, platform, material) {
|
|
3310
3458
|
if (material && material.trim().length > 200) {
|
|
3311
|
-
|
|
3459
|
+
// v8.3.268: 仅提取样式/主题/设计 Token 相关段落
|
|
3460
|
+
const extracted = extractSectionByKeywords(material, ['样式', '主题', '设计 Token', '视觉', '响应式']);
|
|
3461
|
+
if (extracted) {
|
|
3462
|
+
return `# ${section.name} — 样式规范 (${platform})\n\n> 来源: analyze → TECH.md(样式/主题段提取)\n\n${extracted}\n`;
|
|
3463
|
+
}
|
|
3312
3464
|
}
|
|
3313
3465
|
const isH5 = platform.includes('h5') || platform.includes('mobile');
|
|
3314
3466
|
const isMiniapp = platform.includes('miniapp') || platform.includes('小程序');
|
|
@@ -4143,7 +4295,11 @@ function parseFunctionMapTable(content, allPlatforms) {
|
|
|
4143
4295
|
continue;
|
|
4144
4296
|
if (cells.some(c => c.includes('功能单元')) || cells.some(c => c.includes('涉及端')))
|
|
4145
4297
|
continue;
|
|
4146
|
-
|
|
4298
|
+
// v8.3.268: 功能单元列取 cells[0](表头第一列)。此前取 cells[1] 依赖旧数据
|
|
4299
|
+
// 「| 目录名 | 名称 | 端列表 | …」的列错位(cells[1]=名称),一旦表格对齐表头
|
|
4300
|
+
// (cells[1]=涉及端)任务名就变成端列表。「dir(名称)」格式剥尾部括号注释。
|
|
4301
|
+
const unitNameRaw = cells[0] || cells[1];
|
|
4302
|
+
const unitName = unitNameRaw.replace(/[((][^))]*[))]$/, '').trim();
|
|
4147
4303
|
if (!unitName || unitName === '#' || unitName === '功能单元')
|
|
4148
4304
|
continue;
|
|
4149
4305
|
let platforms = [];
|
|
@@ -4153,6 +4309,18 @@ function parseFunctionMapTable(content, allPlatforms) {
|
|
|
4153
4309
|
platforms = raw.split(/[,,]/).map(p => p.trim()).filter(p => p && allPlatforms.includes(p));
|
|
4154
4310
|
}
|
|
4155
4311
|
}
|
|
4312
|
+
// v8.3.268: 端列自适应——AI 落盘 FUNCTION_MAP 表格可能列错位(涉及端表头下是
|
|
4313
|
+
// 名称、端列表落在其他列),按表头解析落空时扫描整行,找「逗号分隔且 ≥2 个
|
|
4314
|
+
// 值全部属于标准端名」的单元格作为端列,避免静默回退全端平铺。
|
|
4315
|
+
if (platforms.length === 0) {
|
|
4316
|
+
for (const cell of cells) {
|
|
4317
|
+
const parts = cell.split(/[,,]/).map(p => p.trim()).filter(Boolean);
|
|
4318
|
+
if (parts.length >= 2 && parts.every(p => allPlatforms.includes(p))) {
|
|
4319
|
+
platforms = parts;
|
|
4320
|
+
break;
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4156
4324
|
if (platforms.length === 0)
|
|
4157
4325
|
platforms = [...allPlatforms];
|
|
4158
4326
|
let sharedCapability = '无';
|