@ynhcj/xiaoyi-channel 1.0.0 → 1.0.2

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/src/bot.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
3
- import { parseA2AMessage, extractTextFromParts, extractFileParts } from "./parser.js";
3
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
4
4
  import { downloadFilesFromParts } from "./file-download.js";
5
5
  import { resolveXYConfig } from "./config.js";
6
6
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
7
  import { registerSession, unregisterSession } from "./tools/session-manager.js";
8
+ import { configManager } from "./utils/config-manager.js";
8
9
  /**
9
10
  * Handle an incoming A2A message.
10
11
  * This is the main entry point for message processing.
@@ -55,6 +56,18 @@ export async function handleXYMessage(params) {
55
56
  }
56
57
  // Parse the A2A message (for regular messages)
57
58
  const parsed = parseA2AMessage(message);
59
+ // Extract and update push_id if present
60
+ const pushId = extractPushId(parsed.parts);
61
+ if (pushId) {
62
+ log(`[BOT] 📌 Extracted push_id from user message`);
63
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
64
+ log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
65
+ log(`[BOT] - Full push_id: ${pushId}`);
66
+ configManager.updatePushId(parsed.sessionId, pushId);
67
+ }
68
+ else {
69
+ log(`[BOT] â„šī¸ No push_id found in message, will use config default`);
70
+ }
58
71
  // Resolve configuration (needed for status updates)
59
72
  const config = resolveXYConfig(cfg);
60
73
  // ✅ Resolve agent route (following feishu pattern)
@@ -99,7 +112,7 @@ export async function handleXYMessage(params) {
99
112
  messageBody = `${speaker}: ${messageBody}`;
100
113
  // Format agent envelope (following feishu pattern)
101
114
  const body = core.channel.reply.formatAgentEnvelope({
102
- channel: "XY",
115
+ channel: "xiaoyi-channel",
103
116
  from: speaker,
104
117
  timestamp: new Date(),
105
118
  envelope: envelopeOptions,
@@ -5,6 +5,7 @@ import { xyOnboardingAdapter } from "./onboarding.js";
5
5
  import { locationTool } from "./tools/location-tool.js";
6
6
  import { noteTool } from "./tools/note-tool.js";
7
7
  import { searchNoteTool } from "./tools/search-note-tool.js";
8
+ import { calendarTool } from "./tools/calendar-tool.js";
8
9
  /**
9
10
  * Xiaoyi Channel Plugin for OpenClaw.
10
11
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -43,7 +44,7 @@ export const xyPlugin = {
43
44
  },
44
45
  outbound: xyOutbound,
45
46
  onboarding: xyOnboardingAdapter,
46
- agentTools: [locationTool, noteTool, searchNoteTool],
47
+ agentTools: [locationTool, noteTool, searchNoteTool, calendarTool],
47
48
  messaging: {
48
49
  normalizeTarget: (raw) => {
49
50
  const trimmed = raw.trim();
@@ -76,8 +76,8 @@ export const xyOutbound = {
76
76
  const pushService = new XYPushService(config);
77
77
  // Extract title (first 57 chars or first line)
78
78
  const title = text.split("\n")[0].slice(0, 57);
79
- // Send push message
80
- await pushService.sendPush(text, title, actualTo);
79
+ // Send push message (content, title, data, sessionId)
80
+ await pushService.sendPush(text, title, undefined, actualTo);
81
81
  console.log(`[xyOutbound.sendText] Completed successfully`);
82
82
  // Return message info
83
83
  return {
@@ -38,6 +38,11 @@ export declare function isClearContextMessage(method: string): boolean;
38
38
  * Check if message is a tasks/cancel request.
39
39
  */
40
40
  export declare function isTasksCancelMessage(method: string): boolean;
41
+ /**
42
+ * Extract push_id from message parts.
43
+ * Looks for push_id in data parts under variables.systemVariables.push_id
44
+ */
45
+ export declare function extractPushId(parts: A2AMessagePart[]): string | null;
41
46
  /**
42
47
  * Validate A2A request structure.
43
48
  */
@@ -57,6 +57,21 @@ export function isClearContextMessage(method) {
57
57
  export function isTasksCancelMessage(method) {
58
58
  return method === "tasks/cancel" || method === "tasks_cancel";
59
59
  }
60
+ /**
61
+ * Extract push_id from message parts.
62
+ * Looks for push_id in data parts under variables.systemVariables.push_id
63
+ */
64
+ export function extractPushId(parts) {
65
+ for (const part of parts) {
66
+ if (part.kind === "data" && part.data) {
67
+ const pushId = part.data.variables?.systemVariables?.push_id;
68
+ if (pushId && typeof pushId === "string") {
69
+ return pushId;
70
+ }
71
+ }
72
+ }
73
+ return null;
74
+ }
60
75
  /**
61
76
  * Validate A2A request structure.
62
77
  */
@@ -5,11 +5,17 @@ import type { XYChannelConfig } from "./types.js";
5
5
  */
6
6
  export declare class XYPushService {
7
7
  private config;
8
+ private readonly DEFAULT_PUSH_URL;
9
+ private readonly REQUEST_FROM;
8
10
  constructor(config: XYChannelConfig);
11
+ /**
12
+ * Generate a random trace ID for request tracking.
13
+ */
14
+ private generateTraceId;
9
15
  /**
10
16
  * Send a push message to a user session.
11
17
  */
12
- sendPush(content: string, title: string, sessionId?: string): Promise<void>;
18
+ sendPush(content: string, title: string, data?: Record<string, any>, sessionId?: string): Promise<void>;
13
19
  /**
14
20
  * Send a push message with file attachments.
15
21
  */
package/dist/src/push.js CHANGED
@@ -1,46 +1,104 @@
1
1
  // Push message service for scheduled tasks
2
2
  import fetch from "node-fetch";
3
+ import { randomUUID } from "crypto";
3
4
  import { logger } from "./utils/logger.js";
5
+ import { configManager } from "./utils/config-manager.js";
4
6
  /**
5
7
  * Service for sending push messages to users.
6
8
  * Used for outbound messages and scheduled tasks.
7
9
  */
8
10
  export class XYPushService {
9
11
  config;
12
+ DEFAULT_PUSH_URL = "https://hag.cloud.huawei.com/open-ability-agent/v1/agent-webhook";
13
+ REQUEST_FROM = "openclaw";
10
14
  constructor(config) {
11
15
  this.config = config;
12
16
  }
17
+ /**
18
+ * Generate a random trace ID for request tracking.
19
+ */
20
+ generateTraceId() {
21
+ return `trace-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
22
+ }
13
23
  /**
14
24
  * Send a push message to a user session.
15
25
  */
16
- async sendPush(content, title, sessionId) {
17
- const pushUrl = this.config.pushUrl || `${this.config.fileUploadUrl}/push`;
18
- logger.debug(`Sending push message: title="${title}"`);
26
+ async sendPush(content, title, data, sessionId) {
27
+ const pushUrl = this.config.pushUrl || this.DEFAULT_PUSH_URL;
28
+ const traceId = this.generateTraceId();
29
+ // Get dynamic pushId for the session (falls back to config pushId)
30
+ const dynamicPushId = configManager.getPushId(sessionId);
31
+ const pushId = dynamicPushId || this.config.pushId;
32
+ logger.log(`[PUSH] 📤 Preparing to send push message`);
33
+ logger.log(`[PUSH] - Title: "${title}"`);
34
+ logger.log(`[PUSH] - Content length: ${content.length} chars`);
35
+ logger.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
36
+ logger.log(`[PUSH] - Trace ID: ${traceId}`);
37
+ logger.log(`[PUSH] - Push URL: ${pushUrl}`);
38
+ if (dynamicPushId) {
39
+ logger.log(`[PUSH] - Using dynamic pushId (from session): ${pushId.substring(0, 20)}...`);
40
+ logger.log(`[PUSH] - Full dynamic pushId: ${pushId}`);
41
+ }
42
+ else {
43
+ logger.log(`[PUSH] - Using config pushId (fallback): ${pushId.substring(0, 20)}...`);
44
+ logger.log(`[PUSH] - Full config pushId: ${pushId}`);
45
+ }
46
+ logger.log(`[PUSH] - API ID: ${this.config.apiId}`);
47
+ logger.log(`[PUSH] - UID: ${this.config.uid}`);
19
48
  try {
20
- const request = {
21
- apiKey: this.config.apiKey,
22
- apiId: this.config.apiId,
23
- pushId: this.config.pushId,
24
- sessionId: sessionId || "default",
25
- title,
26
- content,
49
+ const requestBody = {
50
+ jsonrpc: "2.0",
51
+ id: randomUUID(),
52
+ result: {
53
+ id: randomUUID(),
54
+ apiId: this.config.apiId,
55
+ pushId: pushId, // Use dynamic pushId
56
+ pushText: title,
57
+ kind: "task",
58
+ artifacts: [
59
+ {
60
+ artifactId: randomUUID(),
61
+ parts: [
62
+ {
63
+ kind: "data",
64
+ data: data || { content },
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ },
27
70
  };
71
+ logger.debug(`[PUSH] Full request body:`, JSON.stringify(requestBody, null, 2));
28
72
  const response = await fetch(pushUrl, {
29
73
  method: "POST",
30
74
  headers: {
31
75
  "Content-Type": "application/json",
76
+ "Accept": "application/json",
77
+ "x-hag-trace-id": traceId,
78
+ "x-uid": this.config.uid,
32
79
  "x-api-key": this.config.apiKey,
33
- "x-request-from": "openclaw",
80
+ "x-request-from": this.REQUEST_FROM,
34
81
  },
35
- body: JSON.stringify(request),
82
+ body: JSON.stringify(requestBody),
36
83
  });
37
84
  if (!response.ok) {
38
- throw new Error(`Push failed: HTTP ${response.status}`);
85
+ const errorText = await response.text();
86
+ logger.error(`[PUSH] ❌ Push request failed`);
87
+ logger.error(`[PUSH] - HTTP Status: ${response.status}`);
88
+ logger.error(`[PUSH] - Error: ${errorText}`);
89
+ throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
39
90
  }
40
- logger.log(`Push message sent successfully: "${title}"`);
91
+ const result = await response.json();
92
+ logger.log(`[PUSH] ✅ Push message sent successfully`);
93
+ logger.log(`[PUSH] - Title: "${title}"`);
94
+ logger.log(`[PUSH] - Trace ID: ${traceId}`);
95
+ logger.log(`[PUSH] - Used pushId: ${pushId.substring(0, 20)}...`);
96
+ logger.debug(`[PUSH] - Response:`, result);
41
97
  }
42
98
  catch (error) {
43
- logger.error("Failed to send push message:", error);
99
+ logger.error(`[PUSH] ❌ Failed to send push message`);
100
+ logger.error(`[PUSH] - Trace ID: ${traceId}`);
101
+ logger.error(`[PUSH] - Error:`, error);
44
102
  throw error;
45
103
  }
46
104
  }
@@ -48,8 +106,10 @@ export class XYPushService {
48
106
  * Send a push message with file attachments.
49
107
  */
50
108
  async sendPushWithFiles(content, title, fileIds, sessionId) {
51
- // Build content with file references
52
- const contentWithFiles = `${content}\n\n[文äģļ: ${fileIds.join(", ")}]`;
53
- await this.sendPush(contentWithFiles, title, sessionId);
109
+ const data = {
110
+ content,
111
+ fileIds,
112
+ };
113
+ await this.sendPush(content, title, data, sessionId);
54
114
  }
55
115
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * XY calendar event tool - creates a calendar event on user's device.
3
+ * Requires title, dtStart (start time), and dtEnd (end time) parameters.
4
+ * Time format must be: yyyy-mm-dd hh:mm:ss
5
+ */
6
+ export declare const calendarTool: any;
@@ -0,0 +1,167 @@
1
+ import { getXYWebSocketManager } from "../client.js";
2
+ import { sendCommand } from "../formatter.js";
3
+ import { getLatestSessionContext } from "./session-manager.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * XY calendar event tool - creates a calendar event on user's device.
7
+ * Requires title, dtStart (start time), and dtEnd (end time) parameters.
8
+ * Time format must be: yyyy-mm-dd hh:mm:ss
9
+ */
10
+ export const calendarTool = {
11
+ name: "create_calendar_event",
12
+ label: "Create Calendar Event",
13
+ description: "åœ¨į”¨æˆˇčŽžå¤‡ä¸Šåˆ›åģēæ—Ĩį¨‹ã€‚éœ€čĻæäž›æ—Ĩį¨‹æ ‡éĸ˜ã€åŧ€å§‹æ—ļ间和į쓿Ÿæ—ļ间。æ—ļ间æ ŧåŧåŋ…éĄģä¸ēīŧšyyyy-mm-dd hh:mm:ssīŧˆäž‹åĻ‚īŧš2024-01-15 14:30:00īŧ‰",
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ title: {
18
+ type: "string",
19
+ description: "æ—Ĩį¨‹æ ‡éĸ˜/åį§°",
20
+ },
21
+ dtStart: {
22
+ type: "string",
23
+ description: "æ—Ĩፋåŧ€å§‹æ—ļ间īŧŒæ ŧåŧåŋ…éĄģä¸ēīŧšyyyy-mm-dd hh:mm:ssīŧˆäž‹åĻ‚īŧš2024-01-15 14:30:00īŧ‰",
24
+ },
25
+ dtEnd: {
26
+ type: "string",
27
+ description: "æ—Ĩፋį쓿Ÿæ—ļ间īŧŒæ ŧåŧåŋ…éĄģä¸ēīŧšyyyy-mm-dd hh:mm:ssīŧˆäž‹åĻ‚īŧš2024-01-15 17:30:00īŧ‰",
28
+ },
29
+ },
30
+ required: ["title", "dtStart", "dtEnd"],
31
+ },
32
+ async execute(toolCallId, params) {
33
+ logger.log(`[CALENDAR_TOOL] 🚀 Starting execution`);
34
+ logger.log(`[CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
35
+ logger.log(`[CALENDAR_TOOL] - params:`, JSON.stringify(params));
36
+ logger.log(`[CALENDAR_TOOL] - timestamp: ${new Date().toISOString()}`);
37
+ // Validate parameters
38
+ if (!params.title || !params.dtStart || !params.dtEnd) {
39
+ logger.error(`[CALENDAR_TOOL] ❌ Missing required parameters`);
40
+ throw new Error("Missing required parameters: title, dtStart, and dtEnd are required");
41
+ }
42
+ // Convert time strings to millisecond timestamps
43
+ logger.log(`[CALENDAR_TOOL] 🕒 Converting time strings to timestamps...`);
44
+ logger.log(`[CALENDAR_TOOL] - dtStart input: ${params.dtStart}`);
45
+ logger.log(`[CALENDAR_TOOL] - dtEnd input: ${params.dtEnd}`);
46
+ const dtStartMs = new Date(params.dtStart).getTime();
47
+ const dtEndMs = new Date(params.dtEnd).getTime();
48
+ if (isNaN(dtStartMs) || isNaN(dtEndMs)) {
49
+ logger.error(`[CALENDAR_TOOL] ❌ Invalid time format`);
50
+ throw new Error("Invalid time format. Required format: yyyy-mm-dd hh:mm:ss (e.g., 2024-01-15 14:30:00)");
51
+ }
52
+ logger.log(`[CALENDAR_TOOL] ✅ Time conversion successful`);
53
+ logger.log(`[CALENDAR_TOOL] - dtStart timestamp: ${dtStartMs}`);
54
+ logger.log(`[CALENDAR_TOOL] - dtEnd timestamp: ${dtEndMs}`);
55
+ // Get session context
56
+ logger.log(`[CALENDAR_TOOL] 🔍 Attempting to get session context...`);
57
+ const sessionContext = getLatestSessionContext();
58
+ if (!sessionContext) {
59
+ logger.error(`[CALENDAR_TOOL] ❌ FAILED: No active session found!`);
60
+ logger.error(`[CALENDAR_TOOL] - toolCallId: ${toolCallId}`);
61
+ throw new Error("No active XY session found. Calendar tool can only be used during an active conversation.");
62
+ }
63
+ logger.log(`[CALENDAR_TOOL] ✅ Session context found`);
64
+ logger.log(`[CALENDAR_TOOL] - sessionId: ${sessionContext.sessionId}`);
65
+ logger.log(`[CALENDAR_TOOL] - taskId: ${sessionContext.taskId}`);
66
+ logger.log(`[CALENDAR_TOOL] - messageId: ${sessionContext.messageId}`);
67
+ const { config, sessionId, taskId, messageId } = sessionContext;
68
+ // Get WebSocket manager
69
+ logger.log(`[CALENDAR_TOOL] 🔌 Getting WebSocket manager...`);
70
+ const wsManager = getXYWebSocketManager(config);
71
+ logger.log(`[CALENDAR_TOOL] ✅ WebSocket manager obtained`);
72
+ // Build CreateCalendarEvent command
73
+ logger.log(`[CALENDAR_TOOL] đŸ“Ļ Building CreateCalendarEvent command...`);
74
+ const command = {
75
+ header: {
76
+ namespace: "Common",
77
+ name: "Action",
78
+ },
79
+ payload: {
80
+ cardParam: {},
81
+ executeParam: {
82
+ executeMode: "background",
83
+ intentName: "CreateCalendarEvent",
84
+ bundleName: "com.huawei.hmos.calendardata",
85
+ dimension: "",
86
+ needUnlock: true,
87
+ actionResponse: true,
88
+ timeOut: 5,
89
+ intentParam: {
90
+ title: params.title,
91
+ dtStart: dtStartMs,
92
+ dtEnd: dtEndMs,
93
+ },
94
+ achieveType: "INTENT",
95
+ },
96
+ responses: [
97
+ {
98
+ resultCode: "",
99
+ displayText: "",
100
+ ttsText: "",
101
+ },
102
+ ],
103
+ needUploadResult: true,
104
+ noHalfPage: false,
105
+ pageControlRelated: false,
106
+ },
107
+ };
108
+ // Send command and wait for response (5 second timeout)
109
+ logger.log(`[CALENDAR_TOOL] âŗ Setting up promise to wait for calendar event response...`);
110
+ logger.log(`[CALENDAR_TOOL] - Timeout: 5 seconds`);
111
+ return new Promise((resolve, reject) => {
112
+ const timeout = setTimeout(() => {
113
+ logger.error(`[CALENDAR_TOOL] ⏰ Timeout: No response received within 5 seconds`);
114
+ wsManager.off("data-event", handler);
115
+ reject(new Error("创åģēæ—Ĩፋčļ…æ—ļīŧˆ15į§’īŧ‰"));
116
+ }, 15000);
117
+ // Listen for data events from WebSocket
118
+ const handler = (event) => {
119
+ logger.log(`[CALENDAR_TOOL] 📨 Received data event:`, JSON.stringify(event));
120
+ if (event.intentName === "CreateCalendarEvent") {
121
+ logger.log(`[CALENDAR_TOOL] đŸŽ¯ CreateCalendarEvent event received`);
122
+ logger.log(`[CALENDAR_TOOL] - status: ${event.status}`);
123
+ clearTimeout(timeout);
124
+ wsManager.off("data-event", handler);
125
+ if (event.status === "success" && event.outputs) {
126
+ logger.log(`[CALENDAR_TOOL] ✅ Calendar event created successfully`);
127
+ logger.log(`[CALENDAR_TOOL] - outputs:`, JSON.stringify(event.outputs));
128
+ resolve({
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: JSON.stringify(event.outputs),
133
+ },
134
+ ],
135
+ });
136
+ }
137
+ else {
138
+ logger.error(`[CALENDAR_TOOL] ❌ Calendar event creation failed`);
139
+ logger.error(`[CALENDAR_TOOL] - status: ${event.status}`);
140
+ reject(new Error(`创åģēæ—Ĩį¨‹å¤ąč´Ĩ: ${event.status}`));
141
+ }
142
+ }
143
+ };
144
+ // Register event handler
145
+ logger.log(`[CALENDAR_TOOL] 📡 Registering data-event handler on WebSocket manager`);
146
+ wsManager.on("data-event", handler);
147
+ // Send the command
148
+ logger.log(`[CALENDAR_TOOL] 📤 Sending CreateCalendarEvent command...`);
149
+ sendCommand({
150
+ config,
151
+ sessionId,
152
+ taskId,
153
+ messageId,
154
+ command,
155
+ })
156
+ .then(() => {
157
+ logger.log(`[CALENDAR_TOOL] ✅ Command sent successfully, waiting for response...`);
158
+ })
159
+ .catch((error) => {
160
+ logger.error(`[CALENDAR_TOOL] ❌ Failed to send command:`, error);
161
+ clearTimeout(timeout);
162
+ wsManager.off("data-event", handler);
163
+ reject(error);
164
+ });
165
+ });
166
+ },
167
+ };
@@ -76,8 +76,8 @@ export const locationTool = {
76
76
  const timeout = setTimeout(() => {
77
77
  logger.error(`[LOCATION_TOOL] ⏰ Timeout: No response received within 5 seconds`);
78
78
  wsManager.off("data-event", handler);
79
- reject(new Error("čŽˇå–äŊįŊŽčļ…æ—ļīŧˆ5į§’īŧ‰"));
80
- }, 5000);
79
+ reject(new Error("čŽˇå–äŊįŊŽčļ…æ—ļīŧˆ10į§’īŧ‰"));
80
+ }, 10000);
81
81
  // Listen for data events from WebSocket
82
82
  const handler = (event) => {
83
83
  logger.log(`[LOCATION_TOOL] 📨 Received data event:`, JSON.stringify(event));
@@ -76,8 +76,8 @@ export const noteTool = {
76
76
  return new Promise((resolve, reject) => {
77
77
  const timeout = setTimeout(() => {
78
78
  wsManager.off("data-event", handler);
79
- reject(new Error("创åģē备åŋ˜åŊ•čļ…æ—ļīŧˆ5į§’īŧ‰"));
80
- }, 5000);
79
+ reject(new Error("创åģē备åŋ˜åŊ•čļ…æ—ļīŧˆ10į§’īŧ‰"));
80
+ }, 10000);
81
81
  // Listen for data events from WebSocket
82
82
  const handler = (event) => {
83
83
  logger.debug("Received data event:", event);
@@ -71,8 +71,8 @@ export const searchNoteTool = {
71
71
  return new Promise((resolve, reject) => {
72
72
  const timeout = setTimeout(() => {
73
73
  wsManager.off("data-event", handler);
74
- reject(new Error("搜į´ĸ备åŋ˜åŊ•čļ…æ—ļīŧˆ5į§’īŧ‰"));
75
- }, 5000);
74
+ reject(new Error("搜į´ĸ备åŋ˜åŊ•čļ…æ—ļīŧˆ15į§’īŧ‰"));
75
+ }, 15000);
76
76
  // Listen for data events from WebSocket
77
77
  const handler = (event) => {
78
78
  logger.debug("Received data event:", event);
@@ -1,4 +1,5 @@
1
1
  import { logger } from "../utils/logger.js";
2
+ import { configManager } from "../utils/config-manager.js";
2
3
  // Map of sessionKey -> SessionContext
3
4
  const activeSessions = new Map();
4
5
  /**
@@ -24,7 +25,13 @@ export function unregisterSession(sessionKey) {
24
25
  logger.log(`[SESSION_MANAGER] đŸ—‘ī¸ Unregistering session: ${sessionKey}`);
25
26
  logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
26
27
  logger.log(`[SESSION_MANAGER] - Session existed: ${activeSessions.has(sessionKey)}`);
28
+ // Get session context before deleting to clear associated pushId
29
+ const context = activeSessions.get(sessionKey);
27
30
  const existed = activeSessions.delete(sessionKey);
31
+ // Clear cached pushId for this session
32
+ if (context) {
33
+ configManager.clearSession(context.sessionId);
34
+ }
28
35
  logger.log(`[SESSION_MANAGER] - Deleted: ${existed}`);
29
36
  logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
30
37
  logger.log(`[SESSION_MANAGER] - Remaining session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
@@ -155,14 +155,6 @@ export interface FileUploadCompleteRequest {
155
155
  export interface FileUploadCompleteResponse {
156
156
  fileId: string;
157
157
  }
158
- export interface PushMessageRequest {
159
- apiKey: string;
160
- apiId: string;
161
- pushId: string;
162
- sessionId: string;
163
- title: string;
164
- content: string;
165
- }
166
158
  export type ServerIdentifier = "server1" | "server2";
167
159
  export interface SessionBinding {
168
160
  sessionId: string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Manages dynamic configuration updates that can change at runtime.
3
+ * Specifically handles pushId which can be updated per-session.
4
+ */
5
+ declare class ConfigManager {
6
+ private sessionPushIds;
7
+ private globalPushId;
8
+ /**
9
+ * Update push ID for a specific session.
10
+ */
11
+ updatePushId(sessionId: string, pushId: string): void;
12
+ /**
13
+ * Get push ID for a session (falls back to global if not found).
14
+ */
15
+ getPushId(sessionId?: string): string | null;
16
+ /**
17
+ * Clear push ID for a session.
18
+ */
19
+ clearSession(sessionId: string): void;
20
+ /**
21
+ * Clear all cached push IDs.
22
+ */
23
+ clear(): void;
24
+ }
25
+ export declare const configManager: ConfigManager;
26
+ export {};
@@ -0,0 +1,56 @@
1
+ // Dynamic configuration manager for runtime updates
2
+ import { logger } from "./logger.js";
3
+ /**
4
+ * Manages dynamic configuration updates that can change at runtime.
5
+ * Specifically handles pushId which can be updated per-session.
6
+ */
7
+ class ConfigManager {
8
+ sessionPushIds = new Map();
9
+ globalPushId = null;
10
+ /**
11
+ * Update push ID for a specific session.
12
+ */
13
+ updatePushId(sessionId, pushId) {
14
+ if (!pushId) {
15
+ logger.warn(`[ConfigManager] Attempted to set empty pushId for session ${sessionId}`);
16
+ return;
17
+ }
18
+ const previous = this.sessionPushIds.get(sessionId);
19
+ if (previous !== pushId) {
20
+ logger.log(`[ConfigManager] ✨ Updated pushId for session ${sessionId}`);
21
+ logger.log(`[ConfigManager] - Previous: ${previous ? previous.substring(0, 20) + '...' : 'none'}`);
22
+ logger.log(`[ConfigManager] - New: ${pushId.substring(0, 20)}...`);
23
+ logger.log(`[ConfigManager] - Full new pushId: ${pushId}`);
24
+ this.sessionPushIds.set(sessionId, pushId);
25
+ this.globalPushId = pushId; // Also update global for backward compatibility
26
+ }
27
+ }
28
+ /**
29
+ * Get push ID for a session (falls back to global if not found).
30
+ */
31
+ getPushId(sessionId) {
32
+ if (sessionId) {
33
+ const sessionPushId = this.sessionPushIds.get(sessionId);
34
+ if (sessionPushId) {
35
+ return sessionPushId;
36
+ }
37
+ }
38
+ return this.globalPushId;
39
+ }
40
+ /**
41
+ * Clear push ID for a session.
42
+ */
43
+ clearSession(sessionId) {
44
+ this.sessionPushIds.delete(sessionId);
45
+ logger.debug(`[ConfigManager] Cleared pushId for session ${sessionId}`);
46
+ }
47
+ /**
48
+ * Clear all cached push IDs.
49
+ */
50
+ clear() {
51
+ this.sessionPushIds.clear();
52
+ this.globalPushId = null;
53
+ logger.debug(`[ConfigManager] Cleared all cached pushIds`);
54
+ }
55
+ }
56
+ export const configManager = new ConfigManager();
@@ -317,19 +317,23 @@ export class XYWebSocketManager extends EventEmitter {
317
317
  // Only emit data-event, don't send to openclaw
318
318
  console.log(`[XY-${serverId}] Message contains only data parts, processing as tool result`);
319
319
  for (const dataPart of dataParts) {
320
- const dataArray = dataPart.data;
321
- if (Array.isArray(dataArray)) {
322
- for (const item of dataArray) {
323
- // Check if it's an UploadExeResult (intent execution result)
324
- if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
325
- const dataEvent = {
326
- intentName: item.payload.intentName,
327
- outputs: item.payload.outputs || {},
328
- status: "success",
329
- };
330
- console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
331
- this.emit("data-event", dataEvent);
332
- }
320
+ // Data format: {events: [{header, payload}, ...]}
321
+ const events = dataPart.data?.events;
322
+ if (!Array.isArray(events)) {
323
+ console.warn(`[XY-${serverId}] dataPart.data.events is not an array, skipping`);
324
+ continue;
325
+ }
326
+ console.log(`[XY-${serverId}] Processing ${events.length} events from data.events`);
327
+ for (const item of events) {
328
+ // Check if it's an UploadExeResult (intent execution result)
329
+ if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
330
+ const dataEvent = {
331
+ intentName: item.payload.intentName,
332
+ outputs: item.payload.outputs || {},
333
+ status: "success",
334
+ };
335
+ console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
336
+ this.emit("data-event", dataEvent);
333
337
  }
334
338
  }
335
339
  }
@@ -356,19 +360,23 @@ export class XYWebSocketManager extends EventEmitter {
356
360
  const dataParts = a2aRequest.params?.message?.parts?.filter((p) => p.kind === "data");
357
361
  if (dataParts && dataParts.length > 0) {
358
362
  for (const dataPart of dataParts) {
359
- const dataArray = dataPart.data;
360
- if (Array.isArray(dataArray)) {
361
- for (const item of dataArray) {
362
- // Check if it's an UploadExeResult (intent execution result)
363
- if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
364
- const dataEvent = {
365
- intentName: item.payload.intentName,
366
- outputs: item.payload.outputs || {},
367
- status: "success",
368
- };
369
- console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
370
- this.emit("data-event", dataEvent);
371
- }
363
+ // Data format: {events: [{header, payload}, ...]}
364
+ const events = dataPart.data?.events;
365
+ if (!Array.isArray(events)) {
366
+ console.warn(`[XY-${serverId}] dataPart.data.events is not an array, skipping`);
367
+ continue;
368
+ }
369
+ console.log(`[XY-${serverId}] Processing ${events.length} events from data.events`);
370
+ for (const item of events) {
371
+ // Check if it's an UploadExeResult (intent execution result)
372
+ if (item.header?.name === "UploadExeResult" && item.payload?.intentName) {
373
+ const dataEvent = {
374
+ intentName: item.payload.intentName,
375
+ outputs: item.payload.outputs || {},
376
+ status: "success",
377
+ };
378
+ console.log(`[XY-${serverId}] Emitting data-event:`, dataEvent);
379
+ this.emit("data-event", dataEvent);
372
380
  }
373
381
  }
374
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",