@ynhcj/xiaoyi-channel 1.1.27 → 1.1.29
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 +132 -73
- 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 +96 -8
- 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 +11 -6
- package/dist/src/cspl/steer-context.js +1 -1
- package/dist/src/cspl/utils.d.ts +17 -2
- package/dist/src/cspl/utils.js +271 -15
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +102 -0
- package/dist/src/formatter.d.ts +43 -1
- package/dist/src/formatter.js +171 -41
- package/dist/src/monitor.js +64 -43
- package/dist/src/outbound.js +8 -9
- package/dist/src/parser.d.ts +8 -1
- package/dist/src/parser.js +71 -0
- package/dist/src/provider.js +51 -17
- package/dist/src/push.d.ts +11 -1
- package/dist/src/push.js +101 -17
- package/dist/src/reply-dispatcher.js +152 -59
- package/dist/src/self-evolution-handler.d.ts +1 -1
- package/dist/src/self-evolution-handler.js +14 -3
- 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/check-plugin-privilege-tool.d.ts +6 -0
- package/dist/src/tools/check-plugin-privilege-tool.js +182 -0
- 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 +20 -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-html-card-tool.d.ts +7 -0
- package/dist/src/tools/send-html-card-tool.js +113 -0
- package/dist/src/tools/send-message-tool.js +2 -1
- package/dist/src/tools/session-manager.d.ts +17 -1
- package/dist/src/tools/session-manager.js +87 -1
- 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 +8 -2
- package/dist/src/trigger-handler.js +4 -7
- package/dist/src/types.d.ts +25 -1
- 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,7 @@
|
|
|
1
|
+
import { updateSessionStoreEntry, updateSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
1
2
|
import { getXYRuntime } from "./runtime.js";
|
|
2
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
4
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractModelName, extractTriggerData, extractRunCrossTaskContext } from "./parser.js";
|
|
4
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
5
6
|
import { resolveXYConfig } from "./config.js";
|
|
6
7
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -22,6 +23,9 @@ import { logger } from "./utils/logger.js";
|
|
|
22
23
|
*/
|
|
23
24
|
export async function handleXYMessage(params) {
|
|
24
25
|
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
26
|
+
const distributionSessionId = typeof message?.sessionId === "string" && message.sessionId.length > 0
|
|
27
|
+
? message.sessionId
|
|
28
|
+
: undefined;
|
|
25
29
|
// Cache context for CSPL steer injection (after_tool_call hook)
|
|
26
30
|
setCsplSteerContext(cfg, runtime);
|
|
27
31
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
@@ -29,13 +33,14 @@ export async function handleXYMessage(params) {
|
|
|
29
33
|
try {
|
|
30
34
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
31
35
|
const messageMethod = message.method;
|
|
32
|
-
// Handle clearContext messages (
|
|
36
|
+
// Handle clearContext messages (sessionId at top level, no params)
|
|
33
37
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
34
|
-
const sessionId = message.params?.sessionId;
|
|
38
|
+
const sessionId = message.sessionId ?? message.params?.sessionId;
|
|
35
39
|
if (!sessionId) {
|
|
36
40
|
throw new Error("clearContext request missing sessionId in params");
|
|
37
41
|
}
|
|
38
|
-
logger.
|
|
42
|
+
const log = logger.withContext(sessionId, "");
|
|
43
|
+
log.log(`[BOT] Clear context request`);
|
|
39
44
|
const config = resolveXYConfig(cfg);
|
|
40
45
|
await sendClearContextResponse({
|
|
41
46
|
config,
|
|
@@ -44,14 +49,15 @@ export async function handleXYMessage(params) {
|
|
|
44
49
|
});
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
|
-
// Handle tasks/cancel messages
|
|
52
|
+
// Handle tasks/cancel messages (sessionId at top level, no params)
|
|
48
53
|
if (messageMethod === "tasks/cancel" || messageMethod === "tasks_cancel") {
|
|
49
|
-
const sessionId = message.params?.sessionId;
|
|
54
|
+
const sessionId = message.sessionId ?? message.params?.sessionId;
|
|
50
55
|
const taskId = message.params?.id || message.id;
|
|
51
56
|
if (!sessionId) {
|
|
52
57
|
throw new Error("tasks/cancel request missing sessionId in params");
|
|
53
58
|
}
|
|
54
|
-
logger.
|
|
59
|
+
const log = logger.withContext(sessionId, taskId);
|
|
60
|
+
log.log(`[BOT] Tasks cancel request`);
|
|
55
61
|
const config = resolveXYConfig(cfg);
|
|
56
62
|
await sendTasksCancelResponse({
|
|
57
63
|
config,
|
|
@@ -63,22 +69,21 @@ export async function handleXYMessage(params) {
|
|
|
63
69
|
}
|
|
64
70
|
// Parse the A2A message (for regular messages)
|
|
65
71
|
const parsed = parseA2AMessage(message);
|
|
72
|
+
// Scoped logger for this session — avoids concurrent session log mixing
|
|
73
|
+
const log = logger.withContext(parsed.sessionId, parsed.taskId);
|
|
66
74
|
// ========== 检测 Trigger 消息 ==========
|
|
67
75
|
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
68
76
|
const triggerData = extractTriggerData(parsed.parts);
|
|
69
77
|
if (triggerData) {
|
|
70
|
-
|
|
71
|
-
logger.log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
72
|
-
logger.log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
78
|
+
log.log(`[BOT] Detected Trigger message, pushDataId=${triggerData.pushDataId}`);
|
|
73
79
|
try {
|
|
74
80
|
// 读取 pushData
|
|
75
81
|
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
76
82
|
if (!pushDataItem) {
|
|
77
|
-
|
|
83
|
+
log.error(`[BOT] pushData not found for ID: ${triggerData.pushDataId}`);
|
|
78
84
|
return;
|
|
79
85
|
}
|
|
80
|
-
|
|
81
|
-
logger.log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
86
|
+
log.log(`[BOT] Found pushData, sending direct response, pushDataId=${pushDataItem.pushDataId}`);
|
|
82
87
|
const config = resolveXYConfig(cfg);
|
|
83
88
|
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
84
89
|
await sendA2AResponse({
|
|
@@ -90,11 +95,11 @@ export async function handleXYMessage(params) {
|
|
|
90
95
|
append: false,
|
|
91
96
|
final: true,
|
|
92
97
|
});
|
|
93
|
-
|
|
98
|
+
log.log(`[BOT] Trigger response sent successfully`);
|
|
94
99
|
return; // 提前返回,不继续处理
|
|
95
100
|
}
|
|
96
101
|
catch (err) {
|
|
97
|
-
|
|
102
|
+
log.error(`[BOT] Failed to handle Trigger message:`, err);
|
|
98
103
|
return;
|
|
99
104
|
}
|
|
100
105
|
}
|
|
@@ -103,9 +108,7 @@ export async function handleXYMessage(params) {
|
|
|
103
108
|
const isUpdate = hasActiveTask(parsed.sessionId);
|
|
104
109
|
const skipReg = params.skipRegistration === true;
|
|
105
110
|
if (isUpdate) {
|
|
106
|
-
|
|
107
|
-
logger.log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
108
|
-
logger.log(`[BOT] - New taskId: ${parsed.taskId}`);
|
|
111
|
+
log.log(`[BOT] STEER MODE - Second message detected, new taskId=${parsed.taskId}`);
|
|
109
112
|
}
|
|
110
113
|
// Steer injections skip taskId registration to avoid overwriting the active taskId
|
|
111
114
|
if (!skipReg) {
|
|
@@ -113,29 +116,35 @@ export async function handleXYMessage(params) {
|
|
|
113
116
|
// Extract and update push_id if present
|
|
114
117
|
const pushId = extractPushId(parsed.parts);
|
|
115
118
|
if (pushId) {
|
|
116
|
-
|
|
119
|
+
log.log(`[BOT] Extracted push_id from user message`);
|
|
117
120
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
118
121
|
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
119
122
|
addPushId(pushId).catch((err) => {
|
|
120
|
-
|
|
123
|
+
log.error(`[BOT] Failed to persist pushId:`, err);
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
else {
|
|
124
|
-
|
|
127
|
+
log.log(`[BOT] No push_id found in message, using config default`);
|
|
125
128
|
}
|
|
126
129
|
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
127
130
|
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
128
131
|
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
129
132
|
parsed.taskId // TASK_ID (param.id)
|
|
130
133
|
).catch((err) => {
|
|
131
|
-
|
|
134
|
+
log.error(`[BOT] Failed to save runtime info:`, err);
|
|
132
135
|
});
|
|
133
136
|
}
|
|
134
137
|
// Extract deviceType if present (always parse — used in ctxPayload.MessageSid)
|
|
135
138
|
const deviceType = extractDeviceType(parsed.parts);
|
|
136
139
|
if (deviceType) {
|
|
137
|
-
|
|
140
|
+
log.log(`[BOT] Extracted deviceType: ${deviceType}`);
|
|
138
141
|
}
|
|
142
|
+
// Extract modelName if present (used by provider.ts to override model.id)
|
|
143
|
+
const modelName = extractModelName(parsed.parts);
|
|
144
|
+
if (modelName) {
|
|
145
|
+
log.log(`[BOT] Extracted modelName: ${modelName}`);
|
|
146
|
+
}
|
|
147
|
+
const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
|
|
139
148
|
// Resolve configuration (needed for status updates)
|
|
140
149
|
const config = resolveXYConfig(cfg);
|
|
141
150
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -150,19 +159,69 @@ export async function handleXYMessage(params) {
|
|
|
150
159
|
id: parsed.sessionId, // ✅ Use sessionId to share context within the same conversation session
|
|
151
160
|
},
|
|
152
161
|
});
|
|
153
|
-
|
|
162
|
+
log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
|
|
154
163
|
// Steer injections skip session registration to avoid refCount leaks
|
|
155
164
|
if (!skipReg) {
|
|
156
165
|
registerSession(route.sessionKey, {
|
|
157
166
|
config,
|
|
158
167
|
sessionId: parsed.sessionId,
|
|
168
|
+
distributionSessionId,
|
|
159
169
|
taskId: parsed.taskId,
|
|
160
170
|
messageId: parsed.messageId,
|
|
161
171
|
agentId: route.accountId,
|
|
162
172
|
deviceType,
|
|
173
|
+
modelName,
|
|
174
|
+
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
163
175
|
});
|
|
176
|
+
// 🔑 Sync A2A modelName to OpenClaw session store so that session_status
|
|
177
|
+
// reports the correct model. Without this, session_status returns the
|
|
178
|
+
// configured default model instead of the A2A-specified one.
|
|
179
|
+
if (modelName && modelName.trim() !== "" && modelName.toLowerCase() !== "none") {
|
|
180
|
+
try {
|
|
181
|
+
const storePath = resolveStorePath();
|
|
182
|
+
const result = await updateSessionStoreEntry({
|
|
183
|
+
storePath,
|
|
184
|
+
sessionKey: route.sessionKey,
|
|
185
|
+
update: async () => ({
|
|
186
|
+
providerOverride: "xiaoyiprovider",
|
|
187
|
+
modelOverride: modelName,
|
|
188
|
+
modelOverrideSource: "user",
|
|
189
|
+
model: "",
|
|
190
|
+
modelProvider: "",
|
|
191
|
+
contextTokens: 256_000,
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
if (!result) {
|
|
195
|
+
// Session entry doesn't exist yet (first message, xy_channel
|
|
196
|
+
// bypasses the standard turn kernel). Create a minimal entry
|
|
197
|
+
// with the override via updateSessionStore.
|
|
198
|
+
await updateSessionStore(storePath, (store) => {
|
|
199
|
+
if (!store[route.sessionKey]) {
|
|
200
|
+
store[route.sessionKey] = {
|
|
201
|
+
// sessionId must pass validateSessionId regex /^[a-z0-9][a-z0-9._-]{0,127}$/i
|
|
202
|
+
// route.sessionKey like "agent:main:direct:xxx" contains colons which are invalid.
|
|
203
|
+
// Use parsed.sessionId (raw UUID from A2A) which is always safe.
|
|
204
|
+
sessionId: parsed.sessionId,
|
|
205
|
+
updatedAt: Date.now(),
|
|
206
|
+
providerOverride: "xiaoyiprovider",
|
|
207
|
+
modelOverride: modelName,
|
|
208
|
+
modelOverrideSource: "user",
|
|
209
|
+
contextTokens: 256_000,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
log.log(`[BOT] Created session entry with model override: xiaoyiprovider/${modelName}`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
log.log(`[BOT] Patched session store model override: xiaoyiprovider/${modelName}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (patchErr) {
|
|
220
|
+
log.error(`[BOT] Failed to patch session model override:`, patchErr);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
164
223
|
// 🔑 发送初始状态更新
|
|
165
|
-
|
|
224
|
+
log.log(`[BOT] Sending initial status update`);
|
|
166
225
|
void sendStatusUpdate({
|
|
167
226
|
config,
|
|
168
227
|
sessionId: parsed.sessionId,
|
|
@@ -171,7 +230,7 @@ export async function handleXYMessage(params) {
|
|
|
171
230
|
text: "任务正在处理中,请稍候~",
|
|
172
231
|
state: "working",
|
|
173
232
|
}).catch((err) => {
|
|
174
|
-
|
|
233
|
+
log.error(`Failed to send initial status update:`, err);
|
|
175
234
|
});
|
|
176
235
|
}
|
|
177
236
|
// Extract text and files from parts
|
|
@@ -183,18 +242,18 @@ export async function handleXYMessage(params) {
|
|
|
183
242
|
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
184
243
|
if (selfEvolutionEnabled && shouldNudgeForSelfEvolutionKeyword(textForAgent)) {
|
|
185
244
|
const shouldNudge = toolCallNudgeManager.tryMarkKeywordNudge(route.sessionKey);
|
|
186
|
-
|
|
245
|
+
log.log(`[SELF_EVOLUTION] Keyword check hit during inbound build: sessionKey=${route.sessionKey}, shouldNudge=${shouldNudge}`);
|
|
187
246
|
if (shouldNudge) {
|
|
188
247
|
const augmented = appendSelfEvolutionKeywordNudge(textForAgent);
|
|
189
248
|
textForAgent = augmented.text;
|
|
190
249
|
if (augmented.appended) {
|
|
191
|
-
|
|
250
|
+
log.log(`[SELF_EVOLUTION] Keyword-triggered inline nudge appended: sessionKey=${route.sessionKey}`);
|
|
192
251
|
}
|
|
193
252
|
}
|
|
194
253
|
}
|
|
195
254
|
}
|
|
196
255
|
catch (selfEvolutionError) {
|
|
197
|
-
|
|
256
|
+
log.error(`[SELF_EVOLUTION] Failed to append inline keyword nudge: ${String(selfEvolutionError)}`);
|
|
198
257
|
}
|
|
199
258
|
}
|
|
200
259
|
// 🔑 Steer消息: 跳过旧路径直接进入 streaming-signal 队列
|
|
@@ -209,9 +268,11 @@ export async function handleXYMessage(params) {
|
|
|
209
268
|
const steerDownloadedFiles = await downloadFilesFromParts(steerFileParts);
|
|
210
269
|
const steerMediaPayload = buildXYMediaPayload(steerDownloadedFiles);
|
|
211
270
|
if (steerFileParts.length > 0) {
|
|
212
|
-
|
|
271
|
+
log.log(`[BOT] Steer message with ${steerFileParts.length} file(s), enqueuing to streaming-signal queue`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
log.log(`[BOT] Steer message — enqueuing to streaming-signal queue`);
|
|
213
275
|
}
|
|
214
|
-
logger.log(`[BOT] 🔄 Steer message — enqueuing to streaming-signal queue`);
|
|
215
276
|
await enqueueSteer({
|
|
216
277
|
sessionId: parsed.sessionId,
|
|
217
278
|
sessionKey: route.sessionKey,
|
|
@@ -223,8 +284,7 @@ export async function handleXYMessage(params) {
|
|
|
223
284
|
route,
|
|
224
285
|
deviceType,
|
|
225
286
|
});
|
|
226
|
-
|
|
227
|
-
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
287
|
+
log.log(`[BOT] Steer queue completed`);
|
|
228
288
|
return;
|
|
229
289
|
}
|
|
230
290
|
// ── First message (non-steer) path below ──────────────────────
|
|
@@ -236,7 +296,7 @@ export async function handleXYMessage(params) {
|
|
|
236
296
|
if (!skipReg) {
|
|
237
297
|
const fileParts = extractFileParts(parsed.parts);
|
|
238
298
|
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
239
|
-
|
|
299
|
+
log.log(`[BOT] Downloaded ${downloadedFiles.length} file(s)`);
|
|
240
300
|
mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
241
301
|
}
|
|
242
302
|
// Resolve envelope format options (following feishu pattern)
|
|
@@ -282,8 +342,7 @@ export async function handleXYMessage(params) {
|
|
|
282
342
|
// 🔑 Streaming 信号已在上方创建(在文件下载之前)
|
|
283
343
|
const steerState = { steered: false };
|
|
284
344
|
// 🔑 创建dispatcher
|
|
285
|
-
|
|
286
|
-
logger.log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
345
|
+
log.log(`[BOT-DISPATCHER] Creating reply dispatcher`);
|
|
287
346
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
288
347
|
cfg,
|
|
289
348
|
runtime,
|
|
@@ -301,26 +360,28 @@ export async function handleXYMessage(params) {
|
|
|
301
360
|
const sessionContext = {
|
|
302
361
|
config,
|
|
303
362
|
sessionId: parsed.sessionId,
|
|
363
|
+
distributionSessionId,
|
|
304
364
|
taskId: parsed.taskId,
|
|
305
365
|
messageId: parsed.messageId,
|
|
306
366
|
agentId: route.accountId,
|
|
307
367
|
deviceType,
|
|
368
|
+
modelName,
|
|
369
|
+
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
308
370
|
};
|
|
309
|
-
|
|
371
|
+
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
310
372
|
await core.channel.reply.withReplyDispatcher({
|
|
311
373
|
dispatcher,
|
|
312
374
|
onSettled: () => {
|
|
313
|
-
|
|
314
|
-
logger.log(`[BOT] - steered: ${steerState.steered}`);
|
|
375
|
+
log.log(`[BOT] onSettled, steered=${steerState.steered}`);
|
|
315
376
|
// 🔑 When steered, skip heavy cleanup — the first message's dispatcher is still running
|
|
316
377
|
if (steerState.steered) {
|
|
317
|
-
|
|
378
|
+
log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
|
|
318
379
|
return;
|
|
319
380
|
}
|
|
320
381
|
streamingSignals.delete(parsed.sessionId);
|
|
321
382
|
decrementTaskIdRef(parsed.sessionId);
|
|
322
383
|
unregisterSession(route.sessionKey);
|
|
323
|
-
|
|
384
|
+
log.log(`[BOT] Cleanup completed`);
|
|
324
385
|
},
|
|
325
386
|
run: () => {
|
|
326
387
|
// 🔐 Use AsyncLocalStorage to provide session context to tools.
|
|
@@ -329,12 +390,7 @@ export async function handleXYMessage(params) {
|
|
|
329
390
|
// signal init complete to release the global dispatch gate
|
|
330
391
|
// for the next session.
|
|
331
392
|
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}`);
|
|
393
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
|
|
338
394
|
try {
|
|
339
395
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
340
396
|
ctx: ctxPayload,
|
|
@@ -342,15 +398,11 @@ export async function handleXYMessage(params) {
|
|
|
342
398
|
dispatcher,
|
|
343
399
|
replyOptions,
|
|
344
400
|
});
|
|
345
|
-
|
|
346
|
-
logger.log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
401
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, result=${JSON.stringify(result)}`);
|
|
347
402
|
return result;
|
|
348
403
|
}
|
|
349
404
|
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"}`);
|
|
405
|
+
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
406
|
throw dispatchErr;
|
|
355
407
|
}
|
|
356
408
|
});
|
|
@@ -359,20 +411,23 @@ export async function handleXYMessage(params) {
|
|
|
359
411
|
return dispatchPromise;
|
|
360
412
|
},
|
|
361
413
|
});
|
|
362
|
-
|
|
363
|
-
logger.log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
414
|
+
log.log(`[BOT] Dispatcher completed`);
|
|
364
415
|
}
|
|
365
416
|
catch (err) {
|
|
366
417
|
// ✅ Only log error, don't re-throw to prevent gateway restart
|
|
367
|
-
|
|
418
|
+
// Note: if error occurs before parseA2AMessage, `log` may not be defined yet
|
|
419
|
+
const errSessionId = message.params?.sessionId || "";
|
|
420
|
+
const errTaskId = message.params?.id || message.id || "";
|
|
421
|
+
const errLog = logger.withContext(errSessionId, errTaskId);
|
|
422
|
+
errLog.error("Failed to handle XY message:", err);
|
|
368
423
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
369
|
-
|
|
424
|
+
errLog.log(`[BOT] Error occurred, attempting cleanup`);
|
|
370
425
|
// 🔑 错误时也要清理taskId和session
|
|
371
426
|
try {
|
|
372
427
|
const params = message.params;
|
|
373
428
|
const sessionId = params?.sessionId;
|
|
374
429
|
if (sessionId) {
|
|
375
|
-
|
|
430
|
+
errLog.log(`[BOT] Cleaning up after error`);
|
|
376
431
|
// 清理 taskId
|
|
377
432
|
decrementTaskIdRef(sessionId);
|
|
378
433
|
// 清理 session
|
|
@@ -387,11 +442,11 @@ export async function handleXYMessage(params) {
|
|
|
387
442
|
},
|
|
388
443
|
});
|
|
389
444
|
unregisterSession(route.sessionKey);
|
|
390
|
-
|
|
445
|
+
errLog.log(`[BOT] Cleanup completed after error`);
|
|
391
446
|
}
|
|
392
447
|
}
|
|
393
448
|
catch (cleanupErr) {
|
|
394
|
-
|
|
449
|
+
errLog.log(`[BOT] Cleanup failed:`, cleanupErr);
|
|
395
450
|
// Ignore cleanup errors
|
|
396
451
|
}
|
|
397
452
|
// ❌ Don't re-throw: message processing error should not affect gateway stability
|
|
@@ -428,20 +483,22 @@ const steerQueues = _g.__xySteerQueues;
|
|
|
428
483
|
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
429
484
|
*/
|
|
430
485
|
export function notifyModelStreaming(sessionId) {
|
|
486
|
+
const log = logger.withContext(sessionId, "");
|
|
431
487
|
const signal = streamingSignals.get(sessionId);
|
|
432
488
|
if (signal) {
|
|
433
489
|
// 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
|
|
434
490
|
// 清理由第一条消息的 onSettled 兜底。
|
|
435
491
|
signal.notify();
|
|
436
|
-
|
|
492
|
+
log.log(`[STEER-QUEUE] Model streaming signal fired`);
|
|
437
493
|
}
|
|
438
494
|
}
|
|
439
495
|
function createStreamingSignal(sessionId) {
|
|
496
|
+
const log = logger.withContext(sessionId, "");
|
|
440
497
|
let resolve;
|
|
441
498
|
const promise = new Promise(r => { resolve = r; });
|
|
442
499
|
const signal = { promise, notify: resolve };
|
|
443
500
|
streamingSignals.set(sessionId, signal);
|
|
444
|
-
|
|
501
|
+
log.log(`[STEER-QUEUE] Streaming signal created`);
|
|
445
502
|
return signal;
|
|
446
503
|
}
|
|
447
504
|
/**
|
|
@@ -451,13 +508,14 @@ function createStreamingSignal(sessionId) {
|
|
|
451
508
|
*/
|
|
452
509
|
function enqueueSteer(params) {
|
|
453
510
|
const { sessionId } = params;
|
|
511
|
+
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
454
512
|
// 取出当前队列尾部(或 undefined),然后链上新的 Promise
|
|
455
513
|
const prev = steerQueues.get(sessionId);
|
|
456
514
|
const next = (prev ?? Promise.resolve()).then(() => dispatchSteerWhenReady(params));
|
|
457
515
|
steerQueues.set(sessionId, next);
|
|
458
516
|
// 链条结束后清理
|
|
459
517
|
next.catch((err) => {
|
|
460
|
-
|
|
518
|
+
log.error(`[STEER-QUEUE] Steer chain failed: ${String(err)}`);
|
|
461
519
|
}).finally(() => {
|
|
462
520
|
if (steerQueues.get(sessionId) === next) {
|
|
463
521
|
steerQueues.delete(sessionId);
|
|
@@ -467,37 +525,38 @@ function enqueueSteer(params) {
|
|
|
467
525
|
}
|
|
468
526
|
async function dispatchSteerWhenReady(params) {
|
|
469
527
|
const { sessionId, sessionKey, steerText } = params;
|
|
528
|
+
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
470
529
|
// 1. 等待第一条消息开始 streaming
|
|
471
530
|
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
472
531
|
// 轮询等待直到 signal 出现,最長等待 ~5 秒。
|
|
473
532
|
let signal = streamingSignals.get(sessionId);
|
|
474
533
|
if (!signal) {
|
|
475
|
-
|
|
534
|
+
log.log(`[STEER-QUEUE] Signal not yet created, polling`);
|
|
476
535
|
for (let i = 0; i < 50; i++) {
|
|
477
536
|
await new Promise(r => setTimeout(r, 100));
|
|
478
537
|
signal = streamingSignals.get(sessionId);
|
|
479
538
|
if (signal)
|
|
480
539
|
break;
|
|
481
540
|
if (!hasActiveTask(sessionId)) {
|
|
482
|
-
|
|
541
|
+
log.log(`[STEER-QUEUE] First message completed while waiting, skip steer`);
|
|
483
542
|
return;
|
|
484
543
|
}
|
|
485
544
|
}
|
|
486
545
|
}
|
|
487
546
|
if (signal) {
|
|
488
|
-
|
|
547
|
+
log.log(`[STEER-QUEUE] Waiting for streaming signal`);
|
|
489
548
|
await signal.promise;
|
|
490
|
-
|
|
549
|
+
log.log(`[STEER-QUEUE] Streaming signal received`);
|
|
491
550
|
}
|
|
492
551
|
else {
|
|
493
552
|
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
494
553
|
// 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
|
|
495
|
-
|
|
554
|
+
log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
|
|
496
555
|
return;
|
|
497
556
|
}
|
|
498
557
|
// 2. 第一条消息已结束 → 放弃
|
|
499
558
|
if (!hasActiveTask(sessionId)) {
|
|
500
|
-
|
|
559
|
+
log.log(`[STEER-QUEUE] First message completed, skip steer`);
|
|
501
560
|
return;
|
|
502
561
|
}
|
|
503
562
|
// 3. 构建 dispatch 上下文并 dispatch /steer
|
|
@@ -559,11 +618,11 @@ async function dispatchSteerWhenReady(params) {
|
|
|
559
618
|
agentId: params.route.accountId,
|
|
560
619
|
deviceType: params.deviceType,
|
|
561
620
|
};
|
|
562
|
-
|
|
621
|
+
log.log(`[STEER-QUEUE] Dispatching steer`);
|
|
563
622
|
await core.channel.reply.withReplyDispatcher({
|
|
564
623
|
dispatcher,
|
|
565
624
|
onSettled: () => {
|
|
566
|
-
|
|
625
|
+
log.log(`[STEER-QUEUE] Steer dispatch settled`);
|
|
567
626
|
},
|
|
568
627
|
run: () => {
|
|
569
628
|
return runWithSessionContext(sessionContext, async () => {
|
|
@@ -573,10 +632,10 @@ async function dispatchSteerWhenReady(params) {
|
|
|
573
632
|
dispatcher,
|
|
574
633
|
replyOptions,
|
|
575
634
|
});
|
|
576
|
-
|
|
635
|
+
log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
577
636
|
return result;
|
|
578
637
|
});
|
|
579
638
|
},
|
|
580
639
|
});
|
|
581
|
-
|
|
640
|
+
log.log(`[STEER-QUEUE] Steer dispatch completed`);
|
|
582
641
|
}
|