flowmind 1.5.4 → 1.5.6

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 CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [1.5.6] - 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
+
8
+ ### Fixed
9
+ - TUI and dashboard now degrade gracefully on narrow or very small terminals instead of crashing on resize
10
+ - CLI TUI/dashboard startup now uses a shared Ink launcher with consistent stdin proxy cleanup
11
+ - 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
12
+ - 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
13
+ - Log audit now also tries to infer a keyword from natural-language log queries when the user forgets to add quotes
14
+ - Log audit and SLS log audit now share a natural-language parser for keyword, level, and time-window extraction
15
+ - Natural-language log queries now also infer service and project names when the user writes "项目" or "服务" in a loose sentence
16
+
17
+ ### Changed
18
+ - Sidebar, chat, status, and dashboard panels now switch to compact layouts when terminal width drops below the normal threshold
19
+ - Terminal resize handling now keeps the current session alive and shows a recovery hint instead of exiting on render errors
4
20
 
5
21
  ## [1.5.4] - 2026-07-01
6
22
 
package/README.md CHANGED
@@ -15,6 +15,8 @@
15
15
 
16
16
  </div>
17
17
 
18
+ Weekly downloads: 3,280
19
+
18
20
  ---
19
21
 
20
22
  ## What It Is
package/README_CN.md CHANGED
@@ -15,6 +15,8 @@
15
15
 
16
16
  </div>
17
17
 
18
+ 本周下载量:3,280 次
19
+
18
20
  ---
19
21
 
20
22
  ## 它是什么
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, shouldProxyInkStdin, getNestedTuiGuardMessage } = require('../core/utils');
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
- const realStdin = process.stdin;
1632
- let inkStdin = realStdin;
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
- const realStdin = process.stdin;
1719
- let inkStdin = realStdin;
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
 
@@ -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
+ };