@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 +78 -9
- package/dist/src/bot.d.ts +6 -0
- package/dist/src/bot.js +32 -22
- package/dist/src/cspl/steer-context.d.ts +17 -3
- package/dist/src/cspl/steer-context.js +15 -7
- package/package.json +1 -1
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 {
|
|
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
|
|
24
|
-
// CSPL security scanning via
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
20
|
+
export async function tryInjectSteer(params) {
|
|
21
|
+
const { sessionId, taskId, message, source } = params;
|
|
15
22
|
if (!cachedCfg || !cachedRuntime) {
|
|
16
|
-
logger.error(
|
|
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: `
|
|
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(`[
|
|
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(`[
|
|
52
|
+
logger.error(`[STEER:${source}] Failed to inject steer: ${err}`);
|
|
45
53
|
return false;
|
|
46
54
|
}
|
|
47
55
|
}
|