From 1ecd4d87c32f778edf8b67ee93a9138fb8fb6451 Mon Sep 17 00:00:00 2001 From: ruoyunbai <1153712410@qq.com> Date: Tue, 4 Nov 2025 17:28:53 +0800 Subject: [PATCH] flow --- .node-red-data/projects/zsy/flows.json | 66 ++++++-------------------- 1 file changed, 14 insertions(+), 52 deletions(-) diff --git a/.node-red-data/projects/zsy/flows.json b/.node-red-data/projects/zsy/flows.json index 0d09eee..ce0aaee 100644 --- a/.node-red-data/projects/zsy/flows.json +++ b/.node-red-data/projects/zsy/flows.json @@ -1208,7 +1208,7 @@ "type": "debug", "z": "2bbc1a949095492c", "name": "创建结果", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -1559,7 +1559,7 @@ "type": "debug", "z": "2bbc1a949095492c", "name": "debug 16", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -1567,8 +1567,8 @@ "targetType": "jsonata", "statusVal": "", "statusType": "auto", - "x": 1080, - "y": 660, + "x": 1000, + "y": 600, "wires": [] }, { @@ -1583,7 +1583,7 @@ "complete": "false", "statusVal": "", "statusType": "auto", - "x": 880, + "x": 800, "y": 660, "wires": [] }, @@ -1962,13 +1962,11 @@ "type": "http request", "z": "2bbc1a949095492c", "name": "", - "method": "POST", + "method": "GET", "ret": "txt", - "paytoqs": "ignore", + "paytoqs": false, "url": "", - "tls": "", "persist": false, - "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, @@ -1998,8 +1996,7 @@ "y": 480, "wires": [ [ - "6b82573f9ffd406c", - "b82b55ab1d9ec1f0" + "6b82573f9ffd406c" ] ] }, @@ -2070,8 +2067,8 @@ "tosidebar": true, "console": false, "tostatus": false, - "complete": "msg", - "targetType": "jsonata", + "complete": "payload", + "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 480, @@ -2095,23 +2092,6 @@ "y": 820, "wires": [] }, - { - "id": "b82b55ab1d9ec1f0", - "type": "debug", - "z": "2bbc1a949095492c", - "name": "create内容", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "msg", - "targetType": "jsonata", - "statusVal": "", - "statusType": "auto", - "x": 850, - "y": 600, - "wires": [] - }, { "id": "fd3672b8ebb314ee", "type": "http in", @@ -2203,7 +2183,7 @@ "type": "debug", "z": "78d15f59dee4b6d8", "name": "debug 10", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -2356,7 +2336,7 @@ "type": "function", "z": "78d15f59dee4b6d8", "name": "准备llm输入", - "func": "'use strict';\nmsg.operationId = \"createResource\"\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\n const lines = [];\n lines.push('You are an assistant that generates realistic API request parameter examples.');\n lines.push('Return a strict JSON object with the shape:');\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('Set missing sections to empty objects. Avoid explanatory text outside JSON.');\n lines.push('');\n lines.push(`Target operation: ${method} ${path}`);\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 schema;\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\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);\n const requiredFieldsText = requiredFields.length > 0\n ? requiredFields.map(field => `- ${field}`).join('\\n')\n : '';\n\n const lines = [];\n lines.push('You are an assistant that generates realistic API request parameter examples.');\n lines.push('Return a strict JSON object with the shape:');\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('Set missing sections to empty objects. Avoid explanatory text outside JSON.');\n lines.push('');\n lines.push('Rules for body.data[0]:');\n lines.push(\"- Include every required field from the schema, using realistic sample values that respect each field's type/format.\");\n lines.push('- Do not omit any required field listed below; ensure nested required fields are also present.');\n lines.push('- Preserve numeric/date/string formats. Use placeholders like \\\"sample\\\" or \\\"2025-01-01\\\" only when schema provides no better hint.');\n if (requiredFieldsText) {\n lines.push('Required fields list (must all appear in body.data[0]):');\n lines.push(requiredFieldsText);\n }\n lines.push('');\n lines.push(`Target operation: ${method} ${path}`);\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 schema;\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) {\n const result = new Set();\n collectRequiredFields(schema, result, '');\n return Array.from(result);\n}\n\nfunction collectRequiredFields(schema, result, pathPrefix) {\n if (!schema || typeof schema !== 'object') {\n return;\n }\n\n if (schema.$ref) {\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], result, nextPath);\n }\n }\n\n if (schema.type === 'array' && schema.items) {\n collectRequiredFields(schema.items, result, pathPrefix);\n }\n\n if (schema.allOf && Array.isArray(schema.allOf)) {\n for (const part of schema.allOf) {\n collectRequiredFields(part, result, pathPrefix);\n }\n }\n}\n\nreturn prepareLlmRequest(msg, node);\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -2430,8 +2410,7 @@ "wires": [ [ "20818ab4243934bf", - "44fc6e2d405c2a78", - "15b4d87bbd3395aa" + "44fc6e2d405c2a78" ] ] }, @@ -2449,7 +2428,7 @@ "statusVal": "", "statusType": "auto", "x": 750, - "y": 600, + "y": 620, "wires": [] }, { @@ -2542,22 +2521,5 @@ "x": 1155, "y": 540, "wires": [] - }, - { - "id": "15b4d87bbd3395aa", - "type": "file", - "z": "78d15f59dee4b6d8", - "name": "", - "filename": "C:\\workspace\\testllm.txt", - "filenameType": "str", - "appendNewline": true, - "createDir": false, - "overwriteFile": "false", - "encoding": "none", - "x": 790, - "y": 660, - "wires": [ - [] - ] } ] \ No newline at end of file