@ynhcj/xiaoyi-channel 0.0.5 โ†’ 0.0.7

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
@@ -19,7 +19,8 @@ export async function handleXYMessage(params) {
19
19
  try {
20
20
  // Check for special messages BEFORE parsing (these have different param structures)
21
21
  const messageMethod = message.method;
22
- log(`[DEBUG] Received message with method: ${messageMethod}, id: ${message.id}`);
22
+ log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
23
+ log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
23
24
  // Handle clearContext messages (params only has sessionId)
24
25
  if (messageMethod === "clearContext" || messageMethod === "clear_context") {
25
26
  const sessionId = message.params?.sessionId;
@@ -61,7 +62,7 @@ export async function handleXYMessage(params) {
61
62
  // Use sessionId as peer.id to ensure all messages in the same session share context
62
63
  let route = core.channel.routing.resolveAgentRoute({
63
64
  cfg,
64
- channel: "xy",
65
+ channel: "xiaoyi-channel",
65
66
  accountId, // "default"
66
67
  peer: {
67
68
  kind: "direct",
@@ -70,6 +71,10 @@ export async function handleXYMessage(params) {
70
71
  });
71
72
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
72
73
  // Register session context for tools
74
+ log(`[BOT] ๐Ÿ“ About to register session for tools...`);
75
+ log(`[BOT] - sessionKey: ${route.sessionKey}`);
76
+ log(`[BOT] - sessionId: ${parsed.sessionId}`);
77
+ log(`[BOT] - taskId: ${parsed.taskId}`);
73
78
  registerSession(route.sessionKey, {
74
79
  config,
75
80
  sessionId: parsed.sessionId,
@@ -77,6 +82,7 @@ export async function handleXYMessage(params) {
77
82
  messageId: parsed.messageId,
78
83
  agentId: route.accountId,
79
84
  });
85
+ log(`[BOT] โœ… Session registered for tools`);
80
86
  // Extract text and files from parts
81
87
  const text = extractTextFromParts(parsed.parts);
82
88
  const fileParts = extractFileParts(parsed.parts);
@@ -113,13 +119,13 @@ export async function handleXYMessage(params) {
113
119
  GroupSubject: undefined,
114
120
  SenderName: parsed.sessionId,
115
121
  SenderId: parsed.sessionId,
116
- Provider: "xy",
117
- Surface: "xy",
122
+ Provider: "xiaoyi-channel",
123
+ Surface: "xiaoyi-channel",
118
124
  MessageSid: parsed.messageId,
119
125
  Timestamp: Date.now(),
120
126
  WasMentioned: false,
121
127
  CommandAuthorized: true,
122
- OriginatingChannel: "xy",
128
+ OriginatingChannel: "xiaoyi-channel",
123
129
  OriginatingTo: parsed.sessionId, // Original message target
124
130
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
125
131
  ...mediaPayload,
@@ -150,12 +156,16 @@ export async function handleXYMessage(params) {
150
156
  startStatusInterval();
151
157
  log(`xy: dispatching to agent (session=${parsed.sessionId})`);
152
158
  // Dispatch to OpenClaw core using correct API (following feishu pattern)
159
+ log(`[BOT] ๐Ÿš€ Starting dispatcher with session: ${route.sessionKey}`);
153
160
  await core.channel.reply.withReplyDispatcher({
154
161
  dispatcher,
155
162
  onSettled: () => {
163
+ log(`[BOT] ๐Ÿ onSettled called for session: ${route.sessionKey}`);
164
+ log(`[BOT] - About to unregister session...`);
156
165
  markDispatchIdle();
157
166
  // Unregister session context when done
158
167
  unregisterSession(route.sessionKey);
168
+ log(`[BOT] โœ… Session unregistered in onSettled`);
159
169
  },
160
170
  run: () => core.channel.reply.dispatchReplyFromConfig({
161
171
  ctx: ctxPayload,
@@ -164,30 +174,36 @@ export async function handleXYMessage(params) {
164
174
  replyOptions,
165
175
  }),
166
176
  });
177
+ log(`[BOT] โœ… Dispatcher completed for session: ${parsed.sessionId}`);
167
178
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
168
179
  }
169
180
  catch (err) {
170
181
  error("Failed to handle XY message:", err);
171
182
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
183
+ log(`[BOT] โŒ Error occurred, attempting cleanup...`);
172
184
  // Try to unregister session on error (if route was established)
173
185
  try {
174
186
  const core = getXYRuntime();
175
187
  const params = message.params;
176
188
  const sessionId = params?.sessionId;
177
189
  if (sessionId) {
190
+ log(`[BOT] ๐Ÿงน Cleaning up session after error: ${sessionId}`);
178
191
  const route = core.channel.routing.resolveAgentRoute({
179
192
  cfg,
180
- channel: "xy",
193
+ channel: "xiaoyi-channel",
181
194
  accountId,
182
195
  peer: {
183
196
  kind: "direct",
184
197
  id: sessionId, // โœ… Use sessionId for cleanup consistency
185
198
  },
186
199
  });
200
+ log(`[BOT] - Unregistering session: ${route.sessionKey}`);
187
201
  unregisterSession(route.sessionKey);
202
+ log(`[BOT] โœ… Session unregistered after error`);
188
203
  }
189
204
  }
190
- catch {
205
+ catch (cleanupErr) {
206
+ log(`[BOT] โš ๏ธ Cleanup failed:`, cleanupErr);
191
207
  // Ignore cleanup errors
192
208
  }
193
209
  throw err;
@@ -3,6 +3,8 @@ import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
4
  import { xyOnboardingAdapter } from "./onboarding.js";
5
5
  import { locationTool } from "./tools/location-tool.js";
6
+ import { noteTool } from "./tools/note-tool.js";
7
+ import { searchNoteTool } from "./tools/search-note-tool.js";
6
8
  /**
7
9
  * Xiaoyi Channel Plugin for OpenClaw.
8
10
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -41,7 +43,7 @@ export const xyPlugin = {
41
43
  },
42
44
  outbound: xyOutbound,
43
45
  onboarding: xyOnboardingAdapter,
44
- agentTools: [locationTool],
46
+ agentTools: [locationTool, noteTool, searchNoteTool],
45
47
  messaging: {
46
48
  normalizeTarget: (raw) => {
47
49
  const trimmed = raw.trim();
@@ -51,8 +51,21 @@ export async function sendA2AResponse(params) {
51
51
  taskId,
52
52
  msgDetail: JSON.stringify(jsonRpcResponse),
53
53
  };
54
+ // ๐Ÿ“‹ Log complete response body
55
+ log(`[A2A_RESPONSE] ๐Ÿ“ค Sending A2A artifact-update response:`);
56
+ log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
57
+ log(`[A2A_RESPONSE] - taskId: ${taskId}`);
58
+ log(`[A2A_RESPONSE] - messageId: ${messageId}`);
59
+ log(`[A2A_RESPONSE] - append: ${append}`);
60
+ log(`[A2A_RESPONSE] - final: ${final}`);
61
+ log(`[A2A_RESPONSE] - text length: ${text?.length ?? 0}`);
62
+ log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
63
+ log(`[A2A_RESPONSE] ๐Ÿ“ฆ Complete outbound message:`);
64
+ log(JSON.stringify(outboundMessage, null, 2));
65
+ log(`[A2A_RESPONSE] ๐Ÿ“ฆ JSON-RPC response body:`);
66
+ log(JSON.stringify(jsonRpcResponse, null, 2));
54
67
  await wsManager.sendMessage(sessionId, outboundMessage);
55
- log(`Sent A2A response: sessionId=${sessionId}, taskId=${taskId}, final=${final}`);
68
+ log(`[A2A_RESPONSE] โœ… Message sent successfully`);
56
69
  }
57
70
  /**
58
71
  * Send an A2A task status update.
@@ -96,8 +109,19 @@ export async function sendStatusUpdate(params) {
96
109
  taskId,
97
110
  msgDetail: JSON.stringify(jsonRpcResponse),
98
111
  };
112
+ // ๐Ÿ“‹ Log complete response body
113
+ log(`[A2A_STATUS] ๐Ÿ“ค Sending A2A status-update:`);
114
+ log(`[A2A_STATUS] - sessionId: ${sessionId}`);
115
+ log(`[A2A_STATUS] - taskId: ${taskId}`);
116
+ log(`[A2A_STATUS] - messageId: ${messageId}`);
117
+ log(`[A2A_STATUS] - state: ${state}`);
118
+ log(`[A2A_STATUS] - text: "${text}"`);
119
+ log(`[A2A_STATUS] ๐Ÿ“ฆ Complete outbound message:`);
120
+ log(JSON.stringify(outboundMessage, null, 2));
121
+ log(`[A2A_STATUS] ๐Ÿ“ฆ JSON-RPC response body:`);
122
+ log(JSON.stringify(jsonRpcResponse, null, 2));
99
123
  await wsManager.sendMessage(sessionId, outboundMessage);
100
- log(`Sent status update: sessionId=${sessionId}, state=${state}, text="${text}"`);
124
+ log(`[A2A_STATUS] โœ… Status update sent successfully`);
101
125
  }
102
126
  /**
103
127
  * Send a command as an artifact update (final=false).
@@ -139,8 +163,18 @@ export async function sendCommand(params) {
139
163
  taskId,
140
164
  msgDetail: JSON.stringify(jsonRpcResponse),
141
165
  };
166
+ // ๐Ÿ“‹ Log complete response body
167
+ log(`[A2A_COMMAND] ๐Ÿ“ค Sending A2A command:`);
168
+ log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
169
+ log(`[A2A_COMMAND] - taskId: ${taskId}`);
170
+ log(`[A2A_COMMAND] - messageId: ${messageId}`);
171
+ log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
172
+ log(`[A2A_COMMAND] ๐Ÿ“ฆ Complete outbound message:`);
173
+ log(JSON.stringify(outboundMessage, null, 2));
174
+ log(`[A2A_COMMAND] ๐Ÿ“ฆ JSON-RPC response body:`);
175
+ log(JSON.stringify(jsonRpcResponse, null, 2));
142
176
  await wsManager.sendMessage(sessionId, outboundMessage);
143
- log(`Sent command: sessionId=${sessionId}, command=${command.header.name}`);
177
+ log(`[A2A_COMMAND] โœ… Command sent successfully`);
144
178
  }
145
179
  /**
146
180
  * Send a clearContext response.
@@ -44,25 +44,9 @@ export async function monitorXYProvider(opts = {}) {
44
44
  // Create session queue for ordered message processing
45
45
  const enqueue = createSessionQueue();
46
46
  return new Promise((resolve, reject) => {
47
- const cleanup = () => {
48
- log("XY gateway: cleaning up...");
49
- wsManager.disconnect();
50
- loggedServers.clear();
51
- };
52
- const handleAbort = () => {
53
- log("XY gateway: abort signal received, stopping");
54
- cleanup();
55
- log("XY gateway stopped");
56
- resolve();
57
- };
58
- if (opts.abortSignal?.aborted) {
59
- cleanup();
60
- resolve();
61
- return;
62
- }
63
- opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
64
- // Setup event handlers
47
+ // Event handlers (defined early so they can be referenced in cleanup)
65
48
  const messageHandler = (message, sessionId, serverId) => {
49
+ log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
66
50
  const task = async () => {
67
51
  try {
68
52
  await handleXYMessage({
@@ -95,7 +79,30 @@ export async function monitorXYProvider(opts = {}) {
95
79
  const errorHandler = (err, serverId) => {
96
80
  error(`XY gateway: ${serverId} error: ${String(err)}`);
97
81
  };
98
- // Register event handlers
82
+ const cleanup = () => {
83
+ log("XY gateway: cleaning up...");
84
+ // Remove event handlers to prevent duplicate calls on gateway restart
85
+ wsManager.off("message", messageHandler);
86
+ wsManager.off("connected", connectedHandler);
87
+ wsManager.off("disconnected", disconnectedHandler);
88
+ wsManager.off("error", errorHandler);
89
+ // Don't disconnect the shared wsManager as it may be used elsewhere
90
+ // wsManager.disconnect();
91
+ loggedServers.clear();
92
+ };
93
+ const handleAbort = () => {
94
+ log("XY gateway: abort signal received, stopping");
95
+ cleanup();
96
+ log("XY gateway stopped");
97
+ resolve();
98
+ };
99
+ if (opts.abortSignal?.aborted) {
100
+ cleanup();
101
+ resolve();
102
+ return;
103
+ }
104
+ opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
105
+ // Register event handlers (handlers are defined above in cleanup scope)
99
106
  wsManager.on("message", messageHandler);
100
107
  wsManager.on("connected", connectedHandler);
101
108
  wsManager.on("disconnected", disconnectedHandler);
@@ -1,10 +1,10 @@
1
- const channel = "xy";
1
+ const channel = "xiaoyi-channel";
2
2
  /**
3
3
  * Check if XY channel is properly configured with required fields
4
4
  */
5
5
  function isXYConfigured(cfg) {
6
6
  try {
7
- const xyConfig = cfg.channels?.xy;
7
+ const xyConfig = cfg.channels?.["xiaoyi-channel"];
8
8
  if (!xyConfig) {
9
9
  return false;
10
10
  }
@@ -26,7 +26,7 @@ function isXYConfigured(cfg) {
26
26
  */
27
27
  async function getStatus({ cfg }) {
28
28
  const configured = isXYConfigured(cfg);
29
- const xyConfig = cfg.channels?.xy;
29
+ const xyConfig = cfg.channels?.["xiaoyi-channel"];
30
30
  const statusLines = [];
31
31
  if (configured) {
32
32
  const wsUrl1 = xyConfig?.wsUrl1 || "ws://localhost:8765/ws/link";
@@ -49,7 +49,7 @@ async function getStatus({ cfg }) {
49
49
  */
50
50
  async function configure({ cfg, prompter, }) {
51
51
  // Note current configuration status
52
- const currentConfig = cfg.channels?.xy;
52
+ const currentConfig = cfg.channels?.["xiaoyi-channel"];
53
53
  const isUpdate = Boolean(currentConfig);
54
54
  await prompter.note([
55
55
  "XY Channel - ๅฐ่‰บ A2A ๅ่ฎฎ้…็ฝฎ",
@@ -117,7 +117,7 @@ async function configure({ cfg, prompter, }) {
117
117
  ...cfg,
118
118
  channels: {
119
119
  ...cfg.channels,
120
- xy: {
120
+ "xiaoyi-channel": {
121
121
  enabled: true,
122
122
  wsUrl1: wsUrl1.trim(),
123
123
  wsUrl2: wsUrl2.trim(),
@@ -164,8 +164,8 @@ export const xyOnboardingAdapter = {
164
164
  ...cfg,
165
165
  channels: {
166
166
  ...cfg.channels,
167
- xy: {
168
- ...(cfg.channels?.xy || {}),
167
+ "xiaoyi-channel": {
168
+ ...(cfg.channels?.["xiaoyi-channel"] || {}),
169
169
  enabled: false,
170
170
  },
171
171
  },
@@ -1,6 +1,7 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
2
  import { XYFileUploadService } from "./file-upload.js";
3
3
  import { XYPushService } from "./push.js";
4
+ import { getLatestSessionContext } from "./tools/session-manager.js";
4
5
  // Special marker for default push delivery when no target is specified
5
6
  const DEFAULT_PUSH_MARKER = "default";
6
7
  /**
@@ -14,6 +15,9 @@ export const xyOutbound = {
14
15
  * Resolve delivery target for XY channel.
15
16
  * When no target is specified (e.g., in cron jobs with announce mode),
16
17
  * returns a default marker that will be handled by sendText.
18
+ *
19
+ * For message tool calls, if only sessionId is provided, it will look up
20
+ * the active session context to construct the full "sessionId::taskId" format.
17
21
  */
18
22
  resolveTarget: ({ cfg, to, accountId, mode }) => {
19
23
  // If no target provided, use default marker for push delivery
@@ -24,11 +28,30 @@ export const xyOutbound = {
24
28
  to: DEFAULT_PUSH_MARKER,
25
29
  };
26
30
  }
27
- // Otherwise, use the provided target
28
- console.log(`[xyOutbound.resolveTarget] Using provided target:`, to);
31
+ const trimmedTo = to.trim();
32
+ // If the target doesn't contain "::", try to enhance it with taskId from session context
33
+ if (!trimmedTo.includes("::")) {
34
+ console.log(`[xyOutbound.resolveTarget] Target "${trimmedTo}" missing taskId, looking up session context`);
35
+ // Try to get the latest session context
36
+ const sessionContext = getLatestSessionContext();
37
+ if (sessionContext && sessionContext.sessionId === trimmedTo) {
38
+ const enhancedTarget = `${trimmedTo}::${sessionContext.taskId}`;
39
+ console.log(`[xyOutbound.resolveTarget] Enhanced target: ${enhancedTarget}`);
40
+ return {
41
+ ok: true,
42
+ to: enhancedTarget,
43
+ };
44
+ }
45
+ else {
46
+ console.log(`[xyOutbound.resolveTarget] Could not find matching session context for "${trimmedTo}"`);
47
+ // Still return the original target, but it may fail in sendMedia
48
+ }
49
+ }
50
+ // Otherwise, use the provided target (either already in correct format or for sendText)
51
+ console.log(`[xyOutbound.resolveTarget] Using provided target:`, trimmedTo);
29
52
  return {
30
53
  ok: true,
31
- to: to.trim(),
54
+ to: trimmedTo,
32
55
  };
33
56
  },
34
57
  sendText: async ({ cfg, to, text, accountId }) => {
@@ -58,7 +81,7 @@ export const xyOutbound = {
58
81
  console.log(`[xyOutbound.sendText] Completed successfully`);
59
82
  // Return message info
60
83
  return {
61
- channel: "xy",
84
+ channel: "xiaoyi-channel",
62
85
  messageId: Date.now().toString(),
63
86
  chatId: actualTo,
64
87
  };
@@ -135,7 +158,7 @@ export const xyOutbound = {
135
158
  console.log(`[xyOutbound.sendMedia] WebSocket message sent successfully`);
136
159
  // Return message info
137
160
  return {
138
- channel: "xy",
161
+ channel: "xiaoyi-channel",
139
162
  messageId: fileId,
140
163
  chatId: to,
141
164
  };
@@ -11,6 +11,11 @@ export function createXYReplyDispatcher(params) {
11
11
  const { cfg, runtime, sessionId, taskId, messageId, accountId } = params;
12
12
  const log = runtime?.log ?? console.log;
13
13
  const error = runtime?.error ?? console.error;
14
+ log(`[DISPATCHER-CREATE] ******* Creating dispatcher for session=${sessionId}, taskId=${taskId}, messageId=${messageId} *******`);
15
+ log(`[DISPATCHER-CREATE] Stack trace:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
16
+ log(`[DISPATCHER-CREATE] ======== Creating reply dispatcher ========`);
17
+ log(`[DISPATCHER-CREATE] sessionId: ${sessionId}, taskId: ${taskId}, messageId: ${messageId}`);
18
+ log(`[DISPATCHER-CREATE] Stack trace:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
14
19
  // Get runtime (already validated in monitor.ts, but get reference for use)
15
20
  const core = getXYRuntime();
16
21
  // Resolve configuration
@@ -23,6 +28,8 @@ export function createXYReplyDispatcher(params) {
23
28
  let hasSentResponse = false;
24
29
  // Track if we've sent the final empty message
25
30
  let finalSent = false;
31
+ // Accumulate all text from deliver calls
32
+ let accumulatedText = "";
26
33
  /**
27
34
  * Start the status update interval
28
35
  * Call this immediately after creating the dispatcher
@@ -76,19 +83,10 @@ export function createXYReplyDispatcher(params) {
76
83
  log(`[DELIVER SKIP] Empty text, skipping`);
77
84
  return;
78
85
  }
79
- // Send text with append=true (backend will accumulate)
80
- log(`[DELIVER SEND] Sending text, length=${text.length}, kind=${info?.kind || "undefined"}`);
81
- await sendA2AResponse({
82
- config,
83
- sessionId,
84
- taskId,
85
- messageId,
86
- text: text,
87
- append: true,
88
- final: false,
89
- });
86
+ // Accumulate text instead of sending immediately
87
+ accumulatedText += text;
90
88
  hasSentResponse = true;
91
- log(`[DELIVER DONE] Sent text, hasSentResponse=${hasSentResponse}`);
89
+ log(`[DELIVER ACCUMULATE] Accumulated text, current length=${accumulatedText.length}`);
92
90
  }
93
91
  catch (deliverError) {
94
92
  error(`Failed to deliver message:`, deliverError);
@@ -117,24 +115,24 @@ export function createXYReplyDispatcher(params) {
117
115
  },
118
116
  onIdle: async () => {
119
117
  log(`[ON_IDLE] Reply idle for session ${sessionId}, hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
120
- // Send final empty message to signal end if we haven't sent it yet
118
+ // Send accumulated text with append=false and final=true
121
119
  if (hasSentResponse && !finalSent) {
122
- log(`[ON_IDLE] Sending final empty message`);
120
+ log(`[ON_IDLE] Sending accumulated text, length=${accumulatedText.length}`);
123
121
  try {
124
122
  await sendA2AResponse({
125
123
  config,
126
124
  sessionId,
127
125
  taskId,
128
126
  messageId,
129
- text: "",
130
- append: true,
127
+ text: accumulatedText,
128
+ append: false,
131
129
  final: true,
132
130
  });
133
131
  finalSent = true;
134
- log(`[ON_IDLE] Sent final empty message`);
132
+ log(`[ON_IDLE] Sent accumulated text`);
135
133
  }
136
134
  catch (err) {
137
- error(`[ON_IDLE] Failed to send final message:`, err);
135
+ error(`[ON_IDLE] Failed to send accumulated text:`, err);
138
136
  }
139
137
  }
140
138
  else {
@@ -16,16 +16,31 @@ export const locationTool = {
16
16
  required: [],
17
17
  },
18
18
  async execute(toolCallId, params) {
19
- logger.debug("Executing location tool, toolCallId:", toolCallId);
19
+ logger.log(`[LOCATION_TOOL] ๐Ÿš€ Starting execution`);
20
+ logger.log(`[LOCATION_TOOL] - toolCallId: ${toolCallId}`);
21
+ logger.log(`[LOCATION_TOOL] - params:`, JSON.stringify(params));
22
+ logger.log(`[LOCATION_TOOL] - timestamp: ${new Date().toISOString()}`);
20
23
  // Get session context
24
+ logger.log(`[LOCATION_TOOL] ๐Ÿ” Attempting to get session context...`);
21
25
  const sessionContext = getLatestSessionContext();
22
26
  if (!sessionContext) {
27
+ logger.error(`[LOCATION_TOOL] โŒ FAILED: No active session found!`);
28
+ logger.error(`[LOCATION_TOOL] - toolCallId: ${toolCallId}`);
29
+ logger.error(`[LOCATION_TOOL] - This suggests the session was not registered or already cleaned up`);
23
30
  throw new Error("No active XY session found. Location tool can only be used during an active conversation.");
24
31
  }
32
+ logger.log(`[LOCATION_TOOL] โœ… Session context found`);
33
+ logger.log(`[LOCATION_TOOL] - sessionId: ${sessionContext.sessionId}`);
34
+ logger.log(`[LOCATION_TOOL] - taskId: ${sessionContext.taskId}`);
35
+ logger.log(`[LOCATION_TOOL] - messageId: ${sessionContext.messageId}`);
36
+ logger.log(`[LOCATION_TOOL] - agentId: ${sessionContext.agentId}`);
25
37
  const { config, sessionId, taskId, messageId } = sessionContext;
26
38
  // Get WebSocket manager
39
+ logger.log(`[LOCATION_TOOL] ๐Ÿ”Œ Getting WebSocket manager...`);
27
40
  const wsManager = getXYWebSocketManager(config);
41
+ logger.log(`[LOCATION_TOOL] โœ… WebSocket manager obtained`);
28
42
  // Build GetCurrentLocation command
43
+ logger.log(`[LOCATION_TOOL] ๐Ÿ“ฆ Building GetCurrentLocation command...`);
29
44
  const command = {
30
45
  header: {
31
46
  namespace: "Common",
@@ -45,20 +60,27 @@ export const locationTool = {
45
60
  },
46
61
  };
47
62
  // Send command and wait for response (5 second timeout)
63
+ logger.log(`[LOCATION_TOOL] โณ Setting up promise to wait for location response...`);
64
+ logger.log(`[LOCATION_TOOL] - Timeout: 5 seconds`);
48
65
  return new Promise((resolve, reject) => {
49
66
  const timeout = setTimeout(() => {
67
+ logger.error(`[LOCATION_TOOL] โฐ Timeout: No response received within 5 seconds`);
50
68
  wsManager.off("data-event", handler);
51
69
  reject(new Error("่Žทๅ–ไฝ็ฝฎ่ถ…ๆ—ถ๏ผˆ5็ง’๏ผ‰"));
52
70
  }, 5000);
53
71
  // Listen for data events from WebSocket
54
72
  const handler = (event) => {
55
- logger.debug("Received data event:", event);
73
+ logger.log(`[LOCATION_TOOL] ๐Ÿ“จ Received data event:`, JSON.stringify(event));
56
74
  if (event.intentName === "GetCurrentLocation") {
75
+ logger.log(`[LOCATION_TOOL] ๐ŸŽฏ GetCurrentLocation event received`);
76
+ logger.log(`[LOCATION_TOOL] - status: ${event.status}`);
57
77
  clearTimeout(timeout);
58
78
  wsManager.off("data-event", handler);
59
79
  if (event.status === "success" && event.outputs) {
60
80
  const { latitude, longitude } = event.outputs;
61
- logger.log(`Location retrieved: lat=${latitude}, lon=${longitude}`);
81
+ logger.log(`[LOCATION_TOOL] โœ… Location retrieved successfully`);
82
+ logger.log(`[LOCATION_TOOL] - latitude: ${latitude}`);
83
+ logger.log(`[LOCATION_TOOL] - longitude: ${longitude}`);
62
84
  resolve({
63
85
  content: [
64
86
  {
@@ -73,21 +95,28 @@ export const locationTool = {
73
95
  });
74
96
  }
75
97
  else {
98
+ logger.error(`[LOCATION_TOOL] โŒ Location retrieval failed`);
99
+ logger.error(`[LOCATION_TOOL] - status: ${event.status}`);
76
100
  reject(new Error(`่Žทๅ–ไฝ็ฝฎๅคฑ่ดฅ: ${event.status}`));
77
101
  }
78
102
  }
79
103
  };
80
104
  // Register event handler
81
105
  // Note: The WebSocket manager needs to emit 'data-event' when receiving data events
106
+ logger.log(`[LOCATION_TOOL] ๐Ÿ“ก Registering data-event handler on WebSocket manager`);
82
107
  wsManager.on("data-event", handler);
83
108
  // Send the command
109
+ logger.log(`[LOCATION_TOOL] ๐Ÿ“ค Sending GetCurrentLocation command...`);
84
110
  sendCommand({
85
111
  config,
86
112
  sessionId,
87
113
  taskId,
88
114
  messageId,
89
115
  command,
116
+ }).then(() => {
117
+ logger.log(`[LOCATION_TOOL] โœ… Command sent successfully, waiting for response...`);
90
118
  }).catch((error) => {
119
+ logger.error(`[LOCATION_TOOL] โŒ Failed to send command:`, error);
91
120
  clearTimeout(timeout);
92
121
  wsManager.off("data-event", handler);
93
122
  reject(error);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * XY note tool - creates a note on user's device.
3
+ * Requires title and content parameters.
4
+ */
5
+ export declare const noteTool: any;
@@ -0,0 +1,130 @@
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 note tool - creates a note on user's device.
7
+ * Requires title and content parameters.
8
+ */
9
+ export const noteTool = {
10
+ name: "create_note",
11
+ label: "Create Note",
12
+ description: "ๅœจ็”จๆˆท่ฎพๅค‡ไธŠๅˆ›ๅปบๅค‡ๅฟ˜ๅฝ•ใ€‚้œ€่ฆๆไพ›ๅค‡ๅฟ˜ๅฝ•ๆ ‡้ข˜ๅ’Œๅ†…ๅฎนใ€‚",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ title: {
17
+ type: "string",
18
+ description: "ๅค‡ๅฟ˜ๅฝ•ๆ ‡้ข˜",
19
+ },
20
+ content: {
21
+ type: "string",
22
+ description: "ๅค‡ๅฟ˜ๅฝ•ๅ†…ๅฎน",
23
+ },
24
+ },
25
+ required: ["title", "content"],
26
+ },
27
+ async execute(toolCallId, params) {
28
+ logger.debug("Executing note tool, toolCallId:", toolCallId);
29
+ // Validate parameters
30
+ if (!params.title || !params.content) {
31
+ throw new Error("Missing required parameters: title and content are required");
32
+ }
33
+ // Get session context
34
+ const sessionContext = getLatestSessionContext();
35
+ if (!sessionContext) {
36
+ throw new Error("No active XY session found. Note tool can only be used during an active conversation.");
37
+ }
38
+ const { config, sessionId, taskId, messageId } = sessionContext;
39
+ // Get WebSocket manager
40
+ const wsManager = getXYWebSocketManager(config);
41
+ // Build CreateNote command
42
+ const command = {
43
+ header: {
44
+ namespace: "Common",
45
+ name: "Action",
46
+ },
47
+ payload: {
48
+ cardParam: {},
49
+ executeParam: {
50
+ executeMode: "background",
51
+ intentName: "CreateNote",
52
+ bundleName: "com.huawei.hmos.notepad",
53
+ dimension: "",
54
+ needUnlock: true,
55
+ actionResponse: true,
56
+ timeOut: 5,
57
+ intentParam: {
58
+ title: params.title,
59
+ content: params.content,
60
+ },
61
+ achieveType: "INTENT",
62
+ },
63
+ responses: [
64
+ {
65
+ resultCode: "",
66
+ displayText: "",
67
+ ttsText: "",
68
+ },
69
+ ],
70
+ needUploadResult: true,
71
+ noHalfPage: false,
72
+ pageControlRelated: false,
73
+ },
74
+ };
75
+ // Send command and wait for response (5 second timeout)
76
+ return new Promise((resolve, reject) => {
77
+ const timeout = setTimeout(() => {
78
+ wsManager.off("data-event", handler);
79
+ reject(new Error("ๅˆ›ๅปบๅค‡ๅฟ˜ๅฝ•่ถ…ๆ—ถ๏ผˆ5็ง’๏ผ‰"));
80
+ }, 5000);
81
+ // Listen for data events from WebSocket
82
+ const handler = (event) => {
83
+ logger.debug("Received data event:", event);
84
+ if (event.intentName === "CreateNote") {
85
+ clearTimeout(timeout);
86
+ wsManager.off("data-event", handler);
87
+ if (event.status === "success" && event.outputs) {
88
+ const { result, code } = event.outputs;
89
+ logger.log(`Note created: title=${result?.title}, id=${result?.entityId}`);
90
+ resolve({
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: JSON.stringify({
95
+ success: true,
96
+ note: {
97
+ entityId: result?.entityId,
98
+ title: result?.title,
99
+ content: result?.content,
100
+ entityName: result?.entityName,
101
+ modifiedDate: result?.modifiedDate,
102
+ },
103
+ code,
104
+ }),
105
+ },
106
+ ],
107
+ });
108
+ }
109
+ else {
110
+ reject(new Error(`ๅˆ›ๅปบๅค‡ๅฟ˜ๅฝ•ๅคฑ่ดฅ: ${event.status}`));
111
+ }
112
+ }
113
+ };
114
+ // Register event handler
115
+ wsManager.on("data-event", handler);
116
+ // Send the command
117
+ sendCommand({
118
+ config,
119
+ sessionId,
120
+ taskId,
121
+ messageId,
122
+ command,
123
+ }).catch((error) => {
124
+ clearTimeout(timeout);
125
+ wsManager.off("data-event", handler);
126
+ reject(error);
127
+ });
128
+ });
129
+ },
130
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * XY search note tool - searches notes on user's device.
3
+ * Returns matching notes based on query string.
4
+ */
5
+ export declare const searchNoteTool: any;
@@ -0,0 +1,130 @@
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 search note tool - searches notes on user's device.
7
+ * Returns matching notes based on query string.
8
+ */
9
+ export const searchNoteTool = {
10
+ name: "search_notes",
11
+ label: "Search Notes",
12
+ description: "ๆœ็ดข็”จๆˆท่ฎพๅค‡ไธŠ็š„ๅค‡ๅฟ˜ๅฝ•ๅ†…ๅฎนใ€‚ๆ นๆฎๅ…ณ้”ฎ่ฏๅœจๅค‡ๅฟ˜ๅฝ•็š„ๆ ‡้ข˜ใ€ๅ†…ๅฎนๅ’Œ้™„ไปถๅ็งฐไธญ่ฟ›่กŒๆฃ€็ดขใ€‚",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ query: {
17
+ type: "string",
18
+ description: "ๆœ็ดขๅ…ณ้”ฎ่ฏ๏ผŒ็”จไบŽๅœจๅค‡ๅฟ˜ๅฝ•ไธญๆฃ€็ดข็›ธๅ…ณๅ†…ๅฎน",
19
+ },
20
+ },
21
+ required: ["query"],
22
+ },
23
+ async execute(toolCallId, params) {
24
+ logger.debug("Executing search note tool, toolCallId:", toolCallId);
25
+ // Validate parameters
26
+ if (!params.query) {
27
+ throw new Error("Missing required parameter: query is required");
28
+ }
29
+ // Get session context
30
+ const sessionContext = getLatestSessionContext();
31
+ if (!sessionContext) {
32
+ throw new Error("No active XY session found. Search note tool can only be used during an active conversation.");
33
+ }
34
+ const { config, sessionId, taskId, messageId } = sessionContext;
35
+ // Get WebSocket manager
36
+ const wsManager = getXYWebSocketManager(config);
37
+ // Build SearchNote command
38
+ const command = {
39
+ header: {
40
+ namespace: "Common",
41
+ name: "Action",
42
+ },
43
+ payload: {
44
+ cardParam: {},
45
+ executeParam: {
46
+ executeMode: "background",
47
+ intentName: "SearchNote",
48
+ bundleName: "com.huawei.hmos.notepad",
49
+ dimension: "",
50
+ needUnlock: true,
51
+ actionResponse: true,
52
+ timeOut: 5,
53
+ intentParam: {
54
+ query: params.query,
55
+ },
56
+ achieveType: "INTENT",
57
+ },
58
+ responses: [
59
+ {
60
+ resultCode: "",
61
+ displayText: "",
62
+ ttsText: "",
63
+ },
64
+ ],
65
+ needUploadResult: true,
66
+ noHalfPage: false,
67
+ pageControlRelated: false,
68
+ },
69
+ };
70
+ // Send command and wait for response (5 second timeout)
71
+ return new Promise((resolve, reject) => {
72
+ const timeout = setTimeout(() => {
73
+ wsManager.off("data-event", handler);
74
+ reject(new Error("ๆœ็ดขๅค‡ๅฟ˜ๅฝ•่ถ…ๆ—ถ๏ผˆ5็ง’๏ผ‰"));
75
+ }, 5000);
76
+ // Listen for data events from WebSocket
77
+ const handler = (event) => {
78
+ logger.debug("Received data event:", event);
79
+ if (event.intentName === "SearchNote") {
80
+ clearTimeout(timeout);
81
+ wsManager.off("data-event", handler);
82
+ if (event.status === "success" && event.outputs) {
83
+ const { result, code } = event.outputs;
84
+ const items = result?.items || [];
85
+ logger.log(`Notes found: ${items.length} results for query "${params.query}"`);
86
+ resolve({
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: JSON.stringify({
91
+ success: true,
92
+ query: params.query,
93
+ totalResults: items.length,
94
+ notes: items.map((item) => ({
95
+ entityId: item.entityId,
96
+ entityName: item.entityName,
97
+ title: item.title?.replace(/<\/?em>/g, ''), // Remove <em> tags
98
+ content: item.content,
99
+ createdDate: item.createdDate,
100
+ modifiedDate: item.modifiedDate,
101
+ })),
102
+ indexName: result?.indexName,
103
+ code,
104
+ }),
105
+ },
106
+ ],
107
+ });
108
+ }
109
+ else {
110
+ reject(new Error(`ๆœ็ดขๅค‡ๅฟ˜ๅฝ•ๅคฑ่ดฅ: ${event.status}`));
111
+ }
112
+ }
113
+ };
114
+ // Register event handler
115
+ wsManager.on("data-event", handler);
116
+ // Send the command
117
+ sendCommand({
118
+ config,
119
+ sessionId,
120
+ taskId,
121
+ messageId,
122
+ command,
123
+ }).catch((error) => {
124
+ clearTimeout(timeout);
125
+ wsManager.off("data-event", handler);
126
+ reject(error);
127
+ });
128
+ });
129
+ },
130
+ };
@@ -1,3 +1,4 @@
1
+ import { logger } from "../utils/logger.js";
1
2
  // Map of sessionKey -> SessionContext
2
3
  const activeSessions = new Map();
3
4
  /**
@@ -5,21 +6,42 @@ const activeSessions = new Map();
5
6
  * Should be called when starting to process a message.
6
7
  */
7
8
  export function registerSession(sessionKey, context) {
9
+ logger.log(`[SESSION_MANAGER] ๐Ÿ“ Registering session: ${sessionKey}`);
10
+ logger.log(`[SESSION_MANAGER] - sessionId: ${context.sessionId}`);
11
+ logger.log(`[SESSION_MANAGER] - taskId: ${context.taskId}`);
12
+ logger.log(`[SESSION_MANAGER] - messageId: ${context.messageId}`);
13
+ logger.log(`[SESSION_MANAGER] - agentId: ${context.agentId}`);
14
+ logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
8
15
  activeSessions.set(sessionKey, context);
16
+ logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
17
+ logger.log(`[SESSION_MANAGER] - All session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
9
18
  }
10
19
  /**
11
20
  * Unregister a session context.
12
21
  * Should be called when message processing is complete.
13
22
  */
14
23
  export function unregisterSession(sessionKey) {
15
- activeSessions.delete(sessionKey);
24
+ logger.log(`[SESSION_MANAGER] ๐Ÿ—‘๏ธ Unregistering session: ${sessionKey}`);
25
+ logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
26
+ logger.log(`[SESSION_MANAGER] - Session existed: ${activeSessions.has(sessionKey)}`);
27
+ const existed = activeSessions.delete(sessionKey);
28
+ logger.log(`[SESSION_MANAGER] - Deleted: ${existed}`);
29
+ logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
30
+ logger.log(`[SESSION_MANAGER] - Remaining session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
16
31
  }
17
32
  /**
18
33
  * Get session context by sessionKey.
19
34
  * Returns null if session not found.
20
35
  */
21
36
  export function getSessionContext(sessionKey) {
22
- return activeSessions.get(sessionKey) ?? null;
37
+ logger.log(`[SESSION_MANAGER] ๐Ÿ” Getting session by key: ${sessionKey}`);
38
+ logger.log(`[SESSION_MANAGER] - Active sessions: ${activeSessions.size}`);
39
+ const context = activeSessions.get(sessionKey) ?? null;
40
+ logger.log(`[SESSION_MANAGER] - Found: ${context !== null}`);
41
+ if (context) {
42
+ logger.log(`[SESSION_MANAGER] - sessionId: ${context.sessionId}`);
43
+ }
44
+ return context;
23
45
  }
24
46
  /**
25
47
  * Get the most recent session context.
@@ -27,9 +49,19 @@ export function getSessionContext(sessionKey) {
27
49
  * Returns null if no sessions are active.
28
50
  */
29
51
  export function getLatestSessionContext() {
30
- if (activeSessions.size === 0)
52
+ logger.log(`[SESSION_MANAGER] ๐Ÿ” Getting latest session context`);
53
+ logger.log(`[SESSION_MANAGER] - Active sessions count: ${activeSessions.size}`);
54
+ logger.log(`[SESSION_MANAGER] - Active session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
55
+ if (activeSessions.size === 0) {
56
+ logger.error(`[SESSION_MANAGER] - โŒ No active sessions found!`);
31
57
  return null;
58
+ }
32
59
  // Return the last added session
33
60
  const sessions = Array.from(activeSessions.values());
34
- return sessions[sessions.length - 1];
61
+ const latestSession = sessions[sessions.length - 1];
62
+ logger.log(`[SESSION_MANAGER] - โœ… Found latest session:`);
63
+ logger.log(`[SESSION_MANAGER] - sessionId: ${latestSession.sessionId}`);
64
+ logger.log(`[SESSION_MANAGER] - taskId: ${latestSession.taskId}`);
65
+ logger.log(`[SESSION_MANAGER] - messageId: ${latestSession.messageId}`);
66
+ return latestSession;
35
67
  }
@@ -274,6 +274,7 @@ export class XYWebSocketManager extends EventEmitter {
274
274
  * Handle incoming message from server.
275
275
  */
276
276
  handleMessage(serverId, data) {
277
+ console.log(`[WEBSOCKET-HANDLE] >>>>>>> serverId: ${serverId}, receiving message... <<<<<<<`);
277
278
  try {
278
279
  const messageStr = data.toString();
279
280
  const parsed = JSON.parse(messageStr);
@@ -324,6 +325,7 @@ export class XYWebSocketManager extends EventEmitter {
324
325
  return; // Don't emit message event
325
326
  }
326
327
  // Emit message event for non-data-only messages
328
+ console.log(`[XY-${serverId}] *** EMITTING message event (Direct A2A path) ***`);
327
329
  this.emit("message", a2aRequest, sessionId, serverId);
328
330
  return;
329
331
  }
@@ -377,6 +379,7 @@ export class XYWebSocketManager extends EventEmitter {
377
379
  }
378
380
  console.log(`[XY-${serverId}] Session ID: ${sessionId}`);
379
381
  // Emit message event
382
+ console.log(`[XY-${serverId}] *** EMITTING message event (Wrapped path) ***`);
380
383
  this.emit("message", a2aRequest, sessionId, serverId);
381
384
  }
382
385
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",