const http = require("http"); const https = require("https"); const SYSTEM_PROMPT = `你是一个场景信息提取助手。根据场景目录的内容,提取 scene-id 和 scene-name。 scene-id 规则: - 使用英文短横线连接,如 tq-lineloss-report - 全小写,有业务含义 scene-name 规则: - 使用中文,简短描述性名称 - 如 "台区线损报表"、"知乎热榜导出" 请以 JSON 格式返回:{"sceneId": "...", "sceneName": "..."}`; const DEEP_SYSTEM_PROMPT = `你是一个场景代码分析专家。分析场景源码,提取关键业务信息。 ## 分析目标 1. **多模式识别** (关键): - 查找条件分支逻辑 (if/switch) 中基于 period_mode、reportType 等字段的分支 - 识别不同分支对应的 API 端点、列定义、请求格式 - 如果发现多模式,使用 modes 数组格式输出 2. **API 端点**: 识别所有 HTTP 请求地址 (URL, method, contentType, 用途) - 从 \$.ajax/fetch 调用中提取 contentType - 检测请求格式: application/json 或 application/x-www-form-urlencoded 3. **请求模板**: 识别请求参数结构 - 提取硬编码的分页参数 (rows, page, sidx, sord) - 识别模板变量如 \${args.org_code} 4. **数据归一化**: 识别数据处理规则 - 查找数据渲染/表格填充逻辑 - 检测数据验证条件 (哪些字段不能为空) 5. **响应路径**: 识别数据在响应中的位置 - 如 response.content 或 response.data ## 输出格式 ### 单模式场景 (无 modes 数组): { "sceneId": "string", "sceneName": "string", "sceneKind": "report_collection | monitoring", "expectedDomain": "string", "targetUrl": "string", "apiEndpoints": [{"name": "", "url": "", "method": "POST"}], "staticParams": {"key": "value"}, "columnDefs": [["fieldName", "中文列名"]] } ### 多模式场景 (有 modes 数组): { "sceneId": "tq-lineloss-report", "sceneName": "台区线损报表", "sceneKind": "report_collection", "modes": [ { "name": "month", "label": "月度报表", "condition": {"field": "period_mode", "operator": "equals", "value": "month"}, "apiEndpoint": { "name": "月度线损查询", "url": "http://...", "method": "POST", "contentType": "application/x-www-form-urlencoded" }, "columnDefs": [["ORG_NAME", "供电单位"], ...], "requestTemplate": {"orgno": "\${args.org_code}", "rows": 1000, "page": 1}, "normalizeRules": {"type": "validate_all_columns", "filterNull": true}, "responsePath": "content" }, { "name": "week", "label": "周报表", "condition": {"field": "period_mode", "operator": "equals", "value": "week"}, "apiEndpoint": {...}, "columnDefs": [...], ... } ], "defaultMode": "month", "modeSwitchField": "period_mode" } **重要**: 如果发现代码中有基于 period_mode 的 if/switch 分支,必须使用多模式格式输出!`; function buildAnalyzePrompt(sourceDir, dirContents) { const parts = []; parts.push(`=== 目录结构 ===`); parts.push(dirContents.tree || "(empty)"); if (dirContents["scene.toml"]) { parts.push(`\n=== scene.toml ===`); parts.push(dirContents["scene.toml"]); } if (dirContents["SKILL.toml"]) { parts.push(`\n=== SKILL.toml ===`); parts.push(dirContents["SKILL.toml"]); } if (dirContents["SKILL.md"]) { parts.push(`\n=== SKILL.md ===`); parts.push(dirContents["SKILL.md"]); } if (dirContents.scripts && Object.keys(dirContents.scripts).length > 0) { parts.push(`\n=== 脚本文件 ===`); for (const [name, content] of Object.entries(dirContents.scripts)) { parts.push(`\n--- ${name} ---`); const contentStr = typeof content === 'string' ? content : String(content || ''); parts.push(contentStr.substring(0, 2000)); } } return `以下是场景目录 "${sourceDir}" 的内容:\n\n${parts.join("\n")}\n\n请以 JSON 格式返回:{"sceneId": "...", "sceneName": "..."}`; } function buildDeepAnalyzePrompt(sourceDir, dirContents, indexHtmlContent) { const parts = []; parts.push(`=== 目录结构 ===`); parts.push(dirContents.tree || "(empty)"); if (dirContents["scene.toml"]) { parts.push(`\n=== scene.toml ===`); parts.push(dirContents["scene.toml"]); } if (dirContents["SKILL.toml"]) { parts.push(`\n=== SKILL.toml ===`); parts.push(dirContents["SKILL.toml"]); } if (dirContents["SKILL.md"]) { parts.push(`\n=== SKILL.md ===`); parts.push(dirContents["SKILL.md"]); } // Include index.html content (key addition) if (indexHtmlContent && typeof indexHtmlContent === 'string') { parts.push(`\n=== index.html ===`); // Limit to first 15000 chars to avoid token limits parts.push(indexHtmlContent.substring(0, 15000)); } if (dirContents.scripts && Object.keys(dirContents.scripts).length > 0) { parts.push(`\n=== 脚本文件 ===`); for (const [name, content] of Object.entries(dirContents.scripts)) { parts.push(`\n--- ${name} ---`); const contentStr = typeof content === 'string' ? content : String(content || ''); parts.push(contentStr.substring(0, 3000)); } } return `以下是场景目录 "${sourceDir}" 的内容:\n\n${parts.join("\n")}\n\n请分析以上代码,提取完整的场景信息。`; } function extractJsonFromResponse(text) { const codeBlockMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/); if (codeBlockMatch) return JSON.parse(codeBlockMatch[1]); const jsonMatch = text.match( /\{[\s\S]*"sceneId"[\s\S]*"sceneName"[\s\S]*\}/ ); if (jsonMatch) return JSON.parse(jsonMatch[0]); return JSON.parse(text); } function extractSceneInfo(text) { // Try code block first const codeBlockMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/); if (codeBlockMatch) { try { return JSON.parse(codeBlockMatch[1]); } catch (e) { // fall through } } // Try to find JSON object with sceneId const jsonMatch = text.match(/\{[\s\S]*"sceneId"[\s\S]*\}/); if (jsonMatch) { try { return JSON.parse(jsonMatch[0]); } catch (e) { // fall through } } // Last resort: parse entire text try { return JSON.parse(text); } catch (e) { throw new Error("Failed to extract valid SceneInfo JSON from LLM response"); } } function analyzeScene(sourceDir, dirContents, { apiKey, baseUrl, model }) { const userPrompt = buildAnalyzePrompt(sourceDir, dirContents); const requestBody = JSON.stringify({ model, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: userPrompt }, ], temperature: 0.1, max_tokens: 256, }); return new Promise((resolve, reject) => { const url = new URL(baseUrl.replace(/\/v1\/?$/, "") + "/v1/chat/completions"); const options = { hostname: url.hostname, port: url.port || (url.protocol === "https:" ? 443 : 80), path: url.pathname, method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, "Content-Length": Buffer.byteLength(requestBody), }, }; const req = https.request(options, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { if (res.statusCode !== 200) { return reject(new Error(`LLM API error ${res.statusCode}: ${data}`)); } try { const parsed = JSON.parse(data); const content = parsed.choices?.[0]?.message?.content; if (!content) return reject(new Error("LLM returned empty response")); const result = extractJsonFromResponse(content); if (!result.sceneId || !result.sceneName) { return reject( new Error(`LLM response missing sceneId/sceneName: ${content}`) ); } resolve(result); } catch (err) { reject(new Error(`Failed to parse LLM response: ${err.message}`)); } }); }); req.on("error", reject); req.setTimeout(30000, () => { req.destroy(new Error("LLM API request timed out")); }); req.write(requestBody); req.end(); }); } function analyzeSceneDeep(sourceDir, dirContents, indexHtmlContent, { apiKey, baseUrl, model }) { const userPrompt = buildDeepAnalyzePrompt(sourceDir, dirContents, indexHtmlContent); const requestBody = JSON.stringify({ model, messages: [ { role: "system", content: DEEP_SYSTEM_PROMPT }, { role: "user", content: userPrompt }, ], temperature: 0.1, max_tokens: 2048, // Increased for detailed response }); return new Promise((resolve, reject) => { const url = new URL(baseUrl.replace(/\/v1\/?$/, "") + "/v1/chat/completions"); const options = { hostname: url.hostname, port: url.port || (url.protocol === "https:" ? 443 : 80), path: url.pathname, method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, "Content-Length": Buffer.byteLength(requestBody), }, }; const httpModule = url.protocol === "https:" ? https : http; const req = httpModule.request(options, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { if (res.statusCode !== 200) { return reject(new Error(`LLM API error ${res.statusCode}: ${data}`)); } try { const parsed = JSON.parse(data); const content = parsed.choices?.[0]?.message?.content; if (!content) return reject(new Error("LLM returned empty response")); const result = extractSceneInfo(content); // Validate required fields if (!result.sceneId || !result.sceneName) { return reject(new Error(`LLM response missing sceneId/sceneName: ${content}`)); } // Set defaults for optional fields result.sceneKind = result.sceneKind || "report_collection"; result.apiEndpoints = result.apiEndpoints || []; result.staticParams = result.staticParams || {}; result.columnDefs = result.columnDefs || []; result.businessLogic = result.businessLogic || {}; result.modes = result.modes || []; result.defaultMode = result.defaultMode || (result.modes.length > 0 ? result.modes[0].name : null); result.modeSwitchField = result.modeSwitchField || "period_mode"; resolve(result); } catch (err) { reject(new Error(`Failed to parse LLM response: ${err.message}`)); } }); }); req.on("error", reject); req.setTimeout(60000, () => { req.destroy(new Error("LLM API request timed out")); }); req.write(requestBody); req.end(); }); } module.exports = { buildAnalyzePrompt, extractJsonFromResponse, analyzeScene, // New exports buildDeepAnalyzePrompt, extractSceneInfo, analyzeSceneDeep, };