@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.
@@ -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,6 @@
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 with retry on failure.
4
+ * Retries up to 3 times with delays of 10s, 20s, 30s.
5
5
  */
6
- export declare function sendReport(reportUrl: string, fileUrl: string, result: ScanResult): Promise<void>;
6
+ export declare function sendReport(payload: ReportPayload, env: LogReporterEnv): Promise<void>;
@@ -1,17 +1,56 @@
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";
5
+ /** Retry delays in milliseconds: 10s, 20s, 30s */
6
+ const RETRY_DELAYS_MS = [10000, 20000, 30000];
7
+ const MAX_RETRIES = RETRY_DELAYS_MS.length;
8
+ /** Sleep for a given duration in milliseconds */
9
+ function sleep(ms) {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
1
12
  /**
2
- * Send a log report to the server with the uploaded file URL.
3
- * MOCK implementation real request logic will be added later.
13
+ * Send a log report to the sync API with retry on failure.
14
+ * Retries up to 3 times with delays of 10s, 20s, 30s.
4
15
  */
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(),
16
+ export async function sendReport(payload, env) {
17
+ if (payload.logFiles.length === 0) {
18
+ return; // nothing to report
19
+ }
20
+ const url = `${env.serviceUrl}/fulfillment/v1/claw/log-file/sync`;
21
+ const traceId = `${calculateSHA256String(env.uid).substring(0, 32)}_${Date.now()}`;
22
+ const headers = {
23
+ "Content-Type": "application/json",
24
+ "x-api-key": env.apiKey,
25
+ "x-uid": env.uid,
26
+ "x-hag-trace-id": traceId,
27
+ "x-request-from": "openclaw",
15
28
  };
16
- console.log(`[log-reporter] Mock report to ${reportUrl}:`, JSON.stringify(payload, null, 2));
29
+ logger.log(`[log-reporter] Sending report to ${url}, ${payload.logFiles.length} log file(s)`);
30
+ let lastError;
31
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
32
+ try {
33
+ const resp = await fetch(url, {
34
+ method: "POST",
35
+ headers,
36
+ body: JSON.stringify(payload),
37
+ });
38
+ if (!resp.ok) {
39
+ const body = await resp.text().catch(() => "");
40
+ throw new Error(`Report API returned HTTP ${resp.status}: ${body}`);
41
+ }
42
+ logger.log(`[log-reporter] Report sent successfully, status: ${resp.status}`);
43
+ return; // success, exit
44
+ }
45
+ catch (err) {
46
+ lastError = err instanceof Error ? err : new Error(String(err));
47
+ if (attempt < MAX_RETRIES) {
48
+ const delay = RETRY_DELAYS_MS[attempt];
49
+ logger.warn(`[log-reporter] Report failed (attempt ${attempt + 1}/${MAX_RETRIES + 1}): ${lastError.message}. Retrying in ${delay / 1000}s...`);
50
+ await sleep(delay);
51
+ }
52
+ }
53
+ }
54
+ // All retries exhausted
55
+ throw lastError;
17
56
  }
@@ -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>;
@@ -1,4 +1,4 @@
1
- // Core scanner: resolves wildcard paths, reads incremental log content using byte-offset cursors
1
+ // Core scanner: reads incremental log content using byte-offset cursors
2
2
  import { statSync, createReadStream } from "fs";
3
3
  import { getCursor } from "./cursor-store.js";
4
4
  /**
@@ -39,9 +39,9 @@ function resolveStartByte(currentSize, currentMtimeMs, cursor) {
39
39
  }
40
40
  /**
41
41
  * Scan a single log file for new content.
42
- * Returns a ScanResult if there are new lines, or null if no changes.
42
+ * Returns { filePath, content, newCursor } if there are new lines, or null if no changes.
43
43
  */
44
- export async function scanFile(filePath, name, cursorStore) {
44
+ export async function scanFile(filePath, cursorStore) {
45
45
  let stats;
46
46
  try {
47
47
  stats = statSync(filePath);
@@ -72,11 +72,7 @@ export async function scanFile(filePath, name, cursorStore) {
72
72
  };
73
73
  return {
74
74
  filePath,
75
- name,
76
75
  content,
77
- lineStart: prevLine + 1,
78
- lineEnd: prevLine + newLineCount,
79
- newLineCount,
80
76
  newCursor,
81
77
  };
82
78
  }
@@ -1,20 +1,11 @@
1
- /** Configuration for a single log file to monitor */
2
- export interface LogFileConfig {
3
- /** File path with optional date wildcards: {year}, {month}, {day}, {year-month-day}, {year}{month}{day} */
1
+ /** Configuration for a single log monitor (hardcoded) */
2
+ export interface LogMonitorConfig {
3
+ /** File path with date wildcards: {year}, {month}, {day}, {year-month-day}, {year}{month}{day}, {hour}, {minute}, {second} */
4
4
  path: string;
5
- /** Logical name used in .bak filename and reporting */
6
- name: string;
7
- }
8
- /** Top-level log reporter configuration (loaded from JSON file) */
9
- export interface LogReporterConfig {
10
- /** Scan interval in milliseconds (default: 600000 = 10 min) */
11
- scanIntervalMs: number;
12
- /** Directory for .bak files and cursor state */
13
- bakDir: string;
14
- /** Report server URL (mock for now) */
15
- reportUrl: string;
16
- /** Log files to monitor */
17
- logFiles: LogFileConfig[];
5
+ /** Business type for reporting */
6
+ businessType: string;
7
+ /** Whether to parse JSON log lines (openclaw gateway format) */
8
+ jsonParse: boolean;
18
9
  }
19
10
  /** Cursor state for a single log file */
20
11
  export interface FileCursor {
@@ -33,23 +24,15 @@ export interface CursorStore {
33
24
  export interface ScanResult {
34
25
  /** Resolved absolute file path */
35
26
  filePath: string;
36
- /** Logical name from config */
37
- name: string;
38
- /** The new log lines (incremental content) */
27
+ /** Business type from monitor config */
28
+ businessType: string;
29
+ /** The new log lines (incremental content, may be parsed/formatted) */
39
30
  content: string;
40
- /** Start line number (1-based, inclusive) */
41
- lineStart: number;
42
- /** End line number (1-based, inclusive) */
43
- lineEnd: number;
44
- /** Number of new lines */
45
- newLineCount: number;
46
31
  /** Updated cursor to persist after successful upload */
47
32
  newCursor: FileCursor;
48
33
  }
49
34
  /** Options passed to startLogReporter */
50
35
  export interface LogReporterOptions {
51
- /** Absolute path to the JSON config file */
52
- configPath: string;
53
36
  /** File upload service instance (from xy_channel) */
54
37
  uploadService: UploadService;
55
38
  }
@@ -57,3 +40,19 @@ export interface LogReporterOptions {
57
40
  export interface UploadService {
58
41
  uploadFileAndGetUrl(filePath: string, objectType?: string): Promise<string>;
59
42
  }
43
+ /** A single log file entry in the report payload */
44
+ export interface ReportLogFileEntry {
45
+ businessType: string;
46
+ fileUrl: string;
47
+ }
48
+ /** Report payload sent to the sync API */
49
+ export interface ReportPayload {
50
+ instanceId: string;
51
+ logFiles: ReportLogFileEntry[];
52
+ }
53
+ /** Environment config read from .xiaoyienv */
54
+ export interface LogReporterEnv {
55
+ serviceUrl: string;
56
+ apiKey: string;
57
+ uid: string;
58
+ }
@@ -1,6 +1,7 @@
1
- import type { UploadService, ScanResult } from "./types.js";
1
+ import type { UploadService } from "./types.js";
2
2
  /**
3
- * Write incremental content to .bak file, upload, and return the URL.
3
+ * Write content to a .bak file, upload, and return the URL.
4
+ * Retries upload up to 3 times with delays of 10s, 20s, 30s.
4
5
  * Cleans up .bak file regardless of success/failure.
5
6
  */
6
- export declare function uploadIncrementalContent(result: ScanResult, bakDir: string, uploadService: UploadService): Promise<string>;
7
+ export declare function uploadContent(content: string, name: string, bakDir: string, uploadService: UploadService): Promise<string>;
@@ -1,26 +1,49 @@
1
- // Uploader: creates .bak file, uploads via XYFileUploadService, cleans up
1
+ // Uploader: creates .bak file, uploads via UploadService, cleans up
2
2
  import { writeFileSync, unlinkSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { mkdirSync } from "fs";
5
+ import { logger } from "../utils/logger.js";
6
+ /** Retry delays in milliseconds: 10s, 20s, 30s */
7
+ const RETRY_DELAYS_MS = [10000, 20000, 30000];
8
+ const MAX_RETRIES = RETRY_DELAYS_MS.length;
9
+ /** Sleep for a given duration in milliseconds */
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
5
13
  /**
6
- * Write incremental content to .bak file, upload, and return the URL.
14
+ * Write content to a .bak file, upload, and return the URL.
15
+ * Retries upload up to 3 times with delays of 10s, 20s, 30s.
7
16
  * Cleans up .bak file regardless of success/failure.
8
17
  */
9
- export async function uploadIncrementalContent(result, bakDir, uploadService) {
18
+ export async function uploadContent(content, name, bakDir, uploadService) {
10
19
  const timestamp = Date.now();
11
- const bakFileName = `${result.name}_${timestamp}.bak`;
20
+ const bakFileName = `${name}_${timestamp}.bak`;
12
21
  const bakPath = join(bakDir, bakFileName);
13
22
  // Ensure bakDir exists
14
23
  try {
15
24
  mkdirSync(bakDir, { recursive: true });
16
25
  }
17
26
  catch { }
27
+ // Write incremental content to .bak file (only once)
28
+ writeFileSync(bakPath, content, "utf-8");
29
+ let lastError;
18
30
  try {
19
- // Write incremental content to .bak file
20
- writeFileSync(bakPath, result.content, "utf-8");
21
- // Upload and get URL
22
- const url = await uploadService.uploadFileAndGetUrl(bakPath);
23
- return url;
31
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
32
+ try {
33
+ const url = await uploadService.uploadFileAndGetUrl(bakPath);
34
+ return url; // success
35
+ }
36
+ catch (err) {
37
+ lastError = err instanceof Error ? err : new Error(String(err));
38
+ if (attempt < MAX_RETRIES) {
39
+ const delay = RETRY_DELAYS_MS[attempt];
40
+ logger.warn(`[log-reporter] Upload failed for "${name}" (attempt ${attempt + 1}/${MAX_RETRIES + 1}): ${lastError.message}. Retrying in ${delay / 1000}s...`);
41
+ await sleep(delay);
42
+ }
43
+ }
44
+ }
45
+ // All retries exhausted
46
+ throw lastError;
24
47
  }
25
48
  finally {
26
49
  // Always clean up the .bak file
@@ -326,7 +326,6 @@ export async function monitorXYProvider(opts = {}) {
326
326
  logger.log("XY gateway: started successfully");
327
327
  // Start log reporter (independent periodic scanner + uploader)
328
328
  startLogReporter({
329
- configPath: "/home/ynhcj/.openclaw/log-reporter-config.json",
330
329
  uploadService: new XYFileUploadService(account.fileUploadUrl, account.apiKey, account.uid),
331
330
  }).then((stop) => {
332
331
  stopLogReporter = stop;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.209-beta",
3
+ "version": "0.0.210-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",