create-vela-workflow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -0
- package/bin/cli.js +188 -0
- package/docs/ai-workflow-tutorial.md +462 -0
- package/docs/official-site-tutorial.md +391 -0
- package/package.json +34 -0
- package/templates/.github/HARNESS-ENGINEERING-GUIDE.md +407 -0
- package/templates/.github/agents/vela-knowledge.agent.md +45 -0
- package/templates/.github/agents/vela-s1-prd.agent.md +69 -0
- package/templates/.github/agents/vela-s2-tech.agent.md +66 -0
- package/templates/.github/agents/vela-s3-coding.agent.md +301 -0
- package/templates/.github/agents/vela-workflow.agent.md +110 -0
- package/templates/.github/copilot-instructions.md +64 -0
- package/templates/.github/prompts/vela-apis.prompt.md +98 -0
- package/templates/.github/prompts/vela-best-practices.prompt.md +93 -0
- package/templates/.github/prompts/vela-components.prompt.md +118 -0
- package/templates/.github/prompts/vela-dev-guide.prompt.md +622 -0
- package/templates/.github/rules/project-init.md +45 -0
- package/templates/.github/rules/vela-coding-convention.md +324 -0
- package/templates/.github/rules/vela-css.md +217 -0
- package/templates/.github/rules/vela-design-driven.md +306 -0
- package/templates/.github/rules/vela-figma-mcp.md +198 -0
- package/templates/.github/rules/vela-format.md +119 -0
- package/templates/.github/rules/vela-layout.md +67 -0
- package/templates/.github/rules/vela-platform.md +46 -0
- package/templates/.github/rules/vela-quality.md +109 -0
- package/templates/.kiro/hooks/figma-design-check.kiro.hook +14 -0
- package/templates/.kiro/hooks/post-coding-validation.kiro.hook +13 -0
- package/templates/.kiro/hooks/validate-ux-files.kiro.hook +16 -0
- package/templates/.kiro/settings/mcp.json +7 -0
- package/templates/.kiro/skills/vela-js-app/SKILL.md +1072 -0
- package/templates/.kiro/steering/workflow-conventions.md +110 -0
- package/templates/.workflow/resource-paths.json +62 -0
- package/templates/.workflow/scripts/.gitkeep +0 -0
- package/templates/.workflow/scripts/checkpoint_manager.js +284 -0
- package/templates/.workflow/scripts/context_loader.js +841 -0
- package/templates/.workflow/scripts/figma_export.js +346 -0
- package/templates/.workflow/scripts/session_manager.js +438 -0
- package/templates/.workflow/stages/.gitkeep +0 -0
- package/templates/.workflow/stages/commands.md +171 -0
- package/templates/.workflow/stages/s1_prd.md +286 -0
- package/templates/.workflow/stages/s2_tech_design.md +302 -0
- package/templates/.workflow/stages/s3_coding.md +699 -0
- package/templates/.workflow/stages/s4_simulator.md +259 -0
- package/templates/.workflow/workflow-config.json +46 -0
- package/templates/.workflow/workflow_starter.md +912 -0
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文加载器(Context Loader)
|
|
3
|
+
*
|
|
4
|
+
* 负责为每个阶段的 Agent 组装执行上下文:
|
|
5
|
+
* 1. 从 resource-paths.json 解析知识库路径
|
|
6
|
+
* 2. 加载知识库文件内容
|
|
7
|
+
* 3. 加载前置阶段产出物
|
|
8
|
+
* 4. 将上下文注入到 Agent 提示词模板的占位符中
|
|
9
|
+
*
|
|
10
|
+
* Requirements: 7.2, 8.2, 8.4, 4.1, 5.1
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// 项目根目录(vela-quickapp-workflow/)
|
|
17
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
18
|
+
|
|
19
|
+
// 配置文件路径
|
|
20
|
+
const RESOURCE_PATHS_FILE = path.resolve(__dirname, '../resource-paths.json');
|
|
21
|
+
const WORKFLOW_CONFIG_FILE = path.resolve(__dirname, '../workflow-config.json');
|
|
22
|
+
|
|
23
|
+
// Session 存储根目录
|
|
24
|
+
const SESSIONS_DIR = path.resolve(PROJECT_ROOT, '.ai-workspace/sessions');
|
|
25
|
+
|
|
26
|
+
// 有效的阶段 ID
|
|
27
|
+
const VALID_STAGE_IDS = ['S1', 'S2', 'S3', 'S4'];
|
|
28
|
+
|
|
29
|
+
// 知识库文件内存缓存: filePath → { content, mtimeMs }
|
|
30
|
+
const _fileCache = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 带缓存的文件读取
|
|
34
|
+
* 根据文件 mtime 判断是否需要重新读取,避免重复加载未变更的知识库文件。
|
|
35
|
+
*
|
|
36
|
+
* @param {string} filePath - 文件绝对路径
|
|
37
|
+
* @returns {string} 文件内容
|
|
38
|
+
*/
|
|
39
|
+
function readFileWithCache(filePath) {
|
|
40
|
+
const stat = fs.statSync(filePath);
|
|
41
|
+
const cached = _fileCache.get(filePath);
|
|
42
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
43
|
+
return cached.content;
|
|
44
|
+
}
|
|
45
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
46
|
+
_fileCache.set(filePath, { content, mtimeMs: stat.mtimeMs });
|
|
47
|
+
return content;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 加载 resource-paths.json 配置
|
|
52
|
+
* @returns {object} 资源路径配置对象
|
|
53
|
+
* @throws {Error} 配置文件不存在或格式非法
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* 加载 resource-paths.json 配置
|
|
57
|
+
* @returns {object} 资源路径配置对象
|
|
58
|
+
* @throws {Error} 配置文件不存在或格式非法(含详细诊断信息)
|
|
59
|
+
*/
|
|
60
|
+
function loadResourcePaths() {
|
|
61
|
+
if (!fs.existsSync(RESOURCE_PATHS_FILE)) {
|
|
62
|
+
throw new Error(`资源路径配置文件不存在: ${RESOURCE_PATHS_FILE}`);
|
|
63
|
+
}
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = fs.readFileSync(RESOURCE_PATHS_FILE, 'utf-8');
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throw new Error(`读取资源路径配置文件失败: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(raw);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
throw new Error(`资源路径配置文件 JSON 格式错误: ${err.message}(文件: ${RESOURCE_PATHS_FILE})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 加载 workflow-config.json 配置
|
|
79
|
+
* @returns {object} 工作流配置对象
|
|
80
|
+
* @throws {Error} 配置文件不存在或格式非法
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* 加载 workflow-config.json 配置
|
|
84
|
+
* @returns {object} 工作流配置对象
|
|
85
|
+
* @throws {Error} 配置文件不存在或格式非法(含详细诊断信息)
|
|
86
|
+
*/
|
|
87
|
+
function loadWorkflowConfig() {
|
|
88
|
+
if (!fs.existsSync(WORKFLOW_CONFIG_FILE)) {
|
|
89
|
+
throw new Error(`工作流配置文件不存在: ${WORKFLOW_CONFIG_FILE}`);
|
|
90
|
+
}
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = fs.readFileSync(WORKFLOW_CONFIG_FILE, 'utf-8');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(`读取工作流配置文件失败: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(raw);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new Error(`工作流配置文件 JSON 格式错误: ${err.message}(文件: ${WORKFLOW_CONFIG_FILE})`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 加载指定知识库分类的文件内容
|
|
107
|
+
*
|
|
108
|
+
* 采用 INDEX 优先策略:
|
|
109
|
+
* 1. 优先读取知识库目录下的 INDEX.md(由 index_file 字段指定)
|
|
110
|
+
* 2. 从 INDEX.md 的文件列表表格中解析出实际存在的文件名
|
|
111
|
+
* 3. 加载 INDEX.md 中列出的所有文件(排除 .gitkeep 等无关文件)
|
|
112
|
+
* 4. 若 INDEX.md 不存在,回退到 key_files 列表
|
|
113
|
+
*
|
|
114
|
+
* @param {string} knowledgeKey - 知识库分类键(如 "vela/dev-paradigm")
|
|
115
|
+
* @param {object} knowledgeMappings - resource-paths.json 中的 knowledge_mappings
|
|
116
|
+
* @returns {{ key: string, files: Array<{ name: string, content: string }>, indexContent: string|null, warnings: string[] }}
|
|
117
|
+
*/
|
|
118
|
+
function loadKnowledgeFiles(knowledgeKey, knowledgeMappings) {
|
|
119
|
+
const warnings = [];
|
|
120
|
+
const files = [];
|
|
121
|
+
let indexContent = null;
|
|
122
|
+
|
|
123
|
+
const mapping = knowledgeMappings[knowledgeKey];
|
|
124
|
+
if (!mapping) {
|
|
125
|
+
warnings.push(`知识库映射未找到: ${knowledgeKey}`);
|
|
126
|
+
return { key: knowledgeKey, files, indexContent, warnings };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const knowledgeDir = path.resolve(PROJECT_ROOT, mapping.path);
|
|
130
|
+
if (!fs.existsSync(knowledgeDir)) {
|
|
131
|
+
warnings.push(`知识库目录不存在: ${mapping.path}`);
|
|
132
|
+
return { key: knowledgeKey, files, indexContent, warnings };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// INDEX 优先策略:尝试读取 INDEX.md
|
|
136
|
+
const indexFileName = mapping.index_file || 'INDEX.md';
|
|
137
|
+
const indexPath = path.join(knowledgeDir, indexFileName);
|
|
138
|
+
let fileList = null;
|
|
139
|
+
|
|
140
|
+
if (fs.existsSync(indexPath)) {
|
|
141
|
+
try {
|
|
142
|
+
indexContent = readFileWithCache(indexPath);
|
|
143
|
+
// 从 INDEX.md 表格中解析文件列表
|
|
144
|
+
// 表格格式: | 文件名 | 路径 | 大小 |
|
|
145
|
+
fileList = parseIndexFileList(indexContent);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
warnings.push(`读取知识库索引文件失败: ${mapping.path}/${indexFileName} - ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
warnings.push(`知识库索引文件不存在: ${mapping.path}/${indexFileName},回退到 key_files`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 确定要加载的文件列表
|
|
154
|
+
const filesToLoad = fileList || (mapping.key_files || []);
|
|
155
|
+
|
|
156
|
+
for (const fileName of filesToLoad) {
|
|
157
|
+
const filePath = path.join(knowledgeDir, fileName);
|
|
158
|
+
if (!fs.existsSync(filePath)) {
|
|
159
|
+
warnings.push(`知识库文件缺失: ${mapping.path}/${fileName}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const content = readFileWithCache(filePath);
|
|
164
|
+
files.push({ name: fileName, content });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
warnings.push(`读取知识库文件失败: ${mapping.path}/${fileName} - ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { key: knowledgeKey, files, indexContent, warnings };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 从 INDEX.md 内容中解析文件列表
|
|
175
|
+
*
|
|
176
|
+
* 解析 INDEX.md 中的所有 Markdown 表格,提取 .md 文件名列表。
|
|
177
|
+
* 支持多种表格格式:
|
|
178
|
+
* - | 文件名 | 路径 | 大小 |(dev-paradigm、api-reference 格式,取第一列或第二列)
|
|
179
|
+
* - | 文件名 | 描述 |(components 格式,取第一列)
|
|
180
|
+
* - | 目录/文件 | 说明 |(examples 格式,取第一列中的 .md 文件)
|
|
181
|
+
* 支持同一 INDEX.md 中包含多个表格(如 components 按类别分组)。
|
|
182
|
+
* 自动排除 .gitkeep、INDEX.md 等非内容文件。
|
|
183
|
+
*
|
|
184
|
+
* @param {string} indexContent - INDEX.md 文件内容
|
|
185
|
+
* @returns {string[]|null} 文件名数组,解析失败返回 null
|
|
186
|
+
*/
|
|
187
|
+
function parseIndexFileList(indexContent) {
|
|
188
|
+
if (!indexContent) return null;
|
|
189
|
+
|
|
190
|
+
const lines = indexContent.split('\n');
|
|
191
|
+
const fileNames = [];
|
|
192
|
+
const seenFiles = new Set();
|
|
193
|
+
// 跳过的非内容文件
|
|
194
|
+
const skipFiles = new Set(['.gitkeep', 'INDEX.md']);
|
|
195
|
+
|
|
196
|
+
let inTable = false;
|
|
197
|
+
let headerPassed = false;
|
|
198
|
+
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
const trimmed = line.trim();
|
|
201
|
+
// 检测表格行(以 | 开头和结尾)
|
|
202
|
+
if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
|
|
203
|
+
// 跳过分隔行(如 |--------|------|------|)
|
|
204
|
+
if (trimmed.replace(/[\|\s\-:]/g, '') === '') {
|
|
205
|
+
headerPassed = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// 跳过表头行(每个新表格的第一行)
|
|
209
|
+
if (!inTable) {
|
|
210
|
+
inTable = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!headerPassed) continue;
|
|
214
|
+
|
|
215
|
+
// 解析数据行,从各列中查找 .md 文件名
|
|
216
|
+
const cells = trimmed.split('|').map(c => c.trim()).filter(c => c !== '');
|
|
217
|
+
if (cells.length >= 1) {
|
|
218
|
+
// 优先检查第一列是否为 .md 文件名(适用于大多数格式)
|
|
219
|
+
let fileName = null;
|
|
220
|
+
if (cells[0] && cells[0].endsWith('.md')) {
|
|
221
|
+
fileName = cells[0];
|
|
222
|
+
} else if (cells.length >= 2 && cells[1] && cells[1].endsWith('.md')) {
|
|
223
|
+
// 回退到第二列(兼容旧格式:| 名称 | 路径.md | 大小 |)
|
|
224
|
+
fileName = cells[1];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (fileName && !skipFiles.has(fileName) && !seenFiles.has(fileName)) {
|
|
228
|
+
seenFiles.add(fileName);
|
|
229
|
+
fileNames.push(fileName);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
// 非表格行,重置表格状态以支持解析下一个表格
|
|
234
|
+
inTable = false;
|
|
235
|
+
headerPassed = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return fileNames.length > 0 ? fileNames : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 加载前置阶段的产出物内容
|
|
244
|
+
*
|
|
245
|
+
* 根据 workflow-config.json 中的 prerequisites 和 inputs 配置,
|
|
246
|
+
* 从 session 目录中读取前置阶段的产出物文件。
|
|
247
|
+
*
|
|
248
|
+
* @param {string} stageId - 当前阶段 ID(如 "S2")
|
|
249
|
+
* @param {object} session - Session 数据对象
|
|
250
|
+
* @returns {{ outputs: Array<{ stage: string, file: string, content: string }>, warnings: string[] }}
|
|
251
|
+
*/
|
|
252
|
+
function loadPrerequisiteOutputs(stageId, session) {
|
|
253
|
+
const warnings = [];
|
|
254
|
+
const outputs = [];
|
|
255
|
+
|
|
256
|
+
const workflowConfig = loadWorkflowConfig();
|
|
257
|
+
const stageConfig = workflowConfig.stages[stageId];
|
|
258
|
+
if (!stageConfig) {
|
|
259
|
+
warnings.push(`阶段配置未找到: ${stageId}`);
|
|
260
|
+
return { outputs, warnings };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const prerequisites = stageConfig.prerequisites || [];
|
|
264
|
+
if (prerequisites.length === 0) {
|
|
265
|
+
return { outputs, warnings };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const sessionDir = path.join(SESSIONS_DIR, session.session_id);
|
|
269
|
+
|
|
270
|
+
// 加载 inputs 中定义的前置产出物文件
|
|
271
|
+
const inputs = stageConfig.inputs || [];
|
|
272
|
+
for (const inputFile of inputs) {
|
|
273
|
+
// 跳过非文件输入(如 figma_export, requirement_description)
|
|
274
|
+
if (!inputFile.endsWith('.md')) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const filePath = path.join(sessionDir, inputFile);
|
|
279
|
+
if (!fs.existsSync(filePath)) {
|
|
280
|
+
warnings.push(`前置产出物缺失: ${inputFile}`);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
286
|
+
// 确定该文件属于哪个前置阶段
|
|
287
|
+
const sourceStage = findStageByOutput(inputFile, workflowConfig);
|
|
288
|
+
outputs.push({
|
|
289
|
+
stage: sourceStage || 'unknown',
|
|
290
|
+
file: inputFile,
|
|
291
|
+
content
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
warnings.push(`读取前置产出物失败: ${inputFile} - ${err.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { outputs, warnings };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 根据产出物文件名查找对应的阶段 ID
|
|
303
|
+
*
|
|
304
|
+
* @param {string} outputFile - 产出物文件名(如 "01-prd.md")
|
|
305
|
+
* @param {object} workflowConfig - 工作流配置对象
|
|
306
|
+
* @returns {string|null} 阶段 ID 或 null
|
|
307
|
+
*/
|
|
308
|
+
function findStageByOutput(outputFile, workflowConfig) {
|
|
309
|
+
for (const [stageId, config] of Object.entries(workflowConfig.stages)) {
|
|
310
|
+
if (config.output_template === outputFile) {
|
|
311
|
+
return stageId;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 加载 Figma 导出数据
|
|
320
|
+
*
|
|
321
|
+
* 从 session 目录下的 figma-exports/ 子目录读取导出的 JSON 数据。
|
|
322
|
+
*
|
|
323
|
+
* @param {object} session - Session 数据对象
|
|
324
|
+
* @returns {{ data: string, warnings: string[] }}
|
|
325
|
+
*/
|
|
326
|
+
/**
|
|
327
|
+
* 加载 Figma 导出数据
|
|
328
|
+
*
|
|
329
|
+
* 从 session 目录下的 figma-exports/ 子目录读取导出的 JSON 数据。
|
|
330
|
+
* 对损坏的 JSON 文件进行容错处理,跳过无法解析的文件并记录警告。
|
|
331
|
+
*
|
|
332
|
+
* @param {object} session - Session 数据对象
|
|
333
|
+
* @returns {{ data: string, warnings: string[] }}
|
|
334
|
+
*/
|
|
335
|
+
function loadFigmaData(session) {
|
|
336
|
+
const warnings = [];
|
|
337
|
+
const sessionDir = path.join(SESSIONS_DIR, session.session_id);
|
|
338
|
+
const figmaDir = path.join(sessionDir, 'figma-exports');
|
|
339
|
+
|
|
340
|
+
if (!fs.existsSync(figmaDir)) {
|
|
341
|
+
return { data: '无 Figma 设计稿数据', warnings };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const files = fs.readdirSync(figmaDir).filter(f => f.endsWith('.json'));
|
|
346
|
+
if (files.length === 0) {
|
|
347
|
+
return { data: '无 Figma 设计稿数据', warnings };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const figmaContents = [];
|
|
351
|
+
for (const file of files) {
|
|
352
|
+
const filePath = path.join(figmaDir, file);
|
|
353
|
+
try {
|
|
354
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
355
|
+
// 验证 JSON 合法性
|
|
356
|
+
JSON.parse(content);
|
|
357
|
+
figmaContents.push(`### ${file}\n\`\`\`json\n${content}\n\`\`\``);
|
|
358
|
+
} catch (fileErr) {
|
|
359
|
+
warnings.push(`Figma 数据文件损坏,已跳过: ${file} - ${fileErr.message}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (figmaContents.length === 0) {
|
|
364
|
+
return { data: '无 Figma 设计稿数据(所有文件均损坏)', warnings };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 加载图片清单(如果存在)
|
|
368
|
+
const manifestPath = path.join(figmaDir, 'image-manifest.json');
|
|
369
|
+
if (fs.existsSync(manifestPath)) {
|
|
370
|
+
try {
|
|
371
|
+
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
|
372
|
+
const manifest = JSON.parse(manifestContent);
|
|
373
|
+
if (manifest.images && manifest.images.length > 0) {
|
|
374
|
+
const imageList = manifest.images.map(img =>
|
|
375
|
+
`- \`${img.fileName}\` (nodeId: ${img.nodeId})`
|
|
376
|
+
).join('\n');
|
|
377
|
+
figmaContents.push(`### 已下载的设计资源图片\n\n共 ${manifest.images.length} 个文件,保存在 \`figma-exports/images/\` 目录:\n\n${imageList}`);
|
|
378
|
+
}
|
|
379
|
+
} catch (manifestErr) {
|
|
380
|
+
warnings.push(`图片清单文件损坏,已跳过: ${manifestErr.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return { data: figmaContents.join('\n\n'), warnings };
|
|
385
|
+
} catch (err) {
|
|
386
|
+
warnings.push(`读取 Figma 数据目录失败: ${err.message}`);
|
|
387
|
+
return { data: '无 Figma 设计稿数据', warnings };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 扫描快应用项目工程目录,分析现有项目结构
|
|
393
|
+
*
|
|
394
|
+
* 读取项目的 manifest.json、页面目录、组件目录等,
|
|
395
|
+
* 生成项目现状摘要,供技术方案 Agent 判断增量开发策略。
|
|
396
|
+
*
|
|
397
|
+
* @param {object} session - Session 数据对象(需包含 inputs.project_path)
|
|
398
|
+
* @returns {{ data: string, isExistingProject: boolean, isEmptyProject?: boolean, warnings: string[] }}
|
|
399
|
+
*/
|
|
400
|
+
function scanProjectStructure(session) {
|
|
401
|
+
const warnings = [];
|
|
402
|
+
const projectPath = session && session.inputs && session.inputs.project_path;
|
|
403
|
+
|
|
404
|
+
if (!projectPath) {
|
|
405
|
+
return { data: '未指定项目工程路径(将作为全新项目处理)', isExistingProject: false, warnings };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const resolvedPath = path.resolve(PROJECT_ROOT, projectPath);
|
|
409
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
410
|
+
return { data: `项目工程路径不存在: ${projectPath}(将作为全新项目处理)`, isExistingProject: false, warnings };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 检查目录是否为空(仅含 .gitkeep 等隐藏文件也视为空)
|
|
414
|
+
try {
|
|
415
|
+
const entries = fs.readdirSync(resolvedPath).filter(f => !f.startsWith('.'));
|
|
416
|
+
if (entries.length === 0) {
|
|
417
|
+
return {
|
|
418
|
+
data: `## 项目工程现状分析\n\n**项目路径**: \`${projectPath}\`\n**状态**: ⚠️ 空目录\n\n该目录存在但为空,尚未初始化快应用工程。研发阶段(S3)开始前,需先提示开发者执行以下命令初始化项目:\n\n\`\`\`bash\nnpm create aiot\n\`\`\`\n\n初始化完成后再进行编码实现。`,
|
|
419
|
+
isExistingProject: false,
|
|
420
|
+
isEmptyProject: true,
|
|
421
|
+
warnings
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
warnings.push(`读取项目目录失败: ${err.message}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 检查 src 目录(快应用标准结构)
|
|
429
|
+
const srcDir = path.join(resolvedPath, 'src');
|
|
430
|
+
const hasSrc = fs.existsSync(srcDir);
|
|
431
|
+
|
|
432
|
+
// 读取 manifest.json
|
|
433
|
+
let manifest = null;
|
|
434
|
+
const manifestPath = hasSrc
|
|
435
|
+
? path.join(srcDir, 'manifest.json')
|
|
436
|
+
: path.join(resolvedPath, 'manifest.json');
|
|
437
|
+
if (fs.existsSync(manifestPath)) {
|
|
438
|
+
try {
|
|
439
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
440
|
+
} catch (err) {
|
|
441
|
+
warnings.push(`manifest.json 解析失败: ${err.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 如果没有 manifest.json,可能是目录有文件但未初始化快应用工程
|
|
446
|
+
if (!manifest) {
|
|
447
|
+
return {
|
|
448
|
+
data: `## 项目工程现状分析\n\n**项目路径**: \`${projectPath}\`\n**状态**: ⚠️ 非快应用工程(无 manifest.json)\n\n该目录存在但未包含有效的快应用工程结构(缺少 manifest.json)。研发阶段(S3)开始前,需先提示开发者在该目录下执行以下命令初始化项目:\n\n\`\`\`bash\nnpm create aiot\n\`\`\`\n\n初始化完成后再进行编码实现。`,
|
|
449
|
+
isExistingProject: false,
|
|
450
|
+
isEmptyProject: true,
|
|
451
|
+
warnings
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ---- 以下为已有项目分析 ----
|
|
456
|
+
const sections = [];
|
|
457
|
+
sections.push(`## 项目工程现状分析\n`);
|
|
458
|
+
sections.push(`**项目路径**: \`${projectPath}\``);
|
|
459
|
+
sections.push(`**包名**: \`${manifest.package || '未定义'}\``);
|
|
460
|
+
sections.push(`**应用名称**: \`${manifest.name || '未定义'}\``);
|
|
461
|
+
sections.push(`**版本**: ${manifest.versionName || '?'} (code: ${manifest.versionCode || '?'})`);
|
|
462
|
+
sections.push(`**目标设备**: ${(manifest.deviceTypeList || []).join(', ') || '未定义'}`);
|
|
463
|
+
|
|
464
|
+
// 分析 features(系统 API 依赖)
|
|
465
|
+
if (manifest.features && manifest.features.length > 0) {
|
|
466
|
+
const featureNames = manifest.features.map(f => f.name);
|
|
467
|
+
sections.push(`\n### 已声明的系统 API (features)\n`);
|
|
468
|
+
sections.push(featureNames.map(f => `- \`${f}\``).join('\n'));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 分析路由和页面
|
|
472
|
+
if (manifest.router && manifest.router.pages) {
|
|
473
|
+
const pages = manifest.router.pages;
|
|
474
|
+
const pageRoutes = Object.keys(pages);
|
|
475
|
+
sections.push(`\n### 已有页面列表(共 ${pageRoutes.length} 个)\n`);
|
|
476
|
+
sections.push(`| 路由路径 | 组件名 | 启动模式 |`);
|
|
477
|
+
sections.push(`|---------|--------|---------|`);
|
|
478
|
+
for (const route of pageRoutes) {
|
|
479
|
+
const cfg = pages[route];
|
|
480
|
+
sections.push(`| \`${route}\` | ${cfg.component || '-'} | ${cfg.launchMode || 'standard'} |`);
|
|
481
|
+
}
|
|
482
|
+
sections.push(`\n**入口页面**: \`${manifest.router.entry || '未定义'}\``);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 扫描页面目录,获取每个页面的文件列表
|
|
486
|
+
const baseDir = hasSrc ? srcDir : resolvedPath;
|
|
487
|
+
const pagesDir = path.join(baseDir, 'pages');
|
|
488
|
+
if (fs.existsSync(pagesDir)) {
|
|
489
|
+
try {
|
|
490
|
+
const pageFolders = fs.readdirSync(pagesDir).filter(f => {
|
|
491
|
+
return fs.statSync(path.join(pagesDir, f)).isDirectory();
|
|
492
|
+
});
|
|
493
|
+
if (pageFolders.length > 0) {
|
|
494
|
+
sections.push(`\n### 页面目录文件结构\n`);
|
|
495
|
+
for (const folder of pageFolders) {
|
|
496
|
+
const folderPath = path.join(pagesDir, folder);
|
|
497
|
+
const files = fs.readdirSync(folderPath).filter(f => {
|
|
498
|
+
return fs.statSync(path.join(folderPath, f)).isFile();
|
|
499
|
+
});
|
|
500
|
+
sections.push(`- \`pages/${folder}/\`: ${files.map(f => `\`${f}\``).join(', ')}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} catch (err) {
|
|
504
|
+
warnings.push(`扫描页面目录失败: ${err.message}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 扫描组件目录
|
|
509
|
+
const componentsDir = path.join(baseDir, 'components');
|
|
510
|
+
if (fs.existsSync(componentsDir)) {
|
|
511
|
+
try {
|
|
512
|
+
const compFolders = fs.readdirSync(componentsDir).filter(f => {
|
|
513
|
+
return fs.statSync(path.join(componentsDir, f)).isDirectory();
|
|
514
|
+
});
|
|
515
|
+
if (compFolders.length > 0) {
|
|
516
|
+
sections.push(`\n### 已有自定义组件(共 ${compFolders.length} 个)\n`);
|
|
517
|
+
for (const folder of compFolders) {
|
|
518
|
+
const folderPath = path.join(componentsDir, folder);
|
|
519
|
+
const files = fs.readdirSync(folderPath).filter(f => {
|
|
520
|
+
return fs.statSync(path.join(folderPath, f)).isFile();
|
|
521
|
+
});
|
|
522
|
+
sections.push(`- \`components/${folder}/\`: ${files.map(f => `\`${f}\``).join(', ')}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
warnings.push(`扫描组件目录失败: ${err.message}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 扫描 common 目录
|
|
531
|
+
const commonDir = path.join(baseDir, 'common');
|
|
532
|
+
if (fs.existsSync(commonDir)) {
|
|
533
|
+
try {
|
|
534
|
+
const commonFiles = fs.readdirSync(commonDir).filter(f => {
|
|
535
|
+
return fs.statSync(path.join(commonDir, f)).isFile();
|
|
536
|
+
});
|
|
537
|
+
if (commonFiles.length > 0) {
|
|
538
|
+
sections.push(`\n### 公共资源 (common/)\n`);
|
|
539
|
+
sections.push(commonFiles.map(f => `- \`${f}\``).join('\n'));
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
warnings.push(`扫描 common 目录失败: ${err.message}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 扫描 i18n 目录
|
|
547
|
+
const i18nDir = path.join(baseDir, 'i18n');
|
|
548
|
+
if (fs.existsSync(i18nDir)) {
|
|
549
|
+
try {
|
|
550
|
+
const i18nFiles = fs.readdirSync(i18nDir).filter(f => f.endsWith('.json'));
|
|
551
|
+
if (i18nFiles.length > 0) {
|
|
552
|
+
sections.push(`\n### 国际化资源 (i18n/)\n`);
|
|
553
|
+
sections.push(i18nFiles.map(f => `- \`${f}\``).join('\n'));
|
|
554
|
+
}
|
|
555
|
+
} catch (err) {
|
|
556
|
+
warnings.push(`扫描 i18n 目录失败: ${err.message}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 检查 app.ux / app.js
|
|
561
|
+
for (const appFile of ['app.ux', 'app.js']) {
|
|
562
|
+
const appPath = path.join(baseDir, appFile);
|
|
563
|
+
if (fs.existsSync(appPath)) {
|
|
564
|
+
sections.push(`\n**应用入口文件**: \`${appFile}\``);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// 检查 config 相关
|
|
570
|
+
if (manifest.config) {
|
|
571
|
+
sections.push(`\n### 应用配置 (config)\n`);
|
|
572
|
+
sections.push('```json\n' + JSON.stringify(manifest.config, null, 2) + '\n```');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 检查 package.json
|
|
576
|
+
const pkgPath = path.join(resolvedPath, 'package.json');
|
|
577
|
+
if (fs.existsSync(pkgPath)) {
|
|
578
|
+
try {
|
|
579
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
580
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
581
|
+
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
582
|
+
if (deps.length > 0 || devDeps.length > 0) {
|
|
583
|
+
sections.push(`\n### 项目依赖\n`);
|
|
584
|
+
if (deps.length > 0) sections.push(`**dependencies**: ${deps.map(d => `\`${d}\``).join(', ')}`);
|
|
585
|
+
if (devDeps.length > 0) sections.push(`**devDependencies**: ${devDeps.map(d => `\`${d}\``).join(', ')}`);
|
|
586
|
+
}
|
|
587
|
+
} catch (err) {
|
|
588
|
+
warnings.push(`package.json 解析失败: ${err.message}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return { data: sections.join('\n'), isExistingProject: true, warnings };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* 加载阶段上下文
|
|
597
|
+
*
|
|
598
|
+
* 为指定阶段组装完整的执行上下文,包括:
|
|
599
|
+
* 1. 知识库文件内容(根据 stage_knowledge 映射)
|
|
600
|
+
* 2. 前置阶段产出物
|
|
601
|
+
* 3. Figma 设计稿数据
|
|
602
|
+
* 4. Session 信息
|
|
603
|
+
* 5. 项目工程现状(S2/S3 阶段,若指定了 project_path)
|
|
604
|
+
*
|
|
605
|
+
* @param {string} stageId - 阶段 ID("S1" | "S2" | "S3")
|
|
606
|
+
* @param {object} session - Session 数据对象
|
|
607
|
+
* @returns {{ success: boolean, context?: object, error?: string, warnings?: string[] }}
|
|
608
|
+
*/
|
|
609
|
+
function loadStageContext(stageId, session) {
|
|
610
|
+
// 参数校验
|
|
611
|
+
if (!VALID_STAGE_IDS.includes(stageId)) {
|
|
612
|
+
return { success: false, error: `无效的阶段 ID: ${stageId},应为 S1/S2/S3` };
|
|
613
|
+
}
|
|
614
|
+
if (!session || !session.session_id) {
|
|
615
|
+
return { success: false, error: 'Session 数据无效' };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const allWarnings = [];
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
// 1. 加载资源路径配置
|
|
622
|
+
const resourcePaths = loadResourcePaths();
|
|
623
|
+
|
|
624
|
+
// 2. 加载知识(SKILL.md 优先 + 按需加载知识库)
|
|
625
|
+
const stageKnowledge = resourcePaths.stage_knowledge[stageId] || [];
|
|
626
|
+
const knowledgeResults = [];
|
|
627
|
+
let skillContent = null;
|
|
628
|
+
|
|
629
|
+
// 2a. 加载 SKILL.md(核心知识源)
|
|
630
|
+
const skillFilePath = resourcePaths.paths.skill_file
|
|
631
|
+
? path.resolve(PROJECT_ROOT, resourcePaths.paths.skill_file)
|
|
632
|
+
: null;
|
|
633
|
+
if (skillFilePath && fs.existsSync(skillFilePath)) {
|
|
634
|
+
try {
|
|
635
|
+
skillContent = readFileWithCache(skillFilePath);
|
|
636
|
+
knowledgeResults.push({
|
|
637
|
+
key: 'skill',
|
|
638
|
+
files: [{ name: 'SKILL.md', content: skillContent }],
|
|
639
|
+
indexContent: null,
|
|
640
|
+
warnings: []
|
|
641
|
+
});
|
|
642
|
+
} catch (err) {
|
|
643
|
+
allWarnings.push(`读取 SKILL.md 失败: ${err.message}`);
|
|
644
|
+
}
|
|
645
|
+
} else if (skillFilePath) {
|
|
646
|
+
allWarnings.push(`SKILL.md 文件不存在: ${resourcePaths.paths.skill_file},回退到传统知识库加载`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 2b. 加载额外的知识库(如 examples,仅在 stage_knowledge 中显式配置的非 skill 项)
|
|
650
|
+
for (const knowledgeKey of stageKnowledge) {
|
|
651
|
+
if (knowledgeKey === 'skill') continue; // 已在 2a 中加载
|
|
652
|
+
const result = loadKnowledgeFiles(knowledgeKey, resourcePaths.knowledge_mappings);
|
|
653
|
+
knowledgeResults.push(result);
|
|
654
|
+
allWarnings.push(...result.warnings);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 格式化知识库内容
|
|
658
|
+
const knowledgeContent = formatKnowledgeContent(knowledgeResults);
|
|
659
|
+
|
|
660
|
+
// 3. 加载前置阶段产出物
|
|
661
|
+
const prereqResult = loadPrerequisiteOutputs(stageId, session);
|
|
662
|
+
allWarnings.push(...prereqResult.warnings);
|
|
663
|
+
const previousOutputs = formatPreviousOutputs(prereqResult.outputs);
|
|
664
|
+
|
|
665
|
+
// 4. 加载 Figma 数据
|
|
666
|
+
const figmaResult = loadFigmaData(session);
|
|
667
|
+
allWarnings.push(...figmaResult.warnings);
|
|
668
|
+
|
|
669
|
+
// 5. 扫描项目工程现状(S2/S3 阶段,若指定了 project_path)
|
|
670
|
+
let projectAnalysis = null;
|
|
671
|
+
let isExistingProject = false;
|
|
672
|
+
let isEmptyProject = false;
|
|
673
|
+
if ((stageId === 'S2' || stageId === 'S3') && session.inputs && session.inputs.project_path) {
|
|
674
|
+
const scanResult = scanProjectStructure(session);
|
|
675
|
+
projectAnalysis = scanResult.data;
|
|
676
|
+
isExistingProject = scanResult.isExistingProject;
|
|
677
|
+
isEmptyProject = scanResult.isEmptyProject || false;
|
|
678
|
+
allWarnings.push(...scanResult.warnings);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 6. 组装上下文对象
|
|
682
|
+
const context = {
|
|
683
|
+
stageId,
|
|
684
|
+
session,
|
|
685
|
+
knowledgeContent,
|
|
686
|
+
knowledgeResults,
|
|
687
|
+
previousOutputs,
|
|
688
|
+
prerequisiteOutputs: prereqResult.outputs,
|
|
689
|
+
figmaData: figmaResult.data,
|
|
690
|
+
projectAnalysis: projectAnalysis || '未指定项目工程路径(全新项目)',
|
|
691
|
+
isExistingProject,
|
|
692
|
+
isEmptyProject,
|
|
693
|
+
stageKnowledgeKeys: stageKnowledge
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
return { success: true, context, warnings: allWarnings };
|
|
697
|
+
} catch (err) {
|
|
698
|
+
return { success: false, error: `加载阶段上下文失败: ${err.message}` };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* 格式化知识库内容为可注入的字符串
|
|
704
|
+
*
|
|
705
|
+
* @param {Array} knowledgeResults - loadKnowledgeFiles 的结果数组
|
|
706
|
+
* @returns {string} 格式化后的知识库内容
|
|
707
|
+
*/
|
|
708
|
+
function formatKnowledgeContent(knowledgeResults) {
|
|
709
|
+
if (!knowledgeResults || knowledgeResults.length === 0) {
|
|
710
|
+
return '无知识库内容';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const sections = [];
|
|
714
|
+
for (const result of knowledgeResults) {
|
|
715
|
+
if (result.files.length === 0) continue;
|
|
716
|
+
|
|
717
|
+
const fileContents = result.files
|
|
718
|
+
.map(f => `#### ${f.name}\n${f.content}`)
|
|
719
|
+
.join('\n\n');
|
|
720
|
+
sections.push(`### ${result.key}\n\n${fileContents}`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return sections.length > 0 ? sections.join('\n\n---\n\n') : '无知识库内容';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 格式化前置产出物为可注入的字符串
|
|
728
|
+
*
|
|
729
|
+
* @param {Array} outputs - loadPrerequisiteOutputs 的结果数组
|
|
730
|
+
* @returns {string} 格式化后的前置产出物内容
|
|
731
|
+
*/
|
|
732
|
+
function formatPreviousOutputs(outputs) {
|
|
733
|
+
if (!outputs || outputs.length === 0) {
|
|
734
|
+
return '无前置产出';
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return outputs
|
|
738
|
+
.map(o => `### ${o.file}(来自阶段 ${o.stage})\n\n${o.content}`)
|
|
739
|
+
.join('\n\n---\n\n');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* 注入上下文到 Agent 提示词模板
|
|
745
|
+
*
|
|
746
|
+
* 替换提示词模板中的所有占位符:
|
|
747
|
+
* - {session.requirement_name} → 需求名称
|
|
748
|
+
* - {knowledge_content} → 知识库内容
|
|
749
|
+
* - {previous_outputs} → 前置产出物
|
|
750
|
+
* - {figma_data} → Figma 设计稿数据
|
|
751
|
+
*
|
|
752
|
+
* 同时支持 session 对象中的其他字段占位符(如 {session.session_id})。
|
|
753
|
+
*
|
|
754
|
+
* @param {string} agentPrompt - Agent 提示词模板字符串
|
|
755
|
+
* @param {object} context - loadStageContext 返回的上下文对象
|
|
756
|
+
* @returns {string} 替换占位符后的提示词字符串
|
|
757
|
+
*/
|
|
758
|
+
function injectContext(agentPrompt, context) {
|
|
759
|
+
if (!agentPrompt || typeof agentPrompt !== 'string') {
|
|
760
|
+
return agentPrompt || '';
|
|
761
|
+
}
|
|
762
|
+
if (!context) {
|
|
763
|
+
return agentPrompt;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
let result = agentPrompt;
|
|
767
|
+
|
|
768
|
+
// 替换 session 相关占位符
|
|
769
|
+
if (context.session) {
|
|
770
|
+
// 替换 {session.xxx} 格式的占位符
|
|
771
|
+
result = result.replace(/\{session\.(\w+)\}/g, (match, field) => {
|
|
772
|
+
const value = context.session[field];
|
|
773
|
+
if (value !== undefined && value !== null) {
|
|
774
|
+
return String(value);
|
|
775
|
+
}
|
|
776
|
+
return match;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 替换知识库内容占位符
|
|
781
|
+
if (context.knowledgeContent !== undefined) {
|
|
782
|
+
result = result.replace(/\{knowledge_content\}/g, context.knowledgeContent);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 替换前置产出物占位符
|
|
786
|
+
if (context.previousOutputs !== undefined) {
|
|
787
|
+
result = result.replace(/\{previous_outputs\}/g, context.previousOutputs);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// 替换 Figma 数据占位符
|
|
791
|
+
if (context.figmaData !== undefined) {
|
|
792
|
+
result = result.replace(/\{figma_data\}/g, context.figmaData);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 替换屏幕适配规格占位符
|
|
796
|
+
if (context.session && context.session.inputs && context.session.inputs.screen_spec) {
|
|
797
|
+
const spec = context.session.inputs.screen_spec;
|
|
798
|
+
const shapeMap = { round: '圆屏', oval: '跑道屏', square: '方屏' };
|
|
799
|
+
const shapeLabel = shapeMap[spec.shape] || spec.shape || '未指定';
|
|
800
|
+
const screenSpecStr = `${spec.width || '?'}×${spec.height || '?'} 像素,屏幕形状: ${shapeLabel}`;
|
|
801
|
+
result = result.replace(/\{screen_spec\}/g, screenSpecStr);
|
|
802
|
+
} else {
|
|
803
|
+
result = result.replace(/\{screen_spec\}/g, '未指定屏幕适配规格');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// 替换项目工程现状占位符
|
|
807
|
+
if (context.projectAnalysis !== undefined) {
|
|
808
|
+
result = result.replace(/\{project_analysis\}/g, context.projectAnalysis);
|
|
809
|
+
} else {
|
|
810
|
+
result = result.replace(/\{project_analysis\}/g, '未指定项目工程路径(全新项目)');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Fallback: 将所有未替换的占位符统一替换为 [未提供],避免 Agent 看到原始占位符
|
|
814
|
+
result = result.replace(/\{(?:session\.\w+|knowledge_content|previous_outputs|figma_data|screen_spec|project_analysis)\}/g, '[未提供]');
|
|
815
|
+
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// 导出所有函数
|
|
820
|
+
module.exports = {
|
|
821
|
+
loadStageContext,
|
|
822
|
+
injectContext,
|
|
823
|
+
// 导出辅助函数,便于测试
|
|
824
|
+
loadResourcePaths,
|
|
825
|
+
loadWorkflowConfig,
|
|
826
|
+
loadKnowledgeFiles,
|
|
827
|
+
parseIndexFileList,
|
|
828
|
+
loadPrerequisiteOutputs,
|
|
829
|
+
loadFigmaData,
|
|
830
|
+
scanProjectStructure,
|
|
831
|
+
findStageByOutput,
|
|
832
|
+
formatKnowledgeContent,
|
|
833
|
+
formatPreviousOutputs,
|
|
834
|
+
readFileWithCache,
|
|
835
|
+
_fileCache,
|
|
836
|
+
PROJECT_ROOT,
|
|
837
|
+
RESOURCE_PATHS_FILE,
|
|
838
|
+
WORKFLOW_CONFIG_FILE,
|
|
839
|
+
SESSIONS_DIR,
|
|
840
|
+
VALID_STAGE_IDS
|
|
841
|
+
};
|