@ynhcj/xiaoyi-channel 0.0.24-beta → 0.0.26-beta
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/src/bot.js +56 -30
- package/dist/src/monitor.js +35 -5
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +117 -100
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/session-manager.js +42 -18
- package/dist/src/tools/xiaoyi-gui-tool.js +6 -6
- package/package.json +1 -1
package/dist/src/bot.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolveXYConfig } from "./config.js";
|
|
|
6
6
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
|
|
7
7
|
import { registerSession, unregisterSession } from "./tools/session-manager.js";
|
|
8
8
|
import { configManager } from "./utils/config-manager.js";
|
|
9
|
+
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
9
10
|
/**
|
|
10
11
|
* Handle an incoming A2A message.
|
|
11
12
|
* This is the main entry point for message processing.
|
|
@@ -56,6 +57,22 @@ export async function handleXYMessage(params) {
|
|
|
56
57
|
}
|
|
57
58
|
// Parse the A2A message (for regular messages)
|
|
58
59
|
const parsed = parseA2AMessage(message);
|
|
60
|
+
// 🔑 检测steer模式和是否是第二条消息
|
|
61
|
+
const isSteerMode = cfg.messages?.queue?.mode === "steer";
|
|
62
|
+
const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
|
|
63
|
+
if (isSecondMessage) {
|
|
64
|
+
log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
|
|
65
|
+
log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
66
|
+
log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
|
|
67
|
+
}
|
|
68
|
+
// 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
|
|
69
|
+
const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
|
|
70
|
+
);
|
|
71
|
+
// 🔑 如果是第一条消息,锁定taskId防止被过早清理
|
|
72
|
+
if (!isUpdate) {
|
|
73
|
+
lockTaskId(parsed.sessionId);
|
|
74
|
+
log(`[BOT] 🔒 Locked taskId for first message`);
|
|
75
|
+
}
|
|
59
76
|
// Extract and update push_id if present
|
|
60
77
|
const pushId = extractPushId(parsed.parts);
|
|
61
78
|
if (pushId) {
|
|
@@ -83,11 +100,12 @@ export async function handleXYMessage(params) {
|
|
|
83
100
|
},
|
|
84
101
|
});
|
|
85
102
|
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
86
|
-
//
|
|
103
|
+
// 🔑 注册session(带引用计数)
|
|
87
104
|
log(`[BOT] 📝 About to register session for tools...`);
|
|
88
105
|
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
89
106
|
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
90
107
|
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
108
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
91
109
|
registerSession(route.sessionKey, {
|
|
92
110
|
config,
|
|
93
111
|
sessionId: parsed.sessionId,
|
|
@@ -96,14 +114,14 @@ export async function handleXYMessage(params) {
|
|
|
96
114
|
agentId: route.accountId,
|
|
97
115
|
});
|
|
98
116
|
log(`[BOT] ✅ Session registered for tools`);
|
|
99
|
-
//
|
|
117
|
+
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
100
118
|
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
101
119
|
void sendStatusUpdate({
|
|
102
120
|
config,
|
|
103
121
|
sessionId: parsed.sessionId,
|
|
104
122
|
taskId: parsed.taskId,
|
|
105
123
|
messageId: parsed.messageId,
|
|
106
|
-
text: "任务正在处理中,请稍后~",
|
|
124
|
+
text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
|
|
107
125
|
state: "working",
|
|
108
126
|
}).catch((err) => {
|
|
109
127
|
error(`Failed to send initial status update:`, err);
|
|
@@ -155,32 +173,30 @@ export async function handleXYMessage(params) {
|
|
|
155
173
|
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
156
174
|
...mediaPayload,
|
|
157
175
|
});
|
|
158
|
-
//
|
|
159
|
-
log(`[
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
taskId: parsed.taskId,
|
|
164
|
-
messageId: parsed.messageId,
|
|
165
|
-
text: "任务正在处理中,请稍后~",
|
|
166
|
-
state: "working",
|
|
167
|
-
}).catch((err) => {
|
|
168
|
-
error(`Failed to send initial status update:`, err);
|
|
169
|
-
});
|
|
170
|
-
// Create reply dispatcher (following feishu pattern)
|
|
171
|
-
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher for session=${parsed.sessionId}, taskId=${parsed.taskId}, messageId=${parsed.messageId}`);
|
|
176
|
+
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
177
|
+
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
178
|
+
log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
|
|
179
|
+
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
180
|
+
log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
|
|
172
181
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
173
182
|
cfg,
|
|
174
183
|
runtime,
|
|
175
184
|
sessionId: parsed.sessionId,
|
|
176
185
|
taskId: parsed.taskId,
|
|
177
186
|
messageId: parsed.messageId,
|
|
178
|
-
accountId: route.accountId,
|
|
187
|
+
accountId: route.accountId,
|
|
188
|
+
isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
|
|
179
189
|
});
|
|
180
190
|
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
191
|
+
// 🔑 只有第一条消息启动状态定时器
|
|
192
|
+
// 第二条消息会很快返回,不需要定时器
|
|
193
|
+
if (!isSecondMessage) {
|
|
194
|
+
startStatusInterval();
|
|
195
|
+
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
|
|
199
|
+
}
|
|
184
200
|
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
185
201
|
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
186
202
|
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
@@ -188,11 +204,18 @@ export async function handleXYMessage(params) {
|
|
|
188
204
|
dispatcher,
|
|
189
205
|
onSettled: () => {
|
|
190
206
|
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
191
|
-
log(`[BOT] -
|
|
207
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
192
208
|
markDispatchIdle();
|
|
193
|
-
//
|
|
209
|
+
// 🔑 减少引用计数
|
|
210
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
211
|
+
// 🔑 如果是第一条消息完成,解锁
|
|
212
|
+
if (!isSecondMessage) {
|
|
213
|
+
unlockTaskId(parsed.sessionId);
|
|
214
|
+
log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
|
|
215
|
+
}
|
|
216
|
+
// 减少session引用计数
|
|
194
217
|
unregisterSession(route.sessionKey);
|
|
195
|
-
log(`[BOT] ✅
|
|
218
|
+
log(`[BOT] ✅ Cleanup completed`);
|
|
196
219
|
},
|
|
197
220
|
run: () => core.channel.reply.dispatchReplyFromConfig({
|
|
198
221
|
ctx: ctxPayload,
|
|
@@ -209,25 +232,28 @@ export async function handleXYMessage(params) {
|
|
|
209
232
|
error("Failed to handle XY message:", err);
|
|
210
233
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
211
234
|
log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
212
|
-
//
|
|
235
|
+
// 🔑 错误时也要清理taskId和session
|
|
213
236
|
try {
|
|
214
|
-
const core = getXYRuntime();
|
|
215
237
|
const params = message.params;
|
|
216
238
|
const sessionId = params?.sessionId;
|
|
217
239
|
if (sessionId) {
|
|
218
|
-
log(`[BOT] 🧹 Cleaning up
|
|
240
|
+
log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
|
|
241
|
+
// 清理 taskId
|
|
242
|
+
decrementTaskIdRef(sessionId);
|
|
243
|
+
unlockTaskId(sessionId);
|
|
244
|
+
// 清理 session
|
|
245
|
+
const core = getXYRuntime();
|
|
219
246
|
const route = core.channel.routing.resolveAgentRoute({
|
|
220
247
|
cfg,
|
|
221
248
|
channel: "xiaoyi-channel",
|
|
222
249
|
accountId,
|
|
223
250
|
peer: {
|
|
224
251
|
kind: "direct",
|
|
225
|
-
id: sessionId,
|
|
252
|
+
id: sessionId,
|
|
226
253
|
},
|
|
227
254
|
});
|
|
228
|
-
log(`[BOT] - Unregistering session: ${route.sessionKey}`);
|
|
229
255
|
unregisterSession(route.sessionKey);
|
|
230
|
-
log(`[BOT] ✅
|
|
256
|
+
log(`[BOT] ✅ Cleanup completed after error`);
|
|
231
257
|
}
|
|
232
258
|
}
|
|
233
259
|
catch (cleanupErr) {
|
package/dist/src/monitor.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolveXYConfig } from "./config.js";
|
|
2
2
|
import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
|
|
3
3
|
import { handleXYMessage } from "./bot.js";
|
|
4
|
+
import { parseA2AMessage } from "./parser.js";
|
|
5
|
+
import { hasActiveTask } from "./task-manager.js";
|
|
4
6
|
/**
|
|
5
7
|
* Per-session serial queue that ensures messages from the same session are processed
|
|
6
8
|
* in arrival order while allowing different sessions to run concurrently.
|
|
@@ -94,11 +96,39 @@ export async function monitorXYProvider(opts = {}) {
|
|
|
94
96
|
log(`[MONITOR-HANDLER] 🧹 Cleaned up messageKey=${messageKey}, remaining active: ${activeMessages.size}`);
|
|
95
97
|
}
|
|
96
98
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
// 🔑 核心改造:检测steer模式
|
|
100
|
+
// 需要提前解析消息以获取sessionId
|
|
101
|
+
try {
|
|
102
|
+
const parsed = parseA2AMessage(message);
|
|
103
|
+
const steerMode = cfg.messages?.queue?.mode === "steer";
|
|
104
|
+
const hasActiveRun = hasActiveTask(parsed.sessionId);
|
|
105
|
+
if (steerMode && hasActiveRun) {
|
|
106
|
+
// Steer模式且有活跃任务:不入队列,直接并发执行
|
|
107
|
+
log(`[MONITOR-HANDLER] 🔄 STEER MODE: Executing concurrently for messageKey=${messageKey}`);
|
|
108
|
+
log(`[MONITOR-HANDLER] - sessionId: ${parsed.sessionId}`);
|
|
109
|
+
log(`[MONITOR-HANDLER] - Bypassing queue to allow message insertion`);
|
|
110
|
+
void task().catch((err) => {
|
|
111
|
+
error(`XY gateway: concurrent steer task failed for ${messageKey}: ${String(err)}`);
|
|
112
|
+
activeMessages.delete(messageKey);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// 正常模式:入队列串行执行
|
|
117
|
+
log(`[MONITOR-HANDLER] 📋 NORMAL MODE: Enqueuing for messageKey=${messageKey}`);
|
|
118
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
119
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
120
|
+
activeMessages.delete(messageKey);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (parseErr) {
|
|
125
|
+
// 解析失败,回退到正常队列模式
|
|
126
|
+
error(`[MONITOR-HANDLER] Failed to parse message for steer detection: ${String(parseErr)}`);
|
|
127
|
+
void enqueue(sessionId, task).catch((err) => {
|
|
128
|
+
error(`XY gateway: queue processing failed for session ${sessionId}: ${String(err)}`);
|
|
129
|
+
activeMessages.delete(messageKey);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
102
132
|
};
|
|
103
133
|
const connectedHandler = (serverId) => {
|
|
104
134
|
if (!loggedServers.has(serverId)) {
|
|
@@ -2,47 +2,58 @@ import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
|
2
2
|
import { getXYRuntime } from "./runtime.js";
|
|
3
3
|
import { sendA2AResponse, sendStatusUpdate, sendReasoningTextUpdate } from "./formatter.js";
|
|
4
4
|
import { resolveXYConfig } from "./config.js";
|
|
5
|
+
import { getCurrentTaskId, getCurrentMessageId } from "./task-manager.js";
|
|
5
6
|
/**
|
|
6
7
|
* Create a reply dispatcher for XY channel messages.
|
|
7
8
|
* Follows feishu pattern with status updates and streaming support.
|
|
8
9
|
* Runtime is expected to be validated before calling this function.
|
|
9
10
|
*/
|
|
10
11
|
export function createXYReplyDispatcher(params) {
|
|
11
|
-
const { cfg, runtime, sessionId, taskId, messageId, accountId } = params;
|
|
12
|
+
const { cfg, runtime, sessionId, taskId, messageId, accountId, isSteerFollower } = params;
|
|
12
13
|
const log = runtime?.log ?? console.log;
|
|
13
14
|
const error = runtime?.error ?? console.error;
|
|
14
|
-
log(`[DISPATCHER-CREATE] ******* Creating dispatcher
|
|
15
|
-
log(`[DISPATCHER-CREATE]
|
|
16
|
-
log(`[DISPATCHER-CREATE]
|
|
17
|
-
log(`[DISPATCHER-CREATE]
|
|
18
|
-
log(`[DISPATCHER-CREATE]
|
|
19
|
-
//
|
|
15
|
+
log(`[DISPATCHER-CREATE] ******* Creating dispatcher *******`);
|
|
16
|
+
log(`[DISPATCHER-CREATE] - sessionId: ${sessionId}`);
|
|
17
|
+
log(`[DISPATCHER-CREATE] - taskId: ${taskId}`);
|
|
18
|
+
log(`[DISPATCHER-CREATE] - messageId: ${messageId}`);
|
|
19
|
+
log(`[DISPATCHER-CREATE] - isSteerFollower: ${isSteerFollower ?? false}`);
|
|
20
|
+
// 初始taskId和messageId(作为fallback)
|
|
21
|
+
const initialTaskId = taskId;
|
|
22
|
+
const initialMessageId = messageId;
|
|
23
|
+
/**
|
|
24
|
+
* 🔑 核心改造:动态获取当前活跃的taskId和messageId
|
|
25
|
+
* 每次需要taskId时,都从TaskManager获取最新值
|
|
26
|
+
*/
|
|
27
|
+
const getActiveTaskId = () => {
|
|
28
|
+
return getCurrentTaskId(sessionId) ?? initialTaskId;
|
|
29
|
+
};
|
|
30
|
+
const getActiveMessageId = () => {
|
|
31
|
+
return getCurrentMessageId(sessionId) ?? initialMessageId;
|
|
32
|
+
};
|
|
20
33
|
const core = getXYRuntime();
|
|
21
|
-
// Resolve configuration
|
|
22
34
|
const config = resolveXYConfig(cfg);
|
|
23
|
-
// Create reply prefix context (for model selection, etc.)
|
|
24
35
|
const prefixContext = createReplyPrefixContext({ cfg, agentId: accountId });
|
|
25
|
-
// Status update interval (every 60 seconds)
|
|
26
36
|
let statusUpdateInterval = null;
|
|
27
|
-
// Track if we've sent any response
|
|
28
37
|
let hasSentResponse = false;
|
|
29
|
-
// Track if we've sent the final empty message
|
|
30
38
|
let finalSent = false;
|
|
31
|
-
// Accumulate all text from deliver calls
|
|
32
39
|
let accumulatedText = "";
|
|
33
40
|
/**
|
|
34
41
|
* Start the status update interval
|
|
35
|
-
* Call this immediately after creating the dispatcher
|
|
36
42
|
*/
|
|
37
43
|
const startStatusInterval = () => {
|
|
38
|
-
log(`[STATUS INTERVAL] Starting interval for session ${sessionId}
|
|
44
|
+
log(`[STATUS INTERVAL] Starting interval for session ${sessionId}`);
|
|
39
45
|
statusUpdateInterval = setInterval(() => {
|
|
40
|
-
|
|
46
|
+
// 🔑 使用动态taskId
|
|
47
|
+
const currentTaskId = getActiveTaskId();
|
|
48
|
+
const currentMessageId = getActiveMessageId();
|
|
49
|
+
log(`[STATUS INTERVAL] Triggering status update`);
|
|
50
|
+
log(`[STATUS INTERVAL] - sessionId: ${sessionId}`);
|
|
51
|
+
log(`[STATUS INTERVAL] - currentTaskId: ${currentTaskId}`);
|
|
41
52
|
void sendStatusUpdate({
|
|
42
53
|
config,
|
|
43
54
|
sessionId,
|
|
44
|
-
taskId,
|
|
45
|
-
messageId,
|
|
55
|
+
taskId: currentTaskId, // 🔑 动态taskId
|
|
56
|
+
messageId: currentMessageId, // 🔑 动态messageId
|
|
46
57
|
text: "任务正在处理中,请稍后~",
|
|
47
58
|
state: "working",
|
|
48
59
|
}).catch((err) => {
|
|
@@ -50,15 +61,11 @@ export function createXYReplyDispatcher(params) {
|
|
|
50
61
|
});
|
|
51
62
|
}, 30000); // 30 seconds
|
|
52
63
|
};
|
|
53
|
-
/**
|
|
54
|
-
* Stop the status update interval
|
|
55
|
-
*/
|
|
56
64
|
const stopStatusInterval = () => {
|
|
57
65
|
if (statusUpdateInterval) {
|
|
58
|
-
log(`[STATUS INTERVAL] Stopping interval for session ${sessionId}
|
|
66
|
+
log(`[STATUS INTERVAL] Stopping interval for session ${sessionId}`);
|
|
59
67
|
clearInterval(statusUpdateInterval);
|
|
60
68
|
statusUpdateInterval = null;
|
|
61
|
-
log(`[STATUS INTERVAL] Stopped interval for session ${sessionId}, taskId=${taskId}`);
|
|
62
69
|
}
|
|
63
70
|
};
|
|
64
71
|
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
@@ -66,33 +73,28 @@ export function createXYReplyDispatcher(params) {
|
|
|
66
73
|
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
67
74
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, accountId),
|
|
68
75
|
onReplyStart: () => {
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
const currentTaskId = getActiveTaskId();
|
|
77
|
+
log(`[REPLY START] Reply started for session ${sessionId}, taskId=${currentTaskId}, isSteerFollower=${isSteerFollower}`);
|
|
71
78
|
},
|
|
72
79
|
deliver: async (payload, info) => {
|
|
73
80
|
const text = payload.text ?? "";
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
log(`[DELIVER]
|
|
77
|
-
if (payload.mediaUrls) {
|
|
78
|
-
log(`[DELIVER] mediaUrls: ${payload.mediaUrls.length} files`);
|
|
79
|
-
}
|
|
81
|
+
const currentTaskId = getActiveTaskId();
|
|
82
|
+
const currentMessageId = getActiveMessageId();
|
|
83
|
+
log(`[DELIVER] sessionId=${sessionId}, taskId=${currentTaskId}, info.kind=${info?.kind}, text.length=${text.length}`);
|
|
80
84
|
try {
|
|
81
|
-
// Skip empty messages
|
|
82
85
|
if (!text.trim()) {
|
|
83
86
|
log(`[DELIVER SKIP] Empty text, skipping`);
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
|
-
// Accumulate text instead of sending immediately
|
|
87
89
|
accumulatedText += text;
|
|
88
90
|
hasSentResponse = true;
|
|
89
91
|
log(`[DELIVER ACCUMULATE] Accumulated text, current length=${accumulatedText.length}`);
|
|
90
|
-
//
|
|
92
|
+
// 🔑 使用动态taskId发送reasoningText更新
|
|
91
93
|
await sendReasoningTextUpdate({
|
|
92
94
|
config,
|
|
93
95
|
sessionId,
|
|
94
|
-
taskId,
|
|
95
|
-
messageId,
|
|
96
|
+
taskId: currentTaskId,
|
|
97
|
+
messageId: currentMessageId,
|
|
96
98
|
text,
|
|
97
99
|
});
|
|
98
100
|
log(`[DELIVER] ✅ Sent deliver text as reasoningText update`);
|
|
@@ -103,16 +105,21 @@ export function createXYReplyDispatcher(params) {
|
|
|
103
105
|
},
|
|
104
106
|
onError: async (err, info) => {
|
|
105
107
|
runtime.error?.(`xy: ${info.kind} reply failed: ${String(err)}`);
|
|
106
|
-
// Stop status updates
|
|
107
108
|
stopStatusInterval();
|
|
108
|
-
//
|
|
109
|
+
// 🔑 steer follower不发送错误状态(让主dispatcher处理)
|
|
110
|
+
if (isSteerFollower) {
|
|
111
|
+
log(`[ON_ERROR] Steer follower - skipping error response`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
109
114
|
if (!hasSentResponse) {
|
|
115
|
+
const currentTaskId = getActiveTaskId();
|
|
116
|
+
const currentMessageId = getActiveMessageId();
|
|
110
117
|
try {
|
|
111
118
|
await sendStatusUpdate({
|
|
112
119
|
config,
|
|
113
120
|
sessionId,
|
|
114
|
-
taskId,
|
|
115
|
-
messageId,
|
|
121
|
+
taskId: currentTaskId,
|
|
122
|
+
messageId: currentMessageId,
|
|
116
123
|
text: "处理失败,请稍后重试",
|
|
117
124
|
state: "failed",
|
|
118
125
|
});
|
|
@@ -123,46 +130,61 @@ export function createXYReplyDispatcher(params) {
|
|
|
123
130
|
}
|
|
124
131
|
},
|
|
125
132
|
onIdle: async () => {
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
const currentTaskId = getActiveTaskId();
|
|
134
|
+
const currentMessageId = getActiveMessageId();
|
|
135
|
+
log(`[ON_IDLE] Reply idle`);
|
|
136
|
+
log(`[ON_IDLE] - sessionId: ${sessionId}`);
|
|
137
|
+
log(`[ON_IDLE] - taskId: ${currentTaskId}`);
|
|
138
|
+
log(`[ON_IDLE] - isSteerFollower: ${isSteerFollower}`);
|
|
139
|
+
log(`[ON_IDLE] - hasSentResponse: ${hasSentResponse}`);
|
|
140
|
+
log(`[ON_IDLE] - finalSent: ${finalSent}`);
|
|
141
|
+
// 🔑 核心改动:steer follower不发送final响应
|
|
142
|
+
if (isSteerFollower) {
|
|
143
|
+
log(`[ON_IDLE] Steer follower - skipping final response`);
|
|
144
|
+
log(`[ON_IDLE] - Message queued successfully, waiting for primary dispatcher`);
|
|
145
|
+
stopStatusInterval();
|
|
146
|
+
return; // ← 直接返回,不发送任何东西!
|
|
147
|
+
}
|
|
148
|
+
// 正常模式(或steer的第一条消息)
|
|
128
149
|
if (hasSentResponse && !finalSent) {
|
|
129
150
|
log(`[ON_IDLE] Sending accumulated text, length=${accumulatedText.length}`);
|
|
130
151
|
try {
|
|
131
|
-
//
|
|
152
|
+
// 🔑 使用动态taskId发送完成状态
|
|
132
153
|
await sendStatusUpdate({
|
|
133
154
|
config,
|
|
134
155
|
sessionId,
|
|
135
|
-
taskId,
|
|
136
|
-
messageId,
|
|
156
|
+
taskId: currentTaskId,
|
|
157
|
+
messageId: currentMessageId,
|
|
137
158
|
text: "任务处理已完成~",
|
|
138
159
|
state: "completed",
|
|
139
160
|
});
|
|
140
161
|
log(`[ON_IDLE] ✅ Sent completion status update`);
|
|
162
|
+
// 🔑 使用动态taskId发送最终响应
|
|
141
163
|
await sendA2AResponse({
|
|
142
164
|
config,
|
|
143
165
|
sessionId,
|
|
144
|
-
taskId,
|
|
145
|
-
messageId,
|
|
166
|
+
taskId: currentTaskId,
|
|
167
|
+
messageId: currentMessageId,
|
|
146
168
|
text: accumulatedText,
|
|
147
169
|
append: false,
|
|
148
170
|
final: true,
|
|
149
171
|
});
|
|
150
172
|
finalSent = true;
|
|
151
|
-
log(`[ON_IDLE] Sent
|
|
173
|
+
log(`[ON_IDLE] ✅ Sent final response with taskId=${currentTaskId}`);
|
|
152
174
|
}
|
|
153
175
|
catch (err) {
|
|
154
|
-
error(`[ON_IDLE] Failed to send
|
|
176
|
+
error(`[ON_IDLE] Failed to send final response:`, err);
|
|
155
177
|
}
|
|
156
178
|
}
|
|
157
179
|
else {
|
|
180
|
+
// 正常失败场景(非steer follower)
|
|
158
181
|
log(`[ON_IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
|
|
159
|
-
// Task was interrupted - send failure status and error response
|
|
160
182
|
try {
|
|
161
183
|
await sendStatusUpdate({
|
|
162
184
|
config,
|
|
163
185
|
sessionId,
|
|
164
|
-
taskId,
|
|
165
|
-
messageId,
|
|
186
|
+
taskId: currentTaskId,
|
|
187
|
+
messageId: currentMessageId,
|
|
166
188
|
text: "任务处理中断了~",
|
|
167
189
|
state: "failed",
|
|
168
190
|
});
|
|
@@ -170,8 +192,8 @@ export function createXYReplyDispatcher(params) {
|
|
|
170
192
|
await sendA2AResponse({
|
|
171
193
|
config,
|
|
172
194
|
sessionId,
|
|
173
|
-
taskId,
|
|
174
|
-
messageId,
|
|
195
|
+
taskId: currentTaskId,
|
|
196
|
+
messageId: currentMessageId,
|
|
175
197
|
text: "任务执行异常,请重试~",
|
|
176
198
|
append: false,
|
|
177
199
|
final: true,
|
|
@@ -180,14 +202,14 @@ export function createXYReplyDispatcher(params) {
|
|
|
180
202
|
log(`[ON_IDLE] ✅ Sent error response`);
|
|
181
203
|
}
|
|
182
204
|
catch (err) {
|
|
183
|
-
error(`[ON_IDLE] Failed to send
|
|
205
|
+
error(`[ON_IDLE] Failed to send error response:`, err);
|
|
184
206
|
}
|
|
185
207
|
}
|
|
186
|
-
// Stop status updates
|
|
187
208
|
stopStatusInterval();
|
|
188
209
|
},
|
|
189
210
|
onCleanup: () => {
|
|
190
|
-
|
|
211
|
+
const currentTaskId = getActiveTaskId();
|
|
212
|
+
log(`[ON_CLEANUP] Reply cleanup, taskId=${currentTaskId}, isSteerFollower=${isSteerFollower}`);
|
|
191
213
|
},
|
|
192
214
|
});
|
|
193
215
|
return {
|
|
@@ -195,17 +217,22 @@ export function createXYReplyDispatcher(params) {
|
|
|
195
217
|
replyOptions: {
|
|
196
218
|
...replyOptions,
|
|
197
219
|
onModelSelected: prefixContext.onModelSelected,
|
|
198
|
-
// 🔧 Tool execution start callback
|
|
199
220
|
onToolStart: async ({ name, phase }) => {
|
|
200
|
-
|
|
221
|
+
// 🔑 steer follower不发送tool状态(让主dispatcher处理)
|
|
222
|
+
if (isSteerFollower) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const currentTaskId = getActiveTaskId();
|
|
226
|
+
const currentMessageId = getActiveMessageId();
|
|
227
|
+
log(`[TOOL START] Tool: ${name}, phase: ${phase}, taskId: ${currentTaskId}`);
|
|
201
228
|
if (phase === "start") {
|
|
202
229
|
const toolName = name || "unknown";
|
|
203
230
|
try {
|
|
204
231
|
await sendStatusUpdate({
|
|
205
232
|
config,
|
|
206
233
|
sessionId,
|
|
207
|
-
taskId,
|
|
208
|
-
messageId,
|
|
234
|
+
taskId: currentTaskId,
|
|
235
|
+
messageId: currentMessageId,
|
|
209
236
|
text: `正在使用工具: ${toolName}...`,
|
|
210
237
|
state: "working",
|
|
211
238
|
});
|
|
@@ -216,25 +243,24 @@ export function createXYReplyDispatcher(params) {
|
|
|
216
243
|
}
|
|
217
244
|
}
|
|
218
245
|
},
|
|
219
|
-
// 🔧 Tool execution result callback
|
|
220
246
|
onToolResult: async (payload) => {
|
|
247
|
+
// 🔑 steer follower不发送tool结果(让主dispatcher处理)
|
|
248
|
+
if (isSteerFollower) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const currentTaskId = getActiveTaskId();
|
|
252
|
+
const currentMessageId = getActiveMessageId();
|
|
221
253
|
const text = payload.text ?? "";
|
|
222
254
|
const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
|
|
223
|
-
log(`[TOOL RESULT]
|
|
224
|
-
log(`[TOOL RESULT] - text.length=${text.length}`);
|
|
225
|
-
log(`[TOOL RESULT] - hasMedia=${hasMedia}`);
|
|
226
|
-
log(`[TOOL RESULT] - isError=${payload.isError}`);
|
|
227
|
-
if (text.length > 0) {
|
|
228
|
-
log(`[TOOL RESULT] - text preview: "${text.slice(0, 200)}"`);
|
|
229
|
-
}
|
|
255
|
+
log(`[TOOL RESULT] Tool result, taskId: ${currentTaskId}, text.length: ${text.length}`);
|
|
230
256
|
try {
|
|
231
257
|
if (text.length > 0 || hasMedia) {
|
|
232
258
|
const resultText = text.length > 0 ? text : "工具执行完成";
|
|
233
259
|
await sendStatusUpdate({
|
|
234
260
|
config,
|
|
235
261
|
sessionId,
|
|
236
|
-
taskId,
|
|
237
|
-
messageId,
|
|
262
|
+
taskId: currentTaskId,
|
|
263
|
+
messageId: currentMessageId,
|
|
238
264
|
text: resultText,
|
|
239
265
|
state: "working",
|
|
240
266
|
});
|
|
@@ -245,54 +271,45 @@ export function createXYReplyDispatcher(params) {
|
|
|
245
271
|
error(`[TOOL RESULT] ❌ Failed to send tool result status:`, err);
|
|
246
272
|
}
|
|
247
273
|
},
|
|
248
|
-
// 🧠 Reasoning/thinking process streaming callback
|
|
249
274
|
onReasoningStream: async (payload) => {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (text.length > 0) {
|
|
254
|
-
log(`[REASONING STREAM] - text preview: "${text.slice(0, 200)}"`);
|
|
275
|
+
// 🔑 steer follower不发送reasoning stream
|
|
276
|
+
if (isSteerFollower) {
|
|
277
|
+
return;
|
|
255
278
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// sessionId,
|
|
261
|
-
// taskId,
|
|
262
|
-
// messageId,
|
|
263
|
-
// text,
|
|
264
|
-
// });
|
|
265
|
-
// log(`[REASONING STREAM] ✅ Sent reasoning chunk as reasoningText update`);
|
|
266
|
-
// }
|
|
267
|
-
// } catch (err) {
|
|
268
|
-
// error(`[REASONING STREAM] ❌ Failed to send reasoning chunk reasoningText:`, err);
|
|
269
|
-
// }
|
|
279
|
+
const text = payload.text ?? "";
|
|
280
|
+
log(`[REASONING STREAM] Reasoning chunk received, text.length: ${text.length}`);
|
|
281
|
+
// Reasoning stream 目前被注释掉
|
|
282
|
+
// 如果需要可以启用
|
|
270
283
|
},
|
|
271
|
-
// 📝 Partial reply streaming callback (real-time preview)
|
|
272
284
|
onPartialReply: async (payload) => {
|
|
285
|
+
// 🔑 steer follower不发送partial reply(让主dispatcher处理)
|
|
286
|
+
if (isSteerFollower) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const currentTaskId = getActiveTaskId();
|
|
290
|
+
const currentMessageId = getActiveMessageId();
|
|
273
291
|
const text = payload.text ?? "";
|
|
274
|
-
|
|
275
|
-
log(`[PARTIAL REPLY] 📝 Partial reply chunk received: session=${sessionId}, taskId=${taskId}`);
|
|
292
|
+
log(`[PARTIAL REPLY] Partial reply chunk received, taskId: ${currentTaskId}`);
|
|
276
293
|
try {
|
|
277
294
|
if (text.length > 0) {
|
|
278
295
|
await sendReasoningTextUpdate({
|
|
279
296
|
config,
|
|
280
297
|
sessionId,
|
|
281
|
-
taskId,
|
|
282
|
-
messageId,
|
|
298
|
+
taskId: currentTaskId,
|
|
299
|
+
messageId: currentMessageId,
|
|
283
300
|
text,
|
|
284
301
|
append: false,
|
|
285
302
|
});
|
|
286
|
-
log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update
|
|
303
|
+
log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update`);
|
|
287
304
|
}
|
|
288
305
|
}
|
|
289
306
|
catch (err) {
|
|
290
|
-
error(`[PARTIAL REPLY] ❌ Failed to send partial reply
|
|
307
|
+
error(`[PARTIAL REPLY] ❌ Failed to send partial reply:`, err);
|
|
291
308
|
}
|
|
292
309
|
},
|
|
293
310
|
},
|
|
294
311
|
markDispatchIdle,
|
|
295
|
-
startStatusInterval,
|
|
296
|
-
stopStatusInterval,
|
|
312
|
+
startStatusInterval,
|
|
313
|
+
stopStatusInterval,
|
|
297
314
|
};
|
|
298
315
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface TaskIdBinding {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
currentTaskId: string;
|
|
4
|
+
currentMessageId: string;
|
|
5
|
+
refCount: number;
|
|
6
|
+
updatedAt: number;
|
|
7
|
+
locked: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 注册或更新session的活跃taskId
|
|
11
|
+
* 返回是否是更新(用于判断是否是第二条消息)
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerTaskId(sessionId: string, taskId: string, messageId: string, options?: {
|
|
14
|
+
incrementRef?: boolean;
|
|
15
|
+
}): {
|
|
16
|
+
isUpdate: boolean;
|
|
17
|
+
refCount: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* 增加引用计数(消息开始处理时调用)
|
|
21
|
+
*/
|
|
22
|
+
export declare function incrementTaskIdRef(sessionId: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* 减少引用计数,当refCount=0时才真正清理
|
|
25
|
+
*/
|
|
26
|
+
export declare function decrementTaskIdRef(sessionId: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* 锁定taskId,防止被清理(第一个消息使用)
|
|
29
|
+
*/
|
|
30
|
+
export declare function lockTaskId(sessionId: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* 解锁taskId(第一个消息完成时使用)
|
|
33
|
+
*/
|
|
34
|
+
export declare function unlockTaskId(sessionId: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* 获取session的当前活跃taskId
|
|
37
|
+
*/
|
|
38
|
+
export declare function getCurrentTaskId(sessionId: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* 获取session的当前活跃messageId
|
|
41
|
+
*/
|
|
42
|
+
export declare function getCurrentMessageId(sessionId: string): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* 检查session是否有活跃的taskId
|
|
45
|
+
*/
|
|
46
|
+
export declare function hasActiveTask(sessionId: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* 获取完整的binding信息(用于调试)
|
|
49
|
+
*/
|
|
50
|
+
export declare function getTaskIdBinding(sessionId: string): TaskIdBinding | null;
|
|
51
|
+
/**
|
|
52
|
+
* 强制清理(错误恢复用)
|
|
53
|
+
*/
|
|
54
|
+
export declare function forceCleanTaskId(sessionId: string): void;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// TaskId Manager - 管理session级别的活跃taskId
|
|
2
|
+
// 支持动态切换taskId,用于steer模式下的消息插队
|
|
3
|
+
import { logger } from "./utils/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Session到活跃TaskId的映射
|
|
6
|
+
* Key: sessionId (注意:这里用sessionId,不是sessionKey)
|
|
7
|
+
* Value: TaskIdBinding
|
|
8
|
+
*/
|
|
9
|
+
const activeTaskIds = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* 注册或更新session的活跃taskId
|
|
12
|
+
* 返回是否是更新(用于判断是否是第二条消息)
|
|
13
|
+
*/
|
|
14
|
+
export function registerTaskId(sessionId, taskId, messageId, options) {
|
|
15
|
+
logger.log(`[TASK_MANAGER] 📝 Registering/Updating taskId for session: ${sessionId}`);
|
|
16
|
+
logger.log(`[TASK_MANAGER] - New taskId: ${taskId}`);
|
|
17
|
+
logger.log(`[TASK_MANAGER] - New messageId: ${messageId}`);
|
|
18
|
+
logger.log(`[TASK_MANAGER] - incrementRef: ${options?.incrementRef ?? false}`);
|
|
19
|
+
const existing = activeTaskIds.get(sessionId);
|
|
20
|
+
if (existing) {
|
|
21
|
+
logger.log(`[TASK_MANAGER] - Previous taskId: ${existing.currentTaskId}`);
|
|
22
|
+
logger.log(`[TASK_MANAGER] - Previous refCount: ${existing.refCount}`);
|
|
23
|
+
logger.log(`[TASK_MANAGER] - 🔄 Switching taskId (steer mode detected)`);
|
|
24
|
+
// 更新taskId,但保持引用计数
|
|
25
|
+
existing.currentTaskId = taskId;
|
|
26
|
+
existing.currentMessageId = messageId;
|
|
27
|
+
existing.updatedAt = Date.now();
|
|
28
|
+
if (options?.incrementRef) {
|
|
29
|
+
existing.refCount++;
|
|
30
|
+
logger.log(`[TASK_MANAGER] - Incremented refCount: ${existing.refCount}`);
|
|
31
|
+
}
|
|
32
|
+
logger.log(`[TASK_MANAGER] - ✅ TaskId updated, refCount=${existing.refCount}`);
|
|
33
|
+
return { isUpdate: true, refCount: existing.refCount };
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// 新注册
|
|
37
|
+
const binding = {
|
|
38
|
+
sessionId,
|
|
39
|
+
currentTaskId: taskId,
|
|
40
|
+
currentMessageId: messageId,
|
|
41
|
+
refCount: 1,
|
|
42
|
+
updatedAt: Date.now(),
|
|
43
|
+
locked: false,
|
|
44
|
+
};
|
|
45
|
+
activeTaskIds.set(sessionId, binding);
|
|
46
|
+
logger.log(`[TASK_MANAGER] - ✅ TaskId registered (new), refCount=1`);
|
|
47
|
+
return { isUpdate: false, refCount: 1 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 增加引用计数(消息开始处理时调用)
|
|
52
|
+
*/
|
|
53
|
+
export function incrementTaskIdRef(sessionId) {
|
|
54
|
+
const binding = activeTaskIds.get(sessionId);
|
|
55
|
+
if (binding) {
|
|
56
|
+
binding.refCount++;
|
|
57
|
+
logger.log(`[TASK_MANAGER] ➕ Incremented refCount for ${sessionId}: ${binding.refCount}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 减少引用计数,当refCount=0时才真正清理
|
|
62
|
+
*/
|
|
63
|
+
export function decrementTaskIdRef(sessionId) {
|
|
64
|
+
const binding = activeTaskIds.get(sessionId);
|
|
65
|
+
if (!binding) {
|
|
66
|
+
logger.log(`[TASK_MANAGER] ⚠️ No binding found for ${sessionId}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
binding.refCount--;
|
|
70
|
+
logger.log(`[TASK_MANAGER] ➖ Decremented refCount for ${sessionId}: ${binding.refCount}`);
|
|
71
|
+
if (binding.refCount <= 0 && !binding.locked) {
|
|
72
|
+
logger.log(`[TASK_MANAGER] 🗑️ RefCount=0 and unlocked, clearing taskId`);
|
|
73
|
+
activeTaskIds.delete(sessionId);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
logger.log(`[TASK_MANAGER] - Keeping binding (refCount=${binding.refCount}, locked=${binding.locked})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 锁定taskId,防止被清理(第一个消息使用)
|
|
81
|
+
*/
|
|
82
|
+
export function lockTaskId(sessionId) {
|
|
83
|
+
const binding = activeTaskIds.get(sessionId);
|
|
84
|
+
if (binding) {
|
|
85
|
+
binding.locked = true;
|
|
86
|
+
logger.log(`[TASK_MANAGER] 🔒 Locked taskId for ${sessionId}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 解锁taskId(第一个消息完成时使用)
|
|
91
|
+
*/
|
|
92
|
+
export function unlockTaskId(sessionId) {
|
|
93
|
+
const binding = activeTaskIds.get(sessionId);
|
|
94
|
+
if (binding) {
|
|
95
|
+
binding.locked = false;
|
|
96
|
+
logger.log(`[TASK_MANAGER] 🔓 Unlocked taskId for ${sessionId}`);
|
|
97
|
+
// 解锁后,如果refCount=0,立即清理
|
|
98
|
+
if (binding.refCount <= 0) {
|
|
99
|
+
logger.log(`[TASK_MANAGER] 🗑️ Unlocked and refCount=0, clearing taskId`);
|
|
100
|
+
activeTaskIds.delete(sessionId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 获取session的当前活跃taskId
|
|
106
|
+
*/
|
|
107
|
+
export function getCurrentTaskId(sessionId) {
|
|
108
|
+
const binding = activeTaskIds.get(sessionId);
|
|
109
|
+
return binding?.currentTaskId ?? null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 获取session的当前活跃messageId
|
|
113
|
+
*/
|
|
114
|
+
export function getCurrentMessageId(sessionId) {
|
|
115
|
+
const binding = activeTaskIds.get(sessionId);
|
|
116
|
+
return binding?.currentMessageId ?? null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 检查session是否有活跃的taskId
|
|
120
|
+
*/
|
|
121
|
+
export function hasActiveTask(sessionId) {
|
|
122
|
+
return activeTaskIds.has(sessionId);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 获取完整的binding信息(用于调试)
|
|
126
|
+
*/
|
|
127
|
+
export function getTaskIdBinding(sessionId) {
|
|
128
|
+
return activeTaskIds.get(sessionId) ?? null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 强制清理(错误恢复用)
|
|
132
|
+
*/
|
|
133
|
+
export function forceCleanTaskId(sessionId) {
|
|
134
|
+
logger.log(`[TASK_MANAGER] ⚠️ Force clearing taskId for ${sessionId}`);
|
|
135
|
+
activeTaskIds.delete(sessionId);
|
|
136
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.js";
|
|
2
2
|
import { configManager } from "../utils/config-manager.js";
|
|
3
|
-
// Map of sessionKey ->
|
|
3
|
+
// Map of sessionKey -> SessionContextWithRef
|
|
4
4
|
const activeSessions = new Map();
|
|
5
5
|
/**
|
|
6
6
|
* Register a session context for tool access.
|
|
@@ -13,7 +13,22 @@ export function registerSession(sessionKey, context) {
|
|
|
13
13
|
logger.log(`[SESSION_MANAGER] - messageId: ${context.messageId}`);
|
|
14
14
|
logger.log(`[SESSION_MANAGER] - agentId: ${context.agentId}`);
|
|
15
15
|
logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
|
|
16
|
-
activeSessions.
|
|
16
|
+
const existing = activeSessions.get(sessionKey);
|
|
17
|
+
if (existing) {
|
|
18
|
+
// 更新上下文,增加引用计数
|
|
19
|
+
existing.taskId = context.taskId;
|
|
20
|
+
existing.messageId = context.messageId;
|
|
21
|
+
existing.refCount++;
|
|
22
|
+
logger.log(`[SESSION_MANAGER] - Updated existing, refCount=${existing.refCount}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// 新建
|
|
26
|
+
activeSessions.set(sessionKey, {
|
|
27
|
+
...context,
|
|
28
|
+
refCount: 1,
|
|
29
|
+
});
|
|
30
|
+
logger.log(`[SESSION_MANAGER] - Created new, refCount=1`);
|
|
31
|
+
}
|
|
17
32
|
logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
|
|
18
33
|
logger.log(`[SESSION_MANAGER] - All session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
|
|
19
34
|
}
|
|
@@ -25,14 +40,18 @@ export function unregisterSession(sessionKey) {
|
|
|
25
40
|
logger.log(`[SESSION_MANAGER] 🗑️ Unregistering session: ${sessionKey}`);
|
|
26
41
|
logger.log(`[SESSION_MANAGER] - Active sessions before: ${activeSessions.size}`);
|
|
27
42
|
logger.log(`[SESSION_MANAGER] - Session existed: ${activeSessions.has(sessionKey)}`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
const existing = activeSessions.get(sessionKey);
|
|
44
|
+
if (!existing) {
|
|
45
|
+
logger.log(`[SESSION_MANAGER] - Session not found`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
existing.refCount--;
|
|
49
|
+
logger.log(`[SESSION_MANAGER] - Decremented refCount: ${existing.refCount}`);
|
|
50
|
+
if (existing.refCount <= 0) {
|
|
51
|
+
activeSessions.delete(sessionKey);
|
|
52
|
+
configManager.clearSession(existing.sessionId);
|
|
53
|
+
logger.log(`[SESSION_MANAGER] - Deleted (refCount=0)`);
|
|
34
54
|
}
|
|
35
|
-
logger.log(`[SESSION_MANAGER] - Deleted: ${existed}`);
|
|
36
55
|
logger.log(`[SESSION_MANAGER] - Active sessions after: ${activeSessions.size}`);
|
|
37
56
|
logger.log(`[SESSION_MANAGER] - Remaining session keys: [${Array.from(activeSessions.keys()).join(", ")}]`);
|
|
38
57
|
}
|
|
@@ -43,12 +62,15 @@ export function unregisterSession(sessionKey) {
|
|
|
43
62
|
export function getSessionContext(sessionKey) {
|
|
44
63
|
logger.log(`[SESSION_MANAGER] 🔍 Getting session by key: ${sessionKey}`);
|
|
45
64
|
logger.log(`[SESSION_MANAGER] - Active sessions: ${activeSessions.size}`);
|
|
46
|
-
const
|
|
47
|
-
logger.log(`[SESSION_MANAGER] - Found: ${
|
|
48
|
-
if (
|
|
49
|
-
logger.log(`[SESSION_MANAGER] - sessionId: ${
|
|
65
|
+
const contextWithRef = activeSessions.get(sessionKey) ?? null;
|
|
66
|
+
logger.log(`[SESSION_MANAGER] - Found: ${contextWithRef !== null}`);
|
|
67
|
+
if (contextWithRef) {
|
|
68
|
+
logger.log(`[SESSION_MANAGER] - sessionId: ${contextWithRef.sessionId}`);
|
|
69
|
+
// 返回时去掉refCount字段
|
|
70
|
+
const { refCount, ...context } = contextWithRef;
|
|
71
|
+
return context;
|
|
50
72
|
}
|
|
51
|
-
return
|
|
73
|
+
return null;
|
|
52
74
|
}
|
|
53
75
|
/**
|
|
54
76
|
* Get the most recent session context.
|
|
@@ -65,10 +87,12 @@ export function getLatestSessionContext() {
|
|
|
65
87
|
}
|
|
66
88
|
// Return the last added session
|
|
67
89
|
const sessions = Array.from(activeSessions.values());
|
|
68
|
-
const
|
|
90
|
+
const latestSessionWithRef = sessions[sessions.length - 1];
|
|
69
91
|
logger.log(`[SESSION_MANAGER] - ✅ Found latest session:`);
|
|
70
|
-
logger.log(`[SESSION_MANAGER] - sessionId: ${
|
|
71
|
-
logger.log(`[SESSION_MANAGER] - taskId: ${
|
|
72
|
-
logger.log(`[SESSION_MANAGER] - messageId: ${
|
|
92
|
+
logger.log(`[SESSION_MANAGER] - sessionId: ${latestSessionWithRef.sessionId}`);
|
|
93
|
+
logger.log(`[SESSION_MANAGER] - taskId: ${latestSessionWithRef.taskId}`);
|
|
94
|
+
logger.log(`[SESSION_MANAGER] - messageId: ${latestSessionWithRef.messageId}`);
|
|
95
|
+
// 返回时去掉refCount字段
|
|
96
|
+
const { refCount, ...latestSession } = latestSessionWithRef;
|
|
73
97
|
return latestSession;
|
|
74
98
|
}
|
|
@@ -21,18 +21,18 @@ export const xiaoyiGuiTool = {
|
|
|
21
21
|
- 需要在APP中发布或发送内容
|
|
22
22
|
- 需要修改APP或手机设置
|
|
23
23
|
|
|
24
|
-
理论上,所有可以通过人在手机上操作完成的任务,该Agent都可以尝试执行。
|
|
25
|
-
|
|
26
24
|
注意事项:
|
|
27
|
-
- 操作超时时间为
|
|
25
|
+
- 操作超时时间为3分钟(180秒)
|
|
28
26
|
- 该工具执行时间较长,请勿重复调用
|
|
29
|
-
-
|
|
27
|
+
- 该工具执行期间不要执行别的工具调用,必须等到该工具有结果返回或者超时之后才能执行别的操作,无论是新的文本回复还是下一步的工具调用,在此工具执行期间必须严格等待
|
|
28
|
+
- 如果超时或失败,最多重试一次
|
|
29
|
+
- 如果用户指令中包含备忘录读写,日程查看,不需要将这类操作放在gui tool的query参数中,需要使用预置的note相关工具与calendar相关工具完成相关操作`,
|
|
30
30
|
parameters: {
|
|
31
31
|
type: "object",
|
|
32
32
|
properties: {
|
|
33
33
|
query: {
|
|
34
34
|
type: "string",
|
|
35
|
-
description: "
|
|
35
|
+
description: "操作手机的指令以及期望返回的结果。",
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
required: ["query"],
|
|
@@ -87,7 +87,7 @@ export const xiaoyiGuiTool = {
|
|
|
87
87
|
logger.error(`[XIAOYI_GUI_TOOL] ⏰ Timeout: No response received within 300 seconds (5 minutes)`);
|
|
88
88
|
wsManager.off("gui-agent-response", handler);
|
|
89
89
|
reject(new Error("XiaoYi GUI Agent 操作超时(5分钟)"));
|
|
90
|
-
},
|
|
90
|
+
}, 180000); // 5 minutes timeout
|
|
91
91
|
// Listen for GUI agent response events
|
|
92
92
|
const handler = (event) => {
|
|
93
93
|
logger.log(`[XIAOYI_GUI_TOOL] 📨 Received event:`, JSON.stringify(event));
|