@ynhcj/xiaoyi-channel 0.0.128-beta โ 0.0.128-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 +36 -7
- package/dist/src/bot.d.ts +8 -0
- package/dist/src/bot.js +151 -139
- package/dist/src/client.d.ts +1 -5
- package/dist/src/client.js +25 -39
- package/dist/src/cspl/call-api.d.ts +6 -0
- package/dist/src/cspl/call-api.js +33 -13
- package/dist/src/cspl/config.d.ts +11 -1
- package/dist/src/cspl/config.js +30 -0
- package/dist/src/cspl/middleware.d.ts +8 -0
- package/dist/src/cspl/middleware.js +90 -0
- package/dist/src/cspl/steer-context.d.ts +21 -0
- package/dist/src/cspl/steer-context.js +78 -0
- package/dist/src/file-download.js +3 -3
- package/dist/src/formatter.d.ts +0 -2
- package/dist/src/formatter.js +28 -21
- package/dist/src/heartbeat.js +3 -2
- package/dist/src/login-token-handler.js +13 -10
- package/dist/src/message-queue.js +2 -1
- package/dist/src/monitor.js +55 -46
- package/dist/src/outbound.js +3 -0
- package/dist/src/provider.js +82 -34
- package/dist/src/push.js +9 -9
- package/dist/src/reply-dispatcher.d.ts +3 -1
- package/dist/src/reply-dispatcher.js +61 -68
- package/dist/src/self-evolution-handler.js +11 -14
- package/dist/src/skill-retriever/hooks.js +0 -1
- package/dist/src/skill-retriever/tool-search.js +7 -12
- package/dist/src/task-manager.d.ts +4 -27
- package/dist/src/task-manager.js +13 -78
- package/dist/src/tools/calendar-tool.js +5 -1
- package/dist/src/tools/call-phone-tool.js +5 -1
- package/dist/src/tools/create-alarm-tool.js +5 -1
- package/dist/src/tools/delete-alarm-tool.js +5 -1
- package/dist/src/tools/image-reading-tool.d.ts +1 -1
- package/dist/src/tools/image-reading-tool.js +38 -114
- package/dist/src/tools/location-tool.js +5 -1
- package/dist/src/tools/login-token-tool.js +13 -2
- package/dist/src/tools/modify-alarm-tool.js +5 -1
- package/dist/src/tools/modify-note-tool.js +5 -1
- package/dist/src/tools/note-tool.js +5 -1
- package/dist/src/tools/query-app-message-tool.js +5 -1
- package/dist/src/tools/query-memory-data-tool.js +5 -1
- package/dist/src/tools/query-todo-task-tool.js +5 -1
- package/dist/src/tools/save-file-to-phone-tool.js +5 -1
- package/dist/src/tools/save-media-to-gallery-tool.js +5 -1
- package/dist/src/tools/search-alarm-tool.js +5 -1
- package/dist/src/tools/search-calendar-tool.js +5 -1
- package/dist/src/tools/search-contact-tool.js +5 -1
- package/dist/src/tools/search-email-tool.js +5 -1
- package/dist/src/tools/search-file-tool.js +5 -1
- package/dist/src/tools/search-message-tool.js +5 -1
- package/dist/src/tools/search-note-tool.js +5 -1
- package/dist/src/tools/search-photo-gallery-tool.js +5 -1
- package/dist/src/tools/send-email-tool.js +5 -1
- package/dist/src/tools/send-file-to-user-tool.js +8 -5
- package/dist/src/tools/send-message-tool.js +5 -1
- package/dist/src/tools/session-helper.d.ts +24 -0
- package/dist/src/tools/session-helper.js +45 -0
- package/dist/src/tools/session-manager.d.ts +8 -0
- package/dist/src/tools/session-manager.js +2 -14
- package/dist/src/tools/upload-file-tool.js +5 -1
- package/dist/src/tools/upload-photo-tool.js +5 -1
- package/dist/src/tools/xiaoyi-add-collection-tool.js +5 -1
- package/dist/src/tools/xiaoyi-collection-tool.js +5 -1
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +5 -1
- package/dist/src/tools/xiaoyi-gui-tool.js +7 -2
- package/dist/src/trigger-handler.js +8 -9
- package/dist/src/utils/logger.js +106 -22
- package/dist/src/utils/throw.d.ts +5 -0
- package/dist/src/utils/throw.js +10 -0
- package/dist/src/websocket.js +4 -2
- package/dist/src/xy-session-store.d.ts +79 -0
- package/dist/src/xy-session-store.js +153 -0
- package/openclaw.plugin.json +3 -0
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
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";
|
|
5
6
|
import { ALLOWED_TOOLS, MAX_TEXT_LENGTH, MAX_TOTAL_LENGTH, MIN_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
|
|
6
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";
|
|
7
11
|
import { setXYRuntime } from "./src/runtime.js";
|
|
8
|
-
import { tryInjectSteer } from "./src/steer-injector.js";
|
|
9
12
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
10
13
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
11
14
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
@@ -22,17 +25,25 @@ function registerFullHooks(api) {
|
|
|
22
25
|
const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
|
|
23
26
|
api.on("before_prompt_build", beforePromptBuildHandler);
|
|
24
27
|
registerSelfEvolutionToolResultNudge(api);
|
|
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.
|
|
25
36
|
api.on("after_tool_call", async (event, ctx) => {
|
|
26
37
|
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
27
38
|
return;
|
|
28
39
|
}
|
|
29
|
-
console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
30
40
|
try {
|
|
31
41
|
const resultText = extractResultText(event, event.toolName);
|
|
32
42
|
const resultLength = resultText.length;
|
|
33
43
|
if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
|
|
34
44
|
return;
|
|
35
45
|
}
|
|
46
|
+
logger.log(`[SENTINEL HOOK] after_tool_call: toolName=${event.toolName}, textLength=${resultLength}`);
|
|
36
47
|
const questionText = {
|
|
37
48
|
subSceneID: "TOOL_OUTPUT",
|
|
38
49
|
tool: event.toolName,
|
|
@@ -47,15 +58,32 @@ function registerFullHooks(api) {
|
|
|
47
58
|
questionText.output[0].content = trimmed;
|
|
48
59
|
finalJson = JSON.stringify(questionText);
|
|
49
60
|
}
|
|
50
|
-
const
|
|
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;
|
|
51
68
|
const result = parseSecurityResult(response);
|
|
52
|
-
|
|
69
|
+
logger.log(`[SENTINEL HOOK] Security result: status=${result.status}, toolName=${event.toolName}, elapsed=${csplElapsed}ms`);
|
|
53
70
|
if (result.status === "REJECT") {
|
|
54
|
-
|
|
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
|
+
}
|
|
55
83
|
}
|
|
56
84
|
}
|
|
57
85
|
catch (err) {
|
|
58
|
-
|
|
86
|
+
logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
|
|
59
87
|
}
|
|
60
88
|
});
|
|
61
89
|
}
|
|
@@ -82,6 +110,7 @@ export default definePluginEntry({
|
|
|
82
110
|
}
|
|
83
111
|
if (api.registrationMode === "full") {
|
|
84
112
|
registerFullHooks(api);
|
|
113
|
+
registerCsplHook(api);
|
|
85
114
|
}
|
|
86
115
|
},
|
|
87
116
|
});
|
package/dist/src/bot.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ export interface HandleXYMessageParams {
|
|
|
9
9
|
message: A2AJsonRpcRequest;
|
|
10
10
|
accountId: string;
|
|
11
11
|
webSocketSessionId?: string;
|
|
12
|
+
/** Called after dispatch init is complete (agentTools/wrapStreamFn done). */
|
|
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;
|
|
12
20
|
}
|
|
13
21
|
/**
|
|
14
22
|
* Handle an incoming A2A message.
|
package/dist/src/bot.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
|
-
import { setCachedContext } from "./steer-injector.js";
|
|
3
2
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
4
3
|
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
5
4
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
@@ -13,7 +12,9 @@ import { getPushDataById } from "./utils/pushdata-manager.js";
|
|
|
13
12
|
import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
14
13
|
import { saveRuntimeInfo } from "./utils/runtime-manager.js";
|
|
15
14
|
import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
|
|
16
|
-
import {
|
|
15
|
+
import { setCsplSteerContext } from "./cspl/steer-context.js";
|
|
16
|
+
import { registerTaskId, decrementTaskIdRef, hasActiveTask, } from "./task-manager.js";
|
|
17
|
+
import { logger } from "./utils/logger.js";
|
|
17
18
|
/**
|
|
18
19
|
* Handle an incoming A2A message.
|
|
19
20
|
* This is the main entry point for message processing.
|
|
@@ -21,10 +22,8 @@ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActive
|
|
|
21
22
|
*/
|
|
22
23
|
export async function handleXYMessage(params) {
|
|
23
24
|
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// ๆฏๆฌกๆถๅฐๆถๆฏๆถๆดๆฐ็ผๅญ๏ผไพ steer ๆณจๅ
ฅไฝฟ็จ
|
|
27
|
-
setCachedContext(cfg, runtime, accountId);
|
|
25
|
+
// Cache context for CSPL steer injection (after_tool_call hook)
|
|
26
|
+
setCsplSteerContext(cfg, runtime);
|
|
28
27
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
29
28
|
const core = getXYRuntime();
|
|
30
29
|
try {
|
|
@@ -36,7 +35,7 @@ export async function handleXYMessage(params) {
|
|
|
36
35
|
if (!sessionId) {
|
|
37
36
|
throw new Error("clearContext request missing sessionId in params");
|
|
38
37
|
}
|
|
39
|
-
log(`Clear context request for session ${sessionId}`);
|
|
38
|
+
logger.log(`Clear context request for session ${sessionId}`);
|
|
40
39
|
const config = resolveXYConfig(cfg);
|
|
41
40
|
await sendClearContextResponse({
|
|
42
41
|
config,
|
|
@@ -52,7 +51,7 @@ export async function handleXYMessage(params) {
|
|
|
52
51
|
if (!sessionId) {
|
|
53
52
|
throw new Error("tasks/cancel request missing sessionId in params");
|
|
54
53
|
}
|
|
55
|
-
log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
|
|
54
|
+
logger.log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
|
|
56
55
|
const config = resolveXYConfig(cfg);
|
|
57
56
|
await sendTasksCancelResponse({
|
|
58
57
|
config,
|
|
@@ -68,18 +67,18 @@ export async function handleXYMessage(params) {
|
|
|
68
67
|
// ๅฆๆๆถๆฏไธญๅ
ๅซ Trigger ไบไปถๆฐๆฎ๏ผ็ดๆฅ่ฟๅ pushData ๅ
ๅฎน๏ผไธ่ตฐๆญฃๅธธๆต็จ
|
|
69
68
|
const triggerData = extractTriggerData(parsed.parts);
|
|
70
69
|
if (triggerData) {
|
|
71
|
-
log(`[BOT] ๐ Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
|
|
72
|
-
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
73
|
-
log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
70
|
+
logger.log(`[BOT] ๐ Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
|
|
71
|
+
logger.log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
72
|
+
logger.log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
74
73
|
try {
|
|
75
74
|
// ่ฏปๅ pushData
|
|
76
75
|
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
77
76
|
if (!pushDataItem) {
|
|
78
|
-
error(`[BOT] โ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
77
|
+
logger.error(`[BOT] โ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
79
78
|
return;
|
|
80
79
|
}
|
|
81
|
-
log(`[BOT] โ
Found pushData, sending direct response`);
|
|
82
|
-
log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
80
|
+
logger.log(`[BOT] โ
Found pushData, sending direct response`);
|
|
81
|
+
logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
83
82
|
const config = resolveXYConfig(cfg);
|
|
84
83
|
// ็ดๆฅๅ้ๅๅบ๏ผfinal=true๏ผไธ่ตฐ openclaw ๆต็จ๏ผ
|
|
85
84
|
await sendA2AResponse({
|
|
@@ -90,58 +89,53 @@ export async function handleXYMessage(params) {
|
|
|
90
89
|
text: pushDataItem.dataDetail,
|
|
91
90
|
append: false,
|
|
92
91
|
final: true,
|
|
93
|
-
runtime,
|
|
94
92
|
});
|
|
95
|
-
log(`[BOT] โ
Trigger response sent successfully, exiting early`);
|
|
93
|
+
logger.log(`[BOT] โ
Trigger response sent successfully, exiting early`);
|
|
96
94
|
return; // ๆๅ่ฟๅ๏ผไธ็ปง็ปญๅค็
|
|
97
95
|
}
|
|
98
96
|
catch (err) {
|
|
99
|
-
error(`[BOT] โ Failed to handle Trigger message:`, err);
|
|
97
|
+
logger.error(`[BOT] โ Failed to handle Trigger message:`, err);
|
|
100
98
|
return;
|
|
101
99
|
}
|
|
102
100
|
}
|
|
103
101
|
// ========================================
|
|
104
|
-
// ๐
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
log(`[BOT] ๐ STEER MODE - Second message detected (will
|
|
109
|
-
log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
110
|
-
log(`[BOT] - New taskId: ${parsed.taskId}
|
|
102
|
+
// ๐ ๆณจๅtaskId๏ผๆฃๆตๆฏๅฆๆฏๅทฒๆๆดป่ทไปปๅก็ session๏ผ
|
|
103
|
+
const isUpdate = hasActiveTask(parsed.sessionId);
|
|
104
|
+
const skipReg = params.skipRegistration === true;
|
|
105
|
+
if (isUpdate) {
|
|
106
|
+
logger.log(`[BOT] ๐ STEER MODE - Second message detected (core will handle steer)`);
|
|
107
|
+
logger.log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
108
|
+
logger.log(`[BOT] - New taskId: ${parsed.taskId}`);
|
|
111
109
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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);
|
|
128
132
|
});
|
|
129
133
|
}
|
|
130
|
-
|
|
131
|
-
log(`[BOT] โน๏ธ No push_id found in message, will use config default`);
|
|
132
|
-
}
|
|
133
|
-
// Extract deviceType if present (same level as push_id in systemVariables)
|
|
134
|
+
// Extract deviceType if present (always parse โ used in ctxPayload.MessageSid)
|
|
134
135
|
const deviceType = extractDeviceType(parsed.parts);
|
|
135
136
|
if (deviceType) {
|
|
136
|
-
log(`[BOT] ๐ฑ Extracted deviceType from user message: ${deviceType}`);
|
|
137
|
+
logger.log(`[BOT] ๐ฑ Extracted deviceType from user message: ${deviceType}`);
|
|
137
138
|
}
|
|
138
|
-
// ไฟๅญ runtime ไฟกๆฏๅฐ .xiaoyiruntime ๆไปถ๏ผๅผๆญฅ๏ผไธ้ปๅกไธปๆต็จ๏ผ
|
|
139
|
-
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket ๅฑ็บง๏ผๅฆๆๆฒกๆๅ fallback)
|
|
140
|
-
parsed.sessionId, // CONVERSATION_ID (param ้็ sessionId)
|
|
141
|
-
parsed.taskId // TASK_ID (param.id)
|
|
142
|
-
).catch((err) => {
|
|
143
|
-
error(`[BOT] Failed to save runtime info:`, err);
|
|
144
|
-
});
|
|
145
139
|
// Resolve configuration (needed for status updates)
|
|
146
140
|
const config = resolveXYConfig(cfg);
|
|
147
141
|
// โ
Resolve agent route (following feishu pattern)
|
|
@@ -156,55 +150,66 @@ export async function handleXYMessage(params) {
|
|
|
156
150
|
id: parsed.sessionId, // โ
Use sessionId to share context within the same conversation session
|
|
157
151
|
},
|
|
158
152
|
});
|
|
159
|
-
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
153
|
+
logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
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
|
+
}
|
|
181
177
|
// Extract text and files from parts
|
|
182
178
|
const text = extractTextFromParts(parsed.parts);
|
|
183
179
|
let textForAgent = text || "";
|
|
184
|
-
|
|
180
|
+
// Self-evolution keyword nudge โ only for real user messages, not steer injections
|
|
181
|
+
if (!skipReg && route.sessionKey && textForAgent) {
|
|
185
182
|
try {
|
|
186
183
|
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
187
184
|
if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
|
|
188
185
|
const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
|
|
189
|
-
log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
|
|
186
|
+
logger.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
|
|
190
187
|
if (shouldNudge) {
|
|
191
188
|
const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
|
|
192
189
|
textForAgent = augmented.text;
|
|
193
190
|
if (augmented.appended) {
|
|
194
|
-
log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
|
|
191
|
+
logger.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
|
|
195
192
|
}
|
|
196
193
|
}
|
|
197
194
|
}
|
|
198
195
|
}
|
|
199
196
|
catch (selfEvolutionError) {
|
|
200
|
-
error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
197
|
+
logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
201
198
|
}
|
|
202
199
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
200
|
+
// ๐ Steerๆถๆฏๅ /steer ๅ็ผ๏ผ่งฆๅcore็ queueEmbeddedPiMessage
|
|
201
|
+
if (isUpdate && textForAgent) {
|
|
202
|
+
textForAgent = `/steer ${textForAgent}`;
|
|
203
|
+
logger.log(`[BOT] ๐ Prepended /steer for steer injection`);
|
|
204
|
+
}
|
|
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
|
+
}
|
|
208
213
|
// Resolve envelope format options (following feishu pattern)
|
|
209
214
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
210
215
|
// Build message body with speaker prefix (following feishu pattern)
|
|
@@ -236,7 +241,7 @@ export async function handleXYMessage(params) {
|
|
|
236
241
|
SenderId: parsed.sessionId,
|
|
237
242
|
Provider: "xiaoyi-channel",
|
|
238
243
|
Surface: "xiaoyi-channel",
|
|
239
|
-
MessageSid: parsed.
|
|
244
|
+
MessageSid: `${parsed.taskId}_${deviceType}`,
|
|
240
245
|
Timestamp: Date.now(),
|
|
241
246
|
WasMentioned: false,
|
|
242
247
|
CommandAuthorized: true,
|
|
@@ -245,9 +250,13 @@ export async function handleXYMessage(params) {
|
|
|
245
250
|
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
246
251
|
...mediaPayload,
|
|
247
252
|
});
|
|
248
|
-
// ๐
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
// ๐ Dynamic steer state: when isUpdate (second message), start as steered=true
|
|
254
|
+
// so the dispatcher skips all user-facing callbacks (deliver, onIdle, etc.)
|
|
255
|
+
// and onSettled skips cleanup.
|
|
256
|
+
const steerState = { steered: isUpdate };
|
|
257
|
+
// ๐ ๅๅปบdispatcher
|
|
258
|
+
logger.log(`[BOT-DISPATCHER] ๐ฏ Creating reply dispatcher`);
|
|
259
|
+
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
251
260
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
252
261
|
cfg,
|
|
253
262
|
runtime,
|
|
@@ -255,13 +264,11 @@ export async function handleXYMessage(params) {
|
|
|
255
264
|
taskId: parsed.taskId,
|
|
256
265
|
messageId: parsed.messageId,
|
|
257
266
|
accountId: route.accountId,
|
|
258
|
-
|
|
267
|
+
steerState,
|
|
259
268
|
});
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
if (!isSecondMessage) {
|
|
269
|
+
// Steer injections don't need status intervals
|
|
270
|
+
if (!skipReg) {
|
|
263
271
|
startStatusInterval();
|
|
264
|
-
log(`[BOT-DISPATCHER] โ
Status interval started for first message`);
|
|
265
272
|
}
|
|
266
273
|
// Build session context for AsyncLocalStorage
|
|
267
274
|
const sessionContext = {
|
|
@@ -272,69 +279,74 @@ export async function handleXYMessage(params) {
|
|
|
272
279
|
agentId: route.accountId,
|
|
273
280
|
deviceType,
|
|
274
281
|
};
|
|
275
|
-
log(`[BOT-DISPATCH] โณ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
282
|
+
logger.log(`[BOT-DISPATCH] โณ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
276
283
|
await core.channel.reply.withReplyDispatcher({
|
|
277
284
|
dispatcher,
|
|
278
285
|
onSettled: () => {
|
|
279
|
-
log(`[BOT] ๐ onSettled called for session: ${route.sessionKey}`);
|
|
280
|
-
log(`[BOT] -
|
|
281
|
-
// ๐
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
unlockTaskId(parsed.sessionId);
|
|
286
|
-
log(`[BOT] ๐ Unlocked taskId (first message completed)`);
|
|
286
|
+
logger.log(`[BOT] ๐ onSettled called for session: ${route.sessionKey}`);
|
|
287
|
+
logger.log(`[BOT] - steered: ${steerState.steered}`);
|
|
288
|
+
// ๐ When steered, skip heavy cleanup โ the first message's dispatcher is still running
|
|
289
|
+
if (steerState.steered) {
|
|
290
|
+
logger.log(`[BOT] โ
Steered dispatch settled (skipping cleanup)`);
|
|
291
|
+
return;
|
|
287
292
|
}
|
|
288
|
-
|
|
293
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
289
294
|
unregisterSession(route.sessionKey);
|
|
290
|
-
log(`[BOT] โ
Cleanup completed`);
|
|
295
|
+
logger.log(`[BOT] โ
Cleanup completed`);
|
|
296
|
+
},
|
|
297
|
+
run: () => {
|
|
298
|
+
// ๐ Use AsyncLocalStorage to provide session context to tools.
|
|
299
|
+
// runWithSessionContext returns after the sync part of dispatch
|
|
300
|
+
// (including agentTools + wrapStreamFn) has executed, so we
|
|
301
|
+
// signal init complete to release the global dispatch gate
|
|
302
|
+
// for the next session.
|
|
303
|
+
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
304
|
+
logger.log(`[BOT-DISPATCH] โณ dispatchReplyFromConfig starting...`);
|
|
305
|
+
logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
306
|
+
logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
307
|
+
logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
308
|
+
logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
309
|
+
logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
310
|
+
try {
|
|
311
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
312
|
+
ctx: ctxPayload,
|
|
313
|
+
cfg,
|
|
314
|
+
dispatcher,
|
|
315
|
+
replyOptions,
|
|
316
|
+
});
|
|
317
|
+
logger.log(`[BOT-DISPATCH] โ
dispatchReplyFromConfig returned`);
|
|
318
|
+
logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
catch (dispatchErr) {
|
|
322
|
+
logger.error(`[BOT-DISPATCH] โ dispatchReplyFromConfig threw`);
|
|
323
|
+
logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
324
|
+
logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
325
|
+
logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
326
|
+
throw dispatchErr;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
// Signal init complete โ sync part (agentTools, wrapStreamFn) is done
|
|
330
|
+
params.onInitComplete?.();
|
|
331
|
+
return dispatchPromise;
|
|
291
332
|
},
|
|
292
|
-
run: () =>
|
|
293
|
-
// ๐ Use AsyncLocalStorage to provide session context to tools
|
|
294
|
-
runWithSessionContext(sessionContext, async () => {
|
|
295
|
-
log(`[BOT-DISPATCH] โณ dispatchReplyFromConfig starting...`);
|
|
296
|
-
log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
297
|
-
log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
298
|
-
log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
299
|
-
log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
300
|
-
log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
301
|
-
try {
|
|
302
|
-
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
303
|
-
ctx: ctxPayload,
|
|
304
|
-
cfg,
|
|
305
|
-
dispatcher,
|
|
306
|
-
replyOptions,
|
|
307
|
-
});
|
|
308
|
-
log(`[BOT-DISPATCH] โ
dispatchReplyFromConfig returned`);
|
|
309
|
-
log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
310
|
-
return result;
|
|
311
|
-
}
|
|
312
|
-
catch (dispatchErr) {
|
|
313
|
-
error(`[BOT-DISPATCH] โ dispatchReplyFromConfig threw`);
|
|
314
|
-
error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
315
|
-
error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
316
|
-
error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
317
|
-
throw dispatchErr;
|
|
318
|
-
}
|
|
319
|
-
}),
|
|
320
333
|
});
|
|
321
|
-
log(`[BOT] โ
Dispatcher completed for session: ${parsed.sessionId}`);
|
|
322
|
-
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
334
|
+
logger.log(`[BOT] โ
Dispatcher completed for session: ${parsed.sessionId}`);
|
|
335
|
+
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
323
336
|
}
|
|
324
337
|
catch (err) {
|
|
325
338
|
// โ
Only log error, don't re-throw to prevent gateway restart
|
|
326
|
-
error("Failed to handle XY message:", err);
|
|
339
|
+
logger.error("Failed to handle XY message:", err);
|
|
327
340
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
328
|
-
log(`[BOT] โ Error occurred, attempting cleanup...`);
|
|
341
|
+
logger.log(`[BOT] โ Error occurred, attempting cleanup...`);
|
|
329
342
|
// ๐ ้่ฏฏๆถไน่ฆๆธ
็taskIdๅsession
|
|
330
343
|
try {
|
|
331
344
|
const params = message.params;
|
|
332
345
|
const sessionId = params?.sessionId;
|
|
333
346
|
if (sessionId) {
|
|
334
|
-
log(`[BOT] ๐งน Cleaning up after error: ${sessionId}`);
|
|
347
|
+
logger.log(`[BOT] ๐งน Cleaning up after error: ${sessionId}`);
|
|
335
348
|
// ๆธ
็ taskId
|
|
336
349
|
decrementTaskIdRef(sessionId);
|
|
337
|
-
unlockTaskId(sessionId);
|
|
338
350
|
// ๆธ
็ session
|
|
339
351
|
const core = getXYRuntime();
|
|
340
352
|
const route = core.channel.routing.resolveAgentRoute({
|
|
@@ -347,11 +359,11 @@ export async function handleXYMessage(params) {
|
|
|
347
359
|
},
|
|
348
360
|
});
|
|
349
361
|
unregisterSession(route.sessionKey);
|
|
350
|
-
log(`[BOT] โ
Cleanup completed after error`);
|
|
362
|
+
logger.log(`[BOT] โ
Cleanup completed after error`);
|
|
351
363
|
}
|
|
352
364
|
}
|
|
353
365
|
catch (cleanupErr) {
|
|
354
|
-
log(`[BOT] โ ๏ธ Cleanup failed:`, cleanupErr);
|
|
366
|
+
logger.log(`[BOT] โ ๏ธ Cleanup failed:`, cleanupErr);
|
|
355
367
|
// Ignore cleanup errors
|
|
356
368
|
}
|
|
357
369
|
// โ Don't re-throw: message processing error should not affect gateway stability
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { XYWebSocketManager } from "./websocket.js";
|
|
2
2
|
import type { XYChannelConfig } from "./types.js";
|
|
3
3
|
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
4
|
-
/**
|
|
5
|
-
* Set the runtime for logging in client module.
|
|
6
|
-
*/
|
|
7
|
-
export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
|
|
8
4
|
/**
|
|
9
5
|
* Get or create a WebSocket manager for the given configuration.
|
|
10
6
|
* Reuses existing managers if config matches.
|
|
11
7
|
*/
|
|
12
|
-
export declare function getXYWebSocketManager(config: XYChannelConfig): XYWebSocketManager;
|
|
8
|
+
export declare function getXYWebSocketManager(config: XYChannelConfig, runtime?: RuntimeEnv): XYWebSocketManager;
|
|
13
9
|
/**
|
|
14
10
|
* Remove a specific WebSocket manager from cache.
|
|
15
11
|
* Disconnects the manager and removes it from the cache.
|