@ynhcj/xiaoyi-channel 0.0.193-next → 0.0.194-next
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/index.d.ts +3 -8
- package/dist/index.js +5 -1
- package/dist/src/bot.js +16 -77
- package/dist/src/cspl/config.js +3 -0
- 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 +3 -4
- package/dist/src/log-reporter/reporter.js +28 -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 +3 -3
- package/dist/src/log-reporter/uploader.js +5 -5
- package/dist/src/monitor.js +0 -1
- package/dist/src/reply-dispatcher.js +4 -3
- package/dist/src/skill-retriever/config.js +2 -0
- package/dist/src/skill-retriever/hooks.js +1 -0
- package/dist/src/skill-retriever/tool-search.d.ts +3 -0
- package/dist/src/skill-retriever/tool-search.js +27 -11
- package/dist/src/skill-retriever/types.d.ts +1 -0
- package/dist/src/tools/device-tool-map.js +0 -6
- package/dist/src/tools/hmos-cli.d.ts +103 -0
- package/dist/src/tools/hmos-cli.js +561 -0
- package/dist/src/tools/invoke.d.ts +48 -0
- package/dist/src/tools/invoke.js +1201 -0
- package/dist/src/websocket.js +10 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
description: string;
|
|
5
|
-
configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
|
|
6
|
-
register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
|
|
7
|
-
} & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
|
|
8
|
-
export default _default;
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
+
declare const pluginEntry: ReturnType<typeof definePluginEntry>;
|
|
3
|
+
export default pluginEntry;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getAllPushIds } from "./src/utils/pushid-manager.js";
|
|
|
10
10
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
11
11
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
12
12
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
13
|
+
import { registerCLIHook } from "./src/tools/hmos-cli.js";
|
|
13
14
|
/**
|
|
14
15
|
* Register the cron detection hook.
|
|
15
16
|
*
|
|
@@ -181,7 +182,7 @@ function registerFullHooks(api) {
|
|
|
181
182
|
api.on("before_prompt_build", beforePromptBuildHandler);
|
|
182
183
|
registerSelfEvolutionToolResultNudge(api);
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
+
const pluginEntry = definePluginEntry({
|
|
185
186
|
id: "xiaoyi-channel",
|
|
186
187
|
name: "Xiaoyi Channel",
|
|
187
188
|
description: "Xiaoyi channel plugin - Xiaoyi A2A protocol integration",
|
|
@@ -208,6 +209,9 @@ export default definePluginEntry({
|
|
|
208
209
|
registerSentinelHook(api);
|
|
209
210
|
// Cron detection hook: marks toolCallIds from cron sessions
|
|
210
211
|
registerCronDetectionHook(api);
|
|
212
|
+
// CLI exec hook: intercepts built-in exec for HarmonyOS CLI skill tools
|
|
213
|
+
registerCLIHook(api);
|
|
211
214
|
}
|
|
212
215
|
},
|
|
213
216
|
});
|
|
217
|
+
export default pluginEntry;
|
package/dist/src/bot.js
CHANGED
|
@@ -7,6 +7,7 @@ import { downloadFilesFromParts } from "./file-download.js";
|
|
|
7
7
|
import { resolveXYConfig } from "./config.js";
|
|
8
8
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
9
9
|
import { appendSelfEvolutionKeywordNudge, shouldNudgeForSelfEvolutionKeyword, } from "./self-evolution-keyword.js";
|
|
10
|
+
import { queueAgentHarnessMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
10
11
|
import { runWithSessionContext } from "./tools/session-manager.js";
|
|
11
12
|
import { configManager } from "./utils/config-manager.js";
|
|
12
13
|
import { addPushId } from "./utils/pushid-manager.js";
|
|
@@ -552,7 +553,7 @@ function enqueueSteer(params) {
|
|
|
552
553
|
return next;
|
|
553
554
|
}
|
|
554
555
|
async function dispatchSteerWhenReady(params) {
|
|
555
|
-
const { sessionId,
|
|
556
|
+
const { sessionId, steerText } = params;
|
|
556
557
|
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
557
558
|
// 1. 等待第一条消息开始 streaming
|
|
558
559
|
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
@@ -578,7 +579,7 @@ async function dispatchSteerWhenReady(params) {
|
|
|
578
579
|
}
|
|
579
580
|
else {
|
|
580
581
|
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
581
|
-
// 没有创建 signal
|
|
582
|
+
// 没有创建 signal。此时放弃,避免并发碰撞。
|
|
582
583
|
log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
|
|
583
584
|
return;
|
|
584
585
|
}
|
|
@@ -587,84 +588,22 @@ async function dispatchSteerWhenReady(params) {
|
|
|
587
588
|
log.log(`[STEER-QUEUE] First message completed, skip steer`);
|
|
588
589
|
return;
|
|
589
590
|
}
|
|
590
|
-
// 3.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// 如果有文件附件,把路径拼到 steer 文本末尾,让模型通过工具读取
|
|
591
|
+
// 3. 直接注入到活跃的 Pi run 中,不创建独立 dispatcher。
|
|
592
|
+
// 模型回复通过第一条消息的 dispatcher 的 onPartialReply 流式发出,
|
|
593
|
+
// 使用 registerTaskId + updateFallbackTaskId 已同步的最新 taskId。
|
|
594
594
|
const mediaPaths = params.mediaPayload?.MediaPaths;
|
|
595
595
|
const fileHint = mediaPaths && mediaPaths.length > 0
|
|
596
596
|
? `\n【用户上传附件】:${JSON.stringify(mediaPaths)}`
|
|
597
597
|
: "";
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
channel: "xiaoyi-channel",
|
|
603
|
-
from: speaker,
|
|
604
|
-
timestamp: new Date(),
|
|
605
|
-
envelope: envelopeOptions,
|
|
606
|
-
body: messageBody,
|
|
607
|
-
});
|
|
608
|
-
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
609
|
-
Body: body,
|
|
610
|
-
RawBody: steerCommand,
|
|
611
|
-
CommandBody: steerCommand,
|
|
612
|
-
From: sessionId,
|
|
613
|
-
To: sessionId,
|
|
614
|
-
SessionKey: params.route.sessionKey,
|
|
615
|
-
AccountId: params.route.accountId,
|
|
616
|
-
ChatType: "direct",
|
|
617
|
-
GroupSubject: undefined,
|
|
618
|
-
SenderName: sessionId,
|
|
619
|
-
SenderId: sessionId,
|
|
620
|
-
Provider: "xiaoyi-channel",
|
|
621
|
-
Surface: "xiaoyi-channel",
|
|
622
|
-
MessageSid: `xiaoyi_${params.parsed.taskId}_${params.deviceType}`,
|
|
623
|
-
Timestamp: Date.now(),
|
|
624
|
-
WasMentioned: false,
|
|
625
|
-
CommandAuthorized: true,
|
|
626
|
-
OriginatingChannel: "xiaoyi-channel",
|
|
627
|
-
OriginatingTo: sessionId,
|
|
628
|
-
ReplyToBody: undefined,
|
|
629
|
-
...params.mediaPayload,
|
|
630
|
-
});
|
|
631
|
-
const steerState = { steered: true };
|
|
632
|
-
const { dispatcher, replyOptions } = createXYReplyDispatcher({
|
|
633
|
-
cfg: params.cfg,
|
|
634
|
-
runtime: params.runtime,
|
|
635
|
-
sessionId,
|
|
636
|
-
taskId: params.parsed.taskId,
|
|
637
|
-
messageId: params.parsed.messageId,
|
|
638
|
-
accountId: params.route.accountId,
|
|
639
|
-
steerState,
|
|
640
|
-
});
|
|
641
|
-
const sessionContext = {
|
|
642
|
-
config: resolveXYConfig(params.cfg),
|
|
643
|
-
sessionId,
|
|
644
|
-
taskId: params.parsed.taskId,
|
|
645
|
-
messageId: params.parsed.messageId,
|
|
646
|
-
agentId: params.route.accountId,
|
|
647
|
-
deviceType: params.deviceType,
|
|
648
|
-
};
|
|
649
|
-
log.log(`[STEER-QUEUE] Dispatching steer`);
|
|
650
|
-
await core.channel.reply.withReplyDispatcher({
|
|
651
|
-
dispatcher,
|
|
652
|
-
onSettled: () => {
|
|
653
|
-
log.log(`[STEER-QUEUE] Steer dispatch settled`);
|
|
654
|
-
},
|
|
655
|
-
run: () => {
|
|
656
|
-
return runWithSessionContext(sessionContext, async () => {
|
|
657
|
-
log.log(`[ALS-PROOF] bot entered steer dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=true`);
|
|
658
|
-
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
659
|
-
ctx: ctxPayload,
|
|
660
|
-
cfg: params.cfg,
|
|
661
|
-
dispatcher,
|
|
662
|
-
replyOptions,
|
|
663
|
-
});
|
|
664
|
-
log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
665
|
-
return result;
|
|
666
|
-
});
|
|
667
|
-
},
|
|
598
|
+
const steerMessage = `${steerText}${fileHint}`;
|
|
599
|
+
log.log(`[STEER-QUEUE] Injecting steer message directly into active run`);
|
|
600
|
+
const injected = queueAgentHarnessMessage(sessionId, steerMessage, {
|
|
601
|
+
steeringMode: "all",
|
|
668
602
|
});
|
|
669
|
-
|
|
603
|
+
if (injected) {
|
|
604
|
+
log.log(`[STEER-QUEUE] Steer message injected successfully`);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
log.log(`[STEER-QUEUE] Steer injection failed — run may not be accepting messages`);
|
|
608
|
+
}
|
|
670
609
|
}
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
6
9
|
import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
|
|
7
10
|
import { logger } from '../utils/logger.js';
|
|
8
11
|
let cachedConfig = null;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Path resolver: resolves file path templates with date wildcards to actual files on disk
|
|
2
|
+
import { readdirSync } from "fs";
|
|
3
|
+
import { dirname, basename, join } from "path";
|
|
4
|
+
// Replace longer tokens first to avoid partial matches
|
|
5
|
+
const WILDCARD_TOKENS = [
|
|
6
|
+
["{year-month-day}", "\\d{4}-\\d{2}-\\d{2}"],
|
|
7
|
+
["{year}{month}{day}", "\\d{8}"],
|
|
8
|
+
["{hour}{minute}{second}", "\\d{6}"],
|
|
9
|
+
["{year}", "\\d{4}"],
|
|
10
|
+
["{month}", "\\d{2}"],
|
|
11
|
+
["{day}", "\\d{2}"],
|
|
12
|
+
["{hour}", "\\d{2}"],
|
|
13
|
+
["{minute}", "\\d{2}"],
|
|
14
|
+
["{second}", "\\d{2}"],
|
|
15
|
+
];
|
|
16
|
+
function escapeRegex(s) {
|
|
17
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Convert a path with date wildcards into a RegExp that matches the filename part only.
|
|
21
|
+
* Returns { dir, regex } where dir is the directory portion and regex matches filenames.
|
|
22
|
+
*/
|
|
23
|
+
function pathToPattern(templatePath) {
|
|
24
|
+
const dir = dirname(templatePath);
|
|
25
|
+
let pattern = basename(templatePath);
|
|
26
|
+
// Escape regex special chars in the literal parts, then replace tokens
|
|
27
|
+
pattern = escapeRegex(pattern);
|
|
28
|
+
for (const [token, replacement] of WILDCARD_TOKENS) {
|
|
29
|
+
pattern = pattern.replaceAll(escapeRegex(token), replacement);
|
|
30
|
+
}
|
|
31
|
+
return { dir, regex: new RegExp(`^${pattern}$`) };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a path template with date wildcards to actual file paths on disk.
|
|
35
|
+
* Scans the directory and returns all files matching the pattern.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveLogFiles(templatePath) {
|
|
38
|
+
const { dir, regex } = pathToPattern(templatePath);
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = readdirSync(dir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
return entries
|
|
47
|
+
.filter((f) => regex.test(f))
|
|
48
|
+
.map((f) => join(dir, f))
|
|
49
|
+
.sort(); // chronological order for date-named logs
|
|
50
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReportPayload, LogReporterEnv } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Send a log report to the
|
|
4
|
-
* MOCK implementation — real request logic will be added later.
|
|
3
|
+
* Send a log report to the sync API.
|
|
5
4
|
*/
|
|
6
|
-
export declare function sendReport(
|
|
5
|
+
export declare function sendReport(payload: ReportPayload, env: LogReporterEnv): Promise<void>;
|
|
@@ -1,17 +1,32 @@
|
|
|
1
|
+
// Reporter: sends log report to the sync API
|
|
2
|
+
import fetch from "node-fetch";
|
|
3
|
+
import { calculateSHA256String } from "../utils/crypto.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
1
5
|
/**
|
|
2
|
-
* Send a log report to the
|
|
3
|
-
* MOCK implementation — real request logic will be added later.
|
|
6
|
+
* Send a log report to the sync API.
|
|
4
7
|
*/
|
|
5
|
-
export async function sendReport(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
export async function sendReport(payload, env) {
|
|
9
|
+
if (payload.logFiles.length === 0) {
|
|
10
|
+
return; // nothing to report
|
|
11
|
+
}
|
|
12
|
+
const url = `${env.serviceUrl}/fulfillment/v1/claw/log-file/sync`;
|
|
13
|
+
const traceId = `${calculateSHA256String(env.uid).substring(0, 32)}_${Date.now()}`;
|
|
14
|
+
const headers = {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"x-api-key": env.apiKey,
|
|
17
|
+
"x-uid": env.uid,
|
|
18
|
+
"x-hag-trace-id": traceId,
|
|
19
|
+
"x-request-from": "openclaw",
|
|
15
20
|
};
|
|
16
|
-
|
|
21
|
+
logger.log(`[log-reporter] Sending report to ${url}, ${payload.logFiles.length} log file(s)`);
|
|
22
|
+
const resp = await fetch(url, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers,
|
|
25
|
+
body: JSON.stringify(payload),
|
|
26
|
+
});
|
|
27
|
+
if (!resp.ok) {
|
|
28
|
+
const body = await resp.text().catch(() => "");
|
|
29
|
+
throw new Error(`Report API returned HTTP ${resp.status}: ${body}`);
|
|
30
|
+
}
|
|
31
|
+
logger.log(`[log-reporter] Report sent successfully, status: ${resp.status}`);
|
|
17
32
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FileCursor, CursorStore } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Scan a single log file for new content.
|
|
4
|
-
* Returns
|
|
4
|
+
* Returns { filePath, content, newCursor } if there are new lines, or null if no changes.
|
|
5
5
|
*/
|
|
6
|
-
export declare function scanFile(filePath: string,
|
|
6
|
+
export declare function scanFile(filePath: string, cursorStore: CursorStore): Promise<{
|
|
7
|
+
filePath: string;
|
|
8
|
+
content: string;
|
|
9
|
+
newCursor: FileCursor;
|
|
10
|
+
} | null>;
|