diff --git a/.node-red-data/projects/zsy/flows.json b/.node-red-data/projects/zsy/flows.json index d4d2d31..6c3ae11 100644 --- a/.node-red-data/projects/zsy/flows.json +++ b/.node-red-data/projects/zsy/flows.json @@ -2373,7 +2373,7 @@ "type": "function", "z": "78d15f59dee4b6d8", "name": "准备llm输入", - "func": "'use strict';\n\n/**\n * Node-RED Function 节点脚本:准备大模型 HTTP 请求。\n * 将生成 prompt、HTTP 请求参数,并把上下文写入 msg.llmContext。\n */\n\nconst DEFAULT_API_KEY = 'sk-lbGrsUPL1iby86h554FaE536C343435dAa9bA65967A840B2';\nconst DEFAULT_BASE_URL = 'https://aiproxy.petrotech.cnpc/v1';\nconst DEFAULT_ENDPOINT_PATH = '/chat/completions';\nconst DEFAULT_MODEL = 'deepseek-v3';\n\nfunction prepareLlmRequest(msg, node) {\n if (!msg.operationId) {\n msg.operationId = 'createResource';\n }\n\n try {\n const apiDoc = extractOpenApiDocument(msg);\n const operationCtx = resolveOperationContext(msg, apiDoc);\n\n if (!operationCtx.operation) {\n const err = `未找到匹配的接口定义(operationId=${operationCtx.requestedOperationId || '未指定'}, path=${operationCtx.requestedPath || '未指定'}, method=${operationCtx.requestedMethod || '未指定'})`;\n node.error(err, msg);\n msg.error = err;\n return null;\n }\n\n const prompt = buildPrompt(operationCtx, apiDoc, msg);\n const requestPayload = buildRequestPayload(prompt, msg);\n\n const apiKey = (msg.llm && msg.llm.apiKey) || DEFAULT_API_KEY;\n const baseUrl = (msg.llm && msg.llm.baseUrl) || DEFAULT_BASE_URL;\n const endpointPath = (msg.llm && msg.llm.endpoint) || DEFAULT_ENDPOINT_PATH;\n\n const url = buildUrl(baseUrl, endpointPath);\n msg.method = 'POST';\n msg.url = url;\n msg.headers = Object.assign({}, msg.headers, {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiKey}`,\n });\n msg.payload = requestPayload;\n msg.rejectUnauthorized = false;\n\n msg.llmContext = {\n prompt,\n operationCtx,\n provider: 'dashscope',\n requestConfig: {\n baseUrl,\n endpointPath,\n model: requestPayload.model,\n temperature: requestPayload.temperature,\n },\n };\n delete msg.error;\n return msg;\n } catch (error) {\n node.error(`LLM 请求准备失败:${error.message}`, msg);\n msg.error = error.message;\n return null;\n }\n}\n\nfunction extractOpenApiDocument(message) {\n const candidate =\n message && message.oas_def ? message.oas_def :\n message && message.swagger ? message.swagger :\n message && message.payload ? message.payload :\n null;\n\n if (!candidate) {\n throw new Error('未提供 OpenAPI 文档(需要 msg.oas_def / msg.swagger / msg.payload)');\n }\n\n if (typeof candidate === 'string') {\n try {\n return JSON.parse(candidate);\n } catch (error) {\n throw new Error(`OpenAPI JSON 解析失败:${error.message}`);\n }\n }\n\n if (typeof candidate !== 'object') {\n throw new Error('OpenAPI 文档必须是对象或 JSON 字符串');\n }\n\n if (!candidate.paths || typeof candidate.paths !== 'object') {\n throw new Error('OpenAPI 文档缺少 paths 字段');\n }\n\n return candidate;\n}\n\nfunction resolveOperationContext(message, apiDoc) {\n const requestedOperationId =\n (message && message.operationId) ||\n (message && message.req && message.req.body && message.req.body.operationId) ||\n null;\n\n const requestedPath =\n (message && message.path) ||\n (message && message.req && message.req.body && message.req.body.path) ||\n null;\n\n const requestedMethodRaw =\n (message && message.method) ||\n (message && message.req && message.req.body && message.req.body.method) ||\n null;\n const requestedMethod = requestedMethodRaw ? String(requestedMethodRaw).toLowerCase() : null;\n\n const operations = enumerateOperations(apiDoc);\n\n let matched = null;\n if (requestedOperationId) {\n matched = operations.find(op => op.operation.operationId === requestedOperationId);\n }\n\n if (!matched && requestedPath && requestedMethod) {\n matched = operations.find(op => op.path === requestedPath && op.method === requestedMethod);\n }\n\n let autoSelected = false;\n if (!matched && operations.length > 0) {\n matched = operations[0];\n autoSelected = true;\n }\n\n return {\n operation: matched ? matched.operation : undefined,\n method: matched ? matched.method : undefined,\n pathItem: matched ? matched.pathItem : undefined,\n path: matched ? matched.path : undefined,\n autoSelected,\n candidates: operations.map(op => ({\n operationId: op.operation && op.operation.operationId ? op.operation.operationId : '',\n method: op.method ? op.method.toUpperCase() : '',\n path: op.path,\n summary: op.operation && op.operation.summary ? op.operation.summary : '',\n })),\n requestedOperationId,\n requestedPath,\n requestedMethod,\n };\n}\n\nfunction enumerateOperations(apiDoc) {\n const results = [];\n const validMethods = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'];\n for (const path of Object.keys(apiDoc.paths || {})) {\n const pathItem = apiDoc.paths[path];\n if (!pathItem || typeof pathItem !== 'object') {\n continue;\n }\n for (const method of validMethods) {\n if (pathItem[method] && typeof pathItem[method] === 'object') {\n results.push({\n path,\n method,\n operation: pathItem[method],\n pathItem,\n });\n }\n }\n }\n return results;\n}\n\nfunction buildPrompt(operationCtx, apiDoc, message) {\n const operation = operationCtx.operation;\n const method = (operationCtx.method || '').toUpperCase();\n const path = operationCtx.path || '';\n const summary = operation.summary || '';\n const description = operation.description || '';\n\n const parameters = gatherParameters(operationCtx, apiDoc);\n const requestBodySchema = gatherRequestBodySchema(operation, apiDoc);\n\n const userNotes = message && message.prompt ? String(message.prompt) : '';\n const requiredFields = extractRequiredFields(requestBodySchema, apiDoc);\n const requiredFieldsText = requiredFields.length > 0\n ? requiredFields.map(field => `- ${field}`).join('\\n')\n : '';\n\n const lines = [];\n lines.push('你是一名负责生成 HTTP 接口测试参数的助手,请严格输出以下结构的 JSON:');\n lines.push('{');\n lines.push(' \"pathParams\": { ... },');\n lines.push(' \"query\": { ... },');\n lines.push(' \"headers\": { ... },');\n lines.push(' \"cookies\": { ... },');\n lines.push(' \"body\": { ... },');\n lines.push(' \"notes\": \"...\"');\n lines.push('}');\n lines.push('未用到的分区请返回空对象,不要在 JSON 外输出任何文字。');\n lines.push('');\n lines.push('body.data[0] 必须遵守:');\n lines.push('- 覆盖 schema 中声明的全部必填字段,并给出符合字段类型/格式的真实感样例值。');\n lines.push('- 不得遗漏必填字段;若 schema 内有嵌套对象/数组的必填字段,同样要补齐。');\n lines.push('- 保持数值、日期、字符串等格式,只在 schema 无提示时使用 \"sample\"、\"2025-01-01\" 等占位值。');\n if (requiredFieldsText) {\n lines.push('必填字段清单(必须全部出现在 body.data[0] 中):');\n lines.push(requiredFieldsText);\n }\n lines.push('');\n lines.push('若 schema 中存在数组元素或复合结构的必填字段,也要为这些子字段提供值。');\n lines.push('');\n lines.push(`Target operation: ${method} ${path}`);\n\n if (summary) {\n lines.push(`Summary: ${summary}`);\n }\n if (description) {\n lines.push(`Description: ${description}`);\n }\n if (parameters.length > 0) {\n lines.push('Parameters:');\n for (const param of parameters) {\n lines.push(`- [${param.in}] ${param.name}: ${param.type || 'any'}${param.required ? ' (required)' : ''}${param.description ? ` - ${param.description}` : ''}`);\n }\n } else {\n lines.push('Parameters: none defined.');\n }\n\n if (requestBodySchema) {\n lines.push('Request body schema (JSON Schema excerpt):');\n lines.push(indentSnippet(JSON.stringify(requestBodySchema, null, 2), 2, 900));\n } else {\n lines.push('Request body: not defined.');\n }\n\n if (userNotes) {\n lines.push('');\n lines.push('User notes:');\n lines.push(userNotes);\n }\n\n return lines.join('\\n');\n}\n\nfunction gatherParameters(operationCtx, apiDoc) {\n const aggregated = [];\n const seen = new Set();\n\n const sources = [];\n if (Array.isArray(operationCtx.pathItem && operationCtx.pathItem.parameters)) {\n sources.push(operationCtx.pathItem.parameters);\n }\n if (Array.isArray(operationCtx.operation.parameters)) {\n sources.push(operationCtx.operation.parameters);\n }\n\n for (const list of sources) {\n for (const param of list) {\n if (!param || typeof param !== 'object') {\n continue;\n }\n const resolved = resolveMaybeRef(param, apiDoc);\n const key = `${resolved.in}:${resolved.name}`;\n if (!resolved.name || seen.has(key)) {\n continue;\n }\n seen.add(key);\n aggregated.push({\n name: resolved.name,\n in: resolved.in || 'query',\n required: !!resolved.required,\n description: resolved.description || '',\n type: resolved.schema ? inferFriendlyType(resolved.schema, apiDoc) : '',\n });\n }\n }\n\n return aggregated;\n}\n\nfunction gatherRequestBodySchema(operation, apiDoc) {\n const requestBody = resolveMaybeRef(operation.requestBody, apiDoc);\n if (!requestBody || typeof requestBody !== 'object' || !requestBody.content) {\n return null;\n }\n\n const content = requestBody.content;\n const mediaType = Object.keys(content).find(key => key.includes('json')) || Object.keys(content)[0];\n if (!mediaType) {\n return null;\n }\n\n const mediaObject = resolveMaybeRef(content[mediaType], apiDoc);\n if (!mediaObject || typeof mediaObject !== 'object' || !mediaObject.schema) {\n return null;\n }\n\n const schema = resolveMaybeRef(mediaObject.schema, apiDoc);\n return resolveSchemaDeep(schema, apiDoc);\n}\n\nfunction inferFriendlyType(schema, apiDoc) {\n if (!schema) {\n return '';\n }\n const resolved = resolveMaybeRef(schema, apiDoc);\n if (!resolved || typeof resolved !== 'object') {\n return '';\n }\n\n if (resolved.type) {\n if (resolved.type === 'array' && resolved.items) {\n const itemType = inferFriendlyType(resolved.items, apiDoc) || 'any';\n return `[${itemType}]`;\n }\n return resolved.type;\n }\n\n if (resolved.enum && Array.isArray(resolved.enum)) {\n return `enum(${resolved.enum.slice(0, 3).join(', ')}${resolved.enum.length > 3 ? ', …' : ''})`;\n }\n\n if (resolved.properties) {\n return 'object';\n }\n\n return '';\n}\n\nfunction buildRequestPayload(prompt, message) {\n const systemPrompt = (message.llm && message.llm.systemPrompt) ||\n 'You write JSON only. Focus on realistic values for testing HTTP APIs.';\n\n const model = (message.llm && message.llm.model) || DEFAULT_MODEL;\n const temperature = (message.llm && typeof message.llm.temperature === 'number')\n ? message.llm.temperature : 0.2;\n\n return {\n model,\n temperature,\n response_format: { type: 'json_object' },\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: prompt },\n ],\n };\n}\n\nfunction buildUrl(baseUrl, endpointPath) {\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;\n const path = endpointPath.startsWith('/') ? endpointPath.slice(1) : endpointPath;\n return `${normalizedBase}${path}`;\n}\n\nfunction resolveMaybeRef(node, apiDoc) {\n if (!node || typeof node !== 'object') {\n return node;\n }\n if (!node.$ref) {\n return node;\n }\n\n const resolved = resolveRef(node.$ref, apiDoc);\n if (!resolved || typeof resolved !== 'object') {\n return node;\n }\n\n const remainder = Object.assign({}, node);\n delete remainder.$ref;\n return Object.assign({}, clone(resolved), remainder);\n}\n\nfunction resolveRef(ref, apiDoc) {\n if (typeof ref !== 'string' || !ref.startsWith('#/')) {\n return null;\n }\n\n const tokens = ref.slice(2).split('/').map(unescapeRefToken);\n let current = apiDoc;\n for (const token of tokens) {\n if (current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, token)) {\n current = current[token];\n } else {\n return null;\n }\n }\n return current;\n}\n\nfunction unescapeRefToken(token) {\n return token.replace(/~1/g, '/').replace(/~0/g, '~');\n}\n\nfunction indentSnippet(text, indentLevel, maxLength) {\n const trimmed = maxLength && text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;\n const indent = ' '.repeat(indentLevel * 2);\n return trimmed.split('\\n').map(line => `${indent}${line}`).join('\\n');\n}\n\nfunction clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n\nfunction extractRequiredFields(schema, apiDoc) {\n const result = new Set();\n collectRequiredFields(schema, apiDoc, result, '');\n return Array.from(result);\n}\n\nfunction collectRequiredFields(schema, apiDoc, result, pathPrefix) {\n if (!schema || typeof schema !== 'object') {\n return;\n }\n\n if (schema.$ref) {\n const resolved = resolveRef(schema.$ref, apiDoc);\n if (!resolved) {\n return;\n }\n collectRequiredFields(resolveSchemaDeep(resolved, apiDoc), apiDoc, result, pathPrefix);\n return;\n }\n\n if (Array.isArray(schema.required) && schema.properties && typeof schema.properties === 'object') {\n for (const key of schema.required) {\n const nextPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n result.add(nextPath);\n collectRequiredFields(schema.properties[key], apiDoc, result, nextPath);\n }\n }\n\n if (schema.type === 'array' && schema.items) {\n const nextPrefix = pathPrefix ? `${pathPrefix}[]` : '[]';\n collectRequiredFields(schema.items, apiDoc, result, nextPrefix);\n }\n\n if (schema.allOf && Array.isArray(schema.allOf)) {\n for (const part of schema.allOf) {\n collectRequiredFields(part, apiDoc, result, pathPrefix);\n }\n }\n}\n\nfunction resolveSchemaDeep(schema, apiDoc, seen = new Set()) {\n if (!schema || typeof schema !== 'object') {\n return schema;\n }\n\n if (schema.$ref) {\n const ref = schema.$ref;\n if (seen.has(ref)) {\n return {};\n }\n seen.add(ref);\n const resolved = resolveRef(ref, apiDoc);\n if (!resolved) {\n return schema;\n }\n const merged = Object.assign({}, clone(resolved), clone(schema));\n delete merged.$ref;\n return resolveSchemaDeep(merged, apiDoc, seen);\n }\n\n const cloned = clone(schema);\n\n if (cloned.properties && typeof cloned.properties === 'object') {\n for (const key of Object.keys(cloned.properties)) {\n cloned.properties[key] = resolveSchemaDeep(cloned.properties[key], apiDoc, new Set(seen));\n }\n }\n\n if (cloned.items) {\n cloned.items = resolveSchemaDeep(cloned.items, apiDoc, new Set(seen));\n }\n\n if (cloned.allOf && Array.isArray(cloned.allOf)) {\n cloned.allOf = cloned.allOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen)));\n }\n\n if (cloned.oneOf && Array.isArray(cloned.oneOf)) {\n cloned.oneOf = cloned.oneOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen)));\n }\n\n if (cloned.anyOf && Array.isArray(cloned.anyOf)) {\n cloned.anyOf = cloned.anyOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen)));\n }\n\n return cloned;\n}\n\nreturn prepareLlmRequest(msg, node);\n", + "func": "'use strict';\n\n/**\n * Node-RED Function 节点脚本:准备大模型 HTTP 请求。\n * 将生成 prompt、HTTP 请求参数,并把上下文写入 msg.llmContext。\n */\n\nconst DEFAULT_API_KEY = 'sk-lbGrsUPL1iby86h554FaE536C343435dAa9bA65967A840B2';\nconst DEFAULT_BASE_URL = 'https://aiproxy.petrotech.cnpc/v1';\nconst DEFAULT_ENDPOINT_PATH = '/chat/completions';\nconst DEFAULT_MODEL = 'deepseek-v3';\n\nfunction prepareLlmRequest(msg, node) {\n if (!msg.operationId) {\n msg.operationId = 'createResource';\n }\n\n try {\n const apiDoc = extractOpenApiDocument(msg);\n const operationCtx = resolveOperationContext(msg, apiDoc);\n\n if (!operationCtx.operation) {\n const err = `未找到匹配的接口定义(operationId=${operationCtx.requestedOperationId || '未指定'}, path=${operationCtx.requestedPath || '未指定'}, method=${operationCtx.requestedMethod || '未指定'})`;\n node.error(err, msg);\n msg.error = err;\n return null;\n }\n\n const prompt = buildPrompt(operationCtx, apiDoc, msg);\n const requestPayload = buildRequestPayload(prompt, msg);\n\n const apiKey = (msg.llm && msg.llm.apiKey) || DEFAULT_API_KEY;\n const baseUrl = (msg.llm && msg.llm.baseUrl) || DEFAULT_BASE_URL;\n const endpointPath = (msg.llm && msg.llm.endpoint) || DEFAULT_ENDPOINT_PATH;\n\n const url = buildUrl(baseUrl, endpointPath);\n msg.method = 'POST';\n msg.url = url;\n msg.headers = Object.assign({}, msg.headers, {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiKey}`,\n });\n msg.payload = requestPayload;\n msg.rejectUnauthorized = false;\n\n msg.llmContext = {\n prompt,\n operationCtx,\n provider: 'dashscope',\n requestConfig: {\n baseUrl,\n endpointPath,\n model: requestPayload.model,\n temperature: requestPayload.temperature,\n },\n };\n delete msg.error;\n return msg;\n } catch (error) {\n node.error(`LLM 请求准备失败:${error.message}`, msg);\n msg.error = error.message;\n return null;\n }\n}\n\nfunction extractOpenApiDocument(message) {\n const candidate =\n message && message.oas_def ? message.oas_def :\n message && message.swagger ? message.swagger :\n message && message.payload ? message.payload :\n null;\n\n if (!candidate) {\n throw new Error('未提供 OpenAPI 文档(需要 msg.oas_def / msg.swagger / msg.payload)');\n }\n\n if (typeof candidate === 'string') {\n try {\n return JSON.parse(candidate);\n } catch (error) {\n throw new Error(`OpenAPI JSON 解析失败:${error.message}`);\n }\n }\n\n if (typeof candidate !== 'object') {\n throw new Error('OpenAPI 文档必须是对象或 JSON 字符串');\n }\n\n if (!candidate.paths || typeof candidate.paths !== 'object') {\n throw new Error('OpenAPI 文档缺少 paths 字段');\n }\n\n return candidate;\n}\n\nfunction resolveOperationContext(message, apiDoc) {\n const requestedOperationId =\n (message && message.operationId) ||\n (message && message.req && message.req.body && message.req.body.operationId) ||\n null;\n\n const requestedPath =\n (message && message.path) ||\n (message && message.req && message.req.body && message.req.body.path) ||\n null;\n\n const requestedMethodRaw =\n (message && message.method) ||\n (message && message.req && message.req.body && message.req.body.method) ||\n null;\n const requestedMethod = requestedMethodRaw ? String(requestedMethodRaw).toLowerCase() : null;\n\n const operations = enumerateOperations(apiDoc);\n\n let matched = null;\n if (requestedOperationId) {\n matched = operations.find(op => op.operation.operationId === requestedOperationId);\n }\n\n if (!matched && requestedPath && requestedMethod) {\n matched = operations.find(op => op.path === requestedPath && op.method === requestedMethod);\n }\n\n let autoSelected = false;\n if (!matched && operations.length > 0) {\n matched = operations[0];\n autoSelected = true;\n }\n\n return {\n operation: matched ? matched.operation : undefined,\n method: matched ? matched.method : undefined,\n pathItem: matched ? matched.pathItem : undefined,\n path: matched ? matched.path : undefined,\n autoSelected,\n candidates: operations.map(op => ({\n operationId: op.operation && op.operation.operationId ? op.operation.operationId : '',\n method: op.method ? op.method.toUpperCase() : '',\n path: op.path,\n summary: op.operation && op.operation.summary ? op.operation.summary : '',\n })),\n requestedOperationId,\n requestedPath,\n requestedMethod,\n };\n}\n\nfunction enumerateOperations(apiDoc) {\n const results = [];\n const validMethods = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'];\n for (const path of Object.keys(apiDoc.paths || {})) {\n const pathItem = apiDoc.paths[path];\n if (!pathItem || typeof pathItem !== 'object') {\n continue;\n }\n for (const method of validMethods) {\n if (pathItem[method] && typeof pathItem[method] === 'object') {\n results.push({\n path,\n method,\n operation: pathItem[method],\n pathItem,\n });\n }\n }\n }\n return results;\n}\n\nfunction buildPrompt(operationCtx, apiDoc, message) {\n const operation = operationCtx.operation;\n const method = (operationCtx.method || '').toUpperCase();\n const path = operationCtx.path || '';\n const summary = operation.summary || '';\n const description = operation.description || '';\n\n const parameters = gatherParameters(operationCtx, apiDoc);\n const requestBodySchema = gatherRequestBodySchema(operation, apiDoc);\n\n const userNotes = message && message.prompt ? String(message.prompt) : '';\n const requiredFields = extractRequiredFields(requestBodySchema, apiDoc);\n const requiredFieldsText = requiredFields.length > 0\n ? requiredFields.map(field => `- ${field}`).join('\\n')\n : '';\n\n const lines = [];\n lines.push('你是一名负责生成 HTTP 接口测试参数的助手,请严格输出以下结构的 JSON:');\n lines.push('{');\n lines.push(' \"pathParams\": { ... },');\n lines.push(' \"query\": { ... },');\n lines.push(' \"headers\": { ... },');\n lines.push(' \"cookies\": { ... },');\n lines.push(' \"body\": { ... },');\n lines.push(' \"notes\": \"...\"');\n lines.push('}');\n lines.push('未用到的分区请返回空对象,不要在 JSON 外输出任何文字。');\n lines.push('');\n lines.push('body.data[0] 必须遵守:');\n lines.push('- 覆盖 schema 中声明的全部必填字段,并给出符合字段类型/格式的真实感样例值。');\n lines.push('- 非必填字段也尽可能全覆盖');\n lines.push('- 不得遗漏必填字段;若 schema 内有嵌套对象/数组的必填字段,同样要补齐。');\n lines.push('- 保持数值、日期、字符串等格式,只在 schema 无提示时使用 \"sample\"、\"2025-01-01\" 等占位值。');\n if (requiredFieldsText) {\n lines.push('必填字段清单(必须全部出现在 body.data[0] 中):');\n lines.push(requiredFieldsText);\n }\n lines.push('');\n lines.push('若 schema 中存在数组元素或复合结构的必填字段,也要为这些子字段提供值。');\n lines.push('');\n lines.push(`Target operation: ${method} ${path}`);\n\n if (summary) {\n lines.push(`Summary: ${summary}`);\n }\n if (description) {\n lines.push(`Description: ${description}`);\n }\n if (parameters.length > 0) {\n lines.push('Parameters:');\n for (const param of parameters) {\n lines.push(`- [${param.in}] ${param.name}: ${param.type || 'any'}${param.required ? ' (required)' : ''}${param.description ? ` - ${param.description}` : ''}`);\n }\n } else {\n lines.push('Parameters: none defined.');\n }\n\n if (requestBodySchema) {\n lines.push('Request body schema (JSON Schema excerpt):');\n lines.push(indentSnippet(JSON.stringify(requestBodySchema, null, 2), 2));\n } else {\n lines.push('Request body: not defined.');\n }\n\n if (userNotes) {\n lines.push('');\n lines.push('User notes:');\n lines.push(userNotes);\n }\n\n return lines.join('\\n');\n}\n\nfunction gatherParameters(operationCtx, apiDoc) {\n const aggregated = [];\n const seen = new Set();\n\n const sources = [];\n if (Array.isArray(operationCtx.pathItem && operationCtx.pathItem.parameters)) {\n sources.push(operationCtx.pathItem.parameters);\n }\n if (Array.isArray(operationCtx.operation.parameters)) {\n sources.push(operationCtx.operation.parameters);\n }\n\n for (const list of sources) {\n for (const param of list) {\n if (!param || typeof param !== 'object') {\n continue;\n }\n const resolved = resolveMaybeRef(param, apiDoc);\n const key = `${resolved.in}:${resolved.name}`;\n if (!resolved.name || seen.has(key)) {\n continue;\n }\n seen.add(key);\n aggregated.push({\n name: resolved.name,\n in: resolved.in || 'query',\n required: !!resolved.required,\n description: resolved.description || '',\n type: resolved.schema ? inferFriendlyType(resolved.schema, apiDoc) : '',\n });\n }\n }\n\n return aggregated;\n}\n\nfunction gatherRequestBodySchema(operation, apiDoc) {\n const requestBody = resolveMaybeRef(operation.requestBody, apiDoc);\n if (!requestBody || typeof requestBody !== 'object' || !requestBody.content) {\n return null;\n }\n\n const content = requestBody.content;\n const mediaType = Object.keys(content).find(key => key.includes('json')) || Object.keys(content)[0];\n if (!mediaType) {\n return null;\n }\n\n const mediaObject = resolveMaybeRef(content[mediaType], apiDoc);\n if (!mediaObject || typeof mediaObject !== 'object' || !mediaObject.schema) {\n return null;\n }\n\n const schema = resolveMaybeRef(mediaObject.schema, apiDoc);\n return resolveSchemaDeep(schema, apiDoc);\n}\n\nfunction inferFriendlyType(schema, apiDoc) {\n if (!schema) {\n return '';\n }\n const resolved = resolveMaybeRef(schema, apiDoc);\n if (!resolved || typeof resolved !== 'object') {\n return '';\n }\n\n if (resolved.type) {\n if (resolved.type === 'array' && resolved.items) {\n const itemType = inferFriendlyType(resolved.items, apiDoc) || 'any';\n return `[${itemType}]`;\n }\n return resolved.type;\n }\n\n if (resolved.enum && Array.isArray(resolved.enum)) {\n return `enum(${resolved.enum.slice(0, 3).join(', ')}${resolved.enum.length > 3 ? ', …' : ''})`;\n }\n\n if (resolved.properties) {\n return 'object';\n }\n\n return '';\n}\n\nfunction buildRequestPayload(prompt, message) {\n const systemPrompt = (message.llm && message.llm.systemPrompt) ||\n 'You write JSON only. Focus on realistic values for testing HTTP APIs.';\n\n const model = (message.llm && message.llm.model) || DEFAULT_MODEL;\n const temperature = (message.llm && typeof message.llm.temperature === 'number')\n ? message.llm.temperature : 0.2;\n\n return {\n model,\n temperature,\n response_format: { type: 'json_object' },\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: prompt },\n ],\n };\n}\n\nfunction buildUrl(baseUrl, endpointPath) {\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;\n const path = endpointPath.startsWith('/') ? endpointPath.slice(1) : endpointPath;\n return `${normalizedBase}${path}`;\n}\n\nfunction resolveMaybeRef(node, apiDoc) {\n if (!node || typeof node !== 'object') {\n return node;\n }\n if (!node.$ref) {\n return node;\n }\n\n const resolved = resolveRef(node.$ref, apiDoc);\n if (!resolved || typeof resolved !== 'object') {\n return node;\n }\n\n const remainder = Object.assign({}, node);\n delete remainder.$ref;\n return Object.assign({}, clone(resolved), remainder);\n}\n\nfunction resolveRef(ref, apiDoc) {\n if (typeof ref !== 'string' || !ref.startsWith('#/')) {\n return null;\n }\n\n const tokens = ref.slice(2).split('/').map(unescapeRefToken);\n let current = apiDoc;\n for (const token of tokens) {\n if (current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, token)) {\n current = current[token];\n } else {\n return null;\n }\n }\n return current;\n}\n\nfunction unescapeRefToken(token) {\n return token.replace(/~1/g, '/').replace(/~0/g, '~');\n}\n\nfunction indentSnippet(text, indentLevel, maxLength) {\n const trimmed = maxLength && text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;\n const indent = ' '.repeat(indentLevel * 2);\n return trimmed.split('\\n').map(line => `${indent}${line}`).join('\\n');\n}\n\nfunction clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n\nfunction extractRequiredFields(schema, apiDoc) {\n const result = new Set();\n collectRequiredFields(schema, apiDoc, result, '');\n return Array.from(result);\n}\n\nfunction collectRequiredFields(schema, apiDoc, result, pathPrefix) {\n if (!schema || typeof schema !== 'object') {\n return;\n }\n\n if (schema.$ref) {\n const resolved = resolveRef(schema.$ref, apiDoc);\n if (!resolved) {\n return;\n }\n collectRequiredFields(resolveSchemaDeep(resolved, apiDoc), apiDoc, result, pathPrefix);\n return;\n }\n\n if (Array.isArray(schema.required) && schema.properties && typeof schema.properties === 'object') {\n for (const key of schema.required) {\n const nextPath = pathPrefix ? `${pathPrefix}.${key}` : key;\n result.add(nextPath);\n collectRequiredFields(schema.properties[key], apiDoc, result, nextPath);\n }\n }\n\n if (schema.type === 'array' && schema.items) {\n const nextPrefix = pathPrefix ? `${pathPrefix}[]` : '[]';\n collectRequiredFields(schema.items, apiDoc, result, nextPrefix);\n }\n\n if (schema.allOf && Array.isArray(schema.allOf)) {\n for (const part of schema.allOf) {\n collectRequiredFields(part, apiDoc, result, pathPrefix);\n }\n }\n}\n\nfunction resolveSchemaDeep(schema, apiDoc, seen = new Set()) {\n if (!schema || typeof schema !== 'object') {\n return schema;\n }\n\n if (schema.$ref) {\n const ref = schema.$ref;\n if (seen.has(ref)) {\n return {};\n }\n seen.add(ref);\n const resolved = resolveRef(ref, apiDoc);\n if (!resolved) {\n return schema;\n }\n const merged = Object.assign({}, clone(resolved), clone(schema));\n delete merged.$ref;\n return resolveSchemaDeep(merged, apiDoc, seen);\n }\n\n const cloned = clone(schema);\n\n if (cloned.properties && typeof cloned.properties === 'object') {\n for (const key of Object.keys(cloned.properties)) {\n cloned.properties[key] = resolveSchemaDeep(cloned.properties[key], apiDoc, new Set(seen));\n }\n }\n\n if (cloned.items) {\n cloned.items = resolveSchemaDeep(cloned.items, apiDoc, new Set(seen));\n }\n\n if (cloned.allOf && Array.isArray(cloned.allOf)) {\n cloned.allOf = cloned.allOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen)));\n }\n\n if (cloned.oneOf && Array.isArray(cloned.oneOf)) {\n cloned.oneOf = cloned.oneOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen)));\n }\n\n if (cloned.anyOf && Array.isArray(cloned.anyOf)) {\n cloned.anyOf = cloned.anyOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen)));\n }\n\n return cloned;\n}\n\nreturn prepareLlmRequest(msg, node);\n", "outputs": 1, "timeout": 0, "noerr": 0,