@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 CHANGED
@@ -1,8 +1,3 @@
1
- declare const _default: {
2
- id: string;
3
- name: string;
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
- export default definePluginEntry({
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, sessionKey, steerText } = params;
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。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
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. 构建 dispatch 上下文并 dispatch /steer
591
- const core = getXYRuntime();
592
- const speaker = sessionId;
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 steerCommand = `/steer ${steerText}${fileHint}`;
599
- const messageBody = `${speaker}: ${steerCommand}`;
600
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
601
- const body = core.channel.reply.formatAgentEnvelope({
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
- log.log(`[STEER-QUEUE] Steer dispatch completed`);
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
  }
@@ -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 the configured interval.
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 { resolveLogFiles, loadConfig } from "./config-loader.js";
4
+ import { readFileSync } from "fs";
5
+ import { resolveLogFiles } from "./path-resolver.js";
5
6
  import { scanFile } from "./scanner.js";
6
- import { uploadIncrementalContent } from "./uploader.js";
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 { join } from "path";
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
- * Start the log reporter. Runs the first scan immediately, then on the configured interval.
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 config = loadConfig(options.configPath);
18
- const cursorPath = join(config.bakDir, ".log-reporter-cursor.json");
19
- console.log(`[log-reporter] Starting with interval ${config.scanIntervalMs}ms, ${config.logFiles.length} log file(s) configured`);
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(cursorPath);
26
- for (const logFile of config.logFiles) {
27
- const resolvedFiles = resolveLogFiles(logFile.path);
28
- console.log(`[log-reporter] Scanning "${logFile.name}": pattern=${logFile.path}, resolved ${resolvedFiles.length} file(s)`);
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
- await processFile(filePath, logFile.name, config, cursorStore, options);
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
- saveCursorStore(cursorPath, cursorStore);
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
- console.error("[log-reporter] Scan failed:", err);
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, config.scanIntervalMs);
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
- console.log("[log-reporter] Stopped");
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,5 @@
1
+ /**
2
+ * Resolve a path template with date wildcards to actual file paths on disk.
3
+ * Scans the directory and returns all files matching the pattern.
4
+ */
5
+ export declare function resolveLogFiles(templatePath: string): string[];
@@ -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 { ScanResult } from "./types.js";
1
+ import type { ReportPayload, LogReporterEnv } from "./types.js";
2
2
  /**
3
- * Send a log report to the server with the uploaded file URL.
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(reportUrl: string, fileUrl: string, result: ScanResult): Promise<void>;
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 server with the uploaded file URL.
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(reportUrl, fileUrl, result) {
6
- // TODO: Replace with actual HTTP request
7
- const payload = {
8
- logName: result.name,
9
- filePath: result.filePath,
10
- lineStart: result.lineStart,
11
- lineEnd: result.lineEnd,
12
- newLineCount: result.newLineCount,
13
- fileUrl,
14
- timestamp: new Date().toISOString(),
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
- console.log(`[log-reporter] Mock report to ${reportUrl}:`, JSON.stringify(payload, null, 2));
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 { ScanResult, CursorStore } from "./types.js";
1
+ import type { FileCursor, CursorStore } from "./types.js";
2
2
  /**
3
3
  * Scan a single log file for new content.
4
- * Returns a ScanResult if there are new lines, or null if no changes.
4
+ * Returns { filePath, content, newCursor } if there are new lines, or null if no changes.
5
5
  */
6
- export declare function scanFile(filePath: string, name: string, cursorStore: CursorStore): Promise<ScanResult | null>;
6
+ export declare function scanFile(filePath: string, cursorStore: CursorStore): Promise<{
7
+ filePath: string;
8
+ content: string;
9
+ newCursor: FileCursor;
10
+ } | null>;