@ynhcj/xiaoyi-channel 0.0.145-next → 0.0.146-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 +69 -26
- package/dist/src/bot.js +69 -73
- package/dist/src/channel.js +5 -59
- package/dist/src/client.js +23 -13
- package/dist/src/cron-query-handler.d.ts +17 -0
- package/dist/src/cron-query-handler.js +101 -0
- package/dist/src/cspl/config.d.ts +17 -4
- package/dist/src/cspl/config.js +70 -80
- package/dist/src/cspl/constants.d.ts +24 -46
- package/dist/src/cspl/constants.js +16 -41
- package/dist/src/cspl/sentinel_hook.js +16 -2
- package/dist/src/cspl/steer-context.js +1 -1
- package/dist/src/cspl/utils.d.ts +2 -11
- package/dist/src/cspl/utils.js +15 -265
- package/dist/src/formatter.d.ts +1 -11
- package/dist/src/formatter.js +39 -114
- package/dist/src/monitor.js +22 -22
- package/dist/src/outbound.js +9 -8
- package/dist/src/parser.d.ts +1 -2
- package/dist/src/parser.js +0 -25
- package/dist/src/push.d.ts +1 -11
- package/dist/src/push.js +17 -101
- package/dist/src/reply-dispatcher.js +49 -112
- package/dist/src/self-evolution-handler.js +1 -1
- package/dist/src/task-manager.js +10 -6
- package/dist/src/tools/agent-as-skill-tool.js +55 -4
- package/dist/src/tools/calendar-tool.js +1 -2
- package/dist/src/tools/call-device-tool.js +0 -3
- package/dist/src/tools/call-phone-tool.js +1 -2
- package/dist/src/tools/create-alarm-tool.js +1 -2
- package/dist/src/tools/create-all-tools.js +1 -9
- package/dist/src/tools/delete-alarm-tool.js +1 -2
- package/dist/src/tools/discover-cross-devices-tool.js +1 -1
- package/dist/src/tools/get-device-file-tool-schema.js +2 -3
- package/dist/src/tools/location-tool.js +1 -2
- package/dist/src/tools/modify-alarm-tool.js +1 -2
- package/dist/src/tools/modify-note-tool.js +1 -2
- package/dist/src/tools/note-tool.js +1 -2
- package/dist/src/tools/query-app-message-tool.js +2 -3
- package/dist/src/tools/query-memory-data-tool.js +2 -3
- package/dist/src/tools/query-todo-task-tool.js +2 -3
- package/dist/src/tools/save-file-to-phone-tool.js +1 -2
- package/dist/src/tools/save-media-to-gallery-tool.js +1 -2
- package/dist/src/tools/search-alarm-tool.js +1 -2
- package/dist/src/tools/search-calendar-tool.js +1 -2
- package/dist/src/tools/search-contact-tool.js +1 -2
- package/dist/src/tools/search-email-tool.js +2 -3
- package/dist/src/tools/search-file-tool.js +8 -12
- package/dist/src/tools/search-message-tool.js +1 -2
- package/dist/src/tools/search-note-tool.js +1 -2
- package/dist/src/tools/search-photo-gallery-tool.js +3 -4
- package/dist/src/tools/send-cross-device-task-tool.js +18 -22
- package/dist/src/tools/send-email-tool.js +2 -3
- package/dist/src/tools/send-file-to-user-tool.js +2 -2
- package/dist/src/tools/send-message-tool.js +1 -2
- package/dist/src/tools/session-manager.d.ts +1 -13
- package/dist/src/tools/session-manager.js +0 -43
- package/dist/src/tools/upload-file-tool.d.ts +1 -1
- package/dist/src/tools/upload-file-tool.js +5 -21
- package/dist/src/tools/upload-photo-tool.js +3 -4
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -2
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -2
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -2
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -2
- package/dist/src/trigger-handler.js +7 -4
- package/dist/src/types.d.ts +0 -17
- package/dist/src/utils/config-manager.js +6 -3
- package/dist/src/utils/logger.d.ts +0 -8
- package/dist/src/utils/logger.js +34 -69
- package/dist/src/utils/pushdata-manager.js +5 -1
- package/dist/src/utils/pushid-manager.js +2 -1
- package/dist/src/utils/runtime-manager.js +4 -1
- package/dist/src/websocket.d.ts +0 -3
- package/dist/src/websocket.js +38 -203
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
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
|
-
import { markCronToolCall, clearCronToolCall } from "./src/tools/session-manager.js";
|
|
7
12
|
import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
|
|
8
13
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
9
14
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
10
|
-
/**
|
|
11
|
-
* Register the cron detection hook.
|
|
12
|
-
*
|
|
13
|
-
* When openclaw's cron runner triggers a tool call, the sessionKey has the
|
|
14
|
-
* format "cron:<jobId>". We use this to mark the toolCallId in a global Map
|
|
15
|
-
* so that sendCommand() can route the command through the push channel
|
|
16
|
-
* instead of the (non-existent) WebSocket session.
|
|
17
|
-
*/
|
|
18
|
-
function registerCronDetectionHook(api) {
|
|
19
|
-
api.on("before_tool_call", async (event, ctx) => {
|
|
20
|
-
if (ctx.sessionKey?.startsWith("cron:") && event.toolCallId) {
|
|
21
|
-
markCronToolCall(event.toolCallId);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
api.on("after_tool_call", async (event, ctx) => {
|
|
25
|
-
if (event.toolCallId) {
|
|
26
|
-
clearCronToolCall(event.toolCallId);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
15
|
function registerFullHooks(api) {
|
|
31
16
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
32
17
|
const pluginConfig = api.pluginConfig || {};
|
|
@@ -41,6 +26,67 @@ function registerFullHooks(api) {
|
|
|
41
26
|
api.on("before_prompt_build", beforePromptBuildHandler);
|
|
42
27
|
registerSelfEvolutionToolResultNudge(api);
|
|
43
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
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
44
90
|
export default definePluginEntry({
|
|
45
91
|
id: "xiaoyi-channel",
|
|
46
92
|
name: "Xiaoyi Channel",
|
|
@@ -64,10 +110,7 @@ export default definePluginEntry({
|
|
|
64
110
|
}
|
|
65
111
|
if (api.registrationMode === "full") {
|
|
66
112
|
registerFullHooks(api);
|
|
67
|
-
|
|
68
|
-
registerSentinelHook(api);
|
|
69
|
-
// Cron detection hook: marks toolCallIds from cron sessions
|
|
70
|
-
registerCronDetectionHook(api);
|
|
113
|
+
registerCsplHook(api);
|
|
71
114
|
}
|
|
72
115
|
},
|
|
73
116
|
});
|
package/dist/src/bot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData
|
|
3
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
4
4
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
5
5
|
import { resolveXYConfig } from "./config.js";
|
|
6
6
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -22,9 +22,6 @@ import { logger } from "./utils/logger.js";
|
|
|
22
22
|
*/
|
|
23
23
|
export async function handleXYMessage(params) {
|
|
24
24
|
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
25
|
-
const distributionSessionId = typeof message?.sessionId === "string" && message.sessionId.length > 0
|
|
26
|
-
? message.sessionId
|
|
27
|
-
: undefined;
|
|
28
25
|
// Cache context for CSPL steer injection (after_tool_call hook)
|
|
29
26
|
setCsplSteerContext(cfg, runtime);
|
|
30
27
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
@@ -38,8 +35,7 @@ export async function handleXYMessage(params) {
|
|
|
38
35
|
if (!sessionId) {
|
|
39
36
|
throw new Error("clearContext request missing sessionId in params");
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
log.log(`[BOT] Clear context request`);
|
|
38
|
+
logger.log(`Clear context request for session ${sessionId}`);
|
|
43
39
|
const config = resolveXYConfig(cfg);
|
|
44
40
|
await sendClearContextResponse({
|
|
45
41
|
config,
|
|
@@ -55,8 +51,7 @@ export async function handleXYMessage(params) {
|
|
|
55
51
|
if (!sessionId) {
|
|
56
52
|
throw new Error("tasks/cancel request missing sessionId in params");
|
|
57
53
|
}
|
|
58
|
-
|
|
59
|
-
log.log(`[BOT] Tasks cancel request`);
|
|
54
|
+
logger.log(`Tasks cancel request for session ${sessionId}, task ${taskId}`);
|
|
60
55
|
const config = resolveXYConfig(cfg);
|
|
61
56
|
await sendTasksCancelResponse({
|
|
62
57
|
config,
|
|
@@ -68,21 +63,22 @@ export async function handleXYMessage(params) {
|
|
|
68
63
|
}
|
|
69
64
|
// Parse the A2A message (for regular messages)
|
|
70
65
|
const parsed = parseA2AMessage(message);
|
|
71
|
-
// Scoped logger for this session — avoids concurrent session log mixing
|
|
72
|
-
const log = logger.withContext(parsed.sessionId, parsed.taskId);
|
|
73
66
|
// ========== 检测 Trigger 消息 ==========
|
|
74
67
|
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
75
68
|
const triggerData = extractTriggerData(parsed.parts);
|
|
76
69
|
if (triggerData) {
|
|
77
|
-
|
|
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}`);
|
|
78
73
|
try {
|
|
79
74
|
// 读取 pushData
|
|
80
75
|
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
81
76
|
if (!pushDataItem) {
|
|
82
|
-
|
|
77
|
+
logger.error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
83
78
|
return;
|
|
84
79
|
}
|
|
85
|
-
|
|
80
|
+
logger.log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
81
|
+
logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
86
82
|
const config = resolveXYConfig(cfg);
|
|
87
83
|
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
88
84
|
await sendA2AResponse({
|
|
@@ -94,11 +90,11 @@ export async function handleXYMessage(params) {
|
|
|
94
90
|
append: false,
|
|
95
91
|
final: true,
|
|
96
92
|
});
|
|
97
|
-
|
|
93
|
+
logger.log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
|
|
98
94
|
return; // 提前返回,不继续处理
|
|
99
95
|
}
|
|
100
96
|
catch (err) {
|
|
101
|
-
|
|
97
|
+
logger.error(`[BOT] ❌ Failed to handle Trigger message:`, err);
|
|
102
98
|
return;
|
|
103
99
|
}
|
|
104
100
|
}
|
|
@@ -107,7 +103,9 @@ export async function handleXYMessage(params) {
|
|
|
107
103
|
const isUpdate = hasActiveTask(parsed.sessionId);
|
|
108
104
|
const skipReg = params.skipRegistration === true;
|
|
109
105
|
if (isUpdate) {
|
|
110
|
-
|
|
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
110
|
// Steer injections skip taskId registration to avoid overwriting the active taskId
|
|
113
111
|
if (!skipReg) {
|
|
@@ -115,30 +113,29 @@ export async function handleXYMessage(params) {
|
|
|
115
113
|
// Extract and update push_id if present
|
|
116
114
|
const pushId = extractPushId(parsed.parts);
|
|
117
115
|
if (pushId) {
|
|
118
|
-
|
|
116
|
+
logger.log(`[BOT] 📌 Extracted push_id from user message`);
|
|
119
117
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
120
118
|
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
121
119
|
addPushId(pushId).catch((err) => {
|
|
122
|
-
|
|
120
|
+
logger.error(`[BOT] Failed to persist pushId:`, err);
|
|
123
121
|
});
|
|
124
122
|
}
|
|
125
123
|
else {
|
|
126
|
-
|
|
124
|
+
logger.log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
127
125
|
}
|
|
128
126
|
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
129
127
|
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
130
128
|
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
131
129
|
parsed.taskId // TASK_ID (param.id)
|
|
132
130
|
).catch((err) => {
|
|
133
|
-
|
|
131
|
+
logger.error(`[BOT] Failed to save runtime info:`, err);
|
|
134
132
|
});
|
|
135
133
|
}
|
|
136
134
|
// Extract deviceType if present (always parse — used in ctxPayload.MessageSid)
|
|
137
135
|
const deviceType = extractDeviceType(parsed.parts);
|
|
138
136
|
if (deviceType) {
|
|
139
|
-
|
|
137
|
+
logger.log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
|
|
140
138
|
}
|
|
141
|
-
const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
|
|
142
139
|
// Resolve configuration (needed for status updates)
|
|
143
140
|
const config = resolveXYConfig(cfg);
|
|
144
141
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -153,21 +150,19 @@ export async function handleXYMessage(params) {
|
|
|
153
150
|
id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
|
|
154
151
|
},
|
|
155
152
|
});
|
|
156
|
-
|
|
153
|
+
logger.log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
157
154
|
// Steer injections skip session registration to avoid refCount leaks
|
|
158
155
|
if (!skipReg) {
|
|
159
156
|
registerSession(route.sessionKey, {
|
|
160
157
|
config,
|
|
161
158
|
sessionId: parsed.sessionId,
|
|
162
|
-
distributionSessionId,
|
|
163
159
|
taskId: parsed.taskId,
|
|
164
160
|
messageId: parsed.messageId,
|
|
165
161
|
agentId: route.accountId,
|
|
166
162
|
deviceType,
|
|
167
|
-
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
168
163
|
});
|
|
169
164
|
// 🔑 发送初始状态更新
|
|
170
|
-
|
|
165
|
+
logger.log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
171
166
|
void sendStatusUpdate({
|
|
172
167
|
config,
|
|
173
168
|
sessionId: parsed.sessionId,
|
|
@@ -176,7 +171,7 @@ export async function handleXYMessage(params) {
|
|
|
176
171
|
text: "任务正在处理中,请稍候~",
|
|
177
172
|
state: "working",
|
|
178
173
|
}).catch((err) => {
|
|
179
|
-
|
|
174
|
+
logger.error(`Failed to send initial status update:`, err);
|
|
180
175
|
});
|
|
181
176
|
}
|
|
182
177
|
// Extract text and files from parts
|
|
@@ -188,18 +183,18 @@ export async function handleXYMessage(params) {
|
|
|
188
183
|
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
189
184
|
if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
|
|
190
185
|
const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
|
|
191
|
-
|
|
186
|
+
logger.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
|
|
192
187
|
if (shouldNudge) {
|
|
193
188
|
const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
|
|
194
189
|
textForAgent = augmented.text;
|
|
195
190
|
if (augmented.appended) {
|
|
196
|
-
|
|
191
|
+
logger.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
|
|
197
192
|
}
|
|
198
193
|
}
|
|
199
194
|
}
|
|
200
195
|
}
|
|
201
196
|
catch (selfEvolutionError) {
|
|
202
|
-
|
|
197
|
+
logger.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
203
198
|
}
|
|
204
199
|
}
|
|
205
200
|
// 🔑 Steer消息: 跳过旧路径直接进入 streaming-signal 队列
|
|
@@ -214,11 +209,9 @@ export async function handleXYMessage(params) {
|
|
|
214
209
|
const steerDownloadedFiles = await downloadFilesFromParts(steerFileParts);
|
|
215
210
|
const steerMediaPayload = buildXYMediaPayload(steerDownloadedFiles);
|
|
216
211
|
if (steerFileParts.length > 0) {
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
log.log(`[BOT] Steer message — enqueuing to streaming-signal queue`);
|
|
212
|
+
logger.log(`[BOT] 📎 Steer message with files: ${steerFileParts.length} file(s)`);
|
|
221
213
|
}
|
|
214
|
+
logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
|
|
222
215
|
await enqueueSteer({
|
|
223
216
|
sessionId: parsed.sessionId,
|
|
224
217
|
sessionKey: route.sessionKey,
|
|
@@ -230,7 +223,8 @@ export async function handleXYMessage(params) {
|
|
|
230
223
|
route,
|
|
231
224
|
deviceType,
|
|
232
225
|
});
|
|
233
|
-
|
|
226
|
+
logger.log(`[BOT] ✅ Steer queue completed for session: ${parsed.sessionId}`);
|
|
227
|
+
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
234
228
|
return;
|
|
235
229
|
}
|
|
236
230
|
// ── First message (non-steer) path below ──────────────────────
|
|
@@ -242,7 +236,7 @@ export async function handleXYMessage(params) {
|
|
|
242
236
|
if (!skipReg) {
|
|
243
237
|
const fileParts = extractFileParts(parsed.parts);
|
|
244
238
|
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
245
|
-
|
|
239
|
+
logger.log("Downloaded files:", JSON.stringify(downloadedFiles, null, 2));
|
|
246
240
|
mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
247
241
|
}
|
|
248
242
|
// Resolve envelope format options (following feishu pattern)
|
|
@@ -288,7 +282,8 @@ export async function handleXYMessage(params) {
|
|
|
288
282
|
// 🔑 Streaming 信号已在上方创建(在文件下载之前)
|
|
289
283
|
const steerState = { steered: false };
|
|
290
284
|
// 🔑 创建dispatcher
|
|
291
|
-
|
|
285
|
+
logger.log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
286
|
+
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
292
287
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
293
288
|
cfg,
|
|
294
289
|
runtime,
|
|
@@ -306,27 +301,26 @@ export async function handleXYMessage(params) {
|
|
|
306
301
|
const sessionContext = {
|
|
307
302
|
config,
|
|
308
303
|
sessionId: parsed.sessionId,
|
|
309
|
-
distributionSessionId,
|
|
310
304
|
taskId: parsed.taskId,
|
|
311
305
|
messageId: parsed.messageId,
|
|
312
306
|
agentId: route.accountId,
|
|
313
307
|
deviceType,
|
|
314
|
-
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
315
308
|
};
|
|
316
|
-
|
|
309
|
+
logger.log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
317
310
|
await core.channel.reply.withReplyDispatcher({
|
|
318
311
|
dispatcher,
|
|
319
312
|
onSettled: () => {
|
|
320
|
-
|
|
313
|
+
logger.log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
314
|
+
logger.log(`[BOT] - steered: ${steerState.steered}`);
|
|
321
315
|
// 🔑 When steered, skip heavy cleanup — the first message's dispatcher is still running
|
|
322
316
|
if (steerState.steered) {
|
|
323
|
-
|
|
317
|
+
logger.log(`[BOT] ✅ Steered dispatch settled (skipping cleanup)`);
|
|
324
318
|
return;
|
|
325
319
|
}
|
|
326
320
|
streamingSignals.delete(parsed.sessionId);
|
|
327
321
|
decrementTaskIdRef(parsed.sessionId);
|
|
328
322
|
unregisterSession(route.sessionKey);
|
|
329
|
-
|
|
323
|
+
logger.log(`[BOT] ✅ Cleanup completed`);
|
|
330
324
|
},
|
|
331
325
|
run: () => {
|
|
332
326
|
// 🔐 Use AsyncLocalStorage to provide session context to tools.
|
|
@@ -335,7 +329,12 @@ export async function handleXYMessage(params) {
|
|
|
335
329
|
// signal init complete to release the global dispatch gate
|
|
336
330
|
// for the next session.
|
|
337
331
|
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
338
|
-
|
|
332
|
+
logger.log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
|
|
333
|
+
logger.log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
334
|
+
logger.log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
335
|
+
logger.log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
336
|
+
logger.log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
337
|
+
logger.log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
339
338
|
try {
|
|
340
339
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
341
340
|
ctx: ctxPayload,
|
|
@@ -343,11 +342,15 @@ export async function handleXYMessage(params) {
|
|
|
343
342
|
dispatcher,
|
|
344
343
|
replyOptions,
|
|
345
344
|
});
|
|
346
|
-
|
|
345
|
+
logger.log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
|
|
346
|
+
logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
347
347
|
return result;
|
|
348
348
|
}
|
|
349
349
|
catch (dispatchErr) {
|
|
350
|
-
|
|
350
|
+
logger.error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
|
|
351
|
+
logger.error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
352
|
+
logger.error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
353
|
+
logger.error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
351
354
|
throw dispatchErr;
|
|
352
355
|
}
|
|
353
356
|
});
|
|
@@ -356,23 +359,20 @@ export async function handleXYMessage(params) {
|
|
|
356
359
|
return dispatchPromise;
|
|
357
360
|
},
|
|
358
361
|
});
|
|
359
|
-
|
|
362
|
+
logger.log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
363
|
+
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
360
364
|
}
|
|
361
365
|
catch (err) {
|
|
362
366
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
363
|
-
|
|
364
|
-
const errSessionId = message.params?.sessionId || "";
|
|
365
|
-
const errTaskId = message.params?.id || message.id || "";
|
|
366
|
-
const errLog = logger.withContext(errSessionId, errTaskId);
|
|
367
|
-
errLog.error("Failed to handle XY message:", err);
|
|
367
|
+
logger.error("Failed to handle XY message:", err);
|
|
368
368
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
369
|
-
|
|
369
|
+
logger.log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
370
370
|
// 🔑 错误时也要清理taskId和session
|
|
371
371
|
try {
|
|
372
372
|
const params = message.params;
|
|
373
373
|
const sessionId = params?.sessionId;
|
|
374
374
|
if (sessionId) {
|
|
375
|
-
|
|
375
|
+
logger.log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
|
|
376
376
|
// 清理 taskId
|
|
377
377
|
decrementTaskIdRef(sessionId);
|
|
378
378
|
// 清理 session
|
|
@@ -387,11 +387,11 @@ export async function handleXYMessage(params) {
|
|
|
387
387
|
},
|
|
388
388
|
});
|
|
389
389
|
unregisterSession(route.sessionKey);
|
|
390
|
-
|
|
390
|
+
logger.log(`[BOT] ✅ Cleanup completed after error`);
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
catch (cleanupErr) {
|
|
394
|
-
|
|
394
|
+
logger.log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
|
|
395
395
|
// Ignore cleanup errors
|
|
396
396
|
}
|
|
397
397
|
// ❌ Don't re-throw: message processing error should not affect gateway stability
|
|
@@ -428,22 +428,20 @@ const steerQueues = _g.__xySteerQueues;
|
|
|
428
428
|
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
429
429
|
*/
|
|
430
430
|
export function notifyModelStreaming(sessionId) {
|
|
431
|
-
const log = logger.withContext(sessionId, "");
|
|
432
431
|
const signal = streamingSignals.get(sessionId);
|
|
433
432
|
if (signal) {
|
|
434
433
|
// 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
|
|
435
434
|
// 清理由第一条消息的 onSettled 兜底。
|
|
436
435
|
signal.notify();
|
|
437
|
-
|
|
436
|
+
logger.log(`[STEER-QUEUE] 📡 Model streaming signal fired for session=${sessionId}`);
|
|
438
437
|
}
|
|
439
438
|
}
|
|
440
439
|
function createStreamingSignal(sessionId) {
|
|
441
|
-
const log = logger.withContext(sessionId, "");
|
|
442
440
|
let resolve;
|
|
443
441
|
const promise = new Promise(r => { resolve = r; });
|
|
444
442
|
const signal = { promise, notify: resolve };
|
|
445
443
|
streamingSignals.set(sessionId, signal);
|
|
446
|
-
|
|
444
|
+
logger.log(`[STEER-QUEUE] 🟢 Streaming signal created for session ${sessionId}`);
|
|
447
445
|
return signal;
|
|
448
446
|
}
|
|
449
447
|
/**
|
|
@@ -453,14 +451,13 @@ function createStreamingSignal(sessionId) {
|
|
|
453
451
|
*/
|
|
454
452
|
function enqueueSteer(params) {
|
|
455
453
|
const { sessionId } = params;
|
|
456
|
-
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
457
454
|
// 取出当前队列尾部(或 undefined),然后链上新的 Promise
|
|
458
455
|
const prev = steerQueues.get(sessionId);
|
|
459
456
|
const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
|
|
460
457
|
steerQueues.set(sessionId, next);
|
|
461
458
|
// 链条结束后清理
|
|
462
459
|
next.catch((err) => {
|
|
463
|
-
|
|
460
|
+
logger.error(`[STEER-QUEUE] ❌ Steer chain failed: ${String(err)}`);
|
|
464
461
|
}).finally(() => {
|
|
465
462
|
if (steerQueues.get(sessionId) === next) {
|
|
466
463
|
steerQueues.delete(sessionId);
|
|
@@ -470,38 +467,37 @@ function enqueueSteer(params) {
|
|
|
470
467
|
}
|
|
471
468
|
async function dispatchSteerWhenReady(params) {
|
|
472
469
|
const { sessionId, sessionKey, steerText } = params;
|
|
473
|
-
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
474
470
|
// 1. 等待第一条消息开始 streaming
|
|
475
471
|
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
476
472
|
// 轮询等待直到 signal 出现,最長等待 ~5 秒。
|
|
477
473
|
let signal = streamingSignals.get(sessionId);
|
|
478
474
|
if (!signal) {
|
|
479
|
-
|
|
475
|
+
logger.log(`[STEER-QUEUE] ⏳ Signal not yet created, polling for session=${sessionId}`);
|
|
480
476
|
for (let i = 0; i < 50; i++) {
|
|
481
477
|
await new Promise(r => setTimeout(r, 100));
|
|
482
478
|
signal = streamingSignals.get(sessionId);
|
|
483
479
|
if (signal)
|
|
484
480
|
break;
|
|
485
481
|
if (!hasActiveTask(sessionId)) {
|
|
486
|
-
|
|
482
|
+
logger.log(`[STEER-QUEUE] ℹ️ First message completed while waiting, skip steer`);
|
|
487
483
|
return;
|
|
488
484
|
}
|
|
489
485
|
}
|
|
490
486
|
}
|
|
491
487
|
if (signal) {
|
|
492
|
-
|
|
488
|
+
logger.log(`[STEER-QUEUE] ⏳ Waiting for streaming signal, session=${sessionId}`);
|
|
493
489
|
await signal.promise;
|
|
494
|
-
|
|
490
|
+
logger.log(`[STEER-QUEUE] ✅ Streaming signal received, session=${sessionId}`);
|
|
495
491
|
}
|
|
496
492
|
else {
|
|
497
493
|
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
498
494
|
// 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
|
|
499
|
-
|
|
495
|
+
logger.log(`[STEER-QUEUE] ⚠️ Signal never appeared after polling, skip steer to avoid collision`);
|
|
500
496
|
return;
|
|
501
497
|
}
|
|
502
498
|
// 2. 第一条消息已结束 → 放弃
|
|
503
499
|
if (!hasActiveTask(sessionId)) {
|
|
504
|
-
|
|
500
|
+
logger.log(`[STEER-QUEUE] ℹ️ First message completed, skip steer`);
|
|
505
501
|
return;
|
|
506
502
|
}
|
|
507
503
|
// 3. 构建 dispatch 上下文并 dispatch /steer
|
|
@@ -563,11 +559,11 @@ async function dispatchSteerWhenReady(params) {
|
|
|
563
559
|
agentId: params.route.accountId,
|
|
564
560
|
deviceType: params.deviceType,
|
|
565
561
|
};
|
|
566
|
-
|
|
562
|
+
logger.log(`[STEER-QUEUE] 🚀 Dispatching steer for session=${sessionId}`);
|
|
567
563
|
await core.channel.reply.withReplyDispatcher({
|
|
568
564
|
dispatcher,
|
|
569
565
|
onSettled: () => {
|
|
570
|
-
|
|
566
|
+
logger.log(`[STEER-QUEUE] 🏁 Steer dispatch settled for session=${sessionId}`);
|
|
571
567
|
},
|
|
572
568
|
run: () => {
|
|
573
569
|
return runWithSessionContext(sessionContext, async () => {
|
|
@@ -577,10 +573,10 @@ async function dispatchSteerWhenReady(params) {
|
|
|
577
573
|
dispatcher,
|
|
578
574
|
replyOptions,
|
|
579
575
|
});
|
|
580
|
-
|
|
576
|
+
logger.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
581
577
|
return result;
|
|
582
578
|
});
|
|
583
579
|
},
|
|
584
580
|
});
|
|
585
|
-
|
|
581
|
+
logger.log(`[STEER-QUEUE] ✅ Steer dispatch completed for session=${sessionId}`);
|
|
586
582
|
}
|
package/dist/src/channel.js
CHANGED
|
@@ -2,15 +2,9 @@ import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./conf
|
|
|
2
2
|
import { xyConfigSchema } from "./config-schema.js";
|
|
3
3
|
import { xyOutbound } from "./outbound.js";
|
|
4
4
|
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
5
|
-
import { getCurrentSessionContext
|
|
5
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
6
6
|
import { createAllTools } from "./tools/create-all-tools.js";
|
|
7
7
|
import { logger } from "./utils/logger.js";
|
|
8
|
-
/**
|
|
9
|
-
* Prefix used for synthetic sessionIds created during cron-triggered tool
|
|
10
|
-
* execution. `sendCommand()` checks this prefix to route commands through
|
|
11
|
-
* the push channel instead of the (non-existent) WebSocket session.
|
|
12
|
-
*/
|
|
13
|
-
const CRON_SESSION_PREFIX = "cron-";
|
|
14
8
|
/**
|
|
15
9
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
16
10
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -49,59 +43,11 @@ export const xyPlugin = {
|
|
|
49
43
|
schema: xyConfigSchema,
|
|
50
44
|
},
|
|
51
45
|
outbound: xyOutbound,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* Two execution contexts are supported:
|
|
56
|
-
*
|
|
57
|
-
* 1. **Normal (WebSocket) session** – `getCurrentSessionContext()` returns
|
|
58
|
-
* a context that was registered by bot.ts during message processing.
|
|
59
|
-
* Tools send commands through the WebSocket and listen for responses.
|
|
60
|
-
*
|
|
61
|
-
* 2. **Cron / scheduled-task session** – openclaw's cron runner calls
|
|
62
|
-
* `agentTools({ cfg })` without an active WebSocket session. When no
|
|
63
|
-
* session context exists but `cfg` is provided, we create a synthetic
|
|
64
|
-
* "cron session" with `isCron: true` and a `cron-`-prefixed sessionId.
|
|
65
|
-
* `sendCommand()` detects this prefix and routes commands through the
|
|
66
|
-
* push channel. Response listening (WebSocket events) works unchanged
|
|
67
|
-
* because the gateway WebSocket connection is always active.
|
|
68
|
-
*/
|
|
69
|
-
agentTools: (params) => {
|
|
70
|
-
let ctx = getCurrentSessionContext();
|
|
71
|
-
// ── Cron / non-session fallback ──────────────────────────────
|
|
72
|
-
// When no active xy WebSocket session exists but the openclaw cfg
|
|
73
|
-
// is provided (framework calls agentTools({ cfg })), create a
|
|
74
|
-
// synthetic "cron session". This enables cron-triggered agent
|
|
75
|
-
// turns and cross-channel tool calls to use xiaoyi tools via the
|
|
76
|
-
// push channel. sendCommand() detects the "cron-" sessionId
|
|
77
|
-
// prefix and routes commands through push instead of WebSocket.
|
|
78
|
-
if (!ctx && params?.cfg) {
|
|
79
|
-
try {
|
|
80
|
-
const config = resolveXYConfig(params.cfg);
|
|
81
|
-
const cronId = `${CRON_SESSION_PREFIX}${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
82
|
-
ctx = {
|
|
83
|
-
config,
|
|
84
|
-
sessionId: cronId,
|
|
85
|
-
taskId: cronId,
|
|
86
|
-
messageId: cronId,
|
|
87
|
-
agentId: "default",
|
|
88
|
-
isCron: true,
|
|
89
|
-
};
|
|
90
|
-
// Register so getCurrentSessionContext() fallback can find it
|
|
91
|
-
registerSession(`__cron__${cronId}`, ctx);
|
|
92
|
-
logger.log(`[CRON-TOOLS] Created cron session context: ${cronId}`);
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
logger.error("[CRON-TOOLS] Failed to create cron context:", err);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (!ctx) {
|
|
99
|
-
logger.log("[CREATE-ALL-TOOLS] no session context, returning empty tools list");
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
46
|
+
agentTools: () => {
|
|
47
|
+
const ctx = getCurrentSessionContext();
|
|
102
48
|
const allTools = createAllTools(ctx);
|
|
103
|
-
const filtered = filterToolsByDevice(allTools, ctx
|
|
104
|
-
logger.log(`[DEVICE-FILTER] deviceType=${ctx
|
|
49
|
+
const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
|
|
50
|
+
logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
105
51
|
return filtered;
|
|
106
52
|
},
|
|
107
53
|
messaging: {
|