From 817400dfd79e7b2aaf206bff09e600cd72eafc13 Mon Sep 17 00:00:00 2001 From: ruoyunbai <19376215@buaa.edu.cn> Date: Mon, 3 Nov 2025 08:52:11 +0800 Subject: [PATCH] flow --- .node-red-data/projects/zsy/flows.json | 82 +++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/.node-red-data/projects/zsy/flows.json b/.node-red-data/projects/zsy/flows.json index 4349eed..87d6ee3 100644 --- a/.node-red-data/projects/zsy/flows.json +++ b/.node-red-data/projects/zsy/flows.json @@ -1168,7 +1168,7 @@ "type": "function", "z": "78d15f59dee4b6d8", "name": "dms转化为oas", - "func": "'use strict';\n\nconst DEFAULT_OPENAPI_VERSION = '3.0.1';\nconst DEFAULT_API_VERSION = '1.0.0';\n\nlet schema;\ntry {\n // 优先使用 HTTP In 提供的 req.body.schema,缺省时回退到 msg.payload。\n const schemaInput = extractSchemaInput(msg);\n schema = parseSchema(schemaInput);\n} catch (error) {\n node.error(`DMS -> Swagger 解析失败:${error.message}`, msg);\n msg.error = error.message;\n return msg;\n}\n\nconst resourceTitle = typeof schema.title === 'string' && schema.title.trim()\n ? schema.title.trim()\n : 'DMS Resource';\n\nconst resourceName = pascalCase(resourceTitle);\nconst collectionName = pluralize(kebabCase(resourceTitle));\nconst identityField = Array.isArray(schema.identityId) && schema.identityId.length > 0\n ? String(schema.identityId[0])\n : 'id';\n\nconst schemaComponent = buildComponentSchema(resourceName, schema);\nconst identitySchema = schemaComponent.properties && schemaComponent.properties[identityField]\n ? clone(schemaComponent.properties[identityField])\n : { type: 'string' };\n\nconst openApiDocument = {\n openapi: DEFAULT_OPENAPI_VERSION,\n info: {\n title: `${resourceTitle} API`,\n version: DEFAULT_API_VERSION,\n description: schema.description || `${schema.$id || ''}`.trim(),\n 'x-dms-sourceId': schema.$id || undefined,\n },\n paths: buildCrudPaths({\n collectionName,\n resourceName,\n identityField,\n identitySchema,\n }),\n components: {\n schemas: {\n [resourceName]: schemaComponent,\n },\n },\n};\n\nmsg.oas_def = openApiDocument;\nmsg.payload = openApiDocument;\nreturn msg;\n\nfunction extractSchemaInput(message) {\n if (message && message.req && message.req.body && typeof message.req.body === 'object') {\n if (message.req.body.schema !== undefined) {\n return message.req.body.schema;\n }\n }\n\n if (message && message.payload !== undefined) {\n return message.payload;\n }\n\n throw new Error('未找到schema,请在请求体的schema字段或msg.payload中提供');\n}\n\nfunction parseSchema(source) {\n if (typeof source === 'string') {\n try {\n return JSON.parse(source);\n } catch (error) {\n throw new Error(`JSON 解析失败:${error.message}`);\n }\n }\n\n if (!source || typeof source !== 'object') {\n throw new Error('schema 必须是 DMS 定义对象或 JSON 字符串');\n }\n\n return source;\n}\n\nfunction buildComponentSchema(resourceName, dmsSchema) {\n const { properties = {}, required = [], groupView, identityId, naturalKey, defaultShow } = dmsSchema;\n const openApiProps = {};\n\n for (const [propName, propSchema] of Object.entries(properties)) {\n openApiProps[propName] = mapProperty(propSchema);\n }\n\n return {\n type: 'object',\n required: Array.isArray(required) ? required.slice() : [],\n properties: openApiProps,\n description: dmsSchema.description || dmsSchema.title || resourceName,\n 'x-dms-groupView': groupView || undefined,\n 'x-dms-identityId': identityId || undefined,\n 'x-dms-naturalKey': naturalKey || undefined,\n 'x-dms-defaultShow': defaultShow || undefined,\n };\n}\n\nfunction mapProperty(propSchema) {\n if (!propSchema || typeof propSchema !== 'object') {\n return { type: 'string' };\n }\n\n const result = {};\n\n const type = normalizeType(propSchema.type);\n result.type = type.type;\n if (type.format) {\n result.format = type.format;\n }\n if (type.items) {\n result.items = type.items;\n }\n\n if (propSchema.description) {\n result.description = propSchema.description;\n } else if (propSchema.title) {\n result.description = propSchema.title;\n }\n\n if (propSchema.enum) {\n result.enum = propSchema.enum.slice();\n }\n\n if (propSchema.default !== undefined) {\n result.default = propSchema.default;\n }\n\n if (propSchema.mask) {\n result['x-dms-mask'] = propSchema.mask;\n }\n if (propSchema.geom) {\n result['x-dms-geom'] = propSchema.geom;\n }\n if (propSchema.title) {\n result['x-dms-title'] = propSchema.title;\n }\n if (propSchema.type) {\n result['x-dms-originalType'] = propSchema.type;\n }\n\n return result;\n}\n\nfunction normalizeType(typeValue) {\n const normalized = typeof typeValue === 'string' ? typeValue.toLowerCase() : undefined;\n\n switch (normalized) {\n case 'number':\n case 'integer':\n case 'long':\n case 'float':\n case 'double':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'array':\n return { type: 'array', items: { type: 'string' } };\n case 'date':\n return { type: 'string', format: 'date' };\n case 'date-time':\n return { type: 'string', format: 'date-time' };\n case 'object':\n return { type: 'object' };\n case 'string':\n default:\n return { type: 'string' };\n }\n}\n\nfunction buildCrudPaths({ collectionName, resourceName, identityField, identitySchema }) {\n const ref = `#/components/schemas/${resourceName}`;\n const collectionPath = `/${collectionName}`;\n const itemPath = `/${collectionName}/{${identityField}}`;\n\n return {\n [collectionPath]: {\n get: {\n operationId: `list${resourceName}s`,\n summary: `List ${resourceName} resources`,\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: {\n type: 'array',\n items: { $ref: ref },\n },\n },\n },\n },\n },\n },\n /*\n post: {\n operationId: `create${resourceName}`,\n summary: `Create a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n responses: {\n 201: {\n description: 'Created',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n */\n },\n [itemPath]: {\n parameters: [\n {\n name: identityField,\n in: 'path',\n required: true,\n schema: identitySchema,\n },\n ],\n get: {\n operationId: `get${resourceName}`,\n summary: `Get a single ${resourceName}`,\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n 404: { description: `${resourceName} not found` },\n },\n },\n /*\n put: {\n operationId: `update${resourceName}`,\n summary: `Update a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n responses: {\n 200: {\n description: 'Successful update',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n delete: {\n operationId: `delete${resourceName}`,\n summary: `Delete a ${resourceName}`,\n responses: {\n 204: { description: 'Deleted' },\n },\n },\n */\n },\n };\n}\n\nfunction pascalCase(input) {\n return input\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .split(' ')\n .filter(Boolean)\n .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join('') || 'Resource';\n}\n\nfunction kebabCase(input) {\n return input\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/[^a-zA-Z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .toLowerCase() || 'resource';\n}\n\nfunction pluralize(word) {\n if (word.endsWith('s')) {\n return word;\n }\n if (word.endsWith('y')) {\n return word.slice(0, -1) + 'ies';\n }\n return `${word}s`;\n}\n\nfunction clone(value) {\n return JSON.parse(JSON.stringify(value));\n}\n", + "func": "'use strict';\n\nconst DEFAULT_OPENAPI_VERSION = '3.0.1';\nconst DEFAULT_API_VERSION = '1.0.0';\n\nlet schema;\ntry {\n // 优先使用 HTTP In 提供的 req.body.schema,缺省时回退到 msg.payload。\n const schemaInput = extractSchemaInput(msg);\n schema = parseSchema(schemaInput);\n} catch (error) {\n node.error(`DMS -> Swagger 解析失败:${error.message}`, msg);\n msg.error = error.message;\n return msg;\n}\n\nconst resourceTitle = typeof schema.title === 'string' && schema.title.trim()\n ? schema.title.trim()\n : 'DMS Resource';\n\nconst resourceName = pascalCase(resourceTitle);\nconst collectionName = pluralize(kebabCase(resourceTitle));\nconst identityField = Array.isArray(schema.identityId) && schema.identityId.length > 0\n ? String(schema.identityId[0])\n : 'id';\n\nconst schemaComponent = buildComponentSchema(resourceName, schema);\nconst identitySchema = schemaComponent.properties && schemaComponent.properties[identityField]\n ? clone(schemaComponent.properties[identityField])\n : { type: 'string' };\n\nconst openApiDocument = {\n openapi: DEFAULT_OPENAPI_VERSION,\n info: {\n title: `${resourceTitle} API`,\n version: DEFAULT_API_VERSION,\n description: schema.description || `${schema.$id || ''}`.trim(),\n 'x-dms-sourceId': schema.$id || undefined,\n },\n paths: buildCrudPaths({\n collectionName,\n resourceName,\n identityField,\n identitySchema,\n }),\n components: {\n schemas: {\n [resourceName]: schemaComponent,\n },\n },\n};\n\nmsg.oas_def = openApiDocument;\nmsg.payload = openApiDocument;\nreturn msg;\n\nfunction extractSchemaInput(message) {\n if (message && message.req && message.req.body && typeof message.req.body === 'object') {\n if (message.req.body.schema !== undefined) {\n return message.req.body.schema;\n }\n }\n\n if (message && message.payload !== undefined) {\n return message.payload;\n }\n\n throw new Error('未找到schema,请在请求体的schema字段或msg.payload中提供');\n}\n\nfunction parseSchema(source) {\n if (typeof source === 'string') {\n try {\n return JSON.parse(source);\n } catch (error) {\n throw new Error(`JSON 解析失败:${error.message}`);\n }\n }\n\n if (!source || typeof source !== 'object') {\n throw new Error('schema 必须是 DMS 定义对象或 JSON 字符串');\n }\n\n return source;\n}\n\nfunction buildComponentSchema(resourceName, dmsSchema) {\n const { properties = {}, required = [], groupView, identityId, naturalKey, defaultShow } = dmsSchema;\n const openApiProps = {};\n\n for (const [propName, propSchema] of Object.entries(properties)) {\n openApiProps[propName] = mapProperty(propSchema);\n }\n\n return {\n type: 'object',\n required: Array.isArray(required) ? required.slice() : [],\n properties: openApiProps,\n description: dmsSchema.description || dmsSchema.title || resourceName,\n 'x-dms-groupView': groupView || undefined,\n 'x-dms-identityId': identityId || undefined,\n 'x-dms-naturalKey': naturalKey || undefined,\n 'x-dms-defaultShow': defaultShow || undefined,\n };\n}\n\nfunction mapProperty(propSchema) {\n if (!propSchema || typeof propSchema !== 'object') {\n return { type: 'string' };\n }\n\n const result = {};\n\n const type = normalizeType(propSchema.type);\n result.type = type.type;\n if (type.format) {\n result.format = type.format;\n }\n if (type.items) {\n result.items = type.items;\n }\n\n if (propSchema.description) {\n result.description = propSchema.description;\n } else if (propSchema.title) {\n result.description = propSchema.title;\n }\n\n if (propSchema.enum) {\n result.enum = propSchema.enum.slice();\n }\n\n if (propSchema.default !== undefined) {\n result.default = propSchema.default;\n }\n\n if (propSchema.mask) {\n result['x-dms-mask'] = propSchema.mask;\n }\n if (propSchema.geom) {\n result['x-dms-geom'] = propSchema.geom;\n }\n if (propSchema.title) {\n result['x-dms-title'] = propSchema.title;\n }\n if (propSchema.type) {\n result['x-dms-originalType'] = propSchema.type;\n }\n\n return result;\n}\n\nfunction normalizeType(typeValue) {\n const normalized = typeof typeValue === 'string' ? typeValue.toLowerCase() : undefined;\n\n switch (normalized) {\n case 'number':\n case 'integer':\n case 'long':\n case 'float':\n case 'double':\n return { type: 'number' };\n case 'boolean':\n return { type: 'boolean' };\n case 'array':\n return { type: 'array', items: { type: 'string' } };\n case 'date':\n return { type: 'string', format: 'date' };\n case 'date-time':\n return { type: 'string', format: 'date-time' };\n case 'object':\n return { type: 'object' };\n case 'string':\n default:\n return { type: 'string' };\n }\n}\n\nfunction buildCrudPaths({ collectionName, resourceName, identityField, identitySchema }) {\n const ref = `#/components/schemas/${resourceName}`;\n const collectionPath = `/${collectionName}`;\n const itemPath = `/${collectionName}/{${identityField}}`;\n\n return {\n [collectionPath]: {\n get: {\n operationId: `list${resourceName}s`,\n summary: `List ${resourceName} resources`,\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: {\n type: 'array',\n items: { $ref: ref },\n },\n },\n },\n },\n },\n },\n \n post: {\n operationId: `create${resourceName}`,\n summary: `Create a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n responses: {\n 201: {\n description: 'Created',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n \n },\n [itemPath]: {\n parameters: [\n {\n name: identityField,\n in: 'path',\n required: true,\n schema: identitySchema,\n },\n ],\n get: {\n operationId: `get${resourceName}`,\n summary: `Get a single ${resourceName}`,\n responses: {\n 200: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n 404: { description: `${resourceName} not found` },\n },\n },\n \n put: {\n operationId: `update${resourceName}`,\n summary: `Update a ${resourceName}`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n responses: {\n 200: {\n description: 'Successful update',\n content: {\n 'application/json': {\n schema: { $ref: ref },\n },\n },\n },\n },\n },\n delete: {\n operationId: `delete${resourceName}`,\n summary: `Delete a ${resourceName}`,\n responses: {\n 204: { description: 'Deleted' },\n },\n },\n \n },\n };\n}\n\nfunction pascalCase(input) {\n return input\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .split(' ')\n .filter(Boolean)\n .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join('') || 'Resource';\n}\n\nfunction kebabCase(input) {\n return input\n .replace(/([a-z])([A-Z])/g, '$1-$2')\n .replace(/[^a-zA-Z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .toLowerCase() || 'resource';\n}\n\nfunction pluralize(word) {\n if (word.endsWith('s')) {\n return word;\n }\n if (word.endsWith('y')) {\n return word.slice(0, -1) + 'ies';\n }\n return `${word}s`;\n}\n\nfunction clone(value) {\n return JSON.parse(JSON.stringify(value));\n}\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -1180,7 +1180,8 @@ "wires": [ [ "dfe6ed572461c4a5", - "5231ed8a796d6f17" + "5231ed8a796d6f17", + "6ab9403df07fcefa" ] ] }, @@ -1274,7 +1275,7 @@ "initialize": "", "finalize": "", "libs": [], - "x": 460, + "x": 480, "y": 320, "wires": [ [ @@ -1328,5 +1329,80 @@ "f414ed448fcf544b" ] ] + }, + { + "id": "6ab9403df07fcefa", + "type": "function", + "z": "78d15f59dee4b6d8", + "name": "参数生成器", + "func": "'use strict';\nmsg.operationId =\"createResource\"\n/**\n * 根据 OpenAPI (Swagger) 规范为指定接口生成示例请求参数。\n *\n * 输入:\n * - msg.oas_def / msg.payload / msg.swagger: OpenAPI 3.x 文档对象或 JSON 字符串\n * - msg.operationId / msg.req.body.operationId: 目标 operationId(优先级最高)\n * - msg.path + msg.method: 目标路径与方法(可选)\n * - 如果以上字段均缺失且文档仅包含一个 operation,则默认使用该 operation\n *\n * 输出:\n * - msg.mock: {\n * operationId,\n * method,\n * path,\n * mediaType,\n * pathParams: {},\n * query: {},\n * headers: {},\n * cookies: {},\n * body: <示例请求体或 undefined>\n * }\n * - msg.payload 同步为 msg.mock 便于调试\n */\n\nlet openApi;\ntry {\n openApi = extractOpenApiDocument(msg);\n} catch (error) {\n node.error(`Swagger 自动填充初始化失败:${error.message}`, msg);\n msg.error = error.message;\n return msg;\n}\n\nconst operationCtx = resolveOperationContext(msg, openApi);\nif (!operationCtx.operation) {\n const message = `未找到匹配的接口定义(operationId=${operationCtx.requestedOperationId || '未指定'}, path=${operationCtx.requestedPath || '未指定'}, method=${operationCtx.requestedMethod || '未指定'})`;\n node.error(message, msg);\n msg.error = message;\n return msg;\n}\n\nconst parameterSamples = buildParameterSamples(operationCtx, openApi);\nconst bodySample = buildRequestBodySample(operationCtx.operation, openApi);\n\nmsg.mock = Object.assign({}, msg.mock, {\n operationId: operationCtx.operation.operationId || operationCtx.requestedOperationId || '',\n method: (operationCtx.method || '').toUpperCase(),\n path: operationCtx.path || '',\n mediaType: bodySample.mediaType,\n pathParams: parameterSamples.path,\n query: parameterSamples.query,\n headers: parameterSamples.header,\n cookies: parameterSamples.cookie,\n body: bodySample.payload,\n});\n\nmsg.payload = msg.mock;\nmsg.mockCandidates = operationCtx.candidates;\nif (operationCtx.autoSelected) {\n msg.mockAutoSelected = true;\n}\nreturn msg;\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 buildParameterSamples(operationContext, apiDoc) {\n const aggregated = {\n path: {},\n query: {},\n header: {},\n cookie: {},\n };\n\n const seen = new Set();\n const paramSources = [];\n if (Array.isArray(operationContext.pathItem && operationContext.pathItem.parameters)) {\n paramSources.push(operationContext.pathItem.parameters);\n }\n if (Array.isArray(operationContext.operation.parameters)) {\n paramSources.push(operationContext.operation.parameters);\n }\n\n for (const source of paramSources) {\n for (const param of source) {\n if (!param || typeof param !== 'object') {\n continue;\n }\n const name = param.name || '';\n const location = (param.in || 'query').toLowerCase();\n const key = `${location}:${name}`;\n if (!name || seen.has(key)) {\n continue;\n }\n seen.add(key);\n\n const resolvedParam = resolveMaybeRef(param, apiDoc);\n const targetBucket = aggregated[location] || aggregated.query;\n targetBucket[name] = generateParameterSample(resolvedParam, apiDoc, location);\n }\n }\n\n return aggregated;\n}\n\nfunction generateParameterSample(param, apiDoc, location) {\n if (!param || typeof param !== 'object') {\n return 'sample';\n }\n\n const example = pickExample(param);\n if (example !== undefined) {\n return example;\n }\n\n const schema = resolveMaybeRef(param.schema, apiDoc);\n const sample = generateSampleValue(schema, apiDoc);\n if (sample !== undefined) {\n return sample;\n }\n\n // fallback\n switch (location) {\n case 'path':\n return 'sample-id';\n case 'header':\n return 'sample-header';\n case 'cookie':\n return 'sample-cookie';\n default:\n return 'sample';\n }\n}\n\nfunction buildRequestBodySample(operation, apiDoc) {\n const resolvedBody = resolveMaybeRef(operation.requestBody, apiDoc);\n if (!resolvedBody || typeof resolvedBody !== 'object') {\n return { payload: undefined, mediaType: undefined };\n }\n\n const content = resolvedBody.content;\n if (!content || typeof content !== 'object') {\n return { payload: undefined, mediaType: undefined };\n }\n\n const preferredMediaTypes = [\n 'application/json',\n 'application/*+json',\n 'application/x-www-form-urlencoded',\n 'multipart/form-data',\n 'text/plain',\n ];\n\n let chosenMediaType = null;\n for (const mediaType of preferredMediaTypes) {\n if (content[mediaType]) {\n chosenMediaType = mediaType;\n break;\n }\n }\n if (!chosenMediaType) {\n const mediaKeys = Object.keys(content);\n chosenMediaType = mediaKeys.length > 0 ? mediaKeys[0] : null;\n }\n if (!chosenMediaType) {\n return { payload: undefined, mediaType: undefined };\n }\n\n const mediaObject = content[chosenMediaType];\n if (!mediaObject || typeof mediaObject !== 'object') {\n return { payload: undefined, mediaType: chosenMediaType };\n }\n\n if (mediaObject.example !== undefined) {\n return { payload: clone(mediaObject.example), mediaType: chosenMediaType };\n }\n if (mediaObject.examples && typeof mediaObject.examples === 'object') {\n const firstExample = Object.values(mediaObject.examples)[0];\n if (firstExample && typeof firstExample === 'object' && firstExample.value !== undefined) {\n return { payload: clone(firstExample.value), mediaType: chosenMediaType };\n }\n }\n\n const schema = resolveMaybeRef(mediaObject.schema, apiDoc);\n const payload = generateSampleValue(schema, apiDoc);\n return { payload, mediaType: chosenMediaType };\n}\n\nfunction pickExample(node) {\n if (!node || typeof node !== 'object') {\n return undefined;\n }\n if (node.example !== undefined) {\n return clone(node.example);\n }\n if (node.examples && typeof node.examples === 'object') {\n const first = Object.values(node.examples)[0];\n if (first && typeof first === 'object' && first.value !== undefined) {\n return clone(first.value);\n }\n }\n if (node.default !== undefined) {\n return clone(node.default);\n }\n return undefined;\n}\n\nfunction generateSampleValue(schema, apiDoc, depth = 0, seenRefs = new Set()) {\n if (!schema || typeof schema !== 'object') {\n return undefined;\n }\n if (depth > 8) {\n return undefined;\n }\n\n if (schema.example !== undefined) {\n return clone(schema.example);\n }\n if (schema.default !== undefined) {\n return clone(schema.default);\n }\n if (schema.const !== undefined) {\n return clone(schema.const);\n }\n if (Array.isArray(schema.enum) && schema.enum.length > 0) {\n return clone(schema.enum[0]);\n }\n\n if (schema.$ref) {\n if (seenRefs.has(schema.$ref)) {\n return undefined;\n }\n seenRefs.add(schema.$ref);\n const resolved = resolveMaybeRef(schema, apiDoc, seenRefs);\n return generateSampleValue(resolved, apiDoc, depth + 1, seenRefs);\n }\n\n if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {\n return generateSampleValue(schema.oneOf[0], apiDoc, depth + 1, seenRefs);\n }\n if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {\n return generateSampleValue(schema.anyOf[0], apiDoc, depth + 1, seenRefs);\n }\n if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {\n const merged = schema.allOf\n .map(part => resolveMaybeRef(part, apiDoc, seenRefs))\n .filter(part => part && typeof part === 'object')\n .reduce((acc, part) => Object.assign(acc, part), {});\n return generateSampleValue(merged, apiDoc, depth + 1, seenRefs);\n }\n\n const type = inferSchemaType(schema);\n switch (type) {\n case 'object': {\n const result = {};\n if (schema.properties && typeof schema.properties === 'object') {\n for (const [key, value] of Object.entries(schema.properties)) {\n const resolvedChild = resolveMaybeRef(value, apiDoc, seenRefs);\n const childSample = generateSampleValue(resolvedChild, apiDoc, depth + 1, seenRefs);\n if (childSample !== undefined) {\n result[key] = childSample;\n }\n }\n }\n const required = Array.isArray(schema.required) ? schema.required : [];\n for (const propertyName of required) {\n if (!Object.prototype.hasOwnProperty.call(result, propertyName)) {\n result[propertyName] = pickFallbackByFormat({ type: 'string' });\n }\n }\n return Object.keys(result).length > 0 ? result : {};\n }\n case 'array': {\n const itemSchema = resolveMaybeRef(schema.items, apiDoc, seenRefs) || { type: 'string' };\n const itemSample = generateSampleValue(itemSchema, apiDoc, depth + 1, seenRefs);\n return itemSample !== undefined ? [itemSample] : [];\n }\n case 'integer':\n return Number.isInteger(schema.minimum) ? schema.minimum : 1;\n case 'number':\n if (schema.minimum !== undefined) {\n return typeof schema.minimum === 'number' ? schema.minimum : 0;\n }\n return 1;\n case 'boolean':\n return true;\n case 'string':\n default:\n return pickFallbackByFormat(schema);\n }\n}\n\nfunction inferSchemaType(schema) {\n if (!schema || typeof schema !== 'object') {\n return 'string';\n }\n if (schema.type) {\n return schema.type;\n }\n if (schema.properties) {\n return 'object';\n }\n if (schema.items) {\n return 'array';\n }\n return 'string';\n}\n\nfunction pickFallbackByFormat(schema) {\n const format = schema && schema.format ? schema.format.toLowerCase() : null;\n switch (format) {\n case 'date':\n return '2025-01-01';\n case 'date-time':\n return '2025-01-01T00:00:00Z';\n case 'email':\n return 'user@example.com';\n case 'uuid':\n return '00000000-0000-4000-8000-000000000000';\n case 'uri':\n case 'url':\n return 'https://example.com/resource';\n case 'byte':\n return Buffer.from('sample').toString('base64');\n case 'binary':\n return '';\n default:\n break;\n }\n\n const pattern = schema && schema.pattern;\n if (pattern && /[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(pattern)) {\n return '2025-01-01';\n }\n\n return 'sample';\n}\n\nfunction resolveMaybeRef(node, apiDoc, seenRefs = new Set()) {\n if (!node || typeof node !== 'object') {\n return node;\n }\n if (!node.$ref) {\n return node;\n }\n\n const ref = node.$ref;\n if (seenRefs.has(ref)) {\n return {};\n }\n seenRefs.add(ref);\n\n const resolved = resolveRef(ref, apiDoc);\n if (!resolved || typeof resolved !== 'object') {\n return node;\n }\n\n const remainder = Object.assign({}, node);\n delete remainder.$ref;\n\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 clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 470, + "y": 400, + "wires": [ + [ + "6e56a2ccd9fcaacc", + "60ce224bd2ed8e69" + ] + ] + }, + { + "id": "6e56a2ccd9fcaacc", + "type": "debug", + "z": "78d15f59dee4b6d8", + "name": "参数展示", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "mock", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 700, + "y": 360, + "wires": [] + }, + { + "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", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 610, + "y": 440, + "wires": [ + [ + "e87c2057c28d5eee" + ] + ] + }, + { + "id": "e87c2057c28d5eee", + "type": "debug", + "z": "78d15f59dee4b6d8", + "name": "llm参数展示", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "mock", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 790, + "y": 420, + "wires": [] } ] \ No newline at end of file