@ray-js/t-agent-plugin-aistream 0.2.0-beta-2 → 0.2.0-beta-4

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.
@@ -0,0 +1,197 @@
1
+ import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
2
+ import "core-js/modules/es.json.stringify.js";
3
+ import { authorize } from './ttt';
4
+ import { DEFAULT_TOKEN_API, DEFAULT_TOKEN_API_VERSION, globalAIStreamClient } from '../global';
5
+ import { ConnectClientType, AIStreamAttributePayloadType, AIStreamAttributeType, AIStreamChatSysWorkflow } from '../AIStreamTypes';
6
+ import logger from './logger';
7
+ class AsrAgent {
8
+ constructor(options) {
9
+ /** 数据链接,一个agent保持一个链接就可以 */
10
+ /** 当前请求的session,每次请求都需要是一个新的session */
11
+ /** 音频流监听 */
12
+ /** 录音时长定时器 */
13
+ _defineProperty(this, "recordDurationTimer", null);
14
+ this.options = options;
15
+ }
16
+
17
+ /** 获取录音权限 */
18
+ getRecordScope() {
19
+ return authorize({
20
+ scope: 'scope.record'
21
+ }).then(() => true, () => false);
22
+ }
23
+
24
+ /** 获取数据链接,一般只有一个链接就可以 */
25
+ getConnection(clientType, deviceId) {
26
+ if (this.streamConn) {
27
+ return this.streamConn;
28
+ }
29
+ // 创建 activeSession
30
+ this.streamConn = globalAIStreamClient.getConnection({
31
+ clientType: clientType || ConnectClientType.APP,
32
+ deviceId: deviceId
33
+ });
34
+ return this.streamConn;
35
+ }
36
+ createSession() {
37
+ var _this$activeSession;
38
+ // 每次新的请求,都需要重新创建一个Session
39
+ const {
40
+ clientType,
41
+ deviceId,
42
+ tokenApi,
43
+ tokenApiVersion,
44
+ agentId
45
+ } = this.options;
46
+ const streamConn = this.getConnection(clientType, deviceId);
47
+ // 如果有激活的,需要释放
48
+ if ((_this$activeSession = this.activeSession) !== null && _this$activeSession !== void 0 && _this$activeSession.sessionId) {
49
+ this.activeSession.close();
50
+ }
51
+ const activeSession = streamConn.createSession({
52
+ api: tokenApi || DEFAULT_TOKEN_API,
53
+ apiVersion: tokenApiVersion || DEFAULT_TOKEN_API_VERSION,
54
+ solutionCode: agentId
55
+ });
56
+ this.activeSession = activeSession;
57
+ return this.activeSession;
58
+ }
59
+
60
+ /** 开始录音时长监听 */
61
+ startRecordTimer() {
62
+ const {
63
+ maxDuration
64
+ } = this.options;
65
+ if (maxDuration) {
66
+ if (this.recordDurationTimer) {
67
+ clearTimeout(this.recordDurationTimer);
68
+ }
69
+ this.recordDurationTimer = setTimeout(() => {
70
+ this.stop(true);
71
+ }, maxDuration * 1000);
72
+ }
73
+ }
74
+ async start() {
75
+ const hasScope = await this.getRecordScope();
76
+ if (!hasScope) {
77
+ throw new Error('authorize failed');
78
+ }
79
+ const {
80
+ onMessage,
81
+ onFinish,
82
+ onError
83
+ } = this.options || {};
84
+ const activeSession = await this.createSession();
85
+ const attribute = {
86
+ 'processing.interrupt': 'false',
87
+ 'asr.enableVad': 'false',
88
+ 'sys.workflow': AIStreamChatSysWorkflow.ASR
89
+ };
90
+ const activeEvent = await activeSession.startEvent({
91
+ userData: [{
92
+ type: AIStreamAttributeType.AI_CHAT,
93
+ payloadType: AIStreamAttributePayloadType.STRING,
94
+ value: JSON.stringify(attribute)
95
+ }]
96
+ });
97
+ this.activeEvent = activeEvent;
98
+ const audioStream = activeEvent.stream({
99
+ type: 'audio'
100
+ });
101
+ this.audioStream = audioStream;
102
+ await audioStream.start();
103
+ this.startRecordTimer();
104
+ activeEvent.on('data', entry => {
105
+ if (entry.type === 'text') {
106
+ console.log('text', entry, JSON.parse(entry.body.text));
107
+ let data = {
108
+ text: ''
109
+ };
110
+ try {
111
+ data = JSON.parse(entry.body.text) || {};
112
+ } catch (error) {
113
+ logger.error('JSON.parse error', error);
114
+ }
115
+ typeof onMessage === 'function' && onMessage(data);
116
+ }
117
+ });
118
+ activeEvent.on('finish', () => {
119
+ this.stop();
120
+ typeof onFinish === 'function' && onFinish();
121
+ });
122
+ activeEvent.on('error', error => {
123
+ typeof onError === 'function' && onError(error);
124
+ });
125
+ }
126
+ async stop(isAbort) {
127
+ var _this$activeSession2;
128
+ if (this.recordDurationTimer) {
129
+ clearTimeout(this.recordDurationTimer);
130
+ this.recordDurationTimer = null;
131
+ }
132
+ if (this.audioStream) {
133
+ await this.audioStream.stop();
134
+ this.audioStream = null;
135
+ }
136
+ if (this.activeEvent) {
137
+ if (isAbort) {
138
+ await this.activeEvent.abort();
139
+ } else {
140
+ await this.activeEvent.end();
141
+ }
142
+ this.activeEvent = null;
143
+ }
144
+ if (isAbort) {
145
+ const {
146
+ onAbort
147
+ } = this.options || {};
148
+ typeof onAbort === 'function' && onAbort();
149
+ }
150
+ await ((_this$activeSession2 = this.activeSession) === null || _this$activeSession2 === void 0 ? void 0 : _this$activeSession2.close());
151
+ }
152
+ async abort() {
153
+ await this.stop(true);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 创建一个AsrAgent实例,用于语音转文本
159
+ * @param options AsrAgentOptions
160
+ * @param options.agentId 必填 语音转文本的agentId
161
+ * @param options.tokenApi 语音转文本的tokenApi
162
+ * @param options.tokenApiVersion 语音转文本的tokenApiVersion
163
+ * @param options.clientType 语音转文本的clientType
164
+ * @param options.deviceId 语音转文本的deviceId
165
+ * @example
166
+ * const asrAgent = createAsrAgent({
167
+ * agentId: 'asr-agent',
168
+ * tokenApi: 'm.thing.aigc.basic.server.token',
169
+ * tokenApiVersion: '1.0',
170
+ * clientType: ConnectClientType.APP,
171
+ * deviceId: 'deviceId',
172
+ * maxDuration: 60,
173
+ * onMessage: (message) => {
174
+ * console.log('onMessage', message);
175
+ * },
176
+ * onFinish: () => {
177
+ * console.log('onFinish');
178
+ * },
179
+ * onError: (error) => {
180
+ * console.log('onError', error);
181
+ * },
182
+ * onAbort: () => {
183
+ * console.log('onAbort');
184
+ * },
185
+ * });
186
+ * // 开始录音
187
+ * asrAgent.start()
188
+ * // 停止录音
189
+ * asrAgent.stop()
190
+ * // 中断录音
191
+ * asrAgent.abort()
192
+ * @returns
193
+ */
194
+ export function createAsrAgent(options) {
195
+ const asrAgent = new AsrAgent(options);
196
+ return asrAgent;
197
+ }
@@ -1,3 +1,4 @@
1
+ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
1
2
  import "core-js/modules/es.array.sort.js";
2
3
  import "core-js/modules/es.json.stringify.js";
3
4
  import "core-js/modules/es.regexp.exec.js";
@@ -34,19 +35,20 @@ mock.hooks.hook('getMiniAppConfig', context => {
34
35
  config: {}
35
36
  };
36
37
  });
38
+ const getCurrentConnection = () => {
39
+ return mock.data.get('currentConnection');
40
+ };
37
41
  mock.hooks.hook('connect', context => {
38
- const {
39
- options
40
- } = context;
41
- const key = "connection-".concat(options.clientType, "-").concat(options.deviceId || '');
42
- if (!mock.data.has(key)) {
43
- const connectionId = generateId();
44
- mock.data.set(key, {
45
- connectionId
46
- });
47
- mock.data.set("connection-".concat(connectionId), key);
42
+ let connection = getCurrentConnection();
43
+ if (connection) {
44
+ throw new Error('already connected');
45
+ } else {
46
+ connection = {
47
+ connectionId: generateId()
48
+ };
49
+ mock.data.set('currentConnection', connection);
48
50
  }
49
- context.result = mock.data.get(key);
51
+ context.result = connection;
50
52
  });
51
53
  mock.hooks.hook('disconnect', context => {
52
54
  const {
@@ -54,9 +56,15 @@ mock.hooks.hook('disconnect', context => {
54
56
  connectionId
55
57
  }
56
58
  } = context;
57
- const key = mock.data.get("connection-".concat(connectionId));
58
- mock.data.delete(key);
59
- mock.data.delete("connection-".concat(connectionId));
59
+ const connection = getCurrentConnection();
60
+ if (!connection) {
61
+ throw new Error('not connected');
62
+ }
63
+ if (connection.connectionId !== connectionId) {
64
+ throw new Error('connectionId mismatch');
65
+ }
66
+ mock.data.set('currentConnection', null);
67
+ mock.data.set('sessionMap', new Map());
60
68
  });
61
69
  mock.hooks.hook('queryAgentToken', context => {
62
70
  context.result = {
@@ -106,6 +114,14 @@ mock.hooks.hook('createSession', context => {
106
114
  sendDataChannels: session.sendDataChannels,
107
115
  revDataChannels: session.revDataChannels
108
116
  };
117
+
118
+ // setTimeout(() => {
119
+ // dispatch('onConnectStateChanged', {
120
+ // connectionId: getCurrentConnection().connectionId,
121
+ // connectState: ConnectState.DISCONNECTED,
122
+ // code: 200,
123
+ // });
124
+ // }, 1000);
109
125
  });
110
126
  mock.hooks.hook('closeSession', context => {
111
127
  const map = mock.data.get('sessionMap');
@@ -149,21 +165,31 @@ mock.hooks.hook('unregisterVoiceAmplitudes', context => {
149
165
  mock.hooks.hook('sendEventEnd', context => {
150
166
  const session = getSession(context.options.sessionId, context.options.eventId);
151
167
  context.result = true;
168
+ const event = session.currentEvent;
152
169
  (async () => {
170
+ await event.asrStatus.promise;
153
171
  const ctx = {
154
- data: session.currentEvent.data,
172
+ data: event.data,
155
173
  responseText: ''
156
174
  };
157
- await session.currentEvent.asrStatus.promise;
158
175
  await mock.hooks.callHook('sendToAIStream', ctx);
159
176
  const text = ctx.responseText || '⚠️ No mock text response matched!';
160
177
  const words = splitString(text);
178
+ if (event.controller.signal.aborted) {
179
+ return;
180
+ }
161
181
  session.replyEvent(EventType.EVENT_START);
162
182
  await mock.sleep(500);
183
+ if (event.controller.signal.aborted) {
184
+ return;
185
+ }
163
186
  const bizId = generateId();
164
187
  session.replyText(StreamFlag.START);
165
188
  for (const word of words) {
166
189
  await mock.sleep(100);
190
+ if (event.controller.signal.aborted) {
191
+ return;
192
+ }
167
193
  session.replyText(StreamFlag.IN_PROGRESS, {
168
194
  bizType: ReceivedTextPacketType.NLG,
169
195
  eof: ReceivedTextPacketEof.CONTINUE,
@@ -175,6 +201,10 @@ mock.hooks.hook('sendEventEnd', context => {
175
201
  }
176
202
  });
177
203
  }
204
+ await mock.sleep(100);
205
+ if (event.controller.signal.aborted) {
206
+ return;
207
+ }
178
208
  session.replyText(StreamFlag.IN_PROGRESS, {
179
209
  bizType: ReceivedTextPacketType.NLG,
180
210
  eof: ReceivedTextPacketEof.END,
@@ -186,8 +216,14 @@ mock.hooks.hook('sendEventEnd', context => {
186
216
  }
187
217
  });
188
218
  await mock.sleep(10);
219
+ if (event.controller.signal.aborted) {
220
+ return;
221
+ }
189
222
  session.replyText(StreamFlag.END);
190
223
  await mock.sleep(500);
224
+ if (event.controller.signal.aborted) {
225
+ return;
226
+ }
191
227
  session.replyEvent(EventType.EVENT_END);
192
228
  session.currentEvent = null;
193
229
  })();
@@ -238,6 +274,7 @@ mock.hooks.hook('startRecordAndSendAudioData', context => {
238
274
  let text = '';
239
275
  controller.signal.addEventListener('abort', async () => {
240
276
  var _finishResolve;
277
+ // 终止识别到出完整结果的延迟
241
278
  await mock.sleep(300);
242
279
  session.replyText(StreamFlag.IN_PROGRESS, {
243
280
  bizType: ReceivedTextPacketType.ASR,
@@ -249,8 +286,16 @@ mock.hooks.hook('startRecordAndSendAudioData', context => {
249
286
  });
250
287
  await mock.sleep(100);
251
288
  session.replyText(StreamFlag.END);
289
+ if (session.currentEvent) {
290
+ session.currentEvent.data.push({
291
+ type: 'text',
292
+ text
293
+ });
294
+ }
252
295
  (_finishResolve = finishResolve) === null || _finishResolve === void 0 || _finishResolve();
253
296
  });
297
+
298
+ // 识别 ASR 的延迟
254
299
  await mock.sleep(100);
255
300
  if (controller.signal.aborted) {
256
301
  return;
@@ -426,4 +471,24 @@ mock.hooks.hook('insertRecord', context => {
426
471
  context.result = {
427
472
  id
428
473
  };
474
+ });
475
+ mock.hooks.hook('updateRecord', context => {
476
+ const options = context.options;
477
+ const id = options.id;
478
+
479
+ // 默认倒序
480
+ const records = filterRecords({
481
+ id: [id]
482
+ });
483
+ if (!records.length) {
484
+ throw new Error('record not exists');
485
+ }
486
+ const newRecord = _objectSpread(_objectSpread({}, records[0]), options);
487
+ mock.setRecord({
488
+ id,
489
+ record: newRecord
490
+ });
491
+ context.result = {
492
+ newRecord
493
+ };
429
494
  });
@@ -9,3 +9,5 @@ export * from './AIStream';
9
9
  export * from './observer';
10
10
  export * from './sendMessage';
11
11
  export * from './version';
12
+ export * from './createAsrAgent';
13
+ export * from './actions';
@@ -8,4 +8,6 @@ export * from './url';
8
8
  export * from './AIStream';
9
9
  export * from './observer';
10
10
  export * from './sendMessage';
11
- export * from './version';
11
+ export * from './version';
12
+ export * from './createAsrAgent';
13
+ export * from './actions';
@@ -41,6 +41,7 @@ declare const mock: {
41
41
  getRecords: <T = any>() => Row<T>[];
42
42
  getRecord: <T_1 = any>(id: number) => Row<T_1> | undefined;
43
43
  setRecord: <T_2 = any>(entry: Row<T_2>) => void;
44
+ updateRecord: <T_3 = any>(entry: Row<T_3>) => void;
44
45
  deleteRecord: (id: number) => void;
45
46
  clearRecords: () => void;
46
47
  getId: () => number;
@@ -52,6 +52,14 @@ const mock = {
52
52
  data: JSON.stringify(mock.getRecords())
53
53
  });
54
54
  },
55
+ updateRecord: entry => {
56
+ mock.getRecords();
57
+ mock.database.set(entry.id, entry);
58
+ ty.setStorageSync({
59
+ key: 'AIStreamMockDatabase',
60
+ data: JSON.stringify(mock.getRecords())
61
+ });
62
+ },
55
63
  deleteRecord: id => {
56
64
  mock.getRecords();
57
65
  mock.database.delete(id);
@@ -11,6 +11,12 @@ interface AsyncTTTFnParams<P> {
11
11
  }) => void;
12
12
  [key: string]: any;
13
13
  }
14
+ export declare class TTTError extends Error {
15
+ message: string;
16
+ errorCode: string | number;
17
+ constructor(message: string, errorCode: string | number);
18
+ toString(): string;
19
+ }
14
20
  export declare const getEnableMock: () => boolean;
15
21
  export declare const setEnableMock: (enable: boolean) => boolean;
16
22
  export declare function promisify<T extends AsyncTTTFnParams<any>>(fn: (options: any) => void, enableMock?: boolean): (options?: Omit<T, 'success' | 'fail'>) => Promise<Parameters<NonNullable<T["success"]>>[0]>;
@@ -2,30 +2,40 @@ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
2
  import { isDevTools } from './ttt';
3
3
  import logger from './logger';
4
4
  import { mock } from './mock';
5
+ export class TTTError extends Error {
6
+ constructor(message, errorCode) {
7
+ super(message);
8
+ this.message = message;
9
+ this.errorCode = errorCode;
10
+ }
11
+ toString() {
12
+ return "TTTError: ".concat(this.message, ", errorCode: ").concat(this.errorCode);
13
+ }
14
+ }
5
15
  let callId = 100000;
6
16
  export const getEnableMock = () => {
7
17
  try {
8
18
  const result = ty.getStorageSync({
9
- key: 'AIAssistantEnableMock'
19
+ key: 'AIStreamEnableMock'
10
20
  });
11
21
  // @ts-ignore
12
22
  if (result && (result === 'true' || result.data === 'true')) {
13
23
  return true;
14
24
  }
15
25
  } catch (e) {
16
- console.error('获取AIAssistantEnableMock配置失败', e);
26
+ console.error('获取AIStreamEnableMock配置失败', e);
17
27
  }
18
28
  return isDevTools();
19
29
  };
20
30
  export const setEnableMock = enable => {
21
31
  try {
22
32
  ty.setStorageSync({
23
- key: 'AIAssistantEnableMock',
33
+ key: 'AIStreamEnableMock',
24
34
  data: String(enable)
25
35
  });
26
36
  return true;
27
37
  } catch (e) {
28
- console.error('设置AIAssistantEnableMock配置失败', e);
38
+ console.error('设置AIStreamEnableMock配置失败', e);
29
39
  return false;
30
40
  }
31
41
  };
@@ -44,6 +54,16 @@ export function promisify(fn) {
44
54
  };
45
55
  const reject = error => {
46
56
  logger.debug("TTT error #".concat(id, " %c").concat(fn.name), 'background: red; color: white', error);
57
+
58
+ // 这是 TTT 的错误
59
+ if (error.errorCode != null && error.errorMsg != null) {
60
+ var _error$innerError;
61
+ if (((_error$innerError = error.innerError) === null || _error$innerError === void 0 ? void 0 : _error$innerError.errorCode) != null && error.innerError.errorMsg != null) {
62
+ error = new TTTError(error.innerError.errorMsg, error.innerError.errorCode);
63
+ } else {
64
+ error = new TTTError(error.errorMsg, error.errorCode);
65
+ }
66
+ }
47
67
  _reject(error);
48
68
  };
49
69
  const context = {
package/dist/utils/ttt.js CHANGED
@@ -2,7 +2,7 @@ import { listening, promisify } from './promisify';
2
2
  import { ConnectClientType, ConnectState } from '../AIStreamTypes';
3
3
  import { generateId } from '@ray-js/t-agent';
4
4
  export const getMiniAppConfig = promisify(ty.getMiniAppConfig, true);
5
- export const getAccountInfo = promisify(ty.getAccountInfo, true);
5
+ export const getAccountInfo = promisify(ty.getAccountInfo);
6
6
  export const isDevTools = () => {
7
7
  return ty.getSystemInfoSync().brand === 'devtools';
8
8
  };
@@ -1,33 +1,33 @@
1
1
  import { ChatAgent, ChatMessage, ComposeHandler, InputBlock } from '@ray-js/t-agent';
2
2
  import { ConnectClientType } from './AIStreamTypes';
3
- import { ChatHistoryStore, StoredMessageObject } from './ChatHistoryStore';
3
+ import { ChatHistoryLocalStore, StoredMessageObject } from './ChatHistoryStore';
4
4
  export interface AIStreamOptions {
5
5
  /** client 类型: 1-作为设备代理, 2-作为 App */
6
6
  clientType?: ConnectClientType;
7
7
  /** 代理的设备ID, clientType == 1 时必传 */
8
8
  deviceId?: string;
9
+ /** Agent ID */
9
10
  agentId?: string;
10
- channel?: string;
11
- /** 打开小程序的链接,取链接参数字段 aiPtChannel */
12
- aiPtChannel?: string;
13
- sessionId?: string;
14
- tokenApi?: string;
15
- tokenApiVersion?: string;
11
+ /** 获取 agent token 的参数 */
12
+ tokenOptions?: {
13
+ api: string;
14
+ version: string;
15
+ extParams?: Record<string, any>;
16
+ };
16
17
  /** 历史消息数量,默认1000,为0的话,则已分页的方式取全部数据 */
17
18
  historySize?: number;
18
19
  /** 是否支持多模态 */
19
20
  multiModal?: boolean;
21
+ /** 是否将输入块传递给智能体,默认为 true,设置为 false 时,需要你自己编写 onInputBlocksPush Hook 来处理输入块 */
20
22
  wireInput?: boolean;
21
23
  /** 索引ID,如果传了 createChatHistoryStore,则会覆盖 */
22
24
  indexId?: string;
23
25
  /** 家庭id,不填默认当前家庭 */
24
26
  homeId?: number;
25
- /** 自定义消息存储, 返回的实例需要实现 ChatHistoryStore 接口 */
26
- createChatHistoryStore?: (agent: ChatAgent) => ChatHistoryStore;
27
- messageOptions?: {
28
- device_id?: string;
29
- [key: string]: string;
30
- };
27
+ /** 是否在 onAgentStart 阶段就建立连接 */
28
+ earlyStart?: boolean;
29
+ /** 自定义消息存储, 返回的实例需要实现 ChatHistoryLocalStore 接口, 返回null则不存储历史聊天记录 */
30
+ createChatHistoryStore?: (agent: ChatAgent) => ChatHistoryLocalStore | null;
31
31
  }
32
32
  export interface AIStreamHooks {
33
33
  onMessageParse: (msgItem: StoredMessageObject, result: {
@@ -39,7 +39,7 @@ export interface AIStreamHooks {
39
39
  }
40
40
  export declare function withAIStream(options?: AIStreamOptions): (agent: ChatAgent) => {
41
41
  hooks: import("hookable").Hookable<any, string>;
42
- assistant: {
42
+ aiStream: {
43
43
  send: (blocks: InputBlock[], signal?: AbortSignal, extraOptions?: Record<string, any>) => {
44
44
  response: import("@ray-js/t-agent").StreamResponse;
45
45
  metaPromise: Promise<Record<string, any>>;
@@ -53,12 +53,12 @@ export declare function withAIStream(options?: AIStreamOptions): (agent: ChatAge
53
53
  options: AIStreamOptions;
54
54
  removeMessage: (message: ChatMessage) => Promise<void>;
55
55
  clearAllMessages: () => Promise<void>;
56
- feedback: ({ requestId, type, }: {
56
+ feedback: ({ requestId, type }: {
57
57
  requestId: string;
58
- type: 'like' | 'unlike';
58
+ type: string;
59
59
  }) => Promise<{
60
60
  thingjson?: any;
61
61
  data: string;
62
- } | null>;
62
+ }>;
63
63
  };
64
64
  };