@ynhcj/xiaoyi-channel 0.0.18-beta → 0.0.18-next
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +40 -0
- package/dist/src/bot.js +123 -41
- package/dist/src/channel.js +17 -4
- package/dist/src/client.js +11 -24
- package/dist/src/config.js +2 -2
- package/dist/src/cspl/call-api.d.ts +3 -0
- package/dist/src/cspl/call-api.js +79 -0
- package/dist/src/cspl/config.d.ts +19 -0
- package/dist/src/cspl/config.js +50 -0
- package/dist/src/cspl/constants.d.ts +43 -0
- package/dist/src/cspl/constants.js +22 -0
- package/dist/src/cspl/utils.d.ts +10 -0
- package/dist/src/cspl/utils.js +57 -0
- package/dist/src/file-upload.d.ts +5 -0
- package/dist/src/file-upload.js +92 -0
- package/dist/src/formatter.d.ts +14 -0
- package/dist/src/formatter.js +46 -2
- package/dist/src/monitor.js +47 -5
- package/dist/src/outbound.js +52 -6
- package/dist/src/parser.d.ts +7 -0
- package/dist/src/parser.js +22 -0
- package/dist/src/push.d.ts +8 -1
- package/dist/src/push.js +30 -22
- package/dist/src/reply-dispatcher.d.ts +1 -0
- package/dist/src/reply-dispatcher.js +116 -101
- package/dist/src/steer-injector.d.ts +16 -0
- package/dist/src/steer-injector.js +82 -0
- package/dist/src/task-manager.d.ts +55 -0
- package/dist/src/task-manager.js +136 -0
- package/dist/src/tools/calendar-tool.js +5 -3
- 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 +420 -0
- package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
- package/dist/src/tools/delete-alarm-tool.js +216 -0
- package/dist/src/tools/image-reading-tool.d.ts +5 -0
- package/dist/src/tools/image-reading-tool.js +375 -0
- package/dist/src/tools/location-tool.js +8 -11
- package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
- package/dist/src/tools/modify-alarm-tool.js +439 -0
- package/dist/src/tools/modify-note-tool.js +4 -9
- package/dist/src/tools/note-tool.js +32 -21
- package/dist/src/tools/search-alarm-tool.d.ts +8 -0
- package/dist/src/tools/search-alarm-tool.js +343 -0
- package/dist/src/tools/search-calendar-tool.js +9 -46
- package/dist/src/tools/search-contact-tool.js +4 -27
- package/dist/src/tools/search-file-tool.d.ts +5 -0
- package/dist/src/tools/search-file-tool.js +161 -0
- package/dist/src/tools/search-message-tool.d.ts +5 -0
- package/dist/src/tools/search-message-tool.js +149 -0
- package/dist/src/tools/search-note-tool.js +29 -22
- package/dist/src/tools/search-photo-gallery-tool.js +51 -34
- 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 +176 -0
- package/dist/src/tools/session-manager.d.ts +15 -0
- package/dist/src/tools/session-manager.js +99 -18
- 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.js +14 -4
- 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 +1 -8
- package/dist/src/types.js +4 -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 +26 -31
- package/dist/src/websocket.js +227 -267
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { xyPlugin } from "./src/channel.js";
|
|
3
3
|
import { setXYRuntime } from "./src/runtime.js";
|
|
4
|
+
import { tryInjectSteer } from "./src/steer-injector.js";
|
|
5
|
+
import { callCsplApi } from "./src/cspl/call-api.js";
|
|
6
|
+
import { extractResultText, processText, parseSecurityResult, validateAndTruncateText } from "./src/cspl/utils.js";
|
|
7
|
+
import { ALLOWED_TOOLS, MIN_TEXT_LENGTH, MAX_TOTAL_LENGTH, MAX_TEXT_LENGTH, STEER_ABORT_MESSAGE, } from "./src/cspl/constants.js";
|
|
4
8
|
/**
|
|
5
9
|
* Xiaoyi Channel Plugin Entry Point.
|
|
6
10
|
* Exports the plugin for OpenClaw to load.
|
|
@@ -14,6 +18,42 @@ const plugin = {
|
|
|
14
18
|
register(api) {
|
|
15
19
|
setXYRuntime(api.runtime);
|
|
16
20
|
api.registerChannel({ plugin: xyPlugin });
|
|
21
|
+
// CSPL after_tool_call hook: 监听工具结果,发送至 CSPL API 进行安全检测
|
|
22
|
+
// 如果响应为 REJECT,注入 steer 消息中止当前对话
|
|
23
|
+
api.on("after_tool_call", async (event, ctx) => {
|
|
24
|
+
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const resultText = extractResultText(event, event.toolName);
|
|
29
|
+
const resultLength = resultText.length;
|
|
30
|
+
if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
|
|
34
|
+
const questionText = {
|
|
35
|
+
tool: event.toolName,
|
|
36
|
+
output: [{ content: "" }],
|
|
37
|
+
};
|
|
38
|
+
const originText = processText(resultText);
|
|
39
|
+
questionText.output[0].content = originText;
|
|
40
|
+
let finalJson = JSON.stringify(questionText);
|
|
41
|
+
if (finalJson.length > MAX_TEXT_LENGTH) {
|
|
42
|
+
const diff = finalJson.length - MAX_TEXT_LENGTH;
|
|
43
|
+
const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
|
|
44
|
+
questionText.output[0].content = trimmed;
|
|
45
|
+
finalJson = JSON.stringify(questionText);
|
|
46
|
+
}
|
|
47
|
+
const response = await callCsplApi(finalJson, api.config);
|
|
48
|
+
const result = parseSecurityResult(response);
|
|
49
|
+
if (result.status === "REJECT") {
|
|
50
|
+
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
api.logger.error(`[CSPL] after_tool_call error: ${err}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
17
57
|
},
|
|
18
58
|
};
|
|
19
59
|
export default plugin;
|
package/dist/src/bot.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
|
+
import { setCachedContext } from "./steer-injector.js";
|
|
2
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
3
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
|
|
4
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
|
|
4
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
5
6
|
import { resolveXYConfig } from "./config.js";
|
|
6
|
-
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
|
|
7
|
-
import { registerSession, unregisterSession } from "./tools/session-manager.js";
|
|
7
|
+
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
8
|
+
import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
|
|
8
9
|
import { configManager } from "./utils/config-manager.js";
|
|
10
|
+
import { addPushId } from "./utils/pushid-manager.js";
|
|
11
|
+
import { getPushDataById } from "./utils/pushdata-manager.js";
|
|
12
|
+
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
9
13
|
/**
|
|
10
14
|
* Handle an incoming A2A message.
|
|
11
15
|
* This is the main entry point for message processing.
|
|
@@ -15,6 +19,8 @@ export async function handleXYMessage(params) {
|
|
|
15
19
|
const { cfg, runtime, message, accountId } = params;
|
|
16
20
|
const log = runtime?.log ?? console.log;
|
|
17
21
|
const error = runtime?.error ?? console.error;
|
|
22
|
+
// 每次收到消息时更新缓存,供 steer 注入使用
|
|
23
|
+
setCachedContext(cfg, runtime, accountId);
|
|
18
24
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
19
25
|
const core = getXYRuntime();
|
|
20
26
|
try {
|
|
@@ -56,6 +62,60 @@ export async function handleXYMessage(params) {
|
|
|
56
62
|
}
|
|
57
63
|
// Parse the A2A message (for regular messages)
|
|
58
64
|
const parsed = parseA2AMessage(message);
|
|
65
|
+
// ========== 检测 Trigger 消息 ==========
|
|
66
|
+
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
67
|
+
const triggerData = extractTriggerData(parsed.parts);
|
|
68
|
+
if (triggerData) {
|
|
69
|
+
log(`[BOT] 📌 Detected Trigger message with pushDataId: ${triggerData.pushDataId}`);
|
|
70
|
+
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
71
|
+
log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
72
|
+
try {
|
|
73
|
+
// 读取 pushData
|
|
74
|
+
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
75
|
+
if (!pushDataItem) {
|
|
76
|
+
error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
80
|
+
log(`[BOT] - pushDataId: ${pushDataItem.pushDataId}`);
|
|
81
|
+
log(`[BOT] - time: ${pushDataItem.time}`);
|
|
82
|
+
log(`[BOT] - content length: ${pushDataItem.dataDetail.length} chars`);
|
|
83
|
+
const config = resolveXYConfig(cfg);
|
|
84
|
+
// 直接发送响应(final=true,不走 openclaw 流程)
|
|
85
|
+
await sendA2AResponse({
|
|
86
|
+
config,
|
|
87
|
+
sessionId: parsed.sessionId,
|
|
88
|
+
taskId: parsed.taskId,
|
|
89
|
+
messageId: parsed.messageId,
|
|
90
|
+
text: pushDataItem.dataDetail,
|
|
91
|
+
append: false,
|
|
92
|
+
final: true,
|
|
93
|
+
});
|
|
94
|
+
log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
|
|
95
|
+
return; // 提前返回,不继续处理
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
error(`[BOT] ❌ Failed to handle Trigger message:`, err);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ========================================
|
|
103
|
+
// 🔑 检测steer模式和是否是第二条消息
|
|
104
|
+
const isSteerMode = cfg.messages?.queue?.mode === "steer";
|
|
105
|
+
const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
|
|
106
|
+
if (isSecondMessage) {
|
|
107
|
+
log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
|
|
108
|
+
log(`[BOT] - Session: ${parsed.sessionId}`);
|
|
109
|
+
log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
|
|
110
|
+
}
|
|
111
|
+
// 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
|
|
112
|
+
const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
|
|
113
|
+
);
|
|
114
|
+
// 🔑 如果是第一条消息,锁定taskId防止被过早清理
|
|
115
|
+
if (!isUpdate) {
|
|
116
|
+
lockTaskId(parsed.sessionId);
|
|
117
|
+
log(`[BOT] 🔒 Locked taskId for first message`);
|
|
118
|
+
}
|
|
59
119
|
// Extract and update push_id if present
|
|
60
120
|
const pushId = extractPushId(parsed.parts);
|
|
61
121
|
if (pushId) {
|
|
@@ -64,6 +124,10 @@ export async function handleXYMessage(params) {
|
|
|
64
124
|
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
65
125
|
log(`[BOT] - Full push_id: ${pushId}`);
|
|
66
126
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
127
|
+
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
128
|
+
addPushId(pushId).catch((err) => {
|
|
129
|
+
error(`[BOT] Failed to persist pushId:`, err);
|
|
130
|
+
});
|
|
67
131
|
}
|
|
68
132
|
else {
|
|
69
133
|
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
@@ -83,11 +147,12 @@ export async function handleXYMessage(params) {
|
|
|
83
147
|
},
|
|
84
148
|
});
|
|
85
149
|
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
86
|
-
//
|
|
150
|
+
// 🔑 注册session(带引用计数)
|
|
87
151
|
log(`[BOT] 📝 About to register session for tools...`);
|
|
88
152
|
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
89
153
|
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
90
154
|
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
155
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
91
156
|
registerSession(route.sessionKey, {
|
|
92
157
|
config,
|
|
93
158
|
sessionId: parsed.sessionId,
|
|
@@ -96,14 +161,14 @@ export async function handleXYMessage(params) {
|
|
|
96
161
|
agentId: route.accountId,
|
|
97
162
|
});
|
|
98
163
|
log(`[BOT] ✅ Session registered for tools`);
|
|
99
|
-
//
|
|
164
|
+
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
100
165
|
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
101
166
|
void sendStatusUpdate({
|
|
102
167
|
config,
|
|
103
168
|
sessionId: parsed.sessionId,
|
|
104
169
|
taskId: parsed.taskId,
|
|
105
170
|
messageId: parsed.messageId,
|
|
106
|
-
text: "
|
|
171
|
+
text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍候~",
|
|
107
172
|
state: "working",
|
|
108
173
|
}).catch((err) => {
|
|
109
174
|
error(`Failed to send initial status update:`, err);
|
|
@@ -111,10 +176,9 @@ export async function handleXYMessage(params) {
|
|
|
111
176
|
// Extract text and files from parts
|
|
112
177
|
const text = extractTextFromParts(parsed.parts);
|
|
113
178
|
const fileParts = extractFileParts(parsed.parts);
|
|
114
|
-
// Download files
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const mediaPayload = buildXYMediaPayload(mediaList);
|
|
179
|
+
// Download files to local disk
|
|
180
|
+
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
181
|
+
const mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
118
182
|
// Resolve envelope format options (following feishu pattern)
|
|
119
183
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
120
184
|
// Build message body with speaker prefix (following feishu pattern)
|
|
@@ -155,51 +219,66 @@ export async function handleXYMessage(params) {
|
|
|
155
219
|
ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
|
|
156
220
|
...mediaPayload,
|
|
157
221
|
});
|
|
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}`);
|
|
222
|
+
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
223
|
+
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
224
|
+
log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
|
|
225
|
+
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
226
|
+
log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
|
|
172
227
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
173
228
|
cfg,
|
|
174
229
|
runtime,
|
|
175
230
|
sessionId: parsed.sessionId,
|
|
176
231
|
taskId: parsed.taskId,
|
|
177
232
|
messageId: parsed.messageId,
|
|
178
|
-
accountId: route.accountId,
|
|
233
|
+
accountId: route.accountId,
|
|
234
|
+
isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
|
|
179
235
|
});
|
|
180
236
|
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
237
|
+
// 🔑 只有第一条消息启动状态定时器
|
|
238
|
+
// 第二条消息会很快返回,不需要定时器
|
|
239
|
+
if (!isSecondMessage) {
|
|
240
|
+
startStatusInterval();
|
|
241
|
+
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
|
|
245
|
+
}
|
|
184
246
|
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
185
247
|
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
186
248
|
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
249
|
+
// Build session context for AsyncLocalStorage
|
|
250
|
+
const sessionContext = {
|
|
251
|
+
config,
|
|
252
|
+
sessionId: parsed.sessionId,
|
|
253
|
+
taskId: parsed.taskId,
|
|
254
|
+
messageId: parsed.messageId,
|
|
255
|
+
agentId: route.accountId,
|
|
256
|
+
};
|
|
187
257
|
await core.channel.reply.withReplyDispatcher({
|
|
188
258
|
dispatcher,
|
|
189
259
|
onSettled: () => {
|
|
190
260
|
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
191
|
-
log(`[BOT] -
|
|
261
|
+
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
192
262
|
markDispatchIdle();
|
|
193
|
-
//
|
|
263
|
+
// 🔑 减少引用计数
|
|
264
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
265
|
+
// 🔑 如果是第一条消息完成,解锁
|
|
266
|
+
if (!isSecondMessage) {
|
|
267
|
+
unlockTaskId(parsed.sessionId);
|
|
268
|
+
log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
|
|
269
|
+
}
|
|
270
|
+
// 减少session引用计数
|
|
194
271
|
unregisterSession(route.sessionKey);
|
|
195
|
-
log(`[BOT] ✅
|
|
272
|
+
log(`[BOT] ✅ Cleanup completed`);
|
|
196
273
|
},
|
|
197
|
-
run: () =>
|
|
274
|
+
run: () =>
|
|
275
|
+
// 🔐 Use AsyncLocalStorage to provide session context to tools
|
|
276
|
+
runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
|
|
198
277
|
ctx: ctxPayload,
|
|
199
278
|
cfg,
|
|
200
279
|
dispatcher,
|
|
201
280
|
replyOptions,
|
|
202
|
-
}),
|
|
281
|
+
})),
|
|
203
282
|
});
|
|
204
283
|
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
205
284
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
@@ -209,25 +288,28 @@ export async function handleXYMessage(params) {
|
|
|
209
288
|
error("Failed to handle XY message:", err);
|
|
210
289
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
211
290
|
log(`[BOT] ❌ Error occurred, attempting cleanup...`);
|
|
212
|
-
//
|
|
291
|
+
// 🔑 错误时也要清理taskId和session
|
|
213
292
|
try {
|
|
214
|
-
const core = getXYRuntime();
|
|
215
293
|
const params = message.params;
|
|
216
294
|
const sessionId = params?.sessionId;
|
|
217
295
|
if (sessionId) {
|
|
218
|
-
log(`[BOT] 🧹 Cleaning up
|
|
296
|
+
log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
|
|
297
|
+
// 清理 taskId
|
|
298
|
+
decrementTaskIdRef(sessionId);
|
|
299
|
+
unlockTaskId(sessionId);
|
|
300
|
+
// 清理 session
|
|
301
|
+
const core = getXYRuntime();
|
|
219
302
|
const route = core.channel.routing.resolveAgentRoute({
|
|
220
303
|
cfg,
|
|
221
304
|
channel: "xiaoyi-channel",
|
|
222
305
|
accountId,
|
|
223
306
|
peer: {
|
|
224
307
|
kind: "direct",
|
|
225
|
-
id: sessionId,
|
|
308
|
+
id: sessionId,
|
|
226
309
|
},
|
|
227
310
|
});
|
|
228
|
-
log(`[BOT] - Unregistering session: ${route.sessionKey}`);
|
|
229
311
|
unregisterSession(route.sessionKey);
|
|
230
|
-
log(`[BOT] ✅
|
|
312
|
+
log(`[BOT] ✅ Cleanup completed after error`);
|
|
231
313
|
}
|
|
232
314
|
}
|
|
233
315
|
catch (cleanupErr) {
|
|
@@ -240,6 +322,8 @@ export async function handleXYMessage(params) {
|
|
|
240
322
|
/**
|
|
241
323
|
* Build media payload for inbound context.
|
|
242
324
|
* Following feishu pattern: buildFeishuMediaPayload().
|
|
325
|
+
*
|
|
326
|
+
* @param mediaList - Downloaded files with local paths
|
|
243
327
|
*/
|
|
244
328
|
function buildXYMediaPayload(mediaList) {
|
|
245
329
|
const first = mediaList[0];
|
|
@@ -248,9 +332,7 @@ function buildXYMediaPayload(mediaList) {
|
|
|
248
332
|
return {
|
|
249
333
|
MediaPath: first?.path,
|
|
250
334
|
MediaType: first?.mimeType,
|
|
251
|
-
MediaUrl: first?.path,
|
|
252
335
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
253
|
-
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
254
336
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
255
337
|
};
|
|
256
338
|
}
|
package/dist/src/channel.js
CHANGED
|
@@ -11,6 +11,20 @@ import { searchCalendarTool } from "./tools/search-calendar-tool.js";
|
|
|
11
11
|
import { searchContactTool } from "./tools/search-contact-tool.js";
|
|
12
12
|
import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
|
|
13
13
|
import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
|
|
14
|
+
import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
|
|
15
|
+
import { callPhoneTool } from "./tools/call-phone-tool.js";
|
|
16
|
+
import { searchMessageTool } from "./tools/search-message-tool.js";
|
|
17
|
+
import { sendMessageTool } from "./tools/send-message-tool.js";
|
|
18
|
+
import { searchFileTool } from "./tools/search-file-tool.js";
|
|
19
|
+
import { uploadFileTool } from "./tools/upload-file-tool.js";
|
|
20
|
+
import { createAlarmTool } from "./tools/create-alarm-tool.js";
|
|
21
|
+
import { searchAlarmTool } from "./tools/search-alarm-tool.js";
|
|
22
|
+
import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
|
|
23
|
+
import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
|
|
24
|
+
import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
25
|
+
import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
|
|
26
|
+
import { viewPushResultTool } from "./tools/view-push-result-tool.js";
|
|
27
|
+
import { imageReadingTool } from "./tools/image-reading-tool.js";
|
|
14
28
|
/**
|
|
15
29
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
16
30
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -50,7 +64,7 @@ export const xyPlugin = {
|
|
|
50
64
|
},
|
|
51
65
|
outbound: xyOutbound,
|
|
52
66
|
onboarding: xyOnboardingAdapter,
|
|
53
|
-
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool],
|
|
67
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, xiaoyiCollectionTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool],
|
|
54
68
|
messaging: {
|
|
55
69
|
normalizeTarget: (raw) => {
|
|
56
70
|
const trimmed = raw.trim();
|
|
@@ -77,10 +91,9 @@ export const xyPlugin = {
|
|
|
77
91
|
const account = resolveXYConfig(context.cfg);
|
|
78
92
|
context.setStatus?.({
|
|
79
93
|
accountId: context.accountId,
|
|
80
|
-
|
|
81
|
-
wsUrl2: account.wsUrl2,
|
|
94
|
+
wsUrl: account.wsUrl,
|
|
82
95
|
});
|
|
83
|
-
context.log?.info(`[${context.accountId}] starting xiaoyi channel (
|
|
96
|
+
context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
|
|
84
97
|
return monitorXYProvider({
|
|
85
98
|
config: context.cfg,
|
|
86
99
|
runtime: context.runtime,
|
package/dist/src/client.js
CHANGED
|
@@ -89,29 +89,16 @@ export function diagnoseAllManagers() {
|
|
|
89
89
|
console.log(`📌 Manager: ${key}`);
|
|
90
90
|
console.log(` Shutting down: ${diag.isShuttingDown}`);
|
|
91
91
|
console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
92
|
-
//
|
|
93
|
-
console.log(` 🔌
|
|
94
|
-
console.log(` - Exists: ${diag.
|
|
95
|
-
console.log(` - ReadyState: ${diag.
|
|
96
|
-
console.log(` - State connected/ready: ${diag.
|
|
97
|
-
console.log(` - Reconnect attempts: ${diag.
|
|
98
|
-
console.log(` - Listeners on WebSocket: ${diag.
|
|
99
|
-
console.log(` - Heartbeat active: ${diag.
|
|
100
|
-
console.log(` - Has reconnect timer: ${diag.
|
|
101
|
-
if (diag.
|
|
102
|
-
console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
103
|
-
orphanCount++;
|
|
104
|
-
}
|
|
105
|
-
// Server 2
|
|
106
|
-
console.log(` 🔌 Server2:`);
|
|
107
|
-
console.log(` - Exists: ${diag.server2.exists}`);
|
|
108
|
-
console.log(` - ReadyState: ${diag.server2.readyState}`);
|
|
109
|
-
console.log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
|
|
110
|
-
console.log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
|
|
111
|
-
console.log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
|
|
112
|
-
console.log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
|
|
113
|
-
console.log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
|
|
114
|
-
if (diag.server2.isOrphan) {
|
|
92
|
+
// Connection
|
|
93
|
+
console.log(` 🔌 Connection:`);
|
|
94
|
+
console.log(` - Exists: ${diag.connection.exists}`);
|
|
95
|
+
console.log(` - ReadyState: ${diag.connection.readyState}`);
|
|
96
|
+
console.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
|
|
97
|
+
console.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
|
|
98
|
+
console.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
|
|
99
|
+
console.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
|
|
100
|
+
console.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
|
|
101
|
+
if (diag.connection.isOrphan) {
|
|
115
102
|
console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
116
103
|
orphanCount++;
|
|
117
104
|
}
|
|
@@ -134,7 +121,7 @@ export function cleanupOrphanConnections() {
|
|
|
134
121
|
let cleanedCount = 0;
|
|
135
122
|
wsManagerCache.forEach((manager, key) => {
|
|
136
123
|
const diag = manager.getConnectionDiagnostics();
|
|
137
|
-
if (diag.
|
|
124
|
+
if (diag.connection.isOrphan) {
|
|
138
125
|
console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
139
126
|
manager.disconnect();
|
|
140
127
|
cleanedCount++;
|
package/dist/src/config.js
CHANGED
|
@@ -17,8 +17,8 @@ export function resolveXYConfig(cfg) {
|
|
|
17
17
|
// Return configuration with defaults
|
|
18
18
|
return {
|
|
19
19
|
enabled: xyConfig.enabled ?? false,
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// ✅ 兼容旧配置:优先使用 wsUrl,然后 wsUrl2(wsUrl1 被忽略)
|
|
21
|
+
wsUrl: xyConfig.wsUrl ?? xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
|
|
22
22
|
apiKey: xyConfig.apiKey,
|
|
23
23
|
uid: xyConfig.uid,
|
|
24
24
|
agentId: xyConfig.agentId,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// CSPL API 请求模块
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { getCsplConfig } from "./config.js";
|
|
6
|
+
import { DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST } from "./constants.js";
|
|
7
|
+
function generateTraceId() {
|
|
8
|
+
return randomBytes(16).toString("hex");
|
|
9
|
+
}
|
|
10
|
+
function buildHeaders(config) {
|
|
11
|
+
return {
|
|
12
|
+
"x-hag-trace-id": generateTraceId(),
|
|
13
|
+
"x-uid": config.uid,
|
|
14
|
+
"x-api-key": config.apiKey,
|
|
15
|
+
"x-request-from": config.requestFrom,
|
|
16
|
+
"x-skill-id": config.skillId,
|
|
17
|
+
"content-type": "application/json",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildRequestOptions(url, headers, timeout) {
|
|
21
|
+
const urlObj = new URL(url);
|
|
22
|
+
return {
|
|
23
|
+
hostname: urlObj.hostname,
|
|
24
|
+
port: urlObj.port || DEFAULT_HTTP_PORT,
|
|
25
|
+
path: urlObj.pathname,
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: headers,
|
|
28
|
+
timeout,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function parseResponse(data) {
|
|
32
|
+
if (!data?.trim())
|
|
33
|
+
throw new Error("[CSPL] API response is empty");
|
|
34
|
+
const json = JSON.parse(data);
|
|
35
|
+
if (json.retCode && json.retCode !== "0") {
|
|
36
|
+
throw new Error(`[CSPL] API error: ${json.retMsg || "unknown"}`);
|
|
37
|
+
}
|
|
38
|
+
if (!json.retCode && json.code) {
|
|
39
|
+
throw new Error(`[CSPL] Backend error: ${json.desc || "unknown"}`);
|
|
40
|
+
}
|
|
41
|
+
return json;
|
|
42
|
+
}
|
|
43
|
+
export async function callCsplApi(questionText, cfg) {
|
|
44
|
+
const config = getCsplConfig(cfg);
|
|
45
|
+
const headers = buildHeaders(config);
|
|
46
|
+
const payload = {
|
|
47
|
+
questionText,
|
|
48
|
+
textSource: config.textSource,
|
|
49
|
+
action: config.action,
|
|
50
|
+
};
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
|
|
53
|
+
const req = https.request(options, (res) => {
|
|
54
|
+
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
55
|
+
reject(new Error(`[CSPL] HTTP error: ${res.statusCode}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let data = "";
|
|
59
|
+
res.on("data", (chunk) => {
|
|
60
|
+
data += chunk;
|
|
61
|
+
});
|
|
62
|
+
res.on("end", () => {
|
|
63
|
+
try {
|
|
64
|
+
resolve(parseResponse(data));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
reject(e);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
req.on("error", reject);
|
|
72
|
+
req.on("timeout", () => {
|
|
73
|
+
req.destroy();
|
|
74
|
+
reject(new Error("[CSPL] Request timeout"));
|
|
75
|
+
});
|
|
76
|
+
req.write(JSON.stringify(payload));
|
|
77
|
+
req.end();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
export interface ApiConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
timeout: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CsplConfig {
|
|
7
|
+
api: ApiConfig;
|
|
8
|
+
uid: string;
|
|
9
|
+
apiKey: string;
|
|
10
|
+
skillId: string;
|
|
11
|
+
requestFrom: string;
|
|
12
|
+
textSource: string;
|
|
13
|
+
action: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
17
|
+
* serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
|
|
18
|
+
*/
|
|
19
|
+
export declare function getCsplConfig(cfg: ClawdbotConfig): CsplConfig;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// CSPL Hook 配置管理
|
|
2
|
+
// uid 和 apiKey 复用 XYChannelConfig,skillId 写死在常量中
|
|
3
|
+
import { resolveXYConfig } from "../config.js";
|
|
4
|
+
import { CSPL_STATIC_CONFIG, API_URL_SUFFIX, ENV_FILE_PATH } from "./constants.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
let cachedConfig = null;
|
|
8
|
+
function readServiceUrl() {
|
|
9
|
+
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
10
|
+
throw new Error(`[CSPL] Environment file not found: ${ENV_FILE_PATH}`);
|
|
11
|
+
}
|
|
12
|
+
const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
|
|
13
|
+
for (const line of envData.split("\n")) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
16
|
+
continue;
|
|
17
|
+
const eqIdx = trimmed.indexOf("=");
|
|
18
|
+
if (eqIdx === -1)
|
|
19
|
+
continue;
|
|
20
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
21
|
+
const value = trimmed.substring(eqIdx + 1).trim();
|
|
22
|
+
if (key === "SERVICE_URL" && value)
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("[CSPL] Missing SERVICE_URL in env file");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
29
|
+
* serviceUrl 从 .xiaoyienv 文件读取,skillId 写死在常量中。
|
|
30
|
+
*/
|
|
31
|
+
export function getCsplConfig(cfg) {
|
|
32
|
+
if (cachedConfig)
|
|
33
|
+
return cachedConfig;
|
|
34
|
+
const xyConfig = resolveXYConfig(cfg);
|
|
35
|
+
const serviceUrl = readServiceUrl();
|
|
36
|
+
cachedConfig = {
|
|
37
|
+
api: {
|
|
38
|
+
url: `${serviceUrl}${API_URL_SUFFIX}`,
|
|
39
|
+
timeout: CSPL_STATIC_CONFIG.api.timeout,
|
|
40
|
+
},
|
|
41
|
+
uid: xyConfig.uid,
|
|
42
|
+
apiKey: xyConfig.apiKey,
|
|
43
|
+
skillId: CSPL_STATIC_CONFIG.skillId,
|
|
44
|
+
requestFrom: CSPL_STATIC_CONFIG.requestFrom,
|
|
45
|
+
textSource: CSPL_STATIC_CONFIG.textSource,
|
|
46
|
+
action: CSPL_STATIC_CONFIG.action,
|
|
47
|
+
};
|
|
48
|
+
logger.log("[CSPL] Config loaded (uid/apiKey from XYChannelConfig)");
|
|
49
|
+
return cachedConfig;
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface HttpHeaders {
|
|
2
|
+
"x-hag-trace-id": string;
|
|
3
|
+
"x-uid": string;
|
|
4
|
+
"x-api-key": string;
|
|
5
|
+
"x-request-from": string;
|
|
6
|
+
"x-skill-id": string;
|
|
7
|
+
"content-type": string;
|
|
8
|
+
}
|
|
9
|
+
export interface ApiPayload {
|
|
10
|
+
questionText: string;
|
|
11
|
+
textSource: string;
|
|
12
|
+
action: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ApiResponse {
|
|
15
|
+
data?: {
|
|
16
|
+
securityResult?: string;
|
|
17
|
+
};
|
|
18
|
+
retCode?: string;
|
|
19
|
+
retMsg?: string;
|
|
20
|
+
code?: string;
|
|
21
|
+
desc?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
export declare const MIN_TEXT_LENGTH = 0;
|
|
25
|
+
export declare const MAX_TEXT_LENGTH = 4096;
|
|
26
|
+
export declare const MAX_TOTAL_LENGTH = 40960;
|
|
27
|
+
export declare const regex: RegExp;
|
|
28
|
+
export declare const DEFAULT_HTTP_PORT = 80;
|
|
29
|
+
export declare const HTTP_STATUS_BAD_REQUEST = 400;
|
|
30
|
+
export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
31
|
+
export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
32
|
+
export declare const REQUIRED_ENV_VARS: string[];
|
|
33
|
+
export declare const ALLOWED_TOOLS: string[];
|
|
34
|
+
export declare const CSPL_STATIC_CONFIG: {
|
|
35
|
+
readonly api: {
|
|
36
|
+
readonly timeout: 5000;
|
|
37
|
+
};
|
|
38
|
+
readonly skillId: "skill-scope";
|
|
39
|
+
readonly requestFrom: "openclaw";
|
|
40
|
+
readonly textSource: "question";
|
|
41
|
+
readonly action: "TOOL_OUTPUT_SCAN";
|
|
42
|
+
};
|
|
43
|
+
export declare const STEER_ABORT_MESSAGE = "\u5F53\u524D\u884C\u4E3A\u5B58\u5728\u5B89\u5168\u9690\u60A3\uFF0C\u7EC8\u6B62\u6240\u6709\u64CD\u4F5C";
|