@ynhcj/xiaoyi-channel 0.0.52-beta → 0.0.52-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 +44 -32
- package/dist/src/channel.js +32 -4
- package/dist/src/client.js +0 -6
- 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-download.js +1 -1
- package/dist/src/file-upload.js +1 -11
- package/dist/src/formatter.d.ts +2 -0
- package/dist/src/formatter.js +11 -6
- 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 +6 -0
- package/dist/src/parser.js +16 -0
- package/dist/src/provider.d.ts +2 -0
- package/dist/src/provider.js +124 -0
- package/dist/src/push.js +0 -21
- package/dist/src/reply-dispatcher.d.ts +4 -0
- package/dist/src/reply-dispatcher.js +21 -13
- package/dist/src/thread-bindings.d.ts +54 -0
- package/dist/src/thread-bindings.js +214 -0
- package/dist/src/tools/call-phone-tool.js +2 -18
- package/dist/src/tools/create-alarm-tool.js +3 -7
- 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 -3
- package/dist/src/tools/modify-alarm-tool.js +3 -7
- 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/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 -0
- package/dist/src/tools/session-manager.d.ts +1 -0
- 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 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-add-collection-tool.js +188 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +42 -7
- 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/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/dist/src/websocket.js +15 -9
- 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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { resolveThreadBindingIdleTimeoutMsForChannel, resolveThreadBindingMaxAgeMsForChannel, registerSessionBindingAdapter, resolveThreadBindingConversationIdFromBindingId, unregisterSessionBindingAdapter, } from "openclaw/plugin-sdk/conversation-runtime";
|
|
2
|
+
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
|
3
|
+
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/text-runtime";
|
|
4
|
+
const XY_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.xyThreadBindingsState");
|
|
5
|
+
const state = resolveGlobalSingleton(XY_THREAD_BINDINGS_STATE_KEY, () => ({
|
|
6
|
+
managersByAccountId: new Map(),
|
|
7
|
+
bindingsByAccountSession: new Map(),
|
|
8
|
+
}));
|
|
9
|
+
function getState() {
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
function resolveBindingKey(params) {
|
|
13
|
+
return `${params.accountId}:${params.sessionId}`;
|
|
14
|
+
}
|
|
15
|
+
function toSessionBindingTargetKind(raw) {
|
|
16
|
+
return raw === "subagent" ? "subagent" : "session";
|
|
17
|
+
}
|
|
18
|
+
function toXYTargetKind(raw) {
|
|
19
|
+
return raw === "subagent" ? "subagent" : "session";
|
|
20
|
+
}
|
|
21
|
+
function toSessionBindingRecord(record, defaults) {
|
|
22
|
+
const idleExpiresAt = defaults.idleTimeoutMs > 0 ? record.lastActivityAt + defaults.idleTimeoutMs : undefined;
|
|
23
|
+
const maxAgeExpiresAt = defaults.maxAgeMs > 0 ? record.boundAt + defaults.maxAgeMs : undefined;
|
|
24
|
+
const expiresAt = idleExpiresAt != null && maxAgeExpiresAt != null
|
|
25
|
+
? Math.min(idleExpiresAt, maxAgeExpiresAt)
|
|
26
|
+
: (idleExpiresAt ?? maxAgeExpiresAt);
|
|
27
|
+
return {
|
|
28
|
+
bindingId: resolveBindingKey({
|
|
29
|
+
accountId: record.accountId,
|
|
30
|
+
sessionId: record.sessionId,
|
|
31
|
+
}),
|
|
32
|
+
targetSessionKey: record.targetSessionKey,
|
|
33
|
+
targetKind: toSessionBindingTargetKind(record.targetKind),
|
|
34
|
+
conversation: {
|
|
35
|
+
channel: "xiaoyi-channel",
|
|
36
|
+
accountId: record.accountId,
|
|
37
|
+
conversationId: record.sessionId, // sessionId is the conversationId for Xiaoyi
|
|
38
|
+
parentConversationId: undefined, // Xiaoyi doesn't have parent conversations
|
|
39
|
+
},
|
|
40
|
+
status: "active",
|
|
41
|
+
boundAt: record.boundAt,
|
|
42
|
+
expiresAt,
|
|
43
|
+
metadata: {
|
|
44
|
+
agentId: record.agentId,
|
|
45
|
+
lastActivityAt: record.lastActivityAt,
|
|
46
|
+
idleTimeoutMs: defaults.idleTimeoutMs,
|
|
47
|
+
maxAgeMs: defaults.maxAgeMs,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates a thread binding manager for Xiaoyi channel.
|
|
53
|
+
* Based on feishu implementation but simplified for single-account mode.
|
|
54
|
+
*/
|
|
55
|
+
export function createXYThreadBindingManager(params) {
|
|
56
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
57
|
+
const existing = getState().managersByAccountId.get(accountId);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
const idleTimeoutMs = resolveThreadBindingIdleTimeoutMsForChannel({
|
|
62
|
+
cfg: params.cfg,
|
|
63
|
+
channel: "xiaoyi-channel",
|
|
64
|
+
accountId,
|
|
65
|
+
});
|
|
66
|
+
const maxAgeMs = resolveThreadBindingMaxAgeMsForChannel({
|
|
67
|
+
cfg: params.cfg,
|
|
68
|
+
channel: "xiaoyi-channel",
|
|
69
|
+
accountId,
|
|
70
|
+
});
|
|
71
|
+
const manager = {
|
|
72
|
+
accountId,
|
|
73
|
+
getBySessionId: (sessionId) => getState().bindingsByAccountSession.get(resolveBindingKey({ accountId, sessionId })),
|
|
74
|
+
listBySessionKey: (targetSessionKey) => [...getState().bindingsByAccountSession.values()].filter((record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey),
|
|
75
|
+
bindSession: ({ sessionId, targetKind, targetSessionKey, metadata, }) => {
|
|
76
|
+
const normalizedSessionId = sessionId.trim();
|
|
77
|
+
if (!normalizedSessionId || !targetSessionKey.trim()) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const record = {
|
|
82
|
+
accountId,
|
|
83
|
+
sessionId: normalizedSessionId,
|
|
84
|
+
targetKind: toXYTargetKind(targetKind),
|
|
85
|
+
targetSessionKey: targetSessionKey.trim(),
|
|
86
|
+
agentId: typeof metadata?.agentId === "string" && metadata.agentId.trim()
|
|
87
|
+
? metadata.agentId.trim()
|
|
88
|
+
: resolveAgentIdFromSessionKey(targetSessionKey),
|
|
89
|
+
boundAt: now,
|
|
90
|
+
lastActivityAt: now,
|
|
91
|
+
};
|
|
92
|
+
getState().bindingsByAccountSession.set(resolveBindingKey({ accountId, sessionId: normalizedSessionId }), record);
|
|
93
|
+
return record;
|
|
94
|
+
},
|
|
95
|
+
touchSession: (sessionId, at = Date.now()) => {
|
|
96
|
+
const key = resolveBindingKey({ accountId, sessionId });
|
|
97
|
+
const existingRecord = getState().bindingsByAccountSession.get(key);
|
|
98
|
+
if (!existingRecord) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const updated = { ...existingRecord, lastActivityAt: at };
|
|
102
|
+
getState().bindingsByAccountSession.set(key, updated);
|
|
103
|
+
return updated;
|
|
104
|
+
},
|
|
105
|
+
unbindSession: (sessionId) => {
|
|
106
|
+
const key = resolveBindingKey({ accountId, sessionId });
|
|
107
|
+
const existingRecord = getState().bindingsByAccountSession.get(key);
|
|
108
|
+
if (!existingRecord) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
getState().bindingsByAccountSession.delete(key);
|
|
112
|
+
return existingRecord;
|
|
113
|
+
},
|
|
114
|
+
unbindBySessionKey: (targetSessionKey) => {
|
|
115
|
+
const removed = [];
|
|
116
|
+
for (const record of [...getState().bindingsByAccountSession.values()]) {
|
|
117
|
+
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
getState().bindingsByAccountSession.delete(resolveBindingKey({ accountId, sessionId: record.sessionId }));
|
|
121
|
+
removed.push(record);
|
|
122
|
+
}
|
|
123
|
+
return removed;
|
|
124
|
+
},
|
|
125
|
+
stop: () => {
|
|
126
|
+
for (const key of [...getState().bindingsByAccountSession.keys()]) {
|
|
127
|
+
if (key.startsWith(`${accountId}:`)) {
|
|
128
|
+
getState().bindingsByAccountSession.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
getState().managersByAccountId.delete(accountId);
|
|
132
|
+
unregisterSessionBindingAdapter({
|
|
133
|
+
channel: "xiaoyi-channel",
|
|
134
|
+
accountId,
|
|
135
|
+
adapter: sessionBindingAdapter,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const sessionBindingAdapter = {
|
|
140
|
+
channel: "xiaoyi-channel",
|
|
141
|
+
accountId,
|
|
142
|
+
capabilities: {
|
|
143
|
+
placements: ["current"],
|
|
144
|
+
},
|
|
145
|
+
bind: async (input) => {
|
|
146
|
+
if (input.conversation.channel !== "xiaoyi-channel" || input.placement === "child") {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const bound = manager.bindSession({
|
|
150
|
+
sessionId: input.conversation.conversationId,
|
|
151
|
+
targetKind: input.targetKind,
|
|
152
|
+
targetSessionKey: input.targetSessionKey,
|
|
153
|
+
metadata: input.metadata,
|
|
154
|
+
});
|
|
155
|
+
return bound ? toSessionBindingRecord(bound, { idleTimeoutMs, maxAgeMs }) : null;
|
|
156
|
+
},
|
|
157
|
+
listBySession: (targetSessionKey) => manager
|
|
158
|
+
.listBySessionKey(targetSessionKey)
|
|
159
|
+
.map((entry) => toSessionBindingRecord(entry, { idleTimeoutMs, maxAgeMs })),
|
|
160
|
+
resolveByConversation: (ref) => {
|
|
161
|
+
if (ref.channel !== "xiaoyi-channel") {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const found = manager.getBySessionId(ref.conversationId);
|
|
165
|
+
return found ? toSessionBindingRecord(found, { idleTimeoutMs, maxAgeMs }) : null;
|
|
166
|
+
},
|
|
167
|
+
touch: (bindingId, at) => {
|
|
168
|
+
const sessionId = resolveThreadBindingConversationIdFromBindingId({
|
|
169
|
+
accountId,
|
|
170
|
+
bindingId,
|
|
171
|
+
});
|
|
172
|
+
if (sessionId) {
|
|
173
|
+
manager.touchSession(sessionId, at);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
unbind: async (input) => {
|
|
177
|
+
if (input.targetSessionKey?.trim()) {
|
|
178
|
+
return manager
|
|
179
|
+
.unbindBySessionKey(input.targetSessionKey.trim())
|
|
180
|
+
.map((entry) => toSessionBindingRecord(entry, { idleTimeoutMs, maxAgeMs }));
|
|
181
|
+
}
|
|
182
|
+
const sessionId = resolveThreadBindingConversationIdFromBindingId({
|
|
183
|
+
accountId,
|
|
184
|
+
bindingId: input.bindingId,
|
|
185
|
+
});
|
|
186
|
+
if (!sessionId) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const removed = manager.unbindSession(sessionId);
|
|
190
|
+
return removed ? [toSessionBindingRecord(removed, { idleTimeoutMs, maxAgeMs })] : [];
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
registerSessionBindingAdapter(sessionBindingAdapter);
|
|
194
|
+
getState().managersByAccountId.set(accountId, manager);
|
|
195
|
+
return manager;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Gets the thread binding manager for a given account ID.
|
|
199
|
+
*/
|
|
200
|
+
export function getXYThreadBindingManager(accountId) {
|
|
201
|
+
return getState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Testing utilities for thread bindings.
|
|
205
|
+
*/
|
|
206
|
+
export const __testing = {
|
|
207
|
+
resetXYThreadBindingsForTests() {
|
|
208
|
+
for (const manager of getState().managersByAccountId.values()) {
|
|
209
|
+
manager.stop();
|
|
210
|
+
}
|
|
211
|
+
getState().managersByAccountId.clear();
|
|
212
|
+
getState().bindingsByAccountSession.clear();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
@@ -90,33 +90,17 @@ export const callPhoneTool = {
|
|
|
90
90
|
clearTimeout(timeout);
|
|
91
91
|
wsManager.off("data-event", handler);
|
|
92
92
|
if (event.status === "success" && event.outputs) {
|
|
93
|
-
// Check for error code in outputs
|
|
94
|
-
const code = event.outputs.code !== undefined ? event.outputs.code : null;
|
|
95
|
-
if (code !== null && code !== 0) {
|
|
96
|
-
const errorMsg = event.outputs.errorMsg || event.outputs.errMsg || "未知错误";
|
|
97
|
-
reject(new Error(`拨打电话失败: ${errorMsg} (错误代码: ${code})`));
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
// Return the outputs directly
|
|
101
|
-
const result = {
|
|
102
|
-
success: true,
|
|
103
|
-
code: code,
|
|
104
|
-
phoneNumber: params.phoneNumber,
|
|
105
|
-
slotId: slotId,
|
|
106
|
-
message: "电话拨打成功",
|
|
107
|
-
};
|
|
108
93
|
resolve({
|
|
109
94
|
content: [
|
|
110
95
|
{
|
|
111
96
|
type: "text",
|
|
112
|
-
text: JSON.stringify(
|
|
97
|
+
text: JSON.stringify(event.outputs),
|
|
113
98
|
},
|
|
114
99
|
],
|
|
115
100
|
});
|
|
116
101
|
}
|
|
117
102
|
else {
|
|
118
|
-
|
|
119
|
-
reject(new Error(`拨打电话失败: ${errorDetail}`));
|
|
103
|
+
reject(new Error(`拨打电话失败: ${event.status}`));
|
|
120
104
|
}
|
|
121
105
|
}
|
|
122
106
|
};
|
|
@@ -6,7 +6,7 @@ const ALARM_SNOOZE_DURATION_VALUES = [5, 10, 15, 20, 25, 30];
|
|
|
6
6
|
const ALARM_SNOOZE_TOTAL_VALUES = [0, 1, 3, 5, 10];
|
|
7
7
|
const ALARM_RING_DURATION_VALUES = [1, 5, 10, 15, 20, 30];
|
|
8
8
|
const DAYS_OF_WAKE_TYPE_VALUES = [0, 1, 2, 3, 4];
|
|
9
|
-
const DAYS_OF_WEEK_VALUES = ["Mon", "
|
|
9
|
+
const DAYS_OF_WEEK_VALUES = ["Mon", "Tues", "Wed", "Thur", "Fri", "Sat", "Sun"];
|
|
10
10
|
/**
|
|
11
11
|
* XY create alarm tool - creates an alarm on user's device.
|
|
12
12
|
* Requires alarmTime parameter. Other parameters are optional with sensible defaults.
|
|
@@ -27,7 +27,7 @@ export const createAlarmTool = {
|
|
|
27
27
|
- alarmSnoozeTotal: 再响次数,枚举值:0,1,3,5,10,默认0(表示不再响)
|
|
28
28
|
- alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30,默认5
|
|
29
29
|
- daysOfWakeType: 闹钟响铃类型,枚举值:0=单次响铃,1=法定节假日,2=每天,3=自定义时间,4=法定工作日,默认0
|
|
30
|
-
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,
|
|
30
|
+
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。
|
|
31
31
|
|
|
32
32
|
注意事项:
|
|
33
33
|
a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
|
|
@@ -64,7 +64,7 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
64
64
|
daysOfWeek: {
|
|
65
65
|
// 不指定 type,允许传入数组或 JSON 字符串
|
|
66
66
|
// 具体的类型验证和转换在 execute 函数内部进行
|
|
67
|
-
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,
|
|
67
|
+
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。",
|
|
68
68
|
},
|
|
69
69
|
},
|
|
70
70
|
required: ["alarmTime"],
|
|
@@ -163,10 +163,6 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
163
163
|
if (!normalizedDaysOfWeek || normalizedDaysOfWeek.length === 0) {
|
|
164
164
|
throw new Error("daysOfWeek array cannot be empty");
|
|
165
165
|
}
|
|
166
|
-
// 验证数组长度必须为1
|
|
167
|
-
if (normalizedDaysOfWeek.length !== 1) {
|
|
168
|
-
throw new Error("daysOfWeek 仅支持长度为1的数组。如果需要一周中不同的几天,需要多次调用此工具");
|
|
169
|
-
}
|
|
170
166
|
// Validate each day
|
|
171
167
|
for (const day of normalizedDaysOfWeek) {
|
|
172
168
|
if (typeof day !== "string" || !DAYS_OF_WEEK_VALUES.includes(day)) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Device type to tool name mapping.
|
|
2
|
+
// Supports two modes:
|
|
3
|
+
// - allowlist: only listed tools are available (used for restrictive devices like car)
|
|
4
|
+
// - denylist: listed tools are blocked, everything else is available (used for permissive devices like pc)
|
|
5
|
+
// Tools NOT listed in any device entry → available to all devices (no restriction).
|
|
6
|
+
/** Known device type enum. */
|
|
7
|
+
export const DEVICE_TYPES = ["car", "2in1", "phone"];
|
|
8
|
+
const DEVICE_TOOL_POLICY = {
|
|
9
|
+
"2in1": {
|
|
10
|
+
allowlist: false,
|
|
11
|
+
tools: [
|
|
12
|
+
"xiaoyi_gui_agent",
|
|
13
|
+
"call_phone",
|
|
14
|
+
"send_message",
|
|
15
|
+
"search_message",
|
|
16
|
+
"search_contact",
|
|
17
|
+
"QueryCollection",
|
|
18
|
+
"AddCollection",
|
|
19
|
+
"DeleteCollection",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export function filterToolsByDevice(tools, deviceType) {
|
|
24
|
+
if (!deviceType)
|
|
25
|
+
return tools;
|
|
26
|
+
const policy = DEVICE_TOOL_POLICY[deviceType];
|
|
27
|
+
if (!policy)
|
|
28
|
+
return tools; // unrecognized device → no filtering
|
|
29
|
+
if (policy.allowlist) {
|
|
30
|
+
return tools.filter((tool) => policy.tools.includes(tool.name));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return tools.filter((tool) => !policy.tools.includes(tool.name));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
/**
|
|
5
|
+
* XY find PC devices tool - finds all PC devices associated with the user.
|
|
6
|
+
* Returns device IDs for use in subsequent file search operations.
|
|
7
|
+
*/
|
|
8
|
+
export const findPcDevicesTool = {
|
|
9
|
+
name: "find_pc_devices",
|
|
10
|
+
label: "Find PC Devices",
|
|
11
|
+
description: `查找用户所有PC/电脑设备,获取设备ID列表。当用户说"帮我找一下PC/电脑上的xxx文件"、"帮我搜索电脑上的xxx"等涉及PC设备的请求时,先调用此工具获取设备ID,再进行后续操作。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,请严格遵守。`,
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {},
|
|
15
|
+
required: [],
|
|
16
|
+
},
|
|
17
|
+
async execute(toolCallId, params) {
|
|
18
|
+
// Get session context
|
|
19
|
+
const sessionContext = getCurrentSessionContext();
|
|
20
|
+
if (!sessionContext) {
|
|
21
|
+
throw new Error("No active XY session found. Find PC devices tool can only be used during an active conversation.");
|
|
22
|
+
}
|
|
23
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
24
|
+
// Get WebSocket manager
|
|
25
|
+
const wsManager = getXYWebSocketManager(config);
|
|
26
|
+
// Build GetAllDevice command
|
|
27
|
+
const command = {
|
|
28
|
+
header: {
|
|
29
|
+
namespace: "Common",
|
|
30
|
+
name: "Action",
|
|
31
|
+
},
|
|
32
|
+
payload: {
|
|
33
|
+
cardParam: {},
|
|
34
|
+
executeParam: {
|
|
35
|
+
achieveType: "INTENT",
|
|
36
|
+
actionResponse: true,
|
|
37
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
38
|
+
dimension: "",
|
|
39
|
+
executeMode: "background",
|
|
40
|
+
intentName: "GetAllDevice",
|
|
41
|
+
intentParam: {},
|
|
42
|
+
needUnlock: true,
|
|
43
|
+
permissionId: [],
|
|
44
|
+
timeOut: 5,
|
|
45
|
+
},
|
|
46
|
+
needUploadResult: true,
|
|
47
|
+
pageControlRelated: false,
|
|
48
|
+
responses: [{
|
|
49
|
+
displayText: "",
|
|
50
|
+
resultCode: "",
|
|
51
|
+
ttsText: "",
|
|
52
|
+
}],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
// Send command and wait for response (60 second timeout)
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
wsManager.off("data-event", handler);
|
|
59
|
+
reject(new Error("查找PC设备超时(60秒)"));
|
|
60
|
+
}, 60000);
|
|
61
|
+
// Listen for data events from WebSocket
|
|
62
|
+
const handler = (event) => {
|
|
63
|
+
if (event.intentName === "GetAllDevice") {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
wsManager.off("data-event", handler);
|
|
66
|
+
if (event.status === "success" && event.outputs) {
|
|
67
|
+
resolve({
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: JSON.stringify(event.outputs),
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
reject(new Error(`查找PC设备失败: ${event.status}`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
// Register event handler
|
|
82
|
+
wsManager.on("data-event", handler);
|
|
83
|
+
// Send the command
|
|
84
|
+
sendCommand({
|
|
85
|
+
config,
|
|
86
|
+
sessionId,
|
|
87
|
+
taskId,
|
|
88
|
+
messageId,
|
|
89
|
+
command,
|
|
90
|
+
}).then(() => {
|
|
91
|
+
}).catch((error) => {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
wsManager.off("data-event", handler);
|
|
94
|
+
reject(error);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -87,8 +87,8 @@ async function processImageInput(imageInput, uploadService) {
|
|
|
87
87
|
/**
|
|
88
88
|
* Call image understanding API with streaming response
|
|
89
89
|
*/
|
|
90
|
-
async function callImageUnderstandingAPI(imageUrl, text, apiKey, uid) {
|
|
91
|
-
const apiUrl =
|
|
90
|
+
async function callImageUnderstandingAPI(imageUrl, text, apiKey, uid, fileUploadUrl) {
|
|
91
|
+
const apiUrl = `${fileUploadUrl}/celia-claw/v1/sse-api/skill/execute`;
|
|
92
92
|
const traceId = uuidv4();
|
|
93
93
|
const headers = {
|
|
94
94
|
"Content-Type": "application/json",
|
|
@@ -276,7 +276,7 @@ d. 返回图像理解的文本描述内容`,
|
|
|
276
276
|
downloadedFile = processedImage.localPath;
|
|
277
277
|
}
|
|
278
278
|
// Call image understanding API
|
|
279
|
-
const caption = await callImageUnderstandingAPI(processedImage.imageUrl, prompt, config.apiKey, config.uid);
|
|
279
|
+
const caption = await callImageUnderstandingAPI(processedImage.imageUrl, prompt, config.apiKey, config.uid, config.fileUploadUrl);
|
|
280
280
|
// Clean up downloaded file if any
|
|
281
281
|
if (downloadedFile) {
|
|
282
282
|
try {
|
|
@@ -6,7 +6,7 @@ const ALARM_SNOOZE_DURATION_VALUES = [5, 10, 15, 20, 25, 30];
|
|
|
6
6
|
const ALARM_SNOOZE_TOTAL_VALUES = [0, 1, 3, 5, 10];
|
|
7
7
|
const ALARM_RING_DURATION_VALUES = [1, 5, 10, 15, 20, 30];
|
|
8
8
|
const DAYS_OF_WAKE_TYPE_VALUES = [0, 1, 2, 3, 4];
|
|
9
|
-
const DAYS_OF_WEEK_VALUES = ["Mon", "
|
|
9
|
+
const DAYS_OF_WEEK_VALUES = ["Mon", "Tues", "Wed", "Thur", "Fri", "Sat", "Sun"];
|
|
10
10
|
/**
|
|
11
11
|
* XY modify alarm tool - modifies an existing alarm on user's device.
|
|
12
12
|
* Requires entityId from search_alarm or create_alarm tool.
|
|
@@ -31,7 +31,7 @@ export const modifyAlarmTool = {
|
|
|
31
31
|
- alarmSnoozeTotal: 再响次数,枚举值:0,1,3,5,10
|
|
32
32
|
- alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30
|
|
33
33
|
- daysOfWakeType: 闹钟响铃类型,枚举值:0=单次,1=法定节假日,2=每天,3=自定义,4=法定工作日
|
|
34
|
-
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,
|
|
34
|
+
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。
|
|
35
35
|
|
|
36
36
|
使用流程:
|
|
37
37
|
1. 先调用 search_alarm 工具查询闹钟,获取 entityId,
|
|
@@ -79,7 +79,7 @@ export const modifyAlarmTool = {
|
|
|
79
79
|
daysOfWeek: {
|
|
80
80
|
// 不指定 type,允许传入数组或 JSON 字符串
|
|
81
81
|
// 具体的类型验证和转换在 execute 函数内部进行
|
|
82
|
-
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,
|
|
82
|
+
description: "自定义响铃星期(仅当daysOfWakeType=3时需要,其他情况不要传递),数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。",
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
85
|
required: ["entityId"],
|
|
@@ -199,10 +199,6 @@ export const modifyAlarmTool = {
|
|
|
199
199
|
if (!normalizedDaysOfWeek || normalizedDaysOfWeek.length === 0) {
|
|
200
200
|
throw new Error("daysOfWeek array cannot be empty");
|
|
201
201
|
}
|
|
202
|
-
// 验证数组长度必须为1
|
|
203
|
-
if (normalizedDaysOfWeek.length !== 1) {
|
|
204
|
-
throw new Error("daysOfWeek 仅支持长度为1的数组。如果需要一周中不同的几天,需要多次调用此工具");
|
|
205
|
-
}
|
|
206
202
|
// Validate each day
|
|
207
203
|
for (const day of normalizedDaysOfWeek) {
|
|
208
204
|
if (typeof day !== "string" || !DAYS_OF_WEEK_VALUES.includes(day)) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
import { XYFileUploadService } from "../file-upload.js";
|
|
5
|
+
/**
|
|
6
|
+
* Duck-typed ToolInputError: openclaw 按 .name 字段匹配,不用 instanceof。
|
|
7
|
+
* 抛出此错误会让 openclaw 返回 HTTP 400 而非 500,
|
|
8
|
+
* LLM 会将其识别为参数错误而非瞬时故障,不会触发重试。
|
|
9
|
+
*/
|
|
10
|
+
class ToolInputError extends Error {
|
|
11
|
+
status = 400;
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ToolInputError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* XY save file to phone tool - saves files to user's device file manager.
|
|
19
|
+
* Supports local file paths (auto-uploaded to get public URL) and public URLs.
|
|
20
|
+
*/
|
|
21
|
+
export const saveFileToPhoneTool = {
|
|
22
|
+
name: "save_file_to_file_manager",
|
|
23
|
+
label: "Save File to Phone",
|
|
24
|
+
description: `将文件保存到手机文件管理器。
|
|
25
|
+
工具参数说明:
|
|
26
|
+
a. fileName:必填,string类型,文件名称。
|
|
27
|
+
b. url:必填,string类型,支持本地路径或者公网url路径。如果是本地路径,会先上传获取公网url再保存到手机。
|
|
28
|
+
c. suffix:必填,string类型,文件后缀,例如 ppt、doc、pdf 等。
|
|
29
|
+
|
|
30
|
+
注意:
|
|
31
|
+
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
32
|
+
b. 如果遇到各类调用失败场景,不可以重试,直接返回错误。
|
|
33
|
+
c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
34
|
+
|
|
35
|
+
回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。
|
|
36
|
+
`,
|
|
37
|
+
parameters: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
fileName: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "必填,文件名称。",
|
|
43
|
+
},
|
|
44
|
+
url: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "必填,支持本地路径或者公网url路径。如果是本地路径会先上传获取公网url。",
|
|
47
|
+
},
|
|
48
|
+
suffix: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "必填,文件后缀,例如 ppt、doc、pdf 等。",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ["fileName", "url", "suffix"],
|
|
54
|
+
},
|
|
55
|
+
async execute(toolCallId, params) {
|
|
56
|
+
// Validate parameters
|
|
57
|
+
const { fileName, url, suffix } = params;
|
|
58
|
+
if (!url || typeof url !== "string") {
|
|
59
|
+
throw new ToolInputError("缺少必填参数: url");
|
|
60
|
+
}
|
|
61
|
+
if (!fileName || typeof fileName !== "string") {
|
|
62
|
+
throw new ToolInputError("缺少必填参数: fileName");
|
|
63
|
+
}
|
|
64
|
+
if (!suffix || typeof suffix !== "string") {
|
|
65
|
+
throw new ToolInputError("缺少必填参数: suffix");
|
|
66
|
+
}
|
|
67
|
+
// Get session context
|
|
68
|
+
const sessionContext = getCurrentSessionContext();
|
|
69
|
+
if (!sessionContext) {
|
|
70
|
+
throw new Error("No active XY session found. SaveFileToFileManager tool can only be used during an active conversation.");
|
|
71
|
+
}
|
|
72
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
73
|
+
// Get WebSocket manager
|
|
74
|
+
const wsManager = getXYWebSocketManager(config);
|
|
75
|
+
// Determine the URL: if it's a local path, upload first to get public URL
|
|
76
|
+
let publicUrl = url;
|
|
77
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
78
|
+
// Local file path - upload to get public URL
|
|
79
|
+
const uploadService = new XYFileUploadService(config.fileUploadUrl, config.apiKey, config.uid);
|
|
80
|
+
publicUrl = await uploadService.uploadFileAndGetUrl(url);
|
|
81
|
+
if (!publicUrl) {
|
|
82
|
+
throw new Error("本地文件上传失败,无法获取公网URL");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Build intentParam
|
|
86
|
+
const intentParam = {
|
|
87
|
+
fileName: fileName,
|
|
88
|
+
url: publicUrl,
|
|
89
|
+
suffix: suffix,
|
|
90
|
+
};
|
|
91
|
+
// Build SaveFileToFileManager command
|
|
92
|
+
const command = {
|
|
93
|
+
header: {
|
|
94
|
+
namespace: "Common",
|
|
95
|
+
name: "Action",
|
|
96
|
+
},
|
|
97
|
+
payload: {
|
|
98
|
+
cardParam: {},
|
|
99
|
+
executeParam: {
|
|
100
|
+
executeMode: "background",
|
|
101
|
+
intentName: "SaveFileToFileManager",
|
|
102
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
103
|
+
dimension: "",
|
|
104
|
+
needUnlock: true,
|
|
105
|
+
actionResponse: true,
|
|
106
|
+
appType: "OHOS_APP",
|
|
107
|
+
timeOut: 5,
|
|
108
|
+
timeout: 55000,
|
|
109
|
+
intentParam,
|
|
110
|
+
permissionId: ["ohos.permission.WRITE_IMAGEVIDEO"],
|
|
111
|
+
achieveType: "INTENT",
|
|
112
|
+
},
|
|
113
|
+
responses: [
|
|
114
|
+
{
|
|
115
|
+
resultCode: "",
|
|
116
|
+
displayText: "",
|
|
117
|
+
ttsText: "",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
needUploadResult: true,
|
|
121
|
+
noHalfPage: false,
|
|
122
|
+
pageControlRelated: false,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
// Send command and wait for response (60 second timeout)
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const timeout = setTimeout(() => {
|
|
128
|
+
wsManager.off("data-event", handler);
|
|
129
|
+
reject(new Error("保存文件到手机超时(60秒)"));
|
|
130
|
+
}, 60000);
|
|
131
|
+
// Listen for data events from WebSocket
|
|
132
|
+
const handler = (event) => {
|
|
133
|
+
if (event.intentName === "SaveFileToFileManager") {
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
wsManager.off("data-event", handler);
|
|
136
|
+
if (event.status === "success" && event.outputs) {
|
|
137
|
+
resolve({
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify(event.outputs),
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
reject(new Error(`保存文件到手机失败: ${event.status}`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// Register event handler
|
|
152
|
+
wsManager.on("data-event", handler);
|
|
153
|
+
// Send the command
|
|
154
|
+
sendCommand({
|
|
155
|
+
config,
|
|
156
|
+
sessionId,
|
|
157
|
+
taskId,
|
|
158
|
+
messageId,
|
|
159
|
+
command,
|
|
160
|
+
})
|
|
161
|
+
.then(() => {
|
|
162
|
+
})
|
|
163
|
+
.catch((error) => {
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
wsManager.off("data-event", handler);
|
|
166
|
+
reject(error);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
};
|