@ynhcj/xiaoyi-channel 0.0.51-beta → 0.0.51-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 +7 -6
- package/dist/src/bot.d.ts +1 -0
- package/dist/src/bot.js +83 -68
- package/dist/src/channel.js +32 -4
- package/dist/src/client.js +0 -8
- package/dist/src/cspl/call-api.js +19 -9
- package/dist/src/cspl/config.js +3 -3
- package/dist/src/cspl/constants.d.ts +2 -1
- package/dist/src/cspl/constants.js +1 -1
- package/dist/src/file-upload.js +1 -11
- package/dist/src/formatter.d.ts +2 -0
- package/dist/src/formatter.js +13 -28
- package/dist/src/heartbeat.js +0 -1
- package/dist/src/monitor.js +8 -10
- 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 +1 -19
- package/dist/src/parser.d.ts +13 -4
- package/dist/src/parser.js +23 -2
- package/dist/src/provider.d.ts +2 -0
- package/dist/src/provider.js +142 -0
- package/dist/src/push.js +0 -21
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +45 -5
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/calendar-tool.js +2 -37
- package/dist/src/tools/call-phone-tool.js +3 -60
- package/dist/src/tools/create-alarm-tool.js +6 -81
- package/dist/src/tools/delete-alarm-tool.js +3 -45
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +35 -0
- package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
- package/dist/src/tools/find-pc-devices-tool.js +98 -0
- package/dist/src/tools/image-reading-tool.js +3 -50
- package/dist/src/tools/location-tool.js +1 -32
- package/dist/src/tools/modify-alarm-tool.js +6 -84
- package/dist/src/tools/modify-note-tool.js +1 -34
- package/dist/src/tools/note-tool.js +2 -4
- package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
- package/dist/src/tools/save-file-to-phone-tool.js +170 -0
- package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
- package/dist/src/tools/search-alarm-tool.js +3 -61
- package/dist/src/tools/search-calendar-tool.js +2 -39
- package/dist/src/tools/search-contact-tool.js +0 -30
- package/dist/src/tools/search-file-tool.js +0 -33
- package/dist/src/tools/search-message-tool.js +0 -33
- package/dist/src/tools/search-note-tool.js +1 -26
- package/dist/src/tools/search-photo-gallery-tool.js +2 -31
- package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
- package/dist/src/tools/send-command-to-car-tool.js +85 -0
- package/dist/src/tools/send-file-to-user-tool.js +1 -39
- package/dist/src/tools/send-message-tool.js +1 -39
- package/dist/src/tools/session-manager.d.ts +1 -0
- package/dist/src/tools/session-manager.js +0 -45
- package/dist/src/tools/timestamp-to-utc8-tool.d.ts +12 -0
- package/dist/src/tools/timestamp-to-utc8-tool.js +104 -0
- package/dist/src/tools/upload-file-tool.js +2 -51
- package/dist/src/tools/upload-photo-tool.js +0 -42
- package/dist/src/tools/view-push-result-tool.js +0 -11
- package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-add-collection-tool.js +187 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +46 -89
- package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +0 -34
- package/dist/src/utils/pushdata-manager.d.ts +5 -0
- package/dist/src/utils/pushdata-manager.js +33 -0
- package/dist/src/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/dist/src/websocket.js +33 -11
- package/openclaw.plugin.json +1 -0
- package/package.json +3 -4
- 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,5 +1,6 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { xyPlugin } from "./src/channel.js";
|
|
3
|
+
import { xiaoyiProvider } from "./src/provider.js";
|
|
3
4
|
import { setXYRuntime } from "./src/runtime.js";
|
|
4
5
|
import { tryInjectSteer } from "./src/steer-injector.js";
|
|
5
6
|
import { callCsplApi } from "./src/cspl/call-api.js";
|
|
@@ -18,13 +19,14 @@ const plugin = {
|
|
|
18
19
|
register(api) {
|
|
19
20
|
setXYRuntime(api.runtime);
|
|
20
21
|
api.registerChannel({ plugin: xyPlugin });
|
|
21
|
-
|
|
22
|
+
api.registerProvider(xiaoyiProvider);
|
|
23
|
+
// SENTINEL HOOK after_tool_call hook: 监听工具结果,发送至安全检测 API 进行安全检测
|
|
22
24
|
// 如果响应为 REJECT,注入 steer 消息中止当前对话
|
|
23
25
|
api.on("after_tool_call", async (event, ctx) => {
|
|
24
26
|
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
|
-
console.log(`[
|
|
29
|
+
console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
28
30
|
try {
|
|
29
31
|
const resultText = extractResultText(event, event.toolName);
|
|
30
32
|
const resultLength = resultText.length;
|
|
@@ -33,6 +35,7 @@ const plugin = {
|
|
|
33
35
|
}
|
|
34
36
|
// 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
|
|
35
37
|
const questionText = {
|
|
38
|
+
subSceneID: 'TOOL_OUTPUT',
|
|
36
39
|
tool: event.toolName,
|
|
37
40
|
output: [{ content: "" }],
|
|
38
41
|
};
|
|
@@ -47,17 +50,15 @@ const plugin = {
|
|
|
47
50
|
}
|
|
48
51
|
const response = await callCsplApi(finalJson, api.config);
|
|
49
52
|
const result = parseSecurityResult(response);
|
|
50
|
-
console.log(`[
|
|
53
|
+
console.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
|
|
51
54
|
if (result.status === "REJECT") {
|
|
52
55
|
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
catch (err) {
|
|
56
|
-
api.logger.error(`[
|
|
59
|
+
api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
|
|
57
60
|
}
|
|
58
61
|
});
|
|
59
62
|
},
|
|
60
63
|
};
|
|
61
64
|
export default plugin;
|
|
62
|
-
// Also export the plugin directly for testing
|
|
63
|
-
export { xyPlugin };
|
package/dist/src/bot.d.ts
CHANGED
package/dist/src/bot.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { setCachedContext } from "./steer-injector.js";
|
|
3
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
4
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
|
|
4
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
5
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
6
6
|
import { resolveXYConfig } from "./config.js";
|
|
7
7
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
8
8
|
import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
|
|
9
9
|
import { configManager } from "./utils/config-manager.js";
|
|
10
10
|
import { addPushId } from "./utils/pushid-manager.js";
|
|
11
|
-
import { getPushDataById } from "./utils/pushdata-manager.js";
|
|
11
|
+
import { getPushDataById, getPushDataAfterTimestamp } from "./utils/pushdata-manager.js";
|
|
12
|
+
import { saveRuntimeInfo } from "./utils/runtime-manager.js";
|
|
12
13
|
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
13
14
|
/**
|
|
14
15
|
* Handle an incoming A2A message.
|
|
@@ -16,7 +17,7 @@ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActive
|
|
|
16
17
|
* Runtime is expected to be validated before calling this function.
|
|
17
18
|
*/
|
|
18
19
|
export async function handleXYMessage(params) {
|
|
19
|
-
const { cfg, runtime, message, accountId } = params;
|
|
20
|
+
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
20
21
|
const log = runtime?.log ?? console.log;
|
|
21
22
|
const error = runtime?.error ?? console.error;
|
|
22
23
|
// 每次收到消息时更新缓存,供 steer 注入使用
|
|
@@ -26,8 +27,6 @@ export async function handleXYMessage(params) {
|
|
|
26
27
|
try {
|
|
27
28
|
// Check for special messages BEFORE parsing (these have different param structures)
|
|
28
29
|
const messageMethod = message.method;
|
|
29
|
-
log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
|
|
30
|
-
log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
|
|
31
30
|
// Handle clearContext messages (params only has sessionId)
|
|
32
31
|
if (messageMethod === "clearContext" || messageMethod === "clear_context") {
|
|
33
32
|
const sessionId = message.params?.sessionId;
|
|
@@ -66,31 +65,49 @@ export async function handleXYMessage(params) {
|
|
|
66
65
|
// 如果消息中包含 Trigger 事件数据,直接返回 pushData 内容,不走正常流程
|
|
67
66
|
const triggerData = extractTriggerData(parsed.parts);
|
|
68
67
|
if (triggerData) {
|
|
69
|
-
log(`[BOT] 📌 Detected Trigger message
|
|
68
|
+
log(`[BOT] 📌 Detected Trigger message`);
|
|
70
69
|
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
71
70
|
log(`[BOT] - Task ID: ${parsed.taskId}`);
|
|
72
71
|
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
72
|
const config = resolveXYConfig(cfg);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
if ("pushDataId" in triggerData) {
|
|
74
|
+
// 单条查询:按 pushDataId 返回
|
|
75
|
+
log(`[BOT] - Type: pushDataId = ${triggerData.pushDataId}`);
|
|
76
|
+
const pushDataItem = await getPushDataById(triggerData.pushDataId);
|
|
77
|
+
if (!pushDataItem) {
|
|
78
|
+
error(`[BOT] ❌ pushData not found for ID: ${triggerData.pushDataId}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
log(`[BOT] ✅ Found pushData, sending direct response`);
|
|
82
|
+
await sendA2AResponse({
|
|
83
|
+
config,
|
|
84
|
+
sessionId: parsed.sessionId,
|
|
85
|
+
taskId: parsed.taskId,
|
|
86
|
+
messageId: parsed.messageId,
|
|
87
|
+
text: pushDataItem.dataDetail,
|
|
88
|
+
append: false,
|
|
89
|
+
final: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// 批量查询:按 timestamp 返回所有之后的 push 数据
|
|
94
|
+
log(`[BOT] - Type: timestamp = ${triggerData.timestamp}`);
|
|
95
|
+
const items = await getPushDataAfterTimestamp(triggerData.timestamp);
|
|
96
|
+
const result = items.map(item => ({
|
|
97
|
+
pushDataId: item.pushDataId,
|
|
98
|
+
pushData: item.dataDetail,
|
|
99
|
+
}));
|
|
100
|
+
log(`[BOT] ✅ Found ${result.length} pushData items after timestamp, sending response`);
|
|
101
|
+
await sendA2AResponse({
|
|
102
|
+
config,
|
|
103
|
+
sessionId: parsed.sessionId,
|
|
104
|
+
taskId: parsed.taskId,
|
|
105
|
+
messageId: parsed.messageId,
|
|
106
|
+
text: JSON.stringify(result),
|
|
107
|
+
append: false,
|
|
108
|
+
final: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
94
111
|
log(`[BOT] ✅ Trigger response sent successfully, exiting early`);
|
|
95
112
|
return; // 提前返回,不继续处理
|
|
96
113
|
}
|
|
@@ -120,9 +137,6 @@ export async function handleXYMessage(params) {
|
|
|
120
137
|
const pushId = extractPushId(parsed.parts);
|
|
121
138
|
if (pushId) {
|
|
122
139
|
log(`[BOT] 📌 Extracted push_id from user message`);
|
|
123
|
-
log(`[BOT] - Session ID: ${parsed.sessionId}`);
|
|
124
|
-
log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
|
|
125
|
-
log(`[BOT] - Full push_id: ${pushId}`);
|
|
126
140
|
configManager.updatePushId(parsed.sessionId, pushId);
|
|
127
141
|
// 持久化 pushId 到本地文件(异步,不阻塞主流程)
|
|
128
142
|
addPushId(pushId).catch((err) => {
|
|
@@ -132,6 +146,18 @@ export async function handleXYMessage(params) {
|
|
|
132
146
|
else {
|
|
133
147
|
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
134
148
|
}
|
|
149
|
+
// Extract deviceType if present (same level as push_id in systemVariables)
|
|
150
|
+
const deviceType = extractDeviceType(parsed.parts);
|
|
151
|
+
if (deviceType) {
|
|
152
|
+
log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
|
|
153
|
+
}
|
|
154
|
+
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
155
|
+
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
156
|
+
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
157
|
+
parsed.taskId // TASK_ID (param.id)
|
|
158
|
+
).catch((err) => {
|
|
159
|
+
error(`[BOT] Failed to save runtime info:`, err);
|
|
160
|
+
});
|
|
135
161
|
// Resolve configuration (needed for status updates)
|
|
136
162
|
const config = resolveXYConfig(cfg);
|
|
137
163
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -147,12 +173,6 @@ export async function handleXYMessage(params) {
|
|
|
147
173
|
},
|
|
148
174
|
});
|
|
149
175
|
log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
|
|
150
|
-
// 🔑 注册session(带引用计数)
|
|
151
|
-
log(`[BOT] 📝 About to register session for tools...`);
|
|
152
|
-
log(`[BOT] - sessionKey: ${route.sessionKey}`);
|
|
153
|
-
log(`[BOT] - sessionId: ${parsed.sessionId}`);
|
|
154
|
-
log(`[BOT] - taskId: ${parsed.taskId}`);
|
|
155
|
-
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
156
176
|
registerSession(route.sessionKey, {
|
|
157
177
|
config,
|
|
158
178
|
sessionId: parsed.sessionId,
|
|
@@ -160,7 +180,6 @@ export async function handleXYMessage(params) {
|
|
|
160
180
|
messageId: parsed.messageId,
|
|
161
181
|
agentId: route.accountId,
|
|
162
182
|
});
|
|
163
|
-
log(`[BOT] ✅ Session registered for tools`);
|
|
164
183
|
// 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
|
|
165
184
|
log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
|
|
166
185
|
void sendStatusUpdate({
|
|
@@ -221,9 +240,7 @@ export async function handleXYMessage(params) {
|
|
|
221
240
|
});
|
|
222
241
|
// 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
|
|
223
242
|
log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
|
|
224
|
-
log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
|
|
225
243
|
log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
|
|
226
|
-
log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
|
|
227
244
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
|
|
228
245
|
cfg,
|
|
229
246
|
runtime,
|
|
@@ -233,19 +250,12 @@ export async function handleXYMessage(params) {
|
|
|
233
250
|
accountId: route.accountId,
|
|
234
251
|
isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
|
|
235
252
|
});
|
|
236
|
-
log(`[BOT-DISPATCHER] ✅ Reply dispatcher created successfully`);
|
|
237
253
|
// 🔑 只有第一条消息启动状态定时器
|
|
238
254
|
// 第二条消息会很快返回,不需要定时器
|
|
239
255
|
if (!isSecondMessage) {
|
|
240
256
|
startStatusInterval();
|
|
241
257
|
log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
|
|
242
258
|
}
|
|
243
|
-
else {
|
|
244
|
-
log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
|
|
245
|
-
}
|
|
246
|
-
log(`xy: dispatching to agent (session=${parsed.sessionId})`);
|
|
247
|
-
// Dispatch to OpenClaw core using correct API (following feishu pattern)
|
|
248
|
-
log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
|
|
249
259
|
// Build session context for AsyncLocalStorage
|
|
250
260
|
const sessionContext = {
|
|
251
261
|
config,
|
|
@@ -253,13 +263,14 @@ export async function handleXYMessage(params) {
|
|
|
253
263
|
taskId: parsed.taskId,
|
|
254
264
|
messageId: parsed.messageId,
|
|
255
265
|
agentId: route.accountId,
|
|
266
|
+
deviceType,
|
|
256
267
|
};
|
|
268
|
+
log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
257
269
|
await core.channel.reply.withReplyDispatcher({
|
|
258
270
|
dispatcher,
|
|
259
271
|
onSettled: () => {
|
|
260
272
|
log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
|
|
261
273
|
log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
|
|
262
|
-
markDispatchIdle();
|
|
263
274
|
// 🔑 减少引用计数
|
|
264
275
|
decrementTaskIdRef(parsed.sessionId);
|
|
265
276
|
// 🔑 如果是第一条消息完成,解锁
|
|
@@ -273,12 +284,32 @@ export async function handleXYMessage(params) {
|
|
|
273
284
|
},
|
|
274
285
|
run: () =>
|
|
275
286
|
// 🔐 Use AsyncLocalStorage to provide session context to tools
|
|
276
|
-
runWithSessionContext(sessionContext, () =>
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
runWithSessionContext(sessionContext, async () => {
|
|
288
|
+
log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
|
|
289
|
+
log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
290
|
+
log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
291
|
+
log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
292
|
+
log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
293
|
+
log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
294
|
+
try {
|
|
295
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
296
|
+
ctx: ctxPayload,
|
|
297
|
+
cfg,
|
|
298
|
+
dispatcher,
|
|
299
|
+
replyOptions,
|
|
300
|
+
});
|
|
301
|
+
log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
|
|
302
|
+
log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
catch (dispatchErr) {
|
|
306
|
+
error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
|
|
307
|
+
error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
308
|
+
error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
309
|
+
error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
310
|
+
throw dispatchErr;
|
|
311
|
+
}
|
|
312
|
+
}),
|
|
282
313
|
});
|
|
283
314
|
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
284
315
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
|
@@ -336,19 +367,3 @@ function buildXYMediaPayload(mediaList) {
|
|
|
336
367
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
337
368
|
};
|
|
338
369
|
}
|
|
339
|
-
/**
|
|
340
|
-
* Infer OpenClaw media type from file type string.
|
|
341
|
-
*/
|
|
342
|
-
function inferMediaType(fileType) {
|
|
343
|
-
const lower = fileType.toLowerCase();
|
|
344
|
-
if (lower.includes("image") || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(lower)) {
|
|
345
|
-
return "image";
|
|
346
|
-
}
|
|
347
|
-
if (lower.includes("video") || /\.(mp4|avi|mov|mkv|webm)$/i.test(lower)) {
|
|
348
|
-
return "video";
|
|
349
|
-
}
|
|
350
|
-
if (lower.includes("audio") || /\.(mp3|wav|ogg|m4a)$/i.test(lower)) {
|
|
351
|
-
return "audio";
|
|
352
|
-
}
|
|
353
|
-
return "file";
|
|
354
|
-
}
|
package/dist/src/channel.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
@@ -22,9 +21,17 @@ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
|
|
|
22
21
|
import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
|
|
23
22
|
import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
|
|
24
23
|
import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
25
|
-
import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
|
|
26
24
|
import { viewPushResultTool } from "./tools/view-push-result-tool.js";
|
|
27
25
|
import { imageReadingTool } from "./tools/image-reading-tool.js";
|
|
26
|
+
import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
|
|
27
|
+
import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
|
|
28
|
+
import { xiaoyiAddCollectionTool } from "./tools/xiaoyi-add-collection-tool.js";
|
|
29
|
+
import { xiaoyiDeleteCollectionTool } from "./tools/xiaoyi-delete-collection-tool.js";
|
|
30
|
+
import { saveMediaToGalleryTool } from "./tools/save-media-to-gallery-tool.js";
|
|
31
|
+
import { saveFileToPhoneTool } from "./tools/save-file-to-phone-tool.js";
|
|
32
|
+
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
33
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
34
|
+
import { logger } from "./utils/logger.js";
|
|
28
35
|
/**
|
|
29
36
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
30
37
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -63,8 +70,13 @@ export const xyPlugin = {
|
|
|
63
70
|
schema: xyConfigSchema,
|
|
64
71
|
},
|
|
65
72
|
outbound: xyOutbound,
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
agentTools: () => {
|
|
74
|
+
const allTools = [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, xiaoyiCollectionTool, xiaoyiAddCollectionTool, xiaoyiDeleteCollectionTool, saveMediaToGalleryTool, saveFileToPhoneTool];
|
|
75
|
+
const ctx = getCurrentSessionContext();
|
|
76
|
+
const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
|
|
77
|
+
logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
78
|
+
return filtered;
|
|
79
|
+
},
|
|
68
80
|
messaging: {
|
|
69
81
|
normalizeTarget: (raw) => {
|
|
70
82
|
const trimmed = raw.trim();
|
|
@@ -81,6 +93,22 @@ export const xyPlugin = {
|
|
|
81
93
|
hint: "<sessionId>",
|
|
82
94
|
},
|
|
83
95
|
},
|
|
96
|
+
bindings: {
|
|
97
|
+
compileConfiguredBinding: ({ conversationId }) => {
|
|
98
|
+
const sessionId = conversationId.trim();
|
|
99
|
+
if (!sessionId)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
conversationId: sessionId,
|
|
103
|
+
parentConversationId: undefined,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
matchInboundConversation: ({ compiledBinding, conversationId }) => {
|
|
107
|
+
return compiledBinding.conversationId === conversationId
|
|
108
|
+
? { conversationId, matchPriority: 2 }
|
|
109
|
+
: null;
|
|
110
|
+
},
|
|
111
|
+
},
|
|
84
112
|
reload: {
|
|
85
113
|
configPrefixes: ["channels.xiaoyi-channel"],
|
|
86
114
|
},
|
package/dist/src/client.js
CHANGED
|
@@ -72,21 +72,14 @@ export function getCachedManagerCount() {
|
|
|
72
72
|
* Helps identify connection issues and orphan connections.
|
|
73
73
|
*/
|
|
74
74
|
export function diagnoseAllManagers() {
|
|
75
|
-
console.log("========================================");
|
|
76
|
-
console.log("📊 WebSocket Manager Global Diagnostics");
|
|
77
|
-
console.log("========================================");
|
|
78
75
|
console.log(`Total cached managers: ${wsManagerCache.size}`);
|
|
79
|
-
console.log("");
|
|
80
76
|
if (wsManagerCache.size === 0) {
|
|
81
77
|
console.log("ℹ️ No managers in cache");
|
|
82
|
-
console.log("========================================");
|
|
83
78
|
return;
|
|
84
79
|
}
|
|
85
80
|
let orphanCount = 0;
|
|
86
81
|
wsManagerCache.forEach((manager, key) => {
|
|
87
82
|
const diag = manager.getConnectionDiagnostics();
|
|
88
|
-
console.log(`📌 Manager: ${key}`);
|
|
89
|
-
console.log(` Shutting down: ${diag.isShuttingDown}`);
|
|
90
83
|
console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
|
|
91
84
|
// Connection
|
|
92
85
|
console.log(` 🔌 Connection:`);
|
|
@@ -110,7 +103,6 @@ export function diagnoseAllManagers() {
|
|
|
110
103
|
else {
|
|
111
104
|
console.log(`✅ No orphan connections found`);
|
|
112
105
|
}
|
|
113
|
-
console.log("========================================");
|
|
114
106
|
}
|
|
115
107
|
/**
|
|
116
108
|
* Clean up orphan connections across all managers.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// SENTINEL HOOK API 请求模块
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -8,8 +8,10 @@ function generateTraceId() {
|
|
|
8
8
|
return randomBytes(16).toString("hex");
|
|
9
9
|
}
|
|
10
10
|
function buildHeaders(config) {
|
|
11
|
+
const traceId = generateTraceId();
|
|
12
|
+
console.log(`[SENTINEL HOOK] trace-id: ${traceId}`);
|
|
11
13
|
return {
|
|
12
|
-
"x-hag-trace-id":
|
|
14
|
+
"x-hag-trace-id": traceId,
|
|
13
15
|
"x-uid": config.uid,
|
|
14
16
|
"x-api-key": config.apiKey,
|
|
15
17
|
"x-request-from": config.requestFrom,
|
|
@@ -30,13 +32,13 @@ function buildRequestOptions(url, headers, timeout) {
|
|
|
30
32
|
}
|
|
31
33
|
function parseResponse(data) {
|
|
32
34
|
if (!data?.trim())
|
|
33
|
-
throw new Error("[
|
|
35
|
+
throw new Error("[SENTINEL HOOK] API response is empty");
|
|
34
36
|
const json = JSON.parse(data);
|
|
35
37
|
if (json.retCode && json.retCode !== "0") {
|
|
36
|
-
throw new Error(`[
|
|
38
|
+
throw new Error(`[SENTINEL HOOK] API error: ${json.retMsg || "unknown"}`);
|
|
37
39
|
}
|
|
38
40
|
if (!json.retCode && json.code) {
|
|
39
|
-
throw new Error(`[
|
|
41
|
+
throw new Error(`[SENTINEL HOOK] Backend error: ${json.desc || "unknown"}`);
|
|
40
42
|
}
|
|
41
43
|
return json;
|
|
42
44
|
}
|
|
@@ -47,12 +49,13 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
47
49
|
questionText,
|
|
48
50
|
textSource: config.textSource,
|
|
49
51
|
action: config.action,
|
|
52
|
+
extra: JSON.stringify({ userId: config.uid }),
|
|
50
53
|
};
|
|
51
54
|
return new Promise((resolve, reject) => {
|
|
52
55
|
const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
|
|
53
56
|
const req = https.request(options, (res) => {
|
|
54
57
|
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
55
|
-
reject(new Error(`[
|
|
58
|
+
reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
58
61
|
let data = "";
|
|
@@ -61,17 +64,24 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
61
64
|
});
|
|
62
65
|
res.on("end", () => {
|
|
63
66
|
try {
|
|
64
|
-
|
|
67
|
+
const result = parseResponse(data);
|
|
68
|
+
console.log(`[SENTINEL HOOK] ✅ 请求成功`);
|
|
69
|
+
resolve(result);
|
|
65
70
|
}
|
|
66
71
|
catch (e) {
|
|
72
|
+
console.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
67
73
|
reject(e);
|
|
68
74
|
}
|
|
69
75
|
});
|
|
70
76
|
});
|
|
71
|
-
req.on("error",
|
|
77
|
+
req.on("error", (error) => {
|
|
78
|
+
console.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
reject(error);
|
|
80
|
+
});
|
|
72
81
|
req.on("timeout", () => {
|
|
82
|
+
console.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
|
|
73
83
|
req.destroy();
|
|
74
|
-
reject(new Error("[
|
|
84
|
+
reject(new Error("[SENTINEL HOOK] Request timeout"));
|
|
75
85
|
});
|
|
76
86
|
req.write(JSON.stringify(payload));
|
|
77
87
|
req.end();
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -7,7 +7,7 @@ import { logger } from "../utils/logger.js";
|
|
|
7
7
|
let cachedConfig = null;
|
|
8
8
|
function readServiceUrl() {
|
|
9
9
|
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
10
|
-
throw new Error(`[
|
|
10
|
+
throw new Error(`[SENTINEL HOOK] Environment file not found: ${ENV_FILE_PATH}`);
|
|
11
11
|
}
|
|
12
12
|
const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
|
|
13
13
|
for (const line of envData.split("\n")) {
|
|
@@ -22,7 +22,7 @@ function readServiceUrl() {
|
|
|
22
22
|
if (key === "SERVICE_URL" && value)
|
|
23
23
|
return value;
|
|
24
24
|
}
|
|
25
|
-
throw new Error("[
|
|
25
|
+
throw new Error("[SENTINEL HOOK] Missing SERVICE_URL in env file");
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
@@ -45,6 +45,6 @@ export function getCsplConfig(cfg) {
|
|
|
45
45
|
textSource: CSPL_STATIC_CONFIG.textSource,
|
|
46
46
|
action: CSPL_STATIC_CONFIG.action,
|
|
47
47
|
};
|
|
48
|
-
logger.log("[
|
|
48
|
+
logger.log("[SENTINEL HOOK] Config loaded (uid/apiKey from XYChannelConfig)");
|
|
49
49
|
return cachedConfig;
|
|
50
50
|
}
|
|
@@ -10,6 +10,7 @@ export interface ApiPayload {
|
|
|
10
10
|
questionText: string;
|
|
11
11
|
textSource: string;
|
|
12
12
|
action: string;
|
|
13
|
+
extra: string;
|
|
13
14
|
}
|
|
14
15
|
export interface ApiResponse {
|
|
15
16
|
data?: {
|
|
@@ -25,7 +26,7 @@ export declare const MIN_TEXT_LENGTH = 0;
|
|
|
25
26
|
export declare const MAX_TEXT_LENGTH = 4096;
|
|
26
27
|
export declare const MAX_TOTAL_LENGTH = 40960;
|
|
27
28
|
export declare const regex: RegExp;
|
|
28
|
-
export declare const DEFAULT_HTTP_PORT =
|
|
29
|
+
export declare const DEFAULT_HTTP_PORT = 443;
|
|
29
30
|
export declare const HTTP_STATUS_BAD_REQUEST = 400;
|
|
30
31
|
export declare const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
31
32
|
export declare const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
|
@@ -3,7 +3,7 @@ export const MIN_TEXT_LENGTH = 0;
|
|
|
3
3
|
export const MAX_TEXT_LENGTH = 4096;
|
|
4
4
|
export const MAX_TOTAL_LENGTH = 40960;
|
|
5
5
|
export const regex = /[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:""\'\'()()\[\]【】]/;
|
|
6
|
-
export const DEFAULT_HTTP_PORT =
|
|
6
|
+
export const DEFAULT_HTTP_PORT = 443;
|
|
7
7
|
export const HTTP_STATUS_BAD_REQUEST = 400;
|
|
8
8
|
export const ENV_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
9
9
|
export const API_URL_SUFFIX = "/celia-claw/v1/rest-api/skill/execute";
|
package/dist/src/file-upload.js
CHANGED
|
@@ -55,12 +55,10 @@ export class XYFileUploadService {
|
|
|
55
55
|
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
56
56
|
}
|
|
57
57
|
const prepareData = await prepareResp.json();
|
|
58
|
-
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
59
58
|
if (prepareData.code !== "0") {
|
|
60
59
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
61
60
|
}
|
|
62
61
|
const { objectId, draftId, uploadInfos } = prepareData;
|
|
63
|
-
console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
|
|
64
62
|
// Phase 2: Upload
|
|
65
63
|
console.log(`[XY File Upload] Phase 2: Upload file data`);
|
|
66
64
|
const uploadInfo = uploadInfos[0]; // Single-part upload
|
|
@@ -69,11 +67,8 @@ export class XYFileUploadService {
|
|
|
69
67
|
headers: uploadInfo.headers,
|
|
70
68
|
body: fileBuffer,
|
|
71
69
|
});
|
|
72
|
-
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}, url: ${uploadInfo.url}`);
|
|
73
|
-
console.log(`[XY File Upload] Upload response headers:`, JSON.stringify(Object.fromEntries(uploadResp.headers.entries()), null, 2));
|
|
74
70
|
if (!uploadResp.ok) {
|
|
75
71
|
const uploadErrorText = await uploadResp.text();
|
|
76
|
-
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
77
72
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
78
73
|
}
|
|
79
74
|
console.log(`[XY File Upload] Upload complete`);
|
|
@@ -96,7 +91,6 @@ export class XYFileUploadService {
|
|
|
96
91
|
throw new Error(`Complete failed: HTTP ${completeResp.status}`);
|
|
97
92
|
}
|
|
98
93
|
const completeData = await completeResp.json();
|
|
99
|
-
console.log(`[XY File Upload] Complete response:`, JSON.stringify(completeData, null, 2));
|
|
100
94
|
console.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
|
|
101
95
|
return objectId;
|
|
102
96
|
}
|
|
@@ -110,7 +104,6 @@ export class XYFileUploadService {
|
|
|
110
104
|
* Uses completeAndQuery endpoint to get the file URL directly.
|
|
111
105
|
*/
|
|
112
106
|
async uploadFileAndGetUrl(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
|
|
113
|
-
console.log(`[XY File Upload] Starting file upload with URL retrieval: ${filePath}`);
|
|
114
107
|
try {
|
|
115
108
|
// Read file
|
|
116
109
|
const fileBuffer = await fs.readFile(filePath);
|
|
@@ -143,7 +136,6 @@ export class XYFileUploadService {
|
|
|
143
136
|
throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
|
|
144
137
|
}
|
|
145
138
|
const prepareData = await prepareResp.json();
|
|
146
|
-
console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
|
|
147
139
|
if (prepareData.code !== "0") {
|
|
148
140
|
throw new Error(`Prepare failed: ${prepareData.desc}`);
|
|
149
141
|
}
|
|
@@ -160,7 +152,6 @@ export class XYFileUploadService {
|
|
|
160
152
|
console.log(`[XY File Upload] Upload response status: ${uploadResp.status}`);
|
|
161
153
|
if (!uploadResp.ok) {
|
|
162
154
|
const uploadErrorText = await uploadResp.text();
|
|
163
|
-
console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
|
|
164
155
|
throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
|
|
165
156
|
}
|
|
166
157
|
console.log(`[XY File Upload] Upload complete`);
|
|
@@ -183,13 +174,12 @@ export class XYFileUploadService {
|
|
|
183
174
|
throw new Error(`CompleteAndQuery failed: HTTP ${completeResp.status}`);
|
|
184
175
|
}
|
|
185
176
|
const completeData = await completeResp.json();
|
|
186
|
-
console.log(`[XY File Upload] CompleteAndQuery response:`, JSON.stringify(completeData, null, 2));
|
|
187
177
|
// Extract file URL from response
|
|
188
178
|
const fileUrl = completeData?.fileDetailInfo?.url || "";
|
|
189
179
|
if (!fileUrl) {
|
|
190
180
|
throw new Error("No file URL returned from completeAndQuery");
|
|
191
181
|
}
|
|
192
|
-
console.log(`[XY File Upload] File upload successful
|
|
182
|
+
console.log(`[XY File Upload] File upload successful`);
|
|
193
183
|
return fileUrl;
|
|
194
184
|
}
|
|
195
185
|
catch (error) {
|