@microsoft/agents-copilotstudio-client 0.6.1 → 0.6.11
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/browser.mjs +9 -9
- package/dist/src/browser.mjs.map +4 -4
- package/dist/src/copilotStudioClient.d.ts +8 -4
- package/dist/src/copilotStudioClient.js +34 -17
- package/dist/src/copilotStudioClient.js.map +1 -1
- package/dist/src/copilotStudioWebChat.d.ts +146 -28
- package/dist/src/copilotStudioWebChat.js +157 -27
- package/dist/src/copilotStudioWebChat.js.map +1 -1
- package/dist/src/powerPlatformEnvironment.js +13 -3
- package/dist/src/powerPlatformEnvironment.js.map +1 -1
- package/package.json +3 -4
- package/src/copilotStudioClient.ts +33 -19
- package/src/copilotStudioWebChat.ts +220 -56
- package/src/powerPlatformEnvironment.ts +14 -3
|
@@ -6,106 +6,229 @@
|
|
|
6
6
|
import { v4 as uuid } from 'uuid'
|
|
7
7
|
|
|
8
8
|
import { Activity, ConversationAccount } from '@microsoft/agents-activity'
|
|
9
|
-
import { Observable, BehaviorSubject, type
|
|
9
|
+
import { Observable, BehaviorSubject, type Subscriber } from 'rxjs'
|
|
10
10
|
|
|
11
11
|
import { CopilotStudioClient } from './copilotStudioClient'
|
|
12
|
+
import { debug } from '@microsoft/agents-activity/src/logger'
|
|
12
13
|
|
|
14
|
+
const logger = debug('copilot-studio:webchat')
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration settings for the Copilot Studio WebChat connection.
|
|
18
|
+
* These settings control the behavior and appearance of the WebChat interface
|
|
19
|
+
* when connected to the Copilot Studio service.
|
|
20
|
+
*/
|
|
13
21
|
export interface CopilotStudioWebChatSettings {
|
|
14
22
|
/**
|
|
15
|
-
* Whether to show typing indicators in the WebChat.
|
|
16
|
-
*
|
|
23
|
+
* Whether to show typing indicators in the WebChat when the agent is processing a response.
|
|
24
|
+
* When enabled, users will see a typing indicator while waiting for the agent's reply,
|
|
25
|
+
* providing visual feedback that their message is being processed.
|
|
26
|
+
* @default false
|
|
17
27
|
*/
|
|
18
28
|
showTyping?: boolean;
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Represents a connection interface for integrating Copilot Studio with WebChat.
|
|
33
|
+
* This interface provides the necessary methods and observables to facilitate
|
|
34
|
+
* bidirectional communication between a WebChat client and the Copilot Studio service.
|
|
35
|
+
*
|
|
36
|
+
* The connection follows the DirectLine protocol pattern, making it compatible with
|
|
37
|
+
* Microsoft Bot Framework WebChat components.
|
|
38
|
+
*/
|
|
21
39
|
export interface CopilotStudioWebChatConnection {
|
|
22
40
|
/**
|
|
23
|
-
* An observable that emits the connection status.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
41
|
+
* An observable that emits the current connection status as numeric values.
|
|
42
|
+
* This allows WebChat clients to monitor and react to connection state changes.
|
|
43
|
+
*
|
|
44
|
+
* Connection status values:
|
|
45
|
+
* - 0: Disconnected - No active connection to the service
|
|
46
|
+
* - 1: Connecting - Attempting to establish connection
|
|
47
|
+
* - 2: Connected - Successfully connected and ready for communication
|
|
48
|
+
*/
|
|
28
49
|
connectionStatus$: BehaviorSubject<number>;
|
|
29
50
|
|
|
30
51
|
/**
|
|
31
|
-
* An observable that emits incoming activities.
|
|
32
|
-
*
|
|
52
|
+
* An observable stream that emits incoming activities from the Copilot Studio service.
|
|
53
|
+
* Each activity represents a message, card, or other interactive element sent by the agent.
|
|
54
|
+
*
|
|
55
|
+
* All emitted activities include:
|
|
56
|
+
* - A timestamp indicating when the activity was received
|
|
57
|
+
* - A 'webchat:sequence-id' in their channelData for proper message ordering
|
|
58
|
+
* - Standard Bot Framework Activity properties (type, text, attachments, etc.)
|
|
33
59
|
*/
|
|
34
60
|
activity$: Observable<Partial<Activity>>;
|
|
35
61
|
|
|
36
62
|
/**
|
|
37
|
-
* Posts
|
|
38
|
-
*
|
|
39
|
-
* Returns an observable that emits the activity ID once the activity is posted.
|
|
63
|
+
* Posts a user activity to the Copilot Studio service and returns an observable
|
|
64
|
+
* that emits the activity ID once the message is successfully sent.
|
|
40
65
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
66
|
+
* The method validates that the activity contains meaningful content and handles
|
|
67
|
+
* the complete message flow including optional typing indicators.
|
|
68
|
+
*
|
|
69
|
+
* @param activity - The user activity to send. Must contain a non-empty text field.
|
|
70
|
+
* @returns An observable that emits the unique activity ID upon successful posting.
|
|
71
|
+
* @throws Error if the activity text is empty or if the connection is not properly initialized.
|
|
43
72
|
*/
|
|
44
73
|
postActivity(activity: Activity): Observable<string>;
|
|
45
74
|
|
|
46
75
|
/**
|
|
47
|
-
*
|
|
48
|
-
* This
|
|
76
|
+
* Gracefully terminates the connection to the Copilot Studio service.
|
|
77
|
+
* This method ensures proper cleanup by completing all active observables
|
|
78
|
+
* and releasing associated resources.
|
|
79
|
+
*
|
|
80
|
+
* After calling this method:
|
|
81
|
+
* - The connectionStatus$ observable will be completed
|
|
82
|
+
* - The activity$ observable will stop emitting new activities
|
|
83
|
+
* - No further activities can be posted through this connection
|
|
49
84
|
*/
|
|
50
85
|
end(): void;
|
|
51
86
|
}
|
|
52
87
|
|
|
53
88
|
/**
|
|
54
|
-
*
|
|
89
|
+
* A utility class that provides WebChat integration capabilities for Copilot Studio services.
|
|
90
|
+
* This class acts as a bridge between Microsoft Bot Framework WebChat and Copilot Studio,
|
|
91
|
+
* enabling seamless communication through a DirectLine-compatible interface.
|
|
55
92
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
93
|
+
* ## Key Features:
|
|
94
|
+
* - DirectLine protocol compatibility for easy WebChat integration
|
|
95
|
+
* - Real-time bidirectional messaging with Copilot Studio agents
|
|
96
|
+
* - Automatic conversation management and message sequencing
|
|
97
|
+
* - Optional typing indicators for enhanced user experience
|
|
98
|
+
* - Observable-based architecture for reactive programming patterns
|
|
99
|
+
*
|
|
100
|
+
* ## Usage Scenarios:
|
|
101
|
+
* - Embedding Copilot Studio agents in web applications
|
|
102
|
+
* - Creating custom chat interfaces with WebChat components
|
|
103
|
+
* - Building conversational AI experiences with Microsoft's bot ecosystem
|
|
104
|
+
*
|
|
105
|
+
* @example Basic WebChat Integration
|
|
106
|
+
* ```typescript
|
|
107
|
+
* import { CopilotStudioClient } from '@microsoft/agents-copilotstudio-client';
|
|
108
|
+
* import { CopilotStudioWebChat } from '@microsoft/agents-copilotstudio-client';
|
|
109
|
+
*
|
|
110
|
+
* // Initialize the Copilot Studio client
|
|
111
|
+
* const client = new CopilotStudioClient({
|
|
112
|
+
* botId: 'your-bot-id',
|
|
113
|
+
* tenantId: 'your-tenant-id'
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* // Create a WebChat-compatible connection
|
|
117
|
+
* const directLine = CopilotStudioWebChat.createConnection(client, {
|
|
118
|
+
* showTyping: true
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* // Integrate with WebChat
|
|
59
122
|
* window.WebChat.renderWebChat({
|
|
60
|
-
* directLine:
|
|
61
|
-
*
|
|
123
|
+
* directLine: directLine,
|
|
124
|
+
* // ... other WebChat options
|
|
125
|
+
* }, document.getElementById('webchat'));
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @example Advanced Usage with Connection Monitoring
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const connection = CopilotStudioWebChat.createConnection(client);
|
|
131
|
+
*
|
|
132
|
+
* // Monitor connection status
|
|
133
|
+
* connection.connectionStatus$.subscribe(status => {
|
|
134
|
+
* switch (status) {
|
|
135
|
+
* case 0: console.log('Disconnected'); break;
|
|
136
|
+
* case 1: console.log('Connecting...'); break;
|
|
137
|
+
* case 2: console.log('Connected and ready'); break;
|
|
138
|
+
* }
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* // Listen for incoming activities
|
|
142
|
+
* connection.activity$.subscribe(activity => {
|
|
143
|
+
* console.log('Received activity:', activity);
|
|
144
|
+
* });
|
|
62
145
|
* ```
|
|
63
146
|
*/
|
|
64
147
|
export class CopilotStudioWebChat {
|
|
65
148
|
/**
|
|
66
|
-
* Creates a
|
|
67
|
-
*
|
|
68
|
-
*
|
|
149
|
+
* Creates a DirectLine-compatible connection for integrating Copilot Studio with WebChat.
|
|
150
|
+
*
|
|
151
|
+
* This method establishes a real-time communication channel between WebChat and the
|
|
152
|
+
* Copilot Studio service. The returned connection object implements the DirectLine
|
|
153
|
+
* protocol, making it fully compatible with Microsoft Bot Framework WebChat components.
|
|
154
|
+
*
|
|
155
|
+
* ## Connection Lifecycle:
|
|
156
|
+
* 1. **Initialization**: Creates observables for connection status and activity streaming
|
|
157
|
+
* 2. **Conversation Start**: Automatically initiates conversation when first activity is posted
|
|
158
|
+
* 3. **Message Flow**: Handles bidirectional message exchange with proper sequencing
|
|
159
|
+
* 4. **Cleanup**: Provides graceful connection termination
|
|
69
160
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
161
|
+
* ## Message Processing:
|
|
162
|
+
* - User messages are validated and sent to Copilot Studio
|
|
163
|
+
* - Agent responses are received and formatted for WebChat
|
|
164
|
+
* - All activities include timestamps and sequence IDs for proper ordering
|
|
165
|
+
* - Optional typing indicators provide visual feedback during processing
|
|
166
|
+
*
|
|
167
|
+
* @param client - A configured CopilotStudioClient instance that handles the underlying
|
|
168
|
+
* communication with the Copilot Studio service. This client should be
|
|
169
|
+
* properly authenticated and configured with the target bot details.
|
|
170
|
+
*
|
|
171
|
+
* @param settings - Optional configuration settings that control the behavior of the
|
|
172
|
+
* WebChat connection. These settings allow customization of features
|
|
173
|
+
* like typing indicators and other user experience enhancements.
|
|
174
|
+
*
|
|
175
|
+
* @returns A new CopilotStudioWebChatConnection instance that can be passed directly
|
|
176
|
+
* to WebChat's renderWebChat function as the directLine parameter. The
|
|
177
|
+
* connection is immediately ready for use and will automatically manage
|
|
178
|
+
* the conversation lifecycle.
|
|
179
|
+
*
|
|
180
|
+
* @throws Error if the provided client is not properly configured or if there are
|
|
181
|
+
* issues establishing the initial connection to the Copilot Studio service.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* const connection = CopilotStudioWebChat.createConnection(client, {
|
|
186
|
+
* showTyping: true
|
|
187
|
+
* });
|
|
188
|
+
*
|
|
189
|
+
* // Use with WebChat
|
|
190
|
+
* window.WebChat.renderWebChat({
|
|
191
|
+
* directLine: connection
|
|
192
|
+
* }, document.getElementById('webchat'));
|
|
193
|
+
* ```
|
|
73
194
|
*/
|
|
74
195
|
static createConnection (
|
|
75
196
|
client: CopilotStudioClient,
|
|
76
197
|
settings?: CopilotStudioWebChatSettings
|
|
77
198
|
):CopilotStudioWebChatConnection {
|
|
199
|
+
logger.info('--> Creating connection between Copilot Studio and WebChat ...')
|
|
78
200
|
let sequence = 0
|
|
79
|
-
let
|
|
201
|
+
let activitySubscriber: Subscriber<Partial<Activity>> | undefined
|
|
80
202
|
let conversation: ConversationAccount | undefined
|
|
81
203
|
|
|
82
204
|
const connectionStatus$ = new BehaviorSubject(0)
|
|
83
|
-
const activity$ =
|
|
84
|
-
|
|
85
|
-
activityObserver = observer
|
|
205
|
+
const activity$ = createObservable<Partial<Activity>>(async (subscriber) => {
|
|
206
|
+
activitySubscriber = subscriber
|
|
86
207
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
notifyTyping()
|
|
93
|
-
const activity = await client.startConversationAsync()
|
|
94
|
-
conversation = activity.conversation
|
|
95
|
-
sequence = 0
|
|
96
|
-
notifyActivity(activity)
|
|
208
|
+
if (connectionStatus$.value < 2) {
|
|
209
|
+
connectionStatus$.next(2)
|
|
210
|
+
return
|
|
97
211
|
}
|
|
98
|
-
|
|
212
|
+
|
|
213
|
+
logger.debug('--> Connection established.')
|
|
214
|
+
notifyTyping()
|
|
215
|
+
const activity = await client.startConversationAsync()
|
|
216
|
+
conversation = activity.conversation
|
|
217
|
+
sequence = 0
|
|
218
|
+
notifyActivity(activity)
|
|
219
|
+
})
|
|
99
220
|
|
|
100
221
|
const notifyActivity = (activity: Partial<Activity>) => {
|
|
101
|
-
|
|
222
|
+
const newActivity = {
|
|
102
223
|
...activity,
|
|
103
224
|
timestamp: new Date().toISOString(),
|
|
104
225
|
channelData: {
|
|
105
226
|
...activity.channelData,
|
|
106
227
|
'webchat:sequence-id': sequence++,
|
|
107
228
|
},
|
|
108
|
-
}
|
|
229
|
+
}
|
|
230
|
+
logger.debug(`Notify '${newActivity.type}' activity to WebChat:`, newActivity)
|
|
231
|
+
activitySubscriber?.next(newActivity)
|
|
109
232
|
}
|
|
110
233
|
|
|
111
234
|
const notifyTyping = () => {
|
|
@@ -123,18 +246,22 @@ export class CopilotStudioWebChat {
|
|
|
123
246
|
connectionStatus$,
|
|
124
247
|
activity$,
|
|
125
248
|
postActivity (activity: Activity) {
|
|
249
|
+
logger.info('--> Preparing to send activity to Copilot Studio ...')
|
|
250
|
+
|
|
126
251
|
if (!activity.text?.trim()) {
|
|
127
252
|
throw new Error('Activity text cannot be empty.')
|
|
128
253
|
}
|
|
129
254
|
|
|
130
|
-
if (!
|
|
131
|
-
throw new Error('Activity
|
|
255
|
+
if (!activitySubscriber) {
|
|
256
|
+
throw new Error('Activity subscriber is not initialized.')
|
|
132
257
|
}
|
|
133
258
|
|
|
134
|
-
return
|
|
259
|
+
return createObservable<string>(async (subscriber) => {
|
|
135
260
|
try {
|
|
136
261
|
const id = uuid()
|
|
137
262
|
|
|
263
|
+
logger.info('--> Sending activity to Copilot Studio ...')
|
|
264
|
+
|
|
138
265
|
notifyActivity({ ...activity, id })
|
|
139
266
|
notifyTyping()
|
|
140
267
|
|
|
@@ -143,22 +270,59 @@ export class CopilotStudioWebChat {
|
|
|
143
270
|
notifyActivity(responseActivity)
|
|
144
271
|
}
|
|
145
272
|
|
|
146
|
-
|
|
147
|
-
|
|
273
|
+
subscriber.next(id)
|
|
274
|
+
subscriber.complete()
|
|
275
|
+
logger.info('--> Activity received correctly from Copilot Studio.')
|
|
148
276
|
} catch (error) {
|
|
149
|
-
|
|
277
|
+
logger.error('Error sending Activity to Copilot Studio:', error)
|
|
278
|
+
subscriber.error(error)
|
|
150
279
|
}
|
|
151
280
|
})
|
|
152
281
|
},
|
|
153
282
|
|
|
154
283
|
end () {
|
|
284
|
+
logger.info('--> Ending connection between Copilot Studio and WebChat ...')
|
|
155
285
|
connectionStatus$.complete()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
activityObserver = undefined
|
|
286
|
+
if (activitySubscriber) {
|
|
287
|
+
activitySubscriber.complete()
|
|
288
|
+
activitySubscriber = undefined
|
|
160
289
|
}
|
|
161
290
|
},
|
|
162
291
|
}
|
|
163
292
|
}
|
|
164
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Creates an RxJS Observable that wraps an asynchronous function execution.
|
|
297
|
+
*
|
|
298
|
+
* This utility function provides a clean way to convert async/await patterns
|
|
299
|
+
* into Observable streams, enabling integration with reactive programming patterns
|
|
300
|
+
* used throughout the WebChat connection implementation.
|
|
301
|
+
*
|
|
302
|
+
* The created Observable handles promise resolution and rejection automatically,
|
|
303
|
+
* converting them to appropriate next/error signals for subscribers.
|
|
304
|
+
*
|
|
305
|
+
* @template T - The type of value that the observable will emit
|
|
306
|
+
* @param fn - An asynchronous function that receives a Subscriber and performs
|
|
307
|
+
* the desired async operation. The function should call subscriber.next()
|
|
308
|
+
* with results and subscriber.complete() when finished.
|
|
309
|
+
* @returns A new Observable that executes the provided function and emits its results
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* const dataObservable = createObservable<string>(async (subscriber) => {
|
|
314
|
+
* try {
|
|
315
|
+
* const result = await fetchData();
|
|
316
|
+
* subscriber.next(result);
|
|
317
|
+
* subscriber.complete();
|
|
318
|
+
* } catch (error) {
|
|
319
|
+
* subscriber.error(error);
|
|
320
|
+
* }
|
|
321
|
+
* });
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
function createObservable<T> (fn: (subscriber: Subscriber<T>) => void): Observable<T> {
|
|
325
|
+
return new Observable<T>((subscriber: Subscriber<T>) => {
|
|
326
|
+
Promise.resolve(fn(subscriber)).catch((error) => subscriber.error(error))
|
|
327
|
+
})
|
|
328
|
+
}
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { AgentType } from './agentType'
|
|
7
7
|
import { ConnectionSettings } from './connectionSettings'
|
|
8
|
+
import { debug } from '@microsoft/agents-activity/src/logger'
|
|
8
9
|
import { PowerPlatformCloud } from './powerPlatformCloud'
|
|
9
10
|
import { PrebuiltBotStrategy } from './strategies/prebuiltBotStrategy'
|
|
10
11
|
import { PublishedBotStrategy } from './strategies/publishedBotStrategy'
|
|
11
12
|
|
|
13
|
+
const logger = debug('copilot-studio:power-platform')
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Generates the connection URL for Copilot Studio.
|
|
14
17
|
* @param settings - The connection settings.
|
|
@@ -23,12 +26,14 @@ export function getCopilotStudioConnectionUrl (
|
|
|
23
26
|
let cloudValue: PowerPlatformCloud = PowerPlatformCloud.Prod
|
|
24
27
|
|
|
25
28
|
if (settings.directConnectUrl?.trim()) {
|
|
29
|
+
logger.debug(`Using direct connection: ${settings.directConnectUrl}`)
|
|
26
30
|
if (!isValidUri(settings.directConnectUrl)) {
|
|
27
31
|
throw new Error('directConnectUrl must be a valid URL')
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
// FIX for Missing Tenant ID
|
|
31
|
-
if (settings.directConnectUrl.
|
|
35
|
+
if (settings.directConnectUrl.toLowerCase().includes('tenants/00000000-0000-0000-0000-000000000000')) {
|
|
36
|
+
logger.debug(`Direct connection cannot be used, forcing default settings flow. Tenant ID is missing in the URL: ${settings.directConnectUrl}`)
|
|
32
37
|
// Direct connection cannot be used, ejecting and forcing the normal settings flow:
|
|
33
38
|
return getCopilotStudioConnectionUrl({ ...settings, directConnectUrl: '' }, conversationId)
|
|
34
39
|
}
|
|
@@ -58,15 +63,17 @@ export function getCopilotStudioConnectionUrl (
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
if (cloudSetting !== PowerPlatformCloud.Unknown) {
|
|
66
|
+
logger.debug(`Using specified cloud setting: ${cloudSetting}`)
|
|
61
67
|
cloudValue = cloudSetting
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
if (cloudSetting === PowerPlatformCloud.Other) {
|
|
65
71
|
if (isNotEmptyCustomPowerPlatformCloud && isValidUri(settings.customPowerPlatformCloud!)) {
|
|
72
|
+
logger.debug(`Using custom Power Platform cloud: ${settings.customPowerPlatformCloud}`)
|
|
66
73
|
cloudValue = PowerPlatformCloud.Other
|
|
67
74
|
} else {
|
|
68
75
|
throw new Error(
|
|
69
|
-
'customPowerPlatformCloud must be
|
|
76
|
+
'customPowerPlatformCloud must be a valid URL'
|
|
70
77
|
)
|
|
71
78
|
}
|
|
72
79
|
}
|
|
@@ -77,9 +84,11 @@ export function getCopilotStudioConnectionUrl (
|
|
|
77
84
|
if (!Object.values(AgentType).includes(settings.copilotAgentType)) {
|
|
78
85
|
throw new Error('Invalid AgentType enum key')
|
|
79
86
|
} else {
|
|
87
|
+
logger.debug(`Using specified agent type: ${settings.copilotAgentType}`)
|
|
80
88
|
agentType = settings.copilotAgentType
|
|
81
89
|
}
|
|
82
90
|
} else {
|
|
91
|
+
logger.debug('Using default agent type: Published')
|
|
83
92
|
agentType = AgentType.Published
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -98,7 +107,9 @@ export function getCopilotStudioConnectionUrl (
|
|
|
98
107
|
}),
|
|
99
108
|
}[agentType]()
|
|
100
109
|
|
|
101
|
-
|
|
110
|
+
const url = strategy.getConversationUrl(conversationId)
|
|
111
|
+
logger.debug(`Generated Copilot Studio connection URL: ${url}`)
|
|
112
|
+
return url
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
/**
|