flowmind 1.5.4 → 1.5.7
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 +18 -1
- package/README.md +2 -0
- package/README_CN.md +2 -0
- package/bin/flowmind.js +19 -110
- package/core/cli-ink.js +79 -0
- package/core/log-query-parser.js +324 -0
- package/core/update-notifier.js +127 -0
- package/dashboard/app.jsx +69 -10
- package/dashboard/components/ActivityFeed.jsx +5 -4
- package/dashboard/components/DragonPanel.jsx +17 -1
- package/dashboard/components/McpStatusBar.jsx +19 -1
- package/dashboard/components/StatsRow.jsx +27 -2
- package/package.json +2 -1
- package/scripts/check-update.js +52 -0
- package/skills/auto-flow/index.js +58 -82
- package/skills/log-audit/index.js +55 -27
- package/skills/sls-log-audit/index.js +7 -30
- package/skills/yuque-sync-design/index.js +2 -2
- package/tui/app.jsx +117 -5
- package/tui/components/ChatPanel.jsx +9 -6
- package/tui/components/DragonTotem.jsx +12 -1
- package/tui/components/Sidebar.jsx +19 -7
- package/tui/components/StatusBar.jsx +28 -1
- package/tui/format-result.js +60 -0
- package/tui/layout.js +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [1.5.7] - 2026-07-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added a `check-update` script and CLI command so already installed users can quickly see whether a newer npm release is available
|
|
7
|
+
- TUI now checks npm for a newer release after startup and shows a non-blocking update reminder banner when one is available
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- TUI and dashboard now degrade gracefully on narrow or very small terminals instead of crashing on resize
|
|
11
|
+
- CLI TUI/dashboard startup now uses a shared Ink launcher with consistent stdin proxy cleanup
|
|
12
|
+
- Auto-flow remote checklist resolution now requires an exact service/pipeline descriptor match, so a neighboring service can no longer be started by index position alone
|
|
13
|
+
- Log audit now extracts quoted Chinese exception text as a query keyword, shows explicit no-records guidance, and surfaces raw MCP response summaries for TUI debugging
|
|
14
|
+
- Log audit now also tries to infer a keyword from natural-language log queries when the user forgets to add quotes
|
|
15
|
+
- Log audit and SLS log audit now share a natural-language parser for keyword, level, and time-window extraction
|
|
16
|
+
- Natural-language log queries now also infer service and project names when the user writes "项目" or "服务" in a loose sentence
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Sidebar, chat, status, and dashboard panels now switch to compact layouts when terminal width drops below the normal threshold
|
|
20
|
+
- Terminal resize handling now keeps the current session alive and shows a recovery hint instead of exiting on render errors
|
|
4
21
|
|
|
5
22
|
## [1.5.4] - 2026-07-01
|
|
6
23
|
|
package/README.md
CHANGED
package/README_CN.md
CHANGED
package/bin/flowmind.js
CHANGED
|
@@ -14,8 +14,10 @@ const path = require('path');
|
|
|
14
14
|
const { execSync } = require('child_process');
|
|
15
15
|
const FlowMind = require('../core');
|
|
16
16
|
const HonorEngine = require('../core/honor-engine');
|
|
17
|
+
const { main: runUpdateCheck } = require('../scripts/check-update');
|
|
18
|
+
const { launchInkApp } = require('../core/cli-ink');
|
|
17
19
|
const { syncSddAgentToFlowMind } = require('../core/sdd-agent-sync');
|
|
18
|
-
const { detectManagedCliHost, shouldUseAsciiUi,
|
|
20
|
+
const { detectManagedCliHost, shouldUseAsciiUi, getNestedTuiGuardMessage } = require('../core/utils');
|
|
19
21
|
const { isExitCommand } = require('../tui/ui');
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -1606,8 +1608,6 @@ program
|
|
|
1606
1608
|
.description('Launch enhanced TUI with split panels, skill browser, and dragon display')
|
|
1607
1609
|
.option('--ascii', 'Force ASCII-safe terminal rendering')
|
|
1608
1610
|
.action(async (options) => {
|
|
1609
|
-
let stdinWrapper = null;
|
|
1610
|
-
let stdinForwarder = null;
|
|
1611
1611
|
try {
|
|
1612
1612
|
const managedHost = detectManagedCliHost();
|
|
1613
1613
|
if (managedHost) {
|
|
@@ -1617,71 +1617,21 @@ program
|
|
|
1617
1617
|
return;
|
|
1618
1618
|
}
|
|
1619
1619
|
|
|
1620
|
-
// Register .jsx extension for CJS
|
|
1621
|
-
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
1622
|
-
|
|
1623
1620
|
const React = require('react');
|
|
1624
|
-
const { render } = require('ink');
|
|
1625
|
-
const { PassThrough } = require('stream');
|
|
1626
1621
|
const App = require('../tui/app.jsx');
|
|
1627
1622
|
const asciiMode = options.ascii || shouldUseAsciiUi();
|
|
1628
1623
|
|
|
1629
1624
|
const fm = await initFlowMind();
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
// Only proxy stdin when the host stream cannot satisfy Ink's raw-mode requirements.
|
|
1635
|
-
if (shouldProxyInkStdin(realStdin)) {
|
|
1636
|
-
stdinWrapper = new PassThrough();
|
|
1637
|
-
stdinWrapper.isTTY = true;
|
|
1638
|
-
stdinWrapper.isRaw = false;
|
|
1639
|
-
stdinWrapper.setRawMode = (mode) => {
|
|
1640
|
-
try {
|
|
1641
|
-
if (realStdin.setRawMode) {
|
|
1642
|
-
realStdin.setRawMode(mode);
|
|
1643
|
-
}
|
|
1644
|
-
} catch (e) {
|
|
1645
|
-
// Suppress raw mode errors in non-TTY environments
|
|
1646
|
-
}
|
|
1647
|
-
return stdinWrapper;
|
|
1648
|
-
};
|
|
1649
|
-
// Forward real stdin data to the wrapper (store reference for cleanup)
|
|
1650
|
-
if (realStdin.readable) {
|
|
1651
|
-
stdinForwarder = (chunk) => {
|
|
1652
|
-
if (!stdinWrapper.destroyed) {
|
|
1653
|
-
try {
|
|
1654
|
-
stdinWrapper.write(chunk);
|
|
1655
|
-
} catch (e) {
|
|
1656
|
-
// Ignore write-after-destroy errors
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
realStdin.on('data', stdinForwarder);
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
inkStdin = stdinWrapper;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
const { unmount, waitUntilExit } = render(
|
|
1667
|
-
React.createElement(App, { flowmind: fm, asciiMode: asciiMode }),
|
|
1668
|
-
{ stdin: inkStdin }
|
|
1669
|
-
);
|
|
1670
|
-
await waitUntilExit();
|
|
1671
|
-
unmount();
|
|
1625
|
+
await launchInkApp({
|
|
1626
|
+
element: React.createElement(App, { flowmind: fm, asciiMode: asciiMode }),
|
|
1627
|
+
stdin: process.stdin
|
|
1628
|
+
});
|
|
1672
1629
|
} catch (error) {
|
|
1673
1630
|
console.error(chalk.red('TUI Error:'), error.message);
|
|
1674
1631
|
if (error.message.includes('Cannot find module')) {
|
|
1675
1632
|
console.log(chalk.yellow('Try running: npm install ink@3 react ink-text-input ink-spinner'));
|
|
1676
1633
|
}
|
|
1677
1634
|
} finally {
|
|
1678
|
-
// Clean up stdin listener to prevent leak
|
|
1679
|
-
if (stdinForwarder) {
|
|
1680
|
-
process.stdin.removeListener('data', stdinForwarder);
|
|
1681
|
-
}
|
|
1682
|
-
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1683
|
-
stdinWrapper.destroy();
|
|
1684
|
-
}
|
|
1685
1635
|
restoreTerminal();
|
|
1686
1636
|
}
|
|
1687
1637
|
});
|
|
@@ -1692,8 +1642,6 @@ program
|
|
|
1692
1642
|
.description('Launch real-time monitoring dashboard for MCP activity and events')
|
|
1693
1643
|
.option('--ascii', 'Force ASCII-safe terminal rendering')
|
|
1694
1644
|
.action(async (options) => {
|
|
1695
|
-
let stdinWrapper = null;
|
|
1696
|
-
let stdinForwarder = null;
|
|
1697
1645
|
try {
|
|
1698
1646
|
const managedHost = detectManagedCliHost();
|
|
1699
1647
|
if (managedHost) {
|
|
@@ -1703,68 +1651,22 @@ program
|
|
|
1703
1651
|
return;
|
|
1704
1652
|
}
|
|
1705
1653
|
|
|
1706
|
-
// Register .jsx extension for CJS
|
|
1707
|
-
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
1708
|
-
|
|
1709
1654
|
const React = require('react');
|
|
1710
|
-
const { render } = require('ink');
|
|
1711
|
-
const { PassThrough } = require('stream');
|
|
1712
1655
|
const DashboardApp = require('../dashboard/app.jsx');
|
|
1713
1656
|
const eventBus = require('../core/event-bus');
|
|
1714
1657
|
const asciiMode = options.ascii || shouldUseAsciiUi();
|
|
1715
1658
|
|
|
1716
1659
|
const fm = await initFlowMind();
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
// Only proxy stdin when the host stream cannot satisfy Ink's raw-mode requirements.
|
|
1722
|
-
if (shouldProxyInkStdin(realStdin)) {
|
|
1723
|
-
stdinWrapper = new PassThrough();
|
|
1724
|
-
stdinWrapper.isTTY = true;
|
|
1725
|
-
stdinWrapper.isRaw = false;
|
|
1726
|
-
stdinWrapper.setRawMode = (mode) => {
|
|
1727
|
-
try {
|
|
1728
|
-
if (realStdin.setRawMode) {
|
|
1729
|
-
realStdin.setRawMode(mode);
|
|
1730
|
-
}
|
|
1731
|
-
} catch (e) {
|
|
1732
|
-
// Suppress raw mode errors in non-TTY environments
|
|
1733
|
-
}
|
|
1734
|
-
return stdinWrapper;
|
|
1735
|
-
};
|
|
1736
|
-
if (realStdin.readable) {
|
|
1737
|
-
stdinForwarder = (chunk) => {
|
|
1738
|
-
if (!stdinWrapper.destroyed) {
|
|
1739
|
-
try {
|
|
1740
|
-
stdinWrapper.write(chunk);
|
|
1741
|
-
} catch (e) { /* ignore write-after-destroy */ }
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
realStdin.on('data', stdinForwarder);
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
inkStdin = stdinWrapper;
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
const { unmount, waitUntilExit } = render(
|
|
1751
|
-
React.createElement(DashboardApp, { flowmind: fm, eventBus, asciiMode: asciiMode }),
|
|
1752
|
-
{ stdin: inkStdin }
|
|
1753
|
-
);
|
|
1754
|
-
await waitUntilExit();
|
|
1755
|
-
unmount();
|
|
1660
|
+
await launchInkApp({
|
|
1661
|
+
element: React.createElement(DashboardApp, { flowmind: fm, eventBus, asciiMode: asciiMode }),
|
|
1662
|
+
stdin: process.stdin
|
|
1663
|
+
});
|
|
1756
1664
|
} catch (error) {
|
|
1757
1665
|
console.error(chalk.red('Dashboard Error:'), error.message);
|
|
1758
1666
|
if (error.message.includes('Cannot find module')) {
|
|
1759
1667
|
console.log(chalk.yellow('Try running: npm install ink@3 react'));
|
|
1760
1668
|
}
|
|
1761
1669
|
} finally {
|
|
1762
|
-
if (stdinForwarder) {
|
|
1763
|
-
process.stdin.removeListener('data', stdinForwarder);
|
|
1764
|
-
}
|
|
1765
|
-
if (stdinWrapper && !stdinWrapper.destroyed) {
|
|
1766
|
-
stdinWrapper.destroy();
|
|
1767
|
-
}
|
|
1768
1670
|
restoreTerminal();
|
|
1769
1671
|
}
|
|
1770
1672
|
});
|
|
@@ -1890,10 +1792,17 @@ program
|
|
|
1890
1792
|
}
|
|
1891
1793
|
});
|
|
1892
1794
|
|
|
1795
|
+
program
|
|
1796
|
+
.command('check-update')
|
|
1797
|
+
.description('Check whether a newer FlowMind version is available')
|
|
1798
|
+
.action(async () => {
|
|
1799
|
+
await runUpdateCheck();
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1893
1802
|
// Parse arguments
|
|
1894
1803
|
const knownCommands = new Set([
|
|
1895
1804
|
'init', 'process', 'p', 'learn', 'scenes', 'skills', 'skill', 'resource', 'res',
|
|
1896
|
-
'stats', 'honor', 'config', 'ai', 'tui', 'dashboard', 'doctor', 'update', 'help'
|
|
1805
|
+
'stats', 'honor', 'config', 'ai', 'tui', 'dashboard', 'doctor', 'update', 'check-update', 'help'
|
|
1897
1806
|
]);
|
|
1898
1807
|
const cliArgs = process.argv.slice(2);
|
|
1899
1808
|
|
package/core/cli-ink.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { PassThrough } = require('stream');
|
|
2
|
+
const { shouldProxyInkStdin } = require('./utils');
|
|
3
|
+
|
|
4
|
+
function registerJsxExtension() {
|
|
5
|
+
require('module')._extensions['.jsx'] = require('module')._extensions['.js'];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createInkStdinProxy(realStdin) {
|
|
9
|
+
if (!realStdin) {
|
|
10
|
+
return { stdin: null, wrapper: null, forwarder: null };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const wrapper = new PassThrough();
|
|
14
|
+
wrapper.isTTY = true;
|
|
15
|
+
wrapper.isRaw = false;
|
|
16
|
+
wrapper.setRawMode = (mode) => {
|
|
17
|
+
try {
|
|
18
|
+
if (realStdin.setRawMode) {
|
|
19
|
+
realStdin.setRawMode(mode);
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Ignore raw mode failures in wrapped environments.
|
|
23
|
+
}
|
|
24
|
+
return wrapper;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let forwarder = null;
|
|
28
|
+
if (realStdin.readable) {
|
|
29
|
+
forwarder = (chunk) => {
|
|
30
|
+
if (!wrapper.destroyed) {
|
|
31
|
+
try {
|
|
32
|
+
wrapper.write(chunk);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Ignore write-after-destroy errors while shutting down.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
realStdin.on('data', forwarder);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { stdin: wrapper, wrapper, forwarder };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cleanupInkStdinProxy(realStdin, proxy) {
|
|
45
|
+
if (!proxy) return;
|
|
46
|
+
|
|
47
|
+
if (proxy.forwarder && realStdin?.removeListener) {
|
|
48
|
+
realStdin.removeListener('data', proxy.forwarder);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (proxy.wrapper && !proxy.wrapper.destroyed) {
|
|
52
|
+
proxy.wrapper.destroy();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function launchInkApp({ element, stdin = process.stdin }) {
|
|
57
|
+
registerJsxExtension();
|
|
58
|
+
const { render } = require('ink');
|
|
59
|
+
const proxy = shouldProxyInkStdin(stdin) ? createInkStdinProxy(stdin) : { stdin: null, wrapper: null, forwarder: null };
|
|
60
|
+
let unmount = null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const rendered = render(element, { stdin: proxy.stdin || stdin });
|
|
64
|
+
unmount = rendered.unmount;
|
|
65
|
+
await rendered.waitUntilExit();
|
|
66
|
+
} finally {
|
|
67
|
+
if (typeof unmount === 'function') {
|
|
68
|
+
unmount();
|
|
69
|
+
}
|
|
70
|
+
cleanupInkStdinProxy(stdin, proxy);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
cleanupInkStdinProxy,
|
|
76
|
+
createInkStdinProxy,
|
|
77
|
+
launchInkApp,
|
|
78
|
+
registerJsxExtension
|
|
79
|
+
};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
function parseLogQueryParams(input, options = {}) {
|
|
2
|
+
const params = {};
|
|
3
|
+
const text = String(input || '');
|
|
4
|
+
|
|
5
|
+
const traceMatch = text.match(/(?:trace[_-]?id|调用链)\s*[:=]?\s*(\S+)/i);
|
|
6
|
+
if (traceMatch) params.traceId = traceMatch[1];
|
|
7
|
+
|
|
8
|
+
const naturalProjectService = options.allowNaturalService !== false || options.allowNaturalProject !== false
|
|
9
|
+
? extractNaturalProjectServicePair(text)
|
|
10
|
+
: null;
|
|
11
|
+
if (naturalProjectService) {
|
|
12
|
+
if (naturalProjectService.project && !params.project) {
|
|
13
|
+
params.project = naturalProjectService.project;
|
|
14
|
+
}
|
|
15
|
+
if (naturalProjectService.service && !params.service) {
|
|
16
|
+
params.service = naturalProjectService.service;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const serviceMatch = text.match(/(?:服务|service)\s*[:=]?\s*(\S+)/i);
|
|
21
|
+
if (serviceMatch) {
|
|
22
|
+
params.service = serviceMatch[1];
|
|
23
|
+
} else if (!params.service && options.allowNaturalService !== false) {
|
|
24
|
+
const naturalService = extractNaturalService(text);
|
|
25
|
+
if (naturalService) {
|
|
26
|
+
params.service = naturalService;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const levelMatch = text.match(/(?:级别|level)\s*[:=]?\s*(ERROR|WARN|INFO|DEBUG)/i);
|
|
31
|
+
if (levelMatch) {
|
|
32
|
+
params.level = levelMatch[1].toUpperCase();
|
|
33
|
+
} else if (options.allowNaturalLevel !== false) {
|
|
34
|
+
const naturalLevel = extractNaturalLevel(text);
|
|
35
|
+
if (naturalLevel) {
|
|
36
|
+
params.level = naturalLevel;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const projectMatch = text.match(/(?:项目|project)\s*[:=]?\s*([A-Za-z][A-Za-z0-9._-]{2,})\b/i);
|
|
41
|
+
if (projectMatch) {
|
|
42
|
+
params.project = projectMatch[1];
|
|
43
|
+
} else if (!params.project && options.allowNaturalProject !== false) {
|
|
44
|
+
const naturalProject = extractNaturalProject(text);
|
|
45
|
+
if (naturalProject) {
|
|
46
|
+
params.project = naturalProject;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const logstoreMatch = text.match(/(?:logstore|日志库)\s*[:=]?\s*(\S+)/i);
|
|
51
|
+
if (logstoreMatch) params.logstore = logstoreMatch[1];
|
|
52
|
+
|
|
53
|
+
const envMatch = text.match(/(?:环境|env)\s*[:=]?\s*(test|uat|gray|prod)/i);
|
|
54
|
+
if (envMatch) params.env = envMatch[1].toLowerCase();
|
|
55
|
+
|
|
56
|
+
const keywordMatch = text.match(/(?:^|[^A-Za-z0-9._-])(?:关键词|keyword|搜索|search)(?![A-Za-z0-9._-])\s*[:=]?\s*(.+?)(?:\s*$)/i);
|
|
57
|
+
if (keywordMatch) {
|
|
58
|
+
params.keyword = keywordMatch[1].trim();
|
|
59
|
+
} else if (options.allowQuotedKeyword !== false) {
|
|
60
|
+
const quotedKeyword = extractQuotedText(text);
|
|
61
|
+
if (quotedKeyword) {
|
|
62
|
+
params.keyword = quotedKeyword;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!params.keyword && options.allowNaturalKeyword !== false) {
|
|
67
|
+
const naturalKeyword = extractNaturalKeyword(text);
|
|
68
|
+
if (naturalKeyword) {
|
|
69
|
+
params.keyword = naturalKeyword;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const timeWindow = extractTimeWindow(text);
|
|
74
|
+
if (timeWindow) {
|
|
75
|
+
params.timeRange = timeWindow.label;
|
|
76
|
+
params.from = timeWindow.from;
|
|
77
|
+
params.to = timeWindow.to;
|
|
78
|
+
} else {
|
|
79
|
+
const timeMatch = text.match(/(?:最近|last)\s*(\d+)\s*(分钟|小时|天|min|hour|day)/i);
|
|
80
|
+
if (timeMatch) {
|
|
81
|
+
params.timeRange = `last ${timeMatch[1]} ${timeMatch[2]}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return params;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractNaturalLevel(text) {
|
|
89
|
+
const normalized = String(text || '').toLowerCase();
|
|
90
|
+
if (/(错误|异常|报错|失败|宕机|超时|timeout|error|fail)/i.test(normalized)) {
|
|
91
|
+
return 'ERROR';
|
|
92
|
+
}
|
|
93
|
+
if (/(告警|警告|warning|warn)/i.test(normalized)) {
|
|
94
|
+
return 'WARN';
|
|
95
|
+
}
|
|
96
|
+
if (/(信息|info)/i.test(normalized)) {
|
|
97
|
+
return 'INFO';
|
|
98
|
+
}
|
|
99
|
+
if (/(调试|debug)/i.test(normalized)) {
|
|
100
|
+
return 'DEBUG';
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractQuotedText(input) {
|
|
106
|
+
const quotedMatches = [...String(input || '').matchAll(/["'“”‘’]([^"'“”‘’]{2,})["'“”‘’]/g)];
|
|
107
|
+
if (quotedMatches.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let best = '';
|
|
112
|
+
for (const match of quotedMatches) {
|
|
113
|
+
const candidate = (match[1] || '').trim();
|
|
114
|
+
if (candidate.length > best.length) {
|
|
115
|
+
best = candidate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return normalizeNaturalKeyword(best);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractNaturalKeyword(input) {
|
|
123
|
+
const text = String(input || '').trim();
|
|
124
|
+
if (!text) return null;
|
|
125
|
+
|
|
126
|
+
const patterns = [
|
|
127
|
+
/(?:日志|log(?:s| audit| analysis)?)[,,、::\s]+([^。!?\n]{2,120}?)(?=(?:是\s*什么异常|是什么异常|怎么回事|为什么|什么原因|原因|报错|错误|异常|问题|$))/i,
|
|
128
|
+
/(?:日志|log(?:s| audit| analysis)?)[,,、::\s]+([^。!?\n]{2,120}?)(?=$|\n|[。!?])/i
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const pattern of patterns) {
|
|
132
|
+
const match = text.match(pattern);
|
|
133
|
+
if (!match || !match[1]) continue;
|
|
134
|
+
const candidate = normalizeNaturalKeyword(match[1]);
|
|
135
|
+
if (candidate) {
|
|
136
|
+
return candidate;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractNaturalService(input) {
|
|
144
|
+
const text = String(input || '').trim();
|
|
145
|
+
if (!text) return null;
|
|
146
|
+
|
|
147
|
+
const patterns = [
|
|
148
|
+
/([A-Za-z][A-Za-z0-9._-]{2,})\s*项目(?:的)?\s*([A-Za-z][A-Za-z0-9._-]{2,})\s*(?:错误|异常|告警|故障|问题|调试|排查)?(?:日志|log|链路|trace|调用链)(?![A-Za-z0-9._-])/i,
|
|
149
|
+
/([A-Za-z][A-Za-z0-9._-]{2,})\s*(?:的)?\s*(?:错误|异常|告警|故障|问题|调试|排查)?(?:日志|log|链路|trace|调用链)(?![A-Za-z0-9._-])/i,
|
|
150
|
+
/(?:查|查询|查看|排查|分析|定位)?(?:下)?(?:服务|service|应用|app|系统|模块)?\s*([A-Za-z][A-Za-z0-9._-]{2,})\s*(?:的)?(?:错误|异常|告警|故障|问题|调试|排查)?(?:日志|log|链路|trace|调用链)(?![A-Za-z0-9._-])/i,
|
|
151
|
+
/(?:项目|project)\s*(?:的)?\s*([A-Za-z][A-Za-z0-9._-]{2,})\s*(?:错误|异常|告警|故障|问题|调试|排查)?(?:日志|log|链路|trace|调用链)(?![A-Za-z0-9._-])/i,
|
|
152
|
+
/(?:错误|异常|告警|故障|问题|调试|排查)?(?:日志|log|链路|trace)\s*[::\s]*([A-Za-z][A-Za-z0-9._-]{2,})/i
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
for (const pattern of patterns) {
|
|
156
|
+
const match = text.match(pattern);
|
|
157
|
+
if (!match || !match[1]) continue;
|
|
158
|
+
const candidate = normalizeEntityCandidate(match[1]);
|
|
159
|
+
if (candidate) {
|
|
160
|
+
return candidate;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function extractNaturalProject(input) {
|
|
168
|
+
const text = String(input || '').trim();
|
|
169
|
+
if (!text) return null;
|
|
170
|
+
|
|
171
|
+
const patterns = [
|
|
172
|
+
/([A-Za-z][A-Za-z0-9._-]{2,})\s*项目(?:的)?\s*[A-Za-z][A-Za-z0-9._-]{2,}\s*(?:错误|异常|告警|故障|问题|调试|排查)?(?:日志|log|链路|trace|调用链)(?![A-Za-z0-9._-])/i,
|
|
173
|
+
/(?:项目|project)\s*[:=]?\s*([A-Za-z][A-Za-z0-9._-]{2,})/i,
|
|
174
|
+
/([A-Za-z][A-Za-z0-9._-]{2,})\s*(?:项目|project)(?:的)?/i
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const pattern of patterns) {
|
|
178
|
+
const match = text.match(pattern);
|
|
179
|
+
if (!match || !match[1]) continue;
|
|
180
|
+
const candidate = normalizeEntityCandidate(match[1]);
|
|
181
|
+
if (candidate) {
|
|
182
|
+
return candidate;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeNaturalKeyword(value) {
|
|
190
|
+
const candidate = String(value || '')
|
|
191
|
+
.replace(/^[,,、::\s]+/, '')
|
|
192
|
+
.replace(/[,,、::\s]+$/, '')
|
|
193
|
+
.replace(/^(查询|查|查看|分析|定位|排查)\s*(?:下)?\s*/i, '')
|
|
194
|
+
.replace(/^(最近|近|过去|前)\s*\d+\s*(分钟|小时|天|min|hour|day)\s*的?/i, '')
|
|
195
|
+
.replace(/^(今天|今日|昨天|昨日|本周|这周|近7天|近24小时|近1小时|最近1小时|最近1天)\s*的?/i, '')
|
|
196
|
+
.replace(/^(下|下个|一下|一下子)/, '')
|
|
197
|
+
.trim();
|
|
198
|
+
|
|
199
|
+
if (!candidate) return null;
|
|
200
|
+
if (/^(日志|log|logs|错误|异常|报错|失败|问题|警告|告警|error|warn|info|debug)$/i.test(candidate)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (/(?:项目|服务|日志|log|trace|调用链)/i.test(candidate) && /[A-Za-z][A-Za-z0-9._-]{2,}/.test(candidate)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return candidate;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeEntityCandidate(value) {
|
|
211
|
+
const candidate = String(value || '').trim();
|
|
212
|
+
if (!candidate) return null;
|
|
213
|
+
if (/^(日志|log|logs|错误|异常|报错|失败|问题|警告|告警|error|warn|info|debug)$/i.test(candidate)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return candidate;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function extractNaturalProjectServicePair(input) {
|
|
220
|
+
const text = String(input || '').trim();
|
|
221
|
+
if (!text) return null;
|
|
222
|
+
|
|
223
|
+
const patterns = [
|
|
224
|
+
/([A-Za-z][A-Za-z0-9._-]{2,})\s*项目(?:的)?\s*([A-Za-z][A-Za-z0-9._-]{2,})\b/i,
|
|
225
|
+
/(?:项目|project)\s*(?:的)?\s*([A-Za-z][A-Za-z0-9._-]{2,})\b/i
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
for (const pattern of patterns) {
|
|
229
|
+
const match = text.match(pattern);
|
|
230
|
+
if (!match) continue;
|
|
231
|
+
|
|
232
|
+
if (match.length >= 3) {
|
|
233
|
+
const project = normalizeEntityCandidate(match[1]);
|
|
234
|
+
const service = normalizeEntityCandidate(match[2]);
|
|
235
|
+
if (project || service) {
|
|
236
|
+
return { project, service };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const service = normalizeEntityCandidate(match[1]);
|
|
241
|
+
if (service) {
|
|
242
|
+
return { service };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function extractTimeWindow(input) {
|
|
250
|
+
const text = String(input || '');
|
|
251
|
+
const now = new Date();
|
|
252
|
+
|
|
253
|
+
const recentMatch = text.match(/(?:最近|近|过去|前)\s*(\d+)\s*(分钟|小时|天|min|hour|day)/i);
|
|
254
|
+
if (recentMatch) {
|
|
255
|
+
const amount = Number(recentMatch[1]);
|
|
256
|
+
const unit = normalizeTimeUnit(recentMatch[2]);
|
|
257
|
+
if (Number.isFinite(amount) && amount > 0 && unit) {
|
|
258
|
+
const from = new Date(now.getTime() - amount * unitToMs(unit));
|
|
259
|
+
return {
|
|
260
|
+
label: `last ${amount} ${recentMatch[2]}`,
|
|
261
|
+
from: from.toISOString(),
|
|
262
|
+
to: now.toISOString()
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (/今天|今日/i.test(text)) {
|
|
268
|
+
const from = new Date(now);
|
|
269
|
+
from.setHours(0, 0, 0, 0);
|
|
270
|
+
return {
|
|
271
|
+
label: 'today',
|
|
272
|
+
from: from.toISOString(),
|
|
273
|
+
to: now.toISOString()
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (/昨天|昨日/i.test(text)) {
|
|
278
|
+
const to = new Date(now);
|
|
279
|
+
to.setHours(0, 0, 0, 0);
|
|
280
|
+
const from = new Date(to.getTime() - 24 * 60 * 60 * 1000);
|
|
281
|
+
return {
|
|
282
|
+
label: 'yesterday',
|
|
283
|
+
from: from.toISOString(),
|
|
284
|
+
to: to.toISOString()
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (/本周|这周|近7天/i.test(text)) {
|
|
289
|
+
const to = new Date(now);
|
|
290
|
+
const from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
291
|
+
return {
|
|
292
|
+
label: 'last 7 days',
|
|
293
|
+
from: from.toISOString(),
|
|
294
|
+
to: to.toISOString()
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeTimeUnit(unit) {
|
|
302
|
+
const text = String(unit || '').toLowerCase();
|
|
303
|
+
if (text === 'min' || text === 'minute' || text === 'minutes' || text === '分钟') return 'minute';
|
|
304
|
+
if (text === 'hour' || text === 'hours' || text === '小时') return 'hour';
|
|
305
|
+
if (text === 'day' || text === 'days' || text === '天') return 'day';
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function unitToMs(unit) {
|
|
310
|
+
if (unit === 'minute') return 60 * 1000;
|
|
311
|
+
if (unit === 'hour') return 60 * 60 * 1000;
|
|
312
|
+
if (unit === 'day') return 24 * 60 * 60 * 1000;
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
detectNaturalLevel: extractNaturalLevel,
|
|
318
|
+
extractNaturalProject,
|
|
319
|
+
extractNaturalService,
|
|
320
|
+
extractNaturalKeyword,
|
|
321
|
+
extractQuotedText,
|
|
322
|
+
extractTimeWindow,
|
|
323
|
+
parseLogQueryParams
|
|
324
|
+
};
|