@ynhcj/xiaoyi-channel 0.0.191-beta → 0.0.193-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 @@
1
+ export declare function handleMemoryQueryEvent(context: any, cfg: any): Promise<void>;
@@ -0,0 +1,250 @@
1
+ // Memory query event handler.
2
+ // Listens for memory-query-event from the WebSocket manager,
3
+ // handles memory state read/write and MEMORY.md/USER.md file queries.
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ import { readFileSync, writeFileSync } from "fs";
7
+ import { sendCommand } from "./formatter.js";
8
+ import { resolveXYConfig } from "./config.js";
9
+ import { logger } from "./utils/logger.js";
10
+ const XIAOYIRUNTIME_PATH_PRIMARY = "/home/sandbox/.openclaw/.xiaoyiruntime";
11
+ const XIAOYIRUNTIME_PATH_FALLBACK = `${os.homedir()}/.openclaw/.xiaoyiruntime`;
12
+ const MEMORY_STATE_KEY = "MEMORYSTATE";
13
+ /** Resolve writable .xiaoyiruntime path: try sandbox path first, fallback to user home. */
14
+ function resolveXiaoyiRuntimePath() {
15
+ try {
16
+ // If primary path's parent dir exists and is writable, use it
17
+ const fs = require("fs");
18
+ fs.accessSync(XIAOYIRUNTIME_PATH_PRIMARY, fs.constants.W_OK);
19
+ return XIAOYIRUNTIME_PATH_PRIMARY;
20
+ }
21
+ catch {
22
+ // If primary path doesn't exist, try creating parent
23
+ try {
24
+ const fs = require("fs");
25
+ const dir = require("path").dirname(XIAOYIRUNTIME_PATH_PRIMARY);
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ return XIAOYIRUNTIME_PATH_PRIMARY;
28
+ }
29
+ catch {
30
+ // Fallback to user home
31
+ }
32
+ }
33
+ return XIAOYIRUNTIME_PATH_FALLBACK;
34
+ }
35
+ export async function handleMemoryQueryEvent(context, cfg) {
36
+ const { action, params, sessionId, taskId, messageId } = context;
37
+ const log = logger.withContext(sessionId ?? "", taskId ?? "");
38
+ log.log(`[MEMORY-QUERY] Received event: action=${action}`);
39
+ let result;
40
+ try {
41
+ switch (action) {
42
+ case "MemoryStateSet":
43
+ result = handleMemoryStateSet(params);
44
+ break;
45
+ case "UserMdQuery":
46
+ result = handleUserMdQuery();
47
+ break;
48
+ case "MemoryMdQuery":
49
+ result = handleMemoryMdQuery();
50
+ break;
51
+ case "MemoryHistory":
52
+ result = handleMemoryHistory();
53
+ break;
54
+ default:
55
+ log.error(`[MEMORY-QUERY] Unknown action: ${action}`);
56
+ result = { error: `Unknown action: ${action}` };
57
+ }
58
+ }
59
+ catch (err) {
60
+ const errorMsg = err instanceof Error ? err.message : String(err);
61
+ log.error(`[MEMORY-QUERY] Handler failed for action=${action}:`, err);
62
+ result = { error: errorMsg };
63
+ }
64
+ log.log(`[MEMORY-QUERY] Result for action=${action}: ${JSON.stringify(result)}`);
65
+ // Send result back via sendCommand
66
+ if (cfg && sessionId && taskId && messageId) {
67
+ try {
68
+ const config = resolveXYConfig(cfg);
69
+ const command = {
70
+ header: {
71
+ namespace: "AgentEvent",
72
+ name: "MemoryQuery",
73
+ },
74
+ payload: {
75
+ action,
76
+ ans: result,
77
+ },
78
+ };
79
+ await sendCommand({
80
+ config,
81
+ sessionId,
82
+ taskId,
83
+ messageId,
84
+ command,
85
+ final: true,
86
+ });
87
+ log.log(`[MEMORY-QUERY] Sent response via sendCommand, action=${action}`);
88
+ }
89
+ catch (sendErr) {
90
+ log.error(`[MEMORY-QUERY] Failed to send response via sendCommand:`, sendErr);
91
+ }
92
+ }
93
+ else {
94
+ log.warn(`[MEMORY-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
95
+ }
96
+ }
97
+ /**
98
+ * Write MEMORYSTATE=true/false to .xiaoyiruntime.
99
+ */
100
+ function handleMemoryStateSet(params) {
101
+ const memoryState = params?.memoryState;
102
+ if (typeof memoryState !== "boolean") {
103
+ logger.error(`[MEMORY-QUERY] memoryStateSet: invalid memoryState type: ${typeof memoryState}`);
104
+ return { code: 0 };
105
+ }
106
+ const value = String(memoryState);
107
+ const filePath = resolveXiaoyiRuntimePath();
108
+ let content;
109
+ try {
110
+ content = readFileSync(filePath, "utf-8");
111
+ }
112
+ catch {
113
+ logger.log(`[MEMORY-QUERY] ${filePath} not found, creating new file`);
114
+ writeFileSync(filePath, `${MEMORY_STATE_KEY}=${value}\n`, "utf-8");
115
+ logger.log(`[MEMORY-QUERY] wrote ${MEMORY_STATE_KEY}=${value}`);
116
+ return { code: 0 };
117
+ }
118
+ const lines = content.split("\n");
119
+ const key = MEMORY_STATE_KEY;
120
+ let found = false;
121
+ const updated = lines.map((line) => {
122
+ if (line.startsWith(`${key}=`)) {
123
+ found = true;
124
+ return `${key}=${value}`;
125
+ }
126
+ return line;
127
+ });
128
+ if (!found) {
129
+ const trimmed = content.trimEnd();
130
+ writeFileSync(filePath, `${trimmed}\n${key}=${value}\n`, "utf-8");
131
+ }
132
+ else {
133
+ writeFileSync(filePath, updated.join("\n"), "utf-8");
134
+ }
135
+ logger.log(`[MEMORY-QUERY] updated ${MEMORY_STATE_KEY}=${value} in ${filePath}`);
136
+ return { code: 0 };
137
+ }
138
+ /**
139
+ * Read ~/.openclaw/workspace/USER.md and return content in fileDetail.
140
+ */
141
+ function handleUserMdQuery() {
142
+ const filePath = path.join(os.homedir(), ".openclaw", "workspace", "USER.md");
143
+ return readMdFile(filePath);
144
+ }
145
+ /**
146
+ * Read ~/.openclaw/workspace/MEMORY.md and return content in fileDetail.
147
+ */
148
+ function handleMemoryMdQuery() {
149
+ const filePath = path.join(os.homedir(), ".openclaw", "workspace", "MEMORY.md");
150
+ return readMdFile(filePath);
151
+ }
152
+ function readMdFile(filePath) {
153
+ try {
154
+ const content = readFileSync(filePath, "utf-8");
155
+ logger.log(`[MEMORY-QUERY] Read file: ${filePath}, size: ${content.length}`);
156
+ return { fileDetail: content };
157
+ }
158
+ catch (err) {
159
+ if (err.code === "ENOENT") {
160
+ logger.log(`[MEMORY-QUERY] File not found: ${filePath}`);
161
+ }
162
+ else {
163
+ logger.error(`[MEMORY-QUERY] Failed to read ${filePath}:`, err);
164
+ }
165
+ return { fileDetail: "" };
166
+ }
167
+ }
168
+ const MEMORY_LOG_PATH = path.join(os.homedir(), ".openclaw", ".memory.log");
169
+ const MEMORY_HISTORY_DAYS = 7;
170
+ const MEMORY_RETENTION_DAYS = 30;
171
+ /**
172
+ * Read ~/.openclaw/.memory.log, return last 7 days grouped by date,
173
+ * then prune entries older than 30 days.
174
+ *
175
+ * Log line format: `2026-06-22T15:18:00|user.md|更新了xxxx`
176
+ * Only split on the first two `|`; everything after is the detail
177
+ * (detail itself may contain `|`).
178
+ */
179
+ function handleMemoryHistory() {
180
+ let content;
181
+ try {
182
+ content = readFileSync(MEMORY_LOG_PATH, "utf-8");
183
+ }
184
+ catch (err) {
185
+ if (err.code === "ENOENT") {
186
+ logger.log(`[MEMORY-QUERY] memory.log not found: ${MEMORY_LOG_PATH}`);
187
+ }
188
+ else {
189
+ logger.error(`[MEMORY-QUERY] Failed to read memory.log:`, err);
190
+ }
191
+ return [];
192
+ }
193
+ const lines = content.split("\n");
194
+ const now = new Date();
195
+ const historySince = new Date(now);
196
+ historySince.setDate(now.getDate() - (MEMORY_HISTORY_DAYS - 1));
197
+ historySince.setHours(0, 0, 0, 0);
198
+ const retentionSince = new Date(now);
199
+ retentionSince.setDate(now.getDate() - (MEMORY_RETENTION_DAYS - 1));
200
+ retentionSince.setHours(0, 0, 0, 0);
201
+ const byDate = new Map();
202
+ const keptLines = [];
203
+ for (const raw of lines) {
204
+ const line = raw.trimEnd();
205
+ if (!line)
206
+ continue;
207
+ // Split on only the first two `|`; rest is detail (may contain `|`).
208
+ const firstPipe = line.indexOf("|");
209
+ if (firstPipe === -1)
210
+ continue;
211
+ const secondPipe = line.indexOf("|", firstPipe + 1);
212
+ if (secondPipe === -1)
213
+ continue;
214
+ const timestamp = line.slice(0, firstPipe);
215
+ const fileName = line.slice(firstPipe + 1, secondPipe);
216
+ const detail = line.slice(secondPipe + 1);
217
+ // timestamp format: 2026-06-22T15:18:00
218
+ const datePart = timestamp.slice(0, 10);
219
+ const timePart = timestamp.slice(11, 19);
220
+ const entryDate = new Date(`${datePart}T00:00:00`);
221
+ // Retain log lines within the 30-day window.
222
+ if (!isNaN(entryDate.getTime()) && entryDate >= retentionSince) {
223
+ keptLines.push(line);
224
+ }
225
+ // Include in response if within the 7-day window.
226
+ if (!isNaN(entryDate.getTime()) && entryDate >= historySince) {
227
+ let bucket = byDate.get(datePart);
228
+ if (!bucket) {
229
+ bucket = [];
230
+ byDate.set(datePart, bucket);
231
+ }
232
+ bucket.push({ fileName, detail, time: timePart });
233
+ }
234
+ }
235
+ // Build ans array sorted by date ascending, each entry is { <date>: [...] }.
236
+ const ans = Array.from(byDate.keys())
237
+ .sort()
238
+ .map((dateStr) => ({ [dateStr]: byDate.get(dateStr) }));
239
+ // Prune memory.log: keep only the last 30 days.
240
+ try {
241
+ const newContent = keptLines.length > 0 ? `${keptLines.join("\n")}\n` : "";
242
+ writeFileSync(MEMORY_LOG_PATH, newContent, "utf-8");
243
+ logger.log(`[MEMORY-QUERY] Pruned memory.log, kept ${keptLines.length} entries (>= ${retentionSince.toISOString().slice(0, 10)})`);
244
+ }
245
+ catch (err) {
246
+ logger.error(`[MEMORY-QUERY] Failed to prune memory.log:`, err);
247
+ }
248
+ logger.log(`[MEMORY-QUERY] MemoryHistory: returning ${ans.length} date buckets`);
249
+ return ans;
250
+ }
@@ -8,6 +8,7 @@ import { handleTriggerEvent } from "./trigger-handler.js";
8
8
  import { handleSelfEvolutionEvent, handleSelfEvolutionStateGetEvent } from "./self-evolution-handler.js";
9
9
  import { handleLoginTokenEvent } from "./login-token-handler.js";
10
10
  import { handleCronQueryEvent } from "./cron-query-handler.js";
11
+ import { handleMemoryQueryEvent } from "./memory-query-handler.js";
11
12
  import { cleanupStaleTempFiles } from "./reply-dispatcher.js";
12
13
  import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./tools/session-manager.js";
13
14
  import { logger } from "./utils/logger.js";
@@ -205,6 +206,12 @@ export async function monitorXYProvider(opts = {}) {
205
206
  logger.error(`[MONITOR] Failed to handle cron-query-event:`, err);
206
207
  });
207
208
  };
209
+ const memoryQueryEventHandler = (context) => {
210
+ logger.log(`[MONITOR] Received memory-query-event, dispatching to handler...`);
211
+ handleMemoryQueryEvent(context, cfg).catch((err) => {
212
+ logger.error(`[MONITOR] Failed to handle memory-query-event:`, err);
213
+ });
214
+ };
208
215
  const cleanup = () => {
209
216
  logger.log("XY gateway: cleaning up...");
210
217
  // 🔍 Diagnose before cleanup
@@ -226,6 +233,7 @@ export async function monitorXYProvider(opts = {}) {
226
233
  wsManager.off("self-evolution-state-get-event", selfEvolutionStateGetHandler);
227
234
  wsManager.off("login-token-event", loginTokenEventHandler);
228
235
  wsManager.off("cron-query-event", cronQueryEventHandler);
236
+ wsManager.off("memory-query-event", memoryQueryEventHandler);
229
237
  // ✅ Disconnect the wsManager to prevent connection leaks
230
238
  // This is safe because each gateway lifecycle should have clean connections
231
239
  wsManager.disconnect();
@@ -290,6 +298,7 @@ export async function monitorXYProvider(opts = {}) {
290
298
  wsManager.on("self-evolution-state-get-event", selfEvolutionStateGetHandler);
291
299
  wsManager.on("login-token-event", loginTokenEventHandler);
292
300
  wsManager.on("cron-query-event", cronQueryEventHandler);
301
+ wsManager.on("memory-query-event", memoryQueryEventHandler);
293
302
  // Start periodic health check (every 6 hours)
294
303
  logger.log("Starting periodic health check (every 6 hours)...");
295
304
  healthCheckInterval = setInterval(() => {
@@ -630,6 +630,15 @@ export class XYWebSocketManager extends EventEmitter {
630
630
  messageId: a2aRequest.id,
631
631
  });
632
632
  }
633
+ else if (item.header?.namespace === "AgentEvent" && item.header?.name === "MemoryQuery") {
634
+ log.log("[XY] AgentEvent.MemoryQuery detected, emitting memory-query-event");
635
+ this.emit("memory-query-event", {
636
+ ...(item.payload ?? {}),
637
+ sessionId,
638
+ taskId: a2aRequest.params?.id,
639
+ messageId: a2aRequest.id,
640
+ });
641
+ }
633
642
  else if (item.header?.namespace === "System" && item.header?.name === "ExecuteAgentAsSkillResponse") {
634
643
  log.log("[XY] ExecuteAgentAsSkillResponse detected, emitting agent-as-skill-response");
635
644
  this.emit("agent-as-skill-response", item);
@@ -719,6 +728,15 @@ export class XYWebSocketManager extends EventEmitter {
719
728
  messageId: a2aRequest.id,
720
729
  });
721
730
  }
731
+ else if (item.header?.namespace === "AgentEvent" && item.header?.name === "MemoryQuery") {
732
+ log.log("[XY] AgentEvent.MemoryQuery detected (wrapped format), emitting memory-query-event");
733
+ this.emit("memory-query-event", {
734
+ ...(item.payload ?? {}),
735
+ sessionId: inboundMsg.sessionId || a2aRequest.params?.sessionId,
736
+ taskId: inboundMsg.taskId || a2aRequest.params?.id,
737
+ messageId: a2aRequest.id,
738
+ });
739
+ }
722
740
  else if (item.header?.namespace === "System" && item.header?.name === "ExecuteAgentAsSkillResponse") {
723
741
  log.log("[XY] ExecuteAgentAsSkillResponse detected (wrapped format), emitting agent-as-skill-response");
724
742
  this.emit("agent-as-skill-response", item);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.191-beta",
3
+ "version": "0.0.193-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",