@ray-js/t-agent-plugin-aistream 0.2.6-beta-8 → 0.2.7-beta.1
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/AIStreamTypes.d.ts +37 -35
- package/dist/AIStreamTypes.js +1 -2
- package/dist/asr/AsrAgent.js +6 -3
- package/dist/utils/AIStream.d.ts +6 -14
- package/dist/utils/AIStream.js +28 -44
- package/dist/utils/defaultMock.js +2 -4
- package/dist/utils/mock.d.ts +1 -3
- package/dist/utils/object.d.ts +1 -0
- package/dist/utils/object.js +24 -1
- package/dist/utils/sendMessage.d.ts +2 -2
- package/dist/utils/sendMessage.js +16 -7
- package/dist/utils/ttt.d.ts +1 -0
- package/dist/utils/ttt.js +2 -0
- package/dist/withAIStream.d.ts +9 -3
- package/dist/withAIStream.js +27 -5
- package/package.json +2 -2
package/dist/AIStreamTypes.d.ts
CHANGED
|
@@ -216,6 +216,23 @@ export type MiniProgramAccountInfo = {
|
|
|
216
216
|
/** 小程序图标 */
|
|
217
217
|
appIcon: string;
|
|
218
218
|
};
|
|
219
|
+
export interface CustomParamItem {
|
|
220
|
+
value: any;
|
|
221
|
+
/** 生效次数,不传一直生效 */
|
|
222
|
+
effectiveCount?: number;
|
|
223
|
+
}
|
|
224
|
+
export type AIStreamUserData = {
|
|
225
|
+
sessionAttributes?: {
|
|
226
|
+
'custom.param'?: Record<string, CustomParamItem>;
|
|
227
|
+
[key: string]: any;
|
|
228
|
+
};
|
|
229
|
+
eventAttributes?: {
|
|
230
|
+
'custom.param'?: Record<string, CustomParamItem>;
|
|
231
|
+
[key: string]: any;
|
|
232
|
+
};
|
|
233
|
+
chatAttributes?: AIStreamChatAttribute;
|
|
234
|
+
[key: string]: any;
|
|
235
|
+
};
|
|
219
236
|
/**
|
|
220
237
|
* 获取小程序账号信息
|
|
221
238
|
*/
|
|
@@ -630,9 +647,7 @@ export declare enum EventType {
|
|
|
630
647
|
/** 聊天中断 */
|
|
631
648
|
CHAT_BREAK = 4,
|
|
632
649
|
/** 服务端 VAD */
|
|
633
|
-
SERVER_VAD = 5
|
|
634
|
-
/** 服务端错误 */
|
|
635
|
-
SERVER_ERROR = 6
|
|
650
|
+
SERVER_VAD = 5
|
|
636
651
|
}
|
|
637
652
|
export declare enum StreamFlag {
|
|
638
653
|
/** 仅一包 */
|
|
@@ -821,8 +836,6 @@ export type EventBody = {
|
|
|
821
836
|
sessionId: string;
|
|
822
837
|
/** 事件类型: 0-Event Start, 1-Event Payload End, 2-Event End, 3 - OneShot, 4-Chat Break, 5-Server VAD */
|
|
823
838
|
eventType: EventType;
|
|
824
|
-
/** 属性列表 */
|
|
825
|
-
userData?: Attribute[];
|
|
826
839
|
};
|
|
827
840
|
export type AudioBody = {
|
|
828
841
|
/** 接收数据通道 Code */
|
|
@@ -831,8 +844,6 @@ export type AudioBody = {
|
|
|
831
844
|
streamFlag: number;
|
|
832
845
|
/** 音频缓存路径 */
|
|
833
846
|
path: string;
|
|
834
|
-
/** 扩展属性 */
|
|
835
|
-
userData?: Attribute[];
|
|
836
847
|
/** SessionId 列表, 云端返回,可能为空 */
|
|
837
848
|
sessionIdList?: string[];
|
|
838
849
|
/**
|
|
@@ -945,8 +956,6 @@ export type VideoBody = {
|
|
|
945
956
|
streamFlag: StreamFlag;
|
|
946
957
|
/** 视频缓存路径,传输结束后可访问(streamFlag == 1 || StreamFlag == 3) */
|
|
947
958
|
path: string;
|
|
948
|
-
/** 扩展属性 */
|
|
949
|
-
userData?: Attribute[];
|
|
950
959
|
/** SessionId 列表, 云端返回,可能为空 */
|
|
951
960
|
sessionIdList?: string[];
|
|
952
961
|
};
|
|
@@ -961,8 +970,6 @@ export type ImageBody = {
|
|
|
961
970
|
width: number;
|
|
962
971
|
/** 图片高度 */
|
|
963
972
|
height: number;
|
|
964
|
-
/** 扩展属性 */
|
|
965
|
-
userData?: Attribute[];
|
|
966
973
|
/** SessionId 列表, 云端返回,可能为空 */
|
|
967
974
|
sessionIdList?: string[];
|
|
968
975
|
};
|
|
@@ -973,8 +980,6 @@ export type TextBody = {
|
|
|
973
980
|
streamFlag: StreamFlag;
|
|
974
981
|
/** 文本内容(JSON String,见云端文档定义) */
|
|
975
982
|
text: string;
|
|
976
|
-
/** 扩展属性 */
|
|
977
|
-
userData?: Attribute[];
|
|
978
983
|
/** SessionId 列表, 云端返回,可能为空 */
|
|
979
984
|
sessionIdList?: string[];
|
|
980
985
|
};
|
|
@@ -987,8 +992,6 @@ export type FileBody = {
|
|
|
987
992
|
path: string;
|
|
988
993
|
/** 文件类型: 1 - MP4, 2 - OGG_OPUS, 3 - PDF, 4 - JSON, 5 - IPC_LOG, 6 - SweeperMap */
|
|
989
994
|
format: FileFormat;
|
|
990
|
-
/** 扩展属性 */
|
|
991
|
-
userData?: Attribute[];
|
|
992
995
|
/** SessionId 列表, 云端返回,可能为空 */
|
|
993
996
|
sessionIdList?: string[];
|
|
994
997
|
};
|
|
@@ -1142,8 +1145,13 @@ export type CreateSessionParams = {
|
|
|
1142
1145
|
sessionId?: string;
|
|
1143
1146
|
/** 业务配置定义(通过 queryAgentToken 传递) */
|
|
1144
1147
|
bizConfig: BizConfig;
|
|
1145
|
-
/**
|
|
1148
|
+
/**
|
|
1149
|
+
* 扩展属性
|
|
1150
|
+
* @deprecated
|
|
1151
|
+
*/
|
|
1146
1152
|
userData?: Attribute[];
|
|
1153
|
+
/** 扩展属性 */
|
|
1154
|
+
userDataJson?: string;
|
|
1147
1155
|
/** 是否复用数据通道,默认不复用 */
|
|
1148
1156
|
reuseDataChannel?: boolean;
|
|
1149
1157
|
success?: (params: {
|
|
@@ -1211,8 +1219,13 @@ export type SendEventStartParams = {
|
|
|
1211
1219
|
/** 会话 id */
|
|
1212
1220
|
sessionId: string;
|
|
1213
1221
|
eventIdPrefix?: string;
|
|
1214
|
-
/**
|
|
1222
|
+
/**
|
|
1223
|
+
* 扩展属性
|
|
1224
|
+
* @deprecated
|
|
1225
|
+
*/
|
|
1215
1226
|
userData?: Attribute[];
|
|
1227
|
+
/** 扩展属性 */
|
|
1228
|
+
userDataJson?: string;
|
|
1216
1229
|
success?: (params: {
|
|
1217
1230
|
/** 事件 id。此后 sendEventPayloadEnd、sendEventEnd、sendEventChatBreak 都需要使用该 eventId 直到下发一次 sendEventStart 生成新 eventId 之前。 */
|
|
1218
1231
|
eventId: string;
|
|
@@ -1228,7 +1241,7 @@ export type SendEventStartParams = {
|
|
|
1228
1241
|
complete?: () => void;
|
|
1229
1242
|
};
|
|
1230
1243
|
/**
|
|
1231
|
-
|
|
1244
|
+
* @description 某个数据流传输结束事件
|
|
1232
1245
|
*/
|
|
1233
1246
|
export type SendEventPayloadEndParams = {
|
|
1234
1247
|
/** 事件 id */
|
|
@@ -1237,8 +1250,6 @@ export type SendEventPayloadEndParams = {
|
|
|
1237
1250
|
sessionId: string;
|
|
1238
1251
|
/** 发送结束 的 dataChannel */
|
|
1239
1252
|
dataChannel: string;
|
|
1240
|
-
/** 扩展属性 */
|
|
1241
|
-
userData?: Attribute[];
|
|
1242
1253
|
success?: (params: null) => void;
|
|
1243
1254
|
fail?: (params: {
|
|
1244
1255
|
errorMsg: string;
|
|
@@ -1258,8 +1269,6 @@ export type SendEventEndParams = {
|
|
|
1258
1269
|
eventId: string;
|
|
1259
1270
|
/** 会话 id */
|
|
1260
1271
|
sessionId: string;
|
|
1261
|
-
/** 扩展属性 */
|
|
1262
|
-
userData?: Attribute[];
|
|
1263
1272
|
success?: (params: null) => void;
|
|
1264
1273
|
fail?: (params: {
|
|
1265
1274
|
errorMsg: string;
|
|
@@ -1279,8 +1288,13 @@ export type SendEventChatBreakParams = {
|
|
|
1279
1288
|
eventId: string;
|
|
1280
1289
|
/** 会话 id */
|
|
1281
1290
|
sessionId: string;
|
|
1282
|
-
/**
|
|
1291
|
+
/**
|
|
1292
|
+
* 扩展属性
|
|
1293
|
+
* @deprecated
|
|
1294
|
+
*/
|
|
1283
1295
|
userData?: Attribute[];
|
|
1296
|
+
/** 扩展属性 */
|
|
1297
|
+
userDataJson?: string;
|
|
1284
1298
|
success?: (params: null) => void;
|
|
1285
1299
|
fail?: (params: {
|
|
1286
1300
|
errorMsg: string;
|
|
@@ -1304,8 +1318,6 @@ export type StartRecordAndSendAudioDataParams = {
|
|
|
1304
1318
|
sessionId: string;
|
|
1305
1319
|
/** 下发数据通道,当音频只有单路的时候,可以不传 */
|
|
1306
1320
|
dataChannel?: string;
|
|
1307
|
-
/** 扩展属性 */
|
|
1308
|
-
userData?: Attribute[];
|
|
1309
1321
|
success?: (params: null) => void;
|
|
1310
1322
|
fail?: (params: {
|
|
1311
1323
|
errorMsg: string;
|
|
@@ -1322,8 +1334,6 @@ export type StopRecordAndSendAudioDataParams = {
|
|
|
1322
1334
|
sessionId: string;
|
|
1323
1335
|
/** 下发数据通道,当音频只有单路的时候,可以不传 */
|
|
1324
1336
|
dataChannel?: string;
|
|
1325
|
-
/** 扩展属性 */
|
|
1326
|
-
userData?: Attribute[];
|
|
1327
1337
|
success?: (params: AIStreamAudioFile | null) => void;
|
|
1328
1338
|
fail?: (params: {
|
|
1329
1339
|
errorMsg: string;
|
|
@@ -1372,8 +1382,6 @@ export type StopRecordAndSendVideoDataParams = {
|
|
|
1372
1382
|
* 1-前置摄像头, 2-后置摄像头
|
|
1373
1383
|
*/
|
|
1374
1384
|
cameraType: VideoCameraType;
|
|
1375
|
-
/** 扩展属性 */
|
|
1376
|
-
userData?: Attribute[];
|
|
1377
1385
|
success?: (params: null) => void;
|
|
1378
1386
|
fail?: (params: {
|
|
1379
1387
|
errorMsg: string;
|
|
@@ -1392,8 +1400,6 @@ export type SendImageDataParams = {
|
|
|
1392
1400
|
dataChannel?: string;
|
|
1393
1401
|
/** 图片路径 */
|
|
1394
1402
|
path: string;
|
|
1395
|
-
/** 扩展属性 */
|
|
1396
|
-
userData?: Attribute[];
|
|
1397
1403
|
success?: (params: null) => void;
|
|
1398
1404
|
fail?: (params: {
|
|
1399
1405
|
errorMsg: string;
|
|
@@ -1412,8 +1418,6 @@ export type SendTextDataParams = {
|
|
|
1412
1418
|
dataChannel?: string;
|
|
1413
1419
|
/** 文本内容 */
|
|
1414
1420
|
text: string;
|
|
1415
|
-
/** 扩展属性 */
|
|
1416
|
-
userData?: Attribute[];
|
|
1417
1421
|
success?: (params: null) => void;
|
|
1418
1422
|
fail?: (params: {
|
|
1419
1423
|
errorMsg: string;
|
|
@@ -1447,8 +1451,6 @@ export type SendFileDataParams = {
|
|
|
1447
1451
|
path: string;
|
|
1448
1452
|
/** 文件类型: 1 - MP4, 2 - OGG_OPUS, 3 - PDF, 4 - JSON, 5 - IPC_LOG, 6 - SweeperMap */
|
|
1449
1453
|
format: FileFormat;
|
|
1450
|
-
/** 扩展属性 */
|
|
1451
|
-
userData?: Attribute[];
|
|
1452
1454
|
success?: (params: null) => void;
|
|
1453
1455
|
fail?: (params: {
|
|
1454
1456
|
errorMsg: string;
|
package/dist/AIStreamTypes.js
CHANGED
|
@@ -100,7 +100,6 @@ export let EventType = /*#__PURE__*/function (EventType) {
|
|
|
100
100
|
EventType[EventType["ONE_SHOT"] = 3] = "ONE_SHOT";
|
|
101
101
|
EventType[EventType["CHAT_BREAK"] = 4] = "CHAT_BREAK";
|
|
102
102
|
EventType[EventType["SERVER_VAD"] = 5] = "SERVER_VAD";
|
|
103
|
-
EventType[EventType["SERVER_ERROR"] = 6] = "SERVER_ERROR";
|
|
104
103
|
return EventType;
|
|
105
104
|
}({});
|
|
106
105
|
export let StreamFlag = /*#__PURE__*/function (StreamFlag) {
|
|
@@ -228,7 +227,7 @@ export let NetworkType = /*#__PURE__*/function (NetworkType) {
|
|
|
228
227
|
*/
|
|
229
228
|
|
|
230
229
|
/**
|
|
231
|
-
|
|
230
|
+
* @description 某个数据流传输结束事件
|
|
232
231
|
*/
|
|
233
232
|
|
|
234
233
|
/**
|
package/dist/asr/AsrAgent.js
CHANGED
|
@@ -177,7 +177,7 @@ export class AsrAgent {
|
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
179
|
const activeSession = await this.createSession();
|
|
180
|
-
const
|
|
180
|
+
const chatAttributes = {
|
|
181
181
|
'processing.interrupt': 'false',
|
|
182
182
|
'asr.enableVad': 'false'
|
|
183
183
|
};
|
|
@@ -186,8 +186,11 @@ export class AsrAgent {
|
|
|
186
186
|
userData: [{
|
|
187
187
|
type: AIStreamAttributeType.AI_CHAT,
|
|
188
188
|
payloadType: AIStreamAttributePayloadType.STRING,
|
|
189
|
-
value: JSON.stringify(
|
|
190
|
-
}]
|
|
189
|
+
value: JSON.stringify(chatAttributes)
|
|
190
|
+
}],
|
|
191
|
+
userDataJson: JSON.stringify({
|
|
192
|
+
chatAttributes
|
|
193
|
+
})
|
|
191
194
|
}));
|
|
192
195
|
if (startEventError) {
|
|
193
196
|
finish(startEventError);
|
package/dist/utils/AIStream.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AIStreamAudioFile, Attribute, BizTag, ConnectClientType, ConnectState, FileFormat, VideoCameraType } from '../AIStreamTypes';
|
|
1
|
+
import { AIStreamAudioFile, AIStreamUserData, Attribute, BizTag, ConnectClientType, ConnectState, FileFormat, VideoCameraType } from '../AIStreamTypes';
|
|
2
2
|
import { AIStreamDataEntry, AIStreamObserverPool } from './observer';
|
|
3
3
|
import { AIStreamError } from './errors';
|
|
4
4
|
interface AIStreamConnectionOptions {
|
|
@@ -40,12 +40,13 @@ interface AIStreamSessionOptions {
|
|
|
40
40
|
apiVersion?: string;
|
|
41
41
|
/** 业务额外参数 */
|
|
42
42
|
extParams?: any;
|
|
43
|
-
|
|
43
|
+
getSessionUserData?: () => Promise<AIStreamUserData>;
|
|
44
44
|
}
|
|
45
45
|
interface AIStreamEventOptions {
|
|
46
46
|
signal?: AbortSignal;
|
|
47
|
-
userData?: Attribute[];
|
|
48
47
|
eventIdPrefix?: string;
|
|
48
|
+
userData?: Attribute[];
|
|
49
|
+
userDataJson?: string;
|
|
49
50
|
}
|
|
50
51
|
export declare class AIStreamSession {
|
|
51
52
|
private connection;
|
|
@@ -75,30 +76,25 @@ type AIStreamEventWriteChunk = {
|
|
|
75
76
|
type: 'text';
|
|
76
77
|
text: string;
|
|
77
78
|
dataChannel?: string;
|
|
78
|
-
userData?: Attribute[];
|
|
79
79
|
} | {
|
|
80
80
|
type: 'file';
|
|
81
81
|
path: string;
|
|
82
82
|
format: FileFormat;
|
|
83
83
|
dataChannel?: string;
|
|
84
|
-
userData?: Attribute[];
|
|
85
84
|
} | {
|
|
86
85
|
type: 'image';
|
|
87
86
|
path: string;
|
|
88
87
|
dataChannel?: string;
|
|
89
|
-
userData?: Attribute[];
|
|
90
88
|
};
|
|
91
89
|
type AIStreamEventSource = {
|
|
92
90
|
type: 'audio';
|
|
93
91
|
dataChannel?: string;
|
|
94
|
-
userData?: Attribute[];
|
|
95
92
|
saveFile?: boolean;
|
|
96
93
|
sampleRate?: number;
|
|
97
94
|
} | {
|
|
98
95
|
type: 'video';
|
|
99
96
|
dataChannel?: string;
|
|
100
97
|
cameraType?: VideoCameraType;
|
|
101
|
-
userData?: Attribute[];
|
|
102
98
|
};
|
|
103
99
|
export interface AIStreamEventStream {
|
|
104
100
|
type: 'video' | 'audio';
|
|
@@ -123,12 +119,8 @@ export declare class AIStreamEvent {
|
|
|
123
119
|
private findFirstCode;
|
|
124
120
|
write(chunk: AIStreamEventWriteChunk): Promise<void>;
|
|
125
121
|
stream(source: AIStreamEventSource): AIStreamEventStream;
|
|
126
|
-
end(
|
|
127
|
-
|
|
128
|
-
}): Promise<void>;
|
|
129
|
-
abort(options?: {
|
|
130
|
-
userData?: Attribute[];
|
|
131
|
-
}): void;
|
|
122
|
+
end(): Promise<void>;
|
|
123
|
+
abort(): void;
|
|
132
124
|
on(name: 'close', callback: () => void): void;
|
|
133
125
|
on(name: 'finish', callback: () => void): void;
|
|
134
126
|
on(name: 'error', callback: (error: AIStreamError) => void): void;
|
package/dist/utils/AIStream.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
|
|
2
1
|
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
3
2
|
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
|
|
4
|
-
|
|
3
|
+
import "core-js/modules/es.json.stringify.js";
|
|
5
4
|
import "core-js/modules/esnext.iterator.constructor.js";
|
|
6
5
|
import "core-js/modules/esnext.iterator.find.js";
|
|
7
6
|
import "core-js/modules/esnext.iterator.for-each.js";
|
|
8
7
|
import "core-js/modules/esnext.iterator.map.js";
|
|
9
8
|
import "core-js/modules/web.dom-collections.iterator.js";
|
|
10
|
-
import {
|
|
9
|
+
import { AIStreamErrorCode, AIStreamServerErrorCode, BizTag, ConnectClientType, ConnectState, EventType, NetworkType, SessionState } from '../AIStreamTypes';
|
|
11
10
|
import { closeSession, connect, createSession, disconnect, getCurrentHomeInfo, getNetworkType, isConnected, queryAgentToken, sendEventChatBreak, sendEventEnd, sendEventPayloadEnd, sendEventStart, sendImageData, sendTextData, startRecordAndSendAudioData, stopRecordAndSendAudioData } from './ttt';
|
|
12
11
|
import { AIStreamObserver, AIStreamObserverPool } from './observer';
|
|
13
12
|
import { isAbortError, safeParseJSON } from '@ray-js/t-agent';
|
|
@@ -180,27 +179,12 @@ export class AIStreamSession {
|
|
|
180
179
|
(_this$activeEvent2 = this.activeEvent) === null || _this$activeEvent2 === void 0 || _this$activeEvent2.emit('data', entry);
|
|
181
180
|
if (entry.type === 'event' && entry.body.eventId === this.activeEvent.eventId) {
|
|
182
181
|
const {
|
|
183
|
-
eventType
|
|
184
|
-
userData
|
|
182
|
+
eventType
|
|
185
183
|
} = entry.body;
|
|
186
184
|
if (eventType === EventType.EVENT_END || eventType === EventType.CHAT_BREAK || eventType === EventType.ONE_SHOT) {
|
|
187
185
|
var _this$activeEvent3;
|
|
188
186
|
(_this$activeEvent3 = this.activeEvent) === null || _this$activeEvent3 === void 0 || _this$activeEvent3.emit('finish');
|
|
189
187
|
this.cleanupEvent();
|
|
190
|
-
} else if (eventType === EventType.SERVER_ERROR) {
|
|
191
|
-
var _this$activeEvent4;
|
|
192
|
-
let code = '';
|
|
193
|
-
let message = '';
|
|
194
|
-
for (const item of userData) {
|
|
195
|
-
if (item.type === AIStreamAttributeType.ERROR_CODE) {
|
|
196
|
-
code = item.value;
|
|
197
|
-
} else if (item.type === AIStreamAttributeType.ERROR_MESSAGE) {
|
|
198
|
-
message = item.value;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const error = new AIStreamError(message || 'Event error occurred', code || AIStreamErrorCode.UNKNOWN_ERROR);
|
|
202
|
-
(_this$activeEvent4 = this.activeEvent) === null || _this$activeEvent4 === void 0 || _this$activeEvent4.emit('error', error);
|
|
203
|
-
this.cleanupEvent();
|
|
204
188
|
}
|
|
205
189
|
}
|
|
206
190
|
});
|
|
@@ -274,12 +258,16 @@ export class AIStreamSession {
|
|
|
274
258
|
} else {
|
|
275
259
|
this.tokenExtParamsResolvable.resolve({});
|
|
276
260
|
}
|
|
261
|
+
let userData = {};
|
|
262
|
+
if (options.getSessionUserData) {
|
|
263
|
+
userData = await options.getSessionUserData();
|
|
264
|
+
}
|
|
277
265
|
{
|
|
278
266
|
const [err, res] = await tryCatchTTT(() => createSession({
|
|
279
267
|
bizTag: options.bizTag,
|
|
280
268
|
agentToken,
|
|
281
269
|
bizConfig,
|
|
282
|
-
|
|
270
|
+
userDataJson: JSON.stringify(userData)
|
|
283
271
|
}));
|
|
284
272
|
if (err) {
|
|
285
273
|
this.promise = null;
|
|
@@ -302,18 +290,23 @@ export class AIStreamSession {
|
|
|
302
290
|
throw new AIStreamError('Cannot start a new event while another is active', AIStreamErrorCode.EVENT_EXISTS);
|
|
303
291
|
}
|
|
304
292
|
const {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
293
|
+
signal,
|
|
294
|
+
userData,
|
|
295
|
+
eventIdPrefix,
|
|
296
|
+
userDataJson
|
|
297
|
+
} = options;
|
|
308
298
|
await this.ensureSession();
|
|
309
299
|
if (signal !== null && signal !== void 0 && signal.aborted) {
|
|
310
300
|
const error = new AIStreamError('start event was aborted', AIStreamErrorCode.EVENT_ABORTED);
|
|
311
301
|
error.name = 'AbortError';
|
|
312
302
|
throw error;
|
|
313
303
|
}
|
|
314
|
-
const [error, result] = await tryCatchTTT(() => sendEventStart(
|
|
315
|
-
sessionId: this.sessionId
|
|
316
|
-
|
|
304
|
+
const [error, result] = await tryCatchTTT(() => sendEventStart({
|
|
305
|
+
sessionId: this.sessionId,
|
|
306
|
+
userDataJson,
|
|
307
|
+
userData,
|
|
308
|
+
eventIdPrefix
|
|
309
|
+
}));
|
|
317
310
|
if (error) {
|
|
318
311
|
throw error;
|
|
319
312
|
}
|
|
@@ -358,11 +351,11 @@ export class AIStreamSession {
|
|
|
358
351
|
return this.activeEvent;
|
|
359
352
|
}
|
|
360
353
|
cleanupEvent() {
|
|
361
|
-
var _this$activeObserver, _this$
|
|
354
|
+
var _this$activeObserver, _this$activeEvent4;
|
|
362
355
|
logger.debug('AIStreamSession cleanupEvent');
|
|
363
356
|
(_this$activeObserver = this.activeObserver) === null || _this$activeObserver === void 0 || _this$activeObserver.disconnect();
|
|
364
357
|
this.activeObserver = null;
|
|
365
|
-
(_this$
|
|
358
|
+
(_this$activeEvent4 = this.activeEvent) === null || _this$activeEvent4 === void 0 || _this$activeEvent4.emit('close');
|
|
366
359
|
this.activeEvent = null;
|
|
367
360
|
}
|
|
368
361
|
cleanup() {
|
|
@@ -420,8 +413,7 @@ export class AIStreamEvent {
|
|
|
420
413
|
const [error] = await tryCatchTTT(() => sendTextData({
|
|
421
414
|
sessionId: this.sessionId,
|
|
422
415
|
text: chunk.text,
|
|
423
|
-
dataChannel
|
|
424
|
-
userData: chunk.userData
|
|
416
|
+
dataChannel
|
|
425
417
|
}));
|
|
426
418
|
if (error) {
|
|
427
419
|
throw error;
|
|
@@ -433,13 +425,11 @@ export class AIStreamEvent {
|
|
|
433
425
|
// path: chunk.path,
|
|
434
426
|
// format: chunk.format,
|
|
435
427
|
// dataChannel,
|
|
436
|
-
// userData: chunk.userData,
|
|
437
428
|
// });
|
|
438
429
|
} else if (chunk.type === 'image') {
|
|
439
430
|
const [error] = await tryCatchTTT(() => sendImageData({
|
|
440
431
|
sessionId: this.sessionId,
|
|
441
|
-
path: chunk.path
|
|
442
|
-
userData: chunk.userData
|
|
432
|
+
path: chunk.path
|
|
443
433
|
}));
|
|
444
434
|
if (error) {
|
|
445
435
|
throw error;
|
|
@@ -485,7 +475,6 @@ export class AIStreamEvent {
|
|
|
485
475
|
startPromise = startRecordAndSendAudioData({
|
|
486
476
|
sessionId: this.sessionId,
|
|
487
477
|
dataChannel,
|
|
488
|
-
userData: source.userData,
|
|
489
478
|
recordInitParams: source.sampleRate ? {
|
|
490
479
|
sampleRate: source.sampleRate
|
|
491
480
|
} : undefined,
|
|
@@ -497,7 +486,6 @@ export class AIStreamEvent {
|
|
|
497
486
|
// await startRecordAndSendVideoData({
|
|
498
487
|
// dataChannel,
|
|
499
488
|
// cameraType,
|
|
500
|
-
// userData: source.userData,
|
|
501
489
|
// });
|
|
502
490
|
}
|
|
503
491
|
},
|
|
@@ -514,8 +502,7 @@ export class AIStreamEvent {
|
|
|
514
502
|
if (source.type === 'audio') {
|
|
515
503
|
file = await stopRecordAndSendAudioData({
|
|
516
504
|
sessionId: this.sessionId,
|
|
517
|
-
dataChannel
|
|
518
|
-
userData: source.userData
|
|
505
|
+
dataChannel
|
|
519
506
|
});
|
|
520
507
|
} else if (source.type === 'video') {
|
|
521
508
|
logger.warn('Video data sending is not implemented yet');
|
|
@@ -523,7 +510,6 @@ export class AIStreamEvent {
|
|
|
523
510
|
// await stopRecordAndSendVideoData({
|
|
524
511
|
// dataChannel,
|
|
525
512
|
// cameraType,
|
|
526
|
-
// userData: source.userData,
|
|
527
513
|
// });
|
|
528
514
|
}
|
|
529
515
|
sendEventPayloadEnd({
|
|
@@ -539,18 +525,17 @@ export class AIStreamEvent {
|
|
|
539
525
|
this.streams[dataChannel] = stream;
|
|
540
526
|
return stream;
|
|
541
527
|
}
|
|
542
|
-
async end(
|
|
528
|
+
async end() {
|
|
543
529
|
if (this.closed) {
|
|
544
530
|
return;
|
|
545
531
|
}
|
|
546
532
|
await Promise.all([...Object.values(this.chains), ...Object.values(this.streams).map(s => s.stop())]);
|
|
547
533
|
await sendEventEnd({
|
|
548
534
|
eventId: this.eventId,
|
|
549
|
-
sessionId: this.sessionId
|
|
550
|
-
userData: options === null || options === void 0 ? void 0 : options.userData
|
|
535
|
+
sessionId: this.sessionId
|
|
551
536
|
});
|
|
552
537
|
}
|
|
553
|
-
abort(
|
|
538
|
+
abort() {
|
|
554
539
|
if (this.closed) {
|
|
555
540
|
return;
|
|
556
541
|
}
|
|
@@ -558,8 +543,7 @@ export class AIStreamEvent {
|
|
|
558
543
|
// 故意不等,直接发送中断事件
|
|
559
544
|
sendEventChatBreak({
|
|
560
545
|
eventId: this.eventId,
|
|
561
|
-
sessionId: this.sessionId
|
|
562
|
-
userData: options === null || options === void 0 ? void 0 : options.userData
|
|
546
|
+
sessionId: this.sessionId
|
|
563
547
|
});
|
|
564
548
|
}
|
|
565
549
|
const error = new AIStreamError('This operation was aborted', AIStreamErrorCode.EVENT_ABORTED);
|
|
@@ -504,8 +504,7 @@ mock.hooks.hook('sendImageData', context => {
|
|
|
504
504
|
}
|
|
505
505
|
session.currentEvent.data.push({
|
|
506
506
|
type: 'image',
|
|
507
|
-
path: context.options.path
|
|
508
|
-
userData: context.options.userData
|
|
507
|
+
path: context.options.path
|
|
509
508
|
});
|
|
510
509
|
context.result = true;
|
|
511
510
|
});
|
|
@@ -517,8 +516,7 @@ mock.hooks.hook('sendTextData', context => {
|
|
|
517
516
|
}
|
|
518
517
|
session.currentEvent.data.push({
|
|
519
518
|
type: 'text',
|
|
520
|
-
text: context.options.text
|
|
521
|
-
userData: context.options.userData
|
|
519
|
+
text: context.options.text
|
|
522
520
|
});
|
|
523
521
|
});
|
|
524
522
|
const filterRecords = query => {
|
package/dist/utils/mock.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Emitter } from '@ray-js/t-agent';
|
|
2
|
-
import {
|
|
2
|
+
import { ReceivedTextSkillPacketBody } from '../AIStreamTypes';
|
|
3
3
|
interface TTTCallContext {
|
|
4
4
|
options: any;
|
|
5
5
|
result: any;
|
|
@@ -8,11 +8,9 @@ export interface SendToAIStreamContext {
|
|
|
8
8
|
data: Array<{
|
|
9
9
|
type: 'text';
|
|
10
10
|
text: string;
|
|
11
|
-
userData?: Attribute[];
|
|
12
11
|
} | {
|
|
13
12
|
type: 'image';
|
|
14
13
|
path: string;
|
|
15
|
-
userData?: Attribute[];
|
|
16
14
|
}>;
|
|
17
15
|
responseText: string | undefined;
|
|
18
16
|
wordDelayMs: number;
|
package/dist/utils/object.d.ts
CHANGED
package/dist/utils/object.js
CHANGED
|
@@ -22,4 +22,27 @@ export const createResolvable = () => {
|
|
|
22
22
|
};
|
|
23
23
|
});
|
|
24
24
|
return ret;
|
|
25
|
-
};
|
|
25
|
+
};
|
|
26
|
+
export function deepMerge(target, source) {
|
|
27
|
+
if (typeof target !== 'object' || target === null) {
|
|
28
|
+
return source; // 如果目标不是对象,直接返回来源
|
|
29
|
+
}
|
|
30
|
+
if (typeof source !== 'object' || source === null) {
|
|
31
|
+
return target; // 如果来源不是对象,返回目标
|
|
32
|
+
}
|
|
33
|
+
for (const key in source) {
|
|
34
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
35
|
+
const sourceValue = source[key];
|
|
36
|
+
const targetValue = target[key];
|
|
37
|
+
|
|
38
|
+
// 如果是对象,递归合并
|
|
39
|
+
if (typeof sourceValue === 'object' && sourceValue !== null) {
|
|
40
|
+
target[key] = deepMerge(Array.isArray(targetValue) ? [] : targetValue || {}, sourceValue);
|
|
41
|
+
} else {
|
|
42
|
+
// 否则直接赋值
|
|
43
|
+
target[key] = sourceValue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return target;
|
|
48
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import '../polyfill';
|
|
2
2
|
import { AIStreamSession } from './AIStream';
|
|
3
|
-
import {
|
|
3
|
+
import { AIStreamUserData } from '../AIStreamTypes';
|
|
4
4
|
import { InputBlock, StreamResponse } from '@ray-js/t-agent';
|
|
5
5
|
export interface SendBlocksToAIStreamParams {
|
|
6
6
|
blocks: InputBlock[];
|
|
7
7
|
session: AIStreamSession;
|
|
8
|
-
attribute?: AIStreamChatAttribute;
|
|
9
8
|
signal?: AbortSignal;
|
|
10
9
|
enableTts?: boolean;
|
|
11
10
|
eventIdPrefix?: string;
|
|
11
|
+
getUserData: () => Promise<AIStreamUserData>;
|
|
12
12
|
}
|
|
13
13
|
export declare function sendBlocksToAIStream(params: SendBlocksToAIStreamParams): {
|
|
14
14
|
response: StreamResponse;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
1
|
import "core-js/modules/es.json.stringify.js";
|
|
3
2
|
import "core-js/modules/web.dom-collections.iterator.js";
|
|
4
3
|
import '../polyfill';
|
|
@@ -8,6 +7,7 @@ import { EmitterEvent, generateId, safeParseJSON, StreamResponse } from '@ray-js
|
|
|
8
7
|
import { tryCatch } from './misc';
|
|
9
8
|
import { AIStreamError, transformErrorCode } from './errors';
|
|
10
9
|
import logger from './logger';
|
|
10
|
+
import { deepMerge } from './object';
|
|
11
11
|
const mimeTypeToFormatMap = {
|
|
12
12
|
'video/mp4': FileFormat.MP4,
|
|
13
13
|
'text/json': FileFormat.JSON,
|
|
@@ -20,7 +20,8 @@ export function sendBlocksToAIStream(params) {
|
|
|
20
20
|
session,
|
|
21
21
|
blocks,
|
|
22
22
|
signal,
|
|
23
|
-
eventIdPrefix
|
|
23
|
+
eventIdPrefix,
|
|
24
|
+
getUserData
|
|
24
25
|
} = params;
|
|
25
26
|
let audioEmitter = null;
|
|
26
27
|
for (const block of blocks) {
|
|
@@ -31,10 +32,6 @@ export function sendBlocksToAIStream(params) {
|
|
|
31
32
|
audioEmitter = block.audio_emitter;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
const attribute = _objectSpread({
|
|
35
|
-
'processing.interrupt': 'false',
|
|
36
|
-
'asr.enableVad': 'false'
|
|
37
|
-
}, params.attribute);
|
|
38
35
|
let canceled = false;
|
|
39
36
|
let closed = false;
|
|
40
37
|
let event = null;
|
|
@@ -103,14 +100,26 @@ export function sendBlocksToAIStream(params) {
|
|
|
103
100
|
}
|
|
104
101
|
});
|
|
105
102
|
}
|
|
103
|
+
const chatAttributes = {
|
|
104
|
+
'processing.interrupt': 'false',
|
|
105
|
+
'asr.enableVad': 'false'
|
|
106
|
+
};
|
|
107
|
+
let eventUserData = {};
|
|
108
|
+
if (getUserData) {
|
|
109
|
+
eventUserData = await getUserData();
|
|
110
|
+
}
|
|
111
|
+
const userData = deepMerge({
|
|
112
|
+
chatAttributes
|
|
113
|
+
}, eventUserData);
|
|
106
114
|
let error;
|
|
107
115
|
[error, event] = await tryCatch(() => session.startEvent({
|
|
108
116
|
signal,
|
|
109
117
|
eventIdPrefix,
|
|
118
|
+
userDataJson: JSON.stringify(userData),
|
|
110
119
|
userData: [{
|
|
111
120
|
type: AIStreamAttributeType.AI_CHAT,
|
|
112
121
|
payloadType: AIStreamAttributePayloadType.STRING,
|
|
113
|
-
value: JSON.stringify(
|
|
122
|
+
value: JSON.stringify(chatAttributes)
|
|
114
123
|
}]
|
|
115
124
|
}));
|
|
116
125
|
if (error) {
|
package/dist/utils/ttt.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export declare const closeSession: (options?: Omit<CloseSessionParams, "success"
|
|
|
77
77
|
export declare const sendEventStart: (options?: Omit<SendEventStartParams, "success" | "fail"> | undefined) => Promise<{
|
|
78
78
|
eventId: string;
|
|
79
79
|
}>;
|
|
80
|
+
/** @deprecated */
|
|
80
81
|
export declare const sendEventPayloadEnd: (options?: Omit<SendEventPayloadEndParams, "success" | "fail"> | undefined) => Promise<null>;
|
|
81
82
|
export declare const sendEventEnd: (options?: Omit<SendEventEndParams, "success" | "fail"> | undefined) => Promise<null>;
|
|
82
83
|
export declare const sendEventChatBreak: (options?: Omit<SendEventChatBreakParams, "success" | "fail"> | undefined) => Promise<null>;
|
package/dist/utils/ttt.js
CHANGED
|
@@ -37,6 +37,8 @@ export const queryAgentToken = promisify(ty.aistream.queryAgentToken, true);
|
|
|
37
37
|
export const createSession = promisify(ty.aistream.createSession, true);
|
|
38
38
|
export const closeSession = promisify(ty.aistream.closeSession, true);
|
|
39
39
|
export const sendEventStart = promisify(ty.aistream.sendEventStart, true);
|
|
40
|
+
|
|
41
|
+
/** @deprecated */
|
|
40
42
|
export const sendEventPayloadEnd = promisify(ty.aistream.sendEventPayloadEnd, true);
|
|
41
43
|
export const sendEventEnd = promisify(ty.aistream.sendEventEnd, true);
|
|
42
44
|
export const sendEventChatBreak = promisify(ty.aistream.sendEventChatBreak, true);
|
package/dist/withAIStream.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChatAgent, ChatCardObject, ChatMessage, ChatMessageStatus, ChatTile, GetChatPluginHandler, InputBlock } from '@ray-js/t-agent';
|
|
2
2
|
import { TTTAction } from './utils';
|
|
3
|
-
import { ConnectClientType, ReceivedTextSkillPacketBody } from './AIStreamTypes';
|
|
3
|
+
import { AIStreamUserData, ConnectClientType, ReceivedTextSkillPacketBody } from './AIStreamTypes';
|
|
4
4
|
import { ChatHistoryStore, StoredMessageObject } from './ChatHistoryStore';
|
|
5
5
|
export interface AIStreamOptions {
|
|
6
6
|
/** client 类型: 1-作为设备代理, 2-作为 App,3-作为开发者(行业 App) */
|
|
@@ -58,18 +58,23 @@ export interface AIStreamHooks {
|
|
|
58
58
|
onCardsReceived: (skills: ReceivedTextSkillPacketBody[], result: {
|
|
59
59
|
cards: ChatCardObject[];
|
|
60
60
|
}) => void;
|
|
61
|
+
onUserDataRead: (type: 'create-session' | 'start-event', data: {
|
|
62
|
+
blocks?: InputBlock[];
|
|
63
|
+
}, result: {
|
|
64
|
+
userData: AIStreamUserData;
|
|
65
|
+
}) => void;
|
|
61
66
|
}
|
|
62
67
|
export declare function withAIStream(options?: AIStreamOptions): (agent: ChatAgent) => {
|
|
63
68
|
hooks: import("hookable").Hookable<AIStreamHooks, import("hookable").HookKeys<AIStreamHooks>>;
|
|
64
69
|
aiStream: {
|
|
65
|
-
send: (blocks: InputBlock[], signal?: AbortSignal,
|
|
70
|
+
send: (blocks: InputBlock[], signal?: AbortSignal, eventUserData?: AIStreamUserData) => {
|
|
66
71
|
response: import("@ray-js/t-agent").StreamResponse;
|
|
67
72
|
metaPromise: Promise<Record<string, any>>;
|
|
68
73
|
};
|
|
69
74
|
chat: (blocks: InputBlock[], signal?: AbortSignal, options?: {
|
|
70
75
|
sendBy?: string | undefined;
|
|
71
76
|
responseBy?: string | undefined;
|
|
72
|
-
|
|
77
|
+
userData?: AIStreamUserData | undefined;
|
|
73
78
|
} | undefined) => Promise<ChatMessage[]>;
|
|
74
79
|
options: AIStreamOptions;
|
|
75
80
|
removeMessage: (message: ChatMessage) => Promise<void>;
|
|
@@ -86,6 +91,7 @@ export declare function withAIStream(options?: AIStreamOptions): (agent: ChatAge
|
|
|
86
91
|
onTextCompose: (fn: AIStreamHooks['onTextCompose']) => () => void;
|
|
87
92
|
onSkillsEnd: (fn: AIStreamHooks['onSkillsEnd']) => () => void;
|
|
88
93
|
onCardsReceived: (fn: AIStreamHooks['onCardsReceived']) => () => void;
|
|
94
|
+
onUserDataRead: (fn: AIStreamHooks['onUserDataRead']) => () => void;
|
|
89
95
|
onTTTAction: (fn: AIStreamHooks['onTTTAction']) => () => void;
|
|
90
96
|
getChatId: () => Promise<string>;
|
|
91
97
|
};
|
package/dist/withAIStream.js
CHANGED
|
@@ -14,6 +14,7 @@ import { BizCode, ConnectClientType } from './AIStreamTypes';
|
|
|
14
14
|
import { DEFAULT_TOKEN_API, DEFAULT_TOKEN_API_VERSION, globalAIStreamClient } from './global';
|
|
15
15
|
import logger from './utils/logger';
|
|
16
16
|
import { ChatHistoryLocalStore } from './ChatHistoryLocalStore';
|
|
17
|
+
import { deepMerge } from './utils/object';
|
|
17
18
|
export function withAIStream() {
|
|
18
19
|
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
19
20
|
const hooks = createHooks();
|
|
@@ -88,7 +89,14 @@ export function withAIStream() {
|
|
|
88
89
|
extParams: _objectSpread({
|
|
89
90
|
needTts: !!options.enableTts,
|
|
90
91
|
deviceId
|
|
91
|
-
}, tokenOptions.extParams)
|
|
92
|
+
}, tokenOptions.extParams),
|
|
93
|
+
getSessionUserData: async () => {
|
|
94
|
+
const result = {
|
|
95
|
+
userData: {}
|
|
96
|
+
};
|
|
97
|
+
await hooks.callHook('onUserDataRead', 'create-session', {}, result);
|
|
98
|
+
return result.userData;
|
|
99
|
+
}
|
|
92
100
|
});
|
|
93
101
|
await session.set('AIStream.streamSession', streamSession);
|
|
94
102
|
if (options.earlyStart) {
|
|
@@ -221,14 +229,25 @@ export function withAIStream() {
|
|
|
221
229
|
};
|
|
222
230
|
}
|
|
223
231
|
});
|
|
224
|
-
const send = (blocks, signal,
|
|
232
|
+
const send = (blocks, signal, eventUserData) => {
|
|
225
233
|
const streamSession = session.get('AIStream.streamSession');
|
|
226
234
|
const result = sendBlocksToAIStream({
|
|
227
235
|
blocks,
|
|
228
236
|
session: streamSession,
|
|
229
237
|
signal,
|
|
230
238
|
eventIdPrefix: options.eventIdPrefix,
|
|
231
|
-
|
|
239
|
+
getUserData: async () => {
|
|
240
|
+
const userDataResult = {
|
|
241
|
+
userData: {}
|
|
242
|
+
};
|
|
243
|
+
await hooks.callHook('onUserDataRead', 'start-event', {
|
|
244
|
+
blocks
|
|
245
|
+
}, userDataResult);
|
|
246
|
+
const userData = {};
|
|
247
|
+
deepMerge(userData, userDataResult.userData);
|
|
248
|
+
deepMerge(userData, eventUserData);
|
|
249
|
+
return userData;
|
|
250
|
+
}
|
|
232
251
|
});
|
|
233
252
|
signal === null || signal === void 0 || signal.addEventListener('abort', event => {
|
|
234
253
|
logger.debug('withAIStream signal aborted, response.started:', result.response.started);
|
|
@@ -245,7 +264,7 @@ export function withAIStream() {
|
|
|
245
264
|
const {
|
|
246
265
|
sendBy = 'user',
|
|
247
266
|
responseBy = 'assistant',
|
|
248
|
-
|
|
267
|
+
userData
|
|
249
268
|
} = options || {};
|
|
250
269
|
let audioEmitter = null;
|
|
251
270
|
for (const block of blocks) {
|
|
@@ -345,7 +364,7 @@ export function withAIStream() {
|
|
|
345
364
|
const {
|
|
346
365
|
response,
|
|
347
366
|
metaPromise
|
|
348
|
-
} = send(blocks, signal,
|
|
367
|
+
} = send(blocks, signal, userData);
|
|
349
368
|
if (audioPromise) {
|
|
350
369
|
try {
|
|
351
370
|
await audioPromise;
|
|
@@ -585,6 +604,9 @@ export function withAIStream() {
|
|
|
585
604
|
onCardsReceived: fn => {
|
|
586
605
|
return hooks.hook('onCardsReceived', fn);
|
|
587
606
|
},
|
|
607
|
+
onUserDataRead: fn => {
|
|
608
|
+
return hooks.hook('onUserDataRead', fn);
|
|
609
|
+
},
|
|
588
610
|
onTTTAction: fn => {
|
|
589
611
|
return hooks.hook('onTTTAction', fn);
|
|
590
612
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ray-js/t-agent-plugin-aistream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7-beta.1",
|
|
4
4
|
"author": "Tuya.inc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/url-parse": "^1.4.11"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "d7754f9879a7ec837eb343ac759a08734cc789d3"
|
|
39
39
|
}
|