@ynhcj/xiaoyi-channel 0.0.185-beta → 0.0.185-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.
Files changed (150) hide show
  1. package/dist/index.js +141 -1
  2. package/dist/src/acp-session-binding.d.ts +37 -0
  3. package/dist/src/acp-session-binding.js +237 -0
  4. package/dist/src/bot.js +62 -33
  5. package/dist/src/channel.js +68 -64
  6. package/dist/src/client.d.ts +5 -0
  7. package/dist/src/client.js +10 -0
  8. package/dist/src/cron-command.d.ts +2 -0
  9. package/dist/src/cron-command.js +14 -8
  10. package/dist/src/cron-query-handler.js +36 -0
  11. package/dist/src/cspl/call_api.js +4 -2
  12. package/dist/src/cspl/sentinel_hook.js +9 -8
  13. package/dist/src/cspl/upload_file.js +2 -2
  14. package/dist/src/formatter.js +72 -36
  15. package/dist/src/log-reporter/config-loader.d.ts +11 -0
  16. package/dist/src/log-reporter/config-loader.js +68 -0
  17. package/dist/src/log-reporter/cursor-store.d.ts +5 -0
  18. package/dist/src/log-reporter/cursor-store.js +26 -0
  19. package/dist/src/log-reporter/index.d.ts +10 -0
  20. package/dist/src/log-reporter/index.js +77 -0
  21. package/dist/src/log-reporter/reporter.d.ts +6 -0
  22. package/dist/src/log-reporter/reporter.js +17 -0
  23. package/dist/src/log-reporter/scanner.d.ts +6 -0
  24. package/dist/src/log-reporter/scanner.js +82 -0
  25. package/dist/src/log-reporter/types.d.ts +59 -0
  26. package/dist/src/log-reporter/types.js +2 -0
  27. package/dist/src/log-reporter/uploader.d.ts +6 -0
  28. package/dist/src/log-reporter/uploader.js +32 -0
  29. package/dist/src/memory-query-handler.d.ts +1 -0
  30. package/dist/src/memory-query-handler.js +283 -0
  31. package/dist/src/monitor.js +29 -9
  32. package/dist/src/outbound.js +5 -0
  33. package/dist/src/parser.d.ts +12 -0
  34. package/dist/src/parser.js +39 -13
  35. package/dist/src/provider.js +79 -16
  36. package/dist/src/reply-dispatcher.d.ts +3 -0
  37. package/dist/src/reply-dispatcher.js +166 -164
  38. package/dist/src/self-evolution-handler.js +2 -1
  39. package/dist/src/task-manager.d.ts +5 -6
  40. package/dist/src/task-manager.js +5 -9
  41. package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
  42. package/dist/src/tools/agent-as-skill-tool.js +144 -151
  43. package/dist/src/tools/calendar-tool.d.ts +24 -2
  44. package/dist/src/tools/calendar-tool.js +115 -117
  45. package/dist/src/tools/call-device-tool.d.ts +20 -6
  46. package/dist/src/tools/call-device-tool.js +116 -138
  47. package/dist/src/tools/call-phone-tool.d.ts +21 -2
  48. package/dist/src/tools/call-phone-tool.js +112 -114
  49. package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
  50. package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
  51. package/dist/src/tools/create-alarm-tool.d.ts +39 -2
  52. package/dist/src/tools/create-alarm-tool.js +229 -231
  53. package/dist/src/tools/create-all-tools.js +8 -4
  54. package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
  55. package/dist/src/tools/delete-alarm-tool.js +134 -136
  56. package/dist/src/tools/device-tool-map.d.ts +1 -1
  57. package/dist/src/tools/device-tool-map.js +9 -2
  58. package/dist/src/tools/discover-cross-devices-tool.d.ts +16 -2
  59. package/dist/src/tools/discover-cross-devices-tool.js +121 -124
  60. package/dist/src/tools/display-a2ui-card-bypath-tool.d.ts +21 -0
  61. package/dist/src/tools/display-a2ui-card-bypath-tool.js +65 -0
  62. package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
  63. package/dist/src/tools/get-alarm-tool-schema.js +10 -16
  64. package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
  65. package/dist/src/tools/get-calendar-tool-schema.js +8 -12
  66. package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
  67. package/dist/src/tools/get-collection-tool-schema.js +9 -11
  68. package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
  69. package/dist/src/tools/get-contact-tool-schema.js +10 -16
  70. package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
  71. package/dist/src/tools/get-device-file-tool-schema.js +9 -13
  72. package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
  73. package/dist/src/tools/get-email-tool-schema.js +8 -11
  74. package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
  75. package/dist/src/tools/get-note-tool-schema.js +9 -14
  76. package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
  77. package/dist/src/tools/get-photo-tool-schema.js +9 -12
  78. package/dist/src/tools/image-reading-tool.d.ts +28 -2
  79. package/dist/src/tools/image-reading-tool.js +76 -76
  80. package/dist/src/tools/location-tool.d.ts +11 -2
  81. package/dist/src/tools/location-tool.js +93 -92
  82. package/dist/src/tools/login-token-tool.d.ts +20 -2
  83. package/dist/src/tools/login-token-tool.js +123 -125
  84. package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
  85. package/dist/src/tools/modify-alarm-tool.js +250 -252
  86. package/dist/src/tools/modify-note-tool.d.ts +20 -2
  87. package/dist/src/tools/modify-note-tool.js +107 -109
  88. package/dist/src/tools/note-tool.d.ts +20 -2
  89. package/dist/src/tools/note-tool.js +106 -108
  90. package/dist/src/tools/query-app-message-tool.d.ts +28 -2
  91. package/dist/src/tools/query-app-message-tool.js +111 -113
  92. package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
  93. package/dist/src/tools/query-memory-data-tool.js +112 -114
  94. package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
  95. package/dist/src/tools/query-todo-task-tool.js +106 -108
  96. package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
  97. package/dist/src/tools/save-file-to-phone-tool.js +130 -132
  98. package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
  99. package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
  100. package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
  101. package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
  102. package/dist/src/tools/search-alarm-tool.d.ts +34 -2
  103. package/dist/src/tools/search-alarm-tool.js +174 -176
  104. package/dist/src/tools/search-calendar-tool.d.ts +24 -2
  105. package/dist/src/tools/search-calendar-tool.js +148 -150
  106. package/dist/src/tools/search-contact-tool.d.ts +16 -2
  107. package/dist/src/tools/search-contact-tool.js +101 -103
  108. package/dist/src/tools/search-email-tool.d.ts +21 -2
  109. package/dist/src/tools/search-email-tool.js +110 -112
  110. package/dist/src/tools/search-file-tool.d.ts +16 -2
  111. package/dist/src/tools/search-file-tool.js +104 -106
  112. package/dist/src/tools/search-message-tool.d.ts +16 -2
  113. package/dist/src/tools/search-message-tool.js +103 -105
  114. package/dist/src/tools/search-note-tool.d.ts +16 -2
  115. package/dist/src/tools/search-note-tool.js +98 -100
  116. package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
  117. package/dist/src/tools/search-photo-gallery-tool.js +35 -37
  118. package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
  119. package/dist/src/tools/send-cross-device-task-tool.js +216 -150
  120. package/dist/src/tools/send-email-tool.d.ts +24 -2
  121. package/dist/src/tools/send-email-tool.js +108 -110
  122. package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
  123. package/dist/src/tools/send-file-to-user-tool.js +174 -178
  124. package/dist/src/tools/send-html-card-tool.d.ts +20 -2
  125. package/dist/src/tools/send-html-card-tool.js +85 -87
  126. package/dist/src/tools/send-message-tool.d.ts +20 -2
  127. package/dist/src/tools/send-message-tool.js +122 -124
  128. package/dist/src/tools/session-manager.d.ts +19 -52
  129. package/dist/src/tools/session-manager.js +95 -247
  130. package/dist/src/tools/upload-file-tool.d.ts +20 -2
  131. package/dist/src/tools/upload-file-tool.js +80 -82
  132. package/dist/src/tools/upload-photo-tool.d.ts +20 -2
  133. package/dist/src/tools/upload-photo-tool.js +68 -70
  134. package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
  135. package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
  136. package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
  137. package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
  138. package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
  139. package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
  140. package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
  141. package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
  142. package/dist/src/types.d.ts +6 -6
  143. package/dist/src/utils/config-manager.d.ts +3 -2
  144. package/dist/src/utils/config-manager.js +22 -2
  145. package/dist/src/utils/cron-push-map.d.ts +26 -0
  146. package/dist/src/utils/cron-push-map.js +131 -0
  147. package/dist/src/utils/logger.js +3 -14
  148. package/dist/src/websocket.d.ts +1 -1
  149. package/dist/src/websocket.js +29 -13
  150. package/package.json +1 -1
@@ -0,0 +1,283 @@
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 "MemoryStateGet":
46
+ result = handleMemoryStateGet();
47
+ break;
48
+ case "UserMdQuery":
49
+ result = handleUserMdQuery();
50
+ break;
51
+ case "MemoryMdQuery":
52
+ result = handleMemoryMdQuery();
53
+ break;
54
+ case "MemoryHistory":
55
+ result = handleMemoryHistory();
56
+ break;
57
+ default:
58
+ log.error(`[MEMORY-QUERY] Unknown action: ${action}`);
59
+ result = { error: `Unknown action: ${action}` };
60
+ }
61
+ }
62
+ catch (err) {
63
+ const errorMsg = err instanceof Error ? err.message : String(err);
64
+ log.error(`[MEMORY-QUERY] Handler failed for action=${action}:`, err);
65
+ result = { error: errorMsg };
66
+ }
67
+ log.log(`[MEMORY-QUERY] Result for action=${action}: ${JSON.stringify(result)}`);
68
+ // Send result back via sendCommand
69
+ if (cfg && sessionId && taskId && messageId) {
70
+ try {
71
+ const config = resolveXYConfig(cfg);
72
+ const command = {
73
+ header: {
74
+ namespace: "AgentEvent",
75
+ name: "MemoryQuery",
76
+ },
77
+ payload: {
78
+ action,
79
+ ans: result,
80
+ },
81
+ };
82
+ await sendCommand({
83
+ config,
84
+ sessionId,
85
+ taskId,
86
+ messageId,
87
+ command,
88
+ final: true,
89
+ });
90
+ log.log(`[MEMORY-QUERY] Sent response via sendCommand, action=${action}`);
91
+ }
92
+ catch (sendErr) {
93
+ log.error(`[MEMORY-QUERY] Failed to send response via sendCommand:`, sendErr);
94
+ }
95
+ }
96
+ else {
97
+ log.warn(`[MEMORY-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
98
+ }
99
+ }
100
+ /**
101
+ * Write MEMORYSTATE=true/false to .xiaoyiruntime.
102
+ */
103
+ function handleMemoryStateSet(params) {
104
+ const memoryState = params?.memoryState;
105
+ if (typeof memoryState !== "boolean") {
106
+ logger.error(`[MEMORY-QUERY] memoryStateSet: invalid memoryState type: ${typeof memoryState}`);
107
+ return { code: 0 };
108
+ }
109
+ const value = String(memoryState);
110
+ const filePath = resolveXiaoyiRuntimePath();
111
+ let content;
112
+ try {
113
+ content = readFileSync(filePath, "utf-8");
114
+ }
115
+ catch {
116
+ logger.log(`[MEMORY-QUERY] ${filePath} not found, creating new file`);
117
+ writeFileSync(filePath, `${MEMORY_STATE_KEY}=${value}\n`, "utf-8");
118
+ logger.log(`[MEMORY-QUERY] wrote ${MEMORY_STATE_KEY}=${value}`);
119
+ return { code: 0 };
120
+ }
121
+ const lines = content.split("\n");
122
+ const key = MEMORY_STATE_KEY;
123
+ let found = false;
124
+ const updated = lines.map((line) => {
125
+ if (line.startsWith(`${key}=`)) {
126
+ found = true;
127
+ return `${key}=${value}`;
128
+ }
129
+ return line;
130
+ });
131
+ if (!found) {
132
+ const trimmed = content.trimEnd();
133
+ writeFileSync(filePath, `${trimmed}\n${key}=${value}\n`, "utf-8");
134
+ }
135
+ else {
136
+ writeFileSync(filePath, updated.join("\n"), "utf-8");
137
+ }
138
+ logger.log(`[MEMORY-QUERY] updated ${MEMORY_STATE_KEY}=${value} in ${filePath}`);
139
+ return { code: 0 };
140
+ }
141
+ /**
142
+ * Read MEMORYSTATE from .xiaoyiruntime and return its boolean value.
143
+ * Missing file or key defaults to false.
144
+ */
145
+ function handleMemoryStateGet() {
146
+ const filePath = resolveXiaoyiRuntimePath();
147
+ let content;
148
+ try {
149
+ content = readFileSync(filePath, "utf-8");
150
+ }
151
+ catch (err) {
152
+ if (err.code === "ENOENT") {
153
+ logger.log(`[MEMORY-QUERY] ${filePath} not found`);
154
+ }
155
+ else {
156
+ logger.error(`[MEMORY-QUERY] Failed to read ${filePath}:`, err);
157
+ }
158
+ return { memoryState: false };
159
+ }
160
+ for (const line of content.split("\n")) {
161
+ if (line.startsWith(`${MEMORY_STATE_KEY}=`)) {
162
+ const value = line.slice(`${MEMORY_STATE_KEY}=`.length).trim();
163
+ logger.log(`[MEMORY-QUERY] read ${MEMORY_STATE_KEY}=${value} from ${filePath}`);
164
+ return { memoryState: value === "true" };
165
+ }
166
+ }
167
+ logger.log(`[MEMORY-QUERY] ${MEMORY_STATE_KEY} not found in ${filePath}`);
168
+ return { memoryState: false };
169
+ }
170
+ /**
171
+ * Read ~/.openclaw/workspace/USER.md and return content in fileDetail.
172
+ */
173
+ function handleUserMdQuery() {
174
+ const filePath = path.join(os.homedir(), ".openclaw", "workspace", "USER.md");
175
+ return readMdFile(filePath);
176
+ }
177
+ /**
178
+ * Read ~/.openclaw/workspace/MEMORY.md and return content in fileDetail.
179
+ */
180
+ function handleMemoryMdQuery() {
181
+ const filePath = path.join(os.homedir(), ".openclaw", "workspace", "MEMORY.md");
182
+ return readMdFile(filePath);
183
+ }
184
+ function readMdFile(filePath) {
185
+ try {
186
+ const content = readFileSync(filePath, "utf-8");
187
+ logger.log(`[MEMORY-QUERY] Read file: ${filePath}, size: ${content.length}`);
188
+ return { fileDetail: content };
189
+ }
190
+ catch (err) {
191
+ if (err.code === "ENOENT") {
192
+ logger.log(`[MEMORY-QUERY] File not found: ${filePath}`);
193
+ }
194
+ else {
195
+ logger.error(`[MEMORY-QUERY] Failed to read ${filePath}:`, err);
196
+ }
197
+ return { fileDetail: "" };
198
+ }
199
+ }
200
+ const MEMORY_LOG_PATH = path.join(os.homedir(), ".openclaw", ".memory.log");
201
+ const MEMORY_HISTORY_DAYS = 7;
202
+ const MEMORY_RETENTION_DAYS = 30;
203
+ /**
204
+ * Read ~/.openclaw/.memory.log, return last 7 days grouped by date,
205
+ * then prune entries older than 30 days.
206
+ *
207
+ * Log line format: `2026-06-22T15:18:00|user.md|更新了xxxx`
208
+ * Only split on the first two `|`; everything after is the detail
209
+ * (detail itself may contain `|`).
210
+ */
211
+ function handleMemoryHistory() {
212
+ let content;
213
+ try {
214
+ content = readFileSync(MEMORY_LOG_PATH, "utf-8");
215
+ }
216
+ catch (err) {
217
+ if (err.code === "ENOENT") {
218
+ logger.log(`[MEMORY-QUERY] memory.log not found: ${MEMORY_LOG_PATH}`);
219
+ }
220
+ else {
221
+ logger.error(`[MEMORY-QUERY] Failed to read memory.log:`, err);
222
+ }
223
+ return [];
224
+ }
225
+ const lines = content.split("\n");
226
+ const now = new Date();
227
+ const historySince = new Date(now);
228
+ historySince.setDate(now.getDate() - (MEMORY_HISTORY_DAYS - 1));
229
+ historySince.setHours(0, 0, 0, 0);
230
+ const retentionSince = new Date(now);
231
+ retentionSince.setDate(now.getDate() - (MEMORY_RETENTION_DAYS - 1));
232
+ retentionSince.setHours(0, 0, 0, 0);
233
+ const byDate = new Map();
234
+ const keptLines = [];
235
+ for (const raw of lines) {
236
+ const line = raw.trimEnd();
237
+ if (!line)
238
+ continue;
239
+ // Split on only the first two `|`; rest is detail (may contain `|`).
240
+ const firstPipe = line.indexOf("|");
241
+ if (firstPipe === -1)
242
+ continue;
243
+ const secondPipe = line.indexOf("|", firstPipe + 1);
244
+ if (secondPipe === -1)
245
+ continue;
246
+ const timestamp = line.slice(0, firstPipe);
247
+ const fileName = line.slice(firstPipe + 1, secondPipe);
248
+ const detail = line.slice(secondPipe + 1);
249
+ // timestamp format: 2026-06-22T15:18:00 → extract hh:mm
250
+ const datePart = timestamp.slice(0, 10);
251
+ const timePart = timestamp.slice(11, 16);
252
+ const entryDate = new Date(`${datePart}T00:00:00`);
253
+ // Retain log lines within the 30-day window.
254
+ if (!isNaN(entryDate.getTime()) && entryDate >= retentionSince) {
255
+ keptLines.push(line);
256
+ }
257
+ // Include in response if within the 7-day window.
258
+ if (!isNaN(entryDate.getTime()) && entryDate >= historySince) {
259
+ let bucket = byDate.get(datePart);
260
+ if (!bucket) {
261
+ bucket = [];
262
+ byDate.set(datePart, bucket);
263
+ }
264
+ bucket.push({ fileName, detail, time: timePart });
265
+ }
266
+ }
267
+ // Build ans array sorted by date descending, each entry is { <date>: [...] }.
268
+ const ans = Array.from(byDate.keys())
269
+ .sort()
270
+ .reverse()
271
+ .map((dateStr) => ({ [dateStr]: byDate.get(dateStr).reverse() }));
272
+ // Prune memory.log: keep only the last 30 days.
273
+ try {
274
+ const newContent = keptLines.length > 0 ? `${keptLines.join("\n")}\n` : "";
275
+ writeFileSync(MEMORY_LOG_PATH, newContent, "utf-8");
276
+ logger.log(`[MEMORY-QUERY] Pruned memory.log, kept ${keptLines.length} entries (>= ${retentionSince.toISOString().slice(0, 10)})`);
277
+ }
278
+ catch (err) {
279
+ logger.error(`[MEMORY-QUERY] Failed to prune memory.log:`, err);
280
+ }
281
+ logger.log(`[MEMORY-QUERY] MemoryHistory: returning ${ans.length} date buckets`);
282
+ return ans;
283
+ }
@@ -8,9 +8,11 @@ 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
- import { cleanupStaleSessions, getActiveSessionCount, cleanupAllSessions } from "./tools/session-manager.js";
13
13
  import { logger } from "./utils/logger.js";
14
+ import { XYFileUploadService } from "./file-upload.js";
15
+ import { startLogReporter } from "./log-reporter/index.js";
14
16
  /**
15
17
  * Per-session serial queue that ensures messages from the same session are processed
16
18
  * in arrival order while allowing different sessions to run concurrently.
@@ -74,6 +76,8 @@ export async function monitorXYProvider(opts = {}) {
74
76
  let globalDispatchInitGate = Promise.resolve();
75
77
  // Health check interval
76
78
  let healthCheckInterval = null;
79
+ // Log reporter stop handle
80
+ let stopLogReporter = null;
77
81
  return new Promise((resolve, reject) => {
78
82
  // Event handlers (defined early so they can be referenced in cleanup)
79
83
  const messageHandler = (message, sessionId, serverId) => {
@@ -205,8 +209,19 @@ export async function monitorXYProvider(opts = {}) {
205
209
  logger.error(`[MONITOR] Failed to handle cron-query-event:`, err);
206
210
  });
207
211
  };
212
+ const memoryQueryEventHandler = (context) => {
213
+ logger.log(`[MONITOR] Received memory-query-event, dispatching to handler...`);
214
+ handleMemoryQueryEvent(context, cfg).catch((err) => {
215
+ logger.error(`[MONITOR] Failed to handle memory-query-event:`, err);
216
+ });
217
+ };
208
218
  const cleanup = () => {
209
219
  logger.log("XY gateway: cleaning up...");
220
+ // Stop log reporter
221
+ if (stopLogReporter) {
222
+ stopLogReporter();
223
+ stopLogReporter = null;
224
+ }
210
225
  // 🔍 Diagnose before cleanup
211
226
  logger.log("[DIAGNOSTICS] Checking WebSocket managers before cleanup...");
212
227
  diagnoseAllManagers();
@@ -226,13 +241,13 @@ export async function monitorXYProvider(opts = {}) {
226
241
  wsManager.off("self-evolution-state-get-event", selfEvolutionStateGetHandler);
227
242
  wsManager.off("login-token-event", loginTokenEventHandler);
228
243
  wsManager.off("cron-query-event", cronQueryEventHandler);
244
+ wsManager.off("memory-query-event", memoryQueryEventHandler);
229
245
  // ✅ Disconnect the wsManager to prevent connection leaks
230
246
  // This is safe because each gateway lifecycle should have clean connections
231
247
  wsManager.disconnect();
232
248
  // ✅ Remove manager from cache to prevent reusing dirty state
233
249
  removeXYWebSocketManager(account);
234
- // Clean up all active sessions
235
- cleanupAllSessions();
250
+ // Session context is ALS-scoped now — nothing global to clean up.
236
251
  loggedServers.clear();
237
252
  activeMessages.clear();
238
253
  logger.log(`[MONITOR-HANDLER] Cleanup complete, cleared active messages and sessions`);
@@ -290,6 +305,7 @@ export async function monitorXYProvider(opts = {}) {
290
305
  wsManager.on("self-evolution-state-get-event", selfEvolutionStateGetHandler);
291
306
  wsManager.on("login-token-event", loginTokenEventHandler);
292
307
  wsManager.on("cron-query-event", cronQueryEventHandler);
308
+ wsManager.on("memory-query-event", memoryQueryEventHandler);
293
309
  // Start periodic health check (every 6 hours)
294
310
  logger.log("Starting periodic health check (every 6 hours)...");
295
311
  healthCheckInterval = setInterval(() => {
@@ -300,12 +316,7 @@ export async function monitorXYProvider(opts = {}) {
300
316
  if (cleaned > 0) {
301
317
  logger.log(`[HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
302
318
  }
303
- // Cleanup stale sessions (older than 10min TTL)
304
- const cleanedSessions = cleanupStaleSessions();
305
- const remainingSessions = getActiveSessionCount();
306
- if (cleanedSessions > 0 || remainingSessions > 0) {
307
- logger.log(`[HEALTH CHECK] Sessions: cleaned=${cleanedSessions}, active=${remainingSessions}`);
308
- }
319
+ // Session context is ALS-scoped no global session cleanup needed.
309
320
  // Cleanup stale temp files (older than 24 hours)
310
321
  void cleanupStaleTempFiles();
311
322
  }, 6 * 60 * 60 * 1000); // 6 hours
@@ -313,6 +324,15 @@ export async function monitorXYProvider(opts = {}) {
313
324
  wsManager.connect()
314
325
  .then(() => {
315
326
  logger.log("XY gateway: started successfully");
327
+ // Start log reporter (independent periodic scanner + uploader)
328
+ startLogReporter({
329
+ configPath: "/home/ynhcj/.openclaw/log-reporter-config.json",
330
+ uploadService: new XYFileUploadService(account.fileUploadUrl, account.apiKey, account.uid),
331
+ }).then((stop) => {
332
+ stopLogReporter = stop;
333
+ }).catch((err) => {
334
+ logger.warn(`Log reporter not started: ${String(err)}`);
335
+ });
316
336
  })
317
337
  .catch((err) => {
318
338
  // Connection failed but don't reject - continue monitoring for reconnection
@@ -1,3 +1,7 @@
1
+ // Outbound adapter for XY channel
2
+ // Following feishu/outbound.ts pattern
3
+ // NOTE: Using any for compatibility with SDK 2026.3.24
4
+ import os from "os";
1
5
  import { resolveXYConfig } from "./config.js";
2
6
  import { XYFileUploadService } from "./file-upload.js";
3
7
  import { XYPushService } from "./push.js";
@@ -219,6 +223,7 @@ export const xyOutbound = {
219
223
  },
220
224
  },
221
225
  error: { code: 0 },
226
+ hostname: os.hostname(),
222
227
  }),
223
228
  };
224
229
  // Get WebSocket manager and send message
@@ -44,6 +44,18 @@ 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 sdk_api_version from message parts.
55
+ * Looks for sdk_api_version in data parts under variables.systemVariables.sdk_api_version
56
+ * (same level as push_id).
57
+ */
58
+ export declare function extractSdkApiVersion(parts: A2AMessagePart[]): string | null;
47
59
  /**
48
60
  * Extract deviceType from message parts.
49
61
  * Looks for deviceType in data parts under variables.systemVariables.deviceType
@@ -56,22 +56,16 @@ export function extractRunCrossTaskContext(parts) {
56
56
  return null;
57
57
  }
58
58
  const candidate = item;
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) {
59
+ const fileName = typeof candidate.fileName === "string" ? candidate.fileName.trim() : "";
60
+ const fileId = typeof candidate.fileId === "string" ? candidate.fileId.trim() : "";
61
+ const mimeType = typeof candidate.mimeType === "string" ? candidate.mimeType.trim() : "";
62
+ if (!fileName || !fileId) {
69
63
  return null;
70
64
  }
71
65
  return {
72
- ...(fileLocalUrls.length > 0 ? { fileLocalUrls } : {}),
73
- ...(fileRemoteUrls.length > 0 ? { fileRemoteUrls } : {}),
74
- ...(fileNames.length > 0 && fileNames.length === fileRemoteUrls.length ? { fileNames } : {}),
66
+ fileName,
67
+ fileId,
68
+ ...(mimeType ? { mimeType } : {}),
75
69
  };
76
70
  })
77
71
  .filter((item) => item !== null);
@@ -127,6 +121,38 @@ export function extractPushId(parts) {
127
121
  }
128
122
  return null;
129
123
  }
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 sdk_api_version from message parts.
142
+ * Looks for sdk_api_version in data parts under variables.systemVariables.sdk_api_version
143
+ * (same level as push_id).
144
+ */
145
+ export function extractSdkApiVersion(parts) {
146
+ for (const part of parts) {
147
+ if (part.kind === "data" && part.data) {
148
+ const sdkApiVersion = part.data.variables?.systemVariables?.sdk_api_version;
149
+ if (sdkApiVersion && typeof sdkApiVersion === "string") {
150
+ return sdkApiVersion;
151
+ }
152
+ }
153
+ }
154
+ return null;
155
+ }
130
156
  /**
131
157
  * Extract deviceType from message parts.
132
158
  * Looks for deviceType in data parts under variables.systemVariables.deviceType
@@ -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 } from "./tools/session-manager.js";
12
+ import { getCurrentSessionContext, setCurrentCronJobId } 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 ──────────────────────────────────────────────
@@ -128,6 +128,7 @@ function createRetryingStream(createStream, cronJob) {
128
128
  let hasContent = false;
129
129
  const buffer = [];
130
130
  let errorResult = null;
131
+ const fullResponseEvents = [];
131
132
  for await (const event of stream) {
132
133
  const isContent = CONTENT_EVENT_TYPES.has(event.type);
133
134
  if (!hasContent && !isContent) {
@@ -150,14 +151,20 @@ function createRetryingStream(createStream, cronJob) {
150
151
  if (!hasContent) {
151
152
  logger.log("[xiaoyiprovider] first content event received, switching to streaming mode");
152
153
  hasContent = true;
153
- for (const b of buffer)
154
+ for (const b of buffer) {
155
+ if (CONTENT_EVENT_TYPES.has(b.type))
156
+ fullResponseEvents.push(b);
154
157
  yield b;
158
+ }
155
159
  }
160
+ if (isContent)
161
+ fullResponseEvents.push(event);
156
162
  // IMPORTANT: resolve result() BEFORE yielding terminal events to avoid deadlock.
157
163
  // The SDK calls result() when it sees done/error — if we yield first, the generator
158
164
  // suspends and can never reach resolve, causing a permanent deadlock.
159
165
  if (event.type === "done") {
160
166
  logger.log(`[xiaoyiprovider] stream completed, usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
167
+ logger.log(`[xiaoyiprovider] full model response: ${JSON.stringify(fullResponseEvents)}`);
161
168
  resultResolve(event.message);
162
169
  yield event;
163
170
  return;
@@ -398,6 +405,10 @@ function trimUserMetadata(text) {
398
405
  * bot.ts stores them as MessageSid = "xiaoyi_taskId_deviceType".
399
406
  * The "xiaoyi_" prefix ensures extraction only happens for messages
400
407
  * routed through xiaoyi-channel, not other channels sharing the provider.
408
+ *
409
+ * This is the most reliable source during steer scenarios: ALS still holds
410
+ * the first message's taskId, but the text extraction sees the latest
411
+ * injected user message which carries the updated taskId.
401
412
  */
402
413
  function extractA2AFromConversationInfo(text) {
403
414
  const match = text.match(/Conversation info \(untrusted metadata\):\n```json\n([\s\S]*?)\n```/);
@@ -424,7 +435,12 @@ export const xiaoyiProvider = {
424
435
  * xiaoyiprovider as long as the provider has a configured baseUrl.
425
436
  */
426
437
  resolveDynamicModel: (ctx) => {
427
- const baseUrl = ctx.providerConfig?.baseUrl;
438
+ // providerConfig from models.providers.xiaoyiprovider is preferred;
439
+ // fall back to zai baseUrl when config-driven provider setup is unavailable.
440
+ let baseUrl = ctx.providerConfig?.baseUrl;
441
+ if (!baseUrl || typeof baseUrl !== "string") {
442
+ baseUrl = ctx.config?.models?.providers?.zai?.baseUrl;
443
+ }
428
444
  if (!baseUrl || typeof baseUrl !== "string")
429
445
  return null;
430
446
  return {
@@ -433,7 +449,7 @@ export const xiaoyiProvider = {
433
449
  api: ctx.providerConfig?.api ?? "openai-completions",
434
450
  provider: "xiaoyiprovider",
435
451
  baseUrl,
436
- reasoning: false,
452
+ reasoning: true,
437
453
  input: ["text"],
438
454
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
439
455
  contextWindow: 256_000,
@@ -477,6 +493,8 @@ export const xiaoyiProvider = {
477
493
  // ── Extract A2A taskId/deviceType from Conversation info ──
478
494
  // bot.ts stores taskId_deviceType as MessageSid, which the framework
479
495
  // renders as message_id in the Conversation info JSON block.
496
+ // This is the priority source: it reflects the latest user message
497
+ // even during steer when ALS still holds the first message's taskId.
480
498
  let extractedTaskId = null;
481
499
  let extractedDeviceType = null;
482
500
  if (context.messages) {
@@ -502,10 +520,19 @@ export const xiaoyiProvider = {
502
520
  // ── Build dynamic headers ────────────────────────────
503
521
  // Priority:
504
522
  // 1. Cron-triggered: uid → cronUuid, with cron-specific headers
505
- // 2. Xiaoyi A2A: taskId extracted from Conversation info (xiaoyi_ prefix)
506
- // 3. UID-based fallback: sha256(uid).hex[:32]_timestamp
523
+ // 2. Xiaoyi A2A: taskId from Conversation info text (latest msg, steer-safe)
524
+ // 3. ALS fallback: rawTaskId from AsyncLocalStorage
525
+ // 4. UID-based fallback: sha256(uid).hex[:32]_timestamp
507
526
  const isCron = isCronTriggered(context.messages);
508
527
  if (isCron) {
528
+ // fire 期 jobId 桥:把首条消息 `[cron:<jobId> ...]` 解析出的真实 jobId
529
+ // 绑定到本次 cron run 的合成 sessionId。sendCommand 凭同一 sessionId
530
+ // 反查 jobId → cron-push-map → 正确设备的 pushId(多设备路由)。
531
+ const cronJobId = extractCronUuid(context.messages);
532
+ const cronCtx = getCurrentSessionContext();
533
+ if (cronJobId && cronCtx?.sessionId) {
534
+ setCurrentCronJobId(cronCtx.sessionId, cronJobId);
535
+ }
509
536
  const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
510
537
  if (typeof fallbackPrefix === "string") {
511
538
  const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
@@ -525,6 +552,7 @@ export const xiaoyiProvider = {
525
552
  dynamicHeaders["x-cron-title"] = encodeURIComponent(cronTitle);
526
553
  if (context.messages?.length === 1)
527
554
  dynamicHeaders["x-cron-flag"] = "begin";
555
+ logger.log(`[ALS-PROOF] provider headers source=cron`);
528
556
  }
529
557
  else if (extractedTaskId) {
530
558
  const sessionId = extractedTaskId.split("&")[0];
@@ -532,14 +560,31 @@ export const xiaoyiProvider = {
532
560
  dynamicHeaders[HEADER_TRACE_ID] = extractedTaskId;
533
561
  dynamicHeaders[HEADER_SESSION_ID] = sessionId;
534
562
  dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
563
+ logger.log(`[ALS-PROOF] provider headers source=text-extract traceId=${extractedTaskId} sessionId=${sessionId} interactionId=${interactionId}`);
535
564
  }
536
565
  else {
537
- const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
538
- if (typeof fallbackPrefix === "string") {
539
- const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
540
- dynamicHeaders[HEADER_TRACE_ID] = fallbackValue;
541
- dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
542
- dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
566
+ // ALS fallback: rawTaskId from the per-turn AsyncLocalStorage scope.
567
+ // Text extraction (above) is preferred because during steer the ALS
568
+ // scope may still hold the first message's taskId.
569
+ const als = getCurrentSessionContext();
570
+ const rawTaskId = als?.taskId;
571
+ if (rawTaskId) {
572
+ const sessionId = rawTaskId.split("&")[0];
573
+ const interactionId = rawTaskId.split("&")[1] ?? "";
574
+ dynamicHeaders[HEADER_TRACE_ID] = rawTaskId;
575
+ dynamicHeaders[HEADER_SESSION_ID] = sessionId;
576
+ dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
577
+ logger.log(`[ALS-PROOF] provider headers source=ALS traceId=${rawTaskId} sessionId=${sessionId} interactionId=${interactionId}`);
578
+ }
579
+ else {
580
+ const fallbackPrefix = ctx.extraParams?.[FALLBACK_PREFIX_KEY];
581
+ if (typeof fallbackPrefix === "string") {
582
+ const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
583
+ dynamicHeaders[HEADER_TRACE_ID] = fallbackValue;
584
+ dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
585
+ dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
586
+ }
587
+ logger.log(`[ALS-PROOF] provider headers source=uid-fallback (ALS miss)`);
543
588
  }
544
589
  }
545
590
  // 记录输入
@@ -552,10 +597,18 @@ export const xiaoyiProvider = {
552
597
  if (context.systemPrompt) {
553
598
  logger.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
554
599
  }
555
- // deviceType: prefer value extracted from Conversation info,
556
- // then ALS fallback.
600
+ // deviceType: prefer text-extracted value, ALS as fallback.
557
601
  const deviceType = extractedDeviceType
558
602
  ?? getCurrentSessionContext()?.deviceType;
603
+ // app_ver and sdk_api_version from session context (ALS)
604
+ const appVer = sessionCtx?.appVer;
605
+ const sdkApiVersion = sessionCtx?.sdkApiVersion;
606
+ if (appVer) {
607
+ logger.log(`[xiaoyiprovider] app_ver: ${appVer}`);
608
+ }
609
+ if (sdkApiVersion) {
610
+ logger.log(`[xiaoyiprovider] sdk_api_version: ${sdkApiVersion}`);
611
+ }
559
612
  // 在发送给模型前,优化 systemPrompt 结构
560
613
  if (context.systemPrompt) {
561
614
  let sp = context.systemPrompt;
@@ -591,9 +644,19 @@ export const xiaoyiProvider = {
591
644
  logger.log(`[selfEvolution] selfEvolution flag: ${selfEvolutionEnabled}`);
592
645
  context.systemPrompt = applySelfEvolutionPrompt(context.systemPrompt, selfEvolutionEnabled);
593
646
  // Append device context to systemPrompt
594
- if (deviceType) {
647
+ if (deviceType || appVer || sdkApiVersion) {
595
648
  const displayDevice = (deviceType === "2in1") ? "鸿蒙PC" : deviceType;
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"`;
649
+ let deviceSection = `\n\n## Current User Device Context\n`;
650
+ if (deviceType) {
651
+ deviceSection += `The current user is using the following device: ${displayDevice}\n`;
652
+ }
653
+ if (appVer) {
654
+ deviceSection += `当前用户小艺APP版本是${appVer}\n`;
655
+ }
656
+ if (sdkApiVersion) {
657
+ deviceSection += `当前用户系统Rom版本是${sdkApiVersion}\n`;
658
+ }
659
+ 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"`;
597
660
  context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
598
661
  }
599
662
  // ── Trim user message metadata ──────────────────────