flowmind 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/core/adapters/mcp-adapter.js +25 -0
- package/core/adapters/workflow-adapter.js +26 -0
- package/core/component-registry.js +19 -1
- package/core/config-manager.js +7 -2
- package/core/index.js +14 -1
- package/core/mcp-http-client.js +63 -0
- package/core/providers/aliyun/rds-query-adapter.js +70 -0
- package/core/providers/friday/flow-adapter.js +19 -30
- package/core/sdd-agent-sync.js +341 -17
- package/core/source-inference.js +324 -0
- package/package.json +1 -1
- package/skills/auto-flow/index.js +528 -52
- package/skills/data-logic-validation/index.js +133 -12
- package/skills/resource-bind/index.js +61 -18
- package/tui/app.jsx +2 -1
- package/tui/format-result.js +43 -4
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Route deployment and workflow requests to the configured workflow MCP adapter.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
6
10
|
const BUILTIN_WORKFLOWS = {
|
|
7
11
|
'dev-workflow': {
|
|
8
12
|
name: 'Development Workflow',
|
|
@@ -39,6 +43,21 @@ const TOOL_NAMES = {
|
|
|
39
43
|
listPipelineRuns: 'flowListPipelineRuns'
|
|
40
44
|
};
|
|
41
45
|
|
|
46
|
+
const LOCAL_PIPELINE_MAP_CANDIDATES = [
|
|
47
|
+
process.env.FLOWMIND_AUTO_FLOW_MAP,
|
|
48
|
+
path.join(process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir(), '.flowmind', 'source', 'auto-flow-pipeline-map.json'),
|
|
49
|
+
path.join(process.cwd(), 'source', 'auto-flow-pipeline-map.json'),
|
|
50
|
+
path.join(__dirname, '..', '..', '..', 'source', 'auto-flow-pipeline-map.json')
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
|
|
53
|
+
const ENV_PREFIX_TO_ENV = {
|
|
54
|
+
test: 'test',
|
|
55
|
+
dev: 'test',
|
|
56
|
+
uat: 'uat',
|
|
57
|
+
gray: 'gray',
|
|
58
|
+
prod: 'prod'
|
|
59
|
+
};
|
|
60
|
+
|
|
42
61
|
module.exports = {
|
|
43
62
|
canHandle(input) {
|
|
44
63
|
if (!input) return false;
|
|
@@ -77,27 +96,39 @@ module.exports = {
|
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
if (params.action === 'status') {
|
|
80
|
-
|
|
99
|
+
await executeStatus(workflow, params, input);
|
|
100
|
+
return {
|
|
101
|
+
type: 'result',
|
|
102
|
+
skill: 'auto-flow',
|
|
103
|
+
message: 'Workflow status: query completed',
|
|
104
|
+
data: {
|
|
105
|
+
action: 'status',
|
|
106
|
+
provider: workflow.provider,
|
|
107
|
+
binding: workflow.binding,
|
|
108
|
+
status: 'completed',
|
|
109
|
+
mcpServer: getWorkflowMcpServer(workflow)
|
|
110
|
+
},
|
|
111
|
+
input,
|
|
112
|
+
timestamp: new Date().toISOString()
|
|
113
|
+
};
|
|
81
114
|
}
|
|
82
115
|
|
|
83
116
|
if (params.action === 'deploy' || params.action === 'run') {
|
|
84
|
-
return executeDeploy(workflow, params, input);
|
|
117
|
+
return executeDeploy(workflow, params, input, context);
|
|
85
118
|
}
|
|
86
119
|
|
|
87
120
|
if (params.action === 'list' || params.serviceNames.length > 0) {
|
|
88
|
-
|
|
121
|
+
await workflow.client.listPipelines(buildListPipelineParams(params));
|
|
89
122
|
return {
|
|
90
123
|
type: 'result',
|
|
91
124
|
skill: 'auto-flow',
|
|
92
|
-
message:
|
|
93
|
-
? `Resolved workflow query for: ${params.serviceNames.join(', ')}`
|
|
94
|
-
: 'Listing available deployment pipelines',
|
|
125
|
+
message: 'Workflow status: pipeline lookup completed',
|
|
95
126
|
data: {
|
|
96
127
|
action: 'list',
|
|
97
128
|
provider: workflow.provider,
|
|
98
129
|
binding: workflow.binding,
|
|
99
|
-
|
|
100
|
-
|
|
130
|
+
status: 'completed',
|
|
131
|
+
mcpServer: getWorkflowMcpServer(workflow)
|
|
101
132
|
},
|
|
102
133
|
input,
|
|
103
134
|
timestamp: new Date().toISOString()
|
|
@@ -147,7 +178,8 @@ function createWorkflowClient(context) {
|
|
|
147
178
|
return {
|
|
148
179
|
client: adapter,
|
|
149
180
|
provider: adapter.providerName,
|
|
150
|
-
binding: context.resourceBinding?.componentType === 'workflow' ? context.resourceBinding : null
|
|
181
|
+
binding: context.resourceBinding?.componentType === 'workflow' ? context.resourceBinding : null,
|
|
182
|
+
transportBacked: true
|
|
151
183
|
};
|
|
152
184
|
}
|
|
153
185
|
|
|
@@ -156,7 +188,7 @@ function createWorkflowClient(context) {
|
|
|
156
188
|
: null;
|
|
157
189
|
|
|
158
190
|
if (!binding?.mcpServer) {
|
|
159
|
-
return { client: null, provider: null, binding: null };
|
|
191
|
+
return { client: null, provider: null, binding: null, transportBacked: false };
|
|
160
192
|
}
|
|
161
193
|
|
|
162
194
|
return {
|
|
@@ -165,6 +197,15 @@ function createWorkflowClient(context) {
|
|
|
165
197
|
async listPipelines(params) {
|
|
166
198
|
return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.listPipelines, params };
|
|
167
199
|
},
|
|
200
|
+
async getCurrentIterate() {
|
|
201
|
+
return { mcpServer: binding.mcpServer, tool: 'getCurrentIterate', params: {} };
|
|
202
|
+
},
|
|
203
|
+
async listDeployChecklists(params) {
|
|
204
|
+
return { mcpServer: binding.mcpServer, tool: 'listDeployChecklists', params };
|
|
205
|
+
},
|
|
206
|
+
async orderList(params) {
|
|
207
|
+
return { mcpServer: binding.mcpServer, tool: 'orderList', params };
|
|
208
|
+
},
|
|
168
209
|
async startPipelineRun(pipelineId) {
|
|
169
210
|
return { mcpServer: binding.mcpServer, tool: TOOL_NAMES.startPipelineRun, params: { pipelineId } };
|
|
170
211
|
},
|
|
@@ -179,7 +220,8 @@ function createWorkflowClient(context) {
|
|
|
179
220
|
}
|
|
180
221
|
},
|
|
181
222
|
provider: binding.provider || 'workflow-binding',
|
|
182
|
-
binding
|
|
223
|
+
binding,
|
|
224
|
+
transportBacked: false
|
|
183
225
|
};
|
|
184
226
|
}
|
|
185
227
|
|
|
@@ -204,7 +246,7 @@ function buildNoAdapterResult(input, params) {
|
|
|
204
246
|
skill: 'auto-flow',
|
|
205
247
|
message: 'Workflow service not configured. Connect friday-auto-flow first.',
|
|
206
248
|
data: {
|
|
207
|
-
|
|
249
|
+
status: 'not_configured',
|
|
208
250
|
hint: 'Run `flowmind resource` to review current bindings, then save one like: `flowmind "绑定发布业务 mcp=friday-auto-flow token=xxx env=uat"`'
|
|
209
251
|
},
|
|
210
252
|
input,
|
|
@@ -212,20 +254,53 @@ function buildNoAdapterResult(input, params) {
|
|
|
212
254
|
};
|
|
213
255
|
}
|
|
214
256
|
|
|
215
|
-
async function executeDeploy(workflow, params, input) {
|
|
257
|
+
async function executeDeploy(workflow, params, input, context) {
|
|
216
258
|
if (params.pipelineId) {
|
|
217
259
|
const execution = await workflow.client.startPipelineRun(params.pipelineId);
|
|
218
260
|
return {
|
|
219
261
|
type: 'result',
|
|
220
262
|
skill: 'auto-flow',
|
|
221
|
-
message:
|
|
263
|
+
message: 'Workflow status: deployment submitted',
|
|
264
|
+
data: {
|
|
265
|
+
action: 'deploy',
|
|
266
|
+
provider: workflow.provider,
|
|
267
|
+
binding: workflow.binding,
|
|
268
|
+
status: 'submitted',
|
|
269
|
+
mcpServer: getWorkflowMcpServer(workflow),
|
|
270
|
+
execution,
|
|
271
|
+
resolution: {
|
|
272
|
+
source: 'direct-input',
|
|
273
|
+
pipelineId: params.pipelineId
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
input,
|
|
277
|
+
timestamp: new Date().toISOString()
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const resolvedTargets = await resolvePipelineTargets(workflow, params, context);
|
|
282
|
+
if (resolvedTargets.pipelineIds.length > 0) {
|
|
283
|
+
const executions = [];
|
|
284
|
+
for (const pipelineId of resolvedTargets.pipelineIds) {
|
|
285
|
+
executions.push(await workflow.client.startPipelineRun(pipelineId));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
type: 'result',
|
|
290
|
+
skill: 'auto-flow',
|
|
291
|
+
message: 'Workflow status: deployment submitted',
|
|
222
292
|
data: {
|
|
223
293
|
action: 'deploy',
|
|
224
|
-
pipelineId: params.pipelineId,
|
|
225
|
-
environment: params.environment,
|
|
226
294
|
provider: workflow.provider,
|
|
227
295
|
binding: workflow.binding,
|
|
228
|
-
|
|
296
|
+
status: 'submitted',
|
|
297
|
+
mcpServer: getWorkflowMcpServer(workflow),
|
|
298
|
+
execution: executions.length === 1 ? executions[0] : executions,
|
|
299
|
+
resolution: {
|
|
300
|
+
source: resolvedTargets.source,
|
|
301
|
+
pipelineIds: resolvedTargets.pipelineIds,
|
|
302
|
+
services: resolvedTargets.resolutions
|
|
303
|
+
}
|
|
229
304
|
},
|
|
230
305
|
input,
|
|
231
306
|
timestamp: new Date().toISOString()
|
|
@@ -233,61 +308,52 @@ async function executeDeploy(workflow, params, input) {
|
|
|
233
308
|
}
|
|
234
309
|
|
|
235
310
|
const listParams = buildListPipelineParams(params);
|
|
236
|
-
const
|
|
311
|
+
const lookup = await workflow.client.listPipelines(listParams);
|
|
237
312
|
const batchParams = buildBatchRunParams(params);
|
|
238
313
|
const execution = await workflow.client.startBatchPipelineRun(batchParams);
|
|
239
314
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
execution: resolveExecution
|
|
315
|
+
return {
|
|
316
|
+
type: 'result',
|
|
317
|
+
skill: 'auto-flow',
|
|
318
|
+
message: 'Workflow status: deployment submitted',
|
|
319
|
+
data: {
|
|
320
|
+
action: 'deploy',
|
|
321
|
+
provider: workflow.provider,
|
|
322
|
+
binding: workflow.binding,
|
|
323
|
+
status: 'submitted',
|
|
324
|
+
mcpServer: getWorkflowMcpServer(workflow),
|
|
325
|
+
execution,
|
|
326
|
+
resolution: {
|
|
327
|
+
source: 'batch-fallback',
|
|
328
|
+
lookup
|
|
329
|
+
}
|
|
256
330
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
262
|
-
}
|
|
331
|
+
input,
|
|
332
|
+
timestamp: new Date().toISOString()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
263
335
|
|
|
264
336
|
async function executeStatus(workflow, params, input) {
|
|
265
|
-
let execution;
|
|
266
337
|
if (params.pipelineId && params.runId) {
|
|
267
|
-
|
|
338
|
+
await workflow.client.getPipelineRun(params.pipelineId, params.runId);
|
|
268
339
|
} else if (params.pipelineId) {
|
|
269
|
-
|
|
340
|
+
await workflow.client.listPipelineRuns(params.pipelineId, {
|
|
270
341
|
env: params.environment
|
|
271
342
|
});
|
|
272
343
|
} else {
|
|
273
|
-
|
|
344
|
+
await workflow.client.listPipelines(buildListPipelineParams(params));
|
|
274
345
|
}
|
|
275
346
|
|
|
276
347
|
return {
|
|
277
348
|
type: 'result',
|
|
278
349
|
skill: 'auto-flow',
|
|
279
|
-
message:
|
|
280
|
-
? `Querying run status for ${params.runId}`
|
|
281
|
-
: 'Querying pipeline status',
|
|
350
|
+
message: 'Workflow status: query completed',
|
|
282
351
|
data: {
|
|
283
352
|
action: 'status',
|
|
284
|
-
services: params.serviceNames,
|
|
285
|
-
pipelineId: params.pipelineId,
|
|
286
|
-
runId: params.runId,
|
|
287
|
-
environment: params.environment,
|
|
288
353
|
provider: workflow.provider,
|
|
289
354
|
binding: workflow.binding,
|
|
290
|
-
|
|
355
|
+
status: 'completed',
|
|
356
|
+
mcpServer: workflow.binding?.mcpServer || workflow.provider
|
|
291
357
|
},
|
|
292
358
|
input,
|
|
293
359
|
timestamp: new Date().toISOString()
|
|
@@ -337,6 +403,7 @@ function parseFlowParams(input) {
|
|
|
337
403
|
}
|
|
338
404
|
|
|
339
405
|
params.serviceNames = extractServiceNames(input);
|
|
406
|
+
applyPrefixedServiceNormalization(params);
|
|
340
407
|
|
|
341
408
|
if (!params.workflow && /deploy-workflow|dev-workflow/i.test(input)) {
|
|
342
409
|
params.workflow = input.match(/deploy-workflow|dev-workflow/i)[0];
|
|
@@ -394,6 +461,415 @@ function normalizeEnvironment(value) {
|
|
|
394
461
|
return normalized;
|
|
395
462
|
}
|
|
396
463
|
|
|
464
|
+
function applyPrefixedServiceNormalization(params) {
|
|
465
|
+
const normalizedServices = [];
|
|
466
|
+
|
|
467
|
+
for (const originalName of params.serviceNames) {
|
|
468
|
+
const prefixed = splitPrefixedServiceName(originalName);
|
|
469
|
+
if (prefixed) {
|
|
470
|
+
if (!params.environment) {
|
|
471
|
+
params.environment = prefixed.environment;
|
|
472
|
+
}
|
|
473
|
+
normalizedServices.push(prefixed.serviceName);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
normalizedServices.push(originalName);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
params.serviceNames = [...new Set(normalizedServices)];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function splitPrefixedServiceName(serviceName) {
|
|
483
|
+
const match = String(serviceName || '').match(/^(test|dev|uat|gray|prod)-(.+)$/i);
|
|
484
|
+
if (!match || !match[2] || !match[2].includes('-')) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
environment: ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1]),
|
|
490
|
+
serviceName: match[2]
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function resolvePipelineTargets(workflow, params, context) {
|
|
495
|
+
if (params.serviceNames.length === 0) {
|
|
496
|
+
return { source: null, resolutions: [], pipelineIds: [] };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const resolutions = [];
|
|
500
|
+
let source = null;
|
|
501
|
+
|
|
502
|
+
for (const serviceName of params.serviceNames) {
|
|
503
|
+
const resolution = await resolvePipelineForService(workflow, {
|
|
504
|
+
serviceName,
|
|
505
|
+
environment: params.environment,
|
|
506
|
+
context
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (!resolution?.pipelineId) {
|
|
510
|
+
return { source: null, resolutions: [], pipelineIds: [] };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
source = source || resolution.source;
|
|
514
|
+
resolutions.push(resolution);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
source,
|
|
519
|
+
resolutions,
|
|
520
|
+
pipelineIds: [...new Set(resolutions.map((item) => String(item.pipelineId)))]
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function resolvePipelineForService(workflow, { serviceName, environment, context }) {
|
|
525
|
+
const localResolution = resolveLocalPipelineForService(serviceName, environment, context);
|
|
526
|
+
if (localResolution) {
|
|
527
|
+
return localResolution;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!workflow.transportBacked || typeof workflow.client?.getCurrentIterate !== 'function') {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return resolveRemotePipelineForService(workflow.client, serviceName, environment);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function resolveLocalPipelineForService(serviceName, environment, context) {
|
|
538
|
+
const pipelineMap = readLocalPipelineMap(context);
|
|
539
|
+
if (!pipelineMap) return null;
|
|
540
|
+
|
|
541
|
+
const entry = pipelineMap[serviceName];
|
|
542
|
+
if (!entry) return null;
|
|
543
|
+
|
|
544
|
+
const pipeline = selectPipelineFromEntry(entry, environment);
|
|
545
|
+
if (!pipeline?.pipelineId) return null;
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
source: 'local-map',
|
|
549
|
+
serviceName,
|
|
550
|
+
environment: environment || inferEnvironmentFromPipelineName(pipeline.pipelineName),
|
|
551
|
+
pipelineId: String(pipeline.pipelineId),
|
|
552
|
+
pipelineName: pipeline.pipelineName || null
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function readLocalPipelineMap(context) {
|
|
557
|
+
if (context?.pipelineMap && typeof context.pipelineMap === 'object') {
|
|
558
|
+
return context.pipelineMap;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (const candidate of LOCAL_PIPELINE_MAP_CANDIDATES) {
|
|
562
|
+
if (!candidate || !fs.existsSync(candidate)) continue;
|
|
563
|
+
try {
|
|
564
|
+
return JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
565
|
+
} catch (error) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function selectPipelineFromEntry(entry, environment) {
|
|
574
|
+
const preferredKeys = getEnvLookupOrder(environment);
|
|
575
|
+
for (const key of preferredKeys) {
|
|
576
|
+
const candidate = normalizePipelineCollection(entry?.[key]);
|
|
577
|
+
if (candidate.length > 0) {
|
|
578
|
+
return candidate[0];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for (const value of Object.values(entry || {})) {
|
|
583
|
+
const candidate = normalizePipelineCollection(value);
|
|
584
|
+
if (candidate.length > 0) {
|
|
585
|
+
return candidate[0];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function normalizePipelineCollection(value) {
|
|
593
|
+
if (!value) return [];
|
|
594
|
+
const items = Array.isArray(value) ? value : [value];
|
|
595
|
+
return items
|
|
596
|
+
.map((item) => {
|
|
597
|
+
if (!item) return null;
|
|
598
|
+
if (typeof item === 'string') {
|
|
599
|
+
return parsePipelineDescriptor(item);
|
|
600
|
+
}
|
|
601
|
+
if (typeof item === 'object' && item.pipelineId) {
|
|
602
|
+
return {
|
|
603
|
+
pipelineName: item.pipelineName || item.name || null,
|
|
604
|
+
pipelineId: String(item.pipelineId)
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
})
|
|
609
|
+
.filter(Boolean);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function resolveRemotePipelineForService(client, serviceName, environment) {
|
|
613
|
+
const iterateName = await lookupCurrentIterateName(client);
|
|
614
|
+
const searchers = [
|
|
615
|
+
async () => parseRemotePipelineLookup(
|
|
616
|
+
await client.listDeployChecklists({ pageNum: '1', pageSize: '200', iterate: iterateName }),
|
|
617
|
+
serviceName,
|
|
618
|
+
environment,
|
|
619
|
+
'deploy-checklists'
|
|
620
|
+
),
|
|
621
|
+
async () => parseRemotePipelineLookup(
|
|
622
|
+
await client.orderList({ pageNum: '1', pageSize: '200', iterateName }),
|
|
623
|
+
serviceName,
|
|
624
|
+
environment,
|
|
625
|
+
'order-list'
|
|
626
|
+
)
|
|
627
|
+
];
|
|
628
|
+
|
|
629
|
+
for (const search of searchers) {
|
|
630
|
+
const resolution = await search();
|
|
631
|
+
if (resolution?.pipelineId) {
|
|
632
|
+
return resolution;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function lookupCurrentIterateName(client) {
|
|
640
|
+
const payload = unwrapMcpPayload(await client.getCurrentIterate());
|
|
641
|
+
if (payload?.name && typeof payload.name === 'string') {
|
|
642
|
+
return payload.name;
|
|
643
|
+
}
|
|
644
|
+
const values = collectNestedValues(payload);
|
|
645
|
+
const iterateName = values.find((value) => typeof value === 'string' && /\d+\.\d+/.test(value));
|
|
646
|
+
return iterateName || null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function parseRemotePipelineLookup(payload, serviceName, environment, source) {
|
|
650
|
+
const records = extractRecordArray(payload);
|
|
651
|
+
for (const record of records) {
|
|
652
|
+
const fields = parseFields(record.fields);
|
|
653
|
+
if (!fields || !matchesRemoteService(fields, serviceName, environment)) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const pipeline = selectPipelineFromFields(fields, environment, serviceName);
|
|
658
|
+
if (pipeline?.pipelineId) {
|
|
659
|
+
return {
|
|
660
|
+
source,
|
|
661
|
+
serviceName,
|
|
662
|
+
environment: environment || inferEnvironmentFromPipelineName(pipeline.pipelineName),
|
|
663
|
+
pipelineId: String(pipeline.pipelineId),
|
|
664
|
+
pipelineName: pipeline.pipelineName || null
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function extractRecordArray(payload) {
|
|
673
|
+
const normalized = unwrapMcpPayload(payload);
|
|
674
|
+
if (!normalized) return [];
|
|
675
|
+
if (Array.isArray(normalized)) return normalized;
|
|
676
|
+
|
|
677
|
+
const candidates = [
|
|
678
|
+
normalized.data,
|
|
679
|
+
normalized.rows,
|
|
680
|
+
normalized.list,
|
|
681
|
+
normalized.records,
|
|
682
|
+
normalized.result?.data,
|
|
683
|
+
normalized.result?.rows,
|
|
684
|
+
normalized.result?.list,
|
|
685
|
+
normalized.result?.records
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
for (const candidate of candidates) {
|
|
689
|
+
if (Array.isArray(candidate)) {
|
|
690
|
+
return candidate;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function parseFields(fields) {
|
|
698
|
+
if (!fields) return null;
|
|
699
|
+
if (typeof fields === 'object') return fields;
|
|
700
|
+
if (typeof fields !== 'string') return null;
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
return JSON.parse(fields);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function unwrapMcpPayload(payload) {
|
|
710
|
+
if (!payload || typeof payload !== 'object') {
|
|
711
|
+
return payload;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const textContent = Array.isArray(payload.content) ? payload.content : payload.result?.content;
|
|
715
|
+
if (Array.isArray(textContent)) {
|
|
716
|
+
const textItem = textContent.find((item) => item?.type === 'text' && typeof item.text === 'string');
|
|
717
|
+
if (textItem?.text) {
|
|
718
|
+
try {
|
|
719
|
+
return JSON.parse(textItem.text);
|
|
720
|
+
} catch (error) {
|
|
721
|
+
return payload;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return payload;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function matchesRemoteService(fields, serviceName, environment) {
|
|
730
|
+
const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
|
|
731
|
+
if (services.includes(serviceName)) {
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const expectedPipelineName = buildPrefixedPipelineName(serviceName, environment);
|
|
736
|
+
const pipelineCandidates = collectFieldPipelineDescriptors(fields, environment)
|
|
737
|
+
.map((item) => item.pipelineName)
|
|
738
|
+
.filter(Boolean);
|
|
739
|
+
|
|
740
|
+
return expectedPipelineName ? pipelineCandidates.includes(expectedPipelineName) : false;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function selectPipelineFromFields(fields, environment, serviceName) {
|
|
744
|
+
const indexedCandidate = selectIndexedPipelineForService(fields, environment, serviceName);
|
|
745
|
+
if (indexedCandidate?.pipelineId) {
|
|
746
|
+
return indexedCandidate;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const candidates = collectFieldPipelineDescriptors(fields, environment);
|
|
750
|
+
if (candidates.length > 0) {
|
|
751
|
+
return candidates[0];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const pipelineIds = String(fields.pipelineId || '')
|
|
755
|
+
.split(',')
|
|
756
|
+
.map((item) => item.trim())
|
|
757
|
+
.filter(Boolean);
|
|
758
|
+
|
|
759
|
+
if (pipelineIds.length === 0) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
pipelineName: buildPrefixedPipelineName(
|
|
765
|
+
Array.isArray(fields.service) ? fields.service[0] : null,
|
|
766
|
+
environment
|
|
767
|
+
),
|
|
768
|
+
pipelineId: pipelineIds[0]
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function selectIndexedPipelineForService(fields, environment, serviceName) {
|
|
773
|
+
if (!serviceName) return null;
|
|
774
|
+
|
|
775
|
+
const services = Array.isArray(fields.service) ? fields.service.map((item) => String(item).trim()) : [];
|
|
776
|
+
const serviceIndex = services.indexOf(serviceName);
|
|
777
|
+
if (serviceIndex < 0) return null;
|
|
778
|
+
|
|
779
|
+
for (const key of getFieldKeysForEnvironment(environment)) {
|
|
780
|
+
const values = Array.isArray(fields[key]) ? fields[key] : [];
|
|
781
|
+
const parsed = parsePipelineDescriptor(values[serviceIndex]);
|
|
782
|
+
if (parsed?.pipelineId) {
|
|
783
|
+
return parsed;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function collectFieldPipelineDescriptors(fields, environment) {
|
|
791
|
+
const keys = getFieldKeysForEnvironment(environment);
|
|
792
|
+
const results = [];
|
|
793
|
+
|
|
794
|
+
for (const key of keys) {
|
|
795
|
+
const values = Array.isArray(fields[key]) ? fields[key] : (fields[key] ? [fields[key]] : []);
|
|
796
|
+
for (const value of values) {
|
|
797
|
+
const parsed = parsePipelineDescriptor(value);
|
|
798
|
+
if (parsed?.pipelineId) {
|
|
799
|
+
results.push(parsed);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return results;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function getFieldKeysForEnvironment(environment) {
|
|
808
|
+
const normalized = normalizeEnvironment(environment || '');
|
|
809
|
+
if (normalized === 'prod') return ['prodPipeline', 'pipeline'];
|
|
810
|
+
if (normalized === 'gray') return ['grayPipeline', 'prodPipeline', 'pipeline'];
|
|
811
|
+
return ['pipeline', 'grayPipeline', 'prodPipeline'];
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function parsePipelineDescriptor(value) {
|
|
815
|
+
if (!value) return null;
|
|
816
|
+
const parts = String(value).split('#@').map((item) => item.trim()).filter(Boolean);
|
|
817
|
+
if (parts.length < 2) return null;
|
|
818
|
+
return {
|
|
819
|
+
pipelineName: parts[0] || null,
|
|
820
|
+
pipelineId: parts[1] || null
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function getEnvLookupOrder(environment) {
|
|
825
|
+
const normalized = normalizeEnvironment(environment || '');
|
|
826
|
+
if (normalized === 'prod') return ['prod'];
|
|
827
|
+
if (normalized === 'gray') return ['gray', 'prod'];
|
|
828
|
+
if (normalized === 'uat') return ['uat'];
|
|
829
|
+
if (normalized === 'test') return ['test', 'dev', 'uat'];
|
|
830
|
+
return ['uat', 'test', 'dev', 'gray', 'prod'];
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function buildPrefixedPipelineName(serviceName, environment) {
|
|
834
|
+
if (!serviceName) return null;
|
|
835
|
+
const normalized = normalizeEnvironment(environment || '');
|
|
836
|
+
if (!normalized) return null;
|
|
837
|
+
const prefix = normalized === 'test' ? 'test' : normalized;
|
|
838
|
+
return `${prefix}-${serviceName}`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function inferEnvironmentFromPipelineName(pipelineName) {
|
|
842
|
+
const match = String(pipelineName || '').match(/^(test|dev|uat|gray|prod)-/i);
|
|
843
|
+
return match ? (ENV_PREFIX_TO_ENV[match[1].toLowerCase()] || normalizeEnvironment(match[1])) : null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function collectNestedValues(payload, values = []) {
|
|
847
|
+
if (payload == null) {
|
|
848
|
+
return values;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (Array.isArray(payload)) {
|
|
852
|
+
for (const item of payload) {
|
|
853
|
+
collectNestedValues(item, values);
|
|
854
|
+
}
|
|
855
|
+
return values;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (typeof payload === 'object') {
|
|
859
|
+
for (const value of Object.values(payload)) {
|
|
860
|
+
collectNestedValues(value, values);
|
|
861
|
+
}
|
|
862
|
+
return values;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
values.push(payload);
|
|
866
|
+
return values;
|
|
867
|
+
}
|
|
868
|
+
|
|
397
869
|
function compactObject(value) {
|
|
398
870
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ''));
|
|
399
871
|
}
|
|
872
|
+
|
|
873
|
+
function getWorkflowMcpServer(workflow) {
|
|
874
|
+
return workflow.client?.mcpServer || workflow.binding?.mcpServer || workflow.provider;
|
|
875
|
+
}
|