@ynhcj/xiaoyi-channel 0.0.153-beta → 0.0.154-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +23 -0
- package/dist/src/bot.js +9 -1
- package/dist/src/channel.js +59 -5
- package/dist/src/cron-command.d.ts +16 -0
- package/dist/src/cron-command.js +64 -0
- package/dist/src/formatter.d.ts +11 -1
- package/dist/src/formatter.js +22 -2
- package/dist/src/parser.d.ts +2 -1
- package/dist/src/parser.js +25 -0
- package/dist/src/reply-dispatcher.js +73 -1
- package/dist/src/tools/agent-as-skill-tool.js +1 -0
- package/dist/src/tools/calendar-tool.js +1 -0
- package/dist/src/tools/call-phone-tool.js +1 -0
- package/dist/src/tools/create-alarm-tool.js +1 -0
- package/dist/src/tools/create-all-tools.js +4 -0
- package/dist/src/tools/delete-alarm-tool.js +1 -0
- package/dist/src/tools/discover-cross-devices-tool.d.ts +2 -0
- package/dist/src/tools/discover-cross-devices-tool.js +235 -0
- package/dist/src/tools/find-pc-devices-tool.js +1 -0
- package/dist/src/tools/location-tool.js +1 -0
- package/dist/src/tools/modify-alarm-tool.js +1 -0
- package/dist/src/tools/modify-note-tool.js +1 -0
- package/dist/src/tools/note-tool.js +1 -0
- package/dist/src/tools/query-app-message-tool.js +3 -2
- package/dist/src/tools/query-memory-data-tool.js +3 -2
- package/dist/src/tools/query-todo-task-tool.js +3 -2
- package/dist/src/tools/save-file-to-phone-tool.js +1 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +1 -0
- package/dist/src/tools/search-alarm-tool.js +1 -0
- package/dist/src/tools/search-calendar-tool.js +1 -0
- package/dist/src/tools/search-contact-tool.js +1 -0
- package/dist/src/tools/search-email-tool.js +3 -2
- package/dist/src/tools/search-file-tool.js +1 -0
- package/dist/src/tools/search-message-tool.js +1 -0
- package/dist/src/tools/search-note-tool.js +1 -0
- package/dist/src/tools/search-photo-gallery-tool.js +3 -2
- package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
- package/dist/src/tools/send-cross-device-task-tool.js +303 -0
- package/dist/src/tools/send-email-tool.js +3 -2
- package/dist/src/tools/send-message-tool.js +1 -0
- package/dist/src/tools/session-manager.d.ts +13 -1
- package/dist/src/tools/session-manager.js +38 -0
- package/dist/src/tools/upload-file-tool.d.ts +1 -1
- package/dist/src/tools/upload-file-tool.js +8 -2
- package/dist/src/tools/upload-photo-tool.js +3 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +1 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/websocket.d.ts +3 -0
- package/dist/src/websocket.js +168 -15
- package/package.json +2 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { sendCommand, sendStatusUpdate } from "../formatter.js";
|
|
2
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
3
|
+
import { getCurrentMessageId, getCurrentTaskId } from "../task-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
const DISCOVER_DEVICES_INTENT = "SearchAllDeviceInfo";
|
|
6
|
+
const DISCOVER_DEVICES_BUNDLE = "com.huawei.hmos.vassistant";
|
|
7
|
+
const DISCOVER_DEVICES_TIMEOUT_MS = 30_000;
|
|
8
|
+
const LOG_TAG = "[GetPCDeviceList]";
|
|
9
|
+
const DISCOVER_DEVICES_STATUS_TEXT = "正在查询设备列表...";
|
|
10
|
+
const DEVICE_TYPE_LABELS = {
|
|
11
|
+
"14": "phone",
|
|
12
|
+
"17": "pad",
|
|
13
|
+
"131": "car",
|
|
14
|
+
"2607": "PC",
|
|
15
|
+
};
|
|
16
|
+
function buildResultText(result) {
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: JSON.stringify(result),
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function normalizeDevices(rawDevices) {
|
|
27
|
+
if (!Array.isArray(rawDevices)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return rawDevices
|
|
31
|
+
.filter((item) => Boolean(item) && typeof item === "object")
|
|
32
|
+
.map((device) => {
|
|
33
|
+
const networkId = typeof device.networkId === "string"
|
|
34
|
+
? device.networkId
|
|
35
|
+
: typeof device.deviceId === "string"
|
|
36
|
+
? device.deviceId
|
|
37
|
+
: "";
|
|
38
|
+
const deviceTypeId = typeof device.deviceTypeId === "string"
|
|
39
|
+
? device.deviceTypeId
|
|
40
|
+
: typeof device.deviceType === "string"
|
|
41
|
+
? device.deviceType
|
|
42
|
+
: "";
|
|
43
|
+
return {
|
|
44
|
+
networkId,
|
|
45
|
+
deviceName: typeof device.deviceName === "string" ? device.deviceName : "",
|
|
46
|
+
deviceTypeId,
|
|
47
|
+
deviceTypeLabel: DEVICE_TYPE_LABELS[deviceTypeId] ?? "unknown",
|
|
48
|
+
nearby: device.nearby === true,
|
|
49
|
+
rawDevice: device,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function inferDesiredDeviceTypes(query) {
|
|
54
|
+
const normalized = query.toLowerCase();
|
|
55
|
+
if (/(pc|computer|desktop|laptop|notebook)/iu.test(normalized) || /电脑|台式机|笔记本/iu.test(query)) {
|
|
56
|
+
return ["2607"];
|
|
57
|
+
}
|
|
58
|
+
if (/(tablet|pad|ipad)/iu.test(normalized) || /平板/iu.test(query)) {
|
|
59
|
+
return ["17"];
|
|
60
|
+
}
|
|
61
|
+
if (/(phone|mobile)/iu.test(normalized) || /手机/iu.test(query)) {
|
|
62
|
+
return ["14"];
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
function sortByNearby(devices) {
|
|
67
|
+
return [...devices].sort((a, b) => Number(b.nearby) - Number(a.nearby));
|
|
68
|
+
}
|
|
69
|
+
function recommendDevices(query, devices) {
|
|
70
|
+
const desiredTypes = inferDesiredDeviceTypes(query);
|
|
71
|
+
if (desiredTypes.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
recommendedDevices: [],
|
|
74
|
+
recommendationReason: "No explicit target device type was detected in the query.",
|
|
75
|
+
needsUserSelection: devices.length > 1,
|
|
76
|
+
selectionPrompt: devices.length > 1
|
|
77
|
+
? "The query does not identify a unique device type. Ask the user to choose a target device by deviceName or networkId before sending a cross-device task."
|
|
78
|
+
: "",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const matches = devices.filter((device) => desiredTypes.includes(device.deviceTypeId));
|
|
82
|
+
if (matches.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
recommendedDevices: [],
|
|
85
|
+
recommendationReason: `No discovered device matches requested type(s): ${desiredTypes.join(", ")}.`,
|
|
86
|
+
needsUserSelection: false,
|
|
87
|
+
selectionPrompt: "",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const sortedMatches = sortByNearby(matches);
|
|
91
|
+
return {
|
|
92
|
+
recommendedDevices: sortedMatches,
|
|
93
|
+
recommendationReason: `Matched requested device type(s): ${desiredTypes.join(", ")}. Nearby devices are ranked first.`,
|
|
94
|
+
needsUserSelection: sortedMatches.length > 1,
|
|
95
|
+
selectionPrompt: sortedMatches.length > 1
|
|
96
|
+
? "Multiple candidate devices match the user request. Ask the user to choose one target device by deviceName or networkId before calling send_cross_device_task."
|
|
97
|
+
: "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function createDiscoverCrossDevicesTool(ctx) {
|
|
101
|
+
const { config, sessionId, taskId, messageId } = ctx;
|
|
102
|
+
return {
|
|
103
|
+
name: "discover_cross_devices",
|
|
104
|
+
label: "发现跨设备协作设备",
|
|
105
|
+
description: `跨设备协作的设备发现工具。
|
|
106
|
+
|
|
107
|
+
当用户明确表达要从另一台设备获取、查找、使用或操作内容时,必须优先调用本工具,例如从 PC、电脑、平板、手机等设备获取文件或查找内容。
|
|
108
|
+
|
|
109
|
+
本工具只做设备发现和目标设备推荐,不会读取副设备文件内容,不会上传文件,也不会真正下发跨端执行任务。`,
|
|
110
|
+
parameters: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
query: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "The user's original cross-device request, used to recommend the target device type.",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ["query"],
|
|
119
|
+
},
|
|
120
|
+
async execute(_toolCallId, params) {
|
|
121
|
+
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
122
|
+
logger.log(`${LOG_TAG} tool invoked`);
|
|
123
|
+
if (!query) {
|
|
124
|
+
return buildResultText({
|
|
125
|
+
success: false,
|
|
126
|
+
rawOutputs: null,
|
|
127
|
+
devices: [],
|
|
128
|
+
recommendedDevices: [],
|
|
129
|
+
recommendationReason: "",
|
|
130
|
+
message: "Missing required parameter: query.",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
134
|
+
const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
|
|
135
|
+
const wsManager = getXYWebSocketManager(config);
|
|
136
|
+
const command = {
|
|
137
|
+
header: {
|
|
138
|
+
namespace: "Common",
|
|
139
|
+
name: "Action",
|
|
140
|
+
},
|
|
141
|
+
payload: {
|
|
142
|
+
needUploadResult: true,
|
|
143
|
+
actionResponseConfig: {},
|
|
144
|
+
response: [],
|
|
145
|
+
executeParam: {
|
|
146
|
+
executeMode: "background",
|
|
147
|
+
intentName: DISCOVER_DEVICES_INTENT,
|
|
148
|
+
intentParam: {},
|
|
149
|
+
bundleName: DISCOVER_DEVICES_BUNDLE,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
let timeout;
|
|
155
|
+
let handler;
|
|
156
|
+
let settled = false;
|
|
157
|
+
const cleanup = () => {
|
|
158
|
+
clearTimeout(timeout);
|
|
159
|
+
wsManager.off("data-event", handler);
|
|
160
|
+
};
|
|
161
|
+
const finish = (result) => {
|
|
162
|
+
if (settled) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
settled = true;
|
|
166
|
+
cleanup();
|
|
167
|
+
resolve(buildResultText(result));
|
|
168
|
+
};
|
|
169
|
+
handler = (event) => {
|
|
170
|
+
if (event.intentName !== DISCOVER_DEVICES_INTENT) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const rawOutputs = event.outputs ?? {};
|
|
174
|
+
const code = rawOutputs.code;
|
|
175
|
+
const success = event.status === "success" && String(code) === "0";
|
|
176
|
+
const devices = normalizeDevices(rawOutputs.result?.devices);
|
|
177
|
+
const recommendation = recommendDevices(query, devices);
|
|
178
|
+
logger.log(`${LOG_TAG} completed, success=${success}, deviceCount=${devices.length}, recommendedCount=${recommendation.recommendedDevices.length}`);
|
|
179
|
+
finish({
|
|
180
|
+
success,
|
|
181
|
+
rawOutputs,
|
|
182
|
+
devices,
|
|
183
|
+
recommendedDevices: recommendation.recommendedDevices,
|
|
184
|
+
recommendationReason: recommendation.recommendationReason,
|
|
185
|
+
needsUserSelection: recommendation.needsUserSelection,
|
|
186
|
+
selectionPrompt: recommendation.selectionPrompt,
|
|
187
|
+
message: success
|
|
188
|
+
? recommendation.needsUserSelection
|
|
189
|
+
? `Discovered ${devices.length} device(s). Multiple candidates may match; ask the user to choose the target device before sending a cross-device task.`
|
|
190
|
+
: `Discovered ${devices.length} device(s). The model should choose the final target device based on the user request.`
|
|
191
|
+
: "Device discovery failed on the device side.",
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
timeout = setTimeout(() => {
|
|
195
|
+
logger.log(`${LOG_TAG} timeout waiting UploadExeResult after ${DISCOVER_DEVICES_TIMEOUT_MS}ms`);
|
|
196
|
+
finish({
|
|
197
|
+
success: false,
|
|
198
|
+
rawOutputs: null,
|
|
199
|
+
devices: [],
|
|
200
|
+
recommendedDevices: [],
|
|
201
|
+
recommendationReason: "",
|
|
202
|
+
message: `Device discovery timed out after ${DISCOVER_DEVICES_TIMEOUT_MS / 1000} seconds.`,
|
|
203
|
+
});
|
|
204
|
+
}, DISCOVER_DEVICES_TIMEOUT_MS);
|
|
205
|
+
wsManager.on("data-event", handler);
|
|
206
|
+
sendStatusUpdate({
|
|
207
|
+
config,
|
|
208
|
+
sessionId,
|
|
209
|
+
taskId: currentTaskId,
|
|
210
|
+
messageId: currentMessageId,
|
|
211
|
+
text: DISCOVER_DEVICES_STATUS_TEXT,
|
|
212
|
+
state: "working",
|
|
213
|
+
})
|
|
214
|
+
.then(() => sendCommand({
|
|
215
|
+
config,
|
|
216
|
+
sessionId,
|
|
217
|
+
taskId: currentTaskId,
|
|
218
|
+
messageId: currentMessageId,
|
|
219
|
+
command,
|
|
220
|
+
}))
|
|
221
|
+
.catch((error) => {
|
|
222
|
+
logger.error(`${LOG_TAG} failed to send device discovery command: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
finish({
|
|
224
|
+
success: false,
|
|
225
|
+
rawOutputs: null,
|
|
226
|
+
devices: [],
|
|
227
|
+
recommendedDevices: [],
|
|
228
|
+
recommendationReason: "",
|
|
229
|
+
message: `Failed to send device discovery command: ${error instanceof Error ? error.message : String(error)}`,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -47,7 +47,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
47
47
|
},
|
|
48
48
|
required: [],
|
|
49
49
|
},
|
|
50
|
-
async execute(
|
|
50
|
+
async execute(toolCallId, params) {
|
|
51
51
|
const wsManager = getXYWebSocketManager(config);
|
|
52
52
|
const intentParam = {};
|
|
53
53
|
if (params.startTime !== undefined)
|
|
@@ -96,7 +96,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
96
96
|
return new Promise((resolve, reject) => {
|
|
97
97
|
const timeout = setTimeout(() => {
|
|
98
98
|
wsManager.off("data-event", handler);
|
|
99
|
-
logger.error("超时: 查询通知消息超时(60秒)", { toolCallId:
|
|
99
|
+
logger.error("超时: 查询通知消息超时(60秒)", { toolCallId: toolCallId });
|
|
100
100
|
reject(new Error("查询通知消息超时(60秒)"));
|
|
101
101
|
}, 60000);
|
|
102
102
|
const handler = (event) => {
|
|
@@ -126,6 +126,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
126
126
|
taskId: currentTaskId,
|
|
127
127
|
messageId,
|
|
128
128
|
command,
|
|
129
|
+
toolCallId,
|
|
129
130
|
})
|
|
130
131
|
.then(() => { })
|
|
131
132
|
.catch((error) => {
|
|
@@ -56,7 +56,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
56
56
|
},
|
|
57
57
|
required: [],
|
|
58
58
|
},
|
|
59
|
-
async execute(
|
|
59
|
+
async execute(toolCallId, params) {
|
|
60
60
|
const { category, subCategory } = params;
|
|
61
61
|
// Validate category
|
|
62
62
|
if (category && !VALID_CATEGORIES.includes(category)) {
|
|
@@ -112,7 +112,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
112
112
|
return new Promise((resolve, reject) => {
|
|
113
113
|
const timeout = setTimeout(() => {
|
|
114
114
|
wsManager.off("data-event", handler);
|
|
115
|
-
logger.error("超时: 查询记忆数据超时(60秒)", { toolCallId:
|
|
115
|
+
logger.error("超时: 查询记忆数据超时(60秒)", { toolCallId: toolCallId });
|
|
116
116
|
reject(new Error("查询记忆数据超时(60秒)"));
|
|
117
117
|
}, 60000);
|
|
118
118
|
const handler = (event) => {
|
|
@@ -142,6 +142,7 @@ c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
|
142
142
|
taskId: currentTaskId,
|
|
143
143
|
messageId,
|
|
144
144
|
command,
|
|
145
|
+
toolCallId,
|
|
145
146
|
})
|
|
146
147
|
.then(() => { })
|
|
147
148
|
.catch((error) => {
|
|
@@ -44,7 +44,7 @@ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只
|
|
|
44
44
|
},
|
|
45
45
|
required: [],
|
|
46
46
|
},
|
|
47
|
-
async execute(
|
|
47
|
+
async execute(toolCallId, params) {
|
|
48
48
|
const { status } = params;
|
|
49
49
|
if (status && !["all", "completed", "pending"].includes(status)) {
|
|
50
50
|
throw new ToolInputError('status 参数只能为 "all"、"completed" 或 "pending"');
|
|
@@ -91,7 +91,7 @@ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只
|
|
|
91
91
|
return new Promise((resolve, reject) => {
|
|
92
92
|
const timeout = setTimeout(() => {
|
|
93
93
|
wsManager.off("data-event", handler);
|
|
94
|
-
logger.error("超时: 查询待办任务超时(60秒)", { toolCallId:
|
|
94
|
+
logger.error("超时: 查询待办任务超时(60秒)", { toolCallId: toolCallId });
|
|
95
95
|
reject(new Error("查询待办任务超时(60秒)"));
|
|
96
96
|
}, 60000);
|
|
97
97
|
const handler = (event) => {
|
|
@@ -121,6 +121,7 @@ d. 当只传入 startTime 时,返回该时间点之后的所有任务;当只
|
|
|
121
121
|
taskId: currentTaskId,
|
|
122
122
|
messageId,
|
|
123
123
|
command,
|
|
124
|
+
toolCallId,
|
|
124
125
|
})
|
|
125
126
|
.then(() => { })
|
|
126
127
|
.catch((error) => {
|
|
@@ -40,7 +40,7 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
40
40
|
},
|
|
41
41
|
required: ["queryText"],
|
|
42
42
|
},
|
|
43
|
-
async execute(
|
|
43
|
+
async execute(toolCallId, params) {
|
|
44
44
|
// ===== Validate queryText =====
|
|
45
45
|
if (!params.queryText || typeof params.queryText !== "string" || !params.queryText.trim()) {
|
|
46
46
|
throw new Error("queryText 为必填参数,且不能为空字符串");
|
|
@@ -91,7 +91,7 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
91
91
|
return new Promise((resolve, reject) => {
|
|
92
92
|
const timeout = setTimeout(() => {
|
|
93
93
|
wsManager.off("data-event", handler);
|
|
94
|
-
logger.error("超时: 检索邮件超时(60秒)", { toolCallId:
|
|
94
|
+
logger.error("超时: 检索邮件超时(60秒)", { toolCallId: toolCallId });
|
|
95
95
|
reject(new Error("检索邮件超时(60秒)"));
|
|
96
96
|
}, 60000);
|
|
97
97
|
// Listen for data events from WebSocket
|
|
@@ -124,6 +124,7 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
124
124
|
taskId: currentTaskId,
|
|
125
125
|
messageId,
|
|
126
126
|
command,
|
|
127
|
+
toolCallId,
|
|
127
128
|
})
|
|
128
129
|
.then(() => { })
|
|
129
130
|
.catch((error) => {
|
|
@@ -75,7 +75,7 @@ export function createSearchPhotoGalleryTool(ctx) {
|
|
|
75
75
|
const wsManager = getXYWebSocketManager(config);
|
|
76
76
|
// Search for photos
|
|
77
77
|
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
78
|
-
const outputs = await searchPhotos(wsManager, config, sessionId, currentTaskId, messageId, params.query);
|
|
78
|
+
const outputs = await searchPhotos(wsManager, config, sessionId, currentTaskId, messageId, toolCallId, params.query);
|
|
79
79
|
return {
|
|
80
80
|
content: [
|
|
81
81
|
{
|
|
@@ -91,7 +91,7 @@ export function createSearchPhotoGalleryTool(ctx) {
|
|
|
91
91
|
* Search for photos using query description
|
|
92
92
|
* Returns complete event.outputs object
|
|
93
93
|
*/
|
|
94
|
-
async function searchPhotos(wsManager, config, sessionId, taskId, messageId, query) {
|
|
94
|
+
async function searchPhotos(wsManager, config, sessionId, taskId, messageId, toolCallId, query) {
|
|
95
95
|
const command = {
|
|
96
96
|
header: {
|
|
97
97
|
namespace: "Common",
|
|
@@ -151,6 +151,7 @@ async function searchPhotos(wsManager, config, sessionId, taskId, messageId, que
|
|
|
151
151
|
taskId,
|
|
152
152
|
messageId,
|
|
153
153
|
command,
|
|
154
|
+
toolCallId,
|
|
154
155
|
})
|
|
155
156
|
.then(() => {
|
|
156
157
|
})
|