@ynhcj/xiaoyi-channel 0.0.30-beta → 0.0.30-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.d.ts +0 -2
- package/dist/index.js +42 -2
- package/dist/src/bot.js +55 -25
- package/dist/src/channel.js +24 -8
- package/dist/src/client.js +11 -27
- package/dist/src/config.js +2 -2
- package/dist/src/cspl/call-api.d.ts +3 -0
- package/dist/src/cspl/call-api.js +107 -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 +49 -25
- package/dist/src/heartbeat.js +0 -4
- package/dist/src/monitor.js +12 -0
- package/dist/src/onboarding.d.ts +3 -4
- package/dist/src/onboarding.js +2 -2
- package/dist/src/outbound.d.ts +2 -1
- package/dist/src/outbound.js +122 -88
- 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.js +39 -5
- package/dist/src/steer-injector.d.ts +16 -0
- package/dist/src/steer-injector.js +74 -0
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/calendar-tool.js +5 -38
- package/dist/src/tools/call-phone-tool.js +1 -42
- package/dist/src/tools/create-alarm-tool.js +9 -104
- package/dist/src/tools/delete-alarm-tool.js +5 -69
- package/dist/src/tools/image-reading-tool.d.ts +5 -0
- package/dist/src/tools/image-reading-tool.js +328 -0
- package/dist/src/tools/location-tool.js +6 -40
- package/dist/src/tools/modify-alarm-tool.js +8 -117
- package/dist/src/tools/modify-note-tool.js +3 -41
- package/dist/src/tools/note-tool.js +31 -22
- package/dist/src/tools/search-alarm-tool.js +14 -118
- package/dist/src/tools/search-calendar-tool.js +8 -82
- package/dist/src/tools/search-contact-tool.js +2 -55
- package/dist/src/tools/search-file-tool.js +4 -61
- package/dist/src/tools/search-message-tool.js +2 -59
- package/dist/src/tools/search-note-tool.js +4 -22
- package/dist/src/tools/search-photo-gallery-tool.js +38 -59
- package/dist/src/tools/send-file-to-user-tool.js +0 -39
- package/dist/src/tools/send-message-tool.js +5 -56
- package/dist/src/tools/session-manager.js +0 -45
- package/dist/src/tools/upload-file-tool.js +0 -49
- package/dist/src/tools/upload-photo-tool.js +0 -42
- package/dist/src/tools/view-push-result-tool.d.ts +5 -0
- package/dist/src/tools/view-push-result-tool.js +107 -0
- package/dist/src/tools/xiaoyi-collection-tool.d.ts +5 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +112 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
- 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 +25 -31
- package/dist/src/websocket.js +233 -271
- package/package.json +2 -2
- package/dist/src/tools/search-photo-tool.d.ts +0 -9
- package/dist/src/tools/search-photo-tool.js +0 -270
- package/dist/src/utils/session.d.ts +0 -34
- package/dist/src/utils/session.js +0 -50
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { xyPlugin } from "./src/channel.js";
|
|
3
2
|
/**
|
|
4
3
|
* Xiaoyi Channel Plugin Entry Point.
|
|
5
4
|
* Exports the plugin for OpenClaw to load.
|
|
@@ -13,4 +12,3 @@ declare const plugin: {
|
|
|
13
12
|
register(api: OpenClawPluginApi): void;
|
|
14
13
|
};
|
|
15
14
|
export default plugin;
|
|
16
|
-
export { xyPlugin };
|
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,8 +18,44 @@ 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
|
+
console.log(`[CSPL] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
28
|
+
try {
|
|
29
|
+
const resultText = extractResultText(event, event.toolName);
|
|
30
|
+
const resultLength = resultText.length;
|
|
31
|
+
if (resultLength <= MIN_TEXT_LENGTH || resultLength > MAX_TOTAL_LENGTH) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
|
|
35
|
+
const questionText = {
|
|
36
|
+
tool: event.toolName,
|
|
37
|
+
output: [{ content: "" }],
|
|
38
|
+
};
|
|
39
|
+
const originText = processText(resultText);
|
|
40
|
+
questionText.output[0].content = originText;
|
|
41
|
+
let finalJson = JSON.stringify(questionText);
|
|
42
|
+
if (finalJson.length > MAX_TEXT_LENGTH) {
|
|
43
|
+
const diff = finalJson.length - MAX_TEXT_LENGTH;
|
|
44
|
+
const { text: trimmed } = validateAndTruncateText(originText, MAX_TEXT_LENGTH - diff);
|
|
45
|
+
questionText.output[0].content = trimmed;
|
|
46
|
+
finalJson = JSON.stringify(questionText);
|
|
47
|
+
}
|
|
48
|
+
const response = await callCsplApi(finalJson, api.config);
|
|
49
|
+
const result = parseSecurityResult(response);
|
|
50
|
+
console.log(`[CSPL] Security result: status=${result.status}`);
|
|
51
|
+
if (result.status === "REJECT") {
|
|
52
|
+
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
api.logger.error(`[CSPL] after_tool_call error: ${err}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
17
59
|
},
|
|
18
60
|
};
|
|
19
61
|
export default plugin;
|
|
20
|
-
// Also export the plugin directly for testing
|
|
21
|
-
export { xyPlugin };
|
package/dist/src/bot.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
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 { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
7
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";
|
|
9
12
|
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
10
13
|
/**
|
|
11
14
|
* Handle an incoming A2A message.
|
|
@@ -16,6 +19,8 @@ export async function handleXYMessage(params) {
|
|
|
16
19
|
const { cfg, runtime, message, accountId } = params;
|
|
17
20
|
const log = runtime?.log ?? console.log;
|
|
18
21
|
const error = runtime?.error ?? console.error;
|
|
22
|
+
// 每次收到消息时更新缓存,供 steer 注入使用
|
|
23
|
+
setCachedContext(cfg, runtime, accountId);
|
|
19
24
|
// Get runtime (already validated in monitor.ts, but get reference for use)
|
|
20
25
|
const core = getXYRuntime();
|
|
21
26
|
try {
|
|
@@ -57,6 +62,44 @@ export async function handleXYMessage(params) {
|
|
|
57
62
|
}
|
|
58
63
|
// Parse the A2A message (for regular messages)
|
|
59
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
|
+
// ========================================
|
|
60
103
|
// 🔑 检测steer模式和是否是第二条消息
|
|
61
104
|
const isSteerMode = cfg.messages?.queue?.mode === "steer";
|
|
62
105
|
const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
|
|
@@ -81,6 +124,10 @@ export async function handleXYMessage(params) {
|
|
|
81
124
|
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
82
125
|
log(`[BOT] - Full push_id: ${pushId}`);
|
|
83
126
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
127
|
+
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
128
|
+
addPushId(pushId).catch((err) => {
|
|
129
|
+
error(`[BOT] Failed to persist pushId:`, err);
|
|
130
|
+
});
|
|
84
131
|
}
|
|
85
132
|
else {
|
|
86
133
|
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
@@ -121,7 +168,7 @@ export async function handleXYMessage(params) {
|
|
|
121
168
|
sessionId: parsed.sessionId,
|
|
122
169
|
taskId: parsed.taskId,
|
|
123
170
|
messageId: parsed.messageId,
|
|
124
|
-
text: isSecondMessage ? "新消息已接收,正在处理..." : "
|
|
171
|
+
text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍候~",
|
|
125
172
|
state: "working",
|
|
126
173
|
}).catch((err) => {
|
|
127
174
|
error(`Failed to send initial status update:`, err);
|
|
@@ -129,10 +176,9 @@ export async function handleXYMessage(params) {
|
|
|
129
176
|
// Extract text and files from parts
|
|
130
177
|
const text = extractTextFromParts(parsed.parts);
|
|
131
178
|
const fileParts = extractFileParts(parsed.parts);
|
|
132
|
-
// Download files
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
const mediaPayload = buildXYMediaPayload(mediaList);
|
|
179
|
+
// Download files to local disk
|
|
180
|
+
const downloadedFiles = await downloadFilesFromParts(fileParts);
|
|
181
|
+
const mediaPayload = buildXYMediaPayload(downloadedFiles);
|
|
136
182
|
// Resolve envelope format options (following feishu pattern)
|
|
137
183
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
138
184
|
// Build message body with speaker prefix (following feishu pattern)
|
|
@@ -276,6 +322,8 @@ export async function handleXYMessage(params) {
|
|
|
276
322
|
/**
|
|
277
323
|
* Build media payload for inbound context.
|
|
278
324
|
* Following feishu pattern: buildFeishuMediaPayload().
|
|
325
|
+
*
|
|
326
|
+
* @param mediaList - Downloaded files with local paths
|
|
279
327
|
*/
|
|
280
328
|
function buildXYMediaPayload(mediaList) {
|
|
281
329
|
const first = mediaList[0];
|
|
@@ -284,25 +332,7 @@ function buildXYMediaPayload(mediaList) {
|
|
|
284
332
|
return {
|
|
285
333
|
MediaPath: first?.path,
|
|
286
334
|
MediaType: first?.mimeType,
|
|
287
|
-
MediaUrl: first?.path,
|
|
288
335
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
289
|
-
MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
290
336
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
291
337
|
};
|
|
292
338
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Infer OpenClaw media type from file type string.
|
|
295
|
-
*/
|
|
296
|
-
function inferMediaType(fileType) {
|
|
297
|
-
const lower = fileType.toLowerCase();
|
|
298
|
-
if (lower.includes("image") || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(lower)) {
|
|
299
|
-
return "image";
|
|
300
|
-
}
|
|
301
|
-
if (lower.includes("video") || /\.(mp4|avi|mov|mkv|webm)$/i.test(lower)) {
|
|
302
|
-
return "video";
|
|
303
|
-
}
|
|
304
|
-
if (lower.includes("audio") || /\.(mp3|wav|ogg|m4a)$/i.test(lower)) {
|
|
305
|
-
return "audio";
|
|
306
|
-
}
|
|
307
|
-
return "file";
|
|
308
|
-
}
|
package/dist/src/channel.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
import { resolveXYConfig, listXYAccountIds, getDefaultXYAccountId } from "./config.js";
|
|
2
2
|
import { xyConfigSchema } from "./config-schema.js";
|
|
3
3
|
import { xyOutbound } from "./outbound.js";
|
|
4
|
-
import { xyOnboardingAdapter } from "./onboarding.js";
|
|
5
4
|
import { locationTool } from "./tools/location-tool.js";
|
|
6
5
|
import { noteTool } from "./tools/note-tool.js";
|
|
7
6
|
import { searchNoteTool } from "./tools/search-note-tool.js";
|
|
8
7
|
import { modifyNoteTool } from "./tools/modify-note-tool.js";
|
|
9
8
|
import { calendarTool } from "./tools/calendar-tool.js";
|
|
10
9
|
import { searchCalendarTool } from "./tools/search-calendar-tool.js";
|
|
11
|
-
|
|
10
|
+
import { searchContactTool } from "./tools/search-contact-tool.js";
|
|
12
11
|
import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
|
|
13
12
|
import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
|
|
14
13
|
import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
|
|
15
14
|
import { callPhoneTool } from "./tools/call-phone-tool.js";
|
|
16
15
|
import { searchMessageTool } from "./tools/search-message-tool.js";
|
|
16
|
+
import { sendMessageTool } from "./tools/send-message-tool.js";
|
|
17
17
|
import { searchFileTool } from "./tools/search-file-tool.js";
|
|
18
18
|
import { uploadFileTool } from "./tools/upload-file-tool.js";
|
|
19
19
|
import { createAlarmTool } from "./tools/create-alarm-tool.js";
|
|
20
20
|
import { searchAlarmTool } from "./tools/search-alarm-tool.js";
|
|
21
21
|
import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
|
|
22
22
|
import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
|
|
23
|
-
import { sendMessageTool } from "./tools/send-message-tool.js";
|
|
24
23
|
import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
24
|
+
import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
|
|
25
|
+
import { viewPushResultTool } from "./tools/view-push-result-tool.js";
|
|
26
|
+
import { imageReadingTool } from "./tools/image-reading-tool.js";
|
|
25
27
|
/**
|
|
26
28
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
27
29
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -60,8 +62,7 @@ export const xyPlugin = {
|
|
|
60
62
|
schema: xyConfigSchema,
|
|
61
63
|
},
|
|
62
64
|
outbound: xyOutbound,
|
|
63
|
-
|
|
64
|
-
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool, sendFileToUserTool], // searchContactTool 已暂时禁用
|
|
65
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, xiaoyiCollectionTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool],
|
|
65
66
|
messaging: {
|
|
66
67
|
normalizeTarget: (raw) => {
|
|
67
68
|
const trimmed = raw.trim();
|
|
@@ -78,6 +79,22 @@ export const xyPlugin = {
|
|
|
78
79
|
hint: "<sessionId>",
|
|
79
80
|
},
|
|
80
81
|
},
|
|
82
|
+
bindings: {
|
|
83
|
+
compileConfiguredBinding: ({ conversationId }) => {
|
|
84
|
+
const sessionId = conversationId.trim();
|
|
85
|
+
if (!sessionId)
|
|
86
|
+
return null;
|
|
87
|
+
return {
|
|
88
|
+
conversationId: sessionId,
|
|
89
|
+
parentConversationId: undefined,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
matchInboundConversation: ({ compiledBinding, conversationId }) => {
|
|
93
|
+
return compiledBinding.conversationId === conversationId
|
|
94
|
+
? { conversationId, matchPriority: 2 }
|
|
95
|
+
: null;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
81
98
|
reload: {
|
|
82
99
|
configPrefixes: ["channels.xiaoyi-channel"],
|
|
83
100
|
},
|
|
@@ -88,10 +105,9 @@ export const xyPlugin = {
|
|
|
88
105
|
const account = resolveXYConfig(context.cfg);
|
|
89
106
|
context.setStatus?.({
|
|
90
107
|
accountId: context.accountId,
|
|
91
|
-
|
|
92
|
-
wsUrl2: account.wsUrl2,
|
|
108
|
+
wsUrl: account.wsUrl,
|
|
93
109
|
});
|
|
94
|
-
context.log?.info(`[${context.accountId}] starting xiaoyi channel (
|
|
110
|
+
context.log?.info(`[${context.accountId}] starting xiaoyi channel (wsUrl: ${account.wsUrl})`);
|
|
95
111
|
return monitorXYProvider({
|
|
96
112
|
config: context.cfg,
|
|
97
113
|
runtime: context.runtime,
|
package/dist/src/client.js
CHANGED
|
@@ -23,7 +23,6 @@ export function getXYWebSocketManager(config) {
|
|
|
23
23
|
let cached = wsManagerCache.get(cacheKey);
|
|
24
24
|
if (cached && cached.isConfigMatch(config)) {
|
|
25
25
|
const log = runtime?.log ?? console.log;
|
|
26
|
-
log(`[WS-MANAGER-CACHE] ✅ Reusing cached WebSocket manager: ${cacheKey}, total managers: ${wsManagerCache.size}`);
|
|
27
26
|
return cached;
|
|
28
27
|
}
|
|
29
28
|
// Create new manager
|
|
@@ -86,32 +85,17 @@ export function diagnoseAllManagers() {
|
|
|
86
85
|
let orphanCount = 0;
|
|
87
86
|
wsManagerCache.forEach((manager, key) => {
|
|
88
87
|
const diag = manager.getConnectionDiagnostics();
|
|
89
|
-
console.log(`📌 Manager: ${key}`);
|
|
90
|
-
console.log(` Shutting down: ${diag.isShuttingDown}`);
|
|
91
88
|
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) {
|
|
89
|
+
// Connection
|
|
90
|
+
console.log(` 🔌 Connection:`);
|
|
91
|
+
console.log(` - Exists: ${diag.connection.exists}`);
|
|
92
|
+
console.log(` - ReadyState: ${diag.connection.readyState}`);
|
|
93
|
+
console.log(` - State connected/ready: ${diag.connection.stateConnected}/${diag.connection.stateReady}`);
|
|
94
|
+
console.log(` - Reconnect attempts: ${diag.connection.reconnectAttempts}`);
|
|
95
|
+
console.log(` - Listeners on WebSocket: ${diag.connection.listenerCount}`);
|
|
96
|
+
console.log(` - Heartbeat active: ${diag.connection.heartbeatActive}`);
|
|
97
|
+
console.log(` - Has reconnect timer: ${diag.connection.hasReconnectTimer}`);
|
|
98
|
+
if (diag.connection.isOrphan) {
|
|
115
99
|
console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
|
|
116
100
|
orphanCount++;
|
|
117
101
|
}
|
|
@@ -134,7 +118,7 @@ export function cleanupOrphanConnections() {
|
|
|
134
118
|
let cleanedCount = 0;
|
|
135
119
|
wsManagerCache.forEach((manager, key) => {
|
|
136
120
|
const diag = manager.getConnectionDiagnostics();
|
|
137
|
-
if (diag.
|
|
121
|
+
if (diag.connection.isOrphan) {
|
|
138
122
|
console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
|
|
139
123
|
manager.disconnect();
|
|
140
124
|
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,107 @@
|
|
|
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
|
+
// 打印请求信息
|
|
52
|
+
console.log(`[CSPL API] ==================== 发起请求 ====================`);
|
|
53
|
+
console.log(`[CSPL API] URL: ${config.api.url}`);
|
|
54
|
+
console.log(`[CSPL API] Method: POST`);
|
|
55
|
+
console.log(`[CSPL API] Headers:`);
|
|
56
|
+
console.log(`[CSPL API] - x-hag-trace-id: ${headers["x-hag-trace-id"]}`);
|
|
57
|
+
console.log(`[CSPL API] - x-uid: ${headers["x-uid"]}`);
|
|
58
|
+
console.log(`[CSPL API] - x-api-key: ${headers["x-api-key"] ? "***" + headers["x-api-key"].slice(-8) : "undefined"}`);
|
|
59
|
+
console.log(`[CSPL API] - x-request-from: ${headers["x-request-from"]}`);
|
|
60
|
+
console.log(`[CSPL API] - x-skill-id: ${headers["x-skill-id"]}`);
|
|
61
|
+
console.log(`[CSPL API] - content-type: ${headers["content-type"]}`);
|
|
62
|
+
console.log(`[CSPL API] Body:`);
|
|
63
|
+
console.log(`[CSPL API] - questionText: ${questionText.substring(0, 100)}${questionText.length > 100 ? "..." : ""}`);
|
|
64
|
+
console.log(`[CSPL API] - textSource: ${payload.textSource}`);
|
|
65
|
+
console.log(`[CSPL API] - action: ${payload.action}`);
|
|
66
|
+
console.log(`[CSPL API] =================================================`);
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
|
|
69
|
+
const req = https.request(options, (res) => {
|
|
70
|
+
console.log(`[CSPL API] Response Status: ${res.statusCode}`);
|
|
71
|
+
console.log(`[CSPL API] Response Headers: ${JSON.stringify(res.headers)}`);
|
|
72
|
+
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
73
|
+
reject(new Error(`[CSPL] HTTP error: ${res.statusCode}`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let data = "";
|
|
77
|
+
res.on("data", (chunk) => {
|
|
78
|
+
data += chunk;
|
|
79
|
+
});
|
|
80
|
+
res.on("end", () => {
|
|
81
|
+
try {
|
|
82
|
+
const result = parseResponse(data);
|
|
83
|
+
console.log(`[CSPL API] ✅ 请求成功`);
|
|
84
|
+
console.log(`[CSPL API] Response Body: ${data.substring(0, 200)}${data.length > 200 ? "..." : ""}`);
|
|
85
|
+
console.log(`[CSPL API] =================================================`);
|
|
86
|
+
resolve(result);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
console.error(`[CSPL API] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
90
|
+
console.error(`[CSPL API] Response Body: ${data}`);
|
|
91
|
+
reject(e);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
req.on("error", (error) => {
|
|
96
|
+
console.error(`[CSPL API] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
reject(error);
|
|
98
|
+
});
|
|
99
|
+
req.on("timeout", () => {
|
|
100
|
+
console.error(`[CSPL API] ⏰ 请求超时 (${config.api.timeout}ms)`);
|
|
101
|
+
req.destroy();
|
|
102
|
+
reject(new Error("[CSPL] Request timeout"));
|
|
103
|
+
});
|
|
104
|
+
req.write(JSON.stringify(payload));
|
|
105
|
+
req.end();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -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 = 443;
|
|
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\uFF0C\u5E76\u4E14\u5728\u6700\u7EC8\u56DE\u590D\u4E2D\u8BF4\u660E\uFF0C\u8BC6\u522B\u5230\u5F53\u524D\u6D41\u7A0B\u4E2D\u5B58\u5728\u6F5C\u5728\u5B89\u5168\u98CE\u9669\uFF0C\u5DF2\u5E2E\u60A8\u4E2D\u6B62\u5F53\u524D\u6D41\u7A0B\uFF0C\u8BF7\u6E05\u7A7A\u4E0A\u4E0B\u6587\u540E\u91CD\u8BD5";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// CSPL Hook 常量与类型定义
|
|
2
|
+
export const MIN_TEXT_LENGTH = 0;
|
|
3
|
+
export const MAX_TEXT_LENGTH = 4096;
|
|
4
|
+
export const MAX_TOTAL_LENGTH = 40960;
|
|
5
|
+
export const regex = /[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:""\'\'()()\[\]【】]/;
|
|
6
|
+
export const DEFAULT_HTTP_PORT = 443;
|
|
7
|
+
export const HTTP_STATUS_BAD_REQUEST = 400;
|
|
8
|
+
export const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
9
|
+
export const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
10
|
+
export const REQUIRED_ENV_VARS = ["PERSONAL-API-KEY", "PERSONAL-UID", "SERVICE_URL"];
|
|
11
|
+
// 工具白名单
|
|
12
|
+
export const ALLOWED_TOOLS = ["web_fetch", "read", "pdf"];
|
|
13
|
+
// 静态配置(非敏感,敏感值从 ENV 文件读取)
|
|
14
|
+
export const CSPL_STATIC_CONFIG = {
|
|
15
|
+
api: { timeout: 5000 },
|
|
16
|
+
skillId: "skill-scope",
|
|
17
|
+
requestFrom: "openclaw",
|
|
18
|
+
textSource: "question",
|
|
19
|
+
action: "TOOL_OUTPUT_SCAN",
|
|
20
|
+
};
|
|
21
|
+
// Steer 注入常量
|
|
22
|
+
export const STEER_ABORT_MESSAGE = "当前行为存在安全隐患,终止所有操作,并且在最终回复中说明,识别到当前流程中存在潜在安全风险,已帮您中止当前流程,请清空上下文后重试";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function filterText(text: string): string;
|
|
2
|
+
export declare function validateAndTruncateText(text: string, maxLength: number): {
|
|
3
|
+
text: string;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function extractResultText(event: any, toolName: string): string;
|
|
7
|
+
export declare function processText(resultText: string): string;
|
|
8
|
+
export declare function parseSecurityResult(response: any): {
|
|
9
|
+
status: "ACCEPT" | "REJECT";
|
|
10
|
+
};
|