@ynhcj/xiaoyi-channel 0.0.125-next β†’ 0.0.126-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.
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";
@@ -20,13 +26,66 @@ function registerFullHooks(api) {
20
26
  api.on("before_prompt_build", beforePromptBuildHandler);
21
27
  registerSelfEvolutionToolResultNudge(api);
22
28
  }
23
- function registerCsplMiddleware(api) {
24
- // CSPL security scanning via AgentToolResultMiddleware.
25
- // Intercepts tool results BEFORE they reach the LLM, so on REJECT
26
- // the result is replaced entirely β€” no steer injection needed.
27
- // Only registered in "full" mode because it depends on session context
28
- // for CSPL config resolution.
29
- api.registerAgentToolResultMiddleware(createCsplMiddleware(), { runtimes: ["pi"] });
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
+ }
88
+ });
30
89
  }
31
90
  export default definePluginEntry({
32
91
  id: "xiaoyi-channel",
@@ -36,12 +95,22 @@ export default definePluginEntry({
36
95
  // Always register the provider so wrapStreamFn/prepareExtraParams work
37
96
  // in ALL registration modes (not just "full").
38
97
  api.registerProvider(xiaoyiProvider);
98
+ if (api.registrationMode === "cli-metadata") {
99
+ return;
100
+ }
101
+ if (api.registrationMode === "tool-discovery") {
102
+ registerFullHooks(api);
103
+ return;
104
+ }
39
105
  // Register channel plugin and set runtime
40
106
  api.registerChannel({ plugin: xyPlugin });
41
107
  setXYRuntime(api.runtime);
108
+ if (api.registrationMode === "discovery") {
109
+ return;
110
+ }
42
111
  if (api.registrationMode === "full") {
43
112
  registerFullHooks(api);
44
- registerCsplMiddleware(api);
113
+ registerCsplHook(api);
45
114
  }
46
115
  },
47
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
@@ -101,12 +101,16 @@ export async function handleXYMessage(params) {
101
101
  // ========================================
102
102
  // πŸ”‘ ζ³¨ε†ŒtaskIdοΌˆζ£€ζ΅‹ζ˜―ε¦ζ˜―ε·²ζœ‰ζ΄»θ·ƒδ»»εŠ‘ηš„ sessionοΌ‰
103
103
  const isUpdate = hasActiveTask(parsed.sessionId);
104
+ const skipReg = params.skipRegistration === true;
104
105
  if (isUpdate) {
105
106
  logger.log(`[BOT] πŸ”„ STEER MODE - Second message detected (core will handle steer)`);
106
107
  logger.log(`[BOT] - Session: ${parsed.sessionId}`);
107
108
  logger.log(`[BOT] - New taskId: ${parsed.taskId}`);
108
109
  }
109
- registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
110
+ // Steer injections skip taskId registration to avoid overwriting the active taskId
111
+ if (!skipReg) {
112
+ registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
113
+ }
110
114
  // Extract and update push_id if present
111
115
  const pushId = extractPushId(parsed.parts);
112
116
  if (pushId) {
@@ -147,26 +151,29 @@ export async function handleXYMessage(params) {
147
151
  },
148
152
  });
149
153
  logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
150
- registerSession(route.sessionKey, {
151
- config,
152
- sessionId: parsed.sessionId,
153
- taskId: parsed.taskId,
154
- messageId: parsed.messageId,
155
- agentId: route.accountId,
156
- deviceType,
157
- });
158
- // πŸ”‘ ε‘ι€εˆε§‹ηŠΆζ€ζ›΄ζ–°
159
- logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
160
- void sendStatusUpdate({
161
- config,
162
- sessionId: parsed.sessionId,
163
- taskId: parsed.taskId,
164
- messageId: parsed.messageId,
165
- text: "δ»»εŠ‘ζ­£εœ¨ε€„η†δΈ­οΌŒθ―·η¨ε€™~",
166
- state: "working",
167
- }).catch((err) => {
168
- logger.error(`Failed to send initial status update:`, err);
169
- });
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
+ }
170
177
  // Extract text and files from parts
171
178
  const text = extractTextFromParts(parsed.parts);
172
179
  let textForAgent = text || "";
@@ -255,7 +262,10 @@ export async function handleXYMessage(params) {
255
262
  accountId: route.accountId,
256
263
  steerState,
257
264
  });
258
- startStatusInterval();
265
+ // Steer injections don't need status intervals
266
+ if (!skipReg) {
267
+ startStatusInterval();
268
+ }
259
269
  // Build session context for AsyncLocalStorage
260
270
  const sessionContext = {
261
271
  config,
@@ -1,7 +1,21 @@
1
1
  import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ /** Called from handleXYMessage on every inbound A2A message to keep cfg/runtime fresh. */
2
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
+ }
3
12
  /**
4
- * Inject a steer message into the given session by constructing a synthetic
5
- * A2A message and dispatching it through handleXYMessage.
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.
6
20
  */
7
- export declare function injectCsplSteer(sessionId: string, taskId: string, message: string): Promise<boolean>;
21
+ export declare function tryInjectSteer(params: SteerInjectionParams): Promise<boolean>;
@@ -3,23 +3,30 @@ import { logger } from "../utils/logger.js";
3
3
  import { randomUUID } from "node:crypto";
4
4
  let cachedCfg = null;
5
5
  let cachedRuntime = null;
6
+ /** Called from handleXYMessage on every inbound A2A message to keep cfg/runtime fresh. */
6
7
  export function setCsplSteerContext(cfg, runtime) {
7
8
  cachedCfg = cfg;
8
9
  cachedRuntime = runtime;
9
10
  }
10
11
  /**
11
- * Inject a steer message into the given session by constructing a synthetic
12
- * A2A message and dispatching it through handleXYMessage.
12
+ * Inject a steer message into an active session by constructing a synthetic
13
+ * A2A tasks/send message and dispatching it through handleXYMessage.
14
+ *
15
+ * Uses skipRegistration so the steer message doesn't register a new taskId,
16
+ * increment session refCount, or send extra status updates.
17
+ *
18
+ * Returns true if the injection was dispatched successfully.
13
19
  */
14
- export async function injectCsplSteer(sessionId, taskId, message) {
20
+ export async function tryInjectSteer(params) {
21
+ const { sessionId, taskId, message, source } = params;
15
22
  if (!cachedCfg || !cachedRuntime) {
16
- logger.error("[CSPL STEER] No cached cfg/runtime, cannot inject steer");
23
+ logger.error(`[STEER:${source}] No cached cfg/runtime, cannot inject steer`);
17
24
  return false;
18
25
  }
19
26
  const syntheticMessage = {
20
27
  jsonrpc: "2.0",
21
28
  method: "tasks/send",
22
- id: `cspl-steer-${randomUUID()}`,
29
+ id: `steer-${source}-${randomUUID()}`,
23
30
  params: {
24
31
  sessionId,
25
32
  id: taskId,
@@ -30,18 +37,19 @@ export async function injectCsplSteer(sessionId, taskId, message) {
30
37
  },
31
38
  },
32
39
  };
33
- logger.log(`[CSPL STEER] Injecting steer for sessionId=${sessionId}, taskId=${taskId}`);
40
+ logger.log(`[STEER:${source}] Injecting steer for sessionId=${sessionId}, taskId=${taskId}`);
34
41
  try {
35
42
  await handleXYMessage({
36
43
  cfg: cachedCfg,
37
44
  runtime: cachedRuntime,
38
45
  message: syntheticMessage,
39
46
  accountId: "default",
47
+ skipRegistration: true,
40
48
  });
41
49
  return true;
42
50
  }
43
51
  catch (err) {
44
- logger.error(`[CSPL STEER] Failed to inject steer: ${err}`);
52
+ logger.error(`[STEER:${source}] Failed to inject steer: ${err}`);
45
53
  return false;
46
54
  }
47
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "0.0.125-next",
3
+ "version": "0.0.126-next",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",