@ynhcj/xiaoyi-channel 0.0.196-beta → 0.0.198-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/index.js +1 -141
- package/dist/src/acp-session-binding.d.ts +37 -0
- package/dist/src/acp-session-binding.js +237 -0
- package/dist/src/bot.js +1 -12
- package/dist/src/cron-command.d.ts +0 -2
- package/dist/src/cron-command.js +8 -14
- package/dist/src/cron-query-handler.js +0 -36
- package/dist/src/formatter.js +5 -51
- package/dist/src/log-reporter/config-loader.d.ts +11 -0
- package/dist/src/log-reporter/config-loader.js +68 -0
- package/dist/src/log-reporter/cursor-store.d.ts +5 -0
- package/dist/src/log-reporter/cursor-store.js +26 -0
- package/dist/src/log-reporter/index.d.ts +10 -0
- package/dist/src/log-reporter/index.js +77 -0
- package/dist/src/log-reporter/reporter.d.ts +6 -0
- package/dist/src/log-reporter/reporter.js +17 -0
- package/dist/src/log-reporter/scanner.d.ts +6 -0
- package/dist/src/log-reporter/scanner.js +82 -0
- package/dist/src/log-reporter/types.d.ts +59 -0
- package/dist/src/log-reporter/types.js +2 -0
- package/dist/src/log-reporter/uploader.d.ts +6 -0
- package/dist/src/log-reporter/uploader.js +32 -0
- package/dist/src/memory-query-handler.js +1 -1
- package/dist/src/parser.d.ts +0 -12
- package/dist/src/parser.js +13 -39
- package/dist/src/provider.js +3 -30
- package/dist/src/reply-dispatcher.js +11 -26
- package/dist/src/tools/create-all-tools.js +5 -10
- package/dist/src/tools/send-cross-device-task-tool.js +15 -84
- package/dist/src/tools/send-file-to-user-tool.js +11 -9
- package/dist/src/tools/session-manager.d.ts +2 -12
- package/dist/src/tools/session-manager.js +17 -56
- package/dist/src/types.d.ts +6 -6
- package/dist/src/utils/config-manager.d.ts +2 -3
- package/dist/src/utils/config-manager.js +2 -22
- package/dist/src/websocket.js +13 -12
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Config loader: reads JSON config file, validates fields, resolves date wildcards to actual files
|
|
2
|
+
import { readFileSync, 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
|
+
["{year}", "\\d{4}"],
|
|
9
|
+
["{month}", "\\d{2}"],
|
|
10
|
+
["{day}", "\\d{2}"],
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Load and validate the JSON config file.
|
|
14
|
+
* Falls back to defaults for optional fields.
|
|
15
|
+
*/
|
|
16
|
+
export function loadConfig(configPath) {
|
|
17
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (!parsed.logFiles || !Array.isArray(parsed.logFiles) || parsed.logFiles.length === 0) {
|
|
20
|
+
throw new Error("log-reporter config: 'logFiles' must be a non-empty array");
|
|
21
|
+
}
|
|
22
|
+
for (const lf of parsed.logFiles) {
|
|
23
|
+
if (!lf.path || !lf.name) {
|
|
24
|
+
throw new Error(`log-reporter config: each logFile must have 'path' and 'name', got ${JSON.stringify(lf)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
scanIntervalMs: parsed.scanIntervalMs ?? 600000,
|
|
29
|
+
bakDir: parsed.bakDir ?? "/tmp/openclaw",
|
|
30
|
+
reportUrl: parsed.reportUrl ?? "",
|
|
31
|
+
logFiles: parsed.logFiles,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convert a path with date wildcards into a RegExp that matches the filename part only.
|
|
36
|
+
* Returns { dir, regex } where dir is the directory portion and regex matches filenames.
|
|
37
|
+
*/
|
|
38
|
+
function pathToPattern(templatePath) {
|
|
39
|
+
const dir = dirname(templatePath);
|
|
40
|
+
let pattern = basename(templatePath);
|
|
41
|
+
// Escape regex special chars in the literal parts, then replace tokens
|
|
42
|
+
pattern = escapeRegex(pattern);
|
|
43
|
+
for (const [token, replacement] of WILDCARD_TOKENS) {
|
|
44
|
+
pattern = pattern.replaceAll(escapeRegex(token), replacement);
|
|
45
|
+
}
|
|
46
|
+
return { dir, regex: new RegExp(`^${pattern}$`) };
|
|
47
|
+
}
|
|
48
|
+
function escapeRegex(s) {
|
|
49
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a path template with date wildcards to actual file paths on disk.
|
|
53
|
+
* Scans the directory and returns all files matching the pattern.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveLogFiles(templatePath) {
|
|
56
|
+
const { dir, regex } = pathToPattern(templatePath);
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = readdirSync(dir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return entries
|
|
65
|
+
.filter((f) => regex.test(f))
|
|
66
|
+
.map((f) => join(dir, f))
|
|
67
|
+
.sort(); // chronological order for date-named logs
|
|
68
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CursorStore, FileCursor } from "./types.js";
|
|
2
|
+
export declare function loadCursorStore(storePath: string): CursorStore;
|
|
3
|
+
export declare function saveCursorStore(storePath: string, store: CursorStore): void;
|
|
4
|
+
export declare function getCursor(store: CursorStore, filePath: string): FileCursor | undefined;
|
|
5
|
+
export declare function setCursor(store: CursorStore, filePath: string, cursor: FileCursor): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Cursor state persistence — reads/writes FileCursor entries keyed by file path
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
export function loadCursorStore(storePath) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = readFileSync(storePath, "utf-8");
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
return { files: parsed.files ?? {} };
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { files: {} };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function saveCursorStore(storePath, store) {
|
|
15
|
+
try {
|
|
16
|
+
mkdirSync(dirname(storePath), { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
|
|
20
|
+
}
|
|
21
|
+
export function getCursor(store, filePath) {
|
|
22
|
+
return store.files[filePath];
|
|
23
|
+
}
|
|
24
|
+
export function setCursor(store, filePath, cursor) {
|
|
25
|
+
store.files[filePath] = cursor;
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LogReporterOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Start the log reporter. Runs the first scan immediately, then on the configured interval.
|
|
4
|
+
* Returns a stop function.
|
|
5
|
+
*/
|
|
6
|
+
export declare function startLogReporter(options: LogReporterOptions): Promise<() => void>;
|
|
7
|
+
/**
|
|
8
|
+
* Stop the log reporter timer.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stopLogReporter(): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Log Reporter Framework
|
|
2
|
+
// Self-contained periodic log scanner + uploader + reporter.
|
|
3
|
+
// Start via startLogReporter(), stop via stopLogReporter().
|
|
4
|
+
import { resolveLogFiles, loadConfig } from "./config-loader.js";
|
|
5
|
+
import { scanFile } from "./scanner.js";
|
|
6
|
+
import { uploadIncrementalContent } from "./uploader.js";
|
|
7
|
+
import { sendReport } from "./reporter.js";
|
|
8
|
+
import { loadCursorStore, saveCursorStore, setCursor } from "./cursor-store.js";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
let intervalId = null;
|
|
11
|
+
let isRunning = false;
|
|
12
|
+
/**
|
|
13
|
+
* Start the log reporter. Runs the first scan immediately, then on the configured interval.
|
|
14
|
+
* Returns a stop function.
|
|
15
|
+
*/
|
|
16
|
+
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`);
|
|
20
|
+
async function doScan() {
|
|
21
|
+
if (isRunning)
|
|
22
|
+
return; // skip if previous scan still running
|
|
23
|
+
isRunning = true;
|
|
24
|
+
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)`);
|
|
29
|
+
for (const filePath of resolvedFiles) {
|
|
30
|
+
await processFile(filePath, logFile.name, config, cursorStore, options);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
saveCursorStore(cursorPath, cursorStore);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error("[log-reporter] Scan failed:", err);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
isRunning = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Run first scan immediately
|
|
43
|
+
await doScan();
|
|
44
|
+
// Schedule periodic scans
|
|
45
|
+
intervalId = setInterval(doScan, config.scanIntervalMs);
|
|
46
|
+
intervalId.unref?.();
|
|
47
|
+
return () => stopLogReporter();
|
|
48
|
+
}
|
|
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
|
+
/**
|
|
69
|
+
* Stop the log reporter timer.
|
|
70
|
+
*/
|
|
71
|
+
export function stopLogReporter() {
|
|
72
|
+
if (intervalId !== null) {
|
|
73
|
+
clearInterval(intervalId);
|
|
74
|
+
intervalId = null;
|
|
75
|
+
console.log("[log-reporter] Stopped");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ScanResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Send a log report to the server with the uploaded file URL.
|
|
4
|
+
* MOCK implementation — real request logic will be added later.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sendReport(reportUrl: string, fileUrl: string, result: ScanResult): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send a log report to the server with the uploaded file URL.
|
|
3
|
+
* MOCK implementation — real request logic will be added later.
|
|
4
|
+
*/
|
|
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(),
|
|
15
|
+
};
|
|
16
|
+
console.log(`[log-reporter] Mock report to ${reportUrl}:`, JSON.stringify(payload, null, 2));
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ScanResult, CursorStore } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Scan a single log file for new content.
|
|
4
|
+
* Returns a ScanResult if there are new lines, or null if no changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function scanFile(filePath: string, name: string, cursorStore: CursorStore): Promise<ScanResult | null>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Core scanner: resolves wildcard paths, reads incremental log content using byte-offset cursors
|
|
2
|
+
import { statSync, createReadStream } from "fs";
|
|
3
|
+
import { getCursor } from "./cursor-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read file content starting from a byte offset.
|
|
6
|
+
* Returns the full text content from that offset to end of file.
|
|
7
|
+
*/
|
|
8
|
+
function readFromOffset(filePath, startByte) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
const stream = createReadStream(filePath, {
|
|
12
|
+
start: startByte,
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
});
|
|
15
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
16
|
+
stream.on("end", () => resolve(chunks.join("")));
|
|
17
|
+
stream.on("error", reject);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Determine the byte offset to start reading from, based on file state and cursor.
|
|
22
|
+
* Returns the startByte (0-based) or null if no new content.
|
|
23
|
+
*/
|
|
24
|
+
function resolveStartByte(currentSize, currentMtimeMs, cursor) {
|
|
25
|
+
if (!cursor) {
|
|
26
|
+
// New file: first scan — read from beginning
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (currentSize > cursor.lastSize) {
|
|
30
|
+
// File grew: read from where we left off
|
|
31
|
+
return cursor.lastSize;
|
|
32
|
+
}
|
|
33
|
+
if (currentSize < cursor.lastSize && currentMtimeMs > cursor.lastModified) {
|
|
34
|
+
// File was rotated (truncated + rewritten): reset to beginning
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
// No change (currentSize === cursor.lastSize) or edge case — skip
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Scan a single log file for new content.
|
|
42
|
+
* Returns a ScanResult if there are new lines, or null if no changes.
|
|
43
|
+
*/
|
|
44
|
+
export async function scanFile(filePath, name, cursorStore) {
|
|
45
|
+
let stats;
|
|
46
|
+
try {
|
|
47
|
+
stats = statSync(filePath);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// File doesn't exist (yet) or was deleted — skip
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const currentSize = stats.size;
|
|
54
|
+
const currentMtimeMs = stats.mtimeMs;
|
|
55
|
+
const cursor = getCursor(cursorStore, filePath);
|
|
56
|
+
const startByte = resolveStartByte(currentSize, currentMtimeMs, cursor);
|
|
57
|
+
if (startByte === null) {
|
|
58
|
+
return null; // no new content
|
|
59
|
+
}
|
|
60
|
+
const content = await readFromOffset(filePath, startByte);
|
|
61
|
+
// Count new lines — each \n represents one log line
|
|
62
|
+
const newLineCount = content.length > 0 ? content.split("\n").length - 1 : 0;
|
|
63
|
+
if (newLineCount === 0) {
|
|
64
|
+
// No complete lines yet (partial write), don't report
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const prevLine = cursor?.lastLine ?? 0;
|
|
68
|
+
const newCursor = {
|
|
69
|
+
lastSize: currentSize,
|
|
70
|
+
lastLine: prevLine + newLineCount,
|
|
71
|
+
lastModified: currentMtimeMs,
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
filePath,
|
|
75
|
+
name,
|
|
76
|
+
content,
|
|
77
|
+
lineStart: prevLine + 1,
|
|
78
|
+
lineEnd: prevLine + newLineCount,
|
|
79
|
+
newLineCount,
|
|
80
|
+
newCursor,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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} */
|
|
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[];
|
|
18
|
+
}
|
|
19
|
+
/** Cursor state for a single log file */
|
|
20
|
+
export interface FileCursor {
|
|
21
|
+
/** Byte offset we last read to */
|
|
22
|
+
lastSize: number;
|
|
23
|
+
/** Cumulative line count read so far */
|
|
24
|
+
lastLine: number;
|
|
25
|
+
/** File mtime (ms) at last read */
|
|
26
|
+
lastModified: number;
|
|
27
|
+
}
|
|
28
|
+
/** Persisted cursor store structure */
|
|
29
|
+
export interface CursorStore {
|
|
30
|
+
files: Record<string, FileCursor>;
|
|
31
|
+
}
|
|
32
|
+
/** Result of scanning a single log file */
|
|
33
|
+
export interface ScanResult {
|
|
34
|
+
/** Resolved absolute file path */
|
|
35
|
+
filePath: string;
|
|
36
|
+
/** Logical name from config */
|
|
37
|
+
name: string;
|
|
38
|
+
/** The new log lines (incremental content) */
|
|
39
|
+
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
|
+
/** Updated cursor to persist after successful upload */
|
|
47
|
+
newCursor: FileCursor;
|
|
48
|
+
}
|
|
49
|
+
/** Options passed to startLogReporter */
|
|
50
|
+
export interface LogReporterOptions {
|
|
51
|
+
/** Absolute path to the JSON config file */
|
|
52
|
+
configPath: string;
|
|
53
|
+
/** File upload service instance (from xy_channel) */
|
|
54
|
+
uploadService: UploadService;
|
|
55
|
+
}
|
|
56
|
+
/** Minimal interface for the upload service (duck-typed from XYFileUploadService) */
|
|
57
|
+
export interface UploadService {
|
|
58
|
+
uploadFileAndGetUrl(filePath: string, objectType?: string): Promise<string>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { UploadService, ScanResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Write incremental content to .bak file, upload, and return the URL.
|
|
4
|
+
* Cleans up .bak file regardless of success/failure.
|
|
5
|
+
*/
|
|
6
|
+
export declare function uploadIncrementalContent(result: ScanResult, bakDir: string, uploadService: UploadService): Promise<string>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Uploader: creates .bak file, uploads via XYFileUploadService, cleans up
|
|
2
|
+
import { writeFileSync, unlinkSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { mkdirSync } from "fs";
|
|
5
|
+
/**
|
|
6
|
+
* Write incremental content to .bak file, upload, and return the URL.
|
|
7
|
+
* Cleans up .bak file regardless of success/failure.
|
|
8
|
+
*/
|
|
9
|
+
export async function uploadIncrementalContent(result, bakDir, uploadService) {
|
|
10
|
+
const timestamp = Date.now();
|
|
11
|
+
const bakFileName = `${result.name}_${timestamp}.bak`;
|
|
12
|
+
const bakPath = join(bakDir, bakFileName);
|
|
13
|
+
// Ensure bakDir exists
|
|
14
|
+
try {
|
|
15
|
+
mkdirSync(bakDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
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;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
// Always clean up the .bak file
|
|
27
|
+
try {
|
|
28
|
+
unlinkSync(bakPath);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -267,7 +267,7 @@ function handleMemoryHistory() {
|
|
|
267
267
|
// Build ans array sorted by date ascending, each entry is { <date>: [...] }.
|
|
268
268
|
const ans = Array.from(byDate.keys())
|
|
269
269
|
.sort()
|
|
270
|
-
.map((dateStr) => ({ [dateStr]: byDate.get(dateStr) }));
|
|
270
|
+
.map((dateStr) => ({ [dateStr]: byDate.get(dateStr).reverse() }));
|
|
271
271
|
// Prune memory.log: keep only the last 30 days.
|
|
272
272
|
try {
|
|
273
273
|
const newContent = keptLines.length > 0 ? `${keptLines.join("\n")}\n` : "";
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -44,18 +44,6 @@ export declare function isTasksCancelMessage(method: string): boolean;
|
|
|
44
44
|
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
45
45
|
*/
|
|
46
46
|
export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
47
|
-
/**
|
|
48
|
-
* Extract app_ver from message parts.
|
|
49
|
-
* Looks for app_ver in data parts under variables.systemVariables.app_ver
|
|
50
|
-
* (same level as push_id).
|
|
51
|
-
*/
|
|
52
|
-
export declare function extractAppVer(parts: A2AMessagePart[]): string | null;
|
|
53
|
-
/**
|
|
54
|
-
* Extract display_version from message parts.
|
|
55
|
-
* Looks for display_version in data parts under variables.systemVariables.display_version
|
|
56
|
-
* (same level as push_id).
|
|
57
|
-
*/
|
|
58
|
-
export declare function extractDisplayVersion(parts: A2AMessagePart[]): string | null;
|
|
59
47
|
/**
|
|
60
48
|
* Extract deviceType from message parts.
|
|
61
49
|
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
package/dist/src/parser.js
CHANGED
|
@@ -56,16 +56,22 @@ export function extractRunCrossTaskContext(parts) {
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
const candidate = item;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const fileLocalUrls = Array.isArray(candidate.fileLocalUrls)
|
|
60
|
+
? candidate.fileLocalUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
61
|
+
: [];
|
|
62
|
+
const fileRemoteUrls = Array.isArray(candidate.fileRemoteUrls)
|
|
63
|
+
? candidate.fileRemoteUrls.filter((url) => typeof url === "string" && url.length > 0)
|
|
64
|
+
: [];
|
|
65
|
+
const fileNames = Array.isArray(candidate.fileNames)
|
|
66
|
+
? candidate.fileNames.filter((name) => typeof name === "string" && name.length > 0)
|
|
67
|
+
: [];
|
|
68
|
+
if (fileLocalUrls.length === 0 && fileRemoteUrls.length === 0) {
|
|
63
69
|
return null;
|
|
64
70
|
}
|
|
65
71
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
...(
|
|
72
|
+
...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
|
|
73
|
+
...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
|
|
74
|
+
...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
|
|
69
75
|
};
|
|
70
76
|
})
|
|
71
77
|
.filter((item) => item !== null);
|
|
@@ -121,38 +127,6 @@ export function extractPushId(parts) {
|
|
|
121
127
|
}
|
|
122
128
|
return null;
|
|
123
129
|
}
|
|
124
|
-
/**
|
|
125
|
-
* Extract app_ver from message parts.
|
|
126
|
-
* Looks for app_ver in data parts under variables.systemVariables.app_ver
|
|
127
|
-
* (same level as push_id).
|
|
128
|
-
*/
|
|
129
|
-
export function extractAppVer(parts) {
|
|
130
|
-
for (const part of parts) {
|
|
131
|
-
if (part.kind === "data" && part.data) {
|
|
132
|
-
const appVer = part.data.variables?.systemVariables?.app_ver;
|
|
133
|
-
if (appVer && typeof appVer === "string") {
|
|
134
|
-
return appVer;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Extract display_version from message parts.
|
|
142
|
-
* Looks for display_version in data parts under variables.systemVariables.display_version
|
|
143
|
-
* (same level as push_id).
|
|
144
|
-
*/
|
|
145
|
-
export function extractDisplayVersion(parts) {
|
|
146
|
-
for (const part of parts) {
|
|
147
|
-
if (part.kind === "data" && part.data) {
|
|
148
|
-
const displayVersion = part.data.variables?.systemVariables?.display_version;
|
|
149
|
-
if (displayVersion && typeof displayVersion === "string") {
|
|
150
|
-
return displayVersion;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
130
|
/**
|
|
157
131
|
* Extract deviceType from message parts.
|
|
158
132
|
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
package/dist/src/provider.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// models.providers.xiaoyiprovider.models = [...]
|
|
10
10
|
import { createHash } from "crypto";
|
|
11
11
|
import { logger } from "./utils/logger.js";
|
|
12
|
-
import { getCurrentSessionContext
|
|
12
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
13
13
|
import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
14
14
|
import { notifyModelStreaming } from "./bot.js";
|
|
15
15
|
// ── Retry config ──────────────────────────────────────────────
|
|
@@ -506,14 +506,6 @@ export const xiaoyiProvider = {
|
|
|
506
506
|
// 3. UID-based fallback: sha256(uid).hex[:32]_timestamp
|
|
507
507
|
const isCron = isCronTriggered(context.messages);
|
|
508
508
|
if (isCron) {
|
|
509
|
-
// fire 期 jobId 桥:把首条消息 `[cron:<jobId> ...]` 解析出的真实 jobId
|
|
510
|
-
// 绑定到本次 cron run 的合成 sessionId。sendCommand 凭同一 sessionId
|
|
511
|
-
// 反查 jobId → cron-push-map → 正确设备的 pushId(多设备路由)。
|
|
512
|
-
const cronJobId = extractCronUuid(context.messages);
|
|
513
|
-
const cronCtx = getCurrentSessionContext();
|
|
514
|
-
if (cronJobId && cronCtx?.sessionId) {
|
|
515
|
-
setCurrentCronJobId(cronCtx.sessionId, cronJobId);
|
|
516
|
-
}
|
|
517
509
|
const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
|
|
518
510
|
if (typeof fallbackPrefix === "string") {
|
|
519
511
|
const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
|
|
@@ -564,15 +556,6 @@ export const xiaoyiProvider = {
|
|
|
564
556
|
// then ALS fallback.
|
|
565
557
|
const deviceType = extractedDeviceType
|
|
566
558
|
?? getCurrentSessionContext()?.deviceType;
|
|
567
|
-
// app_ver and display_version from session context (ALS)
|
|
568
|
-
const appVer = sessionCtx?.appVer;
|
|
569
|
-
const displayVersion = sessionCtx?.displayVersion;
|
|
570
|
-
if (appVer) {
|
|
571
|
-
logger.log(`[xiaoyiprovider] app_ver: ${appVer}`);
|
|
572
|
-
}
|
|
573
|
-
if (displayVersion) {
|
|
574
|
-
logger.log(`[xiaoyiprovider] display_version: ${displayVersion}`);
|
|
575
|
-
}
|
|
576
559
|
// 在发送给模型前,优化 systemPrompt 结构
|
|
577
560
|
if (context.systemPrompt) {
|
|
578
561
|
let sp = context.systemPrompt;
|
|
@@ -608,19 +591,9 @@ export const xiaoyiProvider = {
|
|
|
608
591
|
logger.log(`[selfEvolution] selfEvolution flag: ${selfEvolutionEnabled}`);
|
|
609
592
|
context.systemPrompt = applySelfEvolutionPrompt(context.systemPrompt, selfEvolutionEnabled);
|
|
610
593
|
// Append device context to systemPrompt
|
|
611
|
-
if (deviceType
|
|
594
|
+
if (deviceType) {
|
|
612
595
|
const displayDevice = (deviceType === "2in1") ? "鸿蒙PC" : deviceType;
|
|
613
|
-
|
|
614
|
-
if (deviceType) {
|
|
615
|
-
deviceSection += `The current user is using the following device: ${displayDevice}\n`;
|
|
616
|
-
}
|
|
617
|
-
if (appVer) {
|
|
618
|
-
deviceSection += `当前用户小艺APP版本是${appVer}\n`;
|
|
619
|
-
}
|
|
620
|
-
if (displayVersion) {
|
|
621
|
-
deviceSection += `当前用户系统Rom版本是${displayVersion}\n`;
|
|
622
|
-
}
|
|
623
|
-
deviceSection += `You need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
|
|
596
|
+
const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
|
|
624
597
|
context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
|
|
625
598
|
}
|
|
626
599
|
// ── Trim user message metadata ──────────────────────
|
|
@@ -39,23 +39,17 @@ function buildCrossTaskExecuteResultCommand(code, message, sentFiles = []) {
|
|
|
39
39
|
async function sendRunCrossTaskResult(params) {
|
|
40
40
|
const { config, sessionId, taskId, messageId, context, resultCode, resultMessage } = params;
|
|
41
41
|
const sentFiles = Array.isArray(context.sentFiles) ? context.sentFiles : [];
|
|
42
|
-
const fileCardCount = sentFiles.length;
|
|
43
42
|
const statusCommand = buildDistributionStatusCommand(context);
|
|
44
43
|
const resultCommand = buildCrossTaskExecuteResultCommand(resultCode, resultMessage, sentFiles);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
finally {
|
|
56
|
-
clearRunCrossTaskSentFiles(context);
|
|
57
|
-
logger.log(`${RUN_CROSS_TASK_LOG_TAG} cleared cross-task sentFiles, sessionId=${sessionId}, taskId=${taskId}, clearedFileCardCount=${fileCardCount}`);
|
|
58
|
-
}
|
|
44
|
+
await sendCommand({
|
|
45
|
+
config,
|
|
46
|
+
sessionId,
|
|
47
|
+
taskId,
|
|
48
|
+
messageId,
|
|
49
|
+
commands: [statusCommand, resultCommand],
|
|
50
|
+
});
|
|
51
|
+
clearRunCrossTaskSentFiles(context);
|
|
52
|
+
logger.log(`${RUN_CROSS_TASK_LOG_TAG} sent cross-task result, sessionId=${sessionId}, taskId=${taskId}, code=${resultCode}, sentFileCount=${sentFiles.length}, clearedSentFileCount=${sentFiles.length}, messageLength=${resultMessage.length}`);
|
|
59
53
|
}
|
|
60
54
|
/**
|
|
61
55
|
* 清理 /tmp/xy_channel 目录中超过 24 小时的旧文件
|
|
@@ -122,7 +116,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
122
116
|
let hasSentResponse = false;
|
|
123
117
|
let finalSent = false;
|
|
124
118
|
let accumulatedText = "";
|
|
125
|
-
let finalReplyText = "";
|
|
126
119
|
const initialRunCrossTaskContext = getCurrentSessionContext()?.runCrossTaskContext;
|
|
127
120
|
const getRunCrossTaskContext = () => {
|
|
128
121
|
return getCurrentSessionContext()?.runCrossTaskContext ?? initialRunCrossTaskContext;
|
|
@@ -179,10 +172,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
179
172
|
scopedLog().log(`[DELIVER SKIP] Empty text, skipping`);
|
|
180
173
|
return;
|
|
181
174
|
}
|
|
182
|
-
if (info?.kind === "final") {
|
|
183
|
-
finalReplyText = text;
|
|
184
|
-
scopedLog().log(`[DELIVER] Captured final reply text, length=${finalReplyText.length}`);
|
|
185
|
-
}
|
|
186
175
|
// 🔑 如果 onPartialReply 已经流式发送过文本,deliver 不再重复发送
|
|
187
176
|
if (hasSentResponse) {
|
|
188
177
|
scopedLog().log(`[DELIVER SKIP] Already sent via onPartialReply`);
|
|
@@ -243,11 +232,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
243
232
|
}
|
|
244
233
|
// 正常模式(或未被steer的dispatch)
|
|
245
234
|
if (hasSentResponse && !finalSent) {
|
|
246
|
-
|
|
247
|
-
const trimmedAccumulatedText = accumulatedText.trim();
|
|
248
|
-
const crossTaskResultMessage = trimmedFinalReplyText || trimmedAccumulatedText;
|
|
249
|
-
const crossTaskResultSource = trimmedFinalReplyText ? "final" : "accumulated";
|
|
250
|
-
scopedLog().log(`[ON-IDLE] [SendCrossResult]Sending cross-task result, source=${crossTaskResultSource}, resultMessage.length=${crossTaskResultMessage.length}`);
|
|
235
|
+
scopedLog().log(`[ON-IDLE] Sending accumulated text, length=${accumulatedText.length}`);
|
|
251
236
|
try {
|
|
252
237
|
const runCrossTaskContext = getRunCrossTaskContext();
|
|
253
238
|
if (runCrossTaskContext) {
|
|
@@ -258,7 +243,7 @@ export function createXYReplyDispatcher(params) {
|
|
|
258
243
|
messageId: currentMessageId,
|
|
259
244
|
context: runCrossTaskContext,
|
|
260
245
|
resultCode: "0",
|
|
261
|
-
resultMessage:
|
|
246
|
+
resultMessage: accumulatedText,
|
|
262
247
|
});
|
|
263
248
|
}
|
|
264
249
|
// 🔑 使用动态taskId发送完成状态
|