@microsoft/agents-copilotstudio-client 1.1.0-alpha.9.g154c2c8a32 → 1.1.4-g8d884129e7
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/package.json +3 -3
- package/dist/src/browser.mjs +6 -8
- package/dist/src/browser.mjs.map +4 -4
- package/dist/src/copilotStudioClient.d.ts +30 -6
- package/dist/src/copilotStudioClient.js +152 -102
- package/dist/src/copilotStudioClient.js.map +1 -1
- package/dist/src/copilotStudioWebChat.js +88 -14
- package/dist/src/copilotStudioWebChat.js.map +1 -1
- package/package.json +3 -3
- package/src/copilotStudioClient.ts +157 -113
- package/src/copilotStudioWebChat.ts +97 -16
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createEventSource, EventSourceClient } from 'eventsource-client'
|
|
6
7
|
import { ConnectionSettings } from './connectionSettings'
|
|
7
|
-
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
|
8
8
|
import { getCopilotStudioConnectionUrl, getTokenAudience } from './powerPlatformEnvironment'
|
|
9
9
|
import { Activity, ActivityTypes, ConversationAccount } from '@microsoft/agents-activity'
|
|
10
10
|
import { ExecuteTurnRequest } from './executeTurnRequest'
|
|
@@ -14,11 +14,6 @@ import os from 'os'
|
|
|
14
14
|
|
|
15
15
|
const logger = debug('copilot-studio:client')
|
|
16
16
|
|
|
17
|
-
interface streamRead {
|
|
18
|
-
done: boolean,
|
|
19
|
-
value: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
17
|
/**
|
|
23
18
|
* Client for interacting with Microsoft Copilot Studio services.
|
|
24
19
|
* Provides functionality to start conversations and send messages to Copilot Studio bots.
|
|
@@ -33,8 +28,8 @@ export class CopilotStudioClient {
|
|
|
33
28
|
private conversationId: string = ''
|
|
34
29
|
/** The connection settings for the client. */
|
|
35
30
|
private readonly settings: ConnectionSettings
|
|
36
|
-
/** The
|
|
37
|
-
private readonly
|
|
31
|
+
/** The authenticaton token. */
|
|
32
|
+
private readonly token: string
|
|
38
33
|
|
|
39
34
|
/**
|
|
40
35
|
* Returns the scope URL needed to connect to Copilot Studio from the connection settings.
|
|
@@ -51,79 +46,97 @@ export class CopilotStudioClient {
|
|
|
51
46
|
*/
|
|
52
47
|
constructor (settings: ConnectionSettings, token: string) {
|
|
53
48
|
this.settings = settings
|
|
54
|
-
this.
|
|
55
|
-
this.client.defaults.headers.common.Authorization = `Bearer ${token}`
|
|
56
|
-
this.client.defaults.headers.common['User-Agent'] = CopilotStudioClient.getProductInfo()
|
|
49
|
+
this.token = token
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (islandExperimentalUrl) {
|
|
69
|
-
this.settings.directConnectUrl = islandExperimentalUrl
|
|
70
|
-
logger.debug(`Island Experimental URL: ${islandExperimentalUrl}`)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this.conversationId = response.headers?.[CopilotStudioClient.conversationIdHeaderKey] ?? ''
|
|
75
|
-
if (this.conversationId) {
|
|
76
|
-
logger.debug(`Conversation ID: ${this.conversationId}`)
|
|
77
|
-
}
|
|
52
|
+
/**
|
|
53
|
+
* Streams activities from the Copilot Studio service using eventsource-client.
|
|
54
|
+
* @param url The connection URL for Copilot Studio.
|
|
55
|
+
* @param body Optional. The request body (for POST).
|
|
56
|
+
* @param method Optional. The HTTP method (default: POST).
|
|
57
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
58
|
+
*/
|
|
59
|
+
private async * postRequestAsync (url: string, body?: any, method: string = 'POST'): AsyncGenerator<Activity> {
|
|
60
|
+
logger.debug(`>>> SEND TO ${url}`)
|
|
78
61
|
|
|
79
|
-
const
|
|
80
|
-
delete sanitizedHeaders['Authorization']
|
|
81
|
-
delete sanitizedHeaders[CopilotStudioClient.conversationIdHeaderKey]
|
|
82
|
-
logger.debug('Headers received:', sanitizedHeaders)
|
|
62
|
+
const streamMap = new Map<string, { text: string, sequence: number }[]>()
|
|
83
63
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
64
|
+
const eventSource: EventSourceClient = createEventSource({
|
|
65
|
+
url,
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${this.token}`,
|
|
68
|
+
'User-Agent': CopilotStudioClient.getProductInfo(),
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
Accept: 'text/event-stream'
|
|
71
|
+
},
|
|
72
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
73
|
+
method,
|
|
74
|
+
fetch: async (url, init) => {
|
|
75
|
+
const response = await fetch(url, init)
|
|
76
|
+
this.processResponseHeaders(response.headers)
|
|
77
|
+
return response
|
|
95
78
|
}
|
|
96
|
-
|
|
97
|
-
result += value
|
|
79
|
+
})
|
|
98
80
|
|
|
99
|
-
|
|
100
|
-
|
|
81
|
+
try {
|
|
82
|
+
for await (const { data, event } of eventSource) {
|
|
83
|
+
if (data && event === 'activity') {
|
|
84
|
+
try {
|
|
85
|
+
const activity = Activity.fromJson(data)
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
87
|
+
// check to see if this activity is part of the streamed response, in which case we need to accumulate the text
|
|
88
|
+
const streamingEntity = activity.entities?.find(e => e.type === 'streaminfo' && e.streamType === 'streaming')
|
|
89
|
+
switch (activity.type) {
|
|
90
|
+
case ActivityTypes.Message:
|
|
91
|
+
if (!this.conversationId.trim()) { // Did not get it from the header.
|
|
92
|
+
this.conversationId = activity.conversation?.id ?? ''
|
|
93
|
+
logger.debug(`Conversation ID: ${this.conversationId}`)
|
|
94
|
+
}
|
|
95
|
+
yield activity
|
|
96
|
+
break
|
|
97
|
+
case ActivityTypes.Typing:
|
|
98
|
+
logger.debug(`Activity type: ${activity.type}`)
|
|
99
|
+
// Accumulate the text as it comes in from the stream.
|
|
100
|
+
// This also accounts for the "old style" of streaming where the stream info is in channelData.
|
|
101
|
+
if (streamingEntity || activity.channelData?.streamType === 'streaming') {
|
|
102
|
+
const text = activity.text ?? ''
|
|
103
|
+
const id = (streamingEntity?.streamId ?? activity.channelData?.streamId)
|
|
104
|
+
const sequence = (streamingEntity?.streamSequence ?? activity.channelData?.streamSequence)
|
|
105
|
+
// Accumulate the text chunks based on stream ID and sequence number.
|
|
106
|
+
if (id && sequence) {
|
|
107
|
+
if (streamMap.has(id)) {
|
|
108
|
+
const existing = streamMap.get(id)!
|
|
109
|
+
existing.push({ text, sequence })
|
|
110
|
+
streamMap.set(id, existing)
|
|
111
|
+
} else {
|
|
112
|
+
streamMap.set(id, [{ text, sequence }])
|
|
113
|
+
}
|
|
114
|
+
activity.text = streamMap.get(id)?.sort((a, b) => a.sequence - b.sequence).map(item => item.text).join('') || ''
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
yield activity
|
|
118
|
+
break
|
|
119
|
+
default:
|
|
120
|
+
logger.debug(`Activity type: ${activity.type}`)
|
|
121
|
+
yield activity
|
|
122
|
+
break
|
|
116
123
|
}
|
|
117
|
-
}
|
|
118
|
-
logger.
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.error('Failed to parse activity:', error)
|
|
119
126
|
}
|
|
120
|
-
}
|
|
121
|
-
logger.
|
|
122
|
-
|
|
127
|
+
} else if (event === 'end') {
|
|
128
|
+
logger.debug('Stream complete')
|
|
129
|
+
break
|
|
123
130
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
|
|
132
|
+
if (eventSource.readyState === 'closed') {
|
|
133
|
+
logger.debug('Connection closed')
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
eventSource.close()
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
/**
|
|
@@ -146,43 +159,83 @@ export class CopilotStudioClient {
|
|
|
146
159
|
return userAgent
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
private processResponseHeaders (responseHeaders: Headers): void {
|
|
163
|
+
if (this.settings.useExperimentalEndpoint && !this.settings.directConnectUrl?.trim()) {
|
|
164
|
+
const islandExperimentalUrl = responseHeaders?.get(CopilotStudioClient.islandExperimentalUrlHeaderKey)
|
|
165
|
+
if (islandExperimentalUrl) {
|
|
166
|
+
this.settings.directConnectUrl = islandExperimentalUrl
|
|
167
|
+
logger.debug(`Island Experimental URL: ${islandExperimentalUrl}`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.conversationId = responseHeaders?.get(CopilotStudioClient.conversationIdHeaderKey) ?? ''
|
|
172
|
+
if (this.conversationId) {
|
|
173
|
+
logger.debug(`Conversation ID: ${this.conversationId}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sanitizedHeaders = new Headers()
|
|
177
|
+
responseHeaders.forEach((value, key) => {
|
|
178
|
+
if (key.toLowerCase() !== 'authorization' && key.toLowerCase() !== CopilotStudioClient.conversationIdHeaderKey.toLowerCase()) {
|
|
179
|
+
sanitizedHeaders.set(key, value)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
logger.debug('Headers received:', sanitizedHeaders)
|
|
183
|
+
}
|
|
184
|
+
|
|
149
185
|
/**
|
|
150
186
|
* Starts a new conversation with the Copilot Studio service.
|
|
151
187
|
* @param emitStartConversationEvent Whether to emit a start conversation event. Defaults to true.
|
|
152
|
-
* @returns
|
|
188
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
153
189
|
*/
|
|
154
|
-
public async
|
|
190
|
+
public async * startConversationStreaming (emitStartConversationEvent: boolean = true): AsyncGenerator<Activity> {
|
|
155
191
|
const uriStart: string = getCopilotStudioConnectionUrl(this.settings)
|
|
156
192
|
const body = { emitStartConversationEvent }
|
|
157
193
|
|
|
158
|
-
const config: AxiosRequestConfig = {
|
|
159
|
-
method: 'post',
|
|
160
|
-
url: uriStart,
|
|
161
|
-
headers: {
|
|
162
|
-
Accept: 'text/event-stream',
|
|
163
|
-
'Content-Type': 'application/json',
|
|
164
|
-
},
|
|
165
|
-
data: body,
|
|
166
|
-
responseType: 'stream',
|
|
167
|
-
adapter: 'fetch'
|
|
168
|
-
}
|
|
169
|
-
|
|
170
194
|
logger.info('Starting conversation ...')
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
195
|
+
|
|
196
|
+
yield * this.postRequestAsync(uriStart, body, 'POST')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Sends an activity to the Copilot Studio service and retrieves the response activities.
|
|
201
|
+
* @param activity The activity to send.
|
|
202
|
+
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
203
|
+
* @returns An async generator yielding the Agent's Activities.
|
|
204
|
+
*/
|
|
205
|
+
public async * sendActivityStreaming (activity: Activity, conversationId: string = this.conversationId) : AsyncGenerator<Activity> {
|
|
206
|
+
const localConversationId = activity.conversation?.id ?? conversationId
|
|
207
|
+
const uriExecute = getCopilotStudioConnectionUrl(this.settings, localConversationId)
|
|
208
|
+
const qbody: ExecuteTurnRequest = new ExecuteTurnRequest(activity)
|
|
209
|
+
|
|
210
|
+
logger.info('Sending activity...', activity)
|
|
211
|
+
yield * this.postRequestAsync(uriExecute, qbody, 'POST')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Starts a new conversation with the Copilot Studio service.
|
|
216
|
+
* @param emitStartConversationEvent Whether to emit a start conversation event. Defaults to true.
|
|
217
|
+
* @returns A promise yielding an array of activities.
|
|
218
|
+
* @deprecated Use startConversationStreaming instead.
|
|
219
|
+
*/
|
|
220
|
+
public async startConversationAsync (emitStartConversationEvent: boolean = true): Promise<Activity[]> {
|
|
221
|
+
const result: Activity[] = []
|
|
222
|
+
for await (const value of this.startConversationStreaming(emitStartConversationEvent)) {
|
|
223
|
+
result.push(value)
|
|
224
|
+
}
|
|
225
|
+
return result
|
|
175
226
|
}
|
|
176
227
|
|
|
177
228
|
/**
|
|
178
229
|
* Sends a question to the Copilot Studio service and retrieves the response activities.
|
|
179
230
|
* @param question The question to ask.
|
|
180
231
|
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
181
|
-
* @returns A promise
|
|
232
|
+
* @returns A promise yielding an array of activities.
|
|
233
|
+
* @deprecated Use sendActivityStreaming instead.
|
|
182
234
|
*/
|
|
183
|
-
public async askQuestionAsync (question: string, conversationId
|
|
235
|
+
public async askQuestionAsync (question: string, conversationId?: string) : Promise<Activity[]> {
|
|
236
|
+
const localConversationId = conversationId?.trim() ? conversationId : this.conversationId
|
|
184
237
|
const conversationAccount: ConversationAccount = {
|
|
185
|
-
id:
|
|
238
|
+
id: localConversationId
|
|
186
239
|
}
|
|
187
240
|
const activityObj = {
|
|
188
241
|
type: 'message',
|
|
@@ -191,34 +244,25 @@ export class CopilotStudioClient {
|
|
|
191
244
|
}
|
|
192
245
|
const activity = Activity.fromObject(activityObj)
|
|
193
246
|
|
|
194
|
-
|
|
247
|
+
const result: Activity[] = []
|
|
248
|
+
for await (const value of this.sendActivityStreaming(activity, conversationId)) {
|
|
249
|
+
result.push(value)
|
|
250
|
+
}
|
|
251
|
+
return result
|
|
195
252
|
}
|
|
196
253
|
|
|
197
254
|
/**
|
|
198
255
|
* Sends an activity to the Copilot Studio service and retrieves the response activities.
|
|
199
256
|
* @param activity The activity to send.
|
|
200
257
|
* @param conversationId The ID of the conversation. Defaults to the current conversation ID.
|
|
201
|
-
* @returns A promise
|
|
258
|
+
* @returns A promise yielding an array of activities.
|
|
259
|
+
* @deprecated Use sendActivityStreaming instead.
|
|
202
260
|
*/
|
|
203
|
-
public async sendActivity (activity: Activity, conversationId: string = this.conversationId) {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const config: AxiosRequestConfig = {
|
|
209
|
-
method: 'post',
|
|
210
|
-
url: uriExecute,
|
|
211
|
-
headers: {
|
|
212
|
-
Accept: 'text/event-stream',
|
|
213
|
-
'Content-Type': 'application/json',
|
|
214
|
-
},
|
|
215
|
-
data: qbody,
|
|
216
|
-
responseType: 'stream',
|
|
217
|
-
adapter: 'fetch'
|
|
261
|
+
public async sendActivity (activity: Activity, conversationId: string = this.conversationId) : Promise<Activity[]> {
|
|
262
|
+
const result: Activity[] = []
|
|
263
|
+
for await (const value of this.sendActivityStreaming(activity, conversationId)) {
|
|
264
|
+
result.push(value)
|
|
218
265
|
}
|
|
219
|
-
|
|
220
|
-
const values = await this.postRequestAsync(config)
|
|
221
|
-
logger.info(`Received ${values.length} activities.`, values)
|
|
222
|
-
return values
|
|
266
|
+
return result
|
|
223
267
|
}
|
|
224
268
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { v4 as uuid } from 'uuid'
|
|
7
7
|
|
|
8
|
-
import { Activity, ConversationAccount } from '@microsoft/agents-activity'
|
|
8
|
+
import { Activity, Attachment, ConversationAccount } from '@microsoft/agents-activity'
|
|
9
9
|
import { Observable, BehaviorSubject, type Subscriber } from 'rxjs'
|
|
10
10
|
|
|
11
11
|
import { CopilotStudioClient } from './copilotStudioClient'
|
|
@@ -201,7 +201,7 @@ export class CopilotStudioWebChat {
|
|
|
201
201
|
static createConnection (
|
|
202
202
|
client: CopilotStudioClient,
|
|
203
203
|
settings?: CopilotStudioWebChatSettings
|
|
204
|
-
):CopilotStudioWebChatConnection {
|
|
204
|
+
): CopilotStudioWebChatConnection {
|
|
205
205
|
logger.info('--> Creating connection between Copilot Studio and WebChat ...')
|
|
206
206
|
let sequence = 0
|
|
207
207
|
let activitySubscriber: Subscriber<Partial<Activity>> | undefined
|
|
@@ -211,17 +211,25 @@ export class CopilotStudioWebChat {
|
|
|
211
211
|
const activity$ = createObservable<Partial<Activity>>(async (subscriber) => {
|
|
212
212
|
activitySubscriber = subscriber
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
const handleAcknowledgementOnce = async (): Promise<void> => {
|
|
215
215
|
connectionStatus$.next(2)
|
|
216
|
-
|
|
216
|
+
await 0 // Webchat requires an extra tick to process the connection status change
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
logger.debug('--> Connection established.')
|
|
220
220
|
notifyTyping()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
|
|
222
|
+
if (connectionStatus$.value < 2) {
|
|
223
|
+
for await (const activity of client.startConversationStreaming()) {
|
|
224
|
+
delete activity.replyToId
|
|
225
|
+
if (!conversation && activity.conversation) {
|
|
226
|
+
conversation = activity.conversation
|
|
227
|
+
await handleAcknowledgementOnce()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
notifyActivity(activity)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
225
233
|
})
|
|
226
234
|
|
|
227
235
|
const notifyActivity = (activity: Partial<Activity>) => {
|
|
@@ -230,9 +238,10 @@ export class CopilotStudioWebChat {
|
|
|
230
238
|
timestamp: new Date().toISOString(),
|
|
231
239
|
channelData: {
|
|
232
240
|
...activity.channelData,
|
|
233
|
-
'webchat:sequence-id': sequence
|
|
241
|
+
'webchat:sequence-id': sequence,
|
|
234
242
|
},
|
|
235
243
|
}
|
|
244
|
+
sequence++
|
|
236
245
|
logger.debug(`Notify '${newActivity.type}' activity to WebChat:`, newActivity)
|
|
237
246
|
activitySubscriber?.next(newActivity)
|
|
238
247
|
}
|
|
@@ -264,22 +273,26 @@ export class CopilotStudioWebChat {
|
|
|
264
273
|
|
|
265
274
|
return createObservable<string>(async (subscriber) => {
|
|
266
275
|
try {
|
|
267
|
-
const id = uuid()
|
|
268
|
-
|
|
269
276
|
logger.info('--> Sending activity to Copilot Studio ...')
|
|
277
|
+
const newActivity = Activity.fromObject({
|
|
278
|
+
...activity,
|
|
279
|
+
id: uuid(),
|
|
280
|
+
attachments: await processAttachments(activity)
|
|
281
|
+
})
|
|
270
282
|
|
|
271
|
-
notifyActivity(
|
|
283
|
+
notifyActivity(newActivity)
|
|
272
284
|
notifyTyping()
|
|
273
285
|
|
|
274
|
-
|
|
286
|
+
// Notify WebChat immediately that the message was sent
|
|
287
|
+
subscriber.next(newActivity.id!)
|
|
275
288
|
|
|
276
|
-
|
|
289
|
+
// Stream the agent's response, but don't block the UI
|
|
290
|
+
for await (const responseActivity of client.sendActivityStreaming(newActivity)) {
|
|
277
291
|
notifyActivity(responseActivity)
|
|
292
|
+
logger.info('<-- Activity received correctly from Copilot Studio.')
|
|
278
293
|
}
|
|
279
294
|
|
|
280
|
-
subscriber.next(id)
|
|
281
295
|
subscriber.complete()
|
|
282
|
-
logger.info('--> Activity received correctly from Copilot Studio.')
|
|
283
296
|
} catch (error) {
|
|
284
297
|
logger.error('Error sending Activity to Copilot Studio:', error)
|
|
285
298
|
subscriber.error(error)
|
|
@@ -299,6 +312,74 @@ export class CopilotStudioWebChat {
|
|
|
299
312
|
}
|
|
300
313
|
}
|
|
301
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Processes activity attachments.
|
|
317
|
+
* @param activity The activity to process for attachments.
|
|
318
|
+
* @returns A promise that resolves to the activity with all attachments converted.
|
|
319
|
+
*/
|
|
320
|
+
async function processAttachments (activity: Activity): Promise<Attachment[]> {
|
|
321
|
+
if (activity.type !== 'message' || !activity.attachments?.length) {
|
|
322
|
+
return activity.attachments || []
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const attachments: Attachment[] = []
|
|
326
|
+
for (const attachment of activity.attachments) {
|
|
327
|
+
const processed = await processBlobAttachment(attachment)
|
|
328
|
+
attachments.push(processed)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return attachments
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Processes a blob attachment to convert its content URL to a data URL.
|
|
336
|
+
* @param attachment The attachment to process.
|
|
337
|
+
* @returns A promise that resolves to the processed attachment.
|
|
338
|
+
*/
|
|
339
|
+
async function processBlobAttachment (attachment: Attachment): Promise<Attachment> {
|
|
340
|
+
let newContentUrl = attachment.contentUrl
|
|
341
|
+
if (!newContentUrl?.startsWith('blob:')) {
|
|
342
|
+
return attachment
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const response = await fetch(newContentUrl)
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
throw new Error(`Failed to fetch blob URL: ${response.status} ${response.statusText}`)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const blob = await response.blob()
|
|
352
|
+
const arrayBuffer = await blob.arrayBuffer()
|
|
353
|
+
const base64 = arrayBufferToBase64(arrayBuffer)
|
|
354
|
+
newContentUrl = `data:${blob.type};base64,${base64}`
|
|
355
|
+
} catch (error) {
|
|
356
|
+
newContentUrl = attachment.contentUrl
|
|
357
|
+
logger.error('Error processing blob attachment:', newContentUrl, error)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { ...attachment, contentUrl: newContentUrl }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Converts an ArrayBuffer to a base64 string.
|
|
365
|
+
* @param buffer The ArrayBuffer to convert.
|
|
366
|
+
* @returns The base64 encoded string.
|
|
367
|
+
*/
|
|
368
|
+
function arrayBufferToBase64 (buffer: ArrayBuffer): string {
|
|
369
|
+
// Node.js environment
|
|
370
|
+
const BufferClass = typeof globalThis.Buffer === 'function' ? globalThis.Buffer : undefined
|
|
371
|
+
if (BufferClass && typeof BufferClass.from === 'function') {
|
|
372
|
+
return BufferClass.from(buffer).toString('base64')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Browser environment
|
|
376
|
+
let binary = ''
|
|
377
|
+
for (const byte of new Uint8Array(buffer)) {
|
|
378
|
+
binary += String.fromCharCode(byte)
|
|
379
|
+
}
|
|
380
|
+
return btoa(binary)
|
|
381
|
+
}
|
|
382
|
+
|
|
302
383
|
/**
|
|
303
384
|
* Creates an RxJS Observable that wraps an asynchronous function execution.
|
|
304
385
|
*
|