From 5a4b41b0f53add2196b2a7faa90e743041027a6f Mon Sep 17 00:00:00 2001 From: ruoyunbai <1153712410@qq.com> Date: Tue, 4 Nov 2025 16:40:07 +0800 Subject: [PATCH] flow --- .node-red-data/projects/zsy/flows.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.node-red-data/projects/zsy/flows.json b/.node-red-data/projects/zsy/flows.json index 91e8d1d..846bd44 100644 --- a/.node-red-data/projects/zsy/flows.json +++ b/.node-red-data/projects/zsy/flows.json @@ -2090,7 +2090,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';\nconst BASE_PREFIX = 'https://www.dev.ideas.cnpc/api/dms';\nconst DEFAULT_SERVICE_ID = 'well_kd_wellbore_ideas01';\nconst DEFAULT_API_SEGMENT = 'v1';\nconst FALLBACK_BASE_URL = `${BASE_PREFIX}/${DEFAULT_SERVICE_ID}/${DEFAULT_API_SEGMENT}`;\nconst FALLBACK_HEADERS = {\n 'Content-Type': 'application/json',\n 'Authorization': '1',\n 'Dataregion': 'ZZLH',\n};\nconst SERVICE_DOMAIN_CATALOG = {\n 'well_kd_wellbore_ideas01': {\n name: '井筒',\n id: 'well_kd_wellbore_ideas01',\n keywords: ['wb', 'wb_dr', 'wb_ml', 'wb_wl', 'wb_tp', 'wb_dh', 'wb_fr'],\n },\n 'geo_kd_res_ideas01': {\n name: '采油气',\n id: 'geo_kd_res_ideas01',\n keywords: ['pc', 'pc_op', 'pc_oe', 'pc_ge'],\n },\n 'kd_cr_ideas01': {\n name: '分析化验',\n id: 'kd_cr_ideas01',\n keywords: ['cr', 'cr_se'],\n },\n 'kd_rs_ideas01': {\n name: '油气藏',\n id: 'kd_rs_ideas01',\n keywords: ['rs', 'rs_rd', 'rs_rm', 'rs_gs', 'rs_in'],\n },\n};\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\nconst crudConfig = Object.assign({}, msg.crudConfig || {});\n\nconst dmsMeta = extractDmsMeta(msg);\nmsg.dms_meta = dmsMeta;\n\nconst serviceInfo = resolveServiceInfo(dmsMeta && dmsMeta.domain);\nconst apiVersionSegment = DEFAULT_API_SEGMENT;\nconst serviceId = serviceInfo.id;\n\nconst { resourceSegment, versionSegment } = deriveResourceSegments(dmsMeta, schema, collectionName);\nconst listPath = buildListPath(resourceSegment, versionSegment);\nconst singlePath = buildSinglePath(resourceSegment);\nconst detailPath = buildDetailPath(resourceSegment, versionSegment, identityField);\n\nif (!crudConfig.baseUrl) {\n crudConfig.baseUrl = `${BASE_PREFIX}/${serviceId}/${apiVersionSegment}`;\n}\n\nconst headers = Object.assign({}, FALLBACK_HEADERS, crudConfig.headers || {});\nconst dataRegionValue = extractDataRegion(schema, headers.Dataregion);\n\nif (dataRegionValue) {\n headers.Dataregion = dataRegionValue;\n crudConfig.dataRegion = dataRegionValue;\n}\n\ncrudConfig.headers = headers;\ncrudConfig.identityField = crudConfig.identityField || identityField;\n\ncrudConfig.service = crudConfig.service || {\n id: serviceId,\n name: serviceInfo.name,\n};\n\ncrudConfig.list = crudConfig.list || {};\nif (!crudConfig.list.path) {\n crudConfig.list.path = listPath;\n}\ncrudConfig.list.method = crudConfig.list.method || 'GET';\n\ncrudConfig.create = crudConfig.create || {};\nif (!crudConfig.create.path) {\n crudConfig.create.path = singlePath;\n}\ncrudConfig.create.method = crudConfig.create.method || 'POST';\n\ncrudConfig.delete = crudConfig.delete || {};\nif (!crudConfig.delete.path) {\n crudConfig.delete.path = singlePath;\n}\ncrudConfig.delete.method = crudConfig.delete.method || 'POST';\n\ncrudConfig.detailPath = crudConfig.detailPath || detailPath;\ncrudConfig.version = crudConfig.version || versionSegment;\n\nmsg.crudConfig = crudConfig;\nmsg.headers = Object.assign({}, headers);\n\nmsg.oas_def = openApiDocument;\nmsg.payload = openApiDocument;\nreturn msg;\n\nfunction extractDataRegion(dmsSchema, fallback) {\n if (!dmsSchema || typeof dmsSchema !== 'object') {\n return fallback;\n }\n\n if (typeof dmsSchema.dataRegion === 'string' && dmsSchema.dataRegion.trim()) {\n return dmsSchema.dataRegion.trim();\n }\n\n if (dmsSchema.defaultShow && Array.isArray(dmsSchema.defaultShow)) {\n const candidate = dmsSchema.defaultShow.find(item => item && typeof item === 'string' && item.toLowerCase().includes('dataregion'));\n if (candidate) {\n return fallback;\n }\n }\n\n const props = dmsSchema.properties;\n if (props && typeof props === 'object' && props.dataRegion) {\n if (typeof props.dataRegion.default === 'string' && props.dataRegion.default.trim()) {\n return props.dataRegion.default.trim();\n }\n }\n\n return fallback;\n}\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 if (looksLikeDmsSchema(message.req.body)) {\n return message.req.body;\n }\n }\n\n if (message && message.payload !== undefined) {\n if (message.payload && typeof message.payload === 'object') {\n if (message.payload.schema !== undefined) {\n return message.payload.schema;\n }\n if (looksLikeDmsSchema(message.payload)) {\n return message.payload;\n }\n }\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\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n\nfunction extractDmsMeta(message) {\n const candidateReq = message && message.req && message.req.body && message.req.body.dms_meta;\n const parsedReq = parseMetaCandidate(candidateReq);\n if (parsedReq) {\n return parsedReq;\n }\n\n const candidateMsg = message && message.dms_meta;\n const parsedMsg = parseMetaCandidate(candidateMsg);\n if (parsedMsg) {\n return parsedMsg;\n }\n\n return null;\n}\n\nfunction parseMetaCandidate(candidate) {\n if (!candidate) {\n return null;\n }\n if (typeof candidate === 'string') {\n try {\n const value = JSON.parse(candidate);\n return parseMetaCandidate(value);\n } catch (err) {\n return null;\n }\n }\n if (typeof candidate === 'object') {\n return clone(candidate);\n }\n return null;\n}\n\nfunction resolveServiceInfo(domain) {\n if (!domain || typeof domain !== 'string') {\n return SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n }\n const normalized = domain.toLowerCase();\n let best = null;\n for (const entry of Object.values(SERVICE_DOMAIN_CATALOG)) {\n const matched = entry.keywords.some(keyword => {\n if (typeof keyword !== 'string') return false;\n const normKeyword = keyword.toLowerCase();\n return normalized.includes(normKeyword) || normKeyword.includes(normalized);\n });\n if (matched) {\n best = entry;\n break;\n }\n }\n return best || SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n}\n\nfunction deriveResourceSegments(meta, schema, collectionName) {\n const defaultResource = (collectionName || '').replace(/^\\//, '');\n let resourceSegment = defaultResource;\n let versionSegment = null;\n\n if (meta && typeof meta === 'object') {\n if (typeof meta.id === 'string' && meta.id.trim()) {\n const parts = meta.id.trim().split('.');\n if (parts.length > 0 && parts[0]) {\n resourceSegment = parts[0];\n }\n if (parts.length > 1) {\n versionSegment = parts.slice(1).join('.');\n }\n } else if (typeof meta.name === 'string' && meta.name.trim()) {\n resourceSegment = meta.name.trim();\n }\n }\n\n if (!resourceSegment && schema && schema.title) {\n resourceSegment = kebabCase(schema.title);\n }\n\n return {\n resourceSegment: resourceSegment || defaultResource || 'resource',\n versionSegment: versionSegment,\n };\n}\n\nfunction buildListPath(resourceSegment, versionSegment) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}`;\n }\n return `/${resourceSegment}`;\n}\n\nfunction buildSinglePath(resourceSegment) {\n return `/${resourceSegment}`;\n}\n\nfunction buildDetailPath(resourceSegment, versionSegment, identityField) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}/{${identityField}}`;\n }\n return `/${resourceSegment}/{${identityField}}`;\n}\n\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n\n\n\n", + "func": "'use strict';\n\nconst DEFAULT_OPENAPI_VERSION = '3.0.1';\nconst DEFAULT_API_VERSION = '1.0.0';\nconst BASE_PREFIX = 'https://www.dev.ideas.cnpc/api/dms';\nconst DEFAULT_SERVICE_ID = 'well_kd_wellbore_ideas01';\nconst DEFAULT_API_SEGMENT = 'v1';\nconst FALLBACK_BASE_URL = `${BASE_PREFIX}/${DEFAULT_SERVICE_ID}/${DEFAULT_API_SEGMENT}`;\nconst FALLBACK_HEADERS = {\n 'Content-Type': 'application/json',\n 'Authorization': '1',\n 'Dataregion': 'ZZLH',\n};\nconst SERVICE_DOMAIN_CATALOG = {\n 'well_kd_wellbore_ideas01': {\n name: '井筒',\n id: 'well_kd_wellbore_ideas01',\n keywords: ['wb', 'wb_dr', 'wb_ml', 'wb_wl', 'wb_tp', 'wb_dh', 'wb_fr'],\n },\n 'geo_kd_res_ideas01': {\n name: '采油气',\n id: 'geo_kd_res_ideas01',\n keywords: ['pc', 'pc_op', 'pc_oe', 'pc_ge'],\n },\n 'kd_cr_ideas01': {\n name: '分析化验',\n id: 'kd_cr_ideas01',\n keywords: ['cr', 'cr_se'],\n },\n 'kd_rs_ideas01': {\n name: '油气藏',\n id: 'kd_rs_ideas01',\n keywords: ['rs', 'rs_rd', 'rs_rm', 'rs_gs', 'rs_in'],\n },\n};\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\nconst crudConfig = Object.assign({}, msg.crudConfig || {});\n\nconst dmsMeta = extractDmsMeta(msg);\nmsg.dms_meta = dmsMeta;\n\nconst serviceInfo = resolveServiceInfo(dmsMeta && dmsMeta.domain);\nconst apiVersionSegment = DEFAULT_API_SEGMENT;\nconst serviceId = serviceInfo.id;\n\nconst { resourceSegment, versionSegment } = deriveResourceSegments(dmsMeta, schema, collectionName);\nconst listPath = buildListPath(resourceSegment, versionSegment);\nconst singlePath = buildSinglePath(resourceSegment);\nconst detailPath = buildDetailPath(resourceSegment, versionSegment, identityField);\n\nif (!crudConfig.baseUrl) {\n crudConfig.baseUrl = `${BASE_PREFIX}/${serviceId}/${apiVersionSegment}`;\n}\n\nconst headers = Object.assign({}, FALLBACK_HEADERS, crudConfig.headers || {});\nconst dataRegionValue = extractDataRegion(schema, headers.Dataregion);\n\nif (dataRegionValue) {\n headers.Dataregion = dataRegionValue;\n crudConfig.dataRegion = dataRegionValue;\n}\n\ncrudConfig.headers = headers;\ncrudConfig.identityField = crudConfig.identityField || identityField;\n\ncrudConfig.service = crudConfig.service || {\n id: serviceId,\n name: serviceInfo.name,\n};\n\ncrudConfig.list = crudConfig.list || {};\nif (!crudConfig.list.path) {\n crudConfig.list.path = listPath;\n}\ncrudConfig.list.method = crudConfig.list.method || 'GET';\n\ncrudConfig.create = crudConfig.create || {};\nif (!crudConfig.create.path) {\n crudConfig.create.path = singlePath;\n}\ncrudConfig.create.method = crudConfig.create.method || 'POST';\n\ncrudConfig.delete = crudConfig.delete || {};\nif (!crudConfig.delete.path) {\n crudConfig.delete.path = singlePath;\n}\ncrudConfig.delete.method = crudConfig.delete.method || 'POST';\n\ncrudConfig.detailPath = crudConfig.detailPath || detailPath;\ncrudConfig.version = crudConfig.version || versionSegment;\n\nmsg.crudConfig = crudConfig;\nmsg.headers = Object.assign({}, headers);\n\nmsg.oas_def = openApiDocument;\nmsg.payload = openApiDocument;\nreturn msg;\n\nfunction extractDataRegion(dmsSchema, fallback) {\n if (!dmsSchema || typeof dmsSchema !== 'object') {\n return fallback;\n }\n\n if (typeof dmsSchema.dataRegion === 'string' && dmsSchema.dataRegion.trim()) {\n return dmsSchema.dataRegion.trim();\n }\n\n if (dmsSchema.defaultShow && Array.isArray(dmsSchema.defaultShow)) {\n const candidate = dmsSchema.defaultShow.find(item => item && typeof item === 'string' && item.toLowerCase().includes('dataregion'));\n if (candidate) {\n return fallback;\n }\n }\n\n const props = dmsSchema.properties;\n if (props && typeof props === 'object' && props.dataRegion) {\n if (typeof props.dataRegion.default === 'string' && props.dataRegion.default.trim()) {\n return props.dataRegion.default.trim();\n }\n }\n\n return fallback;\n}\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 if (looksLikeDmsSchema(message.req.body)) {\n return message.req.body;\n }\n }\n\n if (message && message.payload !== undefined) {\n if (message.payload && typeof message.payload === 'object') {\n if (message.payload.schema !== undefined) {\n return message.payload.schema;\n }\n if (looksLikeDmsSchema(message.payload)) {\n return message.payload;\n }\n }\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\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n\nfunction extractDmsMeta(message) {\n const candidateReq = message && message.req && message.req.body && message.req.body.dms_meta;\n const parsedReq = parseMetaCandidate(candidateReq);\n if (parsedReq) {\n return parsedReq;\n }\n\n const candidateMsg = message && message.dms_meta;\n const parsedMsg = parseMetaCandidate(candidateMsg);\n if (parsedMsg) {\n return parsedMsg;\n }\n\n return null;\n}\n\nfunction parseMetaCandidate(candidate) {\n if (!candidate) {\n return null;\n }\n if (typeof candidate === 'string') {\n try {\n const value = JSON.parse(candidate);\n return parseMetaCandidate(value);\n } catch (err) {\n return null;\n }\n }\n if (typeof candidate === 'object') {\n return clone(candidate);\n }\n return null;\n}\n\nfunction resolveServiceInfo(domain) {\n if (!domain || typeof domain !== 'string') {\n return SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n }\n const normalized = domain.toLowerCase();\n let best = null;\n for (const entry of Object.values(SERVICE_DOMAIN_CATALOG)) {\n const matched = entry.keywords.some(keyword => {\n if (typeof keyword !== 'string') return false;\n const normKeyword = keyword.toLowerCase();\n return normalized.includes(normKeyword) || normKeyword.includes(normalized);\n });\n if (matched) {\n best = entry;\n break;\n }\n }\n return best || SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID];\n}\n\nfunction deriveResourceSegments(meta, schema, collectionName) {\n const defaultResource = (collectionName || '').replace(/^\\//, '');\n let resourceSegment = defaultResource;\n let versionSegment = null;\n\n if (meta && typeof meta === 'object') {\n if (typeof meta.id === 'string' && meta.id.trim()) {\n const parts = meta.id.trim().split('.');\n if (parts.length > 0 && parts[0]) {\n resourceSegment = parts[0];\n }\n if (parts.length > 1) {\n versionSegment = parts.slice(1).join('.');\n }\n } else if (typeof meta.name === 'string' && meta.name.trim()) {\n resourceSegment = meta.name.trim();\n }\n }\n\n if (!resourceSegment && schema && schema.title) {\n resourceSegment = kebabCase(schema.title);\n }\n\n return {\n resourceSegment: resourceSegment || defaultResource || 'resource',\n versionSegment: versionSegment,\n };\n}\n\nfunction buildListPath(resourceSegment, versionSegment) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}`;\n }\n return `/${resourceSegment}`;\n}\n\nfunction buildSinglePath(resourceSegment) {\n return `/${resourceSegment}`;\n}\n\nfunction buildDetailPath(resourceSegment, versionSegment, identityField) {\n if (versionSegment) {\n return `/${resourceSegment}/${versionSegment}/{${identityField}}`;\n }\n return `/${resourceSegment}/{${identityField}}`;\n}\n\nfunction looksLikeDmsSchema(candidate) {\n if (!candidate || typeof candidate !== 'object') {\n return false;\n }\n if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) {\n return true;\n }\n if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') {\n return true;\n }\n return false;\n}\n\n\n// 需要你加一个逻辑,注入的时候多传入一个\n// msg.req.body.dms_meta={\"flow_state\":\"5\",\"domain\":\"wb_cd\",\"name\":\"cd_well\",\"add_tree\":\"Y\",\"id\":\"cd_well.1.0.0\",\"create_us\n// er\":\"admin\",\"title\":\"井基本信息\",\"type\":\"master-data\",\"version\":\"1.0.0\",\"update_date\":\"2025-10-21\n// 10:45:38\",\"tags\":[\"主数据\"]} 并且还需要在脚本里保存一个信息(一个json,不需要用户传,可以写死){\n// \"well_kd_wellbore_ideas01\": {\n// \"name\": \"井筒\",\n// \"id\":\"well_kd_wellbore_ideas01\",\n// \"keywords\":[\n// \"wb\",\n// \"wb_dr\",\n// \"wb_ml\",\n// \"wb_wl\",\n// \"wb_tp\",\n// \"wb_dh\",\n// \"wb_fr\"\n// ]\n// },\n// \"geo_kd_res_ideas01\": {\n// \"name\": \"采油气\",\n// \"id\":\"geo_kd_res_ideas01\",\n// \"keywords\":[\n// \"pc\",\n// \"pc_op\",\n// \"pc_oe\",\n// \"pc_ge\"\n// ]\n// },\n// \"kd_cr_ideas01\": {\n// \"name\": \"分析化验\",\n// \"id\": \"kd_cr_ideas01\",\n// \"keywords\":[\n// \"cr\",\n// \"cr_se\"\n// ]\n// },\n// \"kd_rs_ideas01\": {\n// \"name\": \"油气藏\",\n// \"id\": \"kd_cr_ideas01\",\n// \"keywords\":[\n// \"rs\",\n// \"rs_rd\",\n// \"rs_rm\",\n// \"rs_gs\",\n// \"rs_in\"\n\n// ]\n// }\n// }\n// 然后就可以用meta和这个json生成url了\n// 以cd_well为例,首先根据meta里的\"domain\":\"wb_cd\",可以去那个json查询到well_kd_wellbore_ideas01(包含或者被包含keywords,这里包含wb)\n// list:https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1/cd_well/1.0.0\n// 是baseurl+well_kd_wellbore_ideas01+v1+ meta里的name也就是cd_well.1.0.0用第一个.分割开,变成/cd_well/1.0.0\n// create:https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1/cd_well\n// delete:https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1/cd_well\n// put:https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1/cd_well\n// 查询详情:https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1/cd_well/1.0.0/{主键}\n// 你把这个逻辑加上\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -2400,7 +2400,7 @@ "type": "function", "z": "78d15f59dee4b6d8", "name": "构建CRUD计划", - "func": "'use strict';\n\n/**\n * 构建 CRUD 执行动作所需的指令集合,保存于 msg.crudFlow。\n * 约定 operationId 采用 dms→oas 转换后默认的命名:\n * lists / create / delete\n * 允许使用 msg.crudConfig.* 进行覆盖。\n */\nconst FALLBACK_BASE_URL = 'https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1';\nconst DEFAULT_LIST_PAYLOAD = {\n version: '1.0.0',\n data: [],\n pageNo: 1,\n pageSize: 20,\n isSearchCount: true,\n};\nconst DEFAULT_CREATE_SAMPLE = {\n version: '1.0.0',\n act: -1,\n data: [\n {\n dsid: 'testid2',\n wellId: 'WELL-zzlhTEST-002',\n wellCommonName: 'zzlh测试用井名',\n wellLegalName: 'zzlh-test-01',\n wellPurpose: '开发井',\n wellType: '直井',\n dataRegion: 'ZZLH',\n projectId: 'PROJ-ZZLH-001',\n projectName: 'zzlh测试地质单元',\n orgId: 'ORG-ZZLH-01',\n orgName: 'zzlh采油厂',\n bsflag: 1,\n wellState: '生产中',\n spudDate: '2024-01-15',\n completionDate: '2024-05-20',\n prodDate: '2024-06-01',\n egl: 145.5,\n kbd: 5.2,\n kb: 150.7,\n actualXAxis: 550123.45,\n actualYAxis: 4998765.32,\n coordinateSystemName: 'zzlh测试坐标系',\n geoDescription: '位于zzlh测试区域',\n remarks: '这是一口用于系统测试的生产井。',\n createUserId: 'testuser001',\n createDate: '2025-09-12T10:00:00Z',\n updateUserId: 'testuser001',\n updateDate: '2025-09-12T10:00:00Z',\n },\n ],\n};\nconst DEFAULT_DELETE_TEMPLATE = {\n version: '1.0.0',\n data: ['{{primaryKey}}'],\n};\n\nreturn buildCrudPlan(msg, node);\n\nfunction buildCrudPlan(message, node) {\n const requestBody = getRequestBody(message);\n const crudConfig = mergeDeep({}, requestBody.crudConfig || {}, message.crudConfig || {});\n const apiDoc = normaliseOpenApi(message, crudConfig, requestBody, node);\n const operations = collectOperations(apiDoc);\n\n const listOp = pickOperation('list', operations, crudConfig);\n const createOp = pickOperation('create', operations, crudConfig);\n const deleteOp = pickOperation('delete', operations, crudConfig);\n\n if (!listOp || !createOp || !deleteOp) {\n const missing = [\n !listOp ? 'list' : null,\n !createOp ? 'create' : null,\n !deleteOp ? 'delete' : null,\n ].filter(Boolean).join(', ');\n const errMsg = `未能在 OpenAPI 文档中找到必要的 CRUD 操作:${missing}`;\n node.error(errMsg, message);\n message.error = errMsg;\n return null;\n }\n\n const resourceName = determineResourceName([createOp, listOp, deleteOp]) || 'Resource';\n const identityField = crudConfig.identityField ||\n requestBody.identityField ||\n selectIdentityField(apiDoc, crudConfig) ||\n 'dsid';\n const baseUrl = trimTrailingSlash(\n crudConfig.baseUrl ||\n requestBody.baseUrl ||\n message.baseUrl ||\n FALLBACK_BASE_URL\n );\n const headers = mergeDeep({}, requestBody.headers || {}, crudConfig.headers || {});\n\n const listConfig = mergeDeep(\n {},\n { payload: clone(DEFAULT_LIST_PAYLOAD) },\n requestBody.list || {},\n crudConfig.list || {}\n );\n const createConfig = mergeDeep(\n {},\n { payload: clone(DEFAULT_CREATE_SAMPLE) },\n requestBody.create || {},\n crudConfig.create || {}\n );\n const deleteConfig = mergeDeep(\n {},\n { payloadTemplate: clone(DEFAULT_DELETE_TEMPLATE) },\n requestBody.delete || {},\n crudConfig.delete || {}\n );\n\n if (requestBody.dataRegion) {\n headers.Dataregion = requestBody.dataRegion;\n }\n\n message.identityField = identityField;\n message.crudFlow = {\n resourceName,\n identityField,\n baseUrl,\n headers,\n list: Object.assign({\n operationId: listOp.operationId,\n method: listOp.method,\n path: listOp.path,\n }, listConfig),\n create: Object.assign({\n operationId: createOp.operationId,\n method: createOp.method,\n path: createOp.path,\n samplePayload: clone(DEFAULT_CREATE_SAMPLE),\n }, createConfig),\n delete: Object.assign({\n operationId: deleteOp.operationId,\n method: deleteOp.method,\n path: deleteOp.path,\n }, deleteConfig),\n openapi: apiDoc,\n };\n\n message.oas_def = apiDoc;\n delete message.error;\n return message;\n}\n\nfunction normaliseOpenApi(message, crudConfig, requestBody, node) {\n let candidate =\n crudConfig.openapi ||\n requestBody.openapi ||\n message.oas_def ||\n message.swagger ||\n message.payload;\n if (typeof candidate === 'string') {\n try {\n candidate = JSON.parse(candidate);\n } catch (err) {\n throw new Error(`OpenAPI JSON 解析失败:${err.message}`);\n }\n }\n if (!candidate || typeof candidate !== 'object') {\n throw new Error('未提供合法的 OpenAPI 文档');\n }\n if (!candidate.paths || typeof candidate.paths !== 'object' || Object.keys(candidate.paths).length === 0) {\n throw new Error('OpenAPI 文档缺少 paths 定义');\n }\n return candidate;\n}\n\nfunction collectOperations(apiDoc) {\n const operations = [];\n const allowed = ['get', 'post', 'put', '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 Object.keys(pathItem)) {\n if (!allowed.includes(method.toLowerCase())) {\n continue;\n }\n const operation = pathItem[method];\n if (!operation || typeof operation !== 'object') {\n continue;\n }\n operations.push({\n operationId: operation.operationId || '',\n summary: operation.summary || '',\n description: operation.description || '',\n method: method.toUpperCase(),\n path,\n operation,\n });\n }\n }\n return operations;\n}\n\nfunction pickOperation(kind, operations, crudConfig) {\n const overrideId =\n (crudConfig[kind] && crudConfig[kind].operationId) ||\n crudConfig[`${kind}OperationId`];\n if (overrideId) {\n return operations.find(op => op.operationId === overrideId);\n }\n\n const matcher = getDefaultMatcher(kind);\n let matched = operations.find(op => matcher.test(op.operationId));\n if (matched) {\n return matched;\n }\n\n // 兜底策略\n switch (kind) {\n case 'list':\n return operations.find(op => op.method === 'GET') || null;\n case 'create':\n return operations.find(op => op.method === 'POST') || null;\n case 'delete':\n return operations.find(op => op.method === 'DELETE') ||\n operations.find(op => op.method === 'POST' && /delete/i.test(op.operationId)) ||\n null;\n default:\n return null;\n }\n}\n\nfunction getDefaultMatcher(kind) {\n switch (kind) {\n case 'list':\n return /^list[A-Z].*s$/;\n case 'create':\n return /^create[A-Z].*/;\n case 'delete':\n return /^delete[A-Z].*/;\n default:\n return /^$/;\n }\n}\n\nfunction determineResourceName(candidates) {\n for (const item of candidates) {\n const opId = item && item.operationId ? item.operationId : '';\n let match;\n if ((match = opId.match(/^list([A-Z].*)s$/))) {\n return match[1];\n }\n if ((match = opId.match(/^create([A-Z].*)$/))) {\n return match[1];\n }\n if ((match = opId.match(/^delete([A-Z].*)$/))) {\n return match[1];\n }\n }\n return null;\n}\n\nfunction selectIdentityField(apiDoc, crudConfig) {\n if (crudConfig.identityField) {\n return crudConfig.identityField;\n }\n if (apiDoc.components && apiDoc.components.schemas) {\n for (const schemaName of Object.keys(apiDoc.components.schemas)) {\n const schema = apiDoc.components.schemas[schemaName];\n if (!schema || typeof schema !== 'object') {\n continue;\n }\n const identity = Array.isArray(schema['x-dms-identityId']) ? schema['x-dms-identityId'][0] : null;\n if (identity) {\n return identity;\n }\n }\n }\n if (apiDoc.components && apiDoc.components.schemas) {\n for (const schemaName of Object.keys(apiDoc.components.schemas)) {\n const schema = apiDoc.components.schemas[schemaName];\n if (schema && schema.properties && Object.prototype.hasOwnProperty.call(schema.properties, 'dsid')) {\n return 'dsid';\n }\n }\n }\n return 'dsid';\n}\n\nfunction trimTrailingSlash(url) {\n if (!url) {\n return '';\n }\n return url.replace(/\\/+$/, '');\n}\n\nfunction getRequestBody(message) {\n if (message && message.req && message.req.body && typeof message.req.body === 'object') {\n return message.req.body;\n }\n return {};\n}\n\nfunction clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n\nfunction mergeDeep(target, ...sources) {\n for (const source of sources) {\n if (!isPlainObject(source)) {\n continue;\n }\n for (const key of Object.keys(source)) {\n const value = source[key];\n if (value === undefined) {\n continue;\n }\n if (isPlainObject(value)) {\n const base = isPlainObject(target[key]) ? target[key] : {};\n target[key] = mergeDeep({}, base, value);\n } else {\n target[key] = clone(value);\n }\n }\n }\n return target;\n}\n\nfunction isPlainObject(value) {\n return Object.prototype.toString.call(value) === '[object Object]';\n}\n", + "func": "'use strict';\n\n/**\n * 构建 CRUD 执行动作所需的指令集合,保存于 msg.crudFlow。\n * 约定 operationId 采用 dms→oas 转换后默认的命名:\n * lists / create / delete\n * 允许使用 msg.crudConfig.* 进行覆盖。\n */\nconst FALLBACK_BASE_URL = 'https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1';\nconst DEFAULT_LIST_PAYLOAD = {\n version: '1.0.0',\n data: [],\n pageNo: 1,\n pageSize: 20,\n isSearchCount: true,\n};\nconst DEFAULT_CREATE_SAMPLE = {\n version: '1.0.0',\n act: -1,\n data: [\n {\n dsid: 'testid2',\n wellId: 'WELL-zzlhTEST-002',\n wellCommonName: 'zzlh测试用井名',\n wellLegalName: 'zzlh-test-01',\n wellPurpose: '开发井',\n wellType: '直井',\n dataRegion: 'ZZLH',\n projectId: 'PROJ-ZZLH-001',\n projectName: 'zzlh测试地质单元',\n orgId: 'ORG-ZZLH-01',\n orgName: 'zzlh采油厂',\n bsflag: 1,\n wellState: '生产中',\n spudDate: '2024-01-15',\n completionDate: '2024-05-20',\n prodDate: '2024-06-01',\n egl: 145.5,\n kbd: 5.2,\n kb: 150.7,\n actualXAxis: 550123.45,\n actualYAxis: 4998765.32,\n coordinateSystemName: 'zzlh测试坐标系',\n geoDescription: '位于zzlh测试区域',\n remarks: '这是一口用于系统测试的生产井。',\n createUserId: 'testuser001',\n createDate: '2025-09-12T10:00:00Z',\n updateUserId: 'testuser001',\n updateDate: '2025-09-12T10:00:00Z',\n },\n ],\n};\nconst DEFAULT_DELETE_TEMPLATE = {\n version: '1.0.0',\n data: ['{{primaryKey}}'],\n};\n\nreturn buildCrudPlan(msg, node);\n\nfunction buildCrudPlan(message, node) {\n const requestBody = getRequestBody(message);\n const crudConfig = mergeDeep({}, requestBody.crudConfig || {}, message.crudConfig || {});\n const apiDoc = normaliseOpenApi(message, crudConfig, requestBody, node);\n const operations = collectOperations(apiDoc);\n\n const listOp = pickOperation('list', operations, crudConfig);\n const createOp = pickOperation('create', operations, crudConfig);\n const deleteOp = pickOperation('delete', operations, crudConfig);\n\n if (!listOp || !createOp || !deleteOp) {\n const missing = [\n !listOp ? 'list' : null,\n !createOp ? 'create' : null,\n !deleteOp ? 'delete' : null,\n ].filter(Boolean).join(', ');\n const errMsg = `未能在 OpenAPI 文档中找到必要的 CRUD 操作:${missing}`;\n node.error(errMsg, message);\n message.error = errMsg;\n return null;\n }\n\n const resourceName = determineResourceName([createOp, listOp, deleteOp]) || 'Resource';\n const identityField = crudConfig.identityField ||\n requestBody.identityField ||\n selectIdentityField(apiDoc, crudConfig) ||\n 'dsid';\n const baseUrl = trimTrailingSlash(\n crudConfig.baseUrl ||\n requestBody.baseUrl ||\n message.baseUrl ||\n FALLBACK_BASE_URL\n );\n const headers = mergeDeep({}, requestBody.headers || {}, crudConfig.headers || {});\n\n const listConfig = mergeDeep(\n {},\n { payload: clone(DEFAULT_LIST_PAYLOAD) },\n requestBody.list || {},\n crudConfig.list || {}\n );\n const createConfig = mergeDeep(\n {},\n { payload: clone(DEFAULT_CREATE_SAMPLE) },\n requestBody.create || {},\n crudConfig.create || {}\n );\n const deleteConfig = mergeDeep(\n {},\n { payloadTemplate: clone(DEFAULT_DELETE_TEMPLATE) },\n requestBody.delete || {},\n crudConfig.delete || {}\n );\n\n if (requestBody.dataRegion) {\n headers.Dataregion = requestBody.dataRegion;\n }\n\n message.crudFlow = {\n resourceName,\n identityField,\n baseUrl,\n headers,\n list: Object.assign({\n operationId: listOp.operationId,\n method: listOp.method,\n path: listOp.path,\n }, listConfig),\n create: Object.assign({\n operationId: createOp.operationId,\n method: createOp.method,\n path: createOp.path,\n samplePayload: clone(DEFAULT_CREATE_SAMPLE),\n }, createConfig),\n delete: Object.assign({\n operationId: deleteOp.operationId,\n method: deleteOp.method,\n path: deleteOp.path,\n }, deleteConfig),\n openapi: apiDoc,\n };\n\n delete message.crudConfig;\n if (message.baseUrl) {\n delete message.baseUrl;\n }\n if (message.headers && !Object.keys(message.headers).length) {\n delete message.headers;\n }\n\n message.oas_def = apiDoc;\n delete message.error;\n return message;\n}\n\nfunction normaliseOpenApi(message, crudConfig, requestBody, node) {\n let candidate =\n crudConfig.openapi ||\n requestBody.openapi ||\n message.oas_def ||\n message.swagger ||\n message.payload;\n if (typeof candidate === 'string') {\n try {\n candidate = JSON.parse(candidate);\n } catch (err) {\n throw new Error(`OpenAPI JSON 解析失败:${err.message}`);\n }\n }\n if (!candidate || typeof candidate !== 'object') {\n throw new Error('未提供合法的 OpenAPI 文档');\n }\n if (!candidate.paths || typeof candidate.paths !== 'object' || Object.keys(candidate.paths).length === 0) {\n throw new Error('OpenAPI 文档缺少 paths 定义');\n }\n return candidate;\n}\n\nfunction collectOperations(apiDoc) {\n const operations = [];\n const allowed = ['get', 'post', 'put', '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 Object.keys(pathItem)) {\n if (!allowed.includes(method.toLowerCase())) {\n continue;\n }\n const operation = pathItem[method];\n if (!operation || typeof operation !== 'object') {\n continue;\n }\n operations.push({\n operationId: operation.operationId || '',\n summary: operation.summary || '',\n description: operation.description || '',\n method: method.toUpperCase(),\n path,\n operation,\n });\n }\n }\n return operations;\n}\n\nfunction pickOperation(kind, operations, crudConfig) {\n const overrideId =\n (crudConfig[kind] && crudConfig[kind].operationId) ||\n crudConfig[`${kind}OperationId`];\n if (overrideId) {\n return operations.find(op => op.operationId === overrideId);\n }\n\n const matcher = getDefaultMatcher(kind);\n let matched = operations.find(op => matcher.test(op.operationId));\n if (matched) {\n return matched;\n }\n\n // 兜底策略\n switch (kind) {\n case 'list':\n return operations.find(op => op.method === 'GET') || null;\n case 'create':\n return operations.find(op => op.method === 'POST') || null;\n case 'delete':\n return operations.find(op => op.method === 'DELETE') ||\n operations.find(op => op.method === 'POST' && /delete/i.test(op.operationId)) ||\n null;\n default:\n return null;\n }\n}\n\nfunction getDefaultMatcher(kind) {\n switch (kind) {\n case 'list':\n return /^list[A-Z].*s$/;\n case 'create':\n return /^create[A-Z].*/;\n case 'delete':\n return /^delete[A-Z].*/;\n default:\n return /^$/;\n }\n}\n\nfunction determineResourceName(candidates) {\n for (const item of candidates) {\n const opId = item && item.operationId ? item.operationId : '';\n let match;\n if ((match = opId.match(/^list([A-Z].*)s$/))) {\n return match[1];\n }\n if ((match = opId.match(/^create([A-Z].*)$/))) {\n return match[1];\n }\n if ((match = opId.match(/^delete([A-Z].*)$/))) {\n return match[1];\n }\n }\n return null;\n}\n\nfunction selectIdentityField(apiDoc, crudConfig) {\n if (crudConfig.identityField) {\n return crudConfig.identityField;\n }\n if (apiDoc.components && apiDoc.components.schemas) {\n for (const schemaName of Object.keys(apiDoc.components.schemas)) {\n const schema = apiDoc.components.schemas[schemaName];\n if (!schema || typeof schema !== 'object') {\n continue;\n }\n const identity = Array.isArray(schema['x-dms-identityId']) ? schema['x-dms-identityId'][0] : null;\n if (identity) {\n return identity;\n }\n }\n }\n if (apiDoc.components && apiDoc.components.schemas) {\n for (const schemaName of Object.keys(apiDoc.components.schemas)) {\n const schema = apiDoc.components.schemas[schemaName];\n if (schema && schema.properties && Object.prototype.hasOwnProperty.call(schema.properties, 'dsid')) {\n return 'dsid';\n }\n }\n }\n return 'dsid';\n}\n\nfunction trimTrailingSlash(url) {\n if (!url) {\n return '';\n }\n return url.replace(/\\/+$/, '');\n}\n\nfunction getRequestBody(message) {\n if (message && message.req && message.req.body && typeof message.req.body === 'object') {\n return message.req.body;\n }\n return {};\n}\n\nfunction clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n\nfunction mergeDeep(target, ...sources) {\n for (const source of sources) {\n if (!isPlainObject(source)) {\n continue;\n }\n for (const key of Object.keys(source)) {\n const value = source[key];\n if (value === undefined) {\n continue;\n }\n if (isPlainObject(value)) {\n const base = isPlainObject(target[key]) ? target[key] : {};\n target[key] = mergeDeep({}, base, value);\n } else {\n target[key] = clone(value);\n }\n }\n }\n return target;\n}\n\nfunction isPlainObject(value) {\n return Object.prototype.toString.call(value) === '[object Object]';\n}\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -2440,7 +2440,7 @@ "type": "function", "z": "78d15f59dee4b6d8", "name": "缓存create结果", - "func": "'use strict';\n\n/**\n * 将 LLM 生成的创建请求体写回 crudFlow,提取主键供删除步骤使用。\n */\n\nreturn storeCreateResult(msg, node);\n\nfunction storeCreateResult(message, node) {\n if (!message.crudFlow || !message.crudFlow.create) {\n return message;\n }\n\n const identityField = message.crudFlow.identityField || message.identityField || 'dsid';\n const mockBody = extractMockBody(message);\n\n if (mockBody) {\n message.crudFlow.create.payload = clone(mockBody);\n } else if (!message.crudFlow.create.payload) {\n node.warn('未从模型生成创建参数,继续使用默认样例', message);\n message.crudFlow.create.payload = clone(message.crudFlow.create.samplePayload || {});\n }\n\n const payload = message.crudFlow.create.payload || {};\n const primaryKeyValue = payload[identityField];\n if (primaryKeyValue !== undefined) {\n message.primaryKeyValue = primaryKeyValue;\n message.crudFlow.delete = message.crudFlow.delete || {};\n message.crudFlow.delete.actualKey = primaryKeyValue;\n }\n\n delete message.mock;\n message.payload = message.crudFlow;\n\n return message;\n}\n\nfunction extractMockBody(message) {\n if (message && message.mock && typeof message.mock === 'object' && message.mock.body && typeof message.mock.body === 'object') {\n return message.mock.body;\n }\n if (message && message.payload && typeof message.payload === 'object' && !Array.isArray(message.payload)) {\n return message.payload;\n }\n return null;\n}\n\nfunction clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n", + "func": "'use strict';\n\n/**\n * 将 LLM 生成的创建请求体写回 crudFlow,提取主键供删除步骤使用。\n */\n\nreturn storeCreateResult(msg, node);\n\nfunction storeCreateResult(message, node) {\n if (!message.crudFlow || !message.crudFlow.create) {\n return message;\n }\n\n const identityField = message.crudFlow.identityField || message.identityField || 'dsid';\n const mockBody = extractMockBody(message);\n\n if (mockBody) {\n message.crudFlow.create.payload = clone(mockBody);\n } else if (!message.crudFlow.create.payload) {\n node.warn('未从模型生成创建参数,继续使用默认样例', message);\n message.crudFlow.create.payload = clone(message.crudFlow.create.samplePayload || {});\n }\n\n const payload = message.crudFlow.create.payload || {};\n const primaryKeyValue = payload[identityField];\n if (primaryKeyValue !== undefined) {\n message.primaryKeyValue = primaryKeyValue;\n message.crudFlow.delete = message.crudFlow.delete || {};\n message.crudFlow.delete.actualKey = primaryKeyValue;\n }\n\n delete message.mock;\n delete message.mockCandidates;\n delete message.mockPrompt;\n delete message.mockSource;\n delete message.mockAutoSelected;\n delete message.llmContext;\n delete message.llmRaw;\n delete message.prompt;\n delete message.method;\n delete message.path;\n delete message.url;\n delete message.headers;\n delete message.statusCode;\n delete message.payload;\n\n return message;\n}\n\nfunction extractMockBody(message) {\n if (message && message.mock && typeof message.mock === 'object' && message.mock.body && typeof message.mock.body === 'object') {\n return message.mock.body;\n }\n if (message && message.payload && typeof message.payload === 'object' && !Array.isArray(message.payload)) {\n return message.payload;\n }\n return null;\n}\n\nfunction clone(value) {\n return value == null ? value : JSON.parse(JSON.stringify(value));\n}\n", "outputs": 1, "timeout": 0, "noerr": 0,