@ynhcj/xiaoyi-channel 0.0.136-beta → 0.0.138-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { definePluginEntry } from "openclaw/plugin-sdk/core";
2
2
  import { xiaoyiProvider } from "./src/provider.js";
3
3
  import { xyPlugin } from "./src/channel.js";
4
- import { createCsplMiddleware } from "./src/cspl/middleware.js";
4
+ import { callCsplApiWithConfig } from "./src/cspl/call-api.js";
5
+ import { getCsplConfig, initCsplConfigFromXYConfig } from "./src/cspl/config.js";
6
+ import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
7
+ import { extractResultText, parseSecurityResult, processText, validateAndTruncateText, } from "./src/cspl/utils.js";
8
+ import { tryInjectSteer } from "./src/cspl/steer-context.js";
9
+ import { getSessionContext } from "./src/tools/session-manager.js";
10
+ import { logger } from "./src/utils/logger.js";
5
11
  import { setXYRuntime } from "./src/runtime.js";
6
12
  import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
7
13
  import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
@@ -19,11 +25,66 @@ function registerFullHooks(api) {
19
25
  const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
20
26
  api.on("before_prompt_build", beforePromptBuildHandler);
21
27
  registerSelfEvolutionToolResultNudge(api);
22
- // CSPL security scanning via AgentToolResultMiddleware.
23
- // Intercepts tool results BEFORE they reach the LLM, replacing them
24
- // with a security rejection message when CSPL returns REJECT.
25
- api.registerAgentToolResultMiddleware(createCsplMiddleware(), {
26
- runtimes: ["pi"],
28
+ }
29
+ function registerCsplHook(api) {
30
+ // CSPL security scanning via after_tool_call hook.
31
+ // When CSPL returns REJECT, injects a steer message via tryInjectSteer
32
+ // to interrupt the agent. Uses skipRegistration to avoid refCount leaks
33
+ // and taskId overwrites.
34
+ // Only registered in "full" mode because it depends on handleXYMessage
35
+ // having cached cfg/runtime via setCsplSteerContext.
36
+ api.on("after_tool_call", async (event, ctx) => {
37
+ if (!ALLOWED_TOOLS.includes(event.toolName)) {
38
+ return;
39
+ }
40
+ try {
41
+ const resultText = extractResultText(event, event.toolName);
42
+ const resultLength = resultText.length;
43
+ if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
44
+ return;
45
+ }
46
+ logger.log(`[SENTINEL HOOK] after_tool_call: toolName=${event.toolName}, textLength=${resultLength}`);
47
+ const questionText = {
48
+ subSceneID: "TOOL_OUTPUT",
49
+ tool: event.toolName,
50
+ output: [{ content: "" }],
51
+ };
52
+ const originText = processText(resultText);
53
+ questionText.output[0].content = originText;
54
+ let finalJson = JSON.stringify(questionText);
55
+ if (finalJson.length > MAX_TEXT_LENGTH) {
56
+ const diff = finalJson.length - MAX_TEXT_LENGTH;
57
+ const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
58
+ questionText.output[0].content = trimmed;
59
+ finalJson = JSON.stringify(questionText);
60
+ }
61
+ const sessionCtx = getSessionContext(ctx.sessionKey ?? "");
62
+ const csplConfig = sessionCtx
63
+ ? initCsplConfigFromXYConfig(sessionCtx.config)
64
+ : getCsplConfig();
65
+ const csplStartTime = Date.now();
66
+ const response = await callCsplApiWithConfig(finalJson, csplConfig);
67
+ const csplElapsed = Date.now() - csplStartTime;
68
+ const result = parseSecurityResult(response);
69
+ logger.log(`[SENTINEL HOOK] Security result: status=${result.status}, toolName=${event.toolName}, elapsed=${csplElapsed}ms`);
70
+ if (result.status === "REJECT") {
71
+ logger.log(`[SENTINEL HOOK] REJECT - injecting steer via tryInjectSteer`);
72
+ if (sessionCtx) {
73
+ await tryInjectSteer({
74
+ sessionId: sessionCtx.sessionId,
75
+ taskId: sessionCtx.taskId,
76
+ message: STEER_ABORT_MESSAGE,
77
+ source: "cspl",
78
+ });
79
+ }
80
+ else {
81
+ logger.error("[SENTINEL HOOK] No session context, cannot inject steer");
82
+ }
83
+ }
84
+ }
85
+ catch (err) {
86
+ logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
87
+ }
27
88
  });
28
89
  }
29
90
  export default definePluginEntry({
@@ -49,6 +110,7 @@ export default definePluginEntry({
49
110
  }
50
111
  if (api.registrationMode === "full") {
51
112
  registerFullHooks(api);
113
+ registerCsplHook(api);
52
114
  }
53
115
  },
54
116
  });
package/dist/src/bot.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface HandleXYMessageParams {
11
11
  webSocketSessionId?: string;
12
12
  /** Called after dispatch init is complete (agentTools/wrapStreamFn done). */
13
13
  onInitComplete?: () => void;
14
+ /**
15
+ * When true, skip taskId/session registration. Used by tryInjectSteer to
16
+ * inject a steer message without overwriting the active taskId or leaking
17
+ * session refCount.
18
+ */
19
+ skipRegistration?: boolean;
14
20
  }
15
21
  /**
16
22
  * Handle an incoming A2A message.
package/dist/src/bot.js CHANGED
@@ -12,6 +12,7 @@ import { getPushDataById } from "./utils/pushdata-manager.js";
12
12
  import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
13
13
  import { saveRuntimeInfo } from "./utils/runtime-manager.js";
14
14
  import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
15
+ import { setCsplSteerContext } from "./cspl/steer-context.js";
15
16
  import { registerTaskId, decrementTaskIdRef, hasActiveTask, } from "./task-manager.js";
16
17
  import { logger } from "./utils/logger.js";
17
18
  /**
@@ -21,6 +22,8 @@ import { logger } from "./utils/logger.js";
21
22
  */
22
23
  export async function handleXYMessage(params) {
23
24
  const { cfg, runtime, message, accountId, webSocketSessionId } = params;
25
+ // Cache context for CSPL steer injection (after_tool_call hook)
26
+ setCsplSteerContext(cfg, runtime);
24
27
  // Get runtime (already validated in monitor.ts, but get reference for use)
25
28
  const core = getXYRuntime();
26
29
  try {
@@ -98,37 +101,41 @@ export async function handleXYMessage(params) {
98
101
  // ========================================
99
102
  // 🔑 注册taskId(检测是否是已有活跃任务的 session)
100
103
  const isUpdate = hasActiveTask(parsed.sessionId);
104
+ const skipReg = params.skipRegistration === true;
101
105
  if (isUpdate) {
102
106
  logger.log(`[BOT] 🔄 STEER MODE - Second message detected (core will handle steer)`);
103
107
  logger.log(`[BOT] - Session: ${parsed.sessionId}`);
104
108
  logger.log(`[BOT] - New taskId: ${parsed.taskId}`);
105
109
  }
106
- registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
107
- // Extract and update push_id if present
108
- const pushId = extractPushId(parsed.parts);
109
- if (pushId) {
110
- logger.log(`[BOT] 📌 Extracted push_id from user message`);
111
- configManager.updatePushId(parsed.sessionId, pushId);
112
- // 持久化 pushId 到本地文件(异步,不阻塞主流程)
113
- addPushId(pushId).catch((err) => {
114
- logger.error(`[BOT] Failed to persist pushId:`, err);
110
+ // Steer injections skip taskId registration to avoid overwriting the active taskId
111
+ if (!skipReg) {
112
+ registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
113
+ // Extract and update push_id if present
114
+ const pushId = extractPushId(parsed.parts);
115
+ if (pushId) {
116
+ logger.log(`[BOT] 📌 Extracted push_id from user message`);
117
+ configManager.updatePushId(parsed.sessionId, pushId);
118
+ // 持久化 pushId 到本地文件(异步,不阻塞主流程)
119
+ addPushId(pushId).catch((err) => {
120
+ logger.error(`[BOT] Failed to persist pushId:`, err);
121
+ });
122
+ }
123
+ else {
124
+ logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
125
+ }
126
+ // 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
127
+ saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
128
+ parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
129
+ parsed.taskId // TASK_ID (param.id)
130
+ ).catch((err) => {
131
+ logger.error(`[BOT] Failed to save runtime info:`, err);
115
132
  });
116
133
  }
117
- else {
118
- logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
119
- }
120
- // Extract deviceType if present (same level as push_id in systemVariables)
134
+ // Extract deviceType if present (always parse — used in ctxPayload.MessageSid)
121
135
  const deviceType = extractDeviceType(parsed.parts);
122
136
  if (deviceType) {
123
137
  logger.log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
124
138
  }
125
- // 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
126
- saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
127
- parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
128
- parsed.taskId // TASK_ID (param.id)
129
- ).catch((err) => {
130
- logger.error(`[BOT] Failed to save runtime info:`, err);
131
- });
132
139
  // Resolve configuration (needed for status updates)
133
140
  const config = resolveXYConfig(cfg);
134
141
  // ✅ Resolve agent route (following feishu pattern)
@@ -144,30 +151,34 @@ export async function handleXYMessage(params) {
144
151
  },
145
152
  });
146
153
  logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
147
- registerSession(route.sessionKey, {
148
- config,
149
- sessionId: parsed.sessionId,
150
- taskId: parsed.taskId,
151
- messageId: parsed.messageId,
152
- agentId: route.accountId,
153
- deviceType,
154
- });
155
- // 🔑 发送初始状态更新
156
- logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
157
- void sendStatusUpdate({
158
- config,
159
- sessionId: parsed.sessionId,
160
- taskId: parsed.taskId,
161
- messageId: parsed.messageId,
162
- text: "任务正在处理中,请稍候~",
163
- state: "working",
164
- }).catch((err) => {
165
- logger.error(`Failed to send initial status update:`, err);
166
- });
154
+ // Steer injections skip session registration to avoid refCount leaks
155
+ if (!skipReg) {
156
+ registerSession(route.sessionKey, {
157
+ config,
158
+ sessionId: parsed.sessionId,
159
+ taskId: parsed.taskId,
160
+ messageId: parsed.messageId,
161
+ agentId: route.accountId,
162
+ deviceType,
163
+ });
164
+ // 🔑 发送初始状态更新
165
+ logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
166
+ void sendStatusUpdate({
167
+ config,
168
+ sessionId: parsed.sessionId,
169
+ taskId: parsed.taskId,
170
+ messageId: parsed.messageId,
171
+ text: "任务正在处理中,请稍候~",
172
+ state: "working",
173
+ }).catch((err) => {
174
+ logger.error(`Failed to send initial status update:`, err);
175
+ });
176
+ }
167
177
  // Extract text and files from parts
168
178
  const text = extractTextFromParts(parsed.parts);
169
179
  let textForAgent = text || "";
170
- if (route.sessionKey && textForAgent) {
180
+ // Self-evolution keyword nudge — only for real user messages, not steer injections
181
+ if (!skipReg && route.sessionKey && textForAgent) {
171
182
  try {
172
183
  const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
173
184
  if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
@@ -191,11 +202,14 @@ export async function handleXYMessage(params) {
191
202
  textForAgent = `/steer ${textForAgent}`;
192
203
  logger.log(`[BOT] 🔄 Prepended /steer for steer injection`);
193
204
  }
194
- const fileParts = extractFileParts(parsed.parts);
195
- // Download files to local disk
196
- const downloadedFiles = await downloadFilesFromParts(fileParts);
197
- logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
198
- const mediaPayload = buildXYMediaPayload(downloadedFiles);
205
+ // File download — only for real user messages, steer injections have no files
206
+ let mediaPayload = {};
207
+ if (!skipReg) {
208
+ const fileParts = extractFileParts(parsed.parts);
209
+ const downloadedFiles = await downloadFilesFromParts(fileParts);
210
+ logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
211
+ mediaPayload = buildXYMediaPayload(downloadedFiles);
212
+ }
199
213
  // Resolve envelope format options (following feishu pattern)
200
214
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
201
215
  // Build message body with speaker prefix (following feishu pattern)
@@ -252,7 +266,10 @@ export async function handleXYMessage(params) {
252
266
  accountId: route.accountId,
253
267
  steerState,
254
268
  });
255
- startStatusInterval();
269
+ // Steer injections don't need status intervals
270
+ if (!skipReg) {
271
+ startStatusInterval();
272
+ }
256
273
  // Build session context for AsyncLocalStorage
257
274
  const sessionContext = {
258
275
  config,
@@ -4,6 +4,6 @@ import type { ApiResponse } from "./constants.js";
4
4
  export declare function callCsplApi(questionText: string, cfg: ClawdbotConfig): Promise<ApiResponse>;
5
5
  /**
6
6
  * Call CSPL API with a pre-resolved CsplConfig.
7
- * Used by AgentToolResultMiddleware which doesn't have ClawdbotConfig.
7
+ * Used by after_tool_call hook which has session context but not ClawdbotConfig.
8
8
  */
9
9
  export declare function callCsplApiWithConfig(questionText: string, config: CsplConfig): Promise<ApiResponse>;
@@ -1,4 +1,5 @@
1
1
  // SENTINEL HOOK API 请求模块
2
+ import http from "node:http";
2
3
  import https from "node:https";
3
4
  import { URL } from "node:url";
4
5
  import { randomBytes } from "node:crypto";
@@ -43,18 +44,12 @@ function parseResponse(data) {
43
44
  }
44
45
  return json;
45
46
  }
46
- export async function callCsplApi(questionText, cfg) {
47
- const config = getCsplConfig(cfg);
48
- const headers = buildHeaders(config);
49
- const payload = {
50
- questionText,
51
- textSource: config.textSource,
52
- action: config.action,
53
- extra: JSON.stringify({ userId: config.uid }),
54
- };
47
+ function doApiRequest(url, headers, payload, timeout) {
48
+ const isHttp = url.startsWith("http://");
49
+ const module = isHttp ? http : https;
50
+ const options = buildRequestOptions(url, headers, timeout);
55
51
  return new Promise((resolve, reject) => {
56
- const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
57
- const req = https.request(options, (res) => {
52
+ const req = module.request(options, (res) => {
58
53
  if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
59
54
  reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
60
55
  return;
@@ -66,7 +61,7 @@ export async function callCsplApi(questionText, cfg) {
66
61
  res.on("end", () => {
67
62
  try {
68
63
  const result = parseResponse(data);
69
- logger.log(`[SENTINEL HOOK] ✅ 请求成功`);
64
+ logger.log(`[SENTINEL HOOK] ✅ 请求成功, securityResult=${result?.data?.securityResult ?? "N/A"}`);
70
65
  resolve(result);
71
66
  }
72
67
  catch (e) {
@@ -80,7 +75,7 @@ export async function callCsplApi(questionText, cfg) {
80
75
  reject(error);
81
76
  });
82
77
  req.on("timeout", () => {
83
- logger.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
78
+ logger.error(`[SENTINEL HOOK] ⏰ 请求超时 (${timeout}ms)`);
84
79
  req.destroy();
85
80
  reject(new Error("[SENTINEL HOOK] Request timeout"));
86
81
  });
@@ -88,9 +83,20 @@ export async function callCsplApi(questionText, cfg) {
88
83
  req.end();
89
84
  });
90
85
  }
86
+ export async function callCsplApi(questionText, cfg) {
87
+ const config = getCsplConfig(cfg);
88
+ const headers = buildHeaders(config);
89
+ const payload = {
90
+ questionText,
91
+ textSource: config.textSource,
92
+ action: config.action,
93
+ extra: JSON.stringify({ userId: config.uid }),
94
+ };
95
+ return doApiRequest(config.api.url, headers, payload, config.api.timeout);
96
+ }
91
97
  /**
92
98
  * Call CSPL API with a pre-resolved CsplConfig.
93
- * Used by AgentToolResultMiddleware which doesn't have ClawdbotConfig.
99
+ * Used by after_tool_call hook which has session context but not ClawdbotConfig.
94
100
  */
95
101
  export async function callCsplApiWithConfig(questionText, config) {
96
102
  const headers = buildHeaders(config);
@@ -100,39 +106,5 @@ export async function callCsplApiWithConfig(questionText, config) {
100
106
  action: config.action,
101
107
  extra: JSON.stringify({ userId: config.uid }),
102
108
  };
103
- return new Promise((resolve, reject) => {
104
- const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
105
- const req = https.request(options, (res) => {
106
- if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
107
- reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
108
- return;
109
- }
110
- let data = "";
111
- res.on("data", (chunk) => {
112
- data += chunk;
113
- });
114
- res.on("end", () => {
115
- try {
116
- const result = parseResponse(data);
117
- logger.log(`[SENTINEL HOOK] ✅ 请求成功`);
118
- resolve(result);
119
- }
120
- catch (e) {
121
- logger.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
122
- reject(e);
123
- }
124
- });
125
- });
126
- req.on("error", (error) => {
127
- logger.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
128
- reject(error);
129
- });
130
- req.on("timeout", () => {
131
- logger.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
132
- req.destroy();
133
- reject(new Error("[SENTINEL HOOK] Request timeout"));
134
- });
135
- req.write(JSON.stringify(payload));
136
- req.end();
137
- });
109
+ return doApiRequest(config.api.url, headers, payload, config.api.timeout);
138
110
  }
@@ -46,6 +46,7 @@ export function createCsplMiddleware() {
46
46
  if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
47
47
  return;
48
48
  }
49
+ logger.log(`[CSPL MIDDLEWARE] Scanning tool result: toolName=${event.toolName}, textLength=${resultLength}`);
49
50
  // Build CSPL request payload
50
51
  const questionText = {
51
52
  subSceneID: "TOOL_OUTPUT",
@@ -67,9 +68,11 @@ export function createCsplMiddleware() {
67
68
  const csplConfig = sessionCtx
68
69
  ? initCsplConfigFromXYConfig(sessionCtx.config)
69
70
  : getCsplConfig();
71
+ const csplStartTime = Date.now();
70
72
  const response = await callCsplApiWithConfig(finalJson, csplConfig);
73
+ const csplElapsed = Date.now() - csplStartTime;
71
74
  const result = parseSecurityResult(response);
72
- logger.log(`[CSPL MIDDLEWARE] Security result: status=${result.status}, toolName=${event.toolName}`);
75
+ logger.log(`[CSPL MIDDLEWARE] Security result: status=${result.status}, toolName=${event.toolName}, elapsed=${csplElapsed}ms`);
73
76
  if (result.status === "REJECT") {
74
77
  logger.log(`[CSPL MIDDLEWARE] REJECT - replacing tool result with security message`);
75
78
  return {
@@ -0,0 +1,21 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ /** Called from handleXYMessage on every inbound A2A message to keep cfg/runtime fresh. */
3
+ export declare function setCsplSteerContext(cfg: ClawdbotConfig, runtime: RuntimeEnv): void;
4
+ /** Parameters for steer message injection. */
5
+ export interface SteerInjectionParams {
6
+ sessionId: string;
7
+ taskId: string;
8
+ message: string;
9
+ /** Human-readable source label for logging (e.g. "cspl", "self-evolution"). */
10
+ source: string;
11
+ }
12
+ /**
13
+ * Inject a steer message into an active session by constructing a synthetic
14
+ * A2A tasks/send message and dispatching it through handleXYMessage.
15
+ *
16
+ * Uses skipRegistration so the steer message doesn't register a new taskId,
17
+ * increment session refCount, or send extra status updates.
18
+ *
19
+ * Returns true if the injection was dispatched successfully.
20
+ */
21
+ export declare function tryInjectSteer(params: SteerInjectionParams): Promise<boolean>;
@@ -0,0 +1,78 @@
1
+ import { handleXYMessage } from "../bot.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { randomUUID } from "node:crypto";
4
+ // Use globalThis to ensure a single cache across all module copies.
5
+ // The xy_channel plugin may be loaded by openclaw from different module
6
+ // resolution paths, causing steer-context.ts to be instantiated multiple
7
+ // times. globalThis guarantees all copies share the same cfg/runtime.
8
+ const _g = globalThis;
9
+ if (!_g.__xySteerCachedCfg) {
10
+ _g.__xySteerCachedCfg = null;
11
+ }
12
+ if (!_g.__xySteerCachedRuntime) {
13
+ _g.__xySteerCachedRuntime = null;
14
+ }
15
+ function getCachedCfg() {
16
+ return _g.__xySteerCachedCfg;
17
+ }
18
+ function setCachedCfg(cfg) {
19
+ _g.__xySteerCachedCfg = cfg;
20
+ }
21
+ function getCachedRuntime() {
22
+ return _g.__xySteerCachedRuntime;
23
+ }
24
+ function setCachedRuntime(runtime) {
25
+ _g.__xySteerCachedRuntime = runtime;
26
+ }
27
+ /** Called from handleXYMessage on every inbound A2A message to keep cfg/runtime fresh. */
28
+ export function setCsplSteerContext(cfg, runtime) {
29
+ setCachedCfg(cfg);
30
+ setCachedRuntime(runtime);
31
+ }
32
+ /**
33
+ * Inject a steer message into an active session by constructing a synthetic
34
+ * A2A tasks/send message and dispatching it through handleXYMessage.
35
+ *
36
+ * Uses skipRegistration so the steer message doesn't register a new taskId,
37
+ * increment session refCount, or send extra status updates.
38
+ *
39
+ * Returns true if the injection was dispatched successfully.
40
+ */
41
+ export async function tryInjectSteer(params) {
42
+ const { sessionId, taskId, message, source } = params;
43
+ const cfg = getCachedCfg();
44
+ const runtime = getCachedRuntime();
45
+ if (!cfg || !runtime) {
46
+ logger.error(`[STEER:${source}] No cached cfg/runtime, cannot inject steer`);
47
+ return false;
48
+ }
49
+ const syntheticMessage = {
50
+ jsonrpc: "2.0",
51
+ method: "tasks/send",
52
+ id: `steer-${source}-${randomUUID()}`,
53
+ params: {
54
+ sessionId,
55
+ id: taskId,
56
+ agentLoginSessionId: "",
57
+ message: {
58
+ role: "user",
59
+ parts: [{ kind: "text", text: message }],
60
+ },
61
+ },
62
+ };
63
+ logger.log(`[STEER:${source}] Injecting steer for sessionId=${sessionId}, taskId=${taskId}`);
64
+ try {
65
+ await handleXYMessage({
66
+ cfg,
67
+ runtime,
68
+ message: syntheticMessage,
69
+ accountId: "default",
70
+ skipRegistration: true,
71
+ });
72
+ return true;
73
+ }
74
+ catch (err) {
75
+ logger.error(`[STEER:${source}] Failed to inject steer: ${err}`);
76
+ return false;
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.136-beta",
3
+ "version": "0.0.138-beta",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",