rl-rockcli 0.0.1

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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 时间解析工具
3
+ * 支持时间戳、相对时间、日期字符串等多种格式
4
+ */
5
+
6
+ /**
7
+ * 解析时间字符串为时间戳(毫秒)
8
+ * @param {string} timeStr - 时间字符串
9
+ * @returns {number|null} 时间戳(毫秒)
10
+ * @throws {Error} 无法解析的时间格式
11
+ */
12
+ function parseTime(timeStr) {
13
+ if (!timeStr) return null;
14
+
15
+ // 如果是纯数字,智能判断是秒级还是毫秒级时间戳
16
+ if (/^\d+$/.test(timeStr)) {
17
+ const timestamp = parseInt(timeStr);
18
+ // Unix 时间戳标准是秒级的(10位数字),毫秒级通常是13位数字
19
+ // 如果数字长度 <= 10,认为是秒级时间戳,需要转换为毫秒
20
+ // 如果数字长度 > 10,认为是毫秒级时间戳,直接使用
21
+ if (timestamp <= 9999999999) {
22
+ return timestamp * 1000; // 秒级 -> 毫秒级
23
+ } else {
24
+ return timestamp; // 已经是毫秒级
25
+ }
26
+ }
27
+
28
+ // 解析相对时间格式(如 "15m", "1h", "2d")
29
+ const relativeMatch = timeStr.match(/^(\d+)([mhd])$/);
30
+ if (relativeMatch) {
31
+ const value = parseInt(relativeMatch[1]);
32
+ const unit = relativeMatch[2];
33
+ const now = Date.now();
34
+ switch (unit) {
35
+ case 'm': return now - value * 60 * 1000; // 分钟
36
+ case 'h': return now - value * 60 * 60 * 1000; // 小时
37
+ case 'd': return now - value * 24 * 60 * 60 * 1000; // 天
38
+ }
39
+ }
40
+
41
+ // 尝试解析为日期字符串
42
+ const date = new Date(timeStr);
43
+ if (!isNaN(date.getTime())) {
44
+ return date.getTime();
45
+ }
46
+
47
+ throw new Error(`无法解析时间格式: ${timeStr}`);
48
+ }
49
+
50
+ module.exports = {
51
+ parseTime,
52
+ };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * 本地文件日志客户端
3
+ * 实现 LogProvider 接口
4
+ *
5
+ * 默认读取路径:~/.rock/logs/rock-cli-YYYY-MM-DD.log
6
+ * 每行日志建议为 JSON 字符串,至少包含 timestamp(ms) 或 time_iso8601、level、message 等字段。
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+ const path = require('path');
12
+ const readline = require('readline');
13
+
14
+ /**
15
+ * 根据时间范围推导需要读取的日志文件列表。
16
+ * @param {Object} params
17
+ * @param {string} [params.logDir] 日志目录,默认 ~/.rock/logs
18
+ * @param {number} params.startTime 毫秒时间戳
19
+ * @param {number} params.endTime 毫秒时间戳
20
+ * @returns {string[]}
21
+ */
22
+ function getLogFilesInRange({ logDir, startTime, endTime }) {
23
+ const dir = logDir || path.join(os.homedir(), '.rock', 'logs');
24
+ const files = [];
25
+
26
+ const start = new Date(startTime);
27
+ const end = new Date(endTime);
28
+
29
+ const current = new Date(start.getFullYear(), start.getMonth(), start.getDate());
30
+ const last = new Date(end.getFullYear(), end.getMonth(), end.getDate());
31
+
32
+ while (current <= last) {
33
+ const yyyy = current.getFullYear();
34
+ const mm = String(current.getMonth() + 1).padStart(2, '0');
35
+ const dd = String(current.getDate()).padStart(2, '0');
36
+ const fileName = `rock-cli-${yyyy}-${mm}-${dd}.log`;
37
+ files.push(path.join(dir, fileName));
38
+ current.setDate(current.getDate() + 1);
39
+ }
40
+
41
+ return files;
42
+ }
43
+
44
+ /**
45
+ * 简单的 filter 函数构造器:把 filter 当作空格分隔的关键字 AND 匹配。
46
+ * @param {string | null | undefined} filter
47
+ */
48
+ function buildSimpleFilterFn(filter) {
49
+ if (!filter) {
50
+ return () => true;
51
+ }
52
+
53
+ const keywords = String(filter)
54
+ .split(/\s+/)
55
+ .map((s) => s.trim())
56
+ .filter(Boolean)
57
+ .map((s) => s.toLowerCase());
58
+
59
+ return (logObj, rawLine) => {
60
+ const haystack = rawLine.toLowerCase();
61
+ return keywords.every((kw) => haystack.includes(kw));
62
+ };
63
+ }
64
+
65
+ /**
66
+ * 创建本地文件日志客户端。
67
+ * 其接口尽量与内部版的 mysqlClient 保持一致:connect/query/close。
68
+ *
69
+ * @param {Object} config
70
+ * @param {string} [config.logDir] 日志目录,默认 ~/.rock/logs
71
+ */
72
+ function createFileLogClient(config = {}) {
73
+ return {
74
+ async connect() {
75
+ // 与数据库客户端接口对齐,这里不需要实际连接
76
+ return this;
77
+ },
78
+
79
+ /**
80
+ * 查询日志。
81
+ * @param {Object} queryOptions
82
+ * @param {number} queryOptions.startTime - 毫秒时间戳
83
+ * @param {number} queryOptions.endTime - 毫秒时间戳
84
+ * @param {string} [queryOptions.filter] - 简单查询字符串(关键字匹配)
85
+ * @param {number} [queryOptions.limit] - 返回条数限制
86
+ * @param {number} [queryOptions.offset] - 偏移量
87
+ * @param {boolean} [queryOptions.count] - 是否只返回数量
88
+ * @param {string} [queryOptions.table] - 表名(内网专有字段,开源版忽略)
89
+ * @param {string} [queryOptions.orderBy] - 排序字段(开源版忽略)
90
+ * @param {string} [queryOptions.orderDirection] - 排序方向(开源版忽略)
91
+ */
92
+ async query(queryOptions) {
93
+ const {
94
+ startTime,
95
+ endTime,
96
+ filter,
97
+ limit = 1000,
98
+ offset = 0,
99
+ count = false,
100
+ // table, orderBy, orderDirection 字段被命令层传递,但开源版忽略
101
+ } = queryOptions;
102
+
103
+ if (typeof startTime !== 'number' || typeof endTime !== 'number') {
104
+ throw new Error('startTime and endTime are required (number, ms)');
105
+ }
106
+
107
+ const logFiles = getLogFilesInRange({
108
+ logDir: config.logDir,
109
+ startTime,
110
+ endTime,
111
+ });
112
+
113
+ const filterFn = buildSimpleFilterFn(filter);
114
+
115
+ let matched = [];
116
+ let total = 0;
117
+
118
+ for (const file of logFiles) {
119
+ if (!fs.existsSync(file)) continue;
120
+
121
+ const stream = fs.createReadStream(file, { encoding: 'utf8' });
122
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
123
+
124
+ // eslint-disable-next-line no-restricted-syntax
125
+ for await (const line of rl) {
126
+ if (!line.trim()) continue;
127
+
128
+ let obj;
129
+ try {
130
+ obj = JSON.parse(line);
131
+ } catch (e) {
132
+ obj = { message: line };
133
+ }
134
+
135
+ let ts = obj.timestamp;
136
+ if (!ts && obj.time_iso8601) {
137
+ const parsed = Date.parse(obj.time_iso8601);
138
+ if (!Number.isNaN(parsed)) {
139
+ ts = parsed;
140
+ }
141
+ }
142
+ if (!ts) continue;
143
+
144
+ if (ts < startTime || ts > endTime) {
145
+ continue;
146
+ }
147
+
148
+ if (!filterFn(obj, line)) {
149
+ continue;
150
+ }
151
+
152
+ total += 1;
153
+
154
+ if (!count) {
155
+ matched.push({
156
+ ...obj,
157
+ '@timestamp': String(ts),
158
+ _file: file,
159
+ });
160
+ }
161
+ }
162
+ }
163
+
164
+ if (count) {
165
+ // 与内部版约定保持一致:返回带 total 字段的对象数组
166
+ return [{ total }];
167
+ }
168
+
169
+ matched.sort((a, b) => {
170
+ const ta = Number(a['@timestamp']) || 0;
171
+ const tb = Number(b['@timestamp']) || 0;
172
+ return ta - tb;
173
+ });
174
+
175
+ const sliced = matched.slice(offset, offset + limit);
176
+ return sliced;
177
+ },
178
+
179
+ async close() {
180
+ // 文件读取无需显式关闭资源(readline 已处理),此处保留接口
181
+ return undefined;
182
+ },
183
+ };
184
+ }
185
+
186
+ module.exports = createFileLogClient;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * oss-file-log 插件入口
3
+ * 提供本地文件日志后端
4
+ */
5
+
6
+ const createFileLogClient = require('./file-client');
7
+
8
+ module.exports = {
9
+ name: 'oss-file-log',
10
+ description: 'Local file log backend for rock-cli',
11
+ version: '0.1.0',
12
+
13
+ // 导出客户端创建函数
14
+ createClient: createFileLogClient,
15
+
16
+ // Provider 工厂
17
+ createProvider: (config = {}) => createFileLogClient(config),
18
+ };