From 5fec1bd023ea3b5ef9da43ad89cce8a962570c09 Mon Sep 17 00:00:00 2001 From: ruoyunbai <1153712410@qq.com> Date: Mon, 3 Nov 2025 10:44:14 +0800 Subject: [PATCH] flow --- .node-red-data/projects/zsy/flows.json | 64 ++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/.node-red-data/projects/zsy/flows.json b/.node-red-data/projects/zsy/flows.json index 87d6ee3..c7d50d3 100644 --- a/.node-red-data/projects/zsy/flows.json +++ b/.node-red-data/projects/zsy/flows.json @@ -1181,7 +1181,8 @@ [ "dfe6ed572461c4a5", "5231ed8a796d6f17", - "6ab9403df07fcefa" + "6ab9403df07fcefa", + "60ce224bd2ed8e69" ] ] }, @@ -1219,7 +1220,7 @@ "type": "debug", "z": "78d15f59dee4b6d8", "name": "debug 10", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -1346,8 +1347,7 @@ "y": 400, "wires": [ [ - "6e56a2ccd9fcaacc", - "60ce224bd2ed8e69" + "6e56a2ccd9fcaacc" ] ] }, @@ -1356,7 +1356,7 @@ "type": "debug", "z": "78d15f59dee4b6d8", "name": "参数展示", - "active": true, + "active": false, "tosidebar": true, "console": false, "tostatus": false, @@ -1372,19 +1372,19 @@ "id": "60ce224bd2ed8e69", "type": "function", "z": "78d15f59dee4b6d8", - "name": "llm", - "func": "'use strict';\nmsg.operationId = \"createResource\"\nconst https = require('https');\nconst { URL } = require('url');\n\nconst DEFAULT_API_KEY = process.env.DASHSCOPE_API_KEY || process.env.DASH_API_KEY ||\n 'sk-lbGrsUPL1iby86h554FaE536C343435dAa9bA65967A840B2';\nconst DEFAULT_BASE_URL = process.env.DASHSCOPE_BASE_URL || 'https://aiproxy.petrotech.cnpc/v1';\nconst DEFAULT_ENDPOINT_PATH = process.env.DASHSCOPE_ENDPOINT || '/chat/completions';\nconst DEFAULT_MODEL = process.env.DASHSCOPE_MODEL || 'deepseek-v3';\n\nprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';\n\n(async () => {\n const finalMsg = msg;\n try {\n const apiDoc = extractOpenApiDocument(finalMsg);\n const operationCtx = resolveOperationContext(finalMsg, apiDoc);\n\n if (!operationCtx.operation) {\n const errMessage = `未找到匹配的接口定义(operationId=${operationCtx.requestedOperationId || '未指定'}, path=${operationCtx.requestedPath || '未指定'}, method=${operationCtx.requestedMethod || '未指定'})`;\n node.error(errMessage, finalMsg);\n finalMsg.error = errMessage;\n node.send(finalMsg);\n if (typeof node.done === 'function') {\n node.done();\n }\n return;\n }\n\n const prompt = buildPrompt(operationCtx, apiDoc, finalMsg);\n const requestPayload = buildRequestPayload(prompt, finalMsg);\n\n const apiKey = (finalMsg.llm && finalMsg.llm.apiKey) || DEFAULT_API_KEY;\n const baseUrl = (finalMsg.llm && finalMsg.llm.baseUrl) || DEFAULT_BASE_URL;\n const endpointPath = (finalMsg.llm && finalMsg.llm.endpoint) || DEFAULT_ENDPOINT_PATH;\n\n const rawResponse = await postJson(baseUrl, endpointPath, requestPayload, apiKey);\n const parsed = parseModelResponse(rawResponse);\n\n finalMsg.mock = Object.assign({}, finalMsg.mock, parsed.mock);\n finalMsg.mockSource = 'dashscope';\n finalMsg.mockPrompt = prompt;\n finalMsg.mockCandidates = operationCtx.candidates;\n finalMsg.mockAutoSelected = operationCtx.autoSelected || false;\n finalMsg.llmRaw = rawResponse;\n finalMsg.payload = finalMsg.mock;\n\n node.send(finalMsg);\n if (typeof node.done === 'function') {\n node.done();\n }\n } catch (error) {\n node.error(`大模型参数生成失败:${error.message}`, msg);\n msg.error = error.message;\n node.send(msg);\n if (typeof node.done === 'function') {\n node.done();\n }\n }\n})();\n\nreturn;\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 if (resolved.enum) {\n return `enum(${resolved.enum.slice(0, 3).join(', ')}${resolved.enum.length > 3 ? ', ...' : ''})`;\n }\n if (resolved.properties) {\n return 'object';\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 postJson(baseUrl, endpointPath, body, apiKey) {\n return new Promise((resolve, reject) => {\n let url;\n try {\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;\n const path = endpointPath.startsWith('/') ? endpointPath.slice(1) : endpointPath;\n url = new URL(path, normalizedBase);\n } catch (err) {\n return reject(new Error(`无法解析大模型接口地址:${err.message}`));\n }\n\n const payload = JSON.stringify(body);\n\n const options = {\n method: 'POST',\n protocol: url.protocol,\n hostname: url.hostname,\n port: url.port || 443,\n path: `${url.pathname}${url.search}`,\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(payload),\n 'Authorization': `Bearer ${apiKey}`,\n },\n rejectUnauthorized: false,\n };\n\n const req = https.request(options, res => {\n const chunks = [];\n res.on('data', chunk => chunks.push(chunk));\n res.on('end', () => {\n const text = Buffer.concat(chunks).toString('utf8');\n if (res.statusCode >= 200 && res.statusCode < 300) {\n try {\n const json = JSON.parse(text);\n resolve(json);\n } catch (err) {\n reject(new Error(`解析大模型响应失败:${err.message},原始响应:${text}`));\n }\n } else {\n reject(new Error(`大模型接口返回状态码 ${res.statusCode}:${text}`));\n }\n });\n });\n\n req.on('error', err => reject(err));\n req.write(payload);\n req.end();\n });\n}\n\nfunction parseModelResponse(response) {\n if (!response || typeof response !== 'object') {\n throw new Error('大模型响应为空或不是对象');\n }\n\n if (response.mock && typeof response.mock === 'object') {\n return { mock: response.mock };\n }\n\n const choices = Array.isArray(response.choices) ? response.choices : [];\n const firstChoice = choices[0];\n const message = firstChoice && firstChoice.message ? firstChoice.message : null;\n const content = message && typeof message === 'object' ? message.content : null;\n\n if (!content) {\n throw new Error('响应中缺少 choices[0].message.content');\n }\n\n let mockObject;\n if (typeof content === 'string') {\n try {\n mockObject = JSON.parse(content);\n } catch (err) {\n throw new Error(`无法解析模型返回的 JSON:${err.message},原始文本:${content}`);\n }\n } else if (Array.isArray(content)) {\n const jsonPart = content.find(part => part.type === 'output_text' || part.type === 'text' || part.type === 'json');\n const text = jsonPart && jsonPart.text ? jsonPart.text : null;\n if (!text) {\n throw new Error('响应内容不是字符串,且未找到可解析的文本段');\n }\n try {\n mockObject = JSON.parse(text);\n } catch (err) {\n throw new Error(`无法解析模型返回的 JSON:${err.message},原始文本:${text}`);\n }\n } else {\n throw new Error('模型返回的 message.content 既不是字符串也不是文本片段数组');\n }\n\n if (!mockObject || typeof mockObject !== 'object') {\n throw new Error('模型返回的 JSON 不是对象');\n }\n\n const normalisedMock = {\n pathParams: mockObject.pathParams || mockObject.path_parameters || {},\n query: mockObject.query || mockObject.query_params || {},\n headers: mockObject.headers || {},\n cookies: mockObject.cookies || {},\n body: mockObject.body || {},\n notes: mockObject.notes || '',\n };\n\n return { mock: normalisedMock };\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", + "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", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 610, - "y": 440, + "x": 450, + "y": 540, "wires": [ [ - "e87c2057c28d5eee" + "540a5771113ca5dd" ] ] }, @@ -1404,5 +1404,49 @@ "x": 790, "y": 420, "wires": [] + }, + { + "id": "20818ab4243934bf", + "type": "function", + "z": "78d15f59dee4b6d8", + "name": "处理llm返回", + "func": "'use strict';\n\n/**\n * Node-RED Function 节点脚本:处理大模型 HTTP 响应。\n * 将响应解析为 mock 数据并写回 msg。\n */\n\nfunction processLlmResponse(msg, node) {\n const context = msg.llmContext || {};\n if (!context.prompt || !context.operationCtx) {\n const err = 'LLM 上下文缺失,无法解析响应';\n node.error(err, msg);\n msg.error = err;\n return msg;\n }\n\n if (msg.statusCode && (msg.statusCode < 200 || msg.statusCode >= 300)) {\n const err = `大模型接口返回状态码 ${msg.statusCode}`;\n node.error(err, msg);\n msg.error = err;\n return msg;\n }\n\n let raw = msg.payload;\n if (Buffer.isBuffer(raw)) {\n raw = raw.toString('utf8');\n }\n if (typeof raw === 'string') {\n try {\n raw = JSON.parse(raw);\n } catch (error) {\n node.error(`无法解析大模型响应:${error.message}`, msg);\n msg.error = error.message;\n return msg;\n }\n }\n\n try {\n const parsed = parseModelResponse(raw);\n\n msg.llmRaw = raw;\n msg.mock = Object.assign({}, msg.mock, parsed.mock);\n msg.mockSource = context.provider || 'dashscope';\n msg.mockPrompt = context.prompt;\n msg.mockCandidates = context.operationCtx.candidates;\n msg.mockAutoSelected = !!context.operationCtx.autoSelected;\n msg.payload = msg.mock;\n\n delete msg.llmContext;\n if (msg.headers && msg.headers.Authorization) {\n delete msg.headers.Authorization;\n }\n\n delete msg.error;\n return msg;\n } catch (error) {\n node.error(`大模型参数生成失败:${error.message}`, msg);\n msg.error = error.message;\n return msg;\n }\n}\n\nfunction parseModelResponse(response) {\n if (!response || typeof response !== 'object') {\n throw new Error('大模型响应为空或不是对象');\n }\n\n if (response.mock && typeof response.mock === 'object') {\n return { mock: response.mock };\n }\n\n const choices = Array.isArray(response.choices) ? response.choices : [];\n const firstChoice = choices[0];\n const message = firstChoice && firstChoice.message ? firstChoice.message : null;\n const content = message && typeof message === 'object' ? message.content : null;\n\n if (!content) {\n throw new Error('响应中缺少 choices[0].message.content');\n }\n\n let mockObject;\n if (typeof content === 'string') {\n try {\n mockObject = JSON.parse(content);\n } catch (err) {\n throw new Error(`无法解析模型返回的 JSON:${err.message},原始文本:${content}`);\n }\n } else if (Array.isArray(content)) {\n const jsonPart = content.find(part => part.type === 'output_text' || part.type === 'text' || part.type === 'json');\n const text = jsonPart && jsonPart.text ? jsonPart.text : null;\n if (!text) {\n throw new Error('响应内容不是字符串,且未找到可解析的文本段');\n }\n try {\n mockObject = JSON.parse(text);\n } catch (err) {\n throw new Error(`无法解析模型返回的 JSON:${err.message},原始文本:${text}`);\n }\n } else {\n throw new Error('模型返回的 message.content 既不是字符串也不是文本片段数组');\n }\n\n if (!mockObject || typeof mockObject !== 'object') {\n throw new Error('模型返回的 JSON 不是对象');\n }\n\n const normalisedMock = {\n pathParams: mockObject.pathParams || mockObject.path_parameters || {},\n query: mockObject.query || mockObject.query_params || {},\n headers: mockObject.headers || {},\n cookies: mockObject.cookies || {},\n body: mockObject.body || {},\n notes: mockObject.notes || '',\n };\n\n return { mock: normalisedMock };\n}\n\nreturn processLlmResponse(msg, node);\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 730, + "y": 520, + "wires": [ + [ + "e87c2057c28d5eee" + ] + ] + }, + { + "id": "540a5771113ca5dd", + "type": "http request", + "z": "78d15f59dee4b6d8", + "name": "", + "method": "POST", + "ret": "txt", + "paytoqs": "ignore", + "url": "", + "tls": "", + "persist": false, + "proxy": "", + "insecureHTTPParser": false, + "authType": "", + "senderr": false, + "headers": [], + "x": 600, + "y": 600, + "wires": [ + [ + "20818ab4243934bf" + ] + ] } ] \ No newline at end of file