@cvtoolman/huawei-lts-cli 1.0.3 → 2.0.0

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createQueryCommand = createQueryCommand;
4
4
  const commander_1 = require("commander");
5
5
  const client_1 = require("../../lts/client");
6
+ const pagination_1 = require("../../lts/pagination");
6
7
  const output_1 = require("../../output");
7
8
  const common_1 = require("./common");
8
9
  // commander 的处理函数会收到 (value, previous),直接传 parseInt 会把 previous 当作进制
@@ -19,21 +20,18 @@ function createQueryCommand() {
19
20
  .option('--st <startTime>', 'Alias for --start-time')
20
21
  .option('--et <endTime>', 'Alias for --end-time')
21
22
  .option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (mutually exclusive with --st/--et)')
22
- .option('-k, --keyword <keyword>', 'Simple keyword search (legacy, use --query for advanced queries)')
23
- .option('-q, --query <query>', 'Advanced query expression. Supports:\n' +
23
+ .option('-q, --query <query>', 'Query expression. Supports:\n' +
24
24
  ' - Field queries: content:error AND appName:myapp\n' +
25
25
  ' - Comparison: time>60 AND region:r\n' +
26
- ' - SQL syntax: SELECT * FROM log WHERE content LIKE \'%error%\'\n' +
26
+ ' - Pipeline analysis: error | SELECT count(*) AS c\n' +
27
27
  ' - Logical operators: AND, OR, NOT (must be surrounded by spaces)')
28
- .option('--app <appName>', 'Filter by appName field (e.g. --app acerp-gateway), merged with -q via AND')
28
+ .option('--app <appName>', 'Filter by appName field, merged with -q via AND')
29
29
  .option('--content <keyword>', 'Filter by log content (content:<keyword>), merged with -q via AND')
30
30
  .option('--request-id <id>', 'Filter by REQUEST_ID field; output in chronological order')
31
- .option('-l, --limit <limit>', 'Limit number of results', parseIntOption, 100)
32
- .option('-o, --offset <offset>', 'Offset for pagination', parseIntOption, 0)
31
+ .option('-l, --limit <limit>', 'Page size (also the cap for --all)', parseIntOption, 100)
33
32
  .option('-r, --reverse', 'Reverse order (newest first)')
34
33
  .option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
35
- .option('--all', 'Fetch all pages (up to 10000 logs; default is single page)')
36
- .option('--no-paginate', '(deprecated) Single page is the default; kept for compatibility')
34
+ .option('--all', 'Follow the cursor to the end (up to 10000 logs; default is a single page)')
37
35
  .action(async (options) => {
38
36
  const config = (0, common_1.loadCheckedConfig)();
39
37
  const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
@@ -47,45 +45,28 @@ function createQueryCommand() {
47
45
  groupId,
48
46
  streamId,
49
47
  ...timeParams,
50
- keyword: options.keyword,
51
48
  query,
52
49
  limit: options.limit,
53
- offset: options.offset,
54
50
  reverse,
55
51
  };
56
52
  try {
57
53
  if (!options.all) {
58
54
  // 默认单页查询,结果量由 --limit 控制
59
55
  const result = await client.queryLogs(params);
56
+ (0, common_1.warnIfIncomplete)(result);
60
57
  console.log((0, output_1.formatOutput)(result, format));
61
58
  }
62
59
  else {
63
- // --all:自动翻页拉取全部日志,上限 10000 条防止失控
64
- const MAX_LOGS = 10000;
65
- const allLogs = [];
66
- let offset = options.offset || 0;
67
- const limit = options.limit || 100;
68
- let hasMore = true;
69
- while (hasMore && allLogs.length < MAX_LOGS) {
70
- const pageParams = {
71
- ...params,
72
- offset,
73
- limit,
74
- };
75
- const result = await client.queryLogs(pageParams);
76
- allLogs.push(...result.logs);
77
- if (result.logs.length < limit) {
78
- hasMore = false;
79
- }
80
- else {
81
- offset += limit;
82
- }
60
+ // --all:沿游标读到末尾,上限 10000 条防止失控
61
+ const result = await (0, pagination_1.fetchAllPages)((p) => client.queryLogs(p), params, {
62
+ max: 10000,
63
+ pageSize: options.limit,
64
+ });
65
+ (0, common_1.warnIfIncomplete)(result);
66
+ if (result.truncated) {
67
+ console.error(`Warning: stopped at the ${result.logs.length}-log cap; more logs matched. Narrow the time range or add a filter.`);
83
68
  }
84
- const result = {
85
- logs: allLogs,
86
- total: allLogs.length,
87
- };
88
- console.log((0, output_1.formatOutput)(result, format));
69
+ console.log((0, output_1.formatOutput)({ logs: result.logs, total: result.total }, format));
89
70
  }
90
71
  }
91
72
  catch (error) {
@@ -64,7 +64,7 @@ function createGroupsCommand() {
64
64
  function createStreamsCommand() {
65
65
  const cmd = new commander_1.Command('streams');
66
66
  cmd.description('List LTS log streams, optionally filtered by log group name')
67
- .option('-g, --group <name>', 'Filter by log group NAME (e.g. acerp)')
67
+ .option('--group <name>', 'Filter by log group NAME (the API filters by name, not ID)')
68
68
  .option('-f, --format <format>', 'Output format: json, pretty', 'pretty')
69
69
  .action(async (options) => {
70
70
  const config = (0, common_1.loadCheckedConfig)();
@@ -7,15 +7,25 @@ exports.createTraceCommand = createTraceCommand;
7
7
  const commander_1 = require("commander");
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const client_1 = require("../../lts/client");
10
+ const pagination_1 = require("../../lts/pagination");
10
11
  const output_1 = require("../../output");
12
+ const config_1 = require("../../config");
11
13
  const common_1 = require("./common");
14
+ /**
15
+ * 默认按全文索引检索,而不是 REQUEST_ID。
16
+ *
17
+ * 理由:`REQUEST_ID` 是某类应用的字段命名约定,不是 LTS 的通用概念。新用户的日志流
18
+ * 里往往没有这个字段,写死它会让 `trace` 在正确的用法下返回空。全文检索总能命中,
19
+ * 而用到专用字段的人可以在 config 里设一次 `traceField`。
20
+ */
21
+ const DEFAULT_TRACE_FIELD = 'content';
12
22
  function createTraceCommand() {
13
23
  const cmd = new commander_1.Command('trace');
14
- cmd.description('Trace a request by REQUEST_ID — fetch the full request chain in chronological order')
15
- .argument('<requestId>', 'REQUEST_ID value to trace')
24
+ cmd.description('Trace a request by ID — fetch the full request chain in chronological order')
25
+ .argument('<requestId>', 'Value to trace (request ID, trace ID, or any search text)')
16
26
  .option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
17
27
  .option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
18
- .option('--field <name>', 'Field to match: REQUEST_ID (default), trace_id, or content (full-text fallback)', 'REQUEST_ID')
28
+ .option('--field <name>', `Field to match. Defaults to config.traceField, then "${DEFAULT_TRACE_FIELD}"`)
19
29
  .option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (default: 24h)')
20
30
  .option('--start-time <startTime>', 'Start time (ISO 8601)')
21
31
  .option('--end-time <endTime>', 'End time (ISO 8601)')
@@ -28,7 +38,7 @@ function createTraceCommand() {
28
38
  const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
29
39
  const timeParams = (0, common_1.resolveTimeParams)(options, '24h');
30
40
  const format = (0, common_1.resolveFormat)(options.format);
31
- const field = options.field;
41
+ const field = options.field || (0, config_1.getConfig)('traceField') || DEFAULT_TRACE_FIELD;
32
42
  // 值加双引号:含连字符的 UUID 不加引号会被分词导致匹配不到
33
43
  const query = `${field}:"${requestId}"`;
34
44
  const client = new client_1.LTSClient(config);
@@ -40,24 +50,18 @@ function createTraceCommand() {
40
50
  reverse: false, // 链路按时间正序
41
51
  };
42
52
  try {
43
- // Auto-pagination up to --limit
44
- const pageSize = Math.min(options.limit || 200, 500);
45
53
  const maxTotal = options.limit || 200;
46
- const allLogs = [];
47
- let offset = 0;
48
- let hasMore = true;
49
- while (hasMore && allLogs.length < maxTotal) {
50
- const result = await client.queryLogs({ ...params, offset, limit: pageSize });
51
- allLogs.push(...result.logs);
52
- if (result.logs.length < pageSize) {
53
- hasMore = false;
54
- }
55
- else {
56
- offset += pageSize;
57
- }
54
+ // 每页不超过 500:再大 LTS 会开始截断,且首屏延迟明显变差
55
+ const pageSize = Math.min(maxTotal, 500);
56
+ const result = await (0, pagination_1.fetchAllPages)((p) => client.queryLogs(p), params, {
57
+ max: maxTotal,
58
+ pageSize,
59
+ });
60
+ (0, common_1.warnIfIncomplete)(result);
61
+ if (result.truncated) {
62
+ console.error(`Warning: stopped at --limit ${maxTotal}; the chain may continue. Re-run with a larger --limit.`);
58
63
  }
59
- const result = { logs: allLogs.slice(0, maxTotal), total: allLogs.length };
60
- let output = (0, output_1.formatOutput)(result, format);
64
+ let output = (0, output_1.formatOutput)({ logs: result.logs, total: result.total }, format);
61
65
  // pretty 格式下高亮 requestId,便于肉眼定位
62
66
  if (format === 'pretty' && result.logs.length > 0) {
63
67
  output = output.split(requestId).join(chalk_1.default.yellow.bold(requestId));
package/dist/cli/index.js CHANGED
@@ -1,6 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
4
39
  const commander_1 = require("commander");
5
40
  const query_1 = require("./commands/query");
6
41
  const trace_1 = require("./commands/trace");
@@ -8,20 +43,38 @@ const context_1 = require("./commands/context");
8
43
  const streams_1 = require("./commands/streams");
9
44
  const config_1 = require("./commands/config");
10
45
  const init_1 = require("./commands/init");
46
+ const fields_1 = require("./commands/fields");
47
+ /**
48
+ * Read the version from package.json at runtime.
49
+ *
50
+ * A hard-coded version string drifts silently from package.json — `lts-cli --version`
51
+ * then reports a build that was never published. Resolved relative to __dirname so it
52
+ * works both from src/ (tsx) and dist/ (published package).
53
+ */
54
+ function readVersion() {
55
+ try {
56
+ const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
57
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
58
+ }
59
+ catch {
60
+ return '0.0.0';
61
+ }
62
+ }
11
63
  const program = new commander_1.Command();
12
64
  program
13
65
  .name('lts-cli')
14
66
  .description('Huawei Cloud LTS (Log Tank Service) CLI — query logs from the terminal')
15
- .version('1.0.0')
67
+ .version(readVersion())
16
68
  .addHelpText('after', `
17
69
  Examples:
18
70
  $ lts-cli init Setup credentials interactively
19
71
  $ lts-cli init --from-url <url> Import region/groupId/streamId from a console URL
20
72
  $ lts-cli config --list Show current configuration
21
- $ lts-cli query --last 2h --app acerp-gateway --content "error"
22
- $ lts-cli trace <requestId> Fetch full request chain by REQUEST_ID
23
73
  $ lts-cli groups List log groups
24
- $ lts-cli streams List log streams
74
+ $ lts-cli streams List log streams in a group
75
+ $ lts-cli fields Show the index config of a log stream
76
+ $ lts-cli query --last 2h -q 'level:error' --all
77
+ $ lts-cli trace <requestId> Fetch the full request chain
25
78
 
26
79
  Environment variables:
27
80
  LTS_AK Access Key
@@ -38,7 +91,15 @@ program.addCommand((0, trace_1.createTraceCommand)());
38
91
  program.addCommand((0, context_1.createContextCommand)());
39
92
  program.addCommand((0, streams_1.createGroupsCommand)());
40
93
  program.addCommand((0, streams_1.createStreamsCommand)());
94
+ program.addCommand((0, fields_1.createFieldsCommand)());
41
95
  program.addCommand((0, config_1.createConfigCommand)());
96
+ /**
97
+ * 补全列表手写在下面,和命令定义分居两处,很容易漏更新。
98
+ * 改任何命令的选项时请同步这三段。
99
+ */
100
+ const QUERY_OPTS = '-g --group-id -s --stream-id --start-time --end-time --st --et --last -q --query --app --content --request-id -l --limit -r --reverse -f --format --all';
101
+ const TRACE_OPTS = '-g --group-id -s --stream-id --field --last --start-time --end-time --st --et -l --limit -f --format';
102
+ const VALUE_OPTS = '-g --group-id -s --stream-id -f --format';
42
103
  // Shell completion support
43
104
  const completionCmd = new commander_1.Command('completion')
44
105
  .description('Generate shell completion script')
@@ -53,7 +114,7 @@ _lts_cli_completion() {
53
114
  local cur="\${words[cword]}"
54
115
 
55
116
  if (( cword == 1 )); then
56
- COMPREPLY=( $(compgen -W "query trace context groups streams config init completion" -- "\$cur") )
117
+ COMPREPLY=( $(compgen -W "query trace context groups streams fields config init completion" -- "\$cur") )
57
118
  return
58
119
  fi
59
120
 
@@ -61,16 +122,32 @@ _lts_cli_completion() {
61
122
  case "\$cmd" in
62
123
  query)
63
124
  case "\$prev" in
64
- -g|--group-id|-s|--stream-id|-k|--keyword|-l|--limit|-o|--offset|-f|--format)
125
+ -g|--group-id|-s|--stream-id|-q|--query|--app|--content|--request-id|-l|--limit|-f|--format)
126
+ COMPREPLY=()
127
+ return
128
+ ;;
129
+ *)
130
+ COMPREPLY=( $(compgen -W "${QUERY_OPTS}" -- "\$cur") )
131
+ return
132
+ ;;
133
+ esac
134
+ ;;
135
+ trace)
136
+ case "\$prev" in
137
+ -g|--group-id|-s|--stream-id|--field|--last|-l|--limit|-f|--format)
65
138
  COMPREPLY=()
66
139
  return
67
140
  ;;
68
141
  *)
69
- COMPREPLY=( $(compgen -W "-g --group-id -s --stream-id --start-time --end-time --st --et -k --keyword -l --limit -o --offset -r --reverse -f --format --no-paginate" -- "\$cur") )
142
+ COMPREPLY=( $(compgen -W "${TRACE_OPTS}" -- "\$cur") )
70
143
  return
71
144
  ;;
72
145
  esac
73
146
  ;;
147
+ fields)
148
+ COMPREPLY=( $(compgen -W "${VALUE_OPTS}" -- "\$cur") )
149
+ return
150
+ ;;
74
151
  config)
75
152
  COMPREPLY=( $(compgen -W "--set --get --list --unset" -- "\$cur") )
76
153
  return
@@ -94,10 +171,11 @@ _lts_cli() {
94
171
  local -a commands
95
172
  commands=(
96
173
  'query:Query LTS logs'
97
- 'trace:Trace request chain by REQUEST_ID'
174
+ 'trace:Trace a request by ID'
98
175
  'context:View logs before/after a log entry'
99
176
  'groups:List log groups'
100
177
  'streams:List log streams'
178
+ 'fields:Show the index config of a log stream'
101
179
  'config:Manage configuration'
102
180
  'init:Interactive setup wizard'
103
181
  'completion:Generate shell completion script'
@@ -124,12 +202,34 @@ _lts_cli() {
124
202
  '--end-time[End time (ISO 8601)]:time:' \\
125
203
  '--st[Alias for --start-time]:time:' \\
126
204
  '--et[Alias for --end-time]:time:' \\
127
- '-k[Search keyword]:keyword:' \\
128
- '-l[Limit]:number:' \\
129
- '-o[Offset]:number:' \\
205
+ '--last[Relative range: 30m, 2h, 1d, 7d]:duration:' \\
206
+ '-q[Query expression]:query:' \\
207
+ '--app[Filter by appName]:appName:' \\
208
+ '--content[Filter by content]:keyword:' \\
209
+ '--request-id[Filter by REQUEST_ID]:id:' \\
210
+ '-l[Page size]:number:' \\
130
211
  '-r[Reverse order]' \\
131
212
  '-f[Output format]:format:(json table pretty)' \\
132
- '--no-paginate[Disable pagination]'
213
+ '--all[Follow the cursor to the end]'
214
+ ;;
215
+ trace)
216
+ _arguments \\
217
+ '-g[Log group ID]:group ID:' \\
218
+ '-s[Log stream ID]:stream ID:' \\
219
+ '--field[Field to match]:field:' \\
220
+ '--last[Relative range: 30m, 2h, 1d, 7d]:duration:' \\
221
+ '--start-time[Start time (ISO 8601)]:time:' \\
222
+ '--end-time[End time (ISO 8601)]:time:' \\
223
+ '--st[Alias for --start-time]:time:' \\
224
+ '--et[Alias for --end-time]:time:' \\
225
+ '-l[Max logs]:number:' \\
226
+ '-f[Output format]:format:(json table pretty)'
227
+ ;;
228
+ fields|groups|streams)
229
+ _arguments \\
230
+ '-g[Log group ID]:group ID:' \\
231
+ '-s[Log stream ID]:stream ID:' \\
232
+ '-f[Output format]:format:(json table pretty)'
133
233
  ;;
134
234
  config)
135
235
  _arguments \\
@@ -152,7 +252,7 @@ compdef _lts_cli lts-cli`);
152
252
  Register-ArgumentCompleter -CommandName lts-cli -ScriptBlock {
153
253
  param(\$wordToComplete, \$commandAst, \$cursorPosition)
154
254
 
155
- \$commands = @('query', 'trace', 'context', 'groups', 'streams', 'config', 'init', 'completion')
255
+ \$commands = @('query', 'trace', 'context', 'groups', 'streams', 'fields', 'config', 'init', 'completion')
156
256
 
157
257
  \$args = \$commandAst.ToString() -split '\\s+'
158
258
  if (\$args.Count -le 1) {
@@ -161,7 +261,15 @@ Register-ArgumentCompleter -CommandName lts-cli -ScriptBlock {
161
261
 
162
262
  switch (\$args[1]) {
163
263
  'query' {
164
- \$opts = @('-g', '-s', '--start-time', '--end-time', '--st', '--et', '-k', '-l', '-o', '-r', '-f', '--no-paginate')
264
+ \$opts = @('-g', '-s', '--start-time', '--end-time', '--st', '--et', '--last', '-q', '--app', '--content', '--request-id', '-l', '-r', '-f', '--all')
265
+ return \$opts | Where-Object { \$_ -like "\$wordToComplete*" }
266
+ }
267
+ 'trace' {
268
+ \$opts = @('-g', '-s', '--field', '--last', '--start-time', '--end-time', '--st', '--et', '-l', '-f')
269
+ return \$opts | Where-Object { \$_ -like "\$wordToComplete*" }
270
+ }
271
+ 'fields' {
272
+ \$opts = @('-g', '-s', '-f')
165
273
  return \$opts | Where-Object { \$_ -like "\$wordToComplete*" }
166
274
  }
167
275
  'config' {
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getProfilePath = getProfilePath;
37
+ exports.profileExists = profileExists;
38
+ exports.writeProfileSkeleton = writeProfileSkeleton;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const store_1 = require("./store");
42
+ /**
43
+ * `profile.md` 是给人写、给 AI 读的业务词表。CLI 从不读它。
44
+ *
45
+ * 分界线:`config.json` 存 CLI 的行为(凭证、traceField、默认 group/stream);
46
+ * `profile.md` 存 API 拿不到的东西(业务词 → 日志里的值、命名约定、黑话)。
47
+ * 字段清单、日志组/流 ID 一律不进 profile —— 那是第二份会过期的真相,
48
+ * 用 `lts-cli groups` / `streams` / `fields` 现查。
49
+ */
50
+ function getProfilePath() {
51
+ return path.join((0, store_1.getConfigDir)(), 'profile.md');
52
+ }
53
+ function profileExists() {
54
+ return fs.existsSync(getProfilePath());
55
+ }
56
+ const SKELETON = `# LTS 业务词表(本机私有)
57
+
58
+ > 这份文件给人改、给 AI 读。\`lts-cli\` 本身**不读**它——CLI 的行为配置在 \`~/.lts-cli/config.json\`。
59
+ >
60
+ > 只写 API 拿不到的东西:业务词怎么落到日志里的值、团队自己的命名约定、黑话。
61
+ > **不要**把日志组/日志流 ID、字段清单写进来——用 \`lts-cli groups\` / \`lts-cli streams\` /
62
+ > \`lts-cli fields\` 现查。抄进文档的那一刻它就开始过期。
63
+
64
+ ## 1. 业务称呼 → 日志里的取值
65
+
66
+ 查日志前先在这里翻译,避免每次重新猜应用名。
67
+
68
+ | 业务称呼 | 日志中的取值 | 说明 |
69
+ | --- | --- | --- |
70
+ | 订单服务 | (填字段值和值) | 例如 appName 或 service_name 的取值 |
71
+
72
+ ## 2. 字段含义
73
+
74
+ 只写「这个字段在我们这里表示什么」。类型、是否大小写敏感、分词符请用 \`lts-cli fields\` 查。
75
+
76
+ | 字段 | 含义 |
77
+ | --- | --- |
78
+ | (字段名) | (说明) |
79
+
80
+ ## 3. 命名约定与分词陷阱
81
+
82
+ - 哪些值含连字符/下划线,必须加双引号才会被当成一个词
83
+ - 值的大小写约定(全大写?小写?)
84
+ - 哪些字段看起来该有索引、实际上没建(没建索引的字段只能全文检索命中)
85
+
86
+ ## 4. 团队黑话
87
+
88
+ | 说法 | 实际指什么 |
89
+ | --- | --- |
90
+ | (黑话) | (含义) |
91
+
92
+ ## 5. 常用排查入口
93
+
94
+ | 想查什么 | 建议命令 |
95
+ | --- | --- |
96
+ | 某个 ID 的完整链路 | \`lts-cli trace <id>\` |
97
+ | 某业务最近的报错 | \`lts-cli query --last 1h -q '(字段):(值)'\` |
98
+ | 先看清楚这条流有什么字段 | \`lts-cli fields\` |
99
+ `;
100
+ function writeProfileSkeleton() {
101
+ const target = getProfilePath();
102
+ if (fs.existsSync(target)) {
103
+ return { path: target, created: false };
104
+ }
105
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
106
+ fs.writeFileSync(target, SKELETON, { encoding: 'utf-8', mode: 0o600 });
107
+ return { path: target, created: true };
108
+ }
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.readConfigFile = readConfigFile;
37
37
  exports.writeConfigFile = writeConfigFile;
38
38
  exports.getConfigFilePath = getConfigFilePath;
39
+ exports.getConfigDir = getConfigDir;
39
40
  const fs = __importStar(require("fs"));
40
41
  const path = __importStar(require("path"));
41
42
  const os = __importStar(require("os"));
@@ -43,7 +44,7 @@ const CONFIG_DIR = path.join(os.homedir(), '.lts-cli');
43
44
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
44
45
  function ensureDir(dir) {
45
46
  if (!fs.existsSync(dir)) {
46
- fs.mkdirSync(dir, { recursive: true });
47
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
47
48
  }
48
49
  }
49
50
  function readConfigFile() {
@@ -60,8 +61,12 @@ function readConfigFile() {
60
61
  }
61
62
  function writeConfigFile(config) {
62
63
  ensureDir(CONFIG_DIR);
63
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
64
+ // 配置里是明文 AK/SK:目录 0700、文件 0600,别让同机器上的其他账号读到
65
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
64
66
  }
65
67
  function getConfigFilePath() {
66
68
  return CONFIG_FILE;
67
69
  }
70
+ function getConfigDir() {
71
+ return CONFIG_DIR;
72
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LTSClient = void 0;
4
4
  exports.resolveTimeRange = resolveTimeRange;
5
+ exports.parseQueryExpression = parseQueryExpression;
5
6
  const LtsClient_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/LtsClient");
6
7
  const ClientBuilder_1 = require("@huaweicloud/huaweicloud-sdk-core/ClientBuilder");
7
8
  const BasicCredentials_1 = require("@huaweicloud/huaweicloud-sdk-core/auth/BasicCredentials");
@@ -12,6 +13,8 @@ const ListLogContextRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/mod
12
13
  const ListLogContextRequestBody_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogContextRequestBody");
13
14
  const ListLogGroupsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogGroupsRequest");
14
15
  const ListLogStreamsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogStreamsRequest");
16
+ const ListLogStreamIndexRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogStreamIndexRequest");
17
+ const pagination_1 = require("./pagination");
15
18
  function sleep(ms) {
16
19
  return new Promise((resolve) => setTimeout(resolve, ms));
17
20
  }
@@ -47,6 +50,9 @@ function resolveTimeRange(params) {
47
50
  * - "搜索语句 | SELECT ..." → keywords=搜索语句, query=SQL, is_analysis_query=true
48
51
  * - 裸 "SELECT ..." → query=SQL, is_analysis_query=true
49
52
  * - 其他(field:value 等) → keywords=搜索语句
53
+ *
54
+ * 导出仅为测试:这一段决定了用户写的字符串怎么落到 keywords/query 两个字段上,
55
+ * 拆错了就会静默返回全量日志。
50
56
  */
51
57
  function parseQueryExpression(expr) {
52
58
  const trimmed = expr.trim();
@@ -94,7 +100,6 @@ class LTSClient {
94
100
  .withStartTime(startMs.toString())
95
101
  .withEndTime(endMs.toString())
96
102
  .withLimit(Number.isFinite(params.limit) ? params.limit : 100);
97
- // 优先使用 query 参数(支持SQL和结构化查询)
98
103
  if (params.query) {
99
104
  const parsed = parseQueryExpression(params.query);
100
105
  if (parsed.keywords) {
@@ -107,34 +112,37 @@ class LTSClient {
107
112
  queryParams.withIsAnalysisQuery(true);
108
113
  }
109
114
  }
110
- else if (params.keyword) {
111
- // 向后兼容:简单关键词搜索
112
- queryParams.withKeywords(params.keyword);
113
- }
114
115
  if (params.reverse !== undefined) {
115
116
  queryParams.withIsDesc(params.reverse);
116
117
  }
118
+ // 游标翻页:首屏不带游标,后续页把上一页末条的 line_num 当作续读起点
119
+ if (params.lineNum) {
120
+ queryParams.withLineNum(params.lineNum);
121
+ queryParams.withSearchType(params.searchType ?? (0, pagination_1.cursorDirection)(params.reverse));
122
+ }
117
123
  const request = new ListLogsRequest_1.ListLogsRequest(params.groupId, params.streamId, 'application/json')
118
124
  .withBody(queryParams);
119
125
  const response = await this.retryRequest(() => this.client.listLogs(request));
120
126
  const raw = response;
121
- // SQL 分析查询:结果在 analysisLogs 中(行为 JSON 对象)
127
+ // SQL 分析查询:结果在 analysisLogs 中(行为 JSON 对象,没有 line_num,无法游标翻页)
122
128
  if (Array.isArray(raw.analysisLogs)) {
123
129
  const logs = raw.analysisLogs.map((row) => ({
124
- logTime: '',
130
+ lineNum: '',
125
131
  content: JSON.stringify(row),
126
132
  labels: {},
127
133
  }));
128
- return { logs, total: logs.length };
134
+ return { logs, total: logs.length, isQueryComplete: raw.isQueryComplete };
129
135
  }
130
136
  const logs = (response.logs || []).map((log) => ({
131
- logTime: log.line_num ?? log.lineNum ?? '',
137
+ lineNum: log.line_num ?? log.lineNum ?? '',
132
138
  content: stripHighlightTags(log.content ?? ''),
133
139
  labels: Object.fromEntries(Object.entries(log.labels ?? {}).map(([k, v]) => [k, stripHighlightTags(v)])),
134
140
  }));
135
141
  return {
136
142
  logs,
137
143
  total: response.count ?? logs.length,
144
+ // 服务端报告结果被截断时必须传出去,否则 AI 会拿着残缺数据下结论
145
+ isQueryComplete: raw.isQueryComplete,
138
146
  };
139
147
  }
140
148
  /** 查询某条日志的上下文(前后各 N 行),对应控制台的“查看上下文” */
@@ -148,7 +156,7 @@ class LTSClient {
148
156
  .withBody(body);
149
157
  const response = await this.retryRequest(() => this.client.listLogContext(request));
150
158
  const logs = (response.logs || []).map((log) => ({
151
- logTime: log.line_num ?? log.lineNum ?? '',
159
+ lineNum: log.line_num ?? log.lineNum ?? '',
152
160
  content: log.content ?? '',
153
161
  labels: log.labels ?? {},
154
162
  }));
@@ -184,6 +192,32 @@ class LTSClient {
184
192
  groupId: s.log_group_id,
185
193
  }));
186
194
  }
195
+ /** 读取日志流的索引配置(全文索引 + 字段索引),供 `fields` 命令与 AI 运行时发现 */
196
+ async getStreamIndex(groupId, streamId) {
197
+ const request = new ListLogStreamIndexRequest_1.ListLogStreamIndexRequest(groupId, streamId, 'application/json');
198
+ const response = await this.retryRequest(() => this.client.listLogStreamIndex(request));
199
+ const raw = response;
200
+ // SDK 在不同版本下可能给出 class 实例(camelCase)或纯对象(snake_case),两者都认
201
+ const fullText = raw.fullTextIndex ?? raw.full_text_index ?? {};
202
+ const fields = (raw.fields ?? []);
203
+ return {
204
+ streamId: raw.logStreamId ?? raw.log_stream_id ?? streamId,
205
+ fullText: {
206
+ enabled: fullText.enable === true,
207
+ caseSensitive: fullText.caseSensitive ?? fullText.case_sensitive,
208
+ includeChinese: fullText.includeChinese ?? fullText.include_chinese,
209
+ tokenizer: fullText.tokenizer,
210
+ },
211
+ fields: fields.map((f) => ({
212
+ name: f.fieldName ?? f.field_name ?? '',
213
+ type: f.fieldType ?? f.field_type ?? '',
214
+ caseSensitive: f.caseSensitive ?? f.case_sensitive,
215
+ includeChinese: f.includeChinese ?? f.include_chinese,
216
+ tokenizer: f.tokenizer,
217
+ quickAnalysis: f.quickAnalysis ?? f.quick_analysis,
218
+ })),
219
+ };
220
+ }
187
221
  async retryRequest(fn, maxRetries = 3) {
188
222
  let lastError;
189
223
  for (let attempt = 0; attempt <= maxRetries; attempt++) {