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