speccore 8.3.121 → 8.3.127
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 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.d.ts.map +1 -1
- package/dist/commands/about.js +16 -0
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/analyze.d.ts +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +198 -70
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/change.js +14 -7
- package/dist/commands/change.js.map +1 -1
- package/dist/commands/context.js +25 -31
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +6 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/done.js +29 -28
- package/dist/commands/done.js.map +1 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +281 -20
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/import.js +1 -1
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +513 -28
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/iteration/create.js +1 -1
- package/dist/commands/iteration/split.d.ts.map +1 -1
- package/dist/commands/iteration/split.js +158 -70
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/iteration-from-global.js +1 -2
- package/dist/commands/iteration-from-global.js.map +1 -1
- package/dist/commands/pattern.js +30 -16
- package/dist/commands/pattern.js.map +1 -1
- package/dist/commands/pipeline.d.ts.map +1 -1
- package/dist/commands/pipeline.js +15 -4
- package/dist/commands/pipeline.js.map +1 -1
- package/dist/commands/retro.d.ts.map +1 -1
- package/dist/commands/retro.js +15 -8
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/status-panel.js +1 -1
- package/dist/commands/status-panel.js.map +1 -1
- package/dist/commands/sync.js +15 -8
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/trace.js +27 -24
- package/dist/commands/trace.js.map +1 -1
- package/dist/commands/update-env-configs.d.ts.map +1 -1
- package/dist/commands/update-env-configs.js +33 -5
- package/dist/commands/update-env-configs.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +35 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/verify.d.ts +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +25 -0
- package/dist/commands/verify.js.map +1 -1
- package/dist/core/ai-context-generator.js +35 -22
- package/dist/core/ai-context-generator.js.map +1 -1
- package/dist/core/ai-impact-analyzer.d.ts.map +1 -1
- package/dist/core/ai-impact-analyzer.js +1 -1
- package/dist/core/ai-impact-analyzer.js.map +1 -1
- package/dist/core/analyze-engine.d.ts +2 -1
- package/dist/core/analyze-engine.d.ts.map +1 -1
- package/dist/core/analyze-engine.js +217 -53
- package/dist/core/analyze-engine.js.map +1 -1
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +31 -13
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +21 -5
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/code-scanner.d.ts +12 -3
- package/dist/core/code-scanner.d.ts.map +1 -1
- package/dist/core/code-scanner.js +376 -10
- package/dist/core/code-scanner.js.map +1 -1
- package/dist/core/context-builder.d.ts +2 -1
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +104 -25
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/deploy/engine.d.ts.map +1 -1
- package/dist/core/deploy/engine.js +21 -6
- package/dist/core/deploy/engine.js.map +1 -1
- package/dist/core/git-integration.d.ts +5 -1
- package/dist/core/git-integration.d.ts.map +1 -1
- package/dist/core/git-integration.js +52 -15
- package/dist/core/git-integration.js.map +1 -1
- package/dist/core/knowledge-graph.d.ts +2 -0
- package/dist/core/knowledge-graph.d.ts.map +1 -1
- package/dist/core/knowledge-graph.js +246 -232
- package/dist/core/knowledge-graph.js.map +1 -1
- package/dist/core/name-validator.js +1 -1
- package/dist/core/name-validator.js.map +1 -1
- package/dist/core/pipeline-test.d.ts.map +1 -1
- package/dist/core/pipeline-test.js +7 -4
- package/dist/core/pipeline-test.js.map +1 -1
- package/dist/core/prompt-builder.d.ts +3 -0
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +629 -77
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/rag-engine.d.ts +10 -5
- package/dist/core/rag-engine.d.ts.map +1 -1
- package/dist/core/rag-engine.js +134 -16
- package/dist/core/rag-engine.js.map +1 -1
- package/dist/core/spec-paths.d.ts +42 -2
- package/dist/core/spec-paths.d.ts.map +1 -1
- package/dist/core/spec-paths.js +207 -11
- package/dist/core/spec-paths.js.map +1 -1
- package/dist/core/structured-extractor.d.ts +67 -0
- package/dist/core/structured-extractor.d.ts.map +1 -1
- package/dist/core/structured-extractor.js +1298 -23
- package/dist/core/structured-extractor.js.map +1 -1
- package/dist/core/test-config.d.ts +1 -1
- package/dist/core/test-config.d.ts.map +1 -1
- package/dist/core/test-config.js +85 -65
- package/dist/core/test-config.js.map +1 -1
- package/dist/core/ui-verify/report-generator.d.ts.map +1 -1
- package/dist/core/ui-verify/report-generator.js +7 -3
- package/dist/core/ui-verify/report-generator.js.map +1 -1
- package/dist/core/ui-verify/smoke-engine.js +7 -1
- package/dist/core/ui-verify/smoke-engine.js.map +1 -1
- package/dist/core/unified-config.d.ts +43 -3
- package/dist/core/unified-config.d.ts.map +1 -1
- package/dist/core/unified-config.js +133 -13
- package/dist/core/unified-config.js.map +1 -1
- package/dist/core/unified-retrieval.d.ts.map +1 -1
- package/dist/core/unified-retrieval.js +41 -15
- package/dist/core/unified-retrieval.js.map +1 -1
- package/dist/core/unit-context-assembler.js +2 -2
- package/dist/core/unit-context-assembler.js.map +1 -1
- package/dist/core/validator.d.ts.map +1 -1
- package/dist/core/validator.js +59 -44
- package/dist/core/validator.js.map +1 -1
- package/dist/core/verify-engine.d.ts +9 -2
- package/dist/core/verify-engine.d.ts.map +1 -1
- package/dist/core/verify-engine.js +274 -108
- package/dist/core/verify-engine.js.map +1 -1
- package/dist/core/verify-report-aggregator.js +9 -20
- package/dist/core/verify-report-aggregator.js.map +1 -1
- package/dist/utils/synonyms.d.ts +24 -0
- package/dist/utils/synonyms.d.ts.map +1 -0
- package/dist/utils/synonyms.js +114 -0
- package/dist/utils/synonyms.js.map +1 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ exports.loadGlobalContext = loadGlobalContext;
|
|
|
4
4
|
exports.formatGlobalContext = formatGlobalContext;
|
|
5
5
|
exports.buildPrompt = buildPrompt;
|
|
6
6
|
exports.formatPrompt = formatPrompt;
|
|
7
|
+
exports.extractInfoGaps = extractInfoGaps;
|
|
7
8
|
exports.parseAiResponse = parseAiResponse;
|
|
8
9
|
exports.outputNeedsInfo = outputNeedsInfo;
|
|
9
10
|
exports.processMarkdownContent = processMarkdownContent;
|
|
@@ -22,8 +23,12 @@ const logger_1 = require("../utils/logger");
|
|
|
22
23
|
const knowledge_graph_1 = require("./knowledge-graph");
|
|
23
24
|
const context_builder_1 = require("./context-builder");
|
|
24
25
|
const spec_paths_1 = require("./spec-paths");
|
|
25
|
-
const
|
|
26
|
+
const context_1 = require("./context");
|
|
26
27
|
const unified_retrieval_1 = require("./unified-retrieval");
|
|
28
|
+
// v8.3.122+: execute 时深入读取关联源码
|
|
29
|
+
const code_scanner_1 = require("./code-scanner");
|
|
30
|
+
// v8.3.125+: 同义词扩展,解决中英文/缩写关键词匹配失败
|
|
31
|
+
const synonyms_1 = require("../utils/synonyms");
|
|
27
32
|
// v8.3.94+: 视觉模型引擎( specs 图片理解)
|
|
28
33
|
const vision_engine_1 = require("./vision-engine");
|
|
29
34
|
// v6.93.0+: Prompt 插件系统
|
|
@@ -246,6 +251,8 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
|
|
|
246
251
|
{ name: '已知问题', path: '.issues.md' },
|
|
247
252
|
];
|
|
248
253
|
// 加载迭代级设计文档(020-specs/)—— 填补迭代层上下文断裂
|
|
254
|
+
// v8.3.126+: feature/platform 端级规格改为阅读清单模式(省 Token + AI 按需 Read)
|
|
255
|
+
const readingListItems = [];
|
|
249
256
|
if (iteration) {
|
|
250
257
|
const iterDir = (0, path_1.join)(cwd, `Iteration-${iteration}`);
|
|
251
258
|
files.push({ name: '迭代设计文档', path: (0, path_1.join)(iterDir, '020-specs', 'DESIGN.md') });
|
|
@@ -255,7 +262,7 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
|
|
|
255
262
|
files.push({ name: `迭代overview/${f}`, path: (0, path_1.join)(overviewDir, f) });
|
|
256
263
|
}
|
|
257
264
|
if (platform) {
|
|
258
|
-
// v8.3.
|
|
265
|
+
// v8.3.126+: 扫描 020-specs/{feature}/{platform}/ 下的 .md 文件,改为阅读清单
|
|
259
266
|
const specsDir = (0, path_1.join)(iterDir, '020-specs');
|
|
260
267
|
try {
|
|
261
268
|
const features = await (0, spec_paths_1.parseFeatureList)(iterDir);
|
|
@@ -265,11 +272,11 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
|
|
|
265
272
|
const entries = await (0, fs_extra_1.readdir)(featurePlatDir, { withFileTypes: true });
|
|
266
273
|
for (const entry of entries) {
|
|
267
274
|
const isMd = entry.name.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(entry.name);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
files.push({
|
|
271
|
-
name: `${feature}-${platform}端规格`,
|
|
275
|
+
if (isMd) {
|
|
276
|
+
readingListItems.push({
|
|
272
277
|
path: (0, path_1.join)(featurePlatDir, entry.name),
|
|
278
|
+
feature,
|
|
279
|
+
name: entry.name,
|
|
273
280
|
});
|
|
274
281
|
}
|
|
275
282
|
}
|
|
@@ -300,32 +307,17 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
|
|
|
300
307
|
files.unshift({ name: `${platform}端组件树`, path: (0, path_1.join)(platform, firstSub, 'COMPONENT_TREE.md') }, { name: `${platform}端路由`, path: (0, path_1.join)(platform, firstSub, 'ROUTES.md') }, { name: `${platform}端状态管理`, path: (0, path_1.join)(platform, firstSub, 'STATE.md') });
|
|
301
308
|
}
|
|
302
309
|
}
|
|
303
|
-
//
|
|
304
|
-
const isBk = platform === 'backend' || platform.startsWith('后台');
|
|
305
|
-
const categoryDir = isBk ? '10-backend' : '20-frontend';
|
|
306
|
-
const serviceName = isBk && platform === 'backend' ? 'api' : platform;
|
|
307
|
-
const legacyBase = (0, path_1.join)(cwd, taskDir, categoryDir, serviceName);
|
|
308
|
-
if (subtaskDirsList.length === 0) {
|
|
309
|
-
try {
|
|
310
|
-
if (await (0, fs_extra_1.pathExists)(legacyBase)) {
|
|
311
|
-
const entries = await (0, fs_extra_1.readdir)(legacyBase, { withFileTypes: true });
|
|
312
|
-
const legacySubs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name);
|
|
313
|
-
if (legacySubs.length > 0) {
|
|
314
|
-
files.unshift({ name: `${platform}端子任务(旧)`, path: (0, path_1.join)(categoryDir, serviceName, legacySubs[0], 'TASK.md') });
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
catch { /* ignore */ }
|
|
319
|
-
}
|
|
310
|
+
// v8.3.121+: 已移除 10-backend/20-frontend 旧结构回退,端平铺结构 {platform}/{subtask}/ 为标准
|
|
320
311
|
}
|
|
321
312
|
for (const f of files) {
|
|
322
313
|
const fullPath = (0, path_1.join)(cwd, taskDir, f.path);
|
|
323
314
|
seenPaths.add(fullPath);
|
|
324
315
|
if (await (0, fs_extra_1.pathExists)(fullPath)) {
|
|
325
316
|
let rawContent = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
|
|
326
|
-
// v8.3.
|
|
317
|
+
// v8.3.122+: HTML 原型文件保留完整内容(CSS/JS/结构),不再提取纯文本
|
|
318
|
+
// 原型包含视觉效果和交互逻辑,AI 需要完整解析
|
|
327
319
|
const isHtml = fullPath.endsWith('.html') || fullPath.endsWith('.htm');
|
|
328
|
-
let content =
|
|
320
|
+
let content = rawContent;
|
|
329
321
|
// v8.3.93+: Markdown 链接自动展开 + 图片提取
|
|
330
322
|
if (fullPath.endsWith('.md')) {
|
|
331
323
|
content = await processMarkdownContent(content, fullPath, seenPaths, visionConfig, {
|
|
@@ -363,9 +355,9 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
|
|
|
363
355
|
seenPaths.add(fullPath);
|
|
364
356
|
try {
|
|
365
357
|
let rawContent = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
|
|
366
|
-
// v8.3.
|
|
358
|
+
// v8.3.122+: HTML 原型文件保留完整内容(CSS/JS/结构),不再提取纯文本
|
|
367
359
|
const isHtml = fullPath.endsWith('.html') || fullPath.endsWith('.htm');
|
|
368
|
-
let content =
|
|
360
|
+
let content = rawContent;
|
|
369
361
|
// v8.3.93+: Markdown 链接自动展开 + 图片提取
|
|
370
362
|
if (fullPath.endsWith('.md')) {
|
|
371
363
|
content = await processMarkdownContent(content, fullPath, seenPaths, visionConfig, {
|
|
@@ -391,6 +383,61 @@ async function loadExtraSpecs(cwd, taskDir, platform, iteration, options) {
|
|
|
391
383
|
}
|
|
392
384
|
catch { /* 忽略读取失败的文件 */ }
|
|
393
385
|
}
|
|
386
|
+
// v8.3.126+: 将 020-specs/{feature}/{platform}/ 端级规格转为阅读清单
|
|
387
|
+
if (readingListItems.length > 0) {
|
|
388
|
+
const listLines = [
|
|
389
|
+
'## 📚 迭代层端级规格阅读清单',
|
|
390
|
+
'> 以下文档与当前任务相关,已改为阅读清单模式(节省 Token)。',
|
|
391
|
+
'> 如需深入了解某个功能模块的规格,请按需 Read 对应文件。',
|
|
392
|
+
'',
|
|
393
|
+
];
|
|
394
|
+
for (const item of readingListItems) {
|
|
395
|
+
try {
|
|
396
|
+
const raw = await (0, fs_extra_1.readFile)(item.path, 'utf-8');
|
|
397
|
+
const lines = raw.split(/\r?\n/);
|
|
398
|
+
// 提取第一个 # 标题
|
|
399
|
+
const titleLine = lines.find(l => l.startsWith('#')) || '';
|
|
400
|
+
const title = titleLine.replace(/^#+\s*/, '').trim() || item.name;
|
|
401
|
+
// 提取 ## 标签(前 5 个二级标题)
|
|
402
|
+
const sections = lines
|
|
403
|
+
.filter(l => l.startsWith('## ') && !l.includes('<!--'))
|
|
404
|
+
.slice(0, 5)
|
|
405
|
+
.map(l => l.replace(/^##\s*/, '').trim());
|
|
406
|
+
// 首段摘要(标题后第一个非空段落,最多 120 字)
|
|
407
|
+
let summary = '';
|
|
408
|
+
let foundTitle = false;
|
|
409
|
+
for (const l of lines) {
|
|
410
|
+
if (l.startsWith('#')) {
|
|
411
|
+
foundTitle = true;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (foundTitle && l.trim()) {
|
|
415
|
+
summary = l.trim().slice(0, 120);
|
|
416
|
+
if (l.trim().length > 120)
|
|
417
|
+
summary += '...';
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const relPath = (0, path_1.relative)(cwd, item.path);
|
|
422
|
+
listLines.push(`### ${item.feature}/${item.name}`);
|
|
423
|
+
listLines.push(`- **文件**: \`${relPath}\``);
|
|
424
|
+
listLines.push(`- **标题**: ${title}`);
|
|
425
|
+
if (sections.length > 0)
|
|
426
|
+
listLines.push(`- **章节**: ${sections.join(' | ')}`);
|
|
427
|
+
if (summary)
|
|
428
|
+
listLines.push(`- **摘要**: ${summary}`);
|
|
429
|
+
listLines.push('');
|
|
430
|
+
}
|
|
431
|
+
catch { /* 忽略读取失败 */ }
|
|
432
|
+
}
|
|
433
|
+
listLines.push('---');
|
|
434
|
+
listLines.push('💡 **使用方式**: 如果预加载的任务规格不足以理解实现细节,请打开对应文件查看完整规格。');
|
|
435
|
+
extras.push({
|
|
436
|
+
name: '📚 迭代层端级规格阅读清单',
|
|
437
|
+
path: 'reading-list.md',
|
|
438
|
+
content: listLines.join('\n'),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
394
441
|
return extras;
|
|
395
442
|
}
|
|
396
443
|
/**
|
|
@@ -483,9 +530,9 @@ async function loadAllTaskContext(cwd, taskDir, platform, iteration, graph) {
|
|
|
483
530
|
if (!(await (0, fs_extra_1.pathExists)(fullPath)))
|
|
484
531
|
return;
|
|
485
532
|
let rawContent = await (0, fs_extra_1.readFile)(fullPath, 'utf-8');
|
|
486
|
-
// v8.3.
|
|
533
|
+
// v8.3.122+: HTML 原型文件保留完整内容(CSS/JS/结构),不再提取纯文本
|
|
487
534
|
const isHtml = fullPath.endsWith('.html') || fullPath.endsWith('.htm');
|
|
488
|
-
let content =
|
|
535
|
+
let content = rawContent;
|
|
489
536
|
// v8.3.93+: Markdown 链接自动展开 + 图片提取
|
|
490
537
|
if (fullPath.endsWith('.md')) {
|
|
491
538
|
content = await processMarkdownContent(content, fullPath, seen, visionConfig, {
|
|
@@ -504,7 +551,7 @@ async function loadAllTaskContext(cwd, taskDir, platform, iteration, graph) {
|
|
|
504
551
|
};
|
|
505
552
|
// 1. 递归扫描任务目录所有 .md / .yaml 文件
|
|
506
553
|
// 排除自检/审查/产出阶段文件(这些在代码生成后的 verify 阶段才需要)
|
|
507
|
-
//
|
|
554
|
+
// 排除非代码目录(保留 10-backend/20-frontend 排除项以兼容旧项目数据)
|
|
508
555
|
const CODEGEN_EXCLUDE_DIRS = new Set(['node_modules', '10-backend', '20-frontend', '00-specs', '_shared', '99-artifacts', '.meta']);
|
|
509
556
|
const CODEGEN_EXCLUDE_FILES = new Set(['test.md', 'schema.md', 'review.md', 'changelog.md', 'deploy.md', '.issues.md']);
|
|
510
557
|
const scanTaskDir = async (dir, prefix) => {
|
|
@@ -519,6 +566,9 @@ async function loadAllTaskContext(cwd, taskDir, platform, iteration, graph) {
|
|
|
519
566
|
if (item.isDirectory()) {
|
|
520
567
|
if (CODEGEN_EXCLUDE_DIRS.has(item.name))
|
|
521
568
|
continue;
|
|
569
|
+
// v8.3.125+: 如果有 platform,根目录下只扫描匹配的端 + _shared
|
|
570
|
+
if (platform && prefix === '' && item.name !== '_shared' && item.name !== platform)
|
|
571
|
+
continue;
|
|
522
572
|
await scanTaskDir(fullPath, `${prefix}${item.name}/`);
|
|
523
573
|
}
|
|
524
574
|
else if (/\.(md|yaml|yml|html|htm)$/i.test(item.name)) {
|
|
@@ -997,7 +1047,9 @@ function buildSplitInstruction() {
|
|
|
997
1047
|
' - 哪些功能需要跨端协作?',
|
|
998
1048
|
' - 数据如何在端之间流转?',
|
|
999
1049
|
'',
|
|
1000
|
-
'4. **Read `020-specs/{端名}/TECH.md
|
|
1050
|
+
'4. **Read `020-specs/{功能模块}/{端名}/TECH.md`**(按功能模块×端组合读取)→ 了解各端技术方案',
|
|
1051
|
+
' - 先读 FUNCTION_MAP.md 获取功能模块清单',
|
|
1052
|
+
' - 再按 `020-specs/{功能模块}/{端名}/TECH.md` 读取该功能在各端的技术方案',
|
|
1001
1053
|
' - 各端有哪些接口/页面?',
|
|
1002
1054
|
' - 各端技术栈和架构约束?',
|
|
1003
1055
|
' - **据此确定每个任务涉及哪些端**',
|
|
@@ -1201,6 +1253,25 @@ function getInstruction(command, context) {
|
|
|
1201
1253
|
'2. **有疑问就记录** — 如果对需求理解、技术选型有疑问,按最佳判断实现,并将疑问写入 `.speccore/questions/execute-{任务名}-{日期}-*.md`',
|
|
1202
1254
|
'3. **遇阻断就跳过** — 如果某个功能信息不足无法实现,跳过它并在疑问清单中记录',
|
|
1203
1255
|
'4. **直接输出代码** — 按 JSON 格式输出文件列表,不要输出多余解释',
|
|
1256
|
+
'',
|
|
1257
|
+
'## 🔍 信息充足性自检(v8.3.124+)',
|
|
1258
|
+
'',
|
|
1259
|
+
'生成代码前,必须先评估当前上下文是否充足:',
|
|
1260
|
+
'1. **检查 API 契约**:所有接口的入参、出参、错误码是否都已明确?',
|
|
1261
|
+
'2. **检查数据模型**:Entity 的字段类型、约束、关系是否完整?',
|
|
1262
|
+
'3. **检查依赖服务**:是否需要调用其他 Service/Module,但其接口未提供?',
|
|
1263
|
+
'4. **检查公共模块**:是否需要使用通用工具类、拦截器、中间件,但未提供其签名?',
|
|
1264
|
+
'5. **检查前端映射**:UI 字段→API 字段的映射是否完整?',
|
|
1265
|
+
'',
|
|
1266
|
+
'如果存在信息缺口,在代码文件的注释中标注 `[INFO_GAP: 具体缺什么]`,例如:',
|
|
1267
|
+
'- `// [INFO_GAP: 缺少 UserService.findById 的返回类型定义]`',
|
|
1268
|
+
'- `// [INFO_GAP: 不知道 OrderStatus 枚举的完整取值]`',
|
|
1269
|
+
'- `// [INFO_GAP: 缺少支付回调接口的签名]`',
|
|
1270
|
+
'',
|
|
1271
|
+
'**要求**:',
|
|
1272
|
+
'- 每发现一个缺口就标注一个 `[INFO_GAP: ...]`',
|
|
1273
|
+
'- 不要假设缺失的信息「应该就是这样」,必须标注',
|
|
1274
|
+
'- INFO_GAP 标注不影响代码生成,但为后续补充提供精确目标',
|
|
1204
1275
|
].join('\n');
|
|
1205
1276
|
case 'analyze':
|
|
1206
1277
|
return [
|
|
@@ -1247,7 +1318,7 @@ function getInstruction(command, context) {
|
|
|
1247
1318
|
* 构建完整的 SpecCore Prompt
|
|
1248
1319
|
*/
|
|
1249
1320
|
async function buildPrompt(command, options) {
|
|
1250
|
-
const cwd = options.cwd || process.cwd();
|
|
1321
|
+
const cwd = options.cwd || (0, task_utils_1.findProjectRoot)() || process.cwd();
|
|
1251
1322
|
const techStack = await loadTechStack(cwd);
|
|
1252
1323
|
const taskDir = options.taskDir || '';
|
|
1253
1324
|
// P0-1: 统一读取 REQ.md,避免 loadApiSpecs/loadDataModels/loadBusinessRules 各读一次
|
|
@@ -1257,21 +1328,49 @@ async function buildPrompt(command, options) {
|
|
|
1257
1328
|
const businessRules = await loadBusinessRules(cwd, taskDir, reqContent || undefined);
|
|
1258
1329
|
// 统一检索层:同时查询文档 RAG + 代码切片 + 知识图谱
|
|
1259
1330
|
let extraSpecs = [];
|
|
1260
|
-
|
|
1331
|
+
// v8.3.123+: analyze/split 阶段也需要统一检索,从迭代需求提取查询词
|
|
1332
|
+
let searchQuery = options.task || options.iteration || '';
|
|
1333
|
+
if (reqContent) {
|
|
1334
|
+
const titleMatch = reqContent.match(/^#\s+(.+)$/m);
|
|
1335
|
+
if (titleMatch)
|
|
1336
|
+
searchQuery = titleMatch[1].trim();
|
|
1337
|
+
}
|
|
1338
|
+
else if ((command === 'analyze' || command === 'split') && options.iteration) {
|
|
1261
1339
|
try {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1340
|
+
const iterDir = await (0, context_1.getIterationDir)(options.iteration);
|
|
1341
|
+
if (iterDir) {
|
|
1342
|
+
const reqIndex = (0, path_1.join)(iterDir, '010-requirements', 'INDEX.md');
|
|
1343
|
+
if (await (0, fs_extra_1.pathExists)(reqIndex)) {
|
|
1344
|
+
const idxContent = await (0, fs_extra_1.readFile)(reqIndex, 'utf-8');
|
|
1345
|
+
const titleMatch = idxContent.match(/^#\s+(.+)$/m);
|
|
1346
|
+
if (titleMatch)
|
|
1347
|
+
searchQuery = titleMatch[1].trim();
|
|
1348
|
+
}
|
|
1349
|
+
else {
|
|
1350
|
+
// 尝试读取第一个需求文档
|
|
1351
|
+
const reqDir = (0, path_1.join)(iterDir, '010-requirements');
|
|
1352
|
+
const reqFiles = (await (0, fs_extra_1.readdir)(reqDir).catch(() => []))
|
|
1353
|
+
.filter(f => f.endsWith('.md') && !f.startsWith('.'));
|
|
1354
|
+
if (reqFiles.length > 0) {
|
|
1355
|
+
const firstReq = await (0, fs_extra_1.readFile)((0, path_1.join)(reqDir, reqFiles[0]), 'utf-8');
|
|
1356
|
+
const titleMatch = firstReq.match(/^#\s+(.+)$/m);
|
|
1357
|
+
if (titleMatch)
|
|
1358
|
+
searchQuery = titleMatch[1].trim();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1268
1361
|
}
|
|
1362
|
+
}
|
|
1363
|
+
catch { /* 忽略需求读取失败 */ }
|
|
1364
|
+
}
|
|
1365
|
+
const shouldSearchUnified = !!taskDir || command === 'analyze' || command === 'split';
|
|
1366
|
+
if (shouldSearchUnified && options.iteration) {
|
|
1367
|
+
try {
|
|
1269
1368
|
const unifiedResult = await (0, unified_retrieval_1.unifiedSearch)(cwd, {
|
|
1270
1369
|
query: searchQuery,
|
|
1271
1370
|
iteration: options.iteration,
|
|
1272
1371
|
taskId: options.task,
|
|
1273
1372
|
platform: options.platform,
|
|
1274
|
-
taskDir,
|
|
1373
|
+
taskDir: taskDir || undefined,
|
|
1275
1374
|
});
|
|
1276
1375
|
if (unifiedResult.documentChunks.length > 0 || unifiedResult.codeSlices.length > 0) {
|
|
1277
1376
|
extraSpecs = (0, unified_retrieval_1.assembleUnifiedContext)(unifiedResult, { maxTotalChars: 8000, generous: true });
|
|
@@ -1281,8 +1380,8 @@ async function buildPrompt(command, options) {
|
|
|
1281
1380
|
catch (e) {
|
|
1282
1381
|
logger_1.logger?.debug?.('统一检索失败,回退到传统模式:', e);
|
|
1283
1382
|
}
|
|
1284
|
-
//
|
|
1285
|
-
if (extraSpecs.length === 0) {
|
|
1383
|
+
// 回退:统一检索失败或结果为空时,用传统截断模式(仅 taskDir 存在时)
|
|
1384
|
+
if (extraSpecs.length === 0 && taskDir) {
|
|
1286
1385
|
extraSpecs = await loadExtraSpecs(cwd, taskDir, options.platform, options.iteration, {
|
|
1287
1386
|
maxCharsPerFile: 2000,
|
|
1288
1387
|
maxTotalChars: 8000,
|
|
@@ -1291,25 +1390,383 @@ async function buildPrompt(command, options) {
|
|
|
1291
1390
|
logger_1.logger?.info?.(` 📄 传统模式: ${extraSpecs.length} 个参考文档已加载`);
|
|
1292
1391
|
}
|
|
1293
1392
|
}
|
|
1294
|
-
// 稀疏检测 +
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1393
|
+
// 稀疏检测 + 全量兜底:检索内容不足时,读取所有内容(仅 taskDir 存在时)
|
|
1394
|
+
if (taskDir) {
|
|
1395
|
+
const SPARSE_THRESHOLD = 3000;
|
|
1396
|
+
const currentChars = extraSpecs.reduce((sum, s) => sum + s.content.length, 0);
|
|
1397
|
+
if (currentChars < SPARSE_THRESHOLD) {
|
|
1398
|
+
let fallbackGraph = null;
|
|
1399
|
+
try {
|
|
1400
|
+
fallbackGraph = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
|
|
1401
|
+
}
|
|
1402
|
+
catch { /* 图谱不可用时跳过 */ }
|
|
1403
|
+
const fullContext = await loadAllTaskContext(cwd, taskDir, options.platform, options.iteration, fallbackGraph || undefined);
|
|
1404
|
+
if (fullContext.length > extraSpecs.length) {
|
|
1405
|
+
extraSpecs = fullContext;
|
|
1406
|
+
logger_1.logger?.info?.(` 📚 全量兜底: ${fullContext.length} 个文件 (检索内容不足 ${currentChars} < ${SPARSE_THRESHOLD})`);
|
|
1304
1407
|
}
|
|
1305
1408
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
// v8.3.122+: execute / analyze 时深入读取关联源码(完整文件内容,不只是切片)
|
|
1412
|
+
// v8.3.122++: 同时注入知识图谱任务上下文,帮助 AI 理解代码关联关系
|
|
1413
|
+
if ((command === 'execute' || command === 'analyze') && searchQuery) {
|
|
1414
|
+
try {
|
|
1415
|
+
// v8.3.124+: execute 阶段优先注入结构化事实卡片(structured-data.json)
|
|
1416
|
+
// 这是最高优先级的改进:用结构化卡片替代原始源码,Token 效率提升 10-20 倍
|
|
1417
|
+
const structuredDataPath = (0, path_1.join)(cwd, '.speccore', 'cache', 'structured-data.json');
|
|
1418
|
+
let structuredCardsInjected = false;
|
|
1419
|
+
if (await (0, fs_extra_1.pathExists)(structuredDataPath)) {
|
|
1420
|
+
try {
|
|
1421
|
+
const sdContent = await (0, fs_extra_1.readFile)(structuredDataPath, 'utf-8');
|
|
1422
|
+
const structured = JSON.parse(sdContent);
|
|
1423
|
+
// v8.3.125+: 使用同义词扩展,解决 "登录" 与 "auth" 等跨语言/缩写不匹配
|
|
1424
|
+
const queryWords = [...(0, synonyms_1.expandSynonyms)((0, synonyms_1.extractNormalizedKeywords)(searchQuery))];
|
|
1425
|
+
const matchedApis = [];
|
|
1426
|
+
const matchedEntities = [];
|
|
1427
|
+
const matchedComponents = [];
|
|
1428
|
+
const matchedRoutes = [];
|
|
1429
|
+
const matchedDtos = []; // v8.3.126+
|
|
1430
|
+
const matchedServices = []; // v8.3.126+
|
|
1431
|
+
for (const [platform, data] of Object.entries(structured.endpoints || {})) {
|
|
1432
|
+
// API 匹配
|
|
1433
|
+
for (const api of data.apis || []) {
|
|
1434
|
+
const text = `${api.path || ''} ${api.handler || ''} ${api.description || ''}`.toLowerCase();
|
|
1435
|
+
if (queryWords.some(qw => text.includes(qw)))
|
|
1436
|
+
matchedApis.push({ ...api, platform });
|
|
1437
|
+
}
|
|
1438
|
+
// Entity 匹配
|
|
1439
|
+
for (const entity of data.entities || []) {
|
|
1440
|
+
const text = `${entity.name || ''} ${entity.tableName || ''} ${entity.description || ''}`.toLowerCase();
|
|
1441
|
+
if (queryWords.some(qw => text.includes(qw)))
|
|
1442
|
+
matchedEntities.push({ ...entity, platform });
|
|
1443
|
+
}
|
|
1444
|
+
// Component 匹配
|
|
1445
|
+
for (const comp of data.components || []) {
|
|
1446
|
+
const text = `${comp.name || ''} ${comp.description || ''}`.toLowerCase();
|
|
1447
|
+
if (queryWords.some(qw => text.includes(qw)))
|
|
1448
|
+
matchedComponents.push({ ...comp, platform });
|
|
1449
|
+
}
|
|
1450
|
+
// Route 匹配
|
|
1451
|
+
for (const route of data.routes || []) {
|
|
1452
|
+
const text = `${route.path || ''} ${route.component || ''}`.toLowerCase();
|
|
1453
|
+
if (queryWords.some(qw => text.includes(qw)))
|
|
1454
|
+
matchedRoutes.push({ ...route, platform });
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
// v8.3.126+: 全局 DTO 匹配(跨平台)
|
|
1458
|
+
for (const dto of structured.dtos || []) {
|
|
1459
|
+
const text = `${dto.name || ''} ${dto.fields?.map((f) => f.name).join(' ') || ''}`.toLowerCase();
|
|
1460
|
+
if (queryWords.some(qw => text.includes(qw)))
|
|
1461
|
+
matchedDtos.push(dto);
|
|
1462
|
+
}
|
|
1463
|
+
// v8.3.126+: 全局 Service 匹配(跨平台)
|
|
1464
|
+
for (const svc of structured.services || []) {
|
|
1465
|
+
const text = `${svc.name || ''} ${svc.methods?.map((m) => m.name).join(' ') || ''}`.toLowerCase();
|
|
1466
|
+
if (queryWords.some(qw => text.includes(qw)))
|
|
1467
|
+
matchedServices.push(svc);
|
|
1468
|
+
}
|
|
1469
|
+
const cardLines = ['## 📇 结构化代码事实卡片(自动提取)'];
|
|
1470
|
+
cardLines.push('> 以下信息从代码扫描工具提取的结构化数据生成,信息密度高于原始源码。如需查看具体实现,参考下方源码文件。\n');
|
|
1471
|
+
if (matchedApis.length > 0) {
|
|
1472
|
+
cardLines.push(`### API 接口 (${matchedApis.length} 个匹配)`);
|
|
1473
|
+
for (const api of matchedApis.slice(0, 8)) {
|
|
1474
|
+
cardLines.push(`\n**${api.method || 'GET'} ${api.path || '/'}** — \`${api.handler}\``);
|
|
1475
|
+
cardLines.push(`- 📁 文件: \`${api.filePath}\`:${api.line}`);
|
|
1476
|
+
// v8.3.124+: 优先展示详细参数信息(含类型、装饰器、校验规则)
|
|
1477
|
+
if (api.parameterDetails && api.parameterDetails.length > 0) {
|
|
1478
|
+
const paramLines = api.parameterDetails.map((p) => {
|
|
1479
|
+
let s = ` - \`${p.name}\`${p.type ? `:\`${p.type}\`` : ''}${p.required === false ? ' (可选)' : ''}`;
|
|
1480
|
+
if (p.decorators && p.decorators.length > 0)
|
|
1481
|
+
s += ` 装饰:${p.decorators.map((d) => `\`${d}\``).join(',')}`;
|
|
1482
|
+
if (p.validationRules && p.validationRules.length > 0)
|
|
1483
|
+
s += ` 校验:${p.validationRules.map((v) => `\`${v}\``).join(',')}`;
|
|
1484
|
+
return s;
|
|
1485
|
+
});
|
|
1486
|
+
cardLines.push(`- 📥 参数:`);
|
|
1487
|
+
cardLines.push(...paramLines.slice(0, 6));
|
|
1488
|
+
if (paramLines.length > 6)
|
|
1489
|
+
cardLines.push(` ...等${paramLines.length}个参数`);
|
|
1490
|
+
}
|
|
1491
|
+
else if (api.parameters && api.parameters.length > 0) {
|
|
1492
|
+
cardLines.push(`- 📥 参数: ${api.parameters.join(', ')}`);
|
|
1493
|
+
}
|
|
1494
|
+
if (api.dtoRef)
|
|
1495
|
+
cardLines.push(`- 📦 DTO: \`${api.dtoRef}\``);
|
|
1496
|
+
if (api.responseType)
|
|
1497
|
+
cardLines.push(`- 📤 返回: \`${api.responseType}\``);
|
|
1498
|
+
if (api.authDecorators && api.authDecorators.length > 0) {
|
|
1499
|
+
cardLines.push(`- 🔐 鉴权: ${api.authDecorators.map((d) => `\`${d}\``).join(', ')}`);
|
|
1500
|
+
}
|
|
1501
|
+
if (api.decorators && api.decorators.length > 0)
|
|
1502
|
+
cardLines.push(`- 🏷️ 路由装饰器: ${api.decorators.map((d) => `\`${d}\``).join(', ')}`);
|
|
1503
|
+
// v8.3.125+: Service 调用链
|
|
1504
|
+
if (api.serviceCalls && api.serviceCalls.length > 0) {
|
|
1505
|
+
const callStrs = api.serviceCalls.slice(0, 5).map((c) => `\`${c.service}.${c.method}\``);
|
|
1506
|
+
cardLines.push(`- 🔗 调用链: ${callStrs.join(' → ')}${api.serviceCalls.length > 5 ? ` ...等${api.serviceCalls.length}个` : ''}`);
|
|
1507
|
+
}
|
|
1508
|
+
if (api.description)
|
|
1509
|
+
cardLines.push(`- 📝 说明: ${api.description}`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (matchedEntities.length > 0) {
|
|
1513
|
+
cardLines.push(`\n### 数据实体 (${matchedEntities.length} 个匹配)`);
|
|
1514
|
+
for (const entity of matchedEntities.slice(0, 6)) {
|
|
1515
|
+
cardLines.push(`\n**${entity.name}**${entity.tableName ? ` (表: \`${entity.tableName}\`)` : ''}`);
|
|
1516
|
+
cardLines.push(`- 📁 文件: \`${entity.filePath}\`:${entity.line}`);
|
|
1517
|
+
if (entity.fields && entity.fields.length > 0) {
|
|
1518
|
+
const fieldStrs = entity.fields.slice(0, 8).map((f) => {
|
|
1519
|
+
let s = `\`${f.name}\`: ${f.type}`;
|
|
1520
|
+
if (f.isPrimaryKey)
|
|
1521
|
+
s += ' [PK]';
|
|
1522
|
+
if (f.nullable)
|
|
1523
|
+
s += ' ?';
|
|
1524
|
+
if (f.defaultValue !== undefined)
|
|
1525
|
+
s += ` =${f.defaultValue}`;
|
|
1526
|
+
// v8.3.124+: 展示列配置和校验规则
|
|
1527
|
+
if (f.columnOptions && Object.keys(f.columnOptions).length > 0) {
|
|
1528
|
+
const opts = Object.entries(f.columnOptions).map(([k, v]) => `${k}:${v}`).join(',');
|
|
1529
|
+
s += ` {${opts}}`;
|
|
1530
|
+
}
|
|
1531
|
+
if (f.validationRules && f.validationRules.length > 0)
|
|
1532
|
+
s += ` [${f.validationRules.map((v) => `\`${v}\``).join(',')}]`;
|
|
1533
|
+
return s;
|
|
1534
|
+
});
|
|
1535
|
+
cardLines.push(`- 🏗️ 字段: ${fieldStrs.join(', ')}${entity.fields.length > 8 ? ` ...等${entity.fields.length}个` : ''}`);
|
|
1536
|
+
}
|
|
1537
|
+
if (entity.indexes && entity.indexes.length > 0) {
|
|
1538
|
+
const idxStrs = entity.indexes.map((idx) => {
|
|
1539
|
+
let s = `${idx.fields.join('+')}`;
|
|
1540
|
+
if (idx.unique)
|
|
1541
|
+
s += '(唯一)';
|
|
1542
|
+
if (idx.name)
|
|
1543
|
+
s = `${idx.name}:${s}`;
|
|
1544
|
+
return s;
|
|
1545
|
+
});
|
|
1546
|
+
cardLines.push(`- 📇 索引: ${idxStrs.join('; ')}`);
|
|
1547
|
+
}
|
|
1548
|
+
if (entity.constraints && entity.constraints.length > 0) {
|
|
1549
|
+
cardLines.push(`- ⛓️ 约束: ${entity.constraints.map((c) => `${c.type}(${c.fields.join('+')})`).join('; ')}`);
|
|
1550
|
+
}
|
|
1551
|
+
if (entity.relations && entity.relations.length > 0) {
|
|
1552
|
+
cardLines.push(`- 🔗 关系: ${entity.relations.map((r) => `${r.type} → ${r.target}`).join(', ')}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (matchedComponents.length > 0) {
|
|
1557
|
+
cardLines.push(`\n### 前端组件 (${matchedComponents.length} 个匹配)`);
|
|
1558
|
+
for (const comp of matchedComponents.slice(0, 6)) {
|
|
1559
|
+
cardLines.push(`\n**${comp.name}** — \`${comp.filePath}\`:${comp.line}`);
|
|
1560
|
+
if (comp.props && comp.props.length > 0)
|
|
1561
|
+
cardLines.push(`- Props: ${comp.props.join(', ')}`);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (matchedRoutes.length > 0) {
|
|
1565
|
+
cardLines.push(`\n### 页面路由 (${matchedRoutes.length} 个匹配)`);
|
|
1566
|
+
for (const route of matchedRoutes.slice(0, 6)) {
|
|
1567
|
+
cardLines.push(`- \`${route.path}\` → ${route.component || 'unknown'}${route.lazy ? ' (lazy)' : ''}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// v8.3.126+: DTO 定义展示
|
|
1571
|
+
if (matchedDtos.length > 0) {
|
|
1572
|
+
cardLines.push(`\n### DTO 定义 (${matchedDtos.length} 个匹配)`);
|
|
1573
|
+
for (const dto of matchedDtos.slice(0, 6)) {
|
|
1574
|
+
cardLines.push(`\n**${dto.name}** — \`${dto.filePath}\`:${dto.line}`);
|
|
1575
|
+
if (dto.fields && dto.fields.length > 0) {
|
|
1576
|
+
const fieldStrs = dto.fields.slice(0, 10).map((f) => {
|
|
1577
|
+
let s = `\`${f.name}\`: ${f.type}`;
|
|
1578
|
+
if (f.required === false)
|
|
1579
|
+
s += ' (可选)';
|
|
1580
|
+
if (f.validationRules && f.validationRules.length > 0)
|
|
1581
|
+
s += ` [${f.validationRules.map((v) => `\`${v}\``).join(',')}]`;
|
|
1582
|
+
return s;
|
|
1583
|
+
});
|
|
1584
|
+
cardLines.push(`- 🏷️ 字段: ${fieldStrs.join(', ')}${dto.fields.length > 10 ? ` ...等${dto.fields.length}个` : ''}`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// v8.3.126+: Service 定义展示
|
|
1589
|
+
if (matchedServices.length > 0) {
|
|
1590
|
+
cardLines.push(`\n### Service 定义 (${matchedServices.length} 个匹配)`);
|
|
1591
|
+
for (const svc of matchedServices.slice(0, 6)) {
|
|
1592
|
+
cardLines.push(`\n**${svc.name}** — \`${svc.filePath}\`:${svc.line}`);
|
|
1593
|
+
if (svc.injects && svc.injects.length > 0) {
|
|
1594
|
+
cardLines.push(`- 💉 注入: ${svc.injects.map((i) => `\`${i}\``).join(', ')}`);
|
|
1595
|
+
}
|
|
1596
|
+
if (svc.methods && svc.methods.length > 0) {
|
|
1597
|
+
const methodStrs = svc.methods.slice(0, 8).map((m) => {
|
|
1598
|
+
const params = m.parameters?.map((p) => `${p.name}${p.type ? `:${p.type}` : ''}`).join(', ') || '';
|
|
1599
|
+
return `\`${m.name}(${params})${m.returnType ? ": " + m.returnType : ''}\``;
|
|
1600
|
+
});
|
|
1601
|
+
cardLines.push(`- ⚙️ 方法: ${methodStrs.join(', ')}${svc.methods.length > 8 ? ` ...等${svc.methods.length}个` : ''}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (matchedApis.length > 0 || matchedEntities.length > 0 || matchedComponents.length > 0 || matchedRoutes.length > 0 || matchedDtos.length > 0 || matchedServices.length > 0) {
|
|
1606
|
+
extraSpecs.push({
|
|
1607
|
+
name: '📇 结构化代码事实卡片',
|
|
1608
|
+
path: 'structured-data-cards.md',
|
|
1609
|
+
content: cardLines.join('\n'),
|
|
1610
|
+
});
|
|
1611
|
+
const cardChars = cardLines.join('\n').length;
|
|
1612
|
+
logger_1.logger?.info?.(` 📇 结构化卡片: ${matchedApis.length} API + ${matchedEntities.length} Entity + ${matchedComponents.length} Component + ${matchedRoutes.length} Route + ${matchedDtos.length} DTO + ${matchedServices.length} Service (${Math.round(cardChars / 1000)}K 字符)`);
|
|
1613
|
+
structuredCardsInjected = true;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
catch { /* 结构化数据解析失败不阻断 */ }
|
|
1617
|
+
}
|
|
1618
|
+
const codeMatches = await (0, code_scanner_1.findRelevantCode)(searchQuery, 15, undefined, options.iteration, options.task);
|
|
1619
|
+
if (codeMatches.length > 0) {
|
|
1620
|
+
// v8.3.124+: 如果已注入结构化卡片,减少原始源码的文件数和长度
|
|
1621
|
+
const sourceMaxFiles = structuredCardsInjected ? 8 : 15;
|
|
1622
|
+
const sourceMaxBytes = structuredCardsInjected ? 60000 : 100000;
|
|
1623
|
+
const sourceContents = await (0, code_scanner_1.readRelevantSource)(codeMatches, sourceMaxBytes, sourceMaxFiles);
|
|
1624
|
+
const sourceKeys = Object.keys(sourceContents);
|
|
1625
|
+
if (sourceKeys.length > 0) {
|
|
1626
|
+
// v8.3.122+: execute 时注入知识图谱任务上下文
|
|
1627
|
+
// v8.3.125+: 自动检查过期并刷新
|
|
1628
|
+
if (command === 'execute' && options.iteration && options.task) {
|
|
1629
|
+
try {
|
|
1630
|
+
const kg = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
|
|
1631
|
+
if (kg) {
|
|
1632
|
+
const ctx = (0, knowledge_graph_1.getFullTaskContext)(kg, options.task);
|
|
1633
|
+
const ctxLines = ['## 🔗 任务关联上下文(知识图谱)', ''];
|
|
1634
|
+
if (ctx.requirement)
|
|
1635
|
+
ctxLines.push(`- **上游需求**: ${ctx.requirement.title} (${ctx.requirement.file})`);
|
|
1636
|
+
if (ctx.parentTask)
|
|
1637
|
+
ctxLines.push(`- **父任务**: ${ctx.parentTask.title}`);
|
|
1638
|
+
if (ctx.dependsOn.length > 0)
|
|
1639
|
+
ctxLines.push(`- **依赖任务**(需先完成): ${ctx.dependsOn.map(d => d.title).join(', ')}`);
|
|
1640
|
+
if (ctx.downstreamTasks.length > 0)
|
|
1641
|
+
ctxLines.push(`- **下游任务**(依赖本任务): ${ctx.downstreamTasks.map(d => d.title).join(', ')}`);
|
|
1642
|
+
if (ctx.relatedSpecs.length > 0) {
|
|
1643
|
+
ctxLines.push(`- **关联规格文档**:`);
|
|
1644
|
+
for (const spec of ctx.relatedSpecs) {
|
|
1645
|
+
ctxLines.push(` - ${spec.title}: \`${spec.file}\``);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
if (ctx.dependencyChain.length > 0) {
|
|
1649
|
+
ctxLines.push(`- **依赖链路**: ${ctx.dependencyChain.map(c => c.taskName).join(' → ')}`);
|
|
1650
|
+
}
|
|
1651
|
+
extraSpecs.push({
|
|
1652
|
+
name: '🔗 任务关联上下文',
|
|
1653
|
+
path: 'kg-context.md',
|
|
1654
|
+
content: ctxLines.join('\n'),
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
catch { /* 知识图谱加载失败不阻断 */ }
|
|
1659
|
+
}
|
|
1660
|
+
// v8.3.123+: analyze 时注入知识图谱迭代级上下文
|
|
1661
|
+
// v8.3.125+: 自动检查过期并刷新
|
|
1662
|
+
if (command === 'analyze' && options.iteration) {
|
|
1663
|
+
try {
|
|
1664
|
+
const kg = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
|
|
1665
|
+
if (kg) {
|
|
1666
|
+
const compactCtx = (0, context_builder_1.buildCompactContext)(kg, {});
|
|
1667
|
+
if (compactCtx) {
|
|
1668
|
+
extraSpecs.push({
|
|
1669
|
+
name: '🧠 知识图谱摘要',
|
|
1670
|
+
path: 'kg-summary.md',
|
|
1671
|
+
content: compactCtx,
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
catch { /* 知识图谱加载失败不阻断 */ }
|
|
1677
|
+
}
|
|
1678
|
+
// 按匹配得分排序注入源码,并附加匹配原因
|
|
1679
|
+
const sortedMatches = [...codeMatches].sort((a, b) => b.score - a.score);
|
|
1680
|
+
for (const match of sortedMatches.slice(0, sourceMaxFiles)) {
|
|
1681
|
+
const content = sourceContents[match.file];
|
|
1682
|
+
if (!content)
|
|
1683
|
+
continue;
|
|
1684
|
+
const reasons = [];
|
|
1685
|
+
if (match.score >= 50)
|
|
1686
|
+
reasons.push(`@spec 注释直接关联 (score:${match.score})`);
|
|
1687
|
+
else if (match.score >= 40)
|
|
1688
|
+
reasons.push(`知识图谱 source-file 实体关联 (score:${match.score})`);
|
|
1689
|
+
else if (match.score >= 35)
|
|
1690
|
+
reasons.push(`业务模块 codeEntities 关联 (score:${match.score})`);
|
|
1691
|
+
else if (match.score >= 25)
|
|
1692
|
+
reasons.push(`Spec 技术关键词匹配 (score:${match.score})`);
|
|
1693
|
+
else
|
|
1694
|
+
reasons.push(`关键词匹配 (score:${match.score})`);
|
|
1695
|
+
if (match.exports.length > 0)
|
|
1696
|
+
reasons.push(`导出: ${match.exports.join(', ')}`);
|
|
1697
|
+
if (match.apis.length > 0)
|
|
1698
|
+
reasons.push(`API: ${match.apis.join(', ')}`);
|
|
1699
|
+
extraSpecs.push({
|
|
1700
|
+
name: `🔍 ${(0, path_1.basename)(match.file)}`,
|
|
1701
|
+
path: match.file,
|
|
1702
|
+
content: `## 关联源码: ${match.file}\n> **匹配原因**: ${reasons.join(' | ')}\n\n\`\`\`${match.file.split('.').pop() || 'ts'}\n${content}\n\`\`\``,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
logger_1.logger?.info?.(` 💻 深入源码: ${sourceKeys.length} 个完整文件已注入 (${Math.round(Object.values(sourceContents).reduce((a, c) => a + c.length, 0) / 1000)}K 字符)`);
|
|
1706
|
+
// v8.3.125+: 可选阅读清单 — 让宿主 AI 按需深入读取未预加载的关联文件
|
|
1707
|
+
// 解决 "8文件/60KB 限制导致大型功能读不全" 的问题
|
|
1708
|
+
const unreadMatches = sortedMatches.slice(sourceMaxFiles);
|
|
1709
|
+
if (unreadMatches.length > 0) {
|
|
1710
|
+
const optionalLines = [
|
|
1711
|
+
'## 📖 可选阅读清单(宿主 AI 按需读取)',
|
|
1712
|
+
'> 以下文件与当前任务高度相关,但因 Token 预算限制未完整预加载到上下文中。',
|
|
1713
|
+
'> 如果你的 IDE(Cursor / Claude Code / Windsurf)支持文件读取,可按需打开以下文件深入理解业务逻辑:',
|
|
1714
|
+
'',
|
|
1715
|
+
];
|
|
1716
|
+
for (const match of unreadMatches.slice(0, 15)) {
|
|
1717
|
+
const reasons = [];
|
|
1718
|
+
if (match.score >= 50)
|
|
1719
|
+
reasons.push('@spec 注释直接关联');
|
|
1720
|
+
else if (match.score >= 40)
|
|
1721
|
+
reasons.push('知识图谱关联');
|
|
1722
|
+
else if (match.score >= 35)
|
|
1723
|
+
reasons.push('业务模块关联');
|
|
1724
|
+
else if (match.score >= 25)
|
|
1725
|
+
reasons.push('技术关键词匹配');
|
|
1726
|
+
else
|
|
1727
|
+
reasons.push('关键词匹配');
|
|
1728
|
+
if (match.exports.length > 0)
|
|
1729
|
+
reasons.push(`导出 ${match.exports.join(', ')}`);
|
|
1730
|
+
if (match.apis.length > 0)
|
|
1731
|
+
reasons.push(`API ${match.apis.join(', ')}`);
|
|
1732
|
+
optionalLines.push(`- \`${match.file}\` — ${reasons.join(' | ')} (score:${match.score})`);
|
|
1733
|
+
}
|
|
1734
|
+
if (unreadMatches.length > 15) {
|
|
1735
|
+
optionalLines.push(`\n... 还有 ${unreadMatches.length - 15} 个关联文件未列出`);
|
|
1736
|
+
}
|
|
1737
|
+
optionalLines.push('\n---');
|
|
1738
|
+
optionalLines.push('💡 **使用方式**: 如果上述预加载源码不足以理解实现细节,请打开对应文件查看完整代码。');
|
|
1739
|
+
extraSpecs.push({
|
|
1740
|
+
name: '📖 可选阅读清单',
|
|
1741
|
+
path: 'optional-reading.md',
|
|
1742
|
+
content: optionalLines.join('\n'),
|
|
1743
|
+
});
|
|
1744
|
+
logger_1.logger?.info?.(` 📖 可选阅读清单: ${unreadMatches.length} 个关联文件(宿主 AI 可按需读取)`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
catch (e) {
|
|
1750
|
+
logger_1.logger?.debug?.('深入源码读取失败:', e);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
// v8.3.125+: execute 时注入 INFO_GAP 自动补充上下文(如果存在)
|
|
1754
|
+
if (command === 'execute' && options.task) {
|
|
1755
|
+
try {
|
|
1756
|
+
const supplementPath = (0, path_1.join)(cwd, '.speccore', 'cache', `info-gap-supplement-${options.task}.md`);
|
|
1757
|
+
if (await (0, fs_extra_1.pathExists)(supplementPath)) {
|
|
1758
|
+
const supplementContent = await (0, fs_extra_1.readFile)(supplementPath, 'utf-8');
|
|
1759
|
+
if (supplementContent.trim().length > 0) {
|
|
1760
|
+
extraSpecs.push({
|
|
1761
|
+
name: '📎 INFO_GAP 自动补充上下文',
|
|
1762
|
+
path: supplementPath,
|
|
1763
|
+
content: `## 📎 上一轮执行发现的信息缺口补充\n\n${supplementContent}`,
|
|
1764
|
+
});
|
|
1765
|
+
logger_1.logger?.info?.(` 📎 已注入 INFO_GAP 补充上下文`);
|
|
1766
|
+
}
|
|
1311
1767
|
}
|
|
1312
1768
|
}
|
|
1769
|
+
catch { /* 忽略读取失败 */ }
|
|
1313
1770
|
}
|
|
1314
1771
|
// v6.72.0+: execute 时注入 CONSISTENCY_CHECK.md(前后端一致性校验)
|
|
1315
1772
|
if (command === 'execute' && taskDir) {
|
|
@@ -1341,11 +1798,7 @@ async function buildPrompt(command, options) {
|
|
|
1341
1798
|
// 加载知识图谱 → 生成任务关联链(< 500 tokens)
|
|
1342
1799
|
let taskContextStr;
|
|
1343
1800
|
if (options.task) {
|
|
1344
|
-
|
|
1345
|
-
const stale = await (0, knowledge_graph_1.isGraphStale)(cwd, options.iteration);
|
|
1346
|
-
if (stale) {
|
|
1347
|
-
graph = await (0, knowledge_graph_1.refreshKnowledgeGraph)(cwd, options.iteration);
|
|
1348
|
-
}
|
|
1801
|
+
const graph = await (0, knowledge_graph_1.loadFreshKnowledgeGraph)(cwd, options.iteration);
|
|
1349
1802
|
if (graph) {
|
|
1350
1803
|
taskContextStr = (0, context_builder_1.buildCompactContext)(graph, {
|
|
1351
1804
|
taskId: options.task,
|
|
@@ -1363,7 +1816,7 @@ async function buildPrompt(command, options) {
|
|
|
1363
1816
|
if (command === 'execute') {
|
|
1364
1817
|
const projectInfoMap = await (0, spec_paths_1.parseProjectInfo)();
|
|
1365
1818
|
if (projectInfoMap.size > 0) {
|
|
1366
|
-
const lines = ['## 📂 工程路径(代码输出位置)', '', '| 工程标识 | 工程类型 | 源码路径 |
|
|
1819
|
+
const lines = ['## 📂 工程路径(代码输出位置)', '', '| 工程标识 | 工程类型 | 源码路径 | 对应需求端 |', '| :--- | :--- | :--- | :--- |'];
|
|
1367
1820
|
for (const [identifier, info] of projectInfoMap) {
|
|
1368
1821
|
lines.push(`| ${identifier} | ${info.projectType || '-'} | \`${info.srcPath}\` | ${info.platform} |`);
|
|
1369
1822
|
}
|
|
@@ -1462,16 +1915,75 @@ function formatPrompt(prompt, maxTokens = 12000) {
|
|
|
1462
1915
|
return result;
|
|
1463
1916
|
}
|
|
1464
1917
|
}
|
|
1465
|
-
// Level 2:
|
|
1918
|
+
// Level 2: 分层预算控制 extraSpecs(v8.3.125+ unit-context-assembler)
|
|
1919
|
+
// 优先级: P1(结构化卡片/源码/INFO_GAP) > P2(KG上下文) > P3(全局规范/一致性检查)
|
|
1466
1920
|
if (prompt.extraSpecs.length > 0) {
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1921
|
+
const BUDGET_P1 = 10000; // 高优先级: 结构化卡片、关联源码、INFO_GAP补充
|
|
1922
|
+
const BUDGET_P2 = 5000; // 中优先级: 知识图谱上下文、任务关联链
|
|
1923
|
+
const BUDGET_P3 = 3000; // 低优先级: 全局规范、一致性检查、项目路径
|
|
1924
|
+
function getPriority(spec) {
|
|
1925
|
+
const n = spec.name;
|
|
1926
|
+
if (n.includes('结构化代码事实') || n.includes('INFO_GAP') || n.includes('关联源码'))
|
|
1927
|
+
return 1;
|
|
1928
|
+
if (n.includes('知识图谱') || n.includes('任务关联上下文'))
|
|
1929
|
+
return 2;
|
|
1930
|
+
return 3; // CONSISTENCY_CHECK、工程路径、全局规范等
|
|
1931
|
+
}
|
|
1932
|
+
const sorted = [...prompt.extraSpecs].sort((a, b) => getPriority(a) - getPriority(b));
|
|
1933
|
+
const budgeted = [];
|
|
1934
|
+
let usedP1 = 0, usedP2 = 0, usedP3 = 0;
|
|
1935
|
+
for (const spec of sorted) {
|
|
1936
|
+
const p = getPriority(spec);
|
|
1937
|
+
const len = spec.content.length;
|
|
1938
|
+
let keep = true;
|
|
1939
|
+
let content = spec.content;
|
|
1940
|
+
if (p === 1) {
|
|
1941
|
+
if (usedP1 + len > BUDGET_P1) {
|
|
1942
|
+
const remain = Math.max(0, BUDGET_P1 - usedP1);
|
|
1943
|
+
if (remain < 200) {
|
|
1944
|
+
keep = false;
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
content = content.slice(0, remain) + '\n> ... (P1预算截断)';
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
if (keep)
|
|
1951
|
+
usedP1 += content.length;
|
|
1952
|
+
}
|
|
1953
|
+
else if (p === 2) {
|
|
1954
|
+
if (usedP2 + len > BUDGET_P2) {
|
|
1955
|
+
const remain = Math.max(0, BUDGET_P2 - usedP2);
|
|
1956
|
+
if (remain < 200) {
|
|
1957
|
+
keep = false;
|
|
1958
|
+
}
|
|
1959
|
+
else {
|
|
1960
|
+
content = content.slice(0, remain) + '\n> ... (P2预算截断)';
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (keep)
|
|
1964
|
+
usedP2 += content.length;
|
|
1965
|
+
}
|
|
1966
|
+
else {
|
|
1967
|
+
if (usedP3 + len > BUDGET_P3) {
|
|
1968
|
+
const remain = Math.max(0, BUDGET_P3 - usedP3);
|
|
1969
|
+
if (remain < 200) {
|
|
1970
|
+
keep = false;
|
|
1971
|
+
}
|
|
1972
|
+
else {
|
|
1973
|
+
content = content.slice(0, remain) + '\n> ... (P3预算截断)';
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (keep)
|
|
1977
|
+
usedP3 += content.length;
|
|
1978
|
+
}
|
|
1979
|
+
if (keep)
|
|
1980
|
+
budgeted.push({ ...spec, content });
|
|
1981
|
+
}
|
|
1982
|
+
result = buildPromptText({ ...prompt, extraSpecs: budgeted });
|
|
1472
1983
|
tokens = estimateTokens(result);
|
|
1473
1984
|
if (tokens <= maxTokens) {
|
|
1474
|
-
|
|
1985
|
+
const removed = prompt.extraSpecs.length - budgeted.length;
|
|
1986
|
+
logger_1.logger?.info?.(` 🪶 Prompt 已简化:分层预算控制 (P1:${usedP1}/${BUDGET_P1} P2:${usedP2}/${BUDGET_P2} P3:${usedP3}/${BUDGET_P3}${removed > 0 ? ` 移除${removed}项` : ''})`);
|
|
1475
1987
|
return result;
|
|
1476
1988
|
}
|
|
1477
1989
|
}
|
|
@@ -1601,15 +2113,31 @@ function buildPromptText(prompt) {
|
|
|
1601
2113
|
/**
|
|
1602
2114
|
* 解析 AI 返回的 JSON 文件列表
|
|
1603
2115
|
*/
|
|
2116
|
+
/** v8.3.124+: 解析 AI 响应中的 INFO_GAP 标记 */
|
|
2117
|
+
function extractInfoGaps(response) {
|
|
2118
|
+
const gaps = [];
|
|
2119
|
+
const regex = /\[INFO_GAP:\s*([^\]]+)\]/g;
|
|
2120
|
+
let match;
|
|
2121
|
+
while ((match = regex.exec(response)) !== null) {
|
|
2122
|
+
gaps.push(match[1].trim());
|
|
2123
|
+
}
|
|
2124
|
+
return [...new Set(gaps)]; // 去重
|
|
2125
|
+
}
|
|
1604
2126
|
function parseAiResponse(response) {
|
|
2127
|
+
// 先提取 INFO_GAP(即使 JSON 解析失败也能捕获)
|
|
2128
|
+
const infoGaps = extractInfoGaps(response);
|
|
1605
2129
|
// 尝试从响应中提取 JSON
|
|
1606
2130
|
const jsonMatch = response.match(/\{[\s\S]*"files"[\s\S]*\}/);
|
|
1607
|
-
if (!jsonMatch)
|
|
2131
|
+
if (!jsonMatch) {
|
|
2132
|
+
// 没有 JSON,但有 INFO_GAP,返回空文件列表 + gaps
|
|
2133
|
+
if (infoGaps.length > 0)
|
|
2134
|
+
return { files: [], infoGaps };
|
|
1608
2135
|
return null;
|
|
2136
|
+
}
|
|
1609
2137
|
try {
|
|
1610
2138
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
1611
2139
|
if (parsed.files && Array.isArray(parsed.files)) {
|
|
1612
|
-
return parsed;
|
|
2140
|
+
return { files: parsed.files, infoGaps: infoGaps.length > 0 ? infoGaps : undefined };
|
|
1613
2141
|
}
|
|
1614
2142
|
}
|
|
1615
2143
|
catch {
|
|
@@ -1807,9 +2335,13 @@ async function expandMarkdownLinks(content, baseDir, seenPaths, maxDepth = 2, cu
|
|
|
1807
2335
|
if (links.length === 0)
|
|
1808
2336
|
return content;
|
|
1809
2337
|
const expansions = [];
|
|
2338
|
+
// v8.3.125+: 收集外链资源,供宿主 AI 按需读取
|
|
2339
|
+
const externalLinks = [];
|
|
1810
2340
|
for (const link of links) {
|
|
1811
|
-
if (isExternalLink(link.path))
|
|
2341
|
+
if (isExternalLink(link.path)) {
|
|
2342
|
+
externalLinks.push(link);
|
|
1812
2343
|
continue;
|
|
2344
|
+
}
|
|
1813
2345
|
const resolved = resolveLinkPath(baseDir, link.path);
|
|
1814
2346
|
if (seenPaths.has(resolved))
|
|
1815
2347
|
continue;
|
|
@@ -1821,9 +2353,9 @@ async function expandMarkdownLinks(content, baseDir, seenPaths, maxDepth = 2, cu
|
|
|
1821
2353
|
if (!st.isFile())
|
|
1822
2354
|
continue;
|
|
1823
2355
|
let linkContent = await (0, fs_extra_1.readFile)(resolved, 'utf-8');
|
|
1824
|
-
// HTML
|
|
2356
|
+
// v8.3.122+: HTML 原型文件保留完整内容,不再提取纯文本
|
|
1825
2357
|
if (resolved.endsWith('.html') || resolved.endsWith('.htm')) {
|
|
1826
|
-
|
|
2358
|
+
// 保留原始 HTML(包含 CSS/JS/结构),让 AI 完整解析原型
|
|
1827
2359
|
}
|
|
1828
2360
|
if (linkContent.trim().length <= 30)
|
|
1829
2361
|
continue;
|
|
@@ -1835,7 +2367,16 @@ async function expandMarkdownLinks(content, baseDir, seenPaths, maxDepth = 2, cu
|
|
|
1835
2367
|
}
|
|
1836
2368
|
catch { /* 忽略读取失败的链接文件 */ }
|
|
1837
2369
|
}
|
|
1838
|
-
|
|
2370
|
+
let expanded = content + expansions.join('');
|
|
2371
|
+
// v8.3.125+: 在内容末尾附加外链资源清单,提示宿主 AI 按需读取
|
|
2372
|
+
if (externalLinks.length > 0) {
|
|
2373
|
+
const externalSection = [
|
|
2374
|
+
'\n\n---',
|
|
2375
|
+
'📎 **外链资源清单**(CLI 无法直接获取内容,宿主 AI 可按需访问):',
|
|
2376
|
+
...externalLinks.map(l => `- [${l.text || '链接'}](${l.path})`),
|
|
2377
|
+
].join('\n');
|
|
2378
|
+
expanded += externalSection;
|
|
2379
|
+
}
|
|
1839
2380
|
// 递归展开新内容中的链接(深度 + 1)
|
|
1840
2381
|
if (currentDepth + 1 < maxDepth) {
|
|
1841
2382
|
return expandMarkdownLinks(expanded, baseDir, seenPaths, maxDepth, currentDepth + 1, maxChars);
|
|
@@ -1850,9 +2391,12 @@ async function inlineMarkdownImages(content, baseDir, seenPaths, visionConfig, m
|
|
|
1850
2391
|
const inlines = [];
|
|
1851
2392
|
let visionCallCount = 0;
|
|
1852
2393
|
const maxVisionCalls = visionConfig?.maxImagesPerPrompt ?? 10;
|
|
2394
|
+
// v8.3.125+: 收集外链图片,供宿主 AI 按需查看
|
|
2395
|
+
const externalImages = [];
|
|
1853
2396
|
for (const img of images) {
|
|
1854
2397
|
if (isExternalLink(img.path)) {
|
|
1855
|
-
|
|
2398
|
+
externalImages.push(img);
|
|
2399
|
+
inlines.push(`\n<!-- 外链图片: ${img.alt || '无描述'} | URL: ${img.path} -->`);
|
|
1856
2400
|
continue;
|
|
1857
2401
|
}
|
|
1858
2402
|
const resolved = resolveLinkPath(baseDir, img.path);
|
|
@@ -1902,6 +2446,14 @@ async function inlineMarkdownImages(content, baseDir, seenPaths, visionConfig, m
|
|
|
1902
2446
|
catch { /* ignore */ }
|
|
1903
2447
|
}
|
|
1904
2448
|
}
|
|
2449
|
+
// v8.3.125+: 在内容末尾附加外链图片清单,提示宿主 AI 按需查看
|
|
2450
|
+
if (externalImages.length > 0) {
|
|
2451
|
+
inlines.push([
|
|
2452
|
+
'\n\n---',
|
|
2453
|
+
'🖼️ **外链图片清单**(CLI 无法直接获取内容,宿主 AI 可按需查看):',
|
|
2454
|
+
...externalImages.map(img => `- ${img.alt || '无描述'}: ${img.path}`),
|
|
2455
|
+
].join('\n'));
|
|
2456
|
+
}
|
|
1905
2457
|
return content + inlines.join('');
|
|
1906
2458
|
}
|
|
1907
2459
|
/**
|