@ynhcj/xiaoyi-channel 0.0.22-beta → 0.0.22-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 (78) hide show
  1. package/dist/index.js +42 -0
  2. package/dist/src/bot.js +123 -41
  3. package/dist/src/channel.js +17 -5
  4. package/dist/src/client.js +11 -24
  5. package/dist/src/config.js +2 -2
  6. package/dist/src/cspl/call-api.d.ts +3 -0
  7. package/dist/src/cspl/call-api.js +79 -0
  8. package/dist/src/cspl/config.d.ts +19 -0
  9. package/dist/src/cspl/config.js +50 -0
  10. package/dist/src/cspl/constants.d.ts +43 -0
  11. package/dist/src/cspl/constants.js +22 -0
  12. package/dist/src/cspl/utils.d.ts +10 -0
  13. package/dist/src/cspl/utils.js +57 -0
  14. package/dist/src/file-upload.d.ts +5 -0
  15. package/dist/src/file-upload.js +92 -0
  16. package/dist/src/formatter.d.ts +14 -0
  17. package/dist/src/formatter.js +46 -2
  18. package/dist/src/monitor.js +47 -5
  19. package/dist/src/outbound.js +52 -6
  20. package/dist/src/parser.d.ts +7 -0
  21. package/dist/src/parser.js +22 -0
  22. package/dist/src/push.d.ts +8 -1
  23. package/dist/src/push.js +30 -22
  24. package/dist/src/reply-dispatcher.d.ts +1 -0
  25. package/dist/src/reply-dispatcher.js +116 -101
  26. package/dist/src/steer-injector.d.ts +16 -0
  27. package/dist/src/steer-injector.js +74 -0
  28. package/dist/src/task-manager.d.ts +55 -0
  29. package/dist/src/task-manager.js +136 -0
  30. package/dist/src/tools/calendar-tool.js +5 -3
  31. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  32. package/dist/src/tools/call-phone-tool.js +183 -0
  33. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  34. package/dist/src/tools/create-alarm-tool.js +420 -0
  35. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  36. package/dist/src/tools/delete-alarm-tool.js +216 -0
  37. package/dist/src/tools/image-reading-tool.d.ts +5 -0
  38. package/dist/src/tools/image-reading-tool.js +375 -0
  39. package/dist/src/tools/location-tool.js +8 -11
  40. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  41. package/dist/src/tools/modify-alarm-tool.js +439 -0
  42. package/dist/src/tools/modify-note-tool.js +4 -9
  43. package/dist/src/tools/note-tool.js +32 -21
  44. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  45. package/dist/src/tools/search-alarm-tool.js +343 -0
  46. package/dist/src/tools/search-calendar-tool.js +9 -46
  47. package/dist/src/tools/search-contact-tool.js +4 -27
  48. package/dist/src/tools/search-file-tool.d.ts +5 -0
  49. package/dist/src/tools/search-file-tool.js +161 -0
  50. package/dist/src/tools/search-message-tool.d.ts +5 -0
  51. package/dist/src/tools/search-message-tool.js +149 -0
  52. package/dist/src/tools/search-note-tool.js +29 -22
  53. package/dist/src/tools/search-photo-gallery-tool.js +51 -34
  54. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  55. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  56. package/dist/src/tools/send-message-tool.d.ts +5 -0
  57. package/dist/src/tools/send-message-tool.js +176 -0
  58. package/dist/src/tools/session-manager.d.ts +15 -0
  59. package/dist/src/tools/session-manager.js +99 -18
  60. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  61. package/dist/src/tools/upload-file-tool.js +265 -0
  62. package/dist/src/tools/upload-photo-tool.js +14 -4
  63. package/dist/src/tools/view-push-result-tool.d.ts +5 -0
  64. package/dist/src/tools/view-push-result-tool.js +118 -0
  65. package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
  66. package/dist/src/tools/xiaoyi-collection-tool.js +190 -0
  67. package/dist/src/tools/xiaoyi-gui-tool.js +8 -8
  68. package/dist/src/trigger-handler.d.ts +22 -0
  69. package/dist/src/trigger-handler.js +59 -0
  70. package/dist/src/types.d.ts +1 -8
  71. package/dist/src/types.js +4 -0
  72. package/dist/src/utils/pushdata-manager.d.ts +28 -0
  73. package/dist/src/utils/pushdata-manager.js +171 -0
  74. package/dist/src/utils/pushid-manager.d.ts +12 -0
  75. package/dist/src/utils/pushid-manager.js +105 -0
  76. package/dist/src/websocket.d.ts +25 -31
  77. package/dist/src/websocket.js +218 -270
  78. package/package.json +1 -1
@@ -2,63 +2,70 @@ import { createReplyPrefixContext } from "openclaw/plugin-sdk";
2
2
  import { getXYRuntime } from "./runtime.js";
3
3
  import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
4
4
  import { resolveXYConfig } from "./config.js";
5
+ import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
5
6
  /**
6
7
  * Create a reply dispatcher for XY channel messages.
7
8
  * Follows feishu pattern with status updates and streaming support.
8
9
  * Runtime is expected to be validated before calling this function.
9
10
  */
10
11
  export function createXYReplyDispatcher(params) {
11
- const { cfg, runtime, sessionId, taskId, messageId, accountId } = params;
12
+ const { cfg, runtime, sessionId, taskId, messageId, accountId, isSteerFollower } = params;
12
13
  const log = runtime?.log ?? console.log;
13
14
  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'));
19
- // Get runtime (already validated in monitor.ts, but get reference for use)
15
+ log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
16
+ log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
17
+ log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
18
+ log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
19
+ log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
20
+ // 初始taskId和messageId(作为fallback)
21
+ const initialTaskId = taskId;
22
+ const initialMessageId = messageId;
23
+ /**
24
+ * 🔑 核心改造:动态获取当前活跃的taskId和messageId
25
+ * 每次需要taskId时,都从TaskManager获取最新值
26
+ */
27
+ const getActiveTaskId = () => {
28
+ return getCurrentTaskId(sessionId) ?? initialTaskId;
29
+ };
30
+ const getActiveMessageId = () => {
31
+ return getCurrentMessageId(sessionId) ?? initialMessageId;
32
+ };
20
33
  const core = getXYRuntime();
21
- // Resolve configuration
22
34
  const config = resolveXYConfig(cfg);
23
- // Create reply prefix context (for model selection, etc.)
24
35
  const prefixContext = createReplyPrefixContext({ cfg, agentId: accountId });
25
- // Status update interval (every 60 seconds)
26
36
  let statusUpdateInterval = null;
27
- // Track if we've sent any response
28
37
  let hasSentResponse = false;
29
- // Track if we've sent the final empty message
30
38
  let finalSent = false;
31
- // Accumulate all text from deliver calls
32
39
  let accumulatedText = "";
33
40
  /**
34
41
  * Start the status update interval
35
- * Call this immediately after creating the dispatcher
36
42
  */
37
43
  const startStatusInterval = () => {
38
- log(`[STATUS INTERVAL] Starting interval for session ${sessionId}, taskId=${taskId}`);
44
+ log(`[STATUS INTERVAL] Starting interval for session ${sessionId}`);
39
45
  statusUpdateInterval = setInterval(() => {
40
- log(`[STATUS INTERVAL] Triggering status update for session ${sessionId}, taskId=${taskId}`);
46
+ // 🔑 使用动态taskId
47
+ const currentTaskId = getActiveTaskId();
48
+ const currentMessageId = getActiveMessageId();
49
+ log(`[STATUS INTERVAL] Triggering status update`);
50
+ log(`[STATUS INTERVAL] - sessionId: ${sessionId}`);
51
+ log(`[STATUS INTERVAL] - currentTaskId: ${currentTaskId}`);
41
52
  void sendStatusUpdate({
42
53
  config,
43
54
  sessionId,
44
- taskId,
45
- messageId,
46
- text: "任务正在处理中,请稍后~",
55
+ taskId: currentTaskId, // 🔑 动态taskId
56
+ messageId: currentMessageId, // 🔑 动态messageId
57
+ text: "任务正在处理中,请稍候~",
47
58
  state: "working",
48
59
  }).catch((err) => {
49
60
  error(`Failed to send status update:`, err);
50
61
  });
51
62
  }, 30000); // 30 seconds
52
63
  };
53
- /**
54
- * Stop the status update interval
55
- */
56
64
  const stopStatusInterval = () => {
57
65
  if (statusUpdateInterval) {
58
- log(`[STATUS INTERVAL] Stopping interval for session ${sessionId}, taskId=${taskId}`);
66
+ log(`[STATUS INTERVAL] Stopping interval for session ${sessionId}`);
59
67
  clearInterval(statusUpdateInterval);
60
68
  statusUpdateInterval = null;
61
- log(`[STATUS INTERVAL] Stopped interval for session ${sessionId}, taskId=${taskId}`);
62
69
  }
63
70
  };
64
71
  const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
@@ -66,33 +73,28 @@ export function createXYReplyDispatcher(params) {
66
73
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
67
74
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, accountId),
68
75
  onReplyStart: () => {
69
- log(`[REPLY START] Reply started for session ${sessionId}, taskId=${taskId}`);
70
- // Status update interval is now managed externally
76
+ const currentTaskId = getActiveTaskId();
77
+ log(`[REPLY START] Reply started for session ${sessionId}, taskId=${currentTaskId}, isSteerFollower=${isSteerFollower}`);
71
78
  },
72
79
  deliver: async (payload, info) => {
73
80
  const text = payload.text ?? "";
74
- // 🔍 Debug logging
75
- log(`[DELIVER] sessionId=${sessionId}, info.kind=${info?.kind}, text.length=${text.length}, text="${text.slice(0, 200)}"`);
76
- log(`[DELIVER] payload keys: ${Object.keys(payload).join(", ")}`);
77
- if (payload.mediaUrls) {
78
- log(`[DELIVER] mediaUrls: ${payload.mediaUrls.length} files`);
79
- }
81
+ const currentTaskId = getActiveTaskId();
82
+ const currentMessageId = getActiveMessageId();
83
+ log(`[DELIVER] sessionId=${sessionId}, taskId=${currentTaskId}, info.kind=${info?.kind}, text.length=${text.length}`);
80
84
  try {
81
- // Skip empty messages
82
85
  if (!text.trim()) {
83
86
  log(`[DELIVER SKIP] Empty text, skipping`);
84
87
  return;
85
88
  }
86
- // Accumulate text instead of sending immediately
87
89
  accumulatedText += text;
88
90
  hasSentResponse = true;
89
91
  log(`[DELIVER ACCUMULATE] Accumulated text, current length=${accumulatedText.length}`);
90
- // Also stream text as reasoningText for real-time display
92
+ // 🔑 使用动态taskId发送reasoningText更新
91
93
  await sendReasoningTextUpdate({
92
94
  config,
93
95
  sessionId,
94
- taskId,
95
- messageId,
96
+ taskId: currentTaskId,
97
+ messageId: currentMessageId,
96
98
  text,
97
99
  });
98
100
  log(`[DELIVER] ✅ Sent deliver text as reasoningText update`);
@@ -103,16 +105,21 @@ export function createXYReplyDispatcher(params) {
103
105
  },
104
106
  onError: async (err, info) => {
105
107
  runtime.error?.(`xy: ${info.kind} reply failed: ${String(err)}`);
106
- // Stop status updates
107
108
  stopStatusInterval();
108
- // Send error status if we haven't sent any response yet
109
+ // 🔑 steer follower不发送错误状态(让主dispatcher处理)
110
+ if (isSteerFollower) {
111
+ log(`[ON_ERROR] Steer follower - skipping error response`);
112
+ return;
113
+ }
109
114
  if (!hasSentResponse) {
115
+ const currentTaskId = getActiveTaskId();
116
+ const currentMessageId = getActiveMessageId();
110
117
  try {
111
118
  await sendStatusUpdate({
112
119
  config,
113
120
  sessionId,
114
- taskId,
115
- messageId,
121
+ taskId: currentTaskId,
122
+ messageId: currentMessageId,
116
123
  text: "处理失败,请稍后重试",
117
124
  state: "failed",
118
125
  });
@@ -123,46 +130,61 @@ export function createXYReplyDispatcher(params) {
123
130
  }
124
131
  },
125
132
  onIdle: async () => {
126
- log(`[ON_IDLE] Reply idle for session ${sessionId}, hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
127
- // Send accumulated text with append=false and final=true
133
+ const currentTaskId = getActiveTaskId();
134
+ const currentMessageId = getActiveMessageId();
135
+ log(`[ON_IDLE] Reply idle`);
136
+ log(`[ON_IDLE] - sessionId: ${sessionId}`);
137
+ log(`[ON_IDLE] - taskId: ${currentTaskId}`);
138
+ log(`[ON_IDLE] - isSteerFollower: ${isSteerFollower}`);
139
+ log(`[ON_IDLE] - hasSentResponse: ${hasSentResponse}`);
140
+ log(`[ON_IDLE] - finalSent: ${finalSent}`);
141
+ // 🔑 核心改动:steer follower不发送final响应
142
+ if (isSteerFollower) {
143
+ log(`[ON_IDLE] Steer follower - skipping final response`);
144
+ log(`[ON_IDLE] - Message queued successfully, waiting for primary dispatcher`);
145
+ stopStatusInterval();
146
+ return; // ← 直接返回,不发送任何东西!
147
+ }
148
+ // 正常模式(或steer的第一条消息)
128
149
  if (hasSentResponse && !finalSent) {
129
150
  log(`[ON_IDLE] Sending accumulated text, length=${accumulatedText.length}`);
130
151
  try {
131
- // Send status update before final message
152
+ // 🔑 使用动态taskId发送完成状态
132
153
  await sendStatusUpdate({
133
154
  config,
134
155
  sessionId,
135
- taskId,
136
- messageId,
156
+ taskId: currentTaskId,
157
+ messageId: currentMessageId,
137
158
  text: "任务处理已完成~",
138
159
  state: "completed",
139
160
  });
140
161
  log(`[ON_IDLE] ✅ Sent completion status update`);
162
+ // 🔑 使用动态taskId发送最终响应
141
163
  await sendA2AResponse({
142
164
  config,
143
165
  sessionId,
144
- taskId,
145
- messageId,
166
+ taskId: currentTaskId,
167
+ messageId: currentMessageId,
146
168
  text: accumulatedText,
147
169
  append: false,
148
170
  final: true,
149
171
  });
150
172
  finalSent = true;
151
- log(`[ON_IDLE] Sent accumulated text`);
173
+ log(`[ON_IDLE] Sent final response with taskId=${currentTaskId}`);
152
174
  }
153
175
  catch (err) {
154
- error(`[ON_IDLE] Failed to send accumulated text:`, err);
176
+ error(`[ON_IDLE] Failed to send final response:`, err);
155
177
  }
156
178
  }
157
179
  else {
180
+ // 正常失败场景(非steer follower)
158
181
  log(`[ON_IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
159
- // Task was interrupted - send failure status and error response
160
182
  try {
161
183
  await sendStatusUpdate({
162
184
  config,
163
185
  sessionId,
164
- taskId,
165
- messageId,
186
+ taskId: currentTaskId,
187
+ messageId: currentMessageId,
166
188
  text: "任务处理中断了~",
167
189
  state: "failed",
168
190
  });
@@ -170,8 +192,8 @@ export function createXYReplyDispatcher(params) {
170
192
  await sendA2AResponse({
171
193
  config,
172
194
  sessionId,
173
- taskId,
174
- messageId,
195
+ taskId: currentTaskId,
196
+ messageId: currentMessageId,
175
197
  text: "任务执行异常,请重试~",
176
198
  append: false,
177
199
  final: true,
@@ -180,14 +202,14 @@ export function createXYReplyDispatcher(params) {
180
202
  log(`[ON_IDLE] ✅ Sent error response`);
181
203
  }
182
204
  catch (err) {
183
- error(`[ON_IDLE] Failed to send failure status and error response:`, err);
205
+ error(`[ON_IDLE] Failed to send error response:`, err);
184
206
  }
185
207
  }
186
- // Stop status updates
187
208
  stopStatusInterval();
188
209
  },
189
210
  onCleanup: () => {
190
- log(`[ON_CLEANUP] Reply cleanup for session ${sessionId}, hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
211
+ const currentTaskId = getActiveTaskId();
212
+ log(`[ON_CLEANUP] Reply cleanup, taskId=${currentTaskId}, isSteerFollower=${isSteerFollower}`);
191
213
  },
192
214
  });
193
215
  return {
@@ -195,17 +217,22 @@ export function createXYReplyDispatcher(params) {
195
217
  replyOptions: {
196
218
  ...replyOptions,
197
219
  onModelSelected: prefixContext.onModelSelected,
198
- // 🔧 Tool execution start callback
199
220
  onToolStart: async ({ name, phase }) => {
200
- log(`[TOOL START] 🔧 Tool execution started/updated: name=${name}, phase=${phase}, session=${sessionId}, taskId=${taskId}`);
221
+ // 🔑 steer follower不发送tool状态(让主dispatcher处理)
222
+ if (isSteerFollower) {
223
+ return;
224
+ }
225
+ const currentTaskId = getActiveTaskId();
226
+ const currentMessageId = getActiveMessageId();
227
+ log(`[TOOL START] Tool: ${name}, phase: ${phase}, taskId: ${currentTaskId}`);
201
228
  if (phase === "start") {
202
229
  const toolName = name || "unknown";
203
230
  try {
204
231
  await sendStatusUpdate({
205
232
  config,
206
233
  sessionId,
207
- taskId,
208
- messageId,
234
+ taskId: currentTaskId,
235
+ messageId: currentMessageId,
209
236
  text: `正在使用工具: ${toolName}...`,
210
237
  state: "working",
211
238
  });
@@ -216,25 +243,24 @@ export function createXYReplyDispatcher(params) {
216
243
  }
217
244
  }
218
245
  },
219
- // 🔧 Tool execution result callback
220
246
  onToolResult: async (payload) => {
247
+ // 🔑 steer follower不发送tool结果(让主dispatcher处理)
248
+ if (isSteerFollower) {
249
+ return;
250
+ }
251
+ const currentTaskId = getActiveTaskId();
252
+ const currentMessageId = getActiveMessageId();
221
253
  const text = payload.text ?? "";
222
254
  const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
223
- log(`[TOOL RESULT] 🔧 Tool execution result received: session=${sessionId}, taskId=${taskId}`);
224
- log(`[TOOL RESULT] - text.length=${text.length}`);
225
- log(`[TOOL RESULT] - hasMedia=${hasMedia}`);
226
- log(`[TOOL RESULT] - isError=${payload.isError}`);
227
- if (text.length > 0) {
228
- log(`[TOOL RESULT] - text preview: "${text.slice(0, 200)}"`);
229
- }
255
+ log(`[TOOL RESULT] Tool result, taskId: ${currentTaskId}, text.length: ${text.length}`);
230
256
  try {
231
257
  if (text.length > 0 || hasMedia) {
232
258
  const resultText = text.length > 0 ? text : "工具执行完成";
233
259
  await sendStatusUpdate({
234
260
  config,
235
261
  sessionId,
236
- taskId,
237
- messageId,
262
+ taskId: currentTaskId,
263
+ messageId: currentMessageId,
238
264
  text: resultText,
239
265
  state: "working",
240
266
  });
@@ -245,54 +271,43 @@ export function createXYReplyDispatcher(params) {
245
271
  error(`[TOOL RESULT] ❌ Failed to send tool result status:`, err);
246
272
  }
247
273
  },
248
- // 🧠 Reasoning/thinking process streaming callback
249
274
  onReasoningStream: async (payload) => {
250
- const text = payload.text ?? "";
251
- log(`[REASONING STREAM] 🧠 Reasoning/thinking chunk received: session=${sessionId}, taskId=${taskId}`);
252
- log(`[REASONING STREAM] - text.length=${text.length}`);
253
- if (text.length > 0) {
254
- log(`[REASONING STREAM] - text preview: "${text.slice(0, 200)}"`);
275
+ // 🔑 steer follower不发送reasoning stream
276
+ if (isSteerFollower) {
277
+ return;
255
278
  }
256
- // try {
257
- // if (text.length > 0) {
258
- // await sendReasoningTextUpdate({
259
- // config,
260
- // sessionId,
261
- // taskId,
262
- // messageId,
263
- // text,
264
- // });
265
- // log(`[REASONING STREAM] ✅ Sent reasoning chunk as reasoningText update`);
266
- // }
267
- // } catch (err) {
268
- // error(`[REASONING STREAM] ❌ Failed to send reasoning chunk reasoningText:`, err);
269
- // }
279
+ const text = payload.text ?? "";
280
+ log(`[REASONING STREAM] Reasoning chunk received, text.length: ${text.length}`);
281
+ // Reasoning stream 目前被注释掉
282
+ // 如果需要可以启用
270
283
  },
271
- // 📝 Partial reply streaming callback (real-time preview)
272
284
  onPartialReply: async (payload) => {
285
+ // 🔑 steer follower不发送partial reply(让主dispatcher处理)
286
+ if (isSteerFollower) {
287
+ return;
288
+ }
289
+ const currentTaskId = getActiveTaskId();
290
+ const currentMessageId = getActiveMessageId();
273
291
  const text = payload.text ?? "";
274
- const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
275
- log(`[PARTIAL REPLY] 📝 Partial reply chunk received: session=${sessionId}, taskId=${taskId}`);
276
292
  try {
277
293
  if (text.length > 0) {
278
294
  await sendReasoningTextUpdate({
279
295
  config,
280
296
  sessionId,
281
- taskId,
282
- messageId,
297
+ taskId: currentTaskId,
298
+ messageId: currentMessageId,
283
299
  text,
284
300
  append: false,
285
301
  });
286
- log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update (append=false)`);
287
302
  }
288
303
  }
289
304
  catch (err) {
290
- error(`[PARTIAL REPLY] ❌ Failed to send partial reply reasoningText:`, err);
305
+ error(`[PARTIAL REPLY] ❌ Failed to send partial reply:`, err);
291
306
  }
292
307
  },
293
308
  },
294
309
  markDispatchIdle,
295
- startStatusInterval, // Expose this to be called immediately
296
- stopStatusInterval, // Expose this for manual control if needed
310
+ startStatusInterval,
311
+ stopStatusInterval,
297
312
  };
298
313
  }
@@ -0,0 +1,16 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ /**
3
+ * 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
4
+ */
5
+ export declare function setCachedContext(cfg: ClawdbotConfig, runtime: RuntimeEnv, accountId: string): void;
6
+ /**
7
+ * 尝试向当前活跃会话注入 steer 消息。
8
+ * 两层保险:
9
+ * 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
10
+ * 2. hasActiveTask(sessionId) 确认任务仍在运行
11
+ *
12
+ * @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
13
+ * @param message 要注入的用户消息文本
14
+ * @returns true=已注入,false=跳过
15
+ */
16
+ export declare function tryInjectSteer(sessionKey: string | undefined, message: string): Promise<boolean>;
@@ -0,0 +1,74 @@
1
+ // Steer message injector for CSPL hook integration
2
+ import { getSessionContext } from "./tools/session-manager.js";
3
+ import { hasActiveTask, getCurrentTaskId } from "./task-manager.js";
4
+ import { handleXYMessage } from "./bot.js";
5
+ import { logger } from "./utils/logger.js";
6
+ import { randomUUID } from "node:crypto";
7
+ let cachedCfg = null;
8
+ let cachedRuntime = null;
9
+ let cachedAccountId = "default";
10
+ /**
11
+ * 在 handleXYMessage 入口处调用,缓存 cfg/runtime 供 steer 注入使用。
12
+ */
13
+ export function setCachedContext(cfg, runtime, accountId) {
14
+ cachedCfg = cfg;
15
+ cachedRuntime = runtime;
16
+ cachedAccountId = accountId;
17
+ }
18
+ /**
19
+ * 尝试向当前活跃会话注入 steer 消息。
20
+ * 两层保险:
21
+ * 1. getSessionContext(sessionKey) 确认是当前 XY 活跃 session
22
+ * 2. hasActiveTask(sessionId) 确认任务仍在运行
23
+ *
24
+ * @param sessionKey 来自 after_tool_call ctx.sessionKey(per-peer 下精确对应一个 XY session)
25
+ * @param message 要注入的用户消息文本
26
+ * @returns true=已注入,false=跳过
27
+ */
28
+ export async function tryInjectSteer(sessionKey, message) {
29
+ if (!sessionKey) {
30
+ return false;
31
+ }
32
+ const sessionCtx = getSessionContext(sessionKey);
33
+ if (!sessionCtx) {
34
+ return false;
35
+ }
36
+ const { sessionId } = sessionCtx;
37
+ const activeTaskId = getCurrentTaskId(sessionId);
38
+ if (!hasActiveTask(sessionId)) {
39
+ return false;
40
+ }
41
+ if (!cachedCfg || !cachedRuntime) {
42
+ logger.error("[STEER] No cached cfg/runtime available, cannot inject");
43
+ return false;
44
+ }
45
+ // 3. 构造合成 A2A 消息(伪装成用户在当前会话中发送的新消息)
46
+ const syntheticMessage = {
47
+ jsonrpc: "2.0",
48
+ method: "tasks/send",
49
+ id: `steer-msg-${randomUUID()}`,
50
+ params: {
51
+ sessionId,
52
+ id: activeTaskId ?? `steer-task-${randomUUID()}`,
53
+ agentLoginSessionId: "",
54
+ message: {
55
+ role: "user",
56
+ parts: [{ kind: "text", text: message }],
57
+ },
58
+ },
59
+ };
60
+ console.log(`[STEER] Injecting steer for sessionId=${sessionId}, taskId=${syntheticMessage.params.id}`);
61
+ try {
62
+ await handleXYMessage({
63
+ cfg: cachedCfg,
64
+ runtime: cachedRuntime,
65
+ message: syntheticMessage,
66
+ accountId: cachedAccountId,
67
+ });
68
+ return true;
69
+ }
70
+ catch (err) {
71
+ logger.error(`[STEER] ❌ Failed to inject steer: ${err}`);
72
+ return false;
73
+ }
74
+ }
@@ -0,0 +1,55 @@
1
+ interface TaskIdBinding {
2
+ sessionId: string;
3
+ currentTaskId: string;
4
+ currentMessageId: string;
5
+ refCount: number;
6
+ updatedAt: number;
7
+ locked: boolean;
8
+ }
9
+ /**
10
+ * 注册或更新session的活跃taskId
11
+ * 返回是否是更新(用于判断是否是第二条消息)
12
+ */
13
+ export declare function registerTaskId(sessionId: string, taskId: string, messageId: string, options?: {
14
+ incrementRef?: boolean;
15
+ }): {
16
+ isUpdate: boolean;
17
+ refCount: number;
18
+ };
19
+ /**
20
+ * 增加引用计数(消息开始处理时调用)
21
+ */
22
+ export declare function incrementTaskIdRef(sessionId: string): void;
23
+ /**
24
+ * 减少引用计数,当refCount=0时才真正清理
25
+ */
26
+ export declare function decrementTaskIdRef(sessionId: string): void;
27
+ /**
28
+ * 锁定taskId,防止被清理(第一个消息使用)
29
+ */
30
+ export declare function lockTaskId(sessionId: string): void;
31
+ /**
32
+ * 解锁taskId(第一个消息完成时使用)
33
+ */
34
+ export declare function unlockTaskId(sessionId: string): void;
35
+ /**
36
+ * 获取session的当前活跃taskId
37
+ */
38
+ export declare function getCurrentTaskId(sessionId: string): string | null;
39
+ /**
40
+ * 获取session的当前活跃messageId
41
+ */
42
+ export declare function getCurrentMessageId(sessionId: string): string | null;
43
+ /**
44
+ * 检查session是否有活跃的taskId
45
+ */
46
+ export declare function hasActiveTask(sessionId: string): boolean;
47
+ /**
48
+ * 获取完整的binding信息(用于调试)
49
+ */
50
+ export declare function getTaskIdBinding(sessionId: string): TaskIdBinding | null;
51
+ /**
52
+ * 强制清理(错误恢复用)
53
+ */
54
+ export declare function forceCleanTaskId(sessionId: string): void;
55
+ export {};