@ray-js/t-agent-plugin-aistream 0.2.3-beta-4 → 0.2.4-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.
@@ -23,6 +23,7 @@ export interface AsrAgentOptions {
23
23
  onError?: (error: any) => void;
24
24
  /** 主动中断或者超过录制时长,会调用onAbort */
25
25
  onAbort?: () => void;
26
+ onStatusChanged?: (status: AsrAgentStatus) => void;
26
27
  recordingOptions?: {
27
28
  /** 是否需要保存音频 */
28
29
  saveFile?: boolean;
@@ -33,6 +34,12 @@ export interface AsrAgentOptions {
33
34
  };
34
35
  enableLog?: boolean;
35
36
  }
37
+ export declare enum AsrAgentStatus {
38
+ PENDING = "pending",
39
+ RECORDING = "recording",
40
+ ASR = "asr",
41
+ ABORTING = "aborting"
42
+ }
36
43
  export declare class AsrAgent {
37
44
  /** 数据链接,一个agent保持一个链接就可以 */
38
45
  private streamConn;
@@ -41,19 +48,28 @@ export declare class AsrAgent {
41
48
  private activeEvent;
42
49
  /** 音频流监听 */
43
50
  private audioStream;
44
- options: AsrAgentOptions;
51
+ private options;
45
52
  /** 录音时长定时器 */
46
53
  private recordDurationTimer;
54
+ private startPromise;
55
+ private authorized;
56
+ _status: AsrAgentStatus;
57
+ private disposed;
58
+ get status(): AsrAgentStatus;
59
+ set status(status: AsrAgentStatus);
47
60
  constructor(options: AsrAgentOptions);
48
61
  /** 获取录音权限 */
49
- getRecordScope(): Promise<boolean>;
62
+ private getRecordScope;
50
63
  /** 获取数据链接,一般只有一个链接就可以 */
51
64
  private getConnection;
52
65
  private createSession;
53
66
  /** 开始录音时长监听 */
54
67
  private startRecordTimer;
68
+ private _start;
69
+ private _abort;
70
+ private _stop;
55
71
  start(): Promise<void>;
56
- stop(isAbort?: boolean): Promise<void>;
72
+ stop(): Promise<void>;
57
73
  abort(): Promise<void>;
58
74
  dispose(): void;
59
75
  }
@@ -1,18 +1,44 @@
1
1
  import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
2
  import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
3
3
  import "core-js/modules/es.json.stringify.js";
4
+ import "core-js/modules/es.promise.finally.js";
5
+ import "core-js/modules/web.dom-collections.iterator.js";
4
6
  import { authorize, getCurrentHomeInfo, initAudioRecorder } from '../utils';
5
7
  import { AIStreamAttributePayloadType, AIStreamAttributeType, ConnectClientType, ConnectState, ReceivedTextPacketType, SessionState } from '../AIStreamTypes';
6
8
  import { DEFAULT_TOKEN_API, DEFAULT_TOKEN_API_VERSION, globalAIStreamClient } from '../global';
7
9
  import { Logger, safeParseJSON } from '@ray-js/t-agent';
8
10
  import logger from '../utils/logger';
11
+ import { tryCatch } from '../utils/misc';
12
+ export let AsrAgentStatus = /*#__PURE__*/function (AsrAgentStatus) {
13
+ AsrAgentStatus["PENDING"] = "pending";
14
+ AsrAgentStatus["RECORDING"] = "recording";
15
+ AsrAgentStatus["ASR"] = "asr";
16
+ AsrAgentStatus["ABORTING"] = "aborting";
17
+ return AsrAgentStatus;
18
+ }({});
9
19
  export class AsrAgent {
20
+ get status() {
21
+ return this._status;
22
+ }
23
+ set status(status) {
24
+ this._status = status;
25
+ const {
26
+ onStatusChanged
27
+ } = this.options || {};
28
+ if (typeof onStatusChanged === 'function') {
29
+ onStatusChanged(status);
30
+ }
31
+ }
10
32
  constructor(options) {
11
33
  /** 数据链接,一个agent保持一个链接就可以 */
12
34
  /** 当前请求的session */
13
35
  /** 音频流监听 */
14
36
  /** 录音时长定时器 */
15
37
  _defineProperty(this, "recordDurationTimer", null);
38
+ _defineProperty(this, "startPromise", null);
39
+ _defineProperty(this, "authorized", null);
40
+ _defineProperty(this, "_status", AsrAgentStatus.PENDING);
41
+ _defineProperty(this, "disposed", false);
16
42
  this.options = options;
17
43
  if (options.earlyStart) {
18
44
  const {
@@ -20,7 +46,7 @@ export class AsrAgent {
20
46
  } = options;
21
47
  const sampleRate = recordingOptions === null || recordingOptions === void 0 ? void 0 : recordingOptions.sampleRate;
22
48
  // 如果需要提前启动,直接初始化和创建连接
23
- this.createSession().then(() => initAudioRecorder(sampleRate ? {
49
+ this.createSession().then(session => session.ensureSession()).then(() => initAudioRecorder(sampleRate ? {
24
50
  sampleRate
25
51
  } : {})).catch(error => {
26
52
  logger.error('EarlyStart Failed to create ASR session:', error);
@@ -33,9 +59,15 @@ export class AsrAgent {
33
59
 
34
60
  /** 获取录音权限 */
35
61
  getRecordScope() {
62
+ if (this.authorized !== null) {
63
+ return Promise.resolve(this.authorized);
64
+ }
36
65
  return authorize({
37
66
  scope: 'scope.record'
38
- }).then(() => true, () => false);
67
+ }).then(() => true, () => false).then(authorized => {
68
+ this.authorized = authorized;
69
+ return authorized;
70
+ });
39
71
  }
40
72
 
41
73
  /** 获取数据链接,一般只有一个链接就可以 */
@@ -90,31 +122,60 @@ export class AsrAgent {
90
122
  clearTimeout(this.recordDurationTimer);
91
123
  }
92
124
  this.recordDurationTimer = setTimeout(() => {
93
- this.stop(true);
125
+ this._stop();
94
126
  }, maxDuration);
95
127
  }
96
128
  }
97
- async start() {
98
- const hasScope = await this.getRecordScope();
99
- if (!hasScope) {
100
- throw new Error('authorize failed');
101
- }
129
+ async _start() {
130
+ this.status = AsrAgentStatus.RECORDING;
131
+ let finished = false;
102
132
  const {
103
133
  onMessage,
104
134
  onFinish,
105
- onError
135
+ onError,
136
+ onAbort
106
137
  } = this.options || {};
138
+ const finish = error => {
139
+ if (this.disposed) {
140
+ return;
141
+ }
142
+ if (finished) {
143
+ return;
144
+ }
145
+ this.audioStream = null;
146
+ this.activeEvent = null;
147
+ finished = true;
148
+ const prevStatus = this.status;
149
+ this.status = AsrAgentStatus.PENDING;
150
+ if (prevStatus === AsrAgentStatus.ABORTING) {
151
+ typeof onAbort === 'function' && onAbort();
152
+ } else if (error) {
153
+ typeof onError === 'function' && onError(error);
154
+ } else {
155
+ typeof onFinish === 'function' && onFinish();
156
+ }
157
+ };
158
+ const hasScope = await this.getRecordScope();
159
+ if (!hasScope) {
160
+ finish(new Error('No record permission granted'));
161
+ return;
162
+ }
107
163
  const activeSession = await this.createSession();
108
- const activeEvent = await activeSession.startEvent({
164
+ const attribute = {
165
+ 'processing.interrupt': 'false',
166
+ 'asr.enableVad': 'false'
167
+ };
168
+ const [startEventError, activeEvent] = await tryCatch(() => activeSession.startEvent({
109
169
  userData: [{
110
170
  type: AIStreamAttributeType.AI_CHAT,
111
171
  payloadType: AIStreamAttributePayloadType.STRING,
112
- value: JSON.stringify({
113
- 'processing.interrupt': 'false',
114
- 'asr.enableVad': 'false'
115
- })
172
+ value: JSON.stringify(attribute)
116
173
  }]
117
- });
174
+ }));
175
+ if (startEventError) {
176
+ finish(startEventError);
177
+ return;
178
+ }
118
179
  this.activeEvent = activeEvent;
119
180
  const {
120
181
  recordingOptions
@@ -144,92 +205,109 @@ export class AsrAgent {
144
205
  });
145
206
  } else if (entry.type === 'connectionState') {
146
207
  if (entry.body.connectState === ConnectState.DISCONNECTED || entry.body.connectState === ConnectState.CLOSED) {
147
- this.stop();
148
- typeof onError === 'function' && onError(new Error('Connection closed'));
208
+ finish(new Error('Connection closed'));
149
209
  }
150
210
  } else if (entry.type === 'sessionState') {
151
211
  if (entry.body.sessionState === SessionState.CLOSED || entry.body.sessionState === SessionState.CREATE_FAILED) {
152
- this.stop();
153
- typeof onError === 'function' && onError(new Error('Session closed'));
212
+ finish(new Error('Session closed'));
154
213
  }
155
214
  }
156
215
  });
157
- let finished = false;
158
216
  activeEvent.on('finish', () => {
159
- if (finished) {
160
- return;
161
- }
162
- this.stop();
163
- typeof onFinish === 'function' && onFinish();
164
- finished = true;
217
+ finish();
165
218
  });
166
219
  activeEvent.on('close', () => {
167
- if (finished) {
168
- return;
169
- }
170
- this.stop();
171
- typeof onFinish === 'function' && onFinish();
172
- finished = true;
220
+ finish();
173
221
  });
174
222
  activeEvent.on('error', error => {
175
- typeof onError === 'function' && onError(error);
223
+ finish(error);
176
224
  });
177
- await audioStream.start();
225
+ const [error] = await tryCatch(() => audioStream.start());
226
+ if (error) {
227
+ finish(error);
228
+ return;
229
+ }
178
230
  this.startRecordTimer();
179
231
  }
180
- async stop(isAbort) {
232
+ async _abort() {
233
+ this.status = AsrAgentStatus.ABORTING;
234
+ if (this.recordDurationTimer) {
235
+ clearTimeout(this.recordDurationTimer);
236
+ this.recordDurationTimer = null;
237
+ }
238
+ if (this.startPromise) {
239
+ await this.startPromise;
240
+ }
241
+ this.activeEvent.abort();
242
+ }
243
+ async _stop() {
181
244
  if (this.recordDurationTimer) {
182
245
  clearTimeout(this.recordDurationTimer);
183
246
  this.recordDurationTimer = null;
184
247
  }
248
+ if (this.startPromise) {
249
+ await this.startPromise;
250
+ }
185
251
  if (this.audioStream) {
186
252
  const file = await this.audioStream.stop();
187
- console.log('Audio file result:', file);
188
- if (file !== null && file !== void 0 && file.path) {
253
+ if (file !== null && file !== void 0 && file.path && typeof this.options.onMessage === 'function') {
189
254
  this.options.onMessage({
190
255
  type: 'file',
191
256
  file
192
257
  });
193
258
  }
194
- this.audioStream = null;
195
259
  }
196
260
  if (this.activeEvent) {
197
- if (isAbort) {
198
- await this.activeEvent.abort();
199
- } else {
200
- await this.activeEvent.end();
201
- }
202
- this.activeEvent = null;
261
+ await this.activeEvent.end();
262
+ this.status = AsrAgentStatus.ASR;
203
263
  }
204
- if (isAbort) {
205
- const {
206
- onAbort
207
- } = this.options || {};
208
- typeof onAbort === 'function' && onAbort();
264
+ }
265
+ start() {
266
+ if (this.disposed) {
267
+ throw new Error('AsrAgent has been disposed');
268
+ }
269
+ if (this.status !== AsrAgentStatus.PENDING) {
270
+ throw new Error('AsrAgent is already started or in progress');
271
+ }
272
+ if (!this.startPromise) {
273
+ this.startPromise = this._start().finally(() => {
274
+ this.startPromise = null;
275
+ });
209
276
  }
277
+ return this.startPromise;
278
+ }
279
+ async stop() {
280
+ if (this.disposed) {
281
+ throw new Error('AsrAgent has been disposed');
282
+ }
283
+ if (this.status !== AsrAgentStatus.RECORDING) {
284
+ throw new Error('AsrAgent is not in recording state');
285
+ }
286
+ await this._stop();
210
287
  }
211
288
  async abort() {
212
- await this.stop(true);
289
+ if (this.disposed) {
290
+ throw new Error('AsrAgent has been disposed');
291
+ }
292
+ if (this.status !== AsrAgentStatus.RECORDING && this.status !== AsrAgentStatus.ASR) {
293
+ throw new Error('AsrAgent is not in recording or asr state');
294
+ }
295
+ return this._abort();
213
296
  }
214
297
  dispose() {
215
298
  if (this.recordDurationTimer) {
216
299
  clearTimeout(this.recordDurationTimer);
217
300
  this.recordDurationTimer = null;
218
301
  }
219
- if (this.audioStream && this.audioStream.started) {
220
- this.audioStream.stop();
221
- }
222
- this.audioStream = null;
223
- if (this.activeEvent) {
224
- this.activeEvent.abort();
225
- this.activeEvent = null;
302
+ if (this.disposed) {
303
+ return;
226
304
  }
227
305
  if (this.activeSession) {
228
306
  this.activeSession.close();
229
- this.activeSession = null;
230
- }
231
- if (this.streamConn) {
232
- this.streamConn = null;
233
307
  }
308
+ this.audioStream = null;
309
+ this.activeEvent = null;
310
+ this.streamConn = null;
311
+ this.activeSession = null;
234
312
  }
235
313
  }
@@ -1,3 +1,3 @@
1
- import { AsrAgent } from './AsrAgent';
1
+ import { AsrAgent, AsrAgentStatus } from './AsrAgent';
2
2
  import { createAsrAgent } from './createAsrAgent';
3
- export { AsrAgent, createAsrAgent };
3
+ export { AsrAgent, createAsrAgent, AsrAgentStatus };
package/dist/asr/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { AsrAgent } from './AsrAgent';
1
+ import { AsrAgent, AsrAgentStatus } from './AsrAgent';
2
2
  import { createAsrAgent } from './createAsrAgent';
3
- export { AsrAgent, createAsrAgent };
3
+ export { AsrAgent, createAsrAgent, AsrAgentStatus };
@@ -0,0 +1 @@
1
+ export declare function authorizeAssistantPolicy(): Promise<boolean>;
@@ -0,0 +1,51 @@
1
+ import { authorizePolicyStatus, listenReceiveMessage, openMiniWidget, registerChannel } from './ttt';
2
+ import logger from './logger';
3
+ import { AuthorizePolicySign } from '../AIStreamTypes';
4
+ export async function authorizeAssistantPolicy() {
5
+ // @ts-ignore
6
+ if (!ty.authorizePolicyStatus) {
7
+ logger.warn('authorizeAssistantPolicy: ty.authorizePolicyStatus not found, check if BaseKit >= 3.20.4');
8
+ // 不支持权限状态查询,直接返回true
9
+ return true;
10
+ }
11
+ const {
12
+ sign
13
+ } = await authorizePolicyStatus({
14
+ type: 'ai_algorithm'
15
+ });
16
+ if (sign === AuthorizePolicySign.AUTHORIZED) {
17
+ // 已授权,直接返回true
18
+ return true;
19
+ }
20
+
21
+ // 未授权,打开小程序授权
22
+ await registerChannel({
23
+ eventName: 'AIAuthorization'
24
+ });
25
+ const resultPromise = new Promise(resolve => {
26
+ // 一次性事件
27
+ const cancel = listenReceiveMessage(res => {
28
+ const {
29
+ status
30
+ } = res.event || {};
31
+ if (status) {
32
+ // 同意,直接进入
33
+ resolve(true);
34
+ } else {
35
+ resolve(false);
36
+ }
37
+ cancel();
38
+ });
39
+ });
40
+
41
+ // 打开小程序授权
42
+ await openMiniWidget({
43
+ appId: 'ty9mvmbhtvwrp5zbla',
44
+ pagePath: 'cards/index',
45
+ autoDismiss: false,
46
+ autoCache: false,
47
+ // versionType: ty.WidgetVersionType.preview,
48
+ style: '0.68'
49
+ });
50
+ return resultPromise;
51
+ }
@@ -4,6 +4,7 @@ import "core-js/modules/es.json.stringify.js";
4
4
  import "core-js/modules/es.regexp.exec.js";
5
5
  import "core-js/modules/esnext.iterator.constructor.js";
6
6
  import "core-js/modules/esnext.iterator.filter.js";
7
+ import "core-js/modules/esnext.iterator.for-each.js";
7
8
  import "core-js/modules/esnext.iterator.map.js";
8
9
  import "core-js/modules/web.dom-collections.iterator.js";
9
10
  import { mock } from './mock';
@@ -140,19 +141,19 @@ mock.hooks.hook('createSession', context => {
140
141
  };
141
142
 
142
143
  // 用于测试断开连接;
143
- // setTimeout(() => {
144
- // dispatch('onConnectStateChanged', {
145
- // connectionId: '',
146
- // connectState: ConnectState.DISCONNECTED,
147
- // code: 200,
148
- // });
149
- // mock.data.set('currentConnection', null);
150
- // const map: Map<string, MockSession> = mock.data.get('sessionMap');
151
- // map.forEach(s => {
152
- // s.closed = true;
153
- // });
154
- // mock.data.set('sessionMap', new Map());
155
- // }, 5000);
144
+ setTimeout(() => {
145
+ dispatch('onConnectStateChanged', {
146
+ connectionId: '',
147
+ connectState: ConnectState.DISCONNECTED,
148
+ code: 200
149
+ });
150
+ mock.data.set('currentConnection', null);
151
+ const map = mock.data.get('sessionMap');
152
+ map.forEach(s => {
153
+ s.closed = true;
154
+ });
155
+ mock.data.set('sessionMap', new Map());
156
+ }, 5000);
156
157
 
157
158
  // 用于测试断开会话
158
159
  // setTimeout(() => {
@@ -168,6 +169,8 @@ mock.hooks.hook('createSession', context => {
168
169
  mock.hooks.hook('closeSession', context => {
169
170
  const map = mock.data.get('sessionMap');
170
171
  const sessionId = context.options.sessionId;
172
+ const session = map.get(sessionId);
173
+ session.closed = true;
171
174
  map.delete(sessionId);
172
175
  context.result = true;
173
176
  });
@@ -273,7 +276,7 @@ mock.hooks.hook('sendEventEnd', async context => {
273
276
  if (ctx.data.length === 0) {
274
277
  event.replyEvent(EventType.EVENT_START);
275
278
  await mock.sleep(100);
276
- if (event.controller.signal.aborted) {
279
+ if (event.controller.signal.aborted || session.closed) {
277
280
  return;
278
281
  }
279
282
  event.replyEvent(EventType.EVENT_END);
@@ -282,19 +285,19 @@ mock.hooks.hook('sendEventEnd', async context => {
282
285
  }
283
286
  const text = ctx.responseText || '⚠️ No mock text response matched!';
284
287
  const words = splitString(text);
285
- if (event.controller.signal.aborted) {
288
+ if (event.controller.signal.aborted || session.closed) {
286
289
  return;
287
290
  }
288
291
  event.replyEvent(EventType.EVENT_START);
289
292
  await mock.sleep(500);
290
- if (event.controller.signal.aborted) {
293
+ if (event.controller.signal.aborted || session.closed) {
291
294
  return;
292
295
  }
293
296
  const bizId = generateId();
294
297
  event.replyText(StreamFlag.START);
295
298
  for (const word of words) {
296
299
  await mock.sleep(100);
297
- if (event.controller.signal.aborted) {
300
+ if (event.controller.signal.aborted || session.closed) {
298
301
  return;
299
302
  }
300
303
  event.replyText(StreamFlag.IN_PROGRESS, {
@@ -309,7 +312,7 @@ mock.hooks.hook('sendEventEnd', async context => {
309
312
  });
310
313
  }
311
314
  await mock.sleep(100);
312
- if (event.controller.signal.aborted) {
315
+ if (event.controller.signal.aborted || session.closed) {
313
316
  return;
314
317
  }
315
318
  event.replyText(StreamFlag.IN_PROGRESS, {
@@ -323,13 +326,13 @@ mock.hooks.hook('sendEventEnd', async context => {
323
326
  }
324
327
  });
325
328
  await mock.sleep(10);
326
- if (event.controller.signal.aborted) {
329
+ if (event.controller.signal.aborted || session.closed) {
327
330
  return;
328
331
  }
329
332
  if ((_ctx$responseSkills = ctx.responseSkills) !== null && _ctx$responseSkills !== void 0 && _ctx$responseSkills.length) {
330
333
  for (const skill of ctx.responseSkills) {
331
334
  await mock.sleep(100);
332
- if (event.controller.signal.aborted) {
335
+ if (event.controller.signal.aborted || session.closed) {
333
336
  return;
334
337
  }
335
338
  event.replyText(StreamFlag.IN_PROGRESS, {
@@ -342,7 +345,7 @@ mock.hooks.hook('sendEventEnd', async context => {
342
345
  }
343
346
  event.replyText(StreamFlag.END);
344
347
  await mock.sleep(500);
345
- if (event.controller.signal.aborted) {
348
+ if (event.controller.signal.aborted || session.closed) {
346
349
  return;
347
350
  }
348
351
  event.replyEvent(EventType.EVENT_END);
@@ -400,13 +403,13 @@ mock.hooks.hook('startRecordAndSendAudioData', async context => {
400
403
  let text = '';
401
404
  finishController.signal.addEventListener('abort', async () => {
402
405
  var _finishResolve;
403
- if (event.controller.signal.aborted) {
406
+ if (event.controller.signal.aborted || session.closed) {
404
407
  return;
405
408
  }
406
409
 
407
410
  // 终止识别到出完整结果的延迟
408
411
  await mock.sleep(500);
409
- if (event.controller.signal.aborted) {
412
+ if (event.controller.signal.aborted || session.closed) {
410
413
  return;
411
414
  }
412
415
  event.replyText(StreamFlag.IN_PROGRESS, {
@@ -418,7 +421,7 @@ mock.hooks.hook('startRecordAndSendAudioData', async context => {
418
421
  }
419
422
  });
420
423
  await mock.sleep(100);
421
- if (event.controller.signal.aborted) {
424
+ if (event.controller.signal.aborted || session.closed) {
422
425
  return;
423
426
  }
424
427
  event.replyText(StreamFlag.END);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/t-agent-plugin-aistream",
3
- "version": "0.2.3-beta-4",
3
+ "version": "0.2.4-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": "952d287025737224227ead3ecf6dd4c432fec6a2"
38
+ "gitHead": "83f32cc9add5ebd4c30b426b3593d51c1893b2eb"
39
39
  }