@ynhcj/xiaoyi-channel 0.0.209-beta → 0.0.210-beta
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/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +6 -12
- package/dist/src/cspl/config.d.ts +0 -1
- package/dist/src/cspl/config.js +65 -59
- package/dist/src/cspl/constants.d.ts +36 -1
- package/dist/src/cspl/constants.js +9 -0
- package/dist/src/cspl/sentinel_hook.js +26 -22
- package/dist/src/cspl/upload_file.js +5 -6
- package/dist/src/cspl/utils.d.ts +20 -9
- package/dist/src/cspl/utils.js +119 -75
- package/dist/src/log-reporter/index.d.ts +1 -1
- package/dist/src/log-reporter/index.js +159 -35
- package/dist/src/log-reporter/openclaw-parser.d.ts +18 -0
- package/dist/src/log-reporter/openclaw-parser.js +94 -0
- package/dist/src/log-reporter/path-resolver.d.ts +5 -0
- package/dist/src/log-reporter/path-resolver.js +50 -0
- package/dist/src/log-reporter/reporter.d.ts +4 -4
- package/dist/src/log-reporter/reporter.js +52 -13
- package/dist/src/log-reporter/scanner.d.ts +7 -3
- package/dist/src/log-reporter/scanner.js +3 -7
- package/dist/src/log-reporter/types.d.ts +26 -27
- package/dist/src/log-reporter/uploader.d.ts +4 -3
- package/dist/src/log-reporter/uploader.js +32 -9
- package/dist/src/monitor.js +0 -1
- package/package.json +1 -1
package/dist/src/cspl/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 版权所有 (c) 华为技术有限公司 2026-2026
|
|
3
3
|
*/
|
|
4
|
-
import { MAX_TEXT_LENGTH, regex, SECURITY_NOTICE, MAX_FILE_COUNT, MAX_COMMAND_LENGTH, CODE_FILE_EXTENSIONS,
|
|
4
|
+
import { MAX_TEXT_LENGTH, regex, SECURITY_NOTICE, MAX_FILE_COUNT, MAX_COMMAND_LENGTH, CODE_FILE_EXTENSIONS, FILE_EXTENSION_REGEX, RESULT_CODE_MAP } from './constants.js';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
@@ -46,7 +46,7 @@ export function extractResultText(event, toolName) {
|
|
|
46
46
|
}
|
|
47
47
|
return resultTexts.length > 0 ? resultTexts.join("; ") : "";
|
|
48
48
|
}
|
|
49
|
-
export function processText(resultText
|
|
49
|
+
export function processText(resultText) {
|
|
50
50
|
const questionText = filterText(resultText);
|
|
51
51
|
// 检查是否超过4096字符限制,进行截断
|
|
52
52
|
const { text: finalText, truncated } = validateAndTruncateText(questionText, MAX_TEXT_LENGTH);
|
|
@@ -69,8 +69,14 @@ export function parseSecurityResult(response) {
|
|
|
69
69
|
if (securityResult !== securityResult.trim()) {
|
|
70
70
|
throw new Error('Response.data.securityResult contains leading or trailing spaces');
|
|
71
71
|
}
|
|
72
|
-
if (securityResult !== '
|
|
73
|
-
throw new Error(`Response.data.securityResult must be "
|
|
72
|
+
if (securityResult !== 'accept' && securityResult !== 'reject') {
|
|
73
|
+
throw new Error(`Response.data.securityResult must be "accept" or "reject". Actual value: "${securityResult}"`);
|
|
74
|
+
}
|
|
75
|
+
// 解析 resultCode 并打印错误日志
|
|
76
|
+
const resultCode = response.data?.resultCode;
|
|
77
|
+
if (resultCode !== undefined && resultCode !== null && resultCode !== 0) {
|
|
78
|
+
const errorMsg = RESULT_CODE_MAP[resultCode] || `Unknown error code: ${resultCode}`;
|
|
79
|
+
logger.error(`[SENTINEL HOOK] API returned resultCode=${resultCode}: ${errorMsg}`);
|
|
74
80
|
}
|
|
75
81
|
return { status: securityResult };
|
|
76
82
|
}
|
|
@@ -177,56 +183,103 @@ export function getFileSizeInKB(filePath) {
|
|
|
177
183
|
return 0;
|
|
178
184
|
}
|
|
179
185
|
}
|
|
180
|
-
//
|
|
181
|
-
export function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
// 从taskId中提取sessionID(第一个&之前的内容)
|
|
187
|
+
export function extractSessionId(taskId) {
|
|
188
|
+
if (!taskId)
|
|
189
|
+
return '';
|
|
190
|
+
const idx = taskId.indexOf('&');
|
|
191
|
+
return idx === -1 ? taskId : taskId.substring(0, idx);
|
|
192
|
+
}
|
|
193
|
+
// 从taskId中提取interActionID(第一个&和第二个&之间的值)
|
|
194
|
+
export function extractInterActionId(taskId) {
|
|
195
|
+
if (!taskId)
|
|
196
|
+
return 1;
|
|
197
|
+
const parts = taskId.split('&');
|
|
198
|
+
if (parts.length >= 2) {
|
|
199
|
+
const id = parseInt(parts[1], 10);
|
|
200
|
+
if (!isNaN(id) && id > 0)
|
|
201
|
+
return id;
|
|
186
202
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
// reqTime 生成工具函数
|
|
206
|
+
function formatReqTime() {
|
|
207
|
+
const now = new Date();
|
|
208
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
209
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
210
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
|
|
211
|
+
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${ms}+0800`;
|
|
212
|
+
}
|
|
213
|
+
// 构建工具输入新消息体(新接口格式)
|
|
214
|
+
export function buildToolInputPayload(taskID, sessionID, toolName, toolArguments, toolCallId, interActionID, options) {
|
|
215
|
+
const callObj = {
|
|
216
|
+
type: 'function',
|
|
217
|
+
name: toolName,
|
|
218
|
+
arguments: toolArguments,
|
|
219
|
+
index: 0,
|
|
220
|
+
id: toolCallId
|
|
221
|
+
};
|
|
222
|
+
if (options?.file) {
|
|
223
|
+
callObj.file = [options.file];
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
taskID,
|
|
227
|
+
sessionID,
|
|
228
|
+
businessID: 'xiaoyi_claw',
|
|
229
|
+
sceneID: 'ACTION_DETECT',
|
|
230
|
+
checkPoint: 4,
|
|
231
|
+
interActionID,
|
|
232
|
+
loginType: 'APP',
|
|
233
|
+
reqTime: formatReqTime(),
|
|
234
|
+
message: {
|
|
235
|
+
input: {
|
|
236
|
+
toolIn: [{
|
|
237
|
+
toolCalls: [callObj]
|
|
238
|
+
}]
|
|
239
|
+
}
|
|
203
240
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// 构建工具输出新消息体(新接口格式)
|
|
244
|
+
export function buildToolOutputPayload(taskID, sessionID, funcName, content, toolCallId, interActionID) {
|
|
245
|
+
return {
|
|
246
|
+
taskID,
|
|
247
|
+
sessionID,
|
|
248
|
+
businessID: 'xiaoyi_claw',
|
|
249
|
+
sceneID: 'PROMPT_DETECT',
|
|
250
|
+
checkPoint: 6,
|
|
251
|
+
interActionID,
|
|
252
|
+
loginType: 'APP',
|
|
253
|
+
reqTime: formatReqTime(),
|
|
254
|
+
message: {
|
|
255
|
+
output: {
|
|
256
|
+
toolOut: [{
|
|
257
|
+
funcName,
|
|
258
|
+
content: [{
|
|
259
|
+
type: 'text',
|
|
260
|
+
rawText: content
|
|
261
|
+
}],
|
|
262
|
+
toolCallId
|
|
263
|
+
}]
|
|
264
|
+
}
|
|
208
265
|
}
|
|
209
|
-
}
|
|
210
|
-
bodyStr = JSON.stringify(adjusted);
|
|
211
|
-
if (bodyStr.length > MAX_TEXT_LENGTH) {
|
|
212
|
-
throw new Error(`Field ${lastFieldName} exceeds length limit, unable to send data.`);
|
|
213
|
-
}
|
|
214
|
-
return adjusted;
|
|
266
|
+
};
|
|
215
267
|
}
|
|
216
|
-
//
|
|
217
|
-
async function sendToolInputRequest(
|
|
218
|
-
const response = await callApi(
|
|
268
|
+
// 发送新接口请求并处理响应,返回扫描结果(保留block/steer能力)
|
|
269
|
+
async function sendToolInputRequest(payload, api, sessionId) {
|
|
270
|
+
const response = await callApi(payload, api, sessionId);
|
|
219
271
|
const result = parseSecurityResult(response);
|
|
220
272
|
logger.log(`[SENTINEL HOOK] TOOL_INPUT response: status=${result.status}`);
|
|
221
273
|
return result;
|
|
222
274
|
}
|
|
223
|
-
// 处理exec工具的TOOL_INPUT
|
|
224
|
-
export async function handleExecToolInput(event, api, sessionId) {
|
|
275
|
+
// 处理exec工具的TOOL_INPUT数据采集,返回扫描结果(保留block/steer能力)
|
|
276
|
+
export async function handleExecToolInput(event, api, sessionId, taskId) {
|
|
225
277
|
const command = extractInputParams(event, 'exec');
|
|
226
278
|
if (!command) {
|
|
227
279
|
logger.log('[SENTINEL HOOK] No command found for exec tool');
|
|
228
280
|
return null;
|
|
229
281
|
}
|
|
282
|
+
const interActionID = extractInterActionId(taskId);
|
|
230
283
|
// 解析命令提取文件路径
|
|
231
284
|
const filePaths = extractFilePathsFromCommand(command);
|
|
232
285
|
if (filePaths.length > 0) {
|
|
@@ -243,14 +296,17 @@ export async function handleExecToolInput(event, api, sessionId) {
|
|
|
243
296
|
const fileHash = calculateContentHash(fileContent);
|
|
244
297
|
const fileSize = getFileSizeInKB(filePath);
|
|
245
298
|
const obsUrl = await uploadFileToObsMain(filePath, api, fileHash, sessionId);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
299
|
+
// 截断 body 到 MAX_TEXT_LENGTH
|
|
300
|
+
let bodyContent = fileContent;
|
|
301
|
+
if (Buffer.byteLength(bodyContent, 'utf8') > MAX_TEXT_LENGTH) {
|
|
302
|
+
bodyContent = bodyContent.substring(0, MAX_TEXT_LENGTH);
|
|
303
|
+
logger.warn(`[SENTINEL HOOK] File content truncated to ${MAX_TEXT_LENGTH} characters`);
|
|
304
|
+
}
|
|
305
|
+
const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'exec', command, event.toolCallId, interActionID, { file: { type: 'doc', url: obsUrl, hash: fileHash, size: fileSize, body: bodyContent } });
|
|
306
|
+
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for file: ${path.basename(filePath)}, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
251
307
|
try {
|
|
252
|
-
lastResult = await sendToolInputRequest(
|
|
253
|
-
if (lastResult.status === '
|
|
308
|
+
lastResult = await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
309
|
+
if (lastResult.status === 'reject') {
|
|
254
310
|
return lastResult;
|
|
255
311
|
}
|
|
256
312
|
}
|
|
@@ -268,33 +324,26 @@ export async function handleExecToolInput(event, api, sessionId) {
|
|
|
268
324
|
else {
|
|
269
325
|
// 场景2:直接执行代码(heredoc场景)
|
|
270
326
|
logger.log('[SENTINEL HOOK] No code files found in command, treating as direct code execution');
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const adjustedData = adjustContentLength(toolInputData, api, ['source']);
|
|
275
|
-
const postText = JSON.stringify(adjustedData);
|
|
276
|
-
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for direct code execution, body length: ${postText.length}`);
|
|
277
|
-
return await sendToolInputRequest(postText, api, sessionId);
|
|
327
|
+
const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'exec', command, event.toolCallId, interActionID);
|
|
328
|
+
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for direct code execution, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
329
|
+
return await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
278
330
|
}
|
|
279
331
|
}
|
|
280
|
-
// 处理message工具的TOOL_INPUT
|
|
281
|
-
export async function handleMessageToolInput(event, api, sessionId) {
|
|
332
|
+
// 处理message工具的TOOL_INPUT数据采集,返回扫描结果(保留block/steer能力)
|
|
333
|
+
export async function handleMessageToolInput(event, api, sessionId, taskId) {
|
|
282
334
|
const message = extractInputParams(event, 'message');
|
|
283
335
|
if (!message) {
|
|
284
336
|
logger.log('[SENTINEL HOOK] No message found for message tool');
|
|
285
337
|
return null;
|
|
286
338
|
}
|
|
287
339
|
logger.log(`[SENTINEL HOOK] Processing message tool input, message length: ${message.length}`);
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const postText = JSON.stringify(adjustedData);
|
|
293
|
-
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for message, body length: ${postText.length}`);
|
|
294
|
-
return await sendToolInputRequest(postText, api, sessionId);
|
|
340
|
+
const interActionID = extractInterActionId(taskId);
|
|
341
|
+
const toolInputPayload = buildToolInputPayload(taskId, sessionId, 'message', message, event.toolCallId, interActionID);
|
|
342
|
+
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for message, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
343
|
+
return await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
295
344
|
}
|
|
296
|
-
// 处理其他工具(非 exec 和非 message)的 TOOL_INPUT
|
|
297
|
-
export async function handleOtherToolInput(event, api, sessionId) {
|
|
345
|
+
// 处理其他工具(非 exec 和非 message)的 TOOL_INPUT 数据采集,返回扫描结果(保留block/steer能力)
|
|
346
|
+
export async function handleOtherToolInput(event, api, sessionId, taskId) {
|
|
298
347
|
const params = event.params;
|
|
299
348
|
if (!params) {
|
|
300
349
|
logger.log('[SENTINEL HOOK] No params found for tool');
|
|
@@ -303,13 +352,8 @@ export async function handleOtherToolInput(event, api, sessionId) {
|
|
|
303
352
|
logger.log(`[SENTINEL HOOK] Processing other tool input, toolName: ${event.toolName}`);
|
|
304
353
|
// 将 params 序列化为 JSON 字符串
|
|
305
354
|
const paramsJson = JSON.stringify(params);
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// 对 source 字段进行长度截断处理
|
|
311
|
-
const adjustedData = adjustContentLength(toolInputData, api, ['content']);
|
|
312
|
-
const postText = JSON.stringify(adjustedData);
|
|
313
|
-
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for ${event.toolName}, body length: ${postText.length}`);
|
|
314
|
-
return await sendToolInputRequest(postText, api, sessionId);
|
|
355
|
+
const interActionID = extractInterActionId(taskId);
|
|
356
|
+
const toolInputPayload = buildToolInputPayload(taskId, sessionId, event.toolName, paramsJson, event.toolCallId, interActionID);
|
|
357
|
+
logger.log(`[SENTINEL HOOK] Sending TOOL_INPUT for ${event.toolName}, body length: ${JSON.stringify(toolInputPayload).length}`);
|
|
358
|
+
return await sendToolInputRequest(toolInputPayload, api, sessionId);
|
|
315
359
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LogReporterOptions } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Start the log reporter. Runs the first scan immediately, then on
|
|
3
|
+
* Start the log reporter. Runs the first scan immediately, then on a 5-minute interval.
|
|
4
4
|
* Returns a stop function.
|
|
5
5
|
*/
|
|
6
6
|
export declare function startLogReporter(options: LogReporterOptions): Promise<() => void>;
|
|
@@ -1,39 +1,182 @@
|
|
|
1
1
|
// Log Reporter Framework
|
|
2
2
|
// Self-contained periodic log scanner + uploader + reporter.
|
|
3
3
|
// Start via startLogReporter(), stop via stopLogReporter().
|
|
4
|
-
import {
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { resolveLogFiles } from "./path-resolver.js";
|
|
5
6
|
import { scanFile } from "./scanner.js";
|
|
6
|
-
import {
|
|
7
|
+
import { uploadContent } from "./uploader.js";
|
|
7
8
|
import { sendReport } from "./reporter.js";
|
|
8
9
|
import { loadCursorStore, saveCursorStore, setCursor } from "./cursor-store.js";
|
|
9
|
-
import {
|
|
10
|
+
import { parseAndFormatLogContent } from "./openclaw-parser.js";
|
|
11
|
+
import { logger } from "../utils/logger.js";
|
|
12
|
+
import crypto from "crypto";
|
|
13
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
14
|
+
const SCAN_INTERVAL_MS = 300000; // 5 minutes
|
|
15
|
+
const CURSOR_PATH = "/home/sandbox/.openclaw/.xiaoyilogging/.log-reporter-cursor.json";
|
|
16
|
+
const BAK_DIR = "/tmp/openclaw";
|
|
17
|
+
const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
18
|
+
/** Hardcoded log monitors */
|
|
19
|
+
const MONITORS = [
|
|
20
|
+
{
|
|
21
|
+
path: "/tmp/openclaw/openclaw-{year-month-day}.log",
|
|
22
|
+
businessType: "openclaw-gateway",
|
|
23
|
+
jsonParse: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
path: "/tmp/openclaw/xiaoyi-channel-{year}{month}{day}.log",
|
|
27
|
+
businessType: "xiaoyi-channel",
|
|
28
|
+
jsonParse: false,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
path: "/home/sandbox/.openclaw/workspace/logs/init_{year}{month}{day}_{hour}{minute}{second}.log",
|
|
32
|
+
businessType: "openclaw-init",
|
|
33
|
+
jsonParse: false,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
// ── State ────────────────────────────────────────────────────────────────────
|
|
10
37
|
let intervalId = null;
|
|
11
38
|
let isRunning = false;
|
|
39
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
40
|
/**
|
|
13
|
-
*
|
|
41
|
+
* Read the .xiaoyienv file and extract required environment variables.
|
|
42
|
+
* Expected format: key=value, one per line. Lines starting with # are comments.
|
|
43
|
+
*/
|
|
44
|
+
function readEnvFile() {
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = readFileSync(ENV_FILE_PATH, "utf-8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error(`Environment file not found: ${ENV_FILE_PATH}`);
|
|
51
|
+
}
|
|
52
|
+
const env = {};
|
|
53
|
+
for (const line of raw.split("\n")) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
56
|
+
continue;
|
|
57
|
+
const eqIdx = trimmed.indexOf("=");
|
|
58
|
+
if (eqIdx === -1)
|
|
59
|
+
continue;
|
|
60
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
61
|
+
const value = trimmed.substring(eqIdx + 1).trim();
|
|
62
|
+
env[key] = value;
|
|
63
|
+
}
|
|
64
|
+
const required = ["SERVICE_URL", "PERSONAL-API-KEY", "PERSONAL-UID"];
|
|
65
|
+
for (const key of required) {
|
|
66
|
+
if (!env[key]) {
|
|
67
|
+
throw new Error(`Missing required env variable: ${key}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
serviceUrl: env["SERVICE_URL"],
|
|
72
|
+
apiKey: env["PERSONAL-API-KEY"],
|
|
73
|
+
uid: env["PERSONAL-UID"],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Generate a stable instance ID (UUID) */
|
|
77
|
+
function generateInstanceId() {
|
|
78
|
+
return crypto.randomUUID();
|
|
79
|
+
}
|
|
80
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Start the log reporter. Runs the first scan immediately, then on a 5-minute interval.
|
|
14
83
|
* Returns a stop function.
|
|
15
84
|
*/
|
|
16
85
|
export async function startLogReporter(options) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
86
|
+
const env = readEnvFile();
|
|
87
|
+
const instanceId = generateInstanceId();
|
|
88
|
+
logger.log(`[log-reporter] Starting with interval ${SCAN_INTERVAL_MS}ms, ${MONITORS.length} monitor(s) configured`);
|
|
89
|
+
logger.log(`[log-reporter] Instance ID: ${instanceId}`);
|
|
20
90
|
async function doScan() {
|
|
21
91
|
if (isRunning)
|
|
22
92
|
return; // skip if previous scan still running
|
|
23
93
|
isRunning = true;
|
|
24
94
|
try {
|
|
25
|
-
const cursorStore = loadCursorStore(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
95
|
+
const cursorStore = loadCursorStore(CURSOR_PATH);
|
|
96
|
+
// Accumulated cursors to persist after a successful cycle
|
|
97
|
+
const pendingCursors = {};
|
|
98
|
+
// Phase 1: Scan all monitors, aggregate content by businessType
|
|
99
|
+
const contentMap = new Map();
|
|
100
|
+
const cursorMap = new Map();
|
|
101
|
+
for (const monitor of MONITORS) {
|
|
102
|
+
const resolvedFiles = resolveLogFiles(monitor.path);
|
|
103
|
+
logger.log(`[log-reporter] Scanning "${monitor.businessType}": pattern=${monitor.path}, resolved ${resolvedFiles.length} file(s)`);
|
|
104
|
+
const btCursors = {};
|
|
105
|
+
const btParts = [];
|
|
29
106
|
for (const filePath of resolvedFiles) {
|
|
30
|
-
|
|
107
|
+
try {
|
|
108
|
+
const result = await scanFile(filePath, cursorStore);
|
|
109
|
+
if (!result)
|
|
110
|
+
continue;
|
|
111
|
+
// Apply JSON parsing for openclaw gateway logs
|
|
112
|
+
const finalContent = monitor.jsonParse
|
|
113
|
+
? parseAndFormatLogContent(result.content)
|
|
114
|
+
: result.content;
|
|
115
|
+
if (!finalContent)
|
|
116
|
+
continue;
|
|
117
|
+
btParts.push(finalContent);
|
|
118
|
+
btCursors[filePath] = result.newCursor;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
logger.error(`[log-reporter] Error scanning "${filePath}": ${String(err)}`);
|
|
122
|
+
// Don't persist cursors for this business type on any error
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (btParts.length > 0) {
|
|
126
|
+
const existing = contentMap.get(monitor.businessType);
|
|
127
|
+
contentMap.set(monitor.businessType, existing ? existing + "\n" + btParts.join("\n") : btParts.join("\n"));
|
|
128
|
+
cursorMap.set(monitor.businessType, btCursors);
|
|
31
129
|
}
|
|
32
130
|
}
|
|
33
|
-
|
|
131
|
+
// Phase 2: Skip if no content at all
|
|
132
|
+
if (contentMap.size === 0) {
|
|
133
|
+
logger.log("[log-reporter] No new content across all monitors, skipping report");
|
|
134
|
+
saveCursorStore(CURSOR_PATH, cursorStore);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Phase 3: Upload each business type's content → get URL
|
|
138
|
+
const logFiles = [];
|
|
139
|
+
for (const [businessType, content] of contentMap) {
|
|
140
|
+
try {
|
|
141
|
+
const url = await uploadContent(content, businessType, BAK_DIR, options.uploadService);
|
|
142
|
+
logger.log(`[log-reporter] Uploaded content for "${businessType}", url: ${url}`);
|
|
143
|
+
logFiles.push({ businessType, fileUrl: url });
|
|
144
|
+
// Merge cursors for successful uploads
|
|
145
|
+
const btCursors = cursorMap.get(businessType);
|
|
146
|
+
if (btCursors) {
|
|
147
|
+
for (const [fp, cursor] of Object.entries(btCursors)) {
|
|
148
|
+
pendingCursors[fp] = cursor;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
logger.error(`[log-reporter] Upload failed for "${businessType}": ${String(err)}`);
|
|
154
|
+
// Don't persist cursors for this business type — will retry next cycle
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (logFiles.length === 0) {
|
|
158
|
+
logger.log("[log-reporter] All uploads failed, skipping report");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Phase 4: Send report
|
|
162
|
+
const payload = { instanceId, logFiles };
|
|
163
|
+
try {
|
|
164
|
+
await sendReport(payload, env);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
logger.error(`[log-reporter] Report failed: ${String(err)}`);
|
|
168
|
+
// Don't persist cursors on report failure — will retry next cycle
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Phase 5: Persist cursors after successful upload + report
|
|
172
|
+
for (const [fp, cursor] of Object.entries(pendingCursors)) {
|
|
173
|
+
setCursor(cursorStore, fp, cursor);
|
|
174
|
+
}
|
|
175
|
+
saveCursorStore(CURSOR_PATH, cursorStore);
|
|
34
176
|
}
|
|
35
177
|
catch (err) {
|
|
36
|
-
|
|
178
|
+
logger.error(`[log-reporter] Scan failed: ${String(err)}`);
|
|
179
|
+
// Cursor NOT updated — will retry on next cycle
|
|
37
180
|
}
|
|
38
181
|
finally {
|
|
39
182
|
isRunning = false;
|
|
@@ -42,29 +185,10 @@ export async function startLogReporter(options) {
|
|
|
42
185
|
// Run first scan immediately
|
|
43
186
|
await doScan();
|
|
44
187
|
// Schedule periodic scans
|
|
45
|
-
intervalId = setInterval(doScan,
|
|
188
|
+
intervalId = setInterval(doScan, SCAN_INTERVAL_MS);
|
|
46
189
|
intervalId.unref?.();
|
|
47
190
|
return () => stopLogReporter();
|
|
48
191
|
}
|
|
49
|
-
async function processFile(filePath, name, config, cursorStore, options) {
|
|
50
|
-
try {
|
|
51
|
-
const result = await scanFile(filePath, name, cursorStore);
|
|
52
|
-
if (!result)
|
|
53
|
-
return;
|
|
54
|
-
console.log(`[log-reporter] New content in "${name}": ${filePath} lines ${result.lineStart}-${result.lineEnd} (${result.newLineCount} lines)`);
|
|
55
|
-
// Upload .bak → get URL
|
|
56
|
-
const url = await uploadIncrementalContent(result, config.bakDir, options.uploadService);
|
|
57
|
-
console.log(`[log-reporter] Uploaded .bak for "${name}", url: ${url}`);
|
|
58
|
-
// Send report (mock)
|
|
59
|
-
await sendReport(config.reportUrl, url, result);
|
|
60
|
-
// Only persist cursor after successful upload + report
|
|
61
|
-
setCursor(cursorStore, filePath, result.newCursor);
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
console.error(`[log-reporter] Failed processing "${name}" (${filePath}):`, err);
|
|
65
|
-
// Cursor NOT updated — will retry on next scan
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
192
|
/**
|
|
69
193
|
* Stop the log reporter timer.
|
|
70
194
|
*/
|
|
@@ -72,6 +196,6 @@ export function stopLogReporter() {
|
|
|
72
196
|
if (intervalId !== null) {
|
|
73
197
|
clearInterval(intervalId);
|
|
74
198
|
intervalId = null;
|
|
75
|
-
|
|
199
|
+
logger.log("[log-reporter] Stopped");
|
|
76
200
|
}
|
|
77
201
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type ParsedLogLine = {
|
|
2
|
+
time?: string;
|
|
3
|
+
level?: string;
|
|
4
|
+
subsystem?: string;
|
|
5
|
+
module?: string;
|
|
6
|
+
message: string;
|
|
7
|
+
raw: string;
|
|
8
|
+
};
|
|
9
|
+
/** Parse a single raw JSON log line into structured fields. Returns null for non-JSON lines. */
|
|
10
|
+
export declare function parseLogLine(raw: string): ParsedLogLine | null;
|
|
11
|
+
/** Format a parsed log line as: "time level subsystem message" (matching openclaw logs output) */
|
|
12
|
+
export declare function formatParsedLogLine(parsed: ParsedLogLine): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse and format all lines in a raw log content block.
|
|
15
|
+
* JSON lines are parsed and formatted; non-JSON lines are passed through unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseAndFormatLogContent(rawContent: string): string;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// OpenClaw JSON log line parser
|
|
2
|
+
// Inlined from ~/code/openclaw/src/logging/parse-log-line.ts
|
|
3
|
+
// Parses tslog JSON log lines into human-readable text format matching "openclaw logs --follow" output
|
|
4
|
+
function trimLower(raw) {
|
|
5
|
+
if (typeof raw !== "string")
|
|
6
|
+
return undefined;
|
|
7
|
+
const trimmed = raw.trim();
|
|
8
|
+
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
9
|
+
}
|
|
10
|
+
function extractMessage(value) {
|
|
11
|
+
const parts = [];
|
|
12
|
+
for (const key of Object.keys(value)) {
|
|
13
|
+
if (!/^\d+$/.test(key))
|
|
14
|
+
continue;
|
|
15
|
+
const item = value[key];
|
|
16
|
+
if (typeof item === "string") {
|
|
17
|
+
parts.push(item);
|
|
18
|
+
}
|
|
19
|
+
else if (item != null) {
|
|
20
|
+
parts.push(JSON.stringify(item));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return parts.join(" ");
|
|
24
|
+
}
|
|
25
|
+
function parseMetaName(raw) {
|
|
26
|
+
if (typeof raw !== "string")
|
|
27
|
+
return {};
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
return {
|
|
31
|
+
subsystem: typeof parsed.subsystem === "string" ? parsed.subsystem : undefined,
|
|
32
|
+
module: typeof parsed.module === "string" ? parsed.module : undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Parse a single raw JSON log line into structured fields. Returns null for non-JSON lines. */
|
|
40
|
+
export function parseLogLine(raw) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
const meta = parsed["_meta"];
|
|
44
|
+
const nameMeta = parseMetaName(meta?.name);
|
|
45
|
+
const levelRaw = typeof meta?.logLevelName === "string" ? meta.logLevelName : undefined;
|
|
46
|
+
return {
|
|
47
|
+
time: typeof parsed.time === "string"
|
|
48
|
+
? parsed.time
|
|
49
|
+
: typeof meta?.date === "string"
|
|
50
|
+
? meta.date
|
|
51
|
+
: undefined,
|
|
52
|
+
level: trimLower(levelRaw),
|
|
53
|
+
subsystem: nameMeta.subsystem,
|
|
54
|
+
module: nameMeta.module,
|
|
55
|
+
message: extractMessage(parsed),
|
|
56
|
+
raw,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Format a parsed log line as: "time level subsystem message" (matching openclaw logs output) */
|
|
64
|
+
export function formatParsedLogLine(parsed) {
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (parsed.time)
|
|
67
|
+
parts.push(parsed.time);
|
|
68
|
+
if (parsed.level)
|
|
69
|
+
parts.push(parsed.level.toUpperCase());
|
|
70
|
+
if (parsed.subsystem)
|
|
71
|
+
parts.push(parsed.subsystem);
|
|
72
|
+
if (parsed.message)
|
|
73
|
+
parts.push(parsed.message);
|
|
74
|
+
return parts.join(" ");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse and format all lines in a raw log content block.
|
|
78
|
+
* JSON lines are parsed and formatted; non-JSON lines are passed through unchanged.
|
|
79
|
+
*/
|
|
80
|
+
export function parseAndFormatLogContent(rawContent) {
|
|
81
|
+
const lines = rawContent.split("\n");
|
|
82
|
+
const formatted = [];
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const parsed = parseLogLine(line);
|
|
85
|
+
if (parsed) {
|
|
86
|
+
formatted.push(formatParsedLogLine(parsed));
|
|
87
|
+
}
|
|
88
|
+
else if (line.length > 0) {
|
|
89
|
+
// Pass through non-JSON, non-empty lines as-is
|
|
90
|
+
formatted.push(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return formatted.join("\n");
|
|
94
|
+
}
|