@ynhcj/xiaoyi-channel 0.0.9 → 0.0.10-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/src/bot.js +153 -48
- package/dist/src/channel.js +23 -4
- package/dist/src/client.d.ts +15 -0
- package/dist/src/client.js +81 -0
- package/dist/src/config.js +2 -2
- package/dist/src/file-download.js +10 -1
- package/dist/src/formatter.d.ts +31 -0
- package/dist/src/formatter.js +93 -1
- package/dist/src/heartbeat.d.ts +2 -1
- package/dist/src/heartbeat.js +6 -1
- package/dist/src/monitor.d.ts +5 -0
- package/dist/src/monitor.js +101 -9
- package/dist/src/outbound.js +97 -7
- package/dist/src/parser.d.ts +12 -0
- package/dist/src/parser.js +37 -0
- package/dist/src/push.d.ts +13 -1
- package/dist/src/push.js +125 -19
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +206 -51
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.d.ts +6 -0
- package/dist/src/tools/calendar-tool.js +169 -0
- package/dist/src/tools/call-phone-tool.d.ts +5 -0
- package/dist/src/tools/call-phone-tool.js +183 -0
- package/dist/src/tools/create-alarm-tool.d.ts +7 -0
- package/dist/src/tools/create-alarm-tool.js +446 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +238 -0
- package/dist/src/tools/location-tool.js +18 -8
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +467 -0
- package/dist/src/tools/modify-note-tool.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- package/dist/src/tools/note-tool.js +32 -11
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +391 -0
- package/dist/src/tools/search-calendar-tool.d.ts +12 -0
- package/dist/src/tools/search-calendar-tool.js +262 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +168 -0
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +185 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +173 -0
- package/dist/src/tools/search-note-tool.js +6 -6
- package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
- package/dist/src/tools/search-photo-gallery-tool.js +212 -0
- package/dist/src/tools/search-photo-tool.d.ts +9 -0
- package/dist/src/tools/search-photo-tool.js +270 -0
- package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
- package/dist/src/tools/send-file-to-user-tool.js +318 -0
- package/dist/src/tools/send-message-tool.d.ts +5 -0
- package/dist/src/tools/send-message-tool.js +189 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +101 -13
- package/dist/src/tools/upload-file-tool.d.ts +13 -0
- package/dist/src/tools/upload-file-tool.js +265 -0
- package/dist/src/tools/upload-photo-tool.d.ts +9 -0
- package/dist/src/tools/upload-photo-tool.js +223 -0
- package/dist/src/tools/view-push-result-tool.d.ts +5 -0
- package/dist/src/tools/view-push-result-tool.js +118 -0
- package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +190 -0
- package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
- package/dist/src/trigger-handler.d.ts +22 -0
- package/dist/src/trigger-handler.js +59 -0
- package/dist/src/types.d.ts +6 -17
- package/dist/src/types.js +4 -0
- package/dist/src/utils/config-manager.d.ts +26 -0
- package/dist/src/utils/config-manager.js +56 -0
- package/dist/src/utils/pushdata-manager.d.ts +28 -0
- package/dist/src/utils/pushdata-manager.js +171 -0
- package/dist/src/utils/pushid-manager.d.ts +12 -0
- package/dist/src/utils/pushid-manager.js +105 -0
- package/dist/src/websocket.d.ts +59 -25
- package/dist/src/websocket.js +315 -257
- package/package.json +1 -1
package/dist/src/push.js
CHANGED
|
@@ -1,46 +1,150 @@
|
|
|
1
1
|
// Push message service for scheduled tasks
|
|
2
2
|
import fetch from "node-fetch";
|
|
3
|
-
import {
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { configManager } from "./utils/config-manager.js";
|
|
4
5
|
/**
|
|
5
6
|
* Service for sending push messages to users.
|
|
6
7
|
* Used for outbound messages and scheduled tasks.
|
|
7
8
|
*/
|
|
8
9
|
export class XYPushService {
|
|
9
10
|
config;
|
|
11
|
+
DEFAULT_PUSH_URL = "https://hag.cloud.huawei.com/open-ability-agent/v1/agent-webhook";
|
|
12
|
+
REQUEST_FROM = "openclaw";
|
|
10
13
|
constructor(config) {
|
|
11
14
|
this.config = config;
|
|
12
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate a random trace ID for request tracking.
|
|
18
|
+
*/
|
|
19
|
+
generateTraceId() {
|
|
20
|
+
return `trace-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
21
|
+
}
|
|
13
22
|
/**
|
|
14
23
|
* Send a push message to a user session.
|
|
24
|
+
*
|
|
25
|
+
* @param content - Push message content
|
|
26
|
+
* @param title - Push message title
|
|
27
|
+
* @param data - Optional additional data
|
|
28
|
+
* @param sessionId - Optional session ID
|
|
29
|
+
* @param pushDataId - Optional pushDataId for kind="data" format
|
|
15
30
|
*/
|
|
16
|
-
async sendPush(content, title, sessionId) {
|
|
17
|
-
const pushUrl = this.config.pushUrl ||
|
|
18
|
-
|
|
31
|
+
async sendPush(content, title, data, sessionId, pushDataId) {
|
|
32
|
+
const pushUrl = this.config.pushUrl || this.DEFAULT_PUSH_URL;
|
|
33
|
+
const traceId = this.generateTraceId();
|
|
34
|
+
// Get dynamic pushId for the session (falls back to config pushId)
|
|
35
|
+
const dynamicPushId = configManager.getPushId(sessionId);
|
|
36
|
+
const pushId = dynamicPushId || this.config.pushId;
|
|
37
|
+
console.log(`[PUSH] 📤 Preparing to send push message`);
|
|
38
|
+
console.log(`[PUSH] - Title: "${title}"`);
|
|
39
|
+
console.log(`[PUSH] - Content length: ${content.length} chars`);
|
|
40
|
+
console.log(`[PUSH] - Session ID: ${sessionId || 'none'}`);
|
|
41
|
+
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
42
|
+
console.log(`[PUSH] - Push URL: ${pushUrl}`);
|
|
43
|
+
if (dynamicPushId) {
|
|
44
|
+
console.log(`[PUSH] - Using dynamic pushId (from session): ${pushId.substring(0, 20)}...`);
|
|
45
|
+
console.log(`[PUSH] - Full dynamic pushId: ${pushId}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log(`[PUSH] - Using config pushId (fallback): ${pushId.substring(0, 20)}...`);
|
|
49
|
+
console.log(`[PUSH] - Full config pushId: ${pushId}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(`[PUSH] - API ID: ${this.config.apiId}`);
|
|
52
|
+
console.log(`[PUSH] - UID: ${this.config.uid}`);
|
|
19
53
|
try {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
54
|
+
const requestBody = {
|
|
55
|
+
jsonrpc: "2.0",
|
|
56
|
+
id: randomUUID(),
|
|
57
|
+
result: {
|
|
58
|
+
id: randomUUID(),
|
|
59
|
+
apiId: this.config.apiId,
|
|
60
|
+
pushId: pushId, // Use dynamic pushId
|
|
61
|
+
pushText: title,
|
|
62
|
+
kind: "task",
|
|
63
|
+
artifacts: [
|
|
64
|
+
{
|
|
65
|
+
artifactId: randomUUID(),
|
|
66
|
+
parts: pushDataId
|
|
67
|
+
? [
|
|
68
|
+
{
|
|
69
|
+
kind: "data",
|
|
70
|
+
data: {
|
|
71
|
+
pushDataId: pushDataId,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
: [
|
|
76
|
+
{
|
|
77
|
+
kind: "text",
|
|
78
|
+
text: content,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
27
84
|
};
|
|
85
|
+
console.log(`[PUSH] Full request body:`, JSON.stringify(requestBody, null, 2));
|
|
28
86
|
const response = await fetch(pushUrl, {
|
|
29
87
|
method: "POST",
|
|
30
88
|
headers: {
|
|
31
89
|
"Content-Type": "application/json",
|
|
90
|
+
"Accept": "application/json",
|
|
91
|
+
"x-hag-trace-id": traceId,
|
|
92
|
+
"x-uid": this.config.uid,
|
|
32
93
|
"x-api-key": this.config.apiKey,
|
|
33
|
-
"x-request-from":
|
|
94
|
+
"x-request-from": this.REQUEST_FROM,
|
|
34
95
|
},
|
|
35
|
-
body: JSON.stringify(
|
|
96
|
+
body: JSON.stringify(requestBody),
|
|
36
97
|
});
|
|
98
|
+
// Log response status and headers
|
|
99
|
+
console.log(`[PUSH] 📥 Response received`);
|
|
100
|
+
console.log(`[PUSH] - HTTP Status: ${response.status} ${response.statusText}`);
|
|
101
|
+
console.log(`[PUSH] - Content-Type: ${response.headers.get('content-type')}`);
|
|
102
|
+
console.log(`[PUSH] - Content-Length: ${response.headers.get('content-length')}`);
|
|
37
103
|
if (!response.ok) {
|
|
38
|
-
|
|
104
|
+
const errorText = await response.text();
|
|
105
|
+
console.log(`[PUSH] ❌ Push request failed`);
|
|
106
|
+
console.log(`[PUSH] - HTTP Status: ${response.status}`);
|
|
107
|
+
console.log(`[PUSH] - Response body: ${errorText}`);
|
|
108
|
+
throw new Error(`Push failed: HTTP ${response.status} - ${errorText}`);
|
|
109
|
+
}
|
|
110
|
+
// Try to parse JSON response with detailed error handling
|
|
111
|
+
let result;
|
|
112
|
+
try {
|
|
113
|
+
const responseText = await response.text();
|
|
114
|
+
console.log(`[PUSH] 📄 Response body length: ${responseText.length} chars`);
|
|
115
|
+
console.log(`[PUSH] 📄 Response body preview: ${responseText.substring(0, 200)}`);
|
|
116
|
+
if (!responseText || responseText.trim() === '') {
|
|
117
|
+
console.log(`[PUSH] ⚠️ Received empty response body`);
|
|
118
|
+
result = {};
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
result = JSON.parse(responseText);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (parseError) {
|
|
125
|
+
console.log(`[PUSH] ❌ Failed to parse JSON response`);
|
|
126
|
+
console.log(`[PUSH] - Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
127
|
+
throw new Error(`Invalid JSON response from push service: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
39
128
|
}
|
|
40
|
-
|
|
129
|
+
console.log(`[PUSH] ✅ Push message sent successfully`);
|
|
130
|
+
console.log(`[PUSH] - Title: "${title}"`);
|
|
131
|
+
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
132
|
+
console.log(`[PUSH] - Used pushId: ${pushId.substring(0, 20)}...`);
|
|
133
|
+
console.log(`[PUSH] - Response:`, result);
|
|
41
134
|
}
|
|
42
135
|
catch (error) {
|
|
43
|
-
|
|
136
|
+
console.log(`[PUSH] ❌ Failed to send push message`);
|
|
137
|
+
console.log(`[PUSH] - Trace ID: ${traceId}`);
|
|
138
|
+
console.log(`[PUSH] - Target URL: ${pushUrl}`);
|
|
139
|
+
console.log(`[PUSH] - Push ID: ${pushId.substring(0, 20)}...`);
|
|
140
|
+
if (error instanceof Error) {
|
|
141
|
+
console.log(`[PUSH] - Error name: ${error.name}`);
|
|
142
|
+
console.log(`[PUSH] - Error message: ${error.message}`);
|
|
143
|
+
console.log(`[PUSH] - Error stack:`, error.stack);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(`[PUSH] - Error:`, error);
|
|
147
|
+
}
|
|
44
148
|
throw error;
|
|
45
149
|
}
|
|
46
150
|
}
|
|
@@ -48,8 +152,10 @@ export class XYPushService {
|
|
|
48
152
|
* Send a push message with file attachments.
|
|
49
153
|
*/
|
|
50
154
|
async sendPushWithFiles(content, title, fileIds, sessionId) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
155
|
+
const data = {
|
|
156
|
+
content,
|
|
157
|
+
fileIds,
|
|
158
|
+
};
|
|
159
|
+
await this.sendPush(content, title, data, sessionId);
|
|
54
160
|
}
|
|
55
161
|
}
|
|
@@ -1,64 +1,71 @@
|
|
|
1
1
|
import { createReplyPrefixContext } from "openclaw/plugin-sdk";
|
|
2
2
|
import { getXYRuntime } from "./runtime.js";
|
|
3
|
-
import { sendA2AResponse, sendStatusUpdate } from "./formatter.js";
|
|
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) => {
|
|
49
60
|
error(`Failed to send status update:`, err);
|
|
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,27 +73,31 @@ 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}`);
|
|
92
|
+
// 🔑 使用动态taskId发送reasoningText更新
|
|
93
|
+
await sendReasoningTextUpdate({
|
|
94
|
+
config,
|
|
95
|
+
sessionId,
|
|
96
|
+
taskId: currentTaskId,
|
|
97
|
+
messageId: currentMessageId,
|
|
98
|
+
text,
|
|
99
|
+
});
|
|
100
|
+
log(`[DELIVER] ✅ Sent deliver text as reasoningText update`);
|
|
90
101
|
}
|
|
91
102
|
catch (deliverError) {
|
|
92
103
|
error(`Failed to deliver message:`, deliverError);
|
|
@@ -94,16 +105,21 @@ export function createXYReplyDispatcher(params) {
|
|
|
94
105
|
},
|
|
95
106
|
onError: async (err, info) => {
|
|
96
107
|
runtime.error?.(`xy: ${info.kind} reply failed: ${String(err)}`);
|
|
97
|
-
// Stop status updates
|
|
98
108
|
stopStatusInterval();
|
|
99
|
-
//
|
|
109
|
+
// 🔑 steer follower不发送错误状态(让主dispatcher处理)
|
|
110
|
+
if (isSteerFollower) {
|
|
111
|
+
log(`[ON_ERROR] Steer follower - skipping error response`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
100
114
|
if (!hasSentResponse) {
|
|
115
|
+
const currentTaskId = getActiveTaskId();
|
|
116
|
+
const currentMessageId = getActiveMessageId();
|
|
101
117
|
try {
|
|
102
118
|
await sendStatusUpdate({
|
|
103
119
|
config,
|
|
104
120
|
sessionId,
|
|
105
|
-
taskId,
|
|
106
|
-
messageId,
|
|
121
|
+
taskId: currentTaskId,
|
|
122
|
+
messageId: currentMessageId,
|
|
107
123
|
text: "处理失败,请稍后重试",
|
|
108
124
|
state: "failed",
|
|
109
125
|
});
|
|
@@ -114,37 +130,86 @@ export function createXYReplyDispatcher(params) {
|
|
|
114
130
|
}
|
|
115
131
|
},
|
|
116
132
|
onIdle: async () => {
|
|
117
|
-
|
|
118
|
-
|
|
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的第一条消息)
|
|
119
149
|
if (hasSentResponse && !finalSent) {
|
|
120
150
|
log(`[ON_IDLE] Sending accumulated text, length=${accumulatedText.length}`);
|
|
121
151
|
try {
|
|
152
|
+
// 🔑 使用动态taskId发送完成状态
|
|
153
|
+
await sendStatusUpdate({
|
|
154
|
+
config,
|
|
155
|
+
sessionId,
|
|
156
|
+
taskId: currentTaskId,
|
|
157
|
+
messageId: currentMessageId,
|
|
158
|
+
text: "任务处理已完成~",
|
|
159
|
+
state: "completed",
|
|
160
|
+
});
|
|
161
|
+
log(`[ON_IDLE] ✅ Sent completion status update`);
|
|
162
|
+
// 🔑 使用动态taskId发送最终响应
|
|
122
163
|
await sendA2AResponse({
|
|
123
164
|
config,
|
|
124
165
|
sessionId,
|
|
125
|
-
taskId,
|
|
126
|
-
messageId,
|
|
166
|
+
taskId: currentTaskId,
|
|
167
|
+
messageId: currentMessageId,
|
|
127
168
|
text: accumulatedText,
|
|
128
169
|
append: false,
|
|
129
170
|
final: true,
|
|
130
171
|
});
|
|
131
172
|
finalSent = true;
|
|
132
|
-
log(`[ON_IDLE] Sent
|
|
173
|
+
log(`[ON_IDLE] ✅ Sent final response with taskId=${currentTaskId}`);
|
|
133
174
|
}
|
|
134
175
|
catch (err) {
|
|
135
|
-
error(`[ON_IDLE] Failed to send
|
|
176
|
+
error(`[ON_IDLE] Failed to send final response:`, err);
|
|
136
177
|
}
|
|
137
178
|
}
|
|
138
179
|
else {
|
|
180
|
+
// 正常失败场景(非steer follower)
|
|
139
181
|
log(`[ON_IDLE] Skipping final message: hasSentResponse=${hasSentResponse}, finalSent=${finalSent}`);
|
|
182
|
+
try {
|
|
183
|
+
await sendStatusUpdate({
|
|
184
|
+
config,
|
|
185
|
+
sessionId,
|
|
186
|
+
taskId: currentTaskId,
|
|
187
|
+
messageId: currentMessageId,
|
|
188
|
+
text: "任务处理中断了~",
|
|
189
|
+
state: "failed",
|
|
190
|
+
});
|
|
191
|
+
log(`[ON_IDLE] ✅ Sent failure status update`);
|
|
192
|
+
await sendA2AResponse({
|
|
193
|
+
config,
|
|
194
|
+
sessionId,
|
|
195
|
+
taskId: currentTaskId,
|
|
196
|
+
messageId: currentMessageId,
|
|
197
|
+
text: "任务执行异常,请重试~",
|
|
198
|
+
append: false,
|
|
199
|
+
final: true,
|
|
200
|
+
});
|
|
201
|
+
finalSent = true;
|
|
202
|
+
log(`[ON_IDLE] ✅ Sent error response`);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
error(`[ON_IDLE] Failed to send error response:`, err);
|
|
206
|
+
}
|
|
140
207
|
}
|
|
141
|
-
// Stop status updates
|
|
142
208
|
stopStatusInterval();
|
|
143
209
|
},
|
|
144
210
|
onCleanup: () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
stopStatusInterval();
|
|
211
|
+
const currentTaskId = getActiveTaskId();
|
|
212
|
+
log(`[ON_CLEANUP] Reply cleanup, taskId=${currentTaskId}, isSteerFollower=${isSteerFollower}`);
|
|
148
213
|
},
|
|
149
214
|
});
|
|
150
215
|
return {
|
|
@@ -152,9 +217,99 @@ export function createXYReplyDispatcher(params) {
|
|
|
152
217
|
replyOptions: {
|
|
153
218
|
...replyOptions,
|
|
154
219
|
onModelSelected: prefixContext.onModelSelected,
|
|
220
|
+
onToolStart: async ({ name, phase }) => {
|
|
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}`);
|
|
228
|
+
if (phase === "start") {
|
|
229
|
+
const toolName = name || "unknown";
|
|
230
|
+
try {
|
|
231
|
+
await sendStatusUpdate({
|
|
232
|
+
config,
|
|
233
|
+
sessionId,
|
|
234
|
+
taskId: currentTaskId,
|
|
235
|
+
messageId: currentMessageId,
|
|
236
|
+
text: `正在使用工具: ${toolName}...`,
|
|
237
|
+
state: "working",
|
|
238
|
+
});
|
|
239
|
+
log(`[TOOL START] ✅ Sent status update for tool start: ${toolName}`);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
error(`[TOOL START] ❌ Failed to send tool start status:`, err);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
onToolResult: async (payload) => {
|
|
247
|
+
// 🔑 steer follower不发送tool结果(让主dispatcher处理)
|
|
248
|
+
if (isSteerFollower) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const currentTaskId = getActiveTaskId();
|
|
252
|
+
const currentMessageId = getActiveMessageId();
|
|
253
|
+
const text = payload.text ?? "";
|
|
254
|
+
const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
|
|
255
|
+
log(`[TOOL RESULT] Tool result, taskId: ${currentTaskId}, text.length: ${text.length}`);
|
|
256
|
+
try {
|
|
257
|
+
if (text.length > 0 || hasMedia) {
|
|
258
|
+
const resultText = text.length > 0 ? text : "工具执行完成";
|
|
259
|
+
await sendStatusUpdate({
|
|
260
|
+
config,
|
|
261
|
+
sessionId,
|
|
262
|
+
taskId: currentTaskId,
|
|
263
|
+
messageId: currentMessageId,
|
|
264
|
+
text: resultText,
|
|
265
|
+
state: "working",
|
|
266
|
+
});
|
|
267
|
+
log(`[TOOL RESULT] ✅ Sent tool result as status update`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
error(`[TOOL RESULT] ❌ Failed to send tool result status:`, err);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
onReasoningStream: async (payload) => {
|
|
275
|
+
// 🔑 steer follower不发送reasoning stream
|
|
276
|
+
if (isSteerFollower) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const text = payload.text ?? "";
|
|
280
|
+
log(`[REASONING STREAM] Reasoning chunk received, text.length: ${text.length}`);
|
|
281
|
+
// Reasoning stream 目前被注释掉
|
|
282
|
+
// 如果需要可以启用
|
|
283
|
+
},
|
|
284
|
+
onPartialReply: async (payload) => {
|
|
285
|
+
// 🔑 steer follower不发送partial reply(让主dispatcher处理)
|
|
286
|
+
if (isSteerFollower) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const currentTaskId = getActiveTaskId();
|
|
290
|
+
const currentMessageId = getActiveMessageId();
|
|
291
|
+
const text = payload.text ?? "";
|
|
292
|
+
log(`[PARTIAL REPLY] Partial reply chunk received, taskId: ${currentTaskId}`);
|
|
293
|
+
try {
|
|
294
|
+
if (text.length > 0) {
|
|
295
|
+
await sendReasoningTextUpdate({
|
|
296
|
+
config,
|
|
297
|
+
sessionId,
|
|
298
|
+
taskId: currentTaskId,
|
|
299
|
+
messageId: currentMessageId,
|
|
300
|
+
text,
|
|
301
|
+
append: false,
|
|
302
|
+
});
|
|
303
|
+
log(`[PARTIAL REPLY] ✅ Sent partial reply as reasoningText update`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
error(`[PARTIAL REPLY] ❌ Failed to send partial reply:`, err);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
155
310
|
},
|
|
156
311
|
markDispatchIdle,
|
|
157
|
-
startStatusInterval,
|
|
158
|
-
stopStatusInterval,
|
|
312
|
+
startStatusInterval,
|
|
313
|
+
stopStatusInterval,
|
|
159
314
|
};
|
|
160
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 {};
|