diff --git a/.node-red-data/.config.nodes.json b/.node-red-data/.config.nodes.json index d695208..70b46a6 100644 --- a/.node-red-data/.config.nodes.json +++ b/.node-red-data/.config.nodes.json @@ -14,7 +14,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\05-junction.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/05-junction.js" }, "inject": { "name": "inject", @@ -25,7 +25,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\20-inject.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/20-inject.js" }, "debug": { "name": "debug", @@ -36,7 +36,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\21-debug.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/21-debug.js" }, "complete": { "name": "complete", @@ -47,7 +47,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\24-complete.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/24-complete.js" }, "catch": { "name": "catch", @@ -58,7 +58,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\25-catch.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/25-catch.js" }, "status": { "name": "status", @@ -69,7 +69,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\25-status.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/25-status.js" }, "link": { "name": "link", @@ -82,7 +82,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\60-link.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/60-link.js" }, "comment": { "name": "comment", @@ -93,7 +93,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\90-comment.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/90-comment.js" }, "global-config": { "name": "global-config", @@ -104,7 +104,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\91-global-config.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/91-global-config.js" }, "unknown": { "name": "unknown", @@ -115,7 +115,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\98-unknown.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/common/98-unknown.js" }, "function": { "name": "function", @@ -126,7 +126,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\10-function.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/10-function.js" }, "switch": { "name": "switch", @@ -137,7 +137,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\10-switch.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/10-switch.js" }, "change": { "name": "change", @@ -148,7 +148,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\15-change.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/15-change.js" }, "range": { "name": "range", @@ -159,7 +159,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\16-range.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/16-range.js" }, "template": { "name": "template", @@ -170,7 +170,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\80-template.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/80-template.js" }, "delay": { "name": "delay", @@ -181,7 +181,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\89-delay.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/89-delay.js" }, "trigger": { "name": "trigger", @@ -192,7 +192,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\89-trigger.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/89-trigger.js" }, "exec": { "name": "exec", @@ -203,7 +203,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\90-exec.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/90-exec.js" }, "rbe": { "name": "rbe", @@ -214,7 +214,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\function\\rbe.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/function/rbe.js" }, "tls": { "name": "tls", @@ -225,7 +225,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\05-tls.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/05-tls.js" }, "httpproxy": { "name": "httpproxy", @@ -236,7 +236,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\06-httpproxy.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/06-httpproxy.js" }, "mqtt": { "name": "mqtt", @@ -249,7 +249,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\10-mqtt.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js" }, "httpin": { "name": "httpin", @@ -261,7 +261,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\21-httpin.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/21-httpin.js" }, "httprequest": { "name": "httprequest", @@ -272,7 +272,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\21-httprequest.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js" }, "websocket": { "name": "websocket", @@ -286,7 +286,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\22-websocket.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/22-websocket.js" }, "tcpin": { "name": "tcpin", @@ -299,7 +299,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\31-tcpin.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js" }, "udp": { "name": "udp", @@ -311,7 +311,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\network\\32-udp.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/network/32-udp.js" }, "CSV": { "name": "CSV", @@ -322,7 +322,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\parsers\\70-CSV.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js" }, "HTML": { "name": "HTML", @@ -333,7 +333,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\parsers\\70-HTML.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.js" }, "JSON": { "name": "JSON", @@ -344,7 +344,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\parsers\\70-JSON.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/parsers/70-JSON.js" }, "XML": { "name": "XML", @@ -355,7 +355,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\parsers\\70-XML.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/parsers/70-XML.js" }, "YAML": { "name": "YAML", @@ -366,7 +366,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\parsers\\70-YAML.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/parsers/70-YAML.js" }, "split": { "name": "split", @@ -378,7 +378,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\sequence\\17-split.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/sequence/17-split.js" }, "sort": { "name": "sort", @@ -389,7 +389,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\sequence\\18-sort.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/sequence/18-sort.js" }, "batch": { "name": "batch", @@ -400,7 +400,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\sequence\\19-batch.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/sequence/19-batch.js" }, "file": { "name": "file", @@ -412,7 +412,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\storage\\10-file.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/storage/10-file.js" }, "watch": { "name": "watch", @@ -423,7 +423,7 @@ "local": false, "user": false, "module": "node-red", - "file": "C:\\workspace\\动态合规\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\storage\\23-watch.js" + "file": "/Users/zpc01/workspace/zzlh/node-red-master/packages/node_modules/@node-red/nodes/core/storage/23-watch.js" } } } diff --git a/.node-red-data/.config.users.json b/.node-red-data/.config.users.json index cd20980..4a695ff 100644 --- a/.node-red-data/.config.users.json +++ b/.node-red-data/.config.users.json @@ -15,6 +15,12 @@ }, "tours": { "welcome": "4.1.0" + }, + "dialog": { + "export": { + "pretty": true, + "json-view": false + } } }, "menu-menu-item-palette": true, diff --git a/compiliance-js/dms2oas.js b/compiliance-js/dms2oas.js new file mode 100644 index 0000000..312b581 --- /dev/null +++ b/compiliance-js/dms2oas.js @@ -0,0 +1,696 @@ +'use strict'; + +const DEFAULT_OPENAPI_VERSION = '3.0.1'; +const DEFAULT_API_VERSION = '1.0.0'; +const BASE_PREFIX = 'https://www.dev.ideas.cnpc/api/dms'; +const DEFAULT_SERVICE_ID = 'well_kd_wellbore_ideas01'; +const DEFAULT_API_SEGMENT = 'v1'; +const FALLBACK_BASE_URL = `${BASE_PREFIX}/${DEFAULT_SERVICE_ID}/${DEFAULT_API_SEGMENT}`; +const FALLBACK_HEADERS = { + 'Content-Type': 'application/json', + 'Authorization': '1', + 'Dataregion': 'ZZLH', +}; +const SERVICE_DOMAIN_CATALOG = { + 'well_kd_wellbore_ideas01': { + name: '井筒', + id: 'well_kd_wellbore_ideas01', + keywords: ['wb', 'wb_dr', 'wb_ml', 'wb_wl', 'wb_tp', 'wb_dh', 'wb_fr'], + }, + 'geo_kd_res_ideas01': { + name: '采油气', + id: 'geo_kd_res_ideas01', + keywords: ['pc', 'pc_op', 'pc_oe', 'pc_ge'], + }, + 'kd_cr_ideas01': { + name: '分析化验', + id: 'kd_cr_ideas01', + keywords: ['cr', 'cr_se'], + }, + 'kd_rs_ideas01': { + name: '油气藏', + id: 'kd_rs_ideas01', + keywords: ['rs', 'rs_rd', 'rs_rm', 'rs_gs', 'rs_in'], + }, +}; + +let schema; +try { + // 优先使用 HTTP In 提供的 req.body.schema,缺省时回退到 msg.payload。 + const schemaInput = extractSchemaInput(msg); + schema = parseSchema(schemaInput); +} catch (error) { + node.error(`DMS -> Swagger 解析失败:${error.message}`, msg); + msg.error = error.message; + return msg; +} + +const resourceTitle = typeof schema.title === 'string' && schema.title.trim() + ? schema.title.trim() + : 'DMS Resource'; + +const resourceName = pascalCase(resourceTitle); +const collectionName = pluralize(kebabCase(resourceTitle)); +const identityField = Array.isArray(schema.identityId) && schema.identityId.length > 0 + ? String(schema.identityId[0]) + : 'id'; + +const schemaComponent = buildComponentSchema(resourceName, schema); +const identitySchema = schemaComponent.properties && schemaComponent.properties[identityField] + ? clone(schemaComponent.properties[identityField]) + : { type: 'string' }; + +const crudConfig = Object.assign({}, msg.crudConfig || {}); + +const dmsMeta = extractDmsMeta(msg); +msg.dms_meta = dmsMeta; + +const serviceInfo = resolveServiceInfo(dmsMeta && dmsMeta.domain); +const apiVersionSegment = DEFAULT_API_SEGMENT; +const serviceId = serviceInfo.id; + +const { resourceSegment, versionSegment } = deriveResourceSegments(dmsMeta, schema, collectionName); +const listPath = buildListPath(resourceSegment, versionSegment); +const singlePath = buildSinglePath(resourceSegment); +const detailPath = buildDetailPath(resourceSegment, versionSegment, identityField); +const createRequestSchema = buildCreateRequestSchema(resourceName); +const updateRequestSchema = buildCreateRequestSchema(resourceName); +const deleteRequestSchema = buildDeleteRequestSchema(identityField); +const listRequestSchema = buildListRequestSchema(); +const listResponseSchema = buildListResponseSchema(resourceName); + +const openApiDocument = { + openapi: DEFAULT_OPENAPI_VERSION, + info: { + title: `${resourceTitle} API`, + version: DEFAULT_API_VERSION, + description: schema.description || `${schema.$id || ''}`.trim(), + 'x-dms-sourceId': schema.$id || undefined, + }, + paths: buildCrudPaths({ + resourceName, + identityField, + identitySchema, + listPath, + createPath: singlePath, + detailPath, + createRequestSchema, + updateRequestSchema, + deleteRequestSchema, + listRequestSchema, + listResponseSchema, + }), + components: { + schemas: { + [resourceName]: schemaComponent, + }, + }, +}; + +if (!crudConfig.baseUrl) { + crudConfig.baseUrl = `${BASE_PREFIX}/${serviceId}/${apiVersionSegment}`; +} + +const headers = Object.assign({}, FALLBACK_HEADERS, crudConfig.headers || {}); +const dataRegionValue = extractDataRegion(schema, headers.Dataregion); + +if (dataRegionValue) { + headers.Dataregion = dataRegionValue; + crudConfig.dataRegion = dataRegionValue; +} + +crudConfig.headers = headers; +crudConfig.identityField = crudConfig.identityField || identityField; + +crudConfig.service = crudConfig.service || { + id: serviceId, + name: serviceInfo.name, +}; + +crudConfig.list = crudConfig.list || {}; +if (!crudConfig.list.path) { + crudConfig.list.path = listPath; +} +crudConfig.list.method = crudConfig.list.method || 'POST'; + +crudConfig.create = crudConfig.create || {}; +if (!crudConfig.create.path) { + crudConfig.create.path = singlePath; +} +crudConfig.create.method = crudConfig.create.method || 'POST'; + +crudConfig.delete = crudConfig.delete || {}; +if (!crudConfig.delete.path) { + crudConfig.delete.path = singlePath; +} +crudConfig.delete.method = crudConfig.delete.method || 'DELETE'; + +crudConfig.detailPath = crudConfig.detailPath || detailPath; +crudConfig.version = crudConfig.version || versionSegment; + +msg.crudConfig = crudConfig; +msg.headers = Object.assign({}, headers); + +msg.oas_def = openApiDocument; +msg.payload = openApiDocument; +return msg; + +function extractDataRegion(dmsSchema, fallback) { + if (!dmsSchema || typeof dmsSchema !== 'object') { + return fallback; + } + + if (typeof dmsSchema.dataRegion === 'string' && dmsSchema.dataRegion.trim()) { + return dmsSchema.dataRegion.trim(); + } + + if (dmsSchema.defaultShow && Array.isArray(dmsSchema.defaultShow)) { + const candidate = dmsSchema.defaultShow.find(item => item && typeof item === 'string' && item.toLowerCase().includes('dataregion')); + if (candidate) { + return fallback; + } + } + + const props = dmsSchema.properties; + if (props && typeof props === 'object' && props.dataRegion) { + if (typeof props.dataRegion.default === 'string' && props.dataRegion.default.trim()) { + return props.dataRegion.default.trim(); + } + } + + return fallback; +} + +function extractSchemaInput(message) { + if (message && message.req && message.req.body && typeof message.req.body === 'object') { + if (message.req.body.schema !== undefined) { + return message.req.body.schema; + } + if (looksLikeDmsSchema(message.req.body)) { + return message.req.body; + } + } + + if (message && message.payload !== undefined) { + if (message.payload && typeof message.payload === 'object') { + if (message.payload.schema !== undefined) { + return message.payload.schema; + } + if (looksLikeDmsSchema(message.payload)) { + return message.payload; + } + } + return message.payload; + } + + throw new Error('未找到schema,请在请求体的schema字段或msg.payload中提供'); +} + +function parseSchema(source) { + if (typeof source === 'string') { + try { + return JSON.parse(source); + } catch (error) { + throw new Error(`JSON 解析失败:${error.message}`); + } + } + + if (!source || typeof source !== 'object') { + throw new Error('schema 必须是 DMS 定义对象或 JSON 字符串'); + } + + return source; +} + +function buildComponentSchema(resourceName, dmsSchema) { + const { properties = {}, required = [], groupView, identityId, naturalKey, defaultShow } = dmsSchema; + const openApiProps = {}; + + for (const [propName, propSchema] of Object.entries(properties)) { + openApiProps[propName] = mapProperty(propSchema); + } + + return { + type: 'object', + required: Array.isArray(required) ? required.slice() : [], + properties: openApiProps, + description: dmsSchema.description || dmsSchema.title || resourceName, + 'x-dms-groupView': groupView || undefined, + 'x-dms-identityId': identityId || undefined, + 'x-dms-naturalKey': naturalKey || undefined, + 'x-dms-defaultShow': defaultShow || undefined, + }; +} + +function mapProperty(propSchema) { + if (!propSchema || typeof propSchema !== 'object') { + return { type: 'string' }; + } + + const result = {}; + + const type = normalizeType(propSchema.type); + result.type = type.type; + if (type.format) { + result.format = type.format; + } + if (type.items) { + result.items = type.items; + } + + if (propSchema.description) { + result.description = propSchema.description; + } else if (propSchema.title) { + result.description = propSchema.title; + } + + if (propSchema.enum) { + result.enum = propSchema.enum.slice(); + } + + if (propSchema.default !== undefined) { + result.default = propSchema.default; + } + + if (propSchema.mask) { + result['x-dms-mask'] = propSchema.mask; + } + if (propSchema.geom) { + result['x-dms-geom'] = propSchema.geom; + } + if (propSchema.title) { + result['x-dms-title'] = propSchema.title; + } + if (propSchema.type) { + result['x-dms-originalType'] = propSchema.type; + } + + return result; +} + +function normalizeType(typeValue) { + const normalized = typeof typeValue === 'string' ? typeValue.toLowerCase() : undefined; + + switch (normalized) { + case 'number': + case 'integer': + case 'long': + case 'float': + case 'double': + return { type: 'number' }; + case 'boolean': + return { type: 'boolean' }; + case 'array': + return { type: 'array', items: { type: 'string' } }; + case 'date': + return { type: 'string', format: 'date' }; + case 'date-time': + return { type: 'string', format: 'date-time' }; + case 'object': + return { type: 'object' }; + case 'string': + default: + return { type: 'string' }; + } +} + +function buildCrudPaths({ + resourceName, + identityField, + identitySchema, + listPath, + createPath, + detailPath, + createRequestSchema, + updateRequestSchema, + deleteRequestSchema, + listRequestSchema, + listResponseSchema, +}) { + const ref = `#/components/schemas/${resourceName}`; + const paths = {}; + + const normalisedListPath = normalisePath(listPath); + const normalisedCreatePath = normalisePath(createPath); + const normalisedDetailPath = detailPath ? normalisePath(detailPath) : null; + + paths[normalisedListPath] = { + post: { + operationId: `list${resourceName}s`, + summary: `List ${resourceName} resources`, + requestBody: { + required: false, + content: { + 'application/json': { + schema: listRequestSchema, + }, + }, + }, + responses: { + 200: { + description: 'Successful response', + content: { + 'application/json': { + schema: listResponseSchema || { + type: 'array', + items: { $ref: ref }, + }, + }, + }, + }, + }, + }, + }; + + paths[normalisedCreatePath] = { + post: { + operationId: `create${resourceName}`, + summary: `Create a ${resourceName}`, + requestBody: { + required: true, + content: { + 'application/json': { + schema: createRequestSchema, + }, + }, + }, + responses: { + 201: { + description: 'Created', + content: { + 'application/json': { + schema: { $ref: ref }, + }, + }, + }, + }, + }, + put: { + operationId: `update${resourceName}`, + summary: `Update a ${resourceName}`, + requestBody: { + required: true, + content: { + 'application/json': { + schema: updateRequestSchema, + }, + }, + }, + responses: { + 200: { + description: 'Successful update', + content: { + 'application/json': { + schema: { $ref: ref }, + }, + }, + }, + }, + }, + delete: { + operationId: `delete${resourceName}`, + summary: `Delete ${resourceName} resources`, + requestBody: { + required: true, + content: { + 'application/json': { + schema: deleteRequestSchema, + }, + }, + }, + responses: { + 200: { description: 'Deleted' }, + }, + }, + }; + + if (normalisedDetailPath) { + paths[normalisedDetailPath] = { + parameters: [ + { + name: identityField, + in: 'path', + required: true, + schema: identitySchema, + }, + ], + get: { + operationId: `get${resourceName}`, + summary: `Get a single ${resourceName}`, + responses: { + 200: { + description: 'Successful response', + content: { + 'application/json': { + schema: { $ref: ref }, + }, + }, + }, + 404: { description: `${resourceName} not found` }, + }, + }, + }; + } + + return paths; +} + +function normalisePath(path) { + if (!path) { + return '/'; + } + return path.startsWith('/') ? path : `/${path}`; +} + +function pascalCase(input) { + return input + .replace(/[^a-zA-Z0-9]+/g, ' ') + .split(' ') + .filter(Boolean) + .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) + .join('') || 'Resource'; +} + +function kebabCase(input) { + return input + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/[^a-zA-Z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .toLowerCase() || 'resource'; +} + +function pluralize(word) { + if (word.endsWith('s')) { + return word; + } + if (word.endsWith('y')) { + return word.slice(0, -1) + 'ies'; + } + return `${word}s`; +} + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function looksLikeDmsSchema(candidate) { + if (!candidate || typeof candidate !== 'object') { + return false; + } + if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) { + return true; + } + if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') { + return true; + } + return false; +} + +function extractDmsMeta(message) { + const candidateReq = message && message.req && message.req.body && message.req.body.dms_meta; + const parsedReq = parseMetaCandidate(candidateReq); + if (parsedReq) { + return parsedReq; + } + + const candidateMsg = message && message.dms_meta; + const parsedMsg = parseMetaCandidate(candidateMsg); + if (parsedMsg) { + return parsedMsg; + } + + return null; +} + +function parseMetaCandidate(candidate) { + if (!candidate) { + return null; + } + if (typeof candidate === 'string') { + try { + const value = JSON.parse(candidate); + return parseMetaCandidate(value); + } catch (err) { + return null; + } + } + if (typeof candidate === 'object') { + return clone(candidate); + } + return null; +} + +function resolveServiceInfo(domain) { + if (!domain || typeof domain !== 'string') { + return SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID]; + } + const normalized = domain.toLowerCase(); + let best = null; + for (const entry of Object.values(SERVICE_DOMAIN_CATALOG)) { + const matched = entry.keywords.some(keyword => { + if (typeof keyword !== 'string') return false; + const normKeyword = keyword.toLowerCase(); + return normalized.includes(normKeyword) || normKeyword.includes(normalized); + }); + if (matched) { + best = entry; + break; + } + } + return best || SERVICE_DOMAIN_CATALOG[DEFAULT_SERVICE_ID]; +} + +function deriveResourceSegments(meta, schema, collectionName) { + const defaultResource = (collectionName || '').replace(/^\//, ''); + let resourceSegment = defaultResource; + let versionSegment = null; + + if (meta && typeof meta === 'object') { + if (typeof meta.id === 'string' && meta.id.trim()) { + const parts = meta.id.trim().split('.'); + if (parts.length > 0 && parts[0]) { + resourceSegment = parts[0]; + } + if (parts.length > 1) { + versionSegment = parts.slice(1).join('.'); + } + } else if (typeof meta.name === 'string' && meta.name.trim()) { + resourceSegment = meta.name.trim(); + } + } + + if (!resourceSegment && schema && schema.title) { + resourceSegment = kebabCase(schema.title); + } + + return { + resourceSegment: resourceSegment || defaultResource || 'resource', + versionSegment: versionSegment, + }; +} + +function buildListPath(resourceSegment, versionSegment) { + if (versionSegment) { + return `/${resourceSegment}/${versionSegment}`; + } + return `/${resourceSegment}`; +} + +function buildSinglePath(resourceSegment) { + return `/${resourceSegment}`; +} + +function buildDetailPath(resourceSegment, versionSegment, identityField) { + if (versionSegment) { + return `/${resourceSegment}/${versionSegment}/{${identityField}}`; + } + return `/${resourceSegment}/{${identityField}}`; +} + +function buildCreateRequestSchema(resourceName) { + const ref = `#/components/schemas/${resourceName}`; + return { + type: 'object', + required: ['version', 'act', 'data'], + properties: { + version: { type: 'string', default: '1.0.0' }, + act: { type: 'integer', default: -1 }, + data: { + type: 'array', + items: { $ref: ref }, + }, + }, + }; +} + +function buildDeleteRequestSchema(identityField) { + return { + type: 'object', + required: ['version', 'data'], + properties: { + version: { type: 'string', default: '1.0.0' }, + data: { + type: 'array', + items: { + type: 'string', + description: `Value of ${identityField}`, + }, + }, + }, + }; +} + +function buildListRequestSchema() { + return { + type: 'object', + properties: { + version: { type: 'string', default: '1.0.0' }, + data: { + type: 'object', + properties: { + pageNo: { type: 'integer', default: 1 }, + pageSize: { type: 'integer', default: 20 }, + isSearchCount: { type: 'boolean', default: true }, + filters: { + type: 'array', + items: { type: 'object' }, + }, + }, + }, + }, + }; +} + +function buildListResponseSchema(resourceName) { + const ref = `#/components/schemas/${resourceName}`; + return { + type: 'object', + properties: { + code: { type: 'integer' }, + message: { type: 'string' }, + data: { + type: 'object', + properties: { + list: { + type: 'array', + items: { $ref: ref }, + }, + total: { type: 'integer' }, + }, + }, + }, + }; +} + +function looksLikeDmsSchema(candidate) { + if (!candidate || typeof candidate !== 'object') { + return false; + } + if (candidate.properties && typeof candidate.properties === 'object' && candidate.title) { + return true; + } + if (candidate.$id && candidate.type && candidate.type.toLowerCase && candidate.type.toLowerCase() === 'object') { + return true; + } + return false; +} diff --git a/compiliance-js/flow1-compose-create-request.js b/compiliance-js/flow1-compose-create-request.js new file mode 100644 index 0000000..0e34310 --- /dev/null +++ b/compiliance-js/flow1-compose-create-request.js @@ -0,0 +1,79 @@ +'use strict'; + +/** + * 使用 msg.crudFlow.create 配置创建请求;若未提供则保持旧有注入负载。 + */ + +return configureCreateRequest(msg, node); + +function configureCreateRequest(message, node) { + if (!message.crudFlow || !message.crudFlow.create) { + return message; + } + + const create = message.crudFlow.create; + const baseUrl = message.crudFlow.baseUrl || ''; + const payload = selectCreatePayload(create); + + message.method = (create.method || 'POST').toUpperCase(); + message.url = mergeUrl(baseUrl, create.path || ''); + message.headers = Object.assign({}, message.crudFlow.headers || {}, create.headers || {}); + + if (payload !== undefined) { + message.payload = clone(payload); + } else if (message.payload !== undefined) { + delete message.payload; + } + + const identityField = message.crudFlow.identityField || message.identityField || 'dsid'; + const keyValue = payload && typeof payload === 'object' + ? extractPrimaryKey(payload, identityField) + : undefined; + + if (keyValue !== undefined) { + message.primaryKeyValue = keyValue; + message.crudFlow.delete = message.crudFlow.delete || {}; + message.crudFlow.delete.actualKey = keyValue; + } + + return message; +} + +function selectCreatePayload(create) { + if (create.payload && Object.keys(create.payload).length > 0) { + return create.payload; + } + if (create.samplePayload) { + return create.samplePayload; + } + return undefined; +} + +function extractPrimaryKey(payload, identityField) { + if (!payload) { + return undefined; + } + if (Object.prototype.hasOwnProperty.call(payload, identityField)) { + return payload[identityField]; + } + if (Array.isArray(payload.data) && payload.data.length > 0 && payload.data[0][identityField] !== undefined) { + return payload.data[0][identityField]; + } + return undefined; +} + +function mergeUrl(base, path) { + const prefix = (base || '').replace(/\/+$/, ''); + const suffix = (path || '').replace(/^\/+/, ''); + if (!prefix) { + return `/${suffix}`; + } + if (!suffix) { + return prefix; + } + return `${prefix}/${suffix}`; +} + +function clone(value) { + return value == null ? value : JSON.parse(JSON.stringify(value)); +} diff --git a/compiliance-js/flow1-compose-delete-request.js b/compiliance-js/flow1-compose-delete-request.js new file mode 100644 index 0000000..3779c3d --- /dev/null +++ b/compiliance-js/flow1-compose-delete-request.js @@ -0,0 +1,78 @@ +'use strict'; + +/** + * 依据 msg.crudFlow.delete 生成删除请求体,支持占位符 {{primaryKey}}。 + */ + +return configureDeleteRequest(msg, node); + +function configureDeleteRequest(message, node) { + if (!message.crudFlow || !message.crudFlow.delete) { + return message; + } + + const del = message.crudFlow.delete; + const baseUrl = message.crudFlow.baseUrl || ''; + const primaryKeyValue = del.actualKey || message.primaryKeyValue || del.primaryKey || 'testid2'; + const method = (del.method || 'POST').toUpperCase(); + + let path = del.path || ''; + let payload = null; + + if (method === 'DELETE' && path.includes('{')) { + path = path.replace(/\{[^}]+\}/g, encodeURIComponent(primaryKeyValue)); + } else { + const template = del.payloadTemplate || { version: '1.0.0', data: ['{{primaryKey}}'] }; + payload = materialiseTemplate(template, primaryKeyValue); + } + + message.method = method; + message.url = mergeUrl(baseUrl, path); + message.headers = Object.assign({}, message.crudFlow.headers || {}, del.headers || {}); + + if (payload !== null) { + message.payload = payload; + } else if (message.payload !== undefined) { + delete message.payload; + } + + return message; +} + +function materialiseTemplate(template, primaryKeyValue) { + const cloned = clone(template); + return replacePlaceholder(cloned, primaryKeyValue); +} + +function replacePlaceholder(value, primaryKeyValue) { + if (typeof value === 'string') { + return value.replace(/\{\{\s*primaryKey\s*\}\}/g, primaryKeyValue); + } + if (Array.isArray(value)) { + return value.map(item => replacePlaceholder(item, primaryKeyValue)); + } + if (value && typeof value === 'object') { + const result = {}; + for (const key of Object.keys(value)) { + result[key] = replacePlaceholder(value[key], primaryKeyValue); + } + return result; + } + return value; +} + +function mergeUrl(base, path) { + const prefix = (base || '').replace(/\/+$/, ''); + const suffix = (path || '').replace(/^\/+/, ''); + if (!prefix) { + return `/${suffix}`; + } + if (!suffix) { + return prefix; + } + return `${prefix}/${suffix}`; +} + +function clone(value) { + return value == null ? value : JSON.parse(JSON.stringify(value)); +} diff --git a/compiliance-js/flow1-compose-list-request.js b/compiliance-js/flow1-compose-list-request.js new file mode 100644 index 0000000..a96be65 --- /dev/null +++ b/compiliance-js/flow1-compose-list-request.js @@ -0,0 +1,45 @@ +'use strict'; + +/** + * 根据 msg.crudFlow.list 设置查询请求,兼容旧的手工注入流程。 + */ + +return configureListRequest(msg, node); + +function configureListRequest(message, node) { + if (!message.crudFlow || !message.crudFlow.list) { + return message; + } + + const list = message.crudFlow.list; + const baseUrl = message.crudFlow.baseUrl || ''; + + message.method = (list.method || 'GET').toUpperCase(); + message.url = mergeUrl(baseUrl, list.path || ''); + message.headers = Object.assign({}, message.crudFlow.headers || {}, list.headers || {}); + + if (list.payload !== undefined) { + message.payload = clone(list.payload); + } else if (message.payload !== undefined) { + delete message.payload; + } + + message.listRequestConfigured = true; + return message; +} + +function mergeUrl(base, path) { + const prefix = (base || '').replace(/\/+$/, ''); + const suffix = (path || '').replace(/^\/+/, ''); + if (!prefix) { + return `/${suffix}`; + } + if (!suffix) { + return prefix; + } + return `${prefix}/${suffix}`; +} + +function clone(value) { + return value == null ? value : JSON.parse(JSON.stringify(value)); +} diff --git a/compiliance-js/flow1-handle-create-response.js b/compiliance-js/flow1-handle-create-response.js new file mode 100644 index 0000000..80ef876 --- /dev/null +++ b/compiliance-js/flow1-handle-create-response.js @@ -0,0 +1,84 @@ +'use strict'; + +/** + * 解析创建接口返回值,记录是否成功以及实际主键。 + */ + +return handleCreateResponse(msg, node); + +function handleCreateResponse(message, node) { + let parsed; + try { + parsed = typeof message.payload === 'string' ? JSON.parse(message.payload) : message.payload; + } catch (error) { + node.error(`创建接口响应解析失败:${error.message}`, message); + message.isCreated = false; + return message; + } + + const success = parsed && typeof parsed === 'object' && parsed.code === 0; + message.isCreated = success; + + if (!success) { + return message; + } + + const identityField = (message.crudFlow && message.crudFlow.identityField) || + message.identityField || 'dsid'; + + const candidate = extractPrimaryKey(parsed, identityField); + if (candidate) { + message.primaryKeyValue = candidate; + if (message.crudFlow && message.crudFlow.delete) { + message.crudFlow.delete.actualKey = candidate; + } + } + + return message; +} + +function extractPrimaryKey(response, identityField) { + if (!response || typeof response !== 'object') { + return null; + } + + const data = response.data; + if (!data) { + return null; + } + + if (typeof data === 'string' || typeof data === 'number') { + return String(data); + } + + if (Array.isArray(data)) { + for (const item of data) { + const value = extractPrimaryKey(item, identityField); + if (value) { + return value; + } + } + } else if (typeof data === 'object') { + if (data[identityField] !== undefined && data[identityField] !== null) { + return String(data[identityField]); + } + if (Array.isArray(data.list)) { + for (const item of data.list) { + const value = extractPrimaryKey(item, identityField); + if (value) { + return value; + } + } + } + if (Array.isArray(data.items)) { + for (const item of data.items) { + const value = extractPrimaryKey(item, identityField); + if (value) { + return value; + } + } + } + } + + return null; +} diff --git a/compiliance-js/flow1-summarize-crud.js b/compiliance-js/flow1-summarize-crud.js new file mode 100644 index 0000000..d39d5a4 --- /dev/null +++ b/compiliance-js/flow1-summarize-crud.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * 汇总 CRUD 执行结果,生成统一响应结构。 + */ + +return summariseCrudResult(msg, node); + +function summariseCrudResult(message, node) { + const identityField = (message.crudFlow && message.crudFlow.identityField) || + message.identityField || 'dsid'; + const primaryKey = (message.crudFlow && message.crudFlow.delete && message.crudFlow.delete.actualKey) || + message.primaryKeyValue || null; + + const listOk = message.listError ? false : true; + const createOk = message.isCreated === undefined ? true : !!message.isCreated; + const deleteOk = message.isDeleted === undefined ? true : !!message.isDeleted; + + const success = listOk && createOk && deleteOk; + + message.payload = { + code: success ? 0 : 1, + message: success ? '井创建删除流程测试成功' : '井创建删除流程部分失败', + details: { + listOk, + createOk, + deleteOk, + identityField, + primaryKey, + }, + }; + + delete message.crudFlow; + delete message.identityField; + delete message.primaryKeyValue; + delete message.listRequestConfigured; + delete message.listError; + delete message.isCreated; + delete message.isDeleted; + delete message.method; + delete message.url; + delete message.headers; + delete message.statusCode; + + return message; +} diff --git a/compiliance-js/flow2-build-crud-plan.js b/compiliance-js/flow2-build-crud-plan.js new file mode 100644 index 0000000..99725e4 --- /dev/null +++ b/compiliance-js/flow2-build-crud-plan.js @@ -0,0 +1,334 @@ +'use strict'; + +/** + * 构建 CRUD 执行动作所需的指令集合,保存于 msg.crudFlow。 + * 约定 operationId 采用 dms→oas 转换后默认的命名: + * lists / create / delete + * 允许使用 msg.crudConfig.* 进行覆盖。 + */ +const FALLBACK_BASE_URL = 'https://www.dev.ideas.cnpc/api/dms/well_kd_wellbore_ideas01/v1'; +const DEFAULT_LIST_PAYLOAD = { + version: '1.0.0', + data: [], + pageNo: 1, + pageSize: 20, + isSearchCount: true, +}; +const DEFAULT_CREATE_SAMPLE = { + version: '1.0.0', + act: -1, + data: [ + { + dsid: 'testid2', + wellId: 'WELL-zzlhTEST-002', + wellCommonName: 'zzlh测试用井名', + wellLegalName: 'zzlh-test-01', + wellPurpose: '开发井', + wellType: '直井', + dataRegion: 'ZZLH', + projectId: 'PROJ-ZZLH-001', + projectName: 'zzlh测试地质单元', + orgId: 'ORG-ZZLH-01', + orgName: 'zzlh采油厂', + bsflag: 1, + wellState: '生产中', + spudDate: '2024-01-15', + completionDate: '2024-05-20', + prodDate: '2024-06-01', + egl: 145.5, + kbd: 5.2, + kb: 150.7, + actualXAxis: 550123.45, + actualYAxis: 4998765.32, + coordinateSystemName: 'zzlh测试坐标系', + geoDescription: '位于zzlh测试区域', + remarks: '这是一口用于系统测试的生产井。', + createUserId: 'testuser001', + createDate: '2025-09-12T10:00:00Z', + updateUserId: 'testuser001', + updateDate: '2025-09-12T10:00:00Z', + }, + ], +}; +const DEFAULT_DELETE_TEMPLATE = { + version: '1.0.0', + data: ['{{primaryKey}}'], +}; + +return buildCrudPlan(msg, node); + +function buildCrudPlan(message, node) { + const requestBody = getRequestBody(message); + const crudConfig = mergeDeep({}, requestBody.crudConfig || {}, message.crudConfig || {}); + const apiDoc = normaliseOpenApi(message, crudConfig, requestBody, node); + const operations = collectOperations(apiDoc); + + const listOp = pickOperation('list', operations, crudConfig); + const createOp = pickOperation('create', operations, crudConfig); + const deleteOp = pickOperation('delete', operations, crudConfig); + + if (!listOp || !createOp || !deleteOp) { + const missing = [ + !listOp ? 'list' : null, + !createOp ? 'create' : null, + !deleteOp ? 'delete' : null, + ].filter(Boolean).join(', '); + const errMsg = `未能在 OpenAPI 文档中找到必要的 CRUD 操作:${missing}`; + node.error(errMsg, message); + message.error = errMsg; + return null; + } + + const resourceName = determineResourceName([createOp, listOp, deleteOp]) || 'Resource'; + const identityField = crudConfig.identityField || + requestBody.identityField || + selectIdentityField(apiDoc, crudConfig) || + 'dsid'; + const baseUrl = trimTrailingSlash( + crudConfig.baseUrl || + requestBody.baseUrl || + message.baseUrl || + FALLBACK_BASE_URL + ); + const headers = mergeDeep({}, requestBody.headers || {}, crudConfig.headers || {}); + + const listConfig = mergeDeep( + {}, + { payload: clone(DEFAULT_LIST_PAYLOAD) }, + requestBody.list || {}, + crudConfig.list || {} + ); + const createConfig = mergeDeep( + {}, + { payload: clone(DEFAULT_CREATE_SAMPLE) }, + requestBody.create || {}, + crudConfig.create || {} + ); + const deleteConfig = mergeDeep( + {}, + { payloadTemplate: clone(DEFAULT_DELETE_TEMPLATE) }, + requestBody.delete || {}, + crudConfig.delete || {} + ); + + if (requestBody.dataRegion) { + headers.Dataregion = requestBody.dataRegion; + } + + message.crudFlow = { + resourceName, + identityField, + baseUrl, + headers, + list: Object.assign({ + operationId: listOp.operationId, + method: listOp.method, + path: listOp.path, + }, listConfig), + create: Object.assign({ + operationId: createOp.operationId, + method: createOp.method, + path: createOp.path, + samplePayload: clone(DEFAULT_CREATE_SAMPLE), + }, createConfig), + delete: Object.assign({ + operationId: deleteOp.operationId, + method: deleteOp.method, + path: deleteOp.path, + }, deleteConfig), + openapi: apiDoc, + }; + + delete message.crudConfig; + if (message.baseUrl) { + delete message.baseUrl; + } + if (message.headers && !Object.keys(message.headers).length) { + delete message.headers; + } + + message.oas_def = apiDoc; + delete message.error; + return message; +} + +function normaliseOpenApi(message, crudConfig, requestBody, node) { + let candidate = + crudConfig.openapi || + requestBody.openapi || + message.oas_def || + message.swagger || + message.payload; + if (typeof candidate === 'string') { + try { + candidate = JSON.parse(candidate); + } catch (err) { + throw new Error(`OpenAPI JSON 解析失败:${err.message}`); + } + } + if (!candidate || typeof candidate !== 'object') { + throw new Error('未提供合法的 OpenAPI 文档'); + } + if (!candidate.paths || typeof candidate.paths !== 'object' || Object.keys(candidate.paths).length === 0) { + throw new Error('OpenAPI 文档缺少 paths 定义'); + } + return candidate; +} + +function collectOperations(apiDoc) { + const operations = []; + const allowed = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace']; + for (const path of Object.keys(apiDoc.paths)) { + const pathItem = apiDoc.paths[path]; + if (!pathItem || typeof pathItem !== 'object') { + continue; + } + for (const method of Object.keys(pathItem)) { + if (!allowed.includes(method.toLowerCase())) { + continue; + } + const operation = pathItem[method]; + if (!operation || typeof operation !== 'object') { + continue; + } + operations.push({ + operationId: operation.operationId || '', + summary: operation.summary || '', + description: operation.description || '', + method: method.toUpperCase(), + path, + operation, + }); + } + } + return operations; +} + +function pickOperation(kind, operations, crudConfig) { + const overrideId = + (crudConfig[kind] && crudConfig[kind].operationId) || + crudConfig[`${kind}OperationId`]; + if (overrideId) { + return operations.find(op => op.operationId === overrideId); + } + + const matcher = getDefaultMatcher(kind); + let matched = operations.find(op => matcher.test(op.operationId)); + if (matched) { + return matched; + } + + // 兜底策略 + switch (kind) { + case 'list': + return operations.find(op => op.method === 'GET') || null; + case 'create': + return operations.find(op => op.method === 'POST') || null; + case 'delete': + return operations.find(op => op.method === 'DELETE') || + operations.find(op => op.method === 'POST' && /delete/i.test(op.operationId)) || + null; + default: + return null; + } +} + +function getDefaultMatcher(kind) { + switch (kind) { + case 'list': + return /^list[A-Z].*s$/; + case 'create': + return /^create[A-Z].*/; + case 'delete': + return /^delete[A-Z].*/; + default: + return /^$/; + } +} + +function determineResourceName(candidates) { + for (const item of candidates) { + const opId = item && item.operationId ? item.operationId : ''; + let match; + if ((match = opId.match(/^list([A-Z].*)s$/))) { + return match[1]; + } + if ((match = opId.match(/^create([A-Z].*)$/))) { + return match[1]; + } + if ((match = opId.match(/^delete([A-Z].*)$/))) { + return match[1]; + } + } + return null; +} + +function selectIdentityField(apiDoc, crudConfig) { + if (crudConfig.identityField) { + return crudConfig.identityField; + } + if (apiDoc.components && apiDoc.components.schemas) { + for (const schemaName of Object.keys(apiDoc.components.schemas)) { + const schema = apiDoc.components.schemas[schemaName]; + if (!schema || typeof schema !== 'object') { + continue; + } + const identity = Array.isArray(schema['x-dms-identityId']) ? schema['x-dms-identityId'][0] : null; + if (identity) { + return identity; + } + } + } + if (apiDoc.components && apiDoc.components.schemas) { + for (const schemaName of Object.keys(apiDoc.components.schemas)) { + const schema = apiDoc.components.schemas[schemaName]; + if (schema && schema.properties && Object.prototype.hasOwnProperty.call(schema.properties, 'dsid')) { + return 'dsid'; + } + } + } + return 'dsid'; +} + +function trimTrailingSlash(url) { + if (!url) { + return ''; + } + return url.replace(/\/+$/, ''); +} + +function getRequestBody(message) { + if (message && message.req && message.req.body && typeof message.req.body === 'object') { + return message.req.body; + } + return {}; +} + +function clone(value) { + return value == null ? value : JSON.parse(JSON.stringify(value)); +} + +function mergeDeep(target, ...sources) { + for (const source of sources) { + if (!isPlainObject(source)) { + continue; + } + for (const key of Object.keys(source)) { + const value = source[key]; + if (value === undefined) { + continue; + } + if (isPlainObject(value)) { + const base = isPlainObject(target[key]) ? target[key] : {}; + target[key] = mergeDeep({}, base, value); + } else { + target[key] = clone(value); + } + } + } + return target; +} + +function isPlainObject(value) { + return Object.prototype.toString.call(value) === '[object Object]'; +} diff --git a/compiliance-js/flow2-set-create-context.js b/compiliance-js/flow2-set-create-context.js new file mode 100644 index 0000000..b67a909 --- /dev/null +++ b/compiliance-js/flow2-set-create-context.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * 为参数生成器 / LLM 设定目标的 operationId/path,并清理 msg.mock。 + */ + +return setCreateContext(msg, node); + +function setCreateContext(message, node) { + if (!message.crudFlow || !message.crudFlow.create) { + const err = '缺少 crudFlow.create 配置,无法准备创建操作'; + node.error(err, message); + message.error = err; + return null; + } + + const create = message.crudFlow.create; + message.operationId = create.operationId; + message.method = create.method || 'POST'; + message.path = create.path; + message.mock = {}; + + if (create.prompt) { + message.prompt = create.prompt; + } + if (!message.oas_def && message.crudFlow.openapi) { + message.oas_def = message.crudFlow.openapi; + } + + delete message.error; + return message; +} diff --git a/compiliance-js/flow2-store-create-result.js b/compiliance-js/flow2-store-create-result.js new file mode 100644 index 0000000..9169220 --- /dev/null +++ b/compiliance-js/flow2-store-create-result.js @@ -0,0 +1,62 @@ +'use strict'; + +/** + * 将 LLM 生成的创建请求体写回 crudFlow,提取主键供删除步骤使用。 + */ + +return storeCreateResult(msg, node); + +function storeCreateResult(message, node) { + if (!message.crudFlow || !message.crudFlow.create) { + return message; + } + + const identityField = message.crudFlow.identityField || message.identityField || 'dsid'; + const mockBody = extractMockBody(message); + + if (mockBody) { + message.crudFlow.create.payload = clone(mockBody); + } else if (!message.crudFlow.create.payload) { + node.warn('未从模型生成创建参数,继续使用默认样例', message); + message.crudFlow.create.payload = clone(message.crudFlow.create.samplePayload || {}); + } + + const payload = message.crudFlow.create.payload || {}; + const primaryKeyValue = payload[identityField]; + if (primaryKeyValue !== undefined) { + message.primaryKeyValue = primaryKeyValue; + message.crudFlow.delete = message.crudFlow.delete || {}; + message.crudFlow.delete.actualKey = primaryKeyValue; + } + + delete message.mock; + delete message.mockCandidates; + delete message.mockPrompt; + delete message.mockSource; + delete message.mockAutoSelected; + delete message.llmContext; + delete message.llmRaw; + delete message.prompt; + delete message.method; + delete message.path; + delete message.url; + delete message.headers; + delete message.statusCode; + delete message.payload; + + return message; +} + +function extractMockBody(message) { + if (message && message.mock && typeof message.mock === 'object' && message.mock.body && typeof message.mock.body === 'object') { + return message.mock.body; + } + if (message && message.payload && typeof message.payload === 'object' && !Array.isArray(message.payload)) { + return message.payload; + } + return null; +} + +function clone(value) { + return value == null ? value : JSON.parse(JSON.stringify(value)); +} diff --git a/compiliance-js/oas.txt b/compiliance-js/oas.txt new file mode 100644 index 0000000..049db47 --- /dev/null +++ b/compiliance-js/oas.txt @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"井 API","version":"1.0.0","description":"https://schema.ideas.cnpc/json/core-data/cd_well.1.0.0.json","x-dms-sourceId":"https://schema.ideas.cnpc/json/core-data/cd_well.1.0.0.json"},"paths":{"/cd_well/1.0.0":{"post":{"operationId":"listResources","summary":"List Resource resources","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","default":"1.0.0"},"data":{"type":"object","properties":{"pageNo":{"type":"integer","default":1},"pageSize":{"type":"integer","default":20},"isSearchCount":{"type":"boolean","default":true},"filters":{"type":"array","items":{"type":"object"}}}}}}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"list":{"type":"array","items":{"$ref":"#/components/schemas/Resource"}},"total":{"type":"integer"}}}}}}}}}}},"/cd_well":{"post":{"operationId":"createResource","summary":"Create a Resource","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["version","act","data"],"properties":{"version":{"type":"string","default":"1.0.0"},"act":{"type":"integer","default":-1},"data":{"type":"array","items":{"$ref":"#/components/schemas/Resource"}}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}}}},"put":{"operationId":"updateResource","summary":"Update a Resource","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["version","act","data"],"properties":{"version":{"type":"string","default":"1.0.0"},"act":{"type":"integer","default":-1},"data":{"type":"array","items":{"$ref":"#/components/schemas/Resource"}}}}}}},"responses":{"200":{"description":"Successful update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}}}},"delete":{"operationId":"deleteResource","summary":"Delete Resource resources","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["version","data"],"properties":{"version":{"type":"string","default":"1.0.0"},"data":{"type":"array","items":{"type":"string","description":"Value of dsid"}}}}}}},"responses":{"200":{"description":"Deleted"}}}},"/cd_well/1.0.0/{dsid}":{"parameters":[{"name":"dsid","in":"path","required":true,"schema":{"type":"string","description":"主键ID","x-dms-title":"DSID","x-dms-originalType":"string"}}],"get":{"operationId":"getResource","summary":"Get a single Resource","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"404":{"description":"Resource not found"}}}}},"components":{"schemas":{"Resource":{"type":"object","required":["bsflag","dataRegion","createUserId","orgId","wellPurpose","createDate","wellLegalName","dsid","wellType","updateUserId","updateDate","wellId","wellCommonName","projectId"],"properties":{"kb":{"type":"number","description":"海拔(高程),补心海拔=地面海拔+补心高","x-dms-title":"补心海拔","x-dms-originalType":"number"},"egl":{"type":"number","description":"地面海拔","x-dms-title":"地面海拔","x-dms-originalType":"number"},"kbd":{"type":"number","description":"补心高度","x-dms-title":"补心高度","x-dms-originalType":"number"},"dsid":{"type":"string","description":"主键ID","x-dms-title":"DSID","x-dms-originalType":"string"},"orgId":{"type":"string","description":"单位唯一标识符,关联CD_ORGANIZATION的主键","x-dms-title":"机构ID","x-dms-originalType":"string"},"bsflag":{"type":"number","description":"填写数据逻辑删除标识,1=在用,-5=废弃","x-dms-title":"删除标识","x-dms-originalType":"number"},"canton":{"type":"string","description":"填写行政区代码对应的行政区名称","x-dms-title":"行政区名称","x-dms-originalType":"string"},"siteId":{"type":"string","description":"物探工区ID","x-dms-title":"物探工区ID","x-dms-originalType":"string"},"wellId":{"type":"string","description":"井标识符,非限定唯一。由EPDM系统自动产生维护,无需人工干预,是用于唯一标识EPDM系统的每一口井的内部机器码","x-dms-title":"井ID","x-dms-originalType":"string"},"orgName":{"type":"string","description":"单位名称,必填","x-dms-title":"机构名称","x-dms-originalType":"string"},"remarks":{"type":"string","description":"备注","x-dms-title":"备注","x-dms-originalType":"string"},"prodDate":{"type":"string","format":"date","description":"投产日期","x-dms-title":"投产日期","x-dms-originalType":"date"},"siteName":{"type":"string","description":"物探工区名称","x-dms-title":"物探工区名称","x-dms-originalType":"string"},"spudDate":{"type":"string","format":"date","description":"主井筒的开钻日期","x-dms-title":"开钻日期","x-dms-originalType":"date"},"wellDesc":{"type":"string","description":"填写这口井的曾用名","x-dms-title":"曾用名","x-dms-originalType":"string"},"wellType":{"type":"string","description":"属性规范值字段,引用属性代码WELL_TYPE下的属性值","x-dms-title":"井型","x-dms-originalType":"string"},"checkDate":{"type":"string","format":"date","description":"记录数据在本系统的审核时间,需精确到时分秒","x-dms-title":"审核日期","x-dms-originalType":"date"},"dataGroup":{"type":"string","description":"数据分组","x-dms-title":"数据分组","x-dms-originalType":"string"},"projectId":{"type":"string","description":"地质单元唯一标识符,根据井别不同,选择关联构造单元(探井)还是油气田单元(开发井),关联CD_GEO_UNIT表的主键","x-dms-title":"地质单元ID","x-dms-originalType":"string"},"stationId":{"type":"string","description":"站库ID","x-dms-title":"站库ID","x-dms-originalType":"string"},"wellState":{"type":"string","description":"井状态","x-dms-title":"井状态","x-dms-originalType":"string"},"activityId":{"type":"string","description":"项目唯一标示符,关联CD_ACTIVITY表的主键","x-dms-title":"项目ID","x-dms-originalType":"string"},"cantonCode":{"type":"string","description":"属性规范值字段,引用属性代码CANTON下的属性值","x-dms-title":"行政区代码","x-dms-originalType":"string"},"createDate":{"type":"string","format":"date","description":"记录数据在本系统的创建时间,需精确到时分秒","x-dms-title":"创建日期","x-dms-originalType":"date"},"dataRegion":{"type":"string","description":"油田标识","x-dms-title":"油田标识","x-dms-originalType":"string"},"dataSource":{"type":"string","description":"填写数据来源的表CODE","x-dms-title":"数据来源","x-dms-originalType":"string"},"desgWellId":{"type":"string","description":"设计井的唯一标识","x-dms-title":"设计井ID","x-dms-originalType":"string"},"energyType":{"type":"string","description":"描述本井生产的油气资源类型,如煤层气、致密气、页岩气等","x-dms-title":"能源类型","x-dms-originalType":"string"},"platformId":{"type":"string","description":"平台ID","x-dms-title":"平台ID","x-dms-originalType":"string"},"updateDate":{"type":"string","format":"date","description":"记录数据在本系统最新的更新时间,需精确到时分秒,默认=创建时间","x-dms-title":"更新日期","x-dms-originalType":"date"},"wellTypeId":{"type":"string","description":"属性规范值字段,引用属性代码WELL_TYPE下的属性值","x-dms-title":"井型ID","x-dms-originalType":"string"},"abandonDate":{"type":"string","format":"date","description":"报废日期","x-dms-title":"报废日期","x-dms-originalType":"date"},"abondonType":{"type":"string","description":"报废类型","x-dms-title":"报废类型","x-dms-originalType":"string"},"actualXAxis":{"type":"number","description":"实际X坐标,实际X坐标","x-dms-mask":"coordinate","x-dms-geom":"Point.x.actual","x-dms-title":"实际X坐标,实际X坐标","x-dms-originalType":"number"},"actualYAxis":{"type":"number","description":"实际Y坐标,实际Y坐标","x-dms-mask":"coordinate","x-dms-geom":"Point.y.actual","x-dms-title":"实际Y坐标,实际Y坐标","x-dms-originalType":"number"},"checkUserId":{"type":"string","description":"记录数据在本系统的审核用户","x-dms-title":"审核用户","x-dms-originalType":"string"},"createAppId":{"type":"string","description":"填写数据来源的系统名","x-dms-title":"创建应用","x-dms-originalType":"string"},"designXAxis":{"type":"number","description":"设计X坐标,设计X坐标","x-dms-mask":"coordinate","x-dms-geom":"Point.x.design","x-dms-title":"设计X坐标,设计X坐标","x-dms-originalType":"number"},"designYAxis":{"type":"number","description":"设计Y坐标,设计Y坐标","x-dms-mask":"coordinate","x-dms-geom":"Point.y.design","x-dms-title":"设计Y坐标,设计Y坐标","x-dms-originalType":"number"},"projectName":{"type":"string","description":"地质单元名称,填写地质单元的中文名称,该名称在整个油田公司内不能重名,必填","x-dms-title":"地质单元名称","x-dms-originalType":"string"},"stationName":{"type":"string","description":"站库名称,必填","x-dms-title":"站库名称","x-dms-originalType":"string"},"wellPurpose":{"type":"string","description":"属性规范值字段,引用属性代码WELL_PURPOSE下的属性值","x-dms-title":"井别","x-dms-originalType":"string"},"activityName":{"type":"string","description":"项目名称,必填","x-dms-title":"项目名称","x-dms-originalType":"string"},"completionMd":{"type":"number","description":"完钻井深","x-dms-title":"完钻井深","x-dms-originalType":"number"},"createUserId":{"type":"string","description":"记录数据在本系统的创建用户","x-dms-title":"创建用户","x-dms-originalType":"string"},"keyWellLevel":{"type":"string","description":"重点井级别","x-dms-title":"重点井级别","x-dms-originalType":"string"},"platformName":{"type":"string","description":"平台名称","x-dms-title":"平台名称","x-dms-originalType":"string"},"sourceDataId":{"type":"string","description":"存储数据来源的主键信息","x-dms-title":"源库ID标识","x-dms-originalType":"string"},"structurePos":{"type":"string","description":"构造位置的描述","x-dms-title":"构造位置","x-dms-originalType":"string"},"updateUserId":{"type":"string","description":"记录数据在本系统最新的更新用户,默认=创建用户","x-dms-title":"更新用户","x-dms-originalType":"string"},"geoOffsetEast":{"type":"number","description":"井口横坐标","x-dms-mask":"coordinate","x-dms-geom":"Point.x.wellhead","x-dms-title":"井口横坐标","x-dms-originalType":"number"},"seismicLineNo":{"type":"string","description":"井旁地震测线号(勘探井)","x-dms-title":"井旁地震测线号","x-dms-originalType":"string"},"wellLegalName":{"type":"string","description":"规范的井号名称,井号名称命名规范请参考《主数据库技术标准》","x-dms-title":"拼音井号","x-dms-originalType":"string"},"wellPurposeId":{"type":"string","description":"属性规范值字段,引用属性代码WELL_PURPOSE下的属性值","x-dms-title":"井别ID","x-dms-originalType":"string"},"completionDate":{"type":"string","format":"date","description":"完井日期","x-dms-title":"完井日期","x-dms-originalType":"date"},"geoDescription":{"type":"string","description":"地理位置的描述","x-dms-title":"地理位置","x-dms-originalType":"string"},"geoOffsetNorth":{"type":"number","description":"井口纵坐标","x-dms-mask":"coordinate","x-dms-geom":"Point.y.wellhead","x-dms-title":"井口纵坐标","x-dms-originalType":"number"},"wellCommonName":{"type":"string","description":"通用井名,来源于钻井公报的汉字井名,必填","x-dms-title":"井名","x-dms-originalType":"string"},"endDrillingDate":{"type":"string","format":"date","description":"最后一个井筒的完钻日期","x-dms-title":"完钻日期","x-dms-originalType":"date"},"targetFormation":{"type":"string","description":"目的层","x-dms-title":"目的层","x-dms-originalType":"string"},"completionMethod":{"type":"string","description":"完井方法","x-dms-title":"完井方法","x-dms-originalType":"string"},"registrationDate":{"type":"string","format":"date","description":"井位通知单下达日期","x-dms-title":"注册日期","x-dms-originalType":"date"},"sourceCreateDate":{"type":"string","format":"date","description":"记录源头系统采集数据的时间","x-dms-title":"源头数据采集时间","x-dms-originalType":"date"},"coordinateSystemId":{"type":"string","description":"坐标系统的唯一标示符","x-dms-geom":"Point.srid.wellhead,Point.srid.design,Point.srid.actual","x-dms-title":"坐标系统ID","x-dms-originalType":"string"},"completionFormation":{"type":"string","description":"钻井完钻层位","x-dms-title":"钻井完钻层位","x-dms-originalType":"string"},"coordinateSystemName":{"type":"string","description":"坐标系名称","x-dms-title":"坐标系名称","x-dms-originalType":"string"},"wellbaseGeoOffsetEast":{"type":"number","description":"井底横坐标","x-dms-title":"井底横坐标","x-dms-originalType":"number"},"wellbaseGeoOffsetNorth":{"type":"number","description":"井底纵坐标","x-dms-title":"井底纵坐标","x-dms-originalType":"number"}},"description":"井","x-dms-groupView":[{"name":"groupByWellCommonName","group":["dataRegion","wellCommonName","wellId"]},{"name":"groupByPlatformName","group":["dataRegion","platformName"]}],"x-dms-identityId":["dsid"],"x-dms-naturalKey":["dataRegion","wellCommonName"],"x-dms-defaultShow":["wellCommonName"]}}}} diff --git a/compiliance-js/prepare-llm-request.js b/compiliance-js/prepare-llm-request.js new file mode 100644 index 0000000..81a475c --- /dev/null +++ b/compiliance-js/prepare-llm-request.js @@ -0,0 +1,489 @@ +'use strict'; + +/** + * Node-RED Function 节点脚本:准备大模型 HTTP 请求。 + * 将生成 prompt、HTTP 请求参数,并把上下文写入 msg.llmContext。 + */ + +const DEFAULT_API_KEY = 'sk-lbGrsUPL1iby86h554FaE536C343435dAa9bA65967A840B2'; +const DEFAULT_BASE_URL = 'https://aiproxy.petrotech.cnpc/v1'; +const DEFAULT_ENDPOINT_PATH = '/chat/completions'; +const DEFAULT_MODEL = 'deepseek-v3'; + +function prepareLlmRequest(msg, node) { + if (!msg.operationId) { + msg.operationId = 'createResource'; + } + + try { + const apiDoc = extractOpenApiDocument(msg); + const operationCtx = resolveOperationContext(msg, apiDoc); + + if (!operationCtx.operation) { + const err = `未找到匹配的接口定义(operationId=${operationCtx.requestedOperationId || '未指定'}, path=${operationCtx.requestedPath || '未指定'}, method=${operationCtx.requestedMethod || '未指定'})`; + node.error(err, msg); + msg.error = err; + return null; + } + + const prompt = buildPrompt(operationCtx, apiDoc, msg); + const requestPayload = buildRequestPayload(prompt, msg); + + const apiKey = (msg.llm && msg.llm.apiKey) || DEFAULT_API_KEY; + const baseUrl = (msg.llm && msg.llm.baseUrl) || DEFAULT_BASE_URL; + const endpointPath = (msg.llm && msg.llm.endpoint) || DEFAULT_ENDPOINT_PATH; + + const url = buildUrl(baseUrl, endpointPath); + msg.method = 'POST'; + msg.url = url; + msg.headers = Object.assign({}, msg.headers, { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + }); + msg.payload = requestPayload; + msg.rejectUnauthorized = false; + + msg.llmContext = { + prompt, + operationCtx, + provider: 'dashscope', + requestConfig: { + baseUrl, + endpointPath, + model: requestPayload.model, + temperature: requestPayload.temperature, + }, + }; + delete msg.error; + return msg; + } catch (error) { + node.error(`LLM 请求准备失败:${error.message}`, msg); + msg.error = error.message; + return null; + } +} + +function extractOpenApiDocument(message) { + const candidate = + message && message.oas_def ? message.oas_def : + message && message.swagger ? message.swagger : + message && message.payload ? message.payload : + null; + + if (!candidate) { + throw new Error('未提供 OpenAPI 文档(需要 msg.oas_def / msg.swagger / msg.payload)'); + } + + if (typeof candidate === 'string') { + try { + return JSON.parse(candidate); + } catch (error) { + throw new Error(`OpenAPI JSON 解析失败:${error.message}`); + } + } + + if (typeof candidate !== 'object') { + throw new Error('OpenAPI 文档必须是对象或 JSON 字符串'); + } + + if (!candidate.paths || typeof candidate.paths !== 'object') { + throw new Error('OpenAPI 文档缺少 paths 字段'); + } + + return candidate; +} + +function resolveOperationContext(message, apiDoc) { + const requestedOperationId = + (message && message.operationId) || + (message && message.req && message.req.body && message.req.body.operationId) || + null; + + const requestedPath = + (message && message.path) || + (message && message.req && message.req.body && message.req.body.path) || + null; + + const requestedMethodRaw = + (message && message.method) || + (message && message.req && message.req.body && message.req.body.method) || + null; + const requestedMethod = requestedMethodRaw ? String(requestedMethodRaw).toLowerCase() : null; + + const operations = enumerateOperations(apiDoc); + + let matched = null; + if (requestedOperationId) { + matched = operations.find(op => op.operation.operationId === requestedOperationId); + } + + if (!matched && requestedPath && requestedMethod) { + matched = operations.find(op => op.path === requestedPath && op.method === requestedMethod); + } + + let autoSelected = false; + if (!matched && operations.length > 0) { + matched = operations[0]; + autoSelected = true; + } + + return { + operation: matched ? matched.operation : undefined, + method: matched ? matched.method : undefined, + pathItem: matched ? matched.pathItem : undefined, + path: matched ? matched.path : undefined, + autoSelected, + candidates: operations.map(op => ({ + operationId: op.operation && op.operation.operationId ? op.operation.operationId : '', + method: op.method ? op.method.toUpperCase() : '', + path: op.path, + summary: op.operation && op.operation.summary ? op.operation.summary : '', + })), + requestedOperationId, + requestedPath, + requestedMethod, + }; +} + +function enumerateOperations(apiDoc) { + const results = []; + const validMethods = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace']; + for (const path of Object.keys(apiDoc.paths || {})) { + const pathItem = apiDoc.paths[path]; + if (!pathItem || typeof pathItem !== 'object') { + continue; + } + for (const method of validMethods) { + if (pathItem[method] && typeof pathItem[method] === 'object') { + results.push({ + path, + method, + operation: pathItem[method], + pathItem, + }); + } + } + } + return results; +} + +function buildPrompt(operationCtx, apiDoc, message) { + const operation = operationCtx.operation; + const method = (operationCtx.method || '').toUpperCase(); + const path = operationCtx.path || ''; + const summary = operation.summary || ''; + const description = operation.description || ''; + + const parameters = gatherParameters(operationCtx, apiDoc); + const requestBodySchema = gatherRequestBodySchema(operation, apiDoc); + + const userNotes = message && message.prompt ? String(message.prompt) : ''; + const requiredFields = extractRequiredFields(requestBodySchema, apiDoc); + const requiredFieldsText = requiredFields.length > 0 + ? requiredFields.map(field => `- ${field}`).join('\n') + : ''; + + const lines = []; + lines.push('你是一名负责生成 HTTP 接口测试参数的助手,请严格输出以下结构的 JSON:'); + lines.push('{'); + lines.push(' "pathParams": { ... },'); + lines.push(' "query": { ... },'); + lines.push(' "headers": { ... },'); + lines.push(' "cookies": { ... },'); + lines.push(' "body": { ... },'); + lines.push(' "notes": "..."'); + lines.push('}'); + lines.push('未用到的分区请返回空对象,不要在 JSON 外输出任何文字。'); + lines.push(''); + lines.push('body.data[0] 必须遵守:'); + lines.push('- 覆盖 schema 中声明的全部必填字段,并给出符合字段类型/格式的真实感样例值。'); + lines.push('- 非必填字段也尽可能全覆盖'); + lines.push('- 不得遗漏必填字段;若 schema 内有嵌套对象/数组的必填字段,同样要补齐。'); + lines.push('- 保持数值、日期、字符串等格式,只在 schema 无提示时使用 "sample"、"2025-01-01" 等占位值。'); + if (requiredFieldsText) { + lines.push('必填字段清单(必须全部出现在 body.data[0] 中):'); + lines.push(requiredFieldsText); + } + lines.push(''); + lines.push('若 schema 中存在数组元素或复合结构的必填字段,也要为这些子字段提供值。'); + lines.push(''); + lines.push(`Target operation: ${method} ${path}`); + + if (summary) { + lines.push(`Summary: ${summary}`); + } + if (description) { + lines.push(`Description: ${description}`); + } + if (parameters.length > 0) { + lines.push('Parameters:'); + for (const param of parameters) { + lines.push(`- [${param.in}] ${param.name}: ${param.type || 'any'}${param.required ? ' (required)' : ''}${param.description ? ` - ${param.description}` : ''}`); + } + } else { + lines.push('Parameters: none defined.'); + } + + if (requestBodySchema) { + lines.push('Request body schema (JSON Schema excerpt):'); + lines.push(indentSnippet(JSON.stringify(requestBodySchema, null, 2), 2)); + } else { + lines.push('Request body: not defined.'); + } + + if (userNotes) { + lines.push(''); + lines.push('User notes:'); + lines.push(userNotes); + } + + return lines.join('\n'); +} + +function gatherParameters(operationCtx, apiDoc) { + const aggregated = []; + const seen = new Set(); + + const sources = []; + if (Array.isArray(operationCtx.pathItem && operationCtx.pathItem.parameters)) { + sources.push(operationCtx.pathItem.parameters); + } + if (Array.isArray(operationCtx.operation.parameters)) { + sources.push(operationCtx.operation.parameters); + } + + for (const list of sources) { + for (const param of list) { + if (!param || typeof param !== 'object') { + continue; + } + const resolved = resolveMaybeRef(param, apiDoc); + const key = `${resolved.in}:${resolved.name}`; + if (!resolved.name || seen.has(key)) { + continue; + } + seen.add(key); + aggregated.push({ + name: resolved.name, + in: resolved.in || 'query', + required: !!resolved.required, + description: resolved.description || '', + type: resolved.schema ? inferFriendlyType(resolved.schema, apiDoc) : '', + }); + } + } + + return aggregated; +} + +function gatherRequestBodySchema(operation, apiDoc) { + const requestBody = resolveMaybeRef(operation.requestBody, apiDoc); + if (!requestBody || typeof requestBody !== 'object' || !requestBody.content) { + return null; + } + + const content = requestBody.content; + const mediaType = Object.keys(content).find(key => key.includes('json')) || Object.keys(content)[0]; + if (!mediaType) { + return null; + } + + const mediaObject = resolveMaybeRef(content[mediaType], apiDoc); + if (!mediaObject || typeof mediaObject !== 'object' || !mediaObject.schema) { + return null; + } + + const schema = resolveMaybeRef(mediaObject.schema, apiDoc); + return resolveSchemaDeep(schema, apiDoc); +} + +function inferFriendlyType(schema, apiDoc) { + if (!schema) { + return ''; + } + const resolved = resolveMaybeRef(schema, apiDoc); + if (!resolved || typeof resolved !== 'object') { + return ''; + } + + if (resolved.type) { + if (resolved.type === 'array' && resolved.items) { + const itemType = inferFriendlyType(resolved.items, apiDoc) || 'any'; + return `[${itemType}]`; + } + return resolved.type; + } + + if (resolved.enum && Array.isArray(resolved.enum)) { + return `enum(${resolved.enum.slice(0, 3).join(', ')}${resolved.enum.length > 3 ? ', …' : ''})`; + } + + if (resolved.properties) { + return 'object'; + } + + return ''; +} + +function buildRequestPayload(prompt, message) { + const systemPrompt = (message.llm && message.llm.systemPrompt) || + 'You write JSON only. Focus on realistic values for testing HTTP APIs.'; + + const model = (message.llm && message.llm.model) || DEFAULT_MODEL; + const temperature = (message.llm && typeof message.llm.temperature === 'number') + ? message.llm.temperature : 0.2; + + return { + model, + temperature, + response_format: { type: 'json_object' }, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: prompt }, + ], + }; +} + +function buildUrl(baseUrl, endpointPath) { + const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + const path = endpointPath.startsWith('/') ? endpointPath.slice(1) : endpointPath; + return `${normalizedBase}${path}`; +} + +function resolveMaybeRef(node, apiDoc) { + if (!node || typeof node !== 'object') { + return node; + } + if (!node.$ref) { + return node; + } + + const resolved = resolveRef(node.$ref, apiDoc); + if (!resolved || typeof resolved !== 'object') { + return node; + } + + const remainder = Object.assign({}, node); + delete remainder.$ref; + return Object.assign({}, clone(resolved), remainder); +} + +function resolveRef(ref, apiDoc) { + if (typeof ref !== 'string' || !ref.startsWith('#/')) { + return null; + } + + const tokens = ref.slice(2).split('/').map(unescapeRefToken); + let current = apiDoc; + for (const token of tokens) { + if (current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, token)) { + current = current[token]; + } else { + return null; + } + } + return current; +} + +function unescapeRefToken(token) { + return token.replace(/~1/g, '/').replace(/~0/g, '~'); +} + +function indentSnippet(text, indentLevel, maxLength) { + const trimmed = maxLength && text.length > maxLength ? `${text.slice(0, maxLength)}…` : text; + const indent = ' '.repeat(indentLevel * 2); + return trimmed.split('\n').map(line => `${indent}${line}`).join('\n'); +} + +function clone(value) { + return value == null ? value : JSON.parse(JSON.stringify(value)); +} + +function extractRequiredFields(schema, apiDoc) { + const result = new Set(); + collectRequiredFields(schema, apiDoc, result, ''); + return Array.from(result); +} + +function collectRequiredFields(schema, apiDoc, result, pathPrefix) { + if (!schema || typeof schema !== 'object') { + return; + } + + if (schema.$ref) { + const resolved = resolveRef(schema.$ref, apiDoc); + if (!resolved) { + return; + } + collectRequiredFields(resolveSchemaDeep(resolved, apiDoc), apiDoc, result, pathPrefix); + return; + } + + if (Array.isArray(schema.required) && schema.properties && typeof schema.properties === 'object') { + for (const key of schema.required) { + const nextPath = pathPrefix ? `${pathPrefix}.${key}` : key; + result.add(nextPath); + collectRequiredFields(schema.properties[key], apiDoc, result, nextPath); + } + } + + if (schema.type === 'array' && schema.items) { + const nextPrefix = pathPrefix ? `${pathPrefix}[]` : '[]'; + collectRequiredFields(schema.items, apiDoc, result, nextPrefix); + } + + if (schema.allOf && Array.isArray(schema.allOf)) { + for (const part of schema.allOf) { + collectRequiredFields(part, apiDoc, result, pathPrefix); + } + } +} + +function resolveSchemaDeep(schema, apiDoc, seen = new Set()) { + if (!schema || typeof schema !== 'object') { + return schema; + } + + if (schema.$ref) { + const ref = schema.$ref; + if (seen.has(ref)) { + return {}; + } + seen.add(ref); + const resolved = resolveRef(ref, apiDoc); + if (!resolved) { + return schema; + } + const merged = Object.assign({}, clone(resolved), clone(schema)); + delete merged.$ref; + return resolveSchemaDeep(merged, apiDoc, seen); + } + + const cloned = clone(schema); + + if (cloned.properties && typeof cloned.properties === 'object') { + for (const key of Object.keys(cloned.properties)) { + cloned.properties[key] = resolveSchemaDeep(cloned.properties[key], apiDoc, new Set(seen)); + } + } + + if (cloned.items) { + cloned.items = resolveSchemaDeep(cloned.items, apiDoc, new Set(seen)); + } + + if (cloned.allOf && Array.isArray(cloned.allOf)) { + cloned.allOf = cloned.allOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen))); + } + + if (cloned.oneOf && Array.isArray(cloned.oneOf)) { + cloned.oneOf = cloned.oneOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen))); + } + + if (cloned.anyOf && Array.isArray(cloned.anyOf)) { + cloned.anyOf = cloned.anyOf.map(item => resolveSchemaDeep(item, apiDoc, new Set(seen))); + } + + return cloned; +} + +return prepareLlmRequest(msg, node); diff --git a/compiliance-js/preview-prompt.js b/compiliance-js/preview-prompt.js new file mode 100644 index 0000000..5e18557 --- /dev/null +++ b/compiliance-js/preview-prompt.js @@ -0,0 +1,48 @@ +'use strict'; + +/** + * Usage: node compiliance-js/preview-prompt.js path/to/oas.json [operationId] + * Reads the OAS file, loads prepare-llm-request.js in a sandbox, and prints the prompt. + */ + +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const [, , oasPath, operationIdArg] = process.argv; +if (!oasPath) { + console.error('Usage: node compiliance-js/preview-prompt.js [operationId]'); + process.exit(1); +} + +const oas = JSON.parse(fs.readFileSync(oasPath, 'utf8')); +let code = fs.readFileSync(path.join(__dirname, 'prepare-llm-request.js'), 'utf8'); +code = code.replace(/\nreturn\s+prepareLlmRequest\(msg,\s*node\);?\s*$/m, '\n'); + +const context = { + msg: { oas_def: oas, operationId: operationIdArg || 'createResource' }, + node: { + error: (err) => { throw new Error(err); }, + warn: (...args) => console.warn('[WARN]', ...args), + log: (...args) => console.log('[LOG]', ...args), + }, + console, + Buffer, + process, + require, + setTimeout, + env: { get: () => undefined }, +}; + +vm.createContext(context); +vm.runInContext(code, context); + +if (!context.buildPrompt || !context.resolveOperationContext || !context.extractOpenApiDocument) { + throw new Error('Failed to load helper functions from prepare-llm-request.js'); +} + +const apiDoc = context.extractOpenApiDocument(context.msg); +const operationCtx = context.resolveOperationContext(context.msg, apiDoc); +const prompt = context.buildPrompt(operationCtx, apiDoc, context.msg); + +console.log(prompt); diff --git a/compiliance-js/process-llm-response.js b/compiliance-js/process-llm-response.js new file mode 100644 index 0000000..b87218b --- /dev/null +++ b/compiliance-js/process-llm-response.js @@ -0,0 +1,119 @@ +'use strict'; + +/** + * Node-RED Function 节点脚本:处理大模型 HTTP 响应。 + * 将响应解析为 mock 数据并写回 msg。 + */ + +function processLlmResponse(msg, node) { + const context = msg.llmContext || {}; + if (!context.prompt || !context.operationCtx) { + const err = 'LLM 上下文缺失,无法解析响应'; + node.error(err, msg); + msg.error = err; + return msg; + } + + if (msg.statusCode && (msg.statusCode < 200 || msg.statusCode >= 300)) { + const err = `大模型接口返回状态码 ${msg.statusCode}`; + node.error(err, msg); + msg.error = err; + return msg; + } + + let raw = msg.payload; + if (Buffer.isBuffer(raw)) { + raw = raw.toString('utf8'); + } + if (typeof raw === 'string') { + try { + raw = JSON.parse(raw); + } catch (error) { + node.error(`无法解析大模型响应:${error.message}`, msg); + msg.error = error.message; + return msg; + } + } + + try { + const parsed = parseModelResponse(raw); + + msg.llmRaw = raw; + msg.mock = Object.assign({}, msg.mock, parsed.mock); + msg.mockSource = context.provider || 'dashscope'; + msg.mockPrompt = context.prompt; + msg.mockCandidates = context.operationCtx.candidates; + msg.mockAutoSelected = !!context.operationCtx.autoSelected; + msg.payload = msg.mock; + + delete msg.llmContext; + if (msg.headers && msg.headers.Authorization) { + delete msg.headers.Authorization; + } + + delete msg.error; + return msg; + } catch (error) { + node.error(`大模型参数生成失败:${error.message}`, msg); + msg.error = error.message; + return msg; + } +} + +function parseModelResponse(response) { + if (!response || typeof response !== 'object') { + throw new Error('大模型响应为空或不是对象'); + } + + if (response.mock && typeof response.mock === 'object') { + return { mock: response.mock }; + } + + const choices = Array.isArray(response.choices) ? response.choices : []; + const firstChoice = choices[0]; + const message = firstChoice && firstChoice.message ? firstChoice.message : null; + const content = message && typeof message === 'object' ? message.content : null; + + if (!content) { + throw new Error('响应中缺少 choices[0].message.content'); + } + + let mockObject; + if (typeof content === 'string') { + try { + mockObject = JSON.parse(content); + } catch (err) { + throw new Error(`无法解析模型返回的 JSON:${err.message},原始文本:${content}`); + } + } else if (Array.isArray(content)) { + const jsonPart = content.find(part => part.type === 'output_text' || part.type === 'text' || part.type === 'json'); + const text = jsonPart && jsonPart.text ? jsonPart.text : null; + if (!text) { + throw new Error('响应内容不是字符串,且未找到可解析的文本段'); + } + try { + mockObject = JSON.parse(text); + } catch (err) { + throw new Error(`无法解析模型返回的 JSON:${err.message},原始文本:${text}`); + } + } else { + throw new Error('模型返回的 message.content 既不是字符串也不是文本片段数组'); + } + + if (!mockObject || typeof mockObject !== 'object') { + throw new Error('模型返回的 JSON 不是对象'); + } + + const normalisedMock = { + pathParams: mockObject.pathParams || mockObject.path_parameters || {}, + query: mockObject.query || mockObject.query_params || {}, + headers: mockObject.headers || {}, + cookies: mockObject.cookies || {}, + body: mockObject.body || {}, + notes: mockObject.notes || '', + }; + + return { mock: normalisedMock }; +} + +return processLlmResponse(msg, node);