flowmind 1.5.2 → 1.5.4
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 +14 -0
- package/bin/flowmind.js +85 -8
- package/core/adapters/mcp-adapter.js +26 -0
- package/core/adapters/workflow-adapter.js +26 -0
- package/core/ai/providers/mimo.js +1 -1
- package/core/component-registry.js +19 -1
- package/core/config-manager.js +7 -2
- package/core/index.js +153 -2
- package/core/mcp-http-client.js +63 -0
- package/core/providers/aliyun/dms-adapter.js +7 -35
- package/core/providers/aliyun/rds-query-adapter.js +70 -0
- package/core/providers/aliyun/redis-adapter.js +4 -20
- package/core/providers/aliyun/sls-adapter.js +3 -10
- package/core/providers/friday/flow-adapter.js +19 -30
- package/core/providers/friday/report-adapter.js +5 -25
- package/core/providers/yapi/yapi-adapter.js +6 -30
- package/core/providers/yuque/yuque-adapter.js +7 -35
- package/core/sdd-agent-sync.js +240 -16
- package/core/source-inference.js +324 -0
- package/package.json +1 -1
- package/skills/auto-flow/index.js +903 -74
- package/skills/data-logic-validation/index.js +133 -12
- package/skills/log-audit/index.js +94 -1
- package/skills/resource-bind/index.js +61 -18
- package/skills/yapi-sync-interface/index.js +146 -13
- package/skills/yuque-sync-design/index.js +130 -11
- package/tui/app.jsx +15 -5
- package/tui/components/ChatPanel.jsx +1 -1
- package/tui/format-result.js +43 -4
package/core/sdd-agent-sync.js
CHANGED
|
@@ -2,10 +2,14 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
|
|
5
|
+
const SDD_SOURCE_SYNC_FILES = Object.freeze([
|
|
6
|
+
'RESOURCE_INDEX.md',
|
|
7
|
+
'project-db-configs.json',
|
|
8
|
+
'project-git-map.json',
|
|
9
|
+
'feign-link-map.json',
|
|
10
|
+
'auto-flow-apis.md',
|
|
11
|
+
'auto-flow-pipeline-map.json'
|
|
12
|
+
]);
|
|
9
13
|
|
|
10
14
|
const RESOURCE_BINDING_SPECS = Object.freeze({
|
|
11
15
|
yapi: { componentType: 'apiDoc', provider: 'yapi' },
|
|
@@ -23,7 +27,9 @@ function getHomeDir() {
|
|
|
23
27
|
|
|
24
28
|
function normalizeMcpServer(name) {
|
|
25
29
|
if (!name) return null;
|
|
26
|
-
|
|
30
|
+
if (/^[^-]+-yapi-mcp$/i.test(name)) return 'yapi-mcp';
|
|
31
|
+
if (/^[^-]+-yuque-mcp$/i.test(name)) return 'yuque-mcp';
|
|
32
|
+
return name;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
function deepMerge(target, source) {
|
|
@@ -204,7 +210,81 @@ function buildWorkflowResource(skillBindingData = {}) {
|
|
|
204
210
|
};
|
|
205
211
|
}
|
|
206
212
|
|
|
207
|
-
function
|
|
213
|
+
function normalizeWorkflowTransport(transport) {
|
|
214
|
+
if (!transport || !transport.url) return null;
|
|
215
|
+
|
|
216
|
+
const headers = transport.headers || {};
|
|
217
|
+
const normalizedHeaders = {};
|
|
218
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
219
|
+
if (value === undefined || value === null || value === '') continue;
|
|
220
|
+
normalizedHeaders[key] = value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
url: transport.url,
|
|
225
|
+
type: transport.type || 'http',
|
|
226
|
+
headers: normalizedHeaders,
|
|
227
|
+
description: transport.description || 'Workflow MCP server'
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function discoverWorkflowTransport(options = {}) {
|
|
232
|
+
const direct = normalizeWorkflowTransport(options.workflowTransport);
|
|
233
|
+
if (direct) return direct;
|
|
234
|
+
|
|
235
|
+
const envUrl = process.env.FLOWMIND_AUTO_FLOW_MCP_URL;
|
|
236
|
+
if (envUrl) {
|
|
237
|
+
return normalizeWorkflowTransport({
|
|
238
|
+
url: envUrl,
|
|
239
|
+
type: 'http',
|
|
240
|
+
headers: process.env.FLOWMIND_AUTO_FLOW_MCP_TOKEN
|
|
241
|
+
? { mcp_token: process.env.FLOWMIND_AUTO_FLOW_MCP_TOKEN }
|
|
242
|
+
: {},
|
|
243
|
+
description: 'Workflow MCP server from environment'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const debugDir = path.join(os.homedir(), '.claude', 'debug');
|
|
248
|
+
if (!await fs.pathExists(debugDir)) return null;
|
|
249
|
+
|
|
250
|
+
const entries = await fs.readdir(debugDir);
|
|
251
|
+
const candidates = [];
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
const filePath = path.join(debugDir, entry);
|
|
254
|
+
try {
|
|
255
|
+
const stat = await fs.stat(filePath);
|
|
256
|
+
if (stat.isFile()) {
|
|
257
|
+
candidates.push({ filePath, mtimeMs: stat.mtimeMs });
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// skip unreadable files
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
265
|
+
|
|
266
|
+
for (const { filePath } of candidates.slice(0, 10)) {
|
|
267
|
+
try {
|
|
268
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
269
|
+
const match = content.match(/MCP server "friday-auto-flow": HTTP transport options: (\{[^\n]+\})/);
|
|
270
|
+
if (!match) continue;
|
|
271
|
+
|
|
272
|
+
const parsed = JSON.parse(match[1]);
|
|
273
|
+
return normalizeWorkflowTransport({
|
|
274
|
+
url: parsed.url,
|
|
275
|
+
type: parsed.type || 'http',
|
|
276
|
+
headers: parsed.headers || {},
|
|
277
|
+
description: 'Workflow MCP server discovered from Claude debug logs'
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// continue scanning other files
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildWorkflowBindings(skillBindingData = {}, workflowTransport = null) {
|
|
208
288
|
const workflowBinding = skillBindingData.bindings?.['auto-flow'];
|
|
209
289
|
if (!workflowBinding) return [];
|
|
210
290
|
|
|
@@ -249,7 +329,11 @@ function buildWorkflowBindings(skillBindingData = {}) {
|
|
|
249
329
|
learningCount: workflowBinding.learningCount || 0,
|
|
250
330
|
lastLearning: workflowBinding.lastLearning || null,
|
|
251
331
|
records: workflowBinding.records || [],
|
|
252
|
-
rules: workflowBinding.rules || []
|
|
332
|
+
rules: workflowBinding.rules || [],
|
|
333
|
+
transport: workflowTransport ? {
|
|
334
|
+
url: workflowTransport.url,
|
|
335
|
+
type: workflowTransport.type
|
|
336
|
+
} : undefined
|
|
253
337
|
},
|
|
254
338
|
metadata: {
|
|
255
339
|
source: 'sdd-agent',
|
|
@@ -263,7 +347,7 @@ function buildWorkflowBindings(skillBindingData = {}) {
|
|
|
263
347
|
}];
|
|
264
348
|
}
|
|
265
349
|
|
|
266
|
-
function convertResourceBindings(sddConfig, skillBindingData = {}) {
|
|
350
|
+
function convertResourceBindings(sddConfig, skillBindingData = {}, workflowTransport = null) {
|
|
267
351
|
const resources = sddConfig.resources || {};
|
|
268
352
|
const aliasIndex = buildAliasIndex(sddConfig.aliases);
|
|
269
353
|
const bindings = [];
|
|
@@ -281,7 +365,7 @@ function convertResourceBindings(sddConfig, skillBindingData = {}) {
|
|
|
281
365
|
}
|
|
282
366
|
}
|
|
283
367
|
|
|
284
|
-
bindings.push(...buildWorkflowBindings(skillBindingData));
|
|
368
|
+
bindings.push(...buildWorkflowBindings(skillBindingData, workflowTransport));
|
|
285
369
|
|
|
286
370
|
return bindings;
|
|
287
371
|
}
|
|
@@ -317,10 +401,13 @@ function convertSceneMappings(sceneData = {}) {
|
|
|
317
401
|
}));
|
|
318
402
|
}
|
|
319
403
|
|
|
320
|
-
function buildComponentConfig(sddConfig, skillBindingData = {}) {
|
|
404
|
+
function buildComponentConfig(sddConfig, skillBindingData = {}, workflowTransport = null) {
|
|
321
405
|
const resources = sddConfig.resources || {};
|
|
322
406
|
const workflowResource = resources.workflow || buildWorkflowResource(skillBindingData);
|
|
323
407
|
const components = {};
|
|
408
|
+
const directQueryServer = resources.database?.mcpServers?.directQuery
|
|
409
|
+
|| resources.redis?.mcpServers?.directQuery
|
|
410
|
+
|| null;
|
|
324
411
|
|
|
325
412
|
if (resources.sls?.enabled) {
|
|
326
413
|
const defaultEnv = resources.sls.defaultEnv || 'uat';
|
|
@@ -356,6 +443,18 @@ function buildComponentConfig(sddConfig, skillBindingData = {}) {
|
|
|
356
443
|
};
|
|
357
444
|
}
|
|
358
445
|
|
|
446
|
+
if ((resources.database?.enabled || resources.redis?.enabled) && directQueryServer) {
|
|
447
|
+
components.databaseQuery = {
|
|
448
|
+
default: 'aliyun-rds-query',
|
|
449
|
+
providers: {
|
|
450
|
+
'aliyun-rds-query': {
|
|
451
|
+
enabled: true,
|
|
452
|
+
mcpServer: normalizeMcpServer(directQueryServer)
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
359
458
|
if (resources.redis?.enabled && resources.redis.mcpServers?.monitor) {
|
|
360
459
|
components.redisMonitor = {
|
|
361
460
|
default: 'aliyun-redis',
|
|
@@ -398,7 +497,8 @@ function buildComponentConfig(sddConfig, skillBindingData = {}) {
|
|
|
398
497
|
providers: {
|
|
399
498
|
'friday-flow': {
|
|
400
499
|
enabled: true,
|
|
401
|
-
mcpServer: normalizeMcpServer(workflowResource.mcpServer)
|
|
500
|
+
mcpServer: normalizeMcpServer(workflowResource.mcpServer),
|
|
501
|
+
...(workflowTransport ? { transport: workflowTransport } : {})
|
|
402
502
|
}
|
|
403
503
|
}
|
|
404
504
|
};
|
|
@@ -483,6 +583,118 @@ async function readJsonIfExists(filePath, fallback) {
|
|
|
483
583
|
return fs.readJson(filePath);
|
|
484
584
|
}
|
|
485
585
|
|
|
586
|
+
async function statIfExists(filePath) {
|
|
587
|
+
try {
|
|
588
|
+
return await fs.stat(filePath);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function copySelectedSourceFiles(sourceDir, targetDir) {
|
|
595
|
+
const sourceRoot = path.join(sourceDir, 'source');
|
|
596
|
+
const copied = [];
|
|
597
|
+
|
|
598
|
+
if (!(await fs.pathExists(sourceRoot))) {
|
|
599
|
+
return copied;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
await fs.ensureDir(targetDir);
|
|
603
|
+
|
|
604
|
+
for (const fileName of SDD_SOURCE_SYNC_FILES) {
|
|
605
|
+
const sourcePath = path.join(sourceRoot, fileName);
|
|
606
|
+
if (!(await fs.pathExists(sourcePath))) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const targetPath = path.join(targetDir, fileName);
|
|
611
|
+
await fs.copy(sourcePath, targetPath, { overwrite: true, dereference: true });
|
|
612
|
+
copied.push({
|
|
613
|
+
name: fileName,
|
|
614
|
+
sourcePath,
|
|
615
|
+
targetPath
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return copied;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function buildSyncSignature(sourceDir) {
|
|
623
|
+
const trackedFiles = [
|
|
624
|
+
path.join(sourceDir, 'resource-config.json'),
|
|
625
|
+
path.join(sourceDir, 'learning', 'scene-mappings.json'),
|
|
626
|
+
path.join(sourceDir, 'learning', 'skill-bindings.json'),
|
|
627
|
+
...SDD_SOURCE_SYNC_FILES.map((fileName) => path.join(sourceDir, 'source', fileName))
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
const entries = [];
|
|
631
|
+
for (const filePath of trackedFiles) {
|
|
632
|
+
const stat = await statIfExists(filePath);
|
|
633
|
+
if (!stat) continue;
|
|
634
|
+
entries.push({
|
|
635
|
+
path: filePath,
|
|
636
|
+
size: stat.size,
|
|
637
|
+
mtimeMs: stat.mtimeMs
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
642
|
+
return JSON.stringify(entries);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function autoSyncSddAgentToFlowMind(options = {}) {
|
|
646
|
+
if (process.env.FLOWMIND_DISABLE_SDD_AGENT_AUTO_SYNC === '1') {
|
|
647
|
+
return { synced: false, skipped: true, reason: 'disabled' };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const sourceDir = options.sourceDir || path.join(os.homedir(), '.sdd-agent');
|
|
651
|
+
const targetHome = options.targetHome || getHomeDir();
|
|
652
|
+
const sourceConfigPath = path.join(sourceDir, 'resource-config.json');
|
|
653
|
+
|
|
654
|
+
if (!(await fs.pathExists(sourceConfigPath))) {
|
|
655
|
+
return { synced: false, skipped: true, reason: 'missing-source-config' };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const targetDir = path.join(targetHome, '.flowmind');
|
|
659
|
+
const statePath = path.join(targetDir, 'sdd-agent-sync-state.json');
|
|
660
|
+
const signature = await buildSyncSignature(sourceDir);
|
|
661
|
+
const previousState = await readJsonIfExists(statePath, null);
|
|
662
|
+
|
|
663
|
+
if (previousState?.signature === signature) {
|
|
664
|
+
return {
|
|
665
|
+
synced: false,
|
|
666
|
+
skipped: true,
|
|
667
|
+
reason: 'up-to-date',
|
|
668
|
+
sourceDir,
|
|
669
|
+
targetDir,
|
|
670
|
+
statePath
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const summary = await syncSddAgentToFlowMind({
|
|
675
|
+
...options,
|
|
676
|
+
sourceDir,
|
|
677
|
+
targetHome
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
await fs.ensureDir(targetDir);
|
|
681
|
+
await fs.writeJson(statePath, {
|
|
682
|
+
sourceDir,
|
|
683
|
+
targetDir,
|
|
684
|
+
signature,
|
|
685
|
+
syncedAt: new Date().toISOString(),
|
|
686
|
+
files: summary.files,
|
|
687
|
+
counts: summary.counts
|
|
688
|
+
}, { spaces: 2 });
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
synced: true,
|
|
692
|
+
skipped: false,
|
|
693
|
+
statePath,
|
|
694
|
+
...summary
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
486
698
|
async function syncSddAgentToFlowMind(options = {}) {
|
|
487
699
|
const sourceDir = options.sourceDir || path.join(os.homedir(), '.sdd-agent');
|
|
488
700
|
const targetHome = options.targetHome || getHomeDir();
|
|
@@ -500,11 +712,19 @@ async function syncSddAgentToFlowMind(options = {}) {
|
|
|
500
712
|
const sddScenes = await readJsonIfExists(sourceScenesPath, { mappings: [] });
|
|
501
713
|
const sddSkillBindings = await readJsonIfExists(sourceSkillBindingsPath, { version: '1.0', bindings: {} });
|
|
502
714
|
sddConfig.skillBindings = sddSkillBindings;
|
|
715
|
+
const workflowTransport = await discoverWorkflowTransport(options);
|
|
716
|
+
if (workflowTransport) {
|
|
717
|
+
sddConfig.mcpServers = {
|
|
718
|
+
...(sddConfig.mcpServers || {}),
|
|
719
|
+
'friday-auto-flow': workflowTransport
|
|
720
|
+
};
|
|
721
|
+
}
|
|
503
722
|
|
|
504
723
|
const targetResourceConfigPath = path.join(targetDir, 'resource-config.json');
|
|
505
724
|
const targetComponentConfigPath = path.join(targetDir, 'component-config.json');
|
|
506
725
|
const targetBindingsPath = path.join(targetLearningDir, 'resource-bindings.json');
|
|
507
726
|
const targetScenesPath = path.join(targetLearningDir, 'scenes.json');
|
|
727
|
+
const targetSourceDir = path.join(targetDir, 'source');
|
|
508
728
|
|
|
509
729
|
const existingResourceConfig = await readJsonIfExists(targetResourceConfigPath, {});
|
|
510
730
|
const existingComponentConfig = await readJsonIfExists(targetComponentConfigPath, { version: '1.0.0', components: {} });
|
|
@@ -512,8 +732,8 @@ async function syncSddAgentToFlowMind(options = {}) {
|
|
|
512
732
|
const existingScenes = await readJsonIfExists(targetScenesPath, { version: '1.0', mappings: [] });
|
|
513
733
|
|
|
514
734
|
const nextResourceConfig = deepMerge(existingResourceConfig, sanitizeResourceConfig(sddConfig, sddSkillBindings));
|
|
515
|
-
const nextComponentConfig = deepMerge(existingComponentConfig, buildComponentConfig(sddConfig, sddSkillBindings));
|
|
516
|
-
const importedBindings = convertResourceBindings(sddConfig, sddSkillBindings);
|
|
735
|
+
const nextComponentConfig = deepMerge(existingComponentConfig, buildComponentConfig(sddConfig, sddSkillBindings, workflowTransport));
|
|
736
|
+
const importedBindings = convertResourceBindings(sddConfig, sddSkillBindings, workflowTransport);
|
|
517
737
|
const nextBindings = {
|
|
518
738
|
version: existingBindings.version || '1.0',
|
|
519
739
|
lastUpdated: new Date().toISOString(),
|
|
@@ -531,6 +751,7 @@ async function syncSddAgentToFlowMind(options = {}) {
|
|
|
531
751
|
await fs.writeJson(targetComponentConfigPath, nextComponentConfig, { spaces: 2 });
|
|
532
752
|
await fs.writeJson(targetBindingsPath, nextBindings, { spaces: 2 });
|
|
533
753
|
await fs.writeJson(targetScenesPath, nextScenes, { spaces: 2 });
|
|
754
|
+
const copiedSourceFiles = await copySelectedSourceFiles(sourceDir, targetSourceDir);
|
|
534
755
|
|
|
535
756
|
return {
|
|
536
757
|
sourceDir,
|
|
@@ -539,14 +760,16 @@ async function syncSddAgentToFlowMind(options = {}) {
|
|
|
539
760
|
resourceConfig: targetResourceConfigPath,
|
|
540
761
|
componentConfig: targetComponentConfigPath,
|
|
541
762
|
resourceBindings: targetBindingsPath,
|
|
542
|
-
scenes: targetScenesPath
|
|
763
|
+
scenes: targetScenesPath,
|
|
764
|
+
sourceFiles: copiedSourceFiles.map((item) => item.targetPath)
|
|
543
765
|
},
|
|
544
766
|
counts: {
|
|
545
767
|
importedBindings: importedBindings.length,
|
|
546
768
|
totalBindings: nextBindings.bindings.length,
|
|
547
769
|
importedScenes: importedScenes.length,
|
|
548
770
|
totalScenes: nextScenes.mappings.length,
|
|
549
|
-
components: Object.keys(nextComponentConfig.components || {}).length
|
|
771
|
+
components: Object.keys(nextComponentConfig.components || {}).length,
|
|
772
|
+
copiedSourceFiles: copiedSourceFiles.length
|
|
550
773
|
}
|
|
551
774
|
};
|
|
552
775
|
}
|
|
@@ -563,5 +786,6 @@ module.exports = {
|
|
|
563
786
|
buildWorkflowResource,
|
|
564
787
|
normalizeMcpServer,
|
|
565
788
|
sanitizeResourceConfig,
|
|
566
|
-
syncSddAgentToFlowMind
|
|
789
|
+
syncSddAgentToFlowMind,
|
|
790
|
+
autoSyncSddAgentToFlowMind
|
|
567
791
|
};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const ENV_PATTERNS = Object.freeze([
|
|
6
|
+
{ env: 'prod', pattern: /\bprod\b|生产/iu },
|
|
7
|
+
{ env: 'gray', pattern: /\bgray\b|灰度/iu },
|
|
8
|
+
{ env: 'uat', pattern: /\buat\b|预发/iu },
|
|
9
|
+
{ env: 'test', pattern: /\btest\b|测试/iu },
|
|
10
|
+
{ env: 'dev', pattern: /\bdev\b|开发/iu }
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const DEFAULT_SCORE_LIMIT = 5;
|
|
14
|
+
|
|
15
|
+
let cachedSignature = null;
|
|
16
|
+
let cachedContext = null;
|
|
17
|
+
|
|
18
|
+
function getFlowMindHome() {
|
|
19
|
+
return process.env.FLOWMIND_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getSourceDir(options = {}) {
|
|
23
|
+
if (options.sourceDir) {
|
|
24
|
+
return options.sourceDir;
|
|
25
|
+
}
|
|
26
|
+
const flowmindHome = options.flowmindHome || getFlowMindHome();
|
|
27
|
+
return path.join(flowmindHome, '.flowmind', 'source');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readJsonIfExists(filePath, fallback) {
|
|
31
|
+
if (!(await fs.pathExists(filePath))) {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
return fs.readJson(filePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function buildSignature(sourceDir) {
|
|
38
|
+
const files = [
|
|
39
|
+
'RESOURCE_INDEX.md',
|
|
40
|
+
'project-db-configs.json',
|
|
41
|
+
'project-git-map.json'
|
|
42
|
+
];
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const fileName of files) {
|
|
45
|
+
const filePath = path.join(sourceDir, fileName);
|
|
46
|
+
try {
|
|
47
|
+
const stat = await fs.stat(filePath);
|
|
48
|
+
entries.push(`${fileName}:${stat.size}:${stat.mtimeMs}`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
entries.push(`${fileName}:missing`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return entries.join('|');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseList(value) {
|
|
57
|
+
if (!value) return [];
|
|
58
|
+
return String(value)
|
|
59
|
+
.split(',')
|
|
60
|
+
.map((item) => item.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseResourceIndex(markdown = '') {
|
|
65
|
+
const lines = String(markdown).split(/\r?\n/);
|
|
66
|
+
const entries = [];
|
|
67
|
+
let current = null;
|
|
68
|
+
|
|
69
|
+
for (const rawLine of lines) {
|
|
70
|
+
const line = rawLine.trim();
|
|
71
|
+
if (!line) continue;
|
|
72
|
+
|
|
73
|
+
if (line.startsWith('### ')) {
|
|
74
|
+
current = {
|
|
75
|
+
title: line.slice(4).trim(),
|
|
76
|
+
path: null,
|
|
77
|
+
aliases: [],
|
|
78
|
+
domains: [],
|
|
79
|
+
modules: [],
|
|
80
|
+
tags: [],
|
|
81
|
+
note: null
|
|
82
|
+
};
|
|
83
|
+
entries.push(current);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!current || !line.startsWith('- ')) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const separatorIndex = line.indexOf(':');
|
|
92
|
+
if (separatorIndex < 0) continue;
|
|
93
|
+
|
|
94
|
+
const key = line.slice(2, separatorIndex).trim();
|
|
95
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
96
|
+
|
|
97
|
+
if (key === 'path') current.path = value;
|
|
98
|
+
if (key === 'aliases') current.aliases = parseList(value);
|
|
99
|
+
if (key === 'domains') current.domains = parseList(value);
|
|
100
|
+
if (key === 'modules') current.modules = parseList(value);
|
|
101
|
+
if (key === 'tags') current.tags = parseList(value);
|
|
102
|
+
if (key === 'note') current.note = value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return entries;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractKeywords(input) {
|
|
109
|
+
return (String(input || '').match(/[\u4e00-\u9fa5A-Za-z0-9._-]+/g) || [])
|
|
110
|
+
.map((item) => item.toLowerCase());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function detectEnvironment(input) {
|
|
114
|
+
const normalized = String(input || '').toLowerCase();
|
|
115
|
+
const matched = ENV_PATTERNS.find(({ pattern }) => pattern.test(normalized));
|
|
116
|
+
return matched ? matched.env : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeProjectService(service = {}) {
|
|
120
|
+
return {
|
|
121
|
+
...service,
|
|
122
|
+
databaseId: service.databaseId || service.dmsDatabaseId || null,
|
|
123
|
+
sourceId: service.sourceId || null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildProjectCatalog(projectConfigs = {}, gitMap = {}, resourceIndex = []) {
|
|
128
|
+
const catalog = new Map();
|
|
129
|
+
const serverProjects = gitMap.serverProjects || {};
|
|
130
|
+
const mobileProjects = gitMap.mobileProjects || {};
|
|
131
|
+
|
|
132
|
+
const ensureProject = (projectName) => {
|
|
133
|
+
if (!projectName) return null;
|
|
134
|
+
if (!catalog.has(projectName)) {
|
|
135
|
+
catalog.set(projectName, {
|
|
136
|
+
name: projectName,
|
|
137
|
+
config: projectConfigs[projectName] || null,
|
|
138
|
+
git: serverProjects[projectName] || mobileProjects[projectName] || null,
|
|
139
|
+
resourceEntries: []
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return catalog.get(projectName);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
for (const projectName of Object.keys(projectConfigs)) {
|
|
146
|
+
ensureProject(projectName);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const projectName of Object.keys(serverProjects)) {
|
|
150
|
+
ensureProject(projectName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const projectName of Object.keys(mobileProjects)) {
|
|
154
|
+
ensureProject(projectName);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const entry of resourceIndex) {
|
|
158
|
+
for (const moduleName of entry.modules || []) {
|
|
159
|
+
const project = ensureProject(moduleName);
|
|
160
|
+
if (project) {
|
|
161
|
+
project.resourceEntries.push(entry);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Array.from(catalog.values());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function scoreMatch(normalizedInput, value, score, reason) {
|
|
170
|
+
if (!value) return null;
|
|
171
|
+
const normalizedValue = String(value).toLowerCase();
|
|
172
|
+
if (!normalizedValue) return null;
|
|
173
|
+
if (!normalizedInput.includes(normalizedValue)) return null;
|
|
174
|
+
return { score, reason, match: value };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scoreProjectCandidate(project, normalizedInput, keywords = []) {
|
|
178
|
+
let score = 0;
|
|
179
|
+
const reasons = [];
|
|
180
|
+
|
|
181
|
+
const directMatches = [
|
|
182
|
+
scoreMatch(normalizedInput, project.name, 18, 'project-name'),
|
|
183
|
+
scoreMatch(normalizedInput, stripEnvPrefix(project.name), 14, 'project-name-base'),
|
|
184
|
+
scoreMatch(normalizedInput, project.git?.description, 6, 'git-description')
|
|
185
|
+
].filter(Boolean);
|
|
186
|
+
|
|
187
|
+
for (const match of directMatches) {
|
|
188
|
+
score += match.score;
|
|
189
|
+
reasons.push(match);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const entry of project.resourceEntries || []) {
|
|
193
|
+
const entryMatches = [
|
|
194
|
+
scoreMatch(normalizedInput, entry.title, 8, 'resource-title'),
|
|
195
|
+
...entry.aliases.map((value) => scoreMatch(normalizedInput, value, 8, 'resource-alias')),
|
|
196
|
+
...entry.modules.map((value) => scoreMatch(normalizedInput, value, 10, 'resource-module')),
|
|
197
|
+
...entry.domains.map((value) => scoreMatch(normalizedInput, value, 3, 'resource-domain')),
|
|
198
|
+
...entry.tags.map((value) => scoreMatch(normalizedInput, value, 2, 'resource-tag'))
|
|
199
|
+
].filter(Boolean);
|
|
200
|
+
|
|
201
|
+
for (const match of entryMatches) {
|
|
202
|
+
score += match.score;
|
|
203
|
+
reasons.push({ ...match, entry: entry.title });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const description = String(project.git?.description || '').toLowerCase();
|
|
208
|
+
if (description) {
|
|
209
|
+
for (const keyword of keywords) {
|
|
210
|
+
if (keyword.length < 2) continue;
|
|
211
|
+
if (description.includes(keyword)) {
|
|
212
|
+
score += 1;
|
|
213
|
+
reasons.push({ score: 1, reason: 'git-description-keyword', match: keyword });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
name: project.name,
|
|
220
|
+
score,
|
|
221
|
+
reasons,
|
|
222
|
+
config: project.config,
|
|
223
|
+
git: project.git,
|
|
224
|
+
resourceEntries: project.resourceEntries || []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function stripEnvPrefix(projectName) {
|
|
229
|
+
return String(projectName || '').replace(/^(?:test|dev|uat|gray|prod)-/i, '');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function loadSourceContext(options = {}) {
|
|
233
|
+
const sourceDir = getSourceDir(options);
|
|
234
|
+
const signature = await buildSignature(sourceDir);
|
|
235
|
+
|
|
236
|
+
if (cachedContext && cachedSignature === `${sourceDir}:${signature}`) {
|
|
237
|
+
return cachedContext;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const [projectDbConfigs, projectGitMap, resourceIndexContent] = await Promise.all([
|
|
241
|
+
readJsonIfExists(path.join(sourceDir, 'project-db-configs.json'), { configs: {} }),
|
|
242
|
+
readJsonIfExists(path.join(sourceDir, 'project-git-map.json'), { serverProjects: {}, mobileProjects: {} }),
|
|
243
|
+
fs.pathExists(path.join(sourceDir, 'RESOURCE_INDEX.md'))
|
|
244
|
+
? fs.readFile(path.join(sourceDir, 'RESOURCE_INDEX.md'), 'utf8')
|
|
245
|
+
: Promise.resolve('')
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
const resourceIndex = parseResourceIndex(resourceIndexContent);
|
|
249
|
+
const projectConfigs = projectDbConfigs.configs || {};
|
|
250
|
+
const projectCatalog = buildProjectCatalog(projectConfigs, projectGitMap, resourceIndex);
|
|
251
|
+
|
|
252
|
+
cachedContext = {
|
|
253
|
+
sourceDir,
|
|
254
|
+
projectConfigs,
|
|
255
|
+
projectGitMap,
|
|
256
|
+
resourceIndex,
|
|
257
|
+
projectCatalog
|
|
258
|
+
};
|
|
259
|
+
cachedSignature = `${sourceDir}:${signature}`;
|
|
260
|
+
return cachedContext;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function inferSourceContext(input, options = {}) {
|
|
264
|
+
const sourceContext = await loadSourceContext(options);
|
|
265
|
+
const normalizedInput = String(input || '').toLowerCase();
|
|
266
|
+
const keywords = extractKeywords(input);
|
|
267
|
+
const explicitEnv = detectEnvironment(input);
|
|
268
|
+
const candidates = sourceContext.projectCatalog
|
|
269
|
+
.map((project) => scoreProjectCandidate(project, normalizedInput, keywords))
|
|
270
|
+
.filter((candidate) => candidate.score > 0)
|
|
271
|
+
.sort((left, right) => right.score - left.score);
|
|
272
|
+
|
|
273
|
+
const topCandidates = candidates.slice(0, options.limit || DEFAULT_SCORE_LIMIT);
|
|
274
|
+
const selected = topCandidates[0] || null;
|
|
275
|
+
|
|
276
|
+
if (!selected) {
|
|
277
|
+
return {
|
|
278
|
+
matched: false,
|
|
279
|
+
sourceDir: sourceContext.sourceDir,
|
|
280
|
+
environment: explicitEnv,
|
|
281
|
+
candidates: []
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const config = selected.config || {};
|
|
286
|
+
const mysql = config.services?.mysql ? normalizeProjectService(config.services.mysql) : null;
|
|
287
|
+
const redis = config.services?.redis ? normalizeProjectService(config.services.redis) : null;
|
|
288
|
+
const environment = explicitEnv
|
|
289
|
+
|| config.environment
|
|
290
|
+
|| mysql?.envType
|
|
291
|
+
|| redis?.envType
|
|
292
|
+
|| null;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
matched: true,
|
|
296
|
+
sourceDir: sourceContext.sourceDir,
|
|
297
|
+
project: selected.name,
|
|
298
|
+
environment,
|
|
299
|
+
gitProject: selected.git || null,
|
|
300
|
+
database: mysql,
|
|
301
|
+
redis,
|
|
302
|
+
cacheKeys: config.cacheKeys || {},
|
|
303
|
+
config,
|
|
304
|
+
matchedEntries: selected.resourceEntries.slice(0, 3).map((entry) => ({
|
|
305
|
+
title: entry.title,
|
|
306
|
+
aliases: entry.aliases,
|
|
307
|
+
modules: entry.modules,
|
|
308
|
+
tags: entry.tags
|
|
309
|
+
})),
|
|
310
|
+
candidates: topCandidates.map((candidate) => ({
|
|
311
|
+
project: candidate.name,
|
|
312
|
+
score: candidate.score,
|
|
313
|
+
reasons: candidate.reasons.slice(0, 8)
|
|
314
|
+
}))
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
detectEnvironment,
|
|
320
|
+
getSourceDir,
|
|
321
|
+
inferSourceContext,
|
|
322
|
+
loadSourceContext,
|
|
323
|
+
parseResourceIndex
|
|
324
|
+
};
|
package/package.json
CHANGED