@ynhcj/xiaoyi-channel 0.0.10-beta → 0.0.11-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/src/channel.js +3 -1
- package/dist/src/outbound.js +3 -1
- package/dist/src/reply-dispatcher.js +0 -5
- package/dist/src/tools/modify-note-tool.d.ts +9 -0
- package/dist/src/tools/modify-note-tool.js +163 -0
- package/dist/src/tools/search-contact-tool.d.ts +5 -0
- package/dist/src/tools/search-contact-tool.js +147 -0
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -5,8 +5,10 @@ import { xyOnboardingAdapter } from "./onboarding.js";
|
|
|
5
5
|
import { locationTool } from "./tools/location-tool.js";
|
|
6
6
|
import { noteTool } from "./tools/note-tool.js";
|
|
7
7
|
import { searchNoteTool } from "./tools/search-note-tool.js";
|
|
8
|
+
import { modifyNoteTool } from "./tools/modify-note-tool.js";
|
|
8
9
|
import { calendarTool } from "./tools/calendar-tool.js";
|
|
9
10
|
import { searchCalendarTool } from "./tools/search-calendar-tool.js";
|
|
11
|
+
import { searchContactTool } from "./tools/search-contact-tool.js";
|
|
10
12
|
/**
|
|
11
13
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
12
14
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -46,7 +48,7 @@ export const xyPlugin = {
|
|
|
46
48
|
},
|
|
47
49
|
outbound: xyOutbound,
|
|
48
50
|
onboarding: xyOnboardingAdapter,
|
|
49
|
-
agentTools: [locationTool, noteTool, searchNoteTool, calendarTool, searchCalendarTool],
|
|
51
|
+
agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool],
|
|
50
52
|
messaging: {
|
|
51
53
|
normalizeTarget: (raw) => {
|
|
52
54
|
const trimmed = raw.trim();
|
package/dist/src/outbound.js
CHANGED
|
@@ -109,8 +109,10 @@ export const xyOutbound = {
|
|
|
109
109
|
const pushService = new XYPushService(config);
|
|
110
110
|
// Extract title (first 57 chars or first line)
|
|
111
111
|
const title = text.split("\n")[0].slice(0, 57);
|
|
112
|
+
// Truncate push content to max length 1000
|
|
113
|
+
const pushText = text.length > 1000 ? text.slice(0, 1000) : text;
|
|
112
114
|
// Send push message (content, title, data, sessionId)
|
|
113
|
-
await pushService.sendPush(
|
|
115
|
+
await pushService.sendPush(pushText, title, undefined, actualTo);
|
|
114
116
|
console.log(`[xyOutbound.sendText] Completed successfully`);
|
|
115
117
|
// Return message info
|
|
116
118
|
return {
|
|
@@ -273,11 +273,6 @@ export function createXYReplyDispatcher(params) {
|
|
|
273
273
|
const text = payload.text ?? "";
|
|
274
274
|
const hasMedia = Boolean(payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0);
|
|
275
275
|
log(`[PARTIAL REPLY] 📝 Partial reply chunk received: session=${sessionId}, taskId=${taskId}`);
|
|
276
|
-
log(`[PARTIAL REPLY] - text.length=${text.length}`);
|
|
277
|
-
log(`[PARTIAL REPLY] - hasMedia=${hasMedia}`);
|
|
278
|
-
if (text.length > 0) {
|
|
279
|
-
log(`[PARTIAL REPLY] - text preview: "${text.slice(0, 200)}"`);
|
|
280
|
-
}
|
|
281
276
|
try {
|
|
282
277
|
if (text.length > 0) {
|
|
283
278
|
await sendReasoningTextUpdate({
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XY modify note tool - appends content to an existing note on user's device.
|
|
3
|
+
* Requires entityId from search_notes tool as prerequisite.
|
|
4
|
+
*
|
|
5
|
+
* Prerequisites:
|
|
6
|
+
* 1. Call search_notes tool first to get the entityId of target note
|
|
7
|
+
* 2. Use the entityId to append content to that note
|
|
8
|
+
*/
|
|
9
|
+
export declare const modifyNoteTool: any;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getLatestSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY modify note tool - appends content to an existing note on user's device.
|
|
7
|
+
* Requires entityId from search_notes tool as prerequisite.
|
|
8
|
+
*
|
|
9
|
+
* Prerequisites:
|
|
10
|
+
* 1. Call search_notes tool first to get the entityId of target note
|
|
11
|
+
* 2. Use the entityId to append content to that note
|
|
12
|
+
*/
|
|
13
|
+
export const modifyNoteTool = {
|
|
14
|
+
name: "modify_note",
|
|
15
|
+
label: "Modify Note",
|
|
16
|
+
description: "在指定备忘录中追加新内容。使用前必须先调用 search_notes 工具获取备忘录的 entityId。参数说明:entityId 是备忘录的唯一标识符(从 search_notes 工具获取),text 是要追加的文本内容。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
entityId: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "备忘录的唯一标识符,必须先通过 search_notes 工具获取",
|
|
23
|
+
},
|
|
24
|
+
text: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "要追加到备忘录的文本内容",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["entityId", "text"],
|
|
30
|
+
},
|
|
31
|
+
async execute(toolCallId, params) {
|
|
32
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🚀 Starting execution`);
|
|
33
|
+
logger.log(`[MODIFY_NOTE_TOOL] - toolCallId: ${toolCallId}`);
|
|
34
|
+
logger.log(`[MODIFY_NOTE_TOOL] - params:`, JSON.stringify(params));
|
|
35
|
+
logger.log(`[MODIFY_NOTE_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
36
|
+
// Validate parameters
|
|
37
|
+
if (!params.entityId || !params.text) {
|
|
38
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ Missing required parameters`);
|
|
39
|
+
throw new Error("Missing required parameters: entityId and text are required");
|
|
40
|
+
}
|
|
41
|
+
// Get session context
|
|
42
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🔍 Attempting to get session context...`);
|
|
43
|
+
const sessionContext = getLatestSessionContext();
|
|
44
|
+
if (!sessionContext) {
|
|
45
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ FAILED: No active session found!`);
|
|
46
|
+
logger.error(`[MODIFY_NOTE_TOOL] - toolCallId: ${toolCallId}`);
|
|
47
|
+
throw new Error("No active XY session found. Modify note tool can only be used during an active conversation.");
|
|
48
|
+
}
|
|
49
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ Session context found`);
|
|
50
|
+
logger.log(`[MODIFY_NOTE_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
51
|
+
logger.log(`[MODIFY_NOTE_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
52
|
+
logger.log(`[MODIFY_NOTE_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
53
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
54
|
+
// Get WebSocket manager
|
|
55
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🔌 Getting WebSocket manager...`);
|
|
56
|
+
const wsManager = getXYWebSocketManager(config);
|
|
57
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ WebSocket manager obtained`);
|
|
58
|
+
// Build ModifyNote command
|
|
59
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📦 Building ModifyNote command...`);
|
|
60
|
+
const command = {
|
|
61
|
+
header: {
|
|
62
|
+
namespace: "Common",
|
|
63
|
+
name: "Action",
|
|
64
|
+
},
|
|
65
|
+
payload: {
|
|
66
|
+
cardParam: {},
|
|
67
|
+
executeParam: {
|
|
68
|
+
executeMode: "background",
|
|
69
|
+
intentName: "ModifyNote",
|
|
70
|
+
bundleName: "com.huawei.hmos.notepad",
|
|
71
|
+
needUnlock: true,
|
|
72
|
+
actionResponse: true,
|
|
73
|
+
appType: "OHOS_APP",
|
|
74
|
+
timeOut: 5,
|
|
75
|
+
intentParam: {
|
|
76
|
+
contentType: "1", // 1 = append mode (追加模式)
|
|
77
|
+
text: params.text,
|
|
78
|
+
entityId: params.entityId,
|
|
79
|
+
},
|
|
80
|
+
permissionId: [],
|
|
81
|
+
achieveType: "INTENT",
|
|
82
|
+
},
|
|
83
|
+
responses: [
|
|
84
|
+
{
|
|
85
|
+
resultCode: "",
|
|
86
|
+
displayText: "",
|
|
87
|
+
ttsText: "",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
needUploadResult: true,
|
|
91
|
+
noHalfPage: false,
|
|
92
|
+
pageControlRelated: false,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
logger.log(`[MODIFY_NOTE_TOOL] - entityId: ${params.entityId}`);
|
|
96
|
+
logger.log(`[MODIFY_NOTE_TOOL] - contentType: 1 (append mode)`);
|
|
97
|
+
logger.log(`[MODIFY_NOTE_TOOL] - text length: ${params.text.length} characters`);
|
|
98
|
+
// Send command and wait for response (60 second timeout)
|
|
99
|
+
logger.log(`[MODIFY_NOTE_TOOL] ⏳ Setting up promise to wait for note modification response...`);
|
|
100
|
+
logger.log(`[MODIFY_NOTE_TOOL] - Timeout: 60 seconds`);
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const timeout = setTimeout(() => {
|
|
103
|
+
logger.error(`[MODIFY_NOTE_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
104
|
+
wsManager.off("data-event", handler);
|
|
105
|
+
reject(new Error("修改备忘录超时(60秒)"));
|
|
106
|
+
}, 60000);
|
|
107
|
+
// Listen for data events from WebSocket
|
|
108
|
+
const handler = (event) => {
|
|
109
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
110
|
+
if (event.intentName === "ModifyNote") {
|
|
111
|
+
logger.log(`[MODIFY_NOTE_TOOL] 🎯 ModifyNote event received`);
|
|
112
|
+
logger.log(`[MODIFY_NOTE_TOOL] - status: ${event.status}`);
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
wsManager.off("data-event", handler);
|
|
115
|
+
if (event.status === "success" && event.outputs) {
|
|
116
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ Note modified successfully`);
|
|
117
|
+
logger.log(`[MODIFY_NOTE_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
118
|
+
// Return the result directly as requested
|
|
119
|
+
const result = event.outputs.result;
|
|
120
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📝 Note updated:`);
|
|
121
|
+
logger.log(`[MODIFY_NOTE_TOOL] - entityId: ${result?.entityId}`);
|
|
122
|
+
logger.log(`[MODIFY_NOTE_TOOL] - title: ${result?.title}`);
|
|
123
|
+
logger.log(`[MODIFY_NOTE_TOOL] - modifiedDate: ${result?.modifiedDate}`);
|
|
124
|
+
resolve({
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: JSON.stringify(result),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ Note modification failed`);
|
|
135
|
+
logger.error(`[MODIFY_NOTE_TOOL] - status: ${event.status}`);
|
|
136
|
+
reject(new Error(`修改备忘录失败: ${event.status}`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
// Register event handler
|
|
141
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
142
|
+
wsManager.on("data-event", handler);
|
|
143
|
+
// Send the command
|
|
144
|
+
logger.log(`[MODIFY_NOTE_TOOL] 📤 Sending ModifyNote command...`);
|
|
145
|
+
sendCommand({
|
|
146
|
+
config,
|
|
147
|
+
sessionId,
|
|
148
|
+
taskId,
|
|
149
|
+
messageId,
|
|
150
|
+
command,
|
|
151
|
+
})
|
|
152
|
+
.then(() => {
|
|
153
|
+
logger.log(`[MODIFY_NOTE_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
154
|
+
})
|
|
155
|
+
.catch((error) => {
|
|
156
|
+
logger.error(`[MODIFY_NOTE_TOOL] ❌ Failed to send command:`, error);
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
wsManager.off("data-event", handler);
|
|
159
|
+
reject(error);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getLatestSessionContext } from "./session-manager.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* XY search contact tool - searches contacts on user's device.
|
|
7
|
+
* Returns matching contact information based on name.
|
|
8
|
+
*/
|
|
9
|
+
export const searchContactTool = {
|
|
10
|
+
name: "search_contact",
|
|
11
|
+
label: "Search Contact",
|
|
12
|
+
description: "搜索用户设备上的联系人信息。根据姓名在通讯录中检索联系人详细信息(包括姓名、电话号码、邮箱、组织、职位等)。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
name: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "联系人姓名,用于在通讯录中检索联系人信息",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["name"],
|
|
22
|
+
},
|
|
23
|
+
async execute(toolCallId, params) {
|
|
24
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🚀 Starting execution`);
|
|
25
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - toolCallId: ${toolCallId}`);
|
|
26
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - params:`, JSON.stringify(params));
|
|
27
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - timestamp: ${new Date().toISOString()}`);
|
|
28
|
+
// Validate parameters
|
|
29
|
+
if (!params.name) {
|
|
30
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Missing required parameter: name`);
|
|
31
|
+
throw new Error("Missing required parameter: name is required");
|
|
32
|
+
}
|
|
33
|
+
// Get session context
|
|
34
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🔍 Attempting to get session context...`);
|
|
35
|
+
const sessionContext = getLatestSessionContext();
|
|
36
|
+
if (!sessionContext) {
|
|
37
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ FAILED: No active session found!`);
|
|
38
|
+
logger.error(`[SEARCH_CONTACT_TOOL] - toolCallId: ${toolCallId}`);
|
|
39
|
+
throw new Error("No active XY session found. Search contact tool can only be used during an active conversation.");
|
|
40
|
+
}
|
|
41
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ Session context found`);
|
|
42
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - sessionId: ${sessionContext.sessionId}`);
|
|
43
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - taskId: ${sessionContext.taskId}`);
|
|
44
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - messageId: ${sessionContext.messageId}`);
|
|
45
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
46
|
+
// Get WebSocket manager
|
|
47
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🔌 Getting WebSocket manager...`);
|
|
48
|
+
const wsManager = getXYWebSocketManager(config);
|
|
49
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ WebSocket manager obtained`);
|
|
50
|
+
// Build SearchContactLocal command
|
|
51
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📦 Building SearchContactLocal command...`);
|
|
52
|
+
const command = {
|
|
53
|
+
header: {
|
|
54
|
+
namespace: "Common",
|
|
55
|
+
name: "Action",
|
|
56
|
+
},
|
|
57
|
+
payload: {
|
|
58
|
+
cardParam: {},
|
|
59
|
+
executeParam: {
|
|
60
|
+
executeMode: "background",
|
|
61
|
+
intentName: "SearchContactLocal",
|
|
62
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
63
|
+
needUnlock: true,
|
|
64
|
+
actionResponse: true,
|
|
65
|
+
appType: "OHOS_APP",
|
|
66
|
+
timeOut: 5,
|
|
67
|
+
intentParam: {
|
|
68
|
+
name: params.name,
|
|
69
|
+
},
|
|
70
|
+
permissionId: [],
|
|
71
|
+
achieveType: "INTENT",
|
|
72
|
+
},
|
|
73
|
+
responses: [
|
|
74
|
+
{
|
|
75
|
+
resultCode: "",
|
|
76
|
+
displayText: "",
|
|
77
|
+
ttsText: "",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
needUploadResult: true,
|
|
81
|
+
noHalfPage: false,
|
|
82
|
+
pageControlRelated: false,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
// Send command and wait for response (60 second timeout)
|
|
86
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ⏳ Setting up promise to wait for contact search response...`);
|
|
87
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - Timeout: 60 seconds`);
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const timeout = setTimeout(() => {
|
|
90
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ⏰ Timeout: No response received within 60 seconds`);
|
|
91
|
+
wsManager.off("data-event", handler);
|
|
92
|
+
reject(new Error("搜索联系人超时(60秒)"));
|
|
93
|
+
}, 60000);
|
|
94
|
+
// Listen for data events from WebSocket
|
|
95
|
+
const handler = (event) => {
|
|
96
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📨 Received data event:`, JSON.stringify(event));
|
|
97
|
+
if (event.intentName === "SearchContactLocal") {
|
|
98
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 🎯 SearchContactLocal event received`);
|
|
99
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - status: ${event.status}`);
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
wsManager.off("data-event", handler);
|
|
102
|
+
if (event.status === "success" && event.outputs) {
|
|
103
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ Contact search completed successfully`);
|
|
104
|
+
logger.log(`[SEARCH_CONTACT_TOOL] - outputs:`, JSON.stringify(event.outputs));
|
|
105
|
+
// Return the result directly as requested
|
|
106
|
+
const result = event.outputs.result;
|
|
107
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📊 Contacts found: ${result?.items?.length || 0} results for name "${params.name}"`);
|
|
108
|
+
resolve({
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: JSON.stringify(result),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Contact search failed`);
|
|
119
|
+
logger.error(`[SEARCH_CONTACT_TOOL] - status: ${event.status}`);
|
|
120
|
+
reject(new Error(`搜索联系人失败: ${event.status}`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
// Register event handler
|
|
125
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📡 Registering data-event handler on WebSocket manager`);
|
|
126
|
+
wsManager.on("data-event", handler);
|
|
127
|
+
// Send the command
|
|
128
|
+
logger.log(`[SEARCH_CONTACT_TOOL] 📤 Sending SearchContactLocal command...`);
|
|
129
|
+
sendCommand({
|
|
130
|
+
config,
|
|
131
|
+
sessionId,
|
|
132
|
+
taskId,
|
|
133
|
+
messageId,
|
|
134
|
+
command,
|
|
135
|
+
})
|
|
136
|
+
.then(() => {
|
|
137
|
+
logger.log(`[SEARCH_CONTACT_TOOL] ✅ Command sent successfully, waiting for response...`);
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
logger.error(`[SEARCH_CONTACT_TOOL] ❌ Failed to send command:`, error);
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
wsManager.off("data-event", handler);
|
|
143
|
+
reject(error);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|